@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 +1 -1
- package/server/beads-reader.js +21 -19
- package/server/project-routes.js +25 -66
- package/server/ws-beads-watcher.js +2 -2
- package/server/ws-message-router.js +5 -5
package/package.json
CHANGED
package/server/beads-reader.js
CHANGED
|
@@ -1,13 +1,15 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { execFile } from 'node:child_process';
|
|
2
2
|
import { existsSync } from 'node:fs';
|
|
3
|
+
import { promisify } from 'node:util';
|
|
3
4
|
|
|
4
|
-
|
|
5
|
+
const execFileAsync = promisify(execFile);
|
|
6
|
+
|
|
7
|
+
async function runBd(args, dbPath) {
|
|
5
8
|
const fullArgs = [...args, '--json', '--db', dbPath, '--readonly'];
|
|
6
|
-
const stdout =
|
|
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 || []);
|
package/server/project-routes.js
CHANGED
|
@@ -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
|
|
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
|
|
1185
|
-
|
|
1186
|
-
const
|
|
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
|
|
1200
|
-
const
|
|
1187
|
+
for (const [stageName, stage] of Object.entries(stages)) {
|
|
1188
|
+
const iterations = stage.iterations || [];
|
|
1201
1189
|
const iters = [];
|
|
1202
|
-
|
|
1203
|
-
const
|
|
1204
|
-
|
|
1205
|
-
.
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
|
|
1213
|
-
|
|
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)
|
|
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
|
}
|