@mandipadk7/kavi 0.1.1 → 0.1.3
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 +21 -5
- package/dist/adapters/claude.js +131 -8
- package/dist/adapters/shared.js +19 -3
- package/dist/daemon.js +689 -4
- package/dist/git.js +198 -2
- package/dist/main.js +327 -68
- package/dist/paths.js +1 -1
- package/dist/reviews.js +159 -0
- package/dist/rpc.js +262 -0
- package/dist/session.js +25 -0
- package/dist/task-artifacts.js +35 -2
- package/dist/tui.js +1960 -83
- package/package.json +4 -10
package/README.md
CHANGED
|
@@ -3,17 +3,21 @@
|
|
|
3
3
|
Kavi is a local terminal control plane for managed Codex and Claude collaboration.
|
|
4
4
|
|
|
5
5
|
Current capabilities:
|
|
6
|
-
- `kavi init`: create repo-local `.kavi` config, prompt files, and
|
|
6
|
+
- `kavi init`: create repo-local `.kavi` config, prompt files, ignore rules, and bootstrap git if the folder is not already a repository.
|
|
7
7
|
- `kavi init --home`: also scaffold the user-local config file used for binary overrides.
|
|
8
|
+
- `kavi init --no-commit`: skip the bootstrap commit and let `kavi open` or `kavi start` create the first base commit later.
|
|
8
9
|
- `kavi doctor`: verify Node, Codex, Claude, git worktree support, and local readiness.
|
|
9
10
|
- `kavi start`: start a managed session without attaching the TUI.
|
|
10
|
-
- `kavi open`: create a managed session with separate Codex and Claude worktrees and open the
|
|
11
|
-
- `kavi resume`: reopen the
|
|
11
|
+
- `kavi open`: create a managed session with separate Codex and Claude worktrees and open the full-screen operator console, even from an empty folder or a repo with no `HEAD` yet.
|
|
12
|
+
- `kavi resume`: reopen the operator console for the current repo session.
|
|
12
13
|
- `kavi status`: inspect session health and task counts from any terminal.
|
|
13
14
|
- `kavi paths`: inspect resolved repo-local, user-local, worktree, and runtime paths.
|
|
14
15
|
- `kavi task`: enqueue a task for `codex`, `claude`, or `auto` routing.
|
|
15
16
|
- `kavi tasks`: inspect the session task list with summaries and artifact availability.
|
|
16
17
|
- `kavi task-output`: inspect the normalized envelope and raw output for a completed task.
|
|
18
|
+
- `kavi decisions`: inspect the persisted routing, approval, task, and integration decisions.
|
|
19
|
+
- `kavi claims`: inspect active or historical path claims.
|
|
20
|
+
- `kavi reviews`: inspect persisted operator review threads and linked follow-up tasks.
|
|
17
21
|
- `kavi approvals`: inspect the approval inbox.
|
|
18
22
|
- `kavi approve` and `kavi deny`: resolve a pending approval request, optionally with `--remember`.
|
|
19
23
|
- `kavi events`: inspect recent daemon and task events.
|
|
@@ -25,11 +29,15 @@ Runtime model:
|
|
|
25
29
|
- Machine-local state defaults to `~/.config/kavi` and `~/.local/state/kavi`.
|
|
26
30
|
- In restricted environments you can override those with `KAVI_HOME_CONFIG_DIR` and `KAVI_HOME_STATE_DIR`.
|
|
27
31
|
- User-local runtime overrides live in `~/.config/kavi/config.toml` and can point Kavi at custom `node`, `codex`, and `claude` binaries.
|
|
32
|
+
- The operator surface talks to the daemon over a local control socket under the machine-local state root.
|
|
28
33
|
- SQLite event mirroring is opt-in with `KAVI_ENABLE_SQLITE_HISTORY=1`; the default event log is JSONL under `.kavi/state/events.jsonl`.
|
|
34
|
+
- The operator console exposes a task board, dual agent lanes, a live inspector pane, approval actions, inline task composition, worktree diff review with file and hunk navigation, and persisted operator review threads on files or hunks.
|
|
29
35
|
|
|
30
36
|
Development:
|
|
31
37
|
|
|
32
38
|
```bash
|
|
39
|
+
# works in an empty folder; Kavi will initialize git and create the
|
|
40
|
+
# bootstrap commit it needs for managed worktrees
|
|
33
41
|
node bin/kavi.js init --home
|
|
34
42
|
node bin/kavi.js doctor --json
|
|
35
43
|
node bin/kavi.js paths
|
|
@@ -38,15 +46,22 @@ node bin/kavi.js status
|
|
|
38
46
|
node bin/kavi.js task --agent auto "Design the dashboard shell"
|
|
39
47
|
node bin/kavi.js tasks
|
|
40
48
|
node bin/kavi.js task-output latest
|
|
49
|
+
node bin/kavi.js decisions
|
|
50
|
+
node bin/kavi.js claims
|
|
51
|
+
node bin/kavi.js reviews
|
|
41
52
|
node bin/kavi.js approvals
|
|
42
53
|
node bin/kavi.js open
|
|
43
|
-
|
|
54
|
+
npm test
|
|
44
55
|
```
|
|
45
56
|
|
|
46
57
|
Notes:
|
|
58
|
+
- `kavi init` and `kavi open` now support the "empty folder to first managed session" path. If no git repo exists, Kavi initializes one; if git exists but no `HEAD` exists yet, Kavi creates the bootstrap commit it needs for worktrees.
|
|
47
59
|
- Codex runs through `codex app-server` in managed mode, so Codex-side approvals now land in the same Kavi inbox as Claude hook approvals.
|
|
48
60
|
- Claude still runs through the installed `claude` CLI with Kavi-managed hooks and approval decisions.
|
|
49
|
-
- The dashboard
|
|
61
|
+
- The dashboard and operator commands now use the daemon's local RPC socket instead of editing session files directly, and the TUI stays updated from pushed daemon snapshots rather than polling.
|
|
62
|
+
- The socket is machine-local rather than repo-local to avoid Unix socket path-length issues in deep repos and temp directories.
|
|
63
|
+
- The console is keyboard-driven: `1-7` switch views, `j/k` move selection, `[` and `]` cycle task detail sections, `,` and `.` cycle changed files, `{` and `}` cycle patch hunks, `A/C/Q/M` add review notes, `o/O` cycle existing threads, `T` reply, `E` edit, `R` resolve or reopen, `F` queue a fix task, `H` queue a handoff task, `y/n` resolve approvals, and `c` opens the inline task composer.
|
|
64
|
+
- Successful follow-up tasks now auto-resolve linked open review threads, landed follow-up work marks those resolved threads as landed, and replying to a resolved thread reopens it.
|
|
50
65
|
|
|
51
66
|
Local install options:
|
|
52
67
|
|
|
@@ -98,6 +113,7 @@ Notes on publish:
|
|
|
98
113
|
- The package name is scoped as `@mandipadk7/kavi` to match the npm user `mandipadk7`.
|
|
99
114
|
- The publish flow now builds a compiled `dist/` directory first, and the installed CLI prefers `dist/main.js`. Source mode remains available for local development.
|
|
100
115
|
- Hook commands now invoke the compiled entrypoint directly when `dist/` is present, and only use `--experimental-strip-types` in source mode.
|
|
116
|
+
- The interactive publish script strips `repository`, `homepage`, and `bugs` from the packaged `package.json`, so the npm page uses the bundled `README.md` instead of private GitHub links.
|
|
101
117
|
- `prepublishOnly` runs the release checks automatically during publish.
|
|
102
118
|
|
|
103
119
|
User-local config example:
|
package/dist/adapters/claude.js
CHANGED
|
@@ -3,6 +3,80 @@ import { buildAgentInstructions, buildPeerMessages, buildTaskPrompt, extractJson
|
|
|
3
3
|
import { runCommand } from "../process.js";
|
|
4
4
|
import { loadAgentPrompt } from "../prompts.js";
|
|
5
5
|
import { buildKaviShellCommand } from "../runtime.js";
|
|
6
|
+
const CLAUDE_ENVELOPE_SCHEMA = JSON.stringify({
|
|
7
|
+
type: "object",
|
|
8
|
+
additionalProperties: false,
|
|
9
|
+
required: [
|
|
10
|
+
"summary",
|
|
11
|
+
"status",
|
|
12
|
+
"blockers",
|
|
13
|
+
"nextRecommendation",
|
|
14
|
+
"peerMessages"
|
|
15
|
+
],
|
|
16
|
+
properties: {
|
|
17
|
+
summary: {
|
|
18
|
+
type: "string"
|
|
19
|
+
},
|
|
20
|
+
status: {
|
|
21
|
+
type: "string",
|
|
22
|
+
enum: [
|
|
23
|
+
"completed",
|
|
24
|
+
"blocked",
|
|
25
|
+
"needs_review"
|
|
26
|
+
]
|
|
27
|
+
},
|
|
28
|
+
blockers: {
|
|
29
|
+
type: "array",
|
|
30
|
+
items: {
|
|
31
|
+
type: "string"
|
|
32
|
+
}
|
|
33
|
+
},
|
|
34
|
+
nextRecommendation: {
|
|
35
|
+
type: [
|
|
36
|
+
"string",
|
|
37
|
+
"null"
|
|
38
|
+
]
|
|
39
|
+
},
|
|
40
|
+
peerMessages: {
|
|
41
|
+
type: "array",
|
|
42
|
+
items: {
|
|
43
|
+
type: "object",
|
|
44
|
+
additionalProperties: false,
|
|
45
|
+
required: [
|
|
46
|
+
"to",
|
|
47
|
+
"intent",
|
|
48
|
+
"subject",
|
|
49
|
+
"body"
|
|
50
|
+
],
|
|
51
|
+
properties: {
|
|
52
|
+
to: {
|
|
53
|
+
type: "string",
|
|
54
|
+
enum: [
|
|
55
|
+
"codex",
|
|
56
|
+
"claude"
|
|
57
|
+
]
|
|
58
|
+
},
|
|
59
|
+
intent: {
|
|
60
|
+
type: "string",
|
|
61
|
+
enum: [
|
|
62
|
+
"question",
|
|
63
|
+
"handoff",
|
|
64
|
+
"review_request",
|
|
65
|
+
"blocked",
|
|
66
|
+
"context_share"
|
|
67
|
+
]
|
|
68
|
+
},
|
|
69
|
+
subject: {
|
|
70
|
+
type: "string"
|
|
71
|
+
},
|
|
72
|
+
body: {
|
|
73
|
+
type: "string"
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
});
|
|
6
80
|
function findWorktree(session, agent) {
|
|
7
81
|
const worktree = session.worktrees.find((item)=>item.agent === agent);
|
|
8
82
|
if (!worktree) {
|
|
@@ -10,6 +84,47 @@ function findWorktree(session, agent) {
|
|
|
10
84
|
}
|
|
11
85
|
return worktree;
|
|
12
86
|
}
|
|
87
|
+
function asObject(value) {
|
|
88
|
+
return value && typeof value === "object" && !Array.isArray(value) ? value : {};
|
|
89
|
+
}
|
|
90
|
+
function asString(value) {
|
|
91
|
+
return typeof value === "string" ? value : null;
|
|
92
|
+
}
|
|
93
|
+
function normalizeEnvelope(value) {
|
|
94
|
+
if (value && typeof value === "object" && !Array.isArray(value)) {
|
|
95
|
+
const parsed = value;
|
|
96
|
+
return {
|
|
97
|
+
summary: typeof parsed.summary === "string" ? parsed.summary : "",
|
|
98
|
+
status: parsed.status === "blocked" || parsed.status === "needs_review" ? parsed.status : "completed",
|
|
99
|
+
blockers: Array.isArray(parsed.blockers) ? parsed.blockers.map((item)=>String(item)) : [],
|
|
100
|
+
nextRecommendation: parsed.nextRecommendation === null || typeof parsed.nextRecommendation === "string" ? parsed.nextRecommendation : null,
|
|
101
|
+
peerMessages: Array.isArray(parsed.peerMessages) ? parsed.peerMessages.map((message)=>{
|
|
102
|
+
const payload = asObject(message);
|
|
103
|
+
return {
|
|
104
|
+
to: payload.to === "codex" ? "codex" : "claude",
|
|
105
|
+
intent: payload.intent === "handoff" || payload.intent === "review_request" || payload.intent === "blocked" || payload.intent === "context_share" ? payload.intent : "question",
|
|
106
|
+
subject: String(payload.subject ?? ""),
|
|
107
|
+
body: String(payload.body ?? "")
|
|
108
|
+
};
|
|
109
|
+
}) : []
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
return extractJsonObject(String(value ?? ""));
|
|
113
|
+
}
|
|
114
|
+
export function parseClaudeStructuredOutput(rawOutput, fallbackSessionId) {
|
|
115
|
+
const parsed = JSON.parse(rawOutput);
|
|
116
|
+
const wrapper = Array.isArray(parsed) ? asObject(parsed[parsed.length - 1]) : asObject(parsed);
|
|
117
|
+
const sessionId = asString(wrapper.session_id) ?? asString(wrapper.sessionId) ?? fallbackSessionId;
|
|
118
|
+
const resultPayload = "result" in wrapper ? wrapper.result : "content" in wrapper ? wrapper.content : parsed;
|
|
119
|
+
if (wrapper.is_error === true) {
|
|
120
|
+
throw new Error(asString(wrapper.result) ?? "Claude returned an error response.");
|
|
121
|
+
}
|
|
122
|
+
return {
|
|
123
|
+
envelope: normalizeEnvelope(resultPayload),
|
|
124
|
+
sessionId,
|
|
125
|
+
raw: rawOutput
|
|
126
|
+
};
|
|
127
|
+
}
|
|
13
128
|
export async function writeClaudeSettings(paths, session) {
|
|
14
129
|
const buildHookCommand = (event)=>buildKaviShellCommand(session.runtime, [
|
|
15
130
|
"__hook",
|
|
@@ -89,7 +204,7 @@ export async function writeClaudeSettings(paths, session) {
|
|
|
89
204
|
}
|
|
90
205
|
export async function runClaudeTask(session, task, paths) {
|
|
91
206
|
const worktree = findWorktree(session, "claude");
|
|
92
|
-
const claudeSessionId = `${session.id}-claude`;
|
|
207
|
+
const claudeSessionId = session.agentStatus.claude.sessionId ?? `${session.id}-claude`;
|
|
93
208
|
await writeClaudeSettings(paths, session);
|
|
94
209
|
const repoPrompt = await loadAgentPrompt(paths, "claude");
|
|
95
210
|
const prompt = [
|
|
@@ -99,22 +214,30 @@ export async function runClaudeTask(session, task, paths) {
|
|
|
99
214
|
].join("\n");
|
|
100
215
|
const result = await runCommand(session.runtime.claudeExecutable, [
|
|
101
216
|
"-p",
|
|
102
|
-
"--
|
|
103
|
-
|
|
217
|
+
"--output-format",
|
|
218
|
+
"json",
|
|
219
|
+
"--json-schema",
|
|
220
|
+
CLAUDE_ENVELOPE_SCHEMA,
|
|
104
221
|
"--settings",
|
|
105
222
|
paths.claudeSettingsFile,
|
|
106
223
|
"--permission-mode",
|
|
107
224
|
"plan",
|
|
225
|
+
...session.agentStatus.claude.sessionId ? [
|
|
226
|
+
"--resume",
|
|
227
|
+
claudeSessionId
|
|
228
|
+
] : [
|
|
229
|
+
"--session-id",
|
|
230
|
+
claudeSessionId
|
|
231
|
+
],
|
|
108
232
|
prompt
|
|
109
233
|
], {
|
|
110
234
|
cwd: worktree.path
|
|
111
235
|
});
|
|
112
236
|
const rawOutput = result.code === 0 ? result.stdout : `${result.stdout}\n${result.stderr}`;
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
};
|
|
237
|
+
if (result.code !== 0) {
|
|
238
|
+
throw new Error(rawOutput.trim() || "Claude task failed.");
|
|
239
|
+
}
|
|
240
|
+
return parseClaudeStructuredOutput(rawOutput, claudeSessionId);
|
|
118
241
|
}
|
|
119
242
|
export { buildPeerMessages };
|
|
120
243
|
|
package/dist/adapters/shared.js
CHANGED
|
@@ -1,5 +1,21 @@
|
|
|
1
1
|
import { randomUUID } from "node:crypto";
|
|
2
2
|
import { nowIso } from "../paths.js";
|
|
3
|
+
function formatDecisionLine(summary, detail) {
|
|
4
|
+
return detail.trim() ? `- ${summary}: ${detail}` : `- ${summary}`;
|
|
5
|
+
}
|
|
6
|
+
export function buildDecisionReplay(session, task, agent) {
|
|
7
|
+
const taskDecisions = session.decisions.filter((decision)=>decision.taskId === task.id).slice(-6).map((decision)=>formatDecisionLine(`[${decision.kind}] ${decision.summary}`, decision.detail));
|
|
8
|
+
const sharedDecisions = session.decisions.filter((decision)=>decision.taskId !== task.id && (decision.agent === agent || decision.agent === null)).slice(-4).map((decision)=>formatDecisionLine(`[${decision.kind}] ${decision.summary}`, decision.detail));
|
|
9
|
+
const relevantClaims = session.pathClaims.filter((claim)=>claim.status === "active" && (claim.taskId === task.id || claim.agent !== agent)).slice(-6).map((claim)=>`- ${claim.agent} ${claim.source} claim on ${claim.paths.join(", ")}${claim.note ? `: ${claim.note}` : ""}`);
|
|
10
|
+
const replay = [
|
|
11
|
+
`- Current route reason: ${task.routeReason ?? "not recorded"}`,
|
|
12
|
+
`- Current claimed paths: ${task.claimedPaths.join(", ") || "none"}`,
|
|
13
|
+
...taskDecisions,
|
|
14
|
+
...sharedDecisions,
|
|
15
|
+
...relevantClaims
|
|
16
|
+
];
|
|
17
|
+
return replay.slice(0, 16);
|
|
18
|
+
}
|
|
3
19
|
export function extractJsonObject(rawOutput) {
|
|
4
20
|
const trimmed = rawOutput.trim();
|
|
5
21
|
const fencedMatch = trimmed.match(/```json\s*([\s\S]*?)```/i);
|
|
@@ -36,15 +52,15 @@ export function buildPeerMessages(envelope, from, taskId) {
|
|
|
36
52
|
export function buildSharedContext(session, task, agent) {
|
|
37
53
|
const inbox = session.peerMessages.filter((message)=>message.to === agent).slice(-session.config.messageLimit).map((message)=>`- [${message.intent}] ${message.subject}: ${message.body}`).join("\n");
|
|
38
54
|
const tasks = session.tasks.map((item)=>`- ${item.id} | ${item.owner} | ${item.status} | ${item.title}`).join("\n");
|
|
39
|
-
const
|
|
55
|
+
const decisionReplay = buildDecisionReplay(session, task, agent).join("\n");
|
|
40
56
|
const claims = session.pathClaims.filter((claim)=>claim.status === "active").slice(-6).map((claim)=>`- ${claim.agent} | ${claim.paths.join(", ")}`).join("\n");
|
|
41
57
|
return [
|
|
42
58
|
`Session goal: ${session.goal ?? "No goal recorded."}`,
|
|
43
59
|
`Current task: ${task.title}`,
|
|
44
60
|
"Task board:",
|
|
45
61
|
tasks || "- none",
|
|
46
|
-
"
|
|
47
|
-
|
|
62
|
+
"Compaction-safe replay:",
|
|
63
|
+
decisionReplay || "- empty",
|
|
48
64
|
"Active path claims:",
|
|
49
65
|
claims || "- empty",
|
|
50
66
|
`Peer inbox for ${agent}:`,
|