@agent-finops/core 0.5.5 → 0.5.7
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/LICENSE +21 -0
- package/README.md +16 -0
- package/dist/agentInventory.d.ts +35 -9
- package/dist/agentInventory.js +309 -11
- package/dist/analyze.js +6 -2
- package/dist/contextHealth.d.ts +101 -0
- package/dist/contextHealth.js +371 -0
- package/dist/deadContext.js +10 -1
- package/dist/discovery.d.ts +14 -1
- package/dist/discovery.js +54 -28
- package/dist/glance.d.ts +161 -0
- package/dist/glance.js +586 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +2 -0
- package/dist/localAgentLogs.d.ts +42 -2
- package/dist/localAgentLogs.js +460 -7
- package/dist/modelPricing.d.ts +2 -1
- package/dist/modelPricing.js +22 -3
- package/dist/planMath.d.ts +12 -14
- package/dist/planMath.js +18 -18
- package/dist/providerConnectors.d.ts +19 -1
- package/dist/providerConnectors.js +106 -26
- package/dist/scanGuard.d.ts +35 -0
- package/dist/scanGuard.js +196 -8
- package/dist/schema.d.ts +24 -24
- package/dist/sourceRegistry.js +1 -1
- package/dist/toolInvocations.d.ts +42 -1
- package/dist/toolInvocations.js +244 -4
- package/package.json +15 -2
package/dist/schema.d.ts
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
2
|
export declare const costConfidenceValues: readonly ["verified", "estimated", "detected_unverified", "missing"];
|
|
3
3
|
export declare const costConfidenceSchema: z.ZodEnum<{
|
|
4
|
-
estimated: "estimated";
|
|
5
4
|
verified: "verified";
|
|
5
|
+
estimated: "estimated";
|
|
6
6
|
detected_unverified: "detected_unverified";
|
|
7
7
|
missing: "missing";
|
|
8
8
|
}>;
|
|
@@ -12,8 +12,8 @@ export declare const spendSourceSchema: z.ZodObject<{
|
|
|
12
12
|
name: z.ZodString;
|
|
13
13
|
provider: z.ZodString;
|
|
14
14
|
confidence: z.ZodEnum<{
|
|
15
|
-
estimated: "estimated";
|
|
16
15
|
verified: "verified";
|
|
16
|
+
estimated: "estimated";
|
|
17
17
|
detected_unverified: "detected_unverified";
|
|
18
18
|
missing: "missing";
|
|
19
19
|
}>;
|
|
@@ -28,8 +28,8 @@ export declare const usageRecordSchema: z.ZodObject<{
|
|
|
28
28
|
name: z.ZodString;
|
|
29
29
|
provider: z.ZodString;
|
|
30
30
|
confidence: z.ZodEnum<{
|
|
31
|
-
estimated: "estimated";
|
|
32
31
|
verified: "verified";
|
|
32
|
+
estimated: "estimated";
|
|
33
33
|
detected_unverified: "detected_unverified";
|
|
34
34
|
missing: "missing";
|
|
35
35
|
}>;
|
|
@@ -40,8 +40,8 @@ export declare const usageRecordSchema: z.ZodObject<{
|
|
|
40
40
|
outputTokens: z.ZodNumber;
|
|
41
41
|
amountUsd: z.ZodNullable<z.ZodNumber>;
|
|
42
42
|
costConfidence: z.ZodEnum<{
|
|
43
|
-
estimated: "estimated";
|
|
44
43
|
verified: "verified";
|
|
44
|
+
estimated: "estimated";
|
|
45
45
|
detected_unverified: "detected_unverified";
|
|
46
46
|
missing: "missing";
|
|
47
47
|
}>;
|
|
@@ -99,10 +99,10 @@ export declare const attributionMappingSchema: z.ZodObject<{
|
|
|
99
99
|
evidence: z.ZodArray<z.ZodString>;
|
|
100
100
|
}, z.core.$strip>>;
|
|
101
101
|
status: z.ZodEnum<{
|
|
102
|
+
unmapped: "unmapped";
|
|
102
103
|
auto_mapped: "auto_mapped";
|
|
103
104
|
needs_confirmation: "needs_confirmation";
|
|
104
105
|
needs_question: "needs_question";
|
|
105
|
-
unmapped: "unmapped";
|
|
106
106
|
}>;
|
|
107
107
|
evidence: z.ZodArray<z.ZodString>;
|
|
108
108
|
}, z.core.$strip>;
|
|
@@ -112,8 +112,8 @@ export declare const spendBreakdownEntrySchema: z.ZodObject<{
|
|
|
112
112
|
amountUsd: z.ZodNumber;
|
|
113
113
|
recordCount: z.ZodNumber;
|
|
114
114
|
confidence: z.ZodEnum<{
|
|
115
|
-
estimated: "estimated";
|
|
116
115
|
verified: "verified";
|
|
116
|
+
estimated: "estimated";
|
|
117
117
|
detected_unverified: "detected_unverified";
|
|
118
118
|
missing: "missing";
|
|
119
119
|
}>;
|
|
@@ -129,8 +129,8 @@ export declare const spendAnomalySchema: z.ZodObject<{
|
|
|
129
129
|
currentAmountUsd: z.ZodNumber;
|
|
130
130
|
multiplier: z.ZodNumber;
|
|
131
131
|
confidence: z.ZodEnum<{
|
|
132
|
-
estimated: "estimated";
|
|
133
132
|
verified: "verified";
|
|
133
|
+
estimated: "estimated";
|
|
134
134
|
detected_unverified: "detected_unverified";
|
|
135
135
|
missing: "missing";
|
|
136
136
|
}>;
|
|
@@ -146,8 +146,8 @@ export declare const workflowWatchEntrySchema: z.ZodObject<{
|
|
|
146
146
|
shareOfSpend: z.ZodNumber;
|
|
147
147
|
recordCount: z.ZodNumber;
|
|
148
148
|
confidence: z.ZodEnum<{
|
|
149
|
-
estimated: "estimated";
|
|
150
149
|
verified: "verified";
|
|
150
|
+
estimated: "estimated";
|
|
151
151
|
detected_unverified: "detected_unverified";
|
|
152
152
|
missing: "missing";
|
|
153
153
|
}>;
|
|
@@ -171,8 +171,8 @@ export declare const recommendationSchema: z.ZodObject<{
|
|
|
171
171
|
}>;
|
|
172
172
|
estimatedImpactUsd: z.ZodNumber;
|
|
173
173
|
confidence: z.ZodEnum<{
|
|
174
|
-
estimated: "estimated";
|
|
175
174
|
verified: "verified";
|
|
175
|
+
estimated: "estimated";
|
|
176
176
|
detected_unverified: "detected_unverified";
|
|
177
177
|
missing: "missing";
|
|
178
178
|
}>;
|
|
@@ -216,8 +216,8 @@ export declare const spendInsightSchema: z.ZodObject<{
|
|
|
216
216
|
affectedModels: z.ZodArray<z.ZodString>;
|
|
217
217
|
estimatedImpactUsd: z.ZodNumber;
|
|
218
218
|
confidence: z.ZodEnum<{
|
|
219
|
-
estimated: "estimated";
|
|
220
219
|
verified: "verified";
|
|
220
|
+
estimated: "estimated";
|
|
221
221
|
detected_unverified: "detected_unverified";
|
|
222
222
|
missing: "missing";
|
|
223
223
|
}>;
|
|
@@ -229,14 +229,14 @@ export declare const spendSummarySchema: z.ZodObject<{
|
|
|
229
229
|
totalUsd: z.ZodNumber;
|
|
230
230
|
recordCount: z.ZodNumber;
|
|
231
231
|
confidence: z.ZodEnum<{
|
|
232
|
-
estimated: "estimated";
|
|
233
232
|
verified: "verified";
|
|
233
|
+
estimated: "estimated";
|
|
234
234
|
detected_unverified: "detected_unverified";
|
|
235
235
|
missing: "missing";
|
|
236
236
|
}>;
|
|
237
237
|
confidenceBreakdown: z.ZodRecord<z.ZodEnum<{
|
|
238
|
-
estimated: "estimated";
|
|
239
238
|
verified: "verified";
|
|
239
|
+
estimated: "estimated";
|
|
240
240
|
detected_unverified: "detected_unverified";
|
|
241
241
|
missing: "missing";
|
|
242
242
|
}>, z.ZodNumber>;
|
|
@@ -245,8 +245,8 @@ export declare const spendSummarySchema: z.ZodObject<{
|
|
|
245
245
|
amountUsd: z.ZodNumber;
|
|
246
246
|
recordCount: z.ZodNumber;
|
|
247
247
|
confidence: z.ZodEnum<{
|
|
248
|
-
estimated: "estimated";
|
|
249
248
|
verified: "verified";
|
|
249
|
+
estimated: "estimated";
|
|
250
250
|
detected_unverified: "detected_unverified";
|
|
251
251
|
missing: "missing";
|
|
252
252
|
}>;
|
|
@@ -256,8 +256,8 @@ export declare const spendSummarySchema: z.ZodObject<{
|
|
|
256
256
|
amountUsd: z.ZodNumber;
|
|
257
257
|
recordCount: z.ZodNumber;
|
|
258
258
|
confidence: z.ZodEnum<{
|
|
259
|
-
estimated: "estimated";
|
|
260
259
|
verified: "verified";
|
|
260
|
+
estimated: "estimated";
|
|
261
261
|
detected_unverified: "detected_unverified";
|
|
262
262
|
missing: "missing";
|
|
263
263
|
}>;
|
|
@@ -267,8 +267,8 @@ export declare const spendSummarySchema: z.ZodObject<{
|
|
|
267
267
|
amountUsd: z.ZodNumber;
|
|
268
268
|
recordCount: z.ZodNumber;
|
|
269
269
|
confidence: z.ZodEnum<{
|
|
270
|
-
estimated: "estimated";
|
|
271
270
|
verified: "verified";
|
|
271
|
+
estimated: "estimated";
|
|
272
272
|
detected_unverified: "detected_unverified";
|
|
273
273
|
missing: "missing";
|
|
274
274
|
}>;
|
|
@@ -278,8 +278,8 @@ export declare const spendSummarySchema: z.ZodObject<{
|
|
|
278
278
|
amountUsd: z.ZodNumber;
|
|
279
279
|
recordCount: z.ZodNumber;
|
|
280
280
|
confidence: z.ZodEnum<{
|
|
281
|
-
estimated: "estimated";
|
|
282
281
|
verified: "verified";
|
|
282
|
+
estimated: "estimated";
|
|
283
283
|
detected_unverified: "detected_unverified";
|
|
284
284
|
missing: "missing";
|
|
285
285
|
}>;
|
|
@@ -289,8 +289,8 @@ export declare const spendSummarySchema: z.ZodObject<{
|
|
|
289
289
|
amountUsd: z.ZodNumber;
|
|
290
290
|
recordCount: z.ZodNumber;
|
|
291
291
|
confidence: z.ZodEnum<{
|
|
292
|
-
estimated: "estimated";
|
|
293
292
|
verified: "verified";
|
|
293
|
+
estimated: "estimated";
|
|
294
294
|
detected_unverified: "detected_unverified";
|
|
295
295
|
missing: "missing";
|
|
296
296
|
}>;
|
|
@@ -300,8 +300,8 @@ export declare const spendSummarySchema: z.ZodObject<{
|
|
|
300
300
|
amountUsd: z.ZodNumber;
|
|
301
301
|
recordCount: z.ZodNumber;
|
|
302
302
|
confidence: z.ZodEnum<{
|
|
303
|
-
estimated: "estimated";
|
|
304
303
|
verified: "verified";
|
|
304
|
+
estimated: "estimated";
|
|
305
305
|
detected_unverified: "detected_unverified";
|
|
306
306
|
missing: "missing";
|
|
307
307
|
}>;
|
|
@@ -311,8 +311,8 @@ export declare const spendSummarySchema: z.ZodObject<{
|
|
|
311
311
|
amountUsd: z.ZodNumber;
|
|
312
312
|
recordCount: z.ZodNumber;
|
|
313
313
|
confidence: z.ZodEnum<{
|
|
314
|
-
estimated: "estimated";
|
|
315
314
|
verified: "verified";
|
|
315
|
+
estimated: "estimated";
|
|
316
316
|
detected_unverified: "detected_unverified";
|
|
317
317
|
missing: "missing";
|
|
318
318
|
}>;
|
|
@@ -322,8 +322,8 @@ export declare const spendSummarySchema: z.ZodObject<{
|
|
|
322
322
|
amountUsd: z.ZodNumber;
|
|
323
323
|
recordCount: z.ZodNumber;
|
|
324
324
|
confidence: z.ZodEnum<{
|
|
325
|
-
estimated: "estimated";
|
|
326
325
|
verified: "verified";
|
|
326
|
+
estimated: "estimated";
|
|
327
327
|
detected_unverified: "detected_unverified";
|
|
328
328
|
missing: "missing";
|
|
329
329
|
}>;
|
|
@@ -338,8 +338,8 @@ export declare const spendSummarySchema: z.ZodObject<{
|
|
|
338
338
|
shareOfSpend: z.ZodNumber;
|
|
339
339
|
recordCount: z.ZodNumber;
|
|
340
340
|
confidence: z.ZodEnum<{
|
|
341
|
-
estimated: "estimated";
|
|
342
341
|
verified: "verified";
|
|
342
|
+
estimated: "estimated";
|
|
343
343
|
detected_unverified: "detected_unverified";
|
|
344
344
|
missing: "missing";
|
|
345
345
|
}>;
|
|
@@ -359,8 +359,8 @@ export declare const spendSummarySchema: z.ZodObject<{
|
|
|
359
359
|
currentAmountUsd: z.ZodNumber;
|
|
360
360
|
multiplier: z.ZodNumber;
|
|
361
361
|
confidence: z.ZodEnum<{
|
|
362
|
-
estimated: "estimated";
|
|
363
362
|
verified: "verified";
|
|
363
|
+
estimated: "estimated";
|
|
364
364
|
detected_unverified: "detected_unverified";
|
|
365
365
|
missing: "missing";
|
|
366
366
|
}>;
|
|
@@ -378,8 +378,8 @@ export declare const spendSummarySchema: z.ZodObject<{
|
|
|
378
378
|
}>;
|
|
379
379
|
estimatedImpactUsd: z.ZodNumber;
|
|
380
380
|
confidence: z.ZodEnum<{
|
|
381
|
-
estimated: "estimated";
|
|
382
381
|
verified: "verified";
|
|
382
|
+
estimated: "estimated";
|
|
383
383
|
detected_unverified: "detected_unverified";
|
|
384
384
|
missing: "missing";
|
|
385
385
|
}>;
|
|
@@ -416,8 +416,8 @@ export declare const spendSummarySchema: z.ZodObject<{
|
|
|
416
416
|
affectedModels: z.ZodArray<z.ZodString>;
|
|
417
417
|
estimatedImpactUsd: z.ZodNumber;
|
|
418
418
|
confidence: z.ZodEnum<{
|
|
419
|
-
estimated: "estimated";
|
|
420
419
|
verified: "verified";
|
|
420
|
+
estimated: "estimated";
|
|
421
421
|
detected_unverified: "detected_unverified";
|
|
422
422
|
missing: "missing";
|
|
423
423
|
}>;
|
package/dist/sourceRegistry.js
CHANGED
|
@@ -350,7 +350,7 @@ function defaultScopeForSource(type) {
|
|
|
350
350
|
return "Read-only Browser Account UI source. User logs in locally; agent never sees passwords; 2FA/CAPTCHA handoff; audit all page reads/downloads.";
|
|
351
351
|
}
|
|
352
352
|
if (type === "local_tool_detection") {
|
|
353
|
-
return "Read-only local CLI/tool detection path. Detection is not
|
|
353
|
+
return "Read-only local CLI/tool detection path. Detection is not official provider-reported cost; connect an account API or export to add that evidence.";
|
|
354
354
|
}
|
|
355
355
|
if (type === "mcp_tool" || type === "internal_system") {
|
|
356
356
|
return "Read-only approved MCP/internal-system source. No writes, sends, deletes, or production changes without approval.";
|
|
@@ -23,6 +23,27 @@ export type ToolInvocationCount = {
|
|
|
23
23
|
name: string;
|
|
24
24
|
count: number;
|
|
25
25
|
};
|
|
26
|
+
export type SessionContextSignal = {
|
|
27
|
+
agent: "claude-code" | "codex";
|
|
28
|
+
sessionId?: string;
|
|
29
|
+
lastActivityAt?: string;
|
|
30
|
+
/** Explicit compaction markers observed in this transcript. */
|
|
31
|
+
compactionEvents: number;
|
|
32
|
+
/**
|
|
33
|
+
* Explicit file-read tool calls, reduced to basenames so raw local paths
|
|
34
|
+
* never enter the Context Health contract.
|
|
35
|
+
*/
|
|
36
|
+
fileReads: ToolInvocationCount[];
|
|
37
|
+
repeatedFileReads: ToolInvocationCount[];
|
|
38
|
+
/** Whether this transcript is a Claude sidechain/subagent transcript. */
|
|
39
|
+
isSubagent: boolean;
|
|
40
|
+
parentSessionId?: string;
|
|
41
|
+
/**
|
|
42
|
+
* Coverage is intentionally narrow. Shell commands are not parsed as file
|
|
43
|
+
* reads because doing so would turn arbitrary command text into a heuristic.
|
|
44
|
+
*/
|
|
45
|
+
readCoverage: "explicit_read_tools_only";
|
|
46
|
+
};
|
|
26
47
|
export type InvocationSummary = {
|
|
27
48
|
/** aggregated counts by raw tool name across all parsed transcripts */
|
|
28
49
|
invocations: ToolInvocationCount[];
|
|
@@ -40,10 +61,19 @@ export type InvocationSummary = {
|
|
|
40
61
|
totalAssistantTurns: number;
|
|
41
62
|
/** assistant-turn count per session, for cache-read pricing */
|
|
42
63
|
sessionTurnCounts: number[];
|
|
64
|
+
/** Transcript coverage by host. Optional for backwards-compatible fixtures. */
|
|
65
|
+
sourceSessions?: {
|
|
66
|
+
claudeCode: number;
|
|
67
|
+
codex: number;
|
|
68
|
+
};
|
|
69
|
+
/** Per-transcript compaction/read evidence used by Context Health. */
|
|
70
|
+
sessionSignals?: SessionContextSignal[];
|
|
43
71
|
};
|
|
44
72
|
export type ToolInvocationOptions = {
|
|
45
73
|
/** default: join(homedir(), ".claude", "projects") */
|
|
46
74
|
claudeProjectsDir?: string;
|
|
75
|
+
/** default: join(homedir(), ".codex", "sessions") */
|
|
76
|
+
codexSessionsDir?: string;
|
|
47
77
|
/** optional: only count turns at/after this time */
|
|
48
78
|
sinceIso?: string;
|
|
49
79
|
};
|
|
@@ -55,7 +85,18 @@ export declare function parseClaudeCodeInvocations(content: string, sinceMs?: nu
|
|
|
55
85
|
invokedSubagents: string[];
|
|
56
86
|
invokedCommands: string[];
|
|
57
87
|
assistantTurns: number;
|
|
88
|
+
contextSignal: SessionContextSignal;
|
|
89
|
+
};
|
|
90
|
+
/** Parse ONE Codex rollout's tool/skill/subagent invocations. */
|
|
91
|
+
export declare function parseCodexInvocations(content: string, sinceMs?: number): {
|
|
92
|
+
invocations: ToolInvocationCount[];
|
|
93
|
+
invokedMcpTools: string[];
|
|
94
|
+
invokedSkills: string[];
|
|
95
|
+
invokedSubagents: string[];
|
|
96
|
+
invokedCommands: string[];
|
|
97
|
+
assistantTurns: number;
|
|
98
|
+
contextSignal: SessionContextSignal;
|
|
58
99
|
};
|
|
59
|
-
/** Scan this machine's Claude Code transcripts and aggregate
|
|
100
|
+
/** Scan this machine's Claude Code + Codex transcripts and aggregate invocations. */
|
|
60
101
|
export declare function loadToolInvocations(options?: ToolInvocationOptions): Promise<InvocationSummary>;
|
|
61
102
|
//# sourceMappingURL=toolInvocations.d.ts.map
|
package/dist/toolInvocations.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { readdir, readFile, stat } from "node:fs/promises";
|
|
2
|
-
import { join } from "node:path";
|
|
2
|
+
import { basename, join } from "node:path";
|
|
3
3
|
import { homedir } from "node:os";
|
|
4
4
|
/** Parse ONE transcript's content. Exported for tests. Returns the per-file pieces the aggregator needs. */
|
|
5
5
|
export function parseClaudeCodeInvocations(content, sinceMs) {
|
|
@@ -8,8 +8,14 @@ export function parseClaudeCodeInvocations(content, sinceMs) {
|
|
|
8
8
|
const skills = new Set();
|
|
9
9
|
const subagents = new Set();
|
|
10
10
|
const commands = new Set();
|
|
11
|
+
const fileReads = new Map();
|
|
11
12
|
const seen = new Set();
|
|
12
13
|
let assistantTurns = 0;
|
|
14
|
+
let sessionId;
|
|
15
|
+
let lastActivityAt;
|
|
16
|
+
let compactionEvents = 0;
|
|
17
|
+
let isSubagent = false;
|
|
18
|
+
let parentSessionId;
|
|
13
19
|
for (const line of content.split("\n")) {
|
|
14
20
|
if (!line.trim())
|
|
15
21
|
continue;
|
|
@@ -22,12 +28,22 @@ export function parseClaudeCodeInvocations(content, sinceMs) {
|
|
|
22
28
|
}
|
|
23
29
|
if (!isRecord(entry))
|
|
24
30
|
continue;
|
|
31
|
+
sessionId = stringOf(entry.sessionId) ?? sessionId;
|
|
32
|
+
if (entry.isSidechain === true)
|
|
33
|
+
isSubagent = true;
|
|
34
|
+
parentSessionId = stringOf(entry.parentSessionId) ?? parentSessionId;
|
|
35
|
+
const timestamp = stringOf(entry.timestamp);
|
|
36
|
+
if (timestamp && (!lastActivityAt || timestamp > lastActivityAt)) {
|
|
37
|
+
lastActivityAt = timestamp;
|
|
38
|
+
}
|
|
25
39
|
// sinceIso filter: skip lines older than the cutoff when a timestamp exists.
|
|
26
40
|
if (typeof sinceMs === "number") {
|
|
27
41
|
const ts = Date.parse(stringOf(entry.timestamp) ?? "");
|
|
28
42
|
if (Number.isFinite(ts) && ts < sinceMs)
|
|
29
43
|
continue;
|
|
30
44
|
}
|
|
45
|
+
if (isClaudeCompactionEntry(entry))
|
|
46
|
+
compactionEvents += 1;
|
|
31
47
|
// Slash commands surface in user lines as "<command-name>/foo</command-name>".
|
|
32
48
|
if (entry.type === "user") {
|
|
33
49
|
for (const cmd of slashCommandsFrom(entry))
|
|
@@ -70,6 +86,9 @@ export function parseClaudeCodeInvocations(content, sinceMs) {
|
|
|
70
86
|
if (sub)
|
|
71
87
|
subagents.add(sub);
|
|
72
88
|
}
|
|
89
|
+
const file = input && explicitReadFile(name, input);
|
|
90
|
+
if (file)
|
|
91
|
+
fileReads.set(file, (fileReads.get(file) ?? 0) + 1);
|
|
73
92
|
}
|
|
74
93
|
}
|
|
75
94
|
const invocations = [...counts.entries()]
|
|
@@ -81,12 +100,124 @@ export function parseClaudeCodeInvocations(content, sinceMs) {
|
|
|
81
100
|
invokedSkills: [...skills].sort(),
|
|
82
101
|
invokedSubagents: [...subagents].sort(),
|
|
83
102
|
invokedCommands: [...commands].sort(),
|
|
84
|
-
assistantTurns
|
|
103
|
+
assistantTurns,
|
|
104
|
+
contextSignal: buildSessionContextSignal({
|
|
105
|
+
agent: "claude-code",
|
|
106
|
+
sessionId,
|
|
107
|
+
lastActivityAt,
|
|
108
|
+
compactionEvents,
|
|
109
|
+
fileReads,
|
|
110
|
+
isSubagent,
|
|
111
|
+
parentSessionId
|
|
112
|
+
})
|
|
85
113
|
};
|
|
86
114
|
}
|
|
87
|
-
/**
|
|
115
|
+
/** Parse ONE Codex rollout's tool/skill/subagent invocations. */
|
|
116
|
+
export function parseCodexInvocations(content, sinceMs) {
|
|
117
|
+
const counts = new Map();
|
|
118
|
+
const mcpTools = new Set();
|
|
119
|
+
const skills = new Set();
|
|
120
|
+
const subagents = new Set();
|
|
121
|
+
const commands = new Set();
|
|
122
|
+
const fileReads = new Map();
|
|
123
|
+
let assistantTurns = 0;
|
|
124
|
+
let sessionId;
|
|
125
|
+
let lastActivityAt;
|
|
126
|
+
let compactionEvents = 0;
|
|
127
|
+
for (const line of content.split("\n")) {
|
|
128
|
+
if (!line.trim())
|
|
129
|
+
continue;
|
|
130
|
+
let entry;
|
|
131
|
+
try {
|
|
132
|
+
entry = JSON.parse(line);
|
|
133
|
+
}
|
|
134
|
+
catch {
|
|
135
|
+
continue;
|
|
136
|
+
}
|
|
137
|
+
if (!isRecord(entry))
|
|
138
|
+
continue;
|
|
139
|
+
const timestampValue = stringOf(entry.timestamp);
|
|
140
|
+
if (timestampValue && (!lastActivityAt || timestampValue > lastActivityAt)) {
|
|
141
|
+
lastActivityAt = timestampValue;
|
|
142
|
+
}
|
|
143
|
+
if (typeof sinceMs === "number") {
|
|
144
|
+
const timestamp = Date.parse(stringOf(entry.timestamp) ?? "");
|
|
145
|
+
if (Number.isFinite(timestamp) && timestamp < sinceMs)
|
|
146
|
+
continue;
|
|
147
|
+
}
|
|
148
|
+
const payload = isRecord(entry.payload) ? entry.payload : undefined;
|
|
149
|
+
if (!payload)
|
|
150
|
+
continue;
|
|
151
|
+
if (entry.type === "session_meta") {
|
|
152
|
+
sessionId = stringOf(payload.id) ?? sessionId;
|
|
153
|
+
}
|
|
154
|
+
// Codex writes both a top-level `compacted` entry and a separate
|
|
155
|
+
// `context_compacted` event for one compaction. Count only the former.
|
|
156
|
+
if (entry.type === "compacted")
|
|
157
|
+
compactionEvents += 1;
|
|
158
|
+
// Codex emits one turn_context per model turn.
|
|
159
|
+
if (entry.type === "turn_context") {
|
|
160
|
+
assistantTurns += 1;
|
|
161
|
+
continue;
|
|
162
|
+
}
|
|
163
|
+
if (payload.type === "message" && payload.role === "user") {
|
|
164
|
+
for (const text of codexTextValues(payload.content)) {
|
|
165
|
+
const command = /^\s*\/([A-Za-z0-9:_-]+)/.exec(text)?.[1];
|
|
166
|
+
if (command)
|
|
167
|
+
commands.add(command);
|
|
168
|
+
}
|
|
169
|
+
continue;
|
|
170
|
+
}
|
|
171
|
+
if (payload.type !== "function_call" && payload.type !== "custom_tool_call") {
|
|
172
|
+
continue;
|
|
173
|
+
}
|
|
174
|
+
const name = stringOf(payload.name);
|
|
175
|
+
if (!name)
|
|
176
|
+
continue;
|
|
177
|
+
counts.set(name, (counts.get(name) ?? 0) + 1);
|
|
178
|
+
const input = codexToolInput(payload);
|
|
179
|
+
if (name.startsWith("mcp__")) {
|
|
180
|
+
mcpTools.add(name);
|
|
181
|
+
}
|
|
182
|
+
else if (name === "Skill") {
|
|
183
|
+
const skill = input && stringOf(input.skill);
|
|
184
|
+
if (skill)
|
|
185
|
+
skills.add(skill);
|
|
186
|
+
}
|
|
187
|
+
else if (name === "Task" || name === "Agent" || name === "spawn_agent") {
|
|
188
|
+
const subagent = input && (stringOf(input.subagent_type) ??
|
|
189
|
+
stringOf(input.task_name) ??
|
|
190
|
+
stringOf(input.agent));
|
|
191
|
+
if (subagent)
|
|
192
|
+
subagents.add(subagent);
|
|
193
|
+
}
|
|
194
|
+
const file = input && explicitReadFile(name, input);
|
|
195
|
+
if (file)
|
|
196
|
+
fileReads.set(file, (fileReads.get(file) ?? 0) + 1);
|
|
197
|
+
}
|
|
198
|
+
return {
|
|
199
|
+
invocations: [...counts.entries()]
|
|
200
|
+
.map(([name, count]) => ({ name, count }))
|
|
201
|
+
.sort((left, right) => right.count - left.count || left.name.localeCompare(right.name)),
|
|
202
|
+
invokedMcpTools: [...mcpTools].sort(),
|
|
203
|
+
invokedSkills: [...skills].sort(),
|
|
204
|
+
invokedSubagents: [...subagents].sort(),
|
|
205
|
+
invokedCommands: [...commands].sort(),
|
|
206
|
+
assistantTurns,
|
|
207
|
+
contextSignal: buildSessionContextSignal({
|
|
208
|
+
agent: "codex",
|
|
209
|
+
sessionId,
|
|
210
|
+
lastActivityAt,
|
|
211
|
+
compactionEvents,
|
|
212
|
+
fileReads,
|
|
213
|
+
isSubagent: false
|
|
214
|
+
})
|
|
215
|
+
};
|
|
216
|
+
}
|
|
217
|
+
/** Scan this machine's Claude Code + Codex transcripts and aggregate invocations. */
|
|
88
218
|
export async function loadToolInvocations(options = {}) {
|
|
89
219
|
const claudeDir = options.claudeProjectsDir ?? join(homedir(), ".claude", "projects");
|
|
220
|
+
const codexDir = options.codexSessionsDir ?? join(homedir(), ".codex", "sessions");
|
|
90
221
|
const since = options.sinceIso ? Date.parse(options.sinceIso) : undefined;
|
|
91
222
|
const sinceMs = typeof since === "number" && Number.isFinite(since) ? since : undefined;
|
|
92
223
|
const counts = new Map();
|
|
@@ -95,14 +226,42 @@ export async function loadToolInvocations(options = {}) {
|
|
|
95
226
|
const subagents = new Set();
|
|
96
227
|
const commands = new Set();
|
|
97
228
|
const sessionTurnCounts = [];
|
|
229
|
+
const sessionSignals = [];
|
|
98
230
|
let sessions = 0;
|
|
231
|
+
let claudeCodeSessions = 0;
|
|
232
|
+
let codexSessions = 0;
|
|
99
233
|
for (const file of await listJsonlFiles(claudeDir)) {
|
|
100
234
|
const content = await readFile(file, "utf8").catch(() => "");
|
|
101
235
|
if (!content)
|
|
102
236
|
continue;
|
|
103
237
|
sessions += 1;
|
|
238
|
+
claudeCodeSessions += 1;
|
|
104
239
|
const parsed = parseClaudeCodeInvocations(content, sinceMs);
|
|
105
240
|
sessionTurnCounts.push(parsed.assistantTurns);
|
|
241
|
+
sessionSignals.push(parsed.contextSignal);
|
|
242
|
+
for (const { name, count } of parsed.invocations) {
|
|
243
|
+
counts.set(name, (counts.get(name) ?? 0) + count);
|
|
244
|
+
}
|
|
245
|
+
for (const t of parsed.invokedMcpTools)
|
|
246
|
+
mcpTools.add(t);
|
|
247
|
+
for (const s of parsed.invokedSkills)
|
|
248
|
+
skills.add(s);
|
|
249
|
+
for (const s of parsed.invokedSubagents)
|
|
250
|
+
subagents.add(s);
|
|
251
|
+
for (const c of parsed.invokedCommands)
|
|
252
|
+
commands.add(c);
|
|
253
|
+
}
|
|
254
|
+
for (const file of await listJsonlFiles(codexDir)) {
|
|
255
|
+
if (!file.split(/[\\/]/).pop()?.startsWith("rollout-"))
|
|
256
|
+
continue;
|
|
257
|
+
const content = await readFile(file, "utf8").catch(() => "");
|
|
258
|
+
if (!content)
|
|
259
|
+
continue;
|
|
260
|
+
sessions += 1;
|
|
261
|
+
codexSessions += 1;
|
|
262
|
+
const parsed = parseCodexInvocations(content, sinceMs);
|
|
263
|
+
sessionTurnCounts.push(parsed.assistantTurns);
|
|
264
|
+
sessionSignals.push(parsed.contextSignal);
|
|
106
265
|
for (const { name, count } of parsed.invocations) {
|
|
107
266
|
counts.set(name, (counts.get(name) ?? 0) + count);
|
|
108
267
|
}
|
|
@@ -126,7 +285,12 @@ export async function loadToolInvocations(options = {}) {
|
|
|
126
285
|
invokedCommands: [...commands].sort(),
|
|
127
286
|
sessions,
|
|
128
287
|
totalAssistantTurns: sessionTurnCounts.reduce((sum, n) => sum + n, 0),
|
|
129
|
-
sessionTurnCounts
|
|
288
|
+
sessionTurnCounts,
|
|
289
|
+
sourceSessions: {
|
|
290
|
+
claudeCode: claudeCodeSessions,
|
|
291
|
+
codex: codexSessions
|
|
292
|
+
},
|
|
293
|
+
sessionSignals
|
|
130
294
|
};
|
|
131
295
|
}
|
|
132
296
|
/** Extract "/foo" slash-command names from a user entry's content. */
|
|
@@ -180,6 +344,82 @@ async function listJsonlFiles(root) {
|
|
|
180
344
|
function stringOf(value) {
|
|
181
345
|
return typeof value === "string" && value.length > 0 ? value : undefined;
|
|
182
346
|
}
|
|
347
|
+
function codexToolInput(payload) {
|
|
348
|
+
const direct = isRecord(payload.input) ? payload.input : undefined;
|
|
349
|
+
if (direct)
|
|
350
|
+
return direct;
|
|
351
|
+
for (const value of [payload.arguments, payload.input]) {
|
|
352
|
+
if (typeof value !== "string")
|
|
353
|
+
continue;
|
|
354
|
+
try {
|
|
355
|
+
const parsed = JSON.parse(value);
|
|
356
|
+
if (isRecord(parsed))
|
|
357
|
+
return parsed;
|
|
358
|
+
}
|
|
359
|
+
catch {
|
|
360
|
+
// Custom tool payloads are often free-form patches, not JSON.
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
return undefined;
|
|
364
|
+
}
|
|
365
|
+
function codexTextValues(value) {
|
|
366
|
+
if (typeof value === "string")
|
|
367
|
+
return [value];
|
|
368
|
+
if (!Array.isArray(value))
|
|
369
|
+
return [];
|
|
370
|
+
const out = [];
|
|
371
|
+
for (const entry of value) {
|
|
372
|
+
if (typeof entry === "string")
|
|
373
|
+
out.push(entry);
|
|
374
|
+
else if (isRecord(entry)) {
|
|
375
|
+
const text = stringOf(entry.text);
|
|
376
|
+
if (text)
|
|
377
|
+
out.push(text);
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
return out;
|
|
381
|
+
}
|
|
382
|
+
function isClaudeCompactionEntry(entry) {
|
|
383
|
+
if (entry.type !== "system")
|
|
384
|
+
return false;
|
|
385
|
+
const subtype = stringOf(entry.subtype)?.toLowerCase();
|
|
386
|
+
return subtype === "compact_boundary" ||
|
|
387
|
+
subtype === "compacted" ||
|
|
388
|
+
subtype === "context_compacted";
|
|
389
|
+
}
|
|
390
|
+
function explicitReadFile(toolName, input) {
|
|
391
|
+
const normalized = toolName.toLowerCase();
|
|
392
|
+
if (![
|
|
393
|
+
"read",
|
|
394
|
+
"read_file",
|
|
395
|
+
"readfile",
|
|
396
|
+
"view_image"
|
|
397
|
+
].includes(normalized))
|
|
398
|
+
return undefined;
|
|
399
|
+
const raw = stringOf(input.file_path) ??
|
|
400
|
+
stringOf(input.path) ??
|
|
401
|
+
stringOf(input.file);
|
|
402
|
+
if (!raw)
|
|
403
|
+
return undefined;
|
|
404
|
+
const name = basename(raw);
|
|
405
|
+
return name && name !== "." && name !== "/" ? name : undefined;
|
|
406
|
+
}
|
|
407
|
+
function buildSessionContextSignal(input) {
|
|
408
|
+
const fileReads = [...input.fileReads.entries()]
|
|
409
|
+
.map(([name, count]) => ({ name, count }))
|
|
410
|
+
.sort((left, right) => right.count - left.count || left.name.localeCompare(right.name));
|
|
411
|
+
return {
|
|
412
|
+
agent: input.agent,
|
|
413
|
+
...(input.sessionId ? { sessionId: input.sessionId } : {}),
|
|
414
|
+
...(input.lastActivityAt ? { lastActivityAt: input.lastActivityAt } : {}),
|
|
415
|
+
compactionEvents: input.compactionEvents,
|
|
416
|
+
fileReads,
|
|
417
|
+
repeatedFileReads: fileReads.filter((file) => file.count > 1),
|
|
418
|
+
isSubagent: input.isSubagent,
|
|
419
|
+
...(input.parentSessionId ? { parentSessionId: input.parentSessionId } : {}),
|
|
420
|
+
readCoverage: "explicit_read_tools_only"
|
|
421
|
+
};
|
|
422
|
+
}
|
|
183
423
|
function isRecord(value) {
|
|
184
424
|
return typeof value === "object" && value !== null;
|
|
185
425
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@agent-finops/core",
|
|
3
|
-
"version": "0.5.
|
|
3
|
+
"version": "0.5.7",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"main": "./dist/index.js",
|
|
6
6
|
"types": "./dist/index.d.ts",
|
|
@@ -20,12 +20,25 @@
|
|
|
20
20
|
"!dist/**/*.tsbuildinfo"
|
|
21
21
|
],
|
|
22
22
|
"license": "MIT",
|
|
23
|
-
"description": "
|
|
23
|
+
"description": "Canonical aibill engine for local agent activity, billing semantics, attribution, provenance, runway, and Context Health.",
|
|
24
|
+
"keywords": [
|
|
25
|
+
"ai",
|
|
26
|
+
"finops",
|
|
27
|
+
"agent-economics",
|
|
28
|
+
"claude-code",
|
|
29
|
+
"codex",
|
|
30
|
+
"billing",
|
|
31
|
+
"provenance"
|
|
32
|
+
],
|
|
24
33
|
"repository": {
|
|
25
34
|
"type": "git",
|
|
26
35
|
"url": "git+https://github.com/futurastudio/ai-spend-agent.git",
|
|
27
36
|
"directory": "packages/core"
|
|
28
37
|
},
|
|
38
|
+
"homepage": "https://github.com/futurastudio/ai-spend-agent#readme",
|
|
39
|
+
"bugs": {
|
|
40
|
+
"url": "https://github.com/futurastudio/ai-spend-agent/issues"
|
|
41
|
+
},
|
|
29
42
|
"engines": {
|
|
30
43
|
"node": ">=22"
|
|
31
44
|
}
|