@open-mercato/ai-assistant 0.6.8-develop.6931.1.4a1e1c786b → 0.6.8-develop.6941.1.3c26284542
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/.turbo/turbo-build.log +1 -1
- package/AGENTS.md +9 -33
- package/dist/modules/ai_assistant/api/tools/route.js +2 -2
- package/dist/modules/ai_assistant/api/tools/route.js.map +2 -2
- package/dist/modules/ai_assistant/lib/generated-registry-loader.js +55 -10
- package/dist/modules/ai_assistant/lib/generated-registry-loader.js.map +2 -2
- package/dist/modules/ai_assistant/lib/in-process-client.js +2 -2
- package/dist/modules/ai_assistant/lib/in-process-client.js.map +2 -2
- package/dist/modules/ai_assistant/lib/mcp-server.js +2 -3
- package/dist/modules/ai_assistant/lib/mcp-server.js.map +2 -2
- package/dist/modules/ai_assistant/lib/tool-input-schema.js +13 -0
- package/dist/modules/ai_assistant/lib/tool-input-schema.js.map +7 -0
- package/dist/modules/ai_assistant/lib/tool-loader.js +16 -2
- package/dist/modules/ai_assistant/lib/tool-loader.js.map +2 -2
- package/dist/modules/ai_assistant/lib/tool-registry.js +1 -1
- package/dist/modules/ai_assistant/lib/tool-registry.js.map +2 -2
- package/package.json +6 -6
- package/src/modules/ai_assistant/api/tools/route.ts +2 -2
- package/src/modules/ai_assistant/lib/__tests__/generated-registry-loader.test.ts +128 -1
- package/src/modules/ai_assistant/lib/__tests__/tool-input-schema.test.ts +52 -0
- package/src/modules/ai_assistant/lib/__tests__/tool-loader.test.ts +65 -0
- package/src/modules/ai_assistant/lib/generated-registry-loader.ts +107 -12
- package/src/modules/ai_assistant/lib/in-process-client.ts +2 -2
- package/src/modules/ai_assistant/lib/mcp-server.ts +2 -3
- package/src/modules/ai_assistant/lib/tool-input-schema.ts +33 -0
- package/src/modules/ai_assistant/lib/tool-loader.ts +18 -1
- package/src/modules/ai_assistant/lib/tool-registry.ts +1 -1
package/.turbo/turbo-build.log
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
[build:ai-assistant] found
|
|
1
|
+
[build:ai-assistant] found 232 entry points
|
|
2
2
|
[build:ai-assistant] built successfully
|
package/AGENTS.md
CHANGED
|
@@ -1489,6 +1489,10 @@ Agents that need multi-step tool loops configure the `loop` block on `AiAgentDef
|
|
|
1489
1489
|
|
|
1490
1490
|
**Backward compatibility**: additive. With `MCP_SERVER_API_KEY_FILE` unset the OpenCode entrypoint behaves byte-for-byte as before; host-MCP mode (`yarn mcp:serve` + `MCP_SERVER_API_KEY`) is unchanged. Spec: `.ai/specs/2026-07-07-windows-one-command-agentic-dev-environment.md`.
|
|
1491
1491
|
|
|
1492
|
+
### 2026-08-05 - @app module entries are compiled, not raw TS
|
|
1493
|
+
|
|
1494
|
+
`compileAndImportGenerated` (`lib/generated-registry-loader.ts`) compiles every `@app` module entry a generated registry references into `<appRoot>/.mercato/generated/app-modules/`, via the new `compileAppSourceFile` in `shared/lib/bootstrap/dynamicLoader` (reuses `loadBootstrapData`'s esbuild bundle and dep cache; packages stay external). The 2026-06-24 fix below only rewrote the specifier to an absolute `.ts` path, which holds only while the target's graph stays inside Node's type stripping — a real module's does not (`./di` is extensionless, `./data/entities` has decorators), so the first app-local `ai-tools.ts` killed the tool registry. Uncompilable modules log and fall back to the raw path. Additive: the artifact map is an optional third `rewriteGeneratedAliasImports` arg.
|
|
1495
|
+
|
|
1492
1496
|
### 2026-06-24 - MCP dev server loads ai-tools for @app local modules (#3524)
|
|
1493
1497
|
|
|
1494
1498
|
**What changed** (`lib/generated-registry-loader.ts`):
|
|
@@ -1595,20 +1599,9 @@ Note: the guard is intentionally NOT added to `mcp-client.ts` `connectHttp`, whi
|
|
|
1595
1599
|
- Removed auto-discovered module AI tools from `ai-tools.generated.ts`
|
|
1596
1600
|
- Token savings: from ~10+ tool schemas to exactly 2, with fixed footprint regardless of API surface growth
|
|
1597
1601
|
|
|
1598
|
-
**Files created**:
|
|
1599
|
-
- `lib/codemode-tools.ts` — `search` and `execute` tool definitions
|
|
1600
|
-
- `lib/sandbox.ts` — `node:vm` sandbox executor with security restrictions
|
|
1601
|
-
- `lib/truncate.ts` — Response size limiter (40K chars / ~10K tokens)
|
|
1602
|
+
**Files created**: `lib/codemode-tools.ts` (the two tool definitions), `lib/sandbox.ts` (`node:vm` executor), `lib/truncate.ts` (40K-char response limiter) — all three described under "Rules for Code Mode Internals" above.
|
|
1602
1603
|
|
|
1603
|
-
**Files modified**:
|
|
1604
|
-
- `lib/api-endpoint-index.ts` — Added `getRawOpenApiSpec()` for raw spec caching
|
|
1605
|
-
- `lib/tool-loader.ts` — Loads Code Mode tools instead of legacy tools + module tools
|
|
1606
|
-
- `lib/http-server.ts` — Pre-caches raw OpenAPI spec at startup
|
|
1607
|
-
- `lib/mcp-server.ts` — Generates entity graph and caches spec for stdio mode
|
|
1608
|
-
|
|
1609
|
-
**Files kept but unused**:
|
|
1610
|
-
- `lib/api-discovery-tools.ts` — Old find_api/call_api (no longer imported, deleted in #1876)
|
|
1611
|
-
- `lib/entity-graph-tools.ts` — Old discover_schema (no longer imported, deleted in #1876)
|
|
1604
|
+
**Files modified**: `lib/api-endpoint-index.ts` (raw spec caching), `lib/tool-loader.ts`, `lib/http-server.ts`, `lib/mcp-server.ts`. The superseded `lib/api-discovery-tools.ts` and `lib/entity-graph-tools.ts` were later deleted in #1876.
|
|
1612
1605
|
|
|
1613
1606
|
### 2026-01-17 - Session Persistence Fix
|
|
1614
1607
|
|
|
@@ -1623,31 +1616,14 @@ Note: the guard is intentionally NOT added to `mcp-client.ts` `connectHttp`, whi
|
|
|
1623
1616
|
2. **React stale closure**: `handleSubmit` callback captured initial `null` sessionId value.
|
|
1624
1617
|
|
|
1625
1618
|
**Fixes applied**:
|
|
1626
|
-
- `opencode-handlers.ts`:
|
|
1627
|
-
- `useCommandPalette.ts`:
|
|
1628
|
-
|
|
1629
|
-
**Files modified**:
|
|
1630
|
-
- `src/modules/ai_assistant/lib/opencode-handlers.ts` - Fixed Promise.race completion bug
|
|
1631
|
-
- `src/frontend/hooks/useCommandPalette.ts` - Added ref pattern for sessionId
|
|
1619
|
+
- `lib/opencode-handlers.ts`: removed `Promise.race`, await only the SSE eventPromise
|
|
1620
|
+
- `src/frontend/hooks/useCommandPalette.ts`: added `opencodeSessionIdRef` alongside state to avoid stale closures
|
|
1632
1621
|
|
|
1633
1622
|
### 2026-01 - OpenCode Integration
|
|
1634
1623
|
|
|
1635
1624
|
**Lesson learned:** When replacing an AI backend, preserve the session management contract — the frontend depends on `sessionId` in `done` events regardless of the underlying AI engine.
|
|
1636
1625
|
|
|
1637
|
-
|
|
1638
|
-
|
|
1639
|
-
**What changed**:
|
|
1640
|
-
- Chat API now routes all requests to OpenCode
|
|
1641
|
-
- Added session management for conversation context
|
|
1642
|
-
- Added "Agent is working..." indicator
|
|
1643
|
-
- OpenCode connects to MCP server for tools
|
|
1644
|
-
- Removed direct AI provider integration
|
|
1645
|
-
|
|
1646
|
-
**Files modified**:
|
|
1647
|
-
- `src/modules/ai_assistant/api/chat/route.ts` - Complete rewrite to use OpenCode
|
|
1648
|
-
- `src/frontend/hooks/useCommandPalette.ts` - Added session state, thinking indicator
|
|
1649
|
-
- `src/frontend/components/CommandPalette/ToolChatPage.tsx` - Added thinking UI
|
|
1650
|
-
- `src/frontend/types.ts` - Added ChatSSEEvent, isThinking
|
|
1626
|
+
Replaced the Vercel AI SDK with OpenCode: the chat API routes to OpenCode, session management carries conversation context, and OpenCode reaches tools through the MCP server. Current shape: "Rules for the Chat Flow" and "Rules for Session Management" above; see git history for per-file detail.
|
|
1651
1627
|
|
|
1652
1628
|
### 2026-01 - API Discovery Tools / Hybrid Tool Discovery (superseded)
|
|
1653
1629
|
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import { createLogger } from "@open-mercato/shared/lib/logger";
|
|
2
2
|
import { NextResponse } from "next/server";
|
|
3
|
-
import { zodToJsonSchema } from "zod-to-json-schema";
|
|
4
3
|
import { getAuthFromRequest } from "@open-mercato/shared/lib/auth/server";
|
|
5
4
|
import { createRequestContainer } from "@open-mercato/shared/lib/di/container";
|
|
6
5
|
import { getToolRegistry } from "../../lib/tool-registry.js";
|
|
6
|
+
import { toolInputJsonSchema } from "../../lib/tool-input-schema.js";
|
|
7
7
|
import { loadAllModuleTools } from "../../lib/tool-loader.js";
|
|
8
8
|
import { hasRequiredFeatures } from "../../lib/auth.js";
|
|
9
9
|
const logger = createLogger("ai_assistant");
|
|
@@ -40,7 +40,7 @@ async function GET(req) {
|
|
|
40
40
|
return {
|
|
41
41
|
name: tool.name,
|
|
42
42
|
description: tool.description,
|
|
43
|
-
inputSchema:
|
|
43
|
+
inputSchema: toolInputJsonSchema(tool.inputSchema),
|
|
44
44
|
module: nameParts[0] || "other"
|
|
45
45
|
};
|
|
46
46
|
});
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../../../src/modules/ai_assistant/api/tools/route.ts"],
|
|
4
|
-
"sourcesContent": ["import { createLogger } from '@open-mercato/shared/lib/logger'\nimport { NextResponse, type NextRequest } from 'next/server'\nimport type { OpenApiRouteDoc } from '@open-mercato/shared/lib/openapi'\nimport {
|
|
5
|
-
"mappings": "AAAA,SAAS,oBAAoB;AAC7B,SAAS,oBAAsC;AAE/C,SAAS,
|
|
4
|
+
"sourcesContent": ["import { createLogger } from '@open-mercato/shared/lib/logger'\nimport { NextResponse, type NextRequest } from 'next/server'\nimport type { OpenApiRouteDoc } from '@open-mercato/shared/lib/openapi'\nimport { getAuthFromRequest } from '@open-mercato/shared/lib/auth/server'\nimport { createRequestContainer } from '@open-mercato/shared/lib/di/container'\nimport { getToolRegistry } from '../../lib/tool-registry'\nimport { toolInputJsonSchema } from '../../lib/tool-input-schema'\nimport { loadAllModuleTools } from '../../lib/tool-loader'\nimport { hasRequiredFeatures } from '../../lib/auth'\nimport type { RbacService } from '@open-mercato/core/modules/auth/services/rbacService'\n\nconst logger = createLogger('ai_assistant')\n\nexport const openApi: OpenApiRouteDoc = {\n tag: 'AI Assistant',\n summary: 'List AI tools',\n methods: {\n GET: { summary: 'List available MCP tools filtered by user permissions' },\n },\n}\n\nexport const metadata = {\n GET: { requireAuth: true, requireFeatures: ['ai_assistant.view'] },\n}\n\nexport async function GET(req: NextRequest) {\n const auth = await getAuthFromRequest(req)\n\n if (!auth) {\n return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })\n }\n\n try {\n const container = await createRequestContainer()\n const rbacService = container.resolve<RbacService>('rbacService')\n\n // Load ACL for user\n const acl = await rbacService.loadAcl(auth.sub, {\n tenantId: auth.tenantId,\n organizationId: auth.orgId,\n })\n\n // Ensure tools are loaded\n await loadAllModuleTools()\n\n // Get tools filtered by ACL\n const registry = getToolRegistry()\n const allTools = Array.from(registry.getTools().values())\n\n const accessibleTools = allTools.filter((tool) =>\n hasRequiredFeatures(tool.requiredFeatures, acl.features, acl.isSuperAdmin, rbacService)\n )\n\n const tools = accessibleTools.map((tool) => {\n const nameParts = tool.name.split('.')\n return {\n name: tool.name,\n description: tool.description,\n inputSchema: toolInputJsonSchema(tool.inputSchema),\n module: nameParts[0] || 'other',\n }\n })\n\n return NextResponse.json({ tools })\n } catch (error) {\n logger.error('AI Tools \u2014 Error listing tools', { err: error })\n return NextResponse.json({ error: 'Failed to list tools' }, { status: 500 })\n }\n}\n"],
|
|
5
|
+
"mappings": "AAAA,SAAS,oBAAoB;AAC7B,SAAS,oBAAsC;AAE/C,SAAS,0BAA0B;AACnC,SAAS,8BAA8B;AACvC,SAAS,uBAAuB;AAChC,SAAS,2BAA2B;AACpC,SAAS,0BAA0B;AACnC,SAAS,2BAA2B;AAGpC,MAAM,SAAS,aAAa,cAAc;AAEnC,MAAM,UAA2B;AAAA,EACtC,KAAK;AAAA,EACL,SAAS;AAAA,EACT,SAAS;AAAA,IACP,KAAK,EAAE,SAAS,wDAAwD;AAAA,EAC1E;AACF;AAEO,MAAM,WAAW;AAAA,EACtB,KAAK,EAAE,aAAa,MAAM,iBAAiB,CAAC,mBAAmB,EAAE;AACnE;AAEA,eAAsB,IAAI,KAAkB;AAC1C,QAAM,OAAO,MAAM,mBAAmB,GAAG;AAEzC,MAAI,CAAC,MAAM;AACT,WAAO,aAAa,KAAK,EAAE,OAAO,eAAe,GAAG,EAAE,QAAQ,IAAI,CAAC;AAAA,EACrE;AAEA,MAAI;AACF,UAAM,YAAY,MAAM,uBAAuB;AAC/C,UAAM,cAAc,UAAU,QAAqB,aAAa;AAGhE,UAAM,MAAM,MAAM,YAAY,QAAQ,KAAK,KAAK;AAAA,MAC9C,UAAU,KAAK;AAAA,MACf,gBAAgB,KAAK;AAAA,IACvB,CAAC;AAGD,UAAM,mBAAmB;AAGzB,UAAM,WAAW,gBAAgB;AACjC,UAAM,WAAW,MAAM,KAAK,SAAS,SAAS,EAAE,OAAO,CAAC;AAExD,UAAM,kBAAkB,SAAS;AAAA,MAAO,CAAC,SACvC,oBAAoB,KAAK,kBAAkB,IAAI,UAAU,IAAI,cAAc,WAAW;AAAA,IACxF;AAEA,UAAM,QAAQ,gBAAgB,IAAI,CAAC,SAAS;AAC1C,YAAM,YAAY,KAAK,KAAK,MAAM,GAAG;AACrC,aAAO;AAAA,QACL,MAAM,KAAK;AAAA,QACX,aAAa,KAAK;AAAA,QAClB,aAAa,oBAAoB,KAAK,WAAW;AAAA,QACjD,QAAQ,UAAU,CAAC,KAAK;AAAA,MAC1B;AAAA,IACF,CAAC;AAED,WAAO,aAAa,KAAK,EAAE,MAAM,CAAC;AAAA,EACpC,SAAS,OAAO;AACd,WAAO,MAAM,uCAAkC,EAAE,KAAK,MAAM,CAAC;AAC7D,WAAO,aAAa,KAAK,EAAE,OAAO,uBAAuB,GAAG,EAAE,QAAQ,IAAI,CAAC;AAAA,EAC7E;AACF;",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { createLogger } from "@open-mercato/shared/lib/logger";
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
import fs from "node:fs";
|
|
4
|
+
import crypto from "node:crypto";
|
|
4
5
|
import { createRequire } from "node:module";
|
|
5
6
|
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
6
7
|
const logger = createLogger("ai_assistant");
|
|
@@ -36,15 +37,18 @@ async function compileAndImportGenerated(tsPath) {
|
|
|
36
37
|
if (!fs.existsSync(tsPath)) {
|
|
37
38
|
throw new Error(`Generated file not found: ${tsPath}`);
|
|
38
39
|
}
|
|
40
|
+
const runtime = useJestCjsArtifact ? "cjs" : "esm";
|
|
41
|
+
const tsSource = fs.readFileSync(tsPath, "utf-8");
|
|
42
|
+
const appLocalArtifacts = await compileAppLocalModuleEntries(tsSource, appRoot, runtime);
|
|
39
43
|
const jsExists = fs.existsSync(jsPath);
|
|
40
44
|
const needsCompile = !jsExists || fs.statSync(tsPath).mtimeMs > fs.statSync(jsPath).mtimeMs;
|
|
41
45
|
if (needsCompile) {
|
|
42
46
|
const esbuild = await import("esbuild");
|
|
43
|
-
const tsSource = fs.readFileSync(tsPath, "utf-8");
|
|
44
47
|
const aliasRewritten = rewriteGeneratedAliasImportsForRuntime(
|
|
45
48
|
tsSource,
|
|
46
49
|
appRoot,
|
|
47
|
-
|
|
50
|
+
runtime,
|
|
51
|
+
appLocalArtifacts
|
|
48
52
|
);
|
|
49
53
|
const result = await esbuild.transform(aliasRewritten, {
|
|
50
54
|
loader: "ts",
|
|
@@ -63,6 +67,40 @@ async function compileAndImportGenerated(tsPath) {
|
|
|
63
67
|
function isJestRuntime() {
|
|
64
68
|
return typeof process.env.JEST_WORKER_ID === "string";
|
|
65
69
|
}
|
|
70
|
+
function collectAppLocalSpecifiers(source) {
|
|
71
|
+
const specifiers = /* @__PURE__ */ new Set();
|
|
72
|
+
for (const [, specifier] of source.matchAll(APP_LOCAL_STATIC_IMPORT)) specifiers.add(specifier);
|
|
73
|
+
for (const [, specifier] of source.matchAll(APP_LOCAL_DYNAMIC_IMPORT)) specifiers.add(specifier);
|
|
74
|
+
return [...specifiers].sort((a, b) => a < b ? -1 : a > b ? 1 : 0);
|
|
75
|
+
}
|
|
76
|
+
function appLocalArtifactName(specifier, runtime) {
|
|
77
|
+
const slug = specifier.replace(/[^a-zA-Z0-9]+/g, "-").replace(/^-+|-+$/g, "");
|
|
78
|
+
const digest = crypto.createHash("sha256").update(specifier).digest("hex").slice(0, 8);
|
|
79
|
+
return `${slug}-${digest}.${runtime === "cjs" ? "cjs" : "mjs"}`;
|
|
80
|
+
}
|
|
81
|
+
async function compileAppLocalModuleEntries(source, appRoot, runtime) {
|
|
82
|
+
const specifiers = collectAppLocalSpecifiers(source);
|
|
83
|
+
const artifacts = /* @__PURE__ */ new Map();
|
|
84
|
+
if (specifiers.length === 0) return artifacts;
|
|
85
|
+
const generatedDir = path.join(appRoot, ".mercato", "generated");
|
|
86
|
+
const { compileAppSourceFile } = await import("@open-mercato/shared/lib/bootstrap/dynamicLoader");
|
|
87
|
+
for (const specifier of specifiers) {
|
|
88
|
+
const target = path.resolve(generatedDir, specifier);
|
|
89
|
+
const tsPath = fs.existsSync(`${target}.ts`) ? `${target}.ts` : fs.existsSync(target) && target.endsWith(".ts") ? target : null;
|
|
90
|
+
if (tsPath === null) continue;
|
|
91
|
+
const outFile = path.join(generatedDir, "app-modules", appLocalArtifactName(specifier, runtime));
|
|
92
|
+
try {
|
|
93
|
+
await compileAppSourceFile(tsPath, { appRoot, outFile, format: runtime });
|
|
94
|
+
artifacts.set(specifier, outFile);
|
|
95
|
+
} catch (error) {
|
|
96
|
+
logger.warn("Could not compile an app-local module entry for the generated registry", {
|
|
97
|
+
specifier,
|
|
98
|
+
err: error
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
return artifacts;
|
|
103
|
+
}
|
|
66
104
|
const UNSAFE_JS_STRING_CHAR_ESCAPES = {
|
|
67
105
|
60: "\\u003C",
|
|
68
106
|
// < — HTML/script-tag breakout
|
|
@@ -82,25 +120,31 @@ function escapeUnsafeJsStringChars(value) {
|
|
|
82
120
|
function toSafeJsStringLiteral(value) {
|
|
83
121
|
return escapeUnsafeJsStringChars(JSON.stringify(value));
|
|
84
122
|
}
|
|
85
|
-
function rewriteGeneratedAliasImports(source, appRoot) {
|
|
86
|
-
return rewriteGeneratedAliasImportsForRuntime(source, appRoot, "esm");
|
|
123
|
+
function rewriteGeneratedAliasImports(source, appRoot, appLocalArtifacts) {
|
|
124
|
+
return rewriteGeneratedAliasImportsForRuntime(source, appRoot, "esm", appLocalArtifacts);
|
|
87
125
|
}
|
|
88
|
-
|
|
126
|
+
const APP_LOCAL_STATIC_IMPORT = /from\s+["']((?:\.\.\/)+src\/[^"']+)["']/g;
|
|
127
|
+
const APP_LOCAL_DYNAMIC_IMPORT = /import\s*\(\s*["']((?:\.\.\/)+src\/[^"']+)["']\s*\)/g;
|
|
128
|
+
function rewriteGeneratedAliasImportsForRuntime(source, appRoot, runtime, appLocalArtifacts = /* @__PURE__ */ new Map()) {
|
|
89
129
|
const generatedDir = path.join(appRoot, ".mercato", "generated");
|
|
130
|
+
const toRuntimeLiteral = (target) => toSafeJsStringLiteral(runtime === "esm" ? pathToFileURL(target).href : target);
|
|
90
131
|
const toResolvedLiteral = (target) => {
|
|
91
132
|
const candidate = fs.existsSync(target) ? target : fs.existsSync(target + ".ts") ? target + ".ts" : target;
|
|
92
|
-
|
|
93
|
-
return toSafeJsStringLiteral(specifier);
|
|
133
|
+
return toRuntimeLiteral(candidate);
|
|
94
134
|
};
|
|
95
135
|
const resolveAlias = (relativePath) => toResolvedLiteral(path.join(appRoot, relativePath));
|
|
96
|
-
const resolveRelative = (specifier) =>
|
|
136
|
+
const resolveRelative = (specifier) => {
|
|
137
|
+
const artifact = appLocalArtifacts.get(specifier);
|
|
138
|
+
if (artifact !== void 0) return toRuntimeLiteral(artifact);
|
|
139
|
+
return toResolvedLiteral(path.resolve(generatedDir, specifier));
|
|
140
|
+
};
|
|
97
141
|
return source.replace(/from\s+["']@\/([^"']+)["']/g, (_match, relativePath) => {
|
|
98
142
|
return `from ${resolveAlias(relativePath)}`;
|
|
99
143
|
}).replace(/import\s*\(\s*["']@\/([^"']+)["']\s*\)/g, (_match, relativePath) => {
|
|
100
144
|
return `import(${resolveAlias(relativePath)})`;
|
|
101
|
-
}).replace(
|
|
145
|
+
}).replace(APP_LOCAL_STATIC_IMPORT, (_match, specifier) => {
|
|
102
146
|
return `from ${resolveRelative(specifier)}`;
|
|
103
|
-
}).replace(
|
|
147
|
+
}).replace(APP_LOCAL_DYNAMIC_IMPORT, (_match, specifier) => {
|
|
104
148
|
return `import(${resolveRelative(specifier)})`;
|
|
105
149
|
});
|
|
106
150
|
}
|
|
@@ -124,6 +168,7 @@ async function ensureApiRouteManifestsRegistered() {
|
|
|
124
168
|
}
|
|
125
169
|
}
|
|
126
170
|
export {
|
|
171
|
+
collectAppLocalSpecifiers,
|
|
127
172
|
compileAndImportGenerated,
|
|
128
173
|
ensureApiRouteManifestsRegistered,
|
|
129
174
|
escapeUnsafeJsStringChars,
|
|
@@ -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 { createLogger } from '@open-mercato/shared/lib/logger'\nimport path from 'node:path'\nimport fs from 'node:fs'\nimport { createRequire } from 'node:module'\nimport { fileURLToPath, pathToFileURL } from 'node:url'\n\nconst logger = createLogger('ai_assistant')\n\nconst requireFromHere = createRequire(import.meta.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 useJestCjsArtifact = isJestRuntime()\n const jsPath = tsPath.replace(/\\.ts$/, useJestCjsArtifact ? '.jest.cjs' : '.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 = rewriteGeneratedAliasImportsForRuntime(\n tsSource,\n appRoot,\n useJestCjsArtifact ? 'cjs' : 'esm',\n )\n const result = await esbuild.transform(aliasRewritten, {\n loader: 'ts',\n format: useJestCjsArtifact ? 'cjs' : 'esm',\n target: 'node18',\n sourcemap: false,\n sourcefile: tsPath,\n })\n fs.writeFileSync(jsPath, result.code)\n }\n\n if (useJestCjsArtifact) {\n return requireFromHere(jsPath) as Record<string, unknown>\n }\n return (await import(pathToFileURL(jsPath).href)) as Record<string, unknown>\n}\n\nfunction isJestRuntime(): boolean {\n return typeof process.env.JEST_WORKER_ID === 'string'\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 return rewriteGeneratedAliasImportsForRuntime(source, appRoot, 'esm')\n}\n\nfunction rewriteGeneratedAliasImportsForRuntime(\n source: string,\n appRoot: string,\n runtime: 'esm' | 'cjs',\n): 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 const specifier = runtime === 'esm' ? pathToFileURL(candidate).href : candidate\n return toSafeJsStringLiteral(specifier)\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 logger.warn('Could not register api-routes manifest', { err: error })\n return 0\n }\n}\n"],
|
|
5
|
-
"mappings": "AAgBA,SAAS,oBAAoB;AAC7B,OAAO,UAAU;AACjB,OAAO,QAAQ;AACf,SAAS,qBAAqB;AAC9B,SAAS,eAAe,qBAAqB;AAE7C,MAAM,SAAS,aAAa,cAAc;AAE1C,MAAM,kBAAkB,cAAc,YAAY,GAAG;AAQ9C,SAAS,kBAAkB,UAAiC;AACjE,QAAM,QAAQ,MAAM;AAClB,QAAI;AACF,aAAO,cAAc,YAAY,GAAG;AAAA,IACtC,QAAQ;AACN,aAAO;AAAA,IACT;AAAA,EACF,GAAG;AAEH,MAAI,MAAM;AACR,QAAI,SAAS,KAAK,QAAQ,IAAI;AAC9B,aAAS,IAAI,GAAG,IAAI,IAAI,KAAK;AAC3B,YAAM,YAAY,KAAK,KAAK,QAAQ,QAAQ,WAAW,YAAY,aAAa,QAAQ;AACxF,UAAI,GAAG,WAAW,SAAS,EAAG,QAAO;AACrC,YAAM,OAAO,KAAK,QAAQ,MAAM;AAChC,UAAI,SAAS,OAAQ;AACrB,eAAS;AAAA,IACX;AAAA,EACF;AAGA,QAAM,UAAU,KAAK,QAAQ,QAAQ,IAAI,GAAG,QAAQ,WAAW,YAAY,aAAa,QAAQ;AAChG,MAAI,GAAG,WAAW,OAAO,EAAG,QAAO;AACnC,QAAM,gBAAgB,KAAK,QAAQ,QAAQ,IAAI,GAAG,YAAY,aAAa,QAAQ;AACnF,MAAI,GAAG,WAAW,aAAa,EAAG,QAAO;AACzC,SAAO;AACT;
|
|
4
|
+
"sourcesContent": ["/**\n * Runtime loader for `.mercato/generated/*.generated.ts` registry files.\n *\n * The generated registries import their entries through the `@/` path alias\n * (e.g. `@/.mercato/generated/ai-tools.generated`). That alias is only\n * understood by the Next.js bundler \u2014 in a standalone Node process (the\n * `mcp:dev` / `mcp:serve` MCP servers, the CLI tool-test runner) a raw\n * `import('@/.mercato/...')` throws `ERR_MODULE_NOT_FOUND: Cannot find\n * package '@/.mercato'` because Node treats `@/` as a package specifier.\n *\n * These helpers locate the generated `.ts` file on disk and compile-and-import\n * it with esbuild (transpile-only), rewriting `@/` aliases to absolute paths.\n * This mirrors `loadBootstrapData` in\n * `@open-mercato/shared/lib/bootstrap/dynamicLoader` and works in both the\n * monorepo and standalone apps.\n */\nimport { createLogger } from '@open-mercato/shared/lib/logger'\nimport path from 'node:path'\nimport fs from 'node:fs'\nimport crypto from 'node:crypto'\nimport { createRequire } from 'node:module'\nimport { fileURLToPath, pathToFileURL } from 'node:url'\n\nconst logger = createLogger('ai_assistant')\n\nconst requireFromHere = createRequire(import.meta.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 *\n * The `@app` local module entries are the one exception: those targets are raw\n * app TypeScript with no compiled sibling, so they are compiled separately\n * (see `compileAppLocalModuleEntries`) and the registry points at the artifact.\n */\nexport async function compileAndImportGenerated(tsPath: string): Promise<Record<string, unknown>> {\n const useJestCjsArtifact = isJestRuntime()\n const jsPath = tsPath.replace(/\\.ts$/, useJestCjsArtifact ? '.jest.cjs' : '.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 runtime = useJestCjsArtifact ? 'cjs' : 'esm'\n const tsSource = fs.readFileSync(tsPath, 'utf-8')\n // Runs on every call, not only when the registry itself is stale: the\n // artifact path is stable, so a registry cache hit would otherwise pin an\n // app module's compiled output to whatever it was when the registry was\n // last regenerated.\n const appLocalArtifacts = await compileAppLocalModuleEntries(tsSource, appRoot, runtime)\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 aliasRewritten = rewriteGeneratedAliasImportsForRuntime(\n tsSource,\n appRoot,\n runtime,\n appLocalArtifacts,\n )\n const result = await esbuild.transform(aliasRewritten, {\n loader: 'ts',\n format: useJestCjsArtifact ? 'cjs' : 'esm',\n target: 'node18',\n sourcemap: false,\n sourcefile: tsPath,\n })\n fs.writeFileSync(jsPath, result.code)\n }\n\n if (useJestCjsArtifact) {\n return requireFromHere(jsPath) as Record<string, unknown>\n }\n return (await import(pathToFileURL(jsPath).href)) as Record<string, unknown>\n}\n\nfunction isJestRuntime(): boolean {\n return typeof process.env.JEST_WORKER_ID === 'string'\n}\n\n/** Every `../../src/...` specifier the generator emitted for `@app` local modules. */\nexport function collectAppLocalSpecifiers(source: string): string[] {\n const specifiers = new Set<string>()\n for (const [, specifier] of source.matchAll(APP_LOCAL_STATIC_IMPORT)) specifiers.add(specifier)\n for (const [, specifier] of source.matchAll(APP_LOCAL_DYNAMIC_IMPORT)) specifiers.add(specifier)\n return [...specifiers].sort((a, b) => (a < b ? -1 : a > b ? 1 : 0))\n}\n\n/** Stable, collision-free artifact name for one app-local specifier. */\nfunction appLocalArtifactName(specifier: string, runtime: 'esm' | 'cjs'): string {\n const slug = specifier.replace(/[^a-zA-Z0-9]+/g, '-').replace(/^-+|-+$/g, '')\n const digest = crypto.createHash('sha256').update(specifier).digest('hex').slice(0, 8)\n return `${slug}-${digest}.${runtime === 'cjs' ? 'cjs' : 'mjs'}`\n}\n\n/**\n * Compile every `@app` local module entry the generated registry references,\n * and map each specifier to its artifact.\n *\n * Package-backed modules (`@open-mercato/*`) never reach here \u2014 their bare\n * specifiers resolve through `node_modules` to compiled `.js`. App-local\n * modules have no compiled sibling, and Node cannot load their `.ts` source\n * directly: relative specifiers need explicit extensions under type stripping,\n * and the module's own graph (`./di`, `./data/entities`) carries decorator and\n * enum syntax that type stripping rejects outright. Bundling the entry with\n * every package import left external is what makes the source loadable.\n *\n * A module that fails to compile is logged and left pointing at its raw source,\n * which reproduces the pre-existing resolution error rather than silently\n * dropping the module's tools from the registry.\n */\nasync function compileAppLocalModuleEntries(\n source: string,\n appRoot: string,\n runtime: 'esm' | 'cjs',\n): Promise<Map<string, string>> {\n const specifiers = collectAppLocalSpecifiers(source)\n const artifacts = new Map<string, string>()\n if (specifiers.length === 0) return artifacts\n\n const generatedDir = path.join(appRoot, '.mercato', 'generated')\n const { compileAppSourceFile } = await import('@open-mercato/shared/lib/bootstrap/dynamicLoader')\n\n for (const specifier of specifiers) {\n const target = path.resolve(generatedDir, specifier)\n const tsPath = fs.existsSync(`${target}.ts`)\n ? `${target}.ts`\n : fs.existsSync(target) && target.endsWith('.ts')\n ? target\n : null\n if (tsPath === null) continue\n\n const outFile = path.join(generatedDir, 'app-modules', appLocalArtifactName(specifier, runtime))\n try {\n await compileAppSourceFile(tsPath, { appRoot, outFile, format: runtime })\n artifacts.set(specifier, outFile)\n } catch (error) {\n logger.warn('Could not compile an app-local module entry for the generated registry', {\n specifier,\n err: error,\n })\n }\n }\n\n return artifacts\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 * When `appLocalArtifacts` maps a shape-2 specifier to a compiled artifact, that\n * artifact wins: pointing Node at raw app TypeScript only works while the file\n * and its whole graph stay within what type stripping accepts, which app\n * modules do not (see `compileAppLocalModuleEntries`). Without a mapping both\n * shapes fall back to the same `.ts`-suffix probe. Other specifiers (bare\n * packages, sibling `./` imports) are left untouched. Exported for unit testing.\n */\nexport function rewriteGeneratedAliasImports(\n source: string,\n appRoot: string,\n appLocalArtifacts?: Map<string, string>,\n): string {\n return rewriteGeneratedAliasImportsForRuntime(source, appRoot, 'esm', appLocalArtifacts)\n}\n\nconst APP_LOCAL_STATIC_IMPORT = /from\\s+[\"']((?:\\.\\.\\/)+src\\/[^\"']+)[\"']/g\nconst APP_LOCAL_DYNAMIC_IMPORT = /import\\s*\\(\\s*[\"']((?:\\.\\.\\/)+src\\/[^\"']+)[\"']\\s*\\)/g\n\nfunction rewriteGeneratedAliasImportsForRuntime(\n source: string,\n appRoot: string,\n runtime: 'esm' | 'cjs',\n appLocalArtifacts: Map<string, string> = new Map(),\n): string {\n const generatedDir = path.join(appRoot, '.mercato', 'generated')\n const toRuntimeLiteral = (target: string): string =>\n toSafeJsStringLiteral(runtime === 'esm' ? pathToFileURL(target).href : target)\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 toRuntimeLiteral(candidate)\n }\n const resolveAlias = (relativePath: string): string =>\n toResolvedLiteral(path.join(appRoot, relativePath))\n const resolveRelative = (specifier: string): string => {\n const artifact = appLocalArtifacts.get(specifier)\n if (artifact !== undefined) return toRuntimeLiteral(artifact)\n return toResolvedLiteral(path.resolve(generatedDir, specifier))\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 .replace(APP_LOCAL_STATIC_IMPORT, (_match, specifier: string) => {\n return `from ${resolveRelative(specifier)}`\n })\n .replace(APP_LOCAL_DYNAMIC_IMPORT, (_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 logger.warn('Could not register api-routes manifest', { err: error })\n return 0\n }\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { toolInputJsonSchema } from "./tool-input-schema.js";
|
|
2
2
|
import { getToolRegistry } from "./tool-registry.js";
|
|
3
3
|
import { executeTool } from "./tool-executor.js";
|
|
4
4
|
import { loadAllModuleTools } from "./tool-loader.js";
|
|
@@ -70,7 +70,7 @@ class InProcessMcpClient {
|
|
|
70
70
|
return accessibleTools.map((tool) => ({
|
|
71
71
|
name: tool.name,
|
|
72
72
|
description: tool.description,
|
|
73
|
-
inputSchema:
|
|
73
|
+
inputSchema: toolInputJsonSchema(tool.inputSchema)
|
|
74
74
|
}));
|
|
75
75
|
}
|
|
76
76
|
/**
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../../src/modules/ai_assistant/lib/in-process-client.ts"],
|
|
4
|
-
"sourcesContent": ["import type { AwilixContainer } from 'awilix'\nimport type { z } from 'zod'\nimport {
|
|
5
|
-
"mappings": "AAEA,SAAS,
|
|
4
|
+
"sourcesContent": ["import type { AwilixContainer } from 'awilix'\nimport type { z } from 'zod'\nimport { toolInputJsonSchema } from './tool-input-schema'\nimport { getToolRegistry } from './tool-registry'\nimport { executeTool } from './tool-executor'\nimport { loadAllModuleTools } from './tool-loader'\nimport { authenticateMcpRequest, hasRequiredFeatures, type McpAuthSuccess } from './auth'\nimport type { McpToolContext, McpClientInterface, ToolInfo, ToolResult, McpToolDefinition } from './types'\nimport type { RbacService } from '@open-mercato/core/modules/auth/services/rbacService'\n\n/**\n * Options for creating an in-process MCP client.\n */\nexport type InProcessClientOptions = {\n /** API key secret for authentication */\n apiKeySecret: string\n /** DI container */\n container: AwilixContainer\n}\n\n/**\n * Options for creating an in-process MCP client with direct auth context.\n * Used when the caller already has authenticated user context (e.g., from session).\n */\nexport type AuthContextOptions = {\n /** DI container */\n container: AwilixContainer\n /** Pre-authenticated user context */\n authContext: {\n tenantId: string | null\n organizationId: string | null\n userId: string\n userFeatures: string[]\n isSuperAdmin: boolean\n }\n}\n\n/**\n * Tool info with raw Zod schema for AI SDK integration.\n */\nexport type ToolInfoWithSchema = {\n name: string\n description: string\n inputSchema: z.ZodType<unknown>\n}\n\n/**\n * In-process MCP client for direct tool execution.\n *\n * This client executes tools directly without MCP protocol overhead,\n * making it the fastest option when running in the same process as\n * the LLM service.\n *\n * Authentication is still performed via API key to ensure proper\n * ACL filtering of available tools.\n */\nexport class InProcessMcpClient implements McpClientInterface {\n private auth: McpAuthSuccess\n private container: AwilixContainer\n private toolContext: McpToolContext\n private toolsLoaded = false\n\n private constructor(auth: McpAuthSuccess, container: AwilixContainer) {\n this.auth = auth\n this.container = container\n this.toolContext = {\n tenantId: auth.tenantId,\n organizationId: auth.organizationId,\n userId: auth.userId,\n container,\n userFeatures: auth.features,\n isSuperAdmin: auth.isSuperAdmin,\n }\n }\n\n /**\n * Create and authenticate an in-process client using API key.\n */\n static async create(options: InProcessClientOptions): Promise<InProcessMcpClient> {\n const { apiKeySecret, container } = options\n\n const authResult = await authenticateMcpRequest(apiKeySecret, container)\n if (!authResult.success) {\n throw new Error(`Authentication failed: ${authResult.error}`)\n }\n\n return new InProcessMcpClient(authResult, container)\n }\n\n /**\n * Create an in-process client with pre-authenticated context.\n * Use this when you already have user auth context (e.g., from session auth).\n */\n static async createWithAuthContext(options: AuthContextOptions): Promise<InProcessMcpClient> {\n const { container, authContext } = options\n\n // Create a synthetic auth result (no API key lookup needed)\n const syntheticAuth: McpAuthSuccess = {\n success: true,\n keyId: 'session-auth',\n keyName: 'Session Authentication',\n tenantId: authContext.tenantId,\n organizationId: authContext.organizationId,\n userId: authContext.userId,\n features: authContext.userFeatures,\n isSuperAdmin: authContext.isSuperAdmin,\n }\n\n return new InProcessMcpClient(syntheticAuth, container)\n }\n\n /**\n * Ensure tools are loaded from all modules.\n */\n private async ensureToolsLoaded(): Promise<void> {\n if (!this.toolsLoaded) {\n await loadAllModuleTools()\n this.toolsLoaded = true\n }\n }\n\n /**\n * List available tools filtered by API key's permissions.\n * Returns JSON Schema format (for MCP protocol compatibility).\n */\n async listTools(): Promise<ToolInfo[]> {\n await this.ensureToolsLoaded()\n\n const registry = getToolRegistry()\n const tools = Array.from(registry.getTools().values())\n\n const rbacService = this.container.resolve<RbacService>('rbacService')\n const accessibleTools = tools.filter((tool) =>\n hasRequiredFeatures(tool.requiredFeatures, this.auth.features, this.auth.isSuperAdmin, rbacService)\n )\n\n return accessibleTools.map((tool) => ({\n name: tool.name,\n description: tool.description,\n inputSchema: toolInputJsonSchema(tool.inputSchema),\n }))\n }\n\n /**\n * List available tools with raw Zod schemas.\n * Use this for AI SDK integration which requires Zod schemas.\n */\n async listToolsWithSchemas(): Promise<ToolInfoWithSchema[]> {\n await this.ensureToolsLoaded()\n\n const registry = getToolRegistry()\n const tools = Array.from(registry.getTools().values())\n\n const rbacService = this.container.resolve<RbacService>('rbacService')\n const accessibleTools = tools.filter((tool) =>\n hasRequiredFeatures(tool.requiredFeatures, this.auth.features, this.auth.isSuperAdmin, rbacService)\n )\n\n return accessibleTools.map((tool) => ({\n name: tool.name,\n description: tool.description,\n inputSchema: tool.inputSchema,\n }))\n }\n\n /**\n * Execute a tool directly.\n */\n async callTool(name: string, args: unknown): Promise<ToolResult> {\n await this.ensureToolsLoaded()\n\n const result = await executeTool(name, args ?? {}, this.toolContext)\n\n return {\n success: result.success,\n result: result.result,\n error: result.error,\n }\n }\n\n /**\n * Close the client (no-op for in-process).\n */\n async close(): Promise<void> {\n // No resources to clean up for in-process client\n }\n\n /**\n * Get the authenticated context info.\n */\n getAuthInfo(): {\n keyId: string\n keyName: string\n tenantId: string | null\n organizationId: string | null\n userId: string\n isSuperAdmin: boolean\n } {\n return {\n keyId: this.auth.keyId,\n keyName: this.auth.keyName,\n tenantId: this.auth.tenantId,\n organizationId: this.auth.organizationId,\n userId: this.auth.userId,\n isSuperAdmin: this.auth.isSuperAdmin,\n }\n }\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -4,7 +4,7 @@ import {
|
|
|
4
4
|
ListToolsRequestSchema,
|
|
5
5
|
CallToolRequestSchema
|
|
6
6
|
} from "@modelcontextprotocol/sdk/types.js";
|
|
7
|
-
import {
|
|
7
|
+
import { toolInputJsonSchema } from "./tool-input-schema.js";
|
|
8
8
|
import { getToolRegistry } from "./tool-registry.js";
|
|
9
9
|
import { executeTool } from "./tool-executor.js";
|
|
10
10
|
import { loadAllModuleTools, indexToolsForSearch } from "./tool-loader.js";
|
|
@@ -91,8 +91,7 @@ async function createMcpServer(options) {
|
|
|
91
91
|
tools: accessibleTools.map((tool) => ({
|
|
92
92
|
name: tool.name,
|
|
93
93
|
description: tool.description,
|
|
94
|
-
|
|
95
|
-
inputSchema: zodToJsonSchema(tool.inputSchema)
|
|
94
|
+
inputSchema: toolInputJsonSchema(tool.inputSchema)
|
|
96
95
|
}))
|
|
97
96
|
};
|
|
98
97
|
});
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../../src/modules/ai_assistant/lib/mcp-server.ts"],
|
|
4
|
-
"sourcesContent": ["import { Server } from '@modelcontextprotocol/sdk/server/index.js'\nimport { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'\nimport {\n ListToolsRequestSchema,\n CallToolRequestSchema,\n} from '@modelcontextprotocol/sdk/types.js'\nimport {
|
|
5
|
-
"mappings": "AAAA,SAAS,cAAc;AACvB,SAAS,4BAA4B;AACrC;AAAA,EACE;AAAA,EACA;AAAA,OACK;AACP,SAAS,
|
|
4
|
+
"sourcesContent": ["import { Server } from '@modelcontextprotocol/sdk/server/index.js'\nimport { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'\nimport {\n ListToolsRequestSchema,\n CallToolRequestSchema,\n} from '@modelcontextprotocol/sdk/types.js'\nimport { toolInputJsonSchema } from './tool-input-schema'\nimport { getToolRegistry } from './tool-registry'\nimport { executeTool } from './tool-executor'\nimport { loadAllModuleTools, indexToolsForSearch } from './tool-loader'\nimport { authenticateMcpRequest, hasRequiredFeatures } from './auth'\nimport type { McpServerOptions, McpToolContext } from './types'\nimport type { SearchService } from '@open-mercato/search/service'\nimport type { RbacService } from '@open-mercato/core/modules/auth/services/rbacService'\n\n\nconst formatStderrError = (error: unknown): string =>\n error instanceof Error ? error.stack ?? error.message : String(error)\n\nconst writeStderrLine = (line: string): void => {\n process.stderr.write(`${line}\\n`)\n}\n\n/**\n * Create and configure an MCP server instance.\n */\nexport async function createMcpServer(options: McpServerOptions): Promise<Server> {\n const { config, container, context, allowUnauthenticatedSuperadmin } = options\n\n // Treat empty / whitespace-only secrets as missing so a blank api key cannot\n // fall through into an unauthenticated branch.\n const apiKeySecret =\n typeof options.apiKeySecret === 'string' && options.apiKeySecret.trim().length > 0\n ? options.apiKeySecret\n : undefined\n\n let tenantId: string | null = null\n let organizationId: string | null = null\n let userId: string | null = null\n let userFeatures: string[] = []\n let isSuperAdmin = false\n\n // API key authentication takes precedence\n if (apiKeySecret) {\n const authResult = await authenticateMcpRequest(apiKeySecret, container)\n if (!authResult.success) {\n throw new Error(`API key authentication failed: ${authResult.error}`)\n }\n tenantId = authResult.tenantId\n organizationId = authResult.organizationId\n userId = authResult.userId\n userFeatures = authResult.features\n isSuperAdmin = authResult.isSuperAdmin\n writeStderrLine(`[MCP Server] Authenticated via API key: ${authResult.keyName}`)\n } else if (context && context.userId) {\n // Manual context with a real user \u2014 load that user's ACL.\n tenantId = context.tenantId\n organizationId = context.organizationId\n userId = context.userId\n\n try {\n const rbacService = container.resolve('rbacService') as {\n loadAcl: (\n userId: string,\n scope: { tenantId: string | null; organizationId: string | null }\n ) => Promise<{\n isSuperAdmin: boolean\n features: string[]\n }>\n }\n const acl = await rbacService.loadAcl(userId, {\n tenantId,\n organizationId,\n })\n userFeatures = acl.features\n isSuperAdmin = acl.isSuperAdmin\n } catch (error) {\n writeStderrLine(`[MCP Server] Failed to load user ACL: ${formatStderrError(error)}`)\n }\n } else if (allowUnauthenticatedSuperadmin) {\n // Explicit, loud dev/testing opt-in. Without a user there is no ACL to load,\n // so the server runs as superadmin with no tenant scoping beyond whatever the\n // caller pinned via `context`. NEVER enable this in production.\n if (context) {\n tenantId = context.tenantId\n organizationId = context.organizationId\n }\n isSuperAdmin = true\n writeStderrLine(\n '[MCP Server] WARNING: allowUnauthenticatedSuperadmin is enabled \u2014 running with UNAUTHENTICATED SUPERADMIN access and no per-user ACL. Do not use this outside local development/testing.'\n )\n } else {\n // Fail closed: refuse to start rather than silently escalating to superadmin.\n throw new Error(\n '[internal] MCP server refused to start: no authentication provided. Supply a valid apiKeySecret, a context with a non-empty userId, or explicitly set allowUnauthenticatedSuperadmin: true for local development.'\n )\n }\n\n const toolContext: McpToolContext = {\n tenantId,\n organizationId,\n userId,\n container,\n userFeatures,\n isSuperAdmin,\n apiKeySecret,\n }\n\n const server = new Server(\n { name: config.name, version: config.version },\n { capabilities: { tools: {} } }\n )\n\n // List tools handler\n server.setRequestHandler(ListToolsRequestSchema, async () => {\n const registry = getToolRegistry()\n const tools = Array.from(registry.getTools().values())\n\n // Filter tools based on user permissions\n const rbacService = container.resolve<RbacService>('rbacService')\n const accessibleTools = tools.filter((tool) =>\n hasRequiredFeatures(tool.requiredFeatures, userFeatures, isSuperAdmin, rbacService)\n )\n\n if (config.debug) {\n writeStderrLine(\n `[MCP Server] Listing ${accessibleTools.length}/${tools.length} tools (filtered by ACL)`\n )\n }\n\n return {\n tools: accessibleTools.map((tool) => ({\n name: tool.name,\n description: tool.description,\n inputSchema: toolInputJsonSchema(tool.inputSchema),\n })),\n }\n })\n\n // Call tool handler\n server.setRequestHandler(CallToolRequestSchema, async (request) => {\n const { name, arguments: args } = request.params\n\n if (config.debug) {\n writeStderrLine(`[MCP Server] Calling tool: ${name} argKeys=${Object.keys(args ?? {}).join(',')}`)\n }\n\n const result = await executeTool(name, args ?? {}, toolContext)\n\n if (!result.success) {\n return {\n content: [\n {\n type: 'text',\n text: JSON.stringify({ error: result.error, code: result.errorCode }),\n },\n ],\n isError: true,\n }\n }\n\n return {\n content: [\n {\n type: 'text',\n text: JSON.stringify(result.result, null, 2),\n },\n ],\n }\n })\n\n return server\n}\n\n/**\n * Run MCP server with stdio transport.\n * This keeps the process running until terminated.\n *\n * Supports two authentication modes:\n * 1. API key: Provide `apiKeySecret` option\n * 2. Manual context: Provide `context` with tenant/org/user\n */\nexport async function runMcpServer(options: McpServerOptions): Promise<void> {\n // Generate entity graph for Code Mode search tool\n try {\n const { extractEntityGraph, cacheEntityGraph } = await import('./entity-graph')\n const { getOrm } = await import('@open-mercato/shared/lib/db/mikro')\n const orm = await getOrm()\n const graph = await extractEntityGraph(orm)\n cacheEntityGraph(graph)\n writeStderrLine(`[MCP Server] Entity graph: ${graph.nodes.length} entities`)\n } catch (error) {\n writeStderrLine(`[MCP Server] Entity graph skipped: ${error instanceof Error ? error.message : String(error)}`)\n }\n\n // Pre-cache raw OpenAPI spec for Code Mode search tool\n try {\n const { getRawOpenApiSpec } = await import('./api-endpoint-index')\n await getRawOpenApiSpec()\n writeStderrLine('[MCP Server] Raw OpenAPI spec cached for Code Mode')\n } catch (error) {\n writeStderrLine(`[MCP Server] Raw OpenAPI spec caching skipped: ${error instanceof Error ? error.message : String(error)}`)\n }\n\n // Load tools from all modules before starting\n await loadAllModuleTools()\n\n // Index tools for hybrid search discovery (if search service available)\n try {\n const searchService = options.container.resolve('searchService') as SearchService\n await indexToolsForSearch(searchService)\n } catch (error) {\n // Search service might not be configured - discovery will use fallback\n writeStderrLine('[MCP Server] Search indexing skipped (search service not available)')\n }\n\n const server = await createMcpServer(options)\n const transport = new StdioServerTransport()\n\n const toolCount = getToolRegistry().listToolNames().length\n\n writeStderrLine(`[MCP Server] Starting ${options.config.name} v${options.config.version}`)\n\n if (options.apiKeySecret && options.apiKeySecret.trim().length > 0) {\n writeStderrLine(`[MCP Server] Authentication: API key`)\n } else if (options.context && options.context.userId) {\n writeStderrLine(`[MCP Server] Tenant: ${options.context.tenantId ?? '(none)'}`)\n writeStderrLine(`[MCP Server] Organization: ${options.context.organizationId ?? '(none)'}`)\n writeStderrLine(`[MCP Server] User: ${options.context.userId}`)\n } else {\n writeStderrLine(`[MCP Server] Authentication: none (unauthenticated superadmin opt-in)`)\n }\n\n writeStderrLine(`[MCP Server] Tools registered: ${toolCount}`)\n\n await server.connect(transport)\n\n writeStderrLine('[MCP Server] Connected and ready for requests')\n\n // Handle shutdown gracefully\n const shutdown = async () => {\n writeStderrLine('[MCP Server] Shutting down...')\n await server.close()\n process.exit(0)\n }\n\n process.on('SIGINT', shutdown)\n process.on('SIGTERM', shutdown)\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
function toolInputJsonSchema(schema) {
|
|
3
|
+
if (!schema) return { type: "object", properties: {} };
|
|
4
|
+
try {
|
|
5
|
+
return z.toJSONSchema(schema, { io: "input", unrepresentable: "any" });
|
|
6
|
+
} catch {
|
|
7
|
+
return { type: "object", properties: {} };
|
|
8
|
+
}
|
|
9
|
+
}
|
|
10
|
+
export {
|
|
11
|
+
toolInputJsonSchema
|
|
12
|
+
};
|
|
13
|
+
//# sourceMappingURL=tool-input-schema.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../../../src/modules/ai_assistant/lib/tool-input-schema.ts"],
|
|
4
|
+
"sourcesContent": ["import { z, type ZodTypeAny } from 'zod'\n\n/**\n * Publish a tool's declared input as JSON Schema.\n *\n * Every place a tool is advertised \u2014 the MCP `tools/list` handler, the in-process client and\n * `GET /api/ai_assistant/tools` \u2014 used to call `zod-to-json-schema`, a Zod **3** converter. This\n * repository is on Zod 4, whose internals that package cannot read, so it silently returned\n * `{ \"$schema\": \"\u2026\" }` for every tool: no `properties`, no `required`, nothing. It did not throw\n * and nothing failed a type check, so the tools kept listing and a model was simply never told\n * that `example.get_customer_priority` takes a `customerId`. An argument-free tool looked\n * identical to one with three required fields.\n *\n * Zod 4 ships its own converter, so the fix is to use it. Two deliberate choices:\n *\n * - `io: 'input'` \u2014 a tool schema describes what the CALLER sends. With the default (`'output'`)\n * a field carrying a `.default()` is published as required-on-output, which is the opposite of\n * what a caller needs to know.\n * - `unrepresentable: 'any'` \u2014 the default throws on a type with no JSON Schema form (`z.date()`,\n * `z.bigint()`). Throwing here would take down the whole tool listing because one tool declared\n * a date, so such a field is published as an unconstrained value instead.\n *\n * A schema that still cannot be converted degrades to the empty object schema rather than\n * propagating: listing the remaining tools is worth more than failing the request.\n */\nexport function toolInputJsonSchema(schema: ZodTypeAny | undefined): Record<string, unknown> {\n if (!schema) return { type: 'object', properties: {} }\n try {\n return z.toJSONSchema(schema, { io: 'input', unrepresentable: 'any' }) as Record<string, unknown>\n } catch {\n return { type: 'object', properties: {} }\n }\n}\n"],
|
|
5
|
+
"mappings": "AAAA,SAAS,SAA0B;AAyB5B,SAAS,oBAAoB,QAAyD;AAC3F,MAAI,CAAC,OAAQ,QAAO,EAAE,MAAM,UAAU,YAAY,CAAC,EAAE;AACrD,MAAI;AACF,WAAO,EAAE,aAAa,QAAQ,EAAE,IAAI,SAAS,iBAAiB,MAAM,CAAC;AAAA,EACvE,QAAQ;AACN,WAAO,EAAE,MAAM,UAAU,YAAY,CAAC,EAAE;AAAA,EAC1C;AACF;",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|
|
@@ -12,6 +12,7 @@ import {
|
|
|
12
12
|
ensureApiRouteManifestsRegistered
|
|
13
13
|
} from "./generated-registry-loader.js";
|
|
14
14
|
const logger = createLogger("ai_assistant").child({ component: "tools" });
|
|
15
|
+
let allModuleToolsLoad = null;
|
|
15
16
|
function isModuleAiTool(value) {
|
|
16
17
|
if (!value || typeof value !== "object") return false;
|
|
17
18
|
const candidate = value;
|
|
@@ -109,7 +110,7 @@ async function loadGeneratedModuleAiTools() {
|
|
|
109
110
|
return 0;
|
|
110
111
|
}
|
|
111
112
|
}
|
|
112
|
-
async function
|
|
113
|
+
async function loadAllModuleToolsUncached() {
|
|
113
114
|
registerMcpTool(contextWhoamiTool, { moduleId: "context" });
|
|
114
115
|
logger.debug("Registered built-in context_whoami tool");
|
|
115
116
|
try {
|
|
@@ -134,6 +135,18 @@ async function loadAllModuleTools() {
|
|
|
134
135
|
logger.error("Could not register API route manifests", { err: error });
|
|
135
136
|
}
|
|
136
137
|
}
|
|
138
|
+
function loadAllModuleTools() {
|
|
139
|
+
if (!allModuleToolsLoad) {
|
|
140
|
+
allModuleToolsLoad = loadAllModuleToolsUncached().catch((error) => {
|
|
141
|
+
allModuleToolsLoad = null;
|
|
142
|
+
throw error;
|
|
143
|
+
});
|
|
144
|
+
}
|
|
145
|
+
return allModuleToolsLoad;
|
|
146
|
+
}
|
|
147
|
+
function resetAllModuleToolsLoadForTests() {
|
|
148
|
+
allModuleToolsLoad = null;
|
|
149
|
+
}
|
|
137
150
|
async function indexToolsForSearch(searchService, force = false) {
|
|
138
151
|
const registry = getToolRegistry();
|
|
139
152
|
const toolSearchService = new ToolSearchService(searchService, registry);
|
|
@@ -162,6 +175,7 @@ export {
|
|
|
162
175
|
loadGeneratedAiToolOverrides,
|
|
163
176
|
loadGeneratedModuleAiTools,
|
|
164
177
|
loadModuleTools,
|
|
165
|
-
registerGeneratedAiToolEntries
|
|
178
|
+
registerGeneratedAiToolEntries,
|
|
179
|
+
resetAllModuleToolsLoadForTests
|
|
166
180
|
};
|
|
167
181
|
//# sourceMappingURL=tool-loader.js.map
|