sentinelayer-cli 0.1.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/README.md +996 -0
- package/bin/create-sentinelayer.js +5 -0
- package/bin/sentinelayer-cli.js +5 -0
- package/bin/sl.js +5 -0
- package/package.json +54 -0
- package/src/agents/jules/config/definition.js +209 -0
- package/src/agents/jules/config/system-prompt.js +175 -0
- package/src/agents/jules/error-intake.js +51 -0
- package/src/agents/jules/fix-cycle.js +377 -0
- package/src/agents/jules/loop.js +367 -0
- package/src/agents/jules/pulse.js +319 -0
- package/src/agents/jules/stream.js +186 -0
- package/src/agents/jules/swarm/file-scanner.js +74 -0
- package/src/agents/jules/swarm/index.js +11 -0
- package/src/agents/jules/swarm/orchestrator.js +362 -0
- package/src/agents/jules/swarm/pattern-hunter.js +123 -0
- package/src/agents/jules/swarm/sub-agent.js +308 -0
- package/src/agents/jules/tools/auth-audit.js +222 -0
- package/src/agents/jules/tools/dispatch.js +327 -0
- package/src/agents/jules/tools/file-edit.js +180 -0
- package/src/agents/jules/tools/file-read.js +100 -0
- package/src/agents/jules/tools/frontend-analyze.js +570 -0
- package/src/agents/jules/tools/glob.js +168 -0
- package/src/agents/jules/tools/grep.js +228 -0
- package/src/agents/jules/tools/index.js +29 -0
- package/src/agents/jules/tools/path-guards.js +161 -0
- package/src/agents/jules/tools/runtime-audit.js +409 -0
- package/src/agents/jules/tools/shell.js +383 -0
- package/src/ai/aidenid.js +945 -0
- package/src/ai/client.js +508 -0
- package/src/ai/domain-target-store.js +268 -0
- package/src/ai/identity-store.js +270 -0
- package/src/ai/site-store.js +145 -0
- package/src/audit/agents/architecture.js +180 -0
- package/src/audit/agents/compliance.js +179 -0
- package/src/audit/agents/documentation.js +165 -0
- package/src/audit/agents/performance.js +145 -0
- package/src/audit/agents/security.js +215 -0
- package/src/audit/agents/testing.js +172 -0
- package/src/audit/orchestrator.js +557 -0
- package/src/audit/package.js +204 -0
- package/src/audit/registry.js +284 -0
- package/src/audit/replay.js +103 -0
- package/src/auth/http.js +113 -0
- package/src/auth/service.js +848 -0
- package/src/auth/session-store.js +345 -0
- package/src/cli.js +244 -0
- package/src/commands/ai/identity-lifecycle.js +1337 -0
- package/src/commands/ai/provision-governance.js +1246 -0
- package/src/commands/ai/shared.js +147 -0
- package/src/commands/ai.js +11 -0
- package/src/commands/apply.js +19 -0
- package/src/commands/audit.js +1147 -0
- package/src/commands/auth.js +366 -0
- package/src/commands/chat.js +191 -0
- package/src/commands/config.js +184 -0
- package/src/commands/cost.js +311 -0
- package/src/commands/daemon/core.js +850 -0
- package/src/commands/daemon/extended.js +1048 -0
- package/src/commands/daemon/shared.js +213 -0
- package/src/commands/daemon.js +11 -0
- package/src/commands/guide.js +174 -0
- package/src/commands/ingest.js +58 -0
- package/src/commands/init.js +55 -0
- package/src/commands/legacy-args.js +30 -0
- package/src/commands/mcp.js +404 -0
- package/src/commands/omargate.js +21 -0
- package/src/commands/persona.js +27 -0
- package/src/commands/plugin.js +260 -0
- package/src/commands/policy.js +132 -0
- package/src/commands/prompt.js +238 -0
- package/src/commands/review.js +704 -0
- package/src/commands/scan.js +788 -0
- package/src/commands/spec.js +716 -0
- package/src/commands/swarm.js +651 -0
- package/src/commands/telemetry.js +202 -0
- package/src/commands/watch.js +510 -0
- package/src/config/agent-dictionary.js +182 -0
- package/src/config/io.js +56 -0
- package/src/config/paths.js +18 -0
- package/src/config/schema.js +55 -0
- package/src/config/service.js +184 -0
- package/src/cost/budget.js +235 -0
- package/src/cost/history.js +188 -0
- package/src/cost/tracker.js +171 -0
- package/src/daemon/artifact-lineage.js +534 -0
- package/src/daemon/assignment-ledger.js +770 -0
- package/src/daemon/ast-parser-layer.js +258 -0
- package/src/daemon/budget-governor.js +633 -0
- package/src/daemon/callgraph-overlay.js +646 -0
- package/src/daemon/error-worker.js +626 -0
- package/src/daemon/hybrid-mapper.js +929 -0
- package/src/daemon/jira-lifecycle.js +632 -0
- package/src/daemon/operator-control.js +657 -0
- package/src/daemon/reliability-lane.js +471 -0
- package/src/daemon/watchdog.js +971 -0
- package/src/guide/generator.js +316 -0
- package/src/ingest/engine.js +918 -0
- package/src/legacy-cli.js +2435 -0
- package/src/mcp/registry.js +695 -0
- package/src/memory/blackboard.js +301 -0
- package/src/memory/retrieval.js +581 -0
- package/src/plugin/manifest.js +553 -0
- package/src/policy/packs.js +144 -0
- package/src/prompt/generator.js +106 -0
- package/src/review/ai-review.js +669 -0
- package/src/review/local-review.js +1284 -0
- package/src/review/replay.js +235 -0
- package/src/review/report.js +664 -0
- package/src/review/spec-binding.js +487 -0
- package/src/scan/generator.js +351 -0
- package/src/spec/generator.js +519 -0
- package/src/spec/regenerate.js +237 -0
- package/src/spec/templates.js +91 -0
- package/src/swarm/dashboard.js +247 -0
- package/src/swarm/factory.js +363 -0
- package/src/swarm/pentest.js +934 -0
- package/src/swarm/registry.js +419 -0
- package/src/swarm/report.js +158 -0
- package/src/swarm/runtime.js +576 -0
- package/src/swarm/scenario-dsl.js +272 -0
- package/src/telemetry/ledger.js +302 -0
- package/src/ui/markdown.js +220 -0
- package/src/ui/progress.js +100 -0
|
@@ -0,0 +1,235 @@
|
|
|
1
|
+
function normalizeNumber(value, field) {
|
|
2
|
+
const normalized = Number(value || 0);
|
|
3
|
+
if (!Number.isFinite(normalized) || normalized < 0) {
|
|
4
|
+
throw new Error(`${field} must be a non-negative number.`);
|
|
5
|
+
}
|
|
6
|
+
return normalized;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
function formatNumber(value, decimals = 0) {
|
|
10
|
+
return Number(value || 0).toFixed(decimals);
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function normalizeWarningThresholdPercent(value) {
|
|
14
|
+
const normalized = Number(value || 0);
|
|
15
|
+
if (!Number.isFinite(normalized) || normalized < 0 || normalized > 100) {
|
|
16
|
+
throw new Error("warningThresholdPercent must be between 0 and 100.");
|
|
17
|
+
}
|
|
18
|
+
return normalized;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function collectLimitStatusWithThreshold({
|
|
22
|
+
reasons,
|
|
23
|
+
warnings,
|
|
24
|
+
usageValue,
|
|
25
|
+
limitValue,
|
|
26
|
+
warningThresholdPercent,
|
|
27
|
+
stopCode,
|
|
28
|
+
warningCode,
|
|
29
|
+
stopMessage,
|
|
30
|
+
warningMessage,
|
|
31
|
+
}) {
|
|
32
|
+
const normalizedUsage = normalizeNumber(usageValue, "usageValue");
|
|
33
|
+
const normalizedLimit = normalizeNumber(limitValue, "limitValue");
|
|
34
|
+
const normalizedWarningThresholdPercent =
|
|
35
|
+
normalizeWarningThresholdPercent(warningThresholdPercent);
|
|
36
|
+
|
|
37
|
+
if (normalizedLimit <= 0) {
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
if (normalizedUsage > normalizedLimit) {
|
|
42
|
+
reasons.push({
|
|
43
|
+
code: stopCode,
|
|
44
|
+
message: stopMessage(normalizedUsage, normalizedLimit),
|
|
45
|
+
});
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
if (normalizedWarningThresholdPercent <= 0) {
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const thresholdValue = (normalizedWarningThresholdPercent / 100) * normalizedLimit;
|
|
54
|
+
if (normalizedUsage >= thresholdValue) {
|
|
55
|
+
warnings.push({
|
|
56
|
+
code: warningCode,
|
|
57
|
+
message: warningMessage(normalizedUsage, normalizedLimit, normalizedWarningThresholdPercent),
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Evaluate runtime/cost/token/tool/no-progress budgets and return deterministic stop/warning signals.
|
|
64
|
+
*
|
|
65
|
+
* @param {{
|
|
66
|
+
* sessionSummary?: {
|
|
67
|
+
* costUsd?: number,
|
|
68
|
+
* outputTokens?: number,
|
|
69
|
+
* noProgressStreak?: number,
|
|
70
|
+
* durationMs?: number,
|
|
71
|
+
* toolCalls?: number
|
|
72
|
+
* },
|
|
73
|
+
* maxCostUsd?: number,
|
|
74
|
+
* maxOutputTokens?: number,
|
|
75
|
+
* maxNoProgress?: number,
|
|
76
|
+
* maxRuntimeMs?: number,
|
|
77
|
+
* maxToolCalls?: number,
|
|
78
|
+
* warningThresholdPercent?: number
|
|
79
|
+
* }} [options]
|
|
80
|
+
* @returns {{
|
|
81
|
+
* blocking: boolean,
|
|
82
|
+
* warnings: Array<{ code: string, message: string }>,
|
|
83
|
+
* reasons: Array<{ code: string, message: string }>,
|
|
84
|
+
* limits: {
|
|
85
|
+
* maxCostUsd: number,
|
|
86
|
+
* maxOutputTokens: number,
|
|
87
|
+
* maxNoProgress: number,
|
|
88
|
+
* maxRuntimeMs: number,
|
|
89
|
+
* maxToolCalls: number,
|
|
90
|
+
* warningThresholdPercent: number
|
|
91
|
+
* },
|
|
92
|
+
* usage: {
|
|
93
|
+
* costUsd: number,
|
|
94
|
+
* outputTokens: number,
|
|
95
|
+
* noProgressStreak: number,
|
|
96
|
+
* runtimeMs: number,
|
|
97
|
+
* toolCalls: number
|
|
98
|
+
* }
|
|
99
|
+
* }}
|
|
100
|
+
*/
|
|
101
|
+
export function evaluateBudget({
|
|
102
|
+
sessionSummary = {},
|
|
103
|
+
maxCostUsd = 1.0,
|
|
104
|
+
maxOutputTokens = 0,
|
|
105
|
+
maxNoProgress = 3,
|
|
106
|
+
maxRuntimeMs = 0,
|
|
107
|
+
maxToolCalls = 0,
|
|
108
|
+
warningThresholdPercent = 80,
|
|
109
|
+
} = {}) {
|
|
110
|
+
const normalizedMaxCost = normalizeNumber(maxCostUsd, "maxCostUsd");
|
|
111
|
+
const normalizedMaxOutputTokens = normalizeNumber(maxOutputTokens, "maxOutputTokens");
|
|
112
|
+
const normalizedMaxNoProgress = Math.max(1, normalizeNumber(maxNoProgress, "maxNoProgress"));
|
|
113
|
+
const normalizedMaxRuntimeMs = normalizeNumber(maxRuntimeMs, "maxRuntimeMs");
|
|
114
|
+
const normalizedMaxToolCalls = normalizeNumber(maxToolCalls, "maxToolCalls");
|
|
115
|
+
const normalizedWarningThresholdPercent = normalizeWarningThresholdPercent(
|
|
116
|
+
warningThresholdPercent
|
|
117
|
+
);
|
|
118
|
+
|
|
119
|
+
const totalCostUsd = normalizeNumber(sessionSummary.costUsd || 0, "sessionSummary.costUsd");
|
|
120
|
+
const totalOutputTokens = normalizeNumber(
|
|
121
|
+
sessionSummary.outputTokens || 0,
|
|
122
|
+
"sessionSummary.outputTokens"
|
|
123
|
+
);
|
|
124
|
+
const noProgressStreak = normalizeNumber(
|
|
125
|
+
sessionSummary.noProgressStreak || 0,
|
|
126
|
+
"sessionSummary.noProgressStreak"
|
|
127
|
+
);
|
|
128
|
+
const totalRuntimeMs = normalizeNumber(sessionSummary.durationMs || 0, "sessionSummary.durationMs");
|
|
129
|
+
const totalToolCalls = normalizeNumber(
|
|
130
|
+
sessionSummary.toolCalls || 0,
|
|
131
|
+
"sessionSummary.toolCalls"
|
|
132
|
+
);
|
|
133
|
+
|
|
134
|
+
const reasons = [];
|
|
135
|
+
const warnings = [];
|
|
136
|
+
|
|
137
|
+
collectLimitStatusWithThreshold({
|
|
138
|
+
reasons,
|
|
139
|
+
warnings,
|
|
140
|
+
usageValue: totalCostUsd,
|
|
141
|
+
limitValue: normalizedMaxCost,
|
|
142
|
+
warningThresholdPercent: normalizedWarningThresholdPercent,
|
|
143
|
+
stopCode: "MAX_COST_EXCEEDED",
|
|
144
|
+
warningCode: "COST_BUDGET_NEAR_LIMIT",
|
|
145
|
+
stopMessage: (usage, limit) =>
|
|
146
|
+
`Cost budget exceeded (${formatNumber(usage, 6)} > ${formatNumber(limit, 6)}).`,
|
|
147
|
+
warningMessage: (usage, limit, thresholdPercent) =>
|
|
148
|
+
`Cost budget near limit (${formatNumber(usage, 6)} / ${formatNumber(limit, 6)} at ${formatNumber(
|
|
149
|
+
thresholdPercent,
|
|
150
|
+
0
|
|
151
|
+
)}%).`,
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
collectLimitStatusWithThreshold({
|
|
155
|
+
reasons,
|
|
156
|
+
warnings,
|
|
157
|
+
usageValue: totalOutputTokens,
|
|
158
|
+
limitValue: normalizedMaxOutputTokens,
|
|
159
|
+
warningThresholdPercent: normalizedWarningThresholdPercent,
|
|
160
|
+
stopCode: "MAX_OUTPUT_TOKENS_EXCEEDED",
|
|
161
|
+
warningCode: "OUTPUT_TOKENS_NEAR_LIMIT",
|
|
162
|
+
stopMessage: (usage, limit) =>
|
|
163
|
+
`Output token budget exceeded (${formatNumber(usage, 0)} > ${formatNumber(limit, 0)}).`,
|
|
164
|
+
warningMessage: (usage, limit, thresholdPercent) =>
|
|
165
|
+
`Output token budget near limit (${formatNumber(usage, 0)} / ${formatNumber(
|
|
166
|
+
limit,
|
|
167
|
+
0
|
|
168
|
+
)} at ${formatNumber(thresholdPercent, 0)}%).`,
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
collectLimitStatusWithThreshold({
|
|
172
|
+
reasons,
|
|
173
|
+
warnings,
|
|
174
|
+
usageValue: totalRuntimeMs,
|
|
175
|
+
limitValue: normalizedMaxRuntimeMs,
|
|
176
|
+
warningThresholdPercent: normalizedWarningThresholdPercent,
|
|
177
|
+
stopCode: "MAX_RUNTIME_MS_EXCEEDED",
|
|
178
|
+
warningCode: "RUNTIME_MS_NEAR_LIMIT",
|
|
179
|
+
stopMessage: (usage, limit) =>
|
|
180
|
+
`Runtime budget exceeded (${formatNumber(usage, 0)}ms > ${formatNumber(limit, 0)}ms).`,
|
|
181
|
+
warningMessage: (usage, limit, thresholdPercent) =>
|
|
182
|
+
`Runtime budget near limit (${formatNumber(usage, 0)}ms / ${formatNumber(
|
|
183
|
+
limit,
|
|
184
|
+
0
|
|
185
|
+
)}ms at ${formatNumber(thresholdPercent, 0)}%).`,
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
collectLimitStatusWithThreshold({
|
|
189
|
+
reasons,
|
|
190
|
+
warnings,
|
|
191
|
+
usageValue: totalToolCalls,
|
|
192
|
+
limitValue: normalizedMaxToolCalls,
|
|
193
|
+
warningThresholdPercent: normalizedWarningThresholdPercent,
|
|
194
|
+
stopCode: "MAX_TOOL_CALLS_EXCEEDED",
|
|
195
|
+
warningCode: "TOOL_CALLS_NEAR_LIMIT",
|
|
196
|
+
stopMessage: (usage, limit) =>
|
|
197
|
+
`Tool-call budget exceeded (${formatNumber(usage, 0)} > ${formatNumber(limit, 0)}).`,
|
|
198
|
+
warningMessage: (usage, limit, thresholdPercent) =>
|
|
199
|
+
`Tool-call budget near limit (${formatNumber(usage, 0)} / ${formatNumber(
|
|
200
|
+
limit,
|
|
201
|
+
0
|
|
202
|
+
)} at ${formatNumber(thresholdPercent, 0)}%).`,
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
if (noProgressStreak >= normalizedMaxNoProgress) {
|
|
206
|
+
reasons.push({
|
|
207
|
+
code: "DIMINISHING_RETURNS",
|
|
208
|
+
message: `No-progress streak reached ${formatNumber(noProgressStreak, 0)} (threshold ${formatNumber(
|
|
209
|
+
normalizedMaxNoProgress,
|
|
210
|
+
0
|
|
211
|
+
)}).`,
|
|
212
|
+
});
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
return {
|
|
216
|
+
blocking: reasons.length > 0,
|
|
217
|
+
warnings,
|
|
218
|
+
reasons,
|
|
219
|
+
limits: {
|
|
220
|
+
maxCostUsd: normalizedMaxCost,
|
|
221
|
+
maxOutputTokens: normalizedMaxOutputTokens,
|
|
222
|
+
maxNoProgress: normalizedMaxNoProgress,
|
|
223
|
+
maxRuntimeMs: normalizedMaxRuntimeMs,
|
|
224
|
+
maxToolCalls: normalizedMaxToolCalls,
|
|
225
|
+
warningThresholdPercent: normalizedWarningThresholdPercent,
|
|
226
|
+
},
|
|
227
|
+
usage: {
|
|
228
|
+
costUsd: totalCostUsd,
|
|
229
|
+
outputTokens: totalOutputTokens,
|
|
230
|
+
noProgressStreak,
|
|
231
|
+
runtimeMs: totalRuntimeMs,
|
|
232
|
+
toolCalls: totalToolCalls,
|
|
233
|
+
},
|
|
234
|
+
};
|
|
235
|
+
}
|
|
@@ -0,0 +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
|
+
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
const DEFAULT_MODEL_PRICING = Object.freeze({
|
|
2
|
+
"gpt-4o": Object.freeze({
|
|
3
|
+
inputPerMillionUsd: 2.5,
|
|
4
|
+
outputPerMillionUsd: 10.0,
|
|
5
|
+
}),
|
|
6
|
+
"gpt-5.3-codex": Object.freeze({
|
|
7
|
+
inputPerMillionUsd: 1.5,
|
|
8
|
+
outputPerMillionUsd: 6.0,
|
|
9
|
+
}),
|
|
10
|
+
"claude-sonnet-4": Object.freeze({
|
|
11
|
+
inputPerMillionUsd: 3.0,
|
|
12
|
+
outputPerMillionUsd: 15.0,
|
|
13
|
+
}),
|
|
14
|
+
"claude-sonnet-4.5": Object.freeze({
|
|
15
|
+
inputPerMillionUsd: 3.0,
|
|
16
|
+
outputPerMillionUsd: 15.0,
|
|
17
|
+
}),
|
|
18
|
+
"gemini-2.5-pro": Object.freeze({
|
|
19
|
+
inputPerMillionUsd: 2.5,
|
|
20
|
+
outputPerMillionUsd: 10.0,
|
|
21
|
+
}),
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
function normalizeTokenCount(value, field) {
|
|
25
|
+
const normalized = Number(value || 0);
|
|
26
|
+
if (!Number.isFinite(normalized) || normalized < 0) {
|
|
27
|
+
throw new Error(`${field} must be a non-negative number.`);
|
|
28
|
+
}
|
|
29
|
+
return normalized;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function normalizeUsd(value, field) {
|
|
33
|
+
const normalized = Number(value || 0);
|
|
34
|
+
if (!Number.isFinite(normalized) || normalized < 0) {
|
|
35
|
+
throw new Error(`${field} must be a non-negative number.`);
|
|
36
|
+
}
|
|
37
|
+
return normalized;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function roundUsd(value) {
|
|
41
|
+
return Math.round((Number(value || 0) + Number.EPSILON) * 1_000_000) / 1_000_000;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Estimate cost in USD from token counts and per-million pricing inputs.
|
|
46
|
+
*
|
|
47
|
+
* @param {{
|
|
48
|
+
* inputTokens?: number,
|
|
49
|
+
* outputTokens?: number,
|
|
50
|
+
* inputPerMillionUsd?: number,
|
|
51
|
+
* outputPerMillionUsd?: number
|
|
52
|
+
* }} [options]
|
|
53
|
+
* @returns {number}
|
|
54
|
+
*/
|
|
55
|
+
export function estimateCostUsd({
|
|
56
|
+
inputTokens = 0,
|
|
57
|
+
outputTokens = 0,
|
|
58
|
+
inputPerMillionUsd = 0,
|
|
59
|
+
outputPerMillionUsd = 0,
|
|
60
|
+
} = {}) {
|
|
61
|
+
const normalizedInputTokens = normalizeTokenCount(inputTokens, "inputTokens");
|
|
62
|
+
const normalizedOutputTokens = normalizeTokenCount(outputTokens, "outputTokens");
|
|
63
|
+
const normalizedInputRate = normalizeUsd(inputPerMillionUsd, "inputPerMillionUsd");
|
|
64
|
+
const normalizedOutputRate = normalizeUsd(outputPerMillionUsd, "outputPerMillionUsd");
|
|
65
|
+
|
|
66
|
+
const inputCost = (normalizedInputTokens / 1_000_000) * normalizedInputRate;
|
|
67
|
+
const outputCost = (normalizedOutputTokens / 1_000_000) * normalizedOutputRate;
|
|
68
|
+
|
|
69
|
+
return roundUsd(inputCost + outputCost);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Estimate cost in USD using a named model pricing table entry.
|
|
74
|
+
*
|
|
75
|
+
* @param {{
|
|
76
|
+
* modelId: string,
|
|
77
|
+
* inputTokens?: number,
|
|
78
|
+
* outputTokens?: number,
|
|
79
|
+
* pricingTable?: Record<string, { inputPerMillionUsd: number, outputPerMillionUsd: number }>
|
|
80
|
+
* }} [options]
|
|
81
|
+
* @returns {number}
|
|
82
|
+
*/
|
|
83
|
+
export function estimateModelCost({
|
|
84
|
+
modelId,
|
|
85
|
+
inputTokens = 0,
|
|
86
|
+
outputTokens = 0,
|
|
87
|
+
pricingTable = DEFAULT_MODEL_PRICING,
|
|
88
|
+
} = {}) {
|
|
89
|
+
const normalizedModelId = String(modelId || "").trim();
|
|
90
|
+
if (!normalizedModelId) {
|
|
91
|
+
throw new Error("modelId is required for model-based cost estimation.");
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
const modelPricing = pricingTable[normalizedModelId];
|
|
95
|
+
if (!modelPricing) {
|
|
96
|
+
throw new Error(`No pricing data configured for model '${normalizedModelId}'.`);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
return estimateCostUsd({
|
|
100
|
+
inputTokens,
|
|
101
|
+
outputTokens,
|
|
102
|
+
inputPerMillionUsd: modelPricing.inputPerMillionUsd,
|
|
103
|
+
outputPerMillionUsd: modelPricing.outputPerMillionUsd,
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Aggregate usage rows into a single token and cost summary.
|
|
109
|
+
*
|
|
110
|
+
* @param {Array<{ inputTokens?: number, outputTokens?: number, costUsd?: number }>} [entries]
|
|
111
|
+
* @returns {{ inputTokens: number, outputTokens: number, costUsd: number }}
|
|
112
|
+
*/
|
|
113
|
+
export function rollupUsage(entries = []) {
|
|
114
|
+
if (!Array.isArray(entries)) {
|
|
115
|
+
throw new Error("entries must be an array.");
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
const totals = entries.reduce(
|
|
119
|
+
(accumulator, entry) => {
|
|
120
|
+
const inputTokens = normalizeTokenCount(entry?.inputTokens || 0, "entry.inputTokens");
|
|
121
|
+
const outputTokens = normalizeTokenCount(entry?.outputTokens || 0, "entry.outputTokens");
|
|
122
|
+
const costUsd = normalizeUsd(entry?.costUsd || 0, "entry.costUsd");
|
|
123
|
+
|
|
124
|
+
return {
|
|
125
|
+
inputTokens: accumulator.inputTokens + inputTokens,
|
|
126
|
+
outputTokens: accumulator.outputTokens + outputTokens,
|
|
127
|
+
costUsd: accumulator.costUsd + costUsd,
|
|
128
|
+
};
|
|
129
|
+
},
|
|
130
|
+
{ inputTokens: 0, outputTokens: 0, costUsd: 0 }
|
|
131
|
+
);
|
|
132
|
+
|
|
133
|
+
return {
|
|
134
|
+
inputTokens: totals.inputTokens,
|
|
135
|
+
outputTokens: totals.outputTokens,
|
|
136
|
+
costUsd: roundUsd(totals.costUsd),
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Evaluate whether cumulative cost has exceeded a configured budget.
|
|
142
|
+
*
|
|
143
|
+
* @param {{ totalCostUsd?: number, budgetUsd?: number }} [options]
|
|
144
|
+
* @returns {{ budgetUsd: number, totalCostUsd: number, remainingUsd: number, exceeded: boolean }}
|
|
145
|
+
*/
|
|
146
|
+
export function enforceCostBudget({ totalCostUsd = 0, budgetUsd = 0 } = {}) {
|
|
147
|
+
const normalizedTotal = normalizeUsd(totalCostUsd, "totalCostUsd");
|
|
148
|
+
const normalizedBudget = normalizeUsd(budgetUsd, "budgetUsd");
|
|
149
|
+
const remainingUsd = roundUsd(Math.max(0, normalizedBudget - normalizedTotal));
|
|
150
|
+
|
|
151
|
+
return {
|
|
152
|
+
budgetUsd: roundUsd(normalizedBudget),
|
|
153
|
+
totalCostUsd: roundUsd(normalizedTotal),
|
|
154
|
+
remainingUsd,
|
|
155
|
+
exceeded: normalizedTotal > normalizedBudget,
|
|
156
|
+
};
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* Return the built-in model pricing catalog for diagnostics and UI display.
|
|
161
|
+
*
|
|
162
|
+
* @returns {Array<{ modelId: string, inputPerMillionUsd: number, outputPerMillionUsd: number }>}
|
|
163
|
+
*/
|
|
164
|
+
export function listKnownModelPricing() {
|
|
165
|
+
return Object.entries(DEFAULT_MODEL_PRICING).map(([modelId, pricing]) => ({
|
|
166
|
+
modelId,
|
|
167
|
+
inputPerMillionUsd: pricing.inputPerMillionUsd,
|
|
168
|
+
outputPerMillionUsd: pricing.outputPerMillionUsd,
|
|
169
|
+
}));
|
|
170
|
+
}
|
|
171
|
+
|