@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 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 promptText = ctxParts.length > 0 ? `[${ctxParts.join(' | ')}]\n\n${text}` : text;
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
- '-p', promptText,
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: ['ignore', 'pipe', 'pipe'],
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 claude session ID from the first system/init event
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: sessionId,
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
- console.log(`[${ts}] ▶ ${text}`);
363
- handleMessage(conversationId, text, sessionId ?? null, auth, vaultFileOpen, clientCapabilities, syncedFolders).catch((err) => {
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@1presence/bridge",
3
- "version": "0.21.0",
3
+ "version": "0.23.0",
4
4
  "description": "Run 1Presence on your Mac and use your Claude.ai Pro subscription from any device",
5
5
  "bin": {
6
6
  "1presence-bridge": "dist/index.js"
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
- }