@cleocode/caamp 2026.4.6 → 2026.4.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.js CHANGED
@@ -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-43GULI6J.js";
55
+ } from "./chunk-JC77OAHA.js";
48
56
  import {
49
57
  buildSkillsMap,
50
58
  checkAllInjections,
@@ -1537,6 +1545,1399 @@ 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/cant.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/cant.ts
1993
+ async function resolveCantSource(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
+ return {
2005
+ localPath: expanded,
2006
+ cleanup: async () => {
2007
+ },
2008
+ inferredName: inferNameFromPath(expanded)
2009
+ };
2010
+ }
2011
+ if (/^https?:\/\//.test(source)) {
2012
+ const parsed2 = parseSource(source);
2013
+ if (parsed2.type === "github" && parsed2.owner !== void 0 && parsed2.repo !== void 0) {
2014
+ const cloneResult = await cloneRepo(parsed2.owner, parsed2.repo, parsed2.ref);
2015
+ const filePath = parsed2.path !== void 0 ? join5(cloneResult.localPath, parsed2.path) : cloneResult.localPath;
2016
+ if (!existsSync4(filePath)) {
2017
+ await cloneResult.cleanup();
2018
+ throw new LAFSCommandError(
2019
+ PI_ERROR_CODES.NOT_FOUND,
2020
+ `Source path not found inside cloned repo: ${parsed2.path ?? "(root)"}`,
2021
+ "Check the repository URL and path.",
2022
+ false
2023
+ );
2024
+ }
2025
+ return {
2026
+ localPath: filePath,
2027
+ cleanup: cloneResult.cleanup,
2028
+ inferredName: inferNameFromPath(filePath)
2029
+ };
2030
+ }
2031
+ if (parsed2.type === "gitlab" && parsed2.owner !== void 0 && parsed2.repo !== void 0) {
2032
+ const cloneResult = await cloneGitLabRepo(parsed2.owner, parsed2.repo, parsed2.ref);
2033
+ const filePath = parsed2.path !== void 0 ? join5(cloneResult.localPath, parsed2.path) : cloneResult.localPath;
2034
+ if (!existsSync4(filePath)) {
2035
+ await cloneResult.cleanup();
2036
+ throw new LAFSCommandError(
2037
+ PI_ERROR_CODES.NOT_FOUND,
2038
+ `Source path not found inside cloned repo: ${parsed2.path ?? "(root)"}`,
2039
+ "Check the repository URL and path.",
2040
+ false
2041
+ );
2042
+ }
2043
+ return {
2044
+ localPath: filePath,
2045
+ cleanup: cloneResult.cleanup,
2046
+ inferredName: inferNameFromPath(filePath)
2047
+ };
2048
+ }
2049
+ const resp = await fetchWithTimeout(source);
2050
+ if (!resp.ok) {
2051
+ throw new LAFSCommandError(
2052
+ PI_ERROR_CODES.TRANSIENT,
2053
+ `Failed to download source from ${source}: HTTP ${resp.status}`,
2054
+ "Check the URL and network connectivity.",
2055
+ true
2056
+ );
2057
+ }
2058
+ const body = await resp.text();
2059
+ const baseName = inferNameFromUrl(source);
2060
+ const tmp = join5(tmpdir3(), `caamp-pi-cant-${process.pid}-${Date.now()}-${baseName}.cant`);
2061
+ await writeFile(tmp, body, "utf8");
2062
+ return {
2063
+ localPath: tmp,
2064
+ cleanup: async () => {
2065
+ try {
2066
+ await (await import("fs/promises")).rm(tmp, { force: true });
2067
+ } catch {
2068
+ }
2069
+ },
2070
+ inferredName: baseName
2071
+ };
2072
+ }
2073
+ const parsed = parseSource(source);
2074
+ if (parsed.type === "github" && parsed.owner !== void 0 && parsed.repo !== void 0) {
2075
+ const cloneResult = await cloneRepo(parsed.owner, parsed.repo, parsed.ref);
2076
+ const filePath = parsed.path !== void 0 ? join5(cloneResult.localPath, parsed.path) : cloneResult.localPath;
2077
+ if (!existsSync4(filePath)) {
2078
+ await cloneResult.cleanup();
2079
+ throw new LAFSCommandError(
2080
+ PI_ERROR_CODES.NOT_FOUND,
2081
+ `Source path not found inside cloned repo: ${parsed.path ?? "(root)"}`,
2082
+ "Check the repository shorthand and path.",
2083
+ false
2084
+ );
2085
+ }
2086
+ return {
2087
+ localPath: filePath,
2088
+ cleanup: cloneResult.cleanup,
2089
+ inferredName: inferNameFromPath(filePath)
2090
+ };
2091
+ }
2092
+ throw new LAFSCommandError(
2093
+ PI_ERROR_CODES.VALIDATION,
2094
+ `Unsupported source: ${source}`,
2095
+ "Use a local file path, HTTPS URL, or GitHub shorthand (owner/repo/path.cant).",
2096
+ false
2097
+ );
2098
+ }
2099
+ function inferNameFromPath(filePath) {
2100
+ const base = filePath.split(/[/\\]/).pop() ?? filePath;
2101
+ return base.replace(/\.cant$/, "");
2102
+ }
2103
+ function inferNameFromUrl(url) {
2104
+ try {
2105
+ const u = new URL(url);
2106
+ const seg = u.pathname.split("/").filter(Boolean).pop() ?? "profile";
2107
+ return seg.replace(/\.cant$/, "");
2108
+ } catch {
2109
+ return "profile";
2110
+ }
2111
+ }
2112
+ async function invokeInstallCantProfile(harness, sourcePath, name, tier, projectDir, installOpts) {
2113
+ try {
2114
+ return await harness.installCantProfile(sourcePath, name, tier, projectDir, installOpts);
2115
+ } catch (err) {
2116
+ rethrowAsLafs(err);
2117
+ }
2118
+ }
2119
+ function rethrowAsLafs(err) {
2120
+ const message = err instanceof Error ? err.message : String(err);
2121
+ if (/already exists/i.test(message)) {
2122
+ throw new LAFSCommandError(
2123
+ PI_ERROR_CODES.CONFLICT,
2124
+ message,
2125
+ "Pass --force to overwrite the existing profile.",
2126
+ false
2127
+ );
2128
+ }
2129
+ if (/does not exist|not found/i.test(message)) {
2130
+ throw new LAFSCommandError(PI_ERROR_CODES.NOT_FOUND, message, "Check the path.", false);
2131
+ }
2132
+ if (/failed cant-core validation|expected a CANT source file|not a regular file/i.test(message)) {
2133
+ throw new LAFSCommandError(
2134
+ PI_ERROR_CODES.VALIDATION,
2135
+ message,
2136
+ "Run `caamp pi cant validate <path>` to inspect the diagnostics.",
2137
+ false
2138
+ );
2139
+ }
2140
+ throw new LAFSCommandError(
2141
+ "E_INTERNAL_UNEXPECTED",
2142
+ message,
2143
+ "Inspect the message for the underlying cant-core failure mode.",
2144
+ false
2145
+ );
2146
+ }
2147
+ function registerPiCantCommands(parent) {
2148
+ const cant = parent.command("cant").description("Manage Pi CANT profiles across tiers");
2149
+ cant.command("list").description("List Pi CANT profiles across project, user, and global tiers").option("--scope <tier>", "Filter to a single tier: project|user|global").option("--project-dir <path>", "Project directory for the project tier (default: cwd)").action(
2150
+ async (opts) => runLafsCommand("pi.cant.list", "standard", async () => {
2151
+ const harness = requirePiHarness();
2152
+ const projectDir = opts.projectDir ?? process.cwd();
2153
+ const allEntries = await harness.listCantProfiles(projectDir);
2154
+ const filterTier = opts.scope === void 0 ? null : parseScope2(opts.scope, "project");
2155
+ const entries = filterTier === null ? allEntries : allEntries.filter((e) => e.tier === filterTier);
2156
+ const sorted = [...entries].sort((a, b) => a.name.localeCompare(b.name));
2157
+ return {
2158
+ count: sorted.length,
2159
+ entries: sorted
2160
+ };
2161
+ })
2162
+ );
2163
+ cant.command("install <source>").description("Install a Pi CANT profile 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 profile name").option("--force", "Overwrite an existing profile at the target tier").option("--project-dir <path>", "Project directory for the project tier (default: cwd)").action(
2164
+ async (source, opts) => runLafsCommand("pi.cant.install", "standard", async () => {
2165
+ const harness = requirePiHarness();
2166
+ const tier = parseScope2(opts.scope, "project");
2167
+ const projectDir = resolveProjectDir2(tier, opts.projectDir);
2168
+ const resolved = await resolveCantSource(source);
2169
+ try {
2170
+ const name = opts.name ?? resolved.inferredName;
2171
+ const installOpts = { force: opts.force ?? false };
2172
+ const result = await invokeInstallCantProfile(
2173
+ harness,
2174
+ resolved.localPath,
2175
+ name,
2176
+ tier,
2177
+ projectDir,
2178
+ installOpts
2179
+ );
2180
+ return {
2181
+ installed: {
2182
+ name,
2183
+ tier: result.tier,
2184
+ targetPath: result.targetPath,
2185
+ counts: result.counts,
2186
+ source
2187
+ }
2188
+ };
2189
+ } finally {
2190
+ await resolved.cleanup();
2191
+ }
2192
+ })
2193
+ );
2194
+ cant.command("remove <name>").description("Remove a Pi CANT profile 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(
2195
+ async (name, opts) => runLafsCommand("pi.cant.remove", "standard", async () => {
2196
+ const harness = requirePiHarness();
2197
+ const tier = parseScope2(opts.scope, "project");
2198
+ const projectDir = resolveProjectDir2(tier, opts.projectDir);
2199
+ const removed = await harness.removeCantProfile(name, tier, projectDir);
2200
+ return {
2201
+ name,
2202
+ tier,
2203
+ removed
2204
+ };
2205
+ })
2206
+ );
2207
+ cant.command("validate <path>").description("Validate a .cant file via cant-core without installing it").action(
2208
+ async (path) => runLafsCommand("pi.cant.validate", "standard", async () => {
2209
+ const harness = requirePiHarness();
2210
+ if (!existsSync4(path)) {
2211
+ throw new LAFSCommandError(
2212
+ PI_ERROR_CODES.NOT_FOUND,
2213
+ `Source file does not exist: ${path}`,
2214
+ "Check the path and try again.",
2215
+ false
2216
+ );
2217
+ }
2218
+ const result = await harness.validateCantProfile(path);
2219
+ if (!result.valid) {
2220
+ throw new LAFSCommandError(
2221
+ PI_ERROR_CODES.VALIDATION,
2222
+ `cant-core validation failed with ${result.errors.length} diagnostic(s)`,
2223
+ "See the `errors` field for ruleId/line/col/message details.",
2224
+ false,
2225
+ { valid: false, counts: result.counts, errors: result.errors }
2226
+ );
2227
+ }
2228
+ return {
2229
+ valid: true,
2230
+ counts: result.counts,
2231
+ errors: result.errors
2232
+ };
2233
+ })
2234
+ );
2235
+ }
2236
+
2237
+ // src/commands/pi/extensions.ts
2238
+ import { existsSync as existsSync5 } from "fs";
2239
+ import { writeFile as writeFile2 } from "fs/promises";
2240
+ import { tmpdir as tmpdir4 } from "os";
2241
+ import { join as join6 } from "path";
2242
+ async function resolveExtensionSource(source) {
2243
+ if (source.startsWith("/") || source.startsWith("./") || source.startsWith("../") || source.startsWith("~")) {
2244
+ const expanded = source.startsWith("~/") ? join6(process.env["HOME"] ?? "", source.slice(2)) : source;
2245
+ if (!existsSync5(expanded)) {
2246
+ throw new LAFSCommandError(
2247
+ PI_ERROR_CODES.NOT_FOUND,
2248
+ `Source file does not exist: ${expanded}`,
2249
+ "Check the path and try again.",
2250
+ false
2251
+ );
2252
+ }
2253
+ const inferredName = inferNameFromPath2(expanded);
2254
+ return {
2255
+ localPath: expanded,
2256
+ cleanup: async () => {
2257
+ },
2258
+ inferredName
2259
+ };
2260
+ }
2261
+ if (/^https?:\/\//.test(source)) {
2262
+ const parsed2 = parseSource(source);
2263
+ if (parsed2.type === "github" && parsed2.owner !== void 0 && parsed2.repo !== void 0) {
2264
+ const cloneResult = await cloneRepo(parsed2.owner, parsed2.repo, parsed2.ref);
2265
+ const filePath = parsed2.path !== void 0 ? join6(cloneResult.localPath, parsed2.path) : cloneResult.localPath;
2266
+ if (!existsSync5(filePath)) {
2267
+ await cloneResult.cleanup();
2268
+ throw new LAFSCommandError(
2269
+ PI_ERROR_CODES.NOT_FOUND,
2270
+ `Source path not found inside cloned repo: ${parsed2.path ?? "(root)"}`,
2271
+ "Check the repository URL and path.",
2272
+ false
2273
+ );
2274
+ }
2275
+ return {
2276
+ localPath: filePath,
2277
+ cleanup: cloneResult.cleanup,
2278
+ inferredName: inferNameFromPath2(filePath)
2279
+ };
2280
+ }
2281
+ if (parsed2.type === "gitlab" && parsed2.owner !== void 0 && parsed2.repo !== void 0) {
2282
+ const cloneResult = await cloneGitLabRepo(parsed2.owner, parsed2.repo, parsed2.ref);
2283
+ const filePath = parsed2.path !== void 0 ? join6(cloneResult.localPath, parsed2.path) : cloneResult.localPath;
2284
+ if (!existsSync5(filePath)) {
2285
+ await cloneResult.cleanup();
2286
+ throw new LAFSCommandError(
2287
+ PI_ERROR_CODES.NOT_FOUND,
2288
+ `Source path not found inside cloned repo: ${parsed2.path ?? "(root)"}`,
2289
+ "Check the repository URL and path.",
2290
+ false
2291
+ );
2292
+ }
2293
+ return {
2294
+ localPath: filePath,
2295
+ cleanup: cloneResult.cleanup,
2296
+ inferredName: inferNameFromPath2(filePath)
2297
+ };
2298
+ }
2299
+ const resp = await fetchWithTimeout(source);
2300
+ if (!resp.ok) {
2301
+ throw new LAFSCommandError(
2302
+ PI_ERROR_CODES.TRANSIENT,
2303
+ `Failed to download source from ${source}: HTTP ${resp.status}`,
2304
+ "Check the URL and network connectivity.",
2305
+ true
2306
+ );
2307
+ }
2308
+ const body = await resp.text();
2309
+ const baseName = inferNameFromUrl2(source);
2310
+ const tmp = join6(tmpdir4(), `caamp-pi-ext-${process.pid}-${Date.now()}-${baseName}.ts`);
2311
+ await writeFile2(tmp, body, "utf8");
2312
+ return {
2313
+ localPath: tmp,
2314
+ cleanup: async () => {
2315
+ try {
2316
+ await (await import("fs/promises")).rm(tmp, { force: true });
2317
+ } catch {
2318
+ }
2319
+ },
2320
+ inferredName: baseName
2321
+ };
2322
+ }
2323
+ const parsed = parseSource(source);
2324
+ if (parsed.type === "github" && parsed.owner !== void 0 && parsed.repo !== void 0) {
2325
+ const cloneResult = await cloneRepo(parsed.owner, parsed.repo, parsed.ref);
2326
+ const filePath = parsed.path !== void 0 ? join6(cloneResult.localPath, parsed.path) : cloneResult.localPath;
2327
+ if (!existsSync5(filePath)) {
2328
+ await cloneResult.cleanup();
2329
+ throw new LAFSCommandError(
2330
+ PI_ERROR_CODES.NOT_FOUND,
2331
+ `Source path not found inside cloned repo: ${parsed.path ?? "(root)"}`,
2332
+ "Check the repository shorthand and path.",
2333
+ false
2334
+ );
2335
+ }
2336
+ return {
2337
+ localPath: filePath,
2338
+ cleanup: cloneResult.cleanup,
2339
+ inferredName: inferNameFromPath2(filePath)
2340
+ };
2341
+ }
2342
+ throw new LAFSCommandError(
2343
+ PI_ERROR_CODES.VALIDATION,
2344
+ `Unsupported source: ${source}`,
2345
+ "Use a local file path, HTTPS URL, or GitHub shorthand (owner/repo/path.ts).",
2346
+ false
2347
+ );
2348
+ }
2349
+ function inferNameFromPath2(filePath) {
2350
+ const base = filePath.split(/[/\\]/).pop() ?? filePath;
2351
+ return base.replace(/\.(ts|tsx|mts)$/, "");
2352
+ }
2353
+ function inferNameFromUrl2(url) {
2354
+ try {
2355
+ const u = new URL(url);
2356
+ const seg = u.pathname.split("/").filter(Boolean).pop() ?? "extension";
2357
+ return seg.replace(/\.(ts|tsx|mts)$/, "");
2358
+ } catch {
2359
+ return "extension";
2360
+ }
2361
+ }
2362
+ function registerPiExtensionsCommands(parent) {
2363
+ const ext = parent.command("extensions").description("Manage Pi extensions across tiers");
2364
+ 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(
2365
+ async (opts) => runLafsCommand("pi.extensions.list", "standard", async () => {
2366
+ const harness = requirePiHarness();
2367
+ const projectDir = opts.projectDir ?? process.cwd();
2368
+ const entries = await harness.listExtensions(projectDir);
2369
+ return {
2370
+ count: entries.length,
2371
+ extensions: entries
2372
+ };
2373
+ })
2374
+ );
2375
+ 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(
2376
+ async (source, opts) => runLafsCommand("pi.extensions.install", "standard", async () => {
2377
+ const harness = requirePiHarness();
2378
+ const tier = parseScope2(opts.scope, "project");
2379
+ const projectDir = resolveProjectDir2(tier, opts.projectDir);
2380
+ const resolved = await resolveExtensionSource(source);
2381
+ try {
2382
+ const name = opts.name ?? resolved.inferredName;
2383
+ const installOpts = { force: opts.force ?? false };
2384
+ const result = await harness.installExtension(
2385
+ resolved.localPath,
2386
+ name,
2387
+ tier,
2388
+ projectDir,
2389
+ installOpts
2390
+ );
2391
+ return {
2392
+ installed: {
2393
+ name,
2394
+ tier: result.tier,
2395
+ targetPath: result.targetPath,
2396
+ source
2397
+ }
2398
+ };
2399
+ } finally {
2400
+ await resolved.cleanup();
2401
+ }
2402
+ })
2403
+ );
2404
+ 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(
2405
+ async (name, opts) => runLafsCommand("pi.extensions.remove", "standard", async () => {
2406
+ const harness = requirePiHarness();
2407
+ const tier = parseScope2(opts.scope, "project");
2408
+ const projectDir = resolveProjectDir2(tier, opts.projectDir);
2409
+ const removed = await harness.removeExtension(name, tier, projectDir);
2410
+ return {
2411
+ name,
2412
+ tier,
2413
+ removed
2414
+ };
2415
+ })
2416
+ );
2417
+ }
2418
+
2419
+ // src/commands/pi/models.ts
2420
+ function parseModelSpec(spec) {
2421
+ const idx = spec.indexOf(":");
2422
+ if (idx <= 0 || idx === spec.length - 1) {
2423
+ throw new LAFSCommandError(
2424
+ PI_ERROR_CODES.VALIDATION,
2425
+ `Invalid model specifier: ${spec}`,
2426
+ "Use 'provider:model-id', e.g. 'anthropic:claude-sonnet-4-20250514'.",
2427
+ false
2428
+ );
2429
+ }
2430
+ return { provider: spec.slice(0, idx), id: spec.slice(idx + 1) };
2431
+ }
2432
+ function resolveModelsScope(opts) {
2433
+ if (opts.global === true) return { kind: "global" };
2434
+ if (opts.projectDir !== void 0 && opts.projectDir.length > 0) {
2435
+ return { kind: "project", projectDir: opts.projectDir };
2436
+ }
2437
+ return { kind: "global" };
2438
+ }
2439
+ function parsePositiveInt(raw, name) {
2440
+ if (raw === void 0) return void 0;
2441
+ const parsed = Number.parseInt(raw, 10);
2442
+ if (!Number.isFinite(parsed) || parsed <= 0 || String(parsed) !== raw.trim()) {
2443
+ throw new LAFSCommandError(
2444
+ PI_ERROR_CODES.VALIDATION,
2445
+ `Invalid value for --${name}: ${raw}`,
2446
+ `--${name} must be a positive integer.`,
2447
+ false
2448
+ );
2449
+ }
2450
+ return parsed;
2451
+ }
2452
+ function registerPiModelsCommands(parent) {
2453
+ const models = parent.command("models").description("Manage Pi's dual-file models configuration");
2454
+ 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(
2455
+ async (opts) => runLafsCommand("pi.models.list", "standard", async () => {
2456
+ const harness = requirePiHarness();
2457
+ const scope = resolveModelsScope(opts);
2458
+ const entries = await harness.listModels(scope);
2459
+ const active = entries.filter((e) => e.enabled);
2460
+ const def = entries.find((e) => e.isDefault) ?? null;
2461
+ return {
2462
+ scope: scope.kind,
2463
+ count: entries.length,
2464
+ activeCount: active.length,
2465
+ default: def,
2466
+ models: entries
2467
+ };
2468
+ })
2469
+ );
2470
+ 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(
2471
+ async (spec, opts) => runLafsCommand("pi.models.add", "standard", async () => {
2472
+ const harness = requirePiHarness();
2473
+ const scope = resolveModelsScope(opts);
2474
+ const { provider, id } = parseModelSpec(spec);
2475
+ const contextWindow = parsePositiveInt(opts.contextWindow, "context-window");
2476
+ const maxTokens = parsePositiveInt(opts.maxTokens, "max-tokens");
2477
+ const config = await harness.readModelsConfig(scope);
2478
+ const providerBlock = config.providers[provider] ?? {};
2479
+ if (opts.baseUrl !== void 0) providerBlock.baseUrl = opts.baseUrl;
2480
+ const nextModels = providerBlock.models ? [...providerBlock.models] : [];
2481
+ const existingIdx = nextModels.findIndex((m) => m.id === id);
2482
+ const definition = {
2483
+ id,
2484
+ name: opts.displayName ?? id
2485
+ };
2486
+ if (opts.reasoning === true) definition.reasoning = true;
2487
+ if (contextWindow !== void 0) definition.contextWindow = contextWindow;
2488
+ if (maxTokens !== void 0) definition.maxTokens = maxTokens;
2489
+ if (existingIdx >= 0) {
2490
+ nextModels[existingIdx] = definition;
2491
+ } else {
2492
+ nextModels.push(definition);
2493
+ }
2494
+ providerBlock.models = nextModels;
2495
+ const nextConfig = {
2496
+ providers: { ...config.providers, [provider]: providerBlock }
2497
+ };
2498
+ await harness.writeModelsConfig(nextConfig, scope);
2499
+ return {
2500
+ added: { provider, id, name: definition.name },
2501
+ replaced: existingIdx >= 0,
2502
+ scope: scope.kind
2503
+ };
2504
+ })
2505
+ );
2506
+ 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(
2507
+ async (spec, opts) => runLafsCommand("pi.models.remove", "standard", async () => {
2508
+ const harness = requirePiHarness();
2509
+ const scope = resolveModelsScope(opts);
2510
+ const { provider, id } = parseModelSpec(spec);
2511
+ const config = await harness.readModelsConfig(scope);
2512
+ const providerBlock = config.providers[provider];
2513
+ if (providerBlock === void 0 || providerBlock.models === void 0) {
2514
+ return { removed: false, provider, id, reason: "provider-not-found" };
2515
+ }
2516
+ const before = providerBlock.models.length;
2517
+ const filtered = providerBlock.models.filter((m) => m.id !== id);
2518
+ if (filtered.length === before) {
2519
+ return { removed: false, provider, id, reason: "model-not-found" };
2520
+ }
2521
+ const nextProviderBlock = { ...providerBlock, models: filtered };
2522
+ if (filtered.length === 0) {
2523
+ delete nextProviderBlock.models;
2524
+ }
2525
+ const nextConfig = {
2526
+ providers: { ...config.providers, [provider]: nextProviderBlock }
2527
+ };
2528
+ await harness.writeModelsConfig(nextConfig, scope);
2529
+ return { removed: true, provider, id, scope: scope.kind };
2530
+ })
2531
+ );
2532
+ 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(
2533
+ async (spec, opts) => runLafsCommand("pi.models.enable", "standard", async () => {
2534
+ const harness = requirePiHarness();
2535
+ const scope = resolveModelsScope(opts);
2536
+ const { provider, id } = parseModelSpec(spec);
2537
+ if (!id.includes("*")) {
2538
+ const config = await harness.readModelsConfig(scope);
2539
+ const providerBlock = config.providers[provider];
2540
+ const defined = providerBlock?.models?.some((m) => m.id === id) ?? false;
2541
+ if (!defined) {
2542
+ }
2543
+ }
2544
+ const current = await harness.readSettings(scope);
2545
+ const currentObj = typeof current === "object" && current !== null && !Array.isArray(current) ? current : {};
2546
+ const enabledRaw = currentObj["enabledModels"];
2547
+ const enabled = Array.isArray(enabledRaw) ? enabledRaw.filter((v) => typeof v === "string") : [];
2548
+ const already = enabled.includes(spec);
2549
+ if (already) {
2550
+ return { enabled: false, reason: "already-enabled", spec, scope: scope.kind };
2551
+ }
2552
+ enabled.push(spec);
2553
+ await harness.writeSettings({ enabledModels: enabled }, scope);
2554
+ return { enabled: true, spec, provider, id, scope: scope.kind };
2555
+ })
2556
+ );
2557
+ 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(
2558
+ async (spec, opts) => runLafsCommand("pi.models.disable", "standard", async () => {
2559
+ const harness = requirePiHarness();
2560
+ const scope = resolveModelsScope(opts);
2561
+ const current = await harness.readSettings(scope);
2562
+ const currentObj = typeof current === "object" && current !== null && !Array.isArray(current) ? current : {};
2563
+ const enabledRaw = currentObj["enabledModels"];
2564
+ const enabled = Array.isArray(enabledRaw) ? enabledRaw.filter((v) => typeof v === "string") : [];
2565
+ const filtered = enabled.filter((e) => e !== spec);
2566
+ if (filtered.length === enabled.length) {
2567
+ return { disabled: false, reason: "not-enabled", spec, scope: scope.kind };
2568
+ }
2569
+ await harness.writeSettings({ enabledModels: filtered }, scope);
2570
+ return { disabled: true, spec, scope: scope.kind };
2571
+ })
2572
+ );
2573
+ 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(
2574
+ async (spec, opts) => runLafsCommand("pi.models.default", "standard", async () => {
2575
+ const harness = requirePiHarness();
2576
+ const scope = resolveModelsScope(opts);
2577
+ const { provider, id } = parseModelSpec(spec);
2578
+ const config = await harness.readModelsConfig(scope);
2579
+ const providerBlock = config.providers[provider];
2580
+ const defined = providerBlock?.models?.some((m) => m.id === id) ?? false;
2581
+ await harness.writeSettings({ defaultProvider: provider, defaultModel: id }, scope);
2582
+ return {
2583
+ set: true,
2584
+ provider,
2585
+ id,
2586
+ knownInModelsJson: defined,
2587
+ scope: scope.kind
2588
+ };
2589
+ })
2590
+ );
2591
+ }
2592
+
2593
+ // src/commands/pi/prompts.ts
2594
+ import { existsSync as existsSync6 } from "fs";
2595
+ import { join as join7, resolve } from "path";
2596
+ function inferPromptName(sourceDir) {
2597
+ const normalized = resolve(sourceDir).replace(/[\\/]+$/, "");
2598
+ const base = normalized.split(/[\\/]/).pop();
2599
+ if (base === void 0 || base.length === 0) {
2600
+ throw new LAFSCommandError(
2601
+ PI_ERROR_CODES.VALIDATION,
2602
+ `Could not infer a prompt name from source: ${sourceDir}`,
2603
+ "Pass --name <name> to override the inferred name.",
2604
+ false
2605
+ );
2606
+ }
2607
+ return base;
2608
+ }
2609
+ function registerPiPromptsCommands(parent) {
2610
+ const prompts = parent.command("prompts").description("Manage Pi prompts across tiers");
2611
+ 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(
2612
+ async (source, opts) => runLafsCommand("pi.prompts.install", "standard", async () => {
2613
+ const harness = requirePiHarness();
2614
+ const tier = parseScope2(opts.scope, "project");
2615
+ const projectDir = resolveProjectDir2(tier, opts.projectDir);
2616
+ const absSource = resolve(source);
2617
+ if (!existsSync6(absSource)) {
2618
+ throw new LAFSCommandError(
2619
+ PI_ERROR_CODES.NOT_FOUND,
2620
+ `Source directory does not exist: ${absSource}`,
2621
+ "Check the path and try again.",
2622
+ false
2623
+ );
2624
+ }
2625
+ if (!existsSync6(join7(absSource, "prompt.md"))) {
2626
+ throw new LAFSCommandError(
2627
+ PI_ERROR_CODES.VALIDATION,
2628
+ `Source directory is missing prompt.md: ${absSource}`,
2629
+ "Add a prompt.md to the source directory and retry.",
2630
+ false
2631
+ );
2632
+ }
2633
+ const name = opts.name ?? inferPromptName(absSource);
2634
+ const installOpts = { force: opts.force ?? false };
2635
+ const result = await harness.installPrompt(absSource, name, tier, projectDir, installOpts);
2636
+ return {
2637
+ installed: {
2638
+ name,
2639
+ tier: result.tier,
2640
+ targetPath: result.targetPath,
2641
+ source: absSource
2642
+ }
2643
+ };
2644
+ })
2645
+ );
2646
+ 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(
2647
+ async (opts) => runLafsCommand("pi.prompts.list", "standard", async () => {
2648
+ const harness = requirePiHarness();
2649
+ const projectDir = opts.projectDir ?? process.cwd();
2650
+ const entries = await harness.listPrompts(projectDir);
2651
+ return {
2652
+ count: entries.length,
2653
+ prompts: entries
2654
+ };
2655
+ })
2656
+ );
2657
+ 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(
2658
+ async (name, opts) => runLafsCommand("pi.prompts.remove", "standard", async () => {
2659
+ const harness = requirePiHarness();
2660
+ const tier = parseScope2(opts.scope, "project");
2661
+ const projectDir = resolveProjectDir2(tier, opts.projectDir);
2662
+ const removed = await harness.removePrompt(name, tier, projectDir);
2663
+ return { name, tier, removed };
2664
+ })
2665
+ );
2666
+ }
2667
+
2668
+ // src/commands/pi/sessions.ts
2669
+ import { spawn } from "child_process";
2670
+ import { createReadStream, createWriteStream, existsSync as existsSync7 } from "fs";
2671
+ import { createInterface } from "readline/promises";
2672
+ async function streamSession(filePath, outputPath, transform) {
2673
+ const writeToFile = outputPath !== void 0 && outputPath.length > 0;
2674
+ const out = writeToFile ? createWriteStream(outputPath) : process.stdout;
2675
+ const reader = createInterface({
2676
+ input: createReadStream(filePath, { encoding: "utf8" }),
2677
+ crlfDelay: Infinity
2678
+ });
2679
+ let emitted = 0;
2680
+ try {
2681
+ for await (const line of reader) {
2682
+ const result = transform(line);
2683
+ if (result === null) continue;
2684
+ out.write(`${result}
2685
+ `);
2686
+ emitted += 1;
2687
+ }
2688
+ } finally {
2689
+ reader.close();
2690
+ if (writeToFile && "end" in out) {
2691
+ await new Promise((resolve3) => {
2692
+ out.end(resolve3);
2693
+ });
2694
+ }
2695
+ }
2696
+ return emitted;
2697
+ }
2698
+ function sessionEntryToMarkdown(line) {
2699
+ let parsed;
2700
+ try {
2701
+ parsed = JSON.parse(line);
2702
+ } catch {
2703
+ return null;
2704
+ }
2705
+ if (typeof parsed !== "object" || parsed === null) return null;
2706
+ const obj = parsed;
2707
+ const type = typeof obj["type"] === "string" ? obj["type"] : null;
2708
+ if (type === "session") {
2709
+ const id = typeof obj["id"] === "string" ? obj["id"] : "(no id)";
2710
+ const ts = typeof obj["timestamp"] === "string" ? obj["timestamp"] : "";
2711
+ return `# Session ${id}${ts.length > 0 ? ` \xB7 ${ts}` : ""}
2712
+ `;
2713
+ }
2714
+ if (type === "message") {
2715
+ const role = typeof obj["role"] === "string" ? obj["role"] : "assistant";
2716
+ const content = extractMessageContent(obj["content"]);
2717
+ if (content === null) return null;
2718
+ const label = role.charAt(0).toUpperCase() + role.slice(1);
2719
+ return `## ${label}
2720
+
2721
+ ${content}
2722
+ `;
2723
+ }
2724
+ if (type === "custom_message") {
2725
+ const label = typeof obj["label"] === "string" ? obj["label"] : "Custom";
2726
+ const text = typeof obj["text"] === "string" ? obj["text"] : "";
2727
+ return `### ${label}
2728
+
2729
+ ${text}
2730
+ `;
2731
+ }
2732
+ return null;
2733
+ }
2734
+ function extractMessageContent(content) {
2735
+ if (typeof content === "string") return content;
2736
+ if (!Array.isArray(content)) return null;
2737
+ const parts = [];
2738
+ for (const block of content) {
2739
+ if (typeof block === "string") {
2740
+ parts.push(block);
2741
+ continue;
2742
+ }
2743
+ if (typeof block !== "object" || block === null) continue;
2744
+ const b = block;
2745
+ if (b["type"] === "text" && typeof b["text"] === "string") {
2746
+ parts.push(b["text"]);
2747
+ }
2748
+ }
2749
+ if (parts.length === 0) return null;
2750
+ return parts.join("\n\n");
2751
+ }
2752
+ function registerPiSessionsCommands(parent) {
2753
+ const sessions = parent.command("sessions").description("Inspect and resume Pi sessions");
2754
+ sessions.command("list").description("List Pi sessions (reads only line 1 of each JSONL file)").option("--no-subagents", "Skip sessions under subagents/").action(
2755
+ async (opts) => runLafsCommand("pi.sessions.list", "standard", async () => {
2756
+ const harness = requirePiHarness();
2757
+ const summaries = await harness.listSessions({
2758
+ includeSubagents: opts.includeSubagents !== false
2759
+ });
2760
+ return {
2761
+ count: summaries.length,
2762
+ sessions: summaries
2763
+ };
2764
+ })
2765
+ );
2766
+ sessions.command("show <id>").description("Show the full body of a Pi session by id").action(
2767
+ async (id) => runLafsCommand("pi.sessions.show", "full", async () => {
2768
+ const harness = requirePiHarness();
2769
+ const doc = await harness.showSession(id);
2770
+ return {
2771
+ summary: doc.summary,
2772
+ entryCount: doc.entries.length,
2773
+ entries: doc.entries
2774
+ };
2775
+ })
2776
+ );
2777
+ 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(
2778
+ async (id, opts) => runLafsCommand("pi.sessions.export", "standard", async () => {
2779
+ if (opts.jsonl === true && opts.md === true) {
2780
+ throw new LAFSCommandError(
2781
+ PI_ERROR_CODES.VALIDATION,
2782
+ "Cannot pass both --jsonl and --md",
2783
+ "Pick one of --jsonl or --md.",
2784
+ false
2785
+ );
2786
+ }
2787
+ const harness = requirePiHarness();
2788
+ const summaries = await harness.listSessions({ includeSubagents: true });
2789
+ const match = summaries.find((s) => s.id === id);
2790
+ if (match === void 0) {
2791
+ throw new LAFSCommandError(
2792
+ PI_ERROR_CODES.NOT_FOUND,
2793
+ `No session found with id ${id}`,
2794
+ "Run `caamp pi sessions list` to see known ids.",
2795
+ false
2796
+ );
2797
+ }
2798
+ const format = opts.md === true ? "md" : "jsonl";
2799
+ const emitted = format === "md" ? await streamSession(match.filePath, opts.output, sessionEntryToMarkdown) : await streamSession(
2800
+ match.filePath,
2801
+ opts.output,
2802
+ (line) => line.length === 0 ? null : line
2803
+ );
2804
+ return {
2805
+ id,
2806
+ format,
2807
+ filePath: match.filePath,
2808
+ output: opts.output ?? "stdout",
2809
+ entriesEmitted: emitted
2810
+ };
2811
+ })
2812
+ );
2813
+ sessions.command("resume <id>").description("Resume a Pi session by shelling out to `pi --session <id>`").action(
2814
+ async (id) => runLafsCommand("pi.sessions.resume", "standard", async () => {
2815
+ const harness = requirePiHarness();
2816
+ const summaries = await harness.listSessions({ includeSubagents: true });
2817
+ const match = summaries.find((s) => s.id === id);
2818
+ if (match === void 0) {
2819
+ throw new LAFSCommandError(
2820
+ PI_ERROR_CODES.NOT_FOUND,
2821
+ `No session found with id ${id}`,
2822
+ "Run `caamp pi sessions list` to see known ids.",
2823
+ false
2824
+ );
2825
+ }
2826
+ const piBinary = harness.provider.detection.binary ?? "pi";
2827
+ if (!existsSync7(piBinary) && piBinary === "pi") {
2828
+ }
2829
+ const child = spawn(piBinary, ["--session", id], {
2830
+ stdio: "inherit",
2831
+ detached: false
2832
+ });
2833
+ const exitCode = await new Promise((resolve3) => {
2834
+ child.on("exit", (code) => resolve3(code ?? 0));
2835
+ });
2836
+ if (exitCode !== 0) {
2837
+ throw new LAFSCommandError(
2838
+ PI_ERROR_CODES.TRANSIENT,
2839
+ `pi --session ${id} exited with code ${exitCode}`,
2840
+ "Check the Pi binary output for details.",
2841
+ true
2842
+ );
2843
+ }
2844
+ return {
2845
+ id,
2846
+ filePath: match.filePath,
2847
+ exitCode
2848
+ };
2849
+ })
2850
+ );
2851
+ }
2852
+
2853
+ // src/commands/pi/themes.ts
2854
+ import { existsSync as existsSync8, statSync } from "fs";
2855
+ import { extname, resolve as resolve2 } from "path";
2856
+ function inferThemeName(sourceFile) {
2857
+ const base = resolve2(sourceFile).split(/[\\/]/).pop();
2858
+ if (base === void 0 || base.length === 0) {
2859
+ throw new LAFSCommandError(
2860
+ PI_ERROR_CODES.VALIDATION,
2861
+ `Could not infer a theme name from source: ${sourceFile}`,
2862
+ "Pass --name <name> to override the inferred name.",
2863
+ false
2864
+ );
2865
+ }
2866
+ const ext = extname(base);
2867
+ if (ext === "") return base;
2868
+ return base.slice(0, -ext.length);
2869
+ }
2870
+ function registerPiThemesCommands(parent) {
2871
+ const themes = parent.command("themes").description("Manage Pi themes across tiers");
2872
+ 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(
2873
+ async (source, opts) => runLafsCommand("pi.themes.install", "standard", async () => {
2874
+ const harness = requirePiHarness();
2875
+ const tier = parseScope2(opts.scope, "project");
2876
+ const projectDir = resolveProjectDir2(tier, opts.projectDir);
2877
+ const absSource = resolve2(source);
2878
+ if (!existsSync8(absSource)) {
2879
+ throw new LAFSCommandError(
2880
+ PI_ERROR_CODES.NOT_FOUND,
2881
+ `Source theme does not exist: ${absSource}`,
2882
+ "Check the path and try again.",
2883
+ false
2884
+ );
2885
+ }
2886
+ const stats = statSync(absSource);
2887
+ if (!stats.isFile()) {
2888
+ throw new LAFSCommandError(
2889
+ PI_ERROR_CODES.VALIDATION,
2890
+ `Source theme is not a regular file: ${absSource}`,
2891
+ "Themes must be a single .ts/.tsx/.mts/.json file.",
2892
+ false
2893
+ );
2894
+ }
2895
+ const name = opts.name ?? inferThemeName(absSource);
2896
+ const installOpts = { force: opts.force ?? false };
2897
+ const result = await harness.installTheme(absSource, name, tier, projectDir, installOpts);
2898
+ return {
2899
+ installed: {
2900
+ name,
2901
+ tier: result.tier,
2902
+ targetPath: result.targetPath,
2903
+ source: absSource
2904
+ }
2905
+ };
2906
+ })
2907
+ );
2908
+ 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(
2909
+ async (opts) => runLafsCommand("pi.themes.list", "standard", async () => {
2910
+ const harness = requirePiHarness();
2911
+ const projectDir = opts.projectDir ?? process.cwd();
2912
+ const entries = await harness.listThemes(projectDir);
2913
+ return {
2914
+ count: entries.length,
2915
+ themes: entries
2916
+ };
2917
+ })
2918
+ );
2919
+ 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(
2920
+ async (name, opts) => runLafsCommand("pi.themes.remove", "standard", async () => {
2921
+ const harness = requirePiHarness();
2922
+ const tier = parseScope2(opts.scope, "project");
2923
+ const projectDir = resolveProjectDir2(tier, opts.projectDir);
2924
+ const removed = await harness.removeTheme(name, tier, projectDir);
2925
+ return { name, tier, removed };
2926
+ })
2927
+ );
2928
+ }
2929
+
2930
+ // src/commands/pi/index.ts
2931
+ function registerPiCommands(program2) {
2932
+ const pi = program2.command("pi").description("Pi harness operations (extensions, sessions, models, prompts, themes, cant)");
2933
+ registerPiExtensionsCommands(pi);
2934
+ registerPiSessionsCommands(pi);
2935
+ registerPiModelsCommands(pi);
2936
+ registerPiPromptsCommands(pi);
2937
+ registerPiThemesCommands(pi);
2938
+ registerPiCantCommands(pi);
2939
+ }
2940
+
1540
2941
  // src/commands/providers.ts
1541
2942
  import { randomUUID as randomUUID3 } from "crypto";
1542
2943
  import { resolveOutputFormat as resolveOutputFormat2 } from "@cleocode/lafs";
@@ -2083,9 +3484,9 @@ CAMP Hook Support (mappings v${getHookMappingsVersion()})
2083
3484
  console.log(` ${"\u2500".repeat(22)} ${"\u2500".repeat(20)} ${"\u2500".repeat(8)} ${"\u2500".repeat(20)}`);
2084
3485
  for (const row of matrix) {
2085
3486
  const hooks2 = row.hooksCount > 0 ? String(row.hooksCount) : "-";
2086
- const spawn = row.spawnMechanism ?? "-";
3487
+ const spawn2 = row.spawnMechanism ?? "-";
2087
3488
  console.log(
2088
- ` ${row.toolName.padEnd(22)} ${row.skillsPrecedence.padEnd(20)} ${hooks2.padEnd(8)} ${spawn}`
3489
+ ` ${row.toolName.padEnd(22)} ${row.skillsPrecedence.padEnd(20)} ${hooks2.padEnd(8)} ${spawn2}`
2089
3490
  );
2090
3491
  }
2091
3492
  console.log(pc6.dim(`
@@ -2126,13 +3527,13 @@ function emitJsonError2(operation, mvi, code, message, category, details = {}) {
2126
3527
  }
2127
3528
 
2128
3529
  // src/commands/skills/audit.ts
2129
- import { existsSync as existsSync3, statSync } from "fs";
3530
+ import { existsSync as existsSync9, statSync as statSync2 } from "fs";
2130
3531
  import pc7 from "picocolors";
2131
3532
  function registerSkillsAudit(parent) {
2132
3533
  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
3534
  const operation = "skills.audit";
2134
3535
  const mvi = "standard";
2135
- if (!existsSync3(path)) {
3536
+ if (!existsSync9(path)) {
2136
3537
  const message = `Path not found: ${path}`;
2137
3538
  if (opts.sarif) {
2138
3539
  console.error(
@@ -2194,7 +3595,7 @@ function registerSkillsAudit(parent) {
2194
3595
  );
2195
3596
  process.exit(1);
2196
3597
  }
2197
- const stat = statSync(path);
3598
+ const stat = statSync2(path);
2198
3599
  let results;
2199
3600
  try {
2200
3601
  if (stat.isFile()) {
@@ -2704,9 +4105,9 @@ function emitJsonError3(operation, mvi, code, message, category, details = {}) {
2704
4105
  }
2705
4106
 
2706
4107
  // src/commands/skills/init.ts
2707
- import { existsSync as existsSync4 } from "fs";
2708
- import { mkdir, writeFile } from "fs/promises";
2709
- import { join as join3 } from "path";
4108
+ import { existsSync as existsSync10 } from "fs";
4109
+ import { mkdir, writeFile as writeFile3 } from "fs/promises";
4110
+ import { join as join8 } from "path";
2710
4111
  import pc10 from "picocolors";
2711
4112
  function registerSkillsInit(parent) {
2712
4113
  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 +4133,8 @@ function registerSkillsInit(parent) {
2732
4133
  process.exit(1);
2733
4134
  }
2734
4135
  const skillName = name ?? "my-skill";
2735
- const skillDir = join3(opts.dir, skillName);
2736
- if (existsSync4(skillDir)) {
4136
+ const skillDir = join8(opts.dir, skillName);
4137
+ if (existsSync10(skillDir)) {
2737
4138
  const message = `Directory already exists: ${skillDir}`;
2738
4139
  if (format === "json") {
2739
4140
  emitJsonError(
@@ -2775,7 +4176,7 @@ Provide detailed instructions for the AI agent here.
2775
4176
 
2776
4177
  Show example inputs and expected outputs.
2777
4178
  `;
2778
- await writeFile(join3(skillDir, "SKILL.md"), template, "utf-8");
4179
+ await writeFile3(join8(skillDir, "SKILL.md"), template, "utf-8");
2779
4180
  const result = {
2780
4181
  name: skillName,
2781
4182
  directory: skillDir,
@@ -2789,69 +4190,15 @@ Show example inputs and expected outputs.
2789
4190
  console.log(pc10.green(`\u2713 Created skill template: ${skillDir}/SKILL.md`));
2790
4191
  console.log(pc10.dim("\nNext steps:"));
2791
4192
  console.log(pc10.dim(" 1. Edit SKILL.md with your instructions"));
2792
- console.log(pc10.dim(` 2. Validate: caamp skills validate ${join3(skillDir, "SKILL.md")}`));
4193
+ console.log(pc10.dim(` 2. Validate: caamp skills validate ${join8(skillDir, "SKILL.md")}`));
2793
4194
  console.log(pc10.dim(` 3. Install: caamp skills install ${skillDir}`));
2794
4195
  }
2795
4196
  );
2796
4197
  }
2797
4198
 
2798
4199
  // src/commands/skills/install.ts
2799
- import { existsSync as existsSync5 } from "fs";
4200
+ import { existsSync as existsSync11 } from "fs";
2800
4201
  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
4202
  function registerSkillsInstall(parent) {
2856
4203
  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
4204
  "-a, --agent <name>",
@@ -3335,7 +4682,7 @@ async function handleMarketplaceSource(source, _providers, _isGlobal, format, op
3335
4682
  for (const subPath of subPathCandidates) {
3336
4683
  try {
3337
4684
  const result = await cloneRepo(parsed.owner, parsed.repo, parsed.ref, subPath);
3338
- if (subPath && !existsSync5(result.localPath)) {
4685
+ if (subPath && !existsSync11(result.localPath)) {
3339
4686
  await result.cleanup();
3340
4687
  continue;
3341
4688
  }
@@ -3650,8 +4997,8 @@ ${outdated.length} skill(s) have updates available:
3650
4997
  if (!opts.yes && format === "human") {
3651
4998
  const readline = await import("readline");
3652
4999
  const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
3653
- const answer = await new Promise((resolve) => {
3654
- rl.question(pc14.dim("\nProceed with update? [y/N] "), resolve);
5000
+ const answer = await new Promise((resolve3) => {
5001
+ rl.question(pc14.dim("\nProceed with update? [y/N] "), resolve3);
3655
5002
  });
3656
5003
  rl.close();
3657
5004
  if (answer.toLowerCase() !== "y" && answer.toLowerCase() !== "yes") {
@@ -3884,6 +5231,8 @@ registerInstructionsCommands(program);
3884
5231
  registerConfigCommand(program);
3885
5232
  registerDoctorCommand(program);
3886
5233
  registerAdvancedCommands(program);
5234
+ registerMcpCommands(program);
5235
+ registerPiCommands(program);
3887
5236
  function toError(error) {
3888
5237
  if (error instanceof Error) return error;
3889
5238
  return new Error(String(error));