@elliotding/ai-agent-mcp 0.2.20 → 0.2.22
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/client-adapters/codex-adapter.d.ts +48 -0
- package/dist/client-adapters/codex-adapter.d.ts.map +1 -0
- package/dist/client-adapters/codex-adapter.js +69 -0
- package/dist/client-adapters/codex-adapter.js.map +1 -0
- package/dist/client-adapters/cursor-adapter.d.ts +36 -0
- package/dist/client-adapters/cursor-adapter.d.ts.map +1 -0
- package/dist/client-adapters/cursor-adapter.js +65 -0
- package/dist/client-adapters/cursor-adapter.js.map +1 -0
- package/dist/client-adapters/index.d.ts +89 -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 +18 -0
- package/dist/prompts/manager.d.ts.map +1 -1
- package/dist/prompts/manager.js +51 -5
- 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 +83 -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 +126 -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 +33 -2
- 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/resolve-prompt-content.d.ts.map +1 -1
- package/dist/tools/resolve-prompt-content.js +14 -1
- package/dist/tools/resolve-prompt-content.js.map +1 -1
- package/dist/tools/search-resources.d.ts.map +1 -1
- package/dist/tools/search-resources.js +24 -0
- 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 +232 -58
- 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 +21 -3
- package/dist/tools/uninstall-resource.js.map +1 -1
- package/dist/types/tools.d.ts +113 -4
- package/dist/types/tools.d.ts.map +1 -1
- package/dist/utils/codex-paths.d.ts +35 -0
- package/dist/utils/codex-paths.d.ts.map +1 -0
- package/dist/utils/codex-paths.js +49 -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}`;
|
|
@@ -85,6 +88,73 @@ function extractFrontmatterDescription(content) {
|
|
|
85
88
|
}
|
|
86
89
|
return undefined;
|
|
87
90
|
}
|
|
91
|
+
// ── Render-safety: base64-encode non-markdown file content ─────────────────
|
|
92
|
+
//
|
|
93
|
+
// Background: shipping raw shell / script bodies inside `content` /
|
|
94
|
+
// `expected_content` fields of LocalAction items causes Cursor's renderer
|
|
95
|
+
// (marked.js inline-link regex) to enter catastrophic backtracking on
|
|
96
|
+
// `$()`, `[[ ]]`, URL-like substrings, and freezes the main thread.
|
|
97
|
+
// See workspace/Issues/mcp-sync-complex-skill-cursor-crash.md.
|
|
98
|
+
//
|
|
99
|
+
// First attempt (markdown_fence_v1) tried to wrap content in a code fence,
|
|
100
|
+
// but Cursor's renderer feeds the *raw JSON literal* to marked.js, where
|
|
101
|
+
// `\n` is escaped as `\\n` and the fence never starts a real new line, so
|
|
102
|
+
// the fence is just inert text and the inline-link regex still walks the
|
|
103
|
+
// shell-script chars. Real-world test confirmed the freeze persisted.
|
|
104
|
+
//
|
|
105
|
+
// Final fix: for any non-`.md`/`.mdc` text file, base64-encode the content
|
|
106
|
+
// and set `encoding: 'base64'`. The base64 alphabet (`[A-Za-z0-9+/=]`)
|
|
107
|
+
// contains none of the metacharacters that marked.js's link regex looks
|
|
108
|
+
// for (`[`, `]`, `(`, `)`, `<`, `>`, `!`), so backtracking cannot happen
|
|
109
|
+
// no matter how long the string is.
|
|
110
|
+
//
|
|
111
|
+
// Markdown / mdc files stay UTF-8: they are valid markdown by themselves
|
|
112
|
+
// and field testing has not produced a freeze on plain markdown content.
|
|
113
|
+
const MARKDOWN_EXTENSIONS = new Set(['md', 'mdc']);
|
|
114
|
+
function fileExtension(filePath) {
|
|
115
|
+
const m = /\.([A-Za-z0-9]+)$/.exec(filePath);
|
|
116
|
+
return m ? m[1].toLowerCase() : '';
|
|
117
|
+
}
|
|
118
|
+
/**
|
|
119
|
+
* Encode a text file body for safe transport in the JSON tool response.
|
|
120
|
+
*
|
|
121
|
+
* Returns `{ content, encoding }` ready to spread into a write_file action.
|
|
122
|
+
*
|
|
123
|
+
* For `.md` / `.mdc` files: `{ content: rawContent, encoding: 'utf8' }`
|
|
124
|
+
* (no transformation, valid markdown is safe to render directly).
|
|
125
|
+
*
|
|
126
|
+
* For everything else: `{ content: base64(rawContent), encoding: 'base64' }`
|
|
127
|
+
* to defuse Cursor's marked.js link-regex catastrophic backtracking on
|
|
128
|
+
* shell/script characters.
|
|
129
|
+
*
|
|
130
|
+
* If the caller already declared a `priorEncoding` of 'base64' (binary file
|
|
131
|
+
* pre-encoded by the upstream), we pass content through unchanged.
|
|
132
|
+
*/
|
|
133
|
+
function encodeForRender(filePath, rawContent, priorEncoding) {
|
|
134
|
+
if (priorEncoding === 'base64') {
|
|
135
|
+
return { content: rawContent, encoding: 'base64' };
|
|
136
|
+
}
|
|
137
|
+
if (MARKDOWN_EXTENSIONS.has(fileExtension(filePath))) {
|
|
138
|
+
return { content: rawContent, encoding: 'utf8' };
|
|
139
|
+
}
|
|
140
|
+
return {
|
|
141
|
+
content: Buffer.from(rawContent, 'utf8').toString('base64'),
|
|
142
|
+
encoding: 'base64',
|
|
143
|
+
};
|
|
144
|
+
}
|
|
145
|
+
/**
|
|
146
|
+
* Same as encodeForRender, but produces the field shape required by
|
|
147
|
+
* CheckFileAction (`expected_content` + a sibling encoding hint stored as
|
|
148
|
+
* `expected_content_encoding`). Returns the encoding even when 'utf8' so
|
|
149
|
+
* the AI Agent has an unambiguous signal.
|
|
150
|
+
*/
|
|
151
|
+
function encodeForCheck(filePath, rawContent, priorEncoding) {
|
|
152
|
+
const enc = encodeForRender(filePath, rawContent, priorEncoding);
|
|
153
|
+
return {
|
|
154
|
+
expected_content: enc.content,
|
|
155
|
+
expected_content_encoding: enc.encoding,
|
|
156
|
+
};
|
|
157
|
+
}
|
|
88
158
|
async function syncResources(params) {
|
|
89
159
|
const startTime = Date.now();
|
|
90
160
|
const typedParams = params;
|
|
@@ -104,6 +174,13 @@ async function syncResources(params) {
|
|
|
104
174
|
? new Set(typedParams.resource_ids)
|
|
105
175
|
: null;
|
|
106
176
|
const confirmedFullSync = typedParams._confirmed_full_sync === true;
|
|
177
|
+
// Resolve client adapter: prefer the caller-supplied agent_profile, fall
|
|
178
|
+
// back to the server-wide config.agentProfile (set via CSP_AGENT_PROFILE).
|
|
179
|
+
const resolvedProfile = typedParams.agent_profile ?? index_js_4.config.agentProfile ?? 'cursor';
|
|
180
|
+
const clientAdapter = index_js_3.adapterRegistry.get(resolvedProfile);
|
|
181
|
+
// Collect per-rule content for Codex policy aggregation (stage 4).
|
|
182
|
+
// Each entry is raw rule markdown to be merged into csp-routing-policy.md.
|
|
183
|
+
const codexRuleContents = [];
|
|
107
184
|
(0, logger_1.logToolStep)('sync_resources', 'Parameters validated', {
|
|
108
185
|
mode,
|
|
109
186
|
scope,
|
|
@@ -248,17 +325,18 @@ async function syncResources(params) {
|
|
|
248
325
|
localActions.push({
|
|
249
326
|
action: 'check_file',
|
|
250
327
|
path: `${skillDir}/${scriptFile.relative_path}`,
|
|
251
|
-
|
|
328
|
+
...encodeForCheck(scriptFile.relative_path, scriptFile.content, scriptFile.encoding),
|
|
252
329
|
resource_id: sub.id,
|
|
253
330
|
resource_name: sub.name,
|
|
254
331
|
resource_type: sub.type,
|
|
255
332
|
});
|
|
256
333
|
}
|
|
257
334
|
// Also check the manifest file
|
|
335
|
+
const proxyScript = metadata.script_files[0];
|
|
258
336
|
localActions.push({
|
|
259
337
|
action: 'check_file',
|
|
260
338
|
path: `${(0, cursor_paths_1.getCspAgentRootDirForClient)()}/.manifests/${sub.name}.md`,
|
|
261
|
-
|
|
339
|
+
...encodeForCheck(proxyScript?.relative_path ?? '', proxyScript?.content ?? '', proxyScript?.encoding), // Use first script as proxy
|
|
262
340
|
resource_id: sub.id,
|
|
263
341
|
resource_name: sub.name,
|
|
264
342
|
resource_type: sub.type,
|
|
@@ -327,7 +405,7 @@ async function syncResources(params) {
|
|
|
327
405
|
localActions.push({
|
|
328
406
|
action: 'check_file',
|
|
329
407
|
path: checkPath,
|
|
330
|
-
|
|
408
|
+
...encodeForCheck(file.path, file.content),
|
|
331
409
|
resource_id: sub.id,
|
|
332
410
|
resource_name: sub.name,
|
|
333
411
|
resource_type: sub.type,
|
|
@@ -340,7 +418,7 @@ async function syncResources(params) {
|
|
|
340
418
|
localActions.push({
|
|
341
419
|
action: 'check_file',
|
|
342
420
|
path: mcpJsonPath,
|
|
343
|
-
|
|
421
|
+
...encodeForCheck(file.path, file.content),
|
|
344
422
|
resource_id: sub.id,
|
|
345
423
|
resource_name: sub.name,
|
|
346
424
|
resource_type: sub.type,
|
|
@@ -352,7 +430,7 @@ async function syncResources(params) {
|
|
|
352
430
|
localActions.push({
|
|
353
431
|
action: 'check_file',
|
|
354
432
|
path: checkPath,
|
|
355
|
-
|
|
433
|
+
...encodeForCheck(file.path, file.content),
|
|
356
434
|
resource_id: sub.id,
|
|
357
435
|
resource_name: sub.name,
|
|
358
436
|
resource_type: sub.type,
|
|
@@ -484,13 +562,12 @@ async function syncResources(params) {
|
|
|
484
562
|
localActions.push({
|
|
485
563
|
action: 'write_file',
|
|
486
564
|
path: `${skillDir}/${firstScript.path}`,
|
|
487
|
-
|
|
488
|
-
encoding: 'utf8',
|
|
565
|
+
...encodeForRender(firstScript.path, firstScript.content),
|
|
489
566
|
mode: firstScript.path.includes('/scripts/') ? '0755' : undefined,
|
|
490
567
|
// Atomic update marker: client checks manifest FIRST
|
|
491
568
|
is_skill_manifest: true,
|
|
492
569
|
// SKILL.md content for version comparison (stored separately in .manifests/)
|
|
493
|
-
skill_manifest_content: rawContent,
|
|
570
|
+
skill_manifest_content: Buffer.from(rawContent, "utf8").toString("base64"),
|
|
494
571
|
});
|
|
495
572
|
}
|
|
496
573
|
// 2. Remaining script files (client writes these ONLY if manifest changed)
|
|
@@ -501,8 +578,7 @@ async function syncResources(params) {
|
|
|
501
578
|
localActions.push({
|
|
502
579
|
action: 'write_file',
|
|
503
580
|
path: `${skillDir}/${scriptFile.path}`,
|
|
504
|
-
|
|
505
|
-
encoding: 'utf8',
|
|
581
|
+
...encodeForRender(scriptFile.path, scriptFile.content),
|
|
506
582
|
mode: scriptFile.path.includes('/scripts/') ? '0755' : undefined,
|
|
507
583
|
});
|
|
508
584
|
}
|
|
@@ -532,11 +608,10 @@ async function syncResources(params) {
|
|
|
532
608
|
localActions.push({
|
|
533
609
|
action: 'write_file',
|
|
534
610
|
path: `${skillDir}/${firstScript.relative_path}`,
|
|
535
|
-
|
|
536
|
-
encoding: firstScript.encoding ?? 'utf8',
|
|
611
|
+
...encodeForRender(firstScript.relative_path, firstScript.content, firstScript.encoding),
|
|
537
612
|
mode: firstScript.mode,
|
|
538
613
|
is_skill_manifest: true,
|
|
539
|
-
skill_manifest_content: rawContent,
|
|
614
|
+
skill_manifest_content: Buffer.from(rawContent, "utf8").toString("base64"),
|
|
540
615
|
});
|
|
541
616
|
}
|
|
542
617
|
for (let i = 1; i < metadata.script_files.length; i++) {
|
|
@@ -546,8 +621,7 @@ async function syncResources(params) {
|
|
|
546
621
|
localActions.push({
|
|
547
622
|
action: 'write_file',
|
|
548
623
|
path: `${skillDir}/${scriptFile.relative_path}`,
|
|
549
|
-
|
|
550
|
-
encoding: scriptFile.encoding ?? 'utf8',
|
|
624
|
+
...encodeForRender(scriptFile.relative_path, scriptFile.content, scriptFile.encoding),
|
|
551
625
|
mode: scriptFile.mode,
|
|
552
626
|
});
|
|
553
627
|
}
|
|
@@ -672,8 +746,10 @@ async function syncResources(params) {
|
|
|
672
746
|
// not on this (possibly remote Linux) server.
|
|
673
747
|
if (sub.type === 'mcp') {
|
|
674
748
|
const mcpConfigFile = resourceFiles.find((f) => path.basename(f.path) === 'mcp-config.json');
|
|
675
|
-
//
|
|
676
|
-
const mcpJsonPath =
|
|
749
|
+
// Config path differs per client: mcp.json (Cursor) vs config.toml (Codex)
|
|
750
|
+
const mcpJsonPath = resolvedProfile === 'codex'
|
|
751
|
+
? clientAdapter.getMcpConfigPath() // ~/.codex/config.toml
|
|
752
|
+
: `${(0, cursor_paths_1.getCursorRootDirForClient)()}/mcp.json`;
|
|
677
753
|
// ── Optimization: skip if already configured (incremental mode only) ────
|
|
678
754
|
// In incremental mode, if the AI Agent reports this MCP server is already
|
|
679
755
|
// in ~/.cursor/mcp.json, skip downloading and generating write_file actions.
|
|
@@ -721,7 +797,25 @@ async function syncResources(params) {
|
|
|
721
797
|
catch {
|
|
722
798
|
logger_1.logger.warn({ resourceId: sub.id, resourceName: sub.name }, 'sync_resources: failed to parse mcp-config.json — treating as empty config');
|
|
723
799
|
}
|
|
724
|
-
if (
|
|
800
|
+
if (resolvedProfile === 'codex') {
|
|
801
|
+
// ── Codex MCP: inject server entry into config.toml via merge_toml ──
|
|
802
|
+
// The Codex CLI reads MCP server configuration from ~/.codex/config.toml.
|
|
803
|
+
// We emit a merge_toml action for the AI Agent to update that file.
|
|
804
|
+
// The value is the JSON-serialised mcp-config entry so the agent can
|
|
805
|
+
// parse and embed it in the correct TOML table.
|
|
806
|
+
const tomlPath = clientAdapter.getMcpConfigPath();
|
|
807
|
+
const serverName = cfg['name'] ?? sub.name;
|
|
808
|
+
localActions.push({
|
|
809
|
+
action: 'merge_toml',
|
|
810
|
+
toml_path: tomlPath,
|
|
811
|
+
key: `mcp.servers.${serverName}`,
|
|
812
|
+
value: JSON.stringify(cfg),
|
|
813
|
+
overwrite: false,
|
|
814
|
+
});
|
|
815
|
+
logger_1.logger.info({ resourceId: sub.id, resourceName: sub.name, serverName, tomlPath, profile: 'codex' }, 'sync_resources: Codex MCP — merge_toml action queued');
|
|
816
|
+
(0, logger_1.logToolStep)('sync_resources', 'Codex MCP: merge_toml queued', { resourceId: sub.id });
|
|
817
|
+
}
|
|
818
|
+
else if (typeof cfg['command'] === 'string') {
|
|
725
819
|
// ── Format A: local executable ──────────────────────────────────
|
|
726
820
|
const installDir = `${(0, cursor_paths_1.getCursorTypeDirForClient)('mcp')}/${sub.name}`;
|
|
727
821
|
const writeActions = [];
|
|
@@ -733,7 +827,7 @@ async function syncResources(params) {
|
|
|
733
827
|
localActions.push({
|
|
734
828
|
action: 'write_file',
|
|
735
829
|
path: fileDest,
|
|
736
|
-
|
|
830
|
+
...encodeForRender(file.path, file.content),
|
|
737
831
|
});
|
|
738
832
|
writeActions.push(fileDest);
|
|
739
833
|
}
|
|
@@ -803,43 +897,57 @@ async function syncResources(params) {
|
|
|
803
897
|
}
|
|
804
898
|
else {
|
|
805
899
|
// No mcp-config.json: heuristic fallback
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
const normalised = path.normalize(file.path);
|
|
810
|
-
if (normalised.startsWith('..'))
|
|
811
|
-
continue;
|
|
812
|
-
const fileDest = `${installDir}/${normalised}`;
|
|
900
|
+
if (resolvedProfile === 'codex') {
|
|
901
|
+
// Codex: emit a bare merge_toml with minimal entry
|
|
902
|
+
const tomlPath = clientAdapter.getMcpConfigPath();
|
|
813
903
|
localActions.push({
|
|
814
|
-
action: '
|
|
815
|
-
|
|
816
|
-
|
|
904
|
+
action: 'merge_toml',
|
|
905
|
+
toml_path: tomlPath,
|
|
906
|
+
key: `mcp.servers.${sub.name}`,
|
|
907
|
+
value: JSON.stringify({ command: 'node', args: [] }),
|
|
908
|
+
overwrite: false,
|
|
817
909
|
});
|
|
818
|
-
|
|
910
|
+
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)');
|
|
911
|
+
}
|
|
912
|
+
else {
|
|
913
|
+
const installDir = `${(0, cursor_paths_1.getCursorTypeDirForClient)('mcp')}/${sub.name}`;
|
|
914
|
+
const writeActions = [];
|
|
915
|
+
for (const file of resourceFiles) {
|
|
916
|
+
const normalised = path.normalize(file.path);
|
|
917
|
+
if (normalised.startsWith('..'))
|
|
918
|
+
continue;
|
|
919
|
+
const fileDest = `${installDir}/${normalised}`;
|
|
920
|
+
localActions.push({
|
|
921
|
+
action: 'write_file',
|
|
922
|
+
path: fileDest,
|
|
923
|
+
...encodeForRender(file.path, file.content),
|
|
924
|
+
});
|
|
925
|
+
writeActions.push(fileDest);
|
|
926
|
+
}
|
|
927
|
+
const jsEntry = resourceFiles.find((f) => f.path.endsWith('.js'));
|
|
928
|
+
const pyEntry = resourceFiles.find((f) => f.path.endsWith('.py'));
|
|
929
|
+
const entryFile = jsEntry ?? pyEntry ?? resourceFiles[0];
|
|
930
|
+
const cmd = jsEntry ? 'node' : 'python3';
|
|
931
|
+
const entryPath = `${installDir}/${entryFile?.path ?? ''}`;
|
|
932
|
+
localActions.push({
|
|
933
|
+
action: 'merge_mcp_json',
|
|
934
|
+
mcp_json_path: mcpJsonPath,
|
|
935
|
+
server_name: sub.name,
|
|
936
|
+
entry: { command: cmd, args: [entryPath] },
|
|
937
|
+
skip_if_exists: true,
|
|
938
|
+
});
|
|
939
|
+
logger_1.logger.info({
|
|
940
|
+
resourceId: sub.id,
|
|
941
|
+
resourceName: sub.name,
|
|
942
|
+
format: 'heuristic',
|
|
943
|
+
installDir,
|
|
944
|
+
mcpJsonPath,
|
|
945
|
+
cmd,
|
|
946
|
+
entryPath,
|
|
947
|
+
writeFiles: writeActions,
|
|
948
|
+
}, 'sync_resources: MCP heuristic fallback — write_file + merge_mcp_json actions queued');
|
|
949
|
+
(0, logger_1.logToolStep)('sync_resources', 'MCP heuristic fallback: write_file + merge_mcp_json queued', { resourceId: sub.id });
|
|
819
950
|
}
|
|
820
|
-
const jsEntry = resourceFiles.find((f) => f.path.endsWith('.js'));
|
|
821
|
-
const pyEntry = resourceFiles.find((f) => f.path.endsWith('.py'));
|
|
822
|
-
const entryFile = jsEntry ?? pyEntry ?? resourceFiles[0];
|
|
823
|
-
const cmd = jsEntry ? 'node' : 'python3';
|
|
824
|
-
const entryPath = `${installDir}/${entryFile?.path ?? ''}`;
|
|
825
|
-
localActions.push({
|
|
826
|
-
action: 'merge_mcp_json',
|
|
827
|
-
mcp_json_path: mcpJsonPath,
|
|
828
|
-
server_name: sub.name,
|
|
829
|
-
entry: { command: cmd, args: [entryPath] },
|
|
830
|
-
skip_if_exists: true,
|
|
831
|
-
});
|
|
832
|
-
logger_1.logger.info({
|
|
833
|
-
resourceId: sub.id,
|
|
834
|
-
resourceName: sub.name,
|
|
835
|
-
format: 'heuristic',
|
|
836
|
-
installDir,
|
|
837
|
-
mcpJsonPath,
|
|
838
|
-
cmd,
|
|
839
|
-
entryPath,
|
|
840
|
-
writeFiles: writeActions,
|
|
841
|
-
}, 'sync_resources: MCP heuristic fallback — write_file + merge_mcp_json actions queued');
|
|
842
|
-
(0, logger_1.logToolStep)('sync_resources', 'MCP heuristic fallback: write_file + merge_mcp_json queued', { resourceId: sub.id });
|
|
843
951
|
}
|
|
844
952
|
tally.synced++;
|
|
845
953
|
details.push({ id: sub.id, name: sub.name, action: 'synced', version: resourceVersion });
|
|
@@ -853,11 +961,25 @@ async function syncResources(params) {
|
|
|
853
961
|
// or has different content, the AI writes it unconditionally, which also
|
|
854
962
|
// recovers files that were accidentally deleted by the user.
|
|
855
963
|
//
|
|
856
|
-
// DUAL-LAYER STRATEGY (v1.6):
|
|
964
|
+
// DUAL-LAYER STRATEGY (v1.6, Cursor only):
|
|
857
965
|
// - scope='global' → write to ~/.cursor/rules/ only (macOS support)
|
|
858
966
|
// - scope='workspace' → write to ${WORKSPACE}/.cursor/rules/ only (Windows support)
|
|
859
967
|
// - scope='all' → write to BOTH locations (maximum compatibility)
|
|
968
|
+
//
|
|
969
|
+
// CODEX STRATEGY: rules are aggregated into csp-routing-policy.md and injected
|
|
970
|
+
// via developer_instructions in config.toml (no .mdc files written to disk).
|
|
860
971
|
if (sub.type === 'rule') {
|
|
972
|
+
if (resolvedProfile === 'codex') {
|
|
973
|
+
// Collect rule content for policy aggregation; no individual file actions.
|
|
974
|
+
for (const file of resourceFiles) {
|
|
975
|
+
codexRuleContents.push({ name: sub.name, content: file.content });
|
|
976
|
+
}
|
|
977
|
+
logger_1.logger.info({ resourceId: sub.id, resourceName: sub.name, profile: 'codex' }, 'sync_resources: Rule collected for Codex policy aggregation');
|
|
978
|
+
tally.synced++;
|
|
979
|
+
details.push({ id: sub.id, name: sub.name, action: 'synced', version: resourceVersion });
|
|
980
|
+
continue;
|
|
981
|
+
}
|
|
982
|
+
// ── Cursor: dual-layer .mdc file strategy (unchanged) ──────────────
|
|
861
983
|
const writeActions = [];
|
|
862
984
|
// Determine target directories based on scope parameter
|
|
863
985
|
const targetDirs = [];
|
|
@@ -878,7 +1000,7 @@ async function syncResources(params) {
|
|
|
878
1000
|
localActions.push({
|
|
879
1001
|
action: 'write_file',
|
|
880
1002
|
path: destPath,
|
|
881
|
-
|
|
1003
|
+
...encodeForRender(file.path, file.content),
|
|
882
1004
|
});
|
|
883
1005
|
writeActions.push({ destPath, contentLength: file.content.length });
|
|
884
1006
|
}
|
|
@@ -936,7 +1058,43 @@ async function syncResources(params) {
|
|
|
936
1058
|
else if (resourceIds) {
|
|
937
1059
|
logger_1.logger.info({ resourceIdsFilter: [...resourceIds], expectedPromptCount: expectedPromptNames.size }, 'sync_resources: skipping pruneStalePrompts — resource_ids filter active (partial sync)');
|
|
938
1060
|
}
|
|
939
|
-
// ── Step 5:
|
|
1061
|
+
// ── Step 5 (Codex): Aggregate collected rules into policy actions ──────
|
|
1062
|
+
// If any rule content was collected during this sync run, materialise the
|
|
1063
|
+
// csp-routing-policy.md file and emit a merge_toml action to inject it
|
|
1064
|
+
// into developer_instructions in ~/.codex/config.toml.
|
|
1065
|
+
let restartRequired = false;
|
|
1066
|
+
let restartHint;
|
|
1067
|
+
if (resolvedProfile === 'codex' && codexRuleContents.length > 0) {
|
|
1068
|
+
try {
|
|
1069
|
+
const policyStrategy = clientAdapter.getPolicyStrategy();
|
|
1070
|
+
const policyContent = (0, policy_generator_js_1.generatePolicyContent)(codexRuleContents);
|
|
1071
|
+
const policyFile = policyStrategy.policyFile ?? '~/.csp-ai-agent/codex/csp-routing-policy.md';
|
|
1072
|
+
const tomlPath = policyStrategy.configTomlPath ?? '~/.codex/config.toml';
|
|
1073
|
+
const tomlKey = policyStrategy.configTomlKey ?? 'developer_instructions';
|
|
1074
|
+
// 1. Write the policy markdown file
|
|
1075
|
+
localActions.push({
|
|
1076
|
+
action: 'write_file',
|
|
1077
|
+
path: policyFile,
|
|
1078
|
+
content: policyContent,
|
|
1079
|
+
encoding: 'utf8',
|
|
1080
|
+
});
|
|
1081
|
+
// 2. Inject the policy file reference into developer_instructions
|
|
1082
|
+
localActions.push({
|
|
1083
|
+
action: 'merge_toml',
|
|
1084
|
+
toml_path: tomlPath,
|
|
1085
|
+
key: tomlKey,
|
|
1086
|
+
value: `Please read and follow the CSP routing policy at: ${policyFile}`,
|
|
1087
|
+
overwrite: false,
|
|
1088
|
+
});
|
|
1089
|
+
restartRequired = true;
|
|
1090
|
+
restartHint = `CSP routing policy has been updated. Please restart Codex for the policy to take effect: codex`;
|
|
1091
|
+
logger_1.logger.info({ profile: 'codex', policyFile, tomlPath, ruleCount: codexRuleContents.length }, 'sync_resources: Codex policy materialised — merge_toml action queued');
|
|
1092
|
+
}
|
|
1093
|
+
catch (err) {
|
|
1094
|
+
logger_1.logger.error({ error: err instanceof Error ? err.message : String(err) }, 'sync_resources: Failed to generate Codex policy');
|
|
1095
|
+
}
|
|
1096
|
+
}
|
|
1097
|
+
// ── Step 6: Health score ───────────────────────────────────────────────
|
|
940
1098
|
const healthScore = tally.total > 0
|
|
941
1099
|
? Math.round(((tally.synced + tally.cached + tally.skipped) / tally.total) * 100)
|
|
942
1100
|
: 100;
|
|
@@ -947,6 +1105,7 @@ async function syncResources(params) {
|
|
|
947
1105
|
details,
|
|
948
1106
|
...(skippedResources.length > 0 ? { skipped_resources: skippedResources } : {}),
|
|
949
1107
|
...(localActions.length > 0 ? { local_actions_required: localActions } : {}),
|
|
1108
|
+
...(restartRequired ? { restart_required: true, restart_hint: restartHint } : {}),
|
|
950
1109
|
};
|
|
951
1110
|
const duration = Date.now() - startTime;
|
|
952
1111
|
(0, logger_1.logToolCall)('sync_resources', 'user-id', params, duration);
|
|
@@ -1055,16 +1214,31 @@ exports.syncResourcesTool = {
|
|
|
1055
1214
|
'This allows the server to skip downloading MCP resources that are already configured, ' +
|
|
1056
1215
|
'significantly reducing API calls, network traffic, and local action overhead. ' +
|
|
1057
1216
|
'\n' +
|
|
1217
|
+
'⚠️ BASE64 DECODE — APPLIES TO BOTH write_file AND check_file: ' +
|
|
1218
|
+
' When `encoding === "base64"` (write_file) or `expected_content_encoding === "base64"` ' +
|
|
1219
|
+
' (check_file), the corresponding `content` / `expected_content` field is base64-encoded ' +
|
|
1220
|
+
' by the server. The server does this for ALL non-.md/.mdc files (shell scripts, configs, ' +
|
|
1221
|
+
' binary blobs) to keep Cursor\'s renderer from triggering catastrophic regex backtracking ' +
|
|
1222
|
+
' on raw script characters ($(), [[ ]], URLs etc.) — see prior incident notes. ' +
|
|
1223
|
+
' AI Agent MUST decode base64 BEFORE writing to disk or comparing against local content: ' +
|
|
1224
|
+
' write_file: fileBytes = Buffer.from(action.content, "base64") (write as binary buffer) ' +
|
|
1225
|
+
' check_file: decoded = Buffer.from(action.expected_content, "base64").toString("utf8") ' +
|
|
1226
|
+
' then string-compare against fs.readFileSync(action.path, "utf8") ' +
|
|
1227
|
+
' When the encoding field is "utf8" (or omitted), the value is the literal file body — ' +
|
|
1228
|
+
' write or compare as-is, no decoding. ' +
|
|
1058
1229
|
'For write_file actions: ' +
|
|
1059
1230
|
' SPECIAL HANDLING FOR COMPLEX SKILLS (when action has `is_skill_manifest: true`): ' +
|
|
1060
1231
|
' (1) This action is the FIRST script file — check skill version using manifest FIRST. ' +
|
|
1061
1232
|
' (2) Read manifest file at {CSP_AGENT_ROOT}/.manifests/<skill-name>.md (if exists). ' +
|
|
1062
1233
|
' Where {CSP_AGENT_ROOT} is a SIBLING of {CURSOR_ROOT} (same parent directory). ' +
|
|
1063
|
-
' (3)
|
|
1234
|
+
' (3) action.skill_manifest_content is ALWAYS base64-encoded — first decode it: ' +
|
|
1235
|
+
' decodedManifest = Buffer.from(action.skill_manifest_content, "base64").toString("utf8") ' +
|
|
1236
|
+
' Then compare decodedManifest against the existing manifest file content (string equality). ' +
|
|
1064
1237
|
' (4) If identical: SKIP this action AND ALL SUBSEQUENT write_file actions for this skill. ' +
|
|
1065
1238
|
' (5) If different or manifest missing: ' +
|
|
1066
1239
|
' - PROCEED with this action and all subsequent write_file actions for this skill. ' +
|
|
1067
|
-
' - After writing all script files, write
|
|
1240
|
+
' - After writing all script files, write decodedManifest (the UTF-8 form, NOT the raw base64 string) ' +
|
|
1241
|
+
' to {CSP_AGENT_ROOT}/.manifests/<skill-name>.md ' +
|
|
1068
1242
|
' (6) NOTE: SKILL.md is NEVER written to the skills directory (only to .manifests/) to prevent Cursor auto-discovery. ' +
|
|
1069
1243
|
' GENERAL HANDLING (when action has NO `is_skill_manifest` marker): ' +
|
|
1070
1244
|
' (1) Read the existing file at `path` (if it exists) using fs.readFile() or equivalent. ' +
|