@cleocode/caamp 2026.4.6 → 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/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-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 spawn = row.spawnMechanism ?? "-";
3237
+ const spawn2 = row.spawnMechanism ?? "-";
2087
3238
  console.log(
2088
- ` ${row.toolName.padEnd(22)} ${row.skillsPrecedence.padEnd(20)} ${hooks2.padEnd(8)} ${spawn}`
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 existsSync3, statSync } from "fs";
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 (!existsSync3(path)) {
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 = statSync(path);
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 existsSync4 } from "fs";
2708
- import { mkdir, writeFile } from "fs/promises";
2709
- import { join as join3 } from "path";
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 = join3(opts.dir, skillName);
2736
- if (existsSync4(skillDir)) {
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 writeFile(join3(skillDir, "SKILL.md"), template, "utf-8");
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 ${join3(skillDir, "SKILL.md")}`));
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 existsSync5 } from "fs";
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 && !existsSync5(result.localPath)) {
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((resolve) => {
3654
- rl.question(pc14.dim("\nProceed with update? [y/N] "), resolve);
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));