agent-orchestrator-kit 0.6.0 → 0.8.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/CHANGELOG.md +32 -0
- package/README.md +45 -18
- package/bin/agent-orchestrator.js +625 -181
- package/bin/session-client.js +187 -0
- package/bin/spend-collect.js +204 -40
- package/package.json +1 -1
- package/templates/.agents/commands/opsx-archive.md +1 -1
- package/templates/.agents/rules/session-handoff.mdc +9 -8
- package/templates/.agents/skills/agent-orchestration/SKILL.md +16 -6
- package/templates/.agents/subagents/session-handoff.md +6 -5
- package/templates/.agents/subagents/spec-archiver.md +3 -2
- package/templates/AGENTS.md +1 -1
- package/templates/CLAUDE.md +1 -1
- package/templates/scripts/cursor-spend-collect.cjs +285 -0
- package/templates/scripts/cursor-spend-hook.cjs +1 -1
|
@@ -6,6 +6,7 @@ import { join, dirname, basename, resolve } from 'path';
|
|
|
6
6
|
import { fileURLToPath } from 'url';
|
|
7
7
|
import { execSync } from 'child_process';
|
|
8
8
|
import { collectSpend } from './spend-collect.js';
|
|
9
|
+
import { resolveRestoreClient, ampThreadIdFromEnv } from './session-client.js';
|
|
9
10
|
|
|
10
11
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
11
12
|
const KIT_ROOT = join(__dirname, '..');
|
|
@@ -87,9 +88,12 @@ const GITLAB_LAUNCHER_REL = join('scripts', 'gitlab-mcp-launcher.cjs');
|
|
|
87
88
|
const BROWSER_LAUNCHER_REL = join('scripts', 'browser-mcp-launcher.cjs');
|
|
88
89
|
const HOOK_SCRIPT_REL = join('scripts', 'pre-commit-gate-check.sh');
|
|
89
90
|
const CURSOR_SPEND_HOOK_REL = join('scripts', 'cursor-spend-hook.cjs');
|
|
91
|
+
const CURSOR_SPEND_COLLECT_REL = join('scripts', 'cursor-spend-collect.cjs');
|
|
90
92
|
const CURSOR_HOOKS_JSON_REL = join('.cursor', 'hooks.json');
|
|
91
93
|
const CURSOR_SPEND_HOOK_COMMAND = 'node scripts/cursor-spend-hook.cjs';
|
|
92
|
-
const
|
|
94
|
+
const CURSOR_SPEND_COLLECT_COMMAND = 'node scripts/cursor-spend-collect.cjs';
|
|
95
|
+
const CURSOR_SPEND_HOOK_EVENTS = ['stop', 'subagentStop', 'afterAgentResponse'];
|
|
96
|
+
const CURSOR_SPEND_COLLECT_EVENTS = ['sessionEnd'];
|
|
93
97
|
const CURSOR_USAGE_FILE_REL = join('.agents', 'spend', 'cursor-usage.jsonl');
|
|
94
98
|
const MCP_EXAMPLE_REL = join('.agents', 'mcp.json.example');
|
|
95
99
|
const AMP_EXAMPLE_REL = join('.agents', 'amp.settings.json.example');
|
|
@@ -159,6 +163,7 @@ const HANDOFF_SECTIONS = [
|
|
|
159
163
|
'Subagents to spawn',
|
|
160
164
|
'Constraints',
|
|
161
165
|
'Runtime',
|
|
166
|
+
'Metrics',
|
|
162
167
|
'Prompt',
|
|
163
168
|
];
|
|
164
169
|
const CLOUD_ENV_MARKERS = ['CURSOR_BACKGROUND_AGENT'];
|
|
@@ -368,6 +373,12 @@ function refreshOptionalMcpManagedFiles(projectDir) {
|
|
|
368
373
|
refreshManagedRelPaths(projectDir, OPTIONAL_MCP_MANAGED_PATHS);
|
|
369
374
|
}
|
|
370
375
|
|
|
376
|
+
function hookEventHasCommand(hooks, event, needle) {
|
|
377
|
+
const entries = hooks[event];
|
|
378
|
+
return Array.isArray(entries)
|
|
379
|
+
&& entries.some((entry) => entry && String(entry.command || '').includes(needle));
|
|
380
|
+
}
|
|
381
|
+
|
|
371
382
|
function cursorSpendHookEntryOk(projectDir) {
|
|
372
383
|
const hooksPath = join(projectDir, CURSOR_HOOKS_JSON_REL);
|
|
373
384
|
if (!existsSync(hooksPath)) return false;
|
|
@@ -379,24 +390,39 @@ function cursorSpendHookEntryOk(projectDir) {
|
|
|
379
390
|
}
|
|
380
391
|
const hooks = config && typeof config === 'object' ? config.hooks : null;
|
|
381
392
|
if (!hooks || typeof hooks !== 'object') return false;
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
&& entries.some((entry) => entry && String(entry.command || '').includes('cursor-spend-hook.cjs'));
|
|
386
|
-
});
|
|
393
|
+
const writesOk = CURSOR_SPEND_HOOK_EVENTS.every((event) => hookEventHasCommand(hooks, event, 'cursor-spend-hook.cjs'));
|
|
394
|
+
const collectOk = CURSOR_SPEND_COLLECT_EVENTS.every((event) => hookEventHasCommand(hooks, event, 'cursor-spend-collect.cjs'));
|
|
395
|
+
return writesOk && collectOk;
|
|
387
396
|
}
|
|
388
397
|
|
|
389
398
|
// Mandatory spend capture: every kit project must record Cursor token usage
|
|
390
399
|
// locally so handoff/archive can collect real spend without manual flags.
|
|
400
|
+
function ensureManagedScript(projectDir, rel) {
|
|
401
|
+
const src = join(KIT_ROOT, 'templates', rel);
|
|
402
|
+
const dest = join(projectDir, rel);
|
|
403
|
+
if (!existsSync(src)) return false;
|
|
404
|
+
mkdirSync(dirname(dest), { recursive: true });
|
|
405
|
+
copyFileSync(src, dest);
|
|
406
|
+
return true;
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
function mergeHookCommands(config, events, command, needle) {
|
|
410
|
+
let changed = false;
|
|
411
|
+
for (const event of events) {
|
|
412
|
+
const entries = Array.isArray(config.hooks[event]) ? config.hooks[event] : [];
|
|
413
|
+
if (!entries.some((entry) => entry && String(entry.command || '').includes(needle))) {
|
|
414
|
+
entries.push({ command });
|
|
415
|
+
config.hooks[event] = entries;
|
|
416
|
+
changed = true;
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
return changed;
|
|
420
|
+
}
|
|
421
|
+
|
|
391
422
|
function ensureCursorSpendHook(projectDir) {
|
|
392
423
|
const result = { script: false, hooksJson: false, error: null };
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
if (existsSync(src)) {
|
|
396
|
-
mkdirSync(dirname(dest), { recursive: true });
|
|
397
|
-
copyFileSync(src, dest);
|
|
398
|
-
result.script = true;
|
|
399
|
-
}
|
|
424
|
+
if (ensureManagedScript(projectDir, CURSOR_SPEND_HOOK_REL)) result.script = true;
|
|
425
|
+
ensureManagedScript(projectDir, CURSOR_SPEND_COLLECT_REL);
|
|
400
426
|
|
|
401
427
|
const hooksPath = join(projectDir, CURSOR_HOOKS_JSON_REL);
|
|
402
428
|
let config = { version: 1, hooks: {} };
|
|
@@ -412,15 +438,8 @@ function ensureCursorSpendHook(projectDir) {
|
|
|
412
438
|
if (config.version == null) config.version = 1;
|
|
413
439
|
if (!config.hooks || typeof config.hooks !== 'object') config.hooks = {};
|
|
414
440
|
let changed = !existsSync(hooksPath);
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
const present = entries.some((entry) => entry && String(entry.command || '').includes('cursor-spend-hook.cjs'));
|
|
418
|
-
if (!present) {
|
|
419
|
-
entries.push({ command: CURSOR_SPEND_HOOK_COMMAND });
|
|
420
|
-
config.hooks[event] = entries;
|
|
421
|
-
changed = true;
|
|
422
|
-
}
|
|
423
|
-
}
|
|
441
|
+
changed = mergeHookCommands(config, CURSOR_SPEND_HOOK_EVENTS, CURSOR_SPEND_HOOK_COMMAND, 'cursor-spend-hook.cjs') || changed;
|
|
442
|
+
changed = mergeHookCommands(config, CURSOR_SPEND_COLLECT_EVENTS, CURSOR_SPEND_COLLECT_COMMAND, 'cursor-spend-collect.cjs') || changed;
|
|
424
443
|
if (changed) {
|
|
425
444
|
mkdirSync(dirname(hooksPath), { recursive: true });
|
|
426
445
|
writeFileSync(hooksPath, `${JSON.stringify(config, null, 2)}\n`);
|
|
@@ -436,7 +455,7 @@ function reportCursorSpendHook(projectDir, emit) {
|
|
|
436
455
|
return result;
|
|
437
456
|
}
|
|
438
457
|
if (result.script) emit.ok(CURSOR_SPEND_HOOK_REL);
|
|
439
|
-
if (result.hooksJson) emit.ok(`${CURSOR_HOOKS_JSON_REL} (stop
|
|
458
|
+
if (result.hooksJson) emit.ok(`${CURSOR_HOOKS_JSON_REL} (stop / subagentStop / afterAgentResponse + sessionEnd collect)`);
|
|
440
459
|
return result;
|
|
441
460
|
}
|
|
442
461
|
|
|
@@ -457,7 +476,7 @@ function printSpendHealth(projectDir) {
|
|
|
457
476
|
const records = countCursorUsageRecords(projectDir);
|
|
458
477
|
const cursorState = scriptOk && entryOk
|
|
459
478
|
? `ok${records != null ? ` (${records} records)` : ' (no turns recorded yet)'}`
|
|
460
|
-
: '
|
|
479
|
+
: 'optional — not configured (init/update/sync/mcp-setup)';
|
|
461
480
|
console.log(` cursor ${cursorState}`);
|
|
462
481
|
const home = process.env.HOME || '';
|
|
463
482
|
const claudeOk = home && existsSync(join(home, '.claude', 'projects'));
|
|
@@ -466,7 +485,7 @@ function printSpendHealth(projectDir) {
|
|
|
466
485
|
? String(process.env.AMP_DATA_DIR).trim()
|
|
467
486
|
: join(home, '.local', 'share', 'amp');
|
|
468
487
|
const ampOk = existsSync(join(ampDir, 'threads'));
|
|
469
|
-
console.log(` amp ${ampOk ? 'ok (threads found)' : 'no local Amp data'}`);
|
|
488
|
+
console.log(` amp ${ampOk ? 'ok (threads found)' : 'no local Amp data'}; locked client + amp threads export`);
|
|
470
489
|
console.log('');
|
|
471
490
|
}
|
|
472
491
|
|
|
@@ -1005,16 +1024,135 @@ function applyRuntimeToFields(fields, opts, env) {
|
|
|
1005
1024
|
}
|
|
1006
1025
|
|
|
1007
1026
|
const VALID_PLATFORMS = new Set(['cursor', 'claude', 'amp']);
|
|
1027
|
+
const METRICS_NULL_TOKENS = new Set(['unknown', 'none', 'n/a', 'na', '-', '—', '–', 'null']);
|
|
1028
|
+
|
|
1029
|
+
function emptyMetricsFields(warnings = []) {
|
|
1030
|
+
return {
|
|
1031
|
+
platform: null,
|
|
1032
|
+
model: null,
|
|
1033
|
+
inputTokens: null,
|
|
1034
|
+
outputTokens: null,
|
|
1035
|
+
totalTokens: null,
|
|
1036
|
+
costUsd: null,
|
|
1037
|
+
ampCredits: null,
|
|
1038
|
+
spendSource: null,
|
|
1039
|
+
warnings,
|
|
1040
|
+
};
|
|
1041
|
+
}
|
|
1042
|
+
|
|
1043
|
+
function normalizeMetricsBlank(value) {
|
|
1044
|
+
const raw = value == null ? '' : String(value).trim();
|
|
1045
|
+
if (!raw) return null;
|
|
1046
|
+
const lower = raw.toLowerCase();
|
|
1047
|
+
if (METRICS_NULL_TOKENS.has(lower) || METRICS_NULL_TOKENS.has(raw)) return null;
|
|
1048
|
+
return raw;
|
|
1049
|
+
}
|
|
1050
|
+
|
|
1051
|
+
function parseMetricsNumber(raw, key, warnings) {
|
|
1052
|
+
const normalized = normalizeMetricsBlank(raw);
|
|
1053
|
+
if (normalized == null) return null;
|
|
1054
|
+
const cleaned = normalized.replace(/[$,\s]/g, '');
|
|
1055
|
+
const n = Number(cleaned);
|
|
1056
|
+
if (!Number.isFinite(n)) {
|
|
1057
|
+
warnings.push(`metrics: unparsable ${key} in ## Metrics: ${raw}`);
|
|
1058
|
+
return null;
|
|
1059
|
+
}
|
|
1060
|
+
return n;
|
|
1061
|
+
}
|
|
1062
|
+
|
|
1063
|
+
function parseMetricsSection(body) {
|
|
1064
|
+
const warnings = [];
|
|
1065
|
+
const values = {};
|
|
1066
|
+
const present = new Set();
|
|
1067
|
+
for (const line of String(body || '').split(/\r?\n/)) {
|
|
1068
|
+
const match = line.match(/^\s*[-*]\s*([A-Za-z][A-Za-z0-9_]*)\s*:\s*(.*)$/);
|
|
1069
|
+
if (!match) continue;
|
|
1070
|
+
const key = match[1].toLowerCase();
|
|
1071
|
+
values[key] = match[2].trim();
|
|
1072
|
+
present.add(key);
|
|
1073
|
+
}
|
|
1074
|
+
const result = emptyMetricsFields(warnings);
|
|
1075
|
+
const platformRaw = normalizeMetricsBlank(values.platform);
|
|
1076
|
+
if (platformRaw) {
|
|
1077
|
+
const lower = platformRaw.toLowerCase();
|
|
1078
|
+
if (VALID_PLATFORMS.has(lower)) result.platform = lower;
|
|
1079
|
+
else {
|
|
1080
|
+
result.platform = null;
|
|
1081
|
+
warnings.push(`metrics: invalid platform in ## Metrics: ${platformRaw}`);
|
|
1082
|
+
}
|
|
1083
|
+
}
|
|
1084
|
+
result.model = normalizeMetricsBlank(values.model);
|
|
1085
|
+
result.inputTokens = parseMetricsNumber(values.input_tokens, 'input_tokens', warnings);
|
|
1086
|
+
result.outputTokens = parseMetricsNumber(values.output_tokens, 'output_tokens', warnings);
|
|
1087
|
+
result.totalTokens = present.has('total_tokens')
|
|
1088
|
+
? parseMetricsNumber(values.total_tokens, 'total_tokens', warnings)
|
|
1089
|
+
: null;
|
|
1090
|
+
result.costUsd = parseMetricsNumber(values.cost_usd, 'cost_usd', warnings);
|
|
1091
|
+
result.ampCredits = parseMetricsNumber(values.amp_credits, 'amp_credits', warnings);
|
|
1092
|
+
result.spendSource = normalizeMetricsBlank(values.spend_source);
|
|
1093
|
+
return result;
|
|
1094
|
+
}
|
|
1095
|
+
|
|
1096
|
+
function formatMetricsField(value) {
|
|
1097
|
+
return value == null || value === '' ? 'unknown' : String(value);
|
|
1098
|
+
}
|
|
1008
1099
|
|
|
1009
|
-
function
|
|
1100
|
+
function renderMetricsSection(metrics) {
|
|
1101
|
+
const m = metrics || emptyMetricsFields();
|
|
1102
|
+
return `## Metrics
|
|
1103
|
+
- platform: ${formatMetricsField(m.platform)}
|
|
1104
|
+
- model: ${formatMetricsField(m.model)}
|
|
1105
|
+
- input_tokens: ${formatMetricsField(m.inputTokens)}
|
|
1106
|
+
- output_tokens: ${formatMetricsField(m.outputTokens)}
|
|
1107
|
+
- cost_usd: ${formatMetricsField(m.costUsd)}
|
|
1108
|
+
- amp_credits: ${formatMetricsField(m.ampCredits)}
|
|
1109
|
+
- spend_source: ${formatMetricsField(m.spendSource)}`;
|
|
1110
|
+
}
|
|
1111
|
+
|
|
1112
|
+
function printMetricsSectionWarnings(metrics, platformAlreadyWarned) {
|
|
1113
|
+
for (const warning of (metrics && metrics.warnings) || []) {
|
|
1114
|
+
if (platformAlreadyWarned && /invalid platform/i.test(warning)) continue;
|
|
1115
|
+
console.error(warning);
|
|
1116
|
+
}
|
|
1117
|
+
}
|
|
1118
|
+
|
|
1119
|
+
function firstNonNull(...values) {
|
|
1120
|
+
for (const value of values) {
|
|
1121
|
+
if (value != null) return value;
|
|
1122
|
+
}
|
|
1123
|
+
return null;
|
|
1124
|
+
}
|
|
1125
|
+
|
|
1126
|
+
function resolveModel(opts, env, reported) {
|
|
1010
1127
|
const flag = opts && opts.model != null ? String(opts.model).trim() : '';
|
|
1011
1128
|
if (flag) return flag;
|
|
1129
|
+
const fromReport = reported && reported.model != null ? String(reported.model).trim() : '';
|
|
1130
|
+
if (fromReport) return fromReport;
|
|
1012
1131
|
const fromEnv = env && env.AOK_MODEL != null ? String(env.AOK_MODEL).trim() : '';
|
|
1013
1132
|
if (fromEnv) return fromEnv;
|
|
1014
1133
|
return null;
|
|
1015
1134
|
}
|
|
1016
1135
|
|
|
1017
|
-
function
|
|
1136
|
+
function envFlagOn(value) {
|
|
1137
|
+
if (value == null) return false;
|
|
1138
|
+
const normalized = String(value).trim().toLowerCase();
|
|
1139
|
+
return normalized !== '' && normalized !== '0' && normalized !== 'false';
|
|
1140
|
+
}
|
|
1141
|
+
|
|
1142
|
+
function inferPlatformFromHost(env) {
|
|
1143
|
+
if (!env) return null;
|
|
1144
|
+
if (env.AMP_CURRENT_THREAD != null && String(env.AMP_CURRENT_THREAD).trim()) return 'amp';
|
|
1145
|
+
if (env.AMP_THREAD_ID != null && String(env.AMP_THREAD_ID).trim()) return 'amp';
|
|
1146
|
+
if (envFlagOn(env.CURSOR_AGENT) || (env.CURSOR_CONVERSATION_ID != null && String(env.CURSOR_CONVERSATION_ID).trim())) {
|
|
1147
|
+
return 'cursor';
|
|
1148
|
+
}
|
|
1149
|
+
if (envFlagOn(env.CLAUDECODE) || envFlagOn(env.CLAUDE_CODE) || (env.CLAUDE_CODE_ENTRYPOINT != null && String(env.CLAUDE_CODE_ENTRYPOINT).trim())) {
|
|
1150
|
+
return 'claude';
|
|
1151
|
+
}
|
|
1152
|
+
return null;
|
|
1153
|
+
}
|
|
1154
|
+
|
|
1155
|
+
function resolvePlatform(opts, env, reported, pending) {
|
|
1018
1156
|
const flag = opts && opts.platform != null ? String(opts.platform).trim() : '';
|
|
1019
1157
|
if (flag) {
|
|
1020
1158
|
const lower = flag.toLowerCase();
|
|
@@ -1023,13 +1161,23 @@ function resolvePlatform(opts, env) {
|
|
|
1023
1161
|
}
|
|
1024
1162
|
return { value: lower };
|
|
1025
1163
|
}
|
|
1164
|
+
const fromReport = reported && reported.platform != null ? String(reported.platform).trim() : '';
|
|
1165
|
+
if (fromReport) {
|
|
1166
|
+
const lower = fromReport.toLowerCase();
|
|
1167
|
+
if (VALID_PLATFORMS.has(lower)) return { value: lower };
|
|
1168
|
+
return { value: null, warn: 'invalid platform in ## Metrics (use cursor, claude, or amp)' };
|
|
1169
|
+
}
|
|
1026
1170
|
const fromEnv = env && env.AOK_PLATFORM != null ? String(env.AOK_PLATFORM).trim() : '';
|
|
1027
1171
|
if (fromEnv) {
|
|
1028
1172
|
const lower = fromEnv.toLowerCase();
|
|
1029
1173
|
if (VALID_PLATFORMS.has(lower)) return { value: lower };
|
|
1030
1174
|
return { value: null, warn: 'invalid AOK_PLATFORM (use cursor, claude, or amp)' };
|
|
1031
1175
|
}
|
|
1032
|
-
|
|
1176
|
+
const pendingPlatform = pending && pending.platform != null ? String(pending.platform).trim() : '';
|
|
1177
|
+
if (pendingPlatform && VALID_PLATFORMS.has(pendingPlatform)) {
|
|
1178
|
+
return { value: pendingPlatform };
|
|
1179
|
+
}
|
|
1180
|
+
return { value: inferPlatformFromHost(env) };
|
|
1033
1181
|
}
|
|
1034
1182
|
|
|
1035
1183
|
function warnMissingModel() {
|
|
@@ -1040,6 +1188,12 @@ function warnMissingUsd() {
|
|
|
1040
1188
|
console.error('metrics: spend.costUsd is null — USD spend was not collected');
|
|
1041
1189
|
}
|
|
1042
1190
|
|
|
1191
|
+
function warnUnreportedSelfReport() {
|
|
1192
|
+
console.error(
|
|
1193
|
+
'metrics: session spend is unreported — fill ## Metrics in handoff.md with platform, model, input_tokens, output_tokens, cost_usd, amp_credits, spend_source (use unknown when a value is missing)',
|
|
1194
|
+
);
|
|
1195
|
+
}
|
|
1196
|
+
|
|
1043
1197
|
function gitTry(projectDir, command) {
|
|
1044
1198
|
try {
|
|
1045
1199
|
const stdout = execSync(command, {
|
|
@@ -1140,7 +1294,9 @@ ${fields.constraints}
|
|
|
1140
1294
|
|
|
1141
1295
|
## Runtime
|
|
1142
1296
|
- runtime: ${runtime}
|
|
1143
|
-
- agent_id: ${agentId}
|
|
1297
|
+
- agent_id: ${agentId}
|
|
1298
|
+
|
|
1299
|
+
${renderMetricsSection(fields.metrics)}${prompt}
|
|
1144
1300
|
`;
|
|
1145
1301
|
}
|
|
1146
1302
|
|
|
@@ -1161,6 +1317,7 @@ function fieldsFromSections(changeName, sections, extra = {}) {
|
|
|
1161
1317
|
constraints: extra.constraints || sectionOr(sections, 'Constraints', ''),
|
|
1162
1318
|
runtime: extra.runtime || runtimeParsed.runtime,
|
|
1163
1319
|
agentId: extra.agentId || runtimeParsed.agentId,
|
|
1320
|
+
metrics: extra.metrics || parseMetricsSection(sectionOr(sections, 'Metrics', '')),
|
|
1164
1321
|
status: extra.status || '',
|
|
1165
1322
|
tasks: extra.tasks || '',
|
|
1166
1323
|
review: extra.review || '',
|
|
@@ -1569,6 +1726,10 @@ function lastSessionEndedAt(metrics) {
|
|
|
1569
1726
|
return last;
|
|
1570
1727
|
}
|
|
1571
1728
|
|
|
1729
|
+
function collectWindowStart(metrics) {
|
|
1730
|
+
return lastSessionEndedAt(metrics) || metrics.createdAt;
|
|
1731
|
+
}
|
|
1732
|
+
|
|
1572
1733
|
function existingSourceIdSet(metrics) {
|
|
1573
1734
|
const ids = new Set();
|
|
1574
1735
|
for (const session of metrics.sessions || []) {
|
|
@@ -1587,9 +1748,8 @@ function uniqueSourceModels(sources) {
|
|
|
1587
1748
|
return seen;
|
|
1588
1749
|
}
|
|
1589
1750
|
|
|
1590
|
-
function
|
|
1591
|
-
|
|
1592
|
-
const ranked = [...sources].sort((a, b) => {
|
|
1751
|
+
function rankSources(sources) {
|
|
1752
|
+
return [...sources].sort((a, b) => {
|
|
1593
1753
|
const ta = a.totalTokens ?? 0;
|
|
1594
1754
|
const tb = b.totalTokens ?? 0;
|
|
1595
1755
|
if (tb !== ta) return tb - ta;
|
|
@@ -1597,12 +1757,79 @@ function primaryModelFromSources(sources) {
|
|
|
1597
1757
|
if (platformCmp !== 0) return platformCmp;
|
|
1598
1758
|
return String(a.id || '').localeCompare(String(b.id || ''));
|
|
1599
1759
|
});
|
|
1600
|
-
|
|
1760
|
+
}
|
|
1761
|
+
|
|
1762
|
+
function primaryModelFromSources(sources) {
|
|
1763
|
+
if (!sources || !sources.length) return null;
|
|
1764
|
+
const top = rankSources(sources)[0];
|
|
1765
|
+
const model = top && top.model;
|
|
1601
1766
|
return model == null || model === '' ? null : String(model);
|
|
1602
1767
|
}
|
|
1603
1768
|
|
|
1769
|
+
function primaryPlatformFromSources(sources) {
|
|
1770
|
+
if (!sources || !sources.length) return null;
|
|
1771
|
+
const platform = rankSources(sources)[0] && rankSources(sources)[0].platform;
|
|
1772
|
+
return platform && VALID_PLATFORMS.has(platform) ? platform : null;
|
|
1773
|
+
}
|
|
1774
|
+
|
|
1604
1775
|
function hasSpendOverride(opts) {
|
|
1605
|
-
return
|
|
1776
|
+
return Boolean(
|
|
1777
|
+
opts
|
|
1778
|
+
&& (opts.inputTokens != null || opts.outputTokens != null || opts.totalTokens != null || opts.costUsd != null || opts.ampCredits != null),
|
|
1779
|
+
);
|
|
1780
|
+
}
|
|
1781
|
+
|
|
1782
|
+
function reportedHasSpendNumbers(reported) {
|
|
1783
|
+
if (!reported) return false;
|
|
1784
|
+
return (
|
|
1785
|
+
reported.inputTokens != null
|
|
1786
|
+
|| reported.outputTokens != null
|
|
1787
|
+
|| reported.totalTokens != null
|
|
1788
|
+
|| reported.costUsd != null
|
|
1789
|
+
|| reported.ampCredits != null
|
|
1790
|
+
);
|
|
1791
|
+
}
|
|
1792
|
+
|
|
1793
|
+
function sourceAmpCredits(sources) {
|
|
1794
|
+
let sum = null;
|
|
1795
|
+
for (const src of sources || []) {
|
|
1796
|
+
sum = addNullable(sum, numOrNull(src.ampCredits));
|
|
1797
|
+
}
|
|
1798
|
+
return sum;
|
|
1799
|
+
}
|
|
1800
|
+
|
|
1801
|
+
function resolveSessionSpend(opts, reported, sources) {
|
|
1802
|
+
const flags = opts || {};
|
|
1803
|
+
const self = reported || emptyMetricsFields();
|
|
1804
|
+
const fromSources = sessionTotalsFromSources(sources || []);
|
|
1805
|
+
const sourceCredits = sourceAmpCredits(sources || []);
|
|
1806
|
+
const flagInput = numOrNull(flags.inputTokens);
|
|
1807
|
+
const flagOutput = numOrNull(flags.outputTokens);
|
|
1808
|
+
const flagTotal = numOrNull(flags.totalTokens);
|
|
1809
|
+
const flagCost = numOrNull(flags.costUsd);
|
|
1810
|
+
const flagCredits = numOrNull(flags.ampCredits);
|
|
1811
|
+
const inputTokens = firstNonNull(flagInput, self.inputTokens, fromSources.inputTokens);
|
|
1812
|
+
const outputTokens = firstNonNull(flagOutput, self.outputTokens, fromSources.outputTokens);
|
|
1813
|
+
let totalTokens = firstNonNull(flagTotal, self.totalTokens, fromSources.totalTokens);
|
|
1814
|
+
if (totalTokens == null && (inputTokens != null || outputTokens != null)) {
|
|
1815
|
+
totalTokens = (inputTokens ?? 0) + (outputTokens ?? 0);
|
|
1816
|
+
}
|
|
1817
|
+
const costUsd = firstNonNull(flagCost, self.costUsd, fromSources.costUsd);
|
|
1818
|
+
const ampCredits = firstNonNull(flagCredits, self.ampCredits, sourceCredits);
|
|
1819
|
+
let spendSource = 'unreported';
|
|
1820
|
+
if (self.spendSource) spendSource = String(self.spendSource);
|
|
1821
|
+
else if (hasSpendOverride(flags)) spendSource = 'flag';
|
|
1822
|
+
else if (reportedHasSpendNumbers(self)) spendSource = 'self-report';
|
|
1823
|
+
else if (
|
|
1824
|
+
fromSources.inputTokens != null
|
|
1825
|
+
|| fromSources.outputTokens != null
|
|
1826
|
+
|| fromSources.totalTokens != null
|
|
1827
|
+
|| fromSources.costUsd != null
|
|
1828
|
+
|| sourceCredits != null
|
|
1829
|
+
) {
|
|
1830
|
+
spendSource = 'adapter';
|
|
1831
|
+
}
|
|
1832
|
+
return { inputTokens, outputTokens, totalTokens, costUsd, ampCredits, spendSource };
|
|
1606
1833
|
}
|
|
1607
1834
|
|
|
1608
1835
|
function sessionTotalsFromFlags(opts) {
|
|
@@ -1634,7 +1861,7 @@ function sessionTotalsFromSources(sources) {
|
|
|
1634
1861
|
return { inputTokens, outputTokens, totalTokens, costUsd };
|
|
1635
1862
|
}
|
|
1636
1863
|
|
|
1637
|
-
function runCollectSpend(metrics, windowStart, windowEnd) {
|
|
1864
|
+
function runCollectSpend(metrics, windowStart, windowEnd, extra = {}) {
|
|
1638
1865
|
try {
|
|
1639
1866
|
return collectSpend({
|
|
1640
1867
|
cwd: process.cwd(),
|
|
@@ -1643,60 +1870,165 @@ function runCollectSpend(metrics, windowStart, windowEnd) {
|
|
|
1643
1870
|
existingSourceIds: existingSourceIdSet(metrics),
|
|
1644
1871
|
env: process.env,
|
|
1645
1872
|
homedir: process.env.HOME,
|
|
1873
|
+
platforms: extra.platforms,
|
|
1874
|
+
ampThreadId: extra.ampThreadId,
|
|
1875
|
+
ampCli: extra.ampCli === true,
|
|
1876
|
+
exportAmpThread: extra.exportAmpThread,
|
|
1646
1877
|
});
|
|
1647
1878
|
} catch {
|
|
1648
1879
|
return { sources: [], byPlatform: defaultSpendByPlatform(), byModel: [], notes: [] };
|
|
1649
1880
|
}
|
|
1650
1881
|
}
|
|
1651
1882
|
|
|
1652
|
-
function applyCollectedSessionFields(session, sources, resolvedModel, opts) {
|
|
1653
|
-
session.sources = sources;
|
|
1654
|
-
const uniqueModels = uniqueSourceModels(sources);
|
|
1655
|
-
session.model = primaryModelFromSources(sources) ||
|
|
1883
|
+
function applyCollectedSessionFields(session, sources, resolvedModel, opts, reported) {
|
|
1884
|
+
session.sources = sources || [];
|
|
1885
|
+
const uniqueModels = uniqueSourceModels(session.sources);
|
|
1886
|
+
if (!session.model) session.model = resolvedModel || primaryModelFromSources(session.sources) || null;
|
|
1887
|
+
if (!session.platform) session.platform = primaryPlatformFromSources(session.sources) || null;
|
|
1656
1888
|
if (uniqueModels.length > 1) session.models = uniqueModels;
|
|
1657
|
-
const
|
|
1658
|
-
session.inputTokens =
|
|
1659
|
-
session.outputTokens =
|
|
1660
|
-
session.totalTokens =
|
|
1661
|
-
session.costUsd =
|
|
1889
|
+
const spend = resolveSessionSpend(opts, reported, session.sources);
|
|
1890
|
+
session.inputTokens = spend.inputTokens;
|
|
1891
|
+
session.outputTokens = spend.outputTokens;
|
|
1892
|
+
session.totalTokens = spend.totalTokens;
|
|
1893
|
+
session.costUsd = spend.costUsd;
|
|
1894
|
+
session.ampCredits = spend.ampCredits;
|
|
1895
|
+
session.spendSource = spend.spendSource;
|
|
1896
|
+
}
|
|
1897
|
+
|
|
1898
|
+
function sessionTotalsLookOverridden(session) {
|
|
1899
|
+
const fromSources = sessionTotalsFromSources(session.sources || []);
|
|
1900
|
+
return ['inputTokens', 'outputTokens', 'totalTokens', 'costUsd'].some((key) => {
|
|
1901
|
+
const sessionVal = numOrNull(session[key]);
|
|
1902
|
+
const sourceVal = numOrNull(fromSources[key]);
|
|
1903
|
+
if (sessionVal == null) return false;
|
|
1904
|
+
if (sourceVal == null) return true;
|
|
1905
|
+
return sessionVal !== sourceVal;
|
|
1906
|
+
});
|
|
1907
|
+
}
|
|
1908
|
+
|
|
1909
|
+
function metricsBackfillLastSession(projectDir, changeName) {
|
|
1910
|
+
const resolved = resolveMetricsFile(projectDir, changeName);
|
|
1911
|
+
if (resolved.missing) return { filePath: resolved.filePath, added: 0, missing: true };
|
|
1912
|
+
return metricsBackfillFile(resolved.filePath, changeName);
|
|
1913
|
+
}
|
|
1914
|
+
|
|
1915
|
+
function metricsBackfillFile(filePath, changeName) {
|
|
1916
|
+
const nowIso = new Date().toISOString();
|
|
1917
|
+
const metrics = loadMetricsFile(filePath, changeName, nowIso);
|
|
1918
|
+
const sessions = metrics.sessions || [];
|
|
1919
|
+
if (!sessions.length) return { filePath, added: 0, empty: true };
|
|
1920
|
+
const last = sessions[sessions.length - 1];
|
|
1921
|
+
const windowStart = last.startedAt || collectWindowStart(metrics);
|
|
1922
|
+
const lastPlatform = last && last.platform;
|
|
1923
|
+
const collected = runCollectSpend(metrics, windowStart, nowIso, {
|
|
1924
|
+
ampThreadId: last && last.threadId || ampThreadIdFromEnv(process.env) || null,
|
|
1925
|
+
ampCli: lastPlatform === 'amp',
|
|
1926
|
+
});
|
|
1927
|
+
const incoming = collected.sources || [];
|
|
1928
|
+
if (!incoming.length) return { filePath, added: 0 };
|
|
1929
|
+
const overridden = sessionTotalsLookOverridden(last);
|
|
1930
|
+
const merged = [...(last.sources || []), ...incoming];
|
|
1931
|
+
const keepReportedTotals = overridden
|
|
1932
|
+
|| last.spendSource === 'flag'
|
|
1933
|
+
|| last.spendSource === 'self-report'
|
|
1934
|
+
|| (last.spendSource && last.spendSource !== 'adapter' && last.spendSource !== 'unreported');
|
|
1935
|
+
if (keepReportedTotals) {
|
|
1936
|
+
last.sources = merged;
|
|
1937
|
+
const uniqueModels = uniqueSourceModels(merged);
|
|
1938
|
+
if (uniqueModels.length > 1) last.models = uniqueModels;
|
|
1939
|
+
} else {
|
|
1940
|
+
applyCollectedSessionFields(last, merged, last.model, {}, {
|
|
1941
|
+
model: last.model,
|
|
1942
|
+
platform: last.platform,
|
|
1943
|
+
});
|
|
1944
|
+
}
|
|
1945
|
+
metrics.updatedAt = nowIso;
|
|
1946
|
+
recomputeMetricsAggregates(metrics);
|
|
1947
|
+
saveMetricsFile(filePath, metrics);
|
|
1948
|
+
return { filePath, added: incoming.length };
|
|
1949
|
+
}
|
|
1950
|
+
|
|
1951
|
+
function adapterSourceName(platform, via) {
|
|
1952
|
+
if (platform === 'claude') return 'claude-jsonl';
|
|
1953
|
+
if (platform === 'amp') return via === 'amp-cli' ? 'amp-cli' : 'amp-thread';
|
|
1954
|
+
if (platform === 'cursor') return 'cursor-hook';
|
|
1955
|
+
return null;
|
|
1956
|
+
}
|
|
1957
|
+
|
|
1958
|
+
function spendTuple(obj) {
|
|
1959
|
+
return {
|
|
1960
|
+
inputTokens: numOrNull(obj && obj.inputTokens),
|
|
1961
|
+
outputTokens: numOrNull(obj && obj.outputTokens),
|
|
1962
|
+
totalTokens: numOrNull(obj && obj.totalTokens),
|
|
1963
|
+
costUsd: numOrNull(obj && obj.costUsd),
|
|
1964
|
+
ampCredits: numOrNull(obj && obj.ampCredits),
|
|
1965
|
+
};
|
|
1966
|
+
}
|
|
1967
|
+
|
|
1968
|
+
function spendTuplesMatch(a, b) {
|
|
1969
|
+
return ['inputTokens', 'outputTokens', 'totalTokens', 'costUsd'].every((key) => {
|
|
1970
|
+
const left = a[key];
|
|
1971
|
+
const right = b[key];
|
|
1972
|
+
if (left == null && right == null) return true;
|
|
1973
|
+
return left === right;
|
|
1974
|
+
});
|
|
1975
|
+
}
|
|
1976
|
+
|
|
1977
|
+
function addSpendNums(target, nums) {
|
|
1978
|
+
target.inputTokens = addNullable(target.inputTokens, nums.inputTokens);
|
|
1979
|
+
target.outputTokens = addNullable(target.outputTokens, nums.outputTokens);
|
|
1980
|
+
target.totalTokens = addNullable(target.totalTokens, nums.totalTokens);
|
|
1981
|
+
target.costUsd = addNullable(target.costUsd, nums.costUsd);
|
|
1982
|
+
target.ampCredits = addNullable(target.ampCredits, nums.ampCredits);
|
|
1662
1983
|
}
|
|
1663
1984
|
|
|
1664
1985
|
function recomputeSpendMaps(metrics) {
|
|
1665
1986
|
const byPlatform = defaultSpendByPlatform();
|
|
1666
1987
|
const byModel = new Map();
|
|
1988
|
+
|
|
1989
|
+
function addModelRow(model, platform, nums) {
|
|
1990
|
+
if (!model) return;
|
|
1991
|
+
const key = `${model}::${platform || ''}`;
|
|
1992
|
+
const row = byModel.get(key) || {
|
|
1993
|
+
model,
|
|
1994
|
+
platform: platform || null,
|
|
1995
|
+
inputTokens: null,
|
|
1996
|
+
outputTokens: null,
|
|
1997
|
+
totalTokens: null,
|
|
1998
|
+
costUsd: null,
|
|
1999
|
+
ampCredits: null,
|
|
2000
|
+
};
|
|
2001
|
+
addSpendNums(row, nums);
|
|
2002
|
+
byModel.set(key, row);
|
|
2003
|
+
}
|
|
2004
|
+
|
|
2005
|
+
function addSourceRow(src) {
|
|
2006
|
+
const nums = spendTuple(src);
|
|
2007
|
+
if (src.platform && byPlatform[src.platform]) {
|
|
2008
|
+
addSpendNums(byPlatform[src.platform], nums);
|
|
2009
|
+
const label = adapterSourceName(src.platform);
|
|
2010
|
+
if (label) byPlatform[src.platform].source = label;
|
|
2011
|
+
}
|
|
2012
|
+
addModelRow(src.model, src.platform, nums);
|
|
2013
|
+
}
|
|
2014
|
+
|
|
1667
2015
|
for (const session of metrics.sessions || []) {
|
|
1668
|
-
|
|
1669
|
-
|
|
1670
|
-
|
|
1671
|
-
|
|
1672
|
-
|
|
1673
|
-
|
|
1674
|
-
|
|
1675
|
-
|
|
1676
|
-
|
|
1677
|
-
|
|
1678
|
-
|
|
1679
|
-
|
|
1680
|
-
|
|
1681
|
-
if (src.model) {
|
|
1682
|
-
const key = `${src.model}::${src.platform || ''}`;
|
|
1683
|
-
const row = byModel.get(key) || {
|
|
1684
|
-
model: src.model,
|
|
1685
|
-
platform: src.platform || null,
|
|
1686
|
-
inputTokens: null,
|
|
1687
|
-
outputTokens: null,
|
|
1688
|
-
totalTokens: null,
|
|
1689
|
-
costUsd: null,
|
|
1690
|
-
ampCredits: null,
|
|
1691
|
-
};
|
|
1692
|
-
row.inputTokens = addNullable(row.inputTokens, numOrNull(src.inputTokens));
|
|
1693
|
-
row.outputTokens = addNullable(row.outputTokens, numOrNull(src.outputTokens));
|
|
1694
|
-
row.totalTokens = addNullable(row.totalTokens, numOrNull(src.totalTokens));
|
|
1695
|
-
row.costUsd = addNullable(row.costUsd, numOrNull(src.costUsd));
|
|
1696
|
-
row.ampCredits = addNullable(row.ampCredits, numOrNull(src.ampCredits));
|
|
1697
|
-
byModel.set(key, row);
|
|
1698
|
-
}
|
|
2016
|
+
const sessionNums = spendTuple(session);
|
|
2017
|
+
const sourceTotals = sessionTotalsFromSources(session.sources || []);
|
|
2018
|
+
sourceTotals.ampCredits = sourceAmpCredits(session.sources || []);
|
|
2019
|
+
const sources = session.sources || [];
|
|
2020
|
+
const sourcesMatchSession = sources.length > 0 && spendTuplesMatch(sessionNums, sourceTotals);
|
|
2021
|
+
|
|
2022
|
+
if (sourcesMatchSession) {
|
|
2023
|
+
for (const src of sources) addSourceRow(src);
|
|
2024
|
+
continue;
|
|
2025
|
+
}
|
|
2026
|
+
|
|
2027
|
+
if (session.platform && byPlatform[session.platform]) {
|
|
2028
|
+
addSpendNums(byPlatform[session.platform], sessionNums);
|
|
1699
2029
|
}
|
|
2030
|
+
addModelRow(session.model, session.platform, sessionNums);
|
|
2031
|
+
for (const src of sources) addSourceRow(src);
|
|
1700
2032
|
}
|
|
1701
2033
|
metrics.spendByPlatform = byPlatform;
|
|
1702
2034
|
metrics.spendByModel = [...byModel.values()];
|
|
@@ -1741,11 +2073,17 @@ function recomputeMetricsAggregates(metrics) {
|
|
|
1741
2073
|
recomputeSpendMaps(metrics);
|
|
1742
2074
|
}
|
|
1743
2075
|
|
|
1744
|
-
function metricsRecordSessionStart(projectDir, changeName, role) {
|
|
2076
|
+
function metricsRecordSessionStart(projectDir, changeName, role, client = {}) {
|
|
1745
2077
|
const filePath = metricsFilePath(projectDir, changeName);
|
|
1746
2078
|
const nowIso = new Date().toISOString();
|
|
1747
2079
|
const metrics = loadMetricsFile(filePath, changeName, nowIso);
|
|
1748
|
-
metrics.pending = {
|
|
2080
|
+
metrics.pending = {
|
|
2081
|
+
startedAt: nowIso,
|
|
2082
|
+
role: role || '',
|
|
2083
|
+
platform: client.platform || null,
|
|
2084
|
+
threadId: client.threadId || null,
|
|
2085
|
+
clientSource: client.source || null,
|
|
2086
|
+
};
|
|
1749
2087
|
metrics.updatedAt = nowIso;
|
|
1750
2088
|
saveMetricsFile(filePath, metrics);
|
|
1751
2089
|
return filePath;
|
|
@@ -1757,11 +2095,21 @@ function metricsRecordSessionEnd(projectDir, fields, opts = {}) {
|
|
|
1757
2095
|
const metrics = loadMetricsFile(filePath, fields.changeName, nowIso);
|
|
1758
2096
|
const startedAt = isoOrNull(opts.startedAt) || (metrics.pending && metrics.pending.startedAt) || null;
|
|
1759
2097
|
const durationMs = startedAt ? Math.max(0, Date.parse(nowIso) - Date.parse(startedAt)) : null;
|
|
1760
|
-
const
|
|
1761
|
-
const
|
|
1762
|
-
const
|
|
1763
|
-
|
|
1764
|
-
|
|
2098
|
+
const reported = opts.reported || fields.metrics || emptyMetricsFields();
|
|
2099
|
+
const resolvedModel = opts.model === undefined ? resolveModel(opts, process.env, reported) : opts.model;
|
|
2100
|
+
const windowStart = collectWindowStart(metrics);
|
|
2101
|
+
const pending = metrics.pending || {};
|
|
2102
|
+
const platform = opts.platform || pending.platform || null;
|
|
2103
|
+
const collectAll = opts.collect === true;
|
|
2104
|
+
const platforms = collectAll ? undefined : (platform ? [platform] : []);
|
|
2105
|
+
const shouldCollect = collectAll || (Array.isArray(platforms) && platforms.length > 0);
|
|
2106
|
+
const collected = shouldCollect
|
|
2107
|
+
? runCollectSpend(metrics, windowStart, nowIso, {
|
|
2108
|
+
platforms,
|
|
2109
|
+
ampThreadId: opts.ampThreadId || pending.threadId || ampThreadIdFromEnv(process.env) || null,
|
|
2110
|
+
ampCli: collectAll || platform === 'amp',
|
|
2111
|
+
})
|
|
2112
|
+
: { sources: [] };
|
|
1765
2113
|
const session = {
|
|
1766
2114
|
startedAt,
|
|
1767
2115
|
endedAt: nowIso,
|
|
@@ -1771,21 +2119,28 @@ function metricsRecordSessionEnd(projectDir, fields, opts = {}) {
|
|
|
1771
2119
|
runtime: fields.runtime || 'local',
|
|
1772
2120
|
agentId: fields.agentId || 'none',
|
|
1773
2121
|
model: resolvedModel || null,
|
|
1774
|
-
platform: opts.platform || null,
|
|
2122
|
+
platform: opts.platform || pending.platform || null,
|
|
2123
|
+
threadId: opts.ampThreadId || pending.threadId || null,
|
|
1775
2124
|
tasks: fields.tasks || null,
|
|
1776
2125
|
sources: [],
|
|
1777
2126
|
inputTokens: null,
|
|
1778
2127
|
outputTokens: null,
|
|
1779
2128
|
totalTokens: null,
|
|
1780
2129
|
costUsd: null,
|
|
2130
|
+
ampCredits: null,
|
|
2131
|
+
spendSource: 'unreported',
|
|
1781
2132
|
};
|
|
1782
|
-
applyCollectedSessionFields(session, collected.sources || [], resolvedModel, opts);
|
|
2133
|
+
applyCollectedSessionFields(session, collected.sources || [], resolvedModel, opts, reported);
|
|
1783
2134
|
metrics.sessions.push(session);
|
|
1784
2135
|
metrics.pending = null;
|
|
1785
2136
|
metrics.updatedAt = nowIso;
|
|
1786
2137
|
recomputeMetricsAggregates(metrics);
|
|
1787
|
-
if (session.model == null) warnMissingModel();
|
|
1788
2138
|
saveMetricsFile(filePath, metrics);
|
|
2139
|
+
if (opts.collect === true) metricsBackfillFile(filePath, fields.changeName);
|
|
2140
|
+
const latest = loadMetricsFile(filePath, fields.changeName, nowIso);
|
|
2141
|
+
const last = (latest.sessions || []).at(-1);
|
|
2142
|
+
if (last && last.spendSource === 'unreported') warnUnreportedSelfReport();
|
|
2143
|
+
if (!last || last.model == null) warnMissingModel();
|
|
1789
2144
|
return filePath;
|
|
1790
2145
|
}
|
|
1791
2146
|
|
|
@@ -1794,10 +2149,19 @@ function metricsFinalizeArchive(targetDir, changeName, opts = {}) {
|
|
|
1794
2149
|
const nowIso = new Date().toISOString();
|
|
1795
2150
|
const metrics = loadMetricsFile(filePath, changeName, nowIso);
|
|
1796
2151
|
const windowStart = lastSessionEndedAt(metrics) || metrics.createdAt;
|
|
1797
|
-
const
|
|
1798
|
-
|
|
1799
|
-
|
|
1800
|
-
const
|
|
2152
|
+
const collectAll = opts.collect === true;
|
|
2153
|
+
const platform = opts.platform || null;
|
|
2154
|
+
const platforms = collectAll ? undefined : (platform ? [platform] : []);
|
|
2155
|
+
const shouldCollect = collectAll || (Array.isArray(platforms) && platforms.length > 0);
|
|
2156
|
+
const collected = shouldCollect
|
|
2157
|
+
? runCollectSpend(metrics, windowStart, nowIso, {
|
|
2158
|
+
platforms,
|
|
2159
|
+
ampThreadId: opts.ampThreadId || ampThreadIdFromEnv(process.env) || null,
|
|
2160
|
+
ampCli: collectAll || platform === 'amp',
|
|
2161
|
+
})
|
|
2162
|
+
: { sources: [] };
|
|
2163
|
+
const reported = opts.reported || emptyMetricsFields();
|
|
2164
|
+
const resolvedModel = opts.model === undefined ? resolveModel(opts, process.env, reported) : opts.model;
|
|
1801
2165
|
const session = {
|
|
1802
2166
|
startedAt: nowIso,
|
|
1803
2167
|
endedAt: nowIso,
|
|
@@ -1814,16 +2178,22 @@ function metricsFinalizeArchive(targetDir, changeName, opts = {}) {
|
|
|
1814
2178
|
outputTokens: null,
|
|
1815
2179
|
totalTokens: null,
|
|
1816
2180
|
costUsd: null,
|
|
2181
|
+
ampCredits: null,
|
|
2182
|
+
spendSource: 'unreported',
|
|
1817
2183
|
};
|
|
1818
|
-
applyCollectedSessionFields(session, collected.sources || [], resolvedModel,
|
|
2184
|
+
applyCollectedSessionFields(session, collected.sources || [], resolvedModel, opts, reported);
|
|
1819
2185
|
metrics.sessions.push(session);
|
|
1820
2186
|
metrics.archivedAt = nowIso;
|
|
1821
2187
|
metrics.pending = null;
|
|
1822
2188
|
metrics.updatedAt = nowIso;
|
|
1823
2189
|
recomputeMetricsAggregates(metrics);
|
|
1824
|
-
if (session.model == null) warnMissingModel();
|
|
1825
|
-
if (metrics.spend.costUsd === null) warnMissingUsd();
|
|
1826
2190
|
saveMetricsFile(filePath, metrics);
|
|
2191
|
+
if (opts.collect === true) metricsBackfillFile(filePath, changeName);
|
|
2192
|
+
const latest = loadMetricsFile(filePath, changeName, nowIso);
|
|
2193
|
+
const last = (latest.sessions || []).at(-1);
|
|
2194
|
+
if (last && last.spendSource === 'unreported') warnUnreportedSelfReport();
|
|
2195
|
+
if (!last || last.model == null) warnMissingModel();
|
|
2196
|
+
if (latest.spend.costUsd === null) warnMissingUsd();
|
|
1827
2197
|
return filePath;
|
|
1828
2198
|
}
|
|
1829
2199
|
|
|
@@ -1846,6 +2216,95 @@ function formatMetricsCost(value) {
|
|
|
1846
2216
|
return value == null ? '—' : `$${Number(value).toFixed(2)}`;
|
|
1847
2217
|
}
|
|
1848
2218
|
|
|
2219
|
+
function sessionSpendSourceLabel(session) {
|
|
2220
|
+
const raw = session && session.spendSource;
|
|
2221
|
+
if (raw == null || String(raw).trim() === '') return 'unreported';
|
|
2222
|
+
return String(raw);
|
|
2223
|
+
}
|
|
2224
|
+
|
|
2225
|
+
function renderMetricsSummary(metrics) {
|
|
2226
|
+
const lines = [];
|
|
2227
|
+
const sessions = Array.isArray(metrics.sessions) ? metrics.sessions : [];
|
|
2228
|
+
const unreported = sessions.filter((session) => sessionSpendSourceLabel(session) === 'unreported').length;
|
|
2229
|
+
lines.push(`sessions: ${metrics.totals.sessions}${metrics.totals.cloudSessions ? ` (cloud: ${metrics.totals.cloudSessions})` : ''}`);
|
|
2230
|
+
lines.push(`work time: ${formatMetricsDuration(metrics.totals.durationMs)}`);
|
|
2231
|
+
lines.push(`lead time: ${formatMetricsDuration(metrics.totals.leadTimeMs)}`);
|
|
2232
|
+
lines.push(`tokens: ${formatMetricsNumber(metrics.spend.totalTokens)} (in: ${formatMetricsNumber(metrics.spend.inputTokens)}, out: ${formatMetricsNumber(metrics.spend.outputTokens)})`);
|
|
2233
|
+
lines.push(`cost: ${formatMetricsCost(metrics.spend.costUsd)}`);
|
|
2234
|
+
lines.push(`unreported: ${unreported}`);
|
|
2235
|
+
if (metrics.archivedAt) lines.push(`archived: ${metrics.archivedAt}`);
|
|
2236
|
+
if (metrics.pending) {
|
|
2237
|
+
const pendingClient = metrics.pending.platform
|
|
2238
|
+
? ` ${metrics.pending.platform}${metrics.pending.threadId ? ` ${metrics.pending.threadId}` : ''}`
|
|
2239
|
+
: '';
|
|
2240
|
+
lines.push(`open session since ${metrics.pending.startedAt} (${metrics.pending.role || 'unknown role'}${pendingClient})`);
|
|
2241
|
+
}
|
|
2242
|
+
|
|
2243
|
+
const phaseOrder = ['explore', 'design', 'spec', 'review', 'apply', 'archive', 'other'];
|
|
2244
|
+
const phaseKeys = phaseOrder.filter((key) => metrics.phases[key]);
|
|
2245
|
+
if (phaseKeys.length) {
|
|
2246
|
+
lines.push('');
|
|
2247
|
+
lines.push('phase sessions time tokens cost roles models');
|
|
2248
|
+
for (const key of phaseKeys) {
|
|
2249
|
+
const phase = metrics.phases[key];
|
|
2250
|
+
lines.push([
|
|
2251
|
+
key.padEnd(10),
|
|
2252
|
+
String(phase.sessions).padEnd(9),
|
|
2253
|
+
formatMetricsDuration(phase.durationMs).padEnd(9),
|
|
2254
|
+
formatMetricsNumber(phase.totalTokens).padEnd(9),
|
|
2255
|
+
formatMetricsCost(phase.costUsd).padEnd(9),
|
|
2256
|
+
(phase.agents.join(', ') || '—').padEnd(20),
|
|
2257
|
+
phase.models.join(', ') || '—',
|
|
2258
|
+
].join(' '));
|
|
2259
|
+
}
|
|
2260
|
+
}
|
|
2261
|
+
|
|
2262
|
+
const byPlatform = metrics.spendByPlatform || defaultSpendByPlatform();
|
|
2263
|
+
lines.push('');
|
|
2264
|
+
lines.push('by platform:');
|
|
2265
|
+
lines.push('platform tokens cost credits source');
|
|
2266
|
+
for (const key of ['cursor', 'claude', 'amp']) {
|
|
2267
|
+
const row = byPlatform[key] || emptyPlatformSpend();
|
|
2268
|
+
lines.push([
|
|
2269
|
+
key.padEnd(10),
|
|
2270
|
+
formatMetricsNumber(row.totalTokens).padEnd(9),
|
|
2271
|
+
formatMetricsCost(row.costUsd).padEnd(9),
|
|
2272
|
+
formatMetricsNumber(row.ampCredits).padEnd(9),
|
|
2273
|
+
row.source || 'none',
|
|
2274
|
+
].join(' '));
|
|
2275
|
+
}
|
|
2276
|
+
|
|
2277
|
+
lines.push('');
|
|
2278
|
+
lines.push('by model:');
|
|
2279
|
+
lines.push('model platform tokens cost credits');
|
|
2280
|
+
const byModel = Array.isArray(metrics.spendByModel) ? metrics.spendByModel : [];
|
|
2281
|
+
if (!byModel.length) {
|
|
2282
|
+
lines.push('— — — — —');
|
|
2283
|
+
} else {
|
|
2284
|
+
for (const row of byModel) {
|
|
2285
|
+
lines.push([
|
|
2286
|
+
String(row.model || '—').padEnd(20),
|
|
2287
|
+
String(row.platform || '—').padEnd(10),
|
|
2288
|
+
formatMetricsNumber(row.totalTokens).padEnd(9),
|
|
2289
|
+
formatMetricsCost(row.costUsd).padEnd(9),
|
|
2290
|
+
formatMetricsNumber(row.ampCredits),
|
|
2291
|
+
].join(' '));
|
|
2292
|
+
}
|
|
2293
|
+
}
|
|
2294
|
+
|
|
2295
|
+
if (sessions.length) {
|
|
2296
|
+
lines.push('');
|
|
2297
|
+
lines.push('recent sessions:');
|
|
2298
|
+
for (const session of sessions.slice(-5)) {
|
|
2299
|
+
const spendLabel = session.totalTokens != null || session.costUsd != null
|
|
2300
|
+
? ` — ${formatMetricsNumber(session.totalTokens)} tok, ${formatMetricsCost(session.costUsd)}`
|
|
2301
|
+
: '';
|
|
2302
|
+
lines.push(`- ${session.endedAt} ${String(session.phase || '').padEnd(7)} ${formatMetricsDuration(session.durationMs).padEnd(9)} ${session.role || '(no role)'}${session.model ? ` [${session.model}]` : ''} (${sessionSpendSourceLabel(session)})${spendLabel}`);
|
|
2303
|
+
}
|
|
2304
|
+
}
|
|
2305
|
+
return lines;
|
|
2306
|
+
}
|
|
2307
|
+
|
|
1849
2308
|
function resolveMetricsFile(projectDir, changeName) {
|
|
1850
2309
|
const activePath = metricsFilePath(projectDir, changeName);
|
|
1851
2310
|
if (existsSync(activePath)) return { filePath: activePath, archived: false };
|
|
@@ -3015,7 +3474,11 @@ program
|
|
|
3015
3474
|
.option('--force', 'confirm archiving without merge when delta specs exist', false)
|
|
3016
3475
|
.option('--model <name>', 'LLM product id recorded on the Archiver session')
|
|
3017
3476
|
.option('--platform <platform>', 'Session platform: cursor | claude | amp')
|
|
3018
|
-
.option('--
|
|
3477
|
+
.option('--input-tokens <n>', 'Input tokens spent in the Archiver session')
|
|
3478
|
+
.option('--output-tokens <n>', 'Output tokens spent in the Archiver session')
|
|
3479
|
+
.option('--total-tokens <n>', 'Total tokens spent in the Archiver session (default: input + output)')
|
|
3480
|
+
.option('--cost-usd <usd>', 'Cost of the Archiver session in USD')
|
|
3481
|
+
.option('--collect', 'Additionally collect local spend adapters', false)
|
|
3019
3482
|
.action((name, opts) => {
|
|
3020
3483
|
const projectDir = process.cwd();
|
|
3021
3484
|
const fail = (msg) => {
|
|
@@ -3135,6 +3598,11 @@ program
|
|
|
3135
3598
|
if (existsSync(archivedHandoffPath)) {
|
|
3136
3599
|
priorFields = fieldsFromSections(name, parseHandoffMarkdown(readFileSync(archivedHandoffPath, 'utf-8')));
|
|
3137
3600
|
}
|
|
3601
|
+
const reported = priorFields.metrics || emptyMetricsFields();
|
|
3602
|
+
const archivePlatform = resolvePlatform(opts, process.env, reported);
|
|
3603
|
+
const archiveModel = resolveModel(opts, process.env, reported);
|
|
3604
|
+
if (archivePlatform.warn) console.error(archivePlatform.warn);
|
|
3605
|
+
printMetricsSectionWarnings(reported, Boolean(archivePlatform.warn));
|
|
3138
3606
|
const runtimeResult = resolveRuntime({}, process.env, priorFields);
|
|
3139
3607
|
const progress = parseTasksProgress(targetDir);
|
|
3140
3608
|
const fields = {
|
|
@@ -3151,6 +3619,7 @@ program
|
|
|
3151
3619
|
constraints: 'Pipeline complete — no next session.',
|
|
3152
3620
|
runtime: runtimeResult.value || 'local',
|
|
3153
3621
|
agentId: resolveAgentId({}, process.env, priorFields),
|
|
3622
|
+
metrics: reported,
|
|
3154
3623
|
status: 'archived',
|
|
3155
3624
|
tasks: progress ? `${progress.done}/${progress.total}` : '',
|
|
3156
3625
|
review: parseReviewVerdict(targetDir) || '',
|
|
@@ -3159,12 +3628,17 @@ program
|
|
|
3159
3628
|
writeFileSync(join(targetDir, 'handoff.md'), `${buildHandoffMarkdown(fields).trim()}\n`);
|
|
3160
3629
|
const memoryPath = persistMemoryFromHandoff(projectDir, fields);
|
|
3161
3630
|
const metricsPath = metricsFinalizeArchive(targetDir, name, {
|
|
3162
|
-
model:
|
|
3163
|
-
platform:
|
|
3631
|
+
model: archiveModel,
|
|
3632
|
+
platform: archivePlatform.value || null,
|
|
3164
3633
|
runtime: fields.runtime,
|
|
3165
3634
|
agentId: fields.agentId,
|
|
3166
3635
|
tasks: fields.tasks,
|
|
3167
|
-
collect: opts.collect
|
|
3636
|
+
collect: opts.collect === true,
|
|
3637
|
+
inputTokens: opts.inputTokens,
|
|
3638
|
+
outputTokens: opts.outputTokens,
|
|
3639
|
+
totalTokens: opts.totalTokens,
|
|
3640
|
+
costUsd: opts.costUsd,
|
|
3641
|
+
reported,
|
|
3168
3642
|
});
|
|
3169
3643
|
|
|
3170
3644
|
console.log(`change: ${name}`);
|
|
@@ -3174,6 +3648,10 @@ program
|
|
|
3174
3648
|
console.log(`handoff: ${join(targetRel, 'handoff.md')} (next_command: none)`);
|
|
3175
3649
|
console.log(`memory: ${memoryPath.replace(`${projectDir}/`, '')}`);
|
|
3176
3650
|
console.log(`metrics: ${metricsPath.replace(`${projectDir}/`, '')} (archived_at set)`);
|
|
3651
|
+
try {
|
|
3652
|
+
const archivedMetrics = loadMetricsFile(metricsPath, name, new Date().toISOString());
|
|
3653
|
+
for (const line of renderMetricsSummary(archivedMetrics)) console.log(line);
|
|
3654
|
+
} catch {}
|
|
3177
3655
|
log.ok(`archived ${name}`);
|
|
3178
3656
|
});
|
|
3179
3657
|
|
|
@@ -3381,7 +3859,7 @@ program
|
|
|
3381
3859
|
.option('--total-tokens <n>', 'Total tokens spent in this session (default: input + output)')
|
|
3382
3860
|
.option('--cost-usd <usd>', 'Cost of this session in USD')
|
|
3383
3861
|
.option('--no-metrics', 'Skip recording this session into metrics.json')
|
|
3384
|
-
.option('--
|
|
3862
|
+
.option('--collect', 'Additionally collect local spend adapters', false)
|
|
3385
3863
|
.action((changeName, opts) => {
|
|
3386
3864
|
const projectDir = process.cwd();
|
|
3387
3865
|
const resolved = resolveHandoffChange(projectDir, changeName);
|
|
@@ -3439,12 +3917,19 @@ program
|
|
|
3439
3917
|
log.warn(`Memory JSON empty or missing at ${memoryPath}`);
|
|
3440
3918
|
}
|
|
3441
3919
|
if (opts.metrics !== false && existsSync(changeDir)) {
|
|
3442
|
-
const
|
|
3920
|
+
const client = resolveRestoreClient({
|
|
3921
|
+
env: process.env,
|
|
3922
|
+
cwd: projectDir,
|
|
3923
|
+
homedir: process.env.HOME,
|
|
3924
|
+
platform: opts.platform,
|
|
3925
|
+
});
|
|
3926
|
+
const metricsPath = metricsRecordSessionStart(projectDir, name, fields ? fields.nextRole : '', client);
|
|
3927
|
+
const clientLabel = client.platform
|
|
3928
|
+
? `${client.platform}${client.threadId ? ` ${client.threadId}` : ''} (${client.source})`
|
|
3929
|
+
: 'unknown — pass --platform or fill ## Metrics';
|
|
3443
3930
|
log.ok(`metrics: session start recorded (${metricsPath.replace(`${projectDir}/`, '')})`);
|
|
3931
|
+
log.info(`metrics: client ${clientLabel}`);
|
|
3444
3932
|
}
|
|
3445
|
-
const spendHook = ensureCursorSpendHook(projectDir);
|
|
3446
|
-
if (spendHook.error) log.warn(`Cursor spend hook: ${spendHook.error}`);
|
|
3447
|
-
else if (spendHook.hooksJson) log.ok('Cursor spend hook installed — restart Cursor once to activate it');
|
|
3448
3933
|
return;
|
|
3449
3934
|
}
|
|
3450
3935
|
|
|
@@ -3512,14 +3997,17 @@ program
|
|
|
3512
3997
|
return;
|
|
3513
3998
|
}
|
|
3514
3999
|
|
|
3515
|
-
const
|
|
4000
|
+
const reported = fields.metrics || emptyMetricsFields();
|
|
4001
|
+
const metricsPreview = loadMetricsFile(metricsFilePath(projectDir, name), name, new Date().toISOString());
|
|
4002
|
+
const platformResult = resolvePlatform(opts, process.env, reported, metricsPreview.pending);
|
|
3516
4003
|
if (platformResult.error) {
|
|
3517
4004
|
log.err(platformResult.error);
|
|
3518
4005
|
process.exitCode = 1;
|
|
3519
4006
|
return;
|
|
3520
4007
|
}
|
|
3521
4008
|
if (platformResult.warn) console.error(platformResult.warn);
|
|
3522
|
-
|
|
4009
|
+
printMetricsSectionWarnings(reported, Boolean(platformResult.warn));
|
|
4010
|
+
const resolvedModel = resolveModel(opts, process.env, reported);
|
|
3523
4011
|
|
|
3524
4012
|
const prompt = buildNextSessionPrompt(fields, agentLanguage).replace(/^\n+|\n+$/g, '');
|
|
3525
4013
|
fields.prompt = prompt;
|
|
@@ -3530,10 +4018,6 @@ program
|
|
|
3530
4018
|
const memoryPath = persistMemoryFromHandoff(projectDir, fields);
|
|
3531
4019
|
console.error(pc.green(' ✓'), `Memory JSON upserted: ${memoryPath}`);
|
|
3532
4020
|
|
|
3533
|
-
const spendHook = ensureCursorSpendHook(projectDir);
|
|
3534
|
-
if (spendHook.error) console.error(pc.yellow(' !'), `Cursor spend hook: ${spendHook.error}`);
|
|
3535
|
-
else if (spendHook.hooksJson) console.error(pc.green(' ✓'), 'Cursor spend hook installed — restart Cursor once to activate it');
|
|
3536
|
-
|
|
3537
4021
|
if (opts.metrics !== false) {
|
|
3538
4022
|
const metricsPath = metricsRecordSessionEnd(projectDir, fields, {
|
|
3539
4023
|
startedAt: opts.startedAt,
|
|
@@ -3543,7 +4027,9 @@ program
|
|
|
3543
4027
|
outputTokens: opts.outputTokens,
|
|
3544
4028
|
totalTokens: opts.totalTokens,
|
|
3545
4029
|
costUsd: opts.costUsd,
|
|
3546
|
-
collect: opts.collect
|
|
4030
|
+
collect: opts.collect === true,
|
|
4031
|
+
ampThreadId: (metricsPreview.pending && metricsPreview.pending.threadId) || ampThreadIdFromEnv(process.env) || null,
|
|
4032
|
+
reported,
|
|
3547
4033
|
});
|
|
3548
4034
|
console.error(pc.green(' ✓'), `metrics.json updated: ${metricsPath.replace(`${projectDir}/`, '')}`);
|
|
3549
4035
|
}
|
|
@@ -3558,9 +4044,11 @@ program
|
|
|
3558
4044
|
.command('metrics [change-name]')
|
|
3559
4045
|
.description('Show recorded session metrics for a change: time per phase, tokens, cost, roles, and models')
|
|
3560
4046
|
.option('--json', 'Print raw metrics.json', false)
|
|
4047
|
+
.option('--collect', 'Backfill the last session from local spend adapters without adding a new session', false)
|
|
3561
4048
|
.action((changeName, opts) => {
|
|
3562
4049
|
const projectDir = process.cwd();
|
|
3563
4050
|
let name = changeName;
|
|
4051
|
+
let collectedAlready = false;
|
|
3564
4052
|
if (!name) {
|
|
3565
4053
|
const resolved = resolveHandoffChange(projectDir, changeName);
|
|
3566
4054
|
if (!resolved) {
|
|
@@ -3569,11 +4057,37 @@ program
|
|
|
3569
4057
|
return;
|
|
3570
4058
|
}
|
|
3571
4059
|
if (resolved.ambiguous) {
|
|
3572
|
-
|
|
4060
|
+
if (opts.collect) {
|
|
4061
|
+
let added = 0;
|
|
4062
|
+
for (const change of resolved.ambiguous) {
|
|
4063
|
+
const result = metricsBackfillLastSession(projectDir, change);
|
|
4064
|
+
added += result.added || 0;
|
|
4065
|
+
}
|
|
4066
|
+
collectedAlready = true;
|
|
4067
|
+
if (!opts.json) log.ok(`collect: ${added} new source(s) across ${resolved.ambiguous.length} changes`);
|
|
4068
|
+
if (opts.json) {
|
|
4069
|
+
process.stdout.write(`${JSON.stringify({ collected: added, changes: resolved.ambiguous }, null, 2)}\n`);
|
|
4070
|
+
return;
|
|
4071
|
+
}
|
|
4072
|
+
name = resolved.ambiguous[0];
|
|
4073
|
+
} else {
|
|
4074
|
+
log.err(`Multiple active changes: ${resolved.ambiguous.join(', ')}. Pass the change name argument.`);
|
|
4075
|
+
process.exitCode = 1;
|
|
4076
|
+
return;
|
|
4077
|
+
}
|
|
4078
|
+
} else {
|
|
4079
|
+
name = resolved;
|
|
4080
|
+
}
|
|
4081
|
+
}
|
|
4082
|
+
|
|
4083
|
+
if (opts.collect && name && !collectedAlready) {
|
|
4084
|
+
const result = metricsBackfillLastSession(projectDir, name);
|
|
4085
|
+
if (result.missing) {
|
|
4086
|
+
log.err(`No metrics.json for ${name}`);
|
|
3573
4087
|
process.exitCode = 1;
|
|
3574
4088
|
return;
|
|
3575
4089
|
}
|
|
3576
|
-
|
|
4090
|
+
if (!opts.json) log.ok(`collect: ${result.added} new source(s) on last session`);
|
|
3577
4091
|
}
|
|
3578
4092
|
|
|
3579
4093
|
const { filePath, archived, missing } = resolveMetricsFile(projectDir, name);
|
|
@@ -3593,77 +4107,7 @@ program
|
|
|
3593
4107
|
|
|
3594
4108
|
log.title(`metrics ${name}${archived ? ' (archived)' : ''}`);
|
|
3595
4109
|
console.log(`file: ${filePath.replace(`${projectDir}/`, '')}`);
|
|
3596
|
-
|
|
3597
|
-
console.log(`work time: ${formatMetricsDuration(metrics.totals.durationMs)}`);
|
|
3598
|
-
console.log(`lead time: ${formatMetricsDuration(metrics.totals.leadTimeMs)}`);
|
|
3599
|
-
console.log(`tokens: ${formatMetricsNumber(metrics.spend.totalTokens)} (in: ${formatMetricsNumber(metrics.spend.inputTokens)}, out: ${formatMetricsNumber(metrics.spend.outputTokens)})`);
|
|
3600
|
-
console.log(`cost: ${formatMetricsCost(metrics.spend.costUsd)}`);
|
|
3601
|
-
if (metrics.archivedAt) console.log(`archived: ${metrics.archivedAt}`);
|
|
3602
|
-
if (metrics.pending) log.warn(`open session since ${metrics.pending.startedAt} (${metrics.pending.role || 'unknown role'})`);
|
|
3603
|
-
|
|
3604
|
-
const phaseOrder = ['explore', 'design', 'spec', 'review', 'apply', 'archive', 'other'];
|
|
3605
|
-
const phaseKeys = phaseOrder.filter((key) => metrics.phases[key]);
|
|
3606
|
-
if (phaseKeys.length) {
|
|
3607
|
-
console.log('');
|
|
3608
|
-
console.log('phase sessions time tokens cost roles models');
|
|
3609
|
-
for (const key of phaseKeys) {
|
|
3610
|
-
const phase = metrics.phases[key];
|
|
3611
|
-
const cols = [
|
|
3612
|
-
key.padEnd(10),
|
|
3613
|
-
String(phase.sessions).padEnd(9),
|
|
3614
|
-
formatMetricsDuration(phase.durationMs).padEnd(9),
|
|
3615
|
-
formatMetricsNumber(phase.totalTokens).padEnd(9),
|
|
3616
|
-
formatMetricsCost(phase.costUsd).padEnd(9),
|
|
3617
|
-
(phase.agents.join(', ') || '—').padEnd(20),
|
|
3618
|
-
phase.models.join(', ') || '—',
|
|
3619
|
-
];
|
|
3620
|
-
console.log(cols.join(' '));
|
|
3621
|
-
}
|
|
3622
|
-
}
|
|
3623
|
-
|
|
3624
|
-
const byPlatform = metrics.spendByPlatform || defaultSpendByPlatform();
|
|
3625
|
-
console.log('');
|
|
3626
|
-
console.log('by platform:');
|
|
3627
|
-
console.log('platform tokens cost credits source');
|
|
3628
|
-
for (const key of ['cursor', 'claude', 'amp']) {
|
|
3629
|
-
const row = byPlatform[key] || emptyPlatformSpend();
|
|
3630
|
-
console.log([
|
|
3631
|
-
key.padEnd(10),
|
|
3632
|
-
formatMetricsNumber(row.totalTokens).padEnd(9),
|
|
3633
|
-
formatMetricsCost(row.costUsd).padEnd(9),
|
|
3634
|
-
formatMetricsNumber(row.ampCredits).padEnd(9),
|
|
3635
|
-
row.source || 'none',
|
|
3636
|
-
].join(' '));
|
|
3637
|
-
}
|
|
3638
|
-
|
|
3639
|
-
console.log('');
|
|
3640
|
-
console.log('by model:');
|
|
3641
|
-
console.log('model platform tokens cost credits');
|
|
3642
|
-
const byModel = Array.isArray(metrics.spendByModel) ? metrics.spendByModel : [];
|
|
3643
|
-
if (!byModel.length) {
|
|
3644
|
-
console.log('— — — — —');
|
|
3645
|
-
} else {
|
|
3646
|
-
for (const row of byModel) {
|
|
3647
|
-
console.log([
|
|
3648
|
-
String(row.model || '—').padEnd(20),
|
|
3649
|
-
String(row.platform || '—').padEnd(10),
|
|
3650
|
-
formatMetricsNumber(row.totalTokens).padEnd(9),
|
|
3651
|
-
formatMetricsCost(row.costUsd).padEnd(9),
|
|
3652
|
-
formatMetricsNumber(row.ampCredits),
|
|
3653
|
-
].join(' '));
|
|
3654
|
-
}
|
|
3655
|
-
}
|
|
3656
|
-
|
|
3657
|
-
if (metrics.sessions.length) {
|
|
3658
|
-
console.log('');
|
|
3659
|
-
console.log('recent sessions:');
|
|
3660
|
-
for (const session of metrics.sessions.slice(-5)) {
|
|
3661
|
-
const spendLabel = session.totalTokens != null || session.costUsd != null
|
|
3662
|
-
? ` — ${formatMetricsNumber(session.totalTokens)} tok, ${formatMetricsCost(session.costUsd)}`
|
|
3663
|
-
: '';
|
|
3664
|
-
console.log(`- ${session.endedAt} ${session.phase.padEnd(7)} ${formatMetricsDuration(session.durationMs).padEnd(9)} ${session.role || '(no role)'}${session.model ? ` [${session.model}]` : ''}${spendLabel}`);
|
|
3665
|
-
}
|
|
3666
|
-
}
|
|
4110
|
+
for (const line of renderMetricsSummary(metrics)) console.log(line);
|
|
3667
4111
|
});
|
|
3668
4112
|
|
|
3669
4113
|
program.parse();
|