@open-mercato/ai-assistant 0.6.5-develop.5240.2.bbd9d30275 → 0.6.5-develop.5266.2.9acdca82ec

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.
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../../src/modules/ai_assistant/lib/tool-test-runner.ts"],
4
- "sourcesContent": ["/**\n * In-process AI tool test runner.\n *\n * Iterates every tool registered in `ai-tools.generated.ts`, invokes the\n * handler against a super-admin tenant context, and returns a structured\n * report. Used by the `mercato ai_assistant test-tools` CLI subcommand and\n * by `.ai/qa/tests/integration/TC-INT-AI-TOOLS.spec.ts`.\n *\n * Safety posture:\n * - No HTTP exposure \u2014 runs only inside the Node process driving the CLI.\n * - Mutation tools are exercised through `prepareMutation` and we assert a\n * pending-action envelope is returned. The pending-action row is created\n * in `ai_pending_actions` but never confirmed, so no real write happens.\n * - Tools without an explicit fixture entry are skipped with reason\n * `'no fixture'` rather than failing.\n */\nimport path from 'node:path'\nimport fs from 'node:fs'\nimport { fileURLToPath, pathToFileURL } from 'node:url'\nimport type { AwilixContainer } from 'awilix'\nimport { createRequestContainer } from '@open-mercato/shared/lib/di/container'\nimport type { McpToolContext, AiToolDefinition } from './types'\nimport type { AiAgentDefinition, AiAgentMutationPolicy } from './ai-agent-definition'\nimport { getFixture, type ToolFixture } from './tool-test-fixtures'\nimport { prepareMutation } from './prepare-mutation'\nimport { executePendingActionConfirm } from './pending-action-executor'\n\nexport type ToolTestStatus = 'pass' | 'fail' | 'skip'\n\nexport interface ToolTestRecord {\n module: string\n tool: string\n isMutation: boolean\n status: ToolTestStatus\n durationMs: number\n reason?: string\n resultPreview?: unknown\n}\n\nexport interface ToolTestReport {\n tenantId: string | null\n organizationId: string | null\n total: number\n passed: number\n failed: number\n skipped: number\n records: ToolTestRecord[]\n}\n\ninterface RawAiToolsModule {\n aiToolConfigEntriesRaw?: { moduleId: string; tools: unknown[] }[]\n aiToolConfigEntries?: { moduleId: string; tools: unknown[] }[]\n}\n\nfunction isToolDefinition(value: unknown): value is AiToolDefinition {\n if (!value || typeof value !== 'object') return false\n const candidate = value as Record<string, unknown>\n return (\n typeof candidate.name === 'string' &&\n typeof candidate.description === 'string' &&\n candidate.inputSchema !== undefined &&\n typeof candidate.handler === 'function'\n )\n}\n\n/**\n * Locate `apps/mercato/.mercato/generated/ai-tools.generated.ts` without\n * hardcoding the workspace layout. Searches upward from this file's\n * compiled location; in the monorepo dist this is\n * `packages/ai-assistant/dist/...` so we walk up until we hit a directory\n * containing `apps/mercato/.mercato/generated`.\n */\nfunction findGeneratedAiToolsPath(): string | null {\n const here = (() => {\n try {\n return fileURLToPath(import.meta.url)\n } catch {\n return null\n }\n })()\n if (!here) return null\n let cursor = path.dirname(here)\n for (let i = 0; i < 12; i++) {\n const candidate = path.join(cursor, 'apps', 'mercato', '.mercato', 'generated', 'ai-tools.generated.ts')\n if (fs.existsSync(candidate)) return candidate\n const next = path.dirname(cursor)\n if (next === cursor) break\n cursor = next\n }\n // Fallback: cwd-based lookup (when CLI is invoked from apps/mercato).\n const fromCwd = path.resolve(process.cwd(), 'apps', 'mercato', '.mercato', 'generated', 'ai-tools.generated.ts')\n if (fs.existsSync(fromCwd)) return fromCwd\n const fromCwdDirect = path.resolve(process.cwd(), '.mercato', 'generated', 'ai-tools.generated.ts')\n if (fs.existsSync(fromCwdDirect)) return fromCwdDirect\n return null\n}\n\n/**\n * Compile-and-import `ai-tools.generated.ts` on the fly. Mirrors the approach\n * used by `loadBootstrapData` in `@open-mercato/shared/lib/bootstrap/dynamicLoader`:\n * resolves the `@/` alias to the app root, marks every other package import as\n * external, and emits a sibling `.mjs` we can `import()` from Node. Cached on\n * mtime so repeat runs in the same process don't recompile.\n */\nasync function compileAndImportGenerated(tsPath: string): Promise<RawAiToolsModule> {\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 const tsExists = fs.existsSync(tsPath)\n if (!tsExists) {\n throw new Error(`Generated file not found: ${tsPath}`)\n }\n const jsExists = fs.existsSync(jsPath)\n const needsCompile =\n !jsExists || fs.statSync(tsPath).mtimeMs > fs.statSync(jsPath).mtimeMs\n if (needsCompile) {\n const esbuild = await import('esbuild')\n // Transpile-only: don't bundle. Generated registry files only declare an\n // array literal whose entries are static `import(\"\u2026\")` arrow functions \u2014\n // we want those `import()` strings to stay as runtime imports so Node\n // resolves them lazily through the workspace's normal module resolution.\n // Eagerly bundling them pulls Next.js / route handler internals into the\n // .mjs and breaks at runtime (e.g. `next/server` package-exports map).\n const tsSource = fs.readFileSync(tsPath, 'utf-8')\n // Rewrite `@/...` aliases to absolute paths so Node can resolve them.\n const aliasRewritten = tsSource.replace(\n /from\\s+[\"']@\\/([^\"']+)[\"']/g,\n (_match, p1: string) => {\n const target = path.join(appRoot, p1)\n const candidate = fs.existsSync(target)\n ? target\n : fs.existsSync(target + '.ts')\n ? target + '.ts'\n : target\n return `from ${JSON.stringify(pathToFileURL(candidate).href)}`\n },\n ).replace(\n /import\\s*\\(\\s*[\"']@\\/([^\"']+)[\"']\\s*\\)/g,\n (_match, p1: string) => {\n const target = path.join(appRoot, p1)\n const candidate = fs.existsSync(target)\n ? target\n : fs.existsSync(target + '.ts')\n ? target + '.ts'\n : target\n return `import(${JSON.stringify(pathToFileURL(candidate).href)})`\n },\n )\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 return (await import(pathToFileURL(jsPath).href)) as RawAiToolsModule\n}\n\n/**\n * Compile-and-import `api-routes.generated.ts` and register its manifest with\n * the shared registry. Many tool handlers delegate to `aiApiOperationRunner`\n * which fails closed when no manifest is registered. Idempotent: registering\n * the same array twice is safe.\n */\nasync function ensureApiRouteManifestsRegistered(generatedDir: string): Promise<void> {\n const tsPath = path.join(generatedDir, 'api-routes.generated.ts')\n if (!fs.existsSync(tsPath)) return\n try {\n const mod = (await compileAndImportGenerated(tsPath)) as Record<string, unknown>\n const apiRoutes = (mod as { apiRoutes?: unknown }).apiRoutes\n if (!Array.isArray(apiRoutes)) {\n console.warn('[tool-test-runner] api-routes.generated.mjs returned no apiRoutes array')\n return\n }\n const registry = await import('@open-mercato/shared/modules/registry')\n registry.registerApiRouteManifests(\n apiRoutes as Parameters<typeof registry.registerApiRouteManifests>[0],\n )\n if (process.env.OM_TOOL_TEST_DEBUG === '1') {\n console.log(\n `[tool-test-runner] Registered ${apiRoutes.length} api-route manifests; getApiRouteManifests().length=${registry.getApiRouteManifests().length}`,\n )\n }\n } catch (error) {\n console.warn(\n '[tool-test-runner] Could not register api-routes manifest:',\n error instanceof Error ? error.message : error,\n )\n }\n}\n\nasync function loadGeneratedTools(): Promise<{ moduleId: string; tools: AiToolDefinition[] }[]> {\n const tsPath = findGeneratedAiToolsPath()\n if (!tsPath) {\n throw new Error(\n 'Could not locate apps/<app>/.mercato/generated/ai-tools.generated.ts. Run `yarn generate` first.',\n )\n }\n await ensureApiRouteManifestsRegistered(path.dirname(tsPath))\n const mod = await compileAndImportGenerated(tsPath)\n const entries = mod.aiToolConfigEntriesRaw ?? mod.aiToolConfigEntries ?? []\n const result: { moduleId: string; tools: AiToolDefinition[] }[] = []\n for (const entry of entries) {\n if (!entry || typeof entry.moduleId !== 'string') continue\n const tools = Array.isArray(entry.tools)\n ? entry.tools.filter(isToolDefinition)\n : []\n result.push({ moduleId: entry.moduleId, tools })\n }\n return result\n}\n\nexport async function pickDefaultTenant(\n container: AwilixContainer,\n): Promise<{ tenantId: string; organizationId: string | null; userId: string | null } | null> {\n try {\n const em = container.resolve<{\n getConnection: () => {\n execute: (sql: string, params?: unknown[]) => Promise<Record<string, unknown>[]>\n }\n }>('em') as unknown as {\n getConnection: () => {\n execute: (sql: string, params?: unknown[]) => Promise<Record<string, unknown>[]>\n }\n }\n const conn = em.getConnection()\n const tenantRows = await conn.execute(\n `SELECT id FROM tenants WHERE deleted_at IS NULL ORDER BY created_at ASC LIMIT 1`,\n )\n const tenantId =\n Array.isArray(tenantRows) && tenantRows[0]\n ? String((tenantRows[0] as Record<string, unknown>).id)\n : null\n if (!tenantId) return null\n const orgRows = await conn.execute(\n `SELECT id FROM organizations WHERE tenant_id = ? AND deleted_at IS NULL ORDER BY created_at ASC LIMIT 1`,\n [tenantId],\n )\n const organizationId =\n Array.isArray(orgRows) && orgRows[0]\n ? String((orgRows[0] as Record<string, unknown>).id)\n : null\n const userRows = await conn.execute(\n `SELECT id FROM users WHERE tenant_id = ? AND deleted_at IS NULL ORDER BY created_at ASC LIMIT 1`,\n [tenantId],\n )\n const userId =\n Array.isArray(userRows) && userRows[0]\n ? String((userRows[0] as Record<string, unknown>).id)\n : null\n return { tenantId, organizationId, userId }\n } catch (error) {\n if (process.env.OM_TOOL_TEST_DEBUG === '1') {\n console.warn('[tool-test-runner] pickDefaultTenant failed:', error)\n }\n return null\n }\n}\n\nfunction buildSuperAdminContext(\n container: AwilixContainer,\n tenantId: string | null,\n organizationId: string | null,\n userId: string | null,\n): McpToolContext {\n return {\n tenantId,\n organizationId,\n userId,\n container,\n userFeatures: ['*'],\n isSuperAdmin: true,\n }\n}\n\nfunction clipPreview(value: unknown): unknown {\n try {\n const json = JSON.stringify(value)\n if (json.length <= 400) return value\n return `${json.slice(0, 400)}\u2026(truncated, ${json.length} bytes)`\n } catch {\n return '[unserializable]'\n }\n}\n\nfunction dummyAgentForTool(tool: AiToolDefinition): AiAgentDefinition {\n // The runner never registers this agent; it's only used as input to\n // `prepareMutation` for shape compatibility. The id namespace is namespaced\n // under `__test__` so it cannot collide with real agents.\n const policy: AiAgentMutationPolicy = 'destructive-confirm-required'\n return {\n id: `__test__.${tool.name}`,\n moduleId: tool.name.split('.')[0] ?? '__test__',\n label: 'Tool Test Runner',\n description: 'Synthetic agent used by the tool test runner',\n systemPrompt: '',\n allowedTools: [tool.name],\n readOnly: false,\n mutationPolicy: policy,\n } as AiAgentDefinition\n}\n\nasync function executeReadTool(\n tool: AiToolDefinition,\n args: Record<string, unknown>,\n container: AwilixContainer,\n ctx: McpToolContext,\n): Promise<unknown> {\n // Mirror the dispatcher: resolve a fresh container per call so EM identity\n // map state is clean between tools.\n const fresh = await createRequestContainer()\n const freshCtx: McpToolContext = { ...ctx, container: fresh, tool }\n void container // keep arg for future use\n return tool.handler(args as never, freshCtx)\n}\n\nasync function executeMutationTool(\n tool: AiToolDefinition,\n args: Record<string, unknown>,\n container: AwilixContainer,\n ctx: McpToolContext,\n): Promise<unknown> {\n // Route through prepareMutation so we assert the approval contract still\n // holds \u2014 the handler must NEVER write directly. We pass a destructive\n // policy so the runtime treats the call as a confirmation candidate and\n // creates a pending-action row.\n const fresh = await createRequestContainer()\n const agent = dummyAgentForTool(tool)\n const userId = ctx.userId ?? '00000000-0000-0000-0000-000000000000'\n const { uiPart, pendingAction } = await prepareMutation(\n {\n agent,\n tool,\n toolCallArgs: args,\n conversationId: null,\n mutationPolicyOverride: null,\n },\n {\n tenantId: ctx.tenantId,\n organizationId: ctx.organizationId,\n userId,\n features: ctx.userFeatures,\n isSuperAdmin: ctx.isSuperAdmin,\n container: fresh,\n },\n )\n // Exercise the actual handler via the same path the production confirm\n // route uses. Without this, mutation tools whose handler crashes (e.g.\n // missing `ctx.tool` for the API operation runner) would be reported as\n // passing because `prepareMutation` never invokes the handler.\n // `prepareMutation` already enforced `tenantId` is non-null above; cast\n // here for the stricter `PendingActionExecuteContext` shape.\n const confirmTenantId = ctx.tenantId as string\n const confirmContainer = await createRequestContainer()\n const confirmation = await executePendingActionConfirm({\n action: pendingAction,\n agent,\n tool,\n ctx: {\n tenantId: confirmTenantId,\n organizationId: ctx.organizationId,\n userId,\n container: confirmContainer,\n userFeatures: ctx.userFeatures,\n isSuperAdmin: ctx.isSuperAdmin,\n },\n emitEvent: async () => {},\n })\n if (!confirmation.ok) {\n const error = (confirmation.executionResult as { error?: { message?: string } } | undefined)?.error\n const cause = confirmation.cause\n const causeMessage =\n cause instanceof Error\n ? cause.message\n : typeof cause === 'string'\n ? cause\n : null\n const message = error?.message ?? causeMessage ?? 'handler invocation failed'\n throw new Error(message)\n }\n return {\n status: 'pending-confirmation',\n pendingActionId: pendingAction.id,\n expiresAt: pendingAction.expiresAt.toISOString(),\n uiPartType: (uiPart as { type?: string } | undefined)?.type ?? null,\n handlerExecuted: true,\n }\n}\n\nasync function resolveFixtureInput(\n fixture: ToolFixture,\n resolved: Map<string, AiToolDefinition>,\n container: AwilixContainer,\n ctx: McpToolContext,\n): Promise<{ ok: true; input: Record<string, unknown> } | { ok: false; reason: string }> {\n if (fixture.input) return { ok: true, input: { ...fixture.input } }\n if (!fixture.idFrom) {\n return { ok: false, reason: 'fixture has neither input nor idFrom' }\n }\n const sourceTool = resolved.get(fixture.idFrom)\n if (!sourceTool) {\n return { ok: false, reason: `idFrom tool not found: ${fixture.idFrom}` }\n }\n const sourceFixture = getFixture(fixture.idFrom)\n if (!sourceFixture?.input) {\n return {\n ok: false,\n reason: `idFrom source ${fixture.idFrom} has no static input fixture`,\n }\n }\n let listResult: unknown\n try {\n listResult = await executeReadTool(\n sourceTool,\n { ...sourceFixture.input },\n container,\n ctx,\n )\n } catch (error) {\n return {\n ok: false,\n reason: `idFrom source ${fixture.idFrom} threw: ${\n error instanceof Error ? error.message : String(error)\n }`,\n }\n }\n const records = extractRecords(listResult)\n if (!records.length) {\n return {\n ok: false,\n reason: `idFrom source ${fixture.idFrom} returned no records`,\n }\n }\n const idField = 'id'\n const id = (records[0] as Record<string, unknown>)[idField]\n if (typeof id !== 'string' || !id) {\n return {\n ok: false,\n reason: `idFrom source ${fixture.idFrom} first record has no string id`,\n }\n }\n const bindAs = fixture.bindAs ?? 'id'\n return {\n ok: true,\n input: { [bindAs]: id, ...(fixture.extra ?? {}) },\n }\n}\n\nfunction extractRecords(value: unknown): unknown[] {\n if (!value || typeof value !== 'object') return []\n const candidate = value as Record<string, unknown>\n for (const key of ['records', 'items', 'people', 'companies', 'deals', 'results', 'data']) {\n const arr = candidate[key]\n if (Array.isArray(arr)) return arr\n }\n return []\n}\n\nexport interface RunToolTestsOptions {\n tenantId?: string | null\n organizationId?: string | null\n userId?: string | null\n moduleFilter?: string | null\n includeMutations?: boolean\n}\n\nexport async function runToolTests(\n options: RunToolTestsOptions = {},\n): Promise<ToolTestReport> {\n const includeMutations = options.includeMutations ?? true\n const container = await createRequestContainer()\n let tenantId: string | null = options.tenantId ?? null\n let organizationId: string | null = options.organizationId ?? null\n let userId: string | null = options.userId ?? null\n if (!tenantId) {\n const picked = await pickDefaultTenant(container)\n if (picked) {\n tenantId = picked.tenantId\n organizationId = picked.organizationId\n userId = userId ?? picked.userId\n }\n }\n const ctx = buildSuperAdminContext(container, tenantId, organizationId, userId)\n const grouped = await loadGeneratedTools()\n\n const flat: { moduleId: string; tool: AiToolDefinition }[] = []\n for (const group of grouped) {\n if (options.moduleFilter && group.moduleId !== options.moduleFilter) continue\n for (const tool of group.tools) flat.push({ moduleId: group.moduleId, tool })\n }\n const resolved = new Map<string, AiToolDefinition>()\n for (const { tool } of flat) resolved.set(tool.name, tool)\n\n const records: ToolTestRecord[] = []\n for (const { moduleId, tool } of flat) {\n const start = Date.now()\n const isMutation = tool.isMutation === true\n const fixture = getFixture(tool.name)\n if (!fixture) {\n records.push({\n module: moduleId,\n tool: tool.name,\n isMutation,\n status: 'skip',\n durationMs: Date.now() - start,\n reason: 'no fixture',\n })\n continue\n }\n if (fixture.skip) {\n records.push({\n module: moduleId,\n tool: tool.name,\n isMutation,\n status: 'skip',\n durationMs: Date.now() - start,\n reason: fixture.note ?? 'skip-by-fixture',\n })\n continue\n }\n if (isMutation && !includeMutations) {\n records.push({\n module: moduleId,\n tool: tool.name,\n isMutation,\n status: 'skip',\n durationMs: Date.now() - start,\n reason: 'mutations excluded',\n })\n continue\n }\n const inputResult = await resolveFixtureInput(fixture, resolved, container, ctx)\n if (!inputResult.ok) {\n records.push({\n module: moduleId,\n tool: tool.name,\n isMutation,\n status: 'skip',\n durationMs: Date.now() - start,\n reason: inputResult.reason,\n })\n continue\n }\n try {\n const result = isMutation\n ? await executeMutationTool(tool, inputResult.input, container, ctx)\n : await executeReadTool(tool, inputResult.input, container, ctx)\n // Result must be JSON-serializable.\n JSON.stringify(result)\n // Mutation tools must return a pending-confirmation envelope.\n if (\n isMutation &&\n (typeof result !== 'object' ||\n result === null ||\n (result as Record<string, unknown>).status !== 'pending-confirmation')\n ) {\n records.push({\n module: moduleId,\n tool: tool.name,\n isMutation,\n status: 'fail',\n durationMs: Date.now() - start,\n reason: 'mutation tool did not route through prepareMutation (no pending-confirmation envelope)',\n resultPreview: clipPreview(result),\n })\n continue\n }\n records.push({\n module: moduleId,\n tool: tool.name,\n isMutation,\n status: 'pass',\n durationMs: Date.now() - start,\n resultPreview: clipPreview(result),\n })\n } catch (error) {\n records.push({\n module: moduleId,\n tool: tool.name,\n isMutation,\n status: 'fail',\n durationMs: Date.now() - start,\n reason: error instanceof Error ? error.message : String(error),\n })\n }\n }\n const passed = records.filter((r) => r.status === 'pass').length\n const failed = records.filter((r) => r.status === 'fail').length\n const skipped = records.filter((r) => r.status === 'skip').length\n return {\n tenantId,\n organizationId,\n total: records.length,\n passed,\n failed,\n skipped,\n records,\n }\n}\n"],
5
- "mappings": "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",
4
+ "sourcesContent": ["/**\n * In-process AI tool test runner.\n *\n * Iterates every tool registered in `ai-tools.generated.ts`, invokes the\n * handler against a super-admin tenant context, and returns a structured\n * report. Used by the `mercato ai_assistant test-tools` CLI subcommand and\n * by `.ai/qa/tests/integration/TC-INT-AI-TOOLS.spec.ts`.\n *\n * Safety posture:\n * - No HTTP exposure \u2014 runs only inside the Node process driving the CLI.\n * - Mutation tools are exercised through `prepareMutation` and we assert a\n * pending-action envelope is returned. The pending-action row is created\n * in `ai_pending_actions` but never confirmed, so no real write happens.\n * - Tools without an explicit fixture entry are skipped with reason\n * `'no fixture'` rather than failing.\n */\nimport type { AwilixContainer } from 'awilix'\nimport { createRequestContainer } from '@open-mercato/shared/lib/di/container'\nimport type { McpToolContext, AiToolDefinition } from './types'\nimport type { AiAgentDefinition, AiAgentMutationPolicy } from './ai-agent-definition'\nimport { getFixture, type ToolFixture } from './tool-test-fixtures'\nimport { prepareMutation } from './prepare-mutation'\nimport { executePendingActionConfirm } from './pending-action-executor'\nimport {\n findGeneratedFile,\n compileAndImportGenerated,\n ensureApiRouteManifestsRegistered,\n} from './generated-registry-loader'\n\nexport type ToolTestStatus = 'pass' | 'fail' | 'skip'\n\nexport interface ToolTestRecord {\n module: string\n tool: string\n isMutation: boolean\n status: ToolTestStatus\n durationMs: number\n reason?: string\n resultPreview?: unknown\n}\n\nexport interface ToolTestReport {\n tenantId: string | null\n organizationId: string | null\n total: number\n passed: number\n failed: number\n skipped: number\n records: ToolTestRecord[]\n}\n\ninterface RawAiToolsModule {\n aiToolConfigEntriesRaw?: { moduleId: string; tools: unknown[] }[]\n aiToolConfigEntries?: { moduleId: string; tools: unknown[] }[]\n}\n\nfunction isToolDefinition(value: unknown): value is AiToolDefinition {\n if (!value || typeof value !== 'object') return false\n const candidate = value as Record<string, unknown>\n return (\n typeof candidate.name === 'string' &&\n typeof candidate.description === 'string' &&\n candidate.inputSchema !== undefined &&\n typeof candidate.handler === 'function'\n )\n}\n\nasync function loadGeneratedTools(): Promise<{ moduleId: string; tools: AiToolDefinition[] }[]> {\n const tsPath = findGeneratedFile('ai-tools.generated.ts')\n if (!tsPath) {\n throw new Error(\n 'Could not locate apps/<app>/.mercato/generated/ai-tools.generated.ts. Run `yarn generate` first.',\n )\n }\n await ensureApiRouteManifestsRegistered()\n const mod = (await compileAndImportGenerated(tsPath)) as RawAiToolsModule\n const entries = mod.aiToolConfigEntriesRaw ?? mod.aiToolConfigEntries ?? []\n const result: { moduleId: string; tools: AiToolDefinition[] }[] = []\n for (const entry of entries) {\n if (!entry || typeof entry.moduleId !== 'string') continue\n const tools = Array.isArray(entry.tools)\n ? entry.tools.filter(isToolDefinition)\n : []\n result.push({ moduleId: entry.moduleId, tools })\n }\n return result\n}\n\nexport async function pickDefaultTenant(\n container: AwilixContainer,\n): Promise<{ tenantId: string; organizationId: string | null; userId: string | null } | null> {\n try {\n const em = container.resolve<{\n getConnection: () => {\n execute: (sql: string, params?: unknown[]) => Promise<Record<string, unknown>[]>\n }\n }>('em') as unknown as {\n getConnection: () => {\n execute: (sql: string, params?: unknown[]) => Promise<Record<string, unknown>[]>\n }\n }\n const conn = em.getConnection()\n const tenantRows = await conn.execute(\n `SELECT id FROM tenants WHERE deleted_at IS NULL ORDER BY created_at ASC LIMIT 1`,\n )\n const tenantId =\n Array.isArray(tenantRows) && tenantRows[0]\n ? String((tenantRows[0] as Record<string, unknown>).id)\n : null\n if (!tenantId) return null\n const orgRows = await conn.execute(\n `SELECT id FROM organizations WHERE tenant_id = ? AND deleted_at IS NULL ORDER BY created_at ASC LIMIT 1`,\n [tenantId],\n )\n const organizationId =\n Array.isArray(orgRows) && orgRows[0]\n ? String((orgRows[0] as Record<string, unknown>).id)\n : null\n const userRows = await conn.execute(\n `SELECT id FROM users WHERE tenant_id = ? AND deleted_at IS NULL ORDER BY created_at ASC LIMIT 1`,\n [tenantId],\n )\n const userId =\n Array.isArray(userRows) && userRows[0]\n ? String((userRows[0] as Record<string, unknown>).id)\n : null\n return { tenantId, organizationId, userId }\n } catch (error) {\n if (process.env.OM_TOOL_TEST_DEBUG === '1') {\n console.warn('[tool-test-runner] pickDefaultTenant failed:', error)\n }\n return null\n }\n}\n\nfunction buildSuperAdminContext(\n container: AwilixContainer,\n tenantId: string | null,\n organizationId: string | null,\n userId: string | null,\n): McpToolContext {\n return {\n tenantId,\n organizationId,\n userId,\n container,\n userFeatures: ['*'],\n isSuperAdmin: true,\n }\n}\n\nfunction clipPreview(value: unknown): unknown {\n try {\n const json = JSON.stringify(value)\n if (json.length <= 400) return value\n return `${json.slice(0, 400)}\u2026(truncated, ${json.length} bytes)`\n } catch {\n return '[unserializable]'\n }\n}\n\nfunction dummyAgentForTool(tool: AiToolDefinition): AiAgentDefinition {\n // The runner never registers this agent; it's only used as input to\n // `prepareMutation` for shape compatibility. The id namespace is namespaced\n // under `__test__` so it cannot collide with real agents.\n const policy: AiAgentMutationPolicy = 'destructive-confirm-required'\n return {\n id: `__test__.${tool.name}`,\n moduleId: tool.name.split('.')[0] ?? '__test__',\n label: 'Tool Test Runner',\n description: 'Synthetic agent used by the tool test runner',\n systemPrompt: '',\n allowedTools: [tool.name],\n readOnly: false,\n mutationPolicy: policy,\n } as AiAgentDefinition\n}\n\nasync function executeReadTool(\n tool: AiToolDefinition,\n args: Record<string, unknown>,\n container: AwilixContainer,\n ctx: McpToolContext,\n): Promise<unknown> {\n // Mirror the dispatcher: resolve a fresh container per call so EM identity\n // map state is clean between tools.\n const fresh = await createRequestContainer()\n const freshCtx: McpToolContext = { ...ctx, container: fresh, tool }\n void container // keep arg for future use\n return tool.handler(args as never, freshCtx)\n}\n\nasync function executeMutationTool(\n tool: AiToolDefinition,\n args: Record<string, unknown>,\n container: AwilixContainer,\n ctx: McpToolContext,\n): Promise<unknown> {\n // Route through prepareMutation so we assert the approval contract still\n // holds \u2014 the handler must NEVER write directly. We pass a destructive\n // policy so the runtime treats the call as a confirmation candidate and\n // creates a pending-action row.\n const fresh = await createRequestContainer()\n const agent = dummyAgentForTool(tool)\n const userId = ctx.userId ?? '00000000-0000-0000-0000-000000000000'\n const { uiPart, pendingAction } = await prepareMutation(\n {\n agent,\n tool,\n toolCallArgs: args,\n conversationId: null,\n mutationPolicyOverride: null,\n },\n {\n tenantId: ctx.tenantId,\n organizationId: ctx.organizationId,\n userId,\n features: ctx.userFeatures,\n isSuperAdmin: ctx.isSuperAdmin,\n container: fresh,\n },\n )\n // Exercise the actual handler via the same path the production confirm\n // route uses. Without this, mutation tools whose handler crashes (e.g.\n // missing `ctx.tool` for the API operation runner) would be reported as\n // passing because `prepareMutation` never invokes the handler.\n // `prepareMutation` already enforced `tenantId` is non-null above; cast\n // here for the stricter `PendingActionExecuteContext` shape.\n const confirmTenantId = ctx.tenantId as string\n const confirmContainer = await createRequestContainer()\n const confirmation = await executePendingActionConfirm({\n action: pendingAction,\n agent,\n tool,\n ctx: {\n tenantId: confirmTenantId,\n organizationId: ctx.organizationId,\n userId,\n container: confirmContainer,\n userFeatures: ctx.userFeatures,\n isSuperAdmin: ctx.isSuperAdmin,\n },\n emitEvent: async () => {},\n })\n if (!confirmation.ok) {\n const error = (confirmation.executionResult as { error?: { message?: string } } | undefined)?.error\n const cause = confirmation.cause\n const causeMessage =\n cause instanceof Error\n ? cause.message\n : typeof cause === 'string'\n ? cause\n : null\n const message = error?.message ?? causeMessage ?? 'handler invocation failed'\n throw new Error(message)\n }\n return {\n status: 'pending-confirmation',\n pendingActionId: pendingAction.id,\n expiresAt: pendingAction.expiresAt.toISOString(),\n uiPartType: (uiPart as { type?: string } | undefined)?.type ?? null,\n handlerExecuted: true,\n }\n}\n\nasync function resolveFixtureInput(\n fixture: ToolFixture,\n resolved: Map<string, AiToolDefinition>,\n container: AwilixContainer,\n ctx: McpToolContext,\n): Promise<{ ok: true; input: Record<string, unknown> } | { ok: false; reason: string }> {\n if (fixture.input) return { ok: true, input: { ...fixture.input } }\n if (!fixture.idFrom) {\n return { ok: false, reason: 'fixture has neither input nor idFrom' }\n }\n const sourceTool = resolved.get(fixture.idFrom)\n if (!sourceTool) {\n return { ok: false, reason: `idFrom tool not found: ${fixture.idFrom}` }\n }\n const sourceFixture = getFixture(fixture.idFrom)\n if (!sourceFixture?.input) {\n return {\n ok: false,\n reason: `idFrom source ${fixture.idFrom} has no static input fixture`,\n }\n }\n let listResult: unknown\n try {\n listResult = await executeReadTool(\n sourceTool,\n { ...sourceFixture.input },\n container,\n ctx,\n )\n } catch (error) {\n return {\n ok: false,\n reason: `idFrom source ${fixture.idFrom} threw: ${\n error instanceof Error ? error.message : String(error)\n }`,\n }\n }\n const records = extractRecords(listResult)\n if (!records.length) {\n return {\n ok: false,\n reason: `idFrom source ${fixture.idFrom} returned no records`,\n }\n }\n const idField = 'id'\n const id = (records[0] as Record<string, unknown>)[idField]\n if (typeof id !== 'string' || !id) {\n return {\n ok: false,\n reason: `idFrom source ${fixture.idFrom} first record has no string id`,\n }\n }\n const bindAs = fixture.bindAs ?? 'id'\n return {\n ok: true,\n input: { [bindAs]: id, ...(fixture.extra ?? {}) },\n }\n}\n\nfunction extractRecords(value: unknown): unknown[] {\n if (!value || typeof value !== 'object') return []\n const candidate = value as Record<string, unknown>\n for (const key of ['records', 'items', 'people', 'companies', 'deals', 'results', 'data']) {\n const arr = candidate[key]\n if (Array.isArray(arr)) return arr\n }\n return []\n}\n\nexport interface RunToolTestsOptions {\n tenantId?: string | null\n organizationId?: string | null\n userId?: string | null\n moduleFilter?: string | null\n includeMutations?: boolean\n}\n\nexport async function runToolTests(\n options: RunToolTestsOptions = {},\n): Promise<ToolTestReport> {\n const includeMutations = options.includeMutations ?? true\n const container = await createRequestContainer()\n let tenantId: string | null = options.tenantId ?? null\n let organizationId: string | null = options.organizationId ?? null\n let userId: string | null = options.userId ?? null\n if (!tenantId) {\n const picked = await pickDefaultTenant(container)\n if (picked) {\n tenantId = picked.tenantId\n organizationId = picked.organizationId\n userId = userId ?? picked.userId\n }\n }\n const ctx = buildSuperAdminContext(container, tenantId, organizationId, userId)\n const grouped = await loadGeneratedTools()\n\n const flat: { moduleId: string; tool: AiToolDefinition }[] = []\n for (const group of grouped) {\n if (options.moduleFilter && group.moduleId !== options.moduleFilter) continue\n for (const tool of group.tools) flat.push({ moduleId: group.moduleId, tool })\n }\n const resolved = new Map<string, AiToolDefinition>()\n for (const { tool } of flat) resolved.set(tool.name, tool)\n\n const records: ToolTestRecord[] = []\n for (const { moduleId, tool } of flat) {\n const start = Date.now()\n const isMutation = tool.isMutation === true\n const fixture = getFixture(tool.name)\n if (!fixture) {\n records.push({\n module: moduleId,\n tool: tool.name,\n isMutation,\n status: 'skip',\n durationMs: Date.now() - start,\n reason: 'no fixture',\n })\n continue\n }\n if (fixture.skip) {\n records.push({\n module: moduleId,\n tool: tool.name,\n isMutation,\n status: 'skip',\n durationMs: Date.now() - start,\n reason: fixture.note ?? 'skip-by-fixture',\n })\n continue\n }\n if (isMutation && !includeMutations) {\n records.push({\n module: moduleId,\n tool: tool.name,\n isMutation,\n status: 'skip',\n durationMs: Date.now() - start,\n reason: 'mutations excluded',\n })\n continue\n }\n const inputResult = await resolveFixtureInput(fixture, resolved, container, ctx)\n if (!inputResult.ok) {\n records.push({\n module: moduleId,\n tool: tool.name,\n isMutation,\n status: 'skip',\n durationMs: Date.now() - start,\n reason: inputResult.reason,\n })\n continue\n }\n try {\n const result = isMutation\n ? await executeMutationTool(tool, inputResult.input, container, ctx)\n : await executeReadTool(tool, inputResult.input, container, ctx)\n // Result must be JSON-serializable.\n JSON.stringify(result)\n // Mutation tools must return a pending-confirmation envelope.\n if (\n isMutation &&\n (typeof result !== 'object' ||\n result === null ||\n (result as Record<string, unknown>).status !== 'pending-confirmation')\n ) {\n records.push({\n module: moduleId,\n tool: tool.name,\n isMutation,\n status: 'fail',\n durationMs: Date.now() - start,\n reason: 'mutation tool did not route through prepareMutation (no pending-confirmation envelope)',\n resultPreview: clipPreview(result),\n })\n continue\n }\n records.push({\n module: moduleId,\n tool: tool.name,\n isMutation,\n status: 'pass',\n durationMs: Date.now() - start,\n resultPreview: clipPreview(result),\n })\n } catch (error) {\n records.push({\n module: moduleId,\n tool: tool.name,\n isMutation,\n status: 'fail',\n durationMs: Date.now() - start,\n reason: error instanceof Error ? error.message : String(error),\n })\n }\n }\n const passed = records.filter((r) => r.status === 'pass').length\n const failed = records.filter((r) => r.status === 'fail').length\n const skipped = records.filter((r) => r.status === 'skip').length\n return {\n tenantId,\n organizationId,\n total: records.length,\n passed,\n failed,\n skipped,\n records,\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.5-develop.5240.2.bbd9d30275",
3
+ "version": "0.6.5-develop.5266.2.9acdca82ec",
4
4
  "type": "module",
5
5
  "engines": {
6
6
  "node": ">=22.0.0"
@@ -98,16 +98,16 @@
98
98
  "zod-to-json-schema": "^3.25.2"
99
99
  },
100
100
  "peerDependencies": {
101
- "@open-mercato/shared": "0.6.5-develop.5240.2.bbd9d30275",
102
- "@open-mercato/ui": "0.6.5-develop.5240.2.bbd9d30275",
101
+ "@open-mercato/shared": "0.6.5-develop.5266.2.9acdca82ec",
102
+ "@open-mercato/ui": "0.6.5-develop.5266.2.9acdca82ec",
103
103
  "react": "^19.0.0",
104
104
  "react-dom": "^19.0.0",
105
105
  "zod": ">=3.23.0"
106
106
  },
107
107
  "devDependencies": {
108
- "@open-mercato/cli": "0.6.5-develop.5240.2.bbd9d30275",
109
- "@open-mercato/shared": "0.6.5-develop.5240.2.bbd9d30275",
110
- "@open-mercato/ui": "0.6.5-develop.5240.2.bbd9d30275",
108
+ "@open-mercato/cli": "0.6.5-develop.5266.2.9acdca82ec",
109
+ "@open-mercato/shared": "0.6.5-develop.5266.2.9acdca82ec",
110
+ "@open-mercato/ui": "0.6.5-develop.5266.2.9acdca82ec",
111
111
  "@types/react": "^19.2.17",
112
112
  "@types/react-dom": "^19.2.3",
113
113
  "react": "19.2.7",
@@ -0,0 +1,140 @@
1
+ import fs from 'node:fs'
2
+ import os from 'node:os'
3
+ import path from 'node:path'
4
+ import { pathToFileURL } from 'node:url'
5
+ import * as registry from '@open-mercato/shared/modules/registry'
6
+ import {
7
+ rewriteGeneratedAliasImports,
8
+ findGeneratedFile,
9
+ ensureApiRouteManifestsRegistered,
10
+ } from '../generated-registry-loader'
11
+
12
+ function makeTempDir(): string {
13
+ return fs.mkdtempSync(path.join(os.tmpdir(), 'om-gen-loader-'))
14
+ }
15
+
16
+ describe('rewriteGeneratedAliasImports', () => {
17
+ // Regression for the MCP dev-server crash:
18
+ // "Cannot find package '@/.mercato' imported from .../tool-loader.js"
19
+ // Generated registries use the Next.js `@/` alias, which a standalone Node
20
+ // process cannot resolve. The rewrite turns it into an absolute file URL.
21
+
22
+ it('rewrites a static `from "@/..."` import to an absolute file URL', () => {
23
+ const appRoot = makeTempDir()
24
+ const out = rewriteGeneratedAliasImports(
25
+ `import { x } from '@/.mercato/generated/entities'`,
26
+ appRoot,
27
+ )
28
+ const expectedUrl = pathToFileURL(
29
+ path.join(appRoot, '.mercato/generated/entities'),
30
+ ).href
31
+ expect(out).toBe(`import { x } from ${JSON.stringify(expectedUrl)}`)
32
+ expect(out).not.toContain('@/')
33
+ })
34
+
35
+ it('rewrites a dynamic `import("@/...")` call to an absolute file URL', () => {
36
+ const appRoot = makeTempDir()
37
+ const out = rewriteGeneratedAliasImports(
38
+ `const tools = () => import("@/.mercato/generated/ai-tools.generated")`,
39
+ appRoot,
40
+ )
41
+ const expectedUrl = pathToFileURL(
42
+ path.join(appRoot, '.mercato/generated/ai-tools.generated'),
43
+ ).href
44
+ expect(out).toBe(`const tools = () => import(${JSON.stringify(expectedUrl)})`)
45
+ expect(out).not.toContain('@/')
46
+ })
47
+
48
+ it('appends `.ts` when the aliased target exists only as a TypeScript file', () => {
49
+ const appRoot = makeTempDir()
50
+ fs.mkdirSync(path.join(appRoot, 'src'), { recursive: true })
51
+ fs.writeFileSync(path.join(appRoot, 'src', 'thing.ts'), 'export const a = 1\n')
52
+
53
+ const out = rewriteGeneratedAliasImports(`import x from '@/src/thing'`, appRoot)
54
+
55
+ const expectedUrl = pathToFileURL(path.join(appRoot, 'src', 'thing.ts')).href
56
+ expect(out).toBe(`import x from ${JSON.stringify(expectedUrl)}`)
57
+ })
58
+
59
+ it('leaves non-alias imports untouched', () => {
60
+ const source = [
61
+ `import { z } from 'zod'`,
62
+ `import x from './local'`,
63
+ `import y from '@open-mercato/shared/x'`,
64
+ ].join('\n')
65
+ expect(rewriteGeneratedAliasImports(source, makeTempDir())).toBe(source)
66
+ })
67
+ })
68
+
69
+ describe('findGeneratedFile', () => {
70
+ let cwdSpy: jest.SpyInstance
71
+
72
+ afterEach(() => {
73
+ cwdSpy?.mockRestore()
74
+ })
75
+
76
+ it('returns null when the file exists nowhere on the search path', () => {
77
+ // A name that cannot exist anywhere up the tree or under cwd.
78
+ expect(findGeneratedFile('definitely-not-real-xyz.generated.ts')).toBeNull()
79
+ })
80
+
81
+ it('locates the file under <cwd>/.mercato/generated (standalone app layout)', () => {
82
+ const appRoot = makeTempDir()
83
+ const generatedDir = path.join(appRoot, '.mercato', 'generated')
84
+ fs.mkdirSync(generatedDir, { recursive: true })
85
+ const target = path.join(generatedDir, 'unique-standalone.generated.ts')
86
+ fs.writeFileSync(target, 'export const apiRoutes = []\n')
87
+ cwdSpy = jest.spyOn(process, 'cwd').mockReturnValue(appRoot)
88
+
89
+ expect(findGeneratedFile('unique-standalone.generated.ts')).toBe(target)
90
+ })
91
+
92
+ it('locates the file under <cwd>/apps/mercato/.mercato/generated (monorepo layout)', () => {
93
+ const root = makeTempDir()
94
+ const generatedDir = path.join(root, 'apps', 'mercato', '.mercato', 'generated')
95
+ fs.mkdirSync(generatedDir, { recursive: true })
96
+ const target = path.join(generatedDir, 'unique-monorepo.generated.ts')
97
+ fs.writeFileSync(target, 'export const apiRoutes = []\n')
98
+ cwdSpy = jest.spyOn(process, 'cwd').mockReturnValue(root)
99
+
100
+ expect(findGeneratedFile('unique-monorepo.generated.ts')).toBe(target)
101
+ })
102
+
103
+ it('prefers the apps/mercato layout over the cwd-root layout when both exist', () => {
104
+ const root = makeTempDir()
105
+ const appsDir = path.join(root, 'apps', 'mercato', '.mercato', 'generated')
106
+ const rootDir = path.join(root, '.mercato', 'generated')
107
+ fs.mkdirSync(appsDir, { recursive: true })
108
+ fs.mkdirSync(rootDir, { recursive: true })
109
+ const fileName = 'unique-both.generated.ts'
110
+ fs.writeFileSync(path.join(appsDir, fileName), 'export const apiRoutes = []\n')
111
+ fs.writeFileSync(path.join(rootDir, fileName), 'export const apiRoutes = []\n')
112
+ cwdSpy = jest.spyOn(process, 'cwd').mockReturnValue(root)
113
+
114
+ expect(findGeneratedFile(fileName)).toBe(path.join(appsDir, fileName))
115
+ })
116
+ })
117
+
118
+ describe('ensureApiRouteManifestsRegistered', () => {
119
+ // Regression for "Operation runner manifest unavailable: No API route manifest
120
+ // registered" when calling API-backed module tools over MCP. The standalone
121
+ // MCP servers must register the manifest, but must NOT clobber the in-app
122
+ // agents-framework manifest registered at bootstrap.
123
+
124
+ it('is a no-op when a manifest is already registered (does not interfere with bootstrap)', async () => {
125
+ const getSpy = jest
126
+ .spyOn(registry, 'getApiRouteManifests')
127
+ .mockReturnValue([{ id: 'existing' }] as never)
128
+ const registerSpy = jest
129
+ .spyOn(registry, 'registerApiRouteManifests')
130
+ .mockImplementation(() => {})
131
+
132
+ const count = await ensureApiRouteManifestsRegistered()
133
+
134
+ expect(count).toBe(1)
135
+ expect(registerSpy).not.toHaveBeenCalled()
136
+
137
+ getSpy.mockRestore()
138
+ registerSpy.mockRestore()
139
+ })
140
+ })
@@ -8,10 +8,38 @@ import {
8
8
  type AiAgentOverrideConfigEntry,
9
9
  } from './ai-overrides'
10
10
  import { TASK_PLAN_TOOL_NAME } from './task-plan-labels'
11
+ import { findGeneratedFile, compileAndImportGenerated } from './generated-registry-loader'
11
12
 
12
13
  const agentsById = new Map<string, AiAgentDefinition>()
13
14
  let loaded = false
14
15
 
16
+ /**
17
+ * Import the generated `ai-agents.generated.ts` registry.
18
+ *
19
+ * Dual-strategy, mirroring `tool-loader`'s `importGeneratedAiToolsModule`:
20
+ * 1. Prefer the `@/` path-alias import — the Next.js bundler resolves it at
21
+ * build time, so the in-app agent runtime is unchanged.
22
+ * 2. Fall back to locating + compiling the file from disk when the alias
23
+ * import throws. That only happens in a standalone Node process (the
24
+ * `mcp:dev` / `mcp:serve` MCP servers), where `@/` is not a real package
25
+ * specifier and Node throws `ERR_MODULE_NOT_FOUND`. Without this the
26
+ * `meta.list_agents` / `meta.describe_agent` tools return an empty agent
27
+ * registry over MCP.
28
+ *
29
+ * Returns `null` when no generated file exists (pre-generate builds, tests).
30
+ */
31
+ async function importGeneratedAiAgentsModule(): Promise<Record<string, unknown> | null> {
32
+ try {
33
+ return (await import(
34
+ '@/.mercato/generated/ai-agents.generated'
35
+ )) as Record<string, unknown>
36
+ } catch {
37
+ const tsPath = findGeneratedFile('ai-agents.generated.ts')
38
+ if (!tsPath) return null
39
+ return compileAndImportGenerated(tsPath)
40
+ }
41
+ }
42
+
15
43
  function isStringArray(value: unknown): value is string[] {
16
44
  return Array.isArray(value) && value.every((item) => typeof item === 'string')
17
45
  }
@@ -170,10 +198,10 @@ function populateFromAgents(agents: unknown[]): void {
170
198
 
171
199
  async function loadOverrideEntries(): Promise<AiAgentOverrideConfigEntry[]> {
172
200
  try {
173
- const mod = (await import(
174
- '@/.mercato/generated/ai-agents.generated'
175
- )) as { aiAgentOverrideEntries?: unknown[] }
176
- return Array.isArray(mod.aiAgentOverrideEntries)
201
+ const mod = (await importGeneratedAiAgentsModule()) as {
202
+ aiAgentOverrideEntries?: unknown[]
203
+ } | null
204
+ return mod && Array.isArray(mod.aiAgentOverrideEntries)
177
205
  ? (mod.aiAgentOverrideEntries as AiAgentOverrideConfigEntry[])
178
206
  : []
179
207
  } catch {
@@ -196,12 +224,13 @@ function applyOverridesToRegistry(entries: readonly AiAgentOverrideConfigEntry[]
196
224
 
197
225
  async function loadExtensionEntries(): Promise<AiAgentExtension[]> {
198
226
  try {
199
- const mod = (await import(
200
- '@/.mercato/generated/ai-agents.generated'
201
- )) as { aiAgentExtensionEntries?: unknown[] }
202
- const entries = Array.isArray(mod.aiAgentExtensionEntries)
203
- ? (mod.aiAgentExtensionEntries as AiAgentExtensionConfigEntry[])
204
- : []
227
+ const mod = (await importGeneratedAiAgentsModule()) as {
228
+ aiAgentExtensionEntries?: unknown[]
229
+ } | null
230
+ const entries =
231
+ mod && Array.isArray(mod.aiAgentExtensionEntries)
232
+ ? (mod.aiAgentExtensionEntries as AiAgentExtensionConfigEntry[])
233
+ : []
205
234
  return composeAgentExtensionEntries(entries).filter(isAiAgentExtension)
206
235
  } catch {
207
236
  return []
@@ -249,10 +278,10 @@ function applyExtensionsToRegistry(extensions: readonly AiAgentExtension[]): voi
249
278
  export async function loadAgentRegistry(): Promise<void> {
250
279
  if (loaded) return
251
280
  try {
252
- const mod = (await import(
253
- '@/.mercato/generated/ai-agents.generated'
254
- )) as { allAiAgents?: unknown[] }
255
- const agents = Array.isArray(mod.allAiAgents) ? mod.allAiAgents : []
281
+ const mod = (await importGeneratedAiAgentsModule()) as {
282
+ allAiAgents?: unknown[]
283
+ } | null
284
+ const agents = mod && Array.isArray(mod.allAiAgents) ? mod.allAiAgents : []
256
285
  populateFromAgents(agents)
257
286
  } catch (error) {
258
287
  console.error(
@@ -0,0 +1,161 @@
1
+ /**
2
+ * Runtime loader for `.mercato/generated/*.generated.ts` registry files.
3
+ *
4
+ * The generated registries import their entries through the `@/` path alias
5
+ * (e.g. `@/.mercato/generated/ai-tools.generated`). That alias is only
6
+ * understood by the Next.js bundler — in a standalone Node process (the
7
+ * `mcp:dev` / `mcp:serve` MCP servers, the CLI tool-test runner) a raw
8
+ * `import('@/.mercato/...')` throws `ERR_MODULE_NOT_FOUND: Cannot find
9
+ * package '@/.mercato'` because Node treats `@/` as a package specifier.
10
+ *
11
+ * These helpers locate the generated `.ts` file on disk and compile-and-import
12
+ * it with esbuild (transpile-only), rewriting `@/` aliases to absolute paths.
13
+ * This mirrors `loadBootstrapData` in
14
+ * `@open-mercato/shared/lib/bootstrap/dynamicLoader` and works in both the
15
+ * monorepo and standalone apps.
16
+ */
17
+ import path from 'node:path'
18
+ import fs from 'node:fs'
19
+ import { fileURLToPath, pathToFileURL } from 'node:url'
20
+
21
+ /**
22
+ * Locate a generated registry file (e.g. `ai-tools.generated.ts`) without
23
+ * hardcoding the workspace layout. Searches upward from this module's compiled
24
+ * location for a `apps/mercato/.mercato/generated/<fileName>` (monorepo), then
25
+ * falls back to cwd-relative lookups (standalone apps run from the app dir).
26
+ */
27
+ export function findGeneratedFile(fileName: string): string | null {
28
+ const here = (() => {
29
+ try {
30
+ return fileURLToPath(import.meta.url)
31
+ } catch {
32
+ return null
33
+ }
34
+ })()
35
+
36
+ if (here) {
37
+ let cursor = path.dirname(here)
38
+ for (let i = 0; i < 12; i++) {
39
+ const candidate = path.join(cursor, 'apps', 'mercato', '.mercato', 'generated', fileName)
40
+ if (fs.existsSync(candidate)) return candidate
41
+ const next = path.dirname(cursor)
42
+ if (next === cursor) break
43
+ cursor = next
44
+ }
45
+ }
46
+ // Fallbacks: cwd-based lookup (CLI invoked from apps/mercato, or a standalone
47
+ // app whose root holds `.mercato/generated`).
48
+ const fromCwd = path.resolve(process.cwd(), 'apps', 'mercato', '.mercato', 'generated', fileName)
49
+ if (fs.existsSync(fromCwd)) return fromCwd
50
+ const fromCwdDirect = path.resolve(process.cwd(), '.mercato', 'generated', fileName)
51
+ if (fs.existsSync(fromCwdDirect)) return fromCwdDirect
52
+ return null
53
+ }
54
+
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.
60
+ *
61
+ * Transpile-only (no bundling): the generated registries declare an array
62
+ * literal whose entries are static `import("…")` arrow functions — we want
63
+ * those `import()` strings to stay as runtime imports so Node resolves them
64
+ * lazily through the workspace's normal module resolution. Eagerly bundling
65
+ * pulls Next.js / route-handler internals into the `.mjs` and breaks at runtime
66
+ * (e.g. `next/server` package-exports map).
67
+ */
68
+ export async function compileAndImportGenerated(tsPath: string): Promise<Record<string, unknown>> {
69
+ const jsPath = tsPath.replace(/\.ts$/, '.mjs')
70
+ // appRoot is two directories up from `.mercato/generated/<file>.ts`.
71
+ const appRoot = path.dirname(path.dirname(path.dirname(tsPath)))
72
+
73
+ if (!fs.existsSync(tsPath)) {
74
+ throw new Error(`Generated file not found: ${tsPath}`)
75
+ }
76
+
77
+ const jsExists = fs.existsSync(jsPath)
78
+ const needsCompile =
79
+ !jsExists || fs.statSync(tsPath).mtimeMs > fs.statSync(jsPath).mtimeMs
80
+
81
+ if (needsCompile) {
82
+ const esbuild = await import('esbuild')
83
+ const tsSource = fs.readFileSync(tsPath, 'utf-8')
84
+ const aliasRewritten = rewriteGeneratedAliasImports(tsSource, appRoot)
85
+ const result = await esbuild.transform(aliasRewritten, {
86
+ loader: 'ts',
87
+ format: 'esm',
88
+ target: 'node18',
89
+ sourcemap: false,
90
+ sourcefile: tsPath,
91
+ })
92
+ fs.writeFileSync(jsPath, result.code)
93
+ }
94
+
95
+ return (await import(pathToFileURL(jsPath).href)) as Record<string, unknown>
96
+ }
97
+
98
+ /**
99
+ * Rewrite `@/...` path-alias imports (both `from "@/x"` and dynamic
100
+ * `import("@/x")`) in generated-registry source to absolute `file://` URLs
101
+ * rooted at `appRoot`. The `@/` alias is a Next.js bundler convention; outside
102
+ * the bundler Node treats `@/...` as a bare package specifier and throws
103
+ * `ERR_MODULE_NOT_FOUND`. Exported for unit testing.
104
+ */
105
+ export function rewriteGeneratedAliasImports(source: string, appRoot: string): string {
106
+ const resolveAlias = (relativePath: string): string => {
107
+ const target = path.join(appRoot, relativePath)
108
+ const candidate = fs.existsSync(target)
109
+ ? target
110
+ : fs.existsSync(target + '.ts')
111
+ ? target + '.ts'
112
+ : target
113
+ return pathToFileURL(candidate).href
114
+ }
115
+ return source
116
+ .replace(/from\s+["']@\/([^"']+)["']/g, (_match, relativePath: string) => {
117
+ return `from ${JSON.stringify(resolveAlias(relativePath))}`
118
+ })
119
+ .replace(/import\s*\(\s*["']@\/([^"']+)["']\s*\)/g, (_match, relativePath: string) => {
120
+ return `import(${JSON.stringify(resolveAlias(relativePath))})`
121
+ })
122
+ }
123
+
124
+ /**
125
+ * Compile-and-import `api-routes.generated.ts` and register its manifest with
126
+ * the shared registry. Many module tools are "API-backed" — their handlers
127
+ * delegate to `createAiApiOperationRunner`, which fails closed with
128
+ * "No API route manifest registered" unless the manifest is present. In the
129
+ * Next.js app this is wired at bootstrap, but the standalone MCP servers
130
+ * (`mcp:dev` / `mcp:serve`) bootstrap DI without it, so we register it here.
131
+ *
132
+ * Idempotent: `registerApiRouteManifests` replaces the stored manifest, so
133
+ * calling this repeatedly (e.g. per-request HTTP handlers) is safe. Returns the
134
+ * number of registered routes (0 when the generated file is absent).
135
+ */
136
+ export async function ensureApiRouteManifestsRegistered(): Promise<number> {
137
+ const registry = await import('@open-mercato/shared/modules/registry')
138
+ // Already wired (e.g. the Next.js app bootstrap, or a prior call). Leave the
139
+ // existing manifest untouched so we never interfere with the in-app agents
140
+ // framework, which registers it at bootstrap with its own override pipeline.
141
+ const existing = registry.getApiRouteManifests()
142
+ if (existing.length > 0) return existing.length
143
+
144
+ const tsPath = findGeneratedFile('api-routes.generated.ts')
145
+ if (!tsPath) return 0
146
+ try {
147
+ const mod = await compileAndImportGenerated(tsPath)
148
+ const apiRoutes = (mod as { apiRoutes?: unknown }).apiRoutes
149
+ if (!Array.isArray(apiRoutes)) return 0
150
+ registry.registerApiRouteManifests(
151
+ apiRoutes as Parameters<typeof registry.registerApiRouteManifests>[0],
152
+ )
153
+ return apiRoutes.length
154
+ } catch (error) {
155
+ console.warn(
156
+ '[MCP Tools] Could not register api-routes manifest:',
157
+ error instanceof Error ? error.message : error,
158
+ )
159
+ return 0
160
+ }
161
+ }