@doingdev/opencode-claude-manager-plugin 0.1.9 → 0.1.11
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 +26 -29
- package/dist/claude/claude-agent-sdk-adapter.js +249 -62
- package/dist/claude/delegated-can-use-tool.d.ts +7 -0
- package/dist/claude/delegated-can-use-tool.js +178 -0
- package/dist/index.d.ts +1 -1
- package/dist/manager/context-tracker.d.ts +33 -0
- package/dist/manager/context-tracker.js +108 -0
- package/dist/manager/git-operations.d.ts +12 -0
- package/dist/manager/git-operations.js +76 -0
- package/dist/manager/manager-orchestrator.d.ts +1 -4
- package/dist/manager/manager-orchestrator.js +37 -53
- package/dist/manager/persistent-manager.d.ts +64 -0
- package/dist/manager/persistent-manager.js +152 -0
- package/dist/manager/session-controller.d.ts +38 -0
- package/dist/manager/session-controller.js +135 -0
- package/dist/manager/task-planner.d.ts +2 -2
- package/dist/manager/task-planner.js +4 -31
- package/dist/plugin/claude-code-permission-bridge.d.ts +15 -0
- package/dist/plugin/claude-code-permission-bridge.js +184 -0
- package/dist/plugin/claude-manager.plugin.d.ts +2 -2
- package/dist/plugin/claude-manager.plugin.js +150 -192
- package/dist/plugin/service-factory.d.ts +2 -2
- package/dist/plugin/service-factory.js +12 -4
- package/dist/prompts/registry.js +42 -8
- package/dist/state/file-run-state-store.d.ts +5 -5
- package/dist/types/contracts.d.ts +68 -45
- package/dist/util/transcript-append.d.ts +7 -0
- package/dist/util/transcript-append.js +29 -0
- package/package.json +10 -10
package/README.md
CHANGED
|
@@ -4,13 +4,13 @@ This package provides an OpenCode plugin that lets an OpenCode-side manager agen
|
|
|
4
4
|
|
|
5
5
|
## Overview
|
|
6
6
|
|
|
7
|
-
Use this when you want OpenCode to act as a manager over Claude Code instead of talking to Claude directly. The plugin gives OpenCode a stable tool surface for discovering Claude metadata, delegating work to Claude sessions, splitting tasks into subagents
|
|
7
|
+
Use this when you want OpenCode to act as a manager over Claude Code instead of talking to Claude directly. The plugin gives OpenCode a stable tool surface for discovering Claude metadata, delegating work to Claude sessions, and splitting tasks into subagents (all using the same working directory).
|
|
8
8
|
|
|
9
9
|
## Features
|
|
10
10
|
|
|
11
11
|
- Runs Claude Code tasks from OpenCode through `@anthropic-ai/claude-agent-sdk`.
|
|
12
12
|
- Discovers repo-local Claude metadata from `.claude/skills`, `.claude/commands`, `CLAUDE.md`, and settings hooks.
|
|
13
|
-
- Splits multi-step tasks into subagents
|
|
13
|
+
- Splits multi-step tasks into subagents that run sequentially in the same repository directory.
|
|
14
14
|
- Persists manager runs under `.claude-manager/runs` so sessions can be inspected later.
|
|
15
15
|
- Exposes manager-facing tools instead of relying on undocumented plugin-defined slash commands.
|
|
16
16
|
|
|
@@ -19,21 +19,20 @@ Use this when you want OpenCode to act as a manager over Claude Code instead of
|
|
|
19
19
|
- Node `22+`
|
|
20
20
|
- OpenCode with plugin loading enabled
|
|
21
21
|
- Access to Claude Code / Claude Agent SDK on the machine where OpenCode is running
|
|
22
|
-
- A git repository if you want automatic worktree allocation
|
|
23
22
|
|
|
24
23
|
## Installation
|
|
25
24
|
|
|
26
|
-
Install from npm:
|
|
25
|
+
Install from the npm registry:
|
|
27
26
|
|
|
28
27
|
```bash
|
|
29
|
-
|
|
28
|
+
pnpm add @doingdev/opencode-claude-manager-plugin
|
|
30
29
|
```
|
|
31
30
|
|
|
32
31
|
Or for local development in this repo:
|
|
33
32
|
|
|
34
33
|
```bash
|
|
35
|
-
|
|
36
|
-
|
|
34
|
+
pnpm install
|
|
35
|
+
pnpm run build
|
|
37
36
|
```
|
|
38
37
|
|
|
39
38
|
## OpenCode Config
|
|
@@ -50,11 +49,10 @@ If you are testing locally, point OpenCode at the local package or plugin file u
|
|
|
50
49
|
|
|
51
50
|
## OpenCode tools
|
|
52
51
|
|
|
53
|
-
- `claude_manager_run` - run a task through Claude with optional splitting
|
|
52
|
+
- `claude_manager_run` - run a task through Claude with optional splitting into subagents; returns a compact output summary and a `runId` for deeper inspection
|
|
54
53
|
- `claude_manager_metadata` - inspect available Claude commands, skills, hooks, and settings
|
|
55
54
|
- `claude_manager_sessions` - list Claude sessions or inspect a saved transcript
|
|
56
55
|
- `claude_manager_runs` - inspect persisted manager run records
|
|
57
|
-
- `claude_manager_cleanup_run` - explicitly remove worktrees created for a prior manager run
|
|
58
56
|
|
|
59
57
|
## Plugin-provided agents and commands
|
|
60
58
|
|
|
@@ -77,7 +75,7 @@ Typical flow inside OpenCode:
|
|
|
77
75
|
Example task:
|
|
78
76
|
|
|
79
77
|
```text
|
|
80
|
-
Use claude_manager_run to split this implementation into subagents
|
|
78
|
+
Use claude_manager_run to split this implementation into subagents and summarize the final result.
|
|
81
79
|
```
|
|
82
80
|
|
|
83
81
|
## Local Development
|
|
@@ -85,11 +83,11 @@ Use claude_manager_run to split this implementation into subagents, use worktree
|
|
|
85
83
|
Clone the repo and run:
|
|
86
84
|
|
|
87
85
|
```bash
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
86
|
+
pnpm install
|
|
87
|
+
pnpm run lint
|
|
88
|
+
pnpm run typecheck
|
|
89
|
+
pnpm run test
|
|
90
|
+
pnpm run build
|
|
93
91
|
```
|
|
94
92
|
|
|
95
93
|
The compiled plugin output is written to `dist/`.
|
|
@@ -119,13 +117,13 @@ Notes for trusted publishing:
|
|
|
119
117
|
Release flow:
|
|
120
118
|
|
|
121
119
|
```bash
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
120
|
+
pnpm login
|
|
121
|
+
pnpm whoami
|
|
122
|
+
pnpm version patch
|
|
123
|
+
pnpm run lint
|
|
124
|
+
pnpm run typecheck
|
|
125
|
+
pnpm run test
|
|
126
|
+
pnpm run build
|
|
129
127
|
```
|
|
130
128
|
|
|
131
129
|
Then publish from GitHub by either:
|
|
@@ -138,14 +136,13 @@ After trusted publishing is working, you can tighten npm package security by dis
|
|
|
138
136
|
## Limitations
|
|
139
137
|
|
|
140
138
|
- Claude slash commands and skills come primarily from filesystem discovery; SDK probing is available but optional.
|
|
141
|
-
-
|
|
142
|
-
- Worktrees are preserved by default so you can inspect changes; clean them up explicitly with `claude_manager_cleanup_run`.
|
|
139
|
+
- Multiple subagents share one working directory and run one after another to avoid overlapping edits.
|
|
143
140
|
- Run state is local to the repo under `.claude-manager/` and is ignored by git.
|
|
144
141
|
|
|
145
142
|
## Scripts
|
|
146
143
|
|
|
147
|
-
- `
|
|
148
|
-
- `
|
|
149
|
-
- `
|
|
150
|
-
- `
|
|
151
|
-
- `
|
|
144
|
+
- `pnpm run build`
|
|
145
|
+
- `pnpm run typecheck`
|
|
146
|
+
- `pnpm run lint`
|
|
147
|
+
- `pnpm run format`
|
|
148
|
+
- `pnpm run test`
|
|
@@ -1,39 +1,59 @@
|
|
|
1
1
|
import { getSessionMessages, listSessions, query, } from '@anthropic-ai/claude-agent-sdk';
|
|
2
|
+
import { appendTranscriptEvents } from '../util/transcript-append.js';
|
|
2
3
|
const defaultFacade = {
|
|
3
4
|
query,
|
|
4
5
|
listSessions,
|
|
5
6
|
getSessionMessages,
|
|
6
7
|
};
|
|
8
|
+
const TOOL_INPUT_PREVIEW_MAX = 2000;
|
|
9
|
+
const USER_MESSAGE_MAX = 4000;
|
|
7
10
|
export class ClaudeAgentSdkAdapter {
|
|
8
11
|
sdkFacade;
|
|
9
12
|
constructor(sdkFacade = defaultFacade) {
|
|
10
13
|
this.sdkFacade = sdkFacade;
|
|
11
14
|
}
|
|
12
15
|
async runSession(input, onEvent) {
|
|
16
|
+
const options = this.buildOptions(input);
|
|
17
|
+
const includePartials = options.includePartialMessages === true;
|
|
13
18
|
const sessionQuery = this.sdkFacade.query({
|
|
14
19
|
prompt: input.prompt,
|
|
15
|
-
options
|
|
20
|
+
options,
|
|
16
21
|
});
|
|
17
|
-
|
|
22
|
+
let events = [];
|
|
18
23
|
let finalText = '';
|
|
19
24
|
let sessionId;
|
|
20
25
|
let turns;
|
|
21
26
|
let totalCostUsd;
|
|
27
|
+
let inputTokens;
|
|
28
|
+
let outputTokens;
|
|
29
|
+
let contextWindowSize;
|
|
22
30
|
try {
|
|
23
31
|
for await (const message of sessionQuery) {
|
|
24
|
-
const
|
|
25
|
-
|
|
26
|
-
|
|
32
|
+
const batch = normalizeSdkMessages(message, includePartials);
|
|
33
|
+
for (const event of batch) {
|
|
34
|
+
sessionId ??= event.sessionId;
|
|
35
|
+
if (event.type === 'result') {
|
|
36
|
+
finalText = event.text;
|
|
37
|
+
turns = event.turns;
|
|
38
|
+
totalCostUsd = event.totalCostUsd;
|
|
39
|
+
}
|
|
40
|
+
events = appendTranscriptEvents(events, [event]);
|
|
41
|
+
if (onEvent) {
|
|
42
|
+
await onEvent(event);
|
|
43
|
+
}
|
|
27
44
|
}
|
|
28
|
-
|
|
29
|
-
if (
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
45
|
+
// Extract usage data from result messages
|
|
46
|
+
if (message.type === 'result') {
|
|
47
|
+
const usage = extractUsageFromResult(message);
|
|
48
|
+
if (usage.inputTokens !== undefined) {
|
|
49
|
+
inputTokens = usage.inputTokens;
|
|
50
|
+
}
|
|
51
|
+
if (usage.outputTokens !== undefined) {
|
|
52
|
+
outputTokens = usage.outputTokens;
|
|
53
|
+
}
|
|
54
|
+
if (usage.contextWindowSize !== undefined) {
|
|
55
|
+
contextWindowSize = usage.contextWindowSize;
|
|
56
|
+
}
|
|
37
57
|
}
|
|
38
58
|
}
|
|
39
59
|
}
|
|
@@ -46,6 +66,9 @@ export class ClaudeAgentSdkAdapter {
|
|
|
46
66
|
finalText,
|
|
47
67
|
turns,
|
|
48
68
|
totalCostUsd,
|
|
69
|
+
inputTokens,
|
|
70
|
+
outputTokens,
|
|
71
|
+
contextWindowSize,
|
|
49
72
|
};
|
|
50
73
|
}
|
|
51
74
|
async listSavedSessions(cwd) {
|
|
@@ -110,7 +133,7 @@ export class ClaudeAgentSdkAdapter {
|
|
|
110
133
|
settingSources: input.settingSources,
|
|
111
134
|
maxTurns: input.maxTurns,
|
|
112
135
|
model: input.model,
|
|
113
|
-
permissionMode: input.permissionMode,
|
|
136
|
+
permissionMode: input.permissionMode ?? 'acceptEdits',
|
|
114
137
|
systemPrompt: input.systemPrompt
|
|
115
138
|
? { type: 'preset', preset: 'claude_code', append: input.systemPrompt }
|
|
116
139
|
: { type: 'preset', preset: 'claude_code' },
|
|
@@ -125,73 +148,210 @@ export class ClaudeAgentSdkAdapter {
|
|
|
125
148
|
return options;
|
|
126
149
|
}
|
|
127
150
|
}
|
|
128
|
-
function
|
|
151
|
+
function normalizeSdkMessages(message, includePartials) {
|
|
129
152
|
const sessionId = 'session_id' in message ? message.session_id : undefined;
|
|
130
153
|
if (message.type === 'assistant') {
|
|
131
|
-
return
|
|
132
|
-
type: 'assistant',
|
|
133
|
-
sessionId,
|
|
134
|
-
text: extractText(message.message),
|
|
135
|
-
rawType: message.type,
|
|
136
|
-
};
|
|
154
|
+
return normalizeAssistantSdkMessage(message, sessionId);
|
|
137
155
|
}
|
|
138
156
|
if (message.type === 'stream_event') {
|
|
157
|
+
if (!includePartials) {
|
|
158
|
+
return [];
|
|
159
|
+
}
|
|
139
160
|
const partialText = extractPartialEventText(message.event);
|
|
140
161
|
if (!partialText) {
|
|
141
|
-
return
|
|
162
|
+
return [];
|
|
142
163
|
}
|
|
143
|
-
return
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
164
|
+
return [
|
|
165
|
+
{
|
|
166
|
+
type: 'partial',
|
|
167
|
+
sessionId,
|
|
168
|
+
text: partialText,
|
|
169
|
+
rawType: message.type,
|
|
170
|
+
},
|
|
171
|
+
];
|
|
149
172
|
}
|
|
150
173
|
if (message.type === 'result') {
|
|
151
|
-
return
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
174
|
+
return [
|
|
175
|
+
{
|
|
176
|
+
type: message.is_error ? 'error' : 'result',
|
|
177
|
+
sessionId,
|
|
178
|
+
text: message.subtype === 'success'
|
|
179
|
+
? message.result
|
|
180
|
+
: message.errors.join('\n') || message.subtype,
|
|
181
|
+
turns: message.num_turns,
|
|
182
|
+
totalCostUsd: message.total_cost_usd,
|
|
183
|
+
rawType: `${message.type}:${message.subtype}`,
|
|
184
|
+
},
|
|
185
|
+
];
|
|
161
186
|
}
|
|
162
187
|
if (message.type === 'system') {
|
|
163
|
-
return
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
188
|
+
return [
|
|
189
|
+
{
|
|
190
|
+
type: message.subtype === 'init' ? 'init' : 'system',
|
|
191
|
+
sessionId,
|
|
192
|
+
text: message.subtype,
|
|
193
|
+
rawType: `${message.type}:${message.subtype}`,
|
|
194
|
+
},
|
|
195
|
+
];
|
|
169
196
|
}
|
|
170
197
|
if (message.type === 'auth_status') {
|
|
171
|
-
return
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
198
|
+
return [
|
|
199
|
+
{
|
|
200
|
+
type: 'system',
|
|
201
|
+
sessionId,
|
|
202
|
+
text: message.output.join('\n') || 'auth_status',
|
|
203
|
+
rawType: message.type,
|
|
204
|
+
},
|
|
205
|
+
];
|
|
177
206
|
}
|
|
178
207
|
if (message.type === 'prompt_suggestion') {
|
|
179
|
-
return
|
|
208
|
+
return [
|
|
209
|
+
{
|
|
210
|
+
type: 'system',
|
|
211
|
+
sessionId,
|
|
212
|
+
text: message.suggestion,
|
|
213
|
+
rawType: message.type,
|
|
214
|
+
},
|
|
215
|
+
];
|
|
216
|
+
}
|
|
217
|
+
if (message.type === 'user') {
|
|
218
|
+
return normalizeUserSdkMessage(message, sessionId);
|
|
219
|
+
}
|
|
220
|
+
return [
|
|
221
|
+
{
|
|
180
222
|
type: 'system',
|
|
181
223
|
sessionId,
|
|
182
|
-
text: message.
|
|
224
|
+
text: message.type,
|
|
183
225
|
rawType: message.type,
|
|
184
|
-
}
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
226
|
+
},
|
|
227
|
+
];
|
|
228
|
+
}
|
|
229
|
+
function normalizeAssistantSdkMessage(message, sessionId) {
|
|
230
|
+
const raw = message.message;
|
|
231
|
+
if (!isRecord(raw) || !Array.isArray(raw.content)) {
|
|
232
|
+
const text = extractText(raw);
|
|
233
|
+
return text.trim()
|
|
234
|
+
? [
|
|
235
|
+
{
|
|
236
|
+
type: 'assistant',
|
|
237
|
+
sessionId,
|
|
238
|
+
text,
|
|
239
|
+
rawType: 'assistant',
|
|
240
|
+
},
|
|
241
|
+
]
|
|
242
|
+
: [];
|
|
188
243
|
}
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
244
|
+
const out = [];
|
|
245
|
+
let textBuffer = '';
|
|
246
|
+
const flushText = () => {
|
|
247
|
+
if (textBuffer.trim()) {
|
|
248
|
+
out.push({
|
|
249
|
+
type: 'assistant',
|
|
250
|
+
sessionId,
|
|
251
|
+
text: textBuffer,
|
|
252
|
+
rawType: 'assistant',
|
|
253
|
+
});
|
|
254
|
+
}
|
|
255
|
+
textBuffer = '';
|
|
194
256
|
};
|
|
257
|
+
for (const block of raw.content) {
|
|
258
|
+
if (!isRecord(block)) {
|
|
259
|
+
continue;
|
|
260
|
+
}
|
|
261
|
+
if (block.type === 'text' && typeof block.text === 'string') {
|
|
262
|
+
textBuffer += block.text;
|
|
263
|
+
}
|
|
264
|
+
else if (block.type === 'tool_use' && typeof block.name === 'string') {
|
|
265
|
+
flushText();
|
|
266
|
+
const id = typeof block.id === 'string' ? block.id : '';
|
|
267
|
+
const preview = truncateJsonish(block.input, TOOL_INPUT_PREVIEW_MAX);
|
|
268
|
+
out.push({
|
|
269
|
+
type: 'tool_call',
|
|
270
|
+
sessionId,
|
|
271
|
+
text: JSON.stringify({
|
|
272
|
+
name: block.name,
|
|
273
|
+
id,
|
|
274
|
+
input: preview,
|
|
275
|
+
}),
|
|
276
|
+
rawType: 'assistant:tool_use',
|
|
277
|
+
});
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
flushText();
|
|
281
|
+
return out;
|
|
282
|
+
}
|
|
283
|
+
function normalizeUserSdkMessage(message, sessionId) {
|
|
284
|
+
let payload = serializeUserMessageContent(message.message);
|
|
285
|
+
if (message.tool_use_result !== undefined) {
|
|
286
|
+
const extra = truncateJsonish(message.tool_use_result, 1500);
|
|
287
|
+
if (extra) {
|
|
288
|
+
payload = payload
|
|
289
|
+
? `${payload}\n[tool_use_result] ${extra}`
|
|
290
|
+
: `[tool_use_result] ${extra}`;
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
payload = truncateString(payload, USER_MESSAGE_MAX);
|
|
294
|
+
if (!payload.trim()) {
|
|
295
|
+
return [];
|
|
296
|
+
}
|
|
297
|
+
return [
|
|
298
|
+
{
|
|
299
|
+
type: 'user',
|
|
300
|
+
sessionId,
|
|
301
|
+
text: payload,
|
|
302
|
+
rawType: 'user',
|
|
303
|
+
},
|
|
304
|
+
];
|
|
305
|
+
}
|
|
306
|
+
function serializeUserMessageContent(message) {
|
|
307
|
+
if (typeof message === 'string') {
|
|
308
|
+
return message;
|
|
309
|
+
}
|
|
310
|
+
if (!isRecord(message)) {
|
|
311
|
+
return '';
|
|
312
|
+
}
|
|
313
|
+
const content = message.content;
|
|
314
|
+
if (typeof content === 'string') {
|
|
315
|
+
return content;
|
|
316
|
+
}
|
|
317
|
+
if (!Array.isArray(content)) {
|
|
318
|
+
return truncateJsonish(message, USER_MESSAGE_MAX);
|
|
319
|
+
}
|
|
320
|
+
const lines = [];
|
|
321
|
+
for (const part of content) {
|
|
322
|
+
if (!isRecord(part)) {
|
|
323
|
+
continue;
|
|
324
|
+
}
|
|
325
|
+
if (part.type === 'text' && typeof part.text === 'string') {
|
|
326
|
+
lines.push(part.text);
|
|
327
|
+
}
|
|
328
|
+
else if (part.type === 'tool_result') {
|
|
329
|
+
const id = typeof part.tool_use_id === 'string' ? part.tool_use_id : '';
|
|
330
|
+
const body = truncateJsonish(part.content, TOOL_INPUT_PREVIEW_MAX);
|
|
331
|
+
lines.push(`[tool_result:${id}] ${body}`);
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
return lines.filter(Boolean).join('\n');
|
|
335
|
+
}
|
|
336
|
+
function truncateJsonish(value, max) {
|
|
337
|
+
if (value === undefined || value === null) {
|
|
338
|
+
return '';
|
|
339
|
+
}
|
|
340
|
+
if (typeof value === 'string') {
|
|
341
|
+
return truncateString(value, max);
|
|
342
|
+
}
|
|
343
|
+
try {
|
|
344
|
+
return truncateString(JSON.stringify(value), max);
|
|
345
|
+
}
|
|
346
|
+
catch {
|
|
347
|
+
return truncateString(String(value), max);
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
function truncateString(s, max) {
|
|
351
|
+
if (s.length <= max) {
|
|
352
|
+
return s;
|
|
353
|
+
}
|
|
354
|
+
return s.slice(0, max);
|
|
195
355
|
}
|
|
196
356
|
function isRecord(value) {
|
|
197
357
|
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
@@ -257,3 +417,30 @@ function mapAgent(agent) {
|
|
|
257
417
|
source: 'sdk',
|
|
258
418
|
};
|
|
259
419
|
}
|
|
420
|
+
function extractUsageFromResult(message) {
|
|
421
|
+
if (message.type !== 'result') {
|
|
422
|
+
return {};
|
|
423
|
+
}
|
|
424
|
+
const result = {};
|
|
425
|
+
// Extract from usage field
|
|
426
|
+
const usage = message.usage;
|
|
427
|
+
if (isRecord(usage)) {
|
|
428
|
+
if (typeof usage.input_tokens === 'number') {
|
|
429
|
+
result.inputTokens = usage.input_tokens;
|
|
430
|
+
}
|
|
431
|
+
if (typeof usage.output_tokens === 'number') {
|
|
432
|
+
result.outputTokens = usage.output_tokens;
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
// Extract contextWindow from modelUsage
|
|
436
|
+
const modelUsage = message.model_usage;
|
|
437
|
+
if (isRecord(modelUsage)) {
|
|
438
|
+
for (const model of Object.values(modelUsage)) {
|
|
439
|
+
if (isRecord(model) && typeof model.context_window === 'number') {
|
|
440
|
+
result.contextWindowSize = model.context_window;
|
|
441
|
+
break;
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
return result;
|
|
446
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { CanUseTool } from '@anthropic-ai/claude-agent-sdk';
|
|
2
|
+
import type { DelegatedToolPermissionPolicy } from '../types/contracts.js';
|
|
3
|
+
export type DelegatedCanUseToolOptions = {
|
|
4
|
+
/** Override TTY detection (for tests or custom hosts). */
|
|
5
|
+
isInteractiveTerminal?: () => boolean;
|
|
6
|
+
};
|
|
7
|
+
export declare function createDelegatedCanUseTool(policy: DelegatedToolPermissionPolicy, factoryOptions?: DelegatedCanUseToolOptions): CanUseTool;
|
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
import { createInterface } from 'node:readline/promises';
|
|
2
|
+
function defaultIsInteractiveTerminal() {
|
|
3
|
+
return Boolean(process.stdin.isTTY && process.stdout.isTTY);
|
|
4
|
+
}
|
|
5
|
+
function isRecord(value) {
|
|
6
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
7
|
+
}
|
|
8
|
+
function formatToolRequest(toolName, input, meta) {
|
|
9
|
+
const headline = meta.title?.trim() ||
|
|
10
|
+
[meta.displayName, toolName].filter(Boolean).join(': ') ||
|
|
11
|
+
toolName;
|
|
12
|
+
const lines = [headline];
|
|
13
|
+
if (meta.description?.trim()) {
|
|
14
|
+
lines.push(meta.description.trim());
|
|
15
|
+
}
|
|
16
|
+
if (meta.decisionReason?.trim()) {
|
|
17
|
+
lines.push(`Reason: ${meta.decisionReason.trim()}`);
|
|
18
|
+
}
|
|
19
|
+
if (toolName === 'Bash' && typeof input.command === 'string') {
|
|
20
|
+
lines.push(`Command: ${input.command}`);
|
|
21
|
+
if (typeof input.description === 'string' && input.description) {
|
|
22
|
+
lines.push(`Intent: ${input.description}`);
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
else if (toolName !== 'AskUserQuestion') {
|
|
26
|
+
lines.push(`Input: ${JSON.stringify(input, null, 2)}`);
|
|
27
|
+
}
|
|
28
|
+
return lines.join('\n');
|
|
29
|
+
}
|
|
30
|
+
function applyDefaultAskUserAnswers(input) {
|
|
31
|
+
const rawQuestions = input.questions;
|
|
32
|
+
if (!Array.isArray(rawQuestions)) {
|
|
33
|
+
return input;
|
|
34
|
+
}
|
|
35
|
+
const answers = {};
|
|
36
|
+
for (const entry of rawQuestions) {
|
|
37
|
+
if (!isRecord(entry)) {
|
|
38
|
+
continue;
|
|
39
|
+
}
|
|
40
|
+
const questionText = entry.question;
|
|
41
|
+
if (typeof questionText !== 'string') {
|
|
42
|
+
continue;
|
|
43
|
+
}
|
|
44
|
+
const options = entry.options;
|
|
45
|
+
if (!Array.isArray(options) || options.length === 0) {
|
|
46
|
+
continue;
|
|
47
|
+
}
|
|
48
|
+
const first = options[0];
|
|
49
|
+
if (isRecord(first) && typeof first.label === 'string') {
|
|
50
|
+
answers[questionText] = first.label;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
return {
|
|
54
|
+
...input,
|
|
55
|
+
questions: rawQuestions,
|
|
56
|
+
answers,
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
async function promptLine(question) {
|
|
60
|
+
const rl = createInterface({
|
|
61
|
+
input: process.stdin,
|
|
62
|
+
output: process.stdout,
|
|
63
|
+
});
|
|
64
|
+
try {
|
|
65
|
+
return (await rl.question(question)).trim();
|
|
66
|
+
}
|
|
67
|
+
finally {
|
|
68
|
+
rl.close();
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
async function handleAskUserQuestionInteractive(input, toolUseID) {
|
|
72
|
+
const rawQuestions = input.questions;
|
|
73
|
+
if (!Array.isArray(rawQuestions)) {
|
|
74
|
+
return {
|
|
75
|
+
behavior: 'deny',
|
|
76
|
+
message: 'AskUserQuestion invoked without a questions array; cannot collect answers interactively.',
|
|
77
|
+
toolUseID,
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
const answers = {};
|
|
81
|
+
for (const entry of rawQuestions) {
|
|
82
|
+
if (!isRecord(entry)) {
|
|
83
|
+
continue;
|
|
84
|
+
}
|
|
85
|
+
const questionText = entry.question;
|
|
86
|
+
const header = entry.header;
|
|
87
|
+
if (typeof questionText !== 'string') {
|
|
88
|
+
continue;
|
|
89
|
+
}
|
|
90
|
+
const label = typeof header === 'string' && header.trim()
|
|
91
|
+
? `${header.trim()}: ${questionText}`
|
|
92
|
+
: questionText;
|
|
93
|
+
const options = entry.options;
|
|
94
|
+
if (!Array.isArray(options) || options.length === 0) {
|
|
95
|
+
return {
|
|
96
|
+
behavior: 'deny',
|
|
97
|
+
message: `Question "${questionText}" has no options to choose from.`,
|
|
98
|
+
toolUseID,
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
process.stdout.write(`\n${label}\n`);
|
|
102
|
+
for (const [index, opt] of options.entries()) {
|
|
103
|
+
if (isRecord(opt) && typeof opt.label === 'string') {
|
|
104
|
+
const desc = typeof opt.description === 'string' ? ` — ${opt.description}` : '';
|
|
105
|
+
process.stdout.write(` ${index + 1}. ${opt.label}${desc}\n`);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
const multi = entry.multiSelect === true
|
|
109
|
+
? ' (numbers separated by commas, or type your own answer)'
|
|
110
|
+
: ' (number, or type your own answer)';
|
|
111
|
+
const response = await promptLine(`Your choice${multi}: `);
|
|
112
|
+
const parsed = parseChoiceResponse(response, options);
|
|
113
|
+
answers[questionText] = parsed;
|
|
114
|
+
}
|
|
115
|
+
return {
|
|
116
|
+
behavior: 'allow',
|
|
117
|
+
updatedInput: {
|
|
118
|
+
...input,
|
|
119
|
+
questions: rawQuestions,
|
|
120
|
+
answers,
|
|
121
|
+
},
|
|
122
|
+
toolUseID,
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
function parseChoiceResponse(response, options) {
|
|
126
|
+
const indices = response
|
|
127
|
+
.split(',')
|
|
128
|
+
.map((part) => Number.parseInt(part.trim(), 10) - 1)
|
|
129
|
+
.filter((index) => Number.isFinite(index));
|
|
130
|
+
const labels = indices
|
|
131
|
+
.filter((index) => index >= 0 && index < options.length)
|
|
132
|
+
.map((index) => {
|
|
133
|
+
const opt = options[index];
|
|
134
|
+
if (isRecord(opt) && typeof opt.label === 'string') {
|
|
135
|
+
return opt.label;
|
|
136
|
+
}
|
|
137
|
+
return '';
|
|
138
|
+
})
|
|
139
|
+
.filter(Boolean);
|
|
140
|
+
return labels.length > 0 ? labels.join(', ') : response;
|
|
141
|
+
}
|
|
142
|
+
export function createDelegatedCanUseTool(policy, factoryOptions) {
|
|
143
|
+
const isInteractiveTerminal = factoryOptions?.isInteractiveTerminal ?? defaultIsInteractiveTerminal;
|
|
144
|
+
return async (toolName, input, options) => {
|
|
145
|
+
const usePrompt = policy === 'prompt_if_tty' && isInteractiveTerminal();
|
|
146
|
+
if (toolName === 'AskUserQuestion') {
|
|
147
|
+
if (usePrompt) {
|
|
148
|
+
return handleAskUserQuestionInteractive(input, options.toolUseID);
|
|
149
|
+
}
|
|
150
|
+
return {
|
|
151
|
+
behavior: 'allow',
|
|
152
|
+
updatedInput: applyDefaultAskUserAnswers(input),
|
|
153
|
+
toolUseID: options.toolUseID,
|
|
154
|
+
};
|
|
155
|
+
}
|
|
156
|
+
if (policy === 'allow_all' || !usePrompt) {
|
|
157
|
+
return {
|
|
158
|
+
behavior: 'allow',
|
|
159
|
+
updatedInput: input,
|
|
160
|
+
toolUseID: options.toolUseID,
|
|
161
|
+
};
|
|
162
|
+
}
|
|
163
|
+
const summary = formatToolRequest(toolName, input, options);
|
|
164
|
+
const answer = await promptLine(`${summary}\nAllow this action? [y/N] `);
|
|
165
|
+
if (answer.toLowerCase() === 'y' || answer.toLowerCase() === 'yes') {
|
|
166
|
+
return {
|
|
167
|
+
behavior: 'allow',
|
|
168
|
+
updatedInput: input,
|
|
169
|
+
toolUseID: options.toolUseID,
|
|
170
|
+
};
|
|
171
|
+
}
|
|
172
|
+
return {
|
|
173
|
+
behavior: 'deny',
|
|
174
|
+
message: 'User denied this tool action in the delegated Claude Code session (TTY prompt).',
|
|
175
|
+
toolUseID: options.toolUseID,
|
|
176
|
+
};
|
|
177
|
+
};
|
|
178
|
+
}
|