@adecore/agents 0.38.0 → 0.40.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/async.d.ts +5 -0
- package/dist/async.js +13 -5
- package/dist/chat/acp-protocol.js +3 -5
- package/dist/chat/chat-core.d.ts +1 -0
- package/dist/chat/chat-core.js +9 -2
- package/dist/chat/chat-process.d.ts +1 -0
- package/dist/chat/chat-process.js +1 -1
- package/dist/chat/chat-session.d.ts +2 -0
- package/dist/chat/chat-session.js +9 -10
- package/dist/chat/claude-protocol.js +1 -6
- package/dist/chat/claude-transcript.js +1 -3
- package/dist/chat/codex-protocol.js +1 -6
- package/dist/chat/projector.js +1 -3
- package/dist/chat/request-summary.js +1 -3
- package/dist/coded-error.js +1 -5
- package/dist/context/argv.js +1 -2
- package/dist/context/verb.js +1 -3
- package/dist/error-text.d.ts +1 -0
- package/dist/error-text.js +4 -0
- package/dist/fields.d.ts +3 -0
- package/dist/fields.js +12 -0
- package/dist/fs.d.ts +1 -0
- package/dist/fs.js +8 -4
- package/dist/lineage.js +11 -5
- package/dist/messages/notice-store.js +3 -4
- package/dist/providers/accounts/acp-account.js +3 -20
- package/dist/providers/accounts/service.d.ts +1 -2
- package/dist/providers/accounts/service.js +5 -9
- package/dist/providers/accounts/status.js +1 -3
- package/dist/providers/acp-agent-state.js +1 -3
- package/dist/providers/claude.js +0 -1
- package/dist/providers/codex.js +0 -1
- package/dist/providers/detect.js +1 -1
- package/dist/providers/shell-commands.js +1 -2
- package/dist/run-process.js +1 -3
- package/dist/skills.d.ts +1 -1
- package/dist/skills.js +5 -8
- package/dist/tasks/background-limit.js +3 -2
- package/dist/tasks/task-coordinator.js +11 -17
- package/dist/testing/watch.js +0 -1
- package/dist/usage/limits/monitor.js +1 -2
- package/dist/usage/readers/claude.js +3 -10
- package/dist/usage/readers/codex.js +2 -9
- package/dist/usage/readers/grok.js +3 -10
- package/dist/usage/readers/host-log.js +3 -10
- package/dist/usage/record.d.ts +2 -0
- package/dist/usage/record.js +13 -0
- package/package.json +6 -6
- package/src/async.ts +23 -5
- package/src/chat/acp-backend.ts +121 -0
- package/src/chat/acp-protocol.ts +172 -6
- package/src/chat/attachment-store.ts +21 -0
- package/src/chat/backend.ts +0 -2
- package/src/chat/bookmark-store.ts +23 -0
- package/src/chat/chat-core.ts +218 -7
- package/src/chat/chat-log.ts +19 -0
- package/src/chat/chat-process.ts +52 -1
- package/src/chat/chat-session.ts +414 -13
- package/src/chat/chat-store.ts +50 -0
- package/src/chat/chat-title.ts +9 -0
- package/src/chat/claude-backend.ts +50 -0
- package/src/chat/claude-protocol.ts +155 -8
- package/src/chat/claude-title.ts +10 -0
- package/src/chat/claude-transcript.ts +47 -4
- package/src/chat/codex-backend.ts +131 -0
- package/src/chat/codex-protocol.ts +180 -8
- package/src/chat/codex-thread.ts +23 -0
- package/src/chat/composer-preferences.ts +6 -0
- package/src/chat/context-breakdown.ts +11 -0
- package/src/chat/delta-coalescer.ts +8 -0
- package/src/chat/external-sessions.ts +28 -0
- package/src/chat/generated-image.ts +25 -0
- package/src/chat/index.ts +5 -0
- package/src/chat/input.ts +13 -0
- package/src/chat/json-rpc-transport.ts +28 -0
- package/src/chat/limit-resume.ts +13 -0
- package/src/chat/projector.ts +160 -4
- package/src/chat/readable-text.ts +3 -0
- package/src/chat/request-summary.ts +18 -4
- package/src/chat/subagent-projection.ts +3 -0
- package/src/chat/subagent-reader.ts +76 -0
- package/src/chat/subagent-settlement.ts +24 -0
- package/src/chat/thread-search.ts +13 -0
- package/src/chat/thread.ts +31 -0
- package/src/chat/ui-queries.ts +122 -0
- package/src/chat/ui-replies.ts +34 -0
- package/src/chat/visual-store.ts +49 -0
- package/src/chat/wake-chat.ts +3 -0
- package/src/client-sinks.ts +2 -0
- package/src/coded-error.ts +1 -5
- package/src/context/argv.ts +18 -2
- package/src/context/index.ts +1 -0
- package/src/context/refusal.ts +3 -0
- package/src/context/verb.ts +32 -4
- package/src/coordination/index.ts +10 -0
- package/src/error-text.ts +7 -0
- package/src/fields.ts +14 -0
- package/src/fs.ts +20 -4
- package/src/host/agent-host.ts +20 -0
- package/src/host/environment.ts +6 -0
- package/src/host/handlers.ts +18 -0
- package/src/host/memory-port.ts +4 -0
- package/src/host/wiring.ts +10 -0
- package/src/index.ts +1 -0
- package/src/lineage.ts +24 -5
- package/src/messages/deliver-message.ts +6 -0
- package/src/messages/deliver-notice.ts +13 -0
- package/src/messages/notice-store.ts +16 -4
- package/src/modes.ts +1 -0
- package/src/outbox/outbox-worker.ts +31 -0
- package/src/outbox/outbox.ts +5 -0
- package/src/providers/accounts/accounts.ts +36 -0
- package/src/providers/accounts/acp-account.ts +22 -27
- package/src/providers/accounts/launch.ts +4 -0
- package/src/providers/accounts/service.ts +110 -12
- package/src/providers/accounts/shadow-home.ts +10 -0
- package/src/providers/accounts/status.ts +13 -4
- package/src/providers/accounts/store.ts +25 -0
- package/src/providers/accounts/variables.ts +11 -1
- package/src/providers/acp-agent-state.ts +24 -3
- package/src/providers/acp-provider.ts +5 -0
- package/src/providers/acp-usage-log.ts +2 -0
- package/src/providers/catalog.ts +17 -0
- package/src/providers/claude.ts +10 -1
- package/src/providers/codex-rules.ts +3 -0
- package/src/providers/codex.ts +2 -1
- package/src/providers/detect.ts +6 -1
- package/src/providers/provider.ts +1 -1
- package/src/providers/registry.ts +18 -2
- package/src/providers/shell-commands.ts +37 -2
- package/src/record-directory.ts +19 -0
- package/src/run-process.ts +24 -3
- package/src/serializer.ts +3 -0
- package/src/skills.ts +73 -10
- package/src/tasks/background-limit.ts +9 -2
- package/src/tasks/end-children.ts +9 -0
- package/src/tasks/give-task.ts +8 -0
- package/src/tasks/task-coordinator.ts +81 -17
- package/src/tasks/task-store.ts +28 -0
- package/src/tasks/waiting-child.ts +21 -0
- package/src/tasks/wake-parent.ts +26 -0
- package/src/tasks/wiring.ts +22 -0
- package/src/testing/accounts.ts +2 -0
- package/src/testing/fake-acp-capture.ts +16 -0
- package/src/testing/fake-acp.ts +66 -0
- package/src/testing/fake-claude.ts +177 -0
- package/src/testing/fake-cli.ts +29 -0
- package/src/testing/fake-codex.ts +122 -0
- package/src/testing/manual-clock.ts +2 -0
- package/src/testing/watch.ts +7 -1
- package/src/title-file.ts +12 -0
- package/src/usage/accounts.ts +6 -0
- package/src/usage/aggregate.ts +19 -0
- package/src/usage/exchange.ts +12 -0
- package/src/usage/index-file.ts +27 -0
- package/src/usage/limits/monitor.ts +31 -3
- package/src/usage/limits/normalize.ts +30 -0
- package/src/usage/limits/probe.ts +17 -0
- package/src/usage/pricing.ts +35 -0
- package/src/usage/projects.ts +8 -0
- package/src/usage/readers/claude.ts +12 -9
- package/src/usage/readers/codex.ts +40 -8
- package/src/usage/readers/grok.ts +17 -9
- package/src/usage/readers/host-log.ts +9 -9
- package/src/usage/record.ts +22 -0
- package/src/usage/roots.ts +6 -0
- package/src/usage/scanner.ts +61 -0
- package/src/usage/usage-service.ts +14 -0
- package/src/watch-seam.ts +8 -0
- package/src/windows-command.ts +12 -0
|
@@ -26,6 +26,7 @@ function timeoutOf<T>(work: Promise<T>, ms: number, what: string): Promise<T> {
|
|
|
26
26
|
*/
|
|
27
27
|
export async function probeClaude(command: readonly string[], env: Record<string, string>, spawn: SpawnChatProcess = spawnChatProcess): Promise<ProbeResult> {
|
|
28
28
|
let child: ChatProcess;
|
|
29
|
+
|
|
29
30
|
try {
|
|
30
31
|
child = spawn({
|
|
31
32
|
command: [...command, '-p', '--input-format', 'stream-json', '--output-format', 'stream-json', '--verbose'],
|
|
@@ -36,45 +37,58 @@ export async function probeClaude(command: readonly string[], env: Record<string
|
|
|
36
37
|
} catch (error) {
|
|
37
38
|
return failure(errorText(error));
|
|
38
39
|
}
|
|
40
|
+
|
|
39
41
|
const write = (frame: unknown): void => {
|
|
40
42
|
child.stdin.write(`${JSON.stringify(frame)}\n`);
|
|
41
43
|
child.stdin.flush();
|
|
42
44
|
};
|
|
45
|
+
|
|
43
46
|
const read = async (): Promise<ProbeResult> => {
|
|
44
47
|
write({ type: 'control_request', request_id: 'probe-init', request: { subtype: 'initialize', hooks: {} } });
|
|
45
48
|
let asked = false;
|
|
46
49
|
let buffer = '';
|
|
47
50
|
const decoder = new TextDecoder();
|
|
51
|
+
|
|
48
52
|
for await (const chunk of child.stdout as unknown as AsyncIterable<Uint8Array>) {
|
|
49
53
|
buffer += decoder.decode(chunk, { stream: true });
|
|
50
54
|
let at = buffer.indexOf('\n');
|
|
55
|
+
|
|
51
56
|
for (; at !== -1; at = buffer.indexOf('\n')) {
|
|
52
57
|
const line = buffer.slice(0, at);
|
|
53
58
|
buffer = buffer.slice(at + 1);
|
|
54
59
|
let frame: unknown;
|
|
60
|
+
|
|
55
61
|
try {
|
|
56
62
|
frame = JSON.parse(line);
|
|
57
63
|
} catch {
|
|
58
64
|
continue;
|
|
59
65
|
}
|
|
66
|
+
|
|
60
67
|
if (typeof frame !== 'object' || frame === null) {
|
|
61
68
|
continue;
|
|
62
69
|
}
|
|
70
|
+
|
|
63
71
|
const envelope = frame as { type?: unknown; request_id?: unknown; response?: { subtype?: unknown; response?: unknown } };
|
|
72
|
+
|
|
64
73
|
if (envelope.type !== 'control_response') {
|
|
65
74
|
continue;
|
|
66
75
|
}
|
|
76
|
+
|
|
67
77
|
if (!asked) {
|
|
68
78
|
asked = true;
|
|
69
79
|
write({ type: 'control_request', request_id: 'probe-usage', request: { subtype: 'get_usage' } });
|
|
80
|
+
|
|
70
81
|
continue;
|
|
71
82
|
}
|
|
83
|
+
|
|
72
84
|
if (envelope.response?.subtype === 'error') {
|
|
73
85
|
return failure('Claude Code refused to report its usage');
|
|
74
86
|
}
|
|
87
|
+
|
|
75
88
|
return readClaudeUsage(envelope.response?.response ?? envelope.response);
|
|
76
89
|
}
|
|
77
90
|
}
|
|
91
|
+
|
|
78
92
|
return failure('Claude Code closed without reporting its usage');
|
|
79
93
|
};
|
|
80
94
|
|
|
@@ -95,6 +109,7 @@ export async function probeCodex(
|
|
|
95
109
|
client: CodexClientInfo = DEFAULT_CODEX_CLIENT
|
|
96
110
|
): Promise<ProbeResult> {
|
|
97
111
|
let transport: CodexTransport | null = null;
|
|
112
|
+
|
|
98
113
|
try {
|
|
99
114
|
transport = new CodexTransport({
|
|
100
115
|
command: [...command],
|
|
@@ -103,12 +118,14 @@ export async function probeCodex(
|
|
|
103
118
|
onFrame: () => undefined,
|
|
104
119
|
onExit: () => undefined
|
|
105
120
|
});
|
|
121
|
+
|
|
106
122
|
const open = transport;
|
|
107
123
|
await timeoutOf(open.request('initialize', { clientInfo: client, capabilities: { experimentalApi: true } }), PROBE_TIMEOUT_MS, 'Codex');
|
|
108
124
|
open.notify('initialized', {});
|
|
109
125
|
const answer = await timeoutOf(open.request('account/rateLimits/read', undefined), CODEX_READ_TIMEOUT_MS, 'Codex');
|
|
110
126
|
const snapshot = typeof answer === 'object' && answer !== null ? (answer as { rateLimits?: unknown }).rateLimits : null;
|
|
111
127
|
const reading = readCodexLimits(snapshot);
|
|
128
|
+
|
|
112
129
|
// An account on an API key has no plan windows; Codex answers, with nothing in it.
|
|
113
130
|
return reading ?? { unavailable: { reason: 'no-subscription', message: null } };
|
|
114
131
|
} catch (error) {
|
package/src/usage/pricing.ts
CHANGED
|
@@ -61,24 +61,31 @@ function rate(value: unknown): number | null {
|
|
|
61
61
|
*/
|
|
62
62
|
export function parsePriceTable(document: unknown): PriceTable {
|
|
63
63
|
const table: PriceTable = new Map();
|
|
64
|
+
|
|
64
65
|
if (typeof document !== 'object' || document === null) {
|
|
65
66
|
return table;
|
|
66
67
|
}
|
|
68
|
+
|
|
67
69
|
for (const [name, value] of Object.entries(document as Record<string, LiteLlmEntry>)) {
|
|
68
70
|
if (typeof value !== 'object' || value === null) {
|
|
69
71
|
continue;
|
|
70
72
|
}
|
|
73
|
+
|
|
71
74
|
if (typeof value.litellm_provider !== 'string' || !PROVIDERS.has(value.litellm_provider)) {
|
|
72
75
|
continue;
|
|
73
76
|
}
|
|
77
|
+
|
|
74
78
|
if (typeof value.mode !== 'string' || !MODES.has(value.mode)) {
|
|
75
79
|
continue;
|
|
76
80
|
}
|
|
81
|
+
|
|
77
82
|
const input = rate(value.input_cost_per_token);
|
|
78
83
|
const output = rate(value.output_cost_per_token);
|
|
84
|
+
|
|
79
85
|
if (input === null || output === null) {
|
|
80
86
|
continue;
|
|
81
87
|
}
|
|
88
|
+
|
|
82
89
|
const price: ModelPrice = {
|
|
83
90
|
input,
|
|
84
91
|
output,
|
|
@@ -86,7 +93,9 @@ export function parsePriceTable(document: unknown): PriceTable {
|
|
|
86
93
|
cacheWrite: rate(value.cache_creation_input_token_cost) ?? input * CACHE_WRITE_SHARE,
|
|
87
94
|
cacheWrite1h: input * CACHE_WRITE_1H_SHARE
|
|
88
95
|
};
|
|
96
|
+
|
|
89
97
|
const tiers: Partial<Record<string, ModelPrice>> = {};
|
|
98
|
+
|
|
90
99
|
for (const [tier, multiplier] of [
|
|
91
100
|
['priority', 2],
|
|
92
101
|
['flex', 0.5],
|
|
@@ -100,22 +109,27 @@ export function parsePriceTable(document: unknown): PriceTable {
|
|
|
100
109
|
cacheWrite1h: price.cacheWrite1h * multiplier
|
|
101
110
|
};
|
|
102
111
|
}
|
|
112
|
+
|
|
103
113
|
if (value.litellm_provider === 'openai') {
|
|
104
114
|
price.tiers = tiers;
|
|
105
115
|
}
|
|
116
|
+
|
|
106
117
|
const key = name.trim().toLowerCase();
|
|
107
118
|
table.set(key, price);
|
|
108
119
|
const bare = key.slice(key.lastIndexOf('/') + 1);
|
|
120
|
+
|
|
109
121
|
if (bare !== key && !table.has(bare)) {
|
|
110
122
|
table.set(bare, price);
|
|
111
123
|
}
|
|
112
124
|
}
|
|
125
|
+
|
|
113
126
|
return table;
|
|
114
127
|
}
|
|
115
128
|
|
|
116
129
|
/* `claude-opus-4-5-20251101` is the same model as `claude-opus-4-5`, dated. */
|
|
117
130
|
function withoutDate(key: string): string | null {
|
|
118
131
|
const match = /^(.*)-\d{6,8}$/.exec(key);
|
|
132
|
+
|
|
119
133
|
return match === null ? null : match[1]!;
|
|
120
134
|
}
|
|
121
135
|
|
|
@@ -134,37 +148,48 @@ const REVIEW_MODELS = [
|
|
|
134
148
|
export function lookupPrice(table: PriceTable, model: string, serviceTier?: string, timestampMs?: number): PriceLookup {
|
|
135
149
|
const found = lookupBasePrice(table, model, timestampMs);
|
|
136
150
|
const tier = serviceTier === 'fast' ? 'priority' : serviceTier;
|
|
151
|
+
|
|
137
152
|
return { ...found, price: (tier === undefined ? undefined : found.price?.tiers?.[tier]) ?? found.price };
|
|
138
153
|
}
|
|
139
154
|
|
|
140
155
|
function lookupBasePrice(table: PriceTable, model: string, timestampMs?: number): PriceLookup {
|
|
141
156
|
const key = model.trim().toLowerCase().split('[')[0]!;
|
|
142
157
|
const bare = key.slice(key.lastIndexOf('/') + 1);
|
|
158
|
+
|
|
143
159
|
if (bare === '' || AMBIGUOUS.has(bare)) {
|
|
144
160
|
return { price: null, basis: 'unknown', pricedAs: null };
|
|
145
161
|
}
|
|
162
|
+
|
|
146
163
|
const exact = table.get(key) ?? table.get(bare);
|
|
164
|
+
|
|
147
165
|
if (exact !== undefined) {
|
|
148
166
|
return { price: exact, basis: 'exact', pricedAs: null };
|
|
149
167
|
}
|
|
168
|
+
|
|
150
169
|
if (bare === 'codex-auto-review' && timestampMs !== undefined && Number.isFinite(timestampMs)) {
|
|
151
170
|
const date = new Date(timestampMs).toISOString().slice(0, 10);
|
|
152
171
|
const fallback = REVIEW_MODELS.find(([released]) => date >= released)?.[1] ?? 'gpt-5';
|
|
153
172
|
const found = lookupBasePrice(table, fallback);
|
|
173
|
+
|
|
154
174
|
return { price: found.price, basis: found.price === null ? 'unknown' : 'family', pricedAs: found.price === null ? null : fallback };
|
|
155
175
|
}
|
|
176
|
+
|
|
156
177
|
const dated = withoutDate(bare);
|
|
157
178
|
const undated = dated === null ? undefined : table.get(dated);
|
|
179
|
+
|
|
158
180
|
if (undated !== undefined && dated !== null) {
|
|
159
181
|
return { price: undated, basis: 'exact', pricedAs: dated };
|
|
160
182
|
}
|
|
183
|
+
|
|
161
184
|
for (let family = dated ?? bare; family.includes('-');) {
|
|
162
185
|
family = family.slice(0, family.lastIndexOf('-'));
|
|
163
186
|
const found = AMBIGUOUS.has(family) ? undefined : table.get(family);
|
|
187
|
+
|
|
164
188
|
if (found !== undefined) {
|
|
165
189
|
return { price: found, basis: 'family', pricedAs: family };
|
|
166
190
|
}
|
|
167
191
|
}
|
|
192
|
+
|
|
168
193
|
return { price: null, basis: 'unknown', pricedAs: null };
|
|
169
194
|
}
|
|
170
195
|
|
|
@@ -222,6 +247,7 @@ export class PriceBook {
|
|
|
222
247
|
/* Reads the table an earlier run stored, once; asking LiteLLM is left to `ensure`. */
|
|
223
248
|
load(): Promise<void> {
|
|
224
249
|
this.stored ??= this.readStored();
|
|
250
|
+
|
|
225
251
|
return this.stored;
|
|
226
252
|
}
|
|
227
253
|
|
|
@@ -232,6 +258,7 @@ export class PriceBook {
|
|
|
232
258
|
.finally(() => {
|
|
233
259
|
this.inFlight = null;
|
|
234
260
|
});
|
|
261
|
+
|
|
235
262
|
return this.inFlight;
|
|
236
263
|
}
|
|
237
264
|
|
|
@@ -243,6 +270,7 @@ export class PriceBook {
|
|
|
243
270
|
try {
|
|
244
271
|
const snapshot = JSON.parse(await readFile(this.file, 'utf8')) as Snapshot;
|
|
245
272
|
const table = parsePriceTable(snapshot?.document);
|
|
273
|
+
|
|
246
274
|
if (table.size > 0 && typeof snapshot.fetchedAt === 'number') {
|
|
247
275
|
this.table = table;
|
|
248
276
|
this.source = 'litellm';
|
|
@@ -259,17 +287,23 @@ export class PriceBook {
|
|
|
259
287
|
if (!this.allowFetch || (this.fetchedAt !== null && Date.now() - this.fetchedAt < TTL_MS)) {
|
|
260
288
|
return false;
|
|
261
289
|
}
|
|
290
|
+
|
|
262
291
|
let changed = false;
|
|
292
|
+
|
|
263
293
|
try {
|
|
264
294
|
const response = await fetch(LITELLM_URL, { signal: AbortSignal.timeout(FETCH_TIMEOUT_MS) });
|
|
295
|
+
|
|
265
296
|
if (!response.ok) {
|
|
266
297
|
throw new Error(`LiteLLM answered ${response.status}`);
|
|
267
298
|
}
|
|
299
|
+
|
|
268
300
|
const document: unknown = await response.json();
|
|
269
301
|
const table = parsePriceTable(document);
|
|
302
|
+
|
|
270
303
|
if (table.size === 0) {
|
|
271
304
|
throw new Error('The LiteLLM table held no model this app prices');
|
|
272
305
|
}
|
|
306
|
+
|
|
273
307
|
this.table = table;
|
|
274
308
|
this.source = 'litellm';
|
|
275
309
|
this.fetchedAt = Date.now();
|
|
@@ -280,6 +314,7 @@ export class PriceBook {
|
|
|
280
314
|
// Whatever is loaded keeps pricing; a table that could not be refreshed is not a failure.
|
|
281
315
|
console.warn('Could not refresh the price table:', errorText(e));
|
|
282
316
|
}
|
|
317
|
+
|
|
283
318
|
return changed;
|
|
284
319
|
}
|
|
285
320
|
}
|
package/src/usage/projects.ts
CHANGED
|
@@ -22,6 +22,7 @@ async function mainCheckoutOf(gitPath: string): Promise<string | null> {
|
|
|
22
22
|
const match = /^gitdir:\s*(.+)$/m.exec(text);
|
|
23
23
|
const gitDir = match?.[1]?.trim();
|
|
24
24
|
const at = gitDir?.indexOf('/.git/worktrees/') ?? -1;
|
|
25
|
+
|
|
25
26
|
return gitDir !== undefined && at > 0 ? gitDir.slice(0, at) : null;
|
|
26
27
|
}
|
|
27
28
|
|
|
@@ -41,25 +42,32 @@ export class ProjectResolver {
|
|
|
41
42
|
async resolve(cwd: string): Promise<ResolvedProject> {
|
|
42
43
|
const folder = cwd === '' ? '' : await this.rootOf(cwd);
|
|
43
44
|
const project = this.known.get(folder);
|
|
45
|
+
|
|
44
46
|
return { folder, name: project?.name ?? (folder === '' ? 'Elsewhere' : basename(folder)), projectId: project?.projectId ?? null };
|
|
45
47
|
}
|
|
46
48
|
|
|
47
49
|
private async rootOf(cwd: string): Promise<string> {
|
|
48
50
|
const cached = this.roots.get(cwd);
|
|
51
|
+
|
|
49
52
|
if (cached !== undefined) {
|
|
50
53
|
return cached;
|
|
51
54
|
}
|
|
55
|
+
|
|
52
56
|
let root = cwd;
|
|
57
|
+
|
|
53
58
|
for (let at = cwd; at !== dirname(at); at = dirname(at)) {
|
|
54
59
|
try {
|
|
55
60
|
const info = await stat(join(at, '.git'));
|
|
56
61
|
root = (info.isFile() ? await mainCheckoutOf(join(at, '.git')) : null) ?? at;
|
|
62
|
+
|
|
57
63
|
break;
|
|
58
64
|
} catch {
|
|
59
65
|
// Not a checkout at this level; the one above it may be.
|
|
60
66
|
}
|
|
61
67
|
}
|
|
68
|
+
|
|
62
69
|
this.roots.set(cwd, root);
|
|
70
|
+
|
|
63
71
|
return root;
|
|
64
72
|
}
|
|
65
73
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { asObject, asString, int, type UsageRecord } from '../record.ts';
|
|
1
|
+
import { asObject, asString, countsTokens, int, parseObject, type UsageRecord } from '../record.ts';
|
|
2
2
|
|
|
3
3
|
/* Only a line with this in it can carry counts, which keeps `JSON.parse` off about two thirds of them. */
|
|
4
4
|
export function claudeMightCarryUsage(line: string): boolean {
|
|
@@ -11,31 +11,32 @@ export function claudeMightCarryUsage(line: string): boolean {
|
|
|
11
11
|
* Anthropic's, so `input_tokens` already excludes what was read from or written to the cache.
|
|
12
12
|
*/
|
|
13
13
|
export function parseClaudeLine(line: string): UsageRecord | null {
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
parsed = JSON.parse(line);
|
|
17
|
-
} catch {
|
|
18
|
-
return null;
|
|
19
|
-
}
|
|
20
|
-
const record = asObject(parsed);
|
|
14
|
+
const record = parseObject(line);
|
|
15
|
+
|
|
21
16
|
if (record === null || record.type !== 'assistant') {
|
|
22
17
|
return null;
|
|
23
18
|
}
|
|
19
|
+
|
|
24
20
|
const message = asObject(record.message);
|
|
25
21
|
const usage = message === null ? null : asObject(message.usage);
|
|
22
|
+
|
|
26
23
|
if (message === null || usage === null) {
|
|
27
24
|
return null;
|
|
28
25
|
}
|
|
26
|
+
|
|
29
27
|
const model = asString(message.model);
|
|
30
28
|
const timestampMs = Date.parse(asString(record.timestamp));
|
|
29
|
+
|
|
31
30
|
// `<synthetic>` is what Claude Code writes for a message it made up itself, an error or an
|
|
32
31
|
// interruption. It never reached the API, and its counts are zero, so it is not a call.
|
|
33
32
|
if (model === '' || model === '<synthetic>' || Number.isNaN(timestampMs)) {
|
|
34
33
|
return null;
|
|
35
34
|
}
|
|
35
|
+
|
|
36
36
|
const creation = asObject(usage.cache_creation);
|
|
37
37
|
const outputDetails = asObject(usage.output_tokens_details);
|
|
38
38
|
const messageId = asString(message.id);
|
|
39
|
+
|
|
39
40
|
const totals = {
|
|
40
41
|
calls: 1,
|
|
41
42
|
input: int(usage.input_tokens),
|
|
@@ -45,9 +46,11 @@ export function parseClaudeLine(line: string): UsageRecord | null {
|
|
|
45
46
|
output: int(usage.output_tokens),
|
|
46
47
|
reasoning: outputDetails === null ? 0 : int(outputDetails.thinking_tokens)
|
|
47
48
|
};
|
|
48
|
-
|
|
49
|
+
|
|
50
|
+
if (!countsTokens(totals)) {
|
|
49
51
|
return null;
|
|
50
52
|
}
|
|
53
|
+
|
|
51
54
|
return {
|
|
52
55
|
provider: 'claude',
|
|
53
56
|
timestampMs,
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { asObject, asString, int, type UsageRecord } from '../record.ts';
|
|
1
|
+
import { asObject, asString, int, parseObject, type UsageRecord } from '../record.ts';
|
|
2
2
|
|
|
3
3
|
export function codexMightCarryUsage(line: string): boolean {
|
|
4
4
|
return ['token_count', 'turn_context', 'session_meta', 'thread_settings_applied', 'token_usage_record', 'compacted'].some((type) => line.includes(type));
|
|
@@ -76,6 +76,7 @@ function deltaOf(total: CodexCounts, previous: CodexCounts | null): CodexCounts
|
|
|
76
76
|
if (previous === null) {
|
|
77
77
|
return total;
|
|
78
78
|
}
|
|
79
|
+
|
|
79
80
|
const delta: CodexCounts = {
|
|
80
81
|
input: total.input - previous.input,
|
|
81
82
|
cached: total.cached - previous.cached,
|
|
@@ -83,6 +84,7 @@ function deltaOf(total: CodexCounts, previous: CodexCounts | null): CodexCounts
|
|
|
83
84
|
output: total.output - previous.output,
|
|
84
85
|
reasoning: total.reasoning - previous.reasoning
|
|
85
86
|
};
|
|
87
|
+
|
|
86
88
|
// A compaction or a resume restarts the count, so anything falling is not a delta at all.
|
|
87
89
|
return delta.input < 0 || delta.cached < 0 || delta.cacheWrite < 0 || delta.output < 0 || delta.reasoning < 0 ? null : delta;
|
|
88
90
|
}
|
|
@@ -92,21 +94,18 @@ export function isForkOrSubagent(payload: Record<string, unknown>): boolean {
|
|
|
92
94
|
if (typeof payload.forked_from_id === 'string' || typeof payload.parent_thread_id === 'string') {
|
|
93
95
|
return true;
|
|
94
96
|
}
|
|
97
|
+
|
|
95
98
|
const spawn = asObject(asObject(payload.source)?.subagent)?.thread_spawn;
|
|
99
|
+
|
|
96
100
|
return typeof asObject(spawn)?.parent_thread_id === 'string';
|
|
97
101
|
}
|
|
98
102
|
|
|
99
103
|
/* Codex includes cache reads and writes in input_tokens. Compaction can appear separately from
|
|
100
104
|
cumulative token_count events, so response identities prevent charging the same request twice. */
|
|
101
105
|
export function parseCodexLine(line: string, state: CodexParserState): UsageRecord | null {
|
|
102
|
-
|
|
103
|
-
try {
|
|
104
|
-
parsed = JSON.parse(line);
|
|
105
|
-
} catch {
|
|
106
|
-
return null;
|
|
107
|
-
}
|
|
108
|
-
const record = asObject(parsed);
|
|
106
|
+
const record = parseObject(line);
|
|
109
107
|
const payload = record === null ? null : asObject(record.payload);
|
|
108
|
+
|
|
110
109
|
if (record === null || payload === null) {
|
|
111
110
|
return null;
|
|
112
111
|
}
|
|
@@ -116,33 +115,42 @@ export function parseCodexLine(line: string, state: CodexParserState): UsageReco
|
|
|
116
115
|
if (state.sawSessionMeta) {
|
|
117
116
|
return null;
|
|
118
117
|
}
|
|
118
|
+
|
|
119
119
|
state.sawSessionMeta = true;
|
|
120
120
|
state.sessionId = asString(payload.id) || asString(payload.session_id);
|
|
121
121
|
state.cwd = asString(payload.cwd);
|
|
122
|
+
|
|
122
123
|
if (isForkOrSubagent(payload)) {
|
|
123
124
|
state.suppressing = true;
|
|
124
125
|
state.anchorMs = Date.parse(asString(record.timestamp)) || 0;
|
|
125
126
|
}
|
|
127
|
+
|
|
126
128
|
return null;
|
|
127
129
|
}
|
|
128
130
|
|
|
129
131
|
if (record.type === 'turn_context') {
|
|
130
132
|
const model = asString(payload.model);
|
|
133
|
+
|
|
131
134
|
if (model !== '') {
|
|
132
135
|
state.model = model;
|
|
133
136
|
}
|
|
137
|
+
|
|
134
138
|
const cwd = asString(payload.cwd);
|
|
139
|
+
|
|
135
140
|
if (cwd !== '') {
|
|
136
141
|
state.cwd = cwd;
|
|
137
142
|
}
|
|
143
|
+
|
|
138
144
|
return null;
|
|
139
145
|
}
|
|
140
146
|
|
|
141
147
|
if (payload.type === 'thread_settings_applied') {
|
|
142
148
|
const settings = asObject(payload.thread_settings);
|
|
149
|
+
|
|
143
150
|
if (settings !== null && typeof settings.service_tier === 'string') {
|
|
144
151
|
state.serviceTier = settings.service_tier;
|
|
145
152
|
}
|
|
153
|
+
|
|
146
154
|
return null;
|
|
147
155
|
}
|
|
148
156
|
|
|
@@ -153,26 +161,33 @@ export function parseCodexLine(line: string, state: CodexParserState): UsageReco
|
|
|
153
161
|
if (payload.type !== 'token_count') {
|
|
154
162
|
return null;
|
|
155
163
|
}
|
|
164
|
+
|
|
156
165
|
const info = asObject(payload.info);
|
|
157
166
|
const total = info === null ? null : asObject(info.total_token_usage);
|
|
167
|
+
|
|
158
168
|
if (info === null || total === null || state.model === null) {
|
|
159
169
|
return null;
|
|
160
170
|
}
|
|
171
|
+
|
|
161
172
|
const timestampMs = Date.parse(asString(record.timestamp));
|
|
173
|
+
|
|
162
174
|
if (Number.isNaN(timestampMs)) {
|
|
163
175
|
return null;
|
|
164
176
|
}
|
|
165
177
|
|
|
166
178
|
const signature = JSON.stringify(info.total_token_usage);
|
|
179
|
+
|
|
167
180
|
if (signature === state.lastSignature) {
|
|
168
181
|
return null;
|
|
169
182
|
}
|
|
183
|
+
|
|
170
184
|
state.lastSignature = signature;
|
|
171
185
|
|
|
172
186
|
const cumulative = countsOf(total);
|
|
173
187
|
const last = asObject(info.last_token_usage);
|
|
174
188
|
const counts = deltaOf(cumulative, state.total) ?? (last === null ? null : countsOf(last));
|
|
175
189
|
state.total = cumulative;
|
|
190
|
+
|
|
176
191
|
if (counts === null) {
|
|
177
192
|
return null;
|
|
178
193
|
}
|
|
@@ -180,19 +195,23 @@ export function parseCodexLine(line: string, state: CodexParserState): UsageReco
|
|
|
180
195
|
if (state.suppressing) {
|
|
181
196
|
if (timestampMs - state.anchorMs <= FORK_COPY_MAX_GAP_MS) {
|
|
182
197
|
state.anchorMs = timestampMs;
|
|
198
|
+
|
|
183
199
|
return null;
|
|
184
200
|
}
|
|
201
|
+
|
|
185
202
|
// The first event that is not part of the replayed burst is this thread's own work.
|
|
186
203
|
state.suppressing = false;
|
|
187
204
|
}
|
|
188
205
|
|
|
189
206
|
let dedupeKey: string | null = null;
|
|
190
207
|
const pending = state.latestResponse === null ? undefined : state.pendingCompactions[state.latestResponse];
|
|
208
|
+
|
|
191
209
|
if (pending !== undefined && (sameCounts(pending.counts, counts) || sameCounts(pending.threadTotal, cumulative))) {
|
|
192
210
|
dedupeKey = `codex:response:${state.latestResponse}`;
|
|
193
211
|
delete state.pendingCompactions[state.latestResponse!];
|
|
194
212
|
state.handledResponses.push(state.latestResponse!);
|
|
195
213
|
}
|
|
214
|
+
|
|
196
215
|
return usageRecord(counts, timestampMs, state.model, state, dedupeKey);
|
|
197
216
|
}
|
|
198
217
|
|
|
@@ -209,9 +228,11 @@ function sameCounts(first: CodexCounts | null, second: CodexCounts): boolean {
|
|
|
209
228
|
|
|
210
229
|
function parseCompaction(record: Record<string, unknown>, payload: Record<string, unknown>, state: CodexParserState): UsageRecord | null {
|
|
211
230
|
const id = asString(record.type === 'compacted' ? payload.compaction_response_id : payload.response_id);
|
|
231
|
+
|
|
212
232
|
if (id === '' || state.handledResponses.includes(id)) {
|
|
213
233
|
return null;
|
|
214
234
|
}
|
|
235
|
+
|
|
215
236
|
if (record.type === 'compacted') {
|
|
216
237
|
if (!state.compacted.includes(id)) {
|
|
217
238
|
state.compacted.push(id);
|
|
@@ -220,29 +241,38 @@ function parseCompaction(record: Record<string, unknown>, payload: Record<string
|
|
|
220
241
|
const usage = asObject(payload.usage);
|
|
221
242
|
const model = asString(payload.model) || state.model;
|
|
222
243
|
const timestampMs = Date.parse(asString(record.timestamp));
|
|
244
|
+
|
|
223
245
|
if (usage === null || model === null || !Number.isFinite(timestampMs)) {
|
|
224
246
|
return null;
|
|
225
247
|
}
|
|
248
|
+
|
|
226
249
|
const counts = countsOf(usage);
|
|
227
250
|
const parsed = usageRecord(counts, timestampMs, model, state, `codex:response:${id}`);
|
|
251
|
+
|
|
228
252
|
if (parsed === null) {
|
|
229
253
|
return null;
|
|
230
254
|
}
|
|
255
|
+
|
|
231
256
|
state.latestResponse = id;
|
|
232
257
|
const threadTotal = asObject(payload.thread_token_usage);
|
|
233
258
|
state.pendingCompactions[id] ??= { record: parsed, counts, threadTotal: threadTotal === null ? null : countsOf(threadTotal) };
|
|
234
259
|
}
|
|
260
|
+
|
|
235
261
|
const pending = state.pendingCompactions[id];
|
|
262
|
+
|
|
236
263
|
if (!state.compacted.includes(id) || pending === undefined) {
|
|
237
264
|
return null;
|
|
238
265
|
}
|
|
266
|
+
|
|
239
267
|
state.handledResponses.push(id);
|
|
240
268
|
delete state.pendingCompactions[id];
|
|
269
|
+
|
|
241
270
|
return pending.record;
|
|
242
271
|
}
|
|
243
272
|
|
|
244
273
|
function usageRecord(counts: CodexCounts, timestampMs: number, model: string, state: CodexParserState, dedupeKey: string | null): UsageRecord | null {
|
|
245
274
|
const uncached = Math.max(0, counts.input - counts.cached - counts.cacheWrite);
|
|
275
|
+
|
|
246
276
|
const totals = {
|
|
247
277
|
calls: 1,
|
|
248
278
|
input: uncached,
|
|
@@ -252,9 +282,11 @@ function usageRecord(counts: CodexCounts, timestampMs: number, model: string, st
|
|
|
252
282
|
output: counts.output,
|
|
253
283
|
reasoning: Math.min(counts.output, counts.reasoning)
|
|
254
284
|
};
|
|
285
|
+
|
|
255
286
|
if (uncached + counts.cached + counts.cacheWrite + counts.output === 0) {
|
|
256
287
|
return null;
|
|
257
288
|
}
|
|
289
|
+
|
|
258
290
|
return {
|
|
259
291
|
provider: 'codex',
|
|
260
292
|
timestampMs,
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { basename, dirname } from 'node:path';
|
|
2
|
-
import { asObject, asString, int, type UsageRecord } from '../record.ts';
|
|
2
|
+
import { asObject, asString, countsTokens, int, parseObject, type UsageRecord } from '../record.ts';
|
|
3
3
|
|
|
4
4
|
/* The file Grok keeps the counts of a session in, beside its other files in the session's folder. */
|
|
5
5
|
export const GROK_USAGE_FILE = 'usage.json';
|
|
@@ -19,6 +19,7 @@ function cwdOf(path: string): string {
|
|
|
19
19
|
function recordOf(counts: Record<string, unknown>, model: string, base: Omit<UsageRecord, 'model' | 'totals' | 'dedupeKey'>, turn: string): UsageRecord | null {
|
|
20
20
|
const cacheRead = int(counts.cachedReadTokens);
|
|
21
21
|
const cacheWrite = int(counts.cacheCreationTokens);
|
|
22
|
+
|
|
22
23
|
const totals = {
|
|
23
24
|
calls: int(counts.modelCalls),
|
|
24
25
|
// Grok's input holds both cache kinds: its total is input plus output.
|
|
@@ -29,10 +30,13 @@ function recordOf(counts: Record<string, unknown>, model: string, base: Omit<Usa
|
|
|
29
30
|
output: int(counts.outputTokens),
|
|
30
31
|
reasoning: int(counts.reasoningTokens)
|
|
31
32
|
};
|
|
32
|
-
|
|
33
|
+
|
|
34
|
+
if (!countsTokens(totals)) {
|
|
33
35
|
return null;
|
|
34
36
|
}
|
|
37
|
+
|
|
35
38
|
const ticks = int(counts.costUsdTicks);
|
|
39
|
+
|
|
36
40
|
return {
|
|
37
41
|
...base,
|
|
38
42
|
model,
|
|
@@ -49,41 +53,45 @@ function recordOf(counts: Record<string, unknown>, model: string, base: Omit<Usa
|
|
|
49
53
|
* read as one, which leaves what the scanner held of it.
|
|
50
54
|
*/
|
|
51
55
|
export function parseGrokUsage(text: string, path: string): UsageRecord[] | null {
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
parsed = JSON.parse(text);
|
|
55
|
-
} catch {
|
|
56
|
-
return null;
|
|
57
|
-
}
|
|
58
|
-
const file = asObject(parsed);
|
|
56
|
+
const file = parseObject(text);
|
|
57
|
+
|
|
59
58
|
if (file === null || !Array.isArray(file.turns)) {
|
|
60
59
|
return null;
|
|
61
60
|
}
|
|
61
|
+
|
|
62
62
|
const sessionId = asString(file.sessionId) || basename(dirname(path));
|
|
63
63
|
const cwd = cwdOf(path);
|
|
64
64
|
const records: UsageRecord[] = [];
|
|
65
|
+
|
|
65
66
|
for (const entry of file.turns) {
|
|
66
67
|
const turn = asObject(entry);
|
|
67
68
|
const timestampMs = turn === null ? Number.NaN : Date.parse(asString(turn.endedAt));
|
|
69
|
+
|
|
68
70
|
if (turn === null || Number.isNaN(timestampMs)) {
|
|
69
71
|
continue;
|
|
70
72
|
}
|
|
73
|
+
|
|
71
74
|
const number = typeof turn.turnNumber === 'number' ? String(turn.turnNumber) : asString(turn.endedAt);
|
|
72
75
|
const base = { provider: 'grok' as const, timestampMs, sessionId, cwd };
|
|
73
76
|
const byModel = asObject(turn.modelUsage);
|
|
77
|
+
|
|
74
78
|
const models: Array<[string, Record<string, unknown>]> =
|
|
75
79
|
byModel === null
|
|
76
80
|
? [[asString(turn.primaryModelId), turn]]
|
|
77
81
|
: Object.entries(byModel).flatMap(([model, counts]) => {
|
|
78
82
|
const object = asObject(counts);
|
|
83
|
+
|
|
79
84
|
return object === null ? [] : [[model, object] as [string, Record<string, unknown>]];
|
|
80
85
|
});
|
|
86
|
+
|
|
81
87
|
for (const [model, counts] of models) {
|
|
82
88
|
const record = model === '' ? null : recordOf(counts, model, base, number);
|
|
89
|
+
|
|
83
90
|
if (record !== null) {
|
|
84
91
|
records.push(record);
|
|
85
92
|
}
|
|
86
93
|
}
|
|
87
94
|
}
|
|
95
|
+
|
|
88
96
|
return records;
|
|
89
97
|
}
|