@rasensio/aidlc 1.2.1 → 1.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (120) hide show
  1. package/dist/cli.js +4 -0
  2. package/dist/cli.js.map +1 -1
  3. package/dist/commands/claim.d.ts +15 -0
  4. package/dist/commands/claim.d.ts.map +1 -0
  5. package/dist/commands/claim.js +122 -0
  6. package/dist/commands/claim.js.map +1 -0
  7. package/dist/commands/cost.d.ts +12 -0
  8. package/dist/commands/cost.d.ts.map +1 -0
  9. package/dist/commands/cost.js +409 -0
  10. package/dist/commands/cost.js.map +1 -0
  11. package/dist/commands/gate.d.ts.map +1 -1
  12. package/dist/commands/gate.js +17 -0
  13. package/dist/commands/gate.js.map +1 -1
  14. package/dist/commands/init.d.ts.map +1 -1
  15. package/dist/commands/init.js +38 -2
  16. package/dist/commands/init.js.map +1 -1
  17. package/dist/commands/status.d.ts.map +1 -1
  18. package/dist/commands/status.js +59 -2
  19. package/dist/commands/status.js.map +1 -1
  20. package/dist/commands/transition.d.ts.map +1 -1
  21. package/dist/commands/transition.js +30 -0
  22. package/dist/commands/transition.js.map +1 -1
  23. package/dist/core/gate.d.ts +7 -0
  24. package/dist/core/gate.d.ts.map +1 -1
  25. package/dist/core/gate.js.map +1 -1
  26. package/dist/core/types.d.ts +18 -1
  27. package/dist/core/types.d.ts.map +1 -1
  28. package/dist/cost/config.d.ts +8 -0
  29. package/dist/cost/config.d.ts.map +1 -0
  30. package/dist/cost/config.js +37 -0
  31. package/dist/cost/config.js.map +1 -0
  32. package/dist/cost/hooks/claude-code.d.ts +29 -0
  33. package/dist/cost/hooks/claude-code.d.ts.map +1 -0
  34. package/dist/cost/hooks/claude-code.js +101 -0
  35. package/dist/cost/hooks/claude-code.js.map +1 -0
  36. package/dist/cost/hooks/kiro.d.ts +17 -0
  37. package/dist/cost/hooks/kiro.d.ts.map +1 -0
  38. package/dist/cost/hooks/kiro.js +66 -0
  39. package/dist/cost/hooks/kiro.js.map +1 -0
  40. package/dist/cost/ledger.d.ts +70 -0
  41. package/dist/cost/ledger.d.ts.map +1 -0
  42. package/dist/cost/ledger.js +314 -0
  43. package/dist/cost/ledger.js.map +1 -0
  44. package/dist/cost/providers/claude-code.d.ts +20 -0
  45. package/dist/cost/providers/claude-code.d.ts.map +1 -0
  46. package/dist/cost/providers/claude-code.js +116 -0
  47. package/dist/cost/providers/claude-code.js.map +1 -0
  48. package/dist/cost/providers/kiro.d.ts +19 -0
  49. package/dist/cost/providers/kiro.d.ts.map +1 -0
  50. package/dist/cost/providers/kiro.js +147 -0
  51. package/dist/cost/providers/kiro.js.map +1 -0
  52. package/dist/cost/providers/provider.d.ts +36 -0
  53. package/dist/cost/providers/provider.d.ts.map +1 -0
  54. package/dist/cost/providers/provider.js +21 -0
  55. package/dist/cost/providers/provider.js.map +1 -0
  56. package/dist/cost/rates-bundled.d.ts +11 -0
  57. package/dist/cost/rates-bundled.d.ts.map +1 -0
  58. package/dist/cost/rates-bundled.js +23 -0
  59. package/dist/cost/rates-bundled.js.map +1 -0
  60. package/dist/cost/rates.d.ts +42 -0
  61. package/dist/cost/rates.d.ts.map +1 -0
  62. package/dist/cost/rates.js +190 -0
  63. package/dist/cost/rates.js.map +1 -0
  64. package/dist/cost/recorder.d.ts +57 -0
  65. package/dist/cost/recorder.d.ts.map +1 -0
  66. package/dist/cost/recorder.js +339 -0
  67. package/dist/cost/recorder.js.map +1 -0
  68. package/dist/cost/report.d.ts +44 -0
  69. package/dist/cost/report.d.ts.map +1 -0
  70. package/dist/cost/report.js +164 -0
  71. package/dist/cost/report.js.map +1 -0
  72. package/dist/cost/sessions.d.ts +40 -0
  73. package/dist/cost/sessions.d.ts.map +1 -0
  74. package/dist/cost/sessions.js +200 -0
  75. package/dist/cost/sessions.js.map +1 -0
  76. package/dist/cost/sync.d.ts +24 -0
  77. package/dist/cost/sync.d.ts.map +1 -0
  78. package/dist/cost/sync.js +191 -0
  79. package/dist/cost/sync.js.map +1 -0
  80. package/dist/cost/types.d.ts +116 -0
  81. package/dist/cost/types.d.ts.map +1 -0
  82. package/dist/cost/types.js +13 -0
  83. package/dist/cost/types.js.map +1 -0
  84. package/dist/setup/setup-compute.d.ts +6 -0
  85. package/dist/setup/setup-compute.d.ts.map +1 -1
  86. package/dist/setup/setup-compute.js +3 -0
  87. package/dist/setup/setup-compute.js.map +1 -1
  88. package/dist/traceability/cache.d.ts +21 -0
  89. package/dist/traceability/cache.d.ts.map +1 -0
  90. package/dist/traceability/cache.js +106 -0
  91. package/dist/traceability/cache.js.map +1 -0
  92. package/dist/traceability/coverage-block.d.ts +21 -0
  93. package/dist/traceability/coverage-block.d.ts.map +1 -0
  94. package/dist/traceability/coverage-block.js +41 -0
  95. package/dist/traceability/coverage-block.js.map +1 -0
  96. package/dist/traceability/gate-criterion.d.ts +34 -0
  97. package/dist/traceability/gate-criterion.d.ts.map +1 -0
  98. package/dist/traceability/gate-criterion.js +128 -0
  99. package/dist/traceability/gate-criterion.js.map +1 -0
  100. package/dist/traceability/matrix.d.ts +35 -0
  101. package/dist/traceability/matrix.d.ts.map +1 -0
  102. package/dist/traceability/matrix.js +93 -0
  103. package/dist/traceability/matrix.js.map +1 -0
  104. package/dist/traceability/parser.d.ts +27 -0
  105. package/dist/traceability/parser.d.ts.map +1 -0
  106. package/dist/traceability/parser.js +101 -0
  107. package/dist/traceability/parser.js.map +1 -0
  108. package/dist/traceability/scanner.d.ts +62 -0
  109. package/dist/traceability/scanner.d.ts.map +1 -0
  110. package/dist/traceability/scanner.js +198 -0
  111. package/dist/traceability/scanner.js.map +1 -0
  112. package/dist/traceability/types.d.ts +77 -0
  113. package/dist/traceability/types.d.ts.map +1 -0
  114. package/dist/traceability/types.js +8 -0
  115. package/dist/traceability/types.js.map +1 -0
  116. package/package.json +3 -5
  117. package/dist/commands/setup.d.ts +0 -24
  118. package/dist/commands/setup.d.ts.map +0 -1
  119. package/dist/commands/setup.js +0 -262
  120. package/dist/commands/setup.js.map +0 -1
@@ -0,0 +1,37 @@
1
+ /**
2
+ * Cost config loader: tolerant reader for the optional `cost:` section of
3
+ * .aidlc/config.yaml (mirrors the loadOrchestratorConfig pattern).
4
+ */
5
+ import { readFileSync, existsSync } from 'node:fs';
6
+ import { join } from 'node:path';
7
+ import { parse as parseYaml } from 'yaml';
8
+ const DEFAULT_CLAIM_TIMEOUT_MINUTES = 60;
9
+ export function loadCostConfig(projectRoot) {
10
+ const configPath = join(projectRoot, '.aidlc', 'config.yaml');
11
+ try {
12
+ if (!existsSync(configPath))
13
+ return null;
14
+ const config = parseYaml(readFileSync(configPath, 'utf8'));
15
+ const cost = config?.cost;
16
+ if (typeof cost !== 'object' || cost === null)
17
+ return null;
18
+ return cost;
19
+ }
20
+ catch {
21
+ return null;
22
+ }
23
+ }
24
+ export function loadClaimTimeoutMinutes(projectRoot) {
25
+ const configPath = join(projectRoot, '.aidlc', 'config.yaml');
26
+ try {
27
+ if (!existsSync(configPath))
28
+ return DEFAULT_CLAIM_TIMEOUT_MINUTES;
29
+ const config = parseYaml(readFileSync(configPath, 'utf8'));
30
+ const v = config?.claim_timeout_minutes;
31
+ return typeof v === 'number' && v > 0 ? v : DEFAULT_CLAIM_TIMEOUT_MINUTES;
32
+ }
33
+ catch {
34
+ return DEFAULT_CLAIM_TIMEOUT_MINUTES;
35
+ }
36
+ }
37
+ //# sourceMappingURL=config.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"config.js","sourceRoot":"","sources":["../../src/cost/config.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,EAAE,YAAY,EAAE,UAAU,EAAE,MAAM,SAAS,CAAC;AACnD,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AACjC,OAAO,EAAE,KAAK,IAAI,SAAS,EAAE,MAAM,MAAM,CAAC;AAG1C,MAAM,6BAA6B,GAAG,EAAE,CAAC;AAEzC,MAAM,UAAU,cAAc,CAAC,WAAmB;IAChD,MAAM,UAAU,GAAG,IAAI,CAAC,WAAW,EAAE,QAAQ,EAAE,aAAa,CAAC,CAAC;IAC9D,IAAI,CAAC;QACH,IAAI,CAAC,UAAU,CAAC,UAAU,CAAC;YAAE,OAAO,IAAI,CAAC;QACzC,MAAM,MAAM,GAAG,SAAS,CAAC,YAAY,CAAC,UAAU,EAAE,MAAM,CAAC,CAAmC,CAAC;QAC7F,MAAM,IAAI,GAAG,MAAM,EAAE,IAAI,CAAC;QAC1B,IAAI,OAAO,IAAI,KAAK,QAAQ,IAAI,IAAI,KAAK,IAAI;YAAE,OAAO,IAAI,CAAC;QAC3D,OAAO,IAAkB,CAAC;IAC5B,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,IAAI,CAAC;IACd,CAAC;AACH,CAAC;AAED,MAAM,UAAU,uBAAuB,CAAC,WAAmB;IACzD,MAAM,UAAU,GAAG,IAAI,CAAC,WAAW,EAAE,QAAQ,EAAE,aAAa,CAAC,CAAC;IAC9D,IAAI,CAAC;QACH,IAAI,CAAC,UAAU,CAAC,UAAU,CAAC;YAAE,OAAO,6BAA6B,CAAC;QAClE,MAAM,MAAM,GAAG,SAAS,CAAC,YAAY,CAAC,UAAU,EAAE,MAAM,CAAC,CAAmC,CAAC;QAC7F,MAAM,CAAC,GAAG,MAAM,EAAE,qBAAqB,CAAC;QACxC,OAAO,OAAO,CAAC,KAAK,QAAQ,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,6BAA6B,CAAC;IAC5E,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,6BAA6B,CAAC;IACvC,CAAC;AACH,CAAC"}
@@ -0,0 +1,29 @@
1
+ /**
2
+ * claude-code hook installer: SessionEnd (capture) + SessionStart (stash).
3
+ *
4
+ * Edits .claude/settings.json parse-preservingly: only the two hook arrays
5
+ * are touched; every other key is carried through untouched (FR3.4).
6
+ * Identity rule: our hook commands end with the `#aidlc-cost-hook` marker —
7
+ * install/remove/status match on the marker alone, so command bodies can
8
+ * change between versions without orphaning old hooks (design §5).
9
+ *
10
+ * Hook merge semantics are additive across settings files by platform
11
+ * design, so user hooks are safe by construction.
12
+ */
13
+ export declare const HOOK_MARKER = "#aidlc-cost-hook";
14
+ /** The hook commands, with absolute paths resolved at install time (FR3.10). */
15
+ export declare function hookCommands(nodePath: string, cliPath: string): {
16
+ sessionEnd: string;
17
+ sessionStart: string;
18
+ };
19
+ /** Install (idempotently) the cost hooks into project settings. */
20
+ export declare function installClaudeCodeHooks(projectRoot: string, nodePath: string, cliPath: string): void;
21
+ /** Remove our hooks; leaves everything else untouched. */
22
+ export declare function removeClaudeCodeHooks(projectRoot: string): void;
23
+ export interface HookStatus {
24
+ installed: boolean;
25
+ /** The recorded CLI path still resolves. */
26
+ commandResolvable: boolean;
27
+ }
28
+ export declare function claudeCodeHookStatus(projectRoot: string): HookStatus;
29
+ //# sourceMappingURL=claude-code.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"claude-code.d.ts","sourceRoot":"","sources":["../../../src/cost/hooks/claude-code.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAKH,eAAO,MAAM,WAAW,qBAAqB,CAAC;AA2B9C,gFAAgF;AAChF,wBAAgB,YAAY,CAAC,QAAQ,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG;IAAE,UAAU,EAAE,MAAM,CAAC;IAAC,YAAY,EAAE,MAAM,CAAA;CAAE,CAM5G;AAwBD,mEAAmE;AACnE,wBAAgB,sBAAsB,CAAC,WAAW,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,IAAI,CAenG;AAED,0DAA0D;AAC1D,wBAAgB,qBAAqB,CAAC,WAAW,EAAE,MAAM,GAAG,IAAI,CAW/D;AAED,MAAM,WAAW,UAAU;IACzB,SAAS,EAAE,OAAO,CAAC;IACnB,4CAA4C;IAC5C,iBAAiB,EAAE,OAAO,CAAC;CAC5B;AAED,wBAAgB,oBAAoB,CAAC,WAAW,EAAE,MAAM,GAAG,UAAU,CAiBpE"}
@@ -0,0 +1,101 @@
1
+ /**
2
+ * claude-code hook installer: SessionEnd (capture) + SessionStart (stash).
3
+ *
4
+ * Edits .claude/settings.json parse-preservingly: only the two hook arrays
5
+ * are touched; every other key is carried through untouched (FR3.4).
6
+ * Identity rule: our hook commands end with the `#aidlc-cost-hook` marker —
7
+ * install/remove/status match on the marker alone, so command bodies can
8
+ * change between versions without orphaning old hooks (design §5).
9
+ *
10
+ * Hook merge semantics are additive across settings files by platform
11
+ * design, so user hooks are safe by construction.
12
+ */
13
+ import { readFileSync, writeFileSync, existsSync, mkdirSync } from 'node:fs';
14
+ import { dirname, join } from 'node:path';
15
+ export const HOOK_MARKER = '#aidlc-cost-hook';
16
+ function settingsPath(projectRoot) {
17
+ return join(projectRoot, '.claude', 'settings.json');
18
+ }
19
+ function quote(p) {
20
+ return `"${p.replace(/"/g, '\\"')}"`;
21
+ }
22
+ /** The hook commands, with absolute paths resolved at install time (FR3.10). */
23
+ export function hookCommands(nodePath, cliPath) {
24
+ const base = `${quote(nodePath)} ${quote(cliPath)}`;
25
+ return {
26
+ sessionEnd: `${base} cost record --provider claude-code --detach ${HOOK_MARKER}`,
27
+ sessionStart: `${base} cost session-start --provider claude-code ${HOOK_MARKER}`,
28
+ };
29
+ }
30
+ function loadSettings(projectRoot) {
31
+ const path = settingsPath(projectRoot);
32
+ if (!existsSync(path))
33
+ return {};
34
+ return JSON.parse(readFileSync(path, 'utf8'));
35
+ }
36
+ function saveSettings(projectRoot, settings) {
37
+ const path = settingsPath(projectRoot);
38
+ mkdirSync(dirname(path), { recursive: true });
39
+ writeFileSync(path, JSON.stringify(settings, null, 2) + '\n');
40
+ }
41
+ function hasMarkedHook(groups) {
42
+ return (groups ?? []).some((g) => g.hooks.some((h) => h.command.endsWith(HOOK_MARKER)));
43
+ }
44
+ function withoutMarkedHooks(groups) {
45
+ return (groups ?? [])
46
+ .map((g) => ({ ...g, hooks: g.hooks.filter((h) => !h.command.endsWith(HOOK_MARKER)) }))
47
+ .filter((g) => g.hooks.length > 0);
48
+ }
49
+ /** Install (idempotently) the cost hooks into project settings. */
50
+ export function installClaudeCodeHooks(projectRoot, nodePath, cliPath) {
51
+ const settings = loadSettings(projectRoot);
52
+ const hooks = settings.hooks ?? {};
53
+ const cmds = hookCommands(nodePath, cliPath);
54
+ const ensure = (event, command) => {
55
+ const cleaned = withoutMarkedHooks(hooks[event]);
56
+ cleaned.push({ hooks: [{ type: 'command', command, timeout: 30 }] });
57
+ hooks[event] = cleaned;
58
+ };
59
+ ensure('SessionEnd', cmds.sessionEnd);
60
+ ensure('SessionStart', cmds.sessionStart);
61
+ settings.hooks = hooks;
62
+ saveSettings(projectRoot, settings);
63
+ }
64
+ /** Remove our hooks; leaves everything else untouched. */
65
+ export function removeClaudeCodeHooks(projectRoot) {
66
+ if (!existsSync(settingsPath(projectRoot)))
67
+ return;
68
+ const settings = loadSettings(projectRoot);
69
+ if (!settings.hooks)
70
+ return;
71
+ for (const event of Object.keys(settings.hooks)) {
72
+ const cleaned = withoutMarkedHooks(settings.hooks[event]);
73
+ if (cleaned.length > 0)
74
+ settings.hooks[event] = cleaned;
75
+ else
76
+ delete settings.hooks[event];
77
+ }
78
+ if (Object.keys(settings.hooks).length === 0)
79
+ delete settings.hooks;
80
+ saveSettings(projectRoot, settings);
81
+ }
82
+ export function claudeCodeHookStatus(projectRoot) {
83
+ let settings;
84
+ try {
85
+ settings = loadSettings(projectRoot);
86
+ }
87
+ catch {
88
+ return { installed: false, commandResolvable: false };
89
+ }
90
+ const installed = hasMarkedHook(settings.hooks?.SessionEnd) && hasMarkedHook(settings.hooks?.SessionStart);
91
+ if (!installed)
92
+ return { installed: false, commandResolvable: false };
93
+ // Extract the quoted CLI path (second quoted token) and check it exists.
94
+ const cmd = (settings.hooks?.SessionEnd ?? [])
95
+ .flatMap((g) => g.hooks)
96
+ .find((h) => h.command.endsWith(HOOK_MARKER))?.command;
97
+ const quoted = cmd?.match(/"([^"]+)"\s+"([^"]+)"/);
98
+ const resolvable = quoted ? existsSync(quoted[2]) : false;
99
+ return { installed, commandResolvable: resolvable };
100
+ }
101
+ //# sourceMappingURL=claude-code.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"claude-code.js","sourceRoot":"","sources":["../../../src/cost/hooks/claude-code.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,17 @@
1
+ /**
2
+ * kiro hook installer: own-file v1 hooks in .kiro/hooks/aidlc-cost.json.
3
+ *
4
+ * Kiro v1 hooks (IDE 1.0 / CLI 3.0+) are standalone JSON files, so install
5
+ * is a file write and remove is a delete — no merge risk. `Stop` fires per
6
+ * response (no session-end event exists); the recorder's cursor + lock make
7
+ * repeated firings cheap no-ops (design §4.2, D2).
8
+ *
9
+ * Kiro CLI 2.x uses a different hook format and is out of scope for v1.
10
+ */
11
+ export declare function installKiroHooks(projectRoot: string, nodePath: string, cliPath: string): void;
12
+ export declare function removeKiroHooks(projectRoot: string): void;
13
+ export declare function kiroHookStatus(projectRoot: string): {
14
+ installed: boolean;
15
+ commandResolvable: boolean;
16
+ };
17
+ //# sourceMappingURL=kiro.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"kiro.d.ts","sourceRoot":"","sources":["../../../src/cost/hooks/kiro.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAcH,wBAAgB,gBAAgB,CAAC,WAAW,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,IAAI,CA0B7F;AAED,wBAAgB,eAAe,CAAC,WAAW,EAAE,MAAM,GAAG,IAAI,CAGzD;AAED,wBAAgB,cAAc,CAAC,WAAW,EAAE,MAAM,GAAG;IAAE,SAAS,EAAE,OAAO,CAAC;IAAC,iBAAiB,EAAE,OAAO,CAAA;CAAE,CAatG"}
@@ -0,0 +1,66 @@
1
+ /**
2
+ * kiro hook installer: own-file v1 hooks in .kiro/hooks/aidlc-cost.json.
3
+ *
4
+ * Kiro v1 hooks (IDE 1.0 / CLI 3.0+) are standalone JSON files, so install
5
+ * is a file write and remove is a delete — no merge risk. `Stop` fires per
6
+ * response (no session-end event exists); the recorder's cursor + lock make
7
+ * repeated firings cheap no-ops (design §4.2, D2).
8
+ *
9
+ * Kiro CLI 2.x uses a different hook format and is out of scope for v1.
10
+ */
11
+ import { readFileSync, writeFileSync, existsSync, unlinkSync, mkdirSync } from 'node:fs';
12
+ import { dirname, join } from 'node:path';
13
+ import { HOOK_MARKER } from './claude-code.js';
14
+ function hookFilePath(projectRoot) {
15
+ return join(projectRoot, '.kiro', 'hooks', 'aidlc-cost.json');
16
+ }
17
+ function quote(p) {
18
+ return `"${p.replace(/"/g, '\\"')}"`;
19
+ }
20
+ export function installKiroHooks(projectRoot, nodePath, cliPath) {
21
+ const base = `${quote(nodePath)} ${quote(cliPath)}`;
22
+ const file = {
23
+ version: 'v1',
24
+ hooks: [
25
+ {
26
+ name: 'aidlc-cost-record',
27
+ trigger: 'Stop',
28
+ action: {
29
+ type: 'command',
30
+ command: `${base} cost record --provider kiro --detach ${HOOK_MARKER}`,
31
+ },
32
+ },
33
+ {
34
+ name: 'aidlc-cost-session-start',
35
+ trigger: 'SessionStart',
36
+ action: {
37
+ type: 'command',
38
+ command: `${base} cost session-start --provider kiro ${HOOK_MARKER}`,
39
+ },
40
+ },
41
+ ],
42
+ };
43
+ const path = hookFilePath(projectRoot);
44
+ mkdirSync(dirname(path), { recursive: true });
45
+ writeFileSync(path, JSON.stringify(file, null, 2) + '\n');
46
+ }
47
+ export function removeKiroHooks(projectRoot) {
48
+ const path = hookFilePath(projectRoot);
49
+ if (existsSync(path))
50
+ unlinkSync(path);
51
+ }
52
+ export function kiroHookStatus(projectRoot) {
53
+ const path = hookFilePath(projectRoot);
54
+ if (!existsSync(path))
55
+ return { installed: false, commandResolvable: false };
56
+ try {
57
+ const parsed = JSON.parse(readFileSync(path, 'utf8'));
58
+ const cmd = parsed.hooks?.find((h) => h.action?.command?.includes(HOOK_MARKER))?.action?.command;
59
+ const quoted = cmd?.match(/"([^"]+)"\s+"([^"]+)"/);
60
+ return { installed: true, commandResolvable: quoted ? existsSync(quoted[2]) : false };
61
+ }
62
+ catch {
63
+ return { installed: false, commandResolvable: false };
64
+ }
65
+ }
66
+ //# sourceMappingURL=kiro.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"kiro.js","sourceRoot":"","sources":["../../../src/cost/hooks/kiro.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAEH,OAAO,EAAE,YAAY,EAAE,aAAa,EAAE,UAAU,EAAE,UAAU,EAAE,SAAS,EAAE,MAAM,SAAS,CAAC;AACzF,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AAC1C,OAAO,EAAE,WAAW,EAAE,MAAM,kBAAkB,CAAC;AAE/C,SAAS,YAAY,CAAC,WAAmB;IACvC,OAAO,IAAI,CAAC,WAAW,EAAE,OAAO,EAAE,OAAO,EAAE,iBAAiB,CAAC,CAAC;AAChE,CAAC;AAED,SAAS,KAAK,CAAC,CAAS;IACtB,OAAO,IAAI,CAAC,CAAC,OAAO,CAAC,IAAI,EAAE,KAAK,CAAC,GAAG,CAAC;AACvC,CAAC;AAED,MAAM,UAAU,gBAAgB,CAAC,WAAmB,EAAE,QAAgB,EAAE,OAAe;IACrF,MAAM,IAAI,GAAG,GAAG,KAAK,CAAC,QAAQ,CAAC,IAAI,KAAK,CAAC,OAAO,CAAC,EAAE,CAAC;IACpD,MAAM,IAAI,GAAG;QACX,OAAO,EAAE,IAAI;QACb,KAAK,EAAE;YACL;gBACE,IAAI,EAAE,mBAAmB;gBACzB,OAAO,EAAE,MAAM;gBACf,MAAM,EAAE;oBACN,IAAI,EAAE,SAAS;oBACf,OAAO,EAAE,GAAG,IAAI,yCAAyC,WAAW,EAAE;iBACvE;aACF;YACD;gBACE,IAAI,EAAE,0BAA0B;gBAChC,OAAO,EAAE,cAAc;gBACvB,MAAM,EAAE;oBACN,IAAI,EAAE,SAAS;oBACf,OAAO,EAAE,GAAG,IAAI,uCAAuC,WAAW,EAAE;iBACrE;aACF;SACF;KACF,CAAC;IACF,MAAM,IAAI,GAAG,YAAY,CAAC,WAAW,CAAC,CAAC;IACvC,SAAS,CAAC,OAAO,CAAC,IAAI,CAAC,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;IAC9C,aAAa,CAAC,IAAI,EAAE,IAAI,CAAC,SAAS,CAAC,IAAI,EAAE,IAAI,EAAE,CAAC,CAAC,GAAG,IAAI,CAAC,CAAC;AAC5D,CAAC;AAED,MAAM,UAAU,eAAe,CAAC,WAAmB;IACjD,MAAM,IAAI,GAAG,YAAY,CAAC,WAAW,CAAC,CAAC;IACvC,IAAI,UAAU,CAAC,IAAI,CAAC;QAAE,UAAU,CAAC,IAAI,CAAC,CAAC;AACzC,CAAC;AAED,MAAM,UAAU,cAAc,CAAC,WAAmB;IAChD,MAAM,IAAI,GAAG,YAAY,CAAC,WAAW,CAAC,CAAC;IACvC,IAAI,CAAC,UAAU,CAAC,IAAI,CAAC;QAAE,OAAO,EAAE,SAAS,EAAE,KAAK,EAAE,iBAAiB,EAAE,KAAK,EAAE,CAAC;IAC7E,IAAI,CAAC;QACH,MAAM,MAAM,GAAG,IAAI,CAAC,KAAK,CAAC,YAAY,CAAC,IAAI,EAAE,MAAM,CAAC,CAEnD,CAAC;QACF,MAAM,GAAG,GAAG,MAAM,CAAC,KAAK,EAAE,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,MAAM,EAAE,OAAO,EAAE,QAAQ,CAAC,WAAW,CAAC,CAAC,EAAE,MAAM,EAAE,OAAO,CAAC;QACjG,MAAM,MAAM,GAAG,GAAG,EAAE,KAAK,CAAC,uBAAuB,CAAC,CAAC;QACnD,OAAO,EAAE,SAAS,EAAE,IAAI,EAAE,iBAAiB,EAAE,MAAM,CAAC,CAAC,CAAC,UAAU,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,KAAK,EAAE,CAAC;IACxF,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,EAAE,SAAS,EAAE,KAAK,EAAE,iBAAiB,EAAE,KAAK,EAAE,CAAC;IACxD,CAAC;AACH,CAAC"}
@@ -0,0 +1,70 @@
1
+ /**
2
+ * Cost ledger: append-only NDJSON with closed-schema validation.
3
+ *
4
+ * Write semantics mirror transition-log.ts: O_APPEND single-line writes,
5
+ * whole-line atomicity under concurrent writers. Readers skip torn or
6
+ * malformed lines with a stderr warning.
7
+ *
8
+ * Privacy (SR1/FR2.4): validateEntry enforces a closed key set with
9
+ * numeric/enum/identifier values only — free-form text cannot enter the
10
+ * ledger regardless of origin (provider, sync, or --manual).
11
+ *
12
+ * Design: design.md §2.1, §4.5
13
+ */
14
+ import type { CostEntry, CostFidelity } from './types.js';
15
+ /**
16
+ * Validate a candidate ledger entry against the closed schema.
17
+ *
18
+ * @returns null when valid, otherwise a reason string (safe to log).
19
+ */
20
+ export declare function validateEntry(entry: unknown): string | null;
21
+ /**
22
+ * Append one validated entry to a ledger file. Throws on invalid entries —
23
+ * callers decide whether that is a logged rejection (hook path) or an error
24
+ * surfaced to the user (manual path).
25
+ */
26
+ export declare function appendEntry(ledgerPath: string, entry: CostEntry): void;
27
+ /**
28
+ * Read all valid entries from a ledger file. Missing file → empty array.
29
+ * Malformed or schema-invalid lines are skipped with a stderr warning
30
+ * (line number only — never line content, per SR1).
31
+ */
32
+ export declare function readEntries(ledgerPath: string): CostEntry[];
33
+ /** Ledger path for an instance. */
34
+ export declare function ledgerPath(projectRoot: string, instance: string): string;
35
+ /** Path of the unattributed ledger. */
36
+ export declare function unattributedPath(projectRoot: string): string;
37
+ /**
38
+ * Newest cursor recorded for a session across the given ledger files
39
+ * (instance ledgers plus unattributed — design §2.4). "Newest" is by entry
40
+ * timestamp; ties resolved by later coverage.to.
41
+ */
42
+ export declare function newestCursor(session_id: string, ledgerPaths: string[]): string | null;
43
+ /** A cost entry with its counted weight after precedence reduction. */
44
+ export interface WeightedEntry {
45
+ entry: CostEntry;
46
+ /** 0..1 — fraction of this entry's units that count toward totals. */
47
+ weight: number;
48
+ }
49
+ /**
50
+ * Apply read-time fidelity precedence: each entry's weight is the fraction
51
+ * of its coverage window not overlapped by higher-fidelity coverage for the
52
+ * same session. `session_id: null` entries always count in full. `unknown`
53
+ * entries carry no units and get weight 0.
54
+ *
55
+ * Linear sweep over per-session sorted window lists — O(n log n).
56
+ */
57
+ export declare function applyPrecedence(entries: CostEntry[]): WeightedEntry[];
58
+ /** Sum weighted units per model (tokens) and total credits. */
59
+ export interface UsageTotals {
60
+ tokens: Map<string, {
61
+ input: number;
62
+ output: number;
63
+ cacheRead: number;
64
+ cacheWrite: number;
65
+ }>;
66
+ credits: number;
67
+ fidelityWeights: Record<CostFidelity, number>;
68
+ }
69
+ export declare function totalUsage(weighted: WeightedEntry[]): UsageTotals;
70
+ //# sourceMappingURL=ledger.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ledger.d.ts","sourceRoot":"","sources":["../../src/cost/ledger.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAWH,OAAO,KAAK,EAAE,SAAS,EAAE,YAAY,EAAa,MAAM,YAAY,CAAC;AAmDrE;;;;GAIG;AACH,wBAAgB,aAAa,CAAC,KAAK,EAAE,OAAO,GAAG,MAAM,GAAG,IAAI,CAmC3D;AAED;;;;GAIG;AACH,wBAAgB,WAAW,CAAC,UAAU,EAAE,MAAM,EAAE,KAAK,EAAE,SAAS,GAAG,IAAI,CAUtE;AAED;;;;GAIG;AACH,wBAAgB,WAAW,CAAC,UAAU,EAAE,MAAM,GAAG,SAAS,EAAE,CA6B3D;AAED,mCAAmC;AACnC,wBAAgB,UAAU,CAAC,WAAW,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,MAAM,CAExE;AAED,uCAAuC;AACvC,wBAAgB,gBAAgB,CAAC,WAAW,EAAE,MAAM,GAAG,MAAM,CAE5D;AAED;;;;GAIG;AACH,wBAAgB,YAAY,CAAC,UAAU,EAAE,MAAM,EAAE,WAAW,EAAE,MAAM,EAAE,GAAG,MAAM,GAAG,IAAI,CAerF;AAiDD,uEAAuE;AACvE,MAAM,WAAW,aAAa;IAC5B,KAAK,EAAE,SAAS,CAAC;IACjB,sEAAsE;IACtE,MAAM,EAAE,MAAM,CAAC;CAChB;AAED;;;;;;;GAOG;AACH,wBAAgB,eAAe,CAAC,OAAO,EAAE,SAAS,EAAE,GAAG,aAAa,EAAE,CA8CrE;AAED,+DAA+D;AAC/D,MAAM,WAAW,WAAW;IAC1B,MAAM,EAAE,GAAG,CAAC,MAAM,EAAE;QAAE,KAAK,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,MAAM,CAAC;QAAC,SAAS,EAAE,MAAM,CAAC;QAAC,UAAU,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;IAC9F,OAAO,EAAE,MAAM,CAAC;IAChB,eAAe,EAAE,MAAM,CAAC,YAAY,EAAE,MAAM,CAAC,CAAC;CAC/C;AAED,wBAAgB,UAAU,CAAC,QAAQ,EAAE,aAAa,EAAE,GAAG,WAAW,CAsBjE"}
@@ -0,0 +1,314 @@
1
+ /**
2
+ * Cost ledger: append-only NDJSON with closed-schema validation.
3
+ *
4
+ * Write semantics mirror transition-log.ts: O_APPEND single-line writes,
5
+ * whole-line atomicity under concurrent writers. Readers skip torn or
6
+ * malformed lines with a stderr warning.
7
+ *
8
+ * Privacy (SR1/FR2.4): validateEntry enforces a closed key set with
9
+ * numeric/enum/identifier values only — free-form text cannot enter the
10
+ * ledger regardless of origin (provider, sync, or --manual).
11
+ *
12
+ * Design: design.md §2.1, §4.5
13
+ */
14
+ import { readFileSync, openSync, writeSync, closeSync, constants, mkdirSync, } from 'node:fs';
15
+ import { dirname, join } from 'node:path';
16
+ import { SESSION_ID_RE, MODEL_ID_RE } from './types.js';
17
+ const FIDELITIES = ['metered', 'reconciled', 'estimated', 'unknown'];
18
+ const PROVIDER_RE = /^[a-z][a-z0-9-]{0,31}$/;
19
+ const PHASE_RE = /^[a-z][a-z0-9-]{0,31}$/;
20
+ const CURSOR_RE = /^[A-Za-z0-9_.:/@-]{1,256}$/;
21
+ const ENTRY_KEYS = new Set([
22
+ 'timestamp', 'session_id', 'phase', 'provider', 'fidelity', 'coverage', 'cursor', 'units',
23
+ ]);
24
+ const TOKEN_KEYS = new Set([
25
+ 'kind', 'model', 'input_tokens', 'output_tokens', 'cache_read_tokens', 'cache_write_tokens',
26
+ ]);
27
+ const CREDIT_KEYS = new Set(['kind', 'credits', 'model']);
28
+ function isIso8601(v) {
29
+ return typeof v === 'string' && !Number.isNaN(Date.parse(v)) && /^\d{4}-\d{2}-\d{2}T/.test(v);
30
+ }
31
+ function isCount(v) {
32
+ return typeof v === 'number' && Number.isFinite(v) && v >= 0;
33
+ }
34
+ function keysWithin(obj, allowed) {
35
+ return Object.keys(obj).every((k) => allowed.has(k));
36
+ }
37
+ function validateUnits(units) {
38
+ if (units === null)
39
+ return null;
40
+ if (typeof units !== 'object' || Array.isArray(units))
41
+ return 'units must be an object or null';
42
+ const u = units;
43
+ if (u.kind === 'tokens') {
44
+ if (!keysWithin(u, TOKEN_KEYS))
45
+ return 'unknown key in token units';
46
+ if (typeof u.model !== 'string' || !MODEL_ID_RE.test(u.model))
47
+ return 'invalid model id';
48
+ for (const k of ['input_tokens', 'output_tokens', 'cache_read_tokens', 'cache_write_tokens']) {
49
+ if (!isCount(u[k]))
50
+ return `invalid ${k}`;
51
+ }
52
+ return null;
53
+ }
54
+ if (u.kind === 'credits') {
55
+ if (!keysWithin(u, CREDIT_KEYS))
56
+ return 'unknown key in credit units';
57
+ if (!isCount(u.credits))
58
+ return 'invalid credits';
59
+ if (u.model !== null && (typeof u.model !== 'string' || !MODEL_ID_RE.test(u.model))) {
60
+ return 'invalid model id';
61
+ }
62
+ return null;
63
+ }
64
+ return 'units.kind must be tokens or credits';
65
+ }
66
+ /**
67
+ * Validate a candidate ledger entry against the closed schema.
68
+ *
69
+ * @returns null when valid, otherwise a reason string (safe to log).
70
+ */
71
+ export function validateEntry(entry) {
72
+ if (typeof entry !== 'object' || entry === null || Array.isArray(entry)) {
73
+ return 'entry must be an object';
74
+ }
75
+ const e = entry;
76
+ if (!keysWithin(e, ENTRY_KEYS))
77
+ return 'unknown key in entry';
78
+ for (const k of ENTRY_KEYS) {
79
+ if (!(k in e))
80
+ return `missing key ${k}`;
81
+ }
82
+ if (!isIso8601(e.timestamp))
83
+ return 'invalid timestamp';
84
+ if (e.session_id !== null && (typeof e.session_id !== 'string' || !SESSION_ID_RE.test(e.session_id))) {
85
+ return 'invalid session_id';
86
+ }
87
+ if (e.phase !== null && (typeof e.phase !== 'string' || !PHASE_RE.test(e.phase))) {
88
+ return 'invalid phase';
89
+ }
90
+ if (typeof e.provider !== 'string' || !PROVIDER_RE.test(e.provider))
91
+ return 'invalid provider';
92
+ if (!FIDELITIES.includes(e.fidelity))
93
+ return 'invalid fidelity';
94
+ const cov = e.coverage;
95
+ if (typeof cov !== 'object' || cov === null || !keysWithin(cov, new Set(['from', 'to']))) {
96
+ return 'invalid coverage';
97
+ }
98
+ if (!isIso8601(cov.from) || !isIso8601(cov.to))
99
+ return 'invalid coverage timestamps';
100
+ if (Date.parse(cov.from) > Date.parse(cov.to)) {
101
+ return 'coverage.from after coverage.to';
102
+ }
103
+ if (e.cursor !== null && (typeof e.cursor !== 'string' || !CURSOR_RE.test(e.cursor))) {
104
+ return 'invalid cursor';
105
+ }
106
+ const unitsErr = validateUnits(e.units);
107
+ if (unitsErr)
108
+ return unitsErr;
109
+ if ((e.units === null) !== (e.fidelity === 'unknown')) {
110
+ return 'units null iff fidelity unknown';
111
+ }
112
+ return null;
113
+ }
114
+ /**
115
+ * Append one validated entry to a ledger file. Throws on invalid entries —
116
+ * callers decide whether that is a logged rejection (hook path) or an error
117
+ * surfaced to the user (manual path).
118
+ */
119
+ export function appendEntry(ledgerPath, entry) {
120
+ const err = validateEntry(entry);
121
+ if (err)
122
+ throw new Error(`invalid cost entry: ${err}`);
123
+ mkdirSync(dirname(ledgerPath), { recursive: true });
124
+ const fd = openSync(ledgerPath, constants.O_WRONLY | constants.O_APPEND | constants.O_CREAT);
125
+ try {
126
+ writeSync(fd, JSON.stringify(entry) + '\n');
127
+ }
128
+ finally {
129
+ closeSync(fd);
130
+ }
131
+ }
132
+ /**
133
+ * Read all valid entries from a ledger file. Missing file → empty array.
134
+ * Malformed or schema-invalid lines are skipped with a stderr warning
135
+ * (line number only — never line content, per SR1).
136
+ */
137
+ export function readEntries(ledgerPath) {
138
+ let raw;
139
+ try {
140
+ raw = readFileSync(ledgerPath, 'utf8');
141
+ }
142
+ catch (err) {
143
+ if (err.code === 'ENOENT')
144
+ return [];
145
+ throw err;
146
+ }
147
+ const entries = [];
148
+ const lines = raw.split('\n');
149
+ let skipped = 0;
150
+ for (let i = 0; i < lines.length; i++) {
151
+ const line = lines[i].trim();
152
+ if (line === '')
153
+ continue;
154
+ try {
155
+ const parsed = JSON.parse(line);
156
+ if (validateEntry(parsed) === null) {
157
+ entries.push(parsed);
158
+ }
159
+ else {
160
+ skipped++;
161
+ }
162
+ }
163
+ catch {
164
+ skipped++;
165
+ }
166
+ }
167
+ if (skipped > 0) {
168
+ process.stderr.write(`${ledgerPath}: skipped ${skipped} invalid line(s)\n`);
169
+ }
170
+ return entries;
171
+ }
172
+ /** Ledger path for an instance. */
173
+ export function ledgerPath(projectRoot, instance) {
174
+ return join(projectRoot, '.aidlc', 'state', instance, 'costs.ndjson');
175
+ }
176
+ /** Path of the unattributed ledger. */
177
+ export function unattributedPath(projectRoot) {
178
+ return join(projectRoot, '.aidlc', 'cost', 'unattributed.ndjson');
179
+ }
180
+ /**
181
+ * Newest cursor recorded for a session across the given ledger files
182
+ * (instance ledgers plus unattributed — design §2.4). "Newest" is by entry
183
+ * timestamp; ties resolved by later coverage.to.
184
+ */
185
+ export function newestCursor(session_id, ledgerPaths) {
186
+ let best = null;
187
+ for (const p of ledgerPaths) {
188
+ for (const e of readEntries(p)) {
189
+ if (e.session_id !== session_id || e.cursor === null)
190
+ continue;
191
+ if (best === null ||
192
+ Date.parse(e.timestamp) > Date.parse(best.timestamp) ||
193
+ (e.timestamp === best.timestamp && Date.parse(e.coverage.to) > Date.parse(best.coverage.to))) {
194
+ best = e;
195
+ }
196
+ }
197
+ }
198
+ return best?.cursor ?? null;
199
+ }
200
+ // ---------------------------------------------------------------------------
201
+ // Read-time fidelity precedence (design §4.5)
202
+ // ---------------------------------------------------------------------------
203
+ const FIDELITY_RANK = {
204
+ metered: 3,
205
+ reconciled: 2,
206
+ estimated: 1,
207
+ unknown: 0,
208
+ };
209
+ /** Merge windows into a sorted, non-overlapping list. */
210
+ function mergeWindows(windows) {
211
+ const sorted = [...windows].sort((a, b) => a.from - b.from);
212
+ const out = [];
213
+ for (const w of sorted) {
214
+ const last = out[out.length - 1];
215
+ if (last && w.from <= last.to) {
216
+ last.to = Math.max(last.to, w.to);
217
+ }
218
+ else {
219
+ out.push({ ...w });
220
+ }
221
+ }
222
+ return out;
223
+ }
224
+ /** Fraction of [from, to] NOT overlapped by any window in `covered`. */
225
+ function uncoveredFraction(from, to, covered) {
226
+ const span = to - from;
227
+ if (span <= 0) {
228
+ // Zero-length coverage: counted fully unless a window contains the point.
229
+ return covered.some((w) => w.from <= from && from <= w.to) ? 0 : 1;
230
+ }
231
+ let overlapped = 0;
232
+ for (const w of covered) {
233
+ const lo = Math.max(from, w.from);
234
+ const hi = Math.min(to, w.to);
235
+ if (hi > lo)
236
+ overlapped += hi - lo;
237
+ }
238
+ return Math.max(0, (span - overlapped) / span);
239
+ }
240
+ /**
241
+ * Apply read-time fidelity precedence: each entry's weight is the fraction
242
+ * of its coverage window not overlapped by higher-fidelity coverage for the
243
+ * same session. `session_id: null` entries always count in full. `unknown`
244
+ * entries carry no units and get weight 0.
245
+ *
246
+ * Linear sweep over per-session sorted window lists — O(n log n).
247
+ */
248
+ export function applyPrecedence(entries) {
249
+ const bySession = new Map();
250
+ const out = [];
251
+ for (const e of entries) {
252
+ if (e.units === null) {
253
+ out.push({ entry: e, weight: 0 });
254
+ continue;
255
+ }
256
+ if (e.session_id === null) {
257
+ out.push({ entry: e, weight: 1 });
258
+ continue;
259
+ }
260
+ const list = bySession.get(e.session_id) ?? [];
261
+ list.push(e);
262
+ bySession.set(e.session_id, list);
263
+ }
264
+ for (const list of bySession.values()) {
265
+ // Pre-merge windows per fidelity rank for this session.
266
+ const windowsByRank = new Map();
267
+ for (const e of list) {
268
+ const rank = FIDELITY_RANK[e.fidelity];
269
+ const w = windowsByRank.get(rank) ?? [];
270
+ w.push({ from: Date.parse(e.coverage.from), to: Date.parse(e.coverage.to) });
271
+ windowsByRank.set(rank, w);
272
+ }
273
+ const mergedByRank = new Map();
274
+ for (const [rank, ws] of windowsByRank)
275
+ mergedByRank.set(rank, mergeWindows(ws));
276
+ for (const e of list) {
277
+ const rank = FIDELITY_RANK[e.fidelity];
278
+ const higher = [];
279
+ for (const [r, ws] of mergedByRank) {
280
+ if (r > rank)
281
+ higher.push(...ws);
282
+ }
283
+ const weight = uncoveredFraction(Date.parse(e.coverage.from), Date.parse(e.coverage.to), mergeWindows(higher));
284
+ out.push({ entry: e, weight });
285
+ }
286
+ }
287
+ return out;
288
+ }
289
+ export function totalUsage(weighted) {
290
+ const totals = {
291
+ tokens: new Map(),
292
+ credits: 0,
293
+ fidelityWeights: { metered: 0, reconciled: 0, estimated: 0, unknown: 0 },
294
+ };
295
+ for (const { entry, weight } of weighted) {
296
+ if (weight === 0 || entry.units === null)
297
+ continue;
298
+ totals.fidelityWeights[entry.fidelity] += weight;
299
+ const u = entry.units;
300
+ if (u.kind === 'tokens') {
301
+ const t = totals.tokens.get(u.model) ?? { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 };
302
+ t.input += u.input_tokens * weight;
303
+ t.output += u.output_tokens * weight;
304
+ t.cacheRead += u.cache_read_tokens * weight;
305
+ t.cacheWrite += u.cache_write_tokens * weight;
306
+ totals.tokens.set(u.model, t);
307
+ }
308
+ else {
309
+ totals.credits += u.credits * weight;
310
+ }
311
+ }
312
+ return totals;
313
+ }
314
+ //# sourceMappingURL=ledger.js.map