@1presence/bridge 0.15.0 → 0.17.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/config.js +63 -27
- package/dist/index.js +81 -37
- package/package.json +1 -1
package/dist/config.js
CHANGED
|
@@ -90,45 +90,81 @@ const PROMPT_TIMEOUT_MS = 10_000;
|
|
|
90
90
|
const DEFAULT_OPTION_NUM = 1;
|
|
91
91
|
function promptForModel(defaultModel) {
|
|
92
92
|
return new Promise((resolve) => {
|
|
93
|
-
const
|
|
93
|
+
const initialIdx = Math.max(0, MODEL_OPTIONS.findIndex((o) => o.num === DEFAULT_OPTION_NUM));
|
|
94
|
+
let idx = initialIdx;
|
|
94
95
|
process.stdout.write('\nWhich Claude model should the bridge use?\n');
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
96
|
+
process.stdout.write(` (↑/↓ to move, Enter to select, 1–${MODEL_OPTIONS.length} to jump, auto-selects in ${PROMPT_TIMEOUT_MS / 1000}s)\n`);
|
|
97
|
+
let rendered = 0;
|
|
98
|
+
const render = () => {
|
|
99
|
+
if (rendered > 0)
|
|
100
|
+
process.stdout.write(`\x1b[${rendered}A`);
|
|
101
|
+
rendered = 0;
|
|
102
|
+
for (let i = 0; i < MODEL_OPTIONS.length; i++) {
|
|
103
|
+
const opt = MODEL_OPTIONS[i];
|
|
104
|
+
const active = i === idx;
|
|
105
|
+
const marker = active ? '›' : ' ';
|
|
106
|
+
const suffix = opt.num === 1 && defaultModel ? ` (${defaultModel})` : '';
|
|
107
|
+
const text = ` ${marker} ${opt.num}) ${opt.label}${suffix}`;
|
|
108
|
+
const line = active ? `\x1b[1;36m${text}\x1b[0m` : text;
|
|
109
|
+
process.stdout.write(`\x1b[2K${line}\n`);
|
|
110
|
+
rendered++;
|
|
111
|
+
}
|
|
112
|
+
};
|
|
113
|
+
render();
|
|
114
|
+
(0, readline_1.emitKeypressEvents)(process.stdin);
|
|
115
|
+
const wasRaw = process.stdin.isRaw;
|
|
116
|
+
if (process.stdin.isTTY)
|
|
117
|
+
process.stdin.setRawMode(true);
|
|
118
|
+
process.stdin.resume();
|
|
102
119
|
let settled = false;
|
|
120
|
+
const cleanup = () => {
|
|
121
|
+
clearTimeout(timer);
|
|
122
|
+
process.stdin.removeListener('keypress', onKey);
|
|
123
|
+
if (process.stdin.isTTY)
|
|
124
|
+
process.stdin.setRawMode(!!wasRaw);
|
|
125
|
+
process.stdin.pause();
|
|
126
|
+
};
|
|
103
127
|
const finish = (model) => {
|
|
104
128
|
if (settled)
|
|
105
129
|
return;
|
|
106
130
|
settled = true;
|
|
107
|
-
|
|
108
|
-
rl.close();
|
|
131
|
+
cleanup();
|
|
109
132
|
resolve(model);
|
|
110
133
|
};
|
|
111
|
-
const
|
|
112
|
-
|
|
113
|
-
process.stdout.write(`\n(timed out — using option ${DEFAULT_OPTION_NUM})\n`);
|
|
114
|
-
finish(def.model);
|
|
115
|
-
}, PROMPT_TIMEOUT_MS);
|
|
116
|
-
rl.question(' choice: ', (answer) => {
|
|
117
|
-
const trimmed = answer.trim();
|
|
118
|
-
if (!trimmed) {
|
|
119
|
-
const def = MODEL_OPTIONS.find((o) => o.num === DEFAULT_OPTION_NUM);
|
|
120
|
-
finish(def.model);
|
|
134
|
+
const onKey = (_str, key) => {
|
|
135
|
+
if (!key)
|
|
121
136
|
return;
|
|
137
|
+
if (key.ctrl && key.name === 'c') {
|
|
138
|
+
cleanup();
|
|
139
|
+
process.stdout.write('\n');
|
|
140
|
+
process.exit(130);
|
|
122
141
|
}
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
finish(opt.model);
|
|
142
|
+
if (key.name === 'up' || key.name === 'k') {
|
|
143
|
+
idx = (idx - 1 + MODEL_OPTIONS.length) % MODEL_OPTIONS.length;
|
|
144
|
+
render();
|
|
127
145
|
return;
|
|
128
146
|
}
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
147
|
+
if (key.name === 'down' || key.name === 'j') {
|
|
148
|
+
idx = (idx + 1) % MODEL_OPTIONS.length;
|
|
149
|
+
render();
|
|
150
|
+
return;
|
|
151
|
+
}
|
|
152
|
+
if (key.name === 'return' || key.name === 'enter') {
|
|
153
|
+
finish(MODEL_OPTIONS[idx].model);
|
|
154
|
+
return;
|
|
155
|
+
}
|
|
156
|
+
const n = Number(key.sequence ?? '');
|
|
157
|
+
if (Number.isInteger(n) && n >= 1 && n <= MODEL_OPTIONS.length) {
|
|
158
|
+
idx = n - 1;
|
|
159
|
+
render();
|
|
160
|
+
finish(MODEL_OPTIONS[idx].model);
|
|
161
|
+
}
|
|
162
|
+
};
|
|
163
|
+
process.stdin.on('keypress', onKey);
|
|
164
|
+
const timer = setTimeout(() => {
|
|
165
|
+
process.stdout.write(`\n(timed out — using option ${MODEL_OPTIONS[idx].num})\n`);
|
|
166
|
+
finish(MODEL_OPTIONS[idx].model);
|
|
167
|
+
}, PROMPT_TIMEOUT_MS);
|
|
132
168
|
});
|
|
133
169
|
}
|
|
134
170
|
/**
|
package/dist/index.js
CHANGED
|
@@ -38,38 +38,41 @@ const PWA_URL = process.env.BRIDGE_PWA_URL ?? GATEWAY_HTTP.replace('://api.', ':
|
|
|
38
38
|
// ─── In-memory state ──────────────────────────────────────────────────────────
|
|
39
39
|
let currentAuth = null;
|
|
40
40
|
let currentWs = null;
|
|
41
|
-
// ───
|
|
42
|
-
|
|
41
|
+
// ─── System prompt fetch ──────────────────────────────────────────────────────
|
|
42
|
+
// Pulls the fully-built system prompt from agent-api (via gateway proxy).
|
|
43
|
+
// This MUST match the hosted runtime exactly — STATIC_SYSTEM_PROMPT + dynamic
|
|
44
|
+
// context (timezone, connector scopes, vault state, personal AGENT.md, skills,
|
|
45
|
+
// onboarding). There is intentionally NO fallback: if this fails the bridge
|
|
46
|
+
// must surface the error, not silently degrade to a different prompt source
|
|
47
|
+
// (which historically caused the "Skills section authoritative" rule to
|
|
48
|
+
// vanish and the agent to vault-hunt for skills).
|
|
49
|
+
async function fetchSystemPrompt(token) {
|
|
50
|
+
const tz = Intl.DateTimeFormat().resolvedOptions().timeZone;
|
|
51
|
+
const url = `${GATEWAY_HTTP}/system-prompt?timezone=${encodeURIComponent(tz)}`;
|
|
52
|
+
let res;
|
|
43
53
|
try {
|
|
44
|
-
|
|
54
|
+
res = await fetch(url, {
|
|
45
55
|
headers: { Authorization: `Bearer ${token}` },
|
|
46
56
|
});
|
|
47
|
-
if (!res.ok)
|
|
48
|
-
return null;
|
|
49
|
-
return res.text();
|
|
50
57
|
}
|
|
51
|
-
catch {
|
|
52
|
-
|
|
58
|
+
catch (err) {
|
|
59
|
+
throw new Error(`fetch failed for ${url}: ${err.message}`);
|
|
53
60
|
}
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
async function fetchSystemPrompt(token) {
|
|
61
|
+
if (!res.ok) {
|
|
62
|
+
const body = await res.text().catch(() => '<unreadable body>');
|
|
63
|
+
throw new Error(`/system-prompt returned ${res.status} ${res.statusText}: ${body.slice(0, 500)}`);
|
|
64
|
+
}
|
|
65
|
+
let data;
|
|
60
66
|
try {
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
});
|
|
65
|
-
if (!res.ok)
|
|
66
|
-
return null;
|
|
67
|
-
const data = await res.json();
|
|
68
|
-
return data.text ?? null;
|
|
67
|
+
data = await res.json();
|
|
68
|
+
}
|
|
69
|
+
catch (err) {
|
|
70
|
+
throw new Error(`/system-prompt returned non-JSON: ${err.message}`);
|
|
69
71
|
}
|
|
70
|
-
|
|
71
|
-
|
|
72
|
+
if (!data.text) {
|
|
73
|
+
throw new Error(`/system-prompt returned no "text" field (got keys: ${Object.keys(data).join(', ') || '<none>'})`);
|
|
72
74
|
}
|
|
75
|
+
return data.text;
|
|
73
76
|
}
|
|
74
77
|
// ─── Setup files ──────────────────────────────────────────────────────────────
|
|
75
78
|
function tmpFile(name) {
|
|
@@ -79,13 +82,10 @@ function tmpFile(name) {
|
|
|
79
82
|
// runtime rebuilds buildSystemBlocks() per turn (dynamic context: vault state,
|
|
80
83
|
// connector status, palace, onboarding phase, skills) — call this per turn in
|
|
81
84
|
// the bridge too, otherwise newly shipped skills and mid-session vault writes
|
|
82
|
-
// never reach a long-running bridge.
|
|
85
|
+
// never reach a long-running bridge. Throws on failure; caller must handle.
|
|
83
86
|
async function writeSystemPrompt(auth) {
|
|
84
87
|
const { uid, token } = auth;
|
|
85
|
-
const systemPrompt =
|
|
86
|
-
?? (await fetchVaultFile('AGENT.md', token))
|
|
87
|
-
?? (await fetchVaultFile('CLAUDE.md', token))
|
|
88
|
-
?? '';
|
|
88
|
+
const systemPrompt = await fetchSystemPrompt(token);
|
|
89
89
|
writeRestricted(tmpFile(`agent-${uid}.md`), systemPrompt);
|
|
90
90
|
if (VERBOSE) {
|
|
91
91
|
console.log('\n[bridge:verbose] ─── system prompt ───────────────────────');
|
|
@@ -144,13 +144,21 @@ async function handleMessage(conversationId, text, sessionId, auth, vaultFileOpe
|
|
|
144
144
|
}
|
|
145
145
|
// Refresh the system prompt on every turn — the hosted runtime rebuilds its
|
|
146
146
|
// dynamic context per turn (vault state, connector status, palace, onboarding
|
|
147
|
-
// phase, newly enabled skills).
|
|
148
|
-
// snapshot
|
|
147
|
+
// phase, newly enabled skills). If this fails we abort the turn rather than
|
|
148
|
+
// silently reuse a stale snapshot — parity with agent-api is the whole point
|
|
149
|
+
// of bridge mode, and stale prompts have caused user-visible regressions
|
|
150
|
+
// (e.g. agent vault-hunting for skills because the Skills authoritative
|
|
151
|
+
// rule was missing from the previous snapshot).
|
|
149
152
|
try {
|
|
150
153
|
await writeSystemPrompt(activeAuth);
|
|
151
154
|
}
|
|
152
155
|
catch (err) {
|
|
153
|
-
|
|
156
|
+
const message = `System prompt refresh failed: ${err.message}`;
|
|
157
|
+
console.error(`[${new Date().toLocaleTimeString()}] ✗ ${message}`);
|
|
158
|
+
if (currentWs?.readyState === ws_1.default.OPEN) {
|
|
159
|
+
currentWs.send(JSON.stringify({ type: 'error', conversationId, message }));
|
|
160
|
+
}
|
|
161
|
+
return;
|
|
154
162
|
}
|
|
155
163
|
let responding = false;
|
|
156
164
|
(0, claude_1.spawnClaude)({
|
|
@@ -258,7 +266,9 @@ function connect(auth, retryDelay = 1000) {
|
|
|
258
266
|
try {
|
|
259
267
|
msg = JSON.parse(raw.toString());
|
|
260
268
|
}
|
|
261
|
-
catch {
|
|
269
|
+
catch (err) {
|
|
270
|
+
const preview = raw.toString().slice(0, 200);
|
|
271
|
+
console.error(`[bridge] failed to parse ws message as JSON: ${err.message} (raw: ${preview})`);
|
|
262
272
|
return;
|
|
263
273
|
}
|
|
264
274
|
// Application-level pong — clear the timeout
|
|
@@ -293,19 +303,26 @@ function connect(auth, retryDelay = 1000) {
|
|
|
293
303
|
console.log(`Bridge disconnected (${code}). Reconnecting in ${delay / 1000}s…`);
|
|
294
304
|
setTimeout(async () => {
|
|
295
305
|
try {
|
|
296
|
-
// Refresh setup files on reconnect in case token was refreshed
|
|
306
|
+
// Refresh setup files on reconnect in case token was refreshed.
|
|
307
|
+
// If /system-prompt is down this throws — log and retry; the bridge
|
|
308
|
+
// is useless without a current prompt so don't paper over it.
|
|
297
309
|
if (currentAuth)
|
|
298
310
|
await writeSetupFiles(currentAuth);
|
|
299
311
|
connect(currentAuth, Math.min(retryDelay * 2, 30_000));
|
|
300
312
|
}
|
|
301
313
|
catch (err) {
|
|
302
|
-
console.error(
|
|
314
|
+
console.error(`[bridge] reconnect setup failed: ${err.message}`);
|
|
315
|
+
console.error('[bridge] will retry connection anyway — system prompt may be stale until next refresh');
|
|
316
|
+
if (currentAuth)
|
|
317
|
+
connect(currentAuth, Math.min(retryDelay * 2, 30_000));
|
|
303
318
|
}
|
|
304
319
|
}, delay);
|
|
305
320
|
});
|
|
306
321
|
ws.on('error', (err) => {
|
|
307
322
|
// close event fires after error — reconnect handled there
|
|
308
323
|
console.error(`[bridge] ws error: ${err.message}`);
|
|
324
|
+
if (VERBOSE && err.stack)
|
|
325
|
+
console.error(err.stack);
|
|
309
326
|
});
|
|
310
327
|
return ws;
|
|
311
328
|
}
|
|
@@ -325,9 +342,19 @@ async function main() {
|
|
|
325
342
|
// ~/.1presence/config.json). In a non-TTY environment this is a no-op and
|
|
326
343
|
// Claude Code's own default is used.
|
|
327
344
|
await (0, config_1.ensureModelChoice)();
|
|
328
|
-
// Write system prompt + MCP config
|
|
345
|
+
// Write system prompt + MCP config. If this fails the bridge is dead in the
|
|
346
|
+
// water — surface the underlying error rather than letting it bubble up as
|
|
347
|
+
// a generic "Fatal:" with no context.
|
|
329
348
|
process.stdout.write('Setting up…');
|
|
330
|
-
|
|
349
|
+
try {
|
|
350
|
+
await writeSetupFiles(auth);
|
|
351
|
+
}
|
|
352
|
+
catch (err) {
|
|
353
|
+
process.stdout.write(' FAILED.\n');
|
|
354
|
+
console.error(`[bridge] setup failed: ${err.message}`);
|
|
355
|
+
console.error('[bridge] cannot start without a system prompt from the gateway. Check network, auth, and that the gateway is reachable.');
|
|
356
|
+
process.exit(1);
|
|
357
|
+
}
|
|
331
358
|
process.stdout.write(' done.\n');
|
|
332
359
|
// Connect
|
|
333
360
|
connect(auth);
|
|
@@ -339,6 +366,21 @@ async function main() {
|
|
|
339
366
|
};
|
|
340
367
|
process.on('SIGINT', shutdown);
|
|
341
368
|
process.on('SIGTERM', shutdown);
|
|
369
|
+
// Surface anything that would otherwise vanish into the void. Without these,
|
|
370
|
+
// a thrown error inside an async callback (ws handler, child process event,
|
|
371
|
+
// setTimeout) silently kills the bridge with no diagnostic.
|
|
372
|
+
process.on('uncaughtException', (err) => {
|
|
373
|
+
console.error(`[bridge] uncaughtException: ${err.message}`);
|
|
374
|
+
if (err.stack)
|
|
375
|
+
console.error(err.stack);
|
|
376
|
+
});
|
|
377
|
+
process.on('unhandledRejection', (reason) => {
|
|
378
|
+
const message = reason instanceof Error ? reason.message : String(reason);
|
|
379
|
+
const stack = reason instanceof Error ? reason.stack : undefined;
|
|
380
|
+
console.error(`[bridge] unhandledRejection: ${message}`);
|
|
381
|
+
if (stack)
|
|
382
|
+
console.error(stack);
|
|
383
|
+
});
|
|
342
384
|
}
|
|
343
385
|
main().catch((err) => {
|
|
344
386
|
if (err instanceof auth_1.AuthCancelledError) {
|
|
@@ -347,5 +389,7 @@ main().catch((err) => {
|
|
|
347
389
|
process.exit(0);
|
|
348
390
|
}
|
|
349
391
|
console.error('Fatal:', err.message);
|
|
392
|
+
if (err.stack)
|
|
393
|
+
console.error(err.stack);
|
|
350
394
|
process.exit(1);
|
|
351
395
|
});
|