@openape/apes 1.29.1 → 1.31.0

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
@@ -50,7 +50,7 @@ import {
50
50
  getAgentChallengeEndpoint,
51
51
  getDelegationsEndpoint,
52
52
  getGrantsEndpoint
53
- } from "./chunk-NYJSBFLG.js";
53
+ } from "./chunk-MMBFV5WN.js";
54
54
  import {
55
55
  AUTH_FILE,
56
56
  CONFIG_DIR,
@@ -65,7 +65,7 @@ import {
65
65
  } from "./chunk-OBF7IMQ2.js";
66
66
 
67
67
  // src/cli.ts
68
- import consola52 from "consola";
68
+ import consola53 from "consola";
69
69
 
70
70
  // src/ape-shell.ts
71
71
  import path from "path";
@@ -98,7 +98,7 @@ function rewriteApeShellArgs(argv, argv0) {
98
98
  import { defineCommand as defineCommand64, runMain } from "citty";
99
99
 
100
100
  // src/commands/auth/login.ts
101
- import { Buffer as Buffer2 } from "buffer";
101
+ import { Buffer } from "buffer";
102
102
  import { execFile } from "child_process";
103
103
  import { createServer } from "http";
104
104
  import { homedir as homedir2 } from "os";
@@ -308,7 +308,7 @@ async function loginWithPKCE(idp) {
308
308
  authUrl.searchParams.set("state", state);
309
309
  authUrl.searchParams.set("nonce", nonce);
310
310
  authUrl.searchParams.set("scope", "openid email profile offline_access");
311
- const code = await new Promise((resolve4, reject) => {
311
+ const code = await new Promise((resolve5, reject) => {
312
312
  const server = createServer((req, res) => {
313
313
  const url = new URL(req.url, `http://localhost:${CALLBACK_PORT}`);
314
314
  if (url.pathname === "/callback") {
@@ -325,7 +325,7 @@ async function loginWithPKCE(idp) {
325
325
  res.writeHead(200, { "Content-Type": "text/html" });
326
326
  res.end("<h1>Login successful!</h1><p>You can close this window.</p>");
327
327
  server.close();
328
- resolve4(authCode);
328
+ resolve5(authCode);
329
329
  return;
330
330
  }
331
331
  res.writeHead(400);
@@ -381,7 +381,7 @@ async function loginWithPKCE(idp) {
381
381
  consola2.success(`Logged in as ${payload.email || payload.sub}`);
382
382
  }
383
383
  async function loginWithKey(idp, keyPath, agentEmail) {
384
- const { readFileSync: readFileSync21 } = await import("fs");
384
+ const { readFileSync: readFileSync19 } = await import("fs");
385
385
  const { sign: sign3 } = await import("crypto");
386
386
  const { loadEd25519PrivateKey: loadEd25519PrivateKey2 } = await import("./ssh-key-YBNNG5K5.js");
387
387
  const challengeUrl = await getAgentChallengeEndpoint(idp);
@@ -395,9 +395,9 @@ async function loginWithKey(idp, keyPath, agentEmail) {
395
395
  throw new CliError(`Challenge failed: ${await challengeResp.text()}`);
396
396
  }
397
397
  const { challenge } = await challengeResp.json();
398
- const keyContent = readFileSync21(keyPath, "utf-8");
398
+ const keyContent = readFileSync19(keyPath, "utf-8");
399
399
  const privateKey = loadEd25519PrivateKey2(keyContent);
400
- const signature = sign3(null, Buffer2.from(challenge), privateKey).toString("base64");
400
+ const signature = sign3(null, Buffer.from(challenge), privateKey).toString("base64");
401
401
  const authenticateUrl = await getAgentAuthenticateEndpoint(idp);
402
402
  const authResp = await fetch(authenticateUrl, {
403
403
  method: "POST",
@@ -888,7 +888,7 @@ async function waitForApproval2(grantsUrl, grantId) {
888
888
  if (grant.status === "revoked") {
889
889
  throw new CliError("Grant revoked.");
890
890
  }
891
- await new Promise((resolve4) => setTimeout(resolve4, interval));
891
+ await new Promise((resolve5) => setTimeout(resolve5, interval));
892
892
  }
893
893
  throw new CliError("Timed out waiting for approval.");
894
894
  }
@@ -1976,15 +1976,13 @@ var agentCommand = defineCommand21({
1976
1976
  import { defineCommand as defineCommand32 } from "citty";
1977
1977
 
1978
1978
  // src/commands/agents/allow.ts
1979
- import { execFileSync as execFileSync10 } from "child_process";
1979
+ import { execFileSync as execFileSync6 } from "child_process";
1980
1980
  import { defineCommand as defineCommand22 } from "citty";
1981
1981
  import consola19 from "consola";
1982
1982
 
1983
1983
  // src/lib/agent-bootstrap.ts
1984
- import { Buffer as Buffer3 } from "buffer";
1985
- import { execFileSync as execFileSync2 } from "child_process";
1984
+ import { Buffer as Buffer2 } from "buffer";
1986
1985
  import { createPrivateKey, sign } from "crypto";
1987
- import { rmSync } from "fs";
1988
1986
  import { exchangeWithDelegation } from "@openape/cli-auth";
1989
1987
  var AGENT_NAME_REGEX = /^[a-z][a-z0-9-]{0,23}$/;
1990
1988
  var SSH_ED25519_PREFIX = "ssh-ed25519 ";
@@ -2049,7 +2047,7 @@ function decodeJwtClaims(token) {
2049
2047
  const part = token.split(".")[1];
2050
2048
  if (!part) return null;
2051
2049
  const padded = part + "=".repeat((4 - part.length % 4) % 4);
2052
- const json = Buffer3.from(padded, "base64").toString("utf8");
2050
+ const json = Buffer2.from(padded, "base64").toString("utf8");
2053
2051
  return JSON.parse(json);
2054
2052
  } catch {
2055
2053
  return null;
@@ -2061,19 +2059,23 @@ async function issueAgentToken(input) {
2061
2059
  const challengeResp = await fetch(challengeUrl, {
2062
2060
  method: "POST",
2063
2061
  headers: { "Content-Type": "application/json" },
2064
- body: JSON.stringify({ agent_id: input.agentEmail })
2062
+ // Canonical /api/auth/challenge expects `id` (M3). The legacy
2063
+ // /api/agent/challenge field was `agent_id`; discovery now resolves
2064
+ // the canonical endpoint, so the payload must use `id` to match.
2065
+ body: JSON.stringify({ id: input.agentEmail })
2065
2066
  });
2066
2067
  if (!challengeResp.ok) {
2067
2068
  const text = await challengeResp.text().catch(() => "");
2068
2069
  throw new Error(`Challenge failed (${challengeResp.status}): ${text}`);
2069
2070
  }
2070
2071
  const { challenge } = await challengeResp.json();
2071
- const signature = sign(null, Buffer3.from(challenge), privateKey).toString("base64");
2072
+ const signature = sign(null, Buffer2.from(challenge), privateKey).toString("base64");
2072
2073
  const authenticateUrl = await getAgentAuthenticateEndpoint(input.idp);
2073
2074
  const authResp = await fetch(authenticateUrl, {
2074
2075
  method: "POST",
2075
2076
  headers: { "Content-Type": "application/json" },
2076
- body: JSON.stringify({ agent_id: input.agentEmail, challenge, signature })
2077
+ // Canonical /api/auth/authenticate expects `id` (same M3 rename).
2078
+ body: JSON.stringify({ id: input.agentEmail, challenge, signature })
2077
2079
  });
2078
2080
  if (!authResp.ok) {
2079
2081
  const text = await authResp.text().catch(() => "");
@@ -2092,7 +2094,7 @@ ${content}
2092
2094
  ${SH_HEREDOC_DELIMITER}`;
2093
2095
  }
2094
2096
  function buildSpawnSetupScript(input) {
2095
- const { name, macOSUsername, homeDir, shellPath } = input;
2097
+ const { name, homeDir, shellPath } = input;
2096
2098
  const privatePemForHeredoc = input.privateKeyPem.endsWith("\n") ? input.privateKeyPem : `${input.privateKeyPem}
2097
2099
  `;
2098
2100
  const claudeBlock = input.claudeSettingsJson && input.hookScriptSource ? `
@@ -2100,118 +2102,54 @@ mkdir -p "$HOME_DIR/.claude/hooks"
2100
2102
  cat > "$HOME_DIR/.claude/settings.json" ${shHeredoc(input.claudeSettingsJson)}
2101
2103
  cat > "$HOME_DIR/.claude/hooks/bash-via-ape-shell.sh" ${shHeredoc(input.hookScriptSource)}
2102
2104
  chmod 755 "$HOME_DIR/.claude/hooks/bash-via-ape-shell.sh"
2103
- ` : "";
2104
- const claudeTokenBlock = input.claudeOauthToken ? `
2105
- mkdir -p "$HOME_DIR/.config/openape"
2106
- cat > "$HOME_DIR/.config/openape/claude-token.env" ${shHeredoc(`# Auto-generated by 'apes agents spawn'. chmod 600 \u2014 contains a long-lived
2107
- # Claude Code OAuth token. Rotate by editing this file in place; the
2108
- # .zshenv / .profile source-lines below will pick it up automatically.
2109
- export CLAUDE_CODE_OAUTH_TOKEN=${shQuote(input.claudeOauthToken)}
2110
- `)}
2111
- SOURCE_LINE='[ -f "$HOME/.config/openape/claude-token.env" ] && . "$HOME/.config/openape/claude-token.env"'
2112
- for f in "$HOME_DIR/.zshenv" "$HOME_DIR/.profile"; do
2113
- touch "$f"
2114
- if ! grep -qF 'config/openape/claude-token.env' "$f" 2>/dev/null; then
2115
- {
2116
- echo ''
2117
- echo '# OpenApe: load Claude Code OAuth token (added by apes agents spawn)'
2118
- echo "$SOURCE_LINE"
2119
- } >> "$f"
2120
- fi
2121
- done
2122
2105
  ` : "";
2123
2106
  return `#!/bin/bash
2124
2107
  set -euo pipefail
2125
2108
 
2126
- # escapes-spawned scripts inherit a minimal PATH that doesn't include
2127
- # /usr/sbin \u2014 which is where chown / dscl / pwpolicy live. Force a
2128
- # wide PATH so the privileged setup commands resolve without absolute
2129
- # paths everywhere.
2130
- export PATH="/usr/sbin:/usr/bin:/bin:/sbin:/opt/homebrew/bin:/usr/local/bin"
2109
+ # Wide PATH so useradd / getent / install / chown resolve regardless of
2110
+ # how the privileged wrapper trimmed the environment.
2111
+ export PATH="/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"
2131
2112
 
2132
2113
  NAME=${shQuote(name)}
2133
- MACOS_USER=${shQuote(macOSUsername)}
2134
2114
  HOME_DIR=${shQuote(homeDir)}
2135
2115
  SHELL_PATH=${shQuote(shellPath)}
2136
2116
 
2137
- # Tombstone-reuse: \`apes agents destroy\` leaves the dscl user
2138
- # record behind (opendirectoryd refuses \`dscl . -delete\` from
2139
- # escapes' setuid-root context without admin auth, so we accept the
2140
- # stranded record as a harmless tombstone \u2014 see
2141
- # runPhaseGTeardownInProcess). When the operator re-spawns with the
2142
- # same name, we recycle the tombstone instead of refusing: if the
2143
- # previously-recorded home dir is gone from disk (matching the
2144
- # Phase-G teardown's \`rm -rf\`), we reuse the existing UID +
2145
- # overwrite the home. If the home is still on disk it's a real,
2146
- # live agent \u2014 refuse.
2147
- TOMBSTONE_REUSE=0
2148
- if dscl . -read "/Users/$MACOS_USER" >/dev/null 2>&1; then
2149
- EXISTING_HOME=$(dscl . -read "/Users/$MACOS_USER" NFSHomeDirectory 2>/dev/null | awk '/NFSHomeDirectory:/ {print $2}')
2150
- if [ -n "$EXISTING_HOME" ] && [ -d "$EXISTING_HOME" ]; then
2151
- echo "User $MACOS_USER already exists with a live home at $EXISTING_HOME; refusing to overwrite." >&2
2152
- exit 1
2153
- fi
2154
- NEXT_UID=$(dscl . -read "/Users/$MACOS_USER" UniqueID 2>/dev/null | awk '/UniqueID:/ {print $2}')
2155
- if [ -z "$NEXT_UID" ]; then
2156
- echo "User $MACOS_USER exists but has no UniqueID; record is malformed, refusing to recycle." >&2
2157
- exit 1
2158
- fi
2159
- echo "Recycling tombstone dscl record for $MACOS_USER (uid=$NEXT_UID, prior home $EXISTING_HOME \u2014 gone)"
2160
- TOMBSTONE_REUSE=1
2117
+ # Agent homes live under /var/lib/openape/homes/ (the persisted
2118
+ # openape-homes volume) \u2014 out of /home/ where real operator accounts
2119
+ # live. useradd --create-home makes the leaf dir but not missing
2120
+ # parents; the volume mount provides the parent, but pre-create it
2121
+ # anyway so a bare-metal/non-volume run still works.
2122
+ mkdir -p /var/lib/openape/homes
2123
+ chmod 755 /var/lib/openape/homes
2124
+
2125
+ # Create the agent's OS user if absent. spawn.ts already refused earlier
2126
+ # when the user existed, but guard here too so a re-run of the privileged
2127
+ # script is idempotent rather than erroring on a half-created account.
2128
+ if ! getent passwd "$NAME" >/dev/null 2>&1; then
2129
+ useradd --create-home --home-dir "$HOME_DIR" --shell "$SHELL_PATH" --comment "OpenApe Agent $NAME" "$NAME"
2161
2130
  fi
2162
2131
 
2163
- # Phase G: agent home dirs live under /var/openape/homes/, not
2164
- # /Users/. Pre-create the parent and chmod it world-traversable
2165
- # so per-agent dirs can be reached by their respective uids.
2166
- mkdir -p /var/openape/homes
2167
- chmod 755 /var/openape/homes
2168
-
2169
- if [ "$TOMBSTONE_REUSE" = "0" ]; then
2170
- # Pick the LOWEST free UID in the [200, 500) hidden service-account
2171
- # range. We must skip every occupied UID \u2014 agent users AND the many
2172
- # macOS system _* accounts \u2014 and reuse gaps. The old code took
2173
- # max(existing)+1, so a single agent landing on 499 wedged every
2174
- # future spawn ("No free UID") even with 100+ UIDs free. Now we scan
2175
- # for the first actually-unused UID.
2176
- OCCUPIED_UIDS=$(dscl . -list /Users UniqueID | awk '$2 >= 200 && $2 < 500 {print $2}' | sort -n -u)
2177
- NEXT_UID=""
2178
- candidate=200
2179
- while [ "$candidate" -lt 500 ]; do
2180
- if ! printf '%s
2181
- ' "$OCCUPIED_UIDS" | grep -qx "$candidate"; then
2182
- NEXT_UID=$candidate
2183
- break
2184
- fi
2185
- candidate=$((candidate + 1))
2186
- done
2187
- if [ -z "$NEXT_UID" ]; then
2188
- echo "No free UID in [200, 500) \u2014 refusing to clobber a real user." >&2
2189
- exit 1
2190
- fi
2191
-
2192
- dscl . -create "/Users/$MACOS_USER"
2193
- fi
2194
-
2195
- # Idempotent attribute writes \u2014 \`dscl . -create\` on an existing
2196
- # property overwrites in place, so the tombstone-reuse path lands
2197
- # the same end-state as a fresh create.
2198
- dscl . -create "/Users/$MACOS_USER" UserShell "$SHELL_PATH"
2199
- dscl . -create "/Users/$MACOS_USER" RealName "OpenApe Agent $NAME"
2200
- dscl . -create "/Users/$MACOS_USER" UniqueID "$NEXT_UID"
2201
- dscl . -create "/Users/$MACOS_USER" PrimaryGroupID 20
2202
- dscl . -create "/Users/$MACOS_USER" NFSHomeDirectory "$HOME_DIR"
2203
- dscl . -create "/Users/$MACOS_USER" IsHidden 1
2132
+ # Resolve the uid for the final report line (getent is the canonical read).
2133
+ NEW_UID=$(getent passwd "$NAME" | cut -d: -f3)
2204
2134
 
2205
- mkdir -p "$HOME_DIR/.ssh" "$HOME_DIR/.config/apes"
2135
+ # Identity dirs \u2014 created owned by the agent so the file writes below land
2136
+ # with the right owner even before the final recursive chown.
2137
+ install -d -m 700 -o "$NAME" "$HOME_DIR/.ssh"
2138
+ install -d -m 700 -o "$NAME" "$HOME_DIR/.config"
2139
+ install -d -m 700 -o "$NAME" "$HOME_DIR/.config/apes"
2140
+ install -d -m 700 -o "$NAME" "$HOME_DIR/.config/openape"
2206
2141
 
2207
2142
  cat > "$HOME_DIR/.ssh/id_ed25519" ${shHeredoc(privatePemForHeredoc.trimEnd())}
2208
- cat > "$HOME_DIR/.ssh/id_ed25519.pub" ${shHeredoc(`${input.publicKeySshLine}`)}
2143
+ cat > "$HOME_DIR/.ssh/id_ed25519.pub" ${shHeredoc(input.publicKeySshLine)}
2209
2144
  cat > "$HOME_DIR/.config/apes/auth.json" ${shHeredoc(input.authJson)}
2210
- mkdir -p "$HOME_DIR/.config/openape"
2211
2145
  cat > "$HOME_DIR/.config/openape/agent-x25519.key" ${shHeredoc(input.x25519PrivateKey)}
2212
2146
  cat > "$HOME_DIR/.config/openape/agent-x25519.key.pub" ${shHeredoc(input.x25519PublicKey)}
2213
- ${claudeBlock}${claudeTokenBlock}${buildBridgeBlock(input.bridge)}${buildTroopBlock(input.troop)}
2214
- chown -R "$MACOS_USER:staff" "$HOME_DIR"
2147
+ ${claudeBlock}
2148
+ # Per-agent task dir that \`apes agents sync\` writes to (XDG-style on
2149
+ # Linux; was ~/Library/... on macOS).
2150
+ mkdir -p "$HOME_DIR/.openape/agent/tasks"
2151
+
2152
+ chown -R "$NAME:" "$HOME_DIR"
2215
2153
  chmod 700 "$HOME_DIR/.ssh"
2216
2154
  chmod 700 "$HOME_DIR/.config"
2217
2155
  chmod 700 "$HOME_DIR/.config/openape"
@@ -2220,143 +2158,8 @@ chmod 644 "$HOME_DIR/.ssh/id_ed25519.pub"
2220
2158
  chmod 600 "$HOME_DIR/.config/apes/auth.json"
2221
2159
  chmod 600 "$HOME_DIR/.config/openape/agent-x25519.key"
2222
2160
  chmod 644 "$HOME_DIR/.config/openape/agent-x25519.key.pub"
2223
- if [ -f "$HOME_DIR/.config/openape/claude-token.env" ]; then
2224
- chmod 700 "$HOME_DIR/.config/openape"
2225
- chmod 600 "$HOME_DIR/.config/openape/claude-token.env"
2226
- fi
2227
-
2228
- echo "OK $NAME (macOS user $MACOS_USER) uid=$NEXT_UID home=$HOME_DIR"
2229
- ${buildBridgeBootstrapBlock(input.bridge, name)}${buildTroopBootstrapBlock(input.troop, name)}`;
2230
- }
2231
- function buildBridgeBlock(bridge) {
2232
- if (!bridge) return "";
2233
- return `
2234
- mkdir -p "$HOME_DIR/Library/Application Support/openape/bridge" "$HOME_DIR/Library/Logs"
2235
- cat > "$HOME_DIR/Library/Application Support/openape/bridge/.env" ${shHeredoc(bridge.envFile)}
2236
- chmod 600 "$HOME_DIR/Library/Application Support/openape/bridge/.env"
2237
- `;
2238
- }
2239
- function buildBridgeBootstrapBlock(_bridge, _name) {
2240
- return "";
2241
- }
2242
- function buildTroopBlock(_troop) {
2243
- return `
2244
- mkdir -p "$HOME_DIR/Library/Logs" "$HOME_DIR/.openape/agent/tasks"
2245
- `;
2246
- }
2247
- function buildTroopBootstrapBlock(_troop, _name) {
2248
- return "";
2249
- }
2250
- function runPhaseGTeardownInProcess(input) {
2251
- const { name, homeDir } = input;
2252
- const macOSUser = input.macOSUsername ?? name;
2253
- let uid = null;
2254
- try {
2255
- const out = execFileSync2("/usr/bin/dscl", [".", "-read", `/Users/${macOSUser}`, "UniqueID"], { encoding: "utf8" });
2256
- const m = out.match(/UniqueID:\s*(\d+)/);
2257
- if (m) uid = m[1];
2258
- } catch {
2259
- }
2260
- if (uid) {
2261
- try {
2262
- execFileSync2("/bin/launchctl", ["bootout", `user/${uid}`], { stdio: "ignore" });
2263
- } catch {
2264
- }
2265
- try {
2266
- execFileSync2("/usr/bin/pkill", ["-9", "-u", uid], { stdio: "ignore" });
2267
- } catch {
2268
- }
2269
- }
2270
- const agentDir = `/var/openape/agents/${name}`;
2271
- try {
2272
- rmSync(agentDir, { recursive: true, force: true });
2273
- } catch {
2274
- }
2275
- if (homeDir && homeDir !== "/" && homeDir.startsWith("/var/openape/homes/")) {
2276
- try {
2277
- rmSync(homeDir, { recursive: true, force: true });
2278
- } catch {
2279
- }
2280
- }
2281
- console.log(`OK Phase-G teardown done for ${name} (dscl record /Users/${macOSUser} kept as tombstone)`);
2282
- }
2283
- function buildDestroyTeardownScript(input) {
2284
- const { name, homeDir, adminUser } = input;
2285
- return `#!/bin/bash
2286
- # Best-effort teardown. set -u catches typos; we deliberately do NOT use -e
2287
- # because pkill / launchctl are allowed to fail when the user has no live
2288
- # sessions.
2289
- set -u
2290
-
2291
- NAME=${shQuote(name)}
2292
- HOME_DIR=${shQuote(homeDir)}
2293
- ADMIN_USER=${shQuote(adminUser)}
2294
-
2295
- # Read the admin password from stdin (line 1). The caller pipes it in.
2296
- # We never accept it as an argv element so it can't show up in process
2297
- # listings or escapes' audit log.
2298
- read -r ADMIN_PASSWORD
2299
- if [ -z "$ADMIN_PASSWORD" ]; then
2300
- echo "ERROR: no admin password on stdin (expected one line)." >&2
2301
- exit 2
2302
- fi
2303
-
2304
- UID_OF=$(dscl . -read "/Users/$NAME" UniqueID 2>/dev/null | awk '/UniqueID:/ {print $2}')
2305
-
2306
- if [ -n "$UID_OF" ]; then
2307
- launchctl bootout "user/$UID_OF" 2>/dev/null || true
2308
- pkill -9 -u "$UID_OF" 2>/dev/null || true
2309
- fi
2310
-
2311
- # Per-agent system LaunchDaemon written by spawn (unless --no-bridge). Bootout +
2312
- # delete must come BEFORE we delete the user, otherwise launchd keeps a
2313
- # zombie reference. No-op if the plist isn't there.
2314
- BRIDGE_LABEL="eco.hofmann.apes.bridge.$NAME"
2315
- BRIDGE_PLIST="/Library/LaunchDaemons/$BRIDGE_LABEL.plist"
2316
- if [ -f "$BRIDGE_PLIST" ]; then
2317
- launchctl bootout "system/$BRIDGE_LABEL" 2>/dev/null || true
2318
- rm -f "$BRIDGE_PLIST"
2319
- fi
2320
-
2321
- if [ -d "$HOME_DIR" ] && [ "$HOME_DIR" != "/" ] && [ "$HOME_DIR" != "" ]; then
2322
- rm -rf "$HOME_DIR"
2323
- fi
2324
-
2325
- # \`escapes\` is a plain setuid binary \u2014 opendirectoryd sees no audit/PAM
2326
- # session attached (AUDIT_SESSION_ID=unset) and rejects DirectoryService
2327
- # writes from this context: a bare \`sysadminctl -deleteUser\` or
2328
- # \`dscl . -delete\` hangs ~5 minutes and exits with eUndefinedError -14987
2329
- # at DSRecord.m:563. Passing explicit -adminUser/-adminPassword bypasses
2330
- # opendirectoryd's implicit "is current session admin?" check and
2331
- # authenticates against DirectoryService directly \u2014 the delete then
2332
- # completes in ~1 second.
2333
- if ! command -v sysadminctl >/dev/null 2>&1; then
2334
- echo "ERROR: sysadminctl not available; cannot delete user record." >&2
2335
- exit 1
2336
- fi
2337
-
2338
- sysadminctl \\
2339
- -deleteUser "$NAME" \\
2340
- -adminUser "$ADMIN_USER" \\
2341
- -adminPassword "$ADMIN_PASSWORD"
2342
- SYSAD_EC=$?
2343
- unset ADMIN_PASSWORD
2344
-
2345
- if [ $SYSAD_EC -ne 0 ]; then
2346
- echo "ERROR: sysadminctl -deleteUser failed (exit=$SYSAD_EC)." >&2
2347
- echo " Common causes: wrong admin password, admin user '$ADMIN_USER'" >&2
2348
- echo " not in admin group, or target user '$NAME' is the last secure" >&2
2349
- echo " token holder (run \\\`sysadminctl -secureTokenStatus $NAME\\\`)." >&2
2350
- exit 1
2351
- fi
2352
2161
 
2353
- # Verify the record is actually gone.
2354
- if dscl . -read "/Users/$NAME" >/dev/null 2>&1; then
2355
- echo "ERROR: user record /Users/$NAME still exists after teardown" >&2
2356
- exit 1
2357
- fi
2358
-
2359
- echo "OK destroyed $NAME"
2162
+ echo "OK $NAME (linux user) uid=$NEW_UID home=$HOME_DIR"
2360
2163
  `;
2361
2164
  }
2362
2165
  function shQuote(s) {
@@ -2401,81 +2204,11 @@ print(json.dumps(out))
2401
2204
  '
2402
2205
  `;
2403
2206
 
2404
- // src/lib/macos-user.ts
2405
- import { execFileSync as execFileSync3 } from "child_process";
2406
- import { existsSync as existsSync3, readFileSync as readFileSync2 } from "fs";
2407
- function isDarwin() {
2408
- return process.platform === "darwin";
2409
- }
2410
- var MACOS_USER_PREFIX = "openape-agent-";
2411
- function macOSUsernameForAgent(agentName) {
2412
- return `${MACOS_USER_PREFIX}${agentName}`;
2413
- }
2414
- function lookupMacOSUserForAgent(agentName) {
2415
- return readMacOSUser(macOSUsernameForAgent(agentName)) ?? readMacOSUser(agentName);
2416
- }
2417
- function listOrphanedAgentRecords() {
2418
- if (!isDarwin()) return [];
2419
- let output;
2420
- try {
2421
- output = execFileSync3("dscl", [".", "-list", "/Users", "NFSHomeDirectory"], {
2422
- encoding: "utf-8",
2423
- stdio: ["ignore", "pipe", "pipe"]
2424
- });
2425
- } catch {
2426
- return [];
2427
- }
2428
- const orphans = [];
2429
- for (const line of output.split("\n")) {
2430
- const parts = line.trim().split(/\s+/);
2431
- if (parts.length < 2) continue;
2432
- const name = parts[0];
2433
- const homeDir = parts.slice(1).join(" ");
2434
- const looksLikeAgent = name.startsWith(MACOS_USER_PREFIX) || homeDir.startsWith("/var/openape/homes/");
2435
- if (!looksLikeAgent) continue;
2436
- if (existsSync3(homeDir)) continue;
2437
- const record = readMacOSUser(name);
2438
- orphans.push({ name, uid: record?.uid ?? null, homeDir });
2439
- }
2440
- return orphans;
2441
- }
2442
- function readMacOSUser(name) {
2443
- let output;
2444
- try {
2445
- output = execFileSync3("dscl", [".", "-read", `/Users/${name}`], {
2446
- encoding: "utf-8",
2447
- stdio: ["ignore", "pipe", "pipe"]
2448
- });
2449
- } catch {
2450
- return null;
2451
- }
2452
- const uidMatch = output.match(/UniqueID:\s*(\d+)/);
2453
- const shellMatch = output.match(/UserShell:\s*(\S.*)$/m);
2454
- const homeMatch = output.match(/NFSHomeDirectory:\s*(\S.*)$/m);
2455
- return {
2456
- name,
2457
- uid: uidMatch ? Number.parseInt(uidMatch[1], 10) : null,
2458
- shell: shellMatch ? shellMatch[1].trim() : null,
2459
- homeDir: homeMatch ? homeMatch[1].trim() : null
2460
- };
2461
- }
2462
- function listMacOSUserNames() {
2463
- let output;
2464
- try {
2465
- output = execFileSync3("dscl", [".", "-list", "/Users"], {
2466
- encoding: "utf-8",
2467
- stdio: ["ignore", "pipe", "pipe"]
2468
- });
2469
- } catch {
2470
- return /* @__PURE__ */ new Set();
2471
- }
2472
- return new Set(
2473
- output.split("\n").map((line) => line.trim()).filter((line) => line.length > 0)
2474
- );
2475
- }
2207
+ // src/lib/which.ts
2208
+ import { execFileSync as execFileSync2 } from "child_process";
2476
2209
  function whichBinary(name) {
2477
2210
  try {
2478
- const out = execFileSync3("which", [name], {
2211
+ const out = execFileSync2("which", [name], {
2479
2212
  encoding: "utf-8",
2480
2213
  stdio: ["ignore", "pipe", "ignore"]
2481
2214
  }).trim();
@@ -2484,193 +2217,34 @@ function whichBinary(name) {
2484
2217
  return null;
2485
2218
  }
2486
2219
  }
2487
- function isShellRegistered(shellPath) {
2488
- if (!existsSync3("/etc/shells")) return false;
2489
- const content = readFileSync2("/etc/shells", "utf-8");
2490
- return content.split("\n").map((l) => l.trim()).filter((l) => l && !l.startsWith("#")).includes(shellPath);
2491
- }
2492
2220
 
2493
2221
  // src/lib/host-platform/index.ts
2494
2222
  import process2 from "process";
2495
2223
 
2496
- // src/lib/macos-host.ts
2497
- import { execFileSync as execFileSync4 } from "child_process";
2498
- import { hostname as hostname3 } from "os";
2499
- function getHostId() {
2500
- try {
2501
- const output = execFileSync4(
2502
- "/usr/sbin/ioreg",
2503
- ["-d2", "-c", "IOPlatformExpertDevice"],
2504
- { encoding: "utf8", timeout: 2e3 }
2505
- );
2506
- const match = output.match(/"IOPlatformUUID"\s*=\s*"([^"]+)"/);
2507
- return match ? match[1].trim().toLowerCase() : "";
2508
- } catch {
2509
- return "";
2510
- }
2511
- }
2512
- function getHostname() {
2513
- try {
2514
- return hostname3();
2515
- } catch {
2516
- return "";
2517
- }
2518
- }
2519
-
2520
- // src/lib/host-platform/darwin-nest.ts
2521
- import { execFileSync as execFileSync5 } from "child_process";
2522
- import { existsSync as existsSync4, mkdirSync, readFileSync as readFileSync3, unlinkSync, writeFileSync } from "fs";
2523
- import { userInfo } from "os";
2524
- import { join as join2 } from "path";
2525
- var PLIST_LABEL = "ai.openape.nest";
2526
- function plistPath(userHome) {
2527
- return join2(userHome, "Library", "LaunchAgents", `${PLIST_LABEL}.plist`);
2528
- }
2529
- function xmlEscape(s) {
2530
- return s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
2531
- }
2532
- function buildNestPlist(spec) {
2533
- const logsDir = join2(spec.userHome, "Library", "Logs");
2534
- return `<?xml version="1.0" encoding="UTF-8"?>
2535
- <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
2536
- <plist version="1.0">
2537
- <dict>
2538
- <key>Label</key>
2539
- <string>${xmlEscape(PLIST_LABEL)}</string>
2540
- <key>ProgramArguments</key>
2541
- <array>
2542
- <string>${xmlEscape(spec.nestBin)}</string>
2543
- </array>
2544
- <key>WorkingDirectory</key>
2545
- <string>${xmlEscape(spec.nestHome)}</string>
2546
- <key>RunAtLoad</key>
2547
- <true/>
2548
- <key>KeepAlive</key>
2549
- <true/>
2550
- <key>ThrottleInterval</key>
2551
- <integer>10</integer>
2552
- <key>EnvironmentVariables</key>
2553
- <dict>
2554
- <key>HOME</key><string>${xmlEscape(spec.nestHome)}</string>
2555
- <key>PATH</key><string>/opt/homebrew/bin:/usr/local/bin:/usr/bin:/bin</string>
2556
- <key>OPENAPE_NEST_PORT</key><string>${spec.port}</string>
2557
- <key>OPENAPE_APES_BIN</key><string>${xmlEscape(spec.apesBin)}</string>
2558
- </dict>
2559
- <key>StandardOutPath</key>
2560
- <string>${xmlEscape(logsDir)}/openape-nest.log</string>
2561
- <key>StandardErrorPath</key>
2562
- <string>${xmlEscape(logsDir)}/openape-nest.log</string>
2563
- </dict>
2564
- </plist>
2565
- `;
2566
- }
2567
- async function installNestSupervisorOnDarwin(spec) {
2568
- const path2 = plistPath(spec.userHome);
2569
- mkdirSync(join2(spec.userHome, "Library", "LaunchAgents"), { recursive: true });
2570
- const desired = buildNestPlist(spec);
2571
- let existing = "";
2572
- try {
2573
- existing = readFileSync3(path2, "utf8");
2574
- } catch {
2575
- }
2576
- if (existing !== desired) {
2577
- writeFileSync(path2, desired, { mode: 420 });
2578
- }
2579
- const uid = userInfo().uid;
2580
- try {
2581
- execFileSync5("/bin/launchctl", ["bootout", `gui/${uid}/${PLIST_LABEL}`], { stdio: "ignore" });
2582
- } catch {
2583
- }
2584
- execFileSync5("/bin/launchctl", ["bootstrap", `gui/${uid}`, path2], { stdio: "inherit" });
2585
- }
2586
- async function uninstallNestSupervisorOnDarwin() {
2587
- const home = userInfo().homedir;
2588
- const uid = userInfo().uid;
2589
- const path2 = plistPath(home);
2590
- try {
2591
- execFileSync5("/bin/launchctl", ["bootout", `gui/${uid}/${PLIST_LABEL}`], { stdio: "ignore" });
2592
- } catch {
2593
- }
2594
- if (existsSync4(path2)) unlinkSync(path2);
2595
- }
2596
-
2597
- // src/lib/host-platform/darwin-exec.ts
2598
- import { execFileSync as execFileSync6, spawnSync } from "child_process";
2599
- import { mkdtempSync, rmSync as rmSync2, writeFileSync as writeFileSync2 } from "fs";
2600
- import { tmpdir } from "os";
2601
- import { join as join3 } from "path";
2602
- function resolveApesBinary() {
2603
- return process.env.OPENAPE_APES_BIN || "apes";
2604
- }
2605
- async function runPrivilegedBashOnDarwin(script) {
2606
- const dir = mkdtempSync(join3(tmpdir(), "apes-privileged-"));
2607
- const scriptPath = join3(dir, "run.sh");
2608
- writeFileSync2(scriptPath, script, { mode: 448 });
2609
- try {
2610
- if (process.getuid?.() === 0) {
2611
- execFileSync6("bash", [scriptPath], { stdio: "inherit" });
2612
- } else {
2613
- execFileSync6(resolveApesBinary(), ["run", "--as", "root", "--wait", "--", "bash", scriptPath], { stdio: "inherit" });
2614
- }
2615
- } finally {
2616
- try {
2617
- rmSync2(dir, { recursive: true, force: true });
2618
- } catch {
2619
- }
2620
- }
2621
- }
2622
- async function runAsAgentUserOnDarwin(agentName, argv) {
2623
- const r = spawnSync(
2624
- resolveApesBinary(),
2625
- ["run", "--as", agentName, "--wait", "--", ...argv],
2626
- { encoding: "utf8" }
2627
- );
2628
- return {
2629
- stdout: r.stdout ?? "",
2630
- stderr: r.stderr ?? "",
2631
- exitCode: r.status ?? 1
2632
- };
2633
- }
2634
-
2635
- // src/lib/host-platform/darwin.ts
2636
- var darwinHostPlatform = {
2637
- getHostId,
2638
- getHostname,
2639
- agentUsername: macOSUsernameForAgent,
2640
- lookupAgentUser: (agentName) => lookupMacOSUserForAgent(agentName),
2641
- readAgentUser: (osName) => readMacOSUser(osName),
2642
- listAgentUserNames: listMacOSUserNames,
2643
- listOrphanAgentUsers: () => listOrphanedAgentRecords().map((r) => ({ name: r.name, uid: r.uid, homeDir: r.homeDir })),
2644
- installNestSupervisor: installNestSupervisorOnDarwin,
2645
- uninstallNestSupervisor: uninstallNestSupervisorOnDarwin,
2646
- runPrivilegedBash: runPrivilegedBashOnDarwin,
2647
- runAsAgentUser: runAsAgentUserOnDarwin
2648
- };
2649
-
2650
2224
  // src/lib/host-platform/linux-host.ts
2651
- import { hostname as hostname4 } from "os";
2652
- import { existsSync as existsSync5, readFileSync as readFileSync4 } from "fs";
2225
+ import { hostname as hostname3 } from "os";
2226
+ import { existsSync as existsSync3, readFileSync as readFileSync2 } from "fs";
2653
2227
  var FALLBACK_PATHS = ["/etc/machine-id", "/var/lib/dbus/machine-id"];
2654
2228
  function getLinuxHostId() {
2655
2229
  for (const path2 of FALLBACK_PATHS) {
2656
- if (!existsSync5(path2)) continue;
2230
+ if (!existsSync3(path2)) continue;
2657
2231
  try {
2658
- const v = readFileSync4(path2, "utf-8").trim();
2232
+ const v = readFileSync2(path2, "utf-8").trim();
2659
2233
  if (v) return v;
2660
2234
  } catch {
2661
2235
  }
2662
2236
  }
2663
- return hostname4();
2237
+ return hostname3();
2664
2238
  }
2665
2239
  function getLinuxHostname() {
2666
- return hostname4();
2240
+ return hostname3();
2667
2241
  }
2668
2242
 
2669
2243
  // src/lib/host-platform/linux-user.ts
2670
- import { execFileSync as execFileSync7 } from "child_process";
2244
+ import { execFileSync as execFileSync3 } from "child_process";
2671
2245
  function getentPasswd(name) {
2672
2246
  try {
2673
- return execFileSync7("getent", ["passwd", name], {
2247
+ return execFileSync3("getent", ["passwd", name], {
2674
2248
  encoding: "utf-8",
2675
2249
  stdio: ["ignore", "pipe", "ignore"]
2676
2250
  }).trim() || null;
@@ -2698,7 +2272,7 @@ function readLinuxUser(name) {
2698
2272
  }
2699
2273
  function listLinuxUserNames() {
2700
2274
  try {
2701
- const out = execFileSync7("getent", ["passwd"], {
2275
+ const out = execFileSync3("getent", ["passwd"], {
2702
2276
  encoding: "utf-8",
2703
2277
  stdio: ["ignore", "pipe", "ignore"]
2704
2278
  });
@@ -2714,29 +2288,29 @@ function listLinuxUserNames() {
2714
2288
  }
2715
2289
 
2716
2290
  // src/lib/host-platform/linux-exec.ts
2717
- import { execFileSync as execFileSync8, spawnSync as spawnSync2 } from "child_process";
2718
- import { mkdtempSync as mkdtempSync2, rmSync as rmSync3, writeFileSync as writeFileSync3 } from "fs";
2719
- import { tmpdir as tmpdir2 } from "os";
2720
- import { join as join4 } from "path";
2291
+ import { execFileSync as execFileSync4, spawnSync } from "child_process";
2292
+ import { mkdtempSync, rmSync, writeFileSync } from "fs";
2293
+ import { tmpdir } from "os";
2294
+ import { join as join2 } from "path";
2721
2295
  async function runPrivilegedBashOnLinux(script) {
2722
- const dir = mkdtempSync2(join4(tmpdir2(), "apes-privileged-"));
2723
- const scriptPath = join4(dir, "run.sh");
2724
- writeFileSync3(scriptPath, script, { mode: 448 });
2296
+ const dir = mkdtempSync(join2(tmpdir(), "apes-privileged-"));
2297
+ const scriptPath = join2(dir, "run.sh");
2298
+ writeFileSync(scriptPath, script, { mode: 448 });
2725
2299
  try {
2726
2300
  if (process.getuid?.() === 0) {
2727
- execFileSync8("bash", [scriptPath], { stdio: "inherit" });
2301
+ execFileSync4("bash", [scriptPath], { stdio: "inherit" });
2728
2302
  } else {
2729
- execFileSync8("sudo", ["-n", "--", "bash", scriptPath], { stdio: "inherit" });
2303
+ execFileSync4("sudo", ["-n", "--", "bash", scriptPath], { stdio: "inherit" });
2730
2304
  }
2731
2305
  } finally {
2732
2306
  try {
2733
- rmSync3(dir, { recursive: true, force: true });
2307
+ rmSync(dir, { recursive: true, force: true });
2734
2308
  } catch {
2735
2309
  }
2736
2310
  }
2737
2311
  }
2738
2312
  async function runAsAgentUserOnLinux(agentName, argv) {
2739
- const r = spawnSync2("sudo", ["-n", "-H", "-u", agentName, "--", ...argv], { encoding: "utf8" });
2313
+ const r = spawnSync("sudo", ["-n", "-H", "-u", agentName, "--", ...argv], { encoding: "utf8" });
2740
2314
  return {
2741
2315
  stdout: r.stdout ?? "",
2742
2316
  stderr: r.stderr ?? "",
@@ -2745,8 +2319,8 @@ async function runAsAgentUserOnLinux(agentName, argv) {
2745
2319
  }
2746
2320
 
2747
2321
  // src/lib/host-platform/linux-nest.ts
2748
- import { execFileSync as execFileSync9 } from "child_process";
2749
- import { existsSync as existsSync6, readFileSync as readFileSync5, unlinkSync as unlinkSync2, writeFileSync as writeFileSync4 } from "fs";
2322
+ import { execFileSync as execFileSync5 } from "child_process";
2323
+ import { existsSync as existsSync4, readFileSync as readFileSync3, unlinkSync, writeFileSync as writeFileSync2 } from "fs";
2750
2324
  var UNIT_NAME = "openape-nest.service";
2751
2325
  var UNIT_PATH = `/etc/systemd/system/${UNIT_NAME}`;
2752
2326
  function buildNestUnit(spec) {
@@ -2776,23 +2350,23 @@ async function installNestSupervisorOnLinux(spec) {
2776
2350
  const desired = buildNestUnit(spec);
2777
2351
  let existing = "";
2778
2352
  try {
2779
- existing = readFileSync5(UNIT_PATH, "utf8");
2353
+ existing = readFileSync3(UNIT_PATH, "utf8");
2780
2354
  } catch {
2781
2355
  }
2782
2356
  if (existing !== desired) {
2783
- writeFileSync4(UNIT_PATH, desired, { mode: 420 });
2357
+ writeFileSync2(UNIT_PATH, desired, { mode: 420 });
2784
2358
  }
2785
- execFileSync9("systemctl", ["daemon-reload"], { stdio: "inherit" });
2786
- execFileSync9("systemctl", ["enable", "--now", UNIT_NAME], { stdio: "inherit" });
2359
+ execFileSync5("systemctl", ["daemon-reload"], { stdio: "inherit" });
2360
+ execFileSync5("systemctl", ["enable", "--now", UNIT_NAME], { stdio: "inherit" });
2787
2361
  }
2788
2362
  async function uninstallNestSupervisorOnLinux() {
2789
2363
  try {
2790
- execFileSync9("systemctl", ["disable", "--now", UNIT_NAME], { stdio: "inherit" });
2364
+ execFileSync5("systemctl", ["disable", "--now", UNIT_NAME], { stdio: "inherit" });
2791
2365
  } catch {
2792
2366
  }
2793
- if (existsSync6(UNIT_PATH)) unlinkSync2(UNIT_PATH);
2367
+ if (existsSync4(UNIT_PATH)) unlinkSync(UNIT_PATH);
2794
2368
  try {
2795
- execFileSync9("systemctl", ["daemon-reload"], { stdio: "inherit" });
2369
+ execFileSync5("systemctl", ["daemon-reload"], { stdio: "inherit" });
2796
2370
  } catch {
2797
2371
  }
2798
2372
  }
@@ -2818,18 +2392,14 @@ var linuxHostPlatform = {
2818
2392
  };
2819
2393
 
2820
2394
  // src/lib/host-platform/index.ts
2821
- function isDarwin2() {
2822
- return process2.platform === "darwin";
2823
- }
2824
2395
  function isLinux() {
2825
2396
  return process2.platform === "linux";
2826
2397
  }
2827
2398
  var testOverride = null;
2828
2399
  function getHostPlatform() {
2829
2400
  if (testOverride) return testOverride;
2830
- if (isDarwin2()) return darwinHostPlatform;
2831
2401
  if (isLinux()) return linuxHostPlatform;
2832
- throw new Error(`unsupported host platform: ${process2.platform}`);
2402
+ throw new Error(`unsupported host platform: ${process2.platform} \u2014 OpenApe nests are Linux-only`);
2833
2403
  }
2834
2404
 
2835
2405
  // src/commands/agents/allow.ts
@@ -2842,7 +2412,7 @@ var allowAgentCommand = defineCommand22({
2842
2412
  agent: {
2843
2413
  type: "positional",
2844
2414
  required: true,
2845
- description: "Agent name (the macOS short username spawn created)"
2415
+ description: "Agent name (the Linux username spawn created)"
2846
2416
  },
2847
2417
  email: {
2848
2418
  type: "positional",
@@ -2859,11 +2429,8 @@ var allowAgentCommand = defineCommand22({
2859
2429
  if (!email.includes("@")) {
2860
2430
  throw new CliError(`Invalid email "${email}".`);
2861
2431
  }
2862
- if (!isDarwin2()) {
2863
- throw new CliError("`apes agents allow` is currently macOS-only.");
2864
- }
2865
2432
  if (!getHostPlatform().lookupAgentUser(agent)) {
2866
- throw new CliError(`No macOS user for agent "${agent}" \u2014 has it been spawned?`);
2433
+ throw new CliError(`No OS user for agent "${agent}" \u2014 has it been spawned?`);
2867
2434
  }
2868
2435
  const apes = whichBinary("apes");
2869
2436
  if (!apes) throw new CliError("`apes` not found on PATH.");
@@ -2894,7 +2461,7 @@ PY
2894
2461
  chmod 600 "$F"
2895
2462
  `;
2896
2463
  consola19.start(`Adding ${email} to ${agent}'s allowlist\u2026`);
2897
- execFileSync10(apes, ["run", "--as", agent, "--wait", "--", "bash", "-c", script], { stdio: "inherit" });
2464
+ execFileSync6(apes, ["run", "--as", agent, "--wait", "--", "bash", "-c", script], { stdio: "inherit" });
2898
2465
  consola19.success(`${agent} will auto-accept future contact requests from ${email} (within ~30s of next bridge connect).`);
2899
2466
  }
2900
2467
  });
@@ -2903,13 +2470,12 @@ function shQuote2(s) {
2903
2470
  }
2904
2471
 
2905
2472
  // src/commands/agents/cleanup-orphans.ts
2906
- import { execFileSync as execFileSync11 } from "child_process";
2907
2473
  import { defineCommand as defineCommand23 } from "citty";
2908
2474
  import consola20 from "consola";
2909
2475
  var cleanupOrphansCommand = defineCommand23({
2910
2476
  meta: {
2911
2477
  name: "cleanup-orphans",
2912
- description: "Delete tombstoned macOS user records left behind by `apes agents destroy` (run with sudo)."
2478
+ description: "Report agent-user tombstones. Linux userdel is atomic, so there are normally none."
2913
2479
  },
2914
2480
  args: {
2915
2481
  "dry-run": {
@@ -2921,81 +2487,37 @@ var cleanupOrphansCommand = defineCommand23({
2921
2487
  description: "Skip the interactive confirmation. Required when stdin is not a TTY."
2922
2488
  }
2923
2489
  },
2924
- async run({ args }) {
2925
- if (!isDarwin2()) {
2926
- throw new CliError(`\`apes agents cleanup-orphans\` is macOS-only. Detected platform: ${process.platform}.`);
2927
- }
2490
+ async run() {
2928
2491
  const orphans = getHostPlatform().listOrphanAgentUsers();
2929
2492
  if (orphans.length === 0) {
2930
- consola20.success("No agent tombstones found \u2014 dscl is clean.");
2493
+ consola20.success("No agent tombstones \u2014 userdel is clean on Linux.");
2931
2494
  return;
2932
2495
  }
2933
- consola20.info(`Found ${orphans.length} agent tombstone${orphans.length === 1 ? "" : "s"}:`);
2496
+ consola20.warn(`Found ${orphans.length} unexpected agent tombstone${orphans.length === 1 ? "" : "s"}:`);
2934
2497
  for (const o of orphans) {
2935
2498
  console.log(` \u2022 ${o.name}${o.uid !== null ? ` (uid=${o.uid})` : ""} \u2014 was ${o.homeDir}`);
2936
2499
  }
2937
- if (args["dry-run"]) {
2938
- consola20.info("Dry-run \u2014 no records deleted. Re-run without --dry-run to clean up.");
2939
- return;
2940
- }
2941
- if (process.geteuid?.() !== 0) {
2942
- throw new CliError(
2943
- "Must run as root so opendirectoryd accepts the sysadminctl-deleteUser calls. Re-run with `sudo apes agents cleanup-orphans` from a shell login (the sudo session inherits your admin audit-session, which opendirectoryd verifies)."
2944
- );
2945
- }
2946
- if (!args.force) {
2947
- if (!process.stdin.isTTY) {
2948
- throw new CliError(
2949
- "No TTY available for the interactive confirmation. Re-run with --force (this is the same flag CI / scripted callers use)."
2950
- );
2951
- }
2952
- const confirmed = await consola20.prompt(`Delete ${orphans.length} tombstone${orphans.length === 1 ? "" : "s"}?`, {
2953
- type: "confirm",
2954
- initial: false
2955
- });
2956
- if (typeof confirmed === "symbol" || !confirmed) {
2957
- consola20.info("Aborted \u2014 no records deleted.");
2958
- return;
2959
- }
2960
- }
2961
- let deleted = 0;
2962
- let failed = 0;
2963
- for (const o of orphans) {
2964
- try {
2965
- execFileSync11("/usr/sbin/sysadminctl", ["-deleteUser", o.name], {
2966
- stdio: ["ignore", "inherit", "inherit"]
2967
- });
2968
- consola20.success(`Deleted ${o.name}`);
2969
- deleted++;
2970
- } catch (err) {
2971
- consola20.warn(`Failed to delete ${o.name}: ${err instanceof Error ? err.message : String(err)}`);
2972
- failed++;
2973
- }
2974
- }
2975
- if (failed > 0) {
2976
- throw new CliError(`Cleanup finished with errors: ${deleted} deleted, ${failed} failed.`);
2977
- }
2978
- consola20.success(`Cleanup complete \u2014 ${deleted} tombstone${deleted === 1 ? "" : "s"} removed.`);
2500
+ consola20.info("Remove each one manually with `userdel -r <name>`.");
2979
2501
  }
2980
2502
  });
2981
2503
 
2982
2504
  // src/commands/agents/code.ts
2983
- import { existsSync as existsSync8, readFileSync as readFileSync7 } from "fs";
2505
+ import { existsSync as existsSync6, readFileSync as readFileSync5 } from "fs";
2984
2506
  import { homedir as homedir5 } from "os";
2985
- import { join as join6 } from "path";
2507
+ import { join as join4 } from "path";
2986
2508
  import process3 from "process";
2987
2509
  import { defineCommand as defineCommand24 } from "citty";
2988
2510
  import { consola as consola21 } from "consola";
2989
2511
  import { taskTools, runApeShell, runCodingTask, buildIssueGet, detectForge, createLlmReviewer, createLlmRiskAssessor, resolveMergePolicy } from "@openape/agent-runtime";
2990
2512
 
2991
2513
  // src/lib/agent-secrets-runtime.ts
2992
- import { existsSync as existsSync7, readdirSync, readFileSync as readFileSync6, watch } from "fs";
2514
+ import { existsSync as existsSync5, mkdirSync, readdirSync, readFileSync as readFileSync4, statSync, watch, writeFileSync as writeFileSync3 } from "fs";
2993
2515
  import { homedir as homedir4 } from "os";
2994
- import { join as join5 } from "path";
2516
+ import { dirname, join as join3 } from "path";
2995
2517
  import { openString } from "@openape/core";
2996
- var CONFIG_DIR2 = join5(homedir4(), ".config", "openape");
2997
- var SECRETS_DIR = join5(CONFIG_DIR2, "secrets.d");
2998
- var X25519_KEY_PATH = join5(CONFIG_DIR2, "agent-x25519.key");
2518
+ var CONFIG_DIR2 = join3(homedir4(), ".config", "openape");
2519
+ var SECRETS_DIR = join3(CONFIG_DIR2, "secrets.d");
2520
+ var X25519_KEY_PATH = join3(CONFIG_DIR2, "agent-x25519.key");
2999
2521
  var X25519_PUBKEY_PATH = `${X25519_KEY_PATH}.pub`;
3000
2522
  function envNameFromFile(file) {
3001
2523
  if (!file.endsWith(".blob")) return null;
@@ -3003,13 +2525,13 @@ function envNameFromFile(file) {
3003
2525
  return /^[A-Z][A-Z0-9_]*$/.test(env) ? env : null;
3004
2526
  }
3005
2527
  function readAgentEncryptionKey(keyPath = X25519_KEY_PATH) {
3006
- if (!existsSync7(keyPath)) return null;
3007
- const k = readFileSync6(keyPath, "utf8").trim();
2528
+ if (!existsSync5(keyPath)) return null;
2529
+ const k = readFileSync4(keyPath, "utf8").trim();
3008
2530
  return k.length > 0 ? k : null;
3009
2531
  }
3010
2532
  function readAgentEncryptionPublicKey(pubPath = X25519_PUBKEY_PATH) {
3011
- if (!existsSync7(pubPath)) return null;
3012
- const k = readFileSync6(pubPath, "utf8").trim();
2533
+ if (!existsSync5(pubPath)) return null;
2534
+ const k = readFileSync4(pubPath, "utf8").trim();
3013
2535
  return k.length > 0 ? k : null;
3014
2536
  }
3015
2537
  function materializeSecrets(opts = {}) {
@@ -3020,13 +2542,23 @@ function materializeSecrets(opts = {}) {
3020
2542
  const applied = [];
3021
2543
  const failed = [];
3022
2544
  const key = readAgentEncryptionKey(opts.keyPath);
3023
- const files = key && existsSync7(dir) ? readdirSync(dir) : [];
2545
+ const files = key && existsSync5(dir) ? readdirSync(dir) : [];
3024
2546
  for (const file of files) {
3025
2547
  const name = envNameFromFile(file);
3026
2548
  if (!name) continue;
3027
2549
  try {
3028
- const box = JSON.parse(readFileSync6(join5(dir, file), "utf8"));
3029
- env[name] = openString(box, key);
2550
+ const box = JSON.parse(readFileSync4(join3(dir, file), "utf8"));
2551
+ const plaintext = openString(box, key);
2552
+ const target = typeof box.materializeTo === "string" ? box.materializeTo : null;
2553
+ if (target) {
2554
+ const blobMtime = statSync(join3(dir, file)).mtimeMs;
2555
+ if (!existsSync5(target) || statSync(target).mtimeMs < blobMtime) {
2556
+ mkdirSync(dirname(target), { recursive: true });
2557
+ writeFileSync3(target, plaintext, { mode: 384 });
2558
+ }
2559
+ } else {
2560
+ env[name] = plaintext;
2561
+ }
3030
2562
  applied.push(name);
3031
2563
  } catch (e) {
3032
2564
  failed.push(file);
@@ -3052,7 +2584,7 @@ function startSecretsWatcher(opts = {}) {
3052
2584
  appliedNames = new Set(r.applied);
3053
2585
  };
3054
2586
  run();
3055
- if (!existsSync7(dir)) return () => {
2587
+ if (!existsSync5(dir)) return () => {
3056
2588
  };
3057
2589
  let timer = null;
3058
2590
  const watcher = watch(dir, () => {
@@ -3079,9 +2611,9 @@ var DEFAULT_PERSONA = [
3079
2611
  ].join(" ");
3080
2612
  function readLitellmConfig(model) {
3081
2613
  const env = {};
3082
- const envPath = join6(homedir5(), "litellm", ".env");
3083
- if (existsSync8(envPath)) {
3084
- for (const raw of readFileSync7(envPath, "utf8").split("\n")) {
2614
+ const envPath = join4(homedir5(), "litellm", ".env");
2615
+ if (existsSync6(envPath)) {
2616
+ for (const raw of readFileSync5(envPath, "utf8").split("\n")) {
3085
2617
  const line = raw.trim();
3086
2618
  const m = /^([A-Z_][A-Z0-9_]*)=(.*)$/.exec(line);
3087
2619
  if (m) env[m[1]] = m[2].trim().replace(/^["']|["']$/g, "");
@@ -3097,11 +2629,11 @@ function readLitellmConfig(model) {
3097
2629
  return { apiBase, apiKey, model: model || process3.env.APE_CHAT_BRIDGE_MODEL || "claude-haiku-4-5" };
3098
2630
  }
3099
2631
  function readPersona(file) {
3100
- if (file && existsSync8(file)) return readFileSync7(file, "utf8");
3101
- const agentJson = join6(homedir5(), ".openape", "agent", "agent.json");
3102
- if (existsSync8(agentJson)) {
2632
+ if (file && existsSync6(file)) return readFileSync5(file, "utf8");
2633
+ const agentJson = join4(homedir5(), ".openape", "agent", "agent.json");
2634
+ if (existsSync6(agentJson)) {
3103
2635
  try {
3104
- const p = JSON.parse(readFileSync7(agentJson, "utf8"));
2636
+ const p = JSON.parse(readFileSync5(agentJson, "utf8"));
3105
2637
  if (p.systemPrompt?.trim()) return p.systemPrompt;
3106
2638
  } catch {
3107
2639
  }
@@ -3186,30 +2718,27 @@ ${result.reason}`);
3186
2718
  });
3187
2719
 
3188
2720
  // src/commands/agents/destroy.ts
3189
- import { execFileSync as execFileSync12 } from "child_process";
3190
- import { mkdtempSync as mkdtempSync3, rmSync as rmSync4, writeFileSync as writeFileSync6 } from "fs";
3191
- import { tmpdir as tmpdir3, userInfo as userInfo2 } from "os";
3192
- import { join as join8 } from "path";
3193
2721
  import { defineCommand as defineCommand25 } from "citty";
3194
2722
  import consola22 from "consola";
3195
2723
 
3196
2724
  // src/lib/nest-registry.ts
3197
- import { existsSync as existsSync9, mkdirSync as mkdirSync2, readFileSync as readFileSync8, writeFileSync as writeFileSync5 } from "fs";
2725
+ import { existsSync as existsSync7, mkdirSync as mkdirSync2, readFileSync as readFileSync6, writeFileSync as writeFileSync4 } from "fs";
3198
2726
  import { homedir as homedir6 } from "os";
3199
- import { join as join7 } from "path";
2727
+ import { join as join5 } from "path";
3200
2728
  function resolveRegistryPath() {
3201
- if (existsSync9("/var/openape/nest/agents.json")) return "/var/openape/nest/agents.json";
3202
- if (existsSync9("/var/openape/nest")) return "/var/openape/nest/agents.json";
3203
- return join7(homedir6(), ".openape", "nest", "agents.json");
2729
+ if (process.env.OPENAPE_NEST_REGISTRY_PATH) return process.env.OPENAPE_NEST_REGISTRY_PATH;
2730
+ if (existsSync7("/var/openape/nest/agents.json")) return "/var/openape/nest/agents.json";
2731
+ if (existsSync7("/var/openape/nest")) return "/var/openape/nest/agents.json";
2732
+ return join5(homedir6(), ".openape", "nest", "agents.json");
3204
2733
  }
3205
2734
  function emptyRegistry() {
3206
2735
  return { version: 1, agents: [] };
3207
2736
  }
3208
2737
  function readNestRegistry() {
3209
2738
  const path2 = resolveRegistryPath();
3210
- if (!existsSync9(path2)) return emptyRegistry();
2739
+ if (!existsSync7(path2)) return emptyRegistry();
3211
2740
  try {
3212
- const parsed = JSON.parse(readFileSync8(path2, "utf8"));
2741
+ const parsed = JSON.parse(readFileSync6(path2, "utf8"));
3213
2742
  if (parsed?.version !== 1 || !Array.isArray(parsed.agents)) return emptyRegistry();
3214
2743
  return parsed;
3215
2744
  } catch {
@@ -3223,7 +2752,7 @@ function writeNestRegistry(reg) {
3223
2752
  mkdirSync2(dir, { recursive: true });
3224
2753
  } catch {
3225
2754
  }
3226
- writeFileSync5(path2, `${JSON.stringify(reg, null, 2)}
2755
+ writeFileSync4(path2, `${JSON.stringify(reg, null, 2)}
3227
2756
  `, { mode: 432 });
3228
2757
  }
3229
2758
  function upsertNestAgent(entry) {
@@ -3242,65 +2771,11 @@ function removeNestAgent(name) {
3242
2771
  return true;
3243
2772
  }
3244
2773
 
3245
- // src/lib/silent-password.ts
3246
- function readPasswordSilent(prompt) {
3247
- if (!process.stdin.isTTY) {
3248
- return Promise.reject(new CliError(
3249
- "No TTY available for the silent password prompt. Set APES_ADMIN_PASSWORD in the environment instead."
3250
- ));
3251
- }
3252
- return new Promise((resolve4, reject) => {
3253
- process.stdout.write(prompt);
3254
- const wasRaw = process.stdin.isRaw ?? false;
3255
- process.stdin.setRawMode(true);
3256
- process.stdin.resume();
3257
- process.stdin.setEncoding("utf8");
3258
- let buf = "";
3259
- let cleanupFn;
3260
- const cleanup = () => cleanupFn?.();
3261
- const onData = (chunk) => {
3262
- for (const ch of chunk) {
3263
- const code = ch.charCodeAt(0);
3264
- if (ch === "\r" || ch === "\n") {
3265
- cleanup();
3266
- process.stdout.write("\n");
3267
- resolve4(buf);
3268
- return;
3269
- }
3270
- if (code === 3) {
3271
- cleanup();
3272
- process.stdout.write("\n");
3273
- reject(new CliError("Aborted by user (Ctrl-C)."));
3274
- return;
3275
- }
3276
- if (code === 4 && buf.length === 0) {
3277
- cleanup();
3278
- process.stdout.write("\n");
3279
- reject(new CliError("Aborted by user (Ctrl-D)."));
3280
- return;
3281
- }
3282
- if (code === 127 || code === 8) {
3283
- if (buf.length > 0) buf = buf.slice(0, -1);
3284
- continue;
3285
- }
3286
- if (code < 32) continue;
3287
- buf += ch;
3288
- }
3289
- };
3290
- cleanupFn = () => {
3291
- process.stdin.removeListener("data", onData);
3292
- process.stdin.setRawMode(wasRaw);
3293
- process.stdin.pause();
3294
- };
3295
- process.stdin.on("data", onData);
3296
- });
3297
- }
3298
-
3299
2774
  // src/commands/agents/destroy.ts
3300
2775
  var destroyAgentCommand = defineCommand25({
3301
2776
  meta: {
3302
2777
  name: "destroy",
3303
- description: "Tear down an agent: remove macOS user, hard-delete IdP agent, drop all SSH keys"
2778
+ description: "Tear down an agent: remove the OS user, hard-delete IdP agent, drop all SSH keys"
3304
2779
  },
3305
2780
  args: {
3306
2781
  name: {
@@ -3319,10 +2794,6 @@ var destroyAgentCommand = defineCommand25({
3319
2794
  "keep-os-user": {
3320
2795
  type: "boolean",
3321
2796
  description: "Skip OS-side teardown. Useful for CI where the agent has no OS user."
3322
- },
3323
- "root-stage": {
3324
- type: "boolean",
3325
- description: "Internal \u2014 destroy.ts re-invokes itself via `apes run --as root --` with this flag set, then runs only the Phase-G teardown (rm home, launchctl bootout, kill processes). Skips IdP + auth + interactive prompts since those already ran in the outer pass."
3326
2797
  }
3327
2798
  },
3328
2799
  async run({ args }) {
@@ -3332,18 +2803,6 @@ var destroyAgentCommand = defineCommand25({
3332
2803
  `Invalid agent name "${name}". Must match /^[a-z][a-z0-9-]{0,23}$/.`
3333
2804
  );
3334
2805
  }
3335
- if (args["root-stage"]) {
3336
- if (process.geteuid?.() !== 0) {
3337
- throw new CliError("--root-stage was passed but this process is not running as root. Refusing to continue.");
3338
- }
3339
- const platform = getHostPlatform();
3340
- const resolved = platform.lookupAgentUser(name);
3341
- const macOSUsername = resolved?.name ?? platform.agentUsername(name);
3342
- const homeDir = resolved?.homeDir ?? `/var/openape/homes/${macOSUsername}`;
3343
- consola22.start(`Running teardown for ${name} (Phase-G, root-stage)\u2026`);
3344
- runPhaseGTeardownInProcess({ name, homeDir, macOSUsername });
3345
- return;
3346
- }
3347
2806
  const auth = loadAuth();
3348
2807
  if (!auth) {
3349
2808
  throw new CliError("Not authenticated. Run `apes login` first.");
@@ -3355,7 +2814,7 @@ var destroyAgentCommand = defineCommand25({
3355
2814
  const owned = await apiFetch("/api/my-agents", { idp });
3356
2815
  const idpAgent = owned.find((u) => u.name === name);
3357
2816
  const idpExists = idpAgent !== void 0;
3358
- const osUser = isDarwin2() ? getHostPlatform().lookupAgentUser(name) : null;
2817
+ const osUser = getHostPlatform().lookupAgentUser(name);
3359
2818
  const osUserExists = !args["keep-os-user"] && osUser !== null;
3360
2819
  if (!idpExists && !osUserExists) {
3361
2820
  consola22.info(`Nothing to destroy: no IdP agent and no OS user for "${name}".`);
@@ -3364,8 +2823,8 @@ var destroyAgentCommand = defineCommand25({
3364
2823
  if (!args.force) {
3365
2824
  const consequences = [];
3366
2825
  if (osUserExists) {
3367
- const home = osUser?.homeDir ?? `/Users/${name}`;
3368
- consequences.push(`\u2022 Remove macOS user ${osUser?.name ?? name} and rm -rf ${home}`);
2826
+ const home = osUser?.homeDir ?? `/home/${name}`;
2827
+ consequences.push(`\u2022 Remove OS user ${osUser?.name ?? name} and rm -rf ${home}`);
3369
2828
  }
3370
2829
  if (idpExists) {
3371
2830
  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`);
@@ -3395,69 +2854,17 @@ ${consequences.join("\n")}`);
3395
2854
  consola22.info("No IdP agent to remove (skipped).");
3396
2855
  }
3397
2856
  if (osUserExists) {
3398
- const macOSUsername = osUser?.name ?? getHostPlatform().agentUsername(name);
3399
- const fallbackHome = macOSUsername.startsWith("openape-agent-") ? `/var/openape/homes/${macOSUsername}` : `/Users/${macOSUsername}`;
3400
- const homeDir = osUser?.homeDir ?? fallbackHome;
3401
- const isPhaseG = homeDir.startsWith("/var/openape/homes/");
3402
- if (isPhaseG) {
3403
- if (process.geteuid?.() === 0) {
3404
- consola22.start("Running teardown (Phase G \u2014 already root, no grant needed)\u2026");
3405
- runPhaseGTeardownInProcess({ name, homeDir, macOSUsername });
3406
- } else {
3407
- consola22.start("Running teardown (Phase G \u2014 no admin password needed)\u2026");
3408
- execFileSync12("apes", [
3409
- "run",
3410
- "--as",
3411
- "root",
3412
- "--wait",
3413
- "--",
3414
- "apes",
3415
- "agents",
3416
- "destroy",
3417
- name,
3418
- "--force",
3419
- "--root-stage"
3420
- ], { stdio: "inherit" });
3421
- }
3422
- consola22.info(`dscl record /Users/${macOSUsername} kept as tombstone (hidden, no home). Run \`sudo apes agents cleanup-orphans\` to sweep accumulated tombstones.`);
3423
- } else {
3424
- const sudo = whichBinary("sudo");
3425
- if (!sudo) {
3426
- throw new CliError("`sudo` not found on PATH; required for OS teardown.");
3427
- }
3428
- const adminUser = userInfo2().username;
3429
- let adminPassword;
3430
- try {
3431
- adminPassword = await collectAdminPassword({ adminUser });
3432
- } catch (err) {
3433
- const headless = !process.stdin.isTTY && !process.env.APES_ADMIN_PASSWORD;
3434
- if (headless) {
3435
- consola22.warn(`Legacy OS teardown for ${name} requires a TTY or APES_ADMIN_PASSWORD; skipping. Run \`apes agents destroy ${name}\` from a shell later to fully clean up /Users/${name} + dscl record.`);
3436
- adminPassword = "";
3437
- } else {
3438
- throw err;
3439
- }
3440
- }
3441
- if (adminPassword) {
3442
- const scratch = mkdtempSync3(join8(tmpdir3(), `apes-destroy-${name}-`));
3443
- const scriptPath = join8(scratch, "teardown.sh");
3444
- try {
3445
- const script = buildDestroyTeardownScript({ name, homeDir, adminUser });
3446
- writeFileSync6(scriptPath, script, { mode: 448 });
3447
- consola22.start("Running teardown via sudo\u2026");
3448
- execFileSync12(sudo, ["-S", "--prompt=", "--", "bash", scriptPath], {
3449
- input: `${adminPassword}
3450
- ${adminPassword}
3451
- `,
3452
- stdio: ["pipe", "inherit", "inherit"]
3453
- });
3454
- } finally {
3455
- rmSync4(scratch, { recursive: true, force: true });
3456
- }
3457
- }
3458
- }
3459
- } else if (!args["keep-os-user"] && isDarwin2()) {
3460
- consola22.info("No macOS user to remove (skipped).");
2857
+ consola22.start(`Removing OS user ${name}\u2026`);
2858
+ await getHostPlatform().runPrivilegedBash(
2859
+ `#!/bin/bash
2860
+ set -euo pipefail
2861
+ if getent passwd ${JSON.stringify(name)} >/dev/null 2>&1; then
2862
+ pkill -9 -u ${JSON.stringify(name)} 2>/dev/null || true
2863
+ userdel -r ${JSON.stringify(name)}
2864
+ fi
2865
+ `
2866
+ );
2867
+ consola22.success(`Removed OS user ${name}.`);
3461
2868
  }
3462
2869
  try {
3463
2870
  removeNestAgent(name);
@@ -3467,15 +2874,6 @@ ${adminPassword}
3467
2874
  consola22.success(`Destroyed ${name}.`);
3468
2875
  }
3469
2876
  });
3470
- async function collectAdminPassword(opts) {
3471
- const fromEnv = process.env.APES_ADMIN_PASSWORD;
3472
- if (fromEnv && fromEnv.length > 0) return fromEnv;
3473
- const pw = await readPasswordSilent(`Password for ${opts.adminUser}: `);
3474
- if (pw.length === 0) {
3475
- throw new CliExit(0);
3476
- }
3477
- return pw;
3478
- }
3479
2877
 
3480
2878
  // src/commands/agents/list.ts
3481
2879
  import { defineCommand as defineCommand26 } from "citty";
@@ -3507,7 +2905,7 @@ var listAgentsCommand = defineCommand26({
3507
2905
  const all = await apiFetch("/api/my-agents", { idp });
3508
2906
  const filtered = args["include-inactive"] ? all : all.filter((u) => u.isActive !== false);
3509
2907
  const platform = getHostPlatform();
3510
- const osUsers = isDarwin2() ? platform.listAgentUserNames() : /* @__PURE__ */ new Set();
2908
+ const osUsers = platform.listAgentUserNames();
3511
2909
  const osStateOf = (agentName) => {
3512
2910
  const u = platform.lookupAgentUser(agentName);
3513
2911
  if (u) return { osUser: true, home: u.homeDir };
@@ -3543,14 +2941,14 @@ var listAgentsCommand = defineCommand26({
3543
2941
  for (const r of rows) {
3544
2942
  const active = r.isActive ? "\u2713" : "\u2717";
3545
2943
  const os = r.osUser ? "\u2713" : "\u2717";
3546
- const homeCol = r.home ?? (isDarwin2() ? "(missing)" : "(non-darwin)");
2944
+ const homeCol = r.home ?? "(missing)";
3547
2945
  console.log(`${r.name.padEnd(nameW)} ${r.email.padEnd(emailW)} ${active.padEnd(6)} ${os.padEnd(7)} ${homeCol}`);
3548
2946
  }
3549
2947
  }
3550
2948
  });
3551
2949
 
3552
2950
  // src/commands/agents/register.ts
3553
- import { existsSync as existsSync10, readFileSync as readFileSync9 } from "fs";
2951
+ import { existsSync as existsSync8, readFileSync as readFileSync7 } from "fs";
3554
2952
  import { defineCommand as defineCommand27 } from "citty";
3555
2953
  import consola24 from "consola";
3556
2954
  var registerAgentCommand = defineCommand27({
@@ -3598,10 +2996,10 @@ var registerAgentCommand = defineCommand27({
3598
2996
  throw new CliError("Pass either --public-key or --public-key-file, not both.");
3599
2997
  }
3600
2998
  if (!publicKey && keyFile) {
3601
- if (!existsSync10(keyFile)) {
2999
+ if (!existsSync8(keyFile)) {
3602
3000
  throw new CliError(`Public-key file not found: ${keyFile}`);
3603
3001
  }
3604
- publicKey = readFileSync9(keyFile, "utf-8").trim();
3002
+ publicKey = readFileSync7(keyFile, "utf-8").trim();
3605
3003
  }
3606
3004
  if (!publicKey) {
3607
3005
  throw new CliError('Provide --public-key "<ssh-ed25519 line>" or --public-key-file <path>.');
@@ -3636,19 +3034,19 @@ var registerAgentCommand = defineCommand27({
3636
3034
  });
3637
3035
 
3638
3036
  // src/commands/agents/run.ts
3639
- import { existsSync as existsSync11, readFileSync as readFileSync10 } from "fs";
3037
+ import { existsSync as existsSync9, readFileSync as readFileSync8 } from "fs";
3640
3038
  import { homedir as homedir7 } from "os";
3641
- import { join as join9 } from "path";
3039
+ import { join as join6 } from "path";
3642
3040
  import { defineCommand as defineCommand28 } from "citty";
3643
3041
  import consola25 from "consola";
3644
3042
  import { taskTools as taskTools2, runLoop } from "@openape/agent-runtime";
3645
- var AUTH_PATH = join9(homedir7(), ".config", "apes", "auth.json");
3646
- var TASK_CACHE_DIR = join9(homedir7(), ".openape", "agent", "tasks");
3043
+ var AUTH_PATH = join6(homedir7(), ".config", "apes", "auth.json");
3044
+ var TASK_CACHE_DIR = join6(homedir7(), ".openape", "agent", "tasks");
3647
3045
  function readAuth() {
3648
- if (!existsSync11(AUTH_PATH)) {
3046
+ if (!existsSync9(AUTH_PATH)) {
3649
3047
  throw new CliError(`No agent auth found at ${AUTH_PATH}. Run \`apes agents spawn <name>\` first.`);
3650
3048
  }
3651
- const parsed = JSON.parse(readFileSync10(AUTH_PATH, "utf8"));
3049
+ const parsed = JSON.parse(readFileSync8(AUTH_PATH, "utf8"));
3652
3050
  if (!parsed.access_token) throw new CliError("auth.json missing access_token");
3653
3051
  return parsed;
3654
3052
  }
@@ -3684,26 +3082,26 @@ ${msg}`.slice(0, 9e3);
3684
3082
  }
3685
3083
  }
3686
3084
  function readTaskSpec(taskId) {
3687
- const path2 = join9(TASK_CACHE_DIR, `${taskId}.json`);
3688
- if (!existsSync11(path2)) {
3085
+ const path2 = join6(TASK_CACHE_DIR, `${taskId}.json`);
3086
+ if (!existsSync9(path2)) {
3689
3087
  throw new CliError(`No cached task spec at ${path2}. Run \`apes agents sync\` first to pull the task list from troop.`);
3690
3088
  }
3691
- return JSON.parse(readFileSync10(path2, "utf8"));
3089
+ return JSON.parse(readFileSync8(path2, "utf8"));
3692
3090
  }
3693
- var AGENT_CONFIG_PATH = join9(homedir7(), ".openape", "agent", "agent.json");
3091
+ var AGENT_CONFIG_PATH = join6(homedir7(), ".openape", "agent", "agent.json");
3694
3092
  function readAgentConfig() {
3695
- if (!existsSync11(AGENT_CONFIG_PATH)) return { systemPrompt: "" };
3093
+ if (!existsSync9(AGENT_CONFIG_PATH)) return { systemPrompt: "" };
3696
3094
  try {
3697
- return JSON.parse(readFileSync10(AGENT_CONFIG_PATH, "utf8"));
3095
+ return JSON.parse(readFileSync8(AGENT_CONFIG_PATH, "utf8"));
3698
3096
  } catch {
3699
3097
  return { systemPrompt: "" };
3700
3098
  }
3701
3099
  }
3702
3100
  function readLitellmConfig2(model) {
3703
- const envPath = join9(homedir7(), "litellm", ".env");
3101
+ const envPath = join6(homedir7(), "litellm", ".env");
3704
3102
  const env = {};
3705
- if (existsSync11(envPath)) {
3706
- for (const line of readFileSync10(envPath, "utf8").split(/\r?\n/)) {
3103
+ if (existsSync9(envPath)) {
3104
+ for (const line of readFileSync8(envPath, "utf8").split(/\r?\n/)) {
3707
3105
  const m = line.match(/^([A-Z_]+)=(.*)$/);
3708
3106
  if (m) env[m[1]] = m[2].replace(/^["']|["']$/g, "");
3709
3107
  }
@@ -3809,18 +3207,18 @@ var runAgentCommand = defineCommand28({
3809
3207
  });
3810
3208
 
3811
3209
  // src/commands/agents/serve.ts
3812
- import { existsSync as existsSync12, readFileSync as readFileSync11 } from "fs";
3210
+ import { existsSync as existsSync10, readFileSync as readFileSync9 } from "fs";
3813
3211
  import { homedir as homedir8 } from "os";
3814
- import { join as join10 } from "path";
3212
+ import { join as join7 } from "path";
3815
3213
  import { createInterface } from "readline";
3816
3214
  import { defineCommand as defineCommand29 } from "citty";
3817
3215
  import { taskTools as taskTools3, runLoop as runLoop2, RpcSessionMap } from "@openape/agent-runtime";
3818
- var AUTH_PATH2 = join10(homedir8(), ".config", "apes", "auth.json");
3216
+ var AUTH_PATH2 = join7(homedir8(), ".config", "apes", "auth.json");
3819
3217
  function readLitellmConfig3(model) {
3820
- const envPath = join10(homedir8(), "litellm", ".env");
3218
+ const envPath = join7(homedir8(), "litellm", ".env");
3821
3219
  const env = {};
3822
- if (existsSync12(envPath)) {
3823
- for (const line of readFileSync11(envPath, "utf8").split(/\r?\n/)) {
3220
+ if (existsSync10(envPath)) {
3221
+ for (const line of readFileSync9(envPath, "utf8").split(/\r?\n/)) {
3824
3222
  const m = line.match(/^([A-Z_]+)=(.*)$/);
3825
3223
  if (m) env[m[1]] = m[2].replace(/^["']|["']$/g, "");
3826
3224
  }
@@ -3852,9 +3250,9 @@ var serveAgentCommand = defineCommand29({
3852
3250
  if (!args.rpc) {
3853
3251
  throw new CliError("apes agents serve currently only supports --rpc mode");
3854
3252
  }
3855
- if (existsSync12(AUTH_PATH2)) {
3253
+ if (existsSync10(AUTH_PATH2)) {
3856
3254
  try {
3857
- JSON.parse(readFileSync11(AUTH_PATH2, "utf8"));
3255
+ JSON.parse(readFileSync9(AUTH_PATH2, "utf8"));
3858
3256
  } catch {
3859
3257
  }
3860
3258
  }
@@ -3937,101 +3335,49 @@ async function handleInbound(msg, sessions) {
3937
3335
  import { defineCommand as defineCommand30 } from "citty";
3938
3336
  import consola26 from "consola";
3939
3337
 
3940
- // src/lib/troop-bootstrap.ts
3941
- var SYNC_LABEL_PREFIX = "openape.troop.sync";
3942
- var SYNC_INTERVAL_SECONDS = 300;
3943
- function syncPlistLabel(agentName) {
3944
- return `${SYNC_LABEL_PREFIX}.${agentName}`;
3945
- }
3946
- function escape(s) {
3947
- return s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
3948
- }
3949
- function buildSyncPlist(input) {
3950
- const pathDirs = (input.hostBinDirs && input.hostBinDirs.length > 0 ? input.hostBinDirs : ["/opt/homebrew/bin", "/usr/local/bin"]).join(":");
3951
- const pathLine = ` <key>PATH</key><string>${escape(pathDirs)}:/usr/bin:/bin</string>
3952
- `;
3953
- const agentUserLine = ` <key>AGENT_USER</key><string>${escape(input.userName)}</string>
3954
- `;
3955
- const envBlock = input.troopUrl ? ` <key>EnvironmentVariables</key>
3956
- <dict>
3957
- <key>HOME</key><string>${escape(input.homeDir)}</string>
3958
- ${pathLine}${agentUserLine} <key>OPENAPE_TROOP_URL</key><string>${escape(input.troopUrl)}</string>
3959
- </dict>
3960
- ` : ` <key>EnvironmentVariables</key>
3961
- <dict>
3962
- <key>HOME</key><string>${escape(input.homeDir)}</string>
3963
- ${pathLine}${agentUserLine} </dict>
3964
- `;
3965
- return `<?xml version="1.0" encoding="UTF-8"?>
3966
- <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
3967
- <plist version="1.0">
3968
- <dict>
3969
- <key>Label</key>
3970
- <string>${escape(syncPlistLabel(input.agentName))}</string>
3971
- <key>ProgramArguments</key>
3972
- <array>
3973
- <string>${escape(input.apesBin)}</string>
3974
- <string>agents</string>
3975
- <string>sync</string>
3976
- </array>
3977
- <key>WorkingDirectory</key>
3978
- <string>${escape(input.homeDir)}</string>
3979
- ${envBlock} <key>StartInterval</key>
3980
- <integer>${SYNC_INTERVAL_SECONDS}</integer>
3981
- <key>RunAtLoad</key>
3982
- <true/>
3983
- <key>StandardOutPath</key>
3984
- <string>${escape(input.homeDir)}/Library/Logs/openape-troop-sync.log</string>
3985
- <key>StandardErrorPath</key>
3986
- <string>${escape(input.homeDir)}/Library/Logs/openape-troop-sync.log</string>
3987
- </dict>
3988
- </plist>
3989
- `;
3990
- }
3991
-
3992
3338
  // src/lib/keygen.ts
3993
- import { Buffer as Buffer4 } from "buffer";
3994
- import { existsSync as existsSync13, mkdirSync as mkdirSync3, readFileSync as readFileSync12, writeFileSync as writeFileSync7 } from "fs";
3339
+ import { Buffer as Buffer3 } from "buffer";
3340
+ import { existsSync as existsSync11, mkdirSync as mkdirSync3, readFileSync as readFileSync10, writeFileSync as writeFileSync5 } from "fs";
3995
3341
  import { generateKeyPairSync } from "crypto";
3996
3342
  import { homedir as homedir9 } from "os";
3997
- import { dirname, resolve as resolve2 } from "path";
3343
+ import { dirname as dirname2, resolve as resolve2 } from "path";
3998
3344
  import { generateX25519KeyPair } from "@openape/core";
3999
3345
  function resolveKeyPath(p) {
4000
3346
  return resolve2(p.replace(/^~/, homedir9()));
4001
3347
  }
4002
3348
  function buildSshEd25519Line(rawPub) {
4003
3349
  const keyTypeStr = "ssh-ed25519";
4004
- const keyTypeLen = Buffer4.alloc(4);
3350
+ const keyTypeLen = Buffer3.alloc(4);
4005
3351
  keyTypeLen.writeUInt32BE(keyTypeStr.length);
4006
- const pubKeyLen = Buffer4.alloc(4);
3352
+ const pubKeyLen = Buffer3.alloc(4);
4007
3353
  pubKeyLen.writeUInt32BE(rawPub.length);
4008
- const blob = Buffer4.concat([keyTypeLen, Buffer4.from(keyTypeStr), pubKeyLen, rawPub]);
3354
+ const blob = Buffer3.concat([keyTypeLen, Buffer3.from(keyTypeStr), pubKeyLen, rawPub]);
4009
3355
  return `ssh-ed25519 ${blob.toString("base64")}`;
4010
3356
  }
4011
3357
  function readPublicKey(keyPath) {
4012
3358
  const pubPath = `${keyPath}.pub`;
4013
- if (existsSync13(pubPath)) {
4014
- return readFileSync12(pubPath, "utf-8").trim();
3359
+ if (existsSync11(pubPath)) {
3360
+ return readFileSync10(pubPath, "utf-8").trim();
4015
3361
  }
4016
- const keyContent = readFileSync12(keyPath, "utf-8");
3362
+ const keyContent = readFileSync10(keyPath, "utf-8");
4017
3363
  const privateKey = loadEd25519PrivateKey(keyContent);
4018
3364
  const jwk = privateKey.export({ format: "jwk" });
4019
- const pubBytes = Buffer4.from(jwk.x, "base64url");
3365
+ const pubBytes = Buffer3.from(jwk.x, "base64url");
4020
3366
  return buildSshEd25519Line(pubBytes);
4021
3367
  }
4022
3368
  function generateAndSaveKey(keyPath) {
4023
3369
  const resolved = resolveKeyPath(keyPath);
4024
- const dir = dirname(resolved);
4025
- if (!existsSync13(dir)) {
3370
+ const dir = dirname2(resolved);
3371
+ if (!existsSync11(dir)) {
4026
3372
  mkdirSync3(dir, { recursive: true });
4027
3373
  }
4028
3374
  const { publicKey, privateKey } = generateKeyPairSync("ed25519");
4029
3375
  const privatePem = privateKey.export({ type: "pkcs8", format: "pem" });
4030
- writeFileSync7(resolved, privatePem, { mode: 384 });
3376
+ writeFileSync5(resolved, privatePem, { mode: 384 });
4031
3377
  const jwk = publicKey.export({ format: "jwk" });
4032
- const pubBytes = Buffer4.from(jwk.x, "base64url");
3378
+ const pubBytes = Buffer3.from(jwk.x, "base64url");
4033
3379
  const pubKeyStr = buildSshEd25519Line(pubBytes);
4034
- writeFileSync7(`${resolved}.pub`, `${pubKeyStr}
3380
+ writeFileSync5(`${resolved}.pub`, `${pubKeyStr}
4035
3381
  `, { mode: 420 });
4036
3382
  return pubKeyStr;
4037
3383
  }
@@ -4039,7 +3385,7 @@ function generateKeyPairInMemory() {
4039
3385
  const { publicKey, privateKey } = generateKeyPairSync("ed25519");
4040
3386
  const privatePem = privateKey.export({ type: "pkcs8", format: "pem" });
4041
3387
  const jwk = publicKey.export({ format: "jwk" });
4042
- const pubBytes = Buffer4.from(jwk.x, "base64url");
3388
+ const pubBytes = Buffer3.from(jwk.x, "base64url");
4043
3389
  const enc = generateX25519KeyPair();
4044
3390
  return {
4045
3391
  privatePem,
@@ -4049,145 +3395,8 @@ function generateKeyPairInMemory() {
4049
3395
  };
4050
3396
  }
4051
3397
 
4052
- // src/lib/llm-bridge.ts
4053
- import { execFileSync as execFileSync13 } from "child_process";
4054
- import { existsSync as existsSync14, readFileSync as readFileSync13 } from "fs";
4055
- import { homedir as homedir10 } from "os";
4056
- import { dirname as dirname2, join as join11 } from "path";
4057
- var PLIST_LABEL_PREFIX = "eco.hofmann.apes.bridge";
4058
- function readLitellmEnv(envPath = join11(homedir10(), "litellm", ".env")) {
4059
- if (!existsSync14(envPath)) return null;
4060
- try {
4061
- const text = readFileSync13(envPath, "utf8");
4062
- const out = {};
4063
- for (const line of text.split("\n")) {
4064
- const trimmed = line.trim();
4065
- if (!trimmed || trimmed.startsWith("#")) continue;
4066
- const eq = trimmed.indexOf("=");
4067
- if (eq < 0) continue;
4068
- const key = trimmed.slice(0, eq).trim();
4069
- const value = trimmed.slice(eq + 1).trim();
4070
- if (key === "LITELLM_MASTER_KEY" || key === "LITELLM_API_KEY") out.apiKey = value;
4071
- if (key === "LITELLM_BASE_URL") out.baseUrl = value;
4072
- if (key === "APE_CHAT_BRIDGE_MODEL") out.model = value;
4073
- }
4074
- return out;
4075
- } catch {
4076
- return null;
4077
- }
4078
- }
4079
- function resolveBridgeConfig(opts) {
4080
- const env = readLitellmEnv(opts.envPath);
4081
- const apiKey = opts.cliKey ?? env?.apiKey;
4082
- const baseUrl = opts.cliBaseUrl ?? env?.baseUrl ?? "http://127.0.0.1:4000/v1";
4083
- const model = opts.cliModel ?? env?.model;
4084
- if (!apiKey) {
4085
- throw new Error(
4086
- "No LITELLM_API_KEY resolved. Pass --bridge-key sk-\u2026 or write LITELLM_MASTER_KEY into ~/litellm/.env first."
4087
- );
4088
- }
4089
- return { baseUrl, apiKey, model };
4090
- }
4091
- function captureHostBinDirs() {
4092
- const dirs = [];
4093
- const seen = /* @__PURE__ */ new Set();
4094
- for (const bin of ["node", "ape-agent", "apes"]) {
4095
- let resolved;
4096
- try {
4097
- resolved = execFileSync13("/usr/bin/which", [bin], { encoding: "utf8" }).trim();
4098
- } catch {
4099
- const installCmd = bin === "ape-agent" ? "npm i -g @openape/ape-agent" : bin === "apes" ? "npm i -g @openape/apes" : "install Node.js (e.g. brew install node)";
4100
- throw new Error(`'${bin}' not found on host PATH. ${installCmd} before spawning agents \u2014 the bridge runtime resolves these at spawn time and bakes the dir into the agent's launchd plist.`);
4101
- }
4102
- const dir = dirname2(resolved);
4103
- if (!seen.has(dir)) {
4104
- seen.add(dir);
4105
- dirs.push(dir);
4106
- }
4107
- }
4108
- return dirs;
4109
- }
4110
- function bridgePlistLabel(agentName) {
4111
- return `${PLIST_LABEL_PREFIX}.${agentName}`;
4112
- }
4113
- function bridgePlistPath(agentName) {
4114
- return `/Library/LaunchDaemons/${bridgePlistLabel(agentName)}.plist`;
4115
- }
4116
- function buildBridgeEnvFile(cfg) {
4117
- const modelLine = cfg.model ? `APE_CHAT_BRIDGE_MODEL=${cfg.model}
4118
- ` : "";
4119
- return `# Auto-generated by 'apes agents spawn'.
4120
- # Read by the chat-bridge daemon at boot to talk to the local LLM proxy.
4121
- LITELLM_BASE_URL=${cfg.baseUrl}
4122
- LITELLM_API_KEY=${cfg.apiKey}
4123
- ${modelLine}`;
4124
- }
4125
- function buildBridgeStartScript(hostBinDirs) {
4126
- const pathLine = `export PATH="${hostBinDirs.join(":")}:/usr/bin:/bin"`;
4127
- return `#!/usr/bin/env bash
4128
- # Auto-generated by 'apes agents spawn'.
4129
- # Slim launcher \u2014 bridge stack lives on the host, no per-agent install.
4130
- set -euo pipefail
4131
-
4132
- ${pathLine}
4133
-
4134
- # Token refresh is in-process via @openape/cli-auth's challenge-response
4135
- # path (auth.json.key_path -> ~/.ssh/id_ed25519). No "apes login" needed
4136
- # at boot \u2014 keeping start.sh slim avoids the rate-limit dance the old
4137
- # refresh hit when KeepAlive crash-restarted the daemon every 1h.
4138
-
4139
- set -a
4140
- . "$HOME/Library/Application Support/openape/bridge/.env"
4141
- set +a
4142
- exec ape-agent
4143
- `;
4144
- }
4145
- function buildBridgePlist(agentName, homeDir, ownerEmail, hostBinDirs) {
4146
- const startScript = `${homeDir}/Library/Application Support/openape/bridge/start.sh`;
4147
- const stdoutLog = `${homeDir}/Library/Logs/ape-agent.log`;
4148
- const stderrLog = `${homeDir}/Library/Logs/ape-agent.err.log`;
4149
- const pathValue = `${hostBinDirs.join(":")}:/usr/bin:/bin`;
4150
- return `<?xml version="1.0" encoding="UTF-8"?>
4151
- <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
4152
- <plist version="1.0">
4153
- <dict>
4154
- <key>Label</key>
4155
- <string>${bridgePlistLabel(agentName)}</string>
4156
- <key>UserName</key>
4157
- <string>${agentName}</string>
4158
- <key>ProgramArguments</key>
4159
- <array>
4160
- <string>/bin/bash</string>
4161
- <string>${startScript}</string>
4162
- </array>
4163
- <key>WorkingDirectory</key>
4164
- <string>${homeDir}</string>
4165
- <key>RunAtLoad</key>
4166
- <true/>
4167
- <key>KeepAlive</key>
4168
- <true/>
4169
- <key>ThrottleInterval</key>
4170
- <integer>10</integer>
4171
- <key>StandardOutPath</key>
4172
- <string>${stdoutLog}</string>
4173
- <key>StandardErrorPath</key>
4174
- <string>${stderrLog}</string>
4175
- <key>EnvironmentVariables</key>
4176
- <dict>
4177
- <key>HOME</key>
4178
- <string>${homeDir}</string>
4179
- <key>PATH</key>
4180
- <string>${pathValue}</string>
4181
- <key>OPENAPE_OWNER_EMAIL</key>
4182
- <string>${ownerEmail}</string>
4183
- </dict>
4184
- </dict>
4185
- </plist>
4186
- `;
4187
- }
4188
-
4189
3398
  // src/commands/agents/spawn.ts
4190
- function readMacOSUidOrNull(name) {
3399
+ function readUidOrNull(name) {
4191
3400
  try {
4192
3401
  const u = getHostPlatform().readAgentUser(name);
4193
3402
  return u?.uid ?? null;
@@ -4198,30 +3407,22 @@ function readMacOSUidOrNull(name) {
4198
3407
  var spawnAgentCommand = defineCommand30({
4199
3408
  meta: {
4200
3409
  name: "spawn",
4201
- description: "Provision a local macOS agent end-to-end (OS user, keypair, IdP agent, Claude hook)"
3410
+ description: "Provision a local Linux agent end-to-end (OS user, keypair, IdP agent, Claude hook)"
4202
3411
  },
4203
3412
  args: {
4204
3413
  name: {
4205
3414
  type: "positional",
4206
- description: "Agent name \u2014 also the macOS short username (lowercase, [a-z0-9-], must start with a letter)",
3415
+ description: "Agent name \u2014 also the Linux username (lowercase, [a-z0-9-], must start with a letter)",
4207
3416
  required: true
4208
3417
  },
4209
3418
  shell: {
4210
3419
  type: "string",
4211
- description: "Login shell for the macOS user. Default: /bin/zsh. Pass $(which ape-shell) to opt into the grant-mediated REPL as login shell."
3420
+ description: "Login shell for the Linux user. Default: /bin/bash. Pass $(which ape-shell) to opt into the grant-mediated REPL as login shell."
4212
3421
  },
4213
3422
  "no-claude-hook": {
4214
3423
  type: "boolean",
4215
3424
  description: "Skip writing ~/.claude/settings.json + the Bash-rewrite hook"
4216
3425
  },
4217
- "claude-token": {
4218
- type: "string",
4219
- description: "Claude Code OAuth token (sk-ant-oat01-\u2026) from `claude setup-token`. Visible to ps \u2014 prefer --claude-token-stdin in scripts."
4220
- },
4221
- "claude-token-stdin": {
4222
- type: "boolean",
4223
- description: "Read the Claude Code OAuth token from stdin (paranoid form of --claude-token)."
4224
- },
4225
3426
  "no-bridge": {
4226
3427
  type: "boolean",
4227
3428
  description: "Skip the ape-agent runtime install. Default behaviour installs the runtime so the agent answers chat.openape.ai messages (reads LITELLM_API_KEY/BASE_URL from ~/litellm/.env; override via --bridge-key / --bridge-base-url). Use --no-bridge for headless / CI / IdP-only account provisioning where the agent will not run a chat loop."
@@ -4237,6 +3438,18 @@ var spawnAgentCommand = defineCommand30({
4237
3438
  "bridge-model": {
4238
3439
  type: "string",
4239
3440
  description: "Model the bridge sends in chat-completion requests (default: claude-haiku-4-5). Override when fronting a proxy that doesn't route the default \u2014 e.g. ChatGPT-only proxy needs `gpt-5.4`."
3441
+ },
3442
+ "kind": {
3443
+ type: "string",
3444
+ description: `Agent kind: "user" (default, connects to troop-chat) or "service" (polls an SP backend's task queue and runs each task through the LLM). With "service", --serves is required.`
3445
+ },
3446
+ "serves": {
3447
+ type: "string",
3448
+ description: "For --kind service: base URL of the SP backend this agent serves (e.g. https://zaz.delta-mind.at or http://127.0.0.1:3013). The agent pulls GetNextTask / posts ResolveTask there."
3449
+ },
3450
+ "poll-interval": {
3451
+ type: "string",
3452
+ description: "For --kind service: idle poll interval in ms (default 2000)."
4240
3453
  }
4241
3454
  },
4242
3455
  async run({ args }) {
@@ -4246,11 +3459,13 @@ var spawnAgentCommand = defineCommand30({
4246
3459
  `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.`
4247
3460
  );
4248
3461
  }
4249
- if (!isDarwin2()) {
4250
- throw new CliError(
4251
- `\`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.`
4252
- );
4253
- }
3462
+ if (args.kind != null && args.kind !== "user" && args.kind !== "service")
3463
+ throw new CliError(`Invalid --kind "${String(args.kind)}". Must be "user" or "service".`);
3464
+ const isService = args.kind === "service";
3465
+ const servesUrl = typeof args.serves === "string" ? args.serves.replace(/\/$/, "") : void 0;
3466
+ if (isService && !servesUrl)
3467
+ throw new CliError("--kind service requires --serves <SP base URL> (e.g. https://zaz.delta-mind.at).");
3468
+ const pollMs = typeof args["poll-interval"] === "string" ? Number.parseInt(args["poll-interval"], 10) : void 0;
4254
3469
  const auth = loadAuth();
4255
3470
  if (!auth) {
4256
3471
  throw new CliError("Not authenticated. Run `apes login` first.");
@@ -4259,177 +3474,179 @@ var spawnAgentCommand = defineCommand30({
4259
3474
  if (!idp) {
4260
3475
  throw new CliError("No IdP URL configured. Run `apes login` first.");
4261
3476
  }
4262
- const loginShell = (args.shell ?? "/bin/zsh").toString();
4263
- const apes = whichBinary("apes");
4264
- if (!apes) {
4265
- throw new CliError("`apes` not found on PATH. Install @openape/apes globally first.");
4266
- }
4267
- const escapes = whichBinary("escapes");
4268
- if (!escapes) {
4269
- throw new CliError(
4270
- "`escapes` not found on PATH. spawn delegates the privileged setup phase to escapes; install it before running spawn."
4271
- );
4272
- }
4273
- if (!isShellRegistered(loginShell)) {
4274
- throw new CliError(
4275
- `${loginShell} is not registered in /etc/shells. macOS refuses to set it as a login shell. Run:
4276
- echo ${loginShell} | sudo tee -a /etc/shells
4277
- and try again.`
4278
- );
4279
- }
3477
+ const loginShell = (args.shell ?? "/bin/bash").toString();
4280
3478
  const platform = getHostPlatform();
4281
- const macOSUsername = platform.agentUsername(name);
4282
- const existing = platform.readAgentUser(macOSUsername) ?? platform.readAgentUser(name);
3479
+ const osUsername = platform.agentUsername(name);
3480
+ const existing = platform.readAgentUser(osUsername);
4283
3481
  if (existing) {
4284
- throw new CliError(`macOS user "${existing.name}" already exists (uid=${existing.uid ?? "?"}). Refusing to overwrite.`);
3482
+ throw new CliError(`OS user "${existing.name}" already exists (uid=${existing.uid ?? "?"}). Refusing to overwrite.`);
3483
+ }
3484
+ const homeDir = `/var/lib/openape/homes/${osUsername}`;
3485
+ consola26.start(`Generating keypair for ${name}\u2026`);
3486
+ const { privatePem, publicSshLine, x25519PrivateKey, x25519PublicKey } = generateKeyPairInMemory();
3487
+ consola26.start(`Registering agent at ${idp}\u2026`);
3488
+ const registration = await registerAgentAtIdp({ name, publicKey: publicSshLine, idp });
3489
+ consola26.success(`Registered as ${registration.email}`);
3490
+ consola26.start("Issuing agent access token\u2026");
3491
+ const { token, expiresIn } = await issueAgentToken({
3492
+ idp,
3493
+ agentEmail: registration.email,
3494
+ privateKeyPem: privatePem
3495
+ });
3496
+ const authJson = buildAgentAuthJson({
3497
+ idp,
3498
+ accessToken: token,
3499
+ email: registration.email,
3500
+ expiresAt: Math.floor(Date.now() / 1e3) + expiresIn,
3501
+ keyPath: `${homeDir}/.ssh/id_ed25519`,
3502
+ // The IdP resolves the owner transitively (when the caller
3503
+ // is itself an agent — e.g. a Nest spawning a child — the
3504
+ // human at the top of the chain becomes owner). Use the
3505
+ // server-resolved owner, not the local caller's auth.email,
3506
+ // otherwise the agent's auth.json will carry the Nest's
3507
+ // email and troop will reject sync calls because the
3508
+ // encoded owner-domain in the agent email doesn't match
3509
+ // the auth.json's owner_email domain.
3510
+ ownerEmail: registration.owner
3511
+ });
3512
+ const includeClaudeHook = !args["no-claude-hook"];
3513
+ const withBridge = !args["no-bridge"];
3514
+ const script = buildSpawnSetupScript({
3515
+ name,
3516
+ homeDir,
3517
+ shellPath: loginShell,
3518
+ privateKeyPem: privatePem,
3519
+ publicKeySshLine: publicSshLine,
3520
+ x25519PrivateKey,
3521
+ x25519PublicKey,
3522
+ authJson,
3523
+ claudeSettingsJson: includeClaudeHook ? CLAUDE_SETTINGS_JSON : null,
3524
+ hookScriptSource: includeClaudeHook ? BASH_VIA_APE_SHELL_HOOK_SOURCE : null
3525
+ });
3526
+ consola26.start("Running privileged setup\u2026");
3527
+ if (process.getuid?.() !== 0) {
3528
+ consola26.info("You will be asked to approve the as=root grant in your DDISA inbox; this command blocks until you do.");
4285
3529
  }
4286
- const homeDir = `/var/openape/homes/${macOSUsername}`;
3530
+ await platform.runPrivilegedBash(script);
4287
3531
  try {
4288
- consola26.start(`Generating keypair for ${name}\u2026`);
4289
- const { privatePem, publicSshLine, x25519PrivateKey, x25519PublicKey } = generateKeyPairInMemory();
4290
- consola26.start(`Registering agent at ${idp}\u2026`);
4291
- const registration = await registerAgentAtIdp({ name, publicKey: publicSshLine, idp });
4292
- consola26.success(`Registered as ${registration.email}`);
4293
- consola26.start("Issuing agent access token\u2026");
4294
- const { token, expiresIn } = await issueAgentToken({
4295
- idp,
4296
- agentEmail: registration.email,
4297
- privateKeyPem: privatePem
4298
- });
4299
- const authJson = buildAgentAuthJson({
4300
- idp,
4301
- accessToken: token,
4302
- email: registration.email,
4303
- expiresAt: Math.floor(Date.now() / 1e3) + expiresIn,
4304
- keyPath: `${homeDir}/.ssh/id_ed25519`,
4305
- // The IdP resolves the owner transitively (when the caller
4306
- // is itself an agent — e.g. a Nest spawning a child — the
4307
- // human at the top of the chain becomes owner). Use the
4308
- // server-resolved owner, not the local caller's auth.email,
4309
- // otherwise the agent's auth.json will carry the Nest's
4310
- // email and troop will reject sync calls because the
4311
- // encoded owner-domain in the agent email doesn't match
4312
- // the auth.json's owner_email domain.
4313
- ownerEmail: registration.owner
4314
- });
4315
- const includeClaudeHook = !args["no-claude-hook"];
4316
- const claudeOauthToken = await resolveClaudeToken({
4317
- flag: typeof args["claude-token"] === "string" ? args["claude-token"] : void 0,
4318
- fromStdin: !!args["claude-token-stdin"]
4319
- });
4320
- const withBridge = !args["no-bridge"];
4321
- const bridge = withBridge ? (() => {
4322
- const cfg = resolveBridgeConfig({
4323
- cliKey: typeof args["bridge-key"] === "string" ? args["bridge-key"] : void 0,
4324
- cliBaseUrl: typeof args["bridge-base-url"] === "string" ? args["bridge-base-url"] : void 0,
4325
- cliModel: typeof args["bridge-model"] === "string" ? args["bridge-model"] : void 0
4326
- });
4327
- const hostBinDirs = captureHostBinDirs();
4328
- return {
4329
- plistLabel: bridgePlistLabel(name),
4330
- plistPath: bridgePlistPath(name),
4331
- plistContent: buildBridgePlist(name, homeDir, auth.email, hostBinDirs),
4332
- startScript: buildBridgeStartScript(hostBinDirs),
4333
- envFile: buildBridgeEnvFile(cfg)
4334
- };
4335
- })() : null;
4336
- const troopPlistLabel = `openape.troop.sync.${name}`;
4337
- const troopPlistPath = `/Library/LaunchDaemons/${troopPlistLabel}.plist`;
4338
- const troopBinDirs = bridge ? captureHostBinDirs() : captureHostBinDirs();
4339
- const troop = {
4340
- plistLabel: troopPlistLabel,
4341
- plistPath: troopPlistPath,
4342
- plistContent: buildSyncPlist({ agentName: name, apesBin: apes, homeDir, userName: name, hostBinDirs: troopBinDirs })
4343
- };
4344
- const script = buildSpawnSetupScript({
3532
+ const uid = readUidOrNull(osUsername) ?? -1;
3533
+ upsertNestAgent({
4345
3534
  name,
4346
- macOSUsername,
4347
- homeDir,
4348
- shellPath: loginShell,
4349
- privateKeyPem: privatePem,
4350
- publicKeySshLine: publicSshLine,
4351
- x25519PrivateKey,
4352
- x25519PublicKey,
4353
- authJson,
4354
- claudeSettingsJson: includeClaudeHook ? CLAUDE_SETTINGS_JSON : null,
4355
- hookScriptSource: includeClaudeHook ? BASH_VIA_APE_SHELL_HOOK_SOURCE : null,
4356
- claudeOauthToken,
4357
- bridge,
4358
- troop
3535
+ uid,
3536
+ home: homeDir,
3537
+ email: registration.email,
3538
+ registeredAt: Math.floor(Date.now() / 1e3),
3539
+ kind: isService ? "service" : void 0,
3540
+ service: isService && servesUrl ? { spBaseUrl: servesUrl, pollIntervalMs: pollMs != null && Number.isFinite(pollMs) && pollMs > 0 ? pollMs : void 0 } : void 0,
3541
+ // Service agents also carry bridge config — it's the LLM endpoint the
3542
+ // worker forwards to (baseUrl/key/model from --bridge-*).
3543
+ bridge: withBridge || isService ? {
3544
+ baseUrl: typeof args["bridge-base-url"] === "string" ? args["bridge-base-url"] : void 0,
3545
+ apiKey: typeof args["bridge-key"] === "string" ? args["bridge-key"] : void 0,
3546
+ model: typeof args["bridge-model"] === "string" ? args["bridge-model"] : void 0
3547
+ } : void 0
4359
3548
  });
4360
- consola26.start("Running privileged setup\u2026");
4361
- if (process.getuid?.() !== 0) {
4362
- consola26.info("You will be asked to approve the as=root grant in your DDISA inbox; this command blocks until you do.");
4363
- }
4364
- await platform.runPrivilegedBash(script);
4365
- try {
4366
- const uid = readMacOSUidOrNull(macOSUsername) ?? readMacOSUidOrNull(name);
4367
- upsertNestAgent({
4368
- name,
4369
- uid: uid ?? -1,
4370
- home: homeDir,
4371
- email: registration.email,
4372
- registeredAt: Math.floor(Date.now() / 1e3),
4373
- bridge: withBridge ? {
4374
- baseUrl: typeof args["bridge-base-url"] === "string" ? args["bridge-base-url"] : void 0,
4375
- apiKey: typeof args["bridge-key"] === "string" ? args["bridge-key"] : void 0,
4376
- model: typeof args["bridge-model"] === "string" ? args["bridge-model"] : void 0
4377
- } : void 0
4378
- });
4379
- } catch (err) {
4380
- consola26.warn(`Could not write to nest registry: ${err instanceof Error ? err.message : String(err)}`);
4381
- }
4382
- consola26.success(`Agent ${name} spawned.`);
4383
- consola26.info(`\u{1F517} Troop: https://troop.openape.ai/agents/${name}`);
4384
- if (withBridge) {
4385
- consola26.info(`On first boot, the bridge will send you a contact request from ${registration.email}.`);
4386
- consola26.info("Open chat.openape.ai and accept it to start chatting with the agent.");
4387
- }
4388
- console.log("");
4389
- console.log("Run as the agent with:");
4390
- console.log(` apes run --as ${name} -- claude --session-name ${name} --dangerously-skip-permissions`);
4391
- } finally {
3549
+ } catch (err) {
3550
+ consola26.warn(`Could not write to nest registry: ${err instanceof Error ? err.message : String(err)}`);
3551
+ }
3552
+ consola26.success(`Agent ${name} spawned.`);
3553
+ consola26.info(`\u{1F517} Troop: https://troop.openape.ai/agents/${name}`);
3554
+ if (withBridge) {
3555
+ consola26.info(`On first boot, the bridge will send you a contact request from ${registration.email}.`);
3556
+ consola26.info("Open chat.openape.ai and accept it to start chatting with the agent.");
4392
3557
  }
3558
+ console.log("");
3559
+ console.log("Run as the agent with:");
3560
+ console.log(` apes run --as ${name} -- claude --session-name ${name} --dangerously-skip-permissions`);
4393
3561
  }
4394
3562
  });
4395
- async function resolveClaudeToken(opts) {
4396
- if (opts.flag && opts.fromStdin) {
4397
- throw new CliError("Pass --claude-token OR --claude-token-stdin, not both.");
4398
- }
4399
- let raw = null;
4400
- if (typeof opts.flag === "string") {
4401
- raw = opts.flag.trim();
4402
- } else if (opts.fromStdin) {
4403
- const chunks = [];
4404
- for await (const chunk of process.stdin) {
4405
- chunks.push(typeof chunk === "string" ? Buffer.from(chunk) : chunk);
4406
- }
4407
- raw = Buffer.concat(chunks).toString("utf-8").trim();
4408
- }
4409
- if (!raw) return null;
4410
- if (!raw.startsWith("sk-ant-oat01-")) {
4411
- throw new CliError(
4412
- `Claude token doesn't look right (expected sk-ant-oat01-\u2026). Run \`claude setup-token\` and paste the resulting token.`
4413
- );
4414
- }
4415
- return raw;
4416
- }
4417
3563
 
4418
3564
  // src/commands/agents/sync.ts
4419
- import { chownSync, existsSync as existsSync15, mkdirSync as mkdirSync4, readdirSync as readdirSync2, readFileSync as readFileSync14, rmSync as rmSync5, statSync, writeFileSync as writeFileSync8 } from "fs";
4420
- import { homedir as homedir11 } from "os";
4421
- import { join as join12 } from "path";
3565
+ import { chownSync, existsSync as existsSync13, mkdirSync as mkdirSync4, readdirSync as readdirSync2, readFileSync as readFileSync12, rmSync as rmSync3, statSync as statSync2, writeFileSync as writeFileSync7 } from "fs";
3566
+ import { homedir as homedir10 } from "os";
3567
+ import { join as join9 } from "path";
4422
3568
  import { defineCommand as defineCommand31 } from "citty";
3569
+ import consola28 from "consola";
3570
+
3571
+ // src/lib/recipe-checkout.ts
3572
+ import { execFileSync as execFileSync7 } from "child_process";
3573
+ import { cpSync, existsSync as existsSync12, readFileSync as readFileSync11, rmSync as rmSync2, writeFileSync as writeFileSync6 } from "fs";
3574
+ import { join as join8, resolve as resolve3, sep } from "path";
4423
3575
  import consola27 from "consola";
4424
- var AUTH_PATH3 = join12(homedir11(), ".config", "apes", "auth.json");
4425
- var TASK_CACHE_DIR2 = join12(homedir11(), ".openape", "agent", "tasks");
3576
+ var MARKER_FILE = ".recipe-ref";
3577
+ var GITHUB_PREFIX = "github.com/";
3578
+ function ensureRecipeCheckout(recipeRef, recipeDir, exec2 = execFileSync7) {
3579
+ const at = recipeRef.lastIndexOf("@");
3580
+ if (at <= 0) {
3581
+ consola27.warn(`recipe: malformed recipeRef "${recipeRef}" (expected <owner>/<name>[/<subdir>]@<ref>) \u2014 skipping checkout`);
3582
+ return;
3583
+ }
3584
+ const spec = recipeRef.slice(0, at);
3585
+ const ref = recipeRef.slice(at + 1);
3586
+ const slug = spec.startsWith(GITHUB_PREFIX) ? spec.slice(GITHUB_PREFIX.length) : spec;
3587
+ if (!slug || !ref) {
3588
+ consola27.warn(`recipe: malformed recipeRef "${recipeRef}" (empty slug or ref) \u2014 skipping checkout`);
3589
+ return;
3590
+ }
3591
+ const segments = slug.split("/").filter(Boolean);
3592
+ if (segments.length < 2) {
3593
+ consola27.warn(`recipe: malformed recipeRef "${recipeRef}" (expected <owner>/<name>[/<subdir>]@<ref>) \u2014 skipping checkout`);
3594
+ return;
3595
+ }
3596
+ if (segments.some((s) => s === "." || s === ".." || s.includes("\\"))) {
3597
+ consola27.warn(`recipe: unsafe path segment in recipeRef "${recipeRef}" \u2014 skipping checkout`);
3598
+ return;
3599
+ }
3600
+ const repoSlug = segments.slice(0, 2).join("/");
3601
+ const subdir = segments.slice(2).join("/");
3602
+ const markerPath = join8(recipeDir, MARKER_FILE);
3603
+ if (existsSync12(markerPath)) {
3604
+ try {
3605
+ if (readFileSync11(markerPath, "utf8") === recipeRef) return;
3606
+ } catch {
3607
+ }
3608
+ }
3609
+ const cloneUrl = `https://github.com/${repoSlug}`;
3610
+ const staging = subdir ? `${recipeDir}.checkout` : recipeDir;
3611
+ try {
3612
+ rmSync2(recipeDir, { recursive: true, force: true });
3613
+ if (subdir)
3614
+ rmSync2(staging, { recursive: true, force: true });
3615
+ exec2("git", ["clone", "--depth", "1", "--branch", ref, cloneUrl, staging]);
3616
+ if (subdir) {
3617
+ const src = join8(staging, subdir);
3618
+ if (!resolve3(src).startsWith(resolve3(staging) + sep)) {
3619
+ consola27.warn(`recipe: subdirectory "${subdir}" escapes the checkout dir \u2014 skipping`);
3620
+ rmSync2(staging, { recursive: true, force: true });
3621
+ return;
3622
+ }
3623
+ if (!existsSync12(src)) {
3624
+ consola27.warn(`recipe: subdirectory "${subdir}" not found in ${repoSlug}@${ref} \u2014 skipping checkout`);
3625
+ rmSync2(staging, { recursive: true, force: true });
3626
+ return;
3627
+ }
3628
+ cpSync(src, recipeDir, { recursive: true });
3629
+ rmSync2(staging, { recursive: true, force: true });
3630
+ }
3631
+ writeFileSync6(markerPath, recipeRef);
3632
+ consola27.info(`recipe: checked out ${slug}@${ref} \u2192 ${recipeDir}`);
3633
+ } catch (err) {
3634
+ consola27.warn(`recipe: checkout of ${slug}@${ref} failed \u2014 ${err instanceof Error ? err.message : String(err)}`);
3635
+ if (subdir)
3636
+ rmSync2(staging, { recursive: true, force: true });
3637
+ }
3638
+ }
3639
+
3640
+ // src/commands/agents/sync.ts
3641
+ var AUTH_PATH3 = join9(homedir10(), ".config", "apes", "auth.json");
3642
+ var TASK_CACHE_DIR2 = join9(homedir10(), ".openape", "agent", "tasks");
4426
3643
  function readAuthJson() {
4427
- if (!existsSync15(AUTH_PATH3)) {
3644
+ if (!existsSync13(AUTH_PATH3)) {
4428
3645
  throw new CliError(
4429
3646
  `No agent auth found at ${AUTH_PATH3}. Run \`apes agents spawn <name>\` to provision an agent first.`
4430
3647
  );
4431
3648
  }
4432
- const raw = readFileSync14(AUTH_PATH3, "utf8");
3649
+ const raw = readFileSync12(AUTH_PATH3, "utf8");
4433
3650
  let parsed;
4434
3651
  try {
4435
3652
  parsed = JSON.parse(raw);
@@ -4478,7 +3695,7 @@ var syncAgentCommand = defineCommand31({
4478
3695
  if (!auth.owner_email) {
4479
3696
  throw new CliError(`${AUTH_PATH3} is missing owner_email \u2014 re-run \`apes agents spawn\` to update.`);
4480
3697
  }
4481
- consola27.start(`Syncing ${agentName} (${host}, hostId ${hostId.slice(0, 8)}\u2026) with ${troopUrl}`);
3698
+ consola28.start(`Syncing ${agentName} (${host}, hostId ${hostId.slice(0, 8)}\u2026) with ${troopUrl}`);
4482
3699
  const pubkeyX25519 = readAgentEncryptionPublicKey() ?? void 0;
4483
3700
  const sync = await client.sync({
4484
3701
  hostname: host,
@@ -4486,17 +3703,17 @@ var syncAgentCommand = defineCommand31({
4486
3703
  ownerEmail: auth.owner_email,
4487
3704
  ...pubkeyX25519 ? { pubkeyX25519 } : {}
4488
3705
  });
4489
- consola27.info(sync.first_sync ? "\u2713 first sync \u2014 agent registered" : "\u2713 presence updated");
4490
- if (!pubkeyX25519) consola27.warn("No X25519 public key found on disk \u2014 sealed capability secrets cannot be bound until the agent is re-spawned.");
4491
- const { system_prompt: systemPrompt, tools, skills, tasks } = await client.listTasks();
4492
- consola27.info(`Pulled ${tasks.length} task${tasks.length === 1 ? "" : "s"}`);
4493
- consola27.info(`Tools enabled: ${tools.length === 0 ? "(none)" : tools.join(", ")}`);
4494
- consola27.info(`Skills: ${skills.length === 0 ? "(none)" : skills.map((s) => s.name).join(", ")}`);
3706
+ consola28.info(sync.first_sync ? "\u2713 first sync \u2014 agent registered" : "\u2713 presence updated");
3707
+ if (!pubkeyX25519) consola28.warn("No X25519 public key found on disk \u2014 sealed capability secrets cannot be bound until the agent is re-spawned.");
3708
+ const { system_prompt: systemPrompt, tools, skills, tasks, recipe_ref: recipeRef } = await client.listTasks();
3709
+ consola28.info(`Pulled ${tasks.length} task${tasks.length === 1 ? "" : "s"}`);
3710
+ consola28.info(`Tools enabled: ${tools.length === 0 ? "(none)" : tools.join(", ")}`);
3711
+ consola28.info(`Skills: ${skills.length === 0 ? "(none)" : skills.map((s) => s.name).join(", ")}`);
4495
3712
  let agentUid = null;
4496
3713
  let agentGid = null;
4497
3714
  if (process.geteuid?.() === 0) {
4498
3715
  try {
4499
- const homeStat = statSync(homedir11());
3716
+ const homeStat = statSync2(homedir10());
4500
3717
  agentUid = homeStat.uid;
4501
3718
  agentGid = homeStat.gid;
4502
3719
  } catch {
@@ -4510,27 +3727,28 @@ var syncAgentCommand = defineCommand31({
4510
3727
  }
4511
3728
  }
4512
3729
  }
4513
- const agentDir = join12(homedir11(), ".openape", "agent");
3730
+ const agentDir = join9(homedir10(), ".openape", "agent");
4514
3731
  mkdirSync4(agentDir, { recursive: true });
4515
- chownToAgent(join12(homedir11(), ".openape"));
3732
+ chownToAgent(join9(homedir10(), ".openape"));
4516
3733
  chownToAgent(agentDir);
4517
- const agentJsonPath = join12(agentDir, "agent.json");
4518
- writeFileSync8(
3734
+ const agentJsonPath = join9(agentDir, "agent.json");
3735
+ writeFileSync7(
4519
3736
  agentJsonPath,
4520
- `${JSON.stringify({ systemPrompt, tools }, null, 2)}
3737
+ `${JSON.stringify({ systemPrompt, tools, ...recipeRef ? { recipeRef } : {} }, null, 2)}
4521
3738
  `,
4522
3739
  { mode: 384 }
4523
3740
  );
4524
3741
  chownToAgent(agentJsonPath);
3742
+ if (recipeRef) ensureRecipeCheckout(recipeRef, join9(homedir10(), "recipe"));
4525
3743
  mkdirSync4(TASK_CACHE_DIR2, { recursive: true });
4526
3744
  chownToAgent(TASK_CACHE_DIR2);
4527
3745
  for (const task of tasks) {
4528
- const path2 = join12(TASK_CACHE_DIR2, `${task.taskId}.json`);
4529
- writeFileSync8(path2, `${JSON.stringify(task, null, 2)}
3746
+ const path2 = join9(TASK_CACHE_DIR2, `${task.taskId}.json`);
3747
+ writeFileSync7(path2, `${JSON.stringify(task, null, 2)}
4530
3748
  `, { mode: 384 });
4531
3749
  chownToAgent(path2);
4532
3750
  }
4533
- const skillsDir = join12(agentDir, "skills");
3751
+ const skillsDir = join9(agentDir, "skills");
4534
3752
  mkdirSync4(skillsDir, { recursive: true });
4535
3753
  chownToAgent(skillsDir);
4536
3754
  const incomingNames = new Set(skills.map((s) => s.name));
@@ -4538,22 +3756,22 @@ var syncAgentCommand = defineCommand31({
4538
3756
  for (const entry of readdirSync2(skillsDir)) {
4539
3757
  if (incomingNames.has(entry)) continue;
4540
3758
  try {
4541
- rmSync5(join12(skillsDir, entry), { recursive: true, force: true });
3759
+ rmSync3(join9(skillsDir, entry), { recursive: true, force: true });
4542
3760
  } catch {
4543
3761
  }
4544
3762
  }
4545
3763
  } catch {
4546
3764
  }
4547
3765
  for (const skill of skills) {
4548
- const skillDir = join12(skillsDir, skill.name);
3766
+ const skillDir = join9(skillsDir, skill.name);
4549
3767
  mkdirSync4(skillDir, { recursive: true });
4550
3768
  chownToAgent(skillDir);
4551
- const skillPath = join12(skillDir, "SKILL.md");
4552
- writeFileSync8(skillPath, skill.body.endsWith("\n") ? skill.body : `${skill.body}
3769
+ const skillPath = join9(skillDir, "SKILL.md");
3770
+ writeFileSync7(skillPath, skill.body.endsWith("\n") ? skill.body : `${skill.body}
4553
3771
  `, { mode: 384 });
4554
3772
  chownToAgent(skillPath);
4555
3773
  }
4556
- consola27.success("Sync complete.");
3774
+ consola28.success("Sync complete.");
4557
3775
  }
4558
3776
  });
4559
3777
 
@@ -4581,21 +3799,21 @@ var agentsCommand = defineCommand32({
4581
3799
  import { defineCommand as defineCommand40 } from "citty";
4582
3800
 
4583
3801
  // src/commands/nest/authorize.ts
4584
- import { execFileSync as execFileSync14 } from "child_process";
4585
- import { existsSync as existsSync17, readFileSync as readFileSync15 } from "fs";
4586
- import { join as join14 } from "path";
3802
+ import { execFileSync as execFileSync8 } from "child_process";
3803
+ import { existsSync as existsSync15, readFileSync as readFileSync13 } from "fs";
3804
+ import { join as join11 } from "path";
4587
3805
  import { defineCommand as defineCommand34 } from "citty";
4588
- import consola29 from "consola";
3806
+ import consola30 from "consola";
4589
3807
 
4590
3808
  // src/commands/nest/enroll.ts
4591
- import { hostname as hostname5, homedir as homedir12 } from "os";
4592
- import { existsSync as existsSync16, mkdirSync as mkdirSync5, writeFileSync as writeFileSync9, chmodSync } from "fs";
4593
- import { join as join13 } from "path";
3809
+ import { hostname as hostname4, homedir as homedir11 } from "os";
3810
+ import { existsSync as existsSync14, mkdirSync as mkdirSync5, writeFileSync as writeFileSync8, chmodSync } from "fs";
3811
+ import { join as join10 } from "path";
4594
3812
  import { defineCommand as defineCommand33 } from "citty";
4595
- import consola28 from "consola";
4596
- var NEST_DATA_DIR = join13(homedir12(), ".openape", "nest");
3813
+ import consola29 from "consola";
3814
+ var NEST_DATA_DIR = join10(homedir11(), ".openape", "nest");
4597
3815
  function nestAgentName() {
4598
- const raw = hostname5().toLowerCase();
3816
+ const raw = hostname4().toLowerCase();
4599
3817
  const head = raw.split(".")[0] ?? raw;
4600
3818
  const safe = head.replace(/[^a-z0-9-]/g, "-").replace(/-+/g, "-").replace(/^-+|-+$/g, "");
4601
3819
  const trimmed = safe.slice(0, 16);
@@ -4626,25 +3844,25 @@ var enrollNestCommand = defineCommand33({
4626
3844
  throw new CliError("Run `apes login <email>` first \u2014 nest enroll attaches the new identity to your owner account.");
4627
3845
  }
4628
3846
  const name = args.name || nestAgentName();
4629
- const authPath = join13(NEST_DATA_DIR, ".config", "apes", "auth.json");
4630
- if (existsSync16(authPath) && !args.force) {
3847
+ const authPath = join10(NEST_DATA_DIR, ".config", "apes", "auth.json");
3848
+ if (existsSync14(authPath) && !args.force) {
4631
3849
  throw new CliError(`Nest already enrolled at ${authPath}. Pass --force to re-enroll.`);
4632
3850
  }
4633
- const sshDir = join13(NEST_DATA_DIR, ".ssh");
4634
- const configDir = join13(NEST_DATA_DIR, ".config", "apes");
3851
+ const sshDir = join10(NEST_DATA_DIR, ".ssh");
3852
+ const configDir = join10(NEST_DATA_DIR, ".config", "apes");
4635
3853
  mkdirSync5(sshDir, { recursive: true });
4636
3854
  mkdirSync5(configDir, { recursive: true });
4637
- consola28.start(`Generating keypair for ${name}\u2026`);
3855
+ consola29.start(`Generating keypair for ${name}\u2026`);
4638
3856
  const { privatePem, publicSshLine } = generateKeyPairInMemory();
4639
- writeFileSync9(join13(sshDir, "id_ed25519"), `${privatePem.trimEnd()}
3857
+ writeFileSync8(join10(sshDir, "id_ed25519"), `${privatePem.trimEnd()}
4640
3858
  `, { mode: 384 });
4641
- writeFileSync9(join13(sshDir, "id_ed25519.pub"), `${publicSshLine}
3859
+ writeFileSync8(join10(sshDir, "id_ed25519.pub"), `${publicSshLine}
4642
3860
  `, { mode: 420 });
4643
3861
  chmodSync(sshDir, 448);
4644
- consola28.start(`Registering nest at ${idp}\u2026`);
3862
+ consola29.start(`Registering nest at ${idp}\u2026`);
4645
3863
  const registration = await registerAgentAtIdp({ name, publicKey: publicSshLine, idp });
4646
- consola28.success(`Registered as ${registration.email}`);
4647
- consola28.start("Issuing nest access token\u2026");
3864
+ consola29.success(`Registered as ${registration.email}`);
3865
+ consola29.start("Issuing nest access token\u2026");
4648
3866
  const { token, expiresIn } = await issueAgentToken({
4649
3867
  idp,
4650
3868
  agentEmail: registration.email,
@@ -4655,16 +3873,16 @@ var enrollNestCommand = defineCommand33({
4655
3873
  accessToken: token,
4656
3874
  email: registration.email,
4657
3875
  expiresAt: Math.floor(Date.now() / 1e3) + expiresIn,
4658
- keyPath: join13(sshDir, "id_ed25519"),
3876
+ keyPath: join10(sshDir, "id_ed25519"),
4659
3877
  ownerEmail: ownerAuth.email
4660
3878
  });
4661
- writeFileSync9(authPath, authJson, { mode: 384 });
3879
+ writeFileSync8(authPath, authJson, { mode: 384 });
4662
3880
  chmodSync(configDir, 448);
4663
- consola28.success(`Nest enrolled \u2014 auth.json at ${authPath}`);
4664
- consola28.info("");
4665
- consola28.info("Next: configure the YOLO-policy so the nest can spawn/destroy without prompts:");
4666
- consola28.info("");
4667
- consola28.info(" apes nest authorize");
3881
+ consola29.success(`Nest enrolled \u2014 auth.json at ${authPath}`);
3882
+ consola29.info("");
3883
+ consola29.info("Next: configure the YOLO-policy so the nest can spawn/destroy without prompts:");
3884
+ consola29.info("");
3885
+ consola29.info(" apes nest authorize");
4668
3886
  }
4669
3887
  });
4670
3888
 
@@ -4727,14 +3945,14 @@ var authorizeNestCommand = defineCommand34({
4727
3945
  }
4728
3946
  },
4729
3947
  async run({ args }) {
4730
- const nestAuthPath = join14(NEST_DATA_DIR, ".config", "apes", "auth.json");
4731
- if (!existsSync17(nestAuthPath)) {
3948
+ const nestAuthPath = join11(NEST_DATA_DIR, ".config", "apes", "auth.json");
3949
+ if (!existsSync15(nestAuthPath)) {
4732
3950
  throw new CliError("Nest not enrolled. Run `apes nest enroll` first.");
4733
3951
  }
4734
- const nestAuth = JSON.parse(readFileSync15(nestAuthPath, "utf8"));
3952
+ const nestAuth = JSON.parse(readFileSync13(nestAuthPath, "utf8"));
4735
3953
  if (!nestAuth.email) throw new CliError(`${nestAuthPath} has no email`);
4736
3954
  const allow = args.allow ?? DEFAULT_ALLOW_PATTERNS.join(",");
4737
- consola29.info(`Configuring YOLO-policy on ${nestAuth.email} via \`apes yolo set\`\u2026`);
3955
+ consola30.info(`Configuring YOLO-policy on ${nestAuth.email} via \`apes yolo set\`\u2026`);
4738
3956
  const cmdArgs = [
4739
3957
  "yolo",
4740
3958
  "set",
@@ -4748,19 +3966,19 @@ var authorizeNestCommand = defineCommand34({
4748
3966
  cmdArgs.push("--expires-in", args["expires-in"]);
4749
3967
  }
4750
3968
  try {
4751
- execFileSync14("apes", cmdArgs, { stdio: "inherit" });
3969
+ execFileSync8("apes", cmdArgs, { stdio: "inherit" });
4752
3970
  } catch (err) {
4753
3971
  throw new CliError(err instanceof Error ? err.message : String(err));
4754
3972
  }
4755
- consola29.success("Nest-driven agent lifecycle is now zero-prompt.");
4756
- consola29.info("Test: apes agents spawn <name> via the nest API \u2192 no DDISA prompt.");
3973
+ consola30.success("Nest-driven agent lifecycle is now zero-prompt.");
3974
+ consola30.info("Test: apes agents spawn <name> via the nest API \u2192 no DDISA prompt.");
4757
3975
  }
4758
3976
  });
4759
3977
 
4760
3978
  // src/commands/nest/destroy.ts
4761
- import { execFileSync as execFileSync15 } from "child_process";
3979
+ import { execFileSync as execFileSync9 } from "child_process";
4762
3980
  import { defineCommand as defineCommand35 } from "citty";
4763
- import consola30 from "consola";
3981
+ import consola31 from "consola";
4764
3982
  var destroyNestCommand = defineCommand35({
4765
3983
  meta: {
4766
3984
  name: "destroy",
@@ -4772,8 +3990,8 @@ var destroyNestCommand = defineCommand35({
4772
3990
  async run({ args }) {
4773
3991
  const name = String(args.name);
4774
3992
  try {
4775
- execFileSync15("apes", ["run", "--as", "root", "--wait", "--", "apes", "agents", "destroy", name, "--force"], { stdio: "inherit" });
4776
- consola30.success(`Nest will tear down ${name}'s pm2 process on its next reconcile (\u22642s).`);
3993
+ execFileSync9("apes", ["run", "--as", "root", "--wait", "--", "apes", "agents", "destroy", name, "--force"], { stdio: "inherit" });
3994
+ consola31.success(`Nest will tear down ${name}'s pm2 process on its next reconcile (\u22642s).`);
4777
3995
  } catch (err) {
4778
3996
  const status = err.status ?? 1;
4779
3997
  throw new CliExit(status);
@@ -4782,11 +4000,11 @@ var destroyNestCommand = defineCommand35({
4782
4000
  });
4783
4001
 
4784
4002
  // src/commands/nest/install.ts
4785
- import { existsSync as existsSync18, mkdirSync as mkdirSync6, readFileSync as readFileSync16, writeFileSync as writeFileSync10 } from "fs";
4786
- import { homedir as homedir13 } from "os";
4787
- import { dirname as dirname3, join as join15 } from "path";
4003
+ import { existsSync as existsSync16, mkdirSync as mkdirSync6, readFileSync as readFileSync14, writeFileSync as writeFileSync9 } from "fs";
4004
+ import { homedir as homedir12 } from "os";
4005
+ import { dirname as dirname3, join as join12 } from "path";
4788
4006
  import { defineCommand as defineCommand36 } from "citty";
4789
- import consola31 from "consola";
4007
+ import consola32 from "consola";
4790
4008
 
4791
4009
  // src/commands/nest/apes-agents-adapter.ts
4792
4010
  var APES_AGENTS_ADAPTER_TOML = `schema = "openape-shapes/v1"
@@ -4852,41 +4070,41 @@ resource_chain = ["agents:name={name}", "allowlist:email={peer_email}"]
4852
4070
 
4853
4071
  // src/commands/nest/install.ts
4854
4072
  function installAdapter2() {
4855
- const target = join15(homedir13(), ".openape", "shapes", "adapters", "apes-agents.toml");
4073
+ const target = join12(homedir12(), ".openape", "shapes", "adapters", "apes-agents.toml");
4856
4074
  mkdirSync6(dirname3(target), { recursive: true });
4857
4075
  let existing = "";
4858
4076
  try {
4859
- existing = readFileSync16(target, "utf8");
4077
+ existing = readFileSync14(target, "utf8");
4860
4078
  } catch {
4861
4079
  }
4862
4080
  if (existing === APES_AGENTS_ADAPTER_TOML) return false;
4863
- writeFileSync10(target, APES_AGENTS_ADAPTER_TOML, { mode: 420 });
4864
- consola31.success(`Wrote shapes adapter ${target}`);
4081
+ writeFileSync9(target, APES_AGENTS_ADAPTER_TOML, { mode: 420 });
4082
+ consola32.success(`Wrote shapes adapter ${target}`);
4865
4083
  return true;
4866
4084
  }
4867
4085
  function writeBridgeModelDefault(model) {
4868
- for (const envDir of [join15(homedir13(), "litellm"), join15(NEST_DATA_DIR, "litellm")]) {
4869
- const envFile = join15(envDir, ".env");
4086
+ for (const envDir of [join12(homedir12(), "litellm"), join12(NEST_DATA_DIR, "litellm")]) {
4087
+ const envFile = join12(envDir, ".env");
4870
4088
  mkdirSync6(envDir, { recursive: true });
4871
4089
  let lines = [];
4872
- if (existsSync18(envFile)) {
4873
- lines = readFileSync16(envFile, "utf8").split("\n").filter((l) => !l.startsWith("APE_CHAT_BRIDGE_MODEL="));
4090
+ if (existsSync16(envFile)) {
4091
+ lines = readFileSync14(envFile, "utf8").split("\n").filter((l) => !l.startsWith("APE_CHAT_BRIDGE_MODEL="));
4874
4092
  }
4875
4093
  lines.push(`APE_CHAT_BRIDGE_MODEL=${model}`);
4876
4094
  while (lines.length > 0 && lines.at(-1).trim() === "") lines.pop();
4877
- writeFileSync10(envFile, `${lines.join("\n")}
4095
+ writeFileSync9(envFile, `${lines.join("\n")}
4878
4096
  `, { mode: 384 });
4879
4097
  }
4880
4098
  }
4881
4099
  function findBinary(name) {
4882
4100
  for (const dir of [
4883
- join15(homedir13(), ".bun", "bin"),
4101
+ join12(homedir12(), ".bun", "bin"),
4884
4102
  "/opt/homebrew/bin",
4885
4103
  "/usr/local/bin",
4886
4104
  "/usr/bin"
4887
4105
  ]) {
4888
- const p = join15(dir, name);
4889
- if (existsSync18(p)) return p;
4106
+ const p = join12(dir, name);
4107
+ if (existsSync16(p)) return p;
4890
4108
  }
4891
4109
  throw new Error(`could not locate ${name} on PATH; install it first`);
4892
4110
  }
@@ -4906,20 +4124,20 @@ var installNestCommand = defineCommand36({
4906
4124
  }
4907
4125
  },
4908
4126
  async run({ args }) {
4909
- const homeDir = homedir13();
4127
+ const homeDir = homedir12();
4910
4128
  const port = Number(args.port ?? 9091);
4911
4129
  if (!Number.isInteger(port) || port < 1024 || port > 65535) {
4912
4130
  throw new Error(`invalid port ${port}`);
4913
4131
  }
4914
4132
  const nestBin = findBinary("openape-nest");
4915
4133
  const apesBin = findBinary("apes");
4916
- consola31.info(`Installing nest supervisor`);
4917
- consola31.info(` nest binary: ${nestBin}`);
4918
- consola31.info(` apes binary: ${apesBin}`);
4919
- consola31.info(` HTTP port: ${port}`);
4134
+ consola32.info(`Installing nest supervisor`);
4135
+ consola32.info(` nest binary: ${nestBin}`);
4136
+ consola32.info(` apes binary: ${apesBin}`);
4137
+ consola32.info(` HTTP port: ${port}`);
4920
4138
  if (typeof args["bridge-model"] === "string" && args["bridge-model"]) {
4921
4139
  writeBridgeModelDefault(args["bridge-model"]);
4922
- consola31.success(`Default bridge model set to ${args["bridge-model"]} (in ~/litellm/.env)`);
4140
+ consola32.success(`Default bridge model set to ${args["bridge-model"]} (in ~/litellm/.env)`);
4923
4141
  }
4924
4142
  installAdapter2();
4925
4143
  mkdirSync6(NEST_DATA_DIR, { recursive: true });
@@ -4930,20 +4148,20 @@ var installNestCommand = defineCommand36({
4930
4148
  nestHome: NEST_DATA_DIR,
4931
4149
  port
4932
4150
  });
4933
- consola31.success(`Nest daemon bootstrapped \u2014 http://127.0.0.1:${port}`);
4934
- consola31.info("");
4935
- consola31.info("Next steps for zero-prompt spawn \u2014 both one-time:");
4936
- consola31.info("");
4937
- consola31.info(" 1. apes nest enroll # register nest as DDISA agent (creates own auth.json)");
4938
- consola31.info(" 2. apes nest authorize # set YOLO-policy on the nest agent");
4939
- consola31.info("");
4940
- consola31.info("After that, every `POST http://127.0.0.1:9091/agents` runs without DDISA prompts.");
4151
+ consola32.success(`Nest daemon bootstrapped \u2014 http://127.0.0.1:${port}`);
4152
+ consola32.info("");
4153
+ consola32.info("Next steps for zero-prompt spawn \u2014 both one-time:");
4154
+ consola32.info("");
4155
+ consola32.info(" 1. apes nest enroll # register nest as DDISA agent (creates own auth.json)");
4156
+ consola32.info(" 2. apes nest authorize # set YOLO-policy on the nest agent");
4157
+ consola32.info("");
4158
+ consola32.info("After that, every `POST http://127.0.0.1:9091/agents` runs without DDISA prompts.");
4941
4159
  }
4942
4160
  });
4943
4161
 
4944
4162
  // src/commands/nest/list.ts
4945
4163
  import { defineCommand as defineCommand37 } from "citty";
4946
- import consola32 from "consola";
4164
+ import consola33 from "consola";
4947
4165
  var listNestCommand = defineCommand37({
4948
4166
  meta: {
4949
4167
  name: "list",
@@ -4959,21 +4177,21 @@ var listNestCommand = defineCommand37({
4959
4177
  return;
4960
4178
  }
4961
4179
  if (reg.agents.length === 0) {
4962
- consola32.info("(no agents registered with this nest)");
4180
+ consola33.info("(no agents registered with this nest)");
4963
4181
  return;
4964
4182
  }
4965
- consola32.info(`${reg.agents.length} agent(s) registered with this nest:`);
4183
+ consola33.info(`${reg.agents.length} agent(s) registered with this nest:`);
4966
4184
  for (const a of reg.agents) {
4967
4185
  const bridge = a.bridge ? " bridge=on" : "";
4968
- consola32.info(` ${a.name.padEnd(16)} uid=${String(a.uid).padEnd(5)} home=${a.home}${bridge}`);
4186
+ consola33.info(` ${a.name.padEnd(16)} uid=${String(a.uid).padEnd(5)} home=${a.home}${bridge}`);
4969
4187
  }
4970
4188
  }
4971
4189
  });
4972
4190
 
4973
4191
  // src/commands/nest/spawn.ts
4974
- import { execFileSync as execFileSync16 } from "child_process";
4192
+ import { execFileSync as execFileSync10 } from "child_process";
4975
4193
  import { defineCommand as defineCommand38 } from "citty";
4976
- import consola33 from "consola";
4194
+ import consola34 from "consola";
4977
4195
  var spawnNestCommand = defineCommand38({
4978
4196
  meta: {
4979
4197
  name: "spawn",
@@ -5004,8 +4222,8 @@ var spawnNestCommand = defineCommand38({
5004
4222
  if (typeof args["bridge-base-url"] === "string") apesArgs.push("--bridge-base-url", args["bridge-base-url"]);
5005
4223
  if (typeof args["bridge-model"] === "string") apesArgs.push("--bridge-model", args["bridge-model"]);
5006
4224
  try {
5007
- execFileSync16("apes", apesArgs, { stdio: "inherit" });
5008
- consola33.success(`Nest will pick up ${name} on its next reconcile (\u22642s).`);
4225
+ execFileSync10("apes", apesArgs, { stdio: "inherit" });
4226
+ consola34.success(`Nest will pick up ${name} on its next reconcile (\u22642s).`);
5009
4227
  } catch (err) {
5010
4228
  const status = err.status ?? 1;
5011
4229
  throw new CliExit(status);
@@ -5015,7 +4233,7 @@ var spawnNestCommand = defineCommand38({
5015
4233
 
5016
4234
  // src/commands/nest/uninstall.ts
5017
4235
  import { defineCommand as defineCommand39 } from "citty";
5018
- import consola34 from "consola";
4236
+ import consola35 from "consola";
5019
4237
  var uninstallNestCommand = defineCommand39({
5020
4238
  meta: {
5021
4239
  name: "uninstall",
@@ -5023,8 +4241,8 @@ var uninstallNestCommand = defineCommand39({
5023
4241
  },
5024
4242
  async run() {
5025
4243
  await getHostPlatform().uninstallNestSupervisor();
5026
- consola34.success("Nest daemon stopped + supervisor unit removed");
5027
- consola34.info("Registry at ~/.openape/nest/agents.json kept \u2014 re-run `apes nest install` to resume supervision.");
4244
+ consola35.success("Nest daemon stopped + supervisor unit removed");
4245
+ consola35.info("Registry at ~/.openape/nest/agents.json kept \u2014 re-run `apes nest install` to resume supervision.");
5028
4246
  }
5029
4247
  });
5030
4248
 
@@ -5050,7 +4268,7 @@ import { defineCommand as defineCommand44 } from "citty";
5050
4268
 
5051
4269
  // src/commands/yolo/clear.ts
5052
4270
  import { defineCommand as defineCommand41 } from "citty";
5053
- import consola35 from "consola";
4271
+ import consola36 from "consola";
5054
4272
  var yoloClearCommand = defineCommand41({
5055
4273
  meta: {
5056
4274
  name: "clear",
@@ -5080,13 +4298,13 @@ var yoloClearCommand = defineCommand41({
5080
4298
  const text = await res.text().catch(() => "");
5081
4299
  throw new CliError(`DELETE /yolo-policy failed (${res.status}): ${text}`);
5082
4300
  }
5083
- consola35.success(`YOLO-policy cleared on ${email}`);
4301
+ consola36.success(`YOLO-policy cleared on ${email}`);
5084
4302
  }
5085
4303
  });
5086
4304
 
5087
4305
  // src/commands/yolo/set.ts
5088
4306
  import { defineCommand as defineCommand42 } from "citty";
5089
- import consola36 from "consola";
4307
+ import consola37 from "consola";
5090
4308
  var VALID_MODES = ["allow-list", "deny-list"];
5091
4309
  var yoloSetCommand = defineCommand42({
5092
4310
  meta: {
@@ -5136,12 +4354,12 @@ var yoloSetCommand = defineCommand42({
5136
4354
  const denyPatterns = parseList(args.deny);
5137
4355
  const denyRiskThreshold = args["deny-risk"] ?? null;
5138
4356
  const expiresAt = parseExpiresIn(args["expires-in"]);
5139
- consola36.info(`Setting YOLO-policy on ${email}`);
5140
- consola36.info(` mode: ${mode}`);
5141
- if (allowPatterns.length) consola36.info(` allow_patterns: ${allowPatterns.join(", ")}`);
5142
- if (denyPatterns.length) consola36.info(` deny_patterns: ${denyPatterns.join(", ")}`);
5143
- if (denyRiskThreshold) consola36.info(` deny_risk: ${denyRiskThreshold}`);
5144
- if (expiresAt) consola36.info(` expires_at: ${new Date(expiresAt * 1e3).toISOString()}`);
4357
+ consola37.info(`Setting YOLO-policy on ${email}`);
4358
+ consola37.info(` mode: ${mode}`);
4359
+ if (allowPatterns.length) consola37.info(` allow_patterns: ${allowPatterns.join(", ")}`);
4360
+ if (denyPatterns.length) consola37.info(` deny_patterns: ${denyPatterns.join(", ")}`);
4361
+ if (denyRiskThreshold) consola37.info(` deny_risk: ${denyRiskThreshold}`);
4362
+ if (expiresAt) consola37.info(` expires_at: ${new Date(expiresAt * 1e3).toISOString()}`);
5145
4363
  const url = `${idp}/api/users/${encodeURIComponent(email)}/yolo-policy`;
5146
4364
  const res = await fetch(url, {
5147
4365
  method: "PUT",
@@ -5161,7 +4379,7 @@ var yoloSetCommand = defineCommand42({
5161
4379
  const text = await res.text().catch(() => "");
5162
4380
  throw new CliError(`PUT /yolo-policy failed (${res.status}): ${text}`);
5163
4381
  }
5164
- consola36.success(`YOLO-policy applied to ${email}`);
4382
+ consola37.success(`YOLO-policy applied to ${email}`);
5165
4383
  }
5166
4384
  });
5167
4385
  function parseList(s) {
@@ -5180,7 +4398,7 @@ function parseExpiresIn(s) {
5180
4398
 
5181
4399
  // src/commands/yolo/show.ts
5182
4400
  import { defineCommand as defineCommand43 } from "citty";
5183
- import consola37 from "consola";
4401
+ import consola38 from "consola";
5184
4402
  var yoloShowCommand = defineCommand43({
5185
4403
  meta: {
5186
4404
  name: "show",
@@ -5218,12 +4436,12 @@ var yoloShowCommand = defineCommand43({
5218
4436
  console.log(JSON.stringify(policy, null, 2));
5219
4437
  return;
5220
4438
  }
5221
- consola37.info(`YOLO-policy for ${email}`);
5222
- consola37.info(` mode: ${policy.mode}`);
5223
- consola37.info(` allow_patterns: ${policy.allowPatterns.length ? policy.allowPatterns.join(", ") : "(none)"}`);
5224
- consola37.info(` deny_patterns: ${policy.denyPatterns.length ? policy.denyPatterns.join(", ") : "(none)"}`);
5225
- consola37.info(` deny_risk: ${policy.denyRiskThreshold ?? "(none)"}`);
5226
- consola37.info(` expires_at: ${policy.expiresAt ? new Date(policy.expiresAt * 1e3).toISOString() : "(never)"}`);
4439
+ consola38.info(`YOLO-policy for ${email}`);
4440
+ consola38.info(` mode: ${policy.mode}`);
4441
+ consola38.info(` allow_patterns: ${policy.allowPatterns.length ? policy.allowPatterns.join(", ") : "(none)"}`);
4442
+ consola38.info(` deny_patterns: ${policy.denyPatterns.length ? policy.denyPatterns.join(", ") : "(none)"}`);
4443
+ consola38.info(` deny_risk: ${policy.denyRiskThreshold ?? "(none)"}`);
4444
+ consola38.info(` expires_at: ${policy.expiresAt ? new Date(policy.expiresAt * 1e3).toISOString() : "(never)"}`);
5227
4445
  }
5228
4446
  });
5229
4447
 
@@ -5242,7 +4460,7 @@ var yoloCommand = defineCommand44({
5242
4460
 
5243
4461
  // src/commands/adapter/index.ts
5244
4462
  import { defineCommand as defineCommand45 } from "citty";
5245
- import consola38 from "consola";
4463
+ import consola39 from "consola";
5246
4464
  var adapterCommand = defineCommand45({
5247
4465
  meta: {
5248
4466
  name: "adapter",
@@ -5280,7 +4498,7 @@ var adapterCommand = defineCommand45({
5280
4498
  `);
5281
4499
  return;
5282
4500
  }
5283
- consola38.info(`Registry: ${index2.adapters.length} adapters (${index2.generated_at})`);
4501
+ consola39.info(`Registry: ${index2.adapters.length} adapters (${index2.generated_at})`);
5284
4502
  for (const a of index2.adapters) {
5285
4503
  const installed = isInstalled(a.id, false) ? " [installed]" : "";
5286
4504
  console.log(` ${a.id.padEnd(12)} ${a.name.padEnd(24)} ${a.category}${installed}`);
@@ -5302,7 +4520,7 @@ var adapterCommand = defineCommand45({
5302
4520
  return;
5303
4521
  }
5304
4522
  if (local.length === 0) {
5305
- consola38.info("No adapters installed. Use `apes adapter list --remote` to see available adapters.");
4523
+ consola39.info("No adapters installed. Use `apes adapter list --remote` to see available adapters.");
5306
4524
  return;
5307
4525
  }
5308
4526
  for (const a of local) {
@@ -5339,20 +4557,20 @@ var adapterCommand = defineCommand45({
5339
4557
  for (const id of ids) {
5340
4558
  const entry = findAdapter(index, id);
5341
4559
  if (!entry) {
5342
- consola38.error(`Adapter "${id}" not found in registry. Use \`apes adapter search ${id}\` to search.`);
4560
+ consola39.error(`Adapter "${id}" not found in registry. Use \`apes adapter search ${id}\` to search.`);
5343
4561
  continue;
5344
4562
  }
5345
4563
  const conflicts = findConflictingAdapters(entry.executable, id);
5346
4564
  if (conflicts.length > 0) {
5347
4565
  for (const c of conflicts) {
5348
- consola38.warn(`Conflicting adapter found: ${c.path} (id: ${c.adapterId}, executable: ${c.executable})`);
5349
- consola38.warn(` Remove it with: apes adapter remove ${c.adapterId}`);
4566
+ consola39.warn(`Conflicting adapter found: ${c.path} (id: ${c.adapterId}, executable: ${c.executable})`);
4567
+ consola39.warn(` Remove it with: apes adapter remove ${c.adapterId}`);
5350
4568
  }
5351
4569
  }
5352
4570
  const result = await installAdapter(entry, { local });
5353
4571
  const verb = result.updated ? "Updated" : "Installed";
5354
- consola38.success(`${verb} ${result.id} \u2192 ${result.path}`);
5355
- consola38.info(`Digest: ${result.digest}`);
4572
+ consola39.success(`${verb} ${result.id} \u2192 ${result.path}`);
4573
+ consola39.info(`Digest: ${result.digest}`);
5356
4574
  }
5357
4575
  }
5358
4576
  }),
@@ -5379,9 +4597,9 @@ var adapterCommand = defineCommand45({
5379
4597
  let failed = false;
5380
4598
  for (const id of ids) {
5381
4599
  if (removeAdapter(id, local)) {
5382
- consola38.success(`Removed adapter: ${id}`);
4600
+ consola39.success(`Removed adapter: ${id}`);
5383
4601
  } else {
5384
- consola38.error(`Adapter "${id}" is not installed${local ? " locally" : ""}`);
4602
+ consola39.error(`Adapter "${id}" is not installed${local ? " locally" : ""}`);
5385
4603
  failed = true;
5386
4604
  }
5387
4605
  }
@@ -5463,7 +4681,7 @@ var adapterCommand = defineCommand45({
5463
4681
  return;
5464
4682
  }
5465
4683
  if (results.length === 0) {
5466
- consola38.info(`No adapters matching "${query}"`);
4684
+ consola39.info(`No adapters matching "${query}"`);
5467
4685
  return;
5468
4686
  }
5469
4687
  for (const a of results) {
@@ -5498,29 +4716,29 @@ var adapterCommand = defineCommand45({
5498
4716
  const targetId = args.id ? String(args.id) : void 0;
5499
4717
  const targets = targetId ? [targetId] : index.adapters.map((a) => a.id).filter((id) => isInstalled(id, false));
5500
4718
  if (targets.length === 0) {
5501
- consola38.info("No adapters installed to update.");
4719
+ consola39.info("No adapters installed to update.");
5502
4720
  return;
5503
4721
  }
5504
4722
  for (const id of targets) {
5505
4723
  const entry = findAdapter(index, id);
5506
4724
  if (!entry) {
5507
- consola38.warn(`${id}: not found in registry, skipping`);
4725
+ consola39.warn(`${id}: not found in registry, skipping`);
5508
4726
  continue;
5509
4727
  }
5510
4728
  const localDigest = getInstalledDigest(id, false);
5511
4729
  if (localDigest === entry.digest) {
5512
- consola38.info(`${id}: already up to date`);
4730
+ consola39.info(`${id}: already up to date`);
5513
4731
  continue;
5514
4732
  }
5515
4733
  if (localDigest && !args.yes) {
5516
- consola38.warn(`${id}: digest will change \u2014 existing grants for this adapter will be invalidated`);
5517
- consola38.info(` Old: ${localDigest}`);
5518
- consola38.info(` New: ${entry.digest}`);
5519
- consola38.info(" Use --yes to confirm");
4734
+ consola39.warn(`${id}: digest will change \u2014 existing grants for this adapter will be invalidated`);
4735
+ consola39.info(` Old: ${localDigest}`);
4736
+ consola39.info(` New: ${entry.digest}`);
4737
+ consola39.info(" Use --yes to confirm");
5520
4738
  continue;
5521
4739
  }
5522
4740
  const result = await installAdapter(entry);
5523
- consola38.success(`Updated ${result.id} \u2192 ${result.path}`);
4741
+ consola39.success(`Updated ${result.id} \u2192 ${result.path}`);
5524
4742
  }
5525
4743
  }
5526
4744
  }),
@@ -5557,7 +4775,7 @@ var adapterCommand = defineCommand45({
5557
4775
  if (!localDigest)
5558
4776
  throw new Error(`Adapter "${id}" is not installed${local ? " locally" : ""}`);
5559
4777
  if (localDigest === entry.digest) {
5560
- consola38.success(`${id}: digest matches registry`);
4778
+ consola39.success(`${id}: digest matches registry`);
5561
4779
  } else {
5562
4780
  console.log(` Local: ${localDigest}`);
5563
4781
  console.log(` Registry: ${entry.digest}`);
@@ -5569,14 +4787,13 @@ var adapterCommand = defineCommand45({
5569
4787
  });
5570
4788
 
5571
4789
  // src/commands/run.ts
5572
- import { execFileSync as execFileSync17 } from "child_process";
5573
- import { hostname as hostname6 } from "os";
4790
+ import { execFileSync as execFileSync11 } from "child_process";
4791
+ import { hostname as hostname5 } from "os";
5574
4792
  import { basename } from "path";
5575
4793
  import { defineCommand as defineCommand46 } from "citty";
5576
- import consola39 from "consola";
4794
+ import consola40 from "consola";
5577
4795
  function resolveRunAsTarget(runAs) {
5578
4796
  if (!runAs) return runAs;
5579
- if (!isDarwin2()) return runAs;
5580
4797
  if (!AGENT_NAME_REGEX.test(runAs)) return runAs;
5581
4798
  if (runAs.startsWith("openape-agent-")) return runAs;
5582
4799
  const platform = getHostPlatform();
@@ -5630,7 +4847,7 @@ function printPendingGrantInfo(grant, idp) {
5630
4847
  const statusCmd = `apes grants status ${grant.id}`;
5631
4848
  const executeCmd = `apes grants run ${grant.id}`;
5632
4849
  if (mode === "human") {
5633
- consola39.success(`Grant ${grant.id} created \u2014 awaiting your approval`);
4850
+ consola40.success(`Grant ${grant.id} created \u2014 awaiting your approval`);
5634
4851
  console.log(` Approve in browser: ${approveUrl}`);
5635
4852
  console.log(` Check status: ${statusCmd}`);
5636
4853
  console.log(` Run after approval: ${executeCmd}`);
@@ -5640,7 +4857,7 @@ function printPendingGrantInfo(grant, idp) {
5640
4857
  return;
5641
4858
  }
5642
4859
  const maxMin = getPollMaxMinutes();
5643
- consola39.success(`Grant ${grant.id} created (pending approval)`);
4860
+ consola40.success(`Grant ${grant.id} created (pending approval)`);
5644
4861
  console.log(` Approve: ${approveUrl}`);
5645
4862
  console.log(` Status: ${statusCmd} [--json]`);
5646
4863
  console.log(` Execute: ${executeCmd} --wait`);
@@ -5754,7 +4971,7 @@ async function runShellMode(command, args) {
5754
4971
  const adapterHandled = await tryAdapterModeFromShell(command, idp, args);
5755
4972
  if (adapterHandled) return;
5756
4973
  const grantsUrl = await getGrantsEndpoint(idp);
5757
- const targetHost = args.host || hostname6();
4974
+ const targetHost = args.host || hostname5();
5758
4975
  try {
5759
4976
  const grants = await apiFetch(
5760
4977
  `${grantsUrl}?requester=${encodeURIComponent(auth.email)}&status=approved&limit=20`
@@ -5768,7 +4985,7 @@ async function runShellMode(command, args) {
5768
4985
  }
5769
4986
  } catch {
5770
4987
  }
5771
- consola39.info(`Requesting ape-shell session grant on ${targetHost}`);
4988
+ consola40.info(`Requesting ape-shell session grant on ${targetHost}`);
5772
4989
  const grant = await apiFetch(grantsUrl, {
5773
4990
  method: "POST",
5774
4991
  body: {
@@ -5788,8 +5005,8 @@ async function runShellMode(command, args) {
5788
5005
  host: targetHost
5789
5006
  });
5790
5007
  if (shouldWaitForGrant(args)) {
5791
- consola39.info(`Grant requested: ${grant.id}`);
5792
- consola39.info("Waiting for approval...");
5008
+ consola40.info(`Grant requested: ${grant.id}`);
5009
+ consola40.info("Waiting for approval...");
5793
5010
  const maxWait = 3e5;
5794
5011
  const interval = 3e3;
5795
5012
  const start = Date.now();
@@ -5820,13 +5037,13 @@ async function tryAdapterModeFromShell(command, idp, args) {
5820
5037
  try {
5821
5038
  resolved = await resolveCommand(loaded, [normalizedExecutable, ...parsed.argv]);
5822
5039
  } catch (err) {
5823
- consola39.debug(`ape-shell: adapter resolve failed for "${parsed.raw}":`, err);
5040
+ consola40.debug(`ape-shell: adapter resolve failed for "${parsed.raw}":`, err);
5824
5041
  return false;
5825
5042
  }
5826
5043
  try {
5827
5044
  const existingGrantId = await findExistingGrant(resolved, idp);
5828
5045
  if (existingGrantId) {
5829
- consola39.info(`Reusing grant ${existingGrantId} for: ${resolved.detail.display}`);
5046
+ consola40.info(`Reusing grant ${existingGrantId} for: ${resolved.detail.display}`);
5830
5047
  const token = await fetchGrantToken(idp, existingGrantId);
5831
5048
  await verifyAndExecute(token, resolved, existingGrantId);
5832
5049
  return true;
@@ -5834,7 +5051,7 @@ async function tryAdapterModeFromShell(command, idp, args) {
5834
5051
  } catch {
5835
5052
  }
5836
5053
  const approval = args.approval ?? "once";
5837
- consola39.info(`Requesting grant for: ${resolved.detail.display}`);
5054
+ consola40.info(`Requesting grant for: ${resolved.detail.display}`);
5838
5055
  const grant = await createShapesGrant(resolved, {
5839
5056
  idp,
5840
5057
  approval,
@@ -5842,19 +5059,19 @@ async function tryAdapterModeFromShell(command, idp, args) {
5842
5059
  });
5843
5060
  if (grant.similar_grants?.similar_grants?.length) {
5844
5061
  const n = grant.similar_grants.similar_grants.length;
5845
- consola39.info("");
5846
- consola39.info(` Similar grant(s) found (${n}). Your approver can extend an existing grant to cover this request.`);
5062
+ consola40.info("");
5063
+ consola40.info(` Similar grant(s) found (${n}). Your approver can extend an existing grant to cover this request.`);
5847
5064
  }
5848
5065
  notifyGrantPending({
5849
5066
  grantId: grant.id,
5850
5067
  approveUrl: `${idp}/grant-approval?grant_id=${grant.id}`,
5851
5068
  command: resolved.detail?.display || parsed?.raw || "unknown",
5852
5069
  audience: resolved.adapter?.cli?.audience ?? "shapes",
5853
- host: args.host || hostname6()
5070
+ host: args.host || hostname5()
5854
5071
  });
5855
5072
  if (shouldWaitForGrant(args)) {
5856
- consola39.info(`Grant requested: ${grant.id}`);
5857
- consola39.info(`Approve at: ${idp}/grant-approval?grant_id=${grant.id}`);
5073
+ consola40.info(`Grant requested: ${grant.id}`);
5074
+ consola40.info(`Approve at: ${idp}/grant-approval?grant_id=${grant.id}`);
5858
5075
  const status = await waitForGrantStatus(idp, grant.id);
5859
5076
  if (status !== "approved")
5860
5077
  throw new CliError(`Grant ${status}`);
@@ -5870,7 +5087,7 @@ function execShellCommand(command) {
5870
5087
  throw new CliError("No command to execute");
5871
5088
  try {
5872
5089
  const { APES_SHELL_WRAPPER: _wrapperMarker, ...inheritedEnv } = process.env;
5873
- execFileSync17(command[0], command.slice(1), {
5090
+ execFileSync11(command[0], command.slice(1), {
5874
5091
  stdio: "inherit",
5875
5092
  env: inheritedEnv
5876
5093
  });
@@ -5928,7 +5145,7 @@ async function runAdapterMode(command, rawArgs, args) {
5928
5145
  try {
5929
5146
  const existingGrantId = await findExistingGrant(resolved, idp);
5930
5147
  if (existingGrantId) {
5931
- consola39.info(`Reusing existing grant: ${existingGrantId}`);
5148
+ consola40.info(`Reusing existing grant: ${existingGrantId}`);
5932
5149
  const token = await fetchGrantToken(idp, existingGrantId);
5933
5150
  await verifyAndExecute(token, resolved, existingGrantId);
5934
5151
  return;
@@ -5942,17 +5159,17 @@ async function runAdapterMode(command, rawArgs, args) {
5942
5159
  });
5943
5160
  if (grant.similar_grants?.similar_grants?.length) {
5944
5161
  const n = grant.similar_grants.similar_grants.length;
5945
- consola39.info("");
5946
- consola39.info(` Similar grant(s) found (${n}). Your approver can extend an existing grant to cover this request.`);
5162
+ consola40.info("");
5163
+ consola40.info(` Similar grant(s) found (${n}). Your approver can extend an existing grant to cover this request.`);
5947
5164
  if (grant.similar_grants.widened_details?.length) {
5948
5165
  const wider = grant.similar_grants.widened_details.map((d) => d.permission).join(", ");
5949
- consola39.info(` Broader scope: ${wider}`);
5166
+ consola40.info(` Broader scope: ${wider}`);
5950
5167
  }
5951
- consola39.info("");
5168
+ consola40.info("");
5952
5169
  }
5953
5170
  if (shouldWaitForGrant(args)) {
5954
- consola39.info(`Grant requested: ${grant.id}`);
5955
- consola39.info(`Approve at: ${idp}/grant-approval?grant_id=${grant.id}`);
5171
+ consola40.info(`Grant requested: ${grant.id}`);
5172
+ consola40.info(`Approve at: ${idp}/grant-approval?grant_id=${grant.id}`);
5956
5173
  const status = await waitForGrantStatus(idp, grant.id);
5957
5174
  if (status !== "approved")
5958
5175
  throw new Error(`Grant ${status}`);
@@ -5971,7 +5188,7 @@ async function runAudienceMode(audience, action, args, commandArgv) {
5971
5188
  const idp = getIdpUrl(args.idp);
5972
5189
  const grantsUrl = await getGrantsEndpoint(idp);
5973
5190
  const command = commandArgv ?? action.split(" ");
5974
- const targetHost = args.host || hostname6();
5191
+ const targetHost = args.host || hostname5();
5975
5192
  const runAs = resolveRunAsTarget(args.as ?? void 0);
5976
5193
  const reusableId = await findReusableAudienceGrant({
5977
5194
  grantsUrl,
@@ -5985,7 +5202,7 @@ async function runAudienceMode(audience, action, args, commandArgv) {
5985
5202
  const { authz_jwt: authz_jwt2 } = await apiFetch(`${grantsUrl}/${reusableId}/token`, { method: "POST" });
5986
5203
  return executeWithGrantToken({ audience, command, args, token: authz_jwt2 });
5987
5204
  }
5988
- consola39.info(`Requesting ${audience} grant on ${targetHost}: ${command.join(" ")}`);
5205
+ consola40.info(`Requesting ${audience} grant on ${targetHost}: ${command.join(" ")}`);
5989
5206
  const grant = await apiFetch(grantsUrl, {
5990
5207
  method: "POST",
5991
5208
  body: {
@@ -6002,9 +5219,9 @@ async function runAudienceMode(audience, action, args, commandArgv) {
6002
5219
  printPendingGrantInfo(grant, idp);
6003
5220
  throw new CliExit(getAsyncExitCode());
6004
5221
  }
6005
- consola39.success(`Grant requested: ${grant.id}`);
6006
- consola39.info(`Approve at: ${idp}/grant-approval?grant_id=${grant.id}`);
6007
- consola39.info("Waiting for approval...");
5222
+ consola40.success(`Grant requested: ${grant.id}`);
5223
+ consola40.info(`Approve at: ${idp}/grant-approval?grant_id=${grant.id}`);
5224
+ consola40.info("Waiting for approval...");
6008
5225
  const maxWait = 15 * 60 * 1e3;
6009
5226
  const interval = 3e3;
6010
5227
  const start = Date.now();
@@ -6012,7 +5229,7 @@ async function runAudienceMode(audience, action, args, commandArgv) {
6012
5229
  while (Date.now() - start < maxWait) {
6013
5230
  const status = await apiFetch(`${grantsUrl}/${grant.id}`);
6014
5231
  if (status.status === "approved") {
6015
- consola39.success("Grant approved!");
5232
+ consola40.success("Grant approved!");
6016
5233
  approved = true;
6017
5234
  break;
6018
5235
  }
@@ -6027,7 +5244,7 @@ async function runAudienceMode(audience, action, args, commandArgv) {
6027
5244
  `Grant approval timed out after ${minutes} min (still pending). Check your DDISA inbox at ${idp}/grant-approval?grant_id=${grant.id} \u2014 if approved later, re-run the same \`apes run\` command and it will reuse the grant.`
6028
5245
  );
6029
5246
  }
6030
- consola39.info("Fetching grant token...");
5247
+ consola40.info("Fetching grant token...");
6031
5248
  const { authz_jwt } = await apiFetch(`${grantsUrl}/${grant.id}/token`, {
6032
5249
  method: "POST"
6033
5250
  });
@@ -6036,10 +5253,10 @@ async function runAudienceMode(audience, action, args, commandArgv) {
6036
5253
  function executeWithGrantToken(opts) {
6037
5254
  const { audience, command, args, token } = opts;
6038
5255
  if (audience === "escapes") {
6039
- consola39.info(`Executing: ${command.join(" ")}`);
5256
+ consola40.info(`Executing: ${command.join(" ")}`);
6040
5257
  try {
6041
5258
  const { APES_SHELL_WRAPPER: _wrapperMarker, ...inheritedEnv } = process.env;
6042
- execFileSync17(args["escapes-path"] || "escapes", ["--grant", token, "--", ...command], {
5259
+ execFileSync11(args["escapes-path"] || "escapes", ["--grant", token, "--", ...command], {
6043
5260
  stdio: "inherit",
6044
5261
  env: inheritedEnv
6045
5262
  });
@@ -6075,11 +5292,11 @@ async function findReusableAudienceGrant(opts) {
6075
5292
 
6076
5293
  // src/commands/proxy.ts
6077
5294
  import { spawn as spawn2 } from "child_process";
6078
- import { existsSync as existsSync20 } from "fs";
6079
- import { homedir as homedir14 } from "os";
6080
- import { join as join18 } from "path";
5295
+ import { existsSync as existsSync18 } from "fs";
5296
+ import { homedir as homedir13 } from "os";
5297
+ import { join as join15 } from "path";
6081
5298
  import { defineCommand as defineCommand47 } from "citty";
6082
- import consola40 from "consola";
5299
+ import consola41 from "consola";
6083
5300
 
6084
5301
  // src/proxy/config.ts
6085
5302
  function buildDefaultProxyConfigToml(opts) {
@@ -6113,10 +5330,10 @@ note = "VPC-internal hostname suffix"
6113
5330
 
6114
5331
  // src/proxy/local-proxy.ts
6115
5332
  import { spawn } from "child_process";
6116
- import { mkdtempSync as mkdtempSync4, rmSync as rmSync6, writeFileSync as writeFileSync11 } from "fs";
5333
+ import { mkdtempSync as mkdtempSync2, rmSync as rmSync4, writeFileSync as writeFileSync10 } from "fs";
6117
5334
  import { createRequire } from "module";
6118
- import { tmpdir as tmpdir4 } from "os";
6119
- import { dirname as dirname4, join as join16, resolve as resolve3 } from "path";
5335
+ import { tmpdir as tmpdir2 } from "os";
5336
+ import { dirname as dirname4, join as join13, resolve as resolve4 } from "path";
6120
5337
  var require2 = createRequire(import.meta.url);
6121
5338
  function findProxyBin() {
6122
5339
  const pkgPath = require2.resolve("@openape/proxy/package.json");
@@ -6125,12 +5342,12 @@ function findProxyBin() {
6125
5342
  if (!binRel) {
6126
5343
  throw new Error("@openape/proxy is missing the openape-proxy bin entry");
6127
5344
  }
6128
- return resolve3(dirname4(pkgPath), binRel);
5345
+ return resolve4(dirname4(pkgPath), binRel);
6129
5346
  }
6130
5347
  async function startEphemeralProxy(configToml) {
6131
- const tmpDir = mkdtempSync4(join16(tmpdir4(), "openape-proxy-"));
6132
- const configPath = join16(tmpDir, "config.toml");
6133
- writeFileSync11(configPath, configToml, { mode: 384 });
5348
+ const tmpDir = mkdtempSync2(join13(tmpdir2(), "openape-proxy-"));
5349
+ const configPath = join13(tmpDir, "config.toml");
5350
+ writeFileSync10(configPath, configToml, { mode: 384 });
6134
5351
  const binPath = findProxyBin();
6135
5352
  const child = spawn(process.execPath, [binPath, "-c", configPath], {
6136
5353
  stdio: ["ignore", "pipe", "pipe"],
@@ -6138,7 +5355,7 @@ async function startEphemeralProxy(configToml) {
6138
5355
  });
6139
5356
  const cleanupTmp = () => {
6140
5357
  try {
6141
- rmSync6(tmpDir, { recursive: true, force: true });
5358
+ rmSync4(tmpDir, { recursive: true, force: true });
6142
5359
  } catch {
6143
5360
  }
6144
5361
  };
@@ -6212,9 +5429,9 @@ function waitForListenLine(child) {
6212
5429
  }
6213
5430
 
6214
5431
  // src/proxy/trust-bundle.ts
6215
- import { existsSync as existsSync19, mkdtempSync as mkdtempSync5, readFileSync as readFileSync17, rmdirSync, unlinkSync as unlinkSync3, writeFileSync as writeFileSync12 } from "fs";
6216
- import { tmpdir as tmpdir5 } from "os";
6217
- import { join as join17 } from "path";
5432
+ import { existsSync as existsSync17, mkdtempSync as mkdtempSync3, readFileSync as readFileSync15, rmdirSync, unlinkSync as unlinkSync2, writeFileSync as writeFileSync11 } from "fs";
5433
+ import { tmpdir as tmpdir3 } from "os";
5434
+ import { join as join14 } from "path";
6218
5435
  var CANDIDATES = [
6219
5436
  "/etc/ssl/cert.pem",
6220
5437
  // macOS
@@ -6227,25 +5444,25 @@ var CANDIDATES = [
6227
5444
  ];
6228
5445
  function detectSystemCaPath() {
6229
5446
  for (const p of CANDIDATES) {
6230
- if (existsSync19(p)) return p;
5447
+ if (existsSync17(p)) return p;
6231
5448
  }
6232
5449
  throw new Error(
6233
5450
  `Could not locate a system CA bundle. Tried: ${CANDIDATES.join(", ")}. Set NODE_EXTRA_CA_CERTS yourself or pass --allow-no-system-ca.`
6234
5451
  );
6235
5452
  }
6236
5453
  function buildTrustBundle(opts) {
6237
- const dir = mkdtempSync5(join17(tmpdir5(), "openape-trust-"));
6238
- const path2 = join17(dir, "bundle.pem");
6239
- const sys = readFileSync17(opts.systemCaPath, "utf-8");
6240
- const local = readFileSync17(opts.localCaPath, "utf-8");
6241
- writeFileSync12(path2, `${sys.trimEnd()}
5454
+ const dir = mkdtempSync3(join14(tmpdir3(), "openape-trust-"));
5455
+ const path2 = join14(dir, "bundle.pem");
5456
+ const sys = readFileSync15(opts.systemCaPath, "utf-8");
5457
+ const local = readFileSync15(opts.localCaPath, "utf-8");
5458
+ writeFileSync11(path2, `${sys.trimEnd()}
6242
5459
  ${local.trimEnd()}
6243
5460
  `, { mode: 384 });
6244
5461
  return {
6245
5462
  path: path2,
6246
5463
  cleanup: () => {
6247
5464
  try {
6248
- unlinkSync3(path2);
5465
+ unlinkSync2(path2);
6249
5466
  } catch {
6250
5467
  }
6251
5468
  try {
@@ -6267,7 +5484,7 @@ function resolveProxyConfigOptions() {
6267
5484
  77
6268
5485
  );
6269
5486
  }
6270
- consola40.info(`[apes proxy] IdP-mediated mode \u2014 agent=${auth.email}, idp=${auth.idp}`);
5487
+ consola41.info(`[apes proxy] IdP-mediated mode \u2014 agent=${auth.email}, idp=${auth.idp}`);
6271
5488
  return { agentEmail: auth.email, idpUrl: auth.idp, mediated: true };
6272
5489
  }
6273
5490
  var proxyCommand = defineCommand47({
@@ -6294,9 +5511,9 @@ var proxyCommand = defineCommand47({
6294
5511
  let close = null;
6295
5512
  if (reuseHostPort) {
6296
5513
  proxyUrl = `http://${reuseHostPort}`;
6297
- consola40.info(`[apes proxy] using long-running daemon at ${proxyUrl}`);
6298
- const localCaPath = join18(homedir14(), ".openape", "proxy", "ca.crt");
6299
- if (!existsSync20(localCaPath)) {
5514
+ consola41.info(`[apes proxy] using long-running daemon at ${proxyUrl}`);
5515
+ const localCaPath = join15(homedir13(), ".openape", "proxy", "ca.crt");
5516
+ if (!existsSync18(localCaPath)) {
6300
5517
  throw new CliError(
6301
5518
  `OPENAPE_PROXY is set but no local CA found at ${localCaPath}. Start the daemon (sudo -u <agent> apes proxy --global < secrets.toml) first.`
6302
5519
  );
@@ -6305,15 +5522,15 @@ var proxyCommand = defineCommand47({
6305
5522
  systemCaPath: detectSystemCaPath(),
6306
5523
  localCaPath
6307
5524
  });
6308
- consola40.debug(`[apes proxy] trust bundle: ${bundle.path}`);
5525
+ consola41.debug(`[apes proxy] trust bundle: ${bundle.path}`);
6309
5526
  } else if (reuseUrl) {
6310
5527
  proxyUrl = reuseUrl;
6311
- consola40.info(`[apes proxy] reusing existing proxy at ${proxyUrl}`);
5528
+ consola41.info(`[apes proxy] reusing existing proxy at ${proxyUrl}`);
6312
5529
  } else {
6313
5530
  const ephemeral = await startEphemeralProxy(buildDefaultProxyConfigToml(resolveProxyConfigOptions()));
6314
5531
  proxyUrl = ephemeral.url;
6315
5532
  close = ephemeral.close;
6316
- consola40.info(`[apes proxy] started ephemeral proxy at ${proxyUrl}`);
5533
+ consola41.info(`[apes proxy] started ephemeral proxy at ${proxyUrl}`);
6317
5534
  }
6318
5535
  const noProxy = process.env.NO_PROXY ?? process.env.no_proxy ?? "127.0.0.1,localhost";
6319
5536
  const childEnv = {
@@ -6354,7 +5571,7 @@ var proxyCommand = defineCommand47({
6354
5571
  else resolveExit(code ?? 0);
6355
5572
  });
6356
5573
  child.once("error", (err) => {
6357
- consola40.error(`[apes proxy] failed to spawn '${wrapped[0]}':`, err.message);
5574
+ consola41.error(`[apes proxy] failed to spawn '${wrapped[0]}':`, err.message);
6358
5575
  resolveExit(127);
6359
5576
  });
6360
5577
  });
@@ -6411,7 +5628,7 @@ var explainCommand = defineCommand48({
6411
5628
 
6412
5629
  // src/commands/config/get.ts
6413
5630
  import { defineCommand as defineCommand49 } from "citty";
6414
- import consola41 from "consola";
5631
+ import consola42 from "consola";
6415
5632
  var configGetCommand = defineCommand49({
6416
5633
  meta: {
6417
5634
  name: "get",
@@ -6432,7 +5649,7 @@ var configGetCommand = defineCommand49({
6432
5649
  if (idp)
6433
5650
  console.log(idp);
6434
5651
  else
6435
- consola41.info("No IdP configured.");
5652
+ consola42.info("No IdP configured.");
6436
5653
  break;
6437
5654
  }
6438
5655
  case "email": {
@@ -6440,7 +5657,7 @@ var configGetCommand = defineCommand49({
6440
5657
  if (auth?.email)
6441
5658
  console.log(auth.email);
6442
5659
  else
6443
- consola41.info("Not logged in.");
5660
+ consola42.info("Not logged in.");
6444
5661
  break;
6445
5662
  }
6446
5663
  default: {
@@ -6453,7 +5670,7 @@ var configGetCommand = defineCommand49({
6453
5670
  if (sectionObj && field in sectionObj) {
6454
5671
  console.log(sectionObj[field]);
6455
5672
  } else {
6456
- consola41.info(`Key "${key}" not set.`);
5673
+ consola42.info(`Key "${key}" not set.`);
6457
5674
  }
6458
5675
  } else {
6459
5676
  throw new CliError(`Unknown key: "${key}". Use: idp, email, defaults.idp, defaults.approval, agent.key, agent.email`);
@@ -6465,7 +5682,7 @@ var configGetCommand = defineCommand49({
6465
5682
 
6466
5683
  // src/commands/config/set.ts
6467
5684
  import { defineCommand as defineCommand50 } from "citty";
6468
- import consola42 from "consola";
5685
+ import consola43 from "consola";
6469
5686
  var configSetCommand = defineCommand50({
6470
5687
  meta: {
6471
5688
  name: "set",
@@ -6502,7 +5719,7 @@ var configSetCommand = defineCommand50({
6502
5719
  throw new CliError(`Unknown section: "${section}". Use: defaults, agent`);
6503
5720
  }
6504
5721
  saveConfig(config);
6505
- consola42.success(`Set ${key} = ${value}`);
5722
+ consola43.success(`Set ${key} = ${value}`);
6506
5723
  }
6507
5724
  });
6508
5725
 
@@ -6638,42 +5855,42 @@ var mcpCommand = defineCommand52({
6638
5855
  if (transport !== "stdio" && transport !== "sse") {
6639
5856
  throw new Error('Transport must be "stdio" or "sse"');
6640
5857
  }
6641
- const { startMcpServer } = await import("./server-FB7FC2NT.js");
5858
+ const { startMcpServer } = await import("./server-MDXOGP2U.js");
6642
5859
  await startMcpServer(transport, port);
6643
5860
  }
6644
5861
  });
6645
5862
 
6646
5863
  // src/commands/init/index.ts
6647
- import { existsSync as existsSync21, copyFileSync, writeFileSync as writeFileSync13 } from "fs";
5864
+ import { existsSync as existsSync19, copyFileSync, writeFileSync as writeFileSync12 } from "fs";
6648
5865
  import { randomBytes } from "crypto";
6649
- import { execFileSync as execFileSync18 } from "child_process";
6650
- import { join as join19 } from "path";
5866
+ import { execFileSync as execFileSync12 } from "child_process";
5867
+ import { join as join16 } from "path";
6651
5868
  import { defineCommand as defineCommand53 } from "citty";
6652
- import consola43 from "consola";
5869
+ import consola44 from "consola";
6653
5870
  var DEFAULT_IDP_URL = "https://id.openape.at";
6654
5871
  async function downloadTemplate(repo, targetDir) {
6655
5872
  const { downloadTemplate: gigetDownload } = await import("giget");
6656
5873
  await gigetDownload(`gh:${repo}`, { dir: targetDir, force: false });
6657
5874
  }
6658
5875
  function installDeps(dir) {
6659
- const hasLockFile = (name) => existsSync21(join19(dir, name));
5876
+ const hasLockFile = (name) => existsSync19(join16(dir, name));
6660
5877
  if (hasLockFile("pnpm-lock.yaml")) {
6661
- execFileSync18("pnpm", ["install"], { cwd: dir, stdio: "inherit" });
5878
+ execFileSync12("pnpm", ["install"], { cwd: dir, stdio: "inherit" });
6662
5879
  } else if (hasLockFile("bun.lockb")) {
6663
- execFileSync18("bun", ["install"], { cwd: dir, stdio: "inherit" });
5880
+ execFileSync12("bun", ["install"], { cwd: dir, stdio: "inherit" });
6664
5881
  } else {
6665
- execFileSync18("npm", ["install"], { cwd: dir, stdio: "inherit" });
5882
+ execFileSync12("npm", ["install"], { cwd: dir, stdio: "inherit" });
6666
5883
  }
6667
5884
  }
6668
5885
  async function promptChoice(message, choices) {
6669
- const result = await consola43.prompt(message, { type: "select", options: choices });
5886
+ const result = await consola44.prompt(message, { type: "select", options: choices });
6670
5887
  if (typeof result === "symbol") {
6671
5888
  throw new CliExit(0);
6672
5889
  }
6673
5890
  return result;
6674
5891
  }
6675
5892
  async function promptText(message, defaultValue) {
6676
- const result = await consola43.prompt(message, { type: "text", default: defaultValue, placeholder: defaultValue });
5893
+ const result = await consola44.prompt(message, { type: "text", default: defaultValue, placeholder: defaultValue });
6677
5894
  if (typeof result === "symbol") {
6678
5895
  throw new CliExit(0);
6679
5896
  }
@@ -6721,23 +5938,23 @@ var initCommand = defineCommand53({
6721
5938
  });
6722
5939
  async function initSP(targetDir) {
6723
5940
  const dir = targetDir || "my-app";
6724
- if (existsSync21(join19(dir, "package.json"))) {
5941
+ if (existsSync19(join16(dir, "package.json"))) {
6725
5942
  throw new CliError(`Directory "${dir}" already contains a project.`);
6726
5943
  }
6727
- consola43.start("Scaffolding SP starter...");
5944
+ consola44.start("Scaffolding SP starter...");
6728
5945
  await downloadTemplate("openape-ai/openape-sp-starter", dir);
6729
- consola43.success("Scaffolded from openape-sp-starter");
6730
- consola43.start("Installing dependencies...");
5946
+ consola44.success("Scaffolded from openape-sp-starter");
5947
+ consola44.start("Installing dependencies...");
6731
5948
  installDeps(dir);
6732
- consola43.success("Dependencies installed");
6733
- const envExample = join19(dir, ".env.example");
6734
- const envFile = join19(dir, ".env");
6735
- if (existsSync21(envExample) && !existsSync21(envFile)) {
5949
+ consola44.success("Dependencies installed");
5950
+ const envExample = join16(dir, ".env.example");
5951
+ const envFile = join16(dir, ".env");
5952
+ if (existsSync19(envExample) && !existsSync19(envFile)) {
6736
5953
  copyFileSync(envExample, envFile);
6737
- consola43.success(`\`.env\` created (using Free IdP at ${DEFAULT_IDP_URL})`);
5954
+ consola44.success(`\`.env\` created (using Free IdP at ${DEFAULT_IDP_URL})`);
6738
5955
  }
6739
5956
  console.log("");
6740
- consola43.box([
5957
+ consola44.box([
6741
5958
  `cd ${dir}`,
6742
5959
  "npm run dev",
6743
5960
  "",
@@ -6746,7 +5963,7 @@ async function initSP(targetDir) {
6746
5963
  }
6747
5964
  async function initIdP(targetDir) {
6748
5965
  const dir = targetDir || "my-idp";
6749
- if (existsSync21(join19(dir, "package.json"))) {
5966
+ if (existsSync19(join16(dir, "package.json"))) {
6750
5967
  throw new CliError(`Directory "${dir}" already contains a project.`);
6751
5968
  }
6752
5969
  const domain = await promptText("Domain for the IdP", "localhost");
@@ -6756,15 +5973,15 @@ async function initIdP(targetDir) {
6756
5973
  "s3 (S3-compatible)"
6757
5974
  ]);
6758
5975
  const adminEmail = await promptText("Admin email");
6759
- consola43.start("Scaffolding IdP starter...");
5976
+ consola44.start("Scaffolding IdP starter...");
6760
5977
  await downloadTemplate("openape-ai/openape-idp-starter", dir);
6761
- consola43.success("Scaffolded from openape-idp-starter");
6762
- consola43.start("Installing dependencies...");
5978
+ consola44.success("Scaffolded from openape-idp-starter");
5979
+ consola44.start("Installing dependencies...");
6763
5980
  installDeps(dir);
6764
- consola43.success("Dependencies installed");
5981
+ consola44.success("Dependencies installed");
6765
5982
  const sessionSecret = randomBytes(32).toString("hex");
6766
5983
  const managementToken = randomBytes(32).toString("hex");
6767
- consola43.success("Secrets generated");
5984
+ consola44.success("Secrets generated");
6768
5985
  const isLocalhost = domain === "localhost";
6769
5986
  const origin = isLocalhost ? "http://localhost:3000" : `https://${domain}`;
6770
5987
  const envContent = [
@@ -6778,11 +5995,11 @@ async function initIdP(targetDir) {
6778
5995
  `NUXT_OPENAPE_RP_ID=${domain}`,
6779
5996
  `NUXT_OPENAPE_RP_ORIGIN=${origin}`
6780
5997
  ].join("\n");
6781
- writeFileSync13(join19(dir, ".env"), `${envContent}
5998
+ writeFileSync12(join16(dir, ".env"), `${envContent}
6782
5999
  `, { mode: 384 });
6783
- consola43.success(".env created");
6000
+ consola44.success(".env created");
6784
6001
  console.log("");
6785
- consola43.box([
6002
+ consola44.box([
6786
6003
  `cd ${dir}`,
6787
6004
  "npm run dev",
6788
6005
  "",
@@ -6798,12 +6015,12 @@ async function initIdP(targetDir) {
6798
6015
  }
6799
6016
 
6800
6017
  // src/commands/enroll.ts
6801
- import { Buffer as Buffer5 } from "buffer";
6802
- import { existsSync as existsSync22, readFileSync as readFileSync18 } from "fs";
6018
+ import { Buffer as Buffer4 } from "buffer";
6019
+ import { existsSync as existsSync20, readFileSync as readFileSync16 } from "fs";
6803
6020
  import { execFile as execFile2 } from "child_process";
6804
6021
  import { sign as sign2 } from "crypto";
6805
6022
  import { defineCommand as defineCommand54 } from "citty";
6806
- import consola44 from "consola";
6023
+ import consola45 from "consola";
6807
6024
  var DEFAULT_IDP_URL2 = "https://id.openape.at";
6808
6025
  var DEFAULT_KEY_PATH = "~/.ssh/id_ed25519";
6809
6026
  var POLL_INTERVAL = 3e3;
@@ -6815,7 +6032,7 @@ function openBrowser2(url) {
6815
6032
  }
6816
6033
  async function pollForEnrollment(idp, agentEmail, keyPath) {
6817
6034
  const resolvedKey = resolveKeyPath(keyPath);
6818
- const keyContent = readFileSync18(resolvedKey, "utf-8");
6035
+ const keyContent = readFileSync16(resolvedKey, "utf-8");
6819
6036
  const privateKey = loadEd25519PrivateKey(keyContent);
6820
6037
  const challengeUrl = await getAgentChallengeEndpoint(idp);
6821
6038
  const authenticateUrl = await getAgentAuthenticateEndpoint(idp);
@@ -6829,7 +6046,7 @@ async function pollForEnrollment(idp, agentEmail, keyPath) {
6829
6046
  });
6830
6047
  if (challengeResp.ok) {
6831
6048
  const { challenge } = await challengeResp.json();
6832
- const signature = sign2(null, Buffer5.from(challenge), privateKey).toString("base64");
6049
+ const signature = sign2(null, Buffer4.from(challenge), privateKey).toString("base64");
6833
6050
  const authResp = await fetch(authenticateUrl, {
6834
6051
  method: "POST",
6835
6052
  headers: { "Content-Type": "application/json" },
@@ -6842,7 +6059,7 @@ async function pollForEnrollment(idp, agentEmail, keyPath) {
6842
6059
  }
6843
6060
  } catch {
6844
6061
  }
6845
- await new Promise((resolve4) => setTimeout(resolve4, POLL_INTERVAL));
6062
+ await new Promise((resolve5) => setTimeout(resolve5, POLL_INTERVAL));
6846
6063
  }
6847
6064
  throw new Error("Enrollment timed out. Please check the browser and try again.");
6848
6065
  }
@@ -6866,38 +6083,38 @@ var enrollCommand = defineCommand54({
6866
6083
  }
6867
6084
  },
6868
6085
  async run({ args }) {
6869
- const idp = args.idp || await consola44.prompt("IdP URL", { type: "text", default: DEFAULT_IDP_URL2, placeholder: DEFAULT_IDP_URL2 }).then((r) => {
6086
+ const idp = args.idp || await consola45.prompt("IdP URL", { type: "text", default: DEFAULT_IDP_URL2, placeholder: DEFAULT_IDP_URL2 }).then((r) => {
6870
6087
  if (typeof r === "symbol") throw new CliExit(0);
6871
6088
  return r;
6872
6089
  }) || DEFAULT_IDP_URL2;
6873
- const agentName = args.name || await consola44.prompt("Agent name", { type: "text", placeholder: "deploy-bot" }).then((r) => {
6090
+ const agentName = args.name || await consola45.prompt("Agent name", { type: "text", placeholder: "deploy-bot" }).then((r) => {
6874
6091
  if (typeof r === "symbol") throw new CliExit(0);
6875
6092
  return r;
6876
6093
  });
6877
6094
  if (!agentName) {
6878
6095
  throw new CliError("Agent name is required.");
6879
6096
  }
6880
- const keyPath = args.key || await consola44.prompt("Ed25519 key", { type: "text", default: DEFAULT_KEY_PATH, placeholder: DEFAULT_KEY_PATH }).then((r) => {
6097
+ const keyPath = args.key || await consola45.prompt("Ed25519 key", { type: "text", default: DEFAULT_KEY_PATH, placeholder: DEFAULT_KEY_PATH }).then((r) => {
6881
6098
  if (typeof r === "symbol") throw new CliExit(0);
6882
6099
  return r;
6883
6100
  }) || DEFAULT_KEY_PATH;
6884
6101
  const resolvedKey = resolveKeyPath(keyPath);
6885
6102
  let publicKey;
6886
- if (existsSync22(resolvedKey)) {
6103
+ if (existsSync20(resolvedKey)) {
6887
6104
  publicKey = readPublicKey(resolvedKey);
6888
- consola44.success(`Using existing key ${keyPath}`);
6105
+ consola45.success(`Using existing key ${keyPath}`);
6889
6106
  } else {
6890
- consola44.start(`Generating Ed25519 key pair at ${keyPath}...`);
6107
+ consola45.start(`Generating Ed25519 key pair at ${keyPath}...`);
6891
6108
  publicKey = generateAndSaveKey(keyPath);
6892
- consola44.success(`Key pair generated at ${keyPath}`);
6109
+ consola45.success(`Key pair generated at ${keyPath}`);
6893
6110
  }
6894
6111
  const encodedKey = encodeURIComponent(publicKey);
6895
6112
  const enrollUrl = `${idp}/enroll?name=${encodeURIComponent(agentName)}&key=${encodedKey}`;
6896
- consola44.info("Opening browser for enrollment...");
6897
- consola44.info(`\u2192 ${idp}/enroll`);
6113
+ consola45.info("Opening browser for enrollment...");
6114
+ consola45.info(`\u2192 ${idp}/enroll`);
6898
6115
  openBrowser2(enrollUrl);
6899
6116
  console.log("");
6900
- const agentEmail = await consola44.prompt(
6117
+ const agentEmail = await consola45.prompt(
6901
6118
  "Agent email (shown in browser after enrollment)",
6902
6119
  { type: "text", placeholder: `agent+${agentName}@...` }
6903
6120
  ).then((r) => {
@@ -6907,7 +6124,7 @@ var enrollCommand = defineCommand54({
6907
6124
  if (!agentEmail) {
6908
6125
  throw new CliError("Agent email is required to verify enrollment.");
6909
6126
  }
6910
- consola44.start("Verifying enrollment...");
6127
+ consola45.start("Verifying enrollment...");
6911
6128
  const { token, expiresIn } = await pollForEnrollment(idp, agentEmail, keyPath);
6912
6129
  saveAuth({
6913
6130
  idp,
@@ -6919,17 +6136,17 @@ var enrollCommand = defineCommand54({
6919
6136
  config.defaults = { ...config.defaults, idp };
6920
6137
  config.agent = { key: keyPath, email: agentEmail };
6921
6138
  saveConfig(config);
6922
- consola44.success(`Agent enrolled as ${agentEmail}`);
6923
- consola44.success("Config saved to ~/.config/apes/");
6139
+ consola45.success(`Agent enrolled as ${agentEmail}`);
6140
+ consola45.success("Config saved to ~/.config/apes/");
6924
6141
  console.log("");
6925
- consola44.info("Verify with: apes whoami");
6142
+ consola45.info("Verify with: apes whoami");
6926
6143
  }
6927
6144
  });
6928
6145
 
6929
6146
  // src/commands/register-user.ts
6930
- import { existsSync as existsSync23, readFileSync as readFileSync19 } from "fs";
6147
+ import { existsSync as existsSync21, readFileSync as readFileSync17 } from "fs";
6931
6148
  import { defineCommand as defineCommand55 } from "citty";
6932
- import consola45 from "consola";
6149
+ import consola46 from "consola";
6933
6150
  var registerUserCommand = defineCommand55({
6934
6151
  meta: {
6935
6152
  name: "register-user",
@@ -6966,8 +6183,8 @@ var registerUserCommand = defineCommand55({
6966
6183
  throw new CliError("No IdP URL configured. Run `apes login` first.");
6967
6184
  }
6968
6185
  let publicKey = args.key;
6969
- if (existsSync23(args.key)) {
6970
- publicKey = readFileSync19(args.key, "utf-8").trim();
6186
+ if (existsSync21(args.key)) {
6187
+ publicKey = readFileSync17(args.key, "utf-8").trim();
6971
6188
  }
6972
6189
  if (!publicKey.startsWith("ssh-ed25519 ")) {
6973
6190
  throw new CliError("Public key must be in ssh-ed25519 format.");
@@ -6985,7 +6202,7 @@ var registerUserCommand = defineCommand55({
6985
6202
  ...userType ? { type: userType } : {}
6986
6203
  }
6987
6204
  });
6988
- consola45.success(`User registered: ${result.email} (type: ${result.type}, owner: ${result.owner})`);
6205
+ consola46.success(`User registered: ${result.email} (type: ${result.type}, owner: ${result.owner})`);
6989
6206
  }
6990
6207
  });
6991
6208
 
@@ -6994,7 +6211,7 @@ import { defineCommand as defineCommand57 } from "citty";
6994
6211
 
6995
6212
  // src/commands/utils/dig.ts
6996
6213
  import { defineCommand as defineCommand56 } from "citty";
6997
- import consola46 from "consola";
6214
+ import consola47 from "consola";
6998
6215
  import { resolveDDISA as resolveDDISA2 } from "@openape/core";
6999
6216
  var digCommand = defineCommand56({
7000
6217
  meta: {
@@ -7069,12 +6286,12 @@ var digCommand = defineCommand56({
7069
6286
  console.log(` domain: ${domain}`);
7070
6287
  console.log("");
7071
6288
  if (!result.ddisa.found) {
7072
- consola46.warn(`No DDISA record at _ddisa.${domain}`);
6289
+ consola47.warn(`No DDISA record at _ddisa.${domain}`);
7073
6290
  if (result.hint) console.log(`
7074
6291
  ${result.hint}`);
7075
6292
  throw new CliError(`No DDISA record found for ${domain}`);
7076
6293
  }
7077
- consola46.success(`_ddisa.${domain} \u2192 ${result.ddisa.idp}`);
6294
+ consola47.success(`_ddisa.${domain} \u2192 ${result.ddisa.idp}`);
7078
6295
  console.log(` Version: ${result.ddisa.version || "ddisa1"}`);
7079
6296
  console.log(` IdP URL: ${result.ddisa.idp}`);
7080
6297
  if (result.ddisa.mode) console.log(` Mode: ${result.ddisa.mode}`);
@@ -7084,13 +6301,13 @@ ${result.hint}`);
7084
6301
  return;
7085
6302
  }
7086
6303
  if (result.idpDiscovery.ok) {
7087
- consola46.success(`IdP reachable (${result.idpDiscovery.status ?? 200})`);
6304
+ consola47.success(`IdP reachable (${result.idpDiscovery.status ?? 200})`);
7088
6305
  if (result.idpDiscovery.issuer) console.log(` Issuer: ${result.idpDiscovery.issuer}`);
7089
6306
  if (result.idpDiscovery.ddisaVersion) console.log(` DDISA: v${result.idpDiscovery.ddisaVersion}`);
7090
6307
  if (result.idpDiscovery.authMethods?.length) console.log(` Auth: ${result.idpDiscovery.authMethods.join(", ")}`);
7091
6308
  if (result.idpDiscovery.grantTypes?.length) console.log(` Grants: ${result.idpDiscovery.grantTypes.join(", ")}`);
7092
6309
  } else {
7093
- consola46.warn(`IdP discovery failed${result.idpDiscovery.status ? ` (HTTP ${result.idpDiscovery.status})` : ""}`);
6310
+ consola47.warn(`IdP discovery failed${result.idpDiscovery.status ? ` (HTTP ${result.idpDiscovery.status})` : ""}`);
7094
6311
  if (result.hint) console.log(`
7095
6312
  ${result.hint}`);
7096
6313
  throw new CliError(`IdP at ${result.ddisa.idp} not reachable`);
@@ -7114,7 +6331,7 @@ import { defineCommand as defineCommand60 } from "citty";
7114
6331
 
7115
6332
  // src/commands/sessions/list.ts
7116
6333
  import { defineCommand as defineCommand58 } from "citty";
7117
- import consola47 from "consola";
6334
+ import consola48 from "consola";
7118
6335
  var sessionsListCommand = defineCommand58({
7119
6336
  meta: {
7120
6337
  name: "list",
@@ -7133,7 +6350,7 @@ var sessionsListCommand = defineCommand58({
7133
6350
  return;
7134
6351
  }
7135
6352
  if (result.data.length === 0) {
7136
- consola47.info("No active sessions.");
6353
+ consola48.info("No active sessions.");
7137
6354
  return;
7138
6355
  }
7139
6356
  for (const f of result.data) {
@@ -7146,7 +6363,7 @@ var sessionsListCommand = defineCommand58({
7146
6363
 
7147
6364
  // src/commands/sessions/remove.ts
7148
6365
  import { defineCommand as defineCommand59 } from "citty";
7149
- import consola48 from "consola";
6366
+ import consola49 from "consola";
7150
6367
  var sessionsRemoveCommand = defineCommand59({
7151
6368
  meta: {
7152
6369
  name: "remove",
@@ -7163,7 +6380,7 @@ var sessionsRemoveCommand = defineCommand59({
7163
6380
  const id = String(args.familyId).trim();
7164
6381
  if (!id) throw new CliError("familyId required");
7165
6382
  await apiFetch(`/api/me/sessions/${encodeURIComponent(id)}`, { method: "DELETE" });
7166
- consola48.success(`Session ${id} revoked. The device using it will need to \`apes login\` again on its next refresh.`);
6383
+ consola49.success(`Session ${id} revoked. The device using it will need to \`apes login\` again on its next refresh.`);
7167
6384
  }
7168
6385
  });
7169
6386
 
@@ -7181,7 +6398,7 @@ var sessionsCommand = defineCommand60({
7181
6398
 
7182
6399
  // src/commands/dns-check.ts
7183
6400
  import { defineCommand as defineCommand61 } from "citty";
7184
- import consola49 from "consola";
6401
+ import consola50 from "consola";
7185
6402
  import { resolveDDISA as resolveDDISA3 } from "@openape/core";
7186
6403
  var dnsCheckCommand = defineCommand61({
7187
6404
  meta: {
@@ -7197,7 +6414,7 @@ var dnsCheckCommand = defineCommand61({
7197
6414
  },
7198
6415
  async run({ args }) {
7199
6416
  const domain = args.domain;
7200
- consola49.start(`Checking _ddisa.${domain}...`);
6417
+ consola50.start(`Checking _ddisa.${domain}...`);
7201
6418
  try {
7202
6419
  const result = await resolveDDISA3(domain);
7203
6420
  if (!result) {
@@ -7206,7 +6423,7 @@ var dnsCheckCommand = defineCommand61({
7206
6423
  console.log(` _ddisa.${domain} TXT "v=ddisa1 idp=https://id.${domain}"`);
7207
6424
  throw new CliError(`No DDISA record found for ${domain}`);
7208
6425
  }
7209
- consola49.success(`_ddisa.${domain} \u2192 ${result.idp}`);
6426
+ consola50.success(`_ddisa.${domain} \u2192 ${result.idp}`);
7210
6427
  console.log("");
7211
6428
  console.log(` Version: ${result.version || "ddisa1"}`);
7212
6429
  console.log(` IdP URL: ${result.idp}`);
@@ -7215,14 +6432,14 @@ var dnsCheckCommand = defineCommand61({
7215
6432
  if (result.priority !== void 0)
7216
6433
  console.log(` Priority: ${result.priority}`);
7217
6434
  console.log("");
7218
- consola49.start(`Verifying IdP at ${result.idp}...`);
6435
+ consola50.start(`Verifying IdP at ${result.idp}...`);
7219
6436
  const discoResp = await fetch(`${result.idp}/.well-known/openid-configuration`);
7220
6437
  if (!discoResp.ok) {
7221
- consola49.warn(`IdP discovery failed (${discoResp.status}). Is the IdP running at ${result.idp}?`);
6438
+ consola50.warn(`IdP discovery failed (${discoResp.status}). Is the IdP running at ${result.idp}?`);
7222
6439
  return;
7223
6440
  }
7224
6441
  const disco = await discoResp.json();
7225
- consola49.success(`IdP is reachable`);
6442
+ consola50.success(`IdP is reachable`);
7226
6443
  console.log(` Issuer: ${disco.issuer}`);
7227
6444
  console.log(` DDISA: v${disco.ddisa_version || "?"}`);
7228
6445
  if (disco.ddisa_auth_methods_supported) {
@@ -7276,7 +6493,7 @@ async function bestEffortGrantCount(idp) {
7276
6493
  }
7277
6494
  }
7278
6495
  async function runHealth(args) {
7279
- const version = true ? "1.29.1" : "0.0.0";
6496
+ const version = true ? "1.31.0" : "0.0.0";
7280
6497
  const auth = loadAuth();
7281
6498
  if (!auth) {
7282
6499
  throw new CliError("Not logged in. Run `apes login` first.", 1);
@@ -7358,7 +6575,7 @@ var healthCommand = defineCommand62({
7358
6575
 
7359
6576
  // src/commands/workflows.ts
7360
6577
  import { defineCommand as defineCommand63 } from "citty";
7361
- import consola50 from "consola";
6578
+ import consola51 from "consola";
7362
6579
 
7363
6580
  // src/guides/index.ts
7364
6581
  var guides = [
@@ -7429,7 +6646,7 @@ var workflowsCommand = defineCommand63({
7429
6646
  if (args.id) {
7430
6647
  const guide = guides.find((g) => g.id === String(args.id));
7431
6648
  if (!guide) {
7432
- consola50.info(`Available: ${guides.map((g) => g.id).join(", ")}`);
6649
+ consola51.info(`Available: ${guides.map((g) => g.id).join(", ")}`);
7433
6650
  throw new CliError(`Guide not found: ${args.id}`);
7434
6651
  }
7435
6652
  if (args.json) {
@@ -7469,26 +6686,26 @@ var workflowsCommand = defineCommand63({
7469
6686
  });
7470
6687
 
7471
6688
  // src/version-check.ts
7472
- import { existsSync as existsSync24, mkdirSync as mkdirSync7, readFileSync as readFileSync20, writeFileSync as writeFileSync14 } from "fs";
7473
- import { homedir as homedir15 } from "os";
7474
- import { join as join20 } from "path";
7475
- import consola51 from "consola";
6689
+ import { existsSync as existsSync22, mkdirSync as mkdirSync7, readFileSync as readFileSync18, writeFileSync as writeFileSync13 } from "fs";
6690
+ import { homedir as homedir14 } from "os";
6691
+ import { join as join17 } from "path";
6692
+ import consola52 from "consola";
7476
6693
  var PACKAGE_NAME = "@openape/apes";
7477
6694
  var CACHE_TTL_MS = 24 * 60 * 60 * 1e3;
7478
- var CACHE_FILE = join20(homedir15(), ".config", "apes", ".version-check.json");
6695
+ var CACHE_FILE = join17(homedir14(), ".config", "apes", ".version-check.json");
7479
6696
  function readCache() {
7480
- if (!existsSync24(CACHE_FILE)) return null;
6697
+ if (!existsSync22(CACHE_FILE)) return null;
7481
6698
  try {
7482
- return JSON.parse(readFileSync20(CACHE_FILE, "utf-8"));
6699
+ return JSON.parse(readFileSync18(CACHE_FILE, "utf-8"));
7483
6700
  } catch {
7484
6701
  return null;
7485
6702
  }
7486
6703
  }
7487
6704
  function writeCache(entry) {
7488
6705
  try {
7489
- const dir = join20(homedir15(), ".config", "apes");
7490
- if (!existsSync24(dir)) mkdirSync7(dir, { recursive: true, mode: 448 });
7491
- writeFileSync14(CACHE_FILE, JSON.stringify(entry), { mode: 384 });
6706
+ const dir = join17(homedir14(), ".config", "apes");
6707
+ if (!existsSync22(dir)) mkdirSync7(dir, { recursive: true, mode: 448 });
6708
+ writeFileSync13(CACHE_FILE, JSON.stringify(entry), { mode: 384 });
7492
6709
  } catch {
7493
6710
  }
7494
6711
  }
@@ -7517,7 +6734,7 @@ async function fetchLatestVersion() {
7517
6734
  }
7518
6735
  function warnIfBehind(currentVersion, latest) {
7519
6736
  if (compareSemver(currentVersion, latest) < 0) {
7520
- consola51.warn(
6737
+ consola52.warn(
7521
6738
  `apes ${currentVersion} is behind latest @openape/apes@${latest}. Run \`npm i -g @openape/apes@latest\` to update. (Suppress with APES_NO_UPDATE_CHECK=1.)`
7522
6739
  );
7523
6740
  }
@@ -7549,10 +6766,10 @@ if (shellRewrite) {
7549
6766
  if (shellRewrite.action === "rewrite") {
7550
6767
  process.argv = shellRewrite.argv;
7551
6768
  } else if (shellRewrite.action === "version") {
7552
- console.log(`ape-shell ${"1.29.1"} (OpenApe DDISA shell wrapper)`);
6769
+ console.log(`ape-shell ${"1.31.0"} (OpenApe DDISA shell wrapper)`);
7553
6770
  process.exit(0);
7554
6771
  } else if (shellRewrite.action === "help") {
7555
- console.log(`ape-shell ${"1.29.1"} \u2014 OpenApe DDISA shell wrapper`);
6772
+ console.log(`ape-shell ${"1.31.0"} \u2014 OpenApe DDISA shell wrapper`);
7556
6773
  console.log("");
7557
6774
  console.log("Usage:");
7558
6775
  console.log(" ape-shell Start interactive grant-mediated REPL");
@@ -7567,7 +6784,7 @@ if (shellRewrite) {
7567
6784
  console.log(" --help, -h Show this help message");
7568
6785
  process.exit(0);
7569
6786
  } else if (shellRewrite.action === "interactive") {
7570
- const { runInteractiveShell } = await import("./orchestrator-P7QFDBBK.js");
6787
+ const { runInteractiveShell } = await import("./orchestrator-6PZXCE54.js");
7571
6788
  await runInteractiveShell();
7572
6789
  process.exit(0);
7573
6790
  } else {
@@ -7610,7 +6827,7 @@ var configCommand = defineCommand64({
7610
6827
  var main = defineCommand64({
7611
6828
  meta: {
7612
6829
  name: "apes",
7613
- version: "1.29.1",
6830
+ version: "1.31.0",
7614
6831
  description: "Unified CLI for OpenApe"
7615
6832
  },
7616
6833
  subCommands: {
@@ -7662,26 +6879,26 @@ async function maybeRefreshAuth() {
7662
6879
  const { loadAuth: loadAuth2 } = await import("./config-MOB5DJ6H.js");
7663
6880
  if (!loadAuth2()) return;
7664
6881
  try {
7665
- const { ensureFreshToken } = await import("./http-UPOTOYQV.js");
6882
+ const { ensureFreshToken } = await import("./http-SILH37L7.js");
7666
6883
  await ensureFreshToken();
7667
6884
  } catch {
7668
6885
  }
7669
6886
  }
7670
6887
  await maybeRefreshAuth();
7671
- await maybeWarnStaleVersion("1.29.1").catch(() => {
6888
+ await maybeWarnStaleVersion("1.31.0").catch(() => {
7672
6889
  });
7673
6890
  runMain(main).catch((err) => {
7674
6891
  if (err instanceof CliExit) {
7675
6892
  process.exit(err.exitCode);
7676
6893
  }
7677
6894
  if (err instanceof CliError) {
7678
- consola52.error(err.message);
6895
+ consola53.error(err.message);
7679
6896
  process.exit(err.exitCode);
7680
6897
  }
7681
6898
  if (debug) {
7682
- consola52.error(err);
6899
+ consola53.error(err);
7683
6900
  } else {
7684
- consola52.error(err instanceof ApiError ? err.message : err instanceof Error ? err.message : String(err));
6901
+ consola53.error(err instanceof ApiError ? err.message : err instanceof Error ? err.message : String(err));
7685
6902
  }
7686
6903
  process.exit(1);
7687
6904
  });