@openape/apes 0.31.3 → 1.0.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
|
@@ -63,7 +63,7 @@ import {
|
|
|
63
63
|
} from "./chunk-OBF7IMQ2.js";
|
|
64
64
|
|
|
65
65
|
// src/cli.ts
|
|
66
|
-
import
|
|
66
|
+
import consola39 from "consola";
|
|
67
67
|
|
|
68
68
|
// src/ape-shell.ts
|
|
69
69
|
import path from "path";
|
|
@@ -93,7 +93,7 @@ function rewriteApeShellArgs(argv, argv0) {
|
|
|
93
93
|
}
|
|
94
94
|
|
|
95
95
|
// src/cli.ts
|
|
96
|
-
import { defineCommand as
|
|
96
|
+
import { defineCommand as defineCommand48, runMain } from "citty";
|
|
97
97
|
|
|
98
98
|
// src/commands/auth/login.ts
|
|
99
99
|
import { Buffer as Buffer2 } from "buffer";
|
|
@@ -306,7 +306,7 @@ async function loginWithPKCE(idp) {
|
|
|
306
306
|
authUrl.searchParams.set("state", state);
|
|
307
307
|
authUrl.searchParams.set("nonce", nonce);
|
|
308
308
|
authUrl.searchParams.set("scope", "openid email profile offline_access");
|
|
309
|
-
const code = await new Promise((
|
|
309
|
+
const code = await new Promise((resolve5, reject) => {
|
|
310
310
|
const server = createServer((req, res) => {
|
|
311
311
|
const url = new URL(req.url, `http://localhost:${CALLBACK_PORT}`);
|
|
312
312
|
if (url.pathname === "/callback") {
|
|
@@ -323,7 +323,7 @@ async function loginWithPKCE(idp) {
|
|
|
323
323
|
res.writeHead(200, { "Content-Type": "text/html" });
|
|
324
324
|
res.end("<h1>Login successful!</h1><p>You can close this window.</p>");
|
|
325
325
|
server.close();
|
|
326
|
-
|
|
326
|
+
resolve5(authCode);
|
|
327
327
|
return;
|
|
328
328
|
}
|
|
329
329
|
res.writeHead(400);
|
|
@@ -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:
|
|
382
|
+
const { readFileSync: readFileSync14 } = 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 =
|
|
395
|
+
const keyContent = readFileSync14(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);
|
|
@@ -884,7 +884,7 @@ async function waitForApproval2(grantsUrl, grantId) {
|
|
|
884
884
|
if (grant.status === "revoked") {
|
|
885
885
|
throw new CliError("Grant revoked.");
|
|
886
886
|
}
|
|
887
|
-
await new Promise((
|
|
887
|
+
await new Promise((resolve5) => setTimeout(resolve5, interval));
|
|
888
888
|
}
|
|
889
889
|
throw new CliError("Timed out waiting for approval.");
|
|
890
890
|
}
|
|
@@ -1744,7 +1744,7 @@ var adminCommand = defineCommand19({
|
|
|
1744
1744
|
});
|
|
1745
1745
|
|
|
1746
1746
|
// src/commands/agents/index.ts
|
|
1747
|
-
import { defineCommand as
|
|
1747
|
+
import { defineCommand as defineCommand28 } from "citty";
|
|
1748
1748
|
|
|
1749
1749
|
// src/commands/agents/allow.ts
|
|
1750
1750
|
import { execFileSync as execFileSync3 } from "child_process";
|
|
@@ -1871,7 +1871,7 @@ mkdir -p "$HOME_DIR/.ssh" "$HOME_DIR/.config/apes"
|
|
|
1871
1871
|
cat > "$HOME_DIR/.ssh/id_ed25519" ${shHeredoc(privatePemForHeredoc.trimEnd())}
|
|
1872
1872
|
cat > "$HOME_DIR/.ssh/id_ed25519.pub" ${shHeredoc(`${input.publicKeySshLine}`)}
|
|
1873
1873
|
cat > "$HOME_DIR/.config/apes/auth.json" ${shHeredoc(input.authJson)}
|
|
1874
|
-
${claudeBlock}${claudeTokenBlock}${buildBridgeBlock(input.bridge)}
|
|
1874
|
+
${claudeBlock}${claudeTokenBlock}${buildBridgeBlock(input.bridge)}${buildTroopBlock(input.troop)}
|
|
1875
1875
|
chown -R "$NAME:staff" "$HOME_DIR"
|
|
1876
1876
|
chmod 700 "$HOME_DIR/.ssh"
|
|
1877
1877
|
chmod 700 "$HOME_DIR/.config"
|
|
@@ -1884,16 +1884,16 @@ if [ -f "$HOME_DIR/.config/openape/claude-token.env" ]; then
|
|
|
1884
1884
|
fi
|
|
1885
1885
|
|
|
1886
1886
|
echo "OK $NAME uid=$NEXT_UID home=$HOME_DIR"
|
|
1887
|
-
${buildBridgeBootstrapBlock(input.bridge)}`;
|
|
1887
|
+
${buildBridgeBootstrapBlock(input.bridge)}${buildTroopBootstrapBlock(input.troop)}`;
|
|
1888
1888
|
}
|
|
1889
1889
|
function buildBridgeBlock(bridge) {
|
|
1890
1890
|
if (!bridge) return "";
|
|
1891
1891
|
return `
|
|
1892
|
-
mkdir -p "$HOME_DIR/Library/Application Support/openape/bridge" "$HOME_DIR/Library/Logs"
|
|
1893
|
-
cat > "$HOME_DIR
|
|
1892
|
+
mkdir -p "$HOME_DIR/Library/Application Support/openape/bridge" "$HOME_DIR/Library/Logs"
|
|
1893
|
+
cat > "$HOME_DIR/Library/Application Support/openape/bridge/.env" ${shHeredoc(bridge.envFile)}
|
|
1894
1894
|
cat > "$HOME_DIR/Library/Application Support/openape/bridge/start.sh" ${shHeredoc(bridge.startScript)}
|
|
1895
1895
|
chmod 755 "$HOME_DIR/Library/Application Support/openape/bridge/start.sh"
|
|
1896
|
-
chmod 600 "$HOME_DIR
|
|
1896
|
+
chmod 600 "$HOME_DIR/Library/Application Support/openape/bridge/.env"
|
|
1897
1897
|
|
|
1898
1898
|
# System-wide LaunchDaemon \u2014 root-owned, mode 644 (launchd refuses
|
|
1899
1899
|
# group/world-writable plists). UserName in the plist makes launchd run
|
|
@@ -1910,7 +1910,7 @@ echo "==> Installing bridge stack as $NAME via bun (one-time)\u2026"
|
|
|
1910
1910
|
su - "$NAME" -c '
|
|
1911
1911
|
set -euo pipefail
|
|
1912
1912
|
export PATH="/opt/homebrew/bin:/usr/local/bin:/usr/bin:/bin:$HOME/.bun/install/global/bin"
|
|
1913
|
-
bun add -g @openape/chat-bridge @openape/apes
|
|
1913
|
+
bun add -g @openape/chat-bridge @openape/apes
|
|
1914
1914
|
'
|
|
1915
1915
|
|
|
1916
1916
|
# Bootstrap into the system domain. Spawn already runs as root via
|
|
@@ -1921,6 +1921,31 @@ launchctl bootstrap system ${shQuote(bridge.plistPath)} || \\
|
|
|
1921
1921
|
echo "warn: bridge bootstrap into system domain failed; check ${bridge.plistPath}"
|
|
1922
1922
|
`;
|
|
1923
1923
|
}
|
|
1924
|
+
function buildTroopBlock(troop) {
|
|
1925
|
+
if (!troop) return "";
|
|
1926
|
+
return `
|
|
1927
|
+
mkdir -p "$HOME_DIR/Library/LaunchAgents" "$HOME_DIR/Library/Logs" "$HOME_DIR/.openape/agent/tasks"
|
|
1928
|
+
cat > ${shQuote(troop.plistPath)} ${shHeredoc(troop.plistContent)}
|
|
1929
|
+
chmod 644 ${shQuote(troop.plistPath)}
|
|
1930
|
+
`;
|
|
1931
|
+
}
|
|
1932
|
+
function buildTroopBootstrapBlock(troop) {
|
|
1933
|
+
if (!troop) return "";
|
|
1934
|
+
return `
|
|
1935
|
+
# Bootstrap the troop sync launchd into the agent's GUI domain so it
|
|
1936
|
+
# starts firing every 5 minutes. RunAtLoad in the plist also kicks
|
|
1937
|
+
# off an immediate first sync so the agent registers + appears in
|
|
1938
|
+
# the troop SP within seconds of spawn finishing.
|
|
1939
|
+
echo "==> Installing troop sync launchd as $NAME\u2026"
|
|
1940
|
+
su - "$NAME" -c '
|
|
1941
|
+
set -euo pipefail
|
|
1942
|
+
NAME_UID="$(id -u)"
|
|
1943
|
+
launchctl bootout "gui/$NAME_UID/${troop.plistLabel}" 2>/dev/null || true
|
|
1944
|
+
launchctl bootstrap "gui/$NAME_UID" ${shQuote(troop.plistPath)} || \\
|
|
1945
|
+
echo "warn: troop sync bootstrap failed; run \\\`apes agents sync\\\` manually as $NAME to register at troop.openape.ai"
|
|
1946
|
+
'
|
|
1947
|
+
`;
|
|
1948
|
+
}
|
|
1924
1949
|
function buildDestroyTeardownScript(input) {
|
|
1925
1950
|
const { name, homeDir, adminUser } = input;
|
|
1926
1951
|
return `#!/bin/bash
|
|
@@ -2182,7 +2207,7 @@ function readPasswordSilent(prompt) {
|
|
|
2182
2207
|
"No TTY available for the silent password prompt. Set APES_ADMIN_PASSWORD in the environment instead."
|
|
2183
2208
|
));
|
|
2184
2209
|
}
|
|
2185
|
-
return new Promise((
|
|
2210
|
+
return new Promise((resolve5, reject) => {
|
|
2186
2211
|
process.stdout.write(prompt);
|
|
2187
2212
|
const wasRaw = process.stdin.isRaw ?? false;
|
|
2188
2213
|
process.stdin.setRawMode(true);
|
|
@@ -2197,7 +2222,7 @@ function readPasswordSilent(prompt) {
|
|
|
2197
2222
|
if (ch === "\r" || ch === "\n") {
|
|
2198
2223
|
cleanup();
|
|
2199
2224
|
process.stdout.write("\n");
|
|
2200
|
-
|
|
2225
|
+
resolve5(buf);
|
|
2201
2226
|
return;
|
|
2202
2227
|
}
|
|
2203
2228
|
if (code === 3) {
|
|
@@ -2492,22 +2517,814 @@ var registerAgentCommand = defineCommand23({
|
|
|
2492
2517
|
}
|
|
2493
2518
|
});
|
|
2494
2519
|
|
|
2495
|
-
// src/commands/agents/
|
|
2496
|
-
import {
|
|
2497
|
-
import {
|
|
2498
|
-
import {
|
|
2499
|
-
import { join as join4 } from "path";
|
|
2520
|
+
// src/commands/agents/run.ts
|
|
2521
|
+
import { existsSync as existsSync5, readFileSync as readFileSync5 } from "fs";
|
|
2522
|
+
import { homedir as homedir5 } from "os";
|
|
2523
|
+
import { join as join3 } from "path";
|
|
2500
2524
|
import { defineCommand as defineCommand24 } from "citty";
|
|
2501
2525
|
import consola22 from "consola";
|
|
2502
2526
|
|
|
2527
|
+
// src/lib/agent-tools/file.ts
|
|
2528
|
+
import { mkdirSync, readFileSync as readFileSync4, writeFileSync as writeFileSync2 } from "fs";
|
|
2529
|
+
import { homedir as homedir4 } from "os";
|
|
2530
|
+
import { dirname, normalize, resolve as resolve2 } from "path";
|
|
2531
|
+
var MAX_BYTES = 1024 * 1024;
|
|
2532
|
+
function jailPath(input) {
|
|
2533
|
+
if (typeof input !== "string" || input === "") {
|
|
2534
|
+
throw new Error("path must be a non-empty string");
|
|
2535
|
+
}
|
|
2536
|
+
const home = homedir4();
|
|
2537
|
+
const candidate = input.startsWith("~/") ? resolve2(home, input.slice(2)) : input.startsWith("/") ? normalize(input) : resolve2(home, input);
|
|
2538
|
+
if (candidate !== home && !candidate.startsWith(`${home}/`)) {
|
|
2539
|
+
throw new Error(`path "${input}" resolves outside the agent's home`);
|
|
2540
|
+
}
|
|
2541
|
+
return candidate;
|
|
2542
|
+
}
|
|
2543
|
+
var fileTools = [
|
|
2544
|
+
{
|
|
2545
|
+
name: "file.read",
|
|
2546
|
+
description: "Read a UTF-8 file from the agent's home directory ($HOME). Capped at 1MB. Path traversal blocked.",
|
|
2547
|
+
parameters: {
|
|
2548
|
+
type: "object",
|
|
2549
|
+
properties: {
|
|
2550
|
+
path: { type: "string", description: "Path relative to $HOME (or absolute under $HOME). `..` segments are rejected." }
|
|
2551
|
+
},
|
|
2552
|
+
required: ["path"]
|
|
2553
|
+
},
|
|
2554
|
+
execute: async (args) => {
|
|
2555
|
+
const a = args;
|
|
2556
|
+
const p = jailPath(a.path);
|
|
2557
|
+
const content = readFileSync4(p, "utf8");
|
|
2558
|
+
if (Buffer.byteLength(content, "utf8") > MAX_BYTES) {
|
|
2559
|
+
return { path: p, truncated: true, content: content.slice(0, MAX_BYTES) };
|
|
2560
|
+
}
|
|
2561
|
+
return { path: p, truncated: false, content };
|
|
2562
|
+
}
|
|
2563
|
+
},
|
|
2564
|
+
{
|
|
2565
|
+
name: "file.write",
|
|
2566
|
+
description: "Write a UTF-8 file under the agent's home directory. Creates parent dirs as needed. 1MB max.",
|
|
2567
|
+
parameters: {
|
|
2568
|
+
type: "object",
|
|
2569
|
+
properties: {
|
|
2570
|
+
path: { type: "string", description: "Path relative to $HOME (or absolute under $HOME)." },
|
|
2571
|
+
content: { type: "string", description: "File body. Existing files are overwritten." }
|
|
2572
|
+
},
|
|
2573
|
+
required: ["path", "content"]
|
|
2574
|
+
},
|
|
2575
|
+
execute: async (args) => {
|
|
2576
|
+
const a = args;
|
|
2577
|
+
if (typeof a.content !== "string") throw new Error("content must be a string");
|
|
2578
|
+
if (Buffer.byteLength(a.content, "utf8") > MAX_BYTES) {
|
|
2579
|
+
throw new Error(`content exceeds ${MAX_BYTES} byte cap`);
|
|
2580
|
+
}
|
|
2581
|
+
const p = jailPath(a.path);
|
|
2582
|
+
mkdirSync(dirname(p), { recursive: true });
|
|
2583
|
+
writeFileSync2(p, a.content, { encoding: "utf8" });
|
|
2584
|
+
return { path: p, bytes: Buffer.byteLength(a.content, "utf8") };
|
|
2585
|
+
}
|
|
2586
|
+
}
|
|
2587
|
+
];
|
|
2588
|
+
|
|
2589
|
+
// src/lib/agent-tools/http.ts
|
|
2590
|
+
var MAX_BYTES2 = 1024 * 1024;
|
|
2591
|
+
var FORBIDDEN_HEADERS = /* @__PURE__ */ new Set([
|
|
2592
|
+
"host",
|
|
2593
|
+
"authorization",
|
|
2594
|
+
"cookie",
|
|
2595
|
+
"connection",
|
|
2596
|
+
"transfer-encoding",
|
|
2597
|
+
"upgrade",
|
|
2598
|
+
"proxy-authorization"
|
|
2599
|
+
]);
|
|
2600
|
+
function sanitizeHeaders(input) {
|
|
2601
|
+
if (!input || typeof input !== "object") return {};
|
|
2602
|
+
const out = {};
|
|
2603
|
+
for (const [k, v] of Object.entries(input)) {
|
|
2604
|
+
if (typeof v !== "string") continue;
|
|
2605
|
+
if (FORBIDDEN_HEADERS.has(k.toLowerCase())) continue;
|
|
2606
|
+
out[k] = v;
|
|
2607
|
+
}
|
|
2608
|
+
return out;
|
|
2609
|
+
}
|
|
2610
|
+
async function readCappedBody(res) {
|
|
2611
|
+
const buf = new Uint8Array(MAX_BYTES2 + 1);
|
|
2612
|
+
let written = 0;
|
|
2613
|
+
const reader = res.body?.getReader();
|
|
2614
|
+
if (!reader) return await res.text();
|
|
2615
|
+
while (true) {
|
|
2616
|
+
const { value, done } = await reader.read();
|
|
2617
|
+
if (done) break;
|
|
2618
|
+
if (written + value.byteLength > MAX_BYTES2) {
|
|
2619
|
+
buf.set(value.subarray(0, MAX_BYTES2 - written), written);
|
|
2620
|
+
written = MAX_BYTES2;
|
|
2621
|
+
try {
|
|
2622
|
+
await reader.cancel();
|
|
2623
|
+
} catch {
|
|
2624
|
+
}
|
|
2625
|
+
break;
|
|
2626
|
+
}
|
|
2627
|
+
buf.set(value, written);
|
|
2628
|
+
written += value.byteLength;
|
|
2629
|
+
}
|
|
2630
|
+
return new TextDecoder().decode(buf.subarray(0, written));
|
|
2631
|
+
}
|
|
2632
|
+
var httpTools = [
|
|
2633
|
+
{
|
|
2634
|
+
name: "http.get",
|
|
2635
|
+
description: "GET an HTTPS URL and return the response body (capped at 1MB). Useful for reading public APIs, RSS feeds, web pages.",
|
|
2636
|
+
parameters: {
|
|
2637
|
+
type: "object",
|
|
2638
|
+
properties: {
|
|
2639
|
+
url: { type: "string", description: "Absolute HTTPS URL." },
|
|
2640
|
+
headers: { type: "object", description: "Optional headers (Host, Authorization, Cookie are stripped)." }
|
|
2641
|
+
},
|
|
2642
|
+
required: ["url"]
|
|
2643
|
+
},
|
|
2644
|
+
execute: async (args) => {
|
|
2645
|
+
const a = args;
|
|
2646
|
+
if (typeof a.url !== "string" || !a.url.startsWith("http")) {
|
|
2647
|
+
throw new Error("url must be an http(s) URL");
|
|
2648
|
+
}
|
|
2649
|
+
const res = await fetch(a.url, { method: "GET", headers: sanitizeHeaders(a.headers) });
|
|
2650
|
+
const body = await readCappedBody(res);
|
|
2651
|
+
return { status: res.status, headers: Object.fromEntries(res.headers), body };
|
|
2652
|
+
}
|
|
2653
|
+
},
|
|
2654
|
+
{
|
|
2655
|
+
name: "http.post",
|
|
2656
|
+
description: "POST JSON to an HTTPS URL and return the response body (capped at 1MB).",
|
|
2657
|
+
parameters: {
|
|
2658
|
+
type: "object",
|
|
2659
|
+
properties: {
|
|
2660
|
+
url: { type: "string", description: "Absolute HTTPS URL." },
|
|
2661
|
+
body: { description: "JSON-serialisable payload." },
|
|
2662
|
+
headers: { type: "object", description: "Optional headers (Host, Authorization, Cookie are stripped)." }
|
|
2663
|
+
},
|
|
2664
|
+
required: ["url", "body"]
|
|
2665
|
+
},
|
|
2666
|
+
execute: async (args) => {
|
|
2667
|
+
const a = args;
|
|
2668
|
+
if (typeof a.url !== "string" || !a.url.startsWith("http")) {
|
|
2669
|
+
throw new Error("url must be an http(s) URL");
|
|
2670
|
+
}
|
|
2671
|
+
const res = await fetch(a.url, {
|
|
2672
|
+
method: "POST",
|
|
2673
|
+
headers: { "content-type": "application/json", ...sanitizeHeaders(a.headers) },
|
|
2674
|
+
body: JSON.stringify(a.body)
|
|
2675
|
+
});
|
|
2676
|
+
const body = await readCappedBody(res);
|
|
2677
|
+
return { status: res.status, headers: Object.fromEntries(res.headers), body };
|
|
2678
|
+
}
|
|
2679
|
+
}
|
|
2680
|
+
];
|
|
2681
|
+
|
|
2682
|
+
// src/lib/agent-tools/mail.ts
|
|
2683
|
+
import { execFileSync as execFileSync5 } from "child_process";
|
|
2684
|
+
function o365(args) {
|
|
2685
|
+
try {
|
|
2686
|
+
return execFileSync5("o365-cli", args, { encoding: "utf8", stdio: ["ignore", "pipe", "pipe"] });
|
|
2687
|
+
} catch (err) {
|
|
2688
|
+
const e = err;
|
|
2689
|
+
if (e.code === "ENOENT") {
|
|
2690
|
+
throw new Error("o365-cli is not installed on this agent host");
|
|
2691
|
+
}
|
|
2692
|
+
const stderr = typeof e.stderr === "string" ? e.stderr : e.stderr?.toString("utf8");
|
|
2693
|
+
throw new Error(`o365-cli failed: ${stderr ?? e.message ?? err}`);
|
|
2694
|
+
}
|
|
2695
|
+
}
|
|
2696
|
+
var mailTools = [
|
|
2697
|
+
{
|
|
2698
|
+
name: "mail.list",
|
|
2699
|
+
description: "List recent inbox messages via o365-cli. Optional `unread_only` and `limit`.",
|
|
2700
|
+
parameters: {
|
|
2701
|
+
type: "object",
|
|
2702
|
+
properties: {
|
|
2703
|
+
limit: { type: "integer", minimum: 1, maximum: 100, default: 20 },
|
|
2704
|
+
unread_only: { type: "boolean", default: false }
|
|
2705
|
+
},
|
|
2706
|
+
required: []
|
|
2707
|
+
},
|
|
2708
|
+
execute: async (args) => {
|
|
2709
|
+
const a = args ?? {};
|
|
2710
|
+
const argv = ["mail", "list", "--json", "--limit", String(a.limit ?? 20)];
|
|
2711
|
+
if (a.unread_only) argv.push("--unread");
|
|
2712
|
+
const out = o365(argv);
|
|
2713
|
+
try {
|
|
2714
|
+
return JSON.parse(out);
|
|
2715
|
+
} catch {
|
|
2716
|
+
return { raw: out };
|
|
2717
|
+
}
|
|
2718
|
+
}
|
|
2719
|
+
},
|
|
2720
|
+
{
|
|
2721
|
+
name: "mail.search",
|
|
2722
|
+
description: "Search the inbox via o365-cli using a free-form query string.",
|
|
2723
|
+
parameters: {
|
|
2724
|
+
type: "object",
|
|
2725
|
+
properties: {
|
|
2726
|
+
q: { type: "string" },
|
|
2727
|
+
limit: { type: "integer", minimum: 1, maximum: 100, default: 20 }
|
|
2728
|
+
},
|
|
2729
|
+
required: ["q"]
|
|
2730
|
+
},
|
|
2731
|
+
execute: async (args) => {
|
|
2732
|
+
const a = args;
|
|
2733
|
+
if (typeof a.q !== "string" || a.q.length === 0) throw new Error("q is required");
|
|
2734
|
+
const argv = ["mail", "search", a.q, "--json", "--limit", String(a.limit ?? 20)];
|
|
2735
|
+
const out = o365(argv);
|
|
2736
|
+
try {
|
|
2737
|
+
return JSON.parse(out);
|
|
2738
|
+
} catch {
|
|
2739
|
+
return { raw: out };
|
|
2740
|
+
}
|
|
2741
|
+
}
|
|
2742
|
+
}
|
|
2743
|
+
];
|
|
2744
|
+
|
|
2745
|
+
// src/lib/agent-tools/tasks.ts
|
|
2746
|
+
import { execFileSync as execFileSync6 } from "child_process";
|
|
2747
|
+
function ape(args) {
|
|
2748
|
+
try {
|
|
2749
|
+
return execFileSync6("ape-tasks", args, { encoding: "utf8", stdio: ["ignore", "pipe", "pipe"] });
|
|
2750
|
+
} catch (err) {
|
|
2751
|
+
const e = err;
|
|
2752
|
+
const stderr = typeof e.stderr === "string" ? e.stderr : e.stderr?.toString("utf8");
|
|
2753
|
+
throw new Error(`ape-tasks failed: ${stderr ?? e.message ?? err}`);
|
|
2754
|
+
}
|
|
2755
|
+
}
|
|
2756
|
+
var tasksTools = [
|
|
2757
|
+
{
|
|
2758
|
+
name: "tasks.list",
|
|
2759
|
+
description: "List the owner's open ape-tasks (the user's personal task list at tasks.openape.ai).",
|
|
2760
|
+
parameters: {
|
|
2761
|
+
type: "object",
|
|
2762
|
+
properties: {
|
|
2763
|
+
status: { type: "string", enum: ["open", "doing", "done", "archived"] },
|
|
2764
|
+
team_id: { type: "string" }
|
|
2765
|
+
},
|
|
2766
|
+
required: []
|
|
2767
|
+
},
|
|
2768
|
+
execute: async (args) => {
|
|
2769
|
+
const a = args ?? {};
|
|
2770
|
+
const argv = ["list", "--json"];
|
|
2771
|
+
if (a.status) argv.push("--status", a.status);
|
|
2772
|
+
if (a.team_id) argv.push("--team", a.team_id);
|
|
2773
|
+
const out = ape(argv);
|
|
2774
|
+
try {
|
|
2775
|
+
return JSON.parse(out);
|
|
2776
|
+
} catch {
|
|
2777
|
+
return { raw: out };
|
|
2778
|
+
}
|
|
2779
|
+
}
|
|
2780
|
+
},
|
|
2781
|
+
{
|
|
2782
|
+
name: "tasks.create",
|
|
2783
|
+
description: "Create a new ape-task on the owner's task list at tasks.openape.ai.",
|
|
2784
|
+
parameters: {
|
|
2785
|
+
type: "object",
|
|
2786
|
+
properties: {
|
|
2787
|
+
title: { type: "string" },
|
|
2788
|
+
notes: { type: "string" },
|
|
2789
|
+
priority: { type: "string", enum: ["low", "med", "high"] },
|
|
2790
|
+
due_at: { type: "string", description: "ISO date or +Nh/+Nd shorthand." }
|
|
2791
|
+
},
|
|
2792
|
+
required: ["title"]
|
|
2793
|
+
},
|
|
2794
|
+
execute: async (args) => {
|
|
2795
|
+
const a = args;
|
|
2796
|
+
const argv = ["new", "--title", a.title, "--json"];
|
|
2797
|
+
if (a.notes) argv.push("--notes", a.notes);
|
|
2798
|
+
if (a.priority) argv.push("--priority", a.priority);
|
|
2799
|
+
if (a.due_at) argv.push("--due", a.due_at);
|
|
2800
|
+
const out = ape(argv);
|
|
2801
|
+
try {
|
|
2802
|
+
return JSON.parse(out);
|
|
2803
|
+
} catch {
|
|
2804
|
+
return { raw: out };
|
|
2805
|
+
}
|
|
2806
|
+
}
|
|
2807
|
+
}
|
|
2808
|
+
];
|
|
2809
|
+
|
|
2810
|
+
// src/lib/agent-tools/time.ts
|
|
2811
|
+
var timeTools = [
|
|
2812
|
+
{
|
|
2813
|
+
name: "time.now",
|
|
2814
|
+
description: "Returns the current UTC date and time as ISO 8601 plus epoch seconds. No inputs.",
|
|
2815
|
+
parameters: { type: "object", properties: {}, required: [] },
|
|
2816
|
+
execute: async () => {
|
|
2817
|
+
const now = /* @__PURE__ */ new Date();
|
|
2818
|
+
return {
|
|
2819
|
+
iso: now.toISOString(),
|
|
2820
|
+
epoch_seconds: Math.floor(now.getTime() / 1e3),
|
|
2821
|
+
timezone_offset_minutes: -now.getTimezoneOffset()
|
|
2822
|
+
};
|
|
2823
|
+
}
|
|
2824
|
+
}
|
|
2825
|
+
];
|
|
2826
|
+
|
|
2827
|
+
// src/lib/agent-tools/index.ts
|
|
2828
|
+
var ALL_TOOLS = [
|
|
2829
|
+
...timeTools,
|
|
2830
|
+
...httpTools,
|
|
2831
|
+
...fileTools,
|
|
2832
|
+
...tasksTools,
|
|
2833
|
+
...mailTools
|
|
2834
|
+
];
|
|
2835
|
+
var TOOLS = Object.fromEntries(
|
|
2836
|
+
ALL_TOOLS.map((t) => [t.name, t])
|
|
2837
|
+
);
|
|
2838
|
+
function taskTools(names) {
|
|
2839
|
+
const out = [];
|
|
2840
|
+
const missing = [];
|
|
2841
|
+
for (const name of names) {
|
|
2842
|
+
const tool = TOOLS[name];
|
|
2843
|
+
if (!tool) missing.push(name);
|
|
2844
|
+
else out.push(tool);
|
|
2845
|
+
}
|
|
2846
|
+
if (missing.length > 0) {
|
|
2847
|
+
throw new Error(`unknown tool(s): ${missing.join(", ")}`);
|
|
2848
|
+
}
|
|
2849
|
+
return out;
|
|
2850
|
+
}
|
|
2851
|
+
function asOpenAiTools(tools) {
|
|
2852
|
+
return tools.map((t) => ({
|
|
2853
|
+
type: "function",
|
|
2854
|
+
function: { name: t.name, description: t.description, parameters: t.parameters }
|
|
2855
|
+
}));
|
|
2856
|
+
}
|
|
2857
|
+
|
|
2858
|
+
// src/lib/agent-runtime.ts
|
|
2859
|
+
function previewJson(value, max = 500) {
|
|
2860
|
+
let s;
|
|
2861
|
+
try {
|
|
2862
|
+
s = JSON.stringify(value);
|
|
2863
|
+
} catch {
|
|
2864
|
+
s = String(value);
|
|
2865
|
+
}
|
|
2866
|
+
return s.length > max ? `${s.slice(0, max)}\u2026` : s;
|
|
2867
|
+
}
|
|
2868
|
+
async function runLoop(opts) {
|
|
2869
|
+
const fetchFn = opts.fetchImpl ?? fetch;
|
|
2870
|
+
const trace = [];
|
|
2871
|
+
const messages = [
|
|
2872
|
+
{ role: "system", content: opts.systemPrompt },
|
|
2873
|
+
...opts.history ?? [],
|
|
2874
|
+
{ role: "user", content: opts.userMessage }
|
|
2875
|
+
];
|
|
2876
|
+
const tools = asOpenAiTools(opts.tools);
|
|
2877
|
+
for (let step = 1; step <= opts.maxSteps; step++) {
|
|
2878
|
+
const res = await fetchFn(`${opts.config.apiBase}/chat/completions`, {
|
|
2879
|
+
method: "POST",
|
|
2880
|
+
headers: {
|
|
2881
|
+
"authorization": `Bearer ${opts.config.apiKey}`,
|
|
2882
|
+
"content-type": "application/json"
|
|
2883
|
+
},
|
|
2884
|
+
body: JSON.stringify({
|
|
2885
|
+
model: opts.config.model,
|
|
2886
|
+
messages,
|
|
2887
|
+
...tools.length > 0 ? { tools, tool_choice: "auto" } : {}
|
|
2888
|
+
})
|
|
2889
|
+
});
|
|
2890
|
+
if (!res.ok) {
|
|
2891
|
+
const text = await res.text().catch(() => "");
|
|
2892
|
+
throw new Error(`LiteLLM ${res.status}: ${text.slice(0, 500)}`);
|
|
2893
|
+
}
|
|
2894
|
+
const data = await res.json();
|
|
2895
|
+
const choice = data.choices?.[0];
|
|
2896
|
+
if (!choice) throw new Error("LiteLLM response had no choices");
|
|
2897
|
+
const assistant = choice.message;
|
|
2898
|
+
messages.push(assistant);
|
|
2899
|
+
if (assistant.content) opts.handlers?.onTextDelta?.(assistant.content);
|
|
2900
|
+
trace.push({
|
|
2901
|
+
step,
|
|
2902
|
+
type: "assistant",
|
|
2903
|
+
preview: previewJson({ content: assistant.content, tool_calls: assistant.tool_calls?.length ?? 0 })
|
|
2904
|
+
});
|
|
2905
|
+
if (!assistant.tool_calls || assistant.tool_calls.length === 0) {
|
|
2906
|
+
const result2 = {
|
|
2907
|
+
status: "ok",
|
|
2908
|
+
finalMessage: assistant.content,
|
|
2909
|
+
stepCount: step,
|
|
2910
|
+
trace
|
|
2911
|
+
};
|
|
2912
|
+
opts.handlers?.onDone?.(result2);
|
|
2913
|
+
return result2;
|
|
2914
|
+
}
|
|
2915
|
+
for (const call of assistant.tool_calls) {
|
|
2916
|
+
const tool = opts.tools.find((t) => t.name === call.function.name);
|
|
2917
|
+
let parsedArgs;
|
|
2918
|
+
try {
|
|
2919
|
+
parsedArgs = JSON.parse(call.function.arguments);
|
|
2920
|
+
} catch {
|
|
2921
|
+
parsedArgs = {};
|
|
2922
|
+
}
|
|
2923
|
+
opts.handlers?.onToolCall?.({ name: call.function.name, args: parsedArgs });
|
|
2924
|
+
trace.push({ step, type: "tool_call", tool: call.function.name, preview: previewJson(parsedArgs) });
|
|
2925
|
+
let result2;
|
|
2926
|
+
let isError = false;
|
|
2927
|
+
if (!tool) {
|
|
2928
|
+
result2 = `unknown tool: ${call.function.name}`;
|
|
2929
|
+
isError = true;
|
|
2930
|
+
} else {
|
|
2931
|
+
try {
|
|
2932
|
+
result2 = await tool.execute(parsedArgs);
|
|
2933
|
+
} catch (err) {
|
|
2934
|
+
result2 = err?.message ?? String(err);
|
|
2935
|
+
isError = true;
|
|
2936
|
+
}
|
|
2937
|
+
}
|
|
2938
|
+
if (isError) {
|
|
2939
|
+
opts.handlers?.onToolError?.({ name: call.function.name, error: String(result2) });
|
|
2940
|
+
trace.push({ step, type: "tool_error", tool: call.function.name, preview: previewJson(result2) });
|
|
2941
|
+
} else {
|
|
2942
|
+
opts.handlers?.onToolResult?.({ name: call.function.name, result: result2 });
|
|
2943
|
+
trace.push({ step, type: "tool_result", tool: call.function.name, preview: previewJson(result2) });
|
|
2944
|
+
}
|
|
2945
|
+
messages.push({
|
|
2946
|
+
role: "tool",
|
|
2947
|
+
tool_call_id: call.id,
|
|
2948
|
+
name: call.function.name,
|
|
2949
|
+
content: typeof result2 === "string" ? result2 : JSON.stringify(result2)
|
|
2950
|
+
});
|
|
2951
|
+
}
|
|
2952
|
+
}
|
|
2953
|
+
const result = {
|
|
2954
|
+
status: "error",
|
|
2955
|
+
finalMessage: `max_steps (${opts.maxSteps}) reached without completion`,
|
|
2956
|
+
stepCount: opts.maxSteps,
|
|
2957
|
+
trace
|
|
2958
|
+
};
|
|
2959
|
+
opts.handlers?.onDone?.(result);
|
|
2960
|
+
return result;
|
|
2961
|
+
}
|
|
2962
|
+
var RPC_SESSION_TTL_MS = 60 * 60 * 1e3;
|
|
2963
|
+
var RpcSessionMap = class {
|
|
2964
|
+
sessions = /* @__PURE__ */ new Map();
|
|
2965
|
+
get(id) {
|
|
2966
|
+
const s = this.sessions.get(id);
|
|
2967
|
+
if (s) s.lastTouched = Date.now();
|
|
2968
|
+
return s;
|
|
2969
|
+
}
|
|
2970
|
+
put(id, s) {
|
|
2971
|
+
s.lastTouched = Date.now();
|
|
2972
|
+
this.sessions.set(id, s);
|
|
2973
|
+
}
|
|
2974
|
+
evictStale() {
|
|
2975
|
+
const cutoff = Date.now() - RPC_SESSION_TTL_MS;
|
|
2976
|
+
for (const [k, v] of this.sessions) {
|
|
2977
|
+
if (v.lastTouched < cutoff) this.sessions.delete(k);
|
|
2978
|
+
}
|
|
2979
|
+
}
|
|
2980
|
+
size() {
|
|
2981
|
+
return this.sessions.size;
|
|
2982
|
+
}
|
|
2983
|
+
};
|
|
2984
|
+
|
|
2985
|
+
// src/lib/troop-client.ts
|
|
2986
|
+
var DEFAULT_TROOP_URL = "https://troop.openape.ai";
|
|
2987
|
+
var TroopClient = class {
|
|
2988
|
+
constructor(troopUrl, agentJwt) {
|
|
2989
|
+
this.troopUrl = troopUrl;
|
|
2990
|
+
this.agentJwt = agentJwt;
|
|
2991
|
+
}
|
|
2992
|
+
async request(path2, init) {
|
|
2993
|
+
const res = await fetch(`${this.troopUrl}${path2}`, {
|
|
2994
|
+
...init,
|
|
2995
|
+
headers: {
|
|
2996
|
+
...init?.headers ?? {},
|
|
2997
|
+
"Authorization": `Bearer ${this.agentJwt}`,
|
|
2998
|
+
"Content-Type": "application/json"
|
|
2999
|
+
}
|
|
3000
|
+
});
|
|
3001
|
+
if (!res.ok) {
|
|
3002
|
+
const text = await res.text().catch(() => "");
|
|
3003
|
+
throw new Error(`troop ${init?.method ?? "GET"} ${path2} failed: ${res.status} ${text}`);
|
|
3004
|
+
}
|
|
3005
|
+
if (res.status === 204) return void 0;
|
|
3006
|
+
return await res.json();
|
|
3007
|
+
}
|
|
3008
|
+
sync(input) {
|
|
3009
|
+
return this.request("/api/agents/me/sync", {
|
|
3010
|
+
method: "POST",
|
|
3011
|
+
body: JSON.stringify({
|
|
3012
|
+
hostname: input.hostname,
|
|
3013
|
+
host_id: input.hostId,
|
|
3014
|
+
owner_email: input.ownerEmail,
|
|
3015
|
+
...input.pubkeySsh ? { pubkey_ssh: input.pubkeySsh } : {}
|
|
3016
|
+
})
|
|
3017
|
+
});
|
|
3018
|
+
}
|
|
3019
|
+
listTasks() {
|
|
3020
|
+
return this.request("/api/agents/me/tasks");
|
|
3021
|
+
}
|
|
3022
|
+
startRun(taskId) {
|
|
3023
|
+
return this.request("/api/agents/me/runs", {
|
|
3024
|
+
method: "POST",
|
|
3025
|
+
body: JSON.stringify({ task_id: taskId })
|
|
3026
|
+
});
|
|
3027
|
+
}
|
|
3028
|
+
finaliseRun(id, payload) {
|
|
3029
|
+
return this.request(`/api/agents/me/runs/${id}`, {
|
|
3030
|
+
method: "PATCH",
|
|
3031
|
+
body: JSON.stringify(payload)
|
|
3032
|
+
});
|
|
3033
|
+
}
|
|
3034
|
+
};
|
|
3035
|
+
function resolveTroopUrl(override) {
|
|
3036
|
+
if (override) return override.replace(/\/$/, "");
|
|
3037
|
+
const fromEnv = process.env.OPENAPE_TROOP_URL;
|
|
3038
|
+
if (fromEnv) return fromEnv.replace(/\/$/, "");
|
|
3039
|
+
return DEFAULT_TROOP_URL;
|
|
3040
|
+
}
|
|
3041
|
+
|
|
3042
|
+
// src/commands/agents/run.ts
|
|
3043
|
+
var AUTH_PATH = join3(homedir5(), ".config", "apes", "auth.json");
|
|
3044
|
+
var TASK_CACHE_DIR = join3(homedir5(), ".openape", "agent", "tasks");
|
|
3045
|
+
function readAuth() {
|
|
3046
|
+
if (!existsSync5(AUTH_PATH)) {
|
|
3047
|
+
throw new CliError(`No agent auth found at ${AUTH_PATH}. Run \`apes agents spawn <name>\` first.`);
|
|
3048
|
+
}
|
|
3049
|
+
const parsed = JSON.parse(readFileSync5(AUTH_PATH, "utf8"));
|
|
3050
|
+
if (!parsed.access_token) throw new CliError("auth.json missing access_token");
|
|
3051
|
+
return parsed;
|
|
3052
|
+
}
|
|
3053
|
+
function readTaskSpec(taskId) {
|
|
3054
|
+
const path2 = join3(TASK_CACHE_DIR, `${taskId}.json`);
|
|
3055
|
+
if (!existsSync5(path2)) {
|
|
3056
|
+
throw new CliError(`No cached task spec at ${path2}. Run \`apes agents sync\` first to pull the task list from troop.`);
|
|
3057
|
+
}
|
|
3058
|
+
return JSON.parse(readFileSync5(path2, "utf8"));
|
|
3059
|
+
}
|
|
3060
|
+
function readLitellmConfig(model) {
|
|
3061
|
+
const envPath = join3(homedir5(), "litellm", ".env");
|
|
3062
|
+
const env = {};
|
|
3063
|
+
if (existsSync5(envPath)) {
|
|
3064
|
+
for (const line of readFileSync5(envPath, "utf8").split(/\r?\n/)) {
|
|
3065
|
+
const m = line.match(/^([A-Z_]+)=(.*)$/);
|
|
3066
|
+
if (m) env[m[1]] = m[2].replace(/^["']|["']$/g, "");
|
|
3067
|
+
}
|
|
3068
|
+
}
|
|
3069
|
+
for (const k of ["LITELLM_API_KEY", "LITELLM_MASTER_KEY", "LITELLM_BASE_URL"]) {
|
|
3070
|
+
if (process.env[k]) env[k] = process.env[k];
|
|
3071
|
+
}
|
|
3072
|
+
const apiKey = env.LITELLM_API_KEY || env.LITELLM_MASTER_KEY;
|
|
3073
|
+
const apiBase = (env.LITELLM_BASE_URL || "http://127.0.0.1:4000/v1").replace(/\/$/, "");
|
|
3074
|
+
if (!apiKey) {
|
|
3075
|
+
throw new CliError("No LITELLM_API_KEY / LITELLM_MASTER_KEY in ~/litellm/.env or env.");
|
|
3076
|
+
}
|
|
3077
|
+
return { apiBase, apiKey, model: model || "claude-haiku-4-5" };
|
|
3078
|
+
}
|
|
3079
|
+
var runAgentCommand = defineCommand24({
|
|
3080
|
+
meta: {
|
|
3081
|
+
name: "run",
|
|
3082
|
+
description: "Execute one task (typically launchd-invoked). Reports the run record to troop."
|
|
3083
|
+
},
|
|
3084
|
+
args: {
|
|
3085
|
+
"task-id": {
|
|
3086
|
+
type: "positional",
|
|
3087
|
+
description: "Task ID (slug) to run. The cached spec at ~/.openape/agent/tasks/<id>.json is used.",
|
|
3088
|
+
required: true
|
|
3089
|
+
},
|
|
3090
|
+
"troop-url": {
|
|
3091
|
+
type: "string",
|
|
3092
|
+
description: "Override troop SP base URL."
|
|
3093
|
+
},
|
|
3094
|
+
"model": {
|
|
3095
|
+
type: "string",
|
|
3096
|
+
description: "Override the LLM model name. Default: claude-haiku-4-5."
|
|
3097
|
+
}
|
|
3098
|
+
},
|
|
3099
|
+
async run({ args }) {
|
|
3100
|
+
const taskId = args["task-id"];
|
|
3101
|
+
const auth = readAuth();
|
|
3102
|
+
const spec = readTaskSpec(taskId);
|
|
3103
|
+
const config = readLitellmConfig(args.model);
|
|
3104
|
+
let tools;
|
|
3105
|
+
try {
|
|
3106
|
+
tools = taskTools(spec.tools);
|
|
3107
|
+
} catch (err) {
|
|
3108
|
+
throw new CliError(`task ${taskId}: ${err.message}`);
|
|
3109
|
+
}
|
|
3110
|
+
const troop = new TroopClient(resolveTroopUrl(args["troop-url"]), auth.access_token);
|
|
3111
|
+
const { id: runId } = await troop.startRun(taskId);
|
|
3112
|
+
consola22.info(`Run ${runId} started for task ${taskId}`);
|
|
3113
|
+
try {
|
|
3114
|
+
const result = await runLoop({
|
|
3115
|
+
config,
|
|
3116
|
+
systemPrompt: spec.systemPrompt,
|
|
3117
|
+
// Cron tasks have no prior user message — the task fires by
|
|
3118
|
+
// schedule. We use a synthetic kick-off message; tasks can
|
|
3119
|
+
// ignore it via their system prompt.
|
|
3120
|
+
userMessage: "It is time to run this task. Use your tools as needed and report when done.",
|
|
3121
|
+
tools,
|
|
3122
|
+
maxSteps: spec.maxSteps
|
|
3123
|
+
});
|
|
3124
|
+
await troop.finaliseRun(runId, {
|
|
3125
|
+
status: result.status,
|
|
3126
|
+
final_message: result.finalMessage,
|
|
3127
|
+
step_count: result.stepCount,
|
|
3128
|
+
trace: result.trace
|
|
3129
|
+
});
|
|
3130
|
+
consola22.success(`Run ${runId} ${result.status} (${result.stepCount} steps)`);
|
|
3131
|
+
if (result.status === "error") process.exit(1);
|
|
3132
|
+
} catch (err) {
|
|
3133
|
+
const message = err?.message ?? String(err);
|
|
3134
|
+
await troop.finaliseRun(runId, {
|
|
3135
|
+
status: "error",
|
|
3136
|
+
final_message: message.slice(0, 4e3),
|
|
3137
|
+
step_count: 0,
|
|
3138
|
+
trace: []
|
|
3139
|
+
}).catch(() => {
|
|
3140
|
+
});
|
|
3141
|
+
throw new CliError(`Run ${runId} crashed: ${message}`);
|
|
3142
|
+
}
|
|
3143
|
+
}
|
|
3144
|
+
});
|
|
3145
|
+
|
|
3146
|
+
// src/commands/agents/serve.ts
|
|
3147
|
+
import { existsSync as existsSync6, readFileSync as readFileSync6 } from "fs";
|
|
3148
|
+
import { homedir as homedir6 } from "os";
|
|
3149
|
+
import { join as join4 } from "path";
|
|
3150
|
+
import { createInterface } from "readline";
|
|
3151
|
+
import { defineCommand as defineCommand25 } from "citty";
|
|
3152
|
+
var AUTH_PATH2 = join4(homedir6(), ".config", "apes", "auth.json");
|
|
3153
|
+
function readLitellmConfig2(model) {
|
|
3154
|
+
const envPath = join4(homedir6(), "litellm", ".env");
|
|
3155
|
+
const env = {};
|
|
3156
|
+
if (existsSync6(envPath)) {
|
|
3157
|
+
for (const line of readFileSync6(envPath, "utf8").split(/\r?\n/)) {
|
|
3158
|
+
const m = line.match(/^([A-Z_]+)=(.*)$/);
|
|
3159
|
+
if (m) env[m[1]] = m[2].replace(/^["']|["']$/g, "");
|
|
3160
|
+
}
|
|
3161
|
+
}
|
|
3162
|
+
for (const k of ["LITELLM_API_KEY", "LITELLM_MASTER_KEY", "LITELLM_BASE_URL"]) {
|
|
3163
|
+
if (process.env[k]) env[k] = process.env[k];
|
|
3164
|
+
}
|
|
3165
|
+
const apiKey = env.LITELLM_API_KEY || env.LITELLM_MASTER_KEY;
|
|
3166
|
+
const apiBase = (env.LITELLM_BASE_URL || "http://127.0.0.1:4000/v1").replace(/\/$/, "");
|
|
3167
|
+
if (!apiKey) throw new CliError("No LITELLM_API_KEY / LITELLM_MASTER_KEY in ~/litellm/.env or env.");
|
|
3168
|
+
return { apiBase, apiKey, model: model || "claude-haiku-4-5" };
|
|
3169
|
+
}
|
|
3170
|
+
function emit(event) {
|
|
3171
|
+
process.stdout.write(`${JSON.stringify(event)}
|
|
3172
|
+
`);
|
|
3173
|
+
}
|
|
3174
|
+
var serveAgentCommand = defineCommand25({
|
|
3175
|
+
meta: {
|
|
3176
|
+
name: "serve",
|
|
3177
|
+
description: "Long-running stdio RPC server for chat-bridge subprocess use."
|
|
3178
|
+
},
|
|
3179
|
+
args: {
|
|
3180
|
+
rpc: {
|
|
3181
|
+
type: "boolean",
|
|
3182
|
+
description: "Use the line-delimited JSON RPC protocol on stdio (the only mode for now)."
|
|
3183
|
+
}
|
|
3184
|
+
},
|
|
3185
|
+
async run({ args }) {
|
|
3186
|
+
if (!args.rpc) {
|
|
3187
|
+
throw new CliError("apes agents serve currently only supports --rpc mode");
|
|
3188
|
+
}
|
|
3189
|
+
if (existsSync6(AUTH_PATH2)) {
|
|
3190
|
+
try {
|
|
3191
|
+
JSON.parse(readFileSync6(AUTH_PATH2, "utf8"));
|
|
3192
|
+
} catch {
|
|
3193
|
+
}
|
|
3194
|
+
}
|
|
3195
|
+
const sessions = new RpcSessionMap();
|
|
3196
|
+
const evictTimer = setInterval(() => sessions.evictStale(), 5 * 60 * 1e3);
|
|
3197
|
+
process.on("exit", () => clearInterval(evictTimer));
|
|
3198
|
+
const rl = createInterface({ input: process.stdin, terminal: false });
|
|
3199
|
+
rl.on("line", async (line) => {
|
|
3200
|
+
const trimmed = line.trim();
|
|
3201
|
+
if (!trimmed) return;
|
|
3202
|
+
let msg;
|
|
3203
|
+
try {
|
|
3204
|
+
msg = JSON.parse(trimmed);
|
|
3205
|
+
} catch (err) {
|
|
3206
|
+
emit({ type: "error", message: `invalid JSON: ${err.message}` });
|
|
3207
|
+
return;
|
|
3208
|
+
}
|
|
3209
|
+
if (!msg.session_id || !msg.user_msg) {
|
|
3210
|
+
emit({ type: "error", message: "session_id and user_msg are required" });
|
|
3211
|
+
return;
|
|
3212
|
+
}
|
|
3213
|
+
try {
|
|
3214
|
+
await handleInbound(msg, sessions);
|
|
3215
|
+
} catch (err) {
|
|
3216
|
+
emit({ type: "error", session_id: msg.session_id, message: err?.message ?? String(err) });
|
|
3217
|
+
emit({ type: "done", session_id: msg.session_id, step_count: 0, status: "error" });
|
|
3218
|
+
}
|
|
3219
|
+
});
|
|
3220
|
+
rl.on("close", () => process.exit(0));
|
|
3221
|
+
}
|
|
3222
|
+
});
|
|
3223
|
+
async function handleInbound(msg, sessions) {
|
|
3224
|
+
const config = readLitellmConfig2(msg.model);
|
|
3225
|
+
const tools = taskTools(msg.tools ?? []);
|
|
3226
|
+
const maxSteps = msg.max_steps ?? 10;
|
|
3227
|
+
let session = sessions.get(msg.session_id);
|
|
3228
|
+
if (!session) {
|
|
3229
|
+
session = {
|
|
3230
|
+
systemPrompt: msg.system_prompt,
|
|
3231
|
+
tools,
|
|
3232
|
+
maxSteps,
|
|
3233
|
+
messages: [],
|
|
3234
|
+
lastTouched: Date.now()
|
|
3235
|
+
};
|
|
3236
|
+
sessions.put(msg.session_id, session);
|
|
3237
|
+
}
|
|
3238
|
+
const result = await runLoop({
|
|
3239
|
+
config,
|
|
3240
|
+
systemPrompt: session.systemPrompt,
|
|
3241
|
+
userMessage: msg.user_msg,
|
|
3242
|
+
tools: session.tools,
|
|
3243
|
+
maxSteps: session.maxSteps,
|
|
3244
|
+
history: session.messages,
|
|
3245
|
+
handlers: {
|
|
3246
|
+
onTextDelta: (delta) => emit({ type: "text_delta", session_id: msg.session_id, delta }),
|
|
3247
|
+
onToolCall: ({ name, args }) => emit({ type: "tool_call", session_id: msg.session_id, name, args }),
|
|
3248
|
+
onToolResult: ({ name, result: result2 }) => emit({ type: "tool_result", session_id: msg.session_id, name, result: result2 }),
|
|
3249
|
+
onToolError: ({ name, error }) => emit({ type: "tool_error", session_id: msg.session_id, name, error })
|
|
3250
|
+
}
|
|
3251
|
+
});
|
|
3252
|
+
session.messages.push({ role: "user", content: msg.user_msg });
|
|
3253
|
+
if (result.finalMessage) {
|
|
3254
|
+
session.messages.push({ role: "assistant", content: result.finalMessage });
|
|
3255
|
+
}
|
|
3256
|
+
emit({
|
|
3257
|
+
type: "done",
|
|
3258
|
+
session_id: msg.session_id,
|
|
3259
|
+
step_count: result.stepCount,
|
|
3260
|
+
status: result.status,
|
|
3261
|
+
final_message: result.finalMessage
|
|
3262
|
+
});
|
|
3263
|
+
}
|
|
3264
|
+
|
|
3265
|
+
// src/commands/agents/spawn.ts
|
|
3266
|
+
import { execFileSync as execFileSync7 } from "child_process";
|
|
3267
|
+
import { mkdtempSync as mkdtempSync2, rmSync as rmSync2, writeFileSync as writeFileSync4 } from "fs";
|
|
3268
|
+
import { tmpdir as tmpdir2 } from "os";
|
|
3269
|
+
import { join as join6 } from "path";
|
|
3270
|
+
import { defineCommand as defineCommand26 } from "citty";
|
|
3271
|
+
import consola23 from "consola";
|
|
3272
|
+
|
|
3273
|
+
// src/lib/troop-bootstrap.ts
|
|
3274
|
+
var SYNC_LABEL_PREFIX = "openape.troop.sync";
|
|
3275
|
+
var SYNC_INTERVAL_SECONDS = 300;
|
|
3276
|
+
function syncPlistLabel(agentName) {
|
|
3277
|
+
return `${SYNC_LABEL_PREFIX}.${agentName}`;
|
|
3278
|
+
}
|
|
3279
|
+
function escape(s) {
|
|
3280
|
+
return s.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">");
|
|
3281
|
+
}
|
|
3282
|
+
function buildSyncPlist(input) {
|
|
3283
|
+
const envBlock = input.troopUrl ? ` <key>EnvironmentVariables</key>
|
|
3284
|
+
<dict>
|
|
3285
|
+
<key>HOME</key><string>${escape(input.homeDir)}</string>
|
|
3286
|
+
<key>OPENAPE_TROOP_URL</key><string>${escape(input.troopUrl)}</string>
|
|
3287
|
+
</dict>
|
|
3288
|
+
` : ` <key>EnvironmentVariables</key>
|
|
3289
|
+
<dict>
|
|
3290
|
+
<key>HOME</key><string>${escape(input.homeDir)}</string>
|
|
3291
|
+
</dict>
|
|
3292
|
+
`;
|
|
3293
|
+
return `<?xml version="1.0" encoding="UTF-8"?>
|
|
3294
|
+
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
3295
|
+
<plist version="1.0">
|
|
3296
|
+
<dict>
|
|
3297
|
+
<key>Label</key>
|
|
3298
|
+
<string>${escape(syncPlistLabel(input.agentName))}</string>
|
|
3299
|
+
<key>ProgramArguments</key>
|
|
3300
|
+
<array>
|
|
3301
|
+
<string>${escape(input.apesBin)}</string>
|
|
3302
|
+
<string>agents</string>
|
|
3303
|
+
<string>sync</string>
|
|
3304
|
+
</array>
|
|
3305
|
+
<key>WorkingDirectory</key>
|
|
3306
|
+
<string>${escape(input.homeDir)}</string>
|
|
3307
|
+
${envBlock} <key>StartInterval</key>
|
|
3308
|
+
<integer>${SYNC_INTERVAL_SECONDS}</integer>
|
|
3309
|
+
<key>RunAtLoad</key>
|
|
3310
|
+
<true/>
|
|
3311
|
+
<key>StandardOutPath</key>
|
|
3312
|
+
<string>${escape(input.homeDir)}/Library/Logs/openape-troop-sync.log</string>
|
|
3313
|
+
<key>StandardErrorPath</key>
|
|
3314
|
+
<string>${escape(input.homeDir)}/Library/Logs/openape-troop-sync.log</string>
|
|
3315
|
+
</dict>
|
|
3316
|
+
</plist>
|
|
3317
|
+
`;
|
|
3318
|
+
}
|
|
3319
|
+
|
|
2503
3320
|
// src/lib/keygen.ts
|
|
2504
3321
|
import { Buffer as Buffer4 } from "buffer";
|
|
2505
|
-
import { existsSync as
|
|
3322
|
+
import { existsSync as existsSync7, mkdirSync as mkdirSync2, readFileSync as readFileSync7, writeFileSync as writeFileSync3 } from "fs";
|
|
2506
3323
|
import { generateKeyPairSync } from "crypto";
|
|
2507
|
-
import { homedir as
|
|
2508
|
-
import { dirname, resolve as
|
|
3324
|
+
import { homedir as homedir7 } from "os";
|
|
3325
|
+
import { dirname as dirname2, resolve as resolve3 } from "path";
|
|
2509
3326
|
function resolveKeyPath(p) {
|
|
2510
|
-
return
|
|
3327
|
+
return resolve3(p.replace(/^~/, homedir7()));
|
|
2511
3328
|
}
|
|
2512
3329
|
function buildSshEd25519Line(rawPub) {
|
|
2513
3330
|
const keyTypeStr = "ssh-ed25519";
|
|
@@ -2520,10 +3337,10 @@ function buildSshEd25519Line(rawPub) {
|
|
|
2520
3337
|
}
|
|
2521
3338
|
function readPublicKey(keyPath) {
|
|
2522
3339
|
const pubPath = `${keyPath}.pub`;
|
|
2523
|
-
if (
|
|
2524
|
-
return
|
|
3340
|
+
if (existsSync7(pubPath)) {
|
|
3341
|
+
return readFileSync7(pubPath, "utf-8").trim();
|
|
2525
3342
|
}
|
|
2526
|
-
const keyContent =
|
|
3343
|
+
const keyContent = readFileSync7(keyPath, "utf-8");
|
|
2527
3344
|
const privateKey = loadEd25519PrivateKey(keyContent);
|
|
2528
3345
|
const jwk = privateKey.export({ format: "jwk" });
|
|
2529
3346
|
const pubBytes = Buffer4.from(jwk.x, "base64url");
|
|
@@ -2531,17 +3348,17 @@ function readPublicKey(keyPath) {
|
|
|
2531
3348
|
}
|
|
2532
3349
|
function generateAndSaveKey(keyPath) {
|
|
2533
3350
|
const resolved = resolveKeyPath(keyPath);
|
|
2534
|
-
const dir =
|
|
2535
|
-
if (!
|
|
2536
|
-
|
|
3351
|
+
const dir = dirname2(resolved);
|
|
3352
|
+
if (!existsSync7(dir)) {
|
|
3353
|
+
mkdirSync2(dir, { recursive: true });
|
|
2537
3354
|
}
|
|
2538
3355
|
const { publicKey, privateKey } = generateKeyPairSync("ed25519");
|
|
2539
3356
|
const privatePem = privateKey.export({ type: "pkcs8", format: "pem" });
|
|
2540
|
-
|
|
3357
|
+
writeFileSync3(resolved, privatePem, { mode: 384 });
|
|
2541
3358
|
const jwk = publicKey.export({ format: "jwk" });
|
|
2542
3359
|
const pubBytes = Buffer4.from(jwk.x, "base64url");
|
|
2543
3360
|
const pubKeyStr = buildSshEd25519Line(pubBytes);
|
|
2544
|
-
|
|
3361
|
+
writeFileSync3(`${resolved}.pub`, `${pubKeyStr}
|
|
2545
3362
|
`, { mode: 420 });
|
|
2546
3363
|
return pubKeyStr;
|
|
2547
3364
|
}
|
|
@@ -2557,14 +3374,14 @@ function generateKeyPairInMemory() {
|
|
|
2557
3374
|
}
|
|
2558
3375
|
|
|
2559
3376
|
// src/lib/llm-bridge.ts
|
|
2560
|
-
import { existsSync as
|
|
2561
|
-
import { homedir as
|
|
2562
|
-
import { join as
|
|
3377
|
+
import { existsSync as existsSync8, readFileSync as readFileSync8 } from "fs";
|
|
3378
|
+
import { homedir as homedir8 } from "os";
|
|
3379
|
+
import { join as join5 } from "path";
|
|
2563
3380
|
var PLIST_LABEL_PREFIX = "eco.hofmann.apes.bridge";
|
|
2564
|
-
function readLitellmEnv(envPath =
|
|
2565
|
-
if (!
|
|
3381
|
+
function readLitellmEnv(envPath = join5(homedir8(), "litellm", ".env")) {
|
|
3382
|
+
if (!existsSync8(envPath)) return null;
|
|
2566
3383
|
try {
|
|
2567
|
-
const text =
|
|
3384
|
+
const text = readFileSync8(envPath, "utf8");
|
|
2568
3385
|
const out = {};
|
|
2569
3386
|
for (const line of text.split("\n")) {
|
|
2570
3387
|
const trimmed = line.trim();
|
|
@@ -2618,46 +3435,13 @@ export PATH="$HOME/.bun/bin:/opt/homebrew/bin:/usr/local/bin:/usr/bin:/bin"
|
|
|
2618
3435
|
# at boot \u2014 keeping start.sh slim avoids the rate-limit dance the old
|
|
2619
3436
|
# refresh hit when KeepAlive crash-restarted the daemon every 1h.
|
|
2620
3437
|
|
|
2621
|
-
|
|
2622
|
-
|
|
2623
|
-
|
|
2624
|
-
|
|
2625
|
-
import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
|
|
2626
|
-
|
|
2627
|
-
const BASE_URL = process.env.LITELLM_BASE_URL ?? "http://127.0.0.1:4000/v1";
|
|
2628
|
-
|
|
2629
|
-
export default async function (pi: ExtensionAPI) {
|
|
2630
|
-
pi.registerProvider("litellm", {
|
|
2631
|
-
baseUrl: BASE_URL,
|
|
2632
|
-
apiKey: "LITELLM_API_KEY",
|
|
2633
|
-
api: "openai-completions",
|
|
2634
|
-
models: [
|
|
2635
|
-
{
|
|
2636
|
-
id: "gpt-5.4",
|
|
2637
|
-
name: "ChatGPT 5.4 (Subscription)",
|
|
2638
|
-
reasoning: false,
|
|
2639
|
-
input: ["text", "image"],
|
|
2640
|
-
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
|
|
2641
|
-
contextWindow: 200000,
|
|
2642
|
-
maxTokens: 8192,
|
|
2643
|
-
},
|
|
2644
|
-
{
|
|
2645
|
-
id: "gpt-5.3-codex",
|
|
2646
|
-
name: "ChatGPT 5.3 Codex (Subscription)",
|
|
2647
|
-
reasoning: false,
|
|
2648
|
-
input: ["text"],
|
|
2649
|
-
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
|
|
2650
|
-
contextWindow: 200000,
|
|
2651
|
-
maxTokens: 8192,
|
|
2652
|
-
},
|
|
2653
|
-
],
|
|
2654
|
-
});
|
|
2655
|
-
}
|
|
2656
|
-
PI_EXT_EOF
|
|
2657
|
-
fi
|
|
3438
|
+
# M6 dropped the third-party LLM-runtime install + extension write
|
|
3439
|
+
# that used to live here. The bridge now spawns
|
|
3440
|
+
# \`apes agents serve --rpc\` directly (M8) which calls LiteLLM
|
|
3441
|
+
# itself, so no third-party extension config is needed.
|
|
2658
3442
|
|
|
2659
3443
|
set -a
|
|
2660
|
-
. "$HOME
|
|
3444
|
+
. "$HOME/Library/Application Support/openape/bridge/.env"
|
|
2661
3445
|
set +a
|
|
2662
3446
|
exec openape-chat-bridge
|
|
2663
3447
|
`;
|
|
@@ -2706,7 +3490,7 @@ function buildBridgePlist(agentName, homeDir, ownerEmail) {
|
|
|
2706
3490
|
}
|
|
2707
3491
|
|
|
2708
3492
|
// src/commands/agents/spawn.ts
|
|
2709
|
-
var spawnAgentCommand =
|
|
3493
|
+
var spawnAgentCommand = defineCommand26({
|
|
2710
3494
|
meta: {
|
|
2711
3495
|
name: "spawn",
|
|
2712
3496
|
description: "Provision a local macOS agent end-to-end (OS user, keypair, IdP agent, Claude hook)"
|
|
@@ -2789,15 +3573,15 @@ and try again.`
|
|
|
2789
3573
|
throw new CliError(`macOS user "${name}" already exists (uid=${existing.uid ?? "?"}). Refusing to overwrite.`);
|
|
2790
3574
|
}
|
|
2791
3575
|
const homeDir = `/Users/${name}`;
|
|
2792
|
-
const scratch = mkdtempSync2(
|
|
2793
|
-
const scriptPath =
|
|
3576
|
+
const scratch = mkdtempSync2(join6(tmpdir2(), `apes-spawn-${name}-`));
|
|
3577
|
+
const scriptPath = join6(scratch, "setup.sh");
|
|
2794
3578
|
try {
|
|
2795
|
-
|
|
3579
|
+
consola23.start(`Generating keypair for ${name}\u2026`);
|
|
2796
3580
|
const { privatePem, publicSshLine } = generateKeyPairInMemory();
|
|
2797
|
-
|
|
3581
|
+
consola23.start(`Registering agent at ${idp}\u2026`);
|
|
2798
3582
|
const registration = await registerAgentAtIdp({ name, publicKey: publicSshLine, idp });
|
|
2799
|
-
|
|
2800
|
-
|
|
3583
|
+
consola23.success(`Registered as ${registration.email}`);
|
|
3584
|
+
consola23.start("Issuing agent access token\u2026");
|
|
2801
3585
|
const { token, expiresIn } = await issueAgentToken({
|
|
2802
3586
|
idp,
|
|
2803
3587
|
agentEmail: registration.email,
|
|
@@ -2829,6 +3613,13 @@ and try again.`
|
|
|
2829
3613
|
envFile: buildBridgeEnvFile(cfg)
|
|
2830
3614
|
};
|
|
2831
3615
|
})() : null;
|
|
3616
|
+
const troopPlistLabel = `openape.troop.sync.${name}`;
|
|
3617
|
+
const troopPlistPath = `${homeDir}/Library/LaunchAgents/${troopPlistLabel}.plist`;
|
|
3618
|
+
const troop = {
|
|
3619
|
+
plistLabel: troopPlistLabel,
|
|
3620
|
+
plistPath: troopPlistPath,
|
|
3621
|
+
plistContent: buildSyncPlist({ agentName: name, apesBin: apes, homeDir })
|
|
3622
|
+
};
|
|
2832
3623
|
const script = buildSpawnSetupScript({
|
|
2833
3624
|
name,
|
|
2834
3625
|
homeDir,
|
|
@@ -2839,16 +3630,18 @@ and try again.`
|
|
|
2839
3630
|
claudeSettingsJson: includeClaudeHook ? CLAUDE_SETTINGS_JSON : null,
|
|
2840
3631
|
hookScriptSource: includeClaudeHook ? BASH_VIA_APE_SHELL_HOOK_SOURCE : null,
|
|
2841
3632
|
claudeOauthToken,
|
|
2842
|
-
bridge
|
|
3633
|
+
bridge,
|
|
3634
|
+
troop
|
|
2843
3635
|
});
|
|
2844
|
-
|
|
2845
|
-
|
|
2846
|
-
|
|
2847
|
-
|
|
2848
|
-
|
|
3636
|
+
writeFileSync4(scriptPath, script, { mode: 448 });
|
|
3637
|
+
consola23.start("Running privileged setup as root via `apes run --as root --wait`\u2026");
|
|
3638
|
+
consola23.info("You will be asked to approve the as=root grant in your DDISA inbox; this command blocks until you do.");
|
|
3639
|
+
execFileSync7(apes, ["run", "--as", "root", "--wait", "--", "bash", scriptPath], { stdio: "inherit" });
|
|
3640
|
+
consola23.success(`Agent ${name} spawned.`);
|
|
3641
|
+
consola23.info(`\u{1F517} Troop: https://troop.openape.ai/agents/${name}`);
|
|
2849
3642
|
if (args.bridge) {
|
|
2850
|
-
|
|
2851
|
-
|
|
3643
|
+
consola23.info(`On first boot, the bridge will send you a contact request from ${registration.email}.`);
|
|
3644
|
+
consola23.info("Open chat.openape.ai and accept it to start chatting with the agent.");
|
|
2852
3645
|
}
|
|
2853
3646
|
console.log("");
|
|
2854
3647
|
console.log("Run as the agent with:");
|
|
@@ -2881,31 +3674,344 @@ async function resolveClaudeToken(opts) {
|
|
|
2881
3674
|
return raw;
|
|
2882
3675
|
}
|
|
2883
3676
|
|
|
3677
|
+
// src/commands/agents/sync.ts
|
|
3678
|
+
import { existsSync as existsSync9, mkdirSync as mkdirSync4, readFileSync as readFileSync10, writeFileSync as writeFileSync6 } from "fs";
|
|
3679
|
+
import { homedir as homedir10 } from "os";
|
|
3680
|
+
import { join as join8 } from "path";
|
|
3681
|
+
import { defineCommand as defineCommand27 } from "citty";
|
|
3682
|
+
import consola24 from "consola";
|
|
3683
|
+
|
|
3684
|
+
// src/lib/launchd-reconcile.ts
|
|
3685
|
+
import { execFileSync as execFileSync8 } from "child_process";
|
|
3686
|
+
import { mkdirSync as mkdirSync3, readdirSync, readFileSync as readFileSync9, unlinkSync, writeFileSync as writeFileSync5 } from "fs";
|
|
3687
|
+
import { homedir as homedir9, userInfo as userInfo2 } from "os";
|
|
3688
|
+
import { join as join7 } from "path";
|
|
3689
|
+
var PLIST_PREFIX = "openape.troop.";
|
|
3690
|
+
function plistDir() {
|
|
3691
|
+
return join7(homedir9(), "Library", "LaunchAgents");
|
|
3692
|
+
}
|
|
3693
|
+
function plistPath(agentName, taskId) {
|
|
3694
|
+
return join7(plistDir(), `${PLIST_PREFIX}${agentName}.${taskId}.plist`);
|
|
3695
|
+
}
|
|
3696
|
+
function plistLabel(agentName, taskId) {
|
|
3697
|
+
return `${PLIST_PREFIX}${agentName}.${taskId}`;
|
|
3698
|
+
}
|
|
3699
|
+
function fieldValues(token, range) {
|
|
3700
|
+
const [min, max] = range;
|
|
3701
|
+
if (token === "*") return null;
|
|
3702
|
+
if (token.startsWith("*/")) {
|
|
3703
|
+
const step = Number(token.slice(2));
|
|
3704
|
+
if (!Number.isInteger(step) || step < 1) return [];
|
|
3705
|
+
const values = [];
|
|
3706
|
+
for (let i = min; i <= max; i++) {
|
|
3707
|
+
if (i % step === 0) values.push(i);
|
|
3708
|
+
}
|
|
3709
|
+
return values;
|
|
3710
|
+
}
|
|
3711
|
+
const n = Number(token);
|
|
3712
|
+
return Number.isInteger(n) ? [n] : [];
|
|
3713
|
+
}
|
|
3714
|
+
function cronToSchedule(expr) {
|
|
3715
|
+
const parts = expr.trim().split(/\s+/);
|
|
3716
|
+
if (parts.length !== 5) return [];
|
|
3717
|
+
const [m, h, dom, mo, dow] = parts;
|
|
3718
|
+
const minutes = fieldValues(m, [0, 59]);
|
|
3719
|
+
const hours = fieldValues(h, [0, 23]);
|
|
3720
|
+
const doms = fieldValues(dom, [1, 31]);
|
|
3721
|
+
const months = fieldValues(mo, [1, 12]);
|
|
3722
|
+
const dows = fieldValues(dow, [0, 7]);
|
|
3723
|
+
const slots = [];
|
|
3724
|
+
const minList = minutes ?? [null];
|
|
3725
|
+
const hourList = hours ?? [null];
|
|
3726
|
+
const domList = doms ?? [null];
|
|
3727
|
+
const monthList = months ?? [null];
|
|
3728
|
+
const dowList = dows ?? [null];
|
|
3729
|
+
for (const M of minList) {
|
|
3730
|
+
for (const H of hourList) {
|
|
3731
|
+
for (const D of domList) {
|
|
3732
|
+
for (const Mo of monthList) {
|
|
3733
|
+
for (const W of dowList) {
|
|
3734
|
+
const slot = {};
|
|
3735
|
+
if (M !== null) slot.Minute = M;
|
|
3736
|
+
if (H !== null) slot.Hour = H;
|
|
3737
|
+
if (D !== null) slot.Day = D;
|
|
3738
|
+
if (Mo !== null) slot.Month = Mo;
|
|
3739
|
+
if (W !== null) slot.Weekday = W === 7 ? 0 : W;
|
|
3740
|
+
slots.push(slot);
|
|
3741
|
+
}
|
|
3742
|
+
}
|
|
3743
|
+
}
|
|
3744
|
+
}
|
|
3745
|
+
}
|
|
3746
|
+
return slots;
|
|
3747
|
+
}
|
|
3748
|
+
function escape2(s) {
|
|
3749
|
+
return s.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">");
|
|
3750
|
+
}
|
|
3751
|
+
function plistBody(input) {
|
|
3752
|
+
const calendarBlocks = input.schedule.map((slot) => {
|
|
3753
|
+
const lines = [];
|
|
3754
|
+
if (slot.Minute !== void 0) lines.push(` <key>Minute</key><integer>${slot.Minute}</integer>`);
|
|
3755
|
+
if (slot.Hour !== void 0) lines.push(` <key>Hour</key><integer>${slot.Hour}</integer>`);
|
|
3756
|
+
if (slot.Day !== void 0) lines.push(` <key>Day</key><integer>${slot.Day}</integer>`);
|
|
3757
|
+
if (slot.Month !== void 0) lines.push(` <key>Month</key><integer>${slot.Month}</integer>`);
|
|
3758
|
+
if (slot.Weekday !== void 0) lines.push(` <key>Weekday</key><integer>${slot.Weekday}</integer>`);
|
|
3759
|
+
return ` <dict>
|
|
3760
|
+
${lines.join("\n")}
|
|
3761
|
+
</dict>`;
|
|
3762
|
+
}).join("\n");
|
|
3763
|
+
const calendarKey = input.schedule.length === 1 ? ` <key>StartCalendarInterval</key>
|
|
3764
|
+
<dict>
|
|
3765
|
+
${calendarBlocks.replace(/^ {4}/gm, " ").replace(/^ {2}<dict>\n|\n {2}<\/dict>$/g, "")}
|
|
3766
|
+
</dict>` : ` <key>StartCalendarInterval</key>
|
|
3767
|
+
<array>
|
|
3768
|
+
${calendarBlocks}
|
|
3769
|
+
</array>`;
|
|
3770
|
+
return `<?xml version="1.0" encoding="UTF-8"?>
|
|
3771
|
+
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
3772
|
+
<plist version="1.0">
|
|
3773
|
+
<dict>
|
|
3774
|
+
<key>Label</key>
|
|
3775
|
+
<string>${escape2(input.label)}</string>
|
|
3776
|
+
<key>ProgramArguments</key>
|
|
3777
|
+
<array>
|
|
3778
|
+
<string>${escape2(input.apesBin)}</string>
|
|
3779
|
+
<string>agents</string>
|
|
3780
|
+
<string>run</string>
|
|
3781
|
+
<string>${escape2(input.taskId)}</string>
|
|
3782
|
+
</array>
|
|
3783
|
+
<key>WorkingDirectory</key>
|
|
3784
|
+
<string>${escape2(input.homeDir)}</string>
|
|
3785
|
+
<key>EnvironmentVariables</key>
|
|
3786
|
+
<dict>
|
|
3787
|
+
<key>HOME</key>
|
|
3788
|
+
<string>${escape2(input.homeDir)}</string>
|
|
3789
|
+
</dict>
|
|
3790
|
+
${calendarKey}
|
|
3791
|
+
<key>StandardOutPath</key>
|
|
3792
|
+
<string>${escape2(input.homeDir)}/Library/Logs/openape-troop-${escape2(input.taskId)}.log</string>
|
|
3793
|
+
<key>StandardErrorPath</key>
|
|
3794
|
+
<string>${escape2(input.homeDir)}/Library/Logs/openape-troop-${escape2(input.taskId)}.log</string>
|
|
3795
|
+
</dict>
|
|
3796
|
+
</plist>
|
|
3797
|
+
`;
|
|
3798
|
+
}
|
|
3799
|
+
function buildPlistContent(args) {
|
|
3800
|
+
return plistBody({
|
|
3801
|
+
label: plistLabel(args.agentName, args.task.taskId),
|
|
3802
|
+
apesBin: args.apesBin,
|
|
3803
|
+
taskId: args.task.taskId,
|
|
3804
|
+
schedule: cronToSchedule(args.task.cron),
|
|
3805
|
+
homeDir: args.homeDir
|
|
3806
|
+
});
|
|
3807
|
+
}
|
|
3808
|
+
function uid() {
|
|
3809
|
+
return userInfo2().uid;
|
|
3810
|
+
}
|
|
3811
|
+
function bootstrap(label, path2) {
|
|
3812
|
+
try {
|
|
3813
|
+
execFileSync8("/bin/launchctl", ["bootout", `gui/${uid()}/${label}`], { stdio: "ignore" });
|
|
3814
|
+
} catch {
|
|
3815
|
+
}
|
|
3816
|
+
execFileSync8("/bin/launchctl", ["bootstrap", `gui/${uid()}`, path2], { stdio: "ignore" });
|
|
3817
|
+
}
|
|
3818
|
+
function bootout(label) {
|
|
3819
|
+
try {
|
|
3820
|
+
execFileSync8("/bin/launchctl", ["bootout", `gui/${uid()}/${label}`], { stdio: "ignore" });
|
|
3821
|
+
} catch {
|
|
3822
|
+
}
|
|
3823
|
+
}
|
|
3824
|
+
function reconcile(input) {
|
|
3825
|
+
mkdirSync3(plistDir(), { recursive: true });
|
|
3826
|
+
const present = readdirSync(plistDir()).filter((f) => f.startsWith(`${PLIST_PREFIX}${input.agentName}.`) && f.endsWith(".plist"));
|
|
3827
|
+
const presentTaskIds = new Set(
|
|
3828
|
+
present.map((f) => f.slice(`${PLIST_PREFIX}${input.agentName}.`.length, -".plist".length))
|
|
3829
|
+
);
|
|
3830
|
+
const desiredById = new Map(input.desired.map((t) => [t.taskId, t]));
|
|
3831
|
+
const result = { added: [], updated: [], removed: [], unchanged: [] };
|
|
3832
|
+
for (const taskId of presentTaskIds) {
|
|
3833
|
+
if (!desiredById.has(taskId)) {
|
|
3834
|
+
const path2 = plistPath(input.agentName, taskId);
|
|
3835
|
+
bootout(plistLabel(input.agentName, taskId));
|
|
3836
|
+
try {
|
|
3837
|
+
unlinkSync(path2);
|
|
3838
|
+
} catch {
|
|
3839
|
+
}
|
|
3840
|
+
result.removed.push(taskId);
|
|
3841
|
+
}
|
|
3842
|
+
}
|
|
3843
|
+
for (const task of input.desired) {
|
|
3844
|
+
const path2 = plistPath(input.agentName, task.taskId);
|
|
3845
|
+
const desiredContent = buildPlistContent({
|
|
3846
|
+
agentName: input.agentName,
|
|
3847
|
+
apesBin: input.apesBin,
|
|
3848
|
+
homeDir: input.homeDir,
|
|
3849
|
+
task
|
|
3850
|
+
});
|
|
3851
|
+
let existingContent = "";
|
|
3852
|
+
try {
|
|
3853
|
+
existingContent = readFileSync9(path2, "utf8");
|
|
3854
|
+
} catch {
|
|
3855
|
+
}
|
|
3856
|
+
if (existingContent === desiredContent) {
|
|
3857
|
+
if (task.enabled) bootstrap(plistLabel(input.agentName, task.taskId), path2);
|
|
3858
|
+
else bootout(plistLabel(input.agentName, task.taskId));
|
|
3859
|
+
result.unchanged.push(task.taskId);
|
|
3860
|
+
continue;
|
|
3861
|
+
}
|
|
3862
|
+
writeFileSync5(path2, desiredContent, { mode: 420 });
|
|
3863
|
+
if (task.enabled) bootstrap(plistLabel(input.agentName, task.taskId), path2);
|
|
3864
|
+
else bootout(plistLabel(input.agentName, task.taskId));
|
|
3865
|
+
if (presentTaskIds.has(task.taskId)) result.updated.push(task.taskId);
|
|
3866
|
+
else result.added.push(task.taskId);
|
|
3867
|
+
}
|
|
3868
|
+
return result;
|
|
3869
|
+
}
|
|
3870
|
+
|
|
3871
|
+
// src/lib/macos-host.ts
|
|
3872
|
+
import { execFileSync as execFileSync9 } from "child_process";
|
|
3873
|
+
import { hostname as hostname3 } from "os";
|
|
3874
|
+
function getHostId() {
|
|
3875
|
+
try {
|
|
3876
|
+
const output = execFileSync9(
|
|
3877
|
+
"/usr/sbin/ioreg",
|
|
3878
|
+
["-d2", "-c", "IOPlatformExpertDevice"],
|
|
3879
|
+
{ encoding: "utf8", timeout: 2e3 }
|
|
3880
|
+
);
|
|
3881
|
+
const match = output.match(/"IOPlatformUUID"\s*=\s*"([^"]+)"/);
|
|
3882
|
+
return match ? match[1].trim().toLowerCase() : "";
|
|
3883
|
+
} catch {
|
|
3884
|
+
return "";
|
|
3885
|
+
}
|
|
3886
|
+
}
|
|
3887
|
+
function getHostname() {
|
|
3888
|
+
try {
|
|
3889
|
+
return hostname3();
|
|
3890
|
+
} catch {
|
|
3891
|
+
return "";
|
|
3892
|
+
}
|
|
3893
|
+
}
|
|
3894
|
+
|
|
3895
|
+
// src/commands/agents/sync.ts
|
|
3896
|
+
var AUTH_PATH3 = join8(homedir10(), ".config", "apes", "auth.json");
|
|
3897
|
+
var TASK_CACHE_DIR2 = join8(homedir10(), ".openape", "agent", "tasks");
|
|
3898
|
+
function readAuthJson() {
|
|
3899
|
+
if (!existsSync9(AUTH_PATH3)) {
|
|
3900
|
+
throw new CliError(
|
|
3901
|
+
`No agent auth found at ${AUTH_PATH3}. Run \`apes agents spawn <name>\` to provision an agent first.`
|
|
3902
|
+
);
|
|
3903
|
+
}
|
|
3904
|
+
const raw = readFileSync10(AUTH_PATH3, "utf8");
|
|
3905
|
+
let parsed;
|
|
3906
|
+
try {
|
|
3907
|
+
parsed = JSON.parse(raw);
|
|
3908
|
+
} catch (err) {
|
|
3909
|
+
throw new CliError(`${AUTH_PATH3} is not valid JSON: ${err.message}`);
|
|
3910
|
+
}
|
|
3911
|
+
if (!parsed.access_token) throw new CliError(`${AUTH_PATH3} is missing access_token`);
|
|
3912
|
+
if (!parsed.email) throw new CliError(`${AUTH_PATH3} is missing email`);
|
|
3913
|
+
if (!parsed.email.startsWith("agent+")) {
|
|
3914
|
+
throw new CliError(
|
|
3915
|
+
`${AUTH_PATH3} email is "${parsed.email}" \u2014 expected an agent+name+domain@idp address. Run \`apes agents spawn\` rather than calling sync from a human user.`
|
|
3916
|
+
);
|
|
3917
|
+
}
|
|
3918
|
+
return parsed;
|
|
3919
|
+
}
|
|
3920
|
+
function agentNameFromEmail(email) {
|
|
3921
|
+
const m = email.match(/^agent\+([^+]+)\+/);
|
|
3922
|
+
if (!m) throw new CliError(`agent email "${email}" does not match agent+name+domain pattern`);
|
|
3923
|
+
return m[1];
|
|
3924
|
+
}
|
|
3925
|
+
function findApesBin() {
|
|
3926
|
+
const argv1 = process.argv[1];
|
|
3927
|
+
if (argv1 && existsSync9(argv1)) return argv1;
|
|
3928
|
+
for (const candidate of ["/opt/homebrew/bin/apes", "/usr/local/bin/apes"]) {
|
|
3929
|
+
if (existsSync9(candidate)) return candidate;
|
|
3930
|
+
}
|
|
3931
|
+
throw new CliError("Could not locate the apes binary path for launchd. Set APES_BIN env var to point at it.");
|
|
3932
|
+
}
|
|
3933
|
+
var syncAgentCommand = defineCommand27({
|
|
3934
|
+
meta: {
|
|
3935
|
+
name: "sync",
|
|
3936
|
+
description: "Pull this agent's task list from troop.openape.ai and reconcile launchd plists"
|
|
3937
|
+
},
|
|
3938
|
+
args: {
|
|
3939
|
+
"troop-url": {
|
|
3940
|
+
type: "string",
|
|
3941
|
+
description: "Override troop SP base URL (default: $OPENAPE_TROOP_URL or https://troop.openape.ai)"
|
|
3942
|
+
}
|
|
3943
|
+
},
|
|
3944
|
+
async run({ args }) {
|
|
3945
|
+
const auth = readAuthJson();
|
|
3946
|
+
const agentName = agentNameFromEmail(auth.email);
|
|
3947
|
+
const troopUrl = resolveTroopUrl(args["troop-url"]);
|
|
3948
|
+
const client = new TroopClient(troopUrl, auth.access_token);
|
|
3949
|
+
const hostId = getHostId();
|
|
3950
|
+
const host = getHostname();
|
|
3951
|
+
if (!hostId) {
|
|
3952
|
+
throw new CliError("Could not read IOPlatformUUID \u2014 is this macOS? Troop sync only works on macOS for v1.");
|
|
3953
|
+
}
|
|
3954
|
+
if (!auth.owner_email) {
|
|
3955
|
+
throw new CliError(`${AUTH_PATH3} is missing owner_email \u2014 re-run \`apes agents spawn\` to update.`);
|
|
3956
|
+
}
|
|
3957
|
+
consola24.start(`Syncing ${agentName} (${host}, hostId ${hostId.slice(0, 8)}\u2026) with ${troopUrl}`);
|
|
3958
|
+
const sync = await client.sync({
|
|
3959
|
+
hostname: host,
|
|
3960
|
+
hostId,
|
|
3961
|
+
ownerEmail: auth.owner_email
|
|
3962
|
+
});
|
|
3963
|
+
consola24.info(sync.first_sync ? "\u2713 first sync \u2014 agent registered" : "\u2713 presence updated");
|
|
3964
|
+
const tasks = await client.listTasks();
|
|
3965
|
+
consola24.info(`Pulled ${tasks.length} task${tasks.length === 1 ? "" : "s"}`);
|
|
3966
|
+
mkdirSync4(TASK_CACHE_DIR2, { recursive: true });
|
|
3967
|
+
for (const task of tasks) {
|
|
3968
|
+
const path2 = join8(TASK_CACHE_DIR2, `${task.taskId}.json`);
|
|
3969
|
+
writeFileSync6(path2, `${JSON.stringify(task, null, 2)}
|
|
3970
|
+
`, { mode: 384 });
|
|
3971
|
+
}
|
|
3972
|
+
const apesBin = findApesBin();
|
|
3973
|
+
const result = reconcile({
|
|
3974
|
+
agentName,
|
|
3975
|
+
apesBin,
|
|
3976
|
+
homeDir: homedir10(),
|
|
3977
|
+
desired: tasks
|
|
3978
|
+
});
|
|
3979
|
+
if (result.added.length) consola24.success(`launchd: added ${result.added.join(", ")}`);
|
|
3980
|
+
if (result.updated.length) consola24.success(`launchd: updated ${result.updated.join(", ")}`);
|
|
3981
|
+
if (result.removed.length) consola24.warn(`launchd: removed ${result.removed.join(", ")}`);
|
|
3982
|
+
if (result.unchanged.length) consola24.info(`launchd: ${result.unchanged.length} unchanged`);
|
|
3983
|
+
consola24.success("Sync complete.");
|
|
3984
|
+
}
|
|
3985
|
+
});
|
|
3986
|
+
|
|
2884
3987
|
// src/commands/agents/index.ts
|
|
2885
|
-
var agentsCommand =
|
|
3988
|
+
var agentsCommand = defineCommand28({
|
|
2886
3989
|
meta: {
|
|
2887
3990
|
name: "agents",
|
|
2888
|
-
description: "Manage owned agents (register, spawn, list, destroy, allow)"
|
|
3991
|
+
description: "Manage owned agents (register, spawn, list, destroy, allow, sync, run, serve)"
|
|
2889
3992
|
},
|
|
2890
3993
|
subCommands: {
|
|
2891
3994
|
register: registerAgentCommand,
|
|
2892
3995
|
spawn: spawnAgentCommand,
|
|
2893
3996
|
list: listAgentsCommand,
|
|
2894
3997
|
destroy: destroyAgentCommand,
|
|
2895
|
-
allow: allowAgentCommand
|
|
3998
|
+
allow: allowAgentCommand,
|
|
3999
|
+
sync: syncAgentCommand,
|
|
4000
|
+
run: runAgentCommand,
|
|
4001
|
+
serve: serveAgentCommand
|
|
2896
4002
|
}
|
|
2897
4003
|
});
|
|
2898
4004
|
|
|
2899
4005
|
// src/commands/adapter/index.ts
|
|
2900
|
-
import { defineCommand as
|
|
2901
|
-
import
|
|
2902
|
-
var adapterCommand =
|
|
4006
|
+
import { defineCommand as defineCommand29 } from "citty";
|
|
4007
|
+
import consola25 from "consola";
|
|
4008
|
+
var adapterCommand = defineCommand29({
|
|
2903
4009
|
meta: {
|
|
2904
4010
|
name: "adapter",
|
|
2905
4011
|
description: "Manage CLI adapters"
|
|
2906
4012
|
},
|
|
2907
4013
|
subCommands: {
|
|
2908
|
-
list:
|
|
4014
|
+
list: defineCommand29({
|
|
2909
4015
|
meta: {
|
|
2910
4016
|
name: "list",
|
|
2911
4017
|
description: "List available adapters"
|
|
@@ -2936,7 +4042,7 @@ var adapterCommand = defineCommand26({
|
|
|
2936
4042
|
`);
|
|
2937
4043
|
return;
|
|
2938
4044
|
}
|
|
2939
|
-
|
|
4045
|
+
consola25.info(`Registry: ${index2.adapters.length} adapters (${index2.generated_at})`);
|
|
2940
4046
|
for (const a of index2.adapters) {
|
|
2941
4047
|
const installed = isInstalled(a.id, false) ? " [installed]" : "";
|
|
2942
4048
|
console.log(` ${a.id.padEnd(12)} ${a.name.padEnd(24)} ${a.category}${installed}`);
|
|
@@ -2958,7 +4064,7 @@ var adapterCommand = defineCommand26({
|
|
|
2958
4064
|
return;
|
|
2959
4065
|
}
|
|
2960
4066
|
if (local.length === 0) {
|
|
2961
|
-
|
|
4067
|
+
consola25.info("No adapters installed. Use `apes adapter list --remote` to see available adapters.");
|
|
2962
4068
|
return;
|
|
2963
4069
|
}
|
|
2964
4070
|
for (const a of local) {
|
|
@@ -2966,7 +4072,7 @@ var adapterCommand = defineCommand26({
|
|
|
2966
4072
|
}
|
|
2967
4073
|
}
|
|
2968
4074
|
}),
|
|
2969
|
-
install:
|
|
4075
|
+
install: defineCommand29({
|
|
2970
4076
|
meta: {
|
|
2971
4077
|
name: "install",
|
|
2972
4078
|
description: "Install an adapter from the registry"
|
|
@@ -2995,24 +4101,24 @@ var adapterCommand = defineCommand26({
|
|
|
2995
4101
|
for (const id of ids) {
|
|
2996
4102
|
const entry = findAdapter(index, id);
|
|
2997
4103
|
if (!entry) {
|
|
2998
|
-
|
|
4104
|
+
consola25.error(`Adapter "${id}" not found in registry. Use \`apes adapter search ${id}\` to search.`);
|
|
2999
4105
|
continue;
|
|
3000
4106
|
}
|
|
3001
4107
|
const conflicts = findConflictingAdapters(entry.executable, id);
|
|
3002
4108
|
if (conflicts.length > 0) {
|
|
3003
4109
|
for (const c of conflicts) {
|
|
3004
|
-
|
|
3005
|
-
|
|
4110
|
+
consola25.warn(`Conflicting adapter found: ${c.path} (id: ${c.adapterId}, executable: ${c.executable})`);
|
|
4111
|
+
consola25.warn(` Remove it with: apes adapter remove ${c.adapterId}`);
|
|
3006
4112
|
}
|
|
3007
4113
|
}
|
|
3008
4114
|
const result = await installAdapter(entry, { local });
|
|
3009
4115
|
const verb = result.updated ? "Updated" : "Installed";
|
|
3010
|
-
|
|
3011
|
-
|
|
4116
|
+
consola25.success(`${verb} ${result.id} \u2192 ${result.path}`);
|
|
4117
|
+
consola25.info(`Digest: ${result.digest}`);
|
|
3012
4118
|
}
|
|
3013
4119
|
}
|
|
3014
4120
|
}),
|
|
3015
|
-
remove:
|
|
4121
|
+
remove: defineCommand29({
|
|
3016
4122
|
meta: {
|
|
3017
4123
|
name: "remove",
|
|
3018
4124
|
description: "Remove an installed adapter"
|
|
@@ -3035,9 +4141,9 @@ var adapterCommand = defineCommand26({
|
|
|
3035
4141
|
let failed = false;
|
|
3036
4142
|
for (const id of ids) {
|
|
3037
4143
|
if (removeAdapter(id, local)) {
|
|
3038
|
-
|
|
4144
|
+
consola25.success(`Removed adapter: ${id}`);
|
|
3039
4145
|
} else {
|
|
3040
|
-
|
|
4146
|
+
consola25.error(`Adapter "${id}" is not installed${local ? " locally" : ""}`);
|
|
3041
4147
|
failed = true;
|
|
3042
4148
|
}
|
|
3043
4149
|
}
|
|
@@ -3045,7 +4151,7 @@ var adapterCommand = defineCommand26({
|
|
|
3045
4151
|
throw new CliError("Some adapters could not be removed");
|
|
3046
4152
|
}
|
|
3047
4153
|
}),
|
|
3048
|
-
info:
|
|
4154
|
+
info: defineCommand29({
|
|
3049
4155
|
meta: {
|
|
3050
4156
|
name: "info",
|
|
3051
4157
|
description: "Show detailed adapter information"
|
|
@@ -3087,7 +4193,7 @@ var adapterCommand = defineCommand26({
|
|
|
3087
4193
|
}
|
|
3088
4194
|
}
|
|
3089
4195
|
}),
|
|
3090
|
-
search:
|
|
4196
|
+
search: defineCommand29({
|
|
3091
4197
|
meta: {
|
|
3092
4198
|
name: "search",
|
|
3093
4199
|
description: "Search adapters in the registry"
|
|
@@ -3119,7 +4225,7 @@ var adapterCommand = defineCommand26({
|
|
|
3119
4225
|
return;
|
|
3120
4226
|
}
|
|
3121
4227
|
if (results.length === 0) {
|
|
3122
|
-
|
|
4228
|
+
consola25.info(`No adapters matching "${query}"`);
|
|
3123
4229
|
return;
|
|
3124
4230
|
}
|
|
3125
4231
|
for (const a of results) {
|
|
@@ -3128,7 +4234,7 @@ var adapterCommand = defineCommand26({
|
|
|
3128
4234
|
}
|
|
3129
4235
|
}
|
|
3130
4236
|
}),
|
|
3131
|
-
update:
|
|
4237
|
+
update: defineCommand29({
|
|
3132
4238
|
meta: {
|
|
3133
4239
|
name: "update",
|
|
3134
4240
|
description: "Update installed adapters"
|
|
@@ -3154,33 +4260,33 @@ var adapterCommand = defineCommand26({
|
|
|
3154
4260
|
const targetId = args.id ? String(args.id) : void 0;
|
|
3155
4261
|
const targets = targetId ? [targetId] : index.adapters.map((a) => a.id).filter((id) => isInstalled(id, false));
|
|
3156
4262
|
if (targets.length === 0) {
|
|
3157
|
-
|
|
4263
|
+
consola25.info("No adapters installed to update.");
|
|
3158
4264
|
return;
|
|
3159
4265
|
}
|
|
3160
4266
|
for (const id of targets) {
|
|
3161
4267
|
const entry = findAdapter(index, id);
|
|
3162
4268
|
if (!entry) {
|
|
3163
|
-
|
|
4269
|
+
consola25.warn(`${id}: not found in registry, skipping`);
|
|
3164
4270
|
continue;
|
|
3165
4271
|
}
|
|
3166
4272
|
const localDigest = getInstalledDigest(id, false);
|
|
3167
4273
|
if (localDigest === entry.digest) {
|
|
3168
|
-
|
|
4274
|
+
consola25.info(`${id}: already up to date`);
|
|
3169
4275
|
continue;
|
|
3170
4276
|
}
|
|
3171
4277
|
if (localDigest && !args.yes) {
|
|
3172
|
-
|
|
3173
|
-
|
|
3174
|
-
|
|
3175
|
-
|
|
4278
|
+
consola25.warn(`${id}: digest will change \u2014 existing grants for this adapter will be invalidated`);
|
|
4279
|
+
consola25.info(` Old: ${localDigest}`);
|
|
4280
|
+
consola25.info(` New: ${entry.digest}`);
|
|
4281
|
+
consola25.info(" Use --yes to confirm");
|
|
3176
4282
|
continue;
|
|
3177
4283
|
}
|
|
3178
4284
|
const result = await installAdapter(entry);
|
|
3179
|
-
|
|
4285
|
+
consola25.success(`Updated ${result.id} \u2192 ${result.path}`);
|
|
3180
4286
|
}
|
|
3181
4287
|
}
|
|
3182
4288
|
}),
|
|
3183
|
-
verify:
|
|
4289
|
+
verify: defineCommand29({
|
|
3184
4290
|
meta: {
|
|
3185
4291
|
name: "verify",
|
|
3186
4292
|
description: "Verify installed adapter against registry digest"
|
|
@@ -3213,7 +4319,7 @@ var adapterCommand = defineCommand26({
|
|
|
3213
4319
|
if (!localDigest)
|
|
3214
4320
|
throw new Error(`Adapter "${id}" is not installed${local ? " locally" : ""}`);
|
|
3215
4321
|
if (localDigest === entry.digest) {
|
|
3216
|
-
|
|
4322
|
+
consola25.success(`${id}: digest matches registry`);
|
|
3217
4323
|
} else {
|
|
3218
4324
|
console.log(` Local: ${localDigest}`);
|
|
3219
4325
|
console.log(` Registry: ${entry.digest}`);
|
|
@@ -3225,11 +4331,11 @@ var adapterCommand = defineCommand26({
|
|
|
3225
4331
|
});
|
|
3226
4332
|
|
|
3227
4333
|
// src/commands/run.ts
|
|
3228
|
-
import { execFileSync as
|
|
3229
|
-
import { hostname as
|
|
4334
|
+
import { execFileSync as execFileSync10 } from "child_process";
|
|
4335
|
+
import { hostname as hostname4 } from "os";
|
|
3230
4336
|
import { basename } from "path";
|
|
3231
|
-
import { defineCommand as
|
|
3232
|
-
import
|
|
4337
|
+
import { defineCommand as defineCommand30 } from "citty";
|
|
4338
|
+
import consola26 from "consola";
|
|
3233
4339
|
function shouldWaitForGrant(args) {
|
|
3234
4340
|
return args.wait === true || process.env.APE_WAIT === "1";
|
|
3235
4341
|
}
|
|
@@ -3266,7 +4372,7 @@ function printPendingGrantInfo(grant, idp) {
|
|
|
3266
4372
|
const statusCmd = `apes grants status ${grant.id}`;
|
|
3267
4373
|
const executeCmd = `apes grants run ${grant.id}`;
|
|
3268
4374
|
if (mode === "human") {
|
|
3269
|
-
|
|
4375
|
+
consola26.success(`Grant ${grant.id} created \u2014 awaiting your approval`);
|
|
3270
4376
|
console.log(` Approve in browser: ${approveUrl}`);
|
|
3271
4377
|
console.log(` Check status: ${statusCmd}`);
|
|
3272
4378
|
console.log(` Run after approval: ${executeCmd}`);
|
|
@@ -3276,7 +4382,7 @@ function printPendingGrantInfo(grant, idp) {
|
|
|
3276
4382
|
return;
|
|
3277
4383
|
}
|
|
3278
4384
|
const maxMin = getPollMaxMinutes();
|
|
3279
|
-
|
|
4385
|
+
consola26.success(`Grant ${grant.id} created (pending approval)`);
|
|
3280
4386
|
console.log(` Approve: ${approveUrl}`);
|
|
3281
4387
|
console.log(` Status: ${statusCmd} [--json]`);
|
|
3282
4388
|
console.log(` Execute: ${executeCmd} --wait`);
|
|
@@ -3298,7 +4404,7 @@ function printPendingGrantInfo(grant, idp) {
|
|
|
3298
4404
|
console.log(' Tip: Approve as "timed" or "always" in the browser to let this');
|
|
3299
4405
|
console.log(" grant be reused on subsequent invocations without re-approval.");
|
|
3300
4406
|
}
|
|
3301
|
-
var runCommand =
|
|
4407
|
+
var runCommand = defineCommand30({
|
|
3302
4408
|
meta: {
|
|
3303
4409
|
name: "run",
|
|
3304
4410
|
description: "Execute a grant-secured command"
|
|
@@ -3387,7 +4493,7 @@ async function runShellMode(command, args) {
|
|
|
3387
4493
|
const adapterHandled = await tryAdapterModeFromShell(command, idp, args);
|
|
3388
4494
|
if (adapterHandled) return;
|
|
3389
4495
|
const grantsUrl = await getGrantsEndpoint(idp);
|
|
3390
|
-
const targetHost = args.host ||
|
|
4496
|
+
const targetHost = args.host || hostname4();
|
|
3391
4497
|
try {
|
|
3392
4498
|
const grants = await apiFetch(
|
|
3393
4499
|
`${grantsUrl}?requester=${encodeURIComponent(auth.email)}&status=approved&limit=20`
|
|
@@ -3401,7 +4507,7 @@ async function runShellMode(command, args) {
|
|
|
3401
4507
|
}
|
|
3402
4508
|
} catch {
|
|
3403
4509
|
}
|
|
3404
|
-
|
|
4510
|
+
consola26.info(`Requesting ape-shell session grant on ${targetHost}`);
|
|
3405
4511
|
const grant = await apiFetch(grantsUrl, {
|
|
3406
4512
|
method: "POST",
|
|
3407
4513
|
body: {
|
|
@@ -3421,8 +4527,8 @@ async function runShellMode(command, args) {
|
|
|
3421
4527
|
host: targetHost
|
|
3422
4528
|
});
|
|
3423
4529
|
if (shouldWaitForGrant(args)) {
|
|
3424
|
-
|
|
3425
|
-
|
|
4530
|
+
consola26.info(`Grant requested: ${grant.id}`);
|
|
4531
|
+
consola26.info("Waiting for approval...");
|
|
3426
4532
|
const maxWait = 3e5;
|
|
3427
4533
|
const interval = 3e3;
|
|
3428
4534
|
const start = Date.now();
|
|
@@ -3453,13 +4559,13 @@ async function tryAdapterModeFromShell(command, idp, args) {
|
|
|
3453
4559
|
try {
|
|
3454
4560
|
resolved = await resolveCommand(loaded, [normalizedExecutable, ...parsed.argv]);
|
|
3455
4561
|
} catch (err) {
|
|
3456
|
-
|
|
4562
|
+
consola26.debug(`ape-shell: adapter resolve failed for "${parsed.raw}":`, err);
|
|
3457
4563
|
return false;
|
|
3458
4564
|
}
|
|
3459
4565
|
try {
|
|
3460
4566
|
const existingGrantId = await findExistingGrant(resolved, idp);
|
|
3461
4567
|
if (existingGrantId) {
|
|
3462
|
-
|
|
4568
|
+
consola26.info(`Reusing grant ${existingGrantId} for: ${resolved.detail.display}`);
|
|
3463
4569
|
const token = await fetchGrantToken(idp, existingGrantId);
|
|
3464
4570
|
await verifyAndExecute(token, resolved, existingGrantId);
|
|
3465
4571
|
return true;
|
|
@@ -3467,7 +4573,7 @@ async function tryAdapterModeFromShell(command, idp, args) {
|
|
|
3467
4573
|
} catch {
|
|
3468
4574
|
}
|
|
3469
4575
|
const approval = args.approval ?? "once";
|
|
3470
|
-
|
|
4576
|
+
consola26.info(`Requesting grant for: ${resolved.detail.display}`);
|
|
3471
4577
|
const grant = await createShapesGrant(resolved, {
|
|
3472
4578
|
idp,
|
|
3473
4579
|
approval,
|
|
@@ -3475,19 +4581,19 @@ async function tryAdapterModeFromShell(command, idp, args) {
|
|
|
3475
4581
|
});
|
|
3476
4582
|
if (grant.similar_grants?.similar_grants?.length) {
|
|
3477
4583
|
const n = grant.similar_grants.similar_grants.length;
|
|
3478
|
-
|
|
3479
|
-
|
|
4584
|
+
consola26.info("");
|
|
4585
|
+
consola26.info(` Similar grant(s) found (${n}). Your approver can extend an existing grant to cover this request.`);
|
|
3480
4586
|
}
|
|
3481
4587
|
notifyGrantPending({
|
|
3482
4588
|
grantId: grant.id,
|
|
3483
4589
|
approveUrl: `${idp}/grant-approval?grant_id=${grant.id}`,
|
|
3484
4590
|
command: resolved.detail?.display || parsed?.raw || "unknown",
|
|
3485
4591
|
audience: resolved.adapter?.cli?.audience ?? "shapes",
|
|
3486
|
-
host: args.host ||
|
|
4592
|
+
host: args.host || hostname4()
|
|
3487
4593
|
});
|
|
3488
4594
|
if (shouldWaitForGrant(args)) {
|
|
3489
|
-
|
|
3490
|
-
|
|
4595
|
+
consola26.info(`Grant requested: ${grant.id}`);
|
|
4596
|
+
consola26.info(`Approve at: ${idp}/grant-approval?grant_id=${grant.id}`);
|
|
3491
4597
|
const status = await waitForGrantStatus(idp, grant.id);
|
|
3492
4598
|
if (status !== "approved")
|
|
3493
4599
|
throw new CliError(`Grant ${status}`);
|
|
@@ -3503,7 +4609,7 @@ function execShellCommand(command) {
|
|
|
3503
4609
|
throw new CliError("No command to execute");
|
|
3504
4610
|
try {
|
|
3505
4611
|
const { APES_SHELL_WRAPPER: _wrapperMarker, ...inheritedEnv } = process.env;
|
|
3506
|
-
|
|
4612
|
+
execFileSync10(command[0], command.slice(1), {
|
|
3507
4613
|
stdio: "inherit",
|
|
3508
4614
|
env: inheritedEnv
|
|
3509
4615
|
});
|
|
@@ -3561,7 +4667,7 @@ async function runAdapterMode(command, rawArgs, args) {
|
|
|
3561
4667
|
try {
|
|
3562
4668
|
const existingGrantId = await findExistingGrant(resolved, idp);
|
|
3563
4669
|
if (existingGrantId) {
|
|
3564
|
-
|
|
4670
|
+
consola26.info(`Reusing existing grant: ${existingGrantId}`);
|
|
3565
4671
|
const token = await fetchGrantToken(idp, existingGrantId);
|
|
3566
4672
|
await verifyAndExecute(token, resolved, existingGrantId);
|
|
3567
4673
|
return;
|
|
@@ -3575,17 +4681,17 @@ async function runAdapterMode(command, rawArgs, args) {
|
|
|
3575
4681
|
});
|
|
3576
4682
|
if (grant.similar_grants?.similar_grants?.length) {
|
|
3577
4683
|
const n = grant.similar_grants.similar_grants.length;
|
|
3578
|
-
|
|
3579
|
-
|
|
4684
|
+
consola26.info("");
|
|
4685
|
+
consola26.info(` Similar grant(s) found (${n}). Your approver can extend an existing grant to cover this request.`);
|
|
3580
4686
|
if (grant.similar_grants.widened_details?.length) {
|
|
3581
4687
|
const wider = grant.similar_grants.widened_details.map((d) => d.permission).join(", ");
|
|
3582
|
-
|
|
4688
|
+
consola26.info(` Broader scope: ${wider}`);
|
|
3583
4689
|
}
|
|
3584
|
-
|
|
4690
|
+
consola26.info("");
|
|
3585
4691
|
}
|
|
3586
4692
|
if (shouldWaitForGrant(args)) {
|
|
3587
|
-
|
|
3588
|
-
|
|
4693
|
+
consola26.info(`Grant requested: ${grant.id}`);
|
|
4694
|
+
consola26.info(`Approve at: ${idp}/grant-approval?grant_id=${grant.id}`);
|
|
3589
4695
|
const status = await waitForGrantStatus(idp, grant.id);
|
|
3590
4696
|
if (status !== "approved")
|
|
3591
4697
|
throw new Error(`Grant ${status}`);
|
|
@@ -3604,8 +4710,8 @@ async function runAudienceMode(audience, action, args) {
|
|
|
3604
4710
|
const idp = getIdpUrl(args.idp);
|
|
3605
4711
|
const grantsUrl = await getGrantsEndpoint(idp);
|
|
3606
4712
|
const command = action.split(" ");
|
|
3607
|
-
const targetHost = args.host ||
|
|
3608
|
-
|
|
4713
|
+
const targetHost = args.host || hostname4();
|
|
4714
|
+
consola26.info(`Requesting ${audience} grant on ${targetHost}: ${command.join(" ")}`);
|
|
3609
4715
|
const grant = await apiFetch(grantsUrl, {
|
|
3610
4716
|
method: "POST",
|
|
3611
4717
|
body: {
|
|
@@ -3622,9 +4728,9 @@ async function runAudienceMode(audience, action, args) {
|
|
|
3622
4728
|
printPendingGrantInfo(grant, idp);
|
|
3623
4729
|
throw new CliExit(getAsyncExitCode());
|
|
3624
4730
|
}
|
|
3625
|
-
|
|
3626
|
-
|
|
3627
|
-
|
|
4731
|
+
consola26.success(`Grant requested: ${grant.id}`);
|
|
4732
|
+
consola26.info(`Approve at: ${idp}/grant-approval?grant_id=${grant.id}`);
|
|
4733
|
+
consola26.info("Waiting for approval...");
|
|
3628
4734
|
const maxWait = 15 * 60 * 1e3;
|
|
3629
4735
|
const interval = 3e3;
|
|
3630
4736
|
const start = Date.now();
|
|
@@ -3632,7 +4738,7 @@ async function runAudienceMode(audience, action, args) {
|
|
|
3632
4738
|
while (Date.now() - start < maxWait) {
|
|
3633
4739
|
const status = await apiFetch(`${grantsUrl}/${grant.id}`);
|
|
3634
4740
|
if (status.status === "approved") {
|
|
3635
|
-
|
|
4741
|
+
consola26.success("Grant approved!");
|
|
3636
4742
|
approved = true;
|
|
3637
4743
|
break;
|
|
3638
4744
|
}
|
|
@@ -3647,15 +4753,15 @@ async function runAudienceMode(audience, action, args) {
|
|
|
3647
4753
|
`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.`
|
|
3648
4754
|
);
|
|
3649
4755
|
}
|
|
3650
|
-
|
|
4756
|
+
consola26.info("Fetching grant token...");
|
|
3651
4757
|
const { authz_jwt } = await apiFetch(`${grantsUrl}/${grant.id}/token`, {
|
|
3652
4758
|
method: "POST"
|
|
3653
4759
|
});
|
|
3654
4760
|
if (audience === "escapes") {
|
|
3655
|
-
|
|
4761
|
+
consola26.info(`Executing: ${command.join(" ")}`);
|
|
3656
4762
|
try {
|
|
3657
4763
|
const { APES_SHELL_WRAPPER: _wrapperMarker, ...inheritedEnv } = process.env;
|
|
3658
|
-
|
|
4764
|
+
execFileSync10(args["escapes-path"] || "escapes", ["--grant", authz_jwt, "--", ...command], {
|
|
3659
4765
|
stdio: "inherit",
|
|
3660
4766
|
env: inheritedEnv
|
|
3661
4767
|
});
|
|
@@ -3670,8 +4776,8 @@ async function runAudienceMode(audience, action, args) {
|
|
|
3670
4776
|
|
|
3671
4777
|
// src/commands/proxy.ts
|
|
3672
4778
|
import { spawn as spawn2 } from "child_process";
|
|
3673
|
-
import { defineCommand as
|
|
3674
|
-
import
|
|
4779
|
+
import { defineCommand as defineCommand31 } from "citty";
|
|
4780
|
+
import consola27 from "consola";
|
|
3675
4781
|
|
|
3676
4782
|
// src/proxy/config.ts
|
|
3677
4783
|
function buildDefaultProxyConfigToml(opts) {
|
|
@@ -3705,10 +4811,10 @@ note = "VPC-internal hostname suffix"
|
|
|
3705
4811
|
|
|
3706
4812
|
// src/proxy/local-proxy.ts
|
|
3707
4813
|
import { spawn } from "child_process";
|
|
3708
|
-
import { mkdtempSync as mkdtempSync3, rmSync as rmSync3, writeFileSync as
|
|
4814
|
+
import { mkdtempSync as mkdtempSync3, rmSync as rmSync3, writeFileSync as writeFileSync7 } from "fs";
|
|
3709
4815
|
import { createRequire } from "module";
|
|
3710
4816
|
import { tmpdir as tmpdir3 } from "os";
|
|
3711
|
-
import { dirname as
|
|
4817
|
+
import { dirname as dirname3, join as join9, resolve as resolve4 } from "path";
|
|
3712
4818
|
var require2 = createRequire(import.meta.url);
|
|
3713
4819
|
function findProxyBin() {
|
|
3714
4820
|
const pkgPath = require2.resolve("@openape/proxy/package.json");
|
|
@@ -3717,12 +4823,12 @@ function findProxyBin() {
|
|
|
3717
4823
|
if (!binRel) {
|
|
3718
4824
|
throw new Error("@openape/proxy is missing the openape-proxy bin entry");
|
|
3719
4825
|
}
|
|
3720
|
-
return
|
|
4826
|
+
return resolve4(dirname3(pkgPath), binRel);
|
|
3721
4827
|
}
|
|
3722
4828
|
async function startEphemeralProxy(configToml) {
|
|
3723
|
-
const tmpDir = mkdtempSync3(
|
|
3724
|
-
const configPath =
|
|
3725
|
-
|
|
4829
|
+
const tmpDir = mkdtempSync3(join9(tmpdir3(), "openape-proxy-"));
|
|
4830
|
+
const configPath = join9(tmpDir, "config.toml");
|
|
4831
|
+
writeFileSync7(configPath, configToml, { mode: 384 });
|
|
3726
4832
|
const binPath = findProxyBin();
|
|
3727
4833
|
const child = spawn(process.execPath, [binPath, "-c", configPath], {
|
|
3728
4834
|
stdio: ["ignore", "pipe", "pipe"],
|
|
@@ -3814,10 +4920,10 @@ function resolveProxyConfigOptions() {
|
|
|
3814
4920
|
77
|
|
3815
4921
|
);
|
|
3816
4922
|
}
|
|
3817
|
-
|
|
4923
|
+
consola27.info(`[apes proxy] IdP-mediated mode \u2014 agent=${auth.email}, idp=${auth.idp}`);
|
|
3818
4924
|
return { agentEmail: auth.email, idpUrl: auth.idp, mediated: true };
|
|
3819
4925
|
}
|
|
3820
|
-
var proxyCommand =
|
|
4926
|
+
var proxyCommand = defineCommand31({
|
|
3821
4927
|
meta: {
|
|
3822
4928
|
name: "proxy",
|
|
3823
4929
|
description: "Run a command with HTTPS_PROXY routed through the OpenApe egress proxy."
|
|
@@ -3839,12 +4945,12 @@ var proxyCommand = defineCommand28({
|
|
|
3839
4945
|
let close = null;
|
|
3840
4946
|
if (reuseUrl) {
|
|
3841
4947
|
proxyUrl = reuseUrl;
|
|
3842
|
-
|
|
4948
|
+
consola27.info(`[apes proxy] reusing existing proxy at ${proxyUrl}`);
|
|
3843
4949
|
} else {
|
|
3844
4950
|
const ephemeral = await startEphemeralProxy(buildDefaultProxyConfigToml(resolveProxyConfigOptions()));
|
|
3845
4951
|
proxyUrl = ephemeral.url;
|
|
3846
4952
|
close = ephemeral.close;
|
|
3847
|
-
|
|
4953
|
+
consola27.info(`[apes proxy] started ephemeral proxy at ${proxyUrl}`);
|
|
3848
4954
|
}
|
|
3849
4955
|
const noProxy = process.env.NO_PROXY ?? process.env.no_proxy ?? "127.0.0.1,localhost";
|
|
3850
4956
|
const childEnv = {
|
|
@@ -3876,7 +4982,7 @@ var proxyCommand = defineCommand28({
|
|
|
3876
4982
|
else resolveExit(code ?? 0);
|
|
3877
4983
|
});
|
|
3878
4984
|
child.once("error", (err) => {
|
|
3879
|
-
|
|
4985
|
+
consola27.error(`[apes proxy] failed to spawn '${wrapped[0]}':`, err.message);
|
|
3880
4986
|
resolveExit(127);
|
|
3881
4987
|
});
|
|
3882
4988
|
});
|
|
@@ -3890,8 +4996,8 @@ function signalNumber(signal) {
|
|
|
3890
4996
|
}
|
|
3891
4997
|
|
|
3892
4998
|
// src/commands/explain.ts
|
|
3893
|
-
import { defineCommand as
|
|
3894
|
-
var explainCommand =
|
|
4999
|
+
import { defineCommand as defineCommand32 } from "citty";
|
|
5000
|
+
var explainCommand = defineCommand32({
|
|
3895
5001
|
meta: {
|
|
3896
5002
|
name: "explain",
|
|
3897
5003
|
description: "Show what permission a command would need"
|
|
@@ -3929,9 +5035,9 @@ var explainCommand = defineCommand29({
|
|
|
3929
5035
|
});
|
|
3930
5036
|
|
|
3931
5037
|
// src/commands/config/get.ts
|
|
3932
|
-
import { defineCommand as
|
|
3933
|
-
import
|
|
3934
|
-
var configGetCommand =
|
|
5038
|
+
import { defineCommand as defineCommand33 } from "citty";
|
|
5039
|
+
import consola28 from "consola";
|
|
5040
|
+
var configGetCommand = defineCommand33({
|
|
3935
5041
|
meta: {
|
|
3936
5042
|
name: "get",
|
|
3937
5043
|
description: "Get a configuration value"
|
|
@@ -3951,7 +5057,7 @@ var configGetCommand = defineCommand30({
|
|
|
3951
5057
|
if (idp)
|
|
3952
5058
|
console.log(idp);
|
|
3953
5059
|
else
|
|
3954
|
-
|
|
5060
|
+
consola28.info("No IdP configured.");
|
|
3955
5061
|
break;
|
|
3956
5062
|
}
|
|
3957
5063
|
case "email": {
|
|
@@ -3959,7 +5065,7 @@ var configGetCommand = defineCommand30({
|
|
|
3959
5065
|
if (auth?.email)
|
|
3960
5066
|
console.log(auth.email);
|
|
3961
5067
|
else
|
|
3962
|
-
|
|
5068
|
+
consola28.info("Not logged in.");
|
|
3963
5069
|
break;
|
|
3964
5070
|
}
|
|
3965
5071
|
default: {
|
|
@@ -3972,7 +5078,7 @@ var configGetCommand = defineCommand30({
|
|
|
3972
5078
|
if (sectionObj && field in sectionObj) {
|
|
3973
5079
|
console.log(sectionObj[field]);
|
|
3974
5080
|
} else {
|
|
3975
|
-
|
|
5081
|
+
consola28.info(`Key "${key}" not set.`);
|
|
3976
5082
|
}
|
|
3977
5083
|
} else {
|
|
3978
5084
|
throw new CliError(`Unknown key: "${key}". Use: idp, email, defaults.idp, defaults.approval, agent.key, agent.email`);
|
|
@@ -3983,9 +5089,9 @@ var configGetCommand = defineCommand30({
|
|
|
3983
5089
|
});
|
|
3984
5090
|
|
|
3985
5091
|
// src/commands/config/set.ts
|
|
3986
|
-
import { defineCommand as
|
|
3987
|
-
import
|
|
3988
|
-
var configSetCommand =
|
|
5092
|
+
import { defineCommand as defineCommand34 } from "citty";
|
|
5093
|
+
import consola29 from "consola";
|
|
5094
|
+
var configSetCommand = defineCommand34({
|
|
3989
5095
|
meta: {
|
|
3990
5096
|
name: "set",
|
|
3991
5097
|
description: "Set a configuration value"
|
|
@@ -4021,12 +5127,12 @@ var configSetCommand = defineCommand31({
|
|
|
4021
5127
|
throw new CliError(`Unknown section: "${section}". Use: defaults, agent`);
|
|
4022
5128
|
}
|
|
4023
5129
|
saveConfig(config);
|
|
4024
|
-
|
|
5130
|
+
consola29.success(`Set ${key} = ${value}`);
|
|
4025
5131
|
}
|
|
4026
5132
|
});
|
|
4027
5133
|
|
|
4028
5134
|
// src/commands/fetch/index.ts
|
|
4029
|
-
import { defineCommand as
|
|
5135
|
+
import { defineCommand as defineCommand35 } from "citty";
|
|
4030
5136
|
async function doRequest(method, url, body, contentType, raw, showHeaders) {
|
|
4031
5137
|
const token = getAuthToken();
|
|
4032
5138
|
if (!token) {
|
|
@@ -4062,13 +5168,13 @@ async function doRequest(method, url, body, contentType, raw, showHeaders) {
|
|
|
4062
5168
|
throw new CliError(`HTTP ${response.status} ${response.statusText}`);
|
|
4063
5169
|
}
|
|
4064
5170
|
}
|
|
4065
|
-
var fetchCommand =
|
|
5171
|
+
var fetchCommand = defineCommand35({
|
|
4066
5172
|
meta: {
|
|
4067
5173
|
name: "fetch",
|
|
4068
5174
|
description: "Make authenticated HTTP requests"
|
|
4069
5175
|
},
|
|
4070
5176
|
subCommands: {
|
|
4071
|
-
get:
|
|
5177
|
+
get: defineCommand35({
|
|
4072
5178
|
meta: {
|
|
4073
5179
|
name: "get",
|
|
4074
5180
|
description: "GET request with auth token"
|
|
@@ -4094,7 +5200,7 @@ var fetchCommand = defineCommand32({
|
|
|
4094
5200
|
await doRequest("GET", String(args.url), void 0, "application/json", Boolean(args.raw), Boolean(args.headers));
|
|
4095
5201
|
}
|
|
4096
5202
|
}),
|
|
4097
|
-
post:
|
|
5203
|
+
post: defineCommand35({
|
|
4098
5204
|
meta: {
|
|
4099
5205
|
name: "post",
|
|
4100
5206
|
description: "POST request with auth token"
|
|
@@ -4133,8 +5239,8 @@ var fetchCommand = defineCommand32({
|
|
|
4133
5239
|
});
|
|
4134
5240
|
|
|
4135
5241
|
// src/commands/mcp/index.ts
|
|
4136
|
-
import { defineCommand as
|
|
4137
|
-
var mcpCommand =
|
|
5242
|
+
import { defineCommand as defineCommand36 } from "citty";
|
|
5243
|
+
var mcpCommand = defineCommand36({
|
|
4138
5244
|
meta: {
|
|
4139
5245
|
name: "mcp",
|
|
4140
5246
|
description: "Start MCP server for AI agents"
|
|
@@ -4157,48 +5263,48 @@ var mcpCommand = defineCommand33({
|
|
|
4157
5263
|
if (transport !== "stdio" && transport !== "sse") {
|
|
4158
5264
|
throw new Error('Transport must be "stdio" or "sse"');
|
|
4159
5265
|
}
|
|
4160
|
-
const { startMcpServer } = await import("./server-
|
|
5266
|
+
const { startMcpServer } = await import("./server-75ZP2KCO.js");
|
|
4161
5267
|
await startMcpServer(transport, port);
|
|
4162
5268
|
}
|
|
4163
5269
|
});
|
|
4164
5270
|
|
|
4165
5271
|
// src/commands/init/index.ts
|
|
4166
|
-
import { existsSync as
|
|
5272
|
+
import { existsSync as existsSync10, copyFileSync, writeFileSync as writeFileSync8 } from "fs";
|
|
4167
5273
|
import { randomBytes } from "crypto";
|
|
4168
|
-
import { execFileSync as
|
|
4169
|
-
import { join as
|
|
4170
|
-
import { defineCommand as
|
|
4171
|
-
import
|
|
5274
|
+
import { execFileSync as execFileSync11 } from "child_process";
|
|
5275
|
+
import { join as join10 } from "path";
|
|
5276
|
+
import { defineCommand as defineCommand37 } from "citty";
|
|
5277
|
+
import consola30 from "consola";
|
|
4172
5278
|
var DEFAULT_IDP_URL = "https://id.openape.at";
|
|
4173
5279
|
async function downloadTemplate(repo, targetDir) {
|
|
4174
5280
|
const { downloadTemplate: gigetDownload } = await import("giget");
|
|
4175
5281
|
await gigetDownload(`gh:${repo}`, { dir: targetDir, force: false });
|
|
4176
5282
|
}
|
|
4177
5283
|
function installDeps(dir) {
|
|
4178
|
-
const hasLockFile = (name) =>
|
|
5284
|
+
const hasLockFile = (name) => existsSync10(join10(dir, name));
|
|
4179
5285
|
if (hasLockFile("pnpm-lock.yaml")) {
|
|
4180
|
-
|
|
5286
|
+
execFileSync11("pnpm", ["install"], { cwd: dir, stdio: "inherit" });
|
|
4181
5287
|
} else if (hasLockFile("bun.lockb")) {
|
|
4182
|
-
|
|
5288
|
+
execFileSync11("bun", ["install"], { cwd: dir, stdio: "inherit" });
|
|
4183
5289
|
} else {
|
|
4184
|
-
|
|
5290
|
+
execFileSync11("npm", ["install"], { cwd: dir, stdio: "inherit" });
|
|
4185
5291
|
}
|
|
4186
5292
|
}
|
|
4187
5293
|
async function promptChoice(message, choices) {
|
|
4188
|
-
const result = await
|
|
5294
|
+
const result = await consola30.prompt(message, { type: "select", options: choices });
|
|
4189
5295
|
if (typeof result === "symbol") {
|
|
4190
5296
|
throw new CliExit(0);
|
|
4191
5297
|
}
|
|
4192
5298
|
return result;
|
|
4193
5299
|
}
|
|
4194
5300
|
async function promptText(message, defaultValue) {
|
|
4195
|
-
const result = await
|
|
5301
|
+
const result = await consola30.prompt(message, { type: "text", default: defaultValue, placeholder: defaultValue });
|
|
4196
5302
|
if (typeof result === "symbol") {
|
|
4197
5303
|
throw new CliExit(0);
|
|
4198
5304
|
}
|
|
4199
5305
|
return result || defaultValue || "";
|
|
4200
5306
|
}
|
|
4201
|
-
var initCommand =
|
|
5307
|
+
var initCommand = defineCommand37({
|
|
4202
5308
|
meta: {
|
|
4203
5309
|
name: "init",
|
|
4204
5310
|
description: "Scaffold a new OpenApe project"
|
|
@@ -4240,23 +5346,23 @@ var initCommand = defineCommand34({
|
|
|
4240
5346
|
});
|
|
4241
5347
|
async function initSP(targetDir) {
|
|
4242
5348
|
const dir = targetDir || "my-app";
|
|
4243
|
-
if (
|
|
5349
|
+
if (existsSync10(join10(dir, "package.json"))) {
|
|
4244
5350
|
throw new CliError(`Directory "${dir}" already contains a project.`);
|
|
4245
5351
|
}
|
|
4246
|
-
|
|
5352
|
+
consola30.start("Scaffolding SP starter...");
|
|
4247
5353
|
await downloadTemplate("openape-ai/openape-sp-starter", dir);
|
|
4248
|
-
|
|
4249
|
-
|
|
5354
|
+
consola30.success("Scaffolded from openape-sp-starter");
|
|
5355
|
+
consola30.start("Installing dependencies...");
|
|
4250
5356
|
installDeps(dir);
|
|
4251
|
-
|
|
4252
|
-
const envExample =
|
|
4253
|
-
const envFile =
|
|
4254
|
-
if (
|
|
5357
|
+
consola30.success("Dependencies installed");
|
|
5358
|
+
const envExample = join10(dir, ".env.example");
|
|
5359
|
+
const envFile = join10(dir, ".env");
|
|
5360
|
+
if (existsSync10(envExample) && !existsSync10(envFile)) {
|
|
4255
5361
|
copyFileSync(envExample, envFile);
|
|
4256
|
-
|
|
5362
|
+
consola30.success(`\`.env\` created (using Free IdP at ${DEFAULT_IDP_URL})`);
|
|
4257
5363
|
}
|
|
4258
5364
|
console.log("");
|
|
4259
|
-
|
|
5365
|
+
consola30.box([
|
|
4260
5366
|
`cd ${dir}`,
|
|
4261
5367
|
"npm run dev",
|
|
4262
5368
|
"",
|
|
@@ -4265,7 +5371,7 @@ async function initSP(targetDir) {
|
|
|
4265
5371
|
}
|
|
4266
5372
|
async function initIdP(targetDir) {
|
|
4267
5373
|
const dir = targetDir || "my-idp";
|
|
4268
|
-
if (
|
|
5374
|
+
if (existsSync10(join10(dir, "package.json"))) {
|
|
4269
5375
|
throw new CliError(`Directory "${dir}" already contains a project.`);
|
|
4270
5376
|
}
|
|
4271
5377
|
const domain = await promptText("Domain for the IdP", "localhost");
|
|
@@ -4275,15 +5381,15 @@ async function initIdP(targetDir) {
|
|
|
4275
5381
|
"s3 (S3-compatible)"
|
|
4276
5382
|
]);
|
|
4277
5383
|
const adminEmail = await promptText("Admin email");
|
|
4278
|
-
|
|
5384
|
+
consola30.start("Scaffolding IdP starter...");
|
|
4279
5385
|
await downloadTemplate("openape-ai/openape-idp-starter", dir);
|
|
4280
|
-
|
|
4281
|
-
|
|
5386
|
+
consola30.success("Scaffolded from openape-idp-starter");
|
|
5387
|
+
consola30.start("Installing dependencies...");
|
|
4282
5388
|
installDeps(dir);
|
|
4283
|
-
|
|
5389
|
+
consola30.success("Dependencies installed");
|
|
4284
5390
|
const sessionSecret = randomBytes(32).toString("hex");
|
|
4285
5391
|
const managementToken = randomBytes(32).toString("hex");
|
|
4286
|
-
|
|
5392
|
+
consola30.success("Secrets generated");
|
|
4287
5393
|
const isLocalhost = domain === "localhost";
|
|
4288
5394
|
const origin = isLocalhost ? "http://localhost:3000" : `https://${domain}`;
|
|
4289
5395
|
const envContent = [
|
|
@@ -4297,11 +5403,11 @@ async function initIdP(targetDir) {
|
|
|
4297
5403
|
`NUXT_OPENAPE_RP_ID=${domain}`,
|
|
4298
5404
|
`NUXT_OPENAPE_RP_ORIGIN=${origin}`
|
|
4299
5405
|
].join("\n");
|
|
4300
|
-
|
|
5406
|
+
writeFileSync8(join10(dir, ".env"), `${envContent}
|
|
4301
5407
|
`, { mode: 384 });
|
|
4302
|
-
|
|
5408
|
+
consola30.success(".env created");
|
|
4303
5409
|
console.log("");
|
|
4304
|
-
|
|
5410
|
+
consola30.box([
|
|
4305
5411
|
`cd ${dir}`,
|
|
4306
5412
|
"npm run dev",
|
|
4307
5413
|
"",
|
|
@@ -4318,11 +5424,11 @@ async function initIdP(targetDir) {
|
|
|
4318
5424
|
|
|
4319
5425
|
// src/commands/enroll.ts
|
|
4320
5426
|
import { Buffer as Buffer5 } from "buffer";
|
|
4321
|
-
import { existsSync as
|
|
5427
|
+
import { existsSync as existsSync11, readFileSync as readFileSync11 } from "fs";
|
|
4322
5428
|
import { execFile as execFile2 } from "child_process";
|
|
4323
5429
|
import { sign as sign2 } from "crypto";
|
|
4324
|
-
import { defineCommand as
|
|
4325
|
-
import
|
|
5430
|
+
import { defineCommand as defineCommand38 } from "citty";
|
|
5431
|
+
import consola31 from "consola";
|
|
4326
5432
|
var DEFAULT_IDP_URL2 = "https://id.openape.at";
|
|
4327
5433
|
var DEFAULT_KEY_PATH = "~/.ssh/id_ed25519";
|
|
4328
5434
|
var POLL_INTERVAL = 3e3;
|
|
@@ -4334,7 +5440,7 @@ function openBrowser2(url) {
|
|
|
4334
5440
|
}
|
|
4335
5441
|
async function pollForEnrollment(idp, agentEmail, keyPath) {
|
|
4336
5442
|
const resolvedKey = resolveKeyPath(keyPath);
|
|
4337
|
-
const keyContent =
|
|
5443
|
+
const keyContent = readFileSync11(resolvedKey, "utf-8");
|
|
4338
5444
|
const privateKey = loadEd25519PrivateKey(keyContent);
|
|
4339
5445
|
const challengeUrl = await getAgentChallengeEndpoint(idp);
|
|
4340
5446
|
const authenticateUrl = await getAgentAuthenticateEndpoint(idp);
|
|
@@ -4361,11 +5467,11 @@ async function pollForEnrollment(idp, agentEmail, keyPath) {
|
|
|
4361
5467
|
}
|
|
4362
5468
|
} catch {
|
|
4363
5469
|
}
|
|
4364
|
-
await new Promise((
|
|
5470
|
+
await new Promise((resolve5) => setTimeout(resolve5, POLL_INTERVAL));
|
|
4365
5471
|
}
|
|
4366
5472
|
throw new Error("Enrollment timed out. Please check the browser and try again.");
|
|
4367
5473
|
}
|
|
4368
|
-
var enrollCommand =
|
|
5474
|
+
var enrollCommand = defineCommand38({
|
|
4369
5475
|
meta: {
|
|
4370
5476
|
name: "enroll",
|
|
4371
5477
|
description: "Enroll an agent with an Identity Provider"
|
|
@@ -4385,38 +5491,38 @@ var enrollCommand = defineCommand35({
|
|
|
4385
5491
|
}
|
|
4386
5492
|
},
|
|
4387
5493
|
async run({ args }) {
|
|
4388
|
-
const idp = args.idp || await
|
|
5494
|
+
const idp = args.idp || await consola31.prompt("IdP URL", { type: "text", default: DEFAULT_IDP_URL2, placeholder: DEFAULT_IDP_URL2 }).then((r) => {
|
|
4389
5495
|
if (typeof r === "symbol") throw new CliExit(0);
|
|
4390
5496
|
return r;
|
|
4391
5497
|
}) || DEFAULT_IDP_URL2;
|
|
4392
|
-
const agentName = args.name || await
|
|
5498
|
+
const agentName = args.name || await consola31.prompt("Agent name", { type: "text", placeholder: "deploy-bot" }).then((r) => {
|
|
4393
5499
|
if (typeof r === "symbol") throw new CliExit(0);
|
|
4394
5500
|
return r;
|
|
4395
5501
|
});
|
|
4396
5502
|
if (!agentName) {
|
|
4397
5503
|
throw new CliError("Agent name is required.");
|
|
4398
5504
|
}
|
|
4399
|
-
const keyPath = args.key || await
|
|
5505
|
+
const keyPath = args.key || await consola31.prompt("Ed25519 key", { type: "text", default: DEFAULT_KEY_PATH, placeholder: DEFAULT_KEY_PATH }).then((r) => {
|
|
4400
5506
|
if (typeof r === "symbol") throw new CliExit(0);
|
|
4401
5507
|
return r;
|
|
4402
5508
|
}) || DEFAULT_KEY_PATH;
|
|
4403
5509
|
const resolvedKey = resolveKeyPath(keyPath);
|
|
4404
5510
|
let publicKey;
|
|
4405
|
-
if (
|
|
5511
|
+
if (existsSync11(resolvedKey)) {
|
|
4406
5512
|
publicKey = readPublicKey(resolvedKey);
|
|
4407
|
-
|
|
5513
|
+
consola31.success(`Using existing key ${keyPath}`);
|
|
4408
5514
|
} else {
|
|
4409
|
-
|
|
5515
|
+
consola31.start(`Generating Ed25519 key pair at ${keyPath}...`);
|
|
4410
5516
|
publicKey = generateAndSaveKey(keyPath);
|
|
4411
|
-
|
|
5517
|
+
consola31.success(`Key pair generated at ${keyPath}`);
|
|
4412
5518
|
}
|
|
4413
5519
|
const encodedKey = encodeURIComponent(publicKey);
|
|
4414
5520
|
const enrollUrl = `${idp}/enroll?name=${encodeURIComponent(agentName)}&key=${encodedKey}`;
|
|
4415
|
-
|
|
4416
|
-
|
|
5521
|
+
consola31.info("Opening browser for enrollment...");
|
|
5522
|
+
consola31.info(`\u2192 ${idp}/enroll`);
|
|
4417
5523
|
openBrowser2(enrollUrl);
|
|
4418
5524
|
console.log("");
|
|
4419
|
-
const agentEmail = await
|
|
5525
|
+
const agentEmail = await consola31.prompt(
|
|
4420
5526
|
"Agent email (shown in browser after enrollment)",
|
|
4421
5527
|
{ type: "text", placeholder: `agent+${agentName}@...` }
|
|
4422
5528
|
).then((r) => {
|
|
@@ -4426,7 +5532,7 @@ var enrollCommand = defineCommand35({
|
|
|
4426
5532
|
if (!agentEmail) {
|
|
4427
5533
|
throw new CliError("Agent email is required to verify enrollment.");
|
|
4428
5534
|
}
|
|
4429
|
-
|
|
5535
|
+
consola31.start("Verifying enrollment...");
|
|
4430
5536
|
const { token, expiresIn } = await pollForEnrollment(idp, agentEmail, keyPath);
|
|
4431
5537
|
saveAuth({
|
|
4432
5538
|
idp,
|
|
@@ -4438,18 +5544,18 @@ var enrollCommand = defineCommand35({
|
|
|
4438
5544
|
config.defaults = { ...config.defaults, idp };
|
|
4439
5545
|
config.agent = { key: keyPath, email: agentEmail };
|
|
4440
5546
|
saveConfig(config);
|
|
4441
|
-
|
|
4442
|
-
|
|
5547
|
+
consola31.success(`Agent enrolled as ${agentEmail}`);
|
|
5548
|
+
consola31.success("Config saved to ~/.config/apes/");
|
|
4443
5549
|
console.log("");
|
|
4444
|
-
|
|
5550
|
+
consola31.info("Verify with: apes whoami");
|
|
4445
5551
|
}
|
|
4446
5552
|
});
|
|
4447
5553
|
|
|
4448
5554
|
// src/commands/register-user.ts
|
|
4449
|
-
import { existsSync as
|
|
4450
|
-
import { defineCommand as
|
|
4451
|
-
import
|
|
4452
|
-
var registerUserCommand =
|
|
5555
|
+
import { existsSync as existsSync12, readFileSync as readFileSync12 } from "fs";
|
|
5556
|
+
import { defineCommand as defineCommand39 } from "citty";
|
|
5557
|
+
import consola32 from "consola";
|
|
5558
|
+
var registerUserCommand = defineCommand39({
|
|
4453
5559
|
meta: {
|
|
4454
5560
|
name: "register-user",
|
|
4455
5561
|
description: "Register a sub-user with SSH key"
|
|
@@ -4485,8 +5591,8 @@ var registerUserCommand = defineCommand36({
|
|
|
4485
5591
|
throw new CliError("No IdP URL configured. Run `apes login` first.");
|
|
4486
5592
|
}
|
|
4487
5593
|
let publicKey = args.key;
|
|
4488
|
-
if (
|
|
4489
|
-
publicKey =
|
|
5594
|
+
if (existsSync12(args.key)) {
|
|
5595
|
+
publicKey = readFileSync12(args.key, "utf-8").trim();
|
|
4490
5596
|
}
|
|
4491
5597
|
if (!publicKey.startsWith("ssh-ed25519 ")) {
|
|
4492
5598
|
throw new CliError("Public key must be in ssh-ed25519 format.");
|
|
@@ -4504,18 +5610,18 @@ var registerUserCommand = defineCommand36({
|
|
|
4504
5610
|
...userType ? { type: userType } : {}
|
|
4505
5611
|
}
|
|
4506
5612
|
});
|
|
4507
|
-
|
|
5613
|
+
consola32.success(`User registered: ${result.email} (type: ${result.type}, owner: ${result.owner})`);
|
|
4508
5614
|
}
|
|
4509
5615
|
});
|
|
4510
5616
|
|
|
4511
5617
|
// src/commands/utils/index.ts
|
|
4512
|
-
import { defineCommand as
|
|
5618
|
+
import { defineCommand as defineCommand41 } from "citty";
|
|
4513
5619
|
|
|
4514
5620
|
// src/commands/utils/dig.ts
|
|
4515
|
-
import { defineCommand as
|
|
4516
|
-
import
|
|
5621
|
+
import { defineCommand as defineCommand40 } from "citty";
|
|
5622
|
+
import consola33 from "consola";
|
|
4517
5623
|
import { resolveDDISA as resolveDDISA2 } from "@openape/core";
|
|
4518
|
-
var digCommand =
|
|
5624
|
+
var digCommand = defineCommand40({
|
|
4519
5625
|
meta: {
|
|
4520
5626
|
name: "dig",
|
|
4521
5627
|
description: "Resolve DDISA IdP for a domain or email (admin/diag tool)"
|
|
@@ -4588,12 +5694,12 @@ var digCommand = defineCommand37({
|
|
|
4588
5694
|
console.log(` domain: ${domain}`);
|
|
4589
5695
|
console.log("");
|
|
4590
5696
|
if (!result.ddisa.found) {
|
|
4591
|
-
|
|
5697
|
+
consola33.warn(`No DDISA record at _ddisa.${domain}`);
|
|
4592
5698
|
if (result.hint) console.log(`
|
|
4593
5699
|
${result.hint}`);
|
|
4594
5700
|
throw new CliError(`No DDISA record found for ${domain}`);
|
|
4595
5701
|
}
|
|
4596
|
-
|
|
5702
|
+
consola33.success(`_ddisa.${domain} \u2192 ${result.ddisa.idp}`);
|
|
4597
5703
|
console.log(` Version: ${result.ddisa.version || "ddisa1"}`);
|
|
4598
5704
|
console.log(` IdP URL: ${result.ddisa.idp}`);
|
|
4599
5705
|
if (result.ddisa.mode) console.log(` Mode: ${result.ddisa.mode}`);
|
|
@@ -4603,13 +5709,13 @@ ${result.hint}`);
|
|
|
4603
5709
|
return;
|
|
4604
5710
|
}
|
|
4605
5711
|
if (result.idpDiscovery.ok) {
|
|
4606
|
-
|
|
5712
|
+
consola33.success(`IdP reachable (${result.idpDiscovery.status ?? 200})`);
|
|
4607
5713
|
if (result.idpDiscovery.issuer) console.log(` Issuer: ${result.idpDiscovery.issuer}`);
|
|
4608
5714
|
if (result.idpDiscovery.ddisaVersion) console.log(` DDISA: v${result.idpDiscovery.ddisaVersion}`);
|
|
4609
5715
|
if (result.idpDiscovery.authMethods?.length) console.log(` Auth: ${result.idpDiscovery.authMethods.join(", ")}`);
|
|
4610
5716
|
if (result.idpDiscovery.grantTypes?.length) console.log(` Grants: ${result.idpDiscovery.grantTypes.join(", ")}`);
|
|
4611
5717
|
} else {
|
|
4612
|
-
|
|
5718
|
+
consola33.warn(`IdP discovery failed${result.idpDiscovery.status ? ` (HTTP ${result.idpDiscovery.status})` : ""}`);
|
|
4613
5719
|
if (result.hint) console.log(`
|
|
4614
5720
|
${result.hint}`);
|
|
4615
5721
|
throw new CliError(`IdP at ${result.ddisa.idp} not reachable`);
|
|
@@ -4618,7 +5724,7 @@ ${result.hint}`);
|
|
|
4618
5724
|
});
|
|
4619
5725
|
|
|
4620
5726
|
// src/commands/utils/index.ts
|
|
4621
|
-
var utilsCommand =
|
|
5727
|
+
var utilsCommand = defineCommand41({
|
|
4622
5728
|
meta: {
|
|
4623
5729
|
name: "utils",
|
|
4624
5730
|
description: "Admin/diagnostic utilities (dig, \u2026)"
|
|
@@ -4629,12 +5735,12 @@ var utilsCommand = defineCommand38({
|
|
|
4629
5735
|
});
|
|
4630
5736
|
|
|
4631
5737
|
// src/commands/sessions/index.ts
|
|
4632
|
-
import { defineCommand as
|
|
5738
|
+
import { defineCommand as defineCommand44 } from "citty";
|
|
4633
5739
|
|
|
4634
5740
|
// src/commands/sessions/list.ts
|
|
4635
|
-
import { defineCommand as
|
|
4636
|
-
import
|
|
4637
|
-
var sessionsListCommand =
|
|
5741
|
+
import { defineCommand as defineCommand42 } from "citty";
|
|
5742
|
+
import consola34 from "consola";
|
|
5743
|
+
var sessionsListCommand = defineCommand42({
|
|
4638
5744
|
meta: {
|
|
4639
5745
|
name: "list",
|
|
4640
5746
|
description: "List your active refresh-token families (one per logged-in device)."
|
|
@@ -4652,7 +5758,7 @@ var sessionsListCommand = defineCommand39({
|
|
|
4652
5758
|
return;
|
|
4653
5759
|
}
|
|
4654
5760
|
if (result.data.length === 0) {
|
|
4655
|
-
|
|
5761
|
+
consola34.info("No active sessions.");
|
|
4656
5762
|
return;
|
|
4657
5763
|
}
|
|
4658
5764
|
for (const f of result.data) {
|
|
@@ -4664,9 +5770,9 @@ var sessionsListCommand = defineCommand39({
|
|
|
4664
5770
|
});
|
|
4665
5771
|
|
|
4666
5772
|
// src/commands/sessions/remove.ts
|
|
4667
|
-
import { defineCommand as
|
|
4668
|
-
import
|
|
4669
|
-
var sessionsRemoveCommand =
|
|
5773
|
+
import { defineCommand as defineCommand43 } from "citty";
|
|
5774
|
+
import consola35 from "consola";
|
|
5775
|
+
var sessionsRemoveCommand = defineCommand43({
|
|
4670
5776
|
meta: {
|
|
4671
5777
|
name: "remove",
|
|
4672
5778
|
description: "Revoke one of your active refresh-token families by id."
|
|
@@ -4682,12 +5788,12 @@ var sessionsRemoveCommand = defineCommand40({
|
|
|
4682
5788
|
const id = String(args.familyId).trim();
|
|
4683
5789
|
if (!id) throw new CliError("familyId required");
|
|
4684
5790
|
await apiFetch(`/api/me/sessions/${encodeURIComponent(id)}`, { method: "DELETE" });
|
|
4685
|
-
|
|
5791
|
+
consola35.success(`Session ${id} revoked. The device using it will need to \`apes login\` again on its next refresh.`);
|
|
4686
5792
|
}
|
|
4687
5793
|
});
|
|
4688
5794
|
|
|
4689
5795
|
// src/commands/sessions/index.ts
|
|
4690
|
-
var sessionsCommand =
|
|
5796
|
+
var sessionsCommand = defineCommand44({
|
|
4691
5797
|
meta: {
|
|
4692
5798
|
name: "sessions",
|
|
4693
5799
|
description: "Manage your active refresh-token sessions across devices"
|
|
@@ -4699,10 +5805,10 @@ var sessionsCommand = defineCommand41({
|
|
|
4699
5805
|
});
|
|
4700
5806
|
|
|
4701
5807
|
// src/commands/dns-check.ts
|
|
4702
|
-
import { defineCommand as
|
|
4703
|
-
import
|
|
5808
|
+
import { defineCommand as defineCommand45 } from "citty";
|
|
5809
|
+
import consola36 from "consola";
|
|
4704
5810
|
import { resolveDDISA as resolveDDISA3 } from "@openape/core";
|
|
4705
|
-
var dnsCheckCommand =
|
|
5811
|
+
var dnsCheckCommand = defineCommand45({
|
|
4706
5812
|
meta: {
|
|
4707
5813
|
name: "dns-check",
|
|
4708
5814
|
description: "Validate DDISA DNS TXT records for a domain"
|
|
@@ -4716,7 +5822,7 @@ var dnsCheckCommand = defineCommand42({
|
|
|
4716
5822
|
},
|
|
4717
5823
|
async run({ args }) {
|
|
4718
5824
|
const domain = args.domain;
|
|
4719
|
-
|
|
5825
|
+
consola36.start(`Checking _ddisa.${domain}...`);
|
|
4720
5826
|
try {
|
|
4721
5827
|
const result = await resolveDDISA3(domain);
|
|
4722
5828
|
if (!result) {
|
|
@@ -4725,7 +5831,7 @@ var dnsCheckCommand = defineCommand42({
|
|
|
4725
5831
|
console.log(` _ddisa.${domain} TXT "v=ddisa1 idp=https://id.${domain}"`);
|
|
4726
5832
|
throw new CliError(`No DDISA record found for ${domain}`);
|
|
4727
5833
|
}
|
|
4728
|
-
|
|
5834
|
+
consola36.success(`_ddisa.${domain} \u2192 ${result.idp}`);
|
|
4729
5835
|
console.log("");
|
|
4730
5836
|
console.log(` Version: ${result.version || "ddisa1"}`);
|
|
4731
5837
|
console.log(` IdP URL: ${result.idp}`);
|
|
@@ -4734,14 +5840,14 @@ var dnsCheckCommand = defineCommand42({
|
|
|
4734
5840
|
if (result.priority !== void 0)
|
|
4735
5841
|
console.log(` Priority: ${result.priority}`);
|
|
4736
5842
|
console.log("");
|
|
4737
|
-
|
|
5843
|
+
consola36.start(`Verifying IdP at ${result.idp}...`);
|
|
4738
5844
|
const discoResp = await fetch(`${result.idp}/.well-known/openid-configuration`);
|
|
4739
5845
|
if (!discoResp.ok) {
|
|
4740
|
-
|
|
5846
|
+
consola36.warn(`IdP discovery failed (${discoResp.status}). Is the IdP running at ${result.idp}?`);
|
|
4741
5847
|
return;
|
|
4742
5848
|
}
|
|
4743
5849
|
const disco = await discoResp.json();
|
|
4744
|
-
|
|
5850
|
+
consola36.success(`IdP is reachable`);
|
|
4745
5851
|
console.log(` Issuer: ${disco.issuer}`);
|
|
4746
5852
|
console.log(` DDISA: v${disco.ddisa_version || "?"}`);
|
|
4747
5853
|
if (disco.ddisa_auth_methods_supported) {
|
|
@@ -4759,7 +5865,7 @@ var dnsCheckCommand = defineCommand42({
|
|
|
4759
5865
|
// src/commands/health.ts
|
|
4760
5866
|
import { exec } from "child_process";
|
|
4761
5867
|
import { promisify } from "util";
|
|
4762
|
-
import { defineCommand as
|
|
5868
|
+
import { defineCommand as defineCommand46 } from "citty";
|
|
4763
5869
|
var execAsync = promisify(exec);
|
|
4764
5870
|
async function resolveApeShellPath() {
|
|
4765
5871
|
try {
|
|
@@ -4795,7 +5901,7 @@ async function bestEffortGrantCount(idp) {
|
|
|
4795
5901
|
}
|
|
4796
5902
|
}
|
|
4797
5903
|
async function runHealth(args) {
|
|
4798
|
-
const version = true ? "0.
|
|
5904
|
+
const version = true ? "1.0.0" : "0.0.0";
|
|
4799
5905
|
const auth = loadAuth();
|
|
4800
5906
|
if (!auth) {
|
|
4801
5907
|
throw new CliError("Not logged in. Run `apes login` first.", 1);
|
|
@@ -4858,7 +5964,7 @@ async function runHealth(args) {
|
|
|
4858
5964
|
throw new CliError(`IdP ${auth.idp} unreachable: ${idpProbe.error}`, 1);
|
|
4859
5965
|
}
|
|
4860
5966
|
}
|
|
4861
|
-
var healthCommand =
|
|
5967
|
+
var healthCommand = defineCommand46({
|
|
4862
5968
|
meta: {
|
|
4863
5969
|
name: "health",
|
|
4864
5970
|
description: "Report CLI diagnostic state (auth, IdP, grants, binaries)"
|
|
@@ -4876,8 +5982,8 @@ var healthCommand = defineCommand43({
|
|
|
4876
5982
|
});
|
|
4877
5983
|
|
|
4878
5984
|
// src/commands/workflows.ts
|
|
4879
|
-
import { defineCommand as
|
|
4880
|
-
import
|
|
5985
|
+
import { defineCommand as defineCommand47 } from "citty";
|
|
5986
|
+
import consola37 from "consola";
|
|
4881
5987
|
|
|
4882
5988
|
// src/guides/index.ts
|
|
4883
5989
|
var guides = [
|
|
@@ -4927,7 +6033,7 @@ var guides = [
|
|
|
4927
6033
|
];
|
|
4928
6034
|
|
|
4929
6035
|
// src/commands/workflows.ts
|
|
4930
|
-
var workflowsCommand =
|
|
6036
|
+
var workflowsCommand = defineCommand47({
|
|
4931
6037
|
meta: {
|
|
4932
6038
|
name: "workflows",
|
|
4933
6039
|
description: "Discover workflow guides"
|
|
@@ -4948,7 +6054,7 @@ var workflowsCommand = defineCommand44({
|
|
|
4948
6054
|
if (args.id) {
|
|
4949
6055
|
const guide = guides.find((g) => g.id === String(args.id));
|
|
4950
6056
|
if (!guide) {
|
|
4951
|
-
|
|
6057
|
+
consola37.info(`Available: ${guides.map((g) => g.id).join(", ")}`);
|
|
4952
6058
|
throw new CliError(`Guide not found: ${args.id}`);
|
|
4953
6059
|
}
|
|
4954
6060
|
if (args.json) {
|
|
@@ -4988,26 +6094,26 @@ var workflowsCommand = defineCommand44({
|
|
|
4988
6094
|
});
|
|
4989
6095
|
|
|
4990
6096
|
// src/version-check.ts
|
|
4991
|
-
import { existsSync as
|
|
4992
|
-
import { homedir as
|
|
4993
|
-
import { join as
|
|
4994
|
-
import
|
|
6097
|
+
import { existsSync as existsSync13, mkdirSync as mkdirSync5, readFileSync as readFileSync13, writeFileSync as writeFileSync9 } from "fs";
|
|
6098
|
+
import { homedir as homedir11 } from "os";
|
|
6099
|
+
import { join as join11 } from "path";
|
|
6100
|
+
import consola38 from "consola";
|
|
4995
6101
|
var PACKAGE_NAME = "@openape/apes";
|
|
4996
6102
|
var CACHE_TTL_MS = 24 * 60 * 60 * 1e3;
|
|
4997
|
-
var CACHE_FILE =
|
|
6103
|
+
var CACHE_FILE = join11(homedir11(), ".config", "apes", ".version-check.json");
|
|
4998
6104
|
function readCache() {
|
|
4999
|
-
if (!
|
|
6105
|
+
if (!existsSync13(CACHE_FILE)) return null;
|
|
5000
6106
|
try {
|
|
5001
|
-
return JSON.parse(
|
|
6107
|
+
return JSON.parse(readFileSync13(CACHE_FILE, "utf-8"));
|
|
5002
6108
|
} catch {
|
|
5003
6109
|
return null;
|
|
5004
6110
|
}
|
|
5005
6111
|
}
|
|
5006
6112
|
function writeCache(entry) {
|
|
5007
6113
|
try {
|
|
5008
|
-
const dir =
|
|
5009
|
-
if (!
|
|
5010
|
-
|
|
6114
|
+
const dir = join11(homedir11(), ".config", "apes");
|
|
6115
|
+
if (!existsSync13(dir)) mkdirSync5(dir, { recursive: true, mode: 448 });
|
|
6116
|
+
writeFileSync9(CACHE_FILE, JSON.stringify(entry), { mode: 384 });
|
|
5011
6117
|
} catch {
|
|
5012
6118
|
}
|
|
5013
6119
|
}
|
|
@@ -5036,7 +6142,7 @@ async function fetchLatestVersion() {
|
|
|
5036
6142
|
}
|
|
5037
6143
|
function warnIfBehind(currentVersion, latest) {
|
|
5038
6144
|
if (compareSemver(currentVersion, latest) < 0) {
|
|
5039
|
-
|
|
6145
|
+
consola38.warn(
|
|
5040
6146
|
`apes ${currentVersion} is behind latest @openape/apes@${latest}. Run \`npm i -g @openape/apes@latest\` to update. (Suppress with APES_NO_UPDATE_CHECK=1.)`
|
|
5041
6147
|
);
|
|
5042
6148
|
}
|
|
@@ -5068,10 +6174,10 @@ if (shellRewrite) {
|
|
|
5068
6174
|
if (shellRewrite.action === "rewrite") {
|
|
5069
6175
|
process.argv = shellRewrite.argv;
|
|
5070
6176
|
} else if (shellRewrite.action === "version") {
|
|
5071
|
-
console.log(`ape-shell ${"0.
|
|
6177
|
+
console.log(`ape-shell ${"1.0.0"} (OpenApe DDISA shell wrapper)`);
|
|
5072
6178
|
process.exit(0);
|
|
5073
6179
|
} else if (shellRewrite.action === "help") {
|
|
5074
|
-
console.log(`ape-shell ${"0.
|
|
6180
|
+
console.log(`ape-shell ${"1.0.0"} \u2014 OpenApe DDISA shell wrapper`);
|
|
5075
6181
|
console.log("");
|
|
5076
6182
|
console.log("Usage:");
|
|
5077
6183
|
console.log(" ape-shell Start interactive grant-mediated REPL");
|
|
@@ -5095,7 +6201,7 @@ if (shellRewrite) {
|
|
|
5095
6201
|
}
|
|
5096
6202
|
}
|
|
5097
6203
|
var debug = process.argv.includes("--debug");
|
|
5098
|
-
var grantsCommand =
|
|
6204
|
+
var grantsCommand = defineCommand48({
|
|
5099
6205
|
meta: {
|
|
5100
6206
|
name: "grants",
|
|
5101
6207
|
description: "Grant management"
|
|
@@ -5116,7 +6222,7 @@ var grantsCommand = defineCommand45({
|
|
|
5116
6222
|
"delegation-revoke": delegationRevokeCommand
|
|
5117
6223
|
}
|
|
5118
6224
|
});
|
|
5119
|
-
var configCommand =
|
|
6225
|
+
var configCommand = defineCommand48({
|
|
5120
6226
|
meta: {
|
|
5121
6227
|
name: "config",
|
|
5122
6228
|
description: "Configuration management"
|
|
@@ -5126,10 +6232,10 @@ var configCommand = defineCommand45({
|
|
|
5126
6232
|
set: configSetCommand
|
|
5127
6233
|
}
|
|
5128
6234
|
});
|
|
5129
|
-
var main =
|
|
6235
|
+
var main = defineCommand48({
|
|
5130
6236
|
meta: {
|
|
5131
6237
|
name: "apes",
|
|
5132
|
-
version: "0.
|
|
6238
|
+
version: "1.0.0",
|
|
5133
6239
|
description: "Unified CLI for OpenApe"
|
|
5134
6240
|
},
|
|
5135
6241
|
subCommands: {
|
|
@@ -5184,20 +6290,20 @@ async function maybeRefreshAuth() {
|
|
|
5184
6290
|
}
|
|
5185
6291
|
}
|
|
5186
6292
|
await maybeRefreshAuth();
|
|
5187
|
-
await maybeWarnStaleVersion("0.
|
|
6293
|
+
await maybeWarnStaleVersion("1.0.0").catch(() => {
|
|
5188
6294
|
});
|
|
5189
6295
|
runMain(main).catch((err) => {
|
|
5190
6296
|
if (err instanceof CliExit) {
|
|
5191
6297
|
process.exit(err.exitCode);
|
|
5192
6298
|
}
|
|
5193
6299
|
if (err instanceof CliError) {
|
|
5194
|
-
|
|
6300
|
+
consola39.error(err.message);
|
|
5195
6301
|
process.exit(err.exitCode);
|
|
5196
6302
|
}
|
|
5197
6303
|
if (debug) {
|
|
5198
|
-
|
|
6304
|
+
consola39.error(err);
|
|
5199
6305
|
} else {
|
|
5200
|
-
|
|
6306
|
+
consola39.error(err instanceof ApiError ? err.message : err instanceof Error ? err.message : String(err));
|
|
5201
6307
|
}
|
|
5202
6308
|
process.exit(1);
|
|
5203
6309
|
});
|