@mandipadk7/kavi 0.1.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/dist/main.js ADDED
@@ -0,0 +1,667 @@
1
+ #!/usr/bin/env node
2
+ import fs from "node:fs/promises";
3
+ import process from "node:process";
4
+ import { fileURLToPath } from "node:url";
5
+ import { createApprovalRequest, describeToolUse, findApprovalRule, listApprovalRequests, resolveApprovalRequest, waitForApprovalDecision } from "./approvals.js";
6
+ import { appendCommand } from "./command-queue.js";
7
+ import { ensureHomeConfig, ensureProjectScaffold, loadConfig } from "./config.js";
8
+ import { KaviDaemon } from "./daemon.js";
9
+ import { runDoctor } from "./doctor.js";
10
+ import { writeJson } from "./fs.js";
11
+ import { createGitignoreEntries, detectRepoRoot, ensureWorktrees, getHeadCommit, landBranches, resolveTargetBranch } from "./git.js";
12
+ import { buildSessionId, resolveAppPaths } from "./paths.js";
13
+ import { isProcessAlive, spawnDetachedNode } from "./process.js";
14
+ import { resolveSessionRuntime } from "./runtime.js";
15
+ import { routePrompt } from "./router.js";
16
+ import { createSessionRecord, loadSessionRecord, readRecentEvents, recordEvent, sessionExists, sessionHeartbeatAgeMs } from "./session.js";
17
+ import { listTaskArtifacts, loadTaskArtifact } from "./task-artifacts.js";
18
+ import { attachTui } from "./tui.js";
19
+ const HEARTBEAT_STALE_MS = 10_000;
20
+ const CLAUDE_AUTO_ALLOW_TOOLS = new Set([
21
+ "Read",
22
+ "Glob",
23
+ "Grep",
24
+ "LS"
25
+ ]);
26
+ function getFlag(args, name) {
27
+ const index = args.indexOf(name);
28
+ if (index === -1) {
29
+ return null;
30
+ }
31
+ return args[index + 1] ?? null;
32
+ }
33
+ function getGoal(args) {
34
+ const explicit = getFlag(args, "--goal");
35
+ if (explicit) {
36
+ return explicit;
37
+ }
38
+ const filtered = args.filter((arg, index)=>{
39
+ if (arg === "--goal" || args[index - 1] === "--goal") {
40
+ return false;
41
+ }
42
+ return !arg.startsWith("--");
43
+ });
44
+ return filtered.length > 0 ? filtered.join(" ") : null;
45
+ }
46
+ async function readStdinText() {
47
+ if (process.stdin.isTTY) {
48
+ return "";
49
+ }
50
+ let content = "";
51
+ process.stdin.setEncoding("utf8");
52
+ for await (const chunk of process.stdin){
53
+ content += chunk;
54
+ }
55
+ return content;
56
+ }
57
+ function renderUsage() {
58
+ return [
59
+ "Usage:",
60
+ " kavi init [--home]",
61
+ " kavi doctor [--json]",
62
+ " kavi start [--goal \"...\"]",
63
+ " kavi open [--goal \"...\"]",
64
+ " kavi resume",
65
+ " kavi status [--json]",
66
+ " kavi paths [--json]",
67
+ " kavi task [--agent codex|claude|auto] <prompt>",
68
+ " kavi tasks [--json]",
69
+ " kavi task-output <task-id|latest> [--json]",
70
+ " kavi approvals [--json] [--all]",
71
+ " kavi approve <request-id|latest> [--remember]",
72
+ " kavi deny <request-id|latest> [--remember]",
73
+ " kavi events [--limit N]",
74
+ " kavi stop",
75
+ " kavi land",
76
+ " kavi help"
77
+ ].join("\n");
78
+ }
79
+ function isSessionLive(session) {
80
+ if (session.status !== "running") {
81
+ return false;
82
+ }
83
+ const heartbeatAgeMs = sessionHeartbeatAgeMs(session);
84
+ const heartbeatFresh = heartbeatAgeMs !== null && heartbeatAgeMs < HEARTBEAT_STALE_MS;
85
+ const pidAlive = isProcessAlive(session.daemonPid);
86
+ if (heartbeatFresh) {
87
+ return true;
88
+ }
89
+ return pidAlive;
90
+ }
91
+ async function waitForSession(paths, expectedState = "running") {
92
+ const timeoutMs = 10_000;
93
+ const startedAt = Date.now();
94
+ while(Date.now() - startedAt < timeoutMs){
95
+ try {
96
+ if (await sessionExists(paths)) {
97
+ const session = await loadSessionRecord(paths);
98
+ if (expectedState === "running" && isSessionLive(session)) {
99
+ return;
100
+ }
101
+ if (expectedState === "stopped" && session.status === "stopped") {
102
+ return;
103
+ }
104
+ }
105
+ } catch {}
106
+ await new Promise((resolve)=>setTimeout(resolve, 200));
107
+ }
108
+ throw new Error(`Timed out waiting for session state ${expectedState} in ${paths.stateFile}.`);
109
+ }
110
+ async function commandInit(cwd, args) {
111
+ const repoRoot = await detectRepoRoot(cwd);
112
+ const paths = resolveAppPaths(repoRoot);
113
+ await ensureProjectScaffold(paths);
114
+ await createGitignoreEntries(repoRoot);
115
+ if (args.includes("--home")) {
116
+ await ensureHomeConfig(paths);
117
+ console.log(`Initialized user-local Kavi config in ${paths.homeConfigFile}`);
118
+ }
119
+ console.log(`Initialized Kavi project scaffold in ${paths.kaviDir}`);
120
+ }
121
+ async function commandDoctor(cwd, args) {
122
+ const repoRoot = await detectRepoRoot(cwd);
123
+ const paths = resolveAppPaths(repoRoot);
124
+ await ensureProjectScaffold(paths);
125
+ const checks = await runDoctor(repoRoot, paths);
126
+ if (args.includes("--json")) {
127
+ console.log(JSON.stringify(checks, null, 2));
128
+ process.exitCode = checks.some((check)=>!check.ok) ? 1 : 0;
129
+ return;
130
+ }
131
+ let failed = false;
132
+ for (const check of checks){
133
+ if (!check.ok) {
134
+ failed = true;
135
+ }
136
+ console.log(`${check.ok ? "OK" : "FAIL"} ${check.name}: ${check.detail}`);
137
+ }
138
+ process.exitCode = failed ? 1 : 0;
139
+ }
140
+ async function startOrAttachSession(cwd, goal) {
141
+ const repoRoot = await detectRepoRoot(cwd);
142
+ const paths = resolveAppPaths(repoRoot);
143
+ await ensureProjectScaffold(paths);
144
+ await createGitignoreEntries(repoRoot);
145
+ await ensureHomeConfig(paths);
146
+ if (await sessionExists(paths)) {
147
+ try {
148
+ const session = await loadSessionRecord(paths);
149
+ if (isSessionLive(session)) {
150
+ if (goal) {
151
+ await appendCommand(paths, "kickoff", {
152
+ prompt: goal
153
+ });
154
+ }
155
+ return session.socketPath;
156
+ }
157
+ await recordEvent(paths, session.id, "daemon.stale_detected", {
158
+ daemonPid: session.daemonPid,
159
+ daemonHeartbeatAt: session.daemonHeartbeatAt
160
+ });
161
+ } catch {}
162
+ }
163
+ const config = await loadConfig(paths);
164
+ const runtime = await resolveSessionRuntime(paths);
165
+ const baseCommit = await getHeadCommit(repoRoot);
166
+ const sessionId = buildSessionId();
167
+ const rpcEndpoint = "file://session-state";
168
+ await fs.writeFile(paths.commandsFile, "", "utf8");
169
+ const worktrees = await ensureWorktrees(repoRoot, paths, sessionId, config, baseCommit);
170
+ await createSessionRecord(paths, config, runtime, sessionId, baseCommit, worktrees, goal, rpcEndpoint);
171
+ const pid = spawnDetachedNode(runtime.nodeExecutable, [
172
+ fileURLToPath(import.meta.url),
173
+ "__daemon",
174
+ "--repo-root",
175
+ repoRoot
176
+ ], repoRoot);
177
+ const session = await loadSessionRecord(paths);
178
+ session.daemonPid = pid;
179
+ await writeJson(paths.stateFile, session);
180
+ await waitForSession(paths);
181
+ return rpcEndpoint;
182
+ }
183
+ async function requireSession(cwd) {
184
+ const repoRoot = await detectRepoRoot(cwd);
185
+ const paths = resolveAppPaths(repoRoot);
186
+ if (!await sessionExists(paths)) {
187
+ throw new Error("No Kavi session found for this repository.");
188
+ }
189
+ return {
190
+ repoRoot,
191
+ paths
192
+ };
193
+ }
194
+ async function commandOpen(cwd, args) {
195
+ const goal = getGoal(args);
196
+ await startOrAttachSession(cwd, goal);
197
+ const repoRoot = await detectRepoRoot(cwd);
198
+ const paths = resolveAppPaths(repoRoot);
199
+ await attachTui(paths);
200
+ }
201
+ async function commandResume(cwd) {
202
+ const { paths } = await requireSession(cwd);
203
+ await waitForSession(paths);
204
+ await attachTui(paths);
205
+ }
206
+ async function commandStart(cwd, args) {
207
+ const goal = getGoal(args);
208
+ const repoRoot = await detectRepoRoot(cwd);
209
+ const socketPath = await startOrAttachSession(cwd, goal);
210
+ const paths = resolveAppPaths(repoRoot);
211
+ const session = await loadSessionRecord(paths);
212
+ console.log(`Started Kavi session ${session.id}`);
213
+ console.log(`Repo: ${repoRoot}`);
214
+ console.log(`Control: ${socketPath}`);
215
+ console.log(`Runtime: node=${session.runtime.nodeExecutable} codex=${session.runtime.codexExecutable} claude=${session.runtime.claudeExecutable}`);
216
+ for (const worktree of session.worktrees){
217
+ console.log(`- ${worktree.agent}: ${worktree.path}`);
218
+ }
219
+ }
220
+ async function commandStatus(cwd, args) {
221
+ const { paths } = await requireSession(cwd);
222
+ const session = await loadSessionRecord(paths);
223
+ const pendingApprovals = await listApprovalRequests(paths);
224
+ const heartbeatAgeMs = sessionHeartbeatAgeMs(session);
225
+ const payload = {
226
+ id: session.id,
227
+ status: session.status,
228
+ repoRoot: session.repoRoot,
229
+ goal: session.goal,
230
+ daemonPid: session.daemonPid,
231
+ daemonHeartbeatAt: session.daemonHeartbeatAt,
232
+ daemonHealthy: isSessionLive(session),
233
+ heartbeatAgeMs,
234
+ runtime: session.runtime,
235
+ taskCounts: {
236
+ total: session.tasks.length,
237
+ pending: session.tasks.filter((task)=>task.status === "pending").length,
238
+ running: session.tasks.filter((task)=>task.status === "running").length,
239
+ blocked: session.tasks.filter((task)=>task.status === "blocked").length,
240
+ completed: session.tasks.filter((task)=>task.status === "completed").length,
241
+ failed: session.tasks.filter((task)=>task.status === "failed").length
242
+ },
243
+ approvalCounts: {
244
+ pending: pendingApprovals.length
245
+ },
246
+ worktrees: session.worktrees
247
+ };
248
+ if (args.includes("--json")) {
249
+ console.log(JSON.stringify(payload, null, 2));
250
+ return;
251
+ }
252
+ console.log(`Session: ${payload.id}`);
253
+ console.log(`Status: ${payload.status}${payload.daemonHealthy ? " (healthy)" : " (stale or stopped)"}`);
254
+ console.log(`Repo: ${payload.repoRoot}`);
255
+ console.log(`Goal: ${payload.goal ?? "-"}`);
256
+ console.log(`Daemon PID: ${payload.daemonPid ?? "-"}`);
257
+ console.log(`Heartbeat: ${payload.daemonHeartbeatAt ?? "-"}${heartbeatAgeMs === null ? "" : ` (${heartbeatAgeMs} ms ago)`}`);
258
+ console.log(`Runtime: node=${payload.runtime.nodeExecutable} codex=${payload.runtime.codexExecutable} claude=${payload.runtime.claudeExecutable}`);
259
+ console.log(`Tasks: total=${payload.taskCounts.total} pending=${payload.taskCounts.pending} running=${payload.taskCounts.running} blocked=${payload.taskCounts.blocked} completed=${payload.taskCounts.completed} failed=${payload.taskCounts.failed}`);
260
+ console.log(`Approvals: pending=${payload.approvalCounts.pending}`);
261
+ for (const worktree of payload.worktrees){
262
+ console.log(`- ${worktree.agent}: ${worktree.path}`);
263
+ }
264
+ }
265
+ async function commandPaths(cwd, args) {
266
+ const repoRoot = await detectRepoRoot(cwd);
267
+ const paths = resolveAppPaths(repoRoot);
268
+ const runtime = await resolveSessionRuntime(paths);
269
+ const payload = {
270
+ repoRoot,
271
+ kaviDir: paths.kaviDir,
272
+ configFile: paths.configFile,
273
+ homeConfigFile: paths.homeConfigFile,
274
+ homeStateDir: paths.homeStateDir,
275
+ worktreeRoot: paths.worktreeRoot,
276
+ integrationRoot: paths.integrationRoot,
277
+ stateFile: paths.stateFile,
278
+ eventsFile: paths.eventsFile,
279
+ approvalsFile: paths.approvalsFile,
280
+ commandsFile: paths.commandsFile,
281
+ runsDir: paths.runsDir,
282
+ claudeSettingsFile: paths.claudeSettingsFile,
283
+ homeApprovalRulesFile: paths.homeApprovalRulesFile,
284
+ runtime
285
+ };
286
+ if (args.includes("--json")) {
287
+ console.log(JSON.stringify(payload, null, 2));
288
+ return;
289
+ }
290
+ console.log(`Repo: ${payload.repoRoot}`);
291
+ console.log(`Kavi dir: ${payload.kaviDir}`);
292
+ console.log(`Repo config: ${payload.configFile}`);
293
+ console.log(`Home config: ${payload.homeConfigFile}`);
294
+ console.log(`Home state: ${payload.homeStateDir}`);
295
+ console.log(`Worktrees: ${payload.worktreeRoot}`);
296
+ console.log(`Integration: ${payload.integrationRoot}`);
297
+ console.log(`State file: ${payload.stateFile}`);
298
+ console.log(`Events file: ${payload.eventsFile}`);
299
+ console.log(`Approvals file: ${payload.approvalsFile}`);
300
+ console.log(`Command queue: ${payload.commandsFile}`);
301
+ console.log(`Task artifacts: ${payload.runsDir}`);
302
+ console.log(`Claude settings: ${payload.claudeSettingsFile}`);
303
+ console.log(`Approval rules: ${payload.homeApprovalRulesFile}`);
304
+ console.log(`Runtime: node=${runtime.nodeExecutable} codex=${runtime.codexExecutable} claude=${runtime.claudeExecutable}`);
305
+ }
306
+ async function commandTask(cwd, args) {
307
+ const { paths } = await requireSession(cwd);
308
+ const session = await loadSessionRecord(paths);
309
+ const requestedAgent = getFlag(args, "--agent");
310
+ const prompt = getGoal(args.filter((arg, index)=>arg !== "--agent" && args[index - 1] !== "--agent"));
311
+ if (!prompt) {
312
+ throw new Error("A task prompt is required. Example: kavi task --agent auto \"Build auth route\"");
313
+ }
314
+ const owner = requestedAgent === "codex" || requestedAgent === "claude" ? requestedAgent : routePrompt(prompt, session.config);
315
+ await appendCommand(paths, "enqueue", {
316
+ owner,
317
+ prompt
318
+ });
319
+ await recordEvent(paths, session.id, "task.cli_enqueued", {
320
+ owner,
321
+ prompt
322
+ });
323
+ console.log(`Queued task for ${owner}: ${prompt}`);
324
+ }
325
+ async function commandTasks(cwd, args) {
326
+ const { paths } = await requireSession(cwd);
327
+ const session = await loadSessionRecord(paths);
328
+ const artifacts = await listTaskArtifacts(paths);
329
+ const artifactMap = new Map(artifacts.map((artifact)=>[
330
+ artifact.taskId,
331
+ artifact
332
+ ]));
333
+ const payload = session.tasks.map((task)=>({
334
+ id: task.id,
335
+ title: task.title,
336
+ owner: task.owner,
337
+ status: task.status,
338
+ createdAt: task.createdAt,
339
+ updatedAt: task.updatedAt,
340
+ summary: task.summary,
341
+ hasArtifact: artifactMap.has(task.id)
342
+ }));
343
+ if (args.includes("--json")) {
344
+ console.log(JSON.stringify(payload, null, 2));
345
+ return;
346
+ }
347
+ for (const task of payload){
348
+ console.log(`${task.id} | ${task.owner} | ${task.status} | artifact=${task.hasArtifact ? "yes" : "no"}`);
349
+ console.log(` title: ${task.title}`);
350
+ console.log(` updated: ${task.updatedAt}`);
351
+ console.log(` summary: ${task.summary ?? "-"}`);
352
+ }
353
+ }
354
+ function resolveRequestedTaskId(args, knownTaskIds) {
355
+ const positional = args.filter((arg)=>!arg.startsWith("--"));
356
+ const requested = positional[0] ?? "latest";
357
+ if (requested !== "latest") {
358
+ return requested;
359
+ }
360
+ const latest = [
361
+ ...knownTaskIds
362
+ ].pop();
363
+ if (!latest) {
364
+ throw new Error("No tasks found for this session.");
365
+ }
366
+ return latest;
367
+ }
368
+ async function commandTaskOutput(cwd, args) {
369
+ const { paths } = await requireSession(cwd);
370
+ const session = await loadSessionRecord(paths);
371
+ const sortedTasks = [
372
+ ...session.tasks
373
+ ].sort((left, right)=>left.updatedAt.localeCompare(right.updatedAt));
374
+ const taskId = resolveRequestedTaskId(args, sortedTasks.map((task)=>task.id));
375
+ const artifact = await loadTaskArtifact(paths, taskId);
376
+ if (!artifact) {
377
+ throw new Error(`No task artifact found for ${taskId}.`);
378
+ }
379
+ if (args.includes("--json")) {
380
+ console.log(JSON.stringify(artifact, null, 2));
381
+ return;
382
+ }
383
+ console.log(`Task: ${artifact.taskId}`);
384
+ console.log(`Owner: ${artifact.owner}`);
385
+ console.log(`Status: ${artifact.status}`);
386
+ console.log(`Started: ${artifact.startedAt}`);
387
+ console.log(`Finished: ${artifact.finishedAt}`);
388
+ console.log(`Summary: ${artifact.summary ?? "-"}`);
389
+ console.log(`Error: ${artifact.error ?? "-"}`);
390
+ console.log("Envelope:");
391
+ console.log(JSON.stringify(artifact.envelope, null, 2));
392
+ console.log("Raw Output:");
393
+ console.log(artifact.rawOutput ?? "");
394
+ }
395
+ async function commandApprovals(cwd, args) {
396
+ const { paths } = await requireSession(cwd);
397
+ const requests = await listApprovalRequests(paths, {
398
+ includeResolved: args.includes("--all")
399
+ });
400
+ if (args.includes("--json")) {
401
+ console.log(JSON.stringify(requests, null, 2));
402
+ return;
403
+ }
404
+ if (requests.length === 0) {
405
+ console.log("No approval requests.");
406
+ return;
407
+ }
408
+ for (const request of requests){
409
+ console.log(`${request.id} | ${request.agent} | ${request.status} | ${request.summary}${request.remember ? " | remembered" : ""}`);
410
+ console.log(` created: ${request.createdAt}`);
411
+ console.log(` decision: ${request.decision ?? "-"}`);
412
+ }
413
+ }
414
+ function resolveApprovalRequestId(requests, requested) {
415
+ if (requested && requested !== "latest") {
416
+ return requested;
417
+ }
418
+ const latest = [
419
+ ...requests
420
+ ].filter((request)=>request.status === "pending").sort((left, right)=>left.createdAt.localeCompare(right.createdAt)).pop();
421
+ if (!latest) {
422
+ throw new Error("No pending approval requests.");
423
+ }
424
+ return latest.id;
425
+ }
426
+ async function commandResolveApproval(cwd, args, decision) {
427
+ const { paths } = await requireSession(cwd);
428
+ const requests = await listApprovalRequests(paths, {
429
+ includeResolved: true
430
+ });
431
+ const requestedId = args.find((arg)=>!arg.startsWith("--")) ?? "latest";
432
+ const requestId = resolveApprovalRequestId(requests, requestedId);
433
+ const remember = args.includes("--remember");
434
+ const request = await resolveApprovalRequest(paths, requestId, decision, remember);
435
+ const session = await loadSessionRecord(paths);
436
+ await recordEvent(paths, session.id, "approval.resolved", {
437
+ requestId: request.id,
438
+ decision,
439
+ remember,
440
+ agent: request.agent,
441
+ toolName: request.toolName
442
+ });
443
+ console.log(`${decision === "allow" ? "Approved" : "Denied"} ${request.id}: ${request.summary}${remember ? " (remembered)" : ""}`);
444
+ }
445
+ async function commandEvents(cwd, args) {
446
+ const { paths } = await requireSession(cwd);
447
+ const limitArg = getFlag(args, "--limit");
448
+ const limit = limitArg ? Number(limitArg) : 20;
449
+ const events = await readRecentEvents(paths, Number.isFinite(limit) ? limit : 20);
450
+ for (const event of events){
451
+ console.log(`${event.timestamp} ${event.type} ${JSON.stringify(event.payload)}`);
452
+ }
453
+ }
454
+ async function commandStop(cwd) {
455
+ const { paths } = await requireSession(cwd);
456
+ const session = await loadSessionRecord(paths);
457
+ if (!isSessionLive(session)) {
458
+ session.status = "stopped";
459
+ session.daemonHeartbeatAt = new Date().toISOString();
460
+ await writeJson(paths.stateFile, session);
461
+ console.log(`Marked stale Kavi session ${session.id} as stopped`);
462
+ return;
463
+ }
464
+ await appendCommand(paths, "shutdown", {});
465
+ await recordEvent(paths, session.id, "daemon.stop_requested", {});
466
+ await waitForSession(paths, "stopped");
467
+ console.log(`Stopped Kavi session ${session.id}`);
468
+ }
469
+ async function commandLand(cwd) {
470
+ const repoRoot = await detectRepoRoot(cwd);
471
+ const paths = resolveAppPaths(repoRoot);
472
+ const session = await loadSessionRecord(paths);
473
+ const targetBranch = await resolveTargetBranch(repoRoot, session.config.baseBranch);
474
+ const result = await landBranches(repoRoot, targetBranch, session.worktrees, session.config.validationCommand, session.id, paths.integrationRoot);
475
+ await recordEvent(paths, session.id, "land.completed", {
476
+ targetBranch,
477
+ integrationBranch: result.integrationBranch,
478
+ integrationPath: result.integrationPath,
479
+ snapshotCommits: result.snapshotCommits,
480
+ commands: result.commandsRun
481
+ });
482
+ console.log(`Landed branches into ${targetBranch}`);
483
+ console.log(`Integration branch: ${result.integrationBranch}`);
484
+ console.log(`Integration worktree: ${result.integrationPath}`);
485
+ for (const snapshot of result.snapshotCommits){
486
+ console.log(`Snapshot ${snapshot.agent}: ${snapshot.commit}${snapshot.createdCommit ? " (created)" : " (unchanged)"}`);
487
+ }
488
+ for (const command of result.commandsRun){
489
+ console.log(`- ${command}`);
490
+ }
491
+ }
492
+ async function commandDaemon(args) {
493
+ const repoRoot = getFlag(args, "--repo-root") ?? process.cwd();
494
+ const paths = resolveAppPaths(repoRoot);
495
+ const daemon = new KaviDaemon(paths);
496
+ await daemon.start();
497
+ }
498
+ async function commandHook(args) {
499
+ const repoRoot = getFlag(args, "--repo-root") ?? process.cwd();
500
+ const agent = getFlag(args, "--agent") ?? null;
501
+ const eventName = getFlag(args, "--event") ?? "Unknown";
502
+ const stdin = await readStdinText();
503
+ const payload = stdin.trim() ? JSON.parse(stdin) : {};
504
+ const paths = resolveAppPaths(repoRoot);
505
+ const session = await sessionExists(paths) ? await loadSessionRecord(paths) : null;
506
+ const hookPayload = {
507
+ event: eventName,
508
+ sessionId: session?.id ?? null,
509
+ agent,
510
+ payload
511
+ };
512
+ if (session && agent === "claude" && eventName === "PreToolUse") {
513
+ const descriptor = describeToolUse(payload);
514
+ if (CLAUDE_AUTO_ALLOW_TOOLS.has(descriptor.toolName)) {
515
+ await recordEvent(paths, session.id, "approval.auto_allowed", {
516
+ agent,
517
+ toolName: descriptor.toolName,
518
+ summary: descriptor.summary
519
+ });
520
+ console.log(JSON.stringify({
521
+ continue: true,
522
+ suppressOutput: true,
523
+ hookSpecificOutput: {
524
+ hookEventName: "PreToolUse",
525
+ permissionDecision: "allow",
526
+ permissionDecisionReason: `Kavi auto-allowed read-only tool: ${descriptor.summary}`
527
+ }
528
+ }));
529
+ return;
530
+ }
531
+ const rule = await findApprovalRule(paths, {
532
+ repoRoot: session.repoRoot,
533
+ agent,
534
+ toolName: descriptor.toolName,
535
+ matchKey: descriptor.matchKey
536
+ });
537
+ if (rule) {
538
+ await recordEvent(paths, session.id, "approval.auto_decided", {
539
+ agent,
540
+ toolName: descriptor.toolName,
541
+ decision: rule.decision,
542
+ summary: descriptor.summary
543
+ });
544
+ console.log(JSON.stringify({
545
+ continue: true,
546
+ suppressOutput: true,
547
+ hookSpecificOutput: {
548
+ hookEventName: "PreToolUse",
549
+ permissionDecision: rule.decision === "allow" ? "allow" : "deny",
550
+ permissionDecisionReason: `Kavi ${rule.decision} rule matched: ${descriptor.summary}`
551
+ }
552
+ }));
553
+ return;
554
+ }
555
+ const request = await createApprovalRequest(paths, {
556
+ sessionId: session.id,
557
+ repoRoot: session.repoRoot,
558
+ agent,
559
+ hookEvent: eventName,
560
+ payload
561
+ });
562
+ await recordEvent(paths, session.id, "approval.requested", {
563
+ requestId: request.id,
564
+ agent,
565
+ toolName: request.toolName,
566
+ summary: request.summary
567
+ });
568
+ const resolved = await waitForApprovalDecision(paths, request.id);
569
+ const approved = resolved?.status === "approved";
570
+ const denied = resolved?.status === "denied";
571
+ const timedOut = resolved?.status === "expired" || !resolved;
572
+ await recordEvent(paths, session.id, "approval.completed", {
573
+ requestId: request.id,
574
+ outcome: approved ? "approved" : denied ? "denied" : "expired"
575
+ });
576
+ console.log(JSON.stringify({
577
+ continue: true,
578
+ suppressOutput: true,
579
+ hookSpecificOutput: {
580
+ hookEventName: "PreToolUse",
581
+ permissionDecision: approved ? "allow" : "deny",
582
+ permissionDecisionReason: approved ? `Approved by Kavi: ${request.summary}` : timedOut ? `Kavi approval timed out: ${request.summary}` : `Denied by Kavi: ${request.summary}`
583
+ }
584
+ }));
585
+ return;
586
+ }
587
+ if (session) {
588
+ await recordEvent(paths, session.id, "claude.hook", hookPayload);
589
+ }
590
+ console.log(JSON.stringify({
591
+ continue: true
592
+ }));
593
+ }
594
+ async function main() {
595
+ const [command = "open", ...args] = process.argv.slice(2);
596
+ const cwd = process.cwd();
597
+ switch(command){
598
+ case "help":
599
+ case "--help":
600
+ case "-h":
601
+ console.log(renderUsage());
602
+ break;
603
+ case "init":
604
+ await commandInit(cwd, args);
605
+ break;
606
+ case "doctor":
607
+ await commandDoctor(cwd, args);
608
+ break;
609
+ case "start":
610
+ await commandStart(cwd, args);
611
+ break;
612
+ case "open":
613
+ await commandOpen(cwd, args);
614
+ break;
615
+ case "resume":
616
+ await commandResume(cwd);
617
+ break;
618
+ case "status":
619
+ await commandStatus(cwd, args);
620
+ break;
621
+ case "paths":
622
+ await commandPaths(cwd, args);
623
+ break;
624
+ case "task":
625
+ await commandTask(cwd, args);
626
+ break;
627
+ case "tasks":
628
+ await commandTasks(cwd, args);
629
+ break;
630
+ case "task-output":
631
+ await commandTaskOutput(cwd, args);
632
+ break;
633
+ case "approvals":
634
+ await commandApprovals(cwd, args);
635
+ break;
636
+ case "approve":
637
+ await commandResolveApproval(cwd, args, "allow");
638
+ break;
639
+ case "deny":
640
+ await commandResolveApproval(cwd, args, "deny");
641
+ break;
642
+ case "events":
643
+ await commandEvents(cwd, args);
644
+ break;
645
+ case "stop":
646
+ await commandStop(cwd);
647
+ break;
648
+ case "land":
649
+ await commandLand(cwd);
650
+ break;
651
+ case "__daemon":
652
+ await commandDaemon(args);
653
+ break;
654
+ case "__hook":
655
+ await commandHook(args);
656
+ break;
657
+ default:
658
+ throw new Error(`Unknown command: ${command}`);
659
+ }
660
+ }
661
+ main().catch((error)=>{
662
+ console.error(error instanceof Error ? error.message : String(error));
663
+ process.exit(1);
664
+ });
665
+
666
+
667
+ //# sourceURL=main.ts