@1presence/bridge 0.21.0 → 0.22.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 +45 -14
- package/dist/index.js +16 -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,19 @@ 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 <presenceSessionId>` pins the CLI session UUID to the
|
|
101
|
+
// presence sessionId so logs across bridge/gateway are easy to correlate.
|
|
93
102
|
const args = [
|
|
94
|
-
'
|
|
103
|
+
'--print',
|
|
104
|
+
'--input-format', 'stream-json',
|
|
95
105
|
'--output-format', 'stream-json',
|
|
96
106
|
'--verbose',
|
|
97
107
|
'--tools', '',
|
|
@@ -100,23 +110,46 @@ function spawnClaude(params) {
|
|
|
100
110
|
'--system-prompt-file', systemPromptPath,
|
|
101
111
|
'--mcp-config', mcpConfigPath,
|
|
102
112
|
'--strict-mcp-config',
|
|
113
|
+
'--no-session-persistence',
|
|
114
|
+
'--session-id', presenceSessionId,
|
|
103
115
|
];
|
|
104
116
|
const pinnedModel = (0, config_1.getBridgeModel)();
|
|
105
117
|
if (pinnedModel) {
|
|
106
118
|
args.push('--model', pinnedModel);
|
|
107
119
|
}
|
|
108
|
-
if (claudeSessionId) {
|
|
109
|
-
args.push('--resume', claudeSessionId);
|
|
110
|
-
}
|
|
111
120
|
// Strip API key so Claude Code uses the user's claude.ai Pro subscription
|
|
112
121
|
// (OAuth credentials), not an API key that would bill to a separate account.
|
|
113
122
|
const { ANTHROPIC_API_KEY: _stripped, ...safeEnv } = process.env;
|
|
114
123
|
const proc = (0, child_process_1.spawn)('claude', args, {
|
|
115
124
|
cwd: BRIDGE_CWD,
|
|
116
125
|
env: safeEnv,
|
|
117
|
-
stdio: ['
|
|
126
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
118
127
|
});
|
|
119
128
|
active.set(conversationId, proc);
|
|
129
|
+
// Feed prior turns + the new user message via stdin as stream-json.
|
|
130
|
+
// Each line is a JSON object: `{ type, message: { role, content } }`.
|
|
131
|
+
// Sanitisation (orphan tool_use stripping, displayOnly filtering) already
|
|
132
|
+
// happened on the gateway via @presence/shared.toModelMessages — replay
|
|
133
|
+
// the history verbatim and append the fresh user turn.
|
|
134
|
+
try {
|
|
135
|
+
const stdin = proc.stdin;
|
|
136
|
+
if (!stdin) {
|
|
137
|
+
throw new Error('claude stdin is null — spawn must use stdio[0]="pipe"');
|
|
138
|
+
}
|
|
139
|
+
for (const msg of history) {
|
|
140
|
+
const wrapped = { type: msg.role, message: { role: msg.role, content: msg.content } };
|
|
141
|
+
stdin.write(JSON.stringify(wrapped) + '\n');
|
|
142
|
+
}
|
|
143
|
+
const newTurn = { type: 'user', message: { role: 'user', content: userMessageText } };
|
|
144
|
+
stdin.write(JSON.stringify(newTurn) + '\n');
|
|
145
|
+
stdin.end();
|
|
146
|
+
}
|
|
147
|
+
catch (err) {
|
|
148
|
+
process.stderr.write(`[bridge] failed to write stdin: ${err.message}\n`);
|
|
149
|
+
proc.kill('SIGTERM');
|
|
150
|
+
onError(`stdin write failed: ${err.message}`, null, null);
|
|
151
|
+
return;
|
|
152
|
+
}
|
|
120
153
|
let sessionIdExtracted = false;
|
|
121
154
|
let messageCount = 0;
|
|
122
155
|
let costUsd = 0;
|
|
@@ -140,12 +173,10 @@ function spawnClaude(params) {
|
|
|
140
173
|
continue;
|
|
141
174
|
}
|
|
142
175
|
const type = event['type'];
|
|
143
|
-
// Extract
|
|
176
|
+
// Extract model + key source info from the first system/init event.
|
|
177
|
+
// No session-id persistence — Firestore is the only source of truth
|
|
178
|
+
// now, and we pin --session-id to presenceSessionId on every spawn.
|
|
144
179
|
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
180
|
const keySource = event['apiKeySource'];
|
|
150
181
|
const model = event['model'];
|
|
151
182
|
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,10 @@ 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
|
+
// --session-id requires a UUID; turnSessionId is always one in practice
|
|
175
|
+
// (gateway generates via crypto.randomUUID). Defensive: if a non-UUID slips
|
|
176
|
+
// through, fall back to conversationId which is also crypto.randomUUID.
|
|
177
|
+
const claudePinnedSessionId = isUuid(turnSessionId) ? turnSessionId : conversationId;
|
|
169
178
|
function buildSpoolRecord(usage, model) {
|
|
170
179
|
const s = accumulator.state();
|
|
171
180
|
return {
|
|
@@ -205,9 +214,10 @@ async function handleMessage(conversationId, text, sessionId, auth, vaultFileOpe
|
|
|
205
214
|
}
|
|
206
215
|
(0, claude_1.spawnClaude)({
|
|
207
216
|
conversationId,
|
|
208
|
-
presenceSessionId:
|
|
217
|
+
presenceSessionId: claudePinnedSessionId,
|
|
209
218
|
text,
|
|
210
219
|
uid: activeAuth.uid,
|
|
220
|
+
history,
|
|
211
221
|
vaultFileOpen,
|
|
212
222
|
clientCapabilities,
|
|
213
223
|
syncedFolders,
|
|
@@ -357,10 +367,11 @@ function connect(auth, retryDelay = 1000) {
|
|
|
357
367
|
}
|
|
358
368
|
if (msg.type !== 'message' || !msg.conversationId || !msg.text)
|
|
359
369
|
return;
|
|
360
|
-
const { conversationId, text, sessionId, vaultFileOpen, clientCapabilities, syncedFolders } = msg;
|
|
370
|
+
const { conversationId, text, sessionId, history, vaultFileOpen, clientCapabilities, syncedFolders } = msg;
|
|
361
371
|
const ts = new Date().toLocaleTimeString();
|
|
362
|
-
|
|
363
|
-
|
|
372
|
+
const hist = Array.isArray(history) ? history : [];
|
|
373
|
+
console.log(`[${ts}] ▶ ${text}${hist.length ? ` (history: ${hist.length} turn${hist.length === 1 ? '' : 's'})` : ''}`);
|
|
374
|
+
handleMessage(conversationId, text, sessionId ?? null, hist, auth, vaultFileOpen, clientCapabilities, syncedFolders).catch((err) => {
|
|
364
375
|
console.error(`[bridge] handleMessage error: ${err.message}`);
|
|
365
376
|
});
|
|
366
377
|
});
|
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
|
-
}
|