@agentvault/claude-bridge 0.2.1 → 0.3.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.
- package/dist/bridge.d.ts +49 -3
- package/dist/config.d.ts +2 -0
- package/dist/index.js +201 -35
- package/dist/session.d.ts +13 -3
- package/package.json +2 -2
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
|
-
|
|
13
|
-
|
|
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
package/dist/index.js
CHANGED
|
@@ -68497,6 +68497,21 @@ ${messageText}`;
|
|
|
68497
68497
|
return;
|
|
68498
68498
|
}
|
|
68499
68499
|
}
|
|
68500
|
+
if (welcomeRoomId && this._persisted) {
|
|
68501
|
+
await this._registerRoomFromWelcome(
|
|
68502
|
+
welcomeRoomId,
|
|
68503
|
+
groupId,
|
|
68504
|
+
mgr,
|
|
68505
|
+
data.room_name
|
|
68506
|
+
);
|
|
68507
|
+
this._pendingMlsKpBundle = void 0;
|
|
68508
|
+
await releaseA2ASyncLock(this.config.dataDir, `room-kp-${groupId}`).catch(() => {
|
|
68509
|
+
});
|
|
68510
|
+
console.log(
|
|
68511
|
+
`[SecureChannel] Registered room ${welcomeRoomId.slice(0, 8)} from unmatched Welcome (epoch=${mgr.epoch})`
|
|
68512
|
+
);
|
|
68513
|
+
return;
|
|
68514
|
+
}
|
|
68500
68515
|
const a2aChannelId = data.a2a_channel_id;
|
|
68501
68516
|
for (const [channelId, entry] of Object.entries(this._persisted?.a2aChannels ?? {})) {
|
|
68502
68517
|
if (entry.mlsGroupId === groupId || channelId === a2aChannelId) {
|
|
@@ -68553,6 +68568,33 @@ ${messageText}`;
|
|
|
68553
68568
|
throw err;
|
|
68554
68569
|
}
|
|
68555
68570
|
}
|
|
68571
|
+
/**
|
|
68572
|
+
* Self-bootstrap a room entry + MLS group mapping from a Welcome whose room is
|
|
68573
|
+
* not yet persisted. Happens when we were added to an EXISTING room and the
|
|
68574
|
+
* `room_joined` that registers the room raced behind (or was lost relative to)
|
|
68575
|
+
* the Welcome. Without a room entry the Welcome room-match loop can't match, and
|
|
68576
|
+
* every subsequent room message is dropped with "No room found for MLS group".
|
|
68577
|
+
* Mirrors the A2A self-bootstrap fallback. The backend now also sends the new
|
|
68578
|
+
* device its own `room_joined` (PR #412); this is defense-in-depth for the race
|
|
68579
|
+
* where that delivery is missed. A later `room_joined` enriches name/members.
|
|
68580
|
+
*/
|
|
68581
|
+
async _registerRoomFromWelcome(roomId, groupId, mgr, roomName) {
|
|
68582
|
+
if (!this._persisted) return;
|
|
68583
|
+
if (!this._persisted.rooms) this._persisted.rooms = {};
|
|
68584
|
+
const existing = this._persisted.rooms[roomId];
|
|
68585
|
+
this._persisted.rooms[roomId] = {
|
|
68586
|
+
roomId,
|
|
68587
|
+
name: existing?.name ?? (roomName ?? ""),
|
|
68588
|
+
conversationIds: existing?.conversationIds ?? [],
|
|
68589
|
+
members: existing?.members ?? [],
|
|
68590
|
+
mlsGroupId: groupId,
|
|
68591
|
+
lastMlsMessageTs: existing?.lastMlsMessageTs
|
|
68592
|
+
};
|
|
68593
|
+
this._mlsGroups.set(roomId, mgr);
|
|
68594
|
+
await saveMlsState(this.config.dataDir, groupId, JSON.stringify(mgr.exportState()));
|
|
68595
|
+
await this._persistState();
|
|
68596
|
+
await this._applyBufferedMlsCommits(groupId, mgr);
|
|
68597
|
+
}
|
|
68556
68598
|
/**
|
|
68557
68599
|
* Pull pending MLS messages from the delivery queue and process them.
|
|
68558
68600
|
* Called on WS connect, on mls_delivery ping, and every 30s heartbeat.
|
|
@@ -69967,6 +70009,40 @@ var init_account_config = __esm({
|
|
|
69967
70009
|
DEFAULT_HTTP_PORT = 18790;
|
|
69968
70010
|
}
|
|
69969
70011
|
});
|
|
70012
|
+
function terminalReenrollMessage(agentName, reason) {
|
|
70013
|
+
const who = agentName ? `[${agentName}] ` : "";
|
|
70014
|
+
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.`;
|
|
70015
|
+
}
|
|
70016
|
+
function attachLifecycle(channel, opts = {}) {
|
|
70017
|
+
const log = opts.log ?? (() => {
|
|
70018
|
+
});
|
|
70019
|
+
const agentName = opts.agentName ?? "";
|
|
70020
|
+
const onTerminal = opts.onTerminal ?? (() => {
|
|
70021
|
+
});
|
|
70022
|
+
let fired = false;
|
|
70023
|
+
const terminal = (reason) => {
|
|
70024
|
+
if (fired) return;
|
|
70025
|
+
fired = true;
|
|
70026
|
+
log(terminalReenrollMessage(agentName, reason));
|
|
70027
|
+
void Promise.resolve(onTerminal(reason)).catch(() => {
|
|
70028
|
+
});
|
|
70029
|
+
};
|
|
70030
|
+
channel.on("auth_failed", (e7) => terminal(e7.reason));
|
|
70031
|
+
channel.on(
|
|
70032
|
+
"session_replaced",
|
|
70033
|
+
(e7) => terminal(e7?.code ? `session_replaced (${e7.code})` : "session_replaced")
|
|
70034
|
+
);
|
|
70035
|
+
channel.on("error", (err) => {
|
|
70036
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
70037
|
+
if (/reconnect loop detected/i.test(msg)) terminal("reconnect_loop");
|
|
70038
|
+
else if (/device (was )?revoked/i.test(msg)) terminal("device_revoked");
|
|
70039
|
+
});
|
|
70040
|
+
}
|
|
70041
|
+
var init_lifecycle = __esm({
|
|
70042
|
+
"src/lifecycle.ts"() {
|
|
70043
|
+
"use strict";
|
|
70044
|
+
}
|
|
70045
|
+
});
|
|
69970
70046
|
async function _handleInbound(params) {
|
|
69971
70047
|
const { plaintext, metadata, channel, account, cfg } = params;
|
|
69972
70048
|
const core = _ocRuntime;
|
|
@@ -70051,6 +70127,7 @@ var init_openclaw_plugin = __esm({
|
|
|
70051
70127
|
await init_channel();
|
|
70052
70128
|
init_account_config();
|
|
70053
70129
|
init_openclaw_compat();
|
|
70130
|
+
init_lifecycle();
|
|
70054
70131
|
_ocRuntime = null;
|
|
70055
70132
|
_channels = /* @__PURE__ */ new Map();
|
|
70056
70133
|
agentVaultPlugin = {
|
|
@@ -70108,6 +70185,7 @@ var init_openclaw_plugin = __esm({
|
|
|
70108
70185
|
const dataDir = resolve(account.dataDir.replace(/^~/, process.env.HOME ?? "~"));
|
|
70109
70186
|
_log?.(`[AgentVault] starting channel (dataDir=${dataDir})`);
|
|
70110
70187
|
await new Promise((resolvePromise, reject) => {
|
|
70188
|
+
let terminalStopped = false;
|
|
70111
70189
|
const channel = new SecureChannel({
|
|
70112
70190
|
inviteToken: "",
|
|
70113
70191
|
dataDir,
|
|
@@ -70126,13 +70204,29 @@ var init_openclaw_plugin = __esm({
|
|
|
70126
70204
|
},
|
|
70127
70205
|
onStateChange: (state) => {
|
|
70128
70206
|
_log?.(`[AgentVault] state \u2192 ${state}`);
|
|
70129
|
-
if (state === "error")
|
|
70207
|
+
if (state === "error") {
|
|
70208
|
+
queueMicrotask(() => {
|
|
70209
|
+
if (!terminalStopped) reject(new Error("AgentVault channel permanent error"));
|
|
70210
|
+
});
|
|
70211
|
+
}
|
|
70130
70212
|
}
|
|
70131
70213
|
});
|
|
70132
70214
|
_channels.set(account.accountId, channel);
|
|
70133
70215
|
channel.on("error", (err) => {
|
|
70134
70216
|
_log?.(`[AgentVault] channel error (non-fatal): ${String(err)}`);
|
|
70135
70217
|
});
|
|
70218
|
+
attachLifecycle(channel, {
|
|
70219
|
+
agentName: account.agentName,
|
|
70220
|
+
log: (m22) => _log?.(`[AgentVault] ${m22}`),
|
|
70221
|
+
onTerminal: async () => {
|
|
70222
|
+
terminalStopped = true;
|
|
70223
|
+
try {
|
|
70224
|
+
await channel.stop();
|
|
70225
|
+
} catch {
|
|
70226
|
+
}
|
|
70227
|
+
_channels.delete(account.accountId);
|
|
70228
|
+
}
|
|
70229
|
+
});
|
|
70136
70230
|
const httpPort = account.httpPort;
|
|
70137
70231
|
channel.on("ready", () => {
|
|
70138
70232
|
channel.startHttpServer(httpPort);
|
|
@@ -70143,7 +70237,9 @@ var init_openclaw_plugin = __esm({
|
|
|
70143
70237
|
_channels.delete(account.accountId);
|
|
70144
70238
|
resolvePromise();
|
|
70145
70239
|
});
|
|
70146
|
-
channel.start().catch(
|
|
70240
|
+
channel.start().catch((e7) => {
|
|
70241
|
+
if (!terminalStopped) reject(e7);
|
|
70242
|
+
});
|
|
70147
70243
|
});
|
|
70148
70244
|
return { stop: async () => {
|
|
70149
70245
|
} };
|
|
@@ -70262,6 +70358,7 @@ var init_openclaw_entry = __esm({
|
|
|
70262
70358
|
init_fetch_interceptor();
|
|
70263
70359
|
init_http_handlers();
|
|
70264
70360
|
init_openclaw_compat();
|
|
70361
|
+
init_lifecycle();
|
|
70265
70362
|
init_types();
|
|
70266
70363
|
isUsingManagedRoutes = false;
|
|
70267
70364
|
}
|
|
@@ -96325,8 +96422,19 @@ var CRED_FILES = ["agentvault.json", "secure-channel.json"];
|
|
|
96325
96422
|
function hasPersistedCreds(dataDir) {
|
|
96326
96423
|
return CRED_FILES.some((f7) => existsSync(join5(dataDir, f7)));
|
|
96327
96424
|
}
|
|
96425
|
+
function slugify2(name) {
|
|
96426
|
+
return name.toLowerCase().replace(/[^a-z0-9-]/g, "-");
|
|
96427
|
+
}
|
|
96428
|
+
function resolveDataDir(env) {
|
|
96429
|
+
if (env.AV_DATA_DIR) return { dataDir: env.AV_DATA_DIR, source: "explicit" };
|
|
96430
|
+
const base = join5(env.HOME ?? "", ".agentvault", "claude-room-bridge");
|
|
96431
|
+
const perAgent = join5(base, slugify2(env.AV_AGENT_NAME ?? "claude"));
|
|
96432
|
+
if (hasPersistedCreds(perAgent)) return { dataDir: perAgent, source: "per-agent" };
|
|
96433
|
+
if (hasPersistedCreds(base)) return { dataDir: base, source: "legacy" };
|
|
96434
|
+
return { dataDir: perAgent, source: "per-agent" };
|
|
96435
|
+
}
|
|
96328
96436
|
function loadConfig(env, argv = []) {
|
|
96329
|
-
const dataDir
|
|
96437
|
+
const { dataDir, source: dataDirSource } = resolveDataDir(env);
|
|
96330
96438
|
const inviteToken = (argv[0] && !argv[0].startsWith("-") ? argv[0] : "") || env.AV_INVITE_TOKEN || "";
|
|
96331
96439
|
if (!inviteToken && !hasPersistedCreds(dataDir)) {
|
|
96332
96440
|
throw new Error(
|
|
@@ -96336,6 +96444,7 @@ function loadConfig(env, argv = []) {
|
|
|
96336
96444
|
return {
|
|
96337
96445
|
inviteToken,
|
|
96338
96446
|
dataDir,
|
|
96447
|
+
dataDirSource,
|
|
96339
96448
|
apiUrl: env.AV_API_URL ?? "https://api.agentvault.chat",
|
|
96340
96449
|
// Identity shown to the agent's own session. Set this to the agent's
|
|
96341
96450
|
// AgentVault name via AV_AGENT_NAME (the native connect command passes it).
|
|
@@ -118294,7 +118403,7 @@ __export(util_exports2, {
|
|
|
118294
118403
|
required: () => required2,
|
|
118295
118404
|
safeExtend: () => safeExtend2,
|
|
118296
118405
|
shallowClone: () => shallowClone2,
|
|
118297
|
-
slugify: () =>
|
|
118406
|
+
slugify: () => slugify3,
|
|
118298
118407
|
stringifyPrimitive: () => stringifyPrimitive2,
|
|
118299
118408
|
uint8ArrayToBase64: () => uint8ArrayToBase643,
|
|
118300
118409
|
uint8ArrayToBase64url: () => uint8ArrayToBase64url2,
|
|
@@ -118435,7 +118544,7 @@ function randomString2(length = 10) {
|
|
|
118435
118544
|
function esc2(str) {
|
|
118436
118545
|
return JSON.stringify(str);
|
|
118437
118546
|
}
|
|
118438
|
-
function
|
|
118547
|
+
function slugify3(input) {
|
|
118439
118548
|
return input.toLowerCase().trim().replace(/[^\w\s-]/g, "").replace(/[\s_-]+/g, "-").replace(/^-+|-+$/g, "");
|
|
118440
118549
|
}
|
|
118441
118550
|
var captureStackTrace2 = "captureStackTrace" in Error ? Error.captureStackTrace : (..._args) => {
|
|
@@ -128169,7 +128278,7 @@ function _toUpperCase2() {
|
|
|
128169
128278
|
}
|
|
128170
128279
|
// @__NO_SIDE_EFFECTS__
|
|
128171
128280
|
function _slugify2() {
|
|
128172
|
-
return /* @__PURE__ */ _overwrite2((input) =>
|
|
128281
|
+
return /* @__PURE__ */ _overwrite2((input) => slugify3(input));
|
|
128173
128282
|
}
|
|
128174
128283
|
// @__NO_SIDE_EFFECTS__
|
|
128175
128284
|
function _array2(Class3, element, params) {
|
|
@@ -131413,11 +131522,11 @@ config2(en_default3());
|
|
|
131413
131522
|
function makeRoomSayTool(onSay) {
|
|
131414
131523
|
return bs(
|
|
131415
131524
|
"say",
|
|
131416
|
-
"Send a message to the current AgentVault
|
|
131417
|
-
{ message: external_exports.string().describe("The message to
|
|
131525
|
+
"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.",
|
|
131526
|
+
{ message: external_exports.string().describe("The message to send to the current conversation.") },
|
|
131418
131527
|
async (args) => {
|
|
131419
131528
|
await onSay(args.message);
|
|
131420
|
-
return { content: [{ type: "text", text: "Message sent
|
|
131529
|
+
return { content: [{ type: "text", text: "Message sent." }] };
|
|
131421
131530
|
}
|
|
131422
131531
|
);
|
|
131423
131532
|
}
|
|
@@ -131429,37 +131538,52 @@ var PersistentClaudeSession = class {
|
|
|
131429
131538
|
opts;
|
|
131430
131539
|
waiting = null;
|
|
131431
131540
|
pending = [];
|
|
131432
|
-
|
|
131541
|
+
/** Reply sink bound to the message currently being processed. Set as each
|
|
131542
|
+
* message is handed to the model so the say tool routes to the right place. */
|
|
131543
|
+
activeReply;
|
|
131544
|
+
push(text, reply) {
|
|
131433
131545
|
const msg = {
|
|
131434
131546
|
type: "user",
|
|
131435
131547
|
message: { role: "user", content: text },
|
|
131436
131548
|
parent_tool_use_id: null,
|
|
131437
131549
|
session_id: ""
|
|
131438
131550
|
};
|
|
131551
|
+
const item = { msg, reply };
|
|
131439
131552
|
if (this.waiting) {
|
|
131440
131553
|
const w2 = this.waiting;
|
|
131441
131554
|
this.waiting = null;
|
|
131442
|
-
w2(
|
|
131555
|
+
w2(item);
|
|
131443
131556
|
} else {
|
|
131444
|
-
this.pending.push(
|
|
131557
|
+
this.pending.push(item);
|
|
131445
131558
|
}
|
|
131446
131559
|
}
|
|
131560
|
+
/** Route a say-tool message to the reply bound to the in-flight message, falling
|
|
131561
|
+
* back to opts.onSay. This is what closes the DM→room leak: the destination is
|
|
131562
|
+
* the one captured for the message being answered, not a live global target. */
|
|
131563
|
+
async deliver(text) {
|
|
131564
|
+
if (this.activeReply) await this.activeReply(text);
|
|
131565
|
+
else if (this.opts.onSay) await this.opts.onSay(text);
|
|
131566
|
+
}
|
|
131447
131567
|
async *input() {
|
|
131448
131568
|
while (true) {
|
|
131449
131569
|
if (this.pending.length > 0) {
|
|
131450
|
-
|
|
131570
|
+
const item2 = this.pending.shift();
|
|
131571
|
+
this.activeReply = item2.reply;
|
|
131572
|
+
yield item2.msg;
|
|
131451
131573
|
continue;
|
|
131452
131574
|
}
|
|
131453
|
-
|
|
131575
|
+
const item = await new Promise((resolve2) => {
|
|
131454
131576
|
this.waiting = resolve2;
|
|
131455
131577
|
});
|
|
131578
|
+
this.activeReply = item.reply;
|
|
131579
|
+
yield item.msg;
|
|
131456
131580
|
}
|
|
131457
131581
|
}
|
|
131458
131582
|
async start() {
|
|
131459
131583
|
const roomServer = _s({
|
|
131460
131584
|
name: "room",
|
|
131461
131585
|
version: "0.2.0",
|
|
131462
|
-
tools: [makeRoomSayTool(this.
|
|
131586
|
+
tools: [makeRoomSayTool((text) => this.deliver(text))]
|
|
131463
131587
|
});
|
|
131464
131588
|
const sdkOptions = {
|
|
131465
131589
|
model: this.opts.model,
|
|
@@ -131505,7 +131629,47 @@ var PersistentClaudeSession = class {
|
|
|
131505
131629
|
};
|
|
131506
131630
|
|
|
131507
131631
|
// src/bridge.ts
|
|
131508
|
-
|
|
131632
|
+
var ActiveTarget = class {
|
|
131633
|
+
target = null;
|
|
131634
|
+
setRoom(roomId) {
|
|
131635
|
+
this.target = { kind: "room", roomId };
|
|
131636
|
+
}
|
|
131637
|
+
setDm() {
|
|
131638
|
+
this.target = { kind: "dm" };
|
|
131639
|
+
}
|
|
131640
|
+
get current() {
|
|
131641
|
+
return this.target;
|
|
131642
|
+
}
|
|
131643
|
+
async reply(channel, text) {
|
|
131644
|
+
const t7 = this.target;
|
|
131645
|
+
if (!t7) return;
|
|
131646
|
+
if (t7.kind === "room") await channel.sendToRoom(t7.roomId, text);
|
|
131647
|
+
else await channel.send(text);
|
|
131648
|
+
}
|
|
131649
|
+
/**
|
|
131650
|
+
* Capture the CURRENT target into an immutable reply closure. This is the leak
|
|
131651
|
+
* fix: a reply built here routes to where its inbound message came from, even if
|
|
131652
|
+
* a later inbound flips the live `current` target. The bridge captures one of
|
|
131653
|
+
* these per inbound (at push time) and hands it to the session, so a room message
|
|
131654
|
+
* arriving mid-compose can never redirect a private 1:1 reply into the room.
|
|
131655
|
+
*/
|
|
131656
|
+
snapshotReply(channel, log = () => {
|
|
131657
|
+
}) {
|
|
131658
|
+
const t7 = this.target;
|
|
131659
|
+
const where = !t7 ? "nowhere" : t7.kind === "room" ? `room ${t7.roomId.slice(0, 8)}` : "1:1 owner";
|
|
131660
|
+
return async (text) => {
|
|
131661
|
+
if (!t7) return;
|
|
131662
|
+
try {
|
|
131663
|
+
if (t7.kind === "room") await channel.sendToRoom(t7.roomId, text);
|
|
131664
|
+
else await channel.send(text);
|
|
131665
|
+
log(`said to ${where}`);
|
|
131666
|
+
} catch (err) {
|
|
131667
|
+
log(`send failed to ${where}: ${err instanceof Error ? err.message : String(err)}`);
|
|
131668
|
+
}
|
|
131669
|
+
};
|
|
131670
|
+
}
|
|
131671
|
+
};
|
|
131672
|
+
function attachLifecycle2(channel, opts = {}) {
|
|
131509
131673
|
const log = opts.log ?? (() => {
|
|
131510
131674
|
});
|
|
131511
131675
|
const onTerminal = opts.onTerminal ?? (() => process.exit(1));
|
|
@@ -131525,7 +131689,7 @@ function attachLifecycle(channel, opts = {}) {
|
|
|
131525
131689
|
if (/reconnect loop detected/i.test(msg)) terminal("reconnect_loop");
|
|
131526
131690
|
});
|
|
131527
131691
|
}
|
|
131528
|
-
function wireBridge(channel, session, opts = {}) {
|
|
131692
|
+
function wireBridge(channel, session, target, opts = {}) {
|
|
131529
131693
|
const log = opts.log ?? (() => {
|
|
131530
131694
|
});
|
|
131531
131695
|
channel.on("error", (err) => {
|
|
@@ -131535,8 +131699,14 @@ function wireBridge(channel, session, opts = {}) {
|
|
|
131535
131699
|
channel.on("room_message", (e7) => {
|
|
131536
131700
|
if (opts.roomFilter && e7.roomId !== opts.roomFilter) return;
|
|
131537
131701
|
log(`inbound from ${e7.senderName} in ${e7.roomId.slice(0, 8)}`);
|
|
131538
|
-
|
|
131539
|
-
session.push(`[${e7.senderName}]: ${e7.plaintext}
|
|
131702
|
+
target.setRoom(e7.roomId);
|
|
131703
|
+
session.push(`[${e7.senderName}]: ${e7.plaintext}`, target.snapshotReply(channel, log));
|
|
131704
|
+
});
|
|
131705
|
+
channel.on("message", (text, metadata) => {
|
|
131706
|
+
if (metadata?.roomId) return;
|
|
131707
|
+
log("inbound 1:1 DM from owner");
|
|
131708
|
+
target.setDm();
|
|
131709
|
+
session.push(text, target.snapshotReply(channel, log));
|
|
131540
131710
|
});
|
|
131541
131711
|
}
|
|
131542
131712
|
|
|
@@ -131549,7 +131719,9 @@ async function main() {
|
|
|
131549
131719
|
"[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"
|
|
131550
131720
|
);
|
|
131551
131721
|
}
|
|
131552
|
-
|
|
131722
|
+
console.error(`[bridge] data dir: ${cfg.dataDir} (${cfg.dataDirSource})`);
|
|
131723
|
+
const target = new ActiveTarget();
|
|
131724
|
+
if (cfg.roomFilter) target.setRoom(cfg.roomFilter);
|
|
131553
131725
|
const channel = new SecureChannel({
|
|
131554
131726
|
inviteToken: cfg.inviteToken,
|
|
131555
131727
|
dataDir: cfg.dataDir,
|
|
@@ -131559,28 +131731,22 @@ async function main() {
|
|
|
131559
131731
|
});
|
|
131560
131732
|
const session = new PersistentClaudeSession({
|
|
131561
131733
|
model: cfg.model,
|
|
131562
|
-
systemPrompt: cfg.systemPrompt ?? `You are ${cfg.agentName}, an AI agent
|
|
131563
|
-
// Claude speaks by calling
|
|
131564
|
-
|
|
131565
|
-
|
|
131566
|
-
|
|
131567
|
-
|
|
131568
|
-
console.error(`[bridge] said in ${activeRoomId.slice(0, 8)}`);
|
|
131569
|
-
} catch (err) {
|
|
131570
|
-
console.error(`[bridge] send failed to ${activeRoomId.slice(0, 8)}:`, err);
|
|
131571
|
-
}
|
|
131572
|
-
},
|
|
131734
|
+
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.`,
|
|
131735
|
+
// Claude speaks by calling the say tool → the session routes it to the reply
|
|
131736
|
+
// bound to the message being answered (wireBridge captures that per inbound via
|
|
131737
|
+
// ActiveTarget.snapshotReply, which also logs the "said to …" line). This
|
|
131738
|
+
// per-message binding is what prevents a private 1:1 reply from leaking into a
|
|
131739
|
+
// room when room traffic arrives mid-compose.
|
|
131573
131740
|
// Assistant reasoning that wasn't sent — log a short trace only.
|
|
131574
131741
|
onObserve: (text) => console.error(`[bridge] observed (${text.length} chars, not sent)`)
|
|
131575
131742
|
});
|
|
131576
131743
|
wireBridge(
|
|
131577
131744
|
channel,
|
|
131578
|
-
{ push: (t7) => session.push(t7
|
|
131579
|
-
|
|
131580
|
-
} },
|
|
131745
|
+
{ push: (t7, reply) => session.push(t7, reply) },
|
|
131746
|
+
target,
|
|
131581
131747
|
{ roomFilter: cfg.roomFilter, log: (m6) => console.error("[bridge] " + m6) }
|
|
131582
131748
|
);
|
|
131583
|
-
|
|
131749
|
+
attachLifecycle2(channel, {
|
|
131584
131750
|
log: (m6) => console.error("[bridge] " + m6)
|
|
131585
131751
|
});
|
|
131586
131752
|
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
|
-
/**
|
|
50
|
-
|
|
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.
|
|
3
|
+
"version": "0.3.1",
|
|
4
4
|
"type": "module",
|
|
5
|
-
"description": "AgentVault Claude
|
|
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": {
|