@cleocode/caamp 1.0.1 → 1.0.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.js CHANGED
@@ -32,6 +32,7 @@ import {
32
32
  installMcpServerToAll,
33
33
  installSkill,
34
34
  isCatalogAvailable,
35
+ isHuman,
35
36
  isMarketplaceScoped,
36
37
  isVerbose,
37
38
  listCanonicalSkills,
@@ -56,1019 +57,1187 @@ import {
56
57
  scanDirectory,
57
58
  scanFile,
58
59
  selectProvidersByMinimumPriority,
60
+ setHuman,
59
61
  setQuiet,
60
62
  setVerbose,
61
63
  toSarif,
62
64
  tokenizeCriteriaValue,
63
65
  updateInstructionsSingleOperation,
64
66
  validateSkill
65
- } from "./chunk-VAFYP7CI.js";
67
+ } from "./chunk-PBHH6KMJ.js";
66
68
 
67
69
  // src/cli.ts
68
70
  import { Command } from "commander";
69
71
 
70
- // src/commands/providers.ts
71
- import pc from "picocolors";
72
- function registerProvidersCommand(program2) {
73
- const providers = program2.command("providers").description("Manage AI agent providers");
74
- providers.command("list").description("List all supported providers").option("--json", "Output as JSON").option("--tier <tier>", "Filter by priority tier (high, medium, low)").action(async (opts) => {
75
- const all = opts.tier ? getProvidersByPriority(opts.tier) : getAllProviders();
76
- if (opts.json) {
77
- console.log(JSON.stringify(all, null, 2));
78
- return;
79
- }
80
- console.log(pc.bold(`
81
- CAMP Provider Registry v${getRegistryVersion()}`));
82
- console.log(pc.dim(`${getProviderCount()} providers
83
- `));
84
- const tiers = ["high", "medium", "low"];
85
- for (const tier of tiers) {
86
- const tierProviders = all.filter((p) => p.priority === tier);
87
- if (tierProviders.length === 0) continue;
88
- const tierLabel = tier === "high" ? pc.green("HIGH") : tier === "medium" ? pc.yellow("MEDIUM") : pc.dim("LOW");
89
- console.log(`${tierLabel} priority:`);
90
- for (const p of tierProviders) {
91
- const status = p.status === "active" ? pc.green("active") : p.status === "beta" ? pc.yellow("beta") : pc.dim(p.status);
92
- console.log(` ${pc.bold(p.agentFlag.padEnd(20))} ${p.toolName.padEnd(22)} ${p.vendor.padEnd(16)} [${status}]`);
93
- }
94
- console.log();
95
- }
96
- });
97
- providers.command("detect").description("Auto-detect installed providers").option("--json", "Output as JSON").option("--project", "Include project-level detection").action(async (opts) => {
98
- const results = opts.project ? detectProjectProviders(process.cwd()) : detectAllProviders();
99
- const installed = results.filter((r) => r.installed);
100
- if (opts.json) {
101
- console.log(JSON.stringify(installed.map((r) => ({
102
- id: r.provider.id,
103
- toolName: r.provider.toolName,
104
- methods: r.methods,
105
- projectDetected: r.projectDetected
106
- })), null, 2));
107
- return;
108
- }
109
- console.log(pc.bold(`
110
- Detected ${installed.length} installed providers:
111
- `));
112
- for (const r of installed) {
113
- const methods = r.methods.join(", ");
114
- const project = r.projectDetected ? pc.green(" [project]") : "";
115
- console.log(` ${pc.green("\u2713")} ${pc.bold(r.provider.toolName.padEnd(22))} via ${pc.dim(methods)}${project}`);
116
- }
117
- const notInstalled = results.filter((r) => !r.installed);
118
- if (notInstalled.length > 0) {
119
- console.log(pc.dim(`
120
- ${notInstalled.length} providers not detected`));
121
- }
122
- console.log();
123
- });
124
- providers.command("show").description("Show provider details").argument("<id>", "Provider ID or alias").option("--json", "Output as JSON").action(async (id, opts) => {
125
- const provider = getProvider(id);
126
- if (!provider) {
127
- console.error(pc.red(`Provider not found: ${id}`));
128
- process.exit(1);
129
- }
130
- if (opts.json) {
131
- console.log(JSON.stringify(provider, null, 2));
132
- return;
133
- }
134
- console.log(pc.bold(`
135
- ${provider.toolName}`));
136
- console.log(pc.dim(`by ${provider.vendor}
137
- `));
138
- console.log(` ID: ${provider.id}`);
139
- console.log(` Flag: --agent ${provider.agentFlag}`);
140
- if (provider.aliases.length > 0) {
141
- console.log(` Aliases: ${provider.aliases.join(", ")}`);
142
- }
143
- console.log(` Status: ${provider.status}`);
144
- console.log(` Priority: ${provider.priority}`);
145
- console.log();
146
- console.log(` Instruction: ${provider.instructFile}`);
147
- console.log(` Config format: ${provider.configFormat}`);
148
- console.log(` Config key: ${provider.configKey}`);
149
- console.log(` Transports: ${provider.supportedTransports.join(", ")}`);
150
- console.log(` Headers: ${provider.supportsHeaders ? "yes" : "no"}`);
151
- console.log();
152
- console.log(pc.dim(" Paths:"));
153
- console.log(` Global dir: ${provider.pathGlobal}`);
154
- console.log(` Project dir: ${provider.pathProject || "(none)"}`);
155
- console.log(` Global config: ${provider.configPathGlobal}`);
156
- console.log(` Project config: ${provider.configPathProject || "(none)"}`);
157
- console.log(` Global skills: ${provider.pathSkills}`);
158
- console.log(` Project skills: ${provider.pathProjectSkills || "(none)"}`);
159
- console.log();
160
- });
161
- }
162
-
163
- // src/commands/skills/install.ts
164
- import { existsSync } from "fs";
165
- import pc2 from "picocolors";
72
+ // src/commands/advanced/common.ts
73
+ import { readFile } from "fs/promises";
166
74
 
167
- // src/core/sources/github.ts
168
- import { simpleGit } from "simple-git";
169
- import { mkdtemp, rm } from "fs/promises";
170
- import { tmpdir } from "os";
171
- import { join } from "path";
172
- async function cloneRepo(owner, repo, ref, subPath) {
173
- const tmpDir = await mkdtemp(join(tmpdir(), "caamp-"));
174
- const repoUrl = `https://github.com/${owner}/${repo}.git`;
175
- const git = simpleGit();
176
- const cloneOptions = ["--depth", "1"];
177
- if (ref) {
178
- cloneOptions.push("--branch", ref);
75
+ // src/commands/advanced/lafs.ts
76
+ import { randomUUID } from "crypto";
77
+ import {
78
+ isRegisteredErrorCode
79
+ } from "@cleocode/lafs-protocol";
80
+ var LAFSCommandError = class extends Error {
81
+ code;
82
+ category;
83
+ recoverable;
84
+ suggestion;
85
+ retryAfterMs;
86
+ details;
87
+ constructor(code, message, suggestion, recoverable = true, details) {
88
+ super(message);
89
+ this.name = "LAFSCommandError";
90
+ this.code = code;
91
+ this.category = inferErrorCategory(code);
92
+ this.recoverable = recoverable;
93
+ this.suggestion = suggestion;
94
+ this.retryAfterMs = null;
95
+ this.details = details;
179
96
  }
180
- await git.clone(repoUrl, tmpDir, cloneOptions);
181
- const localPath = subPath ? join(tmpDir, subPath) : tmpDir;
97
+ };
98
+ function inferErrorCategory(code) {
99
+ if (code.includes("VALIDATION")) return "VALIDATION";
100
+ if (code.includes("NOT_FOUND")) return "NOT_FOUND";
101
+ if (code.includes("CONFLICT")) return "CONFLICT";
102
+ if (code.includes("AUTH")) return "AUTH";
103
+ if (code.includes("PERMISSION")) return "PERMISSION";
104
+ if (code.includes("RATE_LIMIT")) return "RATE_LIMIT";
105
+ if (code.includes("MIGRATION")) return "MIGRATION";
106
+ if (code.includes("CONTRACT")) return "CONTRACT";
107
+ return "INTERNAL";
108
+ }
109
+ function baseMeta(operation, mvi) {
182
110
  return {
183
- localPath,
184
- cleanup: async () => {
185
- try {
186
- await rm(tmpDir, { recursive: true });
187
- } catch {
188
- }
189
- }
111
+ specVersion: "1.0.0",
112
+ schemaVersion: "1.0.0",
113
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
114
+ operation,
115
+ requestId: randomUUID(),
116
+ transport: "cli",
117
+ strict: true,
118
+ mvi,
119
+ contextVersion: 0
190
120
  };
191
121
  }
192
-
193
- // src/core/sources/gitlab.ts
194
- import { simpleGit as simpleGit2 } from "simple-git";
195
- import { mkdtemp as mkdtemp2, rm as rm2 } from "fs/promises";
196
- import { tmpdir as tmpdir2 } from "os";
197
- import { join as join2 } from "path";
198
- async function cloneGitLabRepo(owner, repo, ref, subPath) {
199
- const tmpDir = await mkdtemp2(join2(tmpdir2(), "caamp-gl-"));
200
- const repoUrl = `https://gitlab.com/${owner}/${repo}.git`;
201
- const git = simpleGit2();
202
- const cloneOptions = ["--depth", "1"];
203
- if (ref) {
204
- cloneOptions.push("--branch", ref);
205
- }
206
- await git.clone(repoUrl, tmpDir, cloneOptions);
207
- const localPath = subPath ? join2(tmpDir, subPath) : tmpDir;
208
- return {
209
- localPath,
210
- cleanup: async () => {
211
- try {
212
- await rm2(tmpDir, { recursive: true });
213
- } catch {
214
- }
215
- }
122
+ function emitSuccess(operation, result, mvi = true) {
123
+ const envelope = {
124
+ $schema: "https://lafs.dev/schemas/v1/envelope.schema.json",
125
+ _meta: {
126
+ ...baseMeta(operation, mvi)
127
+ },
128
+ success: true,
129
+ result,
130
+ error: null,
131
+ page: null
216
132
  };
133
+ console.log(JSON.stringify(envelope, null, 2));
217
134
  }
218
-
219
- // src/commands/skills/install.ts
220
- function registerSkillsInstall(parent) {
221
- parent.command("install").description("Install a skill from GitHub, URL, marketplace, or ct-skills catalog").argument("[source]", "Skill source (GitHub URL, owner/repo, @author/name, skill-name)").option("-a, --agent <name>", "Target specific agent(s)", (v, prev) => [...prev, v], []).option("-g, --global", "Install globally").option("-y, --yes", "Skip confirmation").option("--all", "Install to all detected agents").option("--profile <name>", "Install a ct-skills profile (minimal, core, recommended, full)").action(async (source, opts) => {
222
- let providers;
223
- if (opts.all) {
224
- providers = getInstalledProviders();
225
- } else if (opts.agent.length > 0) {
226
- providers = opts.agent.map((a) => getProvider(a)).filter((p) => p !== void 0);
227
- } else {
228
- providers = getInstalledProviders();
229
- }
230
- if (providers.length === 0) {
231
- console.error(pc2.red("No target providers found. Use --agent or --all."));
232
- process.exit(1);
233
- }
234
- if (opts.profile) {
235
- if (!isCatalogAvailable()) {
236
- console.error(pc2.red("@cleocode/ct-skills is not installed. Run: npm install @cleocode/ct-skills"));
237
- process.exit(1);
238
- }
239
- const profileSkills = resolveProfile(opts.profile);
240
- if (profileSkills.length === 0) {
241
- const available = listProfiles();
242
- console.error(pc2.red(`Profile not found: ${opts.profile}`));
243
- if (available.length > 0) {
244
- console.log(pc2.dim("Available profiles: " + available.join(", ")));
245
- }
246
- process.exit(1);
247
- }
248
- console.log(`Installing profile ${pc2.bold(opts.profile)} (${profileSkills.length} skill(s))...`);
249
- console.log(pc2.dim(`Target: ${providers.length} provider(s)`));
250
- let installed = 0;
251
- let failed = 0;
252
- for (const name of profileSkills) {
253
- const skillDir = getSkillDir(name);
254
- try {
255
- const result = await installSkill(
256
- skillDir,
257
- name,
258
- providers,
259
- opts.global ?? false
260
- );
261
- if (result.success) {
262
- console.log(pc2.green(` + ${name}`));
263
- await recordSkillInstall(
264
- name,
265
- `@cleocode/ct-skills:${name}`,
266
- `@cleocode/ct-skills:${name}`,
267
- "package",
268
- result.linkedAgents,
269
- result.canonicalPath,
270
- true
271
- );
272
- installed++;
273
- } else {
274
- console.log(pc2.yellow(` ! ${name}: ${result.errors.join(", ")}`));
275
- failed++;
276
- }
277
- } catch (err) {
278
- console.log(pc2.red(` x ${name}: ${err instanceof Error ? err.message : String(err)}`));
279
- failed++;
135
+ function emitError(operation, error, mvi = true) {
136
+ let envelope;
137
+ if (error instanceof LAFSCommandError) {
138
+ envelope = {
139
+ $schema: "https://lafs.dev/schemas/v1/envelope.schema.json",
140
+ _meta: {
141
+ ...baseMeta(operation, mvi)
142
+ },
143
+ success: false,
144
+ result: null,
145
+ error: {
146
+ code: isRegisteredErrorCode(error.code) ? error.code : "E_INTERNAL_UNEXPECTED",
147
+ message: error.message,
148
+ category: error.category,
149
+ retryable: error.recoverable,
150
+ retryAfterMs: error.retryAfterMs,
151
+ details: {
152
+ hint: error.suggestion,
153
+ ...error.details !== void 0 ? { payload: error.details } : {}
280
154
  }
281
- }
282
- console.log(`
283
- ${pc2.green(`${installed} installed`)}, ${failed > 0 ? pc2.yellow(`${failed} failed`) : "0 failed"}`);
284
- return;
285
- }
286
- if (!source) {
287
- console.error(pc2.red("Missing required argument: source"));
288
- console.log(pc2.dim("Usage: caamp skills install <source> or caamp skills install --profile <name>"));
289
- process.exit(1);
290
- }
291
- console.log(pc2.dim(`Installing to ${providers.length} provider(s)...`));
292
- let localPath;
293
- let cleanup;
294
- let skillName;
295
- let sourceValue;
296
- let sourceType;
297
- if (isMarketplaceScoped(source)) {
298
- console.log(pc2.dim(`Searching marketplace for ${source}...`));
299
- const client = new MarketplaceClient();
300
- let skill;
301
- try {
302
- skill = await client.getSkill(source);
303
- } catch (error) {
304
- console.error(pc2.red(`Marketplace lookup failed: ${formatNetworkError(error)}`));
305
- process.exit(1);
306
- }
307
- if (!skill) {
308
- console.error(pc2.red(`Skill not found: ${source}`));
309
- process.exit(1);
310
- }
311
- console.log(` Found: ${pc2.bold(skill.name)} by ${skill.author} (${pc2.dim(skill.repoFullName)})`);
312
- const parsed = parseSource(skill.githubUrl);
313
- if (parsed.type !== "github" || !parsed.owner || !parsed.repo) {
314
- console.error(pc2.red("Could not resolve GitHub source"));
315
- process.exit(1);
316
- }
317
- try {
318
- const subPathCandidates = buildSkillSubPathCandidates(skill.path, parsed.path);
319
- let cloneError;
320
- let cloned = false;
321
- for (const subPath of subPathCandidates) {
322
- try {
323
- const result = await cloneRepo(parsed.owner, parsed.repo, parsed.ref, subPath);
324
- if (subPath && !existsSync(result.localPath)) {
325
- await result.cleanup();
326
- continue;
327
- }
328
- localPath = result.localPath;
329
- cleanup = result.cleanup;
330
- cloned = true;
331
- break;
332
- } catch (error) {
333
- cloneError = error;
334
- }
335
- }
336
- if (!cloned) {
337
- throw cloneError ?? new Error("Unable to resolve skill path from marketplace metadata");
155
+ },
156
+ page: null
157
+ };
158
+ } else {
159
+ envelope = {
160
+ $schema: "https://lafs.dev/schemas/v1/envelope.schema.json",
161
+ _meta: {
162
+ ...baseMeta(operation, mvi)
163
+ },
164
+ success: false,
165
+ result: null,
166
+ error: {
167
+ code: "E_INTERNAL_UNEXPECTED",
168
+ message: error instanceof Error ? error.message : String(error),
169
+ category: "INTERNAL",
170
+ retryable: false,
171
+ retryAfterMs: null,
172
+ details: {
173
+ hint: "Rerun with --verbose and validate your inputs."
338
174
  }
339
- skillName = skill.name;
340
- sourceValue = skill.githubUrl;
341
- sourceType = parsed.type;
342
- } catch (error) {
343
- console.error(pc2.red(`Failed to fetch source repository: ${formatNetworkError(error)}`));
344
- process.exit(1);
175
+ },
176
+ page: null
177
+ };
178
+ }
179
+ console.error(JSON.stringify(envelope, null, 2));
180
+ }
181
+ async function runLafsCommand(command, mvi, action) {
182
+ try {
183
+ const result = await action();
184
+ emitSuccess(command, result, mvi);
185
+ } catch (error) {
186
+ emitError(command, error, mvi);
187
+ process.exit(1);
188
+ }
189
+ }
190
+
191
+ // src/commands/advanced/common.ts
192
+ var VALID_PRIORITIES = /* @__PURE__ */ new Set(["high", "medium", "low"]);
193
+ function parsePriority(value) {
194
+ if (!VALID_PRIORITIES.has(value)) {
195
+ throw new LAFSCommandError(
196
+ "E_ADVANCED_VALIDATION_PRIORITY",
197
+ `Invalid tier: ${value}`,
198
+ "Use one of: high, medium, low."
199
+ );
200
+ }
201
+ return value;
202
+ }
203
+ function resolveProviders(options) {
204
+ if (options.all) {
205
+ return getAllProviders();
206
+ }
207
+ const targetAgents = options.agent ?? [];
208
+ if (targetAgents.length === 0) {
209
+ return getInstalledProviders();
210
+ }
211
+ const providers = targetAgents.map((id) => getProvider(id)).filter((provider) => provider !== void 0);
212
+ if (providers.length !== targetAgents.length) {
213
+ const found = new Set(providers.map((provider) => provider.id));
214
+ const missing = targetAgents.filter((id) => !found.has(id));
215
+ throw new LAFSCommandError(
216
+ "E_ADVANCED_PROVIDER_NOT_FOUND",
217
+ `Unknown provider(s): ${missing.join(", ")}`,
218
+ "Check `caamp providers list` for valid provider IDs/aliases."
219
+ );
220
+ }
221
+ return providers;
222
+ }
223
+ async function readJsonFile(path) {
224
+ try {
225
+ const raw = await readFile(path, "utf-8");
226
+ return JSON.parse(raw);
227
+ } catch (error) {
228
+ throw new LAFSCommandError(
229
+ "E_ADVANCED_INPUT_JSON",
230
+ `Failed to read JSON file: ${path}`,
231
+ "Confirm the path exists and contains valid JSON.",
232
+ true,
233
+ { reason: error instanceof Error ? error.message : String(error) }
234
+ );
235
+ }
236
+ }
237
+ async function readMcpOperations(path) {
238
+ const value = await readJsonFile(path);
239
+ if (!Array.isArray(value)) {
240
+ throw new LAFSCommandError(
241
+ "E_ADVANCED_VALIDATION_MCP_ARRAY",
242
+ `MCP operations file must be a JSON array: ${path}`,
243
+ "Provide an array of objects with serverName and config fields."
244
+ );
245
+ }
246
+ const operations = [];
247
+ for (const [index, item] of value.entries()) {
248
+ if (!item || typeof item !== "object") {
249
+ throw new LAFSCommandError(
250
+ "E_ADVANCED_VALIDATION_MCP_ITEM",
251
+ `Invalid MCP operation at index ${index}`,
252
+ "Each operation must be an object with serverName and config."
253
+ );
254
+ }
255
+ const obj = item;
256
+ const serverName = obj.serverName;
257
+ const config = obj.config;
258
+ const scope = obj.scope;
259
+ if (typeof serverName !== "string" || serverName.length === 0) {
260
+ throw new LAFSCommandError(
261
+ "E_ADVANCED_VALIDATION_MCP_NAME",
262
+ `Invalid serverName at index ${index}`,
263
+ "Set serverName to a non-empty string."
264
+ );
265
+ }
266
+ if (!config || typeof config !== "object" || Array.isArray(config)) {
267
+ throw new LAFSCommandError(
268
+ "E_ADVANCED_VALIDATION_MCP_CONFIG",
269
+ `Invalid config at index ${index}`,
270
+ "Set config to an object matching McpServerConfig."
271
+ );
272
+ }
273
+ if (scope !== void 0 && scope !== "project" && scope !== "global") {
274
+ throw new LAFSCommandError(
275
+ "E_ADVANCED_VALIDATION_SCOPE",
276
+ `Invalid scope at index ${index}: ${String(scope)}`,
277
+ "Use scope value 'project' or 'global'."
278
+ );
279
+ }
280
+ operations.push({
281
+ serverName,
282
+ config,
283
+ ...scope ? { scope } : {}
284
+ });
285
+ }
286
+ return operations;
287
+ }
288
+ async function readSkillOperations(path) {
289
+ const value = await readJsonFile(path);
290
+ if (!Array.isArray(value)) {
291
+ throw new LAFSCommandError(
292
+ "E_ADVANCED_VALIDATION_SKILL_ARRAY",
293
+ `Skill operations file must be a JSON array: ${path}`,
294
+ "Provide an array of objects with sourcePath and skillName fields."
295
+ );
296
+ }
297
+ const operations = [];
298
+ for (const [index, item] of value.entries()) {
299
+ if (!item || typeof item !== "object") {
300
+ throw new LAFSCommandError(
301
+ "E_ADVANCED_VALIDATION_SKILL_ITEM",
302
+ `Invalid skill operation at index ${index}`,
303
+ "Each operation must be an object with sourcePath and skillName."
304
+ );
305
+ }
306
+ const obj = item;
307
+ const sourcePath = obj.sourcePath;
308
+ const skillName = obj.skillName;
309
+ const isGlobal = obj.isGlobal;
310
+ if (typeof sourcePath !== "string" || sourcePath.length === 0) {
311
+ throw new LAFSCommandError(
312
+ "E_ADVANCED_VALIDATION_SKILL_SOURCE",
313
+ `Invalid sourcePath at index ${index}`,
314
+ "Set sourcePath to a non-empty string."
315
+ );
316
+ }
317
+ if (typeof skillName !== "string" || skillName.length === 0) {
318
+ throw new LAFSCommandError(
319
+ "E_ADVANCED_VALIDATION_SKILL_NAME",
320
+ `Invalid skillName at index ${index}`,
321
+ "Set skillName to a non-empty string."
322
+ );
323
+ }
324
+ if (isGlobal !== void 0 && typeof isGlobal !== "boolean") {
325
+ throw new LAFSCommandError(
326
+ "E_ADVANCED_VALIDATION_SKILL_SCOPE",
327
+ `Invalid isGlobal value at index ${index}`,
328
+ "Set isGlobal to true or false when provided."
329
+ );
330
+ }
331
+ operations.push({
332
+ sourcePath,
333
+ skillName,
334
+ ...isGlobal !== void 0 ? { isGlobal } : {}
335
+ });
336
+ }
337
+ return operations;
338
+ }
339
+ async function readTextInput(inlineContent, filePath) {
340
+ if (inlineContent && filePath) {
341
+ throw new LAFSCommandError(
342
+ "E_ADVANCED_VALIDATION_INPUT_MODE",
343
+ "Provide either inline content or a content file, not both.",
344
+ "Use --content OR --content-file."
345
+ );
346
+ }
347
+ if (inlineContent) return inlineContent;
348
+ if (!filePath) return void 0;
349
+ try {
350
+ return await readFile(filePath, "utf-8");
351
+ } catch (error) {
352
+ throw new LAFSCommandError(
353
+ "E_ADVANCED_INPUT_TEXT",
354
+ `Failed to read content file: ${filePath}`,
355
+ "Confirm the file exists and is readable.",
356
+ true,
357
+ { reason: error instanceof Error ? error.message : String(error) }
358
+ );
359
+ }
360
+ }
361
+
362
+ // src/commands/advanced/providers.ts
363
+ function registerAdvancedProviders(parent) {
364
+ parent.command("providers").description("Select providers by priority using advanced wrapper logic").option("-a, --agent <name>", "Target specific provider(s)", (v, prev) => [...prev, v], []).option("--all", "Use all registry providers (not only detected)").option("--min-tier <tier>", "Minimum priority tier: high|medium|low", "low").option("--details", "Include full provider objects").action(async (opts) => runLafsCommand("advanced.providers", !opts.details, async () => {
365
+ const providers = resolveProviders({ all: opts.all, agent: opts.agent });
366
+ const minTier = parsePriority(opts.minTier);
367
+ const selected = selectProvidersByMinimumPriority(providers, minTier);
368
+ return {
369
+ objective: "Filter providers by minimum priority tier",
370
+ constraints: {
371
+ minTier,
372
+ selectionMode: opts.all ? "registry" : "detected-or-explicit"
373
+ },
374
+ acceptanceCriteria: {
375
+ selectedCount: selected.length,
376
+ orderedByPriority: true
377
+ },
378
+ data: opts.details ? selected : selected.map((provider) => ({
379
+ id: provider.id,
380
+ priority: provider.priority,
381
+ status: provider.status,
382
+ configFormat: provider.configFormat
383
+ }))
384
+ };
385
+ }));
386
+ }
387
+
388
+ // src/commands/advanced/batch.ts
389
+ function registerAdvancedBatch(parent) {
390
+ parent.command("batch").description("Run rollback-capable batch install for MCP + skills").option("-a, --agent <name>", "Target specific provider(s)", (v, prev) => [...prev, v], []).option("--all", "Use all registry providers (not only detected)").option("--min-tier <tier>", "Minimum priority tier: high|medium|low", "low").option("--mcp-file <path>", "JSON file containing McpBatchOperation[]").option("--skills-file <path>", "JSON file containing SkillBatchOperation[]").option("--project-dir <path>", "Project directory to resolve project-scope paths").option("--details", "Include detailed operation result").action(async (opts) => runLafsCommand("advanced.batch", !opts.details, async () => {
391
+ const baseProviders = resolveProviders({ all: opts.all, agent: opts.agent });
392
+ const minimumPriority = parsePriority(opts.minTier);
393
+ const providers = selectProvidersByMinimumPriority(baseProviders, minimumPriority);
394
+ const mcp = opts.mcpFile ? await readMcpOperations(opts.mcpFile) : [];
395
+ const skills = opts.skillsFile ? await readSkillOperations(opts.skillsFile) : [];
396
+ if (mcp.length === 0 && skills.length === 0) {
397
+ throw new LAFSCommandError(
398
+ "E_ADVANCED_VALIDATION_NO_OPS",
399
+ "No operations provided.",
400
+ "Provide --mcp-file and/or --skills-file."
401
+ );
402
+ }
403
+ if (providers.length === 0) {
404
+ throw new LAFSCommandError(
405
+ "E_ADVANCED_NO_TARGET_PROVIDERS",
406
+ "No target providers resolved for this batch operation.",
407
+ "Use --all or pass provider IDs with --agent."
408
+ );
409
+ }
410
+ const result = await installBatchWithRollback({
411
+ providers,
412
+ minimumPriority,
413
+ mcp,
414
+ skills,
415
+ projectDir: opts.projectDir
416
+ });
417
+ if (!result.success) {
418
+ throw new LAFSCommandError(
419
+ "E_ADVANCED_BATCH_FAILED",
420
+ result.error ?? "Batch operation failed.",
421
+ "Check rollbackErrors and input configs, then retry.",
422
+ true,
423
+ result
424
+ );
425
+ }
426
+ return {
427
+ objective: "Install MCP and skills with rollback safety",
428
+ constraints: {
429
+ minimumPriority,
430
+ providerCount: providers.length,
431
+ mcpOps: mcp.length,
432
+ skillOps: skills.length
433
+ },
434
+ acceptanceCriteria: {
435
+ success: result.success,
436
+ rollbackPerformed: result.rollbackPerformed
437
+ },
438
+ data: opts.details ? result : {
439
+ providerCount: result.providerIds.length,
440
+ mcpApplied: result.mcpApplied,
441
+ skillsApplied: result.skillsApplied,
442
+ rollbackPerformed: result.rollbackPerformed
345
443
  }
346
- } else {
347
- const parsed = parseSource(source);
348
- skillName = parsed.inferredName;
349
- sourceValue = parsed.value;
350
- sourceType = parsed.type;
351
- if (parsed.type === "github" && parsed.owner && parsed.repo) {
352
- try {
353
- const result = await cloneRepo(parsed.owner, parsed.repo, parsed.ref, parsed.path);
354
- localPath = result.localPath;
355
- cleanup = result.cleanup;
356
- } catch (error) {
357
- console.error(pc2.red(`Failed to clone GitHub repository: ${formatNetworkError(error)}`));
358
- process.exit(1);
359
- }
360
- } else if (parsed.type === "gitlab" && parsed.owner && parsed.repo) {
361
- try {
362
- const result = await cloneGitLabRepo(parsed.owner, parsed.repo, parsed.ref, parsed.path);
363
- localPath = result.localPath;
364
- cleanup = result.cleanup;
365
- } catch (error) {
366
- console.error(pc2.red(`Failed to clone GitLab repository: ${formatNetworkError(error)}`));
367
- process.exit(1);
368
- }
369
- } else if (parsed.type === "local") {
370
- localPath = parsed.value;
371
- const discovered = await discoverSkill(localPath);
372
- if (discovered) {
373
- skillName = discovered.name;
374
- }
375
- } else if (parsed.type === "package") {
376
- if (!isCatalogAvailable()) {
377
- console.error(pc2.red("@cleocode/ct-skills is not installed. Run: npm install @cleocode/ct-skills"));
378
- process.exit(1);
379
- }
380
- const catalogSkill = getSkill(parsed.inferredName);
381
- if (catalogSkill) {
382
- localPath = getSkillDir(catalogSkill.name);
383
- skillName = catalogSkill.name;
384
- sourceValue = `@cleocode/ct-skills:${catalogSkill.name}`;
385
- sourceType = "package";
386
- console.log(` Found in catalog: ${pc2.bold(catalogSkill.name)} v${catalogSkill.version} (${pc2.dim(catalogSkill.category)})`);
387
- } else {
388
- console.error(pc2.red(`Skill not found in catalog: ${parsed.inferredName}`));
389
- console.log(pc2.dim("Available skills: " + listSkills().join(", ")));
390
- process.exit(1);
391
- }
392
- } else {
393
- console.error(pc2.red(`Unsupported source type: ${parsed.type}`));
394
- process.exit(1);
444
+ };
445
+ }));
446
+ }
447
+
448
+ // src/commands/advanced/conflicts.ts
449
+ function registerAdvancedConflicts(parent) {
450
+ parent.command("conflicts").description("Preflight MCP conflict detection across providers").requiredOption("--mcp-file <path>", "JSON file containing McpBatchOperation[]").option("-a, --agent <name>", "Target specific provider(s)", (v, prev) => [...prev, v], []).option("--all", "Use all registry providers (not only detected)").option("--min-tier <tier>", "Minimum priority tier: high|medium|low", "low").option("--project-dir <path>", "Project directory to resolve project-scope paths").option("--details", "Include full conflict list").action(async (opts) => runLafsCommand("advanced.conflicts", !opts.details, async () => {
451
+ const baseProviders = resolveProviders({ all: opts.all, agent: opts.agent });
452
+ const minimumPriority = parsePriority(opts.minTier);
453
+ const providers = selectProvidersByMinimumPriority(baseProviders, minimumPriority);
454
+ const operations = await readMcpOperations(opts.mcpFile);
455
+ if (providers.length === 0) {
456
+ throw new LAFSCommandError(
457
+ "E_ADVANCED_NO_TARGET_PROVIDERS",
458
+ "No target providers resolved for conflict detection.",
459
+ "Use --all or pass provider IDs with --agent."
460
+ );
461
+ }
462
+ const conflicts = await detectMcpConfigConflicts(
463
+ providers,
464
+ operations,
465
+ opts.projectDir
466
+ );
467
+ const countByCode = conflicts.reduce((acc, conflict) => {
468
+ acc[conflict.code] = (acc[conflict.code] ?? 0) + 1;
469
+ return acc;
470
+ }, {});
471
+ return {
472
+ objective: "Detect MCP configuration conflicts before mutation",
473
+ constraints: {
474
+ minimumPriority,
475
+ providerCount: providers.length,
476
+ operationCount: operations.length
477
+ },
478
+ acceptanceCriteria: {
479
+ conflictCount: conflicts.length
480
+ },
481
+ data: opts.details ? conflicts : {
482
+ conflictCount: conflicts.length,
483
+ countByCode,
484
+ sample: conflicts.slice(0, 5)
395
485
  }
486
+ };
487
+ }));
488
+ }
489
+
490
+ // src/commands/advanced/apply.ts
491
+ var VALID_POLICIES = /* @__PURE__ */ new Set(["fail", "skip", "overwrite"]);
492
+ function parsePolicy(value) {
493
+ if (!VALID_POLICIES.has(value)) {
494
+ throw new LAFSCommandError(
495
+ "E_ADVANCED_VALIDATION_POLICY",
496
+ `Invalid policy: ${value}`,
497
+ "Use one of: fail, skip, overwrite."
498
+ );
499
+ }
500
+ return value;
501
+ }
502
+ function registerAdvancedApply(parent) {
503
+ parent.command("apply").description("Apply MCP operations with configurable conflict policy").requiredOption("--mcp-file <path>", "JSON file containing McpBatchOperation[]").option("--policy <policy>", "Conflict policy: fail|skip|overwrite", "fail").option("-a, --agent <name>", "Target specific provider(s)", (v, prev) => [...prev, v], []).option("--all", "Use all registry providers (not only detected)").option("--min-tier <tier>", "Minimum priority tier: high|medium|low", "low").option("--project-dir <path>", "Project directory to resolve project-scope paths").option("--details", "Include detailed apply result").action(async (opts) => runLafsCommand("advanced.apply", !opts.details, async () => {
504
+ const baseProviders = resolveProviders({ all: opts.all, agent: opts.agent });
505
+ const minimumPriority = parsePriority(opts.minTier);
506
+ const providers = selectProvidersByMinimumPriority(baseProviders, minimumPriority);
507
+ const operations = await readMcpOperations(opts.mcpFile);
508
+ const policy = parsePolicy(opts.policy);
509
+ if (providers.length === 0) {
510
+ throw new LAFSCommandError(
511
+ "E_ADVANCED_NO_TARGET_PROVIDERS",
512
+ "No target providers resolved for apply operation.",
513
+ "Use --all or pass provider IDs with --agent."
514
+ );
396
515
  }
397
- try {
398
- if (!localPath) {
399
- throw new Error("No local skill path resolved for installation");
400
- }
401
- const result = await installSkill(
402
- localPath,
403
- skillName,
404
- providers,
405
- opts.global ?? false
516
+ const result = await applyMcpInstallWithPolicy(
517
+ providers,
518
+ operations,
519
+ policy,
520
+ opts.projectDir
521
+ );
522
+ if (policy === "fail" && result.conflicts.length > 0) {
523
+ throw new LAFSCommandError(
524
+ "E_ADVANCED_CONFLICTS_BLOCKING",
525
+ "Conflicts detected and policy is set to fail.",
526
+ "Run `caamp advanced conflicts` to inspect, or rerun with --policy skip/overwrite.",
527
+ true,
528
+ result
406
529
  );
407
- if (result.success) {
408
- console.log(pc2.green(`
409
- \u2713 Installed ${pc2.bold(skillName)}`));
410
- console.log(` Canonical: ${pc2.dim(result.canonicalPath)}`);
411
- console.log(` Linked to: ${result.linkedAgents.join(", ")}`);
412
- const isGlobal = sourceType === "package" ? true : opts.global ?? false;
413
- await recordSkillInstall(
414
- skillName,
415
- sourceValue,
416
- sourceValue,
417
- sourceType,
418
- result.linkedAgents,
419
- result.canonicalPath,
420
- isGlobal
421
- );
422
- }
423
- if (result.errors.length > 0) {
424
- console.log(pc2.yellow("\nWarnings:"));
425
- for (const err of result.errors) {
426
- console.log(` ${pc2.yellow("!")} ${err}`);
427
- }
428
- }
429
- } finally {
430
- if (cleanup) await cleanup();
431
530
  }
432
- });
531
+ const failedWrites = result.applied.filter((entry) => !entry.success);
532
+ if (failedWrites.length > 0) {
533
+ throw new LAFSCommandError(
534
+ "E_ADVANCED_APPLY_WRITE_FAILED",
535
+ "One or more MCP writes failed.",
536
+ "Check result details, fix provider config issues, and retry.",
537
+ true,
538
+ result
539
+ );
540
+ }
541
+ return {
542
+ objective: "Apply MCP operations with policy-driven conflict handling",
543
+ constraints: {
544
+ policy,
545
+ minimumPriority,
546
+ providerCount: providers.length,
547
+ operationCount: operations.length
548
+ },
549
+ acceptanceCriteria: {
550
+ conflicts: result.conflicts.length,
551
+ writesSucceeded: result.applied.length
552
+ },
553
+ data: opts.details ? result : {
554
+ conflicts: result.conflicts.length,
555
+ applied: result.applied.length,
556
+ skipped: result.skipped.length
557
+ }
558
+ };
559
+ }));
433
560
  }
434
561
 
435
- // src/commands/skills/remove.ts
436
- import pc3 from "picocolors";
437
- function registerSkillsRemove(parent) {
438
- parent.command("remove").description("Remove installed skill(s)").argument("[name]", "Skill name to remove").option("-g, --global", "Remove from global scope").option("-y, --yes", "Skip confirmation").action(async (name, opts) => {
439
- const providers = getInstalledProviders();
440
- if (name) {
441
- const result = await removeSkill(name, providers, opts.global ?? false);
442
- if (result.removed.length > 0) {
443
- console.log(pc3.green(`\u2713 Removed ${pc3.bold(name)} from: ${result.removed.join(", ")}`));
444
- await removeSkillFromLock(name);
445
- } else {
446
- console.log(pc3.yellow(`Skill ${name} not found in any provider.`));
562
+ // src/commands/advanced/instructions.ts
563
+ function registerAdvancedInstructions(parent) {
564
+ parent.command("instructions").description("Single-operation instruction update across providers").option("-a, --agent <name>", "Target specific provider(s)", (v, prev) => [...prev, v], []).option("--all", "Use all registry providers (not only detected)").option("--min-tier <tier>", "Minimum priority tier: high|medium|low", "low").option("--scope <scope>", "Instruction scope: project|global", "project").option("--content <text>", "Inline content to inject").option("--content-file <path>", "File containing content to inject").option("--project-dir <path>", "Project directory to resolve project-scope paths").option("--details", "Include detailed per-file actions").action(async (opts) => runLafsCommand("advanced.instructions", !opts.details, async () => {
565
+ const minimumPriority = parsePriority(opts.minTier);
566
+ const baseProviders = resolveProviders({ all: opts.all, agent: opts.agent });
567
+ const providers = selectProvidersByMinimumPriority(baseProviders, minimumPriority);
568
+ const scope = opts.scope === "global" ? "global" : opts.scope === "project" ? "project" : null;
569
+ if (!scope) {
570
+ throw new LAFSCommandError(
571
+ "E_ADVANCED_VALIDATION_SCOPE",
572
+ `Invalid scope: ${opts.scope}`,
573
+ "Use --scope project or --scope global."
574
+ );
575
+ }
576
+ const content = await readTextInput(opts.content, opts.contentFile);
577
+ if (!content || content.trim().length === 0) {
578
+ throw new LAFSCommandError(
579
+ "E_ADVANCED_VALIDATION_CONTENT",
580
+ "Instruction content is required.",
581
+ "Provide --content or --content-file with non-empty text."
582
+ );
583
+ }
584
+ if (providers.length === 0) {
585
+ throw new LAFSCommandError(
586
+ "E_ADVANCED_NO_TARGET_PROVIDERS",
587
+ "No target providers resolved for instruction update.",
588
+ "Use --all or pass provider IDs with --agent."
589
+ );
590
+ }
591
+ const summary = await updateInstructionsSingleOperation(
592
+ providers,
593
+ content,
594
+ scope,
595
+ opts.projectDir
596
+ );
597
+ return {
598
+ objective: "Update instruction files across providers in one operation",
599
+ constraints: {
600
+ scope,
601
+ minimumPriority,
602
+ providerCount: providers.length
603
+ },
604
+ acceptanceCriteria: {
605
+ updatedFiles: summary.updatedFiles
606
+ },
607
+ data: opts.details ? summary : {
608
+ updatedFiles: summary.updatedFiles,
609
+ files: summary.actions.map((entry) => ({
610
+ file: entry.file,
611
+ action: entry.action
612
+ }))
447
613
  }
448
- if (result.errors.length > 0) {
449
- for (const err of result.errors) {
450
- console.log(pc3.red(` ${err}`));
614
+ };
615
+ }));
616
+ }
617
+
618
+ // src/commands/advanced/configure.ts
619
+ function registerAdvancedConfigure(parent) {
620
+ parent.command("configure").description("Configure global + project scope for one provider in one operation").requiredOption("-a, --agent <name>", "Target provider ID or alias").option("--global-mcp-file <path>", "JSON file for global MCP operations").option("--project-mcp-file <path>", "JSON file for project MCP operations").option("--instruction <text>", "Instruction content for both scopes").option("--instruction-file <path>", "Instruction content file for both scopes").option("--instruction-global <text>", "Instruction content for global scope").option("--instruction-global-file <path>", "Instruction content file for global scope").option("--instruction-project <text>", "Instruction content for project scope").option("--instruction-project-file <path>", "Instruction content file for project scope").option("--project-dir <path>", "Project directory to resolve project-scope paths").option("--details", "Include detailed write results").action(async (opts) => runLafsCommand("advanced.configure", !opts.details, async () => {
621
+ const provider = getProvider(opts.agent);
622
+ if (!provider) {
623
+ throw new LAFSCommandError(
624
+ "E_ADVANCED_PROVIDER_NOT_FOUND",
625
+ `Unknown provider: ${opts.agent}`,
626
+ "Check `caamp providers list` for valid provider IDs/aliases."
627
+ );
628
+ }
629
+ const globalMcp = opts.globalMcpFile ? await readMcpOperations(opts.globalMcpFile) : [];
630
+ const projectMcp = opts.projectMcpFile ? await readMcpOperations(opts.projectMcpFile) : [];
631
+ const sharedInstruction = await readTextInput(opts.instruction, opts.instructionFile);
632
+ const globalInstruction = await readTextInput(
633
+ opts.instructionGlobal,
634
+ opts.instructionGlobalFile
635
+ );
636
+ const projectInstruction = await readTextInput(
637
+ opts.instructionProject,
638
+ opts.instructionProjectFile
639
+ );
640
+ let instructionContent;
641
+ if (globalInstruction || projectInstruction) {
642
+ instructionContent = {
643
+ ...globalInstruction ? { global: globalInstruction } : {},
644
+ ...projectInstruction ? { project: projectInstruction } : {}
645
+ };
646
+ } else if (sharedInstruction) {
647
+ instructionContent = sharedInstruction;
648
+ }
649
+ if (globalMcp.length === 0 && projectMcp.length === 0 && !instructionContent) {
650
+ throw new LAFSCommandError(
651
+ "E_ADVANCED_VALIDATION_NO_OPS",
652
+ "No configuration operations were provided.",
653
+ "Provide MCP files and/or instruction content."
654
+ );
655
+ }
656
+ const result = await configureProviderGlobalAndProject(provider, {
657
+ globalMcp: globalMcp.map((entry) => ({
658
+ serverName: entry.serverName,
659
+ config: entry.config
660
+ })),
661
+ projectMcp: projectMcp.map((entry) => ({
662
+ serverName: entry.serverName,
663
+ config: entry.config
664
+ })),
665
+ instructionContent,
666
+ projectDir: opts.projectDir
667
+ });
668
+ const globalFailures = result.mcp.global.filter((entry) => !entry.success);
669
+ const projectFailures = result.mcp.project.filter((entry) => !entry.success);
670
+ if (globalFailures.length > 0 || projectFailures.length > 0) {
671
+ throw new LAFSCommandError(
672
+ "E_ADVANCED_CONFIGURE_FAILED",
673
+ "One or more MCP writes failed during configure operation.",
674
+ "Inspect the failed write entries and provider config paths, then retry.",
675
+ true,
676
+ result
677
+ );
678
+ }
679
+ return {
680
+ objective: "Configure global and project settings in one operation",
681
+ constraints: {
682
+ provider: provider.id,
683
+ globalMcpOps: globalMcp.length,
684
+ projectMcpOps: projectMcp.length,
685
+ instructionMode: instructionContent ? typeof instructionContent === "string" ? "shared" : "scoped" : "none"
686
+ },
687
+ acceptanceCriteria: {
688
+ globalWrites: result.mcp.global.length,
689
+ projectWrites: result.mcp.project.length
690
+ },
691
+ data: opts.details ? result : {
692
+ providerId: result.providerId,
693
+ configPaths: result.configPaths,
694
+ globalWrites: result.mcp.global.length,
695
+ projectWrites: result.mcp.project.length,
696
+ instructionUpdates: {
697
+ global: result.instructions.global?.size ?? 0,
698
+ project: result.instructions.project?.size ?? 0
451
699
  }
452
700
  }
453
- } else {
454
- const skills = await listCanonicalSkills();
455
- if (skills.length === 0) {
456
- console.log(pc3.dim("No skills installed."));
457
- return;
458
- }
459
- console.log(pc3.bold("Installed skills:"));
460
- for (const s of skills) {
461
- console.log(` ${s}`);
462
- }
463
- console.log(pc3.dim("\nUse: caamp skills remove <name>"));
464
- }
465
- });
701
+ };
702
+ }));
466
703
  }
467
704
 
468
- // src/commands/skills/list.ts
469
- import pc4 from "picocolors";
470
- function registerSkillsList(parent) {
471
- parent.command("list").description("List installed skills").option("-g, --global", "List global skills").option("-a, --agent <name>", "List skills for specific agent").option("--json", "Output as JSON").action(async (opts) => {
472
- let dirs = [];
473
- if (opts.agent) {
474
- const provider = getProvider(opts.agent);
475
- if (!provider) {
476
- console.error(pc4.red(`Provider not found: ${opts.agent}`));
477
- process.exit(1);
478
- }
479
- dirs = opts.global ? [resolveProviderSkillsDir(provider, "global")] : [resolveProviderSkillsDir(provider, "project")];
480
- } else if (opts.global) {
481
- const providers = getInstalledProviders();
482
- dirs = providers.map((p) => resolveProviderSkillsDir(p, "global")).filter(Boolean);
483
- } else {
484
- const providers = getInstalledProviders();
485
- dirs = providers.map((p) => resolveProviderSkillsDir(p, "project")).filter(Boolean);
486
- }
487
- const skills = await discoverSkillsMulti(dirs);
488
- if (opts.json) {
489
- console.log(JSON.stringify(skills, null, 2));
490
- return;
491
- }
492
- if (skills.length === 0) {
493
- console.log(pc4.dim("No skills found."));
494
- return;
495
- }
496
- console.log(pc4.bold(`
497
- ${skills.length} skill(s) found:
498
- `));
499
- for (const skill of skills) {
500
- console.log(` ${pc4.bold(skill.name.padEnd(30))} ${pc4.dim(skill.metadata.description ?? "")}`);
501
- }
502
- console.log();
503
- });
705
+ // src/commands/advanced/index.ts
706
+ function registerAdvancedCommands(program2) {
707
+ const advanced = program2.command("advanced").description("LAFS-compliant wrappers for advanced orchestration APIs");
708
+ registerAdvancedProviders(advanced);
709
+ registerAdvancedBatch(advanced);
710
+ registerAdvancedConflicts(advanced);
711
+ registerAdvancedApply(advanced);
712
+ registerAdvancedInstructions(advanced);
713
+ registerAdvancedConfigure(advanced);
504
714
  }
505
715
 
506
- // src/commands/skills/find.ts
507
- import { randomUUID } from "crypto";
508
- import {
509
- resolveOutputFormat
510
- } from "@cleocode/lafs-protocol";
511
- import pc5 from "picocolors";
512
- var SkillsFindValidationError = class extends Error {
513
- code;
514
- constructor(code, message) {
515
- super(message);
516
- this.code = code;
517
- this.name = "SkillsFindValidationError";
518
- }
519
- };
520
- function registerSkillsFind(parent) {
521
- parent.command("find").description("Search marketplace for skills").argument("[query]", "Search query").option("--recommend", "Recommend skills from constraints").option("--top <n>", "Number of recommendation candidates", "3").option("--must-have <term>", "Required criteria term", (value, previous) => [...previous, value], []).option("--prefer <term>", "Preferred criteria term", (value, previous) => [...previous, value], []).option("--exclude <term>", "Excluded criteria term", (value, previous) => [...previous, value], []).option("--details", "Include expanded machine output").option("--human", "Force human-readable output").option("--json", "Output as JSON").option("--select <indexes>", "Pre-select recommendation ranks (comma-separated)").option("-l, --limit <n>", "Max results", "20").action(async (query, opts) => {
522
- const operation = opts.recommend ? "skills.find.recommend" : "skills.find.search";
523
- const details = Boolean(opts.details);
524
- const mvi = !details;
525
- let format;
526
- try {
527
- format = resolveOutputFormat({
528
- jsonFlag: opts.json ?? false,
529
- humanFlag: opts.human ?? false,
530
- projectDefault: opts.recommend ? "json" : "human"
531
- }).format;
532
- } catch (error) {
533
- const message = error instanceof Error ? error.message : String(error);
534
- if (opts.json) {
535
- emitJsonError(operation, mvi, "E_FORMAT_CONFLICT", message, "VALIDATION");
536
- } else {
537
- console.error(pc5.red(message));
538
- }
716
+ // src/commands/config.ts
717
+ import pc from "picocolors";
718
+ import { existsSync } from "fs";
719
+ function registerConfigCommand(program2) {
720
+ const config = program2.command("config").description("View provider configuration");
721
+ config.command("show").description("Show provider configuration").argument("<provider>", "Provider ID or alias").option("-g, --global", "Show global config").option("--json", "Output as JSON").action(async (providerId, opts) => {
722
+ const provider = getProvider(providerId);
723
+ if (!provider) {
724
+ console.error(pc.red(`Provider not found: ${providerId}`));
539
725
  process.exit(1);
540
726
  }
541
- if (opts.recommend) {
542
- try {
543
- const top = parseTop(opts.top);
544
- const mustHave = parseConstraintList(opts.mustHave);
545
- const prefer = parseConstraintList(opts.prefer);
546
- const exclude = parseConstraintList(opts.exclude);
547
- validateCriteriaConflicts(mustHave, prefer, exclude);
548
- const selectedRanks = parseSelectList(opts.select);
549
- const seedQuery = buildSeedQuery(query, mustHave, prefer, exclude);
550
- const recommendation = await recommendSkills(
551
- seedQuery,
552
- {
553
- mustHave,
554
- prefer,
555
- exclude
556
- },
557
- {
558
- top,
559
- includeDetails: details
560
- }
561
- );
562
- const options = normalizeRecommendationOptions(recommendation.ranking, details);
563
- validateSelectedRanks(selectedRanks, options.length);
564
- const selected = selectedRanks.length > 0 ? options.filter((option) => selectedRanks.includes(option.rank)) : [];
565
- if (format === "json") {
566
- const result = formatSkillRecommendations(recommendation, { mode: "json", details });
567
- const resultOptions = Array.isArray(result.options) ? result.options : [];
568
- const selectedObjects = resultOptions.filter(
569
- (option) => selectedRanks.includes(Number(option.rank ?? 0))
570
- );
571
- const envelope = buildEnvelope(
572
- operation,
573
- mvi,
574
- {
575
- ...result,
576
- selected: selectedObjects
577
- },
578
- null
579
- );
580
- console.log(JSON.stringify(envelope, null, 2));
581
- return;
582
- }
583
- const human = formatSkillRecommendations(recommendation, { mode: "human", details });
584
- console.log(human);
585
- if (selected.length > 0) {
586
- console.log(`Selected: ${selected.map((option) => option.scopedName).join(", ")}`);
587
- }
588
- return;
589
- } catch (error) {
590
- const message = error instanceof Error ? error.message : String(error);
591
- const errorCode = error instanceof SkillsFindValidationError ? error.code : error.code ?? RECOMMENDATION_ERROR_CODES.SOURCE_UNAVAILABLE;
592
- const category = errorCode === RECOMMENDATION_ERROR_CODES.CRITERIA_CONFLICT ? "CONFLICT" : errorCode === RECOMMENDATION_ERROR_CODES.NO_MATCHES ? "NOT_FOUND" : errorCode === RECOMMENDATION_ERROR_CODES.QUERY_INVALID ? "VALIDATION" : "INTERNAL";
593
- if (format === "json") {
594
- emitJsonError(operation, mvi, errorCode, message, category, {
595
- query: query ?? null
596
- });
597
- } else {
598
- console.error(pc5.red(`Recommendation failed: ${message}`));
599
- }
600
- process.exit(1);
601
- }
602
- }
603
- if (!query) {
604
- console.log(pc5.dim("Usage: caamp skills find <query>"));
727
+ const configPath = resolveProviderConfigPath(
728
+ provider,
729
+ opts.global ? "global" : "project"
730
+ ) ?? provider.configPathGlobal;
731
+ if (!existsSync(configPath)) {
732
+ console.log(pc.dim(`No config file at: ${configPath}`));
605
733
  return;
606
734
  }
607
- const limit = parseInt(opts.limit, 10);
608
- const client = new MarketplaceClient();
609
- if (format === "human") {
610
- console.log(pc5.dim(`Searching marketplaces for "${query}"...
611
- `));
612
- }
613
- let results;
614
735
  try {
615
- results = await client.search(query, limit);
616
- } catch (error) {
617
- const message = formatNetworkError(error);
618
- if (format === "json") {
619
- console.log(JSON.stringify({ error: message }));
736
+ const data = await readConfig(configPath, provider.configFormat);
737
+ if (opts.json) {
738
+ console.log(JSON.stringify(data, null, 2));
620
739
  } else {
621
- console.error(pc5.red(`Marketplace search failed: ${message}`));
740
+ console.log(pc.bold(`
741
+ ${provider.toolName} config (${configPath}):
742
+ `));
743
+ console.log(JSON.stringify(data, null, 2));
622
744
  }
745
+ } catch (err) {
746
+ console.error(pc.red(`Error reading config: ${err instanceof Error ? err.message : String(err)}`));
623
747
  process.exit(1);
624
748
  }
625
- if (format === "json") {
626
- console.log(JSON.stringify(results, null, 2));
627
- return;
628
- }
629
- if (results.length === 0) {
630
- console.log(pc5.yellow("No results found."));
631
- return;
749
+ });
750
+ config.command("path").description("Show config file path").argument("<provider>", "Provider ID or alias").argument("[scope]", "Scope: project (default) or global", "project").action((providerId, scope) => {
751
+ const provider = getProvider(providerId);
752
+ if (!provider) {
753
+ console.error(pc.red(`Provider not found: ${providerId}`));
754
+ process.exit(1);
632
755
  }
633
- for (const skill of results) {
634
- const stars = skill.stars > 0 ? pc5.yellow(`\u2605 ${formatStars(skill.stars)}`) : "";
635
- console.log(` ${pc5.bold(skill.scopedName.padEnd(35))} ${stars}`);
636
- console.log(` ${pc5.dim(skill.description?.slice(0, 80) ?? "")}`);
637
- console.log(` ${pc5.dim(`from ${skill.source}`)}`);
638
- console.log();
756
+ if (scope === "global") {
757
+ console.log(provider.configPathGlobal);
758
+ } else {
759
+ const projectPath = resolveProviderConfigPath(provider, "project");
760
+ if (projectPath) {
761
+ console.log(projectPath);
762
+ } else {
763
+ console.log(pc.dim(`${provider.toolName} has no project-level config`));
764
+ console.log(provider.configPathGlobal);
765
+ }
639
766
  }
640
- console.log(pc5.dim("Install with: caamp skills install <scopedName>"));
641
767
  });
642
768
  }
643
- function formatStars(n) {
644
- if (n >= 1e3) return `${(n / 1e3).toFixed(1)}k`;
645
- return String(n);
769
+
770
+ // src/commands/doctor.ts
771
+ import pc2 from "picocolors";
772
+ import { execFileSync } from "child_process";
773
+ import { existsSync as existsSync2, readdirSync, lstatSync, readlinkSync } from "fs";
774
+ import { homedir } from "os";
775
+ import { join as join2 } from "path";
776
+
777
+ // src/core/version.ts
778
+ import { readFileSync } from "fs";
779
+ import { dirname, join } from "path";
780
+ import { fileURLToPath } from "url";
781
+ var cachedVersion = null;
782
+ function getCaampVersion() {
783
+ if (cachedVersion) return cachedVersion;
784
+ try {
785
+ const currentDir = dirname(fileURLToPath(import.meta.url));
786
+ const packageJsonPath = join(currentDir, "..", "package.json");
787
+ const packageJson = JSON.parse(readFileSync(packageJsonPath, "utf-8"));
788
+ cachedVersion = packageJson.version ?? "0.0.0";
789
+ } catch {
790
+ cachedVersion = "0.0.0";
791
+ }
792
+ return cachedVersion;
646
793
  }
647
- function parseConstraintList(values) {
648
- const normalized = values.flatMap((value) => tokenizeCriteriaValue(value));
649
- return Array.from(new Set(normalized));
794
+
795
+ // src/commands/doctor.ts
796
+ function getNodeVersion() {
797
+ return process.version;
650
798
  }
651
- function parseTop(value) {
652
- const parsed = Number.parseInt(value, 10);
653
- if (!Number.isInteger(parsed) || parsed < 1 || parsed > 20) {
654
- throw new SkillsFindValidationError(RECOMMENDATION_ERROR_CODES.QUERY_INVALID, "--top must be an integer between 1 and 20");
799
+ function getNpmVersion() {
800
+ try {
801
+ return execFileSync("npm", ["--version"], { stdio: "pipe", encoding: "utf-8" }).trim();
802
+ } catch {
803
+ return null;
804
+ }
805
+ }
806
+ function checkEnvironment() {
807
+ const checks = [];
808
+ checks.push({ label: `Node.js ${getNodeVersion()}`, status: "pass" });
809
+ const npmVersion = getNpmVersion();
810
+ if (npmVersion) {
811
+ checks.push({ label: `npm ${npmVersion}`, status: "pass" });
812
+ } else {
813
+ checks.push({ label: "npm not found", status: "warn" });
814
+ }
815
+ checks.push({ label: `CAAMP v${getCaampVersion()}`, status: "pass" });
816
+ checks.push({ label: `${process.platform} ${process.arch}`, status: "pass" });
817
+ return { name: "Environment", checks };
818
+ }
819
+ function checkRegistry() {
820
+ const checks = [];
821
+ try {
822
+ const providers = getAllProviders();
823
+ const count = getProviderCount();
824
+ checks.push({ label: `${count} providers loaded`, status: "pass" });
825
+ const malformed = [];
826
+ for (const p of providers) {
827
+ if (!p.id || !p.toolName || !p.configKey || !p.configFormat) {
828
+ malformed.push(p.id || "(unknown)");
829
+ }
830
+ }
831
+ if (malformed.length === 0) {
832
+ checks.push({ label: "All entries valid", status: "pass" });
833
+ } else {
834
+ checks.push({
835
+ label: `${malformed.length} malformed entries`,
836
+ status: "fail",
837
+ detail: malformed.join(", ")
838
+ });
839
+ }
840
+ } catch (err) {
841
+ checks.push({
842
+ label: "Failed to load registry",
843
+ status: "fail",
844
+ detail: err instanceof Error ? err.message : String(err)
845
+ });
655
846
  }
656
- return parsed;
847
+ return { name: "Registry", checks };
657
848
  }
658
- function parseSelectList(value) {
659
- if (!value) return [];
660
- const parsed = value.split(",").map((entry) => Number.parseInt(entry.trim(), 10)).filter((entry) => Number.isInteger(entry) && entry > 0);
661
- return Array.from(new Set(parsed));
849
+ function checkInstalledProviders() {
850
+ const checks = [];
851
+ try {
852
+ const results = detectAllProviders();
853
+ const installed = results.filter((r) => r.installed);
854
+ checks.push({ label: `${installed.length} found`, status: "pass" });
855
+ for (const r of installed) {
856
+ const methods = r.methods.join(", ");
857
+ checks.push({ label: `${r.provider.toolName} (${methods})`, status: "pass" });
858
+ }
859
+ } catch (err) {
860
+ checks.push({
861
+ label: "Detection failed",
862
+ status: "fail",
863
+ detail: err instanceof Error ? err.message : String(err)
864
+ });
865
+ }
866
+ return { name: "Installed Providers", checks };
662
867
  }
663
- function buildSeedQuery(query, mustHave, prefer, exclude) {
664
- if (query && query.trim().length > 0) {
665
- return query;
868
+ function checkSkillSymlinks() {
869
+ const checks = [];
870
+ const canonicalDir = CANONICAL_SKILLS_DIR;
871
+ if (!existsSync2(canonicalDir)) {
872
+ checks.push({ label: "0 canonical skills", status: "pass" });
873
+ checks.push({ label: "No broken symlinks", status: "pass" });
874
+ return { name: "Skills", checks };
666
875
  }
667
- const seedTerms = [...mustHave, ...prefer, ...exclude].filter((term) => term.length > 0);
668
- if (seedTerms.length > 0) {
669
- return seedTerms.join(" ");
876
+ let canonicalCount = 0;
877
+ let canonicalNames = [];
878
+ try {
879
+ canonicalNames = readdirSync(canonicalDir).filter((name) => {
880
+ const full = join2(canonicalDir, name);
881
+ try {
882
+ const stat = lstatSync(full);
883
+ return stat.isDirectory() || stat.isSymbolicLink();
884
+ } catch {
885
+ return false;
886
+ }
887
+ });
888
+ canonicalCount = canonicalNames.length;
889
+ checks.push({ label: `${canonicalCount} canonical skills`, status: "pass" });
890
+ } catch {
891
+ checks.push({ label: "Cannot read skills directory", status: "warn" });
892
+ return { name: "Skills", checks };
670
893
  }
671
- throw new SkillsFindValidationError(
672
- RECOMMENDATION_ERROR_CODES.QUERY_INVALID,
673
- "Recommendation mode requires a query or at least one criteria flag."
674
- );
675
- }
676
- function normalizeRecommendationOptions(ranking, details) {
677
- return ranking.map((entry, index) => {
678
- const whyCodes = entry.reasons.map((reason) => reason.code);
679
- return {
680
- rank: index + 1,
681
- scopedName: entry.skill.scopedName,
682
- description: entry.skill.description,
683
- score: entry.score,
684
- why: whyCodes.length > 0 ? whyCodes.join(", ") : "score-based match",
685
- source: entry.skill.source,
686
- ...details ? {
687
- evidence: {
688
- reasons: entry.reasons,
689
- breakdown: entry.breakdown
894
+ const broken = [];
895
+ const stale = [];
896
+ const results = detectAllProviders();
897
+ const installed = results.filter((r) => r.installed);
898
+ for (const r of installed) {
899
+ const provider = r.provider;
900
+ const skillDir = provider.pathSkills;
901
+ if (!existsSync2(skillDir)) continue;
902
+ try {
903
+ const entries = readdirSync(skillDir);
904
+ for (const entry of entries) {
905
+ const fullPath = join2(skillDir, entry);
906
+ try {
907
+ const stat = lstatSync(fullPath);
908
+ if (!stat.isSymbolicLink()) continue;
909
+ if (!existsSync2(fullPath)) {
910
+ broken.push(`${provider.id}/${entry}`);
911
+ } else {
912
+ const target = readlinkSync(fullPath);
913
+ const isCanonical = target.includes("/.agents/skills/") || target.includes("\\.agents\\skills\\");
914
+ if (!isCanonical) {
915
+ stale.push(`${provider.id}/${entry}`);
916
+ }
917
+ }
918
+ } catch {
690
919
  }
691
- } : {}
692
- };
693
- });
694
- }
695
- function validateCriteriaConflicts(mustHave, prefer, exclude) {
696
- const overlap = mustHave.filter((term) => exclude.includes(term));
697
- if (overlap.length > 0) {
698
- throw new SkillsFindValidationError(
699
- RECOMMENDATION_ERROR_CODES.CRITERIA_CONFLICT,
700
- "A criteria term cannot be both required and excluded."
701
- );
920
+ }
921
+ } catch {
922
+ }
702
923
  }
703
- const preferOverlap = prefer.filter((term) => exclude.includes(term));
704
- if (preferOverlap.length > 0) {
705
- throw new SkillsFindValidationError(
706
- RECOMMENDATION_ERROR_CODES.CRITERIA_CONFLICT,
707
- "A criteria term cannot be both preferred and excluded."
708
- );
924
+ if (broken.length === 0) {
925
+ checks.push({ label: "No broken symlinks", status: "pass" });
926
+ } else {
927
+ checks.push({
928
+ label: `${broken.length} broken symlink${broken.length !== 1 ? "s" : ""}`,
929
+ status: "warn",
930
+ detail: broken.join(", ")
931
+ });
709
932
  }
710
- }
711
- function validateSelectedRanks(selectedRanks, total) {
712
- for (const rank of selectedRanks) {
713
- if (rank < 1 || rank > total) {
714
- throw new SkillsFindValidationError(
715
- RECOMMENDATION_ERROR_CODES.QUERY_INVALID,
716
- `--select rank ${rank} is out of range (1-${total}).`
717
- );
718
- }
933
+ if (stale.length === 0) {
934
+ checks.push({ label: "No stale symlinks", status: "pass" });
935
+ } else {
936
+ checks.push({
937
+ label: `${stale.length} stale symlink${stale.length !== 1 ? "s" : ""} (not pointing to ~/.agents/skills/)`,
938
+ status: "warn",
939
+ detail: stale.join(", ")
940
+ });
719
941
  }
942
+ return { name: "Skills", checks };
720
943
  }
721
- function buildEnvelope(operation, mvi, result, error) {
722
- return {
723
- $schema: "https://lafs.dev/schemas/v1/envelope.schema.json",
724
- _meta: {
725
- specVersion: "1.0.0",
726
- schemaVersion: "1.0.0",
727
- timestamp: (/* @__PURE__ */ new Date()).toISOString(),
728
- operation,
729
- requestId: randomUUID(),
730
- transport: "cli",
731
- strict: true,
732
- mvi,
733
- contextVersion: 0
734
- },
735
- success: error === null,
736
- result,
737
- error,
738
- page: null
739
- };
740
- }
741
- function emitJsonError(operation, mvi, code, message, category, details = {}) {
742
- const envelope = buildEnvelope(operation, mvi, null, {
743
- code,
744
- message,
745
- category,
746
- retryable: false,
747
- retryAfterMs: null,
748
- details
749
- });
750
- console.error(JSON.stringify(envelope, null, 2));
751
- }
752
-
753
- // src/commands/skills/check.ts
754
- import pc6 from "picocolors";
755
- function registerSkillsCheck(parent) {
756
- parent.command("check").description("Check for available skill updates").option("--json", "Output as JSON").action(async (opts) => {
757
- const tracked = await getTrackedSkills();
758
- const entries = Object.entries(tracked);
759
- if (entries.length === 0) {
760
- console.log(pc6.dim("No tracked skills."));
761
- return;
762
- }
763
- console.log(pc6.dim(`Checking ${entries.length} skill(s) for updates...
764
- `));
765
- const results = [];
766
- for (const [name, entry] of entries) {
767
- const update = await checkSkillUpdate(name);
768
- results.push({ name, entry, ...update });
944
+ async function checkLockFile() {
945
+ const checks = [];
946
+ try {
947
+ const lock = await readLockFile();
948
+ checks.push({ label: "Lock file valid", status: "pass" });
949
+ const lockSkillNames = Object.keys(lock.skills);
950
+ checks.push({ label: `${lockSkillNames.length} skill entries`, status: "pass" });
951
+ const orphaned = [];
952
+ for (const [name, entry] of Object.entries(lock.skills)) {
953
+ if (entry.canonicalPath && !existsSync2(entry.canonicalPath)) {
954
+ orphaned.push(name);
955
+ }
769
956
  }
770
- if (opts.json) {
771
- console.log(JSON.stringify(results, null, 2));
772
- return;
957
+ if (orphaned.length === 0) {
958
+ checks.push({ label: "0 orphaned entries", status: "pass" });
959
+ } else {
960
+ checks.push({
961
+ label: `${orphaned.length} orphaned skill${orphaned.length !== 1 ? "s" : ""} (in lock, missing from disk)`,
962
+ status: "warn",
963
+ detail: orphaned.join(", ")
964
+ });
773
965
  }
774
- let updatesAvailable = 0;
775
- for (const r of results) {
776
- let statusLabel;
777
- if (r.status === "update-available") {
778
- statusLabel = pc6.yellow("update available");
779
- updatesAvailable++;
780
- } else if (r.status === "up-to-date") {
781
- statusLabel = pc6.green("up to date");
966
+ const canonicalDir = CANONICAL_SKILLS_DIR;
967
+ if (existsSync2(canonicalDir)) {
968
+ const onDisk = readdirSync(canonicalDir).filter((name) => {
969
+ try {
970
+ const stat = lstatSync(join2(canonicalDir, name));
971
+ return stat.isDirectory() || stat.isSymbolicLink();
972
+ } catch {
973
+ return false;
974
+ }
975
+ });
976
+ const untracked = onDisk.filter((name) => !lock.skills[name]);
977
+ if (untracked.length === 0) {
978
+ checks.push({ label: "0 untracked skills", status: "pass" });
782
979
  } else {
783
- statusLabel = pc6.dim("unknown");
980
+ checks.push({
981
+ label: `${untracked.length} untracked skill${untracked.length !== 1 ? "s" : ""} (on disk, not in lock)`,
982
+ status: "warn",
983
+ detail: untracked.join(", ")
984
+ });
784
985
  }
785
- console.log(` ${pc6.bold(r.name.padEnd(30))} ${statusLabel}`);
786
- if (r.currentVersion || r.latestVersion) {
787
- const current = r.currentVersion ? r.currentVersion.slice(0, 12) : "?";
788
- const latest = r.latestVersion ?? "?";
789
- if (r.hasUpdate) {
790
- console.log(` ${pc6.dim("current:")} ${current} ${pc6.dim("->")} ${pc6.cyan(latest)}`);
791
- } else {
792
- console.log(` ${pc6.dim("version:")} ${current}`);
986
+ }
987
+ const results = detectAllProviders();
988
+ const installed = results.filter((r) => r.installed);
989
+ const mismatches = [];
990
+ for (const [name, entry] of Object.entries(lock.skills)) {
991
+ if (!entry.agents || entry.agents.length === 0) continue;
992
+ for (const agentId of entry.agents) {
993
+ const provider = installed.find((r) => r.provider.id === agentId);
994
+ if (!provider) continue;
995
+ const linkPath = join2(provider.provider.pathSkills, name);
996
+ if (!existsSync2(linkPath)) {
997
+ mismatches.push(`${name} missing from ${agentId}`);
793
998
  }
794
999
  }
795
- console.log(` ${pc6.dim(`source: ${r.entry.source}`)}`);
796
- console.log(` ${pc6.dim(`agents: ${r.entry.agents.join(", ")}`)}`);
797
- console.log();
798
1000
  }
799
- if (updatesAvailable > 0) {
800
- console.log(pc6.yellow(`${updatesAvailable} update(s) available.`));
801
- console.log(pc6.dim("Run `caamp skills update` to update all."));
1001
+ if (mismatches.length === 0) {
1002
+ checks.push({ label: "Lock agent-lists match symlinks", status: "pass" });
802
1003
  } else {
803
- console.log(pc6.green("All skills are up to date."));
1004
+ checks.push({
1005
+ label: `${mismatches.length} agent-list mismatch${mismatches.length !== 1 ? "es" : ""}`,
1006
+ status: "warn",
1007
+ detail: mismatches.slice(0, 5).join(", ") + (mismatches.length > 5 ? ` (+${mismatches.length - 5} more)` : "")
1008
+ });
804
1009
  }
805
- });
1010
+ } catch (err) {
1011
+ checks.push({
1012
+ label: "Failed to read lock file",
1013
+ status: "fail",
1014
+ detail: err instanceof Error ? err.message : String(err)
1015
+ });
1016
+ }
1017
+ return { name: "Lock File", checks };
806
1018
  }
807
-
808
- // src/commands/skills/update.ts
809
- import pc7 from "picocolors";
810
- function registerSkillsUpdate(parent) {
811
- parent.command("update").description("Update all outdated skills").option("-y, --yes", "Skip confirmation").action(async (opts) => {
812
- const tracked = await getTrackedSkills();
813
- const entries = Object.entries(tracked);
814
- if (entries.length === 0) {
815
- console.log(pc7.dim("No tracked skills to update."));
816
- return;
817
- }
818
- console.log(pc7.dim(`Checking ${entries.length} skill(s) for updates...`));
819
- const outdated = [];
820
- for (const [name] of entries) {
821
- const result = await checkSkillUpdate(name);
822
- if (result.hasUpdate) {
823
- outdated.push({
824
- name,
825
- currentVersion: result.currentVersion,
826
- latestVersion: result.latestVersion
827
- });
828
- }
829
- }
830
- if (outdated.length === 0) {
831
- console.log(pc7.green("\nAll skills are up to date."));
832
- return;
833
- }
834
- console.log(pc7.yellow(`
835
- ${outdated.length} skill(s) have updates available:
836
- `));
837
- for (const skill of outdated) {
838
- const current = skill.currentVersion?.slice(0, 12) ?? "?";
839
- const latest = skill.latestVersion ?? "?";
840
- console.log(` ${pc7.bold(skill.name)} ${pc7.dim(current)} ${pc7.dim("->")} ${pc7.cyan(latest)}`);
1019
+ async function checkConfigFiles() {
1020
+ const checks = [];
1021
+ const results = detectAllProviders();
1022
+ const installed = results.filter((r) => r.installed);
1023
+ for (const r of installed) {
1024
+ const provider = r.provider;
1025
+ const configPath = provider.configPathGlobal;
1026
+ if (!existsSync2(configPath)) {
1027
+ checks.push({
1028
+ label: `${provider.id}: no config file found`,
1029
+ status: "warn",
1030
+ detail: configPath
1031
+ });
1032
+ continue;
841
1033
  }
842
- if (!opts.yes) {
843
- const readline = await import("readline");
844
- const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
845
- const answer = await new Promise((resolve) => {
846
- rl.question(pc7.dim("\nProceed with update? [y/N] "), resolve);
1034
+ try {
1035
+ await readConfig(configPath, provider.configFormat);
1036
+ const relPath = configPath.replace(homedir(), "~");
1037
+ checks.push({
1038
+ label: `${provider.id}: ${relPath} readable`,
1039
+ status: "pass"
1040
+ });
1041
+ } catch (err) {
1042
+ checks.push({
1043
+ label: `${provider.id}: config parse error`,
1044
+ status: "fail",
1045
+ detail: err instanceof Error ? err.message : String(err)
847
1046
  });
848
- rl.close();
849
- if (answer.toLowerCase() !== "y" && answer.toLowerCase() !== "yes") {
850
- console.log(pc7.dim("Update cancelled."));
851
- return;
852
- }
853
1047
  }
854
- console.log();
855
- let successCount = 0;
856
- let failCount = 0;
857
- for (const skill of outdated) {
858
- const entry = tracked[skill.name];
859
- if (!entry) continue;
860
- console.log(pc7.dim(`Updating ${pc7.bold(skill.name)}...`));
861
- try {
862
- const parsed = parseSource(entry.source);
863
- let localPath;
864
- let cleanup;
865
- if (parsed.type === "github" && parsed.owner && parsed.repo) {
866
- const result = await cloneRepo(parsed.owner, parsed.repo, parsed.ref, parsed.path);
867
- localPath = result.localPath;
868
- cleanup = result.cleanup;
869
- } else if (parsed.type === "gitlab" && parsed.owner && parsed.repo) {
870
- const result = await cloneGitLabRepo(parsed.owner, parsed.repo, parsed.ref, parsed.path);
871
- localPath = result.localPath;
872
- cleanup = result.cleanup;
873
- } else {
874
- console.log(pc7.yellow(` Skipped ${skill.name}: source type "${parsed.type}" does not support auto-update`));
875
- continue;
876
- }
877
- try {
878
- const providers = entry.agents.map((a) => getProvider(a)).filter((p) => p !== void 0);
879
- if (providers.length === 0) {
880
- console.log(pc7.yellow(` Skipped ${skill.name}: no valid providers found`));
881
- continue;
882
- }
883
- const installResult = await installSkill(
884
- localPath,
885
- skill.name,
886
- providers,
887
- entry.isGlobal,
888
- entry.projectDir
889
- );
890
- if (installResult.success) {
891
- await recordSkillInstall(
892
- skill.name,
893
- entry.scopedName,
894
- entry.source,
895
- entry.sourceType,
896
- installResult.linkedAgents,
897
- installResult.canonicalPath,
898
- entry.isGlobal,
899
- entry.projectDir,
900
- skill.latestVersion
901
- );
902
- console.log(pc7.green(` Updated ${pc7.bold(skill.name)}`));
903
- successCount++;
904
- } else {
905
- console.log(pc7.red(` Failed to update ${skill.name}: no agents linked`));
906
- failCount++;
907
- }
908
- if (installResult.errors.length > 0) {
909
- for (const err of installResult.errors) {
910
- console.log(pc7.yellow(` ${err}`));
911
- }
912
- }
913
- } finally {
914
- if (cleanup) await cleanup();
915
- }
916
- } catch (err) {
917
- const msg = err instanceof Error ? err.message : String(err);
918
- console.log(pc7.red(` Failed to update ${skill.name}: ${msg}`));
919
- failCount++;
1048
+ }
1049
+ if (installed.length === 0) {
1050
+ checks.push({ label: "No installed providers to check", status: "pass" });
1051
+ }
1052
+ return { name: "Config Files", checks };
1053
+ }
1054
+ function formatSection(section) {
1055
+ const lines = [];
1056
+ lines.push(` ${pc2.bold(section.name)}`);
1057
+ for (const check of section.checks) {
1058
+ const icon = check.status === "pass" ? pc2.green("\u2713") : check.status === "warn" ? pc2.yellow("\u26A0") : pc2.red("\u2717");
1059
+ lines.push(` ${icon} ${check.label}`);
1060
+ if (check.detail) {
1061
+ lines.push(` ${pc2.dim(check.detail)}`);
1062
+ }
1063
+ }
1064
+ return lines.join("\n");
1065
+ }
1066
+ function registerDoctorCommand(program2) {
1067
+ program2.command("doctor").description("Diagnose configuration issues and health").option("--json", "Output as JSON").action(async (opts) => {
1068
+ const sections = [];
1069
+ sections.push(checkEnvironment());
1070
+ sections.push(checkRegistry());
1071
+ sections.push(checkInstalledProviders());
1072
+ sections.push(checkSkillSymlinks());
1073
+ sections.push(await checkLockFile());
1074
+ sections.push(await checkConfigFiles());
1075
+ let passed = 0;
1076
+ let warnings = 0;
1077
+ let errors = 0;
1078
+ for (const section of sections) {
1079
+ for (const check of section.checks) {
1080
+ if (check.status === "pass") passed++;
1081
+ else if (check.status === "warn") warnings++;
1082
+ else errors++;
920
1083
  }
921
1084
  }
922
- console.log();
923
- if (successCount > 0) {
924
- console.log(pc7.green(`Updated ${successCount} skill(s).`));
1085
+ if (opts.json) {
1086
+ const output = {
1087
+ version: getCaampVersion(),
1088
+ sections: sections.map((s) => ({
1089
+ name: s.name,
1090
+ checks: s.checks
1091
+ })),
1092
+ summary: { passed, warnings, errors }
1093
+ };
1094
+ console.log(JSON.stringify(output, null, 2));
1095
+ return;
925
1096
  }
926
- if (failCount > 0) {
927
- console.log(pc7.red(`Failed to update ${failCount} skill(s).`));
1097
+ console.log(pc2.bold("\ncaamp doctor\n"));
1098
+ for (const section of sections) {
1099
+ console.log(formatSection(section));
1100
+ console.log();
928
1101
  }
929
- });
930
- }
931
-
932
- // src/commands/skills/init.ts
933
- import pc8 from "picocolors";
934
- import { writeFile, mkdir } from "fs/promises";
935
- import { existsSync as existsSync2 } from "fs";
936
- import { join as join3 } from "path";
937
- function registerSkillsInit(parent) {
938
- parent.command("init").description("Create a new SKILL.md template").argument("[name]", "Skill name").option("-d, --dir <path>", "Output directory", ".").action(async (name, opts) => {
939
- const skillName = name ?? "my-skill";
940
- const skillDir = join3(opts.dir, skillName);
941
- if (existsSync2(skillDir)) {
942
- console.error(pc8.red(`Directory already exists: ${skillDir}`));
1102
+ const parts = [];
1103
+ parts.push(pc2.green(`${passed} checks passed`));
1104
+ if (warnings > 0) parts.push(pc2.yellow(`${warnings} warning${warnings !== 1 ? "s" : ""}`));
1105
+ if (errors > 0) parts.push(pc2.red(`${errors} error${errors !== 1 ? "s" : ""}`));
1106
+ console.log(` ${pc2.bold("Summary")}: ${parts.join(", ")}`);
1107
+ console.log();
1108
+ if (errors > 0) {
943
1109
  process.exit(1);
944
1110
  }
945
- await mkdir(skillDir, { recursive: true });
946
- const template = `---
947
- name: ${skillName}
948
- description: Describe what this skill does and when to use it
949
- license: MIT
950
- metadata:
951
- author: your-name
952
- version: "1.0"
953
- ---
954
-
955
- # ${skillName}
956
-
957
- ## When to use this skill
958
-
959
- Describe the conditions under which an AI agent should activate this skill.
960
-
961
- ## Instructions
962
-
963
- Provide detailed instructions for the AI agent here.
964
-
965
- ## Examples
966
-
967
- Show example inputs and expected outputs.
968
- `;
969
- await writeFile(join3(skillDir, "SKILL.md"), template, "utf-8");
970
- console.log(pc8.green(`\u2713 Created skill template: ${skillDir}/SKILL.md`));
971
- console.log(pc8.dim("\nNext steps:"));
972
- console.log(pc8.dim(" 1. Edit SKILL.md with your instructions"));
973
- console.log(pc8.dim(` 2. Validate: caamp skills validate ${join3(skillDir, "SKILL.md")}`));
974
- console.log(pc8.dim(` 3. Install: caamp skills install ${skillDir}`));
975
1111
  });
976
1112
  }
977
1113
 
978
- // src/commands/skills/audit.ts
979
- import { existsSync as existsSync3, statSync } from "fs";
980
- import pc9 from "picocolors";
981
- function registerSkillsAudit(parent) {
982
- parent.command("audit").description("Security scan skill files (46+ rules, SARIF output)").argument("[path]", "Path to SKILL.md or directory", ".").option("--sarif", "Output in SARIF format").option("--json", "Output as JSON").action(async (path, opts) => {
983
- if (!existsSync3(path)) {
984
- console.error(pc9.red(`Path not found: ${path}`));
985
- process.exit(1);
986
- }
987
- const stat = statSync(path);
988
- let results;
989
- if (stat.isFile()) {
990
- results = [await scanFile(path)];
1114
+ // src/commands/instructions/inject.ts
1115
+ import pc3 from "picocolors";
1116
+ function registerInstructionsInject(parent) {
1117
+ parent.command("inject").description("Inject instruction blocks into all provider files").option("-a, --agent <name>", "Target specific agent(s)", (v, prev) => [...prev, v], []).option("-g, --global", "Inject into global instruction files").option("--content <text>", "Custom content to inject").option("--dry-run", "Preview without writing").option("--all", "Target all known providers").action(async (opts) => {
1118
+ let providers;
1119
+ if (opts.all) {
1120
+ providers = getAllProviders();
1121
+ } else if (opts.agent.length > 0) {
1122
+ providers = opts.agent.map((a) => getProvider(a)).filter((p) => p !== void 0);
991
1123
  } else {
992
- results = await scanDirectory(path);
1124
+ providers = getInstalledProviders();
993
1125
  }
994
- if (results.length === 0) {
995
- console.log(pc9.dim("No SKILL.md files found to scan."));
1126
+ if (providers.length === 0) {
1127
+ console.error(pc3.red("No providers found."));
1128
+ process.exit(1);
1129
+ }
1130
+ const content = opts.content ?? generateInjectionContent();
1131
+ const scope = opts.global ? "global" : "project";
1132
+ const groups = groupByInstructFile(providers);
1133
+ if (opts.dryRun) {
1134
+ console.log(pc3.bold("Dry run - would inject into:\n"));
1135
+ for (const [file, group] of groups) {
1136
+ console.log(` ${pc3.bold(file)}: ${group.map((p) => p.id).join(", ")}`);
1137
+ }
1138
+ console.log(pc3.dim(`
1139
+ Scope: ${scope}`));
1140
+ console.log(pc3.dim(` Content length: ${content.length} chars`));
996
1141
  return;
997
1142
  }
998
- if (opts.sarif) {
999
- console.log(JSON.stringify(toSarif(results), null, 2));
1000
- return;
1143
+ const results = await injectAll(providers, process.cwd(), scope, content);
1144
+ for (const [file, action] of results) {
1145
+ const icon = action === "created" ? pc3.green("+") : action === "updated" ? pc3.yellow("~") : pc3.blue("^");
1146
+ console.log(` ${icon} ${file} (${action})`);
1147
+ }
1148
+ console.log(pc3.bold(`
1149
+ ${results.size} file(s) processed.`));
1150
+ });
1151
+ }
1152
+
1153
+ // src/commands/instructions/check.ts
1154
+ import pc4 from "picocolors";
1155
+ function registerInstructionsCheck(parent) {
1156
+ parent.command("check").description("Check injection status across providers").option("-a, --agent <name>", "Check specific agent(s)", (v, prev) => [...prev, v], []).option("-g, --global", "Check global instruction files").option("--json", "Output as JSON").option("--all", "Check all known providers").action(async (opts) => {
1157
+ let providers;
1158
+ if (opts.all) {
1159
+ providers = getAllProviders();
1160
+ } else if (opts.agent.length > 0) {
1161
+ providers = opts.agent.map((a) => getProvider(a)).filter((p) => p !== void 0);
1162
+ } else {
1163
+ providers = getInstalledProviders();
1001
1164
  }
1165
+ const scope = opts.global ? "global" : "project";
1166
+ const results = await checkAllInjections(providers, process.cwd(), scope);
1002
1167
  if (opts.json) {
1003
1168
  console.log(JSON.stringify(results, null, 2));
1004
1169
  return;
1005
1170
  }
1006
- let totalFindings = 0;
1007
- let allPassed = true;
1008
- for (const result of results) {
1009
- const icon = result.passed ? pc9.green("\u2713") : pc9.red("\u2717");
1010
- console.log(`
1011
- ${icon} ${pc9.bold(result.file)} (score: ${result.score}/100)`);
1012
- if (result.findings.length === 0) {
1013
- console.log(pc9.dim(" No issues found."));
1014
- continue;
1015
- }
1016
- totalFindings += result.findings.length;
1017
- if (!result.passed) allPassed = false;
1018
- for (const f of result.findings) {
1019
- const sev = f.rule.severity === "critical" ? pc9.red(f.rule.severity) : f.rule.severity === "high" ? pc9.red(f.rule.severity) : f.rule.severity === "medium" ? pc9.yellow(f.rule.severity) : pc9.dim(f.rule.severity);
1020
- console.log(` ${sev.padEnd(20)} ${f.rule.id} ${f.rule.name}`);
1021
- console.log(` ${pc9.dim(`L${f.line}: ${f.context.slice(0, 80)}`)}`);
1171
+ console.log(pc4.bold(`
1172
+ Instruction file status (${scope}):
1173
+ `));
1174
+ for (const r of results) {
1175
+ let icon;
1176
+ let label;
1177
+ switch (r.status) {
1178
+ case "current":
1179
+ icon = pc4.green("\u2713");
1180
+ label = "current";
1181
+ break;
1182
+ case "outdated":
1183
+ icon = pc4.yellow("~");
1184
+ label = "outdated";
1185
+ break;
1186
+ case "missing":
1187
+ icon = pc4.red("\u2717");
1188
+ label = "missing";
1189
+ break;
1190
+ case "none":
1191
+ icon = pc4.dim("-");
1192
+ label = "no injection";
1193
+ break;
1022
1194
  }
1195
+ console.log(` ${icon} ${r.file.padEnd(40)} ${label}`);
1023
1196
  }
1024
- console.log(pc9.bold(`
1025
- ${results.length} file(s) scanned, ${totalFindings} finding(s)`));
1026
- if (!allPassed) {
1027
- process.exit(1);
1028
- }
1197
+ console.log();
1029
1198
  });
1030
1199
  }
1031
1200
 
1032
- // src/commands/skills/validate.ts
1033
- import pc10 from "picocolors";
1034
- function registerSkillsValidate(parent) {
1035
- parent.command("validate").description("Validate SKILL.md format").argument("[path]", "Path to SKILL.md", "SKILL.md").option("--json", "Output as JSON").action(async (path, opts) => {
1036
- const result = await validateSkill(path);
1037
- if (opts.json) {
1038
- console.log(JSON.stringify(result, null, 2));
1201
+ // src/commands/instructions/update.ts
1202
+ import pc5 from "picocolors";
1203
+ function registerInstructionsUpdate(parent) {
1204
+ parent.command("update").description("Update all instruction file injections").option("-g, --global", "Update global instruction files").option("-y, --yes", "Skip confirmation").action(async (opts) => {
1205
+ const providers = getInstalledProviders();
1206
+ const scope = opts.global ? "global" : "project";
1207
+ const content = generateInjectionContent();
1208
+ const checks = await checkAllInjections(providers, process.cwd(), scope, content);
1209
+ const needsUpdate = checks.filter((c) => c.status !== "current");
1210
+ if (needsUpdate.length === 0) {
1211
+ console.log(pc5.green("All instruction files are up to date."));
1039
1212
  return;
1040
1213
  }
1041
- if (result.valid) {
1042
- console.log(pc10.green(`\u2713 ${path} is valid`));
1043
- } else {
1044
- console.log(pc10.red(`\u2717 ${path} has validation errors`));
1045
- }
1046
- for (const issue of result.issues) {
1047
- const icon = issue.level === "error" ? pc10.red("\u2717") : pc10.yellow("!");
1048
- console.log(` ${icon} [${issue.field}] ${issue.message}`);
1214
+ console.log(pc5.bold(`${needsUpdate.length} file(s) need updating:
1215
+ `));
1216
+ for (const c of needsUpdate) {
1217
+ console.log(` ${c.file} (${c.status})`);
1049
1218
  }
1050
- if (!result.valid) {
1051
- process.exit(1);
1219
+ const providerIds = new Set(needsUpdate.map((c) => c.provider));
1220
+ const toUpdate = providers.filter((p) => providerIds.has(p.id));
1221
+ const results = await injectAll(toUpdate, process.cwd(), scope, content);
1222
+ console.log();
1223
+ for (const [file, action] of results) {
1224
+ console.log(` ${pc5.green("\u2713")} ${file} (${action})`);
1052
1225
  }
1226
+ console.log(pc5.bold(`
1227
+ ${results.size} file(s) updated.`));
1053
1228
  });
1054
1229
  }
1055
1230
 
1056
- // src/commands/skills/index.ts
1057
- function registerSkillsCommands(program2) {
1058
- const skills = program2.command("skills").description("Manage AI agent skills");
1059
- registerSkillsInstall(skills);
1060
- registerSkillsRemove(skills);
1061
- registerSkillsList(skills);
1062
- registerSkillsFind(skills);
1063
- registerSkillsCheck(skills);
1064
- registerSkillsUpdate(skills);
1065
- registerSkillsInit(skills);
1066
- registerSkillsAudit(skills);
1067
- registerSkillsValidate(skills);
1231
+ // src/commands/instructions/index.ts
1232
+ function registerInstructionsCommands(program2) {
1233
+ const instructions = program2.command("instructions").description("Manage instruction file injections");
1234
+ registerInstructionsInject(instructions);
1235
+ registerInstructionsCheck(instructions);
1236
+ registerInstructionsUpdate(instructions);
1068
1237
  }
1069
1238
 
1070
1239
  // src/commands/mcp/install.ts
1071
- import pc11 from "picocolors";
1240
+ import pc6 from "picocolors";
1072
1241
  function registerMcpInstall(parent) {
1073
1242
  parent.command("install").description("Install MCP server to agent configs").argument("<source>", "MCP server source (URL, npm package, or command)").option("-a, --agent <name>", "Target specific agent(s)", (v, prev) => [...prev, v], []).option("-g, --global", "Install to global/user config").option("-n, --name <name>", "Override inferred server name").option("-t, --transport <type>", "Transport type: http (default) or sse", "http").option("--header <header>", "HTTP header (Key: Value)", (v, prev) => [...prev, v], []).option("-y, --yes", "Skip confirmation").option("--all", "Install to all detected agents").option("--dry-run", "Preview without writing").action(async (source, opts) => {
1074
1243
  const parsed = parseSource(source);
@@ -1090,19 +1259,19 @@ function registerMcpInstall(parent) {
1090
1259
  providers = getInstalledProviders();
1091
1260
  }
1092
1261
  if (providers.length === 0) {
1093
- console.error(pc11.red("No target providers found."));
1262
+ console.error(pc6.red("No target providers found."));
1094
1263
  process.exit(1);
1095
1264
  }
1096
1265
  const scope = opts.global ? "global" : "project";
1097
1266
  if (opts.dryRun) {
1098
- console.log(pc11.bold("Dry run - would install:"));
1099
- console.log(` Server: ${pc11.bold(serverName)}`);
1267
+ console.log(pc6.bold("Dry run - would install:"));
1268
+ console.log(` Server: ${pc6.bold(serverName)}`);
1100
1269
  console.log(` Config: ${JSON.stringify(config, null, 2)}`);
1101
1270
  console.log(` Scope: ${scope}`);
1102
1271
  console.log(` Providers: ${providers.map((p) => p.id).join(", ")}`);
1103
1272
  return;
1104
1273
  }
1105
- console.log(pc11.dim(`Installing "${serverName}" to ${providers.length} provider(s)...
1274
+ console.log(pc6.dim(`Installing "${serverName}" to ${providers.length} provider(s)...
1106
1275
  `));
1107
1276
  const results = await installMcpServerToAll(
1108
1277
  providers,
@@ -1112,9 +1281,9 @@ function registerMcpInstall(parent) {
1112
1281
  );
1113
1282
  for (const r of results) {
1114
1283
  if (r.success) {
1115
- console.log(` ${pc11.green("\u2713")} ${r.provider.toolName.padEnd(22)} ${pc11.dim(r.configPath)}`);
1284
+ console.log(` ${pc6.green("\u2713")} ${r.provider.toolName.padEnd(22)} ${pc6.dim(r.configPath)}`);
1116
1285
  } else {
1117
- console.log(` ${pc11.red("\u2717")} ${r.provider.toolName.padEnd(22)} ${pc11.red(r.error ?? "failed")}`);
1286
+ console.log(` ${pc6.red("\u2717")} ${r.provider.toolName.padEnd(22)} ${pc6.red(r.error ?? "failed")}`);
1118
1287
  }
1119
1288
  }
1120
1289
  const succeeded = results.filter((r) => r.success);
@@ -1127,13 +1296,13 @@ function registerMcpInstall(parent) {
1127
1296
  opts.global ?? false
1128
1297
  );
1129
1298
  }
1130
- console.log(pc11.bold(`
1299
+ console.log(pc6.bold(`
1131
1300
  ${succeeded.length}/${results.length} providers configured.`));
1132
1301
  });
1133
1302
  }
1134
1303
 
1135
1304
  // src/commands/mcp/remove.ts
1136
- import pc12 from "picocolors";
1305
+ import pc7 from "picocolors";
1137
1306
  function registerMcpRemove(parent) {
1138
1307
  parent.command("remove").description("Remove MCP server from agent configs").argument("<name>", "MCP server name to remove").option("-a, --agent <name>", "Target specific agent(s)", (v, prev) => [...prev, v], []).option("-g, --global", "Remove from global config").option("--all", "Remove from all detected agents").action(async (name, opts) => {
1139
1308
  let providers;
@@ -1149,1275 +1318,1194 @@ function registerMcpRemove(parent) {
1149
1318
  for (const provider of providers) {
1150
1319
  const success = await removeMcpServer(provider, name, scope);
1151
1320
  if (success) {
1152
- console.log(` ${pc12.green("\u2713")} Removed from ${provider.toolName}`);
1321
+ console.log(` ${pc7.green("\u2713")} Removed from ${provider.toolName}`);
1153
1322
  removed++;
1154
1323
  }
1155
1324
  }
1156
1325
  if (removed > 0) {
1157
1326
  await removeMcpFromLock(name);
1158
- console.log(pc12.green(`
1327
+ console.log(pc7.green(`
1159
1328
  \u2713 Removed "${name}" from ${removed} provider(s).`));
1160
1329
  } else {
1161
- console.log(pc12.yellow(`Server "${name}" not found in any provider config.`));
1330
+ console.log(pc7.yellow(`Server "${name}" not found in any provider config.`));
1162
1331
  }
1163
1332
  });
1164
1333
  }
1165
1334
 
1166
1335
  // src/commands/mcp/list.ts
1167
- import pc13 from "picocolors";
1336
+ import pc8 from "picocolors";
1168
1337
  function registerMcpList(parent) {
1169
1338
  parent.command("list").description("List configured MCP servers").option("-a, --agent <name>", "List for specific agent").option("-g, --global", "List global config").option("--json", "Output as JSON").action(async (opts) => {
1170
1339
  const providers = opts.agent ? [getProvider(opts.agent)].filter((p) => p !== void 0) : getInstalledProviders();
1171
1340
  const allEntries = [];
1172
1341
  for (const provider of providers) {
1173
- const scope = resolvePreferredConfigScope(provider, opts.global);
1174
- const entries = await listMcpServers(provider, scope);
1175
- allEntries.push(...entries);
1176
- }
1177
- if (opts.json) {
1178
- console.log(JSON.stringify(allEntries.map((e) => ({
1179
- provider: e.providerId,
1180
- name: e.name,
1181
- config: e.config
1182
- })), null, 2));
1183
- return;
1184
- }
1185
- if (allEntries.length === 0) {
1186
- console.log(pc13.dim("No MCP servers configured."));
1187
- return;
1188
- }
1189
- console.log(pc13.bold(`
1190
- ${allEntries.length} MCP server(s) configured:
1191
- `));
1192
- for (const entry of allEntries) {
1193
- console.log(` ${pc13.bold(entry.name.padEnd(25))} ${pc13.dim(entry.providerId)}`);
1194
- }
1195
- console.log();
1196
- });
1197
- }
1198
-
1199
- // src/commands/mcp/detect.ts
1200
- import pc14 from "picocolors";
1201
- import { existsSync as existsSync4 } from "fs";
1202
- function registerMcpDetect(parent) {
1203
- parent.command("detect").description("Auto-detect installed MCP tools and their configurations").option("--json", "Output as JSON").action(async (opts) => {
1204
- const providers = getInstalledProviders();
1205
- const detected = [];
1206
- for (const provider of providers) {
1207
- const globalPath = resolveConfigPath(provider, "global");
1208
- const projectPath = resolveConfigPath(provider, "project");
1209
- const globalEntries = await listMcpServers(provider, "global");
1210
- const projectEntries = await listMcpServers(provider, "project");
1211
- detected.push({
1212
- provider: provider.id,
1213
- hasGlobalConfig: globalPath !== null && existsSync4(globalPath),
1214
- hasProjectConfig: projectPath !== null && existsSync4(projectPath),
1215
- globalServers: globalEntries.map((e) => e.name),
1216
- projectServers: projectEntries.map((e) => e.name)
1217
- });
1218
- }
1219
- if (opts.json) {
1220
- console.log(JSON.stringify(detected, null, 2));
1221
- return;
1222
- }
1223
- console.log(pc14.bold(`
1224
- ${detected.length} provider(s) with MCP support:
1225
- `));
1226
- for (const d of detected) {
1227
- const globalIcon = d.hasGlobalConfig ? pc14.green("G") : pc14.dim("-");
1228
- const projectIcon = d.hasProjectConfig ? pc14.green("P") : pc14.dim("-");
1229
- const servers = [...d.globalServers, ...d.projectServers];
1230
- const serverList = servers.length > 0 ? pc14.dim(servers.join(", ")) : pc14.dim("no servers");
1231
- console.log(` [${globalIcon}${projectIcon}] ${pc14.bold(d.provider.padEnd(20))} ${serverList}`);
1232
- }
1233
- console.log(pc14.dim("\nG = global config, P = project config"));
1234
- console.log();
1235
- });
1236
- }
1237
-
1238
- // src/commands/mcp/index.ts
1239
- function registerMcpCommands(program2) {
1240
- const mcp = program2.command("mcp").description("Manage MCP server configurations");
1241
- registerMcpInstall(mcp);
1242
- registerMcpRemove(mcp);
1243
- registerMcpList(mcp);
1244
- registerMcpDetect(mcp);
1245
- }
1246
-
1247
- // src/commands/instructions/inject.ts
1248
- import pc15 from "picocolors";
1249
- function registerInstructionsInject(parent) {
1250
- parent.command("inject").description("Inject instruction blocks into all provider files").option("-a, --agent <name>", "Target specific agent(s)", (v, prev) => [...prev, v], []).option("-g, --global", "Inject into global instruction files").option("--content <text>", "Custom content to inject").option("--dry-run", "Preview without writing").option("--all", "Target all known providers").action(async (opts) => {
1251
- let providers;
1252
- if (opts.all) {
1253
- providers = getAllProviders();
1254
- } else if (opts.agent.length > 0) {
1255
- providers = opts.agent.map((a) => getProvider(a)).filter((p) => p !== void 0);
1256
- } else {
1257
- providers = getInstalledProviders();
1258
- }
1259
- if (providers.length === 0) {
1260
- console.error(pc15.red("No providers found."));
1261
- process.exit(1);
1262
- }
1263
- const content = opts.content ?? generateInjectionContent();
1264
- const scope = opts.global ? "global" : "project";
1265
- const groups = groupByInstructFile(providers);
1266
- if (opts.dryRun) {
1267
- console.log(pc15.bold("Dry run - would inject into:\n"));
1268
- for (const [file, group] of groups) {
1269
- console.log(` ${pc15.bold(file)}: ${group.map((p) => p.id).join(", ")}`);
1270
- }
1271
- console.log(pc15.dim(`
1272
- Scope: ${scope}`));
1273
- console.log(pc15.dim(` Content length: ${content.length} chars`));
1274
- return;
1275
- }
1276
- const results = await injectAll(providers, process.cwd(), scope, content);
1277
- for (const [file, action] of results) {
1278
- const icon = action === "created" ? pc15.green("+") : action === "updated" ? pc15.yellow("~") : pc15.blue("^");
1279
- console.log(` ${icon} ${file} (${action})`);
1280
- }
1281
- console.log(pc15.bold(`
1282
- ${results.size} file(s) processed.`));
1283
- });
1284
- }
1285
-
1286
- // src/commands/instructions/check.ts
1287
- import pc16 from "picocolors";
1288
- function registerInstructionsCheck(parent) {
1289
- parent.command("check").description("Check injection status across providers").option("-a, --agent <name>", "Check specific agent(s)", (v, prev) => [...prev, v], []).option("-g, --global", "Check global instruction files").option("--json", "Output as JSON").option("--all", "Check all known providers").action(async (opts) => {
1290
- let providers;
1291
- if (opts.all) {
1292
- providers = getAllProviders();
1293
- } else if (opts.agent.length > 0) {
1294
- providers = opts.agent.map((a) => getProvider(a)).filter((p) => p !== void 0);
1295
- } else {
1296
- providers = getInstalledProviders();
1297
- }
1298
- const scope = opts.global ? "global" : "project";
1299
- const results = await checkAllInjections(providers, process.cwd(), scope);
1300
- if (opts.json) {
1301
- console.log(JSON.stringify(results, null, 2));
1302
- return;
1303
- }
1304
- console.log(pc16.bold(`
1305
- Instruction file status (${scope}):
1306
- `));
1307
- for (const r of results) {
1308
- let icon;
1309
- let label;
1310
- switch (r.status) {
1311
- case "current":
1312
- icon = pc16.green("\u2713");
1313
- label = "current";
1314
- break;
1315
- case "outdated":
1316
- icon = pc16.yellow("~");
1317
- label = "outdated";
1318
- break;
1319
- case "missing":
1320
- icon = pc16.red("\u2717");
1321
- label = "missing";
1322
- break;
1323
- case "none":
1324
- icon = pc16.dim("-");
1325
- label = "no injection";
1326
- break;
1327
- }
1328
- console.log(` ${icon} ${r.file.padEnd(40)} ${label}`);
1342
+ const scope = resolvePreferredConfigScope(provider, opts.global);
1343
+ const entries = await listMcpServers(provider, scope);
1344
+ allEntries.push(...entries);
1345
+ }
1346
+ if (opts.json) {
1347
+ console.log(JSON.stringify(allEntries.map((e) => ({
1348
+ provider: e.providerId,
1349
+ name: e.name,
1350
+ config: e.config
1351
+ })), null, 2));
1352
+ return;
1353
+ }
1354
+ if (allEntries.length === 0) {
1355
+ console.log(pc8.dim("No MCP servers configured."));
1356
+ return;
1357
+ }
1358
+ console.log(pc8.bold(`
1359
+ ${allEntries.length} MCP server(s) configured:
1360
+ `));
1361
+ for (const entry of allEntries) {
1362
+ console.log(` ${pc8.bold(entry.name.padEnd(25))} ${pc8.dim(entry.providerId)}`);
1329
1363
  }
1330
1364
  console.log();
1331
1365
  });
1332
1366
  }
1333
1367
 
1334
- // src/commands/instructions/update.ts
1335
- import pc17 from "picocolors";
1336
- function registerInstructionsUpdate(parent) {
1337
- parent.command("update").description("Update all instruction file injections").option("-g, --global", "Update global instruction files").option("-y, --yes", "Skip confirmation").action(async (opts) => {
1368
+ // src/commands/mcp/detect.ts
1369
+ import pc9 from "picocolors";
1370
+ import { existsSync as existsSync3 } from "fs";
1371
+ function registerMcpDetect(parent) {
1372
+ parent.command("detect").description("Auto-detect installed MCP tools and their configurations").option("--json", "Output as JSON").action(async (opts) => {
1338
1373
  const providers = getInstalledProviders();
1339
- const scope = opts.global ? "global" : "project";
1340
- const content = generateInjectionContent();
1341
- const checks = await checkAllInjections(providers, process.cwd(), scope, content);
1342
- const needsUpdate = checks.filter((c) => c.status !== "current");
1343
- if (needsUpdate.length === 0) {
1344
- console.log(pc17.green("All instruction files are up to date."));
1374
+ const detected = [];
1375
+ for (const provider of providers) {
1376
+ const globalPath = resolveConfigPath(provider, "global");
1377
+ const projectPath = resolveConfigPath(provider, "project");
1378
+ const globalEntries = await listMcpServers(provider, "global");
1379
+ const projectEntries = await listMcpServers(provider, "project");
1380
+ detected.push({
1381
+ provider: provider.id,
1382
+ hasGlobalConfig: globalPath !== null && existsSync3(globalPath),
1383
+ hasProjectConfig: projectPath !== null && existsSync3(projectPath),
1384
+ globalServers: globalEntries.map((e) => e.name),
1385
+ projectServers: projectEntries.map((e) => e.name)
1386
+ });
1387
+ }
1388
+ if (opts.json) {
1389
+ console.log(JSON.stringify(detected, null, 2));
1345
1390
  return;
1346
1391
  }
1347
- console.log(pc17.bold(`${needsUpdate.length} file(s) need updating:
1392
+ console.log(pc9.bold(`
1393
+ ${detected.length} provider(s) with MCP support:
1348
1394
  `));
1349
- for (const c of needsUpdate) {
1350
- console.log(` ${c.file} (${c.status})`);
1395
+ for (const d of detected) {
1396
+ const globalIcon = d.hasGlobalConfig ? pc9.green("G") : pc9.dim("-");
1397
+ const projectIcon = d.hasProjectConfig ? pc9.green("P") : pc9.dim("-");
1398
+ const servers = [...d.globalServers, ...d.projectServers];
1399
+ const serverList = servers.length > 0 ? pc9.dim(servers.join(", ")) : pc9.dim("no servers");
1400
+ console.log(` [${globalIcon}${projectIcon}] ${pc9.bold(d.provider.padEnd(20))} ${serverList}`);
1351
1401
  }
1352
- const providerIds = new Set(needsUpdate.map((c) => c.provider));
1353
- const toUpdate = providers.filter((p) => providerIds.has(p.id));
1354
- const results = await injectAll(toUpdate, process.cwd(), scope, content);
1402
+ console.log(pc9.dim("\nG = global config, P = project config"));
1355
1403
  console.log();
1356
- for (const [file, action] of results) {
1357
- console.log(` ${pc17.green("\u2713")} ${file} (${action})`);
1358
- }
1359
- console.log(pc17.bold(`
1360
- ${results.size} file(s) updated.`));
1361
1404
  });
1362
1405
  }
1363
1406
 
1364
- // src/commands/instructions/index.ts
1365
- function registerInstructionsCommands(program2) {
1366
- const instructions = program2.command("instructions").description("Manage instruction file injections");
1367
- registerInstructionsInject(instructions);
1368
- registerInstructionsCheck(instructions);
1369
- registerInstructionsUpdate(instructions);
1407
+ // src/commands/mcp/index.ts
1408
+ function registerMcpCommands(program2) {
1409
+ const mcp = program2.command("mcp").description("Manage MCP server configurations");
1410
+ registerMcpInstall(mcp);
1411
+ registerMcpRemove(mcp);
1412
+ registerMcpList(mcp);
1413
+ registerMcpDetect(mcp);
1370
1414
  }
1371
1415
 
1372
- // src/commands/config.ts
1373
- import pc18 from "picocolors";
1374
- import { existsSync as existsSync5 } from "fs";
1375
- function registerConfigCommand(program2) {
1376
- const config = program2.command("config").description("View provider configuration");
1377
- config.command("show").description("Show provider configuration").argument("<provider>", "Provider ID or alias").option("-g, --global", "Show global config").option("--json", "Output as JSON").action(async (providerId, opts) => {
1378
- const provider = getProvider(providerId);
1379
- if (!provider) {
1380
- console.error(pc18.red(`Provider not found: ${providerId}`));
1381
- process.exit(1);
1382
- }
1383
- const configPath = resolveProviderConfigPath(
1384
- provider,
1385
- opts.global ? "global" : "project"
1386
- ) ?? provider.configPathGlobal;
1387
- if (!existsSync5(configPath)) {
1388
- console.log(pc18.dim(`No config file at: ${configPath}`));
1416
+ // src/commands/providers.ts
1417
+ import pc10 from "picocolors";
1418
+ function registerProvidersCommand(program2) {
1419
+ const providers = program2.command("providers").description("Manage AI agent providers");
1420
+ providers.command("list").description("List all supported providers").option("--json", "Output as JSON").option("--tier <tier>", "Filter by priority tier (high, medium, low)").action(async (opts) => {
1421
+ const all = opts.tier ? getProvidersByPriority(opts.tier) : getAllProviders();
1422
+ if (opts.json) {
1423
+ console.log(JSON.stringify(all, null, 2));
1389
1424
  return;
1390
1425
  }
1391
- try {
1392
- const data = await readConfig(configPath, provider.configFormat);
1393
- if (opts.json) {
1394
- console.log(JSON.stringify(data, null, 2));
1395
- } else {
1396
- console.log(pc18.bold(`
1397
- ${provider.toolName} config (${configPath}):
1426
+ console.log(pc10.bold(`
1427
+ CAMP Provider Registry v${getRegistryVersion()}`));
1428
+ console.log(pc10.dim(`${getProviderCount()} providers
1398
1429
  `));
1399
- console.log(JSON.stringify(data, null, 2));
1430
+ const tiers = ["high", "medium", "low"];
1431
+ for (const tier of tiers) {
1432
+ const tierProviders = all.filter((p) => p.priority === tier);
1433
+ if (tierProviders.length === 0) continue;
1434
+ const tierLabel = tier === "high" ? pc10.green("HIGH") : tier === "medium" ? pc10.yellow("MEDIUM") : pc10.dim("LOW");
1435
+ console.log(`${tierLabel} priority:`);
1436
+ for (const p of tierProviders) {
1437
+ const status = p.status === "active" ? pc10.green("active") : p.status === "beta" ? pc10.yellow("beta") : pc10.dim(p.status);
1438
+ console.log(` ${pc10.bold(p.agentFlag.padEnd(20))} ${p.toolName.padEnd(22)} ${p.vendor.padEnd(16)} [${status}]`);
1400
1439
  }
1401
- } catch (err) {
1402
- console.error(pc18.red(`Error reading config: ${err instanceof Error ? err.message : String(err)}`));
1403
- process.exit(1);
1440
+ console.log();
1404
1441
  }
1405
1442
  });
1406
- config.command("path").description("Show config file path").argument("<provider>", "Provider ID or alias").argument("[scope]", "Scope: project (default) or global", "project").action((providerId, scope) => {
1407
- const provider = getProvider(providerId);
1443
+ providers.command("detect").description("Auto-detect installed providers").option("--json", "Output as JSON").option("--project", "Include project-level detection").action(async (opts) => {
1444
+ const results = opts.project ? detectProjectProviders(process.cwd()) : detectAllProviders();
1445
+ const installed = results.filter((r) => r.installed);
1446
+ if (opts.json) {
1447
+ console.log(JSON.stringify(installed.map((r) => ({
1448
+ id: r.provider.id,
1449
+ toolName: r.provider.toolName,
1450
+ methods: r.methods,
1451
+ projectDetected: r.projectDetected
1452
+ })), null, 2));
1453
+ return;
1454
+ }
1455
+ console.log(pc10.bold(`
1456
+ Detected ${installed.length} installed providers:
1457
+ `));
1458
+ for (const r of installed) {
1459
+ const methods = r.methods.join(", ");
1460
+ const project = r.projectDetected ? pc10.green(" [project]") : "";
1461
+ console.log(` ${pc10.green("\u2713")} ${pc10.bold(r.provider.toolName.padEnd(22))} via ${pc10.dim(methods)}${project}`);
1462
+ }
1463
+ const notInstalled = results.filter((r) => !r.installed);
1464
+ if (notInstalled.length > 0) {
1465
+ console.log(pc10.dim(`
1466
+ ${notInstalled.length} providers not detected`));
1467
+ }
1468
+ console.log();
1469
+ });
1470
+ providers.command("show").description("Show provider details").argument("<id>", "Provider ID or alias").option("--json", "Output as JSON").action(async (id, opts) => {
1471
+ const provider = getProvider(id);
1408
1472
  if (!provider) {
1409
- console.error(pc18.red(`Provider not found: ${providerId}`));
1473
+ console.error(pc10.red(`Provider not found: ${id}`));
1410
1474
  process.exit(1);
1411
1475
  }
1412
- if (scope === "global") {
1413
- console.log(provider.configPathGlobal);
1414
- } else {
1415
- const projectPath = resolveProviderConfigPath(provider, "project");
1416
- if (projectPath) {
1417
- console.log(projectPath);
1418
- } else {
1419
- console.log(pc18.dim(`${provider.toolName} has no project-level config`));
1420
- console.log(provider.configPathGlobal);
1421
- }
1476
+ if (opts.json) {
1477
+ console.log(JSON.stringify(provider, null, 2));
1478
+ return;
1479
+ }
1480
+ console.log(pc10.bold(`
1481
+ ${provider.toolName}`));
1482
+ console.log(pc10.dim(`by ${provider.vendor}
1483
+ `));
1484
+ console.log(` ID: ${provider.id}`);
1485
+ console.log(` Flag: --agent ${provider.agentFlag}`);
1486
+ if (provider.aliases.length > 0) {
1487
+ console.log(` Aliases: ${provider.aliases.join(", ")}`);
1422
1488
  }
1489
+ console.log(` Status: ${provider.status}`);
1490
+ console.log(` Priority: ${provider.priority}`);
1491
+ console.log();
1492
+ console.log(` Instruction: ${provider.instructFile}`);
1493
+ console.log(` Config format: ${provider.configFormat}`);
1494
+ console.log(` Config key: ${provider.configKey}`);
1495
+ console.log(` Transports: ${provider.supportedTransports.join(", ")}`);
1496
+ console.log(` Headers: ${provider.supportsHeaders ? "yes" : "no"}`);
1497
+ console.log();
1498
+ console.log(pc10.dim(" Paths:"));
1499
+ console.log(` Global dir: ${provider.pathGlobal}`);
1500
+ console.log(` Project dir: ${provider.pathProject || "(none)"}`);
1501
+ console.log(` Global config: ${provider.configPathGlobal}`);
1502
+ console.log(` Project config: ${provider.configPathProject || "(none)"}`);
1503
+ console.log(` Global skills: ${provider.pathSkills}`);
1504
+ console.log(` Project skills: ${provider.pathProjectSkills || "(none)"}`);
1505
+ console.log();
1423
1506
  });
1424
1507
  }
1425
1508
 
1426
- // src/commands/doctor.ts
1427
- import pc19 from "picocolors";
1428
- import { execFileSync } from "child_process";
1429
- import { existsSync as existsSync6, readdirSync, lstatSync, readlinkSync } from "fs";
1430
- import { homedir } from "os";
1431
- import { join as join5 } from "path";
1432
-
1433
- // src/core/version.ts
1434
- import { readFileSync } from "fs";
1435
- import { dirname, join as join4 } from "path";
1436
- import { fileURLToPath } from "url";
1437
- var cachedVersion = null;
1438
- function getCaampVersion() {
1439
- if (cachedVersion) return cachedVersion;
1440
- try {
1441
- const currentDir = dirname(fileURLToPath(import.meta.url));
1442
- const packageJsonPath = join4(currentDir, "..", "..", "package.json");
1443
- const packageJson = JSON.parse(readFileSync(packageJsonPath, "utf-8"));
1444
- cachedVersion = packageJson.version ?? "0.0.0";
1445
- } catch {
1446
- cachedVersion = "0.0.0";
1447
- }
1448
- return cachedVersion;
1449
- }
1509
+ // src/commands/skills/install.ts
1510
+ import { existsSync as existsSync4 } from "fs";
1511
+ import pc11 from "picocolors";
1450
1512
 
1451
- // src/commands/doctor.ts
1452
- function getNodeVersion() {
1453
- return process.version;
1454
- }
1455
- function getNpmVersion() {
1456
- try {
1457
- return execFileSync("npm", ["--version"], { stdio: "pipe", encoding: "utf-8" }).trim();
1458
- } catch {
1459
- return null;
1460
- }
1461
- }
1462
- function checkEnvironment() {
1463
- const checks = [];
1464
- checks.push({ label: `Node.js ${getNodeVersion()}`, status: "pass" });
1465
- const npmVersion = getNpmVersion();
1466
- if (npmVersion) {
1467
- checks.push({ label: `npm ${npmVersion}`, status: "pass" });
1468
- } else {
1469
- checks.push({ label: "npm not found", status: "warn" });
1470
- }
1471
- checks.push({ label: `CAAMP v${getCaampVersion()}`, status: "pass" });
1472
- checks.push({ label: `${process.platform} ${process.arch}`, status: "pass" });
1473
- return { name: "Environment", checks };
1474
- }
1475
- function checkRegistry() {
1476
- const checks = [];
1477
- try {
1478
- const providers = getAllProviders();
1479
- const count = getProviderCount();
1480
- checks.push({ label: `${count} providers loaded`, status: "pass" });
1481
- const malformed = [];
1482
- for (const p of providers) {
1483
- if (!p.id || !p.toolName || !p.configKey || !p.configFormat) {
1484
- malformed.push(p.id || "(unknown)");
1485
- }
1486
- }
1487
- if (malformed.length === 0) {
1488
- checks.push({ label: "All entries valid", status: "pass" });
1489
- } else {
1490
- checks.push({
1491
- label: `${malformed.length} malformed entries`,
1492
- status: "fail",
1493
- detail: malformed.join(", ")
1494
- });
1495
- }
1496
- } catch (err) {
1497
- checks.push({
1498
- label: "Failed to load registry",
1499
- status: "fail",
1500
- detail: err instanceof Error ? err.message : String(err)
1501
- });
1513
+ // src/core/sources/github.ts
1514
+ import { simpleGit } from "simple-git";
1515
+ import { mkdtemp, rm } from "fs/promises";
1516
+ import { tmpdir } from "os";
1517
+ import { join as join3 } from "path";
1518
+ async function cloneRepo(owner, repo, ref, subPath) {
1519
+ const tmpDir = await mkdtemp(join3(tmpdir(), "caamp-"));
1520
+ const repoUrl = `https://github.com/${owner}/${repo}.git`;
1521
+ const git = simpleGit();
1522
+ const cloneOptions = ["--depth", "1"];
1523
+ if (ref) {
1524
+ cloneOptions.push("--branch", ref);
1502
1525
  }
1503
- return { name: "Registry", checks };
1504
- }
1505
- function checkInstalledProviders() {
1506
- const checks = [];
1507
- try {
1508
- const results = detectAllProviders();
1509
- const installed = results.filter((r) => r.installed);
1510
- checks.push({ label: `${installed.length} found`, status: "pass" });
1511
- for (const r of installed) {
1512
- const methods = r.methods.join(", ");
1513
- checks.push({ label: `${r.provider.toolName} (${methods})`, status: "pass" });
1526
+ await git.clone(repoUrl, tmpDir, cloneOptions);
1527
+ const localPath = subPath ? join3(tmpDir, subPath) : tmpDir;
1528
+ return {
1529
+ localPath,
1530
+ cleanup: async () => {
1531
+ try {
1532
+ await rm(tmpDir, { recursive: true });
1533
+ } catch {
1534
+ }
1514
1535
  }
1515
- } catch (err) {
1516
- checks.push({
1517
- label: "Detection failed",
1518
- status: "fail",
1519
- detail: err instanceof Error ? err.message : String(err)
1520
- });
1521
- }
1522
- return { name: "Installed Providers", checks };
1536
+ };
1523
1537
  }
1524
- function checkSkillSymlinks() {
1525
- const checks = [];
1526
- const canonicalDir = CANONICAL_SKILLS_DIR;
1527
- if (!existsSync6(canonicalDir)) {
1528
- checks.push({ label: "0 canonical skills", status: "pass" });
1529
- checks.push({ label: "No broken symlinks", status: "pass" });
1530
- return { name: "Skills", checks };
1538
+
1539
+ // src/core/sources/gitlab.ts
1540
+ import { simpleGit as simpleGit2 } from "simple-git";
1541
+ import { mkdtemp as mkdtemp2, rm as rm2 } from "fs/promises";
1542
+ import { tmpdir as tmpdir2 } from "os";
1543
+ import { join as join4 } from "path";
1544
+ async function cloneGitLabRepo(owner, repo, ref, subPath) {
1545
+ const tmpDir = await mkdtemp2(join4(tmpdir2(), "caamp-gl-"));
1546
+ const repoUrl = `https://gitlab.com/${owner}/${repo}.git`;
1547
+ const git = simpleGit2();
1548
+ const cloneOptions = ["--depth", "1"];
1549
+ if (ref) {
1550
+ cloneOptions.push("--branch", ref);
1531
1551
  }
1532
- let canonicalCount = 0;
1533
- let canonicalNames = [];
1534
- try {
1535
- canonicalNames = readdirSync(canonicalDir).filter((name) => {
1536
- const full = join5(canonicalDir, name);
1552
+ await git.clone(repoUrl, tmpDir, cloneOptions);
1553
+ const localPath = subPath ? join4(tmpDir, subPath) : tmpDir;
1554
+ return {
1555
+ localPath,
1556
+ cleanup: async () => {
1537
1557
  try {
1538
- const stat = lstatSync(full);
1539
- return stat.isDirectory() || stat.isSymbolicLink();
1558
+ await rm2(tmpDir, { recursive: true });
1540
1559
  } catch {
1541
- return false;
1542
1560
  }
1543
- });
1544
- canonicalCount = canonicalNames.length;
1545
- checks.push({ label: `${canonicalCount} canonical skills`, status: "pass" });
1546
- } catch {
1547
- checks.push({ label: "Cannot read skills directory", status: "warn" });
1548
- return { name: "Skills", checks };
1549
- }
1550
- const broken = [];
1551
- const stale = [];
1552
- const results = detectAllProviders();
1553
- const installed = results.filter((r) => r.installed);
1554
- for (const r of installed) {
1555
- const provider = r.provider;
1556
- const skillDir = provider.pathSkills;
1557
- if (!existsSync6(skillDir)) continue;
1558
- try {
1559
- const entries = readdirSync(skillDir);
1560
- for (const entry of entries) {
1561
- const fullPath = join5(skillDir, entry);
1561
+ }
1562
+ };
1563
+ }
1564
+
1565
+ // src/commands/skills/install.ts
1566
+ function registerSkillsInstall(parent) {
1567
+ parent.command("install").description("Install a skill from GitHub, URL, marketplace, or ct-skills catalog").argument("[source]", "Skill source (GitHub URL, owner/repo, @author/name, skill-name)").option("-a, --agent <name>", "Target specific agent(s)", (v, prev) => [...prev, v], []).option("-g, --global", "Install globally").option("-y, --yes", "Skip confirmation").option("--all", "Install to all detected agents").option("--profile <name>", "Install a ct-skills profile (minimal, core, recommended, full)").action(async (source, opts) => {
1568
+ let providers;
1569
+ if (opts.all) {
1570
+ providers = getInstalledProviders();
1571
+ } else if (opts.agent.length > 0) {
1572
+ providers = opts.agent.map((a) => getProvider(a)).filter((p) => p !== void 0);
1573
+ } else {
1574
+ providers = getInstalledProviders();
1575
+ }
1576
+ if (providers.length === 0) {
1577
+ console.error(pc11.red("No target providers found. Use --agent or --all."));
1578
+ process.exit(1);
1579
+ }
1580
+ if (opts.profile) {
1581
+ if (!isCatalogAvailable()) {
1582
+ console.error(pc11.red("@cleocode/ct-skills is not installed. Run: npm install @cleocode/ct-skills"));
1583
+ process.exit(1);
1584
+ }
1585
+ const profileSkills = resolveProfile(opts.profile);
1586
+ if (profileSkills.length === 0) {
1587
+ const available = listProfiles();
1588
+ console.error(pc11.red(`Profile not found: ${opts.profile}`));
1589
+ if (available.length > 0) {
1590
+ console.log(pc11.dim("Available profiles: " + available.join(", ")));
1591
+ }
1592
+ process.exit(1);
1593
+ }
1594
+ console.log(`Installing profile ${pc11.bold(opts.profile)} (${profileSkills.length} skill(s))...`);
1595
+ console.log(pc11.dim(`Target: ${providers.length} provider(s)`));
1596
+ let installed = 0;
1597
+ let failed = 0;
1598
+ for (const name of profileSkills) {
1599
+ const skillDir = getSkillDir(name);
1562
1600
  try {
1563
- const stat = lstatSync(fullPath);
1564
- if (!stat.isSymbolicLink()) continue;
1565
- if (!existsSync6(fullPath)) {
1566
- broken.push(`${provider.id}/${entry}`);
1601
+ const result = await installSkill(
1602
+ skillDir,
1603
+ name,
1604
+ providers,
1605
+ opts.global ?? false
1606
+ );
1607
+ if (result.success) {
1608
+ console.log(pc11.green(` + ${name}`));
1609
+ await recordSkillInstall(
1610
+ name,
1611
+ `@cleocode/ct-skills:${name}`,
1612
+ `@cleocode/ct-skills:${name}`,
1613
+ "package",
1614
+ result.linkedAgents,
1615
+ result.canonicalPath,
1616
+ true
1617
+ );
1618
+ installed++;
1567
1619
  } else {
1568
- const target = readlinkSync(fullPath);
1569
- const isCanonical = target.includes("/.agents/skills/") || target.includes("\\.agents\\skills\\");
1570
- if (!isCanonical) {
1571
- stale.push(`${provider.id}/${entry}`);
1620
+ console.log(pc11.yellow(` ! ${name}: ${result.errors.join(", ")}`));
1621
+ failed++;
1622
+ }
1623
+ } catch (err) {
1624
+ console.log(pc11.red(` x ${name}: ${err instanceof Error ? err.message : String(err)}`));
1625
+ failed++;
1626
+ }
1627
+ }
1628
+ console.log(`
1629
+ ${pc11.green(`${installed} installed`)}, ${failed > 0 ? pc11.yellow(`${failed} failed`) : "0 failed"}`);
1630
+ return;
1631
+ }
1632
+ if (!source) {
1633
+ console.error(pc11.red("Missing required argument: source"));
1634
+ console.log(pc11.dim("Usage: caamp skills install <source> or caamp skills install --profile <name>"));
1635
+ process.exit(1);
1636
+ }
1637
+ console.log(pc11.dim(`Installing to ${providers.length} provider(s)...`));
1638
+ let localPath;
1639
+ let cleanup;
1640
+ let skillName;
1641
+ let sourceValue;
1642
+ let sourceType;
1643
+ if (isMarketplaceScoped(source)) {
1644
+ console.log(pc11.dim(`Searching marketplace for ${source}...`));
1645
+ const client = new MarketplaceClient();
1646
+ let skill;
1647
+ try {
1648
+ skill = await client.getSkill(source);
1649
+ } catch (error) {
1650
+ console.error(pc11.red(`Marketplace lookup failed: ${formatNetworkError(error)}`));
1651
+ process.exit(1);
1652
+ }
1653
+ if (!skill) {
1654
+ console.error(pc11.red(`Skill not found: ${source}`));
1655
+ process.exit(1);
1656
+ }
1657
+ console.log(` Found: ${pc11.bold(skill.name)} by ${skill.author} (${pc11.dim(skill.repoFullName)})`);
1658
+ const parsed = parseSource(skill.githubUrl);
1659
+ if (parsed.type !== "github" || !parsed.owner || !parsed.repo) {
1660
+ console.error(pc11.red("Could not resolve GitHub source"));
1661
+ process.exit(1);
1662
+ }
1663
+ try {
1664
+ const subPathCandidates = buildSkillSubPathCandidates(skill.path, parsed.path);
1665
+ let cloneError;
1666
+ let cloned = false;
1667
+ for (const subPath of subPathCandidates) {
1668
+ try {
1669
+ const result = await cloneRepo(parsed.owner, parsed.repo, parsed.ref, subPath);
1670
+ if (subPath && !existsSync4(result.localPath)) {
1671
+ await result.cleanup();
1672
+ continue;
1572
1673
  }
1674
+ localPath = result.localPath;
1675
+ cleanup = result.cleanup;
1676
+ cloned = true;
1677
+ break;
1678
+ } catch (error) {
1679
+ cloneError = error;
1573
1680
  }
1574
- } catch {
1681
+ }
1682
+ if (!cloned) {
1683
+ throw cloneError ?? new Error("Unable to resolve skill path from marketplace metadata");
1684
+ }
1685
+ skillName = skill.name;
1686
+ sourceValue = skill.githubUrl;
1687
+ sourceType = parsed.type;
1688
+ } catch (error) {
1689
+ console.error(pc11.red(`Failed to fetch source repository: ${formatNetworkError(error)}`));
1690
+ process.exit(1);
1691
+ }
1692
+ } else {
1693
+ const parsed = parseSource(source);
1694
+ skillName = parsed.inferredName;
1695
+ sourceValue = parsed.value;
1696
+ sourceType = parsed.type;
1697
+ if (parsed.type === "github" && parsed.owner && parsed.repo) {
1698
+ try {
1699
+ const result = await cloneRepo(parsed.owner, parsed.repo, parsed.ref, parsed.path);
1700
+ localPath = result.localPath;
1701
+ cleanup = result.cleanup;
1702
+ } catch (error) {
1703
+ console.error(pc11.red(`Failed to clone GitHub repository: ${formatNetworkError(error)}`));
1704
+ process.exit(1);
1705
+ }
1706
+ } else if (parsed.type === "gitlab" && parsed.owner && parsed.repo) {
1707
+ try {
1708
+ const result = await cloneGitLabRepo(parsed.owner, parsed.repo, parsed.ref, parsed.path);
1709
+ localPath = result.localPath;
1710
+ cleanup = result.cleanup;
1711
+ } catch (error) {
1712
+ console.error(pc11.red(`Failed to clone GitLab repository: ${formatNetworkError(error)}`));
1713
+ process.exit(1);
1714
+ }
1715
+ } else if (parsed.type === "local") {
1716
+ localPath = parsed.value;
1717
+ const discovered = await discoverSkill(localPath);
1718
+ if (discovered) {
1719
+ skillName = discovered.name;
1720
+ }
1721
+ } else if (parsed.type === "package") {
1722
+ if (!isCatalogAvailable()) {
1723
+ console.error(pc11.red("@cleocode/ct-skills is not installed. Run: npm install @cleocode/ct-skills"));
1724
+ process.exit(1);
1725
+ }
1726
+ const catalogSkill = getSkill(parsed.inferredName);
1727
+ if (catalogSkill) {
1728
+ localPath = getSkillDir(catalogSkill.name);
1729
+ skillName = catalogSkill.name;
1730
+ sourceValue = `@cleocode/ct-skills:${catalogSkill.name}`;
1731
+ sourceType = "package";
1732
+ console.log(` Found in catalog: ${pc11.bold(catalogSkill.name)} v${catalogSkill.version} (${pc11.dim(catalogSkill.category)})`);
1733
+ } else {
1734
+ console.error(pc11.red(`Skill not found in catalog: ${parsed.inferredName}`));
1735
+ console.log(pc11.dim("Available skills: " + listSkills().join(", ")));
1736
+ process.exit(1);
1737
+ }
1738
+ } else {
1739
+ console.error(pc11.red(`Unsupported source type: ${parsed.type}`));
1740
+ process.exit(1);
1741
+ }
1742
+ }
1743
+ try {
1744
+ if (!localPath) {
1745
+ throw new Error("No local skill path resolved for installation");
1746
+ }
1747
+ const result = await installSkill(
1748
+ localPath,
1749
+ skillName,
1750
+ providers,
1751
+ opts.global ?? false
1752
+ );
1753
+ if (result.success) {
1754
+ console.log(pc11.green(`
1755
+ \u2713 Installed ${pc11.bold(skillName)}`));
1756
+ console.log(` Canonical: ${pc11.dim(result.canonicalPath)}`);
1757
+ console.log(` Linked to: ${result.linkedAgents.join(", ")}`);
1758
+ const isGlobal = sourceType === "package" ? true : opts.global ?? false;
1759
+ await recordSkillInstall(
1760
+ skillName,
1761
+ sourceValue,
1762
+ sourceValue,
1763
+ sourceType,
1764
+ result.linkedAgents,
1765
+ result.canonicalPath,
1766
+ isGlobal
1767
+ );
1768
+ }
1769
+ if (result.errors.length > 0) {
1770
+ console.log(pc11.yellow("\nWarnings:"));
1771
+ for (const err of result.errors) {
1772
+ console.log(` ${pc11.yellow("!")} ${err}`);
1575
1773
  }
1576
1774
  }
1577
- } catch {
1775
+ } finally {
1776
+ if (cleanup) await cleanup();
1578
1777
  }
1579
- }
1580
- if (broken.length === 0) {
1581
- checks.push({ label: "No broken symlinks", status: "pass" });
1582
- } else {
1583
- checks.push({
1584
- label: `${broken.length} broken symlink${broken.length !== 1 ? "s" : ""}`,
1585
- status: "warn",
1586
- detail: broken.join(", ")
1587
- });
1588
- }
1589
- if (stale.length === 0) {
1590
- checks.push({ label: "No stale symlinks", status: "pass" });
1591
- } else {
1592
- checks.push({
1593
- label: `${stale.length} stale symlink${stale.length !== 1 ? "s" : ""} (not pointing to ~/.agents/skills/)`,
1594
- status: "warn",
1595
- detail: stale.join(", ")
1596
- });
1597
- }
1598
- return { name: "Skills", checks };
1778
+ });
1599
1779
  }
1600
- async function checkLockFile() {
1601
- const checks = [];
1602
- try {
1603
- const lock = await readLockFile();
1604
- checks.push({ label: "Lock file valid", status: "pass" });
1605
- const lockSkillNames = Object.keys(lock.skills);
1606
- checks.push({ label: `${lockSkillNames.length} skill entries`, status: "pass" });
1607
- const orphaned = [];
1608
- for (const [name, entry] of Object.entries(lock.skills)) {
1609
- if (entry.canonicalPath && !existsSync6(entry.canonicalPath)) {
1610
- orphaned.push(name);
1611
- }
1612
- }
1613
- if (orphaned.length === 0) {
1614
- checks.push({ label: "0 orphaned entries", status: "pass" });
1615
- } else {
1616
- checks.push({
1617
- label: `${orphaned.length} orphaned skill${orphaned.length !== 1 ? "s" : ""} (in lock, missing from disk)`,
1618
- status: "warn",
1619
- detail: orphaned.join(", ")
1620
- });
1621
- }
1622
- const canonicalDir = CANONICAL_SKILLS_DIR;
1623
- if (existsSync6(canonicalDir)) {
1624
- const onDisk = readdirSync(canonicalDir).filter((name) => {
1625
- try {
1626
- const stat = lstatSync(join5(canonicalDir, name));
1627
- return stat.isDirectory() || stat.isSymbolicLink();
1628
- } catch {
1629
- return false;
1630
- }
1631
- });
1632
- const untracked = onDisk.filter((name) => !lock.skills[name]);
1633
- if (untracked.length === 0) {
1634
- checks.push({ label: "0 untracked skills", status: "pass" });
1780
+
1781
+ // src/commands/skills/remove.ts
1782
+ import pc12 from "picocolors";
1783
+ function registerSkillsRemove(parent) {
1784
+ parent.command("remove").description("Remove installed skill(s)").argument("[name]", "Skill name to remove").option("-g, --global", "Remove from global scope").option("-y, --yes", "Skip confirmation").action(async (name, opts) => {
1785
+ const providers = getInstalledProviders();
1786
+ if (name) {
1787
+ const result = await removeSkill(name, providers, opts.global ?? false);
1788
+ if (result.removed.length > 0) {
1789
+ console.log(pc12.green(`\u2713 Removed ${pc12.bold(name)} from: ${result.removed.join(", ")}`));
1790
+ await removeSkillFromLock(name);
1635
1791
  } else {
1636
- checks.push({
1637
- label: `${untracked.length} untracked skill${untracked.length !== 1 ? "s" : ""} (on disk, not in lock)`,
1638
- status: "warn",
1639
- detail: untracked.join(", ")
1640
- });
1792
+ console.log(pc12.yellow(`Skill ${name} not found in any provider.`));
1641
1793
  }
1642
- }
1643
- const results = detectAllProviders();
1644
- const installed = results.filter((r) => r.installed);
1645
- const mismatches = [];
1646
- for (const [name, entry] of Object.entries(lock.skills)) {
1647
- if (!entry.agents || entry.agents.length === 0) continue;
1648
- for (const agentId of entry.agents) {
1649
- const provider = installed.find((r) => r.provider.id === agentId);
1650
- if (!provider) continue;
1651
- const linkPath = join5(provider.provider.pathSkills, name);
1652
- if (!existsSync6(linkPath)) {
1653
- mismatches.push(`${name} missing from ${agentId}`);
1794
+ if (result.errors.length > 0) {
1795
+ for (const err of result.errors) {
1796
+ console.log(pc12.red(` ${err}`));
1654
1797
  }
1655
1798
  }
1656
- }
1657
- if (mismatches.length === 0) {
1658
- checks.push({ label: "Lock agent-lists match symlinks", status: "pass" });
1659
1799
  } else {
1660
- checks.push({
1661
- label: `${mismatches.length} agent-list mismatch${mismatches.length !== 1 ? "es" : ""}`,
1662
- status: "warn",
1663
- detail: mismatches.slice(0, 5).join(", ") + (mismatches.length > 5 ? ` (+${mismatches.length - 5} more)` : "")
1664
- });
1665
- }
1666
- } catch (err) {
1667
- checks.push({
1668
- label: "Failed to read lock file",
1669
- status: "fail",
1670
- detail: err instanceof Error ? err.message : String(err)
1671
- });
1672
- }
1673
- return { name: "Lock File", checks };
1674
- }
1675
- async function checkConfigFiles() {
1676
- const checks = [];
1677
- const results = detectAllProviders();
1678
- const installed = results.filter((r) => r.installed);
1679
- for (const r of installed) {
1680
- const provider = r.provider;
1681
- const configPath = provider.configPathGlobal;
1682
- if (!existsSync6(configPath)) {
1683
- checks.push({
1684
- label: `${provider.id}: no config file found`,
1685
- status: "warn",
1686
- detail: configPath
1687
- });
1688
- continue;
1689
- }
1690
- try {
1691
- await readConfig(configPath, provider.configFormat);
1692
- const relPath = configPath.replace(homedir(), "~");
1693
- checks.push({
1694
- label: `${provider.id}: ${relPath} readable`,
1695
- status: "pass"
1696
- });
1697
- } catch (err) {
1698
- checks.push({
1699
- label: `${provider.id}: config parse error`,
1700
- status: "fail",
1701
- detail: err instanceof Error ? err.message : String(err)
1702
- });
1703
- }
1704
- }
1705
- if (installed.length === 0) {
1706
- checks.push({ label: "No installed providers to check", status: "pass" });
1707
- }
1708
- return { name: "Config Files", checks };
1709
- }
1710
- function formatSection(section) {
1711
- const lines = [];
1712
- lines.push(` ${pc19.bold(section.name)}`);
1713
- for (const check of section.checks) {
1714
- const icon = check.status === "pass" ? pc19.green("\u2713") : check.status === "warn" ? pc19.yellow("\u26A0") : pc19.red("\u2717");
1715
- lines.push(` ${icon} ${check.label}`);
1716
- if (check.detail) {
1717
- lines.push(` ${pc19.dim(check.detail)}`);
1718
- }
1719
- }
1720
- return lines.join("\n");
1721
- }
1722
- function registerDoctorCommand(program2) {
1723
- program2.command("doctor").description("Diagnose configuration issues and health").option("--json", "Output as JSON").action(async (opts) => {
1724
- const sections = [];
1725
- sections.push(checkEnvironment());
1726
- sections.push(checkRegistry());
1727
- sections.push(checkInstalledProviders());
1728
- sections.push(checkSkillSymlinks());
1729
- sections.push(await checkLockFile());
1730
- sections.push(await checkConfigFiles());
1731
- let passed = 0;
1732
- let warnings = 0;
1733
- let errors = 0;
1734
- for (const section of sections) {
1735
- for (const check of section.checks) {
1736
- if (check.status === "pass") passed++;
1737
- else if (check.status === "warn") warnings++;
1738
- else errors++;
1800
+ const skills = await listCanonicalSkills();
1801
+ if (skills.length === 0) {
1802
+ console.log(pc12.dim("No skills installed."));
1803
+ return;
1739
1804
  }
1740
- }
1741
- if (opts.json) {
1742
- const output = {
1743
- version: getCaampVersion(),
1744
- sections: sections.map((s) => ({
1745
- name: s.name,
1746
- checks: s.checks
1747
- })),
1748
- summary: { passed, warnings, errors }
1749
- };
1750
- console.log(JSON.stringify(output, null, 2));
1751
- return;
1752
- }
1753
- console.log(pc19.bold("\ncaamp doctor\n"));
1754
- for (const section of sections) {
1755
- console.log(formatSection(section));
1756
- console.log();
1757
- }
1758
- const parts = [];
1759
- parts.push(pc19.green(`${passed} checks passed`));
1760
- if (warnings > 0) parts.push(pc19.yellow(`${warnings} warning${warnings !== 1 ? "s" : ""}`));
1761
- if (errors > 0) parts.push(pc19.red(`${errors} error${errors !== 1 ? "s" : ""}`));
1762
- console.log(` ${pc19.bold("Summary")}: ${parts.join(", ")}`);
1763
- console.log();
1764
- if (errors > 0) {
1765
- process.exit(1);
1805
+ console.log(pc12.bold("Installed skills:"));
1806
+ for (const s of skills) {
1807
+ console.log(` ${s}`);
1808
+ }
1809
+ console.log(pc12.dim("\nUse: caamp skills remove <name>"));
1766
1810
  }
1767
1811
  });
1768
1812
  }
1769
1813
 
1770
- // src/commands/advanced/common.ts
1771
- import { readFile } from "fs/promises";
1772
-
1773
- // src/commands/advanced/lafs.ts
1814
+ // src/commands/skills/list.ts
1774
1815
  import { randomUUID as randomUUID2 } from "crypto";
1775
- import {
1776
- isRegisteredErrorCode
1777
- } from "@cleocode/lafs-protocol";
1778
- var LAFSCommandError = class extends Error {
1779
- code;
1780
- category;
1781
- recoverable;
1782
- suggestion;
1783
- retryAfterMs;
1784
- details;
1785
- constructor(code, message, suggestion, recoverable = true, details) {
1786
- super(message);
1787
- this.name = "LAFSCommandError";
1788
- this.code = code;
1789
- this.category = inferErrorCategory(code);
1790
- this.recoverable = recoverable;
1791
- this.suggestion = suggestion;
1792
- this.retryAfterMs = null;
1793
- this.details = details;
1794
- }
1795
- };
1796
- function inferErrorCategory(code) {
1797
- if (code.includes("VALIDATION")) return "VALIDATION";
1798
- if (code.includes("NOT_FOUND")) return "NOT_FOUND";
1799
- if (code.includes("CONFLICT")) return "CONFLICT";
1800
- if (code.includes("AUTH")) return "AUTH";
1801
- if (code.includes("PERMISSION")) return "PERMISSION";
1802
- if (code.includes("RATE_LIMIT")) return "RATE_LIMIT";
1803
- if (code.includes("MIGRATION")) return "MIGRATION";
1804
- if (code.includes("CONTRACT")) return "CONTRACT";
1805
- return "INTERNAL";
1816
+ import { resolveOutputFormat } from "@cleocode/lafs-protocol";
1817
+ import pc13 from "picocolors";
1818
+ function registerSkillsList(parent) {
1819
+ parent.command("list").description("List installed skills").option("-g, --global", "List global skills").option("-a, --agent <name>", "List skills for specific agent").option("--json", "Output as JSON (default)").option("--human", "Output in human-readable format").action(async (opts) => {
1820
+ const operation = "skills.list";
1821
+ const mvi = true;
1822
+ let format;
1823
+ try {
1824
+ format = resolveOutputFormat({
1825
+ jsonFlag: opts.json ?? false,
1826
+ humanFlag: (opts.human ?? false) || isHuman(),
1827
+ projectDefault: "json"
1828
+ }).format;
1829
+ } catch (error) {
1830
+ const message = error instanceof Error ? error.message : String(error);
1831
+ emitJsonError(operation, mvi, "E_FORMAT_CONFLICT", message, "VALIDATION");
1832
+ process.exit(1);
1833
+ }
1834
+ let dirs = [];
1835
+ if (opts.agent) {
1836
+ const provider = getProvider(opts.agent);
1837
+ if (!provider) {
1838
+ const message = `Provider not found: ${opts.agent}`;
1839
+ if (format === "json") {
1840
+ emitJsonError(operation, mvi, "E_PROVIDER_NOT_FOUND", message, "NOT_FOUND", {
1841
+ agent: opts.agent
1842
+ });
1843
+ } else {
1844
+ console.error(pc13.red(message));
1845
+ }
1846
+ process.exit(1);
1847
+ }
1848
+ dirs = opts.global ? [resolveProviderSkillsDir(provider, "global")] : [resolveProviderSkillsDir(provider, "project")];
1849
+ } else if (opts.global) {
1850
+ const providers = getInstalledProviders();
1851
+ dirs = providers.map((p) => resolveProviderSkillsDir(p, "global")).filter(Boolean);
1852
+ } else {
1853
+ const providers = getInstalledProviders();
1854
+ dirs = providers.map((p) => resolveProviderSkillsDir(p, "project")).filter(Boolean);
1855
+ }
1856
+ const skills = await discoverSkillsMulti(dirs);
1857
+ if (format === "json") {
1858
+ const envelope = buildEnvelope(
1859
+ operation,
1860
+ mvi,
1861
+ {
1862
+ skills,
1863
+ count: skills.length,
1864
+ scope: opts.global ? "global" : opts.agent ? `agent:${opts.agent}` : "project"
1865
+ },
1866
+ null
1867
+ );
1868
+ console.log(JSON.stringify(envelope, null, 2));
1869
+ return;
1870
+ }
1871
+ if (skills.length === 0) {
1872
+ console.log(pc13.dim("No skills found."));
1873
+ return;
1874
+ }
1875
+ console.log(pc13.bold(`
1876
+ ${skills.length} skill(s) found:
1877
+ `));
1878
+ skills.forEach((skill, index) => {
1879
+ const num = (index + 1).toString().padStart(2);
1880
+ console.log(` ${pc13.cyan(num)}. ${pc13.bold(skill.name.padEnd(30))} ${pc13.dim(skill.metadata?.description ?? "")}`);
1881
+ });
1882
+ console.log(pc13.dim(`
1883
+ Install with: caamp skills install <name>`));
1884
+ console.log(pc13.dim(`Remove with: caamp skills remove <name>`));
1885
+ });
1806
1886
  }
1807
- function baseMeta(operation, mvi) {
1887
+ function buildEnvelope(operation, mvi, result, error) {
1808
1888
  return {
1809
- specVersion: "1.0.0",
1810
- schemaVersion: "1.0.0",
1811
- timestamp: (/* @__PURE__ */ new Date()).toISOString(),
1812
- operation,
1813
- requestId: randomUUID2(),
1814
- transport: "cli",
1815
- strict: true,
1816
- mvi,
1817
- contextVersion: 0
1818
- };
1819
- }
1820
- function emitSuccess(operation, result, mvi = true) {
1821
- const envelope = {
1822
1889
  $schema: "https://lafs.dev/schemas/v1/envelope.schema.json",
1823
1890
  _meta: {
1824
- ...baseMeta(operation, mvi)
1891
+ specVersion: "1.0.0",
1892
+ schemaVersion: "1.0.0",
1893
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
1894
+ operation,
1895
+ requestId: randomUUID2(),
1896
+ transport: "cli",
1897
+ strict: true,
1898
+ mvi,
1899
+ contextVersion: 0
1825
1900
  },
1826
- success: true,
1901
+ success: error === null,
1827
1902
  result,
1828
- error: null,
1903
+ error,
1829
1904
  page: null
1830
1905
  };
1831
- console.log(JSON.stringify(envelope, null, 2));
1832
1906
  }
1833
- function emitError(operation, error, mvi = true) {
1834
- let envelope;
1835
- if (error instanceof LAFSCommandError) {
1836
- envelope = {
1837
- $schema: "https://lafs.dev/schemas/v1/envelope.schema.json",
1838
- _meta: {
1839
- ...baseMeta(operation, mvi)
1840
- },
1841
- success: false,
1842
- result: null,
1843
- error: {
1844
- code: isRegisteredErrorCode(error.code) ? error.code : "E_INTERNAL_UNEXPECTED",
1845
- message: error.message,
1846
- category: error.category,
1847
- retryable: error.recoverable,
1848
- retryAfterMs: error.retryAfterMs,
1849
- details: {
1850
- hint: error.suggestion,
1851
- ...error.details !== void 0 ? { payload: error.details } : {}
1852
- }
1853
- },
1854
- page: null
1855
- };
1856
- } else {
1857
- envelope = {
1858
- $schema: "https://lafs.dev/schemas/v1/envelope.schema.json",
1859
- _meta: {
1860
- ...baseMeta(operation, mvi)
1861
- },
1862
- success: false,
1863
- result: null,
1864
- error: {
1865
- code: "E_INTERNAL_UNEXPECTED",
1866
- message: error instanceof Error ? error.message : String(error),
1867
- category: "INTERNAL",
1868
- retryable: false,
1869
- retryAfterMs: null,
1870
- details: {
1871
- hint: "Rerun with --verbose and validate your inputs."
1872
- }
1873
- },
1874
- page: null
1875
- };
1876
- }
1907
+ function emitJsonError(operation, mvi, code, message, category, details = {}) {
1908
+ const envelope = buildEnvelope(operation, mvi, null, {
1909
+ code,
1910
+ message,
1911
+ category,
1912
+ retryable: false,
1913
+ retryAfterMs: null,
1914
+ details
1915
+ });
1877
1916
  console.error(JSON.stringify(envelope, null, 2));
1878
1917
  }
1879
- async function runLafsCommand(command, mvi, action) {
1880
- try {
1881
- const result = await action();
1882
- emitSuccess(command, result, mvi);
1883
- } catch (error) {
1884
- emitError(command, error, mvi);
1885
- process.exit(1);
1886
- }
1887
- }
1888
1918
 
1889
- // src/commands/advanced/common.ts
1890
- var VALID_PRIORITIES = /* @__PURE__ */ new Set(["high", "medium", "low"]);
1891
- function parsePriority(value) {
1892
- if (!VALID_PRIORITIES.has(value)) {
1893
- throw new LAFSCommandError(
1894
- "E_ADVANCED_VALIDATION_PRIORITY",
1895
- `Invalid tier: ${value}`,
1896
- "Use one of: high, medium, low."
1897
- );
1898
- }
1899
- return value;
1900
- }
1901
- function resolveProviders(options) {
1902
- if (options.all) {
1903
- return getAllProviders();
1904
- }
1905
- const targetAgents = options.agent ?? [];
1906
- if (targetAgents.length === 0) {
1907
- return getInstalledProviders();
1908
- }
1909
- const providers = targetAgents.map((id) => getProvider(id)).filter((provider) => provider !== void 0);
1910
- if (providers.length !== targetAgents.length) {
1911
- const found = new Set(providers.map((provider) => provider.id));
1912
- const missing = targetAgents.filter((id) => !found.has(id));
1913
- throw new LAFSCommandError(
1914
- "E_ADVANCED_PROVIDER_NOT_FOUND",
1915
- `Unknown provider(s): ${missing.join(", ")}`,
1916
- "Check `caamp providers list` for valid provider IDs/aliases."
1917
- );
1918
- }
1919
- return providers;
1920
- }
1921
- async function readJsonFile(path) {
1922
- try {
1923
- const raw = await readFile(path, "utf-8");
1924
- return JSON.parse(raw);
1925
- } catch (error) {
1926
- throw new LAFSCommandError(
1927
- "E_ADVANCED_INPUT_JSON",
1928
- `Failed to read JSON file: ${path}`,
1929
- "Confirm the path exists and contains valid JSON.",
1930
- true,
1931
- { reason: error instanceof Error ? error.message : String(error) }
1932
- );
1933
- }
1934
- }
1935
- async function readMcpOperations(path) {
1936
- const value = await readJsonFile(path);
1937
- if (!Array.isArray(value)) {
1938
- throw new LAFSCommandError(
1939
- "E_ADVANCED_VALIDATION_MCP_ARRAY",
1940
- `MCP operations file must be a JSON array: ${path}`,
1941
- "Provide an array of objects with serverName and config fields."
1942
- );
1919
+ // src/commands/skills/find.ts
1920
+ import { randomUUID as randomUUID3 } from "crypto";
1921
+ import {
1922
+ resolveOutputFormat as resolveOutputFormat2
1923
+ } from "@cleocode/lafs-protocol";
1924
+ import pc14 from "picocolors";
1925
+ var SkillsFindValidationError = class extends Error {
1926
+ code;
1927
+ constructor(code, message) {
1928
+ super(message);
1929
+ this.code = code;
1930
+ this.name = "SkillsFindValidationError";
1943
1931
  }
1944
- const operations = [];
1945
- for (const [index, item] of value.entries()) {
1946
- if (!item || typeof item !== "object") {
1947
- throw new LAFSCommandError(
1948
- "E_ADVANCED_VALIDATION_MCP_ITEM",
1949
- `Invalid MCP operation at index ${index}`,
1950
- "Each operation must be an object with serverName and config."
1951
- );
1952
- }
1953
- const obj = item;
1954
- const serverName = obj.serverName;
1955
- const config = obj.config;
1956
- const scope = obj.scope;
1957
- if (typeof serverName !== "string" || serverName.length === 0) {
1958
- throw new LAFSCommandError(
1959
- "E_ADVANCED_VALIDATION_MCP_NAME",
1960
- `Invalid serverName at index ${index}`,
1961
- "Set serverName to a non-empty string."
1962
- );
1932
+ };
1933
+ function registerSkillsFind(parent) {
1934
+ parent.command("find").description("Search marketplace for skills").argument("[query]", "Search query").option("--recommend", "Recommend skills from constraints").option("--top <n>", "Number of recommendation candidates", "3").option("--must-have <term>", "Required criteria term", (value, previous) => [...previous, value], []).option("--prefer <term>", "Preferred criteria term", (value, previous) => [...previous, value], []).option("--exclude <term>", "Excluded criteria term", (value, previous) => [...previous, value], []).option("--details", "Include expanded machine output").option("--human", "Force human-readable output").option("--json", "Output as JSON").option("--select <indexes>", "Pre-select recommendation ranks (comma-separated)").option("-l, --limit <n>", "Max results", "20").action(async (query, opts) => {
1935
+ const operation = opts.recommend ? "skills.find.recommend" : "skills.find.search";
1936
+ const details = Boolean(opts.details);
1937
+ const mvi = !details;
1938
+ let format;
1939
+ try {
1940
+ format = resolveOutputFormat2({
1941
+ jsonFlag: opts.json ?? false,
1942
+ humanFlag: (opts.human ?? false) || isHuman(),
1943
+ projectDefault: "json"
1944
+ }).format;
1945
+ } catch (error) {
1946
+ const message = error instanceof Error ? error.message : String(error);
1947
+ if (opts.json) {
1948
+ emitJsonError2(operation, mvi, "E_FORMAT_CONFLICT", message, "VALIDATION");
1949
+ } else {
1950
+ console.error(pc14.red(message));
1951
+ }
1952
+ process.exit(1);
1963
1953
  }
1964
- if (!config || typeof config !== "object" || Array.isArray(config)) {
1965
- throw new LAFSCommandError(
1966
- "E_ADVANCED_VALIDATION_MCP_CONFIG",
1967
- `Invalid config at index ${index}`,
1968
- "Set config to an object matching McpServerConfig."
1969
- );
1954
+ if (opts.recommend) {
1955
+ try {
1956
+ const top = parseTop(opts.top);
1957
+ const mustHave = parseConstraintList(opts.mustHave);
1958
+ const prefer = parseConstraintList(opts.prefer);
1959
+ const exclude = parseConstraintList(opts.exclude);
1960
+ validateCriteriaConflicts(mustHave, prefer, exclude);
1961
+ const selectedRanks = parseSelectList(opts.select);
1962
+ const seedQuery = buildSeedQuery(query, mustHave, prefer, exclude);
1963
+ const recommendation = await recommendSkills(
1964
+ seedQuery,
1965
+ {
1966
+ mustHave,
1967
+ prefer,
1968
+ exclude
1969
+ },
1970
+ {
1971
+ top,
1972
+ includeDetails: details
1973
+ }
1974
+ );
1975
+ const options = normalizeRecommendationOptions(recommendation.ranking, details);
1976
+ validateSelectedRanks(selectedRanks, options.length);
1977
+ const selected = selectedRanks.length > 0 ? options.filter((option) => selectedRanks.includes(option.rank)) : [];
1978
+ if (format === "json") {
1979
+ const result = formatSkillRecommendations(recommendation, { mode: "json", details });
1980
+ const resultOptions = Array.isArray(result.options) ? result.options : [];
1981
+ const selectedObjects = resultOptions.filter(
1982
+ (option) => selectedRanks.includes(Number(option.rank ?? 0))
1983
+ );
1984
+ const envelope = buildEnvelope2(
1985
+ operation,
1986
+ mvi,
1987
+ {
1988
+ ...result,
1989
+ selected: selectedObjects
1990
+ },
1991
+ null
1992
+ );
1993
+ console.log(JSON.stringify(envelope, null, 2));
1994
+ return;
1995
+ }
1996
+ const human = formatSkillRecommendations(recommendation, { mode: "human", details });
1997
+ console.log(human);
1998
+ if (selected.length > 0) {
1999
+ console.log(`Selected: ${selected.map((option) => option.scopedName).join(", ")}`);
2000
+ }
2001
+ return;
2002
+ } catch (error) {
2003
+ const message = error instanceof Error ? error.message : String(error);
2004
+ const errorCode = error instanceof SkillsFindValidationError ? error.code : error.code ?? RECOMMENDATION_ERROR_CODES.SOURCE_UNAVAILABLE;
2005
+ const category = errorCode === RECOMMENDATION_ERROR_CODES.CRITERIA_CONFLICT ? "CONFLICT" : errorCode === RECOMMENDATION_ERROR_CODES.NO_MATCHES ? "NOT_FOUND" : errorCode === RECOMMENDATION_ERROR_CODES.QUERY_INVALID ? "VALIDATION" : "INTERNAL";
2006
+ if (format === "json") {
2007
+ emitJsonError2(operation, mvi, errorCode, message, category, {
2008
+ query: query ?? null
2009
+ });
2010
+ } else {
2011
+ console.error(pc14.red(`Recommendation failed: ${message}`));
2012
+ }
2013
+ process.exit(1);
2014
+ }
1970
2015
  }
1971
- if (scope !== void 0 && scope !== "project" && scope !== "global") {
1972
- throw new LAFSCommandError(
1973
- "E_ADVANCED_VALIDATION_SCOPE",
1974
- `Invalid scope at index ${index}: ${String(scope)}`,
1975
- "Use scope value 'project' or 'global'."
1976
- );
2016
+ if (!query) {
2017
+ console.log(pc14.dim("Usage: caamp skills find <query>"));
2018
+ return;
1977
2019
  }
1978
- operations.push({
1979
- serverName,
1980
- config,
1981
- ...scope ? { scope } : {}
1982
- });
1983
- }
1984
- return operations;
1985
- }
1986
- async function readSkillOperations(path) {
1987
- const value = await readJsonFile(path);
1988
- if (!Array.isArray(value)) {
1989
- throw new LAFSCommandError(
1990
- "E_ADVANCED_VALIDATION_SKILL_ARRAY",
1991
- `Skill operations file must be a JSON array: ${path}`,
1992
- "Provide an array of objects with sourcePath and skillName fields."
1993
- );
1994
- }
1995
- const operations = [];
1996
- for (const [index, item] of value.entries()) {
1997
- if (!item || typeof item !== "object") {
1998
- throw new LAFSCommandError(
1999
- "E_ADVANCED_VALIDATION_SKILL_ITEM",
2000
- `Invalid skill operation at index ${index}`,
2001
- "Each operation must be an object with sourcePath and skillName."
2002
- );
2020
+ const limit = parseInt(opts.limit, 10);
2021
+ const client = new MarketplaceClient();
2022
+ if (format === "human") {
2023
+ console.log(pc14.dim(`Searching marketplaces for "${query}"...
2024
+ `));
2003
2025
  }
2004
- const obj = item;
2005
- const sourcePath = obj.sourcePath;
2006
- const skillName = obj.skillName;
2007
- const isGlobal = obj.isGlobal;
2008
- if (typeof sourcePath !== "string" || sourcePath.length === 0) {
2009
- throw new LAFSCommandError(
2010
- "E_ADVANCED_VALIDATION_SKILL_SOURCE",
2011
- `Invalid sourcePath at index ${index}`,
2012
- "Set sourcePath to a non-empty string."
2013
- );
2026
+ let results;
2027
+ try {
2028
+ results = await client.search(query, limit);
2029
+ } catch (error) {
2030
+ const message = formatNetworkError(error);
2031
+ if (format === "json") {
2032
+ emitJsonError2(operation, mvi, "E_SEARCH_FAILED", message, "TRANSIENT", {
2033
+ query,
2034
+ limit
2035
+ });
2036
+ } else {
2037
+ console.error(pc14.red(`Marketplace search failed: ${message}`));
2038
+ }
2039
+ process.exit(1);
2014
2040
  }
2015
- if (typeof skillName !== "string" || skillName.length === 0) {
2016
- throw new LAFSCommandError(
2017
- "E_ADVANCED_VALIDATION_SKILL_NAME",
2018
- `Invalid skillName at index ${index}`,
2019
- "Set skillName to a non-empty string."
2041
+ if (format === "json") {
2042
+ const envelope = buildEnvelope2(
2043
+ operation,
2044
+ mvi,
2045
+ {
2046
+ query,
2047
+ results,
2048
+ count: results.length,
2049
+ limit
2050
+ },
2051
+ null
2020
2052
  );
2053
+ console.log(JSON.stringify(envelope, null, 2));
2054
+ return;
2021
2055
  }
2022
- if (isGlobal !== void 0 && typeof isGlobal !== "boolean") {
2023
- throw new LAFSCommandError(
2024
- "E_ADVANCED_VALIDATION_SKILL_SCOPE",
2025
- `Invalid isGlobal value at index ${index}`,
2026
- "Set isGlobal to true or false when provided."
2027
- );
2056
+ if (results.length === 0) {
2057
+ console.log(pc14.yellow("No results found."));
2058
+ return;
2028
2059
  }
2029
- operations.push({
2030
- sourcePath,
2031
- skillName,
2032
- ...isGlobal !== void 0 ? { isGlobal } : {}
2060
+ console.log(pc14.dim(`Found ${results.length} result(s) for "${query}":
2061
+ `));
2062
+ results.forEach((skill, index) => {
2063
+ const num = (index + 1).toString().padStart(2);
2064
+ const stars = skill.stars > 0 ? pc14.yellow(`\u2605 ${formatStars(skill.stars)}`) : "";
2065
+ console.log(` ${pc14.cyan(num)}. ${pc14.bold(skill.scopedName)} ${stars}`);
2066
+ console.log(` ${pc14.dim(skill.description?.slice(0, 80) ?? "")}`);
2067
+ console.log(` ${pc14.dim(`from ${skill.source}`)}`);
2068
+ console.log();
2033
2069
  });
2070
+ console.log(pc14.dim("Install with: caamp skills install <name>"));
2071
+ console.log(pc14.dim("Or select by number: caamp skills install <n>"));
2072
+ });
2073
+ }
2074
+ function formatStars(n) {
2075
+ if (n >= 1e3) return `${(n / 1e3).toFixed(1)}k`;
2076
+ return String(n);
2077
+ }
2078
+ function parseConstraintList(values) {
2079
+ const normalized = values.flatMap((value) => tokenizeCriteriaValue(value));
2080
+ return Array.from(new Set(normalized));
2081
+ }
2082
+ function parseTop(value) {
2083
+ const parsed = Number.parseInt(value, 10);
2084
+ if (!Number.isInteger(parsed) || parsed < 1 || parsed > 20) {
2085
+ throw new SkillsFindValidationError(RECOMMENDATION_ERROR_CODES.QUERY_INVALID, "--top must be an integer between 1 and 20");
2034
2086
  }
2035
- return operations;
2087
+ return parsed;
2036
2088
  }
2037
- async function readTextInput(inlineContent, filePath) {
2038
- if (inlineContent && filePath) {
2039
- throw new LAFSCommandError(
2040
- "E_ADVANCED_VALIDATION_INPUT_MODE",
2041
- "Provide either inline content or a content file, not both.",
2042
- "Use --content OR --content-file."
2043
- );
2089
+ function parseSelectList(value) {
2090
+ if (!value) return [];
2091
+ const parsed = value.split(",").map((entry) => Number.parseInt(entry.trim(), 10)).filter((entry) => Number.isInteger(entry) && entry > 0);
2092
+ return Array.from(new Set(parsed));
2093
+ }
2094
+ function buildSeedQuery(query, mustHave, prefer, exclude) {
2095
+ if (query && query.trim().length > 0) {
2096
+ return query;
2044
2097
  }
2045
- if (inlineContent) return inlineContent;
2046
- if (!filePath) return void 0;
2047
- try {
2048
- return await readFile(filePath, "utf-8");
2049
- } catch (error) {
2050
- throw new LAFSCommandError(
2051
- "E_ADVANCED_INPUT_TEXT",
2052
- `Failed to read content file: ${filePath}`,
2053
- "Confirm the file exists and is readable.",
2054
- true,
2055
- { reason: error instanceof Error ? error.message : String(error) }
2056
- );
2098
+ const seedTerms = [...mustHave, ...prefer, ...exclude].filter((term) => term.length > 0);
2099
+ if (seedTerms.length > 0) {
2100
+ return seedTerms.join(" ");
2057
2101
  }
2102
+ throw new SkillsFindValidationError(
2103
+ RECOMMENDATION_ERROR_CODES.QUERY_INVALID,
2104
+ "Recommendation mode requires a query or at least one criteria flag."
2105
+ );
2058
2106
  }
2059
-
2060
- // src/commands/advanced/providers.ts
2061
- function registerAdvancedProviders(parent) {
2062
- parent.command("providers").description("Select providers by priority using advanced wrapper logic").option("-a, --agent <name>", "Target specific provider(s)", (v, prev) => [...prev, v], []).option("--all", "Use all registry providers (not only detected)").option("--min-tier <tier>", "Minimum priority tier: high|medium|low", "low").option("--details", "Include full provider objects").action(async (opts) => runLafsCommand("advanced.providers", !opts.details, async () => {
2063
- const providers = resolveProviders({ all: opts.all, agent: opts.agent });
2064
- const minTier = parsePriority(opts.minTier);
2065
- const selected = selectProvidersByMinimumPriority(providers, minTier);
2107
+ function normalizeRecommendationOptions(ranking, details) {
2108
+ return ranking.map((entry, index) => {
2109
+ const whyCodes = entry.reasons.map((reason) => reason.code);
2066
2110
  return {
2067
- objective: "Filter providers by minimum priority tier",
2068
- constraints: {
2069
- minTier,
2070
- selectionMode: opts.all ? "registry" : "detected-or-explicit"
2071
- },
2072
- acceptanceCriteria: {
2073
- selectedCount: selected.length,
2074
- orderedByPriority: true
2075
- },
2076
- data: opts.details ? selected : selected.map((provider) => ({
2077
- id: provider.id,
2078
- priority: provider.priority,
2079
- status: provider.status,
2080
- configFormat: provider.configFormat
2081
- }))
2111
+ rank: index + 1,
2112
+ scopedName: entry.skill.scopedName,
2113
+ description: entry.skill.description,
2114
+ score: entry.score,
2115
+ why: whyCodes.length > 0 ? whyCodes.join(", ") : "score-based match",
2116
+ source: entry.skill.source,
2117
+ ...details ? {
2118
+ evidence: {
2119
+ reasons: entry.reasons,
2120
+ breakdown: entry.breakdown
2121
+ }
2122
+ } : {}
2082
2123
  };
2083
- }));
2124
+ });
2084
2125
  }
2085
-
2086
- // src/commands/advanced/batch.ts
2087
- function registerAdvancedBatch(parent) {
2088
- parent.command("batch").description("Run rollback-capable batch install for MCP + skills").option("-a, --agent <name>", "Target specific provider(s)", (v, prev) => [...prev, v], []).option("--all", "Use all registry providers (not only detected)").option("--min-tier <tier>", "Minimum priority tier: high|medium|low", "low").option("--mcp-file <path>", "JSON file containing McpBatchOperation[]").option("--skills-file <path>", "JSON file containing SkillBatchOperation[]").option("--project-dir <path>", "Project directory to resolve project-scope paths").option("--details", "Include detailed operation result").action(async (opts) => runLafsCommand("advanced.batch", !opts.details, async () => {
2089
- const baseProviders = resolveProviders({ all: opts.all, agent: opts.agent });
2090
- const minimumPriority = parsePriority(opts.minTier);
2091
- const providers = selectProvidersByMinimumPriority(baseProviders, minimumPriority);
2092
- const mcp = opts.mcpFile ? await readMcpOperations(opts.mcpFile) : [];
2093
- const skills = opts.skillsFile ? await readSkillOperations(opts.skillsFile) : [];
2094
- if (mcp.length === 0 && skills.length === 0) {
2095
- throw new LAFSCommandError(
2096
- "E_ADVANCED_VALIDATION_NO_OPS",
2097
- "No operations provided.",
2098
- "Provide --mcp-file and/or --skills-file."
2126
+ function validateCriteriaConflicts(mustHave, prefer, exclude) {
2127
+ const overlap = mustHave.filter((term) => exclude.includes(term));
2128
+ if (overlap.length > 0) {
2129
+ throw new SkillsFindValidationError(
2130
+ RECOMMENDATION_ERROR_CODES.CRITERIA_CONFLICT,
2131
+ "A criteria term cannot be both required and excluded."
2132
+ );
2133
+ }
2134
+ const preferOverlap = prefer.filter((term) => exclude.includes(term));
2135
+ if (preferOverlap.length > 0) {
2136
+ throw new SkillsFindValidationError(
2137
+ RECOMMENDATION_ERROR_CODES.CRITERIA_CONFLICT,
2138
+ "A criteria term cannot be both preferred and excluded."
2139
+ );
2140
+ }
2141
+ }
2142
+ function validateSelectedRanks(selectedRanks, total) {
2143
+ for (const rank of selectedRanks) {
2144
+ if (rank < 1 || rank > total) {
2145
+ throw new SkillsFindValidationError(
2146
+ RECOMMENDATION_ERROR_CODES.QUERY_INVALID,
2147
+ `--select rank ${rank} is out of range (1-${total}).`
2099
2148
  );
2100
2149
  }
2101
- if (providers.length === 0) {
2102
- throw new LAFSCommandError(
2103
- "E_ADVANCED_NO_TARGET_PROVIDERS",
2104
- "No target providers resolved for this batch operation.",
2105
- "Use --all or pass provider IDs with --agent."
2106
- );
2150
+ }
2151
+ }
2152
+ function buildEnvelope2(operation, mvi, result, error) {
2153
+ return {
2154
+ $schema: "https://lafs.dev/schemas/v1/envelope.schema.json",
2155
+ _meta: {
2156
+ specVersion: "1.0.0",
2157
+ schemaVersion: "1.0.0",
2158
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
2159
+ operation,
2160
+ requestId: randomUUID3(),
2161
+ transport: "cli",
2162
+ strict: true,
2163
+ mvi,
2164
+ contextVersion: 0
2165
+ },
2166
+ success: error === null,
2167
+ result,
2168
+ error,
2169
+ page: null
2170
+ };
2171
+ }
2172
+ function emitJsonError2(operation, mvi, code, message, category, details = {}) {
2173
+ const envelope = buildEnvelope2(operation, mvi, null, {
2174
+ code,
2175
+ message,
2176
+ category,
2177
+ retryable: false,
2178
+ retryAfterMs: null,
2179
+ details
2180
+ });
2181
+ console.error(JSON.stringify(envelope, null, 2));
2182
+ }
2183
+
2184
+ // src/commands/skills/check.ts
2185
+ import pc15 from "picocolors";
2186
+ function registerSkillsCheck(parent) {
2187
+ parent.command("check").description("Check for available skill updates").option("--json", "Output as JSON").action(async (opts) => {
2188
+ const tracked = await getTrackedSkills();
2189
+ const entries = Object.entries(tracked);
2190
+ if (entries.length === 0) {
2191
+ console.log(pc15.dim("No tracked skills."));
2192
+ return;
2107
2193
  }
2108
- const result = await installBatchWithRollback({
2109
- providers,
2110
- minimumPriority,
2111
- mcp,
2112
- skills,
2113
- projectDir: opts.projectDir
2114
- });
2115
- if (!result.success) {
2116
- throw new LAFSCommandError(
2117
- "E_ADVANCED_BATCH_FAILED",
2118
- result.error ?? "Batch operation failed.",
2119
- "Check rollbackErrors and input configs, then retry.",
2120
- true,
2121
- result
2122
- );
2194
+ console.log(pc15.dim(`Checking ${entries.length} skill(s) for updates...
2195
+ `));
2196
+ const results = [];
2197
+ for (const [name, entry] of entries) {
2198
+ const update = await checkSkillUpdate(name);
2199
+ results.push({ name, entry, ...update });
2123
2200
  }
2124
- return {
2125
- objective: "Install MCP and skills with rollback safety",
2126
- constraints: {
2127
- minimumPriority,
2128
- providerCount: providers.length,
2129
- mcpOps: mcp.length,
2130
- skillOps: skills.length
2131
- },
2132
- acceptanceCriteria: {
2133
- success: result.success,
2134
- rollbackPerformed: result.rollbackPerformed
2135
- },
2136
- data: opts.details ? result : {
2137
- providerCount: result.providerIds.length,
2138
- mcpApplied: result.mcpApplied,
2139
- skillsApplied: result.skillsApplied,
2140
- rollbackPerformed: result.rollbackPerformed
2201
+ if (opts.json) {
2202
+ console.log(JSON.stringify(results, null, 2));
2203
+ return;
2204
+ }
2205
+ let updatesAvailable = 0;
2206
+ for (const r of results) {
2207
+ let statusLabel;
2208
+ if (r.status === "update-available") {
2209
+ statusLabel = pc15.yellow("update available");
2210
+ updatesAvailable++;
2211
+ } else if (r.status === "up-to-date") {
2212
+ statusLabel = pc15.green("up to date");
2213
+ } else {
2214
+ statusLabel = pc15.dim("unknown");
2141
2215
  }
2142
- };
2143
- }));
2216
+ console.log(` ${pc15.bold(r.name.padEnd(30))} ${statusLabel}`);
2217
+ if (r.currentVersion || r.latestVersion) {
2218
+ const current = r.currentVersion ? r.currentVersion.slice(0, 12) : "?";
2219
+ const latest = r.latestVersion ?? "?";
2220
+ if (r.hasUpdate) {
2221
+ console.log(` ${pc15.dim("current:")} ${current} ${pc15.dim("->")} ${pc15.cyan(latest)}`);
2222
+ } else {
2223
+ console.log(` ${pc15.dim("version:")} ${current}`);
2224
+ }
2225
+ }
2226
+ console.log(` ${pc15.dim(`source: ${r.entry.source}`)}`);
2227
+ console.log(` ${pc15.dim(`agents: ${r.entry.agents.join(", ")}`)}`);
2228
+ console.log();
2229
+ }
2230
+ if (updatesAvailable > 0) {
2231
+ console.log(pc15.yellow(`${updatesAvailable} update(s) available.`));
2232
+ console.log(pc15.dim("Run `caamp skills update` to update all."));
2233
+ } else {
2234
+ console.log(pc15.green("All skills are up to date."));
2235
+ }
2236
+ });
2144
2237
  }
2145
2238
 
2146
- // src/commands/advanced/conflicts.ts
2147
- function registerAdvancedConflicts(parent) {
2148
- parent.command("conflicts").description("Preflight MCP conflict detection across providers").requiredOption("--mcp-file <path>", "JSON file containing McpBatchOperation[]").option("-a, --agent <name>", "Target specific provider(s)", (v, prev) => [...prev, v], []).option("--all", "Use all registry providers (not only detected)").option("--min-tier <tier>", "Minimum priority tier: high|medium|low", "low").option("--project-dir <path>", "Project directory to resolve project-scope paths").option("--details", "Include full conflict list").action(async (opts) => runLafsCommand("advanced.conflicts", !opts.details, async () => {
2149
- const baseProviders = resolveProviders({ all: opts.all, agent: opts.agent });
2150
- const minimumPriority = parsePriority(opts.minTier);
2151
- const providers = selectProvidersByMinimumPriority(baseProviders, minimumPriority);
2152
- const operations = await readMcpOperations(opts.mcpFile);
2153
- if (providers.length === 0) {
2154
- throw new LAFSCommandError(
2155
- "E_ADVANCED_NO_TARGET_PROVIDERS",
2156
- "No target providers resolved for conflict detection.",
2157
- "Use --all or pass provider IDs with --agent."
2158
- );
2239
+ // src/commands/skills/update.ts
2240
+ import pc16 from "picocolors";
2241
+ function registerSkillsUpdate(parent) {
2242
+ parent.command("update").description("Update all outdated skills").option("-y, --yes", "Skip confirmation").action(async (opts) => {
2243
+ const tracked = await getTrackedSkills();
2244
+ const entries = Object.entries(tracked);
2245
+ if (entries.length === 0) {
2246
+ console.log(pc16.dim("No tracked skills to update."));
2247
+ return;
2159
2248
  }
2160
- const conflicts = await detectMcpConfigConflicts(
2161
- providers,
2162
- operations,
2163
- opts.projectDir
2164
- );
2165
- const countByCode = conflicts.reduce((acc, conflict) => {
2166
- acc[conflict.code] = (acc[conflict.code] ?? 0) + 1;
2167
- return acc;
2168
- }, {});
2169
- return {
2170
- objective: "Detect MCP configuration conflicts before mutation",
2171
- constraints: {
2172
- minimumPriority,
2173
- providerCount: providers.length,
2174
- operationCount: operations.length
2175
- },
2176
- acceptanceCriteria: {
2177
- conflictCount: conflicts.length
2178
- },
2179
- data: opts.details ? conflicts : {
2180
- conflictCount: conflicts.length,
2181
- countByCode,
2182
- sample: conflicts.slice(0, 5)
2249
+ console.log(pc16.dim(`Checking ${entries.length} skill(s) for updates...`));
2250
+ const outdated = [];
2251
+ for (const [name] of entries) {
2252
+ const result = await checkSkillUpdate(name);
2253
+ if (result.hasUpdate) {
2254
+ outdated.push({
2255
+ name,
2256
+ currentVersion: result.currentVersion,
2257
+ latestVersion: result.latestVersion
2258
+ });
2183
2259
  }
2184
- };
2185
- }));
2186
- }
2187
-
2188
- // src/commands/advanced/apply.ts
2189
- var VALID_POLICIES = /* @__PURE__ */ new Set(["fail", "skip", "overwrite"]);
2190
- function parsePolicy(value) {
2191
- if (!VALID_POLICIES.has(value)) {
2192
- throw new LAFSCommandError(
2193
- "E_ADVANCED_VALIDATION_POLICY",
2194
- `Invalid policy: ${value}`,
2195
- "Use one of: fail, skip, overwrite."
2196
- );
2197
- }
2198
- return value;
2199
- }
2200
- function registerAdvancedApply(parent) {
2201
- parent.command("apply").description("Apply MCP operations with configurable conflict policy").requiredOption("--mcp-file <path>", "JSON file containing McpBatchOperation[]").option("--policy <policy>", "Conflict policy: fail|skip|overwrite", "fail").option("-a, --agent <name>", "Target specific provider(s)", (v, prev) => [...prev, v], []).option("--all", "Use all registry providers (not only detected)").option("--min-tier <tier>", "Minimum priority tier: high|medium|low", "low").option("--project-dir <path>", "Project directory to resolve project-scope paths").option("--details", "Include detailed apply result").action(async (opts) => runLafsCommand("advanced.apply", !opts.details, async () => {
2202
- const baseProviders = resolveProviders({ all: opts.all, agent: opts.agent });
2203
- const minimumPriority = parsePriority(opts.minTier);
2204
- const providers = selectProvidersByMinimumPriority(baseProviders, minimumPriority);
2205
- const operations = await readMcpOperations(opts.mcpFile);
2206
- const policy = parsePolicy(opts.policy);
2207
- if (providers.length === 0) {
2208
- throw new LAFSCommandError(
2209
- "E_ADVANCED_NO_TARGET_PROVIDERS",
2210
- "No target providers resolved for apply operation.",
2211
- "Use --all or pass provider IDs with --agent."
2212
- );
2213
2260
  }
2214
- const result = await applyMcpInstallWithPolicy(
2215
- providers,
2216
- operations,
2217
- policy,
2218
- opts.projectDir
2219
- );
2220
- if (policy === "fail" && result.conflicts.length > 0) {
2221
- throw new LAFSCommandError(
2222
- "E_ADVANCED_CONFLICTS_BLOCKING",
2223
- "Conflicts detected and policy is set to fail.",
2224
- "Run `caamp advanced conflicts` to inspect, or rerun with --policy skip/overwrite.",
2225
- true,
2226
- result
2227
- );
2261
+ if (outdated.length === 0) {
2262
+ console.log(pc16.green("\nAll skills are up to date."));
2263
+ return;
2228
2264
  }
2229
- const failedWrites = result.applied.filter((entry) => !entry.success);
2230
- if (failedWrites.length > 0) {
2231
- throw new LAFSCommandError(
2232
- "E_ADVANCED_APPLY_WRITE_FAILED",
2233
- "One or more MCP writes failed.",
2234
- "Check result details, fix provider config issues, and retry.",
2235
- true,
2236
- result
2237
- );
2265
+ console.log(pc16.yellow(`
2266
+ ${outdated.length} skill(s) have updates available:
2267
+ `));
2268
+ for (const skill of outdated) {
2269
+ const current = skill.currentVersion?.slice(0, 12) ?? "?";
2270
+ const latest = skill.latestVersion ?? "?";
2271
+ console.log(` ${pc16.bold(skill.name)} ${pc16.dim(current)} ${pc16.dim("->")} ${pc16.cyan(latest)}`);
2238
2272
  }
2239
- return {
2240
- objective: "Apply MCP operations with policy-driven conflict handling",
2241
- constraints: {
2242
- policy,
2243
- minimumPriority,
2244
- providerCount: providers.length,
2245
- operationCount: operations.length
2246
- },
2247
- acceptanceCriteria: {
2248
- conflicts: result.conflicts.length,
2249
- writesSucceeded: result.applied.length
2250
- },
2251
- data: opts.details ? result : {
2252
- conflicts: result.conflicts.length,
2253
- applied: result.applied.length,
2254
- skipped: result.skipped.length
2273
+ if (!opts.yes) {
2274
+ const readline = await import("readline");
2275
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
2276
+ const answer = await new Promise((resolve) => {
2277
+ rl.question(pc16.dim("\nProceed with update? [y/N] "), resolve);
2278
+ });
2279
+ rl.close();
2280
+ if (answer.toLowerCase() !== "y" && answer.toLowerCase() !== "yes") {
2281
+ console.log(pc16.dim("Update cancelled."));
2282
+ return;
2283
+ }
2284
+ }
2285
+ console.log();
2286
+ let successCount = 0;
2287
+ let failCount = 0;
2288
+ for (const skill of outdated) {
2289
+ const entry = tracked[skill.name];
2290
+ if (!entry) continue;
2291
+ console.log(pc16.dim(`Updating ${pc16.bold(skill.name)}...`));
2292
+ try {
2293
+ const parsed = parseSource(entry.source);
2294
+ let localPath;
2295
+ let cleanup;
2296
+ if (parsed.type === "github" && parsed.owner && parsed.repo) {
2297
+ const result = await cloneRepo(parsed.owner, parsed.repo, parsed.ref, parsed.path);
2298
+ localPath = result.localPath;
2299
+ cleanup = result.cleanup;
2300
+ } else if (parsed.type === "gitlab" && parsed.owner && parsed.repo) {
2301
+ const result = await cloneGitLabRepo(parsed.owner, parsed.repo, parsed.ref, parsed.path);
2302
+ localPath = result.localPath;
2303
+ cleanup = result.cleanup;
2304
+ } else {
2305
+ console.log(pc16.yellow(` Skipped ${skill.name}: source type "${parsed.type}" does not support auto-update`));
2306
+ continue;
2307
+ }
2308
+ try {
2309
+ const providers = entry.agents.map((a) => getProvider(a)).filter((p) => p !== void 0);
2310
+ if (providers.length === 0) {
2311
+ console.log(pc16.yellow(` Skipped ${skill.name}: no valid providers found`));
2312
+ continue;
2313
+ }
2314
+ const installResult = await installSkill(
2315
+ localPath,
2316
+ skill.name,
2317
+ providers,
2318
+ entry.isGlobal,
2319
+ entry.projectDir
2320
+ );
2321
+ if (installResult.success) {
2322
+ await recordSkillInstall(
2323
+ skill.name,
2324
+ entry.scopedName,
2325
+ entry.source,
2326
+ entry.sourceType,
2327
+ installResult.linkedAgents,
2328
+ installResult.canonicalPath,
2329
+ entry.isGlobal,
2330
+ entry.projectDir,
2331
+ skill.latestVersion
2332
+ );
2333
+ console.log(pc16.green(` Updated ${pc16.bold(skill.name)}`));
2334
+ successCount++;
2335
+ } else {
2336
+ console.log(pc16.red(` Failed to update ${skill.name}: no agents linked`));
2337
+ failCount++;
2338
+ }
2339
+ if (installResult.errors.length > 0) {
2340
+ for (const err of installResult.errors) {
2341
+ console.log(pc16.yellow(` ${err}`));
2342
+ }
2343
+ }
2344
+ } finally {
2345
+ if (cleanup) await cleanup();
2346
+ }
2347
+ } catch (err) {
2348
+ const msg = err instanceof Error ? err.message : String(err);
2349
+ console.log(pc16.red(` Failed to update ${skill.name}: ${msg}`));
2350
+ failCount++;
2255
2351
  }
2256
- };
2257
- }));
2352
+ }
2353
+ console.log();
2354
+ if (successCount > 0) {
2355
+ console.log(pc16.green(`Updated ${successCount} skill(s).`));
2356
+ }
2357
+ if (failCount > 0) {
2358
+ console.log(pc16.red(`Failed to update ${failCount} skill(s).`));
2359
+ }
2360
+ });
2258
2361
  }
2259
2362
 
2260
- // src/commands/advanced/instructions.ts
2261
- function registerAdvancedInstructions(parent) {
2262
- parent.command("instructions").description("Single-operation instruction update across providers").option("-a, --agent <name>", "Target specific provider(s)", (v, prev) => [...prev, v], []).option("--all", "Use all registry providers (not only detected)").option("--min-tier <tier>", "Minimum priority tier: high|medium|low", "low").option("--scope <scope>", "Instruction scope: project|global", "project").option("--content <text>", "Inline content to inject").option("--content-file <path>", "File containing content to inject").option("--project-dir <path>", "Project directory to resolve project-scope paths").option("--details", "Include detailed per-file actions").action(async (opts) => runLafsCommand("advanced.instructions", !opts.details, async () => {
2263
- const minimumPriority = parsePriority(opts.minTier);
2264
- const baseProviders = resolveProviders({ all: opts.all, agent: opts.agent });
2265
- const providers = selectProvidersByMinimumPriority(baseProviders, minimumPriority);
2266
- const scope = opts.scope === "global" ? "global" : opts.scope === "project" ? "project" : null;
2267
- if (!scope) {
2268
- throw new LAFSCommandError(
2269
- "E_ADVANCED_VALIDATION_SCOPE",
2270
- `Invalid scope: ${opts.scope}`,
2271
- "Use --scope project or --scope global."
2272
- );
2363
+ // src/commands/skills/init.ts
2364
+ import pc17 from "picocolors";
2365
+ import { writeFile, mkdir } from "fs/promises";
2366
+ import { existsSync as existsSync5 } from "fs";
2367
+ import { join as join5 } from "path";
2368
+ function registerSkillsInit(parent) {
2369
+ parent.command("init").description("Create a new SKILL.md template").argument("[name]", "Skill name").option("-d, --dir <path>", "Output directory", ".").action(async (name, opts) => {
2370
+ const skillName = name ?? "my-skill";
2371
+ const skillDir = join5(opts.dir, skillName);
2372
+ if (existsSync5(skillDir)) {
2373
+ console.error(pc17.red(`Directory already exists: ${skillDir}`));
2374
+ process.exit(1);
2273
2375
  }
2274
- const content = await readTextInput(opts.content, opts.contentFile);
2275
- if (!content || content.trim().length === 0) {
2276
- throw new LAFSCommandError(
2277
- "E_ADVANCED_VALIDATION_CONTENT",
2278
- "Instruction content is required.",
2279
- "Provide --content or --content-file with non-empty text."
2280
- );
2376
+ await mkdir(skillDir, { recursive: true });
2377
+ const template = `---
2378
+ name: ${skillName}
2379
+ description: Describe what this skill does and when to use it
2380
+ license: MIT
2381
+ metadata:
2382
+ author: your-name
2383
+ version: "1.0"
2384
+ ---
2385
+
2386
+ # ${skillName}
2387
+
2388
+ ## When to use this skill
2389
+
2390
+ Describe the conditions under which an AI agent should activate this skill.
2391
+
2392
+ ## Instructions
2393
+
2394
+ Provide detailed instructions for the AI agent here.
2395
+
2396
+ ## Examples
2397
+
2398
+ Show example inputs and expected outputs.
2399
+ `;
2400
+ await writeFile(join5(skillDir, "SKILL.md"), template, "utf-8");
2401
+ console.log(pc17.green(`\u2713 Created skill template: ${skillDir}/SKILL.md`));
2402
+ console.log(pc17.dim("\nNext steps:"));
2403
+ console.log(pc17.dim(" 1. Edit SKILL.md with your instructions"));
2404
+ console.log(pc17.dim(` 2. Validate: caamp skills validate ${join5(skillDir, "SKILL.md")}`));
2405
+ console.log(pc17.dim(` 3. Install: caamp skills install ${skillDir}`));
2406
+ });
2407
+ }
2408
+
2409
+ // src/commands/skills/audit.ts
2410
+ import { existsSync as existsSync6, statSync } from "fs";
2411
+ import pc18 from "picocolors";
2412
+ function registerSkillsAudit(parent) {
2413
+ parent.command("audit").description("Security scan skill files (46+ rules, SARIF output)").argument("[path]", "Path to SKILL.md or directory", ".").option("--sarif", "Output in SARIF format").option("--json", "Output as JSON").action(async (path, opts) => {
2414
+ if (!existsSync6(path)) {
2415
+ console.error(pc18.red(`Path not found: ${path}`));
2416
+ process.exit(1);
2281
2417
  }
2282
- if (providers.length === 0) {
2283
- throw new LAFSCommandError(
2284
- "E_ADVANCED_NO_TARGET_PROVIDERS",
2285
- "No target providers resolved for instruction update.",
2286
- "Use --all or pass provider IDs with --agent."
2287
- );
2418
+ const stat = statSync(path);
2419
+ let results;
2420
+ if (stat.isFile()) {
2421
+ results = [await scanFile(path)];
2422
+ } else {
2423
+ results = await scanDirectory(path);
2288
2424
  }
2289
- const summary = await updateInstructionsSingleOperation(
2290
- providers,
2291
- content,
2292
- scope,
2293
- opts.projectDir
2294
- );
2295
- return {
2296
- objective: "Update instruction files across providers in one operation",
2297
- constraints: {
2298
- scope,
2299
- minimumPriority,
2300
- providerCount: providers.length
2301
- },
2302
- acceptanceCriteria: {
2303
- updatedFiles: summary.updatedFiles
2304
- },
2305
- data: opts.details ? summary : {
2306
- updatedFiles: summary.updatedFiles,
2307
- files: summary.actions.map((entry) => ({
2308
- file: entry.file,
2309
- action: entry.action
2310
- }))
2425
+ if (results.length === 0) {
2426
+ console.log(pc18.dim("No SKILL.md files found to scan."));
2427
+ return;
2428
+ }
2429
+ if (opts.sarif) {
2430
+ console.log(JSON.stringify(toSarif(results), null, 2));
2431
+ return;
2432
+ }
2433
+ if (opts.json) {
2434
+ console.log(JSON.stringify(results, null, 2));
2435
+ return;
2436
+ }
2437
+ let totalFindings = 0;
2438
+ let allPassed = true;
2439
+ for (const result of results) {
2440
+ const icon = result.passed ? pc18.green("\u2713") : pc18.red("\u2717");
2441
+ console.log(`
2442
+ ${icon} ${pc18.bold(result.file)} (score: ${result.score}/100)`);
2443
+ if (result.findings.length === 0) {
2444
+ console.log(pc18.dim(" No issues found."));
2445
+ continue;
2311
2446
  }
2312
- };
2313
- }));
2447
+ totalFindings += result.findings.length;
2448
+ if (!result.passed) allPassed = false;
2449
+ for (const f of result.findings) {
2450
+ const sev = f.rule.severity === "critical" ? pc18.red(f.rule.severity) : f.rule.severity === "high" ? pc18.red(f.rule.severity) : f.rule.severity === "medium" ? pc18.yellow(f.rule.severity) : pc18.dim(f.rule.severity);
2451
+ console.log(` ${sev.padEnd(20)} ${f.rule.id} ${f.rule.name}`);
2452
+ console.log(` ${pc18.dim(`L${f.line}: ${f.context.slice(0, 80)}`)}`);
2453
+ }
2454
+ }
2455
+ console.log(pc18.bold(`
2456
+ ${results.length} file(s) scanned, ${totalFindings} finding(s)`));
2457
+ if (!allPassed) {
2458
+ process.exit(1);
2459
+ }
2460
+ });
2314
2461
  }
2315
2462
 
2316
- // src/commands/advanced/configure.ts
2317
- function registerAdvancedConfigure(parent) {
2318
- parent.command("configure").description("Configure global + project scope for one provider in one operation").requiredOption("-a, --agent <name>", "Target provider ID or alias").option("--global-mcp-file <path>", "JSON file for global MCP operations").option("--project-mcp-file <path>", "JSON file for project MCP operations").option("--instruction <text>", "Instruction content for both scopes").option("--instruction-file <path>", "Instruction content file for both scopes").option("--instruction-global <text>", "Instruction content for global scope").option("--instruction-global-file <path>", "Instruction content file for global scope").option("--instruction-project <text>", "Instruction content for project scope").option("--instruction-project-file <path>", "Instruction content file for project scope").option("--project-dir <path>", "Project directory to resolve project-scope paths").option("--details", "Include detailed write results").action(async (opts) => runLafsCommand("advanced.configure", !opts.details, async () => {
2319
- const provider = getProvider(opts.agent);
2320
- if (!provider) {
2321
- throw new LAFSCommandError(
2322
- "E_ADVANCED_PROVIDER_NOT_FOUND",
2323
- `Unknown provider: ${opts.agent}`,
2324
- "Check `caamp providers list` for valid provider IDs/aliases."
2325
- );
2463
+ // src/commands/skills/validate.ts
2464
+ import pc19 from "picocolors";
2465
+ function registerSkillsValidate(parent) {
2466
+ parent.command("validate").description("Validate SKILL.md format").argument("[path]", "Path to SKILL.md", "SKILL.md").option("--json", "Output as JSON").action(async (path, opts) => {
2467
+ const result = await validateSkill(path);
2468
+ if (opts.json) {
2469
+ console.log(JSON.stringify(result, null, 2));
2470
+ return;
2326
2471
  }
2327
- const globalMcp = opts.globalMcpFile ? await readMcpOperations(opts.globalMcpFile) : [];
2328
- const projectMcp = opts.projectMcpFile ? await readMcpOperations(opts.projectMcpFile) : [];
2329
- const sharedInstruction = await readTextInput(opts.instruction, opts.instructionFile);
2330
- const globalInstruction = await readTextInput(
2331
- opts.instructionGlobal,
2332
- opts.instructionGlobalFile
2333
- );
2334
- const projectInstruction = await readTextInput(
2335
- opts.instructionProject,
2336
- opts.instructionProjectFile
2337
- );
2338
- let instructionContent;
2339
- if (globalInstruction || projectInstruction) {
2340
- instructionContent = {
2341
- ...globalInstruction ? { global: globalInstruction } : {},
2342
- ...projectInstruction ? { project: projectInstruction } : {}
2343
- };
2344
- } else if (sharedInstruction) {
2345
- instructionContent = sharedInstruction;
2472
+ if (result.valid) {
2473
+ console.log(pc19.green(`\u2713 ${path} is valid`));
2474
+ } else {
2475
+ console.log(pc19.red(`\u2717 ${path} has validation errors`));
2346
2476
  }
2347
- if (globalMcp.length === 0 && projectMcp.length === 0 && !instructionContent) {
2348
- throw new LAFSCommandError(
2349
- "E_ADVANCED_VALIDATION_NO_OPS",
2350
- "No configuration operations were provided.",
2351
- "Provide MCP files and/or instruction content."
2352
- );
2477
+ for (const issue of result.issues) {
2478
+ const icon = issue.level === "error" ? pc19.red("\u2717") : pc19.yellow("!");
2479
+ console.log(` ${icon} [${issue.field}] ${issue.message}`);
2353
2480
  }
2354
- const result = await configureProviderGlobalAndProject(provider, {
2355
- globalMcp: globalMcp.map((entry) => ({
2356
- serverName: entry.serverName,
2357
- config: entry.config
2358
- })),
2359
- projectMcp: projectMcp.map((entry) => ({
2360
- serverName: entry.serverName,
2361
- config: entry.config
2362
- })),
2363
- instructionContent,
2364
- projectDir: opts.projectDir
2365
- });
2366
- const globalFailures = result.mcp.global.filter((entry) => !entry.success);
2367
- const projectFailures = result.mcp.project.filter((entry) => !entry.success);
2368
- if (globalFailures.length > 0 || projectFailures.length > 0) {
2369
- throw new LAFSCommandError(
2370
- "E_ADVANCED_CONFIGURE_FAILED",
2371
- "One or more MCP writes failed during configure operation.",
2372
- "Inspect the failed write entries and provider config paths, then retry.",
2373
- true,
2374
- result
2375
- );
2481
+ if (!result.valid) {
2482
+ process.exit(1);
2376
2483
  }
2377
- return {
2378
- objective: "Configure global and project settings in one operation",
2379
- constraints: {
2380
- provider: provider.id,
2381
- globalMcpOps: globalMcp.length,
2382
- projectMcpOps: projectMcp.length,
2383
- instructionMode: instructionContent ? typeof instructionContent === "string" ? "shared" : "scoped" : "none"
2384
- },
2385
- acceptanceCriteria: {
2386
- globalWrites: result.mcp.global.length,
2387
- projectWrites: result.mcp.project.length
2388
- },
2389
- data: opts.details ? result : {
2390
- providerId: result.providerId,
2391
- configPaths: result.configPaths,
2392
- globalWrites: result.mcp.global.length,
2393
- projectWrites: result.mcp.project.length,
2394
- instructionUpdates: {
2395
- global: result.instructions.global?.size ?? 0,
2396
- project: result.instructions.project?.size ?? 0
2397
- }
2398
- }
2399
- };
2400
- }));
2484
+ });
2401
2485
  }
2402
2486
 
2403
- // src/commands/advanced/index.ts
2404
- function registerAdvancedCommands(program2) {
2405
- const advanced = program2.command("advanced").description("LAFS-compliant wrappers for advanced orchestration APIs");
2406
- registerAdvancedProviders(advanced);
2407
- registerAdvancedBatch(advanced);
2408
- registerAdvancedConflicts(advanced);
2409
- registerAdvancedApply(advanced);
2410
- registerAdvancedInstructions(advanced);
2411
- registerAdvancedConfigure(advanced);
2487
+ // src/commands/skills/index.ts
2488
+ function registerSkillsCommands(program2) {
2489
+ const skills = program2.command("skills").description("Manage AI agent skills");
2490
+ registerSkillsInstall(skills);
2491
+ registerSkillsRemove(skills);
2492
+ registerSkillsList(skills);
2493
+ registerSkillsFind(skills);
2494
+ registerSkillsCheck(skills);
2495
+ registerSkillsUpdate(skills);
2496
+ registerSkillsInit(skills);
2497
+ registerSkillsAudit(skills);
2498
+ registerSkillsValidate(skills);
2412
2499
  }
2413
2500
 
2414
2501
  // src/cli.ts
2415
2502
  var program = new Command();
2416
- program.name("caamp").description("Central AI Agent Managed Packages - unified provider registry and package manager").version(getCaampVersion()).option("-v, --verbose", "Show debug output").option("-q, --quiet", "Suppress non-error output");
2503
+ program.name("caamp").description("Central AI Agent Managed Packages - unified provider registry and package manager").version(getCaampVersion()).option("-v, --verbose", "Show debug output").option("-q, --quiet", "Suppress non-error output").option("--human", "Output in human-readable format (default: JSON for LLM agents)");
2417
2504
  program.hook("preAction", (thisCommand) => {
2418
2505
  const opts = thisCommand.optsWithGlobals();
2419
2506
  if (opts.verbose) setVerbose(true);
2420
2507
  if (opts.quiet) setQuiet(true);
2508
+ if (opts.human) setHuman(true);
2421
2509
  });
2422
2510
  registerProvidersCommand(program);
2423
2511
  registerSkillsCommands(program);