@agentvault/claude-bridge 0.3.1 → 0.3.3

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
@@ -9,17 +9,38 @@ export interface RoomMessage {
9
9
  export interface MessageMeta {
10
10
  roomId?: string;
11
11
  }
12
+ /** #392 room hush (advisory) — emitted by SecureChannel from the backend's
13
+ * `room_hushed` event. ``hushedUntil`` is an ISO string, or null when cleared. */
14
+ export interface RoomHushed {
15
+ roomId: string;
16
+ hushedUntil: string | null;
17
+ }
12
18
  export interface RoomChannel {
13
19
  on(ev: "room_message", cb: (e: RoomMessage) => void): unknown;
14
20
  on(ev: "message", cb: (text: string, metadata: MessageMeta) => void): unknown;
21
+ on(ev: "room_hushed", cb: (e: RoomHushed) => void): unknown;
15
22
  on(ev: "error", cb: (err: unknown) => void): unknown;
16
23
  sendToRoom(roomId: string, text: string): Promise<void>;
17
24
  send(text: string): Promise<void>;
18
25
  }
26
+ /**
27
+ * #392 cooperative quiet: tracks per-room hush windows so the native agent holds
28
+ * (does not room_say) while the owner has hushed the room. Advisory — the agent
29
+ * cooperates; the hard backstops are owner mute / remove (enforced server-side).
30
+ */
31
+ export declare class RoomHushState {
32
+ private until;
33
+ set(roomId: string, hushedUntilIso: string | null): void;
34
+ isHushed(roomId: string, now?: number): boolean;
35
+ }
19
36
  export interface RoomSession {
20
37
  /** `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;
38
+ * ActiveTarget.snapshotReply) — the session invokes it when Claude answers.
39
+ * `opts.autoReplyOnText` (set for 1:1 DMs) makes the session fall back to
40
+ * sending plain assistant text when the model never calls the say tool (#416). */
41
+ push(text: string, reply?: (text: string) => Promise<void>, opts?: {
42
+ autoReplyOnText?: boolean;
43
+ }): void;
23
44
  }
24
45
  /** Where a reply should go: a room (sendToRoom) or a 1:1 DM (send). */
25
46
  export type BridgeTarget = {
package/dist/index.js CHANGED
@@ -66743,6 +66743,19 @@ var init_channel = __esm({
66743
66743
  }
66744
66744
  });
66745
66745
  }
66746
+ if (data.event === "room_hushed") {
66747
+ this.emit("room_hushed", {
66748
+ roomId: data.data?.room_id,
66749
+ hushedUntil: data.data?.hushed_until ?? null
66750
+ });
66751
+ }
66752
+ if (data.event === "room_advisory") {
66753
+ this.emit("room_advisory", {
66754
+ roomId: data.data?.room_id,
66755
+ kind: data.data?.kind,
66756
+ message: data.data?.message
66757
+ });
66758
+ }
66746
66759
  if (data.event === "policy_blocked") {
66747
66760
  this.emit("policy_blocked", data.data);
66748
66761
  }
@@ -131541,14 +131554,25 @@ var PersistentClaudeSession = class {
131541
131554
  /** Reply sink bound to the message currently being processed. Set as each
131542
131555
  * message is handed to the model so the say tool routes to the right place. */
131543
131556
  activeReply;
131544
- push(text, reply) {
131557
+ /** Per-turn state for the #416 DM auto-reply fallback. */
131558
+ currentAutoReply = false;
131559
+ saidThisTurn = false;
131560
+ turnText = "";
131561
+ /**
131562
+ * Queue an inbound message for the model.
131563
+ * @param opts.autoReplyOnText — for 1:1 DMs (where the owner always expects a
131564
+ * reply): if the turn produces assistant text but never calls the say tool,
131565
+ * send that text as the reply instead of silently dropping it (#416). Rooms
131566
+ * omit this so the agent can still choose silence.
131567
+ */
131568
+ push(text, reply, opts) {
131545
131569
  const msg = {
131546
131570
  type: "user",
131547
131571
  message: { role: "user", content: text },
131548
131572
  parent_tool_use_id: null,
131549
131573
  session_id: ""
131550
131574
  };
131551
- const item = { msg, reply };
131575
+ const item = { msg, reply, autoReplyOnText: opts?.autoReplyOnText };
131552
131576
  if (this.waiting) {
131553
131577
  const w2 = this.waiting;
131554
131578
  this.waiting = null;
@@ -131561,21 +131585,19 @@ var PersistentClaudeSession = class {
131561
131585
  * back to opts.onSay. This is what closes the DM→room leak: the destination is
131562
131586
  * the one captured for the message being answered, not a live global target. */
131563
131587
  async deliver(text) {
131588
+ this.saidThisTurn = true;
131564
131589
  if (this.activeReply) await this.activeReply(text);
131565
131590
  else if (this.opts.onSay) await this.opts.onSay(text);
131566
131591
  }
131567
131592
  async *input() {
131568
131593
  while (true) {
131569
- if (this.pending.length > 0) {
131570
- const item2 = this.pending.shift();
131571
- this.activeReply = item2.reply;
131572
- yield item2.msg;
131573
- continue;
131574
- }
131575
- const item = await new Promise((resolve2) => {
131594
+ const item = this.pending.length > 0 ? this.pending.shift() : await new Promise((resolve2) => {
131576
131595
  this.waiting = resolve2;
131577
131596
  });
131578
131597
  this.activeReply = item.reply;
131598
+ this.currentAutoReply = item.autoReplyOnText ?? false;
131599
+ this.saidThisTurn = false;
131600
+ this.turnText = "";
131579
131601
  yield item.msg;
131580
131602
  }
131581
131603
  }
@@ -131622,13 +131644,38 @@ var PersistentClaudeSession = class {
131622
131644
  if (m6.type === "assistant") {
131623
131645
  const blocks = m6.message?.content ?? [];
131624
131646
  const text = blocks.filter((b5) => b5.type === "text").map((b5) => b5.text ?? "").join("");
131625
- if (text.trim()) this.opts.onObserve?.(text);
131647
+ if (text.trim()) {
131648
+ this.turnText += text;
131649
+ this.opts.onObserve?.(text);
131650
+ }
131651
+ } else if (m6.type === "result") {
131652
+ const reply = this.activeReply;
131653
+ if (this.currentAutoReply && !this.saidThisTurn && this.turnText.trim() && reply) {
131654
+ void reply(this.turnText);
131655
+ }
131626
131656
  }
131627
131657
  }
131628
131658
  }
131629
131659
  };
131630
131660
 
131631
131661
  // src/bridge.ts
131662
+ var RoomHushState = class {
131663
+ until = /* @__PURE__ */ new Map();
131664
+ set(roomId, hushedUntilIso) {
131665
+ if (!roomId) return;
131666
+ if (!hushedUntilIso) {
131667
+ this.until.delete(roomId);
131668
+ return;
131669
+ }
131670
+ const t7 = new Date(hushedUntilIso).getTime();
131671
+ if (Number.isNaN(t7)) return;
131672
+ this.until.set(roomId, t7);
131673
+ }
131674
+ isHushed(roomId, now = Date.now()) {
131675
+ const t7 = this.until.get(roomId);
131676
+ return t7 !== void 0 && now < t7;
131677
+ }
131678
+ };
131632
131679
  var ActiveTarget = class {
131633
131680
  target = null;
131634
131681
  setRoom(roomId) {
@@ -131696,17 +131743,30 @@ function wireBridge(channel, session, target, opts = {}) {
131696
131743
  const msg = err instanceof Error ? err.message : String(err);
131697
131744
  log(`channel error (auto-reconnecting): ${msg}`);
131698
131745
  });
131746
+ const hush = new RoomHushState();
131747
+ channel.on("room_hushed", (e7) => {
131748
+ hush.set(e7.roomId, e7.hushedUntil);
131749
+ log(
131750
+ e7.hushedUntil ? `room ${e7.roomId.slice(0, 8)} hushed until ${e7.hushedUntil} \u2014 holding` : `room ${e7.roomId.slice(0, 8)} hush cleared`
131751
+ );
131752
+ });
131699
131753
  channel.on("room_message", (e7) => {
131700
131754
  if (opts.roomFilter && e7.roomId !== opts.roomFilter) return;
131755
+ if (hush.isHushed(e7.roomId)) {
131756
+ log(`inbound from ${e7.senderName} in ${e7.roomId.slice(0, 8)} \u2014 room hushed, holding (not replying)`);
131757
+ return;
131758
+ }
131701
131759
  log(`inbound from ${e7.senderName} in ${e7.roomId.slice(0, 8)}`);
131702
131760
  target.setRoom(e7.roomId);
131703
- session.push(`[${e7.senderName}]: ${e7.plaintext}`, target.snapshotReply(channel, log));
131761
+ session.push(`[${e7.senderName}]: ${e7.plaintext}`, target.snapshotReply(channel, log), {
131762
+ autoReplyOnText: false
131763
+ });
131704
131764
  });
131705
131765
  channel.on("message", (text, metadata) => {
131706
131766
  if (metadata?.roomId) return;
131707
131767
  log("inbound 1:1 DM from owner");
131708
131768
  target.setDm();
131709
- session.push(text, target.snapshotReply(channel, log));
131769
+ session.push(text, target.snapshotReply(channel, log), { autoReplyOnText: true });
131710
131770
  });
131711
131771
  }
131712
131772
 
@@ -131742,7 +131802,7 @@ async function main() {
131742
131802
  });
131743
131803
  wireBridge(
131744
131804
  channel,
131745
- { push: (t7, reply) => session.push(t7, reply) },
131805
+ { push: (t7, reply, opts) => session.push(t7, reply, opts) },
131746
131806
  target,
131747
131807
  { roomFilter: cfg.roomFilter, log: (m6) => console.error("[bridge] " + m6) }
131748
131808
  );
package/dist/session.d.ts CHANGED
@@ -63,8 +63,21 @@ export declare class PersistentClaudeSession {
63
63
  /** Reply sink bound to the message currently being processed. Set as each
64
64
  * message is handed to the model so the say tool routes to the right place. */
65
65
  private activeReply?;
66
+ /** Per-turn state for the #416 DM auto-reply fallback. */
67
+ private currentAutoReply;
68
+ private saidThisTurn;
69
+ private turnText;
66
70
  constructor(opts: SessionOpts);
67
- push(text: string, reply?: ReplySink): void;
71
+ /**
72
+ * Queue an inbound message for the model.
73
+ * @param opts.autoReplyOnText — for 1:1 DMs (where the owner always expects a
74
+ * reply): if the turn produces assistant text but never calls the say tool,
75
+ * send that text as the reply instead of silently dropping it (#416). Rooms
76
+ * omit this so the agent can still choose silence.
77
+ */
78
+ push(text: string, reply?: ReplySink, opts?: {
79
+ autoReplyOnText?: boolean;
80
+ }): void;
68
81
  /** Route a say-tool message to the reply bound to the in-flight message, falling
69
82
  * back to opts.onSay. This is what closes the DM→room leak: the destination is
70
83
  * the one captured for the message being answered, not a live global target. */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agentvault/claude-bridge",
3
- "version": "0.3.1",
3
+ "version": "0.3.3",
4
4
  "type": "module",
5
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",