@open-mercato/ai-assistant 0.6.5-develop.5309.1.be1df535b3 → 0.6.5-develop.5382.1.f542de69af

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.
Files changed (38) hide show
  1. package/.turbo/turbo-build.log +1 -1
  2. package/AGENTS.md +2 -2
  3. package/README.md +1 -1
  4. package/dist/modules/ai_assistant/ai-tools/meta-pack.js +3 -1
  5. package/dist/modules/ai_assistant/ai-tools/meta-pack.js.map +2 -2
  6. package/dist/modules/ai_assistant/api/mcp-key/route.js +95 -0
  7. package/dist/modules/ai_assistant/api/mcp-key/route.js.map +7 -0
  8. package/dist/modules/ai_assistant/components/McpConfigDialog.js +1 -1
  9. package/dist/modules/ai_assistant/components/McpConfigDialog.js.map +2 -2
  10. package/dist/modules/ai_assistant/lib/ai-api-operation-runner.js +3 -1
  11. package/dist/modules/ai_assistant/lib/ai-api-operation-runner.js.map +2 -2
  12. package/dist/modules/ai_assistant/lib/ai-overrides.js.map +1 -1
  13. package/dist/modules/ai_assistant/lib/ai-sdk.js.map +1 -1
  14. package/dist/modules/ai_assistant/lib/chat-config.js.map +1 -1
  15. package/dist/modules/ai_assistant/lib/llm-adapters/anthropic.js.map +1 -1
  16. package/dist/modules/ai_assistant/lib/llm-adapters/google.js.map +1 -1
  17. package/dist/modules/ai_assistant/lib/llm-adapters/openai.js.map +1 -1
  18. package/dist/modules/ai_assistant/lib/llm-bootstrap.js.map +1 -1
  19. package/dist/modules/ai_assistant/lib/openai-compatible-presets.js.map +1 -1
  20. package/dist/modules/ai_assistant/lib/task-plan-stream.js.map +1 -1
  21. package/package.json +6 -6
  22. package/src/modules/ai_assistant/ai-tools/__tests__/meta-pack.test.ts +29 -0
  23. package/src/modules/ai_assistant/ai-tools/meta-pack.ts +7 -1
  24. package/src/modules/ai_assistant/api/mcp-key/__tests__/route.test.ts +144 -0
  25. package/src/modules/ai_assistant/api/mcp-key/route.ts +125 -0
  26. package/src/modules/ai_assistant/components/McpConfigDialog.tsx +5 -1
  27. package/src/modules/ai_assistant/lib/__tests__/ai-overrides.test.ts +1 -1
  28. package/src/modules/ai_assistant/lib/__tests__/task-plan-stream.test.ts +1 -1
  29. package/src/modules/ai_assistant/lib/ai-api-operation-runner.ts +9 -0
  30. package/src/modules/ai_assistant/lib/ai-overrides.ts +1 -1
  31. package/src/modules/ai_assistant/lib/ai-sdk.ts +1 -1
  32. package/src/modules/ai_assistant/lib/chat-config.ts +1 -1
  33. package/src/modules/ai_assistant/lib/llm-adapters/anthropic.ts +1 -1
  34. package/src/modules/ai_assistant/lib/llm-adapters/google.ts +1 -1
  35. package/src/modules/ai_assistant/lib/llm-adapters/openai.ts +1 -1
  36. package/src/modules/ai_assistant/lib/llm-bootstrap.ts +1 -1
  37. package/src/modules/ai_assistant/lib/openai-compatible-presets.ts +1 -1
  38. package/src/modules/ai_assistant/lib/task-plan-stream.ts +1 -1
@@ -1,2 +1,2 @@
1
- [build:ai-assistant] found 213 entry points
1
+ [build:ai-assistant] found 214 entry points
2
2
  [build:ai-assistant] built successfully
package/AGENTS.md CHANGED
@@ -219,7 +219,7 @@ Full reference: `apps/docs/docs/framework/ai-assistant/ui-parts.mdx`.
219
219
 
220
220
  ### How to Override or Extend Another Module's Agent or Tool
221
221
 
222
- Modules can replace/disable any AI agent or AI tool that another module registered, or patch an existing agent by appending, deleting, or replacing allowed tools, system-prompt text, and starter suggestions. Use full overrides when you need to swap the whole behavior; use `aiAgentExtensions` when a downstream module only wants to adjust a shipped agent, such as adding "show catalog stats" while removing an irrelevant starter prompt. See spec `.ai/specs/2026-04-30-ai-overrides-and-module-disable.md` and `apps/docs/docs/framework/ai-assistant/overrides.mdx`.
222
+ Modules can replace/disable any AI agent or AI tool that another module registered, or patch an existing agent by appending, deleting, or replacing allowed tools, system-prompt text, and starter suggestions. Use full overrides when you need to swap the whole behavior; use `aiAgentExtensions` when a downstream module only wants to adjust a shipped agent, such as adding "show catalog stats" while removing an irrelevant starter prompt. See spec `.ai/specs/implemented/2026-04-30-ai-overrides-and-module-disable.md` and `apps/docs/docs/framework/ai-assistant/overrides.mdx`.
223
223
 
224
224
  There are three paths.
225
225
 
@@ -277,7 +277,7 @@ export const aiToolOverrides: AiToolOverridesMap = {
277
277
  }
278
278
  ```
279
279
 
280
- **Path B — `modules.ts` inline (app-level static, unified `entry.overrides`).** Declare overrides under the umbrella `overrides.ai` key on a `ModuleEntry` inside `apps/<app>/src/modules.ts`. Other contracts a module presents (routes, events, workers, widgets, notifications, interceptors, setup, ACL, DI, encryption, …) reuse the same `entry.overrides` shape per spec `.ai/specs/2026-05-04-modules-ts-unified-overrides.md`; phases 1-18 are wired. The app's `bootstrap.ts` calls `applyModuleOverridesFromEnabledModules(enabledModules)` from `@open-mercato/shared/modules/overrides` once at boot — both `apps/mercato` and the `create-mercato-app` template ship that wiring.
280
+ **Path B — `modules.ts` inline (app-level static, unified `entry.overrides`).** Declare overrides under the umbrella `overrides.ai` key on a `ModuleEntry` inside `apps/<app>/src/modules.ts`. Other contracts a module presents (routes, events, workers, widgets, notifications, interceptors, setup, ACL, DI, encryption, …) reuse the same `entry.overrides` shape per spec `.ai/specs/implemented/2026-05-04-modules-ts-unified-overrides.md`; phases 1-18 are wired. The app's `bootstrap.ts` calls `applyModuleOverridesFromEnabledModules(enabledModules)` from `@open-mercato/shared/modules/overrides` once at boot — both `apps/mercato` and the `create-mercato-app` template ship that wiring.
281
281
 
282
282
  ```ts
283
283
  // apps/<app>/src/modules.ts
package/README.md CHANGED
@@ -140,7 +140,7 @@ curl http://localhost:4096/mcp
140
140
  ### Built-in Providers
141
141
 
142
142
  The registry ships 10 built-in providers via the ports & adapters
143
- architecture (see [`.ai/specs/2026-04-14-llm-provider-ports-and-adapters.md`](../../.ai/specs/2026-04-14-llm-provider-ports-and-adapters.md)).
143
+ architecture (see [`.ai/specs/implemented/2026-04-14-llm-provider-ports-and-adapters.md`](../../.ai/specs/implemented/2026-04-14-llm-provider-ports-and-adapters.md)).
144
144
 
145
145
  Native protocol adapters:
146
146
 
@@ -1,5 +1,5 @@
1
1
  import { z } from "zod";
2
- import { listAgents, getAgent } from "../lib/agent-registry.js";
2
+ import { listAgents, getAgent, loadAgentRegistry } from "../lib/agent-registry.js";
3
3
  import { hasRequiredFeatures } from "../lib/auth.js";
4
4
  import { defineAiTool } from "../lib/ai-tool-definition.js";
5
5
  import {
@@ -73,6 +73,7 @@ const listAgentsTool = defineAiTool({
73
73
  const input = listAgentsInput.parse(rawInput);
74
74
  let all = [];
75
75
  try {
76
+ await loadAgentRegistry();
76
77
  all = listAgents();
77
78
  } catch {
78
79
  all = [];
@@ -102,6 +103,7 @@ const describeAgentTool = defineAiTool({
102
103
  const input = describeAgentInput.parse(rawInput);
103
104
  let agent;
104
105
  try {
106
+ await loadAgentRegistry();
105
107
  agent = getAgent(input.agentId);
106
108
  } catch {
107
109
  agent = void 0;
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../../src/modules/ai_assistant/ai-tools/meta-pack.ts"],
4
- "sourcesContent": ["/**\n * General-purpose `meta.*` tool pack (Phase 1 WS-C, Step 3.8).\n *\n * `list_agents` enumerates agents the caller can invoke; `describe_agent`\n * returns a serializable description (Zod output schema \u2192 JSON-Schema when\n * possible, otherwise a `non-serializable` marker). Both tools treat a\n * missing agent registry as an empty list \u2014 the chat runtime must not\n * crash if `ai-agents.generated.ts` has not been emitted yet.\n */\nimport { z } from 'zod'\nimport type { AiAgentDefinition } from '../lib/ai-agent-definition'\nimport { listAgents, getAgent } from '../lib/agent-registry'\nimport { hasRequiredFeatures } from '../lib/auth'\nimport { defineAiTool } from '../lib/ai-tool-definition'\nimport {\n TASK_PLAN_DETAIL_MAX_CHARS,\n TASK_PLAN_ID_MAX_CHARS,\n TASK_PLAN_LABEL_MAX_CHARS,\n TASK_PLAN_MAX_TASKS,\n TASK_PLAN_TOOL_NAME_MAX_CHARS,\n looksLikeHiddenReasoning,\n sanitizeAgentTaskPlanInput,\n} from '../lib/task-plan-labels'\n\nfunction summarizeAgent(agent: AiAgentDefinition): Record<string, unknown> {\n return {\n id: agent.id,\n moduleId: agent.moduleId,\n label: agent.label,\n description: agent.description,\n requiredFeatures: agent.requiredFeatures ?? [],\n allowedTools: agent.allowedTools,\n executionMode: agent.executionMode ?? 'chat',\n mutationPolicy: agent.mutationPolicy ?? 'read-only',\n readOnly: typeof agent.readOnly === 'boolean' ? agent.readOnly : true,\n maxSteps: agent.maxSteps ?? null,\n acceptedMediaTypes: agent.acceptedMediaTypes ?? [],\n domain: agent.domain ?? null,\n keywords: agent.keywords ?? [],\n suggestions: agent.suggestions ?? [],\n taskPlan: agent.taskPlan ?? { enabled: false },\n dataCapabilities: agent.dataCapabilities ?? null,\n hasOutputSchema: Boolean(agent.output),\n hasPageContextResolver: typeof agent.resolvePageContext === 'function',\n }\n}\n\nfunction serializeStructuredOutput(\n output: AiAgentDefinition['output'],\n): Record<string, unknown> | null {\n if (!output) return null\n try {\n const jsonSchema = z.toJSONSchema(output.schema as unknown as z.ZodType, {\n unrepresentable: 'any',\n })\n return {\n schemaName: output.schemaName,\n mode: output.mode ?? 'generate',\n jsonSchema,\n }\n } catch (error) {\n return {\n schemaName: output.schemaName,\n mode: output.mode ?? 'generate',\n note: 'non-serializable',\n error: error instanceof Error ? error.message : String(error),\n }\n }\n}\n\nfunction serializePrompt(agent: AiAgentDefinition): Record<string, unknown> {\n return {\n systemPrompt: agent.systemPrompt,\n hasDynamicPageContext: typeof agent.resolvePageContext === 'function',\n }\n}\n\nconst listAgentsInput = z.object({\n moduleId: z.string().optional().describe('Restrict results to one module id.'),\n})\n\nconst listAgentsTool = defineAiTool({\n name: 'meta.list_agents',\n displayName: 'List agents',\n description:\n 'List registered AI agents the caller can invoke. Filters by requiredFeatures RBAC; returns { agents: [] } if the registry is empty.',\n inputSchema: listAgentsInput,\n requiredFeatures: ['ai_assistant.view'],\n tags: ['read', 'meta'],\n handler: async (rawInput, ctx) => {\n const input = listAgentsInput.parse(rawInput)\n let all: AiAgentDefinition[] = []\n try {\n all = listAgents()\n } catch {\n all = []\n }\n const filtered = all.filter((agent) => {\n if (input.moduleId && agent.moduleId !== input.moduleId) return false\n const features = agent.requiredFeatures ?? []\n return hasRequiredFeatures(features, ctx.userFeatures, ctx.isSuperAdmin)\n })\n return {\n agents: filtered.map(summarizeAgent),\n total: filtered.length,\n }\n },\n})\n\nconst describeAgentInput = z.object({\n agentId: z.string().min(1).describe('Agent id (e.g. \"catalog.merchandising_assistant\").'),\n})\n\nconst describeAgentTool = defineAiTool({\n name: 'meta.describe_agent',\n displayName: 'Describe agent',\n description:\n 'Return metadata, RBAC, allowed tools, execution mode, output schema (JSON-Schema when representable), and prompt shape for a single agent. Never throws \u2014 returns { agent: null, reason } if missing or forbidden.',\n inputSchema: describeAgentInput,\n requiredFeatures: ['ai_assistant.view'],\n tags: ['read', 'meta'],\n handler: async (rawInput, ctx) => {\n const input = describeAgentInput.parse(rawInput)\n let agent: AiAgentDefinition | undefined\n try {\n agent = getAgent(input.agentId)\n } catch {\n agent = undefined\n }\n if (!agent) {\n return { agent: null, reason: 'not_found' as const }\n }\n const features = agent.requiredFeatures ?? []\n if (!hasRequiredFeatures(features, ctx.userFeatures, ctx.isSuperAdmin)) {\n return { agent: null, reason: 'forbidden' as const }\n }\n return {\n agent: {\n ...summarizeAgent(agent),\n output: serializeStructuredOutput(agent.output),\n prompt: serializePrompt(agent),\n },\n }\n },\n})\n\nconst visibleTaskLabel = z\n .string()\n .min(1)\n .max(TASK_PLAN_LABEL_MAX_CHARS)\n .superRefine((value, ctx) => {\n if (!looksLikeHiddenReasoning(value)) return\n ctx.addIssue({\n code: 'custom',\n message: 'Task-plan labels are user-visible UI copy and must not include private reasoning.',\n })\n })\n\nconst visibleTaskDetail = z\n .string()\n .max(TASK_PLAN_DETAIL_MAX_CHARS)\n .optional()\n .superRefine((value, ctx) => {\n if (!value || !looksLikeHiddenReasoning(value)) return\n ctx.addIssue({\n code: 'custom',\n message: 'Task-plan details must not include private reasoning.',\n })\n })\n\nconst updateTaskPlanInput = z.object({\n tasks: z\n .array(\n z.object({\n id: z.string().min(1).max(TASK_PLAN_ID_MAX_CHARS).optional(),\n label: visibleTaskLabel.describe('Concise user-visible step label. Do not include private reasoning.'),\n detail: visibleTaskDetail.describe('Optional short visible detail, not private reasoning.'),\n toolName: z\n .string()\n .min(1)\n .max(TASK_PLAN_TOOL_NAME_MAX_CHARS)\n .optional()\n .describe('Optional whitelisted tool name that this planned step maps to.'),\n }),\n )\n .min(1)\n .max(TASK_PLAN_MAX_TASKS),\n})\n\nconst updateTaskPlanTool = defineAiTool({\n name: 'meta.update_task_plan',\n displayName: 'Update task plan',\n description:\n 'Set the concise user-visible task plan for this assistant turn before calling domain tools. Labels are progress UI, not hidden reasoning.',\n inputSchema: updateTaskPlanInput,\n requiredFeatures: ['ai_assistant.view'],\n tags: ['read', 'meta', 'task-plan'],\n isMutation: false,\n handler: async (rawInput) => {\n const input = updateTaskPlanInput.parse(rawInput)\n const sanitized = sanitizeAgentTaskPlanInput(input)\n return {\n ok: sanitized.tasks.length > 0,\n tasks: sanitized.tasks,\n accepted: sanitized.tasks.length,\n dropped: input.tasks.length - sanitized.tasks.length,\n }\n },\n})\n\nexport const metaAiTools = [\n listAgentsTool,\n describeAgentTool,\n updateTaskPlanTool,\n]\n\nexport default metaAiTools\n"],
5
- "mappings": "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",
4
+ "sourcesContent": ["/**\n * General-purpose `meta.*` tool pack (Phase 1 WS-C, Step 3.8).\n *\n * `list_agents` enumerates agents the caller can invoke; `describe_agent`\n * returns a serializable description (Zod output schema \u2192 JSON-Schema when\n * possible, otherwise a `non-serializable` marker). Both tools treat a\n * missing agent registry as an empty list \u2014 the chat runtime must not\n * crash if `ai-agents.generated.ts` has not been emitted yet.\n */\nimport { z } from 'zod'\nimport type { AiAgentDefinition } from '../lib/ai-agent-definition'\nimport { listAgents, getAgent, loadAgentRegistry } from '../lib/agent-registry'\nimport { hasRequiredFeatures } from '../lib/auth'\nimport { defineAiTool } from '../lib/ai-tool-definition'\nimport {\n TASK_PLAN_DETAIL_MAX_CHARS,\n TASK_PLAN_ID_MAX_CHARS,\n TASK_PLAN_LABEL_MAX_CHARS,\n TASK_PLAN_MAX_TASKS,\n TASK_PLAN_TOOL_NAME_MAX_CHARS,\n looksLikeHiddenReasoning,\n sanitizeAgentTaskPlanInput,\n} from '../lib/task-plan-labels'\n\nfunction summarizeAgent(agent: AiAgentDefinition): Record<string, unknown> {\n return {\n id: agent.id,\n moduleId: agent.moduleId,\n label: agent.label,\n description: agent.description,\n requiredFeatures: agent.requiredFeatures ?? [],\n allowedTools: agent.allowedTools,\n executionMode: agent.executionMode ?? 'chat',\n mutationPolicy: agent.mutationPolicy ?? 'read-only',\n readOnly: typeof agent.readOnly === 'boolean' ? agent.readOnly : true,\n maxSteps: agent.maxSteps ?? null,\n acceptedMediaTypes: agent.acceptedMediaTypes ?? [],\n domain: agent.domain ?? null,\n keywords: agent.keywords ?? [],\n suggestions: agent.suggestions ?? [],\n taskPlan: agent.taskPlan ?? { enabled: false },\n dataCapabilities: agent.dataCapabilities ?? null,\n hasOutputSchema: Boolean(agent.output),\n hasPageContextResolver: typeof agent.resolvePageContext === 'function',\n }\n}\n\nfunction serializeStructuredOutput(\n output: AiAgentDefinition['output'],\n): Record<string, unknown> | null {\n if (!output) return null\n try {\n const jsonSchema = z.toJSONSchema(output.schema as unknown as z.ZodType, {\n unrepresentable: 'any',\n })\n return {\n schemaName: output.schemaName,\n mode: output.mode ?? 'generate',\n jsonSchema,\n }\n } catch (error) {\n return {\n schemaName: output.schemaName,\n mode: output.mode ?? 'generate',\n note: 'non-serializable',\n error: error instanceof Error ? error.message : String(error),\n }\n }\n}\n\nfunction serializePrompt(agent: AiAgentDefinition): Record<string, unknown> {\n return {\n systemPrompt: agent.systemPrompt,\n hasDynamicPageContext: typeof agent.resolvePageContext === 'function',\n }\n}\n\nconst listAgentsInput = z.object({\n moduleId: z.string().optional().describe('Restrict results to one module id.'),\n})\n\nconst listAgentsTool = defineAiTool({\n name: 'meta.list_agents',\n displayName: 'List agents',\n description:\n 'List registered AI agents the caller can invoke. Filters by requiredFeatures RBAC; returns { agents: [] } if the registry is empty.',\n inputSchema: listAgentsInput,\n requiredFeatures: ['ai_assistant.view'],\n tags: ['read', 'meta'],\n handler: async (rawInput, ctx) => {\n const input = listAgentsInput.parse(rawInput)\n let all: AiAgentDefinition[] = []\n try {\n // Lazy-load the registry on first use. The in-app agents route loads it\n // at request time; standalone MCP servers have no such bootstrap, so the\n // tool must ensure it itself. Idempotent \u2014 subsequent calls are no-ops.\n await loadAgentRegistry()\n all = listAgents()\n } catch {\n all = []\n }\n const filtered = all.filter((agent) => {\n if (input.moduleId && agent.moduleId !== input.moduleId) return false\n const features = agent.requiredFeatures ?? []\n return hasRequiredFeatures(features, ctx.userFeatures, ctx.isSuperAdmin)\n })\n return {\n agents: filtered.map(summarizeAgent),\n total: filtered.length,\n }\n },\n})\n\nconst describeAgentInput = z.object({\n agentId: z.string().min(1).describe('Agent id (e.g. \"catalog.merchandising_assistant\").'),\n})\n\nconst describeAgentTool = defineAiTool({\n name: 'meta.describe_agent',\n displayName: 'Describe agent',\n description:\n 'Return metadata, RBAC, allowed tools, execution mode, output schema (JSON-Schema when representable), and prompt shape for a single agent. Never throws \u2014 returns { agent: null, reason } if missing or forbidden.',\n inputSchema: describeAgentInput,\n requiredFeatures: ['ai_assistant.view'],\n tags: ['read', 'meta'],\n handler: async (rawInput, ctx) => {\n const input = describeAgentInput.parse(rawInput)\n let agent: AiAgentDefinition | undefined\n try {\n // Lazy-load the registry on first use (see meta.list_agents). Idempotent.\n await loadAgentRegistry()\n agent = getAgent(input.agentId)\n } catch {\n agent = undefined\n }\n if (!agent) {\n return { agent: null, reason: 'not_found' as const }\n }\n const features = agent.requiredFeatures ?? []\n if (!hasRequiredFeatures(features, ctx.userFeatures, ctx.isSuperAdmin)) {\n return { agent: null, reason: 'forbidden' as const }\n }\n return {\n agent: {\n ...summarizeAgent(agent),\n output: serializeStructuredOutput(agent.output),\n prompt: serializePrompt(agent),\n },\n }\n },\n})\n\nconst visibleTaskLabel = z\n .string()\n .min(1)\n .max(TASK_PLAN_LABEL_MAX_CHARS)\n .superRefine((value, ctx) => {\n if (!looksLikeHiddenReasoning(value)) return\n ctx.addIssue({\n code: 'custom',\n message: 'Task-plan labels are user-visible UI copy and must not include private reasoning.',\n })\n })\n\nconst visibleTaskDetail = z\n .string()\n .max(TASK_PLAN_DETAIL_MAX_CHARS)\n .optional()\n .superRefine((value, ctx) => {\n if (!value || !looksLikeHiddenReasoning(value)) return\n ctx.addIssue({\n code: 'custom',\n message: 'Task-plan details must not include private reasoning.',\n })\n })\n\nconst updateTaskPlanInput = z.object({\n tasks: z\n .array(\n z.object({\n id: z.string().min(1).max(TASK_PLAN_ID_MAX_CHARS).optional(),\n label: visibleTaskLabel.describe('Concise user-visible step label. Do not include private reasoning.'),\n detail: visibleTaskDetail.describe('Optional short visible detail, not private reasoning.'),\n toolName: z\n .string()\n .min(1)\n .max(TASK_PLAN_TOOL_NAME_MAX_CHARS)\n .optional()\n .describe('Optional whitelisted tool name that this planned step maps to.'),\n }),\n )\n .min(1)\n .max(TASK_PLAN_MAX_TASKS),\n})\n\nconst updateTaskPlanTool = defineAiTool({\n name: 'meta.update_task_plan',\n displayName: 'Update task plan',\n description:\n 'Set the concise user-visible task plan for this assistant turn before calling domain tools. Labels are progress UI, not hidden reasoning.',\n inputSchema: updateTaskPlanInput,\n requiredFeatures: ['ai_assistant.view'],\n tags: ['read', 'meta', 'task-plan'],\n isMutation: false,\n handler: async (rawInput) => {\n const input = updateTaskPlanInput.parse(rawInput)\n const sanitized = sanitizeAgentTaskPlanInput(input)\n return {\n ok: sanitized.tasks.length > 0,\n tasks: sanitized.tasks,\n accepted: sanitized.tasks.length,\n dropped: input.tasks.length - sanitized.tasks.length,\n }\n },\n})\n\nexport const metaAiTools = [\n listAgentsTool,\n describeAgentTool,\n updateTaskPlanTool,\n]\n\nexport default metaAiTools\n"],
5
+ "mappings": "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",
6
6
  "names": []
7
7
  }
@@ -0,0 +1,95 @@
1
+ import { NextResponse } from "next/server";
2
+ import { z } from "zod";
3
+ import { getAuthFromRequest } from "@open-mercato/shared/lib/auth/server";
4
+ import { createRequestContainer } from "@open-mercato/shared/lib/di/container";
5
+ import { createApiKey } from "@open-mercato/core/modules/api_keys/services/apiKeyService";
6
+ import { UserRole } from "@open-mercato/core/modules/auth/data/entities";
7
+ import { findWithDecryption } from "@open-mercato/shared/lib/encryption/find";
8
+ const metadata = {
9
+ POST: { requireAuth: true, requireFeatures: ["api_keys.create"] }
10
+ };
11
+ async function getUserRoleIds(em, userId, tenantId) {
12
+ if (!tenantId) return [];
13
+ const links = await findWithDecryption(
14
+ em,
15
+ UserRole,
16
+ { user: userId, role: { tenantId } },
17
+ { populate: ["role"] },
18
+ { tenantId, organizationId: null }
19
+ );
20
+ const linkList = Array.isArray(links) ? links : [];
21
+ return linkList.map((link) => link.role?.id).filter((id) => typeof id === "string" && id.length > 0);
22
+ }
23
+ const bodySchema = z.object({
24
+ name: z.string().min(1).max(120).optional(),
25
+ description: z.string().max(1e3).optional().nullable()
26
+ });
27
+ async function POST(req) {
28
+ const auth = await getAuthFromRequest(req);
29
+ if (!auth?.sub) {
30
+ return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
31
+ }
32
+ try {
33
+ const raw = await req.json().catch(() => ({}));
34
+ const body = bodySchema.parse(raw ?? {});
35
+ const container = await createRequestContainer();
36
+ const em = container.resolve("em");
37
+ const roleIds = await getUserRoleIds(em, auth.sub, auth.tenantId);
38
+ const { record, secret } = await createApiKey(em, {
39
+ name: body.name ?? `MCP Config - ${(/* @__PURE__ */ new Date()).toISOString().slice(0, 10)}`,
40
+ description: body.description ?? "Generated from AI Assistant settings for MCP client",
41
+ tenantId: auth.tenantId ?? null,
42
+ organizationId: auth.orgId ?? null,
43
+ roles: roleIds,
44
+ createdBy: auth.sub
45
+ });
46
+ return NextResponse.json({
47
+ id: record.id,
48
+ name: record.name,
49
+ keyPrefix: record.keyPrefix,
50
+ secret,
51
+ tenantId: record.tenantId ?? null,
52
+ organizationId: record.organizationId ?? null,
53
+ roles: roleIds
54
+ });
55
+ } catch (error) {
56
+ console.error("[MCP Key] Error:", error);
57
+ return NextResponse.json(
58
+ { error: "Failed to create MCP API key" },
59
+ { status: 500 }
60
+ );
61
+ }
62
+ }
63
+ const responseSchema = z.object({
64
+ id: z.string(),
65
+ name: z.string(),
66
+ keyPrefix: z.string(),
67
+ secret: z.string().describe("Full API key value (omk_...). Shown once."),
68
+ tenantId: z.string().uuid().nullable(),
69
+ organizationId: z.string().uuid().nullable(),
70
+ roles: z.array(z.string()).describe("Role ids inherited from the calling user.")
71
+ });
72
+ const errorSchema = z.object({ error: z.string() });
73
+ const openApi = {
74
+ summary: "Create MCP API key",
75
+ description: "Creates a persistent MCP API key that inherits the calling user's roles, for use in an MCP client `.mcp.json`.",
76
+ methods: {
77
+ POST: {
78
+ summary: "Generate MCP API key",
79
+ description: "Generates a persistent `omk_` API key scoped to the calling user's tenant/organization and carrying the caller's own roles, so the key has the same ACL as the user.",
80
+ responses: [
81
+ { status: 200, description: "API key created", schema: responseSchema }
82
+ ],
83
+ errors: [
84
+ { status: 401, description: "Unauthorized", schema: errorSchema },
85
+ { status: 500, description: "Failed to create MCP API key", schema: errorSchema }
86
+ ]
87
+ }
88
+ }
89
+ };
90
+ export {
91
+ POST,
92
+ metadata,
93
+ openApi
94
+ };
95
+ //# sourceMappingURL=route.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../../../../src/modules/ai_assistant/api/mcp-key/route.ts"],
4
+ "sourcesContent": ["import { NextResponse, type NextRequest } from 'next/server'\nimport { z } from 'zod'\nimport type { EntityManager } from '@mikro-orm/postgresql'\nimport { getAuthFromRequest } from '@open-mercato/shared/lib/auth/server'\nimport { createRequestContainer } from '@open-mercato/shared/lib/di/container'\nimport { createApiKey } from '@open-mercato/core/modules/api_keys/services/apiKeyService'\nimport { UserRole } from '@open-mercato/core/modules/auth/data/entities'\nimport { findWithDecryption } from '@open-mercato/shared/lib/encryption/find'\nimport type { OpenApiRouteDoc } from '@open-mercato/shared/lib/openapi'\n\nexport const metadata = {\n POST: { requireAuth: true, requireFeatures: ['api_keys.create'] },\n}\n\n/**\n * Resolve the calling user's role ids for the active tenant. Mirrors the\n * `session-key` route so the generated key carries exactly the caller's roles.\n */\nasync function getUserRoleIds(\n em: EntityManager,\n userId: string,\n tenantId: string | null,\n): Promise<string[]> {\n if (!tenantId) return []\n const links = await findWithDecryption(\n em,\n UserRole,\n { user: userId as any, role: { tenantId } } as any,\n { populate: ['role'] },\n { tenantId, organizationId: null },\n )\n const linkList = Array.isArray(links) ? links : []\n return linkList\n .map((link) => (link.role as any)?.id)\n .filter((id): id is string => typeof id === 'string' && id.length > 0)\n}\n\nconst bodySchema = z.object({\n name: z.string().min(1).max(120).optional(),\n description: z.string().max(1000).optional().nullable(),\n})\n\n/**\n * POST /api/ai_assistant/mcp-key\n *\n * Creates a persistent MCP API key (`omk_...`) for use in `.mcp.json`. Unlike a\n * raw `POST /api/api_keys/keys` call \u2014 which assigns no roles unless the client\n * passes them \u2014 this endpoint resolves the **caller's own roles** server-side\n * and attaches them to the key, so the key inherits the current user's ACL.\n * The user cannot escalate: they only ever grant their own roles to their own\n * key.\n */\nexport async function POST(req: NextRequest) {\n const auth = await getAuthFromRequest(req)\n if (!auth?.sub) {\n return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })\n }\n\n try {\n const raw = await req.json().catch(() => ({}))\n const body = bodySchema.parse(raw ?? {})\n\n const container = await createRequestContainer()\n const em = container.resolve<EntityManager>('em')\n\n const roleIds = await getUserRoleIds(em, auth.sub, auth.tenantId)\n\n const { record, secret } = await createApiKey(em, {\n name: body.name ?? `MCP Config - ${new Date().toISOString().slice(0, 10)}`,\n description: body.description ?? 'Generated from AI Assistant settings for MCP client',\n tenantId: auth.tenantId ?? null,\n organizationId: auth.orgId ?? null,\n roles: roleIds,\n createdBy: auth.sub,\n })\n\n return NextResponse.json({\n id: record.id,\n name: record.name,\n keyPrefix: record.keyPrefix,\n secret,\n tenantId: record.tenantId ?? null,\n organizationId: record.organizationId ?? null,\n roles: roleIds,\n })\n } catch (error) {\n console.error('[MCP Key] Error:', error)\n return NextResponse.json(\n { error: 'Failed to create MCP API key' },\n { status: 500 },\n )\n }\n}\n\nconst responseSchema = z.object({\n id: z.string(),\n name: z.string(),\n keyPrefix: z.string(),\n secret: z.string().describe('Full API key value (omk_...). Shown once.'),\n tenantId: z.string().uuid().nullable(),\n organizationId: z.string().uuid().nullable(),\n roles: z.array(z.string()).describe('Role ids inherited from the calling user.'),\n})\n\nconst errorSchema = z.object({ error: z.string() })\n\nexport const openApi: OpenApiRouteDoc = {\n summary: 'Create MCP API key',\n description:\n 'Creates a persistent MCP API key that inherits the calling user\\'s roles, for use in an MCP client `.mcp.json`.',\n methods: {\n POST: {\n summary: 'Generate MCP API key',\n description:\n 'Generates a persistent `omk_` API key scoped to the calling user\\'s tenant/organization and carrying the caller\\'s own roles, so the key has the same ACL as the user.',\n responses: [\n { status: 200, description: 'API key created', schema: responseSchema },\n ],\n errors: [\n { status: 401, description: 'Unauthorized', schema: errorSchema },\n { status: 500, description: 'Failed to create MCP API key', schema: errorSchema },\n ],\n },\n },\n}\n"],
5
+ "mappings": "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",
6
+ "names": []
7
+ }
@@ -25,7 +25,7 @@ function McpConfigDialog({ open, onOpenChange, mcpUrl }) {
25
25
  setIsGenerating(true);
26
26
  setError(null);
27
27
  try {
28
- const res = await apiCall("/api/api_keys/keys", {
28
+ const res = await apiCall("/api/ai_assistant/mcp-key", {
29
29
  method: "POST",
30
30
  body: JSON.stringify({
31
31
  name: `MCP Config - ${(/* @__PURE__ */ new Date()).toLocaleDateString()}`,
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../../src/modules/ai_assistant/components/McpConfigDialog.tsx"],
4
- "sourcesContent": ["'use client'\n\nimport * as React from 'react'\nimport { useState } from 'react'\nimport { Copy, RefreshCw, AlertTriangle, Check, Settings } from 'lucide-react'\nimport {\n Dialog,\n DialogContent,\n DialogDescription,\n DialogFooter,\n DialogHeader,\n DialogTitle,\n} from '@open-mercato/ui/primitives/dialog'\nimport { Button } from '@open-mercato/ui/primitives/button'\nimport { apiCall } from '@open-mercato/ui/backend/utils/apiCall'\nimport { useT } from '@open-mercato/shared/lib/i18n/context'\nimport { useDialogKeyHandler } from '@open-mercato/ui/hooks/useDialogKeyHandler'\n\ntype Props = {\n open: boolean\n onOpenChange: (next: boolean) => void\n mcpUrl: string\n}\n\ntype ApiKeyResponse = {\n id: string\n name: string\n keyPrefix: string\n secret?: string\n}\n\nexport default function McpConfigDialog({ open, onOpenChange, mcpUrl }: Props) {\n const t = useT()\n const [apiKey, setApiKey] = useState<string | null>(null)\n const [isGenerating, setIsGenerating] = useState(false)\n const [copiedConfig, setCopiedConfig] = useState(false)\n const [copiedKey, setCopiedKey] = useState(false)\n const [error, setError] = useState<string | null>(null)\n\n const generateApiKey = async () => {\n setIsGenerating(true)\n setError(null)\n try {\n const res = await apiCall<ApiKeyResponse>('/api/api_keys/keys', {\n method: 'POST',\n body: JSON.stringify({\n name: `MCP Config - ${new Date().toLocaleDateString()}`,\n description: 'Generated from AI Assistant settings for MCP client',\n }),\n })\n if (res.ok && res.result?.secret) {\n setApiKey(res.result.secret)\n } else {\n setError(t('ai_assistant.mcp.error.failed'))\n }\n } catch (err) {\n setError(err instanceof Error ? err.message : t('ai_assistant.mcp.error.failed'))\n } finally {\n setIsGenerating(false)\n }\n }\n\n const mcpConfig = {\n mcpServers: {\n 'open-mercato': {\n type: 'http',\n url: `${mcpUrl}/mcp`,\n headers: {\n 'x-api-key': apiKey || 'omk_xxxx.yyyy...',\n },\n },\n },\n }\n\n const configJson = JSON.stringify(mcpConfig, null, 2)\n\n const copyConfig = async () => {\n await navigator.clipboard.writeText(configJson)\n setCopiedConfig(true)\n setTimeout(() => setCopiedConfig(false), 2000)\n }\n\n const copyKey = async () => {\n if (apiKey) {\n await navigator.clipboard.writeText(apiKey)\n setCopiedKey(true)\n setTimeout(() => setCopiedKey(false), 2000)\n }\n }\n\n const handleClose = () => {\n setApiKey(null)\n setError(null)\n setCopiedConfig(false)\n setCopiedKey(false)\n onOpenChange(false)\n }\n\n const handleKeyDown = useDialogKeyHandler({ onCancel: handleClose })\n\n return (\n <Dialog open={open} onOpenChange={handleClose}>\n <DialogContent onKeyDown={handleKeyDown} className=\"sm:max-w-2xl\">\n <DialogHeader>\n <DialogTitle className=\"flex items-center gap-2\">\n <Settings className=\"h-5 w-5\" />\n {t('ai_assistant.mcp.title')}\n </DialogTitle>\n <DialogDescription>\n {t('ai_assistant.mcp.description')}\n </DialogDescription>\n </DialogHeader>\n\n <div className=\"space-y-4\">\n {/* Config JSON */}\n <div className=\"relative\">\n <pre className=\"bg-muted/50 rounded-lg p-4 text-xs font-mono overflow-x-auto border\">\n {configJson}\n </pre>\n </div>\n\n {/* API Key Section */}\n <div className=\"flex items-center gap-3 flex-wrap\">\n <span className=\"text-sm text-muted-foreground\">{t('ai_assistant.mcp.apiKeyLabel')}</span>\n {apiKey ? (\n <>\n <code className=\"text-xs bg-muted px-2 py-1 rounded font-mono truncate max-w-[200px]\">\n {apiKey.slice(0, 16)}...\n </code>\n <Button variant=\"outline\" size=\"sm\" onClick={copyKey} className=\"gap-1.5\">\n {copiedKey ? <Check className=\"h-3.5 w-3.5\" /> : <Copy className=\"h-3.5 w-3.5\" />}\n {copiedKey ? t('ai_assistant.mcp.copied') : t('ai_assistant.mcp.copyKey')}\n </Button>\n </>\n ) : (\n <span className=\"text-sm text-muted-foreground italic\">{t('ai_assistant.mcp.notGenerated')}</span>\n )}\n <Button\n variant=\"outline\"\n size=\"sm\"\n onClick={generateApiKey}\n disabled={isGenerating}\n className=\"gap-1.5\"\n >\n <RefreshCw className={`h-3.5 w-3.5 ${isGenerating ? 'animate-spin' : ''}`} />\n {apiKey ? t('ai_assistant.mcp.generateNew') : t('ai_assistant.mcp.generateApiKey')}\n </Button>\n </div>\n\n {error && (\n <div className=\"flex items-center gap-2 text-sm text-destructive\">\n <AlertTriangle className=\"h-4 w-4\" />\n {error}\n </div>\n )}\n\n {apiKey && (\n <div className=\"flex items-start gap-2 p-3 bg-status-warning-bg rounded-lg border border-status-warning-border\">\n <AlertTriangle className=\"h-4 w-4 text-status-warning-icon mt-0.5 shrink-0\" />\n <p className=\"text-sm text-status-warning-text\">\n {t('ai_assistant.mcp.saveKeyWarning')}\n </p>\n </div>\n )}\n </div>\n\n <DialogFooter>\n <Button variant=\"outline\" onClick={handleClose}>\n {t('ai_assistant.mcp.close')}\n </Button>\n <Button onClick={copyConfig} className=\"gap-1.5\">\n {copiedConfig ? <Check className=\"h-4 w-4\" /> : <Copy className=\"h-4 w-4\" />}\n {copiedConfig ? t('ai_assistant.mcp.copied') : t('ai_assistant.mcp.copyConfig')}\n </Button>\n </DialogFooter>\n </DialogContent>\n </Dialog>\n )\n}\n"],
5
- "mappings": "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",
4
+ "sourcesContent": ["'use client'\n\nimport * as React from 'react'\nimport { useState } from 'react'\nimport { Copy, RefreshCw, AlertTriangle, Check, Settings } from 'lucide-react'\nimport {\n Dialog,\n DialogContent,\n DialogDescription,\n DialogFooter,\n DialogHeader,\n DialogTitle,\n} from '@open-mercato/ui/primitives/dialog'\nimport { Button } from '@open-mercato/ui/primitives/button'\nimport { apiCall } from '@open-mercato/ui/backend/utils/apiCall'\nimport { useT } from '@open-mercato/shared/lib/i18n/context'\nimport { useDialogKeyHandler } from '@open-mercato/ui/hooks/useDialogKeyHandler'\n\ntype Props = {\n open: boolean\n onOpenChange: (next: boolean) => void\n mcpUrl: string\n}\n\ntype ApiKeyResponse = {\n id: string\n name: string\n keyPrefix: string\n secret?: string\n}\n\nexport default function McpConfigDialog({ open, onOpenChange, mcpUrl }: Props) {\n const t = useT()\n const [apiKey, setApiKey] = useState<string | null>(null)\n const [isGenerating, setIsGenerating] = useState(false)\n const [copiedConfig, setCopiedConfig] = useState(false)\n const [copiedKey, setCopiedKey] = useState(false)\n const [error, setError] = useState<string | null>(null)\n\n const generateApiKey = async () => {\n setIsGenerating(true)\n setError(null)\n try {\n // Use the dedicated MCP-key endpoint so the key inherits the current\n // user's roles/ACL. A raw POST /api/api_keys/keys assigns no roles, which\n // produced a key with zero features (the MCP server then exposes only\n // `context_whoami`).\n const res = await apiCall<ApiKeyResponse>('/api/ai_assistant/mcp-key', {\n method: 'POST',\n body: JSON.stringify({\n name: `MCP Config - ${new Date().toLocaleDateString()}`,\n description: 'Generated from AI Assistant settings for MCP client',\n }),\n })\n if (res.ok && res.result?.secret) {\n setApiKey(res.result.secret)\n } else {\n setError(t('ai_assistant.mcp.error.failed'))\n }\n } catch (err) {\n setError(err instanceof Error ? err.message : t('ai_assistant.mcp.error.failed'))\n } finally {\n setIsGenerating(false)\n }\n }\n\n const mcpConfig = {\n mcpServers: {\n 'open-mercato': {\n type: 'http',\n url: `${mcpUrl}/mcp`,\n headers: {\n 'x-api-key': apiKey || 'omk_xxxx.yyyy...',\n },\n },\n },\n }\n\n const configJson = JSON.stringify(mcpConfig, null, 2)\n\n const copyConfig = async () => {\n await navigator.clipboard.writeText(configJson)\n setCopiedConfig(true)\n setTimeout(() => setCopiedConfig(false), 2000)\n }\n\n const copyKey = async () => {\n if (apiKey) {\n await navigator.clipboard.writeText(apiKey)\n setCopiedKey(true)\n setTimeout(() => setCopiedKey(false), 2000)\n }\n }\n\n const handleClose = () => {\n setApiKey(null)\n setError(null)\n setCopiedConfig(false)\n setCopiedKey(false)\n onOpenChange(false)\n }\n\n const handleKeyDown = useDialogKeyHandler({ onCancel: handleClose })\n\n return (\n <Dialog open={open} onOpenChange={handleClose}>\n <DialogContent onKeyDown={handleKeyDown} className=\"sm:max-w-2xl\">\n <DialogHeader>\n <DialogTitle className=\"flex items-center gap-2\">\n <Settings className=\"h-5 w-5\" />\n {t('ai_assistant.mcp.title')}\n </DialogTitle>\n <DialogDescription>\n {t('ai_assistant.mcp.description')}\n </DialogDescription>\n </DialogHeader>\n\n <div className=\"space-y-4\">\n {/* Config JSON */}\n <div className=\"relative\">\n <pre className=\"bg-muted/50 rounded-lg p-4 text-xs font-mono overflow-x-auto border\">\n {configJson}\n </pre>\n </div>\n\n {/* API Key Section */}\n <div className=\"flex items-center gap-3 flex-wrap\">\n <span className=\"text-sm text-muted-foreground\">{t('ai_assistant.mcp.apiKeyLabel')}</span>\n {apiKey ? (\n <>\n <code className=\"text-xs bg-muted px-2 py-1 rounded font-mono truncate max-w-[200px]\">\n {apiKey.slice(0, 16)}...\n </code>\n <Button variant=\"outline\" size=\"sm\" onClick={copyKey} className=\"gap-1.5\">\n {copiedKey ? <Check className=\"h-3.5 w-3.5\" /> : <Copy className=\"h-3.5 w-3.5\" />}\n {copiedKey ? t('ai_assistant.mcp.copied') : t('ai_assistant.mcp.copyKey')}\n </Button>\n </>\n ) : (\n <span className=\"text-sm text-muted-foreground italic\">{t('ai_assistant.mcp.notGenerated')}</span>\n )}\n <Button\n variant=\"outline\"\n size=\"sm\"\n onClick={generateApiKey}\n disabled={isGenerating}\n className=\"gap-1.5\"\n >\n <RefreshCw className={`h-3.5 w-3.5 ${isGenerating ? 'animate-spin' : ''}`} />\n {apiKey ? t('ai_assistant.mcp.generateNew') : t('ai_assistant.mcp.generateApiKey')}\n </Button>\n </div>\n\n {error && (\n <div className=\"flex items-center gap-2 text-sm text-destructive\">\n <AlertTriangle className=\"h-4 w-4\" />\n {error}\n </div>\n )}\n\n {apiKey && (\n <div className=\"flex items-start gap-2 p-3 bg-status-warning-bg rounded-lg border border-status-warning-border\">\n <AlertTriangle className=\"h-4 w-4 text-status-warning-icon mt-0.5 shrink-0\" />\n <p className=\"text-sm text-status-warning-text\">\n {t('ai_assistant.mcp.saveKeyWarning')}\n </p>\n </div>\n )}\n </div>\n\n <DialogFooter>\n <Button variant=\"outline\" onClick={handleClose}>\n {t('ai_assistant.mcp.close')}\n </Button>\n <Button onClick={copyConfig} className=\"gap-1.5\">\n {copiedConfig ? <Check className=\"h-4 w-4\" /> : <Copy className=\"h-4 w-4\" />}\n {copiedConfig ? t('ai_assistant.mcp.copied') : t('ai_assistant.mcp.copyConfig')}\n </Button>\n </DialogFooter>\n </DialogContent>\n </Dialog>\n )\n}\n"],
5
+ "mappings": "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",
6
6
  "names": []
7
7
  }
@@ -37,13 +37,15 @@ function buildAuthEnvelope(ctx) {
37
37
  if (!userId) {
38
38
  return { auth: null, status: "invalid" };
39
39
  }
40
+ const apiKeyId = userId.startsWith("api_key:") ? userId.slice("api_key:".length) : void 0;
40
41
  const auth = {
41
42
  sub: userId,
42
43
  userId,
43
44
  tenantId: ctx.tenantId,
44
45
  orgId: ctx.organizationId,
45
46
  roles: [],
46
- isSuperAdmin: ctx.isSuperAdmin
47
+ isSuperAdmin: ctx.isSuperAdmin,
48
+ ...apiKeyId ? { keyId: apiKeyId } : {}
47
49
  };
48
50
  return { auth, status: "authenticated" };
49
51
  }
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../../src/modules/ai_assistant/lib/ai-api-operation-runner.ts"],
4
- "sourcesContent": ["// Phase 1 of spec 2026-04-27-ai-tools-api-backed-dry-refactor.md.\n//\n// In-process API operation runner used by typed AI tools to reuse existing\n// API route handlers without HTTP, fetch, or a second RBAC pass. The runner\n// resolves the matched route entry from the generated `apiRoutes` manifest,\n// validates that the route is documented (`openApi`) and that mutation routes\n// declare `requiredFeatures`, asserts the route's required features are\n// covered by the tool definition, then invokes the route handler directly with\n// a synthetic Request that carries a Symbol-keyed trusted-auth envelope so the\n// shared auth resolver short-circuits cookie/JWT/API-key parsing.\nimport {\n attachTrustedAuthContext,\n type AuthContext,\n type TrustedAuthContextEnvelope,\n} from '@open-mercato/shared/lib/auth/server'\nimport {\n findApiRouteManifestMatch,\n getApiRouteManifests,\n type ApiRouteManifestEntry,\n type RouteMatchParams,\n} from '@open-mercato/shared/modules/registry'\nimport { hasAllFeatures } from '@open-mercato/shared/security/features'\nimport type { AiToolDefinition, McpToolContext } from './types'\n\nexport type AiApiHttpMethod = 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE'\n\nconst MUTATION_METHODS: ReadonlySet<AiApiHttpMethod> = new Set(['POST', 'PUT', 'PATCH', 'DELETE'])\n\nconst SYNTHETIC_ORIGIN = 'http://internal.local'\n\nexport type AiApiOperationRequest = {\n method: AiApiHttpMethod\n path: string\n query?: Record<string, string | number | boolean | null | undefined>\n body?: Record<string, unknown>\n allowFeaturelessMutation?: boolean\n}\n\nexport type AiApiOperationResponse<T = unknown> = {\n success: boolean\n statusCode: number\n data?: T\n error?: string\n details?: unknown\n}\n\nexport interface AiToolExecutionContext extends McpToolContext {\n tool: AiToolDefinition\n}\n\nexport type AiApiOperationRunnerOptions = {\n /** Optional override of the API route manifest (used by tests). */\n apiRoutes?: ApiRouteManifestEntry[]\n /** Custom loader for the API route manifest (used by tests). */\n loadApiRoutes?: () => Promise<ApiRouteManifestEntry[]>\n}\n\nexport type AiApiOperationRunner = {\n run<T = unknown>(request: AiApiOperationRequest): Promise<AiApiOperationResponse<T>>\n}\n\ntype ResolvedHandler = (req: Request, ctx?: { params: RouteMatchParams }) => Promise<Response> | Response\n\ntype LoadedRouteModule = Record<string, unknown>\n\ntype MethodMetadata = {\n requireAuth?: boolean\n requireFeatures?: string[]\n}\n\nfunction normalizeMethod(method: string): AiApiHttpMethod {\n const upper = method.toUpperCase()\n if (upper === 'GET' || upper === 'POST' || upper === 'PUT' || upper === 'PATCH' || upper === 'DELETE') {\n return upper\n }\n throw new Error(`Unsupported method \"${method}\"`)\n}\n\n// Exported for unit testing \u2014 see __tests__/ai-api-operation-runner.test.ts.\n// `path` arrives from agent tool inputs (LLM-controlled), so trailing-slash\n// stripping is implemented as a linear character-code scan rather than the\n// /\\/+$/ regex (CodeQL js/polynomial-redos: regex exhibits polynomial\n// backtracking on long runs of `/`). The loop is O(n) regardless of input.\nexport function normalizePath(path: string): string {\n if (typeof path !== 'string' || path.length === 0) return '/'\n const trimmed = path.startsWith('/') ? path : `/${path}`\n let end = trimmed.length\n while (end > 1 && trimmed.charCodeAt(end - 1) === 47 /* '/' */) end--\n return trimmed.slice(0, end)\n}\n\nfunction buildUrl(path: string, query?: AiApiOperationRequest['query']): URL {\n const url = new URL(`${SYNTHETIC_ORIGIN}/api${normalizePath(path)}`)\n if (query) {\n for (const [key, value] of Object.entries(query)) {\n if (value === null || value === undefined) continue\n url.searchParams.append(key, String(value))\n }\n }\n return url\n}\n\nfunction buildAuthEnvelope(ctx: AiToolExecutionContext): TrustedAuthContextEnvelope {\n const userId = ctx.userId\n if (!userId) {\n return { auth: null, status: 'invalid' }\n }\n const auth: AuthContext = {\n sub: userId,\n userId,\n tenantId: ctx.tenantId,\n orgId: ctx.organizationId,\n roles: [],\n isSuperAdmin: ctx.isSuperAdmin,\n }\n return { auth, status: 'authenticated' }\n}\n\nfunction pickHandler(entry: ApiRouteManifestEntry, mod: LoadedRouteModule, method: AiApiHttpMethod): ResolvedHandler | null {\n const direct = mod[method]\n if (typeof direct === 'function') return direct as ResolvedHandler\n if (entry.kind === 'legacy') {\n const fallback = mod.default ?? mod.handler\n if (typeof fallback === 'function') return fallback as ResolvedHandler\n }\n return null\n}\n\nfunction extractMethodMetadata(metadata: unknown, method: AiApiHttpMethod): MethodMetadata | null {\n if (!metadata || typeof metadata !== 'object') return null\n const record = metadata as Record<string, unknown>\n const perMethod = record[method]\n const source: Record<string, unknown> = perMethod && typeof perMethod === 'object'\n ? (perMethod as Record<string, unknown>)\n : record\n const result: MethodMetadata = {}\n if (typeof source.requireAuth === 'boolean') {\n result.requireAuth = source.requireAuth\n }\n if (Array.isArray(source.requireFeatures)) {\n const features = source.requireFeatures.filter((entry): entry is string => typeof entry === 'string' && entry.length > 0)\n if (features.length > 0) result.requireFeatures = features\n }\n return result\n}\n\nasync function readResponseBody(res: Response): Promise<unknown> {\n const contentType = res.headers.get('content-type') ?? ''\n if (contentType.includes('application/json')) {\n try {\n return await res.json()\n } catch {\n return null\n }\n }\n try {\n const text = await res.text()\n if (!text) return null\n try {\n return JSON.parse(text)\n } catch {\n return text\n }\n } catch {\n return null\n }\n}\n\nfunction failure(statusCode: number, error: string, details?: unknown): AiApiOperationResponse {\n const response: AiApiOperationResponse = { success: false, statusCode, error }\n if (details !== undefined) response.details = details\n return response\n}\n\nfunction normalizeError(body: unknown): { error: string; details?: unknown } {\n if (body && typeof body === 'object' && !Array.isArray(body)) {\n const record = body as Record<string, unknown>\n const message = typeof record.error === 'string'\n ? record.error\n : typeof record.message === 'string'\n ? record.message\n : null\n if (message) {\n const { error: _ignored, message: _ignoredMessage, ...rest } = record\n void _ignored\n void _ignoredMessage\n return Object.keys(rest).length > 0 ? { error: message, details: rest } : { error: message }\n }\n }\n if (typeof body === 'string' && body.length > 0) {\n return { error: body }\n }\n return { error: 'Request failed' }\n}\n\nexport function createAiApiOperationRunner(\n ctx: AiToolExecutionContext,\n options: AiApiOperationRunnerOptions = {},\n): AiApiOperationRunner {\n let manifestPromise: Promise<ApiRouteManifestEntry[]> | null = null\n\n const loadManifest = async (): Promise<ApiRouteManifestEntry[]> => {\n if (options.apiRoutes) return options.apiRoutes\n if (manifestPromise) return manifestPromise\n const loader = options.loadApiRoutes ?? defaultLoadApiRoutes\n manifestPromise = loader()\n try {\n return await manifestPromise\n } catch (error) {\n manifestPromise = null\n throw error\n }\n }\n\n return {\n async run<T = unknown>(request: AiApiOperationRequest): Promise<AiApiOperationResponse<T>> {\n let method: AiApiHttpMethod\n try {\n method = normalizeMethod(request.method)\n } catch (error) {\n const message = error instanceof Error ? error.message : 'Invalid method'\n return failure(400, message) as AiApiOperationResponse<T>\n }\n const path = normalizePath(request.path)\n\n let routes: ApiRouteManifestEntry[]\n try {\n routes = await loadManifest()\n } catch (error) {\n const message = error instanceof Error ? error.message : 'Failed to load API route manifest'\n return failure(500, `Operation runner manifest unavailable: ${message}`) as AiApiOperationResponse<T>\n }\n\n const match = findApiRouteManifestMatch(routes, method, path)\n if (!match) {\n return failure(\n 404,\n `No documented API route matches ${method} ${path}`,\n ) as AiApiOperationResponse<T>\n }\n\n let mod: LoadedRouteModule\n try {\n mod = (await match.route.load()) as LoadedRouteModule\n } catch (error) {\n const message = error instanceof Error ? error.message : 'Failed to load route module'\n return failure(500, `Failed to load route module: ${message}`) as AiApiOperationResponse<T>\n }\n\n if (!('openApi' in mod) || mod.openApi === undefined || mod.openApi === null) {\n return failure(\n 501,\n `Route ${method} ${path} is undocumented (missing openApi export); refusing to call from AI tool`,\n ) as AiApiOperationResponse<T>\n }\n\n const handler = pickHandler(match.route, mod, method)\n if (!handler) {\n return failure(\n 405,\n `Route ${path} does not export a handler for method ${method}`,\n ) as AiApiOperationResponse<T>\n }\n\n const methodMetadata = extractMethodMetadata(mod.metadata, method)\n const routeFeatures = methodMetadata?.requireFeatures ?? []\n const isMutation = MUTATION_METHODS.has(method)\n\n if (isMutation && routeFeatures.length === 0 && !request.allowFeaturelessMutation) {\n return failure(\n 403,\n `Mutation route ${method} ${path} declares no requiredFeatures; refusing to call without allowFeaturelessMutation opt-in`,\n ) as AiApiOperationResponse<T>\n }\n\n const toolFeatures = ctx.tool.requiredFeatures ?? []\n if (routeFeatures.length > 0 && !hasAllFeatures(toolFeatures, routeFeatures)) {\n return failure(\n 403,\n `AI tool \"${ctx.tool.name}\" requiredFeatures do not cover route ${method} ${path} requiredFeatures`,\n { toolFeatures, routeFeatures },\n ) as AiApiOperationResponse<T>\n }\n\n const url = buildUrl(path, request.query)\n const headers = new Headers()\n const requestInit: RequestInit = { method, headers }\n if (request.body !== undefined && method !== 'GET') {\n headers.set('content-type', 'application/json')\n requestInit.body = JSON.stringify(request.body)\n }\n\n const syntheticRequest = new Request(url, requestInit)\n attachTrustedAuthContext(syntheticRequest, buildAuthEnvelope(ctx))\n\n let response: Response\n try {\n response = await handler(syntheticRequest, { params: match.params })\n } catch (error) {\n const message = error instanceof Error ? error.message : 'Route handler threw'\n return failure(500, message) as AiApiOperationResponse<T>\n }\n\n const status = response.status\n const body = await readResponseBody(response)\n if (status >= 200 && status < 300) {\n return {\n success: true,\n statusCode: status,\n data: body as T,\n }\n }\n const normalized = normalizeError(body)\n return {\n success: false,\n statusCode: status,\n error: normalized.error,\n ...(normalized.details !== undefined ? { details: normalized.details } : {}),\n }\n },\n }\n}\n\nasync function defaultLoadApiRoutes(): Promise<ApiRouteManifestEntry[]> {\n const registered = getApiRouteManifests()\n if (registered.length === 0) {\n throw new Error(\n 'No API route manifest registered. Call registerApiRouteManifests(...) at app bootstrap or pass apiRoutes/loadApiRoutes to createAiApiOperationRunner.',\n )\n }\n return registered\n}\n\n"],
5
- "mappings": "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",
4
+ "sourcesContent": ["// Phase 1 of spec 2026-04-27-ai-tools-api-backed-dry-refactor.md.\n//\n// In-process API operation runner used by typed AI tools to reuse existing\n// API route handlers without HTTP, fetch, or a second RBAC pass. The runner\n// resolves the matched route entry from the generated `apiRoutes` manifest,\n// validates that the route is documented (`openApi`) and that mutation routes\n// declare `requiredFeatures`, asserts the route's required features are\n// covered by the tool definition, then invokes the route handler directly with\n// a synthetic Request that carries a Symbol-keyed trusted-auth envelope so the\n// shared auth resolver short-circuits cookie/JWT/API-key parsing.\nimport {\n attachTrustedAuthContext,\n type AuthContext,\n type TrustedAuthContextEnvelope,\n} from '@open-mercato/shared/lib/auth/server'\nimport {\n findApiRouteManifestMatch,\n getApiRouteManifests,\n type ApiRouteManifestEntry,\n type RouteMatchParams,\n} from '@open-mercato/shared/modules/registry'\nimport { hasAllFeatures } from '@open-mercato/shared/security/features'\nimport type { AiToolDefinition, McpToolContext } from './types'\n\nexport type AiApiHttpMethod = 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE'\n\nconst MUTATION_METHODS: ReadonlySet<AiApiHttpMethod> = new Set(['POST', 'PUT', 'PATCH', 'DELETE'])\n\nconst SYNTHETIC_ORIGIN = 'http://internal.local'\n\nexport type AiApiOperationRequest = {\n method: AiApiHttpMethod\n path: string\n query?: Record<string, string | number | boolean | null | undefined>\n body?: Record<string, unknown>\n allowFeaturelessMutation?: boolean\n}\n\nexport type AiApiOperationResponse<T = unknown> = {\n success: boolean\n statusCode: number\n data?: T\n error?: string\n details?: unknown\n}\n\nexport interface AiToolExecutionContext extends McpToolContext {\n tool: AiToolDefinition\n}\n\nexport type AiApiOperationRunnerOptions = {\n /** Optional override of the API route manifest (used by tests). */\n apiRoutes?: ApiRouteManifestEntry[]\n /** Custom loader for the API route manifest (used by tests). */\n loadApiRoutes?: () => Promise<ApiRouteManifestEntry[]>\n}\n\nexport type AiApiOperationRunner = {\n run<T = unknown>(request: AiApiOperationRequest): Promise<AiApiOperationResponse<T>>\n}\n\ntype ResolvedHandler = (req: Request, ctx?: { params: RouteMatchParams }) => Promise<Response> | Response\n\ntype LoadedRouteModule = Record<string, unknown>\n\ntype MethodMetadata = {\n requireAuth?: boolean\n requireFeatures?: string[]\n}\n\nfunction normalizeMethod(method: string): AiApiHttpMethod {\n const upper = method.toUpperCase()\n if (upper === 'GET' || upper === 'POST' || upper === 'PUT' || upper === 'PATCH' || upper === 'DELETE') {\n return upper\n }\n throw new Error(`Unsupported method \"${method}\"`)\n}\n\n// Exported for unit testing \u2014 see __tests__/ai-api-operation-runner.test.ts.\n// `path` arrives from agent tool inputs (LLM-controlled), so trailing-slash\n// stripping is implemented as a linear character-code scan rather than the\n// /\\/+$/ regex (CodeQL js/polynomial-redos: regex exhibits polynomial\n// backtracking on long runs of `/`). The loop is O(n) regardless of input.\nexport function normalizePath(path: string): string {\n if (typeof path !== 'string' || path.length === 0) return '/'\n const trimmed = path.startsWith('/') ? path : `/${path}`\n let end = trimmed.length\n while (end > 1 && trimmed.charCodeAt(end - 1) === 47 /* '/' */) end--\n return trimmed.slice(0, end)\n}\n\nfunction buildUrl(path: string, query?: AiApiOperationRequest['query']): URL {\n const url = new URL(`${SYNTHETIC_ORIGIN}/api${normalizePath(path)}`)\n if (query) {\n for (const [key, value] of Object.entries(query)) {\n if (value === null || value === undefined) continue\n url.searchParams.append(key, String(value))\n }\n }\n return url\n}\n\nfunction buildAuthEnvelope(ctx: AiToolExecutionContext): TrustedAuthContextEnvelope {\n const userId = ctx.userId\n if (!userId) {\n return { auth: null, status: 'invalid' }\n }\n // MCP api-key contexts use an `api_key:<uuid>` subject (see auth.ts). Surface\n // the bare key UUID as `keyId` so downstream actor logging resolves to a valid\n // UUID \u2014 the CRUD factory computes `actorUserId = auth.keyId ?? auth.sub`, and\n // the access-log schema rejects a non-UUID `actorUserId`. This matches real\n // HTTP api-key auth, where `keyId` is the api key record id.\n const apiKeyId = userId.startsWith('api_key:')\n ? userId.slice('api_key:'.length)\n : undefined\n const auth: AuthContext = {\n sub: userId,\n userId,\n tenantId: ctx.tenantId,\n orgId: ctx.organizationId,\n roles: [],\n isSuperAdmin: ctx.isSuperAdmin,\n ...(apiKeyId ? { keyId: apiKeyId } : {}),\n }\n return { auth, status: 'authenticated' }\n}\n\nfunction pickHandler(entry: ApiRouteManifestEntry, mod: LoadedRouteModule, method: AiApiHttpMethod): ResolvedHandler | null {\n const direct = mod[method]\n if (typeof direct === 'function') return direct as ResolvedHandler\n if (entry.kind === 'legacy') {\n const fallback = mod.default ?? mod.handler\n if (typeof fallback === 'function') return fallback as ResolvedHandler\n }\n return null\n}\n\nfunction extractMethodMetadata(metadata: unknown, method: AiApiHttpMethod): MethodMetadata | null {\n if (!metadata || typeof metadata !== 'object') return null\n const record = metadata as Record<string, unknown>\n const perMethod = record[method]\n const source: Record<string, unknown> = perMethod && typeof perMethod === 'object'\n ? (perMethod as Record<string, unknown>)\n : record\n const result: MethodMetadata = {}\n if (typeof source.requireAuth === 'boolean') {\n result.requireAuth = source.requireAuth\n }\n if (Array.isArray(source.requireFeatures)) {\n const features = source.requireFeatures.filter((entry): entry is string => typeof entry === 'string' && entry.length > 0)\n if (features.length > 0) result.requireFeatures = features\n }\n return result\n}\n\nasync function readResponseBody(res: Response): Promise<unknown> {\n const contentType = res.headers.get('content-type') ?? ''\n if (contentType.includes('application/json')) {\n try {\n return await res.json()\n } catch {\n return null\n }\n }\n try {\n const text = await res.text()\n if (!text) return null\n try {\n return JSON.parse(text)\n } catch {\n return text\n }\n } catch {\n return null\n }\n}\n\nfunction failure(statusCode: number, error: string, details?: unknown): AiApiOperationResponse {\n const response: AiApiOperationResponse = { success: false, statusCode, error }\n if (details !== undefined) response.details = details\n return response\n}\n\nfunction normalizeError(body: unknown): { error: string; details?: unknown } {\n if (body && typeof body === 'object' && !Array.isArray(body)) {\n const record = body as Record<string, unknown>\n const message = typeof record.error === 'string'\n ? record.error\n : typeof record.message === 'string'\n ? record.message\n : null\n if (message) {\n const { error: _ignored, message: _ignoredMessage, ...rest } = record\n void _ignored\n void _ignoredMessage\n return Object.keys(rest).length > 0 ? { error: message, details: rest } : { error: message }\n }\n }\n if (typeof body === 'string' && body.length > 0) {\n return { error: body }\n }\n return { error: 'Request failed' }\n}\n\nexport function createAiApiOperationRunner(\n ctx: AiToolExecutionContext,\n options: AiApiOperationRunnerOptions = {},\n): AiApiOperationRunner {\n let manifestPromise: Promise<ApiRouteManifestEntry[]> | null = null\n\n const loadManifest = async (): Promise<ApiRouteManifestEntry[]> => {\n if (options.apiRoutes) return options.apiRoutes\n if (manifestPromise) return manifestPromise\n const loader = options.loadApiRoutes ?? defaultLoadApiRoutes\n manifestPromise = loader()\n try {\n return await manifestPromise\n } catch (error) {\n manifestPromise = null\n throw error\n }\n }\n\n return {\n async run<T = unknown>(request: AiApiOperationRequest): Promise<AiApiOperationResponse<T>> {\n let method: AiApiHttpMethod\n try {\n method = normalizeMethod(request.method)\n } catch (error) {\n const message = error instanceof Error ? error.message : 'Invalid method'\n return failure(400, message) as AiApiOperationResponse<T>\n }\n const path = normalizePath(request.path)\n\n let routes: ApiRouteManifestEntry[]\n try {\n routes = await loadManifest()\n } catch (error) {\n const message = error instanceof Error ? error.message : 'Failed to load API route manifest'\n return failure(500, `Operation runner manifest unavailable: ${message}`) as AiApiOperationResponse<T>\n }\n\n const match = findApiRouteManifestMatch(routes, method, path)\n if (!match) {\n return failure(\n 404,\n `No documented API route matches ${method} ${path}`,\n ) as AiApiOperationResponse<T>\n }\n\n let mod: LoadedRouteModule\n try {\n mod = (await match.route.load()) as LoadedRouteModule\n } catch (error) {\n const message = error instanceof Error ? error.message : 'Failed to load route module'\n return failure(500, `Failed to load route module: ${message}`) as AiApiOperationResponse<T>\n }\n\n if (!('openApi' in mod) || mod.openApi === undefined || mod.openApi === null) {\n return failure(\n 501,\n `Route ${method} ${path} is undocumented (missing openApi export); refusing to call from AI tool`,\n ) as AiApiOperationResponse<T>\n }\n\n const handler = pickHandler(match.route, mod, method)\n if (!handler) {\n return failure(\n 405,\n `Route ${path} does not export a handler for method ${method}`,\n ) as AiApiOperationResponse<T>\n }\n\n const methodMetadata = extractMethodMetadata(mod.metadata, method)\n const routeFeatures = methodMetadata?.requireFeatures ?? []\n const isMutation = MUTATION_METHODS.has(method)\n\n if (isMutation && routeFeatures.length === 0 && !request.allowFeaturelessMutation) {\n return failure(\n 403,\n `Mutation route ${method} ${path} declares no requiredFeatures; refusing to call without allowFeaturelessMutation opt-in`,\n ) as AiApiOperationResponse<T>\n }\n\n const toolFeatures = ctx.tool.requiredFeatures ?? []\n if (routeFeatures.length > 0 && !hasAllFeatures(toolFeatures, routeFeatures)) {\n return failure(\n 403,\n `AI tool \"${ctx.tool.name}\" requiredFeatures do not cover route ${method} ${path} requiredFeatures`,\n { toolFeatures, routeFeatures },\n ) as AiApiOperationResponse<T>\n }\n\n const url = buildUrl(path, request.query)\n const headers = new Headers()\n const requestInit: RequestInit = { method, headers }\n if (request.body !== undefined && method !== 'GET') {\n headers.set('content-type', 'application/json')\n requestInit.body = JSON.stringify(request.body)\n }\n\n const syntheticRequest = new Request(url, requestInit)\n attachTrustedAuthContext(syntheticRequest, buildAuthEnvelope(ctx))\n\n let response: Response\n try {\n response = await handler(syntheticRequest, { params: match.params })\n } catch (error) {\n const message = error instanceof Error ? error.message : 'Route handler threw'\n return failure(500, message) as AiApiOperationResponse<T>\n }\n\n const status = response.status\n const body = await readResponseBody(response)\n if (status >= 200 && status < 300) {\n return {\n success: true,\n statusCode: status,\n data: body as T,\n }\n }\n const normalized = normalizeError(body)\n return {\n success: false,\n statusCode: status,\n error: normalized.error,\n ...(normalized.details !== undefined ? { details: normalized.details } : {}),\n }\n },\n }\n}\n\nasync function defaultLoadApiRoutes(): Promise<ApiRouteManifestEntry[]> {\n const registered = getApiRouteManifests()\n if (registered.length === 0) {\n throw new Error(\n 'No API route manifest registered. Call registerApiRouteManifests(...) at app bootstrap or pass apiRoutes/loadApiRoutes to createAiApiOperationRunner.',\n )\n }\n return registered\n}\n\n"],
5
+ "mappings": "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",
6
6
  "names": []
7
7
  }
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../../src/modules/ai_assistant/lib/ai-overrides.ts"],
4
- "sourcesContent": ["import {\n registerModuleOverrideApplier,\n type ModuleOverrideEntry,\n} from '@open-mercato/shared/modules/overrides'\n\n/**\n * Module-to-module override pipeline for AI agents and AI tools.\n *\n * Modules contribute overrides through two surfaces:\n *\n * 1. Per-module: declare additional `aiAgentOverrides` / `aiToolOverrides`\n * exports in the existing `<module>/ai-agents.ts` / `<module>/ai-tools.ts`\n * files (no separate `ai-overrides.ts` file). The generator picks the\n * exports up alongside `aiAgents` / `aiTools` and emits override entries\n * inside `apps/<app>/.mercato/generated/ai-agents.generated.ts`\n * (`aiAgentOverrideEntries`) and `ai-tools.generated.ts`\n * (`aiToolOverrideEntries`).\n *\n * 2. App-level: declare `aiAgentOverrides` / `aiToolOverrides` directly on a\n * `ModuleEntry` inside the app's `src/modules.ts`. {@link\n * applyAiOverridesFromEnabledModules} feeds these into the runtime; they\n * sit one tier higher than the per-module file-based entries but below\n * explicit programmatic calls.\n *\n * Tests and bootstrap code can also override imperatively via\n * {@link applyAiAgentOverrides} and {@link applyAiToolOverrides}, which\n * supersede every other tier and persist for the process lifetime.\n *\n * `null` always means \"remove from the registry\"; a definition replaces.\n *\n * @see ../../../../../../.ai/specs/2026-04-30-ai-overrides-and-module-disable.md\n */\nimport type { AiAgentDefinition, AiAgentExtension } from './ai-agent-definition'\nimport type { AiToolDefinition } from './types'\n\n/** Override for a single agent: replace with a definition or remove with `null`. */\nexport type AiAgentOverride = AiAgentDefinition | null\n\n/** Override for a single tool: replace with a definition or remove with `null`. */\nexport type AiToolOverride = AiToolDefinition | null\n\n/** Map of agent id \u2192 override. Used in the per-module `ai-agents.ts` file. */\nexport type AiAgentOverridesMap = Record<string, AiAgentOverride>\n\n/** Map of tool name \u2192 override. Used in the per-module `ai-tools.ts` file. */\nexport type AiToolOverridesMap = Record<string, AiToolOverride>\n\n/**\n * Per-entry shape produced by the agent generator. Mirrors the per-module\n * record format used elsewhere in the registry generators so the file\n * stays grep-friendly.\n */\nexport interface AiAgentOverrideConfigEntry {\n moduleId: string\n overrides: AiAgentOverridesMap\n}\n\nexport interface AiAgentExtensionConfigEntry {\n moduleId: string\n extensions: AiAgentExtension[]\n}\n\n/**\n * Per-entry shape produced by the tool generator. Same record format as\n * {@link AiAgentOverrideConfigEntry}, but with tool definitions.\n */\nexport interface AiToolOverrideConfigEntry {\n moduleId: string\n overrides: AiToolOverridesMap\n}\n\n/** Shape of the `entry.overrides.ai` sub-tree on a `modules.ts` entry. */\nexport interface AiModuleOverridesShape {\n agents?: AiAgentOverridesMap\n tools?: AiToolOverridesMap\n extensions?: AiAgentExtension[]\n}\n\n/** Shape of a `modules.ts` `ModuleEntry` with the umbrella `overrides.ai`. */\nexport interface EnabledModuleAiOverrides {\n id: string\n overrides?: { ai?: AiModuleOverridesShape }\n}\n\nconst programmaticAgentOverrides: AiAgentOverridesMap = {}\nconst programmaticToolOverrides: AiToolOverridesMap = {}\nconst programmaticAgentExtensions: AiAgentExtension[] = []\n\nconst modulesConfigAgentOverrides: AiAgentOverridesMap = {}\nconst modulesConfigToolOverrides: AiToolOverridesMap = {}\nconst modulesConfigAgentExtensions: AiAgentExtension[] = []\n\n/**\n * Apply programmatic agent overrides \u2014 survive after the registries load\n * and take precedence over both file-based and `modules.ts`-tier overrides\n * for the same id.\n *\n * @example\n * ```ts\n * applyAiAgentOverrides({\n * 'catalog.catalog_assistant': null, // disable\n * 'catalog.merchandising_assistant': customAgent, // replace\n * })\n * ```\n */\nexport function applyAiAgentOverrides(overrides: AiAgentOverridesMap): void {\n for (const [id, value] of Object.entries(overrides)) {\n programmaticAgentOverrides[id] = value\n }\n}\n\n/**\n * Apply programmatic tool overrides \u2014 survive after the registries load\n * and take precedence over both file-based and `modules.ts`-tier overrides\n * for the same name.\n */\nexport function applyAiToolOverrides(overrides: AiToolOverridesMap): void {\n for (const [name, value] of Object.entries(overrides)) {\n programmaticToolOverrides[name] = value\n }\n}\n\n/**\n * Apply programmatic additive agent extensions. Extensions append to an\n * already-registered agent after replacements/disable overrides resolve.\n */\nexport function applyAiAgentExtensions(extensions: readonly AiAgentExtension[]): void {\n programmaticAgentExtensions.push(...extensions)\n}\n\n/**\n * Walk a list of `enabledModules` entries (the `apps/<app>/src/modules.ts`\n * shape) and register their `overrides.ai.agents` / `overrides.ai.tools`\n * into the `modules.ts` tier. Tier precedence (highest first):\n *\n * 1. {@link applyAiAgentOverrides} / {@link applyAiToolOverrides}\n * 2. `modules.ts` inline (this function)\n * 3. `<module>/ai-agents.ts` `aiAgentOverrides` / `<module>/ai-tools.ts`\n * `aiToolOverrides`\n * 4. base `aiAgents` / `aiTools`\n *\n * Calling this multiple times is additive: later calls overlay on the\n * existing tier (last wins per id). Use only at boot time \u2014 re-entering\n * mid-request blurs the resolution order.\n *\n * In practice this is invoked from `applyModuleOverridesFromEnabledModules`\n * (the umbrella dispatcher in `@open-mercato/shared/modules/overrides`)\n * via the registered `'ai'` applier; downstream apps call the dispatcher\n * once from `bootstrap.ts`. The standalone signature is kept for tests\n * and ad-hoc use.\n */\nexport function applyAiOverridesFromEnabledModules(\n modules: ReadonlyArray<EnabledModuleAiOverrides>,\n): void {\n for (const entry of modules) {\n if (!entry || typeof entry.id !== 'string' || !entry.id) continue\n const ai = entry.overrides?.ai\n if (!ai || typeof ai !== 'object') continue\n if (ai.agents && typeof ai.agents === 'object') {\n for (const [id, value] of Object.entries(ai.agents)) {\n modulesConfigAgentOverrides[id] = value as AiAgentOverride\n }\n }\n if (ai.tools && typeof ai.tools === 'object') {\n for (const [name, value] of Object.entries(ai.tools)) {\n modulesConfigToolOverrides[name] = value as AiToolOverride\n }\n }\n if (Array.isArray(ai.extensions)) {\n modulesConfigAgentExtensions.push(...ai.extensions)\n }\n }\n}\n\n/**\n * Bucketed entry shape passed to the umbrella dispatcher's per-domain\n * applier. Each entry carries one module's `overrides.ai` sub-tree.\n */\ntype AiOverrideEntryFromDispatcher = {\n moduleId: string\n overrides: AiModuleOverridesShape\n}\n\n/**\n * Applier registered with `@open-mercato/shared/modules/overrides` for\n * the `'ai'` domain. The dispatcher hands us per-module entries already\n * scoped to `overrides.ai`; we re-shape into `EnabledModuleAiOverrides`\n * and reuse {@link applyAiOverridesFromEnabledModules} so the AI tier\n * has exactly one code path.\n */\nexport function applyAiOverridesDispatcherEntries(\n entries: ReadonlyArray<AiOverrideEntryFromDispatcher>,\n): void {\n applyAiOverridesFromEnabledModules(\n entries.map((entry) => ({\n id: entry.moduleId,\n overrides: { ai: entry.overrides },\n })),\n )\n}\n\n// Side-effect: register the `'ai'` applier on first module load so the\n// umbrella dispatcher in `@open-mercato/shared/modules/overrides` can\n// route `entry.overrides.ai` here. Any consumer that imports\n// `@open-mercato/ai-assistant` (which apps do via `bootstrap.ts`) gets\n// the registration for free \u2014 no second import needed.\nregisterModuleOverrideApplier<AiModuleOverridesShape>(\n 'ai',\n (entries: ReadonlyArray<ModuleOverrideEntry<AiModuleOverridesShape>>) => {\n applyAiOverridesDispatcherEntries(entries)\n },\n)\n\n/** @__internal Test-only hook \u2014 reset programmatic + modules.ts override state. */\nexport function resetProgrammaticOverridesForTests(): void {\n for (const key of Object.keys(programmaticAgentOverrides)) {\n delete programmaticAgentOverrides[key]\n }\n for (const key of Object.keys(programmaticToolOverrides)) {\n delete programmaticToolOverrides[key]\n }\n for (const key of Object.keys(modulesConfigAgentOverrides)) {\n delete modulesConfigAgentOverrides[key]\n }\n for (const key of Object.keys(modulesConfigToolOverrides)) {\n delete modulesConfigToolOverrides[key]\n }\n programmaticAgentExtensions.length = 0\n modulesConfigAgentExtensions.length = 0\n}\n\n/**\n * Resolve the final agent override map from a list of file-based entries\n * plus the `modules.ts` and programmatic state.\n *\n * Resolution order (lowest precedence \u2192 highest):\n * 1. file entries in module load order\n * 2. modules.ts entries\n * 3. programmatic\n */\nexport function composeAgentOverrideMap(\n fileEntries: readonly AiAgentOverrideConfigEntry[],\n): AiAgentOverridesMap {\n const out: AiAgentOverridesMap = {}\n for (const entry of fileEntries) {\n const overrides = entry?.overrides\n if (!overrides || typeof overrides !== 'object') continue\n for (const [id, value] of Object.entries(overrides)) {\n if (typeof id !== 'string' || !id) continue\n out[id] = value as AiAgentOverride\n }\n }\n for (const [id, value] of Object.entries(modulesConfigAgentOverrides)) {\n out[id] = value\n }\n for (const [id, value] of Object.entries(programmaticAgentOverrides)) {\n out[id] = value\n }\n return out\n}\n\n/**\n * Resolve the final tool override map from a list of file-based entries\n * plus the `modules.ts` and programmatic state.\n */\nexport function composeToolOverrideMap(\n fileEntries: readonly AiToolOverrideConfigEntry[],\n): AiToolOverridesMap {\n const out: AiToolOverridesMap = {}\n for (const entry of fileEntries) {\n const overrides = entry?.overrides\n if (!overrides || typeof overrides !== 'object') continue\n for (const [name, value] of Object.entries(overrides)) {\n if (typeof name !== 'string' || !name) continue\n out[name] = value as AiToolOverride\n }\n }\n for (const [name, value] of Object.entries(modulesConfigToolOverrides)) {\n out[name] = value\n }\n for (const [name, value] of Object.entries(programmaticToolOverrides)) {\n out[name] = value\n }\n return out\n}\n\nexport function composeAgentExtensionEntries(\n fileEntries: readonly AiAgentExtensionConfigEntry[],\n): AiAgentExtension[] {\n const out: AiAgentExtension[] = []\n for (const entry of fileEntries) {\n if (Array.isArray(entry?.extensions)) out.push(...entry.extensions)\n }\n out.push(...modulesConfigAgentExtensions)\n out.push(...programmaticAgentExtensions)\n return out\n}\n\n/**\n * Apply an agent override map to a base list. Returns a new array.\n * `null` removes the entry; a non-null override replaces it. Override\n * entries naming an id that is not in `base` log a structured warning so\n * an operator can spot stale override files.\n */\nexport function applyAgentOverrideMap(\n base: readonly AiAgentDefinition[],\n overrides: AiAgentOverridesMap,\n): AiAgentDefinition[] {\n if (!overrides || Object.keys(overrides).length === 0) return base.slice()\n const byId = new Map<string, AiAgentDefinition>()\n for (const agent of base) {\n if (agent && typeof agent.id === 'string' && agent.id) {\n byId.set(agent.id, agent)\n }\n }\n for (const [id, value] of Object.entries(overrides)) {\n if (!byId.has(id) && value !== null) {\n // Allow registering a brand-new agent through the override surface\n // \u2014 useful for app-level \"synthetic\" agents without authoring a\n // module file. Warn at the structured logger so the operator\n // notices a stale id slipping through.\n console.warn(\n `[AI Overrides] Override registers a new agent \"${id}\" \u2014 no base entry to replace.`,\n )\n }\n if (value === null) {\n byId.delete(id)\n continue\n }\n if (!value || typeof value.id !== 'string' || value.id !== id) {\n console.warn(\n `[AI Overrides] Skipping malformed agent override for id \"${id}\" \u2014 id mismatch or missing fields.`,\n )\n continue\n }\n byId.set(id, value)\n }\n return Array.from(byId.values())\n}\n\n/**\n * Apply a tool override map to a base map. Returns a new Map.\n * `null` removes the entry; a non-null override replaces it.\n */\nexport function applyToolOverrideMap<TTool extends { name: string }>(\n base: ReadonlyMap<string, TTool>,\n overrides: Record<string, TTool | null | undefined>,\n): Map<string, TTool> {\n const out = new Map<string, TTool>(base)\n if (!overrides) return out\n for (const [name, value] of Object.entries(overrides)) {\n if (value === null) {\n out.delete(name)\n continue\n }\n if (value === undefined) continue\n if (!value || typeof (value as TTool).name !== 'string' || (value as TTool).name !== name) {\n console.warn(\n `[AI Overrides] Skipping malformed tool override for name \"${name}\" \u2014 name mismatch or missing fields.`,\n )\n continue\n }\n out.set(name, value as TTool)\n }\n return out\n}\n\n/**\n * @__internal \u2014 read the snapshot of programmatic + modules.ts overrides.\n * Used by tests and by the diagnostic helper that reports which overrides\n * are in effect.\n */\nexport function snapshotProgrammaticOverrides(): {\n agents: Readonly<AiAgentOverridesMap>\n tools: Readonly<AiToolOverridesMap>\n modulesConfigAgents: Readonly<AiAgentOverridesMap>\n modulesConfigTools: Readonly<AiToolOverridesMap>\n agentExtensions: readonly AiAgentExtension[]\n modulesConfigAgentExtensions: readonly AiAgentExtension[]\n} {\n return {\n agents: { ...programmaticAgentOverrides },\n tools: { ...programmaticToolOverrides },\n modulesConfigAgents: { ...modulesConfigAgentOverrides },\n modulesConfigTools: { ...modulesConfigToolOverrides },\n agentExtensions: programmaticAgentExtensions.slice(),\n modulesConfigAgentExtensions: modulesConfigAgentExtensions.slice(),\n }\n}\n"],
4
+ "sourcesContent": ["import {\n registerModuleOverrideApplier,\n type ModuleOverrideEntry,\n} from '@open-mercato/shared/modules/overrides'\n\n/**\n * Module-to-module override pipeline for AI agents and AI tools.\n *\n * Modules contribute overrides through two surfaces:\n *\n * 1. Per-module: declare additional `aiAgentOverrides` / `aiToolOverrides`\n * exports in the existing `<module>/ai-agents.ts` / `<module>/ai-tools.ts`\n * files (no separate `ai-overrides.ts` file). The generator picks the\n * exports up alongside `aiAgents` / `aiTools` and emits override entries\n * inside `apps/<app>/.mercato/generated/ai-agents.generated.ts`\n * (`aiAgentOverrideEntries`) and `ai-tools.generated.ts`\n * (`aiToolOverrideEntries`).\n *\n * 2. App-level: declare `aiAgentOverrides` / `aiToolOverrides` directly on a\n * `ModuleEntry` inside the app's `src/modules.ts`. {@link\n * applyAiOverridesFromEnabledModules} feeds these into the runtime; they\n * sit one tier higher than the per-module file-based entries but below\n * explicit programmatic calls.\n *\n * Tests and bootstrap code can also override imperatively via\n * {@link applyAiAgentOverrides} and {@link applyAiToolOverrides}, which\n * supersede every other tier and persist for the process lifetime.\n *\n * `null` always means \"remove from the registry\"; a definition replaces.\n *\n * @see ../../../../../../.ai/specs/implemented/2026-04-30-ai-overrides-and-module-disable.md\n */\nimport type { AiAgentDefinition, AiAgentExtension } from './ai-agent-definition'\nimport type { AiToolDefinition } from './types'\n\n/** Override for a single agent: replace with a definition or remove with `null`. */\nexport type AiAgentOverride = AiAgentDefinition | null\n\n/** Override for a single tool: replace with a definition or remove with `null`. */\nexport type AiToolOverride = AiToolDefinition | null\n\n/** Map of agent id \u2192 override. Used in the per-module `ai-agents.ts` file. */\nexport type AiAgentOverridesMap = Record<string, AiAgentOverride>\n\n/** Map of tool name \u2192 override. Used in the per-module `ai-tools.ts` file. */\nexport type AiToolOverridesMap = Record<string, AiToolOverride>\n\n/**\n * Per-entry shape produced by the agent generator. Mirrors the per-module\n * record format used elsewhere in the registry generators so the file\n * stays grep-friendly.\n */\nexport interface AiAgentOverrideConfigEntry {\n moduleId: string\n overrides: AiAgentOverridesMap\n}\n\nexport interface AiAgentExtensionConfigEntry {\n moduleId: string\n extensions: AiAgentExtension[]\n}\n\n/**\n * Per-entry shape produced by the tool generator. Same record format as\n * {@link AiAgentOverrideConfigEntry}, but with tool definitions.\n */\nexport interface AiToolOverrideConfigEntry {\n moduleId: string\n overrides: AiToolOverridesMap\n}\n\n/** Shape of the `entry.overrides.ai` sub-tree on a `modules.ts` entry. */\nexport interface AiModuleOverridesShape {\n agents?: AiAgentOverridesMap\n tools?: AiToolOverridesMap\n extensions?: AiAgentExtension[]\n}\n\n/** Shape of a `modules.ts` `ModuleEntry` with the umbrella `overrides.ai`. */\nexport interface EnabledModuleAiOverrides {\n id: string\n overrides?: { ai?: AiModuleOverridesShape }\n}\n\nconst programmaticAgentOverrides: AiAgentOverridesMap = {}\nconst programmaticToolOverrides: AiToolOverridesMap = {}\nconst programmaticAgentExtensions: AiAgentExtension[] = []\n\nconst modulesConfigAgentOverrides: AiAgentOverridesMap = {}\nconst modulesConfigToolOverrides: AiToolOverridesMap = {}\nconst modulesConfigAgentExtensions: AiAgentExtension[] = []\n\n/**\n * Apply programmatic agent overrides \u2014 survive after the registries load\n * and take precedence over both file-based and `modules.ts`-tier overrides\n * for the same id.\n *\n * @example\n * ```ts\n * applyAiAgentOverrides({\n * 'catalog.catalog_assistant': null, // disable\n * 'catalog.merchandising_assistant': customAgent, // replace\n * })\n * ```\n */\nexport function applyAiAgentOverrides(overrides: AiAgentOverridesMap): void {\n for (const [id, value] of Object.entries(overrides)) {\n programmaticAgentOverrides[id] = value\n }\n}\n\n/**\n * Apply programmatic tool overrides \u2014 survive after the registries load\n * and take precedence over both file-based and `modules.ts`-tier overrides\n * for the same name.\n */\nexport function applyAiToolOverrides(overrides: AiToolOverridesMap): void {\n for (const [name, value] of Object.entries(overrides)) {\n programmaticToolOverrides[name] = value\n }\n}\n\n/**\n * Apply programmatic additive agent extensions. Extensions append to an\n * already-registered agent after replacements/disable overrides resolve.\n */\nexport function applyAiAgentExtensions(extensions: readonly AiAgentExtension[]): void {\n programmaticAgentExtensions.push(...extensions)\n}\n\n/**\n * Walk a list of `enabledModules` entries (the `apps/<app>/src/modules.ts`\n * shape) and register their `overrides.ai.agents` / `overrides.ai.tools`\n * into the `modules.ts` tier. Tier precedence (highest first):\n *\n * 1. {@link applyAiAgentOverrides} / {@link applyAiToolOverrides}\n * 2. `modules.ts` inline (this function)\n * 3. `<module>/ai-agents.ts` `aiAgentOverrides` / `<module>/ai-tools.ts`\n * `aiToolOverrides`\n * 4. base `aiAgents` / `aiTools`\n *\n * Calling this multiple times is additive: later calls overlay on the\n * existing tier (last wins per id). Use only at boot time \u2014 re-entering\n * mid-request blurs the resolution order.\n *\n * In practice this is invoked from `applyModuleOverridesFromEnabledModules`\n * (the umbrella dispatcher in `@open-mercato/shared/modules/overrides`)\n * via the registered `'ai'` applier; downstream apps call the dispatcher\n * once from `bootstrap.ts`. The standalone signature is kept for tests\n * and ad-hoc use.\n */\nexport function applyAiOverridesFromEnabledModules(\n modules: ReadonlyArray<EnabledModuleAiOverrides>,\n): void {\n for (const entry of modules) {\n if (!entry || typeof entry.id !== 'string' || !entry.id) continue\n const ai = entry.overrides?.ai\n if (!ai || typeof ai !== 'object') continue\n if (ai.agents && typeof ai.agents === 'object') {\n for (const [id, value] of Object.entries(ai.agents)) {\n modulesConfigAgentOverrides[id] = value as AiAgentOverride\n }\n }\n if (ai.tools && typeof ai.tools === 'object') {\n for (const [name, value] of Object.entries(ai.tools)) {\n modulesConfigToolOverrides[name] = value as AiToolOverride\n }\n }\n if (Array.isArray(ai.extensions)) {\n modulesConfigAgentExtensions.push(...ai.extensions)\n }\n }\n}\n\n/**\n * Bucketed entry shape passed to the umbrella dispatcher's per-domain\n * applier. Each entry carries one module's `overrides.ai` sub-tree.\n */\ntype AiOverrideEntryFromDispatcher = {\n moduleId: string\n overrides: AiModuleOverridesShape\n}\n\n/**\n * Applier registered with `@open-mercato/shared/modules/overrides` for\n * the `'ai'` domain. The dispatcher hands us per-module entries already\n * scoped to `overrides.ai`; we re-shape into `EnabledModuleAiOverrides`\n * and reuse {@link applyAiOverridesFromEnabledModules} so the AI tier\n * has exactly one code path.\n */\nexport function applyAiOverridesDispatcherEntries(\n entries: ReadonlyArray<AiOverrideEntryFromDispatcher>,\n): void {\n applyAiOverridesFromEnabledModules(\n entries.map((entry) => ({\n id: entry.moduleId,\n overrides: { ai: entry.overrides },\n })),\n )\n}\n\n// Side-effect: register the `'ai'` applier on first module load so the\n// umbrella dispatcher in `@open-mercato/shared/modules/overrides` can\n// route `entry.overrides.ai` here. Any consumer that imports\n// `@open-mercato/ai-assistant` (which apps do via `bootstrap.ts`) gets\n// the registration for free \u2014 no second import needed.\nregisterModuleOverrideApplier<AiModuleOverridesShape>(\n 'ai',\n (entries: ReadonlyArray<ModuleOverrideEntry<AiModuleOverridesShape>>) => {\n applyAiOverridesDispatcherEntries(entries)\n },\n)\n\n/** @__internal Test-only hook \u2014 reset programmatic + modules.ts override state. */\nexport function resetProgrammaticOverridesForTests(): void {\n for (const key of Object.keys(programmaticAgentOverrides)) {\n delete programmaticAgentOverrides[key]\n }\n for (const key of Object.keys(programmaticToolOverrides)) {\n delete programmaticToolOverrides[key]\n }\n for (const key of Object.keys(modulesConfigAgentOverrides)) {\n delete modulesConfigAgentOverrides[key]\n }\n for (const key of Object.keys(modulesConfigToolOverrides)) {\n delete modulesConfigToolOverrides[key]\n }\n programmaticAgentExtensions.length = 0\n modulesConfigAgentExtensions.length = 0\n}\n\n/**\n * Resolve the final agent override map from a list of file-based entries\n * plus the `modules.ts` and programmatic state.\n *\n * Resolution order (lowest precedence \u2192 highest):\n * 1. file entries in module load order\n * 2. modules.ts entries\n * 3. programmatic\n */\nexport function composeAgentOverrideMap(\n fileEntries: readonly AiAgentOverrideConfigEntry[],\n): AiAgentOverridesMap {\n const out: AiAgentOverridesMap = {}\n for (const entry of fileEntries) {\n const overrides = entry?.overrides\n if (!overrides || typeof overrides !== 'object') continue\n for (const [id, value] of Object.entries(overrides)) {\n if (typeof id !== 'string' || !id) continue\n out[id] = value as AiAgentOverride\n }\n }\n for (const [id, value] of Object.entries(modulesConfigAgentOverrides)) {\n out[id] = value\n }\n for (const [id, value] of Object.entries(programmaticAgentOverrides)) {\n out[id] = value\n }\n return out\n}\n\n/**\n * Resolve the final tool override map from a list of file-based entries\n * plus the `modules.ts` and programmatic state.\n */\nexport function composeToolOverrideMap(\n fileEntries: readonly AiToolOverrideConfigEntry[],\n): AiToolOverridesMap {\n const out: AiToolOverridesMap = {}\n for (const entry of fileEntries) {\n const overrides = entry?.overrides\n if (!overrides || typeof overrides !== 'object') continue\n for (const [name, value] of Object.entries(overrides)) {\n if (typeof name !== 'string' || !name) continue\n out[name] = value as AiToolOverride\n }\n }\n for (const [name, value] of Object.entries(modulesConfigToolOverrides)) {\n out[name] = value\n }\n for (const [name, value] of Object.entries(programmaticToolOverrides)) {\n out[name] = value\n }\n return out\n}\n\nexport function composeAgentExtensionEntries(\n fileEntries: readonly AiAgentExtensionConfigEntry[],\n): AiAgentExtension[] {\n const out: AiAgentExtension[] = []\n for (const entry of fileEntries) {\n if (Array.isArray(entry?.extensions)) out.push(...entry.extensions)\n }\n out.push(...modulesConfigAgentExtensions)\n out.push(...programmaticAgentExtensions)\n return out\n}\n\n/**\n * Apply an agent override map to a base list. Returns a new array.\n * `null` removes the entry; a non-null override replaces it. Override\n * entries naming an id that is not in `base` log a structured warning so\n * an operator can spot stale override files.\n */\nexport function applyAgentOverrideMap(\n base: readonly AiAgentDefinition[],\n overrides: AiAgentOverridesMap,\n): AiAgentDefinition[] {\n if (!overrides || Object.keys(overrides).length === 0) return base.slice()\n const byId = new Map<string, AiAgentDefinition>()\n for (const agent of base) {\n if (agent && typeof agent.id === 'string' && agent.id) {\n byId.set(agent.id, agent)\n }\n }\n for (const [id, value] of Object.entries(overrides)) {\n if (!byId.has(id) && value !== null) {\n // Allow registering a brand-new agent through the override surface\n // \u2014 useful for app-level \"synthetic\" agents without authoring a\n // module file. Warn at the structured logger so the operator\n // notices a stale id slipping through.\n console.warn(\n `[AI Overrides] Override registers a new agent \"${id}\" \u2014 no base entry to replace.`,\n )\n }\n if (value === null) {\n byId.delete(id)\n continue\n }\n if (!value || typeof value.id !== 'string' || value.id !== id) {\n console.warn(\n `[AI Overrides] Skipping malformed agent override for id \"${id}\" \u2014 id mismatch or missing fields.`,\n )\n continue\n }\n byId.set(id, value)\n }\n return Array.from(byId.values())\n}\n\n/**\n * Apply a tool override map to a base map. Returns a new Map.\n * `null` removes the entry; a non-null override replaces it.\n */\nexport function applyToolOverrideMap<TTool extends { name: string }>(\n base: ReadonlyMap<string, TTool>,\n overrides: Record<string, TTool | null | undefined>,\n): Map<string, TTool> {\n const out = new Map<string, TTool>(base)\n if (!overrides) return out\n for (const [name, value] of Object.entries(overrides)) {\n if (value === null) {\n out.delete(name)\n continue\n }\n if (value === undefined) continue\n if (!value || typeof (value as TTool).name !== 'string' || (value as TTool).name !== name) {\n console.warn(\n `[AI Overrides] Skipping malformed tool override for name \"${name}\" \u2014 name mismatch or missing fields.`,\n )\n continue\n }\n out.set(name, value as TTool)\n }\n return out\n}\n\n/**\n * @__internal \u2014 read the snapshot of programmatic + modules.ts overrides.\n * Used by tests and by the diagnostic helper that reports which overrides\n * are in effect.\n */\nexport function snapshotProgrammaticOverrides(): {\n agents: Readonly<AiAgentOverridesMap>\n tools: Readonly<AiToolOverridesMap>\n modulesConfigAgents: Readonly<AiAgentOverridesMap>\n modulesConfigTools: Readonly<AiToolOverridesMap>\n agentExtensions: readonly AiAgentExtension[]\n modulesConfigAgentExtensions: readonly AiAgentExtension[]\n} {\n return {\n agents: { ...programmaticAgentOverrides },\n tools: { ...programmaticToolOverrides },\n modulesConfigAgents: { ...modulesConfigAgentOverrides },\n modulesConfigTools: { ...modulesConfigToolOverrides },\n agentExtensions: programmaticAgentExtensions.slice(),\n modulesConfigAgentExtensions: modulesConfigAgentExtensions.slice(),\n }\n}\n"],
5
5
  "mappings": "AAAA;AAAA,EACE;AAAA,OAEK;AAiFP,MAAM,6BAAkD,CAAC;AACzD,MAAM,4BAAgD,CAAC;AACvD,MAAM,8BAAkD,CAAC;AAEzD,MAAM,8BAAmD,CAAC;AAC1D,MAAM,6BAAiD,CAAC;AACxD,MAAM,+BAAmD,CAAC;AAenD,SAAS,sBAAsB,WAAsC;AAC1E,aAAW,CAAC,IAAI,KAAK,KAAK,OAAO,QAAQ,SAAS,GAAG;AACnD,+BAA2B,EAAE,IAAI;AAAA,EACnC;AACF;AAOO,SAAS,qBAAqB,WAAqC;AACxE,aAAW,CAAC,MAAM,KAAK,KAAK,OAAO,QAAQ,SAAS,GAAG;AACrD,8BAA0B,IAAI,IAAI;AAAA,EACpC;AACF;AAMO,SAAS,uBAAuB,YAA+C;AACpF,8BAA4B,KAAK,GAAG,UAAU;AAChD;AAuBO,SAAS,mCACd,SACM;AACN,aAAW,SAAS,SAAS;AAC3B,QAAI,CAAC,SAAS,OAAO,MAAM,OAAO,YAAY,CAAC,MAAM,GAAI;AACzD,UAAM,KAAK,MAAM,WAAW;AAC5B,QAAI,CAAC,MAAM,OAAO,OAAO,SAAU;AACnC,QAAI,GAAG,UAAU,OAAO,GAAG,WAAW,UAAU;AAC9C,iBAAW,CAAC,IAAI,KAAK,KAAK,OAAO,QAAQ,GAAG,MAAM,GAAG;AACnD,oCAA4B,EAAE,IAAI;AAAA,MACpC;AAAA,IACF;AACA,QAAI,GAAG,SAAS,OAAO,GAAG,UAAU,UAAU;AAC5C,iBAAW,CAAC,MAAM,KAAK,KAAK,OAAO,QAAQ,GAAG,KAAK,GAAG;AACpD,mCAA2B,IAAI,IAAI;AAAA,MACrC;AAAA,IACF;AACA,QAAI,MAAM,QAAQ,GAAG,UAAU,GAAG;AAChC,mCAA6B,KAAK,GAAG,GAAG,UAAU;AAAA,IACpD;AAAA,EACF;AACF;AAkBO,SAAS,kCACd,SACM;AACN;AAAA,IACE,QAAQ,IAAI,CAAC,WAAW;AAAA,MACtB,IAAI,MAAM;AAAA,MACV,WAAW,EAAE,IAAI,MAAM,UAAU;AAAA,IACnC,EAAE;AAAA,EACJ;AACF;AAOA;AAAA,EACE;AAAA,EACA,CAAC,YAAwE;AACvE,sCAAkC,OAAO;AAAA,EAC3C;AACF;AAGO,SAAS,qCAA2C;AACzD,aAAW,OAAO,OAAO,KAAK,0BAA0B,GAAG;AACzD,WAAO,2BAA2B,GAAG;AAAA,EACvC;AACA,aAAW,OAAO,OAAO,KAAK,yBAAyB,GAAG;AACxD,WAAO,0BAA0B,GAAG;AAAA,EACtC;AACA,aAAW,OAAO,OAAO,KAAK,2BAA2B,GAAG;AAC1D,WAAO,4BAA4B,GAAG;AAAA,EACxC;AACA,aAAW,OAAO,OAAO,KAAK,0BAA0B,GAAG;AACzD,WAAO,2BAA2B,GAAG;AAAA,EACvC;AACA,8BAA4B,SAAS;AACrC,+BAA6B,SAAS;AACxC;AAWO,SAAS,wBACd,aACqB;AACrB,QAAM,MAA2B,CAAC;AAClC,aAAW,SAAS,aAAa;AAC/B,UAAM,YAAY,OAAO;AACzB,QAAI,CAAC,aAAa,OAAO,cAAc,SAAU;AACjD,eAAW,CAAC,IAAI,KAAK,KAAK,OAAO,QAAQ,SAAS,GAAG;AACnD,UAAI,OAAO,OAAO,YAAY,CAAC,GAAI;AACnC,UAAI,EAAE,IAAI;AAAA,IACZ;AAAA,EACF;AACA,aAAW,CAAC,IAAI,KAAK,KAAK,OAAO,QAAQ,2BAA2B,GAAG;AACrE,QAAI,EAAE,IAAI;AAAA,EACZ;AACA,aAAW,CAAC,IAAI,KAAK,KAAK,OAAO,QAAQ,0BAA0B,GAAG;AACpE,QAAI,EAAE,IAAI;AAAA,EACZ;AACA,SAAO;AACT;AAMO,SAAS,uBACd,aACoB;AACpB,QAAM,MAA0B,CAAC;AACjC,aAAW,SAAS,aAAa;AAC/B,UAAM,YAAY,OAAO;AACzB,QAAI,CAAC,aAAa,OAAO,cAAc,SAAU;AACjD,eAAW,CAAC,MAAM,KAAK,KAAK,OAAO,QAAQ,SAAS,GAAG;AACrD,UAAI,OAAO,SAAS,YAAY,CAAC,KAAM;AACvC,UAAI,IAAI,IAAI;AAAA,IACd;AAAA,EACF;AACA,aAAW,CAAC,MAAM,KAAK,KAAK,OAAO,QAAQ,0BAA0B,GAAG;AACtE,QAAI,IAAI,IAAI;AAAA,EACd;AACA,aAAW,CAAC,MAAM,KAAK,KAAK,OAAO,QAAQ,yBAAyB,GAAG;AACrE,QAAI,IAAI,IAAI;AAAA,EACd;AACA,SAAO;AACT;AAEO,SAAS,6BACd,aACoB;AACpB,QAAM,MAA0B,CAAC;AACjC,aAAW,SAAS,aAAa;AAC/B,QAAI,MAAM,QAAQ,OAAO,UAAU,EAAG,KAAI,KAAK,GAAG,MAAM,UAAU;AAAA,EACpE;AACA,MAAI,KAAK,GAAG,4BAA4B;AACxC,MAAI,KAAK,GAAG,2BAA2B;AACvC,SAAO;AACT;AAQO,SAAS,sBACd,MACA,WACqB;AACrB,MAAI,CAAC,aAAa,OAAO,KAAK,SAAS,EAAE,WAAW,EAAG,QAAO,KAAK,MAAM;AACzE,QAAM,OAAO,oBAAI,IAA+B;AAChD,aAAW,SAAS,MAAM;AACxB,QAAI,SAAS,OAAO,MAAM,OAAO,YAAY,MAAM,IAAI;AACrD,WAAK,IAAI,MAAM,IAAI,KAAK;AAAA,IAC1B;AAAA,EACF;AACA,aAAW,CAAC,IAAI,KAAK,KAAK,OAAO,QAAQ,SAAS,GAAG;AACnD,QAAI,CAAC,KAAK,IAAI,EAAE,KAAK,UAAU,MAAM;AAKnC,cAAQ;AAAA,QACN,kDAAkD,EAAE;AAAA,MACtD;AAAA,IACF;AACA,QAAI,UAAU,MAAM;AAClB,WAAK,OAAO,EAAE;AACd;AAAA,IACF;AACA,QAAI,CAAC,SAAS,OAAO,MAAM,OAAO,YAAY,MAAM,OAAO,IAAI;AAC7D,cAAQ;AAAA,QACN,4DAA4D,EAAE;AAAA,MAChE;AACA;AAAA,IACF;AACA,SAAK,IAAI,IAAI,KAAK;AAAA,EACpB;AACA,SAAO,MAAM,KAAK,KAAK,OAAO,CAAC;AACjC;AAMO,SAAS,qBACd,MACA,WACoB;AACpB,QAAM,MAAM,IAAI,IAAmB,IAAI;AACvC,MAAI,CAAC,UAAW,QAAO;AACvB,aAAW,CAAC,MAAM,KAAK,KAAK,OAAO,QAAQ,SAAS,GAAG;AACrD,QAAI,UAAU,MAAM;AAClB,UAAI,OAAO,IAAI;AACf;AAAA,IACF;AACA,QAAI,UAAU,OAAW;AACzB,QAAI,CAAC,SAAS,OAAQ,MAAgB,SAAS,YAAa,MAAgB,SAAS,MAAM;AACzF,cAAQ;AAAA,QACN,6DAA6D,IAAI;AAAA,MACnE;AACA;AAAA,IACF;AACA,QAAI,IAAI,MAAM,KAAc;AAAA,EAC9B;AACA,SAAO;AACT;AAOO,SAAS,gCAOd;AACA,SAAO;AAAA,IACL,QAAQ,EAAE,GAAG,2BAA2B;AAAA,IACxC,OAAO,EAAE,GAAG,0BAA0B;AAAA,IACtC,qBAAqB,EAAE,GAAG,4BAA4B;AAAA,IACtD,oBAAoB,EAAE,GAAG,2BAA2B;AAAA,IACpD,iBAAiB,4BAA4B,MAAM;AAAA,IACnD,8BAA8B,6BAA6B,MAAM;AAAA,EACnE;AACF;",
6
6
  "names": []
7
7
  }
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../../src/modules/ai_assistant/lib/ai-sdk.ts"],
4
- "sourcesContent": ["// Re-export AI SDK functions from the ai-assistant package\nexport { streamText, generateObject, stepCountIs } from 'ai'\nexport { createOpenAI } from '@ai-sdk/openai'\nexport { createAnthropic } from '@ai-sdk/anthropic'\nexport { createGoogleGenerativeAI } from '@ai-sdk/google'\n\n// Side-effect import: registers built-in LLM providers (Anthropic, Google,\n// OpenAI + OpenAI-compatible presets for DeepInfra, Groq, Together, etc.)\n// with the shared `llmProviderRegistry` singleton. Consumers that import\n// from `./ai-sdk` transitively trigger provider bootstrap, so any module\n// using `generateObject` / `streamText` already has the registry populated.\n//\n// @see ./llm-bootstrap.ts\n// @see .ai/specs/2026-04-14-llm-provider-ports-and-adapters.md\nimport './llm-bootstrap'\n"],
4
+ "sourcesContent": ["// Re-export AI SDK functions from the ai-assistant package\nexport { streamText, generateObject, stepCountIs } from 'ai'\nexport { createOpenAI } from '@ai-sdk/openai'\nexport { createAnthropic } from '@ai-sdk/anthropic'\nexport { createGoogleGenerativeAI } from '@ai-sdk/google'\n\n// Side-effect import: registers built-in LLM providers (Anthropic, Google,\n// OpenAI + OpenAI-compatible presets for DeepInfra, Groq, Together, etc.)\n// with the shared `llmProviderRegistry` singleton. Consumers that import\n// from `./ai-sdk` transitively trigger provider bootstrap, so any module\n// using `generateObject` / `streamText` already has the registry populated.\n//\n// @see ./llm-bootstrap.ts\n// @see .ai/specs/implemented/2026-04-14-llm-provider-ports-and-adapters.md\nimport './llm-bootstrap'\n"],
5
5
  "mappings": "AACA,SAAS,YAAY,gBAAgB,mBAAmB;AACxD,SAAS,oBAAoB;AAC7B,SAAS,uBAAuB;AAChC,SAAS,gCAAgC;AAUzC,OAAO;",
6
6
  "names": []
7
7
  }
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../../src/modules/ai_assistant/lib/chat-config.ts"],
4
- "sourcesContent": ["import type { ModuleConfigService } from '@open-mercato/core/modules/configs/lib/module-config-service'\nimport { llmProviderRegistry } from '@open-mercato/shared/lib/ai/llm-provider-registry'\nimport type { LlmProvider } from '@open-mercato/shared/lib/ai/llm-provider'\n// Side-effect: ensures the registry is populated with built-in adapters\n// and OpenAI-compatible presets before this module's getters run.\nimport './llm-bootstrap'\n\n// Types\n//\n// `ChatProviderId` was previously a narrow literal union of three ids\n// (`'anthropic' | 'openai' | 'google'`). After the ports & adapters\n// refactor the registry accepts any stable id string, so this type\n// becomes `string`. Backward-compatibility note: downstream callers that\n// used exhaustive switches on the old union must add a `default:` branch.\n// See `.ai/specs/2026-04-14-llm-provider-ports-and-adapters.md`.\nexport type ChatProviderId = string\n\nexport type ChatModelInfo = {\n id: string\n name: string\n contextWindow: number\n}\n\nexport type ChatProviderInfo = {\n name: string\n envKeyRequired: string\n defaultModel: string\n models: ChatModelInfo[]\n}\n\nexport type ChatProviderConfig = {\n providerId: ChatProviderId\n model: string\n updatedAt: string\n}\n\n// Constants\nexport const CHAT_CONFIG_KEY = 'chat_provider'\n\nfunction providerToChatInfo(provider: LlmProvider): ChatProviderInfo {\n return {\n name: provider.name,\n envKeyRequired: provider.envKeys[0],\n defaultModel: provider.defaultModel,\n models: provider.defaultModels.map((m) => ({\n id: m.id,\n name: m.name,\n contextWindow: m.contextWindow,\n })),\n }\n}\n\n/**\n * `CHAT_PROVIDERS` is a dynamic getter that returns all providers\n * registered with `llmProviderRegistry`. The shape\n * (`Record<string, ChatProviderInfo>`) is preserved so existing code that\n * indexed the map with a string literal (`CHAT_PROVIDERS['anthropic']`)\n * keeps working \u2014 it is now a runtime lookup against the registry.\n */\nexport const CHAT_PROVIDERS: Record<string, ChatProviderInfo> = new Proxy(\n {} as Record<string, ChatProviderInfo>,\n {\n get(_target, prop: string): ChatProviderInfo | undefined {\n if (typeof prop !== 'string') return undefined\n const provider = llmProviderRegistry.get(prop)\n return provider ? providerToChatInfo(provider) : undefined\n },\n has(_target, prop: string): boolean {\n if (typeof prop !== 'string') return false\n return llmProviderRegistry.get(prop) !== null\n },\n ownKeys(): string[] {\n return llmProviderRegistry.list().map((p) => p.id)\n },\n getOwnPropertyDescriptor(_target, prop: string): PropertyDescriptor | undefined {\n if (typeof prop !== 'string') return undefined\n const provider = llmProviderRegistry.get(prop)\n if (!provider) return undefined\n return {\n enumerable: true,\n configurable: true,\n value: providerToChatInfo(provider),\n }\n },\n },\n)\n\nexport const DEFAULT_CHAT_CONFIG: Omit<ChatProviderConfig, 'updatedAt'> = {\n providerId: 'openai',\n get model(): string {\n // Lazy resolution so the bootstrap has a chance to register providers\n // before the default is computed.\n const provider = llmProviderRegistry.get('openai')\n return provider?.defaultModel ?? 'gpt-5-mini'\n },\n}\n\n// Provider configuration checks\nexport function isProviderConfigured(providerId: ChatProviderId): boolean {\n const provider = llmProviderRegistry.get(providerId)\n return provider?.isConfigured() ?? false\n}\n\nexport function getConfiguredProviders(): ChatProviderId[] {\n return llmProviderRegistry\n .listConfigured()\n .map((p) => p.id)\n}\n\n// Config resolution\ntype Resolver = {\n resolve: <T = unknown>(name: string) => T\n}\n\nexport async function resolveChatConfig(\n resolver: Resolver,\n options?: { defaultValue?: ChatProviderConfig | null }\n): Promise<ChatProviderConfig | null> {\n const fallback = options?.defaultValue ?? null\n let service: ModuleConfigService\n try {\n service = resolver.resolve<ModuleConfigService>('moduleConfigService')\n } catch {\n return fallback\n }\n try {\n const value = await service.getValue<ChatProviderConfig>('ai_assistant', CHAT_CONFIG_KEY, { defaultValue: fallback })\n return value\n } catch {\n return fallback\n }\n}\n\nexport async function saveChatConfig(\n resolver: Resolver,\n config: Omit<ChatProviderConfig, 'updatedAt'>\n): Promise<ChatProviderConfig> {\n let service: ModuleConfigService\n try {\n service = resolver.resolve<ModuleConfigService>('moduleConfigService')\n } catch {\n throw new Error('Configuration service unavailable')\n }\n const fullConfig: ChatProviderConfig = {\n ...config,\n updatedAt: new Date().toISOString(),\n }\n await service.setValue('ai_assistant', CHAT_CONFIG_KEY, fullConfig)\n return fullConfig\n}\n\nexport function createDefaultConfig(): ChatProviderConfig {\n return {\n providerId: DEFAULT_CHAT_CONFIG.providerId,\n model: DEFAULT_CHAT_CONFIG.model,\n updatedAt: new Date().toISOString(),\n }\n}\n\n// Get model info by ID\nexport function getModelInfo(providerId: ChatProviderId, modelId: string): ChatModelInfo | null {\n const provider = CHAT_PROVIDERS[providerId]\n if (!provider) return null\n return provider.models.find((m) => m.id === modelId) ?? null\n}\n\n// Format context window for display\nexport function formatContextWindow(contextWindow: number): string {\n if (contextWindow >= 1000000) {\n return `${(contextWindow / 1000000).toFixed(1)}M`\n }\n return `${(contextWindow / 1000).toFixed(0)}K`\n}\n"],
4
+ "sourcesContent": ["import type { ModuleConfigService } from '@open-mercato/core/modules/configs/lib/module-config-service'\nimport { llmProviderRegistry } from '@open-mercato/shared/lib/ai/llm-provider-registry'\nimport type { LlmProvider } from '@open-mercato/shared/lib/ai/llm-provider'\n// Side-effect: ensures the registry is populated with built-in adapters\n// and OpenAI-compatible presets before this module's getters run.\nimport './llm-bootstrap'\n\n// Types\n//\n// `ChatProviderId` was previously a narrow literal union of three ids\n// (`'anthropic' | 'openai' | 'google'`). After the ports & adapters\n// refactor the registry accepts any stable id string, so this type\n// becomes `string`. Backward-compatibility note: downstream callers that\n// used exhaustive switches on the old union must add a `default:` branch.\n// See `.ai/specs/implemented/2026-04-14-llm-provider-ports-and-adapters.md`.\nexport type ChatProviderId = string\n\nexport type ChatModelInfo = {\n id: string\n name: string\n contextWindow: number\n}\n\nexport type ChatProviderInfo = {\n name: string\n envKeyRequired: string\n defaultModel: string\n models: ChatModelInfo[]\n}\n\nexport type ChatProviderConfig = {\n providerId: ChatProviderId\n model: string\n updatedAt: string\n}\n\n// Constants\nexport const CHAT_CONFIG_KEY = 'chat_provider'\n\nfunction providerToChatInfo(provider: LlmProvider): ChatProviderInfo {\n return {\n name: provider.name,\n envKeyRequired: provider.envKeys[0],\n defaultModel: provider.defaultModel,\n models: provider.defaultModels.map((m) => ({\n id: m.id,\n name: m.name,\n contextWindow: m.contextWindow,\n })),\n }\n}\n\n/**\n * `CHAT_PROVIDERS` is a dynamic getter that returns all providers\n * registered with `llmProviderRegistry`. The shape\n * (`Record<string, ChatProviderInfo>`) is preserved so existing code that\n * indexed the map with a string literal (`CHAT_PROVIDERS['anthropic']`)\n * keeps working \u2014 it is now a runtime lookup against the registry.\n */\nexport const CHAT_PROVIDERS: Record<string, ChatProviderInfo> = new Proxy(\n {} as Record<string, ChatProviderInfo>,\n {\n get(_target, prop: string): ChatProviderInfo | undefined {\n if (typeof prop !== 'string') return undefined\n const provider = llmProviderRegistry.get(prop)\n return provider ? providerToChatInfo(provider) : undefined\n },\n has(_target, prop: string): boolean {\n if (typeof prop !== 'string') return false\n return llmProviderRegistry.get(prop) !== null\n },\n ownKeys(): string[] {\n return llmProviderRegistry.list().map((p) => p.id)\n },\n getOwnPropertyDescriptor(_target, prop: string): PropertyDescriptor | undefined {\n if (typeof prop !== 'string') return undefined\n const provider = llmProviderRegistry.get(prop)\n if (!provider) return undefined\n return {\n enumerable: true,\n configurable: true,\n value: providerToChatInfo(provider),\n }\n },\n },\n)\n\nexport const DEFAULT_CHAT_CONFIG: Omit<ChatProviderConfig, 'updatedAt'> = {\n providerId: 'openai',\n get model(): string {\n // Lazy resolution so the bootstrap has a chance to register providers\n // before the default is computed.\n const provider = llmProviderRegistry.get('openai')\n return provider?.defaultModel ?? 'gpt-5-mini'\n },\n}\n\n// Provider configuration checks\nexport function isProviderConfigured(providerId: ChatProviderId): boolean {\n const provider = llmProviderRegistry.get(providerId)\n return provider?.isConfigured() ?? false\n}\n\nexport function getConfiguredProviders(): ChatProviderId[] {\n return llmProviderRegistry\n .listConfigured()\n .map((p) => p.id)\n}\n\n// Config resolution\ntype Resolver = {\n resolve: <T = unknown>(name: string) => T\n}\n\nexport async function resolveChatConfig(\n resolver: Resolver,\n options?: { defaultValue?: ChatProviderConfig | null }\n): Promise<ChatProviderConfig | null> {\n const fallback = options?.defaultValue ?? null\n let service: ModuleConfigService\n try {\n service = resolver.resolve<ModuleConfigService>('moduleConfigService')\n } catch {\n return fallback\n }\n try {\n const value = await service.getValue<ChatProviderConfig>('ai_assistant', CHAT_CONFIG_KEY, { defaultValue: fallback })\n return value\n } catch {\n return fallback\n }\n}\n\nexport async function saveChatConfig(\n resolver: Resolver,\n config: Omit<ChatProviderConfig, 'updatedAt'>\n): Promise<ChatProviderConfig> {\n let service: ModuleConfigService\n try {\n service = resolver.resolve<ModuleConfigService>('moduleConfigService')\n } catch {\n throw new Error('Configuration service unavailable')\n }\n const fullConfig: ChatProviderConfig = {\n ...config,\n updatedAt: new Date().toISOString(),\n }\n await service.setValue('ai_assistant', CHAT_CONFIG_KEY, fullConfig)\n return fullConfig\n}\n\nexport function createDefaultConfig(): ChatProviderConfig {\n return {\n providerId: DEFAULT_CHAT_CONFIG.providerId,\n model: DEFAULT_CHAT_CONFIG.model,\n updatedAt: new Date().toISOString(),\n }\n}\n\n// Get model info by ID\nexport function getModelInfo(providerId: ChatProviderId, modelId: string): ChatModelInfo | null {\n const provider = CHAT_PROVIDERS[providerId]\n if (!provider) return null\n return provider.models.find((m) => m.id === modelId) ?? null\n}\n\n// Format context window for display\nexport function formatContextWindow(contextWindow: number): string {\n if (contextWindow >= 1000000) {\n return `${(contextWindow / 1000000).toFixed(1)}M`\n }\n return `${(contextWindow / 1000).toFixed(0)}K`\n}\n"],
5
5
  "mappings": "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",
6
6
  "names": []
7
7
  }
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../../../src/modules/ai_assistant/lib/llm-adapters/anthropic.ts"],
4
- "sourcesContent": ["/**\n * AnthropicAdapter \u2014 implements the LlmProvider port for Anthropic's\n * Messages API (Claude Haiku, Sonnet, Opus).\n *\n * Wraps `createAnthropic({ apiKey })` from `@ai-sdk/anthropic` and exposes\n * a curated model list for the AI Assistant UI dropdown.\n *\n * @see packages/shared/src/lib/ai/llm-provider.ts\n * @see .ai/specs/2026-04-14-llm-provider-ports-and-adapters.md\n */\n\nimport { createAnthropic } from '@ai-sdk/anthropic'\nimport type {\n EnvLookup,\n LlmCreateModelOptions,\n LlmModelInfo,\n LlmProvider,\n} from '@open-mercato/shared/lib/ai/llm-provider'\n\nconst DEFAULT_MODEL = 'claude-haiku-4-5-20251001'\n\nconst DEFAULT_MODELS: readonly LlmModelInfo[] = [\n {\n id: 'claude-haiku-4-5-20251001',\n name: 'Claude Haiku 4.5',\n contextWindow: 200000,\n tags: ['budget'],\n },\n {\n id: 'claude-sonnet-4-6-20260107',\n name: 'Claude Sonnet 4.6',\n contextWindow: 200000,\n tags: ['flagship'],\n },\n {\n id: 'claude-opus-4-6-20260107',\n name: 'Claude Opus 4.6',\n contextWindow: 1000000,\n tags: ['flagship', 'reasoning'],\n },\n] as const\n\n/**\n * Factory returning a fresh `AnthropicAdapter` instance. The adapter is\n * stateless \u2014 caller is free to reuse the returned object.\n *\n * `createModel` accepts an optional `baseURL` from {@link LlmCreateModelOptions}\n * and forwards it to `createAnthropic({ apiKey, baseURL })`.\n *\n * baseURL only works for Anthropic Messages-protocol relays (Cloudflare AI\n * Gateway in Anthropic mode, Helicone proxy). For OpenAI-format gateways,\n * use the OpenAI / OpenRouter presets.\n */\nexport function createAnthropicAdapter(): LlmProvider {\n const envKeys = ['ANTHROPIC_API_KEY', 'OPENCODE_ANTHROPIC_API_KEY'] as const\n\n function resolveApiKey(env?: EnvLookup): string | null {\n const lookup = env ?? process.env\n for (const key of envKeys) {\n const value = lookup[key]\n if (typeof value === 'string') {\n const trimmed = value.trim()\n if (trimmed.length > 0) return trimmed\n }\n }\n return null\n }\n\n return {\n id: 'anthropic',\n name: 'Anthropic',\n envKeys,\n defaultModel: DEFAULT_MODEL,\n defaultModels: DEFAULT_MODELS,\n\n isConfigured(env?: EnvLookup): boolean {\n return resolveApiKey(env) !== null\n },\n\n resolveApiKey,\n\n getConfiguredEnvKey(env?: EnvLookup): string {\n const lookup = env ?? process.env\n for (const key of envKeys) {\n const value = lookup[key]\n if (typeof value === 'string' && value.trim().length > 0) {\n return key\n }\n }\n return envKeys[0]\n },\n\n createModel(options: LlmCreateModelOptions): unknown {\n const anthropic = createAnthropic({\n apiKey: options.apiKey,\n ...(options.baseURL ? { baseURL: options.baseURL } : {}),\n })\n return anthropic(options.modelId)\n },\n }\n}\n"],
4
+ "sourcesContent": ["/**\n * AnthropicAdapter \u2014 implements the LlmProvider port for Anthropic's\n * Messages API (Claude Haiku, Sonnet, Opus).\n *\n * Wraps `createAnthropic({ apiKey })` from `@ai-sdk/anthropic` and exposes\n * a curated model list for the AI Assistant UI dropdown.\n *\n * @see packages/shared/src/lib/ai/llm-provider.ts\n * @see .ai/specs/implemented/2026-04-14-llm-provider-ports-and-adapters.md\n */\n\nimport { createAnthropic } from '@ai-sdk/anthropic'\nimport type {\n EnvLookup,\n LlmCreateModelOptions,\n LlmModelInfo,\n LlmProvider,\n} from '@open-mercato/shared/lib/ai/llm-provider'\n\nconst DEFAULT_MODEL = 'claude-haiku-4-5-20251001'\n\nconst DEFAULT_MODELS: readonly LlmModelInfo[] = [\n {\n id: 'claude-haiku-4-5-20251001',\n name: 'Claude Haiku 4.5',\n contextWindow: 200000,\n tags: ['budget'],\n },\n {\n id: 'claude-sonnet-4-6-20260107',\n name: 'Claude Sonnet 4.6',\n contextWindow: 200000,\n tags: ['flagship'],\n },\n {\n id: 'claude-opus-4-6-20260107',\n name: 'Claude Opus 4.6',\n contextWindow: 1000000,\n tags: ['flagship', 'reasoning'],\n },\n] as const\n\n/**\n * Factory returning a fresh `AnthropicAdapter` instance. The adapter is\n * stateless \u2014 caller is free to reuse the returned object.\n *\n * `createModel` accepts an optional `baseURL` from {@link LlmCreateModelOptions}\n * and forwards it to `createAnthropic({ apiKey, baseURL })`.\n *\n * baseURL only works for Anthropic Messages-protocol relays (Cloudflare AI\n * Gateway in Anthropic mode, Helicone proxy). For OpenAI-format gateways,\n * use the OpenAI / OpenRouter presets.\n */\nexport function createAnthropicAdapter(): LlmProvider {\n const envKeys = ['ANTHROPIC_API_KEY', 'OPENCODE_ANTHROPIC_API_KEY'] as const\n\n function resolveApiKey(env?: EnvLookup): string | null {\n const lookup = env ?? process.env\n for (const key of envKeys) {\n const value = lookup[key]\n if (typeof value === 'string') {\n const trimmed = value.trim()\n if (trimmed.length > 0) return trimmed\n }\n }\n return null\n }\n\n return {\n id: 'anthropic',\n name: 'Anthropic',\n envKeys,\n defaultModel: DEFAULT_MODEL,\n defaultModels: DEFAULT_MODELS,\n\n isConfigured(env?: EnvLookup): boolean {\n return resolveApiKey(env) !== null\n },\n\n resolveApiKey,\n\n getConfiguredEnvKey(env?: EnvLookup): string {\n const lookup = env ?? process.env\n for (const key of envKeys) {\n const value = lookup[key]\n if (typeof value === 'string' && value.trim().length > 0) {\n return key\n }\n }\n return envKeys[0]\n },\n\n createModel(options: LlmCreateModelOptions): unknown {\n const anthropic = createAnthropic({\n apiKey: options.apiKey,\n ...(options.baseURL ? { baseURL: options.baseURL } : {}),\n })\n return anthropic(options.modelId)\n },\n }\n}\n"],
5
5
  "mappings": "AAWA,SAAS,uBAAuB;AAQhC,MAAM,gBAAgB;AAEtB,MAAM,iBAA0C;AAAA,EAC9C;AAAA,IACE,IAAI;AAAA,IACJ,MAAM;AAAA,IACN,eAAe;AAAA,IACf,MAAM,CAAC,QAAQ;AAAA,EACjB;AAAA,EACA;AAAA,IACE,IAAI;AAAA,IACJ,MAAM;AAAA,IACN,eAAe;AAAA,IACf,MAAM,CAAC,UAAU;AAAA,EACnB;AAAA,EACA;AAAA,IACE,IAAI;AAAA,IACJ,MAAM;AAAA,IACN,eAAe;AAAA,IACf,MAAM,CAAC,YAAY,WAAW;AAAA,EAChC;AACF;AAaO,SAAS,yBAAsC;AACpD,QAAM,UAAU,CAAC,qBAAqB,4BAA4B;AAElE,WAAS,cAAc,KAAgC;AACrD,UAAM,SAAS,OAAO,QAAQ;AAC9B,eAAW,OAAO,SAAS;AACzB,YAAM,QAAQ,OAAO,GAAG;AACxB,UAAI,OAAO,UAAU,UAAU;AAC7B,cAAM,UAAU,MAAM,KAAK;AAC3B,YAAI,QAAQ,SAAS,EAAG,QAAO;AAAA,MACjC;AAAA,IACF;AACA,WAAO;AAAA,EACT;AAEA,SAAO;AAAA,IACL,IAAI;AAAA,IACJ,MAAM;AAAA,IACN;AAAA,IACA,cAAc;AAAA,IACd,eAAe;AAAA,IAEf,aAAa,KAA0B;AACrC,aAAO,cAAc,GAAG,MAAM;AAAA,IAChC;AAAA,IAEA;AAAA,IAEA,oBAAoB,KAAyB;AAC3C,YAAM,SAAS,OAAO,QAAQ;AAC9B,iBAAW,OAAO,SAAS;AACzB,cAAM,QAAQ,OAAO,GAAG;AACxB,YAAI,OAAO,UAAU,YAAY,MAAM,KAAK,EAAE,SAAS,GAAG;AACxD,iBAAO;AAAA,QACT;AAAA,MACF;AACA,aAAO,QAAQ,CAAC;AAAA,IAClB;AAAA,IAEA,YAAY,SAAyC;AACnD,YAAM,YAAY,gBAAgB;AAAA,QAChC,QAAQ,QAAQ;AAAA,QAChB,GAAI,QAAQ,UAAU,EAAE,SAAS,QAAQ,QAAQ,IAAI,CAAC;AAAA,MACxD,CAAC;AACD,aAAO,UAAU,QAAQ,OAAO;AAAA,IAClC;AAAA,EACF;AACF;",
6
6
  "names": []
7
7
  }