@cleocode/caamp 2026.4.0 → 2026.4.3
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/{chunk-3WKBFXLE.js → chunk-6NBM4CAF.js} +796 -1611
- package/dist/chunk-6NBM4CAF.js.map +1 -0
- package/dist/{chunk-5UUABVWS.js → chunk-CRU25LRL.js} +2 -2
- package/dist/{chunk-OH6Z2N3G.js → chunk-FLBRAXDW.js} +2 -2
- package/dist/{chunk-3IEKCREL.js → chunk-XWQ5WPHC.js} +1 -8
- package/dist/{chunk-3IEKCREL.js.map → chunk-XWQ5WPHC.js.map} +1 -1
- package/dist/cli.js +364 -1906
- package/dist/cli.js.map +1 -1
- package/dist/{hooks-Q7KO2SGK.js → hooks-VLIP52LY.js} +3 -3
- package/dist/index.d.ts +6 -1043
- package/dist/index.js +5 -60
- package/dist/index.js.map +1 -1
- package/dist/{injector-XCWEBXWK.js → injector-ALLOKC54.js} +3 -3
- package/package.json +2 -2
- package/dist/chunk-3WKBFXLE.js.map +0 -1
- /package/dist/{chunk-5UUABVWS.js.map → chunk-CRU25LRL.js.map} +0 -0
- /package/dist/{chunk-OH6Z2N3G.js.map → chunk-FLBRAXDW.js.map} +0 -0
- /package/dist/{hooks-Q7KO2SGK.js.map → hooks-VLIP52LY.js.map} +0 -0
- /package/dist/{injector-XCWEBXWK.js.map → injector-ALLOKC54.js.map} +0 -0
|
@@ -2,7 +2,7 @@ import {
|
|
|
2
2
|
getAllProviders,
|
|
3
3
|
groupByInstructFile,
|
|
4
4
|
injectAll
|
|
5
|
-
} from "./chunk-
|
|
5
|
+
} from "./chunk-CRU25LRL.js";
|
|
6
6
|
import {
|
|
7
7
|
__export,
|
|
8
8
|
getAgentsConfigPath,
|
|
@@ -12,10 +12,9 @@ import {
|
|
|
12
12
|
getCanonicalSkillsDir,
|
|
13
13
|
getLockFilePath,
|
|
14
14
|
getPlatformLocations,
|
|
15
|
-
resolveProviderConfigPath,
|
|
16
15
|
resolveProviderProjectPath,
|
|
17
16
|
resolveProviderSkillsDirs
|
|
18
|
-
} from "./chunk-
|
|
17
|
+
} from "./chunk-XWQ5WPHC.js";
|
|
19
18
|
|
|
20
19
|
// src/core/logger.ts
|
|
21
20
|
var verboseMode = false;
|
|
@@ -43,880 +42,364 @@ function isHuman() {
|
|
|
43
42
|
return humanMode;
|
|
44
43
|
}
|
|
45
44
|
|
|
46
|
-
// src/core/
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
}
|
|
60
|
-
}
|
|
61
|
-
return result;
|
|
62
|
-
}
|
|
63
|
-
function getNestedValue(obj, keyPath) {
|
|
64
|
-
const parts = keyPath.split(".");
|
|
65
|
-
let current = obj;
|
|
66
|
-
for (const part of parts) {
|
|
67
|
-
if (current === null || typeof current !== "object") return void 0;
|
|
68
|
-
current = current[part];
|
|
45
|
+
// src/core/registry/detection.ts
|
|
46
|
+
import { execFileSync } from "child_process";
|
|
47
|
+
import { existsSync } from "fs";
|
|
48
|
+
import { join } from "path";
|
|
49
|
+
var DEFAULT_DETECTION_CACHE_TTL_MS = 3e4;
|
|
50
|
+
var detectionCache = null;
|
|
51
|
+
function checkBinary(binary) {
|
|
52
|
+
try {
|
|
53
|
+
const cmd = process.platform === "win32" ? "where" : "which";
|
|
54
|
+
execFileSync(cmd, [binary], { stdio: "pipe" });
|
|
55
|
+
return true;
|
|
56
|
+
} catch {
|
|
57
|
+
return false;
|
|
69
58
|
}
|
|
70
|
-
return current;
|
|
71
59
|
}
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
const { dirname: dirname3 } = await import("path");
|
|
75
|
-
await mkdir4(dirname3(filePath), { recursive: true });
|
|
60
|
+
function checkDirectory(dir) {
|
|
61
|
+
return existsSync(dir);
|
|
76
62
|
}
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
import * as jsonc from "jsonc-parser";
|
|
82
|
-
async function readJsonConfig(filePath) {
|
|
83
|
-
if (!existsSync(filePath)) return {};
|
|
84
|
-
const content = await readFile(filePath, "utf-8");
|
|
85
|
-
if (!content.trim()) return {};
|
|
86
|
-
const errors = [];
|
|
87
|
-
const result = jsonc.parse(content, errors);
|
|
88
|
-
if (errors.length > 0) {
|
|
89
|
-
return JSON.parse(content);
|
|
90
|
-
}
|
|
91
|
-
return result ?? {};
|
|
63
|
+
function checkAppBundle(appName) {
|
|
64
|
+
if (process.platform !== "darwin") return false;
|
|
65
|
+
const applications = getPlatformLocations().applications;
|
|
66
|
+
return applications.some((base) => existsSync(join(base, appName)));
|
|
92
67
|
}
|
|
93
|
-
function
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
return { indent: " ", insertSpaces: false, tabSize: 1 };
|
|
101
|
-
}
|
|
102
|
-
return { indent: ws, insertSpaces: true, tabSize: ws.length };
|
|
103
|
-
}
|
|
68
|
+
function checkFlatpak(flatpakId) {
|
|
69
|
+
if (process.platform !== "linux") return false;
|
|
70
|
+
try {
|
|
71
|
+
execFileSync("flatpak", ["info", flatpakId], { stdio: "pipe" });
|
|
72
|
+
return true;
|
|
73
|
+
} catch {
|
|
74
|
+
return false;
|
|
104
75
|
}
|
|
105
|
-
return { indent: " ", insertSpaces: true, tabSize: 2 };
|
|
106
76
|
}
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
77
|
+
function detectProvider(provider) {
|
|
78
|
+
const matchedMethods = [];
|
|
79
|
+
const detection = provider.detection;
|
|
80
|
+
debug(`detecting provider ${provider.id} via methods: ${detection.methods.join(", ")}`);
|
|
81
|
+
for (const method of detection.methods) {
|
|
82
|
+
switch (method) {
|
|
83
|
+
case "binary":
|
|
84
|
+
if (detection.binary && checkBinary(detection.binary)) {
|
|
85
|
+
debug(` ${provider.id}: binary "${detection.binary}" found`);
|
|
86
|
+
matchedMethods.push("binary");
|
|
87
|
+
}
|
|
88
|
+
break;
|
|
89
|
+
case "directory":
|
|
90
|
+
if (detection.directories) {
|
|
91
|
+
for (const dir of detection.directories) {
|
|
92
|
+
if (checkDirectory(dir)) {
|
|
93
|
+
matchedMethods.push("directory");
|
|
94
|
+
break;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
break;
|
|
99
|
+
case "appBundle":
|
|
100
|
+
if (detection.appBundle && checkAppBundle(detection.appBundle)) {
|
|
101
|
+
matchedMethods.push("appBundle");
|
|
102
|
+
}
|
|
103
|
+
break;
|
|
104
|
+
case "flatpak":
|
|
105
|
+
if (detection.flatpakId && checkFlatpak(detection.flatpakId)) {
|
|
106
|
+
matchedMethods.push("flatpak");
|
|
107
|
+
}
|
|
108
|
+
break;
|
|
114
109
|
}
|
|
115
|
-
} else {
|
|
116
|
-
content = "{}";
|
|
117
110
|
}
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
111
|
+
return {
|
|
112
|
+
provider,
|
|
113
|
+
installed: matchedMethods.length > 0,
|
|
114
|
+
methods: matchedMethods,
|
|
115
|
+
projectDetected: false
|
|
123
116
|
};
|
|
124
|
-
const keyParts = configKey.split(".");
|
|
125
|
-
const jsonPath = [...keyParts, serverName];
|
|
126
|
-
const edits = jsonc.modify(content, jsonPath, serverConfig, { formattingOptions: formatOptions });
|
|
127
|
-
if (edits.length > 0) {
|
|
128
|
-
content = jsonc.applyEdits(content, edits);
|
|
129
|
-
}
|
|
130
|
-
if (!content.endsWith("\n")) {
|
|
131
|
-
content += "\n";
|
|
132
|
-
}
|
|
133
|
-
await writeFile(filePath, content, "utf-8");
|
|
134
117
|
}
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
118
|
+
function providerSignature(provider) {
|
|
119
|
+
return JSON.stringify({
|
|
120
|
+
id: provider.id,
|
|
121
|
+
methods: provider.detection.methods,
|
|
122
|
+
binary: provider.detection.binary,
|
|
123
|
+
directories: provider.detection.directories,
|
|
124
|
+
appBundle: provider.detection.appBundle,
|
|
125
|
+
flatpakId: provider.detection.flatpakId
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
function buildProvidersSignature(providers) {
|
|
129
|
+
if (!providers || !Array.isArray(providers)) return "";
|
|
130
|
+
return providers.map(providerSignature).join("|");
|
|
131
|
+
}
|
|
132
|
+
function cloneDetectionResults(results) {
|
|
133
|
+
return results.map((result) => ({
|
|
134
|
+
provider: result.provider,
|
|
135
|
+
installed: result.installed,
|
|
136
|
+
methods: [...result.methods],
|
|
137
|
+
projectDetected: result.projectDetected
|
|
138
|
+
}));
|
|
139
|
+
}
|
|
140
|
+
function getCachedResults(signature, options) {
|
|
141
|
+
if (!detectionCache || options.forceRefresh) return null;
|
|
142
|
+
if (detectionCache.signature !== signature) return null;
|
|
143
|
+
const ttlMs = options.ttlMs ?? DEFAULT_DETECTION_CACHE_TTL_MS;
|
|
144
|
+
if (ttlMs <= 0) return null;
|
|
145
|
+
if (Date.now() - detectionCache.createdAt > ttlMs) return null;
|
|
146
|
+
return cloneDetectionResults(detectionCache.results);
|
|
147
|
+
}
|
|
148
|
+
function setCachedResults(signature, results) {
|
|
149
|
+
detectionCache = {
|
|
150
|
+
createdAt: Date.now(),
|
|
151
|
+
signature,
|
|
152
|
+
results: cloneDetectionResults(results)
|
|
144
153
|
};
|
|
145
|
-
const keyParts = configKey.split(".");
|
|
146
|
-
const jsonPath = [...keyParts, serverName];
|
|
147
|
-
const edits = jsonc.modify(content, jsonPath, void 0, { formattingOptions: formatOptions });
|
|
148
|
-
if (edits.length === 0) return false;
|
|
149
|
-
content = jsonc.applyEdits(content, edits);
|
|
150
|
-
if (!content.endsWith("\n")) {
|
|
151
|
-
content += "\n";
|
|
152
|
-
}
|
|
153
|
-
await writeFile(filePath, content, "utf-8");
|
|
154
|
-
return true;
|
|
155
154
|
}
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
import { readFile as readFile2, writeFile as writeFile2 } from "fs/promises";
|
|
160
|
-
import TOML from "@iarna/toml";
|
|
161
|
-
async function readTomlConfig(filePath) {
|
|
162
|
-
if (!existsSync2(filePath)) return {};
|
|
163
|
-
const content = await readFile2(filePath, "utf-8");
|
|
164
|
-
if (!content.trim()) return {};
|
|
165
|
-
const result = TOML.parse(content);
|
|
166
|
-
return result;
|
|
155
|
+
function detectProjectProvider(provider, projectDir) {
|
|
156
|
+
if (!provider.pathProject) return false;
|
|
157
|
+
return existsSync(resolveProviderProjectPath(provider, projectDir));
|
|
167
158
|
}
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
const
|
|
171
|
-
const
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
159
|
+
function detectAllProviders(options = {}) {
|
|
160
|
+
const providers = getAllProviders() ?? [];
|
|
161
|
+
const signature = buildProvidersSignature(providers);
|
|
162
|
+
const cached = getCachedResults(signature, options);
|
|
163
|
+
if (cached) {
|
|
164
|
+
debug(`detection cache hit for ${providers.length} providers`);
|
|
165
|
+
return cached;
|
|
175
166
|
}
|
|
176
|
-
const
|
|
177
|
-
|
|
178
|
-
|
|
167
|
+
const results = providers.map(detectProvider);
|
|
168
|
+
setCachedResults(signature, results);
|
|
169
|
+
return cloneDetectionResults(results);
|
|
179
170
|
}
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
const existing = await readTomlConfig(filePath);
|
|
183
|
-
const keyParts = configKey.split(".");
|
|
184
|
-
let current = existing;
|
|
185
|
-
for (const part of keyParts) {
|
|
186
|
-
const next = current[part];
|
|
187
|
-
if (typeof next !== "object" || next === null) return false;
|
|
188
|
-
current = next;
|
|
189
|
-
}
|
|
190
|
-
if (!(serverName in current)) return false;
|
|
191
|
-
delete current[serverName];
|
|
192
|
-
const content = TOML.stringify(existing);
|
|
193
|
-
await writeFile2(filePath, content, "utf-8");
|
|
194
|
-
return true;
|
|
171
|
+
function getInstalledProviders(options = {}) {
|
|
172
|
+
return detectAllProviders(options).filter((r) => r.installed).map((r) => r.provider);
|
|
195
173
|
}
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
if (!existsSync3(filePath)) return {};
|
|
203
|
-
const content = await readFile3(filePath, "utf-8");
|
|
204
|
-
if (!content.trim()) return {};
|
|
205
|
-
const result = yaml.load(content);
|
|
206
|
-
return result ?? {};
|
|
174
|
+
function detectProjectProviders(projectDir, options = {}) {
|
|
175
|
+
const results = detectAllProviders(options);
|
|
176
|
+
return results.map((r) => ({
|
|
177
|
+
...r,
|
|
178
|
+
projectDetected: detectProjectProvider(r.provider, projectDir)
|
|
179
|
+
}));
|
|
207
180
|
}
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
const existing = await readYamlConfig(filePath);
|
|
211
|
-
const keyParts = configKey.split(".");
|
|
212
|
-
let newEntry = { [serverName]: serverConfig };
|
|
213
|
-
for (const part of [...keyParts].reverse()) {
|
|
214
|
-
newEntry = { [part]: newEntry };
|
|
215
|
-
}
|
|
216
|
-
const merged = deepMerge(existing, newEntry);
|
|
217
|
-
const content = yaml.dump(merged, {
|
|
218
|
-
indent: 2,
|
|
219
|
-
lineWidth: -1,
|
|
220
|
-
noRefs: true,
|
|
221
|
-
sortKeys: false
|
|
222
|
-
});
|
|
223
|
-
await writeFile3(filePath, content, "utf-8");
|
|
224
|
-
}
|
|
225
|
-
async function removeYamlConfig(filePath, configKey, serverName) {
|
|
226
|
-
if (!existsSync3(filePath)) return false;
|
|
227
|
-
const existing = await readYamlConfig(filePath);
|
|
228
|
-
const keyParts = configKey.split(".");
|
|
229
|
-
let current = existing;
|
|
230
|
-
for (const part of keyParts) {
|
|
231
|
-
const next = current[part];
|
|
232
|
-
if (typeof next !== "object" || next === null) return false;
|
|
233
|
-
current = next;
|
|
234
|
-
}
|
|
235
|
-
if (!(serverName in current)) return false;
|
|
236
|
-
delete current[serverName];
|
|
237
|
-
const content = yaml.dump(existing, {
|
|
238
|
-
indent: 2,
|
|
239
|
-
lineWidth: -1,
|
|
240
|
-
noRefs: true,
|
|
241
|
-
sortKeys: false
|
|
242
|
-
});
|
|
243
|
-
await writeFile3(filePath, content, "utf-8");
|
|
244
|
-
return true;
|
|
245
|
-
}
|
|
246
|
-
|
|
247
|
-
// src/core/formats/index.ts
|
|
248
|
-
async function readConfig(filePath, format) {
|
|
249
|
-
debug(`reading config: ${filePath} (format: ${format})`);
|
|
250
|
-
switch (format) {
|
|
251
|
-
case "json":
|
|
252
|
-
case "jsonc":
|
|
253
|
-
return readJsonConfig(filePath);
|
|
254
|
-
case "yaml":
|
|
255
|
-
return readYamlConfig(filePath);
|
|
256
|
-
case "toml":
|
|
257
|
-
return readTomlConfig(filePath);
|
|
258
|
-
default:
|
|
259
|
-
throw new Error(`Unsupported config format: ${format}`);
|
|
260
|
-
}
|
|
261
|
-
}
|
|
262
|
-
async function writeConfig(filePath, format, key, serverName, serverConfig) {
|
|
263
|
-
debug(`writing config: ${filePath} (format: ${format}, key: ${key}, server: ${serverName})`);
|
|
264
|
-
switch (format) {
|
|
265
|
-
case "json":
|
|
266
|
-
case "jsonc":
|
|
267
|
-
return writeJsonConfig(filePath, key, serverName, serverConfig);
|
|
268
|
-
case "yaml":
|
|
269
|
-
return writeYamlConfig(filePath, key, serverName, serverConfig);
|
|
270
|
-
case "toml":
|
|
271
|
-
return writeTomlConfig(filePath, key, serverName, serverConfig);
|
|
272
|
-
default:
|
|
273
|
-
throw new Error(`Unsupported config format: ${format}`);
|
|
274
|
-
}
|
|
275
|
-
}
|
|
276
|
-
async function removeConfig(filePath, format, key, serverName) {
|
|
277
|
-
switch (format) {
|
|
278
|
-
case "json":
|
|
279
|
-
case "jsonc":
|
|
280
|
-
return removeJsonConfig(filePath, key, serverName);
|
|
281
|
-
case "yaml":
|
|
282
|
-
return removeYamlConfig(filePath, key, serverName);
|
|
283
|
-
case "toml":
|
|
284
|
-
return removeTomlConfig(filePath, key, serverName);
|
|
285
|
-
default:
|
|
286
|
-
throw new Error(`Unsupported config format: ${format}`);
|
|
287
|
-
}
|
|
181
|
+
function resetDetectionCache() {
|
|
182
|
+
detectionCache = null;
|
|
288
183
|
}
|
|
289
184
|
|
|
290
|
-
// src/core/
|
|
291
|
-
import { existsSync as
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
const configPath = resolveConfigPath(provider, scope, projectDir);
|
|
297
|
-
debug(`listing MCP servers for ${provider.id} (${scope}) at ${configPath ?? "(none)"}`);
|
|
298
|
-
if (!configPath || !existsSync4(configPath)) return [];
|
|
299
|
-
try {
|
|
300
|
-
const config = await readConfig(configPath, provider.configFormat);
|
|
301
|
-
const servers = getNestedValue(config, provider.configKey);
|
|
302
|
-
if (!servers || typeof servers !== "object") return [];
|
|
303
|
-
const entries = [];
|
|
304
|
-
for (const [name, cfg] of Object.entries(servers)) {
|
|
305
|
-
entries.push({
|
|
306
|
-
name,
|
|
307
|
-
providerId: provider.id,
|
|
308
|
-
providerName: provider.toolName,
|
|
309
|
-
scope,
|
|
310
|
-
configPath,
|
|
311
|
-
config: cfg ?? {}
|
|
312
|
-
});
|
|
313
|
-
}
|
|
314
|
-
return entries;
|
|
315
|
-
} catch {
|
|
316
|
-
return [];
|
|
317
|
-
}
|
|
185
|
+
// src/core/skills/installer.ts
|
|
186
|
+
import { existsSync as existsSync2, lstatSync } from "fs";
|
|
187
|
+
import { cp, mkdir, rm, symlink } from "fs/promises";
|
|
188
|
+
import { join as join2 } from "path";
|
|
189
|
+
async function ensureCanonicalDir() {
|
|
190
|
+
await mkdir(getCanonicalSkillsDir(), { recursive: true });
|
|
318
191
|
}
|
|
319
|
-
async function
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
192
|
+
async function installToCanonical(sourcePath, skillName) {
|
|
193
|
+
await ensureCanonicalDir();
|
|
194
|
+
const targetDir = join2(getCanonicalSkillsDir(), skillName);
|
|
195
|
+
await rm(targetDir, { recursive: true, force: true });
|
|
323
196
|
try {
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
if (
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
providerId: ".agents",
|
|
332
|
-
providerName: ".agents/ standard",
|
|
333
|
-
scope,
|
|
334
|
-
configPath: serversPath,
|
|
335
|
-
config: cfg ?? {}
|
|
336
|
-
});
|
|
197
|
+
await cp(sourcePath, targetDir, { recursive: true });
|
|
198
|
+
} catch (err) {
|
|
199
|
+
if (err && typeof err === "object" && "code" in err && err.code === "EEXIST") {
|
|
200
|
+
await rm(targetDir, { recursive: true, force: true });
|
|
201
|
+
await cp(sourcePath, targetDir, { recursive: true });
|
|
202
|
+
} else {
|
|
203
|
+
throw err;
|
|
337
204
|
}
|
|
338
|
-
return entries;
|
|
339
|
-
} catch {
|
|
340
|
-
return [];
|
|
341
205
|
}
|
|
206
|
+
return targetDir;
|
|
342
207
|
}
|
|
343
|
-
async function
|
|
344
|
-
const
|
|
345
|
-
const
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
if (agentsEntries.length > 0) {
|
|
349
|
-
allEntries.push(...agentsEntries);
|
|
350
|
-
seen.add(agentsServersPath);
|
|
208
|
+
async function linkToAgent(canonicalPath, provider, skillName, isGlobal, projectDir) {
|
|
209
|
+
const scope = isGlobal ? "global" : "project";
|
|
210
|
+
const targetDirs = resolveProviderSkillsDirs(provider, scope, projectDir);
|
|
211
|
+
if (targetDirs.length === 0) {
|
|
212
|
+
return { success: false, error: `Provider ${provider.id} has no skills directory` };
|
|
351
213
|
}
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
214
|
+
const errors = [];
|
|
215
|
+
let anySuccess = false;
|
|
216
|
+
for (const targetSkillsDir of targetDirs) {
|
|
217
|
+
if (!targetSkillsDir) continue;
|
|
218
|
+
try {
|
|
219
|
+
await mkdir(targetSkillsDir, { recursive: true });
|
|
220
|
+
const linkPath = join2(targetSkillsDir, skillName);
|
|
221
|
+
if (existsSync2(linkPath)) {
|
|
222
|
+
const stat2 = lstatSync(linkPath);
|
|
223
|
+
if (stat2.isSymbolicLink()) {
|
|
224
|
+
await rm(linkPath);
|
|
225
|
+
} else {
|
|
226
|
+
await rm(linkPath, { recursive: true });
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
const symlinkType = process.platform === "win32" ? "junction" : "dir";
|
|
230
|
+
try {
|
|
231
|
+
await symlink(canonicalPath, linkPath, symlinkType);
|
|
232
|
+
} catch {
|
|
233
|
+
await cp(canonicalPath, linkPath, { recursive: true });
|
|
234
|
+
}
|
|
235
|
+
anySuccess = true;
|
|
236
|
+
} catch (err) {
|
|
237
|
+
errors.push(err instanceof Error ? err.message : String(err));
|
|
238
|
+
}
|
|
358
239
|
}
|
|
359
|
-
|
|
360
|
-
}
|
|
361
|
-
async function removeMcpServer(provider, serverName, scope, projectDir) {
|
|
362
|
-
const configPath = resolveConfigPath(provider, scope, projectDir);
|
|
363
|
-
if (!configPath) return false;
|
|
364
|
-
return removeConfig(configPath, provider.configFormat, provider.configKey, serverName);
|
|
365
|
-
}
|
|
366
|
-
|
|
367
|
-
// src/core/mcp/transforms.ts
|
|
368
|
-
function transformGoose(serverName, config) {
|
|
369
|
-
if (config.url) {
|
|
370
|
-
const transport = config.type === "sse" ? "sse" : "streamable_http";
|
|
371
|
-
return {
|
|
372
|
-
name: serverName,
|
|
373
|
-
type: transport,
|
|
374
|
-
uri: config.url,
|
|
375
|
-
...config.headers ? { headers: config.headers } : {},
|
|
376
|
-
enabled: true,
|
|
377
|
-
timeout: 300
|
|
378
|
-
};
|
|
240
|
+
if (anySuccess) {
|
|
241
|
+
return { success: true };
|
|
379
242
|
}
|
|
380
243
|
return {
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
cmd: config.command,
|
|
384
|
-
args: config.args ?? [],
|
|
385
|
-
...config.env ? { envs: config.env } : {},
|
|
386
|
-
enabled: true,
|
|
387
|
-
timeout: 300
|
|
244
|
+
success: false,
|
|
245
|
+
error: errors.join("; ") || `Provider ${provider.id} has no skills directory`
|
|
388
246
|
};
|
|
389
247
|
}
|
|
390
|
-
function
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
248
|
+
async function installSkill(sourcePath, skillName, providers, isGlobal, projectDir) {
|
|
249
|
+
const errors = [];
|
|
250
|
+
const linkedAgents = [];
|
|
251
|
+
const canonicalPath = await installToCanonical(sourcePath, skillName);
|
|
252
|
+
for (const provider of providers) {
|
|
253
|
+
const result = await linkToAgent(canonicalPath, provider, skillName, isGlobal, projectDir);
|
|
254
|
+
if (result.success) {
|
|
255
|
+
linkedAgents.push(provider.id);
|
|
256
|
+
} else if (result.error) {
|
|
257
|
+
errors.push(`${provider.id}: ${result.error}`);
|
|
258
|
+
}
|
|
398
259
|
}
|
|
399
260
|
return {
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
261
|
+
name: skillName,
|
|
262
|
+
canonicalPath,
|
|
263
|
+
linkedAgents,
|
|
264
|
+
errors,
|
|
265
|
+
success: linkedAgents.length > 0
|
|
404
266
|
};
|
|
405
267
|
}
|
|
406
|
-
function
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
268
|
+
async function removeSkill(skillName, providers, isGlobal, projectDir) {
|
|
269
|
+
const removed = [];
|
|
270
|
+
const errors = [];
|
|
271
|
+
for (const provider of providers) {
|
|
272
|
+
const scope = isGlobal ? "global" : "project";
|
|
273
|
+
const targetDirs = resolveProviderSkillsDirs(provider, scope, projectDir);
|
|
274
|
+
let providerRemoved = false;
|
|
275
|
+
for (const skillsDir of targetDirs) {
|
|
276
|
+
if (!skillsDir) continue;
|
|
277
|
+
const linkPath = join2(skillsDir, skillName);
|
|
278
|
+
if (existsSync2(linkPath)) {
|
|
279
|
+
try {
|
|
280
|
+
await rm(linkPath, { recursive: true });
|
|
281
|
+
providerRemoved = true;
|
|
282
|
+
} catch (err) {
|
|
283
|
+
errors.push(`${provider.id}: ${err instanceof Error ? err.message : String(err)}`);
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
if (providerRemoved) {
|
|
288
|
+
removed.push(provider.id);
|
|
289
|
+
}
|
|
414
290
|
}
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
}
|
|
422
|
-
function transformCodex(_serverName, config) {
|
|
423
|
-
if (config.url) {
|
|
424
|
-
return {
|
|
425
|
-
type: config.type ?? "http",
|
|
426
|
-
url: config.url,
|
|
427
|
-
...config.headers ? { headers: config.headers } : {}
|
|
428
|
-
};
|
|
291
|
+
const canonicalPath = join2(getCanonicalSkillsDir(), skillName);
|
|
292
|
+
if (existsSync2(canonicalPath)) {
|
|
293
|
+
try {
|
|
294
|
+
await rm(canonicalPath, { recursive: true });
|
|
295
|
+
} catch (err) {
|
|
296
|
+
errors.push(`canonical: ${err instanceof Error ? err.message : String(err)}`);
|
|
297
|
+
}
|
|
429
298
|
}
|
|
430
|
-
return {
|
|
431
|
-
command: config.command,
|
|
432
|
-
args: config.args ?? [],
|
|
433
|
-
...config.env ? { env: config.env } : {}
|
|
434
|
-
};
|
|
299
|
+
return { removed, errors };
|
|
435
300
|
}
|
|
436
|
-
function
|
|
437
|
-
if (
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
};
|
|
442
|
-
}
|
|
443
|
-
return config;
|
|
444
|
-
}
|
|
445
|
-
function getTransform(providerId) {
|
|
446
|
-
switch (providerId) {
|
|
447
|
-
case "goose":
|
|
448
|
-
return transformGoose;
|
|
449
|
-
case "zed":
|
|
450
|
-
return transformZed;
|
|
451
|
-
case "opencode":
|
|
452
|
-
return transformOpenCode;
|
|
453
|
-
case "codex":
|
|
454
|
-
return transformCodex;
|
|
455
|
-
case "cursor":
|
|
456
|
-
return transformCursor;
|
|
457
|
-
default:
|
|
458
|
-
return void 0;
|
|
459
|
-
}
|
|
301
|
+
async function listCanonicalSkills() {
|
|
302
|
+
if (!existsSync2(getCanonicalSkillsDir())) return [];
|
|
303
|
+
const { readdir: readdir2 } = await import("fs/promises");
|
|
304
|
+
const entries = await readdir2(getCanonicalSkillsDir(), { withFileTypes: true });
|
|
305
|
+
return entries.filter((e) => e.isDirectory() || e.isSymbolicLink()).map((e) => e.name);
|
|
460
306
|
}
|
|
461
307
|
|
|
462
|
-
// src/core/
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
const transform = getTransform(provider.id);
|
|
486
|
-
debug(` transform applied: ${transform ? "yes" : "no"}`);
|
|
487
|
-
await writeConfig(
|
|
488
|
-
configPath,
|
|
489
|
-
provider.configFormat,
|
|
490
|
-
provider.configKey,
|
|
491
|
-
serverName,
|
|
492
|
-
transformedConfig
|
|
493
|
-
);
|
|
494
|
-
return {
|
|
495
|
-
provider,
|
|
496
|
-
scope,
|
|
497
|
-
configPath,
|
|
498
|
-
success: true
|
|
499
|
-
};
|
|
500
|
-
} catch (err) {
|
|
501
|
-
return {
|
|
502
|
-
provider,
|
|
503
|
-
scope,
|
|
504
|
-
configPath,
|
|
505
|
-
success: false,
|
|
506
|
-
error: err instanceof Error ? err.message : String(err)
|
|
507
|
-
};
|
|
508
|
-
}
|
|
308
|
+
// src/core/advanced/orchestration.ts
|
|
309
|
+
import { existsSync as existsSync3, lstatSync as lstatSync2 } from "fs";
|
|
310
|
+
import { cp as cp2, mkdir as mkdir2, readlink, rm as rm2, symlink as symlink2 } from "fs/promises";
|
|
311
|
+
import { tmpdir } from "os";
|
|
312
|
+
import { basename, dirname, join as join3 } from "path";
|
|
313
|
+
|
|
314
|
+
// src/core/paths/agents.ts
|
|
315
|
+
var AGENTS_HOME = getAgentsHome();
|
|
316
|
+
var LOCK_FILE_PATH = getLockFilePath();
|
|
317
|
+
var CANONICAL_SKILLS_DIR = getCanonicalSkillsDir();
|
|
318
|
+
var AGENTS_MCP_DIR = getAgentsMcpDir();
|
|
319
|
+
var AGENTS_MCP_SERVERS_PATH = getAgentsMcpServersPath();
|
|
320
|
+
var AGENTS_CONFIG_PATH = getAgentsConfigPath();
|
|
321
|
+
|
|
322
|
+
// src/core/advanced/orchestration.ts
|
|
323
|
+
var PRIORITY_ORDER = {
|
|
324
|
+
high: 0,
|
|
325
|
+
medium: 1,
|
|
326
|
+
low: 2
|
|
327
|
+
};
|
|
328
|
+
function selectProvidersByMinimumPriority(providers, minimumPriority = "low") {
|
|
329
|
+
const maxRank = PRIORITY_ORDER[minimumPriority];
|
|
330
|
+
return [...providers].filter((provider) => PRIORITY_ORDER[provider.priority] <= maxRank).sort((a, b) => PRIORITY_ORDER[a.priority] - PRIORITY_ORDER[b.priority]);
|
|
509
331
|
}
|
|
510
|
-
|
|
511
|
-
const
|
|
512
|
-
|
|
513
|
-
const result = await installMcpServer(provider, serverName, config, scope, projectDir);
|
|
514
|
-
results.push(result);
|
|
515
|
-
}
|
|
516
|
-
return results;
|
|
332
|
+
function resolveSkillLinkPath(provider, skillName, isGlobal, projectDir) {
|
|
333
|
+
const skillDir = isGlobal ? provider.pathSkills : join3(projectDir, provider.pathProjectSkills);
|
|
334
|
+
return join3(skillDir, skillName);
|
|
517
335
|
}
|
|
518
|
-
function
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
return {
|
|
528
|
-
command: "npx",
|
|
529
|
-
args: ["-y", source.value]
|
|
530
|
-
};
|
|
531
|
-
}
|
|
532
|
-
const parts = source.value.trim().split(/\s+/);
|
|
533
|
-
const command = parts[0] ?? source.value;
|
|
534
|
-
return {
|
|
535
|
-
command,
|
|
536
|
-
args: parts.slice(1)
|
|
537
|
-
};
|
|
538
|
-
}
|
|
539
|
-
|
|
540
|
-
// src/core/registry/detection.ts
|
|
541
|
-
import { execFileSync } from "child_process";
|
|
542
|
-
import { existsSync as existsSync5 } from "fs";
|
|
543
|
-
import { join } from "path";
|
|
544
|
-
var DEFAULT_DETECTION_CACHE_TTL_MS = 3e4;
|
|
545
|
-
var detectionCache = null;
|
|
546
|
-
function checkBinary(binary) {
|
|
547
|
-
try {
|
|
548
|
-
const cmd = process.platform === "win32" ? "where" : "which";
|
|
549
|
-
execFileSync(cmd, [binary], { stdio: "pipe" });
|
|
550
|
-
return true;
|
|
551
|
-
} catch {
|
|
552
|
-
return false;
|
|
553
|
-
}
|
|
554
|
-
}
|
|
555
|
-
function checkDirectory(dir) {
|
|
556
|
-
return existsSync5(dir);
|
|
557
|
-
}
|
|
558
|
-
function checkAppBundle(appName) {
|
|
559
|
-
if (process.platform !== "darwin") return false;
|
|
560
|
-
const applications = getPlatformLocations().applications;
|
|
561
|
-
return applications.some((base) => existsSync5(join(base, appName)));
|
|
562
|
-
}
|
|
563
|
-
function checkFlatpak(flatpakId) {
|
|
564
|
-
if (process.platform !== "linux") return false;
|
|
565
|
-
try {
|
|
566
|
-
execFileSync("flatpak", ["info", flatpakId], { stdio: "pipe" });
|
|
567
|
-
return true;
|
|
568
|
-
} catch {
|
|
569
|
-
return false;
|
|
336
|
+
async function snapshotSkillState(providerTargets, operation, projectDir, backupRoot) {
|
|
337
|
+
const skillName = operation.skillName;
|
|
338
|
+
const isGlobal = operation.isGlobal ?? true;
|
|
339
|
+
const canonicalPath = join3(CANONICAL_SKILLS_DIR, skillName);
|
|
340
|
+
const canonicalExisted = existsSync3(canonicalPath);
|
|
341
|
+
const canonicalBackupPath = join3(backupRoot, "canonical", skillName);
|
|
342
|
+
if (canonicalExisted) {
|
|
343
|
+
await mkdir2(dirname(canonicalBackupPath), { recursive: true });
|
|
344
|
+
await cp2(canonicalPath, canonicalBackupPath, { recursive: true });
|
|
570
345
|
}
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
break;
|
|
594
|
-
case "appBundle":
|
|
595
|
-
if (detection.appBundle && checkAppBundle(detection.appBundle)) {
|
|
596
|
-
matchedMethods.push("appBundle");
|
|
597
|
-
}
|
|
598
|
-
break;
|
|
599
|
-
case "flatpak":
|
|
600
|
-
if (detection.flatpakId && checkFlatpak(detection.flatpakId)) {
|
|
601
|
-
matchedMethods.push("flatpak");
|
|
602
|
-
}
|
|
603
|
-
break;
|
|
346
|
+
const pathSnapshots = [];
|
|
347
|
+
for (const provider of providerTargets) {
|
|
348
|
+
const linkPath = resolveSkillLinkPath(provider, skillName, isGlobal, projectDir);
|
|
349
|
+
if (!existsSync3(linkPath)) {
|
|
350
|
+
pathSnapshots.push({ linkPath, state: "missing" });
|
|
351
|
+
continue;
|
|
352
|
+
}
|
|
353
|
+
const stat2 = lstatSync2(linkPath);
|
|
354
|
+
if (stat2.isSymbolicLink()) {
|
|
355
|
+
pathSnapshots.push({
|
|
356
|
+
linkPath,
|
|
357
|
+
state: "symlink",
|
|
358
|
+
symlinkTarget: await readlink(linkPath)
|
|
359
|
+
});
|
|
360
|
+
continue;
|
|
361
|
+
}
|
|
362
|
+
const backupPath = join3(backupRoot, "links", provider.id, `${skillName}-${basename(linkPath)}`);
|
|
363
|
+
await mkdir2(dirname(backupPath), { recursive: true });
|
|
364
|
+
if (stat2.isDirectory()) {
|
|
365
|
+
await cp2(linkPath, backupPath, { recursive: true });
|
|
366
|
+
pathSnapshots.push({ linkPath, state: "directory", backupPath });
|
|
367
|
+
continue;
|
|
604
368
|
}
|
|
369
|
+
await cp2(linkPath, backupPath);
|
|
370
|
+
pathSnapshots.push({ linkPath, state: "file", backupPath });
|
|
605
371
|
}
|
|
606
372
|
return {
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
function providerSignature(provider) {
|
|
614
|
-
return JSON.stringify({
|
|
615
|
-
id: provider.id,
|
|
616
|
-
methods: provider.detection.methods,
|
|
617
|
-
binary: provider.detection.binary,
|
|
618
|
-
directories: provider.detection.directories,
|
|
619
|
-
appBundle: provider.detection.appBundle,
|
|
620
|
-
flatpakId: provider.detection.flatpakId
|
|
621
|
-
});
|
|
622
|
-
}
|
|
623
|
-
function buildProvidersSignature(providers) {
|
|
624
|
-
if (!providers || !Array.isArray(providers)) return "";
|
|
625
|
-
return providers.map(providerSignature).join("|");
|
|
626
|
-
}
|
|
627
|
-
function cloneDetectionResults(results) {
|
|
628
|
-
return results.map((result) => ({
|
|
629
|
-
provider: result.provider,
|
|
630
|
-
installed: result.installed,
|
|
631
|
-
methods: [...result.methods],
|
|
632
|
-
projectDetected: result.projectDetected
|
|
633
|
-
}));
|
|
634
|
-
}
|
|
635
|
-
function getCachedResults(signature, options) {
|
|
636
|
-
if (!detectionCache || options.forceRefresh) return null;
|
|
637
|
-
if (detectionCache.signature !== signature) return null;
|
|
638
|
-
const ttlMs = options.ttlMs ?? DEFAULT_DETECTION_CACHE_TTL_MS;
|
|
639
|
-
if (ttlMs <= 0) return null;
|
|
640
|
-
if (Date.now() - detectionCache.createdAt > ttlMs) return null;
|
|
641
|
-
return cloneDetectionResults(detectionCache.results);
|
|
642
|
-
}
|
|
643
|
-
function setCachedResults(signature, results) {
|
|
644
|
-
detectionCache = {
|
|
645
|
-
createdAt: Date.now(),
|
|
646
|
-
signature,
|
|
647
|
-
results: cloneDetectionResults(results)
|
|
373
|
+
skillName,
|
|
374
|
+
isGlobal,
|
|
375
|
+
canonicalPath,
|
|
376
|
+
canonicalBackupPath: canonicalExisted ? canonicalBackupPath : void 0,
|
|
377
|
+
canonicalExisted,
|
|
378
|
+
pathSnapshots
|
|
648
379
|
};
|
|
649
380
|
}
|
|
650
|
-
function
|
|
651
|
-
if (
|
|
652
|
-
|
|
653
|
-
}
|
|
654
|
-
function detectAllProviders(options = {}) {
|
|
655
|
-
const providers = getAllProviders() ?? [];
|
|
656
|
-
const signature = buildProvidersSignature(providers);
|
|
657
|
-
const cached = getCachedResults(signature, options);
|
|
658
|
-
if (cached) {
|
|
659
|
-
debug(`detection cache hit for ${providers.length} providers`);
|
|
660
|
-
return cached;
|
|
661
|
-
}
|
|
662
|
-
const results = providers.map(detectProvider);
|
|
663
|
-
setCachedResults(signature, results);
|
|
664
|
-
return cloneDetectionResults(results);
|
|
665
|
-
}
|
|
666
|
-
function getInstalledProviders(options = {}) {
|
|
667
|
-
return detectAllProviders(options).filter((r) => r.installed).map((r) => r.provider);
|
|
668
|
-
}
|
|
669
|
-
function detectProjectProviders(projectDir, options = {}) {
|
|
670
|
-
const results = detectAllProviders(options);
|
|
671
|
-
return results.map((r) => ({
|
|
672
|
-
...r,
|
|
673
|
-
projectDetected: detectProjectProvider(r.provider, projectDir)
|
|
674
|
-
}));
|
|
675
|
-
}
|
|
676
|
-
function resetDetectionCache() {
|
|
677
|
-
detectionCache = null;
|
|
678
|
-
}
|
|
679
|
-
|
|
680
|
-
// src/core/skills/installer.ts
|
|
681
|
-
import { existsSync as existsSync6, lstatSync } from "fs";
|
|
682
|
-
import { cp, mkdir, rm, symlink } from "fs/promises";
|
|
683
|
-
import { join as join2 } from "path";
|
|
684
|
-
async function ensureCanonicalDir() {
|
|
685
|
-
await mkdir(getCanonicalSkillsDir(), { recursive: true });
|
|
686
|
-
}
|
|
687
|
-
async function installToCanonical(sourcePath, skillName) {
|
|
688
|
-
await ensureCanonicalDir();
|
|
689
|
-
const targetDir = join2(getCanonicalSkillsDir(), skillName);
|
|
690
|
-
await rm(targetDir, { recursive: true, force: true });
|
|
691
|
-
try {
|
|
692
|
-
await cp(sourcePath, targetDir, { recursive: true });
|
|
693
|
-
} catch (err) {
|
|
694
|
-
if (err && typeof err === "object" && "code" in err && err.code === "EEXIST") {
|
|
695
|
-
await rm(targetDir, { recursive: true, force: true });
|
|
696
|
-
await cp(sourcePath, targetDir, { recursive: true });
|
|
697
|
-
} else {
|
|
698
|
-
throw err;
|
|
699
|
-
}
|
|
381
|
+
async function restoreSkillSnapshot(snapshot) {
|
|
382
|
+
if (existsSync3(snapshot.canonicalPath)) {
|
|
383
|
+
await rm2(snapshot.canonicalPath, { recursive: true, force: true });
|
|
700
384
|
}
|
|
701
|
-
|
|
702
|
-
}
|
|
703
|
-
|
|
704
|
-
const scope = isGlobal ? "global" : "project";
|
|
705
|
-
const targetDirs = resolveProviderSkillsDirs(provider, scope, projectDir);
|
|
706
|
-
if (targetDirs.length === 0) {
|
|
707
|
-
return { success: false, error: `Provider ${provider.id} has no skills directory` };
|
|
385
|
+
if (snapshot.canonicalExisted && snapshot.canonicalBackupPath && existsSync3(snapshot.canonicalBackupPath)) {
|
|
386
|
+
await mkdir2(dirname(snapshot.canonicalPath), { recursive: true });
|
|
387
|
+
await cp2(snapshot.canonicalBackupPath, snapshot.canonicalPath, { recursive: true });
|
|
708
388
|
}
|
|
709
|
-
const
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
}
|
|
724
|
-
const symlinkType = process.platform === "win32" ? "junction" : "dir";
|
|
725
|
-
try {
|
|
726
|
-
await symlink(canonicalPath, linkPath, symlinkType);
|
|
727
|
-
} catch {
|
|
728
|
-
await cp(canonicalPath, linkPath, { recursive: true });
|
|
729
|
-
}
|
|
730
|
-
anySuccess = true;
|
|
731
|
-
} catch (err) {
|
|
732
|
-
errors.push(err instanceof Error ? err.message : String(err));
|
|
733
|
-
}
|
|
734
|
-
}
|
|
735
|
-
if (anySuccess) {
|
|
736
|
-
return { success: true };
|
|
737
|
-
}
|
|
738
|
-
return {
|
|
739
|
-
success: false,
|
|
740
|
-
error: errors.join("; ") || `Provider ${provider.id} has no skills directory`
|
|
741
|
-
};
|
|
742
|
-
}
|
|
743
|
-
async function installSkill(sourcePath, skillName, providers, isGlobal, projectDir) {
|
|
744
|
-
const errors = [];
|
|
745
|
-
const linkedAgents = [];
|
|
746
|
-
const canonicalPath = await installToCanonical(sourcePath, skillName);
|
|
747
|
-
for (const provider of providers) {
|
|
748
|
-
const result = await linkToAgent(canonicalPath, provider, skillName, isGlobal, projectDir);
|
|
749
|
-
if (result.success) {
|
|
750
|
-
linkedAgents.push(provider.id);
|
|
751
|
-
} else if (result.error) {
|
|
752
|
-
errors.push(`${provider.id}: ${result.error}`);
|
|
753
|
-
}
|
|
754
|
-
}
|
|
755
|
-
return {
|
|
756
|
-
name: skillName,
|
|
757
|
-
canonicalPath,
|
|
758
|
-
linkedAgents,
|
|
759
|
-
errors,
|
|
760
|
-
success: linkedAgents.length > 0
|
|
761
|
-
};
|
|
762
|
-
}
|
|
763
|
-
async function removeSkill(skillName, providers, isGlobal, projectDir) {
|
|
764
|
-
const removed = [];
|
|
765
|
-
const errors = [];
|
|
766
|
-
for (const provider of providers) {
|
|
767
|
-
const scope = isGlobal ? "global" : "project";
|
|
768
|
-
const targetDirs = resolveProviderSkillsDirs(provider, scope, projectDir);
|
|
769
|
-
let providerRemoved = false;
|
|
770
|
-
for (const skillsDir of targetDirs) {
|
|
771
|
-
if (!skillsDir) continue;
|
|
772
|
-
const linkPath = join2(skillsDir, skillName);
|
|
773
|
-
if (existsSync6(linkPath)) {
|
|
774
|
-
try {
|
|
775
|
-
await rm(linkPath, { recursive: true });
|
|
776
|
-
providerRemoved = true;
|
|
777
|
-
} catch (err) {
|
|
778
|
-
errors.push(`${provider.id}: ${err instanceof Error ? err.message : String(err)}`);
|
|
779
|
-
}
|
|
780
|
-
}
|
|
781
|
-
}
|
|
782
|
-
if (providerRemoved) {
|
|
783
|
-
removed.push(provider.id);
|
|
784
|
-
}
|
|
785
|
-
}
|
|
786
|
-
const canonicalPath = join2(getCanonicalSkillsDir(), skillName);
|
|
787
|
-
if (existsSync6(canonicalPath)) {
|
|
788
|
-
try {
|
|
789
|
-
await rm(canonicalPath, { recursive: true });
|
|
790
|
-
} catch (err) {
|
|
791
|
-
errors.push(`canonical: ${err instanceof Error ? err.message : String(err)}`);
|
|
792
|
-
}
|
|
793
|
-
}
|
|
794
|
-
return { removed, errors };
|
|
795
|
-
}
|
|
796
|
-
async function listCanonicalSkills() {
|
|
797
|
-
if (!existsSync6(getCanonicalSkillsDir())) return [];
|
|
798
|
-
const { readdir: readdir2 } = await import("fs/promises");
|
|
799
|
-
const entries = await readdir2(getCanonicalSkillsDir(), { withFileTypes: true });
|
|
800
|
-
return entries.filter((e) => e.isDirectory() || e.isSymbolicLink()).map((e) => e.name);
|
|
801
|
-
}
|
|
802
|
-
|
|
803
|
-
// src/core/advanced/orchestration.ts
|
|
804
|
-
import { existsSync as existsSync7, lstatSync as lstatSync2 } from "fs";
|
|
805
|
-
import { cp as cp2, mkdir as mkdir2, readFile as readFile4, readlink, rm as rm2, symlink as symlink2, writeFile as writeFile4 } from "fs/promises";
|
|
806
|
-
import { tmpdir } from "os";
|
|
807
|
-
import { basename, dirname, join as join3 } from "path";
|
|
808
|
-
|
|
809
|
-
// src/core/paths/agents.ts
|
|
810
|
-
var AGENTS_HOME = getAgentsHome();
|
|
811
|
-
var LOCK_FILE_PATH = getLockFilePath();
|
|
812
|
-
var CANONICAL_SKILLS_DIR = getCanonicalSkillsDir();
|
|
813
|
-
var AGENTS_MCP_DIR = getAgentsMcpDir();
|
|
814
|
-
var AGENTS_MCP_SERVERS_PATH = getAgentsMcpServersPath();
|
|
815
|
-
var AGENTS_CONFIG_PATH = getAgentsConfigPath();
|
|
816
|
-
|
|
817
|
-
// src/core/advanced/orchestration.ts
|
|
818
|
-
var PRIORITY_ORDER = {
|
|
819
|
-
high: 0,
|
|
820
|
-
medium: 1,
|
|
821
|
-
low: 2
|
|
822
|
-
};
|
|
823
|
-
function selectProvidersByMinimumPriority(providers, minimumPriority = "low") {
|
|
824
|
-
const maxRank = PRIORITY_ORDER[minimumPriority];
|
|
825
|
-
return [...providers].filter((provider) => PRIORITY_ORDER[provider.priority] <= maxRank).sort((a, b) => PRIORITY_ORDER[a.priority] - PRIORITY_ORDER[b.priority]);
|
|
826
|
-
}
|
|
827
|
-
function resolveSkillLinkPath(provider, skillName, isGlobal, projectDir) {
|
|
828
|
-
const skillDir = isGlobal ? provider.pathSkills : join3(projectDir, provider.pathProjectSkills);
|
|
829
|
-
return join3(skillDir, skillName);
|
|
830
|
-
}
|
|
831
|
-
async function snapshotConfigs(paths) {
|
|
832
|
-
const snapshots = /* @__PURE__ */ new Map();
|
|
833
|
-
for (const path of paths) {
|
|
834
|
-
if (!path || snapshots.has(path)) continue;
|
|
835
|
-
if (!existsSync7(path)) {
|
|
836
|
-
snapshots.set(path, null);
|
|
837
|
-
continue;
|
|
838
|
-
}
|
|
839
|
-
snapshots.set(path, await readFile4(path, "utf-8"));
|
|
840
|
-
}
|
|
841
|
-
return snapshots;
|
|
842
|
-
}
|
|
843
|
-
async function restoreConfigSnapshots(snapshots) {
|
|
844
|
-
for (const [path, content] of snapshots) {
|
|
845
|
-
if (content === null) {
|
|
846
|
-
await rm2(path, { force: true });
|
|
847
|
-
continue;
|
|
848
|
-
}
|
|
849
|
-
await mkdir2(dirname(path), { recursive: true });
|
|
850
|
-
await writeFile4(path, content, "utf-8");
|
|
851
|
-
}
|
|
852
|
-
}
|
|
853
|
-
async function snapshotSkillState(providerTargets, operation, projectDir, backupRoot) {
|
|
854
|
-
const skillName = operation.skillName;
|
|
855
|
-
const isGlobal = operation.isGlobal ?? true;
|
|
856
|
-
const canonicalPath = join3(CANONICAL_SKILLS_DIR, skillName);
|
|
857
|
-
const canonicalExisted = existsSync7(canonicalPath);
|
|
858
|
-
const canonicalBackupPath = join3(backupRoot, "canonical", skillName);
|
|
859
|
-
if (canonicalExisted) {
|
|
860
|
-
await mkdir2(dirname(canonicalBackupPath), { recursive: true });
|
|
861
|
-
await cp2(canonicalPath, canonicalBackupPath, { recursive: true });
|
|
862
|
-
}
|
|
863
|
-
const pathSnapshots = [];
|
|
864
|
-
for (const provider of providerTargets) {
|
|
865
|
-
const linkPath = resolveSkillLinkPath(provider, skillName, isGlobal, projectDir);
|
|
866
|
-
if (!existsSync7(linkPath)) {
|
|
867
|
-
pathSnapshots.push({ linkPath, state: "missing" });
|
|
868
|
-
continue;
|
|
869
|
-
}
|
|
870
|
-
const stat2 = lstatSync2(linkPath);
|
|
871
|
-
if (stat2.isSymbolicLink()) {
|
|
872
|
-
pathSnapshots.push({
|
|
873
|
-
linkPath,
|
|
874
|
-
state: "symlink",
|
|
875
|
-
symlinkTarget: await readlink(linkPath)
|
|
876
|
-
});
|
|
877
|
-
continue;
|
|
878
|
-
}
|
|
879
|
-
const backupPath = join3(backupRoot, "links", provider.id, `${skillName}-${basename(linkPath)}`);
|
|
880
|
-
await mkdir2(dirname(backupPath), { recursive: true });
|
|
881
|
-
if (stat2.isDirectory()) {
|
|
882
|
-
await cp2(linkPath, backupPath, { recursive: true });
|
|
883
|
-
pathSnapshots.push({ linkPath, state: "directory", backupPath });
|
|
884
|
-
continue;
|
|
885
|
-
}
|
|
886
|
-
await cp2(linkPath, backupPath);
|
|
887
|
-
pathSnapshots.push({ linkPath, state: "file", backupPath });
|
|
888
|
-
}
|
|
889
|
-
return {
|
|
890
|
-
skillName,
|
|
891
|
-
isGlobal,
|
|
892
|
-
canonicalPath,
|
|
893
|
-
canonicalBackupPath: canonicalExisted ? canonicalBackupPath : void 0,
|
|
894
|
-
canonicalExisted,
|
|
895
|
-
pathSnapshots
|
|
896
|
-
};
|
|
897
|
-
}
|
|
898
|
-
async function restoreSkillSnapshot(snapshot) {
|
|
899
|
-
if (existsSync7(snapshot.canonicalPath)) {
|
|
900
|
-
await rm2(snapshot.canonicalPath, { recursive: true, force: true });
|
|
901
|
-
}
|
|
902
|
-
if (snapshot.canonicalExisted && snapshot.canonicalBackupPath && existsSync7(snapshot.canonicalBackupPath)) {
|
|
903
|
-
await mkdir2(dirname(snapshot.canonicalPath), { recursive: true });
|
|
904
|
-
await cp2(snapshot.canonicalBackupPath, snapshot.canonicalPath, { recursive: true });
|
|
905
|
-
}
|
|
906
|
-
for (const pathSnapshot of snapshot.pathSnapshots) {
|
|
907
|
-
await rm2(pathSnapshot.linkPath, { recursive: true, force: true });
|
|
908
|
-
if (pathSnapshot.state === "missing") continue;
|
|
909
|
-
await mkdir2(dirname(pathSnapshot.linkPath), { recursive: true });
|
|
910
|
-
if (pathSnapshot.state === "symlink" && pathSnapshot.symlinkTarget) {
|
|
911
|
-
const linkType = process.platform === "win32" ? "junction" : "dir";
|
|
912
|
-
await symlink2(pathSnapshot.symlinkTarget, pathSnapshot.linkPath, linkType);
|
|
913
|
-
continue;
|
|
914
|
-
}
|
|
915
|
-
if ((pathSnapshot.state === "directory" || pathSnapshot.state === "file") && pathSnapshot.backupPath) {
|
|
916
|
-
if (pathSnapshot.state === "directory") {
|
|
917
|
-
await cp2(pathSnapshot.backupPath, pathSnapshot.linkPath, { recursive: true });
|
|
918
|
-
} else {
|
|
919
|
-
await cp2(pathSnapshot.backupPath, pathSnapshot.linkPath);
|
|
389
|
+
for (const pathSnapshot of snapshot.pathSnapshots) {
|
|
390
|
+
await rm2(pathSnapshot.linkPath, { recursive: true, force: true });
|
|
391
|
+
if (pathSnapshot.state === "missing") continue;
|
|
392
|
+
await mkdir2(dirname(pathSnapshot.linkPath), { recursive: true });
|
|
393
|
+
if (pathSnapshot.state === "symlink" && pathSnapshot.symlinkTarget) {
|
|
394
|
+
const linkType = process.platform === "win32" ? "junction" : "dir";
|
|
395
|
+
await symlink2(pathSnapshot.symlinkTarget, pathSnapshot.linkPath, linkType);
|
|
396
|
+
continue;
|
|
397
|
+
}
|
|
398
|
+
if ((pathSnapshot.state === "directory" || pathSnapshot.state === "file") && pathSnapshot.backupPath) {
|
|
399
|
+
if (pathSnapshot.state === "directory") {
|
|
400
|
+
await cp2(pathSnapshot.backupPath, pathSnapshot.linkPath, { recursive: true });
|
|
401
|
+
} else {
|
|
402
|
+
await cp2(pathSnapshot.backupPath, pathSnapshot.linkPath);
|
|
920
403
|
}
|
|
921
404
|
}
|
|
922
405
|
}
|
|
@@ -924,19 +407,9 @@ async function restoreSkillSnapshot(snapshot) {
|
|
|
924
407
|
async function installBatchWithRollback(options) {
|
|
925
408
|
const projectDir = options.projectDir ?? process.cwd();
|
|
926
409
|
const minimumPriority = options.minimumPriority ?? "low";
|
|
927
|
-
const mcpOps = options.mcp ?? [];
|
|
928
410
|
const skillOps = options.skills ?? [];
|
|
929
411
|
const baseProviders = options.providers ?? getInstalledProviders();
|
|
930
412
|
const providers = selectProvidersByMinimumPriority(baseProviders, minimumPriority);
|
|
931
|
-
const configPaths = providers.flatMap((provider) => {
|
|
932
|
-
const paths = [];
|
|
933
|
-
for (const operation of mcpOps) {
|
|
934
|
-
const path = resolveConfigPath(provider, operation.scope ?? "project", projectDir);
|
|
935
|
-
if (path) paths.push(path);
|
|
936
|
-
}
|
|
937
|
-
return paths;
|
|
938
|
-
});
|
|
939
|
-
const configSnapshots = await snapshotConfigs(configPaths);
|
|
940
413
|
const backupRoot = join3(
|
|
941
414
|
tmpdir(),
|
|
942
415
|
`caamp-skill-backup-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`
|
|
@@ -946,26 +419,9 @@ async function installBatchWithRollback(options) {
|
|
|
946
419
|
);
|
|
947
420
|
const appliedSkills = [];
|
|
948
421
|
const rollbackErrors = [];
|
|
949
|
-
let mcpApplied = 0;
|
|
950
422
|
let skillsApplied = 0;
|
|
951
423
|
let rollbackPerformed = false;
|
|
952
424
|
try {
|
|
953
|
-
for (const operation of mcpOps) {
|
|
954
|
-
const scope = operation.scope ?? "project";
|
|
955
|
-
for (const provider of providers) {
|
|
956
|
-
const result = await installMcpServer(
|
|
957
|
-
provider,
|
|
958
|
-
operation.serverName,
|
|
959
|
-
operation.config,
|
|
960
|
-
scope,
|
|
961
|
-
projectDir
|
|
962
|
-
);
|
|
963
|
-
if (!result.success) {
|
|
964
|
-
throw new Error(result.error ?? `Failed MCP install for ${provider.id}`);
|
|
965
|
-
}
|
|
966
|
-
mcpApplied += 1;
|
|
967
|
-
}
|
|
968
|
-
}
|
|
969
425
|
for (const operation of skillOps) {
|
|
970
426
|
const isGlobal = operation.isGlobal ?? true;
|
|
971
427
|
const result = await installSkill(
|
|
@@ -992,7 +448,6 @@ async function installBatchWithRollback(options) {
|
|
|
992
448
|
return {
|
|
993
449
|
success: true,
|
|
994
450
|
providerIds: providers.map((provider) => provider.id),
|
|
995
|
-
mcpApplied,
|
|
996
451
|
skillsApplied,
|
|
997
452
|
rollbackPerformed: false,
|
|
998
453
|
rollbackErrors: []
|
|
@@ -1006,11 +461,6 @@ async function installBatchWithRollback(options) {
|
|
|
1006
461
|
rollbackErrors.push(err instanceof Error ? err.message : String(err));
|
|
1007
462
|
}
|
|
1008
463
|
}
|
|
1009
|
-
try {
|
|
1010
|
-
await restoreConfigSnapshots(configSnapshots);
|
|
1011
|
-
} catch (err) {
|
|
1012
|
-
rollbackErrors.push(err instanceof Error ? err.message : String(err));
|
|
1013
|
-
}
|
|
1014
464
|
for (const snapshot of skillSnapshots) {
|
|
1015
465
|
try {
|
|
1016
466
|
await restoreSkillSnapshot(snapshot);
|
|
@@ -1022,7 +472,6 @@ async function installBatchWithRollback(options) {
|
|
|
1022
472
|
return {
|
|
1023
473
|
success: false,
|
|
1024
474
|
providerIds: providers.map((provider) => provider.id),
|
|
1025
|
-
mcpApplied,
|
|
1026
475
|
skillsApplied,
|
|
1027
476
|
rollbackPerformed,
|
|
1028
477
|
rollbackErrors,
|
|
@@ -1030,99 +479,6 @@ async function installBatchWithRollback(options) {
|
|
|
1030
479
|
};
|
|
1031
480
|
}
|
|
1032
481
|
}
|
|
1033
|
-
function stableStringify(value) {
|
|
1034
|
-
if (Array.isArray(value)) {
|
|
1035
|
-
return `[${value.map(stableStringify).join(",")}]`;
|
|
1036
|
-
}
|
|
1037
|
-
if (value && typeof value === "object") {
|
|
1038
|
-
const record = value;
|
|
1039
|
-
const keys = Object.keys(record).sort();
|
|
1040
|
-
return `{${keys.map((key) => `${JSON.stringify(key)}:${stableStringify(record[key])}`).join(",")}}`;
|
|
1041
|
-
}
|
|
1042
|
-
return JSON.stringify(value);
|
|
1043
|
-
}
|
|
1044
|
-
async function detectMcpConfigConflicts(providers, operations, projectDir = process.cwd()) {
|
|
1045
|
-
const conflicts = [];
|
|
1046
|
-
for (const provider of providers) {
|
|
1047
|
-
for (const operation of operations) {
|
|
1048
|
-
const scope = operation.scope ?? "project";
|
|
1049
|
-
if (operation.config.type && !provider.supportedTransports.includes(operation.config.type)) {
|
|
1050
|
-
conflicts.push({
|
|
1051
|
-
providerId: provider.id,
|
|
1052
|
-
serverName: operation.serverName,
|
|
1053
|
-
scope,
|
|
1054
|
-
code: "unsupported-transport",
|
|
1055
|
-
message: `${provider.id} does not support transport ${operation.config.type}`
|
|
1056
|
-
});
|
|
1057
|
-
}
|
|
1058
|
-
if (operation.config.headers && !provider.supportsHeaders) {
|
|
1059
|
-
conflicts.push({
|
|
1060
|
-
providerId: provider.id,
|
|
1061
|
-
serverName: operation.serverName,
|
|
1062
|
-
scope,
|
|
1063
|
-
code: "unsupported-headers",
|
|
1064
|
-
message: `${provider.id} does not support header configuration`
|
|
1065
|
-
});
|
|
1066
|
-
}
|
|
1067
|
-
const existingEntries = await listMcpServers(provider, scope, projectDir);
|
|
1068
|
-
const current = existingEntries.find((entry) => entry.name === operation.serverName);
|
|
1069
|
-
if (!current) continue;
|
|
1070
|
-
const transform = getTransform(provider.id);
|
|
1071
|
-
const desired = transform ? transform(operation.serverName, operation.config) : operation.config;
|
|
1072
|
-
if (stableStringify(current.config) !== stableStringify(desired)) {
|
|
1073
|
-
conflicts.push({
|
|
1074
|
-
providerId: provider.id,
|
|
1075
|
-
serverName: operation.serverName,
|
|
1076
|
-
scope,
|
|
1077
|
-
code: "existing-mismatch",
|
|
1078
|
-
message: `${provider.id} has existing config mismatch for ${operation.serverName}`
|
|
1079
|
-
});
|
|
1080
|
-
}
|
|
1081
|
-
}
|
|
1082
|
-
}
|
|
1083
|
-
return conflicts;
|
|
1084
|
-
}
|
|
1085
|
-
async function applyMcpInstallWithPolicy(providers, operations, policy = "fail", projectDir = process.cwd()) {
|
|
1086
|
-
const conflicts = await detectMcpConfigConflicts(providers, operations, projectDir);
|
|
1087
|
-
const conflictKey = (providerId, serverName, scope) => `${providerId}::${serverName}::${scope}`;
|
|
1088
|
-
const conflictMap = /* @__PURE__ */ new Map();
|
|
1089
|
-
for (const conflict of conflicts) {
|
|
1090
|
-
conflictMap.set(
|
|
1091
|
-
conflictKey(conflict.providerId, conflict.serverName, conflict.scope),
|
|
1092
|
-
conflict
|
|
1093
|
-
);
|
|
1094
|
-
}
|
|
1095
|
-
if (policy === "fail" && conflicts.length > 0) {
|
|
1096
|
-
return { conflicts, applied: [], skipped: [] };
|
|
1097
|
-
}
|
|
1098
|
-
const applied = [];
|
|
1099
|
-
const skipped = [];
|
|
1100
|
-
for (const provider of providers) {
|
|
1101
|
-
for (const operation of operations) {
|
|
1102
|
-
const scope = operation.scope ?? "project";
|
|
1103
|
-
const key = conflictKey(provider.id, operation.serverName, scope);
|
|
1104
|
-
const conflict = conflictMap.get(key);
|
|
1105
|
-
if (policy === "skip" && conflict) {
|
|
1106
|
-
skipped.push({
|
|
1107
|
-
providerId: provider.id,
|
|
1108
|
-
serverName: operation.serverName,
|
|
1109
|
-
scope,
|
|
1110
|
-
reason: conflict.code
|
|
1111
|
-
});
|
|
1112
|
-
continue;
|
|
1113
|
-
}
|
|
1114
|
-
const result = await installMcpServer(
|
|
1115
|
-
provider,
|
|
1116
|
-
operation.serverName,
|
|
1117
|
-
operation.config,
|
|
1118
|
-
scope,
|
|
1119
|
-
projectDir
|
|
1120
|
-
);
|
|
1121
|
-
applied.push(result);
|
|
1122
|
-
}
|
|
1123
|
-
}
|
|
1124
|
-
return { conflicts, applied, skipped };
|
|
1125
|
-
}
|
|
1126
482
|
async function updateInstructionsSingleOperation(providers, content, scope = "project", projectDir = process.cwd()) {
|
|
1127
483
|
const actions = await injectAll(providers, projectDir, scope, content);
|
|
1128
484
|
const groupedByFile = groupByInstructFile(providers);
|
|
@@ -1147,634 +503,254 @@ async function updateInstructionsSingleOperation(providers, content, scope = "pr
|
|
|
1147
503
|
}
|
|
1148
504
|
return summary;
|
|
1149
505
|
}
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
const
|
|
1154
|
-
const
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
"global",
|
|
1162
|
-
projectDir
|
|
1163
|
-
)
|
|
1164
|
-
);
|
|
1165
|
-
}
|
|
1166
|
-
const projectResults = [];
|
|
1167
|
-
for (const operation of projectOps) {
|
|
1168
|
-
projectResults.push(
|
|
1169
|
-
await installMcpServer(
|
|
1170
|
-
provider,
|
|
1171
|
-
operation.serverName,
|
|
1172
|
-
operation.config,
|
|
1173
|
-
"project",
|
|
1174
|
-
projectDir
|
|
1175
|
-
)
|
|
1176
|
-
);
|
|
1177
|
-
}
|
|
1178
|
-
const instructionResults = {};
|
|
1179
|
-
const instructionContent = options.instructionContent;
|
|
1180
|
-
if (typeof instructionContent === "string") {
|
|
1181
|
-
instructionResults.global = await injectAll(
|
|
1182
|
-
[provider],
|
|
1183
|
-
projectDir,
|
|
1184
|
-
"global",
|
|
1185
|
-
instructionContent
|
|
1186
|
-
);
|
|
1187
|
-
instructionResults.project = await injectAll(
|
|
1188
|
-
[provider],
|
|
1189
|
-
projectDir,
|
|
1190
|
-
"project",
|
|
1191
|
-
instructionContent
|
|
1192
|
-
);
|
|
1193
|
-
} else if (instructionContent) {
|
|
1194
|
-
if (instructionContent.global) {
|
|
1195
|
-
instructionResults.global = await injectAll(
|
|
1196
|
-
[provider],
|
|
1197
|
-
projectDir,
|
|
1198
|
-
"global",
|
|
1199
|
-
instructionContent.global
|
|
1200
|
-
);
|
|
1201
|
-
}
|
|
1202
|
-
if (instructionContent.project) {
|
|
1203
|
-
instructionResults.project = await injectAll(
|
|
1204
|
-
[provider],
|
|
1205
|
-
projectDir,
|
|
1206
|
-
"project",
|
|
1207
|
-
instructionContent.project
|
|
506
|
+
|
|
507
|
+
// src/core/formats/utils.ts
|
|
508
|
+
function deepMerge(target, source) {
|
|
509
|
+
const result = { ...target };
|
|
510
|
+
for (const key of Object.keys(source)) {
|
|
511
|
+
const sourceVal = source[key];
|
|
512
|
+
const targetVal = target[key];
|
|
513
|
+
if (sourceVal !== null && typeof sourceVal === "object" && !Array.isArray(sourceVal) && targetVal !== null && typeof targetVal === "object" && !Array.isArray(targetVal)) {
|
|
514
|
+
result[key] = deepMerge(
|
|
515
|
+
targetVal,
|
|
516
|
+
sourceVal
|
|
1208
517
|
);
|
|
518
|
+
} else {
|
|
519
|
+
result[key] = sourceVal;
|
|
1209
520
|
}
|
|
1210
521
|
}
|
|
1211
|
-
return
|
|
1212
|
-
providerId: provider.id,
|
|
1213
|
-
configPaths: {
|
|
1214
|
-
global: resolveConfigPath(provider, "global", projectDir),
|
|
1215
|
-
project: resolveConfigPath(provider, "project", projectDir)
|
|
1216
|
-
},
|
|
1217
|
-
mcp: {
|
|
1218
|
-
global: globalResults,
|
|
1219
|
-
project: projectResults
|
|
1220
|
-
},
|
|
1221
|
-
instructions: instructionResults
|
|
1222
|
-
};
|
|
1223
|
-
}
|
|
1224
|
-
|
|
1225
|
-
// src/core/mcp/cleo.ts
|
|
1226
|
-
import { execFileSync as execFileSync2 } from "child_process";
|
|
1227
|
-
import { existsSync as existsSync8 } from "fs";
|
|
1228
|
-
import { homedir } from "os";
|
|
1229
|
-
import { isAbsolute, resolve } from "path";
|
|
1230
|
-
var CLEO_SERVER_NAMES = {
|
|
1231
|
-
stable: "cleo",
|
|
1232
|
-
beta: "cleo-beta",
|
|
1233
|
-
dev: "cleo-dev"
|
|
1234
|
-
};
|
|
1235
|
-
var CLEO_MCP_NPM_PACKAGE = "@cleocode/cleo";
|
|
1236
|
-
var CLEO_DEV_DIR_DEFAULT = "~/.cleo-dev";
|
|
1237
|
-
function normalizeCleoChannel(value) {
|
|
1238
|
-
if (!value || value.trim() === "") return "stable";
|
|
1239
|
-
const normalized = value.trim().toLowerCase();
|
|
1240
|
-
if (normalized === "stable" || normalized === "beta" || normalized === "dev") {
|
|
1241
|
-
return normalized;
|
|
1242
|
-
}
|
|
1243
|
-
throw new Error(`Invalid channel "${value}". Expected stable, beta, or dev.`);
|
|
1244
|
-
}
|
|
1245
|
-
function resolveCleoServerName(channel) {
|
|
1246
|
-
return CLEO_SERVER_NAMES[channel];
|
|
1247
|
-
}
|
|
1248
|
-
function resolveChannelFromServerName(serverName) {
|
|
1249
|
-
if (serverName === CLEO_SERVER_NAMES.stable) return "stable";
|
|
1250
|
-
if (serverName === CLEO_SERVER_NAMES.beta) return "beta";
|
|
1251
|
-
if (serverName === CLEO_SERVER_NAMES.dev) return "dev";
|
|
1252
|
-
return null;
|
|
522
|
+
return result;
|
|
1253
523
|
}
|
|
1254
|
-
function
|
|
1255
|
-
|
|
1256
|
-
|
|
1257
|
-
|
|
1258
|
-
|
|
1259
|
-
|
|
1260
|
-
if (!binary) {
|
|
1261
|
-
throw new Error("Command is required for dev channel.");
|
|
524
|
+
function getNestedValue(obj, keyPath) {
|
|
525
|
+
const parts = keyPath.split(".");
|
|
526
|
+
let current = obj;
|
|
527
|
+
for (const part of parts) {
|
|
528
|
+
if (current === null || typeof current !== "object") return void 0;
|
|
529
|
+
current = current[part];
|
|
1262
530
|
}
|
|
1263
|
-
return
|
|
1264
|
-
command: binary,
|
|
1265
|
-
args: parts.slice(1)
|
|
1266
|
-
};
|
|
531
|
+
return current;
|
|
1267
532
|
}
|
|
1268
|
-
function
|
|
1269
|
-
const
|
|
1270
|
-
|
|
1271
|
-
|
|
1272
|
-
}
|
|
1273
|
-
return Object.keys(result).length > 0 ? result : void 0;
|
|
533
|
+
async function ensureDir(filePath) {
|
|
534
|
+
const { mkdir: mkdir4 } = await import("fs/promises");
|
|
535
|
+
const { dirname: dirname3 } = await import("path");
|
|
536
|
+
await mkdir4(dirname3(filePath), { recursive: true });
|
|
1274
537
|
}
|
|
1275
|
-
|
|
1276
|
-
|
|
1277
|
-
|
|
538
|
+
|
|
539
|
+
// src/core/formats/json.ts
|
|
540
|
+
import { existsSync as existsSync4 } from "fs";
|
|
541
|
+
import { readFile, writeFile } from "fs/promises";
|
|
542
|
+
import * as jsonc from "jsonc-parser";
|
|
543
|
+
async function readJsonConfig(filePath) {
|
|
544
|
+
if (!existsSync4(filePath)) return {};
|
|
545
|
+
const content = await readFile(filePath, "utf-8");
|
|
546
|
+
if (!content.trim()) return {};
|
|
547
|
+
const errors = [];
|
|
548
|
+
const result = jsonc.parse(content, errors);
|
|
549
|
+
if (errors.length > 0) {
|
|
550
|
+
return JSON.parse(content);
|
|
551
|
+
}
|
|
552
|
+
return result ?? {};
|
|
1278
553
|
}
|
|
1279
|
-
function
|
|
1280
|
-
const
|
|
1281
|
-
const
|
|
1282
|
-
|
|
1283
|
-
if (
|
|
1284
|
-
|
|
1285
|
-
|
|
1286
|
-
|
|
1287
|
-
const env = normalizeEnv(options.env, channel, options.cleoDir);
|
|
1288
|
-
return {
|
|
1289
|
-
channel,
|
|
1290
|
-
serverName,
|
|
1291
|
-
config: {
|
|
1292
|
-
command: parsed.command,
|
|
1293
|
-
args: parsed.args,
|
|
1294
|
-
...env ? { env } : {}
|
|
554
|
+
function detectIndent(content) {
|
|
555
|
+
const lines = content.split("\n");
|
|
556
|
+
for (const line of lines) {
|
|
557
|
+
const match = line.match(/^(\s+)/);
|
|
558
|
+
if (match?.[1]) {
|
|
559
|
+
const ws = match[1];
|
|
560
|
+
if (ws.startsWith(" ")) {
|
|
561
|
+
return { indent: " ", insertSpaces: false, tabSize: 1 };
|
|
1295
562
|
}
|
|
1296
|
-
|
|
563
|
+
return { indent: ws, insertSpaces: true, tabSize: ws.length };
|
|
564
|
+
}
|
|
1297
565
|
}
|
|
1298
|
-
|
|
1299
|
-
return {
|
|
1300
|
-
channel,
|
|
1301
|
-
serverName,
|
|
1302
|
-
packageSpec,
|
|
1303
|
-
config: {
|
|
1304
|
-
command: "npx",
|
|
1305
|
-
args: ["-y", packageSpec, "mcp"]
|
|
1306
|
-
}
|
|
1307
|
-
};
|
|
1308
|
-
}
|
|
1309
|
-
function expandHome(pathValue) {
|
|
1310
|
-
if (pathValue === "~") return homedir();
|
|
1311
|
-
if (pathValue.startsWith("~/")) {
|
|
1312
|
-
return resolve(homedir(), pathValue.slice(2));
|
|
1313
|
-
}
|
|
1314
|
-
return pathValue;
|
|
566
|
+
return { indent: " ", insertSpaces: true, tabSize: 2 };
|
|
1315
567
|
}
|
|
1316
|
-
function
|
|
1317
|
-
|
|
1318
|
-
|
|
1319
|
-
|
|
1320
|
-
|
|
1321
|
-
if (
|
|
1322
|
-
|
|
568
|
+
async function writeJsonConfig(filePath, configKey, serverName, serverConfig) {
|
|
569
|
+
await ensureDir(filePath);
|
|
570
|
+
let content;
|
|
571
|
+
if (existsSync4(filePath)) {
|
|
572
|
+
content = await readFile(filePath, "utf-8");
|
|
573
|
+
if (!content.trim()) {
|
|
574
|
+
content = "{}";
|
|
1323
575
|
}
|
|
1324
|
-
|
|
1325
|
-
|
|
1326
|
-
try {
|
|
1327
|
-
const lookup = process.platform === "win32" ? "where" : "which";
|
|
1328
|
-
execFileSync2(lookup, [command], { stdio: "pipe" });
|
|
1329
|
-
return { reachable: true, method: "lookup", detail: command };
|
|
1330
|
-
} catch {
|
|
1331
|
-
return { reachable: false, method: "lookup", detail: command };
|
|
576
|
+
} else {
|
|
577
|
+
content = "{}";
|
|
1332
578
|
}
|
|
1333
|
-
}
|
|
1334
|
-
|
|
1335
|
-
|
|
1336
|
-
|
|
1337
|
-
|
|
1338
|
-
|
|
1339
|
-
|
|
1340
|
-
|
|
1341
|
-
|
|
1342
|
-
|
|
1343
|
-
|
|
1344
|
-
throw new Error(`Invalid --env value "${value}". Key cannot be empty.`);
|
|
1345
|
-
}
|
|
1346
|
-
env[key] = val;
|
|
579
|
+
const { tabSize, insertSpaces } = detectIndent(content);
|
|
580
|
+
const formatOptions = {
|
|
581
|
+
tabSize,
|
|
582
|
+
insertSpaces,
|
|
583
|
+
eol: "\n"
|
|
584
|
+
};
|
|
585
|
+
const keyParts = configKey.split(".");
|
|
586
|
+
const jsonPath = [...keyParts, serverName];
|
|
587
|
+
const edits = jsonc.modify(content, jsonPath, serverConfig, { formattingOptions: formatOptions });
|
|
588
|
+
if (edits.length > 0) {
|
|
589
|
+
content = jsonc.applyEdits(content, edits);
|
|
1347
590
|
}
|
|
1348
|
-
|
|
1349
|
-
|
|
1350
|
-
function extractVersionTag(packageSpec) {
|
|
1351
|
-
if (!packageSpec) return void 0;
|
|
1352
|
-
const atIndex = packageSpec.lastIndexOf("@");
|
|
1353
|
-
if (atIndex <= 0) return void 0;
|
|
1354
|
-
return packageSpec.slice(atIndex + 1);
|
|
1355
|
-
}
|
|
1356
|
-
function isCleoSource(source) {
|
|
1357
|
-
return source.trim().toLowerCase() === "cleo";
|
|
1358
|
-
}
|
|
1359
|
-
|
|
1360
|
-
// src/core/lock-utils.ts
|
|
1361
|
-
import { existsSync as existsSync9 } from "fs";
|
|
1362
|
-
import { mkdir as mkdir3, open, readFile as readFile5, rename, rm as rm3, stat, writeFile as writeFile5 } from "fs/promises";
|
|
1363
|
-
var LOCK_GUARD_PATH = `${LOCK_FILE_PATH}.lock`;
|
|
1364
|
-
var STALE_LOCK_MS = 5e3;
|
|
1365
|
-
function sleep(ms) {
|
|
1366
|
-
return new Promise((resolve2) => setTimeout(resolve2, ms));
|
|
1367
|
-
}
|
|
1368
|
-
async function removeStaleLock() {
|
|
1369
|
-
try {
|
|
1370
|
-
const info = await stat(LOCK_GUARD_PATH);
|
|
1371
|
-
if (Date.now() - info.mtimeMs > STALE_LOCK_MS) {
|
|
1372
|
-
await rm3(LOCK_GUARD_PATH, { force: true });
|
|
1373
|
-
return true;
|
|
1374
|
-
}
|
|
1375
|
-
} catch {
|
|
591
|
+
if (!content.endsWith("\n")) {
|
|
592
|
+
content += "\n";
|
|
1376
593
|
}
|
|
1377
|
-
|
|
594
|
+
await writeFile(filePath, content, "utf-8");
|
|
1378
595
|
}
|
|
1379
|
-
async function
|
|
1380
|
-
|
|
1381
|
-
|
|
1382
|
-
|
|
1383
|
-
|
|
1384
|
-
|
|
1385
|
-
|
|
1386
|
-
|
|
1387
|
-
|
|
1388
|
-
|
|
1389
|
-
|
|
1390
|
-
|
|
1391
|
-
|
|
1392
|
-
|
|
1393
|
-
|
|
1394
|
-
|
|
1395
|
-
|
|
596
|
+
async function removeJsonConfig(filePath, configKey, serverName) {
|
|
597
|
+
if (!existsSync4(filePath)) return false;
|
|
598
|
+
let content = await readFile(filePath, "utf-8");
|
|
599
|
+
if (!content.trim()) return false;
|
|
600
|
+
const { tabSize, insertSpaces } = detectIndent(content);
|
|
601
|
+
const formatOptions = {
|
|
602
|
+
tabSize,
|
|
603
|
+
insertSpaces,
|
|
604
|
+
eol: "\n"
|
|
605
|
+
};
|
|
606
|
+
const keyParts = configKey.split(".");
|
|
607
|
+
const jsonPath = [...keyParts, serverName];
|
|
608
|
+
const edits = jsonc.modify(content, jsonPath, void 0, { formattingOptions: formatOptions });
|
|
609
|
+
if (edits.length === 0) return false;
|
|
610
|
+
content = jsonc.applyEdits(content, edits);
|
|
611
|
+
if (!content.endsWith("\n")) {
|
|
612
|
+
content += "\n";
|
|
1396
613
|
}
|
|
1397
|
-
|
|
1398
|
-
|
|
1399
|
-
async function releaseLockGuard() {
|
|
1400
|
-
await rm3(LOCK_GUARD_PATH, { force: true });
|
|
614
|
+
await writeFile(filePath, content, "utf-8");
|
|
615
|
+
return true;
|
|
1401
616
|
}
|
|
1402
|
-
|
|
1403
|
-
|
|
1404
|
-
|
|
1405
|
-
|
|
617
|
+
|
|
618
|
+
// src/core/formats/toml.ts
|
|
619
|
+
import { existsSync as existsSync5 } from "fs";
|
|
620
|
+
import { readFile as readFile2, writeFile as writeFile2 } from "fs/promises";
|
|
621
|
+
import TOML from "@iarna/toml";
|
|
622
|
+
async function readTomlConfig(filePath) {
|
|
623
|
+
if (!existsSync5(filePath)) return {};
|
|
624
|
+
const content = await readFile2(filePath, "utf-8");
|
|
625
|
+
if (!content.trim()) return {};
|
|
626
|
+
const result = TOML.parse(content);
|
|
627
|
+
return result;
|
|
1406
628
|
}
|
|
1407
|
-
async function
|
|
1408
|
-
|
|
1409
|
-
|
|
1410
|
-
|
|
1411
|
-
|
|
1412
|
-
|
|
1413
|
-
|
|
1414
|
-
} catch {
|
|
1415
|
-
return { version: 1, skills: {}, mcpServers: {} };
|
|
629
|
+
async function writeTomlConfig(filePath, configKey, serverName, serverConfig) {
|
|
630
|
+
await ensureDir(filePath);
|
|
631
|
+
const existing = await readTomlConfig(filePath);
|
|
632
|
+
const keyParts = configKey.split(".");
|
|
633
|
+
let newEntry = { [serverName]: serverConfig };
|
|
634
|
+
for (const part of [...keyParts].reverse()) {
|
|
635
|
+
newEntry = { [part]: newEntry };
|
|
1416
636
|
}
|
|
637
|
+
const merged = deepMerge(existing, newEntry);
|
|
638
|
+
const content = TOML.stringify(merged);
|
|
639
|
+
await writeFile2(filePath, content, "utf-8");
|
|
1417
640
|
}
|
|
1418
|
-
async function
|
|
1419
|
-
|
|
1420
|
-
|
|
1421
|
-
|
|
1422
|
-
|
|
1423
|
-
|
|
1424
|
-
|
|
1425
|
-
|
|
1426
|
-
|
|
641
|
+
async function removeTomlConfig(filePath, configKey, serverName) {
|
|
642
|
+
if (!existsSync5(filePath)) return false;
|
|
643
|
+
const existing = await readTomlConfig(filePath);
|
|
644
|
+
const keyParts = configKey.split(".");
|
|
645
|
+
let current = existing;
|
|
646
|
+
for (const part of keyParts) {
|
|
647
|
+
const next = current[part];
|
|
648
|
+
if (typeof next !== "object" || next === null) return false;
|
|
649
|
+
current = next;
|
|
1427
650
|
}
|
|
651
|
+
if (!(serverName in current)) return false;
|
|
652
|
+
delete current[serverName];
|
|
653
|
+
const content = TOML.stringify(existing);
|
|
654
|
+
await writeFile2(filePath, content, "utf-8");
|
|
655
|
+
return true;
|
|
1428
656
|
}
|
|
1429
657
|
|
|
1430
|
-
// src/core/
|
|
1431
|
-
|
|
1432
|
-
|
|
1433
|
-
|
|
1434
|
-
|
|
1435
|
-
|
|
1436
|
-
|
|
1437
|
-
|
|
1438
|
-
|
|
1439
|
-
|
|
1440
|
-
version: version ?? existing?.version,
|
|
1441
|
-
installedAt: existing?.installedAt ?? now,
|
|
1442
|
-
updatedAt: now,
|
|
1443
|
-
agents: [.../* @__PURE__ */ new Set([...existing?.agents ?? [], ...agents])],
|
|
1444
|
-
canonicalPath: "",
|
|
1445
|
-
isGlobal
|
|
1446
|
-
};
|
|
1447
|
-
});
|
|
1448
|
-
}
|
|
1449
|
-
async function removeMcpFromLock(serverName) {
|
|
1450
|
-
let removed = false;
|
|
1451
|
-
await updateLockFile((lock) => {
|
|
1452
|
-
if (!(serverName in lock.mcpServers)) return;
|
|
1453
|
-
delete lock.mcpServers[serverName];
|
|
1454
|
-
removed = true;
|
|
1455
|
-
});
|
|
1456
|
-
return removed;
|
|
1457
|
-
}
|
|
1458
|
-
async function getTrackedMcpServers() {
|
|
1459
|
-
const lock = await readLockFile();
|
|
1460
|
-
return lock.mcpServers;
|
|
1461
|
-
}
|
|
1462
|
-
async function saveLastSelectedAgents(agents) {
|
|
1463
|
-
await updateLockFile((lock) => {
|
|
1464
|
-
lock.lastSelectedAgents = agents;
|
|
1465
|
-
});
|
|
1466
|
-
}
|
|
1467
|
-
async function getLastSelectedAgents() {
|
|
1468
|
-
const lock = await readLockFile();
|
|
1469
|
-
return lock.lastSelectedAgents;
|
|
658
|
+
// src/core/formats/yaml.ts
|
|
659
|
+
import { existsSync as existsSync6 } from "fs";
|
|
660
|
+
import { readFile as readFile3, writeFile as writeFile3 } from "fs/promises";
|
|
661
|
+
import yaml from "js-yaml";
|
|
662
|
+
async function readYamlConfig(filePath) {
|
|
663
|
+
if (!existsSync6(filePath)) return {};
|
|
664
|
+
const content = await readFile3(filePath, "utf-8");
|
|
665
|
+
if (!content.trim()) return {};
|
|
666
|
+
const result = yaml.load(content);
|
|
667
|
+
return result ?? {};
|
|
1470
668
|
}
|
|
1471
|
-
|
|
1472
|
-
|
|
1473
|
-
|
|
1474
|
-
const
|
|
1475
|
-
|
|
1476
|
-
const
|
|
1477
|
-
|
|
1478
|
-
const version = extractVersionTag(packageArg);
|
|
1479
|
-
return {
|
|
1480
|
-
source: packageArg,
|
|
1481
|
-
sourceType: "package",
|
|
1482
|
-
version
|
|
1483
|
-
};
|
|
1484
|
-
}
|
|
1485
|
-
if (channel === "dev" || command.includes("/") || command.includes("\\")) {
|
|
1486
|
-
return {
|
|
1487
|
-
source: command,
|
|
1488
|
-
sourceType: "command",
|
|
1489
|
-
version: void 0
|
|
1490
|
-
};
|
|
669
|
+
async function writeYamlConfig(filePath, configKey, serverName, serverConfig) {
|
|
670
|
+
await ensureDir(filePath);
|
|
671
|
+
const existing = await readYamlConfig(filePath);
|
|
672
|
+
const keyParts = configKey.split(".");
|
|
673
|
+
let newEntry = { [serverName]: serverConfig };
|
|
674
|
+
for (const part of [...keyParts].reverse()) {
|
|
675
|
+
newEntry = { [part]: newEntry };
|
|
1491
676
|
}
|
|
1492
|
-
const
|
|
1493
|
-
|
|
1494
|
-
|
|
1495
|
-
|
|
1496
|
-
|
|
1497
|
-
|
|
677
|
+
const merged = deepMerge(existing, newEntry);
|
|
678
|
+
const content = yaml.dump(merged, {
|
|
679
|
+
indent: 2,
|
|
680
|
+
lineWidth: -1,
|
|
681
|
+
noRefs: true,
|
|
682
|
+
sortKeys: false
|
|
683
|
+
});
|
|
684
|
+
await writeFile3(filePath, content, "utf-8");
|
|
1498
685
|
}
|
|
1499
|
-
async function
|
|
1500
|
-
|
|
1501
|
-
|
|
1502
|
-
|
|
1503
|
-
|
|
1504
|
-
|
|
1505
|
-
|
|
1506
|
-
|
|
1507
|
-
|
|
1508
|
-
const targetProviders = options.providerIds?.length ? providers.filter((p) => options.providerIds.includes(p.id)) : providers;
|
|
1509
|
-
const scopes = [];
|
|
1510
|
-
if (options.global && !options.project) {
|
|
1511
|
-
scopes.push("global");
|
|
1512
|
-
} else if (options.project && !options.global) {
|
|
1513
|
-
scopes.push("project");
|
|
1514
|
-
} else {
|
|
1515
|
-
scopes.push("project", "global");
|
|
1516
|
-
}
|
|
1517
|
-
const groups = /* @__PURE__ */ new Map();
|
|
1518
|
-
const liveCleoServerNames = /* @__PURE__ */ new Set();
|
|
1519
|
-
for (const scope of scopes) {
|
|
1520
|
-
for (const provider of targetProviders) {
|
|
1521
|
-
let entries;
|
|
1522
|
-
try {
|
|
1523
|
-
entries = await listMcpServers(provider, scope);
|
|
1524
|
-
} catch {
|
|
1525
|
-
result.errors.push({
|
|
1526
|
-
message: `Failed to read config for ${provider.id} (${scope})`
|
|
1527
|
-
});
|
|
1528
|
-
continue;
|
|
1529
|
-
}
|
|
1530
|
-
for (const entry of entries) {
|
|
1531
|
-
const channel = resolveChannelFromServerName(entry.name);
|
|
1532
|
-
if (!channel) continue;
|
|
1533
|
-
liveCleoServerNames.add(entry.name);
|
|
1534
|
-
const isGlobal = scope === "global";
|
|
1535
|
-
const groupKey = `${entry.name}:${isGlobal ? "global" : "project"}`;
|
|
1536
|
-
if (lockEntries[entry.name] !== void 0) {
|
|
1537
|
-
const existing2 = groups.get(groupKey);
|
|
1538
|
-
if (!existing2) {
|
|
1539
|
-
result.alreadyTracked++;
|
|
1540
|
-
}
|
|
1541
|
-
continue;
|
|
1542
|
-
}
|
|
1543
|
-
const existing = groups.get(groupKey);
|
|
1544
|
-
if (existing) {
|
|
1545
|
-
if (!existing.agents.includes(provider.id)) {
|
|
1546
|
-
existing.agents.push(provider.id);
|
|
1547
|
-
}
|
|
1548
|
-
} else {
|
|
1549
|
-
groups.set(groupKey, {
|
|
1550
|
-
serverName: entry.name,
|
|
1551
|
-
channel,
|
|
1552
|
-
scope,
|
|
1553
|
-
agents: [provider.id],
|
|
1554
|
-
config: entry.config
|
|
1555
|
-
});
|
|
1556
|
-
}
|
|
1557
|
-
}
|
|
1558
|
-
}
|
|
1559
|
-
}
|
|
1560
|
-
for (const group of groups.values()) {
|
|
1561
|
-
const inferred = inferCleoLockData(group.config, group.channel);
|
|
1562
|
-
if (!options.dryRun) {
|
|
1563
|
-
try {
|
|
1564
|
-
await recordMcpInstall(
|
|
1565
|
-
group.serverName,
|
|
1566
|
-
inferred.source,
|
|
1567
|
-
inferred.sourceType,
|
|
1568
|
-
group.agents,
|
|
1569
|
-
group.scope === "global",
|
|
1570
|
-
inferred.version
|
|
1571
|
-
);
|
|
1572
|
-
} catch (err) {
|
|
1573
|
-
result.errors.push({
|
|
1574
|
-
message: `Failed to backfill ${group.serverName}: ${err instanceof Error ? err.message : String(err)}`
|
|
1575
|
-
});
|
|
1576
|
-
continue;
|
|
1577
|
-
}
|
|
1578
|
-
}
|
|
1579
|
-
result.backfilled.push({
|
|
1580
|
-
serverName: group.serverName,
|
|
1581
|
-
channel: group.channel,
|
|
1582
|
-
scope: group.scope,
|
|
1583
|
-
agents: group.agents,
|
|
1584
|
-
source: inferred.source,
|
|
1585
|
-
sourceType: inferred.sourceType,
|
|
1586
|
-
version: inferred.version
|
|
1587
|
-
});
|
|
1588
|
-
}
|
|
1589
|
-
if (options.prune) {
|
|
1590
|
-
for (const [serverName] of Object.entries(lockEntries)) {
|
|
1591
|
-
const channel = resolveChannelFromServerName(serverName);
|
|
1592
|
-
if (!channel) continue;
|
|
1593
|
-
if (!liveCleoServerNames.has(serverName)) {
|
|
1594
|
-
if (!options.dryRun) {
|
|
1595
|
-
try {
|
|
1596
|
-
await removeMcpFromLock(serverName);
|
|
1597
|
-
} catch (err) {
|
|
1598
|
-
result.errors.push({
|
|
1599
|
-
message: `Failed to prune ${serverName}: ${err instanceof Error ? err.message : String(err)}`
|
|
1600
|
-
});
|
|
1601
|
-
continue;
|
|
1602
|
-
}
|
|
1603
|
-
}
|
|
1604
|
-
result.pruned.push(serverName);
|
|
1605
|
-
}
|
|
1606
|
-
}
|
|
686
|
+
async function removeYamlConfig(filePath, configKey, serverName) {
|
|
687
|
+
if (!existsSync6(filePath)) return false;
|
|
688
|
+
const existing = await readYamlConfig(filePath);
|
|
689
|
+
const keyParts = configKey.split(".");
|
|
690
|
+
let current = existing;
|
|
691
|
+
for (const part of keyParts) {
|
|
692
|
+
const next = current[part];
|
|
693
|
+
if (typeof next !== "object" || next === null) return false;
|
|
694
|
+
current = next;
|
|
1607
695
|
}
|
|
1608
|
-
return
|
|
696
|
+
if (!(serverName in current)) return false;
|
|
697
|
+
delete current[serverName];
|
|
698
|
+
const content = yaml.dump(existing, {
|
|
699
|
+
indent: 2,
|
|
700
|
+
lineWidth: -1,
|
|
701
|
+
noRefs: true,
|
|
702
|
+
sortKeys: false
|
|
703
|
+
});
|
|
704
|
+
await writeFile3(filePath, content, "utf-8");
|
|
705
|
+
return true;
|
|
1609
706
|
}
|
|
1610
707
|
|
|
1611
|
-
// src/core/
|
|
1612
|
-
|
|
1613
|
-
|
|
1614
|
-
|
|
1615
|
-
|
|
1616
|
-
|
|
1617
|
-
|
|
1618
|
-
|
|
1619
|
-
|
|
1620
|
-
|
|
1621
|
-
|
|
1622
|
-
|
|
1623
|
-
|
|
1624
|
-
if (type === "remote") {
|
|
1625
|
-
try {
|
|
1626
|
-
const url = new URL(source);
|
|
1627
|
-
const parts = url.hostname.split(".");
|
|
1628
|
-
if (parts.length >= 2) {
|
|
1629
|
-
const fallback = parts[0] ?? source;
|
|
1630
|
-
const secondLevel = parts[parts.length - 2] ?? fallback;
|
|
1631
|
-
const brand = parts.length === 3 ? secondLevel : fallback;
|
|
1632
|
-
if (brand !== "www" && brand !== "api" && brand !== "mcp") {
|
|
1633
|
-
return brand;
|
|
1634
|
-
}
|
|
1635
|
-
return secondLevel;
|
|
1636
|
-
}
|
|
1637
|
-
return parts[0] ?? source;
|
|
1638
|
-
} catch {
|
|
1639
|
-
return source;
|
|
1640
|
-
}
|
|
1641
|
-
}
|
|
1642
|
-
if (type === "package") {
|
|
1643
|
-
let name = source.replace(/^@[^/]+\//, "");
|
|
1644
|
-
name = name.replace(/^mcp-server-/, "");
|
|
1645
|
-
name = name.replace(/^server-/, "");
|
|
1646
|
-
name = name.replace(/-mcp$/, "");
|
|
1647
|
-
name = name.replace(/-server$/, "");
|
|
1648
|
-
return name;
|
|
1649
|
-
}
|
|
1650
|
-
if (type === "github" || type === "gitlab") {
|
|
1651
|
-
const match = source.match(/\/([^/]+?)(?:\.git)?$/);
|
|
1652
|
-
return match?.[1] ?? source;
|
|
1653
|
-
}
|
|
1654
|
-
if (type === "local") {
|
|
1655
|
-
const normalized = source.replace(/\\/g, "/").replace(/\/+$/, "");
|
|
1656
|
-
const lastSegment = normalized.split("/").pop();
|
|
1657
|
-
return lastSegment ?? source;
|
|
1658
|
-
}
|
|
1659
|
-
if (type === "command") {
|
|
1660
|
-
const parts = source.split(/\s+/);
|
|
1661
|
-
const command = parts.find(
|
|
1662
|
-
(p) => !p.startsWith("-") && p !== "npx" && p !== "node" && p !== "python" && p !== "python3"
|
|
1663
|
-
);
|
|
1664
|
-
return command ?? parts[0] ?? source;
|
|
708
|
+
// src/core/formats/index.ts
|
|
709
|
+
async function readConfig(filePath, format) {
|
|
710
|
+
debug(`reading config: ${filePath} (format: ${format})`);
|
|
711
|
+
switch (format) {
|
|
712
|
+
case "json":
|
|
713
|
+
case "jsonc":
|
|
714
|
+
return readJsonConfig(filePath);
|
|
715
|
+
case "yaml":
|
|
716
|
+
return readYamlConfig(filePath);
|
|
717
|
+
case "toml":
|
|
718
|
+
return readTomlConfig(filePath);
|
|
719
|
+
default:
|
|
720
|
+
throw new Error(`Unsupported config format: ${format}`);
|
|
1665
721
|
}
|
|
1666
|
-
return source;
|
|
1667
722
|
}
|
|
1668
|
-
function
|
|
1669
|
-
|
|
1670
|
-
|
|
1671
|
-
|
|
1672
|
-
|
|
1673
|
-
|
|
1674
|
-
|
|
1675
|
-
return
|
|
1676
|
-
|
|
1677
|
-
|
|
1678
|
-
|
|
1679
|
-
|
|
1680
|
-
value: input,
|
|
1681
|
-
inferredName,
|
|
1682
|
-
owner,
|
|
1683
|
-
repo,
|
|
1684
|
-
ref: ghUrlMatch[3],
|
|
1685
|
-
path
|
|
1686
|
-
};
|
|
1687
|
-
}
|
|
1688
|
-
const glUrlMatch = input.match(GITLAB_URL);
|
|
1689
|
-
if (glUrlMatch) {
|
|
1690
|
-
const owner = glUrlMatch[1];
|
|
1691
|
-
const repo = glUrlMatch[2];
|
|
1692
|
-
const path = glUrlMatch[4];
|
|
1693
|
-
if (!owner || !repo) {
|
|
1694
|
-
return { type: "command", value: input, inferredName: inferName(input, "command") };
|
|
1695
|
-
}
|
|
1696
|
-
const inferredName = path ? path.split("/").pop() ?? repo : repo;
|
|
1697
|
-
return {
|
|
1698
|
-
type: "gitlab",
|
|
1699
|
-
value: input,
|
|
1700
|
-
inferredName,
|
|
1701
|
-
owner,
|
|
1702
|
-
repo,
|
|
1703
|
-
ref: glUrlMatch[3],
|
|
1704
|
-
path
|
|
1705
|
-
};
|
|
1706
|
-
}
|
|
1707
|
-
if (HTTP_URL.test(input)) {
|
|
1708
|
-
return {
|
|
1709
|
-
type: "remote",
|
|
1710
|
-
value: input,
|
|
1711
|
-
inferredName: inferName(input, "remote")
|
|
1712
|
-
};
|
|
1713
|
-
}
|
|
1714
|
-
if (input.startsWith("/") || input.startsWith("./") || input.startsWith("../") || input.startsWith("~")) {
|
|
1715
|
-
return {
|
|
1716
|
-
type: "local",
|
|
1717
|
-
value: input,
|
|
1718
|
-
inferredName: inferName(input, "local")
|
|
1719
|
-
};
|
|
1720
|
-
}
|
|
1721
|
-
const ghShorthand = input.match(GITHUB_SHORTHAND);
|
|
1722
|
-
if (ghShorthand && !NPM_SCOPED.test(input)) {
|
|
1723
|
-
const owner = ghShorthand[1];
|
|
1724
|
-
const repo = ghShorthand[2];
|
|
1725
|
-
const path = ghShorthand[3];
|
|
1726
|
-
if (!owner || !repo) {
|
|
1727
|
-
return { type: "command", value: input, inferredName: inferName(input, "command") };
|
|
1728
|
-
}
|
|
1729
|
-
const inferredName = path ? path.split("/").pop() ?? repo : repo;
|
|
1730
|
-
return {
|
|
1731
|
-
type: "github",
|
|
1732
|
-
value: `https://github.com/${owner}/${repo}`,
|
|
1733
|
-
inferredName,
|
|
1734
|
-
owner,
|
|
1735
|
-
repo,
|
|
1736
|
-
path
|
|
1737
|
-
};
|
|
1738
|
-
}
|
|
1739
|
-
const libraryMatch = input.match(LIBRARY_SKILL);
|
|
1740
|
-
if (libraryMatch) {
|
|
1741
|
-
return {
|
|
1742
|
-
type: "library",
|
|
1743
|
-
value: input,
|
|
1744
|
-
inferredName: inferName(input, "library"),
|
|
1745
|
-
owner: libraryMatch[1],
|
|
1746
|
-
// This will be the package name, e.g. @cleocode/ct-skills
|
|
1747
|
-
repo: libraryMatch[2]
|
|
1748
|
-
// This will be the skill name, e.g. ct-research-agent
|
|
1749
|
-
};
|
|
1750
|
-
}
|
|
1751
|
-
if (NPM_SCOPED.test(input)) {
|
|
1752
|
-
return {
|
|
1753
|
-
type: "package",
|
|
1754
|
-
value: input,
|
|
1755
|
-
inferredName: inferName(input, "package")
|
|
1756
|
-
};
|
|
1757
|
-
}
|
|
1758
|
-
if (NPM_PACKAGE.test(input) && !input.includes(" ")) {
|
|
1759
|
-
return {
|
|
1760
|
-
type: "package",
|
|
1761
|
-
value: input,
|
|
1762
|
-
inferredName: inferName(input, "package")
|
|
1763
|
-
};
|
|
723
|
+
async function writeConfig(filePath, format, key, serverName, serverConfig) {
|
|
724
|
+
debug(`writing config: ${filePath} (format: ${format}, key: ${key}, server: ${serverName})`);
|
|
725
|
+
switch (format) {
|
|
726
|
+
case "json":
|
|
727
|
+
case "jsonc":
|
|
728
|
+
return writeJsonConfig(filePath, key, serverName, serverConfig);
|
|
729
|
+
case "yaml":
|
|
730
|
+
return writeYamlConfig(filePath, key, serverName, serverConfig);
|
|
731
|
+
case "toml":
|
|
732
|
+
return writeTomlConfig(filePath, key, serverName, serverConfig);
|
|
733
|
+
default:
|
|
734
|
+
throw new Error(`Unsupported config format: ${format}`);
|
|
1764
735
|
}
|
|
1765
|
-
return {
|
|
1766
|
-
type: "command",
|
|
1767
|
-
value: input,
|
|
1768
|
-
inferredName: inferName(input, "command")
|
|
1769
|
-
};
|
|
1770
736
|
}
|
|
1771
|
-
function
|
|
1772
|
-
|
|
737
|
+
async function removeConfig(filePath, format, key, serverName) {
|
|
738
|
+
switch (format) {
|
|
739
|
+
case "json":
|
|
740
|
+
case "jsonc":
|
|
741
|
+
return removeJsonConfig(filePath, key, serverName);
|
|
742
|
+
case "yaml":
|
|
743
|
+
return removeYamlConfig(filePath, key, serverName);
|
|
744
|
+
case "toml":
|
|
745
|
+
return removeTomlConfig(filePath, key, serverName);
|
|
746
|
+
default:
|
|
747
|
+
throw new Error(`Unsupported config format: ${format}`);
|
|
748
|
+
}
|
|
1773
749
|
}
|
|
1774
750
|
|
|
1775
751
|
// src/core/skills/audit/scanner.ts
|
|
1776
|
-
import { existsSync as
|
|
1777
|
-
import { readFile as
|
|
752
|
+
import { existsSync as existsSync7 } from "fs";
|
|
753
|
+
import { readFile as readFile4 } from "fs/promises";
|
|
1778
754
|
|
|
1779
755
|
// src/core/skills/audit/rules.ts
|
|
1780
756
|
function rule(id, name, description, severity, category, pattern) {
|
|
@@ -2146,10 +1122,10 @@ var SEVERITY_WEIGHTS = {
|
|
|
2146
1122
|
info: 0
|
|
2147
1123
|
};
|
|
2148
1124
|
async function scanFile(filePath, rules) {
|
|
2149
|
-
if (!
|
|
1125
|
+
if (!existsSync7(filePath)) {
|
|
2150
1126
|
return { file: filePath, findings: [], score: 100, passed: true };
|
|
2151
1127
|
}
|
|
2152
|
-
const content = await
|
|
1128
|
+
const content = await readFile4(filePath, "utf-8");
|
|
2153
1129
|
const lines = content.split("\n");
|
|
2154
1130
|
const activeRules = rules ?? AUDIT_RULES;
|
|
2155
1131
|
const findings = [];
|
|
@@ -2181,13 +1157,13 @@ async function scanFile(filePath, rules) {
|
|
|
2181
1157
|
async function scanDirectory(dirPath) {
|
|
2182
1158
|
const { readdir: readdir2 } = await import("fs/promises");
|
|
2183
1159
|
const { join: join7 } = await import("path");
|
|
2184
|
-
if (!
|
|
1160
|
+
if (!existsSync7(dirPath)) return [];
|
|
2185
1161
|
const entries = await readdir2(dirPath, { withFileTypes: true });
|
|
2186
1162
|
const results = [];
|
|
2187
1163
|
for (const entry of entries) {
|
|
2188
1164
|
if (entry.isDirectory() || entry.isSymbolicLink()) {
|
|
2189
1165
|
const skillFile = join7(dirPath, entry.name, "SKILL.md");
|
|
2190
|
-
if (
|
|
1166
|
+
if (existsSync7(skillFile)) {
|
|
2191
1167
|
results.push(await scanFile(skillFile));
|
|
2192
1168
|
}
|
|
2193
1169
|
}
|
|
@@ -2238,10 +1214,246 @@ function toSarif(results) {
|
|
|
2238
1214
|
};
|
|
2239
1215
|
}
|
|
2240
1216
|
|
|
1217
|
+
// src/core/sources/parser.ts
|
|
1218
|
+
var GITHUB_SHORTHAND = /^([a-zA-Z0-9_.-]+)\/([a-zA-Z0-9_.-]+)(?:\/(.+))?$/;
|
|
1219
|
+
var GITHUB_URL = /^https?:\/\/(?:www\.)?github\.com\/([^/]+)\/([^/]+)(?:\/(?:tree|blob)\/([^/]+)(?:\/(.+))?)?/;
|
|
1220
|
+
var GITLAB_URL = /^https?:\/\/(?:www\.)?gitlab\.com\/([^/]+)\/([^/]+)(?:\/-\/(?:tree|blob)\/([^/]+)(?:\/(.+))?)?/;
|
|
1221
|
+
var HTTP_URL = /^https?:\/\//;
|
|
1222
|
+
var NPM_SCOPED = /^@[a-zA-Z0-9_.-]+\/[a-zA-Z0-9_.-]+$/;
|
|
1223
|
+
var NPM_PACKAGE = /^[a-zA-Z0-9_.-]+$/;
|
|
1224
|
+
var LIBRARY_SKILL = /^(@[a-zA-Z0-9_.-]+\/[a-zA-Z0-9_.-]+|[a-zA-Z0-9_.-]+):([a-zA-Z0-9_.-]+)$/;
|
|
1225
|
+
function inferName(source, type) {
|
|
1226
|
+
if (type === "library") {
|
|
1227
|
+
const match = source.match(LIBRARY_SKILL);
|
|
1228
|
+
return match?.[2] ?? source;
|
|
1229
|
+
}
|
|
1230
|
+
if (type === "remote") {
|
|
1231
|
+
try {
|
|
1232
|
+
const url = new URL(source);
|
|
1233
|
+
const parts = url.hostname.split(".");
|
|
1234
|
+
if (parts.length >= 2) {
|
|
1235
|
+
const fallback = parts[0] ?? source;
|
|
1236
|
+
const secondLevel = parts[parts.length - 2] ?? fallback;
|
|
1237
|
+
const brand = parts.length === 3 ? secondLevel : fallback;
|
|
1238
|
+
if (brand !== "www" && brand !== "api" && brand !== "mcp") {
|
|
1239
|
+
return brand;
|
|
1240
|
+
}
|
|
1241
|
+
return secondLevel;
|
|
1242
|
+
}
|
|
1243
|
+
return parts[0] ?? source;
|
|
1244
|
+
} catch {
|
|
1245
|
+
return source;
|
|
1246
|
+
}
|
|
1247
|
+
}
|
|
1248
|
+
if (type === "package") {
|
|
1249
|
+
let name = source.replace(/^@[^/]+\//, "");
|
|
1250
|
+
name = name.replace(/^mcp-server-/, "");
|
|
1251
|
+
name = name.replace(/^server-/, "");
|
|
1252
|
+
name = name.replace(/-mcp$/, "");
|
|
1253
|
+
name = name.replace(/-server$/, "");
|
|
1254
|
+
return name;
|
|
1255
|
+
}
|
|
1256
|
+
if (type === "github" || type === "gitlab") {
|
|
1257
|
+
const match = source.match(/\/([^/]+?)(?:\.git)?$/);
|
|
1258
|
+
return match?.[1] ?? source;
|
|
1259
|
+
}
|
|
1260
|
+
if (type === "local") {
|
|
1261
|
+
const normalized = source.replace(/\\/g, "/").replace(/\/+$/, "");
|
|
1262
|
+
const lastSegment = normalized.split("/").pop();
|
|
1263
|
+
return lastSegment ?? source;
|
|
1264
|
+
}
|
|
1265
|
+
if (type === "command") {
|
|
1266
|
+
const parts = source.split(/\s+/);
|
|
1267
|
+
const command = parts.find(
|
|
1268
|
+
(p) => !p.startsWith("-") && p !== "npx" && p !== "node" && p !== "python" && p !== "python3"
|
|
1269
|
+
);
|
|
1270
|
+
return command ?? parts[0] ?? source;
|
|
1271
|
+
}
|
|
1272
|
+
return source;
|
|
1273
|
+
}
|
|
1274
|
+
function parseSource(input) {
|
|
1275
|
+
const ghUrlMatch = input.match(GITHUB_URL);
|
|
1276
|
+
if (ghUrlMatch) {
|
|
1277
|
+
const owner = ghUrlMatch[1];
|
|
1278
|
+
const repo = ghUrlMatch[2];
|
|
1279
|
+
const path = ghUrlMatch[4];
|
|
1280
|
+
if (!owner || !repo) {
|
|
1281
|
+
return { type: "command", value: input, inferredName: inferName(input, "command") };
|
|
1282
|
+
}
|
|
1283
|
+
const inferredName = path ? path.split("/").pop() ?? repo : repo;
|
|
1284
|
+
return {
|
|
1285
|
+
type: "github",
|
|
1286
|
+
value: input,
|
|
1287
|
+
inferredName,
|
|
1288
|
+
owner,
|
|
1289
|
+
repo,
|
|
1290
|
+
ref: ghUrlMatch[3],
|
|
1291
|
+
path
|
|
1292
|
+
};
|
|
1293
|
+
}
|
|
1294
|
+
const glUrlMatch = input.match(GITLAB_URL);
|
|
1295
|
+
if (glUrlMatch) {
|
|
1296
|
+
const owner = glUrlMatch[1];
|
|
1297
|
+
const repo = glUrlMatch[2];
|
|
1298
|
+
const path = glUrlMatch[4];
|
|
1299
|
+
if (!owner || !repo) {
|
|
1300
|
+
return { type: "command", value: input, inferredName: inferName(input, "command") };
|
|
1301
|
+
}
|
|
1302
|
+
const inferredName = path ? path.split("/").pop() ?? repo : repo;
|
|
1303
|
+
return {
|
|
1304
|
+
type: "gitlab",
|
|
1305
|
+
value: input,
|
|
1306
|
+
inferredName,
|
|
1307
|
+
owner,
|
|
1308
|
+
repo,
|
|
1309
|
+
ref: glUrlMatch[3],
|
|
1310
|
+
path
|
|
1311
|
+
};
|
|
1312
|
+
}
|
|
1313
|
+
if (HTTP_URL.test(input)) {
|
|
1314
|
+
return {
|
|
1315
|
+
type: "remote",
|
|
1316
|
+
value: input,
|
|
1317
|
+
inferredName: inferName(input, "remote")
|
|
1318
|
+
};
|
|
1319
|
+
}
|
|
1320
|
+
if (input.startsWith("/") || input.startsWith("./") || input.startsWith("../") || input.startsWith("~")) {
|
|
1321
|
+
return {
|
|
1322
|
+
type: "local",
|
|
1323
|
+
value: input,
|
|
1324
|
+
inferredName: inferName(input, "local")
|
|
1325
|
+
};
|
|
1326
|
+
}
|
|
1327
|
+
const ghShorthand = input.match(GITHUB_SHORTHAND);
|
|
1328
|
+
if (ghShorthand && !NPM_SCOPED.test(input)) {
|
|
1329
|
+
const owner = ghShorthand[1];
|
|
1330
|
+
const repo = ghShorthand[2];
|
|
1331
|
+
const path = ghShorthand[3];
|
|
1332
|
+
if (!owner || !repo) {
|
|
1333
|
+
return { type: "command", value: input, inferredName: inferName(input, "command") };
|
|
1334
|
+
}
|
|
1335
|
+
const inferredName = path ? path.split("/").pop() ?? repo : repo;
|
|
1336
|
+
return {
|
|
1337
|
+
type: "github",
|
|
1338
|
+
value: `https://github.com/${owner}/${repo}`,
|
|
1339
|
+
inferredName,
|
|
1340
|
+
owner,
|
|
1341
|
+
repo,
|
|
1342
|
+
path
|
|
1343
|
+
};
|
|
1344
|
+
}
|
|
1345
|
+
const libraryMatch = input.match(LIBRARY_SKILL);
|
|
1346
|
+
if (libraryMatch) {
|
|
1347
|
+
return {
|
|
1348
|
+
type: "library",
|
|
1349
|
+
value: input,
|
|
1350
|
+
inferredName: inferName(input, "library"),
|
|
1351
|
+
owner: libraryMatch[1],
|
|
1352
|
+
// This will be the package name, e.g. @cleocode/ct-skills
|
|
1353
|
+
repo: libraryMatch[2]
|
|
1354
|
+
// This will be the skill name, e.g. ct-research-agent
|
|
1355
|
+
};
|
|
1356
|
+
}
|
|
1357
|
+
if (NPM_SCOPED.test(input)) {
|
|
1358
|
+
return {
|
|
1359
|
+
type: "package",
|
|
1360
|
+
value: input,
|
|
1361
|
+
inferredName: inferName(input, "package")
|
|
1362
|
+
};
|
|
1363
|
+
}
|
|
1364
|
+
if (NPM_PACKAGE.test(input) && !input.includes(" ")) {
|
|
1365
|
+
return {
|
|
1366
|
+
type: "package",
|
|
1367
|
+
value: input,
|
|
1368
|
+
inferredName: inferName(input, "package")
|
|
1369
|
+
};
|
|
1370
|
+
}
|
|
1371
|
+
return {
|
|
1372
|
+
type: "command",
|
|
1373
|
+
value: input,
|
|
1374
|
+
inferredName: inferName(input, "command")
|
|
1375
|
+
};
|
|
1376
|
+
}
|
|
1377
|
+
function isMarketplaceScoped(input) {
|
|
1378
|
+
return /^@[a-zA-Z0-9_.-]+\/[a-zA-Z0-9_.-]+$/.test(input);
|
|
1379
|
+
}
|
|
1380
|
+
|
|
2241
1381
|
// src/core/skills/lock.ts
|
|
2242
1382
|
import { execFile } from "child_process";
|
|
2243
1383
|
import { promisify } from "util";
|
|
2244
1384
|
import { simpleGit } from "simple-git";
|
|
1385
|
+
|
|
1386
|
+
// src/core/lock-utils.ts
|
|
1387
|
+
import { existsSync as existsSync8 } from "fs";
|
|
1388
|
+
import { mkdir as mkdir3, open, readFile as readFile5, rename, rm as rm3, stat, writeFile as writeFile4 } from "fs/promises";
|
|
1389
|
+
var LOCK_GUARD_PATH = `${LOCK_FILE_PATH}.lock`;
|
|
1390
|
+
var STALE_LOCK_MS = 5e3;
|
|
1391
|
+
function sleep(ms) {
|
|
1392
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
1393
|
+
}
|
|
1394
|
+
async function removeStaleLock() {
|
|
1395
|
+
try {
|
|
1396
|
+
const info = await stat(LOCK_GUARD_PATH);
|
|
1397
|
+
if (Date.now() - info.mtimeMs > STALE_LOCK_MS) {
|
|
1398
|
+
await rm3(LOCK_GUARD_PATH, { force: true });
|
|
1399
|
+
return true;
|
|
1400
|
+
}
|
|
1401
|
+
} catch {
|
|
1402
|
+
}
|
|
1403
|
+
return false;
|
|
1404
|
+
}
|
|
1405
|
+
async function acquireLockGuard(retries = 40, delayMs = 25) {
|
|
1406
|
+
await mkdir3(AGENTS_HOME, { recursive: true });
|
|
1407
|
+
for (let attempt = 0; attempt < retries; attempt += 1) {
|
|
1408
|
+
try {
|
|
1409
|
+
const handle = await open(LOCK_GUARD_PATH, "wx");
|
|
1410
|
+
await handle.close();
|
|
1411
|
+
return;
|
|
1412
|
+
} catch (error) {
|
|
1413
|
+
if (!(error instanceof Error) || !("code" in error) || error.code !== "EEXIST") {
|
|
1414
|
+
throw error;
|
|
1415
|
+
}
|
|
1416
|
+
if (attempt === 0) {
|
|
1417
|
+
const removed = await removeStaleLock();
|
|
1418
|
+
if (removed) continue;
|
|
1419
|
+
}
|
|
1420
|
+
await sleep(delayMs);
|
|
1421
|
+
}
|
|
1422
|
+
}
|
|
1423
|
+
throw new Error("Timed out waiting for lock file guard");
|
|
1424
|
+
}
|
|
1425
|
+
async function releaseLockGuard() {
|
|
1426
|
+
await rm3(LOCK_GUARD_PATH, { force: true });
|
|
1427
|
+
}
|
|
1428
|
+
async function writeLockFileUnsafe(lock) {
|
|
1429
|
+
const tmpPath = `${LOCK_FILE_PATH}.tmp-${process.pid}-${Date.now()}`;
|
|
1430
|
+
await writeFile4(tmpPath, JSON.stringify(lock, null, 2) + "\n", "utf-8");
|
|
1431
|
+
await rename(tmpPath, LOCK_FILE_PATH);
|
|
1432
|
+
}
|
|
1433
|
+
async function readLockFile() {
|
|
1434
|
+
try {
|
|
1435
|
+
if (!existsSync8(LOCK_FILE_PATH)) {
|
|
1436
|
+
return { version: 1, skills: {}, mcpServers: {} };
|
|
1437
|
+
}
|
|
1438
|
+
const content = await readFile5(LOCK_FILE_PATH, "utf-8");
|
|
1439
|
+
return JSON.parse(content);
|
|
1440
|
+
} catch {
|
|
1441
|
+
return { version: 1, skills: {}, mcpServers: {} };
|
|
1442
|
+
}
|
|
1443
|
+
}
|
|
1444
|
+
async function updateLockFile(updater) {
|
|
1445
|
+
await acquireLockGuard();
|
|
1446
|
+
try {
|
|
1447
|
+
const lock = await readLockFile();
|
|
1448
|
+
await updater(lock);
|
|
1449
|
+
await writeLockFileUnsafe(lock);
|
|
1450
|
+
return lock;
|
|
1451
|
+
} finally {
|
|
1452
|
+
await releaseLockGuard();
|
|
1453
|
+
}
|
|
1454
|
+
}
|
|
1455
|
+
|
|
1456
|
+
// src/core/skills/lock.ts
|
|
2245
1457
|
var execFileAsync = promisify(execFile);
|
|
2246
1458
|
async function recordSkillInstall(skillName, scopedName, source, sourceType, agents, canonicalPath, isGlobal, projectDir, version) {
|
|
2247
1459
|
await updateLockFile((lock) => {
|
|
@@ -2951,7 +2163,7 @@ async function recommendSkills2(query, criteria, options = {}) {
|
|
|
2951
2163
|
}
|
|
2952
2164
|
|
|
2953
2165
|
// src/core/skills/library-loader.ts
|
|
2954
|
-
import { existsSync as
|
|
2166
|
+
import { existsSync as existsSync9, readdirSync, readFileSync } from "fs";
|
|
2955
2167
|
import { createRequire } from "module";
|
|
2956
2168
|
import { basename as basename2, dirname as dirname2, join as join4 } from "path";
|
|
2957
2169
|
var require2 = createRequire(import.meta.url);
|
|
@@ -3000,7 +2212,7 @@ function loadLibraryFromModule(root) {
|
|
|
3000
2212
|
}
|
|
3001
2213
|
function buildLibraryFromFiles(root) {
|
|
3002
2214
|
const catalogPath = join4(root, "skills.json");
|
|
3003
|
-
if (!
|
|
2215
|
+
if (!existsSync9(catalogPath)) {
|
|
3004
2216
|
throw new Error(`No skills.json found at ${root}`);
|
|
3005
2217
|
}
|
|
3006
2218
|
const catalogData = JSON.parse(readFileSync(catalogPath, "utf-8"));
|
|
@@ -3008,7 +2220,7 @@ function buildLibraryFromFiles(root) {
|
|
|
3008
2220
|
const version = catalogData.version ?? "0.0.0";
|
|
3009
2221
|
const manifestPath = join4(root, "skills", "manifest.json");
|
|
3010
2222
|
let manifest;
|
|
3011
|
-
if (
|
|
2223
|
+
if (existsSync9(manifestPath)) {
|
|
3012
2224
|
manifest = JSON.parse(readFileSync(manifestPath, "utf-8"));
|
|
3013
2225
|
} else {
|
|
3014
2226
|
manifest = {
|
|
@@ -3020,7 +2232,7 @@ function buildLibraryFromFiles(root) {
|
|
|
3020
2232
|
}
|
|
3021
2233
|
const profilesDir = join4(root, "profiles");
|
|
3022
2234
|
const profiles = /* @__PURE__ */ new Map();
|
|
3023
|
-
if (
|
|
2235
|
+
if (existsSync9(profilesDir)) {
|
|
3024
2236
|
for (const file of readdirSync(profilesDir)) {
|
|
3025
2237
|
if (!file.endsWith(".json")) continue;
|
|
3026
2238
|
try {
|
|
@@ -3069,7 +2281,7 @@ function buildLibraryFromFiles(root) {
|
|
|
3069
2281
|
return resolveDeps([...new Set(skills)]);
|
|
3070
2282
|
}
|
|
3071
2283
|
function discoverFiles(dir, ext) {
|
|
3072
|
-
if (!
|
|
2284
|
+
if (!existsSync9(dir)) return [];
|
|
3073
2285
|
return readdirSync(dir).filter((f) => f.endsWith(ext)).map((f) => basename2(f, ext));
|
|
3074
2286
|
}
|
|
3075
2287
|
const library = {
|
|
@@ -3093,7 +2305,7 @@ function buildLibraryFromFiles(root) {
|
|
|
3093
2305
|
getSkillDir: getSkillDir2,
|
|
3094
2306
|
readSkillContent(name) {
|
|
3095
2307
|
const skillPath = library.getSkillPath(name);
|
|
3096
|
-
if (!
|
|
2308
|
+
if (!existsSync9(skillPath)) {
|
|
3097
2309
|
throw new Error(`Skill content not found: ${skillPath}`);
|
|
3098
2310
|
}
|
|
3099
2311
|
return readFileSync(skillPath, "utf-8");
|
|
@@ -3124,7 +2336,7 @@ function buildLibraryFromFiles(root) {
|
|
|
3124
2336
|
},
|
|
3125
2337
|
getSharedResourcePath(name) {
|
|
3126
2338
|
const resourcePath = join4(root, "skills", "_shared", `${name}.md`);
|
|
3127
|
-
return
|
|
2339
|
+
return existsSync9(resourcePath) ? resourcePath : void 0;
|
|
3128
2340
|
},
|
|
3129
2341
|
readSharedResource(name) {
|
|
3130
2342
|
const resourcePath = library.getSharedResourcePath(name);
|
|
@@ -3138,9 +2350,9 @@ function buildLibraryFromFiles(root) {
|
|
|
3138
2350
|
},
|
|
3139
2351
|
getProtocolPath(name) {
|
|
3140
2352
|
const rootPath = join4(root, "protocols", `${name}.md`);
|
|
3141
|
-
if (
|
|
2353
|
+
if (existsSync9(rootPath)) return rootPath;
|
|
3142
2354
|
const skillsPath = join4(root, "skills", "protocols", `${name}.md`);
|
|
3143
|
-
return
|
|
2355
|
+
return existsSync9(skillsPath) ? skillsPath : void 0;
|
|
3144
2356
|
},
|
|
3145
2357
|
readProtocol(name) {
|
|
3146
2358
|
const protocolPath = library.getProtocolPath(name);
|
|
@@ -3166,7 +2378,7 @@ function buildLibraryFromFiles(root) {
|
|
|
3166
2378
|
issues.push({ level: "warn", field: "version", message: "Missing version" });
|
|
3167
2379
|
}
|
|
3168
2380
|
const skillPath = join4(root, entry.path);
|
|
3169
|
-
if (!
|
|
2381
|
+
if (!existsSync9(skillPath)) {
|
|
3170
2382
|
issues.push({
|
|
3171
2383
|
level: "error",
|
|
3172
2384
|
field: "path",
|
|
@@ -3225,7 +2437,7 @@ __export(catalog_exports, {
|
|
|
3225
2437
|
validateAll: () => validateAll,
|
|
3226
2438
|
validateSkillFrontmatter: () => validateSkillFrontmatter
|
|
3227
2439
|
});
|
|
3228
|
-
import { existsSync as
|
|
2440
|
+
import { existsSync as existsSync10 } from "fs";
|
|
3229
2441
|
import { join as join5 } from "path";
|
|
3230
2442
|
var _library = null;
|
|
3231
2443
|
function registerSkillLibrary(library) {
|
|
@@ -3233,7 +2445,7 @@ function registerSkillLibrary(library) {
|
|
|
3233
2445
|
}
|
|
3234
2446
|
function registerSkillLibraryFromPath(root) {
|
|
3235
2447
|
const indexPath = join5(root, "index.js");
|
|
3236
|
-
if (
|
|
2448
|
+
if (existsSync10(indexPath)) {
|
|
3237
2449
|
_library = loadLibraryFromModule(root);
|
|
3238
2450
|
return;
|
|
3239
2451
|
}
|
|
@@ -3244,13 +2456,13 @@ function clearRegisteredLibrary() {
|
|
|
3244
2456
|
}
|
|
3245
2457
|
function discoverLibrary() {
|
|
3246
2458
|
const envPath = process.env["CAAMP_SKILL_LIBRARY"];
|
|
3247
|
-
if (envPath &&
|
|
2459
|
+
if (envPath && existsSync10(envPath)) {
|
|
3248
2460
|
try {
|
|
3249
2461
|
const indexPath = join5(envPath, "index.js");
|
|
3250
|
-
if (
|
|
2462
|
+
if (existsSync10(indexPath)) {
|
|
3251
2463
|
return loadLibraryFromModule(envPath);
|
|
3252
2464
|
}
|
|
3253
|
-
if (
|
|
2465
|
+
if (existsSync10(join5(envPath, "skills.json"))) {
|
|
3254
2466
|
return buildLibraryFromFiles(envPath);
|
|
3255
2467
|
}
|
|
3256
2468
|
} catch {
|
|
@@ -3357,13 +2569,13 @@ function getLibraryRoot() {
|
|
|
3357
2569
|
}
|
|
3358
2570
|
|
|
3359
2571
|
// src/core/skills/discovery.ts
|
|
3360
|
-
import { existsSync as
|
|
3361
|
-
import { readdir, readFile as
|
|
2572
|
+
import { existsSync as existsSync11 } from "fs";
|
|
2573
|
+
import { readdir, readFile as readFile6 } from "fs/promises";
|
|
3362
2574
|
import { join as join6 } from "path";
|
|
3363
2575
|
import matter from "gray-matter";
|
|
3364
2576
|
async function parseSkillFile(filePath) {
|
|
3365
2577
|
try {
|
|
3366
|
-
const content = await
|
|
2578
|
+
const content = await readFile6(filePath, "utf-8");
|
|
3367
2579
|
const { data } = matter(content);
|
|
3368
2580
|
if (!data.name || !data.description) {
|
|
3369
2581
|
return null;
|
|
@@ -3384,7 +2596,7 @@ async function parseSkillFile(filePath) {
|
|
|
3384
2596
|
}
|
|
3385
2597
|
async function discoverSkill(skillDir) {
|
|
3386
2598
|
const skillFile = join6(skillDir, "SKILL.md");
|
|
3387
|
-
if (!
|
|
2599
|
+
if (!existsSync11(skillFile)) return null;
|
|
3388
2600
|
const metadata = await parseSkillFile(skillFile);
|
|
3389
2601
|
if (!metadata) return null;
|
|
3390
2602
|
return {
|
|
@@ -3395,7 +2607,7 @@ async function discoverSkill(skillDir) {
|
|
|
3395
2607
|
};
|
|
3396
2608
|
}
|
|
3397
2609
|
async function discoverSkills(rootDir) {
|
|
3398
|
-
if (!
|
|
2610
|
+
if (!existsSync11(rootDir)) return [];
|
|
3399
2611
|
const entries = await readdir(rootDir, { withFileTypes: true });
|
|
3400
2612
|
const skills = [];
|
|
3401
2613
|
for (const entry of entries) {
|
|
@@ -3424,8 +2636,8 @@ async function discoverSkillsMulti(dirs) {
|
|
|
3424
2636
|
}
|
|
3425
2637
|
|
|
3426
2638
|
// src/core/skills/validator.ts
|
|
3427
|
-
import { existsSync as
|
|
3428
|
-
import { readFile as
|
|
2639
|
+
import { existsSync as existsSync12 } from "fs";
|
|
2640
|
+
import { readFile as readFile7 } from "fs/promises";
|
|
3429
2641
|
import matter2 from "gray-matter";
|
|
3430
2642
|
var RESERVED_NAMES = [
|
|
3431
2643
|
"anthropic",
|
|
@@ -3446,14 +2658,14 @@ var WARN_BODY_LINES = 500;
|
|
|
3446
2658
|
var WARN_DESCRIPTION_LENGTH = 50;
|
|
3447
2659
|
async function validateSkill(filePath) {
|
|
3448
2660
|
const issues = [];
|
|
3449
|
-
if (!
|
|
2661
|
+
if (!existsSync12(filePath)) {
|
|
3450
2662
|
return {
|
|
3451
2663
|
valid: false,
|
|
3452
2664
|
issues: [{ level: "error", field: "file", message: "File does not exist" }],
|
|
3453
2665
|
metadata: null
|
|
3454
2666
|
};
|
|
3455
2667
|
}
|
|
3456
|
-
const content = await
|
|
2668
|
+
const content = await readFile7(filePath, "utf-8");
|
|
3457
2669
|
if (!content.startsWith("---")) {
|
|
3458
2670
|
issues.push({
|
|
3459
2671
|
level: "error",
|
|
@@ -3563,28 +2775,13 @@ async function validateSkill(filePath) {
|
|
|
3563
2775
|
}
|
|
3564
2776
|
|
|
3565
2777
|
export {
|
|
2778
|
+
CANONICAL_SKILLS_DIR,
|
|
3566
2779
|
setVerbose,
|
|
3567
2780
|
setQuiet,
|
|
3568
2781
|
isVerbose,
|
|
3569
2782
|
isQuiet,
|
|
3570
2783
|
setHuman,
|
|
3571
2784
|
isHuman,
|
|
3572
|
-
deepMerge,
|
|
3573
|
-
getNestedValue,
|
|
3574
|
-
ensureDir,
|
|
3575
|
-
readConfig,
|
|
3576
|
-
writeConfig,
|
|
3577
|
-
removeConfig,
|
|
3578
|
-
resolveConfigPath,
|
|
3579
|
-
listMcpServers,
|
|
3580
|
-
listAgentsMcpServers,
|
|
3581
|
-
listAllMcpServers,
|
|
3582
|
-
removeMcpServer,
|
|
3583
|
-
getTransform,
|
|
3584
|
-
installMcpServer,
|
|
3585
|
-
installMcpServerToAll,
|
|
3586
|
-
buildServerConfig,
|
|
3587
|
-
CANONICAL_SKILLS_DIR,
|
|
3588
2785
|
detectProvider,
|
|
3589
2786
|
detectAllProviders,
|
|
3590
2787
|
getInstalledProviders,
|
|
@@ -3595,31 +2792,19 @@ export {
|
|
|
3595
2792
|
listCanonicalSkills,
|
|
3596
2793
|
selectProvidersByMinimumPriority,
|
|
3597
2794
|
installBatchWithRollback,
|
|
3598
|
-
detectMcpConfigConflicts,
|
|
3599
|
-
applyMcpInstallWithPolicy,
|
|
3600
2795
|
updateInstructionsSingleOperation,
|
|
3601
|
-
|
|
3602
|
-
|
|
3603
|
-
|
|
3604
|
-
|
|
3605
|
-
|
|
3606
|
-
|
|
3607
|
-
parseEnvAssignments,
|
|
3608
|
-
extractVersionTag,
|
|
3609
|
-
isCleoSource,
|
|
2796
|
+
deepMerge,
|
|
2797
|
+
getNestedValue,
|
|
2798
|
+
ensureDir,
|
|
2799
|
+
readConfig,
|
|
2800
|
+
writeConfig,
|
|
2801
|
+
removeConfig,
|
|
3610
2802
|
readLockFile,
|
|
3611
|
-
recordMcpInstall,
|
|
3612
|
-
removeMcpFromLock,
|
|
3613
|
-
getTrackedMcpServers,
|
|
3614
|
-
saveLastSelectedAgents,
|
|
3615
|
-
getLastSelectedAgents,
|
|
3616
|
-
inferCleoLockData,
|
|
3617
|
-
reconcileCleoLock,
|
|
3618
|
-
parseSource,
|
|
3619
|
-
isMarketplaceScoped,
|
|
3620
2803
|
scanFile,
|
|
3621
2804
|
scanDirectory,
|
|
3622
2805
|
toSarif,
|
|
2806
|
+
parseSource,
|
|
2807
|
+
isMarketplaceScoped,
|
|
3623
2808
|
recordSkillInstall,
|
|
3624
2809
|
removeSkillFromLock,
|
|
3625
2810
|
getTrackedSkills,
|
|
@@ -3654,4 +2839,4 @@ export {
|
|
|
3654
2839
|
discoverSkillsMulti,
|
|
3655
2840
|
validateSkill
|
|
3656
2841
|
};
|
|
3657
|
-
//# sourceMappingURL=chunk-
|
|
2842
|
+
//# sourceMappingURL=chunk-6NBM4CAF.js.map
|