@1presence/bridge 0.21.0 → 0.23.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/claude.js +49 -14
- package/dist/index.js +20 -5
- package/package.json +1 -1
- package/dist/sessions.js +0 -36
package/dist/claude.js
CHANGED
|
@@ -7,7 +7,6 @@ const child_process_1 = require("child_process");
|
|
|
7
7
|
const fs_1 = require("fs");
|
|
8
8
|
const os_1 = require("os");
|
|
9
9
|
const path_1 = require("path");
|
|
10
|
-
const sessions_1 = require("./sessions");
|
|
11
10
|
// ─── Bridge working directory ─────────────────────────────────────────────────
|
|
12
11
|
//
|
|
13
12
|
// Claude Code always loads CLAUDE.md files from cwd upward plus the global
|
|
@@ -52,7 +51,7 @@ function formatPayload(value) {
|
|
|
52
51
|
const active = new Map();
|
|
53
52
|
// ─── Spawn ────────────────────────────────────────────────────────────────────
|
|
54
53
|
function spawnClaude(params) {
|
|
55
|
-
const { conversationId, presenceSessionId, text, uid, vaultFileOpen, clientCapabilities, syncedFolders, onEvent, onDone, onError } = params;
|
|
54
|
+
const { conversationId, presenceSessionId, text, uid, history, vaultFileOpen, clientCapabilities, syncedFolders, onEvent, onDone, onError } = params;
|
|
56
55
|
const systemPromptPath = (0, path_1.join)((0, os_1.tmpdir)(), `agent-${uid}.md`);
|
|
57
56
|
const mcpConfigPath = (0, path_1.join)((0, os_1.tmpdir)(), `mcp-${uid}.json`);
|
|
58
57
|
if (verbose) {
|
|
@@ -60,6 +59,7 @@ function spawnClaude(params) {
|
|
|
60
59
|
process.stderr.write(`[bridge:verbose] override md: ${(0, path_1.join)(BRIDGE_CWD, 'CLAUDE.md')}\n`);
|
|
61
60
|
process.stderr.write(`[bridge:verbose] system prompt: ${systemPromptPath}\n`);
|
|
62
61
|
process.stderr.write(`[bridge:verbose] mcp config: ${mcpConfigPath}\n`);
|
|
62
|
+
process.stderr.write(`[bridge:verbose] history turns: ${history.length}\n`);
|
|
63
63
|
}
|
|
64
64
|
// If a prior process is still running for this conversation (user sent a
|
|
65
65
|
// follow-up before the previous turn finished), supersede it. The latest
|
|
@@ -70,7 +70,6 @@ function spawnClaude(params) {
|
|
|
70
70
|
existing.kill('SIGTERM');
|
|
71
71
|
active.delete(conversationId);
|
|
72
72
|
}
|
|
73
|
-
const claudeSessionId = presenceSessionId ? (0, sessions_1.getClaudeSession)(presenceSessionId) : undefined;
|
|
74
73
|
const ctxParts = [];
|
|
75
74
|
if (vaultFileOpen)
|
|
76
75
|
ctxParts.push(`vault_file_open: ${vaultFileOpen}`);
|
|
@@ -78,7 +77,7 @@ function spawnClaude(params) {
|
|
|
78
77
|
ctxParts.push(`client_capabilities: ${clientCapabilities.join(', ')}`);
|
|
79
78
|
if (syncedFolders?.length)
|
|
80
79
|
ctxParts.push(`synced_folders: ${syncedFolders.join(', ')}`);
|
|
81
|
-
const
|
|
80
|
+
const userMessageText = ctxParts.length > 0 ? `[${ctxParts.join(' | ')}]\n\n${text}` : text;
|
|
82
81
|
// Lockdown rationale:
|
|
83
82
|
// - `--tools ""` disables ALL built-in tools (Bash/Read/Write/Edit/Glob/Grep/
|
|
84
83
|
// WebFetch/etc.). MCP tools are not "built-in" so the 1Presence MCP surface
|
|
@@ -90,8 +89,23 @@ function spawnClaude(params) {
|
|
|
90
89
|
// - `--strict-mcp-config` keeps the MCP surface to exactly what we wire in
|
|
91
90
|
// via --mcp-config. Together these guarantee the bridge can only call
|
|
92
91
|
// `mcp__1presence__*` — no filesystem, no shell, no arbitrary network.
|
|
92
|
+
//
|
|
93
|
+
// Session continuity rationale:
|
|
94
|
+
// - `--input-format stream-json` accepts structured user/assistant messages
|
|
95
|
+
// on stdin. We replay prior turns (loaded by the gateway from Firestore)
|
|
96
|
+
// followed by the new user turn — this is how the bridge sees history.
|
|
97
|
+
// - `--no-session-persistence` keeps no jsonl on disk. The bridge has zero
|
|
98
|
+
// local filesystem dependency for continuity; Firestore is the only
|
|
99
|
+
// source of truth.
|
|
100
|
+
// - `--session-id <uuid>` must be a fresh UUID per spawn: the CLI treats
|
|
101
|
+
// this flag as a "claim a new session ID" operation and rejects the
|
|
102
|
+
// second spawn with "Session ID X is already in use" if we reuse one
|
|
103
|
+
// across turns of a chat — even with --no-session-persistence. The
|
|
104
|
+
// bridge passes the per-spawn `conversationId` here; the presence
|
|
105
|
+
// sessionId is correlated separately via bridge logs and spool records.
|
|
93
106
|
const args = [
|
|
94
|
-
'
|
|
107
|
+
'--print',
|
|
108
|
+
'--input-format', 'stream-json',
|
|
95
109
|
'--output-format', 'stream-json',
|
|
96
110
|
'--verbose',
|
|
97
111
|
'--tools', '',
|
|
@@ -100,23 +114,46 @@ function spawnClaude(params) {
|
|
|
100
114
|
'--system-prompt-file', systemPromptPath,
|
|
101
115
|
'--mcp-config', mcpConfigPath,
|
|
102
116
|
'--strict-mcp-config',
|
|
117
|
+
'--no-session-persistence',
|
|
118
|
+
'--session-id', presenceSessionId,
|
|
103
119
|
];
|
|
104
120
|
const pinnedModel = (0, config_1.getBridgeModel)();
|
|
105
121
|
if (pinnedModel) {
|
|
106
122
|
args.push('--model', pinnedModel);
|
|
107
123
|
}
|
|
108
|
-
if (claudeSessionId) {
|
|
109
|
-
args.push('--resume', claudeSessionId);
|
|
110
|
-
}
|
|
111
124
|
// Strip API key so Claude Code uses the user's claude.ai Pro subscription
|
|
112
125
|
// (OAuth credentials), not an API key that would bill to a separate account.
|
|
113
126
|
const { ANTHROPIC_API_KEY: _stripped, ...safeEnv } = process.env;
|
|
114
127
|
const proc = (0, child_process_1.spawn)('claude', args, {
|
|
115
128
|
cwd: BRIDGE_CWD,
|
|
116
129
|
env: safeEnv,
|
|
117
|
-
stdio: ['
|
|
130
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
118
131
|
});
|
|
119
132
|
active.set(conversationId, proc);
|
|
133
|
+
// Feed prior turns + the new user message via stdin as stream-json.
|
|
134
|
+
// Each line is a JSON object: `{ type, message: { role, content } }`.
|
|
135
|
+
// Sanitisation (orphan tool_use stripping, displayOnly filtering) already
|
|
136
|
+
// happened on the gateway via @presence/shared.toModelMessages — replay
|
|
137
|
+
// the history verbatim and append the fresh user turn.
|
|
138
|
+
try {
|
|
139
|
+
const stdin = proc.stdin;
|
|
140
|
+
if (!stdin) {
|
|
141
|
+
throw new Error('claude stdin is null — spawn must use stdio[0]="pipe"');
|
|
142
|
+
}
|
|
143
|
+
for (const msg of history) {
|
|
144
|
+
const wrapped = { type: msg.role, message: { role: msg.role, content: msg.content } };
|
|
145
|
+
stdin.write(JSON.stringify(wrapped) + '\n');
|
|
146
|
+
}
|
|
147
|
+
const newTurn = { type: 'user', message: { role: 'user', content: userMessageText } };
|
|
148
|
+
stdin.write(JSON.stringify(newTurn) + '\n');
|
|
149
|
+
stdin.end();
|
|
150
|
+
}
|
|
151
|
+
catch (err) {
|
|
152
|
+
process.stderr.write(`[bridge] failed to write stdin: ${err.message}\n`);
|
|
153
|
+
proc.kill('SIGTERM');
|
|
154
|
+
onError(`stdin write failed: ${err.message}`, null, null);
|
|
155
|
+
return;
|
|
156
|
+
}
|
|
120
157
|
let sessionIdExtracted = false;
|
|
121
158
|
let messageCount = 0;
|
|
122
159
|
let costUsd = 0;
|
|
@@ -140,12 +177,10 @@ function spawnClaude(params) {
|
|
|
140
177
|
continue;
|
|
141
178
|
}
|
|
142
179
|
const type = event['type'];
|
|
143
|
-
// Extract
|
|
180
|
+
// Extract model + key source info from the first system/init event.
|
|
181
|
+
// No session-id persistence — Firestore is the only source of truth
|
|
182
|
+
// now, and we pin --session-id to presenceSessionId on every spawn.
|
|
144
183
|
if (!sessionIdExtracted && type === 'system' && event['subtype'] === 'init') {
|
|
145
|
-
const sid = event['session_id'];
|
|
146
|
-
if (sid && presenceSessionId) {
|
|
147
|
-
(0, sessions_1.saveClaudeSession)(presenceSessionId, sid);
|
|
148
|
-
}
|
|
149
184
|
const keySource = event['apiKeySource'];
|
|
150
185
|
const model = event['model'];
|
|
151
186
|
if (model)
|
package/dist/index.js
CHANGED
|
@@ -119,8 +119,13 @@ function writeRestricted(path, data) {
|
|
|
119
119
|
(0, fs_1.writeFileSync)(path, data, { mode: 0o600 });
|
|
120
120
|
(0, fs_1.chmodSync)(path, 0o600);
|
|
121
121
|
}
|
|
122
|
+
// ─── Helpers ──────────────────────────────────────────────────────────────────
|
|
123
|
+
const UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
124
|
+
function isUuid(value) {
|
|
125
|
+
return UUID_RE.test(value);
|
|
126
|
+
}
|
|
122
127
|
// ─── Handle a single incoming message (token refresh + spawn) ─────────────────
|
|
123
|
-
async function handleMessage(conversationId, text, sessionId, auth, vaultFileOpen, clientCapabilities, syncedFolders) {
|
|
128
|
+
async function handleMessage(conversationId, text, sessionId, history, auth, vaultFileOpen, clientCapabilities, syncedFolders) {
|
|
124
129
|
// Refresh JWT if <10 min remaining before spawning Claude
|
|
125
130
|
let activeAuth = auth;
|
|
126
131
|
try {
|
|
@@ -166,6 +171,14 @@ async function handleMessage(conversationId, text, sessionId, auth, vaultFileOpe
|
|
|
166
171
|
const accumulator = (0, accumulator_1.makeBridgeAccumulator)();
|
|
167
172
|
const startedAt = Date.now();
|
|
168
173
|
const turnSessionId = sessionId ?? conversationId; // gateway always supplies one; defensive fallback
|
|
174
|
+
// The CLI's `--session-id` is treated as a "claim this new session ID"
|
|
175
|
+
// operation — passing the same UUID across turns of one chat (which is
|
|
176
|
+
// what the presence sessionId is) makes turn 2 fail with "Session ID X
|
|
177
|
+
// is already in use", even with --no-session-persistence. Use the
|
|
178
|
+
// per-spawn conversationId instead — continuity comes from history
|
|
179
|
+
// replay via --input-format stream-json, not from CLI session tracking.
|
|
180
|
+
// turnSessionId is still kept for spool records / log correlation.
|
|
181
|
+
const claudePinnedSessionId = isUuid(conversationId) ? conversationId : crypto.randomUUID();
|
|
169
182
|
function buildSpoolRecord(usage, model) {
|
|
170
183
|
const s = accumulator.state();
|
|
171
184
|
return {
|
|
@@ -205,9 +218,10 @@ async function handleMessage(conversationId, text, sessionId, auth, vaultFileOpe
|
|
|
205
218
|
}
|
|
206
219
|
(0, claude_1.spawnClaude)({
|
|
207
220
|
conversationId,
|
|
208
|
-
presenceSessionId:
|
|
221
|
+
presenceSessionId: claudePinnedSessionId,
|
|
209
222
|
text,
|
|
210
223
|
uid: activeAuth.uid,
|
|
224
|
+
history,
|
|
211
225
|
vaultFileOpen,
|
|
212
226
|
clientCapabilities,
|
|
213
227
|
syncedFolders,
|
|
@@ -357,10 +371,11 @@ function connect(auth, retryDelay = 1000) {
|
|
|
357
371
|
}
|
|
358
372
|
if (msg.type !== 'message' || !msg.conversationId || !msg.text)
|
|
359
373
|
return;
|
|
360
|
-
const { conversationId, text, sessionId, vaultFileOpen, clientCapabilities, syncedFolders } = msg;
|
|
374
|
+
const { conversationId, text, sessionId, history, vaultFileOpen, clientCapabilities, syncedFolders } = msg;
|
|
361
375
|
const ts = new Date().toLocaleTimeString();
|
|
362
|
-
|
|
363
|
-
|
|
376
|
+
const hist = Array.isArray(history) ? history : [];
|
|
377
|
+
console.log(`[${ts}] ▶ ${text}${hist.length ? ` (history: ${hist.length} turn${hist.length === 1 ? '' : 's'})` : ''}`);
|
|
378
|
+
handleMessage(conversationId, text, sessionId ?? null, hist, auth, vaultFileOpen, clientCapabilities, syncedFolders).catch((err) => {
|
|
364
379
|
console.error(`[bridge] handleMessage error: ${err.message}`);
|
|
365
380
|
});
|
|
366
381
|
});
|
package/package.json
CHANGED
package/dist/sessions.js
DELETED
|
@@ -1,36 +0,0 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.getClaudeSession = getClaudeSession;
|
|
4
|
-
exports.saveClaudeSession = saveClaudeSession;
|
|
5
|
-
const fs_1 = require("fs");
|
|
6
|
-
const os_1 = require("os");
|
|
7
|
-
const path_1 = require("path");
|
|
8
|
-
// ─── Storage ──────────────────────────────────────────────────────────────────
|
|
9
|
-
const CONFIG_DIR = (0, path_1.join)((0, os_1.homedir)(), '.1presence');
|
|
10
|
-
const SESSIONS_FILE = (0, path_1.join)(CONFIG_DIR, 'sessions.json');
|
|
11
|
-
let cache = null;
|
|
12
|
-
function getMap() {
|
|
13
|
-
if (cache)
|
|
14
|
-
return cache;
|
|
15
|
-
try {
|
|
16
|
-
const raw = (0, fs_1.readFileSync)(SESSIONS_FILE, 'utf-8');
|
|
17
|
-
cache = JSON.parse(raw);
|
|
18
|
-
}
|
|
19
|
-
catch {
|
|
20
|
-
cache = {};
|
|
21
|
-
}
|
|
22
|
-
return cache;
|
|
23
|
-
}
|
|
24
|
-
function persist(map) {
|
|
25
|
-
(0, fs_1.mkdirSync)(CONFIG_DIR, { recursive: true });
|
|
26
|
-
(0, fs_1.writeFileSync)(SESSIONS_FILE, JSON.stringify(map, null, 2), 'utf-8');
|
|
27
|
-
}
|
|
28
|
-
// ─── Public API ───────────────────────────────────────────────────────────────
|
|
29
|
-
function getClaudeSession(presenceSessionId) {
|
|
30
|
-
return getMap()[presenceSessionId];
|
|
31
|
-
}
|
|
32
|
-
function saveClaudeSession(presenceSessionId, claudeSessionId) {
|
|
33
|
-
const map = getMap();
|
|
34
|
-
map[presenceSessionId] = claudeSessionId;
|
|
35
|
-
persist(map);
|
|
36
|
-
}
|