@openape/apes 0.14.3 → 0.15.1

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
@@ -61,7 +61,7 @@ import {
61
61
  } from "./chunk-6GPSKAMU.js";
62
62
 
63
63
  // src/cli.ts
64
- import consola28 from "consola";
64
+ import consola32 from "consola";
65
65
 
66
66
  // src/ape-shell.ts
67
67
  import path from "path";
@@ -91,7 +91,7 @@ function rewriteApeShellArgs(argv, argv0) {
91
91
  }
92
92
 
93
93
  // src/cli.ts
94
- import { defineCommand as defineCommand34, runMain } from "citty";
94
+ import { defineCommand as defineCommand39, runMain } from "citty";
95
95
 
96
96
  // src/commands/auth/login.ts
97
97
  import { Buffer } from "buffer";
@@ -329,8 +329,8 @@ async function loginWithPKCE(idp) {
329
329
  consola2.success(`Logged in as ${payload.email || payload.sub}`);
330
330
  }
331
331
  async function loginWithKey(idp, keyPath, agentEmail) {
332
- const { readFileSync: readFileSync4 } = await import("fs");
333
- const { sign: sign2 } = await import("crypto");
332
+ const { readFileSync: readFileSync7 } = await import("fs");
333
+ const { sign: sign3 } = await import("crypto");
334
334
  const { loadEd25519PrivateKey: loadEd25519PrivateKey2 } = await import("./ssh-key-YBNNG5K5.js");
335
335
  const challengeUrl = await getAgentChallengeEndpoint(idp);
336
336
  const challengeResp = await fetch(challengeUrl, {
@@ -342,9 +342,9 @@ async function loginWithKey(idp, keyPath, agentEmail) {
342
342
  throw new CliError(`Challenge failed: ${await challengeResp.text()}`);
343
343
  }
344
344
  const { challenge } = await challengeResp.json();
345
- const keyContent = readFileSync4(keyPath, "utf-8");
345
+ const keyContent = readFileSync7(keyPath, "utf-8");
346
346
  const privateKey = loadEd25519PrivateKey2(keyContent);
347
- const signature = sign2(null, Buffer.from(challenge), privateKey).toString("base64");
347
+ const signature = sign3(null, Buffer.from(challenge), privateKey).toString("base64");
348
348
  const authenticateUrl = await getAgentAuthenticateEndpoint(idp);
349
349
  const authResp = await fetch(authenticateUrl, {
350
350
  method: "POST",
@@ -1692,16 +1692,698 @@ var adminCommand = defineCommand19({
1692
1692
  }
1693
1693
  });
1694
1694
 
1695
- // src/commands/adapter/index.ts
1695
+ // src/commands/agents/index.ts
1696
+ import { defineCommand as defineCommand24 } from "citty";
1697
+
1698
+ // src/commands/agents/destroy.ts
1699
+ import { execFileSync as execFileSync3 } from "child_process";
1700
+ import { mkdtempSync, rmSync, writeFileSync } from "fs";
1701
+ import { tmpdir } from "os";
1702
+ import { join as join2 } from "path";
1696
1703
  import { defineCommand as defineCommand20 } from "citty";
1697
1704
  import consola18 from "consola";
1698
- var adapterCommand = defineCommand20({
1705
+
1706
+ // src/lib/agent-bootstrap.ts
1707
+ import { Buffer as Buffer2 } from "buffer";
1708
+ import { createPrivateKey, sign } from "crypto";
1709
+ var AGENT_NAME_REGEX = /^[a-z][a-z0-9-]{0,23}$/;
1710
+ var SSH_ED25519_PREFIX = "ssh-ed25519 ";
1711
+ var SSH_ED25519_REGEX = /^ssh-ed25519 [A-Za-z0-9+/=]+(\s.*)?$/;
1712
+ async function registerAgentAtIdp(input) {
1713
+ return await apiFetch("/api/enroll", {
1714
+ method: "POST",
1715
+ body: { name: input.name, publicKey: input.publicKey },
1716
+ idp: input.idp
1717
+ });
1718
+ }
1719
+ async function issueAgentToken(input) {
1720
+ const privateKey = createPrivateKey(input.privateKeyPem);
1721
+ const challengeUrl = await getAgentChallengeEndpoint(input.idp);
1722
+ const challengeResp = await fetch(challengeUrl, {
1723
+ method: "POST",
1724
+ headers: { "Content-Type": "application/json" },
1725
+ body: JSON.stringify({ agent_id: input.agentEmail })
1726
+ });
1727
+ if (!challengeResp.ok) {
1728
+ const text = await challengeResp.text().catch(() => "");
1729
+ throw new Error(`Challenge failed (${challengeResp.status}): ${text}`);
1730
+ }
1731
+ const { challenge } = await challengeResp.json();
1732
+ const signature = sign(null, Buffer2.from(challenge), privateKey).toString("base64");
1733
+ const authenticateUrl = await getAgentAuthenticateEndpoint(input.idp);
1734
+ const authResp = await fetch(authenticateUrl, {
1735
+ method: "POST",
1736
+ headers: { "Content-Type": "application/json" },
1737
+ body: JSON.stringify({ agent_id: input.agentEmail, challenge, signature })
1738
+ });
1739
+ if (!authResp.ok) {
1740
+ const text = await authResp.text().catch(() => "");
1741
+ throw new Error(`Authenticate failed (${authResp.status}): ${text}`);
1742
+ }
1743
+ const result = await authResp.json();
1744
+ return { token: result.token, expiresIn: result.expires_in || 3600 };
1745
+ }
1746
+ var SH_HEREDOC_DELIMITER = "APES_HEREDOC_END";
1747
+ function shHeredoc(content) {
1748
+ if (content.includes(SH_HEREDOC_DELIMITER)) {
1749
+ throw new Error(`Refusing to emit heredoc: content contains ${SH_HEREDOC_DELIMITER}`);
1750
+ }
1751
+ return `<< '${SH_HEREDOC_DELIMITER}'
1752
+ ${content}
1753
+ ${SH_HEREDOC_DELIMITER}`;
1754
+ }
1755
+ function buildSpawnSetupScript(input) {
1756
+ const { name, homeDir, shellPath } = input;
1757
+ const privatePemForHeredoc = input.privateKeyPem.endsWith("\n") ? input.privateKeyPem : `${input.privateKeyPem}
1758
+ `;
1759
+ const claudeBlock = input.claudeSettingsJson && input.hookScriptSource ? `
1760
+ mkdir -p "$HOME_DIR/.claude/hooks"
1761
+ cat > "$HOME_DIR/.claude/settings.json" ${shHeredoc(input.claudeSettingsJson)}
1762
+ cat > "$HOME_DIR/.claude/hooks/bash-via-ape-shell.sh" ${shHeredoc(input.hookScriptSource)}
1763
+ chmod 755 "$HOME_DIR/.claude/hooks/bash-via-ape-shell.sh"
1764
+ ` : "";
1765
+ return `#!/bin/bash
1766
+ set -euo pipefail
1767
+
1768
+ NAME=${shQuote(name)}
1769
+ HOME_DIR=${shQuote(homeDir)}
1770
+ SHELL_PATH=${shQuote(shellPath)}
1771
+
1772
+ if dscl . -read "/Users/$NAME" >/dev/null 2>&1; then
1773
+ echo "User $NAME already exists; refusing to overwrite." >&2
1774
+ exit 1
1775
+ fi
1776
+
1777
+ # Pick the next free UID in the [200, 500) hidden service-account range.
1778
+ # Starts the running max at 199 so an empty range yields 200 after the
1779
+ # floor check; otherwise NEXT_UID = max(existing in-range UIDs) + 1.
1780
+ NEXT_UID=199
1781
+ for uid in $(dscl . -list /Users UniqueID | awk '$2 >= 200 && $2 < 500 {print $2}'); do
1782
+ if [ "$uid" -ge "$NEXT_UID" ]; then
1783
+ NEXT_UID=$((uid + 1))
1784
+ fi
1785
+ done
1786
+ if [ "$NEXT_UID" -lt 200 ]; then
1787
+ NEXT_UID=200
1788
+ fi
1789
+ if [ "$NEXT_UID" -ge 500 ]; then
1790
+ echo "No free UID in [200, 500) \u2014 refusing to clobber a real user." >&2
1791
+ exit 1
1792
+ fi
1793
+
1794
+ dscl . -create "/Users/$NAME"
1795
+ dscl . -create "/Users/$NAME" UserShell "$SHELL_PATH"
1796
+ dscl . -create "/Users/$NAME" RealName "OpenApe Agent $NAME"
1797
+ dscl . -create "/Users/$NAME" UniqueID "$NEXT_UID"
1798
+ dscl . -create "/Users/$NAME" PrimaryGroupID 20
1799
+ dscl . -create "/Users/$NAME" NFSHomeDirectory "$HOME_DIR"
1800
+ dscl . -create "/Users/$NAME" IsHidden 1
1801
+
1802
+ mkdir -p "$HOME_DIR/.ssh" "$HOME_DIR/.config/apes"
1803
+
1804
+ cat > "$HOME_DIR/.ssh/id_ed25519" ${shHeredoc(privatePemForHeredoc.trimEnd())}
1805
+ cat > "$HOME_DIR/.ssh/id_ed25519.pub" ${shHeredoc(`${input.publicKeySshLine}`)}
1806
+ cat > "$HOME_DIR/.config/apes/auth.json" ${shHeredoc(input.authJson)}
1807
+ ${claudeBlock}
1808
+ chown -R "$NAME:staff" "$HOME_DIR"
1809
+ chmod 700 "$HOME_DIR/.ssh"
1810
+ chmod 700 "$HOME_DIR/.config"
1811
+ chmod 600 "$HOME_DIR/.ssh/id_ed25519"
1812
+ chmod 644 "$HOME_DIR/.ssh/id_ed25519.pub"
1813
+ chmod 600 "$HOME_DIR/.config/apes/auth.json"
1814
+
1815
+ echo "OK $NAME uid=$NEXT_UID home=$HOME_DIR"
1816
+ `;
1817
+ }
1818
+ function buildDestroyTeardownScript(input) {
1819
+ const { name, homeDir } = input;
1820
+ return `#!/bin/bash
1821
+ # Best-effort teardown. set -u catches typos; we deliberately do NOT use -e
1822
+ # because pkill / launchctl are allowed to fail when the user has no live
1823
+ # sessions, and dscl -delete is allowed to fail when the user is already gone.
1824
+ set -u
1825
+
1826
+ NAME=${shQuote(name)}
1827
+ HOME_DIR=${shQuote(homeDir)}
1828
+
1829
+ UID_OF=$(dscl . -read "/Users/$NAME" UniqueID 2>/dev/null | awk '/UniqueID:/ {print $2}')
1830
+
1831
+ if [ -n "$UID_OF" ]; then
1832
+ launchctl bootout "user/$UID_OF" 2>/dev/null || true
1833
+ pkill -9 -u "$UID_OF" 2>/dev/null || true
1834
+ fi
1835
+
1836
+ if [ -d "$HOME_DIR" ] && [ "$HOME_DIR" != "/" ] && [ "$HOME_DIR" != "" ]; then
1837
+ rm -rf "$HOME_DIR"
1838
+ fi
1839
+
1840
+ dscl . -delete "/Users/$NAME" 2>/dev/null || true
1841
+
1842
+ echo "OK destroyed $NAME"
1843
+ `;
1844
+ }
1845
+ function shQuote(s) {
1846
+ return `'${s.replace(/'/g, `'\\''`)}'`;
1847
+ }
1848
+ function buildAgentAuthJson(input) {
1849
+ return `${JSON.stringify({
1850
+ idp: input.idp,
1851
+ access_token: input.accessToken,
1852
+ email: input.email,
1853
+ expires_at: input.expiresAt
1854
+ }, null, 2)}
1855
+ `;
1856
+ }
1857
+ var CLAUDE_SETTINGS_JSON = `${JSON.stringify({
1858
+ hooks: {
1859
+ PreToolUse: [
1860
+ {
1861
+ matcher: "Bash",
1862
+ hooks: [
1863
+ { type: "command", command: "$HOME/.claude/hooks/bash-via-ape-shell.sh" }
1864
+ ]
1865
+ }
1866
+ ]
1867
+ }
1868
+ }, null, 2)}
1869
+ `;
1870
+ var BASH_VIA_APE_SHELL_HOOK_SOURCE = `#!/bin/bash
1871
+ # PreToolUse hook for the Bash tool: rewrite the tool input so the
1872
+ # original command runs via \`ape-shell -c <cmd>\`. That re-routes every
1873
+ # Bash invocation through the apes grant flow, so the agent cannot
1874
+ # execute shell commands without an approved grant.
1875
+ exec python3 -c '
1876
+ import json, shlex, sys
1877
+ data = json.load(sys.stdin)
1878
+ cmd = data["tool_input"]["command"]
1879
+ wrapped = "ape-shell -c " + shlex.quote(cmd)
1880
+ out = {"hookSpecificOutput": {"hookEventName": "PreToolUse", "updatedToolInput": {"command": wrapped}}}
1881
+ print(json.dumps(out))
1882
+ '
1883
+ `;
1884
+
1885
+ // src/lib/macos-user.ts
1886
+ import { execFileSync as execFileSync2 } from "child_process";
1887
+ import { existsSync as existsSync3, readFileSync as readFileSync2 } from "fs";
1888
+ function isDarwin() {
1889
+ return process.platform === "darwin";
1890
+ }
1891
+ function readMacOSUser(name) {
1892
+ let output;
1893
+ try {
1894
+ output = execFileSync2("dscl", [".", "-read", `/Users/${name}`], {
1895
+ encoding: "utf-8",
1896
+ stdio: ["ignore", "pipe", "pipe"]
1897
+ });
1898
+ } catch {
1899
+ return null;
1900
+ }
1901
+ const uidMatch = output.match(/UniqueID:\s*(\d+)/);
1902
+ const shellMatch = output.match(/UserShell:\s*(\S.*)$/m);
1903
+ return {
1904
+ name,
1905
+ uid: uidMatch ? Number.parseInt(uidMatch[1], 10) : null,
1906
+ shell: shellMatch ? shellMatch[1].trim() : null
1907
+ };
1908
+ }
1909
+ function listMacOSUserNames() {
1910
+ let output;
1911
+ try {
1912
+ output = execFileSync2("dscl", [".", "-list", "/Users"], {
1913
+ encoding: "utf-8",
1914
+ stdio: ["ignore", "pipe", "pipe"]
1915
+ });
1916
+ } catch {
1917
+ return /* @__PURE__ */ new Set();
1918
+ }
1919
+ return new Set(
1920
+ output.split("\n").map((line) => line.trim()).filter((line) => line.length > 0)
1921
+ );
1922
+ }
1923
+ function whichBinary(name) {
1924
+ try {
1925
+ const out = execFileSync2("which", [name], {
1926
+ encoding: "utf-8",
1927
+ stdio: ["ignore", "pipe", "ignore"]
1928
+ }).trim();
1929
+ return out || null;
1930
+ } catch {
1931
+ return null;
1932
+ }
1933
+ }
1934
+ function isShellRegistered(shellPath) {
1935
+ if (!existsSync3("/etc/shells")) return false;
1936
+ const content = readFileSync2("/etc/shells", "utf-8");
1937
+ return content.split("\n").map((l) => l.trim()).filter((l) => l && !l.startsWith("#")).includes(shellPath);
1938
+ }
1939
+
1940
+ // src/commands/agents/destroy.ts
1941
+ var destroyAgentCommand = defineCommand20({
1942
+ meta: {
1943
+ name: "destroy",
1944
+ description: "Tear down an agent: remove macOS user, hard-delete IdP agent, drop all SSH keys"
1945
+ },
1946
+ args: {
1947
+ name: {
1948
+ type: "positional",
1949
+ description: "Agent name to destroy",
1950
+ required: true
1951
+ },
1952
+ force: {
1953
+ type: "boolean",
1954
+ description: "Skip the interactive confirmation. Required for CI."
1955
+ },
1956
+ soft: {
1957
+ type: "boolean",
1958
+ description: "Soft deactivate at the IdP (PATCH isActive=false) instead of hard-delete"
1959
+ },
1960
+ "keep-os-user": {
1961
+ type: "boolean",
1962
+ description: "Skip OS-side teardown. Useful for CI where the agent has no OS user."
1963
+ }
1964
+ },
1965
+ async run({ args }) {
1966
+ const name = args.name;
1967
+ if (!AGENT_NAME_REGEX.test(name)) {
1968
+ throw new CliError(
1969
+ `Invalid agent name "${name}". Must match /^[a-z][a-z0-9-]{0,23}$/.`
1970
+ );
1971
+ }
1972
+ const auth = loadAuth();
1973
+ if (!auth) {
1974
+ throw new CliError("Not authenticated. Run `apes login` first.");
1975
+ }
1976
+ const idp = getIdpUrl();
1977
+ if (!idp) {
1978
+ throw new CliError("No IdP URL configured. Run `apes login` first.");
1979
+ }
1980
+ const owned = await apiFetch("/api/my-agents", { idp });
1981
+ const idpAgent = owned.find((u) => u.name === name);
1982
+ const idpExists = idpAgent !== void 0;
1983
+ const osUserExists = !args["keep-os-user"] && isDarwin() && readMacOSUser(name) !== null;
1984
+ if (!idpExists && !osUserExists) {
1985
+ consola18.info(`Nothing to destroy: no IdP agent and no OS user named "${name}".`);
1986
+ return;
1987
+ }
1988
+ if (!args.force) {
1989
+ const consequences = [];
1990
+ if (osUserExists) consequences.push(`\u2022 Remove macOS user ${name} and rm -rf /Users/${name}`);
1991
+ if (idpExists) {
1992
+ consequences.push(args.soft ? `\u2022 Deactivate IdP agent ${idpAgent.email} (PATCH isActive=false)` : `\u2022 Hard-delete IdP agent ${idpAgent.email} and all its SSH keys`);
1993
+ }
1994
+ consola18.warn(`About to destroy "${name}":
1995
+ ${consequences.join("\n")}`);
1996
+ const confirmed = await consola18.prompt("Proceed?", { type: "confirm", initial: false });
1997
+ if (typeof confirmed === "symbol" || !confirmed) {
1998
+ throw new CliExit(0);
1999
+ }
2000
+ }
2001
+ if (osUserExists) {
2002
+ const apes = whichBinary("apes");
2003
+ if (!apes) {
2004
+ throw new CliError("`apes` not found on PATH. Install @openape/apes globally first.");
2005
+ }
2006
+ const escapes = whichBinary("escapes");
2007
+ if (!escapes) {
2008
+ throw new CliError("`escapes` not found on PATH; OS teardown requires escapes.");
2009
+ }
2010
+ const scratch = mkdtempSync(join2(tmpdir(), `apes-destroy-${name}-`));
2011
+ const scriptPath = join2(scratch, "teardown.sh");
2012
+ try {
2013
+ const script = buildDestroyTeardownScript({ name, homeDir: `/Users/${name}` });
2014
+ writeFileSync(scriptPath, script, { mode: 448 });
2015
+ consola18.start("Running teardown as root via `apes run --as root --wait`\u2026");
2016
+ consola18.info("You will be asked to approve the as=root grant in your DDISA inbox; this command blocks until you do.");
2017
+ execFileSync3(apes, ["run", "--as", "root", "--wait", "--", "bash", scriptPath], { stdio: "inherit" });
2018
+ } finally {
2019
+ rmSync(scratch, { recursive: true, force: true });
2020
+ }
2021
+ } else if (!args["keep-os-user"] && isDarwin()) {
2022
+ consola18.info("No macOS user to remove (skipped).");
2023
+ }
2024
+ if (idpExists) {
2025
+ const id = encodeURIComponent(idpAgent.email);
2026
+ if (args.soft) {
2027
+ await apiFetch(`/api/my-agents/${id}`, { method: "PATCH", body: { isActive: false }, idp });
2028
+ consola18.success(`Deactivated IdP agent ${idpAgent.email}`);
2029
+ } else {
2030
+ await apiFetch(`/api/my-agents/${id}`, { method: "DELETE", idp });
2031
+ consola18.success(`Deleted IdP agent ${idpAgent.email}`);
2032
+ }
2033
+ } else {
2034
+ consola18.info("No IdP agent to remove (skipped).");
2035
+ }
2036
+ consola18.success(`Destroyed ${name}.`);
2037
+ }
2038
+ });
2039
+
2040
+ // src/commands/agents/list.ts
2041
+ import { defineCommand as defineCommand21 } from "citty";
2042
+ import consola19 from "consola";
2043
+ var listAgentsCommand = defineCommand21({
2044
+ meta: {
2045
+ name: "list",
2046
+ description: "List agents owned by the current user, with local OS-user status"
2047
+ },
2048
+ args: {
2049
+ json: {
2050
+ type: "boolean",
2051
+ description: "Output as JSON"
2052
+ },
2053
+ "include-inactive": {
2054
+ type: "boolean",
2055
+ description: "Include agents whose IdP record is deactivated (default hides them)"
2056
+ }
2057
+ },
2058
+ async run({ args }) {
2059
+ const auth = loadAuth();
2060
+ if (!auth) {
2061
+ throw new CliError("Not authenticated. Run `apes login` first.");
2062
+ }
2063
+ const idp = getIdpUrl();
2064
+ if (!idp) {
2065
+ throw new CliError("No IdP URL configured. Run `apes login` first.");
2066
+ }
2067
+ const all = await apiFetch("/api/my-agents", { idp });
2068
+ const filtered = args["include-inactive"] ? all : all.filter((u) => u.isActive !== false);
2069
+ const osUsers = isDarwin() ? listMacOSUserNames() : /* @__PURE__ */ new Set();
2070
+ const home = (name) => `/Users/${name}`;
2071
+ const rows = filtered.map((u) => ({
2072
+ name: u.name,
2073
+ email: u.email,
2074
+ isActive: u.isActive !== false,
2075
+ osUser: osUsers.has(u.name),
2076
+ home: osUsers.has(u.name) ? home(u.name) : null
2077
+ }));
2078
+ if (args.json) {
2079
+ process.stdout.write(`${JSON.stringify(rows, null, 2)}
2080
+ `);
2081
+ return;
2082
+ }
2083
+ if (rows.length === 0) {
2084
+ consola19.info(args["include-inactive"] ? "No agents found." : "No active agents found. Use --include-inactive to show deactivated.");
2085
+ return;
2086
+ }
2087
+ const nameW = Math.max(4, ...rows.map((r) => r.name.length));
2088
+ const emailW = Math.max(5, ...rows.map((r) => r.email.length));
2089
+ const header = `${"NAME".padEnd(nameW)} ${"EMAIL".padEnd(emailW)} ACTIVE OS-USER HOME`;
2090
+ console.log(header);
2091
+ console.log("-".repeat(header.length));
2092
+ for (const r of rows) {
2093
+ const active = r.isActive ? "\u2713" : "\u2717";
2094
+ const os = r.osUser ? "\u2713" : "\u2717";
2095
+ const homeCol = r.home ?? (isDarwin() ? "(missing)" : "(non-darwin)");
2096
+ console.log(`${r.name.padEnd(nameW)} ${r.email.padEnd(emailW)} ${active.padEnd(6)} ${os.padEnd(7)} ${homeCol}`);
2097
+ }
2098
+ }
2099
+ });
2100
+
2101
+ // src/commands/agents/register.ts
2102
+ import { existsSync as existsSync4, readFileSync as readFileSync3 } from "fs";
2103
+ import { defineCommand as defineCommand22 } from "citty";
2104
+ import consola20 from "consola";
2105
+ var registerAgentCommand = defineCommand22({
2106
+ meta: {
2107
+ name: "register",
2108
+ description: "Register an agent at the IdP using a supplied public key"
2109
+ },
2110
+ args: {
2111
+ name: {
2112
+ type: "string",
2113
+ description: "Agent name (lowercase, [a-z0-9-], 1\u201324 chars, must start with a letter)",
2114
+ required: true
2115
+ },
2116
+ "public-key": {
2117
+ type: "string",
2118
+ description: 'Full ssh-ed25519 public key line (e.g. "ssh-ed25519 AAAAC3...")'
2119
+ },
2120
+ "public-key-file": {
2121
+ type: "string",
2122
+ description: "Path to a .pub file containing the ssh-ed25519 line"
2123
+ },
2124
+ json: {
2125
+ type: "boolean",
2126
+ description: "Emit machine-readable JSON instead of a human summary"
2127
+ }
2128
+ },
2129
+ async run({ args }) {
2130
+ const auth = loadAuth();
2131
+ if (!auth) {
2132
+ throw new CliError("Not authenticated. Run `apes login` first.");
2133
+ }
2134
+ const idp = getIdpUrl();
2135
+ if (!idp) {
2136
+ throw new CliError("No IdP URL configured. Run `apes login` first.");
2137
+ }
2138
+ const name = args.name;
2139
+ if (!AGENT_NAME_REGEX.test(name)) {
2140
+ throw new CliError(
2141
+ `Invalid agent name "${name}". Must match /^[a-z][a-z0-9-]{0,23}$/ \u2014 lowercase letters, digits and hyphens, 1\u201324 chars, must start with a letter.`
2142
+ );
2143
+ }
2144
+ let publicKey = args["public-key"];
2145
+ const keyFile = args["public-key-file"];
2146
+ if (publicKey && keyFile) {
2147
+ throw new CliError("Pass either --public-key or --public-key-file, not both.");
2148
+ }
2149
+ if (!publicKey && keyFile) {
2150
+ if (!existsSync4(keyFile)) {
2151
+ throw new CliError(`Public-key file not found: ${keyFile}`);
2152
+ }
2153
+ publicKey = readFileSync3(keyFile, "utf-8").trim();
2154
+ }
2155
+ if (!publicKey) {
2156
+ throw new CliError('Provide --public-key "<ssh-ed25519 line>" or --public-key-file <path>.');
2157
+ }
2158
+ if (!publicKey.startsWith(SSH_ED25519_PREFIX) || !SSH_ED25519_REGEX.test(publicKey)) {
2159
+ throw new CliError(
2160
+ 'Public key must be a full ssh-ed25519 line, e.g. "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAA... [optional comment]".'
2161
+ );
2162
+ }
2163
+ const result = await registerAgentAtIdp({ name, publicKey, idp });
2164
+ if (args.json) {
2165
+ process.stdout.write(`${JSON.stringify({
2166
+ email: result.email,
2167
+ name: result.name,
2168
+ owner: result.owner,
2169
+ approver: result.approver,
2170
+ idp
2171
+ })}
2172
+ `);
2173
+ return;
2174
+ }
2175
+ consola20.success("Agent registered.");
2176
+ console.log(` Name: ${result.name}`);
2177
+ console.log(` Email: ${result.email}`);
2178
+ console.log(` IdP: ${idp}`);
2179
+ console.log(` Owner: ${result.owner}`);
2180
+ console.log(` Approver: ${result.approver}`);
2181
+ console.log("");
2182
+ console.log("Tell the agent to log in with:");
2183
+ console.log(` apes login --idp ${idp} --email ${result.email} --key <path-to-matching-private-key>`);
2184
+ }
2185
+ });
2186
+
2187
+ // src/commands/agents/spawn.ts
2188
+ import { execFileSync as execFileSync4 } from "child_process";
2189
+ import { mkdtempSync as mkdtempSync2, rmSync as rmSync2, writeFileSync as writeFileSync3 } from "fs";
2190
+ import { tmpdir as tmpdir2 } from "os";
2191
+ import { join as join3 } from "path";
2192
+ import { defineCommand as defineCommand23 } from "citty";
2193
+ import consola21 from "consola";
2194
+
2195
+ // src/lib/keygen.ts
2196
+ import { Buffer as Buffer3 } from "buffer";
2197
+ import { existsSync as existsSync5, mkdirSync, readFileSync as readFileSync4, writeFileSync as writeFileSync2 } from "fs";
2198
+ import { generateKeyPairSync } from "crypto";
2199
+ import { homedir as homedir4 } from "os";
2200
+ import { dirname, resolve as resolve2 } from "path";
2201
+ function resolveKeyPath(p) {
2202
+ return resolve2(p.replace(/^~/, homedir4()));
2203
+ }
2204
+ function buildSshEd25519Line(rawPub) {
2205
+ const keyTypeStr = "ssh-ed25519";
2206
+ const keyTypeLen = Buffer3.alloc(4);
2207
+ keyTypeLen.writeUInt32BE(keyTypeStr.length);
2208
+ const pubKeyLen = Buffer3.alloc(4);
2209
+ pubKeyLen.writeUInt32BE(rawPub.length);
2210
+ const blob = Buffer3.concat([keyTypeLen, Buffer3.from(keyTypeStr), pubKeyLen, rawPub]);
2211
+ return `ssh-ed25519 ${blob.toString("base64")}`;
2212
+ }
2213
+ function readPublicKey(keyPath) {
2214
+ const pubPath = `${keyPath}.pub`;
2215
+ if (existsSync5(pubPath)) {
2216
+ return readFileSync4(pubPath, "utf-8").trim();
2217
+ }
2218
+ const keyContent = readFileSync4(keyPath, "utf-8");
2219
+ const privateKey = loadEd25519PrivateKey(keyContent);
2220
+ const jwk = privateKey.export({ format: "jwk" });
2221
+ const pubBytes = Buffer3.from(jwk.x, "base64url");
2222
+ return buildSshEd25519Line(pubBytes);
2223
+ }
2224
+ function generateAndSaveKey(keyPath) {
2225
+ const resolved = resolveKeyPath(keyPath);
2226
+ const dir = dirname(resolved);
2227
+ if (!existsSync5(dir)) {
2228
+ mkdirSync(dir, { recursive: true });
2229
+ }
2230
+ const { publicKey, privateKey } = generateKeyPairSync("ed25519");
2231
+ const privatePem = privateKey.export({ type: "pkcs8", format: "pem" });
2232
+ writeFileSync2(resolved, privatePem, { mode: 384 });
2233
+ const jwk = publicKey.export({ format: "jwk" });
2234
+ const pubBytes = Buffer3.from(jwk.x, "base64url");
2235
+ const pubKeyStr = buildSshEd25519Line(pubBytes);
2236
+ writeFileSync2(`${resolved}.pub`, `${pubKeyStr}
2237
+ `, { mode: 420 });
2238
+ return pubKeyStr;
2239
+ }
2240
+ function generateKeyPairInMemory() {
2241
+ const { publicKey, privateKey } = generateKeyPairSync("ed25519");
2242
+ const privatePem = privateKey.export({ type: "pkcs8", format: "pem" });
2243
+ const jwk = publicKey.export({ format: "jwk" });
2244
+ const pubBytes = Buffer3.from(jwk.x, "base64url");
2245
+ return {
2246
+ privatePem,
2247
+ publicSshLine: buildSshEd25519Line(pubBytes)
2248
+ };
2249
+ }
2250
+
2251
+ // src/commands/agents/spawn.ts
2252
+ var spawnAgentCommand = defineCommand23({
2253
+ meta: {
2254
+ name: "spawn",
2255
+ description: "Provision a local macOS agent end-to-end (OS user, keypair, IdP agent, ape-shell, Claude hook)"
2256
+ },
2257
+ args: {
2258
+ name: {
2259
+ type: "positional",
2260
+ description: "Agent name \u2014 also the macOS short username (lowercase, [a-z0-9-], must start with a letter)",
2261
+ required: true
2262
+ },
2263
+ shell: {
2264
+ type: "string",
2265
+ description: "Override login shell. Default: $(which ape-shell)"
2266
+ },
2267
+ "no-claude-hook": {
2268
+ type: "boolean",
2269
+ description: "Skip writing ~/.claude/settings.json + the Bash-rewrite hook"
2270
+ }
2271
+ },
2272
+ async run({ args }) {
2273
+ const name = args.name;
2274
+ if (!AGENT_NAME_REGEX.test(name)) {
2275
+ throw new CliError(
2276
+ `Invalid agent name "${name}". Must match /^[a-z][a-z0-9-]{0,23}$/ \u2014 lowercase letters, digits and hyphens, 1\u201324 chars, must start with a letter.`
2277
+ );
2278
+ }
2279
+ if (!isDarwin()) {
2280
+ throw new CliError(
2281
+ `\`apes agents spawn\` is currently macOS-only. Detected platform: ${process.platform}. Linux support is a follow-up; for now, use \`apes agents register\` plus a manually provisioned user.`
2282
+ );
2283
+ }
2284
+ const auth = loadAuth();
2285
+ if (!auth) {
2286
+ throw new CliError("Not authenticated. Run `apes login` first.");
2287
+ }
2288
+ const idp = getIdpUrl();
2289
+ if (!idp) {
2290
+ throw new CliError("No IdP URL configured. Run `apes login` first.");
2291
+ }
2292
+ const apeShell = args.shell ?? whichBinary("ape-shell");
2293
+ if (!apeShell) {
2294
+ throw new CliError("`ape-shell` not found on PATH. Install @openape/apes globally first.");
2295
+ }
2296
+ const apes = whichBinary("apes");
2297
+ if (!apes) {
2298
+ throw new CliError("`apes` not found on PATH. Install @openape/apes globally first.");
2299
+ }
2300
+ const escapes = whichBinary("escapes");
2301
+ if (!escapes) {
2302
+ throw new CliError(
2303
+ "`escapes` not found on PATH. spawn delegates the privileged setup phase to escapes; install it before running spawn."
2304
+ );
2305
+ }
2306
+ if (!isShellRegistered(apeShell)) {
2307
+ throw new CliError(
2308
+ `${apeShell} is not registered in /etc/shells. macOS refuses to set it as a login shell. Run:
2309
+ echo ${apeShell} | sudo tee -a /etc/shells
2310
+ and try again.`
2311
+ );
2312
+ }
2313
+ const existing = readMacOSUser(name);
2314
+ if (existing) {
2315
+ throw new CliError(`macOS user "${name}" already exists (uid=${existing.uid ?? "?"}). Refusing to overwrite.`);
2316
+ }
2317
+ const homeDir = `/Users/${name}`;
2318
+ const scratch = mkdtempSync2(join3(tmpdir2(), `apes-spawn-${name}-`));
2319
+ const scriptPath = join3(scratch, "setup.sh");
2320
+ try {
2321
+ consola21.start(`Generating keypair for ${name}\u2026`);
2322
+ const { privatePem, publicSshLine } = generateKeyPairInMemory();
2323
+ consola21.start(`Registering agent at ${idp}\u2026`);
2324
+ const registration = await registerAgentAtIdp({ name, publicKey: publicSshLine, idp });
2325
+ consola21.success(`Registered as ${registration.email}`);
2326
+ consola21.start("Issuing agent access token\u2026");
2327
+ const { token, expiresIn } = await issueAgentToken({
2328
+ idp,
2329
+ agentEmail: registration.email,
2330
+ privateKeyPem: privatePem
2331
+ });
2332
+ const authJson = buildAgentAuthJson({
2333
+ idp,
2334
+ accessToken: token,
2335
+ email: registration.email,
2336
+ expiresAt: Math.floor(Date.now() / 1e3) + expiresIn
2337
+ });
2338
+ const includeClaudeHook = !args["no-claude-hook"];
2339
+ const script = buildSpawnSetupScript({
2340
+ name,
2341
+ homeDir,
2342
+ shellPath: apeShell,
2343
+ privateKeyPem: privatePem,
2344
+ publicKeySshLine: publicSshLine,
2345
+ authJson,
2346
+ claudeSettingsJson: includeClaudeHook ? CLAUDE_SETTINGS_JSON : null,
2347
+ hookScriptSource: includeClaudeHook ? BASH_VIA_APE_SHELL_HOOK_SOURCE : null
2348
+ });
2349
+ writeFileSync3(scriptPath, script, { mode: 448 });
2350
+ consola21.start("Running privileged setup as root via `apes run --as root --wait`\u2026");
2351
+ consola21.info("You will be asked to approve the as=root grant in your DDISA inbox; this command blocks until you do.");
2352
+ execFileSync4(apes, ["run", "--as", "root", "--wait", "--", "bash", scriptPath], { stdio: "inherit" });
2353
+ consola21.success(`Agent ${name} spawned.`);
2354
+ console.log("");
2355
+ console.log("Run as the agent with:");
2356
+ console.log(` apes run --as ${name} -- claude --session-name ${name} --dangerously-skip-permissions`);
2357
+ } finally {
2358
+ rmSync2(scratch, { recursive: true, force: true });
2359
+ }
2360
+ }
2361
+ });
2362
+
2363
+ // src/commands/agents/index.ts
2364
+ var agentsCommand = defineCommand24({
2365
+ meta: {
2366
+ name: "agents",
2367
+ description: "Manage owned agents (register, spawn, list, destroy)"
2368
+ },
2369
+ subCommands: {
2370
+ register: registerAgentCommand,
2371
+ spawn: spawnAgentCommand,
2372
+ list: listAgentsCommand,
2373
+ destroy: destroyAgentCommand
2374
+ }
2375
+ });
2376
+
2377
+ // src/commands/adapter/index.ts
2378
+ import { defineCommand as defineCommand25 } from "citty";
2379
+ import consola22 from "consola";
2380
+ var adapterCommand = defineCommand25({
1699
2381
  meta: {
1700
2382
  name: "adapter",
1701
2383
  description: "Manage CLI adapters"
1702
2384
  },
1703
2385
  subCommands: {
1704
- list: defineCommand20({
2386
+ list: defineCommand25({
1705
2387
  meta: {
1706
2388
  name: "list",
1707
2389
  description: "List available adapters"
@@ -1732,7 +2414,7 @@ var adapterCommand = defineCommand20({
1732
2414
  `);
1733
2415
  return;
1734
2416
  }
1735
- consola18.info(`Registry: ${index2.adapters.length} adapters (${index2.generated_at})`);
2417
+ consola22.info(`Registry: ${index2.adapters.length} adapters (${index2.generated_at})`);
1736
2418
  for (const a of index2.adapters) {
1737
2419
  const installed = isInstalled(a.id, false) ? " [installed]" : "";
1738
2420
  console.log(` ${a.id.padEnd(12)} ${a.name.padEnd(24)} ${a.category}${installed}`);
@@ -1754,7 +2436,7 @@ var adapterCommand = defineCommand20({
1754
2436
  return;
1755
2437
  }
1756
2438
  if (local.length === 0) {
1757
- consola18.info("No adapters installed. Use `apes adapter list --remote` to see available adapters.");
2439
+ consola22.info("No adapters installed. Use `apes adapter list --remote` to see available adapters.");
1758
2440
  return;
1759
2441
  }
1760
2442
  for (const a of local) {
@@ -1762,7 +2444,7 @@ var adapterCommand = defineCommand20({
1762
2444
  }
1763
2445
  }
1764
2446
  }),
1765
- install: defineCommand20({
2447
+ install: defineCommand25({
1766
2448
  meta: {
1767
2449
  name: "install",
1768
2450
  description: "Install an adapter from the registry"
@@ -1791,24 +2473,24 @@ var adapterCommand = defineCommand20({
1791
2473
  for (const id of ids) {
1792
2474
  const entry = findAdapter(index, id);
1793
2475
  if (!entry) {
1794
- consola18.error(`Adapter "${id}" not found in registry. Use \`apes adapter search ${id}\` to search.`);
2476
+ consola22.error(`Adapter "${id}" not found in registry. Use \`apes adapter search ${id}\` to search.`);
1795
2477
  continue;
1796
2478
  }
1797
2479
  const conflicts = findConflictingAdapters(entry.executable, id);
1798
2480
  if (conflicts.length > 0) {
1799
2481
  for (const c of conflicts) {
1800
- consola18.warn(`Conflicting adapter found: ${c.path} (id: ${c.adapterId}, executable: ${c.executable})`);
1801
- consola18.warn(` Remove it with: apes adapter remove ${c.adapterId}`);
2482
+ consola22.warn(`Conflicting adapter found: ${c.path} (id: ${c.adapterId}, executable: ${c.executable})`);
2483
+ consola22.warn(` Remove it with: apes adapter remove ${c.adapterId}`);
1802
2484
  }
1803
2485
  }
1804
2486
  const result = await installAdapter(entry, { local });
1805
2487
  const verb = result.updated ? "Updated" : "Installed";
1806
- consola18.success(`${verb} ${result.id} \u2192 ${result.path}`);
1807
- consola18.info(`Digest: ${result.digest}`);
2488
+ consola22.success(`${verb} ${result.id} \u2192 ${result.path}`);
2489
+ consola22.info(`Digest: ${result.digest}`);
1808
2490
  }
1809
2491
  }
1810
2492
  }),
1811
- remove: defineCommand20({
2493
+ remove: defineCommand25({
1812
2494
  meta: {
1813
2495
  name: "remove",
1814
2496
  description: "Remove an installed adapter"
@@ -1831,9 +2513,9 @@ var adapterCommand = defineCommand20({
1831
2513
  let failed = false;
1832
2514
  for (const id of ids) {
1833
2515
  if (removeAdapter(id, local)) {
1834
- consola18.success(`Removed adapter: ${id}`);
2516
+ consola22.success(`Removed adapter: ${id}`);
1835
2517
  } else {
1836
- consola18.error(`Adapter "${id}" is not installed${local ? " locally" : ""}`);
2518
+ consola22.error(`Adapter "${id}" is not installed${local ? " locally" : ""}`);
1837
2519
  failed = true;
1838
2520
  }
1839
2521
  }
@@ -1841,7 +2523,7 @@ var adapterCommand = defineCommand20({
1841
2523
  throw new CliError("Some adapters could not be removed");
1842
2524
  }
1843
2525
  }),
1844
- info: defineCommand20({
2526
+ info: defineCommand25({
1845
2527
  meta: {
1846
2528
  name: "info",
1847
2529
  description: "Show detailed adapter information"
@@ -1883,7 +2565,7 @@ var adapterCommand = defineCommand20({
1883
2565
  }
1884
2566
  }
1885
2567
  }),
1886
- search: defineCommand20({
2568
+ search: defineCommand25({
1887
2569
  meta: {
1888
2570
  name: "search",
1889
2571
  description: "Search adapters in the registry"
@@ -1915,7 +2597,7 @@ var adapterCommand = defineCommand20({
1915
2597
  return;
1916
2598
  }
1917
2599
  if (results.length === 0) {
1918
- consola18.info(`No adapters matching "${query}"`);
2600
+ consola22.info(`No adapters matching "${query}"`);
1919
2601
  return;
1920
2602
  }
1921
2603
  for (const a of results) {
@@ -1924,7 +2606,7 @@ var adapterCommand = defineCommand20({
1924
2606
  }
1925
2607
  }
1926
2608
  }),
1927
- update: defineCommand20({
2609
+ update: defineCommand25({
1928
2610
  meta: {
1929
2611
  name: "update",
1930
2612
  description: "Update installed adapters"
@@ -1950,33 +2632,33 @@ var adapterCommand = defineCommand20({
1950
2632
  const targetId = args.id ? String(args.id) : void 0;
1951
2633
  const targets = targetId ? [targetId] : index.adapters.map((a) => a.id).filter((id) => isInstalled(id, false));
1952
2634
  if (targets.length === 0) {
1953
- consola18.info("No adapters installed to update.");
2635
+ consola22.info("No adapters installed to update.");
1954
2636
  return;
1955
2637
  }
1956
2638
  for (const id of targets) {
1957
2639
  const entry = findAdapter(index, id);
1958
2640
  if (!entry) {
1959
- consola18.warn(`${id}: not found in registry, skipping`);
2641
+ consola22.warn(`${id}: not found in registry, skipping`);
1960
2642
  continue;
1961
2643
  }
1962
2644
  const localDigest = getInstalledDigest(id, false);
1963
2645
  if (localDigest === entry.digest) {
1964
- consola18.info(`${id}: already up to date`);
2646
+ consola22.info(`${id}: already up to date`);
1965
2647
  continue;
1966
2648
  }
1967
2649
  if (localDigest && !args.yes) {
1968
- consola18.warn(`${id}: digest will change \u2014 existing grants for this adapter will be invalidated`);
1969
- consola18.info(` Old: ${localDigest}`);
1970
- consola18.info(` New: ${entry.digest}`);
1971
- consola18.info(" Use --yes to confirm");
2650
+ consola22.warn(`${id}: digest will change \u2014 existing grants for this adapter will be invalidated`);
2651
+ consola22.info(` Old: ${localDigest}`);
2652
+ consola22.info(` New: ${entry.digest}`);
2653
+ consola22.info(" Use --yes to confirm");
1972
2654
  continue;
1973
2655
  }
1974
2656
  const result = await installAdapter(entry);
1975
- consola18.success(`Updated ${result.id} \u2192 ${result.path}`);
2657
+ consola22.success(`Updated ${result.id} \u2192 ${result.path}`);
1976
2658
  }
1977
2659
  }
1978
2660
  }),
1979
- verify: defineCommand20({
2661
+ verify: defineCommand25({
1980
2662
  meta: {
1981
2663
  name: "verify",
1982
2664
  description: "Verify installed adapter against registry digest"
@@ -2009,7 +2691,7 @@ var adapterCommand = defineCommand20({
2009
2691
  if (!localDigest)
2010
2692
  throw new Error(`Adapter "${id}" is not installed${local ? " locally" : ""}`);
2011
2693
  if (localDigest === entry.digest) {
2012
- consola18.success(`${id}: digest matches registry`);
2694
+ consola22.success(`${id}: digest matches registry`);
2013
2695
  } else {
2014
2696
  console.log(` Local: ${localDigest}`);
2015
2697
  console.log(` Registry: ${entry.digest}`);
@@ -2021,11 +2703,11 @@ var adapterCommand = defineCommand20({
2021
2703
  });
2022
2704
 
2023
2705
  // src/commands/run.ts
2024
- import { execFileSync as execFileSync2 } from "child_process";
2706
+ import { execFileSync as execFileSync5 } from "child_process";
2025
2707
  import { hostname as hostname3 } from "os";
2026
2708
  import { basename } from "path";
2027
- import { defineCommand as defineCommand21 } from "citty";
2028
- import consola19 from "consola";
2709
+ import { defineCommand as defineCommand26 } from "citty";
2710
+ import consola23 from "consola";
2029
2711
  function shouldWaitForGrant(args) {
2030
2712
  return args.wait === true || process.env.APE_WAIT === "1";
2031
2713
  }
@@ -2062,7 +2744,7 @@ function printPendingGrantInfo(grant, idp) {
2062
2744
  const statusCmd = `apes grants status ${grant.id}`;
2063
2745
  const executeCmd = `apes grants run ${grant.id}`;
2064
2746
  if (mode === "human") {
2065
- consola19.success(`Grant ${grant.id} created \u2014 awaiting your approval`);
2747
+ consola23.success(`Grant ${grant.id} created \u2014 awaiting your approval`);
2066
2748
  console.log(` Approve in browser: ${approveUrl}`);
2067
2749
  console.log(` Check status: ${statusCmd}`);
2068
2750
  console.log(` Run after approval: ${executeCmd}`);
@@ -2072,7 +2754,7 @@ function printPendingGrantInfo(grant, idp) {
2072
2754
  return;
2073
2755
  }
2074
2756
  const maxMin = getPollMaxMinutes();
2075
- consola19.success(`Grant ${grant.id} created (pending approval)`);
2757
+ consola23.success(`Grant ${grant.id} created (pending approval)`);
2076
2758
  console.log(` Approve: ${approveUrl}`);
2077
2759
  console.log(` Status: ${statusCmd} [--json]`);
2078
2760
  console.log(` Execute: ${executeCmd} --wait`);
@@ -2094,7 +2776,7 @@ function printPendingGrantInfo(grant, idp) {
2094
2776
  console.log(' Tip: Approve as "timed" or "always" in the browser to let this');
2095
2777
  console.log(" grant be reused on subsequent invocations without re-approval.");
2096
2778
  }
2097
- var runCommand = defineCommand21({
2779
+ var runCommand = defineCommand26({
2098
2780
  meta: {
2099
2781
  name: "run",
2100
2782
  description: "Execute a grant-secured command"
@@ -2197,7 +2879,7 @@ async function runShellMode(command, args) {
2197
2879
  }
2198
2880
  } catch {
2199
2881
  }
2200
- consola19.info(`Requesting ape-shell session grant on ${targetHost}`);
2882
+ consola23.info(`Requesting ape-shell session grant on ${targetHost}`);
2201
2883
  const grant = await apiFetch(grantsUrl, {
2202
2884
  method: "POST",
2203
2885
  body: {
@@ -2217,8 +2899,8 @@ async function runShellMode(command, args) {
2217
2899
  host: targetHost
2218
2900
  });
2219
2901
  if (shouldWaitForGrant(args)) {
2220
- consola19.info(`Grant requested: ${grant.id}`);
2221
- consola19.info("Waiting for approval...");
2902
+ consola23.info(`Grant requested: ${grant.id}`);
2903
+ consola23.info("Waiting for approval...");
2222
2904
  const maxWait = 3e5;
2223
2905
  const interval = 3e3;
2224
2906
  const start = Date.now();
@@ -2249,13 +2931,13 @@ async function tryAdapterModeFromShell(command, idp, args) {
2249
2931
  try {
2250
2932
  resolved = await resolveCommand(loaded, [normalizedExecutable, ...parsed.argv]);
2251
2933
  } catch (err) {
2252
- consola19.debug(`ape-shell: adapter resolve failed for "${parsed.raw}":`, err);
2934
+ consola23.debug(`ape-shell: adapter resolve failed for "${parsed.raw}":`, err);
2253
2935
  return false;
2254
2936
  }
2255
2937
  try {
2256
2938
  const existingGrantId = await findExistingGrant(resolved, idp);
2257
2939
  if (existingGrantId) {
2258
- consola19.info(`Reusing grant ${existingGrantId} for: ${resolved.detail.display}`);
2940
+ consola23.info(`Reusing grant ${existingGrantId} for: ${resolved.detail.display}`);
2259
2941
  const token = await fetchGrantToken(idp, existingGrantId);
2260
2942
  await verifyAndExecute(token, resolved, existingGrantId);
2261
2943
  return true;
@@ -2263,7 +2945,7 @@ async function tryAdapterModeFromShell(command, idp, args) {
2263
2945
  } catch {
2264
2946
  }
2265
2947
  const approval = args.approval ?? "once";
2266
- consola19.info(`Requesting grant for: ${resolved.detail.display}`);
2948
+ consola23.info(`Requesting grant for: ${resolved.detail.display}`);
2267
2949
  const grant = await createShapesGrant(resolved, {
2268
2950
  idp,
2269
2951
  approval,
@@ -2271,8 +2953,8 @@ async function tryAdapterModeFromShell(command, idp, args) {
2271
2953
  });
2272
2954
  if (grant.similar_grants?.similar_grants?.length) {
2273
2955
  const n = grant.similar_grants.similar_grants.length;
2274
- consola19.info("");
2275
- consola19.info(` Similar grant(s) found (${n}). Your approver can extend an existing grant to cover this request.`);
2956
+ consola23.info("");
2957
+ consola23.info(` Similar grant(s) found (${n}). Your approver can extend an existing grant to cover this request.`);
2276
2958
  }
2277
2959
  notifyGrantPending({
2278
2960
  grantId: grant.id,
@@ -2282,8 +2964,8 @@ async function tryAdapterModeFromShell(command, idp, args) {
2282
2964
  host: args.host || hostname3()
2283
2965
  });
2284
2966
  if (shouldWaitForGrant(args)) {
2285
- consola19.info(`Grant requested: ${grant.id}`);
2286
- consola19.info(`Approve at: ${idp}/grant-approval?grant_id=${grant.id}`);
2967
+ consola23.info(`Grant requested: ${grant.id}`);
2968
+ consola23.info(`Approve at: ${idp}/grant-approval?grant_id=${grant.id}`);
2287
2969
  const status = await waitForGrantStatus(idp, grant.id);
2288
2970
  if (status !== "approved")
2289
2971
  throw new CliError(`Grant ${status}`);
@@ -2299,7 +2981,7 @@ function execShellCommand(command) {
2299
2981
  throw new CliError("No command to execute");
2300
2982
  try {
2301
2983
  const { APES_SHELL_WRAPPER: _wrapperMarker, ...inheritedEnv } = process.env;
2302
- execFileSync2(command[0], command.slice(1), {
2984
+ execFileSync5(command[0], command.slice(1), {
2303
2985
  stdio: "inherit",
2304
2986
  env: inheritedEnv
2305
2987
  });
@@ -2357,7 +3039,7 @@ async function runAdapterMode(command, rawArgs, args) {
2357
3039
  try {
2358
3040
  const existingGrantId = await findExistingGrant(resolved, idp);
2359
3041
  if (existingGrantId) {
2360
- consola19.info(`Reusing existing grant: ${existingGrantId}`);
3042
+ consola23.info(`Reusing existing grant: ${existingGrantId}`);
2361
3043
  const token = await fetchGrantToken(idp, existingGrantId);
2362
3044
  await verifyAndExecute(token, resolved, existingGrantId);
2363
3045
  return;
@@ -2371,17 +3053,17 @@ async function runAdapterMode(command, rawArgs, args) {
2371
3053
  });
2372
3054
  if (grant.similar_grants?.similar_grants?.length) {
2373
3055
  const n = grant.similar_grants.similar_grants.length;
2374
- consola19.info("");
2375
- consola19.info(` Similar grant(s) found (${n}). Your approver can extend an existing grant to cover this request.`);
3056
+ consola23.info("");
3057
+ consola23.info(` Similar grant(s) found (${n}). Your approver can extend an existing grant to cover this request.`);
2376
3058
  if (grant.similar_grants.widened_details?.length) {
2377
3059
  const wider = grant.similar_grants.widened_details.map((d) => d.permission).join(", ");
2378
- consola19.info(` Broader scope: ${wider}`);
3060
+ consola23.info(` Broader scope: ${wider}`);
2379
3061
  }
2380
- consola19.info("");
3062
+ consola23.info("");
2381
3063
  }
2382
3064
  if (shouldWaitForGrant(args)) {
2383
- consola19.info(`Grant requested: ${grant.id}`);
2384
- consola19.info(`Approve at: ${idp}/grant-approval?grant_id=${grant.id}`);
3065
+ consola23.info(`Grant requested: ${grant.id}`);
3066
+ consola23.info(`Approve at: ${idp}/grant-approval?grant_id=${grant.id}`);
2385
3067
  const status = await waitForGrantStatus(idp, grant.id);
2386
3068
  if (status !== "approved")
2387
3069
  throw new Error(`Grant ${status}`);
@@ -2401,7 +3083,7 @@ async function runAudienceMode(audience, action, args) {
2401
3083
  const grantsUrl = await getGrantsEndpoint(idp);
2402
3084
  const command = action.split(" ");
2403
3085
  const targetHost = args.host || hostname3();
2404
- consola19.info(`Requesting ${audience} grant on ${targetHost}: ${command.join(" ")}`);
3086
+ consola23.info(`Requesting ${audience} grant on ${targetHost}: ${command.join(" ")}`);
2405
3087
  const grant = await apiFetch(grantsUrl, {
2406
3088
  method: "POST",
2407
3089
  body: {
@@ -2418,15 +3100,15 @@ async function runAudienceMode(audience, action, args) {
2418
3100
  printPendingGrantInfo(grant, idp);
2419
3101
  throw new CliExit(getAsyncExitCode());
2420
3102
  }
2421
- consola19.success(`Grant requested: ${grant.id}`);
2422
- consola19.info("Waiting for approval...");
3103
+ consola23.success(`Grant requested: ${grant.id}`);
3104
+ consola23.info("Waiting for approval...");
2423
3105
  const maxWait = 3e5;
2424
3106
  const interval = 3e3;
2425
3107
  const start = Date.now();
2426
3108
  while (Date.now() - start < maxWait) {
2427
3109
  const status = await apiFetch(`${grantsUrl}/${grant.id}`);
2428
3110
  if (status.status === "approved") {
2429
- consola19.success("Grant approved!");
3111
+ consola23.success("Grant approved!");
2430
3112
  break;
2431
3113
  }
2432
3114
  if (status.status === "denied" || status.status === "revoked") {
@@ -2434,15 +3116,15 @@ async function runAudienceMode(audience, action, args) {
2434
3116
  }
2435
3117
  await new Promise((r) => setTimeout(r, interval));
2436
3118
  }
2437
- consola19.info("Fetching grant token...");
3119
+ consola23.info("Fetching grant token...");
2438
3120
  const { authz_jwt } = await apiFetch(`${grantsUrl}/${grant.id}/token`, {
2439
3121
  method: "POST"
2440
3122
  });
2441
3123
  if (audience === "escapes") {
2442
- consola19.info(`Executing: ${command.join(" ")}`);
3124
+ consola23.info(`Executing: ${command.join(" ")}`);
2443
3125
  try {
2444
3126
  const { APES_SHELL_WRAPPER: _wrapperMarker, ...inheritedEnv } = process.env;
2445
- execFileSync2(args["escapes-path"] || "escapes", ["--grant", authz_jwt, "--", ...command], {
3127
+ execFileSync5(args["escapes-path"] || "escapes", ["--grant", authz_jwt, "--", ...command], {
2446
3128
  stdio: "inherit",
2447
3129
  env: inheritedEnv
2448
3130
  });
@@ -2457,8 +3139,8 @@ async function runAudienceMode(audience, action, args) {
2457
3139
 
2458
3140
  // src/commands/proxy.ts
2459
3141
  import { spawn as spawn2 } from "child_process";
2460
- import { defineCommand as defineCommand22 } from "citty";
2461
- import consola20 from "consola";
3142
+ import { defineCommand as defineCommand27 } from "citty";
3143
+ import consola24 from "consola";
2462
3144
 
2463
3145
  // src/proxy/config.ts
2464
3146
  function buildDefaultProxyConfigToml(opts) {
@@ -2492,10 +3174,10 @@ note = "VPC-internal hostname suffix"
2492
3174
 
2493
3175
  // src/proxy/local-proxy.ts
2494
3176
  import { spawn } from "child_process";
2495
- import { mkdtempSync, rmSync, writeFileSync } from "fs";
3177
+ import { mkdtempSync as mkdtempSync3, rmSync as rmSync3, writeFileSync as writeFileSync4 } from "fs";
2496
3178
  import { createRequire } from "module";
2497
- import { tmpdir } from "os";
2498
- import { dirname, join as join2, resolve as resolve2 } from "path";
3179
+ import { tmpdir as tmpdir3 } from "os";
3180
+ import { dirname as dirname2, join as join4, resolve as resolve3 } from "path";
2499
3181
  var require2 = createRequire(import.meta.url);
2500
3182
  function findProxyBin() {
2501
3183
  const pkgPath = require2.resolve("@openape/proxy/package.json");
@@ -2504,12 +3186,12 @@ function findProxyBin() {
2504
3186
  if (!binRel) {
2505
3187
  throw new Error("@openape/proxy is missing the openape-proxy bin entry");
2506
3188
  }
2507
- return resolve2(dirname(pkgPath), binRel);
3189
+ return resolve3(dirname2(pkgPath), binRel);
2508
3190
  }
2509
3191
  async function startEphemeralProxy(configToml) {
2510
- const tmpDir = mkdtempSync(join2(tmpdir(), "openape-proxy-"));
2511
- const configPath = join2(tmpDir, "config.toml");
2512
- writeFileSync(configPath, configToml, { mode: 384 });
3192
+ const tmpDir = mkdtempSync3(join4(tmpdir3(), "openape-proxy-"));
3193
+ const configPath = join4(tmpDir, "config.toml");
3194
+ writeFileSync4(configPath, configToml, { mode: 384 });
2513
3195
  const binPath = findProxyBin();
2514
3196
  const child = spawn(process.execPath, [binPath, "-c", configPath], {
2515
3197
  stdio: ["ignore", "pipe", "pipe"],
@@ -2517,7 +3199,7 @@ async function startEphemeralProxy(configToml) {
2517
3199
  });
2518
3200
  const cleanupTmp = () => {
2519
3201
  try {
2520
- rmSync(tmpDir, { recursive: true, force: true });
3202
+ rmSync3(tmpDir, { recursive: true, force: true });
2521
3203
  } catch {
2522
3204
  }
2523
3205
  };
@@ -2601,10 +3283,10 @@ function resolveProxyConfigOptions() {
2601
3283
  77
2602
3284
  );
2603
3285
  }
2604
- consola20.info(`[apes proxy] IdP-mediated mode \u2014 agent=${auth.email}, idp=${auth.idp}`);
3286
+ consola24.info(`[apes proxy] IdP-mediated mode \u2014 agent=${auth.email}, idp=${auth.idp}`);
2605
3287
  return { agentEmail: auth.email, idpUrl: auth.idp, mediated: true };
2606
3288
  }
2607
- var proxyCommand = defineCommand22({
3289
+ var proxyCommand = defineCommand27({
2608
3290
  meta: {
2609
3291
  name: "proxy",
2610
3292
  description: "Run a command with HTTPS_PROXY routed through the OpenApe egress proxy."
@@ -2626,12 +3308,12 @@ var proxyCommand = defineCommand22({
2626
3308
  let close = null;
2627
3309
  if (reuseUrl) {
2628
3310
  proxyUrl = reuseUrl;
2629
- consola20.info(`[apes proxy] reusing existing proxy at ${proxyUrl}`);
3311
+ consola24.info(`[apes proxy] reusing existing proxy at ${proxyUrl}`);
2630
3312
  } else {
2631
3313
  const ephemeral = await startEphemeralProxy(buildDefaultProxyConfigToml(resolveProxyConfigOptions()));
2632
3314
  proxyUrl = ephemeral.url;
2633
3315
  close = ephemeral.close;
2634
- consola20.info(`[apes proxy] started ephemeral proxy at ${proxyUrl}`);
3316
+ consola24.info(`[apes proxy] started ephemeral proxy at ${proxyUrl}`);
2635
3317
  }
2636
3318
  const noProxy = process.env.NO_PROXY ?? process.env.no_proxy ?? "127.0.0.1,localhost";
2637
3319
  const childEnv = {
@@ -2663,7 +3345,7 @@ var proxyCommand = defineCommand22({
2663
3345
  else resolveExit(code ?? 0);
2664
3346
  });
2665
3347
  child.once("error", (err) => {
2666
- consola20.error(`[apes proxy] failed to spawn '${wrapped[0]}':`, err.message);
3348
+ consola24.error(`[apes proxy] failed to spawn '${wrapped[0]}':`, err.message);
2667
3349
  resolveExit(127);
2668
3350
  });
2669
3351
  });
@@ -2677,8 +3359,8 @@ function signalNumber(signal) {
2677
3359
  }
2678
3360
 
2679
3361
  // src/commands/explain.ts
2680
- import { defineCommand as defineCommand23 } from "citty";
2681
- var explainCommand = defineCommand23({
3362
+ import { defineCommand as defineCommand28 } from "citty";
3363
+ var explainCommand = defineCommand28({
2682
3364
  meta: {
2683
3365
  name: "explain",
2684
3366
  description: "Show what permission a command would need"
@@ -2716,9 +3398,9 @@ var explainCommand = defineCommand23({
2716
3398
  });
2717
3399
 
2718
3400
  // src/commands/config/get.ts
2719
- import { defineCommand as defineCommand24 } from "citty";
2720
- import consola21 from "consola";
2721
- var configGetCommand = defineCommand24({
3401
+ import { defineCommand as defineCommand29 } from "citty";
3402
+ import consola25 from "consola";
3403
+ var configGetCommand = defineCommand29({
2722
3404
  meta: {
2723
3405
  name: "get",
2724
3406
  description: "Get a configuration value"
@@ -2738,7 +3420,7 @@ var configGetCommand = defineCommand24({
2738
3420
  if (idp)
2739
3421
  console.log(idp);
2740
3422
  else
2741
- consola21.info("No IdP configured.");
3423
+ consola25.info("No IdP configured.");
2742
3424
  break;
2743
3425
  }
2744
3426
  case "email": {
@@ -2746,7 +3428,7 @@ var configGetCommand = defineCommand24({
2746
3428
  if (auth?.email)
2747
3429
  console.log(auth.email);
2748
3430
  else
2749
- consola21.info("Not logged in.");
3431
+ consola25.info("Not logged in.");
2750
3432
  break;
2751
3433
  }
2752
3434
  default: {
@@ -2759,7 +3441,7 @@ var configGetCommand = defineCommand24({
2759
3441
  if (sectionObj && field in sectionObj) {
2760
3442
  console.log(sectionObj[field]);
2761
3443
  } else {
2762
- consola21.info(`Key "${key}" not set.`);
3444
+ consola25.info(`Key "${key}" not set.`);
2763
3445
  }
2764
3446
  } else {
2765
3447
  throw new CliError(`Unknown key: "${key}". Use: idp, email, defaults.idp, defaults.approval, agent.key, agent.email`);
@@ -2770,9 +3452,9 @@ var configGetCommand = defineCommand24({
2770
3452
  });
2771
3453
 
2772
3454
  // src/commands/config/set.ts
2773
- import { defineCommand as defineCommand25 } from "citty";
2774
- import consola22 from "consola";
2775
- var configSetCommand = defineCommand25({
3455
+ import { defineCommand as defineCommand30 } from "citty";
3456
+ import consola26 from "consola";
3457
+ var configSetCommand = defineCommand30({
2776
3458
  meta: {
2777
3459
  name: "set",
2778
3460
  description: "Set a configuration value"
@@ -2808,12 +3490,12 @@ var configSetCommand = defineCommand25({
2808
3490
  throw new CliError(`Unknown section: "${section}". Use: defaults, agent`);
2809
3491
  }
2810
3492
  saveConfig(config);
2811
- consola22.success(`Set ${key} = ${value}`);
3493
+ consola26.success(`Set ${key} = ${value}`);
2812
3494
  }
2813
3495
  });
2814
3496
 
2815
3497
  // src/commands/fetch/index.ts
2816
- import { defineCommand as defineCommand26 } from "citty";
3498
+ import { defineCommand as defineCommand31 } from "citty";
2817
3499
  async function doRequest(method, url, body, contentType, raw, showHeaders) {
2818
3500
  const token = getAuthToken();
2819
3501
  if (!token) {
@@ -2849,13 +3531,13 @@ async function doRequest(method, url, body, contentType, raw, showHeaders) {
2849
3531
  throw new CliError(`HTTP ${response.status} ${response.statusText}`);
2850
3532
  }
2851
3533
  }
2852
- var fetchCommand = defineCommand26({
3534
+ var fetchCommand = defineCommand31({
2853
3535
  meta: {
2854
3536
  name: "fetch",
2855
3537
  description: "Make authenticated HTTP requests"
2856
3538
  },
2857
3539
  subCommands: {
2858
- get: defineCommand26({
3540
+ get: defineCommand31({
2859
3541
  meta: {
2860
3542
  name: "get",
2861
3543
  description: "GET request with auth token"
@@ -2881,7 +3563,7 @@ var fetchCommand = defineCommand26({
2881
3563
  await doRequest("GET", String(args.url), void 0, "application/json", Boolean(args.raw), Boolean(args.headers));
2882
3564
  }
2883
3565
  }),
2884
- post: defineCommand26({
3566
+ post: defineCommand31({
2885
3567
  meta: {
2886
3568
  name: "post",
2887
3569
  description: "POST request with auth token"
@@ -2920,8 +3602,8 @@ var fetchCommand = defineCommand26({
2920
3602
  });
2921
3603
 
2922
3604
  // src/commands/mcp/index.ts
2923
- import { defineCommand as defineCommand27 } from "citty";
2924
- var mcpCommand = defineCommand27({
3605
+ import { defineCommand as defineCommand32 } from "citty";
3606
+ var mcpCommand = defineCommand32({
2925
3607
  meta: {
2926
3608
  name: "mcp",
2927
3609
  description: "Start MCP server for AI agents"
@@ -2944,48 +3626,48 @@ var mcpCommand = defineCommand27({
2944
3626
  if (transport !== "stdio" && transport !== "sse") {
2945
3627
  throw new Error('Transport must be "stdio" or "sse"');
2946
3628
  }
2947
- const { startMcpServer } = await import("./server-EAT2FXDR.js");
3629
+ const { startMcpServer } = await import("./server-VZ325IPS.js");
2948
3630
  await startMcpServer(transport, port);
2949
3631
  }
2950
3632
  });
2951
3633
 
2952
3634
  // src/commands/init/index.ts
2953
- import { existsSync as existsSync3, copyFileSync, writeFileSync as writeFileSync2 } from "fs";
3635
+ import { existsSync as existsSync6, copyFileSync, writeFileSync as writeFileSync5 } from "fs";
2954
3636
  import { randomBytes } from "crypto";
2955
- import { execFileSync as execFileSync3 } from "child_process";
2956
- import { join as join3 } from "path";
2957
- import { defineCommand as defineCommand28 } from "citty";
2958
- import consola23 from "consola";
3637
+ import { execFileSync as execFileSync6 } from "child_process";
3638
+ import { join as join5 } from "path";
3639
+ import { defineCommand as defineCommand33 } from "citty";
3640
+ import consola27 from "consola";
2959
3641
  var DEFAULT_IDP_URL = "https://id.openape.at";
2960
3642
  async function downloadTemplate(repo, targetDir) {
2961
3643
  const { downloadTemplate: gigetDownload } = await import("giget");
2962
3644
  await gigetDownload(`gh:${repo}`, { dir: targetDir, force: false });
2963
3645
  }
2964
3646
  function installDeps(dir) {
2965
- const hasLockFile = (name) => existsSync3(join3(dir, name));
3647
+ const hasLockFile = (name) => existsSync6(join5(dir, name));
2966
3648
  if (hasLockFile("pnpm-lock.yaml")) {
2967
- execFileSync3("pnpm", ["install"], { cwd: dir, stdio: "inherit" });
3649
+ execFileSync6("pnpm", ["install"], { cwd: dir, stdio: "inherit" });
2968
3650
  } else if (hasLockFile("bun.lockb")) {
2969
- execFileSync3("bun", ["install"], { cwd: dir, stdio: "inherit" });
3651
+ execFileSync6("bun", ["install"], { cwd: dir, stdio: "inherit" });
2970
3652
  } else {
2971
- execFileSync3("npm", ["install"], { cwd: dir, stdio: "inherit" });
3653
+ execFileSync6("npm", ["install"], { cwd: dir, stdio: "inherit" });
2972
3654
  }
2973
3655
  }
2974
3656
  async function promptChoice(message, choices) {
2975
- const result = await consola23.prompt(message, { type: "select", options: choices });
3657
+ const result = await consola27.prompt(message, { type: "select", options: choices });
2976
3658
  if (typeof result === "symbol") {
2977
3659
  throw new CliExit(0);
2978
3660
  }
2979
3661
  return result;
2980
3662
  }
2981
3663
  async function promptText(message, defaultValue) {
2982
- const result = await consola23.prompt(message, { type: "text", default: defaultValue, placeholder: defaultValue });
3664
+ const result = await consola27.prompt(message, { type: "text", default: defaultValue, placeholder: defaultValue });
2983
3665
  if (typeof result === "symbol") {
2984
3666
  throw new CliExit(0);
2985
3667
  }
2986
3668
  return result || defaultValue || "";
2987
3669
  }
2988
- var initCommand = defineCommand28({
3670
+ var initCommand = defineCommand33({
2989
3671
  meta: {
2990
3672
  name: "init",
2991
3673
  description: "Scaffold a new OpenApe project"
@@ -3027,23 +3709,23 @@ var initCommand = defineCommand28({
3027
3709
  });
3028
3710
  async function initSP(targetDir) {
3029
3711
  const dir = targetDir || "my-app";
3030
- if (existsSync3(join3(dir, "package.json"))) {
3712
+ if (existsSync6(join5(dir, "package.json"))) {
3031
3713
  throw new CliError(`Directory "${dir}" already contains a project.`);
3032
3714
  }
3033
- consola23.start("Scaffolding SP starter...");
3715
+ consola27.start("Scaffolding SP starter...");
3034
3716
  await downloadTemplate("openape-ai/openape-sp-starter", dir);
3035
- consola23.success("Scaffolded from openape-sp-starter");
3036
- consola23.start("Installing dependencies...");
3717
+ consola27.success("Scaffolded from openape-sp-starter");
3718
+ consola27.start("Installing dependencies...");
3037
3719
  installDeps(dir);
3038
- consola23.success("Dependencies installed");
3039
- const envExample = join3(dir, ".env.example");
3040
- const envFile = join3(dir, ".env");
3041
- if (existsSync3(envExample) && !existsSync3(envFile)) {
3720
+ consola27.success("Dependencies installed");
3721
+ const envExample = join5(dir, ".env.example");
3722
+ const envFile = join5(dir, ".env");
3723
+ if (existsSync6(envExample) && !existsSync6(envFile)) {
3042
3724
  copyFileSync(envExample, envFile);
3043
- consola23.success(`\`.env\` created (using Free IdP at ${DEFAULT_IDP_URL})`);
3725
+ consola27.success(`\`.env\` created (using Free IdP at ${DEFAULT_IDP_URL})`);
3044
3726
  }
3045
3727
  console.log("");
3046
- consola23.box([
3728
+ consola27.box([
3047
3729
  `cd ${dir}`,
3048
3730
  "npm run dev",
3049
3731
  "",
@@ -3052,7 +3734,7 @@ async function initSP(targetDir) {
3052
3734
  }
3053
3735
  async function initIdP(targetDir) {
3054
3736
  const dir = targetDir || "my-idp";
3055
- if (existsSync3(join3(dir, "package.json"))) {
3737
+ if (existsSync6(join5(dir, "package.json"))) {
3056
3738
  throw new CliError(`Directory "${dir}" already contains a project.`);
3057
3739
  }
3058
3740
  const domain = await promptText("Domain for the IdP", "localhost");
@@ -3062,15 +3744,15 @@ async function initIdP(targetDir) {
3062
3744
  "s3 (S3-compatible)"
3063
3745
  ]);
3064
3746
  const adminEmail = await promptText("Admin email");
3065
- consola23.start("Scaffolding IdP starter...");
3747
+ consola27.start("Scaffolding IdP starter...");
3066
3748
  await downloadTemplate("openape-ai/openape-idp-starter", dir);
3067
- consola23.success("Scaffolded from openape-idp-starter");
3068
- consola23.start("Installing dependencies...");
3749
+ consola27.success("Scaffolded from openape-idp-starter");
3750
+ consola27.start("Installing dependencies...");
3069
3751
  installDeps(dir);
3070
- consola23.success("Dependencies installed");
3752
+ consola27.success("Dependencies installed");
3071
3753
  const sessionSecret = randomBytes(32).toString("hex");
3072
3754
  const managementToken = randomBytes(32).toString("hex");
3073
- consola23.success("Secrets generated");
3755
+ consola27.success("Secrets generated");
3074
3756
  const isLocalhost = domain === "localhost";
3075
3757
  const origin = isLocalhost ? "http://localhost:3000" : `https://${domain}`;
3076
3758
  const envContent = [
@@ -3084,11 +3766,11 @@ async function initIdP(targetDir) {
3084
3766
  `NUXT_OPENAPE_RP_ID=${domain}`,
3085
3767
  `NUXT_OPENAPE_RP_ORIGIN=${origin}`
3086
3768
  ].join("\n");
3087
- writeFileSync2(join3(dir, ".env"), `${envContent}
3769
+ writeFileSync5(join5(dir, ".env"), `${envContent}
3088
3770
  `, { mode: 384 });
3089
- consola23.success(".env created");
3771
+ consola27.success(".env created");
3090
3772
  console.log("");
3091
- consola23.box([
3773
+ consola27.box([
3092
3774
  `cd ${dir}`,
3093
3775
  "npm run dev",
3094
3776
  "",
@@ -3104,68 +3786,24 @@ async function initIdP(targetDir) {
3104
3786
  }
3105
3787
 
3106
3788
  // src/commands/enroll.ts
3107
- import { Buffer as Buffer2 } from "buffer";
3108
- import { existsSync as existsSync4, readFileSync as readFileSync2, writeFileSync as writeFileSync3, mkdirSync } from "fs";
3789
+ import { Buffer as Buffer4 } from "buffer";
3790
+ import { existsSync as existsSync7, readFileSync as readFileSync5 } from "fs";
3109
3791
  import { execFile as execFile2 } from "child_process";
3110
- import { generateKeyPairSync, sign } from "crypto";
3111
- import { dirname as dirname2, resolve as resolve3 } from "path";
3112
- import { homedir as homedir4 } from "os";
3113
- import { defineCommand as defineCommand29 } from "citty";
3114
- import consola24 from "consola";
3792
+ import { sign as sign2 } from "crypto";
3793
+ import { defineCommand as defineCommand34 } from "citty";
3794
+ import consola28 from "consola";
3115
3795
  var DEFAULT_IDP_URL2 = "https://id.openape.at";
3116
3796
  var DEFAULT_KEY_PATH = "~/.ssh/id_ed25519";
3117
3797
  var POLL_INTERVAL = 3e3;
3118
3798
  var POLL_TIMEOUT = 3e5;
3119
- function resolvePath2(p) {
3120
- return resolve3(p.replace(/^~/, homedir4()));
3121
- }
3122
3799
  function openBrowser2(url) {
3123
3800
  const cmd = process.platform === "darwin" ? "open" : process.platform === "win32" ? "start" : "xdg-open";
3124
3801
  execFile2(cmd, [url], () => {
3125
3802
  });
3126
3803
  }
3127
- function readPublicKey(keyPath) {
3128
- const pubPath = `${keyPath}.pub`;
3129
- if (existsSync4(pubPath)) {
3130
- return readFileSync2(pubPath, "utf-8").trim();
3131
- }
3132
- const keyContent = readFileSync2(keyPath, "utf-8");
3133
- const privateKey = loadEd25519PrivateKey(keyContent);
3134
- const jwk = privateKey.export({ format: "jwk" });
3135
- const pubBytes = Buffer2.from(jwk.x, "base64url");
3136
- const keyTypeStr = "ssh-ed25519";
3137
- const keyTypeLen = Buffer2.alloc(4);
3138
- keyTypeLen.writeUInt32BE(keyTypeStr.length);
3139
- const pubKeyLen = Buffer2.alloc(4);
3140
- pubKeyLen.writeUInt32BE(pubBytes.length);
3141
- const blob = Buffer2.concat([keyTypeLen, Buffer2.from(keyTypeStr), pubKeyLen, pubBytes]);
3142
- return `ssh-ed25519 ${blob.toString("base64")}`;
3143
- }
3144
- function generateAndSaveKey(keyPath) {
3145
- const resolved = resolvePath2(keyPath);
3146
- const dir = dirname2(resolved);
3147
- if (!existsSync4(dir)) {
3148
- mkdirSync(dir, { recursive: true });
3149
- }
3150
- const { publicKey, privateKey } = generateKeyPairSync("ed25519");
3151
- const privatePem = privateKey.export({ type: "pkcs8", format: "pem" });
3152
- writeFileSync3(resolved, privatePem, { mode: 384 });
3153
- const jwk = publicKey.export({ format: "jwk" });
3154
- const pubBytes = Buffer2.from(jwk.x, "base64url");
3155
- const keyTypeStr = "ssh-ed25519";
3156
- const keyTypeLen = Buffer2.alloc(4);
3157
- keyTypeLen.writeUInt32BE(keyTypeStr.length);
3158
- const pubKeyLen = Buffer2.alloc(4);
3159
- pubKeyLen.writeUInt32BE(pubBytes.length);
3160
- const blob = Buffer2.concat([keyTypeLen, Buffer2.from(keyTypeStr), pubKeyLen, pubBytes]);
3161
- const pubKeyStr = `ssh-ed25519 ${blob.toString("base64")}`;
3162
- writeFileSync3(`${resolved}.pub`, `${pubKeyStr}
3163
- `, { mode: 420 });
3164
- return pubKeyStr;
3165
- }
3166
3804
  async function pollForEnrollment(idp, agentEmail, keyPath) {
3167
- const resolvedKey = resolvePath2(keyPath);
3168
- const keyContent = readFileSync2(resolvedKey, "utf-8");
3805
+ const resolvedKey = resolveKeyPath(keyPath);
3806
+ const keyContent = readFileSync5(resolvedKey, "utf-8");
3169
3807
  const privateKey = loadEd25519PrivateKey(keyContent);
3170
3808
  const challengeUrl = await getAgentChallengeEndpoint(idp);
3171
3809
  const authenticateUrl = await getAgentAuthenticateEndpoint(idp);
@@ -3179,7 +3817,7 @@ async function pollForEnrollment(idp, agentEmail, keyPath) {
3179
3817
  });
3180
3818
  if (challengeResp.ok) {
3181
3819
  const { challenge } = await challengeResp.json();
3182
- const signature = sign(null, Buffer2.from(challenge), privateKey).toString("base64");
3820
+ const signature = sign2(null, Buffer4.from(challenge), privateKey).toString("base64");
3183
3821
  const authResp = await fetch(authenticateUrl, {
3184
3822
  method: "POST",
3185
3823
  headers: { "Content-Type": "application/json" },
@@ -3196,7 +3834,7 @@ async function pollForEnrollment(idp, agentEmail, keyPath) {
3196
3834
  }
3197
3835
  throw new Error("Enrollment timed out. Please check the browser and try again.");
3198
3836
  }
3199
- var enrollCommand = defineCommand29({
3837
+ var enrollCommand = defineCommand34({
3200
3838
  meta: {
3201
3839
  name: "enroll",
3202
3840
  description: "Enroll an agent with an Identity Provider"
@@ -3216,38 +3854,38 @@ var enrollCommand = defineCommand29({
3216
3854
  }
3217
3855
  },
3218
3856
  async run({ args }) {
3219
- const idp = args.idp || await consola24.prompt("IdP URL", { type: "text", default: DEFAULT_IDP_URL2, placeholder: DEFAULT_IDP_URL2 }).then((r) => {
3857
+ const idp = args.idp || await consola28.prompt("IdP URL", { type: "text", default: DEFAULT_IDP_URL2, placeholder: DEFAULT_IDP_URL2 }).then((r) => {
3220
3858
  if (typeof r === "symbol") throw new CliExit(0);
3221
3859
  return r;
3222
3860
  }) || DEFAULT_IDP_URL2;
3223
- const agentName = args.name || await consola24.prompt("Agent name", { type: "text", placeholder: "deploy-bot" }).then((r) => {
3861
+ const agentName = args.name || await consola28.prompt("Agent name", { type: "text", placeholder: "deploy-bot" }).then((r) => {
3224
3862
  if (typeof r === "symbol") throw new CliExit(0);
3225
3863
  return r;
3226
3864
  });
3227
3865
  if (!agentName) {
3228
3866
  throw new CliError("Agent name is required.");
3229
3867
  }
3230
- const keyPath = args.key || await consola24.prompt("Ed25519 key", { type: "text", default: DEFAULT_KEY_PATH, placeholder: DEFAULT_KEY_PATH }).then((r) => {
3868
+ const keyPath = args.key || await consola28.prompt("Ed25519 key", { type: "text", default: DEFAULT_KEY_PATH, placeholder: DEFAULT_KEY_PATH }).then((r) => {
3231
3869
  if (typeof r === "symbol") throw new CliExit(0);
3232
3870
  return r;
3233
3871
  }) || DEFAULT_KEY_PATH;
3234
- const resolvedKey = resolvePath2(keyPath);
3872
+ const resolvedKey = resolveKeyPath(keyPath);
3235
3873
  let publicKey;
3236
- if (existsSync4(resolvedKey)) {
3874
+ if (existsSync7(resolvedKey)) {
3237
3875
  publicKey = readPublicKey(resolvedKey);
3238
- consola24.success(`Using existing key ${keyPath}`);
3876
+ consola28.success(`Using existing key ${keyPath}`);
3239
3877
  } else {
3240
- consola24.start(`Generating Ed25519 key pair at ${keyPath}...`);
3878
+ consola28.start(`Generating Ed25519 key pair at ${keyPath}...`);
3241
3879
  publicKey = generateAndSaveKey(keyPath);
3242
- consola24.success(`Key pair generated at ${keyPath}`);
3880
+ consola28.success(`Key pair generated at ${keyPath}`);
3243
3881
  }
3244
3882
  const encodedKey = encodeURIComponent(publicKey);
3245
3883
  const enrollUrl = `${idp}/enroll?name=${encodeURIComponent(agentName)}&key=${encodedKey}`;
3246
- consola24.info("Opening browser for enrollment...");
3247
- consola24.info(`\u2192 ${idp}/enroll`);
3884
+ consola28.info("Opening browser for enrollment...");
3885
+ consola28.info(`\u2192 ${idp}/enroll`);
3248
3886
  openBrowser2(enrollUrl);
3249
3887
  console.log("");
3250
- const agentEmail = await consola24.prompt(
3888
+ const agentEmail = await consola28.prompt(
3251
3889
  "Agent email (shown in browser after enrollment)",
3252
3890
  { type: "text", placeholder: `agent+${agentName}@...` }
3253
3891
  ).then((r) => {
@@ -3257,7 +3895,7 @@ var enrollCommand = defineCommand29({
3257
3895
  if (!agentEmail) {
3258
3896
  throw new CliError("Agent email is required to verify enrollment.");
3259
3897
  }
3260
- consola24.start("Verifying enrollment...");
3898
+ consola28.start("Verifying enrollment...");
3261
3899
  const { token, expiresIn } = await pollForEnrollment(idp, agentEmail, keyPath);
3262
3900
  saveAuth({
3263
3901
  idp,
@@ -3269,18 +3907,18 @@ var enrollCommand = defineCommand29({
3269
3907
  config.defaults = { ...config.defaults, idp };
3270
3908
  config.agent = { key: keyPath, email: agentEmail };
3271
3909
  saveConfig(config);
3272
- consola24.success(`Agent enrolled as ${agentEmail}`);
3273
- consola24.success("Config saved to ~/.config/apes/");
3910
+ consola28.success(`Agent enrolled as ${agentEmail}`);
3911
+ consola28.success("Config saved to ~/.config/apes/");
3274
3912
  console.log("");
3275
- consola24.info("Verify with: apes whoami");
3913
+ consola28.info("Verify with: apes whoami");
3276
3914
  }
3277
3915
  });
3278
3916
 
3279
3917
  // src/commands/register-user.ts
3280
- import { existsSync as existsSync5, readFileSync as readFileSync3 } from "fs";
3281
- import { defineCommand as defineCommand30 } from "citty";
3282
- import consola25 from "consola";
3283
- var registerUserCommand = defineCommand30({
3918
+ import { existsSync as existsSync8, readFileSync as readFileSync6 } from "fs";
3919
+ import { defineCommand as defineCommand35 } from "citty";
3920
+ import consola29 from "consola";
3921
+ var registerUserCommand = defineCommand35({
3284
3922
  meta: {
3285
3923
  name: "register-user",
3286
3924
  description: "Register a sub-user with SSH key"
@@ -3316,8 +3954,8 @@ var registerUserCommand = defineCommand30({
3316
3954
  throw new CliError("No IdP URL configured. Run `apes login` first.");
3317
3955
  }
3318
3956
  let publicKey = args.key;
3319
- if (existsSync5(args.key)) {
3320
- publicKey = readFileSync3(args.key, "utf-8").trim();
3957
+ if (existsSync8(args.key)) {
3958
+ publicKey = readFileSync6(args.key, "utf-8").trim();
3321
3959
  }
3322
3960
  if (!publicKey.startsWith("ssh-ed25519 ")) {
3323
3961
  throw new CliError("Public key must be in ssh-ed25519 format.");
@@ -3335,15 +3973,15 @@ var registerUserCommand = defineCommand30({
3335
3973
  ...userType ? { type: userType } : {}
3336
3974
  }
3337
3975
  });
3338
- consola25.success(`User registered: ${result.email} (type: ${result.type}, owner: ${result.owner})`);
3976
+ consola29.success(`User registered: ${result.email} (type: ${result.type}, owner: ${result.owner})`);
3339
3977
  }
3340
3978
  });
3341
3979
 
3342
3980
  // src/commands/dns-check.ts
3343
- import { defineCommand as defineCommand31 } from "citty";
3344
- import consola26 from "consola";
3981
+ import { defineCommand as defineCommand36 } from "citty";
3982
+ import consola30 from "consola";
3345
3983
  import { resolveDDISA as resolveDDISA2 } from "@openape/core";
3346
- var dnsCheckCommand = defineCommand31({
3984
+ var dnsCheckCommand = defineCommand36({
3347
3985
  meta: {
3348
3986
  name: "dns-check",
3349
3987
  description: "Validate DDISA DNS TXT records for a domain"
@@ -3357,7 +3995,7 @@ var dnsCheckCommand = defineCommand31({
3357
3995
  },
3358
3996
  async run({ args }) {
3359
3997
  const domain = args.domain;
3360
- consola26.start(`Checking _ddisa.${domain}...`);
3998
+ consola30.start(`Checking _ddisa.${domain}...`);
3361
3999
  try {
3362
4000
  const result = await resolveDDISA2(domain);
3363
4001
  if (!result) {
@@ -3366,7 +4004,7 @@ var dnsCheckCommand = defineCommand31({
3366
4004
  console.log(` _ddisa.${domain} TXT "v=ddisa1 idp=https://id.${domain}"`);
3367
4005
  throw new CliError(`No DDISA record found for ${domain}`);
3368
4006
  }
3369
- consola26.success(`_ddisa.${domain} \u2192 ${result.idp}`);
4007
+ consola30.success(`_ddisa.${domain} \u2192 ${result.idp}`);
3370
4008
  console.log("");
3371
4009
  console.log(` Version: ${result.version || "ddisa1"}`);
3372
4010
  console.log(` IdP URL: ${result.idp}`);
@@ -3375,14 +4013,14 @@ var dnsCheckCommand = defineCommand31({
3375
4013
  if (result.priority !== void 0)
3376
4014
  console.log(` Priority: ${result.priority}`);
3377
4015
  console.log("");
3378
- consola26.start(`Verifying IdP at ${result.idp}...`);
4016
+ consola30.start(`Verifying IdP at ${result.idp}...`);
3379
4017
  const discoResp = await fetch(`${result.idp}/.well-known/openid-configuration`);
3380
4018
  if (!discoResp.ok) {
3381
- consola26.warn(`IdP discovery failed (${discoResp.status}). Is the IdP running at ${result.idp}?`);
4019
+ consola30.warn(`IdP discovery failed (${discoResp.status}). Is the IdP running at ${result.idp}?`);
3382
4020
  return;
3383
4021
  }
3384
4022
  const disco = await discoResp.json();
3385
- consola26.success(`IdP is reachable`);
4023
+ consola30.success(`IdP is reachable`);
3386
4024
  console.log(` Issuer: ${disco.issuer}`);
3387
4025
  console.log(` DDISA: v${disco.ddisa_version || "?"}`);
3388
4026
  if (disco.ddisa_auth_methods_supported) {
@@ -3400,7 +4038,7 @@ var dnsCheckCommand = defineCommand31({
3400
4038
  // src/commands/health.ts
3401
4039
  import { exec } from "child_process";
3402
4040
  import { promisify } from "util";
3403
- import { defineCommand as defineCommand32 } from "citty";
4041
+ import { defineCommand as defineCommand37 } from "citty";
3404
4042
  var execAsync = promisify(exec);
3405
4043
  async function resolveApeShellPath() {
3406
4044
  try {
@@ -3436,7 +4074,7 @@ async function bestEffortGrantCount(idp) {
3436
4074
  }
3437
4075
  }
3438
4076
  async function runHealth(args) {
3439
- const version = true ? "0.14.3" : "0.0.0";
4077
+ const version = true ? "0.15.1" : "0.0.0";
3440
4078
  const auth = loadAuth();
3441
4079
  if (!auth) {
3442
4080
  throw new CliError("Not logged in. Run `apes login` first.", 1);
@@ -3499,7 +4137,7 @@ async function runHealth(args) {
3499
4137
  throw new CliError(`IdP ${auth.idp} unreachable: ${idpProbe.error}`, 1);
3500
4138
  }
3501
4139
  }
3502
- var healthCommand = defineCommand32({
4140
+ var healthCommand = defineCommand37({
3503
4141
  meta: {
3504
4142
  name: "health",
3505
4143
  description: "Report CLI diagnostic state (auth, IdP, grants, binaries)"
@@ -3517,8 +4155,8 @@ var healthCommand = defineCommand32({
3517
4155
  });
3518
4156
 
3519
4157
  // src/commands/workflows.ts
3520
- import { defineCommand as defineCommand33 } from "citty";
3521
- import consola27 from "consola";
4158
+ import { defineCommand as defineCommand38 } from "citty";
4159
+ import consola31 from "consola";
3522
4160
 
3523
4161
  // src/guides/index.ts
3524
4162
  var guides = [
@@ -3568,7 +4206,7 @@ var guides = [
3568
4206
  ];
3569
4207
 
3570
4208
  // src/commands/workflows.ts
3571
- var workflowsCommand = defineCommand33({
4209
+ var workflowsCommand = defineCommand38({
3572
4210
  meta: {
3573
4211
  name: "workflows",
3574
4212
  description: "Discover workflow guides"
@@ -3589,7 +4227,7 @@ var workflowsCommand = defineCommand33({
3589
4227
  if (args.id) {
3590
4228
  const guide = guides.find((g) => g.id === String(args.id));
3591
4229
  if (!guide) {
3592
- consola27.info(`Available: ${guides.map((g) => g.id).join(", ")}`);
4230
+ consola31.info(`Available: ${guides.map((g) => g.id).join(", ")}`);
3593
4231
  throw new CliError(`Guide not found: ${args.id}`);
3594
4232
  }
3595
4233
  if (args.json) {
@@ -3638,10 +4276,10 @@ if (shellRewrite) {
3638
4276
  if (shellRewrite.action === "rewrite") {
3639
4277
  process.argv = shellRewrite.argv;
3640
4278
  } else if (shellRewrite.action === "version") {
3641
- console.log(`ape-shell ${"0.14.3"} (OpenApe DDISA shell wrapper)`);
4279
+ console.log(`ape-shell ${"0.15.1"} (OpenApe DDISA shell wrapper)`);
3642
4280
  process.exit(0);
3643
4281
  } else if (shellRewrite.action === "help") {
3644
- console.log(`ape-shell ${"0.14.3"} \u2014 OpenApe DDISA shell wrapper`);
4282
+ console.log(`ape-shell ${"0.15.1"} \u2014 OpenApe DDISA shell wrapper`);
3645
4283
  console.log("");
3646
4284
  console.log("Usage:");
3647
4285
  console.log(" ape-shell Start interactive grant-mediated REPL");
@@ -3665,7 +4303,7 @@ if (shellRewrite) {
3665
4303
  }
3666
4304
  }
3667
4305
  var debug = process.argv.includes("--debug");
3668
- var grantsCommand = defineCommand34({
4306
+ var grantsCommand = defineCommand39({
3669
4307
  meta: {
3670
4308
  name: "grants",
3671
4309
  description: "Grant management"
@@ -3686,7 +4324,7 @@ var grantsCommand = defineCommand34({
3686
4324
  "delegation-revoke": delegationRevokeCommand
3687
4325
  }
3688
4326
  });
3689
- var configCommand = defineCommand34({
4327
+ var configCommand = defineCommand39({
3690
4328
  meta: {
3691
4329
  name: "config",
3692
4330
  description: "Configuration management"
@@ -3696,10 +4334,10 @@ var configCommand = defineCommand34({
3696
4334
  set: configSetCommand
3697
4335
  }
3698
4336
  });
3699
- var main = defineCommand34({
4337
+ var main = defineCommand39({
3700
4338
  meta: {
3701
4339
  name: "apes",
3702
- version: "0.14.3",
4340
+ version: "0.15.1",
3703
4341
  description: "Unified CLI for OpenApe"
3704
4342
  },
3705
4343
  subCommands: {
@@ -3712,6 +4350,7 @@ var main = defineCommand34({
3712
4350
  whoami: whoamiCommand,
3713
4351
  health: healthCommand,
3714
4352
  grants: grantsCommand,
4353
+ agents: agentsCommand,
3715
4354
  admin: adminCommand,
3716
4355
  run: runCommand,
3717
4356
  proxy: proxyCommand,
@@ -3728,13 +4367,13 @@ runMain(main).catch((err) => {
3728
4367
  process.exit(err.exitCode);
3729
4368
  }
3730
4369
  if (err instanceof CliError) {
3731
- consola28.error(err.message);
4370
+ consola32.error(err.message);
3732
4371
  process.exit(err.exitCode);
3733
4372
  }
3734
4373
  if (debug) {
3735
- consola28.error(err);
4374
+ consola32.error(err);
3736
4375
  } else {
3737
- consola28.error(err instanceof ApiError ? err.message : err instanceof Error ? err.message : String(err));
4376
+ consola32.error(err instanceof ApiError ? err.message : err instanceof Error ? err.message : String(err));
3738
4377
  }
3739
4378
  process.exit(1);
3740
4379
  });