@tekyzinc/gsd-t 3.13.16 → 3.16.11
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 +44 -0
- package/README.md +1 -0
- package/bin/gsd-t-benchmark-orchestrator.js +437 -0
- package/bin/gsd-t-capture-lint.cjs +276 -0
- package/bin/gsd-t-completion-check.cjs +106 -0
- package/bin/gsd-t-orchestrator-config.cjs +64 -0
- package/bin/gsd-t-orchestrator-queue.cjs +180 -0
- package/bin/gsd-t-orchestrator-recover.cjs +231 -0
- package/bin/gsd-t-orchestrator-worker.cjs +219 -0
- package/bin/gsd-t-orchestrator.js +534 -0
- package/bin/gsd-t-stream-feed-client.cjs +151 -0
- package/bin/gsd-t-task-brief-compactor.cjs +89 -0
- package/bin/gsd-t-task-brief-template.cjs +96 -0
- package/bin/gsd-t-task-brief.js +249 -0
- package/bin/gsd-t-token-backfill.cjs +366 -0
- package/bin/gsd-t-token-capture.cjs +306 -0
- package/bin/gsd-t-token-dashboard.cjs +318 -0
- package/bin/gsd-t-token-regenerate-log.cjs +129 -0
- package/bin/gsd-t-transcript-tee.cjs +246 -0
- package/bin/gsd-t-unattended-heartbeat.cjs +188 -0
- package/bin/gsd-t-unattended-platform.cjs +191 -27
- package/bin/gsd-t-unattended-safety.cjs +8 -1
- package/bin/gsd-t-unattended.cjs +192 -31
- package/bin/gsd-t.js +329 -2
- package/bin/supervisor-pid-fingerprint.cjs +126 -0
- package/commands/gsd-t-debug.md +63 -51
- package/commands/gsd-t-design-decompose.md +2 -7
- package/commands/gsd-t-doc-ripple.md +20 -11
- package/commands/gsd-t-execute.md +82 -50
- package/commands/gsd-t-integrate.md +43 -16
- package/commands/gsd-t-plan.md +20 -7
- package/commands/gsd-t-prd.md +19 -12
- package/commands/gsd-t-quick.md +64 -29
- package/commands/gsd-t-resume.md +51 -4
- package/commands/gsd-t-unattended.md +19 -20
- package/commands/gsd-t-verify.md +48 -32
- package/commands/gsd-t-visualize.md +19 -17
- package/commands/gsd-t-wave.md +29 -27
- package/docs/architecture.md +16 -0
- package/docs/m40-benchmark-report.md +35 -0
- package/docs/requirements.md +20 -0
- package/package.json +1 -1
- package/scripts/gsd-t-dashboard-server.js +291 -4
- package/scripts/gsd-t-dashboard.html +31 -1
- package/scripts/gsd-t-design-review-server.js +3 -1
- package/scripts/gsd-t-stream-feed-server.js +428 -0
- package/scripts/gsd-t-stream-feed.html +1168 -0
- package/scripts/gsd-t-token-aggregator.js +373 -0
- package/scripts/gsd-t-transcript.html +422 -0
- package/scripts/hooks/gsd-t-in-session-probe.js +62 -0
- package/scripts/hooks/pre-commit-capture-lint +26 -0
- package/templates/CLAUDE-global.md +69 -0
- package/scripts/gsd-t-agent-dashboard-server.js +0 -424
- package/scripts/gsd-t-agent-dashboard.html +0 -1043
|
@@ -0,0 +1,373 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* GSD-T Token Aggregator (M40 D4-T6)
|
|
4
|
+
*
|
|
5
|
+
* Reads stream-feed JSONL (one file per UTC day), parses `usage` fields from
|
|
6
|
+
* `{type:"assistant"}` and `{type:"result"}` frames, groups by workerPid /
|
|
7
|
+
* taskId / wave / domain / milestone (inferred from orchestrator state.json),
|
|
8
|
+
* and:
|
|
9
|
+
* 1. Appends per-task rows to .gsd-t/metrics/token-usage.jsonl (schema v1)
|
|
10
|
+
* 2. Updates the matching row in .gsd-t/token-log.md (Tokens column rewrite)
|
|
11
|
+
*
|
|
12
|
+
* Modes: --once (one-shot scan) | --tail (follow JSONL live)
|
|
13
|
+
* Zero external deps — node fs + readline only.
|
|
14
|
+
*
|
|
15
|
+
* Contract: .gsd-t/contracts/stream-json-sink-contract.md §"Usage field propagation"
|
|
16
|
+
*/
|
|
17
|
+
'use strict';
|
|
18
|
+
|
|
19
|
+
const fs = require('fs');
|
|
20
|
+
const path = require('path');
|
|
21
|
+
|
|
22
|
+
const SCHEMA_VERSION = 1;
|
|
23
|
+
|
|
24
|
+
function parseArgs(argv) {
|
|
25
|
+
const opts = {
|
|
26
|
+
mode: 'once',
|
|
27
|
+
feedLog: null,
|
|
28
|
+
projectDir: process.cwd(),
|
|
29
|
+
outputPath: null,
|
|
30
|
+
tokenLogPath: null,
|
|
31
|
+
};
|
|
32
|
+
for (let i = 0; i < argv.length; i++) {
|
|
33
|
+
const a = argv[i];
|
|
34
|
+
if (a === '--tail') opts.mode = 'tail';
|
|
35
|
+
else if (a === '--once') opts.mode = 'once';
|
|
36
|
+
else if (a === '--feed-log') opts.feedLog = argv[++i];
|
|
37
|
+
else if (a === '--project-dir') opts.projectDir = argv[++i];
|
|
38
|
+
else if (a === '--output') opts.outputPath = argv[++i];
|
|
39
|
+
else if (a === '--token-log') opts.tokenLogPath = argv[++i];
|
|
40
|
+
else if (a === '--help' || a === '-h') opts.help = true;
|
|
41
|
+
}
|
|
42
|
+
if (!opts.outputPath) opts.outputPath = path.join(opts.projectDir, '.gsd-t', 'metrics', 'token-usage.jsonl');
|
|
43
|
+
if (!opts.tokenLogPath) opts.tokenLogPath = path.join(opts.projectDir, '.gsd-t', 'token-log.md');
|
|
44
|
+
if (!opts.feedLog) {
|
|
45
|
+
const today = new Date().toISOString().slice(0, 10);
|
|
46
|
+
opts.feedLog = path.join(opts.projectDir, '.gsd-t', 'stream-feed', `${today}.jsonl`);
|
|
47
|
+
}
|
|
48
|
+
return opts;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function showHelp() {
|
|
52
|
+
process.stdout.write(`
|
|
53
|
+
gsd-t-token-aggregator — Per-task token + cost rollup from stream-feed JSONL
|
|
54
|
+
|
|
55
|
+
Usage:
|
|
56
|
+
node scripts/gsd-t-token-aggregator.js --once [--feed-log PATH]
|
|
57
|
+
node scripts/gsd-t-token-aggregator.js --tail [--feed-log PATH]
|
|
58
|
+
|
|
59
|
+
Options:
|
|
60
|
+
--once One-shot scan, exit (default).
|
|
61
|
+
--tail Follow JSONL live; update as frames arrive.
|
|
62
|
+
--feed-log PATH JSONL to read. Default: .gsd-t/stream-feed/<today>.jsonl
|
|
63
|
+
--project-dir PATH Project root (default: cwd).
|
|
64
|
+
--output PATH Aggregate output path. Default: .gsd-t/metrics/token-usage.jsonl
|
|
65
|
+
--token-log PATH Markdown log to update. Default: .gsd-t/token-log.md
|
|
66
|
+
--help, -h Show this help.
|
|
67
|
+
`);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// ── Core aggregation ─────────────────────────────────────────────────────
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Parse a single JSON frame. Updates `groups` map keyed by (workerPid, taskId).
|
|
74
|
+
* Each group holds: workerPid, taskId, wave, domain, lastTs, startTs,
|
|
75
|
+
* inputTokens, outputTokens, cacheReadInputTokens, cacheCreationInputTokens,
|
|
76
|
+
* costUSD, numTurns, hasResult, assistantFrames, partial.
|
|
77
|
+
*/
|
|
78
|
+
function processFrame(frame, groups, ctx) {
|
|
79
|
+
if (!frame || typeof frame !== 'object') return;
|
|
80
|
+
|
|
81
|
+
// Track current worker/task context from task-boundary frames.
|
|
82
|
+
if (frame.type === 'task-boundary') {
|
|
83
|
+
const key = `${frame.workerPid}::${frame.taskId}`;
|
|
84
|
+
let g = groups.get(key);
|
|
85
|
+
if (!g) {
|
|
86
|
+
g = initGroup(frame.workerPid, frame.taskId, frame.domain, frame.wave);
|
|
87
|
+
groups.set(key, g);
|
|
88
|
+
} else {
|
|
89
|
+
if (frame.domain) g.domain = frame.domain;
|
|
90
|
+
if (typeof frame.wave === 'number') g.wave = frame.wave;
|
|
91
|
+
}
|
|
92
|
+
if (frame.state === 'start') g.startTs = frame.ts || null;
|
|
93
|
+
if (frame.state === 'done' || frame.state === 'failed') {
|
|
94
|
+
g.endTs = frame.ts || null;
|
|
95
|
+
g.state = frame.state;
|
|
96
|
+
}
|
|
97
|
+
ctx.current = { workerPid: frame.workerPid, taskId: frame.taskId };
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// Need context to attribute a frame to a task.
|
|
102
|
+
const attributeKey = pickAttribution(frame, ctx);
|
|
103
|
+
if (!attributeKey) return;
|
|
104
|
+
let g = groups.get(attributeKey);
|
|
105
|
+
if (!g) {
|
|
106
|
+
const [pid, tid] = attributeKey.split('::');
|
|
107
|
+
g = initGroup(Number(pid) || null, tid || null, null, null);
|
|
108
|
+
groups.set(attributeKey, g);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
if (frame.type === 'assistant' && frame.message && frame.message.usage) {
|
|
112
|
+
const u = frame.message.usage;
|
|
113
|
+
g.inputTokens += u.input_tokens || 0;
|
|
114
|
+
g.outputTokens += u.output_tokens || 0;
|
|
115
|
+
g.cacheReadInputTokens += u.cache_read_input_tokens || 0;
|
|
116
|
+
g.cacheCreationInputTokens += u.cache_creation_input_tokens || 0;
|
|
117
|
+
g.assistantFrames += 1;
|
|
118
|
+
} else if (frame.type === 'assistant' && frame.usage) {
|
|
119
|
+
// Some stream formats inline usage at top level.
|
|
120
|
+
const u = frame.usage;
|
|
121
|
+
g.inputTokens += u.input_tokens || 0;
|
|
122
|
+
g.outputTokens += u.output_tokens || 0;
|
|
123
|
+
g.cacheReadInputTokens += u.cache_read_input_tokens || 0;
|
|
124
|
+
g.cacheCreationInputTokens += u.cache_creation_input_tokens || 0;
|
|
125
|
+
g.assistantFrames += 1;
|
|
126
|
+
} else if (frame.type === 'result') {
|
|
127
|
+
if (frame.usage) {
|
|
128
|
+
// Result carries the aggregate — prefer this when present (overwrites per-assistant sum to match official count).
|
|
129
|
+
const u = frame.usage;
|
|
130
|
+
g.inputTokens = u.input_tokens != null ? u.input_tokens : g.inputTokens;
|
|
131
|
+
g.outputTokens = u.output_tokens != null ? u.output_tokens : g.outputTokens;
|
|
132
|
+
g.cacheReadInputTokens = u.cache_read_input_tokens != null ? u.cache_read_input_tokens : g.cacheReadInputTokens;
|
|
133
|
+
g.cacheCreationInputTokens = u.cache_creation_input_tokens != null ? u.cache_creation_input_tokens : g.cacheCreationInputTokens;
|
|
134
|
+
}
|
|
135
|
+
if (typeof frame.total_cost_usd === 'number') g.costUSD = frame.total_cost_usd;
|
|
136
|
+
else if (typeof frame.cost_usd === 'number') g.costUSD = frame.cost_usd;
|
|
137
|
+
else if (typeof frame.costUSD === 'number') g.costUSD = frame.costUSD;
|
|
138
|
+
if (typeof frame.num_turns === 'number') g.numTurns = frame.num_turns;
|
|
139
|
+
if (typeof frame.duration_ms === 'number') g.durationMs = frame.duration_ms;
|
|
140
|
+
g.hasResult = true;
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function pickAttribution(frame, ctx) {
|
|
145
|
+
if (frame.workerPid && frame.taskId) return `${frame.workerPid}::${frame.taskId}`;
|
|
146
|
+
if (frame.session_id && ctx.sessionMap && ctx.sessionMap.has(frame.session_id)) {
|
|
147
|
+
return ctx.sessionMap.get(frame.session_id);
|
|
148
|
+
}
|
|
149
|
+
if (ctx.current && ctx.current.workerPid != null && ctx.current.taskId) {
|
|
150
|
+
return `${ctx.current.workerPid}::${ctx.current.taskId}`;
|
|
151
|
+
}
|
|
152
|
+
return null;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
function initGroup(workerPid, taskId, domain, wave) {
|
|
156
|
+
return {
|
|
157
|
+
workerPid, taskId, domain: domain || null, wave: wave == null ? null : wave,
|
|
158
|
+
startTs: null, endTs: null, state: null,
|
|
159
|
+
inputTokens: 0, outputTokens: 0,
|
|
160
|
+
cacheReadInputTokens: 0, cacheCreationInputTokens: 0,
|
|
161
|
+
costUSD: null, numTurns: null, durationMs: null,
|
|
162
|
+
hasResult: false, assistantFrames: 0, partial: false,
|
|
163
|
+
};
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// ── File I/O ────────────────────────────────────────────────────────────
|
|
167
|
+
|
|
168
|
+
function readFrames(filePath) {
|
|
169
|
+
const frames = [];
|
|
170
|
+
if (!fs.existsSync(filePath)) return frames;
|
|
171
|
+
const content = fs.readFileSync(filePath, 'utf8');
|
|
172
|
+
for (const line of content.split('\n')) {
|
|
173
|
+
const t = line.trim();
|
|
174
|
+
if (!t) continue;
|
|
175
|
+
try { frames.push(JSON.parse(t)); }
|
|
176
|
+
catch { /* malformed — logged elsewhere */ }
|
|
177
|
+
}
|
|
178
|
+
return frames;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
function inferMilestone(projectDir) {
|
|
182
|
+
const statePath = path.join(projectDir, '.gsd-t', 'orchestrator', 'state.json');
|
|
183
|
+
try {
|
|
184
|
+
const obj = JSON.parse(fs.readFileSync(statePath, 'utf8'));
|
|
185
|
+
return obj.milestone || null;
|
|
186
|
+
} catch { return null; }
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
function writeTokenUsageJsonl(outputPath, rows) {
|
|
190
|
+
try { fs.mkdirSync(path.dirname(outputPath), { recursive: true }); } catch { /* exists */ }
|
|
191
|
+
const lines = rows.map(r => JSON.stringify(r));
|
|
192
|
+
fs.appendFileSync(outputPath, lines.join('\n') + (lines.length > 0 ? '\n' : ''));
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
/**
|
|
196
|
+
* Update .gsd-t/token-log.md rows in place: match by (command, startDatetime, task)
|
|
197
|
+
* and rewrite the Tokens column with `in=N out=N cr=N cc=N $X.XX`.
|
|
198
|
+
* We don't KNOW command from the JSONL; caller supplies it. For auto-mode, best
|
|
199
|
+
* match is by taskId alone.
|
|
200
|
+
*/
|
|
201
|
+
function updateTokenLog(tokenLogPath, rows) {
|
|
202
|
+
if (!fs.existsSync(tokenLogPath)) return { updated: 0, matched: 0 };
|
|
203
|
+
const content = fs.readFileSync(tokenLogPath, 'utf8');
|
|
204
|
+
const lines = content.split('\n');
|
|
205
|
+
let updated = 0, matched = 0;
|
|
206
|
+
|
|
207
|
+
// Build an index by taskId for O(1) lookup.
|
|
208
|
+
const byTask = new Map();
|
|
209
|
+
for (const r of rows) {
|
|
210
|
+
if (r.taskId) byTask.set(r.taskId, r);
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
for (let i = 0; i < lines.length; i++) {
|
|
214
|
+
const line = lines[i];
|
|
215
|
+
if (!line.startsWith('|') || line.includes('--------')) continue;
|
|
216
|
+
// Split by | keeping trailing empties.
|
|
217
|
+
const parts = line.split('|').map(s => s.trim());
|
|
218
|
+
// Table shape: | DT-s | DT-e | Command | Step | Model | Duration(s) | Notes | Tokens | Compacted | Domain | Task | Ctx% |
|
|
219
|
+
// parts[0] is leading empty, parts[1] = DT-s, …, parts[11] = Task, parts[12] = Ctx%
|
|
220
|
+
if (parts.length < 13) continue;
|
|
221
|
+
const taskCell = parts[11];
|
|
222
|
+
if (!taskCell) continue;
|
|
223
|
+
matched += 1;
|
|
224
|
+
const r = byTask.get(taskCell);
|
|
225
|
+
if (!r) continue;
|
|
226
|
+
const tokenSummary = formatTokenSummary(r);
|
|
227
|
+
if (parts[8] === tokenSummary) continue;
|
|
228
|
+
parts[8] = tokenSummary;
|
|
229
|
+
lines[i] = parts.join(' | ');
|
|
230
|
+
updated += 1;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
if (updated > 0) {
|
|
234
|
+
fs.writeFileSync(tokenLogPath, lines.join('\n'));
|
|
235
|
+
}
|
|
236
|
+
return { updated, matched };
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
function formatTokenSummary(r) {
|
|
240
|
+
const cost = (typeof r.costUSD === 'number') ? `$${r.costUSD.toFixed(2)}` : '—';
|
|
241
|
+
return `in=${r.inputTokens} out=${r.outputTokens} cr=${r.cacheReadInputTokens} cc=${r.cacheCreationInputTokens} ${cost}`;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
// ── Run modes ───────────────────────────────────────────────────────────
|
|
245
|
+
|
|
246
|
+
function runOnce(opts) {
|
|
247
|
+
const frames = readFrames(opts.feedLog);
|
|
248
|
+
const groups = new Map();
|
|
249
|
+
const ctx = { current: null, sessionMap: new Map() };
|
|
250
|
+
for (const f of frames) processFrame(f, groups, ctx);
|
|
251
|
+
const milestone = inferMilestone(opts.projectDir);
|
|
252
|
+
const nowIso = new Date().toISOString();
|
|
253
|
+
const rows = [];
|
|
254
|
+
for (const g of groups.values()) {
|
|
255
|
+
if (!g.hasResult && g.assistantFrames === 0) continue;
|
|
256
|
+
g.partial = !g.hasResult;
|
|
257
|
+
rows.push({
|
|
258
|
+
schemaVersion: SCHEMA_VERSION,
|
|
259
|
+
ts: nowIso,
|
|
260
|
+
workerPid: g.workerPid,
|
|
261
|
+
taskId: g.taskId,
|
|
262
|
+
domain: g.domain,
|
|
263
|
+
wave: g.wave,
|
|
264
|
+
milestone,
|
|
265
|
+
inputTokens: g.inputTokens,
|
|
266
|
+
outputTokens: g.outputTokens,
|
|
267
|
+
cacheReadInputTokens: g.cacheReadInputTokens,
|
|
268
|
+
cacheCreationInputTokens: g.cacheCreationInputTokens,
|
|
269
|
+
costUSD: g.costUSD,
|
|
270
|
+
numTurns: g.numTurns,
|
|
271
|
+
durationMs: g.durationMs,
|
|
272
|
+
startTs: g.startTs,
|
|
273
|
+
endTs: g.endTs,
|
|
274
|
+
state: g.state,
|
|
275
|
+
assistantFrames: g.assistantFrames,
|
|
276
|
+
partial: g.partial,
|
|
277
|
+
});
|
|
278
|
+
}
|
|
279
|
+
writeTokenUsageJsonl(opts.outputPath, rows);
|
|
280
|
+
const logResult = updateTokenLog(opts.tokenLogPath, rows);
|
|
281
|
+
return { rows, logUpdate: logResult };
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
function runTail(opts) {
|
|
285
|
+
// Follow file from end. Simple poll every 500ms.
|
|
286
|
+
let offset = 0;
|
|
287
|
+
try { offset = fs.statSync(opts.feedLog).size; } catch { /* new */ }
|
|
288
|
+
const groups = new Map();
|
|
289
|
+
const ctx = { current: null, sessionMap: new Map() };
|
|
290
|
+
let lastWroteRows = 0;
|
|
291
|
+
|
|
292
|
+
const poll = () => {
|
|
293
|
+
try {
|
|
294
|
+
const stat = fs.statSync(opts.feedLog);
|
|
295
|
+
if (stat.size <= offset) return;
|
|
296
|
+
const fd = fs.openSync(opts.feedLog, 'r');
|
|
297
|
+
try {
|
|
298
|
+
const buf = Buffer.alloc(stat.size - offset);
|
|
299
|
+
fs.readSync(fd, buf, 0, buf.length, offset);
|
|
300
|
+
offset = stat.size;
|
|
301
|
+
const chunk = buf.toString('utf8');
|
|
302
|
+
let acc = '';
|
|
303
|
+
for (const line of chunk.split('\n')) {
|
|
304
|
+
const t = line.trim();
|
|
305
|
+
if (!t) continue;
|
|
306
|
+
try { processFrame(JSON.parse(t), groups, ctx); } catch { /* skip */ }
|
|
307
|
+
}
|
|
308
|
+
} finally { fs.closeSync(fd); }
|
|
309
|
+
// Rewrite aggregate on every tick when groups is non-empty — idempotent.
|
|
310
|
+
const milestone = inferMilestone(opts.projectDir);
|
|
311
|
+
const nowIso = new Date().toISOString();
|
|
312
|
+
const rows = [];
|
|
313
|
+
for (const g of groups.values()) {
|
|
314
|
+
if (!g.hasResult && g.assistantFrames === 0) continue;
|
|
315
|
+
g.partial = !g.hasResult;
|
|
316
|
+
rows.push({
|
|
317
|
+
schemaVersion: SCHEMA_VERSION, ts: nowIso,
|
|
318
|
+
workerPid: g.workerPid, taskId: g.taskId,
|
|
319
|
+
domain: g.domain, wave: g.wave, milestone,
|
|
320
|
+
inputTokens: g.inputTokens, outputTokens: g.outputTokens,
|
|
321
|
+
cacheReadInputTokens: g.cacheReadInputTokens,
|
|
322
|
+
cacheCreationInputTokens: g.cacheCreationInputTokens,
|
|
323
|
+
costUSD: g.costUSD, numTurns: g.numTurns, durationMs: g.durationMs,
|
|
324
|
+
startTs: g.startTs, endTs: g.endTs, state: g.state,
|
|
325
|
+
assistantFrames: g.assistantFrames, partial: g.partial,
|
|
326
|
+
});
|
|
327
|
+
}
|
|
328
|
+
if (rows.length !== lastWroteRows) {
|
|
329
|
+
writeTokenUsageJsonl(opts.outputPath, rows);
|
|
330
|
+
updateTokenLog(opts.tokenLogPath, rows);
|
|
331
|
+
lastWroteRows = rows.length;
|
|
332
|
+
}
|
|
333
|
+
} catch { /* file missing or I/O error */ }
|
|
334
|
+
};
|
|
335
|
+
|
|
336
|
+
const interval = setInterval(poll, 500);
|
|
337
|
+
let stopped = false;
|
|
338
|
+
function stop() {
|
|
339
|
+
if (stopped) return;
|
|
340
|
+
stopped = true;
|
|
341
|
+
clearInterval(interval);
|
|
342
|
+
}
|
|
343
|
+
process.on('SIGINT', () => { stop(); process.exit(0); });
|
|
344
|
+
process.on('SIGTERM', () => { stop(); process.exit(0); });
|
|
345
|
+
return { stop };
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
module.exports = {
|
|
349
|
+
processFrame,
|
|
350
|
+
readFrames,
|
|
351
|
+
inferMilestone,
|
|
352
|
+
writeTokenUsageJsonl,
|
|
353
|
+
updateTokenLog,
|
|
354
|
+
formatTokenSummary,
|
|
355
|
+
initGroup,
|
|
356
|
+
runOnce,
|
|
357
|
+
runTail,
|
|
358
|
+
parseArgs,
|
|
359
|
+
SCHEMA_VERSION,
|
|
360
|
+
};
|
|
361
|
+
|
|
362
|
+
if (require.main === module) {
|
|
363
|
+
const opts = parseArgs(process.argv.slice(2));
|
|
364
|
+
if (opts.help) { showHelp(); process.exit(0); }
|
|
365
|
+
if (opts.mode === 'tail') {
|
|
366
|
+
const handle = runTail(opts);
|
|
367
|
+
process.stdout.write(`[token-aggregator] tailing ${opts.feedLog}\n`);
|
|
368
|
+
// Keep process alive.
|
|
369
|
+
} else {
|
|
370
|
+
const { rows, logUpdate } = runOnce(opts);
|
|
371
|
+
process.stdout.write(`[token-aggregator] processed ${rows.length} task groups; ${logUpdate.updated}/${logUpdate.matched} token-log rows updated\n`);
|
|
372
|
+
}
|
|
373
|
+
}
|