@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.
@@ -2,7 +2,7 @@ import {
2
2
  getAllProviders,
3
3
  groupByInstructFile,
4
4
  injectAll
5
- } from "./chunk-5UUABVWS.js";
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-3IEKCREL.js";
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/formats/utils.ts
47
- function deepMerge(target, source) {
48
- const result = { ...target };
49
- for (const key of Object.keys(source)) {
50
- const sourceVal = source[key];
51
- const targetVal = target[key];
52
- if (sourceVal !== null && typeof sourceVal === "object" && !Array.isArray(sourceVal) && targetVal !== null && typeof targetVal === "object" && !Array.isArray(targetVal)) {
53
- result[key] = deepMerge(
54
- targetVal,
55
- sourceVal
56
- );
57
- } else {
58
- result[key] = sourceVal;
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
- async function ensureDir(filePath) {
73
- const { mkdir: mkdir4 } = await import("fs/promises");
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
- // src/core/formats/json.ts
79
- import { existsSync } from "fs";
80
- import { readFile, writeFile } from "fs/promises";
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 detectIndent(content) {
94
- const lines = content.split("\n");
95
- for (const line of lines) {
96
- const match = line.match(/^(\s+)/);
97
- if (match?.[1]) {
98
- const ws = match[1];
99
- if (ws.startsWith(" ")) {
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
- async function writeJsonConfig(filePath, configKey, serverName, serverConfig) {
108
- await ensureDir(filePath);
109
- let content;
110
- if (existsSync(filePath)) {
111
- content = await readFile(filePath, "utf-8");
112
- if (!content.trim()) {
113
- content = "{}";
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
- const { tabSize, insertSpaces } = detectIndent(content);
119
- const formatOptions = {
120
- tabSize,
121
- insertSpaces,
122
- eol: "\n"
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
- async function removeJsonConfig(filePath, configKey, serverName) {
136
- if (!existsSync(filePath)) return false;
137
- let content = await readFile(filePath, "utf-8");
138
- if (!content.trim()) return false;
139
- const { tabSize, insertSpaces } = detectIndent(content);
140
- const formatOptions = {
141
- tabSize,
142
- insertSpaces,
143
- eol: "\n"
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
- // src/core/formats/toml.ts
158
- import { existsSync as existsSync2 } from "fs";
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
- async function writeTomlConfig(filePath, configKey, serverName, serverConfig) {
169
- await ensureDir(filePath);
170
- const existing = await readTomlConfig(filePath);
171
- const keyParts = configKey.split(".");
172
- let newEntry = { [serverName]: serverConfig };
173
- for (const part of [...keyParts].reverse()) {
174
- newEntry = { [part]: newEntry };
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 merged = deepMerge(existing, newEntry);
177
- const content = TOML.stringify(merged);
178
- await writeFile2(filePath, content, "utf-8");
167
+ const results = providers.map(detectProvider);
168
+ setCachedResults(signature, results);
169
+ return cloneDetectionResults(results);
179
170
  }
180
- async function removeTomlConfig(filePath, configKey, serverName) {
181
- if (!existsSync2(filePath)) return false;
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
- // src/core/formats/yaml.ts
198
- import { existsSync as existsSync3 } from "fs";
199
- import { readFile as readFile3, writeFile as writeFile3 } from "fs/promises";
200
- import yaml from "js-yaml";
201
- async function readYamlConfig(filePath) {
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
- async function writeYamlConfig(filePath, configKey, serverName, serverConfig) {
209
- await ensureDir(filePath);
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/mcp/reader.ts
291
- import { existsSync as existsSync4 } from "fs";
292
- function resolveConfigPath(provider, scope, projectDir) {
293
- return resolveProviderConfigPath(provider, scope, projectDir ?? process.cwd());
294
- }
295
- async function listMcpServers(provider, scope, projectDir) {
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 listAgentsMcpServers(scope, projectDir) {
320
- const serversPath = getAgentsMcpServersPath(scope, projectDir);
321
- debug(`listing .agents/ MCP servers (${scope}) at ${serversPath}`);
322
- if (!existsSync4(serversPath)) return [];
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
- const config = await readConfig(serversPath, "json");
325
- const servers = config["servers"];
326
- if (!servers || typeof servers !== "object") return [];
327
- const entries = [];
328
- for (const [name, cfg] of Object.entries(servers)) {
329
- entries.push({
330
- name,
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 listAllMcpServers(providers, scope, projectDir) {
344
- const seen = /* @__PURE__ */ new Set();
345
- const allEntries = [];
346
- const agentsServersPath = getAgentsMcpServersPath(scope, projectDir);
347
- const agentsEntries = await listAgentsMcpServers(scope, projectDir);
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
- for (const provider of providers) {
353
- const configPath = resolveConfigPath(provider, scope, projectDir);
354
- if (!configPath || seen.has(configPath)) continue;
355
- seen.add(configPath);
356
- const entries = await listMcpServers(provider, scope, projectDir);
357
- allEntries.push(...entries);
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
- return allEntries;
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
- name: serverName,
382
- type: "stdio",
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 transformZed(_serverName, config) {
391
- if (config.url) {
392
- return {
393
- source: "custom",
394
- type: config.type ?? "http",
395
- url: config.url,
396
- ...config.headers ? { headers: config.headers } : {}
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
- source: "custom",
401
- command: config.command,
402
- args: config.args ?? [],
403
- ...config.env ? { env: config.env } : {}
261
+ name: skillName,
262
+ canonicalPath,
263
+ linkedAgents,
264
+ errors,
265
+ success: linkedAgents.length > 0
404
266
  };
405
267
  }
406
- function transformOpenCode(_serverName, config) {
407
- if (config.url) {
408
- return {
409
- type: "remote",
410
- url: config.url,
411
- enabled: true,
412
- ...config.headers ? { headers: config.headers } : {}
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
- return {
416
- type: "local",
417
- command: [config.command, ...config.args ?? []],
418
- enabled: true,
419
- ...config.env ? { environment: config.env } : {}
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 transformCursor(_serverName, config) {
437
- if (config.url) {
438
- return {
439
- url: config.url,
440
- ...config.headers ? { headers: config.headers } : {}
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/mcp/installer.ts
463
- function buildConfig(provider, serverName, config) {
464
- const transform = getTransform(provider.id);
465
- if (transform) {
466
- return transform(serverName, config);
467
- }
468
- return config;
469
- }
470
- async function installMcpServer(provider, serverName, config, scope = "project", projectDir) {
471
- const configPath = resolveConfigPath(provider, scope, projectDir);
472
- debug(`installing MCP server "${serverName}" for ${provider.id} (${scope})`);
473
- debug(` config path: ${configPath ?? "(none)"}`);
474
- if (!configPath) {
475
- return {
476
- provider,
477
- scope,
478
- configPath: "",
479
- success: false,
480
- error: `Provider ${provider.id} does not support ${scope} config`
481
- };
482
- }
483
- try {
484
- const transformedConfig = buildConfig(provider, serverName, config);
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
- async function installMcpServerToAll(providers, serverName, config, scope = "project", projectDir) {
511
- const results = [];
512
- for (const provider of providers) {
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 buildServerConfig(source, transport, headers) {
519
- if (source.type === "remote") {
520
- return {
521
- type: transport ?? "http",
522
- url: source.value,
523
- ...headers && Object.keys(headers).length > 0 ? { headers } : {}
524
- };
525
- }
526
- if (source.type === "package") {
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
- function detectProvider(provider) {
573
- const matchedMethods = [];
574
- const detection = provider.detection;
575
- debug(`detecting provider ${provider.id} via methods: ${detection.methods.join(", ")}`);
576
- for (const method of detection.methods) {
577
- switch (method) {
578
- case "binary":
579
- if (detection.binary && checkBinary(detection.binary)) {
580
- debug(` ${provider.id}: binary "${detection.binary}" found`);
581
- matchedMethods.push("binary");
582
- }
583
- break;
584
- case "directory":
585
- if (detection.directories) {
586
- for (const dir of detection.directories) {
587
- if (checkDirectory(dir)) {
588
- matchedMethods.push("directory");
589
- break;
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
- provider,
608
- installed: matchedMethods.length > 0,
609
- methods: matchedMethods,
610
- projectDetected: false
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 detectProjectProvider(provider, projectDir) {
651
- if (!provider.pathProject) return false;
652
- return existsSync5(resolveProviderProjectPath(provider, projectDir));
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
- return targetDir;
702
- }
703
- async function linkToAgent(canonicalPath, provider, skillName, isGlobal, projectDir) {
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 errors = [];
710
- let anySuccess = false;
711
- for (const targetSkillsDir of targetDirs) {
712
- if (!targetSkillsDir) continue;
713
- try {
714
- await mkdir(targetSkillsDir, { recursive: true });
715
- const linkPath = join2(targetSkillsDir, skillName);
716
- if (existsSync6(linkPath)) {
717
- const stat2 = lstatSync(linkPath);
718
- if (stat2.isSymbolicLink()) {
719
- await rm(linkPath);
720
- } else {
721
- await rm(linkPath, { recursive: true });
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
- async function configureProviderGlobalAndProject(provider, options) {
1151
- const projectDir = options.projectDir ?? process.cwd();
1152
- const globalOps = options.globalMcp ?? [];
1153
- const projectOps = options.projectMcp ?? [];
1154
- const globalResults = [];
1155
- for (const operation of globalOps) {
1156
- globalResults.push(
1157
- await installMcpServer(
1158
- provider,
1159
- operation.serverName,
1160
- operation.config,
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 splitCommand(command, explicitArgs = []) {
1255
- if (explicitArgs.length > 0) {
1256
- return { command, args: explicitArgs };
1257
- }
1258
- const parts = command.trim().split(/\s+/);
1259
- const binary = parts[0] ?? "";
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 normalizeEnv(env, channel, cleoDir) {
1269
- const result = { ...env ?? {} };
1270
- if (channel === "dev" && !result.CLEO_DIR) {
1271
- result.CLEO_DIR = cleoDir ?? CLEO_DEV_DIR_DEFAULT;
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
- function resolvePackageSpec(channel, version) {
1276
- const tag = version?.trim() || (channel === "stable" ? "latest" : "beta");
1277
- return `${CLEO_MCP_NPM_PACKAGE}@${tag}`;
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 buildCleoProfile(options) {
1280
- const channel = options.channel;
1281
- const serverName = resolveCleoServerName(channel);
1282
- if (channel === "dev") {
1283
- if (!options.command || options.command.trim() === "") {
1284
- throw new Error("Dev channel requires --command.");
1285
- }
1286
- const parsed = splitCommand(options.command, options.args ?? []);
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
- const packageSpec = resolvePackageSpec(channel, options.version);
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 checkCommandReachability(command) {
1317
- const hasPathSeparator = command.includes("/") || command.includes("\\");
1318
- if (hasPathSeparator || command.startsWith("~")) {
1319
- const expanded = expandHome(command);
1320
- const candidate = isAbsolute(expanded) ? expanded : resolve(process.cwd(), expanded);
1321
- if (existsSync8(candidate)) {
1322
- return { reachable: true, method: "path", detail: candidate };
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
- return { reachable: false, method: "path", detail: candidate };
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
- function parseEnvAssignments(values) {
1335
- const env = {};
1336
- for (const value of values) {
1337
- const idx = value.indexOf("=");
1338
- if (idx <= 0) {
1339
- throw new Error(`Invalid --env value "${value}". Use KEY=value.`);
1340
- }
1341
- const key = value.slice(0, idx).trim();
1342
- const val = value.slice(idx + 1).trim();
1343
- if (!key) {
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
- return env;
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
- return false;
594
+ await writeFile(filePath, content, "utf-8");
1378
595
  }
1379
- async function acquireLockGuard(retries = 40, delayMs = 25) {
1380
- await mkdir3(AGENTS_HOME, { recursive: true });
1381
- for (let attempt = 0; attempt < retries; attempt += 1) {
1382
- try {
1383
- const handle = await open(LOCK_GUARD_PATH, "wx");
1384
- await handle.close();
1385
- return;
1386
- } catch (error) {
1387
- if (!(error instanceof Error) || !("code" in error) || error.code !== "EEXIST") {
1388
- throw error;
1389
- }
1390
- if (attempt === 0) {
1391
- const removed = await removeStaleLock();
1392
- if (removed) continue;
1393
- }
1394
- await sleep(delayMs);
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
- throw new Error("Timed out waiting for lock file guard");
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
- async function writeLockFileUnsafe(lock) {
1403
- const tmpPath = `${LOCK_FILE_PATH}.tmp-${process.pid}-${Date.now()}`;
1404
- await writeFile5(tmpPath, JSON.stringify(lock, null, 2) + "\n", "utf-8");
1405
- await rename(tmpPath, LOCK_FILE_PATH);
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 readLockFile() {
1408
- try {
1409
- if (!existsSync9(LOCK_FILE_PATH)) {
1410
- return { version: 1, skills: {}, mcpServers: {} };
1411
- }
1412
- const content = await readFile5(LOCK_FILE_PATH, "utf-8");
1413
- return JSON.parse(content);
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 updateLockFile(updater) {
1419
- await acquireLockGuard();
1420
- try {
1421
- const lock = await readLockFile();
1422
- await updater(lock);
1423
- await writeLockFileUnsafe(lock);
1424
- return lock;
1425
- } finally {
1426
- await releaseLockGuard();
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/mcp/lock.ts
1431
- async function recordMcpInstall(serverName, source, sourceType, agents, isGlobal, version) {
1432
- await updateLockFile((lock) => {
1433
- const now = (/* @__PURE__ */ new Date()).toISOString();
1434
- const existing = lock.mcpServers[serverName];
1435
- lock.mcpServers[serverName] = {
1436
- name: serverName,
1437
- scopedName: serverName,
1438
- source,
1439
- sourceType,
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
- // src/core/mcp/reconcile.ts
1473
- function inferCleoLockData(config, channel) {
1474
- const command = typeof config.command === "string" ? config.command : "";
1475
- const args = Array.isArray(config.args) ? config.args.filter((a) => typeof a === "string") : [];
1476
- const packageArg = args.find((a) => a.includes(CLEO_MCP_NPM_PACKAGE));
1477
- if (packageArg) {
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 full = args.length > 0 ? `${command} ${args.join(" ")}` : command;
1493
- return {
1494
- source: full || "unknown",
1495
- sourceType: "command",
1496
- version: void 0
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 reconcileCleoLock(options = {}) {
1500
- const result = {
1501
- backfilled: [],
1502
- pruned: [],
1503
- alreadyTracked: 0,
1504
- errors: []
1505
- };
1506
- const lockEntries = await getTrackedMcpServers();
1507
- const providers = getInstalledProviders();
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 result;
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/sources/parser.ts
1612
- var GITHUB_SHORTHAND = /^([a-zA-Z0-9_.-]+)\/([a-zA-Z0-9_.-]+)(?:\/(.+))?$/;
1613
- var GITHUB_URL = /^https?:\/\/(?:www\.)?github\.com\/([^/]+)\/([^/]+)(?:\/(?:tree|blob)\/([^/]+)(?:\/(.+))?)?/;
1614
- var GITLAB_URL = /^https?:\/\/(?:www\.)?gitlab\.com\/([^/]+)\/([^/]+)(?:\/-\/(?:tree|blob)\/([^/]+)(?:\/(.+))?)?/;
1615
- var HTTP_URL = /^https?:\/\//;
1616
- var NPM_SCOPED = /^@[a-zA-Z0-9_.-]+\/[a-zA-Z0-9_.-]+$/;
1617
- var NPM_PACKAGE = /^[a-zA-Z0-9_.-]+$/;
1618
- var LIBRARY_SKILL = /^(@[a-zA-Z0-9_.-]+\/[a-zA-Z0-9_.-]+|[a-zA-Z0-9_.-]+):([a-zA-Z0-9_.-]+)$/;
1619
- function inferName(source, type) {
1620
- if (type === "library") {
1621
- const match = source.match(LIBRARY_SKILL);
1622
- return match?.[2] ?? source;
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 parseSource(input) {
1669
- const ghUrlMatch = input.match(GITHUB_URL);
1670
- if (ghUrlMatch) {
1671
- const owner = ghUrlMatch[1];
1672
- const repo = ghUrlMatch[2];
1673
- const path = ghUrlMatch[4];
1674
- if (!owner || !repo) {
1675
- return { type: "command", value: input, inferredName: inferName(input, "command") };
1676
- }
1677
- const inferredName = path ? path.split("/").pop() ?? repo : repo;
1678
- return {
1679
- type: "github",
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 isMarketplaceScoped(input) {
1772
- return /^@[a-zA-Z0-9_.-]+\/[a-zA-Z0-9_.-]+$/.test(input);
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 existsSync10 } from "fs";
1777
- import { readFile as readFile6 } from "fs/promises";
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 (!existsSync10(filePath)) {
1125
+ if (!existsSync7(filePath)) {
2150
1126
  return { file: filePath, findings: [], score: 100, passed: true };
2151
1127
  }
2152
- const content = await readFile6(filePath, "utf-8");
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 (!existsSync10(dirPath)) return [];
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 (existsSync10(skillFile)) {
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 existsSync11, readdirSync, readFileSync } from "fs";
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 (!existsSync11(catalogPath)) {
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 (existsSync11(manifestPath)) {
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 (existsSync11(profilesDir)) {
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 (!existsSync11(dir)) return [];
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 (!existsSync11(skillPath)) {
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 existsSync11(resourcePath) ? resourcePath : void 0;
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 (existsSync11(rootPath)) return rootPath;
2353
+ if (existsSync9(rootPath)) return rootPath;
3142
2354
  const skillsPath = join4(root, "skills", "protocols", `${name}.md`);
3143
- return existsSync11(skillsPath) ? skillsPath : void 0;
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 (!existsSync11(skillPath)) {
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 existsSync12 } from "fs";
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 (existsSync12(indexPath)) {
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 && existsSync12(envPath)) {
2459
+ if (envPath && existsSync10(envPath)) {
3248
2460
  try {
3249
2461
  const indexPath = join5(envPath, "index.js");
3250
- if (existsSync12(indexPath)) {
2462
+ if (existsSync10(indexPath)) {
3251
2463
  return loadLibraryFromModule(envPath);
3252
2464
  }
3253
- if (existsSync12(join5(envPath, "skills.json"))) {
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 existsSync13 } from "fs";
3361
- import { readdir, readFile as readFile7 } from "fs/promises";
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 readFile7(filePath, "utf-8");
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 (!existsSync13(skillFile)) return null;
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 (!existsSync13(rootDir)) return [];
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 existsSync14 } from "fs";
3428
- import { readFile as readFile8 } from "fs/promises";
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 (!existsSync14(filePath)) {
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 readFile8(filePath, "utf-8");
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
- configureProviderGlobalAndProject,
3602
- normalizeCleoChannel,
3603
- resolveCleoServerName,
3604
- resolveChannelFromServerName,
3605
- buildCleoProfile,
3606
- checkCommandReachability,
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-3WKBFXLE.js.map
2842
+ //# sourceMappingURL=chunk-6NBM4CAF.js.map