@openape/apes 0.23.0 → 0.25.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
@@ -1891,23 +1891,26 @@ ${buildBridgeBootstrapBlock(input.bridge)}`;
1891
1891
  function buildBridgeBlock(bridge) {
1892
1892
  if (!bridge) return "";
1893
1893
  return `
1894
- mkdir -p "$HOME_DIR/Library/LaunchAgents" "$HOME_DIR/Library/Application Support/openape/bridge" "$HOME_DIR/Library/Logs" "$HOME_DIR/.pi/agent"
1894
+ mkdir -p "$HOME_DIR/Library/Application Support/openape/bridge" "$HOME_DIR/Library/Logs" "$HOME_DIR/.pi/agent"
1895
1895
  cat > "$HOME_DIR/.pi/agent/.env" ${shHeredoc(bridge.envFile)}
1896
1896
  cat > "$HOME_DIR/Library/Application Support/openape/bridge/start.sh" ${shHeredoc(bridge.startScript)}
1897
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
1898
  chmod 600 "$HOME_DIR/.pi/agent/.env"
1899
+
1900
+ # System-wide LaunchDaemon \u2014 root-owned, mode 644 (launchd refuses
1901
+ # group/world-writable plists). UserName in the plist makes launchd run
1902
+ # the binary as the agent, not root.
1903
+ cat > ${shQuote(bridge.plistPath)} ${shHeredoc(bridge.plistContent)}
1904
+ chown root:wheel ${shQuote(bridge.plistPath)}
1905
+ chmod 644 ${shQuote(bridge.plistPath)}
1900
1906
  `;
1901
1907
  }
1902
1908
  function buildBridgeBootstrapBlock(bridge) {
1903
1909
  if (!bridge) return "";
1904
1910
  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"
1911
+ launchctl bootout "system/${bridge.plistLabel}" 2>/dev/null || true
1912
+ launchctl bootstrap system ${shQuote(bridge.plistPath)} || \\
1913
+ echo "warn: bridge bootstrap into system domain failed; check ${bridge.plistPath}"
1911
1914
  `;
1912
1915
  }
1913
1916
  function buildDestroyTeardownScript(input) {
@@ -1938,6 +1941,16 @@ if [ -n "$UID_OF" ]; then
1938
1941
  pkill -9 -u "$UID_OF" 2>/dev/null || true
1939
1942
  fi
1940
1943
 
1944
+ # Per-agent system LaunchDaemon written by spawn --bridge. Bootout +
1945
+ # delete must come BEFORE we delete the user, otherwise launchd keeps a
1946
+ # zombie reference. No-op if the plist isn't there.
1947
+ BRIDGE_LABEL="eco.hofmann.apes.bridge.$NAME"
1948
+ BRIDGE_PLIST="/Library/LaunchDaemons/$BRIDGE_LABEL.plist"
1949
+ if [ -f "$BRIDGE_PLIST" ]; then
1950
+ launchctl bootout "system/$BRIDGE_LABEL" 2>/dev/null || true
1951
+ rm -f "$BRIDGE_PLIST"
1952
+ fi
1953
+
1941
1954
  if [ -d "$HOME_DIR" ] && [ "$HOME_DIR" != "/" ] && [ "$HOME_DIR" != "" ]; then
1942
1955
  rm -rf "$HOME_DIR"
1943
1956
  fi
@@ -2074,6 +2087,60 @@ function isShellRegistered(shellPath) {
2074
2087
  return content.split("\n").map((l) => l.trim()).filter((l) => l && !l.startsWith("#")).includes(shellPath);
2075
2088
  }
2076
2089
 
2090
+ // src/lib/silent-password.ts
2091
+ function readPasswordSilent(prompt) {
2092
+ if (!process.stdin.isTTY) {
2093
+ return Promise.reject(new CliError(
2094
+ "No TTY available for the silent password prompt. Set APES_ADMIN_PASSWORD in the environment instead."
2095
+ ));
2096
+ }
2097
+ return new Promise((resolve4, reject) => {
2098
+ process.stdout.write(prompt);
2099
+ const wasRaw = process.stdin.isRaw ?? false;
2100
+ process.stdin.setRawMode(true);
2101
+ process.stdin.resume();
2102
+ process.stdin.setEncoding("utf8");
2103
+ let buf = "";
2104
+ let cleanupFn;
2105
+ const cleanup = () => cleanupFn?.();
2106
+ const onData = (chunk) => {
2107
+ for (const ch of chunk) {
2108
+ const code = ch.charCodeAt(0);
2109
+ if (ch === "\r" || ch === "\n") {
2110
+ cleanup();
2111
+ process.stdout.write("\n");
2112
+ resolve4(buf);
2113
+ return;
2114
+ }
2115
+ if (code === 3) {
2116
+ cleanup();
2117
+ process.stdout.write("\n");
2118
+ reject(new CliError("Aborted by user (Ctrl-C)."));
2119
+ return;
2120
+ }
2121
+ if (code === 4 && buf.length === 0) {
2122
+ cleanup();
2123
+ process.stdout.write("\n");
2124
+ reject(new CliError("Aborted by user (Ctrl-D)."));
2125
+ return;
2126
+ }
2127
+ if (code === 127 || code === 8) {
2128
+ if (buf.length > 0) buf = buf.slice(0, -1);
2129
+ continue;
2130
+ }
2131
+ if (code < 32) continue;
2132
+ buf += ch;
2133
+ }
2134
+ };
2135
+ cleanupFn = () => {
2136
+ process.stdin.removeListener("data", onData);
2137
+ process.stdin.setRawMode(wasRaw);
2138
+ process.stdin.pause();
2139
+ };
2140
+ process.stdin.on("data", onData);
2141
+ });
2142
+ }
2143
+
2077
2144
  // src/commands/agents/destroy.ts
2078
2145
  var destroyAgentCommand = defineCommand20({
2079
2146
  meta: {
@@ -2153,25 +2220,21 @@ ${consequences.join("\n")}`);
2153
2220
  consola18.info("No IdP agent to remove (skipped).");
2154
2221
  }
2155
2222
  if (osUserExists) {
2156
- const apes = whichBinary("apes");
2157
- if (!apes) {
2158
- throw new CliError("`apes` not found on PATH. Install @openape/apes globally first.");
2159
- }
2160
- const escapes = whichBinary("escapes");
2161
- if (!escapes) {
2162
- throw new CliError("`escapes` not found on PATH; OS teardown requires escapes.");
2223
+ const sudo = whichBinary("sudo");
2224
+ if (!sudo) {
2225
+ throw new CliError("`sudo` not found on PATH; required for OS teardown.");
2163
2226
  }
2164
2227
  const adminUser = userInfo().username;
2165
- const adminPassword = await collectAdminPassword({ adminUser, force: !!args.force });
2228
+ const adminPassword = await collectAdminPassword({ adminUser });
2166
2229
  const scratch = mkdtempSync(join2(tmpdir(), `apes-destroy-${name}-`));
2167
2230
  const scriptPath = join2(scratch, "teardown.sh");
2168
2231
  try {
2169
2232
  const script = buildDestroyTeardownScript({ name, homeDir: `/Users/${name}`, adminUser });
2170
2233
  writeFileSync(scriptPath, script, { mode: 448 });
2171
- consola18.start("Running teardown as root via `apes run --as root --wait`\u2026");
2172
- consola18.info("You will be asked to approve the as=root grant in your DDISA inbox; this command blocks until you do.");
2173
- execFileSync3(apes, ["run", "--as", "root", "--wait", "--", "bash", scriptPath], {
2234
+ consola18.start("Running teardown via sudo\u2026");
2235
+ execFileSync3(sudo, ["-S", "--prompt=", "--", "bash", scriptPath], {
2174
2236
  input: `${adminPassword}
2237
+ ${adminPassword}
2175
2238
  `,
2176
2239
  stdio: ["pipe", "inherit", "inherit"]
2177
2240
  });
@@ -2187,14 +2250,8 @@ ${consequences.join("\n")}`);
2187
2250
  async function collectAdminPassword(opts) {
2188
2251
  const fromEnv = process.env.APES_ADMIN_PASSWORD;
2189
2252
  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) {
2253
+ const pw = await readPasswordSilent(`Password for ${opts.adminUser}: `);
2254
+ if (pw.length === 0) {
2198
2255
  throw new CliExit(0);
2199
2256
  }
2200
2257
  return pw;
@@ -2355,6 +2412,57 @@ import { join as join4 } from "path";
2355
2412
  import { defineCommand as defineCommand23 } from "citty";
2356
2413
  import consola21 from "consola";
2357
2414
 
2415
+ // src/lib/chat-room.ts
2416
+ var DEFAULT_CHAT_ENDPOINT = "https://chat.openape.ai";
2417
+ function chatEndpoint() {
2418
+ return (process.env.APE_CHAT_ENDPOINT ?? DEFAULT_CHAT_ENDPOINT).replace(/\/$/, "");
2419
+ }
2420
+ async function chatFetch(bearer, path2, init) {
2421
+ const url = `${chatEndpoint()}${path2}`;
2422
+ const res = await fetch(url, {
2423
+ method: init?.method ?? "GET",
2424
+ headers: {
2425
+ Authorization: `Bearer ${bearer}`,
2426
+ "Content-Type": "application/json"
2427
+ },
2428
+ body: init?.body !== void 0 ? JSON.stringify(init.body) : void 0
2429
+ });
2430
+ if (!res.ok) {
2431
+ const detail = await res.text().catch(() => "");
2432
+ throw new Error(`chat.openape.ai ${init?.method ?? "GET"} ${path2} \u2192 ${res.status}: ${detail.slice(0, 200)}`);
2433
+ }
2434
+ return await res.json();
2435
+ }
2436
+ async function findRoomByName(bearer, name) {
2437
+ const rooms = await chatFetch(bearer, "/api/rooms");
2438
+ return rooms.find((r) => r.name === name) ?? null;
2439
+ }
2440
+ async function createRoom(bearer, name) {
2441
+ return chatFetch(bearer, "/api/rooms", {
2442
+ method: "POST",
2443
+ body: { name, kind: "channel", members: [] }
2444
+ });
2445
+ }
2446
+ async function addMember(bearer, roomId, email, role = "member") {
2447
+ await chatFetch(bearer, `/api/rooms/${encodeURIComponent(roomId)}/members`, {
2448
+ method: "POST",
2449
+ body: { email, role }
2450
+ });
2451
+ }
2452
+ async function ensureRoomMembership(opts) {
2453
+ const existing = await findRoomByName(opts.callerBearer, opts.roomName);
2454
+ let room;
2455
+ let created = false;
2456
+ if (existing) {
2457
+ room = existing;
2458
+ } else {
2459
+ room = await createRoom(opts.callerBearer, opts.roomName);
2460
+ created = true;
2461
+ }
2462
+ await addMember(opts.callerBearer, room.id, opts.agentEmail);
2463
+ return { roomId: room.id, created };
2464
+ }
2465
+
2358
2466
  // src/lib/keygen.ts
2359
2467
  import { Buffer as Buffer4 } from "buffer";
2360
2468
  import { existsSync as existsSync5, mkdirSync, readFileSync as readFileSync4, writeFileSync as writeFileSync2 } from "fs";
@@ -2415,7 +2523,7 @@ function generateKeyPairInMemory() {
2415
2523
  import { existsSync as existsSync6, readFileSync as readFileSync5 } from "fs";
2416
2524
  import { homedir as homedir5 } from "os";
2417
2525
  import { join as join3 } from "path";
2418
- var BRIDGE_PLIST_LABEL = "eco.hofmann.apes.bridge";
2526
+ var PLIST_LABEL_PREFIX = "eco.hofmann.apes.bridge";
2419
2527
  function readLitellmEnv(envPath = join3(homedir5(), "litellm", ".env")) {
2420
2528
  if (!existsSync6(envPath)) return null;
2421
2529
  try {
@@ -2447,6 +2555,12 @@ function resolveBridgeConfig(opts) {
2447
2555
  }
2448
2556
  return { baseUrl, apiKey };
2449
2557
  }
2558
+ function bridgePlistLabel(agentName) {
2559
+ return `${PLIST_LABEL_PREFIX}.${agentName}`;
2560
+ }
2561
+ function bridgePlistPath(agentName) {
2562
+ return `/Library/LaunchDaemons/${bridgePlistLabel(agentName)}.plist`;
2563
+ }
2450
2564
  function buildBridgeEnvFile(cfg) {
2451
2565
  return `# Auto-generated by 'apes agents spawn --bridge'.
2452
2566
  # Read by the chat-bridge daemon at boot to talk to the local LLM proxy.
@@ -2457,20 +2571,66 @@ LITELLM_API_KEY=${cfg.apiKey}
2457
2571
  function buildBridgeStartScript() {
2458
2572
  return `#!/usr/bin/env bash
2459
2573
  # 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.
2574
+ # Idempotent installer + launcher.
2462
2575
  set -euo pipefail
2463
- export PATH="/opt/homebrew/bin:$HOME/.bun/bin:$PATH"
2576
+
2577
+ export NPM_CONFIG_PREFIX="$HOME/.npm-global"
2578
+ export PATH="$NPM_CONFIG_PREFIX/bin:/opt/homebrew/bin:/usr/local/bin:/usr/bin:/bin"
2579
+ mkdir -p "$NPM_CONFIG_PREFIX"
2580
+
2464
2581
  if ! command -v openape-chat-bridge >/dev/null 2>&1; then
2465
- bun add -g @openape/chat-bridge
2582
+ npm install -g --silent @openape/chat-bridge
2583
+ fi
2584
+
2585
+ if ! command -v pi >/dev/null 2>&1; then
2586
+ npm install -g --silent @mariozechner/pi-coding-agent
2587
+ fi
2588
+
2589
+ EXT_DIR="$HOME/.pi/agent/extensions"
2590
+ mkdir -p "$EXT_DIR"
2591
+ if [ ! -f "$EXT_DIR/litellm.ts" ]; then
2592
+ cat > "$EXT_DIR/litellm.ts" <<'PI_EXT_EOF'
2593
+ import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
2594
+
2595
+ const BASE_URL = process.env.LITELLM_BASE_URL ?? "http://127.0.0.1:4000/v1";
2596
+
2597
+ export default async function (pi: ExtensionAPI) {
2598
+ pi.registerProvider("litellm", {
2599
+ baseUrl: BASE_URL,
2600
+ apiKey: "LITELLM_API_KEY",
2601
+ api: "openai-completions",
2602
+ models: [
2603
+ {
2604
+ id: "gpt-5.4",
2605
+ name: "ChatGPT 5.4 (Subscription)",
2606
+ reasoning: false,
2607
+ input: ["text", "image"],
2608
+ cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
2609
+ contextWindow: 200000,
2610
+ maxTokens: 8192,
2611
+ },
2612
+ {
2613
+ id: "gpt-5.3-codex",
2614
+ name: "ChatGPT 5.3 Codex (Subscription)",
2615
+ reasoning: false,
2616
+ input: ["text"],
2617
+ cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
2618
+ contextWindow: 200000,
2619
+ maxTokens: 8192,
2620
+ },
2621
+ ],
2622
+ });
2623
+ }
2624
+ PI_EXT_EOF
2466
2625
  fi
2626
+
2467
2627
  set -a
2468
2628
  . "$HOME/.pi/agent/.env"
2469
2629
  set +a
2470
2630
  exec openape-chat-bridge
2471
2631
  `;
2472
2632
  }
2473
- function buildBridgePlist(homeDir) {
2633
+ function buildBridgePlist(agentName, homeDir) {
2474
2634
  const startScript = `${homeDir}/Library/Application Support/openape/bridge/start.sh`;
2475
2635
  const stdoutLog = `${homeDir}/Library/Logs/openape-chat-bridge.log`;
2476
2636
  const stderrLog = `${homeDir}/Library/Logs/openape-chat-bridge.err.log`;
@@ -2479,9 +2639,12 @@ function buildBridgePlist(homeDir) {
2479
2639
  <plist version="1.0">
2480
2640
  <dict>
2481
2641
  <key>Label</key>
2482
- <string>${BRIDGE_PLIST_LABEL}</string>
2642
+ <string>${bridgePlistLabel(agentName)}</string>
2643
+ <key>UserName</key>
2644
+ <string>${agentName}</string>
2483
2645
  <key>ProgramArguments</key>
2484
2646
  <array>
2647
+ <string>/bin/bash</string>
2485
2648
  <string>${startScript}</string>
2486
2649
  </array>
2487
2650
  <key>WorkingDirectory</key>
@@ -2501,7 +2664,7 @@ function buildBridgePlist(homeDir) {
2501
2664
  <key>HOME</key>
2502
2665
  <string>${homeDir}</string>
2503
2666
  <key>PATH</key>
2504
- <string>/opt/homebrew/bin:/usr/bin:/bin</string>
2667
+ <string>/opt/homebrew/bin:/usr/local/bin:/usr/bin:/bin</string>
2505
2668
  </dict>
2506
2669
  </dict>
2507
2670
  </plist>
@@ -2547,6 +2710,10 @@ var spawnAgentCommand = defineCommand23({
2547
2710
  "bridge-base-url": {
2548
2711
  type: "string",
2549
2712
  description: "Override LITELLM_BASE_URL for the bridge (default: read from ~/litellm/.env or http://127.0.0.1:4000/v1)."
2713
+ },
2714
+ "bridge-room": {
2715
+ type: "string",
2716
+ description: "After spawn, create (or find) a chat.openape.ai room with this name and add the new agent as a member. Uses the spawning user's IdP bearer."
2550
2717
  }
2551
2718
  },
2552
2719
  async run({ args }) {
@@ -2623,8 +2790,9 @@ and try again.`
2623
2790
  cliBaseUrl: typeof args["bridge-base-url"] === "string" ? args["bridge-base-url"] : void 0
2624
2791
  });
2625
2792
  return {
2626
- plistLabel: BRIDGE_PLIST_LABEL,
2627
- plistContent: buildBridgePlist(homeDir),
2793
+ plistLabel: bridgePlistLabel(name),
2794
+ plistPath: bridgePlistPath(name),
2795
+ plistContent: buildBridgePlist(name, homeDir),
2628
2796
  startScript: buildBridgeStartScript(),
2629
2797
  envFile: buildBridgeEnvFile(cfg)
2630
2798
  };
@@ -2646,6 +2814,24 @@ and try again.`
2646
2814
  consola21.info("You will be asked to approve the as=root grant in your DDISA inbox; this command blocks until you do.");
2647
2815
  execFileSync4(apes, ["run", "--as", "root", "--wait", "--", "bash", scriptPath], { stdio: "inherit" });
2648
2816
  consola21.success(`Agent ${name} spawned.`);
2817
+ const bridgeRoom = typeof args["bridge-room"] === "string" ? args["bridge-room"] : void 0;
2818
+ if (args.bridge && bridgeRoom) {
2819
+ try {
2820
+ consola21.start(`Inviting agent into chat.openape.ai room "${bridgeRoom}"\u2026`);
2821
+ const result = await ensureRoomMembership({
2822
+ callerBearer: auth.access_token,
2823
+ roomName: bridgeRoom,
2824
+ agentEmail: registration.email
2825
+ });
2826
+ consola21.success(
2827
+ result.created ? `Created room ${result.roomId} and added ${registration.email}` : `Room ${result.roomId} already existed; added ${registration.email}`
2828
+ );
2829
+ } catch (err) {
2830
+ const msg = err instanceof Error ? err.message : String(err);
2831
+ consola21.warn(`Could not auto-create / invite to chat room: ${msg}`);
2832
+ consola21.info("Add the agent manually with: ape-chat members add <agent-email>");
2833
+ }
2834
+ }
2649
2835
  console.log("");
2650
2836
  console.log("Run as the agent with:");
2651
2837
  console.log(` apes run --as ${name} -- claude --session-name ${name} --dangerously-skip-permissions`);
@@ -3952,7 +4138,7 @@ var mcpCommand = defineCommand32({
3952
4138
  if (transport !== "stdio" && transport !== "sse") {
3953
4139
  throw new Error('Transport must be "stdio" or "sse"');
3954
4140
  }
3955
- const { startMcpServer } = await import("./server-D5EQSCWF.js");
4141
+ const { startMcpServer } = await import("./server-3HW3TBY5.js");
3956
4142
  await startMcpServer(transport, port);
3957
4143
  }
3958
4144
  });
@@ -4590,7 +4776,7 @@ async function bestEffortGrantCount(idp) {
4590
4776
  }
4591
4777
  }
4592
4778
  async function runHealth(args) {
4593
- const version = true ? "0.23.0" : "0.0.0";
4779
+ const version = true ? "0.25.0" : "0.0.0";
4594
4780
  const auth = loadAuth();
4595
4781
  if (!auth) {
4596
4782
  throw new CliError("Not logged in. Run `apes login` first.", 1);
@@ -4863,10 +5049,10 @@ if (shellRewrite) {
4863
5049
  if (shellRewrite.action === "rewrite") {
4864
5050
  process.argv = shellRewrite.argv;
4865
5051
  } else if (shellRewrite.action === "version") {
4866
- console.log(`ape-shell ${"0.23.0"} (OpenApe DDISA shell wrapper)`);
5052
+ console.log(`ape-shell ${"0.25.0"} (OpenApe DDISA shell wrapper)`);
4867
5053
  process.exit(0);
4868
5054
  } else if (shellRewrite.action === "help") {
4869
- console.log(`ape-shell ${"0.23.0"} \u2014 OpenApe DDISA shell wrapper`);
5055
+ console.log(`ape-shell ${"0.25.0"} \u2014 OpenApe DDISA shell wrapper`);
4870
5056
  console.log("");
4871
5057
  console.log("Usage:");
4872
5058
  console.log(" ape-shell Start interactive grant-mediated REPL");
@@ -4924,7 +5110,7 @@ var configCommand = defineCommand44({
4924
5110
  var main = defineCommand44({
4925
5111
  meta: {
4926
5112
  name: "apes",
4927
- version: "0.23.0",
5113
+ version: "0.25.0",
4928
5114
  description: "Unified CLI for OpenApe"
4929
5115
  },
4930
5116
  subCommands: {
@@ -4979,7 +5165,7 @@ async function maybeRefreshAuth() {
4979
5165
  }
4980
5166
  }
4981
5167
  await maybeRefreshAuth();
4982
- await maybeWarnStaleVersion("0.23.0").catch(() => {
5168
+ await maybeWarnStaleVersion("0.25.0").catch(() => {
4983
5169
  });
4984
5170
  runMain(main).catch((err) => {
4985
5171
  if (err instanceof CliExit) {