@mandipadk7/kavi 0.1.0 → 0.1.1

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 CHANGED
@@ -44,7 +44,8 @@ node --experimental-strip-types --test src/**/*.test.ts
44
44
  ```
45
45
 
46
46
  Notes:
47
- - The current managed task runners use the installed `codex` and `claude` CLIs directly.
47
+ - 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
+ - Claude still runs through the installed `claude` CLI with Kavi-managed hooks and approval decisions.
48
49
  - The dashboard uses a file-backed control loop instead of local sockets so it can run in restricted shells and sandboxes.
49
50
 
50
51
  Local install options:
@@ -63,17 +64,24 @@ npm link
63
64
  Publishing for testers:
64
65
 
65
66
  ```bash
67
+ # interactive publish flow: prompts for version, defaults to the next patch,
68
+ # runs release checks, then publishes the beta tag
69
+ npm run publish
70
+
66
71
  # authenticate once
67
72
  npm login
68
73
 
69
74
  # verify the package before publish
70
75
  npm run release:check
71
76
 
72
- # publish a beta that friends can install immediately
77
+ # prompt for version and publish beta explicitly
73
78
  npm run publish:beta
74
79
 
75
- # later, publish the stable tag
80
+ # prompt for version and publish stable
76
81
  npm run publish:latest
82
+
83
+ # test the publish flow without sending anything to npm
84
+ npm run publish -- --dry-run
77
85
  ```
78
86
 
79
87
  Install commands for testers:
@@ -89,6 +97,7 @@ npx @mandipadk7/kavi@beta help
89
97
  Notes on publish:
90
98
  - The package name is scoped as `@mandipadk7/kavi` to match the npm user `mandipadk7`.
91
99
  - 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
+ - Hook commands now invoke the compiled entrypoint directly when `dist/` is present, and only use `--experimental-strip-types` in source mode.
92
101
  - `prepublishOnly` runs the release checks automatically during publish.
93
102
 
94
103
  User-local config example:
@@ -1,6 +1,7 @@
1
1
  import fs from "node:fs/promises";
2
- import { buildEnvelopeInstruction, buildPeerMessages, buildSharedContext, extractJsonObject } from "./shared.js";
2
+ import { buildAgentInstructions, buildPeerMessages, buildTaskPrompt, extractJsonObject } from "./shared.js";
3
3
  import { runCommand } from "../process.js";
4
+ import { loadAgentPrompt } from "../prompts.js";
4
5
  import { buildKaviShellCommand } from "../runtime.js";
5
6
  function findWorktree(session, agent) {
6
7
  const worktree = session.worktrees.find((item)=>item.agent === agent);
@@ -90,12 +91,11 @@ export async function runClaudeTask(session, task, paths) {
90
91
  const worktree = findWorktree(session, "claude");
91
92
  const claudeSessionId = `${session.id}-claude`;
92
93
  await writeClaudeSettings(paths, session);
94
+ const repoPrompt = await loadAgentPrompt(paths, "claude");
93
95
  const prompt = [
94
- buildSharedContext(session, task, "claude"),
96
+ buildAgentInstructions("claude", worktree.path, repoPrompt),
95
97
  "",
96
- `User goal or prompt:\n${task.prompt}`,
97
- "",
98
- buildEnvelopeInstruction("claude", worktree.path)
98
+ buildTaskPrompt(session, task, "claude")
99
99
  ].join("\n");
100
100
  const result = await runCommand(session.runtime.claudeExecutable, [
101
101
  "-p",
@@ -1,7 +1,82 @@
1
- import path from "node:path";
2
- import fs from "node:fs/promises";
3
- import { runCommand } from "../process.js";
4
- import { buildEnvelopeInstruction, buildPeerMessages, buildSharedContext, extractJsonObject } from "./shared.js";
1
+ import { createApprovalRequest, describeCodexApprovalRequest, findApprovalRule, waitForApprovalDecision } from "../approvals.js";
2
+ import { CodexAppServerClient } from "../codex-app-server.js";
3
+ import { loadAgentPrompt } from "../prompts.js";
4
+ import { recordEvent } from "../session.js";
5
+ import { buildAgentInstructions, buildPeerMessages, buildTaskPrompt, extractJsonObject } from "./shared.js";
6
+ const ENVELOPE_OUTPUT_SCHEMA = {
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
+ };
5
80
  function findWorktree(session, agent) {
6
81
  const worktree = session.worktrees.find((item)=>item.agent === agent);
7
82
  if (!worktree) {
@@ -9,35 +84,165 @@ function findWorktree(session, agent) {
9
84
  }
10
85
  return worktree;
11
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 supportsSessionApproval(params) {
94
+ return Array.isArray(params.availableDecisions) ? params.availableDecisions.some((value)=>value === "acceptForSession") : false;
95
+ }
96
+ function buildPermissionsGrant(params) {
97
+ const requested = asObject(params.permissions);
98
+ const granted = {};
99
+ if (requested.network !== null && requested.network !== undefined) {
100
+ granted.network = requested.network;
101
+ }
102
+ if (requested.fileSystem !== null && requested.fileSystem !== undefined) {
103
+ granted.fileSystem = requested.fileSystem;
104
+ }
105
+ return granted;
106
+ }
107
+ function buildApprovalResponse(method, params, approved, remember) {
108
+ switch(method){
109
+ case "item/commandExecution/requestApproval":
110
+ return {
111
+ decision: approved ? remember && supportsSessionApproval(params) ? "acceptForSession" : "accept" : "decline"
112
+ };
113
+ case "item/fileChange/requestApproval":
114
+ return {
115
+ decision: approved ? remember ? "acceptForSession" : "accept" : "decline"
116
+ };
117
+ case "item/permissions/requestApproval":
118
+ return {
119
+ permissions: approved ? buildPermissionsGrant(params) : {},
120
+ scope: remember ? "session" : "turn"
121
+ };
122
+ case "execCommandApproval":
123
+ case "applyPatchApproval":
124
+ return {
125
+ decision: approved ? remember ? "approved_for_session" : "approved" : "denied"
126
+ };
127
+ default:
128
+ throw new Error(`Unsupported Codex approval request: ${method}`);
129
+ }
130
+ }
131
+ function buildThreadParams(session, worktree, developerInstructions) {
132
+ const configuredModel = session.config.agents.codex.model.trim();
133
+ return {
134
+ cwd: worktree.path,
135
+ approvalPolicy: "on-request",
136
+ approvalsReviewer: "user",
137
+ sandbox: "workspace-write",
138
+ baseInstructions: "You are Codex inside Kavi. Operate inside the assigned worktree and keep work task-scoped.",
139
+ developerInstructions,
140
+ model: configuredModel || null,
141
+ ephemeral: false,
142
+ experimentalRawEvents: false,
143
+ persistExtendedHistory: true
144
+ };
145
+ }
146
+ async function ensureThread(client, session, paths, worktree, developerInstructions) {
147
+ const threadParams = buildThreadParams(session, worktree, developerInstructions);
148
+ const existingThreadId = session.agentStatus.codex.sessionId;
149
+ if (existingThreadId) {
150
+ try {
151
+ return await client.resumeThread({
152
+ threadId: existingThreadId,
153
+ ...threadParams
154
+ });
155
+ } catch (error) {
156
+ await recordEvent(paths, session.id, "codex.thread_resume_failed", {
157
+ threadId: existingThreadId,
158
+ error: error instanceof Error ? error.message : String(error)
159
+ });
160
+ }
161
+ }
162
+ return await client.startThread(threadParams);
163
+ }
164
+ async function handleCodexApproval(session, paths, request) {
165
+ const descriptor = describeCodexApprovalRequest(request.method, request.params);
166
+ const rule = await findApprovalRule(paths, {
167
+ repoRoot: session.repoRoot,
168
+ agent: "codex",
169
+ toolName: descriptor.toolName,
170
+ matchKey: descriptor.matchKey
171
+ });
172
+ if (rule) {
173
+ await recordEvent(paths, session.id, "approval.auto_decided", {
174
+ agent: "codex",
175
+ requestId: request.id,
176
+ method: request.method,
177
+ toolName: descriptor.toolName,
178
+ summary: descriptor.summary,
179
+ decision: rule.decision
180
+ });
181
+ return buildApprovalResponse(request.method, request.params, rule.decision === "allow", true);
182
+ }
183
+ const approval = await createApprovalRequest(paths, {
184
+ sessionId: session.id,
185
+ repoRoot: session.repoRoot,
186
+ agent: "codex",
187
+ hookEvent: request.method,
188
+ payload: request.params,
189
+ toolName: descriptor.toolName,
190
+ summary: descriptor.summary,
191
+ matchKey: descriptor.matchKey
192
+ });
193
+ await recordEvent(paths, session.id, "approval.requested", {
194
+ requestId: approval.id,
195
+ agent: "codex",
196
+ method: request.method,
197
+ toolName: approval.toolName,
198
+ summary: approval.summary
199
+ });
200
+ const resolved = await waitForApprovalDecision(paths, approval.id);
201
+ const approved = resolved?.status === "approved";
202
+ const remember = resolved?.remember ?? false;
203
+ await recordEvent(paths, session.id, "approval.completed", {
204
+ requestId: approval.id,
205
+ agent: "codex",
206
+ method: request.method,
207
+ outcome: approved ? "approved" : resolved?.status === "denied" ? "denied" : "expired"
208
+ });
209
+ return buildApprovalResponse(request.method, request.params, approved, remember);
210
+ }
12
211
  export async function runCodexTask(session, task, paths) {
13
212
  const worktree = findWorktree(session, "codex");
14
- const outputFile = path.join(paths.runtimeDir, `codex-${task.id}.txt`);
15
- const prompt = [
16
- buildSharedContext(session, task, "codex"),
17
- "",
18
- `User goal or prompt:\n${task.prompt}`,
19
- "",
20
- buildEnvelopeInstruction("codex", worktree.path)
21
- ].join("\n");
22
- const result = await runCommand(session.runtime.codexExecutable, [
23
- "exec",
24
- "--skip-git-repo-check",
25
- "--cd",
26
- worktree.path,
27
- "--sandbox",
28
- "workspace-write",
29
- "--output-last-message",
30
- outputFile,
31
- prompt
32
- ], {
33
- cwd: session.repoRoot
213
+ const repoPrompt = await loadAgentPrompt(paths, "codex");
214
+ const developerInstructions = buildAgentInstructions("codex", worktree.path, repoPrompt);
215
+ const client = new CodexAppServerClient(session.runtime, session.repoRoot, async (request)=>{
216
+ return await handleCodexApproval(session, paths, request);
34
217
  });
35
- const rawOutput = result.code === 0 ? await fs.readFile(outputFile, "utf8") : `${result.stdout}\n${result.stderr}`;
36
- const envelope = extractJsonObject(rawOutput);
37
- return {
38
- envelope,
39
- raw: rawOutput
40
- };
218
+ try {
219
+ await client.initialize();
220
+ const threadId = await ensureThread(client, session, paths, worktree, developerInstructions);
221
+ const result = await client.runTurn({
222
+ threadId,
223
+ cwd: worktree.path,
224
+ approvalPolicy: "on-request",
225
+ approvalsReviewer: "user",
226
+ model: session.config.agents.codex.model.trim() || null,
227
+ outputSchema: ENVELOPE_OUTPUT_SCHEMA,
228
+ input: [
229
+ {
230
+ type: "text",
231
+ text: buildTaskPrompt(session, task, "codex"),
232
+ text_elements: []
233
+ }
234
+ ]
235
+ });
236
+ const rawOutput = `${result.assistantMessage}${result.stderr ? `\n\n[stderr]\n${result.stderr}` : ""}`;
237
+ const envelope = extractJsonObject(result.assistantMessage);
238
+ return {
239
+ envelope,
240
+ raw: rawOutput,
241
+ threadId
242
+ };
243
+ } finally{
244
+ await client.close();
245
+ }
41
246
  }
42
247
  export { buildPeerMessages };
43
248
 
@@ -36,15 +36,28 @@ export function buildPeerMessages(envelope, from, taskId) {
36
36
  export function buildSharedContext(session, task, agent) {
37
37
  const inbox = session.peerMessages.filter((message)=>message.to === agent).slice(-session.config.messageLimit).map((message)=>`- [${message.intent}] ${message.subject}: ${message.body}`).join("\n");
38
38
  const tasks = session.tasks.map((item)=>`- ${item.id} | ${item.owner} | ${item.status} | ${item.title}`).join("\n");
39
+ const decisions = session.decisions.slice(-6).map((decision)=>`- [${decision.kind}] ${decision.summary}`).join("\n");
40
+ const claims = session.pathClaims.filter((claim)=>claim.status === "active").slice(-6).map((claim)=>`- ${claim.agent} | ${claim.paths.join(", ")}`).join("\n");
39
41
  return [
40
42
  `Session goal: ${session.goal ?? "No goal recorded."}`,
41
43
  `Current task: ${task.title}`,
42
44
  "Task board:",
43
45
  tasks || "- none",
46
+ "Decision ledger:",
47
+ decisions || "- empty",
48
+ "Active path claims:",
49
+ claims || "- empty",
44
50
  `Peer inbox for ${agent}:`,
45
51
  inbox || "- empty"
46
52
  ].join("\n");
47
53
  }
54
+ export function buildTaskPrompt(session, task, agent) {
55
+ return [
56
+ buildSharedContext(session, task, agent),
57
+ "",
58
+ `User goal or prompt:\n${task.prompt}`
59
+ ].join("\n");
60
+ }
48
61
  export function buildEnvelopeInstruction(agent, worktreePath) {
49
62
  const peer = agent === "codex" ? "claude" : "codex";
50
63
  const focus = agent === "codex" ? "Focus on planning, architecture, backend concerns, codebase structure, and implementation risks." : "Focus on intent, user experience, frontend structure, and interaction quality.";
@@ -64,6 +77,12 @@ export function buildEnvelopeInstruction(agent, worktreePath) {
64
77
  "Do not wrap the JSON in Markdown."
65
78
  ].join("\n");
66
79
  }
80
+ export function buildAgentInstructions(agent, worktreePath, repoPrompt) {
81
+ return [
82
+ repoPrompt.trim(),
83
+ buildEnvelopeInstruction(agent, worktreePath)
84
+ ].filter(Boolean).join("\n\n");
85
+ }
67
86
 
68
87
 
69
88
  //# sourceURL=adapters/shared.ts
package/dist/approvals.js CHANGED
@@ -59,6 +59,73 @@ export function describeToolUse(payload) {
59
59
  matchKey: `${toolName}:${normalized.toLowerCase()}`
60
60
  };
61
61
  }
62
+ export function describeCodexApprovalRequest(method, params) {
63
+ const reason = readString(params, "reason") ?? "";
64
+ switch(method){
65
+ case "item/commandExecution/requestApproval":
66
+ {
67
+ const command = normalizeWhitespace(readString(params, "command") ?? "");
68
+ const detail = command || normalizeWhitespace(reason) || safeJson(params);
69
+ return {
70
+ toolName: "CommandExecution",
71
+ summary: `CommandExecution: ${truncate(detail || "(no details)", 140)}`,
72
+ matchKey: `CommandExecution:${detail.toLowerCase()}`
73
+ };
74
+ }
75
+ case "item/fileChange/requestApproval":
76
+ {
77
+ const grantRoot = normalizeWhitespace(readString(params, "grantRoot") ?? "");
78
+ const detail = grantRoot || normalizeWhitespace(reason) || safeJson(params);
79
+ return {
80
+ toolName: "FileChange",
81
+ summary: `FileChange: ${truncate(detail || "(no details)", 140)}`,
82
+ matchKey: `FileChange:${detail.toLowerCase()}`
83
+ };
84
+ }
85
+ case "item/permissions/requestApproval":
86
+ {
87
+ const permissions = readObject(params, "permissions");
88
+ const detail = normalizeWhitespace([
89
+ reason,
90
+ safeJson(permissions)
91
+ ].filter(Boolean).join(" "));
92
+ return {
93
+ toolName: "Permissions",
94
+ summary: `Permissions: ${truncate(detail || "(no details)", 140)}`,
95
+ matchKey: `Permissions:${detail.toLowerCase()}`
96
+ };
97
+ }
98
+ case "execCommandApproval":
99
+ {
100
+ const command = Array.isArray(params.command) ? normalizeWhitespace(params.command.map((part)=>String(part)).join(" ")) : normalizeWhitespace(readString(params, "command") ?? "");
101
+ const detail = command || normalizeWhitespace(reason) || safeJson(params);
102
+ return {
103
+ toolName: "ExecCommand",
104
+ summary: `ExecCommand: ${truncate(detail || "(no details)", 140)}`,
105
+ matchKey: `ExecCommand:${detail.toLowerCase()}`
106
+ };
107
+ }
108
+ case "applyPatchApproval":
109
+ {
110
+ const grantRoot = normalizeWhitespace(readString(params, "grantRoot") ?? "");
111
+ const detail = grantRoot || normalizeWhitespace(reason) || safeJson(params);
112
+ return {
113
+ toolName: "ApplyPatch",
114
+ summary: `ApplyPatch: ${truncate(detail || "(no details)", 140)}`,
115
+ matchKey: `ApplyPatch:${detail.toLowerCase()}`
116
+ };
117
+ }
118
+ default:
119
+ {
120
+ const detail = normalizeWhitespace(reason) || safeJson(params);
121
+ return {
122
+ toolName: method,
123
+ summary: `${method}: ${truncate(detail || "(no details)", 140)}`,
124
+ matchKey: `${method}:${detail.toLowerCase()}`
125
+ };
126
+ }
127
+ }
128
+ }
62
129
  async function loadRequests(paths) {
63
130
  if (!await fileExists(paths.approvalsFile)) {
64
131
  return [];
@@ -74,7 +141,11 @@ export async function listApprovalRequests(paths, options = {}) {
74
141
  return filtered.sort((left, right)=>left.createdAt.localeCompare(right.createdAt));
75
142
  }
76
143
  export async function createApprovalRequest(paths, input) {
77
- const descriptor = describeToolUse(input.payload);
144
+ const descriptor = input.toolName && input.summary && input.matchKey ? {
145
+ toolName: input.toolName,
146
+ summary: input.summary,
147
+ matchKey: input.matchKey
148
+ } : describeToolUse(input.payload);
78
149
  const timestamp = nowIso();
79
150
  const request = {
80
151
  id: randomUUID(),