@openape/ape-agent 2.8.15 → 2.9.1

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 +72 -206
  2. package/package.json +4 -4
package/dist/bridge.mjs CHANGED
@@ -1398,117 +1398,9 @@ function createHeuristicDetector() {
1398
1398
  import { decodeJwt } from "jose";
1399
1399
  import WebSocket from "ws";
1400
1400
 
1401
- // src/chat-api.ts
1402
- import { ofetch as ofetch6 } from "ofetch";
1403
- var MAX_BODY = 1e4;
1404
- var ChatApi = class {
1405
- constructor(endpoint, bearer) {
1406
- this.endpoint = endpoint;
1407
- this.bearer = bearer;
1408
- }
1409
- endpoint;
1410
- bearer;
1411
- async postMessage(roomId, body, opts = {}) {
1412
- const bodyForServer = opts.streaming ? body : clamp(body, MAX_BODY);
1413
- const url = `${this.endpoint}/api/rooms/${encodeURIComponent(roomId)}/messages`;
1414
- const payload = { body: bodyForServer };
1415
- if (opts.replyTo) payload.reply_to = opts.replyTo;
1416
- if (opts.threadId) payload.thread_id = opts.threadId;
1417
- if (opts.streaming) payload.streaming = true;
1418
- const result = await ofetch6(url, {
1419
- method: "POST",
1420
- headers: { Authorization: await this.bearer() },
1421
- body: payload
1422
- });
1423
- return result;
1424
- }
1425
- /**
1426
- * Fetch the most recent `limit` messages in a thread, oldest-first.
1427
- * Used by ThreadSession to seed `history` so the agent has the
1428
- * full conversation context after a bridge restart — otherwise it
1429
- * only sees messages that arrived via WS since the process boot.
1430
- */
1431
- async listMessages(roomId, threadId, limit = 50) {
1432
- const url = `${this.endpoint}/api/rooms/${encodeURIComponent(roomId)}/messages?thread_id=${encodeURIComponent(threadId)}&limit=${limit}`;
1433
- return await ofetch6(url, {
1434
- method: "GET",
1435
- headers: { Authorization: await this.bearer() }
1436
- });
1437
- }
1438
- async requestContact(peerEmail) {
1439
- const url = `${this.endpoint}/api/contacts`;
1440
- return await ofetch6(url, {
1441
- method: "POST",
1442
- headers: { Authorization: await this.bearer() },
1443
- body: { email: peerEmail }
1444
- });
1445
- }
1446
- async listContacts() {
1447
- const url = `${this.endpoint}/api/contacts`;
1448
- return await ofetch6(url, {
1449
- method: "GET",
1450
- headers: { Authorization: await this.bearer() }
1451
- });
1452
- }
1453
- async acceptContact(peerEmail) {
1454
- const url = `${this.endpoint}/api/contacts/${encodeURIComponent(peerEmail)}/accept`;
1455
- return await ofetch6(url, {
1456
- method: "POST",
1457
- headers: { Authorization: await this.bearer() }
1458
- });
1459
- }
1460
- /**
1461
- * Create a named thread in a room. Used by the cron-runner to drop
1462
- * each task's runs into its own thread so the chat sidebar shows
1463
- * one thread per task instead of all task DMs piling into the
1464
- * main thread.
1465
- */
1466
- async createThread(roomId, name) {
1467
- const url = `${this.endpoint}/api/rooms/${encodeURIComponent(roomId)}/threads`;
1468
- return await ofetch6(url, {
1469
- method: "POST",
1470
- headers: { Authorization: await this.bearer() },
1471
- body: { name: name.slice(0, 100) }
1472
- });
1473
- }
1474
- /**
1475
- * Update an in-flight or completed message. The server differentiates
1476
- * three modes via the message's current `streaming` state and the
1477
- * `streaming` field in this call:
1478
- *
1479
- * - Stream tick: pass `body` only (current accumulated text).
1480
- * Server keeps streaming=true and does NOT bump edited_at.
1481
- * - Stream end: pass `body` + `streaming: false`. Server clears
1482
- * the streaming flag and triggers the user-facing push.
1483
- * - Tool-call status: pass `streamingStatus` only (no body).
1484
- * Renders as "🔧 time.now" in the typing-subtitle.
1485
- * - Tool-call cleared: pass `streamingStatus: null`.
1486
- */
1487
- async patchMessage(messageId, opts = {}) {
1488
- const url = `${this.endpoint}/api/messages/${encodeURIComponent(messageId)}`;
1489
- const payload = {};
1490
- if (opts.body !== void 0) {
1491
- payload.body = opts.streaming === false && opts.body.trim().length === 0 ? clamp(opts.body, MAX_BODY) : opts.body.length <= MAX_BODY ? opts.body : `${opts.body.slice(0, MAX_BODY - 1)}\u2026`;
1492
- }
1493
- if (opts.streaming !== void 0) payload.streaming = opts.streaming;
1494
- if (opts.streamingStatus !== void 0) payload.streaming_status = opts.streamingStatus;
1495
- if (Object.keys(payload).length === 0) return;
1496
- await ofetch6(url, {
1497
- method: "PATCH",
1498
- headers: { Authorization: await this.bearer() },
1499
- body: payload
1500
- });
1501
- }
1502
- };
1503
- function clamp(s2, max) {
1504
- if (s2.trim().length === 0) return "\u2026";
1505
- if (s2.length <= max) return s2;
1506
- return `${s2.slice(0, max - 1)}\u2026`;
1507
- }
1508
-
1509
1401
  // src/troop-chat-api.ts
1510
- import { ofetch as ofetch7 } from "ofetch";
1511
- var MAX_BODY2 = 64 * 1024;
1402
+ import { ofetch as ofetch6 } from "ofetch";
1403
+ var MAX_BODY = 64 * 1024;
1512
1404
  var SYNTHETIC_THREAD_ID = "main";
1513
1405
  function asHistory(msg, agentEmail, ownerEmail) {
1514
1406
  return {
@@ -1542,7 +1434,7 @@ var TroopChatApi = class {
1542
1434
  /** Resolve + cache the agent's chat row (lazy fetch on first use). */
1543
1435
  async getBootstrap() {
1544
1436
  if (this.bootstrap) return this.bootstrap;
1545
- this.bootstrap = await ofetch7(`${this.endpoint}/api/agents/me/chat`, {
1437
+ this.bootstrap = await ofetch6(`${this.endpoint}/api/agents/me/chat`, {
1546
1438
  method: "GET",
1547
1439
  headers: { Authorization: await this.bearer() }
1548
1440
  });
@@ -1557,11 +1449,11 @@ var TroopChatApi = class {
1557
1449
  void roomId;
1558
1450
  void opts.threadId;
1559
1451
  const payload = {
1560
- body: body.length > MAX_BODY2 ? `${body.slice(0, MAX_BODY2 - 1)}\u2026` : body
1452
+ body: body.length > MAX_BODY ? `${body.slice(0, MAX_BODY - 1)}\u2026` : body
1561
1453
  };
1562
1454
  if (opts.replyTo) payload.reply_to = opts.replyTo;
1563
1455
  if (opts.streaming) payload.streaming = true;
1564
- const msg = await ofetch7(`${this.endpoint}/api/agents/me/chat/messages`, {
1456
+ const msg = await ofetch6(`${this.endpoint}/api/agents/me/chat/messages`, {
1565
1457
  method: "POST",
1566
1458
  headers: { Authorization: await this.bearer() },
1567
1459
  body: payload
@@ -1572,7 +1464,7 @@ var TroopChatApi = class {
1572
1464
  void roomId;
1573
1465
  void threadId;
1574
1466
  void limit;
1575
- const fresh = await ofetch7(`${this.endpoint}/api/agents/me/chat`, {
1467
+ const fresh = await ofetch6(`${this.endpoint}/api/agents/me/chat`, {
1576
1468
  method: "GET",
1577
1469
  headers: { Authorization: await this.bearer() }
1578
1470
  });
@@ -1582,12 +1474,12 @@ var TroopChatApi = class {
1582
1474
  async patchMessage(messageId, opts = {}) {
1583
1475
  const payload = {};
1584
1476
  if (opts.body !== void 0) {
1585
- payload.body = opts.body.length > MAX_BODY2 ? `${opts.body.slice(0, MAX_BODY2 - 1)}\u2026` : opts.body;
1477
+ payload.body = opts.body.length > MAX_BODY ? `${opts.body.slice(0, MAX_BODY - 1)}\u2026` : opts.body;
1586
1478
  }
1587
1479
  if (opts.streaming !== void 0) payload.streaming = opts.streaming;
1588
1480
  if (opts.streamingStatus !== void 0) payload.streaming_status = opts.streamingStatus;
1589
1481
  if (Object.keys(payload).length === 0) return;
1590
- await ofetch7(`${this.endpoint}/api/agents/me/chat/messages/${encodeURIComponent(messageId)}`, {
1482
+ await ofetch6(`${this.endpoint}/api/agents/me/chat/messages/${encodeURIComponent(messageId)}`, {
1591
1483
  method: "PATCH",
1592
1484
  headers: { Authorization: await this.bearer() },
1593
1485
  body: payload
@@ -1649,7 +1541,62 @@ import { basename, join as join22 } from "path";
1649
1541
 
1650
1542
  // ../../packages/core/dist/index.js
1651
1543
  import * as jose from "jose";
1544
+ import { lookup } from "dns/promises";
1545
+ import { isIP } from "net";
1652
1546
  var HKDF_INFO = new TextEncoder().encode("openape-sealed-box-v1");
1547
+ function isBlockedAddress(ip) {
1548
+ const fam = isIP(ip);
1549
+ if (fam === 4) {
1550
+ const o3 = ip.split(".");
1551
+ const a2 = Number(o3[0]);
1552
+ const b2 = Number(o3[1]);
1553
+ if (a2 === 0) return true;
1554
+ if (a2 === 127) return true;
1555
+ if (a2 === 10) return true;
1556
+ if (a2 === 172 && b2 >= 16 && b2 <= 31) return true;
1557
+ if (a2 === 192 && b2 === 168) return true;
1558
+ if (a2 === 169 && b2 === 254) return true;
1559
+ if (a2 === 100 && b2 >= 64 && b2 <= 127) return true;
1560
+ return false;
1561
+ }
1562
+ const low = ip.toLowerCase().replace(/^\[|\]$/g, "");
1563
+ if (low === "::" || low === "::1") return true;
1564
+ if (low.startsWith("fe80")) return true;
1565
+ if (low.startsWith("fc") || low.startsWith("fd")) return true;
1566
+ const mapped = low.match(/^::ffff:(\d{1,3}(?:\.\d{1,3}){3})$/);
1567
+ if (mapped) return isBlockedAddress(mapped[1]);
1568
+ return false;
1569
+ }
1570
+ async function assertPublicUrl(rawUrl, opts = {}) {
1571
+ let url;
1572
+ try {
1573
+ url = new URL(rawUrl);
1574
+ } catch {
1575
+ throw new Error(`Invalid URL: ${rawUrl}`);
1576
+ }
1577
+ const allowedSchemes = opts.allowHttp === true ? /* @__PURE__ */ new Set(["https:", "http:"]) : /* @__PURE__ */ new Set(["https:"]);
1578
+ if (!allowedSchemes.has(url.protocol)) {
1579
+ const expected = opts.allowHttp === true ? "http(s)" : "https";
1580
+ throw new Error(`URL must use ${expected}:// (got ${url.protocol}//) \u2014 ${rawUrl}`);
1581
+ }
1582
+ const host = url.hostname.replace(/^\[|\]$/g, "");
1583
+ const addresses = [];
1584
+ if (isIP(host)) {
1585
+ addresses.push(host);
1586
+ } else {
1587
+ const results = await lookup(host, { all: true });
1588
+ for (const r3 of results) addresses.push(r3.address);
1589
+ }
1590
+ if (addresses.length === 0) {
1591
+ throw new Error(`Host did not resolve: ${host}`);
1592
+ }
1593
+ for (const addr of addresses) {
1594
+ if (isBlockedAddress(addr)) {
1595
+ throw new Error(`Refusing to fetch a private/loopback address (${addr}) for ${rawUrl}`);
1596
+ }
1597
+ }
1598
+ return url;
1599
+ }
1653
1600
 
1654
1601
  // ../../packages/grants/dist/index.js
1655
1602
  function normalizeSelector(selector) {
@@ -3198,8 +3145,6 @@ import { dirname, normalize, resolve } from "path";
3198
3145
  import { homedir as homedir23 } from "os";
3199
3146
  import { resolve as resolve2 } from "path";
3200
3147
  import process2 from "process";
3201
- import { lookup } from "dns/promises";
3202
- import { isIP } from "net";
3203
3148
  import { execFileSync } from "child_process";
3204
3149
  import { execFileSync as execFileSync2 } from "child_process";
3205
3150
  var DEFAULT_TIMEOUT_MS = 5 * 60 * 1e3;
@@ -3692,59 +3637,10 @@ var gitWorktreeTools = [
3692
3637
  }
3693
3638
  }
3694
3639
  ];
3695
- function isBlockedAddress(ip) {
3696
- const fam = isIP(ip);
3697
- if (fam === 4) {
3698
- const o3 = ip.split(".");
3699
- const a2 = Number(o3[0]);
3700
- const b2 = Number(o3[1]);
3701
- if (a2 === 0) return true;
3702
- if (a2 === 127) return true;
3703
- if (a2 === 10) return true;
3704
- if (a2 === 172 && b2 >= 16 && b2 <= 31) return true;
3705
- if (a2 === 192 && b2 === 168) return true;
3706
- if (a2 === 169 && b2 === 254) return true;
3707
- if (a2 === 100 && b2 >= 64 && b2 <= 127) return true;
3708
- return false;
3709
- }
3710
- const low = ip.toLowerCase().replace(/^\[|\]$/g, "");
3711
- if (low === "::" || low === "::1") return true;
3712
- if (low.startsWith("fe80")) return true;
3713
- if (low.startsWith("fc") || low.startsWith("fd")) return true;
3714
- const mapped = low.match(/^::ffff:(\d{1,3}(?:\.\d{1,3}){3})$/);
3715
- if (mapped) return isBlockedAddress(mapped[1]);
3716
- return false;
3717
- }
3718
- async function assertPublicUrl(rawUrl) {
3719
- let url;
3720
- try {
3721
- url = new URL(rawUrl);
3722
- } catch {
3723
- throw new Error(`Invalid URL: ${rawUrl}`);
3724
- }
3725
- if (url.protocol !== "https:" && url.protocol !== "http:") {
3726
- throw new Error(`url must be http(s) (got ${url.protocol})`);
3727
- }
3728
- const host = url.hostname.replace(/^\[|\]$/g, "");
3729
- const addresses = [];
3730
- if (isIP(host)) {
3731
- addresses.push(host);
3732
- } else {
3733
- const results = await lookup(host, { all: true });
3734
- for (const r3 of results) addresses.push(r3.address);
3735
- }
3736
- if (addresses.length === 0) throw new Error(`Host did not resolve: ${host}`);
3737
- for (const addr of addresses) {
3738
- if (isBlockedAddress(addr)) {
3739
- throw new Error(`Refusing to fetch a private/loopback address (${addr}) for ${rawUrl}`);
3740
- }
3741
- }
3742
- return url;
3743
- }
3744
3640
  async function safeFetch(rawUrl, init2 = {}, maxRedirects = 5) {
3745
3641
  let current = rawUrl;
3746
3642
  for (let hop = 0; hop <= maxRedirects; hop += 1) {
3747
- await assertPublicUrl(current);
3643
+ await assertPublicUrl(current, { allowHttp: true });
3748
3644
  const res = await fetch(current, { ...init2, redirect: "manual" });
3749
3645
  if (res.status >= 300 && res.status < 400) {
3750
3646
  const location = res.headers.get("location");
@@ -5014,7 +4910,7 @@ function resolveTools(envFallback) {
5014
4910
  }
5015
4911
  return envFallback;
5016
4912
  }
5017
- var DEFAULT_ENDPOINT = "https://chat.openape.ai";
4913
+ var DEFAULT_ENDPOINT = "https://troop.openape.ai";
5018
4914
  var DEFAULT_APES_BIN = "apes";
5019
4915
  var DEFAULT_MAX_STEPS = 10;
5020
4916
  var DEFAULT_SYSTEM_PROMPT = `You are a helpful assistant in a 1:1 chat. Be concise and friendly. When asked for facts, say "I don't know" rather than guess.`;
@@ -5022,28 +4918,7 @@ var PING_INTERVAL_MS = 3e4;
5022
4918
  var RECONNECT_BASE_MS = 1e3;
5023
4919
  var RECONNECT_MAX_MS = 3e4;
5024
4920
  var ALLOWLIST_POLL_INTERVAL_MS = 3e4;
5025
- function loadBridgeEnvFile() {
5026
- const path = join8(homedir9(), "Library", "Application Support", "openape", "bridge", ".env");
5027
- if (!existsSync6(path)) return;
5028
- try {
5029
- const raw = readFileSync8(path, "utf8");
5030
- for (const line of raw.split(/\r?\n/)) {
5031
- const trimmed = line.trim();
5032
- if (!trimmed || trimmed.startsWith("#")) continue;
5033
- const eq = trimmed.indexOf("=");
5034
- if (eq < 0) continue;
5035
- const key = trimmed.slice(0, eq).trim();
5036
- const value = trimmed.slice(eq + 1).trim().replace(/^["']|["']$/g, "");
5037
- if (!key) continue;
5038
- if (process3.env[key] === void 0) {
5039
- process3.env[key] = value;
5040
- }
5041
- }
5042
- } catch {
5043
- }
5044
- }
5045
4921
  function readConfig() {
5046
- loadBridgeEnvFile();
5047
4922
  const toolsRaw = process3.env.APE_CHAT_BRIDGE_TOOLS ?? "";
5048
4923
  const tools = toolsRaw.split(",").map((s2) => s2.trim()).filter(Boolean);
5049
4924
  const maxStepsRaw = process3.env.APE_CHAT_BRIDGE_MAX_STEPS;
@@ -5051,20 +4926,17 @@ function readConfig() {
5051
4926
  const model = process3.env.APE_CHAT_BRIDGE_MODEL;
5052
4927
  if (!model) {
5053
4928
  throw new Error(
5054
- "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)."
4929
+ "APE_CHAT_BRIDGE_MODEL is not set. Set it in the container env (compose environment: block) or globally in `~/litellm/.env`. Common values: `gpt-5.4` (ChatGPT-only LiteLLM proxy), `claude-haiku-4-5` (Anthropic-only)."
5055
4930
  );
5056
4931
  }
5057
- const targetRaw = (process3.env.OPENAPE_BRIDGE_TARGET ?? "chat").toLowerCase();
5058
- const target = targetRaw === "troop" ? "troop" : "chat";
5059
4932
  return {
5060
- endpoint: (process3.env.APE_CHAT_ENDPOINT ?? DEFAULT_ENDPOINT).replace(/\/$/, ""),
4933
+ endpoint: (process3.env.OPENAPE_TROOP_URL ?? DEFAULT_ENDPOINT).replace(/\/$/, ""),
5061
4934
  apesBin: process3.env.APE_CHAT_BRIDGE_APES_BIN ?? DEFAULT_APES_BIN,
5062
4935
  model,
5063
4936
  systemPrompt: process3.env.APE_CHAT_BRIDGE_SYSTEM_PROMPT ?? DEFAULT_SYSTEM_PROMPT,
5064
4937
  tools,
5065
4938
  maxSteps: Number.isFinite(maxSteps) && maxSteps > 0 ? maxSteps : DEFAULT_MAX_STEPS,
5066
- roomFilter: process3.env.APE_CHAT_BRIDGE_ROOM,
5067
- target
4939
+ roomFilter: process3.env.APE_CHAT_BRIDGE_ROOM
5068
4940
  };
5069
4941
  }
5070
4942
  async function getIdentity() {
@@ -5100,13 +4972,13 @@ var Bridge = class {
5100
4972
  const idp = await ensureFreshIdpAuth();
5101
4973
  return `Bearer ${idp.access_token}`;
5102
4974
  };
5103
- this.chat = this.cfg.target === "troop" ? new TroopChatApi(this.cfg.endpoint, this.bearer) : new ChatApi(this.cfg.endpoint, this.bearer);
4975
+ this.chat = new TroopChatApi(this.cfg.endpoint, this.bearer);
5104
4976
  this.cron = new CronRunner({
5105
4977
  runtimeConfig: this.runtimeConfig(),
5106
4978
  chat: this.chat,
5107
4979
  ownerEmail: this.ownerEmail,
5108
4980
  log,
5109
- troopUrl: (process3.env.OPENAPE_TROOP_URL ?? "https://troop.openape.ai").replace(/\/$/, ""),
4981
+ troopUrl: this.cfg.endpoint,
5110
4982
  bearer: this.bearer
5111
4983
  });
5112
4984
  this.cron.start();
@@ -5118,11 +4990,6 @@ var Bridge = class {
5118
4990
  // its own message history and calls @openape/apes' runLoop directly
5119
4991
  // (no stdio JSON-RPC subprocess — see thread-session.ts).
5120
4992
  threads = /* @__PURE__ */ new Map();
5121
- // ChatApi and TroopChatApi expose the same surface (postMessage /
5122
- // listMessages / patchMessage / listContacts / requestContact /
5123
- // acceptContact / createThread) so the rest of the bridge calls
5124
- // through a structurally-typed reference without caring which
5125
- // backend is in play. Picked at construction time from cfg.target.
5126
4993
  chat;
5127
4994
  bearer;
5128
4995
  cron;
@@ -5268,8 +5135,7 @@ var Bridge = class {
5268
5135
  }
5269
5136
  async pumpOnce() {
5270
5137
  const bearer = await this.bearer();
5271
- const wsPath = this.cfg.target === "troop" ? "/_ws/chat" : "/api/ws";
5272
- const wsUrl = `${this.cfg.endpoint.replace(/^http/, "ws")}${wsPath}?token=${encodeURIComponent(bearer.replace(/^Bearer\s+/i, ""))}`;
5138
+ const wsUrl = `${this.cfg.endpoint.replace(/^http/, "ws")}/_ws/chat?token=${encodeURIComponent(bearer.replace(/^Bearer\s+/i, ""))}`;
5273
5139
  const ws = new WebSocket(wsUrl);
5274
5140
  return new Promise((resolve4, reject) => {
5275
5141
  let pingTimer;
@@ -5299,7 +5165,7 @@ var Bridge = class {
5299
5165
  return;
5300
5166
  }
5301
5167
  if (frame.type !== "message" || !frame.payload) return;
5302
- const msg = this.cfg.target === "troop" ? this.translateTroopPayload(frame.chat_id ?? "", frame.payload) : frame.payload;
5168
+ const msg = this.translateTroopPayload(frame.chat_id ?? "", frame.payload);
5303
5169
  void this.handleInbound(msg);
5304
5170
  });
5305
5171
  ws.on("close", () => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@openape/ape-agent",
3
- "version": "2.8.15",
3
+ "version": "2.9.1",
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.29.0",
27
- "@openape/prompt-injection-detector": "0.1.0",
28
- "@openape/cli-auth": "0.5.0"
26
+ "@openape/apes": "1.29.1",
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",