@mandipadk7/kavi 0.1.1 → 0.1.2
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 +13 -4
- package/dist/adapters/claude.js +131 -8
- package/dist/adapters/shared.js +19 -3
- package/dist/daemon.js +367 -4
- package/dist/git.js +97 -0
- package/dist/main.js +159 -45
- package/dist/paths.js +1 -1
- package/dist/rpc.js +226 -0
- package/dist/task-artifacts.js +10 -2
- package/dist/tui.js +1653 -85
- package/package.json +4 -10
package/dist/main.js
CHANGED
|
@@ -12,6 +12,7 @@ import { writeJson } from "./fs.js";
|
|
|
12
12
|
import { createGitignoreEntries, detectRepoRoot, ensureWorktrees, findOverlappingWorktreePaths, getHeadCommit, landBranches, resolveTargetBranch } from "./git.js";
|
|
13
13
|
import { buildSessionId, resolveAppPaths } from "./paths.js";
|
|
14
14
|
import { isProcessAlive, spawnDetachedNode } from "./process.js";
|
|
15
|
+
import { pingRpc, readSnapshot, rpcEnqueueTask, rpcNotifyExternalUpdate, rpcKickoff, rpcRecentEvents, rpcResolveApproval, rpcShutdown, rpcTaskArtifact } from "./rpc.js";
|
|
15
16
|
import { resolveSessionRuntime } from "./runtime.js";
|
|
16
17
|
import { buildAdHocTask, extractPromptPathHints, routeTask } from "./router.js";
|
|
17
18
|
import { createSessionRecord, loadSessionRecord, readRecentEvents, recordEvent, sessionExists, sessionHeartbeatAgeMs } from "./session.js";
|
|
@@ -68,6 +69,8 @@ function renderUsage() {
|
|
|
68
69
|
" kavi task [--agent codex|claude|auto] <prompt>",
|
|
69
70
|
" kavi tasks [--json]",
|
|
70
71
|
" kavi task-output <task-id|latest> [--json]",
|
|
72
|
+
" kavi decisions [--json] [--limit N]",
|
|
73
|
+
" kavi claims [--json] [--all]",
|
|
71
74
|
" kavi approvals [--json] [--all]",
|
|
72
75
|
" kavi approve <request-id|latest> [--remember]",
|
|
73
76
|
" kavi deny <request-id|latest> [--remember]",
|
|
@@ -96,7 +99,7 @@ async function waitForSession(paths, expectedState = "running") {
|
|
|
96
99
|
try {
|
|
97
100
|
if (await sessionExists(paths)) {
|
|
98
101
|
const session = await loadSessionRecord(paths);
|
|
99
|
-
if (expectedState === "running" && isSessionLive(session)) {
|
|
102
|
+
if (expectedState === "running" && isSessionLive(session) && await pingRpc(paths)) {
|
|
100
103
|
return;
|
|
101
104
|
}
|
|
102
105
|
if (expectedState === "stopped" && session.status === "stopped") {
|
|
@@ -148,11 +151,9 @@ async function startOrAttachSession(cwd, goal) {
|
|
|
148
151
|
if (await sessionExists(paths)) {
|
|
149
152
|
try {
|
|
150
153
|
const session = await loadSessionRecord(paths);
|
|
151
|
-
if (isSessionLive(session)) {
|
|
154
|
+
if (isSessionLive(session) && await pingRpc(paths)) {
|
|
152
155
|
if (goal) {
|
|
153
|
-
await
|
|
154
|
-
prompt: goal
|
|
155
|
-
});
|
|
156
|
+
await rpcKickoff(paths, goal);
|
|
156
157
|
}
|
|
157
158
|
return session.socketPath;
|
|
158
159
|
}
|
|
@@ -166,7 +167,7 @@ async function startOrAttachSession(cwd, goal) {
|
|
|
166
167
|
const runtime = await resolveSessionRuntime(paths);
|
|
167
168
|
const baseCommit = await getHeadCommit(repoRoot);
|
|
168
169
|
const sessionId = buildSessionId();
|
|
169
|
-
const rpcEndpoint =
|
|
170
|
+
const rpcEndpoint = paths.socketPath;
|
|
170
171
|
await fs.writeFile(paths.commandsFile, "", "utf8");
|
|
171
172
|
const worktrees = await ensureWorktrees(repoRoot, paths, sessionId, config, baseCommit);
|
|
172
173
|
await createSessionRecord(paths, config, runtime, sessionId, baseCommit, worktrees, goal, rpcEndpoint);
|
|
@@ -210,6 +211,20 @@ async function requireSession(cwd) {
|
|
|
210
211
|
paths
|
|
211
212
|
};
|
|
212
213
|
}
|
|
214
|
+
async function tryRpcSnapshot(paths) {
|
|
215
|
+
if (!await pingRpc(paths)) {
|
|
216
|
+
return null;
|
|
217
|
+
}
|
|
218
|
+
return await readSnapshot(paths);
|
|
219
|
+
}
|
|
220
|
+
async function notifyOperatorSurface(paths, reason) {
|
|
221
|
+
if (!await pingRpc(paths)) {
|
|
222
|
+
return;
|
|
223
|
+
}
|
|
224
|
+
try {
|
|
225
|
+
await rpcNotifyExternalUpdate(paths, reason);
|
|
226
|
+
} catch {}
|
|
227
|
+
}
|
|
213
228
|
async function commandOpen(cwd, args) {
|
|
214
229
|
const goal = getGoal(args);
|
|
215
230
|
await startOrAttachSession(cwd, goal);
|
|
@@ -238,17 +253,20 @@ async function commandStart(cwd, args) {
|
|
|
238
253
|
}
|
|
239
254
|
async function commandStatus(cwd, args) {
|
|
240
255
|
const { paths } = await requireSession(cwd);
|
|
241
|
-
const
|
|
242
|
-
const
|
|
256
|
+
const rpcSnapshot = await tryRpcSnapshot(paths);
|
|
257
|
+
const session = rpcSnapshot?.session ?? await loadSessionRecord(paths);
|
|
258
|
+
const pendingApprovals = rpcSnapshot?.approvals.filter((item)=>item.status === "pending") ?? await listApprovalRequests(paths);
|
|
243
259
|
const heartbeatAgeMs = sessionHeartbeatAgeMs(session);
|
|
244
260
|
const payload = {
|
|
245
261
|
id: session.id,
|
|
246
262
|
status: session.status,
|
|
247
263
|
repoRoot: session.repoRoot,
|
|
264
|
+
socketPath: session.socketPath,
|
|
248
265
|
goal: session.goal,
|
|
249
266
|
daemonPid: session.daemonPid,
|
|
250
267
|
daemonHeartbeatAt: session.daemonHeartbeatAt,
|
|
251
268
|
daemonHealthy: isSessionLive(session),
|
|
269
|
+
rpcConnected: rpcSnapshot !== null,
|
|
252
270
|
heartbeatAgeMs,
|
|
253
271
|
runtime: session.runtime,
|
|
254
272
|
taskCounts: {
|
|
@@ -277,6 +295,7 @@ async function commandStatus(cwd, args) {
|
|
|
277
295
|
console.log(`Session: ${payload.id}`);
|
|
278
296
|
console.log(`Status: ${payload.status}${payload.daemonHealthy ? " (healthy)" : " (stale or stopped)"}`);
|
|
279
297
|
console.log(`Repo: ${payload.repoRoot}`);
|
|
298
|
+
console.log(`Control: ${payload.socketPath}${payload.rpcConnected ? " (connected)" : " (disconnected)"}`);
|
|
280
299
|
console.log(`Goal: ${payload.goal ?? "-"}`);
|
|
281
300
|
console.log(`Daemon PID: ${payload.daemonPid ?? "-"}`);
|
|
282
301
|
console.log(`Heartbeat: ${payload.daemonHeartbeatAt ?? "-"}${heartbeatAgeMs === null ? "" : ` (${heartbeatAgeMs} ms ago)`}`);
|
|
@@ -305,6 +324,7 @@ async function commandPaths(cwd, args) {
|
|
|
305
324
|
eventsFile: paths.eventsFile,
|
|
306
325
|
approvalsFile: paths.approvalsFile,
|
|
307
326
|
commandsFile: paths.commandsFile,
|
|
327
|
+
socketPath: paths.socketPath,
|
|
308
328
|
runsDir: paths.runsDir,
|
|
309
329
|
claudeSettingsFile: paths.claudeSettingsFile,
|
|
310
330
|
homeApprovalRulesFile: paths.homeApprovalRulesFile,
|
|
@@ -325,6 +345,7 @@ async function commandPaths(cwd, args) {
|
|
|
325
345
|
console.log(`Events file: ${payload.eventsFile}`);
|
|
326
346
|
console.log(`Approvals file: ${payload.approvalsFile}`);
|
|
327
347
|
console.log(`Command queue: ${payload.commandsFile}`);
|
|
348
|
+
console.log(`Control socket: ${payload.socketPath}`);
|
|
328
349
|
console.log(`Task artifacts: ${payload.runsDir}`);
|
|
329
350
|
console.log(`Claude settings: ${payload.claudeSettingsFile}`);
|
|
330
351
|
console.log(`Approval rules: ${payload.homeApprovalRulesFile}`);
|
|
@@ -332,7 +353,8 @@ async function commandPaths(cwd, args) {
|
|
|
332
353
|
}
|
|
333
354
|
async function commandTask(cwd, args) {
|
|
334
355
|
const { paths } = await requireSession(cwd);
|
|
335
|
-
const
|
|
356
|
+
const rpcSnapshot = await tryRpcSnapshot(paths);
|
|
357
|
+
const session = rpcSnapshot?.session ?? await loadSessionRecord(paths);
|
|
336
358
|
const requestedAgent = getFlag(args, "--agent");
|
|
337
359
|
const prompt = getGoal(args.filter((arg, index)=>arg !== "--agent" && args[index - 1] !== "--agent"));
|
|
338
360
|
if (!prompt) {
|
|
@@ -345,14 +367,25 @@ async function commandTask(cwd, args) {
|
|
|
345
367
|
reason: `User explicitly assigned the task to ${requestedAgent}.`,
|
|
346
368
|
claimedPaths: extractPromptPathHints(prompt)
|
|
347
369
|
} : await routeTask(prompt, session, paths);
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
370
|
+
if (rpcSnapshot) {
|
|
371
|
+
await rpcEnqueueTask(paths, {
|
|
372
|
+
owner: routeDecision.owner,
|
|
373
|
+
prompt,
|
|
374
|
+
routeReason: routeDecision.reason,
|
|
375
|
+
claimedPaths: routeDecision.claimedPaths,
|
|
376
|
+
routeStrategy: routeDecision.strategy,
|
|
377
|
+
routeConfidence: routeDecision.confidence
|
|
378
|
+
});
|
|
379
|
+
} else {
|
|
380
|
+
await appendCommand(paths, "enqueue", {
|
|
381
|
+
owner: routeDecision.owner,
|
|
382
|
+
prompt,
|
|
383
|
+
routeReason: routeDecision.reason,
|
|
384
|
+
claimedPaths: routeDecision.claimedPaths,
|
|
385
|
+
routeStrategy: routeDecision.strategy,
|
|
386
|
+
routeConfidence: routeDecision.confidence
|
|
387
|
+
});
|
|
388
|
+
}
|
|
356
389
|
await recordEvent(paths, session.id, "task.cli_enqueued", {
|
|
357
390
|
owner: routeDecision.owner,
|
|
358
391
|
prompt,
|
|
@@ -364,7 +397,8 @@ async function commandTask(cwd, args) {
|
|
|
364
397
|
}
|
|
365
398
|
async function commandTasks(cwd, args) {
|
|
366
399
|
const { paths } = await requireSession(cwd);
|
|
367
|
-
const
|
|
400
|
+
const rpcSnapshot = await tryRpcSnapshot(paths);
|
|
401
|
+
const session = rpcSnapshot?.session ?? await loadSessionRecord(paths);
|
|
368
402
|
const artifacts = await listTaskArtifacts(paths);
|
|
369
403
|
const artifactMap = new Map(artifacts.map((artifact)=>[
|
|
370
404
|
artifact.taskId,
|
|
@@ -411,12 +445,13 @@ function resolveRequestedTaskId(args, knownTaskIds) {
|
|
|
411
445
|
}
|
|
412
446
|
async function commandTaskOutput(cwd, args) {
|
|
413
447
|
const { paths } = await requireSession(cwd);
|
|
414
|
-
const
|
|
448
|
+
const rpcSnapshot = await tryRpcSnapshot(paths);
|
|
449
|
+
const session = rpcSnapshot?.session ?? await loadSessionRecord(paths);
|
|
415
450
|
const sortedTasks = [
|
|
416
451
|
...session.tasks
|
|
417
452
|
].sort((left, right)=>left.updatedAt.localeCompare(right.updatedAt));
|
|
418
453
|
const taskId = resolveRequestedTaskId(args, sortedTasks.map((task)=>task.id));
|
|
419
|
-
const artifact = await loadTaskArtifact(paths, taskId);
|
|
454
|
+
const artifact = rpcSnapshot ? await rpcTaskArtifact(paths, taskId) : await loadTaskArtifact(paths, taskId);
|
|
420
455
|
if (!artifact) {
|
|
421
456
|
throw new Error(`No task artifact found for ${taskId}.`);
|
|
422
457
|
}
|
|
@@ -430,15 +465,65 @@ async function commandTaskOutput(cwd, args) {
|
|
|
430
465
|
console.log(`Started: ${artifact.startedAt}`);
|
|
431
466
|
console.log(`Finished: ${artifact.finishedAt}`);
|
|
432
467
|
console.log(`Summary: ${artifact.summary ?? "-"}`);
|
|
468
|
+
console.log(`Route: ${artifact.routeReason ?? "-"}`);
|
|
469
|
+
console.log(`Claimed paths: ${artifact.claimedPaths.join(", ") || "-"}`);
|
|
433
470
|
console.log(`Error: ${artifact.error ?? "-"}`);
|
|
471
|
+
console.log("Decision Replay:");
|
|
472
|
+
for (const line of artifact.decisionReplay){
|
|
473
|
+
console.log(line);
|
|
474
|
+
}
|
|
434
475
|
console.log("Envelope:");
|
|
435
476
|
console.log(JSON.stringify(artifact.envelope, null, 2));
|
|
436
477
|
console.log("Raw Output:");
|
|
437
478
|
console.log(artifact.rawOutput ?? "");
|
|
438
479
|
}
|
|
480
|
+
async function commandDecisions(cwd, args) {
|
|
481
|
+
const { paths } = await requireSession(cwd);
|
|
482
|
+
const rpcSnapshot = await tryRpcSnapshot(paths);
|
|
483
|
+
const session = rpcSnapshot?.session ?? await loadSessionRecord(paths);
|
|
484
|
+
const limitArg = getFlag(args, "--limit");
|
|
485
|
+
const limit = limitArg ? Number(limitArg) : 20;
|
|
486
|
+
const decisions = [
|
|
487
|
+
...session.decisions
|
|
488
|
+
].sort((left, right)=>left.createdAt.localeCompare(right.createdAt)).slice(-Math.max(1, Number.isFinite(limit) ? limit : 20));
|
|
489
|
+
if (args.includes("--json")) {
|
|
490
|
+
console.log(JSON.stringify(decisions, null, 2));
|
|
491
|
+
return;
|
|
492
|
+
}
|
|
493
|
+
if (decisions.length === 0) {
|
|
494
|
+
console.log("No decisions recorded.");
|
|
495
|
+
return;
|
|
496
|
+
}
|
|
497
|
+
for (const decision of decisions){
|
|
498
|
+
console.log(`${decision.createdAt} | ${decision.kind} | ${decision.agent ?? "-"} | ${decision.summary}`);
|
|
499
|
+
console.log(` task: ${decision.taskId ?? "-"}`);
|
|
500
|
+
console.log(` detail: ${decision.detail}`);
|
|
501
|
+
}
|
|
502
|
+
}
|
|
503
|
+
async function commandClaims(cwd, args) {
|
|
504
|
+
const { paths } = await requireSession(cwd);
|
|
505
|
+
const rpcSnapshot = await tryRpcSnapshot(paths);
|
|
506
|
+
const session = rpcSnapshot?.session ?? await loadSessionRecord(paths);
|
|
507
|
+
const claims = args.includes("--all") ? session.pathClaims : session.pathClaims.filter((claim)=>claim.status === "active");
|
|
508
|
+
if (args.includes("--json")) {
|
|
509
|
+
console.log(JSON.stringify(claims, null, 2));
|
|
510
|
+
return;
|
|
511
|
+
}
|
|
512
|
+
if (claims.length === 0) {
|
|
513
|
+
console.log("No path claims recorded.");
|
|
514
|
+
return;
|
|
515
|
+
}
|
|
516
|
+
for (const claim of claims){
|
|
517
|
+
console.log(`${claim.id} | ${claim.agent} | ${claim.status} | ${claim.source} | ${claim.paths.join(", ") || "-"}`);
|
|
518
|
+
console.log(` task: ${claim.taskId}`);
|
|
519
|
+
console.log(` updated: ${claim.updatedAt}`);
|
|
520
|
+
console.log(` note: ${claim.note ?? "-"}`);
|
|
521
|
+
}
|
|
522
|
+
}
|
|
439
523
|
async function commandApprovals(cwd, args) {
|
|
440
524
|
const { paths } = await requireSession(cwd);
|
|
441
|
-
const
|
|
525
|
+
const rpcSnapshot = await tryRpcSnapshot(paths);
|
|
526
|
+
const requests = rpcSnapshot ? rpcSnapshot.approvals.filter((request)=>args.includes("--all") || request.status === "pending") : await listApprovalRequests(paths, {
|
|
442
527
|
includeResolved: args.includes("--all")
|
|
443
528
|
});
|
|
444
529
|
if (args.includes("--json")) {
|
|
@@ -469,40 +554,54 @@ function resolveApprovalRequestId(requests, requested) {
|
|
|
469
554
|
}
|
|
470
555
|
async function commandResolveApproval(cwd, args, decision) {
|
|
471
556
|
const { paths } = await requireSession(cwd);
|
|
472
|
-
const
|
|
557
|
+
const rpcSnapshot = await tryRpcSnapshot(paths);
|
|
558
|
+
const requests = rpcSnapshot?.approvals ?? await listApprovalRequests(paths, {
|
|
473
559
|
includeResolved: true
|
|
474
560
|
});
|
|
475
561
|
const requestedId = args.find((arg)=>!arg.startsWith("--")) ?? "latest";
|
|
476
562
|
const requestId = resolveApprovalRequestId(requests, requestedId);
|
|
477
563
|
const remember = args.includes("--remember");
|
|
478
|
-
const request =
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
564
|
+
const request = requests.find((item)=>item.id === requestId);
|
|
565
|
+
if (!request) {
|
|
566
|
+
throw new Error(`Approval request ${requestId} not found.`);
|
|
567
|
+
}
|
|
568
|
+
if (rpcSnapshot) {
|
|
569
|
+
await rpcResolveApproval(paths, {
|
|
570
|
+
requestId,
|
|
571
|
+
decision,
|
|
572
|
+
remember
|
|
573
|
+
});
|
|
574
|
+
} else {
|
|
575
|
+
const resolved = await resolveApprovalRequest(paths, requestId, decision, remember);
|
|
576
|
+
const session = await loadSessionRecord(paths);
|
|
577
|
+
addDecisionRecord(session, {
|
|
578
|
+
kind: "approval",
|
|
579
|
+
agent: resolved.agent,
|
|
580
|
+
summary: `${decision === "allow" ? "Approved" : "Denied"} ${resolved.toolName}`,
|
|
581
|
+
detail: resolved.summary,
|
|
582
|
+
metadata: {
|
|
583
|
+
requestId: resolved.id,
|
|
584
|
+
remember,
|
|
585
|
+
toolName: resolved.toolName
|
|
586
|
+
}
|
|
587
|
+
});
|
|
588
|
+
await saveSessionRecord(paths, session);
|
|
589
|
+
await recordEvent(paths, session.id, "approval.resolved", {
|
|
590
|
+
requestId: resolved.id,
|
|
591
|
+
decision,
|
|
487
592
|
remember,
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
await recordEvent(paths, session.id, "approval.resolved", {
|
|
493
|
-
requestId: request.id,
|
|
494
|
-
decision,
|
|
495
|
-
remember,
|
|
496
|
-
agent: request.agent,
|
|
497
|
-
toolName: request.toolName
|
|
498
|
-
});
|
|
593
|
+
agent: resolved.agent,
|
|
594
|
+
toolName: resolved.toolName
|
|
595
|
+
});
|
|
596
|
+
}
|
|
499
597
|
console.log(`${decision === "allow" ? "Approved" : "Denied"} ${request.id}: ${request.summary}${remember ? " (remembered)" : ""}`);
|
|
500
598
|
}
|
|
501
599
|
async function commandEvents(cwd, args) {
|
|
502
600
|
const { paths } = await requireSession(cwd);
|
|
503
601
|
const limitArg = getFlag(args, "--limit");
|
|
504
602
|
const limit = limitArg ? Number(limitArg) : 20;
|
|
505
|
-
const
|
|
603
|
+
const rpcSnapshot = await tryRpcSnapshot(paths);
|
|
604
|
+
const events = rpcSnapshot ? await rpcRecentEvents(paths, Number.isFinite(limit) ? limit : 20) : await readRecentEvents(paths, Number.isFinite(limit) ? limit : 20);
|
|
506
605
|
for (const event of events){
|
|
507
606
|
console.log(`${event.timestamp} ${event.type} ${JSON.stringify(event.payload)}`);
|
|
508
607
|
}
|
|
@@ -517,8 +616,12 @@ async function commandStop(cwd) {
|
|
|
517
616
|
console.log(`Marked stale Kavi session ${session.id} as stopped`);
|
|
518
617
|
return;
|
|
519
618
|
}
|
|
520
|
-
await
|
|
521
|
-
|
|
619
|
+
if (await pingRpc(paths)) {
|
|
620
|
+
await rpcShutdown(paths);
|
|
621
|
+
} else {
|
|
622
|
+
await appendCommand(paths, "shutdown", {});
|
|
623
|
+
await recordEvent(paths, session.id, "daemon.stop_requested", {});
|
|
624
|
+
}
|
|
522
625
|
await waitForSession(paths, "stopped");
|
|
523
626
|
console.log(`Stopped Kavi session ${session.id}`);
|
|
524
627
|
}
|
|
@@ -618,6 +721,7 @@ async function commandHook(args) {
|
|
|
618
721
|
toolName: descriptor.toolName,
|
|
619
722
|
summary: descriptor.summary
|
|
620
723
|
});
|
|
724
|
+
await notifyOperatorSurface(paths, "approval.auto_allowed");
|
|
621
725
|
console.log(JSON.stringify({
|
|
622
726
|
continue: true,
|
|
623
727
|
suppressOutput: true,
|
|
@@ -642,6 +746,7 @@ async function commandHook(args) {
|
|
|
642
746
|
decision: rule.decision,
|
|
643
747
|
summary: descriptor.summary
|
|
644
748
|
});
|
|
749
|
+
await notifyOperatorSurface(paths, "approval.auto_decided");
|
|
645
750
|
console.log(JSON.stringify({
|
|
646
751
|
continue: true,
|
|
647
752
|
suppressOutput: true,
|
|
@@ -666,6 +771,7 @@ async function commandHook(args) {
|
|
|
666
771
|
toolName: request.toolName,
|
|
667
772
|
summary: request.summary
|
|
668
773
|
});
|
|
774
|
+
await notifyOperatorSurface(paths, "approval.requested");
|
|
669
775
|
const resolved = await waitForApprovalDecision(paths, request.id);
|
|
670
776
|
const approved = resolved?.status === "approved";
|
|
671
777
|
const denied = resolved?.status === "denied";
|
|
@@ -674,6 +780,7 @@ async function commandHook(args) {
|
|
|
674
780
|
requestId: request.id,
|
|
675
781
|
outcome: approved ? "approved" : denied ? "denied" : "expired"
|
|
676
782
|
});
|
|
783
|
+
await notifyOperatorSurface(paths, "approval.completed");
|
|
677
784
|
console.log(JSON.stringify({
|
|
678
785
|
continue: true,
|
|
679
786
|
suppressOutput: true,
|
|
@@ -687,6 +794,7 @@ async function commandHook(args) {
|
|
|
687
794
|
}
|
|
688
795
|
if (session) {
|
|
689
796
|
await recordEvent(paths, session.id, "claude.hook", hookPayload);
|
|
797
|
+
await notifyOperatorSurface(paths, "claude.hook");
|
|
690
798
|
}
|
|
691
799
|
console.log(JSON.stringify({
|
|
692
800
|
continue: true
|
|
@@ -731,6 +839,12 @@ async function main() {
|
|
|
731
839
|
case "task-output":
|
|
732
840
|
await commandTaskOutput(cwd, args);
|
|
733
841
|
break;
|
|
842
|
+
case "decisions":
|
|
843
|
+
await commandDecisions(cwd, args);
|
|
844
|
+
break;
|
|
845
|
+
case "claims":
|
|
846
|
+
await commandClaims(cwd, args);
|
|
847
|
+
break;
|
|
734
848
|
case "approvals":
|
|
735
849
|
await commandApprovals(cwd, args);
|
|
736
850
|
break;
|
package/dist/paths.js
CHANGED
|
@@ -23,7 +23,7 @@ export function resolveAppPaths(repoRoot) {
|
|
|
23
23
|
approvalsFile: path.join(stateDir, "approvals.json"),
|
|
24
24
|
commandsFile: path.join(runtimeDir, "commands.jsonl"),
|
|
25
25
|
claudeSettingsFile: path.join(runtimeDir, "claude.settings.json"),
|
|
26
|
-
socketPath: path.join(
|
|
26
|
+
socketPath: path.join(homeStateDir, "sockets", `${safeRepoId}.sock`),
|
|
27
27
|
homeConfigDir,
|
|
28
28
|
homeConfigFile: path.join(homeConfigDir, "config.toml"),
|
|
29
29
|
homeApprovalRulesFile: path.join(homeConfigDir, "approval-rules.json"),
|
package/dist/rpc.js
ADDED
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
import net from "node:net";
|
|
2
|
+
const RPC_TIMEOUT_MS = 4_000;
|
|
3
|
+
function randomId() {
|
|
4
|
+
return `${Date.now()}-${Math.random().toString(16).slice(2)}`;
|
|
5
|
+
}
|
|
6
|
+
function asObject(value) {
|
|
7
|
+
return value && typeof value === "object" && !Array.isArray(value) ? value : {};
|
|
8
|
+
}
|
|
9
|
+
export async function sendRpcRequest(paths, method, params = {}) {
|
|
10
|
+
return await new Promise((resolve, reject)=>{
|
|
11
|
+
const socket = net.createConnection(paths.socketPath);
|
|
12
|
+
const requestId = randomId();
|
|
13
|
+
let buffer = "";
|
|
14
|
+
let settled = false;
|
|
15
|
+
const finish = (callback)=>{
|
|
16
|
+
if (settled) {
|
|
17
|
+
return;
|
|
18
|
+
}
|
|
19
|
+
settled = true;
|
|
20
|
+
socket.setTimeout(0);
|
|
21
|
+
socket.end();
|
|
22
|
+
callback();
|
|
23
|
+
};
|
|
24
|
+
socket.setEncoding("utf8");
|
|
25
|
+
socket.setTimeout(RPC_TIMEOUT_MS, ()=>{
|
|
26
|
+
finish(()=>reject(new Error(`RPC ${method} timed out after ${RPC_TIMEOUT_MS}ms.`)));
|
|
27
|
+
});
|
|
28
|
+
socket.on("connect", ()=>{
|
|
29
|
+
const payload = {
|
|
30
|
+
id: requestId,
|
|
31
|
+
method,
|
|
32
|
+
params
|
|
33
|
+
};
|
|
34
|
+
socket.write(`${JSON.stringify(payload)}\n`);
|
|
35
|
+
});
|
|
36
|
+
socket.on("data", (chunk)=>{
|
|
37
|
+
buffer += chunk;
|
|
38
|
+
while(true){
|
|
39
|
+
const newlineIndex = buffer.indexOf("\n");
|
|
40
|
+
if (newlineIndex === -1) {
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
const line = buffer.slice(0, newlineIndex).trim();
|
|
44
|
+
buffer = buffer.slice(newlineIndex + 1);
|
|
45
|
+
if (!line) {
|
|
46
|
+
continue;
|
|
47
|
+
}
|
|
48
|
+
let response;
|
|
49
|
+
try {
|
|
50
|
+
response = JSON.parse(line);
|
|
51
|
+
} catch (error) {
|
|
52
|
+
finish(()=>{
|
|
53
|
+
reject(new Error(error instanceof Error ? error.message : `Unable to parse RPC response for ${method}.`));
|
|
54
|
+
});
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
if (response.id !== requestId) {
|
|
58
|
+
continue;
|
|
59
|
+
}
|
|
60
|
+
if (response.error) {
|
|
61
|
+
finish(()=>reject(new Error(response.error?.message ?? `${method} failed.`)));
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
finish(()=>resolve(response.result));
|
|
65
|
+
return;
|
|
66
|
+
}
|
|
67
|
+
});
|
|
68
|
+
socket.on("error", (error)=>{
|
|
69
|
+
finish(()=>reject(error));
|
|
70
|
+
});
|
|
71
|
+
socket.on("end", ()=>{
|
|
72
|
+
if (!settled) {
|
|
73
|
+
finish(()=>reject(new Error(`Socket closed before completing ${method}.`)));
|
|
74
|
+
}
|
|
75
|
+
});
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
export async function pingRpc(paths) {
|
|
79
|
+
try {
|
|
80
|
+
await sendRpcRequest(paths, "ping");
|
|
81
|
+
return true;
|
|
82
|
+
} catch {
|
|
83
|
+
return false;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
export async function readSnapshot(paths) {
|
|
87
|
+
return await sendRpcRequest(paths, "snapshot");
|
|
88
|
+
}
|
|
89
|
+
export async function rpcKickoff(paths, prompt) {
|
|
90
|
+
await sendRpcRequest(paths, "kickoff", {
|
|
91
|
+
prompt
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
export async function rpcEnqueueTask(paths, params) {
|
|
95
|
+
await sendRpcRequest(paths, "enqueueTask", {
|
|
96
|
+
owner: params.owner,
|
|
97
|
+
prompt: params.prompt,
|
|
98
|
+
routeReason: params.routeReason,
|
|
99
|
+
claimedPaths: params.claimedPaths,
|
|
100
|
+
routeStrategy: params.routeStrategy,
|
|
101
|
+
routeConfidence: params.routeConfidence
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
export async function rpcResolveApproval(paths, params) {
|
|
105
|
+
await sendRpcRequest(paths, "resolveApproval", params);
|
|
106
|
+
}
|
|
107
|
+
export async function rpcShutdown(paths) {
|
|
108
|
+
await sendRpcRequest(paths, "shutdown");
|
|
109
|
+
}
|
|
110
|
+
export async function rpcNotifyExternalUpdate(paths, reason) {
|
|
111
|
+
await sendRpcRequest(paths, "notifyExternalUpdate", {
|
|
112
|
+
reason
|
|
113
|
+
});
|
|
114
|
+
}
|
|
115
|
+
export async function rpcTaskArtifact(paths, taskId) {
|
|
116
|
+
return await sendRpcRequest(paths, "taskArtifact", {
|
|
117
|
+
taskId
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
export async function rpcRecentEvents(paths, limit) {
|
|
121
|
+
return await sendRpcRequest(paths, "events", {
|
|
122
|
+
limit
|
|
123
|
+
});
|
|
124
|
+
}
|
|
125
|
+
export async function rpcWorktreeDiff(paths, agent, filePath) {
|
|
126
|
+
return await sendRpcRequest(paths, "worktreeDiff", {
|
|
127
|
+
agent,
|
|
128
|
+
filePath
|
|
129
|
+
});
|
|
130
|
+
}
|
|
131
|
+
export function subscribeSnapshotRpc(paths, handlers) {
|
|
132
|
+
const socket = net.createConnection(paths.socketPath);
|
|
133
|
+
const requestId = randomId();
|
|
134
|
+
let buffer = "";
|
|
135
|
+
let closed = false;
|
|
136
|
+
let connectedResolver = null;
|
|
137
|
+
let connectedRejecter = null;
|
|
138
|
+
const connected = new Promise((resolve, reject)=>{
|
|
139
|
+
connectedResolver = resolve;
|
|
140
|
+
connectedRejecter = reject;
|
|
141
|
+
});
|
|
142
|
+
const finishWithError = (error)=>{
|
|
143
|
+
handlers.onError?.(error);
|
|
144
|
+
if (connectedRejecter) {
|
|
145
|
+
connectedRejecter(error);
|
|
146
|
+
connectedRejecter = null;
|
|
147
|
+
connectedResolver = null;
|
|
148
|
+
}
|
|
149
|
+
};
|
|
150
|
+
socket.setEncoding("utf8");
|
|
151
|
+
socket.setTimeout(RPC_TIMEOUT_MS, ()=>{
|
|
152
|
+
finishWithError(new Error(`Subscription timed out after ${RPC_TIMEOUT_MS}ms.`));
|
|
153
|
+
socket.end();
|
|
154
|
+
});
|
|
155
|
+
socket.on("connect", ()=>{
|
|
156
|
+
const payload = {
|
|
157
|
+
id: requestId,
|
|
158
|
+
method: "subscribe"
|
|
159
|
+
};
|
|
160
|
+
socket.write(`${JSON.stringify(payload)}\n`);
|
|
161
|
+
});
|
|
162
|
+
socket.on("data", (chunk)=>{
|
|
163
|
+
buffer += chunk;
|
|
164
|
+
while(true){
|
|
165
|
+
const newlineIndex = buffer.indexOf("\n");
|
|
166
|
+
if (newlineIndex === -1) {
|
|
167
|
+
return;
|
|
168
|
+
}
|
|
169
|
+
const line = buffer.slice(0, newlineIndex).trim();
|
|
170
|
+
buffer = buffer.slice(newlineIndex + 1);
|
|
171
|
+
if (!line) {
|
|
172
|
+
continue;
|
|
173
|
+
}
|
|
174
|
+
let message;
|
|
175
|
+
try {
|
|
176
|
+
message = JSON.parse(line);
|
|
177
|
+
} catch (error) {
|
|
178
|
+
finishWithError(new Error(error instanceof Error ? error.message : "Unable to parse snapshot subscription payload."));
|
|
179
|
+
socket.end();
|
|
180
|
+
return;
|
|
181
|
+
}
|
|
182
|
+
if ("id" in message && message.id === requestId) {
|
|
183
|
+
if (message.error) {
|
|
184
|
+
finishWithError(new Error(message.error.message));
|
|
185
|
+
socket.end();
|
|
186
|
+
return;
|
|
187
|
+
}
|
|
188
|
+
socket.setTimeout(0);
|
|
189
|
+
connectedResolver?.();
|
|
190
|
+
connectedResolver = null;
|
|
191
|
+
connectedRejecter = null;
|
|
192
|
+
handlers.onSnapshot({
|
|
193
|
+
reason: "subscribe",
|
|
194
|
+
snapshot: message.result
|
|
195
|
+
});
|
|
196
|
+
continue;
|
|
197
|
+
}
|
|
198
|
+
if ("method" in message && message.method === "snapshot.updated") {
|
|
199
|
+
handlers.onSnapshot(message.params);
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
});
|
|
203
|
+
socket.on("error", (error)=>{
|
|
204
|
+
if (!closed) {
|
|
205
|
+
finishWithError(error instanceof Error ? error : new Error(String(error)));
|
|
206
|
+
}
|
|
207
|
+
});
|
|
208
|
+
socket.on("close", ()=>{
|
|
209
|
+
if (!closed) {
|
|
210
|
+
handlers.onDisconnect?.();
|
|
211
|
+
}
|
|
212
|
+
});
|
|
213
|
+
return {
|
|
214
|
+
connected,
|
|
215
|
+
close: ()=>{
|
|
216
|
+
closed = true;
|
|
217
|
+
socket.end();
|
|
218
|
+
}
|
|
219
|
+
};
|
|
220
|
+
}
|
|
221
|
+
export function parseRpcParams(value) {
|
|
222
|
+
return asObject(value);
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
|
|
226
|
+
//# sourceURL=rpc.ts
|
package/dist/task-artifacts.js
CHANGED
|
@@ -4,6 +4,14 @@ import { ensureDir, fileExists, readJson, writeJson } from "./fs.js";
|
|
|
4
4
|
function artifactPath(paths, taskId) {
|
|
5
5
|
return path.join(paths.runsDir, `${taskId}.json`);
|
|
6
6
|
}
|
|
7
|
+
function normalizeArtifact(artifact) {
|
|
8
|
+
return {
|
|
9
|
+
...artifact,
|
|
10
|
+
routeReason: typeof artifact.routeReason === "string" ? artifact.routeReason : null,
|
|
11
|
+
claimedPaths: Array.isArray(artifact.claimedPaths) ? artifact.claimedPaths.map((item)=>String(item)) : [],
|
|
12
|
+
decisionReplay: Array.isArray(artifact.decisionReplay) ? artifact.decisionReplay.map((item)=>String(item)) : []
|
|
13
|
+
};
|
|
14
|
+
}
|
|
7
15
|
export async function saveTaskArtifact(paths, artifact) {
|
|
8
16
|
await ensureDir(paths.runsDir);
|
|
9
17
|
await writeJson(artifactPath(paths, artifact.taskId), artifact);
|
|
@@ -13,7 +21,7 @@ export async function loadTaskArtifact(paths, taskId) {
|
|
|
13
21
|
if (!await fileExists(filePath)) {
|
|
14
22
|
return null;
|
|
15
23
|
}
|
|
16
|
-
return readJson(filePath);
|
|
24
|
+
return normalizeArtifact(await readJson(filePath));
|
|
17
25
|
}
|
|
18
26
|
export async function listTaskArtifacts(paths) {
|
|
19
27
|
if (!await fileExists(paths.runsDir)) {
|
|
@@ -28,7 +36,7 @@ export async function listTaskArtifacts(paths) {
|
|
|
28
36
|
continue;
|
|
29
37
|
}
|
|
30
38
|
const artifact = await readJson(path.join(paths.runsDir, entry.name));
|
|
31
|
-
artifacts.push(artifact);
|
|
39
|
+
artifacts.push(normalizeArtifact(artifact));
|
|
32
40
|
}
|
|
33
41
|
return artifacts.sort((left, right)=>left.finishedAt.localeCompare(right.finishedAt));
|
|
34
42
|
}
|