@elliotding/ai-agent-mcp 0.2.21 → 0.2.23

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 (75) hide show
  1. package/dist/api/client.d.ts +4 -1
  2. package/dist/api/client.d.ts.map +1 -1
  3. package/dist/api/client.js +7 -1
  4. package/dist/api/client.js.map +1 -1
  5. package/dist/client-adapters/codex-adapter.d.ts +49 -0
  6. package/dist/client-adapters/codex-adapter.d.ts.map +1 -0
  7. package/dist/client-adapters/codex-adapter.js +72 -0
  8. package/dist/client-adapters/codex-adapter.js.map +1 -0
  9. package/dist/client-adapters/cursor-adapter.d.ts +37 -0
  10. package/dist/client-adapters/cursor-adapter.d.ts.map +1 -0
  11. package/dist/client-adapters/cursor-adapter.js +68 -0
  12. package/dist/client-adapters/cursor-adapter.js.map +1 -0
  13. package/dist/client-adapters/index.d.ts +91 -0
  14. package/dist/client-adapters/index.d.ts.map +1 -0
  15. package/dist/client-adapters/index.js +49 -0
  16. package/dist/client-adapters/index.js.map +1 -0
  17. package/dist/config/index.d.ts +19 -1
  18. package/dist/config/index.d.ts.map +1 -1
  19. package/dist/config/index.js +12 -2
  20. package/dist/config/index.js.map +1 -1
  21. package/dist/prompts/manager.d.ts +73 -0
  22. package/dist/prompts/manager.d.ts.map +1 -1
  23. package/dist/prompts/manager.js +243 -7
  24. package/dist/prompts/manager.js.map +1 -1
  25. package/dist/server/http.d.ts +10 -6
  26. package/dist/server/http.d.ts.map +1 -1
  27. package/dist/server/http.js +130 -89
  28. package/dist/server/http.js.map +1 -1
  29. package/dist/server/streamable-http.d.ts +28 -0
  30. package/dist/server/streamable-http.d.ts.map +1 -0
  31. package/dist/server/streamable-http.js +147 -0
  32. package/dist/server/streamable-http.js.map +1 -0
  33. package/dist/server.d.ts +8 -1
  34. package/dist/server.d.ts.map +1 -1
  35. package/dist/server.js +60 -96
  36. package/dist/server.js.map +1 -1
  37. package/dist/telemetry/manager.d.ts +5 -0
  38. package/dist/telemetry/manager.d.ts.map +1 -1
  39. package/dist/telemetry/manager.js +5 -0
  40. package/dist/telemetry/manager.js.map +1 -1
  41. package/dist/tools/manage-subscription.d.ts.map +1 -1
  42. package/dist/tools/manage-subscription.js +162 -20
  43. package/dist/tools/manage-subscription.js.map +1 -1
  44. package/dist/tools/policy-generator.d.ts +31 -0
  45. package/dist/tools/policy-generator.d.ts.map +1 -0
  46. package/dist/tools/policy-generator.js +53 -0
  47. package/dist/tools/policy-generator.js.map +1 -0
  48. package/dist/tools/query-usage-stats.d.ts +8 -0
  49. package/dist/tools/query-usage-stats.d.ts.map +1 -1
  50. package/dist/tools/query-usage-stats.js +26 -1
  51. package/dist/tools/query-usage-stats.js.map +1 -1
  52. package/dist/tools/resolve-prompt-content.d.ts.map +1 -1
  53. package/dist/tools/resolve-prompt-content.js +39 -1
  54. package/dist/tools/resolve-prompt-content.js.map +1 -1
  55. package/dist/tools/search-resources.d.ts +5 -0
  56. package/dist/tools/search-resources.d.ts.map +1 -1
  57. package/dist/tools/search-resources.js +73 -7
  58. package/dist/tools/search-resources.js.map +1 -1
  59. package/dist/tools/sync-resources.d.ts.map +1 -1
  60. package/dist/tools/sync-resources.js +284 -99
  61. package/dist/tools/sync-resources.js.map +1 -1
  62. package/dist/tools/uninstall-resource.d.ts.map +1 -1
  63. package/dist/tools/uninstall-resource.js +100 -12
  64. package/dist/tools/uninstall-resource.js.map +1 -1
  65. package/dist/types/tools.d.ts +128 -1
  66. package/dist/types/tools.d.ts.map +1 -1
  67. package/dist/utils/codex-paths.d.ts +39 -0
  68. package/dist/utils/codex-paths.d.ts.map +1 -0
  69. package/dist/utils/codex-paths.js +56 -0
  70. package/dist/utils/codex-paths.js.map +1 -0
  71. package/package.json +1 -1
  72. package/dist/transport/sse.d.ts +0 -29
  73. package/dist/transport/sse.d.ts.map +0 -1
  74. package/dist/transport/sse.js +0 -271
  75. package/dist/transport/sse.js.map +0 -1
@@ -62,6 +62,9 @@ const errors_1 = require("../types/errors");
62
62
  const index_js_1 = require("../telemetry/index.js");
63
63
  const index_js_2 = require("../prompts/index.js");
64
64
  const md_reference_expander_js_1 = require("../utils/md-reference-expander.js");
65
+ const index_js_3 = require("../client-adapters/index.js");
66
+ const policy_generator_js_1 = require("./policy-generator.js");
67
+ const index_js_4 = require("../config/index.js");
65
68
  const downloadCache = new Map();
66
69
  function syncCacheKey(userToken, resourceId) {
67
70
  return `${userToken}::${resourceId}`;
@@ -139,6 +142,36 @@ function encodeForRender(filePath, rawContent, priorEncoding) {
139
142
  encoding: 'base64',
140
143
  };
141
144
  }
145
+ function codexMcpUrl(url) {
146
+ // Codex consumes Streamable HTTP MCP endpoints. Server-side MCP resource
147
+ // packages may still ship Cursor/SSE URLs for backward compatibility.
148
+ return url.replace(/\/sse\/?$/, '/mcp');
149
+ }
150
+ function toCodexMcpTomlEntry(entry) {
151
+ const converted = { ...entry };
152
+ if (typeof converted.url === 'string') {
153
+ converted.url = codexMcpUrl(converted.url);
154
+ }
155
+ if (converted.headers && !converted.http_headers) {
156
+ converted.http_headers = converted.headers;
157
+ delete converted.headers;
158
+ }
159
+ // Codex infers HTTP MCP transport from `url`; keeping Cursor's `sse`
160
+ // transport marker in config.toml makes the generated config misleading.
161
+ if (converted.transport === 'sse' || converted.transport === 'streamable_http' || converted.transport === 'streamable-http') {
162
+ delete converted.transport;
163
+ }
164
+ return converted;
165
+ }
166
+ function queueCodexMcpTomlAction(localActions, tomlPath, serverName, entry) {
167
+ localActions.push({
168
+ action: 'merge_toml',
169
+ toml_path: tomlPath,
170
+ key: `mcp_servers.${serverName}`,
171
+ value: toCodexMcpTomlEntry(entry),
172
+ overwrite: false,
173
+ });
174
+ }
142
175
  /**
143
176
  * Same as encodeForRender, but produces the field shape required by
144
177
  * CheckFileAction (`expected_content` + a sibling encoding hint stored as
@@ -152,6 +185,52 @@ function encodeForCheck(filePath, rawContent, priorEncoding) {
152
185
  expected_content_encoding: enc.encoding,
153
186
  };
154
187
  }
188
+ async function loadPromptResourceFiles(resourceId, resourceName, resourceType, userToken) {
189
+ const downloadResult = await client_1.apiClient.downloadResource(resourceId, userToken);
190
+ if (downloadResult.files.length > 0) {
191
+ return downloadResult.files;
192
+ }
193
+ return multi_source_manager_1.multiSourceGitManager.readResourceFiles(resourceName, resourceType);
194
+ }
195
+ function getPrimaryPromptFile(sourceFiles, resourceName, isSkill) {
196
+ return isSkill
197
+ ? (sourceFiles.find((f) => path.basename(f.path) === 'SKILL.md') ??
198
+ sourceFiles.find((f) => f.path.endsWith('.md')) ??
199
+ sourceFiles[0])
200
+ : (sourceFiles.find((f) => path.basename(f.path).replace(/\.md$/, '') === resourceName) ??
201
+ sourceFiles.find((f) => f.path.endsWith('.md')) ??
202
+ sourceFiles[0]);
203
+ }
204
+ function getLocalScriptFiles(sourceFiles) {
205
+ return sourceFiles.filter(f => !f.path.endsWith('.md') &&
206
+ f.path !== 'SKILL.md' &&
207
+ !f.path.endsWith('/SKILL.md'));
208
+ }
209
+ function queueComplexSkillCheckActions(localActions, clientAdapter, resourceId, resourceName, scriptFiles, manifestContent) {
210
+ if (scriptFiles.length === 0) {
211
+ return 0;
212
+ }
213
+ const skillDir = clientAdapter.getSkillDir(resourceName);
214
+ for (const scriptFile of scriptFiles) {
215
+ localActions.push({
216
+ action: 'check_file',
217
+ path: `${skillDir}/${scriptFile.path}`,
218
+ ...encodeForCheck(scriptFile.path, scriptFile.content, scriptFile.encoding),
219
+ resource_id: resourceId,
220
+ resource_name: resourceName,
221
+ resource_type: 'skill',
222
+ });
223
+ }
224
+ localActions.push({
225
+ action: 'check_file',
226
+ path: `${clientAdapter.getManifestDir()}/${resourceName}.md`,
227
+ ...encodeForCheck('SKILL.md', manifestContent),
228
+ resource_id: resourceId,
229
+ resource_name: resourceName,
230
+ resource_type: 'skill',
231
+ });
232
+ return scriptFiles.length + 1;
233
+ }
155
234
  async function syncResources(params) {
156
235
  const startTime = Date.now();
157
236
  const typedParams = params;
@@ -171,6 +250,13 @@ async function syncResources(params) {
171
250
  ? new Set(typedParams.resource_ids)
172
251
  : null;
173
252
  const confirmedFullSync = typedParams._confirmed_full_sync === true;
253
+ // Resolve client adapter: prefer the caller-supplied agent_profile, fall
254
+ // back to the server-wide config.agentProfile (set via CSP_AGENT_PROFILE).
255
+ const resolvedProfile = typedParams.agent_profile ?? index_js_4.config.agentProfile ?? 'cursor';
256
+ const clientAdapter = index_js_3.adapterRegistry.get(resolvedProfile);
257
+ // Collect per-rule content for Codex policy aggregation (stage 4).
258
+ // Each entry is raw rule markdown to be merged into csp-routing-policy.md.
259
+ const codexRuleContents = [];
174
260
  (0, logger_1.logToolStep)('sync_resources', 'Parameters validated', {
175
261
  mode,
176
262
  scope,
@@ -208,16 +294,21 @@ async function syncResources(params) {
208
294
  (0, logger_1.logToolStep)('sync_resources', 'Step 1: Fetching subscriptions from API', { scope, types });
209
295
  const t1 = Date.now();
210
296
  const allSubscriptions = await client_1.apiClient.getSubscriptions({ types }, userToken);
297
+ const visibleAllSubscriptions = {
298
+ ...allSubscriptions,
299
+ subscriptions: index_js_2.promptManager.filterSuppressedSubscriptions(userToken ?? '', allSubscriptions.subscriptions),
300
+ };
301
+ visibleAllSubscriptions.total = visibleAllSubscriptions.subscriptions.length;
211
302
  // Apply resource_ids filter if provided — only process the specified resources.
212
303
  // This is done client-side: the subscription list API has no resource_ids filter,
213
304
  // but downloadResource(id) is already a single-resource endpoint so per-resource
214
305
  // processing is efficient regardless.
215
306
  const subscriptions = resourceIds
216
307
  ? {
217
- total: allSubscriptions.subscriptions.filter(s => resourceIds.has(s.id)).length,
218
- subscriptions: allSubscriptions.subscriptions.filter(s => resourceIds.has(s.id)),
308
+ total: visibleAllSubscriptions.subscriptions.filter(s => resourceIds.has(s.id)).length,
309
+ subscriptions: visibleAllSubscriptions.subscriptions.filter(s => resourceIds.has(s.id)),
219
310
  }
220
- : allSubscriptions;
311
+ : visibleAllSubscriptions;
221
312
  (0, logger_1.logToolStep)('sync_resources', 'Subscriptions fetched', {
222
313
  totalFromApi: allSubscriptions.total,
223
314
  afterFilter: subscriptions.total,
@@ -296,47 +387,34 @@ async function syncResources(params) {
296
387
  };
297
388
  const isRegistered = index_js_2.promptManager.has(index_js_2.promptManager.buildPromptName(meta), userToken ?? '');
298
389
  if (isRegistered) {
299
- tally.cached++;
300
- details.push({ id: sub.id, name: sub.name, action: 'cached', version: resourceVersion });
301
- // ── ADDITIONAL: Check if skill has local scripts in .csp-ai-agent ────
302
- // Complex skills store scripts in ~/.csp-ai-agent/skills/<name>/
303
- // We need to verify those are also up-to-date
390
+ let checkAction = 'cached';
391
+ // Complex skills need local script and manifest checks in the
392
+ // active client subtree. Use the same API/Git source-file path
393
+ // as incremental sync; Git metadata alone misses API-backed
394
+ // skills such as zoom-build and can falsely report "cached".
304
395
  if (sub.type === 'skill') {
305
396
  try {
306
- const metadata = await multi_source_manager_1.multiSourceGitManager.scanResourceMetadata(sub.name, sub.type);
307
- if (metadata.has_scripts && metadata.script_files) {
308
- (0, logger_1.logToolStep)('sync_resources', 'Skill has scripts — generating script check actions', {
397
+ const sourceFiles = await loadPromptResourceFiles(sub.id, sub.name, sub.type, userToken);
398
+ const scriptFiles = getLocalScriptFiles(sourceFiles);
399
+ const primaryFile = getPrimaryPromptFile(sourceFiles, sub.name, true);
400
+ const actionCount = queueComplexSkillCheckActions(localActions, clientAdapter, sub.id, sub.name, scriptFiles, primaryFile?.content ?? '');
401
+ if (actionCount > 0) {
402
+ checkAction = 'failed';
403
+ (0, logger_1.logToolStep)('sync_resources', 'Complex skill check actions queued for AI Agent', {
309
404
  resourceId: sub.id,
310
- scriptCount: metadata.script_files.length,
311
- });
312
- // Check each script file in .csp-ai-agent
313
- const skillDir = `${(0, cursor_paths_1.getCspAgentDirForClient)('skills')}/${sub.name}`;
314
- for (const scriptFile of metadata.script_files) {
315
- localActions.push({
316
- action: 'check_file',
317
- path: `${skillDir}/${scriptFile.relative_path}`,
318
- ...encodeForCheck(scriptFile.relative_path, scriptFile.content, scriptFile.encoding),
319
- resource_id: sub.id,
320
- resource_name: sub.name,
321
- resource_type: sub.type,
322
- });
323
- }
324
- // Also check the manifest file
325
- const proxyScript = metadata.script_files[0];
326
- localActions.push({
327
- action: 'check_file',
328
- path: `${(0, cursor_paths_1.getCspAgentRootDirForClient)()}/.manifests/${sub.name}.md`,
329
- ...encodeForCheck(proxyScript?.relative_path ?? '', proxyScript?.content ?? '', proxyScript?.encoding), // Use first script as proxy
330
- resource_id: sub.id,
331
- resource_name: sub.name,
332
- resource_type: sub.type,
405
+ scriptCount: scriptFiles.length,
406
+ manifestPath: `${clientAdapter.getManifestDir()}/${sub.name}.md`,
407
+ actionCount,
333
408
  });
334
409
  }
335
410
  }
336
- catch {
337
- // No scripts or scan failed — skip script check
411
+ catch (err) {
412
+ checkAction = 'failed';
413
+ logger_1.logger.warn({ resourceId: sub.id, resourceName: sub.name, error: err.message }, 'Failed to prepare complex skill local checks');
338
414
  }
339
415
  }
416
+ tally[checkAction]++;
417
+ details.push({ id: sub.id, name: sub.name, action: checkAction, version: resourceVersion });
340
418
  }
341
419
  else {
342
420
  tally.failed++;
@@ -427,12 +505,14 @@ async function syncResources(params) {
427
505
  });
428
506
  }
429
507
  }
430
- // Placeholder: AI will update this after executing check actions
431
- tally.cached++;
508
+ // Server cannot access the user's local filesystem. Once
509
+ // check_file actions are queued, report the resource as failed
510
+ // until the Agent executes those checks and observes matches.
511
+ tally.failed++;
432
512
  details.push({
433
513
  id: sub.id,
434
514
  name: sub.name,
435
- action: 'cached',
515
+ action: 'failed',
436
516
  version: resourceVersion,
437
517
  });
438
518
  (0, logger_1.logToolStep)('sync_resources', 'Check actions queued for AI Agent', {
@@ -484,13 +564,7 @@ async function syncResources(params) {
484
564
  // - command: prefer the file whose name matches the resource name
485
565
  // - fallback: first .md file, then first file of any type
486
566
  const isSkill = sub.type === 'skill';
487
- const primaryFile = isSkill
488
- ? (sourceFiles.find((f) => path.basename(f.path) === 'SKILL.md') ??
489
- sourceFiles.find((f) => f.path.endsWith('.md')) ??
490
- sourceFiles[0])
491
- : (sourceFiles.find((f) => path.basename(f.path).replace(/\.md$/, '') === sub.name) ??
492
- sourceFiles.find((f) => f.path.endsWith('.md')) ??
493
- sourceFiles[0]);
567
+ const primaryFile = getPrimaryPromptFile(sourceFiles, sub.name, isSkill);
494
568
  const rawContent = primaryFile?.content ?? '';
495
569
  // Extract description from frontmatter (---\ndescription: ...\n---)
496
570
  // falling back to the subscription's description field or resource name.
@@ -527,9 +601,7 @@ async function syncResources(params) {
527
601
  // WHY: zoom-build and other complex skills are NOT in git but ARE in API response
528
602
  try {
529
603
  // Filter out markdown files from sourceFiles to identify scripts
530
- const scriptFiles = sourceFiles.filter(f => !f.path.endsWith('.md') &&
531
- f.path !== 'SKILL.md' &&
532
- !f.path.endsWith('/SKILL.md'));
604
+ const scriptFiles = getLocalScriptFiles(sourceFiles);
533
605
  if (scriptFiles.length > 0) {
534
606
  // Complex skill detected via API download
535
607
  (0, logger_1.logToolStep)('sync_resources', 'Complex skill detected (via API) — generating local actions', {
@@ -537,10 +609,15 @@ async function syncResources(params) {
537
609
  scriptCount: scriptFiles.length,
538
610
  source: 'API',
539
611
  });
540
- // Use isolated directory for complex skills (not ~/.cursor/skills/)
541
- // SKILL.md is NOT downloaded — only scripts are cached locally
542
- // This prevents Cursor from auto-discovering the skill while enabling script execution
543
- const skillDir = `${(0, cursor_paths_1.getCspAgentDirForClient)('skills')}/${sub.name}`;
612
+ // Use client-adapter-resolved directory so Cursor and Codex each get
613
+ // their own isolated subtree:
614
+ // Cursor ~/.csp-ai-agent/skills/<name>/
615
+ // Codex → ~/.csp-ai-agent/codex/skills/<name>/
616
+ // SKILL.md is NOT downloaded — only scripts are cached locally.
617
+ // This prevents the client from auto-discovering the skill while
618
+ // enabling script execution.
619
+ const skillDir = clientAdapter.getSkillDir(sub.name);
620
+ const manifestPath = `${clientAdapter.getManifestDir()}/${sub.name}.md`;
544
621
  // Generate write_file actions for script files ONLY (exclude SKILL.md)
545
622
  // First script file carries is_skill_manifest marker for atomic update check
546
623
  // 1. First script file (with manifest check marker)
@@ -553,9 +630,10 @@ async function syncResources(params) {
553
630
  action: 'write_file',
554
631
  path: `${skillDir}/${firstScript.path}`,
555
632
  ...encodeForRender(firstScript.path, firstScript.content),
556
- mode: firstScript.path.includes('/scripts/') ? '0755' : undefined,
633
+ mode: firstScript.path.includes('scripts/') ? '0755' : undefined,
557
634
  // Atomic update marker: client checks manifest FIRST
558
635
  is_skill_manifest: true,
636
+ manifest_path: manifestPath,
559
637
  // SKILL.md content for version comparison (stored separately in .manifests/)
560
638
  skill_manifest_content: Buffer.from(rawContent, "utf8").toString("base64"),
561
639
  });
@@ -569,7 +647,7 @@ async function syncResources(params) {
569
647
  action: 'write_file',
570
648
  path: `${skillDir}/${scriptFile.path}`,
571
649
  ...encodeForRender(scriptFile.path, scriptFile.content),
572
- mode: scriptFile.path.includes('/scripts/') ? '0755' : undefined,
650
+ mode: scriptFile.path.includes('scripts/') ? '0755' : undefined,
573
651
  });
574
652
  }
575
653
  (0, logger_1.logToolStep)('sync_resources', 'Script files added to local_actions_required (SKILL.md excluded)', {
@@ -588,7 +666,9 @@ async function syncResources(params) {
588
666
  scriptCount: metadata.script_files.length,
589
667
  source: 'Git',
590
668
  });
591
- const skillDir = `${(0, cursor_paths_1.getCspAgentDirForClient)('skills')}/${sub.name}`;
669
+ // Use client-adapter-resolved directory (Cursor vs Codex subtree).
670
+ const skillDir = clientAdapter.getSkillDir(sub.name);
671
+ const manifestPath = `${clientAdapter.getManifestDir()}/${sub.name}.md`;
592
672
  if (metadata.script_files.length > 0) {
593
673
  const firstScript = metadata.script_files[0];
594
674
  if (!firstScript) {
@@ -599,8 +679,9 @@ async function syncResources(params) {
599
679
  action: 'write_file',
600
680
  path: `${skillDir}/${firstScript.relative_path}`,
601
681
  ...encodeForRender(firstScript.relative_path, firstScript.content, firstScript.encoding),
602
- mode: firstScript.mode,
682
+ mode: firstScript.mode ?? (firstScript.relative_path.includes('scripts/') ? '0755' : undefined),
603
683
  is_skill_manifest: true,
684
+ manifest_path: manifestPath,
604
685
  skill_manifest_content: Buffer.from(rawContent, "utf8").toString("base64"),
605
686
  });
606
687
  }
@@ -612,7 +693,7 @@ async function syncResources(params) {
612
693
  action: 'write_file',
613
694
  path: `${skillDir}/${scriptFile.relative_path}`,
614
695
  ...encodeForRender(scriptFile.relative_path, scriptFile.content, scriptFile.encoding),
615
- mode: scriptFile.mode,
696
+ mode: scriptFile.mode ?? (scriptFile.relative_path.includes('scripts/') ? '0755' : undefined),
616
697
  });
617
698
  }
618
699
  }
@@ -736,8 +817,10 @@ async function syncResources(params) {
736
817
  // not on this (possibly remote Linux) server.
737
818
  if (sub.type === 'mcp') {
738
819
  const mcpConfigFile = resourceFiles.find((f) => path.basename(f.path) === 'mcp-config.json');
739
- // ~/.cursor/mcp.json on the user's machine
740
- const mcpJsonPath = `${(0, cursor_paths_1.getCursorRootDirForClient)()}/mcp.json`;
820
+ // Config path differs per client: mcp.json (Cursor) vs config.toml (Codex)
821
+ const mcpJsonPath = resolvedProfile === 'codex'
822
+ ? clientAdapter.getMcpConfigPath() // ~/.codex/config.toml
823
+ : `${(0, cursor_paths_1.getCursorRootDirForClient)()}/mcp.json`;
741
824
  // ── Optimization: skip if already configured (incremental mode only) ────
742
825
  // In incremental mode, if the AI Agent reports this MCP server is already
743
826
  // in ~/.cursor/mcp.json, skip downloading and generating write_file actions.
@@ -785,7 +868,33 @@ async function syncResources(params) {
785
868
  catch {
786
869
  logger_1.logger.warn({ resourceId: sub.id, resourceName: sub.name }, 'sync_resources: failed to parse mcp-config.json — treating as empty config');
787
870
  }
788
- if (typeof cfg['command'] === 'string') {
871
+ if (resolvedProfile === 'codex') {
872
+ // ── Codex MCP: inject server entry into config.toml via merge_toml ──
873
+ // The Codex CLI reads MCP server configuration from ~/.codex/config.toml.
874
+ // Format A becomes [mcp_servers.<name>]. Format B emits one
875
+ // table per remote server entry. URL entries are normalised from
876
+ // Cursor/SSE (`/sse`) to Codex Streamable HTTP (`/mcp`).
877
+ const tomlPath = clientAdapter.getMcpConfigPath();
878
+ const queuedServers = [];
879
+ if (typeof cfg['command'] === 'string') {
880
+ const serverName = cfg['name'] ?? sub.name;
881
+ queueCodexMcpTomlAction(localActions, tomlPath, serverName, cfg);
882
+ queuedServers.push(serverName);
883
+ }
884
+ else {
885
+ for (const [serverName, entry] of Object.entries(cfg)) {
886
+ if (!entry || typeof entry !== 'object' || Array.isArray(entry)) {
887
+ logger_1.logger.warn({ resourceId: sub.id, resourceName: sub.name, serverName }, 'sync_resources: skipping invalid Codex MCP config entry');
888
+ continue;
889
+ }
890
+ queueCodexMcpTomlAction(localActions, tomlPath, serverName, entry);
891
+ queuedServers.push(serverName);
892
+ }
893
+ }
894
+ logger_1.logger.info({ resourceId: sub.id, resourceName: sub.name, serverKeys: queuedServers, tomlPath, profile: 'codex' }, 'sync_resources: Codex MCP — merge_toml action queued');
895
+ (0, logger_1.logToolStep)('sync_resources', 'Codex MCP: merge_toml queued', { resourceId: sub.id, serverKeys: queuedServers });
896
+ }
897
+ else if (typeof cfg['command'] === 'string') {
789
898
  // ── Format A: local executable ──────────────────────────────────
790
899
  const installDir = `${(0, cursor_paths_1.getCursorTypeDirForClient)('mcp')}/${sub.name}`;
791
900
  const writeActions = [];
@@ -867,43 +976,51 @@ async function syncResources(params) {
867
976
  }
868
977
  else {
869
978
  // No mcp-config.json: heuristic fallback
870
- const installDir = `${(0, cursor_paths_1.getCursorTypeDirForClient)('mcp')}/${sub.name}`;
871
- const writeActions = [];
872
- for (const file of resourceFiles) {
873
- const normalised = path.normalize(file.path);
874
- if (normalised.startsWith('..'))
875
- continue;
876
- const fileDest = `${installDir}/${normalised}`;
979
+ if (resolvedProfile === 'codex') {
980
+ // Codex: emit a bare merge_toml with minimal entry
981
+ const tomlPath = clientAdapter.getMcpConfigPath();
982
+ queueCodexMcpTomlAction(localActions, tomlPath, sub.name, { command: 'node', args: [] });
983
+ logger_1.logger.info({ resourceId: sub.id, resourceName: sub.name, tomlPath, profile: 'codex' }, 'sync_resources: Codex MCP heuristic — merge_toml action queued (no mcp-config.json)');
984
+ }
985
+ else {
986
+ const installDir = `${(0, cursor_paths_1.getCursorTypeDirForClient)('mcp')}/${sub.name}`;
987
+ const writeActions = [];
988
+ for (const file of resourceFiles) {
989
+ const normalised = path.normalize(file.path);
990
+ if (normalised.startsWith('..'))
991
+ continue;
992
+ const fileDest = `${installDir}/${normalised}`;
993
+ localActions.push({
994
+ action: 'write_file',
995
+ path: fileDest,
996
+ ...encodeForRender(file.path, file.content),
997
+ });
998
+ writeActions.push(fileDest);
999
+ }
1000
+ const jsEntry = resourceFiles.find((f) => f.path.endsWith('.js'));
1001
+ const pyEntry = resourceFiles.find((f) => f.path.endsWith('.py'));
1002
+ const entryFile = jsEntry ?? pyEntry ?? resourceFiles[0];
1003
+ const cmd = jsEntry ? 'node' : 'python3';
1004
+ const entryPath = `${installDir}/${entryFile?.path ?? ''}`;
877
1005
  localActions.push({
878
- action: 'write_file',
879
- path: fileDest,
880
- ...encodeForRender(file.path, file.content),
1006
+ action: 'merge_mcp_json',
1007
+ mcp_json_path: mcpJsonPath,
1008
+ server_name: sub.name,
1009
+ entry: { command: cmd, args: [entryPath] },
1010
+ skip_if_exists: true,
881
1011
  });
882
- writeActions.push(fileDest);
1012
+ logger_1.logger.info({
1013
+ resourceId: sub.id,
1014
+ resourceName: sub.name,
1015
+ format: 'heuristic',
1016
+ installDir,
1017
+ mcpJsonPath,
1018
+ cmd,
1019
+ entryPath,
1020
+ writeFiles: writeActions,
1021
+ }, 'sync_resources: MCP heuristic fallback — write_file + merge_mcp_json actions queued');
1022
+ (0, logger_1.logToolStep)('sync_resources', 'MCP heuristic fallback: write_file + merge_mcp_json queued', { resourceId: sub.id });
883
1023
  }
884
- const jsEntry = resourceFiles.find((f) => f.path.endsWith('.js'));
885
- const pyEntry = resourceFiles.find((f) => f.path.endsWith('.py'));
886
- const entryFile = jsEntry ?? pyEntry ?? resourceFiles[0];
887
- const cmd = jsEntry ? 'node' : 'python3';
888
- const entryPath = `${installDir}/${entryFile?.path ?? ''}`;
889
- localActions.push({
890
- action: 'merge_mcp_json',
891
- mcp_json_path: mcpJsonPath,
892
- server_name: sub.name,
893
- entry: { command: cmd, args: [entryPath] },
894
- skip_if_exists: true,
895
- });
896
- logger_1.logger.info({
897
- resourceId: sub.id,
898
- resourceName: sub.name,
899
- format: 'heuristic',
900
- installDir,
901
- mcpJsonPath,
902
- cmd,
903
- entryPath,
904
- writeFiles: writeActions,
905
- }, 'sync_resources: MCP heuristic fallback — write_file + merge_mcp_json actions queued');
906
- (0, logger_1.logToolStep)('sync_resources', 'MCP heuristic fallback: write_file + merge_mcp_json queued', { resourceId: sub.id });
907
1024
  }
908
1025
  tally.synced++;
909
1026
  details.push({ id: sub.id, name: sub.name, action: 'synced', version: resourceVersion });
@@ -917,11 +1034,25 @@ async function syncResources(params) {
917
1034
  // or has different content, the AI writes it unconditionally, which also
918
1035
  // recovers files that were accidentally deleted by the user.
919
1036
  //
920
- // DUAL-LAYER STRATEGY (v1.6):
1037
+ // DUAL-LAYER STRATEGY (v1.6, Cursor only):
921
1038
  // - scope='global' → write to ~/.cursor/rules/ only (macOS support)
922
1039
  // - scope='workspace' → write to ${WORKSPACE}/.cursor/rules/ only (Windows support)
923
1040
  // - scope='all' → write to BOTH locations (maximum compatibility)
1041
+ //
1042
+ // CODEX STRATEGY: rules are aggregated into csp-routing-policy.md and injected
1043
+ // via developer_instructions in config.toml (no .mdc files written to disk).
924
1044
  if (sub.type === 'rule') {
1045
+ if (resolvedProfile === 'codex') {
1046
+ // Collect rule content for policy aggregation; no individual file actions.
1047
+ for (const file of resourceFiles) {
1048
+ codexRuleContents.push({ name: sub.name, content: file.content });
1049
+ }
1050
+ logger_1.logger.info({ resourceId: sub.id, resourceName: sub.name, profile: 'codex' }, 'sync_resources: Rule collected for Codex policy aggregation');
1051
+ tally.synced++;
1052
+ details.push({ id: sub.id, name: sub.name, action: 'synced', version: resourceVersion });
1053
+ continue;
1054
+ }
1055
+ // ── Cursor: dual-layer .mdc file strategy (unchanged) ──────────────
925
1056
  const writeActions = [];
926
1057
  // Determine target directories based on scope parameter
927
1058
  const targetDirs = [];
@@ -1000,17 +1131,71 @@ async function syncResources(params) {
1000
1131
  else if (resourceIds) {
1001
1132
  logger_1.logger.info({ resourceIdsFilter: [...resourceIds], expectedPromptCount: expectedPromptNames.size }, 'sync_resources: skipping pruneStalePrompts — resource_ids filter active (partial sync)');
1002
1133
  }
1003
- // ── Step 5: Health score ───────────────────────────────────────────────
1134
+ // ── Step 5 (Codex): Aggregate collected rules into policy actions ──────
1135
+ // If any rule content was collected during this sync run, materialise the
1136
+ // csp-routing-policy.md file and emit a merge_toml action to inject it
1137
+ // into developer_instructions in ~/.codex/config.toml.
1138
+ let restartRequired = false;
1139
+ let restartHint;
1140
+ if (resolvedProfile === 'codex' && codexRuleContents.length > 0) {
1141
+ try {
1142
+ const policyStrategy = clientAdapter.getPolicyStrategy();
1143
+ const policyContent = (0, policy_generator_js_1.generatePolicyContent)(codexRuleContents);
1144
+ const policyFile = policyStrategy.policyFile ?? '~/.csp-ai-agent/codex/csp-routing-policy.md';
1145
+ const tomlPath = policyStrategy.configTomlPath ?? '~/.codex/config.toml';
1146
+ const tomlKey = policyStrategy.configTomlKey ?? 'developer_instructions';
1147
+ // 1. Write the policy markdown file
1148
+ localActions.push({
1149
+ action: 'write_file',
1150
+ path: policyFile,
1151
+ content: policyContent,
1152
+ encoding: 'utf8',
1153
+ });
1154
+ // 2. Inject the policy file reference into developer_instructions.
1155
+ // overwrite: false keeps this action idempotent after the first
1156
+ // successful setup, so restart hints do not force a re-apply loop.
1157
+ localActions.push({
1158
+ action: 'merge_toml',
1159
+ toml_path: tomlPath,
1160
+ key: tomlKey,
1161
+ value: `Please read and follow the CSP routing policy at: ${policyFile}`,
1162
+ overwrite: false,
1163
+ });
1164
+ restartRequired = true;
1165
+ restartHint = `CSP routing policy has been updated. Please restart Codex for the policy to take effect: codex`;
1166
+ logger_1.logger.info({ profile: 'codex', policyFile, tomlPath, ruleCount: codexRuleContents.length }, 'sync_resources: Codex policy materialised — merge_toml action queued');
1167
+ }
1168
+ catch (err) {
1169
+ logger_1.logger.error({ error: err instanceof Error ? err.message : String(err) }, 'sync_resources: Failed to generate Codex policy');
1170
+ }
1171
+ }
1172
+ // ── Step 6: Health score ───────────────────────────────────────────────
1004
1173
  const healthScore = tally.total > 0
1005
1174
  ? Math.round(((tally.synced + tally.cached + tally.skipped) / tally.total) * 100)
1006
1175
  : 100;
1176
+ const pendingSetup = localActions.flatMap((action) => {
1177
+ if (action.action !== 'merge_mcp_json' || !action.missing_env || action.missing_env.length === 0) {
1178
+ return [];
1179
+ }
1180
+ return [{
1181
+ server_name: action.server_name,
1182
+ mcp_json_path: action.mcp_json_path,
1183
+ missing_env: action.missing_env,
1184
+ command_needs_verification: typeof action.entry.command === 'string',
1185
+ command: typeof action.entry.command === 'string' ? action.entry.command : '',
1186
+ setup_hint: action.setup_hint ?? `Fill in env vars for MCP server "${action.server_name}".`,
1187
+ ...(action.setup_doc ? { setup_doc: action.setup_doc } : {}),
1188
+ }];
1189
+ });
1007
1190
  const result = {
1008
1191
  mode,
1009
1192
  health_score: healthScore,
1010
1193
  summary: tally,
1011
1194
  details,
1012
1195
  ...(skippedResources.length > 0 ? { skipped_resources: skippedResources } : {}),
1196
+ ...(pendingSetup.length > 0 ? { pending_setup: pendingSetup } : {}),
1013
1197
  ...(localActions.length > 0 ? { local_actions_required: localActions } : {}),
1198
+ ...(restartRequired ? { restart_required: true, restart_hint: restartHint } : {}),
1014
1199
  };
1015
1200
  const duration = Date.now() - startTime;
1016
1201
  (0, logger_1.logToolCall)('sync_resources', 'user-id', params, duration);
@@ -1134,7 +1319,7 @@ exports.syncResourcesTool = {
1134
1319
  'For write_file actions: ' +
1135
1320
  ' SPECIAL HANDLING FOR COMPLEX SKILLS (when action has `is_skill_manifest: true`): ' +
1136
1321
  ' (1) This action is the FIRST script file — check skill version using manifest FIRST. ' +
1137
- ' (2) Read manifest file at {CSP_AGENT_ROOT}/.manifests/<skill-name>.md (if exists). ' +
1322
+ ' (2) Read manifest file at action.manifest_path when present; otherwise read {CSP_AGENT_ROOT}/.manifests/<skill-name>.md. ' +
1138
1323
  ' Where {CSP_AGENT_ROOT} is a SIBLING of {CURSOR_ROOT} (same parent directory). ' +
1139
1324
  ' (3) action.skill_manifest_content is ALWAYS base64-encoded — first decode it: ' +
1140
1325
  ' decodedManifest = Buffer.from(action.skill_manifest_content, "base64").toString("utf8") ' +
@@ -1143,7 +1328,7 @@ exports.syncResourcesTool = {
1143
1328
  ' (5) If different or manifest missing: ' +
1144
1329
  ' - PROCEED with this action and all subsequent write_file actions for this skill. ' +
1145
1330
  ' - After writing all script files, write decodedManifest (the UTF-8 form, NOT the raw base64 string) ' +
1146
- ' to {CSP_AGENT_ROOT}/.manifests/<skill-name>.md ' +
1331
+ ' to action.manifest_path when present, otherwise {CSP_AGENT_ROOT}/.manifests/<skill-name>.md ' +
1147
1332
  ' (6) NOTE: SKILL.md is NEVER written to the skills directory (only to .manifests/) to prevent Cursor auto-discovery. ' +
1148
1333
  ' GENERAL HANDLING (when action has NO `is_skill_manifest` marker): ' +
1149
1334
  ' (1) Read the existing file at `path` (if it exists) using fs.readFile() or equivalent. ' +