@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/config.js ADDED
@@ -0,0 +1,175 @@
1
+ import fs from "node:fs/promises";
2
+ import path from "node:path";
3
+ import { ensureDir, fileExists } from "./fs.js";
4
+ import { parseToml } from "./toml.js";
5
+ const DEFAULT_CONFIG = `version = 1
6
+ base_branch = "main"
7
+ validation_command = "npm test"
8
+ message_limit = 6
9
+
10
+ [routing]
11
+ frontend_keywords = ["frontend", "ui", "ux", "design", "copy", "react", "css", "html"]
12
+ backend_keywords = ["backend", "api", "server", "db", "schema", "migration", "auth", "test"]
13
+
14
+ [agents.codex]
15
+ role = "planning-backend"
16
+ model = ""
17
+
18
+ [agents.claude]
19
+ role = "frontend-intent"
20
+ model = ""
21
+ `;
22
+ const DEFAULT_PROMPTS = {
23
+ "codex.md": `You are Codex inside Kavi.
24
+
25
+ Default role:
26
+ - Own planning, architecture, backend, debugging, and review-heavy work.
27
+ - Keep updates compact and task-scoped.
28
+ - Emit peer messages only when they materially help Claude.
29
+ `,
30
+ "claude.md": `You are Claude inside Kavi.
31
+
32
+ Default role:
33
+ - Own frontend, UX, intent-shaping, copy, and product-sense work.
34
+ - Keep updates compact and task-scoped.
35
+ - Emit peer messages only when they materially help Codex.
36
+ `
37
+ };
38
+ const DEFAULT_HOME_CONFIG = `version = 1
39
+
40
+ [runtime]
41
+ node_bin = ""
42
+ codex_bin = "codex"
43
+ claude_bin = "claude"
44
+ `;
45
+ function asString(value, fallback) {
46
+ return typeof value === "string" ? value : fallback;
47
+ }
48
+ function asNumber(value, fallback) {
49
+ return typeof value === "number" ? value : fallback;
50
+ }
51
+ function asStringArray(value, fallback) {
52
+ return Array.isArray(value) ? value.map((item)=>String(item)) : fallback;
53
+ }
54
+ export function defaultConfig() {
55
+ return {
56
+ version: 1,
57
+ baseBranch: "main",
58
+ validationCommand: "npm test",
59
+ messageLimit: 6,
60
+ routing: {
61
+ frontendKeywords: [
62
+ "frontend",
63
+ "ui",
64
+ "ux",
65
+ "design",
66
+ "copy",
67
+ "react",
68
+ "css",
69
+ "html"
70
+ ],
71
+ backendKeywords: [
72
+ "backend",
73
+ "api",
74
+ "server",
75
+ "db",
76
+ "schema",
77
+ "migration",
78
+ "auth",
79
+ "test"
80
+ ]
81
+ },
82
+ agents: {
83
+ codex: {
84
+ role: "planning-backend",
85
+ model: ""
86
+ },
87
+ claude: {
88
+ role: "frontend-intent",
89
+ model: ""
90
+ }
91
+ }
92
+ };
93
+ }
94
+ export function defaultHomeConfig() {
95
+ return {
96
+ version: 1,
97
+ runtime: {
98
+ nodeBin: "",
99
+ codexBin: "codex",
100
+ claudeBin: "claude"
101
+ }
102
+ };
103
+ }
104
+ export async function ensureProjectScaffold(paths) {
105
+ await ensureDir(paths.kaviDir);
106
+ await ensureDir(paths.promptsDir);
107
+ await ensureDir(paths.stateDir);
108
+ await ensureDir(paths.runtimeDir);
109
+ await ensureDir(paths.runsDir);
110
+ if (!await fileExists(paths.configFile)) {
111
+ await fs.writeFile(paths.configFile, DEFAULT_CONFIG, "utf8");
112
+ }
113
+ for (const [fileName, content] of Object.entries(DEFAULT_PROMPTS)){
114
+ const promptPath = path.join(paths.promptsDir, fileName);
115
+ if (!await fileExists(promptPath)) {
116
+ await fs.writeFile(promptPath, content, "utf8");
117
+ }
118
+ }
119
+ }
120
+ export async function ensureHomeConfig(paths) {
121
+ await ensureDir(paths.homeConfigDir);
122
+ if (!await fileExists(paths.homeConfigFile)) {
123
+ await fs.writeFile(paths.homeConfigFile, DEFAULT_HOME_CONFIG, "utf8");
124
+ }
125
+ }
126
+ export async function loadConfig(paths) {
127
+ if (!await fileExists(paths.configFile)) {
128
+ return defaultConfig();
129
+ }
130
+ const content = await fs.readFile(paths.configFile, "utf8");
131
+ const parsed = parseToml(content);
132
+ const routing = parsed.routing ?? {};
133
+ const agents = parsed.agents ?? {};
134
+ const codex = agents.codex ?? {};
135
+ const claude = agents.claude ?? {};
136
+ return {
137
+ version: asNumber(parsed.version, 1),
138
+ baseBranch: asString(parsed.base_branch, "main"),
139
+ validationCommand: asString(parsed.validation_command, "npm test"),
140
+ messageLimit: asNumber(parsed.message_limit, 6),
141
+ routing: {
142
+ frontendKeywords: asStringArray(routing.frontend_keywords, defaultConfig().routing.frontendKeywords),
143
+ backendKeywords: asStringArray(routing.backend_keywords, defaultConfig().routing.backendKeywords)
144
+ },
145
+ agents: {
146
+ codex: {
147
+ role: asString(codex.role, "planning-backend"),
148
+ model: asString(codex.model, "")
149
+ },
150
+ claude: {
151
+ role: asString(claude.role, "frontend-intent"),
152
+ model: asString(claude.model, "")
153
+ }
154
+ }
155
+ };
156
+ }
157
+ export async function loadHomeConfig(paths) {
158
+ if (!await fileExists(paths.homeConfigFile)) {
159
+ return defaultHomeConfig();
160
+ }
161
+ const content = await fs.readFile(paths.homeConfigFile, "utf8");
162
+ const parsed = parseToml(content);
163
+ const runtime = parsed.runtime ?? {};
164
+ return {
165
+ version: asNumber(parsed.version, 1),
166
+ runtime: {
167
+ nodeBin: asString(runtime.node_bin, ""),
168
+ codexBin: asString(runtime.codex_bin, "codex"),
169
+ claudeBin: asString(runtime.claude_bin, "claude")
170
+ }
171
+ };
172
+ }
173
+
174
+
175
+ //# sourceURL=config.ts
package/dist/daemon.js ADDED
@@ -0,0 +1,202 @@
1
+ import { consumeCommands } from "./command-queue.js";
2
+ import { buildPeerMessages as buildClaudePeerMessages, runClaudeTask } from "./adapters/claude.js";
3
+ import { buildPeerMessages as buildCodexPeerMessages, runCodexTask } from "./adapters/codex.js";
4
+ import { nowIso } from "./paths.js";
5
+ import { buildAdHocTask, buildKickoffTasks } from "./router.js";
6
+ import { loadSessionRecord, recordEvent, saveSessionRecord } from "./session.js";
7
+ import { saveTaskArtifact } from "./task-artifacts.js";
8
+ export class KaviDaemon {
9
+ paths;
10
+ session;
11
+ managedSessionId = "";
12
+ running = false;
13
+ processing = false;
14
+ interval = null;
15
+ stopResolver = null;
16
+ constructor(paths){
17
+ this.paths = paths;
18
+ }
19
+ async start() {
20
+ this.session = await loadSessionRecord(this.paths);
21
+ this.managedSessionId = this.session.id;
22
+ this.session.status = "running";
23
+ this.session.daemonPid = process.pid;
24
+ this.session.daemonHeartbeatAt = new Date().toISOString();
25
+ await saveSessionRecord(this.paths, this.session);
26
+ await recordEvent(this.paths, this.session.id, "daemon.started", {
27
+ daemonPid: process.pid
28
+ });
29
+ this.running = true;
30
+ void this.tick();
31
+ this.interval = setInterval(()=>{
32
+ void this.tick();
33
+ }, 1000);
34
+ await new Promise((resolve)=>{
35
+ this.stopResolver = resolve;
36
+ });
37
+ }
38
+ async tick() {
39
+ if (!this.running || this.processing) {
40
+ return;
41
+ }
42
+ this.processing = true;
43
+ try {
44
+ const onDiskSession = await loadSessionRecord(this.paths);
45
+ if (onDiskSession.id !== this.managedSessionId || onDiskSession.status === "stopped") {
46
+ this.running = false;
47
+ if (this.interval) {
48
+ clearInterval(this.interval);
49
+ this.interval = null;
50
+ }
51
+ this.stopResolver?.();
52
+ return;
53
+ }
54
+ this.session = onDiskSession;
55
+ this.session.daemonPid = process.pid;
56
+ this.session.daemonHeartbeatAt = new Date().toISOString();
57
+ await saveSessionRecord(this.paths, this.session);
58
+ await this.consumeQueuedCommands();
59
+ if (this.session.tasks.length === 0 && this.session.goal) {
60
+ this.session.tasks = buildKickoffTasks(this.session.goal);
61
+ await saveSessionRecord(this.paths, this.session);
62
+ await recordEvent(this.paths, this.session.id, "tasks.kickoff_created", {
63
+ count: this.session.tasks.length
64
+ });
65
+ }
66
+ const pending = this.session.tasks.filter((task)=>task.status === "pending");
67
+ for (const task of pending){
68
+ await this.runTask(task);
69
+ }
70
+ } finally{
71
+ this.processing = false;
72
+ }
73
+ }
74
+ async consumeQueuedCommands() {
75
+ const commands = await consumeCommands(this.paths);
76
+ if (commands.length === 0) {
77
+ return;
78
+ }
79
+ for (const command of commands){
80
+ if (command.type === "shutdown") {
81
+ this.session.status = "stopped";
82
+ this.running = false;
83
+ this.session.daemonHeartbeatAt = new Date().toISOString();
84
+ await saveSessionRecord(this.paths, this.session);
85
+ await recordEvent(this.paths, this.session.id, "daemon.stopped", {});
86
+ if (this.interval) {
87
+ clearInterval(this.interval);
88
+ this.interval = null;
89
+ }
90
+ this.stopResolver?.();
91
+ return;
92
+ }
93
+ if (command.type === "kickoff" && typeof command.payload.prompt === "string") {
94
+ this.session.goal = command.payload.prompt;
95
+ this.session.tasks.push(...buildKickoffTasks(command.payload.prompt));
96
+ await saveSessionRecord(this.paths, this.session);
97
+ await recordEvent(this.paths, this.session.id, "tasks.kickoff_enqueued", {
98
+ count: 2
99
+ });
100
+ continue;
101
+ }
102
+ if (command.type === "enqueue" && typeof command.payload.prompt === "string") {
103
+ const owner = command.payload.owner === "claude" ? "claude" : "codex";
104
+ this.session.tasks.push(buildAdHocTask(owner, command.payload.prompt, `task-${command.id}`));
105
+ await saveSessionRecord(this.paths, this.session);
106
+ await recordEvent(this.paths, this.session.id, "task.enqueued", {
107
+ owner
108
+ });
109
+ }
110
+ }
111
+ }
112
+ async runTask(task) {
113
+ const startedAt = nowIso();
114
+ task.status = "running";
115
+ task.updatedAt = startedAt;
116
+ await saveSessionRecord(this.paths, this.session);
117
+ await recordEvent(this.paths, this.session.id, "task.started", {
118
+ taskId: task.id,
119
+ owner: task.owner
120
+ });
121
+ try {
122
+ let envelope;
123
+ let peerMessages;
124
+ let rawOutput = null;
125
+ if (task.owner === "codex") {
126
+ const result = await runCodexTask(this.session, task, this.paths);
127
+ envelope = result.envelope;
128
+ rawOutput = result.raw;
129
+ peerMessages = buildCodexPeerMessages(result.envelope, "codex", task.id);
130
+ await this.markAgent("codex", result.envelope.summary, 0, null);
131
+ } else if (task.owner === "claude") {
132
+ const result = await runClaudeTask(this.session, task, this.paths);
133
+ envelope = result.envelope;
134
+ rawOutput = result.raw;
135
+ peerMessages = buildClaudePeerMessages(result.envelope, "claude", task.id);
136
+ await this.markAgent("claude", result.envelope.summary, 0, `${this.session.id}-claude`);
137
+ } else {
138
+ throw new Error(`Unsupported task owner ${task.owner}.`);
139
+ }
140
+ task.status = envelope.status === "completed" ? "completed" : "blocked";
141
+ task.summary = envelope.summary;
142
+ task.updatedAt = new Date().toISOString();
143
+ this.session.peerMessages.push(...peerMessages);
144
+ await saveSessionRecord(this.paths, this.session);
145
+ await saveTaskArtifact(this.paths, {
146
+ taskId: task.id,
147
+ sessionId: this.session.id,
148
+ title: task.title,
149
+ owner: task.owner,
150
+ status: task.status,
151
+ summary: task.summary,
152
+ rawOutput,
153
+ error: null,
154
+ envelope,
155
+ startedAt,
156
+ finishedAt: task.updatedAt
157
+ });
158
+ await recordEvent(this.paths, this.session.id, "task.completed", {
159
+ taskId: task.id,
160
+ owner: task.owner,
161
+ status: task.status,
162
+ peerMessages: peerMessages.length
163
+ });
164
+ } catch (error) {
165
+ task.status = "failed";
166
+ task.summary = error instanceof Error ? error.message : String(error);
167
+ task.updatedAt = new Date().toISOString();
168
+ await this.markAgent(task.owner, task.summary, 1, task.owner === "claude" ? `${this.session.id}-claude` : null);
169
+ await saveSessionRecord(this.paths, this.session);
170
+ await saveTaskArtifact(this.paths, {
171
+ taskId: task.id,
172
+ sessionId: this.session.id,
173
+ title: task.title,
174
+ owner: task.owner,
175
+ status: task.status,
176
+ summary: task.summary,
177
+ rawOutput: null,
178
+ error: task.summary,
179
+ envelope: null,
180
+ startedAt,
181
+ finishedAt: task.updatedAt
182
+ });
183
+ await recordEvent(this.paths, this.session.id, "task.failed", {
184
+ taskId: task.id,
185
+ owner: task.owner,
186
+ error: task.summary
187
+ });
188
+ }
189
+ }
190
+ async markAgent(agent, summary, exitCode, sessionId) {
191
+ this.session.agentStatus[agent] = {
192
+ ...this.session.agentStatus[agent],
193
+ lastRunAt: new Date().toISOString(),
194
+ lastExitCode: exitCode,
195
+ sessionId,
196
+ summary
197
+ };
198
+ }
199
+ }
200
+
201
+
202
+ //# sourceURL=daemon.ts
package/dist/doctor.js ADDED
@@ -0,0 +1,78 @@
1
+ import { fileExists } from "./fs.js";
2
+ import { runCommand } from "./process.js";
3
+ import { hasSupportedNode, minimumNodeMajor, resolveSessionRuntime } from "./runtime.js";
4
+ function normalizeVersion(output) {
5
+ return output.trim().split(/\s+/).slice(-1)[0] ?? output.trim();
6
+ }
7
+ export async function runDoctor(repoRoot, paths) {
8
+ const checks = [];
9
+ const runtime = await resolveSessionRuntime(paths);
10
+ const nodeVersion = await runCommand(runtime.nodeExecutable, [
11
+ "--version"
12
+ ], {
13
+ cwd: repoRoot
14
+ });
15
+ checks.push({
16
+ name: "node",
17
+ ok: nodeVersion.code === 0 && hasSupportedNode(nodeVersion.stdout.replace(/^v/, "").trim()),
18
+ detail: nodeVersion.code === 0 ? `${nodeVersion.stdout.trim()} via ${runtime.nodeExecutable} (need >= ${minimumNodeMajor()})` : nodeVersion.stderr.trim()
19
+ });
20
+ const codexVersion = await runCommand(runtime.codexExecutable, [
21
+ "--version"
22
+ ], {
23
+ cwd: repoRoot
24
+ });
25
+ checks.push({
26
+ name: "codex",
27
+ ok: codexVersion.code === 0,
28
+ detail: codexVersion.code === 0 ? `${normalizeVersion(codexVersion.stdout)} via ${runtime.codexExecutable}` : codexVersion.stderr.trim()
29
+ });
30
+ const claudeVersion = await runCommand(runtime.claudeExecutable, [
31
+ "--version"
32
+ ], {
33
+ cwd: repoRoot
34
+ });
35
+ checks.push({
36
+ name: "claude",
37
+ ok: claudeVersion.code === 0,
38
+ detail: claudeVersion.code === 0 ? `${claudeVersion.stdout.trim()} via ${runtime.claudeExecutable}` : claudeVersion.stderr.trim()
39
+ });
40
+ const worktreeCheck = await runCommand("git", [
41
+ "worktree",
42
+ "list"
43
+ ], {
44
+ cwd: repoRoot
45
+ });
46
+ checks.push({
47
+ name: "git-worktree",
48
+ ok: worktreeCheck.code === 0,
49
+ detail: worktreeCheck.code === 0 ? "available" : worktreeCheck.stderr.trim()
50
+ });
51
+ const appServerCheck = await runCommand(runtime.codexExecutable, [
52
+ "app-server",
53
+ "--help"
54
+ ], {
55
+ cwd: repoRoot
56
+ });
57
+ checks.push({
58
+ name: "codex-app-server",
59
+ ok: appServerCheck.code === 0,
60
+ detail: appServerCheck.code === 0 ? "supported" : appServerCheck.stderr.trim()
61
+ });
62
+ const authCheck = await fileExists(`${process.env.HOME}/.codex/auth.json`);
63
+ checks.push({
64
+ name: "codex-auth-file",
65
+ ok: authCheck,
66
+ detail: authCheck ? "present" : "missing ~/.codex/auth.json"
67
+ });
68
+ const homeConfigCheck = await fileExists(paths.homeConfigFile);
69
+ checks.push({
70
+ name: "home-config",
71
+ ok: true,
72
+ detail: homeConfigCheck ? "present" : "will be created on demand"
73
+ });
74
+ return checks;
75
+ }
76
+
77
+
78
+ //# sourceURL=doctor.ts
package/dist/fs.js ADDED
@@ -0,0 +1,30 @@
1
+ import fs from "node:fs/promises";
2
+ import path from "node:path";
3
+ export async function ensureDir(dirPath) {
4
+ await fs.mkdir(dirPath, {
5
+ recursive: true
6
+ });
7
+ }
8
+ export async function fileExists(filePath) {
9
+ try {
10
+ await fs.access(filePath);
11
+ return true;
12
+ } catch {
13
+ return false;
14
+ }
15
+ }
16
+ export async function writeJson(filePath, value) {
17
+ await ensureDir(path.dirname(filePath));
18
+ await fs.writeFile(filePath, `${JSON.stringify(value, null, 2)}\n`, "utf8");
19
+ }
20
+ export async function readJson(filePath) {
21
+ const content = await fs.readFile(filePath, "utf8");
22
+ return JSON.parse(content);
23
+ }
24
+ export async function appendEvent(filePath, event) {
25
+ await ensureDir(path.dirname(filePath));
26
+ await fs.appendFile(filePath, `${JSON.stringify(event)}\n`, "utf8");
27
+ }
28
+
29
+
30
+ //# sourceURL=fs.ts