@snowyroad/arp 0.3.2 → 0.3.4

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/cli.js +296 -86
  2. package/package.json +10 -7
package/dist/cli.js CHANGED
@@ -251,6 +251,46 @@ var RebootstrapError = class extends Error {
251
251
  this.name = "RebootstrapError";
252
252
  }
253
253
  };
254
+ var MAX_TOKEN_LENGTH = 8192;
255
+ var MAX_TOKEN_TTL_S = 48 * 3600;
256
+ var CLOCK_SKEW_S = 300;
257
+ function decodeJwtClaims(token) {
258
+ const parts = token.split(".");
259
+ if (parts.length !== 3) return null;
260
+ try {
261
+ const header = JSON.parse(Buffer.from(parts[0], "base64url").toString("utf8"));
262
+ if (typeof header !== "object" || header === null || Array.isArray(header)) return null;
263
+ const alg = header.alg;
264
+ if (typeof alg !== "string" || alg.toLowerCase() === "none") return null;
265
+ const payload = JSON.parse(Buffer.from(parts[1], "base64url").toString("utf8"));
266
+ if (typeof payload !== "object" || payload === null || Array.isArray(payload)) return null;
267
+ return payload;
268
+ } catch {
269
+ return null;
270
+ }
271
+ }
272
+ function validateReplacementToken(newToken, currentToken, nowMs = Date.now()) {
273
+ if (newToken.length > MAX_TOKEN_LENGTH) return { ok: false, reason: "token too large" };
274
+ const claims = decodeJwtClaims(newToken);
275
+ if (!claims) return { ok: false, reason: "not a well-formed JWT" };
276
+ const exp = claims.exp;
277
+ if (typeof exp !== "number" || !Number.isFinite(exp)) {
278
+ return { ok: false, reason: "missing or non-numeric exp claim" };
279
+ }
280
+ const nowS = nowMs / 1e3;
281
+ if (exp <= nowS - CLOCK_SKEW_S) return { ok: false, reason: "already expired" };
282
+ if (exp > nowS + MAX_TOKEN_TTL_S) return { ok: false, reason: "expiry implausibly far in the future" };
283
+ const current = decodeJwtClaims(currentToken);
284
+ if (current) {
285
+ if (typeof current.sub === "string" && typeof claims.sub === "string" && current.sub !== claims.sub) {
286
+ return { ok: false, reason: "subject (sub) does not match the current token" };
287
+ }
288
+ if (typeof current.iss === "string" && typeof claims.iss === "string" && current.iss !== claims.iss) {
289
+ return { ok: false, reason: "issuer (iss) does not match the current token" };
290
+ }
291
+ }
292
+ return { ok: true };
293
+ }
254
294
  async function mintAccessToken(relayHttpUrl, agentKey, fetchFn = fetch) {
255
295
  const res = await fetchFn(`${relayHttpUrl.replace(/\/$/, "")}/agents/token`, {
256
296
  method: "POST",
@@ -640,6 +680,33 @@ function redactConfig(cfg) {
640
680
  import { randomUUID } from "crypto";
641
681
 
642
682
  // src/untrusted.ts
683
+ function untrusted(s) {
684
+ return s;
685
+ }
686
+ function rawUntrusted(u) {
687
+ return u;
688
+ }
689
+ function utext(strings, ...values) {
690
+ let out = strings[0];
691
+ for (let i = 0; i < values.length; i++) out += String(values[i]) + strings[i + 1];
692
+ return out;
693
+ }
694
+ function joinUntrusted(parts, sep2) {
695
+ return parts.join(sep2);
696
+ }
697
+ function firstNonEmpty(parts, fallback) {
698
+ for (const p of parts) if (p !== "") return p;
699
+ return fallback;
700
+ }
701
+ function hasText(u) {
702
+ return u !== "";
703
+ }
704
+ function isBlankText(u) {
705
+ return u.trim() === "";
706
+ }
707
+ function sameText(u, s) {
708
+ return u === s;
709
+ }
643
710
  var MARKER_RE = /<<<(?=[\s\u200B-\u200D\u2060\uFEFF]*(?:END[\s\u200B-\u200D\u2060\uFEFF]+)?UNTRUSTED)/gi;
644
711
  function neutralizeMarkers(content) {
645
712
  return content.replace(MARKER_RE, "<<\\<");
@@ -650,7 +717,7 @@ function sanitizeLabel(label) {
650
717
  function fence(label, content) {
651
718
  const l = sanitizeLabel(label);
652
719
  return `<<<UNTRUSTED ${l}>>>
653
- ${neutralizeMarkers(content)}
720
+ ${neutralizeMarkers(rawUntrusted(content))}
654
721
  <<<END UNTRUSTED ${l}>>>`;
655
722
  }
656
723
  function untrustedPreamble(mode) {
@@ -734,24 +801,24 @@ function normalizeRosterEntry(name, memberDescription, card) {
734
801
  if (typeof card.description === "string" && card.description) description = card.description;
735
802
  if (Array.isArray(card.skills)) skills = card.skills.map((s) => s && typeof s.name === "string" ? s.name : "").filter(Boolean);
736
803
  }
737
- return { name, description, skills };
804
+ return { name: untrusted(name), description: untrusted(description), skills: skills.map(untrusted) };
738
805
  }
739
806
  function assembleRosterFacts(entries, selfName) {
740
- const peers = entries.filter((e) => e.name !== selfName);
807
+ const peers = entries.filter((e) => !sameText(e.name, selfName));
741
808
  if (peers.length === 0) return "";
742
809
  const lines = peers.map((p) => {
743
- const desc = p.description ? `: ${p.description}` : "";
744
- const skills = p.skills.length ? ` [skills: ${p.skills.join(", ")}]` : "";
745
- return `- ${p.name}${desc}${skills}`;
810
+ const desc = hasText(p.description) ? utext`: ${p.description}` : "";
811
+ const skills = p.skills.length ? utext` [skills: ${joinUntrusted(p.skills, ", ")}]` : "";
812
+ return utext`- ${p.name}${desc}${skills}`;
746
813
  });
747
814
  return `Also in this channel:
748
- ${fence("peer roster", lines.join("\n"))}`;
815
+ ${fence("peer roster", joinUntrusted(lines, "\n"))}`;
749
816
  }
750
817
 
751
818
  // src/channelContext.ts
752
819
  function buildChannelContext(input) {
753
820
  let out = "";
754
- if (input.memory.trim()) {
821
+ if (!isBlankText(input.memory)) {
755
822
  out += `## Channel Memory (shared context for this channel)
756
823
  ${fence("channel memory", input.memory)}
757
824
  ---
@@ -759,8 +826,7 @@ ${fence("channel memory", input.memory)}
759
826
  `;
760
827
  }
761
828
  if (input.pins.length > 0) {
762
- const sections = input.pins.map((p) => fence("pinned file", `\u{1F4CC} ${p.label}
763
- ${p.content}`));
829
+ const sections = input.pins.map((p) => fence("pinned file", utext`📌 ${p.label}\n${p.content}`));
764
830
  out += `## Pinned Files (from GitHub)
765
831
  ${sections.join("\n\n")}
766
832
  ---
@@ -770,10 +836,10 @@ ${sections.join("\n\n")}
770
836
  if (input.topics.length > 0) {
771
837
  const lines = input.topics.map((t) => {
772
838
  const count = t.count != null ? ` (${t.count} messages)` : "";
773
- return `- ${t.title}${count}`;
839
+ return utext`- ${t.title}${count}`;
774
840
  });
775
841
  out += `## Channel Topics
776
- ${fence("channel topic titles", lines.join("\n"))}
842
+ ${fence("channel topic titles", joinUntrusted(lines, "\n"))}
777
843
  ---
778
844
 
779
845
  `;
@@ -785,21 +851,21 @@ ${fence("channel topic titles", lines.join("\n"))}
785
851
  function isAddressed(content, agentName) {
786
852
  const name = agentName.trim();
787
853
  if (!name) return false;
788
- const c = content.toLowerCase();
854
+ const c = rawUntrusted(content).toLowerCase();
789
855
  const forms = /* @__PURE__ */ new Set([name.toLowerCase(), name.toLowerCase().replace(/\s+/g, "_")]);
790
856
  for (const f of forms) if (c.includes("@" + f)) return true;
791
857
  return false;
792
858
  }
793
859
  function classifyCatchUp(messages, agentName, nowMs, opts) {
794
- const inWindow = messages.filter((m) => {
795
- const t = Date.parse(m.createdAt);
796
- return Number.isFinite(t) ? t >= nowMs - opts.ttlMs : true;
860
+ const mentions = messages.filter((m) => {
861
+ if (m.seq <= opts.deliveredMaxSeq) return false;
862
+ const t = Date.parse(rawUntrusted(m.createdAt));
863
+ const withinTtl = Number.isFinite(t) ? t >= nowMs - opts.ttlMs : true;
864
+ if (!withinTtl) return false;
865
+ return !sameText(m.senderName, agentName) && isAddressed(m.content, agentName);
797
866
  });
798
- const mentions = inWindow.filter(
799
- (m) => m.senderName !== agentName && isAddressed(m.content, agentName)
800
- );
801
867
  const capped = mentions.slice(-opts.maxMentions);
802
- return { context: inWindow, mentions: capped };
868
+ return { context: messages, mentions: capped };
803
869
  }
804
870
 
805
871
  // src/relayClient.ts
@@ -811,6 +877,22 @@ var AUTH_GRACE_MS = 1500;
811
877
  var CATCHUP_WINDOW_MS = 8e3;
812
878
  var SEEN_CAP = 5e3;
813
879
  var RESUME_MAX_PAGES = 200;
880
+ var WS_MAX_PAYLOAD_BYTES = 4 * 1024 * 1024;
881
+ var MAX_MESSAGE_CONTENT_CHARS = 65536;
882
+ var MAX_BACKFILL_CHARS_PER_CATCHUP = 2e6;
883
+ var MAX_BACKFILL_CHARS_PER_CONNECTION = 8e6;
884
+ var MAX_CONCURRENT_CATCHUPS = 3;
885
+ var GAP_RESUME_MIN_INTERVAL_MS = 5e3;
886
+ var REBUILD_LOOKBACK_SEQS = 100;
887
+ function clampContent(s) {
888
+ if (s.length <= MAX_MESSAGE_CONTENT_CHARS) return s;
889
+ return `${s.slice(0, MAX_MESSAGE_CONTENT_CHARS)}
890
+ [message truncated by bridge: exceeded ${MAX_MESSAGE_CONTENT_CHARS} chars]`;
891
+ }
892
+ var UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
893
+ function isUuid(s) {
894
+ return UUID_RE.test(s);
895
+ }
814
896
  var FATAL_CLOSE_CODES = /* @__PURE__ */ new Set([
815
897
  4001,
816
898
  // auth failed (bad/expired/tampered token) — recoverable via key re-mint when cfg.mintToken exists
@@ -824,6 +906,7 @@ var FATAL_CLOSE_CODES = /* @__PURE__ */ new Set([
824
906
  var MAX_REMINT_ATTEMPTS = 3;
825
907
  var PRE_HELLO_HINT_AFTER = 5;
826
908
  var RelayClient = class {
909
+ // per-channel gap-backfill rate limit
827
910
  constructor(cfg, deps) {
828
911
  this.cfg = cfg;
829
912
  this.deps = deps;
@@ -848,6 +931,8 @@ var RelayClient = class {
848
931
  graceTimer = null;
849
932
  confirmed = false;
850
933
  // single-shot: onReady has fired (relay accepted the agent)
934
+ reconnectAnnounced = false;
935
+ // per-connection: "reconnected" printed for this connection
851
936
  catchUpCbs = [];
852
937
  caughtUp = /* @__PURE__ */ new Set();
853
938
  // channels caught up this connection
@@ -861,8 +946,22 @@ var RelayClient = class {
861
946
  fatalCb = null;
862
947
  removedCb = null;
863
948
  addedCb = null;
949
+ backfillCharsThisConn = 0;
950
+ // per-connection backfill budget (BRIDGE-09/12)
951
+ activeCatchUps = 0;
952
+ // concurrent catch-up limiter (BRIDGE-12)
953
+ catchUpWaiters = [];
954
+ lastGapResumeAt = /* @__PURE__ */ new Map();
955
+ // Every multi-listener subscription returns an unsubscribe (BRIDGE-18) so no
956
+ // handler array can grow without a way to shrink it. The router registers each
957
+ // once per process, but parity here means future per-channel subscribers cannot
958
+ // accumulate across channel churn the way pre-unsubscribe onRoster handlers did.
864
959
  onInbound(cb) {
865
960
  this.inboundCbs.push(cb);
961
+ return () => {
962
+ const i = this.inboundCbs.indexOf(cb);
963
+ if (i >= 0) this.inboundCbs.splice(i, 1);
964
+ };
866
965
  }
867
966
  /** Subscribe to roster updates; returns an unsubscribe so a per-channel session can drop
868
967
  * its subscription on teardown (otherwise handlers accumulate across channel churn). */
@@ -875,9 +974,17 @@ var RelayClient = class {
875
974
  }
876
975
  onFlowSignal(cb) {
877
976
  this.flowCbs.push(cb);
977
+ return () => {
978
+ const i = this.flowCbs.indexOf(cb);
979
+ if (i >= 0) this.flowCbs.splice(i, 1);
980
+ };
878
981
  }
879
982
  onCatchUp(cb) {
880
983
  this.catchUpCbs.push(cb);
984
+ return () => {
985
+ const i = this.catchUpCbs.indexOf(cb);
986
+ if (i >= 0) this.catchUpCbs.splice(i, 1);
987
+ };
881
988
  }
882
989
  onReady(cb) {
883
990
  this.readyCb = cb;
@@ -894,8 +1001,17 @@ var RelayClient = class {
894
1001
  start() {
895
1002
  this.connect();
896
1003
  }
1004
+ /** Validate a relay-supplied id as a UUID and return it URL-encoded, or null
1005
+ * (with a warning) when it is not one — the REST call is then refused (BRIDGE-11). */
1006
+ pathId(id, what) {
1007
+ if (!isUuid(id)) {
1008
+ console.warn(`[arp-bridge] refusing REST call: ${what} is not a UUID: ${sanitizeForTty(id).slice(0, 80)}`);
1009
+ return null;
1010
+ }
1011
+ return encodeURIComponent(id);
1012
+ }
897
1013
  connect() {
898
- const url = `${this.cfg.relayWsUrl}/ws/agent/${this.cfg.agentId}`;
1014
+ const url = `${this.cfg.relayWsUrl}/ws/agent/${encodeURIComponent(this.cfg.agentId)}`;
899
1015
  const ws = this.deps.wsFactory(url, ["bearer.arp.v1", this.cfg.token]);
900
1016
  this.ws = ws;
901
1017
  ws.on("open", () => this.onOpen());
@@ -906,7 +1022,9 @@ var RelayClient = class {
906
1022
  }
907
1023
  onOpen() {
908
1024
  this.caughtUp.clear();
1025
+ this.backfillCharsThisConn = 0;
909
1026
  this.connectedAt = Date.now();
1027
+ this.reconnectAnnounced = false;
910
1028
  this.heartbeatTimer = setInterval(() => this.send({ type: "ping" }), HEARTBEAT_MS);
911
1029
  this.armWatchdog();
912
1030
  this.stableTimer = setTimeout(() => {
@@ -917,13 +1035,20 @@ var RelayClient = class {
917
1035
  this.graceTimer = setTimeout(() => this.confirmReady(), AUTH_GRACE_MS);
918
1036
  this.onConnected();
919
1037
  }
920
- /** Single-shot: fires onReady at most once, on the FIRST successful connect. */
1038
+ /** Fires onReady at most once, on the FIRST successful connect; on later
1039
+ * connections it announces the reconnect instead (once per connection). */
921
1040
  confirmReady() {
922
1041
  if (this.graceTimer) {
923
1042
  clearTimeout(this.graceTimer);
924
1043
  this.graceTimer = null;
925
1044
  }
926
- if (this.confirmed) return;
1045
+ if (this.confirmed) {
1046
+ if (!this.reconnectAnnounced) {
1047
+ this.reconnectAnnounced = true;
1048
+ console.log("[arp-bridge] reconnected");
1049
+ }
1050
+ return;
1051
+ }
927
1052
  this.confirmed = true;
928
1053
  this.readyCb?.();
929
1054
  }
@@ -945,9 +1070,12 @@ var RelayClient = class {
945
1070
  * infinite loop if a misbehaving server keeps claiming hasMore. */
946
1071
  async fetchAfterSeq(channelId, afterSeq) {
947
1072
  const out = [];
1073
+ const ch = this.pathId(channelId, "channelId");
1074
+ if (!ch) return out;
948
1075
  let cursor = afterSeq;
1076
+ let takenChars = 0;
949
1077
  for (let page = 0; page < RESUME_MAX_PAGES; page++) {
950
- const url = `${this.cfg.relayHttpUrl}/channels/${channelId}/messages?afterSeq=${cursor}`;
1078
+ const url = `${this.cfg.relayHttpUrl}/channels/${ch}/messages?afterSeq=${cursor}`;
951
1079
  let res;
952
1080
  try {
953
1081
  res = await this.deps.fetchFn(url, { headers: { Authorization: `Bearer ${this.cfg.token}` } });
@@ -966,40 +1094,62 @@ var RelayClient = class {
966
1094
  for (const m of list) {
967
1095
  const seq = Number(m.seq ?? 0);
968
1096
  if (seq > maxSeq) maxSeq = seq;
1097
+ const content = clampContent(String(m.content ?? ""));
1098
+ takenChars += content.length;
1099
+ this.backfillCharsThisConn += content.length;
969
1100
  out.push({
970
1101
  id: String(m.id ?? ""),
971
1102
  seq,
972
1103
  channelId,
973
- content: String(m.content ?? ""),
974
- senderId: String(m.agentId ?? ""),
975
- senderName: String(m.agentName ?? ""),
1104
+ content: untrusted(content),
1105
+ senderId: untrusted(String(m.agentId ?? "")),
1106
+ senderName: untrusted(String(m.agentName ?? "")),
976
1107
  senderType: String(m.messageType ?? m.type ?? ""),
977
1108
  // relay live/resume key is messageType; history path uses type
978
- createdAt: String(m.createdAt ?? ""),
1109
+ createdAt: untrusted(String(m.createdAt ?? "")),
979
1110
  isHistory: false
980
1111
  // shape parity with live messages; the caller decides how to handle them
981
1112
  });
982
1113
  }
983
1114
  if (!body.hasMore) return out;
1115
+ if (takenChars >= MAX_BACKFILL_CHARS_PER_CATCHUP || this.backfillCharsThisConn >= MAX_BACKFILL_CHARS_PER_CONNECTION) {
1116
+ console.warn("[arp-bridge] backfill content budget exhausted; truncating catch-up");
1117
+ return out;
1118
+ }
984
1119
  cursor = maxSeq;
985
1120
  }
986
1121
  console.warn("[arp-bridge] backfill hit page cap", RESUME_MAX_PAGES);
987
1122
  return out;
988
1123
  }
1124
+ /** Run a catch-up with bounded concurrency (BRIDGE-12): at most
1125
+ * MAX_CONCURRENT_CATCHUPS paginated backfill loops in flight; extras queue FIFO. */
1126
+ scheduleCatchUp(channelId, afterSeq, deliveredMaxSeq) {
1127
+ const run = () => {
1128
+ this.activeCatchUps++;
1129
+ void this.catchUp(channelId, afterSeq, deliveredMaxSeq).finally(() => {
1130
+ this.activeCatchUps--;
1131
+ const next = this.catchUpWaiters.shift();
1132
+ if (next) next();
1133
+ });
1134
+ };
1135
+ if (this.activeCatchUps < MAX_CONCURRENT_CATCHUPS) run();
1136
+ else this.catchUpWaiters.push(run);
1137
+ }
989
1138
  /** Mid-session gap-fill: replay missed messages live (each dedupes via emitInbound). */
990
1139
  async resumeAfterSeq(channelId, afterSeq) {
991
1140
  for (const m of await this.fetchAfterSeq(channelId, afterSeq)) this.emitInbound(m);
992
1141
  }
993
1142
  /** Offline-rejoin catch-up: classify the missed window and hand it to onCatchUp once.
994
1143
  * Does NOT route through emitInbound/onInbound to avoid per-message passive submits. */
995
- async catchUp(channelId, afterSeq) {
1144
+ async catchUp(channelId, afterSeq, deliveredMaxSeq) {
996
1145
  const missed = await this.fetchAfterSeq(channelId, afterSeq);
997
1146
  if (missed.length === 0) return;
998
1147
  for (const m of missed) if (m.id) this.markSeen(channelId, m.id);
999
1148
  this.bumpCursor(channelId, missed[missed.length - 1].seq);
1000
1149
  const result = classifyCatchUp(missed, this.cfg.agentName, Date.now(), {
1001
1150
  ttlMs: this.cfg.catchUpTtlMs,
1002
- maxMentions: this.cfg.catchUpMaxMentions
1151
+ maxMentions: this.cfg.catchUpMaxMentions,
1152
+ deliveredMaxSeq
1003
1153
  });
1004
1154
  this.catchUpCbs.forEach((cb) => cb(channelId, result));
1005
1155
  }
@@ -1011,6 +1161,8 @@ var RelayClient = class {
1011
1161
  this.remintAttempts++;
1012
1162
  console.log("[arp-bridge] access token rejected; re-minting from agent key");
1013
1163
  void this.cfg.mintToken().then((token) => {
1164
+ const verdict = validateReplacementToken(token, this.cfg.token);
1165
+ if (!verdict.ok) throw new Error(`re-minted token failed validation: ${verdict.reason}`);
1014
1166
  this.cfg.token = token;
1015
1167
  if (!this.stopped) this.connect();
1016
1168
  }).catch((err) => {
@@ -1075,7 +1227,7 @@ var RelayClient = class {
1075
1227
  }
1076
1228
  onMessage(raw) {
1077
1229
  this.armWatchdog();
1078
- if (!this.confirmed) this.confirmReady();
1230
+ this.confirmReady();
1079
1231
  let msg;
1080
1232
  try {
1081
1233
  msg = JSON.parse(raw);
@@ -1089,18 +1241,24 @@ var RelayClient = class {
1089
1241
  case "channel_message":
1090
1242
  this.handleChannelMessage(msg);
1091
1243
  break;
1092
- case "token_refresh":
1093
- if (typeof msg.token === "string" && msg.token.length > 0) {
1094
- this.cfg.token = msg.token;
1095
- console.log("[arp-bridge] token refreshed");
1244
+ case "token_refresh": {
1245
+ if (typeof msg.token !== "string" || msg.token.length === 0) break;
1246
+ const verdict = validateReplacementToken(msg.token, this.cfg.token);
1247
+ if (!verdict.ok) {
1248
+ console.warn(`[arp-bridge] rejected token_refresh (${verdict.reason}); keeping the current token`);
1249
+ break;
1096
1250
  }
1251
+ this.cfg.token = msg.token;
1252
+ console.log("[arp-bridge] token refreshed");
1097
1253
  break;
1254
+ }
1098
1255
  case "removed": {
1099
1256
  const ch = String(msg.channelId ?? "");
1100
1257
  if (!ch) break;
1101
1258
  this.cursors.delete(ch);
1102
1259
  this.seenByChannel.delete(ch);
1103
1260
  this.caughtUp.delete(ch);
1261
+ this.lastGapResumeAt.delete(ch);
1104
1262
  this.removedCb?.(ch);
1105
1263
  break;
1106
1264
  }
@@ -1135,10 +1293,11 @@ var RelayClient = class {
1135
1293
  for (const [ch, seqRaw] of Object.entries(resume)) {
1136
1294
  const seq = Number(seqRaw);
1137
1295
  if (Number.isFinite(seq) && seq > this.cursorOf(ch)) this.cursors.set(ch, seq);
1138
- const floor = this.cursorOf(ch);
1139
- if (!this.caughtUp.has(ch) && floor > 0) {
1296
+ const deliveredMax = this.cursorOf(ch);
1297
+ if (!this.caughtUp.has(ch) && deliveredMax > 0) {
1140
1298
  this.caughtUp.add(ch);
1141
- void this.catchUp(ch, floor);
1299
+ const rebuildFloor = Math.max(0, deliveredMax - REBUILD_LOOKBACK_SEQS);
1300
+ this.scheduleCatchUp(ch, rebuildFloor, deliveredMax);
1142
1301
  }
1143
1302
  }
1144
1303
  }
@@ -1159,18 +1318,25 @@ var RelayClient = class {
1159
1318
  id: String(m.id ?? ""),
1160
1319
  seq: Number(m.seq ?? 0),
1161
1320
  channelId,
1162
- content: String(m.content ?? ""),
1163
- senderId: String(m.agentId ?? ""),
1164
- senderName: String(m.agentName ?? ""),
1321
+ content: untrusted(clampContent(String(m.content ?? ""))),
1322
+ // bound per-message memory/prompt size (BRIDGE-09)
1323
+ senderId: untrusted(String(m.agentId ?? "")),
1324
+ senderName: untrusted(String(m.agentName ?? "")),
1165
1325
  senderType: String(m.messageType ?? m.type ?? ""),
1166
1326
  // relay live/resume key is messageType; history path uses type
1167
- createdAt: String(m.createdAt ?? ""),
1327
+ createdAt: untrusted(String(m.createdAt ?? "")),
1168
1328
  isHistory: Boolean(msg.isHistory)
1169
1329
  };
1170
1330
  const gapDetected = !inbound.isHistory && this.cursorOf(channelId) > 0 && inbound.seq > this.cursorOf(channelId) + 1;
1171
1331
  const gapFrom = this.cursorOf(channelId);
1172
1332
  this.emitInbound(inbound);
1173
- if (gapDetected) void this.resumeAfterSeq(channelId, gapFrom);
1333
+ if (gapDetected) {
1334
+ const now = Date.now();
1335
+ if (now - (this.lastGapResumeAt.get(channelId) ?? 0) >= GAP_RESUME_MIN_INTERVAL_MS) {
1336
+ this.lastGapResumeAt.set(channelId, now);
1337
+ void this.resumeAfterSeq(channelId, gapFrom);
1338
+ }
1339
+ }
1174
1340
  }
1175
1341
  /** Normalize a turn_notification / synthesis_request / direction_request into a FlowSignal and emit it. */
1176
1342
  handleFlowSignal(kind, msg) {
@@ -1178,8 +1344,9 @@ var RelayClient = class {
1178
1344
  if (!flowId) return;
1179
1345
  const rawHistory = kind === "synthesis" || kind === "direction" ? msg.flowHistory ?? msg.messages ?? [] : msg.recentMessages ?? [];
1180
1346
  const history = (Array.isArray(rawHistory) ? rawHistory : []).map((e) => ({
1181
- agentName: String(e.agentName ?? ""),
1182
- content: String(e.content ?? ""),
1347
+ agentName: untrusted(String(e.agentName ?? "")),
1348
+ content: untrusted(clampContent(String(e.content ?? ""))),
1349
+ // bound per-entry size (BRIDGE-09)
1183
1350
  messageType: e.messageType ?? e.type,
1184
1351
  turnNumber: e.turnNumber,
1185
1352
  createdAt: e.createdAt
@@ -1188,11 +1355,11 @@ var RelayClient = class {
1188
1355
  kind,
1189
1356
  flowId,
1190
1357
  channelId: String(msg.channelId ?? ""),
1191
- topic: String(msg.topic ?? ""),
1192
- rolePrompt: typeof msg.rolePrompt === "string" ? msg.rolePrompt : void 0,
1193
- contextPrompt: typeof msg.contextPrompt === "string" ? msg.contextPrompt : void 0,
1194
- synthesisPrompt: typeof msg.synthesisPrompt === "string" ? msg.synthesisPrompt : void 0,
1195
- candidates: Array.isArray(msg.candidates) ? msg.candidates : void 0,
1358
+ topic: untrusted(String(msg.topic ?? "")),
1359
+ rolePrompt: typeof msg.rolePrompt === "string" ? untrusted(msg.rolePrompt) : void 0,
1360
+ contextPrompt: typeof msg.contextPrompt === "string" ? untrusted(msg.contextPrompt) : void 0,
1361
+ synthesisPrompt: typeof msg.synthesisPrompt === "string" ? untrusted(msg.synthesisPrompt) : void 0,
1362
+ candidates: Array.isArray(msg.candidates) ? msg.candidates.map((c) => untrusted(String(c ?? ""))) : void 0,
1196
1363
  history
1197
1364
  };
1198
1365
  this.flowCbs.forEach((cb) => cb(signal));
@@ -1258,7 +1425,9 @@ var RelayClient = class {
1258
1425
  }
1259
1426
  /** Fetch the channel roster and return normalized bot entries (with cards). */
1260
1427
  async fetchRoster(channelId) {
1261
- const url = `${this.cfg.relayHttpUrl}/channels/${channelId}`;
1428
+ const ch = this.pathId(channelId, "channelId");
1429
+ if (!ch) return [];
1430
+ const url = `${this.cfg.relayHttpUrl}/channels/${ch}`;
1262
1431
  try {
1263
1432
  const res = await this.deps.fetchFn(url, { headers: { Authorization: `Bearer ${this.cfg.token}` } });
1264
1433
  if (!res.ok) {
@@ -1274,7 +1443,9 @@ var RelayClient = class {
1274
1443
  }
1275
1444
  }
1276
1445
  async postMessage(channelId, content) {
1277
- const url = `${this.cfg.relayHttpUrl}/channels/${channelId}/messages`;
1446
+ const ch = this.pathId(channelId, "channelId");
1447
+ if (!ch) return;
1448
+ const url = `${this.cfg.relayHttpUrl}/channels/${ch}/messages`;
1278
1449
  const body = JSON.stringify({
1279
1450
  id: randomUUID(),
1280
1451
  // client-generated -> server idempotent on retry
@@ -1300,7 +1471,10 @@ var RelayClient = class {
1300
1471
  * agentId MUST be the agent NAME — the relay's flow gate resolves turn ownership and
1301
1472
  * synthesis role via resolveAgentUUID (a name lookup); a UUID resolves to uuid.Nil -> 403. */
1302
1473
  async postFlowMessage(channelId, flowId, content) {
1303
- const url = `${this.cfg.relayHttpUrl}/channels/${channelId}/flows/${flowId}/messages`;
1474
+ const ch = this.pathId(channelId, "channelId");
1475
+ const fl = this.pathId(flowId, "flowId");
1476
+ if (!ch || !fl) return;
1477
+ const url = `${this.cfg.relayHttpUrl}/channels/${ch}/flows/${fl}/messages`;
1304
1478
  const body = JSON.stringify({
1305
1479
  id: randomUUID(),
1306
1480
  content,
@@ -1320,26 +1494,30 @@ var RelayClient = class {
1320
1494
  console.warn("[arp-bridge] flow post failed:", sanitizeForTty(String(err)));
1321
1495
  }
1322
1496
  }
1323
- /** Channel memory text ("" if none or on error — never throws). */
1497
+ /** Channel memory text (empty if none or on error — never throws). Branded (H2-4). */
1324
1498
  async fetchChannelMemory(channelId) {
1325
- const url = `${this.cfg.relayHttpUrl}/channels/${channelId}/memory`;
1499
+ const ch = this.pathId(channelId, "channelId");
1500
+ if (!ch) return untrusted("");
1501
+ const url = `${this.cfg.relayHttpUrl}/channels/${ch}/memory`;
1326
1502
  try {
1327
1503
  const res = await this.deps.fetchFn(url, { headers: { Authorization: `Bearer ${this.cfg.token}` } });
1328
1504
  if (!res.ok) {
1329
1505
  console.warn("[arp-bridge] memory HTTP", res.status);
1330
- return "";
1506
+ return untrusted("");
1331
1507
  }
1332
1508
  const data = await res.json();
1333
- return typeof data?.content === "string" ? data.content : "";
1509
+ return untrusted(typeof data?.content === "string" ? data.content : "");
1334
1510
  } catch (err) {
1335
1511
  console.warn("[arp-bridge] memory fetch failed:", sanitizeForTty(String(err)));
1336
- return "";
1512
+ return untrusted("");
1337
1513
  }
1338
1514
  }
1339
1515
  /** Channel topics with message counts ([] if none or on error). The relay returns
1340
1516
  * topics with a `title` plus a separate `messageCounts` map keyed by topic id. */
1341
1517
  async fetchChannelTopics(channelId) {
1342
- const url = `${this.cfg.relayHttpUrl}/channels/${channelId}/topics`;
1518
+ const ch = this.pathId(channelId, "channelId");
1519
+ if (!ch) return [];
1520
+ const url = `${this.cfg.relayHttpUrl}/channels/${ch}/topics`;
1343
1521
  try {
1344
1522
  const res = await this.deps.fetchFn(url, { headers: { Authorization: `Bearer ${this.cfg.token}` } });
1345
1523
  if (!res.ok) {
@@ -1349,7 +1527,7 @@ var RelayClient = class {
1349
1527
  const data = await res.json();
1350
1528
  const topics = data?.topics ?? [];
1351
1529
  const counts = data?.messageCounts ?? {};
1352
- return topics.filter((t) => typeof t?.title === "string").map((t) => ({ title: t.title, count: typeof counts[t.id] === "number" ? counts[t.id] : null }));
1530
+ return topics.filter((t) => typeof t?.title === "string").map((t) => ({ title: untrusted(t.title), count: typeof counts[t.id] === "number" ? counts[t.id] : null }));
1353
1531
  } catch (err) {
1354
1532
  console.warn("[arp-bridge] topics fetch failed:", sanitizeForTty(String(err)));
1355
1533
  return [];
@@ -1357,7 +1535,9 @@ var RelayClient = class {
1357
1535
  }
1358
1536
  /** Pinned files marked inject_context=true, labeled with cached content ([] otherwise). */
1359
1537
  async fetchPinnedContext(channelId) {
1360
- const url = `${this.cfg.relayHttpUrl}/channels/${channelId}/pins`;
1538
+ const ch = this.pathId(channelId, "channelId");
1539
+ if (!ch) return [];
1540
+ const url = `${this.cfg.relayHttpUrl}/channels/${ch}/pins`;
1361
1541
  try {
1362
1542
  const res = await this.deps.fetchFn(url, { headers: { Authorization: `Bearer ${this.cfg.token}` } });
1363
1543
  if (!res.ok) {
@@ -1366,7 +1546,10 @@ var RelayClient = class {
1366
1546
  }
1367
1547
  const data = await res.json();
1368
1548
  const pins = data?.pins ?? [];
1369
- return pins.filter((p) => p?.injectContext && typeof p.cachedContent === "string" && p.cachedContent.trim()).map((p) => ({ label: p.displayName || `${p.repoUrl ?? ""}/${p.filePath ?? ""}`, content: p.cachedContent }));
1549
+ return pins.filter((p) => p?.injectContext && typeof p.cachedContent === "string" && p.cachedContent.trim()).map((p) => ({
1550
+ label: untrusted(p.displayName || `${p.repoUrl ?? ""}/${p.filePath ?? ""}`),
1551
+ content: untrusted(p.cachedContent)
1552
+ }));
1370
1553
  } catch (err) {
1371
1554
  console.warn("[arp-bridge] pins fetch failed:", sanitizeForTty(String(err)));
1372
1555
  return [];
@@ -1387,7 +1570,10 @@ var RelayClient = class {
1387
1570
  }
1388
1571
  /** Fetch a flow's transcript (used to backfill a minimal turn_notification). [] on error. */
1389
1572
  async fetchFlowMessages(channelId, flowId) {
1390
- const url = `${this.cfg.relayHttpUrl}/channels/${channelId}/flows/${flowId}/messages`;
1573
+ const ch = this.pathId(channelId, "channelId");
1574
+ const fl = this.pathId(flowId, "flowId");
1575
+ if (!ch || !fl) return [];
1576
+ const url = `${this.cfg.relayHttpUrl}/channels/${ch}/flows/${fl}/messages`;
1391
1577
  try {
1392
1578
  const res = await this.deps.fetchFn(url, { headers: { Authorization: `Bearer ${this.cfg.token}` } });
1393
1579
  if (!res.ok) {
@@ -1397,8 +1583,8 @@ var RelayClient = class {
1397
1583
  const data = await res.json();
1398
1584
  const list = Array.isArray(data) ? data : data.messages ?? [];
1399
1585
  return list.map((e) => ({
1400
- agentName: String(e.agentName ?? ""),
1401
- content: String(e.content ?? ""),
1586
+ agentName: untrusted(String(e.agentName ?? "")),
1587
+ content: untrusted(String(e.content ?? "")),
1402
1588
  messageType: e.messageType ?? e.type,
1403
1589
  turnNumber: e.turnNumber,
1404
1590
  createdAt: e.createdAt
@@ -1413,15 +1599,15 @@ var RelayClient = class {
1413
1599
  // src/flow.ts
1414
1600
  function renderFlowHistory(entries) {
1415
1601
  if (entries.length === 0) return "";
1416
- const lines = entries.map((e) => `${e.agentName || "someone"}: ${e.content}`);
1602
+ const lines = entries.map((e) => utext`${firstNonEmpty([e.agentName], "someone")}: ${e.content}`);
1417
1603
  return `DISCUSSION HISTORY:
1418
- ${fence("flow discussion history", lines.join("\n"))}
1604
+ ${fence("flow discussion history", joinUntrusted(lines, "\n"))}
1419
1605
 
1420
1606
  `;
1421
1607
  }
1422
1608
  function buildFlowPrompt(signal, agentName, channelId, toolMode = "readonly") {
1423
1609
  if (signal.kind === "direction") {
1424
- const candidates = (signal.candidates ?? []).join(", ");
1610
+ const candidates = joinUntrusted(signal.candidates ?? [], ", ");
1425
1611
  const history2 = renderFlowHistory(signal.history);
1426
1612
  const hasHistory = history2 !== "";
1427
1613
  const preamble = hasHistory ? [``, history2.trimEnd(), ``, `Read the conversation above and decide who should speak next.`] : [``, `No turns have been taken yet \u2014 choose who should speak FIRST.`];
@@ -1433,7 +1619,7 @@ function buildFlowPrompt(signal, agentName, channelId, toolMode = "readonly") {
1433
1619
  fence("flow topic", signal.topic),
1434
1620
  ...preamble,
1435
1621
  `Available participants:`,
1436
- fence("flow participant names", candidates || "(none online)"),
1622
+ fence("flow participant names", firstNonEmpty([candidates], "(none online)")),
1437
1623
  ``,
1438
1624
  `Reply with ONLY the name of the single participant who should speak next,`,
1439
1625
  `or reply with ONLY the word END to conclude the discussion and move to synthesis.`,
@@ -1461,6 +1647,22 @@ ${fence("flow topic", signal.topic)}
1461
1647
  ` + role + ctx + synth + "\n" + history + closer;
1462
1648
  }
1463
1649
 
1650
+ // src/promptLimit.ts
1651
+ var DEFAULT_MAX_PROMPT_CHARS = 4e5;
1652
+ function maxPromptChars(env = process.env) {
1653
+ const n = Number(env.ARP_MAX_PROMPT_CHARS);
1654
+ return Number.isFinite(n) && n > 0 ? Math.floor(n) : DEFAULT_MAX_PROMPT_CHARS;
1655
+ }
1656
+ function capPrompt(prompt, max = maxPromptChars()) {
1657
+ if (prompt.length <= max) return prompt;
1658
+ const headLen = Math.floor(max * 0.8);
1659
+ const tailLen = max - headLen;
1660
+ const removed = prompt.length - headLen - tailLen;
1661
+ return prompt.slice(0, headLen) + `
1662
+ [...prompt truncated by bridge: ${removed} chars of channel content removed...]
1663
+ ` + prompt.slice(prompt.length - tailLen);
1664
+ }
1665
+
1464
1666
  // src/session.ts
1465
1667
  var SILENCE_SENTINEL = "<<silent>>";
1466
1668
  var ChannelSession = class {
@@ -1519,7 +1721,7 @@ ${toolStatusLine(this.toolMode)}
1519
1721
  */
1520
1722
  async submit(msg) {
1521
1723
  if (!this.session) throw new Error("ChannelSession not started");
1522
- const who = msg.senderName || msg.senderId || "someone";
1724
+ const who = firstNonEmpty([msg.senderName, msg.senderId], "someone");
1523
1725
  const facts = assembleRosterFacts(this.roster, this.agentName);
1524
1726
  const rosterBlock = facts ? `${facts}
1525
1727
 
@@ -1534,7 +1736,7 @@ ${fence("channel message", msg.content)}
1534
1736
  ` + rosterBlock;
1535
1737
  const instructions = isAddressed(msg.content, this.agentName) ? "You were directly addressed (@mentioned), so respond. Output ONLY your channel message itself, concisely. Do NOT include the silence sentinel and do NOT explain whether or why you are responding." : `You received this as a passive channel message. You do NOT need to respond unless it is directly relevant to you. If you have nothing to add, reply with exactly ${SILENCE_SENTINEL} and nothing else. Otherwise output ONLY your channel message, concisely \u2014 do NOT explain whether or why you are responding.`;
1536
1738
  this.beacon?.begin();
1537
- this.session.submit(head + instructions);
1739
+ this.session.submit(capPrompt(head + instructions));
1538
1740
  }
1539
1741
  /**
1540
1742
  * Run one bounded-flow turn or synthesis. Frames a MUST-RESPOND prompt (no
@@ -1555,7 +1757,7 @@ ${fence("channel message", msg.content)}
1555
1757
  let history = signal.history;
1556
1758
  if (history.length === 0) history = await this.flow.fetchHistory(signal.flowId);
1557
1759
  const prompt = buildFlowPrompt({ ...signal, history }, this.agentName, this.channelId, this.toolMode);
1558
- const reply = await this.session.converseLocal(prompt);
1760
+ const reply = await this.session.converseLocal(capPrompt(prompt));
1559
1761
  await this.flow.postReply(signal.flowId, reply.trim() || "(no response)");
1560
1762
  } finally {
1561
1763
  this.beacon?.end();
@@ -1590,22 +1792,28 @@ ${fence("channel message", msg.content)}
1590
1792
  async submitCatchUp(result) {
1591
1793
  if (!this.session) throw new Error("ChannelSession not started");
1592
1794
  if (result.context.length === 0) return;
1593
- const transcript = result.context.map((m) => `[${m.createdAt}] ${m.senderName || m.senderId || "someone"}: ${m.content}`).join("\n");
1795
+ const transcript = joinUntrusted(
1796
+ result.context.map((m) => utext`[${m.createdAt}] ${firstNonEmpty([m.senderName, m.senderId], "someone")}: ${m.content}`),
1797
+ "\n"
1798
+ );
1594
1799
  if (result.mentions.length === 0) {
1595
1800
  if (!this.session.converseLocal) return;
1596
1801
  this.beacon?.begin();
1597
1802
  try {
1598
- await this.session.converseLocal(
1599
- this.promptHead() + `You just reconnected to ARP channel ${this.channelId} after being away. Here is what you missed (context only, do NOT reply to it):
1600
- ` + fence("missed channel messages", transcript)
1601
- );
1803
+ await this.session.converseLocal(capPrompt(
1804
+ this.promptHead() + `You just (re)connected to ARP channel ${this.channelId}. Here is recent channel history for context \u2014 you may have already seen some of it. Absorb it so you can follow back-references in later messages; do NOT reply to it:
1805
+ ` + fence("recent channel history", transcript)
1806
+ ));
1602
1807
  } finally {
1603
1808
  this.beacon?.end();
1604
1809
  }
1605
1810
  return;
1606
1811
  }
1607
1812
  const channelContext = this.fetchContext ? await this.fetchContext() : "";
1608
- const addressed = result.mentions.map((m) => `[${m.createdAt}] ${m.senderName || m.senderId || "someone"}: ${m.content}`).join("\n");
1813
+ const addressed = joinUntrusted(
1814
+ result.mentions.map((m) => utext`[${m.createdAt}] ${firstNonEmpty([m.senderName, m.senderId], "someone")}: ${m.content}`),
1815
+ "\n"
1816
+ );
1609
1817
  const head = this.promptHead() + channelContext + `You are ${this.agentName}. You just reconnected to ARP channel ${this.channelId} after being away. While you were gone, the channel said (context):
1610
1818
  ${fence("missed channel messages", transcript)}
1611
1819
 
@@ -1615,7 +1823,7 @@ ${fence("messages mentioning you", addressed)}
1615
1823
  `;
1616
1824
  const instructions = `Respond ONCE, concisely, to what was directed at you. If something is already resolved by the later messages above, say so briefly. Output ONLY your channel message \u2014 do NOT include the silence sentinel and do NOT explain whether or why you are responding.`;
1617
1825
  this.beacon?.begin();
1618
- this.session.submit(head + instructions);
1826
+ this.session.submit(capPrompt(head + instructions));
1619
1827
  }
1620
1828
  async stop() {
1621
1829
  this.beacon?.stop?.();
@@ -1717,13 +1925,13 @@ function dropVendorNotifications(input) {
1717
1925
 
1718
1926
  // src/acp/client.ts
1719
1927
  var MODEL_AUTH_ENV_KEYS = ["ANTHROPIC_API_KEY", "ANTHROPIC_AUTH_TOKEN"];
1720
- var BRIDGE_CRED_ENV_KEYS = ["ARP_TOKEN", "ARP_INVITE", "ARP_CONFIG_DIR"];
1928
+ var BRIDGE_ENV_PREFIX = "ARP_";
1721
1929
  function buildAcpEnv(base, extra) {
1722
1930
  const merged = {};
1723
1931
  for (const [k, v] of Object.entries({ ...base, ...extra ?? {} })) {
1724
1932
  if (v === void 0) continue;
1725
1933
  if (MODEL_AUTH_ENV_KEYS.includes(k)) continue;
1726
- if (BRIDGE_CRED_ENV_KEYS.includes(k)) continue;
1934
+ if (k.startsWith(BRIDGE_ENV_PREFIX)) continue;
1727
1935
  merged[k] = v;
1728
1936
  }
1729
1937
  return merged;
@@ -2291,7 +2499,7 @@ var ClaudeAdapter = class {
2291
2499
  if (full.length > 0) turnCbs.forEach((cb) => cb(full));
2292
2500
  } else {
2293
2501
  buffer = "";
2294
- console.warn("[arp-bridge] agent turn ended without success:", m.subtype);
2502
+ console.warn("[arp-bridge] agent turn ended without success:", sanitizeForTty(String(m.subtype)));
2295
2503
  }
2296
2504
  }
2297
2505
  }
@@ -2453,8 +2661,8 @@ async function startBridge(cfg, relay, deps) {
2453
2661
  }
2454
2662
  relay.onInbound((m) => {
2455
2663
  if (m.isHistory) return;
2456
- if (m.senderId && m.senderId === cfg.agentUuid || m.senderName && m.senderName === cfg.agentName) return;
2457
- if (!m.content.trim()) return;
2664
+ if (hasText(m.senderId) && sameText(m.senderId, cfg.agentUuid) || hasText(m.senderName) && sameText(m.senderName, cfg.agentName)) return;
2665
+ if (isBlankText(m.content)) return;
2458
2666
  ensureSession(m.channelId).then((s) => s.submit(m)).catch((e) => console.warn(`[arp-bridge] inbound routing failed for channel ${sanitizeForTty(m.channelId)}:`, sanitizeForTty(String(e))));
2459
2667
  });
2460
2668
  relay.onFlowSignal((signal) => {
@@ -2516,11 +2724,13 @@ function reportFatalClose(code, reason) {
2516
2724
  console.error(`[arp-bridge] relay rejected the connection (close ${code}): ${sanitizeForTty(reason)}. Not retrying.`);
2517
2725
  }
2518
2726
  }
2727
+ function makeDefaultWsFactory(WebSocketImpl) {
2728
+ return (url, protocols) => new WebSocketImpl(url, protocols, { maxPayload: WS_MAX_PAYLOAD_BYTES });
2729
+ }
2519
2730
  async function createAndStartBridge(cfg, deps = {}) {
2520
2731
  let wsFactory = deps.wsFactory;
2521
2732
  if (!wsFactory) {
2522
- const WebSocketImpl = (await import("ws")).default;
2523
- wsFactory = (url, protocols) => new WebSocketImpl(url, protocols);
2733
+ wsFactory = makeDefaultWsFactory((await import("ws")).default);
2524
2734
  }
2525
2735
  const relay = new RelayClient(cfg, {
2526
2736
  wsFactory,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@snowyroad/arp",
3
- "version": "0.3.2",
3
+ "version": "0.3.4",
4
4
  "description": "Connect your own coding agent (Claude Code, Codex, Gemini, Grok) to an Agent Relay Protocol channel and collaborate with other agents and humans.",
5
5
  "license": "SEE LICENSE IN LICENSE.md",
6
6
  "author": "SnowyRoad",
@@ -20,6 +20,7 @@
20
20
  "engines": {
21
21
  "node": ">=20"
22
22
  },
23
+ "packageManager": "pnpm@9.15.4",
23
24
  "bin": {
24
25
  "arp": "dist/cli.js"
25
26
  },
@@ -33,23 +34,25 @@
33
34
  "prepublishOnly": "pnpm build",
34
35
  "dev": "tsx src/index.ts",
35
36
  "join": "tsx src/index.ts",
36
- "generate-invite": "tsx scripts/generate-invite.ts",
37
37
  "test": "vitest run",
38
38
  "test:watch": "vitest",
39
39
  "test:integration": "vitest run --config vitest.integration.config.ts",
40
40
  "typecheck": "tsc --noEmit"
41
41
  },
42
42
  "dependencies": {
43
- "@agentclientprotocol/sdk": "0.24.0",
44
- "@anthropic-ai/claude-agent-sdk": "0.1.77",
45
- "ws": "8.21.0"
43
+ "@agentclientprotocol/sdk": "0.25.1",
44
+ "@anthropic-ai/claude-agent-sdk": "0.3.177",
45
+ "@anthropic-ai/sdk": "0.104.1",
46
+ "@modelcontextprotocol/sdk": "1.29.0",
47
+ "ws": "8.21.0",
48
+ "zod": "4.4.3"
46
49
  },
47
50
  "devDependencies": {
48
- "@types/node": "^22.0.0",
51
+ "@types/node": "^25.9.3",
49
52
  "@types/ws": "^8.5.12",
50
53
  "tsup": "^8.5.1",
51
54
  "tsx": "^4.19.0",
52
- "typescript": "^5.6.0",
55
+ "typescript": "^6.0.3",
53
56
  "vitest": "^2.1.0"
54
57
  }
55
58
  }