@os-eco/overstory-cli 0.6.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +381 -0
- package/agents/builder.md +137 -0
- package/agents/coordinator.md +263 -0
- package/agents/lead.md +301 -0
- package/agents/merger.md +160 -0
- package/agents/monitor.md +214 -0
- package/agents/reviewer.md +140 -0
- package/agents/scout.md +119 -0
- package/agents/supervisor.md +423 -0
- package/package.json +47 -0
- package/src/agents/checkpoint.test.ts +88 -0
- package/src/agents/checkpoint.ts +101 -0
- package/src/agents/hooks-deployer.test.ts +2040 -0
- package/src/agents/hooks-deployer.ts +607 -0
- package/src/agents/identity.test.ts +603 -0
- package/src/agents/identity.ts +384 -0
- package/src/agents/lifecycle.test.ts +196 -0
- package/src/agents/lifecycle.ts +183 -0
- package/src/agents/manifest.test.ts +746 -0
- package/src/agents/manifest.ts +354 -0
- package/src/agents/overlay.test.ts +676 -0
- package/src/agents/overlay.ts +308 -0
- package/src/beads/client.test.ts +217 -0
- package/src/beads/client.ts +202 -0
- package/src/beads/molecules.test.ts +338 -0
- package/src/beads/molecules.ts +198 -0
- package/src/commands/agents.test.ts +322 -0
- package/src/commands/agents.ts +287 -0
- package/src/commands/clean.test.ts +670 -0
- package/src/commands/clean.ts +618 -0
- package/src/commands/completions.test.ts +342 -0
- package/src/commands/completions.ts +887 -0
- package/src/commands/coordinator.test.ts +1530 -0
- package/src/commands/coordinator.ts +733 -0
- package/src/commands/costs.test.ts +1119 -0
- package/src/commands/costs.ts +564 -0
- package/src/commands/dashboard.test.ts +308 -0
- package/src/commands/dashboard.ts +838 -0
- package/src/commands/doctor.test.ts +294 -0
- package/src/commands/doctor.ts +213 -0
- package/src/commands/errors.test.ts +647 -0
- package/src/commands/errors.ts +248 -0
- package/src/commands/feed.test.ts +578 -0
- package/src/commands/feed.ts +361 -0
- package/src/commands/group.test.ts +262 -0
- package/src/commands/group.ts +511 -0
- package/src/commands/hooks.test.ts +458 -0
- package/src/commands/hooks.ts +253 -0
- package/src/commands/init.test.ts +347 -0
- package/src/commands/init.ts +650 -0
- package/src/commands/inspect.test.ts +670 -0
- package/src/commands/inspect.ts +431 -0
- package/src/commands/log.test.ts +1454 -0
- package/src/commands/log.ts +724 -0
- package/src/commands/logs.test.ts +379 -0
- package/src/commands/logs.ts +546 -0
- package/src/commands/mail.test.ts +1270 -0
- package/src/commands/mail.ts +771 -0
- package/src/commands/merge.test.ts +670 -0
- package/src/commands/merge.ts +355 -0
- package/src/commands/metrics.test.ts +444 -0
- package/src/commands/metrics.ts +143 -0
- package/src/commands/monitor.test.ts +191 -0
- package/src/commands/monitor.ts +390 -0
- package/src/commands/nudge.test.ts +230 -0
- package/src/commands/nudge.ts +372 -0
- package/src/commands/prime.test.ts +470 -0
- package/src/commands/prime.ts +381 -0
- package/src/commands/replay.test.ts +741 -0
- package/src/commands/replay.ts +360 -0
- package/src/commands/run.test.ts +431 -0
- package/src/commands/run.ts +351 -0
- package/src/commands/sling.test.ts +657 -0
- package/src/commands/sling.ts +661 -0
- package/src/commands/spec.test.ts +203 -0
- package/src/commands/spec.ts +168 -0
- package/src/commands/status.test.ts +430 -0
- package/src/commands/status.ts +398 -0
- package/src/commands/stop.test.ts +420 -0
- package/src/commands/stop.ts +151 -0
- package/src/commands/supervisor.test.ts +187 -0
- package/src/commands/supervisor.ts +535 -0
- package/src/commands/trace.test.ts +745 -0
- package/src/commands/trace.ts +325 -0
- package/src/commands/watch.test.ts +145 -0
- package/src/commands/watch.ts +247 -0
- package/src/commands/worktree.test.ts +786 -0
- package/src/commands/worktree.ts +311 -0
- package/src/config.test.ts +822 -0
- package/src/config.ts +829 -0
- package/src/doctor/agents.test.ts +454 -0
- package/src/doctor/agents.ts +396 -0
- package/src/doctor/config-check.test.ts +190 -0
- package/src/doctor/config-check.ts +183 -0
- package/src/doctor/consistency.test.ts +651 -0
- package/src/doctor/consistency.ts +294 -0
- package/src/doctor/databases.test.ts +290 -0
- package/src/doctor/databases.ts +218 -0
- package/src/doctor/dependencies.test.ts +184 -0
- package/src/doctor/dependencies.ts +175 -0
- package/src/doctor/logs.test.ts +251 -0
- package/src/doctor/logs.ts +295 -0
- package/src/doctor/merge-queue.test.ts +216 -0
- package/src/doctor/merge-queue.ts +144 -0
- package/src/doctor/structure.test.ts +291 -0
- package/src/doctor/structure.ts +198 -0
- package/src/doctor/types.ts +37 -0
- package/src/doctor/version.test.ts +136 -0
- package/src/doctor/version.ts +129 -0
- package/src/e2e/init-sling-lifecycle.test.ts +277 -0
- package/src/errors.ts +217 -0
- package/src/events/store.test.ts +660 -0
- package/src/events/store.ts +369 -0
- package/src/events/tool-filter.test.ts +330 -0
- package/src/events/tool-filter.ts +126 -0
- package/src/index.ts +316 -0
- package/src/insights/analyzer.test.ts +466 -0
- package/src/insights/analyzer.ts +203 -0
- package/src/logging/color.test.ts +142 -0
- package/src/logging/color.ts +71 -0
- package/src/logging/logger.test.ts +813 -0
- package/src/logging/logger.ts +266 -0
- package/src/logging/reporter.test.ts +259 -0
- package/src/logging/reporter.ts +109 -0
- package/src/logging/sanitizer.test.ts +190 -0
- package/src/logging/sanitizer.ts +57 -0
- package/src/mail/broadcast.test.ts +203 -0
- package/src/mail/broadcast.ts +92 -0
- package/src/mail/client.test.ts +773 -0
- package/src/mail/client.ts +223 -0
- package/src/mail/store.test.ts +705 -0
- package/src/mail/store.ts +387 -0
- package/src/merge/queue.test.ts +359 -0
- package/src/merge/queue.ts +231 -0
- package/src/merge/resolver.test.ts +1345 -0
- package/src/merge/resolver.ts +645 -0
- package/src/metrics/store.test.ts +667 -0
- package/src/metrics/store.ts +445 -0
- package/src/metrics/summary.test.ts +398 -0
- package/src/metrics/summary.ts +178 -0
- package/src/metrics/transcript.test.ts +356 -0
- package/src/metrics/transcript.ts +175 -0
- package/src/mulch/client.test.ts +671 -0
- package/src/mulch/client.ts +332 -0
- package/src/sessions/compat.test.ts +280 -0
- package/src/sessions/compat.ts +104 -0
- package/src/sessions/store.test.ts +873 -0
- package/src/sessions/store.ts +494 -0
- package/src/test-helpers.test.ts +124 -0
- package/src/test-helpers.ts +126 -0
- package/src/tracker/beads.ts +56 -0
- package/src/tracker/factory.test.ts +80 -0
- package/src/tracker/factory.ts +64 -0
- package/src/tracker/seeds.ts +182 -0
- package/src/tracker/types.ts +52 -0
- package/src/types.ts +724 -0
- package/src/watchdog/daemon.test.ts +1975 -0
- package/src/watchdog/daemon.ts +671 -0
- package/src/watchdog/health.test.ts +431 -0
- package/src/watchdog/health.ts +264 -0
- package/src/watchdog/triage.test.ts +164 -0
- package/src/watchdog/triage.ts +179 -0
- package/src/worktree/manager.test.ts +439 -0
- package/src/worktree/manager.ts +198 -0
- package/src/worktree/tmux.test.ts +1009 -0
- package/src/worktree/tmux.ts +509 -0
- package/templates/CLAUDE.md.tmpl +89 -0
- package/templates/hooks.json.tmpl +105 -0
- package/templates/overlay.md.tmpl +81 -0
|
@@ -0,0 +1,564 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CLI command: overstory costs [--agent <name>] [--run <id>] [--by-capability] [--last <n>] [--self] [--json]
|
|
3
|
+
*
|
|
4
|
+
* Shows token/cost analysis and breakdown for agent sessions.
|
|
5
|
+
* Data source: metrics.db via createMetricsStore().
|
|
6
|
+
* Use --self to parse the current orchestrator session's transcript directly.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { readdir, stat } from "node:fs/promises";
|
|
10
|
+
import { join } from "node:path";
|
|
11
|
+
import { loadConfig } from "../config.ts";
|
|
12
|
+
import { ValidationError } from "../errors.ts";
|
|
13
|
+
import { color } from "../logging/color.ts";
|
|
14
|
+
import { createMetricsStore } from "../metrics/store.ts";
|
|
15
|
+
import { estimateCost, parseTranscriptUsage } from "../metrics/transcript.ts";
|
|
16
|
+
import { openSessionStore } from "../sessions/compat.ts";
|
|
17
|
+
import type { SessionMetrics } from "../types.ts";
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Parse a named flag value from args.
|
|
21
|
+
*/
|
|
22
|
+
function getFlag(args: string[], flag: string): string | undefined {
|
|
23
|
+
const idx = args.indexOf(flag);
|
|
24
|
+
if (idx === -1 || idx + 1 >= args.length) {
|
|
25
|
+
return undefined;
|
|
26
|
+
}
|
|
27
|
+
return args[idx + 1];
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function hasFlag(args: string[], flag: string): boolean {
|
|
31
|
+
return args.includes(flag);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/** Format a number with thousands separators (e.g., 12345 -> "12,345"). */
|
|
35
|
+
function formatNumber(n: number): string {
|
|
36
|
+
return n.toLocaleString("en-US");
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** Format a cost value as "$X.XX". Returns "$0.00" for null/undefined. */
|
|
40
|
+
function formatCost(cost: number | null): string {
|
|
41
|
+
if (cost === null || cost === undefined) {
|
|
42
|
+
return "$0.00";
|
|
43
|
+
}
|
|
44
|
+
return `$${cost.toFixed(2)}`;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** Right-pad a string to the given width. */
|
|
48
|
+
function padRight(str: string, width: number): string {
|
|
49
|
+
return str.length >= width ? str : str + " ".repeat(width - str.length);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/** Left-pad a string to the given width. */
|
|
53
|
+
function padLeft(str: string, width: number): string {
|
|
54
|
+
return str.length >= width ? str : " ".repeat(width - str.length) + str;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Discover the orchestrator's Claude Code transcript JSONL file.
|
|
59
|
+
*
|
|
60
|
+
* Scans ~/.claude/projects/{project-key}/ for JSONL files and returns
|
|
61
|
+
* the most recently modified one, corresponding to the current orchestrator session.
|
|
62
|
+
*
|
|
63
|
+
* @param projectRoot - Absolute path to the project root
|
|
64
|
+
* @returns Absolute path to the most recent transcript, or null if none found
|
|
65
|
+
*/
|
|
66
|
+
async function discoverOrchestratorTranscript(projectRoot: string): Promise<string | null> {
|
|
67
|
+
const homeDir = process.env.HOME ?? "";
|
|
68
|
+
if (homeDir.length === 0) return null;
|
|
69
|
+
|
|
70
|
+
const projectKey = projectRoot.replace(/\//g, "-");
|
|
71
|
+
const projectDir = join(homeDir, ".claude", "projects", projectKey);
|
|
72
|
+
|
|
73
|
+
let entries: string[];
|
|
74
|
+
try {
|
|
75
|
+
entries = await readdir(projectDir);
|
|
76
|
+
} catch {
|
|
77
|
+
return null;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const jsonlFiles = entries.filter((e) => e.endsWith(".jsonl"));
|
|
81
|
+
if (jsonlFiles.length === 0) return null;
|
|
82
|
+
|
|
83
|
+
let bestPath: string | null = null;
|
|
84
|
+
let bestMtime = 0;
|
|
85
|
+
|
|
86
|
+
for (const file of jsonlFiles) {
|
|
87
|
+
const filePath = join(projectDir, file);
|
|
88
|
+
try {
|
|
89
|
+
const fileStat = await stat(filePath);
|
|
90
|
+
if (fileStat.mtimeMs > bestMtime) {
|
|
91
|
+
bestMtime = fileStat.mtimeMs;
|
|
92
|
+
bestPath = filePath;
|
|
93
|
+
}
|
|
94
|
+
} catch {
|
|
95
|
+
// Skip files we cannot stat
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
return bestPath;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/** Aggregate totals from a list of SessionMetrics. */
|
|
103
|
+
interface Totals {
|
|
104
|
+
inputTokens: number;
|
|
105
|
+
outputTokens: number;
|
|
106
|
+
cacheTokens: number;
|
|
107
|
+
costUsd: number;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function computeTotals(sessions: SessionMetrics[]): Totals {
|
|
111
|
+
let inputTokens = 0;
|
|
112
|
+
let outputTokens = 0;
|
|
113
|
+
let cacheTokens = 0;
|
|
114
|
+
let costUsd = 0;
|
|
115
|
+
for (const s of sessions) {
|
|
116
|
+
inputTokens += s.inputTokens;
|
|
117
|
+
outputTokens += s.outputTokens;
|
|
118
|
+
cacheTokens += s.cacheReadTokens + s.cacheCreationTokens;
|
|
119
|
+
costUsd += s.estimatedCostUsd ?? 0;
|
|
120
|
+
}
|
|
121
|
+
return { inputTokens, outputTokens, cacheTokens, costUsd };
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/** Group SessionMetrics by capability. */
|
|
125
|
+
interface CapabilityGroup {
|
|
126
|
+
capability: string;
|
|
127
|
+
sessions: SessionMetrics[];
|
|
128
|
+
totals: Totals;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function groupByCapability(sessions: SessionMetrics[]): CapabilityGroup[] {
|
|
132
|
+
const groups = new Map<string, SessionMetrics[]>();
|
|
133
|
+
for (const s of sessions) {
|
|
134
|
+
const existing = groups.get(s.capability);
|
|
135
|
+
if (existing) {
|
|
136
|
+
existing.push(s);
|
|
137
|
+
} else {
|
|
138
|
+
groups.set(s.capability, [s]);
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
const result: CapabilityGroup[] = [];
|
|
142
|
+
for (const [capability, capSessions] of groups) {
|
|
143
|
+
result.push({
|
|
144
|
+
capability,
|
|
145
|
+
sessions: capSessions,
|
|
146
|
+
totals: computeTotals(capSessions),
|
|
147
|
+
});
|
|
148
|
+
}
|
|
149
|
+
// Sort by cost descending
|
|
150
|
+
result.sort((a, b) => b.totals.costUsd - a.totals.costUsd);
|
|
151
|
+
return result;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/** Print the standard per-agent cost summary table. */
|
|
155
|
+
function printCostSummary(sessions: SessionMetrics[]): void {
|
|
156
|
+
const w = process.stdout.write.bind(process.stdout);
|
|
157
|
+
const separator = "\u2500".repeat(70);
|
|
158
|
+
|
|
159
|
+
w(`${color.bold}Cost Summary${color.reset}\n`);
|
|
160
|
+
w(`${"=".repeat(70)}\n`);
|
|
161
|
+
|
|
162
|
+
if (sessions.length === 0) {
|
|
163
|
+
w(`${color.dim}No session data found.${color.reset}\n`);
|
|
164
|
+
return;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
w(
|
|
168
|
+
`${padRight("Agent", 19)}${padRight("Capability", 12)}` +
|
|
169
|
+
`${padLeft("Input", 10)}${padLeft("Output", 10)}` +
|
|
170
|
+
`${padLeft("Cache", 10)}${padLeft("Cost", 10)}\n`,
|
|
171
|
+
);
|
|
172
|
+
w(`${color.dim}${separator}${color.reset}\n`);
|
|
173
|
+
|
|
174
|
+
for (const s of sessions) {
|
|
175
|
+
const cacheTotal = s.cacheReadTokens + s.cacheCreationTokens;
|
|
176
|
+
w(
|
|
177
|
+
`${padRight(s.agentName, 19)}${padRight(s.capability, 12)}` +
|
|
178
|
+
`${padLeft(formatNumber(s.inputTokens), 10)}` +
|
|
179
|
+
`${padLeft(formatNumber(s.outputTokens), 10)}` +
|
|
180
|
+
`${padLeft(formatNumber(cacheTotal), 10)}` +
|
|
181
|
+
`${padLeft(formatCost(s.estimatedCostUsd), 10)}\n`,
|
|
182
|
+
);
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
const totals = computeTotals(sessions);
|
|
186
|
+
w(`${color.dim}${separator}${color.reset}\n`);
|
|
187
|
+
w(
|
|
188
|
+
`${color.green}${color.bold}${padRight("Total", 31)}` +
|
|
189
|
+
`${padLeft(formatNumber(totals.inputTokens), 10)}` +
|
|
190
|
+
`${padLeft(formatNumber(totals.outputTokens), 10)}` +
|
|
191
|
+
`${padLeft(formatNumber(totals.cacheTokens), 10)}` +
|
|
192
|
+
`${padLeft(formatCost(totals.costUsd), 10)}${color.reset}\n`,
|
|
193
|
+
);
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/** Print the capability-grouped cost table. */
|
|
197
|
+
function printByCapability(sessions: SessionMetrics[]): void {
|
|
198
|
+
const w = process.stdout.write.bind(process.stdout);
|
|
199
|
+
const separator = "\u2500".repeat(70);
|
|
200
|
+
|
|
201
|
+
w(`${color.bold}Cost by Capability${color.reset}\n`);
|
|
202
|
+
w(`${"=".repeat(70)}\n`);
|
|
203
|
+
|
|
204
|
+
if (sessions.length === 0) {
|
|
205
|
+
w(`${color.dim}No session data found.${color.reset}\n`);
|
|
206
|
+
return;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
w(
|
|
210
|
+
`${padRight("Capability", 14)}${padLeft("Sessions", 10)}` +
|
|
211
|
+
`${padLeft("Input", 10)}${padLeft("Output", 10)}` +
|
|
212
|
+
`${padLeft("Cache", 10)}${padLeft("Cost", 10)}\n`,
|
|
213
|
+
);
|
|
214
|
+
w(`${color.dim}${separator}${color.reset}\n`);
|
|
215
|
+
|
|
216
|
+
const groups = groupByCapability(sessions);
|
|
217
|
+
|
|
218
|
+
for (const group of groups) {
|
|
219
|
+
w(
|
|
220
|
+
`${padRight(group.capability, 14)}` +
|
|
221
|
+
`${padLeft(formatNumber(group.sessions.length), 10)}` +
|
|
222
|
+
`${padLeft(formatNumber(group.totals.inputTokens), 10)}` +
|
|
223
|
+
`${padLeft(formatNumber(group.totals.outputTokens), 10)}` +
|
|
224
|
+
`${padLeft(formatNumber(group.totals.cacheTokens), 10)}` +
|
|
225
|
+
`${padLeft(formatCost(group.totals.costUsd), 10)}\n`,
|
|
226
|
+
);
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
const totals = computeTotals(sessions);
|
|
230
|
+
w(`${color.dim}${separator}${color.reset}\n`);
|
|
231
|
+
w(
|
|
232
|
+
`${color.green}${color.bold}${padRight("Total", 14)}` +
|
|
233
|
+
`${padLeft(formatNumber(sessions.length), 10)}` +
|
|
234
|
+
`${padLeft(formatNumber(totals.inputTokens), 10)}` +
|
|
235
|
+
`${padLeft(formatNumber(totals.outputTokens), 10)}` +
|
|
236
|
+
`${padLeft(formatNumber(totals.cacheTokens), 10)}` +
|
|
237
|
+
`${padLeft(formatCost(totals.costUsd), 10)}${color.reset}\n`,
|
|
238
|
+
);
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
const COSTS_HELP = `overstory costs -- Token/cost analysis and breakdown
|
|
242
|
+
|
|
243
|
+
Usage: overstory costs [options]
|
|
244
|
+
|
|
245
|
+
Options:
|
|
246
|
+
--live Show real-time token usage for active agents
|
|
247
|
+
--self Show cost for the current orchestrator session
|
|
248
|
+
--agent <name> Filter by agent name
|
|
249
|
+
--run <id> Filter by run ID
|
|
250
|
+
--by-capability Group results by capability with subtotals
|
|
251
|
+
--last <n> Number of recent sessions (default: 20)
|
|
252
|
+
--json Output as JSON
|
|
253
|
+
--help, -h Show this help`;
|
|
254
|
+
|
|
255
|
+
/**
|
|
256
|
+
* Entry point for `overstory costs [--agent <name>] [--run <id>] [--by-capability] [--last <n>] [--self] [--json]`.
|
|
257
|
+
*/
|
|
258
|
+
export async function costsCommand(args: string[]): Promise<void> {
|
|
259
|
+
if (args.includes("--help") || args.includes("-h")) {
|
|
260
|
+
process.stdout.write(`${COSTS_HELP}\n`);
|
|
261
|
+
return;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
const json = hasFlag(args, "--json");
|
|
265
|
+
const live = hasFlag(args, "--live");
|
|
266
|
+
const self = hasFlag(args, "--self");
|
|
267
|
+
const byCapability = hasFlag(args, "--by-capability");
|
|
268
|
+
const agentName = getFlag(args, "--agent");
|
|
269
|
+
const runId = getFlag(args, "--run");
|
|
270
|
+
const lastStr = getFlag(args, "--last");
|
|
271
|
+
|
|
272
|
+
if (lastStr !== undefined) {
|
|
273
|
+
const parsed = Number.parseInt(lastStr, 10);
|
|
274
|
+
if (Number.isNaN(parsed) || parsed < 1) {
|
|
275
|
+
throw new ValidationError("--last must be a positive integer", {
|
|
276
|
+
field: "last",
|
|
277
|
+
value: lastStr,
|
|
278
|
+
});
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
const last = lastStr ? Number.parseInt(lastStr, 10) : 20;
|
|
283
|
+
|
|
284
|
+
const cwd = process.cwd();
|
|
285
|
+
const config = await loadConfig(cwd);
|
|
286
|
+
const overstoryDir = join(config.project.root, ".overstory");
|
|
287
|
+
|
|
288
|
+
// Handle --self flag (early return for self-scan)
|
|
289
|
+
if (self) {
|
|
290
|
+
const transcriptPath = await discoverOrchestratorTranscript(config.project.root);
|
|
291
|
+
if (!transcriptPath) {
|
|
292
|
+
if (json) {
|
|
293
|
+
process.stdout.write(
|
|
294
|
+
JSON.stringify({ error: "no_transcript", message: "No orchestrator transcript found" }) +
|
|
295
|
+
"\n",
|
|
296
|
+
);
|
|
297
|
+
} else {
|
|
298
|
+
process.stdout.write(
|
|
299
|
+
"No orchestrator transcript found.\nExpected at: ~/.claude/projects/{project-key}/*.jsonl\n",
|
|
300
|
+
);
|
|
301
|
+
}
|
|
302
|
+
return;
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
const usage = await parseTranscriptUsage(transcriptPath);
|
|
306
|
+
const cost = estimateCost(usage);
|
|
307
|
+
const cacheTotal = usage.cacheReadTokens + usage.cacheCreationTokens;
|
|
308
|
+
|
|
309
|
+
if (json) {
|
|
310
|
+
process.stdout.write(
|
|
311
|
+
`${JSON.stringify({
|
|
312
|
+
source: "self",
|
|
313
|
+
transcriptPath,
|
|
314
|
+
model: usage.modelUsed,
|
|
315
|
+
inputTokens: usage.inputTokens,
|
|
316
|
+
outputTokens: usage.outputTokens,
|
|
317
|
+
cacheReadTokens: usage.cacheReadTokens,
|
|
318
|
+
cacheCreationTokens: usage.cacheCreationTokens,
|
|
319
|
+
estimatedCostUsd: cost,
|
|
320
|
+
})}\n`,
|
|
321
|
+
);
|
|
322
|
+
} else {
|
|
323
|
+
const w = process.stdout.write.bind(process.stdout);
|
|
324
|
+
const separator = "\u2500".repeat(70);
|
|
325
|
+
|
|
326
|
+
w(`${color.bold}Orchestrator Session Cost${color.reset}\n`);
|
|
327
|
+
w(`${"=".repeat(70)}\n`);
|
|
328
|
+
w(`${padRight("Model:", 12)}${usage.modelUsed ?? "unknown"}\n`);
|
|
329
|
+
w(`${padRight("Transcript:", 12)}${transcriptPath}\n`);
|
|
330
|
+
w(`${color.dim}${separator}${color.reset}\n`);
|
|
331
|
+
w(`${padRight("Input tokens:", 22)}${padLeft(formatNumber(usage.inputTokens), 12)}\n`);
|
|
332
|
+
w(`${padRight("Output tokens:", 22)}${padLeft(formatNumber(usage.outputTokens), 12)}\n`);
|
|
333
|
+
w(`${padRight("Cache tokens:", 22)}${padLeft(formatNumber(cacheTotal), 12)}\n`);
|
|
334
|
+
w(`${color.dim}${separator}${color.reset}\n`);
|
|
335
|
+
w(
|
|
336
|
+
`${color.green}${color.bold}${padRight("Estimated cost:", 22)}${padLeft(formatCost(cost), 12)}${color.reset}\n`,
|
|
337
|
+
);
|
|
338
|
+
}
|
|
339
|
+
return;
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
// Handle --live flag (early return for live view)
|
|
343
|
+
if (live) {
|
|
344
|
+
const metricsDbPath = join(overstoryDir, "metrics.db");
|
|
345
|
+
const metricsFile = Bun.file(metricsDbPath);
|
|
346
|
+
if (!(await metricsFile.exists())) {
|
|
347
|
+
if (json) {
|
|
348
|
+
process.stdout.write(
|
|
349
|
+
`${JSON.stringify({ agents: [], totals: { inputTokens: 0, outputTokens: 0, cacheTokens: 0, costUsd: 0, burnRatePerMin: 0, tokensPerMin: 0 } })}\n`,
|
|
350
|
+
);
|
|
351
|
+
} else {
|
|
352
|
+
process.stdout.write(
|
|
353
|
+
"No live data available. Token snapshots begin after first tool call.\n",
|
|
354
|
+
);
|
|
355
|
+
}
|
|
356
|
+
return;
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
const metricsStore = createMetricsStore(metricsDbPath);
|
|
360
|
+
const { store: sessionStore } = openSessionStore(overstoryDir);
|
|
361
|
+
|
|
362
|
+
try {
|
|
363
|
+
const snapshots = metricsStore.getLatestSnapshots();
|
|
364
|
+
if (snapshots.length === 0) {
|
|
365
|
+
if (json) {
|
|
366
|
+
process.stdout.write(
|
|
367
|
+
`${JSON.stringify({ agents: [], totals: { inputTokens: 0, outputTokens: 0, cacheTokens: 0, costUsd: 0, burnRatePerMin: 0, tokensPerMin: 0 } })}\n`,
|
|
368
|
+
);
|
|
369
|
+
} else {
|
|
370
|
+
process.stdout.write(
|
|
371
|
+
"No live data available. Token snapshots begin after first tool call.\n",
|
|
372
|
+
);
|
|
373
|
+
}
|
|
374
|
+
return;
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
// Get active sessions to join with snapshots
|
|
378
|
+
const activeSessions = sessionStore.getActive();
|
|
379
|
+
|
|
380
|
+
// Filter snapshots by agent if --agent is provided
|
|
381
|
+
const filteredSnapshots = agentName
|
|
382
|
+
? snapshots.filter((s) => s.agentName === agentName)
|
|
383
|
+
: snapshots;
|
|
384
|
+
|
|
385
|
+
// Build agent data with session info
|
|
386
|
+
interface LiveAgentData {
|
|
387
|
+
agentName: string;
|
|
388
|
+
capability: string;
|
|
389
|
+
inputTokens: number;
|
|
390
|
+
outputTokens: number;
|
|
391
|
+
cacheReadTokens: number;
|
|
392
|
+
cacheCreationTokens: number;
|
|
393
|
+
estimatedCostUsd: number;
|
|
394
|
+
modelUsed: string | null;
|
|
395
|
+
snapshotAt: string;
|
|
396
|
+
sessionStartedAt: string;
|
|
397
|
+
elapsedMs: number;
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
const agentData: LiveAgentData[] = [];
|
|
401
|
+
const now = Date.now();
|
|
402
|
+
|
|
403
|
+
for (const snapshot of filteredSnapshots) {
|
|
404
|
+
const session = activeSessions.find((s) => s.agentName === snapshot.agentName);
|
|
405
|
+
if (!session) continue; // Skip inactive agents
|
|
406
|
+
|
|
407
|
+
const startedAt = new Date(session.startedAt).getTime();
|
|
408
|
+
const elapsedMs = now - startedAt;
|
|
409
|
+
|
|
410
|
+
agentData.push({
|
|
411
|
+
agentName: snapshot.agentName,
|
|
412
|
+
capability: session.capability,
|
|
413
|
+
inputTokens: snapshot.inputTokens,
|
|
414
|
+
outputTokens: snapshot.outputTokens,
|
|
415
|
+
cacheReadTokens: snapshot.cacheReadTokens,
|
|
416
|
+
cacheCreationTokens: snapshot.cacheCreationTokens,
|
|
417
|
+
estimatedCostUsd: snapshot.estimatedCostUsd ?? 0,
|
|
418
|
+
modelUsed: snapshot.modelUsed,
|
|
419
|
+
snapshotAt: snapshot.createdAt,
|
|
420
|
+
sessionStartedAt: session.startedAt,
|
|
421
|
+
elapsedMs,
|
|
422
|
+
});
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
// Compute totals
|
|
426
|
+
let totalInput = 0;
|
|
427
|
+
let totalOutput = 0;
|
|
428
|
+
let totalCacheRead = 0;
|
|
429
|
+
let totalCacheCreate = 0;
|
|
430
|
+
let totalCost = 0;
|
|
431
|
+
let totalElapsedMs = 0;
|
|
432
|
+
|
|
433
|
+
for (const agent of agentData) {
|
|
434
|
+
totalInput += agent.inputTokens;
|
|
435
|
+
totalOutput += agent.outputTokens;
|
|
436
|
+
totalCacheRead += agent.cacheReadTokens;
|
|
437
|
+
totalCacheCreate += agent.cacheCreationTokens;
|
|
438
|
+
totalCost += agent.estimatedCostUsd;
|
|
439
|
+
totalElapsedMs += agent.elapsedMs;
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
const avgElapsedMs = agentData.length > 0 ? totalElapsedMs / agentData.length : 0;
|
|
443
|
+
const totalCacheTokens = totalCacheRead + totalCacheCreate;
|
|
444
|
+
const totalTokens = totalInput + totalOutput;
|
|
445
|
+
const burnRatePerMin = avgElapsedMs > 0 ? totalCost / (avgElapsedMs / 60_000) : 0;
|
|
446
|
+
const tokensPerMin = avgElapsedMs > 0 ? totalTokens / (avgElapsedMs / 60_000) : 0;
|
|
447
|
+
|
|
448
|
+
if (json) {
|
|
449
|
+
process.stdout.write(
|
|
450
|
+
`${JSON.stringify({
|
|
451
|
+
agents: agentData,
|
|
452
|
+
totals: {
|
|
453
|
+
inputTokens: totalInput,
|
|
454
|
+
outputTokens: totalOutput,
|
|
455
|
+
cacheTokens: totalCacheTokens,
|
|
456
|
+
costUsd: totalCost,
|
|
457
|
+
burnRatePerMin,
|
|
458
|
+
tokensPerMin,
|
|
459
|
+
},
|
|
460
|
+
})}\n`,
|
|
461
|
+
);
|
|
462
|
+
} else {
|
|
463
|
+
const w = process.stdout.write.bind(process.stdout);
|
|
464
|
+
const separator = "\u2500".repeat(70);
|
|
465
|
+
|
|
466
|
+
w(`${color.bold}Live Token Usage (${agentData.length} active agents)${color.reset}\n`);
|
|
467
|
+
w(`${"=".repeat(70)}\n`);
|
|
468
|
+
w(
|
|
469
|
+
`${padRight("Agent", 19)}${padRight("Capability", 12)}` +
|
|
470
|
+
`${padLeft("Input", 10)}${padLeft("Output", 10)}` +
|
|
471
|
+
`${padLeft("Cache", 10)}${padLeft("Cost", 10)}\n`,
|
|
472
|
+
);
|
|
473
|
+
w(`${color.dim}${separator}${color.reset}\n`);
|
|
474
|
+
|
|
475
|
+
for (const agent of agentData) {
|
|
476
|
+
const cacheTotal = agent.cacheReadTokens + agent.cacheCreationTokens;
|
|
477
|
+
w(
|
|
478
|
+
`${padRight(agent.agentName, 19)}${padRight(agent.capability, 12)}` +
|
|
479
|
+
`${padLeft(formatNumber(agent.inputTokens), 10)}` +
|
|
480
|
+
`${padLeft(formatNumber(agent.outputTokens), 10)}` +
|
|
481
|
+
`${padLeft(formatNumber(cacheTotal), 10)}` +
|
|
482
|
+
`${padLeft(formatCost(agent.estimatedCostUsd), 10)}\n`,
|
|
483
|
+
);
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
w(`${color.dim}${separator}${color.reset}\n`);
|
|
487
|
+
w(
|
|
488
|
+
`${color.green}${color.bold}${padRight("Total", 31)}` +
|
|
489
|
+
`${padLeft(formatNumber(totalInput), 10)}` +
|
|
490
|
+
`${padLeft(formatNumber(totalOutput), 10)}` +
|
|
491
|
+
`${padLeft(formatNumber(totalCacheTokens), 10)}` +
|
|
492
|
+
`${padLeft(formatCost(totalCost), 10)}${color.reset}\n\n`,
|
|
493
|
+
);
|
|
494
|
+
|
|
495
|
+
// Format elapsed time
|
|
496
|
+
const totalElapsedSec = Math.floor(avgElapsedMs / 1000);
|
|
497
|
+
const minutes = Math.floor(totalElapsedSec / 60);
|
|
498
|
+
const seconds = totalElapsedSec % 60;
|
|
499
|
+
const elapsedStr = `${minutes}m ${seconds}s`;
|
|
500
|
+
|
|
501
|
+
w(
|
|
502
|
+
`Burn rate: ${formatCost(burnRatePerMin)}/min | ` +
|
|
503
|
+
`${formatNumber(Math.floor(tokensPerMin))} tokens/min | ` +
|
|
504
|
+
`Elapsed: ${elapsedStr}\n`,
|
|
505
|
+
);
|
|
506
|
+
}
|
|
507
|
+
} finally {
|
|
508
|
+
metricsStore.close();
|
|
509
|
+
sessionStore.close();
|
|
510
|
+
}
|
|
511
|
+
return;
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
// Check if metrics.db exists
|
|
515
|
+
const metricsDbPath = join(overstoryDir, "metrics.db");
|
|
516
|
+
const metricsFile = Bun.file(metricsDbPath);
|
|
517
|
+
if (!(await metricsFile.exists())) {
|
|
518
|
+
if (json) {
|
|
519
|
+
process.stdout.write("[]\n");
|
|
520
|
+
} else {
|
|
521
|
+
process.stdout.write("No metrics data yet.\n");
|
|
522
|
+
}
|
|
523
|
+
return;
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
const metricsStore = createMetricsStore(metricsDbPath);
|
|
527
|
+
|
|
528
|
+
try {
|
|
529
|
+
let sessions: SessionMetrics[];
|
|
530
|
+
|
|
531
|
+
if (agentName !== undefined) {
|
|
532
|
+
sessions = metricsStore.getSessionsByAgent(agentName);
|
|
533
|
+
} else if (runId !== undefined) {
|
|
534
|
+
sessions = metricsStore.getSessionsByRun(runId);
|
|
535
|
+
} else {
|
|
536
|
+
sessions = metricsStore.getRecentSessions(last);
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
if (json) {
|
|
540
|
+
if (byCapability) {
|
|
541
|
+
const groups = groupByCapability(sessions);
|
|
542
|
+
const grouped: Record<string, { sessions: SessionMetrics[]; totals: Totals }> = {};
|
|
543
|
+
for (const group of groups) {
|
|
544
|
+
grouped[group.capability] = {
|
|
545
|
+
sessions: group.sessions,
|
|
546
|
+
totals: group.totals,
|
|
547
|
+
};
|
|
548
|
+
}
|
|
549
|
+
process.stdout.write(`${JSON.stringify(grouped)}\n`);
|
|
550
|
+
} else {
|
|
551
|
+
process.stdout.write(`${JSON.stringify(sessions)}\n`);
|
|
552
|
+
}
|
|
553
|
+
return;
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
if (byCapability) {
|
|
557
|
+
printByCapability(sessions);
|
|
558
|
+
} else {
|
|
559
|
+
printCostSummary(sessions);
|
|
560
|
+
}
|
|
561
|
+
} finally {
|
|
562
|
+
metricsStore.close();
|
|
563
|
+
}
|
|
564
|
+
}
|