@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.
- package/dist/cli.js +4 -0
- package/dist/cli.js.map +1 -1
- package/dist/commands/claim.d.ts +15 -0
- package/dist/commands/claim.d.ts.map +1 -0
- package/dist/commands/claim.js +122 -0
- package/dist/commands/claim.js.map +1 -0
- package/dist/commands/cost.d.ts +12 -0
- package/dist/commands/cost.d.ts.map +1 -0
- package/dist/commands/cost.js +409 -0
- package/dist/commands/cost.js.map +1 -0
- package/dist/commands/gate.d.ts.map +1 -1
- package/dist/commands/gate.js +17 -0
- package/dist/commands/gate.js.map +1 -1
- package/dist/commands/init.d.ts.map +1 -1
- package/dist/commands/init.js +38 -2
- package/dist/commands/init.js.map +1 -1
- package/dist/commands/status.d.ts.map +1 -1
- package/dist/commands/status.js +59 -2
- package/dist/commands/status.js.map +1 -1
- package/dist/commands/transition.d.ts.map +1 -1
- package/dist/commands/transition.js +30 -0
- package/dist/commands/transition.js.map +1 -1
- package/dist/core/gate.d.ts +7 -0
- package/dist/core/gate.d.ts.map +1 -1
- package/dist/core/gate.js.map +1 -1
- package/dist/core/types.d.ts +18 -1
- package/dist/core/types.d.ts.map +1 -1
- package/dist/cost/config.d.ts +8 -0
- package/dist/cost/config.d.ts.map +1 -0
- package/dist/cost/config.js +37 -0
- package/dist/cost/config.js.map +1 -0
- package/dist/cost/hooks/claude-code.d.ts +29 -0
- package/dist/cost/hooks/claude-code.d.ts.map +1 -0
- package/dist/cost/hooks/claude-code.js +101 -0
- package/dist/cost/hooks/claude-code.js.map +1 -0
- package/dist/cost/hooks/kiro.d.ts +17 -0
- package/dist/cost/hooks/kiro.d.ts.map +1 -0
- package/dist/cost/hooks/kiro.js +66 -0
- package/dist/cost/hooks/kiro.js.map +1 -0
- package/dist/cost/ledger.d.ts +70 -0
- package/dist/cost/ledger.d.ts.map +1 -0
- package/dist/cost/ledger.js +314 -0
- package/dist/cost/ledger.js.map +1 -0
- package/dist/cost/providers/claude-code.d.ts +20 -0
- package/dist/cost/providers/claude-code.d.ts.map +1 -0
- package/dist/cost/providers/claude-code.js +116 -0
- package/dist/cost/providers/claude-code.js.map +1 -0
- package/dist/cost/providers/kiro.d.ts +19 -0
- package/dist/cost/providers/kiro.d.ts.map +1 -0
- package/dist/cost/providers/kiro.js +147 -0
- package/dist/cost/providers/kiro.js.map +1 -0
- package/dist/cost/providers/provider.d.ts +36 -0
- package/dist/cost/providers/provider.d.ts.map +1 -0
- package/dist/cost/providers/provider.js +21 -0
- package/dist/cost/providers/provider.js.map +1 -0
- package/dist/cost/rates-bundled.d.ts +11 -0
- package/dist/cost/rates-bundled.d.ts.map +1 -0
- package/dist/cost/rates-bundled.js +23 -0
- package/dist/cost/rates-bundled.js.map +1 -0
- package/dist/cost/rates.d.ts +42 -0
- package/dist/cost/rates.d.ts.map +1 -0
- package/dist/cost/rates.js +190 -0
- package/dist/cost/rates.js.map +1 -0
- package/dist/cost/recorder.d.ts +57 -0
- package/dist/cost/recorder.d.ts.map +1 -0
- package/dist/cost/recorder.js +339 -0
- package/dist/cost/recorder.js.map +1 -0
- package/dist/cost/report.d.ts +44 -0
- package/dist/cost/report.d.ts.map +1 -0
- package/dist/cost/report.js +164 -0
- package/dist/cost/report.js.map +1 -0
- package/dist/cost/sessions.d.ts +40 -0
- package/dist/cost/sessions.d.ts.map +1 -0
- package/dist/cost/sessions.js +200 -0
- package/dist/cost/sessions.js.map +1 -0
- package/dist/cost/sync.d.ts +24 -0
- package/dist/cost/sync.d.ts.map +1 -0
- package/dist/cost/sync.js +191 -0
- package/dist/cost/sync.js.map +1 -0
- package/dist/cost/types.d.ts +116 -0
- package/dist/cost/types.d.ts.map +1 -0
- package/dist/cost/types.js +13 -0
- package/dist/cost/types.js.map +1 -0
- package/dist/setup/setup-compute.d.ts +6 -0
- package/dist/setup/setup-compute.d.ts.map +1 -1
- package/dist/setup/setup-compute.js +3 -0
- package/dist/setup/setup-compute.js.map +1 -1
- package/dist/traceability/cache.d.ts +21 -0
- package/dist/traceability/cache.d.ts.map +1 -0
- package/dist/traceability/cache.js +106 -0
- package/dist/traceability/cache.js.map +1 -0
- package/dist/traceability/coverage-block.d.ts +21 -0
- package/dist/traceability/coverage-block.d.ts.map +1 -0
- package/dist/traceability/coverage-block.js +41 -0
- package/dist/traceability/coverage-block.js.map +1 -0
- package/dist/traceability/gate-criterion.d.ts +34 -0
- package/dist/traceability/gate-criterion.d.ts.map +1 -0
- package/dist/traceability/gate-criterion.js +128 -0
- package/dist/traceability/gate-criterion.js.map +1 -0
- package/dist/traceability/matrix.d.ts +35 -0
- package/dist/traceability/matrix.d.ts.map +1 -0
- package/dist/traceability/matrix.js +93 -0
- package/dist/traceability/matrix.js.map +1 -0
- package/dist/traceability/parser.d.ts +27 -0
- package/dist/traceability/parser.d.ts.map +1 -0
- package/dist/traceability/parser.js +101 -0
- package/dist/traceability/parser.js.map +1 -0
- package/dist/traceability/scanner.d.ts +62 -0
- package/dist/traceability/scanner.d.ts.map +1 -0
- package/dist/traceability/scanner.js +198 -0
- package/dist/traceability/scanner.js.map +1 -0
- package/dist/traceability/types.d.ts +77 -0
- package/dist/traceability/types.d.ts.map +1 -0
- package/dist/traceability/types.js +8 -0
- package/dist/traceability/types.js.map +1 -0
- package/package.json +3 -5
- package/dist/commands/setup.d.ts +0 -24
- package/dist/commands/setup.d.ts.map +0 -1
- package/dist/commands/setup.js +0 -262
- 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
|