@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 +12 -3
- package/dist/adapters/claude.js +5 -5
- package/dist/adapters/codex.js +235 -30
- package/dist/adapters/shared.js +19 -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 +80 -2
- package/dist/decision-ledger.js +75 -0
- package/dist/git.js +74 -0
- package/dist/main.js +109 -8
- package/dist/prompts.js +13 -0
- package/dist/router.js +190 -5
- package/dist/runtime.js +4 -1
- package/dist/session.js +10 -1
- package/dist/tui.js +15 -2
- package/package.json +4 -3
package/README.md
CHANGED
|
@@ -44,7 +44,8 @@ node --experimental-strip-types --test src/**/*.test.ts
|
|
|
44
44
|
```
|
|
45
45
|
|
|
46
46
|
Notes:
|
|
47
|
-
-
|
|
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
|
-
#
|
|
77
|
+
# prompt for version and publish beta explicitly
|
|
73
78
|
npm run publish:beta
|
|
74
79
|
|
|
75
|
-
#
|
|
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:
|
package/dist/adapters/claude.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import fs from "node:fs/promises";
|
|
2
|
-
import {
|
|
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
|
-
|
|
96
|
+
buildAgentInstructions("claude", worktree.path, repoPrompt),
|
|
95
97
|
"",
|
|
96
|
-
|
|
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",
|
package/dist/adapters/codex.js
CHANGED
|
@@ -1,7 +1,82 @@
|
|
|
1
|
-
import
|
|
2
|
-
import
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
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
|
|
15
|
-
const
|
|
16
|
-
|
|
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
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
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
|
|
package/dist/adapters/shared.js
CHANGED
|
@@ -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 =
|
|
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(),
|