@worca/ui 0.1.1-rc.2 → 0.2.0-rc.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/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-rc.1",
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)
package/server/watcher.js CHANGED
@@ -26,22 +26,9 @@ function isTerminal(status) {
26
26
  );
27
27
  }
28
28
 
29
- function isPipelineRunning(worcaDir) {
30
- const pidPath = join(worcaDir, 'pipeline.pid');
31
- if (!existsSync(pidPath)) return false;
32
- try {
33
- const pid = parseInt(readFileSync(pidPath, 'utf8').trim(), 10);
34
- process.kill(pid, 0); // signal 0 = check if alive
35
- return true;
36
- } catch {
37
- return false; // stale PID or unreadable
38
- }
39
- }
40
-
41
29
  export function discoverRuns(worcaDir) {
42
30
  const runs = [];
43
31
  const seenIds = new Set();
44
- const pipelineRunning = isPipelineRunning(worcaDir);
45
32
 
46
33
  // 1. Check active_run pointer for the current run
47
34
  const activeRunPath = join(worcaDir, 'active_run');
@@ -51,7 +38,8 @@ export function discoverRuns(worcaDir) {
51
38
  const candidate = join(worcaDir, 'runs', activeId, 'status.json');
52
39
  if (existsSync(candidate)) {
53
40
  const status = JSON.parse(readFileSync(candidate, 'utf8'));
54
- const active = !isTerminal(status) && pipelineRunning;
41
+ const active =
42
+ !isTerminal(status) && status.pipeline_status === 'running';
55
43
  const id = createRunId(status);
56
44
  runs.push({ id, active, ...status });
57
45
  seenIds.add(id);
@@ -88,7 +76,8 @@ export function discoverRuns(worcaDir) {
88
76
  const status = JSON.parse(readFileSync(statusPath, 'utf8'));
89
77
  const id = createRunId(status);
90
78
  if (!seenIds.has(id)) {
91
- const active = !isTerminal(status) && pipelineRunning;
79
+ const active =
80
+ !isTerminal(status) && status.pipeline_status === 'running';
92
81
  runs.push({ id, active, ...status });
93
82
  seenIds.add(id);
94
83
  }
@@ -142,7 +131,6 @@ export function discoverRuns(worcaDir) {
142
131
  export async function discoverRunsAsync(worcaDir) {
143
132
  const runs = [];
144
133
  const seenIds = new Set();
145
- const pipelineRunning = isPipelineRunning(worcaDir); // cheap check (one stat + one kill)
146
134
 
147
135
  // 1. Active run
148
136
  const activeRunPath = join(worcaDir, 'active_run');
@@ -150,7 +138,7 @@ export async function discoverRunsAsync(worcaDir) {
150
138
  const activeId = (await readFile(activeRunPath, 'utf8')).trim();
151
139
  const candidate = join(worcaDir, 'runs', activeId, 'status.json');
152
140
  const status = JSON.parse(await readFile(candidate, 'utf8'));
153
- const active = !isTerminal(status) && pipelineRunning;
141
+ const active = !isTerminal(status) && status.pipeline_status === 'running';
154
142
  const id = createRunId(status);
155
143
  runs.push({ id, active, ...status });
156
144
  seenIds.add(id);
@@ -191,7 +179,8 @@ export async function discoverRunsAsync(worcaDir) {
191
179
  );
192
180
  const id = createRunId(status);
193
181
  if (!seenIds.has(id)) {
194
- const active = !isTerminal(status) && pipelineRunning;
182
+ const active =
183
+ !isTerminal(status) && status.pipeline_status === 'running';
195
184
  runs.push({ id, active, ...status });
196
185
  seenIds.add(id);
197
186
  }
@@ -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
  }