@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 +23 -2
- package/dist/index.js +73 -13
- package/dist/session.d.ts +14 -1
- package/package.json +1 -1
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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())
|
|
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
|
-
|
|
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.
|
|
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",
|