@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.
- package/dist/api/client.d.ts +4 -1
- package/dist/api/client.d.ts.map +1 -1
- package/dist/api/client.js +7 -1
- package/dist/api/client.js.map +1 -1
- package/dist/client-adapters/codex-adapter.d.ts +49 -0
- package/dist/client-adapters/codex-adapter.d.ts.map +1 -0
- package/dist/client-adapters/codex-adapter.js +72 -0
- package/dist/client-adapters/codex-adapter.js.map +1 -0
- package/dist/client-adapters/cursor-adapter.d.ts +37 -0
- package/dist/client-adapters/cursor-adapter.d.ts.map +1 -0
- package/dist/client-adapters/cursor-adapter.js +68 -0
- package/dist/client-adapters/cursor-adapter.js.map +1 -0
- package/dist/client-adapters/index.d.ts +91 -0
- package/dist/client-adapters/index.d.ts.map +1 -0
- package/dist/client-adapters/index.js +49 -0
- package/dist/client-adapters/index.js.map +1 -0
- package/dist/config/index.d.ts +19 -1
- package/dist/config/index.d.ts.map +1 -1
- package/dist/config/index.js +12 -2
- package/dist/config/index.js.map +1 -1
- package/dist/prompts/manager.d.ts +73 -0
- package/dist/prompts/manager.d.ts.map +1 -1
- package/dist/prompts/manager.js +243 -7
- package/dist/prompts/manager.js.map +1 -1
- package/dist/server/http.d.ts +10 -6
- package/dist/server/http.d.ts.map +1 -1
- package/dist/server/http.js +130 -89
- package/dist/server/http.js.map +1 -1
- package/dist/server/streamable-http.d.ts +28 -0
- package/dist/server/streamable-http.d.ts.map +1 -0
- package/dist/server/streamable-http.js +147 -0
- package/dist/server/streamable-http.js.map +1 -0
- package/dist/server.d.ts +8 -1
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +60 -96
- package/dist/server.js.map +1 -1
- package/dist/telemetry/manager.d.ts +5 -0
- package/dist/telemetry/manager.d.ts.map +1 -1
- package/dist/telemetry/manager.js +5 -0
- package/dist/telemetry/manager.js.map +1 -1
- package/dist/tools/manage-subscription.d.ts.map +1 -1
- package/dist/tools/manage-subscription.js +162 -20
- package/dist/tools/manage-subscription.js.map +1 -1
- package/dist/tools/policy-generator.d.ts +31 -0
- package/dist/tools/policy-generator.d.ts.map +1 -0
- package/dist/tools/policy-generator.js +53 -0
- package/dist/tools/policy-generator.js.map +1 -0
- package/dist/tools/query-usage-stats.d.ts +8 -0
- package/dist/tools/query-usage-stats.d.ts.map +1 -1
- package/dist/tools/query-usage-stats.js +26 -1
- package/dist/tools/query-usage-stats.js.map +1 -1
- package/dist/tools/resolve-prompt-content.d.ts.map +1 -1
- package/dist/tools/resolve-prompt-content.js +39 -1
- package/dist/tools/resolve-prompt-content.js.map +1 -1
- package/dist/tools/search-resources.d.ts +5 -0
- package/dist/tools/search-resources.d.ts.map +1 -1
- package/dist/tools/search-resources.js +73 -7
- package/dist/tools/search-resources.js.map +1 -1
- package/dist/tools/sync-resources.d.ts.map +1 -1
- package/dist/tools/sync-resources.js +284 -99
- package/dist/tools/sync-resources.js.map +1 -1
- package/dist/tools/uninstall-resource.d.ts.map +1 -1
- package/dist/tools/uninstall-resource.js +100 -12
- package/dist/tools/uninstall-resource.js.map +1 -1
- package/dist/types/tools.d.ts +128 -1
- package/dist/types/tools.d.ts.map +1 -1
- package/dist/utils/codex-paths.d.ts +39 -0
- package/dist/utils/codex-paths.d.ts.map +1 -0
- package/dist/utils/codex-paths.js +56 -0
- package/dist/utils/codex-paths.js.map +1 -0
- package/package.json +1 -1
- package/dist/transport/sse.d.ts +0 -29
- package/dist/transport/sse.d.ts.map +0 -1
- package/dist/transport/sse.js +0 -271
- 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:
|
|
218
|
-
subscriptions:
|
|
308
|
+
total: visibleAllSubscriptions.subscriptions.filter(s => resourceIds.has(s.id)).length,
|
|
309
|
+
subscriptions: visibleAllSubscriptions.subscriptions.filter(s => resourceIds.has(s.id)),
|
|
219
310
|
}
|
|
220
|
-
:
|
|
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
|
-
|
|
300
|
-
|
|
301
|
-
//
|
|
302
|
-
//
|
|
303
|
-
//
|
|
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
|
|
307
|
-
|
|
308
|
-
|
|
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:
|
|
311
|
-
|
|
312
|
-
|
|
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
|
-
|
|
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
|
-
//
|
|
431
|
-
|
|
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: '
|
|
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
|
|
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
|
|
541
|
-
//
|
|
542
|
-
//
|
|
543
|
-
|
|
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('
|
|
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('
|
|
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
|
-
|
|
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
|
-
//
|
|
740
|
-
const mcpJsonPath =
|
|
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 (
|
|
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
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
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: '
|
|
879
|
-
|
|
880
|
-
|
|
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
|
-
|
|
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:
|
|
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
|
|
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. ' +
|