crewforth 3.1.0-rc.2 → 3.1.0-rc.4
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 +70 -4
- package/README.md +1 -1
- package/kit/agents/crew-commit-agent.md +5 -2
- package/kit/eval/doctor.sh +26 -0
- package/kit/eval/smoke-test.sh +274 -18
- package/kit/hooks/context-usage.sh +1 -1
- package/kit/hooks/guard-bash.sh +93 -6
- package/kit/hooks/guard-schedule.sh +92 -0
- package/kit/hooks/prompt-approval.sh +68 -21
- package/kit/settings.json +11 -0
- package/kit/skills/crew-approve/SKILL.md +26 -0
- package/kit/studio/server/index.js +18 -0
- package/kit/studio/server/lib/graph.js +137 -15
- package/kit/studio/server/lib/permissions.js +11 -3
- package/kit/studio/server/lib/pricing.js +63 -0
- package/kit/studio/server/lib/usage.js +144 -0
- package/kit/studio/web/app.js +159 -38
- package/kit/studio/web/approvals.js +20 -1
- package/kit/studio/web/canvas.js +158 -30
- package/kit/studio/web/chat.js +1 -0
- package/kit/studio/web/convo.js +4 -2
- package/kit/studio/web/dock.js +1 -0
- package/kit/studio/web/graph-plan.js +173 -13
- package/kit/studio/web/index.html +1 -0
- package/kit/studio/web/inspect.js +14 -1
- package/kit/studio/web/list-plan.js +4 -4
- package/kit/studio/web/list.js +2 -0
- package/kit/studio/web/names.js +44 -0
- package/kit/studio/web/style.css +64 -10
- package/kit/studio/web/timeline-plan.js +13 -5
- package/kit/studio/web/timeline.js +10 -4
- package/kit/studio/web/usage.js +77 -0
- package/package.json +1 -1
|
@@ -6,6 +6,7 @@ import {
|
|
|
6
6
|
rows, groupsOf, extent, windowOf, place, ticks, clock, stepRange, waitNote, factsOf, xOf, RANGES, DEFAULT_RANGE,
|
|
7
7
|
} from './timeline-plan.js';
|
|
8
8
|
import { Press } from './press.js';
|
|
9
|
+
import { shownType, hoverOf } from './names.js';
|
|
9
10
|
|
|
10
11
|
|
|
11
12
|
function mk(tag, cls, text) {
|
|
@@ -260,7 +261,7 @@ export class Timeline {
|
|
|
260
261
|
b.style.left = `${at.left}px`;
|
|
261
262
|
b.style.width = `${at.width}px`;
|
|
262
263
|
const node = this.nodes.find((n) => n.id === seg.id);
|
|
263
|
-
b.setAttribute('aria-label', `${node
|
|
264
|
+
b.setAttribute('aria-label', `${node ? shownType(node) : 'agent'}, ${seg.tone === 'wait' ? 'waiting for you' : (node?.status ?? '')}, ${clock(seg.from)} to ${clock(seg.to)}`);
|
|
264
265
|
if (node && this.dimmed(node)) b.classList.add('dim');
|
|
265
266
|
if (seg.id === this.selected) b.classList.add('on');
|
|
266
267
|
b.addEventListener('click', (e) => { e.stopPropagation(); this.#choose(seg.id); });
|
|
@@ -310,6 +311,7 @@ export class Timeline {
|
|
|
310
311
|
const tile = this.hooks.tileOf?.(r.members[0]);
|
|
311
312
|
if (tile) lab.append(tile);
|
|
312
313
|
lab.append(mk('span', 'tl-name', r.label), mk('span', 'row-fill'), mk('span', 'sub', r.sub));
|
|
314
|
+
lab.title = r.real ?? '';
|
|
313
315
|
this.#bars(track, r.lanes, win, width);
|
|
314
316
|
if (r.members.every((m) => this.dimmed(m))) row.classList.add('dim');
|
|
315
317
|
} else {
|
|
@@ -319,7 +321,7 @@ export class Timeline {
|
|
|
319
321
|
const sub = mk('span', 'sub', r.sub);
|
|
320
322
|
if (r.tone) sub.dataset.tone = r.tone;
|
|
321
323
|
lab.append(mk('span', 'tl-name', r.label), mk('span', 'row-fill'), sub);
|
|
322
|
-
lab.title = r.node
|
|
324
|
+
lab.title = hoverOf(r.node);
|
|
323
325
|
// One listener, on the row: the label is a button inside it, and its click arrives here too. Two
|
|
324
326
|
// listeners chose the agent and un-chose it in the same click.
|
|
325
327
|
row.addEventListener('click', () => this.#choose(r.id));
|
|
@@ -355,8 +357,12 @@ export class Timeline {
|
|
|
355
357
|
const dot = mk('span', 'dot');
|
|
356
358
|
dot.dataset.tone = st.tone ?? 'none';
|
|
357
359
|
pill.append(dot, mk('span', null, st.word ?? 'State not measured'));
|
|
358
|
-
|
|
359
|
-
|
|
360
|
+
const dname = mk('span', 'tl-d-name', shownType(n));
|
|
361
|
+
dname.title = n.agentType ?? '';
|
|
362
|
+
head.append(dname, pill);
|
|
363
|
+
const dtask = mk('div', 'tl-d-task', n.description ?? '');
|
|
364
|
+
dtask.title = n.description ?? '';
|
|
365
|
+
left.append(head, dtask, mk('div', 'sub tl-d-facts', factsOf(n, now, this.hooks.fmt)));
|
|
360
366
|
|
|
361
367
|
const mid = mk('div', 'tl-d-error');
|
|
362
368
|
mid.append(mk('div', 'isec-h', 'Last error'));
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
// What a session spent, as words: how long, how many tokens, and an estimate of the cost.
|
|
2
|
+
//
|
|
3
|
+
// The numbers are the server's (server/lib/usage.js); the prices are in one file there (server/lib/pricing.js).
|
|
4
|
+
// Nothing here knows a price. The cost is an estimate at API list price and is always written as one: a leading
|
|
5
|
+
// "~", and a note that says what it is. A session with tokens from a model the price table does not have gets
|
|
6
|
+
// a dash, not a smaller number.
|
|
7
|
+
|
|
8
|
+
export const COST_NOTE = 'Estimated at API list price; on a subscription it is not what you are billed';
|
|
9
|
+
export const NO_COST = '—';
|
|
10
|
+
|
|
11
|
+
/** 950 · 12.4k · 3.2M · 1.53B */
|
|
12
|
+
export function fmtCount(n) {
|
|
13
|
+
if (n == null || !Number.isFinite(n)) return null;
|
|
14
|
+
if (n < 1000) return String(Math.round(n));
|
|
15
|
+
if (n < 1_000_000) return `${(n / 1000).toFixed(n < 100_000 ? 1 : 0)}k`;
|
|
16
|
+
if (n < 1_000_000_000) return `${(n / 1_000_000).toFixed(n < 100_000_000 ? 1 : 0)}M`;
|
|
17
|
+
return `${(n / 1_000_000_000).toFixed(2)}B`;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/** "~$4.10", "~$1,240", "<$0.01"; a dash when there is no estimate to give. */
|
|
21
|
+
export function fmtCost(usd) {
|
|
22
|
+
if (usd == null || !Number.isFinite(usd)) return NO_COST;
|
|
23
|
+
if (usd > 0 && usd < 0.01) return '<$0.01';
|
|
24
|
+
if (usd >= 1000) return `~$${Math.round(usd).toLocaleString('en-US')}`;
|
|
25
|
+
return `~$${usd.toFixed(2)}`;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/** "40s", "12m", "3h 5m", "2d 4h": coarse, for a line with no room. */
|
|
29
|
+
export function fmtSpan(ms) {
|
|
30
|
+
if (ms == null || !Number.isFinite(ms) || ms < 0) return null;
|
|
31
|
+
const s = Math.round(ms / 1000);
|
|
32
|
+
if (s < 60) return `${s}s`;
|
|
33
|
+
const m = Math.floor(s / 60);
|
|
34
|
+
if (m < 60) return `${m}m`;
|
|
35
|
+
const h = Math.floor(m / 60);
|
|
36
|
+
if (h < 24) return `${h}h ${m % 60}m`;
|
|
37
|
+
return `${Math.floor(h / 24)}d ${h % 24}h`;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* How long a session took. The time Claude Code itself timed its turns at, when it recorded any; otherwise the
|
|
42
|
+
* time from the first record to the last, which counts every pause as well.
|
|
43
|
+
* @returns { ms, kind: 'worked' | 'elapsed', note } or null
|
|
44
|
+
*/
|
|
45
|
+
export function timeOf(u) {
|
|
46
|
+
if (u?.workedMs) return { ms: u.workedMs, kind: 'worked', note: 'The turns Claude Code timed, added up; a turn still running is not in it yet' };
|
|
47
|
+
if (u?.durationMs != null) return { ms: u.durationMs, kind: 'elapsed', note: 'From the first record to the last, pauses included: no turn was timed' };
|
|
48
|
+
return null;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/** Why there is no estimate, when there is none. */
|
|
52
|
+
export function costNote(u) {
|
|
53
|
+
if (!u?.unpriced?.length) return COST_NOTE;
|
|
54
|
+
return `No estimate: the price table has no row for ${u.unpriced.join(', ')}`;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/** The navigator's one line: time · tokens · ~cost. Null when nothing was read. */
|
|
58
|
+
export function usageLine(u) {
|
|
59
|
+
if (!u) return null;
|
|
60
|
+
const t = timeOf(u);
|
|
61
|
+
const tokens = typeof u.tokens === 'number' ? u.tokens : u.tokens?.total;
|
|
62
|
+
return [t ? fmtSpan(t.ms) : null, tokens ? fmtCount(tokens) : null, tokens ? fmtCost(u.cost) : null].filter(Boolean).join(' · ') || null;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/** The summary box's rows: [label, value, note]. */
|
|
66
|
+
export function summaryRows(u) {
|
|
67
|
+
if (!u) return [];
|
|
68
|
+
const t = timeOf(u);
|
|
69
|
+
const k = u.tokens ?? {};
|
|
70
|
+
const rows = [];
|
|
71
|
+
if (t) rows.push([t.kind === 'worked' ? 'Working time' : 'First to last record', fmtSpan(t.ms), t.note]);
|
|
72
|
+
if (t?.kind === 'worked' && u.durationMs != null) rows.push(['First to last record', fmtSpan(u.durationMs), 'Pauses included']);
|
|
73
|
+
rows.push(['Tokens', fmtCount(k.total ?? 0),
|
|
74
|
+
`Agents included, each response counted once\ninput ${fmtCount(k.input ?? 0)} · output ${fmtCount(k.output ?? 0)} · cache write ${fmtCount(k.cacheWrite ?? 0)} · cache read ${fmtCount(k.cacheRead ?? 0)}`]);
|
|
75
|
+
rows.push(['Cost', k.total ? fmtCost(u.cost) : NO_COST, costNote(u)]);
|
|
76
|
+
return rows;
|
|
77
|
+
}
|
package/package.json
CHANGED