@bridge4dev/runner 0.41.1 → 0.42.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/adapters/claude-usage.d.ts +34 -0
- package/dist/adapters/claude-usage.js +111 -0
- package/dist/adapters/claude.js +48 -0
- package/dist/adapters/codex.js +15 -1
- package/dist/adapters/types.d.ts +5 -0
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +1 -1
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import type { AgentRateLimitWindow } from './types.js';
|
|
2
|
+
export interface UsageRow {
|
|
3
|
+
key: 'five_hour' | 'seven_day';
|
|
4
|
+
/** «Weekly · Fable» for a per-model row; absent for the plain ones. */
|
|
5
|
+
label: string | null;
|
|
6
|
+
percent: number;
|
|
7
|
+
}
|
|
8
|
+
/**
|
|
9
|
+
* Every percentage `/usage` printed, in the order it printed them.
|
|
10
|
+
*
|
|
11
|
+
* The per-model weekly rows («Current week (Fable): 0% used») are kept rather
|
|
12
|
+
* than dropped — owner's call, 2026-08-15: on a plan where one model has its
|
|
13
|
+
* own weekly allowance, «50% of the week» is only half the answer.
|
|
14
|
+
*
|
|
15
|
+
* Deliberately forgiving: a line we cannot read is skipped, never thrown over.
|
|
16
|
+
*/
|
|
17
|
+
export declare function parseUsageText(text: string): UsageRow[];
|
|
18
|
+
/**
|
|
19
|
+
* Merge fresh percentages into the windows we already know about.
|
|
20
|
+
*
|
|
21
|
+
* The RESET TIME is never taken from here — whatever the event said stays. A
|
|
22
|
+
* window the event has not mentioned yet is created so the first probe is not
|
|
23
|
+
* wasted, with no clock on it until the event supplies one.
|
|
24
|
+
*/
|
|
25
|
+
export declare function applyUsagePercentages(windows: AgentRateLimitWindow[], rows: UsageRow[]): AgentRateLimitWindow[];
|
|
26
|
+
/**
|
|
27
|
+
* Run `/usage` in a throwaway process and return what it printed.
|
|
28
|
+
*
|
|
29
|
+
* `--output-format text` rather than the stream: we want the rendering, and the
|
|
30
|
+
* JSON wrapper would only have to be unwrapped again. Never throws — a probe
|
|
31
|
+
* that fails is a percentage we do not show, not a session that breaks.
|
|
32
|
+
*/
|
|
33
|
+
export declare function probeUsageText(binary: string, cwd: string, timeoutMs?: number): Promise<string | null>;
|
|
34
|
+
//# sourceMappingURL=claude-usage.d.ts.map
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
import { execFile } from 'node:child_process';
|
|
2
|
+
import { log } from '../log.js';
|
|
3
|
+
/**
|
|
4
|
+
* How much of the plan is spent — read from the CLI's own `/usage` (#279).
|
|
5
|
+
*
|
|
6
|
+
* The machine-readable event (`rate_limit_event`) is the source of truth for
|
|
7
|
+
* WHEN a window resets, and the auto-pause clock is built on it alone. What it
|
|
8
|
+
* almost never carries is the PERCENTAGE, and that is the number the owner
|
|
9
|
+
* actually asked to see. `/usage` has it.
|
|
10
|
+
*
|
|
11
|
+
* Two things make this worth doing despite being text:
|
|
12
|
+
*
|
|
13
|
+
* - it is FREE. Probed 2026-08-15: `total_cost_usd: 0`, `num_turns: 0`,
|
|
14
|
+
* 0 input and 0 output tokens, ~3.7s. It is a local command, not a request —
|
|
15
|
+
* measuring the limit does not spend it.
|
|
16
|
+
* - it can only ever affect a NUMBER ON A SCREEN. The reset time we schedule a
|
|
17
|
+
* wake-up from never comes from here, so a wording change in a future CLI
|
|
18
|
+
* costs us the percentage and nothing else. That is the exact failure #258
|
|
19
|
+
* refused to accept for the clock, and the exact risk it is fine to take for
|
|
20
|
+
* a label.
|
|
21
|
+
*
|
|
22
|
+
* The text, as of 2026-08-15:
|
|
23
|
+
*
|
|
24
|
+
* Current session: 63% used · resets Aug 15, 10:39pm (Europe/Berlin)
|
|
25
|
+
* Current week (all models): 50% used · resets Aug 19, 3:59pm (Europe/Berlin)
|
|
26
|
+
* Current week (Fable): 0% used
|
|
27
|
+
*/
|
|
28
|
+
/** «Current session» is the five-hour window; every «Current week» is a weekly one. */
|
|
29
|
+
const SESSION_LINE = /current session:\s*(\d+(?:\.\d+)?)%\s*used/i;
|
|
30
|
+
const WEEK_LINE = /current week(?:\s*\(([^)]+)\))?:\s*(\d+(?:\.\d+)?)%\s*used/gi;
|
|
31
|
+
const clamp = (raw) => {
|
|
32
|
+
if (raw === undefined)
|
|
33
|
+
return null;
|
|
34
|
+
const value = Number(raw);
|
|
35
|
+
return Number.isFinite(value) ? Math.min(100, Math.max(0, value)) : null;
|
|
36
|
+
};
|
|
37
|
+
/**
|
|
38
|
+
* Every percentage `/usage` printed, in the order it printed them.
|
|
39
|
+
*
|
|
40
|
+
* The per-model weekly rows («Current week (Fable): 0% used») are kept rather
|
|
41
|
+
* than dropped — owner's call, 2026-08-15: on a plan where one model has its
|
|
42
|
+
* own weekly allowance, «50% of the week» is only half the answer.
|
|
43
|
+
*
|
|
44
|
+
* Deliberately forgiving: a line we cannot read is skipped, never thrown over.
|
|
45
|
+
*/
|
|
46
|
+
export function parseUsageText(text) {
|
|
47
|
+
const rows = [];
|
|
48
|
+
const session = clamp(SESSION_LINE.exec(text)?.[1]);
|
|
49
|
+
if (session !== null)
|
|
50
|
+
rows.push({ key: 'five_hour', label: null, percent: session });
|
|
51
|
+
WEEK_LINE.lastIndex = 0;
|
|
52
|
+
for (let match = WEEK_LINE.exec(text); match !== null; match = WEEK_LINE.exec(text)) {
|
|
53
|
+
const percent = clamp(match[2]);
|
|
54
|
+
if (percent === null)
|
|
55
|
+
continue;
|
|
56
|
+
const scope = (match[1] ?? '').trim();
|
|
57
|
+
// «all models» IS the weekly window; anything else is that model's own.
|
|
58
|
+
const isAll = scope === '' || /^all models$/i.test(scope);
|
|
59
|
+
rows.push({ key: 'seven_day', label: isAll ? null : `Weekly · ${scope}`, percent });
|
|
60
|
+
}
|
|
61
|
+
return rows;
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* Merge fresh percentages into the windows we already know about.
|
|
65
|
+
*
|
|
66
|
+
* The RESET TIME is never taken from here — whatever the event said stays. A
|
|
67
|
+
* window the event has not mentioned yet is created so the first probe is not
|
|
68
|
+
* wasted, with no clock on it until the event supplies one.
|
|
69
|
+
*/
|
|
70
|
+
export function applyUsagePercentages(windows, rows) {
|
|
71
|
+
// Keyed by window AND label: a per-model weekly row is its own line, not an
|
|
72
|
+
// overwrite of the plan-wide one.
|
|
73
|
+
const id = (window) => `${window.key}:${window.label ?? ''}`;
|
|
74
|
+
const merged = new Map(windows.map((window) => [id(window), { ...window }]));
|
|
75
|
+
for (const row of rows) {
|
|
76
|
+
const key = `${row.key}:${row.label ?? ''}`;
|
|
77
|
+
const existing = merged.get(key);
|
|
78
|
+
merged.set(key, {
|
|
79
|
+
key: row.key,
|
|
80
|
+
windowMinutes: existing?.windowMinutes ?? (row.key === 'five_hour' ? 300 : 10_080),
|
|
81
|
+
usedPercent: row.percent,
|
|
82
|
+
// The clock stays whatever the machine-readable event said — this file
|
|
83
|
+
// never sets it, so a wording change here can never move a wake-up.
|
|
84
|
+
resetsAt: existing?.resetsAt ?? null,
|
|
85
|
+
...(existing?.status ? { status: existing.status } : {}),
|
|
86
|
+
...(row.label ? { label: row.label } : {}),
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
return [...merged.values()].sort((a, b) => (a.windowMinutes ?? Number.MAX_SAFE_INTEGER) - (b.windowMinutes ?? Number.MAX_SAFE_INTEGER) ||
|
|
90
|
+
(a.label ?? '').localeCompare(b.label ?? ''));
|
|
91
|
+
}
|
|
92
|
+
/**
|
|
93
|
+
* Run `/usage` in a throwaway process and return what it printed.
|
|
94
|
+
*
|
|
95
|
+
* `--output-format text` rather than the stream: we want the rendering, and the
|
|
96
|
+
* JSON wrapper would only have to be unwrapped again. Never throws — a probe
|
|
97
|
+
* that fails is a percentage we do not show, not a session that breaks.
|
|
98
|
+
*/
|
|
99
|
+
export function probeUsageText(binary, cwd, timeoutMs = 20_000) {
|
|
100
|
+
return new Promise((resolve) => {
|
|
101
|
+
execFile(binary, ['-p', '/usage', '--output-format', 'text'], { cwd, timeout: timeoutMs, maxBuffer: 1024 * 1024 }, (error, stdout) => {
|
|
102
|
+
if (error) {
|
|
103
|
+
log.debug('claude: /usage probe failed', { error: String(error) });
|
|
104
|
+
resolve(null);
|
|
105
|
+
return;
|
|
106
|
+
}
|
|
107
|
+
resolve(stdout);
|
|
108
|
+
});
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
//# sourceMappingURL=claude-usage.js.map
|
package/dist/adapters/claude.js
CHANGED
|
@@ -7,6 +7,10 @@ import { mcpConfigPath } from '../paths.js';
|
|
|
7
7
|
import { evaluateToolUse, maskSecrets, maskString, } from '../policy.js';
|
|
8
8
|
import { availableModes, MODE_REFUSED_TEXT, MODE_WITHDRAWN_TEXT, } from './types.js';
|
|
9
9
|
import { clampPercent, RATE_WINDOW_MINUTES, rateWindowKey } from './rate-limits.js';
|
|
10
|
+
import { applyUsagePercentages, parseUsageText, probeUsageText } from './claude-usage.js';
|
|
11
|
+
import { claudeCliPath } from '../agent-binary.js';
|
|
12
|
+
/** How often `/usage` may be read. Free, but still a process. */
|
|
13
|
+
const USAGE_PROBE_INTERVAL_MS = 3 * 60 * 1000;
|
|
10
14
|
import { answerSummary, answerValue, discussMessage, invalidationMessage, mirrorOptions, newAskId, MAX_OPTIONS, MAX_QUESTIONS, OPTION_TEXT_LIMIT, QUESTION_TEXT_LIMIT, } from './questions.js';
|
|
11
15
|
// Claude adapter over the Agent SDK. Three live-verified gotchas (plan §2):
|
|
12
16
|
// 1. Bare tool names in `allowedTools` auto-approve BEFORE canUseTool — we
|
|
@@ -695,6 +699,44 @@ class ClaudeSession {
|
|
|
695
699
|
rateLimitsAvailable = false;
|
|
696
700
|
/** A refusal seen since the last turn ended, waiting to be reported with it. */
|
|
697
701
|
limitBlockPending = false;
|
|
702
|
+
/** When `/usage` was last read, so a busy session does not spawn a process a second. */
|
|
703
|
+
usageProbedAt = 0;
|
|
704
|
+
/**
|
|
705
|
+
* Read the percentages out of the CLI's own `/usage` (#279).
|
|
706
|
+
*
|
|
707
|
+
* Fire-and-forget, throttled, and free — a local command, not a request
|
|
708
|
+
* (probe 2026-08-15: zero tokens, zero turns, zero dollars). The live event
|
|
709
|
+
* gives us the clock and almost never the number; this gives the number and
|
|
710
|
+
* never touches the clock.
|
|
711
|
+
*/
|
|
712
|
+
refreshUsage() {
|
|
713
|
+
if (this.stopped)
|
|
714
|
+
return;
|
|
715
|
+
if (this.usageProbedAt !== 0 && Date.now() - this.usageProbedAt < USAGE_PROBE_INTERVAL_MS) {
|
|
716
|
+
return;
|
|
717
|
+
}
|
|
718
|
+
const binary = claudeCliPath();
|
|
719
|
+
if (!binary)
|
|
720
|
+
return;
|
|
721
|
+
this.usageProbedAt = Date.now();
|
|
722
|
+
void probeUsageText(binary, this.spec.cwd)
|
|
723
|
+
.then((text) => {
|
|
724
|
+
if (!text || this.stopped)
|
|
725
|
+
return;
|
|
726
|
+
const rows = parseUsageText(text);
|
|
727
|
+
if (rows.length === 0)
|
|
728
|
+
return;
|
|
729
|
+
// A percentage proves a plan as surely as the event does.
|
|
730
|
+
this.rateLimitsAvailable = true;
|
|
731
|
+
const merged = applyUsagePercentages([...this.rateLimitWindows.values()], rows);
|
|
732
|
+
this.rateLimitWindows.clear();
|
|
733
|
+
for (const window of merged) {
|
|
734
|
+
this.rateLimitWindows.set(`${window.key}:${window.label ?? ''}`, window);
|
|
735
|
+
}
|
|
736
|
+
this.emitRateLimits();
|
|
737
|
+
})
|
|
738
|
+
.catch((error) => log.debug('claude: usage probe failed', { error: String(error) }));
|
|
739
|
+
}
|
|
698
740
|
/** Read the flag and clear it: one refusal marks exactly one turn end. */
|
|
699
741
|
consumeLimitBlock() {
|
|
700
742
|
const blocked = this.limitBlockPending;
|
|
@@ -1902,6 +1944,8 @@ class ClaudeSession {
|
|
|
1902
1944
|
// account has none — «this organization runs on its own key» is an
|
|
1903
1945
|
// answer the popup has to be able to give.
|
|
1904
1946
|
this.emitRateLimitsFromInit(msg);
|
|
1947
|
+
// #279: the percentages the event does not carry.
|
|
1948
|
+
this.refreshUsage();
|
|
1905
1949
|
}
|
|
1906
1950
|
else if (msg.subtype === 'status') {
|
|
1907
1951
|
const status = msg.status;
|
|
@@ -2057,6 +2101,9 @@ class ClaudeSession {
|
|
|
2057
2101
|
// refused is not a turn that ran.
|
|
2058
2102
|
...(this.consumeLimitBlock() ? { limitBlocked: true } : {}),
|
|
2059
2103
|
});
|
|
2104
|
+
// #279: a turn just moved the plan, so the percentages are worth
|
|
2105
|
+
// re-reading — throttled inside, and free either way.
|
|
2106
|
+
this.refreshUsage();
|
|
2060
2107
|
}
|
|
2061
2108
|
else {
|
|
2062
2109
|
this.emit({
|
|
@@ -2065,6 +2112,7 @@ class ClaudeSession {
|
|
|
2065
2112
|
errorMessage: failure,
|
|
2066
2113
|
...(this.consumeLimitBlock() ? { limitBlocked: true } : {}),
|
|
2067
2114
|
});
|
|
2115
|
+
this.refreshUsage();
|
|
2068
2116
|
}
|
|
2069
2117
|
break;
|
|
2070
2118
|
}
|
package/dist/adapters/codex.js
CHANGED
|
@@ -1385,8 +1385,22 @@ class CodexSession {
|
|
|
1385
1385
|
if (usedPercent === undefined)
|
|
1386
1386
|
continue;
|
|
1387
1387
|
const minutes = num(window['windowMinutes'] ?? window['window_minutes']) ?? null;
|
|
1388
|
+
// Live 2026-08-15: Codex sent a percentage with NO length, so the label
|
|
1389
|
+
// fell back to «Limit window» — true but useless. The slot itself carries
|
|
1390
|
+
// the answer on every ChatGPT plan: `primary` is the short window,
|
|
1391
|
+
// `secondary` the weekly one. Used only when the minutes are missing, so
|
|
1392
|
+
// a provider that does state them still wins.
|
|
1393
|
+
// Only when the length is ABSENT. A stated length we do not recognise is
|
|
1394
|
+
// information — 43 200 minutes is a monthly window, and calling it «5
|
|
1395
|
+
// hours» because it arrived first would be worse than admitting we have
|
|
1396
|
+
// no name for it.
|
|
1397
|
+
const key = minutes === null
|
|
1398
|
+
? slot === 'primary'
|
|
1399
|
+
? 'five_hour'
|
|
1400
|
+
: 'seven_day'
|
|
1401
|
+
: rateWindowKeyFromMinutes(minutes);
|
|
1388
1402
|
windows.push({
|
|
1389
|
-
key
|
|
1403
|
+
key,
|
|
1390
1404
|
windowMinutes: minutes,
|
|
1391
1405
|
usedPercent: clampPercent(usedPercent),
|
|
1392
1406
|
resetsAt: str(window['resetsAt'] ?? window['resets_at']) ?? null,
|
package/dist/adapters/types.d.ts
CHANGED
|
@@ -162,6 +162,11 @@ export interface AgentRateLimitWindow {
|
|
|
162
162
|
resetsAt: string | null;
|
|
163
163
|
/** `allowed` · `allowed_warning` · `rejected`, when the provider says. */
|
|
164
164
|
status?: string | null;
|
|
165
|
+
/**
|
|
166
|
+
* What to call this row when our own vocabulary is not enough — «Weekly ·
|
|
167
|
+
* Fable» for a model with its own weekly allowance (owner's call, #279).
|
|
168
|
+
*/
|
|
169
|
+
label?: string | null;
|
|
165
170
|
}
|
|
166
171
|
/**
|
|
167
172
|
* The account's plan usage, as one snapshot (#279, #258).
|
package/dist/version.d.ts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
export declare const RUNNER_VERSION = "0.
|
|
1
|
+
export declare const RUNNER_VERSION = "0.42.0";
|
|
2
2
|
//# sourceMappingURL=version.d.ts.map
|
package/dist/version.js
CHANGED
package/package.json
CHANGED