@cleocode/caamp 2026.4.5 → 2026.4.7
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 +9 -8
- package/dist/{chunk-43GULI6J.js → chunk-HEAGCHKU.js} +978 -358
- package/dist/chunk-HEAGCHKU.js.map +1 -0
- package/dist/cli.js +1170 -71
- package/dist/cli.js.map +1 -1
- package/dist/index.d.ts +811 -57
- package/dist/index.js +15 -1
- package/dist/index.js.map +1 -1
- package/package.json +2 -3
- package/dist/chunk-43GULI6J.js.map +0 -1
|
@@ -13,6 +13,7 @@ import {
|
|
|
13
13
|
getCanonicalSkillsDir,
|
|
14
14
|
getLockFilePath,
|
|
15
15
|
getPlatformLocations,
|
|
16
|
+
resolveProviderConfigPath,
|
|
16
17
|
resolveProviderProjectPath,
|
|
17
18
|
resolveProviderSkillsDirs
|
|
18
19
|
} from "./chunk-364OHA2T.js";
|
|
@@ -220,8 +221,8 @@ async function linkToAgent(canonicalPath, provider, skillName, isGlobal, project
|
|
|
220
221
|
await mkdir(targetSkillsDir, { recursive: true });
|
|
221
222
|
const linkPath = join2(targetSkillsDir, skillName);
|
|
222
223
|
if (existsSync2(linkPath)) {
|
|
223
|
-
const
|
|
224
|
-
if (
|
|
224
|
+
const stat4 = lstatSync(linkPath);
|
|
225
|
+
if (stat4.isSymbolicLink()) {
|
|
225
226
|
await rm(linkPath);
|
|
226
227
|
} else {
|
|
227
228
|
await rm(linkPath, { recursive: true });
|
|
@@ -352,8 +353,8 @@ async function snapshotSkillState(providerTargets, operation, projectDir, backup
|
|
|
352
353
|
pathSnapshots.push({ linkPath, state: "missing" });
|
|
353
354
|
continue;
|
|
354
355
|
}
|
|
355
|
-
const
|
|
356
|
-
if (
|
|
356
|
+
const stat4 = lstatSync2(linkPath);
|
|
357
|
+
if (stat4.isSymbolicLink()) {
|
|
357
358
|
pathSnapshots.push({
|
|
358
359
|
linkPath,
|
|
359
360
|
state: "symlink",
|
|
@@ -363,7 +364,7 @@ async function snapshotSkillState(providerTargets, operation, projectDir, backup
|
|
|
363
364
|
}
|
|
364
365
|
const backupPath = join3(backupRoot, "links", provider.id, `${skillName}-${basename(linkPath)}`);
|
|
365
366
|
await mkdir2(dirname(backupPath), { recursive: true });
|
|
366
|
-
if (
|
|
367
|
+
if (stat4.isDirectory()) {
|
|
367
368
|
await cp2(linkPath, backupPath, { recursive: true });
|
|
368
369
|
pathSnapshots.push({ linkPath, state: "directory", backupPath });
|
|
369
370
|
continue;
|
|
@@ -513,12 +514,14 @@ async function updateInstructionsSingleOperation(providers, content, scope = "pr
|
|
|
513
514
|
// src/core/harness/pi.ts
|
|
514
515
|
import { spawn } from "child_process";
|
|
515
516
|
import { existsSync as existsSync4 } from "fs";
|
|
516
|
-
import { cp as cp3, mkdir as mkdir3, readdir, readFile, rename, rm as rm3, writeFile } from "fs/promises";
|
|
517
|
+
import { cp as cp3, mkdir as mkdir3, open, readdir, readFile, rename, rm as rm3, stat, writeFile } from "fs/promises";
|
|
518
|
+
import { homedir as homedir2 } from "os";
|
|
519
|
+
import { basename as basename2, dirname as dirname2, extname, join as join5 } from "path";
|
|
520
|
+
|
|
521
|
+
// src/core/harness/scope.ts
|
|
517
522
|
import { homedir } from "os";
|
|
518
|
-
import {
|
|
519
|
-
var
|
|
520
|
-
var MARKER_END = "<!-- CAAMP:END -->";
|
|
521
|
-
var MARKER_PATTERN = /<!-- CAAMP:START -->[\s\S]*?<!-- CAAMP:END -->/;
|
|
523
|
+
import { join as join4 } from "path";
|
|
524
|
+
var TIER_PRECEDENCE = ["project", "user", "global"];
|
|
522
525
|
function getPiAgentDir() {
|
|
523
526
|
const env = process.env["PI_CODING_AGENT_DIR"];
|
|
524
527
|
if (env !== void 0 && env.length > 0) {
|
|
@@ -528,6 +531,79 @@ function getPiAgentDir() {
|
|
|
528
531
|
}
|
|
529
532
|
return join4(homedir(), ".pi", "agent");
|
|
530
533
|
}
|
|
534
|
+
function getCleoHomeDir() {
|
|
535
|
+
const env = process.env["CLEO_HOME"];
|
|
536
|
+
if (env !== void 0 && env.trim().length > 0) {
|
|
537
|
+
return env.trim();
|
|
538
|
+
}
|
|
539
|
+
if (process.platform === "win32") {
|
|
540
|
+
const localAppData = process.env["LOCALAPPDATA"];
|
|
541
|
+
if (localAppData !== void 0 && localAppData.length > 0) {
|
|
542
|
+
return join4(localAppData, "cleo", "Data");
|
|
543
|
+
}
|
|
544
|
+
return join4(homedir(), "AppData", "Local", "cleo", "Data");
|
|
545
|
+
}
|
|
546
|
+
if (process.platform === "darwin") {
|
|
547
|
+
return join4(homedir(), "Library", "Application Support", "cleo");
|
|
548
|
+
}
|
|
549
|
+
const xdgData = process.env["XDG_DATA_HOME"];
|
|
550
|
+
if (xdgData !== void 0 && xdgData.length > 0) {
|
|
551
|
+
return join4(xdgData, "cleo");
|
|
552
|
+
}
|
|
553
|
+
return join4(homedir(), ".local", "share", "cleo");
|
|
554
|
+
}
|
|
555
|
+
function assetDirName(kind) {
|
|
556
|
+
switch (kind) {
|
|
557
|
+
case "extensions":
|
|
558
|
+
return { native: "extensions", hubSuffix: "pi-extensions" };
|
|
559
|
+
case "prompts":
|
|
560
|
+
return { native: "prompts", hubSuffix: "pi-prompts" };
|
|
561
|
+
case "themes":
|
|
562
|
+
return { native: "themes", hubSuffix: "pi-themes" };
|
|
563
|
+
case "sessions":
|
|
564
|
+
return { native: "sessions", hubSuffix: "pi-sessions" };
|
|
565
|
+
case "cant":
|
|
566
|
+
return { native: "cant", hubSuffix: "pi-cant" };
|
|
567
|
+
}
|
|
568
|
+
}
|
|
569
|
+
function resolveTierDir(opts) {
|
|
570
|
+
const { tier, kind } = opts;
|
|
571
|
+
const names = assetDirName(kind);
|
|
572
|
+
if (tier === "project") {
|
|
573
|
+
if (opts.projectDir === void 0 || opts.projectDir.length === 0) {
|
|
574
|
+
throw new Error("resolveTierDir: 'project' tier requires a projectDir argument");
|
|
575
|
+
}
|
|
576
|
+
return join4(opts.projectDir, ".pi", names.native);
|
|
577
|
+
}
|
|
578
|
+
if (tier === "user") {
|
|
579
|
+
return join4(getPiAgentDir(), names.native);
|
|
580
|
+
}
|
|
581
|
+
return join4(getCleoHomeDir(), names.hubSuffix);
|
|
582
|
+
}
|
|
583
|
+
function resolveAllTiers(kind, projectDir) {
|
|
584
|
+
const out = [];
|
|
585
|
+
for (const tier of TIER_PRECEDENCE) {
|
|
586
|
+
if (tier === "project" && (projectDir === void 0 || projectDir.length === 0)) {
|
|
587
|
+
continue;
|
|
588
|
+
}
|
|
589
|
+
out.push({ tier, dir: resolveTierDir({ tier, kind, projectDir }) });
|
|
590
|
+
}
|
|
591
|
+
return out;
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
// src/core/harness/pi.ts
|
|
595
|
+
var MARKER_START = "<!-- CAAMP:START -->";
|
|
596
|
+
var MARKER_END = "<!-- CAAMP:END -->";
|
|
597
|
+
var MARKER_PATTERN = /<!-- CAAMP:START -->[\s\S]*?<!-- CAAMP:END -->/;
|
|
598
|
+
function getPiAgentDir2() {
|
|
599
|
+
const env = process.env["PI_CODING_AGENT_DIR"];
|
|
600
|
+
if (env !== void 0 && env.length > 0) {
|
|
601
|
+
if (env === "~") return homedir2();
|
|
602
|
+
if (env.startsWith("~/")) return join5(homedir2(), env.slice(2));
|
|
603
|
+
return env;
|
|
604
|
+
}
|
|
605
|
+
return join5(homedir2(), ".pi", "agent");
|
|
606
|
+
}
|
|
531
607
|
function isPlainObject(v) {
|
|
532
608
|
return typeof v === "object" && v !== null && !Array.isArray(v);
|
|
533
609
|
}
|
|
@@ -566,19 +642,13 @@ var PiHarness = class {
|
|
|
566
642
|
* Resolve the skills directory for a given scope.
|
|
567
643
|
*/
|
|
568
644
|
skillsDir(scope) {
|
|
569
|
-
return scope.kind === "global" ?
|
|
570
|
-
}
|
|
571
|
-
/**
|
|
572
|
-
* Resolve the extensions directory for a given scope.
|
|
573
|
-
*/
|
|
574
|
-
extensionsDir(scope) {
|
|
575
|
-
return scope.kind === "global" ? join4(getPiAgentDir(), "extensions") : join4(scope.projectDir, ".pi", "extensions");
|
|
645
|
+
return scope.kind === "global" ? join5(getPiAgentDir2(), "skills") : join5(scope.projectDir, ".pi", "skills");
|
|
576
646
|
}
|
|
577
647
|
/**
|
|
578
648
|
* Resolve the settings.json path for a given scope.
|
|
579
649
|
*/
|
|
580
650
|
settingsPath(scope) {
|
|
581
|
-
return scope.kind === "global" ?
|
|
651
|
+
return scope.kind === "global" ? join5(getPiAgentDir2(), "settings.json") : join5(scope.projectDir, ".pi", "settings.json");
|
|
582
652
|
}
|
|
583
653
|
/**
|
|
584
654
|
* Resolve the AGENTS.md instruction file path for a given scope.
|
|
@@ -589,19 +659,19 @@ var PiHarness = class {
|
|
|
589
659
|
* auto-discovering `AGENTS.md` from the working directory upwards.
|
|
590
660
|
*/
|
|
591
661
|
agentsMdPath(scope) {
|
|
592
|
-
return scope.kind === "global" ?
|
|
662
|
+
return scope.kind === "global" ? join5(getPiAgentDir2(), "AGENTS.md") : join5(scope.projectDir, "AGENTS.md");
|
|
593
663
|
}
|
|
594
664
|
// ── Skills ──────────────────────────────────────────────────────────
|
|
595
665
|
/** {@inheritDoc Harness.installSkill} */
|
|
596
666
|
async installSkill(sourcePath, skillName, scope) {
|
|
597
|
-
const targetDir =
|
|
667
|
+
const targetDir = join5(this.skillsDir(scope), skillName);
|
|
598
668
|
await rm3(targetDir, { recursive: true, force: true });
|
|
599
669
|
await mkdir3(dirname2(targetDir), { recursive: true });
|
|
600
670
|
await cp3(sourcePath, targetDir, { recursive: true });
|
|
601
671
|
}
|
|
602
672
|
/** {@inheritDoc Harness.removeSkill} */
|
|
603
673
|
async removeSkill(skillName, scope) {
|
|
604
|
-
const targetDir =
|
|
674
|
+
const targetDir = join5(this.skillsDir(scope), skillName);
|
|
605
675
|
await rm3(targetDir, { recursive: true, force: true });
|
|
606
676
|
}
|
|
607
677
|
/** {@inheritDoc Harness.listSkills} */
|
|
@@ -646,71 +716,6 @@ ${MARKER_END}`;
|
|
|
646
716
|
await writeFile(filePath, stripped.length === 0 ? "" : `${stripped}
|
|
647
717
|
`, "utf8");
|
|
648
718
|
}
|
|
649
|
-
// ── MCP-as-extension scaffold ───────────────────────────────────────
|
|
650
|
-
/**
|
|
651
|
-
* {@inheritDoc Harness.installMcpAsExtension}
|
|
652
|
-
*
|
|
653
|
-
* @remarks
|
|
654
|
-
* Emits a SCAFFOLD Pi extension file under `extensions/mcp-<name>.ts`.
|
|
655
|
-
* The scaffold registers a Pi tool whose `execute` function currently
|
|
656
|
-
* returns an "isError" payload explaining that the MCP bridge runtime
|
|
657
|
-
* is not yet implemented. This preserves the public lifecycle surface
|
|
658
|
-
* (install/list/remove) so orchestration code can treat the bridge as
|
|
659
|
-
* a first-class asset while the concrete JSON-RPC runtime is built out
|
|
660
|
-
* in a later wave.
|
|
661
|
-
*/
|
|
662
|
-
async installMcpAsExtension(server, scope) {
|
|
663
|
-
const dir = this.extensionsDir(scope);
|
|
664
|
-
await mkdir3(dir, { recursive: true });
|
|
665
|
-
const filePath = join4(dir, `mcp-${server.name}.ts`);
|
|
666
|
-
const launchConfig = JSON.stringify(
|
|
667
|
-
{
|
|
668
|
-
command: server.command,
|
|
669
|
-
args: server.args ?? [],
|
|
670
|
-
url: server.url,
|
|
671
|
-
env: server.env ?? {},
|
|
672
|
-
headers: server.headers ?? {}
|
|
673
|
-
},
|
|
674
|
-
null,
|
|
675
|
-
2
|
|
676
|
-
);
|
|
677
|
-
const src = `// AUTO-GENERATED by @cleocode/caamp \u2014 do not edit.
|
|
678
|
-
// MCP-as-Pi-extension bridge scaffold for "${server.name}".
|
|
679
|
-
// TODO: implement the MCP JSON-RPC bridge. Current behavior is a stub
|
|
680
|
-
// that logs every tool invocation. The scaffold exists so that CAAMP
|
|
681
|
-
// can manage the extension lifecycle (install/remove/list) without
|
|
682
|
-
// blocking on a full MCP runtime bridge.
|
|
683
|
-
|
|
684
|
-
const CONFIG = ${launchConfig};
|
|
685
|
-
|
|
686
|
-
export default (pi: unknown) => {
|
|
687
|
-
const api = pi as {
|
|
688
|
-
registerTool: (def: {
|
|
689
|
-
name: string;
|
|
690
|
-
label: string;
|
|
691
|
-
description: string;
|
|
692
|
-
parameters: unknown;
|
|
693
|
-
execute: (...args: unknown[]) => Promise<{ type: 'text'; text: string; isError?: boolean }>;
|
|
694
|
-
}) => void;
|
|
695
|
-
};
|
|
696
|
-
|
|
697
|
-
api.registerTool({
|
|
698
|
-
name: ${JSON.stringify(`mcp_${server.name}`)},
|
|
699
|
-
label: ${JSON.stringify(`MCP: ${server.name}`)},
|
|
700
|
-
description: ${JSON.stringify(
|
|
701
|
-
`MCP server "${server.name}" \u2014 bridge scaffold, not yet implemented.`
|
|
702
|
-
)},
|
|
703
|
-
parameters: { type: 'object', properties: {} },
|
|
704
|
-
execute: async () => ({
|
|
705
|
-
type: 'text',
|
|
706
|
-
text: \`MCP bridge for "${server.name}" is a scaffold. Config: \${JSON.stringify(CONFIG)}\`,
|
|
707
|
-
isError: true,
|
|
708
|
-
}),
|
|
709
|
-
});
|
|
710
|
-
};
|
|
711
|
-
`;
|
|
712
|
-
await writeFile(filePath, src, "utf8");
|
|
713
|
-
}
|
|
714
719
|
// ── Subagent spawn ──────────────────────────────────────────────────
|
|
715
720
|
/**
|
|
716
721
|
* {@inheritDoc Harness.spawnSubagent}
|
|
@@ -798,7 +803,446 @@ export default (pi: unknown) => {
|
|
|
798
803
|
async configureModels(modelPatterns, scope) {
|
|
799
804
|
await this.writeSettings({ enabledModels: modelPatterns }, scope);
|
|
800
805
|
}
|
|
806
|
+
// ── Wave-1 three-tier helpers ───────────────────────────────────────
|
|
807
|
+
/**
|
|
808
|
+
* Resolve the `models.json` path for a given legacy two-tier scope.
|
|
809
|
+
*
|
|
810
|
+
* @remarks
|
|
811
|
+
* Lives next to `settings.json`. Global scope uses the Pi state root,
|
|
812
|
+
* project scope uses the project's `.pi/` directory, matching the
|
|
813
|
+
* dual-file authority model documented in ADR-035 §D3.
|
|
814
|
+
*/
|
|
815
|
+
modelsConfigPath(scope) {
|
|
816
|
+
return scope.kind === "global" ? join5(getPiAgentDir2(), "models.json") : join5(scope.projectDir, ".pi", "models.json");
|
|
817
|
+
}
|
|
818
|
+
/**
|
|
819
|
+
* Resolve the sessions directory — always user-tier because Pi owns
|
|
820
|
+
* session storage and the three-tier model folds session listings to
|
|
821
|
+
* the single authoritative location per ADR-035 §D2.
|
|
822
|
+
*/
|
|
823
|
+
sessionsDir() {
|
|
824
|
+
return join5(getPiAgentDir2(), "sessions");
|
|
825
|
+
}
|
|
826
|
+
// ── Extensions (Wave-1, T263) ───────────────────────────────────────
|
|
827
|
+
/** {@inheritDoc Harness.installExtension} */
|
|
828
|
+
async installExtension(sourcePath, name, tier, projectDir, opts) {
|
|
829
|
+
if (!existsSync4(sourcePath)) {
|
|
830
|
+
throw new Error(`installExtension: source file does not exist: ${sourcePath}`);
|
|
831
|
+
}
|
|
832
|
+
const stats = await stat(sourcePath);
|
|
833
|
+
if (!stats.isFile()) {
|
|
834
|
+
throw new Error(`installExtension: source path is not a regular file: ${sourcePath}`);
|
|
835
|
+
}
|
|
836
|
+
const ext = extname(sourcePath);
|
|
837
|
+
if (ext !== ".ts" && ext !== ".tsx" && ext !== ".mts") {
|
|
838
|
+
throw new Error(
|
|
839
|
+
`installExtension: expected a TypeScript source file (.ts/.tsx/.mts), got: ${ext || "(no extension)"}`
|
|
840
|
+
);
|
|
841
|
+
}
|
|
842
|
+
const contents = await readFile(sourcePath, "utf8");
|
|
843
|
+
if (!/\bexport\s+default\b/.test(contents)) {
|
|
844
|
+
throw new Error(
|
|
845
|
+
`installExtension: source file is missing an 'export default' \u2014 Pi extensions must export a default function`
|
|
846
|
+
);
|
|
847
|
+
}
|
|
848
|
+
const dir = resolveTierDir({ tier, kind: "extensions", projectDir });
|
|
849
|
+
const targetPath = join5(dir, `${name}.ts`);
|
|
850
|
+
if (existsSync4(targetPath) && opts?.force !== true) {
|
|
851
|
+
throw new Error(
|
|
852
|
+
`installExtension: target already exists at ${targetPath} (pass --force to overwrite)`
|
|
853
|
+
);
|
|
854
|
+
}
|
|
855
|
+
await mkdir3(dir, { recursive: true });
|
|
856
|
+
await writeFile(targetPath, contents, "utf8");
|
|
857
|
+
return { targetPath, tier };
|
|
858
|
+
}
|
|
859
|
+
/** {@inheritDoc Harness.removeExtension} */
|
|
860
|
+
async removeExtension(name, tier, projectDir) {
|
|
861
|
+
const dir = resolveTierDir({ tier, kind: "extensions", projectDir });
|
|
862
|
+
const targetPath = join5(dir, `${name}.ts`);
|
|
863
|
+
if (!existsSync4(targetPath)) return false;
|
|
864
|
+
await rm3(targetPath, { force: true });
|
|
865
|
+
return true;
|
|
866
|
+
}
|
|
867
|
+
/** {@inheritDoc Harness.listExtensions} */
|
|
868
|
+
async listExtensions(projectDir) {
|
|
869
|
+
const tiers = resolveAllTiers("extensions", projectDir);
|
|
870
|
+
const out = [];
|
|
871
|
+
const seenNames = /* @__PURE__ */ new Set();
|
|
872
|
+
for (const { tier, dir } of tiers) {
|
|
873
|
+
if (!existsSync4(dir)) continue;
|
|
874
|
+
let entries;
|
|
875
|
+
try {
|
|
876
|
+
entries = await readdir(dir, { withFileTypes: true });
|
|
877
|
+
} catch {
|
|
878
|
+
continue;
|
|
879
|
+
}
|
|
880
|
+
for (const entry of entries) {
|
|
881
|
+
if (!entry.isFile()) continue;
|
|
882
|
+
const fileName = entry.name;
|
|
883
|
+
if (!fileName.endsWith(".ts")) continue;
|
|
884
|
+
const name = fileName.slice(0, -".ts".length);
|
|
885
|
+
const shadowed = seenNames.has(name);
|
|
886
|
+
out.push({
|
|
887
|
+
name,
|
|
888
|
+
tier,
|
|
889
|
+
path: join5(dir, fileName),
|
|
890
|
+
shadowed
|
|
891
|
+
});
|
|
892
|
+
seenNames.add(name);
|
|
893
|
+
}
|
|
894
|
+
}
|
|
895
|
+
return out;
|
|
896
|
+
}
|
|
897
|
+
// ── Sessions (Wave-1, T264) ─────────────────────────────────────────
|
|
898
|
+
/** {@inheritDoc Harness.listSessions} */
|
|
899
|
+
async listSessions(opts) {
|
|
900
|
+
const rootDir = this.sessionsDir();
|
|
901
|
+
if (!existsSync4(rootDir)) return [];
|
|
902
|
+
const files = [];
|
|
903
|
+
let rootEntries;
|
|
904
|
+
try {
|
|
905
|
+
rootEntries = await readdir(rootDir, { withFileTypes: true });
|
|
906
|
+
} catch {
|
|
907
|
+
return [];
|
|
908
|
+
}
|
|
909
|
+
for (const entry of rootEntries) {
|
|
910
|
+
if (entry.isFile() && entry.name.endsWith(".jsonl")) {
|
|
911
|
+
files.push(join5(rootDir, entry.name));
|
|
912
|
+
}
|
|
913
|
+
}
|
|
914
|
+
if (opts?.includeSubagents !== false) {
|
|
915
|
+
const subDir = join5(rootDir, "subagents");
|
|
916
|
+
if (existsSync4(subDir)) {
|
|
917
|
+
try {
|
|
918
|
+
const subEntries = await readdir(subDir, { withFileTypes: true });
|
|
919
|
+
for (const entry of subEntries) {
|
|
920
|
+
if (entry.isFile() && entry.name.endsWith(".jsonl")) {
|
|
921
|
+
files.push(join5(subDir, entry.name));
|
|
922
|
+
}
|
|
923
|
+
}
|
|
924
|
+
} catch {
|
|
925
|
+
}
|
|
926
|
+
}
|
|
927
|
+
}
|
|
928
|
+
const summaries = [];
|
|
929
|
+
for (const filePath of files) {
|
|
930
|
+
const summary = await readSessionHeader(filePath);
|
|
931
|
+
if (summary !== null) {
|
|
932
|
+
summaries.push(summary);
|
|
933
|
+
}
|
|
934
|
+
}
|
|
935
|
+
summaries.sort((a, b) => b.mtimeMs - a.mtimeMs);
|
|
936
|
+
return summaries;
|
|
937
|
+
}
|
|
938
|
+
/** {@inheritDoc Harness.showSession} */
|
|
939
|
+
async showSession(id) {
|
|
940
|
+
const summaries = await this.listSessions({ includeSubagents: true });
|
|
941
|
+
const match = summaries.find((s) => s.id === id);
|
|
942
|
+
if (match === void 0) {
|
|
943
|
+
throw new Error(`showSession: no session found with id ${id}`);
|
|
944
|
+
}
|
|
945
|
+
const raw = await readFile(match.filePath, "utf8");
|
|
946
|
+
const allLines = raw.split("\n");
|
|
947
|
+
while (allLines.length > 0 && allLines[allLines.length - 1] === "") {
|
|
948
|
+
allLines.pop();
|
|
949
|
+
}
|
|
950
|
+
const entries = allLines.slice(1);
|
|
951
|
+
return { summary: match, entries };
|
|
952
|
+
}
|
|
953
|
+
// ── Models (Wave-1, T265) ───────────────────────────────────────────
|
|
954
|
+
/** {@inheritDoc Harness.readModelsConfig} */
|
|
955
|
+
async readModelsConfig(scope) {
|
|
956
|
+
const filePath = this.modelsConfigPath(scope);
|
|
957
|
+
if (!existsSync4(filePath)) return { providers: {} };
|
|
958
|
+
let raw;
|
|
959
|
+
try {
|
|
960
|
+
raw = await readFile(filePath, "utf8");
|
|
961
|
+
} catch {
|
|
962
|
+
return { providers: {} };
|
|
963
|
+
}
|
|
964
|
+
try {
|
|
965
|
+
const parsed = JSON.parse(raw);
|
|
966
|
+
if (!isPlainObject(parsed)) return { providers: {} };
|
|
967
|
+
const providersField = parsed["providers"];
|
|
968
|
+
if (!isPlainObject(providersField)) return { providers: {} };
|
|
969
|
+
const providers = {};
|
|
970
|
+
for (const [id, block] of Object.entries(providersField)) {
|
|
971
|
+
if (isPlainObject(block)) {
|
|
972
|
+
providers[id] = block;
|
|
973
|
+
}
|
|
974
|
+
}
|
|
975
|
+
return { providers };
|
|
976
|
+
} catch {
|
|
977
|
+
return { providers: {} };
|
|
978
|
+
}
|
|
979
|
+
}
|
|
980
|
+
/** {@inheritDoc Harness.writeModelsConfig} */
|
|
981
|
+
async writeModelsConfig(config, scope) {
|
|
982
|
+
const filePath = this.modelsConfigPath(scope);
|
|
983
|
+
await atomicWriteJson(filePath, config);
|
|
984
|
+
}
|
|
985
|
+
/** {@inheritDoc Harness.listModels} */
|
|
986
|
+
async listModels(scope) {
|
|
987
|
+
const models = await this.readModelsConfig(scope);
|
|
988
|
+
const settings = await this.readSettings(scope);
|
|
989
|
+
const settingsObj = isPlainObject(settings) ? settings : {};
|
|
990
|
+
const enabledRaw = settingsObj["enabledModels"];
|
|
991
|
+
const enabled = Array.isArray(enabledRaw) ? enabledRaw.filter((v) => typeof v === "string") : [];
|
|
992
|
+
const defaultModel = typeof settingsObj["defaultModel"] === "string" ? settingsObj["defaultModel"] : null;
|
|
993
|
+
const defaultProvider = typeof settingsObj["defaultProvider"] === "string" ? settingsObj["defaultProvider"] : null;
|
|
994
|
+
const out = [];
|
|
995
|
+
const seen = /* @__PURE__ */ new Set();
|
|
996
|
+
for (const [providerId, providerBlock] of Object.entries(models.providers)) {
|
|
997
|
+
const modelDefs = providerBlock.models ?? [];
|
|
998
|
+
for (const def of modelDefs) {
|
|
999
|
+
const key = `${providerId}:${def.id}`;
|
|
1000
|
+
seen.add(key);
|
|
1001
|
+
const isEnabled = enabled.includes(key) || enabled.includes(`${providerId}/*`);
|
|
1002
|
+
const isDefault = defaultProvider === providerId && defaultModel === def.id;
|
|
1003
|
+
out.push({
|
|
1004
|
+
provider: providerId,
|
|
1005
|
+
id: def.id,
|
|
1006
|
+
name: def.name ?? null,
|
|
1007
|
+
enabled: isEnabled,
|
|
1008
|
+
isDefault,
|
|
1009
|
+
custom: true
|
|
1010
|
+
});
|
|
1011
|
+
}
|
|
1012
|
+
}
|
|
1013
|
+
for (const selection of enabled) {
|
|
1014
|
+
if (!selection.includes(":") && !selection.includes("/")) continue;
|
|
1015
|
+
const match = selection.match(/^([^:/]+)[:/]([^:/].*)$/);
|
|
1016
|
+
if (match === null) continue;
|
|
1017
|
+
const provider = match[1];
|
|
1018
|
+
const id = match[2];
|
|
1019
|
+
if (provider === void 0 || id === void 0) continue;
|
|
1020
|
+
if (id.endsWith("*")) continue;
|
|
1021
|
+
const key = `${provider}:${id}`;
|
|
1022
|
+
if (seen.has(key)) continue;
|
|
1023
|
+
seen.add(key);
|
|
1024
|
+
const isDefault = defaultProvider === provider && defaultModel === id;
|
|
1025
|
+
out.push({
|
|
1026
|
+
provider,
|
|
1027
|
+
id,
|
|
1028
|
+
name: null,
|
|
1029
|
+
enabled: true,
|
|
1030
|
+
isDefault,
|
|
1031
|
+
custom: false
|
|
1032
|
+
});
|
|
1033
|
+
}
|
|
1034
|
+
if (defaultProvider !== null && defaultModel !== null && !seen.has(`${defaultProvider}:${defaultModel}`)) {
|
|
1035
|
+
out.push({
|
|
1036
|
+
provider: defaultProvider,
|
|
1037
|
+
id: defaultModel,
|
|
1038
|
+
name: null,
|
|
1039
|
+
enabled: false,
|
|
1040
|
+
isDefault: true,
|
|
1041
|
+
custom: false
|
|
1042
|
+
});
|
|
1043
|
+
}
|
|
1044
|
+
return out;
|
|
1045
|
+
}
|
|
1046
|
+
// ── Prompts (Wave-1, T266) ──────────────────────────────────────────
|
|
1047
|
+
/** {@inheritDoc Harness.installPrompt} */
|
|
1048
|
+
async installPrompt(sourceDir, name, tier, projectDir, opts) {
|
|
1049
|
+
if (!existsSync4(sourceDir)) {
|
|
1050
|
+
throw new Error(`installPrompt: source directory does not exist: ${sourceDir}`);
|
|
1051
|
+
}
|
|
1052
|
+
const stats = await stat(sourceDir);
|
|
1053
|
+
if (!stats.isDirectory()) {
|
|
1054
|
+
throw new Error(`installPrompt: source path is not a directory: ${sourceDir}`);
|
|
1055
|
+
}
|
|
1056
|
+
if (!existsSync4(join5(sourceDir, "prompt.md"))) {
|
|
1057
|
+
throw new Error(`installPrompt: source directory is missing a prompt.md file: ${sourceDir}`);
|
|
1058
|
+
}
|
|
1059
|
+
const baseDir = resolveTierDir({ tier, kind: "prompts", projectDir });
|
|
1060
|
+
const targetPath = join5(baseDir, name);
|
|
1061
|
+
if (existsSync4(targetPath)) {
|
|
1062
|
+
if (opts?.force !== true) {
|
|
1063
|
+
throw new Error(
|
|
1064
|
+
`installPrompt: target already exists at ${targetPath} (pass --force to overwrite)`
|
|
1065
|
+
);
|
|
1066
|
+
}
|
|
1067
|
+
await rm3(targetPath, { recursive: true, force: true });
|
|
1068
|
+
}
|
|
1069
|
+
await mkdir3(baseDir, { recursive: true });
|
|
1070
|
+
await cp3(sourceDir, targetPath, { recursive: true });
|
|
1071
|
+
return { targetPath, tier };
|
|
1072
|
+
}
|
|
1073
|
+
/** {@inheritDoc Harness.listPrompts} */
|
|
1074
|
+
async listPrompts(projectDir) {
|
|
1075
|
+
const tiers = resolveAllTiers("prompts", projectDir);
|
|
1076
|
+
const out = [];
|
|
1077
|
+
const seenNames = /* @__PURE__ */ new Set();
|
|
1078
|
+
for (const { tier, dir } of tiers) {
|
|
1079
|
+
if (!existsSync4(dir)) continue;
|
|
1080
|
+
let entries;
|
|
1081
|
+
try {
|
|
1082
|
+
entries = await readdir(dir, { withFileTypes: true });
|
|
1083
|
+
} catch {
|
|
1084
|
+
continue;
|
|
1085
|
+
}
|
|
1086
|
+
for (const entry of entries) {
|
|
1087
|
+
if (!entry.isDirectory()) continue;
|
|
1088
|
+
const name = entry.name;
|
|
1089
|
+
const shadowed = seenNames.has(name);
|
|
1090
|
+
out.push({
|
|
1091
|
+
name,
|
|
1092
|
+
tier,
|
|
1093
|
+
path: join5(dir, name),
|
|
1094
|
+
shadowed
|
|
1095
|
+
});
|
|
1096
|
+
seenNames.add(name);
|
|
1097
|
+
}
|
|
1098
|
+
}
|
|
1099
|
+
return out;
|
|
1100
|
+
}
|
|
1101
|
+
/** {@inheritDoc Harness.removePrompt} */
|
|
1102
|
+
async removePrompt(name, tier, projectDir) {
|
|
1103
|
+
const dir = resolveTierDir({ tier, kind: "prompts", projectDir });
|
|
1104
|
+
const targetPath = join5(dir, name);
|
|
1105
|
+
if (!existsSync4(targetPath)) return false;
|
|
1106
|
+
await rm3(targetPath, { recursive: true, force: true });
|
|
1107
|
+
return true;
|
|
1108
|
+
}
|
|
1109
|
+
// ── Themes (Wave-1, T267) ───────────────────────────────────────────
|
|
1110
|
+
/** {@inheritDoc Harness.installTheme} */
|
|
1111
|
+
async installTheme(sourceFile, name, tier, projectDir, opts) {
|
|
1112
|
+
if (!existsSync4(sourceFile)) {
|
|
1113
|
+
throw new Error(`installTheme: source file does not exist: ${sourceFile}`);
|
|
1114
|
+
}
|
|
1115
|
+
const stats = await stat(sourceFile);
|
|
1116
|
+
if (!stats.isFile()) {
|
|
1117
|
+
throw new Error(`installTheme: source path is not a regular file: ${sourceFile}`);
|
|
1118
|
+
}
|
|
1119
|
+
const ext = extname(sourceFile);
|
|
1120
|
+
if (ext !== ".ts" && ext !== ".tsx" && ext !== ".mts" && ext !== ".json") {
|
|
1121
|
+
throw new Error(
|
|
1122
|
+
`installTheme: expected a theme file (.ts/.tsx/.mts/.json), got: ${ext || "(no extension)"}`
|
|
1123
|
+
);
|
|
1124
|
+
}
|
|
1125
|
+
const dir = resolveTierDir({ tier, kind: "themes", projectDir });
|
|
1126
|
+
const targetPath = join5(dir, `${name}${ext}`);
|
|
1127
|
+
if (existsSync4(targetPath) && opts?.force !== true) {
|
|
1128
|
+
throw new Error(
|
|
1129
|
+
`installTheme: target already exists at ${targetPath} (pass --force to overwrite)`
|
|
1130
|
+
);
|
|
1131
|
+
}
|
|
1132
|
+
const otherExts = [".ts", ".tsx", ".mts", ".json"].filter((e) => e !== ext);
|
|
1133
|
+
for (const otherExt of otherExts) {
|
|
1134
|
+
const otherPath = join5(dir, `${name}${otherExt}`);
|
|
1135
|
+
if (existsSync4(otherPath) && opts?.force !== true) {
|
|
1136
|
+
throw new Error(
|
|
1137
|
+
`installTheme: conflicting theme exists at ${otherPath} (pass --force to overwrite both)`
|
|
1138
|
+
);
|
|
1139
|
+
}
|
|
1140
|
+
if (existsSync4(otherPath) && opts?.force === true) {
|
|
1141
|
+
await rm3(otherPath, { force: true });
|
|
1142
|
+
}
|
|
1143
|
+
}
|
|
1144
|
+
await mkdir3(dir, { recursive: true });
|
|
1145
|
+
const contents = await readFile(sourceFile);
|
|
1146
|
+
await writeFile(targetPath, contents);
|
|
1147
|
+
return { targetPath, tier };
|
|
1148
|
+
}
|
|
1149
|
+
/** {@inheritDoc Harness.listThemes} */
|
|
1150
|
+
async listThemes(projectDir) {
|
|
1151
|
+
const tiers = resolveAllTiers("themes", projectDir);
|
|
1152
|
+
const out = [];
|
|
1153
|
+
const seenNames = /* @__PURE__ */ new Set();
|
|
1154
|
+
const validExts = /* @__PURE__ */ new Set([".ts", ".tsx", ".mts", ".json"]);
|
|
1155
|
+
for (const { tier, dir } of tiers) {
|
|
1156
|
+
if (!existsSync4(dir)) continue;
|
|
1157
|
+
let entries;
|
|
1158
|
+
try {
|
|
1159
|
+
entries = await readdir(dir, { withFileTypes: true });
|
|
1160
|
+
} catch {
|
|
1161
|
+
continue;
|
|
1162
|
+
}
|
|
1163
|
+
for (const entry of entries) {
|
|
1164
|
+
if (!entry.isFile()) continue;
|
|
1165
|
+
const fileExt = extname(entry.name);
|
|
1166
|
+
if (!validExts.has(fileExt)) continue;
|
|
1167
|
+
const name = entry.name.slice(0, -fileExt.length);
|
|
1168
|
+
const shadowed = seenNames.has(name);
|
|
1169
|
+
out.push({
|
|
1170
|
+
name,
|
|
1171
|
+
tier,
|
|
1172
|
+
path: join5(dir, entry.name),
|
|
1173
|
+
fileExt,
|
|
1174
|
+
shadowed
|
|
1175
|
+
});
|
|
1176
|
+
seenNames.add(name);
|
|
1177
|
+
}
|
|
1178
|
+
}
|
|
1179
|
+
return out;
|
|
1180
|
+
}
|
|
1181
|
+
/** {@inheritDoc Harness.removeTheme} */
|
|
1182
|
+
async removeTheme(name, tier, projectDir) {
|
|
1183
|
+
const dir = resolveTierDir({ tier, kind: "themes", projectDir });
|
|
1184
|
+
let removed = false;
|
|
1185
|
+
for (const ext of [".ts", ".tsx", ".mts", ".json"]) {
|
|
1186
|
+
const targetPath = join5(dir, `${name}${ext}`);
|
|
1187
|
+
if (existsSync4(targetPath)) {
|
|
1188
|
+
await rm3(targetPath, { force: true });
|
|
1189
|
+
removed = true;
|
|
1190
|
+
}
|
|
1191
|
+
}
|
|
1192
|
+
return removed;
|
|
1193
|
+
}
|
|
801
1194
|
};
|
|
1195
|
+
async function readSessionHeader(filePath) {
|
|
1196
|
+
let handle = null;
|
|
1197
|
+
try {
|
|
1198
|
+
handle = await open(filePath, "r");
|
|
1199
|
+
const stats = await handle.stat();
|
|
1200
|
+
const capacity = Math.min(stats.size, 64 * 1024);
|
|
1201
|
+
if (capacity === 0) return null;
|
|
1202
|
+
const buffer = Buffer.alloc(capacity);
|
|
1203
|
+
const { bytesRead } = await handle.read(buffer, 0, capacity, 0);
|
|
1204
|
+
const text = buffer.subarray(0, bytesRead).toString("utf8");
|
|
1205
|
+
const newlineIdx = text.indexOf("\n");
|
|
1206
|
+
const firstLine = newlineIdx === -1 ? text : text.slice(0, newlineIdx);
|
|
1207
|
+
if (firstLine.trim().length === 0) return null;
|
|
1208
|
+
let parsed;
|
|
1209
|
+
try {
|
|
1210
|
+
parsed = JSON.parse(firstLine);
|
|
1211
|
+
} catch {
|
|
1212
|
+
return null;
|
|
1213
|
+
}
|
|
1214
|
+
if (!isPlainObject(parsed)) return null;
|
|
1215
|
+
const id = typeof parsed["id"] === "string" ? parsed["id"] : null;
|
|
1216
|
+
if (id === null) {
|
|
1217
|
+
const stem = basename2(filePath, ".jsonl");
|
|
1218
|
+
return {
|
|
1219
|
+
id: stem,
|
|
1220
|
+
version: typeof parsed["version"] === "number" ? parsed["version"] : 0,
|
|
1221
|
+
timestamp: typeof parsed["timestamp"] === "string" ? parsed["timestamp"] : null,
|
|
1222
|
+
cwd: typeof parsed["cwd"] === "string" ? parsed["cwd"] : null,
|
|
1223
|
+
parentSession: typeof parsed["parentSession"] === "string" ? parsed["parentSession"] : null,
|
|
1224
|
+
filePath,
|
|
1225
|
+
mtimeMs: stats.mtimeMs
|
|
1226
|
+
};
|
|
1227
|
+
}
|
|
1228
|
+
return {
|
|
1229
|
+
id,
|
|
1230
|
+
version: typeof parsed["version"] === "number" ? parsed["version"] : 0,
|
|
1231
|
+
timestamp: typeof parsed["timestamp"] === "string" ? parsed["timestamp"] : null,
|
|
1232
|
+
cwd: typeof parsed["cwd"] === "string" ? parsed["cwd"] : null,
|
|
1233
|
+
parentSession: typeof parsed["parentSession"] === "string" ? parsed["parentSession"] : null,
|
|
1234
|
+
filePath,
|
|
1235
|
+
mtimeMs: stats.mtimeMs
|
|
1236
|
+
};
|
|
1237
|
+
} catch {
|
|
1238
|
+
return null;
|
|
1239
|
+
} finally {
|
|
1240
|
+
if (handle !== null) {
|
|
1241
|
+
await handle.close().catch(() => {
|
|
1242
|
+
});
|
|
1243
|
+
}
|
|
1244
|
+
}
|
|
1245
|
+
}
|
|
802
1246
|
|
|
803
1247
|
// src/core/harness/index.ts
|
|
804
1248
|
function getHarnessFor(provider) {
|
|
@@ -1105,63 +1549,395 @@ async function removeYamlConfig(filePath, configKey, serverName) {
|
|
|
1105
1549
|
if (typeof next !== "object" || next === null) return false;
|
|
1106
1550
|
current = next;
|
|
1107
1551
|
}
|
|
1108
|
-
if (!(serverName in current)) return false;
|
|
1109
|
-
delete current[serverName];
|
|
1110
|
-
const content = yaml.dump(existing, {
|
|
1111
|
-
indent: 2,
|
|
1112
|
-
lineWidth: -1,
|
|
1113
|
-
noRefs: true,
|
|
1114
|
-
sortKeys: false
|
|
1115
|
-
});
|
|
1116
|
-
await writeFile4(filePath, content, "utf-8");
|
|
1117
|
-
return true;
|
|
1118
|
-
}
|
|
1119
|
-
|
|
1120
|
-
// src/core/formats/index.ts
|
|
1121
|
-
async function readConfig(filePath, format) {
|
|
1122
|
-
debug(`reading config: ${filePath} (format: ${format})`);
|
|
1123
|
-
switch (format) {
|
|
1124
|
-
case "json":
|
|
1125
|
-
case "jsonc":
|
|
1126
|
-
return readJsonConfig(filePath);
|
|
1127
|
-
case "yaml":
|
|
1128
|
-
return readYamlConfig(filePath);
|
|
1129
|
-
case "toml":
|
|
1130
|
-
return readTomlConfig(filePath);
|
|
1131
|
-
default:
|
|
1132
|
-
throw new Error(`Unsupported config format: ${format}`);
|
|
1552
|
+
if (!(serverName in current)) return false;
|
|
1553
|
+
delete current[serverName];
|
|
1554
|
+
const content = yaml.dump(existing, {
|
|
1555
|
+
indent: 2,
|
|
1556
|
+
lineWidth: -1,
|
|
1557
|
+
noRefs: true,
|
|
1558
|
+
sortKeys: false
|
|
1559
|
+
});
|
|
1560
|
+
await writeFile4(filePath, content, "utf-8");
|
|
1561
|
+
return true;
|
|
1562
|
+
}
|
|
1563
|
+
|
|
1564
|
+
// src/core/formats/index.ts
|
|
1565
|
+
async function readConfig(filePath, format) {
|
|
1566
|
+
debug(`reading config: ${filePath} (format: ${format})`);
|
|
1567
|
+
switch (format) {
|
|
1568
|
+
case "json":
|
|
1569
|
+
case "jsonc":
|
|
1570
|
+
return readJsonConfig(filePath);
|
|
1571
|
+
case "yaml":
|
|
1572
|
+
return readYamlConfig(filePath);
|
|
1573
|
+
case "toml":
|
|
1574
|
+
return readTomlConfig(filePath);
|
|
1575
|
+
default:
|
|
1576
|
+
throw new Error(`Unsupported config format: ${format}`);
|
|
1577
|
+
}
|
|
1578
|
+
}
|
|
1579
|
+
async function writeConfig(filePath, format, key, serverName, serverConfig) {
|
|
1580
|
+
debug(`writing config: ${filePath} (format: ${format}, key: ${key}, server: ${serverName})`);
|
|
1581
|
+
switch (format) {
|
|
1582
|
+
case "json":
|
|
1583
|
+
case "jsonc":
|
|
1584
|
+
return writeJsonConfig(filePath, key, serverName, serverConfig);
|
|
1585
|
+
case "yaml":
|
|
1586
|
+
return writeYamlConfig(filePath, key, serverName, serverConfig);
|
|
1587
|
+
case "toml":
|
|
1588
|
+
return writeTomlConfig(filePath, key, serverName, serverConfig);
|
|
1589
|
+
default:
|
|
1590
|
+
throw new Error(`Unsupported config format: ${format}`);
|
|
1591
|
+
}
|
|
1592
|
+
}
|
|
1593
|
+
async function removeConfig(filePath, format, key, serverName) {
|
|
1594
|
+
switch (format) {
|
|
1595
|
+
case "json":
|
|
1596
|
+
case "jsonc":
|
|
1597
|
+
return removeJsonConfig(filePath, key, serverName);
|
|
1598
|
+
case "yaml":
|
|
1599
|
+
return removeYamlConfig(filePath, key, serverName);
|
|
1600
|
+
case "toml":
|
|
1601
|
+
return removeTomlConfig(filePath, key, serverName);
|
|
1602
|
+
default:
|
|
1603
|
+
throw new Error(`Unsupported config format: ${format}`);
|
|
1604
|
+
}
|
|
1605
|
+
}
|
|
1606
|
+
|
|
1607
|
+
// src/core/mcp/reader.ts
|
|
1608
|
+
import { existsSync as existsSync8 } from "fs";
|
|
1609
|
+
import { stat as stat2 } from "fs/promises";
|
|
1610
|
+
function resolveMcpConfigPath(provider, scope, projectDir) {
|
|
1611
|
+
if (provider.capabilities.mcp === null) return null;
|
|
1612
|
+
return resolveProviderConfigPath(provider, scope, projectDir);
|
|
1613
|
+
}
|
|
1614
|
+
async function listMcpServers(provider, scope, projectDir) {
|
|
1615
|
+
const mcp = provider.capabilities.mcp;
|
|
1616
|
+
if (mcp === null) return [];
|
|
1617
|
+
const configPath = resolveMcpConfigPath(provider, scope, projectDir);
|
|
1618
|
+
if (configPath === null) return [];
|
|
1619
|
+
if (!existsSync8(configPath)) {
|
|
1620
|
+
debug(`mcp.list: ${provider.id} (${scope}) \u2014 config file missing at ${configPath}`);
|
|
1621
|
+
return [];
|
|
1622
|
+
}
|
|
1623
|
+
let parsed;
|
|
1624
|
+
try {
|
|
1625
|
+
parsed = await readConfig(configPath, mcp.configFormat);
|
|
1626
|
+
} catch (err) {
|
|
1627
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
1628
|
+
debug(`mcp.list: ${provider.id} parse failed at ${configPath}: ${message}`);
|
|
1629
|
+
return [];
|
|
1630
|
+
}
|
|
1631
|
+
const servers = getNestedValue(parsed, mcp.configKey);
|
|
1632
|
+
if (servers === void 0 || servers === null || typeof servers !== "object") return [];
|
|
1633
|
+
const out = [];
|
|
1634
|
+
for (const [name, raw] of Object.entries(servers)) {
|
|
1635
|
+
out.push({
|
|
1636
|
+
name,
|
|
1637
|
+
providerId: provider.id,
|
|
1638
|
+
providerName: provider.toolName,
|
|
1639
|
+
scope,
|
|
1640
|
+
configPath,
|
|
1641
|
+
config: raw ?? {}
|
|
1642
|
+
});
|
|
1643
|
+
}
|
|
1644
|
+
return out;
|
|
1645
|
+
}
|
|
1646
|
+
async function listAllMcpServers(scope, projectDir) {
|
|
1647
|
+
const out = /* @__PURE__ */ new Map();
|
|
1648
|
+
for (const provider of getAllProviders()) {
|
|
1649
|
+
if (provider.capabilities.mcp === null) continue;
|
|
1650
|
+
const entries = await listMcpServers(provider, scope, projectDir);
|
|
1651
|
+
out.set(provider.id, entries);
|
|
1652
|
+
}
|
|
1653
|
+
return out;
|
|
1654
|
+
}
|
|
1655
|
+
async function detectMcpInstallations(scope, projectDir) {
|
|
1656
|
+
const out = [];
|
|
1657
|
+
for (const provider of getAllProviders()) {
|
|
1658
|
+
const mcp = provider.capabilities.mcp;
|
|
1659
|
+
if (mcp === null) continue;
|
|
1660
|
+
const configPath = resolveMcpConfigPath(provider, scope, projectDir);
|
|
1661
|
+
if (configPath === null) continue;
|
|
1662
|
+
const exists = existsSync8(configPath);
|
|
1663
|
+
let serverCount = null;
|
|
1664
|
+
let lastModified = null;
|
|
1665
|
+
if (exists) {
|
|
1666
|
+
try {
|
|
1667
|
+
const stats = await stat2(configPath);
|
|
1668
|
+
lastModified = stats.mtime.toISOString();
|
|
1669
|
+
} catch {
|
|
1670
|
+
lastModified = null;
|
|
1671
|
+
}
|
|
1672
|
+
const entries = await listMcpServers(provider, scope, projectDir);
|
|
1673
|
+
serverCount = entries.length;
|
|
1674
|
+
}
|
|
1675
|
+
out.push({
|
|
1676
|
+
providerId: provider.id,
|
|
1677
|
+
providerName: provider.toolName,
|
|
1678
|
+
scope,
|
|
1679
|
+
configPath,
|
|
1680
|
+
exists,
|
|
1681
|
+
serverCount,
|
|
1682
|
+
lastModified
|
|
1683
|
+
});
|
|
1684
|
+
}
|
|
1685
|
+
return out;
|
|
1686
|
+
}
|
|
1687
|
+
|
|
1688
|
+
// src/core/mcp/installer.ts
|
|
1689
|
+
async function installMcpServer(provider, serverName, config, opts) {
|
|
1690
|
+
const mcp = provider.capabilities.mcp;
|
|
1691
|
+
if (mcp === null) {
|
|
1692
|
+
throw new Error(`Provider ${provider.id} does not declare an MCP capability.`);
|
|
1693
|
+
}
|
|
1694
|
+
const configPath = resolveMcpConfigPath(provider, opts.scope, opts.projectDir);
|
|
1695
|
+
if (configPath === null) {
|
|
1696
|
+
throw new Error(
|
|
1697
|
+
`Provider ${provider.id} has no ${opts.scope}-scoped MCP config path available.`
|
|
1698
|
+
);
|
|
1699
|
+
}
|
|
1700
|
+
debug(
|
|
1701
|
+
`mcp.install: ${provider.id} ${serverName} \u2192 ${configPath} (format=${mcp.configFormat}, key=${mcp.configKey})`
|
|
1702
|
+
);
|
|
1703
|
+
const existing = await listMcpServers(provider, opts.scope, opts.projectDir);
|
|
1704
|
+
const conflicted = existing.some((e) => e.name === serverName);
|
|
1705
|
+
if (conflicted && opts.force !== true) {
|
|
1706
|
+
return {
|
|
1707
|
+
installed: false,
|
|
1708
|
+
conflicted: true,
|
|
1709
|
+
sourcePath: configPath,
|
|
1710
|
+
providerId: provider.id,
|
|
1711
|
+
serverName
|
|
1712
|
+
};
|
|
1713
|
+
}
|
|
1714
|
+
await writeConfig(configPath, mcp.configFormat, mcp.configKey, serverName, config);
|
|
1715
|
+
return {
|
|
1716
|
+
installed: true,
|
|
1717
|
+
conflicted,
|
|
1718
|
+
sourcePath: configPath,
|
|
1719
|
+
providerId: provider.id,
|
|
1720
|
+
serverName
|
|
1721
|
+
};
|
|
1722
|
+
}
|
|
1723
|
+
|
|
1724
|
+
// src/core/mcp/remover.ts
|
|
1725
|
+
import { existsSync as existsSync9 } from "fs";
|
|
1726
|
+
async function removeMcpServer(provider, serverName, opts) {
|
|
1727
|
+
const mcp = provider.capabilities.mcp;
|
|
1728
|
+
if (mcp === null) {
|
|
1729
|
+
return {
|
|
1730
|
+
providerId: provider.id,
|
|
1731
|
+
serverName,
|
|
1732
|
+
sourcePath: null,
|
|
1733
|
+
removed: false,
|
|
1734
|
+
reason: "no-mcp-capability"
|
|
1735
|
+
};
|
|
1736
|
+
}
|
|
1737
|
+
const configPath = resolveMcpConfigPath(provider, opts.scope, opts.projectDir);
|
|
1738
|
+
if (configPath === null) {
|
|
1739
|
+
return {
|
|
1740
|
+
providerId: provider.id,
|
|
1741
|
+
serverName,
|
|
1742
|
+
sourcePath: null,
|
|
1743
|
+
removed: false,
|
|
1744
|
+
reason: "no-config-path"
|
|
1745
|
+
};
|
|
1746
|
+
}
|
|
1747
|
+
if (!existsSync9(configPath)) {
|
|
1748
|
+
return {
|
|
1749
|
+
providerId: provider.id,
|
|
1750
|
+
serverName,
|
|
1751
|
+
sourcePath: configPath,
|
|
1752
|
+
removed: false,
|
|
1753
|
+
reason: "file-missing"
|
|
1754
|
+
};
|
|
1755
|
+
}
|
|
1756
|
+
debug(`mcp.remove: ${provider.id} ${serverName} \u2192 ${configPath}`);
|
|
1757
|
+
const removed = await removeConfig(configPath, mcp.configFormat, mcp.configKey, serverName);
|
|
1758
|
+
return {
|
|
1759
|
+
providerId: provider.id,
|
|
1760
|
+
serverName,
|
|
1761
|
+
sourcePath: configPath,
|
|
1762
|
+
removed,
|
|
1763
|
+
reason: removed ? null : "entry-missing"
|
|
1764
|
+
};
|
|
1765
|
+
}
|
|
1766
|
+
async function removeMcpServerFromAll(serverName, opts) {
|
|
1767
|
+
const out = [];
|
|
1768
|
+
for (const provider of getAllProviders()) {
|
|
1769
|
+
if (provider.capabilities.mcp === null) continue;
|
|
1770
|
+
out.push(await removeMcpServer(provider, serverName, opts));
|
|
1771
|
+
}
|
|
1772
|
+
return out;
|
|
1773
|
+
}
|
|
1774
|
+
|
|
1775
|
+
// src/core/sources/parser.ts
|
|
1776
|
+
var GITHUB_SHORTHAND = /^([a-zA-Z0-9_.-]+)\/([a-zA-Z0-9_.-]+)(?:\/(.+))?$/;
|
|
1777
|
+
var GITHUB_URL = /^https?:\/\/(?:www\.)?github\.com\/([^/]+)\/([^/]+)(?:\/(?:tree|blob)\/([^/]+)(?:\/(.+))?)?/;
|
|
1778
|
+
var GITLAB_URL = /^https?:\/\/(?:www\.)?gitlab\.com\/([^/]+)\/([^/]+)(?:\/-\/(?:tree|blob)\/([^/]+)(?:\/(.+))?)?/;
|
|
1779
|
+
var HTTP_URL = /^https?:\/\//;
|
|
1780
|
+
var NPM_SCOPED = /^@[a-zA-Z0-9_.-]+\/[a-zA-Z0-9_.-]+$/;
|
|
1781
|
+
var NPM_PACKAGE = /^[a-zA-Z0-9_.-]+$/;
|
|
1782
|
+
var LIBRARY_SKILL = /^(@[a-zA-Z0-9_.-]+\/[a-zA-Z0-9_.-]+|[a-zA-Z0-9_.-]+):([a-zA-Z0-9_.-]+)$/;
|
|
1783
|
+
function inferName(source, type) {
|
|
1784
|
+
if (type === "library") {
|
|
1785
|
+
const match = source.match(LIBRARY_SKILL);
|
|
1786
|
+
return match?.[2] ?? source;
|
|
1787
|
+
}
|
|
1788
|
+
if (type === "remote") {
|
|
1789
|
+
try {
|
|
1790
|
+
const url = new URL(source);
|
|
1791
|
+
const parts = url.hostname.split(".");
|
|
1792
|
+
if (parts.length >= 2) {
|
|
1793
|
+
const fallback = parts[0] ?? source;
|
|
1794
|
+
const secondLevel = parts[parts.length - 2] ?? fallback;
|
|
1795
|
+
const brand = parts.length === 3 ? secondLevel : fallback;
|
|
1796
|
+
if (brand !== "www" && brand !== "api" && brand !== "mcp") {
|
|
1797
|
+
return brand;
|
|
1798
|
+
}
|
|
1799
|
+
return secondLevel;
|
|
1800
|
+
}
|
|
1801
|
+
return parts[0] ?? source;
|
|
1802
|
+
} catch {
|
|
1803
|
+
return source;
|
|
1804
|
+
}
|
|
1805
|
+
}
|
|
1806
|
+
if (type === "package") {
|
|
1807
|
+
let name = source.replace(/^@[^/]+\//, "");
|
|
1808
|
+
name = name.replace(/^mcp-server-/, "");
|
|
1809
|
+
name = name.replace(/^server-/, "");
|
|
1810
|
+
name = name.replace(/-mcp$/, "");
|
|
1811
|
+
name = name.replace(/-server$/, "");
|
|
1812
|
+
return name;
|
|
1813
|
+
}
|
|
1814
|
+
if (type === "github" || type === "gitlab") {
|
|
1815
|
+
const match = source.match(/\/([^/]+?)(?:\.git)?$/);
|
|
1816
|
+
return match?.[1] ?? source;
|
|
1817
|
+
}
|
|
1818
|
+
if (type === "local") {
|
|
1819
|
+
const normalized = source.replace(/\\/g, "/").replace(/\/+$/, "");
|
|
1820
|
+
const lastSegment = normalized.split("/").pop();
|
|
1821
|
+
return lastSegment ?? source;
|
|
1822
|
+
}
|
|
1823
|
+
if (type === "command") {
|
|
1824
|
+
const parts = source.split(/\s+/);
|
|
1825
|
+
const command = parts.find(
|
|
1826
|
+
(p) => !p.startsWith("-") && p !== "npx" && p !== "node" && p !== "python" && p !== "python3"
|
|
1827
|
+
);
|
|
1828
|
+
return command ?? parts[0] ?? source;
|
|
1829
|
+
}
|
|
1830
|
+
return source;
|
|
1831
|
+
}
|
|
1832
|
+
function parseSource(input) {
|
|
1833
|
+
const ghUrlMatch = input.match(GITHUB_URL);
|
|
1834
|
+
if (ghUrlMatch) {
|
|
1835
|
+
const owner = ghUrlMatch[1];
|
|
1836
|
+
const repo = ghUrlMatch[2];
|
|
1837
|
+
const path = ghUrlMatch[4];
|
|
1838
|
+
if (!owner || !repo) {
|
|
1839
|
+
return { type: "command", value: input, inferredName: inferName(input, "command") };
|
|
1840
|
+
}
|
|
1841
|
+
const inferredName = path ? path.split("/").pop() ?? repo : repo;
|
|
1842
|
+
return {
|
|
1843
|
+
type: "github",
|
|
1844
|
+
value: input,
|
|
1845
|
+
inferredName,
|
|
1846
|
+
owner,
|
|
1847
|
+
repo,
|
|
1848
|
+
ref: ghUrlMatch[3],
|
|
1849
|
+
path
|
|
1850
|
+
};
|
|
1851
|
+
}
|
|
1852
|
+
const glUrlMatch = input.match(GITLAB_URL);
|
|
1853
|
+
if (glUrlMatch) {
|
|
1854
|
+
const owner = glUrlMatch[1];
|
|
1855
|
+
const repo = glUrlMatch[2];
|
|
1856
|
+
const path = glUrlMatch[4];
|
|
1857
|
+
if (!owner || !repo) {
|
|
1858
|
+
return { type: "command", value: input, inferredName: inferName(input, "command") };
|
|
1859
|
+
}
|
|
1860
|
+
const inferredName = path ? path.split("/").pop() ?? repo : repo;
|
|
1861
|
+
return {
|
|
1862
|
+
type: "gitlab",
|
|
1863
|
+
value: input,
|
|
1864
|
+
inferredName,
|
|
1865
|
+
owner,
|
|
1866
|
+
repo,
|
|
1867
|
+
ref: glUrlMatch[3],
|
|
1868
|
+
path
|
|
1869
|
+
};
|
|
1870
|
+
}
|
|
1871
|
+
if (HTTP_URL.test(input)) {
|
|
1872
|
+
return {
|
|
1873
|
+
type: "remote",
|
|
1874
|
+
value: input,
|
|
1875
|
+
inferredName: inferName(input, "remote")
|
|
1876
|
+
};
|
|
1877
|
+
}
|
|
1878
|
+
if (input.startsWith("/") || input.startsWith("./") || input.startsWith("../") || input.startsWith("~")) {
|
|
1879
|
+
return {
|
|
1880
|
+
type: "local",
|
|
1881
|
+
value: input,
|
|
1882
|
+
inferredName: inferName(input, "local")
|
|
1883
|
+
};
|
|
1884
|
+
}
|
|
1885
|
+
const ghShorthand = input.match(GITHUB_SHORTHAND);
|
|
1886
|
+
if (ghShorthand && !NPM_SCOPED.test(input)) {
|
|
1887
|
+
const owner = ghShorthand[1];
|
|
1888
|
+
const repo = ghShorthand[2];
|
|
1889
|
+
const path = ghShorthand[3];
|
|
1890
|
+
if (!owner || !repo) {
|
|
1891
|
+
return { type: "command", value: input, inferredName: inferName(input, "command") };
|
|
1892
|
+
}
|
|
1893
|
+
const inferredName = path ? path.split("/").pop() ?? repo : repo;
|
|
1894
|
+
return {
|
|
1895
|
+
type: "github",
|
|
1896
|
+
value: `https://github.com/${owner}/${repo}`,
|
|
1897
|
+
inferredName,
|
|
1898
|
+
owner,
|
|
1899
|
+
repo,
|
|
1900
|
+
path
|
|
1901
|
+
};
|
|
1902
|
+
}
|
|
1903
|
+
const libraryMatch = input.match(LIBRARY_SKILL);
|
|
1904
|
+
if (libraryMatch) {
|
|
1905
|
+
return {
|
|
1906
|
+
type: "library",
|
|
1907
|
+
value: input,
|
|
1908
|
+
inferredName: inferName(input, "library"),
|
|
1909
|
+
owner: libraryMatch[1],
|
|
1910
|
+
// This will be the package name, e.g. @cleocode/skills
|
|
1911
|
+
repo: libraryMatch[2]
|
|
1912
|
+
// This will be the skill name, e.g. ct-research-agent
|
|
1913
|
+
};
|
|
1914
|
+
}
|
|
1915
|
+
if (NPM_SCOPED.test(input)) {
|
|
1916
|
+
return {
|
|
1917
|
+
type: "package",
|
|
1918
|
+
value: input,
|
|
1919
|
+
inferredName: inferName(input, "package")
|
|
1920
|
+
};
|
|
1133
1921
|
}
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
return writeJsonConfig(filePath, key, serverName, serverConfig);
|
|
1141
|
-
case "yaml":
|
|
1142
|
-
return writeYamlConfig(filePath, key, serverName, serverConfig);
|
|
1143
|
-
case "toml":
|
|
1144
|
-
return writeTomlConfig(filePath, key, serverName, serverConfig);
|
|
1145
|
-
default:
|
|
1146
|
-
throw new Error(`Unsupported config format: ${format}`);
|
|
1922
|
+
if (NPM_PACKAGE.test(input) && !input.includes(" ")) {
|
|
1923
|
+
return {
|
|
1924
|
+
type: "package",
|
|
1925
|
+
value: input,
|
|
1926
|
+
inferredName: inferName(input, "package")
|
|
1927
|
+
};
|
|
1147
1928
|
}
|
|
1929
|
+
return {
|
|
1930
|
+
type: "command",
|
|
1931
|
+
value: input,
|
|
1932
|
+
inferredName: inferName(input, "command")
|
|
1933
|
+
};
|
|
1148
1934
|
}
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
case "json":
|
|
1152
|
-
case "jsonc":
|
|
1153
|
-
return removeJsonConfig(filePath, key, serverName);
|
|
1154
|
-
case "yaml":
|
|
1155
|
-
return removeYamlConfig(filePath, key, serverName);
|
|
1156
|
-
case "toml":
|
|
1157
|
-
return removeTomlConfig(filePath, key, serverName);
|
|
1158
|
-
default:
|
|
1159
|
-
throw new Error(`Unsupported config format: ${format}`);
|
|
1160
|
-
}
|
|
1935
|
+
function isMarketplaceScoped(input) {
|
|
1936
|
+
return /^@[a-zA-Z0-9_.-]+\/[a-zA-Z0-9_.-]+$/.test(input);
|
|
1161
1937
|
}
|
|
1162
1938
|
|
|
1163
1939
|
// src/core/skills/audit/scanner.ts
|
|
1164
|
-
import { existsSync as
|
|
1940
|
+
import { existsSync as existsSync10 } from "fs";
|
|
1165
1941
|
import { readFile as readFile5 } from "fs/promises";
|
|
1166
1942
|
|
|
1167
1943
|
// src/core/skills/audit/rules.ts
|
|
@@ -1534,7 +2310,7 @@ var SEVERITY_WEIGHTS = {
|
|
|
1534
2310
|
info: 0
|
|
1535
2311
|
};
|
|
1536
2312
|
async function scanFile(filePath, rules) {
|
|
1537
|
-
if (!
|
|
2313
|
+
if (!existsSync10(filePath)) {
|
|
1538
2314
|
return { file: filePath, findings: [], score: 100, passed: true };
|
|
1539
2315
|
}
|
|
1540
2316
|
const content = await readFile5(filePath, "utf-8");
|
|
@@ -1568,14 +2344,14 @@ async function scanFile(filePath, rules) {
|
|
|
1568
2344
|
}
|
|
1569
2345
|
async function scanDirectory(dirPath) {
|
|
1570
2346
|
const { readdir: readdir3 } = await import("fs/promises");
|
|
1571
|
-
const { join:
|
|
1572
|
-
if (!
|
|
2347
|
+
const { join: join9 } = await import("path");
|
|
2348
|
+
if (!existsSync10(dirPath)) return [];
|
|
1573
2349
|
const entries = await readdir3(dirPath, { withFileTypes: true });
|
|
1574
2350
|
const results = [];
|
|
1575
2351
|
for (const entry of entries) {
|
|
1576
2352
|
if (entry.isDirectory() || entry.isSymbolicLink()) {
|
|
1577
|
-
const skillFile =
|
|
1578
|
-
if (
|
|
2353
|
+
const skillFile = join9(dirPath, entry.name, "SKILL.md");
|
|
2354
|
+
if (existsSync10(skillFile)) {
|
|
1579
2355
|
results.push(await scanFile(skillFile));
|
|
1580
2356
|
}
|
|
1581
2357
|
}
|
|
@@ -1626,178 +2402,14 @@ function toSarif(results) {
|
|
|
1626
2402
|
};
|
|
1627
2403
|
}
|
|
1628
2404
|
|
|
1629
|
-
// src/core/sources/parser.ts
|
|
1630
|
-
var GITHUB_SHORTHAND = /^([a-zA-Z0-9_.-]+)\/([a-zA-Z0-9_.-]+)(?:\/(.+))?$/;
|
|
1631
|
-
var GITHUB_URL = /^https?:\/\/(?:www\.)?github\.com\/([^/]+)\/([^/]+)(?:\/(?:tree|blob)\/([^/]+)(?:\/(.+))?)?/;
|
|
1632
|
-
var GITLAB_URL = /^https?:\/\/(?:www\.)?gitlab\.com\/([^/]+)\/([^/]+)(?:\/-\/(?:tree|blob)\/([^/]+)(?:\/(.+))?)?/;
|
|
1633
|
-
var HTTP_URL = /^https?:\/\//;
|
|
1634
|
-
var NPM_SCOPED = /^@[a-zA-Z0-9_.-]+\/[a-zA-Z0-9_.-]+$/;
|
|
1635
|
-
var NPM_PACKAGE = /^[a-zA-Z0-9_.-]+$/;
|
|
1636
|
-
var LIBRARY_SKILL = /^(@[a-zA-Z0-9_.-]+\/[a-zA-Z0-9_.-]+|[a-zA-Z0-9_.-]+):([a-zA-Z0-9_.-]+)$/;
|
|
1637
|
-
function inferName(source, type) {
|
|
1638
|
-
if (type === "library") {
|
|
1639
|
-
const match = source.match(LIBRARY_SKILL);
|
|
1640
|
-
return match?.[2] ?? source;
|
|
1641
|
-
}
|
|
1642
|
-
if (type === "remote") {
|
|
1643
|
-
try {
|
|
1644
|
-
const url = new URL(source);
|
|
1645
|
-
const parts = url.hostname.split(".");
|
|
1646
|
-
if (parts.length >= 2) {
|
|
1647
|
-
const fallback = parts[0] ?? source;
|
|
1648
|
-
const secondLevel = parts[parts.length - 2] ?? fallback;
|
|
1649
|
-
const brand = parts.length === 3 ? secondLevel : fallback;
|
|
1650
|
-
if (brand !== "www" && brand !== "api" && brand !== "mcp") {
|
|
1651
|
-
return brand;
|
|
1652
|
-
}
|
|
1653
|
-
return secondLevel;
|
|
1654
|
-
}
|
|
1655
|
-
return parts[0] ?? source;
|
|
1656
|
-
} catch {
|
|
1657
|
-
return source;
|
|
1658
|
-
}
|
|
1659
|
-
}
|
|
1660
|
-
if (type === "package") {
|
|
1661
|
-
let name = source.replace(/^@[^/]+\//, "");
|
|
1662
|
-
name = name.replace(/^mcp-server-/, "");
|
|
1663
|
-
name = name.replace(/^server-/, "");
|
|
1664
|
-
name = name.replace(/-mcp$/, "");
|
|
1665
|
-
name = name.replace(/-server$/, "");
|
|
1666
|
-
return name;
|
|
1667
|
-
}
|
|
1668
|
-
if (type === "github" || type === "gitlab") {
|
|
1669
|
-
const match = source.match(/\/([^/]+?)(?:\.git)?$/);
|
|
1670
|
-
return match?.[1] ?? source;
|
|
1671
|
-
}
|
|
1672
|
-
if (type === "local") {
|
|
1673
|
-
const normalized = source.replace(/\\/g, "/").replace(/\/+$/, "");
|
|
1674
|
-
const lastSegment = normalized.split("/").pop();
|
|
1675
|
-
return lastSegment ?? source;
|
|
1676
|
-
}
|
|
1677
|
-
if (type === "command") {
|
|
1678
|
-
const parts = source.split(/\s+/);
|
|
1679
|
-
const command = parts.find(
|
|
1680
|
-
(p) => !p.startsWith("-") && p !== "npx" && p !== "node" && p !== "python" && p !== "python3"
|
|
1681
|
-
);
|
|
1682
|
-
return command ?? parts[0] ?? source;
|
|
1683
|
-
}
|
|
1684
|
-
return source;
|
|
1685
|
-
}
|
|
1686
|
-
function parseSource(input) {
|
|
1687
|
-
const ghUrlMatch = input.match(GITHUB_URL);
|
|
1688
|
-
if (ghUrlMatch) {
|
|
1689
|
-
const owner = ghUrlMatch[1];
|
|
1690
|
-
const repo = ghUrlMatch[2];
|
|
1691
|
-
const path = ghUrlMatch[4];
|
|
1692
|
-
if (!owner || !repo) {
|
|
1693
|
-
return { type: "command", value: input, inferredName: inferName(input, "command") };
|
|
1694
|
-
}
|
|
1695
|
-
const inferredName = path ? path.split("/").pop() ?? repo : repo;
|
|
1696
|
-
return {
|
|
1697
|
-
type: "github",
|
|
1698
|
-
value: input,
|
|
1699
|
-
inferredName,
|
|
1700
|
-
owner,
|
|
1701
|
-
repo,
|
|
1702
|
-
ref: ghUrlMatch[3],
|
|
1703
|
-
path
|
|
1704
|
-
};
|
|
1705
|
-
}
|
|
1706
|
-
const glUrlMatch = input.match(GITLAB_URL);
|
|
1707
|
-
if (glUrlMatch) {
|
|
1708
|
-
const owner = glUrlMatch[1];
|
|
1709
|
-
const repo = glUrlMatch[2];
|
|
1710
|
-
const path = glUrlMatch[4];
|
|
1711
|
-
if (!owner || !repo) {
|
|
1712
|
-
return { type: "command", value: input, inferredName: inferName(input, "command") };
|
|
1713
|
-
}
|
|
1714
|
-
const inferredName = path ? path.split("/").pop() ?? repo : repo;
|
|
1715
|
-
return {
|
|
1716
|
-
type: "gitlab",
|
|
1717
|
-
value: input,
|
|
1718
|
-
inferredName,
|
|
1719
|
-
owner,
|
|
1720
|
-
repo,
|
|
1721
|
-
ref: glUrlMatch[3],
|
|
1722
|
-
path
|
|
1723
|
-
};
|
|
1724
|
-
}
|
|
1725
|
-
if (HTTP_URL.test(input)) {
|
|
1726
|
-
return {
|
|
1727
|
-
type: "remote",
|
|
1728
|
-
value: input,
|
|
1729
|
-
inferredName: inferName(input, "remote")
|
|
1730
|
-
};
|
|
1731
|
-
}
|
|
1732
|
-
if (input.startsWith("/") || input.startsWith("./") || input.startsWith("../") || input.startsWith("~")) {
|
|
1733
|
-
return {
|
|
1734
|
-
type: "local",
|
|
1735
|
-
value: input,
|
|
1736
|
-
inferredName: inferName(input, "local")
|
|
1737
|
-
};
|
|
1738
|
-
}
|
|
1739
|
-
const ghShorthand = input.match(GITHUB_SHORTHAND);
|
|
1740
|
-
if (ghShorthand && !NPM_SCOPED.test(input)) {
|
|
1741
|
-
const owner = ghShorthand[1];
|
|
1742
|
-
const repo = ghShorthand[2];
|
|
1743
|
-
const path = ghShorthand[3];
|
|
1744
|
-
if (!owner || !repo) {
|
|
1745
|
-
return { type: "command", value: input, inferredName: inferName(input, "command") };
|
|
1746
|
-
}
|
|
1747
|
-
const inferredName = path ? path.split("/").pop() ?? repo : repo;
|
|
1748
|
-
return {
|
|
1749
|
-
type: "github",
|
|
1750
|
-
value: `https://github.com/${owner}/${repo}`,
|
|
1751
|
-
inferredName,
|
|
1752
|
-
owner,
|
|
1753
|
-
repo,
|
|
1754
|
-
path
|
|
1755
|
-
};
|
|
1756
|
-
}
|
|
1757
|
-
const libraryMatch = input.match(LIBRARY_SKILL);
|
|
1758
|
-
if (libraryMatch) {
|
|
1759
|
-
return {
|
|
1760
|
-
type: "library",
|
|
1761
|
-
value: input,
|
|
1762
|
-
inferredName: inferName(input, "library"),
|
|
1763
|
-
owner: libraryMatch[1],
|
|
1764
|
-
// This will be the package name, e.g. @cleocode/skills
|
|
1765
|
-
repo: libraryMatch[2]
|
|
1766
|
-
// This will be the skill name, e.g. ct-research-agent
|
|
1767
|
-
};
|
|
1768
|
-
}
|
|
1769
|
-
if (NPM_SCOPED.test(input)) {
|
|
1770
|
-
return {
|
|
1771
|
-
type: "package",
|
|
1772
|
-
value: input,
|
|
1773
|
-
inferredName: inferName(input, "package")
|
|
1774
|
-
};
|
|
1775
|
-
}
|
|
1776
|
-
if (NPM_PACKAGE.test(input) && !input.includes(" ")) {
|
|
1777
|
-
return {
|
|
1778
|
-
type: "package",
|
|
1779
|
-
value: input,
|
|
1780
|
-
inferredName: inferName(input, "package")
|
|
1781
|
-
};
|
|
1782
|
-
}
|
|
1783
|
-
return {
|
|
1784
|
-
type: "command",
|
|
1785
|
-
value: input,
|
|
1786
|
-
inferredName: inferName(input, "command")
|
|
1787
|
-
};
|
|
1788
|
-
}
|
|
1789
|
-
function isMarketplaceScoped(input) {
|
|
1790
|
-
return /^@[a-zA-Z0-9_.-]+\/[a-zA-Z0-9_.-]+$/.test(input);
|
|
1791
|
-
}
|
|
1792
|
-
|
|
1793
2405
|
// src/core/skills/lock.ts
|
|
1794
2406
|
import { execFile } from "child_process";
|
|
1795
2407
|
import { promisify } from "util";
|
|
1796
2408
|
import { simpleGit } from "simple-git";
|
|
1797
2409
|
|
|
1798
2410
|
// src/core/lock-utils.ts
|
|
1799
|
-
import { existsSync as
|
|
1800
|
-
import { mkdir as mkdir4, open, readFile as readFile6, rename as rename2, rm as rm4, stat, writeFile as writeFile5 } from "fs/promises";
|
|
2411
|
+
import { existsSync as existsSync11 } from "fs";
|
|
2412
|
+
import { mkdir as mkdir4, open as open2, readFile as readFile6, rename as rename2, rm as rm4, stat as stat3, writeFile as writeFile5 } from "fs/promises";
|
|
1801
2413
|
var LOCK_GUARD_PATH = `${LOCK_FILE_PATH}.lock`;
|
|
1802
2414
|
var STALE_LOCK_MS = 5e3;
|
|
1803
2415
|
function sleep(ms) {
|
|
@@ -1805,7 +2417,7 @@ function sleep(ms) {
|
|
|
1805
2417
|
}
|
|
1806
2418
|
async function removeStaleLock() {
|
|
1807
2419
|
try {
|
|
1808
|
-
const info = await
|
|
2420
|
+
const info = await stat3(LOCK_GUARD_PATH);
|
|
1809
2421
|
if (Date.now() - info.mtimeMs > STALE_LOCK_MS) {
|
|
1810
2422
|
await rm4(LOCK_GUARD_PATH, { force: true });
|
|
1811
2423
|
return true;
|
|
@@ -1818,7 +2430,7 @@ async function acquireLockGuard(retries = 40, delayMs = 25) {
|
|
|
1818
2430
|
await mkdir4(AGENTS_HOME, { recursive: true });
|
|
1819
2431
|
for (let attempt = 0; attempt < retries; attempt += 1) {
|
|
1820
2432
|
try {
|
|
1821
|
-
const handle = await
|
|
2433
|
+
const handle = await open2(LOCK_GUARD_PATH, "wx");
|
|
1822
2434
|
await handle.close();
|
|
1823
2435
|
return;
|
|
1824
2436
|
} catch (error) {
|
|
@@ -1844,7 +2456,7 @@ async function writeLockFileUnsafe(lock) {
|
|
|
1844
2456
|
}
|
|
1845
2457
|
async function readLockFile() {
|
|
1846
2458
|
try {
|
|
1847
|
-
if (!
|
|
2459
|
+
if (!existsSync11(LOCK_FILE_PATH)) {
|
|
1848
2460
|
return { version: 1, skills: {}, mcpServers: {} };
|
|
1849
2461
|
}
|
|
1850
2462
|
const content = await readFile6(LOCK_FILE_PATH, "utf-8");
|
|
@@ -2575,9 +3187,9 @@ async function recommendSkills2(query, criteria, options = {}) {
|
|
|
2575
3187
|
}
|
|
2576
3188
|
|
|
2577
3189
|
// src/core/skills/library-loader.ts
|
|
2578
|
-
import { existsSync as
|
|
3190
|
+
import { existsSync as existsSync12, readdirSync, readFileSync } from "fs";
|
|
2579
3191
|
import { createRequire } from "module";
|
|
2580
|
-
import { basename as
|
|
3192
|
+
import { basename as basename3, dirname as dirname3, join as join6 } from "path";
|
|
2581
3193
|
var require2 = createRequire(import.meta.url);
|
|
2582
3194
|
function loadLibraryFromModule(root) {
|
|
2583
3195
|
let mod;
|
|
@@ -2623,16 +3235,16 @@ function loadLibraryFromModule(root) {
|
|
|
2623
3235
|
return mod;
|
|
2624
3236
|
}
|
|
2625
3237
|
function buildLibraryFromFiles(root) {
|
|
2626
|
-
const catalogPath =
|
|
2627
|
-
if (!
|
|
3238
|
+
const catalogPath = join6(root, "skills.json");
|
|
3239
|
+
if (!existsSync12(catalogPath)) {
|
|
2628
3240
|
throw new Error(`No skills.json found at ${root}`);
|
|
2629
3241
|
}
|
|
2630
3242
|
const catalogData = JSON.parse(readFileSync(catalogPath, "utf-8"));
|
|
2631
3243
|
const entries = catalogData.skills ?? [];
|
|
2632
3244
|
const version = catalogData.version ?? "0.0.0";
|
|
2633
|
-
const manifestPath =
|
|
3245
|
+
const manifestPath = join6(root, "skills", "manifest.json");
|
|
2634
3246
|
let manifest;
|
|
2635
|
-
if (
|
|
3247
|
+
if (existsSync12(manifestPath)) {
|
|
2636
3248
|
manifest = JSON.parse(readFileSync(manifestPath, "utf-8"));
|
|
2637
3249
|
} else {
|
|
2638
3250
|
manifest = {
|
|
@@ -2642,14 +3254,14 @@ function buildLibraryFromFiles(root) {
|
|
|
2642
3254
|
skills: []
|
|
2643
3255
|
};
|
|
2644
3256
|
}
|
|
2645
|
-
const profilesDir =
|
|
3257
|
+
const profilesDir = join6(root, "profiles");
|
|
2646
3258
|
const profiles = /* @__PURE__ */ new Map();
|
|
2647
|
-
if (
|
|
3259
|
+
if (existsSync12(profilesDir)) {
|
|
2648
3260
|
for (const file of readdirSync(profilesDir)) {
|
|
2649
3261
|
if (!file.endsWith(".json")) continue;
|
|
2650
3262
|
try {
|
|
2651
3263
|
const profile = JSON.parse(
|
|
2652
|
-
readFileSync(
|
|
3264
|
+
readFileSync(join6(profilesDir, file), "utf-8")
|
|
2653
3265
|
);
|
|
2654
3266
|
profiles.set(profile.name, profile);
|
|
2655
3267
|
} catch {
|
|
@@ -2663,9 +3275,9 @@ function buildLibraryFromFiles(root) {
|
|
|
2663
3275
|
function getSkillDir2(name) {
|
|
2664
3276
|
const entry = skillMap.get(name);
|
|
2665
3277
|
if (entry) {
|
|
2666
|
-
return dirname3(
|
|
3278
|
+
return dirname3(join6(root, entry.path));
|
|
2667
3279
|
}
|
|
2668
|
-
return
|
|
3280
|
+
return join6(root, "skills", name);
|
|
2669
3281
|
}
|
|
2670
3282
|
function resolveDeps(names, visited = /* @__PURE__ */ new Set()) {
|
|
2671
3283
|
const result = [];
|
|
@@ -2693,8 +3305,8 @@ function buildLibraryFromFiles(root) {
|
|
|
2693
3305
|
return resolveDeps([...new Set(skills)]);
|
|
2694
3306
|
}
|
|
2695
3307
|
function discoverFiles(dir, ext) {
|
|
2696
|
-
if (!
|
|
2697
|
-
return readdirSync(dir).filter((f) => f.endsWith(ext)).map((f) =>
|
|
3308
|
+
if (!existsSync12(dir)) return [];
|
|
3309
|
+
return readdirSync(dir).filter((f) => f.endsWith(ext)).map((f) => basename3(f, ext));
|
|
2698
3310
|
}
|
|
2699
3311
|
const library = {
|
|
2700
3312
|
version,
|
|
@@ -2710,14 +3322,14 @@ function buildLibraryFromFiles(root) {
|
|
|
2710
3322
|
getSkillPath(name) {
|
|
2711
3323
|
const entry = skillMap.get(name);
|
|
2712
3324
|
if (entry) {
|
|
2713
|
-
return
|
|
3325
|
+
return join6(root, entry.path);
|
|
2714
3326
|
}
|
|
2715
|
-
return
|
|
3327
|
+
return join6(root, "skills", name, "SKILL.md");
|
|
2716
3328
|
},
|
|
2717
3329
|
getSkillDir: getSkillDir2,
|
|
2718
3330
|
readSkillContent(name) {
|
|
2719
3331
|
const skillPath = library.getSkillPath(name);
|
|
2720
|
-
if (!
|
|
3332
|
+
if (!existsSync12(skillPath)) {
|
|
2721
3333
|
throw new Error(`Skill content not found: ${skillPath}`);
|
|
2722
3334
|
}
|
|
2723
3335
|
return readFileSync(skillPath, "utf-8");
|
|
@@ -2744,11 +3356,11 @@ function buildLibraryFromFiles(root) {
|
|
|
2744
3356
|
return resolveProfileByName(name);
|
|
2745
3357
|
},
|
|
2746
3358
|
listSharedResources() {
|
|
2747
|
-
return discoverFiles(
|
|
3359
|
+
return discoverFiles(join6(root, "skills", "_shared"), ".md");
|
|
2748
3360
|
},
|
|
2749
3361
|
getSharedResourcePath(name) {
|
|
2750
|
-
const resourcePath =
|
|
2751
|
-
return
|
|
3362
|
+
const resourcePath = join6(root, "skills", "_shared", `${name}.md`);
|
|
3363
|
+
return existsSync12(resourcePath) ? resourcePath : void 0;
|
|
2752
3364
|
},
|
|
2753
3365
|
readSharedResource(name) {
|
|
2754
3366
|
const resourcePath = library.getSharedResourcePath(name);
|
|
@@ -2756,15 +3368,15 @@ function buildLibraryFromFiles(root) {
|
|
|
2756
3368
|
return readFileSync(resourcePath, "utf-8");
|
|
2757
3369
|
},
|
|
2758
3370
|
listProtocols() {
|
|
2759
|
-
const rootProtocols = discoverFiles(
|
|
3371
|
+
const rootProtocols = discoverFiles(join6(root, "protocols"), ".md");
|
|
2760
3372
|
if (rootProtocols.length > 0) return rootProtocols;
|
|
2761
|
-
return discoverFiles(
|
|
3373
|
+
return discoverFiles(join6(root, "skills", "protocols"), ".md");
|
|
2762
3374
|
},
|
|
2763
3375
|
getProtocolPath(name) {
|
|
2764
|
-
const rootPath =
|
|
2765
|
-
if (
|
|
2766
|
-
const skillsPath =
|
|
2767
|
-
return
|
|
3376
|
+
const rootPath = join6(root, "protocols", `${name}.md`);
|
|
3377
|
+
if (existsSync12(rootPath)) return rootPath;
|
|
3378
|
+
const skillsPath = join6(root, "skills", "protocols", `${name}.md`);
|
|
3379
|
+
return existsSync12(skillsPath) ? skillsPath : void 0;
|
|
2768
3380
|
},
|
|
2769
3381
|
readProtocol(name) {
|
|
2770
3382
|
const protocolPath = library.getProtocolPath(name);
|
|
@@ -2789,8 +3401,8 @@ function buildLibraryFromFiles(root) {
|
|
|
2789
3401
|
if (!entry.version) {
|
|
2790
3402
|
issues.push({ level: "warn", field: "version", message: "Missing version" });
|
|
2791
3403
|
}
|
|
2792
|
-
const skillPath =
|
|
2793
|
-
if (!
|
|
3404
|
+
const skillPath = join6(root, entry.path);
|
|
3405
|
+
if (!existsSync12(skillPath)) {
|
|
2794
3406
|
issues.push({
|
|
2795
3407
|
level: "error",
|
|
2796
3408
|
field: "path",
|
|
@@ -2849,15 +3461,15 @@ __export(catalog_exports, {
|
|
|
2849
3461
|
validateAll: () => validateAll,
|
|
2850
3462
|
validateSkillFrontmatter: () => validateSkillFrontmatter
|
|
2851
3463
|
});
|
|
2852
|
-
import { existsSync as
|
|
2853
|
-
import { join as
|
|
3464
|
+
import { existsSync as existsSync13 } from "fs";
|
|
3465
|
+
import { join as join7 } from "path";
|
|
2854
3466
|
var _library = null;
|
|
2855
3467
|
function registerSkillLibrary(library) {
|
|
2856
3468
|
_library = library;
|
|
2857
3469
|
}
|
|
2858
3470
|
function registerSkillLibraryFromPath(root) {
|
|
2859
|
-
const indexPath =
|
|
2860
|
-
if (
|
|
3471
|
+
const indexPath = join7(root, "index.js");
|
|
3472
|
+
if (existsSync13(indexPath)) {
|
|
2861
3473
|
_library = loadLibraryFromModule(root);
|
|
2862
3474
|
return;
|
|
2863
3475
|
}
|
|
@@ -2868,13 +3480,13 @@ function clearRegisteredLibrary() {
|
|
|
2868
3480
|
}
|
|
2869
3481
|
function discoverLibrary() {
|
|
2870
3482
|
const envPath = process.env["CAAMP_SKILL_LIBRARY"];
|
|
2871
|
-
if (envPath &&
|
|
3483
|
+
if (envPath && existsSync13(envPath)) {
|
|
2872
3484
|
try {
|
|
2873
|
-
const indexPath =
|
|
2874
|
-
if (
|
|
3485
|
+
const indexPath = join7(envPath, "index.js");
|
|
3486
|
+
if (existsSync13(indexPath)) {
|
|
2875
3487
|
return loadLibraryFromModule(envPath);
|
|
2876
3488
|
}
|
|
2877
|
-
if (
|
|
3489
|
+
if (existsSync13(join7(envPath, "skills.json"))) {
|
|
2878
3490
|
return buildLibraryFromFiles(envPath);
|
|
2879
3491
|
}
|
|
2880
3492
|
} catch {
|
|
@@ -2981,9 +3593,9 @@ function getLibraryRoot() {
|
|
|
2981
3593
|
}
|
|
2982
3594
|
|
|
2983
3595
|
// src/core/skills/discovery.ts
|
|
2984
|
-
import { existsSync as
|
|
3596
|
+
import { existsSync as existsSync14 } from "fs";
|
|
2985
3597
|
import { readdir as readdir2, readFile as readFile7 } from "fs/promises";
|
|
2986
|
-
import { join as
|
|
3598
|
+
import { join as join8 } from "path";
|
|
2987
3599
|
import matter from "gray-matter";
|
|
2988
3600
|
async function parseSkillFile(filePath) {
|
|
2989
3601
|
try {
|
|
@@ -3007,8 +3619,8 @@ async function parseSkillFile(filePath) {
|
|
|
3007
3619
|
}
|
|
3008
3620
|
}
|
|
3009
3621
|
async function discoverSkill(skillDir) {
|
|
3010
|
-
const skillFile =
|
|
3011
|
-
if (!
|
|
3622
|
+
const skillFile = join8(skillDir, "SKILL.md");
|
|
3623
|
+
if (!existsSync14(skillFile)) return null;
|
|
3012
3624
|
const metadata = await parseSkillFile(skillFile);
|
|
3013
3625
|
if (!metadata) return null;
|
|
3014
3626
|
return {
|
|
@@ -3019,12 +3631,12 @@ async function discoverSkill(skillDir) {
|
|
|
3019
3631
|
};
|
|
3020
3632
|
}
|
|
3021
3633
|
async function discoverSkills(rootDir) {
|
|
3022
|
-
if (!
|
|
3634
|
+
if (!existsSync14(rootDir)) return [];
|
|
3023
3635
|
const entries = await readdir2(rootDir, { withFileTypes: true });
|
|
3024
3636
|
const skills = [];
|
|
3025
3637
|
for (const entry of entries) {
|
|
3026
3638
|
if (!entry.isDirectory() && !entry.isSymbolicLink()) continue;
|
|
3027
|
-
const skillDir =
|
|
3639
|
+
const skillDir = join8(rootDir, entry.name);
|
|
3028
3640
|
const skill = await discoverSkill(skillDir);
|
|
3029
3641
|
if (skill) {
|
|
3030
3642
|
skills.push(skill);
|
|
@@ -3048,7 +3660,7 @@ async function discoverSkillsMulti(dirs) {
|
|
|
3048
3660
|
}
|
|
3049
3661
|
|
|
3050
3662
|
// src/core/skills/validator.ts
|
|
3051
|
-
import { existsSync as
|
|
3663
|
+
import { existsSync as existsSync15 } from "fs";
|
|
3052
3664
|
import { readFile as readFile8 } from "fs/promises";
|
|
3053
3665
|
import matter2 from "gray-matter";
|
|
3054
3666
|
var RESERVED_NAMES = [
|
|
@@ -3070,7 +3682,7 @@ var WARN_BODY_LINES = 500;
|
|
|
3070
3682
|
var WARN_DESCRIPTION_LENGTH = 50;
|
|
3071
3683
|
async function validateSkill(filePath) {
|
|
3072
3684
|
const issues = [];
|
|
3073
|
-
if (!
|
|
3685
|
+
if (!existsSync15(filePath)) {
|
|
3074
3686
|
return {
|
|
3075
3687
|
valid: false,
|
|
3076
3688
|
issues: [{ level: "error", field: "file", message: "File does not exist" }],
|
|
@@ -3219,17 +3831,25 @@ export {
|
|
|
3219
3831
|
writeConfig,
|
|
3220
3832
|
removeConfig,
|
|
3221
3833
|
readLockFile,
|
|
3834
|
+
resolveMcpConfigPath,
|
|
3835
|
+
listMcpServers,
|
|
3836
|
+
listAllMcpServers,
|
|
3837
|
+
detectMcpInstallations,
|
|
3838
|
+
installMcpServer,
|
|
3839
|
+
removeMcpServer,
|
|
3840
|
+
removeMcpServerFromAll,
|
|
3841
|
+
fetchWithTimeout,
|
|
3842
|
+
formatNetworkError,
|
|
3843
|
+
parseSource,
|
|
3844
|
+
isMarketplaceScoped,
|
|
3222
3845
|
scanFile,
|
|
3223
3846
|
scanDirectory,
|
|
3224
3847
|
toSarif,
|
|
3225
|
-
parseSource,
|
|
3226
|
-
isMarketplaceScoped,
|
|
3227
3848
|
recordSkillInstall,
|
|
3228
3849
|
removeSkillFromLock,
|
|
3229
3850
|
getTrackedSkills,
|
|
3230
3851
|
checkSkillUpdate,
|
|
3231
3852
|
checkAllSkillUpdates,
|
|
3232
|
-
formatNetworkError,
|
|
3233
3853
|
MarketplaceClient,
|
|
3234
3854
|
RECOMMENDATION_ERROR_CODES,
|
|
3235
3855
|
tokenizeCriteriaValue,
|
|
@@ -3258,4 +3878,4 @@ export {
|
|
|
3258
3878
|
discoverSkillsMulti,
|
|
3259
3879
|
validateSkill
|
|
3260
3880
|
};
|
|
3261
|
-
//# sourceMappingURL=chunk-
|
|
3881
|
+
//# sourceMappingURL=chunk-HEAGCHKU.js.map
|