@1presence/bridge 0.59.0 → 0.60.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
@@ -41,6 +41,16 @@ import { getBridgeModel } from './config.js';
41
41
  // Track whether we've already announced the model this process — printing it
42
42
  // per-spawn is noisy; once on startup is what the user actually wants to see.
43
43
  let modelAnnounced = false;
44
+ // Claude Code engine version driving the turns, captured from the SDK init
45
+ // event (`claude_code_version`). Constant per process, so cached module-level
46
+ // and read by the after-turn status line — mirrors the `v2.x.x` segment of the
47
+ // local statusline. Null until the first turn's init event arrives.
48
+ let cliVersion = null;
49
+ export function getCliVersion() { return cliVersion; }
50
+ const rateLimitWindows = new Map();
51
+ export function getRateLimitWindow(type) {
52
+ return rateLimitWindows.get(type);
53
+ }
44
54
  // Verbose flag — when set via --verbose, log full tool inputs and outputs
45
55
  // PLUS the entire system prompt. Great for prompt debugging, noisy for
46
56
  // message debugging (the prompt dump buries the conversation).
@@ -384,10 +394,13 @@ export function spawnClaude(params) {
384
394
  const keySource = event['apiKeySource'];
385
395
  const model = event['model'];
386
396
  const sid = event['session_id'];
397
+ const ccv = event['claude_code_version'];
387
398
  if (sid)
388
399
  currentSessionId = sid;
389
400
  if (model)
390
401
  extractedModel = model;
402
+ if (ccv)
403
+ cliVersion = ccv;
391
404
  if (!modelAnnounced) {
392
405
  const source = keySource === 'none' || !keySource ? 'claude.ai subscription' : keySource;
393
406
  const pin = getBridgeModel() ? ' (selected at startup)' : '';
@@ -647,7 +660,7 @@ export function spawnClaude(params) {
647
660
  const subtype = m.subtype;
648
661
  if (subtype === 'init') {
649
662
  const init = m;
650
- const event = { type: 'system', subtype: 'init', model: init.model, apiKeySource: init.apiKeySource };
663
+ const event = { type: 'system', subtype: 'init', model: init.model, apiKeySource: init.apiKeySource, claude_code_version: init.claude_code_version };
651
664
  if (handleEvent(event))
652
665
  onEvent(event);
653
666
  }
@@ -739,6 +752,16 @@ export function spawnClaude(params) {
739
752
  // a notice when the user is actually warned or throttled.
740
753
  const info = m.rate_limit_info;
741
754
  const status = info?.status;
755
+ // Retain the latest reading for this window so the after-turn status
756
+ // line can render the full 5h/7d picture even though each event only
757
+ // carries one window. utilization is a 0–100 used-percentage.
758
+ if (info?.rateLimitType && typeof info.utilization === 'number') {
759
+ rateLimitWindows.set(info.rateLimitType, {
760
+ utilization: info.utilization,
761
+ resetsAt: info.resetsAt,
762
+ status: status ?? 'allowed',
763
+ });
764
+ }
742
765
  if (status === 'allowed_warning' || status === 'rejected') {
743
766
  // Admin-only ephemeral notice — jargon is fine in Local Mode.
744
767
  onNotice?.(formatRateLimitNotice(status, info?.resetsAt));
package/dist/index.js CHANGED
@@ -7,7 +7,7 @@ import { fileURLToPath } from 'url';
7
7
  import { createRequire } from 'module';
8
8
  import { query } from '@anthropic-ai/claude-agent-sdk';
9
9
  import { getValidAuth, ensureFreshToken, forceRefreshToken, isTokenValid, AuthCancelledError } from './auth.js';
10
- import { spawnClaude, killAll, cancelConversation, setVerbose, setDebug, paint, SECTION_COLORS } from './claude.js';
10
+ import { spawnClaude, killAll, cancelConversation, setVerbose, setDebug, paint, SECTION_COLORS, getCliVersion, getRateLimitWindow } from './claude.js';
11
11
  import { ensureModelChoice } from './config.js';
12
12
  import { checkAndUpdate } from './update.js';
13
13
  import { makeBridgeAccumulator, postSaveTurn } from './accumulator.js';
@@ -101,6 +101,51 @@ function contextWindowFor(model) {
101
101
  }
102
102
  return DEFAULT_CONTEXT_WINDOW;
103
103
  }
104
+ // ─── Rate-limit window rendering ──────────────────────────────────────────────
105
+ // Mirrors the local Claude Code statusline's ⏱️ line: a 10-cell bar + used % +
106
+ // reset time per subscription window. Data comes from the SDK's rate_limit_event
107
+ // (captured across turns in claude.ts), so no auth/header probe is needed.
108
+ // 7-day usage may arrive as the generic `seven_day` window or a model-specific
109
+ // variant; surface whichever the SDK has reported, preferring the generic one.
110
+ function sevenDayWindow() {
111
+ return getRateLimitWindow('seven_day')
112
+ ?? getRateLimitWindow('seven_day_opus')
113
+ ?? getRateLimitWindow('seven_day_sonnet');
114
+ }
115
+ function makeBar(pct) {
116
+ const width = 10;
117
+ const filled = Math.max(0, Math.min(width, Math.floor((pct * width) / 100)));
118
+ return '█'.repeat(filled) + '░'.repeat(width - filled);
119
+ }
120
+ // Green < 70%, yellow 70–89%, red ≥ 90% — same thresholds as the statusline.
121
+ function rlColor(pct) {
122
+ return pct >= 90 ? '31' : pct >= 70 ? '33' : '32';
123
+ }
124
+ // "12:20a.m." within a day, "Mon 11:00p.m." when the reset is > 24h out.
125
+ function formatReset(resetsAt) {
126
+ if (!resetsAt)
127
+ return '';
128
+ const ms = resetsAt < 1e12 ? resetsAt * 1000 : resetsAt;
129
+ const d = new Date(ms);
130
+ const h24 = d.getHours();
131
+ const h12 = h24 % 12 === 0 ? 12 : h24 % 12;
132
+ const mm = String(d.getMinutes()).padStart(2, '0');
133
+ const time = `${h12}:${mm}${h24 < 12 ? 'a.m.' : 'p.m.'}`;
134
+ if (ms - Date.now() > 86_400_000) {
135
+ const wd = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'][d.getDay()];
136
+ return `${wd} ${time}`;
137
+ }
138
+ return time;
139
+ }
140
+ // One coloured "5h ██░░░░░░░░ 20% resets 12:20a.m." segment; null if unknown.
141
+ function formatWindow(label, w) {
142
+ if (!w)
143
+ return null;
144
+ const pct = Math.round(w.utilization);
145
+ const reset = formatReset(w.resetsAt);
146
+ const seg = `${label} ${makeBar(pct)} ${pct}%${reset ? ` resets ${reset}` : ''}`;
147
+ return paint(rlColor(pct), seg);
148
+ }
104
149
  // ─── System prompt fetch ──────────────────────────────────────────────────────
105
150
  // Pulls the fully-built system prompt from agent-api (via gateway proxy).
106
151
  // This MUST match the hosted runtime exactly — STATIC_SYSTEM_PROMPT + dynamic
@@ -436,14 +481,26 @@ async function handleMessage(conversationId, text, sessionId, history, auth, vau
436
481
  parts.push(costStr);
437
482
  const suffix = ` ${parts.join(' ')}`;
438
483
  console.log(`[${new Date().toLocaleTimeString()}] ✓ done${suffix}`);
439
- // Status-bar line, mirroring local Claude Code: model · context fill ·
440
- // session cost. Dimmed and indented so it groups under the done line
441
- // without competing with it. The cost segment falls back to "plan usage"
442
- // whenever the running total is 0 (the subscription case).
484
+ // Status-bar line, mirroring the local Claude Code statusline:
485
+ // 🤖 model v<version> | 🧠 context% | 💰 cost
486
+ // ⏱️ 5h <bar> n% resets | 7d <bar> n% resets
487
+ // Dimmed and indented so it groups under the done line without competing
488
+ // with it. The cost segment falls back to "plan usage" whenever the running
489
+ // total is 0 (the subscription case). The ⏱️ window line is only printed
490
+ // once the SDK has reported at least one window this process.
443
491
  sessionCostUsd += costUsd;
444
492
  const ctxPct = Math.max(0, Math.min(100, Math.round((contextTokens / contextWindowFor(model)) * 100)));
445
493
  const costSeg = sessionCostUsd > 0 ? `$${sessionCostUsd.toFixed(2)} session` : 'plan usage';
446
- console.log(paint('90', ` 🤖 ${friendlyModelName(model)} · 🧠 ${ctxPct}% · 💰 ${costSeg}`));
494
+ const ver = getCliVersion();
495
+ const verSeg = ver ? ` v${ver}` : '';
496
+ console.log(paint('90', ` 🤖 ${friendlyModelName(model)}${verSeg} | 🧠 ${ctxPct}% | 💰 ${costSeg}`));
497
+ const windows = [
498
+ formatWindow('5h', getRateLimitWindow('five_hour')),
499
+ formatWindow('7d', sevenDayWindow()),
500
+ ].filter((s) => s !== null);
501
+ if (windows.length > 0) {
502
+ console.log(` ${paint('90', '⏱️')} ${windows.join(paint('90', ' | '))}`);
503
+ }
447
504
  const mapped = toBridgeUsage(usage);
448
505
  if (currentWs?.readyState === WebSocket.OPEN) {
449
506
  currentWs.send(JSON.stringify({
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@1presence/bridge",
3
- "version": "0.59.0",
3
+ "version": "0.60.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": {