Skip to content

Commit 122d21d

Browse files
Release v0.3.8.
Harden npm install behavior by bundling and resolving OpenAPI artifacts across package-consumer contexts, enforce stdio-safe schema logging, and expand contract/integration coverage for install and output safety.
1 parent c40886f commit 122d21d

File tree

14 files changed

+150
-26
lines changed

14 files changed

+150
-26
lines changed

frontend/src/components/AppNavigationSidebar.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -199,6 +199,7 @@ export function AppNavigationSidebar({ siteName }: AppNavigationSidebarProps) {
199199
<SidebarMenuButton
200200
asChild
201201
isActive={activeSection === section.id}
202+
tooltip={section.shortLabel}
202203
className={sectionLinkClass(activeSection === section.id)}
203204
>
204205
<a href={`#${section.id}`} onClick={onSectionClick}>

package-lock.json

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "neotoma",
3-
"version": "0.3.7",
3+
"version": "0.3.8",
44
"description": "MCP server for structured personal data memory with unified source ingestion",
55
"main": "dist/index.js",
66
"type": "module",
@@ -134,6 +134,7 @@
134134
},
135135
"files": [
136136
"dist",
137+
"openapi.yaml",
137138
"LICENSE",
138139
"README.md"
139140
],

scripts/simulate_npm_install.js

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
* 2. Runs npm pack to create the tarball that would be published
88
* 3. Installs that tarball in a temporary directory (as a consumer would)
99
* 4. Runs the installed binary (npx neotoma --help) to verify it works
10+
* 5. Verifies OpenAPI schema can be resolved from installed package (cwd-independent)
1011
* 5. Cleans up
1112
*
1213
* Run before `npm publish` to catch pack/list/postinstall issues.
@@ -81,6 +82,12 @@ function main() {
8182
console.log("\nStep 4: Verifying installed binary...");
8283
run("npx neotoma --help", { cwd: tmpDir });
8384

85+
console.log("\nStep 5: Verifying OpenAPI schema resolution from installed package...");
86+
run(
87+
`node --input-type=module -e "import { resolveOpenApiPath } from 'neotoma/dist/shared/openapi_file.js'; const p = resolveOpenApiPath(); if (!p.endsWith('openapi.yaml')) { throw new Error('Unexpected OpenAPI path: ' + p); } console.log(p);"`,
88+
{ cwd: tmpDir }
89+
);
90+
8491
console.log("\nSimulate install passed. Package would install and run correctly.");
8592
} finally {
8693
if (tmpDir && fs.existsSync(tmpDir)) {

src/actions.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,7 @@ import {
6565
prepareEntitySnapshotWithEmbedding,
6666
upsertEntitySnapshotWithEmbedding,
6767
} from "./services/entity_snapshot_embedding.js";
68+
import { readOpenApiFile } from "./shared/openapi_file.js";
6869
// import { setupDocumentationRoutes } from "./routes/documentation.js";
6970

7071
type ErrorEnvelope = {
@@ -4390,8 +4391,7 @@ app.post("/health_check_snapshots", async (req, res) => {
43904391
// Conversational interactions should be externalized to MCP-compatible agents per architecture
43914392

43924393
app.get("/openapi.yaml", (req, res) => {
4393-
const openApiPath = path.join(process.cwd(), "openapi.yaml");
4394-
const openApiContent = fs.readFileSync(openApiPath, "utf-8");
4394+
const openApiContent = readOpenApiFile();
43954395
const spec = yaml.load(openApiContent) as { servers?: Array<{ url: string; description?: string }> };
43964396
const baseUrl = (config.apiBase || "").replace(/\/$/, "");
43974397
if (spec.servers?.length && baseUrl) {

src/cli/index.ts

Lines changed: 23 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -11559,19 +11559,38 @@ export type InitContextStatus = {
1155911559
export async function getInitContextStatus(
1156011560
repoRoot: string | null
1156111561
): Promise<InitContextStatus | null> {
11562-
const envTarget: "project" | "user" = repoRoot ? "project" : "user";
11563-
const envPath = repoRoot ? path.join(repoRoot, ".env") : USER_ENV_PATH;
11562+
let effectiveRepoRoot: string | null = repoRoot;
11563+
let envTarget: "project" | "user" = effectiveRepoRoot ? "project" : "user";
11564+
let envPath = effectiveRepoRoot ? path.join(effectiveRepoRoot, ".env") : USER_ENV_PATH;
11565+
11566+
// In package-consumer repos (not source checkouts), `init` can target cwd when
11567+
// neotoma is installed as a dependency. Mirror that resolution here so plain
11568+
// `npx neotoma` recognizes prior init in the same project.
11569+
if (!effectiveRepoRoot) {
11570+
const cwd = process.cwd();
11571+
if (await detectInstalledNeotomaPackage(cwd)) {
11572+
const packageEnvPath = path.join(cwd, ".env");
11573+
if (await pathExists(packageEnvPath)) {
11574+
effectiveRepoRoot = cwd;
11575+
envTarget = "project";
11576+
envPath = packageEnvPath;
11577+
}
11578+
}
11579+
}
11580+
1156411581
const envFileExists = await pathExists(envPath);
1156511582
const homeDir = process.env.HOME || process.env.USERPROFILE || ".";
11566-
const defaultDataDir = repoRoot ? path.join(repoRoot, "data") : path.join(homeDir, "neotoma", "data");
11583+
const defaultDataDir = effectiveRepoRoot
11584+
? path.join(effectiveRepoRoot, "data")
11585+
: path.join(homeDir, "neotoma", "data");
1156711586
let dataDir: string;
1156811587
if (envFileExists) {
1156911588
const envVars = await readEnvFileVars(envPath);
1157011589
const configured = envVars.NEOTOMA_DATA_DIR?.trim();
1157111590
dataDir = configured
1157211591
? path.isAbsolute(configured)
1157311592
? configured
11574-
: path.resolve(repoRoot ?? process.cwd(), configured)
11593+
: path.resolve(effectiveRepoRoot ?? process.cwd(), configured)
1157511594
: defaultDataDir;
1157611595
} else {
1157711596
dataDir = defaultDataDir;

src/services/schema_registry.ts

Lines changed: 11 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,10 @@ import { dirname, join } from "path";
1515

1616
const __dirname = dirname(fileURLToPath(import.meta.url));
1717

18+
function logSchemaRegistryInfo(message: string): void {
19+
process.stderr.write(`${message}\n`);
20+
}
21+
1822
export interface ConverterDefinition {
1923
from: "number" | "string" | "boolean" | "array" | "object";
2024
to: "string" | "number" | "date" | "boolean" | "array" | "object";
@@ -398,7 +402,7 @@ export class SchemaRegistryService {
398402
for (const field of options.fields_to_add || []) {
399403
// Skip if field already exists
400404
if (mergedFields[field.field_name]) {
401-
console.log(
405+
logSchemaRegistryInfo(
402406
`[SCHEMA_REGISTRY] Field ${field.field_name} already exists in schema, skipping`,
403407
);
404408
continue;
@@ -447,13 +451,13 @@ export class SchemaRegistryService {
447451
await this.activate(options.entity_type, newVersion);
448452
}
449453

450-
console.log(
454+
logSchemaRegistryInfo(
451455
`[SCHEMA_REGISTRY] Incrementally updated schema for ${options.entity_type} to version ${newVersion}`,
452456
);
453457

454458
// 7. Migrate raw_fragments if requested (historical data backfill only)
455459
if (options.migrate_existing) {
456-
console.log(
460+
logSchemaRegistryInfo(
457461
`[SCHEMA_REGISTRY] Migrating existing raw_fragments for ${options.entity_type}`,
458462
);
459463
const fieldNamesToMigrate = [
@@ -485,7 +489,7 @@ export class SchemaRegistryService {
485489
const BATCH_SIZE = 100; // Smaller batch size for safety
486490
let totalMigrated = 0;
487491

488-
console.log(
492+
logSchemaRegistryInfo(
489493
`[SCHEMA_REGISTRY] Starting migration for fields: ${options.field_names.join(", ")}`,
490494
);
491495

@@ -525,7 +529,7 @@ export class SchemaRegistryService {
525529
continue; // No more fragments to migrate
526530
}
527531

528-
console.log(
532+
logSchemaRegistryInfo(
529533
`[SCHEMA_REGISTRY] Processing batch of ${fragments.length} fragments for field ${fieldName}`,
530534
);
531535

@@ -638,7 +642,7 @@ export class SchemaRegistryService {
638642
}
639643
} else {
640644
totalMigrated += Object.keys(promotedFields).length;
641-
console.log(
645+
logSchemaRegistryInfo(
642646
`[SCHEMA_REGISTRY] Migrated ${Object.keys(promotedFields).length} fields for entity ${entityId}`,
643647
);
644648

@@ -706,7 +710,7 @@ export class SchemaRegistryService {
706710
}
707711
}
708712

709-
console.log(
713+
logSchemaRegistryInfo(
710714
`[SCHEMA_REGISTRY] Migration complete. Total fragments processed: ${totalMigrated}`,
711715
);
712716

src/shared/openapi_file.ts

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
import { existsSync, readFileSync } from "node:fs";
2+
import { dirname, join, resolve } from "node:path";
3+
import { fileURLToPath } from "node:url";
4+
5+
const OPENAPI_FILENAME = "openapi.yaml";
6+
7+
function getPackageRootFromModule(): string {
8+
const moduleDir = dirname(fileURLToPath(import.meta.url));
9+
return resolve(moduleDir, "..", "..");
10+
}
11+
12+
export function resolveOpenApiPath(): string {
13+
const fromEnv = process.env.NEOTOMA_OPENAPI_PATH?.trim();
14+
const candidates = [
15+
fromEnv || "",
16+
join(getPackageRootFromModule(), OPENAPI_FILENAME),
17+
join(process.cwd(), OPENAPI_FILENAME),
18+
].filter(Boolean);
19+
20+
for (const candidate of candidates) {
21+
if (existsSync(candidate)) {
22+
return candidate;
23+
}
24+
}
25+
26+
throw new Error(
27+
`OpenAPI schema not found. Tried: ${candidates.join(", ")}`
28+
);
29+
}
30+
31+
export function readOpenApiFile(): string {
32+
return readFileSync(resolveOpenApiPath(), "utf-8");
33+
}

src/shared/openapi_schema.ts

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
1-
import { readFileSync } from "node:fs";
2-
import { join } from "node:path";
31
import yaml from "js-yaml";
42
import { MCP_TOOL_TO_OPERATION_ID, OPENAPI_OPERATION_MAPPINGS } from "./contract_mappings.js";
3+
import { readOpenApiFile } from "./openapi_file.js";
54

65
type OpenApiSchema = Record<string, unknown>;
76

@@ -33,8 +32,7 @@ function loadOpenApiSpec(): OpenApiSpec {
3332
if (cachedSpec) {
3433
return cachedSpec;
3534
}
36-
const openApiPath = join(process.cwd(), "openapi.yaml");
37-
const raw = readFileSync(openApiPath, "utf-8");
35+
const raw = readOpenApiFile();
3836
cachedSpec = yaml.load(raw) as OpenApiSpec;
3937
return cachedSpec;
4038
}
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
import { describe, expect, it } from "vitest";
2+
import { readFileSync } from "node:fs";
3+
import { resolve } from "node:path";
4+
5+
describe("MCP stdio output safety", () => {
6+
it("avoids console.log in schema registry to prevent stdout protocol noise", () => {
7+
const schemaRegistryPath = resolve(process.cwd(), "src/services/schema_registry.ts");
8+
const source = readFileSync(schemaRegistryPath, "utf8");
9+
expect(source.includes("console.log(")).toBe(false);
10+
});
11+
});

0 commit comments

Comments
 (0)