@cleocode/caamp 0.1.0
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/README.md +85 -0
- package/dist/chunk-63BH7QMR.js +1998 -0
- package/dist/chunk-63BH7QMR.js.map +1 -0
- package/dist/cli.d.ts +1 -0
- package/dist/cli.js +941 -0
- package/dist/cli.js.map +1 -0
- package/dist/index.d.ts +465 -0
- package/dist/index.js +119 -0
- package/dist/index.js.map +1 -0
- package/package.json +70 -0
- package/providers/registry.json +721 -0
|
@@ -0,0 +1,1998 @@
|
|
|
1
|
+
// src/core/registry/providers.ts
|
|
2
|
+
import { readFileSync, existsSync } from "fs";
|
|
3
|
+
import { homedir } from "os";
|
|
4
|
+
import { join, dirname } from "path";
|
|
5
|
+
import { fileURLToPath } from "url";
|
|
6
|
+
function findRegistryPath() {
|
|
7
|
+
const thisDir = dirname(fileURLToPath(import.meta.url));
|
|
8
|
+
const devPath = join(thisDir, "..", "..", "..", "providers", "registry.json");
|
|
9
|
+
if (existsSync(devPath)) return devPath;
|
|
10
|
+
const distPath = join(thisDir, "..", "providers", "registry.json");
|
|
11
|
+
if (existsSync(distPath)) return distPath;
|
|
12
|
+
let dir = thisDir;
|
|
13
|
+
for (let i = 0; i < 5; i++) {
|
|
14
|
+
const candidate = join(dir, "providers", "registry.json");
|
|
15
|
+
if (existsSync(candidate)) return candidate;
|
|
16
|
+
dir = dirname(dir);
|
|
17
|
+
}
|
|
18
|
+
throw new Error(`Cannot find providers/registry.json (searched from ${thisDir})`);
|
|
19
|
+
}
|
|
20
|
+
var _registry = null;
|
|
21
|
+
var _providers = null;
|
|
22
|
+
var _aliasMap = null;
|
|
23
|
+
function getPlatformPaths() {
|
|
24
|
+
const home = homedir();
|
|
25
|
+
const platform = process.platform;
|
|
26
|
+
if (platform === "win32") {
|
|
27
|
+
const appData = process.env["APPDATA"] ?? join(home, "AppData", "Roaming");
|
|
28
|
+
return {
|
|
29
|
+
config: appData,
|
|
30
|
+
vscodeConfig: join(appData, "Code", "User"),
|
|
31
|
+
zedConfig: join(appData, "Zed"),
|
|
32
|
+
claudeDesktopConfig: join(appData, "Claude")
|
|
33
|
+
};
|
|
34
|
+
} else if (platform === "darwin") {
|
|
35
|
+
return {
|
|
36
|
+
config: process.env["XDG_CONFIG_HOME"] ?? join(home, ".config"),
|
|
37
|
+
vscodeConfig: join(home, "Library", "Application Support", "Code", "User"),
|
|
38
|
+
zedConfig: join(home, "Library", "Application Support", "Zed"),
|
|
39
|
+
claudeDesktopConfig: join(home, "Library", "Application Support", "Claude")
|
|
40
|
+
};
|
|
41
|
+
} else {
|
|
42
|
+
const config = process.env["XDG_CONFIG_HOME"] ?? join(home, ".config");
|
|
43
|
+
return {
|
|
44
|
+
config,
|
|
45
|
+
vscodeConfig: join(config, "Code", "User"),
|
|
46
|
+
zedConfig: join(config, "zed"),
|
|
47
|
+
claudeDesktopConfig: join(config, "Claude")
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
function resolvePath(template) {
|
|
52
|
+
const home = homedir();
|
|
53
|
+
const paths = getPlatformPaths();
|
|
54
|
+
return template.replace(/\$HOME/g, home).replace(/\$CONFIG/g, paths.config).replace(/\$VSCODE_CONFIG/g, paths.vscodeConfig).replace(/\$ZED_CONFIG/g, paths.zedConfig).replace(/\$CLAUDE_DESKTOP_CONFIG/g, paths.claudeDesktopConfig);
|
|
55
|
+
}
|
|
56
|
+
function resolveProvider(raw) {
|
|
57
|
+
return {
|
|
58
|
+
id: raw.id,
|
|
59
|
+
toolName: raw.toolName,
|
|
60
|
+
vendor: raw.vendor,
|
|
61
|
+
agentFlag: raw.agentFlag,
|
|
62
|
+
aliases: raw.aliases,
|
|
63
|
+
pathGlobal: resolvePath(raw.pathGlobal),
|
|
64
|
+
pathProject: raw.pathProject,
|
|
65
|
+
instructFile: raw.instructFile,
|
|
66
|
+
configKey: raw.configKey,
|
|
67
|
+
configFormat: raw.configFormat,
|
|
68
|
+
configPathGlobal: resolvePath(raw.configPathGlobal),
|
|
69
|
+
configPathProject: raw.configPathProject,
|
|
70
|
+
pathSkills: resolvePath(raw.pathSkills),
|
|
71
|
+
pathProjectSkills: raw.pathProjectSkills,
|
|
72
|
+
detection: {
|
|
73
|
+
methods: raw.detection.methods,
|
|
74
|
+
binary: raw.detection.binary,
|
|
75
|
+
directories: raw.detection.directories?.map(resolvePath),
|
|
76
|
+
appBundle: raw.detection.appBundle,
|
|
77
|
+
flatpakId: raw.detection.flatpakId
|
|
78
|
+
},
|
|
79
|
+
supportedTransports: raw.supportedTransports,
|
|
80
|
+
supportsHeaders: raw.supportsHeaders,
|
|
81
|
+
priority: raw.priority,
|
|
82
|
+
status: raw.status,
|
|
83
|
+
agentSkillsCompatible: raw.agentSkillsCompatible
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
function loadRegistry() {
|
|
87
|
+
if (_registry) return _registry;
|
|
88
|
+
const registryPath = findRegistryPath();
|
|
89
|
+
const raw = readFileSync(registryPath, "utf-8");
|
|
90
|
+
_registry = JSON.parse(raw);
|
|
91
|
+
return _registry;
|
|
92
|
+
}
|
|
93
|
+
function ensureProviders() {
|
|
94
|
+
if (_providers) return;
|
|
95
|
+
const registry = loadRegistry();
|
|
96
|
+
_providers = /* @__PURE__ */ new Map();
|
|
97
|
+
_aliasMap = /* @__PURE__ */ new Map();
|
|
98
|
+
for (const [id, raw] of Object.entries(registry.providers)) {
|
|
99
|
+
const provider = resolveProvider(raw);
|
|
100
|
+
_providers.set(id, provider);
|
|
101
|
+
for (const alias of provider.aliases) {
|
|
102
|
+
_aliasMap.set(alias, id);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
function getAllProviders() {
|
|
107
|
+
ensureProviders();
|
|
108
|
+
return Array.from(_providers.values());
|
|
109
|
+
}
|
|
110
|
+
function getProvider(idOrAlias) {
|
|
111
|
+
ensureProviders();
|
|
112
|
+
const resolved = _aliasMap.get(idOrAlias) ?? idOrAlias;
|
|
113
|
+
return _providers.get(resolved);
|
|
114
|
+
}
|
|
115
|
+
function resolveAlias(idOrAlias) {
|
|
116
|
+
ensureProviders();
|
|
117
|
+
return _aliasMap.get(idOrAlias) ?? idOrAlias;
|
|
118
|
+
}
|
|
119
|
+
function getProvidersByPriority(priority) {
|
|
120
|
+
return getAllProviders().filter((p) => p.priority === priority);
|
|
121
|
+
}
|
|
122
|
+
function getProvidersByStatus(status) {
|
|
123
|
+
return getAllProviders().filter((p) => p.status === status);
|
|
124
|
+
}
|
|
125
|
+
function getProvidersByInstructFile(file) {
|
|
126
|
+
return getAllProviders().filter((p) => p.instructFile === file);
|
|
127
|
+
}
|
|
128
|
+
function getInstructionFiles() {
|
|
129
|
+
const files = /* @__PURE__ */ new Set();
|
|
130
|
+
for (const p of getAllProviders()) {
|
|
131
|
+
files.add(p.instructFile);
|
|
132
|
+
}
|
|
133
|
+
return Array.from(files);
|
|
134
|
+
}
|
|
135
|
+
function getProviderCount() {
|
|
136
|
+
ensureProviders();
|
|
137
|
+
return _providers.size;
|
|
138
|
+
}
|
|
139
|
+
function getRegistryVersion() {
|
|
140
|
+
return loadRegistry().version;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// src/core/registry/detection.ts
|
|
144
|
+
import { existsSync as existsSync2 } from "fs";
|
|
145
|
+
import { execFileSync } from "child_process";
|
|
146
|
+
import { join as join2 } from "path";
|
|
147
|
+
function checkBinary(binary) {
|
|
148
|
+
try {
|
|
149
|
+
execFileSync("which", [binary], { stdio: "pipe" });
|
|
150
|
+
return true;
|
|
151
|
+
} catch {
|
|
152
|
+
return false;
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
function checkDirectory(dir) {
|
|
156
|
+
return existsSync2(dir);
|
|
157
|
+
}
|
|
158
|
+
function checkAppBundle(appName) {
|
|
159
|
+
if (process.platform !== "darwin") return false;
|
|
160
|
+
return existsSync2(join2("/Applications", appName));
|
|
161
|
+
}
|
|
162
|
+
function checkFlatpak(flatpakId) {
|
|
163
|
+
if (process.platform !== "linux") return false;
|
|
164
|
+
try {
|
|
165
|
+
execFileSync("flatpak", ["info", flatpakId], { stdio: "pipe" });
|
|
166
|
+
return true;
|
|
167
|
+
} catch {
|
|
168
|
+
return false;
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
function detectProvider(provider) {
|
|
172
|
+
const matchedMethods = [];
|
|
173
|
+
const detection = provider.detection;
|
|
174
|
+
for (const method of detection.methods) {
|
|
175
|
+
switch (method) {
|
|
176
|
+
case "binary":
|
|
177
|
+
if (detection.binary && checkBinary(detection.binary)) {
|
|
178
|
+
matchedMethods.push("binary");
|
|
179
|
+
}
|
|
180
|
+
break;
|
|
181
|
+
case "directory":
|
|
182
|
+
if (detection.directories) {
|
|
183
|
+
for (const dir of detection.directories) {
|
|
184
|
+
if (checkDirectory(dir)) {
|
|
185
|
+
matchedMethods.push("directory");
|
|
186
|
+
break;
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
break;
|
|
191
|
+
case "appBundle":
|
|
192
|
+
if (detection.appBundle && checkAppBundle(detection.appBundle)) {
|
|
193
|
+
matchedMethods.push("appBundle");
|
|
194
|
+
}
|
|
195
|
+
break;
|
|
196
|
+
case "flatpak":
|
|
197
|
+
if (detection.flatpakId && checkFlatpak(detection.flatpakId)) {
|
|
198
|
+
matchedMethods.push("flatpak");
|
|
199
|
+
}
|
|
200
|
+
break;
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
return {
|
|
204
|
+
provider,
|
|
205
|
+
installed: matchedMethods.length > 0,
|
|
206
|
+
methods: matchedMethods,
|
|
207
|
+
projectDetected: false
|
|
208
|
+
};
|
|
209
|
+
}
|
|
210
|
+
function detectProjectProvider(provider, projectDir) {
|
|
211
|
+
if (!provider.pathProject) return false;
|
|
212
|
+
return existsSync2(join2(projectDir, provider.pathProject));
|
|
213
|
+
}
|
|
214
|
+
function detectAllProviders() {
|
|
215
|
+
const providers = getAllProviders();
|
|
216
|
+
return providers.map(detectProvider);
|
|
217
|
+
}
|
|
218
|
+
function getInstalledProviders() {
|
|
219
|
+
return detectAllProviders().filter((r) => r.installed).map((r) => r.provider);
|
|
220
|
+
}
|
|
221
|
+
function detectProjectProviders(projectDir) {
|
|
222
|
+
const results = detectAllProviders();
|
|
223
|
+
return results.map((r) => ({
|
|
224
|
+
...r,
|
|
225
|
+
projectDetected: detectProjectProvider(r.provider, projectDir)
|
|
226
|
+
}));
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
// src/core/sources/parser.ts
|
|
230
|
+
var GITHUB_SHORTHAND = /^([a-zA-Z0-9_.-]+)\/([a-zA-Z0-9_.-]+)(?:\/(.+))?$/;
|
|
231
|
+
var GITHUB_URL = /^https?:\/\/(?:www\.)?github\.com\/([^/]+)\/([^/]+)(?:\/tree\/([^/]+)(?:\/(.+))?)?/;
|
|
232
|
+
var GITLAB_URL = /^https?:\/\/(?:www\.)?gitlab\.com\/([^/]+)\/([^/]+)(?:\/-\/tree\/([^/]+)(?:\/(.+))?)?/;
|
|
233
|
+
var HTTP_URL = /^https?:\/\//;
|
|
234
|
+
var NPM_SCOPED = /^@[a-zA-Z0-9_.-]+\/[a-zA-Z0-9_.-]+$/;
|
|
235
|
+
var NPM_PACKAGE = /^[a-zA-Z0-9_.-]+$/;
|
|
236
|
+
function inferName(source, type) {
|
|
237
|
+
if (type === "remote") {
|
|
238
|
+
try {
|
|
239
|
+
const url = new URL(source);
|
|
240
|
+
const parts = url.hostname.split(".");
|
|
241
|
+
if (parts.length >= 2) {
|
|
242
|
+
const brand = parts.length === 3 ? parts[parts.length - 2] : parts[0];
|
|
243
|
+
if (brand !== "www" && brand !== "api" && brand !== "mcp") {
|
|
244
|
+
return brand;
|
|
245
|
+
}
|
|
246
|
+
return parts[parts.length - 2] ?? parts[0];
|
|
247
|
+
}
|
|
248
|
+
return parts[0];
|
|
249
|
+
} catch {
|
|
250
|
+
return source;
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
if (type === "package") {
|
|
254
|
+
let name = source.replace(/^@[^/]+\//, "");
|
|
255
|
+
name = name.replace(/^mcp-server-/, "");
|
|
256
|
+
name = name.replace(/^server-/, "");
|
|
257
|
+
name = name.replace(/-mcp$/, "");
|
|
258
|
+
name = name.replace(/-server$/, "");
|
|
259
|
+
return name;
|
|
260
|
+
}
|
|
261
|
+
if (type === "github" || type === "gitlab") {
|
|
262
|
+
const match = source.match(/\/([^/]+?)(?:\.git)?$/);
|
|
263
|
+
return match?.[1] ?? source;
|
|
264
|
+
}
|
|
265
|
+
if (type === "command") {
|
|
266
|
+
const parts = source.split(/\s+/);
|
|
267
|
+
const command = parts.find((p) => !p.startsWith("-") && p !== "npx" && p !== "node" && p !== "python" && p !== "python3");
|
|
268
|
+
return command ?? parts[0] ?? source;
|
|
269
|
+
}
|
|
270
|
+
return source;
|
|
271
|
+
}
|
|
272
|
+
function parseSource(input) {
|
|
273
|
+
const ghUrlMatch = input.match(GITHUB_URL);
|
|
274
|
+
if (ghUrlMatch) {
|
|
275
|
+
return {
|
|
276
|
+
type: "github",
|
|
277
|
+
value: input,
|
|
278
|
+
inferredName: ghUrlMatch[2],
|
|
279
|
+
owner: ghUrlMatch[1],
|
|
280
|
+
repo: ghUrlMatch[2],
|
|
281
|
+
ref: ghUrlMatch[3],
|
|
282
|
+
path: ghUrlMatch[4]
|
|
283
|
+
};
|
|
284
|
+
}
|
|
285
|
+
const glUrlMatch = input.match(GITLAB_URL);
|
|
286
|
+
if (glUrlMatch) {
|
|
287
|
+
return {
|
|
288
|
+
type: "gitlab",
|
|
289
|
+
value: input,
|
|
290
|
+
inferredName: glUrlMatch[2],
|
|
291
|
+
owner: glUrlMatch[1],
|
|
292
|
+
repo: glUrlMatch[2],
|
|
293
|
+
ref: glUrlMatch[3],
|
|
294
|
+
path: glUrlMatch[4]
|
|
295
|
+
};
|
|
296
|
+
}
|
|
297
|
+
if (HTTP_URL.test(input)) {
|
|
298
|
+
return {
|
|
299
|
+
type: "remote",
|
|
300
|
+
value: input,
|
|
301
|
+
inferredName: inferName(input, "remote")
|
|
302
|
+
};
|
|
303
|
+
}
|
|
304
|
+
if (input.startsWith("/") || input.startsWith("./") || input.startsWith("../") || input.startsWith("~")) {
|
|
305
|
+
return {
|
|
306
|
+
type: "local",
|
|
307
|
+
value: input,
|
|
308
|
+
inferredName: inferName(input, "local")
|
|
309
|
+
};
|
|
310
|
+
}
|
|
311
|
+
const ghShorthand = input.match(GITHUB_SHORTHAND);
|
|
312
|
+
if (ghShorthand && !NPM_SCOPED.test(input)) {
|
|
313
|
+
return {
|
|
314
|
+
type: "github",
|
|
315
|
+
value: `https://github.com/${ghShorthand[1]}/${ghShorthand[2]}`,
|
|
316
|
+
inferredName: ghShorthand[2],
|
|
317
|
+
owner: ghShorthand[1],
|
|
318
|
+
repo: ghShorthand[2],
|
|
319
|
+
path: ghShorthand[3]
|
|
320
|
+
};
|
|
321
|
+
}
|
|
322
|
+
if (NPM_SCOPED.test(input)) {
|
|
323
|
+
return {
|
|
324
|
+
type: "package",
|
|
325
|
+
value: input,
|
|
326
|
+
inferredName: inferName(input, "package")
|
|
327
|
+
};
|
|
328
|
+
}
|
|
329
|
+
if (NPM_PACKAGE.test(input) && !input.includes(" ")) {
|
|
330
|
+
return {
|
|
331
|
+
type: "package",
|
|
332
|
+
value: input,
|
|
333
|
+
inferredName: inferName(input, "package")
|
|
334
|
+
};
|
|
335
|
+
}
|
|
336
|
+
return {
|
|
337
|
+
type: "command",
|
|
338
|
+
value: input,
|
|
339
|
+
inferredName: inferName(input, "command")
|
|
340
|
+
};
|
|
341
|
+
}
|
|
342
|
+
function isMarketplaceScoped(input) {
|
|
343
|
+
return /^@[a-zA-Z0-9_.-]+\/[a-zA-Z0-9_.-]+$/.test(input);
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
// src/core/skills/installer.ts
|
|
347
|
+
import { mkdir, symlink, rm, cp } from "fs/promises";
|
|
348
|
+
import { existsSync as existsSync3, lstatSync } from "fs";
|
|
349
|
+
import { homedir as homedir2 } from "os";
|
|
350
|
+
import { join as join3 } from "path";
|
|
351
|
+
var CANONICAL_DIR = join3(homedir2(), ".agents", "skills");
|
|
352
|
+
async function ensureCanonicalDir() {
|
|
353
|
+
await mkdir(CANONICAL_DIR, { recursive: true });
|
|
354
|
+
}
|
|
355
|
+
async function installToCanonical(sourcePath, skillName) {
|
|
356
|
+
await ensureCanonicalDir();
|
|
357
|
+
const targetDir = join3(CANONICAL_DIR, skillName);
|
|
358
|
+
if (existsSync3(targetDir)) {
|
|
359
|
+
await rm(targetDir, { recursive: true });
|
|
360
|
+
}
|
|
361
|
+
await cp(sourcePath, targetDir, { recursive: true });
|
|
362
|
+
return targetDir;
|
|
363
|
+
}
|
|
364
|
+
async function linkToAgent(canonicalPath, provider, skillName, isGlobal, projectDir) {
|
|
365
|
+
const targetSkillsDir = isGlobal ? provider.pathSkills : join3(projectDir ?? process.cwd(), provider.pathProjectSkills);
|
|
366
|
+
if (!targetSkillsDir) {
|
|
367
|
+
return { success: false, error: `Provider ${provider.id} has no skills directory` };
|
|
368
|
+
}
|
|
369
|
+
try {
|
|
370
|
+
await mkdir(targetSkillsDir, { recursive: true });
|
|
371
|
+
const linkPath = join3(targetSkillsDir, skillName);
|
|
372
|
+
if (existsSync3(linkPath)) {
|
|
373
|
+
const stat = lstatSync(linkPath);
|
|
374
|
+
if (stat.isSymbolicLink()) {
|
|
375
|
+
await rm(linkPath);
|
|
376
|
+
} else {
|
|
377
|
+
await rm(linkPath, { recursive: true });
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
const symlinkType = process.platform === "win32" ? "junction" : "dir";
|
|
381
|
+
try {
|
|
382
|
+
await symlink(canonicalPath, linkPath, symlinkType);
|
|
383
|
+
} catch {
|
|
384
|
+
await cp(canonicalPath, linkPath, { recursive: true });
|
|
385
|
+
}
|
|
386
|
+
return { success: true };
|
|
387
|
+
} catch (err) {
|
|
388
|
+
return {
|
|
389
|
+
success: false,
|
|
390
|
+
error: err instanceof Error ? err.message : String(err)
|
|
391
|
+
};
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
async function installSkill(sourcePath, skillName, providers, isGlobal, projectDir) {
|
|
395
|
+
const errors = [];
|
|
396
|
+
const linkedAgents = [];
|
|
397
|
+
const canonicalPath = await installToCanonical(sourcePath, skillName);
|
|
398
|
+
for (const provider of providers) {
|
|
399
|
+
const result = await linkToAgent(canonicalPath, provider, skillName, isGlobal, projectDir);
|
|
400
|
+
if (result.success) {
|
|
401
|
+
linkedAgents.push(provider.id);
|
|
402
|
+
} else if (result.error) {
|
|
403
|
+
errors.push(`${provider.id}: ${result.error}`);
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
return {
|
|
407
|
+
name: skillName,
|
|
408
|
+
canonicalPath,
|
|
409
|
+
linkedAgents,
|
|
410
|
+
errors,
|
|
411
|
+
success: linkedAgents.length > 0
|
|
412
|
+
};
|
|
413
|
+
}
|
|
414
|
+
async function removeSkill(skillName, providers, isGlobal, projectDir) {
|
|
415
|
+
const removed = [];
|
|
416
|
+
const errors = [];
|
|
417
|
+
for (const provider of providers) {
|
|
418
|
+
const skillsDir = isGlobal ? provider.pathSkills : join3(projectDir ?? process.cwd(), provider.pathProjectSkills);
|
|
419
|
+
if (!skillsDir) continue;
|
|
420
|
+
const linkPath = join3(skillsDir, skillName);
|
|
421
|
+
if (existsSync3(linkPath)) {
|
|
422
|
+
try {
|
|
423
|
+
await rm(linkPath, { recursive: true });
|
|
424
|
+
removed.push(provider.id);
|
|
425
|
+
} catch (err) {
|
|
426
|
+
errors.push(`${provider.id}: ${err instanceof Error ? err.message : String(err)}`);
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
const canonicalPath = join3(CANONICAL_DIR, skillName);
|
|
431
|
+
if (existsSync3(canonicalPath)) {
|
|
432
|
+
try {
|
|
433
|
+
await rm(canonicalPath, { recursive: true });
|
|
434
|
+
} catch (err) {
|
|
435
|
+
errors.push(`canonical: ${err instanceof Error ? err.message : String(err)}`);
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
return { removed, errors };
|
|
439
|
+
}
|
|
440
|
+
async function listCanonicalSkills() {
|
|
441
|
+
if (!existsSync3(CANONICAL_DIR)) return [];
|
|
442
|
+
const { readdir: readdir2 } = await import("fs/promises");
|
|
443
|
+
const entries = await readdir2(CANONICAL_DIR, { withFileTypes: true });
|
|
444
|
+
return entries.filter((e) => e.isDirectory() || e.isSymbolicLink()).map((e) => e.name);
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
// src/core/mcp/lock.ts
|
|
448
|
+
import { readFile as readFile2, writeFile as writeFile2, mkdir as mkdir2 } from "fs/promises";
|
|
449
|
+
import { existsSync as existsSync4 } from "fs";
|
|
450
|
+
import { homedir as homedir3 } from "os";
|
|
451
|
+
import { join as join4 } from "path";
|
|
452
|
+
var LOCK_DIR = join4(homedir3(), ".agents");
|
|
453
|
+
var LOCK_FILE = join4(LOCK_DIR, ".caamp-lock.json");
|
|
454
|
+
async function readLockFile() {
|
|
455
|
+
try {
|
|
456
|
+
if (!existsSync4(LOCK_FILE)) {
|
|
457
|
+
return { version: 1, skills: {}, mcpServers: {} };
|
|
458
|
+
}
|
|
459
|
+
const content = await readFile2(LOCK_FILE, "utf-8");
|
|
460
|
+
return JSON.parse(content);
|
|
461
|
+
} catch {
|
|
462
|
+
return { version: 1, skills: {}, mcpServers: {} };
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
async function writeLockFile(lock) {
|
|
466
|
+
await mkdir2(LOCK_DIR, { recursive: true });
|
|
467
|
+
await writeFile2(LOCK_FILE, JSON.stringify(lock, null, 2) + "\n", "utf-8");
|
|
468
|
+
}
|
|
469
|
+
async function recordMcpInstall(serverName, source, sourceType, agents, isGlobal) {
|
|
470
|
+
const lock = await readLockFile();
|
|
471
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
472
|
+
const existing = lock.mcpServers[serverName];
|
|
473
|
+
lock.mcpServers[serverName] = {
|
|
474
|
+
name: serverName,
|
|
475
|
+
scopedName: serverName,
|
|
476
|
+
source,
|
|
477
|
+
sourceType,
|
|
478
|
+
installedAt: existing?.installedAt ?? now,
|
|
479
|
+
updatedAt: now,
|
|
480
|
+
agents: [.../* @__PURE__ */ new Set([...existing?.agents ?? [], ...agents])],
|
|
481
|
+
canonicalPath: "",
|
|
482
|
+
isGlobal
|
|
483
|
+
};
|
|
484
|
+
await writeLockFile(lock);
|
|
485
|
+
}
|
|
486
|
+
async function removeMcpFromLock(serverName) {
|
|
487
|
+
const lock = await readLockFile();
|
|
488
|
+
if (!(serverName in lock.mcpServers)) return false;
|
|
489
|
+
delete lock.mcpServers[serverName];
|
|
490
|
+
await writeLockFile(lock);
|
|
491
|
+
return true;
|
|
492
|
+
}
|
|
493
|
+
async function getTrackedMcpServers() {
|
|
494
|
+
const lock = await readLockFile();
|
|
495
|
+
return lock.mcpServers;
|
|
496
|
+
}
|
|
497
|
+
async function saveLastSelectedAgents(agents) {
|
|
498
|
+
const lock = await readLockFile();
|
|
499
|
+
lock.lastSelectedAgents = agents;
|
|
500
|
+
await writeLockFile(lock);
|
|
501
|
+
}
|
|
502
|
+
async function getLastSelectedAgents() {
|
|
503
|
+
const lock = await readLockFile();
|
|
504
|
+
return lock.lastSelectedAgents;
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
// src/core/skills/lock.ts
|
|
508
|
+
import { writeFile as writeFile3, mkdir as mkdir3 } from "fs/promises";
|
|
509
|
+
import { homedir as homedir4 } from "os";
|
|
510
|
+
import { join as join5 } from "path";
|
|
511
|
+
var LOCK_DIR2 = join5(homedir4(), ".agents");
|
|
512
|
+
var LOCK_FILE2 = join5(LOCK_DIR2, ".caamp-lock.json");
|
|
513
|
+
async function writeLockFile2(lock) {
|
|
514
|
+
await mkdir3(LOCK_DIR2, { recursive: true });
|
|
515
|
+
await writeFile3(LOCK_FILE2, JSON.stringify(lock, null, 2) + "\n", "utf-8");
|
|
516
|
+
}
|
|
517
|
+
async function recordSkillInstall(skillName, scopedName, source, sourceType, agents, canonicalPath, isGlobal, projectDir, version) {
|
|
518
|
+
const lock = await readLockFile();
|
|
519
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
520
|
+
const existing = lock.skills[skillName];
|
|
521
|
+
lock.skills[skillName] = {
|
|
522
|
+
name: skillName,
|
|
523
|
+
scopedName,
|
|
524
|
+
source,
|
|
525
|
+
sourceType,
|
|
526
|
+
version,
|
|
527
|
+
installedAt: existing?.installedAt ?? now,
|
|
528
|
+
updatedAt: now,
|
|
529
|
+
agents: [.../* @__PURE__ */ new Set([...existing?.agents ?? [], ...agents])],
|
|
530
|
+
canonicalPath,
|
|
531
|
+
isGlobal,
|
|
532
|
+
projectDir
|
|
533
|
+
};
|
|
534
|
+
await writeLockFile2(lock);
|
|
535
|
+
}
|
|
536
|
+
async function removeSkillFromLock(skillName) {
|
|
537
|
+
const lock = await readLockFile();
|
|
538
|
+
if (!(skillName in lock.skills)) return false;
|
|
539
|
+
delete lock.skills[skillName];
|
|
540
|
+
await writeLockFile2(lock);
|
|
541
|
+
return true;
|
|
542
|
+
}
|
|
543
|
+
async function getTrackedSkills() {
|
|
544
|
+
const lock = await readLockFile();
|
|
545
|
+
return lock.skills;
|
|
546
|
+
}
|
|
547
|
+
async function checkSkillUpdate(skillName) {
|
|
548
|
+
const lock = await readLockFile();
|
|
549
|
+
const entry = lock.skills[skillName];
|
|
550
|
+
if (!entry) {
|
|
551
|
+
return { hasUpdate: false };
|
|
552
|
+
}
|
|
553
|
+
return {
|
|
554
|
+
hasUpdate: false,
|
|
555
|
+
currentVersion: entry.version
|
|
556
|
+
};
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
// src/core/marketplace/skillsmp.ts
|
|
560
|
+
var API_BASE = "https://www.agentskills.in/api/skills";
|
|
561
|
+
function toResult(skill) {
|
|
562
|
+
return {
|
|
563
|
+
name: skill.name,
|
|
564
|
+
scopedName: skill.scopedName,
|
|
565
|
+
description: skill.description,
|
|
566
|
+
author: skill.author,
|
|
567
|
+
stars: skill.stars,
|
|
568
|
+
githubUrl: skill.githubUrl,
|
|
569
|
+
repoFullName: skill.repoFullName,
|
|
570
|
+
path: skill.path,
|
|
571
|
+
source: "agentskills.in"
|
|
572
|
+
};
|
|
573
|
+
}
|
|
574
|
+
var SkillsMPAdapter = class {
|
|
575
|
+
name = "agentskills.in";
|
|
576
|
+
async search(query, limit = 20) {
|
|
577
|
+
const params = new URLSearchParams({
|
|
578
|
+
search: query,
|
|
579
|
+
limit: String(limit),
|
|
580
|
+
sortBy: "stars"
|
|
581
|
+
});
|
|
582
|
+
try {
|
|
583
|
+
const response = await fetch(`${API_BASE}?${params}`);
|
|
584
|
+
if (!response.ok) return [];
|
|
585
|
+
const data = await response.json();
|
|
586
|
+
return data.skills.map(toResult);
|
|
587
|
+
} catch {
|
|
588
|
+
return [];
|
|
589
|
+
}
|
|
590
|
+
}
|
|
591
|
+
async getSkill(scopedName) {
|
|
592
|
+
const params = new URLSearchParams({
|
|
593
|
+
search: scopedName,
|
|
594
|
+
limit: "1"
|
|
595
|
+
});
|
|
596
|
+
try {
|
|
597
|
+
const response = await fetch(`${API_BASE}?${params}`);
|
|
598
|
+
if (!response.ok) return null;
|
|
599
|
+
const data = await response.json();
|
|
600
|
+
const match = data.skills.find(
|
|
601
|
+
(s) => s.scopedName === scopedName || `@${s.author}/${s.name}` === scopedName
|
|
602
|
+
);
|
|
603
|
+
return match ? toResult(match) : null;
|
|
604
|
+
} catch {
|
|
605
|
+
return null;
|
|
606
|
+
}
|
|
607
|
+
}
|
|
608
|
+
};
|
|
609
|
+
|
|
610
|
+
// src/core/marketplace/skillssh.ts
|
|
611
|
+
var API_BASE2 = "https://skills.sh/api";
|
|
612
|
+
function toResult2(skill) {
|
|
613
|
+
return {
|
|
614
|
+
name: skill.name,
|
|
615
|
+
scopedName: `@${skill.author}/${skill.name}`,
|
|
616
|
+
description: skill.description,
|
|
617
|
+
author: skill.author,
|
|
618
|
+
stars: skill.stars ?? 0,
|
|
619
|
+
githubUrl: skill.url,
|
|
620
|
+
repoFullName: skill.repo,
|
|
621
|
+
path: "",
|
|
622
|
+
source: "skills.sh"
|
|
623
|
+
};
|
|
624
|
+
}
|
|
625
|
+
var SkillsShAdapter = class {
|
|
626
|
+
name = "skills.sh";
|
|
627
|
+
async search(query, limit = 20) {
|
|
628
|
+
try {
|
|
629
|
+
const params = new URLSearchParams({
|
|
630
|
+
q: query,
|
|
631
|
+
limit: String(limit)
|
|
632
|
+
});
|
|
633
|
+
const response = await fetch(`${API_BASE2}/search?${params}`);
|
|
634
|
+
if (!response.ok) return [];
|
|
635
|
+
const data = await response.json();
|
|
636
|
+
return data.results.map(toResult2);
|
|
637
|
+
} catch {
|
|
638
|
+
return [];
|
|
639
|
+
}
|
|
640
|
+
}
|
|
641
|
+
async getSkill(scopedName) {
|
|
642
|
+
const results = await this.search(scopedName, 5);
|
|
643
|
+
return results.find((r) => r.scopedName === scopedName) ?? null;
|
|
644
|
+
}
|
|
645
|
+
};
|
|
646
|
+
|
|
647
|
+
// src/core/marketplace/client.ts
|
|
648
|
+
var MarketplaceClient = class {
|
|
649
|
+
adapters;
|
|
650
|
+
constructor(adapters) {
|
|
651
|
+
this.adapters = adapters ?? [
|
|
652
|
+
new SkillsMPAdapter(),
|
|
653
|
+
new SkillsShAdapter()
|
|
654
|
+
];
|
|
655
|
+
}
|
|
656
|
+
/** Search all marketplaces and deduplicate results */
|
|
657
|
+
async search(query, limit = 20) {
|
|
658
|
+
const promises = this.adapters.map(
|
|
659
|
+
(adapter) => adapter.search(query, limit).catch(() => [])
|
|
660
|
+
);
|
|
661
|
+
const allResults = await Promise.all(promises);
|
|
662
|
+
const flat = allResults.flat();
|
|
663
|
+
const seen = /* @__PURE__ */ new Map();
|
|
664
|
+
for (const result of flat) {
|
|
665
|
+
const existing = seen.get(result.scopedName);
|
|
666
|
+
if (!existing || result.stars > existing.stars) {
|
|
667
|
+
seen.set(result.scopedName, result);
|
|
668
|
+
}
|
|
669
|
+
}
|
|
670
|
+
const deduplicated = Array.from(seen.values());
|
|
671
|
+
deduplicated.sort((a, b) => b.stars - a.stars);
|
|
672
|
+
return deduplicated.slice(0, limit);
|
|
673
|
+
}
|
|
674
|
+
/** Get a specific skill by scoped name */
|
|
675
|
+
async getSkill(scopedName) {
|
|
676
|
+
for (const adapter of this.adapters) {
|
|
677
|
+
const result = await adapter.getSkill(scopedName).catch(() => null);
|
|
678
|
+
if (result) return result;
|
|
679
|
+
}
|
|
680
|
+
return null;
|
|
681
|
+
}
|
|
682
|
+
};
|
|
683
|
+
|
|
684
|
+
// src/core/skills/discovery.ts
|
|
685
|
+
import { readFile as readFile3, readdir } from "fs/promises";
|
|
686
|
+
import { existsSync as existsSync5 } from "fs";
|
|
687
|
+
import { join as join6 } from "path";
|
|
688
|
+
import matter from "gray-matter";
|
|
689
|
+
async function parseSkillFile(filePath) {
|
|
690
|
+
try {
|
|
691
|
+
const content = await readFile3(filePath, "utf-8");
|
|
692
|
+
const { data } = matter(content);
|
|
693
|
+
if (!data["name"] || !data["description"]) {
|
|
694
|
+
return null;
|
|
695
|
+
}
|
|
696
|
+
const allowedTools = data["allowed-tools"] ?? data["allowedTools"];
|
|
697
|
+
return {
|
|
698
|
+
name: String(data["name"]),
|
|
699
|
+
description: String(data["description"]),
|
|
700
|
+
license: data["license"] ? String(data["license"]) : void 0,
|
|
701
|
+
compatibility: data["compatibility"] ? String(data["compatibility"]) : void 0,
|
|
702
|
+
metadata: data["metadata"],
|
|
703
|
+
allowedTools: typeof allowedTools === "string" ? allowedTools.split(/\s+/) : Array.isArray(allowedTools) ? allowedTools.map(String) : void 0,
|
|
704
|
+
version: data["version"] ? String(data["version"]) : void 0
|
|
705
|
+
};
|
|
706
|
+
} catch {
|
|
707
|
+
return null;
|
|
708
|
+
}
|
|
709
|
+
}
|
|
710
|
+
async function discoverSkill(skillDir) {
|
|
711
|
+
const skillFile = join6(skillDir, "SKILL.md");
|
|
712
|
+
if (!existsSync5(skillFile)) return null;
|
|
713
|
+
const metadata = await parseSkillFile(skillFile);
|
|
714
|
+
if (!metadata) return null;
|
|
715
|
+
return {
|
|
716
|
+
name: metadata.name,
|
|
717
|
+
scopedName: metadata.name,
|
|
718
|
+
path: skillDir,
|
|
719
|
+
metadata
|
|
720
|
+
};
|
|
721
|
+
}
|
|
722
|
+
async function discoverSkills(rootDir) {
|
|
723
|
+
if (!existsSync5(rootDir)) return [];
|
|
724
|
+
const entries = await readdir(rootDir, { withFileTypes: true });
|
|
725
|
+
const skills = [];
|
|
726
|
+
for (const entry of entries) {
|
|
727
|
+
if (!entry.isDirectory() && !entry.isSymbolicLink()) continue;
|
|
728
|
+
const skillDir = join6(rootDir, entry.name);
|
|
729
|
+
const skill = await discoverSkill(skillDir);
|
|
730
|
+
if (skill) {
|
|
731
|
+
skills.push(skill);
|
|
732
|
+
}
|
|
733
|
+
}
|
|
734
|
+
return skills;
|
|
735
|
+
}
|
|
736
|
+
async function discoverSkillsMulti(dirs) {
|
|
737
|
+
const all = [];
|
|
738
|
+
const seen = /* @__PURE__ */ new Set();
|
|
739
|
+
for (const dir of dirs) {
|
|
740
|
+
const skills = await discoverSkills(dir);
|
|
741
|
+
for (const skill of skills) {
|
|
742
|
+
if (!seen.has(skill.name)) {
|
|
743
|
+
seen.add(skill.name);
|
|
744
|
+
all.push(skill);
|
|
745
|
+
}
|
|
746
|
+
}
|
|
747
|
+
}
|
|
748
|
+
return all;
|
|
749
|
+
}
|
|
750
|
+
|
|
751
|
+
// src/core/skills/audit/scanner.ts
|
|
752
|
+
import { readFile as readFile4 } from "fs/promises";
|
|
753
|
+
import { existsSync as existsSync6 } from "fs";
|
|
754
|
+
|
|
755
|
+
// src/core/skills/audit/rules.ts
|
|
756
|
+
function rule(id, name, description, severity, category, pattern) {
|
|
757
|
+
return { id, name, description, severity, category, pattern };
|
|
758
|
+
}
|
|
759
|
+
var AUDIT_RULES = [
|
|
760
|
+
// ── Prompt Injection ────────────────────────────────────────
|
|
761
|
+
rule(
|
|
762
|
+
"PI001",
|
|
763
|
+
"System prompt override",
|
|
764
|
+
"Attempts to override system instructions",
|
|
765
|
+
"critical",
|
|
766
|
+
"prompt-injection",
|
|
767
|
+
/(?:ignore|forget|disregard)\s+(?:all\s+)?(?:previous|prior|above|system)\s+(?:instructions|prompts|rules)/i
|
|
768
|
+
),
|
|
769
|
+
rule(
|
|
770
|
+
"PI002",
|
|
771
|
+
"Role manipulation",
|
|
772
|
+
"Attempts to assume a different role",
|
|
773
|
+
"critical",
|
|
774
|
+
"prompt-injection",
|
|
775
|
+
/(?:you\s+are\s+now|act\s+as|pretend\s+(?:to\s+be|you're)|your\s+new\s+role\s+is)/i
|
|
776
|
+
),
|
|
777
|
+
rule(
|
|
778
|
+
"PI003",
|
|
779
|
+
"Jailbreak attempt",
|
|
780
|
+
"Common jailbreak patterns",
|
|
781
|
+
"critical",
|
|
782
|
+
"prompt-injection",
|
|
783
|
+
/(?:DAN|Do\s+Anything\s+Now|developer\s+mode|god\s+mode|unrestricted\s+mode)/i
|
|
784
|
+
),
|
|
785
|
+
rule(
|
|
786
|
+
"PI004",
|
|
787
|
+
"Instruction override",
|
|
788
|
+
"Direct instruction override attempt",
|
|
789
|
+
"high",
|
|
790
|
+
"prompt-injection",
|
|
791
|
+
/(?:new\s+instructions?:|updated?\s+instructions?:|override\s+instructions?:)/i
|
|
792
|
+
),
|
|
793
|
+
rule(
|
|
794
|
+
"PI005",
|
|
795
|
+
"Hidden instructions",
|
|
796
|
+
"Instructions hidden in comments or whitespace",
|
|
797
|
+
"high",
|
|
798
|
+
"prompt-injection",
|
|
799
|
+
/<!--[\s\S]*?(?:execute|run|ignore|override)[\s\S]*?-->/i
|
|
800
|
+
),
|
|
801
|
+
rule(
|
|
802
|
+
"PI006",
|
|
803
|
+
"Encoding bypass",
|
|
804
|
+
"Base64 or encoded content",
|
|
805
|
+
"medium",
|
|
806
|
+
"prompt-injection",
|
|
807
|
+
/(?:base64|atob|btoa|decodeURI|unescape)\s*\(/i
|
|
808
|
+
),
|
|
809
|
+
rule(
|
|
810
|
+
"PI007",
|
|
811
|
+
"Context manipulation",
|
|
812
|
+
"Fake conversation context",
|
|
813
|
+
"high",
|
|
814
|
+
"prompt-injection",
|
|
815
|
+
/(?:Human:|Assistant:|User:|System:)\s*(?:ignore|execute|run)/i
|
|
816
|
+
),
|
|
817
|
+
rule(
|
|
818
|
+
"PI008",
|
|
819
|
+
"Token smuggling",
|
|
820
|
+
"Invisible characters or zero-width spaces",
|
|
821
|
+
"medium",
|
|
822
|
+
"prompt-injection",
|
|
823
|
+
/[\u200B\u200C\u200D\u2060\uFEFF]/
|
|
824
|
+
),
|
|
825
|
+
// ── Command Injection ───────────────────────────────────────
|
|
826
|
+
rule(
|
|
827
|
+
"CI001",
|
|
828
|
+
"Destructive command",
|
|
829
|
+
"File deletion or system modification",
|
|
830
|
+
"critical",
|
|
831
|
+
"command-injection",
|
|
832
|
+
/(?:rm\s+-rf|rmdir\s+\/s|del\s+\/f|format\s+[a-z]:|mkfs|dd\s+if=)/i
|
|
833
|
+
),
|
|
834
|
+
rule(
|
|
835
|
+
"CI002",
|
|
836
|
+
"Remote code execution",
|
|
837
|
+
"Downloading and executing remote code",
|
|
838
|
+
"critical",
|
|
839
|
+
"command-injection",
|
|
840
|
+
/(?:curl|wget|fetch)\s+.*\|\s*(?:sh|bash|zsh|python|node|eval)/i
|
|
841
|
+
),
|
|
842
|
+
rule(
|
|
843
|
+
"CI003",
|
|
844
|
+
"Eval usage",
|
|
845
|
+
"Dynamic code execution",
|
|
846
|
+
"high",
|
|
847
|
+
"command-injection",
|
|
848
|
+
/\beval\s*\(/
|
|
849
|
+
),
|
|
850
|
+
rule(
|
|
851
|
+
"CI004",
|
|
852
|
+
"Shell spawn",
|
|
853
|
+
"Spawning shell processes",
|
|
854
|
+
"high",
|
|
855
|
+
"command-injection",
|
|
856
|
+
/(?:exec|spawn|system|popen)\s*\(\s*['"`]/
|
|
857
|
+
),
|
|
858
|
+
rule(
|
|
859
|
+
"CI005",
|
|
860
|
+
"Sudo escalation",
|
|
861
|
+
"Privilege escalation via sudo",
|
|
862
|
+
"critical",
|
|
863
|
+
"command-injection",
|
|
864
|
+
/sudo\s+(?:rm|chmod|chown|mv|cp|dd|mkfs|format)/i
|
|
865
|
+
),
|
|
866
|
+
rule(
|
|
867
|
+
"CI006",
|
|
868
|
+
"Environment manipulation",
|
|
869
|
+
"Modifying PATH or critical env vars",
|
|
870
|
+
"high",
|
|
871
|
+
"command-injection",
|
|
872
|
+
/(?:export\s+PATH|setx?\s+PATH|PATH=.*:)/i
|
|
873
|
+
),
|
|
874
|
+
rule(
|
|
875
|
+
"CI007",
|
|
876
|
+
"Cron/scheduled task",
|
|
877
|
+
"Installing scheduled tasks",
|
|
878
|
+
"high",
|
|
879
|
+
"command-injection",
|
|
880
|
+
/(?:crontab|at\s+\d|schtasks|launchctl\s+load)/i
|
|
881
|
+
),
|
|
882
|
+
rule(
|
|
883
|
+
"CI008",
|
|
884
|
+
"Network listener",
|
|
885
|
+
"Starting network services",
|
|
886
|
+
"high",
|
|
887
|
+
"command-injection",
|
|
888
|
+
/(?:nc\s+-l|ncat\s+-l|socat\s+|python.*SimpleHTTPServer|php\s+-S)/i
|
|
889
|
+
),
|
|
890
|
+
// ── Data Exfiltration ───────────────────────────────────────
|
|
891
|
+
rule(
|
|
892
|
+
"DE001",
|
|
893
|
+
"Credential access",
|
|
894
|
+
"Reading credential files",
|
|
895
|
+
"critical",
|
|
896
|
+
"data-exfiltration",
|
|
897
|
+
/(?:\.env|\.aws\/credentials|\.ssh\/|\.gnupg|\.netrc|credentials\.json|token\.json)/i
|
|
898
|
+
),
|
|
899
|
+
rule(
|
|
900
|
+
"DE002",
|
|
901
|
+
"API key extraction",
|
|
902
|
+
"Patterns matching API key theft",
|
|
903
|
+
"critical",
|
|
904
|
+
"data-exfiltration",
|
|
905
|
+
/(?:API[_-]?KEY|SECRET[_-]?KEY|ACCESS[_-]?TOKEN|PRIVATE[_-]?KEY)\s*[=:]/i
|
|
906
|
+
),
|
|
907
|
+
rule(
|
|
908
|
+
"DE003",
|
|
909
|
+
"Data upload",
|
|
910
|
+
"Uploading data to external services",
|
|
911
|
+
"high",
|
|
912
|
+
"data-exfiltration",
|
|
913
|
+
/(?:curl|wget|fetch).*(?:POST|PUT|PATCH).*(?:pastebin|gist|transfer\.sh|requestbin|webhook)/i
|
|
914
|
+
),
|
|
915
|
+
rule(
|
|
916
|
+
"DE004",
|
|
917
|
+
"Browser data theft",
|
|
918
|
+
"Accessing browser profiles or cookies",
|
|
919
|
+
"critical",
|
|
920
|
+
"data-exfiltration",
|
|
921
|
+
/(?:\.mozilla|\.chrome|\.config\/google-chrome|Cookies|Login\s+Data|Local\s+State)/i
|
|
922
|
+
),
|
|
923
|
+
rule(
|
|
924
|
+
"DE005",
|
|
925
|
+
"Git credential theft",
|
|
926
|
+
"Accessing git credentials",
|
|
927
|
+
"high",
|
|
928
|
+
"data-exfiltration",
|
|
929
|
+
/(?:git\s+credential|\.git-credentials|\.gitconfig\s+credential)/i
|
|
930
|
+
),
|
|
931
|
+
rule(
|
|
932
|
+
"DE006",
|
|
933
|
+
"Keychain access",
|
|
934
|
+
"Accessing system keychain",
|
|
935
|
+
"critical",
|
|
936
|
+
"data-exfiltration",
|
|
937
|
+
/(?:security\s+find-generic-password|security\s+find-internet-password|keyring\s+get)/i
|
|
938
|
+
),
|
|
939
|
+
// ── Privilege Escalation ────────────────────────────────────
|
|
940
|
+
rule(
|
|
941
|
+
"PE001",
|
|
942
|
+
"Chmod dangerous",
|
|
943
|
+
"Setting dangerous file permissions",
|
|
944
|
+
"high",
|
|
945
|
+
"privilege-escalation",
|
|
946
|
+
/chmod\s+(?:777|666|a\+[rwx]|o\+[rwx])/i
|
|
947
|
+
),
|
|
948
|
+
rule(
|
|
949
|
+
"PE002",
|
|
950
|
+
"SUID/SGID",
|
|
951
|
+
"Setting SUID or SGID bits",
|
|
952
|
+
"critical",
|
|
953
|
+
"privilege-escalation",
|
|
954
|
+
/chmod\s+[ug]\+s/i
|
|
955
|
+
),
|
|
956
|
+
rule(
|
|
957
|
+
"PE003",
|
|
958
|
+
"Docker escape",
|
|
959
|
+
"Container escape patterns",
|
|
960
|
+
"critical",
|
|
961
|
+
"privilege-escalation",
|
|
962
|
+
/(?:--privileged|--cap-add\s+SYS_ADMIN|--pid=host|nsenter)/i
|
|
963
|
+
),
|
|
964
|
+
rule(
|
|
965
|
+
"PE004",
|
|
966
|
+
"Kernel module",
|
|
967
|
+
"Loading kernel modules",
|
|
968
|
+
"critical",
|
|
969
|
+
"privilege-escalation",
|
|
970
|
+
/(?:insmod|modprobe|rmmod)\s+/i
|
|
971
|
+
),
|
|
972
|
+
// ── Filesystem Abuse ────────────────────────────────────────
|
|
973
|
+
rule(
|
|
974
|
+
"FS001",
|
|
975
|
+
"System directory write",
|
|
976
|
+
"Writing to system directories",
|
|
977
|
+
"critical",
|
|
978
|
+
"filesystem",
|
|
979
|
+
/(?:\/etc\/|\/usr\/|\/bin\/|\/sbin\/|C:\\Windows\\|C:\\Program Files)/i
|
|
980
|
+
),
|
|
981
|
+
rule(
|
|
982
|
+
"FS002",
|
|
983
|
+
"Hidden file creation",
|
|
984
|
+
"Creating hidden files",
|
|
985
|
+
"medium",
|
|
986
|
+
"filesystem",
|
|
987
|
+
/(?:touch|mkdir|cp|mv)\s+\.[a-zA-Z]/
|
|
988
|
+
),
|
|
989
|
+
rule(
|
|
990
|
+
"FS003",
|
|
991
|
+
"Symlink attack",
|
|
992
|
+
"Creating symlinks to sensitive files",
|
|
993
|
+
"high",
|
|
994
|
+
"filesystem",
|
|
995
|
+
/ln\s+-s.*(?:\/etc\/passwd|\/etc\/shadow|\.ssh|\.env)/i
|
|
996
|
+
),
|
|
997
|
+
rule(
|
|
998
|
+
"FS004",
|
|
999
|
+
"Mass file operation",
|
|
1000
|
+
"Recursive operations on broad paths",
|
|
1001
|
+
"medium",
|
|
1002
|
+
"filesystem",
|
|
1003
|
+
/(?:find|xargs|rm|chmod|chown)\s+.*(?:\/\s|\/\*|-R\s+\/)/i
|
|
1004
|
+
),
|
|
1005
|
+
// ── Network Abuse ──────────────────────────────────────────
|
|
1006
|
+
rule(
|
|
1007
|
+
"NA001",
|
|
1008
|
+
"DNS exfiltration",
|
|
1009
|
+
"Data exfiltration via DNS",
|
|
1010
|
+
"high",
|
|
1011
|
+
"network",
|
|
1012
|
+
/(?:dig|nslookup|host)\s+.*\$\{?[A-Z_]+/i
|
|
1013
|
+
),
|
|
1014
|
+
rule(
|
|
1015
|
+
"NA002",
|
|
1016
|
+
"Reverse shell",
|
|
1017
|
+
"Reverse shell patterns",
|
|
1018
|
+
"critical",
|
|
1019
|
+
"network",
|
|
1020
|
+
/(?:bash\s+-i|\/dev\/tcp\/|mkfifo|nc\s+.*-e)/i
|
|
1021
|
+
),
|
|
1022
|
+
rule(
|
|
1023
|
+
"NA003",
|
|
1024
|
+
"Port scanning",
|
|
1025
|
+
"Network scanning",
|
|
1026
|
+
"medium",
|
|
1027
|
+
"network",
|
|
1028
|
+
/(?:nmap|masscan|zmap)\s+/i
|
|
1029
|
+
),
|
|
1030
|
+
rule(
|
|
1031
|
+
"NA004",
|
|
1032
|
+
"Proxy/tunnel",
|
|
1033
|
+
"Creating network tunnels",
|
|
1034
|
+
"high",
|
|
1035
|
+
"network",
|
|
1036
|
+
/(?:ssh\s+-[DRLW]|ngrok|chisel|bore)/i
|
|
1037
|
+
),
|
|
1038
|
+
// ── Obfuscation ─────────────────────────────────────────────
|
|
1039
|
+
rule(
|
|
1040
|
+
"OB001",
|
|
1041
|
+
"Hex encoding",
|
|
1042
|
+
"Hex-encoded commands",
|
|
1043
|
+
"medium",
|
|
1044
|
+
"obfuscation",
|
|
1045
|
+
/\\x[0-9a-fA-F]{2}(?:\\x[0-9a-fA-F]{2}){3,}/
|
|
1046
|
+
),
|
|
1047
|
+
rule(
|
|
1048
|
+
"OB002",
|
|
1049
|
+
"String concatenation",
|
|
1050
|
+
"Building commands via concatenation",
|
|
1051
|
+
"medium",
|
|
1052
|
+
"obfuscation",
|
|
1053
|
+
/(?:\$\{[A-Z]+\}\$\{[A-Z]+\}|['"][a-z]+['"]\.['"][a-z]+['"])/i
|
|
1054
|
+
),
|
|
1055
|
+
rule(
|
|
1056
|
+
"OB003",
|
|
1057
|
+
"Unicode escape",
|
|
1058
|
+
"Unicode-escaped commands",
|
|
1059
|
+
"medium",
|
|
1060
|
+
"obfuscation",
|
|
1061
|
+
/\\u[0-9a-fA-F]{4}(?:\\u[0-9a-fA-F]{4}){3,}/
|
|
1062
|
+
),
|
|
1063
|
+
// ── Supply Chain ────────────────────────────────────────────
|
|
1064
|
+
rule(
|
|
1065
|
+
"SC001",
|
|
1066
|
+
"Package install",
|
|
1067
|
+
"Installing packages at runtime",
|
|
1068
|
+
"medium",
|
|
1069
|
+
"supply-chain",
|
|
1070
|
+
/(?:npm\s+install|pip\s+install|gem\s+install|cargo\s+install)\s+(?!-)/
|
|
1071
|
+
),
|
|
1072
|
+
rule(
|
|
1073
|
+
"SC002",
|
|
1074
|
+
"Typosquatting patterns",
|
|
1075
|
+
"Packages with suspicious names",
|
|
1076
|
+
"low",
|
|
1077
|
+
"supply-chain",
|
|
1078
|
+
/(?:npm\s+install|pip\s+install)\s+(?:reqeusts|requets|reqests|lodahs|lodashe)/i
|
|
1079
|
+
),
|
|
1080
|
+
rule(
|
|
1081
|
+
"SC003",
|
|
1082
|
+
"Postinstall script",
|
|
1083
|
+
"npm lifecycle scripts",
|
|
1084
|
+
"medium",
|
|
1085
|
+
"supply-chain",
|
|
1086
|
+
/(?:preinstall|postinstall|preuninstall|postuninstall)\s*[":]/i
|
|
1087
|
+
),
|
|
1088
|
+
rule(
|
|
1089
|
+
"SC004",
|
|
1090
|
+
"Registry override",
|
|
1091
|
+
"Changing package registry",
|
|
1092
|
+
"high",
|
|
1093
|
+
"supply-chain",
|
|
1094
|
+
/(?:registry\s*=|--registry\s+)(?!https:\/\/registry\.npmjs\.org)/i
|
|
1095
|
+
),
|
|
1096
|
+
// ── Information Disclosure ──────────────────────────────────
|
|
1097
|
+
rule(
|
|
1098
|
+
"ID001",
|
|
1099
|
+
"Process listing",
|
|
1100
|
+
"Listing running processes",
|
|
1101
|
+
"low",
|
|
1102
|
+
"info-disclosure",
|
|
1103
|
+
/(?:ps\s+aux|top\s+-b|tasklist)/i
|
|
1104
|
+
),
|
|
1105
|
+
rule(
|
|
1106
|
+
"ID002",
|
|
1107
|
+
"System information",
|
|
1108
|
+
"Gathering system information",
|
|
1109
|
+
"low",
|
|
1110
|
+
"info-disclosure",
|
|
1111
|
+
/(?:uname\s+-a|systeminfo|hostnamectl)/i
|
|
1112
|
+
),
|
|
1113
|
+
rule(
|
|
1114
|
+
"ID003",
|
|
1115
|
+
"Network enumeration",
|
|
1116
|
+
"Listing network configuration",
|
|
1117
|
+
"low",
|
|
1118
|
+
"info-disclosure",
|
|
1119
|
+
/(?:ifconfig|ip\s+addr|ipconfig|netstat\s+-[at])/i
|
|
1120
|
+
)
|
|
1121
|
+
];
|
|
1122
|
+
|
|
1123
|
+
// src/core/skills/audit/scanner.ts
|
|
1124
|
+
var SEVERITY_WEIGHTS = {
|
|
1125
|
+
critical: 25,
|
|
1126
|
+
high: 15,
|
|
1127
|
+
medium: 8,
|
|
1128
|
+
low: 3,
|
|
1129
|
+
info: 0
|
|
1130
|
+
};
|
|
1131
|
+
async function scanFile(filePath, rules) {
|
|
1132
|
+
if (!existsSync6(filePath)) {
|
|
1133
|
+
return { file: filePath, findings: [], score: 100, passed: true };
|
|
1134
|
+
}
|
|
1135
|
+
const content = await readFile4(filePath, "utf-8");
|
|
1136
|
+
const lines = content.split("\n");
|
|
1137
|
+
const activeRules = rules ?? AUDIT_RULES;
|
|
1138
|
+
const findings = [];
|
|
1139
|
+
for (const rule2 of activeRules) {
|
|
1140
|
+
for (let i = 0; i < lines.length; i++) {
|
|
1141
|
+
const line = lines[i];
|
|
1142
|
+
const match = line.match(rule2.pattern);
|
|
1143
|
+
if (match) {
|
|
1144
|
+
findings.push({
|
|
1145
|
+
rule: rule2,
|
|
1146
|
+
line: i + 1,
|
|
1147
|
+
column: (match.index ?? 0) + 1,
|
|
1148
|
+
match: match[0],
|
|
1149
|
+
context: line.trim()
|
|
1150
|
+
});
|
|
1151
|
+
}
|
|
1152
|
+
}
|
|
1153
|
+
}
|
|
1154
|
+
const totalPenalty = findings.reduce(
|
|
1155
|
+
(sum, f) => sum + (SEVERITY_WEIGHTS[f.rule.severity] ?? 0),
|
|
1156
|
+
0
|
|
1157
|
+
);
|
|
1158
|
+
const score = Math.max(0, 100 - totalPenalty);
|
|
1159
|
+
const passed = !findings.some((f) => f.rule.severity === "critical" || f.rule.severity === "high");
|
|
1160
|
+
return { file: filePath, findings, score, passed };
|
|
1161
|
+
}
|
|
1162
|
+
async function scanDirectory(dirPath) {
|
|
1163
|
+
const { readdir: readdir2 } = await import("fs/promises");
|
|
1164
|
+
const { join: join9 } = await import("path");
|
|
1165
|
+
if (!existsSync6(dirPath)) return [];
|
|
1166
|
+
const entries = await readdir2(dirPath, { withFileTypes: true });
|
|
1167
|
+
const results = [];
|
|
1168
|
+
for (const entry of entries) {
|
|
1169
|
+
if (entry.isDirectory() || entry.isSymbolicLink()) {
|
|
1170
|
+
const skillFile = join9(dirPath, entry.name, "SKILL.md");
|
|
1171
|
+
if (existsSync6(skillFile)) {
|
|
1172
|
+
results.push(await scanFile(skillFile));
|
|
1173
|
+
}
|
|
1174
|
+
}
|
|
1175
|
+
}
|
|
1176
|
+
return results;
|
|
1177
|
+
}
|
|
1178
|
+
function toSarif(results) {
|
|
1179
|
+
return {
|
|
1180
|
+
$schema: "https://raw.githubusercontent.com/oasis-tcs/sarif-spec/main/sarif-2.1/schema/sarif-schema-2.1.0.json",
|
|
1181
|
+
version: "2.1.0",
|
|
1182
|
+
runs: [
|
|
1183
|
+
{
|
|
1184
|
+
tool: {
|
|
1185
|
+
driver: {
|
|
1186
|
+
name: "caamp-audit",
|
|
1187
|
+
version: "0.1.0",
|
|
1188
|
+
rules: AUDIT_RULES.map((r) => ({
|
|
1189
|
+
id: r.id,
|
|
1190
|
+
name: r.name,
|
|
1191
|
+
shortDescription: { text: r.description },
|
|
1192
|
+
defaultConfiguration: {
|
|
1193
|
+
level: r.severity === "critical" || r.severity === "high" ? "error" : "warning"
|
|
1194
|
+
},
|
|
1195
|
+
properties: { category: r.category }
|
|
1196
|
+
}))
|
|
1197
|
+
}
|
|
1198
|
+
},
|
|
1199
|
+
results: results.flatMap(
|
|
1200
|
+
(result) => result.findings.map((f) => ({
|
|
1201
|
+
ruleId: f.rule.id,
|
|
1202
|
+
level: f.rule.severity === "critical" || f.rule.severity === "high" ? "error" : "warning",
|
|
1203
|
+
message: { text: `${f.rule.description}: ${f.match}` },
|
|
1204
|
+
locations: [
|
|
1205
|
+
{
|
|
1206
|
+
physicalLocation: {
|
|
1207
|
+
artifactLocation: { uri: result.file },
|
|
1208
|
+
region: {
|
|
1209
|
+
startLine: f.line,
|
|
1210
|
+
startColumn: f.column
|
|
1211
|
+
}
|
|
1212
|
+
}
|
|
1213
|
+
}
|
|
1214
|
+
]
|
|
1215
|
+
}))
|
|
1216
|
+
)
|
|
1217
|
+
}
|
|
1218
|
+
]
|
|
1219
|
+
};
|
|
1220
|
+
}
|
|
1221
|
+
|
|
1222
|
+
// src/core/skills/validator.ts
|
|
1223
|
+
import { readFile as readFile5 } from "fs/promises";
|
|
1224
|
+
import { existsSync as existsSync7 } from "fs";
|
|
1225
|
+
import matter2 from "gray-matter";
|
|
1226
|
+
var RESERVED_NAMES = [
|
|
1227
|
+
"anthropic",
|
|
1228
|
+
"claude",
|
|
1229
|
+
"google",
|
|
1230
|
+
"openai",
|
|
1231
|
+
"microsoft",
|
|
1232
|
+
"cursor",
|
|
1233
|
+
"windsurf",
|
|
1234
|
+
"codex",
|
|
1235
|
+
"gemini",
|
|
1236
|
+
"copilot"
|
|
1237
|
+
];
|
|
1238
|
+
var NAME_PATTERN = /^[a-z0-9][a-z0-9-]*[a-z0-9]$|^[a-z0-9]$/;
|
|
1239
|
+
var MAX_NAME_LENGTH = 64;
|
|
1240
|
+
var MAX_DESCRIPTION_LENGTH = 1024;
|
|
1241
|
+
var WARN_BODY_LINES = 500;
|
|
1242
|
+
var WARN_DESCRIPTION_LENGTH = 50;
|
|
1243
|
+
async function validateSkill(filePath) {
|
|
1244
|
+
const issues = [];
|
|
1245
|
+
if (!existsSync7(filePath)) {
|
|
1246
|
+
return {
|
|
1247
|
+
valid: false,
|
|
1248
|
+
issues: [{ level: "error", field: "file", message: "File does not exist" }],
|
|
1249
|
+
metadata: null
|
|
1250
|
+
};
|
|
1251
|
+
}
|
|
1252
|
+
const content = await readFile5(filePath, "utf-8");
|
|
1253
|
+
if (!content.startsWith("---")) {
|
|
1254
|
+
issues.push({
|
|
1255
|
+
level: "error",
|
|
1256
|
+
field: "frontmatter",
|
|
1257
|
+
message: "Missing YAML frontmatter (file must start with ---)"
|
|
1258
|
+
});
|
|
1259
|
+
return { valid: false, issues, metadata: null };
|
|
1260
|
+
}
|
|
1261
|
+
let data;
|
|
1262
|
+
let body;
|
|
1263
|
+
try {
|
|
1264
|
+
const parsed = matter2(content);
|
|
1265
|
+
data = parsed.data;
|
|
1266
|
+
body = parsed.content;
|
|
1267
|
+
} catch (err) {
|
|
1268
|
+
issues.push({
|
|
1269
|
+
level: "error",
|
|
1270
|
+
field: "frontmatter",
|
|
1271
|
+
message: `Invalid YAML frontmatter: ${err instanceof Error ? err.message : String(err)}`
|
|
1272
|
+
});
|
|
1273
|
+
return { valid: false, issues, metadata: null };
|
|
1274
|
+
}
|
|
1275
|
+
if (!data["name"]) {
|
|
1276
|
+
issues.push({ level: "error", field: "name", message: "Missing required field: name" });
|
|
1277
|
+
} else {
|
|
1278
|
+
const name = String(data["name"]);
|
|
1279
|
+
if (name.length > MAX_NAME_LENGTH) {
|
|
1280
|
+
issues.push({
|
|
1281
|
+
level: "error",
|
|
1282
|
+
field: "name",
|
|
1283
|
+
message: `Name too long (${name.length} chars, max ${MAX_NAME_LENGTH})`
|
|
1284
|
+
});
|
|
1285
|
+
}
|
|
1286
|
+
if (!NAME_PATTERN.test(name)) {
|
|
1287
|
+
issues.push({
|
|
1288
|
+
level: "error",
|
|
1289
|
+
field: "name",
|
|
1290
|
+
message: "Name must be lowercase letters, numbers, and hyphens only"
|
|
1291
|
+
});
|
|
1292
|
+
}
|
|
1293
|
+
if (RESERVED_NAMES.includes(name.toLowerCase())) {
|
|
1294
|
+
issues.push({
|
|
1295
|
+
level: "error",
|
|
1296
|
+
field: "name",
|
|
1297
|
+
message: `Name "${name}" is reserved`
|
|
1298
|
+
});
|
|
1299
|
+
}
|
|
1300
|
+
if (/<[^>]+>/.test(name)) {
|
|
1301
|
+
issues.push({
|
|
1302
|
+
level: "error",
|
|
1303
|
+
field: "name",
|
|
1304
|
+
message: "Name must not contain XML/HTML tags"
|
|
1305
|
+
});
|
|
1306
|
+
}
|
|
1307
|
+
}
|
|
1308
|
+
if (!data["description"]) {
|
|
1309
|
+
issues.push({ level: "error", field: "description", message: "Missing required field: description" });
|
|
1310
|
+
} else {
|
|
1311
|
+
const desc = String(data["description"]);
|
|
1312
|
+
if (desc.length > MAX_DESCRIPTION_LENGTH) {
|
|
1313
|
+
issues.push({
|
|
1314
|
+
level: "error",
|
|
1315
|
+
field: "description",
|
|
1316
|
+
message: `Description too long (${desc.length} chars, max ${MAX_DESCRIPTION_LENGTH})`
|
|
1317
|
+
});
|
|
1318
|
+
}
|
|
1319
|
+
if (desc.length < WARN_DESCRIPTION_LENGTH) {
|
|
1320
|
+
issues.push({
|
|
1321
|
+
level: "warning",
|
|
1322
|
+
field: "description",
|
|
1323
|
+
message: `Description is short (${desc.length} chars). Consider adding more detail.`
|
|
1324
|
+
});
|
|
1325
|
+
}
|
|
1326
|
+
if (/<[^>]+>/.test(desc)) {
|
|
1327
|
+
issues.push({
|
|
1328
|
+
level: "error",
|
|
1329
|
+
field: "description",
|
|
1330
|
+
message: "Description must not contain XML/HTML tags"
|
|
1331
|
+
});
|
|
1332
|
+
}
|
|
1333
|
+
}
|
|
1334
|
+
const bodyLines = body.trim().split("\n").length;
|
|
1335
|
+
if (bodyLines > WARN_BODY_LINES) {
|
|
1336
|
+
issues.push({
|
|
1337
|
+
level: "warning",
|
|
1338
|
+
field: "body",
|
|
1339
|
+
message: `Body is long (${bodyLines} lines). Consider splitting into multiple skills.`
|
|
1340
|
+
});
|
|
1341
|
+
}
|
|
1342
|
+
if (!body.trim()) {
|
|
1343
|
+
issues.push({
|
|
1344
|
+
level: "warning",
|
|
1345
|
+
field: "body",
|
|
1346
|
+
message: "Empty skill body. Add instructions for the AI agent."
|
|
1347
|
+
});
|
|
1348
|
+
}
|
|
1349
|
+
const hasErrors = issues.some((i) => i.level === "error");
|
|
1350
|
+
return {
|
|
1351
|
+
valid: !hasErrors,
|
|
1352
|
+
issues,
|
|
1353
|
+
metadata: data
|
|
1354
|
+
};
|
|
1355
|
+
}
|
|
1356
|
+
|
|
1357
|
+
// src/core/formats/utils.ts
|
|
1358
|
+
function deepMerge(target, source) {
|
|
1359
|
+
const result = { ...target };
|
|
1360
|
+
for (const key of Object.keys(source)) {
|
|
1361
|
+
const sourceVal = source[key];
|
|
1362
|
+
const targetVal = target[key];
|
|
1363
|
+
if (sourceVal !== null && typeof sourceVal === "object" && !Array.isArray(sourceVal) && targetVal !== null && typeof targetVal === "object" && !Array.isArray(targetVal)) {
|
|
1364
|
+
result[key] = deepMerge(
|
|
1365
|
+
targetVal,
|
|
1366
|
+
sourceVal
|
|
1367
|
+
);
|
|
1368
|
+
} else {
|
|
1369
|
+
result[key] = sourceVal;
|
|
1370
|
+
}
|
|
1371
|
+
}
|
|
1372
|
+
return result;
|
|
1373
|
+
}
|
|
1374
|
+
function getNestedValue(obj, keyPath) {
|
|
1375
|
+
const parts = keyPath.split(".");
|
|
1376
|
+
let current = obj;
|
|
1377
|
+
for (const part of parts) {
|
|
1378
|
+
if (current === null || typeof current !== "object") return void 0;
|
|
1379
|
+
current = current[part];
|
|
1380
|
+
}
|
|
1381
|
+
return current;
|
|
1382
|
+
}
|
|
1383
|
+
async function ensureDir(filePath) {
|
|
1384
|
+
const { mkdir: mkdir5 } = await import("fs/promises");
|
|
1385
|
+
const { dirname: dirname5 } = await import("path");
|
|
1386
|
+
await mkdir5(dirname5(filePath), { recursive: true });
|
|
1387
|
+
}
|
|
1388
|
+
|
|
1389
|
+
// src/core/formats/json.ts
|
|
1390
|
+
import { readFile as readFile6, writeFile as writeFile4 } from "fs/promises";
|
|
1391
|
+
import { existsSync as existsSync8 } from "fs";
|
|
1392
|
+
import * as jsonc from "jsonc-parser";
|
|
1393
|
+
async function readJsonConfig(filePath) {
|
|
1394
|
+
if (!existsSync8(filePath)) return {};
|
|
1395
|
+
const content = await readFile6(filePath, "utf-8");
|
|
1396
|
+
if (!content.trim()) return {};
|
|
1397
|
+
const errors = [];
|
|
1398
|
+
const result = jsonc.parse(content, errors);
|
|
1399
|
+
if (errors.length > 0) {
|
|
1400
|
+
return JSON.parse(content);
|
|
1401
|
+
}
|
|
1402
|
+
return result ?? {};
|
|
1403
|
+
}
|
|
1404
|
+
function detectIndent(content) {
|
|
1405
|
+
const lines = content.split("\n");
|
|
1406
|
+
for (const line of lines) {
|
|
1407
|
+
const match = line.match(/^(\s+)/);
|
|
1408
|
+
if (match?.[1]) {
|
|
1409
|
+
const ws = match[1];
|
|
1410
|
+
if (ws.startsWith(" ")) {
|
|
1411
|
+
return { indent: " ", insertSpaces: false, tabSize: 1 };
|
|
1412
|
+
}
|
|
1413
|
+
return { indent: ws, insertSpaces: true, tabSize: ws.length };
|
|
1414
|
+
}
|
|
1415
|
+
}
|
|
1416
|
+
return { indent: " ", insertSpaces: true, tabSize: 2 };
|
|
1417
|
+
}
|
|
1418
|
+
async function writeJsonConfig(filePath, configKey, serverName, serverConfig) {
|
|
1419
|
+
await ensureDir(filePath);
|
|
1420
|
+
let content;
|
|
1421
|
+
if (existsSync8(filePath)) {
|
|
1422
|
+
content = await readFile6(filePath, "utf-8");
|
|
1423
|
+
if (!content.trim()) {
|
|
1424
|
+
content = "{}";
|
|
1425
|
+
}
|
|
1426
|
+
} else {
|
|
1427
|
+
content = "{}";
|
|
1428
|
+
}
|
|
1429
|
+
const { tabSize, insertSpaces } = detectIndent(content);
|
|
1430
|
+
const formatOptions = {
|
|
1431
|
+
tabSize,
|
|
1432
|
+
insertSpaces,
|
|
1433
|
+
eol: "\n"
|
|
1434
|
+
};
|
|
1435
|
+
const keyParts = configKey.split(".");
|
|
1436
|
+
const jsonPath = [...keyParts, serverName];
|
|
1437
|
+
const edits = jsonc.modify(content, jsonPath, serverConfig, { formattingOptions: formatOptions });
|
|
1438
|
+
if (edits.length > 0) {
|
|
1439
|
+
content = jsonc.applyEdits(content, edits);
|
|
1440
|
+
}
|
|
1441
|
+
if (!content.endsWith("\n")) {
|
|
1442
|
+
content += "\n";
|
|
1443
|
+
}
|
|
1444
|
+
await writeFile4(filePath, content, "utf-8");
|
|
1445
|
+
}
|
|
1446
|
+
async function removeJsonConfig(filePath, configKey, serverName) {
|
|
1447
|
+
if (!existsSync8(filePath)) return false;
|
|
1448
|
+
let content = await readFile6(filePath, "utf-8");
|
|
1449
|
+
if (!content.trim()) return false;
|
|
1450
|
+
const { tabSize, insertSpaces } = detectIndent(content);
|
|
1451
|
+
const formatOptions = {
|
|
1452
|
+
tabSize,
|
|
1453
|
+
insertSpaces,
|
|
1454
|
+
eol: "\n"
|
|
1455
|
+
};
|
|
1456
|
+
const keyParts = configKey.split(".");
|
|
1457
|
+
const jsonPath = [...keyParts, serverName];
|
|
1458
|
+
const edits = jsonc.modify(content, jsonPath, void 0, { formattingOptions: formatOptions });
|
|
1459
|
+
if (edits.length === 0) return false;
|
|
1460
|
+
content = jsonc.applyEdits(content, edits);
|
|
1461
|
+
if (!content.endsWith("\n")) {
|
|
1462
|
+
content += "\n";
|
|
1463
|
+
}
|
|
1464
|
+
await writeFile4(filePath, content, "utf-8");
|
|
1465
|
+
return true;
|
|
1466
|
+
}
|
|
1467
|
+
|
|
1468
|
+
// src/core/formats/yaml.ts
|
|
1469
|
+
import { readFile as readFile7, writeFile as writeFile5 } from "fs/promises";
|
|
1470
|
+
import { existsSync as existsSync9 } from "fs";
|
|
1471
|
+
import yaml from "js-yaml";
|
|
1472
|
+
async function readYamlConfig(filePath) {
|
|
1473
|
+
if (!existsSync9(filePath)) return {};
|
|
1474
|
+
const content = await readFile7(filePath, "utf-8");
|
|
1475
|
+
if (!content.trim()) return {};
|
|
1476
|
+
const result = yaml.load(content);
|
|
1477
|
+
return result ?? {};
|
|
1478
|
+
}
|
|
1479
|
+
async function writeYamlConfig(filePath, configKey, serverName, serverConfig) {
|
|
1480
|
+
await ensureDir(filePath);
|
|
1481
|
+
const existing = await readYamlConfig(filePath);
|
|
1482
|
+
const keyParts = configKey.split(".");
|
|
1483
|
+
let newEntry = { [serverName]: serverConfig };
|
|
1484
|
+
for (let i = keyParts.length - 1; i >= 0; i--) {
|
|
1485
|
+
newEntry = { [keyParts[i]]: newEntry };
|
|
1486
|
+
}
|
|
1487
|
+
const merged = deepMerge(existing, newEntry);
|
|
1488
|
+
const content = yaml.dump(merged, {
|
|
1489
|
+
indent: 2,
|
|
1490
|
+
lineWidth: -1,
|
|
1491
|
+
noRefs: true,
|
|
1492
|
+
sortKeys: false
|
|
1493
|
+
});
|
|
1494
|
+
await writeFile5(filePath, content, "utf-8");
|
|
1495
|
+
}
|
|
1496
|
+
async function removeYamlConfig(filePath, configKey, serverName) {
|
|
1497
|
+
if (!existsSync9(filePath)) return false;
|
|
1498
|
+
const existing = await readYamlConfig(filePath);
|
|
1499
|
+
const keyParts = configKey.split(".");
|
|
1500
|
+
let current = existing;
|
|
1501
|
+
for (const part of keyParts) {
|
|
1502
|
+
const next = current[part];
|
|
1503
|
+
if (typeof next !== "object" || next === null) return false;
|
|
1504
|
+
current = next;
|
|
1505
|
+
}
|
|
1506
|
+
if (!(serverName in current)) return false;
|
|
1507
|
+
delete current[serverName];
|
|
1508
|
+
const content = yaml.dump(existing, {
|
|
1509
|
+
indent: 2,
|
|
1510
|
+
lineWidth: -1,
|
|
1511
|
+
noRefs: true,
|
|
1512
|
+
sortKeys: false
|
|
1513
|
+
});
|
|
1514
|
+
await writeFile5(filePath, content, "utf-8");
|
|
1515
|
+
return true;
|
|
1516
|
+
}
|
|
1517
|
+
|
|
1518
|
+
// src/core/formats/toml.ts
|
|
1519
|
+
import { readFile as readFile8, writeFile as writeFile6 } from "fs/promises";
|
|
1520
|
+
import { existsSync as existsSync10 } from "fs";
|
|
1521
|
+
import TOML from "@iarna/toml";
|
|
1522
|
+
async function readTomlConfig(filePath) {
|
|
1523
|
+
if (!existsSync10(filePath)) return {};
|
|
1524
|
+
const content = await readFile8(filePath, "utf-8");
|
|
1525
|
+
if (!content.trim()) return {};
|
|
1526
|
+
const result = TOML.parse(content);
|
|
1527
|
+
return result;
|
|
1528
|
+
}
|
|
1529
|
+
async function writeTomlConfig(filePath, configKey, serverName, serverConfig) {
|
|
1530
|
+
await ensureDir(filePath);
|
|
1531
|
+
const existing = await readTomlConfig(filePath);
|
|
1532
|
+
const keyParts = configKey.split(".");
|
|
1533
|
+
let newEntry = { [serverName]: serverConfig };
|
|
1534
|
+
for (let i = keyParts.length - 1; i >= 0; i--) {
|
|
1535
|
+
newEntry = { [keyParts[i]]: newEntry };
|
|
1536
|
+
}
|
|
1537
|
+
const merged = deepMerge(existing, newEntry);
|
|
1538
|
+
const content = TOML.stringify(merged);
|
|
1539
|
+
await writeFile6(filePath, content, "utf-8");
|
|
1540
|
+
}
|
|
1541
|
+
async function removeTomlConfig(filePath, configKey, serverName) {
|
|
1542
|
+
if (!existsSync10(filePath)) return false;
|
|
1543
|
+
const existing = await readTomlConfig(filePath);
|
|
1544
|
+
const keyParts = configKey.split(".");
|
|
1545
|
+
let current = existing;
|
|
1546
|
+
for (const part of keyParts) {
|
|
1547
|
+
const next = current[part];
|
|
1548
|
+
if (typeof next !== "object" || next === null) return false;
|
|
1549
|
+
current = next;
|
|
1550
|
+
}
|
|
1551
|
+
if (!(serverName in current)) return false;
|
|
1552
|
+
delete current[serverName];
|
|
1553
|
+
const content = TOML.stringify(existing);
|
|
1554
|
+
await writeFile6(filePath, content, "utf-8");
|
|
1555
|
+
return true;
|
|
1556
|
+
}
|
|
1557
|
+
|
|
1558
|
+
// src/core/formats/index.ts
|
|
1559
|
+
async function readConfig(filePath, format) {
|
|
1560
|
+
switch (format) {
|
|
1561
|
+
case "json":
|
|
1562
|
+
case "jsonc":
|
|
1563
|
+
return readJsonConfig(filePath);
|
|
1564
|
+
case "yaml":
|
|
1565
|
+
return readYamlConfig(filePath);
|
|
1566
|
+
case "toml":
|
|
1567
|
+
return readTomlConfig(filePath);
|
|
1568
|
+
default:
|
|
1569
|
+
throw new Error(`Unsupported config format: ${format}`);
|
|
1570
|
+
}
|
|
1571
|
+
}
|
|
1572
|
+
async function writeConfig(filePath, format, key, serverName, serverConfig) {
|
|
1573
|
+
switch (format) {
|
|
1574
|
+
case "json":
|
|
1575
|
+
case "jsonc":
|
|
1576
|
+
return writeJsonConfig(filePath, key, serverName, serverConfig);
|
|
1577
|
+
case "yaml":
|
|
1578
|
+
return writeYamlConfig(filePath, key, serverName, serverConfig);
|
|
1579
|
+
case "toml":
|
|
1580
|
+
return writeTomlConfig(filePath, key, serverName, serverConfig);
|
|
1581
|
+
default:
|
|
1582
|
+
throw new Error(`Unsupported config format: ${format}`);
|
|
1583
|
+
}
|
|
1584
|
+
}
|
|
1585
|
+
async function removeConfig(filePath, format, key, serverName) {
|
|
1586
|
+
switch (format) {
|
|
1587
|
+
case "json":
|
|
1588
|
+
case "jsonc":
|
|
1589
|
+
return removeJsonConfig(filePath, key, serverName);
|
|
1590
|
+
case "yaml":
|
|
1591
|
+
return removeYamlConfig(filePath, key, serverName);
|
|
1592
|
+
case "toml":
|
|
1593
|
+
return removeTomlConfig(filePath, key, serverName);
|
|
1594
|
+
default:
|
|
1595
|
+
throw new Error(`Unsupported config format: ${format}`);
|
|
1596
|
+
}
|
|
1597
|
+
}
|
|
1598
|
+
|
|
1599
|
+
// src/core/mcp/transforms.ts
|
|
1600
|
+
function transformGoose(serverName, config) {
|
|
1601
|
+
if (config.url) {
|
|
1602
|
+
const transport = config.type === "sse" ? "sse" : "streamable_http";
|
|
1603
|
+
return {
|
|
1604
|
+
name: serverName,
|
|
1605
|
+
type: transport,
|
|
1606
|
+
uri: config.url,
|
|
1607
|
+
...config.headers ? { headers: config.headers } : {},
|
|
1608
|
+
enabled: true,
|
|
1609
|
+
timeout: 300
|
|
1610
|
+
};
|
|
1611
|
+
}
|
|
1612
|
+
return {
|
|
1613
|
+
name: serverName,
|
|
1614
|
+
type: "stdio",
|
|
1615
|
+
cmd: config.command,
|
|
1616
|
+
args: config.args ?? [],
|
|
1617
|
+
...config.env ? { envs: config.env } : {},
|
|
1618
|
+
enabled: true,
|
|
1619
|
+
timeout: 300
|
|
1620
|
+
};
|
|
1621
|
+
}
|
|
1622
|
+
function transformZed(serverName, config) {
|
|
1623
|
+
if (config.url) {
|
|
1624
|
+
return {
|
|
1625
|
+
source: "custom",
|
|
1626
|
+
type: config.type ?? "http",
|
|
1627
|
+
url: config.url,
|
|
1628
|
+
...config.headers ? { headers: config.headers } : {}
|
|
1629
|
+
};
|
|
1630
|
+
}
|
|
1631
|
+
return {
|
|
1632
|
+
source: "custom",
|
|
1633
|
+
command: config.command,
|
|
1634
|
+
args: config.args ?? [],
|
|
1635
|
+
...config.env ? { env: config.env } : {}
|
|
1636
|
+
};
|
|
1637
|
+
}
|
|
1638
|
+
function transformOpenCode(serverName, config) {
|
|
1639
|
+
if (config.url) {
|
|
1640
|
+
return {
|
|
1641
|
+
type: "remote",
|
|
1642
|
+
url: config.url,
|
|
1643
|
+
enabled: true,
|
|
1644
|
+
...config.headers ? { headers: config.headers } : {}
|
|
1645
|
+
};
|
|
1646
|
+
}
|
|
1647
|
+
return {
|
|
1648
|
+
type: "local",
|
|
1649
|
+
command: config.command,
|
|
1650
|
+
args: config.args ?? [],
|
|
1651
|
+
enabled: true,
|
|
1652
|
+
...config.env ? { environment: config.env } : {}
|
|
1653
|
+
};
|
|
1654
|
+
}
|
|
1655
|
+
function transformCodex(serverName, config) {
|
|
1656
|
+
if (config.url) {
|
|
1657
|
+
return {
|
|
1658
|
+
type: config.type ?? "http",
|
|
1659
|
+
url: config.url,
|
|
1660
|
+
...config.headers ? { headers: config.headers } : {}
|
|
1661
|
+
};
|
|
1662
|
+
}
|
|
1663
|
+
return {
|
|
1664
|
+
command: config.command,
|
|
1665
|
+
args: config.args ?? [],
|
|
1666
|
+
...config.env ? { env: config.env } : {}
|
|
1667
|
+
};
|
|
1668
|
+
}
|
|
1669
|
+
function transformCursor(serverName, config) {
|
|
1670
|
+
if (config.url) {
|
|
1671
|
+
return {
|
|
1672
|
+
url: config.url,
|
|
1673
|
+
...config.headers ? { headers: config.headers } : {}
|
|
1674
|
+
};
|
|
1675
|
+
}
|
|
1676
|
+
return config;
|
|
1677
|
+
}
|
|
1678
|
+
function getTransform(providerId) {
|
|
1679
|
+
switch (providerId) {
|
|
1680
|
+
case "goose":
|
|
1681
|
+
return transformGoose;
|
|
1682
|
+
case "zed":
|
|
1683
|
+
return transformZed;
|
|
1684
|
+
case "opencode":
|
|
1685
|
+
return transformOpenCode;
|
|
1686
|
+
case "codex":
|
|
1687
|
+
return transformCodex;
|
|
1688
|
+
case "cursor":
|
|
1689
|
+
return transformCursor;
|
|
1690
|
+
default:
|
|
1691
|
+
return void 0;
|
|
1692
|
+
}
|
|
1693
|
+
}
|
|
1694
|
+
|
|
1695
|
+
// src/core/mcp/reader.ts
|
|
1696
|
+
import { join as join7 } from "path";
|
|
1697
|
+
import { existsSync as existsSync11 } from "fs";
|
|
1698
|
+
function resolveConfigPath(provider, scope, projectDir) {
|
|
1699
|
+
if (scope === "project") {
|
|
1700
|
+
if (!provider.configPathProject) return null;
|
|
1701
|
+
return join7(projectDir ?? process.cwd(), provider.configPathProject);
|
|
1702
|
+
}
|
|
1703
|
+
return provider.configPathGlobal;
|
|
1704
|
+
}
|
|
1705
|
+
async function listMcpServers(provider, scope, projectDir) {
|
|
1706
|
+
const configPath = resolveConfigPath(provider, scope, projectDir);
|
|
1707
|
+
if (!configPath || !existsSync11(configPath)) return [];
|
|
1708
|
+
try {
|
|
1709
|
+
const config = await readConfig(configPath, provider.configFormat);
|
|
1710
|
+
const servers = getNestedValue(config, provider.configKey);
|
|
1711
|
+
if (!servers || typeof servers !== "object") return [];
|
|
1712
|
+
const entries = [];
|
|
1713
|
+
for (const [name, cfg] of Object.entries(servers)) {
|
|
1714
|
+
entries.push({
|
|
1715
|
+
name,
|
|
1716
|
+
providerId: provider.id,
|
|
1717
|
+
providerName: provider.toolName,
|
|
1718
|
+
scope,
|
|
1719
|
+
configPath,
|
|
1720
|
+
config: cfg ?? {}
|
|
1721
|
+
});
|
|
1722
|
+
}
|
|
1723
|
+
return entries;
|
|
1724
|
+
} catch {
|
|
1725
|
+
return [];
|
|
1726
|
+
}
|
|
1727
|
+
}
|
|
1728
|
+
async function listAllMcpServers(providers, scope, projectDir) {
|
|
1729
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1730
|
+
const allEntries = [];
|
|
1731
|
+
for (const provider of providers) {
|
|
1732
|
+
const configPath = resolveConfigPath(provider, scope, projectDir);
|
|
1733
|
+
if (!configPath || seen.has(configPath)) continue;
|
|
1734
|
+
seen.add(configPath);
|
|
1735
|
+
const entries = await listMcpServers(provider, scope, projectDir);
|
|
1736
|
+
allEntries.push(...entries);
|
|
1737
|
+
}
|
|
1738
|
+
return allEntries;
|
|
1739
|
+
}
|
|
1740
|
+
async function removeMcpServer(provider, serverName, scope, projectDir) {
|
|
1741
|
+
const configPath = resolveConfigPath(provider, scope, projectDir);
|
|
1742
|
+
if (!configPath) return false;
|
|
1743
|
+
return removeConfig(configPath, provider.configFormat, provider.configKey, serverName);
|
|
1744
|
+
}
|
|
1745
|
+
|
|
1746
|
+
// src/core/mcp/installer.ts
|
|
1747
|
+
function buildConfig(provider, serverName, config) {
|
|
1748
|
+
const transform = getTransform(provider.id);
|
|
1749
|
+
if (transform) {
|
|
1750
|
+
return transform(serverName, config);
|
|
1751
|
+
}
|
|
1752
|
+
return config;
|
|
1753
|
+
}
|
|
1754
|
+
async function installMcpServer(provider, serverName, config, scope = "project", projectDir) {
|
|
1755
|
+
const configPath = resolveConfigPath(provider, scope, projectDir);
|
|
1756
|
+
if (!configPath) {
|
|
1757
|
+
return {
|
|
1758
|
+
provider,
|
|
1759
|
+
scope,
|
|
1760
|
+
configPath: "",
|
|
1761
|
+
success: false,
|
|
1762
|
+
error: `Provider ${provider.id} does not support ${scope} config`
|
|
1763
|
+
};
|
|
1764
|
+
}
|
|
1765
|
+
try {
|
|
1766
|
+
const transformedConfig = buildConfig(provider, serverName, config);
|
|
1767
|
+
await writeConfig(
|
|
1768
|
+
configPath,
|
|
1769
|
+
provider.configFormat,
|
|
1770
|
+
provider.configKey,
|
|
1771
|
+
serverName,
|
|
1772
|
+
transformedConfig
|
|
1773
|
+
);
|
|
1774
|
+
return {
|
|
1775
|
+
provider,
|
|
1776
|
+
scope,
|
|
1777
|
+
configPath,
|
|
1778
|
+
success: true
|
|
1779
|
+
};
|
|
1780
|
+
} catch (err) {
|
|
1781
|
+
return {
|
|
1782
|
+
provider,
|
|
1783
|
+
scope,
|
|
1784
|
+
configPath,
|
|
1785
|
+
success: false,
|
|
1786
|
+
error: err instanceof Error ? err.message : String(err)
|
|
1787
|
+
};
|
|
1788
|
+
}
|
|
1789
|
+
}
|
|
1790
|
+
async function installMcpServerToAll(providers, serverName, config, scope = "project", projectDir) {
|
|
1791
|
+
const results = [];
|
|
1792
|
+
for (const provider of providers) {
|
|
1793
|
+
const result = await installMcpServer(provider, serverName, config, scope, projectDir);
|
|
1794
|
+
results.push(result);
|
|
1795
|
+
}
|
|
1796
|
+
return results;
|
|
1797
|
+
}
|
|
1798
|
+
function buildServerConfig(source, transport, headers) {
|
|
1799
|
+
if (source.type === "remote") {
|
|
1800
|
+
return {
|
|
1801
|
+
type: transport ?? "http",
|
|
1802
|
+
url: source.value,
|
|
1803
|
+
...headers && Object.keys(headers).length > 0 ? { headers } : {}
|
|
1804
|
+
};
|
|
1805
|
+
}
|
|
1806
|
+
if (source.type === "package") {
|
|
1807
|
+
return {
|
|
1808
|
+
command: "npx",
|
|
1809
|
+
args: ["-y", source.value]
|
|
1810
|
+
};
|
|
1811
|
+
}
|
|
1812
|
+
const parts = source.value.split(/\s+/);
|
|
1813
|
+
return {
|
|
1814
|
+
command: parts[0],
|
|
1815
|
+
args: parts.slice(1)
|
|
1816
|
+
};
|
|
1817
|
+
}
|
|
1818
|
+
|
|
1819
|
+
// src/core/instructions/injector.ts
|
|
1820
|
+
import { readFile as readFile9, writeFile as writeFile7 } from "fs/promises";
|
|
1821
|
+
import { existsSync as existsSync12 } from "fs";
|
|
1822
|
+
import { join as join8, dirname as dirname4 } from "path";
|
|
1823
|
+
import { mkdir as mkdir4 } from "fs/promises";
|
|
1824
|
+
var MARKER_START = "<!-- CAAMP:START -->";
|
|
1825
|
+
var MARKER_END = "<!-- CAAMP:END -->";
|
|
1826
|
+
var MARKER_PATTERN = /<!-- CAAMP:START -->[\s\S]*?<!-- CAAMP:END -->/;
|
|
1827
|
+
async function checkInjection(filePath, expectedContent) {
|
|
1828
|
+
if (!existsSync12(filePath)) return "missing";
|
|
1829
|
+
const content = await readFile9(filePath, "utf-8");
|
|
1830
|
+
if (!MARKER_PATTERN.test(content)) return "none";
|
|
1831
|
+
if (expectedContent) {
|
|
1832
|
+
const blockContent = extractBlock(content);
|
|
1833
|
+
if (blockContent && blockContent.trim() === expectedContent.trim()) {
|
|
1834
|
+
return "current";
|
|
1835
|
+
}
|
|
1836
|
+
return "outdated";
|
|
1837
|
+
}
|
|
1838
|
+
return "current";
|
|
1839
|
+
}
|
|
1840
|
+
function extractBlock(content) {
|
|
1841
|
+
const match = content.match(MARKER_PATTERN);
|
|
1842
|
+
if (!match) return null;
|
|
1843
|
+
return match[0].replace(MARKER_START, "").replace(MARKER_END, "").trim();
|
|
1844
|
+
}
|
|
1845
|
+
function buildBlock(content) {
|
|
1846
|
+
return `${MARKER_START}
|
|
1847
|
+
${content}
|
|
1848
|
+
${MARKER_END}`;
|
|
1849
|
+
}
|
|
1850
|
+
async function inject(filePath, content) {
|
|
1851
|
+
const block = buildBlock(content);
|
|
1852
|
+
await mkdir4(dirname4(filePath), { recursive: true });
|
|
1853
|
+
if (!existsSync12(filePath)) {
|
|
1854
|
+
await writeFile7(filePath, block + "\n", "utf-8");
|
|
1855
|
+
return "created";
|
|
1856
|
+
}
|
|
1857
|
+
const existing = await readFile9(filePath, "utf-8");
|
|
1858
|
+
if (MARKER_PATTERN.test(existing)) {
|
|
1859
|
+
const updated2 = existing.replace(MARKER_PATTERN, block);
|
|
1860
|
+
await writeFile7(filePath, updated2, "utf-8");
|
|
1861
|
+
return "updated";
|
|
1862
|
+
}
|
|
1863
|
+
const updated = block + "\n\n" + existing;
|
|
1864
|
+
await writeFile7(filePath, updated, "utf-8");
|
|
1865
|
+
return "added";
|
|
1866
|
+
}
|
|
1867
|
+
async function removeInjection(filePath) {
|
|
1868
|
+
if (!existsSync12(filePath)) return false;
|
|
1869
|
+
const content = await readFile9(filePath, "utf-8");
|
|
1870
|
+
if (!MARKER_PATTERN.test(content)) return false;
|
|
1871
|
+
const cleaned = content.replace(MARKER_PATTERN, "").replace(/^\n{2,}/, "\n").trim();
|
|
1872
|
+
if (!cleaned) {
|
|
1873
|
+
const { rm: rm2 } = await import("fs/promises");
|
|
1874
|
+
await rm2(filePath);
|
|
1875
|
+
} else {
|
|
1876
|
+
await writeFile7(filePath, cleaned + "\n", "utf-8");
|
|
1877
|
+
}
|
|
1878
|
+
return true;
|
|
1879
|
+
}
|
|
1880
|
+
async function checkAllInjections(providers, projectDir, scope, expectedContent) {
|
|
1881
|
+
const results = [];
|
|
1882
|
+
const checked = /* @__PURE__ */ new Set();
|
|
1883
|
+
for (const provider of providers) {
|
|
1884
|
+
const filePath = scope === "global" ? join8(provider.pathGlobal, provider.instructFile) : join8(projectDir, provider.instructFile);
|
|
1885
|
+
if (checked.has(filePath)) continue;
|
|
1886
|
+
checked.add(filePath);
|
|
1887
|
+
const status = await checkInjection(filePath, expectedContent);
|
|
1888
|
+
results.push({
|
|
1889
|
+
file: filePath,
|
|
1890
|
+
provider: provider.id,
|
|
1891
|
+
status,
|
|
1892
|
+
fileExists: existsSync12(filePath)
|
|
1893
|
+
});
|
|
1894
|
+
}
|
|
1895
|
+
return results;
|
|
1896
|
+
}
|
|
1897
|
+
async function injectAll(providers, projectDir, scope, content) {
|
|
1898
|
+
const results = /* @__PURE__ */ new Map();
|
|
1899
|
+
const injected = /* @__PURE__ */ new Set();
|
|
1900
|
+
for (const provider of providers) {
|
|
1901
|
+
const filePath = scope === "global" ? join8(provider.pathGlobal, provider.instructFile) : join8(projectDir, provider.instructFile);
|
|
1902
|
+
if (injected.has(filePath)) continue;
|
|
1903
|
+
injected.add(filePath);
|
|
1904
|
+
const action = await inject(filePath, content);
|
|
1905
|
+
results.set(filePath, action);
|
|
1906
|
+
}
|
|
1907
|
+
return results;
|
|
1908
|
+
}
|
|
1909
|
+
|
|
1910
|
+
// src/core/instructions/templates.ts
|
|
1911
|
+
function generateInjectionContent(options) {
|
|
1912
|
+
const lines = [];
|
|
1913
|
+
lines.push("## CAAMP Managed Configuration");
|
|
1914
|
+
lines.push("");
|
|
1915
|
+
lines.push("This section is managed by [CAAMP](https://github.com/caamp/caamp).");
|
|
1916
|
+
lines.push("Do not edit between the CAAMP markers manually.");
|
|
1917
|
+
if (options?.mcpServerName) {
|
|
1918
|
+
lines.push("");
|
|
1919
|
+
lines.push(`### MCP Server: ${options.mcpServerName}`);
|
|
1920
|
+
lines.push(`Configured via \`caamp mcp install\`.`);
|
|
1921
|
+
}
|
|
1922
|
+
if (options?.customContent) {
|
|
1923
|
+
lines.push("");
|
|
1924
|
+
lines.push(options.customContent);
|
|
1925
|
+
}
|
|
1926
|
+
return lines.join("\n");
|
|
1927
|
+
}
|
|
1928
|
+
function groupByInstructFile(providers) {
|
|
1929
|
+
const groups = /* @__PURE__ */ new Map();
|
|
1930
|
+
for (const provider of providers) {
|
|
1931
|
+
const existing = groups.get(provider.instructFile) ?? [];
|
|
1932
|
+
existing.push(provider);
|
|
1933
|
+
groups.set(provider.instructFile, existing);
|
|
1934
|
+
}
|
|
1935
|
+
return groups;
|
|
1936
|
+
}
|
|
1937
|
+
|
|
1938
|
+
export {
|
|
1939
|
+
getAllProviders,
|
|
1940
|
+
getProvider,
|
|
1941
|
+
resolveAlias,
|
|
1942
|
+
getProvidersByPriority,
|
|
1943
|
+
getProvidersByStatus,
|
|
1944
|
+
getProvidersByInstructFile,
|
|
1945
|
+
getInstructionFiles,
|
|
1946
|
+
getProviderCount,
|
|
1947
|
+
getRegistryVersion,
|
|
1948
|
+
detectProvider,
|
|
1949
|
+
detectAllProviders,
|
|
1950
|
+
getInstalledProviders,
|
|
1951
|
+
detectProjectProviders,
|
|
1952
|
+
parseSource,
|
|
1953
|
+
isMarketplaceScoped,
|
|
1954
|
+
installSkill,
|
|
1955
|
+
removeSkill,
|
|
1956
|
+
listCanonicalSkills,
|
|
1957
|
+
readLockFile,
|
|
1958
|
+
recordMcpInstall,
|
|
1959
|
+
removeMcpFromLock,
|
|
1960
|
+
getTrackedMcpServers,
|
|
1961
|
+
saveLastSelectedAgents,
|
|
1962
|
+
getLastSelectedAgents,
|
|
1963
|
+
recordSkillInstall,
|
|
1964
|
+
removeSkillFromLock,
|
|
1965
|
+
getTrackedSkills,
|
|
1966
|
+
checkSkillUpdate,
|
|
1967
|
+
MarketplaceClient,
|
|
1968
|
+
parseSkillFile,
|
|
1969
|
+
discoverSkill,
|
|
1970
|
+
discoverSkills,
|
|
1971
|
+
discoverSkillsMulti,
|
|
1972
|
+
scanFile,
|
|
1973
|
+
scanDirectory,
|
|
1974
|
+
toSarif,
|
|
1975
|
+
validateSkill,
|
|
1976
|
+
deepMerge,
|
|
1977
|
+
getNestedValue,
|
|
1978
|
+
ensureDir,
|
|
1979
|
+
readConfig,
|
|
1980
|
+
writeConfig,
|
|
1981
|
+
removeConfig,
|
|
1982
|
+
getTransform,
|
|
1983
|
+
resolveConfigPath,
|
|
1984
|
+
listMcpServers,
|
|
1985
|
+
listAllMcpServers,
|
|
1986
|
+
removeMcpServer,
|
|
1987
|
+
installMcpServer,
|
|
1988
|
+
installMcpServerToAll,
|
|
1989
|
+
buildServerConfig,
|
|
1990
|
+
checkInjection,
|
|
1991
|
+
inject,
|
|
1992
|
+
removeInjection,
|
|
1993
|
+
checkAllInjections,
|
|
1994
|
+
injectAll,
|
|
1995
|
+
generateInjectionContent,
|
|
1996
|
+
groupByInstructFile
|
|
1997
|
+
};
|
|
1998
|
+
//# sourceMappingURL=chunk-63BH7QMR.js.map
|