@1presence/bridge 0.14.0 → 0.16.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 +1 -1
- package/dist/config.js +24 -59
- package/dist/index.js +100 -40
- package/package.json +1 -1
package/dist/claude.js
CHANGED
|
@@ -227,7 +227,7 @@ function spawnClaude(params) {
|
|
|
227
227
|
// First conversation since bridge started — announce prominently
|
|
228
228
|
// so the user can confirm which model and credential is in use.
|
|
229
229
|
const source = keySource === 'none' || !keySource ? 'claude.ai subscription' : keySource;
|
|
230
|
-
const pin = (0, config_1.getBridgeModel)() ? ' (
|
|
230
|
+
const pin = (0, config_1.getBridgeModel)() ? ' (selected at startup)' : '';
|
|
231
231
|
process.stdout.write(`\n model: ${model ?? 'unknown'}${pin}\n auth: ${source}\n\n`);
|
|
232
232
|
modelAnnounced = true;
|
|
233
233
|
}
|
package/dist/config.js
CHANGED
|
@@ -2,35 +2,16 @@
|
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.ensureModelChoice = ensureModelChoice;
|
|
4
4
|
exports.getBridgeModel = getBridgeModel;
|
|
5
|
-
const fs_1 = require("fs");
|
|
6
|
-
const os_1 = require("os");
|
|
7
|
-
const path_1 = require("path");
|
|
8
5
|
const readline_1 = require("readline");
|
|
9
6
|
const child_process_1 = require("child_process");
|
|
10
|
-
// ───
|
|
7
|
+
// ─── In-memory model choice ───────────────────────────────────────────────────
|
|
11
8
|
//
|
|
12
|
-
//
|
|
13
|
-
//
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
let
|
|
17
|
-
|
|
18
|
-
if (cached)
|
|
19
|
-
return cached;
|
|
20
|
-
try {
|
|
21
|
-
cached = JSON.parse((0, fs_1.readFileSync)(CONFIG_FILE, 'utf-8'));
|
|
22
|
-
}
|
|
23
|
-
catch {
|
|
24
|
-
cached = {};
|
|
25
|
-
}
|
|
26
|
-
return cached;
|
|
27
|
-
}
|
|
28
|
-
function persist(c) {
|
|
29
|
-
(0, fs_1.mkdirSync)(CONFIG_DIR, { recursive: true });
|
|
30
|
-
(0, fs_1.writeFileSync)(CONFIG_FILE, JSON.stringify(c, null, 2), 'utf-8');
|
|
31
|
-
cached = c;
|
|
32
|
-
}
|
|
33
|
-
// ─── Model choice ─────────────────────────────────────────────────────────────
|
|
9
|
+
// The bridge prompts for a model on every interactive startup. The choice is
|
|
10
|
+
// kept in memory for the life of the process — nothing is written to disk.
|
|
11
|
+
// In a non-TTY environment the prompt is skipped and Claude Code's own default
|
|
12
|
+
// is used.
|
|
13
|
+
let selectedModel = null;
|
|
14
|
+
// ─── Default-model probe ──────────────────────────────────────────────────────
|
|
34
15
|
/**
|
|
35
16
|
* Asks the local `claude` CLI which model it would pick by default, by reading
|
|
36
17
|
* the `model` field of the `system/init` stream-json event and killing the
|
|
@@ -106,27 +87,18 @@ const MODEL_OPTIONS = [
|
|
|
106
87
|
{ num: 4, model: 'claude-haiku-4-5', label: 'claude-haiku-4-5' },
|
|
107
88
|
];
|
|
108
89
|
const PROMPT_TIMEOUT_MS = 10_000;
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
const match = MODEL_OPTIONS.find((o) => o.model === previous);
|
|
112
|
-
if (match)
|
|
113
|
-
return match.num;
|
|
114
|
-
}
|
|
115
|
-
// Either never asked (undefined) or explicit no-override (null) → option 1.
|
|
116
|
-
return 1;
|
|
117
|
-
}
|
|
118
|
-
function promptForModel(defaultModel, previousChoice) {
|
|
90
|
+
const DEFAULT_OPTION_NUM = 1;
|
|
91
|
+
function promptForModel(defaultModel) {
|
|
119
92
|
return new Promise((resolve) => {
|
|
120
93
|
const rl = (0, readline_1.createInterface)({ input: process.stdin, output: process.stdout });
|
|
121
|
-
const defaultNum = defaultOptionNum(previousChoice);
|
|
122
94
|
process.stdout.write('\nWhich Claude model should the bridge use?\n');
|
|
123
95
|
for (const opt of MODEL_OPTIONS) {
|
|
124
|
-
const isDefault = opt.num ===
|
|
96
|
+
const isDefault = opt.num === DEFAULT_OPTION_NUM;
|
|
125
97
|
const marker = isDefault ? '*' : ' ';
|
|
126
98
|
const suffix = opt.num === 1 && defaultModel ? ` (${defaultModel})` : '';
|
|
127
99
|
process.stdout.write(` ${marker} ${opt.num}) ${opt.label}${suffix}\n`);
|
|
128
100
|
}
|
|
129
|
-
process.stdout.write(` (* =
|
|
101
|
+
process.stdout.write(` (* = default; auto-selected in ${PROMPT_TIMEOUT_MS / 1000}s if nothing pressed)\n`);
|
|
130
102
|
let settled = false;
|
|
131
103
|
const finish = (model) => {
|
|
132
104
|
if (settled)
|
|
@@ -137,14 +109,14 @@ function promptForModel(defaultModel, previousChoice) {
|
|
|
137
109
|
resolve(model);
|
|
138
110
|
};
|
|
139
111
|
const timer = setTimeout(() => {
|
|
140
|
-
const def = MODEL_OPTIONS.find((o) => o.num ===
|
|
141
|
-
process.stdout.write(`\n(timed out — using option ${
|
|
112
|
+
const def = MODEL_OPTIONS.find((o) => o.num === DEFAULT_OPTION_NUM);
|
|
113
|
+
process.stdout.write(`\n(timed out — using option ${DEFAULT_OPTION_NUM})\n`);
|
|
142
114
|
finish(def.model);
|
|
143
115
|
}, PROMPT_TIMEOUT_MS);
|
|
144
116
|
rl.question(' choice: ', (answer) => {
|
|
145
117
|
const trimmed = answer.trim();
|
|
146
118
|
if (!trimmed) {
|
|
147
|
-
const def = MODEL_OPTIONS.find((o) => o.num ===
|
|
119
|
+
const def = MODEL_OPTIONS.find((o) => o.num === DEFAULT_OPTION_NUM);
|
|
148
120
|
finish(def.model);
|
|
149
121
|
return;
|
|
150
122
|
}
|
|
@@ -160,32 +132,25 @@ function promptForModel(defaultModel, previousChoice) {
|
|
|
160
132
|
});
|
|
161
133
|
}
|
|
162
134
|
/**
|
|
163
|
-
* Asks the user which model to
|
|
164
|
-
*
|
|
165
|
-
*
|
|
166
|
-
* the prompt is skipped: an existing pin is kept, and a first non-TTY start
|
|
167
|
-
* records "no override" so we don't keep trying.
|
|
135
|
+
* Asks the user which model to use for this bridge session. The choice lives
|
|
136
|
+
* in memory only — every startup re-prompts. In a non-TTY environment the
|
|
137
|
+
* prompt is skipped and Claude Code's own default is used.
|
|
168
138
|
*/
|
|
169
139
|
async function ensureModelChoice() {
|
|
170
|
-
const c = load();
|
|
171
140
|
if (!process.stdin.isTTY) {
|
|
172
|
-
|
|
173
|
-
persist({ ...c, model: null });
|
|
141
|
+
selectedModel = null;
|
|
174
142
|
return;
|
|
175
143
|
}
|
|
176
144
|
const defaultModel = await detectClaudeDefaultModel();
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
console.log(`\nPinned model: ${chosen}`);
|
|
181
|
-
console.log(`(Edit or delete ~/.1presence/config.json to change.)\n`);
|
|
145
|
+
selectedModel = await promptForModel(defaultModel);
|
|
146
|
+
if (selectedModel) {
|
|
147
|
+
console.log(`\nUsing model: ${selectedModel} (this session only)\n`);
|
|
182
148
|
}
|
|
183
149
|
else {
|
|
184
|
-
console.log(`\nUsing your Claude Code default${defaultModel ? ` (${defaultModel})` : ''}
|
|
185
|
-
console.log(`(Edit ~/.1presence/config.json to pin a model later.)\n`);
|
|
150
|
+
console.log(`\nUsing your Claude Code default${defaultModel ? ` (${defaultModel})` : ''}.\n`);
|
|
186
151
|
}
|
|
187
152
|
}
|
|
188
|
-
/** Returns the
|
|
153
|
+
/** Returns the model id chosen for this session, or null to defer to Claude Code's own default. */
|
|
189
154
|
function getBridgeModel() {
|
|
190
|
-
return
|
|
155
|
+
return selectedModel;
|
|
191
156
|
}
|
package/dist/index.js
CHANGED
|
@@ -38,61 +38,63 @@ 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) {
|
|
76
79
|
return (0, path_1.join)((0, os_1.tmpdir)(), name);
|
|
77
80
|
}
|
|
78
|
-
|
|
81
|
+
// Fetch the system prompt and write it to /tmp/agent-${uid}.md. The hosted
|
|
82
|
+
// runtime rebuilds buildSystemBlocks() per turn (dynamic context: vault state,
|
|
83
|
+
// connector status, palace, onboarding phase, skills) — call this per turn in
|
|
84
|
+
// the bridge too, otherwise newly shipped skills and mid-session vault writes
|
|
85
|
+
// never reach a long-running bridge. Throws on failure; caller must handle.
|
|
86
|
+
async function writeSystemPrompt(auth) {
|
|
79
87
|
const { uid, token } = auth;
|
|
80
|
-
|
|
81
|
-
// identically to the cloud agent (tool-use policy, ui_payload sparsity,
|
|
82
|
-
// plan thresholds, Gmail safety, connector pivots, personal AGENT.md, etc.).
|
|
83
|
-
// If the pod isn't reachable yet, fall back to the user's AGENT.md alone —
|
|
84
|
-
// Claude will still run, just without the platform policy layer.
|
|
85
|
-
const systemPrompt = (await fetchSystemPrompt(token))
|
|
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 ───────────────────────');
|
|
92
92
|
console.log(systemPrompt);
|
|
93
93
|
console.log('[bridge:verbose] ─── end system prompt ───────────────────\n');
|
|
94
94
|
}
|
|
95
|
-
|
|
95
|
+
}
|
|
96
|
+
function writeMcpConfig(auth) {
|
|
97
|
+
const { uid, token } = auth;
|
|
96
98
|
const mcpConfig = {
|
|
97
99
|
mcpServers: {
|
|
98
100
|
'1presence': {
|
|
@@ -104,6 +106,10 @@ async function writeSetupFiles(auth) {
|
|
|
104
106
|
};
|
|
105
107
|
writeRestricted(tmpFile(`mcp-${uid}.json`), JSON.stringify(mcpConfig, null, 2));
|
|
106
108
|
}
|
|
109
|
+
async function writeSetupFiles(auth) {
|
|
110
|
+
await writeSystemPrompt(auth);
|
|
111
|
+
writeMcpConfig(auth);
|
|
112
|
+
}
|
|
107
113
|
// The MCP config embeds a Bearer JWT and the system prompt may contain vault
|
|
108
114
|
// state. writeFileSync's mode only takes effect on file creation — chmodSync
|
|
109
115
|
// covers the overwrite case so a legacy 0644 file gets tightened on next run.
|
|
@@ -136,6 +142,24 @@ async function handleMessage(conversationId, text, sessionId, auth, vaultFileOpe
|
|
|
136
142
|
}
|
|
137
143
|
console.warn(`[bridge] token refresh failed (proceeding with current token): ${err.message}`);
|
|
138
144
|
}
|
|
145
|
+
// Refresh the system prompt on every turn — the hosted runtime rebuilds its
|
|
146
|
+
// dynamic context per turn (vault state, connector status, palace, onboarding
|
|
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).
|
|
152
|
+
try {
|
|
153
|
+
await writeSystemPrompt(activeAuth);
|
|
154
|
+
}
|
|
155
|
+
catch (err) {
|
|
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;
|
|
162
|
+
}
|
|
139
163
|
let responding = false;
|
|
140
164
|
(0, claude_1.spawnClaude)({
|
|
141
165
|
conversationId,
|
|
@@ -242,7 +266,9 @@ function connect(auth, retryDelay = 1000) {
|
|
|
242
266
|
try {
|
|
243
267
|
msg = JSON.parse(raw.toString());
|
|
244
268
|
}
|
|
245
|
-
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})`);
|
|
246
272
|
return;
|
|
247
273
|
}
|
|
248
274
|
// Application-level pong — clear the timeout
|
|
@@ -277,19 +303,26 @@ function connect(auth, retryDelay = 1000) {
|
|
|
277
303
|
console.log(`Bridge disconnected (${code}). Reconnecting in ${delay / 1000}s…`);
|
|
278
304
|
setTimeout(async () => {
|
|
279
305
|
try {
|
|
280
|
-
// 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.
|
|
281
309
|
if (currentAuth)
|
|
282
310
|
await writeSetupFiles(currentAuth);
|
|
283
311
|
connect(currentAuth, Math.min(retryDelay * 2, 30_000));
|
|
284
312
|
}
|
|
285
313
|
catch (err) {
|
|
286
|
-
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));
|
|
287
318
|
}
|
|
288
319
|
}, delay);
|
|
289
320
|
});
|
|
290
321
|
ws.on('error', (err) => {
|
|
291
322
|
// close event fires after error — reconnect handled there
|
|
292
323
|
console.error(`[bridge] ws error: ${err.message}`);
|
|
324
|
+
if (VERBOSE && err.stack)
|
|
325
|
+
console.error(err.stack);
|
|
293
326
|
});
|
|
294
327
|
return ws;
|
|
295
328
|
}
|
|
@@ -309,9 +342,19 @@ async function main() {
|
|
|
309
342
|
// ~/.1presence/config.json). In a non-TTY environment this is a no-op and
|
|
310
343
|
// Claude Code's own default is used.
|
|
311
344
|
await (0, config_1.ensureModelChoice)();
|
|
312
|
-
// 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.
|
|
313
348
|
process.stdout.write('Setting up…');
|
|
314
|
-
|
|
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
|
+
}
|
|
315
358
|
process.stdout.write(' done.\n');
|
|
316
359
|
// Connect
|
|
317
360
|
connect(auth);
|
|
@@ -323,6 +366,21 @@ async function main() {
|
|
|
323
366
|
};
|
|
324
367
|
process.on('SIGINT', shutdown);
|
|
325
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
|
+
});
|
|
326
384
|
}
|
|
327
385
|
main().catch((err) => {
|
|
328
386
|
if (err instanceof auth_1.AuthCancelledError) {
|
|
@@ -331,5 +389,7 @@ main().catch((err) => {
|
|
|
331
389
|
process.exit(0);
|
|
332
390
|
}
|
|
333
391
|
console.error('Fatal:', err.message);
|
|
392
|
+
if (err.stack)
|
|
393
|
+
console.error(err.stack);
|
|
334
394
|
process.exit(1);
|
|
335
395
|
});
|