@openape/apes 0.22.0 → 0.23.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
@@ -379,7 +379,7 @@ async function loginWithPKCE(idp) {
379
379
  consola2.success(`Logged in as ${payload.email || payload.sub}`);
380
380
  }
381
381
  async function loginWithKey(idp, keyPath, agentEmail) {
382
- const { readFileSync: readFileSync8 } = await import("fs");
382
+ const { readFileSync: readFileSync9 } = await import("fs");
383
383
  const { sign: sign3 } = await import("crypto");
384
384
  const { loadEd25519PrivateKey: loadEd25519PrivateKey2 } = await import("./ssh-key-YBNNG5K5.js");
385
385
  const challengeUrl = await getAgentChallengeEndpoint(idp);
@@ -392,7 +392,7 @@ async function loginWithKey(idp, keyPath, agentEmail) {
392
392
  throw new CliError(`Challenge failed: ${await challengeResp.text()}`);
393
393
  }
394
394
  const { challenge } = await challengeResp.json();
395
- const keyContent = readFileSync8(keyPath, "utf-8");
395
+ const keyContent = readFileSync9(keyPath, "utf-8");
396
396
  const privateKey = loadEd25519PrivateKey2(keyContent);
397
397
  const signature = sign3(null, Buffer2.from(challenge), privateKey).toString("base64");
398
398
  const authenticateUrl = await getAgentAuthenticateEndpoint(idp);
@@ -1748,7 +1748,7 @@ import { defineCommand as defineCommand24 } from "citty";
1748
1748
  // src/commands/agents/destroy.ts
1749
1749
  import { execFileSync as execFileSync3 } from "child_process";
1750
1750
  import { mkdtempSync, rmSync, writeFileSync } from "fs";
1751
- import { tmpdir } from "os";
1751
+ import { tmpdir, userInfo } from "os";
1752
1752
  import { join as join2 } from "path";
1753
1753
  import { defineCommand as defineCommand20 } from "citty";
1754
1754
  import consola18 from "consola";
@@ -1873,7 +1873,7 @@ mkdir -p "$HOME_DIR/.ssh" "$HOME_DIR/.config/apes"
1873
1873
  cat > "$HOME_DIR/.ssh/id_ed25519" ${shHeredoc(privatePemForHeredoc.trimEnd())}
1874
1874
  cat > "$HOME_DIR/.ssh/id_ed25519.pub" ${shHeredoc(`${input.publicKeySshLine}`)}
1875
1875
  cat > "$HOME_DIR/.config/apes/auth.json" ${shHeredoc(input.authJson)}
1876
- ${claudeBlock}${claudeTokenBlock}
1876
+ ${claudeBlock}${claudeTokenBlock}${buildBridgeBlock(input.bridge)}
1877
1877
  chown -R "$NAME:staff" "$HOME_DIR"
1878
1878
  chmod 700 "$HOME_DIR/.ssh"
1879
1879
  chmod 700 "$HOME_DIR/.config"
@@ -1886,10 +1886,32 @@ if [ -f "$HOME_DIR/.config/openape/claude-token.env" ]; then
1886
1886
  fi
1887
1887
 
1888
1888
  echo "OK $NAME uid=$NEXT_UID home=$HOME_DIR"
1889
+ ${buildBridgeBootstrapBlock(input.bridge)}`;
1890
+ }
1891
+ function buildBridgeBlock(bridge) {
1892
+ if (!bridge) return "";
1893
+ return `
1894
+ mkdir -p "$HOME_DIR/Library/LaunchAgents" "$HOME_DIR/Library/Application Support/openape/bridge" "$HOME_DIR/Library/Logs" "$HOME_DIR/.pi/agent"
1895
+ cat > "$HOME_DIR/.pi/agent/.env" ${shHeredoc(bridge.envFile)}
1896
+ cat > "$HOME_DIR/Library/Application Support/openape/bridge/start.sh" ${shHeredoc(bridge.startScript)}
1897
+ chmod 755 "$HOME_DIR/Library/Application Support/openape/bridge/start.sh"
1898
+ cat > "$HOME_DIR/Library/LaunchAgents/${bridge.plistLabel}.plist" ${shHeredoc(bridge.plistContent)}
1899
+ chmod 600 "$HOME_DIR/.pi/agent/.env"
1900
+ `;
1901
+ }
1902
+ function buildBridgeBootstrapBlock(bridge) {
1903
+ if (!bridge) return "";
1904
+ return `
1905
+ # Load the bridge launchd job into the agent's gui domain. Runs as root
1906
+ # from the spawn setup script so we target gui/<agent-uid> explicitly.
1907
+ # Failure here is non-fatal \u2014 the plist still lands and launchd will pick
1908
+ # it up next time the agent logs in.
1909
+ launchctl bootstrap "gui/$NEXT_UID" "$HOME_DIR/Library/LaunchAgents/${bridge.plistLabel}.plist" || \\
1910
+ echo "warn: bridge bootstrap failed for gui/$NEXT_UID; loads on next login"
1889
1911
  `;
1890
1912
  }
1891
1913
  function buildDestroyTeardownScript(input) {
1892
- const { name, homeDir } = input;
1914
+ const { name, homeDir, adminUser } = input;
1893
1915
  return `#!/bin/bash
1894
1916
  # Best-effort teardown. set -u catches typos; we deliberately do NOT use -e
1895
1917
  # because pkill / launchctl are allowed to fail when the user has no live
@@ -1898,6 +1920,16 @@ set -u
1898
1920
 
1899
1921
  NAME=${shQuote(name)}
1900
1922
  HOME_DIR=${shQuote(homeDir)}
1923
+ ADMIN_USER=${shQuote(adminUser)}
1924
+
1925
+ # Read the admin password from stdin (line 1). The caller pipes it in.
1926
+ # We never accept it as an argv element so it can't show up in process
1927
+ # listings or escapes' audit log.
1928
+ read -r ADMIN_PASSWORD
1929
+ if [ -z "$ADMIN_PASSWORD" ]; then
1930
+ echo "ERROR: no admin password on stdin (expected one line)." >&2
1931
+ exit 2
1932
+ fi
1901
1933
 
1902
1934
  UID_OF=$(dscl . -read "/Users/$NAME" UniqueID 2>/dev/null | awk '/UniqueID:/ {print $2}')
1903
1935
 
@@ -1910,25 +1942,32 @@ if [ -d "$HOME_DIR" ] && [ "$HOME_DIR" != "/" ] && [ "$HOME_DIR" != "" ]; then
1910
1942
  rm -rf "$HOME_DIR"
1911
1943
  fi
1912
1944
 
1913
- # Delete the user record. \`sysadminctl -deleteUser\` is the canonical macOS
1914
- # API and removes Open Directory metadata that \`dscl . -delete\` leaves
1915
- # behind. Fall back to dscl if sysadminctl isn't available or rejects the
1916
- # user. Surface a clear error if both fail \u2014 silent failure here is what
1917
- # left orphaned dscl records on previous spawn/destroy round-trips.
1918
- if command -v sysadminctl >/dev/null 2>&1; then
1919
- if sysadminctl -deleteUser "$NAME" 2>/dev/null; then
1920
- :
1921
- elif dscl . -read "/Users/$NAME" >/dev/null 2>&1; then
1922
- dscl . -delete "/Users/$NAME" || {
1923
- echo "ERROR: failed to delete user record /Users/$NAME" >&2
1924
- exit 1
1925
- }
1926
- fi
1927
- elif dscl . -read "/Users/$NAME" >/dev/null 2>&1; then
1928
- dscl . -delete "/Users/$NAME" || {
1929
- echo "ERROR: failed to delete user record /Users/$NAME" >&2
1930
- exit 1
1931
- }
1945
+ # \`escapes\` is a plain setuid binary \u2014 opendirectoryd sees no audit/PAM
1946
+ # session attached (AUDIT_SESSION_ID=unset) and rejects DirectoryService
1947
+ # writes from this context: a bare \`sysadminctl -deleteUser\` or
1948
+ # \`dscl . -delete\` hangs ~5 minutes and exits with eUndefinedError -14987
1949
+ # at DSRecord.m:563. Passing explicit -adminUser/-adminPassword bypasses
1950
+ # opendirectoryd's implicit "is current session admin?" check and
1951
+ # authenticates against DirectoryService directly \u2014 the delete then
1952
+ # completes in ~1 second.
1953
+ if ! command -v sysadminctl >/dev/null 2>&1; then
1954
+ echo "ERROR: sysadminctl not available; cannot delete user record." >&2
1955
+ exit 1
1956
+ fi
1957
+
1958
+ sysadminctl \\
1959
+ -deleteUser "$NAME" \\
1960
+ -adminUser "$ADMIN_USER" \\
1961
+ -adminPassword "$ADMIN_PASSWORD"
1962
+ SYSAD_EC=$?
1963
+ unset ADMIN_PASSWORD
1964
+
1965
+ if [ $SYSAD_EC -ne 0 ]; then
1966
+ echo "ERROR: sysadminctl -deleteUser failed (exit=$SYSAD_EC)." >&2
1967
+ echo " Common causes: wrong admin password, admin user '$ADMIN_USER'" >&2
1968
+ echo " not in admin group, or target user '$NAME' is the last secure" >&2
1969
+ echo " token holder (run \\\`sysadminctl -secureTokenStatus $NAME\\\`)." >&2
1970
+ exit 1
1932
1971
  fi
1933
1972
 
1934
1973
  # Verify the record is actually gone.
@@ -2122,14 +2161,20 @@ ${consequences.join("\n")}`);
2122
2161
  if (!escapes) {
2123
2162
  throw new CliError("`escapes` not found on PATH; OS teardown requires escapes.");
2124
2163
  }
2164
+ const adminUser = userInfo().username;
2165
+ const adminPassword = await collectAdminPassword({ adminUser, force: !!args.force });
2125
2166
  const scratch = mkdtempSync(join2(tmpdir(), `apes-destroy-${name}-`));
2126
2167
  const scriptPath = join2(scratch, "teardown.sh");
2127
2168
  try {
2128
- const script = buildDestroyTeardownScript({ name, homeDir: `/Users/${name}` });
2169
+ const script = buildDestroyTeardownScript({ name, homeDir: `/Users/${name}`, adminUser });
2129
2170
  writeFileSync(scriptPath, script, { mode: 448 });
2130
2171
  consola18.start("Running teardown as root via `apes run --as root --wait`\u2026");
2131
2172
  consola18.info("You will be asked to approve the as=root grant in your DDISA inbox; this command blocks until you do.");
2132
- execFileSync3(apes, ["run", "--as", "root", "--wait", "--", "bash", scriptPath], { stdio: "inherit" });
2173
+ execFileSync3(apes, ["run", "--as", "root", "--wait", "--", "bash", scriptPath], {
2174
+ input: `${adminPassword}
2175
+ `,
2176
+ stdio: ["pipe", "inherit", "inherit"]
2177
+ });
2133
2178
  } finally {
2134
2179
  rmSync(scratch, { recursive: true, force: true });
2135
2180
  }
@@ -2139,6 +2184,21 @@ ${consequences.join("\n")}`);
2139
2184
  consola18.success(`Destroyed ${name}.`);
2140
2185
  }
2141
2186
  });
2187
+ async function collectAdminPassword(opts) {
2188
+ const fromEnv = process.env.APES_ADMIN_PASSWORD;
2189
+ if (fromEnv && fromEnv.length > 0) return fromEnv;
2190
+ if (!process.stdin.isTTY) {
2191
+ throw new CliError(
2192
+ `Admin password required for sysadminctl -deleteUser. No TTY available for the silent prompt; set APES_ADMIN_PASSWORD in the environment (local admin password for ${opts.adminUser}). The teardown reads it from stdin and never stores it.`
2193
+ );
2194
+ }
2195
+ consola18.info(`Local admin password for ${opts.adminUser} (used for sysadminctl -deleteUser; not stored):`);
2196
+ const pw = await consola18.prompt("Admin password", { type: "text", mask: "*" });
2197
+ if (typeof pw === "symbol" || !pw || pw.length === 0) {
2198
+ throw new CliExit(0);
2199
+ }
2200
+ return pw;
2201
+ }
2142
2202
 
2143
2203
  // src/commands/agents/list.ts
2144
2204
  import { defineCommand as defineCommand21 } from "citty";
@@ -2291,7 +2351,7 @@ var registerAgentCommand = defineCommand22({
2291
2351
  import { execFileSync as execFileSync4 } from "child_process";
2292
2352
  import { mkdtempSync as mkdtempSync2, rmSync as rmSync2, writeFileSync as writeFileSync3 } from "fs";
2293
2353
  import { tmpdir as tmpdir2 } from "os";
2294
- import { join as join3 } from "path";
2354
+ import { join as join4 } from "path";
2295
2355
  import { defineCommand as defineCommand23 } from "citty";
2296
2356
  import consola21 from "consola";
2297
2357
 
@@ -2351,6 +2411,103 @@ function generateKeyPairInMemory() {
2351
2411
  };
2352
2412
  }
2353
2413
 
2414
+ // src/lib/llm-bridge.ts
2415
+ import { existsSync as existsSync6, readFileSync as readFileSync5 } from "fs";
2416
+ import { homedir as homedir5 } from "os";
2417
+ import { join as join3 } from "path";
2418
+ var BRIDGE_PLIST_LABEL = "eco.hofmann.apes.bridge";
2419
+ function readLitellmEnv(envPath = join3(homedir5(), "litellm", ".env")) {
2420
+ if (!existsSync6(envPath)) return null;
2421
+ try {
2422
+ const text = readFileSync5(envPath, "utf8");
2423
+ const out = {};
2424
+ for (const line of text.split("\n")) {
2425
+ const trimmed = line.trim();
2426
+ if (!trimmed || trimmed.startsWith("#")) continue;
2427
+ const eq = trimmed.indexOf("=");
2428
+ if (eq < 0) continue;
2429
+ const key = trimmed.slice(0, eq).trim();
2430
+ const value = trimmed.slice(eq + 1).trim();
2431
+ if (key === "LITELLM_MASTER_KEY" || key === "LITELLM_API_KEY") out.apiKey = value;
2432
+ if (key === "LITELLM_BASE_URL") out.baseUrl = value;
2433
+ }
2434
+ return out;
2435
+ } catch {
2436
+ return null;
2437
+ }
2438
+ }
2439
+ function resolveBridgeConfig(opts) {
2440
+ const env = readLitellmEnv(opts.envPath);
2441
+ const apiKey = opts.cliKey ?? env?.apiKey;
2442
+ const baseUrl = opts.cliBaseUrl ?? env?.baseUrl ?? "http://127.0.0.1:4000/v1";
2443
+ if (!apiKey) {
2444
+ throw new Error(
2445
+ "No LITELLM_API_KEY resolved. Pass --bridge-key sk-\u2026 or write LITELLM_MASTER_KEY into ~/litellm/.env first."
2446
+ );
2447
+ }
2448
+ return { baseUrl, apiKey };
2449
+ }
2450
+ function buildBridgeEnvFile(cfg) {
2451
+ return `# Auto-generated by 'apes agents spawn --bridge'.
2452
+ # Read by the chat-bridge daemon at boot to talk to the local LLM proxy.
2453
+ LITELLM_BASE_URL=${cfg.baseUrl}
2454
+ LITELLM_API_KEY=${cfg.apiKey}
2455
+ `;
2456
+ }
2457
+ function buildBridgeStartScript() {
2458
+ return `#!/usr/bin/env bash
2459
+ # Auto-generated by 'apes agents spawn --bridge'.
2460
+ # Idempotent installer + launcher. First boot installs @openape/chat-bridge
2461
+ # globally via bun (homebrew-provided); subsequent boots skip the install.
2462
+ set -euo pipefail
2463
+ export PATH="/opt/homebrew/bin:$HOME/.bun/bin:$PATH"
2464
+ if ! command -v openape-chat-bridge >/dev/null 2>&1; then
2465
+ bun add -g @openape/chat-bridge
2466
+ fi
2467
+ set -a
2468
+ . "$HOME/.pi/agent/.env"
2469
+ set +a
2470
+ exec openape-chat-bridge
2471
+ `;
2472
+ }
2473
+ function buildBridgePlist(homeDir) {
2474
+ const startScript = `${homeDir}/Library/Application Support/openape/bridge/start.sh`;
2475
+ const stdoutLog = `${homeDir}/Library/Logs/openape-chat-bridge.log`;
2476
+ const stderrLog = `${homeDir}/Library/Logs/openape-chat-bridge.err.log`;
2477
+ return `<?xml version="1.0" encoding="UTF-8"?>
2478
+ <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
2479
+ <plist version="1.0">
2480
+ <dict>
2481
+ <key>Label</key>
2482
+ <string>${BRIDGE_PLIST_LABEL}</string>
2483
+ <key>ProgramArguments</key>
2484
+ <array>
2485
+ <string>${startScript}</string>
2486
+ </array>
2487
+ <key>WorkingDirectory</key>
2488
+ <string>${homeDir}</string>
2489
+ <key>RunAtLoad</key>
2490
+ <true/>
2491
+ <key>KeepAlive</key>
2492
+ <true/>
2493
+ <key>ThrottleInterval</key>
2494
+ <integer>10</integer>
2495
+ <key>StandardOutPath</key>
2496
+ <string>${stdoutLog}</string>
2497
+ <key>StandardErrorPath</key>
2498
+ <string>${stderrLog}</string>
2499
+ <key>EnvironmentVariables</key>
2500
+ <dict>
2501
+ <key>HOME</key>
2502
+ <string>${homeDir}</string>
2503
+ <key>PATH</key>
2504
+ <string>/opt/homebrew/bin:/usr/bin:/bin</string>
2505
+ </dict>
2506
+ </dict>
2507
+ </plist>
2508
+ `;
2509
+ }
2510
+
2354
2511
  // src/commands/agents/spawn.ts
2355
2512
  var spawnAgentCommand = defineCommand23({
2356
2513
  meta: {
@@ -2378,6 +2535,18 @@ var spawnAgentCommand = defineCommand23({
2378
2535
  "claude-token-stdin": {
2379
2536
  type: "boolean",
2380
2537
  description: "Read the Claude Code OAuth token from stdin (paranoid form of --claude-token)."
2538
+ },
2539
+ "bridge": {
2540
+ type: "boolean",
2541
+ description: "Install the openape-chat-bridge daemon for this agent: drops a launchd plist that runs `@openape/chat-bridge` so the agent answers chat.openape.ai messages. Reads LITELLM_API_KEY/BASE_URL defaults from ~/litellm/.env; override via --bridge-key / --bridge-base-url."
2542
+ },
2543
+ "bridge-key": {
2544
+ type: "string",
2545
+ description: "Override LITELLM_API_KEY for the bridge (default: read from ~/litellm/.env)."
2546
+ },
2547
+ "bridge-base-url": {
2548
+ type: "string",
2549
+ description: "Override LITELLM_BASE_URL for the bridge (default: read from ~/litellm/.env or http://127.0.0.1:4000/v1)."
2381
2550
  }
2382
2551
  },
2383
2552
  async run({ args }) {
@@ -2423,8 +2592,8 @@ and try again.`
2423
2592
  throw new CliError(`macOS user "${name}" already exists (uid=${existing.uid ?? "?"}). Refusing to overwrite.`);
2424
2593
  }
2425
2594
  const homeDir = `/Users/${name}`;
2426
- const scratch = mkdtempSync2(join3(tmpdir2(), `apes-spawn-${name}-`));
2427
- const scriptPath = join3(scratch, "setup.sh");
2595
+ const scratch = mkdtempSync2(join4(tmpdir2(), `apes-spawn-${name}-`));
2596
+ const scriptPath = join4(scratch, "setup.sh");
2428
2597
  try {
2429
2598
  consola21.start(`Generating keypair for ${name}\u2026`);
2430
2599
  const { privatePem, publicSshLine } = generateKeyPairInMemory();
@@ -2448,6 +2617,18 @@ and try again.`
2448
2617
  flag: typeof args["claude-token"] === "string" ? args["claude-token"] : void 0,
2449
2618
  fromStdin: !!args["claude-token-stdin"]
2450
2619
  });
2620
+ const bridge = args.bridge ? (() => {
2621
+ const cfg = resolveBridgeConfig({
2622
+ cliKey: typeof args["bridge-key"] === "string" ? args["bridge-key"] : void 0,
2623
+ cliBaseUrl: typeof args["bridge-base-url"] === "string" ? args["bridge-base-url"] : void 0
2624
+ });
2625
+ return {
2626
+ plistLabel: BRIDGE_PLIST_LABEL,
2627
+ plistContent: buildBridgePlist(homeDir),
2628
+ startScript: buildBridgeStartScript(),
2629
+ envFile: buildBridgeEnvFile(cfg)
2630
+ };
2631
+ })() : null;
2451
2632
  const script = buildSpawnSetupScript({
2452
2633
  name,
2453
2634
  homeDir,
@@ -2457,7 +2638,8 @@ and try again.`
2457
2638
  authJson,
2458
2639
  claudeSettingsJson: includeClaudeHook ? CLAUDE_SETTINGS_JSON : null,
2459
2640
  hookScriptSource: includeClaudeHook ? BASH_VIA_APE_SHELL_HOOK_SOURCE : null,
2460
- claudeOauthToken
2641
+ claudeOauthToken,
2642
+ bridge
2461
2643
  });
2462
2644
  writeFileSync3(scriptPath, script, { mode: 448 });
2463
2645
  consola21.start("Running privileged setup as root via `apes run --as root --wait`\u2026");
@@ -3321,7 +3503,7 @@ import { spawn } from "child_process";
3321
3503
  import { mkdtempSync as mkdtempSync3, rmSync as rmSync3, writeFileSync as writeFileSync4 } from "fs";
3322
3504
  import { createRequire } from "module";
3323
3505
  import { tmpdir as tmpdir3 } from "os";
3324
- import { dirname as dirname2, join as join4, resolve as resolve3 } from "path";
3506
+ import { dirname as dirname2, join as join5, resolve as resolve3 } from "path";
3325
3507
  var require2 = createRequire(import.meta.url);
3326
3508
  function findProxyBin() {
3327
3509
  const pkgPath = require2.resolve("@openape/proxy/package.json");
@@ -3333,8 +3515,8 @@ function findProxyBin() {
3333
3515
  return resolve3(dirname2(pkgPath), binRel);
3334
3516
  }
3335
3517
  async function startEphemeralProxy(configToml) {
3336
- const tmpDir = mkdtempSync3(join4(tmpdir3(), "openape-proxy-"));
3337
- const configPath = join4(tmpDir, "config.toml");
3518
+ const tmpDir = mkdtempSync3(join5(tmpdir3(), "openape-proxy-"));
3519
+ const configPath = join5(tmpDir, "config.toml");
3338
3520
  writeFileSync4(configPath, configToml, { mode: 384 });
3339
3521
  const binPath = findProxyBin();
3340
3522
  const child = spawn(process.execPath, [binPath, "-c", configPath], {
@@ -3770,16 +3952,16 @@ var mcpCommand = defineCommand32({
3770
3952
  if (transport !== "stdio" && transport !== "sse") {
3771
3953
  throw new Error('Transport must be "stdio" or "sse"');
3772
3954
  }
3773
- const { startMcpServer } = await import("./server-GBFP2XSM.js");
3955
+ const { startMcpServer } = await import("./server-D5EQSCWF.js");
3774
3956
  await startMcpServer(transport, port);
3775
3957
  }
3776
3958
  });
3777
3959
 
3778
3960
  // src/commands/init/index.ts
3779
- import { existsSync as existsSync6, copyFileSync, writeFileSync as writeFileSync5 } from "fs";
3961
+ import { existsSync as existsSync7, copyFileSync, writeFileSync as writeFileSync5 } from "fs";
3780
3962
  import { randomBytes } from "crypto";
3781
3963
  import { execFileSync as execFileSync6 } from "child_process";
3782
- import { join as join5 } from "path";
3964
+ import { join as join6 } from "path";
3783
3965
  import { defineCommand as defineCommand33 } from "citty";
3784
3966
  import consola27 from "consola";
3785
3967
  var DEFAULT_IDP_URL = "https://id.openape.at";
@@ -3788,7 +3970,7 @@ async function downloadTemplate(repo, targetDir) {
3788
3970
  await gigetDownload(`gh:${repo}`, { dir: targetDir, force: false });
3789
3971
  }
3790
3972
  function installDeps(dir) {
3791
- const hasLockFile = (name) => existsSync6(join5(dir, name));
3973
+ const hasLockFile = (name) => existsSync7(join6(dir, name));
3792
3974
  if (hasLockFile("pnpm-lock.yaml")) {
3793
3975
  execFileSync6("pnpm", ["install"], { cwd: dir, stdio: "inherit" });
3794
3976
  } else if (hasLockFile("bun.lockb")) {
@@ -3853,7 +4035,7 @@ var initCommand = defineCommand33({
3853
4035
  });
3854
4036
  async function initSP(targetDir) {
3855
4037
  const dir = targetDir || "my-app";
3856
- if (existsSync6(join5(dir, "package.json"))) {
4038
+ if (existsSync7(join6(dir, "package.json"))) {
3857
4039
  throw new CliError(`Directory "${dir}" already contains a project.`);
3858
4040
  }
3859
4041
  consola27.start("Scaffolding SP starter...");
@@ -3862,9 +4044,9 @@ async function initSP(targetDir) {
3862
4044
  consola27.start("Installing dependencies...");
3863
4045
  installDeps(dir);
3864
4046
  consola27.success("Dependencies installed");
3865
- const envExample = join5(dir, ".env.example");
3866
- const envFile = join5(dir, ".env");
3867
- if (existsSync6(envExample) && !existsSync6(envFile)) {
4047
+ const envExample = join6(dir, ".env.example");
4048
+ const envFile = join6(dir, ".env");
4049
+ if (existsSync7(envExample) && !existsSync7(envFile)) {
3868
4050
  copyFileSync(envExample, envFile);
3869
4051
  consola27.success(`\`.env\` created (using Free IdP at ${DEFAULT_IDP_URL})`);
3870
4052
  }
@@ -3878,7 +4060,7 @@ async function initSP(targetDir) {
3878
4060
  }
3879
4061
  async function initIdP(targetDir) {
3880
4062
  const dir = targetDir || "my-idp";
3881
- if (existsSync6(join5(dir, "package.json"))) {
4063
+ if (existsSync7(join6(dir, "package.json"))) {
3882
4064
  throw new CliError(`Directory "${dir}" already contains a project.`);
3883
4065
  }
3884
4066
  const domain = await promptText("Domain for the IdP", "localhost");
@@ -3910,7 +4092,7 @@ async function initIdP(targetDir) {
3910
4092
  `NUXT_OPENAPE_RP_ID=${domain}`,
3911
4093
  `NUXT_OPENAPE_RP_ORIGIN=${origin}`
3912
4094
  ].join("\n");
3913
- writeFileSync5(join5(dir, ".env"), `${envContent}
4095
+ writeFileSync5(join6(dir, ".env"), `${envContent}
3914
4096
  `, { mode: 384 });
3915
4097
  consola27.success(".env created");
3916
4098
  console.log("");
@@ -3931,7 +4113,7 @@ async function initIdP(targetDir) {
3931
4113
 
3932
4114
  // src/commands/enroll.ts
3933
4115
  import { Buffer as Buffer5 } from "buffer";
3934
- import { existsSync as existsSync7, readFileSync as readFileSync5 } from "fs";
4116
+ import { existsSync as existsSync8, readFileSync as readFileSync6 } from "fs";
3935
4117
  import { execFile as execFile2 } from "child_process";
3936
4118
  import { sign as sign2 } from "crypto";
3937
4119
  import { defineCommand as defineCommand34 } from "citty";
@@ -3947,7 +4129,7 @@ function openBrowser2(url) {
3947
4129
  }
3948
4130
  async function pollForEnrollment(idp, agentEmail, keyPath) {
3949
4131
  const resolvedKey = resolveKeyPath(keyPath);
3950
- const keyContent = readFileSync5(resolvedKey, "utf-8");
4132
+ const keyContent = readFileSync6(resolvedKey, "utf-8");
3951
4133
  const privateKey = loadEd25519PrivateKey(keyContent);
3952
4134
  const challengeUrl = await getAgentChallengeEndpoint(idp);
3953
4135
  const authenticateUrl = await getAgentAuthenticateEndpoint(idp);
@@ -4015,7 +4197,7 @@ var enrollCommand = defineCommand34({
4015
4197
  }) || DEFAULT_KEY_PATH;
4016
4198
  const resolvedKey = resolveKeyPath(keyPath);
4017
4199
  let publicKey;
4018
- if (existsSync7(resolvedKey)) {
4200
+ if (existsSync8(resolvedKey)) {
4019
4201
  publicKey = readPublicKey(resolvedKey);
4020
4202
  consola28.success(`Using existing key ${keyPath}`);
4021
4203
  } else {
@@ -4059,7 +4241,7 @@ var enrollCommand = defineCommand34({
4059
4241
  });
4060
4242
 
4061
4243
  // src/commands/register-user.ts
4062
- import { existsSync as existsSync8, readFileSync as readFileSync6 } from "fs";
4244
+ import { existsSync as existsSync9, readFileSync as readFileSync7 } from "fs";
4063
4245
  import { defineCommand as defineCommand35 } from "citty";
4064
4246
  import consola29 from "consola";
4065
4247
  var registerUserCommand = defineCommand35({
@@ -4098,8 +4280,8 @@ var registerUserCommand = defineCommand35({
4098
4280
  throw new CliError("No IdP URL configured. Run `apes login` first.");
4099
4281
  }
4100
4282
  let publicKey = args.key;
4101
- if (existsSync8(args.key)) {
4102
- publicKey = readFileSync6(args.key, "utf-8").trim();
4283
+ if (existsSync9(args.key)) {
4284
+ publicKey = readFileSync7(args.key, "utf-8").trim();
4103
4285
  }
4104
4286
  if (!publicKey.startsWith("ssh-ed25519 ")) {
4105
4287
  throw new CliError("Public key must be in ssh-ed25519 format.");
@@ -4408,7 +4590,7 @@ async function bestEffortGrantCount(idp) {
4408
4590
  }
4409
4591
  }
4410
4592
  async function runHealth(args) {
4411
- const version = true ? "0.22.0" : "0.0.0";
4593
+ const version = true ? "0.23.0" : "0.0.0";
4412
4594
  const auth = loadAuth();
4413
4595
  if (!auth) {
4414
4596
  throw new CliError("Not logged in. Run `apes login` first.", 1);
@@ -4601,25 +4783,25 @@ var workflowsCommand = defineCommand43({
4601
4783
  });
4602
4784
 
4603
4785
  // src/version-check.ts
4604
- import { existsSync as existsSync9, mkdirSync as mkdirSync2, readFileSync as readFileSync7, writeFileSync as writeFileSync6 } from "fs";
4605
- import { homedir as homedir5 } from "os";
4606
- import { join as join6 } from "path";
4786
+ import { existsSync as existsSync10, mkdirSync as mkdirSync2, readFileSync as readFileSync8, writeFileSync as writeFileSync6 } from "fs";
4787
+ import { homedir as homedir6 } from "os";
4788
+ import { join as join7 } from "path";
4607
4789
  import consola35 from "consola";
4608
4790
  var PACKAGE_NAME = "@openape/apes";
4609
4791
  var CACHE_TTL_MS = 24 * 60 * 60 * 1e3;
4610
- var CACHE_FILE = join6(homedir5(), ".config", "apes", ".version-check.json");
4792
+ var CACHE_FILE = join7(homedir6(), ".config", "apes", ".version-check.json");
4611
4793
  function readCache() {
4612
- if (!existsSync9(CACHE_FILE)) return null;
4794
+ if (!existsSync10(CACHE_FILE)) return null;
4613
4795
  try {
4614
- return JSON.parse(readFileSync7(CACHE_FILE, "utf-8"));
4796
+ return JSON.parse(readFileSync8(CACHE_FILE, "utf-8"));
4615
4797
  } catch {
4616
4798
  return null;
4617
4799
  }
4618
4800
  }
4619
4801
  function writeCache(entry) {
4620
4802
  try {
4621
- const dir = join6(homedir5(), ".config", "apes");
4622
- if (!existsSync9(dir)) mkdirSync2(dir, { recursive: true, mode: 448 });
4803
+ const dir = join7(homedir6(), ".config", "apes");
4804
+ if (!existsSync10(dir)) mkdirSync2(dir, { recursive: true, mode: 448 });
4623
4805
  writeFileSync6(CACHE_FILE, JSON.stringify(entry), { mode: 384 });
4624
4806
  } catch {
4625
4807
  }
@@ -4681,10 +4863,10 @@ if (shellRewrite) {
4681
4863
  if (shellRewrite.action === "rewrite") {
4682
4864
  process.argv = shellRewrite.argv;
4683
4865
  } else if (shellRewrite.action === "version") {
4684
- console.log(`ape-shell ${"0.22.0"} (OpenApe DDISA shell wrapper)`);
4866
+ console.log(`ape-shell ${"0.23.0"} (OpenApe DDISA shell wrapper)`);
4685
4867
  process.exit(0);
4686
4868
  } else if (shellRewrite.action === "help") {
4687
- console.log(`ape-shell ${"0.22.0"} \u2014 OpenApe DDISA shell wrapper`);
4869
+ console.log(`ape-shell ${"0.23.0"} \u2014 OpenApe DDISA shell wrapper`);
4688
4870
  console.log("");
4689
4871
  console.log("Usage:");
4690
4872
  console.log(" ape-shell Start interactive grant-mediated REPL");
@@ -4742,7 +4924,7 @@ var configCommand = defineCommand44({
4742
4924
  var main = defineCommand44({
4743
4925
  meta: {
4744
4926
  name: "apes",
4745
- version: "0.22.0",
4927
+ version: "0.23.0",
4746
4928
  description: "Unified CLI for OpenApe"
4747
4929
  },
4748
4930
  subCommands: {
@@ -4797,7 +4979,7 @@ async function maybeRefreshAuth() {
4797
4979
  }
4798
4980
  }
4799
4981
  await maybeRefreshAuth();
4800
- await maybeWarnStaleVersion("0.22.0").catch(() => {
4982
+ await maybeWarnStaleVersion("0.23.0").catch(() => {
4801
4983
  });
4802
4984
  runMain(main).catch((err) => {
4803
4985
  if (err instanceof CliExit) {