@mandipadk7/kavi 0.1.0 → 0.1.2
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 +25 -7
- package/dist/adapters/claude.js +136 -13
- package/dist/adapters/codex.js +235 -30
- package/dist/adapters/shared.js +35 -0
- package/dist/approvals.js +72 -1
- package/dist/codex-app-server.js +310 -0
- package/dist/command-queue.js +1 -0
- package/dist/daemon.js +446 -5
- package/dist/decision-ledger.js +75 -0
- package/dist/git.js +171 -0
- package/dist/main.js +251 -36
- package/dist/paths.js +1 -1
- package/dist/prompts.js +13 -0
- package/dist/router.js +190 -5
- package/dist/rpc.js +226 -0
- package/dist/runtime.js +4 -1
- package/dist/session.js +10 -1
- package/dist/task-artifacts.js +10 -2
- package/dist/tui.js +1653 -72
- package/package.json +7 -12
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 =
|
|
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(),
|
|
@@ -0,0 +1,310 @@
|
|
|
1
|
+
import { spawn } from "node:child_process";
|
|
2
|
+
function asObject(value) {
|
|
3
|
+
return value && typeof value === "object" && !Array.isArray(value) ? value : {};
|
|
4
|
+
}
|
|
5
|
+
function asString(value) {
|
|
6
|
+
return typeof value === "string" ? value : null;
|
|
7
|
+
}
|
|
8
|
+
function formatRpcError(method, error) {
|
|
9
|
+
const errorObject = asObject(error);
|
|
10
|
+
const message = asString(errorObject.message);
|
|
11
|
+
return new Error(message ? `${method} failed: ${message}` : `${method} failed.`);
|
|
12
|
+
}
|
|
13
|
+
function buildTurnError(completion) {
|
|
14
|
+
const message = completion.turn.error?.message ?? `Codex turn ${completion.turn.id} failed.`;
|
|
15
|
+
const details = completion.turn.error?.additionalDetails?.trim();
|
|
16
|
+
return new Error(details ? `${message}\n${details}` : message);
|
|
17
|
+
}
|
|
18
|
+
export class CodexAppServerClient {
|
|
19
|
+
child;
|
|
20
|
+
pending = new Map();
|
|
21
|
+
bufferedMessages = new Map();
|
|
22
|
+
completedTurns = new Map();
|
|
23
|
+
turnResolvers = new Map();
|
|
24
|
+
onRequest;
|
|
25
|
+
nextId = 0;
|
|
26
|
+
stdoutBuffer = "";
|
|
27
|
+
stderrBuffer = "";
|
|
28
|
+
closed = false;
|
|
29
|
+
closePromise = null;
|
|
30
|
+
constructor(runtime, cwd, onRequest){
|
|
31
|
+
this.onRequest = onRequest;
|
|
32
|
+
this.child = spawn(runtime.codexExecutable, [
|
|
33
|
+
"app-server",
|
|
34
|
+
"--listen",
|
|
35
|
+
"stdio://",
|
|
36
|
+
"--session-source",
|
|
37
|
+
"cli"
|
|
38
|
+
], {
|
|
39
|
+
cwd,
|
|
40
|
+
stdio: [
|
|
41
|
+
"pipe",
|
|
42
|
+
"pipe",
|
|
43
|
+
"pipe"
|
|
44
|
+
]
|
|
45
|
+
});
|
|
46
|
+
this.child.stdout.setEncoding("utf8");
|
|
47
|
+
this.child.stderr.setEncoding("utf8");
|
|
48
|
+
this.child.stdout.on("data", (chunk)=>{
|
|
49
|
+
this.handleStdout(chunk);
|
|
50
|
+
});
|
|
51
|
+
this.child.stderr.on("data", (chunk)=>{
|
|
52
|
+
this.stderrBuffer += chunk;
|
|
53
|
+
});
|
|
54
|
+
this.child.on("error", (error)=>{
|
|
55
|
+
this.rejectAll(error);
|
|
56
|
+
});
|
|
57
|
+
this.child.on("close", (code, signal)=>{
|
|
58
|
+
if (!this.closed) {
|
|
59
|
+
this.closed = true;
|
|
60
|
+
}
|
|
61
|
+
const suffix = this.stderrBuffer.trim();
|
|
62
|
+
const reason = new Error(`Codex app-server exited before the turn completed (code=${code ?? "null"}, signal=${signal ?? "null"})${suffix ? `\n${suffix}` : ""}`);
|
|
63
|
+
this.rejectAll(reason);
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
async initialize() {
|
|
67
|
+
await this.request("initialize", {
|
|
68
|
+
clientInfo: {
|
|
69
|
+
name: "kavi",
|
|
70
|
+
title: "Kavi",
|
|
71
|
+
version: "0.1.0"
|
|
72
|
+
},
|
|
73
|
+
capabilities: {
|
|
74
|
+
experimentalApi: true
|
|
75
|
+
}
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
async startThread(params) {
|
|
79
|
+
const response = asObject(await this.request("thread/start", params));
|
|
80
|
+
const thread = asObject(response.thread);
|
|
81
|
+
const threadId = asString(thread.id);
|
|
82
|
+
if (!threadId) {
|
|
83
|
+
throw new Error("Codex thread/start did not return a thread id.");
|
|
84
|
+
}
|
|
85
|
+
return threadId;
|
|
86
|
+
}
|
|
87
|
+
async resumeThread(params) {
|
|
88
|
+
const response = asObject(await this.request("thread/resume", params));
|
|
89
|
+
const thread = asObject(response.thread);
|
|
90
|
+
const threadId = asString(thread.id);
|
|
91
|
+
if (!threadId) {
|
|
92
|
+
throw new Error("Codex thread/resume did not return a thread id.");
|
|
93
|
+
}
|
|
94
|
+
return threadId;
|
|
95
|
+
}
|
|
96
|
+
async runTurn(params) {
|
|
97
|
+
const response = asObject(await this.request("turn/start", params));
|
|
98
|
+
const turn = asObject(response.turn);
|
|
99
|
+
const turnId = asString(turn.id);
|
|
100
|
+
if (!turnId) {
|
|
101
|
+
throw new Error("Codex turn/start did not return a turn id.");
|
|
102
|
+
}
|
|
103
|
+
const threadId = asString(params.threadId);
|
|
104
|
+
if (!threadId) {
|
|
105
|
+
throw new Error("Codex turn/start requires a thread id.");
|
|
106
|
+
}
|
|
107
|
+
const initialStatus = asString(turn.status);
|
|
108
|
+
if (initialStatus === "completed") {
|
|
109
|
+
return {
|
|
110
|
+
threadId,
|
|
111
|
+
turnId,
|
|
112
|
+
assistantMessage: this.bufferedMessages.get(turnId) ?? "",
|
|
113
|
+
turnStatus: initialStatus,
|
|
114
|
+
stderr: this.stderrBuffer.trim()
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
if (initialStatus === "failed" || initialStatus === "interrupted") {
|
|
118
|
+
throw buildTurnError({
|
|
119
|
+
threadId,
|
|
120
|
+
turn: {
|
|
121
|
+
id: turnId,
|
|
122
|
+
status: initialStatus,
|
|
123
|
+
error: asObject(turn.error)
|
|
124
|
+
}
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
const completion = this.completedTurns.get(turnId) ?? await new Promise((resolve, reject)=>{
|
|
128
|
+
this.turnResolvers.set(turnId, {
|
|
129
|
+
resolve,
|
|
130
|
+
reject
|
|
131
|
+
});
|
|
132
|
+
});
|
|
133
|
+
const turnStatus = completion.turn.status;
|
|
134
|
+
if (turnStatus !== "completed") {
|
|
135
|
+
throw buildTurnError(completion);
|
|
136
|
+
}
|
|
137
|
+
return {
|
|
138
|
+
threadId,
|
|
139
|
+
turnId,
|
|
140
|
+
assistantMessage: this.bufferedMessages.get(turnId) ?? "",
|
|
141
|
+
turnStatus,
|
|
142
|
+
stderr: this.stderrBuffer.trim()
|
|
143
|
+
};
|
|
144
|
+
}
|
|
145
|
+
async close() {
|
|
146
|
+
if (this.closePromise) {
|
|
147
|
+
await this.closePromise;
|
|
148
|
+
return;
|
|
149
|
+
}
|
|
150
|
+
this.closePromise = new Promise((resolve)=>{
|
|
151
|
+
if (this.closed) {
|
|
152
|
+
resolve();
|
|
153
|
+
return;
|
|
154
|
+
}
|
|
155
|
+
this.closed = true;
|
|
156
|
+
const timeout = setTimeout(()=>{
|
|
157
|
+
this.child.kill("SIGTERM");
|
|
158
|
+
}, 1_000);
|
|
159
|
+
this.child.once("close", ()=>{
|
|
160
|
+
clearTimeout(timeout);
|
|
161
|
+
resolve();
|
|
162
|
+
});
|
|
163
|
+
this.child.stdin.end();
|
|
164
|
+
});
|
|
165
|
+
await this.closePromise;
|
|
166
|
+
}
|
|
167
|
+
async request(method, params) {
|
|
168
|
+
const id = ++this.nextId;
|
|
169
|
+
const payload = {
|
|
170
|
+
id,
|
|
171
|
+
method,
|
|
172
|
+
params
|
|
173
|
+
};
|
|
174
|
+
return await new Promise((resolve, reject)=>{
|
|
175
|
+
this.pending.set(id, {
|
|
176
|
+
method,
|
|
177
|
+
resolve,
|
|
178
|
+
reject
|
|
179
|
+
});
|
|
180
|
+
this.child.stdin.write(`${JSON.stringify(payload)}\n`, "utf8", (error)=>{
|
|
181
|
+
if (!error) {
|
|
182
|
+
return;
|
|
183
|
+
}
|
|
184
|
+
this.pending.delete(id);
|
|
185
|
+
reject(error);
|
|
186
|
+
});
|
|
187
|
+
});
|
|
188
|
+
}
|
|
189
|
+
handleStdout(chunk) {
|
|
190
|
+
this.stdoutBuffer += chunk;
|
|
191
|
+
while(true){
|
|
192
|
+
const newlineIndex = this.stdoutBuffer.indexOf("\n");
|
|
193
|
+
if (newlineIndex === -1) {
|
|
194
|
+
return;
|
|
195
|
+
}
|
|
196
|
+
const line = this.stdoutBuffer.slice(0, newlineIndex).trim();
|
|
197
|
+
this.stdoutBuffer = this.stdoutBuffer.slice(newlineIndex + 1);
|
|
198
|
+
if (!line) {
|
|
199
|
+
continue;
|
|
200
|
+
}
|
|
201
|
+
let message;
|
|
202
|
+
try {
|
|
203
|
+
message = JSON.parse(line);
|
|
204
|
+
} catch (error) {
|
|
205
|
+
this.rejectAll(new Error(`Failed to parse Codex app-server output: ${error instanceof Error ? error.message : String(error)}\n${line}`));
|
|
206
|
+
return;
|
|
207
|
+
}
|
|
208
|
+
if (typeof message.id === "number" && typeof message.method === "string") {
|
|
209
|
+
void this.handleServerRequest({
|
|
210
|
+
id: message.id,
|
|
211
|
+
method: message.method,
|
|
212
|
+
params: asObject(message.params)
|
|
213
|
+
});
|
|
214
|
+
continue;
|
|
215
|
+
}
|
|
216
|
+
if (typeof message.id === "number") {
|
|
217
|
+
this.handleResponse(message);
|
|
218
|
+
continue;
|
|
219
|
+
}
|
|
220
|
+
if (typeof message.method === "string") {
|
|
221
|
+
this.handleNotification(message.method, asObject(message.params));
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
handleResponse(message) {
|
|
226
|
+
const pending = this.pending.get(message.id);
|
|
227
|
+
if (!pending) {
|
|
228
|
+
return;
|
|
229
|
+
}
|
|
230
|
+
this.pending.delete(message.id);
|
|
231
|
+
if (message.error) {
|
|
232
|
+
pending.reject(formatRpcError(pending.method, message.error));
|
|
233
|
+
return;
|
|
234
|
+
}
|
|
235
|
+
pending.resolve(message.result);
|
|
236
|
+
}
|
|
237
|
+
handleNotification(method, params) {
|
|
238
|
+
if (method === "item/agentMessage/delta") {
|
|
239
|
+
const turnId = asString(params.turnId);
|
|
240
|
+
const delta = asString(params.delta);
|
|
241
|
+
if (turnId && delta) {
|
|
242
|
+
this.bufferedMessages.set(turnId, `${this.bufferedMessages.get(turnId) ?? ""}${delta}`);
|
|
243
|
+
}
|
|
244
|
+
return;
|
|
245
|
+
}
|
|
246
|
+
if (method === "turn/completed") {
|
|
247
|
+
const turn = asObject(params.turn);
|
|
248
|
+
const turnId = asString(turn.id);
|
|
249
|
+
const threadId = asString(params.threadId);
|
|
250
|
+
const status = asString(turn.status);
|
|
251
|
+
if (!turnId || !threadId || !status) {
|
|
252
|
+
return;
|
|
253
|
+
}
|
|
254
|
+
const completion = {
|
|
255
|
+
threadId,
|
|
256
|
+
turn: {
|
|
257
|
+
id: turnId,
|
|
258
|
+
status,
|
|
259
|
+
error: asObject(turn.error)
|
|
260
|
+
}
|
|
261
|
+
};
|
|
262
|
+
this.completedTurns.set(turnId, completion);
|
|
263
|
+
const resolver = this.turnResolvers.get(turnId);
|
|
264
|
+
if (resolver) {
|
|
265
|
+
this.turnResolvers.delete(turnId);
|
|
266
|
+
resolver.resolve(completion);
|
|
267
|
+
}
|
|
268
|
+
return;
|
|
269
|
+
}
|
|
270
|
+
if (method === "error") {
|
|
271
|
+
const message = asString(params.message) ?? "Codex app-server returned an error notification.";
|
|
272
|
+
this.rejectAll(new Error(message));
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
async handleServerRequest(request) {
|
|
276
|
+
try {
|
|
277
|
+
const result = await this.onRequest(request);
|
|
278
|
+
this.child.stdin.write(`${JSON.stringify({
|
|
279
|
+
id: request.id,
|
|
280
|
+
result
|
|
281
|
+
})}\n`);
|
|
282
|
+
} catch (error) {
|
|
283
|
+
this.child.stdin.write(`${JSON.stringify({
|
|
284
|
+
id: request.id,
|
|
285
|
+
error: {
|
|
286
|
+
message: error instanceof Error ? error.message : String(error)
|
|
287
|
+
}
|
|
288
|
+
})}\n`);
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
rejectAll(error) {
|
|
292
|
+
const pending = [
|
|
293
|
+
...this.pending.values()
|
|
294
|
+
];
|
|
295
|
+
this.pending.clear();
|
|
296
|
+
for (const request of pending){
|
|
297
|
+
request.reject(error);
|
|
298
|
+
}
|
|
299
|
+
const turnResolvers = [
|
|
300
|
+
...this.turnResolvers.values()
|
|
301
|
+
];
|
|
302
|
+
this.turnResolvers.clear();
|
|
303
|
+
for (const turn of turnResolvers){
|
|
304
|
+
turn.reject(error);
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
|
|
310
|
+
//# sourceURL=codex-app-server.ts
|
package/dist/command-queue.js
CHANGED
|
@@ -11,6 +11,7 @@ export async function appendCommand(paths, type, payload) {
|
|
|
11
11
|
};
|
|
12
12
|
await ensureDir(paths.runtimeDir);
|
|
13
13
|
await fs.appendFile(paths.commandsFile, `${JSON.stringify(command)}\n`, "utf8");
|
|
14
|
+
return command;
|
|
14
15
|
}
|
|
15
16
|
export async function consumeCommands(paths) {
|
|
16
17
|
if (!await fileExists(paths.commandsFile)) {
|