@1presence/bridge 0.15.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.
Files changed (2) hide show
  1. package/dist/index.js +81 -37
  2. package/package.json +1 -1
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
- // ─── Vault file fetch ─────────────────────────────────────────────────────────
42
- async function fetchVaultFile(path, token) {
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
- const res = await fetch(`${GATEWAY_HTTP}/vault/file?path=${encodeURIComponent(path)}`, {
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
- return null;
58
+ catch (err) {
59
+ throw new Error(`fetch failed for ${url}: ${err.message}`);
53
60
  }
54
- }
55
- // Pulls the fully-built system prompt from agent-api (via gateway proxy).
56
- // This matches the hosted runtime exactly: STATIC_SYSTEM_PROMPT + dynamic
57
- // context (timezone, connector scopes, vault state, personal AGENT.md, etc.).
58
- // Falls back to null on failure — caller decides whether to use AGENT.md only.
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
- const tz = Intl.DateTimeFormat().resolvedOptions().timeZone;
62
- const res = await fetch(`${GATEWAY_HTTP}/system-prompt?timezone=${encodeURIComponent(tz)}`, {
63
- headers: { Authorization: `Bearer ${token}` },
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
- catch {
71
- return null;
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 = (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 ───────────────────────');
@@ -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). Without this the bridge holds a frozen
148
- // snapshot from process start and misses anything added since.
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
- console.warn(`[bridge] system prompt refresh failed (using cached): ${err.message}`);
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('Reconnect failed:', err.message);
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
- await writeSetupFiles(auth);
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
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@1presence/bridge",
3
- "version": "0.15.0",
3
+ "version": "0.16.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"