@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 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 resolveAlias = (relativePath) => {
72
- const target = path.join(appRoot, relativePath);
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;AAeA,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;AASO,SAAS,6BAA6B,QAAgB,SAAyB;AACpF,QAAM,eAAe,CAAC,iBAAiC;AACrD,UAAM,SAAS,KAAK,KAAK,SAAS,YAAY;AAC9C,UAAM,YAAY,GAAG,WAAW,MAAM,IAClC,SACA,GAAG,WAAW,SAAS,KAAK,IAC1B,SAAS,QACT;AACN,WAAO,sBAAsB,cAAc,SAAS,EAAE,IAAI;AAAA,EAC5D;AACA,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;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;",
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": "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",
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.6246.1.85fd30c705",
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.6246.1.85fd30c705",
103
- "@open-mercato/ui": "0.6.6-develop.6246.1.85fd30c705",
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.6246.1.85fd30c705",
110
- "@open-mercato/shared": "0.6.6-develop.6246.1.85fd30c705",
111
- "@open-mercato/ui": "0.6.6-develop.6246.1.85fd30c705",
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. Resolves the `@/`
57
- * alias to the app root, transpiles TS → ESM, and emits a sibling `.mjs` we can
58
- * `import()` from Node. Cached on mtime so repeat calls in the same process
59
- * don't recompile.
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 `@/...` path-alias imports (both `from "@/x"` and dynamic
132
- * `import("@/x")`) in generated-registry source to absolute `file://` URLs
133
- * rooted at `appRoot`. The `@/` alias is a Next.js bundler convention; outside
134
- * the bundler Node treats `@/...` as a bare package specifier and throws
135
- * `ERR_MODULE_NOT_FOUND`. Exported for unit testing.
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 resolveAlias = (relativePath: string): string => {
139
- const target = path.join(appRoot, relativePath)
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
  /**