@openape/ape-agent 2.8.12 → 2.8.13

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.
Files changed (2) hide show
  1. package/dist/bridge.mjs +228 -23
  2. package/package.json +4 -4
package/dist/bridge.mjs CHANGED
@@ -1053,6 +1053,7 @@ import { ofetch as ofetch2 } from "ofetch";
1053
1053
  import { Buffer as Buffer3 } from "buffer";
1054
1054
  import { createPrivateKey } from "crypto";
1055
1055
  import { ofetch as ofetch4 } from "ofetch";
1056
+ import { ofetch as ofetch5 } from "ofetch";
1056
1057
  function getConfigDir() {
1057
1058
  const override = process.env.OPENAPE_CLI_AUTH_HOME;
1058
1059
  if (override) return override;
@@ -1398,7 +1399,7 @@ import { decodeJwt } from "jose";
1398
1399
  import WebSocket from "ws";
1399
1400
 
1400
1401
  // src/chat-api.ts
1401
- import { ofetch as ofetch5 } from "ofetch";
1402
+ import { ofetch as ofetch6 } from "ofetch";
1402
1403
  var MAX_BODY = 1e4;
1403
1404
  var ChatApi = class {
1404
1405
  constructor(endpoint, bearer) {
@@ -1414,7 +1415,7 @@ var ChatApi = class {
1414
1415
  if (opts.replyTo) payload.reply_to = opts.replyTo;
1415
1416
  if (opts.threadId) payload.thread_id = opts.threadId;
1416
1417
  if (opts.streaming) payload.streaming = true;
1417
- const result = await ofetch5(url, {
1418
+ const result = await ofetch6(url, {
1418
1419
  method: "POST",
1419
1420
  headers: { Authorization: await this.bearer() },
1420
1421
  body: payload
@@ -1429,14 +1430,14 @@ var ChatApi = class {
1429
1430
  */
1430
1431
  async listMessages(roomId, threadId, limit = 50) {
1431
1432
  const url = `${this.endpoint}/api/rooms/${encodeURIComponent(roomId)}/messages?thread_id=${encodeURIComponent(threadId)}&limit=${limit}`;
1432
- return await ofetch5(url, {
1433
+ return await ofetch6(url, {
1433
1434
  method: "GET",
1434
1435
  headers: { Authorization: await this.bearer() }
1435
1436
  });
1436
1437
  }
1437
1438
  async requestContact(peerEmail) {
1438
1439
  const url = `${this.endpoint}/api/contacts`;
1439
- return await ofetch5(url, {
1440
+ return await ofetch6(url, {
1440
1441
  method: "POST",
1441
1442
  headers: { Authorization: await this.bearer() },
1442
1443
  body: { email: peerEmail }
@@ -1444,14 +1445,14 @@ var ChatApi = class {
1444
1445
  }
1445
1446
  async listContacts() {
1446
1447
  const url = `${this.endpoint}/api/contacts`;
1447
- return await ofetch5(url, {
1448
+ return await ofetch6(url, {
1448
1449
  method: "GET",
1449
1450
  headers: { Authorization: await this.bearer() }
1450
1451
  });
1451
1452
  }
1452
1453
  async acceptContact(peerEmail) {
1453
1454
  const url = `${this.endpoint}/api/contacts/${encodeURIComponent(peerEmail)}/accept`;
1454
- return await ofetch5(url, {
1455
+ return await ofetch6(url, {
1455
1456
  method: "POST",
1456
1457
  headers: { Authorization: await this.bearer() }
1457
1458
  });
@@ -1464,7 +1465,7 @@ var ChatApi = class {
1464
1465
  */
1465
1466
  async createThread(roomId, name) {
1466
1467
  const url = `${this.endpoint}/api/rooms/${encodeURIComponent(roomId)}/threads`;
1467
- return await ofetch5(url, {
1468
+ return await ofetch6(url, {
1468
1469
  method: "POST",
1469
1470
  headers: { Authorization: await this.bearer() },
1470
1471
  body: { name: name.slice(0, 100) }
@@ -1492,7 +1493,7 @@ var ChatApi = class {
1492
1493
  if (opts.streaming !== void 0) payload.streaming = opts.streaming;
1493
1494
  if (opts.streamingStatus !== void 0) payload.streaming_status = opts.streamingStatus;
1494
1495
  if (Object.keys(payload).length === 0) return;
1495
- await ofetch5(url, {
1496
+ await ofetch6(url, {
1496
1497
  method: "PATCH",
1497
1498
  headers: { Authorization: await this.bearer() },
1498
1499
  body: payload
@@ -1505,12 +1506,133 @@ function clamp(s2, max) {
1505
1506
  return `${s2.slice(0, max - 1)}\u2026`;
1506
1507
  }
1507
1508
 
1509
+ // src/troop-chat-api.ts
1510
+ import { ofetch as ofetch7 } from "ofetch";
1511
+ var MAX_BODY2 = 64 * 1024;
1512
+ var SYNTHETIC_THREAD_ID = "main";
1513
+ function asHistory(msg, agentEmail, ownerEmail) {
1514
+ return {
1515
+ id: msg.id,
1516
+ roomId: msg.chatId,
1517
+ threadId: SYNTHETIC_THREAD_ID,
1518
+ senderEmail: msg.role === "agent" ? agentEmail : ownerEmail,
1519
+ senderAct: msg.role,
1520
+ body: msg.body,
1521
+ replyTo: msg.replyTo,
1522
+ createdAt: msg.createdAt
1523
+ };
1524
+ }
1525
+ function asPosted(msg) {
1526
+ return {
1527
+ id: msg.id,
1528
+ roomId: msg.chatId,
1529
+ threadId: SYNTHETIC_THREAD_ID,
1530
+ body: msg.body,
1531
+ createdAt: msg.createdAt
1532
+ };
1533
+ }
1534
+ var TroopChatApi = class {
1535
+ constructor(endpoint, bearer) {
1536
+ this.endpoint = endpoint;
1537
+ this.bearer = bearer;
1538
+ }
1539
+ endpoint;
1540
+ bearer;
1541
+ bootstrap = null;
1542
+ /** Resolve + cache the agent's chat row (lazy fetch on first use). */
1543
+ async getBootstrap() {
1544
+ if (this.bootstrap) return this.bootstrap;
1545
+ this.bootstrap = await ofetch7(`${this.endpoint}/api/agents/me/chat`, {
1546
+ method: "GET",
1547
+ headers: { Authorization: await this.bearer() }
1548
+ });
1549
+ return this.bootstrap;
1550
+ }
1551
+ /** chat.id + (lazy-fetched) ownerEmail for the bridge's frame-translation path. */
1552
+ async getChatContext() {
1553
+ const b2 = await this.getBootstrap();
1554
+ return { chatId: b2.chat.id, ownerEmail: b2.chat.ownerEmail, agentEmail: b2.chat.agentEmail };
1555
+ }
1556
+ async postMessage(roomId, body, opts = {}) {
1557
+ void roomId;
1558
+ void opts.threadId;
1559
+ const payload = {
1560
+ body: body.length > MAX_BODY2 ? `${body.slice(0, MAX_BODY2 - 1)}\u2026` : body
1561
+ };
1562
+ if (opts.replyTo) payload.reply_to = opts.replyTo;
1563
+ if (opts.streaming) payload.streaming = true;
1564
+ const msg = await ofetch7(`${this.endpoint}/api/agents/me/chat/messages`, {
1565
+ method: "POST",
1566
+ headers: { Authorization: await this.bearer() },
1567
+ body: payload
1568
+ });
1569
+ return asPosted(msg);
1570
+ }
1571
+ async listMessages(roomId, threadId, limit = 50) {
1572
+ void roomId;
1573
+ void threadId;
1574
+ void limit;
1575
+ const fresh = await ofetch7(`${this.endpoint}/api/agents/me/chat`, {
1576
+ method: "GET",
1577
+ headers: { Authorization: await this.bearer() }
1578
+ });
1579
+ this.bootstrap = fresh;
1580
+ return fresh.messages.map((m2) => asHistory(m2, fresh.chat.agentEmail, fresh.chat.ownerEmail));
1581
+ }
1582
+ async patchMessage(messageId, opts = {}) {
1583
+ const payload = {};
1584
+ if (opts.body !== void 0) {
1585
+ payload.body = opts.body.length > MAX_BODY2 ? `${opts.body.slice(0, MAX_BODY2 - 1)}\u2026` : opts.body;
1586
+ }
1587
+ if (opts.streaming !== void 0) payload.streaming = opts.streaming;
1588
+ if (opts.streamingStatus !== void 0) payload.streaming_status = opts.streamingStatus;
1589
+ if (Object.keys(payload).length === 0) return;
1590
+ await ofetch7(`${this.endpoint}/api/agents/me/chat/messages/${encodeURIComponent(messageId)}`, {
1591
+ method: "PATCH",
1592
+ headers: { Authorization: await this.bearer() },
1593
+ body: payload
1594
+ });
1595
+ }
1596
+ /**
1597
+ * Troop's chat doesn't have contacts — synthesize a single
1598
+ * always-connected entry pointing at the owner so the bridge's
1599
+ * initial-contact + allowlist flows are no-ops.
1600
+ */
1601
+ async listContacts() {
1602
+ const b2 = await this.getBootstrap();
1603
+ return [{
1604
+ peerEmail: b2.chat.ownerEmail,
1605
+ myStatus: "accepted",
1606
+ theirStatus: "accepted",
1607
+ connected: true,
1608
+ roomId: b2.chat.id
1609
+ }];
1610
+ }
1611
+ async requestContact(peerEmail) {
1612
+ void peerEmail;
1613
+ return (await this.listContacts())[0];
1614
+ }
1615
+ async acceptContact(peerEmail) {
1616
+ void peerEmail;
1617
+ return (await this.listContacts())[0];
1618
+ }
1619
+ /**
1620
+ * Troop has no threads — return a synthetic one. The bridge's
1621
+ * cron-runner falls back to the main thread on createThread
1622
+ * failure already, so a stable "main" stand-in is the right shape.
1623
+ */
1624
+ async createThread(roomId, name) {
1625
+ void roomId;
1626
+ return { id: SYNTHETIC_THREAD_ID, name: name.slice(0, 100) };
1627
+ }
1628
+ };
1629
+
1508
1630
  // src/cron-runner.ts
1509
1631
  import { existsSync as existsSync4, mkdirSync as mkdirSync3, readdirSync as readdirSync3, readFileSync as readFileSync5, writeFileSync as writeFileSync3 } from "fs";
1510
1632
  import { homedir as homedir6 } from "os";
1511
1633
  import { join as join6 } from "path";
1512
1634
 
1513
- // ../../packages/apes/dist/chunk-OOKB2IL2.js
1635
+ // ../../packages/apes/dist/chunk-ZEUSCNCH.js
1514
1636
  import { spawn } from "child_process";
1515
1637
  import { mkdirSync as mkdirSync2, readFileSync as readFileSync3, writeFileSync as writeFileSync2 } from "fs";
1516
1638
  import { homedir as homedir3 } from "os";
@@ -1530,8 +1652,10 @@ function capStdio(s2) {
1530
1652
  [truncated to ${MAX_STDIO_BYTES} bytes]`;
1531
1653
  }
1532
1654
  function runApeShell(cmd, timeoutMs = DEFAULT_TIMEOUT_MS) {
1655
+ const bypass = process.env.OPENAPE_BYPASS_APE_SHELL === "1";
1656
+ const [execBin, execArgs] = bypass ? ["/bin/bash", ["-c", cmd]] : [BIN, ["-c", cmd]];
1533
1657
  return new Promise((resolveResult) => {
1534
- const child = spawn(BIN, ["-c", cmd], {
1658
+ const child = spawn(execBin, execArgs, {
1535
1659
  env: { ...process.env, APE_WAIT: "1" },
1536
1660
  stdio: ["ignore", "pipe", "pipe"]
1537
1661
  });
@@ -1566,7 +1690,7 @@ function runApeShell(cmd, timeoutMs = DEFAULT_TIMEOUT_MS) {
1566
1690
  stderr: "",
1567
1691
  exit_code: -1,
1568
1692
  error: spawnError.message,
1569
- hint: `Could not exec '${BIN}'. The agent host needs @openape/apes installed globally so ape-shell is on PATH.`
1693
+ hint: `Could not exec '${execBin}'. The agent host needs @openape/apes installed globally so ape-shell is on PATH (or set OPENAPE_BYPASS_APE_SHELL=1 to skip the gated shell entirely \u2014 meant for the OpenApe pod where the container IS the sandbox).`
1570
1694
  });
1571
1695
  return;
1572
1696
  }
@@ -2324,6 +2448,52 @@ function previewJson(value, max = 500) {
2324
2448
  }
2325
2449
  return s2.length > max ? `${s2.slice(0, max)}\u2026` : s2;
2326
2450
  }
2451
+ async function aggregateChatStream(res) {
2452
+ if (!res.body) throw new Error("LiteLLM streaming response had no body");
2453
+ const reader = res.body.getReader();
2454
+ const decoder = new TextDecoder();
2455
+ let buf = "";
2456
+ let content = "";
2457
+ const toolCalls = /* @__PURE__ */ new Map();
2458
+ let finishReason;
2459
+ while (true) {
2460
+ const { value, done } = await reader.read();
2461
+ if (done) break;
2462
+ buf += decoder.decode(value, { stream: true });
2463
+ while (true) {
2464
+ const nl = buf.indexOf("\n");
2465
+ if (nl === -1) break;
2466
+ const line = buf.slice(0, nl).trim();
2467
+ buf = buf.slice(nl + 1);
2468
+ if (!line.startsWith("data:")) continue;
2469
+ const payload = line.slice(5).trim();
2470
+ if (!payload || payload === "[DONE]") continue;
2471
+ let chunk;
2472
+ try {
2473
+ chunk = JSON.parse(payload);
2474
+ } catch {
2475
+ continue;
2476
+ }
2477
+ const ch0 = chunk.choices?.[0];
2478
+ const delta = ch0?.delta;
2479
+ if (delta?.content) content += delta.content;
2480
+ if (delta?.tool_calls) {
2481
+ for (const tc of delta.tool_calls) {
2482
+ const idx = tc.index ?? 0;
2483
+ const existing = toolCalls.get(idx) ?? { id: "", type: "function", function: { name: "", arguments: "" } };
2484
+ if (tc.id) existing.id = tc.id;
2485
+ if (tc.function?.name) existing.function.name = tc.function.name;
2486
+ if (tc.function?.arguments) existing.function.arguments += tc.function.arguments;
2487
+ toolCalls.set(idx, existing);
2488
+ }
2489
+ }
2490
+ if (ch0?.finish_reason) finishReason = ch0.finish_reason;
2491
+ }
2492
+ }
2493
+ const message = { role: "assistant", content: content || null };
2494
+ if (toolCalls.size > 0) message.tool_calls = Array.from(toolCalls.values());
2495
+ return { choices: [{ message, finish_reason: finishReason }] };
2496
+ }
2327
2497
  async function runLoop(opts) {
2328
2498
  const fetchFn = opts.fetchImpl ?? fetch;
2329
2499
  const trace = [];
@@ -2334,23 +2504,25 @@ async function runLoop(opts) {
2334
2504
  ];
2335
2505
  const tools = asOpenAiTools(opts.tools);
2336
2506
  for (let step = 1; step <= opts.maxSteps; step++) {
2507
+ const requestBody = {
2508
+ model: opts.config.model,
2509
+ messages,
2510
+ ...tools.length > 0 ? { tools, tool_choice: "auto" } : {},
2511
+ ...opts.streamAggregate ? { stream: true } : {}
2512
+ };
2337
2513
  const res = await fetchFn(`${opts.config.apiBase}/chat/completions`, {
2338
2514
  method: "POST",
2339
2515
  headers: {
2340
2516
  "authorization": `Bearer ${opts.config.apiKey}`,
2341
2517
  "content-type": "application/json"
2342
2518
  },
2343
- body: JSON.stringify({
2344
- model: opts.config.model,
2345
- messages,
2346
- ...tools.length > 0 ? { tools, tool_choice: "auto" } : {}
2347
- })
2519
+ body: JSON.stringify(requestBody)
2348
2520
  });
2349
2521
  if (!res.ok) {
2350
2522
  const text = await res.text().catch(() => "");
2351
2523
  throw new Error(`LiteLLM ${res.status}: ${text.slice(0, 500)}`);
2352
2524
  }
2353
- const data = await res.json();
2525
+ const data = opts.streamAggregate ? await aggregateChatStream(res) : await res.json();
2354
2526
  const choice = data.choices?.[0];
2355
2527
  if (!choice) throw new Error("LiteLLM response had no choices");
2356
2528
  const assistant = choice.message;
@@ -3975,7 +4147,7 @@ function extractOption(args, name) {
3975
4147
  }
3976
4148
  var AUTH_FILE2 = join5(homedir52(), ".config", "apes", "auth.json");
3977
4149
 
3978
- // ../../packages/apes/dist/chunk-4KPKANZT.js
4150
+ // ../../packages/apes/dist/chunk-NYJSBFLG.js
3979
4151
  init_chunk_OBF7IMQ2();
3980
4152
  var debug = process.argv.includes("--debug");
3981
4153
 
@@ -4794,6 +4966,8 @@ function readConfig() {
4794
4966
  "APE_CHAT_BRIDGE_MODEL is not set. Set it in the bridge .env (usually `~/Library/Application Support/openape/bridge/.env` on macOS) or globally in `~/litellm/.env` so resolveBridgeConfig picks it up at spawn time. Common values: `gpt-5.4` (ChatGPT-only LiteLLM proxy), `claude-haiku-4-5` (Anthropic-only)."
4795
4967
  );
4796
4968
  }
4969
+ const targetRaw = (process3.env.OPENAPE_BRIDGE_TARGET ?? "chat").toLowerCase();
4970
+ const target = targetRaw === "troop" ? "troop" : "chat";
4797
4971
  return {
4798
4972
  endpoint: (process3.env.APE_CHAT_ENDPOINT ?? DEFAULT_ENDPOINT).replace(/\/$/, ""),
4799
4973
  apesBin: process3.env.APE_CHAT_BRIDGE_APES_BIN ?? DEFAULT_APES_BIN,
@@ -4801,7 +4975,8 @@ function readConfig() {
4801
4975
  systemPrompt: process3.env.APE_CHAT_BRIDGE_SYSTEM_PROMPT ?? DEFAULT_SYSTEM_PROMPT,
4802
4976
  tools,
4803
4977
  maxSteps: Number.isFinite(maxSteps) && maxSteps > 0 ? maxSteps : DEFAULT_MAX_STEPS,
4804
- roomFilter: process3.env.APE_CHAT_BRIDGE_ROOM
4978
+ roomFilter: process3.env.APE_CHAT_BRIDGE_ROOM,
4979
+ target
4805
4980
  };
4806
4981
  }
4807
4982
  async function getIdentity() {
@@ -4837,7 +5012,7 @@ var Bridge = class {
4837
5012
  const idp = await ensureFreshIdpAuth();
4838
5013
  return `Bearer ${idp.access_token}`;
4839
5014
  };
4840
- this.chat = new ChatApi(this.cfg.endpoint, this.bearer);
5015
+ this.chat = this.cfg.target === "troop" ? new TroopChatApi(this.cfg.endpoint, this.bearer) : new ChatApi(this.cfg.endpoint, this.bearer);
4841
5016
  this.cron = new CronRunner({
4842
5017
  runtimeConfig: this.runtimeConfig(),
4843
5018
  chat: this.chat,
@@ -4855,6 +5030,11 @@ var Bridge = class {
4855
5030
  // its own message history and calls @openape/apes' runLoop directly
4856
5031
  // (no stdio JSON-RPC subprocess — see thread-session.ts).
4857
5032
  threads = /* @__PURE__ */ new Map();
5033
+ // ChatApi and TroopChatApi expose the same surface (postMessage /
5034
+ // listMessages / patchMessage / listContacts / requestContact /
5035
+ // acceptContact / createThread) so the rest of the bridge calls
5036
+ // through a structurally-typed reference without caring which
5037
+ // backend is in play. Picked at construction time from cfg.target.
4858
5038
  chat;
4859
5039
  bearer;
4860
5040
  cron;
@@ -4912,6 +5092,29 @@ var Bridge = class {
4912
5092
  if (accepted.length > 0) log(`accepted: ${accepted.join(", ")}`);
4913
5093
  if (skipped.length > 0) log(`skipped (not on allowlist): ${skipped.join(", ")}`);
4914
5094
  }
5095
+ /**
5096
+ * Translate troop's chat-frame payload shape into the
5097
+ * chat.openape.ai-style Message the rest of this bridge expects.
5098
+ * Troop's payload uses `role` (human|agent) + `chatId` + no
5099
+ * senderEmail; the bridge's handleInbound checks
5100
+ * `senderEmail === selfEmail` to skip its own echoes, so we
5101
+ * synthesize the email from role (agent → self, human → owner).
5102
+ * threadId is the synthetic 'main' because troop has no threads.
5103
+ */
5104
+ translateTroopPayload(chatId, payload) {
5105
+ const role = payload.role === "agent" ? "agent" : "human";
5106
+ return {
5107
+ id: String(payload.id ?? ""),
5108
+ roomId: chatId || String(payload.chatId ?? ""),
5109
+ threadId: "main",
5110
+ senderEmail: role === "agent" ? this.selfEmail : this.ownerEmail,
5111
+ senderAct: role,
5112
+ body: typeof payload.body === "string" ? payload.body : "",
5113
+ replyTo: typeof payload.replyTo === "string" ? payload.replyTo : null,
5114
+ createdAt: typeof payload.createdAt === "number" ? payload.createdAt : Math.floor(Date.now() / 1e3),
5115
+ editedAt: typeof payload.editedAt === "number" ? payload.editedAt : null
5116
+ };
5117
+ }
4915
5118
  async handleInbound(msg) {
4916
5119
  if (msg.senderEmail === this.selfEmail) return;
4917
5120
  if (!msg.body.trim()) return;
@@ -4977,7 +5180,8 @@ var Bridge = class {
4977
5180
  }
4978
5181
  async pumpOnce() {
4979
5182
  const bearer = await this.bearer();
4980
- const wsUrl = `${this.cfg.endpoint.replace(/^http/, "ws")}/api/ws?token=${encodeURIComponent(bearer.replace(/^Bearer\s+/i, ""))}`;
5183
+ const wsPath = this.cfg.target === "troop" ? "/_ws/chat" : "/api/ws";
5184
+ const wsUrl = `${this.cfg.endpoint.replace(/^http/, "ws")}${wsPath}?token=${encodeURIComponent(bearer.replace(/^Bearer\s+/i, ""))}`;
4981
5185
  const ws = new WebSocket(wsUrl);
4982
5186
  return new Promise((resolve4, reject) => {
4983
5187
  let pingTimer;
@@ -5006,8 +5210,9 @@ var Bridge = class {
5006
5210
  } catch {
5007
5211
  return;
5008
5212
  }
5009
- if (frame.type !== "message") return;
5010
- void this.handleInbound(frame.payload);
5213
+ if (frame.type !== "message" || !frame.payload) return;
5214
+ const msg = this.cfg.target === "troop" ? this.translateTroopPayload(frame.chat_id ?? "", frame.payload) : frame.payload;
5215
+ void this.handleInbound(msg);
5011
5216
  });
5012
5217
  ws.on("close", () => {
5013
5218
  if (pingTimer) clearInterval(pingTimer);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@openape/ape-agent",
3
- "version": "2.8.12",
3
+ "version": "2.8.13",
4
4
  "description": "OpenApe agent runtime: per-agent process that connects to chat.openape.ai, runs the LLM loop with tools + cron tasks, and streams replies back to owners.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -23,9 +23,9 @@
23
23
  "ofetch": "^1.4.1",
24
24
  "ws": "^8.18.0",
25
25
  "yaml": "^2.8.0",
26
- "@openape/apes": "1.28.11",
27
- "@openape/prompt-injection-detector": "0.1.0",
28
- "@openape/cli-auth": "0.4.1"
26
+ "@openape/apes": "1.28.12",
27
+ "@openape/cli-auth": "0.5.0",
28
+ "@openape/prompt-injection-detector": "0.1.0"
29
29
  },
30
30
  "devDependencies": {
31
31
  "@antfu/eslint-config": "^7.6.1",