@agent-finops/core 0.7.1 → 0.7.3
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/activitySnapshot.d.ts +11 -28
- package/dist/activitySnapshot.js +19 -10
- package/dist/contextHealth.js +3 -1
- package/dist/glance.js +16 -16
- package/dist/index.d.ts +4 -1
- package/dist/index.js +2 -1
- package/dist/localAgentFormats/registry.d.ts +8 -0
- package/dist/localAgentFormats/registry.js +219 -0
- package/dist/localAgentFormats/runtimeRegistry.d.ts +4 -0
- package/dist/localAgentFormats/runtimeRegistry.js +55 -0
- package/dist/localAgentFormats/types.d.ts +84 -0
- package/dist/localAgentFormats/types.js +2 -0
- package/dist/localAgentLogs.d.ts +19 -2
- package/dist/localAgentLogs.js +234 -172
- package/dist/sourceStatus.d.ts +2 -1
- package/dist/sourceStatus.js +7 -13
- package/package.json +1 -1
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import type { LocalAgentCall, LocalAgentLogDiagnostic, LocalAgentSourceScan } from "../localAgentLogs.js";
|
|
2
|
+
import type { ParsedInvocationFile } from "../toolInvocations.js";
|
|
3
|
+
/**
|
|
4
|
+
* Public format identity contract. Add future parser IDs here as part of the
|
|
5
|
+
* registry-owned change so existing exhaustive consumers do not see an
|
|
6
|
+
* unbounded `string` in a patch release.
|
|
7
|
+
*/
|
|
8
|
+
export type LocalAgentFormatId = "claude-code" | "codex";
|
|
9
|
+
export type LocalAgentFormatDescriptor = {
|
|
10
|
+
readonly schemaVersion: 1;
|
|
11
|
+
readonly id: LocalAgentFormatId;
|
|
12
|
+
readonly order: number;
|
|
13
|
+
readonly label: string;
|
|
14
|
+
readonly provider: string;
|
|
15
|
+
readonly defaultHomeRelative: readonly string[];
|
|
16
|
+
readonly legacyDirectoryOption?: "claudeProjectsDir" | "codexSessionsDir";
|
|
17
|
+
readonly discovery: {
|
|
18
|
+
readonly extension?: string;
|
|
19
|
+
readonly basename?: string;
|
|
20
|
+
readonly basenamePrefix?: string;
|
|
21
|
+
};
|
|
22
|
+
readonly confidenceDefaults: {
|
|
23
|
+
readonly validationCoverage: "live_verified" | "fixture_verified" | "untested" | "failed";
|
|
24
|
+
readonly pricedFinancialEvidence: "estimated";
|
|
25
|
+
readonly unpricedFinancialEvidence: "missing";
|
|
26
|
+
readonly sourceConfidence: "estimated";
|
|
27
|
+
};
|
|
28
|
+
readonly sourceRecord: {
|
|
29
|
+
readonly id: "local-agent-logs";
|
|
30
|
+
readonly name: "Local agent session logs";
|
|
31
|
+
readonly observedFrom: string;
|
|
32
|
+
readonly providerCostType: "local_agent_logs";
|
|
33
|
+
readonly usageGranularity: "daily_aggregate";
|
|
34
|
+
readonly operation: string;
|
|
35
|
+
};
|
|
36
|
+
readonly capabilities: {
|
|
37
|
+
readonly activity: boolean;
|
|
38
|
+
readonly contextHealth: boolean;
|
|
39
|
+
readonly financialFastPath: boolean;
|
|
40
|
+
readonly glance: boolean;
|
|
41
|
+
readonly invocationEvidence: boolean;
|
|
42
|
+
readonly planContext: boolean;
|
|
43
|
+
readonly rateLimits: boolean;
|
|
44
|
+
};
|
|
45
|
+
readonly financialRead: "full_jsonl" | "bounded_event_jsonl";
|
|
46
|
+
readonly validationNote: string;
|
|
47
|
+
readonly docs: {
|
|
48
|
+
readonly format: string;
|
|
49
|
+
readonly howRead: readonly string[];
|
|
50
|
+
readonly fieldsRead: readonly string[];
|
|
51
|
+
readonly verified: readonly string[];
|
|
52
|
+
readonly estimated: readonly string[];
|
|
53
|
+
readonly notVerified: readonly string[];
|
|
54
|
+
readonly privacy: readonly string[];
|
|
55
|
+
readonly limitations: readonly string[];
|
|
56
|
+
};
|
|
57
|
+
readonly fixtures: readonly string[];
|
|
58
|
+
};
|
|
59
|
+
export type LocalAgentFormatParseContext = {
|
|
60
|
+
content: string;
|
|
61
|
+
filePath: string;
|
|
62
|
+
sinceMs?: number;
|
|
63
|
+
collectInvocationEvidence: boolean;
|
|
64
|
+
onDiagnostic: (diagnostic: {
|
|
65
|
+
code: "malformed_jsonl" | "unsupported_token_shape";
|
|
66
|
+
count: number;
|
|
67
|
+
}) => void;
|
|
68
|
+
};
|
|
69
|
+
export type LocalAgentFormatParseResult = {
|
|
70
|
+
calls: LocalAgentCall[];
|
|
71
|
+
invocationFile?: ParsedInvocationFile;
|
|
72
|
+
};
|
|
73
|
+
export type LocalAgentFormatFinancialFileContext = {
|
|
74
|
+
filePath: string;
|
|
75
|
+
sinceMs?: number;
|
|
76
|
+
scan: LocalAgentSourceScan;
|
|
77
|
+
diagnostics: LocalAgentLogDiagnostic[];
|
|
78
|
+
};
|
|
79
|
+
export type LocalAgentFormatRuntime = {
|
|
80
|
+
descriptor: LocalAgentFormatDescriptor;
|
|
81
|
+
parseFull: (context: LocalAgentFormatParseContext) => LocalAgentFormatParseResult;
|
|
82
|
+
parseFinancialFile: (context: LocalAgentFormatFinancialFileContext) => Promise<LocalAgentCall[]>;
|
|
83
|
+
};
|
|
84
|
+
//# sourceMappingURL=types.d.ts.map
|
package/dist/localAgentLogs.d.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { type TokenUsage } from "./modelPricing.js";
|
|
2
2
|
import type { UsageRecord } from "./schema.js";
|
|
3
|
-
import {
|
|
3
|
+
import type { ParsedInvocationFile } from "./toolInvocations.js";
|
|
4
|
+
import type { LocalAgentFormatDescriptor, LocalAgentFormatFinancialFileContext, LocalAgentFormatId, LocalAgentFormatRuntime } from "./localAgentFormats/types.js";
|
|
4
5
|
/**
|
|
5
6
|
* Local agent-session log ingestion: turns the transcript files that coding
|
|
6
7
|
* agents already write on this machine into UsageRecords, priced at
|
|
@@ -18,7 +19,7 @@ import { type ParsedInvocationFile } from "./toolInvocations.js";
|
|
|
18
19
|
* total_token_usage (earlier ones are running updates — never summed).
|
|
19
20
|
*/
|
|
20
21
|
export type LocalAgentCall = {
|
|
21
|
-
agent:
|
|
22
|
+
agent: LocalAgentFormatId;
|
|
22
23
|
model: string;
|
|
23
24
|
/** ISO timestamp of this call, or the latest cumulative usage event. */
|
|
24
25
|
timestamp: string;
|
|
@@ -104,6 +105,8 @@ export type LocalAgentLogOptions = {
|
|
|
104
105
|
claudeProjectsDir?: string;
|
|
105
106
|
/** Default: ~/.codex/sessions */
|
|
106
107
|
codexSessionsDir?: string;
|
|
108
|
+
/** Registry-native source-root overrides, keyed by format id. */
|
|
109
|
+
sourceDirectories?: Readonly<Partial<Record<LocalAgentFormatId, string>>>;
|
|
107
110
|
/** Only include calls at/after this ISO timestamp. */
|
|
108
111
|
sinceIso?: string;
|
|
109
112
|
/** Collect privacy-safe Codex invocation summaries during the same JSON pass. */
|
|
@@ -178,6 +181,12 @@ export declare function parseClaudeCodeTranscript(content: string, filePath?: st
|
|
|
178
181
|
export declare function parseCodexRollout(content: string, onEntry?: (entry: Record<string, unknown>) => void, onDiagnostic?: TranscriptParseDiagnosticHandler): LocalAgentCall[];
|
|
179
182
|
/** Scan this machine's agent logs and return aggregated UsageRecords. */
|
|
180
183
|
export declare function loadLocalAgentUsage(options?: LocalAgentLogOptions): Promise<LocalAgentLogResult>;
|
|
184
|
+
/**
|
|
185
|
+
* Registry-driven ingestion engine. Exported from this module for registry
|
|
186
|
+
* contract tests and format modules, but intentionally omitted from the
|
|
187
|
+
* package-root API.
|
|
188
|
+
*/
|
|
189
|
+
export declare function loadLocalAgentUsageWithFormats(registry: readonly LocalAgentFormatRuntime[], options?: LocalAgentLogOptions): Promise<LocalAgentLogResult>;
|
|
181
190
|
/**
|
|
182
191
|
* Stream only the financial evidence required by init/status snapshots.
|
|
183
192
|
*
|
|
@@ -189,8 +198,16 @@ export declare function loadLocalAgentUsage(options?: LocalAgentLogOptions): Pro
|
|
|
189
198
|
* tool-workdir inference remains exclusive to the full qualitative loader.
|
|
190
199
|
*/
|
|
191
200
|
export declare function loadLocalAgentFinancialUsage(options?: LocalAgentFinancialLogOptions): Promise<LocalAgentLogResult>;
|
|
201
|
+
/** Registry-driven financial-only engine; package-root exports stay unchanged. */
|
|
202
|
+
export declare function loadLocalAgentFinancialUsageWithFormats(registry: readonly LocalAgentFormatRuntime[], options?: LocalAgentFinancialLogOptions): Promise<LocalAgentLogResult>;
|
|
203
|
+
/** @internal Runtime hook owned by the Claude Code registry entry. */
|
|
204
|
+
export declare function readClaudeCodeFinancialFileForRegistry(context: LocalAgentFormatFinancialFileContext): Promise<LocalAgentCall[]>;
|
|
205
|
+
/** @internal Runtime hook owned by the Codex registry entry. */
|
|
206
|
+
export declare function readCodexFinancialFileForRegistry(context: LocalAgentFormatFinancialFileContext): Promise<LocalAgentCall[]>;
|
|
192
207
|
/** Aggregate per-call usage into one UsageRecord per day+agent+model+project. */
|
|
193
208
|
export declare function aggregateCalls(calls: LocalAgentCall[]): UsageRecord[];
|
|
209
|
+
/** @internal Registry-aware aggregation used by the extensible ingestion engine. */
|
|
210
|
+
export declare function aggregateCallsForFormats(calls: LocalAgentCall[], descriptors: readonly LocalAgentFormatDescriptor[]): UsageRecord[];
|
|
194
211
|
/**
|
|
195
212
|
* Remove known and assignment-shaped credentials from metadata before it can
|
|
196
213
|
* become a topic, title, Glance field, MCP result, or copy-ready handoff.
|
package/dist/localAgentLogs.js
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
import { createReadStream } from "node:fs";
|
|
2
2
|
import { lstat, open, readdir, readFile, stat } from "node:fs/promises";
|
|
3
|
-
import { basename, isAbsolute, join, resolve, sep } from "node:path";
|
|
3
|
+
import { basename, isAbsolute, join, relative, resolve, sep } from "node:path";
|
|
4
4
|
import { homedir } from "node:os";
|
|
5
5
|
import { createInterface } from "node:readline";
|
|
6
6
|
import { estimateTokenCostUsd } from "./modelPricing.js";
|
|
7
7
|
import { redactSecrets } from "./discovery.js";
|
|
8
|
-
import {
|
|
8
|
+
import { localAgentFormatDescriptors, localAgentFormatLabel, matchesLocalAgentFormatFile, validateLocalAgentFormatDescriptors } from "./localAgentFormats/registry.js";
|
|
9
9
|
/**
|
|
10
10
|
* Resolve the repository root most recently observed in transcript metadata.
|
|
11
11
|
*
|
|
@@ -551,9 +551,16 @@ function parseCodexRateLimitWindow(value) {
|
|
|
551
551
|
}
|
|
552
552
|
/** Scan this machine's agent logs and return aggregated UsageRecords. */
|
|
553
553
|
export async function loadLocalAgentUsage(options = {}) {
|
|
554
|
+
return loadLocalAgentUsageWithFormats(await localAgentFormatRuntimes(), options);
|
|
555
|
+
}
|
|
556
|
+
/**
|
|
557
|
+
* Registry-driven ingestion engine. Exported from this module for registry
|
|
558
|
+
* contract tests and format modules, but intentionally omitted from the
|
|
559
|
+
* package-root API.
|
|
560
|
+
*/
|
|
561
|
+
export async function loadLocalAgentUsageWithFormats(registry, options = {}) {
|
|
562
|
+
validateLocalAgentFormatDescriptors(registry.map((entry) => entry.descriptor));
|
|
554
563
|
const home = homedir();
|
|
555
|
-
const claudeDir = options.claudeProjectsDir ?? join(home, ".claude", "projects");
|
|
556
|
-
const codexDir = options.codexSessionsDir ?? join(home, ".codex", "sessions");
|
|
557
564
|
const calls = [];
|
|
558
565
|
const codexInvocationFiles = options.collectCodexInvocationEvidence
|
|
559
566
|
? []
|
|
@@ -562,59 +569,52 @@ export async function loadLocalAgentUsage(options = {}) {
|
|
|
562
569
|
const since = options.sinceIso ? Date.parse(options.sinceIso) : undefined;
|
|
563
570
|
const sinceMs = typeof since === "number" && Number.isFinite(since) ? since : undefined;
|
|
564
571
|
const diagnostics = [];
|
|
565
|
-
const sourceScans = [
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
content
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
572
|
+
const sourceScans = [];
|
|
573
|
+
for (const runtime of registry) {
|
|
574
|
+
const { descriptor } = runtime;
|
|
575
|
+
const scan = emptySourceScan(descriptor.id);
|
|
576
|
+
sourceScans.push(scan);
|
|
577
|
+
const root = localAgentFormatRoot(descriptor, options, home);
|
|
578
|
+
for (const file of await listFormatCandidateFiles(root, descriptor, scan, diagnostics)) {
|
|
579
|
+
if (!matchesLocalAgentFormatFile(descriptor, file))
|
|
580
|
+
continue;
|
|
581
|
+
let content;
|
|
582
|
+
try {
|
|
583
|
+
content = await readFile(file, "utf8");
|
|
584
|
+
}
|
|
585
|
+
catch (error) {
|
|
586
|
+
recordUnreadableFile(descriptor.id, scan, diagnostics, error);
|
|
587
|
+
continue;
|
|
588
|
+
}
|
|
589
|
+
if (!content)
|
|
590
|
+
continue;
|
|
591
|
+
filesParsed += 1;
|
|
592
|
+
scan.filesParsed += 1;
|
|
593
|
+
const collectInvocationEvidence = Boolean(codexInvocationFiles) &&
|
|
594
|
+
descriptor.id === "codex" && descriptor.capabilities.invocationEvidence;
|
|
595
|
+
const parsed = runtime.parseFull({
|
|
596
|
+
content,
|
|
597
|
+
filePath: file,
|
|
598
|
+
sinceMs,
|
|
599
|
+
collectInvocationEvidence,
|
|
600
|
+
onDiagnostic: (diagnostic) => {
|
|
601
|
+
recordParseDiagnostic(descriptor.id, scan, diagnostics, diagnostic);
|
|
602
|
+
}
|
|
603
|
+
});
|
|
604
|
+
assertFormatCallOwnership(descriptor, parsed.calls);
|
|
605
|
+
assertInvocationOwnership(descriptor, parsed.invocationFile, collectInvocationEvidence);
|
|
606
|
+
calls.push(...parsed.calls);
|
|
607
|
+
if (codexInvocationFiles && collectInvocationEvidence && parsed.invocationFile) {
|
|
608
|
+
codexInvocationFiles.push(parsed.invocationFile);
|
|
609
|
+
}
|
|
598
610
|
}
|
|
599
|
-
if (!content)
|
|
600
|
-
continue;
|
|
601
|
-
filesParsed += 1;
|
|
602
|
-
codexScan.filesParsed += 1;
|
|
603
|
-
const collector = codexInvocationFiles
|
|
604
|
-
? createCodexInvocationCollector(sinceMs)
|
|
605
|
-
: undefined;
|
|
606
|
-
calls.push(...parseCodexRollout(content, collector?.consume, (diagnostic) => {
|
|
607
|
-
recordParseDiagnostic("codex", codexScan, diagnostics, diagnostic);
|
|
608
|
-
}));
|
|
609
|
-
if (collector)
|
|
610
|
-
codexInvocationFiles.push(collector.finish());
|
|
611
611
|
}
|
|
612
612
|
const normalizedCalls = dedupeCumulativeSessionCalls(calls);
|
|
613
613
|
const filtered = typeof sinceMs === "number"
|
|
614
614
|
? normalizedCalls.filter((call) => Date.parse(call.timestamp) >= sinceMs)
|
|
615
615
|
: normalizedCalls;
|
|
616
616
|
return {
|
|
617
|
-
records:
|
|
617
|
+
records: aggregateCallsForFormats(filtered, registry.map((entry) => entry.descriptor)),
|
|
618
618
|
calls: filtered,
|
|
619
619
|
filesParsed,
|
|
620
620
|
agentsDetected: [...new Set(filtered.map((call) => call.agent))],
|
|
@@ -634,129 +634,183 @@ export async function loadLocalAgentUsage(options = {}) {
|
|
|
634
634
|
* tool-workdir inference remains exclusive to the full qualitative loader.
|
|
635
635
|
*/
|
|
636
636
|
export async function loadLocalAgentFinancialUsage(options = {}) {
|
|
637
|
+
return loadLocalAgentFinancialUsageWithFormats(await localAgentFormatRuntimes(), options);
|
|
638
|
+
}
|
|
639
|
+
/** Registry-driven financial-only engine; package-root exports stay unchanged. */
|
|
640
|
+
export async function loadLocalAgentFinancialUsageWithFormats(registry, options = {}) {
|
|
641
|
+
validateLocalAgentFormatDescriptors(registry.map((entry) => entry.descriptor));
|
|
637
642
|
const home = homedir();
|
|
638
|
-
const claudeDir = options.claudeProjectsDir ?? join(home, ".claude", "projects");
|
|
639
|
-
const codexDir = options.codexSessionsDir ?? join(home, ".codex", "sessions");
|
|
640
|
-
const claudeCalls = [];
|
|
641
|
-
const codexCalls = [];
|
|
642
643
|
const since = options.sinceIso ? Date.parse(options.sinceIso) : undefined;
|
|
643
644
|
const sinceMs = typeof since === "number" && Number.isFinite(since) ? since : undefined;
|
|
644
|
-
const
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
{
|
|
653
|
-
...emptySourceScan("codex"),
|
|
654
|
-
filesSkippedBeforeWindow: 0,
|
|
655
|
-
filesReadFinancially: 0,
|
|
656
|
-
bytesSkippedAsNonFinancialHistory: 0,
|
|
657
|
-
nonFinancialLinesPrefiltered: 0,
|
|
658
|
-
nonFinancialBytesPrefiltered: 0,
|
|
659
|
-
jsonlValidationCoverage: "complete"
|
|
660
|
-
}
|
|
661
|
-
];
|
|
662
|
-
const claudeScan = sourceScans[0];
|
|
663
|
-
const codexScan = sourceScans[1];
|
|
664
|
-
const scanClaude = async () => {
|
|
665
|
-
for (const file of await listJsonlFiles(claudeDir, claudeScan, claudeDiagnostics)) {
|
|
666
|
-
if (!await shouldStreamFile(file, sinceMs, "claude-code", claudeScan, claudeDiagnostics)) {
|
|
645
|
+
const scanned = await Promise.all(registry.map(async (runtime) => {
|
|
646
|
+
const { descriptor } = runtime;
|
|
647
|
+
const diagnostics = [];
|
|
648
|
+
const scan = financialSourceScan(descriptor);
|
|
649
|
+
const calls = [];
|
|
650
|
+
const root = localAgentFormatRoot(descriptor, options, home);
|
|
651
|
+
for (const file of await listFormatCandidateFiles(root, descriptor, scan, diagnostics)) {
|
|
652
|
+
if (!matchesLocalAgentFormatFile(descriptor, file))
|
|
667
653
|
continue;
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
try {
|
|
674
|
-
streamed = await streamJsonlRecords(file, (entry) => {
|
|
675
|
-
const call = parseClaudeFinancialEntry(entry, file, sinceMs, seen, (diagnostic) => fileDiagnostics.push(diagnostic));
|
|
676
|
-
if (call)
|
|
677
|
-
fileCalls.push(call);
|
|
678
|
-
});
|
|
679
|
-
}
|
|
680
|
-
catch (error) {
|
|
681
|
-
recordUnreadableFile("claude-code", claudeScan, claudeDiagnostics, error);
|
|
682
|
-
continue;
|
|
683
|
-
}
|
|
684
|
-
if (!streamed.hadContent)
|
|
685
|
-
continue;
|
|
686
|
-
claudeScan.filesParsed += 1;
|
|
687
|
-
claudeCalls.push(...fileCalls);
|
|
688
|
-
for (const diagnostic of fileDiagnostics) {
|
|
689
|
-
recordParseDiagnostic("claude-code", claudeScan, claudeDiagnostics, diagnostic);
|
|
690
|
-
}
|
|
691
|
-
if (streamed.malformedLines > 0) {
|
|
692
|
-
recordParseDiagnostic("claude-code", claudeScan, claudeDiagnostics, {
|
|
693
|
-
code: "malformed_jsonl",
|
|
694
|
-
count: streamed.malformedLines
|
|
695
|
-
});
|
|
696
|
-
}
|
|
697
|
-
}
|
|
698
|
-
};
|
|
699
|
-
const scanCodex = async () => {
|
|
700
|
-
for (const file of await listJsonlFiles(codexDir, codexScan, codexDiagnostics)) {
|
|
701
|
-
if (!basename(file).startsWith("rollout-"))
|
|
702
|
-
continue;
|
|
703
|
-
if (!await shouldStreamFile(file, sinceMs, "codex", codexScan, codexDiagnostics)) {
|
|
704
|
-
continue;
|
|
705
|
-
}
|
|
706
|
-
let financialFile;
|
|
707
|
-
try {
|
|
708
|
-
financialFile = await readCodexFinancialFile(file);
|
|
709
|
-
}
|
|
710
|
-
catch (error) {
|
|
711
|
-
recordUnreadableFile("codex", codexScan, codexDiagnostics, error);
|
|
712
|
-
continue;
|
|
713
|
-
}
|
|
714
|
-
if (!financialFile.hadContent)
|
|
715
|
-
continue;
|
|
716
|
-
codexScan.filesParsed += 1;
|
|
717
|
-
codexScan.filesReadFinancially = (codexScan.filesReadFinancially ?? 0) + 1;
|
|
718
|
-
codexScan.bytesSkippedAsNonFinancialHistory =
|
|
719
|
-
(codexScan.bytesSkippedAsNonFinancialHistory ?? 0) + financialFile.bytesSkipped;
|
|
720
|
-
codexScan.nonFinancialLinesPrefiltered =
|
|
721
|
-
(codexScan.nonFinancialLinesPrefiltered ?? 0) + financialFile.prefilteredLines;
|
|
722
|
-
codexScan.nonFinancialBytesPrefiltered =
|
|
723
|
-
(codexScan.nonFinancialBytesPrefiltered ?? 0) + financialFile.prefilteredBytes;
|
|
724
|
-
if (financialFile.bytesSkipped > 0 || financialFile.prefilteredLines > 0) {
|
|
725
|
-
codexScan.jsonlValidationCoverage = "financial_events_only";
|
|
726
|
-
}
|
|
727
|
-
if (financialFile.malformedLines > 0) {
|
|
728
|
-
recordParseDiagnostic("codex", codexScan, codexDiagnostics, {
|
|
729
|
-
code: "malformed_jsonl",
|
|
730
|
-
count: financialFile.malformedLines
|
|
731
|
-
});
|
|
732
|
-
}
|
|
733
|
-
const state = createCodexFinancialStreamState();
|
|
734
|
-
for (const entry of financialFile.entries) {
|
|
735
|
-
consumeCodexFinancialEntry(state, entry);
|
|
736
|
-
}
|
|
737
|
-
const call = finishCodexFinancialStream(state, (diagnostic) => {
|
|
738
|
-
recordParseDiagnostic("codex", codexScan, codexDiagnostics, diagnostic);
|
|
654
|
+
const parsedCalls = await runtime.parseFinancialFile({
|
|
655
|
+
filePath: file,
|
|
656
|
+
sinceMs,
|
|
657
|
+
scan,
|
|
658
|
+
diagnostics
|
|
739
659
|
});
|
|
740
|
-
|
|
741
|
-
|
|
660
|
+
assertFormatCallOwnership(descriptor, parsedCalls);
|
|
661
|
+
assertFinancialSourceOwnership(descriptor, scan, diagnostics);
|
|
662
|
+
calls.push(...parsedCalls);
|
|
742
663
|
}
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
//
|
|
746
|
-
|
|
747
|
-
const calls =
|
|
664
|
+
return { calls, scan, diagnostics };
|
|
665
|
+
}));
|
|
666
|
+
// Sources scan concurrently, but flatten in registry order to preserve the
|
|
667
|
+
// long-standing Claude-then-Codex output and diagnostic contract.
|
|
668
|
+
const calls = scanned.flatMap((entry) => entry.calls);
|
|
748
669
|
const normalizedCalls = dedupeCumulativeSessionCalls(calls);
|
|
749
670
|
const filtered = typeof sinceMs === "number"
|
|
750
671
|
? normalizedCalls.filter((call) => Date.parse(call.timestamp) >= sinceMs)
|
|
751
672
|
: normalizedCalls;
|
|
752
673
|
return {
|
|
753
|
-
records:
|
|
674
|
+
records: aggregateCallsForFormats(filtered, registry.map((entry) => entry.descriptor)),
|
|
754
675
|
calls: filtered,
|
|
755
|
-
filesParsed:
|
|
676
|
+
filesParsed: scanned.reduce((total, entry) => total + entry.scan.filesParsed, 0),
|
|
756
677
|
agentsDetected: [...new Set(filtered.map((call) => call.agent))],
|
|
757
|
-
sourceScans,
|
|
758
|
-
diagnostics:
|
|
678
|
+
sourceScans: scanned.map((entry) => entry.scan),
|
|
679
|
+
diagnostics: scanned.flatMap((entry) => entry.diagnostics)
|
|
680
|
+
};
|
|
681
|
+
}
|
|
682
|
+
/** @internal Runtime hook owned by the Claude Code registry entry. */
|
|
683
|
+
export async function readClaudeCodeFinancialFileForRegistry(context) {
|
|
684
|
+
const { filePath, sinceMs, scan, diagnostics } = context;
|
|
685
|
+
if (!await shouldStreamFile(filePath, sinceMs, "claude-code", scan, diagnostics)) {
|
|
686
|
+
return [];
|
|
687
|
+
}
|
|
688
|
+
const calls = [];
|
|
689
|
+
const fileDiagnostics = [];
|
|
690
|
+
const seen = new Set();
|
|
691
|
+
let streamed;
|
|
692
|
+
try {
|
|
693
|
+
streamed = await streamJsonlRecords(filePath, (entry) => {
|
|
694
|
+
const call = parseClaudeFinancialEntry(entry, filePath, sinceMs, seen, (diagnostic) => fileDiagnostics.push(diagnostic));
|
|
695
|
+
if (call)
|
|
696
|
+
calls.push(call);
|
|
697
|
+
});
|
|
698
|
+
}
|
|
699
|
+
catch (error) {
|
|
700
|
+
recordUnreadableFile("claude-code", scan, diagnostics, error);
|
|
701
|
+
return [];
|
|
702
|
+
}
|
|
703
|
+
if (!streamed.hadContent)
|
|
704
|
+
return [];
|
|
705
|
+
scan.filesParsed += 1;
|
|
706
|
+
for (const diagnostic of fileDiagnostics) {
|
|
707
|
+
recordParseDiagnostic("claude-code", scan, diagnostics, diagnostic);
|
|
708
|
+
}
|
|
709
|
+
if (streamed.malformedLines > 0) {
|
|
710
|
+
recordParseDiagnostic("claude-code", scan, diagnostics, {
|
|
711
|
+
code: "malformed_jsonl",
|
|
712
|
+
count: streamed.malformedLines
|
|
713
|
+
});
|
|
714
|
+
}
|
|
715
|
+
return calls;
|
|
716
|
+
}
|
|
717
|
+
/** @internal Runtime hook owned by the Codex registry entry. */
|
|
718
|
+
export async function readCodexFinancialFileForRegistry(context) {
|
|
719
|
+
const { filePath, sinceMs, scan, diagnostics } = context;
|
|
720
|
+
if (!await shouldStreamFile(filePath, sinceMs, "codex", scan, diagnostics)) {
|
|
721
|
+
return [];
|
|
722
|
+
}
|
|
723
|
+
let financialFile;
|
|
724
|
+
try {
|
|
725
|
+
financialFile = await readCodexFinancialFile(filePath);
|
|
726
|
+
}
|
|
727
|
+
catch (error) {
|
|
728
|
+
recordUnreadableFile("codex", scan, diagnostics, error);
|
|
729
|
+
return [];
|
|
730
|
+
}
|
|
731
|
+
if (!financialFile.hadContent)
|
|
732
|
+
return [];
|
|
733
|
+
scan.filesParsed += 1;
|
|
734
|
+
scan.filesReadFinancially = (scan.filesReadFinancially ?? 0) + 1;
|
|
735
|
+
scan.bytesSkippedAsNonFinancialHistory =
|
|
736
|
+
(scan.bytesSkippedAsNonFinancialHistory ?? 0) + financialFile.bytesSkipped;
|
|
737
|
+
scan.nonFinancialLinesPrefiltered =
|
|
738
|
+
(scan.nonFinancialLinesPrefiltered ?? 0) + financialFile.prefilteredLines;
|
|
739
|
+
scan.nonFinancialBytesPrefiltered =
|
|
740
|
+
(scan.nonFinancialBytesPrefiltered ?? 0) + financialFile.prefilteredBytes;
|
|
741
|
+
if (financialFile.bytesSkipped > 0 || financialFile.prefilteredLines > 0) {
|
|
742
|
+
scan.jsonlValidationCoverage = "financial_events_only";
|
|
743
|
+
}
|
|
744
|
+
if (financialFile.malformedLines > 0) {
|
|
745
|
+
recordParseDiagnostic("codex", scan, diagnostics, {
|
|
746
|
+
code: "malformed_jsonl",
|
|
747
|
+
count: financialFile.malformedLines
|
|
748
|
+
});
|
|
749
|
+
}
|
|
750
|
+
const state = createCodexFinancialStreamState();
|
|
751
|
+
for (const entry of financialFile.entries) {
|
|
752
|
+
consumeCodexFinancialEntry(state, entry);
|
|
753
|
+
}
|
|
754
|
+
const call = finishCodexFinancialStream(state, (diagnostic) => {
|
|
755
|
+
recordParseDiagnostic("codex", scan, diagnostics, diagnostic);
|
|
756
|
+
});
|
|
757
|
+
return call ? [call] : [];
|
|
758
|
+
}
|
|
759
|
+
async function localAgentFormatRuntimes() {
|
|
760
|
+
const module = await import("./localAgentFormats/runtimeRegistry.js");
|
|
761
|
+
return module.localAgentFormatRuntimeRegistry;
|
|
762
|
+
}
|
|
763
|
+
function localAgentFormatRoot(descriptor, options, home) {
|
|
764
|
+
const registryOverride = options.sourceDirectories?.[descriptor.id];
|
|
765
|
+
if (registryOverride)
|
|
766
|
+
return registryOverride;
|
|
767
|
+
if (descriptor.legacyDirectoryOption === "claudeProjectsDir" && options.claudeProjectsDir) {
|
|
768
|
+
return options.claudeProjectsDir;
|
|
769
|
+
}
|
|
770
|
+
if (descriptor.legacyDirectoryOption === "codexSessionsDir" && options.codexSessionsDir) {
|
|
771
|
+
return options.codexSessionsDir;
|
|
772
|
+
}
|
|
773
|
+
const canonicalHome = resolve(home);
|
|
774
|
+
const root = resolve(canonicalHome, ...descriptor.defaultHomeRelative);
|
|
775
|
+
const fromHome = relative(canonicalHome, root);
|
|
776
|
+
if (fromHome === ".." || fromHome.startsWith(`..${sep}`) || isAbsolute(fromHome)) {
|
|
777
|
+
throw new Error(`Local-agent format ${descriptor.id} resolved outside the home boundary.`);
|
|
778
|
+
}
|
|
779
|
+
return root;
|
|
780
|
+
}
|
|
781
|
+
function assertFormatCallOwnership(descriptor, calls) {
|
|
782
|
+
if (calls.some((call) => call.agent !== descriptor.id)) {
|
|
783
|
+
throw new Error(`Local-agent format ${descriptor.id} emitted a call for a different source.`);
|
|
784
|
+
}
|
|
785
|
+
}
|
|
786
|
+
function assertFinancialSourceOwnership(descriptor, scan, diagnostics) {
|
|
787
|
+
if (scan.agent !== descriptor.id || diagnostics.some((entry) => entry.agent !== descriptor.id)) {
|
|
788
|
+
throw new Error(`Local-agent format ${descriptor.id} emitted financial metadata for a different source.`);
|
|
789
|
+
}
|
|
790
|
+
}
|
|
791
|
+
function assertInvocationOwnership(descriptor, invocationFile, collectionAllowed) {
|
|
792
|
+
if (!invocationFile)
|
|
793
|
+
return;
|
|
794
|
+
if (!collectionAllowed || descriptor.id !== "codex" ||
|
|
795
|
+
invocationFile.contextSignal.agent !== descriptor.id) {
|
|
796
|
+
throw new Error(`Local-agent format ${descriptor.id} emitted invocation evidence for a different source.`);
|
|
797
|
+
}
|
|
798
|
+
}
|
|
799
|
+
function financialSourceScan(descriptor) {
|
|
800
|
+
const scan = {
|
|
801
|
+
...emptySourceScan(descriptor.id),
|
|
802
|
+
filesSkippedBeforeWindow: 0
|
|
759
803
|
};
|
|
804
|
+
if (descriptor.financialRead === "bounded_event_jsonl") {
|
|
805
|
+
scan.filesReadFinancially = 0;
|
|
806
|
+
scan.bytesSkippedAsNonFinancialHistory = 0;
|
|
807
|
+
scan.nonFinancialLinesPrefiltered = 0;
|
|
808
|
+
scan.nonFinancialBytesPrefiltered = 0;
|
|
809
|
+
}
|
|
810
|
+
// Keep this after format-specific metrics: persisted/debug JSON has used
|
|
811
|
+
// this exact insertion order since the optimized financial reader shipped.
|
|
812
|
+
scan.jsonlValidationCoverage = "complete";
|
|
813
|
+
return scan;
|
|
760
814
|
}
|
|
761
815
|
async function streamJsonlRecords(file, onRecord) {
|
|
762
816
|
const input = createReadStream(file, { encoding: "utf8" });
|
|
@@ -1267,6 +1321,11 @@ function finishCodexFinancialStream(state, onDiagnostic) {
|
|
|
1267
1321
|
}
|
|
1268
1322
|
/** Aggregate per-call usage into one UsageRecord per day+agent+model+project. */
|
|
1269
1323
|
export function aggregateCalls(calls) {
|
|
1324
|
+
return aggregateCallsForFormats(calls, localAgentFormatDescriptors);
|
|
1325
|
+
}
|
|
1326
|
+
/** @internal Registry-aware aggregation used by the extensible ingestion engine. */
|
|
1327
|
+
export function aggregateCallsForFormats(calls, descriptors) {
|
|
1328
|
+
const formats = new Map(descriptors.map((descriptor) => [descriptor.id, descriptor]));
|
|
1270
1329
|
const groups = new Map();
|
|
1271
1330
|
for (const call of dedupeCumulativeSessionCalls(calls)) {
|
|
1272
1331
|
const day = call.timestamp.slice(0, 10);
|
|
@@ -1276,6 +1335,7 @@ export function aggregateCalls(calls) {
|
|
|
1276
1335
|
const records = [];
|
|
1277
1336
|
for (const [key, groupCalls] of groups) {
|
|
1278
1337
|
const [day, agent, model, project] = key.split("|");
|
|
1338
|
+
const format = formats.get(agent);
|
|
1279
1339
|
const usage = {
|
|
1280
1340
|
inputTokens: sum(groupCalls, (c) => c.usage.inputTokens),
|
|
1281
1341
|
outputTokens: sum(groupCalls, (c) => c.usage.outputTokens),
|
|
@@ -1284,37 +1344,39 @@ export function aggregateCalls(calls) {
|
|
|
1284
1344
|
cacheWrite1hTokens: sum(groupCalls, (c) => c.usage.cacheWrite1hTokens ?? 0)
|
|
1285
1345
|
};
|
|
1286
1346
|
const usageSupported = groupCalls.every((call) => call.usageSupport !== "unsupported_token_shape");
|
|
1287
|
-
const amountUsd = usageSupported ? estimateTokenCostUsd(model, usage) : undefined;
|
|
1347
|
+
const amountUsd = usageSupported && format ? estimateTokenCostUsd(model, usage) : undefined;
|
|
1288
1348
|
const priced = usageSupported && typeof amountUsd === "number";
|
|
1289
1349
|
records.push({
|
|
1290
1350
|
id: slug(["local", agent, day, model, project].join("-")),
|
|
1291
1351
|
timestamp: new Date(`${day}T00:00:00Z`).toISOString(),
|
|
1292
1352
|
source: {
|
|
1293
|
-
id: "local-agent-logs",
|
|
1294
|
-
name: "Local agent session logs",
|
|
1295
|
-
provider:
|
|
1296
|
-
confidence: "estimated",
|
|
1297
|
-
observedFrom:
|
|
1353
|
+
id: format?.sourceRecord.id ?? "local-agent-logs",
|
|
1354
|
+
name: format?.sourceRecord.name ?? "Local agent session logs",
|
|
1355
|
+
provider: format?.provider ?? "unknown",
|
|
1356
|
+
confidence: format?.confidenceDefaults.sourceConfidence ?? "estimated",
|
|
1357
|
+
observedFrom: format?.sourceRecord.observedFrom ?? "unregistered local transcript (this machine)"
|
|
1298
1358
|
},
|
|
1299
1359
|
model,
|
|
1300
1360
|
inputTokens: usage.inputTokens + (usage.cacheReadTokens ?? 0) + (usage.cacheWrite5mTokens ?? 0) + (usage.cacheWrite1hTokens ?? 0),
|
|
1301
1361
|
outputTokens: usage.outputTokens,
|
|
1302
1362
|
amountUsd: priced ? amountUsd : null,
|
|
1303
|
-
costConfidence: priced
|
|
1363
|
+
costConfidence: priced
|
|
1364
|
+
? format?.confidenceDefaults.pricedFinancialEvidence ?? "estimated"
|
|
1365
|
+
: format?.confidenceDefaults.unpricedFinancialEvidence ?? "missing",
|
|
1304
1366
|
// `(home)` is an attribution fallback, not a real project. Keep it on
|
|
1305
1367
|
// LocalAgentCall for Glance/session context, but do not promote it to a
|
|
1306
1368
|
// high-confidence project id in receipts or the attribution engine.
|
|
1307
1369
|
projectId: project === "unattributed" || project === "(home)" ? undefined : project,
|
|
1308
1370
|
agentId: agent,
|
|
1309
|
-
providerCostType: "local_agent_logs",
|
|
1310
|
-
usageGranularity: "daily_aggregate",
|
|
1371
|
+
providerCostType: format?.sourceRecord.providerCostType ?? "local_agent_logs",
|
|
1372
|
+
usageGranularity: format?.sourceRecord.usageGranularity ?? "daily_aggregate",
|
|
1311
1373
|
quantity: groupCalls.length,
|
|
1312
|
-
operation: `${agent} sessions`
|
|
1374
|
+
operation: format?.sourceRecord.operation ?? `${agent} sessions`
|
|
1313
1375
|
});
|
|
1314
1376
|
}
|
|
1315
1377
|
return records.sort((left, right) => left.id.localeCompare(right.id));
|
|
1316
1378
|
}
|
|
1317
|
-
async function
|
|
1379
|
+
async function listFormatCandidateFiles(root, descriptor, scan, diagnostics) {
|
|
1318
1380
|
let rootStat;
|
|
1319
1381
|
try {
|
|
1320
1382
|
rootStat = await stat(root);
|
|
@@ -1377,7 +1439,7 @@ async function listJsonlFiles(root, scan, diagnostics) {
|
|
|
1377
1439
|
const path = join(dir, entry.name);
|
|
1378
1440
|
if (entry.isDirectory())
|
|
1379
1441
|
queue.push(path);
|
|
1380
|
-
else if (entry.isFile() && entry.name.endsWith(
|
|
1442
|
+
else if (entry.isFile() && (!descriptor.discovery.extension || entry.name.endsWith(descriptor.discovery.extension))) {
|
|
1381
1443
|
out.push(path);
|
|
1382
1444
|
scan.filesDiscovered += 1;
|
|
1383
1445
|
}
|
|
@@ -1428,7 +1490,7 @@ function recordParseDiagnostic(agent, scan, diagnostics, diagnostic) {
|
|
|
1428
1490
|
});
|
|
1429
1491
|
}
|
|
1430
1492
|
function agentLabel(agent) {
|
|
1431
|
-
return agent
|
|
1493
|
+
return localAgentFormatLabel(agent);
|
|
1432
1494
|
}
|
|
1433
1495
|
function errorCodeSuffix(error) {
|
|
1434
1496
|
const code = error instanceof Error
|