@open-mercato/ai-assistant 0.6.8-develop.7100.1.fbf66fca35 → 0.7.0

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 (41) hide show
  1. package/.turbo/turbo-build.log +1 -1
  2. package/AGENTS.md +1 -1
  3. package/dist/modules/ai_assistant/ai-tools/search-pack.js +3 -93
  4. package/dist/modules/ai_assistant/ai-tools/search-pack.js.map +3 -3
  5. package/dist/modules/ai_assistant/backend/config/ai-assistant/moderation-flags/AiModerationFlagsPageClient.js +0 -2
  6. package/dist/modules/ai_assistant/backend/config/ai-assistant/moderation-flags/AiModerationFlagsPageClient.js.map +2 -2
  7. package/dist/modules/ai_assistant/lib/codemode-tools.js +6 -14
  8. package/dist/modules/ai_assistant/lib/codemode-tools.js.map +2 -2
  9. package/dist/modules/ai_assistant/lib/generated-registry-loader.js +2 -10
  10. package/dist/modules/ai_assistant/lib/generated-registry-loader.js.map +2 -2
  11. package/dist/modules/ai_assistant/lib/http-server.js +1 -3
  12. package/dist/modules/ai_assistant/lib/http-server.js.map +2 -2
  13. package/dist/modules/ai_assistant/lib/in-process-client.js +1 -3
  14. package/dist/modules/ai_assistant/lib/in-process-client.js.map +2 -2
  15. package/dist/modules/ai_assistant/lib/mcp-client.js +1 -2
  16. package/dist/modules/ai_assistant/lib/mcp-client.js.map +2 -2
  17. package/dist/modules/ai_assistant/lib/mcp-dev-server.js +1 -3
  18. package/dist/modules/ai_assistant/lib/mcp-dev-server.js.map +2 -2
  19. package/dist/modules/ai_assistant/lib/mcp-server.js +1 -3
  20. package/dist/modules/ai_assistant/lib/mcp-server.js.map +2 -2
  21. package/package.json +7 -8
  22. package/src/modules/ai_assistant/__tests__/integration/ws-c-tool-pack-coverage.test.ts +0 -5
  23. package/src/modules/ai_assistant/ai-tools/__tests__/search-pack.test.ts +5 -211
  24. package/src/modules/ai_assistant/ai-tools/search-pack.ts +4 -110
  25. package/src/modules/ai_assistant/backend/config/ai-assistant/moderation-flags/AiModerationFlagsPageClient.tsx +0 -3
  26. package/src/modules/ai_assistant/lib/__tests__/generated-registry-loader.test.ts +0 -16
  27. package/src/modules/ai_assistant/lib/__tests__/mcp-client.test.ts +0 -30
  28. package/src/modules/ai_assistant/lib/codemode-tools.ts +7 -21
  29. package/src/modules/ai_assistant/lib/generated-registry-loader.ts +2 -10
  30. package/src/modules/ai_assistant/lib/http-server.ts +0 -2
  31. package/src/modules/ai_assistant/lib/in-process-client.ts +0 -2
  32. package/src/modules/ai_assistant/lib/mcp-client.ts +0 -1
  33. package/src/modules/ai_assistant/lib/mcp-dev-server.ts +0 -2
  34. package/src/modules/ai_assistant/lib/mcp-server.ts +0 -2
  35. package/src/modules/ai_assistant/lib/types.ts +0 -11
  36. package/dist/modules/ai_assistant/lib/mcp-tool-annotations.js +0 -18
  37. package/dist/modules/ai_assistant/lib/mcp-tool-annotations.js.map +0 -7
  38. package/src/modules/ai_assistant/lib/__tests__/codemode-tool-annotations.test.ts +0 -58
  39. package/src/modules/ai_assistant/lib/__tests__/mcp-server-tool-annotations.test.ts +0 -120
  40. package/src/modules/ai_assistant/lib/__tests__/mcp-tool-annotations.test.ts +0 -57
  41. package/src/modules/ai_assistant/lib/mcp-tool-annotations.ts +0 -35
@@ -1,2 +1,2 @@
1
- [build:ai-assistant] found 233 entry points
1
+ [build:ai-assistant] found 232 entry points
2
2
  [build:ai-assistant] built successfully
package/AGENTS.md CHANGED
@@ -166,7 +166,7 @@ const listPeopleTool = defineAiTool({
166
166
  MUST rules:
167
167
  - MUST set `requiredFeatures` for any tool that reads or writes tenant data. The wildcard-aware ACL matcher is applied before the handler runs.
168
168
  - MUST use Zod for `inputSchema` — never raw JSON Schema.
169
- - MUST set `isMutation: true` on write tools. It gates read-only agents/overrides and drives the MCP `readOnlyHint` in `tools/list`.
169
+ - MUST set `isMutation: true` on write tools. The policy gate strips these from read-only agents and from read-only tenant overrides.
170
170
  - MUST route every mutation tool through `prepareMutation(...)` (see the Mutation Approvals guide at `/framework/ai-assistant/mutation-approvals`). Writing directly inside the handler bypasses the approval gate — the runtime fails closed and refuses to return a result to the operator.
171
171
  - Mutation preview resolvers SHOULD return a normalized `after` snapshot and display hints when tool inputs contain dictionary IDs or other opaque values. Use `display.fieldLabels`, `display.before`, and `display.after` so `field-diff-card` shows operator-friendly names while `field`, `before`, and `after` keep the raw execution values.
172
172
  - MUST expose tools to an agent by listing the tool name in the agent's `allowedTools`. Tools not on the whitelist never reach the model.
@@ -1,49 +1,5 @@
1
1
  import { z } from "zod";
2
- import {
3
- canReadSearchEntity,
4
- filterSearchResultsByEntityAccess,
5
- resolveReadableEntityTypes
6
- } from "@open-mercato/shared/lib/search/entityAccess";
7
2
  import { defineAiTool } from "../lib/ai-tool-definition.js";
8
- class SearchToolAuthorizationError extends Error {
9
- constructor(message) {
10
- super(message);
11
- this.name = "SearchToolAuthorizationError";
12
- }
13
- }
14
- function resolveSearchIndexer(ctx) {
15
- try {
16
- const indexer = ctx.container.resolve("searchIndexer");
17
- if (indexer && typeof indexer.getEntityConfig === "function" && typeof indexer.getAllEntityConfigs === "function") {
18
- return indexer;
19
- }
20
- } catch {
21
- }
22
- throw new SearchToolAuthorizationError("[internal] Search entity registry unavailable");
23
- }
24
- function authorizeEntityAccess(entityType, lookup, subject) {
25
- if (subject.isSuperAdmin) return;
26
- let denial;
27
- const allowed = canReadSearchEntity(entityType, lookup, subject, {
28
- onDeny: (_, reason) => {
29
- denial = reason;
30
- }
31
- });
32
- if (allowed) return;
33
- if (denial === "unconfigured") {
34
- throw new SearchToolAuthorizationError(`[internal] Entity type "${entityType}" is not configured for search`);
35
- }
36
- if (denial === "no-acl-features") {
37
- throw new SearchToolAuthorizationError(
38
- `[internal] Entity type "${entityType}" does not declare aclFeatures; access denied`
39
- );
40
- }
41
- const config = lookup.getEntityConfig(entityType);
42
- const required = config?.aclFeatures ?? [];
43
- throw new SearchToolAuthorizationError(
44
- `[internal] Insufficient permissions for entity "${entityType}". Required: ${required.join(", ")}`
45
- );
46
- }
47
3
  const hybridSearchInput = z.object({
48
4
  q: z.string().min(1).describe("Search query text."),
49
5
  limit: z.number().int().min(1).max(100).optional().describe("Maximum results (default 20, max 100)."),
@@ -65,49 +21,13 @@ const hybridSearchTool = defineAiTool({
65
21
  const service = ctx.container.resolve("searchService");
66
22
  const limit = input.limit ?? 20;
67
23
  const started = Date.now();
68
- const subject = {
69
- grantedFeatures: ctx.userFeatures,
70
- isSuperAdmin: ctx.isSuperAdmin
71
- };
72
- if (ctx.isSuperAdmin) {
73
- const results2 = await service.search(input.q, {
74
- tenantId: ctx.tenantId,
75
- organizationId: ctx.organizationId,
76
- limit,
77
- strategies: input.strategies,
78
- entityTypes: input.entityTypes
79
- });
80
- const timingMs2 = Date.now() - started;
81
- const strategiesUsed2 = Array.from(
82
- new Set(results2.map((result) => result.source).filter((id) => typeof id === "string"))
83
- );
84
- return {
85
- query: input.q,
86
- totalResults: results2.length,
87
- results: results2,
88
- strategiesUsed: strategiesUsed2,
89
- timing: { ms: timingMs2 }
90
- };
91
- }
92
- const lookup = resolveSearchIndexer(ctx);
93
- const readableEntityTypes = resolveReadableEntityTypes(lookup, subject, input.entityTypes);
94
- if (readableEntityTypes && readableEntityTypes.length === 0) {
95
- return {
96
- query: input.q,
97
- totalResults: 0,
98
- results: [],
99
- strategiesUsed: [],
100
- timing: { ms: Date.now() - started }
101
- };
102
- }
103
- const rawResults = await service.search(input.q, {
24
+ const results = await service.search(input.q, {
104
25
  tenantId: ctx.tenantId,
105
26
  organizationId: ctx.organizationId,
106
27
  limit,
107
28
  strategies: input.strategies,
108
- entityTypes: readableEntityTypes
29
+ entityTypes: input.entityTypes
109
30
  });
110
- const results = filterSearchResultsByEntityAccess(rawResults, lookup, subject);
111
31
  const timingMs = Date.now() - started;
112
32
  const strategiesUsed = Array.from(
113
33
  new Set(results.map((result) => result.source).filter((id) => typeof id === "string"))
@@ -137,23 +57,13 @@ const getRecordContextTool = defineAiTool({
137
57
  throw new Error("Tenant context is required for search.get_record_context");
138
58
  }
139
59
  const input = getRecordContextInput.parse(rawInput);
140
- const subject = {
141
- grantedFeatures: ctx.userFeatures,
142
- isSuperAdmin: ctx.isSuperAdmin
143
- };
144
- let lookup;
145
- if (!ctx.isSuperAdmin) {
146
- lookup = resolveSearchIndexer(ctx);
147
- authorizeEntityAccess(input.entityId, lookup, subject);
148
- }
149
60
  const service = ctx.container.resolve("searchService");
150
- const rawResults = await service.search(input.recordId, {
61
+ const results = await service.search(input.recordId, {
151
62
  tenantId: ctx.tenantId,
152
63
  organizationId: ctx.organizationId,
153
64
  limit: 5,
154
65
  entityTypes: [input.entityId]
155
66
  });
156
- const results = lookup ? filterSearchResultsByEntityAccess(rawResults, lookup, subject) : rawResults;
157
67
  const match = results.find((result) => result.recordId === input.recordId);
158
68
  if (!match) {
159
69
  return {
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../../src/modules/ai_assistant/ai-tools/search-pack.ts"],
4
- "sourcesContent": ["/**\n * General-purpose `search.*` tool pack (Phase 1 WS-C, Step 3.8).\n *\n * These tools are discovered by the generator alongside any other module\n * `ai-tools.ts`; they expose the existing `@open-mercato/search` runtime to\n * agents that whitelist them via `allowedTools`.\n */\nimport { z } from 'zod'\nimport type { SearchOptions, SearchResult, SearchStrategyId } from '@open-mercato/shared/modules/search'\nimport {\n canReadSearchEntity,\n filterSearchResultsByEntityAccess,\n resolveReadableEntityTypes,\n type SearchEntityAccessSubject,\n type SearchEntityConfigLookup,\n type SearchEntityDenyReason,\n} from '@open-mercato/shared/lib/search/entityAccess'\nimport { defineAiTool } from '../lib/ai-tool-definition'\nimport type { AiToolDefinition, McpToolContext } from '../lib/types'\n\ntype SearchServiceLike = {\n search: (query: string, options: SearchOptions) => Promise<SearchResult[]>\n}\n\nclass SearchToolAuthorizationError extends Error {\n constructor(message: string) {\n super(message)\n this.name = 'SearchToolAuthorizationError'\n }\n}\n\nfunction resolveSearchIndexer(ctx: McpToolContext): SearchEntityConfigLookup {\n try {\n const indexer = ctx.container.resolve('searchIndexer') as SearchEntityConfigLookup | undefined\n if (indexer && typeof indexer.getEntityConfig === 'function' && typeof indexer.getAllEntityConfigs === 'function') {\n return indexer\n }\n } catch {\n // fall through to throw\n }\n throw new SearchToolAuthorizationError('[internal] Search entity registry unavailable')\n}\n\nfunction authorizeEntityAccess(\n entityType: string,\n lookup: SearchEntityConfigLookup,\n subject: SearchEntityAccessSubject,\n): void {\n if (subject.isSuperAdmin) return\n let denial: SearchEntityDenyReason | undefined\n const allowed = canReadSearchEntity(entityType, lookup, subject, {\n onDeny: (_, reason) => {\n denial = reason\n },\n })\n if (allowed) return\n if (denial === 'unconfigured') {\n throw new SearchToolAuthorizationError(`[internal] Entity type \"${entityType}\" is not configured for search`)\n }\n if (denial === 'no-acl-features') {\n throw new SearchToolAuthorizationError(\n `[internal] Entity type \"${entityType}\" does not declare aclFeatures; access denied`,\n )\n }\n const config = lookup.getEntityConfig(entityType)\n const required = config?.aclFeatures ?? []\n throw new SearchToolAuthorizationError(\n `[internal] Insufficient permissions for entity \"${entityType}\". Required: ${required.join(', ')}`,\n )\n}\n\nconst hybridSearchInput = z.object({\n q: z.string().min(1).describe('Search query text.'),\n limit: z\n .number()\n .int()\n .min(1)\n .max(100)\n .optional()\n .describe('Maximum results (default 20, max 100).'),\n strategies: z\n .array(z.enum(['fulltext', 'vector', 'tokens']))\n .optional()\n .describe('Subset of strategies to run; defaults to the module defaults.'),\n entityTypes: z\n .array(z.string())\n .optional()\n .describe('Filter to specific entity ids (e.g. \"catalog:product\").'),\n})\n\nconst hybridSearchTool = defineAiTool({\n name: 'search.hybrid_search',\n displayName: 'Hybrid search',\n description:\n 'Run a global fulltext + vector + token search across enabled entities for the current tenant/organization.',\n inputSchema: hybridSearchInput,\n requiredFeatures: ['search.view'],\n tags: ['read', 'search'],\n handler: async (rawInput, ctx) => {\n if (!ctx.tenantId) {\n throw new Error('Tenant context is required for search.hybrid_search')\n }\n const input = hybridSearchInput.parse(rawInput)\n const service = ctx.container.resolve<SearchServiceLike>('searchService')\n const limit = input.limit ?? 20\n const started = Date.now()\n const subject: SearchEntityAccessSubject = {\n grantedFeatures: ctx.userFeatures,\n isSuperAdmin: ctx.isSuperAdmin,\n }\n\n if (ctx.isSuperAdmin) {\n const results = await service.search(input.q, {\n tenantId: ctx.tenantId,\n organizationId: ctx.organizationId,\n limit,\n strategies: input.strategies as SearchStrategyId[] | undefined,\n entityTypes: input.entityTypes,\n })\n const timingMs = Date.now() - started\n const strategiesUsed = Array.from(\n new Set(results.map((result) => result.source).filter((id): id is SearchStrategyId => typeof id === 'string')),\n )\n return {\n query: input.q,\n totalResults: results.length,\n results,\n strategiesUsed,\n timing: { ms: timingMs },\n }\n }\n\n const lookup = resolveSearchIndexer(ctx)\n const readableEntityTypes = resolveReadableEntityTypes(lookup, subject, input.entityTypes)\n if (readableEntityTypes && readableEntityTypes.length === 0) {\n return {\n query: input.q,\n totalResults: 0,\n results: [],\n strategiesUsed: [],\n timing: { ms: Date.now() - started },\n }\n }\n\n const rawResults = await service.search(input.q, {\n tenantId: ctx.tenantId,\n organizationId: ctx.organizationId,\n limit,\n strategies: input.strategies as SearchStrategyId[] | undefined,\n entityTypes: readableEntityTypes,\n })\n const results = filterSearchResultsByEntityAccess(rawResults, lookup, subject)\n const timingMs = Date.now() - started\n const strategiesUsed = Array.from(\n new Set(results.map((result) => result.source).filter((id): id is SearchStrategyId => typeof id === 'string')),\n )\n return {\n query: input.q,\n totalResults: results.length,\n results,\n strategiesUsed,\n timing: { ms: timingMs },\n }\n },\n})\n\nconst getRecordContextInput = z.object({\n entityId: z.string().min(1).describe('Entity identifier (e.g. \"customers:customer_person_profile\").'),\n recordId: z.string().min(1).describe('Record primary key (UUID).'),\n})\n\nconst getRecordContextTool = defineAiTool({\n name: 'search.get_record_context',\n displayName: 'Get record context',\n description:\n 'Resolve presenter, links, and URL for a specific record by re-querying the search index. Returns { found: false } when no hit matches the recordId.',\n inputSchema: getRecordContextInput,\n requiredFeatures: ['search.view'],\n tags: ['read', 'search'],\n handler: async (rawInput, ctx) => {\n if (!ctx.tenantId) {\n throw new Error('Tenant context is required for search.get_record_context')\n }\n const input = getRecordContextInput.parse(rawInput)\n const subject: SearchEntityAccessSubject = {\n grantedFeatures: ctx.userFeatures,\n isSuperAdmin: ctx.isSuperAdmin,\n }\n\n let lookup: SearchEntityConfigLookup | undefined\n if (!ctx.isSuperAdmin) {\n lookup = resolveSearchIndexer(ctx)\n authorizeEntityAccess(input.entityId, lookup, subject)\n }\n\n const service = ctx.container.resolve<SearchServiceLike>('searchService')\n const rawResults = await service.search(input.recordId, {\n tenantId: ctx.tenantId,\n organizationId: ctx.organizationId,\n limit: 5,\n entityTypes: [input.entityId],\n })\n const results = lookup ? filterSearchResultsByEntityAccess(rawResults, lookup, subject) : rawResults\n const match = results.find((result) => result.recordId === input.recordId)\n if (!match) {\n return {\n found: false as const,\n entityId: input.entityId,\n recordId: input.recordId,\n }\n }\n return {\n found: true as const,\n entityId: match.entityId,\n recordId: match.recordId,\n presenter: match.presenter,\n url: match.url,\n links: match.links,\n metadata: match.metadata,\n source: match.source,\n score: match.score,\n }\n },\n})\n\nexport const searchAiTools: AiToolDefinition<any, any>[] = [hybridSearchTool, getRecordContextTool]\n\nexport default searchAiTools\n"],
5
- "mappings": "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",
6
- "names": ["results", "timingMs", "strategiesUsed"]
4
+ "sourcesContent": ["/**\n * General-purpose `search.*` tool pack (Phase 1 WS-C, Step 3.8).\n *\n * These tools are discovered by the generator alongside any other module\n * `ai-tools.ts`; they expose the existing `@open-mercato/search` runtime to\n * agents that whitelist them via `allowedTools`.\n */\nimport { z } from 'zod'\nimport type { SearchOptions, SearchResult, SearchStrategyId } from '@open-mercato/shared/modules/search'\nimport { defineAiTool } from '../lib/ai-tool-definition'\nimport type { AiToolDefinition } from '../lib/types'\n\ntype SearchServiceLike = {\n search: (query: string, options: SearchOptions) => Promise<SearchResult[]>\n}\n\nconst hybridSearchInput = z.object({\n q: z.string().min(1).describe('Search query text.'),\n limit: z\n .number()\n .int()\n .min(1)\n .max(100)\n .optional()\n .describe('Maximum results (default 20, max 100).'),\n strategies: z\n .array(z.enum(['fulltext', 'vector', 'tokens']))\n .optional()\n .describe('Subset of strategies to run; defaults to the module defaults.'),\n entityTypes: z\n .array(z.string())\n .optional()\n .describe('Filter to specific entity ids (e.g. \"catalog:product\").'),\n})\n\nconst hybridSearchTool = defineAiTool({\n name: 'search.hybrid_search',\n displayName: 'Hybrid search',\n description:\n 'Run a global fulltext + vector + token search across enabled entities for the current tenant/organization.',\n inputSchema: hybridSearchInput,\n requiredFeatures: ['search.view'],\n tags: ['read', 'search'],\n handler: async (rawInput, ctx) => {\n if (!ctx.tenantId) {\n throw new Error('Tenant context is required for search.hybrid_search')\n }\n const input = hybridSearchInput.parse(rawInput)\n const service = ctx.container.resolve<SearchServiceLike>('searchService')\n const limit = input.limit ?? 20\n const started = Date.now()\n const results = await service.search(input.q, {\n tenantId: ctx.tenantId,\n organizationId: ctx.organizationId,\n limit,\n strategies: input.strategies as SearchStrategyId[] | undefined,\n entityTypes: input.entityTypes,\n })\n const timingMs = Date.now() - started\n const strategiesUsed = Array.from(\n new Set(results.map((result) => result.source).filter((id): id is SearchStrategyId => typeof id === 'string')),\n )\n return {\n query: input.q,\n totalResults: results.length,\n results,\n strategiesUsed,\n timing: { ms: timingMs },\n }\n },\n})\n\nconst getRecordContextInput = z.object({\n entityId: z.string().min(1).describe('Entity identifier (e.g. \"customers:customer_person_profile\").'),\n recordId: z.string().min(1).describe('Record primary key (UUID).'),\n})\n\nconst getRecordContextTool = defineAiTool({\n name: 'search.get_record_context',\n displayName: 'Get record context',\n description:\n 'Resolve presenter, links, and URL for a specific record by re-querying the search index. Returns { found: false } when no hit matches the recordId.',\n inputSchema: getRecordContextInput,\n requiredFeatures: ['search.view'],\n tags: ['read', 'search'],\n handler: async (rawInput, ctx) => {\n if (!ctx.tenantId) {\n throw new Error('Tenant context is required for search.get_record_context')\n }\n const input = getRecordContextInput.parse(rawInput)\n const service = ctx.container.resolve<SearchServiceLike>('searchService')\n const results = await service.search(input.recordId, {\n tenantId: ctx.tenantId,\n organizationId: ctx.organizationId,\n limit: 5,\n entityTypes: [input.entityId],\n })\n const match = results.find((result) => result.recordId === input.recordId)\n if (!match) {\n return {\n found: false as const,\n entityId: input.entityId,\n recordId: input.recordId,\n }\n }\n return {\n found: true as const,\n entityId: match.entityId,\n recordId: match.recordId,\n presenter: match.presenter,\n url: match.url,\n links: match.links,\n metadata: match.metadata,\n source: match.source,\n score: match.score,\n }\n },\n})\n\nexport const searchAiTools: AiToolDefinition<any, any>[] = [hybridSearchTool, getRecordContextTool]\n\nexport default searchAiTools\n"],
5
+ "mappings": "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",
6
+ "names": []
7
7
  }
@@ -77,7 +77,6 @@ function AiModerationFlagsPageClient() {
77
77
  [t]
78
78
  );
79
79
  const total = query.data?.total ?? 0;
80
- const totalIsCapped = query.data?.totalIsCapped === true;
81
80
  return /* @__PURE__ */ jsxs("div", { className: "flex flex-col gap-4", children: [
82
81
  /* @__PURE__ */ jsxs("div", { children: [
83
82
  /* @__PURE__ */ jsx("h2", { className: "text-base font-semibold", children: t("ai_assistant.moderationFlags.title", "Moderation flags") }),
@@ -126,7 +125,6 @@ function AiModerationFlagsPageClient() {
126
125
  pageSize: PAGE_SIZE,
127
126
  total,
128
127
  totalPages: Math.max(1, Math.ceil(total / PAGE_SIZE)),
129
- totalIsCapped,
130
128
  onPageChange: setPage
131
129
  }
132
130
  }
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../../../../../src/modules/ai_assistant/backend/config/ai-assistant/moderation-flags/AiModerationFlagsPageClient.tsx"],
4
- "sourcesContent": ["'use client'\n\nimport * as React from 'react'\nimport { useQuery } from '@tanstack/react-query'\nimport type { LegacyColumnDef as ColumnDef } from '@tanstack/react-table/legacy'\nimport { useT } from '@open-mercato/shared/lib/i18n/context'\nimport { DataTable } from '@open-mercato/ui/backend/DataTable'\nimport { Button } from '@open-mercato/ui/primitives/button'\nimport { Input } from '@open-mercato/ui/primitives/input'\nimport { Label } from '@open-mercato/ui/primitives/label'\nimport { StatusBadge } from '@open-mercato/ui/primitives/status-badge'\nimport { apiCallOrThrow } from '@open-mercato/ui/backend/utils/apiCall'\n\nconst PAGE_SIZE = 50\n\ntype ModerationCategory = { flagged: boolean; score: number }\n\ntype ModerationFlagRow = {\n id: string\n tenantId: string\n organizationId: string | null\n agentId: string\n userId: string\n providerId: string\n modelId: string\n categories: Record<string, ModerationCategory>\n createdAt: string\n}\n\ntype ModerationFlagsResponse = {\n items: ModerationFlagRow[]\n total: number\n page: number\n pageSize: number\n totalIsCapped?: boolean\n}\n\nasync function fetchModerationFlags(params: {\n page: number\n from: string\n to: string\n agentId: string\n userId: string\n}): Promise<ModerationFlagsResponse> {\n const search = new URLSearchParams()\n search.set('page', String(params.page))\n search.set('pageSize', String(PAGE_SIZE))\n if (params.from) search.set('from', params.from)\n if (params.to) search.set('to', params.to)\n if (params.agentId) search.set('agentId', params.agentId)\n if (params.userId) search.set('userId', params.userId)\n const { result, status } = await apiCallOrThrow<ModerationFlagsResponse>(\n `/api/ai_assistant/moderation-flags?${search.toString()}`,\n undefined,\n { errorMessage: 'Failed to load moderation flags' },\n )\n if (!result) throw new Error(`Failed to load moderation flags (${status})`)\n return result\n}\n\nfunction flaggedCategoryNames(categories: Record<string, ModerationCategory>): string[] {\n return Object.entries(categories)\n .filter(([, value]) => value.flagged)\n .map(([name]) => name)\n}\n\nexport function AiModerationFlagsPageClient() {\n const t = useT()\n const [page, setPage] = React.useState(1)\n const [from, setFrom] = React.useState('')\n const [to, setTo] = React.useState('')\n const [agentId, setAgentId] = React.useState('')\n const [userId, setUserId] = React.useState('')\n const [applied, setApplied] = React.useState({ from: '', to: '', agentId: '', userId: '' })\n\n const query = useQuery<ModerationFlagsResponse>({\n queryKey: ['ai_assistant', 'moderation_flags', page, applied],\n queryFn: () => fetchModerationFlags({ page, ...applied }),\n retry: false,\n })\n\n const applyFilters = React.useCallback(() => {\n setPage(1)\n setApplied({ from, to, agentId, userId })\n }, [from, to, agentId, userId])\n\n const columns = React.useMemo<ColumnDef<ModerationFlagRow, unknown>[]>(\n () => [\n {\n accessorKey: 'agentId',\n header: t('ai_assistant.moderationFlags.columns.agent', 'Agent'),\n },\n {\n accessorKey: 'userId',\n header: t('ai_assistant.moderationFlags.columns.user', 'User'),\n meta: { truncate: true, maxWidth: 220 },\n },\n {\n accessorKey: 'categories',\n header: t('ai_assistant.moderationFlags.columns.categories', 'Categories'),\n cell: ({ row }) => {\n const names = flaggedCategoryNames(row.original.categories)\n if (names.length === 0) {\n return <span className=\"text-muted-foreground\">\u2014</span>\n }\n return (\n <div className=\"flex flex-wrap gap-1\">\n {names.map((name) => (\n <StatusBadge key={name} variant=\"error\" dot>\n {name}\n </StatusBadge>\n ))}\n </div>\n )\n },\n },\n {\n accessorKey: 'createdAt',\n header: t('ai_assistant.moderationFlags.columns.createdAt', 'Flagged at'),\n cell: ({ row }) => new Date(row.original.createdAt).toLocaleString(),\n },\n ],\n [t],\n )\n\n const total = query.data?.total ?? 0\n const totalIsCapped = query.data?.totalIsCapped === true\n\n return (\n <div className=\"flex flex-col gap-4\">\n <div>\n <h2 className=\"text-base font-semibold\">\n {t('ai_assistant.moderationFlags.title', 'Moderation flags')}\n </h2>\n <p className=\"text-xs text-muted-foreground\">\n {t(\n 'ai_assistant.moderationFlags.subtitle',\n 'Inputs blocked by the content safety filter. Category flags and scores only \u2014 no prompt content is stored.',\n )}\n </p>\n </div>\n\n <div className=\"flex flex-wrap items-end gap-3\">\n <div className=\"flex flex-col gap-1\">\n <Label htmlFor=\"moderation-from\">{t('ai_assistant.moderationFlags.filters.from', 'From')}</Label>\n <Input id=\"moderation-from\" type=\"date\" value={from} onChange={(e) => setFrom(e.target.value)} />\n </div>\n <div className=\"flex flex-col gap-1\">\n <Label htmlFor=\"moderation-to\">{t('ai_assistant.moderationFlags.filters.to', 'To')}</Label>\n <Input id=\"moderation-to\" type=\"date\" value={to} onChange={(e) => setTo(e.target.value)} />\n </div>\n <div className=\"flex flex-col gap-1\">\n <Label htmlFor=\"moderation-agent\">{t('ai_assistant.moderationFlags.columns.agent', 'Agent')}</Label>\n <Input\n id=\"moderation-agent\"\n value={agentId}\n onChange={(e) => setAgentId(e.target.value)}\n placeholder={t('ai_assistant.moderationFlags.filters.agentPlaceholder', 'module.agent')}\n />\n </div>\n <div className=\"flex flex-col gap-1\">\n <Label htmlFor=\"moderation-user\">{t('ai_assistant.moderationFlags.columns.user', 'User')}</Label>\n <Input id=\"moderation-user\" value={userId} onChange={(e) => setUserId(e.target.value)} />\n </div>\n <Button type=\"button\" variant=\"outline\" onClick={applyFilters}>\n {t('ai_assistant.moderationFlags.filters.apply', 'Apply')}\n </Button>\n </div>\n\n <DataTable<ModerationFlagRow>\n columns={columns}\n data={query.data?.items ?? []}\n isLoading={query.isLoading}\n error={query.error ? (query.error as Error).message : undefined}\n emptyState={t('ai_assistant.moderationFlags.empty', 'No flagged messages.')}\n pagination={{\n page,\n pageSize: PAGE_SIZE,\n total,\n totalPages: Math.max(1, Math.ceil(total / PAGE_SIZE)),\n totalIsCapped,\n onPageChange: setPage,\n }}\n />\n </div>\n )\n}\n\nexport default AiModerationFlagsPageClient\n"],
5
- "mappings": "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",
4
+ "sourcesContent": ["'use client'\n\nimport * as React from 'react'\nimport { useQuery } from '@tanstack/react-query'\nimport type { LegacyColumnDef as ColumnDef } from '@tanstack/react-table/legacy'\nimport { useT } from '@open-mercato/shared/lib/i18n/context'\nimport { DataTable } from '@open-mercato/ui/backend/DataTable'\nimport { Button } from '@open-mercato/ui/primitives/button'\nimport { Input } from '@open-mercato/ui/primitives/input'\nimport { Label } from '@open-mercato/ui/primitives/label'\nimport { StatusBadge } from '@open-mercato/ui/primitives/status-badge'\nimport { apiCallOrThrow } from '@open-mercato/ui/backend/utils/apiCall'\n\nconst PAGE_SIZE = 50\n\ntype ModerationCategory = { flagged: boolean; score: number }\n\ntype ModerationFlagRow = {\n id: string\n tenantId: string\n organizationId: string | null\n agentId: string\n userId: string\n providerId: string\n modelId: string\n categories: Record<string, ModerationCategory>\n createdAt: string\n}\n\ntype ModerationFlagsResponse = {\n items: ModerationFlagRow[]\n total: number\n page: number\n pageSize: number\n}\n\nasync function fetchModerationFlags(params: {\n page: number\n from: string\n to: string\n agentId: string\n userId: string\n}): Promise<ModerationFlagsResponse> {\n const search = new URLSearchParams()\n search.set('page', String(params.page))\n search.set('pageSize', String(PAGE_SIZE))\n if (params.from) search.set('from', params.from)\n if (params.to) search.set('to', params.to)\n if (params.agentId) search.set('agentId', params.agentId)\n if (params.userId) search.set('userId', params.userId)\n const { result, status } = await apiCallOrThrow<ModerationFlagsResponse>(\n `/api/ai_assistant/moderation-flags?${search.toString()}`,\n undefined,\n { errorMessage: 'Failed to load moderation flags' },\n )\n if (!result) throw new Error(`Failed to load moderation flags (${status})`)\n return result\n}\n\nfunction flaggedCategoryNames(categories: Record<string, ModerationCategory>): string[] {\n return Object.entries(categories)\n .filter(([, value]) => value.flagged)\n .map(([name]) => name)\n}\n\nexport function AiModerationFlagsPageClient() {\n const t = useT()\n const [page, setPage] = React.useState(1)\n const [from, setFrom] = React.useState('')\n const [to, setTo] = React.useState('')\n const [agentId, setAgentId] = React.useState('')\n const [userId, setUserId] = React.useState('')\n const [applied, setApplied] = React.useState({ from: '', to: '', agentId: '', userId: '' })\n\n const query = useQuery<ModerationFlagsResponse>({\n queryKey: ['ai_assistant', 'moderation_flags', page, applied],\n queryFn: () => fetchModerationFlags({ page, ...applied }),\n retry: false,\n })\n\n const applyFilters = React.useCallback(() => {\n setPage(1)\n setApplied({ from, to, agentId, userId })\n }, [from, to, agentId, userId])\n\n const columns = React.useMemo<ColumnDef<ModerationFlagRow, unknown>[]>(\n () => [\n {\n accessorKey: 'agentId',\n header: t('ai_assistant.moderationFlags.columns.agent', 'Agent'),\n },\n {\n accessorKey: 'userId',\n header: t('ai_assistant.moderationFlags.columns.user', 'User'),\n meta: { truncate: true, maxWidth: 220 },\n },\n {\n accessorKey: 'categories',\n header: t('ai_assistant.moderationFlags.columns.categories', 'Categories'),\n cell: ({ row }) => {\n const names = flaggedCategoryNames(row.original.categories)\n if (names.length === 0) {\n return <span className=\"text-muted-foreground\">\u2014</span>\n }\n return (\n <div className=\"flex flex-wrap gap-1\">\n {names.map((name) => (\n <StatusBadge key={name} variant=\"error\" dot>\n {name}\n </StatusBadge>\n ))}\n </div>\n )\n },\n },\n {\n accessorKey: 'createdAt',\n header: t('ai_assistant.moderationFlags.columns.createdAt', 'Flagged at'),\n cell: ({ row }) => new Date(row.original.createdAt).toLocaleString(),\n },\n ],\n [t],\n )\n\n const total = query.data?.total ?? 0\n\n return (\n <div className=\"flex flex-col gap-4\">\n <div>\n <h2 className=\"text-base font-semibold\">\n {t('ai_assistant.moderationFlags.title', 'Moderation flags')}\n </h2>\n <p className=\"text-xs text-muted-foreground\">\n {t(\n 'ai_assistant.moderationFlags.subtitle',\n 'Inputs blocked by the content safety filter. Category flags and scores only \u2014 no prompt content is stored.',\n )}\n </p>\n </div>\n\n <div className=\"flex flex-wrap items-end gap-3\">\n <div className=\"flex flex-col gap-1\">\n <Label htmlFor=\"moderation-from\">{t('ai_assistant.moderationFlags.filters.from', 'From')}</Label>\n <Input id=\"moderation-from\" type=\"date\" value={from} onChange={(e) => setFrom(e.target.value)} />\n </div>\n <div className=\"flex flex-col gap-1\">\n <Label htmlFor=\"moderation-to\">{t('ai_assistant.moderationFlags.filters.to', 'To')}</Label>\n <Input id=\"moderation-to\" type=\"date\" value={to} onChange={(e) => setTo(e.target.value)} />\n </div>\n <div className=\"flex flex-col gap-1\">\n <Label htmlFor=\"moderation-agent\">{t('ai_assistant.moderationFlags.columns.agent', 'Agent')}</Label>\n <Input\n id=\"moderation-agent\"\n value={agentId}\n onChange={(e) => setAgentId(e.target.value)}\n placeholder={t('ai_assistant.moderationFlags.filters.agentPlaceholder', 'module.agent')}\n />\n </div>\n <div className=\"flex flex-col gap-1\">\n <Label htmlFor=\"moderation-user\">{t('ai_assistant.moderationFlags.columns.user', 'User')}</Label>\n <Input id=\"moderation-user\" value={userId} onChange={(e) => setUserId(e.target.value)} />\n </div>\n <Button type=\"button\" variant=\"outline\" onClick={applyFilters}>\n {t('ai_assistant.moderationFlags.filters.apply', 'Apply')}\n </Button>\n </div>\n\n <DataTable<ModerationFlagRow>\n columns={columns}\n data={query.data?.items ?? []}\n isLoading={query.isLoading}\n error={query.error ? (query.error as Error).message : undefined}\n emptyState={t('ai_assistant.moderationFlags.empty', 'No flagged messages.')}\n pagination={{\n page,\n pageSize: PAGE_SIZE,\n total,\n totalPages: Math.max(1, Math.ceil(total / PAGE_SIZE)),\n onPageChange: setPage,\n }}\n />\n </div>\n )\n}\n\nexport default AiModerationFlagsPageClient\n"],
5
+ "mappings": ";AAsGmB,cA0Bb,YA1Ba;AApGnB,YAAY,WAAW;AACvB,SAAS,gBAAgB;AAEzB,SAAS,YAAY;AACrB,SAAS,iBAAiB;AAC1B,SAAS,cAAc;AACvB,SAAS,aAAa;AACtB,SAAS,aAAa;AACtB,SAAS,mBAAmB;AAC5B,SAAS,sBAAsB;AAE/B,MAAM,YAAY;AAuBlB,eAAe,qBAAqB,QAMC;AACnC,QAAM,SAAS,IAAI,gBAAgB;AACnC,SAAO,IAAI,QAAQ,OAAO,OAAO,IAAI,CAAC;AACtC,SAAO,IAAI,YAAY,OAAO,SAAS,CAAC;AACxC,MAAI,OAAO,KAAM,QAAO,IAAI,QAAQ,OAAO,IAAI;AAC/C,MAAI,OAAO,GAAI,QAAO,IAAI,MAAM,OAAO,EAAE;AACzC,MAAI,OAAO,QAAS,QAAO,IAAI,WAAW,OAAO,OAAO;AACxD,MAAI,OAAO,OAAQ,QAAO,IAAI,UAAU,OAAO,MAAM;AACrD,QAAM,EAAE,QAAQ,OAAO,IAAI,MAAM;AAAA,IAC/B,sCAAsC,OAAO,SAAS,CAAC;AAAA,IACvD;AAAA,IACA,EAAE,cAAc,kCAAkC;AAAA,EACpD;AACA,MAAI,CAAC,OAAQ,OAAM,IAAI,MAAM,oCAAoC,MAAM,GAAG;AAC1E,SAAO;AACT;AAEA,SAAS,qBAAqB,YAA0D;AACtF,SAAO,OAAO,QAAQ,UAAU,EAC7B,OAAO,CAAC,CAAC,EAAE,KAAK,MAAM,MAAM,OAAO,EACnC,IAAI,CAAC,CAAC,IAAI,MAAM,IAAI;AACzB;AAEO,SAAS,8BAA8B;AAC5C,QAAM,IAAI,KAAK;AACf,QAAM,CAAC,MAAM,OAAO,IAAI,MAAM,SAAS,CAAC;AACxC,QAAM,CAAC,MAAM,OAAO,IAAI,MAAM,SAAS,EAAE;AACzC,QAAM,CAAC,IAAI,KAAK,IAAI,MAAM,SAAS,EAAE;AACrC,QAAM,CAAC,SAAS,UAAU,IAAI,MAAM,SAAS,EAAE;AAC/C,QAAM,CAAC,QAAQ,SAAS,IAAI,MAAM,SAAS,EAAE;AAC7C,QAAM,CAAC,SAAS,UAAU,IAAI,MAAM,SAAS,EAAE,MAAM,IAAI,IAAI,IAAI,SAAS,IAAI,QAAQ,GAAG,CAAC;AAE1F,QAAM,QAAQ,SAAkC;AAAA,IAC9C,UAAU,CAAC,gBAAgB,oBAAoB,MAAM,OAAO;AAAA,IAC5D,SAAS,MAAM,qBAAqB,EAAE,MAAM,GAAG,QAAQ,CAAC;AAAA,IACxD,OAAO;AAAA,EACT,CAAC;AAED,QAAM,eAAe,MAAM,YAAY,MAAM;AAC3C,YAAQ,CAAC;AACT,eAAW,EAAE,MAAM,IAAI,SAAS,OAAO,CAAC;AAAA,EAC1C,GAAG,CAAC,MAAM,IAAI,SAAS,MAAM,CAAC;AAE9B,QAAM,UAAU,MAAM;AAAA,IACpB,MAAM;AAAA,MACJ;AAAA,QACE,aAAa;AAAA,QACb,QAAQ,EAAE,8CAA8C,OAAO;AAAA,MACjE;AAAA,MACA;AAAA,QACE,aAAa;AAAA,QACb,QAAQ,EAAE,6CAA6C,MAAM;AAAA,QAC7D,MAAM,EAAE,UAAU,MAAM,UAAU,IAAI;AAAA,MACxC;AAAA,MACA;AAAA,QACE,aAAa;AAAA,QACb,QAAQ,EAAE,mDAAmD,YAAY;AAAA,QACzE,MAAM,CAAC,EAAE,IAAI,MAAM;AACjB,gBAAM,QAAQ,qBAAqB,IAAI,SAAS,UAAU;AAC1D,cAAI,MAAM,WAAW,GAAG;AACtB,mBAAO,oBAAC,UAAK,WAAU,yBAAwB,oBAAC;AAAA,UAClD;AACA,iBACE,oBAAC,SAAI,WAAU,wBACZ,gBAAM,IAAI,CAAC,SACV,oBAAC,eAAuB,SAAQ,SAAQ,KAAG,MACxC,kBADe,IAElB,CACD,GACH;AAAA,QAEJ;AAAA,MACF;AAAA,MACA;AAAA,QACE,aAAa;AAAA,QACb,QAAQ,EAAE,kDAAkD,YAAY;AAAA,QACxE,MAAM,CAAC,EAAE,IAAI,MAAM,IAAI,KAAK,IAAI,SAAS,SAAS,EAAE,eAAe;AAAA,MACrE;AAAA,IACF;AAAA,IACA,CAAC,CAAC;AAAA,EACJ;AAEA,QAAM,QAAQ,MAAM,MAAM,SAAS;AAEnC,SACE,qBAAC,SAAI,WAAU,uBACb;AAAA,yBAAC,SACC;AAAA,0BAAC,QAAG,WAAU,2BACX,YAAE,sCAAsC,kBAAkB,GAC7D;AAAA,MACA,oBAAC,OAAE,WAAU,iCACV;AAAA,QACC;AAAA,QACA;AAAA,MACF,GACF;AAAA,OACF;AAAA,IAEA,qBAAC,SAAI,WAAU,kCACb;AAAA,2BAAC,SAAI,WAAU,uBACb;AAAA,4BAAC,SAAM,SAAQ,mBAAmB,YAAE,6CAA6C,MAAM,GAAE;AAAA,QACzF,oBAAC,SAAM,IAAG,mBAAkB,MAAK,QAAO,OAAO,MAAM,UAAU,CAAC,MAAM,QAAQ,EAAE,OAAO,KAAK,GAAG;AAAA,SACjG;AAAA,MACA,qBAAC,SAAI,WAAU,uBACb;AAAA,4BAAC,SAAM,SAAQ,iBAAiB,YAAE,2CAA2C,IAAI,GAAE;AAAA,QACnF,oBAAC,SAAM,IAAG,iBAAgB,MAAK,QAAO,OAAO,IAAI,UAAU,CAAC,MAAM,MAAM,EAAE,OAAO,KAAK,GAAG;AAAA,SAC3F;AAAA,MACA,qBAAC,SAAI,WAAU,uBACb;AAAA,4BAAC,SAAM,SAAQ,oBAAoB,YAAE,8CAA8C,OAAO,GAAE;AAAA,QAC5F;AAAA,UAAC;AAAA;AAAA,YACC,IAAG;AAAA,YACH,OAAO;AAAA,YACP,UAAU,CAAC,MAAM,WAAW,EAAE,OAAO,KAAK;AAAA,YAC1C,aAAa,EAAE,yDAAyD,cAAc;AAAA;AAAA,QACxF;AAAA,SACF;AAAA,MACA,qBAAC,SAAI,WAAU,uBACb;AAAA,4BAAC,SAAM,SAAQ,mBAAmB,YAAE,6CAA6C,MAAM,GAAE;AAAA,QACzF,oBAAC,SAAM,IAAG,mBAAkB,OAAO,QAAQ,UAAU,CAAC,MAAM,UAAU,EAAE,OAAO,KAAK,GAAG;AAAA,SACzF;AAAA,MACA,oBAAC,UAAO,MAAK,UAAS,SAAQ,WAAU,SAAS,cAC9C,YAAE,8CAA8C,OAAO,GAC1D;AAAA,OACF;AAAA,IAEA;AAAA,MAAC;AAAA;AAAA,QACC;AAAA,QACA,MAAM,MAAM,MAAM,SAAS,CAAC;AAAA,QAC5B,WAAW,MAAM;AAAA,QACjB,OAAO,MAAM,QAAS,MAAM,MAAgB,UAAU;AAAA,QACtD,YAAY,EAAE,sCAAsC,sBAAsB;AAAA,QAC1E,YAAY;AAAA,UACV;AAAA,UACA,UAAU;AAAA,UACV;AAAA,UACA,YAAY,KAAK,IAAI,GAAG,KAAK,KAAK,QAAQ,SAAS,CAAC;AAAA,UACpD,cAAc;AAAA,QAChB;AAAA;AAAA,IACF;AAAA,KACF;AAEJ;AAEA,IAAO,sCAAQ;",
6
6
  "names": []
7
7
  }
@@ -378,9 +378,6 @@ function buildEntitySchemas(graph) {
378
378
  }
379
379
  const CODE_MODE_MAX_API_CALLS = 50;
380
380
  const CODE_MODE_MAX_MUTATION_CALLS = 20;
381
- function registerCodeModeTool(tool) {
382
- registerMcpTool(tool, { moduleId: "codemode" });
383
- }
384
381
  async function loadCodeModeTools() {
385
382
  const commonTypes = await generateCommonTypes();
386
383
  registerSearchTool();
@@ -388,10 +385,9 @@ async function loadCodeModeTools() {
388
385
  return 2;
389
386
  }
390
387
  function registerSearchTool() {
391
- registerCodeModeTool(
388
+ registerMcpTool(
392
389
  {
393
390
  name: "search",
394
- isMutation: false,
395
391
  description: `Query the OpenAPI spec and entity schemas. READ-ONLY, no side effects.
396
392
  Globals: spec.findEndpoints(keyword), spec.describeEndpoint(path, method), spec.describeEntity(keyword), spec.paths, spec.entitySchemas.
397
393
  Use BEFORE execute to learn endpoint schemas for CREATE/UPDATE. Skip for common paths (companies, people, orders, quotes, products).`,
@@ -451,22 +447,17 @@ Use BEFORE execute to learn endpoint schemas for CREATE/UPDATE. Skip for common
451
447
  _memoryContext: memoryContext
452
448
  };
453
449
  }
454
- }
450
+ },
451
+ { moduleId: "codemode" }
455
452
  );
456
453
  }
457
454
  function registerExecuteTool(commonTypes) {
458
455
  const typesBlock = commonTypes ? `
459
456
 
460
457
  ${commonTypes}` : "";
461
- registerCodeModeTool(
458
+ registerMcpTool(
462
459
  {
463
460
  name: "execute",
464
- // api.request() reaches every documented endpoint, including POST/PUT/DELETE,
465
- // so the tool is neither read-only nor guaranteed non-destructive. It is
466
- // intentionally exempt from prepareMutation: arbitrary sandbox code cannot
467
- // provide the structured before/after preview that approval flow requires.
468
- isMutation: true,
469
- isDestructive: true,
470
461
  description: `Make API calls. Returns JSON.
471
462
  Globals: api.request({ method, path, query?, body? }) \u2192 { success, statusCode, data }, context { tenantId, organizationId, userId }.
472
463
  RULES: For FIND/LIST \u2192 GET only (1 call). For UPDATE \u2192 PUT to collection path with id in BODY. NEVER PUT/POST/DELETE unless user explicitly asked to change data. Before ANY write operation (POST/PUT/DELETE), you MUST use the AskUserQuestion tool to get explicit user confirmation. Do NOT just ask in text \u2014 use the tool so execution pauses until the user responds.${typesBlock}`,
@@ -535,7 +526,8 @@ RULES: For FIND/LIST \u2192 GET only (1 call). For UPDATE \u2192 PUT to collecti
535
526
  _memoryContext: memoryContext
536
527
  };
537
528
  }
538
- }
529
+ },
530
+ { moduleId: "codemode" }
539
531
  );
540
532
  }
541
533
  function createApiRequestFn(ctx, onCall) {