@agent-finops/core 0.5.4 → 0.5.6
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/LICENSE +21 -0
- package/README.md +16 -0
- package/dist/agentInventory.d.ts +35 -9
- package/dist/agentInventory.js +309 -11
- package/dist/analyze.js +6 -2
- package/dist/contextHealth.d.ts +101 -0
- package/dist/contextHealth.js +371 -0
- package/dist/deadContext.js +10 -1
- package/dist/discovery.d.ts +14 -1
- package/dist/discovery.js +54 -28
- package/dist/glance.d.ts +161 -0
- package/dist/glance.js +586 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +2 -0
- package/dist/localAgentLogs.d.ts +42 -2
- package/dist/localAgentLogs.js +460 -7
- package/dist/modelPricing.d.ts +2 -1
- package/dist/modelPricing.js +22 -3
- package/dist/planMath.d.ts +12 -14
- package/dist/planMath.js +18 -18
- package/dist/providerConnectors.d.ts +19 -1
- package/dist/providerConnectors.js +106 -26
- package/dist/scanGuard.d.ts +35 -0
- package/dist/scanGuard.js +196 -8
- package/dist/schema.d.ts +24 -24
- package/dist/sourceRegistry.js +1 -1
- package/dist/toolInvocations.d.ts +42 -1
- package/dist/toolInvocations.js +244 -4
- package/package.json +15 -2
package/dist/localAgentLogs.js
CHANGED
|
@@ -1,11 +1,19 @@
|
|
|
1
1
|
import { readdir, readFile, stat } from "node:fs/promises";
|
|
2
|
-
import { basename, join, resolve } from "node:path";
|
|
2
|
+
import { basename, isAbsolute, join, resolve, sep } from "node:path";
|
|
3
3
|
import { homedir } from "node:os";
|
|
4
4
|
import { estimateTokenCostUsd } from "./modelPricing.js";
|
|
5
|
+
import { redactSecrets } from "./discovery.js";
|
|
5
6
|
/** Parse one Claude Code transcript (JSONL). Exported for tests. */
|
|
6
7
|
export function parseClaudeCodeTranscript(content, filePath = "") {
|
|
7
8
|
const calls = [];
|
|
8
9
|
const seen = new Set();
|
|
10
|
+
const prompts = [];
|
|
11
|
+
const fileCounts = new Map();
|
|
12
|
+
let title;
|
|
13
|
+
let lastPrompt;
|
|
14
|
+
let toolCallCount = 0;
|
|
15
|
+
let isSubagent = filePath.split(sep).includes("subagents");
|
|
16
|
+
let parentSessionId;
|
|
9
17
|
for (const line of content.split("\n")) {
|
|
10
18
|
if (!line.trim())
|
|
11
19
|
continue;
|
|
@@ -16,9 +24,34 @@ export function parseClaudeCodeTranscript(content, filePath = "") {
|
|
|
16
24
|
catch {
|
|
17
25
|
continue;
|
|
18
26
|
}
|
|
19
|
-
if (!isRecord(entry)
|
|
27
|
+
if (!isRecord(entry))
|
|
20
28
|
continue;
|
|
29
|
+
if (entry.type === "ai-title") {
|
|
30
|
+
title = stringOf(entry.aiTitle) ?? title;
|
|
31
|
+
}
|
|
32
|
+
if (entry.type === "last-prompt") {
|
|
33
|
+
lastPrompt = stringOf(entry.lastPrompt) ?? lastPrompt;
|
|
34
|
+
}
|
|
35
|
+
if (entry.isSidechain === true)
|
|
36
|
+
isSubagent = true;
|
|
37
|
+
parentSessionId = stringOf(entry.parentSessionId) ?? parentSessionId;
|
|
21
38
|
const message = isRecord(entry.message) ? entry.message : undefined;
|
|
39
|
+
if (entry.type === "user" && message) {
|
|
40
|
+
for (const prompt of textValues(message.content)) {
|
|
41
|
+
if (isHumanPrompt(prompt))
|
|
42
|
+
prompts.push(prompt);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
if (entry.type === "assistant" && message) {
|
|
46
|
+
for (const item of recordValues(message.content)) {
|
|
47
|
+
if (item.type !== "tool_use")
|
|
48
|
+
continue;
|
|
49
|
+
toolCallCount += 1;
|
|
50
|
+
collectToolFiles(item.input, fileCounts);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
if (entry.type !== "assistant")
|
|
54
|
+
continue;
|
|
22
55
|
const usage = message && isRecord(message.usage) ? message.usage : undefined;
|
|
23
56
|
if (!message || !usage)
|
|
24
57
|
continue;
|
|
@@ -50,15 +83,37 @@ export function parseClaudeCodeTranscript(content, filePath = "") {
|
|
|
50
83
|
}
|
|
51
84
|
});
|
|
52
85
|
}
|
|
86
|
+
const fallbackPrompt = lastPrompt && isHumanPrompt(lastPrompt) ? lastPrompt : undefined;
|
|
87
|
+
const activity = buildLocalAgentActivity({
|
|
88
|
+
title,
|
|
89
|
+
prompts: prompts.length > 0 ? prompts : fallbackPrompt ? [fallbackPrompt] : [],
|
|
90
|
+
files: fileCounts,
|
|
91
|
+
toolCallCount,
|
|
92
|
+
project: calls[0]?.project ?? projectFromTranscriptPath(filePath),
|
|
93
|
+
isSubagent,
|
|
94
|
+
parentSessionId
|
|
95
|
+
});
|
|
96
|
+
if (activity) {
|
|
97
|
+
for (const call of calls)
|
|
98
|
+
call.activity = activity;
|
|
99
|
+
}
|
|
53
100
|
return calls;
|
|
54
101
|
}
|
|
55
102
|
/** Parse one Codex rollout file (JSONL event stream). Exported for tests. */
|
|
56
103
|
export function parseCodexRollout(content) {
|
|
57
104
|
let model;
|
|
58
105
|
let cwd;
|
|
106
|
+
const toolWorkdirs = new Map();
|
|
59
107
|
let sessionId;
|
|
60
|
-
let
|
|
108
|
+
let startedAt;
|
|
109
|
+
let lastActivityAt;
|
|
61
110
|
let lastTotal;
|
|
111
|
+
let lastRateLimits;
|
|
112
|
+
const prompts = [];
|
|
113
|
+
const fileCounts = new Map();
|
|
114
|
+
let toolCallCount = 0;
|
|
115
|
+
let isSubagent = false;
|
|
116
|
+
let parentSessionId;
|
|
62
117
|
for (const line of content.split("\n")) {
|
|
63
118
|
if (!line.trim())
|
|
64
119
|
continue;
|
|
@@ -75,18 +130,42 @@ export function parseCodexRollout(content) {
|
|
|
75
130
|
if (entry.type === "session_meta" && payload) {
|
|
76
131
|
sessionId = stringOf(payload.id) ?? sessionId;
|
|
77
132
|
cwd = stringOf(payload.cwd) ?? cwd;
|
|
78
|
-
|
|
133
|
+
startedAt = toIso(stringOf(payload.timestamp) ?? stringOf(entry.timestamp)) ?? startedAt;
|
|
134
|
+
isSubagent = stringOf(payload.thread_source) === "subagent" || isRecord(payload.source) && "subagent" in payload.source;
|
|
135
|
+
parentSessionId = stringOf(payload.parent_thread_id) ?? parentSessionId;
|
|
79
136
|
}
|
|
80
137
|
if (entry.type === "turn_context" && payload) {
|
|
81
138
|
model = stringOf(payload.model) ?? model;
|
|
82
139
|
cwd = stringOf(payload.cwd) ?? cwd;
|
|
83
140
|
}
|
|
141
|
+
if (payload?.type === "function_call" || payload?.type === "custom_tool_call") {
|
|
142
|
+
toolCallCount += 1;
|
|
143
|
+
const args = jsonRecord(stringOf(payload.arguments));
|
|
144
|
+
const workdir = stringOf(args?.workdir) ?? stringOf(args?.cwd);
|
|
145
|
+
if (workdir && isAbsolute(workdir)) {
|
|
146
|
+
const normalized = resolve(workdir);
|
|
147
|
+
toolWorkdirs.set(normalized, (toolWorkdirs.get(normalized) ?? 0) + 1);
|
|
148
|
+
}
|
|
149
|
+
collectToolFiles(args, fileCounts);
|
|
150
|
+
collectPatchFiles(stringOf(args?.patch) ?? stringOf(args?.input) ?? stringOf(payload.input), fileCounts);
|
|
151
|
+
}
|
|
152
|
+
if (payload?.type === "message" && payload.role === "user") {
|
|
153
|
+
for (const prompt of textValues(payload.content)) {
|
|
154
|
+
if (isHumanPrompt(prompt))
|
|
155
|
+
prompts.push(prompt);
|
|
156
|
+
}
|
|
157
|
+
}
|
|
84
158
|
if (entry.type === "event_msg" && payload?.type === "token_count") {
|
|
159
|
+
const eventTimestamp = toIso(stringOf(entry.timestamp)) ?? lastActivityAt ?? startedAt;
|
|
85
160
|
const info = isRecord(payload.info) ? payload.info : undefined;
|
|
86
161
|
const total = info && isRecord(info.total_token_usage) ? info.total_token_usage : undefined;
|
|
87
162
|
if (total) {
|
|
88
163
|
lastTotal = total;
|
|
89
|
-
|
|
164
|
+
lastActivityAt = eventTimestamp;
|
|
165
|
+
}
|
|
166
|
+
const rateLimits = parseCodexRateLimits(payload.rate_limits, eventTimestamp);
|
|
167
|
+
if (rateLimits) {
|
|
168
|
+
lastRateLimits = rateLimits;
|
|
90
169
|
}
|
|
91
170
|
}
|
|
92
171
|
}
|
|
@@ -94,12 +173,24 @@ export function parseCodexRollout(content) {
|
|
|
94
173
|
return [];
|
|
95
174
|
const input = numberOf(lastTotal.input_tokens) ?? 0;
|
|
96
175
|
const cached = numberOf(lastTotal.cached_input_tokens) ?? 0;
|
|
176
|
+
const project = projectFromCwd(dominantCodexCwd(cwd, toolWorkdirs));
|
|
177
|
+
const activity = buildLocalAgentActivity({
|
|
178
|
+
prompts,
|
|
179
|
+
files: fileCounts,
|
|
180
|
+
toolCallCount,
|
|
181
|
+
project,
|
|
182
|
+
isSubagent,
|
|
183
|
+
parentSessionId
|
|
184
|
+
});
|
|
97
185
|
return [{
|
|
98
186
|
agent: "codex",
|
|
99
187
|
model: model ?? "codex",
|
|
100
|
-
timestamp:
|
|
101
|
-
|
|
188
|
+
timestamp: lastActivityAt ?? startedAt ?? new Date(0).toISOString(),
|
|
189
|
+
startedAt,
|
|
190
|
+
project,
|
|
102
191
|
sessionId,
|
|
192
|
+
rateLimits: lastRateLimits,
|
|
193
|
+
activity,
|
|
103
194
|
usage: {
|
|
104
195
|
// Codex input_tokens INCLUDES cached tokens; split them out.
|
|
105
196
|
inputTokens: Math.max(0, input - cached),
|
|
@@ -108,6 +199,52 @@ export function parseCodexRollout(content) {
|
|
|
108
199
|
}
|
|
109
200
|
}];
|
|
110
201
|
}
|
|
202
|
+
function parseCodexRateLimits(value, observedAt) {
|
|
203
|
+
if (!isRecord(value) || !observedAt)
|
|
204
|
+
return undefined;
|
|
205
|
+
const windows = [value.primary, value.secondary]
|
|
206
|
+
.map((window) => parseCodexRateLimitWindow(window))
|
|
207
|
+
.filter((window) => Boolean(window));
|
|
208
|
+
if (windows.length === 0)
|
|
209
|
+
return undefined;
|
|
210
|
+
return {
|
|
211
|
+
observedAt,
|
|
212
|
+
limitId: stringOf(value.limit_id),
|
|
213
|
+
planType: stringOf(value.plan_type),
|
|
214
|
+
windows
|
|
215
|
+
};
|
|
216
|
+
}
|
|
217
|
+
function parseCodexRateLimitWindow(value) {
|
|
218
|
+
if (!isRecord(value))
|
|
219
|
+
return undefined;
|
|
220
|
+
const usedPercent = numberOf(value.used_percent);
|
|
221
|
+
const windowMinutes = numberOf(value.window_minutes);
|
|
222
|
+
const resetsAtSeconds = numberOf(value.resets_at);
|
|
223
|
+
if (usedPercent === undefined ||
|
|
224
|
+
windowMinutes === undefined ||
|
|
225
|
+
windowMinutes <= 0 ||
|
|
226
|
+
resetsAtSeconds === undefined) {
|
|
227
|
+
return undefined;
|
|
228
|
+
}
|
|
229
|
+
const resetMs = resetsAtSeconds < 1_000_000_000_000
|
|
230
|
+
? resetsAtSeconds * 1_000
|
|
231
|
+
: resetsAtSeconds;
|
|
232
|
+
const resetsAt = toIso(new Date(resetMs).toISOString());
|
|
233
|
+
if (!resetsAt)
|
|
234
|
+
return undefined;
|
|
235
|
+
const kind = windowMinutes === 300
|
|
236
|
+
? "five-hour"
|
|
237
|
+
: windowMinutes === 10_080
|
|
238
|
+
? "weekly"
|
|
239
|
+
: "custom";
|
|
240
|
+
return {
|
|
241
|
+
kind,
|
|
242
|
+
name: kind === "custom" ? `${windowMinutes}-minute` : kind,
|
|
243
|
+
usedPercent: Math.min(100, Math.max(0, usedPercent)),
|
|
244
|
+
windowMinutes,
|
|
245
|
+
resetsAt
|
|
246
|
+
};
|
|
247
|
+
}
|
|
111
248
|
/** Scan this machine's agent logs and return aggregated UsageRecords. */
|
|
112
249
|
export async function loadLocalAgentUsage(options = {}) {
|
|
113
250
|
const home = homedir();
|
|
@@ -215,6 +352,31 @@ function projectFromCwd(cwd) {
|
|
|
215
352
|
const name = basename(cwd);
|
|
216
353
|
return name.length > 0 ? name : undefined;
|
|
217
354
|
}
|
|
355
|
+
/**
|
|
356
|
+
* Codex can be launched from HOME and do nearly all of its work through tools
|
|
357
|
+
* that declare a more specific working directory. Prefer that observed
|
|
358
|
+
* activity only when the session-level cwd is not already a real project.
|
|
359
|
+
* Nested tool directories roll up to the shallowest observed ancestor with
|
|
360
|
+
* the strongest descendant-weighted activity.
|
|
361
|
+
*/
|
|
362
|
+
function dominantCodexCwd(sessionCwd, toolWorkdirs) {
|
|
363
|
+
const sessionProject = projectFromCwd(sessionCwd);
|
|
364
|
+
if (sessionProject && sessionProject !== "(home)")
|
|
365
|
+
return sessionCwd;
|
|
366
|
+
const home = resolve(homedir());
|
|
367
|
+
const candidates = [...toolWorkdirs.entries()]
|
|
368
|
+
.filter(([path]) => resolve(path) !== home);
|
|
369
|
+
const scored = candidates.map(([candidate]) => ({
|
|
370
|
+
candidate,
|
|
371
|
+
score: candidates.reduce((total, [path, count]) => (path === candidate || path.startsWith(`${candidate}${sep}`)
|
|
372
|
+
? total + count
|
|
373
|
+
: total), 0)
|
|
374
|
+
}));
|
|
375
|
+
scored.sort((left, right) => (right.score - left.score ||
|
|
376
|
+
left.candidate.split(sep).length - right.candidate.split(sep).length ||
|
|
377
|
+
left.candidate.localeCompare(right.candidate)));
|
|
378
|
+
return scored[0]?.candidate ?? sessionCwd;
|
|
379
|
+
}
|
|
218
380
|
/** Claude Code encodes the project path into the transcript's parent dir name. */
|
|
219
381
|
function projectFromTranscriptPath(filePath) {
|
|
220
382
|
if (!filePath)
|
|
@@ -223,6 +385,286 @@ function projectFromTranscriptPath(filePath) {
|
|
|
223
385
|
const tail = parent.split("-").filter(Boolean).pop();
|
|
224
386
|
return tail && tail.length > 0 ? tail : undefined;
|
|
225
387
|
}
|
|
388
|
+
const FOCUS_STOP_WORDS = new Set([
|
|
389
|
+
"about", "after", "again", "also", "and", "are", "at", "been", "being", "but",
|
|
390
|
+
"can", "check", "could", "did", "does", "doing", "dont", "every", "from",
|
|
391
|
+
"for", "have", "here", "how", "in", "into", "its", "just", "like", "make", "more",
|
|
392
|
+
"need", "not", "now", "on", "only", "other", "our", "please", "really", "should",
|
|
393
|
+
"something", "sure", "than", "that", "the", "their", "them", "then", "there",
|
|
394
|
+
"these", "they", "thing", "think", "this", "through", "to", "too", "use", "user",
|
|
395
|
+
"users", "want", "was", "way", "we", "what", "when", "where", "which", "while",
|
|
396
|
+
"who", "why", "will", "with", "work", "working", "would", "you", "your",
|
|
397
|
+
"redacted"
|
|
398
|
+
]);
|
|
399
|
+
const ACTION_WORDS = [
|
|
400
|
+
{ action: "refining", words: new Set(["adjust", "change", "customize", "design", "edit", "improve", "refine", "revise", "update"]) },
|
|
401
|
+
{ action: "fixing", words: new Set(["bug", "debug", "fix", "repair", "resolve"]) },
|
|
402
|
+
{ action: "testing", words: new Set(["test", "testing", "validate", "verification", "verify"]) },
|
|
403
|
+
{ action: "auditing", words: new Set(["audit", "review", "status"]) },
|
|
404
|
+
{ action: "researching", words: new Set(["compare", "find", "investigate", "research"]) },
|
|
405
|
+
{ action: "configuring", words: new Set(["configure", "connect", "setup"]) },
|
|
406
|
+
{ action: "publishing", words: new Set(["deploy", "launch", "publish", "release"]) },
|
|
407
|
+
{ action: "running", words: new Set(["automation", "monitor", "run", "schedule"]) },
|
|
408
|
+
{ action: "building", words: new Set(["add", "build", "create", "develop", "implement", "include"]) }
|
|
409
|
+
];
|
|
410
|
+
function buildLocalAgentActivity(input) {
|
|
411
|
+
// Prompt text is never retained, but derived topic tokens can still disclose
|
|
412
|
+
// a credential if redaction happens only at persistence/output boundaries.
|
|
413
|
+
// Sanitize before every title/topic/action derivation and again at output.
|
|
414
|
+
const prompts = input.prompts
|
|
415
|
+
.map(sanitizeLocalActivityText)
|
|
416
|
+
.filter(isHumanPrompt);
|
|
417
|
+
const topic = focusTopic(prompts);
|
|
418
|
+
const title = cleanTitle(sanitizeLocalActivityText(input.title ?? ""));
|
|
419
|
+
const files = [...input.files.entries()]
|
|
420
|
+
.sort((left, right) => right[1] - left[1] || left[0].localeCompare(right[0]))
|
|
421
|
+
.map(([file]) => sanitizeLocalActivityText(file))
|
|
422
|
+
.filter(Boolean)
|
|
423
|
+
.slice(0, 5);
|
|
424
|
+
const action = inferAction(prompts, title);
|
|
425
|
+
let source;
|
|
426
|
+
let kind;
|
|
427
|
+
let subject;
|
|
428
|
+
if (topic) {
|
|
429
|
+
source = "user_prompts";
|
|
430
|
+
kind = inferActivityKind(topic, input.isSubagent);
|
|
431
|
+
subject = topic;
|
|
432
|
+
}
|
|
433
|
+
else if (title) {
|
|
434
|
+
source = "agent_title";
|
|
435
|
+
kind = inferActivityKind(title, input.isSubagent);
|
|
436
|
+
subject = stripLeadingAction(title);
|
|
437
|
+
}
|
|
438
|
+
else if (files[0]) {
|
|
439
|
+
source = "file_activity";
|
|
440
|
+
kind = "file";
|
|
441
|
+
subject = files[0];
|
|
442
|
+
}
|
|
443
|
+
else if (input.project) {
|
|
444
|
+
source = "project";
|
|
445
|
+
kind = "project";
|
|
446
|
+
subject = sanitizeLocalActivityText(input.project);
|
|
447
|
+
}
|
|
448
|
+
else {
|
|
449
|
+
return undefined;
|
|
450
|
+
}
|
|
451
|
+
if (!subject)
|
|
452
|
+
return undefined;
|
|
453
|
+
return {
|
|
454
|
+
summary: sanitizeLocalActivityText(activitySummary(action, subject, source)),
|
|
455
|
+
kind,
|
|
456
|
+
action,
|
|
457
|
+
source,
|
|
458
|
+
promptCount: prompts.length,
|
|
459
|
+
toolCallCount: input.toolCallCount,
|
|
460
|
+
files,
|
|
461
|
+
isSubagent: input.isSubagent,
|
|
462
|
+
parentSessionId: input.parentSessionId
|
|
463
|
+
};
|
|
464
|
+
}
|
|
465
|
+
function focusTopic(prompts) {
|
|
466
|
+
if (prompts.length === 0)
|
|
467
|
+
return undefined;
|
|
468
|
+
const recent = prompts.slice(-12);
|
|
469
|
+
const observedTopicTokens = new Set(recent.flatMap((prompt) => (topicTokens(prompt).filter((token) => !FOCUS_STOP_WORDS.has(token)))));
|
|
470
|
+
const tokenScores = new Map();
|
|
471
|
+
const pairScores = new Map();
|
|
472
|
+
recent.forEach((prompt, index) => {
|
|
473
|
+
const weight = 1 + index / Math.max(1, recent.length - 1);
|
|
474
|
+
const tokens = topicTokens(prompt).filter((token) => !FOCUS_STOP_WORDS.has(token));
|
|
475
|
+
const unique = [...new Set(tokens)];
|
|
476
|
+
for (const token of unique) {
|
|
477
|
+
tokenScores.set(token, (tokenScores.get(token) ?? 0) + weight);
|
|
478
|
+
}
|
|
479
|
+
for (let pairIndex = 0; pairIndex < tokens.length - 1; pairIndex += 1) {
|
|
480
|
+
const pair = `${tokens[pairIndex]} ${tokens[pairIndex + 1]}`;
|
|
481
|
+
pairScores.set(pair, (pairScores.get(pair) ?? 0) + weight);
|
|
482
|
+
}
|
|
483
|
+
});
|
|
484
|
+
const topPairs = [...pairScores.entries()]
|
|
485
|
+
.filter(([, score]) => score >= 2.5)
|
|
486
|
+
.sort((left, right) => right[1] - left[1] || left[0].localeCompare(right[0]));
|
|
487
|
+
const topTokens = [...tokenScores.entries()]
|
|
488
|
+
.filter(([token]) => !isActionToken(token))
|
|
489
|
+
.sort((left, right) => right[1] - left[1] || left[0].localeCompare(right[0]));
|
|
490
|
+
const candidateTokens = new Set();
|
|
491
|
+
for (const [pair] of topPairs.slice(0, 2)) {
|
|
492
|
+
pair.split(" ").forEach((token) => candidateTokens.add(token));
|
|
493
|
+
}
|
|
494
|
+
for (const [token] of topTokens) {
|
|
495
|
+
if (candidateTokens.size >= 3)
|
|
496
|
+
break;
|
|
497
|
+
candidateTokens.add(token);
|
|
498
|
+
}
|
|
499
|
+
if (candidateTokens.size === 0)
|
|
500
|
+
return undefined;
|
|
501
|
+
const tokens = [...candidateTokens].slice(0, 3);
|
|
502
|
+
if (tokens.includes("glance") && tokens.includes("hover")) {
|
|
503
|
+
return tokens.includes("ui") ? "Glance hover UI" : "Glance hover";
|
|
504
|
+
}
|
|
505
|
+
if (observedTopicTokens.has("glance") &&
|
|
506
|
+
["action", "agent", "handoff", "prompt"].some((token) => observedTopicTokens.has(token))) {
|
|
507
|
+
return "Glance agent handoff";
|
|
508
|
+
}
|
|
509
|
+
if (tokens.includes("hover")) {
|
|
510
|
+
return tokens.includes("ui") ? "hover UI" : "hover interaction";
|
|
511
|
+
}
|
|
512
|
+
if (tokens.includes("landing") && tokens.includes("page"))
|
|
513
|
+
return "landing page";
|
|
514
|
+
if (tokens.includes("mcp"))
|
|
515
|
+
return tokens.includes("feature") ? "MCP feature" : "MCP";
|
|
516
|
+
if (tokens.includes("seo"))
|
|
517
|
+
return tokens.includes("strategy") ? "SEO strategy" : "SEO";
|
|
518
|
+
return tokens.map(displayToken).join(" ");
|
|
519
|
+
}
|
|
520
|
+
function inferAction(prompts, title) {
|
|
521
|
+
const scores = new Map();
|
|
522
|
+
const recent = prompts.slice(-12);
|
|
523
|
+
recent.forEach((prompt, index) => {
|
|
524
|
+
const weight = 1 + index / Math.max(1, recent.length - 1);
|
|
525
|
+
for (const token of promptTokens(prompt)) {
|
|
526
|
+
for (const group of ACTION_WORDS) {
|
|
527
|
+
if (group.words.has(token)) {
|
|
528
|
+
scores.set(group.action, (scores.get(group.action) ?? 0) + weight);
|
|
529
|
+
}
|
|
530
|
+
}
|
|
531
|
+
}
|
|
532
|
+
});
|
|
533
|
+
if (title) {
|
|
534
|
+
for (const token of promptTokens(title)) {
|
|
535
|
+
for (const group of ACTION_WORDS) {
|
|
536
|
+
if (group.words.has(token)) {
|
|
537
|
+
scores.set(group.action, (scores.get(group.action) ?? 0) + 0.75);
|
|
538
|
+
}
|
|
539
|
+
}
|
|
540
|
+
}
|
|
541
|
+
}
|
|
542
|
+
return [...scores.entries()]
|
|
543
|
+
.sort((left, right) => right[1] - left[1])[0]?.[0] ?? "working";
|
|
544
|
+
}
|
|
545
|
+
function inferActivityKind(subject, isSubagent) {
|
|
546
|
+
const tokens = new Set(promptTokens(subject));
|
|
547
|
+
if (tokens.has("automation") || tokens.has("workflow") || tokens.has("schedule"))
|
|
548
|
+
return "automation";
|
|
549
|
+
if (tokens.has("agent") || tokens.has("subagent") || isSubagent)
|
|
550
|
+
return "agent";
|
|
551
|
+
return "task";
|
|
552
|
+
}
|
|
553
|
+
function activitySummary(action, subject, source) {
|
|
554
|
+
if (source === "project")
|
|
555
|
+
return `Working in ${subject}`;
|
|
556
|
+
const actionLabel = action[0].toUpperCase() + action.slice(1);
|
|
557
|
+
return `${actionLabel} ${subject}`.replace(/\s+/g, " ").trim();
|
|
558
|
+
}
|
|
559
|
+
function cleanTitle(value) {
|
|
560
|
+
if (!value)
|
|
561
|
+
return undefined;
|
|
562
|
+
const clean = value.replace(/\s+/g, " ").trim().replace(/[.!?]+$/, "");
|
|
563
|
+
return clean.length >= 3 && clean.length <= 96 ? clean : undefined;
|
|
564
|
+
}
|
|
565
|
+
function stripLeadingAction(value) {
|
|
566
|
+
const words = value.split(/\s+/);
|
|
567
|
+
return isActionToken(words[0]?.toLowerCase() ?? "") && words.length > 1
|
|
568
|
+
? words.slice(1).join(" ")
|
|
569
|
+
: value;
|
|
570
|
+
}
|
|
571
|
+
function isActionToken(token) {
|
|
572
|
+
return ACTION_WORDS.some((group) => group.words.has(token)) ||
|
|
573
|
+
["building", "refining", "fixing", "testing", "auditing", "researching", "configuring", "publishing", "running"].includes(token);
|
|
574
|
+
}
|
|
575
|
+
function displayToken(token) {
|
|
576
|
+
const acronym = new Map([
|
|
577
|
+
["api", "API"],
|
|
578
|
+
["cli", "CLI"],
|
|
579
|
+
["mcp", "MCP"],
|
|
580
|
+
["seo", "SEO"],
|
|
581
|
+
["ui", "UI"],
|
|
582
|
+
["ux", "UX"]
|
|
583
|
+
]).get(token);
|
|
584
|
+
return acronym ?? token;
|
|
585
|
+
}
|
|
586
|
+
function promptTokens(value) {
|
|
587
|
+
return (value.toLowerCase().match(/[a-z][a-z0-9+#.-]*/g) ?? [])
|
|
588
|
+
.map((token) => token.replace(/^[.-]+|[.-]+$/g, ""))
|
|
589
|
+
.filter((token) => token.length >= 2 && token !== "cz" && !/^\d+$/.test(token));
|
|
590
|
+
}
|
|
591
|
+
function topicTokens(value) {
|
|
592
|
+
// Absolute paths often appear in attached-image metadata and tool-oriented
|
|
593
|
+
// prompts. They are machine context, not the user's work topic, and can
|
|
594
|
+
// otherwise outrank meaningful words when only one recent prompt exists.
|
|
595
|
+
const withoutAbsolutePaths = value.replace(/(^|[\s("'=:])(?:file:\/\/)?\/[^\s)"']+/g, "$1");
|
|
596
|
+
return promptTokens(sanitizeLocalActivityText(withoutAbsolutePaths));
|
|
597
|
+
}
|
|
598
|
+
/**
|
|
599
|
+
* Remove known and assignment-shaped credentials from metadata before it can
|
|
600
|
+
* become a topic, title, Glance field, MCP result, or copy-ready handoff.
|
|
601
|
+
* This intentionally favors dropping a suspicious token over displaying it.
|
|
602
|
+
*/
|
|
603
|
+
export function sanitizeLocalActivityText(value) {
|
|
604
|
+
return redactSecrets(value)
|
|
605
|
+
.replace(/\b(?:api[_ -]?key|access[_ -]?token|auth[_ -]?token|secret|password|credential)\s*[:=]\s*[^\s,;]+/gi, " ")
|
|
606
|
+
.replace(/\bbearer\s+[A-Za-z0-9._~+/=-]{12,}/gi, " ")
|
|
607
|
+
.replace(/\b[A-Z][A-Z0-9_]*(?:KEY|TOKEN|SECRET|PASSWORD|PASSWD|CREDENTIALS?|AUTH)=\[REDACTED\]/g, " ")
|
|
608
|
+
.replace(/\[REDACTED\]/g, " ")
|
|
609
|
+
.replace(/\s+/g, " ")
|
|
610
|
+
.trim();
|
|
611
|
+
}
|
|
612
|
+
function isHumanPrompt(value) {
|
|
613
|
+
const text = value.trim();
|
|
614
|
+
if (text.length < 3)
|
|
615
|
+
return false;
|
|
616
|
+
return ![
|
|
617
|
+
/^</,
|
|
618
|
+
/^>>>/,
|
|
619
|
+
/^\s*\[\d+\]\s+(tool|assistant|user)\b/i,
|
|
620
|
+
/^#\s*AGENTS\.md/i,
|
|
621
|
+
/^Assess the exact/i,
|
|
622
|
+
/^Planned action JSON/i,
|
|
623
|
+
/^Reviewed Codex/i,
|
|
624
|
+
/^Some conversation/i,
|
|
625
|
+
/^The Codex agent/i,
|
|
626
|
+
/^The following is the Codex/i,
|
|
627
|
+
/^You are .*primary agent/i,
|
|
628
|
+
/^You have \d+ weighted tokens left/i
|
|
629
|
+
].some((pattern) => pattern.test(text));
|
|
630
|
+
}
|
|
631
|
+
function textValues(value) {
|
|
632
|
+
if (typeof value === "string")
|
|
633
|
+
return [value];
|
|
634
|
+
if (!Array.isArray(value))
|
|
635
|
+
return [];
|
|
636
|
+
return value.flatMap((item) => {
|
|
637
|
+
if (!isRecord(item))
|
|
638
|
+
return [];
|
|
639
|
+
return [stringOf(item.text) ?? stringOf(item.content)].filter((text) => Boolean(text));
|
|
640
|
+
});
|
|
641
|
+
}
|
|
642
|
+
function recordValues(value) {
|
|
643
|
+
return Array.isArray(value) ? value.filter(isRecord) : [];
|
|
644
|
+
}
|
|
645
|
+
function collectToolFiles(value, counts) {
|
|
646
|
+
if (!isRecord(value))
|
|
647
|
+
return;
|
|
648
|
+
for (const key of ["file_path", "path", "notebook_path"]) {
|
|
649
|
+
addFile(stringOf(value[key]), counts);
|
|
650
|
+
}
|
|
651
|
+
collectPatchFiles(stringOf(value.patch) ?? stringOf(value.input), counts);
|
|
652
|
+
}
|
|
653
|
+
function collectPatchFiles(value, counts) {
|
|
654
|
+
if (!value)
|
|
655
|
+
return;
|
|
656
|
+
for (const match of value.matchAll(/^\*\*\* (?:Add|Update|Delete) File: (.+)$/gm)) {
|
|
657
|
+
addFile(match[1], counts);
|
|
658
|
+
}
|
|
659
|
+
}
|
|
660
|
+
function addFile(value, counts) {
|
|
661
|
+
if (!value)
|
|
662
|
+
return;
|
|
663
|
+
const file = basename(value.trim());
|
|
664
|
+
if (!file || file === "." || file === sep)
|
|
665
|
+
return;
|
|
666
|
+
counts.set(file, (counts.get(file) ?? 0) + 1);
|
|
667
|
+
}
|
|
226
668
|
function toIso(value) {
|
|
227
669
|
if (!value)
|
|
228
670
|
return undefined;
|
|
@@ -241,6 +683,17 @@ function stringOf(value) {
|
|
|
241
683
|
function isRecord(value) {
|
|
242
684
|
return typeof value === "object" && value !== null;
|
|
243
685
|
}
|
|
686
|
+
function jsonRecord(value) {
|
|
687
|
+
if (!value)
|
|
688
|
+
return undefined;
|
|
689
|
+
try {
|
|
690
|
+
const parsed = JSON.parse(value);
|
|
691
|
+
return isRecord(parsed) ? parsed : undefined;
|
|
692
|
+
}
|
|
693
|
+
catch {
|
|
694
|
+
return undefined;
|
|
695
|
+
}
|
|
696
|
+
}
|
|
244
697
|
function slug(value) {
|
|
245
698
|
return value.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "") || "x";
|
|
246
699
|
}
|
package/dist/modelPricing.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Published per-token API prices
|
|
2
|
+
* Published per-token API prices used to estimate the
|
|
3
3
|
* API-equivalent dollar value of locally observed usage (e.g. Claude Code /
|
|
4
4
|
* Codex session logs, where the provider never reports a price).
|
|
5
5
|
*
|
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
* matched top-down; first match wins. Unknown models return undefined so
|
|
8
8
|
* callers can label the record "missing" instead of inventing a number.
|
|
9
9
|
*/
|
|
10
|
+
export declare const PRICING_TABLE_AS_OF = "2026-07-28";
|
|
10
11
|
export type TokenUsage = {
|
|
11
12
|
/** Billable, uncached input tokens. */
|
|
12
13
|
inputTokens: number;
|
package/dist/modelPricing.js
CHANGED
|
@@ -1,3 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Published per-token API prices used to estimate the
|
|
3
|
+
* API-equivalent dollar value of locally observed usage (e.g. Claude Code /
|
|
4
|
+
* Codex session logs, where the provider never reports a price).
|
|
5
|
+
*
|
|
6
|
+
* Estimates only — always surfaced with costConfidence "estimated". Rules are
|
|
7
|
+
* matched top-down; first match wins. Unknown models return undefined so
|
|
8
|
+
* callers can label the record "missing" instead of inventing a number.
|
|
9
|
+
*/
|
|
10
|
+
export const PRICING_TABLE_AS_OF = "2026-07-28";
|
|
1
11
|
const pricingRules = [
|
|
2
12
|
// Anthropic
|
|
3
13
|
{ match: /^claude-fable-5/i, inputPerM: 10, outputPerM: 50 },
|
|
@@ -7,9 +17,18 @@ const pricingRules = [
|
|
|
7
17
|
{ match: /^claude-haiku-4/i, inputPerM: 1, outputPerM: 5 },
|
|
8
18
|
{ match: /^claude-3-7-sonnet|^claude-3-5-sonnet/i, inputPerM: 3, outputPerM: 15 },
|
|
9
19
|
{ match: /^claude-3-5-haiku/i, inputPerM: 0.8, outputPerM: 4 },
|
|
10
|
-
// OpenAI (
|
|
11
|
-
{ match: /^gpt-5(
|
|
12
|
-
{ match: /^gpt-5
|
|
20
|
+
// OpenAI (newer and more specific families must precede the GPT-5 fallback)
|
|
21
|
+
{ match: /^gpt-5\.6(?:-sol)?$/i, inputPerM: 5, outputPerM: 30, cacheReadPerM: 0.5 },
|
|
22
|
+
{ match: /^gpt-5\.6-terra/i, inputPerM: 2.5, outputPerM: 15, cacheReadPerM: 0.25 },
|
|
23
|
+
{ match: /^gpt-5\.6-luna/i, inputPerM: 1, outputPerM: 6, cacheReadPerM: 0.1 },
|
|
24
|
+
{ match: /^gpt-5\.5(?:-codex)?/i, inputPerM: 5, outputPerM: 30, cacheReadPerM: 0.5 },
|
|
25
|
+
{ match: /^gpt-5\.4-mini/i, inputPerM: 0.75, outputPerM: 4.5, cacheReadPerM: 0.075 },
|
|
26
|
+
{ match: /^gpt-5\.4-nano/i, inputPerM: 0.2, outputPerM: 1.25, cacheReadPerM: 0.02 },
|
|
27
|
+
{ match: /^gpt-5\.4/i, inputPerM: 2.5, outputPerM: 15, cacheReadPerM: 0.25 },
|
|
28
|
+
{ match: /^gpt-5\.3-codex/i, inputPerM: 1.75, outputPerM: 14, cacheReadPerM: 0.175 },
|
|
29
|
+
{ match: /^gpt-5\.2(?:-codex)?/i, inputPerM: 1.75, outputPerM: 14, cacheReadPerM: 0.175 },
|
|
30
|
+
{ match: /^gpt-5(?:\.1)?-codex/i, inputPerM: 1.25, outputPerM: 10, cacheReadPerM: 0.125 },
|
|
31
|
+
{ match: /^gpt-5(?:\.1)?-mini/i, inputPerM: 0.25, outputPerM: 2, cacheReadPerM: 0.025 },
|
|
13
32
|
{ match: /^gpt-5/i, inputPerM: 1.25, outputPerM: 10, cacheReadPerM: 0.125 },
|
|
14
33
|
{ match: /^gpt-4\.1-nano/i, inputPerM: 0.1, outputPerM: 0.4 },
|
|
15
34
|
{ match: /^gpt-4\.1-mini/i, inputPerM: 0.4, outputPerM: 1.6 },
|
package/dist/planMath.d.ts
CHANGED
|
@@ -2,9 +2,9 @@ import type { DetectedPlan } from "./planDetection.js";
|
|
|
2
2
|
import type { UsageRecord } from "./schema.js";
|
|
3
3
|
/**
|
|
4
4
|
* Plan-price math: compares API-equivalent usage (from local agent logs)
|
|
5
|
-
* against published subscription plan prices
|
|
6
|
-
*
|
|
7
|
-
*
|
|
5
|
+
* against published subscription plan prices. This is comparison context,
|
|
6
|
+
* not proof of entitlement, marginal cash cost, remaining capacity, or the
|
|
7
|
+
* cheapest option for a particular account.
|
|
8
8
|
*
|
|
9
9
|
* Prices are mid-2026 list prices. As of 2026-06-15, programmatic/Agent-SDK
|
|
10
10
|
* usage on Claude plans is metered against a separate monthly credit pool at
|
|
@@ -17,7 +17,7 @@ export type SubscriptionPlan = {
|
|
|
17
17
|
agent: "claude-code" | "codex";
|
|
18
18
|
name: string;
|
|
19
19
|
monthlyUsd: number;
|
|
20
|
-
/** Rough API-equivalent
|
|
20
|
+
/** Rough API-equivalent comparison threshold; not an entitlement limit. */
|
|
21
21
|
coversUpToUsd: number;
|
|
22
22
|
};
|
|
23
23
|
export declare const subscriptionPlans: SubscriptionPlan[];
|
|
@@ -27,19 +27,18 @@ export type PlanCheck = {
|
|
|
27
27
|
apiEquivalentMonthlyUsd: number;
|
|
28
28
|
/** Distinct days of observed usage the projection is based on. */
|
|
29
29
|
windowDays: number;
|
|
30
|
-
/**
|
|
30
|
+
/** Reference plan selected by a rough API-equivalent comparison (if any). */
|
|
31
31
|
suggestedPlan?: SubscriptionPlan;
|
|
32
32
|
/** apiEquivalentMonthlyUsd - plan price, when positive. */
|
|
33
33
|
monthlySavingsVsApiUsd?: number;
|
|
34
34
|
/**
|
|
35
|
-
* API-equivalent usage ÷ plan price
|
|
36
|
-
*
|
|
37
|
-
* money's worth? Present only when a plan covers the usage.
|
|
35
|
+
* API-equivalent usage ÷ plan price. This is a value comparison, not an
|
|
36
|
+
* account entitlement or ROI measurement.
|
|
38
37
|
*/
|
|
39
38
|
valueMultiple?: number;
|
|
40
|
-
/**
|
|
39
|
+
/** Plan label detected in local metadata (or supplied via --plan), if any. */
|
|
41
40
|
detectedPlan?: DetectedPlan;
|
|
42
|
-
/** Set when
|
|
41
|
+
/** Set when projection exceeds a rough comparison threshold or a limit signal exists. */
|
|
43
42
|
upgradeHint?: string;
|
|
44
43
|
/** One-line, render-ready verdict. */
|
|
45
44
|
headline: string;
|
|
@@ -49,10 +48,9 @@ export type PlanCheck = {
|
|
|
49
48
|
* from local agent logs participate (billing-API records already have real
|
|
50
49
|
* prices and a real plan behind them).
|
|
51
50
|
*
|
|
52
|
-
* When `detectedPlans` carries a locally detected plan (or --plan
|
|
53
|
-
*
|
|
54
|
-
*
|
|
55
|
-
* exceeds what the detected tier typically covers.
|
|
51
|
+
* When `detectedPlans` carries a locally detected plan label (or --plan
|
|
52
|
+
* override), the result identifies that provenance and keeps comparison math
|
|
53
|
+
* separate from provider-reported limits.
|
|
56
54
|
*/
|
|
57
55
|
export declare function computePlanChecks(records: UsageRecord[], detectedPlans?: DetectedPlan[]): PlanCheck[];
|
|
58
56
|
//# sourceMappingURL=planMath.d.ts.map
|