@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.
@@ -8,6 +8,11 @@ import {
8
8
  } from './ai-overrides'
9
9
  import type { McpToolDefinition, McpToolContext } from './types'
10
10
  import { ToolSearchService } from './tool-search'
11
+ import {
12
+ findGeneratedFile,
13
+ compileAndImportGenerated,
14
+ ensureApiRouteManifestsRegistered,
15
+ } from './generated-registry-loader'
11
16
 
12
17
  /**
13
18
  * Module tool definition as exported from ai-tools.ts files.
@@ -143,6 +148,33 @@ export function applyAiToolOverrideEntries(
143
148
  }
144
149
  }
145
150
 
151
+ /**
152
+ * Import the generated `ai-tools.generated.ts` registry.
153
+ *
154
+ * Dual-strategy so the same code works in every runtime without changing the
155
+ * existing Next.js / agents-framework behavior:
156
+ * 1. Prefer the `@/` path-alias import. Inside the Next.js bundler this
157
+ * resolves at build time exactly as before — the in-app agents framework
158
+ * path is untouched.
159
+ * 2. Fall back to locating + compiling the file from disk when the alias
160
+ * import throws. That only happens in a standalone Node process (the
161
+ * `mcp:dev` / `mcp:serve` MCP servers), where `@/` is not a real package
162
+ * specifier and Node throws `ERR_MODULE_NOT_FOUND`.
163
+ *
164
+ * Returns `null` when no generated file exists (pre-generate builds, tests).
165
+ */
166
+ async function importGeneratedAiToolsModule(): Promise<Record<string, unknown> | null> {
167
+ try {
168
+ return (await import(
169
+ '@/.mercato/generated/ai-tools.generated'
170
+ )) as Record<string, unknown>
171
+ } catch {
172
+ const tsPath = findGeneratedFile('ai-tools.generated.ts')
173
+ if (!tsPath) return null
174
+ return compileAndImportGenerated(tsPath)
175
+ }
176
+ }
177
+
146
178
  /**
147
179
  * Read `aiToolOverrideEntries` from `ai-tools.generated.ts` and apply
148
180
  * them on top of the live tool registry. Safe to call when the file is
@@ -152,12 +184,13 @@ export function applyAiToolOverrideEntries(
152
184
  export async function loadGeneratedAiToolOverrides(): Promise<void> {
153
185
  let entries: AiToolOverrideConfigEntry[] = []
154
186
  try {
155
- const mod = (await import(
156
- '@/.mercato/generated/ai-tools.generated'
157
- )) as { aiToolOverrideEntries?: unknown[] }
158
- entries = Array.isArray(mod.aiToolOverrideEntries)
159
- ? (mod.aiToolOverrideEntries as AiToolOverrideConfigEntry[])
160
- : []
187
+ const mod = (await importGeneratedAiToolsModule()) as {
188
+ aiToolOverrideEntries?: unknown[]
189
+ } | null
190
+ entries =
191
+ mod && Array.isArray(mod.aiToolOverrideEntries)
192
+ ? (mod.aiToolOverrideEntries as AiToolOverrideConfigEntry[])
193
+ : []
161
194
  } catch {
162
195
  // No override file generated.
163
196
  }
@@ -172,9 +205,13 @@ export async function loadGeneratedAiToolOverrides(): Promise<void> {
172
205
  */
173
206
  export async function loadGeneratedModuleAiTools(): Promise<number> {
174
207
  try {
175
- const mod = (await import(
176
- '@/.mercato/generated/ai-tools.generated'
177
- )) as { aiToolConfigEntries?: AiToolConfigEntry[] }
208
+ const mod = (await importGeneratedAiToolsModule()) as {
209
+ aiToolConfigEntries?: AiToolConfigEntry[]
210
+ } | null
211
+ if (!mod) {
212
+ // No generated file (pre-generate build or tests).
213
+ return 0
214
+ }
178
215
  const entries = Array.isArray(mod.aiToolConfigEntries)
179
216
  ? mod.aiToolConfigEntries
180
217
  : []
@@ -228,6 +265,20 @@ export async function loadAllModuleTools(): Promise<void> {
228
265
  } catch (error) {
229
266
  console.error('[MCP Tools] Could not load module AI tools:', error)
230
267
  }
268
+
269
+ // 4. Register the API route manifest so API-backed module tools can run.
270
+ // Their handlers delegate to createAiApiOperationRunner, which fails closed
271
+ // with "No API route manifest registered" outside the Next.js bootstrap.
272
+ try {
273
+ const routeCount = await ensureApiRouteManifestsRegistered()
274
+ if (routeCount > 0) {
275
+ console.error(
276
+ `[MCP Tools] Registered ${routeCount} API route manifests for API-backed tools`
277
+ )
278
+ }
279
+ } catch (error) {
280
+ console.error('[MCP Tools] Could not register API route manifests:', error)
281
+ }
231
282
  }
232
283
 
233
284
  /**
@@ -14,9 +14,6 @@
14
14
  * - Tools without an explicit fixture entry are skipped with reason
15
15
  * `'no fixture'` rather than failing.
16
16
  */
17
- import path from 'node:path'
18
- import fs from 'node:fs'
19
- import { fileURLToPath, pathToFileURL } from 'node:url'
20
17
  import type { AwilixContainer } from 'awilix'
21
18
  import { createRequestContainer } from '@open-mercato/shared/lib/di/container'
22
19
  import type { McpToolContext, AiToolDefinition } from './types'
@@ -24,6 +21,11 @@ import type { AiAgentDefinition, AiAgentMutationPolicy } from './ai-agent-defini
24
21
  import { getFixture, type ToolFixture } from './tool-test-fixtures'
25
22
  import { prepareMutation } from './prepare-mutation'
26
23
  import { executePendingActionConfirm } from './pending-action-executor'
24
+ import {
25
+ findGeneratedFile,
26
+ compileAndImportGenerated,
27
+ ensureApiRouteManifestsRegistered,
28
+ } from './generated-registry-loader'
27
29
 
28
30
  export type ToolTestStatus = 'pass' | 'fail' | 'skip'
29
31
 
@@ -63,143 +65,15 @@ function isToolDefinition(value: unknown): value is AiToolDefinition {
63
65
  )
64
66
  }
65
67
 
66
- /**
67
- * Locate `apps/mercato/.mercato/generated/ai-tools.generated.ts` without
68
- * hardcoding the workspace layout. Searches upward from this file's
69
- * compiled location; in the monorepo dist this is
70
- * `packages/ai-assistant/dist/...` so we walk up until we hit a directory
71
- * containing `apps/mercato/.mercato/generated`.
72
- */
73
- function findGeneratedAiToolsPath(): string | null {
74
- const here = (() => {
75
- try {
76
- return fileURLToPath(import.meta.url)
77
- } catch {
78
- return null
79
- }
80
- })()
81
- if (!here) return null
82
- let cursor = path.dirname(here)
83
- for (let i = 0; i < 12; i++) {
84
- const candidate = path.join(cursor, 'apps', 'mercato', '.mercato', 'generated', 'ai-tools.generated.ts')
85
- if (fs.existsSync(candidate)) return candidate
86
- const next = path.dirname(cursor)
87
- if (next === cursor) break
88
- cursor = next
89
- }
90
- // Fallback: cwd-based lookup (when CLI is invoked from apps/mercato).
91
- const fromCwd = path.resolve(process.cwd(), 'apps', 'mercato', '.mercato', 'generated', 'ai-tools.generated.ts')
92
- if (fs.existsSync(fromCwd)) return fromCwd
93
- const fromCwdDirect = path.resolve(process.cwd(), '.mercato', 'generated', 'ai-tools.generated.ts')
94
- if (fs.existsSync(fromCwdDirect)) return fromCwdDirect
95
- return null
96
- }
97
-
98
- /**
99
- * Compile-and-import `ai-tools.generated.ts` on the fly. Mirrors the approach
100
- * used by `loadBootstrapData` in `@open-mercato/shared/lib/bootstrap/dynamicLoader`:
101
- * resolves the `@/` alias to the app root, marks every other package import as
102
- * external, and emits a sibling `.mjs` we can `import()` from Node. Cached on
103
- * mtime so repeat runs in the same process don't recompile.
104
- */
105
- async function compileAndImportGenerated(tsPath: string): Promise<RawAiToolsModule> {
106
- const jsPath = tsPath.replace(/\.ts$/, '.mjs')
107
- // appRoot is two directories up from `.mercato/generated/<file>.ts`.
108
- const appRoot = path.dirname(path.dirname(path.dirname(tsPath)))
109
- const tsExists = fs.existsSync(tsPath)
110
- if (!tsExists) {
111
- throw new Error(`Generated file not found: ${tsPath}`)
112
- }
113
- const jsExists = fs.existsSync(jsPath)
114
- const needsCompile =
115
- !jsExists || fs.statSync(tsPath).mtimeMs > fs.statSync(jsPath).mtimeMs
116
- if (needsCompile) {
117
- const esbuild = await import('esbuild')
118
- // Transpile-only: don't bundle. Generated registry files only declare an
119
- // array literal whose entries are static `import("…")` arrow functions —
120
- // we want those `import()` strings to stay as runtime imports so Node
121
- // resolves them lazily through the workspace's normal module resolution.
122
- // Eagerly bundling them pulls Next.js / route handler internals into the
123
- // .mjs and breaks at runtime (e.g. `next/server` package-exports map).
124
- const tsSource = fs.readFileSync(tsPath, 'utf-8')
125
- // Rewrite `@/...` aliases to absolute paths so Node can resolve them.
126
- const aliasRewritten = tsSource.replace(
127
- /from\s+["']@\/([^"']+)["']/g,
128
- (_match, p1: string) => {
129
- const target = path.join(appRoot, p1)
130
- const candidate = fs.existsSync(target)
131
- ? target
132
- : fs.existsSync(target + '.ts')
133
- ? target + '.ts'
134
- : target
135
- return `from ${JSON.stringify(pathToFileURL(candidate).href)}`
136
- },
137
- ).replace(
138
- /import\s*\(\s*["']@\/([^"']+)["']\s*\)/g,
139
- (_match, p1: string) => {
140
- const target = path.join(appRoot, p1)
141
- const candidate = fs.existsSync(target)
142
- ? target
143
- : fs.existsSync(target + '.ts')
144
- ? target + '.ts'
145
- : target
146
- return `import(${JSON.stringify(pathToFileURL(candidate).href)})`
147
- },
148
- )
149
- const result = await esbuild.transform(aliasRewritten, {
150
- loader: 'ts',
151
- format: 'esm',
152
- target: 'node18',
153
- sourcemap: false,
154
- sourcefile: tsPath,
155
- })
156
- fs.writeFileSync(jsPath, result.code)
157
- }
158
- return (await import(pathToFileURL(jsPath).href)) as RawAiToolsModule
159
- }
160
-
161
- /**
162
- * Compile-and-import `api-routes.generated.ts` and register its manifest with
163
- * the shared registry. Many tool handlers delegate to `aiApiOperationRunner`
164
- * which fails closed when no manifest is registered. Idempotent: registering
165
- * the same array twice is safe.
166
- */
167
- async function ensureApiRouteManifestsRegistered(generatedDir: string): Promise<void> {
168
- const tsPath = path.join(generatedDir, 'api-routes.generated.ts')
169
- if (!fs.existsSync(tsPath)) return
170
- try {
171
- const mod = (await compileAndImportGenerated(tsPath)) as Record<string, unknown>
172
- const apiRoutes = (mod as { apiRoutes?: unknown }).apiRoutes
173
- if (!Array.isArray(apiRoutes)) {
174
- console.warn('[tool-test-runner] api-routes.generated.mjs returned no apiRoutes array')
175
- return
176
- }
177
- const registry = await import('@open-mercato/shared/modules/registry')
178
- registry.registerApiRouteManifests(
179
- apiRoutes as Parameters<typeof registry.registerApiRouteManifests>[0],
180
- )
181
- if (process.env.OM_TOOL_TEST_DEBUG === '1') {
182
- console.log(
183
- `[tool-test-runner] Registered ${apiRoutes.length} api-route manifests; getApiRouteManifests().length=${registry.getApiRouteManifests().length}`,
184
- )
185
- }
186
- } catch (error) {
187
- console.warn(
188
- '[tool-test-runner] Could not register api-routes manifest:',
189
- error instanceof Error ? error.message : error,
190
- )
191
- }
192
- }
193
-
194
68
  async function loadGeneratedTools(): Promise<{ moduleId: string; tools: AiToolDefinition[] }[]> {
195
- const tsPath = findGeneratedAiToolsPath()
69
+ const tsPath = findGeneratedFile('ai-tools.generated.ts')
196
70
  if (!tsPath) {
197
71
  throw new Error(
198
72
  'Could not locate apps/<app>/.mercato/generated/ai-tools.generated.ts. Run `yarn generate` first.',
199
73
  )
200
74
  }
201
- await ensureApiRouteManifestsRegistered(path.dirname(tsPath))
202
- const mod = await compileAndImportGenerated(tsPath)
75
+ await ensureApiRouteManifestsRegistered()
76
+ const mod = (await compileAndImportGenerated(tsPath)) as RawAiToolsModule
203
77
  const entries = mod.aiToolConfigEntriesRaw ?? mod.aiToolConfigEntries ?? []
204
78
  const result: { moduleId: string; tools: AiToolDefinition[] }[] = []
205
79
  for (const entry of entries) {