@agentvault/claude-bridge 0.2.0 → 0.3.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/bridge.d.ts CHANGED
@@ -3,14 +3,60 @@ export interface RoomMessage {
3
3
  senderName: string;
4
4
  plaintext: string;
5
5
  }
6
+ /** Metadata SecureChannel attaches to a 1:1 `message` event. `roomId` is set only
7
+ * when the `message` actually originated in a room (handled by room_message), so
8
+ * its ABSENCE is how we identify a true owner↔agent 1:1 DM. */
9
+ export interface MessageMeta {
10
+ roomId?: string;
11
+ }
6
12
  export interface RoomChannel {
7
13
  on(ev: "room_message", cb: (e: RoomMessage) => void): unknown;
14
+ on(ev: "message", cb: (text: string, metadata: MessageMeta) => void): unknown;
8
15
  on(ev: "error", cb: (err: unknown) => void): unknown;
9
16
  sendToRoom(roomId: string, text: string): Promise<void>;
17
+ send(text: string): Promise<void>;
10
18
  }
11
19
  export interface RoomSession {
12
- push(text: string): void;
13
- onActiveRoom(roomId: string): void;
20
+ /** `reply` is the immutable reply sink captured for THIS message (see
21
+ * ActiveTarget.snapshotReply) — the session invokes it when Claude answers. */
22
+ push(text: string, reply?: (text: string) => Promise<void>): void;
23
+ }
24
+ /** Where a reply should go: a room (sendToRoom) or a 1:1 DM (send). */
25
+ export type BridgeTarget = {
26
+ kind: "room";
27
+ roomId: string;
28
+ } | {
29
+ kind: "dm";
30
+ };
31
+ /** Channel surface ActiveTarget needs to route an outbound reply. */
32
+ export interface ReplyChannel {
33
+ sendToRoom(roomId: string, text: string): Promise<void>;
34
+ send(text: string): Promise<void>;
35
+ }
36
+ /**
37
+ * Tracks the destination of the most recent inbound message and routes outbound
38
+ * replies there. Replaces the bridge's old single `activeRoomId` string so the
39
+ * same Claude session can answer BOTH room traffic and 1:1 owner DMs.
40
+ *
41
+ * Known limitation (matches the prior room-only behavior): the target is global,
42
+ * set per-inbound. If a room message lands while Claude is mid-compose on a DM
43
+ * reply, the reply routes to the room. Binding a reply to its originating turn is
44
+ * a larger redesign; for now the single-conversation-at-a-time assumption holds.
45
+ */
46
+ export declare class ActiveTarget {
47
+ private target;
48
+ setRoom(roomId: string): void;
49
+ setDm(): void;
50
+ get current(): BridgeTarget | null;
51
+ reply(channel: ReplyChannel, text: string): Promise<void>;
52
+ /**
53
+ * Capture the CURRENT target into an immutable reply closure. This is the leak
54
+ * fix: a reply built here routes to where its inbound message came from, even if
55
+ * a later inbound flips the live `current` target. The bridge captures one of
56
+ * these per inbound (at push time) and hands it to the session, so a room message
57
+ * arriving mid-compose can never redirect a private 1:1 reply into the room.
58
+ */
59
+ snapshotReply(channel: ReplyChannel, log?: (msg: string) => void): (text: string) => Promise<void>;
14
60
  }
15
61
  export interface LifecycleChannel {
16
62
  on(ev: "auth_failed", cb: (e: {
@@ -38,7 +84,7 @@ export declare function attachLifecycle(channel: LifecycleChannel, opts?: {
38
84
  log?: (msg: string) => void;
39
85
  onTerminal?: (reason: string) => void;
40
86
  }): void;
41
- export declare function wireBridge(channel: RoomChannel, session: RoomSession, opts?: {
87
+ export declare function wireBridge(channel: RoomChannel, session: RoomSession, target: ActiveTarget, opts?: {
42
88
  roomFilter?: string;
43
89
  log?: (msg: string) => void;
44
90
  }): void;
package/dist/config.d.ts CHANGED
@@ -1,6 +1,8 @@
1
+ export type DataDirSource = "explicit" | "per-agent" | "legacy";
1
2
  export interface BridgeConfig {
2
3
  inviteToken: string;
3
4
  dataDir: string;
5
+ dataDirSource: DataDirSource;
4
6
  apiUrl: string;
5
7
  agentName: string;
6
8
  roomFilter?: string;
package/dist/index.js CHANGED
@@ -69967,6 +69967,40 @@ var init_account_config = __esm({
69967
69967
  DEFAULT_HTTP_PORT = 18790;
69968
69968
  }
69969
69969
  });
69970
+ function terminalReenrollMessage(agentName, reason) {
69971
+ const who = agentName ? `[${agentName}] ` : "";
69972
+ return `${who}device is no longer valid (${reason}) \u2014 it was revoked or replaced. Re-enroll with a fresh token from AgentVault; the old credentials are dead. This agent will stop reconnecting.`;
69973
+ }
69974
+ function attachLifecycle(channel, opts = {}) {
69975
+ const log = opts.log ?? (() => {
69976
+ });
69977
+ const agentName = opts.agentName ?? "";
69978
+ const onTerminal = opts.onTerminal ?? (() => {
69979
+ });
69980
+ let fired = false;
69981
+ const terminal = (reason) => {
69982
+ if (fired) return;
69983
+ fired = true;
69984
+ log(terminalReenrollMessage(agentName, reason));
69985
+ void Promise.resolve(onTerminal(reason)).catch(() => {
69986
+ });
69987
+ };
69988
+ channel.on("auth_failed", (e7) => terminal(e7.reason));
69989
+ channel.on(
69990
+ "session_replaced",
69991
+ (e7) => terminal(e7?.code ? `session_replaced (${e7.code})` : "session_replaced")
69992
+ );
69993
+ channel.on("error", (err) => {
69994
+ const msg = err instanceof Error ? err.message : String(err);
69995
+ if (/reconnect loop detected/i.test(msg)) terminal("reconnect_loop");
69996
+ else if (/device (was )?revoked/i.test(msg)) terminal("device_revoked");
69997
+ });
69998
+ }
69999
+ var init_lifecycle = __esm({
70000
+ "src/lifecycle.ts"() {
70001
+ "use strict";
70002
+ }
70003
+ });
69970
70004
  async function _handleInbound(params) {
69971
70005
  const { plaintext, metadata, channel, account, cfg } = params;
69972
70006
  const core = _ocRuntime;
@@ -70051,6 +70085,7 @@ var init_openclaw_plugin = __esm({
70051
70085
  await init_channel();
70052
70086
  init_account_config();
70053
70087
  init_openclaw_compat();
70088
+ init_lifecycle();
70054
70089
  _ocRuntime = null;
70055
70090
  _channels = /* @__PURE__ */ new Map();
70056
70091
  agentVaultPlugin = {
@@ -70108,6 +70143,7 @@ var init_openclaw_plugin = __esm({
70108
70143
  const dataDir = resolve(account.dataDir.replace(/^~/, process.env.HOME ?? "~"));
70109
70144
  _log?.(`[AgentVault] starting channel (dataDir=${dataDir})`);
70110
70145
  await new Promise((resolvePromise, reject) => {
70146
+ let terminalStopped = false;
70111
70147
  const channel = new SecureChannel({
70112
70148
  inviteToken: "",
70113
70149
  dataDir,
@@ -70126,13 +70162,29 @@ var init_openclaw_plugin = __esm({
70126
70162
  },
70127
70163
  onStateChange: (state) => {
70128
70164
  _log?.(`[AgentVault] state \u2192 ${state}`);
70129
- if (state === "error") reject(new Error("AgentVault channel permanent error"));
70165
+ if (state === "error") {
70166
+ queueMicrotask(() => {
70167
+ if (!terminalStopped) reject(new Error("AgentVault channel permanent error"));
70168
+ });
70169
+ }
70130
70170
  }
70131
70171
  });
70132
70172
  _channels.set(account.accountId, channel);
70133
70173
  channel.on("error", (err) => {
70134
70174
  _log?.(`[AgentVault] channel error (non-fatal): ${String(err)}`);
70135
70175
  });
70176
+ attachLifecycle(channel, {
70177
+ agentName: account.agentName,
70178
+ log: (m22) => _log?.(`[AgentVault] ${m22}`),
70179
+ onTerminal: async () => {
70180
+ terminalStopped = true;
70181
+ try {
70182
+ await channel.stop();
70183
+ } catch {
70184
+ }
70185
+ _channels.delete(account.accountId);
70186
+ }
70187
+ });
70136
70188
  const httpPort = account.httpPort;
70137
70189
  channel.on("ready", () => {
70138
70190
  channel.startHttpServer(httpPort);
@@ -70143,7 +70195,9 @@ var init_openclaw_plugin = __esm({
70143
70195
  _channels.delete(account.accountId);
70144
70196
  resolvePromise();
70145
70197
  });
70146
- channel.start().catch(reject);
70198
+ channel.start().catch((e7) => {
70199
+ if (!terminalStopped) reject(e7);
70200
+ });
70147
70201
  });
70148
70202
  return { stop: async () => {
70149
70203
  } };
@@ -70262,6 +70316,7 @@ var init_openclaw_entry = __esm({
70262
70316
  init_fetch_interceptor();
70263
70317
  init_http_handlers();
70264
70318
  init_openclaw_compat();
70319
+ init_lifecycle();
70265
70320
  init_types();
70266
70321
  isUsingManagedRoutes = false;
70267
70322
  }
@@ -96325,8 +96380,19 @@ var CRED_FILES = ["agentvault.json", "secure-channel.json"];
96325
96380
  function hasPersistedCreds(dataDir) {
96326
96381
  return CRED_FILES.some((f7) => existsSync(join5(dataDir, f7)));
96327
96382
  }
96383
+ function slugify2(name) {
96384
+ return name.toLowerCase().replace(/[^a-z0-9-]/g, "-");
96385
+ }
96386
+ function resolveDataDir(env) {
96387
+ if (env.AV_DATA_DIR) return { dataDir: env.AV_DATA_DIR, source: "explicit" };
96388
+ const base = join5(env.HOME ?? "", ".agentvault", "claude-room-bridge");
96389
+ const perAgent = join5(base, slugify2(env.AV_AGENT_NAME ?? "claude"));
96390
+ if (hasPersistedCreds(perAgent)) return { dataDir: perAgent, source: "per-agent" };
96391
+ if (hasPersistedCreds(base)) return { dataDir: base, source: "legacy" };
96392
+ return { dataDir: perAgent, source: "per-agent" };
96393
+ }
96328
96394
  function loadConfig(env, argv = []) {
96329
- const dataDir = env.AV_DATA_DIR ?? `${env.HOME}/.agentvault/claude-room-bridge`;
96395
+ const { dataDir, source: dataDirSource } = resolveDataDir(env);
96330
96396
  const inviteToken = (argv[0] && !argv[0].startsWith("-") ? argv[0] : "") || env.AV_INVITE_TOKEN || "";
96331
96397
  if (!inviteToken && !hasPersistedCreds(dataDir)) {
96332
96398
  throw new Error(
@@ -96336,8 +96402,12 @@ function loadConfig(env, argv = []) {
96336
96402
  return {
96337
96403
  inviteToken,
96338
96404
  dataDir,
96405
+ dataDirSource,
96339
96406
  apiUrl: env.AV_API_URL ?? "https://api.agentvault.chat",
96340
- agentName: env.AV_AGENT_NAME ?? "CClaude",
96407
+ // Identity shown to the agent's own session. Set this to the agent's
96408
+ // AgentVault name via AV_AGENT_NAME (the native connect command passes it).
96409
+ // The neutral default avoids impersonating any specific named agent when unset.
96410
+ agentName: env.AV_AGENT_NAME ?? "Claude",
96341
96411
  roomFilter: env.AV_ROOM_ID || void 0,
96342
96412
  model: env.AV_CLAUDE_MODEL || void 0,
96343
96413
  systemPrompt: env.AV_SYSTEM_PROMPT || void 0
@@ -118291,7 +118361,7 @@ __export(util_exports2, {
118291
118361
  required: () => required2,
118292
118362
  safeExtend: () => safeExtend2,
118293
118363
  shallowClone: () => shallowClone2,
118294
- slugify: () => slugify2,
118364
+ slugify: () => slugify3,
118295
118365
  stringifyPrimitive: () => stringifyPrimitive2,
118296
118366
  uint8ArrayToBase64: () => uint8ArrayToBase643,
118297
118367
  uint8ArrayToBase64url: () => uint8ArrayToBase64url2,
@@ -118432,7 +118502,7 @@ function randomString2(length = 10) {
118432
118502
  function esc2(str) {
118433
118503
  return JSON.stringify(str);
118434
118504
  }
118435
- function slugify2(input) {
118505
+ function slugify3(input) {
118436
118506
  return input.toLowerCase().trim().replace(/[^\w\s-]/g, "").replace(/[\s_-]+/g, "-").replace(/^-+|-+$/g, "");
118437
118507
  }
118438
118508
  var captureStackTrace2 = "captureStackTrace" in Error ? Error.captureStackTrace : (..._args) => {
@@ -128166,7 +128236,7 @@ function _toUpperCase2() {
128166
128236
  }
128167
128237
  // @__NO_SIDE_EFFECTS__
128168
128238
  function _slugify2() {
128169
- return /* @__PURE__ */ _overwrite2((input) => slugify2(input));
128239
+ return /* @__PURE__ */ _overwrite2((input) => slugify3(input));
128170
128240
  }
128171
128241
  // @__NO_SIDE_EFFECTS__
128172
128242
  function _array2(Class3, element, params) {
@@ -131410,11 +131480,11 @@ config2(en_default3());
131410
131480
  function makeRoomSayTool(onSay) {
131411
131481
  return bs(
131412
131482
  "say",
131413
- "Send a message to the current AgentVault room so the other participants see it. Call this ONLY when you have something worth adding. If you have nothing to say, do not call it \u2014 stay silent.",
131414
- { message: external_exports.string().describe("The message to post to the room.") },
131483
+ "Send a message to the current AgentVault conversation \u2014 your owner in a 1:1 direct message, or the other participants in a room. In a room, call this ONLY when you have something worth adding; if you have nothing to say, stay silent.",
131484
+ { message: external_exports.string().describe("The message to send to the current conversation.") },
131415
131485
  async (args) => {
131416
131486
  await onSay(args.message);
131417
- return { content: [{ type: "text", text: "Message sent to the room." }] };
131487
+ return { content: [{ type: "text", text: "Message sent." }] };
131418
131488
  }
131419
131489
  );
131420
131490
  }
@@ -131426,37 +131496,52 @@ var PersistentClaudeSession = class {
131426
131496
  opts;
131427
131497
  waiting = null;
131428
131498
  pending = [];
131429
- push(text) {
131499
+ /** Reply sink bound to the message currently being processed. Set as each
131500
+ * message is handed to the model so the say tool routes to the right place. */
131501
+ activeReply;
131502
+ push(text, reply) {
131430
131503
  const msg = {
131431
131504
  type: "user",
131432
131505
  message: { role: "user", content: text },
131433
131506
  parent_tool_use_id: null,
131434
131507
  session_id: ""
131435
131508
  };
131509
+ const item = { msg, reply };
131436
131510
  if (this.waiting) {
131437
131511
  const w2 = this.waiting;
131438
131512
  this.waiting = null;
131439
- w2(msg);
131513
+ w2(item);
131440
131514
  } else {
131441
- this.pending.push(msg);
131515
+ this.pending.push(item);
131442
131516
  }
131443
131517
  }
131518
+ /** Route a say-tool message to the reply bound to the in-flight message, falling
131519
+ * back to opts.onSay. This is what closes the DM→room leak: the destination is
131520
+ * the one captured for the message being answered, not a live global target. */
131521
+ async deliver(text) {
131522
+ if (this.activeReply) await this.activeReply(text);
131523
+ else if (this.opts.onSay) await this.opts.onSay(text);
131524
+ }
131444
131525
  async *input() {
131445
131526
  while (true) {
131446
131527
  if (this.pending.length > 0) {
131447
- yield this.pending.shift();
131528
+ const item2 = this.pending.shift();
131529
+ this.activeReply = item2.reply;
131530
+ yield item2.msg;
131448
131531
  continue;
131449
131532
  }
131450
- yield await new Promise((resolve2) => {
131533
+ const item = await new Promise((resolve2) => {
131451
131534
  this.waiting = resolve2;
131452
131535
  });
131536
+ this.activeReply = item.reply;
131537
+ yield item.msg;
131453
131538
  }
131454
131539
  }
131455
131540
  async start() {
131456
131541
  const roomServer = _s({
131457
131542
  name: "room",
131458
131543
  version: "0.2.0",
131459
- tools: [makeRoomSayTool(this.opts.onSay)]
131544
+ tools: [makeRoomSayTool((text) => this.deliver(text))]
131460
131545
  });
131461
131546
  const sdkOptions = {
131462
131547
  model: this.opts.model,
@@ -131502,7 +131587,47 @@ var PersistentClaudeSession = class {
131502
131587
  };
131503
131588
 
131504
131589
  // src/bridge.ts
131505
- function attachLifecycle(channel, opts = {}) {
131590
+ var ActiveTarget = class {
131591
+ target = null;
131592
+ setRoom(roomId) {
131593
+ this.target = { kind: "room", roomId };
131594
+ }
131595
+ setDm() {
131596
+ this.target = { kind: "dm" };
131597
+ }
131598
+ get current() {
131599
+ return this.target;
131600
+ }
131601
+ async reply(channel, text) {
131602
+ const t7 = this.target;
131603
+ if (!t7) return;
131604
+ if (t7.kind === "room") await channel.sendToRoom(t7.roomId, text);
131605
+ else await channel.send(text);
131606
+ }
131607
+ /**
131608
+ * Capture the CURRENT target into an immutable reply closure. This is the leak
131609
+ * fix: a reply built here routes to where its inbound message came from, even if
131610
+ * a later inbound flips the live `current` target. The bridge captures one of
131611
+ * these per inbound (at push time) and hands it to the session, so a room message
131612
+ * arriving mid-compose can never redirect a private 1:1 reply into the room.
131613
+ */
131614
+ snapshotReply(channel, log = () => {
131615
+ }) {
131616
+ const t7 = this.target;
131617
+ const where = !t7 ? "nowhere" : t7.kind === "room" ? `room ${t7.roomId.slice(0, 8)}` : "1:1 owner";
131618
+ return async (text) => {
131619
+ if (!t7) return;
131620
+ try {
131621
+ if (t7.kind === "room") await channel.sendToRoom(t7.roomId, text);
131622
+ else await channel.send(text);
131623
+ log(`said to ${where}`);
131624
+ } catch (err) {
131625
+ log(`send failed to ${where}: ${err instanceof Error ? err.message : String(err)}`);
131626
+ }
131627
+ };
131628
+ }
131629
+ };
131630
+ function attachLifecycle2(channel, opts = {}) {
131506
131631
  const log = opts.log ?? (() => {
131507
131632
  });
131508
131633
  const onTerminal = opts.onTerminal ?? (() => process.exit(1));
@@ -131522,7 +131647,7 @@ function attachLifecycle(channel, opts = {}) {
131522
131647
  if (/reconnect loop detected/i.test(msg)) terminal("reconnect_loop");
131523
131648
  });
131524
131649
  }
131525
- function wireBridge(channel, session, opts = {}) {
131650
+ function wireBridge(channel, session, target, opts = {}) {
131526
131651
  const log = opts.log ?? (() => {
131527
131652
  });
131528
131653
  channel.on("error", (err) => {
@@ -131532,8 +131657,14 @@ function wireBridge(channel, session, opts = {}) {
131532
131657
  channel.on("room_message", (e7) => {
131533
131658
  if (opts.roomFilter && e7.roomId !== opts.roomFilter) return;
131534
131659
  log(`inbound from ${e7.senderName} in ${e7.roomId.slice(0, 8)}`);
131535
- session.onActiveRoom(e7.roomId);
131536
- session.push(`[${e7.senderName}]: ${e7.plaintext}`);
131660
+ target.setRoom(e7.roomId);
131661
+ session.push(`[${e7.senderName}]: ${e7.plaintext}`, target.snapshotReply(channel, log));
131662
+ });
131663
+ channel.on("message", (text, metadata) => {
131664
+ if (metadata?.roomId) return;
131665
+ log("inbound 1:1 DM from owner");
131666
+ target.setDm();
131667
+ session.push(text, target.snapshotReply(channel, log));
131537
131668
  });
131538
131669
  }
131539
131670
 
@@ -131546,7 +131677,9 @@ async function main() {
131546
131677
  "[bridge] warning: passing the invite token on the command line is visible to other local users via 'ps'. Prefer: AV_INVITE_TOKEN=\u2026 npx @agentvault/claude-bridge"
131547
131678
  );
131548
131679
  }
131549
- let activeRoomId = cfg.roomFilter ?? "";
131680
+ console.error(`[bridge] data dir: ${cfg.dataDir} (${cfg.dataDirSource})`);
131681
+ const target = new ActiveTarget();
131682
+ if (cfg.roomFilter) target.setRoom(cfg.roomFilter);
131550
131683
  const channel = new SecureChannel({
131551
131684
  inviteToken: cfg.inviteToken,
131552
131685
  dataDir: cfg.dataDir,
@@ -131556,28 +131689,22 @@ async function main() {
131556
131689
  });
131557
131690
  const session = new PersistentClaudeSession({
131558
131691
  model: cfg.model,
131559
- systemPrompt: cfg.systemPrompt ?? "You are CClaude, collaborating with other agents in an AgentVault room. You see every room message. To speak, call the room_say tool \u2014 and only when you have something worth adding. If you have nothing to contribute, stay silent (do not call room_say). Keep messages concise.",
131560
- // Claude speaks by calling room_sayposted to the active room.
131561
- onSay: async (text) => {
131562
- if (!activeRoomId) return;
131563
- try {
131564
- await channel.sendToRoom(activeRoomId, text);
131565
- console.error(`[bridge] said in ${activeRoomId.slice(0, 8)}`);
131566
- } catch (err) {
131567
- console.error(`[bridge] send failed to ${activeRoomId.slice(0, 8)}:`, err);
131568
- }
131569
- },
131692
+ systemPrompt: cfg.systemPrompt ?? `You are ${cfg.agentName}, an AI agent on AgentVault. You talk with your owner in 1:1 direct messages and collaborate with other agents in shared rooms. That is your identity \u2014 introduce yourself by that name and do not claim to be any other agent. To speak, call the say tool. In a 1:1 with your owner, reply to what they say. In a room you see every message \u2014 call say only when you have something worth adding, and otherwise stay silent. Keep messages concise.`,
131693
+ // Claude speaks by calling the say tool the session routes it to the reply
131694
+ // bound to the message being answered (wireBridge captures that per inbound via
131695
+ // ActiveTarget.snapshotReply, which also logs the "said to …" line). This
131696
+ // per-message binding is what prevents a private 1:1 reply from leaking into a
131697
+ // room when room traffic arrives mid-compose.
131570
131698
  // Assistant reasoning that wasn't sent — log a short trace only.
131571
131699
  onObserve: (text) => console.error(`[bridge] observed (${text.length} chars, not sent)`)
131572
131700
  });
131573
131701
  wireBridge(
131574
131702
  channel,
131575
- { push: (t7) => session.push(t7), onActiveRoom: (r7) => {
131576
- activeRoomId = r7;
131577
- } },
131703
+ { push: (t7, reply) => session.push(t7, reply) },
131704
+ target,
131578
131705
  { roomFilter: cfg.roomFilter, log: (m6) => console.error("[bridge] " + m6) }
131579
131706
  );
131580
- attachLifecycle(channel, {
131707
+ attachLifecycle2(channel, {
131581
131708
  log: (m6) => console.error("[bridge] " + m6)
131582
131709
  });
131583
131710
  channel.on("state", (s10) => console.error(`[bridge] channel state: ${JSON.stringify(s10)}`));
package/dist/session.d.ts CHANGED
@@ -45,9 +45,11 @@ export type QueryFn = (args: {
45
45
  type: string;
46
46
  [k: string]: unknown;
47
47
  }>;
48
+ type ReplySink = (text: string) => void | Promise<void>;
48
49
  export interface SessionOpts {
49
- /** Called when Claude chooses to speak (via the room_say tool). */
50
- onSay: (text: string) => void | Promise<void>;
50
+ /** Fallback reply sink when a message carries no per-message reply (e.g. a
51
+ * proactive say). Per-message replies passed to push() take precedence. */
52
+ onSay?: ReplySink;
51
53
  /** Called with assistant text that is NOT sent to the room (for logging). */
52
54
  onObserve?: (text: string) => void;
53
55
  model?: string;
@@ -58,9 +60,17 @@ export declare class PersistentClaudeSession {
58
60
  private opts;
59
61
  private waiting;
60
62
  private pending;
63
+ /** Reply sink bound to the message currently being processed. Set as each
64
+ * message is handed to the model so the say tool routes to the right place. */
65
+ private activeReply?;
61
66
  constructor(opts: SessionOpts);
62
- push(text: string): void;
67
+ push(text: string, reply?: ReplySink): void;
68
+ /** Route a say-tool message to the reply bound to the in-flight message, falling
69
+ * back to opts.onSay. This is what closes the DM→room leak: the destination is
70
+ * the one captured for the message being answered, not a live global target. */
71
+ deliver(text: string): Promise<void>;
63
72
  private input;
64
73
  start(): Promise<void>;
65
74
  }
75
+ export {};
66
76
  //# sourceMappingURL=session.d.ts.map
package/package.json CHANGED
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "name": "@agentvault/claude-bridge",
3
- "version": "0.2.0",
3
+ "version": "0.3.0",
4
4
  "type": "module",
5
- "description": "AgentVault Claude Room Bridge — daemon for bridging AI agents into secure E2E-encrypted AgentVault rooms.",
5
+ "description": "AgentVault Claude Bridge — daemon for bridging a Claude agent into secure E2E-encrypted AgentVault 1:1 direct messages and rooms.",
6
6
  "main": "dist/index.js",
7
7
  "types": "dist/index.d.ts",
8
8
  "bin": {