@open-mercato/ai-assistant 0.6.6-develop.6246.1.85fd30c705 → 0.6.6-develop.6255.1.a6ee4a57c1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +9 -0
- package/dist/modules/ai_assistant/lib/generated-registry-loader.js +8 -2
- package/dist/modules/ai_assistant/lib/generated-registry-loader.js.map +2 -2
- package/package.json +6 -6
- package/src/modules/ai_assistant/lib/__tests__/generated-registry-loader.test.ts +47 -0
- package/src/modules/ai_assistant/lib/generated-registry-loader.ts +39 -11
package/AGENTS.md
CHANGED
|
@@ -1464,6 +1464,15 @@ Agents that need multi-step tool loops configure the `loop` block on `AiAgentDef
|
|
|
1464
1464
|
|
|
1465
1465
|
## Changelog
|
|
1466
1466
|
|
|
1467
|
+
### 2026-06-24 - MCP dev server loads ai-tools for @app local modules (#3524)
|
|
1468
|
+
|
|
1469
|
+
**What changed** (`lib/generated-registry-loader.ts`):
|
|
1470
|
+
- `rewriteGeneratedAliasImports` now also rewrites the `../../src/...` relative imports the generator emits for `@app` local modules (e.g. `from "../../src/modules/<id>/ai-tools"`), resolving them against the generated file's directory (`<appRoot>/.mercato/generated`) to absolute `file://` URLs with the same `.ts`-suffix probe used for `@/` aliases. Previously only `@/` aliases were rewritten, so the `../../src/...` specifier passed through `esbuild.transform` (transpile-only) into the compiled `.mjs` and Node ESM threw `ERR_MODULE_NOT_FOUND` resolving the extensionless `.ts`-only target. Package-backed modules (`@open-mercato/*`) were never affected — their bare specifiers resolve through `node_modules` to compiled `.js`.
|
|
1471
|
+
|
|
1472
|
+
**Files**: `lib/generated-registry-loader.ts`. Regression test: `lib/__tests__/generated-registry-loader.test.ts` (3 cases covering static import, dynamic `import()`, and `.ts`-suffix resolution for the `../../src/...` shape).
|
|
1473
|
+
|
|
1474
|
+
**Backward compatibility**: Strictly additive — the `rewriteGeneratedAliasImports(source, appRoot)` signature is unchanged, and `@/` aliases, bare package imports, and sibling `./` imports keep their prior behavior. Only previously-broken `../../src/...` specifiers change (from unresolved to resolved).
|
|
1475
|
+
|
|
1467
1476
|
### 2026-06-11 - Harden latent MCP server-config module (#2672)
|
|
1468
1477
|
|
|
1469
1478
|
**What changed** (`lib/mcp-server-config.ts`, currently dead code — no callers):
|
|
@@ -68,15 +68,21 @@ function toSafeJsStringLiteral(value) {
|
|
|
68
68
|
return escapeUnsafeJsStringChars(JSON.stringify(value));
|
|
69
69
|
}
|
|
70
70
|
function rewriteGeneratedAliasImports(source, appRoot) {
|
|
71
|
-
const
|
|
72
|
-
|
|
71
|
+
const generatedDir = path.join(appRoot, ".mercato", "generated");
|
|
72
|
+
const toResolvedLiteral = (target) => {
|
|
73
73
|
const candidate = fs.existsSync(target) ? target : fs.existsSync(target + ".ts") ? target + ".ts" : target;
|
|
74
74
|
return toSafeJsStringLiteral(pathToFileURL(candidate).href);
|
|
75
75
|
};
|
|
76
|
+
const resolveAlias = (relativePath) => toResolvedLiteral(path.join(appRoot, relativePath));
|
|
77
|
+
const resolveRelative = (specifier) => toResolvedLiteral(path.resolve(generatedDir, specifier));
|
|
76
78
|
return source.replace(/from\s+["']@\/([^"']+)["']/g, (_match, relativePath) => {
|
|
77
79
|
return `from ${resolveAlias(relativePath)}`;
|
|
78
80
|
}).replace(/import\s*\(\s*["']@\/([^"']+)["']\s*\)/g, (_match, relativePath) => {
|
|
79
81
|
return `import(${resolveAlias(relativePath)})`;
|
|
82
|
+
}).replace(/from\s+["']((?:\.\.\/)+src\/[^"']+)["']/g, (_match, specifier) => {
|
|
83
|
+
return `from ${resolveRelative(specifier)}`;
|
|
84
|
+
}).replace(/import\s*\(\s*["']((?:\.\.\/)+src\/[^"']+)["']\s*\)/g, (_match, specifier) => {
|
|
85
|
+
return `import(${resolveRelative(specifier)})`;
|
|
80
86
|
});
|
|
81
87
|
}
|
|
82
88
|
async function ensureApiRouteManifestsRegistered() {
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../../src/modules/ai_assistant/lib/generated-registry-loader.ts"],
|
|
4
|
-
"sourcesContent": ["/**\n * Runtime loader for `.mercato/generated/*.generated.ts` registry files.\n *\n * The generated registries import their entries through the `@/` path alias\n * (e.g. `@/.mercato/generated/ai-tools.generated`). That alias is only\n * understood by the Next.js bundler \u2014 in a standalone Node process (the\n * `mcp:dev` / `mcp:serve` MCP servers, the CLI tool-test runner) a raw\n * `import('@/.mercato/...')` throws `ERR_MODULE_NOT_FOUND: Cannot find\n * package '@/.mercato'` because Node treats `@/` as a package specifier.\n *\n * These helpers locate the generated `.ts` file on disk and compile-and-import\n * it with esbuild (transpile-only), rewriting `@/` aliases to absolute paths.\n * This mirrors `loadBootstrapData` in\n * `@open-mercato/shared/lib/bootstrap/dynamicLoader` and works in both the\n * monorepo and standalone apps.\n */\nimport path from 'node:path'\nimport fs from 'node:fs'\nimport { fileURLToPath, pathToFileURL } from 'node:url'\n\n/**\n * Locate a generated registry file (e.g. `ai-tools.generated.ts`) without\n * hardcoding the workspace layout. Searches upward from this module's compiled\n * location for a `apps/mercato/.mercato/generated/<fileName>` (monorepo), then\n * falls back to cwd-relative lookups (standalone apps run from the app dir).\n */\nexport function findGeneratedFile(fileName: string): string | null {\n const here = (() => {\n try {\n return fileURLToPath(import.meta.url)\n } catch {\n return null\n }\n })()\n\n if (here) {\n let cursor = path.dirname(here)\n for (let i = 0; i < 12; i++) {\n const candidate = path.join(cursor, 'apps', 'mercato', '.mercato', 'generated', fileName)\n if (fs.existsSync(candidate)) return candidate\n const next = path.dirname(cursor)\n if (next === cursor) break\n cursor = next\n }\n }\n // Fallbacks: cwd-based lookup (CLI invoked from apps/mercato, or a standalone\n // app whose root holds `.mercato/generated`).\n const fromCwd = path.resolve(process.cwd(), 'apps', 'mercato', '.mercato', 'generated', fileName)\n if (fs.existsSync(fromCwd)) return fromCwd\n const fromCwdDirect = path.resolve(process.cwd(), '.mercato', 'generated', fileName)\n if (fs.existsSync(fromCwdDirect)) return fromCwdDirect\n return null\n}\n\n/**\n * Compile-and-import a generated registry file on the fly. Resolves the `@/`\n * alias to the app root, transpiles TS \u2192 ESM, and emits a sibling `.mjs` we can\n * `import()` from Node. Cached on mtime so repeat calls in the same process\n * don't recompile.\n *\n * Transpile-only (no bundling): the generated registries declare an array\n * literal whose entries are static `import(\"\u2026\")` arrow functions \u2014 we want\n * those `import()` strings to stay as runtime imports so Node resolves them\n * lazily through the workspace's normal module resolution. Eagerly bundling\n * pulls Next.js / route-handler internals into the `.mjs` and breaks at runtime\n * (e.g. `next/server` package-exports map).\n */\nexport async function compileAndImportGenerated(tsPath: string): Promise<Record<string, unknown>> {\n const jsPath = tsPath.replace(/\\.ts$/, '.mjs')\n // appRoot is two directories up from `.mercato/generated/<file>.ts`.\n const appRoot = path.dirname(path.dirname(path.dirname(tsPath)))\n\n if (!fs.existsSync(tsPath)) {\n throw new Error(`Generated file not found: ${tsPath}`)\n }\n\n const jsExists = fs.existsSync(jsPath)\n const needsCompile =\n !jsExists || fs.statSync(tsPath).mtimeMs > fs.statSync(jsPath).mtimeMs\n\n if (needsCompile) {\n const esbuild = await import('esbuild')\n const tsSource = fs.readFileSync(tsPath, 'utf-8')\n const aliasRewritten = rewriteGeneratedAliasImports(tsSource, appRoot)\n const result = await esbuild.transform(aliasRewritten, {\n loader: 'ts',\n format: 'esm',\n target: 'node18',\n sourcemap: false,\n sourcefile: tsPath,\n })\n fs.writeFileSync(jsPath, result.code)\n }\n\n return (await import(pathToFileURL(jsPath).href)) as Record<string, unknown>\n}\n\nconst UNSAFE_JS_STRING_CHAR_ESCAPES: Record<number, string> = {\n 0x3c: '\\\\u003C', // < \u2014 HTML/script-tag breakout\n 0x3e: '\\\\u003E', // > \u2014 HTML/script-tag breakout\n 0x2028: '\\\\u2028', // line separator \u2014 string content but a statement terminator pre-ES2019\n 0x2029: '\\\\u2029', // paragraph separator \u2014 same\n}\n\n/**\n * Escape characters that `JSON.stringify` leaves intact but which can still\n * break out of (or alter the meaning of) the JavaScript string literal that the\n * stringified value is embedded into \u2014 notably `<`/`>` (HTML/script-tag\n * breakout) and the U+2028 / U+2029 line separators (valid string content but\n * statement terminators in pre-ES2019 parsers). Apply this on top of\n * `JSON.stringify` so the emitted import source stays well-formed regardless of\n * the resolved path. Exported for unit testing.\n */\nexport function escapeUnsafeJsStringChars(value: string): string {\n return value.replace(\n /[<>\\u2028\\u2029]/g,\n (char) => UNSAFE_JS_STRING_CHAR_ESCAPES[char.charCodeAt(0)],\n )\n}\n\n/**\n * Stringify a resolved path into a JavaScript string literal that is safe to\n * embed in generated source: `JSON.stringify` handles quoting/standard escapes,\n * and `escapeUnsafeJsStringChars` neutralizes the characters it leaves intact.\n */\nfunction toSafeJsStringLiteral(value: string): string {\n return escapeUnsafeJsStringChars(JSON.stringify(value))\n}\n\n/**\n * Rewrite `@/...` path-alias imports (both `from \"@/x\"` and dynamic\n * `import(\"@/x\")`) in generated-registry source to absolute `file://` URLs\n * rooted at `appRoot`. The `@/` alias is a Next.js bundler convention; outside\n * the bundler Node treats `@/...` as a bare package specifier and throws\n * `ERR_MODULE_NOT_FOUND`. Exported for unit testing.\n */\nexport function rewriteGeneratedAliasImports(source: string, appRoot: string): string {\n const resolveAlias = (relativePath: string): string => {\n const target = path.join(appRoot, relativePath)\n const candidate = fs.existsSync(target)\n ? target\n : fs.existsSync(target + '.ts')\n ? target + '.ts'\n : target\n return toSafeJsStringLiteral(pathToFileURL(candidate).href)\n }\n return source\n .replace(/from\\s+[\"']@\\/([^\"']+)[\"']/g, (_match, relativePath: string) => {\n return `from ${resolveAlias(relativePath)}`\n })\n .replace(/import\\s*\\(\\s*[\"']@\\/([^\"']+)[\"']\\s*\\)/g, (_match, relativePath: string) => {\n return `import(${resolveAlias(relativePath)})`\n })\n}\n\n/**\n * Compile-and-import `api-routes.generated.ts` and register its manifest with\n * the shared registry. Many module tools are \"API-backed\" \u2014 their handlers\n * delegate to `createAiApiOperationRunner`, which fails closed with\n * \"No API route manifest registered\" unless the manifest is present. In the\n * Next.js app this is wired at bootstrap, but the standalone MCP servers\n * (`mcp:dev` / `mcp:serve`) bootstrap DI without it, so we register it here.\n *\n * Idempotent: `registerApiRouteManifests` replaces the stored manifest, so\n * calling this repeatedly (e.g. per-request HTTP handlers) is safe. Returns the\n * number of registered routes (0 when the generated file is absent).\n */\nexport async function ensureApiRouteManifestsRegistered(): Promise<number> {\n const registry = await import('@open-mercato/shared/modules/registry')\n // Already wired (e.g. the Next.js app bootstrap, or a prior call). Leave the\n // existing manifest untouched so we never interfere with the in-app agents\n // framework, which registers it at bootstrap with its own override pipeline.\n const existing = registry.getApiRouteManifests()\n if (existing.length > 0) return existing.length\n\n const tsPath = findGeneratedFile('api-routes.generated.ts')\n if (!tsPath) return 0\n try {\n const mod = await compileAndImportGenerated(tsPath)\n const apiRoutes = (mod as { apiRoutes?: unknown }).apiRoutes\n if (!Array.isArray(apiRoutes)) return 0\n registry.registerApiRouteManifests(\n apiRoutes as Parameters<typeof registry.registerApiRouteManifests>[0],\n )\n return apiRoutes.length\n } catch (error) {\n console.warn(\n '[MCP Tools] Could not register api-routes manifest:',\n error instanceof Error ? error.message : error,\n )\n return 0\n }\n}\n"],
|
|
5
|
-
"mappings": "AAgBA,OAAO,UAAU;AACjB,OAAO,QAAQ;AACf,SAAS,eAAe,qBAAqB;AAQtC,SAAS,kBAAkB,UAAiC;AACjE,QAAM,QAAQ,MAAM;AAClB,QAAI;AACF,aAAO,cAAc,YAAY,GAAG;AAAA,IACtC,QAAQ;AACN,aAAO;AAAA,IACT;AAAA,EACF,GAAG;AAEH,MAAI,MAAM;AACR,QAAI,SAAS,KAAK,QAAQ,IAAI;AAC9B,aAAS,IAAI,GAAG,IAAI,IAAI,KAAK;AAC3B,YAAM,YAAY,KAAK,KAAK,QAAQ,QAAQ,WAAW,YAAY,aAAa,QAAQ;AACxF,UAAI,GAAG,WAAW,SAAS,EAAG,QAAO;AACrC,YAAM,OAAO,KAAK,QAAQ,MAAM;AAChC,UAAI,SAAS,OAAQ;AACrB,eAAS;AAAA,IACX;AAAA,EACF;AAGA,QAAM,UAAU,KAAK,QAAQ,QAAQ,IAAI,GAAG,QAAQ,WAAW,YAAY,aAAa,QAAQ;AAChG,MAAI,GAAG,WAAW,OAAO,EAAG,QAAO;AACnC,QAAM,gBAAgB,KAAK,QAAQ,QAAQ,IAAI,GAAG,YAAY,aAAa,QAAQ;AACnF,MAAI,GAAG,WAAW,aAAa,EAAG,QAAO;AACzC,SAAO;AACT;
|
|
4
|
+
"sourcesContent": ["/**\n * Runtime loader for `.mercato/generated/*.generated.ts` registry files.\n *\n * The generated registries import their entries through the `@/` path alias\n * (e.g. `@/.mercato/generated/ai-tools.generated`). That alias is only\n * understood by the Next.js bundler \u2014 in a standalone Node process (the\n * `mcp:dev` / `mcp:serve` MCP servers, the CLI tool-test runner) a raw\n * `import('@/.mercato/...')` throws `ERR_MODULE_NOT_FOUND: Cannot find\n * package '@/.mercato'` because Node treats `@/` as a package specifier.\n *\n * These helpers locate the generated `.ts` file on disk and compile-and-import\n * it with esbuild (transpile-only), rewriting `@/` aliases to absolute paths.\n * This mirrors `loadBootstrapData` in\n * `@open-mercato/shared/lib/bootstrap/dynamicLoader` and works in both the\n * monorepo and standalone apps.\n */\nimport path from 'node:path'\nimport fs from 'node:fs'\nimport { fileURLToPath, pathToFileURL } from 'node:url'\n\n/**\n * Locate a generated registry file (e.g. `ai-tools.generated.ts`) without\n * hardcoding the workspace layout. Searches upward from this module's compiled\n * location for a `apps/mercato/.mercato/generated/<fileName>` (monorepo), then\n * falls back to cwd-relative lookups (standalone apps run from the app dir).\n */\nexport function findGeneratedFile(fileName: string): string | null {\n const here = (() => {\n try {\n return fileURLToPath(import.meta.url)\n } catch {\n return null\n }\n })()\n\n if (here) {\n let cursor = path.dirname(here)\n for (let i = 0; i < 12; i++) {\n const candidate = path.join(cursor, 'apps', 'mercato', '.mercato', 'generated', fileName)\n if (fs.existsSync(candidate)) return candidate\n const next = path.dirname(cursor)\n if (next === cursor) break\n cursor = next\n }\n }\n // Fallbacks: cwd-based lookup (CLI invoked from apps/mercato, or a standalone\n // app whose root holds `.mercato/generated`).\n const fromCwd = path.resolve(process.cwd(), 'apps', 'mercato', '.mercato', 'generated', fileName)\n if (fs.existsSync(fromCwd)) return fromCwd\n const fromCwdDirect = path.resolve(process.cwd(), '.mercato', 'generated', fileName)\n if (fs.existsSync(fromCwdDirect)) return fromCwdDirect\n return null\n}\n\n/**\n * Compile-and-import a generated registry file on the fly. Rewrites the entry\n * specifiers Node can't resolve standalone (`@/...` aliases and the\n * `../../src/...` relative imports the generator emits for `@app` local\n * modules) to absolute file URLs, transpiles TS \u2192 ESM, and emits a sibling\n * `.mjs` we can `import()` from Node. Cached on mtime so repeat calls in the\n * same process don't recompile.\n *\n * Transpile-only (no bundling): the generated registries declare an array\n * literal whose entries are static `import(\"\u2026\")` arrow functions \u2014 we want\n * those `import()` strings to stay as runtime imports so Node resolves them\n * lazily through the workspace's normal module resolution. Eagerly bundling\n * pulls Next.js / route-handler internals into the `.mjs` and breaks at runtime\n * (e.g. `next/server` package-exports map).\n */\nexport async function compileAndImportGenerated(tsPath: string): Promise<Record<string, unknown>> {\n const jsPath = tsPath.replace(/\\.ts$/, '.mjs')\n // appRoot is two directories up from `.mercato/generated/<file>.ts`.\n const appRoot = path.dirname(path.dirname(path.dirname(tsPath)))\n\n if (!fs.existsSync(tsPath)) {\n throw new Error(`Generated file not found: ${tsPath}`)\n }\n\n const jsExists = fs.existsSync(jsPath)\n const needsCompile =\n !jsExists || fs.statSync(tsPath).mtimeMs > fs.statSync(jsPath).mtimeMs\n\n if (needsCompile) {\n const esbuild = await import('esbuild')\n const tsSource = fs.readFileSync(tsPath, 'utf-8')\n const aliasRewritten = rewriteGeneratedAliasImports(tsSource, appRoot)\n const result = await esbuild.transform(aliasRewritten, {\n loader: 'ts',\n format: 'esm',\n target: 'node18',\n sourcemap: false,\n sourcefile: tsPath,\n })\n fs.writeFileSync(jsPath, result.code)\n }\n\n return (await import(pathToFileURL(jsPath).href)) as Record<string, unknown>\n}\n\nconst UNSAFE_JS_STRING_CHAR_ESCAPES: Record<number, string> = {\n 0x3c: '\\\\u003C', // < \u2014 HTML/script-tag breakout\n 0x3e: '\\\\u003E', // > \u2014 HTML/script-tag breakout\n 0x2028: '\\\\u2028', // line separator \u2014 string content but a statement terminator pre-ES2019\n 0x2029: '\\\\u2029', // paragraph separator \u2014 same\n}\n\n/**\n * Escape characters that `JSON.stringify` leaves intact but which can still\n * break out of (or alter the meaning of) the JavaScript string literal that the\n * stringified value is embedded into \u2014 notably `<`/`>` (HTML/script-tag\n * breakout) and the U+2028 / U+2029 line separators (valid string content but\n * statement terminators in pre-ES2019 parsers). Apply this on top of\n * `JSON.stringify` so the emitted import source stays well-formed regardless of\n * the resolved path. Exported for unit testing.\n */\nexport function escapeUnsafeJsStringChars(value: string): string {\n return value.replace(\n /[<>\\u2028\\u2029]/g,\n (char) => UNSAFE_JS_STRING_CHAR_ESCAPES[char.charCodeAt(0)],\n )\n}\n\n/**\n * Stringify a resolved path into a JavaScript string literal that is safe to\n * embed in generated source: `JSON.stringify` handles quoting/standard escapes,\n * and `escapeUnsafeJsStringChars` neutralizes the characters it leaves intact.\n */\nfunction toSafeJsStringLiteral(value: string): string {\n return escapeUnsafeJsStringChars(JSON.stringify(value))\n}\n\n/**\n * Rewrite the two specifier shapes the generator emits for module entries in a\n * generated registry to absolute `file://` URLs Node can resolve in a\n * standalone process:\n *\n * 1. `@/...` path-alias imports (both `from \"@/x\"` and dynamic `import(\"@/x\")`).\n * The `@/` alias is a Next.js bundler convention; outside the bundler Node\n * treats `@/...` as a bare package specifier and throws\n * `ERR_MODULE_NOT_FOUND`. Resolved against `appRoot`.\n * 2. `../../src/...` relative imports the generator emits for `@app` local\n * modules (e.g. `from \"../../src/modules/<id>/ai-tools\"`). esbuild's\n * transform (transpile-only) leaves these untouched, so the compiled\n * `.mjs` keeps an extensionless relative specifier that resolves to a\n * `.ts` file with no compiled `.js`/`.mjs` sibling \u2014 Node ESM then throws\n * `ERR_MODULE_NOT_FOUND`. Resolved against the generated file's directory\n * (`<appRoot>/.mercato/generated`), the location the generator wrote them\n * relative to. Package-backed modules (`@open-mercato/*`) are unaffected \u2014\n * their bare specifiers resolve through `node_modules` to compiled `.js`.\n *\n * Both shapes reuse the same `.ts`-suffix probe so a source-only TypeScript\n * target is loaded directly (Node strips types). Other specifiers (bare\n * packages, sibling `./` imports) are left untouched. Exported for unit testing.\n */\nexport function rewriteGeneratedAliasImports(source: string, appRoot: string): string {\n const generatedDir = path.join(appRoot, '.mercato', 'generated')\n const toResolvedLiteral = (target: string): string => {\n const candidate = fs.existsSync(target)\n ? target\n : fs.existsSync(target + '.ts')\n ? target + '.ts'\n : target\n return toSafeJsStringLiteral(pathToFileURL(candidate).href)\n }\n const resolveAlias = (relativePath: string): string =>\n toResolvedLiteral(path.join(appRoot, relativePath))\n const resolveRelative = (specifier: string): string =>\n toResolvedLiteral(path.resolve(generatedDir, specifier))\n return source\n .replace(/from\\s+[\"']@\\/([^\"']+)[\"']/g, (_match, relativePath: string) => {\n return `from ${resolveAlias(relativePath)}`\n })\n .replace(/import\\s*\\(\\s*[\"']@\\/([^\"']+)[\"']\\s*\\)/g, (_match, relativePath: string) => {\n return `import(${resolveAlias(relativePath)})`\n })\n .replace(/from\\s+[\"']((?:\\.\\.\\/)+src\\/[^\"']+)[\"']/g, (_match, specifier: string) => {\n return `from ${resolveRelative(specifier)}`\n })\n .replace(/import\\s*\\(\\s*[\"']((?:\\.\\.\\/)+src\\/[^\"']+)[\"']\\s*\\)/g, (_match, specifier: string) => {\n return `import(${resolveRelative(specifier)})`\n })\n}\n\n/**\n * Compile-and-import `api-routes.generated.ts` and register its manifest with\n * the shared registry. Many module tools are \"API-backed\" \u2014 their handlers\n * delegate to `createAiApiOperationRunner`, which fails closed with\n * \"No API route manifest registered\" unless the manifest is present. In the\n * Next.js app this is wired at bootstrap, but the standalone MCP servers\n * (`mcp:dev` / `mcp:serve`) bootstrap DI without it, so we register it here.\n *\n * Idempotent: `registerApiRouteManifests` replaces the stored manifest, so\n * calling this repeatedly (e.g. per-request HTTP handlers) is safe. Returns the\n * number of registered routes (0 when the generated file is absent).\n */\nexport async function ensureApiRouteManifestsRegistered(): Promise<number> {\n const registry = await import('@open-mercato/shared/modules/registry')\n // Already wired (e.g. the Next.js app bootstrap, or a prior call). Leave the\n // existing manifest untouched so we never interfere with the in-app agents\n // framework, which registers it at bootstrap with its own override pipeline.\n const existing = registry.getApiRouteManifests()\n if (existing.length > 0) return existing.length\n\n const tsPath = findGeneratedFile('api-routes.generated.ts')\n if (!tsPath) return 0\n try {\n const mod = await compileAndImportGenerated(tsPath)\n const apiRoutes = (mod as { apiRoutes?: unknown }).apiRoutes\n if (!Array.isArray(apiRoutes)) return 0\n registry.registerApiRouteManifests(\n apiRoutes as Parameters<typeof registry.registerApiRouteManifests>[0],\n )\n return apiRoutes.length\n } catch (error) {\n console.warn(\n '[MCP Tools] Could not register api-routes manifest:',\n error instanceof Error ? error.message : error,\n )\n return 0\n }\n}\n"],
|
|
5
|
+
"mappings": "AAgBA,OAAO,UAAU;AACjB,OAAO,QAAQ;AACf,SAAS,eAAe,qBAAqB;AAQtC,SAAS,kBAAkB,UAAiC;AACjE,QAAM,QAAQ,MAAM;AAClB,QAAI;AACF,aAAO,cAAc,YAAY,GAAG;AAAA,IACtC,QAAQ;AACN,aAAO;AAAA,IACT;AAAA,EACF,GAAG;AAEH,MAAI,MAAM;AACR,QAAI,SAAS,KAAK,QAAQ,IAAI;AAC9B,aAAS,IAAI,GAAG,IAAI,IAAI,KAAK;AAC3B,YAAM,YAAY,KAAK,KAAK,QAAQ,QAAQ,WAAW,YAAY,aAAa,QAAQ;AACxF,UAAI,GAAG,WAAW,SAAS,EAAG,QAAO;AACrC,YAAM,OAAO,KAAK,QAAQ,MAAM;AAChC,UAAI,SAAS,OAAQ;AACrB,eAAS;AAAA,IACX;AAAA,EACF;AAGA,QAAM,UAAU,KAAK,QAAQ,QAAQ,IAAI,GAAG,QAAQ,WAAW,YAAY,aAAa,QAAQ;AAChG,MAAI,GAAG,WAAW,OAAO,EAAG,QAAO;AACnC,QAAM,gBAAgB,KAAK,QAAQ,QAAQ,IAAI,GAAG,YAAY,aAAa,QAAQ;AACnF,MAAI,GAAG,WAAW,aAAa,EAAG,QAAO;AACzC,SAAO;AACT;AAiBA,eAAsB,0BAA0B,QAAkD;AAChG,QAAM,SAAS,OAAO,QAAQ,SAAS,MAAM;AAE7C,QAAM,UAAU,KAAK,QAAQ,KAAK,QAAQ,KAAK,QAAQ,MAAM,CAAC,CAAC;AAE/D,MAAI,CAAC,GAAG,WAAW,MAAM,GAAG;AAC1B,UAAM,IAAI,MAAM,6BAA6B,MAAM,EAAE;AAAA,EACvD;AAEA,QAAM,WAAW,GAAG,WAAW,MAAM;AACrC,QAAM,eACJ,CAAC,YAAY,GAAG,SAAS,MAAM,EAAE,UAAU,GAAG,SAAS,MAAM,EAAE;AAEjE,MAAI,cAAc;AAChB,UAAM,UAAU,MAAM,OAAO,SAAS;AACtC,UAAM,WAAW,GAAG,aAAa,QAAQ,OAAO;AAChD,UAAM,iBAAiB,6BAA6B,UAAU,OAAO;AACrE,UAAM,SAAS,MAAM,QAAQ,UAAU,gBAAgB;AAAA,MACrD,QAAQ;AAAA,MACR,QAAQ;AAAA,MACR,QAAQ;AAAA,MACR,WAAW;AAAA,MACX,YAAY;AAAA,IACd,CAAC;AACD,OAAG,cAAc,QAAQ,OAAO,IAAI;AAAA,EACtC;AAEA,SAAQ,MAAM,OAAO,cAAc,MAAM,EAAE;AAC7C;AAEA,MAAM,gCAAwD;AAAA,EAC5D,IAAM;AAAA;AAAA,EACN,IAAM;AAAA;AAAA,EACN,MAAQ;AAAA;AAAA,EACR,MAAQ;AAAA;AACV;AAWO,SAAS,0BAA0B,OAAuB;AAC/D,SAAO,MAAM;AAAA,IACX;AAAA,IACA,CAAC,SAAS,8BAA8B,KAAK,WAAW,CAAC,CAAC;AAAA,EAC5D;AACF;AAOA,SAAS,sBAAsB,OAAuB;AACpD,SAAO,0BAA0B,KAAK,UAAU,KAAK,CAAC;AACxD;AAyBO,SAAS,6BAA6B,QAAgB,SAAyB;AACpF,QAAM,eAAe,KAAK,KAAK,SAAS,YAAY,WAAW;AAC/D,QAAM,oBAAoB,CAAC,WAA2B;AACpD,UAAM,YAAY,GAAG,WAAW,MAAM,IAClC,SACA,GAAG,WAAW,SAAS,KAAK,IAC1B,SAAS,QACT;AACN,WAAO,sBAAsB,cAAc,SAAS,EAAE,IAAI;AAAA,EAC5D;AACA,QAAM,eAAe,CAAC,iBACpB,kBAAkB,KAAK,KAAK,SAAS,YAAY,CAAC;AACpD,QAAM,kBAAkB,CAAC,cACvB,kBAAkB,KAAK,QAAQ,cAAc,SAAS,CAAC;AACzD,SAAO,OACJ,QAAQ,+BAA+B,CAAC,QAAQ,iBAAyB;AACxE,WAAO,QAAQ,aAAa,YAAY,CAAC;AAAA,EAC3C,CAAC,EACA,QAAQ,2CAA2C,CAAC,QAAQ,iBAAyB;AACpF,WAAO,UAAU,aAAa,YAAY,CAAC;AAAA,EAC7C,CAAC,EACA,QAAQ,4CAA4C,CAAC,QAAQ,cAAsB;AAClF,WAAO,QAAQ,gBAAgB,SAAS,CAAC;AAAA,EAC3C,CAAC,EACA,QAAQ,wDAAwD,CAAC,QAAQ,cAAsB;AAC9F,WAAO,UAAU,gBAAgB,SAAS,CAAC;AAAA,EAC7C,CAAC;AACL;AAcA,eAAsB,oCAAqD;AACzE,QAAM,WAAW,MAAM,OAAO,uCAAuC;AAIrE,QAAM,WAAW,SAAS,qBAAqB;AAC/C,MAAI,SAAS,SAAS,EAAG,QAAO,SAAS;AAEzC,QAAM,SAAS,kBAAkB,yBAAyB;AAC1D,MAAI,CAAC,OAAQ,QAAO;AACpB,MAAI;AACF,UAAM,MAAM,MAAM,0BAA0B,MAAM;AAClD,UAAM,YAAa,IAAgC;AACnD,QAAI,CAAC,MAAM,QAAQ,SAAS,EAAG,QAAO;AACtC,aAAS;AAAA,MACP;AAAA,IACF;AACA,WAAO,UAAU;AAAA,EACnB,SAAS,OAAO;AACd,YAAQ;AAAA,MACN;AAAA,MACA,iBAAiB,QAAQ,MAAM,UAAU;AAAA,IAC3C;AACA,WAAO;AAAA,EACT;AACF;",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@open-mercato/ai-assistant",
|
|
3
|
-
"version": "0.6.6-develop.
|
|
3
|
+
"version": "0.6.6-develop.6255.1.a6ee4a57c1",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"engines": {
|
|
@@ -99,16 +99,16 @@
|
|
|
99
99
|
"zod-to-json-schema": "^3.25.2"
|
|
100
100
|
},
|
|
101
101
|
"peerDependencies": {
|
|
102
|
-
"@open-mercato/shared": "0.6.6-develop.
|
|
103
|
-
"@open-mercato/ui": "0.6.6-develop.
|
|
102
|
+
"@open-mercato/shared": "0.6.6-develop.6255.1.a6ee4a57c1",
|
|
103
|
+
"@open-mercato/ui": "0.6.6-develop.6255.1.a6ee4a57c1",
|
|
104
104
|
"react": "^19.0.0",
|
|
105
105
|
"react-dom": "^19.0.0",
|
|
106
106
|
"zod": ">=3.23.0"
|
|
107
107
|
},
|
|
108
108
|
"devDependencies": {
|
|
109
|
-
"@open-mercato/cli": "0.6.6-develop.
|
|
110
|
-
"@open-mercato/shared": "0.6.6-develop.
|
|
111
|
-
"@open-mercato/ui": "0.6.6-develop.
|
|
109
|
+
"@open-mercato/cli": "0.6.6-develop.6255.1.a6ee4a57c1",
|
|
110
|
+
"@open-mercato/shared": "0.6.6-develop.6255.1.a6ee4a57c1",
|
|
111
|
+
"@open-mercato/ui": "0.6.6-develop.6255.1.a6ee4a57c1",
|
|
112
112
|
"@types/react": "^19.2.17",
|
|
113
113
|
"@types/react-dom": "^19.2.3",
|
|
114
114
|
"react": "19.2.7",
|
|
@@ -73,6 +73,53 @@ describe('rewriteGeneratedAliasImports', () => {
|
|
|
73
73
|
].join('\n')
|
|
74
74
|
expect(rewriteGeneratedAliasImports(source, makeTempDir())).toBe(source)
|
|
75
75
|
})
|
|
76
|
+
|
|
77
|
+
// Regression for issue #3524: the MCP dev server crashed with
|
|
78
|
+
// "ERR_MODULE_NOT_FOUND: Cannot find module '.../src/modules/<id>/ai-tools'"
|
|
79
|
+
// because the generator emits `../../src/modules/<id>/ai-tools` relative
|
|
80
|
+
// imports for @app local modules, which the rewriter previously left intact.
|
|
81
|
+
// esbuild.transform keeps them verbatim, so Node ESM can't resolve the
|
|
82
|
+
// extensionless `.ts`-only specifier from the compiled `.mjs`.
|
|
83
|
+
|
|
84
|
+
it('rewrites a `../../src/...` @app relative import to an absolute file URL', () => {
|
|
85
|
+
const appRoot = makeTempDir()
|
|
86
|
+
const out = rewriteGeneratedAliasImports(
|
|
87
|
+
`import * as AI_TOOLS from "../../src/modules/media_campaigns/ai-tools"`,
|
|
88
|
+
appRoot,
|
|
89
|
+
)
|
|
90
|
+
const expectedUrl = pathToFileURL(
|
|
91
|
+
path.join(appRoot, 'src', 'modules', 'media_campaigns', 'ai-tools'),
|
|
92
|
+
).href
|
|
93
|
+
expect(out).toBe(`import * as AI_TOOLS from ${safeJsLiteral(expectedUrl)}`)
|
|
94
|
+
expect(out).not.toContain('../../src/')
|
|
95
|
+
})
|
|
96
|
+
|
|
97
|
+
it('rewrites a dynamic `import("../../src/...")` @app call to an absolute file URL', () => {
|
|
98
|
+
const appRoot = makeTempDir()
|
|
99
|
+
const specifier = '"../../src/modules/media_campaigns/ai-tools"'
|
|
100
|
+
const source = `const load = () => import(${specifier})`
|
|
101
|
+
const out = rewriteGeneratedAliasImports(source, appRoot)
|
|
102
|
+
const expectedUrl = pathToFileURL(
|
|
103
|
+
path.join(appRoot, 'src', 'modules', 'media_campaigns', 'ai-tools'),
|
|
104
|
+
).href
|
|
105
|
+
expect(out).toBe(source.replace(specifier, safeJsLiteral(expectedUrl)))
|
|
106
|
+
expect(out).not.toContain('../../src/')
|
|
107
|
+
})
|
|
108
|
+
|
|
109
|
+
it('appends `.ts` for a `../../src/...` @app import that exists only as TypeScript', () => {
|
|
110
|
+
const appRoot = makeTempDir()
|
|
111
|
+
const moduleDir = path.join(appRoot, 'src', 'modules', 'media_campaigns')
|
|
112
|
+
fs.mkdirSync(moduleDir, { recursive: true })
|
|
113
|
+
fs.writeFileSync(path.join(moduleDir, 'ai-tools.ts'), 'export const aiTools = []\n')
|
|
114
|
+
|
|
115
|
+
const out = rewriteGeneratedAliasImports(
|
|
116
|
+
`import * as AI_TOOLS from "../../src/modules/media_campaigns/ai-tools"`,
|
|
117
|
+
appRoot,
|
|
118
|
+
)
|
|
119
|
+
|
|
120
|
+
const expectedUrl = pathToFileURL(path.join(moduleDir, 'ai-tools.ts')).href
|
|
121
|
+
expect(out).toBe(`import * as AI_TOOLS from ${safeJsLiteral(expectedUrl)}`)
|
|
122
|
+
})
|
|
76
123
|
})
|
|
77
124
|
|
|
78
125
|
describe('escapeUnsafeJsStringChars', () => {
|
|
@@ -53,10 +53,12 @@ export function findGeneratedFile(fileName: string): string | null {
|
|
|
53
53
|
}
|
|
54
54
|
|
|
55
55
|
/**
|
|
56
|
-
* Compile-and-import a generated registry file on the fly.
|
|
57
|
-
*
|
|
58
|
-
*
|
|
59
|
-
*
|
|
56
|
+
* Compile-and-import a generated registry file on the fly. Rewrites the entry
|
|
57
|
+
* specifiers Node can't resolve standalone (`@/...` aliases and the
|
|
58
|
+
* `../../src/...` relative imports the generator emits for `@app` local
|
|
59
|
+
* modules) to absolute file URLs, transpiles TS → ESM, and emits a sibling
|
|
60
|
+
* `.mjs` we can `import()` from Node. Cached on mtime so repeat calls in the
|
|
61
|
+
* same process don't recompile.
|
|
60
62
|
*
|
|
61
63
|
* Transpile-only (no bundling): the generated registries declare an array
|
|
62
64
|
* literal whose entries are static `import("…")` arrow functions — we want
|
|
@@ -128,15 +130,31 @@ function toSafeJsStringLiteral(value: string): string {
|
|
|
128
130
|
}
|
|
129
131
|
|
|
130
132
|
/**
|
|
131
|
-
* Rewrite
|
|
132
|
-
*
|
|
133
|
-
*
|
|
134
|
-
*
|
|
135
|
-
* `
|
|
133
|
+
* Rewrite the two specifier shapes the generator emits for module entries in a
|
|
134
|
+
* generated registry to absolute `file://` URLs Node can resolve in a
|
|
135
|
+
* standalone process:
|
|
136
|
+
*
|
|
137
|
+
* 1. `@/...` path-alias imports (both `from "@/x"` and dynamic `import("@/x")`).
|
|
138
|
+
* The `@/` alias is a Next.js bundler convention; outside the bundler Node
|
|
139
|
+
* treats `@/...` as a bare package specifier and throws
|
|
140
|
+
* `ERR_MODULE_NOT_FOUND`. Resolved against `appRoot`.
|
|
141
|
+
* 2. `../../src/...` relative imports the generator emits for `@app` local
|
|
142
|
+
* modules (e.g. `from "../../src/modules/<id>/ai-tools"`). esbuild's
|
|
143
|
+
* transform (transpile-only) leaves these untouched, so the compiled
|
|
144
|
+
* `.mjs` keeps an extensionless relative specifier that resolves to a
|
|
145
|
+
* `.ts` file with no compiled `.js`/`.mjs` sibling — Node ESM then throws
|
|
146
|
+
* `ERR_MODULE_NOT_FOUND`. Resolved against the generated file's directory
|
|
147
|
+
* (`<appRoot>/.mercato/generated`), the location the generator wrote them
|
|
148
|
+
* relative to. Package-backed modules (`@open-mercato/*`) are unaffected —
|
|
149
|
+
* their bare specifiers resolve through `node_modules` to compiled `.js`.
|
|
150
|
+
*
|
|
151
|
+
* Both shapes reuse the same `.ts`-suffix probe so a source-only TypeScript
|
|
152
|
+
* target is loaded directly (Node strips types). Other specifiers (bare
|
|
153
|
+
* packages, sibling `./` imports) are left untouched. Exported for unit testing.
|
|
136
154
|
*/
|
|
137
155
|
export function rewriteGeneratedAliasImports(source: string, appRoot: string): string {
|
|
138
|
-
const
|
|
139
|
-
|
|
156
|
+
const generatedDir = path.join(appRoot, '.mercato', 'generated')
|
|
157
|
+
const toResolvedLiteral = (target: string): string => {
|
|
140
158
|
const candidate = fs.existsSync(target)
|
|
141
159
|
? target
|
|
142
160
|
: fs.existsSync(target + '.ts')
|
|
@@ -144,6 +162,10 @@ export function rewriteGeneratedAliasImports(source: string, appRoot: string): s
|
|
|
144
162
|
: target
|
|
145
163
|
return toSafeJsStringLiteral(pathToFileURL(candidate).href)
|
|
146
164
|
}
|
|
165
|
+
const resolveAlias = (relativePath: string): string =>
|
|
166
|
+
toResolvedLiteral(path.join(appRoot, relativePath))
|
|
167
|
+
const resolveRelative = (specifier: string): string =>
|
|
168
|
+
toResolvedLiteral(path.resolve(generatedDir, specifier))
|
|
147
169
|
return source
|
|
148
170
|
.replace(/from\s+["']@\/([^"']+)["']/g, (_match, relativePath: string) => {
|
|
149
171
|
return `from ${resolveAlias(relativePath)}`
|
|
@@ -151,6 +173,12 @@ export function rewriteGeneratedAliasImports(source: string, appRoot: string): s
|
|
|
151
173
|
.replace(/import\s*\(\s*["']@\/([^"']+)["']\s*\)/g, (_match, relativePath: string) => {
|
|
152
174
|
return `import(${resolveAlias(relativePath)})`
|
|
153
175
|
})
|
|
176
|
+
.replace(/from\s+["']((?:\.\.\/)+src\/[^"']+)["']/g, (_match, specifier: string) => {
|
|
177
|
+
return `from ${resolveRelative(specifier)}`
|
|
178
|
+
})
|
|
179
|
+
.replace(/import\s*\(\s*["']((?:\.\.\/)+src\/[^"']+)["']\s*\)/g, (_match, specifier: string) => {
|
|
180
|
+
return `import(${resolveRelative(specifier)})`
|
|
181
|
+
})
|
|
154
182
|
}
|
|
155
183
|
|
|
156
184
|
/**
|