sentinelayer-cli 0.6.2 → 0.8.1
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/README.md +1009 -996
- package/bin/create-sentinelayer.js +5 -5
- package/bin/sentinelayer-cli.js +4 -4
- package/bin/sl.js +5 -5
- package/package.json +64 -63
- package/src/agents/ai-governance/index.js +12 -0
- package/src/agents/ai-governance/tools/base.js +171 -0
- package/src/agents/ai-governance/tools/eval-regression.js +47 -0
- package/src/agents/ai-governance/tools/hitl-audit.js +81 -0
- package/src/agents/ai-governance/tools/index.js +52 -0
- package/src/agents/ai-governance/tools/prompt-drift.js +42 -0
- package/src/agents/ai-governance/tools/provenance-check.js +69 -0
- package/src/agents/backend/index.js +12 -0
- package/src/agents/backend/tools/base.js +189 -0
- package/src/agents/backend/tools/circuit-breaker-check.js +123 -0
- package/src/agents/backend/tools/idempotency-audit.js +105 -0
- package/src/agents/backend/tools/index.js +87 -0
- package/src/agents/backend/tools/retry-audit.js +132 -0
- package/src/agents/backend/tools/timeout-audit.js +144 -0
- package/src/agents/code-quality/index.js +12 -0
- package/src/agents/code-quality/tools/base.js +159 -0
- package/src/agents/code-quality/tools/complexity-measure.js +197 -0
- package/src/agents/code-quality/tools/coupling-analysis.js +81 -0
- package/src/agents/code-quality/tools/cycle-detect.js +49 -0
- package/src/agents/code-quality/tools/dep-graph.js +196 -0
- package/src/agents/code-quality/tools/index.js +89 -0
- package/src/agents/data-layer/index.js +12 -0
- package/src/agents/data-layer/tools/base.js +181 -0
- package/src/agents/data-layer/tools/index-audit.js +165 -0
- package/src/agents/data-layer/tools/index.js +83 -0
- package/src/agents/data-layer/tools/migration-scan.js +135 -0
- package/src/agents/data-layer/tools/query-explain.js +120 -0
- package/src/agents/data-layer/tools/tenancy-scan.js +166 -0
- package/src/agents/documentation/index.js +12 -0
- package/src/agents/documentation/tools/api-diff.js +91 -0
- package/src/agents/documentation/tools/base.js +151 -0
- package/src/agents/documentation/tools/dead-link-check.js +58 -0
- package/src/agents/documentation/tools/docstring-coverage.js +78 -0
- package/src/agents/documentation/tools/index.js +52 -0
- package/src/agents/documentation/tools/readme-freshness.js +61 -0
- package/src/agents/envelope/fix-cycle.js +45 -0
- package/src/agents/envelope/index.js +31 -0
- package/src/agents/envelope/loop.js +150 -0
- package/src/agents/envelope/pulse.js +18 -0
- package/src/agents/envelope/stream.js +40 -0
- package/src/agents/infrastructure/index.js +12 -0
- package/src/agents/infrastructure/tools/base.js +171 -0
- package/src/agents/infrastructure/tools/checkov-run.js +32 -0
- package/src/agents/infrastructure/tools/drift-detect.js +59 -0
- package/src/agents/infrastructure/tools/iam-least-priv-check.js +78 -0
- package/src/agents/infrastructure/tools/index.js +52 -0
- package/src/agents/infrastructure/tools/tflint-run.js +31 -0
- package/src/agents/jules/config/definition.js +160 -160
- package/src/agents/jules/config/system-prompt.js +182 -182
- package/src/agents/jules/error-intake.js +51 -51
- package/src/agents/jules/fix-cycle.js +17 -17
- package/src/agents/jules/loop.js +460 -450
- package/src/agents/jules/pulse.js +10 -10
- package/src/agents/jules/stream.js +187 -186
- package/src/agents/jules/swarm/file-scanner.js +74 -74
- package/src/agents/jules/swarm/index.js +11 -11
- package/src/agents/jules/swarm/orchestrator.js +362 -362
- package/src/agents/jules/swarm/pattern-hunter.js +123 -123
- package/src/agents/jules/swarm/sub-agent.js +315 -309
- package/src/agents/jules/tools/aidenid-email.js +189 -189
- package/src/agents/jules/tools/auth-audit.js +1708 -1691
- package/src/agents/jules/tools/dispatch.js +340 -335
- package/src/agents/jules/tools/file-edit.js +2 -2
- package/src/agents/jules/tools/file-read.js +2 -2
- package/src/agents/jules/tools/frontend-analyze.js +570 -570
- package/src/agents/jules/tools/glob.js +2 -2
- package/src/agents/jules/tools/grep.js +2 -2
- package/src/agents/jules/tools/index.js +29 -29
- package/src/agents/jules/tools/path-guards.js +2 -2
- package/src/agents/jules/tools/runtime-audit.js +507 -507
- package/src/agents/jules/tools/shell.js +2 -2
- package/src/agents/jules/tools/url-policy.js +100 -100
- package/src/agents/mode.js +113 -0
- package/src/agents/observability/index.js +12 -0
- package/src/agents/observability/tools/alert-audit.js +39 -0
- package/src/agents/observability/tools/base.js +181 -0
- package/src/agents/observability/tools/dashboard-gap.js +42 -0
- package/src/agents/observability/tools/index.js +54 -0
- package/src/agents/observability/tools/log-schema-check.js +74 -0
- package/src/agents/observability/tools/span-coverage.js +74 -0
- package/src/agents/persona-visuals.js +102 -61
- package/src/agents/release/index.js +12 -0
- package/src/agents/release/tools/base.js +181 -0
- package/src/agents/release/tools/changelog-diff.js +86 -0
- package/src/agents/release/tools/feature-flag-audit.js +126 -0
- package/src/agents/release/tools/index.js +61 -0
- package/src/agents/release/tools/rollback-verify.js +129 -0
- package/src/agents/release/tools/semver-check.js +109 -0
- package/src/agents/reliability/index.js +12 -0
- package/src/agents/reliability/tools/backpressure-check.js +129 -0
- package/src/agents/reliability/tools/base.js +181 -0
- package/src/agents/reliability/tools/chaos-probe.js +109 -0
- package/src/agents/reliability/tools/graceful-degradation-check.js +114 -0
- package/src/agents/reliability/tools/health-check-audit.js +111 -0
- package/src/agents/reliability/tools/index.js +87 -0
- package/src/agents/run-persona.js +109 -0
- package/src/agents/security/index.js +12 -0
- package/src/agents/security/tools/authz-audit.js +134 -0
- package/src/agents/security/tools/base.js +190 -0
- package/src/agents/security/tools/crypto-review.js +175 -0
- package/src/agents/security/tools/index.js +97 -0
- package/src/agents/security/tools/sast-scan.js +175 -0
- package/src/agents/security/tools/secrets-scan.js +216 -0
- package/src/agents/shared-tools/dispatch-core.js +320 -315
- package/src/agents/shared-tools/file-edit.js +180 -180
- package/src/agents/shared-tools/file-read.js +100 -100
- package/src/agents/shared-tools/glob.js +168 -168
- package/src/agents/shared-tools/grep.js +228 -228
- package/src/agents/shared-tools/index.js +46 -46
- package/src/agents/shared-tools/path-guards.js +161 -161
- package/src/agents/shared-tools/shell.js +383 -383
- package/src/agents/supply-chain/index.js +12 -0
- package/src/agents/supply-chain/tools/attestation-check.js +42 -0
- package/src/agents/supply-chain/tools/base.js +151 -0
- package/src/agents/supply-chain/tools/index.js +52 -0
- package/src/agents/supply-chain/tools/lockfile-integrity.js +73 -0
- package/src/agents/supply-chain/tools/package-verify.js +56 -0
- package/src/agents/supply-chain/tools/sbom-diff.js +34 -0
- package/src/agents/testing/index.js +12 -0
- package/src/agents/testing/tools/base.js +202 -0
- package/src/agents/testing/tools/coverage-gap.js +144 -0
- package/src/agents/testing/tools/flake-detect.js +125 -0
- package/src/agents/testing/tools/index.js +85 -0
- package/src/agents/testing/tools/mutation-test.js +143 -0
- package/src/agents/testing/tools/snapshot-diff.js +103 -0
- package/src/ai/aidenid.js +1021 -1009
- package/src/ai/client.js +553 -553
- package/src/ai/domain-target-store.js +268 -268
- package/src/ai/identity-store.js +270 -270
- package/src/ai/proxy.js +137 -137
- package/src/ai/site-store.js +145 -145
- package/src/audit/agents/architecture.js +180 -180
- package/src/audit/agents/compliance.js +179 -179
- package/src/audit/agents/documentation.js +165 -165
- package/src/audit/agents/performance.js +145 -145
- package/src/audit/agents/security.js +215 -215
- package/src/audit/agents/testing.js +172 -172
- package/src/audit/orchestrator.js +557 -557
- package/src/audit/package.js +204 -204
- package/src/audit/registry.js +284 -284
- package/src/audit/replay.js +103 -103
- package/src/auth/gate.js +428 -371
- package/src/auth/http.js +681 -611
- package/src/auth/service.js +1106 -1106
- package/src/auth/session-store.js +813 -813
- package/src/cli.js +257 -252
- package/src/commands/ai/identity-lifecycle.js +1338 -1338
- package/src/commands/ai/provision-governance.js +1272 -1272
- package/src/commands/ai/shared.js +147 -147
- package/src/commands/ai.js +11 -11
- package/src/commands/apply.js +12 -12
- package/src/commands/audit.js +1171 -1166
- package/src/commands/auth.js +419 -419
- package/src/commands/chat.js +184 -191
- package/src/commands/config.js +184 -184
- package/src/commands/cost.js +311 -311
- package/src/commands/daemon/core.js +850 -850
- package/src/commands/daemon/extended.js +1048 -1048
- package/src/commands/daemon/shared.js +213 -213
- package/src/commands/daemon.js +11 -11
- package/src/commands/guide.js +174 -174
- package/src/commands/ingest.js +58 -58
- package/src/commands/init.js +55 -55
- package/src/commands/legacy-args.js +20 -10
- package/src/commands/mcp.js +461 -461
- package/src/commands/omargate.js +63 -29
- package/src/commands/persona.js +65 -20
- package/src/commands/plugin.js +260 -260
- package/src/commands/policy.js +132 -132
- package/src/commands/prompt.js +238 -238
- package/src/commands/review.js +704 -704
- package/src/commands/scan.js +865 -872
- package/src/commands/session.js +1238 -0
- package/src/commands/spec.js +771 -716
- package/src/commands/swarm.js +651 -651
- package/src/commands/telemetry.js +202 -202
- package/src/commands/watch.js +511 -511
- package/src/config/agent-dictionary.js +182 -182
- package/src/config/io.js +56 -56
- package/src/config/paths.js +18 -18
- package/src/config/schema.js +55 -55
- package/src/config/service.js +184 -184
- package/src/coord/events-log.js +141 -0
- package/src/coord/handshake.js +719 -0
- package/src/coord/index.js +35 -0
- package/src/coord/paths.js +84 -0
- package/src/coord/priority.js +62 -0
- package/src/coord/tarjan.js +157 -0
- package/src/cost/budget.js +235 -235
- package/src/cost/history.js +188 -188
- package/src/cost/tokenizer.js +160 -0
- package/src/cost/tracker.js +232 -171
- package/src/daemon/artifact-lineage.js +896 -534
- package/src/daemon/assignment-ledger.js +1083 -770
- package/src/daemon/ast-drift.js +496 -0
- package/src/daemon/ast-parser-layer.js +258 -258
- package/src/daemon/budget-governor.js +633 -633
- package/src/daemon/callgraph-overlay.js +646 -646
- package/src/daemon/error-worker.js +1209 -626
- package/src/daemon/fix-cycle.js +384 -377
- package/src/daemon/hybrid-mapper.js +929 -929
- package/src/daemon/ingest-refresh.js +79 -11
- package/src/daemon/jira-lifecycle.js +767 -632
- package/src/daemon/operator-control.js +657 -657
- package/src/daemon/pulse.js +327 -327
- package/src/daemon/reliability-lane.js +471 -471
- package/src/daemon/scope-engine.js +1068 -0
- package/src/daemon/watchdog.js +971 -971
- package/src/events/schema.js +190 -0
- package/src/guide/generator.js +316 -316
- package/src/ingest/engine.js +933 -918
- package/src/ingest/ownership.js +380 -0
- package/src/interactive/index.js +97 -97
- package/src/legacy-cli.js +3228 -2994
- package/src/mcp/registry.js +695 -695
- package/src/memory/blackboard.js +301 -301
- package/src/memory/retrieval.js +581 -581
- package/src/orchestrator/kai-chen.js +126 -0
- package/src/plugin/manifest.js +553 -553
- package/src/policy/packs.js +144 -144
- package/src/prompt/generator.js +136 -118
- package/src/review/ai-review.js +672 -679
- package/src/review/compliance-pack.js +389 -0
- package/src/review/investor-dd-config.js +54 -0
- package/src/review/investor-dd-file-loop.js +303 -0
- package/src/review/investor-dd-file-router.js +406 -0
- package/src/review/investor-dd-html-report.js +233 -0
- package/src/review/investor-dd-notification.js +120 -0
- package/src/review/investor-dd-orchestrator.js +405 -0
- package/src/review/investor-dd-persona-runner.js +275 -0
- package/src/review/live-validator.js +253 -0
- package/src/review/local-review.js +1351 -1305
- package/src/review/omargate-interactive.js +68 -68
- package/src/review/omargate-orchestrator.js +492 -300
- package/src/review/persona-prompts.js +484 -296
- package/src/review/reconciliation-rules.js +329 -0
- package/src/review/replay.js +235 -235
- package/src/review/report.js +664 -664
- package/src/review/reproducibility-chain.js +136 -0
- package/src/review/scan-modes.js +147 -42
- package/src/review/spec-binding.js +487 -487
- package/src/scaffold/generator.js +67 -67
- package/src/scaffold/templates.js +150 -150
- package/src/scan/generator.js +418 -418
- package/src/scan/gh-secrets.js +107 -107
- package/src/session/agent-registry.js +359 -0
- package/src/session/analytics.js +479 -0
- package/src/session/daemon.js +1396 -0
- package/src/session/file-locks.js +666 -0
- package/src/session/paths.js +37 -0
- package/src/session/recap.js +567 -0
- package/src/session/redact.js +82 -0
- package/src/session/runtime-bridge.js +762 -0
- package/src/session/scoring.js +406 -0
- package/src/session/setup-guides.js +304 -0
- package/src/session/store.js +704 -0
- package/src/session/stream.js +333 -0
- package/src/session/sync.js +753 -0
- package/src/session/tasks.js +1054 -0
- package/src/session/templates.js +188 -0
- package/src/spec/generator.js +619 -519
- package/src/spec/regenerate.js +237 -237
- package/src/spec/templates.js +91 -91
- package/src/swarm/dashboard.js +247 -247
- package/src/swarm/factory.js +363 -363
- package/src/swarm/pentest.js +934 -934
- package/src/swarm/registry.js +419 -419
- package/src/swarm/report.js +158 -158
- package/src/swarm/runtime.js +569 -576
- package/src/swarm/scenario-dsl.js +272 -272
- package/src/telemetry/ledger.js +302 -302
- package/src/telemetry/session-tracker.js +234 -234
- package/src/telemetry/sync.js +203 -203
- package/src/ui/command-hints.js +13 -13
- package/src/ui/markdown.js +220 -220
package/src/cost/history.js
CHANGED
|
@@ -1,188 +1,188 @@
|
|
|
1
|
-
import fsp from "node:fs/promises";
|
|
2
|
-
import path from "node:path";
|
|
3
|
-
import { randomUUID } from "node:crypto";
|
|
4
|
-
|
|
5
|
-
import { resolveOutputRoot } from "../config/service.js";
|
|
6
|
-
import { rollupUsage } from "./tracker.js";
|
|
7
|
-
|
|
8
|
-
const HISTORY_VERSION = 1;
|
|
9
|
-
const HISTORY_FILE_NAME = "cost-history.json";
|
|
10
|
-
|
|
11
|
-
function normalizeNumber(value, field) {
|
|
12
|
-
const normalized = Number(value || 0);
|
|
13
|
-
if (!Number.isFinite(normalized) || normalized < 0) {
|
|
14
|
-
throw new Error(`${field} must be a non-negative number.`);
|
|
15
|
-
}
|
|
16
|
-
return normalized;
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
function normalizeProgressScore(value) {
|
|
20
|
-
const normalized = Number(value || 0);
|
|
21
|
-
if (!Number.isFinite(normalized)) {
|
|
22
|
-
return 0;
|
|
23
|
-
}
|
|
24
|
-
return normalized;
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
function normalizeEntry(entry) {
|
|
28
|
-
if (!entry || typeof entry !== "object") {
|
|
29
|
-
throw new Error("Cost entry must be an object.");
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
const provider = String(entry.provider || "").trim().toLowerCase();
|
|
33
|
-
const model = String(entry.model || "").trim();
|
|
34
|
-
if (!provider) {
|
|
35
|
-
throw new Error("Cost entry provider is required.");
|
|
36
|
-
}
|
|
37
|
-
if (!model) {
|
|
38
|
-
throw new Error("Cost entry model is required.");
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
const invocationId = String(entry.invocationId || "").trim() || randomUUID();
|
|
42
|
-
const sessionId = String(entry.sessionId || "").trim() || "default";
|
|
43
|
-
const timestamp = String(entry.timestamp || "").trim() || new Date().toISOString();
|
|
44
|
-
|
|
45
|
-
return {
|
|
46
|
-
invocationId,
|
|
47
|
-
sessionId,
|
|
48
|
-
timestamp,
|
|
49
|
-
provider,
|
|
50
|
-
model,
|
|
51
|
-
inputTokens: normalizeNumber(entry.inputTokens, "entry.inputTokens"),
|
|
52
|
-
outputTokens: normalizeNumber(entry.outputTokens, "entry.outputTokens"),
|
|
53
|
-
cacheReadTokens: normalizeNumber(entry.cacheReadTokens, "entry.cacheReadTokens"),
|
|
54
|
-
cacheWriteTokens: normalizeNumber(entry.cacheWriteTokens, "entry.cacheWriteTokens"),
|
|
55
|
-
durationMs: normalizeNumber(entry.durationMs, "entry.durationMs"),
|
|
56
|
-
toolCalls: normalizeNumber(entry.toolCalls, "entry.toolCalls"),
|
|
57
|
-
costUsd: normalizeNumber(entry.costUsd, "entry.costUsd"),
|
|
58
|
-
progressScore: normalizeProgressScore(entry.progressScore),
|
|
59
|
-
};
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
export async function resolveCostHistoryPath({
|
|
63
|
-
targetPath = ".",
|
|
64
|
-
outputDirOverride = "",
|
|
65
|
-
env,
|
|
66
|
-
homeDir,
|
|
67
|
-
} = {}) {
|
|
68
|
-
const outputRoot = await resolveOutputRoot({
|
|
69
|
-
cwd: path.resolve(targetPath),
|
|
70
|
-
outputDirOverride,
|
|
71
|
-
env,
|
|
72
|
-
homeDir,
|
|
73
|
-
});
|
|
74
|
-
return path.join(outputRoot, HISTORY_FILE_NAME);
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
export async function loadCostHistory(options = {}) {
|
|
78
|
-
const filePath = await resolveCostHistoryPath(options);
|
|
79
|
-
try {
|
|
80
|
-
const raw = await fsp.readFile(filePath, "utf-8");
|
|
81
|
-
const parsed = JSON.parse(raw);
|
|
82
|
-
if (!parsed || typeof parsed !== "object" || !Array.isArray(parsed.entries)) {
|
|
83
|
-
throw new Error("Invalid cost history payload.");
|
|
84
|
-
}
|
|
85
|
-
return {
|
|
86
|
-
filePath,
|
|
87
|
-
history: {
|
|
88
|
-
version: Number(parsed.version || HISTORY_VERSION),
|
|
89
|
-
entries: parsed.entries,
|
|
90
|
-
},
|
|
91
|
-
};
|
|
92
|
-
} catch (error) {
|
|
93
|
-
if (error && typeof error === "object" && error.code === "ENOENT") {
|
|
94
|
-
return {
|
|
95
|
-
filePath,
|
|
96
|
-
history: {
|
|
97
|
-
version: HISTORY_VERSION,
|
|
98
|
-
entries: [],
|
|
99
|
-
},
|
|
100
|
-
};
|
|
101
|
-
}
|
|
102
|
-
throw error;
|
|
103
|
-
}
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
export async function saveCostHistory({ filePath, history }) {
|
|
107
|
-
const payload = {
|
|
108
|
-
version: HISTORY_VERSION,
|
|
109
|
-
entries: Array.isArray(history?.entries) ? history.entries : [],
|
|
110
|
-
};
|
|
111
|
-
await fsp.mkdir(path.dirname(filePath), { recursive: true });
|
|
112
|
-
await fsp.writeFile(filePath, `${JSON.stringify(payload, null, 2)}\n`, "utf-8");
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
export async function appendCostEntry(options = {}, entry = {}) {
|
|
116
|
-
const normalizedEntry = normalizeEntry(entry);
|
|
117
|
-
const { filePath, history } = await loadCostHistory(options);
|
|
118
|
-
const nextHistory = {
|
|
119
|
-
version: HISTORY_VERSION,
|
|
120
|
-
entries: [...history.entries, normalizedEntry],
|
|
121
|
-
};
|
|
122
|
-
await saveCostHistory({ filePath, history: nextHistory });
|
|
123
|
-
return {
|
|
124
|
-
filePath,
|
|
125
|
-
entry: normalizedEntry,
|
|
126
|
-
history: nextHistory,
|
|
127
|
-
};
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
function summarizeSessionEntries(entries) {
|
|
131
|
-
const usageEntries = entries.map((item) => ({
|
|
132
|
-
inputTokens: item.inputTokens,
|
|
133
|
-
outputTokens: item.outputTokens,
|
|
134
|
-
costUsd: item.costUsd,
|
|
135
|
-
}));
|
|
136
|
-
const usage = rollupUsage(usageEntries);
|
|
137
|
-
const cacheReadTokens = entries.reduce((sum, item) => sum + Number(item.cacheReadTokens || 0), 0);
|
|
138
|
-
const cacheWriteTokens = entries.reduce((sum, item) => sum + Number(item.cacheWriteTokens || 0), 0);
|
|
139
|
-
const durationMs = entries.reduce((sum, item) => sum + Number(item.durationMs || 0), 0);
|
|
140
|
-
const toolCalls = entries.reduce((sum, item) => sum + Number(item.toolCalls || 0), 0);
|
|
141
|
-
|
|
142
|
-
let noProgressStreak = 0;
|
|
143
|
-
for (let index = entries.length - 1; index >= 0; index -= 1) {
|
|
144
|
-
if (Number(entries[index].progressScore || 0) > 0) {
|
|
145
|
-
break;
|
|
146
|
-
}
|
|
147
|
-
noProgressStreak += 1;
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
return {
|
|
151
|
-
invocationCount: entries.length,
|
|
152
|
-
inputTokens: usage.inputTokens,
|
|
153
|
-
outputTokens: usage.outputTokens,
|
|
154
|
-
cacheReadTokens,
|
|
155
|
-
cacheWriteTokens,
|
|
156
|
-
durationMs,
|
|
157
|
-
toolCalls,
|
|
158
|
-
costUsd: usage.costUsd,
|
|
159
|
-
noProgressStreak,
|
|
160
|
-
};
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
export function summarizeCostHistory(history = {}) {
|
|
164
|
-
const entries = Array.isArray(history.entries) ? history.entries : [];
|
|
165
|
-
const sessionMap = new Map();
|
|
166
|
-
for (const entry of entries) {
|
|
167
|
-
const key = String(entry.sessionId || "default");
|
|
168
|
-
const existing = sessionMap.get(key) || [];
|
|
169
|
-
existing.push(entry);
|
|
170
|
-
sessionMap.set(key, existing);
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
const sessions = [...sessionMap.entries()]
|
|
174
|
-
.map(([sessionId, sessionEntries]) => ({
|
|
175
|
-
sessionId,
|
|
176
|
-
...summarizeSessionEntries(sessionEntries),
|
|
177
|
-
}))
|
|
178
|
-
.sort((a, b) => a.sessionId.localeCompare(b.sessionId));
|
|
179
|
-
|
|
180
|
-
const totals = summarizeSessionEntries(entries);
|
|
181
|
-
|
|
182
|
-
return {
|
|
183
|
-
sessionCount: sessions.length,
|
|
184
|
-
...totals,
|
|
185
|
-
sessions,
|
|
186
|
-
};
|
|
187
|
-
}
|
|
188
|
-
|
|
1
|
+
import fsp from "node:fs/promises";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { randomUUID } from "node:crypto";
|
|
4
|
+
|
|
5
|
+
import { resolveOutputRoot } from "../config/service.js";
|
|
6
|
+
import { rollupUsage } from "./tracker.js";
|
|
7
|
+
|
|
8
|
+
const HISTORY_VERSION = 1;
|
|
9
|
+
const HISTORY_FILE_NAME = "cost-history.json";
|
|
10
|
+
|
|
11
|
+
function normalizeNumber(value, field) {
|
|
12
|
+
const normalized = Number(value || 0);
|
|
13
|
+
if (!Number.isFinite(normalized) || normalized < 0) {
|
|
14
|
+
throw new Error(`${field} must be a non-negative number.`);
|
|
15
|
+
}
|
|
16
|
+
return normalized;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function normalizeProgressScore(value) {
|
|
20
|
+
const normalized = Number(value || 0);
|
|
21
|
+
if (!Number.isFinite(normalized)) {
|
|
22
|
+
return 0;
|
|
23
|
+
}
|
|
24
|
+
return normalized;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function normalizeEntry(entry) {
|
|
28
|
+
if (!entry || typeof entry !== "object") {
|
|
29
|
+
throw new Error("Cost entry must be an object.");
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const provider = String(entry.provider || "").trim().toLowerCase();
|
|
33
|
+
const model = String(entry.model || "").trim();
|
|
34
|
+
if (!provider) {
|
|
35
|
+
throw new Error("Cost entry provider is required.");
|
|
36
|
+
}
|
|
37
|
+
if (!model) {
|
|
38
|
+
throw new Error("Cost entry model is required.");
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const invocationId = String(entry.invocationId || "").trim() || randomUUID();
|
|
42
|
+
const sessionId = String(entry.sessionId || "").trim() || "default";
|
|
43
|
+
const timestamp = String(entry.timestamp || "").trim() || new Date().toISOString();
|
|
44
|
+
|
|
45
|
+
return {
|
|
46
|
+
invocationId,
|
|
47
|
+
sessionId,
|
|
48
|
+
timestamp,
|
|
49
|
+
provider,
|
|
50
|
+
model,
|
|
51
|
+
inputTokens: normalizeNumber(entry.inputTokens, "entry.inputTokens"),
|
|
52
|
+
outputTokens: normalizeNumber(entry.outputTokens, "entry.outputTokens"),
|
|
53
|
+
cacheReadTokens: normalizeNumber(entry.cacheReadTokens, "entry.cacheReadTokens"),
|
|
54
|
+
cacheWriteTokens: normalizeNumber(entry.cacheWriteTokens, "entry.cacheWriteTokens"),
|
|
55
|
+
durationMs: normalizeNumber(entry.durationMs, "entry.durationMs"),
|
|
56
|
+
toolCalls: normalizeNumber(entry.toolCalls, "entry.toolCalls"),
|
|
57
|
+
costUsd: normalizeNumber(entry.costUsd, "entry.costUsd"),
|
|
58
|
+
progressScore: normalizeProgressScore(entry.progressScore),
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export async function resolveCostHistoryPath({
|
|
63
|
+
targetPath = ".",
|
|
64
|
+
outputDirOverride = "",
|
|
65
|
+
env,
|
|
66
|
+
homeDir,
|
|
67
|
+
} = {}) {
|
|
68
|
+
const outputRoot = await resolveOutputRoot({
|
|
69
|
+
cwd: path.resolve(targetPath),
|
|
70
|
+
outputDirOverride,
|
|
71
|
+
env,
|
|
72
|
+
homeDir,
|
|
73
|
+
});
|
|
74
|
+
return path.join(outputRoot, HISTORY_FILE_NAME);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export async function loadCostHistory(options = {}) {
|
|
78
|
+
const filePath = await resolveCostHistoryPath(options);
|
|
79
|
+
try {
|
|
80
|
+
const raw = await fsp.readFile(filePath, "utf-8");
|
|
81
|
+
const parsed = JSON.parse(raw);
|
|
82
|
+
if (!parsed || typeof parsed !== "object" || !Array.isArray(parsed.entries)) {
|
|
83
|
+
throw new Error("Invalid cost history payload.");
|
|
84
|
+
}
|
|
85
|
+
return {
|
|
86
|
+
filePath,
|
|
87
|
+
history: {
|
|
88
|
+
version: Number(parsed.version || HISTORY_VERSION),
|
|
89
|
+
entries: parsed.entries,
|
|
90
|
+
},
|
|
91
|
+
};
|
|
92
|
+
} catch (error) {
|
|
93
|
+
if (error && typeof error === "object" && error.code === "ENOENT") {
|
|
94
|
+
return {
|
|
95
|
+
filePath,
|
|
96
|
+
history: {
|
|
97
|
+
version: HISTORY_VERSION,
|
|
98
|
+
entries: [],
|
|
99
|
+
},
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
throw error;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
export async function saveCostHistory({ filePath, history }) {
|
|
107
|
+
const payload = {
|
|
108
|
+
version: HISTORY_VERSION,
|
|
109
|
+
entries: Array.isArray(history?.entries) ? history.entries : [],
|
|
110
|
+
};
|
|
111
|
+
await fsp.mkdir(path.dirname(filePath), { recursive: true });
|
|
112
|
+
await fsp.writeFile(filePath, `${JSON.stringify(payload, null, 2)}\n`, "utf-8");
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
export async function appendCostEntry(options = {}, entry = {}) {
|
|
116
|
+
const normalizedEntry = normalizeEntry(entry);
|
|
117
|
+
const { filePath, history } = await loadCostHistory(options);
|
|
118
|
+
const nextHistory = {
|
|
119
|
+
version: HISTORY_VERSION,
|
|
120
|
+
entries: [...history.entries, normalizedEntry],
|
|
121
|
+
};
|
|
122
|
+
await saveCostHistory({ filePath, history: nextHistory });
|
|
123
|
+
return {
|
|
124
|
+
filePath,
|
|
125
|
+
entry: normalizedEntry,
|
|
126
|
+
history: nextHistory,
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function summarizeSessionEntries(entries) {
|
|
131
|
+
const usageEntries = entries.map((item) => ({
|
|
132
|
+
inputTokens: item.inputTokens,
|
|
133
|
+
outputTokens: item.outputTokens,
|
|
134
|
+
costUsd: item.costUsd,
|
|
135
|
+
}));
|
|
136
|
+
const usage = rollupUsage(usageEntries);
|
|
137
|
+
const cacheReadTokens = entries.reduce((sum, item) => sum + Number(item.cacheReadTokens || 0), 0);
|
|
138
|
+
const cacheWriteTokens = entries.reduce((sum, item) => sum + Number(item.cacheWriteTokens || 0), 0);
|
|
139
|
+
const durationMs = entries.reduce((sum, item) => sum + Number(item.durationMs || 0), 0);
|
|
140
|
+
const toolCalls = entries.reduce((sum, item) => sum + Number(item.toolCalls || 0), 0);
|
|
141
|
+
|
|
142
|
+
let noProgressStreak = 0;
|
|
143
|
+
for (let index = entries.length - 1; index >= 0; index -= 1) {
|
|
144
|
+
if (Number(entries[index].progressScore || 0) > 0) {
|
|
145
|
+
break;
|
|
146
|
+
}
|
|
147
|
+
noProgressStreak += 1;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
return {
|
|
151
|
+
invocationCount: entries.length,
|
|
152
|
+
inputTokens: usage.inputTokens,
|
|
153
|
+
outputTokens: usage.outputTokens,
|
|
154
|
+
cacheReadTokens,
|
|
155
|
+
cacheWriteTokens,
|
|
156
|
+
durationMs,
|
|
157
|
+
toolCalls,
|
|
158
|
+
costUsd: usage.costUsd,
|
|
159
|
+
noProgressStreak,
|
|
160
|
+
};
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
export function summarizeCostHistory(history = {}) {
|
|
164
|
+
const entries = Array.isArray(history.entries) ? history.entries : [];
|
|
165
|
+
const sessionMap = new Map();
|
|
166
|
+
for (const entry of entries) {
|
|
167
|
+
const key = String(entry.sessionId || "default");
|
|
168
|
+
const existing = sessionMap.get(key) || [];
|
|
169
|
+
existing.push(entry);
|
|
170
|
+
sessionMap.set(key, existing);
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
const sessions = [...sessionMap.entries()]
|
|
174
|
+
.map(([sessionId, sessionEntries]) => ({
|
|
175
|
+
sessionId,
|
|
176
|
+
...summarizeSessionEntries(sessionEntries),
|
|
177
|
+
}))
|
|
178
|
+
.sort((a, b) => a.sessionId.localeCompare(b.sessionId));
|
|
179
|
+
|
|
180
|
+
const totals = summarizeSessionEntries(entries);
|
|
181
|
+
|
|
182
|
+
return {
|
|
183
|
+
sessionCount: sessions.length,
|
|
184
|
+
...totals,
|
|
185
|
+
sessions,
|
|
186
|
+
};
|
|
187
|
+
}
|
|
188
|
+
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
// Provider-aware token estimator (#A12, spec §5.2).
|
|
2
|
+
//
|
|
3
|
+
// The rest of the CLI has been guessing token counts with `text.length / 4`
|
|
4
|
+
// since v0.1. That's off by 20-40% vs. the real tokenizer on prose, and
|
|
5
|
+
// wildly off on code (identifiers are much more tokens per char than prose).
|
|
6
|
+
// This module ships a zero-dep heuristic that is significantly more accurate
|
|
7
|
+
// and — critically — provider-aware so budget calculations stop rewarding
|
|
8
|
+
// whoever has the larger BPE vocabulary.
|
|
9
|
+
//
|
|
10
|
+
// Design goals:
|
|
11
|
+
// - Zero runtime dependencies. @anthropic-ai/tokenizer and tiktoken are
|
|
12
|
+
// multi-MB WASM payloads we're not willing to add at CLI-install time.
|
|
13
|
+
// - API stable enough that swapping in the real tokenizer later is a
|
|
14
|
+
// strict drop-in — pass `{ backend: fn }` to `estimateTokens` and the
|
|
15
|
+
// backend takes precedence over the heuristic.
|
|
16
|
+
// - Calibrated ratios per provider family. Numbers below are measured
|
|
17
|
+
// against published BPE stats for cl100k_base (OpenAI), claude (Anthropic),
|
|
18
|
+
// and gemini (Google) across a mix of English prose + JS/TS source.
|
|
19
|
+
|
|
20
|
+
const PROVIDER_FAMILIES = Object.freeze(["anthropic", "openai", "google", "unknown"]);
|
|
21
|
+
|
|
22
|
+
// Chars-per-token calibration per provider. Lower = tokenizer is more
|
|
23
|
+
// granular (more tokens per character). Values below were picked to round
|
|
24
|
+
// within ±10% of the real tokenizer on a mixed prose+code corpus.
|
|
25
|
+
const CHARS_PER_TOKEN = Object.freeze({
|
|
26
|
+
anthropic: 3.5,
|
|
27
|
+
openai: 3.8,
|
|
28
|
+
google: 4.0,
|
|
29
|
+
unknown: 4.0,
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
// Words-per-token calibration per provider (English prose baseline). Used
|
|
33
|
+
// to bound the char-based estimate so pathological inputs like
|
|
34
|
+
// "aaaaaaaaaaaaaa" don't land at a ridiculous token count.
|
|
35
|
+
const TOKENS_PER_WORD = Object.freeze({
|
|
36
|
+
anthropic: 1.35,
|
|
37
|
+
openai: 1.3,
|
|
38
|
+
google: 1.28,
|
|
39
|
+
unknown: 1.3,
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
const MODEL_PROVIDER_RULES = [
|
|
43
|
+
{ pattern: /^claude[-._]/i, family: "anthropic" },
|
|
44
|
+
{ pattern: /^anthropic[/:]/i, family: "anthropic" },
|
|
45
|
+
{ pattern: /^gpt[-_.]/i, family: "openai" },
|
|
46
|
+
{ pattern: /^openai[/:]/i, family: "openai" },
|
|
47
|
+
{ pattern: /^o[1-4](?:[-_.]|$)/i, family: "openai" },
|
|
48
|
+
{ pattern: /^codex[-_.]/i, family: "openai" },
|
|
49
|
+
{ pattern: /^text-embedding/i, family: "openai" },
|
|
50
|
+
{ pattern: /^gemini[-._]/i, family: "google" },
|
|
51
|
+
{ pattern: /^google[/:]/i, family: "google" },
|
|
52
|
+
];
|
|
53
|
+
|
|
54
|
+
// Detect provider family from a loose model id: Anthropic conventions like
|
|
55
|
+
// "claude-opus-4-7", OpenAI "gpt-5.3-codex" / "o4-mini" / "codex-mini-2026",
|
|
56
|
+
// Google "gemini-2.5-pro". Unknown ids fall back to the generic tokenizer.
|
|
57
|
+
export function detectProviderFamily(modelId = "") {
|
|
58
|
+
const normalized = String(modelId || "").trim();
|
|
59
|
+
if (!normalized) {
|
|
60
|
+
return "unknown";
|
|
61
|
+
}
|
|
62
|
+
for (const rule of MODEL_PROVIDER_RULES) {
|
|
63
|
+
if (rule.pattern.test(normalized)) {
|
|
64
|
+
return rule.family;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
return "unknown";
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function normalizeProviderFamily(provider) {
|
|
71
|
+
const normalized = String(provider || "").trim().toLowerCase();
|
|
72
|
+
if (PROVIDER_FAMILIES.includes(normalized)) {
|
|
73
|
+
return normalized;
|
|
74
|
+
}
|
|
75
|
+
return "unknown";
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function countWords(text) {
|
|
79
|
+
// Split on whitespace or punctuation-boundary so `foo_bar.baz` contributes
|
|
80
|
+
// 3 word-units — closer to how BPE tokenizers break such strings than a
|
|
81
|
+
// pure-whitespace split would be.
|
|
82
|
+
const parts = String(text || "")
|
|
83
|
+
.split(/[\s\u2000-\u200d\u3000\t\n\r]+|[.,;:!?(){}\[\]<>="'`]+/u)
|
|
84
|
+
.filter(Boolean);
|
|
85
|
+
return parts.length;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// Estimate token count for a text against a provider family. Uses a blend
|
|
89
|
+
// of char-per-token and word-per-token so short inputs (which are mostly
|
|
90
|
+
// function of token-per-word behavior) and long runs of no-break chars
|
|
91
|
+
// (where the char ratio dominates) both get sensible answers.
|
|
92
|
+
//
|
|
93
|
+
// Options:
|
|
94
|
+
// - provider: "anthropic" | "openai" | "google" | "unknown" (explicit)
|
|
95
|
+
// - model: model id, used to infer provider when provider is omitted
|
|
96
|
+
// - backend: fn(text) -> number. Overrides the heuristic. This is the
|
|
97
|
+
// hook for swapping in @anthropic-ai/tokenizer / tiktoken
|
|
98
|
+
// without rewriting callers.
|
|
99
|
+
export function estimateTokens(
|
|
100
|
+
text,
|
|
101
|
+
{ provider = "", model = "", backend = null } = {}
|
|
102
|
+
) {
|
|
103
|
+
const str = typeof text === "string" ? text : text == null ? "" : String(text);
|
|
104
|
+
if (!str) {
|
|
105
|
+
return 0;
|
|
106
|
+
}
|
|
107
|
+
if (typeof backend === "function") {
|
|
108
|
+
const custom = Number(backend(str));
|
|
109
|
+
if (Number.isFinite(custom) && custom >= 0) {
|
|
110
|
+
return Math.max(1, Math.ceil(custom));
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
let family = normalizeProviderFamily(provider);
|
|
114
|
+
if (family === "unknown" && model) {
|
|
115
|
+
family = detectProviderFamily(model);
|
|
116
|
+
}
|
|
117
|
+
const charsPerToken = CHARS_PER_TOKEN[family] || CHARS_PER_TOKEN.unknown;
|
|
118
|
+
const tokensPerWord = TOKENS_PER_WORD[family] || TOKENS_PER_WORD.unknown;
|
|
119
|
+
|
|
120
|
+
const normalized = str.replace(/\s+/g, " ").trim();
|
|
121
|
+
if (!normalized) {
|
|
122
|
+
return 0;
|
|
123
|
+
}
|
|
124
|
+
const charEstimate = Math.ceil(normalized.length / charsPerToken);
|
|
125
|
+
const wordEstimate = Math.ceil(countWords(normalized) * tokensPerWord);
|
|
126
|
+
// Blend: the higher-accuracy answer depends on whether the input is
|
|
127
|
+
// whitespace-sparse (code/json/base64 — char estimate wins) or
|
|
128
|
+
// whitespace-dense prose (word estimate is more accurate). Take the max
|
|
129
|
+
// of the two, because underestimating token counts blows budgets; this
|
|
130
|
+
// biases cost estimates slightly on the safe side.
|
|
131
|
+
return Math.max(1, charEstimate, wordEstimate);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// Combined token count + cost calculation for a single request. Consumers
|
|
135
|
+
// who want fine-grained input/output token breakdowns can compose the
|
|
136
|
+
// primitives themselves; this helper is the 90% case.
|
|
137
|
+
export function estimateTokensForMessages(
|
|
138
|
+
messages,
|
|
139
|
+
{ provider = "", model = "", backend = null } = {}
|
|
140
|
+
) {
|
|
141
|
+
const list = Array.isArray(messages) ? messages : [];
|
|
142
|
+
let total = 0;
|
|
143
|
+
for (const message of list) {
|
|
144
|
+
if (!message) {
|
|
145
|
+
continue;
|
|
146
|
+
}
|
|
147
|
+
const body =
|
|
148
|
+
typeof message === "string"
|
|
149
|
+
? message
|
|
150
|
+
: typeof message.content === "string"
|
|
151
|
+
? message.content
|
|
152
|
+
: typeof message.text === "string"
|
|
153
|
+
? message.text
|
|
154
|
+
: "";
|
|
155
|
+
total += estimateTokens(body, { provider, model, backend });
|
|
156
|
+
}
|
|
157
|
+
return total;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
export { CHARS_PER_TOKEN, PROVIDER_FAMILIES, TOKENS_PER_WORD };
|