@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/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
  }>;
@@ -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 verified spend until account/API/export source is connected.";
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 tool invocations. */
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
@@ -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
- /** Scan this machine's Claude Code transcripts and aggregate tool invocations. */
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.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": "Core engine for ai-spend-agent: provider billing connectors, local agent-log ingestion, attribution, cut list, plan math.",
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
  }