@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/README.md +108 -0
- package/bin/kavi.js +44 -0
- package/dist/adapters/claude.js +122 -0
- package/dist/adapters/codex.js +45 -0
- package/dist/adapters/shared.js +69 -0
- package/dist/approvals.js +185 -0
- package/dist/command-queue.js +25 -0
- package/dist/config.js +175 -0
- package/dist/daemon.js +202 -0
- package/dist/doctor.js +78 -0
- package/dist/fs.js +30 -0
- package/dist/git.js +289 -0
- package/dist/history.js +39 -0
- package/dist/main.js +667 -0
- package/dist/paths.js +43 -0
- package/dist/process.js +72 -0
- package/dist/router.js +55 -0
- package/dist/runtime.js +43 -0
- package/dist/session.js +113 -0
- package/dist/task-artifacts.js +37 -0
- package/dist/toml.js +55 -0
- package/dist/tui.js +92 -0
- package/dist/types.js +3 -0
- package/package.json +53 -0
package/dist/git.js
ADDED
|
@@ -0,0 +1,289 @@
|
|
|
1
|
+
import fs from "node:fs/promises";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { ensureDir } from "./fs.js";
|
|
4
|
+
import { buildSessionId } from "./paths.js";
|
|
5
|
+
import { runCommand } from "./process.js";
|
|
6
|
+
export async function detectRepoRoot(cwd) {
|
|
7
|
+
const result = await runCommand("git", [
|
|
8
|
+
"rev-parse",
|
|
9
|
+
"--show-toplevel"
|
|
10
|
+
], {
|
|
11
|
+
cwd
|
|
12
|
+
});
|
|
13
|
+
if (result.code !== 0) {
|
|
14
|
+
throw new Error(result.stderr.trim() || "Not inside a git repository.");
|
|
15
|
+
}
|
|
16
|
+
return result.stdout.trim();
|
|
17
|
+
}
|
|
18
|
+
export async function getHeadCommit(repoRoot) {
|
|
19
|
+
const result = await runCommand("git", [
|
|
20
|
+
"rev-parse",
|
|
21
|
+
"HEAD"
|
|
22
|
+
], {
|
|
23
|
+
cwd: repoRoot
|
|
24
|
+
});
|
|
25
|
+
if (result.code !== 0) {
|
|
26
|
+
throw new Error("Unable to resolve HEAD. Create an initial commit before opening a Kavi session.");
|
|
27
|
+
}
|
|
28
|
+
return result.stdout.trim();
|
|
29
|
+
}
|
|
30
|
+
export async function getCurrentBranch(repoRoot) {
|
|
31
|
+
const result = await runCommand("git", [
|
|
32
|
+
"branch",
|
|
33
|
+
"--show-current"
|
|
34
|
+
], {
|
|
35
|
+
cwd: repoRoot
|
|
36
|
+
});
|
|
37
|
+
if (result.code !== 0) {
|
|
38
|
+
throw new Error(result.stderr.trim() || "Unable to resolve current branch.");
|
|
39
|
+
}
|
|
40
|
+
return result.stdout.trim();
|
|
41
|
+
}
|
|
42
|
+
export async function getBranchCommit(repoRoot, ref) {
|
|
43
|
+
const result = await runCommand("git", [
|
|
44
|
+
"rev-parse",
|
|
45
|
+
ref
|
|
46
|
+
], {
|
|
47
|
+
cwd: repoRoot
|
|
48
|
+
});
|
|
49
|
+
if (result.code !== 0) {
|
|
50
|
+
throw new Error(result.stderr.trim() || `Unable to resolve ${ref}.`);
|
|
51
|
+
}
|
|
52
|
+
return result.stdout.trim();
|
|
53
|
+
}
|
|
54
|
+
export async function resolveTargetBranch(repoRoot, configuredBranch) {
|
|
55
|
+
const exists = await runCommand("git", [
|
|
56
|
+
"show-ref",
|
|
57
|
+
"--verify",
|
|
58
|
+
`refs/heads/${configuredBranch}`
|
|
59
|
+
], {
|
|
60
|
+
cwd: repoRoot
|
|
61
|
+
});
|
|
62
|
+
if (exists.code === 0) {
|
|
63
|
+
return configuredBranch;
|
|
64
|
+
}
|
|
65
|
+
return getCurrentBranch(repoRoot);
|
|
66
|
+
}
|
|
67
|
+
async function ensureDetachedWorktree(repoRoot, worktreePath, baseCommit) {
|
|
68
|
+
const result = await runCommand("git", [
|
|
69
|
+
"worktree",
|
|
70
|
+
"add",
|
|
71
|
+
"--detach",
|
|
72
|
+
worktreePath,
|
|
73
|
+
baseCommit
|
|
74
|
+
], {
|
|
75
|
+
cwd: repoRoot
|
|
76
|
+
});
|
|
77
|
+
if (result.code !== 0 && !result.stderr.includes("already exists")) {
|
|
78
|
+
throw new Error(result.stderr.trim() || `Unable to create worktree ${worktreePath}.`);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
async function ensureBranch(worktreePath, branchName, baseCommit) {
|
|
82
|
+
const exists = await runCommand("git", [
|
|
83
|
+
"rev-parse",
|
|
84
|
+
"--verify",
|
|
85
|
+
branchName
|
|
86
|
+
], {
|
|
87
|
+
cwd: worktreePath
|
|
88
|
+
});
|
|
89
|
+
if (exists.code !== 0) {
|
|
90
|
+
const createResult = await runCommand("git", [
|
|
91
|
+
"checkout",
|
|
92
|
+
"-b",
|
|
93
|
+
branchName,
|
|
94
|
+
baseCommit
|
|
95
|
+
], {
|
|
96
|
+
cwd: worktreePath
|
|
97
|
+
});
|
|
98
|
+
if (createResult.code !== 0) {
|
|
99
|
+
throw new Error(createResult.stderr.trim() || `Unable to create branch ${branchName}.`);
|
|
100
|
+
}
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
const checkoutResult = await runCommand("git", [
|
|
104
|
+
"checkout",
|
|
105
|
+
branchName
|
|
106
|
+
], {
|
|
107
|
+
cwd: worktreePath
|
|
108
|
+
});
|
|
109
|
+
if (checkoutResult.code !== 0) {
|
|
110
|
+
throw new Error(checkoutResult.stderr.trim() || `Unable to checkout ${branchName}.`);
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
export async function ensureWorktrees(repoRoot, paths, sessionId, _config, baseCommit) {
|
|
114
|
+
await ensureDir(paths.worktreeRoot);
|
|
115
|
+
const agents = [
|
|
116
|
+
"codex",
|
|
117
|
+
"claude"
|
|
118
|
+
];
|
|
119
|
+
const worktrees = [];
|
|
120
|
+
for (const agent of agents){
|
|
121
|
+
const worktreePath = path.join(paths.worktreeRoot, `${agent}-${sessionId}`);
|
|
122
|
+
const branchName = `kavi/${sessionId}/${agent}`;
|
|
123
|
+
await ensureDetachedWorktree(repoRoot, worktreePath, baseCommit);
|
|
124
|
+
await ensureBranch(worktreePath, branchName, baseCommit);
|
|
125
|
+
worktrees.push({
|
|
126
|
+
agent,
|
|
127
|
+
path: worktreePath,
|
|
128
|
+
branch: branchName
|
|
129
|
+
});
|
|
130
|
+
}
|
|
131
|
+
return worktrees;
|
|
132
|
+
}
|
|
133
|
+
export async function createGitignoreEntries(repoRoot) {
|
|
134
|
+
const gitignorePath = path.join(repoRoot, ".gitignore");
|
|
135
|
+
let content = "";
|
|
136
|
+
try {
|
|
137
|
+
content = await fs.readFile(gitignorePath, "utf8");
|
|
138
|
+
} catch {
|
|
139
|
+
content = "";
|
|
140
|
+
}
|
|
141
|
+
const entries = [
|
|
142
|
+
".kavi/state",
|
|
143
|
+
".kavi/runtime"
|
|
144
|
+
];
|
|
145
|
+
const missing = entries.filter((entry)=>!content.split(/\r?\n/).includes(entry));
|
|
146
|
+
if (missing.length === 0) {
|
|
147
|
+
return;
|
|
148
|
+
}
|
|
149
|
+
const prefix = content.trimEnd() ? "\n" : "";
|
|
150
|
+
await fs.writeFile(gitignorePath, `${content.trimEnd()}${prefix}${missing.join("\n")}\n`, "utf8");
|
|
151
|
+
}
|
|
152
|
+
export async function landBranches(repoRoot, targetBranch, worktrees, validationCommand, sessionId, integrationRoot) {
|
|
153
|
+
const commandsRun = [];
|
|
154
|
+
const snapshotCommits = [];
|
|
155
|
+
const targetHead = await getBranchCommit(repoRoot, targetBranch);
|
|
156
|
+
const integrationId = buildSessionId().slice(0, 8);
|
|
157
|
+
const integrationBranch = `kavi/land/${sessionId}/${integrationId}`;
|
|
158
|
+
const integrationPath = path.join(integrationRoot, `${sessionId}-${integrationId}`);
|
|
159
|
+
await ensureDir(integrationRoot);
|
|
160
|
+
const addIntegration = await runCommand("git", [
|
|
161
|
+
"worktree",
|
|
162
|
+
"add",
|
|
163
|
+
"-b",
|
|
164
|
+
integrationBranch,
|
|
165
|
+
integrationPath,
|
|
166
|
+
targetHead
|
|
167
|
+
], {
|
|
168
|
+
cwd: repoRoot
|
|
169
|
+
});
|
|
170
|
+
commandsRun.push(`git worktree add -b ${integrationBranch} ${integrationPath} ${targetHead}`);
|
|
171
|
+
if (addIntegration.code !== 0) {
|
|
172
|
+
throw new Error(addIntegration.stderr.trim() || `Unable to create integration worktree at ${integrationPath}.`);
|
|
173
|
+
}
|
|
174
|
+
for (const worktree of worktrees){
|
|
175
|
+
const snapshot = await snapshotWorktree(worktree, sessionId);
|
|
176
|
+
snapshotCommits.push(snapshot);
|
|
177
|
+
if (snapshot.createdCommit) {
|
|
178
|
+
commandsRun.push(`git -C ${worktree.path} add -A`);
|
|
179
|
+
commandsRun.push(`git -C ${worktree.path} -c user.name=Kavi -c user.email=kavi@local.invalid commit -m "kavi: snapshot ${worktree.agent} ${sessionId}"`);
|
|
180
|
+
}
|
|
181
|
+
const merge = await runCommand("git", [
|
|
182
|
+
"merge",
|
|
183
|
+
"--no-ff",
|
|
184
|
+
"--no-edit",
|
|
185
|
+
worktree.branch
|
|
186
|
+
], {
|
|
187
|
+
cwd: integrationPath
|
|
188
|
+
});
|
|
189
|
+
commandsRun.push(`git -C ${integrationPath} merge --no-ff --no-edit ${worktree.branch}`);
|
|
190
|
+
if (merge.code !== 0) {
|
|
191
|
+
throw new Error(merge.stderr.trim() || `Unable to merge branch ${worktree.branch} into integration branch ${integrationBranch}.`);
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
if (validationCommand.trim()) {
|
|
195
|
+
const validation = await runCommand("zsh", [
|
|
196
|
+
"-lc",
|
|
197
|
+
validationCommand
|
|
198
|
+
], {
|
|
199
|
+
cwd: integrationPath
|
|
200
|
+
});
|
|
201
|
+
commandsRun.push(validationCommand);
|
|
202
|
+
if (validation.code !== 0) {
|
|
203
|
+
throw new Error(`Validation command failed.\n${validation.stdout}\n${validation.stderr}`.trim());
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
const currentBranch = await getCurrentBranch(repoRoot).catch(()=>"");
|
|
207
|
+
if (currentBranch === targetBranch) {
|
|
208
|
+
const mergeIntegration = await runCommand("git", [
|
|
209
|
+
"merge",
|
|
210
|
+
"--ff-only",
|
|
211
|
+
integrationBranch
|
|
212
|
+
], {
|
|
213
|
+
cwd: repoRoot
|
|
214
|
+
});
|
|
215
|
+
commandsRun.push(`git merge --ff-only ${integrationBranch}`);
|
|
216
|
+
if (mergeIntegration.code !== 0) {
|
|
217
|
+
throw new Error(mergeIntegration.stderr.trim() || `Unable to fast-forward ${targetBranch} to ${integrationBranch}.`);
|
|
218
|
+
}
|
|
219
|
+
} else {
|
|
220
|
+
const updateRef = await runCommand("git", [
|
|
221
|
+
"update-ref",
|
|
222
|
+
`refs/heads/${targetBranch}`,
|
|
223
|
+
`refs/heads/${integrationBranch}`,
|
|
224
|
+
targetHead
|
|
225
|
+
], {
|
|
226
|
+
cwd: repoRoot
|
|
227
|
+
});
|
|
228
|
+
commandsRun.push(`git update-ref refs/heads/${targetBranch} refs/heads/${integrationBranch} ${targetHead}`);
|
|
229
|
+
if (updateRef.code !== 0) {
|
|
230
|
+
throw new Error(updateRef.stderr.trim() || `Unable to advance ${targetBranch}; it changed while landing was in progress.`);
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
return {
|
|
234
|
+
commandsRun,
|
|
235
|
+
integrationBranch,
|
|
236
|
+
integrationPath,
|
|
237
|
+
snapshotCommits
|
|
238
|
+
};
|
|
239
|
+
}
|
|
240
|
+
async function snapshotWorktree(worktree, sessionId) {
|
|
241
|
+
const status = await runCommand("git", [
|
|
242
|
+
"status",
|
|
243
|
+
"--short"
|
|
244
|
+
], {
|
|
245
|
+
cwd: worktree.path
|
|
246
|
+
});
|
|
247
|
+
if (status.code !== 0) {
|
|
248
|
+
throw new Error(status.stderr.trim() || `Unable to inspect worktree ${worktree.path}.`);
|
|
249
|
+
}
|
|
250
|
+
if (!status.stdout.trim()) {
|
|
251
|
+
return {
|
|
252
|
+
agent: worktree.agent,
|
|
253
|
+
createdCommit: false,
|
|
254
|
+
commit: await getBranchCommit(worktree.path, "HEAD")
|
|
255
|
+
};
|
|
256
|
+
}
|
|
257
|
+
const add = await runCommand("git", [
|
|
258
|
+
"add",
|
|
259
|
+
"-A"
|
|
260
|
+
], {
|
|
261
|
+
cwd: worktree.path
|
|
262
|
+
});
|
|
263
|
+
if (add.code !== 0) {
|
|
264
|
+
throw new Error(add.stderr.trim() || `Unable to stage worktree ${worktree.path}.`);
|
|
265
|
+
}
|
|
266
|
+
const commitMessage = `kavi: snapshot ${worktree.agent} ${sessionId}`;
|
|
267
|
+
const commit = await runCommand("git", [
|
|
268
|
+
"-c",
|
|
269
|
+
"user.name=Kavi",
|
|
270
|
+
"-c",
|
|
271
|
+
"user.email=kavi@local.invalid",
|
|
272
|
+
"commit",
|
|
273
|
+
"-m",
|
|
274
|
+
commitMessage
|
|
275
|
+
], {
|
|
276
|
+
cwd: worktree.path
|
|
277
|
+
});
|
|
278
|
+
if (commit.code !== 0) {
|
|
279
|
+
throw new Error(commit.stderr.trim() || `Unable to snapshot worktree ${worktree.path}.`);
|
|
280
|
+
}
|
|
281
|
+
return {
|
|
282
|
+
agent: worktree.agent,
|
|
283
|
+
createdCommit: true,
|
|
284
|
+
commit: await getBranchCommit(worktree.path, "HEAD")
|
|
285
|
+
};
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
|
|
289
|
+
//# sourceURL=git.ts
|
package/dist/history.js
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import { ensureDir } from "./fs.js";
|
|
3
|
+
export class EventHistory {
|
|
4
|
+
db;
|
|
5
|
+
constructor(db){
|
|
6
|
+
this.db = db;
|
|
7
|
+
this.db.exec(`
|
|
8
|
+
CREATE TABLE IF NOT EXISTS events (
|
|
9
|
+
id TEXT PRIMARY KEY,
|
|
10
|
+
session_id TEXT NOT NULL,
|
|
11
|
+
timestamp TEXT NOT NULL,
|
|
12
|
+
type TEXT NOT NULL,
|
|
13
|
+
payload_json TEXT NOT NULL
|
|
14
|
+
);
|
|
15
|
+
`);
|
|
16
|
+
}
|
|
17
|
+
static async open(paths, sessionId) {
|
|
18
|
+
if (process.env.KAVI_ENABLE_SQLITE_HISTORY !== "1") {
|
|
19
|
+
return null;
|
|
20
|
+
}
|
|
21
|
+
await ensureDir(paths.homeStateDir);
|
|
22
|
+
const dbPath = path.join(paths.homeStateDir, `${sessionId}.sqlite`);
|
|
23
|
+
const sqlite = await import("node:sqlite");
|
|
24
|
+
return new EventHistory(new sqlite.DatabaseSync(dbPath));
|
|
25
|
+
}
|
|
26
|
+
insert(sessionId, event) {
|
|
27
|
+
const statement = this.db.prepare(`
|
|
28
|
+
INSERT OR REPLACE INTO events (id, session_id, timestamp, type, payload_json)
|
|
29
|
+
VALUES (?, ?, ?, ?, ?)
|
|
30
|
+
`);
|
|
31
|
+
statement.run(event.id, sessionId, event.timestamp, event.type, JSON.stringify(event.payload));
|
|
32
|
+
}
|
|
33
|
+
close() {
|
|
34
|
+
this.db.close();
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
//# sourceURL=history.ts
|