@1presence/bridge 0.20.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.
@@ -0,0 +1,112 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.makeBridgeAccumulator = makeBridgeAccumulator;
4
+ exports.postSaveTurn = postSaveTurn;
5
+ function makeBridgeAccumulator() {
6
+ const state = {
7
+ assistantText: '',
8
+ toolCalls: [],
9
+ toolResults: {},
10
+ };
11
+ let textEmitted = false;
12
+ let turnTextEmitted = false;
13
+ function appendText(text) {
14
+ if (textEmitted && !turnTextEmitted)
15
+ state.assistantText += '\n\n';
16
+ state.assistantText += text;
17
+ turnTextEmitted = true;
18
+ textEmitted = true;
19
+ }
20
+ return {
21
+ consume(event) {
22
+ const type = event['type'];
23
+ if (type === 'text') {
24
+ const t = event['text'];
25
+ if (t)
26
+ appendText(t);
27
+ return;
28
+ }
29
+ if (type === 'assistant') {
30
+ // New API turn — reset the per-turn flag so the next text emission
31
+ // gets a `\n\n` separator iff text was already emitted in a prior turn.
32
+ turnTextEmitted = false;
33
+ const msg = event['message'];
34
+ const content = msg?.['content'];
35
+ if (!Array.isArray(content))
36
+ return;
37
+ for (const block of content) {
38
+ if (block['type'] === 'text') {
39
+ const t = block['text'];
40
+ if (t)
41
+ appendText(t);
42
+ }
43
+ else if (block['type'] === 'tool_use') {
44
+ const id = block['id'];
45
+ const name = block['name'];
46
+ const input = block['input'] ?? {};
47
+ if (!id || !name)
48
+ continue;
49
+ const bareName = name.replace(/^mcp__1presence__/, '');
50
+ state.toolCalls.push({ id, name: bareName, input });
51
+ if (bareName === 'set_conversation_title') {
52
+ const raw = String(input['title'] ?? '').trim();
53
+ if (raw)
54
+ state.title = raw.charAt(0).toUpperCase() + raw.slice(1);
55
+ }
56
+ }
57
+ }
58
+ return;
59
+ }
60
+ if (type === 'user') {
61
+ const msg = event['message'];
62
+ const content = msg?.['content'];
63
+ if (!Array.isArray(content))
64
+ return;
65
+ for (const block of content) {
66
+ if (block['type'] !== 'tool_result')
67
+ continue;
68
+ const id = block['tool_use_id'];
69
+ const result = block['content'];
70
+ if (!id)
71
+ continue;
72
+ state.toolResults[id] = typeof result === 'string' ? result : JSON.stringify(result);
73
+ }
74
+ return;
75
+ }
76
+ },
77
+ state() {
78
+ return state;
79
+ },
80
+ };
81
+ }
82
+ async function postSaveTurn(gatewayHttp, token, record) {
83
+ let res;
84
+ try {
85
+ res = await fetch(`${gatewayHttp}/bridge/save-turn`, {
86
+ method: 'POST',
87
+ headers: {
88
+ 'Content-Type': 'application/json',
89
+ 'Authorization': `Bearer ${token}`,
90
+ },
91
+ body: JSON.stringify({
92
+ sessionId: record.sessionId,
93
+ conversationId: record.conversationId,
94
+ userMessage: record.userMessage,
95
+ assistantText: record.assistantText,
96
+ toolCalls: record.toolCalls,
97
+ toolResults: record.toolResults,
98
+ ...(record.title ? { title: record.title } : {}),
99
+ ...(record.usage ? { usage: record.usage } : {}),
100
+ }),
101
+ });
102
+ }
103
+ catch (err) {
104
+ return { ok: false, finalized: false, status: 0, error: err.message };
105
+ }
106
+ if (!res.ok) {
107
+ const body = await res.text().catch(() => '');
108
+ return { ok: false, finalized: false, status: res.status, error: body.slice(0, 200) };
109
+ }
110
+ const data = await res.json().catch(() => ({}));
111
+ return { ok: true, finalized: data.finalized === true, status: res.status };
112
+ }
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
@@ -12,6 +12,8 @@ const auth_1 = require("./auth");
12
12
  const claude_1 = require("./claude");
13
13
  const config_1 = require("./config");
14
14
  const update_1 = require("./update");
15
+ const accumulator_1 = require("./accumulator");
16
+ const outbox_1 = require("./outbox");
15
17
  const package_json_1 = require("../package.json");
16
18
  // Published tarballs don't ship src/, so this fires only when running the
17
19
  // dist build from a live workspace checkout. Catches the trap where editing
@@ -117,8 +119,13 @@ function writeRestricted(path, data) {
117
119
  (0, fs_1.writeFileSync)(path, data, { mode: 0o600 });
118
120
  (0, fs_1.chmodSync)(path, 0o600);
119
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
+ }
120
127
  // ─── Handle a single incoming message (token refresh + spawn) ─────────────────
121
- async function handleMessage(conversationId, text, sessionId, auth, vaultFileOpen, clientCapabilities, syncedFolders) {
128
+ async function handleMessage(conversationId, text, sessionId, history, auth, vaultFileOpen, clientCapabilities, syncedFolders) {
122
129
  // Refresh JWT if <10 min remaining before spawning Claude
123
130
  let activeAuth = auth;
124
131
  try {
@@ -161,15 +168,61 @@ async function handleMessage(conversationId, text, sessionId, auth, vaultFileOpe
161
168
  return;
162
169
  }
163
170
  let responding = false;
171
+ const accumulator = (0, accumulator_1.makeBridgeAccumulator)();
172
+ const startedAt = Date.now();
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;
178
+ function buildSpoolRecord(usage, model) {
179
+ const s = accumulator.state();
180
+ return {
181
+ sessionId: turnSessionId,
182
+ conversationId,
183
+ userMessage: text,
184
+ assistantText: s.assistantText,
185
+ toolCalls: s.toolCalls,
186
+ toolResults: s.toolResults,
187
+ ...(s.title ? { title: s.title } : {}),
188
+ usage: usage
189
+ ? { ...usage, ...(model ? { model } : {}) }
190
+ : null,
191
+ startedAt,
192
+ finalizedAt: Date.now(),
193
+ };
194
+ }
195
+ async function finalizeAndPost(record) {
196
+ // Spool BEFORE the network call so a crash between here and the POST
197
+ // ack is recoverable by drain-on-startup. The gateway dedupes on
198
+ // conversationId, so a replay is idempotent.
199
+ try {
200
+ (0, outbox_1.writeSpool)(record);
201
+ }
202
+ catch (err) {
203
+ console.warn(`[bridge] spool write failed: ${err.message}`);
204
+ }
205
+ const result = await (0, accumulator_1.postSaveTurn)(GATEWAY_HTTP, activeAuth.token, record);
206
+ if (result.ok) {
207
+ (0, outbox_1.deleteSpool)(record.conversationId);
208
+ }
209
+ else {
210
+ // Leave the spool file in place — next startup or next successful
211
+ // POST opportunity will retry. Quietly log so users aren't alarmed.
212
+ console.warn(`[bridge] save-turn POST failed (${result.status}): ${result.error ?? 'unknown'} — kept on disk for retry`);
213
+ }
214
+ }
164
215
  (0, claude_1.spawnClaude)({
165
216
  conversationId,
166
- presenceSessionId: sessionId,
217
+ presenceSessionId: claudePinnedSessionId,
167
218
  text,
168
219
  uid: activeAuth.uid,
220
+ history,
169
221
  vaultFileOpen,
170
222
  clientCapabilities,
171
223
  syncedFolders,
172
224
  onEvent: (event) => {
225
+ accumulator.consume(event);
173
226
  if (!responding && event['type'] === 'assistant') {
174
227
  responding = true;
175
228
  console.log(`[${new Date().toLocaleTimeString()}] ◐ responding…`);
@@ -186,6 +239,7 @@ async function handleMessage(conversationId, text, sessionId, auth, vaultFileOpe
186
239
  parts.push(costStr);
187
240
  const suffix = parts.length ? ` ${parts.join(' ')}` : '';
188
241
  console.log(`[${new Date().toLocaleTimeString()}] ✓ done${suffix}`);
242
+ const mapped = toBridgeUsage(usage);
189
243
  if (currentWs?.readyState === ws_1.default.OPEN) {
190
244
  currentWs.send(JSON.stringify({
191
245
  type: 'done',
@@ -193,34 +247,60 @@ async function handleMessage(conversationId, text, sessionId, auth, vaultFileOpe
193
247
  messageCount,
194
248
  costUsd,
195
249
  model,
196
- usage: usage ? {
197
- inputTokens: usage.input_tokens,
198
- outputTokens: usage.output_tokens,
199
- cacheReadTokens: usage.cache_read_input_tokens,
200
- cacheCreationTokens: usage.cache_creation_input_tokens,
201
- } : null,
250
+ usage: mapped,
202
251
  }));
203
252
  }
253
+ // HTTP fallback runs unconditionally — gateway dedupes against WS path.
254
+ void finalizeAndPost(buildSpoolRecord(mapped, model));
204
255
  },
205
256
  onError: (message, usage, model) => {
206
257
  console.error(`[${new Date().toLocaleTimeString()}] ✗ ${message}`);
258
+ const mapped = toBridgeUsage(usage);
207
259
  if (currentWs?.readyState === ws_1.default.OPEN) {
208
260
  currentWs.send(JSON.stringify({
209
261
  type: 'error',
210
262
  conversationId,
211
263
  message,
212
264
  model,
213
- usage: usage ? {
214
- inputTokens: usage.input_tokens,
215
- outputTokens: usage.output_tokens,
216
- cacheReadTokens: usage.cache_read_input_tokens,
217
- cacheCreationTokens: usage.cache_creation_input_tokens,
218
- } : null,
265
+ usage: mapped,
219
266
  }));
220
267
  }
268
+ void finalizeAndPost(buildSpoolRecord(mapped, model));
221
269
  },
222
270
  });
223
271
  }
272
+ // ─── Usage shape adapter ──────────────────────────────────────────────────────
273
+ function toBridgeUsage(usage) {
274
+ if (!usage)
275
+ return null;
276
+ return {
277
+ inputTokens: usage.input_tokens,
278
+ outputTokens: usage.output_tokens,
279
+ cacheReadTokens: usage.cache_read_input_tokens,
280
+ cacheCreationTokens: usage.cache_creation_input_tokens,
281
+ };
282
+ }
283
+ // ─── Outbox drain ─────────────────────────────────────────────────────────────
284
+ //
285
+ // On bridge startup and on every successful reconnect, replay any spool
286
+ // records that didn't get a successful POST ack last time. The gateway
287
+ // dedupes on conversationId — if it already saved via the WS path, the
288
+ // reply is finalized=false and we still delete the spool.
289
+ async function drainOutbox(auth) {
290
+ const records = (0, outbox_1.listSpool)();
291
+ if (records.length === 0)
292
+ return;
293
+ console.log(`[bridge] draining ${records.length} pending save record${records.length === 1 ? '' : 's'}…`);
294
+ for (const record of records) {
295
+ const result = await (0, accumulator_1.postSaveTurn)(GATEWAY_HTTP, auth.token, record);
296
+ if (result.ok) {
297
+ (0, outbox_1.deleteSpool)(record.conversationId);
298
+ }
299
+ else {
300
+ console.warn(`[bridge] drain POST failed (${result.status}): ${result.error ?? 'unknown'} — leaving on disk`);
301
+ }
302
+ }
303
+ }
224
304
  // ─── WebSocket connection ─────────────────────────────────────────────────────
225
305
  // Application-level heartbeat — avoids relying on WebSocket control frames (ping/pong),
226
306
  // which some proxies (GKE LB) may not forward reliably.
@@ -260,6 +340,12 @@ function connect(auth, retryDelay = 1000) {
260
340
  retryDelay = 1000;
261
341
  console.log('✓ Bridge connected. Local Mode active on all your devices.\n');
262
342
  startPing();
343
+ // Drain any save records left behind by an earlier crashed/dropped session.
344
+ // Fire-and-forget — the network is up, the gateway is reachable, and we
345
+ // don't want to block message handling on this.
346
+ if (currentAuth) {
347
+ drainOutbox(currentAuth).catch(err => console.warn(`[bridge] drain failed: ${err.message}`));
348
+ }
263
349
  });
264
350
  ws.on('message', (raw) => {
265
351
  let msg;
@@ -281,11 +367,11 @@ function connect(auth, retryDelay = 1000) {
281
367
  }
282
368
  if (msg.type !== 'message' || !msg.conversationId || !msg.text)
283
369
  return;
284
- const { conversationId, text, sessionId, vaultFileOpen, clientCapabilities, syncedFolders } = msg;
370
+ const { conversationId, text, sessionId, history, vaultFileOpen, clientCapabilities, syncedFolders } = msg;
285
371
  const ts = new Date().toLocaleTimeString();
286
- const preview = text.length > 80 ? text.slice(0, 80) + '…' : text;
287
- console.log(`[${ts}] ▶ ${preview}`);
288
- 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) => {
289
375
  console.error(`[bridge] handleMessage error: ${err.message}`);
290
376
  });
291
377
  });
package/dist/outbox.js ADDED
@@ -0,0 +1,55 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.writeSpool = writeSpool;
4
+ exports.deleteSpool = deleteSpool;
5
+ exports.listSpool = listSpool;
6
+ const fs_1 = require("fs");
7
+ const os_1 = require("os");
8
+ const path_1 = require("path");
9
+ // ─── On-disk turn spool ───────────────────────────────────────────────────────
10
+ //
11
+ // Each in-flight bridge turn writes a record to ~/.1presence/outbox/. The file
12
+ // exists from the moment the turn starts until the gateway acks the save-turn
13
+ // POST. If the bridge is killed (Ctrl+C, terminal closed, crash) between
14
+ // Claude finishing and the ack landing, the next startup drains the directory
15
+ // — covering the failure mode the WS+HTTP path alone can't.
16
+ //
17
+ // Records are keyed by conversationId, which the gateway also dedupes on, so
18
+ // a drained replay is idempotent: if it already saved via WS, the POST is a
19
+ // 200 no-op and the spool file is deleted.
20
+ //
21
+ // Payload mode 0600 — the file contains the user's assistant transcript and
22
+ // tool inputs. Tightened on every write to handle legacy world-readable files.
23
+ const OUTBOX_DIR = (0, path_1.join)((0, os_1.homedir)(), '.1presence', 'outbox');
24
+ function ensureDir() {
25
+ (0, fs_1.mkdirSync)(OUTBOX_DIR, { recursive: true });
26
+ }
27
+ function pathFor(conversationId) {
28
+ return (0, path_1.join)(OUTBOX_DIR, `${conversationId}.json`);
29
+ }
30
+ function writeSpool(record) {
31
+ ensureDir();
32
+ (0, fs_1.writeFileSync)(pathFor(record.conversationId), JSON.stringify(record), { mode: 0o600 });
33
+ }
34
+ function deleteSpool(conversationId) {
35
+ try {
36
+ (0, fs_1.unlinkSync)(pathFor(conversationId));
37
+ }
38
+ catch { /* already gone — fine */ }
39
+ }
40
+ function listSpool() {
41
+ ensureDir();
42
+ const out = [];
43
+ for (const file of (0, fs_1.readdirSync)(OUTBOX_DIR)) {
44
+ if (!file.endsWith('.json'))
45
+ continue;
46
+ try {
47
+ const raw = (0, fs_1.readFileSync)((0, path_1.join)(OUTBOX_DIR, file), 'utf-8');
48
+ out.push(JSON.parse(raw));
49
+ }
50
+ catch {
51
+ // Malformed file — leave it alone so a human can inspect.
52
+ }
53
+ }
54
+ return out;
55
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@1presence/bridge",
3
- "version": "0.20.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
- }