@doingdev/opencode-claude-manager-plugin 0.1.12 → 0.1.16
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/claude/claude-agent-sdk-adapter.d.ts +3 -1
- package/dist/claude/claude-agent-sdk-adapter.js +29 -1
- package/dist/claude/session-live-tailer.d.ts +51 -0
- package/dist/claude/session-live-tailer.js +269 -0
- package/dist/claude/tool-approval-manager.d.ts +27 -0
- package/dist/claude/tool-approval-manager.js +232 -0
- package/dist/index.d.ts +2 -1
- package/dist/index.js +1 -0
- package/dist/manager/persistent-manager.d.ts +1 -0
- package/dist/manager/session-controller.d.ts +2 -0
- package/dist/manager/session-controller.js +5 -1
- package/dist/plugin/claude-manager.plugin.js +155 -7
- package/dist/plugin/service-factory.d.ts +4 -0
- package/dist/plugin/service-factory.js +7 -1
- package/dist/prompts/registry.js +112 -31
- package/dist/types/contracts.d.ts +40 -0
- package/package.json +1 -1
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { type Options, type Query, type SDKSessionInfo, type SessionMessage, type SettingSource } from '@anthropic-ai/claude-agent-sdk';
|
|
2
2
|
import type { ClaudeCapabilitySnapshot, ClaudeSessionEvent, ClaudeSessionRunResult, ClaudeSessionSummary, ClaudeSessionTranscriptMessage, RunClaudeSessionInput } from '../types/contracts.js';
|
|
3
|
+
import type { ToolApprovalManager } from './tool-approval-manager.js';
|
|
3
4
|
export type ClaudeSessionEventHandler = (event: ClaudeSessionEvent) => void | Promise<void>;
|
|
4
5
|
interface ClaudeAgentSdkFacade {
|
|
5
6
|
query(params: {
|
|
@@ -15,7 +16,8 @@ interface ClaudeAgentSdkFacade {
|
|
|
15
16
|
}
|
|
16
17
|
export declare class ClaudeAgentSdkAdapter {
|
|
17
18
|
private readonly sdkFacade;
|
|
18
|
-
|
|
19
|
+
private readonly approvalManager?;
|
|
20
|
+
constructor(sdkFacade?: ClaudeAgentSdkFacade, approvalManager?: ToolApprovalManager | undefined);
|
|
19
21
|
runSession(input: RunClaudeSessionInput, onEvent?: ClaudeSessionEventHandler): Promise<ClaudeSessionRunResult>;
|
|
20
22
|
listSavedSessions(cwd?: string): Promise<ClaudeSessionSummary[]>;
|
|
21
23
|
getTranscript(sessionId: string, cwd?: string): Promise<ClaudeSessionTranscriptMessage[]>;
|
|
@@ -9,12 +9,27 @@ const TOOL_INPUT_PREVIEW_MAX = 2000;
|
|
|
9
9
|
const USER_MESSAGE_MAX = 4000;
|
|
10
10
|
export class ClaudeAgentSdkAdapter {
|
|
11
11
|
sdkFacade;
|
|
12
|
-
|
|
12
|
+
approvalManager;
|
|
13
|
+
constructor(sdkFacade = defaultFacade, approvalManager) {
|
|
13
14
|
this.sdkFacade = sdkFacade;
|
|
15
|
+
this.approvalManager = approvalManager;
|
|
14
16
|
}
|
|
15
17
|
async runSession(input, onEvent) {
|
|
16
18
|
const options = this.buildOptions(input);
|
|
17
19
|
const includePartials = options.includePartialMessages === true;
|
|
20
|
+
const abortController = new AbortController();
|
|
21
|
+
options.abortController = abortController;
|
|
22
|
+
const externalSignal = input.abortSignal;
|
|
23
|
+
let onAbort;
|
|
24
|
+
if (externalSignal) {
|
|
25
|
+
if (externalSignal.aborted) {
|
|
26
|
+
abortController.abort();
|
|
27
|
+
}
|
|
28
|
+
else {
|
|
29
|
+
onAbort = () => abortController.abort();
|
|
30
|
+
externalSignal.addEventListener('abort', onAbort, { once: true });
|
|
31
|
+
}
|
|
32
|
+
}
|
|
18
33
|
const sessionQuery = this.sdkFacade.query({
|
|
19
34
|
prompt: input.prompt,
|
|
20
35
|
options,
|
|
@@ -58,6 +73,9 @@ export class ClaudeAgentSdkAdapter {
|
|
|
58
73
|
}
|
|
59
74
|
}
|
|
60
75
|
finally {
|
|
76
|
+
if (onAbort && externalSignal) {
|
|
77
|
+
externalSignal.removeEventListener('abort', onAbort);
|
|
78
|
+
}
|
|
61
79
|
sessionQuery.close();
|
|
62
80
|
}
|
|
63
81
|
return {
|
|
@@ -133,6 +151,7 @@ export class ClaudeAgentSdkAdapter {
|
|
|
133
151
|
settingSources: input.settingSources,
|
|
134
152
|
maxTurns: input.maxTurns,
|
|
135
153
|
model: input.model,
|
|
154
|
+
effort: input.effort,
|
|
136
155
|
permissionMode: input.permissionMode ?? 'acceptEdits',
|
|
137
156
|
systemPrompt: input.systemPrompt
|
|
138
157
|
? { type: 'preset', preset: 'claude_code', append: input.systemPrompt }
|
|
@@ -145,6 +164,15 @@ export class ClaudeAgentSdkAdapter {
|
|
|
145
164
|
if (!input.resumeSessionId) {
|
|
146
165
|
delete options.resume;
|
|
147
166
|
}
|
|
167
|
+
if (this.approvalManager) {
|
|
168
|
+
const manager = this.approvalManager;
|
|
169
|
+
options.canUseTool = async (toolName, toolInput, opts) => {
|
|
170
|
+
return manager.evaluate(toolName, toolInput, {
|
|
171
|
+
title: opts.title,
|
|
172
|
+
agentID: opts.agentID,
|
|
173
|
+
});
|
|
174
|
+
};
|
|
175
|
+
}
|
|
148
176
|
return options;
|
|
149
177
|
}
|
|
150
178
|
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import type { LiveTailEvent, ToolOutputPreview } from '../types/contracts.js';
|
|
2
|
+
/**
|
|
3
|
+
* Tails Claude Code session JSONL files for live tool output.
|
|
4
|
+
*
|
|
5
|
+
* The SDK streams high-level events (assistant text, tool_call summaries, results)
|
|
6
|
+
* but does not expose the raw tool output that Claude Code writes to the JSONL
|
|
7
|
+
* transcript on disk. This service fills that gap by polling the file for new
|
|
8
|
+
* lines, parsing each one, and forwarding parsed events to a caller-supplied
|
|
9
|
+
* callback.
|
|
10
|
+
*/
|
|
11
|
+
export declare class SessionLiveTailer {
|
|
12
|
+
private activeTails;
|
|
13
|
+
/**
|
|
14
|
+
* Locate the JSONL file for a session.
|
|
15
|
+
*
|
|
16
|
+
* Claude Code stores transcripts at:
|
|
17
|
+
* ~/.claude/projects/<sanitized-cwd>/sessions/<session-id>.jsonl
|
|
18
|
+
*
|
|
19
|
+
* The `<sanitized-cwd>` folder name is an internal implementation detail that
|
|
20
|
+
* can change across Claude Code versions, so we search all project directories
|
|
21
|
+
* for the session file rather than attempting to replicate the sanitisation.
|
|
22
|
+
*/
|
|
23
|
+
findSessionFile(sessionId: string, cwd?: string): string | null;
|
|
24
|
+
/**
|
|
25
|
+
* Check whether we can locate a JSONL file for the given session.
|
|
26
|
+
*/
|
|
27
|
+
sessionFileExists(sessionId: string, cwd?: string): boolean;
|
|
28
|
+
/**
|
|
29
|
+
* Poll a session's JSONL file for new lines and emit parsed events.
|
|
30
|
+
*
|
|
31
|
+
* Returns a stop function. The caller **must** invoke it when tailing is no
|
|
32
|
+
* longer needed (e.g. when the session completes or the tool is interrupted).
|
|
33
|
+
*/
|
|
34
|
+
startTailing(sessionId: string, cwd: string | undefined, onEvent: (event: LiveTailEvent) => void, pollIntervalMs?: number): () => void;
|
|
35
|
+
/**
|
|
36
|
+
* Stop tailing a specific session.
|
|
37
|
+
*/
|
|
38
|
+
stopTailing(sessionId: string): void;
|
|
39
|
+
/**
|
|
40
|
+
* Stop all active tails (cleanup on shutdown).
|
|
41
|
+
*/
|
|
42
|
+
stopAll(): void;
|
|
43
|
+
/**
|
|
44
|
+
* Read the last N lines from a session JSONL file.
|
|
45
|
+
*/
|
|
46
|
+
getLastLines(sessionId: string, cwd: string | undefined, lineCount?: number): Promise<string[]>;
|
|
47
|
+
/**
|
|
48
|
+
* Extract a preview of recent tool output from the tail of a session file.
|
|
49
|
+
*/
|
|
50
|
+
getToolOutputPreview(sessionId: string, cwd: string | undefined, maxEntries?: number): Promise<ToolOutputPreview[]>;
|
|
51
|
+
}
|
|
@@ -0,0 +1,269 @@
|
|
|
1
|
+
import { createReadStream, existsSync, readdirSync, statSync } from 'node:fs';
|
|
2
|
+
import { createInterface } from 'node:readline';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import os from 'node:os';
|
|
5
|
+
/**
|
|
6
|
+
* Tails Claude Code session JSONL files for live tool output.
|
|
7
|
+
*
|
|
8
|
+
* The SDK streams high-level events (assistant text, tool_call summaries, results)
|
|
9
|
+
* but does not expose the raw tool output that Claude Code writes to the JSONL
|
|
10
|
+
* transcript on disk. This service fills that gap by polling the file for new
|
|
11
|
+
* lines, parsing each one, and forwarding parsed events to a caller-supplied
|
|
12
|
+
* callback.
|
|
13
|
+
*/
|
|
14
|
+
export class SessionLiveTailer {
|
|
15
|
+
activeTails = new Map();
|
|
16
|
+
// ── Path discovery ──────────────────────────────────────────────────
|
|
17
|
+
/**
|
|
18
|
+
* Locate the JSONL file for a session.
|
|
19
|
+
*
|
|
20
|
+
* Claude Code stores transcripts at:
|
|
21
|
+
* ~/.claude/projects/<sanitized-cwd>/sessions/<session-id>.jsonl
|
|
22
|
+
*
|
|
23
|
+
* The `<sanitized-cwd>` folder name is an internal implementation detail that
|
|
24
|
+
* can change across Claude Code versions, so we search all project directories
|
|
25
|
+
* for the session file rather than attempting to replicate the sanitisation.
|
|
26
|
+
*/
|
|
27
|
+
findSessionFile(sessionId, cwd) {
|
|
28
|
+
const projectsDir = path.join(os.homedir(), '.claude', 'projects');
|
|
29
|
+
if (!existsSync(projectsDir)) {
|
|
30
|
+
return null;
|
|
31
|
+
}
|
|
32
|
+
// If cwd is provided, try the sanitised form first (best-effort fast path).
|
|
33
|
+
if (cwd) {
|
|
34
|
+
const sanitised = cwd.replace(/\//g, '-');
|
|
35
|
+
const candidate = path.join(projectsDir, sanitised, 'sessions', `${sessionId}.jsonl`);
|
|
36
|
+
if (existsSync(candidate)) {
|
|
37
|
+
return candidate;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
// Fall back to scanning all project directories.
|
|
41
|
+
try {
|
|
42
|
+
for (const entry of readdirSync(projectsDir, { withFileTypes: true })) {
|
|
43
|
+
if (!entry.isDirectory())
|
|
44
|
+
continue;
|
|
45
|
+
const candidate = path.join(projectsDir, entry.name, 'sessions', `${sessionId}.jsonl`);
|
|
46
|
+
if (existsSync(candidate)) {
|
|
47
|
+
return candidate;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
catch {
|
|
52
|
+
// Permission denied or similar — nothing we can do.
|
|
53
|
+
}
|
|
54
|
+
return null;
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Check whether we can locate a JSONL file for the given session.
|
|
58
|
+
*/
|
|
59
|
+
sessionFileExists(sessionId, cwd) {
|
|
60
|
+
return this.findSessionFile(sessionId, cwd) !== null;
|
|
61
|
+
}
|
|
62
|
+
// ── Live tailing ────────────────────────────────────────────────────
|
|
63
|
+
/**
|
|
64
|
+
* Poll a session's JSONL file for new lines and emit parsed events.
|
|
65
|
+
*
|
|
66
|
+
* Returns a stop function. The caller **must** invoke it when tailing is no
|
|
67
|
+
* longer needed (e.g. when the session completes or the tool is interrupted).
|
|
68
|
+
*/
|
|
69
|
+
startTailing(sessionId, cwd, onEvent, pollIntervalMs = 150) {
|
|
70
|
+
const filePath = this.findSessionFile(sessionId, cwd);
|
|
71
|
+
if (!filePath) {
|
|
72
|
+
onEvent({
|
|
73
|
+
type: 'error',
|
|
74
|
+
sessionId,
|
|
75
|
+
error: `Session JSONL not found for ${sessionId}`,
|
|
76
|
+
});
|
|
77
|
+
return () => { };
|
|
78
|
+
}
|
|
79
|
+
let offset = statSync(filePath).size;
|
|
80
|
+
let buffer = '';
|
|
81
|
+
let reading = false; // guard against overlapping reads
|
|
82
|
+
const interval = setInterval(() => {
|
|
83
|
+
if (reading)
|
|
84
|
+
return;
|
|
85
|
+
try {
|
|
86
|
+
const currentSize = statSync(filePath).size;
|
|
87
|
+
if (currentSize < offset) {
|
|
88
|
+
// File was truncated (rotation / compaction) — reset.
|
|
89
|
+
offset = 0;
|
|
90
|
+
buffer = '';
|
|
91
|
+
}
|
|
92
|
+
if (currentSize <= offset)
|
|
93
|
+
return;
|
|
94
|
+
reading = true;
|
|
95
|
+
const stream = createReadStream(filePath, {
|
|
96
|
+
start: offset,
|
|
97
|
+
end: currentSize - 1,
|
|
98
|
+
encoding: 'utf8',
|
|
99
|
+
});
|
|
100
|
+
let chunk = '';
|
|
101
|
+
stream.on('data', (data) => {
|
|
102
|
+
chunk += data;
|
|
103
|
+
});
|
|
104
|
+
stream.on('end', () => {
|
|
105
|
+
reading = false;
|
|
106
|
+
offset = currentSize;
|
|
107
|
+
buffer += chunk;
|
|
108
|
+
const lines = buffer.split('\n');
|
|
109
|
+
// Keep the last (possibly incomplete) segment in the buffer.
|
|
110
|
+
buffer = lines.pop() ?? '';
|
|
111
|
+
for (const line of lines) {
|
|
112
|
+
const trimmed = line.trim();
|
|
113
|
+
if (!trimmed)
|
|
114
|
+
continue;
|
|
115
|
+
try {
|
|
116
|
+
const parsed = JSON.parse(trimmed);
|
|
117
|
+
onEvent({
|
|
118
|
+
type: 'line',
|
|
119
|
+
sessionId,
|
|
120
|
+
data: parsed,
|
|
121
|
+
rawLine: trimmed,
|
|
122
|
+
});
|
|
123
|
+
}
|
|
124
|
+
catch {
|
|
125
|
+
onEvent({
|
|
126
|
+
type: 'line',
|
|
127
|
+
sessionId,
|
|
128
|
+
data: null,
|
|
129
|
+
rawLine: trimmed,
|
|
130
|
+
});
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
});
|
|
134
|
+
stream.on('error', (err) => {
|
|
135
|
+
reading = false;
|
|
136
|
+
onEvent({
|
|
137
|
+
type: 'error',
|
|
138
|
+
sessionId,
|
|
139
|
+
error: err.message,
|
|
140
|
+
});
|
|
141
|
+
});
|
|
142
|
+
}
|
|
143
|
+
catch (err) {
|
|
144
|
+
reading = false;
|
|
145
|
+
onEvent({
|
|
146
|
+
type: 'error',
|
|
147
|
+
sessionId,
|
|
148
|
+
error: err instanceof Error ? err.message : String(err),
|
|
149
|
+
});
|
|
150
|
+
}
|
|
151
|
+
}, pollIntervalMs);
|
|
152
|
+
this.activeTails.set(sessionId, interval);
|
|
153
|
+
return () => this.stopTailing(sessionId);
|
|
154
|
+
}
|
|
155
|
+
/**
|
|
156
|
+
* Stop tailing a specific session.
|
|
157
|
+
*/
|
|
158
|
+
stopTailing(sessionId) {
|
|
159
|
+
const interval = this.activeTails.get(sessionId);
|
|
160
|
+
if (interval) {
|
|
161
|
+
clearInterval(interval);
|
|
162
|
+
this.activeTails.delete(sessionId);
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
/**
|
|
166
|
+
* Stop all active tails (cleanup on shutdown).
|
|
167
|
+
*/
|
|
168
|
+
stopAll() {
|
|
169
|
+
for (const [, interval] of this.activeTails) {
|
|
170
|
+
clearInterval(interval);
|
|
171
|
+
}
|
|
172
|
+
this.activeTails.clear();
|
|
173
|
+
}
|
|
174
|
+
// ── Snapshot helpers ────────────────────────────────────────────────
|
|
175
|
+
/**
|
|
176
|
+
* Read the last N lines from a session JSONL file.
|
|
177
|
+
*/
|
|
178
|
+
async getLastLines(sessionId, cwd, lineCount = 20) {
|
|
179
|
+
const filePath = this.findSessionFile(sessionId, cwd);
|
|
180
|
+
if (!filePath)
|
|
181
|
+
return [];
|
|
182
|
+
const lines = [];
|
|
183
|
+
const rl = createInterface({
|
|
184
|
+
input: createReadStream(filePath, { encoding: 'utf8' }),
|
|
185
|
+
});
|
|
186
|
+
for await (const line of rl) {
|
|
187
|
+
lines.push(line);
|
|
188
|
+
if (lines.length > lineCount) {
|
|
189
|
+
lines.shift();
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
return lines;
|
|
193
|
+
}
|
|
194
|
+
/**
|
|
195
|
+
* Extract a preview of recent tool output from the tail of a session file.
|
|
196
|
+
*/
|
|
197
|
+
async getToolOutputPreview(sessionId, cwd, maxEntries = 5) {
|
|
198
|
+
const lastLines = await this.getLastLines(sessionId, cwd, 100);
|
|
199
|
+
const previews = [];
|
|
200
|
+
for (const line of lastLines) {
|
|
201
|
+
const trimmed = line.trim();
|
|
202
|
+
if (!trimmed)
|
|
203
|
+
continue;
|
|
204
|
+
try {
|
|
205
|
+
const parsed = JSON.parse(trimmed);
|
|
206
|
+
const preview = extractToolOutput(parsed);
|
|
207
|
+
if (preview) {
|
|
208
|
+
previews.push(preview);
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
catch {
|
|
212
|
+
// skip unparseable lines
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
return previews.slice(-maxEntries);
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
// ── Internal helpers ────────────────────────────────────────────────────
|
|
219
|
+
/**
|
|
220
|
+
* Attempt to extract tool output information from a JSONL record.
|
|
221
|
+
*
|
|
222
|
+
* Claude Code JSONL records with tool results can appear as:
|
|
223
|
+
* 1. A top-level `{ type: "user", message: { content: [{ type: "tool_result", ... }] } }`
|
|
224
|
+
* 2. Direct `tool_result` entries (less common).
|
|
225
|
+
*/
|
|
226
|
+
function extractToolOutput(record) {
|
|
227
|
+
// Pattern 1: user message wrapping tool_result content blocks
|
|
228
|
+
if (record.type === 'user') {
|
|
229
|
+
const message = record.message;
|
|
230
|
+
if (!message)
|
|
231
|
+
return null;
|
|
232
|
+
const content = message.content;
|
|
233
|
+
if (!Array.isArray(content))
|
|
234
|
+
return null;
|
|
235
|
+
for (const block of content) {
|
|
236
|
+
if (block &&
|
|
237
|
+
typeof block === 'object' &&
|
|
238
|
+
block.type === 'tool_result') {
|
|
239
|
+
const b = block;
|
|
240
|
+
return {
|
|
241
|
+
toolUseId: typeof b.tool_use_id === 'string' ? b.tool_use_id : '',
|
|
242
|
+
content: stringifyContent(b.content),
|
|
243
|
+
isError: b.is_error === true,
|
|
244
|
+
};
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
// Pattern 2: direct tool_result record (varies by Claude Code version)
|
|
249
|
+
if (record.type === 'tool_result') {
|
|
250
|
+
return {
|
|
251
|
+
toolUseId: typeof record.tool_use_id === 'string' ? record.tool_use_id : '',
|
|
252
|
+
content: stringifyContent(record.content),
|
|
253
|
+
isError: record.is_error === true,
|
|
254
|
+
};
|
|
255
|
+
}
|
|
256
|
+
return null;
|
|
257
|
+
}
|
|
258
|
+
function stringifyContent(value) {
|
|
259
|
+
if (typeof value === 'string')
|
|
260
|
+
return value;
|
|
261
|
+
if (value === undefined || value === null)
|
|
262
|
+
return '';
|
|
263
|
+
try {
|
|
264
|
+
return JSON.stringify(value);
|
|
265
|
+
}
|
|
266
|
+
catch {
|
|
267
|
+
return String(value);
|
|
268
|
+
}
|
|
269
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import type { ToolApprovalDecision, ToolApprovalPolicy, ToolApprovalRule } from '../types/contracts.js';
|
|
2
|
+
export declare class ToolApprovalManager {
|
|
3
|
+
private policy;
|
|
4
|
+
private decisions;
|
|
5
|
+
private readonly maxDecisions;
|
|
6
|
+
constructor(policy?: Partial<ToolApprovalPolicy>, maxDecisions?: number);
|
|
7
|
+
evaluate(toolName: string, input: Record<string, unknown>, options?: {
|
|
8
|
+
title?: string;
|
|
9
|
+
agentID?: string;
|
|
10
|
+
}): {
|
|
11
|
+
behavior: 'allow';
|
|
12
|
+
} | {
|
|
13
|
+
behavior: 'deny';
|
|
14
|
+
message: string;
|
|
15
|
+
};
|
|
16
|
+
getDecisions(limit?: number): ToolApprovalDecision[];
|
|
17
|
+
getDeniedDecisions(limit?: number): ToolApprovalDecision[];
|
|
18
|
+
clearDecisions(): void;
|
|
19
|
+
getPolicy(): ToolApprovalPolicy;
|
|
20
|
+
setPolicy(policy: ToolApprovalPolicy): void;
|
|
21
|
+
addRule(rule: ToolApprovalRule, position?: number): void;
|
|
22
|
+
removeRule(ruleId: string): boolean;
|
|
23
|
+
setDefaultAction(action: 'allow' | 'deny'): void;
|
|
24
|
+
setEnabled(enabled: boolean): void;
|
|
25
|
+
private findMatchingRule;
|
|
26
|
+
private recordDecision;
|
|
27
|
+
}
|
|
@@ -0,0 +1,232 @@
|
|
|
1
|
+
const DEFAULT_MAX_DECISIONS = 500;
|
|
2
|
+
const INPUT_PREVIEW_MAX = 300;
|
|
3
|
+
function getDefaultRules() {
|
|
4
|
+
return [
|
|
5
|
+
// Safe read-only tools
|
|
6
|
+
{
|
|
7
|
+
id: 'allow-read',
|
|
8
|
+
toolPattern: 'Read',
|
|
9
|
+
action: 'allow',
|
|
10
|
+
description: 'Allow reading files',
|
|
11
|
+
},
|
|
12
|
+
{
|
|
13
|
+
id: 'allow-grep',
|
|
14
|
+
toolPattern: 'Grep',
|
|
15
|
+
action: 'allow',
|
|
16
|
+
description: 'Allow grep searches',
|
|
17
|
+
},
|
|
18
|
+
{
|
|
19
|
+
id: 'allow-glob',
|
|
20
|
+
toolPattern: 'Glob',
|
|
21
|
+
action: 'allow',
|
|
22
|
+
description: 'Allow glob file searches',
|
|
23
|
+
},
|
|
24
|
+
{
|
|
25
|
+
id: 'allow-ls',
|
|
26
|
+
toolPattern: 'LS',
|
|
27
|
+
action: 'allow',
|
|
28
|
+
description: 'Allow directory listing',
|
|
29
|
+
},
|
|
30
|
+
{
|
|
31
|
+
id: 'allow-list',
|
|
32
|
+
toolPattern: 'ListDirectory',
|
|
33
|
+
action: 'allow',
|
|
34
|
+
description: 'Allow listing directories',
|
|
35
|
+
},
|
|
36
|
+
// Edit tools
|
|
37
|
+
{
|
|
38
|
+
id: 'allow-edit',
|
|
39
|
+
toolPattern: 'Edit',
|
|
40
|
+
action: 'allow',
|
|
41
|
+
description: 'Allow file edits',
|
|
42
|
+
},
|
|
43
|
+
{
|
|
44
|
+
id: 'allow-multiedit',
|
|
45
|
+
toolPattern: 'MultiEdit',
|
|
46
|
+
action: 'allow',
|
|
47
|
+
description: 'Allow multi-edits',
|
|
48
|
+
},
|
|
49
|
+
{
|
|
50
|
+
id: 'allow-write',
|
|
51
|
+
toolPattern: 'Write',
|
|
52
|
+
action: 'allow',
|
|
53
|
+
description: 'Allow file writes',
|
|
54
|
+
},
|
|
55
|
+
{
|
|
56
|
+
id: 'allow-notebook',
|
|
57
|
+
toolPattern: 'NotebookEdit',
|
|
58
|
+
action: 'allow',
|
|
59
|
+
description: 'Allow notebook edits',
|
|
60
|
+
},
|
|
61
|
+
// Bash - deny dangerous patterns first, then allow the rest
|
|
62
|
+
{
|
|
63
|
+
id: 'deny-bash-rm-rf-root',
|
|
64
|
+
toolPattern: 'Bash',
|
|
65
|
+
inputPattern: 'rm -rf /',
|
|
66
|
+
action: 'deny',
|
|
67
|
+
denyMessage: 'Destructive rm -rf on root path is not allowed.',
|
|
68
|
+
description: 'Block rm -rf on root',
|
|
69
|
+
},
|
|
70
|
+
{
|
|
71
|
+
id: 'deny-bash-force-push',
|
|
72
|
+
toolPattern: 'Bash',
|
|
73
|
+
inputPattern: 'git push --force',
|
|
74
|
+
action: 'deny',
|
|
75
|
+
denyMessage: 'Force push is not allowed.',
|
|
76
|
+
description: 'Block git force push',
|
|
77
|
+
},
|
|
78
|
+
{
|
|
79
|
+
id: 'deny-bash-reset-hard',
|
|
80
|
+
toolPattern: 'Bash',
|
|
81
|
+
inputPattern: 'git reset --hard',
|
|
82
|
+
action: 'deny',
|
|
83
|
+
denyMessage: 'git reset --hard is not allowed from Claude Code. Use the manager git_reset tool instead.',
|
|
84
|
+
description: 'Block git reset --hard',
|
|
85
|
+
},
|
|
86
|
+
{
|
|
87
|
+
id: 'allow-bash',
|
|
88
|
+
toolPattern: 'Bash',
|
|
89
|
+
action: 'allow',
|
|
90
|
+
description: 'Allow bash commands (after dangerous patterns filtered)',
|
|
91
|
+
},
|
|
92
|
+
// Agent / misc
|
|
93
|
+
{
|
|
94
|
+
id: 'allow-agent',
|
|
95
|
+
toolPattern: 'Agent',
|
|
96
|
+
action: 'allow',
|
|
97
|
+
description: 'Allow agent delegation',
|
|
98
|
+
},
|
|
99
|
+
{
|
|
100
|
+
id: 'allow-todowrite',
|
|
101
|
+
toolPattern: 'TodoWrite',
|
|
102
|
+
action: 'allow',
|
|
103
|
+
description: 'Allow todo tracking',
|
|
104
|
+
},
|
|
105
|
+
{
|
|
106
|
+
id: 'allow-todoread',
|
|
107
|
+
toolPattern: 'TodoRead',
|
|
108
|
+
action: 'allow',
|
|
109
|
+
description: 'Allow todo reading',
|
|
110
|
+
},
|
|
111
|
+
];
|
|
112
|
+
}
|
|
113
|
+
export class ToolApprovalManager {
|
|
114
|
+
policy;
|
|
115
|
+
decisions = [];
|
|
116
|
+
maxDecisions;
|
|
117
|
+
constructor(policy, maxDecisions) {
|
|
118
|
+
this.policy = {
|
|
119
|
+
rules: policy?.rules ?? getDefaultRules(),
|
|
120
|
+
defaultAction: policy?.defaultAction ?? 'allow',
|
|
121
|
+
defaultDenyMessage: policy?.defaultDenyMessage ?? 'Tool call denied by approval policy.',
|
|
122
|
+
enabled: policy?.enabled ?? true,
|
|
123
|
+
};
|
|
124
|
+
this.maxDecisions = maxDecisions ?? DEFAULT_MAX_DECISIONS;
|
|
125
|
+
}
|
|
126
|
+
evaluate(toolName, input, options) {
|
|
127
|
+
if (!this.policy.enabled) {
|
|
128
|
+
return { behavior: 'allow' };
|
|
129
|
+
}
|
|
130
|
+
const inputJson = safeJsonStringify(input);
|
|
131
|
+
const matchedRule = this.findMatchingRule(toolName, inputJson);
|
|
132
|
+
const action = matchedRule?.action ?? this.policy.defaultAction;
|
|
133
|
+
const denyMessage = action === 'deny'
|
|
134
|
+
? (matchedRule?.denyMessage ??
|
|
135
|
+
this.policy.defaultDenyMessage ??
|
|
136
|
+
'Denied by policy.')
|
|
137
|
+
: undefined;
|
|
138
|
+
this.recordDecision({
|
|
139
|
+
timestamp: new Date().toISOString(),
|
|
140
|
+
toolName,
|
|
141
|
+
inputPreview: inputJson.slice(0, INPUT_PREVIEW_MAX),
|
|
142
|
+
title: options?.title,
|
|
143
|
+
matchedRuleId: matchedRule?.id ?? 'default',
|
|
144
|
+
action,
|
|
145
|
+
denyMessage,
|
|
146
|
+
agentId: options?.agentID,
|
|
147
|
+
});
|
|
148
|
+
if (action === 'deny') {
|
|
149
|
+
return { behavior: 'deny', message: denyMessage };
|
|
150
|
+
}
|
|
151
|
+
return { behavior: 'allow' };
|
|
152
|
+
}
|
|
153
|
+
getDecisions(limit) {
|
|
154
|
+
const all = [...this.decisions].reverse();
|
|
155
|
+
return limit ? all.slice(0, limit) : all;
|
|
156
|
+
}
|
|
157
|
+
getDeniedDecisions(limit) {
|
|
158
|
+
const denied = this.decisions.filter((d) => d.action === 'deny').reverse();
|
|
159
|
+
return limit ? denied.slice(0, limit) : denied;
|
|
160
|
+
}
|
|
161
|
+
clearDecisions() {
|
|
162
|
+
this.decisions = [];
|
|
163
|
+
}
|
|
164
|
+
getPolicy() {
|
|
165
|
+
return { ...this.policy, rules: [...this.policy.rules] };
|
|
166
|
+
}
|
|
167
|
+
setPolicy(policy) {
|
|
168
|
+
this.policy = { ...policy, rules: [...policy.rules] };
|
|
169
|
+
}
|
|
170
|
+
addRule(rule, position) {
|
|
171
|
+
if (position !== undefined &&
|
|
172
|
+
position >= 0 &&
|
|
173
|
+
position < this.policy.rules.length) {
|
|
174
|
+
this.policy.rules.splice(position, 0, rule);
|
|
175
|
+
}
|
|
176
|
+
else {
|
|
177
|
+
this.policy.rules.push(rule);
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
removeRule(ruleId) {
|
|
181
|
+
const index = this.policy.rules.findIndex((r) => r.id === ruleId);
|
|
182
|
+
if (index === -1) {
|
|
183
|
+
return false;
|
|
184
|
+
}
|
|
185
|
+
this.policy.rules.splice(index, 1);
|
|
186
|
+
return true;
|
|
187
|
+
}
|
|
188
|
+
setDefaultAction(action) {
|
|
189
|
+
this.policy.defaultAction = action;
|
|
190
|
+
}
|
|
191
|
+
setEnabled(enabled) {
|
|
192
|
+
this.policy.enabled = enabled;
|
|
193
|
+
}
|
|
194
|
+
findMatchingRule(toolName, inputJson) {
|
|
195
|
+
for (const rule of this.policy.rules) {
|
|
196
|
+
if (!matchesToolPattern(rule.toolPattern, toolName)) {
|
|
197
|
+
continue;
|
|
198
|
+
}
|
|
199
|
+
if (rule.inputPattern && !inputJson.includes(rule.inputPattern)) {
|
|
200
|
+
continue;
|
|
201
|
+
}
|
|
202
|
+
return rule;
|
|
203
|
+
}
|
|
204
|
+
return null;
|
|
205
|
+
}
|
|
206
|
+
recordDecision(decision) {
|
|
207
|
+
this.decisions.push(decision);
|
|
208
|
+
if (this.decisions.length > this.maxDecisions) {
|
|
209
|
+
this.decisions = this.decisions.slice(-this.maxDecisions);
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
function matchesToolPattern(pattern, toolName) {
|
|
214
|
+
if (pattern === '*') {
|
|
215
|
+
return true;
|
|
216
|
+
}
|
|
217
|
+
if (!pattern.includes('*')) {
|
|
218
|
+
return pattern === toolName;
|
|
219
|
+
}
|
|
220
|
+
const regex = new RegExp('^' +
|
|
221
|
+
pattern.replace(/[.+^${}()|[\]\\]/g, '\\$&').replace(/\*/g, '.*') +
|
|
222
|
+
'$');
|
|
223
|
+
return regex.test(toolName);
|
|
224
|
+
}
|
|
225
|
+
function safeJsonStringify(value) {
|
|
226
|
+
try {
|
|
227
|
+
return JSON.stringify(value);
|
|
228
|
+
}
|
|
229
|
+
catch {
|
|
230
|
+
return String(value);
|
|
231
|
+
}
|
|
232
|
+
}
|
package/dist/index.d.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { Plugin } from '@opencode-ai/plugin';
|
|
2
2
|
import { ClaudeManagerPlugin } from './plugin/claude-manager.plugin.js';
|
|
3
|
-
export type { ClaudeCapabilitySnapshot, ClaudeMetadataSnapshot, ClaudeSessionRunResult, ClaudeSessionSummary, ClaudeSessionTranscriptMessage, ManagerPromptRegistry, RunClaudeSessionInput, SessionContextSnapshot, GitDiffResult, GitOperationResult, PersistentRunRecord, PersistentRunResult, ActiveSessionState, ContextWarningLevel, } from './types/contracts.js';
|
|
3
|
+
export type { ClaudeCapabilitySnapshot, ClaudeMetadataSnapshot, ClaudeSessionRunResult, ClaudeSessionSummary, ClaudeSessionTranscriptMessage, ManagerPromptRegistry, RunClaudeSessionInput, SessionContextSnapshot, GitDiffResult, GitOperationResult, PersistentRunRecord, PersistentRunResult, ActiveSessionState, ContextWarningLevel, LiveTailEvent, ToolOutputPreview, ToolApprovalRule, ToolApprovalPolicy, ToolApprovalDecision, } from './types/contracts.js';
|
|
4
|
+
export { SessionLiveTailer } from './claude/session-live-tailer.js';
|
|
4
5
|
export { ClaudeManagerPlugin };
|
|
5
6
|
export declare const plugin: Plugin;
|
package/dist/index.js
CHANGED
|
@@ -19,6 +19,7 @@ export declare class PersistentManager {
|
|
|
19
19
|
*/
|
|
20
20
|
sendMessage(cwd: string, message: string, options?: {
|
|
21
21
|
model?: string;
|
|
22
|
+
abortSignal?: AbortSignal;
|
|
22
23
|
}, onEvent?: ClaudeSessionEventHandler): Promise<{
|
|
23
24
|
sessionId: string | undefined;
|
|
24
25
|
finalText: string;
|
|
@@ -15,7 +15,9 @@ export declare class SessionController {
|
|
|
15
15
|
*/
|
|
16
16
|
sendMessage(cwd: string, message: string, options?: {
|
|
17
17
|
model?: string;
|
|
18
|
+
effort?: 'low' | 'medium' | 'high' | 'max';
|
|
18
19
|
settingSources?: Array<'user' | 'project' | 'local'>;
|
|
20
|
+
abortSignal?: AbortSignal;
|
|
19
21
|
}, onEvent?: ClaudeSessionEventHandler): Promise<ClaudeSessionRunResult>;
|
|
20
22
|
/**
|
|
21
23
|
* Send /compact to the current session to compress context.
|
|
@@ -29,15 +29,19 @@ export class SessionController {
|
|
|
29
29
|
permissionMode: 'acceptEdits',
|
|
30
30
|
includePartialMessages: true,
|
|
31
31
|
model: options?.model,
|
|
32
|
+
effort: options?.effort,
|
|
32
33
|
settingSources: options?.settingSources ?? ['user', 'project', 'local'],
|
|
34
|
+
abortSignal: options?.abortSignal,
|
|
33
35
|
};
|
|
34
36
|
if (this.activeSessionId) {
|
|
35
37
|
// Resume existing session
|
|
36
38
|
input.resumeSessionId = this.activeSessionId;
|
|
37
39
|
}
|
|
38
40
|
else {
|
|
39
|
-
// New session — apply the expert operator system prompt
|
|
41
|
+
// New session — apply the expert operator system prompt and defaults
|
|
40
42
|
input.systemPrompt = this.sessionPrompt;
|
|
43
|
+
input.model ??= 'claude-opus-4-6';
|
|
44
|
+
input.effort ??= 'high';
|
|
41
45
|
}
|
|
42
46
|
const result = await this.sdkAdapter.runSession(input, onEvent);
|
|
43
47
|
// Track the session ID
|
|
@@ -11,6 +11,9 @@ const MANAGER_TOOL_IDS = [
|
|
|
11
11
|
'claude_manager_metadata',
|
|
12
12
|
'claude_manager_sessions',
|
|
13
13
|
'claude_manager_runs',
|
|
14
|
+
'claude_manager_approval_policy',
|
|
15
|
+
'claude_manager_approval_decisions',
|
|
16
|
+
'claude_manager_approval_update',
|
|
14
17
|
];
|
|
15
18
|
export const ClaudeManagerPlugin = async ({ worktree }) => {
|
|
16
19
|
const services = getOrCreatePluginServices(worktree);
|
|
@@ -30,16 +33,23 @@ export const ClaudeManagerPlugin = async ({ worktree }) => {
|
|
|
30
33
|
toolId === 'claude_manager_send' ||
|
|
31
34
|
toolId === 'claude_manager_git_commit' ||
|
|
32
35
|
toolId === 'claude_manager_git_reset' ||
|
|
33
|
-
toolId === 'claude_manager_clear'
|
|
36
|
+
toolId === 'claude_manager_clear' ||
|
|
37
|
+
toolId === 'claude_manager_approval_update'
|
|
34
38
|
? 'deny'
|
|
35
39
|
: 'allow';
|
|
36
40
|
}
|
|
37
41
|
config.agent['claude-manager'] ??= {
|
|
38
42
|
description: 'Primary agent that operates Claude Code through a persistent session, reviews work via git diff, and commits/resets changes.',
|
|
39
43
|
mode: 'primary',
|
|
40
|
-
color: '
|
|
44
|
+
color: '#D97757',
|
|
41
45
|
permission: {
|
|
42
46
|
'*': 'deny',
|
|
47
|
+
read: 'allow',
|
|
48
|
+
grep: 'allow',
|
|
49
|
+
glob: 'allow',
|
|
50
|
+
codesearch: 'allow',
|
|
51
|
+
webfetch: 'allow',
|
|
52
|
+
websearch: 'allow',
|
|
43
53
|
...managerPermissions,
|
|
44
54
|
},
|
|
45
55
|
prompt: managerPromptRegistry.managerSystemPrompt,
|
|
@@ -106,15 +116,21 @@ export const ClaudeManagerPlugin = async ({ worktree }) => {
|
|
|
106
116
|
async execute(args, context) {
|
|
107
117
|
const cwd = args.cwd ?? context.worktree;
|
|
108
118
|
const hasActiveSession = services.manager.getStatus().sessionId !== null;
|
|
119
|
+
const promptPreview = args.message.length > 100
|
|
120
|
+
? args.message.slice(0, 100) + '...'
|
|
121
|
+
: args.message;
|
|
109
122
|
context.metadata({
|
|
110
123
|
title: hasActiveSession
|
|
111
124
|
? 'Claude Code: Resuming session...'
|
|
112
125
|
: 'Claude Code: Initializing...',
|
|
113
|
-
metadata: {
|
|
126
|
+
metadata: {
|
|
127
|
+
sessionId: services.manager.getStatus().sessionId,
|
|
128
|
+
prompt: promptPreview,
|
|
129
|
+
},
|
|
114
130
|
});
|
|
115
131
|
let turnsSoFar = 0;
|
|
116
132
|
let costSoFar = 0;
|
|
117
|
-
const result = await services.manager.sendMessage(cwd, args.message, { model: args.model }, (event) => {
|
|
133
|
+
const result = await services.manager.sendMessage(cwd, args.message, { model: args.model, abortSignal: context.abort }, (event) => {
|
|
118
134
|
if (event.turns !== undefined) {
|
|
119
135
|
turnsSoFar = event.turns;
|
|
120
136
|
}
|
|
@@ -124,8 +140,19 @@ export const ClaudeManagerPlugin = async ({ worktree }) => {
|
|
|
124
140
|
const costLabel = `$${costSoFar.toFixed(4)}`;
|
|
125
141
|
if (event.type === 'tool_call') {
|
|
126
142
|
let toolName = 'tool';
|
|
143
|
+
let inputPreview = '';
|
|
127
144
|
try {
|
|
128
|
-
|
|
145
|
+
const parsed = JSON.parse(event.text);
|
|
146
|
+
toolName = parsed.name ?? 'tool';
|
|
147
|
+
if (parsed.input) {
|
|
148
|
+
const inputStr = typeof parsed.input === 'string'
|
|
149
|
+
? parsed.input
|
|
150
|
+
: JSON.stringify(parsed.input);
|
|
151
|
+
inputPreview =
|
|
152
|
+
inputStr.length > 150
|
|
153
|
+
? inputStr.slice(0, 150) + '...'
|
|
154
|
+
: inputStr;
|
|
155
|
+
}
|
|
129
156
|
}
|
|
130
157
|
catch {
|
|
131
158
|
// ignore parse errors
|
|
@@ -135,17 +162,39 @@ export const ClaudeManagerPlugin = async ({ worktree }) => {
|
|
|
135
162
|
metadata: {
|
|
136
163
|
sessionId: event.sessionId,
|
|
137
164
|
type: event.type,
|
|
138
|
-
|
|
165
|
+
tool: toolName,
|
|
166
|
+
input: inputPreview,
|
|
139
167
|
},
|
|
140
168
|
});
|
|
141
169
|
}
|
|
142
170
|
else if (event.type === 'assistant') {
|
|
171
|
+
const thinkingPreview = event.text.length > 150
|
|
172
|
+
? event.text.slice(0, 150) + '...'
|
|
173
|
+
: event.text;
|
|
143
174
|
context.metadata({
|
|
144
175
|
title: `Claude Code: Thinking... (${turnsSoFar} turns, ${costLabel})`,
|
|
145
176
|
metadata: {
|
|
146
177
|
sessionId: event.sessionId,
|
|
147
178
|
type: event.type,
|
|
148
|
-
|
|
179
|
+
thinking: thinkingPreview,
|
|
180
|
+
},
|
|
181
|
+
});
|
|
182
|
+
}
|
|
183
|
+
else if (event.type === 'init') {
|
|
184
|
+
context.metadata({
|
|
185
|
+
title: `Claude Code: Session started`,
|
|
186
|
+
metadata: {
|
|
187
|
+
sessionId: event.sessionId,
|
|
188
|
+
prompt: promptPreview,
|
|
189
|
+
},
|
|
190
|
+
});
|
|
191
|
+
}
|
|
192
|
+
else if (event.type === 'error') {
|
|
193
|
+
context.metadata({
|
|
194
|
+
title: `Claude Code: Error`,
|
|
195
|
+
metadata: {
|
|
196
|
+
sessionId: event.sessionId,
|
|
197
|
+
error: event.text.slice(0, 200),
|
|
149
198
|
},
|
|
150
199
|
});
|
|
151
200
|
}
|
|
@@ -165,6 +214,16 @@ export const ClaudeManagerPlugin = async ({ worktree }) => {
|
|
|
165
214
|
metadata: { sessionId: result.sessionId },
|
|
166
215
|
});
|
|
167
216
|
}
|
|
217
|
+
// Fetch recent tool output from the JSONL file for richer feedback.
|
|
218
|
+
let toolOutputs = [];
|
|
219
|
+
if (result.sessionId) {
|
|
220
|
+
try {
|
|
221
|
+
toolOutputs = await services.liveTailer.getToolOutputPreview(result.sessionId, cwd, 3);
|
|
222
|
+
}
|
|
223
|
+
catch {
|
|
224
|
+
// Non-critical — the JSONL file may not exist yet.
|
|
225
|
+
}
|
|
226
|
+
}
|
|
168
227
|
return JSON.stringify({
|
|
169
228
|
sessionId: result.sessionId,
|
|
170
229
|
finalText: result.finalText,
|
|
@@ -172,6 +231,7 @@ export const ClaudeManagerPlugin = async ({ worktree }) => {
|
|
|
172
231
|
totalCostUsd: result.totalCostUsd,
|
|
173
232
|
context: result.context,
|
|
174
233
|
contextWarning,
|
|
234
|
+
toolOutputs: toolOutputs.length > 0 ? toolOutputs : undefined,
|
|
175
235
|
}, null, 2);
|
|
176
236
|
},
|
|
177
237
|
}),
|
|
@@ -299,6 +359,94 @@ export const ClaudeManagerPlugin = async ({ worktree }) => {
|
|
|
299
359
|
return JSON.stringify(runs, null, 2);
|
|
300
360
|
},
|
|
301
361
|
}),
|
|
362
|
+
claude_manager_approval_policy: tool({
|
|
363
|
+
description: 'View the current tool approval policy: rules, default action, and enabled status.',
|
|
364
|
+
args: {},
|
|
365
|
+
async execute(_args, context) {
|
|
366
|
+
annotateToolRun(context, 'Reading approval policy', {});
|
|
367
|
+
return JSON.stringify(services.approvalManager.getPolicy(), null, 2);
|
|
368
|
+
},
|
|
369
|
+
}),
|
|
370
|
+
claude_manager_approval_decisions: tool({
|
|
371
|
+
description: 'View recent tool approval decisions. Shows what tools were allowed or denied. ' +
|
|
372
|
+
'Use deniedOnly to see only denied calls.',
|
|
373
|
+
args: {
|
|
374
|
+
limit: tool.schema.number().optional(),
|
|
375
|
+
deniedOnly: tool.schema.boolean().optional(),
|
|
376
|
+
},
|
|
377
|
+
async execute(args, context) {
|
|
378
|
+
annotateToolRun(context, 'Reading approval decisions', {});
|
|
379
|
+
const decisions = args.deniedOnly
|
|
380
|
+
? services.approvalManager.getDeniedDecisions(args.limit)
|
|
381
|
+
: services.approvalManager.getDecisions(args.limit);
|
|
382
|
+
return JSON.stringify({ total: decisions.length, decisions }, null, 2);
|
|
383
|
+
},
|
|
384
|
+
}),
|
|
385
|
+
claude_manager_approval_update: tool({
|
|
386
|
+
description: 'Update the tool approval policy. Add/remove rules, change default action, or enable/disable. ' +
|
|
387
|
+
'Rules are evaluated top-to-bottom; first match wins.',
|
|
388
|
+
args: {
|
|
389
|
+
action: tool.schema.enum([
|
|
390
|
+
'addRule',
|
|
391
|
+
'removeRule',
|
|
392
|
+
'setDefault',
|
|
393
|
+
'setEnabled',
|
|
394
|
+
'clearDecisions',
|
|
395
|
+
]),
|
|
396
|
+
ruleId: tool.schema.string().optional(),
|
|
397
|
+
toolPattern: tool.schema.string().optional(),
|
|
398
|
+
inputPattern: tool.schema.string().optional(),
|
|
399
|
+
ruleAction: tool.schema.enum(['allow', 'deny']).optional(),
|
|
400
|
+
denyMessage: tool.schema.string().optional(),
|
|
401
|
+
description: tool.schema.string().optional(),
|
|
402
|
+
position: tool.schema.number().optional(),
|
|
403
|
+
defaultAction: tool.schema.enum(['allow', 'deny']).optional(),
|
|
404
|
+
enabled: tool.schema.boolean().optional(),
|
|
405
|
+
},
|
|
406
|
+
async execute(args, context) {
|
|
407
|
+
annotateToolRun(context, `Updating approval: ${args.action}`, {});
|
|
408
|
+
if (args.action === 'addRule') {
|
|
409
|
+
if (!args.ruleId || !args.toolPattern || !args.ruleAction) {
|
|
410
|
+
return JSON.stringify({
|
|
411
|
+
error: 'addRule requires ruleId, toolPattern, and ruleAction',
|
|
412
|
+
});
|
|
413
|
+
}
|
|
414
|
+
services.approvalManager.addRule({
|
|
415
|
+
id: args.ruleId,
|
|
416
|
+
toolPattern: args.toolPattern,
|
|
417
|
+
inputPattern: args.inputPattern,
|
|
418
|
+
action: args.ruleAction,
|
|
419
|
+
denyMessage: args.denyMessage,
|
|
420
|
+
description: args.description,
|
|
421
|
+
}, args.position);
|
|
422
|
+
}
|
|
423
|
+
else if (args.action === 'removeRule') {
|
|
424
|
+
if (!args.ruleId) {
|
|
425
|
+
return JSON.stringify({ error: 'removeRule requires ruleId' });
|
|
426
|
+
}
|
|
427
|
+
const removed = services.approvalManager.removeRule(args.ruleId);
|
|
428
|
+
return JSON.stringify({ removed });
|
|
429
|
+
}
|
|
430
|
+
else if (args.action === 'setDefault') {
|
|
431
|
+
if (!args.defaultAction) {
|
|
432
|
+
return JSON.stringify({
|
|
433
|
+
error: 'setDefault requires defaultAction',
|
|
434
|
+
});
|
|
435
|
+
}
|
|
436
|
+
services.approvalManager.setDefaultAction(args.defaultAction);
|
|
437
|
+
}
|
|
438
|
+
else if (args.action === 'setEnabled') {
|
|
439
|
+
if (args.enabled === undefined) {
|
|
440
|
+
return JSON.stringify({ error: 'setEnabled requires enabled' });
|
|
441
|
+
}
|
|
442
|
+
services.approvalManager.setEnabled(args.enabled);
|
|
443
|
+
}
|
|
444
|
+
else if (args.action === 'clearDecisions') {
|
|
445
|
+
services.approvalManager.clearDecisions();
|
|
446
|
+
}
|
|
447
|
+
return JSON.stringify(services.approvalManager.getPolicy(), null, 2);
|
|
448
|
+
},
|
|
449
|
+
}),
|
|
302
450
|
},
|
|
303
451
|
};
|
|
304
452
|
};
|
|
@@ -1,8 +1,12 @@
|
|
|
1
1
|
import { ClaudeSessionService } from '../claude/claude-session.service.js';
|
|
2
|
+
import { SessionLiveTailer } from '../claude/session-live-tailer.js';
|
|
3
|
+
import { ToolApprovalManager } from '../claude/tool-approval-manager.js';
|
|
2
4
|
import { PersistentManager } from '../manager/persistent-manager.js';
|
|
3
5
|
interface ClaudeManagerPluginServices {
|
|
4
6
|
manager: PersistentManager;
|
|
5
7
|
sessions: ClaudeSessionService;
|
|
8
|
+
approvalManager: ToolApprovalManager;
|
|
9
|
+
liveTailer: SessionLiveTailer;
|
|
6
10
|
}
|
|
7
11
|
export declare function getOrCreatePluginServices(worktree: string): ClaudeManagerPluginServices;
|
|
8
12
|
export {};
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import { ClaudeAgentSdkAdapter } from '../claude/claude-agent-sdk-adapter.js';
|
|
2
2
|
import { ClaudeSessionService } from '../claude/claude-session.service.js';
|
|
3
|
+
import { SessionLiveTailer } from '../claude/session-live-tailer.js';
|
|
4
|
+
import { ToolApprovalManager } from '../claude/tool-approval-manager.js';
|
|
3
5
|
import { ClaudeMetadataService } from '../metadata/claude-metadata.service.js';
|
|
4
6
|
import { RepoClaudeConfigReader } from '../metadata/repo-claude-config-reader.js';
|
|
5
7
|
import { FileRunStateStore } from '../state/file-run-state-store.js';
|
|
@@ -15,7 +17,8 @@ export function getOrCreatePluginServices(worktree) {
|
|
|
15
17
|
if (cachedServices) {
|
|
16
18
|
return cachedServices;
|
|
17
19
|
}
|
|
18
|
-
const
|
|
20
|
+
const approvalManager = new ToolApprovalManager();
|
|
21
|
+
const sdkAdapter = new ClaudeAgentSdkAdapter(undefined, approvalManager);
|
|
19
22
|
const metadataService = new ClaudeMetadataService(new RepoClaudeConfigReader(), sdkAdapter);
|
|
20
23
|
const sessionService = new ClaudeSessionService(sdkAdapter, metadataService);
|
|
21
24
|
const contextTracker = new ContextTracker();
|
|
@@ -26,9 +29,12 @@ export function getOrCreatePluginServices(worktree) {
|
|
|
26
29
|
const manager = new PersistentManager(sessionController, gitOps, stateStore, contextTracker, transcriptStore);
|
|
27
30
|
// Try to restore active session state (fire and forget)
|
|
28
31
|
manager.tryRestore(worktree).catch(() => { });
|
|
32
|
+
const liveTailer = new SessionLiveTailer();
|
|
29
33
|
const services = {
|
|
30
34
|
manager,
|
|
31
35
|
sessions: sessionService,
|
|
36
|
+
approvalManager,
|
|
37
|
+
liveTailer,
|
|
32
38
|
};
|
|
33
39
|
serviceCache.set(worktree, services);
|
|
34
40
|
return services;
|
package/dist/prompts/registry.js
CHANGED
|
@@ -1,41 +1,122 @@
|
|
|
1
1
|
export const managerPromptRegistry = {
|
|
2
2
|
managerSystemPrompt: [
|
|
3
|
-
'You
|
|
4
|
-
'
|
|
3
|
+
'You are a senior IC operating Claude Code through a persistent session.',
|
|
4
|
+
'Your job is to make Claude Code do the work — not to write code yourself.',
|
|
5
|
+
'Think like a staff engineer: correctness, maintainability, tests, rollback safety,',
|
|
6
|
+
'and clear communication to the user.',
|
|
5
7
|
'',
|
|
6
|
-
'##
|
|
7
|
-
'
|
|
8
|
-
'
|
|
9
|
-
'
|
|
10
|
-
'
|
|
11
|
-
'
|
|
8
|
+
'## Decision loop',
|
|
9
|
+
'On every turn, choose exactly one action:',
|
|
10
|
+
' investigate — read files, grep, search the codebase to build context',
|
|
11
|
+
' delegate — send a focused instruction to Claude Code via claude_manager_send',
|
|
12
|
+
' review — run claude_manager_git_diff to inspect what changed',
|
|
13
|
+
' validate — tell Claude Code to run tests, lint, or typecheck',
|
|
14
|
+
' commit — checkpoint good work with claude_manager_git_commit',
|
|
15
|
+
' correct — send a targeted fix instruction (never "try again")',
|
|
16
|
+
' reset — discard bad work with claude_manager_git_reset',
|
|
17
|
+
' ask — ask the user one narrow, high-value question',
|
|
18
|
+
'',
|
|
19
|
+
'Default order: investigate → delegate → review → validate → commit.',
|
|
20
|
+
'Skip steps only when you have strong evidence they are unnecessary.',
|
|
21
|
+
'',
|
|
22
|
+
'## Before you delegate',
|
|
23
|
+
'1. Read the relevant files yourself (you have read, grep, glob).',
|
|
24
|
+
' For broad investigations, scope them narrowly or use subagents to avoid',
|
|
25
|
+
' polluting your own context with excessive file contents.',
|
|
26
|
+
'2. Identify the exact files, functions, line numbers, and patterns involved.',
|
|
27
|
+
'3. Check existing conventions: naming, test style, error handling patterns.',
|
|
28
|
+
'4. Craft an instruction that a senior engineer would find unambiguous.',
|
|
29
|
+
' Bad: "Fix the auth bug"',
|
|
30
|
+
' Good: "In src/auth/session.ts, the `validateToken` function (line 42)',
|
|
31
|
+
' throws on expired tokens instead of returning null. Change it to',
|
|
32
|
+
' return null and update the caller in src/routes/login.ts:87."',
|
|
33
|
+
'',
|
|
34
|
+
'## After delegation — mandatory review',
|
|
35
|
+
'Never claim success without evidence:',
|
|
36
|
+
'1. claude_manager_git_diff — read the actual diff, not just the summary.',
|
|
37
|
+
'2. Verify the diff matches what you asked for. Check for:',
|
|
38
|
+
' - Unintended changes or regressions',
|
|
39
|
+
' - Missing test updates',
|
|
40
|
+
' - Style violations against repo conventions',
|
|
41
|
+
'3. If changes look correct, tell Claude Code to run tests/lint/typecheck.',
|
|
42
|
+
'4. Only commit after verification passes.',
|
|
43
|
+
'5. If the diff is wrong: send a specific correction or reset.',
|
|
44
|
+
'',
|
|
45
|
+
'## Handling ambiguity',
|
|
46
|
+
'When requirements are unclear:',
|
|
47
|
+
'1. First, try to resolve it yourself — read code, check tests, grep for usage.',
|
|
48
|
+
'2. If ambiguity remains, ask the user ONE specific question.',
|
|
49
|
+
' Bad: "What should I do?"',
|
|
50
|
+
' Good: "The `UserService` has both `deactivate()` and `softDelete()` —',
|
|
51
|
+
' should the new endpoint use deactivation (reversible) or',
|
|
52
|
+
' soft-delete (audit-logged)?"',
|
|
53
|
+
'3. Never block on multiple questions at once.',
|
|
54
|
+
'',
|
|
55
|
+
'## Correction and recovery',
|
|
56
|
+
'If Claude Code produces wrong output:',
|
|
57
|
+
'1. First correction: send a specific, targeted fix instruction.',
|
|
58
|
+
'2. Second correction on the same issue: reset, clear the session,',
|
|
59
|
+
' and rewrite the prompt incorporating lessons from both failures.',
|
|
60
|
+
'Never send three corrections for the same problem in one session.',
|
|
61
|
+
'',
|
|
62
|
+
'## Multi-step tasks',
|
|
63
|
+
'- Decompose large tasks into sequential focused instructions.',
|
|
64
|
+
'- Commit after each successful step (checkpoint for rollback).',
|
|
65
|
+
'- Tell Claude Code to use subagents for independent parallel work.',
|
|
66
|
+
'- For complex design decisions, tell Claude Code to "think hard".',
|
|
67
|
+
'- Prefer small diffs — they are easier to review and safer to ship.',
|
|
12
68
|
'',
|
|
13
69
|
'## Context management',
|
|
14
|
-
'Check the context snapshot
|
|
15
|
-
'- Under 50%: proceed freely',
|
|
16
|
-
'- 50
|
|
17
|
-
'- Over 70%: compact or clear before heavy
|
|
18
|
-
'- Over 85
|
|
19
|
-
'',
|
|
20
|
-
'##
|
|
21
|
-
'
|
|
22
|
-
'
|
|
23
|
-
'
|
|
24
|
-
'
|
|
25
|
-
'
|
|
70
|
+
'Check the context snapshot returned by each send:',
|
|
71
|
+
'- Under 50%: proceed freely.',
|
|
72
|
+
'- 50–70%: finish current step, then evaluate if a fresh session is needed.',
|
|
73
|
+
'- Over 70%: compact or clear before sending heavy instructions.',
|
|
74
|
+
'- Over 85%: clear the session immediately.',
|
|
75
|
+
'',
|
|
76
|
+
'## Tools reference',
|
|
77
|
+
'claude_manager_send — send instruction (creates or resumes session)',
|
|
78
|
+
'claude_manager_git_diff — review all uncommitted changes',
|
|
79
|
+
'claude_manager_git_commit — stage all + commit',
|
|
80
|
+
'claude_manager_git_reset — hard reset + clean (destructive)',
|
|
81
|
+
'claude_manager_clear — drop session, next send starts fresh',
|
|
82
|
+
'claude_manager_status — context health snapshot',
|
|
83
|
+
'claude_manager_metadata — inspect repo Claude config',
|
|
84
|
+
'claude_manager_sessions — list sessions or read transcripts',
|
|
85
|
+
'claude_manager_runs — list or inspect run records',
|
|
86
|
+
'',
|
|
87
|
+
'## Autonomy blockers — surface these to the user',
|
|
88
|
+
'Be candid about what you cannot do autonomously:',
|
|
89
|
+
'- Credentials, API keys, or secrets you do not have.',
|
|
90
|
+
'- Architectural decisions with trade-offs the user should weigh.',
|
|
91
|
+
'- Destructive actions on shared state (deploy, publish, force-push).',
|
|
92
|
+
'- Access to external services or environments you cannot reach.',
|
|
93
|
+
'State the blocker, what you need, and a concrete suggestion to unblock.',
|
|
26
94
|
].join('\n'),
|
|
27
95
|
claudeCodeSessionPrompt: [
|
|
28
|
-
'You are
|
|
29
|
-
'as a precise instruction from a
|
|
30
|
-
'',
|
|
31
|
-
'
|
|
32
|
-
'- Execute instructions directly
|
|
33
|
-
'-
|
|
34
|
-
'-
|
|
35
|
-
'-
|
|
36
|
-
'
|
|
37
|
-
'
|
|
38
|
-
'-
|
|
96
|
+
'You are directed by an expert automated operator.',
|
|
97
|
+
'Treat each message as a precise instruction from a senior engineer.',
|
|
98
|
+
'',
|
|
99
|
+
'## Execution rules',
|
|
100
|
+
'- Execute instructions directly. Do not ask for clarification.',
|
|
101
|
+
'- Be concise — no preamble, no restating the task.',
|
|
102
|
+
'- Prefer targeted file reads over reading entire files.',
|
|
103
|
+
'- Use the Agent tool for independent parallel work.',
|
|
104
|
+
'',
|
|
105
|
+
'## Quality expectations',
|
|
106
|
+
'- Follow existing repo conventions (naming, style, patterns).',
|
|
107
|
+
'- When creating or modifying code, consider edge cases and error handling.',
|
|
108
|
+
'- When modifying existing code, preserve surrounding style and structure.',
|
|
109
|
+
'- If asked to implement a feature, include relevant tests unless told otherwise.',
|
|
110
|
+
'- Run tests/lint/typecheck when instructed; report exact output on failure.',
|
|
111
|
+
'',
|
|
112
|
+
'## Git boundary — do NOT run these commands:',
|
|
113
|
+
'git commit, git push, git reset, git checkout, git stash.',
|
|
114
|
+
'The operator manages all git operations externally.',
|
|
115
|
+
'',
|
|
116
|
+
'## Reporting',
|
|
117
|
+
'- End with a brief verification summary: what was done, what was verified.',
|
|
118
|
+
'- Report blockers immediately with specifics: file, line, error message.',
|
|
119
|
+
'- If a task is partially complete, state exactly what remains.',
|
|
39
120
|
].join('\n'),
|
|
40
121
|
contextWarnings: {
|
|
41
122
|
moderate: 'Session context is filling up ({percent}% estimated). Consider whether a fresh session would be more efficient.',
|
|
@@ -57,6 +57,7 @@ export interface RunClaudeSessionInput {
|
|
|
57
57
|
prompt: string;
|
|
58
58
|
systemPrompt?: string;
|
|
59
59
|
model?: string;
|
|
60
|
+
effort?: 'low' | 'medium' | 'high' | 'max';
|
|
60
61
|
permissionMode?: 'default' | 'acceptEdits' | 'plan' | 'dontAsk';
|
|
61
62
|
allowedTools?: string[];
|
|
62
63
|
disallowedTools?: string[];
|
|
@@ -67,6 +68,7 @@ export interface RunClaudeSessionInput {
|
|
|
67
68
|
includePartialMessages?: boolean;
|
|
68
69
|
settingSources?: ClaudeSettingSource[];
|
|
69
70
|
maxTurns?: number;
|
|
71
|
+
abortSignal?: AbortSignal;
|
|
70
72
|
}
|
|
71
73
|
export interface ClaudeSessionRunResult {
|
|
72
74
|
sessionId?: string;
|
|
@@ -166,3 +168,41 @@ export interface PersistentRunRecord {
|
|
|
166
168
|
export interface PersistentRunResult {
|
|
167
169
|
run: PersistentRunRecord;
|
|
168
170
|
}
|
|
171
|
+
export interface LiveTailEvent {
|
|
172
|
+
type: 'line' | 'error' | 'end';
|
|
173
|
+
sessionId: string;
|
|
174
|
+
data?: unknown;
|
|
175
|
+
rawLine?: string;
|
|
176
|
+
error?: string;
|
|
177
|
+
}
|
|
178
|
+
export interface ToolOutputPreview {
|
|
179
|
+
toolUseId: string;
|
|
180
|
+
content: string;
|
|
181
|
+
isError: boolean;
|
|
182
|
+
}
|
|
183
|
+
export interface ToolApprovalRule {
|
|
184
|
+
id: string;
|
|
185
|
+
description?: string;
|
|
186
|
+
/** Tool name — exact match or glob with * wildcard */
|
|
187
|
+
toolPattern: string;
|
|
188
|
+
/** Optional substring match against JSON-serialized tool input */
|
|
189
|
+
inputPattern?: string;
|
|
190
|
+
action: 'allow' | 'deny';
|
|
191
|
+
denyMessage?: string;
|
|
192
|
+
}
|
|
193
|
+
export interface ToolApprovalPolicy {
|
|
194
|
+
rules: ToolApprovalRule[];
|
|
195
|
+
defaultAction: 'allow' | 'deny';
|
|
196
|
+
defaultDenyMessage?: string;
|
|
197
|
+
enabled: boolean;
|
|
198
|
+
}
|
|
199
|
+
export interface ToolApprovalDecision {
|
|
200
|
+
timestamp: string;
|
|
201
|
+
toolName: string;
|
|
202
|
+
inputPreview: string;
|
|
203
|
+
title?: string;
|
|
204
|
+
matchedRuleId: string;
|
|
205
|
+
action: 'allow' | 'deny';
|
|
206
|
+
denyMessage?: string;
|
|
207
|
+
agentId?: string;
|
|
208
|
+
}
|