@1presence/bridge 0.39.0 → 0.42.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/accumulator.js +2 -6
- package/dist/auth.js +16 -23
- package/dist/claude.js +416 -444
- package/dist/config.js +6 -10
- package/dist/index.js +116 -62
- package/dist/outbox.js +13 -18
- package/dist/timer.js +3 -8
- package/dist/update.js +8 -8
- package/package.json +3 -1
package/dist/config.js
CHANGED
|
@@ -1,9 +1,5 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
exports.ensureModelChoice = ensureModelChoice;
|
|
4
|
-
exports.getBridgeModel = getBridgeModel;
|
|
5
|
-
const readline_1 = require("readline");
|
|
6
|
-
const child_process_1 = require("child_process");
|
|
1
|
+
import { emitKeypressEvents } from 'readline';
|
|
2
|
+
import { spawn } from 'child_process';
|
|
7
3
|
// ─── In-memory model choice ───────────────────────────────────────────────────
|
|
8
4
|
//
|
|
9
5
|
// The bridge prompts for a model on every interactive startup. The choice is
|
|
@@ -27,7 +23,7 @@ function detectClaudeDefaultModel() {
|
|
|
27
23
|
} };
|
|
28
24
|
let proc;
|
|
29
25
|
try {
|
|
30
|
-
proc =
|
|
26
|
+
proc = spawn('claude', [
|
|
31
27
|
'-p',
|
|
32
28
|
'--input-format', 'stream-json',
|
|
33
29
|
'--output-format', 'stream-json',
|
|
@@ -111,7 +107,7 @@ function promptForModel(defaultModel) {
|
|
|
111
107
|
}
|
|
112
108
|
};
|
|
113
109
|
render();
|
|
114
|
-
|
|
110
|
+
emitKeypressEvents(process.stdin);
|
|
115
111
|
const wasRaw = process.stdin.isRaw;
|
|
116
112
|
if (process.stdin.isTTY)
|
|
117
113
|
process.stdin.setRawMode(true);
|
|
@@ -172,7 +168,7 @@ function promptForModel(defaultModel) {
|
|
|
172
168
|
* in memory only — every startup re-prompts. In a non-TTY environment the
|
|
173
169
|
* prompt is skipped and Claude Code's own default is used.
|
|
174
170
|
*/
|
|
175
|
-
async function ensureModelChoice() {
|
|
171
|
+
export async function ensureModelChoice() {
|
|
176
172
|
if (!process.stdin.isTTY) {
|
|
177
173
|
selectedModel = null;
|
|
178
174
|
return;
|
|
@@ -187,6 +183,6 @@ async function ensureModelChoice() {
|
|
|
187
183
|
}
|
|
188
184
|
}
|
|
189
185
|
/** Returns the model id chosen for this session, or null to defer to Claude Code's own default. */
|
|
190
|
-
function getBridgeModel() {
|
|
186
|
+
export function getBridgeModel() {
|
|
191
187
|
return selectedModel;
|
|
192
188
|
}
|
package/dist/index.js
CHANGED
|
@@ -1,29 +1,29 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
};
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
const
|
|
18
|
-
const
|
|
2
|
+
import WebSocket from 'ws';
|
|
3
|
+
import { writeFileSync, chmodSync, existsSync, statSync, readdirSync } from 'fs';
|
|
4
|
+
import { tmpdir } from 'os';
|
|
5
|
+
import { join, dirname } from 'path';
|
|
6
|
+
import { fileURLToPath } from 'url';
|
|
7
|
+
import { createRequire } from 'module';
|
|
8
|
+
import { getValidAuth, ensureFreshToken, isTokenValid, AuthCancelledError } from './auth.js';
|
|
9
|
+
import { spawnClaude, killAll, cancelConversation, setVerbose, setDebug, paint, SECTION_COLORS } from './claude.js';
|
|
10
|
+
import { ensureModelChoice } from './config.js';
|
|
11
|
+
import { checkAndUpdate } from './update.js';
|
|
12
|
+
import { makeBridgeAccumulator, postSaveTurn } from './accumulator.js';
|
|
13
|
+
import { writeSpool, deleteSpool, listSpool } from './outbox.js';
|
|
14
|
+
import { startTurnTimer, stopTurnTimer, formatElapsed } from './timer.js';
|
|
15
|
+
// ESM has no __dirname; derive it. JSON version is read via createRequire to
|
|
16
|
+
// avoid version-sensitive import assertions on a published CLI bin.
|
|
17
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
18
|
+
const { version } = createRequire(import.meta.url)('../package.json');
|
|
19
19
|
// Published tarballs don't ship src/, so this fires only when running the
|
|
20
20
|
// dist build from a live workspace checkout. Catches the trap where editing
|
|
21
21
|
// src/ without re-running tsc leaves you executing stale dist code — banner
|
|
22
22
|
// version matches package.json but behavior doesn't match the source.
|
|
23
23
|
if (__dirname.endsWith('dist')) {
|
|
24
|
-
const srcDir =
|
|
25
|
-
if (
|
|
26
|
-
const newest = (dir) => Math.max(...
|
|
24
|
+
const srcDir = join(__dirname, '..', 'src');
|
|
25
|
+
if (existsSync(srcDir)) {
|
|
26
|
+
const newest = (dir) => Math.max(...readdirSync(dir).map(f => statSync(join(dir, f)).mtimeMs));
|
|
27
27
|
if (newest(srcDir) > newest(__dirname)) {
|
|
28
28
|
console.error('Bridge dist is stale (src/ has been edited since last build). Run: npm run build');
|
|
29
29
|
process.exit(1);
|
|
@@ -45,6 +45,52 @@ const PWA_URL = process.env.BRIDGE_PWA_URL ?? GATEWAY_HTTP.replace('://api.', ':
|
|
|
45
45
|
// ─── In-memory state ──────────────────────────────────────────────────────────
|
|
46
46
|
let currentAuth = null;
|
|
47
47
|
let currentWs = null;
|
|
48
|
+
// Running cost across all turns this process has handled, for the cost segment
|
|
49
|
+
// of the per-turn status line. On a pure subscription the CLI often reports a
|
|
50
|
+
// per-turn cost of 0, in which case this stays at 0 and reads as "plan usage".
|
|
51
|
+
let sessionCostUsd = 0;
|
|
52
|
+
// ─── Status line ──────────────────────────────────────────────────────────────
|
|
53
|
+
//
|
|
54
|
+
// A compact line printed after each completed turn echoing the segments local
|
|
55
|
+
// Claude Code shows in its own status bar: model, context fill, and cost. The
|
|
56
|
+
// 5h/7d subscription rate-limit windows it also shows are deliberately absent —
|
|
57
|
+
// those ride in the API's rate-limit response HEADERS, which the bridge (a
|
|
58
|
+
// consumer of the CLI's stream-json stdout only) never sees. Display only.
|
|
59
|
+
// Raw model id (claude-opus-4-7, claude-sonnet-4-6-20250101) to friendly "Opus
|
|
60
|
+
// 4.7". Regex-based so new dated snapshots format without a table edit; an
|
|
61
|
+
// unrecognised shape falls back to the raw id rather than guessing.
|
|
62
|
+
function friendlyModelName(model) {
|
|
63
|
+
if (!model)
|
|
64
|
+
return 'unknown';
|
|
65
|
+
const m = /claude-(opus|sonnet|haiku)-(\d+)-(\d+)/i.exec(model);
|
|
66
|
+
if (!m)
|
|
67
|
+
return model;
|
|
68
|
+
return `${m[1].charAt(0).toUpperCase()}${m[1].slice(1)} ${m[2]}.${m[3]}`;
|
|
69
|
+
}
|
|
70
|
+
// Context window (tokens) per model, for the context-fill estimate. Keyed by a
|
|
71
|
+
// family regex against the raw model id; first match wins, and an unrecognised
|
|
72
|
+
// id falls back to the Claude 4.x baseline rather than guessing high.
|
|
73
|
+
//
|
|
74
|
+
// Every model the bridge can currently run is 200k: Opus/Sonnet/Haiku 4.x are
|
|
75
|
+
// 200k on the standard path, and the 1M-context window is an API beta that the
|
|
76
|
+
// bridge's subscription print mode never opts into — so it does not apply here.
|
|
77
|
+
// When a model ships with a different standard window, add a row above the
|
|
78
|
+
// baseline; that one line keeps the estimate honest without touching anything
|
|
79
|
+
// else. (The percentage is of the raw window — local Claude's own gauge also
|
|
80
|
+
// reserves output headroom, so its reading runs a few points higher near full.)
|
|
81
|
+
const CONTEXT_WINDOWS = [
|
|
82
|
+
{ match: /claude-(opus|sonnet|haiku)-4/i, tokens: 200_000 },
|
|
83
|
+
];
|
|
84
|
+
const DEFAULT_CONTEXT_WINDOW = 200_000;
|
|
85
|
+
function contextWindowFor(model) {
|
|
86
|
+
if (model) {
|
|
87
|
+
for (const { match, tokens } of CONTEXT_WINDOWS) {
|
|
88
|
+
if (match.test(model))
|
|
89
|
+
return tokens;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
return DEFAULT_CONTEXT_WINDOW;
|
|
93
|
+
}
|
|
48
94
|
// ─── System prompt fetch ──────────────────────────────────────────────────────
|
|
49
95
|
// Pulls the fully-built system prompt from agent-api (via gateway proxy).
|
|
50
96
|
// This MUST match the hosted runtime exactly — STATIC_SYSTEM_PROMPT + dynamic
|
|
@@ -111,7 +157,7 @@ async function fetchSystemPrompt(token, agentSlug) {
|
|
|
111
157
|
}
|
|
112
158
|
// ─── Setup files ──────────────────────────────────────────────────────────────
|
|
113
159
|
function tmpFile(name) {
|
|
114
|
-
return
|
|
160
|
+
return join(tmpdir(), name);
|
|
115
161
|
}
|
|
116
162
|
// Fetch the system prompt and write it to /tmp/agent-${uid}.md. The hosted
|
|
117
163
|
// runtime rebuilds buildSystemBlocks() per turn (dynamic context: vault state,
|
|
@@ -123,9 +169,9 @@ async function writeSystemPrompt(auth, agentSlug) {
|
|
|
123
169
|
const systemPrompt = await fetchSystemPrompt(token, agentSlug);
|
|
124
170
|
writeRestricted(tmpFile(`agent-${uid}.md`), systemPrompt);
|
|
125
171
|
if (VERBOSE) {
|
|
126
|
-
console.log(
|
|
127
|
-
console.log(
|
|
128
|
-
console.log(
|
|
172
|
+
console.log(paint(SECTION_COLORS.system, '\n[bridge:verbose] ─── system prompt ───────────────────────'));
|
|
173
|
+
console.log(paint(SECTION_COLORS.system, systemPrompt));
|
|
174
|
+
console.log(paint(SECTION_COLORS.system, '[bridge:verbose] ─── end system prompt ───────────────────\n'));
|
|
129
175
|
}
|
|
130
176
|
}
|
|
131
177
|
function writeMcpConfig(auth) {
|
|
@@ -149,8 +195,8 @@ async function writeSetupFiles(auth, agentSlug) {
|
|
|
149
195
|
// state. writeFileSync's mode only takes effect on file creation — chmodSync
|
|
150
196
|
// covers the overwrite case so a legacy 0644 file gets tightened on next run.
|
|
151
197
|
function writeRestricted(path, data) {
|
|
152
|
-
|
|
153
|
-
|
|
198
|
+
writeFileSync(path, data, { mode: 0o600 });
|
|
199
|
+
chmodSync(path, 0o600);
|
|
154
200
|
}
|
|
155
201
|
// ─── Helpers ──────────────────────────────────────────────────────────────────
|
|
156
202
|
const UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
@@ -162,7 +208,7 @@ async function handleMessage(conversationId, text, sessionId, history, auth, vau
|
|
|
162
208
|
// Refresh JWT if <10 min remaining before spawning Claude
|
|
163
209
|
let activeAuth = auth;
|
|
164
210
|
try {
|
|
165
|
-
const freshAuth = await
|
|
211
|
+
const freshAuth = await ensureFreshToken(auth);
|
|
166
212
|
if (freshAuth.token !== auth.token) {
|
|
167
213
|
currentAuth = freshAuth;
|
|
168
214
|
activeAuth = freshAuth;
|
|
@@ -172,11 +218,11 @@ async function handleMessage(conversationId, text, sessionId, history, auth, vau
|
|
|
172
218
|
catch (err) {
|
|
173
219
|
// If the cached token still has time, proceed — refresh was preemptive.
|
|
174
220
|
// If it's already invalid, MCP calls will 401 mid-turn — fail fast instead.
|
|
175
|
-
if (!
|
|
221
|
+
if (!isTokenValid(auth.token)) {
|
|
176
222
|
const message = 'Authentication expired and refresh failed — please restart the bridge to sign in again.';
|
|
177
|
-
|
|
223
|
+
stopTurnTimer();
|
|
178
224
|
console.error(`[bridge] ${message} (${err.message})`);
|
|
179
|
-
if (currentWs?.readyState ===
|
|
225
|
+
if (currentWs?.readyState === WebSocket.OPEN) {
|
|
180
226
|
currentWs.send(JSON.stringify({ type: 'error', conversationId, message }));
|
|
181
227
|
}
|
|
182
228
|
return;
|
|
@@ -195,15 +241,15 @@ async function handleMessage(conversationId, text, sessionId, history, auth, vau
|
|
|
195
241
|
}
|
|
196
242
|
catch (err) {
|
|
197
243
|
const message = `System prompt refresh failed: ${err.message}`;
|
|
198
|
-
|
|
244
|
+
stopTurnTimer();
|
|
199
245
|
console.error(`[${new Date().toLocaleTimeString()}] ✗ ${message}`);
|
|
200
|
-
if (currentWs?.readyState ===
|
|
246
|
+
if (currentWs?.readyState === WebSocket.OPEN) {
|
|
201
247
|
currentWs.send(JSON.stringify({ type: 'error', conversationId, message }));
|
|
202
248
|
}
|
|
203
249
|
return;
|
|
204
250
|
}
|
|
205
251
|
let responding = false;
|
|
206
|
-
const accumulator =
|
|
252
|
+
const accumulator = makeBridgeAccumulator();
|
|
207
253
|
const startedAt = Date.now();
|
|
208
254
|
const turnSessionId = sessionId ?? conversationId; // gateway always supplies one; defensive fallback
|
|
209
255
|
// The CLI's `--session-id` is treated as a "claim this new session ID"
|
|
@@ -237,14 +283,14 @@ async function handleMessage(conversationId, text, sessionId, history, auth, vau
|
|
|
237
283
|
// ack is recoverable by drain-on-startup. The gateway dedupes on
|
|
238
284
|
// conversationId, so a replay is idempotent.
|
|
239
285
|
try {
|
|
240
|
-
|
|
286
|
+
writeSpool(record);
|
|
241
287
|
}
|
|
242
288
|
catch (err) {
|
|
243
289
|
console.warn(`[bridge] spool write failed: ${err.message}`);
|
|
244
290
|
}
|
|
245
|
-
const result = await
|
|
291
|
+
const result = await postSaveTurn(GATEWAY_HTTP, activeAuth.token, record);
|
|
246
292
|
if (result.ok) {
|
|
247
|
-
|
|
293
|
+
deleteSpool(record.conversationId);
|
|
248
294
|
}
|
|
249
295
|
else {
|
|
250
296
|
// Leave the spool file in place — next startup or next successful
|
|
@@ -252,7 +298,7 @@ async function handleMessage(conversationId, text, sessionId, history, auth, vau
|
|
|
252
298
|
console.warn(`[bridge] save-turn POST failed (${result.status}): ${result.error ?? 'unknown'} — kept on disk for retry`);
|
|
253
299
|
}
|
|
254
300
|
}
|
|
255
|
-
|
|
301
|
+
spawnClaude({
|
|
256
302
|
conversationId,
|
|
257
303
|
presenceSessionId: claudePinnedSessionId,
|
|
258
304
|
text,
|
|
@@ -267,7 +313,7 @@ async function handleMessage(conversationId, text, sessionId, history, auth, vau
|
|
|
267
313
|
responding = true;
|
|
268
314
|
console.log(`[${new Date().toLocaleTimeString()}] ◐ responding…`);
|
|
269
315
|
}
|
|
270
|
-
if (currentWs?.readyState ===
|
|
316
|
+
if (currentWs?.readyState === WebSocket.OPEN) {
|
|
271
317
|
currentWs.send(JSON.stringify({ type: 'stream', conversationId, event }));
|
|
272
318
|
}
|
|
273
319
|
},
|
|
@@ -275,21 +321,29 @@ async function handleMessage(conversationId, text, sessionId, history, auth, vau
|
|
|
275
321
|
// Ephemeral, non-persisted thread notice (admin-only Local Mode). Relayed
|
|
276
322
|
// by the gateway to the PWA SSE stream as a `notice` AgentEvent; it does
|
|
277
323
|
// NOT go through the turn accumulator, so it never lands in history.
|
|
278
|
-
if (currentWs?.readyState ===
|
|
324
|
+
if (currentWs?.readyState === WebSocket.OPEN) {
|
|
279
325
|
currentWs.send(JSON.stringify({ type: 'notice', conversationId, message }));
|
|
280
326
|
}
|
|
281
327
|
},
|
|
282
|
-
onDone: (messageCount, costUsd, usage, model) => {
|
|
283
|
-
const elapsed =
|
|
284
|
-
const parts = [
|
|
328
|
+
onDone: (messageCount, costUsd, usage, model, contextTokens) => {
|
|
329
|
+
const elapsed = stopTurnTimer();
|
|
330
|
+
const parts = [formatElapsed(elapsed)];
|
|
285
331
|
if (usage)
|
|
286
332
|
parts.push(`in:${usage.input_tokens} out:${usage.output_tokens}`);
|
|
287
333
|
const costStr = costUsd === 0 ? '$0.0000 (plan usage)' : `$${costUsd.toFixed(4)}`;
|
|
288
334
|
parts.push(costStr);
|
|
289
335
|
const suffix = ` ${parts.join(' ')}`;
|
|
290
336
|
console.log(`[${new Date().toLocaleTimeString()}] ✓ done${suffix}`);
|
|
337
|
+
// Status-bar line, mirroring local Claude Code: model · context fill ·
|
|
338
|
+
// session cost. Dimmed and indented so it groups under the done line
|
|
339
|
+
// without competing with it. The cost segment falls back to "plan usage"
|
|
340
|
+
// whenever the running total is 0 (the subscription case).
|
|
341
|
+
sessionCostUsd += costUsd;
|
|
342
|
+
const ctxPct = Math.max(0, Math.min(100, Math.round((contextTokens / contextWindowFor(model)) * 100)));
|
|
343
|
+
const costSeg = sessionCostUsd > 0 ? `$${sessionCostUsd.toFixed(2)} session` : 'plan usage';
|
|
344
|
+
console.log(paint('90', ` 🤖 ${friendlyModelName(model)} · 🧠 ${ctxPct}% · 💰 ${costSeg}`));
|
|
291
345
|
const mapped = toBridgeUsage(usage);
|
|
292
|
-
if (currentWs?.readyState ===
|
|
346
|
+
if (currentWs?.readyState === WebSocket.OPEN) {
|
|
293
347
|
currentWs.send(JSON.stringify({
|
|
294
348
|
type: 'done',
|
|
295
349
|
conversationId,
|
|
@@ -303,10 +357,10 @@ async function handleMessage(conversationId, text, sessionId, history, auth, vau
|
|
|
303
357
|
void finalizeAndPost(buildSpoolRecord(mapped, model));
|
|
304
358
|
},
|
|
305
359
|
onError: (message, usage, model) => {
|
|
306
|
-
const elapsed =
|
|
307
|
-
console.error(`[${new Date().toLocaleTimeString()}] ✗ ${message} (${
|
|
360
|
+
const elapsed = stopTurnTimer();
|
|
361
|
+
console.error(`[${new Date().toLocaleTimeString()}] ✗ ${message} (${formatElapsed(elapsed)})`);
|
|
308
362
|
const mapped = toBridgeUsage(usage);
|
|
309
|
-
if (currentWs?.readyState ===
|
|
363
|
+
if (currentWs?.readyState === WebSocket.OPEN) {
|
|
310
364
|
currentWs.send(JSON.stringify({
|
|
311
365
|
type: 'error',
|
|
312
366
|
conversationId,
|
|
@@ -337,14 +391,14 @@ function toBridgeUsage(usage) {
|
|
|
337
391
|
// dedupes on conversationId — if it already saved via the WS path, the
|
|
338
392
|
// reply is finalized=false and we still delete the spool.
|
|
339
393
|
async function drainOutbox(auth) {
|
|
340
|
-
const records =
|
|
394
|
+
const records = listSpool();
|
|
341
395
|
if (records.length === 0)
|
|
342
396
|
return;
|
|
343
397
|
console.log(`[bridge] draining ${records.length} pending save record${records.length === 1 ? '' : 's'}…`);
|
|
344
398
|
for (const record of records) {
|
|
345
|
-
const result = await
|
|
399
|
+
const result = await postSaveTurn(GATEWAY_HTTP, auth.token, record);
|
|
346
400
|
if (result.ok) {
|
|
347
|
-
|
|
401
|
+
deleteSpool(record.conversationId);
|
|
348
402
|
}
|
|
349
403
|
else {
|
|
350
404
|
console.warn(`[bridge] drain POST failed (${result.status}): ${result.error ?? 'unknown'} — leaving on disk`);
|
|
@@ -357,14 +411,14 @@ async function drainOutbox(auth) {
|
|
|
357
411
|
const PING_INTERVAL_MS = 30_000;
|
|
358
412
|
const PONG_TIMEOUT_MS = 10_000;
|
|
359
413
|
function connect(auth, retryDelay = 1000) {
|
|
360
|
-
const ws = new
|
|
414
|
+
const ws = new WebSocket(GATEWAY_WS, {
|
|
361
415
|
headers: { Authorization: `Bearer ${auth.token}` },
|
|
362
416
|
});
|
|
363
417
|
let pingTimer = null;
|
|
364
418
|
let pongTimer = null;
|
|
365
419
|
function startPing() {
|
|
366
420
|
pingTimer = setInterval(() => {
|
|
367
|
-
if (ws.readyState !==
|
|
421
|
+
if (ws.readyState !== WebSocket.OPEN)
|
|
368
422
|
return;
|
|
369
423
|
ws.send(JSON.stringify({ type: 'ping', ts: Date.now() }));
|
|
370
424
|
pongTimer = setTimeout(() => {
|
|
@@ -419,7 +473,7 @@ function connect(auth, retryDelay = 1000) {
|
|
|
419
473
|
// (PWA→gateway connection dropped). Kill the local Claude Code process for
|
|
420
474
|
// this conversation so it stops generating instead of running to the end.
|
|
421
475
|
if (msg.type === 'cancel' && msg.conversationId) {
|
|
422
|
-
const cancelled =
|
|
476
|
+
const cancelled = cancelConversation(msg.conversationId);
|
|
423
477
|
if (cancelled)
|
|
424
478
|
console.log(`[bridge] ✕ stopped conversation ${msg.conversationId}`);
|
|
425
479
|
return;
|
|
@@ -430,9 +484,9 @@ function connect(auth, retryDelay = 1000) {
|
|
|
430
484
|
const ts = new Date().toLocaleTimeString();
|
|
431
485
|
const hist = Array.isArray(history) ? history : [];
|
|
432
486
|
console.log(`[${ts}] ▶ ${text}${hist.length ? ` (history: ${hist.length} turn${hist.length === 1 ? '' : 's'})` : ''}`);
|
|
433
|
-
|
|
487
|
+
startTurnTimer();
|
|
434
488
|
handleMessage(conversationId, text, sessionId ?? null, hist, auth, vaultFileOpen, clientCapabilities, syncedFolders, agentSlug).catch((err) => {
|
|
435
|
-
|
|
489
|
+
stopTurnTimer();
|
|
436
490
|
console.error(`[bridge] handleMessage error: ${err.message}`);
|
|
437
491
|
});
|
|
438
492
|
});
|
|
@@ -475,24 +529,24 @@ function connect(auth, retryDelay = 1000) {
|
|
|
475
529
|
}
|
|
476
530
|
// ─── Main ─────────────────────────────────────────────────────────────────────
|
|
477
531
|
async function main() {
|
|
478
|
-
console.log(`1Presence Bridge v${
|
|
532
|
+
console.log(`1Presence Bridge v${version}\n`);
|
|
479
533
|
if (VERBOSE) {
|
|
480
|
-
|
|
534
|
+
setVerbose(true);
|
|
481
535
|
console.log('[bridge:verbose] verbose logging enabled — system prompts (magenta), user prompts (blue), assistant text (green), tool inputs (cyan), and tool outputs (yellow) will be printed, colour-coded by kind.\n');
|
|
482
536
|
}
|
|
483
537
|
if (DEBUG) {
|
|
484
|
-
|
|
538
|
+
setDebug(true);
|
|
485
539
|
console.log('[bridge:debug] debug transcript enabled — user prompts, assistant text, tool inputs, and tool outputs will be printed (system prompt omitted; use --verbose for that).\n');
|
|
486
540
|
}
|
|
487
|
-
if (await
|
|
541
|
+
if (await checkAndUpdate())
|
|
488
542
|
return;
|
|
489
543
|
// Auth
|
|
490
|
-
const auth = await
|
|
544
|
+
const auth = await getValidAuth(GATEWAY_HTTP, PWA_URL);
|
|
491
545
|
currentAuth = auth;
|
|
492
546
|
// One-time interactive model choice (only prompts on first run; saved to
|
|
493
547
|
// ~/.1presence/config.json). In a non-TTY environment this is a no-op and
|
|
494
548
|
// Claude Code's own default is used.
|
|
495
|
-
await
|
|
549
|
+
await ensureModelChoice();
|
|
496
550
|
// Write system prompt + MCP config. If this fails the bridge is dead in the
|
|
497
551
|
// water — surface the underlying error rather than letting it bubble up as
|
|
498
552
|
// a generic "Fatal:" with no context.
|
|
@@ -512,7 +566,7 @@ async function main() {
|
|
|
512
566
|
// Graceful shutdown
|
|
513
567
|
const shutdown = () => {
|
|
514
568
|
console.log('\nShutting down…');
|
|
515
|
-
|
|
569
|
+
killAll();
|
|
516
570
|
process.exit(0);
|
|
517
571
|
};
|
|
518
572
|
process.on('SIGINT', shutdown);
|
|
@@ -534,7 +588,7 @@ async function main() {
|
|
|
534
588
|
});
|
|
535
589
|
}
|
|
536
590
|
main().catch((err) => {
|
|
537
|
-
if (err instanceof
|
|
591
|
+
if (err instanceof AuthCancelledError) {
|
|
538
592
|
console.error(`\n${err.message}`);
|
|
539
593
|
console.error('Run `npx @1presence/bridge` again when you are ready to sign in.');
|
|
540
594
|
process.exit(0);
|
package/dist/outbox.js
CHANGED
|
@@ -1,11 +1,6 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
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");
|
|
1
|
+
import { mkdirSync, writeFileSync, readdirSync, readFileSync, unlinkSync } from 'fs';
|
|
2
|
+
import { homedir } from 'os';
|
|
3
|
+
import { join } from 'path';
|
|
9
4
|
// ─── On-disk turn spool ───────────────────────────────────────────────────────
|
|
10
5
|
//
|
|
11
6
|
// Each in-flight bridge turn writes a record to ~/.1presence/outbox/. The file
|
|
@@ -20,31 +15,31 @@ const path_1 = require("path");
|
|
|
20
15
|
//
|
|
21
16
|
// Payload mode 0600 — the file contains the user's assistant transcript and
|
|
22
17
|
// tool inputs. Tightened on every write to handle legacy world-readable files.
|
|
23
|
-
const OUTBOX_DIR =
|
|
18
|
+
const OUTBOX_DIR = join(homedir(), '.1presence', 'outbox');
|
|
24
19
|
function ensureDir() {
|
|
25
|
-
|
|
20
|
+
mkdirSync(OUTBOX_DIR, { recursive: true });
|
|
26
21
|
}
|
|
27
22
|
function pathFor(conversationId) {
|
|
28
|
-
return
|
|
23
|
+
return join(OUTBOX_DIR, `${conversationId}.json`);
|
|
29
24
|
}
|
|
30
|
-
function writeSpool(record) {
|
|
25
|
+
export function writeSpool(record) {
|
|
31
26
|
ensureDir();
|
|
32
|
-
|
|
27
|
+
writeFileSync(pathFor(record.conversationId), JSON.stringify(record), { mode: 0o600 });
|
|
33
28
|
}
|
|
34
|
-
function deleteSpool(conversationId) {
|
|
29
|
+
export function deleteSpool(conversationId) {
|
|
35
30
|
try {
|
|
36
|
-
|
|
31
|
+
unlinkSync(pathFor(conversationId));
|
|
37
32
|
}
|
|
38
33
|
catch { /* already gone — fine */ }
|
|
39
34
|
}
|
|
40
|
-
function listSpool() {
|
|
35
|
+
export function listSpool() {
|
|
41
36
|
ensureDir();
|
|
42
37
|
const out = [];
|
|
43
|
-
for (const file of
|
|
38
|
+
for (const file of readdirSync(OUTBOX_DIR)) {
|
|
44
39
|
if (!file.endsWith('.json'))
|
|
45
40
|
continue;
|
|
46
41
|
try {
|
|
47
|
-
const raw =
|
|
42
|
+
const raw = readFileSync(join(OUTBOX_DIR, file), 'utf-8');
|
|
48
43
|
out.push(JSON.parse(raw));
|
|
49
44
|
}
|
|
50
45
|
catch {
|
package/dist/timer.js
CHANGED
|
@@ -1,12 +1,7 @@
|
|
|
1
|
-
"use strict";
|
|
2
1
|
// Live elapsed-time indicator for the active turn. Writes `\r\x1b[K⏱ Xs`
|
|
3
2
|
// once per second; wraps console.log/error/warn so that any other output
|
|
4
3
|
// clears the timer line before printing, then redraws the timer on the new
|
|
5
4
|
// bottom line. Idempotent — stop is safe to call multiple times.
|
|
6
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
7
|
-
exports.formatElapsed = formatElapsed;
|
|
8
|
-
exports.startTurnTimer = startTurnTimer;
|
|
9
|
-
exports.stopTurnTimer = stopTurnTimer;
|
|
10
5
|
let intervalId = null;
|
|
11
6
|
let startedAt = 0;
|
|
12
7
|
let originalLog = null;
|
|
@@ -19,14 +14,14 @@ function draw() {
|
|
|
19
14
|
const elapsedSec = Math.floor((Date.now() - startedAt) / 1000);
|
|
20
15
|
process.stdout.write(`\r\x1b[K⏱ ${formatElapsed(elapsedSec)}`);
|
|
21
16
|
}
|
|
22
|
-
function formatElapsed(seconds) {
|
|
17
|
+
export function formatElapsed(seconds) {
|
|
23
18
|
if (seconds < 60)
|
|
24
19
|
return `${seconds}s`;
|
|
25
20
|
const m = Math.floor(seconds / 60);
|
|
26
21
|
const s = seconds % 60;
|
|
27
22
|
return `${m}m ${s.toString().padStart(2, '0')}s`;
|
|
28
23
|
}
|
|
29
|
-
function startTurnTimer() {
|
|
24
|
+
export function startTurnTimer() {
|
|
30
25
|
if (intervalId !== null)
|
|
31
26
|
return;
|
|
32
27
|
startedAt = Date.now();
|
|
@@ -39,7 +34,7 @@ function startTurnTimer() {
|
|
|
39
34
|
draw();
|
|
40
35
|
intervalId = setInterval(draw, 1000);
|
|
41
36
|
}
|
|
42
|
-
function stopTurnTimer() {
|
|
37
|
+
export function stopTurnTimer() {
|
|
43
38
|
if (intervalId === null)
|
|
44
39
|
return 0;
|
|
45
40
|
clearInterval(intervalId);
|
package/dist/update.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
const
|
|
1
|
+
import { spawn } from 'child_process';
|
|
2
|
+
import { createRequire } from 'module';
|
|
3
|
+
// ESM JSON imports need version-sensitive import assertions; createRequire reads
|
|
4
|
+
// the manifest synchronously on every Node 18+ without that fragility.
|
|
5
|
+
const { version } = createRequire(import.meta.url)('../package.json');
|
|
6
6
|
function isNewer(a, b) {
|
|
7
7
|
const pa = a.split('.').map(Number);
|
|
8
8
|
const pb = b.split('.').map(Number);
|
|
@@ -14,7 +14,7 @@ function isNewer(a, b) {
|
|
|
14
14
|
}
|
|
15
15
|
return false;
|
|
16
16
|
}
|
|
17
|
-
async function checkAndUpdate() {
|
|
17
|
+
export async function checkAndUpdate() {
|
|
18
18
|
try {
|
|
19
19
|
const res = await fetch('https://registry.npmjs.org/@1presence/bridge/latest', {
|
|
20
20
|
signal: AbortSignal.timeout(3000),
|
|
@@ -23,10 +23,10 @@ async function checkAndUpdate() {
|
|
|
23
23
|
return false;
|
|
24
24
|
const data = await res.json();
|
|
25
25
|
const latest = data.version;
|
|
26
|
-
if (!isNewer(latest,
|
|
26
|
+
if (!isNewer(latest, version))
|
|
27
27
|
return false;
|
|
28
28
|
console.log(`Updating to v${latest}…\n`);
|
|
29
|
-
const child =
|
|
29
|
+
const child = spawn('npx', ['--yes', `@1presence/bridge@${latest}`], {
|
|
30
30
|
stdio: 'inherit',
|
|
31
31
|
env: process.env,
|
|
32
32
|
});
|
package/package.json
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@1presence/bridge",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.42.0",
|
|
4
4
|
"description": "Run 1Presence on your Mac and use your Claude.ai Pro subscription from any device",
|
|
5
|
+
"type": "module",
|
|
5
6
|
"bin": {
|
|
6
7
|
"1presence-bridge": "dist/index.js"
|
|
7
8
|
},
|
|
@@ -18,6 +19,7 @@
|
|
|
18
19
|
"start": "node dist/index.js"
|
|
19
20
|
},
|
|
20
21
|
"dependencies": {
|
|
22
|
+
"@anthropic-ai/claude-agent-sdk": "^0.3.153",
|
|
21
23
|
"ws": "^8.20.0"
|
|
22
24
|
},
|
|
23
25
|
"devDependencies": {
|