@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 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,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
- '-p', promptText,
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: ['ignore', 'pipe', 'pipe'],
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 claude session ID from the first system/init event
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: sessionId,
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
- console.log(`[${ts}] ▶ ${text}`);
363
- handleMessage(conversationId, text, sessionId ?? null, auth, vaultFileOpen, clientCapabilities, syncedFolders).catch((err) => {
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@1presence/bridge",
3
- "version": "0.21.0",
3
+ "version": "0.22.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
- }