@worca/ui 0.1.1-rc.2 → 0.2.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@worca/ui",
3
- "version": "0.1.1-rc.2",
3
+ "version": "0.2.0",
4
4
  "description": "Pipeline monitoring UI for worca-cc",
5
5
  "license": "MIT",
6
6
  "author": "Sinisha Djukic",
@@ -1,13 +1,15 @@
1
- import { execFileSync } from 'node:child_process';
1
+ import { execFile } from 'node:child_process';
2
2
  import { existsSync } from 'node:fs';
3
+ import { promisify } from 'node:util';
3
4
 
4
- function runBd(args, dbPath) {
5
+ const execFileAsync = promisify(execFile);
6
+
7
+ async function runBd(args, dbPath) {
5
8
  const fullArgs = [...args, '--json', '--db', dbPath, '--readonly'];
6
- const stdout = execFileSync('bd', fullArgs, {
9
+ const { stdout } = await execFileAsync('bd', fullArgs, {
7
10
  encoding: 'utf8',
8
11
  timeout: 10000,
9
12
  maxBuffer: 10 * 1024 * 1024,
10
- stdio: ['ignore', 'pipe', 'pipe'],
11
13
  });
12
14
  return JSON.parse(stdout);
13
15
  }
@@ -30,12 +32,12 @@ function transformIssue(issue, deps) {
30
32
  };
31
33
  }
32
34
 
33
- function enrichWithDeps(issues, dbPath) {
35
+ async function enrichWithDeps(issues, dbPath) {
34
36
  const needDeps = issues.filter((i) => i.dependency_count > 0);
35
37
  if (needDeps.length === 0) {
36
38
  return issues.map((i) => transformIssue(i, []));
37
39
  }
38
- const detailed = runBd(['show', ...needDeps.map((i) => i.id)], dbPath);
40
+ const detailed = await runBd(['show', ...needDeps.map((i) => i.id)], dbPath);
39
41
  const depMap = new Map(detailed.map((d) => [d.id, d.dependencies || []]));
40
42
  return issues.map((i) => transformIssue(i, depMap.get(i.id) || []));
41
43
  }
@@ -44,18 +46,18 @@ export function dbExists(beadsDb) {
44
46
  return existsSync(beadsDb);
45
47
  }
46
48
 
47
- export function listIssues(beadsDb) {
49
+ export async function listIssues(beadsDb) {
48
50
  try {
49
- const issues = runBd(['list', '--limit', '0'], beadsDb);
51
+ const issues = await runBd(['list', '--limit', '0'], beadsDb);
50
52
  return enrichWithDeps(issues, beadsDb);
51
53
  } catch {
52
54
  return [];
53
55
  }
54
56
  }
55
57
 
56
- export function listIssuesByLabel(beadsDb, label) {
58
+ export async function listIssuesByLabel(beadsDb, label) {
57
59
  try {
58
- const issues = runBd(
60
+ const issues = await runBd(
59
61
  ['list', '--label-any', label, '--all', '--limit', '0'],
60
62
  beadsDb,
61
63
  );
@@ -65,12 +67,12 @@ export function listIssuesByLabel(beadsDb, label) {
65
67
  }
66
68
  }
67
69
 
68
- export function listUnlinkedIssues(beadsDb) {
70
+ export async function listUnlinkedIssues(beadsDb) {
69
71
  try {
70
- const issues = runBd(['list', '--limit', '0'], beadsDb);
72
+ const issues = await runBd(['list', '--limit', '0'], beadsDb);
71
73
  if (issues.length === 0) return [];
72
74
  // bd list doesn't include labels — use bd show to get them
73
- const detailed = runBd(['show', ...issues.map((i) => i.id)], beadsDb);
75
+ const detailed = await runBd(['show', ...issues.map((i) => i.id)], beadsDb);
74
76
  const detailMap = new Map(detailed.map((d) => [d.id, d]));
75
77
  const unlinked = issues.filter((i) => {
76
78
  const d = detailMap.get(i.id);
@@ -87,9 +89,9 @@ export function listUnlinkedIssues(beadsDb) {
87
89
  }
88
90
  }
89
91
 
90
- export function countIssuesByRunLabel(beadsDb) {
92
+ export async function countIssuesByRunLabel(beadsDb) {
91
93
  try {
92
- const rows = runBd(['label', 'list-all'], beadsDb);
94
+ const rows = await runBd(['label', 'list-all'], beadsDb);
93
95
  const counts = {};
94
96
  for (const row of rows) {
95
97
  if (row.label.startsWith('run:')) {
@@ -102,18 +104,18 @@ export function countIssuesByRunLabel(beadsDb) {
102
104
  }
103
105
  }
104
106
 
105
- export function listDistinctRunLabels(beadsDb) {
107
+ export async function listDistinctRunLabels(beadsDb) {
106
108
  try {
107
- const rows = runBd(['label', 'list-all'], beadsDb);
109
+ const rows = await runBd(['label', 'list-all'], beadsDb);
108
110
  return rows.filter((r) => r.label.startsWith('run:')).map((r) => r.label);
109
111
  } catch {
110
112
  return [];
111
113
  }
112
114
  }
113
115
 
114
- export function getIssue(beadsDb, id) {
116
+ export async function getIssue(beadsDb, id) {
115
117
  try {
116
- const results = runBd(['show', id], beadsDb);
118
+ const results = await runBd(['show', id], beadsDb);
117
119
  if (!results || results.length === 0) return null;
118
120
  const issue = results[0];
119
121
  return transformIssue(issue, issue.dependencies || []);
@@ -1174,84 +1174,43 @@ export function createProjectScopedRoutes() {
1174
1174
  });
1175
1175
 
1176
1176
  // GET /api/projects/:projectId/costs — token & cost data
1177
+ // Reads per-iteration token_usage from each run's status.json.
1177
1178
  router.get('/costs', requireWorcaDir, (req, res) => {
1178
1179
  const { worcaDir } = req.project;
1179
- const resultsDir = join(worcaDir, 'results');
1180
- if (!existsSync(resultsDir)) return res.json({ ok: true, tokenData: {} });
1181
-
1180
+ const runs = discoverRuns(worcaDir);
1182
1181
  const tokenData = {};
1183
1182
 
1184
- for (const entry of readdirSync(resultsDir, { withFileTypes: true })) {
1185
- if (!entry.isDirectory()) continue;
1186
- const runDir = join(resultsDir, entry.name);
1187
- const stageNames = [];
1188
- try {
1189
- for (const sub of readdirSync(runDir, { withFileTypes: true })) {
1190
- if (sub.isDirectory()) stageNames.push(sub.name);
1191
- }
1192
- } catch {
1193
- continue;
1194
- }
1195
-
1196
- if (stageNames.length === 0) continue;
1197
- tokenData[entry.name] = {};
1183
+ for (const run of runs) {
1184
+ const stages = run.stages || {};
1185
+ const runEntry = {};
1198
1186
 
1199
- for (const stage of stageNames) {
1200
- const stageDir = join(runDir, stage);
1187
+ for (const [stageName, stage] of Object.entries(stages)) {
1188
+ const iterations = stage.iterations || [];
1201
1189
  const iters = [];
1202
- try {
1203
- const files = readdirSync(stageDir)
1204
- .filter((f) => f.startsWith('iter-') && f.endsWith('.json'))
1205
- .sort();
1206
- for (const file of files) {
1207
- try {
1208
- const data = JSON.parse(
1209
- readFileSync(join(stageDir, file), 'utf8'),
1210
- );
1211
- const mu = data.modelUsage || {};
1212
- let inputTokens = 0,
1213
- outputTokens = 0,
1214
- cacheReadInputTokens = 0,
1215
- cacheCreationInputTokens = 0,
1216
- webSearchRequests = 0;
1217
- const models = [];
1218
- for (const [model, usage] of Object.entries(mu)) {
1219
- inputTokens += usage.inputTokens || 0;
1220
- outputTokens += usage.outputTokens || 0;
1221
- cacheReadInputTokens += usage.cacheReadInputTokens || 0;
1222
- cacheCreationInputTokens += usage.cacheCreationInputTokens || 0;
1223
- webSearchRequests += usage.webSearchRequests || 0;
1224
- models.push(model);
1225
- }
1226
- const cacheCreation = data.usage?.cache_creation || {};
1227
- iters.push({
1228
- inputTokens,
1229
- outputTokens,
1230
- cacheReadInputTokens,
1231
- cacheCreationInputTokens,
1232
- webSearchRequests,
1233
- cacheEphemeral1hTokens:
1234
- cacheCreation.ephemeral_1h_input_tokens || 0,
1235
- cacheEphemeral5mTokens:
1236
- cacheCreation.ephemeral_5m_input_tokens || 0,
1237
- models,
1238
- });
1239
- } catch {
1240
- /* skip bad files */
1241
- }
1242
- }
1243
- } catch {
1244
- /* skip */
1190
+ for (const iter of iterations) {
1191
+ const tu = iter.token_usage || {};
1192
+ iters.push({
1193
+ inputTokens: tu.input_tokens || 0,
1194
+ outputTokens: tu.output_tokens || 0,
1195
+ cacheReadInputTokens: tu.cache_read_input_tokens || 0,
1196
+ cacheCreationInputTokens: tu.cache_creation_input_tokens || 0,
1197
+ webSearchRequests: tu.web_search_requests || 0,
1198
+ cacheEphemeral1hTokens: tu.cache_ephemeral_1h_tokens || 0,
1199
+ cacheEphemeral5mTokens: tu.cache_ephemeral_5m_tokens || 0,
1200
+ models: tu.model ? [tu.model] : [],
1201
+ });
1245
1202
  }
1246
- if (iters.length > 0) tokenData[entry.name][stage] = iters;
1203
+ if (iters.length > 0) runEntry[stageName] = iters;
1247
1204
  }
1205
+
1206
+ if (Object.keys(runEntry).length > 0) tokenData[run.id] = runEntry;
1248
1207
  }
1249
1208
 
1250
1209
  res.json({ ok: true, tokenData });
1251
1210
  });
1252
1211
 
1253
1212
  // ─── Beads (project-scoped) ─────────────────────────────────────────
1254
- router.get('/beads/issues', requireWorcaDir, (req, res) => {
1213
+ router.get('/beads/issues', requireWorcaDir, async (req, res) => {
1255
1214
  const beadsDbPath = join(req.project.worcaDir, '..', '.beads', 'beads.db');
1256
1215
  if (!dbExists(beadsDbPath)) {
1257
1216
  return res.json({
@@ -1262,7 +1221,7 @@ export function createProjectScopedRoutes() {
1262
1221
  });
1263
1222
  }
1264
1223
  try {
1265
- const issues = listIssues(beadsDbPath);
1224
+ const issues = await listIssues(beadsDbPath);
1266
1225
  res.json({ ok: true, issues, dbExists: true, dbPath: beadsDbPath });
1267
1226
  } catch (err) {
1268
1227
  res.status(500).json({ ok: false, error: err.message });
@@ -1277,7 +1236,7 @@ export function createProjectScopedRoutes() {
1277
1236
  .json({ ok: false, error: 'Issue ID must be a positive integer' });
1278
1237
  }
1279
1238
  const beadsDbPath = join(req.project.worcaDir, '..', '.beads', 'beads.db');
1280
- const issue = getIssue(beadsDbPath, issueId);
1239
+ const issue = await getIssue(beadsDbPath, issueId);
1281
1240
  if (!issue) {
1282
1241
  return res
1283
1242
  .status(404)
@@ -21,10 +21,10 @@ export function createBeadsWatcher({ worcaDir, broadcaster, projectId }) {
21
21
 
22
22
  function scheduleBeadsRefresh() {
23
23
  if (BEADS_REFRESH_TIMER) clearTimeout(BEADS_REFRESH_TIMER);
24
- BEADS_REFRESH_TIMER = setTimeout(() => {
24
+ BEADS_REFRESH_TIMER = setTimeout(async () => {
25
25
  BEADS_REFRESH_TIMER = null;
26
26
  try {
27
- const issues = listIssues(beadsDbPath);
27
+ const issues = await listIssues(beadsDbPath);
28
28
  broadcaster.broadcast(
29
29
  'beads-update',
30
30
  {
@@ -531,7 +531,7 @@ export function createMessageRouter({
531
531
  );
532
532
  return;
533
533
  }
534
- const issues = listIssues(beadsDbPath);
534
+ const issues = await listIssues(beadsDbPath);
535
535
  ws.send(
536
536
  JSON.stringify(
537
537
  makeOk(req, { issues, dbExists: true, dbPath: beadsDbPath }),
@@ -552,7 +552,7 @@ export function createMessageRouter({
552
552
  ws.send(JSON.stringify(makeOk(req, { issues: [], dbExists: false })));
553
553
  return;
554
554
  }
555
- const issues = listUnlinkedIssues(beadsDbPath);
555
+ const issues = await listUnlinkedIssues(beadsDbPath);
556
556
  ws.send(JSON.stringify(makeOk(req, { issues, dbExists: true })));
557
557
  return;
558
558
  }
@@ -569,7 +569,7 @@ export function createMessageRouter({
569
569
  ws.send(JSON.stringify(makeOk(req, { refs: [] })));
570
570
  return;
571
571
  }
572
- const refs = listDistinctRunLabels(beadsDbPath);
572
+ const refs = await listDistinctRunLabels(beadsDbPath);
573
573
  ws.send(JSON.stringify(makeOk(req, { refs })));
574
574
  return;
575
575
  }
@@ -586,7 +586,7 @@ export function createMessageRouter({
586
586
  ws.send(JSON.stringify(makeOk(req, { counts: {} })));
587
587
  return;
588
588
  }
589
- const counts = countIssuesByRunLabel(beadsDbPath);
589
+ const counts = await countIssuesByRunLabel(beadsDbPath);
590
590
  ws.send(JSON.stringify(makeOk(req, { counts })));
591
591
  return;
592
592
  }
@@ -612,7 +612,7 @@ export function createMessageRouter({
612
612
  ws.send(JSON.stringify(makeOk(req, { issues: [], runId })));
613
613
  return;
614
614
  }
615
- const issues = listIssuesByLabel(beadsDbPath, `run:${runId}`);
615
+ const issues = await listIssuesByLabel(beadsDbPath, `run:${runId}`);
616
616
  ws.send(JSON.stringify(makeOk(req, { issues, runId })));
617
617
  return;
618
618
  }