@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
package/dist/cli.js
CHANGED
|
@@ -2,14 +2,17 @@
|
|
|
2
2
|
import {
|
|
3
3
|
CANONICAL_SKILLS_DIR,
|
|
4
4
|
MarketplaceClient,
|
|
5
|
+
PiHarness,
|
|
5
6
|
RECOMMENDATION_ERROR_CODES,
|
|
6
7
|
checkSkillUpdate,
|
|
7
8
|
detectAllProviders,
|
|
9
|
+
detectMcpInstallations,
|
|
8
10
|
detectProjectProviders,
|
|
9
11
|
discoverSkill,
|
|
10
12
|
discoverSkillsMulti,
|
|
11
13
|
dispatchInstallSkillAcrossProviders,
|
|
12
14
|
dispatchRemoveSkillAcrossProviders,
|
|
15
|
+
fetchWithTimeout,
|
|
13
16
|
formatNetworkError,
|
|
14
17
|
formatSkillRecommendations,
|
|
15
18
|
getHarnessFor,
|
|
@@ -18,12 +21,15 @@ import {
|
|
|
18
21
|
getSkillDir,
|
|
19
22
|
getTrackedSkills,
|
|
20
23
|
installBatchWithRollback,
|
|
24
|
+
installMcpServer,
|
|
21
25
|
isCatalogAvailable,
|
|
22
26
|
isHuman,
|
|
23
27
|
isMarketplaceScoped,
|
|
24
28
|
isQuiet,
|
|
25
29
|
isVerbose,
|
|
30
|
+
listAllMcpServers,
|
|
26
31
|
listCanonicalSkills,
|
|
32
|
+
listMcpServers,
|
|
27
33
|
listProfiles,
|
|
28
34
|
listSkills,
|
|
29
35
|
parseSource,
|
|
@@ -31,6 +37,8 @@ import {
|
|
|
31
37
|
readLockFile,
|
|
32
38
|
recommendSkills,
|
|
33
39
|
recordSkillInstall,
|
|
40
|
+
removeMcpServer,
|
|
41
|
+
removeMcpServerFromAll,
|
|
34
42
|
removeSkillFromLock,
|
|
35
43
|
resolveDefaultTargetProviders,
|
|
36
44
|
resolveProfile,
|
|
@@ -44,7 +52,7 @@ import {
|
|
|
44
52
|
tokenizeCriteriaValue,
|
|
45
53
|
updateInstructionsSingleOperation,
|
|
46
54
|
validateSkill
|
|
47
|
-
} from "./chunk-
|
|
55
|
+
} from "./chunk-HEAGCHKU.js";
|
|
48
56
|
import {
|
|
49
57
|
buildSkillsMap,
|
|
50
58
|
checkAllInjections,
|
|
@@ -1537,6 +1545,1149 @@ function registerInstructionsCommands(program2) {
|
|
|
1537
1545
|
registerInstructionsUpdate(instructions);
|
|
1538
1546
|
}
|
|
1539
1547
|
|
|
1548
|
+
// src/commands/mcp/common.ts
|
|
1549
|
+
var MCP_ERROR_CODES = {
|
|
1550
|
+
/** Caller-supplied input failed validation (shape, type, enum). */
|
|
1551
|
+
VALIDATION: "E_VALIDATION_SCHEMA",
|
|
1552
|
+
/** Referenced resource does not exist on disk or in the registry. */
|
|
1553
|
+
NOT_FOUND: "E_NOT_FOUND_RESOURCE",
|
|
1554
|
+
/** Server entry already exists and overwrite was not requested. */
|
|
1555
|
+
CONFLICT: "E_CONFLICT_VERSION",
|
|
1556
|
+
/** Upstream operation failed; retry is viable. */
|
|
1557
|
+
TRANSIENT: "E_TRANSIENT_UPSTREAM"
|
|
1558
|
+
};
|
|
1559
|
+
function requireMcpProvider(providerId) {
|
|
1560
|
+
const provider = getProvider(providerId);
|
|
1561
|
+
if (provider === void 0) {
|
|
1562
|
+
throw new LAFSCommandError(
|
|
1563
|
+
MCP_ERROR_CODES.NOT_FOUND,
|
|
1564
|
+
`Unknown provider id: ${providerId}`,
|
|
1565
|
+
"Run `caamp providers list` to see registered provider ids.",
|
|
1566
|
+
false
|
|
1567
|
+
);
|
|
1568
|
+
}
|
|
1569
|
+
if (provider.capabilities.mcp === null) {
|
|
1570
|
+
throw new LAFSCommandError(
|
|
1571
|
+
MCP_ERROR_CODES.NOT_FOUND,
|
|
1572
|
+
`Provider ${providerId} does not declare an MCP capability.`,
|
|
1573
|
+
"This provider does not consume MCP servers via a config file. Pick a different provider, or check `caamp providers list` for MCP-capable providers.",
|
|
1574
|
+
false
|
|
1575
|
+
);
|
|
1576
|
+
}
|
|
1577
|
+
return provider;
|
|
1578
|
+
}
|
|
1579
|
+
function parseScope(raw, defaultScope) {
|
|
1580
|
+
if (raw === void 0) return defaultScope;
|
|
1581
|
+
if (raw === "project" || raw === "global") return raw;
|
|
1582
|
+
throw new LAFSCommandError(
|
|
1583
|
+
MCP_ERROR_CODES.VALIDATION,
|
|
1584
|
+
`Invalid --scope value: ${raw}`,
|
|
1585
|
+
"Use one of: 'project', 'global'.",
|
|
1586
|
+
false
|
|
1587
|
+
);
|
|
1588
|
+
}
|
|
1589
|
+
function resolveProjectDir(scope, explicit) {
|
|
1590
|
+
if (scope !== "project") return void 0;
|
|
1591
|
+
if (explicit !== void 0 && explicit.length > 0) return explicit;
|
|
1592
|
+
return process.cwd();
|
|
1593
|
+
}
|
|
1594
|
+
function parseEnvAssignment(raw) {
|
|
1595
|
+
const idx = raw.indexOf("=");
|
|
1596
|
+
if (idx <= 0) {
|
|
1597
|
+
throw new LAFSCommandError(
|
|
1598
|
+
MCP_ERROR_CODES.VALIDATION,
|
|
1599
|
+
`Invalid --env value: ${raw}`,
|
|
1600
|
+
"Use KEY=VALUE format, e.g. --env GITHUB_TOKEN=ghp_...",
|
|
1601
|
+
false
|
|
1602
|
+
);
|
|
1603
|
+
}
|
|
1604
|
+
const key = raw.slice(0, idx);
|
|
1605
|
+
const value = raw.slice(idx + 1);
|
|
1606
|
+
if (key.length === 0) {
|
|
1607
|
+
throw new LAFSCommandError(
|
|
1608
|
+
MCP_ERROR_CODES.VALIDATION,
|
|
1609
|
+
`Invalid --env value: ${raw}`,
|
|
1610
|
+
"KEY must be non-empty.",
|
|
1611
|
+
false
|
|
1612
|
+
);
|
|
1613
|
+
}
|
|
1614
|
+
return [key, value];
|
|
1615
|
+
}
|
|
1616
|
+
|
|
1617
|
+
// src/commands/mcp/detect.ts
|
|
1618
|
+
function registerMcpDetectCommand(parent) {
|
|
1619
|
+
parent.command("detect").description("Detect which providers currently have MCP config files on disk").option("--scope <scope>", "Scope: project|global (default: project)").option("--project-dir <path>", "Project directory for the project scope (default: cwd)").option("--only-existing", "Only include providers whose config file exists on disk").action(
|
|
1620
|
+
async (opts) => runLafsCommand("mcp.detect", "standard", async () => {
|
|
1621
|
+
const scope = parseScope(opts.scope, "project");
|
|
1622
|
+
const projectDir = resolveProjectDir(scope, opts.projectDir);
|
|
1623
|
+
const all = await detectMcpInstallations(scope, projectDir);
|
|
1624
|
+
const filtered = opts.onlyExisting === true ? all.filter((e) => e.exists) : all;
|
|
1625
|
+
const existingCount = filtered.filter((e) => e.exists).length;
|
|
1626
|
+
const totalServers = filtered.reduce((sum, e) => sum + (e.serverCount ?? 0), 0);
|
|
1627
|
+
return {
|
|
1628
|
+
scope,
|
|
1629
|
+
providersProbed: all.length,
|
|
1630
|
+
existingCount,
|
|
1631
|
+
totalServers,
|
|
1632
|
+
entries: filtered
|
|
1633
|
+
};
|
|
1634
|
+
})
|
|
1635
|
+
);
|
|
1636
|
+
}
|
|
1637
|
+
|
|
1638
|
+
// src/commands/mcp/install.ts
|
|
1639
|
+
import { existsSync as existsSync3 } from "fs";
|
|
1640
|
+
import { readFile as readFile2 } from "fs/promises";
|
|
1641
|
+
function coerceServerConfig(value, source) {
|
|
1642
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) {
|
|
1643
|
+
throw new LAFSCommandError(
|
|
1644
|
+
MCP_ERROR_CODES.VALIDATION,
|
|
1645
|
+
`${source} did not contain a JSON object.`,
|
|
1646
|
+
"Pass an object with at least `command` or `url` (and optional `args`, `env`).",
|
|
1647
|
+
false
|
|
1648
|
+
);
|
|
1649
|
+
}
|
|
1650
|
+
const obj = value;
|
|
1651
|
+
const hasCommand = typeof obj["command"] === "string";
|
|
1652
|
+
const hasUrl = typeof obj["url"] === "string";
|
|
1653
|
+
const hasType = typeof obj["type"] === "string";
|
|
1654
|
+
if (!hasCommand && !hasUrl && !hasType) {
|
|
1655
|
+
throw new LAFSCommandError(
|
|
1656
|
+
MCP_ERROR_CODES.VALIDATION,
|
|
1657
|
+
`${source} must contain at least one of: command, url, type.`,
|
|
1658
|
+
"Provide either a stdio `command` (with optional `args`/`env`) or a remote `url`/`type`.",
|
|
1659
|
+
false
|
|
1660
|
+
);
|
|
1661
|
+
}
|
|
1662
|
+
return obj;
|
|
1663
|
+
}
|
|
1664
|
+
async function buildConfigFromOptions(inlineArgs, opts) {
|
|
1665
|
+
let base = null;
|
|
1666
|
+
if (opts.from !== void 0 && opts.from.length > 0) {
|
|
1667
|
+
if (!existsSync3(opts.from)) {
|
|
1668
|
+
throw new LAFSCommandError(
|
|
1669
|
+
MCP_ERROR_CODES.NOT_FOUND,
|
|
1670
|
+
`--from file does not exist: ${opts.from}`,
|
|
1671
|
+
"Check the path and try again.",
|
|
1672
|
+
false
|
|
1673
|
+
);
|
|
1674
|
+
}
|
|
1675
|
+
let parsed;
|
|
1676
|
+
try {
|
|
1677
|
+
const content = await readFile2(opts.from, "utf8");
|
|
1678
|
+
parsed = JSON.parse(content);
|
|
1679
|
+
} catch (err) {
|
|
1680
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
1681
|
+
throw new LAFSCommandError(
|
|
1682
|
+
MCP_ERROR_CODES.VALIDATION,
|
|
1683
|
+
`Failed to read --from JSON: ${message}`,
|
|
1684
|
+
"Ensure the file is valid JSON containing an MCP server config object.",
|
|
1685
|
+
false
|
|
1686
|
+
);
|
|
1687
|
+
}
|
|
1688
|
+
base = coerceServerConfig(parsed, `--from ${opts.from}`);
|
|
1689
|
+
}
|
|
1690
|
+
if (inlineArgs.length > 0) {
|
|
1691
|
+
const command = inlineArgs[0];
|
|
1692
|
+
if (command === void 0 || command.length === 0) {
|
|
1693
|
+
throw new LAFSCommandError(
|
|
1694
|
+
MCP_ERROR_CODES.VALIDATION,
|
|
1695
|
+
"Inline command was empty.",
|
|
1696
|
+
"Pass `--` followed by a command, e.g. `-- npx -y @mcp/server-github`.",
|
|
1697
|
+
false
|
|
1698
|
+
);
|
|
1699
|
+
}
|
|
1700
|
+
const args = inlineArgs.slice(1);
|
|
1701
|
+
base = {
|
|
1702
|
+
...base ?? {},
|
|
1703
|
+
command,
|
|
1704
|
+
...args.length > 0 ? { args } : {}
|
|
1705
|
+
};
|
|
1706
|
+
}
|
|
1707
|
+
if (base === null) {
|
|
1708
|
+
throw new LAFSCommandError(
|
|
1709
|
+
MCP_ERROR_CODES.VALIDATION,
|
|
1710
|
+
"Either an inline `-- <command> [args...]` or `--from <file>` is required.",
|
|
1711
|
+
"Pass an MCP server definition via inline command or a JSON file.",
|
|
1712
|
+
false
|
|
1713
|
+
);
|
|
1714
|
+
}
|
|
1715
|
+
if (opts.env !== void 0 && opts.env.length > 0) {
|
|
1716
|
+
const env = { ...base.env ?? {} };
|
|
1717
|
+
for (const entry of opts.env) {
|
|
1718
|
+
const [k, v] = parseEnvAssignment(entry);
|
|
1719
|
+
env[k] = v;
|
|
1720
|
+
}
|
|
1721
|
+
base = { ...base, env };
|
|
1722
|
+
}
|
|
1723
|
+
return base;
|
|
1724
|
+
}
|
|
1725
|
+
function registerMcpInstallCommand(parent) {
|
|
1726
|
+
parent.command("install <serverName> [args...]").description("Install an MCP server entry into a provider config file").option("--provider <id>", "Provider id to install into (required)").option("--from <file>", "Path to a JSON file containing an MCP server config").option(
|
|
1727
|
+
"--env <kv>",
|
|
1728
|
+
"Repeatable env var KEY=VALUE",
|
|
1729
|
+
(value, prev = []) => [...prev, value],
|
|
1730
|
+
[]
|
|
1731
|
+
).option("--scope <scope>", "Scope: project|global (default: project)").option("--force", "Overwrite an existing server entry").option("--project-dir <path>", "Project directory for the project scope (default: cwd)").action(
|
|
1732
|
+
async (serverName, inlineArgs, opts) => runLafsCommand("mcp.install", "standard", async () => {
|
|
1733
|
+
if (opts.provider === void 0 || opts.provider.length === 0) {
|
|
1734
|
+
throw new LAFSCommandError(
|
|
1735
|
+
MCP_ERROR_CODES.VALIDATION,
|
|
1736
|
+
"--provider <id> is required",
|
|
1737
|
+
"Pass a provider id, e.g. --provider claude-desktop.",
|
|
1738
|
+
false
|
|
1739
|
+
);
|
|
1740
|
+
}
|
|
1741
|
+
if (serverName.length === 0) {
|
|
1742
|
+
throw new LAFSCommandError(
|
|
1743
|
+
MCP_ERROR_CODES.VALIDATION,
|
|
1744
|
+
"Server name is required",
|
|
1745
|
+
"Pass a non-empty server name as the first positional argument.",
|
|
1746
|
+
false
|
|
1747
|
+
);
|
|
1748
|
+
}
|
|
1749
|
+
const provider = requireMcpProvider(opts.provider);
|
|
1750
|
+
const scope = parseScope(opts.scope, "project");
|
|
1751
|
+
const projectDir = resolveProjectDir(scope, opts.projectDir);
|
|
1752
|
+
const config = await buildConfigFromOptions(inlineArgs, opts);
|
|
1753
|
+
const result = await installMcpServer(provider, serverName, config, {
|
|
1754
|
+
scope,
|
|
1755
|
+
force: opts.force ?? false,
|
|
1756
|
+
projectDir
|
|
1757
|
+
});
|
|
1758
|
+
if (!result.installed && result.conflicted) {
|
|
1759
|
+
throw new LAFSCommandError(
|
|
1760
|
+
MCP_ERROR_CODES.CONFLICT,
|
|
1761
|
+
`Server ${serverName} already exists in ${result.sourcePath}`,
|
|
1762
|
+
"Re-run with --force to overwrite the existing entry.",
|
|
1763
|
+
false,
|
|
1764
|
+
{ sourcePath: result.sourcePath, providerId: result.providerId }
|
|
1765
|
+
);
|
|
1766
|
+
}
|
|
1767
|
+
return {
|
|
1768
|
+
installed: true,
|
|
1769
|
+
conflicted: result.conflicted,
|
|
1770
|
+
provider: provider.id,
|
|
1771
|
+
serverName,
|
|
1772
|
+
scope,
|
|
1773
|
+
sourcePath: result.sourcePath,
|
|
1774
|
+
config
|
|
1775
|
+
};
|
|
1776
|
+
})
|
|
1777
|
+
);
|
|
1778
|
+
}
|
|
1779
|
+
|
|
1780
|
+
// src/commands/mcp/list.ts
|
|
1781
|
+
function registerMcpListCommand(parent) {
|
|
1782
|
+
parent.command("list").description("List MCP servers configured for one or every MCP-capable provider").option("--provider <id>", "Restrict to a single provider id").option("--scope <scope>", "Scope: project|global (default: project)").option("--project-dir <path>", "Project directory for the project scope (default: cwd)").action(
|
|
1783
|
+
async (opts) => runLafsCommand("mcp.list", "standard", async () => {
|
|
1784
|
+
const scope = parseScope(opts.scope, "project");
|
|
1785
|
+
const projectDir = resolveProjectDir(scope, opts.projectDir);
|
|
1786
|
+
if (opts.provider !== void 0 && opts.provider.length > 0) {
|
|
1787
|
+
const provider = requireMcpProvider(opts.provider);
|
|
1788
|
+
const entries = await listMcpServers(provider, scope, projectDir);
|
|
1789
|
+
return {
|
|
1790
|
+
scope,
|
|
1791
|
+
provider: {
|
|
1792
|
+
id: provider.id,
|
|
1793
|
+
toolName: provider.toolName
|
|
1794
|
+
},
|
|
1795
|
+
count: entries.length,
|
|
1796
|
+
entries
|
|
1797
|
+
};
|
|
1798
|
+
}
|
|
1799
|
+
const map = await listAllMcpServers(scope, projectDir);
|
|
1800
|
+
const flat = [];
|
|
1801
|
+
const byProvider = {};
|
|
1802
|
+
for (const [providerId, entries] of map.entries()) {
|
|
1803
|
+
byProvider[providerId] = entries.length;
|
|
1804
|
+
flat.push(...entries);
|
|
1805
|
+
}
|
|
1806
|
+
return {
|
|
1807
|
+
scope,
|
|
1808
|
+
providers: byProvider,
|
|
1809
|
+
count: flat.length,
|
|
1810
|
+
entries: flat
|
|
1811
|
+
};
|
|
1812
|
+
})
|
|
1813
|
+
);
|
|
1814
|
+
}
|
|
1815
|
+
|
|
1816
|
+
// src/commands/mcp/remove.ts
|
|
1817
|
+
function registerMcpRemoveCommand(parent) {
|
|
1818
|
+
parent.command("remove <serverName>").description("Remove an MCP server entry from a provider config file").option("--provider <id>", "Provider id to remove from").option("--all-providers", "Remove from every MCP-capable provider in the registry").option("--scope <scope>", "Scope: project|global (default: project)").option("--project-dir <path>", "Project directory for the project scope (default: cwd)").action(
|
|
1819
|
+
async (serverName, opts) => runLafsCommand("mcp.remove", "standard", async () => {
|
|
1820
|
+
if (serverName.length === 0) {
|
|
1821
|
+
throw new LAFSCommandError(
|
|
1822
|
+
MCP_ERROR_CODES.VALIDATION,
|
|
1823
|
+
"Server name is required",
|
|
1824
|
+
"Pass a non-empty server name as the positional argument.",
|
|
1825
|
+
false
|
|
1826
|
+
);
|
|
1827
|
+
}
|
|
1828
|
+
const usingAll = opts.allProviders === true;
|
|
1829
|
+
const usingProvider = opts.provider !== void 0 && opts.provider.length > 0;
|
|
1830
|
+
if (usingAll === usingProvider) {
|
|
1831
|
+
throw new LAFSCommandError(
|
|
1832
|
+
MCP_ERROR_CODES.VALIDATION,
|
|
1833
|
+
"Pass exactly one of --provider <id> or --all-providers",
|
|
1834
|
+
"Use --provider for a single target, or --all-providers to remove everywhere.",
|
|
1835
|
+
false
|
|
1836
|
+
);
|
|
1837
|
+
}
|
|
1838
|
+
const scope = parseScope(opts.scope, "project");
|
|
1839
|
+
const projectDir = resolveProjectDir(scope, opts.projectDir);
|
|
1840
|
+
if (usingAll) {
|
|
1841
|
+
const results = await removeMcpServerFromAll(serverName, { scope, projectDir });
|
|
1842
|
+
const removedCount = results.filter((r) => r.removed).length;
|
|
1843
|
+
return {
|
|
1844
|
+
mode: "all-providers",
|
|
1845
|
+
serverName,
|
|
1846
|
+
scope,
|
|
1847
|
+
removedCount,
|
|
1848
|
+
providersProbed: results.length,
|
|
1849
|
+
results
|
|
1850
|
+
};
|
|
1851
|
+
}
|
|
1852
|
+
const provider = requireMcpProvider(opts.provider);
|
|
1853
|
+
const result = await removeMcpServer(provider, serverName, { scope, projectDir });
|
|
1854
|
+
return {
|
|
1855
|
+
mode: "single-provider",
|
|
1856
|
+
serverName,
|
|
1857
|
+
scope,
|
|
1858
|
+
provider: provider.id,
|
|
1859
|
+
removed: result.removed,
|
|
1860
|
+
reason: result.reason,
|
|
1861
|
+
sourcePath: result.sourcePath
|
|
1862
|
+
};
|
|
1863
|
+
})
|
|
1864
|
+
);
|
|
1865
|
+
}
|
|
1866
|
+
|
|
1867
|
+
// src/commands/mcp/index.ts
|
|
1868
|
+
function registerMcpCommands(program2) {
|
|
1869
|
+
const mcp = program2.command("mcp").description("MCP server config management across providers");
|
|
1870
|
+
registerMcpDetectCommand(mcp);
|
|
1871
|
+
registerMcpListCommand(mcp);
|
|
1872
|
+
registerMcpInstallCommand(mcp);
|
|
1873
|
+
registerMcpRemoveCommand(mcp);
|
|
1874
|
+
}
|
|
1875
|
+
|
|
1876
|
+
// src/commands/pi/extensions.ts
|
|
1877
|
+
import { existsSync as existsSync4 } from "fs";
|
|
1878
|
+
import { writeFile } from "fs/promises";
|
|
1879
|
+
import { tmpdir as tmpdir3 } from "os";
|
|
1880
|
+
import { join as join5 } from "path";
|
|
1881
|
+
|
|
1882
|
+
// src/core/sources/github.ts
|
|
1883
|
+
import { mkdtemp, rm } from "fs/promises";
|
|
1884
|
+
import { tmpdir } from "os";
|
|
1885
|
+
import { join as join3 } from "path";
|
|
1886
|
+
import { simpleGit } from "simple-git";
|
|
1887
|
+
async function cloneRepo(owner, repo, ref, subPath) {
|
|
1888
|
+
const tmpDir = await mkdtemp(join3(tmpdir(), "caamp-"));
|
|
1889
|
+
const repoUrl = `https://github.com/${owner}/${repo}.git`;
|
|
1890
|
+
const git = simpleGit();
|
|
1891
|
+
const cloneOptions = ["--depth", "1"];
|
|
1892
|
+
if (ref) {
|
|
1893
|
+
cloneOptions.push("--branch", ref);
|
|
1894
|
+
}
|
|
1895
|
+
await git.clone(repoUrl, tmpDir, cloneOptions);
|
|
1896
|
+
const localPath = subPath ? join3(tmpDir, subPath) : tmpDir;
|
|
1897
|
+
return {
|
|
1898
|
+
localPath,
|
|
1899
|
+
cleanup: async () => {
|
|
1900
|
+
try {
|
|
1901
|
+
await rm(tmpDir, { recursive: true });
|
|
1902
|
+
} catch {
|
|
1903
|
+
}
|
|
1904
|
+
}
|
|
1905
|
+
};
|
|
1906
|
+
}
|
|
1907
|
+
|
|
1908
|
+
// src/core/sources/gitlab.ts
|
|
1909
|
+
import { mkdtemp as mkdtemp2, rm as rm2 } from "fs/promises";
|
|
1910
|
+
import { tmpdir as tmpdir2 } from "os";
|
|
1911
|
+
import { join as join4 } from "path";
|
|
1912
|
+
import { simpleGit as simpleGit2 } from "simple-git";
|
|
1913
|
+
async function cloneGitLabRepo(owner, repo, ref, subPath) {
|
|
1914
|
+
const tmpDir = await mkdtemp2(join4(tmpdir2(), "caamp-gl-"));
|
|
1915
|
+
const repoUrl = `https://gitlab.com/${owner}/${repo}.git`;
|
|
1916
|
+
const git = simpleGit2();
|
|
1917
|
+
const cloneOptions = ["--depth", "1"];
|
|
1918
|
+
if (ref) {
|
|
1919
|
+
cloneOptions.push("--branch", ref);
|
|
1920
|
+
}
|
|
1921
|
+
await git.clone(repoUrl, tmpDir, cloneOptions);
|
|
1922
|
+
const localPath = subPath ? join4(tmpDir, subPath) : tmpDir;
|
|
1923
|
+
return {
|
|
1924
|
+
localPath,
|
|
1925
|
+
cleanup: async () => {
|
|
1926
|
+
try {
|
|
1927
|
+
await rm2(tmpDir, { recursive: true });
|
|
1928
|
+
} catch {
|
|
1929
|
+
}
|
|
1930
|
+
}
|
|
1931
|
+
};
|
|
1932
|
+
}
|
|
1933
|
+
|
|
1934
|
+
// src/commands/pi/common.ts
|
|
1935
|
+
var PI_ERROR_CODES = {
|
|
1936
|
+
/** Caller-supplied input failed validation (shape, type, enum). */
|
|
1937
|
+
VALIDATION: "E_VALIDATION_SCHEMA",
|
|
1938
|
+
/** Referenced resource does not exist on disk or in the registry. */
|
|
1939
|
+
NOT_FOUND: "E_NOT_FOUND_RESOURCE",
|
|
1940
|
+
/** Write target already exists and overwrite was not requested. */
|
|
1941
|
+
CONFLICT: "E_CONFLICT_VERSION",
|
|
1942
|
+
/** Network/upstream call failed; retry is viable. */
|
|
1943
|
+
TRANSIENT: "E_TRANSIENT_UPSTREAM"
|
|
1944
|
+
};
|
|
1945
|
+
function requirePiHarness() {
|
|
1946
|
+
const provider = getProvider("pi");
|
|
1947
|
+
if (provider === void 0) {
|
|
1948
|
+
throw new LAFSCommandError(
|
|
1949
|
+
PI_ERROR_CODES.NOT_FOUND,
|
|
1950
|
+
"Pi provider is not registered in the CAAMP registry.",
|
|
1951
|
+
"This is a configuration bug \u2014 open an issue with `caamp providers list`.",
|
|
1952
|
+
false
|
|
1953
|
+
);
|
|
1954
|
+
}
|
|
1955
|
+
const installed = getInstalledProviders();
|
|
1956
|
+
const piInstalled = installed.some((p) => p.id === "pi");
|
|
1957
|
+
if (!piInstalled) {
|
|
1958
|
+
throw new LAFSCommandError(
|
|
1959
|
+
PI_ERROR_CODES.NOT_FOUND,
|
|
1960
|
+
"Pi is not installed. Run: caamp providers install pi",
|
|
1961
|
+
"Install Pi via its official installer, then retry this command.",
|
|
1962
|
+
true
|
|
1963
|
+
);
|
|
1964
|
+
}
|
|
1965
|
+
const harness = getHarnessFor(provider);
|
|
1966
|
+
if (!(harness instanceof PiHarness)) {
|
|
1967
|
+
throw new LAFSCommandError(
|
|
1968
|
+
"E_INTERNAL_UNEXPECTED",
|
|
1969
|
+
"Pi provider is registered but no PiHarness implementation was returned.",
|
|
1970
|
+
"This is a programming error \u2014 the harness dispatcher should always return a PiHarness for Pi.",
|
|
1971
|
+
false
|
|
1972
|
+
);
|
|
1973
|
+
}
|
|
1974
|
+
return harness;
|
|
1975
|
+
}
|
|
1976
|
+
function parseScope2(raw, defaultTier) {
|
|
1977
|
+
if (raw === void 0) return defaultTier;
|
|
1978
|
+
if (raw === "project" || raw === "user" || raw === "global") return raw;
|
|
1979
|
+
throw new LAFSCommandError(
|
|
1980
|
+
PI_ERROR_CODES.VALIDATION,
|
|
1981
|
+
`Invalid --scope value: ${raw}`,
|
|
1982
|
+
"Use one of: 'project', 'user', 'global'.",
|
|
1983
|
+
false
|
|
1984
|
+
);
|
|
1985
|
+
}
|
|
1986
|
+
function resolveProjectDir2(tier, explicit) {
|
|
1987
|
+
if (tier !== "project") return void 0;
|
|
1988
|
+
if (explicit !== void 0 && explicit.length > 0) return explicit;
|
|
1989
|
+
return process.cwd();
|
|
1990
|
+
}
|
|
1991
|
+
|
|
1992
|
+
// src/commands/pi/extensions.ts
|
|
1993
|
+
async function resolveExtensionSource(source) {
|
|
1994
|
+
if (source.startsWith("/") || source.startsWith("./") || source.startsWith("../") || source.startsWith("~")) {
|
|
1995
|
+
const expanded = source.startsWith("~/") ? join5(process.env["HOME"] ?? "", source.slice(2)) : source;
|
|
1996
|
+
if (!existsSync4(expanded)) {
|
|
1997
|
+
throw new LAFSCommandError(
|
|
1998
|
+
PI_ERROR_CODES.NOT_FOUND,
|
|
1999
|
+
`Source file does not exist: ${expanded}`,
|
|
2000
|
+
"Check the path and try again.",
|
|
2001
|
+
false
|
|
2002
|
+
);
|
|
2003
|
+
}
|
|
2004
|
+
const inferredName = inferNameFromPath(expanded);
|
|
2005
|
+
return {
|
|
2006
|
+
localPath: expanded,
|
|
2007
|
+
cleanup: async () => {
|
|
2008
|
+
},
|
|
2009
|
+
inferredName
|
|
2010
|
+
};
|
|
2011
|
+
}
|
|
2012
|
+
if (/^https?:\/\//.test(source)) {
|
|
2013
|
+
const parsed2 = parseSource(source);
|
|
2014
|
+
if (parsed2.type === "github" && parsed2.owner !== void 0 && parsed2.repo !== void 0) {
|
|
2015
|
+
const cloneResult = await cloneRepo(parsed2.owner, parsed2.repo, parsed2.ref);
|
|
2016
|
+
const filePath = parsed2.path !== void 0 ? join5(cloneResult.localPath, parsed2.path) : cloneResult.localPath;
|
|
2017
|
+
if (!existsSync4(filePath)) {
|
|
2018
|
+
await cloneResult.cleanup();
|
|
2019
|
+
throw new LAFSCommandError(
|
|
2020
|
+
PI_ERROR_CODES.NOT_FOUND,
|
|
2021
|
+
`Source path not found inside cloned repo: ${parsed2.path ?? "(root)"}`,
|
|
2022
|
+
"Check the repository URL and path.",
|
|
2023
|
+
false
|
|
2024
|
+
);
|
|
2025
|
+
}
|
|
2026
|
+
return {
|
|
2027
|
+
localPath: filePath,
|
|
2028
|
+
cleanup: cloneResult.cleanup,
|
|
2029
|
+
inferredName: inferNameFromPath(filePath)
|
|
2030
|
+
};
|
|
2031
|
+
}
|
|
2032
|
+
if (parsed2.type === "gitlab" && parsed2.owner !== void 0 && parsed2.repo !== void 0) {
|
|
2033
|
+
const cloneResult = await cloneGitLabRepo(parsed2.owner, parsed2.repo, parsed2.ref);
|
|
2034
|
+
const filePath = parsed2.path !== void 0 ? join5(cloneResult.localPath, parsed2.path) : cloneResult.localPath;
|
|
2035
|
+
if (!existsSync4(filePath)) {
|
|
2036
|
+
await cloneResult.cleanup();
|
|
2037
|
+
throw new LAFSCommandError(
|
|
2038
|
+
PI_ERROR_CODES.NOT_FOUND,
|
|
2039
|
+
`Source path not found inside cloned repo: ${parsed2.path ?? "(root)"}`,
|
|
2040
|
+
"Check the repository URL and path.",
|
|
2041
|
+
false
|
|
2042
|
+
);
|
|
2043
|
+
}
|
|
2044
|
+
return {
|
|
2045
|
+
localPath: filePath,
|
|
2046
|
+
cleanup: cloneResult.cleanup,
|
|
2047
|
+
inferredName: inferNameFromPath(filePath)
|
|
2048
|
+
};
|
|
2049
|
+
}
|
|
2050
|
+
const resp = await fetchWithTimeout(source);
|
|
2051
|
+
if (!resp.ok) {
|
|
2052
|
+
throw new LAFSCommandError(
|
|
2053
|
+
PI_ERROR_CODES.TRANSIENT,
|
|
2054
|
+
`Failed to download source from ${source}: HTTP ${resp.status}`,
|
|
2055
|
+
"Check the URL and network connectivity.",
|
|
2056
|
+
true
|
|
2057
|
+
);
|
|
2058
|
+
}
|
|
2059
|
+
const body = await resp.text();
|
|
2060
|
+
const baseName = inferNameFromUrl(source);
|
|
2061
|
+
const tmp = join5(tmpdir3(), `caamp-pi-ext-${process.pid}-${Date.now()}-${baseName}.ts`);
|
|
2062
|
+
await writeFile(tmp, body, "utf8");
|
|
2063
|
+
return {
|
|
2064
|
+
localPath: tmp,
|
|
2065
|
+
cleanup: async () => {
|
|
2066
|
+
try {
|
|
2067
|
+
await (await import("fs/promises")).rm(tmp, { force: true });
|
|
2068
|
+
} catch {
|
|
2069
|
+
}
|
|
2070
|
+
},
|
|
2071
|
+
inferredName: baseName
|
|
2072
|
+
};
|
|
2073
|
+
}
|
|
2074
|
+
const parsed = parseSource(source);
|
|
2075
|
+
if (parsed.type === "github" && parsed.owner !== void 0 && parsed.repo !== void 0) {
|
|
2076
|
+
const cloneResult = await cloneRepo(parsed.owner, parsed.repo, parsed.ref);
|
|
2077
|
+
const filePath = parsed.path !== void 0 ? join5(cloneResult.localPath, parsed.path) : cloneResult.localPath;
|
|
2078
|
+
if (!existsSync4(filePath)) {
|
|
2079
|
+
await cloneResult.cleanup();
|
|
2080
|
+
throw new LAFSCommandError(
|
|
2081
|
+
PI_ERROR_CODES.NOT_FOUND,
|
|
2082
|
+
`Source path not found inside cloned repo: ${parsed.path ?? "(root)"}`,
|
|
2083
|
+
"Check the repository shorthand and path.",
|
|
2084
|
+
false
|
|
2085
|
+
);
|
|
2086
|
+
}
|
|
2087
|
+
return {
|
|
2088
|
+
localPath: filePath,
|
|
2089
|
+
cleanup: cloneResult.cleanup,
|
|
2090
|
+
inferredName: inferNameFromPath(filePath)
|
|
2091
|
+
};
|
|
2092
|
+
}
|
|
2093
|
+
throw new LAFSCommandError(
|
|
2094
|
+
PI_ERROR_CODES.VALIDATION,
|
|
2095
|
+
`Unsupported source: ${source}`,
|
|
2096
|
+
"Use a local file path, HTTPS URL, or GitHub shorthand (owner/repo/path.ts).",
|
|
2097
|
+
false
|
|
2098
|
+
);
|
|
2099
|
+
}
|
|
2100
|
+
function inferNameFromPath(filePath) {
|
|
2101
|
+
const base = filePath.split(/[/\\]/).pop() ?? filePath;
|
|
2102
|
+
return base.replace(/\.(ts|tsx|mts)$/, "");
|
|
2103
|
+
}
|
|
2104
|
+
function inferNameFromUrl(url) {
|
|
2105
|
+
try {
|
|
2106
|
+
const u = new URL(url);
|
|
2107
|
+
const seg = u.pathname.split("/").filter(Boolean).pop() ?? "extension";
|
|
2108
|
+
return seg.replace(/\.(ts|tsx|mts)$/, "");
|
|
2109
|
+
} catch {
|
|
2110
|
+
return "extension";
|
|
2111
|
+
}
|
|
2112
|
+
}
|
|
2113
|
+
function registerPiExtensionsCommands(parent) {
|
|
2114
|
+
const ext = parent.command("extensions").description("Manage Pi extensions across tiers");
|
|
2115
|
+
ext.command("list").description("List Pi extensions across project, user, and global tiers").option("--project-dir <path>", "Project directory for the project tier (default: cwd)").action(
|
|
2116
|
+
async (opts) => runLafsCommand("pi.extensions.list", "standard", async () => {
|
|
2117
|
+
const harness = requirePiHarness();
|
|
2118
|
+
const projectDir = opts.projectDir ?? process.cwd();
|
|
2119
|
+
const entries = await harness.listExtensions(projectDir);
|
|
2120
|
+
return {
|
|
2121
|
+
count: entries.length,
|
|
2122
|
+
extensions: entries
|
|
2123
|
+
};
|
|
2124
|
+
})
|
|
2125
|
+
);
|
|
2126
|
+
ext.command("install <source>").description("Install a Pi extension from a local path, HTTPS URL, or GitHub shorthand").option("--scope <tier>", "Install tier: project|user|global (default: project)").option("--name <name>", "Override the inferred extension name").option("--force", "Overwrite an existing extension at the target tier").option("--project-dir <path>", "Project directory for the project tier (default: cwd)").action(
|
|
2127
|
+
async (source, opts) => runLafsCommand("pi.extensions.install", "standard", async () => {
|
|
2128
|
+
const harness = requirePiHarness();
|
|
2129
|
+
const tier = parseScope2(opts.scope, "project");
|
|
2130
|
+
const projectDir = resolveProjectDir2(tier, opts.projectDir);
|
|
2131
|
+
const resolved = await resolveExtensionSource(source);
|
|
2132
|
+
try {
|
|
2133
|
+
const name = opts.name ?? resolved.inferredName;
|
|
2134
|
+
const installOpts = { force: opts.force ?? false };
|
|
2135
|
+
const result = await harness.installExtension(
|
|
2136
|
+
resolved.localPath,
|
|
2137
|
+
name,
|
|
2138
|
+
tier,
|
|
2139
|
+
projectDir,
|
|
2140
|
+
installOpts
|
|
2141
|
+
);
|
|
2142
|
+
return {
|
|
2143
|
+
installed: {
|
|
2144
|
+
name,
|
|
2145
|
+
tier: result.tier,
|
|
2146
|
+
targetPath: result.targetPath,
|
|
2147
|
+
source
|
|
2148
|
+
}
|
|
2149
|
+
};
|
|
2150
|
+
} finally {
|
|
2151
|
+
await resolved.cleanup();
|
|
2152
|
+
}
|
|
2153
|
+
})
|
|
2154
|
+
);
|
|
2155
|
+
ext.command("remove <name>").description("Remove a Pi extension from the given tier").option("--scope <tier>", "Target tier: project|user|global (default: project)").option("--project-dir <path>", "Project directory for the project tier (default: cwd)").action(
|
|
2156
|
+
async (name, opts) => runLafsCommand("pi.extensions.remove", "standard", async () => {
|
|
2157
|
+
const harness = requirePiHarness();
|
|
2158
|
+
const tier = parseScope2(opts.scope, "project");
|
|
2159
|
+
const projectDir = resolveProjectDir2(tier, opts.projectDir);
|
|
2160
|
+
const removed = await harness.removeExtension(name, tier, projectDir);
|
|
2161
|
+
return {
|
|
2162
|
+
name,
|
|
2163
|
+
tier,
|
|
2164
|
+
removed
|
|
2165
|
+
};
|
|
2166
|
+
})
|
|
2167
|
+
);
|
|
2168
|
+
}
|
|
2169
|
+
|
|
2170
|
+
// src/commands/pi/models.ts
|
|
2171
|
+
function parseModelSpec(spec) {
|
|
2172
|
+
const idx = spec.indexOf(":");
|
|
2173
|
+
if (idx <= 0 || idx === spec.length - 1) {
|
|
2174
|
+
throw new LAFSCommandError(
|
|
2175
|
+
PI_ERROR_CODES.VALIDATION,
|
|
2176
|
+
`Invalid model specifier: ${spec}`,
|
|
2177
|
+
"Use 'provider:model-id', e.g. 'anthropic:claude-sonnet-4-20250514'.",
|
|
2178
|
+
false
|
|
2179
|
+
);
|
|
2180
|
+
}
|
|
2181
|
+
return { provider: spec.slice(0, idx), id: spec.slice(idx + 1) };
|
|
2182
|
+
}
|
|
2183
|
+
function resolveModelsScope(opts) {
|
|
2184
|
+
if (opts.global === true) return { kind: "global" };
|
|
2185
|
+
if (opts.projectDir !== void 0 && opts.projectDir.length > 0) {
|
|
2186
|
+
return { kind: "project", projectDir: opts.projectDir };
|
|
2187
|
+
}
|
|
2188
|
+
return { kind: "global" };
|
|
2189
|
+
}
|
|
2190
|
+
function parsePositiveInt(raw, name) {
|
|
2191
|
+
if (raw === void 0) return void 0;
|
|
2192
|
+
const parsed = Number.parseInt(raw, 10);
|
|
2193
|
+
if (!Number.isFinite(parsed) || parsed <= 0 || String(parsed) !== raw.trim()) {
|
|
2194
|
+
throw new LAFSCommandError(
|
|
2195
|
+
PI_ERROR_CODES.VALIDATION,
|
|
2196
|
+
`Invalid value for --${name}: ${raw}`,
|
|
2197
|
+
`--${name} must be a positive integer.`,
|
|
2198
|
+
false
|
|
2199
|
+
);
|
|
2200
|
+
}
|
|
2201
|
+
return parsed;
|
|
2202
|
+
}
|
|
2203
|
+
function registerPiModelsCommands(parent) {
|
|
2204
|
+
const models = parent.command("models").description("Manage Pi's dual-file models configuration");
|
|
2205
|
+
models.command("list").description("List every model known to Pi (union of models.json and enabledModels)").option("--global", "Read from the Pi global state root (default)").option("--project-dir <path>", "Read from a project-scoped Pi config").action(
|
|
2206
|
+
async (opts) => runLafsCommand("pi.models.list", "standard", async () => {
|
|
2207
|
+
const harness = requirePiHarness();
|
|
2208
|
+
const scope = resolveModelsScope(opts);
|
|
2209
|
+
const entries = await harness.listModels(scope);
|
|
2210
|
+
const active = entries.filter((e) => e.enabled);
|
|
2211
|
+
const def = entries.find((e) => e.isDefault) ?? null;
|
|
2212
|
+
return {
|
|
2213
|
+
scope: scope.kind,
|
|
2214
|
+
count: entries.length,
|
|
2215
|
+
activeCount: active.length,
|
|
2216
|
+
default: def,
|
|
2217
|
+
models: entries
|
|
2218
|
+
};
|
|
2219
|
+
})
|
|
2220
|
+
);
|
|
2221
|
+
models.command("add <spec>").description("Add a custom model definition to models.json (e.g. provider:model-id)").option("--global", "Write to the Pi global state root (default)").option("--project-dir <path>", "Write to a project-scoped Pi config").option("--display-name <name>", "Human-readable model name").option("--base-url <url>", "Override the provider base URL").option("--reasoning", "Mark the model as reasoning-capable").option("--context-window <tokens>", "Context window size in tokens").option("--max-tokens <tokens>", "Maximum output tokens").action(
|
|
2222
|
+
async (spec, opts) => runLafsCommand("pi.models.add", "standard", async () => {
|
|
2223
|
+
const harness = requirePiHarness();
|
|
2224
|
+
const scope = resolveModelsScope(opts);
|
|
2225
|
+
const { provider, id } = parseModelSpec(spec);
|
|
2226
|
+
const contextWindow = parsePositiveInt(opts.contextWindow, "context-window");
|
|
2227
|
+
const maxTokens = parsePositiveInt(opts.maxTokens, "max-tokens");
|
|
2228
|
+
const config = await harness.readModelsConfig(scope);
|
|
2229
|
+
const providerBlock = config.providers[provider] ?? {};
|
|
2230
|
+
if (opts.baseUrl !== void 0) providerBlock.baseUrl = opts.baseUrl;
|
|
2231
|
+
const nextModels = providerBlock.models ? [...providerBlock.models] : [];
|
|
2232
|
+
const existingIdx = nextModels.findIndex((m) => m.id === id);
|
|
2233
|
+
const definition = {
|
|
2234
|
+
id,
|
|
2235
|
+
name: opts.displayName ?? id
|
|
2236
|
+
};
|
|
2237
|
+
if (opts.reasoning === true) definition.reasoning = true;
|
|
2238
|
+
if (contextWindow !== void 0) definition.contextWindow = contextWindow;
|
|
2239
|
+
if (maxTokens !== void 0) definition.maxTokens = maxTokens;
|
|
2240
|
+
if (existingIdx >= 0) {
|
|
2241
|
+
nextModels[existingIdx] = definition;
|
|
2242
|
+
} else {
|
|
2243
|
+
nextModels.push(definition);
|
|
2244
|
+
}
|
|
2245
|
+
providerBlock.models = nextModels;
|
|
2246
|
+
const nextConfig = {
|
|
2247
|
+
providers: { ...config.providers, [provider]: providerBlock }
|
|
2248
|
+
};
|
|
2249
|
+
await harness.writeModelsConfig(nextConfig, scope);
|
|
2250
|
+
return {
|
|
2251
|
+
added: { provider, id, name: definition.name },
|
|
2252
|
+
replaced: existingIdx >= 0,
|
|
2253
|
+
scope: scope.kind
|
|
2254
|
+
};
|
|
2255
|
+
})
|
|
2256
|
+
);
|
|
2257
|
+
models.command("remove <spec>").description("Remove a custom model definition from models.json").option("--global", "Write to the Pi global state root (default)").option("--project-dir <path>", "Write to a project-scoped Pi config").action(
|
|
2258
|
+
async (spec, opts) => runLafsCommand("pi.models.remove", "standard", async () => {
|
|
2259
|
+
const harness = requirePiHarness();
|
|
2260
|
+
const scope = resolveModelsScope(opts);
|
|
2261
|
+
const { provider, id } = parseModelSpec(spec);
|
|
2262
|
+
const config = await harness.readModelsConfig(scope);
|
|
2263
|
+
const providerBlock = config.providers[provider];
|
|
2264
|
+
if (providerBlock === void 0 || providerBlock.models === void 0) {
|
|
2265
|
+
return { removed: false, provider, id, reason: "provider-not-found" };
|
|
2266
|
+
}
|
|
2267
|
+
const before = providerBlock.models.length;
|
|
2268
|
+
const filtered = providerBlock.models.filter((m) => m.id !== id);
|
|
2269
|
+
if (filtered.length === before) {
|
|
2270
|
+
return { removed: false, provider, id, reason: "model-not-found" };
|
|
2271
|
+
}
|
|
2272
|
+
const nextProviderBlock = { ...providerBlock, models: filtered };
|
|
2273
|
+
if (filtered.length === 0) {
|
|
2274
|
+
delete nextProviderBlock.models;
|
|
2275
|
+
}
|
|
2276
|
+
const nextConfig = {
|
|
2277
|
+
providers: { ...config.providers, [provider]: nextProviderBlock }
|
|
2278
|
+
};
|
|
2279
|
+
await harness.writeModelsConfig(nextConfig, scope);
|
|
2280
|
+
return { removed: true, provider, id, scope: scope.kind };
|
|
2281
|
+
})
|
|
2282
|
+
);
|
|
2283
|
+
models.command("enable <spec>").description("Enable a model by appending it to settings.json:enabledModels").option("--global", "Write to the Pi global state root (default)").option("--project-dir <path>", "Write to a project-scoped Pi config").action(
|
|
2284
|
+
async (spec, opts) => runLafsCommand("pi.models.enable", "standard", async () => {
|
|
2285
|
+
const harness = requirePiHarness();
|
|
2286
|
+
const scope = resolveModelsScope(opts);
|
|
2287
|
+
const { provider, id } = parseModelSpec(spec);
|
|
2288
|
+
if (!id.includes("*")) {
|
|
2289
|
+
const config = await harness.readModelsConfig(scope);
|
|
2290
|
+
const providerBlock = config.providers[provider];
|
|
2291
|
+
const defined = providerBlock?.models?.some((m) => m.id === id) ?? false;
|
|
2292
|
+
if (!defined) {
|
|
2293
|
+
}
|
|
2294
|
+
}
|
|
2295
|
+
const current = await harness.readSettings(scope);
|
|
2296
|
+
const currentObj = typeof current === "object" && current !== null && !Array.isArray(current) ? current : {};
|
|
2297
|
+
const enabledRaw = currentObj["enabledModels"];
|
|
2298
|
+
const enabled = Array.isArray(enabledRaw) ? enabledRaw.filter((v) => typeof v === "string") : [];
|
|
2299
|
+
const already = enabled.includes(spec);
|
|
2300
|
+
if (already) {
|
|
2301
|
+
return { enabled: false, reason: "already-enabled", spec, scope: scope.kind };
|
|
2302
|
+
}
|
|
2303
|
+
enabled.push(spec);
|
|
2304
|
+
await harness.writeSettings({ enabledModels: enabled }, scope);
|
|
2305
|
+
return { enabled: true, spec, provider, id, scope: scope.kind };
|
|
2306
|
+
})
|
|
2307
|
+
);
|
|
2308
|
+
models.command("disable <spec>").description("Disable a model by removing it from settings.json:enabledModels").option("--global", "Write to the Pi global state root (default)").option("--project-dir <path>", "Write to a project-scoped Pi config").action(
|
|
2309
|
+
async (spec, opts) => runLafsCommand("pi.models.disable", "standard", async () => {
|
|
2310
|
+
const harness = requirePiHarness();
|
|
2311
|
+
const scope = resolveModelsScope(opts);
|
|
2312
|
+
const current = await harness.readSettings(scope);
|
|
2313
|
+
const currentObj = typeof current === "object" && current !== null && !Array.isArray(current) ? current : {};
|
|
2314
|
+
const enabledRaw = currentObj["enabledModels"];
|
|
2315
|
+
const enabled = Array.isArray(enabledRaw) ? enabledRaw.filter((v) => typeof v === "string") : [];
|
|
2316
|
+
const filtered = enabled.filter((e) => e !== spec);
|
|
2317
|
+
if (filtered.length === enabled.length) {
|
|
2318
|
+
return { disabled: false, reason: "not-enabled", spec, scope: scope.kind };
|
|
2319
|
+
}
|
|
2320
|
+
await harness.writeSettings({ enabledModels: filtered }, scope);
|
|
2321
|
+
return { disabled: true, spec, scope: scope.kind };
|
|
2322
|
+
})
|
|
2323
|
+
);
|
|
2324
|
+
models.command("default <spec>").description("Set settings.json:defaultProvider and defaultModel").option("--global", "Write to the Pi global state root (default)").option("--project-dir <path>", "Write to a project-scoped Pi config").action(
|
|
2325
|
+
async (spec, opts) => runLafsCommand("pi.models.default", "standard", async () => {
|
|
2326
|
+
const harness = requirePiHarness();
|
|
2327
|
+
const scope = resolveModelsScope(opts);
|
|
2328
|
+
const { provider, id } = parseModelSpec(spec);
|
|
2329
|
+
const config = await harness.readModelsConfig(scope);
|
|
2330
|
+
const providerBlock = config.providers[provider];
|
|
2331
|
+
const defined = providerBlock?.models?.some((m) => m.id === id) ?? false;
|
|
2332
|
+
await harness.writeSettings({ defaultProvider: provider, defaultModel: id }, scope);
|
|
2333
|
+
return {
|
|
2334
|
+
set: true,
|
|
2335
|
+
provider,
|
|
2336
|
+
id,
|
|
2337
|
+
knownInModelsJson: defined,
|
|
2338
|
+
scope: scope.kind
|
|
2339
|
+
};
|
|
2340
|
+
})
|
|
2341
|
+
);
|
|
2342
|
+
}
|
|
2343
|
+
|
|
2344
|
+
// src/commands/pi/prompts.ts
|
|
2345
|
+
import { existsSync as existsSync5 } from "fs";
|
|
2346
|
+
import { join as join6, resolve } from "path";
|
|
2347
|
+
function inferPromptName(sourceDir) {
|
|
2348
|
+
const normalized = resolve(sourceDir).replace(/[\\/]+$/, "");
|
|
2349
|
+
const base = normalized.split(/[\\/]/).pop();
|
|
2350
|
+
if (base === void 0 || base.length === 0) {
|
|
2351
|
+
throw new LAFSCommandError(
|
|
2352
|
+
PI_ERROR_CODES.VALIDATION,
|
|
2353
|
+
`Could not infer a prompt name from source: ${sourceDir}`,
|
|
2354
|
+
"Pass --name <name> to override the inferred name.",
|
|
2355
|
+
false
|
|
2356
|
+
);
|
|
2357
|
+
}
|
|
2358
|
+
return base;
|
|
2359
|
+
}
|
|
2360
|
+
function registerPiPromptsCommands(parent) {
|
|
2361
|
+
const prompts = parent.command("prompts").description("Manage Pi prompts across tiers");
|
|
2362
|
+
prompts.command("install <source>").description("Install a Pi prompt directory (contains prompt.md + optional metadata)").option("--scope <tier>", "Install tier: project|user|global (default: project)").option("--name <name>", "Override the inferred prompt name").option("--force", "Overwrite an existing prompt at the target tier").option("--project-dir <path>", "Project directory for the project tier (default: cwd)").action(
|
|
2363
|
+
async (source, opts) => runLafsCommand("pi.prompts.install", "standard", async () => {
|
|
2364
|
+
const harness = requirePiHarness();
|
|
2365
|
+
const tier = parseScope2(opts.scope, "project");
|
|
2366
|
+
const projectDir = resolveProjectDir2(tier, opts.projectDir);
|
|
2367
|
+
const absSource = resolve(source);
|
|
2368
|
+
if (!existsSync5(absSource)) {
|
|
2369
|
+
throw new LAFSCommandError(
|
|
2370
|
+
PI_ERROR_CODES.NOT_FOUND,
|
|
2371
|
+
`Source directory does not exist: ${absSource}`,
|
|
2372
|
+
"Check the path and try again.",
|
|
2373
|
+
false
|
|
2374
|
+
);
|
|
2375
|
+
}
|
|
2376
|
+
if (!existsSync5(join6(absSource, "prompt.md"))) {
|
|
2377
|
+
throw new LAFSCommandError(
|
|
2378
|
+
PI_ERROR_CODES.VALIDATION,
|
|
2379
|
+
`Source directory is missing prompt.md: ${absSource}`,
|
|
2380
|
+
"Add a prompt.md to the source directory and retry.",
|
|
2381
|
+
false
|
|
2382
|
+
);
|
|
2383
|
+
}
|
|
2384
|
+
const name = opts.name ?? inferPromptName(absSource);
|
|
2385
|
+
const installOpts = { force: opts.force ?? false };
|
|
2386
|
+
const result = await harness.installPrompt(absSource, name, tier, projectDir, installOpts);
|
|
2387
|
+
return {
|
|
2388
|
+
installed: {
|
|
2389
|
+
name,
|
|
2390
|
+
tier: result.tier,
|
|
2391
|
+
targetPath: result.targetPath,
|
|
2392
|
+
source: absSource
|
|
2393
|
+
}
|
|
2394
|
+
};
|
|
2395
|
+
})
|
|
2396
|
+
);
|
|
2397
|
+
prompts.command("list").description("List Pi prompts across project, user, and global tiers").option("--project-dir <path>", "Project directory for the project tier (default: cwd)").action(
|
|
2398
|
+
async (opts) => runLafsCommand("pi.prompts.list", "standard", async () => {
|
|
2399
|
+
const harness = requirePiHarness();
|
|
2400
|
+
const projectDir = opts.projectDir ?? process.cwd();
|
|
2401
|
+
const entries = await harness.listPrompts(projectDir);
|
|
2402
|
+
return {
|
|
2403
|
+
count: entries.length,
|
|
2404
|
+
prompts: entries
|
|
2405
|
+
};
|
|
2406
|
+
})
|
|
2407
|
+
);
|
|
2408
|
+
prompts.command("remove <name>").description("Remove a Pi prompt from the given tier").option("--scope <tier>", "Target tier: project|user|global (default: project)").option("--project-dir <path>", "Project directory for the project tier (default: cwd)").action(
|
|
2409
|
+
async (name, opts) => runLafsCommand("pi.prompts.remove", "standard", async () => {
|
|
2410
|
+
const harness = requirePiHarness();
|
|
2411
|
+
const tier = parseScope2(opts.scope, "project");
|
|
2412
|
+
const projectDir = resolveProjectDir2(tier, opts.projectDir);
|
|
2413
|
+
const removed = await harness.removePrompt(name, tier, projectDir);
|
|
2414
|
+
return { name, tier, removed };
|
|
2415
|
+
})
|
|
2416
|
+
);
|
|
2417
|
+
}
|
|
2418
|
+
|
|
2419
|
+
// src/commands/pi/sessions.ts
|
|
2420
|
+
import { spawn } from "child_process";
|
|
2421
|
+
import { createReadStream, createWriteStream, existsSync as existsSync6 } from "fs";
|
|
2422
|
+
import { createInterface } from "readline/promises";
|
|
2423
|
+
async function streamSession(filePath, outputPath, transform) {
|
|
2424
|
+
const writeToFile = outputPath !== void 0 && outputPath.length > 0;
|
|
2425
|
+
const out = writeToFile ? createWriteStream(outputPath) : process.stdout;
|
|
2426
|
+
const reader = createInterface({
|
|
2427
|
+
input: createReadStream(filePath, { encoding: "utf8" }),
|
|
2428
|
+
crlfDelay: Infinity
|
|
2429
|
+
});
|
|
2430
|
+
let emitted = 0;
|
|
2431
|
+
try {
|
|
2432
|
+
for await (const line of reader) {
|
|
2433
|
+
const result = transform(line);
|
|
2434
|
+
if (result === null) continue;
|
|
2435
|
+
out.write(`${result}
|
|
2436
|
+
`);
|
|
2437
|
+
emitted += 1;
|
|
2438
|
+
}
|
|
2439
|
+
} finally {
|
|
2440
|
+
reader.close();
|
|
2441
|
+
if (writeToFile && "end" in out) {
|
|
2442
|
+
await new Promise((resolve3) => {
|
|
2443
|
+
out.end(resolve3);
|
|
2444
|
+
});
|
|
2445
|
+
}
|
|
2446
|
+
}
|
|
2447
|
+
return emitted;
|
|
2448
|
+
}
|
|
2449
|
+
function sessionEntryToMarkdown(line) {
|
|
2450
|
+
let parsed;
|
|
2451
|
+
try {
|
|
2452
|
+
parsed = JSON.parse(line);
|
|
2453
|
+
} catch {
|
|
2454
|
+
return null;
|
|
2455
|
+
}
|
|
2456
|
+
if (typeof parsed !== "object" || parsed === null) return null;
|
|
2457
|
+
const obj = parsed;
|
|
2458
|
+
const type = typeof obj["type"] === "string" ? obj["type"] : null;
|
|
2459
|
+
if (type === "session") {
|
|
2460
|
+
const id = typeof obj["id"] === "string" ? obj["id"] : "(no id)";
|
|
2461
|
+
const ts = typeof obj["timestamp"] === "string" ? obj["timestamp"] : "";
|
|
2462
|
+
return `# Session ${id}${ts.length > 0 ? ` \xB7 ${ts}` : ""}
|
|
2463
|
+
`;
|
|
2464
|
+
}
|
|
2465
|
+
if (type === "message") {
|
|
2466
|
+
const role = typeof obj["role"] === "string" ? obj["role"] : "assistant";
|
|
2467
|
+
const content = extractMessageContent(obj["content"]);
|
|
2468
|
+
if (content === null) return null;
|
|
2469
|
+
const label = role.charAt(0).toUpperCase() + role.slice(1);
|
|
2470
|
+
return `## ${label}
|
|
2471
|
+
|
|
2472
|
+
${content}
|
|
2473
|
+
`;
|
|
2474
|
+
}
|
|
2475
|
+
if (type === "custom_message") {
|
|
2476
|
+
const label = typeof obj["label"] === "string" ? obj["label"] : "Custom";
|
|
2477
|
+
const text = typeof obj["text"] === "string" ? obj["text"] : "";
|
|
2478
|
+
return `### ${label}
|
|
2479
|
+
|
|
2480
|
+
${text}
|
|
2481
|
+
`;
|
|
2482
|
+
}
|
|
2483
|
+
return null;
|
|
2484
|
+
}
|
|
2485
|
+
function extractMessageContent(content) {
|
|
2486
|
+
if (typeof content === "string") return content;
|
|
2487
|
+
if (!Array.isArray(content)) return null;
|
|
2488
|
+
const parts = [];
|
|
2489
|
+
for (const block of content) {
|
|
2490
|
+
if (typeof block === "string") {
|
|
2491
|
+
parts.push(block);
|
|
2492
|
+
continue;
|
|
2493
|
+
}
|
|
2494
|
+
if (typeof block !== "object" || block === null) continue;
|
|
2495
|
+
const b = block;
|
|
2496
|
+
if (b["type"] === "text" && typeof b["text"] === "string") {
|
|
2497
|
+
parts.push(b["text"]);
|
|
2498
|
+
}
|
|
2499
|
+
}
|
|
2500
|
+
if (parts.length === 0) return null;
|
|
2501
|
+
return parts.join("\n\n");
|
|
2502
|
+
}
|
|
2503
|
+
function registerPiSessionsCommands(parent) {
|
|
2504
|
+
const sessions = parent.command("sessions").description("Inspect and resume Pi sessions");
|
|
2505
|
+
sessions.command("list").description("List Pi sessions (reads only line 1 of each JSONL file)").option("--no-subagents", "Skip sessions under subagents/").action(
|
|
2506
|
+
async (opts) => runLafsCommand("pi.sessions.list", "standard", async () => {
|
|
2507
|
+
const harness = requirePiHarness();
|
|
2508
|
+
const summaries = await harness.listSessions({
|
|
2509
|
+
includeSubagents: opts.includeSubagents !== false
|
|
2510
|
+
});
|
|
2511
|
+
return {
|
|
2512
|
+
count: summaries.length,
|
|
2513
|
+
sessions: summaries
|
|
2514
|
+
};
|
|
2515
|
+
})
|
|
2516
|
+
);
|
|
2517
|
+
sessions.command("show <id>").description("Show the full body of a Pi session by id").action(
|
|
2518
|
+
async (id) => runLafsCommand("pi.sessions.show", "full", async () => {
|
|
2519
|
+
const harness = requirePiHarness();
|
|
2520
|
+
const doc = await harness.showSession(id);
|
|
2521
|
+
return {
|
|
2522
|
+
summary: doc.summary,
|
|
2523
|
+
entryCount: doc.entries.length,
|
|
2524
|
+
entries: doc.entries
|
|
2525
|
+
};
|
|
2526
|
+
})
|
|
2527
|
+
);
|
|
2528
|
+
sessions.command("export <id>").description("Export a Pi session to JSONL or Markdown").option("--jsonl", "Emit the raw JSONL body (default)").option("--md", "Emit a Markdown transcription (messages only)").option("--output <path>", "Write to this file instead of stdout").action(
|
|
2529
|
+
async (id, opts) => runLafsCommand("pi.sessions.export", "standard", async () => {
|
|
2530
|
+
if (opts.jsonl === true && opts.md === true) {
|
|
2531
|
+
throw new LAFSCommandError(
|
|
2532
|
+
PI_ERROR_CODES.VALIDATION,
|
|
2533
|
+
"Cannot pass both --jsonl and --md",
|
|
2534
|
+
"Pick one of --jsonl or --md.",
|
|
2535
|
+
false
|
|
2536
|
+
);
|
|
2537
|
+
}
|
|
2538
|
+
const harness = requirePiHarness();
|
|
2539
|
+
const summaries = await harness.listSessions({ includeSubagents: true });
|
|
2540
|
+
const match = summaries.find((s) => s.id === id);
|
|
2541
|
+
if (match === void 0) {
|
|
2542
|
+
throw new LAFSCommandError(
|
|
2543
|
+
PI_ERROR_CODES.NOT_FOUND,
|
|
2544
|
+
`No session found with id ${id}`,
|
|
2545
|
+
"Run `caamp pi sessions list` to see known ids.",
|
|
2546
|
+
false
|
|
2547
|
+
);
|
|
2548
|
+
}
|
|
2549
|
+
const format = opts.md === true ? "md" : "jsonl";
|
|
2550
|
+
const emitted = format === "md" ? await streamSession(match.filePath, opts.output, sessionEntryToMarkdown) : await streamSession(
|
|
2551
|
+
match.filePath,
|
|
2552
|
+
opts.output,
|
|
2553
|
+
(line) => line.length === 0 ? null : line
|
|
2554
|
+
);
|
|
2555
|
+
return {
|
|
2556
|
+
id,
|
|
2557
|
+
format,
|
|
2558
|
+
filePath: match.filePath,
|
|
2559
|
+
output: opts.output ?? "stdout",
|
|
2560
|
+
entriesEmitted: emitted
|
|
2561
|
+
};
|
|
2562
|
+
})
|
|
2563
|
+
);
|
|
2564
|
+
sessions.command("resume <id>").description("Resume a Pi session by shelling out to `pi --session <id>`").action(
|
|
2565
|
+
async (id) => runLafsCommand("pi.sessions.resume", "standard", async () => {
|
|
2566
|
+
const harness = requirePiHarness();
|
|
2567
|
+
const summaries = await harness.listSessions({ includeSubagents: true });
|
|
2568
|
+
const match = summaries.find((s) => s.id === id);
|
|
2569
|
+
if (match === void 0) {
|
|
2570
|
+
throw new LAFSCommandError(
|
|
2571
|
+
PI_ERROR_CODES.NOT_FOUND,
|
|
2572
|
+
`No session found with id ${id}`,
|
|
2573
|
+
"Run `caamp pi sessions list` to see known ids.",
|
|
2574
|
+
false
|
|
2575
|
+
);
|
|
2576
|
+
}
|
|
2577
|
+
const piBinary = harness.provider.detection.binary ?? "pi";
|
|
2578
|
+
if (!existsSync6(piBinary) && piBinary === "pi") {
|
|
2579
|
+
}
|
|
2580
|
+
const child = spawn(piBinary, ["--session", id], {
|
|
2581
|
+
stdio: "inherit",
|
|
2582
|
+
detached: false
|
|
2583
|
+
});
|
|
2584
|
+
const exitCode = await new Promise((resolve3) => {
|
|
2585
|
+
child.on("exit", (code) => resolve3(code ?? 0));
|
|
2586
|
+
});
|
|
2587
|
+
if (exitCode !== 0) {
|
|
2588
|
+
throw new LAFSCommandError(
|
|
2589
|
+
PI_ERROR_CODES.TRANSIENT,
|
|
2590
|
+
`pi --session ${id} exited with code ${exitCode}`,
|
|
2591
|
+
"Check the Pi binary output for details.",
|
|
2592
|
+
true
|
|
2593
|
+
);
|
|
2594
|
+
}
|
|
2595
|
+
return {
|
|
2596
|
+
id,
|
|
2597
|
+
filePath: match.filePath,
|
|
2598
|
+
exitCode
|
|
2599
|
+
};
|
|
2600
|
+
})
|
|
2601
|
+
);
|
|
2602
|
+
}
|
|
2603
|
+
|
|
2604
|
+
// src/commands/pi/themes.ts
|
|
2605
|
+
import { existsSync as existsSync7, statSync } from "fs";
|
|
2606
|
+
import { extname, resolve as resolve2 } from "path";
|
|
2607
|
+
function inferThemeName(sourceFile) {
|
|
2608
|
+
const base = resolve2(sourceFile).split(/[\\/]/).pop();
|
|
2609
|
+
if (base === void 0 || base.length === 0) {
|
|
2610
|
+
throw new LAFSCommandError(
|
|
2611
|
+
PI_ERROR_CODES.VALIDATION,
|
|
2612
|
+
`Could not infer a theme name from source: ${sourceFile}`,
|
|
2613
|
+
"Pass --name <name> to override the inferred name.",
|
|
2614
|
+
false
|
|
2615
|
+
);
|
|
2616
|
+
}
|
|
2617
|
+
const ext = extname(base);
|
|
2618
|
+
if (ext === "") return base;
|
|
2619
|
+
return base.slice(0, -ext.length);
|
|
2620
|
+
}
|
|
2621
|
+
function registerPiThemesCommands(parent) {
|
|
2622
|
+
const themes = parent.command("themes").description("Manage Pi themes across tiers");
|
|
2623
|
+
themes.command("install <source>").description("Install a Pi theme file (.ts/.tsx/.mts/.json)").option("--scope <tier>", "Install tier: project|user|global (default: project)").option("--name <name>", "Override the inferred theme name").option("--force", "Overwrite an existing theme at the target tier").option("--project-dir <path>", "Project directory for the project tier (default: cwd)").action(
|
|
2624
|
+
async (source, opts) => runLafsCommand("pi.themes.install", "standard", async () => {
|
|
2625
|
+
const harness = requirePiHarness();
|
|
2626
|
+
const tier = parseScope2(opts.scope, "project");
|
|
2627
|
+
const projectDir = resolveProjectDir2(tier, opts.projectDir);
|
|
2628
|
+
const absSource = resolve2(source);
|
|
2629
|
+
if (!existsSync7(absSource)) {
|
|
2630
|
+
throw new LAFSCommandError(
|
|
2631
|
+
PI_ERROR_CODES.NOT_FOUND,
|
|
2632
|
+
`Source theme does not exist: ${absSource}`,
|
|
2633
|
+
"Check the path and try again.",
|
|
2634
|
+
false
|
|
2635
|
+
);
|
|
2636
|
+
}
|
|
2637
|
+
const stats = statSync(absSource);
|
|
2638
|
+
if (!stats.isFile()) {
|
|
2639
|
+
throw new LAFSCommandError(
|
|
2640
|
+
PI_ERROR_CODES.VALIDATION,
|
|
2641
|
+
`Source theme is not a regular file: ${absSource}`,
|
|
2642
|
+
"Themes must be a single .ts/.tsx/.mts/.json file.",
|
|
2643
|
+
false
|
|
2644
|
+
);
|
|
2645
|
+
}
|
|
2646
|
+
const name = opts.name ?? inferThemeName(absSource);
|
|
2647
|
+
const installOpts = { force: opts.force ?? false };
|
|
2648
|
+
const result = await harness.installTheme(absSource, name, tier, projectDir, installOpts);
|
|
2649
|
+
return {
|
|
2650
|
+
installed: {
|
|
2651
|
+
name,
|
|
2652
|
+
tier: result.tier,
|
|
2653
|
+
targetPath: result.targetPath,
|
|
2654
|
+
source: absSource
|
|
2655
|
+
}
|
|
2656
|
+
};
|
|
2657
|
+
})
|
|
2658
|
+
);
|
|
2659
|
+
themes.command("list").description("List Pi themes across project, user, and global tiers").option("--project-dir <path>", "Project directory for the project tier (default: cwd)").action(
|
|
2660
|
+
async (opts) => runLafsCommand("pi.themes.list", "standard", async () => {
|
|
2661
|
+
const harness = requirePiHarness();
|
|
2662
|
+
const projectDir = opts.projectDir ?? process.cwd();
|
|
2663
|
+
const entries = await harness.listThemes(projectDir);
|
|
2664
|
+
return {
|
|
2665
|
+
count: entries.length,
|
|
2666
|
+
themes: entries
|
|
2667
|
+
};
|
|
2668
|
+
})
|
|
2669
|
+
);
|
|
2670
|
+
themes.command("remove <name>").description("Remove a Pi theme from the given tier").option("--scope <tier>", "Target tier: project|user|global (default: project)").option("--project-dir <path>", "Project directory for the project tier (default: cwd)").action(
|
|
2671
|
+
async (name, opts) => runLafsCommand("pi.themes.remove", "standard", async () => {
|
|
2672
|
+
const harness = requirePiHarness();
|
|
2673
|
+
const tier = parseScope2(opts.scope, "project");
|
|
2674
|
+
const projectDir = resolveProjectDir2(tier, opts.projectDir);
|
|
2675
|
+
const removed = await harness.removeTheme(name, tier, projectDir);
|
|
2676
|
+
return { name, tier, removed };
|
|
2677
|
+
})
|
|
2678
|
+
);
|
|
2679
|
+
}
|
|
2680
|
+
|
|
2681
|
+
// src/commands/pi/index.ts
|
|
2682
|
+
function registerPiCommands(program2) {
|
|
2683
|
+
const pi = program2.command("pi").description("Pi harness operations (extensions, sessions, models, prompts, themes)");
|
|
2684
|
+
registerPiExtensionsCommands(pi);
|
|
2685
|
+
registerPiSessionsCommands(pi);
|
|
2686
|
+
registerPiModelsCommands(pi);
|
|
2687
|
+
registerPiPromptsCommands(pi);
|
|
2688
|
+
registerPiThemesCommands(pi);
|
|
2689
|
+
}
|
|
2690
|
+
|
|
1540
2691
|
// src/commands/providers.ts
|
|
1541
2692
|
import { randomUUID as randomUUID3 } from "crypto";
|
|
1542
2693
|
import { resolveOutputFormat as resolveOutputFormat2 } from "@cleocode/lafs";
|
|
@@ -2083,9 +3234,9 @@ CAMP Hook Support (mappings v${getHookMappingsVersion()})
|
|
|
2083
3234
|
console.log(` ${"\u2500".repeat(22)} ${"\u2500".repeat(20)} ${"\u2500".repeat(8)} ${"\u2500".repeat(20)}`);
|
|
2084
3235
|
for (const row of matrix) {
|
|
2085
3236
|
const hooks2 = row.hooksCount > 0 ? String(row.hooksCount) : "-";
|
|
2086
|
-
const
|
|
3237
|
+
const spawn2 = row.spawnMechanism ?? "-";
|
|
2087
3238
|
console.log(
|
|
2088
|
-
` ${row.toolName.padEnd(22)} ${row.skillsPrecedence.padEnd(20)} ${hooks2.padEnd(8)} ${
|
|
3239
|
+
` ${row.toolName.padEnd(22)} ${row.skillsPrecedence.padEnd(20)} ${hooks2.padEnd(8)} ${spawn2}`
|
|
2089
3240
|
);
|
|
2090
3241
|
}
|
|
2091
3242
|
console.log(pc6.dim(`
|
|
@@ -2126,13 +3277,13 @@ function emitJsonError2(operation, mvi, code, message, category, details = {}) {
|
|
|
2126
3277
|
}
|
|
2127
3278
|
|
|
2128
3279
|
// src/commands/skills/audit.ts
|
|
2129
|
-
import { existsSync as
|
|
3280
|
+
import { existsSync as existsSync8, statSync as statSync2 } from "fs";
|
|
2130
3281
|
import pc7 from "picocolors";
|
|
2131
3282
|
function registerSkillsAudit(parent) {
|
|
2132
3283
|
parent.command("audit").description("Security scan skill files (46+ rules, SARIF output)").argument("[path]", "Path to SKILL.md or directory", ".").option("--sarif", "Output in SARIF format (raw SARIF, not LAFS envelope)").option("--json", "Output as JSON (LAFS envelope)").option("--human", "Output in human-readable format").action(async (path, opts) => {
|
|
2133
3284
|
const operation = "skills.audit";
|
|
2134
3285
|
const mvi = "standard";
|
|
2135
|
-
if (!
|
|
3286
|
+
if (!existsSync8(path)) {
|
|
2136
3287
|
const message = `Path not found: ${path}`;
|
|
2137
3288
|
if (opts.sarif) {
|
|
2138
3289
|
console.error(
|
|
@@ -2194,7 +3345,7 @@ function registerSkillsAudit(parent) {
|
|
|
2194
3345
|
);
|
|
2195
3346
|
process.exit(1);
|
|
2196
3347
|
}
|
|
2197
|
-
const stat =
|
|
3348
|
+
const stat = statSync2(path);
|
|
2198
3349
|
let results;
|
|
2199
3350
|
try {
|
|
2200
3351
|
if (stat.isFile()) {
|
|
@@ -2704,9 +3855,9 @@ function emitJsonError3(operation, mvi, code, message, category, details = {}) {
|
|
|
2704
3855
|
}
|
|
2705
3856
|
|
|
2706
3857
|
// src/commands/skills/init.ts
|
|
2707
|
-
import { existsSync as
|
|
2708
|
-
import { mkdir, writeFile } from "fs/promises";
|
|
2709
|
-
import { join as
|
|
3858
|
+
import { existsSync as existsSync9 } from "fs";
|
|
3859
|
+
import { mkdir, writeFile as writeFile2 } from "fs/promises";
|
|
3860
|
+
import { join as join7 } from "path";
|
|
2710
3861
|
import pc10 from "picocolors";
|
|
2711
3862
|
function registerSkillsInit(parent) {
|
|
2712
3863
|
parent.command("init").description("Create a new SKILL.md template").argument("[name]", "Skill name").option("-d, --dir <path>", "Output directory", ".").option("--json", "Output as JSON (default)").option("--human", "Output in human-readable format").action(
|
|
@@ -2732,8 +3883,8 @@ function registerSkillsInit(parent) {
|
|
|
2732
3883
|
process.exit(1);
|
|
2733
3884
|
}
|
|
2734
3885
|
const skillName = name ?? "my-skill";
|
|
2735
|
-
const skillDir =
|
|
2736
|
-
if (
|
|
3886
|
+
const skillDir = join7(opts.dir, skillName);
|
|
3887
|
+
if (existsSync9(skillDir)) {
|
|
2737
3888
|
const message = `Directory already exists: ${skillDir}`;
|
|
2738
3889
|
if (format === "json") {
|
|
2739
3890
|
emitJsonError(
|
|
@@ -2775,7 +3926,7 @@ Provide detailed instructions for the AI agent here.
|
|
|
2775
3926
|
|
|
2776
3927
|
Show example inputs and expected outputs.
|
|
2777
3928
|
`;
|
|
2778
|
-
await
|
|
3929
|
+
await writeFile2(join7(skillDir, "SKILL.md"), template, "utf-8");
|
|
2779
3930
|
const result = {
|
|
2780
3931
|
name: skillName,
|
|
2781
3932
|
directory: skillDir,
|
|
@@ -2789,69 +3940,15 @@ Show example inputs and expected outputs.
|
|
|
2789
3940
|
console.log(pc10.green(`\u2713 Created skill template: ${skillDir}/SKILL.md`));
|
|
2790
3941
|
console.log(pc10.dim("\nNext steps:"));
|
|
2791
3942
|
console.log(pc10.dim(" 1. Edit SKILL.md with your instructions"));
|
|
2792
|
-
console.log(pc10.dim(` 2. Validate: caamp skills validate ${
|
|
3943
|
+
console.log(pc10.dim(` 2. Validate: caamp skills validate ${join7(skillDir, "SKILL.md")}`));
|
|
2793
3944
|
console.log(pc10.dim(` 3. Install: caamp skills install ${skillDir}`));
|
|
2794
3945
|
}
|
|
2795
3946
|
);
|
|
2796
3947
|
}
|
|
2797
3948
|
|
|
2798
3949
|
// src/commands/skills/install.ts
|
|
2799
|
-
import { existsSync as
|
|
3950
|
+
import { existsSync as existsSync10 } from "fs";
|
|
2800
3951
|
import pc11 from "picocolors";
|
|
2801
|
-
|
|
2802
|
-
// src/core/sources/github.ts
|
|
2803
|
-
import { mkdtemp, rm } from "fs/promises";
|
|
2804
|
-
import { tmpdir } from "os";
|
|
2805
|
-
import { join as join4 } from "path";
|
|
2806
|
-
import { simpleGit } from "simple-git";
|
|
2807
|
-
async function cloneRepo(owner, repo, ref, subPath) {
|
|
2808
|
-
const tmpDir = await mkdtemp(join4(tmpdir(), "caamp-"));
|
|
2809
|
-
const repoUrl = `https://github.com/${owner}/${repo}.git`;
|
|
2810
|
-
const git = simpleGit();
|
|
2811
|
-
const cloneOptions = ["--depth", "1"];
|
|
2812
|
-
if (ref) {
|
|
2813
|
-
cloneOptions.push("--branch", ref);
|
|
2814
|
-
}
|
|
2815
|
-
await git.clone(repoUrl, tmpDir, cloneOptions);
|
|
2816
|
-
const localPath = subPath ? join4(tmpDir, subPath) : tmpDir;
|
|
2817
|
-
return {
|
|
2818
|
-
localPath,
|
|
2819
|
-
cleanup: async () => {
|
|
2820
|
-
try {
|
|
2821
|
-
await rm(tmpDir, { recursive: true });
|
|
2822
|
-
} catch {
|
|
2823
|
-
}
|
|
2824
|
-
}
|
|
2825
|
-
};
|
|
2826
|
-
}
|
|
2827
|
-
|
|
2828
|
-
// src/core/sources/gitlab.ts
|
|
2829
|
-
import { mkdtemp as mkdtemp2, rm as rm2 } from "fs/promises";
|
|
2830
|
-
import { tmpdir as tmpdir2 } from "os";
|
|
2831
|
-
import { join as join5 } from "path";
|
|
2832
|
-
import { simpleGit as simpleGit2 } from "simple-git";
|
|
2833
|
-
async function cloneGitLabRepo(owner, repo, ref, subPath) {
|
|
2834
|
-
const tmpDir = await mkdtemp2(join5(tmpdir2(), "caamp-gl-"));
|
|
2835
|
-
const repoUrl = `https://gitlab.com/${owner}/${repo}.git`;
|
|
2836
|
-
const git = simpleGit2();
|
|
2837
|
-
const cloneOptions = ["--depth", "1"];
|
|
2838
|
-
if (ref) {
|
|
2839
|
-
cloneOptions.push("--branch", ref);
|
|
2840
|
-
}
|
|
2841
|
-
await git.clone(repoUrl, tmpDir, cloneOptions);
|
|
2842
|
-
const localPath = subPath ? join5(tmpDir, subPath) : tmpDir;
|
|
2843
|
-
return {
|
|
2844
|
-
localPath,
|
|
2845
|
-
cleanup: async () => {
|
|
2846
|
-
try {
|
|
2847
|
-
await rm2(tmpDir, { recursive: true });
|
|
2848
|
-
} catch {
|
|
2849
|
-
}
|
|
2850
|
-
}
|
|
2851
|
-
};
|
|
2852
|
-
}
|
|
2853
|
-
|
|
2854
|
-
// src/commands/skills/install.ts
|
|
2855
3952
|
function registerSkillsInstall(parent) {
|
|
2856
3953
|
parent.command("install").description("Install a skill from GitHub, URL, marketplace, or registered skill library").argument("[source]", "Skill source (GitHub URL, owner/repo, @author/name, skill-name)").option(
|
|
2857
3954
|
"-a, --agent <name>",
|
|
@@ -3335,7 +4432,7 @@ async function handleMarketplaceSource(source, _providers, _isGlobal, format, op
|
|
|
3335
4432
|
for (const subPath of subPathCandidates) {
|
|
3336
4433
|
try {
|
|
3337
4434
|
const result = await cloneRepo(parsed.owner, parsed.repo, parsed.ref, subPath);
|
|
3338
|
-
if (subPath && !
|
|
4435
|
+
if (subPath && !existsSync10(result.localPath)) {
|
|
3339
4436
|
await result.cleanup();
|
|
3340
4437
|
continue;
|
|
3341
4438
|
}
|
|
@@ -3650,8 +4747,8 @@ ${outdated.length} skill(s) have updates available:
|
|
|
3650
4747
|
if (!opts.yes && format === "human") {
|
|
3651
4748
|
const readline = await import("readline");
|
|
3652
4749
|
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
3653
|
-
const answer = await new Promise((
|
|
3654
|
-
rl.question(pc14.dim("\nProceed with update? [y/N] "),
|
|
4750
|
+
const answer = await new Promise((resolve3) => {
|
|
4751
|
+
rl.question(pc14.dim("\nProceed with update? [y/N] "), resolve3);
|
|
3655
4752
|
});
|
|
3656
4753
|
rl.close();
|
|
3657
4754
|
if (answer.toLowerCase() !== "y" && answer.toLowerCase() !== "yes") {
|
|
@@ -3884,6 +4981,8 @@ registerInstructionsCommands(program);
|
|
|
3884
4981
|
registerConfigCommand(program);
|
|
3885
4982
|
registerDoctorCommand(program);
|
|
3886
4983
|
registerAdvancedCommands(program);
|
|
4984
|
+
registerMcpCommands(program);
|
|
4985
|
+
registerPiCommands(program);
|
|
3887
4986
|
function toError(error) {
|
|
3888
4987
|
if (error instanceof Error) return error;
|
|
3889
4988
|
return new Error(String(error));
|