@1presence/bridge 0.55.0 → 0.56.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 CHANGED
@@ -163,6 +163,28 @@ function describeCliFailure(apiErrorText, authFailure) {
163
163
  }
164
164
  return 'Local Mode stopped unexpectedly. Please try again.';
165
165
  }
166
+ // Join every non-empty error fragment the SDK might carry (a `result` string,
167
+ // an `errors: string[]`, an error enum, a request_id, …) into one de-duplicated
168
+ // line. A 4xx/5xx surfaces its detail unpredictably: `result` rides on the
169
+ // success-shaped error result, `errors[]` on SDKResultError, the coarse bucket
170
+ // on `assistant.error` — so we gather from all of them rather than trusting one.
171
+ function joinErrorDetail(...parts) {
172
+ const seen = new Set();
173
+ const out = [];
174
+ for (const part of parts) {
175
+ for (const item of Array.isArray(part) ? part : [part]) {
176
+ const s = (typeof item === 'string' ? item : item == null ? '' : String(item)).trim();
177
+ if (s && !seen.has(s)) {
178
+ seen.add(s);
179
+ out.push(s);
180
+ }
181
+ }
182
+ }
183
+ return out.join(' | ');
184
+ }
185
+ // Lines the SDK/CLI writes to its own stderr that look like a failure — surfaced
186
+ // even outside verbose mode so a turn that dies upstream leaves a trail.
187
+ const SDK_STDERR_ERROR_RE = /\b(error|exception|fail(?:ed|ure)?|invalid|unauthor|forbidden|refus|denied|40[0-9]|429|5\d\d|overloaded|rate.?limit)\b/i;
166
188
  /**
167
189
  * Copy for an actionable rate-limit notice. The SDK emits `rate_limit_event`
168
190
  * whenever rate-limit info CHANGES — including the routine `allowed` case on
@@ -492,14 +514,31 @@ export function spawnClaude(params) {
492
514
  const status = event['api_error_status'];
493
515
  if (status === 401 || status === 403)
494
516
  sawAuthFailure = true;
495
- if (!apiErrorText && typeof event['result'] === 'string') {
496
- apiErrorText = event['result'].trim();
497
- }
517
+ // Gather the reason from EVERY error-bearing field, not just `result`
518
+ // (empty on most 4xx/5xx). `errors[]` rides on SDKResultError. Fold into
519
+ // apiErrorText, keeping any coarse enum the assistant.error path set.
520
+ const detail = joinErrorDetail(event['result'], event['errors']);
521
+ apiErrorText = joinErrorDetail(apiErrorText, detail) || apiErrorText;
522
+ const subtype = event['subtype'] ? ` subtype=${event['subtype']}` : '';
498
523
  // Operator visibility — a result-borne error would otherwise reach the
499
524
  // user as a chat error with NOTHING in the bridge logs (the failure
500
525
  // mode that made the empty-content `invalid_request` regression so hard
501
- // to diagnose). Mechanical log only; no product logic.
502
- process.stderr.write(paint(SECTION_COLORS.result, `[bridge] result error${status ? ` (${status})` : ''}: ${apiErrorText || 'unknown'}`) + '\n');
526
+ // to diagnose). Logged in ALL modes (not just verbose). Mechanical log
527
+ // only; no product logic.
528
+ process.stderr.write(paint(SECTION_COLORS.result, `[bridge] result error${status != null ? ` (${status})` : ''}${subtype}: ${apiErrorText || 'unknown'}`) + '\n');
529
+ // When no field carried a reason (the SDK bucketed it as bare "unknown"),
530
+ // dump the raw result object so nothing is silently swallowed — the only
531
+ // remaining place a clue could hide. Bounded so it can't flood the log.
532
+ if (!detail) {
533
+ let raw;
534
+ try {
535
+ raw = JSON.stringify(event);
536
+ }
537
+ catch {
538
+ raw = String(event);
539
+ }
540
+ process.stderr.write(paint(SECTION_COLORS.result, `[bridge] result raw: ${raw.slice(0, 2000)}`) + '\n');
541
+ }
503
542
  }
504
543
  }
505
544
  return true;
@@ -571,8 +610,15 @@ export function spawnClaude(params) {
571
610
  includePartialMessages: false, // whole messages, matching the old non-partial path
572
611
  permissionMode: 'default',
573
612
  env: safeEnv,
574
- stderr: (line) => { if (verbose && line.trim())
575
- process.stderr.write(`[claude] ${line.trim()}\n`); },
613
+ // Surface the SDK/CLI's own stderr. In verbose mode pass everything; in
614
+ // normal mode pass only error-looking lines so an upstream failure still
615
+ // leaves a trail (the SDK abstracts the raw API body into an enum, so its
616
+ // stderr is sometimes the only place the real reason appears).
617
+ stderr: (line) => {
618
+ const t = line.trim();
619
+ if (t && (verbose || SDK_STDERR_ERROR_RE.test(t)))
620
+ process.stderr.write(`[claude] ${t}\n`);
621
+ },
576
622
  ...(pinnedModel ? { model: pinnedModel } : {}),
577
623
  };
578
624
  const promptMessages = buildPromptMessages(history);
@@ -584,12 +630,21 @@ export function spawnClaude(params) {
584
630
  continue;
585
631
  switch (m.type) {
586
632
  case 'system': {
587
- if (m.subtype === 'init') {
633
+ const subtype = m.subtype;
634
+ if (subtype === 'init') {
588
635
  const init = m;
589
636
  const event = { type: 'system', subtype: 'init', model: init.model, apiKeySource: init.apiKeySource };
590
637
  if (handleEvent(event))
591
638
  onEvent(event);
592
639
  }
640
+ else if (subtype === 'api_retry') {
641
+ // The SDK retries retryable API failures (5xx / overloaded / some
642
+ // 429s) before giving up. Log each attempt with its status + bucket
643
+ // so a turn that eventually dies after retries leaves a full trail —
644
+ // in all modes, not just verbose. Diagnostic only; never forwarded.
645
+ const r = m;
646
+ process.stderr.write(paint(SECTION_COLORS.result, `[bridge] api retry ${r.attempt ?? '?'}/${r.max_retries ?? '?'}${r.error_status != null ? ` (${r.error_status})` : ''}: ${r.error ?? 'unknown'}${r.retry_delay_ms != null ? `, next in ${r.retry_delay_ms}ms` : ''}`) + '\n');
647
+ }
593
648
  break;
594
649
  }
595
650
  case 'assistant': {
@@ -599,9 +654,21 @@ export function spawnClaude(params) {
599
654
  sawApiError = true;
600
655
  if (am.error === 'authentication_failed' || am.error === 'oauth_org_not_allowed')
601
656
  sawAuthFailure = true;
602
- if (!apiErrorText)
603
- apiErrorText = `API Error: ${am.error}`;
604
- process.stderr.write(paint(SECTION_COLORS.result, `[bridge] assistant error: ${am.error}`) + '\n');
657
+ // Pull any extra reason off the synthetic error message + the
658
+ // request_id (the handle to look the failure up upstream) so the
659
+ // log/chat error isn't just the coarse `unknown` bucket.
660
+ const msgText = Array.isArray(am.message?.['content'])
661
+ ? am.message['content']
662
+ .filter((b) => b['type'] === 'text' && typeof b['text'] === 'string')
663
+ .map((b) => b['text'].trim())
664
+ .filter(Boolean)
665
+ .join(' ')
666
+ : '';
667
+ const rid = am.request_id ? `request_id=${am.request_id}` : '';
668
+ const full = joinErrorDetail(`API Error: ${am.error}`, msgText, rid);
669
+ if (!apiErrorText || /^API Error: \w+$/.test(apiErrorText))
670
+ apiErrorText = full;
671
+ process.stderr.write(paint(SECTION_COLORS.result, `[bridge] assistant error: ${joinErrorDetail(am.error, msgText, rid)}`) + '\n');
605
672
  break;
606
673
  }
607
674
  const event = { type: 'assistant', message: am.message, error: am.error };
@@ -627,6 +694,7 @@ export function spawnClaude(params) {
627
694
  is_error: rm['is_error'],
628
695
  api_error_status: rm['api_error_status'],
629
696
  result: rm['result'],
697
+ errors: rm['errors'], // string[] on SDKResultError — the reason on a non-success result
630
698
  };
631
699
  if (handleEvent(event))
632
700
  onEvent(event);
@@ -670,6 +738,12 @@ export function spawnClaude(params) {
670
738
  if (killedForViolation)
671
739
  return;
672
740
  const message = err?.message ?? String(err);
741
+ // Log the raw thrown error IN FULL before onError sanitises it for chat —
742
+ // describeCliFailure deliberately strips provider detail from the
743
+ // user-facing copy, so the operator log is the only place the full reason
744
+ // (incl. stack) survives. All modes, not just verbose.
745
+ const stack = err?.stack;
746
+ process.stderr.write(paint(SECTION_COLORS.result, `[bridge] query() threw: ${stack || message}`) + '\n');
673
747
  if (/40[13]\b|unauthor|invalid (api key|authentication)|please run \/login/i.test(message)) {
674
748
  sawAuthFailure = true;
675
749
  }
package/dist/config.js CHANGED
@@ -76,15 +76,23 @@ function detectClaudeDefaultModel() {
76
76
  // The fixed menu — keep the option count small so the timeout default is easy
77
77
  // to glance at. Option 1's `model: null` means "let Claude Code pick" (no
78
78
  // `--model` flag passed to the subprocess).
79
+ // NB: no 1M-context (`[1m]`) variant here. The bridge always runs on the user's
80
+ // claude.ai subscription — it strips ANTHROPIC_API_KEY (see claude.ts) so there
81
+ // is no API-key path. The 1M context window is a tier-gated API beta and is NOT
82
+ // available on subscription auth, so selecting it makes EVERY turn fail with an
83
+ // unclassifiable `400 / "unknown"` before the model responds. It used to be the
84
+ // menu default, which broke every default session. Keep this list to models a
85
+ // subscription can actually serve. `num` must stay contiguous 1..N in array
86
+ // order — the jump-key handler maps a typed digit `n` to `idx = n - 1`.
79
87
  const MODEL_OPTIONS = [
80
88
  { num: 1, model: null, label: 'Use Claude Code default' },
81
- { num: 2, model: 'claude-opus-4-8[1m]', label: 'claude-opus-4-8[1m] (1M context)' },
82
- { num: 3, model: 'claude-opus-4-8', label: 'claude-opus-4-8' },
83
- { num: 4, model: 'claude-opus-4-7', label: 'claude-opus-4-7' },
84
- { num: 5, model: 'claude-sonnet-4-6', label: 'claude-sonnet-4-6' },
85
- { num: 6, model: 'claude-haiku-4-5', label: 'claude-haiku-4-5' },
89
+ { num: 2, model: 'claude-opus-4-8', label: 'claude-opus-4-8' },
90
+ { num: 3, model: 'claude-opus-4-7', label: 'claude-opus-4-7' },
91
+ { num: 4, model: 'claude-sonnet-4-6', label: 'claude-sonnet-4-6' },
92
+ { num: 5, model: 'claude-haiku-4-5', label: 'claude-haiku-4-5' },
86
93
  ];
87
94
  const PROMPT_TIMEOUT_MS = 10_000;
95
+ // Default to claude-opus-4-8 (the strongest model a subscription can serve).
88
96
  const DEFAULT_OPTION_NUM = 2;
89
97
  function promptForModel(defaultModel) {
90
98
  return new Promise((resolve) => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@1presence/bridge",
3
- "version": "0.55.0",
3
+ "version": "0.56.0",
4
4
  "description": "Run 1Presence on your Mac and use your Claude.ai Pro subscription from any device",
5
5
  "type": "module",
6
6
  "bin": {