@cmgmyr/hive 1.9.1 → 1.10.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/README.md +1 -0
- package/claude-plugin/README.md +4 -0
- package/claude-plugin/crew/.claude-plugin/plugin.json +1 -0
- package/claude-plugin/crew/hooks/hooks.json +1 -0
- package/claude-plugin/crew/hooks/register.tsx +82 -0
- package/claude-plugin/crew/model.mjs +138 -0
- package/claude-plugin/crew/types/index.d.ts +18 -0
- package/dist/build-info.json +1 -1
- package/dist/cli.js +53 -21
- package/dist/crew.js +507 -0
- package/dist/dashboard.js +179 -156
- package/dist/db.js +1 -0
- package/dist/heldLabel.js +17 -0
- package/dist/leadState.js +8 -1
- package/dist/portfolio.js +8 -5
- package/dist/projectYml.js +14 -0
- package/dist/queenDashboard.js +30 -13
- package/dist/spawn.js +3 -3
- package/dist/tools/agents.js +33 -2
- package/dist/transcript.js +135 -2
- package/package.json +1 -1
package/dist/queenDashboard.js
CHANGED
|
@@ -2,6 +2,7 @@ import { existsSync, realpathSync } from "node:fs";
|
|
|
2
2
|
import { join, sep } from "node:path";
|
|
3
3
|
import { pathToFileURL } from "node:url";
|
|
4
4
|
import { db } from "./db.js";
|
|
5
|
+
import { SECTION_STYLE, SPRITE, chevron, renderBoardPadsTodos, sectionScript } from "./dashboard.js";
|
|
5
6
|
export const QUEEN_BRIEF_KEY = "queen:brief";
|
|
6
7
|
const LANE_ORDER = ["waiting_on_you", "stuck", "moving", "quiet"];
|
|
7
8
|
const LANE_RANK = { waiting_on_you: 0, stuck: 1, moving: 2, quiet: 3 };
|
|
@@ -158,6 +159,8 @@ function leadPill(p) {
|
|
|
158
159
|
return `<span class="status status-ok">lead up</span>${turnPill(p)}`;
|
|
159
160
|
case "dead_pane":
|
|
160
161
|
return '<span class="status status-fail">lead dead</span>';
|
|
162
|
+
case "dormant":
|
|
163
|
+
return '<span class="status status-off">lead dormant</span>';
|
|
161
164
|
case "unknown":
|
|
162
165
|
return '<span class="status status-off">lead unknown</span>';
|
|
163
166
|
default:
|
|
@@ -243,7 +246,7 @@ function quietList(list, asOf, href) {
|
|
|
243
246
|
return ('<section class="card"><ul class="quiet-list">' +
|
|
244
247
|
list
|
|
245
248
|
.map((p) => {
|
|
246
|
-
const sub = `${p.lead.state === "alive" ? (turnText(p) ? `lead up, ${turnText(p)}` : "lead up") : p.lead.state === "dead_pane" ? "lead dead" : p.lead.state === "unknown" ? "lead unknown" : "no lead"}, ${p.todos.open} open`;
|
|
249
|
+
const sub = `${p.lead.state === "alive" ? (turnText(p) ? `lead up, ${turnText(p)}` : "lead up") : p.lead.state === "dead_pane" ? "lead dead" : p.lead.state === "dormant" ? "lead dormant" : p.lead.state === "unknown" ? "lead unknown" : "no lead"}, ${p.todos.open} open`;
|
|
247
250
|
return (`<li data-project="${p.id}"><span>${projectLink(p, href)}` +
|
|
248
251
|
`<a class="sub" href="#row-${p.id}" data-jump="${p.id}">${esc(sub)}</a></span>` +
|
|
249
252
|
`<span class="age">${esc(age(p.last_activity_at, asOf))}</span></li>`);
|
|
@@ -338,7 +341,7 @@ function briefSection(state, report, href, queenHomeId) {
|
|
|
338
341
|
(picks ? `<ol>${picks}</ol>` : "") +
|
|
339
342
|
`${driftLine(brief, report, queenHomeId)}</section>`);
|
|
340
343
|
}
|
|
341
|
-
const CSS =
|
|
344
|
+
const CSS = `${SECTION_STYLE}
|
|
342
345
|
:root { color-scheme: light;
|
|
343
346
|
--font-mono: ui-monospace, "SF Mono", Menlo, Consolas, monospace;
|
|
344
347
|
--font-sans: system-ui, -apple-system, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
|
|
@@ -350,7 +353,7 @@ const CSS = `
|
|
|
350
353
|
--accent: #1f5fd0; --focus: #1f5fd0; --hover: #f1f4f8;
|
|
351
354
|
--shadow: 0 1px 2px rgba(19, 26, 38, 0.06), 0 2px 8px rgba(19, 26, 38, 0.05);
|
|
352
355
|
--card-border: transparent; --r-card: 14px; --r-ctl: 9px; --r-pill: 999px;
|
|
353
|
-
--ease-out: cubic-bezier(0.23, 1, 0.32, 1); }
|
|
356
|
+
--ease-out: cubic-bezier(0.23, 1, 0.32, 1); --header-offset: 5rem; }
|
|
354
357
|
@media (prefers-color-scheme: dark) {
|
|
355
358
|
:root:not([data-theme="light"]) { color-scheme: dark;
|
|
356
359
|
--bg: #101317; --panel: #171b21; --panel-sunken: #1c2129;
|
|
@@ -478,17 +481,22 @@ a.proj:hover { color: var(--accent); text-decoration: underline; text-underline-
|
|
|
478
481
|
a.sub:hover { color: var(--accent); text-decoration: underline; text-underline-offset: 2px; }
|
|
479
482
|
.turn { font-size: 0.75rem; color: var(--fg-muted); }
|
|
480
483
|
.turn-work { color: var(--live); }
|
|
481
|
-
.c-audit { margin-top: 1.5rem;
|
|
484
|
+
.c-audit { margin-top: 1.5rem; }
|
|
485
|
+
.c-audit[open] { padding-bottom: 0.85rem; }
|
|
486
|
+
details.card:not([open]) > .card-head { padding-bottom: 0.9rem; }
|
|
482
487
|
.c-audit .audit-list, .c-audit .brief-empty { padding: 0 1.1rem; }
|
|
483
488
|
.audit-list { list-style: none; margin: 0.5rem 0 0; padding: 0; display: grid; gap: 0.4rem; font-size: 0.8125rem; }
|
|
484
489
|
.audit-list li { display: flex; flex-wrap: wrap; gap: 0.15rem 0.75rem; align-items: baseline; }
|
|
485
490
|
.audit-op { font-weight: 600; }
|
|
486
491
|
.audit-sum { color: var(--fg-muted); min-width: 0; overflow-wrap: anywhere; }
|
|
487
492
|
.d-grid { margin-top: 1.5rem; }
|
|
493
|
+
.card-head > .chev { margin-left: auto; }
|
|
494
|
+
.home { margin-top: 1.5rem; }
|
|
495
|
+
.home-head { padding: 0 0.2rem 0.6rem; }
|
|
488
496
|
.d-grid .g-row:not(.g-head) { box-shadow: inset 3px 0 0 var(--lane, transparent); }
|
|
489
497
|
.d-grid .g-row { scroll-margin-top: 6rem; transition: background-color 600ms ease; }
|
|
490
498
|
.d-grid .g-row.is-flash { background: var(--live-bg); transition: none; }
|
|
491
|
-
.
|
|
499
|
+
.no-projects { margin-top: 1.25rem; padding: 1.25rem 1.1rem; color: var(--fg-muted); }
|
|
492
500
|
@media (prefers-reduced-motion: reduce) { * { transition: none !important; } }
|
|
493
501
|
`;
|
|
494
502
|
const SCRIPT = `(function () {
|
|
@@ -504,12 +512,17 @@ const SCRIPT = `(function () {
|
|
|
504
512
|
var row = document.getElementById("row-" + j.getAttribute("data-jump"));
|
|
505
513
|
if (!row) return;
|
|
506
514
|
e.preventDefault();
|
|
515
|
+
var box = row.closest("details");
|
|
516
|
+
if (box) box.open = true;
|
|
507
517
|
var still = window.matchMedia("(prefers-reduced-motion: reduce)").matches;
|
|
508
518
|
row.scrollIntoView({ behavior: still ? "auto" : "smooth", block: "center" });
|
|
509
519
|
row.classList.add("is-flash");
|
|
510
520
|
setTimeout(function () { row.classList.remove("is-flash"); }, 900);
|
|
511
521
|
});
|
|
512
522
|
})();`;
|
|
523
|
+
const SECTIONS_SCRIPT = `(function () {
|
|
524
|
+
${sectionScript("hive-queen-state")}
|
|
525
|
+
})();`;
|
|
513
526
|
const MARK = '<symbol id="mark" viewBox="0 0 32 32"><path d="M16 3.2 27 9.6v12.8L16 28.8 5 22.4V9.6Z" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linejoin="round"/><path d="M16 11 21 14v6l-5 3-5-3v-6Z" fill="currentColor" fill-opacity="0.22" stroke="currentColor" stroke-width="1.4" stroke-linejoin="round"/></symbol>';
|
|
514
527
|
export const QUEEN_GENERATED_MARKER = "data-generated";
|
|
515
528
|
function auditSection(audit, report) {
|
|
@@ -525,7 +538,7 @@ function auditSection(audit, report) {
|
|
|
525
538
|
.join("") +
|
|
526
539
|
"</ul>"
|
|
527
540
|
: '<p class="brief-empty">No queen actions recorded yet.</p>';
|
|
528
|
-
return `<
|
|
541
|
+
return `<details class="card c-audit" id="queen-recent-actions" open><summary class="card-head"><h2>Recent queen actions</h2><span class="card-sub">the newest writes the queen made into other projects</span>${chevron()}</summary>${body}</details>`;
|
|
529
542
|
}
|
|
530
543
|
export function renderQueenDashboard(report, briefState, href = (p) => dashboardHref(p.root), audit = [], queenHomeId = null) {
|
|
531
544
|
const asOf = report.as_of;
|
|
@@ -552,22 +565,26 @@ export function renderQueenDashboard(report, briefState, href = (p) => dashboard
|
|
|
552
565
|
`<span class="count">${report.totals.lanes[lane]}</span></h2></div>` +
|
|
553
566
|
`<p class="lane-rule">${LANE_RULE[lane]}</p>${body}</section>`);
|
|
554
567
|
}).join("");
|
|
568
|
+
const home = queenHomeId === null
|
|
569
|
+
? ""
|
|
570
|
+
: `<div class="home"><div class="home-head"><h2>Queen home project</h2></div>${renderBoardPadsTodos(queenHomeId)}</div>`;
|
|
555
571
|
const main = n === 0
|
|
556
|
-
? `${briefSection(briefState, report, href, queenHomeId)}<section class="card
|
|
572
|
+
? `${briefSection(briefState, report, href, queenHomeId)}<section class="card no-projects"><h2>No projects registered</h2><p>Run <code>hive lead</code> in a project folder to register it, and it will appear here.</p></section>`
|
|
557
573
|
: `${briefSection(briefState, report, href, queenHomeId)}<div class="lanes">${lanes}</div>` +
|
|
558
|
-
`<
|
|
559
|
-
|
|
574
|
+
`<details class="card d-grid" id="queen-every-project" open><summary class="card-head"><h2>Every project <span class="count">${n}</span></h2>` +
|
|
575
|
+
`<span class="card-sub">in lane order; a name opens that project’s dashboard</span>${chevron()}</summary>` +
|
|
560
576
|
`<div class="grid" role="table">${GRID_HEAD}${ordered.map((p) => gridRow(p, asOf, href)).join("")}</div>` +
|
|
561
|
-
'<p class="freshnote">Every row here is read from the store when the page is written. Only the picks are written by a model, which is why they carry a time.</p></
|
|
562
|
-
|
|
577
|
+
'<p class="freshnote">Every row here is read from the store when the page is written. Only the picks are written by a model, which is why they carry a time.</p></details>';
|
|
578
|
+
const page = main + home + (n === 0 ? "" : auditSection(audit, report));
|
|
563
579
|
return ('<!doctype html><html lang="en"><head><meta charset="utf-8">' +
|
|
564
580
|
'<meta name="viewport" content="width=device-width,initial-scale=1,viewport-fit=cover">' +
|
|
565
581
|
`<title>All projects</title><style>${CSS}</style></head><body>` +
|
|
582
|
+
SPRITE +
|
|
566
583
|
`<svg style="position:absolute;width:0;height:0" aria-hidden="true"><defs>${MARK}</defs></svg>` +
|
|
567
|
-
'<header class="topbar"><div class="wrap topbar-inner"><div class="brand">' +
|
|
584
|
+
'<header class="topbar" id="topbar"><div class="wrap topbar-inner"><div class="brand">' +
|
|
568
585
|
'<svg class="mark" role="img" aria-label="hive"><use href="#mark"/></svg><div><h1>All projects</h1>' +
|
|
569
586
|
`<p class="generated" ${QUEEN_GENERATED_MARKER}>Live rows as of ${esc(asOf)} UTC. ${n} project${n === 1 ? "" : "s"} on this machine.</p></div></div>` +
|
|
570
587
|
'<label class="live-toggle" title="Off stops this page reloading. The scheduler still rewrites the file.">' +
|
|
571
588
|
'<input type="checkbox" id="autoreload"> Reload every minute</label></div></header>' +
|
|
572
|
-
`<main class="wrap">${
|
|
589
|
+
`<main class="wrap">${page}</main><script>${SCRIPT}\n${SECTIONS_SCRIPT}</script></body></html>\n`);
|
|
573
590
|
}
|
package/dist/spawn.js
CHANGED
|
@@ -171,11 +171,11 @@ export function launchAgent(spec) {
|
|
|
171
171
|
let info;
|
|
172
172
|
try {
|
|
173
173
|
info = db
|
|
174
|
-
.prepare("INSERT INTO agents (project_id, name, command, cwd, kind, parent_actor_id, tmux_socket, session_id, codex_home, model, extra_args) " +
|
|
175
|
-
"VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)")
|
|
174
|
+
.prepare("INSERT INTO agents (project_id, name, command, cwd, kind, parent_actor_id, tmux_socket, session_id, codex_home, model, extra_args, todo_id) " +
|
|
175
|
+
"VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)")
|
|
176
176
|
.run(spec.projectId, spec.name, typeof spec.commandString === "string" ? spec.commandString : "", spec.cwd, spec.kind, spec.parentActor, socket, spec.sessionId ?? "", spec.codexHome ?? "", spec.model ?? null, spec.readOnly
|
|
177
177
|
? JSON.stringify({ args: spec.extraArgs ?? [], read_only: true })
|
|
178
|
-
: spec.extraArgs ? JSON.stringify(spec.extraArgs) : null);
|
|
178
|
+
: spec.extraArgs ? JSON.stringify(spec.extraArgs) : null, spec.todoId ?? null);
|
|
179
179
|
}
|
|
180
180
|
catch (e) {
|
|
181
181
|
throw asNameClash(e, spec.name);
|
package/dist/tools/agents.js
CHANGED
|
@@ -17,6 +17,7 @@ import { markGoneReported } from "../scheduler.js";
|
|
|
17
17
|
import { agentIdentityEnv, branchAt, closeAgentRow, currentOwnership, discardOrphanedPane, isReservedAgentName, isRunningLeadActor, killAgentPane, launchAgent, LEAD_KIND, PaneOwnershipLost, paneIdentity, parkAgentRow, reapCodexHomeForClosedAgent, requireStillOwned, releaseParkRow, renameAgent, resumeAgent, storedLaunchOptions, } from "../spawn.js";
|
|
18
18
|
import { COMMAND_KIND, runningCommandRow, stopLine, stopProcess, STOP_REASONS } from "../processes.js";
|
|
19
19
|
import { readContextTokens, readContextFill, resolveTranscriptDir } from "../transcript.js";
|
|
20
|
+
import { leadSessionEnded } from "../leadState.js";
|
|
20
21
|
import { capturePane, captureFinalScreen, DEFAULT_LAYOUT, cancelCopyMode, describePaneChoice, ensureAttached, findUnsafeControlChar, holdsHumanInput, liveTargets, observationFailed, observeRowOwnership, ownershipLiveness, paneCurrentCommand, paneInCopyMode, paneWindow, pollPaneReadiness, rowOwnership, sendText, sessionName, setRemainOnExit, sleep, TEXT_ALLOWED_CONTROL_CHARS, tmux, TmuxTimeoutError, WINDOW_LAYOUTS, } from "../tmux.js";
|
|
21
22
|
import { agentIdParam, agentNameParam, idParam, projectIdParam } from "./params.js";
|
|
22
23
|
import { deriveProvenance, lastLogEvent, lastPermissionMode, reportsAgentStateLog, } from "../stateProvenance.js";
|
|
@@ -357,7 +358,7 @@ function paneField(row, alive) {
|
|
|
357
358
|
return { pane: describePaneChoice(awaitingChoice) };
|
|
358
359
|
}
|
|
359
360
|
function agentSummary(row, snapshot) {
|
|
360
|
-
const alive = summaryLiveness(row, snapshot);
|
|
361
|
+
const { alive, dormant } = row.kind === LEAD_KIND ? leadSummaryLiveness(row, snapshot) : { alive: summaryLiveness(row, snapshot), dormant: null };
|
|
361
362
|
const { state, ...provenance } = deriveProvenance(row, alive);
|
|
362
363
|
return {
|
|
363
364
|
agent_id: row.id,
|
|
@@ -378,7 +379,9 @@ function agentSummary(row, snapshot) {
|
|
|
378
379
|
command: row.command,
|
|
379
380
|
cwd: row.cwd,
|
|
380
381
|
parent_actor_id: row.parent_actor_id,
|
|
382
|
+
todo_id: row.todo_id,
|
|
381
383
|
created_at: row.created_at,
|
|
384
|
+
...(dormant ? { dormant_since: dormant.ended_at } : {}),
|
|
382
385
|
};
|
|
383
386
|
}
|
|
384
387
|
export function contextCheckpointEnv(config) {
|
|
@@ -419,6 +422,9 @@ export function registerAgents(server) {
|
|
|
419
422
|
.describe("Spawn a known harness by name (e.g. \"codex\") instead of a raw command. Ignored when command is also given. Must be in the project's hive.yml agents: list (default: claude only)."),
|
|
420
423
|
extra_args: z.array(z.string()).optional().describe("Extra CLI arguments."),
|
|
421
424
|
read_only: z.boolean().optional().describe("Prevent local file writes and mutating shell commands while allowing Hive MCP tools. Only bare claude/codex executables. Extra args allow codex -c model_reasoning_effort=low|medium|high|xhigh, or claude --effort low|medium|high|xhigh|max; all other arguments are refused."),
|
|
425
|
+
todo_id: idParam
|
|
426
|
+
.optional()
|
|
427
|
+
.describe("The todo this worker is on, recorded as a link (a non-archived todo in the same project). Changes no todo status."),
|
|
422
428
|
cwd: z
|
|
423
429
|
.string()
|
|
424
430
|
.optional()
|
|
@@ -468,6 +474,20 @@ export function registerAgents(server) {
|
|
|
468
474
|
throw new Error(`cwd ${cwd} belongs to project "${cwdProject.name}" (id ${cwdProject.id}), but this spawn resolved to project "${project.name}" (id ${project.id}). ${remedy}`);
|
|
469
475
|
}
|
|
470
476
|
}
|
|
477
|
+
if (args.todo_id !== undefined) {
|
|
478
|
+
const linked = db
|
|
479
|
+
.prepare("SELECT archived_at FROM todos WHERE id = ? AND project_id = ?")
|
|
480
|
+
.get(args.todo_id, project.id);
|
|
481
|
+
if (!linked)
|
|
482
|
+
throw new Error(`todo_id ${args.todo_id} is not a todo in project "${project.name}" (id ${project.id}).`);
|
|
483
|
+
if (linked.archived_at != null)
|
|
484
|
+
throw new Error(`todo_id ${args.todo_id} is archived; unarchive it or link another todo.`);
|
|
485
|
+
}
|
|
486
|
+
const omittedTodoWarning = args.todo_id === undefined && db
|
|
487
|
+
.prepare("SELECT 1 FROM todos WHERE project_id = ? AND status = 'in_progress' AND archived_at IS NULL LIMIT 1")
|
|
488
|
+
.get(project.id)
|
|
489
|
+
? "todo_id was omitted while this project has an in-progress todo; the crew view may infer a link from this worker's comments or pad updates."
|
|
490
|
+
: undefined;
|
|
471
491
|
const name = args.name != null
|
|
472
492
|
? normalizeAgentName(args.name, "name")
|
|
473
493
|
: nextWorkerName(project.id);
|
|
@@ -566,6 +586,7 @@ export function registerAgents(server) {
|
|
|
566
586
|
model: args.model,
|
|
567
587
|
extraArgs: args.extra_args,
|
|
568
588
|
readOnly: args.read_only,
|
|
589
|
+
todoId: args.todo_id,
|
|
569
590
|
retainOnExit: harness.classifiesPaneScreen,
|
|
570
591
|
});
|
|
571
592
|
}
|
|
@@ -622,7 +643,7 @@ export function registerAgents(server) {
|
|
|
622
643
|
}
|
|
623
644
|
}
|
|
624
645
|
const codexInstructions = codexHomeKey ? codexInstructionsPhrase(codexInstructionLayers) : undefined;
|
|
625
|
-
|
|
646
|
+
const receipt = {
|
|
626
647
|
agent_id: agentId,
|
|
627
648
|
...(args.read_only ? { read_only: true } : {}),
|
|
628
649
|
actor_id: actorId,
|
|
@@ -671,6 +692,11 @@ export function registerAgents(server) {
|
|
|
671
692
|
"Wakes aimed at this worker are refused for the same reason.",
|
|
672
693
|
}),
|
|
673
694
|
};
|
|
695
|
+
if (omittedTodoWarning) {
|
|
696
|
+
const existingNote = typeof receipt.note === "string" ? receipt.note : "";
|
|
697
|
+
receipt.note = existingNote ? `${existingNote} ${omittedTodoWarning}` : omittedTodoWarning;
|
|
698
|
+
}
|
|
699
|
+
return receipt;
|
|
674
700
|
}));
|
|
675
701
|
server.registerTool("agent_resume", {
|
|
676
702
|
description: "Resume a CLOSED claude or codex worker from its recorded session id (claude --resume / codex resume): a fresh pane, the same actor_id, and the worker's full prior context. Addressed by name or agent_id among closed agents (agent_list(include_closed: true)). Send it its next instruction with agent_send once resumed - this tool does not.",
|
|
@@ -1580,3 +1606,8 @@ function readInputBox(classifier, target) {
|
|
|
1580
1606
|
const box = classifier.inputBoxState(target);
|
|
1581
1607
|
return box === null ? null : { box };
|
|
1582
1608
|
}
|
|
1609
|
+
function leadSummaryLiveness(row, snapshot) {
|
|
1610
|
+
const ownership = row.status === "running" ? rowOwnership(row, snapshot) : null;
|
|
1611
|
+
const alive = ownership === null ? false : ownershipLiveness(ownership);
|
|
1612
|
+
return { alive, dormant: ownership === null ? null : leadSessionEnded(row, ownership) };
|
|
1613
|
+
}
|
package/dist/transcript.js
CHANGED
|
@@ -35,6 +35,9 @@ function usageTokens(record) {
|
|
|
35
35
|
return tokenNumber(sum) && sum > 0 ? sum : null;
|
|
36
36
|
}
|
|
37
37
|
function tailRecords(path) {
|
|
38
|
+
return tailRead(path).records;
|
|
39
|
+
}
|
|
40
|
+
function tailRead(path) {
|
|
38
41
|
let fd;
|
|
39
42
|
try {
|
|
40
43
|
fd = openSync(path, "r");
|
|
@@ -54,10 +57,10 @@ function tailRecords(path) {
|
|
|
54
57
|
}
|
|
55
58
|
catch { }
|
|
56
59
|
}
|
|
57
|
-
return records;
|
|
60
|
+
return { records, clipped: start > 0 };
|
|
58
61
|
}
|
|
59
62
|
catch {
|
|
60
|
-
return [];
|
|
63
|
+
return { records: [], clipped: false };
|
|
61
64
|
}
|
|
62
65
|
finally {
|
|
63
66
|
if (fd !== undefined)
|
|
@@ -124,3 +127,133 @@ export function readContextTokens(cwd, sessionId, explicitPath = "") {
|
|
|
124
127
|
return null;
|
|
125
128
|
return claudeTokens(tailRecords(transcriptPath(cwd, sessionId, explicitPath)));
|
|
126
129
|
}
|
|
130
|
+
export const RECENT_TOOL_CALLS = 8;
|
|
131
|
+
export const SKILL_TOOL = "Skill";
|
|
132
|
+
export function lastComponent(name) {
|
|
133
|
+
return name.split(/__|[.:/]/).filter(Boolean).pop() ?? name;
|
|
134
|
+
}
|
|
135
|
+
function timestamp(record) {
|
|
136
|
+
return typeof record.timestamp === "string" && record.timestamp !== "" ? record.timestamp : null;
|
|
137
|
+
}
|
|
138
|
+
function reviewMarker(texts, at, skills) {
|
|
139
|
+
for (const skill of skills) {
|
|
140
|
+
const invocation = new RegExp(`^\\s*\\$${skill.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}(?![\\w-])`, "m");
|
|
141
|
+
if (texts.some((text) => typeof text === "string" && invocation.test(text))) {
|
|
142
|
+
return [{ name: SKILL_TOOL, input: { skill }, at }];
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
return [];
|
|
146
|
+
}
|
|
147
|
+
function claudeToolCalls(record, options) {
|
|
148
|
+
if (record.type !== "assistant")
|
|
149
|
+
return [];
|
|
150
|
+
const content = object(record.message)?.content;
|
|
151
|
+
if (!Array.isArray(content))
|
|
152
|
+
return [];
|
|
153
|
+
const at = timestamp(record);
|
|
154
|
+
const calls = [];
|
|
155
|
+
for (const part of content) {
|
|
156
|
+
const block = object(part);
|
|
157
|
+
if (!block)
|
|
158
|
+
continue;
|
|
159
|
+
if (block.type === "tool_use" && typeof block.name === "string") {
|
|
160
|
+
calls.push({ name: block.name, input: object(block.input) ?? {}, at });
|
|
161
|
+
}
|
|
162
|
+
else if (block.type === "text") {
|
|
163
|
+
calls.push(...reviewMarker([block.text], at, options.reviewSkills ?? []));
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
return calls;
|
|
167
|
+
}
|
|
168
|
+
function stringLiteral(text) {
|
|
169
|
+
const quote = text[0];
|
|
170
|
+
if (quote !== '"' && quote !== "'")
|
|
171
|
+
return null;
|
|
172
|
+
const json = quote === '"' ? text : `"${text.slice(1, -1).replace(/\\'/g, "'").replace(/(\\.)|"/g, (m, esc) => esc ?? '\\"')}"`;
|
|
173
|
+
try {
|
|
174
|
+
const value = JSON.parse(json);
|
|
175
|
+
return typeof value === "string" ? value : null;
|
|
176
|
+
}
|
|
177
|
+
catch {
|
|
178
|
+
return null;
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
function execInnerCalls(source, at) {
|
|
182
|
+
const marks = [...source.matchAll(/\btools\.([A-Za-z_]\w*)\s*\(/g)];
|
|
183
|
+
if (marks.length === 0)
|
|
184
|
+
return null;
|
|
185
|
+
const calls = [];
|
|
186
|
+
marks.forEach((mark, i) => {
|
|
187
|
+
const name = lastComponent(mark[1]);
|
|
188
|
+
if (name === "write_stdin")
|
|
189
|
+
return;
|
|
190
|
+
if (name === "exec_command") {
|
|
191
|
+
const span = source.slice(mark.index + mark[0].length, marks[i + 1]?.index ?? source.length);
|
|
192
|
+
const cmd = /["']?\bcmd["']?\s*:\s*("(?:[^"\\]|\\.)*"|'(?:[^'\\]|\\.)*')/.exec(span);
|
|
193
|
+
const command = cmd ? stringLiteral(cmd[1]) : null;
|
|
194
|
+
if (command !== null)
|
|
195
|
+
calls.push({ name: "Bash", input: { command }, at });
|
|
196
|
+
return;
|
|
197
|
+
}
|
|
198
|
+
calls.push({ name, input: {}, at });
|
|
199
|
+
});
|
|
200
|
+
return calls;
|
|
201
|
+
}
|
|
202
|
+
function codexToolCalls(record, options) {
|
|
203
|
+
const payload = object(record.payload);
|
|
204
|
+
if (record.type !== "response_item" || !payload)
|
|
205
|
+
return [];
|
|
206
|
+
const at = timestamp(record);
|
|
207
|
+
if (payload.type === "message") {
|
|
208
|
+
if (payload.role !== "assistant" || !Array.isArray(payload.content))
|
|
209
|
+
return [];
|
|
210
|
+
return reviewMarker(payload.content.map((part) => object(part)?.text), at, options.reviewSkills ?? []);
|
|
211
|
+
}
|
|
212
|
+
if (typeof payload.name !== "string")
|
|
213
|
+
return [];
|
|
214
|
+
const name = lastComponent(payload.name);
|
|
215
|
+
if (payload.type === "custom_tool_call") {
|
|
216
|
+
if (name === "exec" && typeof payload.input === "string") {
|
|
217
|
+
const inner = execInnerCalls(payload.input, at);
|
|
218
|
+
if (inner)
|
|
219
|
+
return inner;
|
|
220
|
+
}
|
|
221
|
+
return [{ name, input: typeof payload.input === "string" ? { input: payload.input } : {}, at }];
|
|
222
|
+
}
|
|
223
|
+
if (payload.type !== "function_call")
|
|
224
|
+
return [];
|
|
225
|
+
if (name === "write_stdin" || name === "sleep")
|
|
226
|
+
return [];
|
|
227
|
+
let args = {};
|
|
228
|
+
if (typeof payload.arguments === "string") {
|
|
229
|
+
try {
|
|
230
|
+
args = object(JSON.parse(payload.arguments)) ?? {};
|
|
231
|
+
}
|
|
232
|
+
catch { }
|
|
233
|
+
}
|
|
234
|
+
if (name === "exec_command")
|
|
235
|
+
return typeof args.cmd === "string" ? [{ name: "Bash", input: { command: args.cmd }, at }] : [];
|
|
236
|
+
return [{ name, input: args, at }];
|
|
237
|
+
}
|
|
238
|
+
export function readRecentToolCalls(kind, worker, options = {}) {
|
|
239
|
+
const path = worker.transcript_path || (kind === "claude" && worker.session_id
|
|
240
|
+
? join(transcriptDir(worker.cwd), `${worker.session_id}.jsonl`) : "");
|
|
241
|
+
if (!path)
|
|
242
|
+
return { calls: [], truncated: false };
|
|
243
|
+
const { records, clipped } = tailRead(path);
|
|
244
|
+
const newestFirst = [];
|
|
245
|
+
let capped = false;
|
|
246
|
+
for (const record of records) {
|
|
247
|
+
const calls = (kind === "claude" ? claudeToolCalls : codexToolCalls)(record, options);
|
|
248
|
+
for (let i = calls.length - 1; i >= 0; i--) {
|
|
249
|
+
if (newestFirst.length === RECENT_TOOL_CALLS) {
|
|
250
|
+
capped = true;
|
|
251
|
+
break;
|
|
252
|
+
}
|
|
253
|
+
newestFirst.push(calls[i]);
|
|
254
|
+
}
|
|
255
|
+
if (capped)
|
|
256
|
+
break;
|
|
257
|
+
}
|
|
258
|
+
return { calls: newestFirst.reverse(), truncated: clipped || capped };
|
|
259
|
+
}
|
package/package.json
CHANGED