@agentvault/claude-bridge 0.5.7 → 0.5.8
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/index.js +167 -22
- package/dist/router.d.ts +30 -0
- package/dist/session.d.ts +11 -2
- package/dist/worker-queue.d.ts +41 -0
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -132437,6 +132437,8 @@ var PersistentClaudeSession = class {
|
|
|
132437
132437
|
turnText = "";
|
|
132438
132438
|
/** In-process room MCP server — hoisted so buildSdkOptions can reference it. */
|
|
132439
132439
|
roomServer;
|
|
132440
|
+
/** AbortController for the in-flight query(); abort() triggers it (queue timeout). */
|
|
132441
|
+
_abort;
|
|
132440
132442
|
/**
|
|
132441
132443
|
* Queue an inbound message for the model.
|
|
132442
132444
|
* @param opts.autoReplyOnText — for 1:1 DMs (where the owner always expects a
|
|
@@ -132473,11 +132475,22 @@ var PersistentClaudeSession = class {
|
|
|
132473
132475
|
if (this.activeReply) await this.activeReply(text);
|
|
132474
132476
|
else if (this.opts.onSay) await this.opts.onSay(text);
|
|
132475
132477
|
}
|
|
132478
|
+
/** Abort the in-flight query() — used by the worker queue on a per-task timeout. */
|
|
132479
|
+
abort() {
|
|
132480
|
+
this._abort?.abort();
|
|
132481
|
+
}
|
|
132476
132482
|
async *input() {
|
|
132477
132483
|
while (true) {
|
|
132478
|
-
|
|
132479
|
-
|
|
132480
|
-
|
|
132484
|
+
let item;
|
|
132485
|
+
if (this.pending.length > 0) {
|
|
132486
|
+
item = this.pending.shift();
|
|
132487
|
+
} else if (this.opts.ephemeral) {
|
|
132488
|
+
return;
|
|
132489
|
+
} else {
|
|
132490
|
+
item = await new Promise((resolve3) => {
|
|
132491
|
+
this.waiting = resolve3;
|
|
132492
|
+
});
|
|
132493
|
+
}
|
|
132481
132494
|
this.activeReply = item.reply;
|
|
132482
132495
|
this.currentAutoReply = item.autoReplyOnText ?? false;
|
|
132483
132496
|
this.currentArmedGetter = item.armed ?? (() => false);
|
|
@@ -132538,8 +132551,28 @@ var PersistentClaudeSession = class {
|
|
|
132538
132551
|
return {
|
|
132539
132552
|
...base,
|
|
132540
132553
|
permissionMode: this.opts.permissionMode ?? "auto",
|
|
132541
|
-
|
|
132554
|
+
// FACET B (disk isolation). The workspace is writable under the Facet-A
|
|
132555
|
+
// allowlist, so a prior worker/armed turn could plant config files in it that
|
|
132556
|
+
// a later worker would otherwise ingest. We close each disk surface explicitly
|
|
132557
|
+
// (they are governed SEPARATELY by the SDK, so one flag is not enough):
|
|
132558
|
+
// - settingSources:[] — "disable filesystem settings (SDK isolation mode)":
|
|
132559
|
+
// no ~/.claude or project settings.json (⇒ no settings-defined hooks, the
|
|
132560
|
+
// RCE-grade vector: a hook is a shell command run OUTSIDE the Bash gate),
|
|
132561
|
+
// no CLAUDE.md. It also means no .mcp.json APPROVAL state is loaded — and
|
|
132562
|
+
// enableAllProjectMcpServers/enabledMcpjsonServers live only in Settings —
|
|
132563
|
+
// so an unapproved project .mcp.json server is never spawned in headless
|
|
132564
|
+
// mode. Any MCP tool that WERE discovered is still denied by the Facet-A
|
|
132565
|
+
// allowlist (mcp__* is not room_say/a file tool/Bash → final deny).
|
|
132566
|
+
// - skills:[] — settingSources does NOT gate skills discovery; [] enables
|
|
132567
|
+
// zero skills, so a planted <ws>/.claude/skills/*/SKILL.md description
|
|
132568
|
+
// cannot be injected into the worker's context.
|
|
132569
|
+
// Project context, if needed, is injected via the bridge-controlled systemPrompt,
|
|
132570
|
+
// never auto-loaded from the mutable workspace.
|
|
132571
|
+
settingSources: [],
|
|
132572
|
+
skills: [],
|
|
132542
132573
|
cwd: this.opts.workspaceDir,
|
|
132574
|
+
...this.opts.maxTurns != null ? { maxTurns: this.opts.maxTurns } : {},
|
|
132575
|
+
abortController: this._abort,
|
|
132543
132576
|
// PRIMARY gate: a PreToolUse hook fires on EVERY tool call regardless of
|
|
132544
132577
|
// permissionMode. canUseTool alone is bypassed in "auto" mode (the SDK's
|
|
132545
132578
|
// classifier auto-approves without hitting the "ask" path) — verified live.
|
|
@@ -132549,6 +132582,7 @@ var PersistentClaudeSession = class {
|
|
|
132549
132582
|
};
|
|
132550
132583
|
}
|
|
132551
132584
|
async start() {
|
|
132585
|
+
this._abort = new AbortController();
|
|
132552
132586
|
this.roomServer = _s({
|
|
132553
132587
|
name: "room",
|
|
132554
132588
|
version: "0.2.0",
|
|
@@ -132580,6 +132614,96 @@ var PersistentClaudeSession = class {
|
|
|
132580
132614
|
}
|
|
132581
132615
|
};
|
|
132582
132616
|
|
|
132617
|
+
// src/worker-queue.ts
|
|
132618
|
+
var GENERIC_ERROR = "Sorry \u2014 I couldn't complete that request.";
|
|
132619
|
+
var WorkerQueue = class {
|
|
132620
|
+
constructor(deps) {
|
|
132621
|
+
this.deps = deps;
|
|
132622
|
+
}
|
|
132623
|
+
deps;
|
|
132624
|
+
q = [];
|
|
132625
|
+
loop = Promise.resolve();
|
|
132626
|
+
running = false;
|
|
132627
|
+
enqueue(task) {
|
|
132628
|
+
this.q.push(task);
|
|
132629
|
+
if (!this.running) {
|
|
132630
|
+
this.running = true;
|
|
132631
|
+
this.loop = this.drain();
|
|
132632
|
+
}
|
|
132633
|
+
}
|
|
132634
|
+
/** Resolves when the queue has processed everything enqueued so far. */
|
|
132635
|
+
whenDrained() {
|
|
132636
|
+
return this.loop;
|
|
132637
|
+
}
|
|
132638
|
+
async drain() {
|
|
132639
|
+
try {
|
|
132640
|
+
while (this.q.length > 0) {
|
|
132641
|
+
const task = this.q.shift();
|
|
132642
|
+
await this.runOne(task);
|
|
132643
|
+
}
|
|
132644
|
+
} finally {
|
|
132645
|
+
this.running = false;
|
|
132646
|
+
}
|
|
132647
|
+
}
|
|
132648
|
+
async runOne(task) {
|
|
132649
|
+
let session;
|
|
132650
|
+
let timer;
|
|
132651
|
+
try {
|
|
132652
|
+
session = this.deps.makeSession(task);
|
|
132653
|
+
session.push(task.instruction, task.reply, {
|
|
132654
|
+
autoReplyOnText: task.autoReplyOnText,
|
|
132655
|
+
armed: task.armed
|
|
132656
|
+
});
|
|
132657
|
+
const currentSession = session;
|
|
132658
|
+
const timeout = new Promise((_resolve, reject) => {
|
|
132659
|
+
timer = setTimeout(() => {
|
|
132660
|
+
currentSession.abort();
|
|
132661
|
+
reject(new Error("worker task timeout"));
|
|
132662
|
+
}, this.deps.timeoutMs);
|
|
132663
|
+
});
|
|
132664
|
+
await Promise.race([session.start(), timeout]);
|
|
132665
|
+
} catch (e7) {
|
|
132666
|
+
this.deps.log(`[worker-queue] task failed: ${e7.message}`);
|
|
132667
|
+
session?.abort();
|
|
132668
|
+
try {
|
|
132669
|
+
await task.reply(GENERIC_ERROR);
|
|
132670
|
+
} catch (replyErr) {
|
|
132671
|
+
this.deps.log(`[worker-queue] failed to deliver error reply: ${replyErr.message}`);
|
|
132672
|
+
}
|
|
132673
|
+
} finally {
|
|
132674
|
+
if (timer) clearTimeout(timer);
|
|
132675
|
+
}
|
|
132676
|
+
}
|
|
132677
|
+
};
|
|
132678
|
+
|
|
132679
|
+
// src/router.ts
|
|
132680
|
+
function makeRouter(deps) {
|
|
132681
|
+
return {
|
|
132682
|
+
push(text, reply, opts) {
|
|
132683
|
+
const replySink = reply ?? (() => {
|
|
132684
|
+
});
|
|
132685
|
+
if (deps.worker) {
|
|
132686
|
+
const isOwnerDm = opts?.autoReplyOnText === true;
|
|
132687
|
+
const isArmedRoom = opts?.armed?.() === true;
|
|
132688
|
+
if (isOwnerDm) {
|
|
132689
|
+
deps.queue.enqueue({ instruction: text, reply: replySink, autoReplyOnText: true });
|
|
132690
|
+
return;
|
|
132691
|
+
}
|
|
132692
|
+
if (isArmedRoom) {
|
|
132693
|
+
deps.queue.enqueue({
|
|
132694
|
+
instruction: text,
|
|
132695
|
+
reply: replySink,
|
|
132696
|
+
autoReplyOnText: false,
|
|
132697
|
+
armed: opts.armed
|
|
132698
|
+
});
|
|
132699
|
+
return;
|
|
132700
|
+
}
|
|
132701
|
+
}
|
|
132702
|
+
deps.listener.push(text, reply, opts);
|
|
132703
|
+
}
|
|
132704
|
+
};
|
|
132705
|
+
}
|
|
132706
|
+
|
|
132583
132707
|
// src/arming.ts
|
|
132584
132708
|
var ArmingState = class {
|
|
132585
132709
|
armed = /* @__PURE__ */ new Set();
|
|
@@ -132943,7 +133067,7 @@ async function main() {
|
|
|
132943
133067
|
"[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"
|
|
132944
133068
|
);
|
|
132945
133069
|
}
|
|
132946
|
-
console.error(`[bridge] version: ${true ? "0.5.
|
|
133070
|
+
console.error(`[bridge] version: ${true ? "0.5.8" : "dev"}`);
|
|
132947
133071
|
console.error(`[bridge] data dir: ${cfg.dataDir} (${cfg.dataDirSource})`);
|
|
132948
133072
|
if (cfg.worker) {
|
|
132949
133073
|
console.error(`[bridge] WORKER MODE \u2014 workspace: ${cfg.workspaceDir} \xB7 permissions: ${cfg.permissionMode} \xB7 enclave dir fenced`);
|
|
@@ -132969,9 +133093,14 @@ async function main() {
|
|
|
132969
133093
|
agentName: cfg.agentName,
|
|
132970
133094
|
platform: "node"
|
|
132971
133095
|
});
|
|
132972
|
-
const
|
|
133096
|
+
const agentSystemPrompt = 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.`;
|
|
133097
|
+
const deviceJwt = () => {
|
|
133098
|
+
const c4 = channel;
|
|
133099
|
+
return c4._deviceJwt ?? c4._persisted?.deviceJwt ?? null;
|
|
133100
|
+
};
|
|
133101
|
+
const listener = new PersistentClaudeSession({
|
|
132973
133102
|
model: cfg.model,
|
|
132974
|
-
systemPrompt:
|
|
133103
|
+
systemPrompt: agentSystemPrompt,
|
|
132975
133104
|
// Claude speaks by calling the say tool → the session routes it to the reply
|
|
132976
133105
|
// bound to the message being answered (wireBridge captures that per inbound via
|
|
132977
133106
|
// ActiveTarget.snapshotReply, which also logs the "said to …" line). This
|
|
@@ -132979,23 +133108,35 @@ async function main() {
|
|
|
132979
133108
|
// room when room traffic arrives mid-compose.
|
|
132980
133109
|
// Assistant reasoning that wasn't sent — log a short trace only.
|
|
132981
133110
|
onObserve: (text) => console.error(`[bridge] observed (${text.length} chars, not sent)`),
|
|
132982
|
-
worker:
|
|
132983
|
-
workspaceDir: cfg.workspaceDir,
|
|
132984
|
-
osIsolated: cfg.osIsolated,
|
|
132985
|
-
permissionMode: cfg.permissionMode,
|
|
132986
|
-
dataDir: cfg.dataDir,
|
|
132987
|
-
// Slice 2 Plan B: audit self-report context (only used on armed-room turns).
|
|
132988
|
-
roomId: cfg.roomFilter,
|
|
132989
|
-
agentId: channel.deviceId,
|
|
132990
|
-
apiUrl: cfg.apiUrl,
|
|
132991
|
-
deviceJwt: () => {
|
|
132992
|
-
const c4 = channel;
|
|
132993
|
-
return c4._deviceJwt ?? c4._persisted?.deviceJwt ?? null;
|
|
132994
|
-
}
|
|
133111
|
+
worker: false
|
|
132995
133112
|
});
|
|
133113
|
+
const WORKER_MAX_TURNS = 20;
|
|
133114
|
+
const WORKER_TIMEOUT_MS = 5 * 6e4;
|
|
133115
|
+
const workerQueue = new WorkerQueue({
|
|
133116
|
+
makeSession: (task) => new PersistentClaudeSession({
|
|
133117
|
+
model: cfg.model,
|
|
133118
|
+
systemPrompt: agentSystemPrompt,
|
|
133119
|
+
onObserve: (text) => console.error(`[worker] observed (${text.length} chars, not sent)`),
|
|
133120
|
+
worker: true,
|
|
133121
|
+
ephemeral: true,
|
|
133122
|
+
maxTurns: WORKER_MAX_TURNS,
|
|
133123
|
+
workspaceDir: cfg.workspaceDir,
|
|
133124
|
+
osIsolated: cfg.osIsolated,
|
|
133125
|
+
permissionMode: cfg.permissionMode,
|
|
133126
|
+
dataDir: cfg.dataDir,
|
|
133127
|
+
// Slice 2 Plan B audit self-report — only fires on armed-room tasks (isArmedTurn).
|
|
133128
|
+
roomId: cfg.roomFilter,
|
|
133129
|
+
agentId: channel.deviceId,
|
|
133130
|
+
apiUrl: cfg.apiUrl,
|
|
133131
|
+
deviceJwt
|
|
133132
|
+
}),
|
|
133133
|
+
timeoutMs: WORKER_TIMEOUT_MS,
|
|
133134
|
+
log: (m6) => console.error(m6)
|
|
133135
|
+
});
|
|
133136
|
+
const router = makeRouter({ worker: !!cfg.worker, listener, queue: workerQueue });
|
|
132996
133137
|
wireBridge(
|
|
132997
133138
|
channel,
|
|
132998
|
-
{ push: (t7, reply, opts) =>
|
|
133139
|
+
{ push: (t7, reply, opts) => router.push(t7, reply, opts) },
|
|
132999
133140
|
target,
|
|
133000
133141
|
{
|
|
133001
133142
|
roomFilter: cfg.roomFilter,
|
|
@@ -133015,7 +133156,11 @@ async function main() {
|
|
|
133015
133156
|
"room_joined",
|
|
133016
133157
|
(e7) => console.error(`[bridge] joined room ${e7.name} (${e7.roomId})`)
|
|
133017
133158
|
);
|
|
133018
|
-
|
|
133159
|
+
listener.start().catch((err) => {
|
|
133160
|
+
console.error("[bridge] fatal:", err);
|
|
133161
|
+
process.exit(1);
|
|
133162
|
+
});
|
|
133163
|
+
await channel.start();
|
|
133019
133164
|
}
|
|
133020
133165
|
main().catch((err) => {
|
|
133021
133166
|
console.error("[bridge] fatal:", err);
|
package/dist/router.d.ts
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import type { ReplySink } from "./session.js";
|
|
2
|
+
import type { WorkerTask } from "./worker-queue.js";
|
|
3
|
+
/** The push interface wireBridge drives (matches PersistentClaudeSession.push). */
|
|
4
|
+
export interface PushLike {
|
|
5
|
+
push(text: string, reply?: ReplySink, opts?: {
|
|
6
|
+
autoReplyOnText?: boolean;
|
|
7
|
+
armed?: () => boolean;
|
|
8
|
+
}): void;
|
|
9
|
+
}
|
|
10
|
+
export interface RouterDeps {
|
|
11
|
+
/** AV_WORKER=1 → tool-eligible turns run in isolated workers; false → no worker ever. */
|
|
12
|
+
worker: boolean;
|
|
13
|
+
/** Always-on locked listener (tools:[]) for untrusted/non-tool turns. */
|
|
14
|
+
listener: PushLike;
|
|
15
|
+
/** Serial queue of tool-eligible turns. */
|
|
16
|
+
queue: {
|
|
17
|
+
enqueue(task: WorkerTask): void;
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Route each inbound turn to EXACTLY ONE lane (facet B). Tool-eligible turns —
|
|
22
|
+
* owner DMs (autoReplyOnText) and armed room turns — go to an isolated ephemeral
|
|
23
|
+
* worker via the queue; everything else stays on the locked listener. A room
|
|
24
|
+
* turn's `armed` getter is read once here at routing time to pick the lane; the
|
|
25
|
+
* same live getter is threaded to the worker task so a mid-turn disarm still
|
|
26
|
+
* denies the next tool call (Facet A D5). Locked agents (worker=false) always
|
|
27
|
+
* route to the listener — a strict no-op vs today.
|
|
28
|
+
*/
|
|
29
|
+
export declare function makeRouter(deps: RouterDeps): PushLike;
|
|
30
|
+
//# sourceMappingURL=router.d.ts.map
|
package/dist/session.d.ts
CHANGED
|
@@ -45,7 +45,7 @@ export type QueryFn = (args: {
|
|
|
45
45
|
type: string;
|
|
46
46
|
[k: string]: unknown;
|
|
47
47
|
}>;
|
|
48
|
-
type ReplySink = (text: string) => void | Promise<void>;
|
|
48
|
+
export type ReplySink = (text: string) => void | Promise<void>;
|
|
49
49
|
export interface SessionOpts {
|
|
50
50
|
/** Fallback reply sink when a message carries no per-message reply (e.g. a
|
|
51
51
|
* proactive say). Per-message replies passed to push() take precedence. */
|
|
@@ -73,6 +73,12 @@ export interface SessionOpts {
|
|
|
73
73
|
apiUrl?: string;
|
|
74
74
|
/** Lazy getter so a JWT refreshed after construction is still picked up. */
|
|
75
75
|
deviceJwt?: () => string | null;
|
|
76
|
+
/** Single-shot ephemeral worker: process exactly the pushed message(s) then
|
|
77
|
+
* terminate the input stream so start() resolves. Used by the worker queue so
|
|
78
|
+
* a per-task query() completes and its state is discarded. */
|
|
79
|
+
ephemeral?: boolean;
|
|
80
|
+
/** Turn cap for a worker query() (bounds a runaway tool loop). */
|
|
81
|
+
maxTurns?: number;
|
|
76
82
|
}
|
|
77
83
|
export declare class PersistentClaudeSession {
|
|
78
84
|
private opts;
|
|
@@ -105,6 +111,8 @@ export declare class PersistentClaudeSession {
|
|
|
105
111
|
private turnText;
|
|
106
112
|
/** In-process room MCP server — hoisted so buildSdkOptions can reference it. */
|
|
107
113
|
private roomServer;
|
|
114
|
+
/** AbortController for the in-flight query(); abort() triggers it (queue timeout). */
|
|
115
|
+
private _abort?;
|
|
108
116
|
constructor(opts: SessionOpts);
|
|
109
117
|
/**
|
|
110
118
|
* Queue an inbound message for the model.
|
|
@@ -126,6 +134,8 @@ export declare class PersistentClaudeSession {
|
|
|
126
134
|
* back to opts.onSay. This is what closes the DM→room leak: the destination is
|
|
127
135
|
* the one captured for the message being answered, not a live global target. */
|
|
128
136
|
deliver(text: string): Promise<void>;
|
|
137
|
+
/** Abort the in-flight query() — used by the worker queue on a per-task timeout. */
|
|
138
|
+
abort(): void;
|
|
129
139
|
private input;
|
|
130
140
|
/**
|
|
131
141
|
* Build the SDK options for this session. Locked mode (default) is safe for
|
|
@@ -136,5 +146,4 @@ export declare class PersistentClaudeSession {
|
|
|
136
146
|
private buildSdkOptions;
|
|
137
147
|
start(): Promise<void>;
|
|
138
148
|
}
|
|
139
|
-
export {};
|
|
140
149
|
//# sourceMappingURL=session.d.ts.map
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import type { PersistentClaudeSession, ReplySink } from "./session.js";
|
|
2
|
+
/** One tool-eligible turn to run in an isolated ephemeral worker. */
|
|
3
|
+
export type WorkerTask = {
|
|
4
|
+
/** The single instruction that seeds the worker's conversation. */
|
|
5
|
+
instruction: string;
|
|
6
|
+
/** Where this task's reply (room_say / #416 fallback) is sent. Per-task. */
|
|
7
|
+
reply: ReplySink;
|
|
8
|
+
/** Owner-DM turns want a plain-text reply fallback (#416). */
|
|
9
|
+
autoReplyOnText: boolean;
|
|
10
|
+
/** Armed-room turns: live getter the gate reads per tool decision (mid-turn
|
|
11
|
+
* disarm denies the next call). Absent for owner DMs. */
|
|
12
|
+
armed?: () => boolean;
|
|
13
|
+
};
|
|
14
|
+
export interface WorkerQueueDeps {
|
|
15
|
+
/** Build a fresh ephemeral worker session for this task (not yet started). */
|
|
16
|
+
makeSession: (task: WorkerTask) => PersistentClaudeSession;
|
|
17
|
+
/** Per-task wall-clock cap; on breach the worker is aborted and the queue advances. */
|
|
18
|
+
timeoutMs: number;
|
|
19
|
+
log: (m: string) => void;
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Serial FIFO of tool-eligible turns. One worker runs at a time (concurrent tool
|
|
23
|
+
* calls against one device/workspace can interleave destructively; the owner is a
|
|
24
|
+
* single actor). Each task runs in a fresh ephemeral worker seeded with only its
|
|
25
|
+
* instruction; on error or timeout the worker is torn down, a generic message is
|
|
26
|
+
* sent to the task's reply, and the queue advances — a task can never wedge the
|
|
27
|
+
* queue (which would starve the owner's DM lane).
|
|
28
|
+
*/
|
|
29
|
+
export declare class WorkerQueue {
|
|
30
|
+
private deps;
|
|
31
|
+
private q;
|
|
32
|
+
private loop;
|
|
33
|
+
private running;
|
|
34
|
+
constructor(deps: WorkerQueueDeps);
|
|
35
|
+
enqueue(task: WorkerTask): void;
|
|
36
|
+
/** Resolves when the queue has processed everything enqueued so far. */
|
|
37
|
+
whenDrained(): Promise<void>;
|
|
38
|
+
private drain;
|
|
39
|
+
private runOne;
|
|
40
|
+
}
|
|
41
|
+
//# sourceMappingURL=worker-queue.d.ts.map
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@agentvault/claude-bridge",
|
|
3
|
-
"version": "0.5.
|
|
3
|
+
"version": "0.5.8",
|
|
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",
|