@agent-finops/core 0.7.1 → 0.7.2

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.
@@ -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
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=types.js.map
@@ -1,6 +1,7 @@
1
1
  import { type TokenUsage } from "./modelPricing.js";
2
2
  import type { UsageRecord } from "./schema.js";
3
- import { type ParsedInvocationFile } from "./toolInvocations.js";
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: "claude-code" | "codex";
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.
@@ -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 { createCodexInvocationCollector } from "./toolInvocations.js";
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
- emptySourceScan("claude-code"),
567
- emptySourceScan("codex")
568
- ];
569
- const claudeScan = sourceScans[0];
570
- const codexScan = sourceScans[1];
571
- for (const file of await listJsonlFiles(claudeDir, claudeScan, diagnostics)) {
572
- let content;
573
- try {
574
- content = await readFile(file, "utf8");
575
- }
576
- catch (error) {
577
- recordUnreadableFile("claude-code", claudeScan, diagnostics, error);
578
- continue;
579
- }
580
- if (!content)
581
- continue;
582
- filesParsed += 1;
583
- claudeScan.filesParsed += 1;
584
- calls.push(...parseClaudeCodeTranscript(content, file, sinceMs, (diagnostic) => {
585
- recordParseDiagnostic("claude-code", claudeScan, diagnostics, diagnostic);
586
- }));
587
- }
588
- for (const file of await listJsonlFiles(codexDir, codexScan, diagnostics)) {
589
- if (!basename(file).startsWith("rollout-"))
590
- continue;
591
- let content;
592
- try {
593
- content = await readFile(file, "utf8");
594
- }
595
- catch (error) {
596
- recordUnreadableFile("codex", codexScan, diagnostics, error);
597
- continue;
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: aggregateCalls(filtered),
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 claudeDiagnostics = [];
645
- const codexDiagnostics = [];
646
- const sourceScans = [
647
- {
648
- ...emptySourceScan("claude-code"),
649
- filesSkippedBeforeWindow: 0,
650
- jsonlValidationCoverage: "complete"
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
- const fileCalls = [];
670
- const fileDiagnostics = [];
671
- const seen = new Set();
672
- let streamed;
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
- if (call)
741
- codexCalls.push(call);
660
+ assertFormatCallOwnership(descriptor, parsedCalls);
661
+ assertFinancialSourceOwnership(descriptor, scan, diagnostics);
662
+ calls.push(...parsedCalls);
742
663
  }
743
- };
744
- // The sources are independent and can be traversed concurrently without
745
- // changing per-file ordering or cumulative-session deduplication.
746
- await Promise.all([scanClaude(), scanCodex()]);
747
- const calls = [...claudeCalls, ...codexCalls];
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: aggregateCalls(filtered),
674
+ records: aggregateCallsForFormats(filtered, registry.map((entry) => entry.descriptor)),
754
675
  calls: filtered,
755
- filesParsed: claudeScan.filesParsed + codexScan.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: [...claudeDiagnostics, ...codexDiagnostics]
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: agent === "claude-code" ? "anthropic" : "openai",
1296
- confidence: "estimated",
1297
- observedFrom: `${agent} transcript JSONL (this machine)`
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 ? "estimated" : "missing",
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 listJsonlFiles(root, scan, diagnostics) {
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(".jsonl")) {
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 === "claude-code" ? "Claude Code" : "Codex";
1493
+ return localAgentFormatLabel(agent);
1432
1494
  }
1433
1495
  function errorCodeSuffix(error) {
1434
1496
  const code = error instanceof Error