@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/paths.js
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import os from "node:os";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { createHash, randomUUID } from "node:crypto";
|
|
4
|
+
export function resolveAppPaths(repoRoot) {
|
|
5
|
+
const home = os.homedir();
|
|
6
|
+
const safeRepoId = createHash("sha1").update(repoRoot).digest("hex").slice(0, 12);
|
|
7
|
+
const kaviDir = path.join(repoRoot, ".kavi");
|
|
8
|
+
const homeConfigDir = process.env.KAVI_HOME_CONFIG_DIR ?? path.join(home, ".config", "kavi");
|
|
9
|
+
const homeStateDir = process.env.KAVI_HOME_STATE_DIR ?? path.join(home, ".local", "state", "kavi");
|
|
10
|
+
const runtimeDir = path.join(kaviDir, "runtime");
|
|
11
|
+
const stateDir = path.join(kaviDir, "state");
|
|
12
|
+
const runsDir = path.join(runtimeDir, "runs");
|
|
13
|
+
return {
|
|
14
|
+
repoRoot,
|
|
15
|
+
kaviDir,
|
|
16
|
+
configFile: path.join(kaviDir, "config.toml"),
|
|
17
|
+
promptsDir: path.join(kaviDir, "prompts"),
|
|
18
|
+
stateDir,
|
|
19
|
+
runtimeDir,
|
|
20
|
+
runsDir,
|
|
21
|
+
stateFile: path.join(stateDir, "session.json"),
|
|
22
|
+
eventsFile: path.join(stateDir, "events.jsonl"),
|
|
23
|
+
approvalsFile: path.join(stateDir, "approvals.json"),
|
|
24
|
+
commandsFile: path.join(runtimeDir, "commands.jsonl"),
|
|
25
|
+
claudeSettingsFile: path.join(runtimeDir, "claude.settings.json"),
|
|
26
|
+
socketPath: path.join(runtimeDir, "kavid.sock"),
|
|
27
|
+
homeConfigDir,
|
|
28
|
+
homeConfigFile: path.join(homeConfigDir, "config.toml"),
|
|
29
|
+
homeApprovalRulesFile: path.join(homeConfigDir, "approval-rules.json"),
|
|
30
|
+
homeStateDir,
|
|
31
|
+
worktreeRoot: path.join(homeStateDir, "worktrees", safeRepoId),
|
|
32
|
+
integrationRoot: path.join(homeStateDir, "integration", safeRepoId)
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
export function buildSessionId() {
|
|
36
|
+
return randomUUID();
|
|
37
|
+
}
|
|
38
|
+
export function nowIso() {
|
|
39
|
+
return new Date().toISOString();
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
//# sourceURL=paths.ts
|
package/dist/process.js
ADDED
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import { spawn } from "node:child_process";
|
|
2
|
+
export async function runCommand(command, args, options = {}) {
|
|
3
|
+
return await new Promise((resolve)=>{
|
|
4
|
+
const child = spawn(command, args, {
|
|
5
|
+
...options,
|
|
6
|
+
stdio: [
|
|
7
|
+
"ignore",
|
|
8
|
+
"pipe",
|
|
9
|
+
"pipe"
|
|
10
|
+
]
|
|
11
|
+
});
|
|
12
|
+
let stdout = "";
|
|
13
|
+
let stderr = "";
|
|
14
|
+
let settled = false;
|
|
15
|
+
child.stdout.setEncoding("utf8");
|
|
16
|
+
child.stderr.setEncoding("utf8");
|
|
17
|
+
child.stdout.on("data", (chunk)=>{
|
|
18
|
+
stdout += chunk;
|
|
19
|
+
});
|
|
20
|
+
child.stderr.on("data", (chunk)=>{
|
|
21
|
+
stderr += chunk;
|
|
22
|
+
});
|
|
23
|
+
child.on("error", (error)=>{
|
|
24
|
+
if (settled) {
|
|
25
|
+
return;
|
|
26
|
+
}
|
|
27
|
+
settled = true;
|
|
28
|
+
resolve({
|
|
29
|
+
code: 127,
|
|
30
|
+
stdout,
|
|
31
|
+
stderr: `${stderr}${stderr ? "\n" : ""}${error instanceof Error ? error.message : String(error)}`
|
|
32
|
+
});
|
|
33
|
+
});
|
|
34
|
+
child.on("close", (code)=>{
|
|
35
|
+
if (settled) {
|
|
36
|
+
return;
|
|
37
|
+
}
|
|
38
|
+
settled = true;
|
|
39
|
+
resolve({
|
|
40
|
+
code: code ?? 1,
|
|
41
|
+
stdout,
|
|
42
|
+
stderr
|
|
43
|
+
});
|
|
44
|
+
});
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
export function spawnDetachedNode(nodeExecutable, args, cwd) {
|
|
48
|
+
const child = spawn(nodeExecutable, [
|
|
49
|
+
"--experimental-strip-types",
|
|
50
|
+
...args
|
|
51
|
+
], {
|
|
52
|
+
cwd,
|
|
53
|
+
detached: true,
|
|
54
|
+
stdio: "ignore"
|
|
55
|
+
});
|
|
56
|
+
child.unref();
|
|
57
|
+
return child.pid ?? -1;
|
|
58
|
+
}
|
|
59
|
+
export function isProcessAlive(pid) {
|
|
60
|
+
if (!pid || pid <= 0) {
|
|
61
|
+
return false;
|
|
62
|
+
}
|
|
63
|
+
try {
|
|
64
|
+
process.kill(pid, 0);
|
|
65
|
+
return true;
|
|
66
|
+
} catch {
|
|
67
|
+
return false;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
//# sourceURL=process.ts
|
package/dist/router.js
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { nowIso } from "./paths.js";
|
|
2
|
+
function containsKeyword(prompt, keywords) {
|
|
3
|
+
const lower = prompt.toLowerCase();
|
|
4
|
+
return keywords.some((keyword)=>lower.includes(keyword.toLowerCase()));
|
|
5
|
+
}
|
|
6
|
+
export function routePrompt(prompt, config) {
|
|
7
|
+
if (containsKeyword(prompt, config.routing.frontendKeywords)) {
|
|
8
|
+
return "claude";
|
|
9
|
+
}
|
|
10
|
+
if (containsKeyword(prompt, config.routing.backendKeywords)) {
|
|
11
|
+
return "codex";
|
|
12
|
+
}
|
|
13
|
+
return "codex";
|
|
14
|
+
}
|
|
15
|
+
export function buildKickoffTasks(goal) {
|
|
16
|
+
const timestamp = nowIso();
|
|
17
|
+
return [
|
|
18
|
+
{
|
|
19
|
+
id: "kickoff-codex",
|
|
20
|
+
title: "Codex kickoff plan",
|
|
21
|
+
owner: "codex",
|
|
22
|
+
status: "pending",
|
|
23
|
+
prompt: goal,
|
|
24
|
+
createdAt: timestamp,
|
|
25
|
+
updatedAt: timestamp,
|
|
26
|
+
summary: null
|
|
27
|
+
},
|
|
28
|
+
{
|
|
29
|
+
id: "kickoff-claude",
|
|
30
|
+
title: "Claude intent interpretation",
|
|
31
|
+
owner: "claude",
|
|
32
|
+
status: "pending",
|
|
33
|
+
prompt: goal,
|
|
34
|
+
createdAt: timestamp,
|
|
35
|
+
updatedAt: timestamp,
|
|
36
|
+
summary: null
|
|
37
|
+
}
|
|
38
|
+
];
|
|
39
|
+
}
|
|
40
|
+
export function buildAdHocTask(owner, prompt, taskId) {
|
|
41
|
+
const timestamp = nowIso();
|
|
42
|
+
return {
|
|
43
|
+
id: taskId,
|
|
44
|
+
title: `Ad hoc task for ${owner}`,
|
|
45
|
+
owner,
|
|
46
|
+
status: "pending",
|
|
47
|
+
prompt,
|
|
48
|
+
createdAt: timestamp,
|
|
49
|
+
updatedAt: timestamp,
|
|
50
|
+
summary: null
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
//# sourceURL=router.ts
|
package/dist/runtime.js
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import process from "node:process";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { fileURLToPath } from "node:url";
|
|
4
|
+
import { loadHomeConfig } from "./config.js";
|
|
5
|
+
const MINIMUM_NODE_MAJOR = 25;
|
|
6
|
+
export function parseNodeMajor(version) {
|
|
7
|
+
const major = Number(version.split(".")[0] ?? "");
|
|
8
|
+
return Number.isFinite(major) ? major : 0;
|
|
9
|
+
}
|
|
10
|
+
export function hasSupportedNode(version = process.versions.node) {
|
|
11
|
+
return parseNodeMajor(version) >= MINIMUM_NODE_MAJOR;
|
|
12
|
+
}
|
|
13
|
+
export function minimumNodeMajor() {
|
|
14
|
+
return MINIMUM_NODE_MAJOR;
|
|
15
|
+
}
|
|
16
|
+
export function resolveKaviEntrypoint() {
|
|
17
|
+
const runtimePath = fileURLToPath(import.meta.url);
|
|
18
|
+
const extension = path.extname(runtimePath) || ".js";
|
|
19
|
+
return fileURLToPath(new URL(`./main${extension}`, import.meta.url));
|
|
20
|
+
}
|
|
21
|
+
export async function resolveSessionRuntime(paths) {
|
|
22
|
+
const homeConfig = await loadHomeConfig(paths);
|
|
23
|
+
return {
|
|
24
|
+
nodeExecutable: homeConfig.runtime.nodeBin.trim() || process.execPath,
|
|
25
|
+
codexExecutable: homeConfig.runtime.codexBin.trim() || "codex",
|
|
26
|
+
claudeExecutable: homeConfig.runtime.claudeBin.trim() || "claude",
|
|
27
|
+
kaviEntryPoint: resolveKaviEntrypoint()
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
export function shellEscape(value) {
|
|
31
|
+
return `'${value.replaceAll("'", `'\\''`)}'`;
|
|
32
|
+
}
|
|
33
|
+
export function buildKaviShellCommand(runtime, args) {
|
|
34
|
+
return [
|
|
35
|
+
shellEscape(runtime.nodeExecutable),
|
|
36
|
+
"--experimental-strip-types",
|
|
37
|
+
shellEscape(runtime.kaviEntryPoint),
|
|
38
|
+
...args.map((arg)=>shellEscape(arg))
|
|
39
|
+
].join(" ");
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
//# sourceURL=runtime.ts
|
package/dist/session.js
ADDED
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
import fs from "node:fs/promises";
|
|
3
|
+
import { appendEvent, readJson, writeJson, fileExists } from "./fs.js";
|
|
4
|
+
import { EventHistory } from "./history.js";
|
|
5
|
+
import { nowIso } from "./paths.js";
|
|
6
|
+
import { resolveSessionRuntime } from "./runtime.js";
|
|
7
|
+
function initialAgentStatus(agent, transport) {
|
|
8
|
+
return {
|
|
9
|
+
agent,
|
|
10
|
+
available: true,
|
|
11
|
+
transport,
|
|
12
|
+
lastRunAt: null,
|
|
13
|
+
lastExitCode: null,
|
|
14
|
+
sessionId: null,
|
|
15
|
+
summary: null
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
export async function createSessionRecord(paths, config, runtime, sessionId, baseCommit, worktrees, goal, rpcEndpoint) {
|
|
19
|
+
const timestamp = nowIso();
|
|
20
|
+
const record = {
|
|
21
|
+
id: sessionId,
|
|
22
|
+
repoRoot: paths.repoRoot,
|
|
23
|
+
baseCommit,
|
|
24
|
+
createdAt: timestamp,
|
|
25
|
+
updatedAt: timestamp,
|
|
26
|
+
socketPath: rpcEndpoint,
|
|
27
|
+
status: "starting",
|
|
28
|
+
goal,
|
|
29
|
+
daemonPid: null,
|
|
30
|
+
daemonHeartbeatAt: null,
|
|
31
|
+
config,
|
|
32
|
+
runtime,
|
|
33
|
+
worktrees,
|
|
34
|
+
tasks: [],
|
|
35
|
+
peerMessages: [],
|
|
36
|
+
agentStatus: {
|
|
37
|
+
codex: initialAgentStatus("codex", "codex-exec"),
|
|
38
|
+
claude: initialAgentStatus("claude", "claude-print")
|
|
39
|
+
}
|
|
40
|
+
};
|
|
41
|
+
await writeJson(paths.stateFile, record);
|
|
42
|
+
return record;
|
|
43
|
+
}
|
|
44
|
+
export async function loadSessionRecord(paths) {
|
|
45
|
+
const record = await readJson(paths.stateFile);
|
|
46
|
+
if (!record.runtime) {
|
|
47
|
+
record.runtime = await resolveSessionRuntime(paths);
|
|
48
|
+
}
|
|
49
|
+
return record;
|
|
50
|
+
}
|
|
51
|
+
export async function saveSessionRecord(paths, record) {
|
|
52
|
+
record.updatedAt = nowIso();
|
|
53
|
+
await writeJson(paths.stateFile, record);
|
|
54
|
+
}
|
|
55
|
+
export async function recordEvent(paths, sessionId, type, payload) {
|
|
56
|
+
const event = {
|
|
57
|
+
id: randomUUID(),
|
|
58
|
+
type,
|
|
59
|
+
timestamp: nowIso(),
|
|
60
|
+
payload
|
|
61
|
+
};
|
|
62
|
+
await appendEvent(paths.eventsFile, event);
|
|
63
|
+
const history = await EventHistory.open(paths, sessionId);
|
|
64
|
+
if (history) {
|
|
65
|
+
history.insert(sessionId, event);
|
|
66
|
+
history.close();
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
export async function sessionExists(paths) {
|
|
70
|
+
return fileExists(paths.stateFile);
|
|
71
|
+
}
|
|
72
|
+
export async function updateTask(paths, task) {
|
|
73
|
+
const session = await loadSessionRecord(paths);
|
|
74
|
+
const tasks = session.tasks.filter((item)=>item.id !== task.id);
|
|
75
|
+
tasks.push(task);
|
|
76
|
+
session.tasks = tasks.sort((a, b)=>a.createdAt.localeCompare(b.createdAt));
|
|
77
|
+
await saveSessionRecord(paths, session);
|
|
78
|
+
}
|
|
79
|
+
export async function addPeerMessages(paths, messages) {
|
|
80
|
+
if (messages.length === 0) {
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
const session = await loadSessionRecord(paths);
|
|
84
|
+
session.peerMessages.push(...messages);
|
|
85
|
+
await saveSessionRecord(paths, session);
|
|
86
|
+
}
|
|
87
|
+
export async function markAgentRun(paths, agent, summary, exitCode, sessionId) {
|
|
88
|
+
const session = await loadSessionRecord(paths);
|
|
89
|
+
session.agentStatus[agent] = {
|
|
90
|
+
...session.agentStatus[agent],
|
|
91
|
+
lastRunAt: nowIso(),
|
|
92
|
+
lastExitCode: exitCode,
|
|
93
|
+
sessionId,
|
|
94
|
+
summary
|
|
95
|
+
};
|
|
96
|
+
await saveSessionRecord(paths, session);
|
|
97
|
+
}
|
|
98
|
+
export async function readRecentEvents(paths, limit = 20) {
|
|
99
|
+
if (!await fileExists(paths.eventsFile)) {
|
|
100
|
+
return [];
|
|
101
|
+
}
|
|
102
|
+
const content = await fs.readFile(paths.eventsFile, "utf8");
|
|
103
|
+
return content.trim().split(/\r?\n/).filter(Boolean).slice(-limit).map((line)=>JSON.parse(line));
|
|
104
|
+
}
|
|
105
|
+
export function sessionHeartbeatAgeMs(session) {
|
|
106
|
+
if (!session.daemonHeartbeatAt) {
|
|
107
|
+
return null;
|
|
108
|
+
}
|
|
109
|
+
return Date.now() - new Date(session.daemonHeartbeatAt).getTime();
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
//# sourceURL=session.ts
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import fs from "node:fs/promises";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { ensureDir, fileExists, readJson, writeJson } from "./fs.js";
|
|
4
|
+
function artifactPath(paths, taskId) {
|
|
5
|
+
return path.join(paths.runsDir, `${taskId}.json`);
|
|
6
|
+
}
|
|
7
|
+
export async function saveTaskArtifact(paths, artifact) {
|
|
8
|
+
await ensureDir(paths.runsDir);
|
|
9
|
+
await writeJson(artifactPath(paths, artifact.taskId), artifact);
|
|
10
|
+
}
|
|
11
|
+
export async function loadTaskArtifact(paths, taskId) {
|
|
12
|
+
const filePath = artifactPath(paths, taskId);
|
|
13
|
+
if (!await fileExists(filePath)) {
|
|
14
|
+
return null;
|
|
15
|
+
}
|
|
16
|
+
return readJson(filePath);
|
|
17
|
+
}
|
|
18
|
+
export async function listTaskArtifacts(paths) {
|
|
19
|
+
if (!await fileExists(paths.runsDir)) {
|
|
20
|
+
return [];
|
|
21
|
+
}
|
|
22
|
+
const entries = await fs.readdir(paths.runsDir, {
|
|
23
|
+
withFileTypes: true
|
|
24
|
+
});
|
|
25
|
+
const artifacts = [];
|
|
26
|
+
for (const entry of entries){
|
|
27
|
+
if (!entry.isFile() || !entry.name.endsWith(".json")) {
|
|
28
|
+
continue;
|
|
29
|
+
}
|
|
30
|
+
const artifact = await readJson(path.join(paths.runsDir, entry.name));
|
|
31
|
+
artifacts.push(artifact);
|
|
32
|
+
}
|
|
33
|
+
return artifacts.sort((left, right)=>left.finishedAt.localeCompare(right.finishedAt));
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
//# sourceURL=task-artifacts.ts
|
package/dist/toml.js
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
function parseScalar(value) {
|
|
2
|
+
const trimmed = value.trim();
|
|
3
|
+
if (trimmed === "true") {
|
|
4
|
+
return true;
|
|
5
|
+
}
|
|
6
|
+
if (trimmed === "false") {
|
|
7
|
+
return false;
|
|
8
|
+
}
|
|
9
|
+
if (/^-?\d+$/.test(trimmed)) {
|
|
10
|
+
return Number(trimmed);
|
|
11
|
+
}
|
|
12
|
+
if (trimmed.startsWith("\"") && trimmed.endsWith("\"")) {
|
|
13
|
+
return trimmed.slice(1, -1).replaceAll("\\\"", "\"");
|
|
14
|
+
}
|
|
15
|
+
return trimmed;
|
|
16
|
+
}
|
|
17
|
+
function parseArray(value) {
|
|
18
|
+
const inner = value.trim().slice(1, -1).trim();
|
|
19
|
+
if (!inner) {
|
|
20
|
+
return [];
|
|
21
|
+
}
|
|
22
|
+
return inner.split(",").map((part)=>parseScalar(part)).map((part)=>String(part));
|
|
23
|
+
}
|
|
24
|
+
export function parseToml(content) {
|
|
25
|
+
const root = {};
|
|
26
|
+
let current = root;
|
|
27
|
+
for (const rawLine of content.split(/\r?\n/)){
|
|
28
|
+
const line = rawLine.trim();
|
|
29
|
+
if (!line || line.startsWith("#")) {
|
|
30
|
+
continue;
|
|
31
|
+
}
|
|
32
|
+
if (line.startsWith("[") && line.endsWith("]")) {
|
|
33
|
+
const sectionPath = line.slice(1, -1).split(".");
|
|
34
|
+
current = root;
|
|
35
|
+
for (const part of sectionPath){
|
|
36
|
+
if (!(part in current)) {
|
|
37
|
+
current[part] = {};
|
|
38
|
+
}
|
|
39
|
+
current = current[part];
|
|
40
|
+
}
|
|
41
|
+
continue;
|
|
42
|
+
}
|
|
43
|
+
const separatorIndex = line.indexOf("=");
|
|
44
|
+
if (separatorIndex === -1) {
|
|
45
|
+
continue;
|
|
46
|
+
}
|
|
47
|
+
const key = line.slice(0, separatorIndex).trim();
|
|
48
|
+
const value = line.slice(separatorIndex + 1).trim();
|
|
49
|
+
current[key] = value.startsWith("[") && value.endsWith("]") ? parseArray(value) : parseScalar(value);
|
|
50
|
+
}
|
|
51
|
+
return root;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
//# sourceURL=toml.ts
|
package/dist/tui.js
ADDED
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import readline from "node:readline";
|
|
2
|
+
import process from "node:process";
|
|
3
|
+
import { listApprovalRequests, resolveApprovalRequest } from "./approvals.js";
|
|
4
|
+
import { appendCommand } from "./command-queue.js";
|
|
5
|
+
import { loadSessionRecord, readRecentEvents } from "./session.js";
|
|
6
|
+
function divider(title) {
|
|
7
|
+
return `\n=== ${title} ${"=".repeat(Math.max(1, 60 - title.length))}`;
|
|
8
|
+
}
|
|
9
|
+
function render(session, events, approvals) {
|
|
10
|
+
const agentLines = Object.values(session.agentStatus).map((status)=>`- ${status.agent}: ${status.transport} | last_exit=${status.lastExitCode ?? "-"} | last_run=${status.lastRunAt ?? "-"}`).join("\n");
|
|
11
|
+
const worktreeLines = session.worktrees.map((worktree)=>`- ${worktree.agent}: ${worktree.path} (${worktree.branch})`).join("\n");
|
|
12
|
+
const taskLines = session.tasks.map((task)=>`- ${task.id} | ${task.owner} | ${task.status} | ${task.title}${task.summary ? ` | ${task.summary}` : ""}`).join("\n");
|
|
13
|
+
const messageLines = session.peerMessages.slice(-8).map((message)=>`- ${message.from} -> ${message.to} [${message.intent}] ${message.subject}`).join("\n");
|
|
14
|
+
const approvalLines = approvals.slice(-6).map((request)=>`- ${request.id} | ${request.agent} | ${request.summary}`).join("\n");
|
|
15
|
+
const eventLines = events.slice(-8).map((event)=>`- ${event.timestamp} ${event.type}`).join("\n");
|
|
16
|
+
return [
|
|
17
|
+
"\u001bc",
|
|
18
|
+
`Kavi Session ${session.id}`,
|
|
19
|
+
`Repo: ${session.repoRoot}`,
|
|
20
|
+
`Goal: ${session.goal ?? "-"}`,
|
|
21
|
+
`Status: ${session.status}`,
|
|
22
|
+
divider("Agents"),
|
|
23
|
+
agentLines || "- none",
|
|
24
|
+
divider("Worktrees"),
|
|
25
|
+
worktreeLines || "- none",
|
|
26
|
+
divider("Tasks"),
|
|
27
|
+
taskLines || "- none",
|
|
28
|
+
divider("Approvals"),
|
|
29
|
+
approvalLines || "- none",
|
|
30
|
+
divider("Peer Messages"),
|
|
31
|
+
messageLines || "- none",
|
|
32
|
+
divider("Events"),
|
|
33
|
+
eventLines || "- none",
|
|
34
|
+
"\nKeys: q quit | r refresh | y approve latest | n deny latest | s shutdown daemon"
|
|
35
|
+
].join("\n");
|
|
36
|
+
}
|
|
37
|
+
export async function attachTui(paths) {
|
|
38
|
+
readline.emitKeypressEvents(process.stdin);
|
|
39
|
+
if (process.stdin.isTTY) {
|
|
40
|
+
process.stdin.setRawMode(true);
|
|
41
|
+
}
|
|
42
|
+
let closed = false;
|
|
43
|
+
const refresh = async ()=>{
|
|
44
|
+
const session = await loadSessionRecord(paths);
|
|
45
|
+
const events = await readRecentEvents(paths, 30);
|
|
46
|
+
const approvals = await listApprovalRequests(paths);
|
|
47
|
+
process.stdout.write(render(session, events, approvals));
|
|
48
|
+
};
|
|
49
|
+
const interval = setInterval(()=>{
|
|
50
|
+
void refresh();
|
|
51
|
+
}, 1500);
|
|
52
|
+
await refresh();
|
|
53
|
+
await new Promise((resolve)=>{
|
|
54
|
+
process.stdin.on("keypress", async (_str, key)=>{
|
|
55
|
+
if (key.name === "q" || key.ctrl && key.name === "c") {
|
|
56
|
+
closed = true;
|
|
57
|
+
clearInterval(interval);
|
|
58
|
+
if (process.stdin.isTTY) {
|
|
59
|
+
process.stdin.setRawMode(false);
|
|
60
|
+
}
|
|
61
|
+
process.stdout.write("\n");
|
|
62
|
+
resolve();
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
if (key.name === "r") {
|
|
66
|
+
await refresh();
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
if (key.name === "s") {
|
|
70
|
+
await appendCommand(paths, "shutdown", {});
|
|
71
|
+
await refresh();
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
if (key.name === "y" || key.name === "n") {
|
|
75
|
+
const approvals = await listApprovalRequests(paths);
|
|
76
|
+
const latest = approvals.sort((left, right)=>left.createdAt.localeCompare(right.createdAt)).pop();
|
|
77
|
+
if (!latest) {
|
|
78
|
+
await refresh();
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
81
|
+
await resolveApprovalRequest(paths, latest.id, key.name === "y" ? "allow" : "deny", false);
|
|
82
|
+
await refresh();
|
|
83
|
+
}
|
|
84
|
+
});
|
|
85
|
+
});
|
|
86
|
+
if (!closed) {
|
|
87
|
+
clearInterval(interval);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
//# sourceURL=tui.ts
|
package/dist/types.js
ADDED
package/package.json
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@mandipadk7/kavi",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Managed Codex + Claude collaboration TUI",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"preferGlobal": true,
|
|
7
|
+
"repository": {
|
|
8
|
+
"type": "git",
|
|
9
|
+
"url": "git+https://github.com/mandipadk/kavi.git"
|
|
10
|
+
},
|
|
11
|
+
"homepage": "https://github.com/mandipadk/kavi#readme",
|
|
12
|
+
"bugs": {
|
|
13
|
+
"url": "https://github.com/mandipadk/kavi/issues"
|
|
14
|
+
},
|
|
15
|
+
"publishConfig": {
|
|
16
|
+
"access": "public"
|
|
17
|
+
},
|
|
18
|
+
"bin": "bin/kavi.js",
|
|
19
|
+
"files": [
|
|
20
|
+
"bin",
|
|
21
|
+
"dist",
|
|
22
|
+
"README.md",
|
|
23
|
+
"package.json"
|
|
24
|
+
],
|
|
25
|
+
"scripts": {
|
|
26
|
+
"build": "node ./scripts/build.mjs",
|
|
27
|
+
"dev": "node ./bin/kavi.js",
|
|
28
|
+
"doctor": "node ./bin/kavi.js doctor",
|
|
29
|
+
"init": "node ./bin/kavi.js init",
|
|
30
|
+
"start": "node ./bin/kavi.js start",
|
|
31
|
+
"open": "node ./bin/kavi.js open",
|
|
32
|
+
"resume": "node ./bin/kavi.js resume",
|
|
33
|
+
"status": "node ./bin/kavi.js status",
|
|
34
|
+
"paths": "node ./bin/kavi.js paths",
|
|
35
|
+
"task": "node ./bin/kavi.js task",
|
|
36
|
+
"tasks": "node ./bin/kavi.js tasks",
|
|
37
|
+
"task-output": "node ./bin/kavi.js task-output",
|
|
38
|
+
"approvals": "node ./bin/kavi.js approvals",
|
|
39
|
+
"approve": "node ./bin/kavi.js approve",
|
|
40
|
+
"deny": "node ./bin/kavi.js deny",
|
|
41
|
+
"events": "node ./bin/kavi.js events",
|
|
42
|
+
"stop": "node ./bin/kavi.js stop",
|
|
43
|
+
"land": "node ./bin/kavi.js land",
|
|
44
|
+
"test": "node --experimental-strip-types --test src/**/*.test.ts",
|
|
45
|
+
"release:check": "npm run build && npm test && npm pack --dry-run --cache /tmp/kavi-npm-cache",
|
|
46
|
+
"publish:beta": "npm publish --access public --tag beta",
|
|
47
|
+
"publish:latest": "npm publish --access public",
|
|
48
|
+
"prepublishOnly": "npm run release:check"
|
|
49
|
+
},
|
|
50
|
+
"engines": {
|
|
51
|
+
"node": ">=25.0.0"
|
|
52
|
+
}
|
|
53
|
+
}
|