@doingdev/opencode-claude-manager-plugin 0.1.34 → 0.1.35
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 +14 -15
- package/dist/claude/claude-agent-sdk-adapter.js +1 -1
- package/dist/claude/session-live-tailer.js +2 -2
- package/dist/plugin/agent-hierarchy.d.ts +1 -1
- package/dist/plugin/agent-hierarchy.js +11 -16
- package/dist/plugin/claude-manager.plugin.js +115 -69
- package/dist/prompts/registry.js +8 -8
- package/package.json +1 -1
- package/dist/metadata/claude-metadata.service.d.ts +0 -12
- package/dist/metadata/claude-metadata.service.js +0 -38
- package/dist/metadata/repo-claude-config-reader.d.ts +0 -7
- package/dist/metadata/repo-claude-config-reader.js +0 -154
- package/dist/plugin/orchestrator.plugin.d.ts +0 -2
- package/dist/plugin/orchestrator.plugin.js +0 -116
- package/dist/providers/claude-code-wrapper.d.ts +0 -13
- package/dist/providers/claude-code-wrapper.js +0 -13
- package/dist/safety/bash-safety.d.ts +0 -21
- package/dist/safety/bash-safety.js +0 -62
package/README.md
CHANGED
|
@@ -53,15 +53,15 @@ If you are testing locally, point OpenCode at the local package or plugin file u
|
|
|
53
53
|
|
|
54
54
|
### Engineer session
|
|
55
55
|
|
|
56
|
-
- `
|
|
56
|
+
- `explore` — investigate and analyze code without making edits. Read-only exploration of the codebase. Preferred first step before implementation.
|
|
57
57
|
- `message` (required) — the instruction to send.
|
|
58
|
-
- `mode` — `"plan"` (read-only investigation) or `"free"` (default, normal execution with edits).
|
|
59
58
|
- `freshSession` — set to `true` to clear the active session before sending. Use when switching to an unrelated task or when context is contaminated.
|
|
60
59
|
- `model` — `"claude-opus-4-6"` (default, recommended for most coding work), `"claude-sonnet-4-6"`, or `"claude-sonnet-4-5"` (faster/lighter tasks).
|
|
61
60
|
- `effort` — `"high"` (default), `"medium"` (lighter tasks), `"low"`, or `"max"` (especially hard problems).
|
|
62
|
-
- `
|
|
63
|
-
- `
|
|
64
|
-
- `
|
|
61
|
+
- `implement` — implement code changes; can read, edit, and create files. Use after exploration to make changes. Same args as `explore`.
|
|
62
|
+
- `compact_context` — compress session history to reclaim context window space. Preserves state while reducing token usage.
|
|
63
|
+
- `clear_session` — clear the active session to start fresh. Use when context is full or starting a new task.
|
|
64
|
+
- `session_health` — check session health metrics: context usage %, turn count, cost, and session ID.
|
|
65
65
|
|
|
66
66
|
### Git operations
|
|
67
67
|
|
|
@@ -71,9 +71,8 @@ If you are testing locally, point OpenCode at the local package or plugin file u
|
|
|
71
71
|
|
|
72
72
|
### Inspection
|
|
73
73
|
|
|
74
|
-
- `
|
|
75
|
-
- `
|
|
76
|
-
- `engineer_runs` — list or inspect persisted manager run records (may be empty if tasks were sent directly via `engineer_send` rather than the run-tracking path).
|
|
74
|
+
- `list_transcripts` — list available session transcripts or inspect a specific transcript by ID.
|
|
75
|
+
- `list_history` — list persistent run records from the manager or inspect a specific run.
|
|
77
76
|
|
|
78
77
|
### Tool approval
|
|
79
78
|
|
|
@@ -86,7 +85,7 @@ If you are testing locally, point OpenCode at the local package or plugin file u
|
|
|
86
85
|
The plugin registers a CTO → Manager → Engineer hierarchy through the OpenCode plugin `config` hook:
|
|
87
86
|
|
|
88
87
|
- **`cto`** (primary agent) — sets direction and orchestrates work by spawning `manager` subagents. Has read/search/web tools but does NOT operate Claude Code directly.
|
|
89
|
-
- **`manager`** (subagent) — operates a Claude Code engineer through a persistent session. Has the full tool surface (`
|
|
88
|
+
- **`manager`** (subagent) — operates a Claude Code engineer through a persistent session. Has the full tool surface (`explore`, `implement`, `compact_context`, `clear_session`, `session_health`, `list_transcripts`, `list_history`, `git_*`, `approval_*`) plus read/search/web tools for investigation.
|
|
90
89
|
- **Engineer** — the Claude Code persistent session itself (not an OpenCode agent). Receives instructions from the manager, executes code changes, and reports results.
|
|
91
90
|
|
|
92
91
|
These are added to OpenCode config at runtime by the plugin, so they do not require separate manual `opencode.json` entries.
|
|
@@ -95,27 +94,27 @@ These are added to OpenCode config at runtime by the plugin, so they do not requ
|
|
|
95
94
|
|
|
96
95
|
Typical flow inside OpenCode:
|
|
97
96
|
|
|
98
|
-
1.
|
|
99
|
-
2.
|
|
97
|
+
1. Explore the codebase with `explore`.
|
|
98
|
+
2. Implement changes with `implement`.
|
|
100
99
|
3. Review changes with `git_diff`, then commit or reset.
|
|
101
|
-
4. Inspect saved Claude history with `
|
|
100
|
+
4. Inspect saved Claude history with `list_transcripts` or prior orchestration records with `list_history`.
|
|
102
101
|
|
|
103
102
|
Example tasks:
|
|
104
103
|
|
|
105
104
|
```text
|
|
106
|
-
Use
|
|
105
|
+
Use implement to add the new validation logic in src/auth.ts, then review with git_diff.
|
|
107
106
|
```
|
|
108
107
|
|
|
109
108
|
Start a fresh session for an unrelated task:
|
|
110
109
|
|
|
111
110
|
```text
|
|
112
|
-
Use
|
|
111
|
+
Use explore with freshSession:true to investigate the failing CI test in test/api.test.ts.
|
|
113
112
|
```
|
|
114
113
|
|
|
115
114
|
Reclaim context mid-session:
|
|
116
115
|
|
|
117
116
|
```text
|
|
118
|
-
Use
|
|
117
|
+
Use compact_context to free up context, then continue with the next implementation step.
|
|
119
118
|
```
|
|
120
119
|
|
|
121
120
|
## Local Development
|
|
@@ -409,7 +409,7 @@ function truncateJsonish(value, max) {
|
|
|
409
409
|
return truncateString(JSON.stringify(value), max);
|
|
410
410
|
}
|
|
411
411
|
catch {
|
|
412
|
-
return truncateString(
|
|
412
|
+
return truncateString('[non-serializable]', max);
|
|
413
413
|
}
|
|
414
414
|
}
|
|
415
415
|
function truncateString(s, max) {
|
|
@@ -99,7 +99,7 @@ export class SessionLiveTailer {
|
|
|
99
99
|
});
|
|
100
100
|
let chunk = '';
|
|
101
101
|
stream.on('data', (data) => {
|
|
102
|
-
chunk += data;
|
|
102
|
+
chunk += typeof data === 'string' ? data : data.toString('utf8');
|
|
103
103
|
});
|
|
104
104
|
stream.on('end', () => {
|
|
105
105
|
reading = false;
|
|
@@ -264,6 +264,6 @@ function stringifyContent(value) {
|
|
|
264
264
|
return JSON.stringify(value);
|
|
265
265
|
}
|
|
266
266
|
catch {
|
|
267
|
-
return
|
|
267
|
+
return '[non-serializable]';
|
|
268
268
|
}
|
|
269
269
|
}
|
|
@@ -11,7 +11,7 @@ export declare const AGENT_CTO = "cto";
|
|
|
11
11
|
export declare const AGENT_ENGINEER_PLAN = "engineer_plan";
|
|
12
12
|
export declare const AGENT_ENGINEER_BUILD = "engineer_build";
|
|
13
13
|
/** All restricted tool IDs (union of all domain groups) */
|
|
14
|
-
export declare const ALL_RESTRICTED_TOOL_IDS: readonly ["
|
|
14
|
+
export declare const ALL_RESTRICTED_TOOL_IDS: readonly ["explore", "implement", "compact_context", "clear_session", "session_health", "list_transcripts", "list_history", "git_diff", "git_commit", "git_reset", "approval_policy", "approval_decisions", "approval_update"];
|
|
15
15
|
type ToolPermission = 'allow' | 'ask' | 'deny';
|
|
16
16
|
type AgentPermission = {
|
|
17
17
|
'*'?: ToolPermission;
|
|
@@ -17,23 +17,18 @@ export const AGENT_ENGINEER_BUILD = 'engineer_build';
|
|
|
17
17
|
// ---------------------------------------------------------------------------
|
|
18
18
|
/** Shared engineer session tools (compact, clear, status, diagnostics) */
|
|
19
19
|
const ENGINEER_SHARED_TOOL_IDS = [
|
|
20
|
-
'
|
|
21
|
-
'
|
|
22
|
-
'
|
|
23
|
-
'
|
|
24
|
-
'
|
|
25
|
-
];
|
|
26
|
-
/** All engineer tools — generic send + mode-locked sends + shared session tools */
|
|
27
|
-
const ENGINEER_TOOL_IDS = [
|
|
28
|
-
'engineer_send',
|
|
29
|
-
'engineer_send_plan',
|
|
30
|
-
'engineer_send_build',
|
|
31
|
-
...ENGINEER_SHARED_TOOL_IDS,
|
|
20
|
+
'compact_context',
|
|
21
|
+
'clear_session',
|
|
22
|
+
'session_health',
|
|
23
|
+
'list_transcripts',
|
|
24
|
+
'list_history',
|
|
32
25
|
];
|
|
26
|
+
/** All engineer tools — mode-locked sends + shared session tools */
|
|
27
|
+
const ENGINEER_TOOL_IDS = ['explore', 'implement', ...ENGINEER_SHARED_TOOL_IDS];
|
|
33
28
|
/** Tools for the engineer_plan wrapper (plan-mode send + shared) */
|
|
34
|
-
const ENGINEER_PLAN_TOOL_IDS = ['
|
|
29
|
+
const ENGINEER_PLAN_TOOL_IDS = ['explore', ...ENGINEER_SHARED_TOOL_IDS];
|
|
35
30
|
/** Tools for the engineer_build wrapper (build-mode send + shared) */
|
|
36
|
-
const ENGINEER_BUILD_TOOL_IDS = ['
|
|
31
|
+
const ENGINEER_BUILD_TOOL_IDS = ['implement', ...ENGINEER_SHARED_TOOL_IDS];
|
|
37
32
|
/** Git tools — owned by CTO */
|
|
38
33
|
const GIT_TOOL_IDS = ['git_diff', 'git_commit', 'git_reset'];
|
|
39
34
|
/** Approval tools — owned by CTO */
|
|
@@ -87,7 +82,7 @@ function buildCtoPermissions() {
|
|
|
87
82
|
},
|
|
88
83
|
};
|
|
89
84
|
}
|
|
90
|
-
/** Engineer plan wrapper: read-only investigation +
|
|
85
|
+
/** Engineer plan wrapper: read-only investigation + explore + shared session tools. */
|
|
91
86
|
function buildEngineerPlanPermissions() {
|
|
92
87
|
const denied = {};
|
|
93
88
|
for (const toolId of ALL_RESTRICTED_TOOL_IDS) {
|
|
@@ -104,7 +99,7 @@ function buildEngineerPlanPermissions() {
|
|
|
104
99
|
...allowed,
|
|
105
100
|
};
|
|
106
101
|
}
|
|
107
|
-
/** Engineer build wrapper: read-only investigation +
|
|
102
|
+
/** Engineer build wrapper: read-only investigation + implement + shared session tools. */
|
|
108
103
|
function buildEngineerBuildPermissions() {
|
|
109
104
|
const denied = {};
|
|
110
105
|
for (const toolId of ALL_RESTRICTED_TOOL_IDS) {
|
|
@@ -5,7 +5,7 @@ import { AGENT_CTO, AGENT_ENGINEER_BUILD, AGENT_ENGINEER_PLAN, buildCtoAgentConf
|
|
|
5
5
|
import { getOrCreatePluginServices } from './service-factory.js';
|
|
6
6
|
export const ClaudeManagerPlugin = async ({ worktree }) => {
|
|
7
7
|
const services = getOrCreatePluginServices(worktree);
|
|
8
|
-
async function
|
|
8
|
+
async function executeDelegate(args, context) {
|
|
9
9
|
const cwd = args.cwd ?? context.worktree;
|
|
10
10
|
if (args.freshSession) {
|
|
11
11
|
await services.manager.clearSession(cwd);
|
|
@@ -13,8 +13,11 @@ export const ClaudeManagerPlugin = async ({ worktree }) => {
|
|
|
13
13
|
const hasActiveSession = services.manager.getStatus().sessionId !== null;
|
|
14
14
|
const promptPreview = args.message.length > 100 ? args.message.slice(0, 100) + '...' : args.message;
|
|
15
15
|
context.metadata({
|
|
16
|
-
title: hasActiveSession
|
|
16
|
+
title: hasActiveSession
|
|
17
|
+
? '⚡ Claude Code: Resuming session...'
|
|
18
|
+
: '⚡ Claude Code: Initializing...',
|
|
17
19
|
metadata: {
|
|
20
|
+
status: 'running',
|
|
18
21
|
sessionId: services.manager.getStatus().sessionId,
|
|
19
22
|
prompt: promptPreview,
|
|
20
23
|
},
|
|
@@ -49,8 +52,9 @@ export const ClaudeManagerPlugin = async ({ worktree }) => {
|
|
|
49
52
|
// ignore parse errors
|
|
50
53
|
}
|
|
51
54
|
context.metadata({
|
|
52
|
-
title:
|
|
55
|
+
title: `⚡ Claude Code: Running ${toolName}...${usageSuffix}`,
|
|
53
56
|
metadata: {
|
|
57
|
+
status: 'running',
|
|
54
58
|
sessionId: event.sessionId,
|
|
55
59
|
type: event.type,
|
|
56
60
|
tool: toolName,
|
|
@@ -61,8 +65,9 @@ export const ClaudeManagerPlugin = async ({ worktree }) => {
|
|
|
61
65
|
else if (event.type === 'assistant') {
|
|
62
66
|
const thinkingPreview = event.text.length > 150 ? event.text.slice(0, 150) + '...' : event.text;
|
|
63
67
|
context.metadata({
|
|
64
|
-
title:
|
|
68
|
+
title: `⚡ Claude Code: Thinking...${usageSuffix}`,
|
|
65
69
|
metadata: {
|
|
70
|
+
status: 'running',
|
|
66
71
|
sessionId: event.sessionId,
|
|
67
72
|
type: event.type,
|
|
68
73
|
thinking: thinkingPreview,
|
|
@@ -71,8 +76,9 @@ export const ClaudeManagerPlugin = async ({ worktree }) => {
|
|
|
71
76
|
}
|
|
72
77
|
else if (event.type === 'init') {
|
|
73
78
|
context.metadata({
|
|
74
|
-
title:
|
|
79
|
+
title: `⚡ Claude Code: Session started`,
|
|
75
80
|
metadata: {
|
|
81
|
+
status: 'running',
|
|
76
82
|
sessionId: event.sessionId,
|
|
77
83
|
prompt: promptPreview,
|
|
78
84
|
},
|
|
@@ -80,9 +86,11 @@ export const ClaudeManagerPlugin = async ({ worktree }) => {
|
|
|
80
86
|
}
|
|
81
87
|
else if (event.type === 'user') {
|
|
82
88
|
const preview = event.text.length > 200 ? event.text.slice(0, 200) + '...' : event.text;
|
|
89
|
+
const outputPreview = formatToolOutputPreview(event.text);
|
|
83
90
|
context.metadata({
|
|
84
|
-
title:
|
|
91
|
+
title: `⚡ Claude Code: ${outputPreview}${usageSuffix}`,
|
|
85
92
|
metadata: {
|
|
93
|
+
status: 'running',
|
|
86
94
|
sessionId: event.sessionId,
|
|
87
95
|
type: event.type,
|
|
88
96
|
output: preview,
|
|
@@ -92,17 +100,25 @@ export const ClaudeManagerPlugin = async ({ worktree }) => {
|
|
|
92
100
|
else if (event.type === 'tool_progress') {
|
|
93
101
|
let toolName = 'tool';
|
|
94
102
|
let elapsed = 0;
|
|
103
|
+
let progressCurrent;
|
|
104
|
+
let progressTotal;
|
|
95
105
|
try {
|
|
96
106
|
const parsed = JSON.parse(event.text);
|
|
97
107
|
toolName = parsed.name ?? 'tool';
|
|
98
108
|
elapsed = parsed.elapsed ?? 0;
|
|
109
|
+
progressCurrent = parsed.current;
|
|
110
|
+
progressTotal = parsed.total;
|
|
99
111
|
}
|
|
100
112
|
catch {
|
|
101
113
|
// ignore
|
|
102
114
|
}
|
|
115
|
+
const progressInfo = progressCurrent !== undefined && progressTotal !== undefined
|
|
116
|
+
? ` [${progressCurrent}/${progressTotal}]`
|
|
117
|
+
: '';
|
|
103
118
|
context.metadata({
|
|
104
|
-
title:
|
|
119
|
+
title: `⚡ Claude Code: ${toolName} running ${elapsed > 0 ? `(${elapsed.toFixed(0)}s)` : ''}${progressInfo}...${usageSuffix}`,
|
|
105
120
|
metadata: {
|
|
121
|
+
status: 'running',
|
|
106
122
|
sessionId: event.sessionId,
|
|
107
123
|
type: event.type,
|
|
108
124
|
tool: toolName,
|
|
@@ -113,8 +129,9 @@ export const ClaudeManagerPlugin = async ({ worktree }) => {
|
|
|
113
129
|
else if (event.type === 'tool_summary') {
|
|
114
130
|
const summary = event.text.length > 200 ? event.text.slice(0, 200) + '...' : event.text;
|
|
115
131
|
context.metadata({
|
|
116
|
-
title:
|
|
132
|
+
title: `✅ Claude Code: Tool done${usageSuffix}`,
|
|
117
133
|
metadata: {
|
|
134
|
+
status: 'success',
|
|
118
135
|
sessionId: event.sessionId,
|
|
119
136
|
type: event.type,
|
|
120
137
|
summary,
|
|
@@ -124,8 +141,9 @@ export const ClaudeManagerPlugin = async ({ worktree }) => {
|
|
|
124
141
|
else if (event.type === 'partial') {
|
|
125
142
|
const delta = event.text.length > 200 ? event.text.slice(0, 200) + '...' : event.text;
|
|
126
143
|
context.metadata({
|
|
127
|
-
title:
|
|
144
|
+
title: `⚡ Claude Code: Writing...${usageSuffix}`,
|
|
128
145
|
metadata: {
|
|
146
|
+
status: 'running',
|
|
129
147
|
sessionId: event.sessionId,
|
|
130
148
|
type: event.type,
|
|
131
149
|
delta,
|
|
@@ -134,12 +152,14 @@ export const ClaudeManagerPlugin = async ({ worktree }) => {
|
|
|
134
152
|
}
|
|
135
153
|
else if (event.type === 'error') {
|
|
136
154
|
context.metadata({
|
|
137
|
-
title:
|
|
155
|
+
title: `❌ Claude Code: Error`,
|
|
138
156
|
metadata: {
|
|
157
|
+
status: 'error',
|
|
139
158
|
sessionId: event.sessionId,
|
|
140
159
|
error: event.text.slice(0, 200),
|
|
141
160
|
},
|
|
142
161
|
});
|
|
162
|
+
showToastIfAvailable(context, `Claude Code error: ${event.text.slice(0, 100)}`);
|
|
143
163
|
}
|
|
144
164
|
});
|
|
145
165
|
const costLabel = `$${(result.totalCostUsd ?? 0).toFixed(4)}`;
|
|
@@ -147,15 +167,17 @@ export const ClaudeManagerPlugin = async ({ worktree }) => {
|
|
|
147
167
|
const contextWarning = formatContextWarning(result.context);
|
|
148
168
|
if (contextWarning) {
|
|
149
169
|
context.metadata({
|
|
150
|
-
title:
|
|
151
|
-
metadata: { sessionId: result.sessionId, contextWarning },
|
|
170
|
+
title: `⚠️ Claude Code: Context at ${result.context.estimatedContextPercent}% (${turns} turns)`,
|
|
171
|
+
metadata: { status: 'warning', sessionId: result.sessionId, contextWarning },
|
|
152
172
|
});
|
|
173
|
+
showToastIfAvailable(context, `⚠️ Context usage at ${result.context.estimatedContextPercent}% — consider compacting`);
|
|
153
174
|
}
|
|
154
175
|
else {
|
|
155
176
|
context.metadata({
|
|
156
|
-
title:
|
|
157
|
-
metadata: { sessionId: result.sessionId },
|
|
177
|
+
title: `✅ Claude Code: Complete (${turns} turns, ${costLabel})`,
|
|
178
|
+
metadata: { status: 'success', sessionId: result.sessionId },
|
|
158
179
|
});
|
|
180
|
+
showToastIfAvailable(context, `✅ Session complete (${turns} turns, ${costLabel})`);
|
|
159
181
|
}
|
|
160
182
|
let toolOutputs = [];
|
|
161
183
|
if (result.sessionId) {
|
|
@@ -196,29 +218,26 @@ export const ClaudeManagerPlugin = async ({ worktree }) => {
|
|
|
196
218
|
config.agent[AGENT_ENGINEER_BUILD] ??= buildEngineerBuildAgentConfig(derivedPrompts);
|
|
197
219
|
},
|
|
198
220
|
tool: {
|
|
199
|
-
|
|
200
|
-
description: '
|
|
201
|
-
'
|
|
221
|
+
explore: tool({
|
|
222
|
+
description: 'Investigate and analyze code without making edits. ' +
|
|
223
|
+
'Read-only exploration of the codebase. ' +
|
|
224
|
+
'Preferred first step before implementation.',
|
|
202
225
|
args: {
|
|
203
226
|
message: tool.schema.string().min(1),
|
|
204
227
|
model: tool.schema
|
|
205
228
|
.enum(['claude-opus-4-6', 'claude-sonnet-4-6', 'claude-sonnet-4-5'])
|
|
206
229
|
.optional(),
|
|
207
230
|
effort: tool.schema.enum(['low', 'medium', 'high', 'max']).default('high'),
|
|
208
|
-
mode: tool.schema.enum(['plan', 'free']).default('free'),
|
|
209
231
|
freshSession: tool.schema.boolean().default(false),
|
|
210
232
|
cwd: tool.schema.string().optional(),
|
|
211
233
|
},
|
|
212
234
|
async execute(args, context) {
|
|
213
|
-
return
|
|
235
|
+
return executeDelegate({ ...args, mode: 'plan' }, context);
|
|
214
236
|
},
|
|
215
237
|
}),
|
|
216
|
-
|
|
217
|
-
description: '
|
|
218
|
-
'
|
|
219
|
-
'The engineer will analyze code without making edits. ' +
|
|
220
|
-
'Auto-creates a session on first call. Resumes the existing session on subsequent calls. ' +
|
|
221
|
-
'Returns the assistant response and current context health snapshot.',
|
|
238
|
+
implement: tool({
|
|
239
|
+
description: 'Implement code changes - can read, edit, and create files. ' +
|
|
240
|
+
'Use after exploration to make changes.',
|
|
222
241
|
args: {
|
|
223
242
|
message: tool.schema.string().min(1),
|
|
224
243
|
model: tool.schema
|
|
@@ -229,35 +248,12 @@ export const ClaudeManagerPlugin = async ({ worktree }) => {
|
|
|
229
248
|
cwd: tool.schema.string().optional(),
|
|
230
249
|
},
|
|
231
250
|
async execute(args, context) {
|
|
232
|
-
return
|
|
251
|
+
return executeDelegate({ ...args, mode: 'free' }, context);
|
|
233
252
|
},
|
|
234
253
|
}),
|
|
235
|
-
|
|
236
|
-
description: '
|
|
237
|
-
'
|
|
238
|
-
'The engineer can read, edit, and create files. ' +
|
|
239
|
-
'Auto-creates a session on first call. Resumes the existing session on subsequent calls. ' +
|
|
240
|
-
'Returns the assistant response and current context health snapshot. ' +
|
|
241
|
-
'Prefer claude-opus-4-6 (default) for most coding work; use a Sonnet model for faster/lighter tasks. ' +
|
|
242
|
-
'Prefer effort "high" (default) for most work; use "medium" for lighter tasks and "max" for especially hard problems.',
|
|
243
|
-
args: {
|
|
244
|
-
message: tool.schema.string().min(1),
|
|
245
|
-
model: tool.schema
|
|
246
|
-
.enum(['claude-opus-4-6', 'claude-sonnet-4-6', 'claude-sonnet-4-5'])
|
|
247
|
-
.optional(),
|
|
248
|
-
effort: tool.schema.enum(['low', 'medium', 'high', 'max']).default('high'),
|
|
249
|
-
freshSession: tool.schema.boolean().default(false),
|
|
250
|
-
cwd: tool.schema.string().optional(),
|
|
251
|
-
},
|
|
252
|
-
async execute(args, context) {
|
|
253
|
-
return executeEngineerSend({ ...args, mode: 'free' }, context);
|
|
254
|
-
},
|
|
255
|
-
}),
|
|
256
|
-
engineer_compact: tool({
|
|
257
|
-
description: 'Compact the active Claude Code session to reclaim context space. ' +
|
|
258
|
-
'Sends /compact to the session, which compresses prior conversation while preserving state. ' +
|
|
259
|
-
'Use before clearing when context is high but the session still has useful state. ' +
|
|
260
|
-
'Fails if there is no active session.',
|
|
254
|
+
compact_context: tool({
|
|
255
|
+
description: 'Compress session history to reclaim context window space. ' +
|
|
256
|
+
'Preserves state while reducing token usage.',
|
|
261
257
|
args: {
|
|
262
258
|
cwd: tool.schema.string().optional(),
|
|
263
259
|
},
|
|
@@ -269,9 +265,12 @@ export const ClaudeManagerPlugin = async ({ worktree }) => {
|
|
|
269
265
|
const contextWarning = formatContextWarning(snap);
|
|
270
266
|
context.metadata({
|
|
271
267
|
title: contextWarning
|
|
272
|
-
?
|
|
273
|
-
:
|
|
274
|
-
metadata: {
|
|
268
|
+
? `⚠️ Claude Code: Compacted — context at ${snap.estimatedContextPercent}%`
|
|
269
|
+
: `✅ Claude Code: Compacted (${snap.totalTurns} turns, $${(snap.totalCostUsd ?? 0).toFixed(4)})`,
|
|
270
|
+
metadata: {
|
|
271
|
+
status: contextWarning ? 'warning' : 'success',
|
|
272
|
+
sessionId: result.sessionId,
|
|
273
|
+
},
|
|
275
274
|
});
|
|
276
275
|
return JSON.stringify({
|
|
277
276
|
sessionId: result.sessionId,
|
|
@@ -319,9 +318,9 @@ export const ClaudeManagerPlugin = async ({ worktree }) => {
|
|
|
319
318
|
return JSON.stringify(result, null, 2);
|
|
320
319
|
},
|
|
321
320
|
}),
|
|
322
|
-
|
|
323
|
-
description: 'Clear the active
|
|
324
|
-
'Use when context is full
|
|
321
|
+
clear_session: tool({
|
|
322
|
+
description: 'Clear the active session to start fresh. ' +
|
|
323
|
+
'Use when context is full or starting a new task.',
|
|
325
324
|
args: {
|
|
326
325
|
cwd: tool.schema.string().optional(),
|
|
327
326
|
reason: tool.schema.string().optional(),
|
|
@@ -334,8 +333,8 @@ export const ClaudeManagerPlugin = async ({ worktree }) => {
|
|
|
334
333
|
return JSON.stringify({ clearedSessionId: clearedId });
|
|
335
334
|
},
|
|
336
335
|
}),
|
|
337
|
-
|
|
338
|
-
description: '
|
|
336
|
+
session_health: tool({
|
|
337
|
+
description: 'Check session health metrics: context usage %, turn count, cost, and session ID.',
|
|
339
338
|
args: {
|
|
340
339
|
cwd: tool.schema.string().optional(),
|
|
341
340
|
},
|
|
@@ -351,9 +350,8 @@ export const ClaudeManagerPlugin = async ({ worktree }) => {
|
|
|
351
350
|
}, null, 2);
|
|
352
351
|
},
|
|
353
352
|
}),
|
|
354
|
-
|
|
355
|
-
description: 'List
|
|
356
|
-
'When sessionId is provided, returns both SDK transcript and local events.',
|
|
353
|
+
list_transcripts: tool({
|
|
354
|
+
description: 'List available session transcripts or inspect a specific transcript by ID.',
|
|
357
355
|
args: {
|
|
358
356
|
cwd: tool.schema.string().optional(),
|
|
359
357
|
sessionId: tool.schema.string().optional(),
|
|
@@ -375,8 +373,8 @@ export const ClaudeManagerPlugin = async ({ worktree }) => {
|
|
|
375
373
|
return JSON.stringify(sessions, null, 2);
|
|
376
374
|
},
|
|
377
375
|
}),
|
|
378
|
-
|
|
379
|
-
description: 'List persistent manager run
|
|
376
|
+
list_history: tool({
|
|
377
|
+
description: 'List persistent run records from the manager or inspect a specific run.',
|
|
380
378
|
args: {
|
|
381
379
|
cwd: tool.schema.string().optional(),
|
|
382
380
|
runId: tool.schema.string().optional(),
|
|
@@ -482,8 +480,12 @@ export const ClaudeManagerPlugin = async ({ worktree }) => {
|
|
|
482
480
|
},
|
|
483
481
|
};
|
|
484
482
|
};
|
|
485
|
-
function annotateToolRun(context, title, metadata) {
|
|
486
|
-
|
|
483
|
+
function annotateToolRun(context, title, metadata, status) {
|
|
484
|
+
const emoji = status ? `${formatStatusEmoji(status)} ` : '';
|
|
485
|
+
context.metadata({
|
|
486
|
+
title: `${emoji}${title}`,
|
|
487
|
+
metadata: { ...metadata, ...(status ? { status } : {}) },
|
|
488
|
+
});
|
|
487
489
|
}
|
|
488
490
|
function formatLiveUsage(turns, cost) {
|
|
489
491
|
if (turns === undefined && cost === undefined) {
|
|
@@ -491,10 +493,10 @@ function formatLiveUsage(turns, cost) {
|
|
|
491
493
|
}
|
|
492
494
|
const parts = [];
|
|
493
495
|
if (turns !== undefined) {
|
|
494
|
-
parts.push(
|
|
496
|
+
parts.push(`🔄 ${turns} turns`);
|
|
495
497
|
}
|
|
496
498
|
if (cost !== undefined) {
|
|
497
|
-
parts.push(
|
|
499
|
+
parts.push(`💰 $${cost.toFixed(4)}`);
|
|
498
500
|
}
|
|
499
501
|
return ` (${parts.join(', ')})`;
|
|
500
502
|
}
|
|
@@ -514,3 +516,47 @@ function formatContextWarning(context) {
|
|
|
514
516
|
.replace('{turns}', String(totalTurns))
|
|
515
517
|
.replace('{cost}', totalCostUsd.toFixed(2));
|
|
516
518
|
}
|
|
519
|
+
function formatStatusEmoji(status) {
|
|
520
|
+
switch (status) {
|
|
521
|
+
case 'running':
|
|
522
|
+
return '⚡';
|
|
523
|
+
case 'success':
|
|
524
|
+
return '✅';
|
|
525
|
+
case 'error':
|
|
526
|
+
return '❌';
|
|
527
|
+
case 'warning':
|
|
528
|
+
return '⚠️';
|
|
529
|
+
}
|
|
530
|
+
}
|
|
531
|
+
function formatToolOutputPreview(text) {
|
|
532
|
+
const lower = text.toLowerCase();
|
|
533
|
+
let prefix;
|
|
534
|
+
if (lower.includes('"tool":"read"') ||
|
|
535
|
+
lower.includes('"name":"read"') ||
|
|
536
|
+
lower.includes('file contents')) {
|
|
537
|
+
prefix = '↳ Read: ';
|
|
538
|
+
}
|
|
539
|
+
else if (lower.includes('"tool":"grep"') ||
|
|
540
|
+
lower.includes('"name":"grep"') ||
|
|
541
|
+
lower.includes('matches found')) {
|
|
542
|
+
prefix = '↳ Found: ';
|
|
543
|
+
}
|
|
544
|
+
else if (lower.includes('"tool":"write"') ||
|
|
545
|
+
lower.includes('"name":"write"') ||
|
|
546
|
+
lower.includes('"tool":"edit"') ||
|
|
547
|
+
lower.includes('"name":"edit"') ||
|
|
548
|
+
lower.includes('file written') ||
|
|
549
|
+
lower.includes('file updated')) {
|
|
550
|
+
prefix = '↳ Wrote: ';
|
|
551
|
+
}
|
|
552
|
+
else {
|
|
553
|
+
prefix = '↳ Result: ';
|
|
554
|
+
}
|
|
555
|
+
const snippet = text.replace(/\s+/g, ' ').trim();
|
|
556
|
+
const truncated = snippet.length > 60 ? snippet.slice(0, 60) + '...' : snippet;
|
|
557
|
+
return `${prefix}${truncated}`;
|
|
558
|
+
}
|
|
559
|
+
function showToastIfAvailable(context, message) {
|
|
560
|
+
const ctx = context;
|
|
561
|
+
ctx.client?.tui?.showToast?.(message);
|
|
562
|
+
}
|
package/dist/prompts/registry.js
CHANGED
|
@@ -148,13 +148,13 @@ export const managerPromptRegistry = {
|
|
|
148
148
|
'- Do NOT implement changes yourself — investigation only.',
|
|
149
149
|
'',
|
|
150
150
|
'## Behavior',
|
|
151
|
-
'- Send the objective to the engineer using
|
|
151
|
+
'- Send the objective to the engineer using explore.',
|
|
152
152
|
"- Return the engineer's response verbatim. Do not summarize.",
|
|
153
|
-
'- Use freshSession:true on
|
|
153
|
+
'- Use freshSession:true on explore when the task is unrelated to prior work.',
|
|
154
154
|
'',
|
|
155
155
|
'## Context management',
|
|
156
|
-
'- Check
|
|
157
|
-
'- Under 50%: proceed. Over 70%:
|
|
156
|
+
'- Check session_health before sending.',
|
|
157
|
+
'- Under 50%: proceed. Over 70%: compact_context. Over 85%: clear_session.',
|
|
158
158
|
'',
|
|
159
159
|
'## Model selection',
|
|
160
160
|
'- claude-opus-4-6 + high: complex analysis (default).',
|
|
@@ -191,13 +191,13 @@ export const managerPromptRegistry = {
|
|
|
191
191
|
'- Do NOT implement changes yourself — investigation only.',
|
|
192
192
|
'',
|
|
193
193
|
'## Behavior',
|
|
194
|
-
'- Send the objective to the engineer using
|
|
194
|
+
'- Send the objective to the engineer using implement.',
|
|
195
195
|
"- Return the engineer's response verbatim. Do not summarize.",
|
|
196
|
-
'- Use freshSession:true on
|
|
196
|
+
'- Use freshSession:true on implement when the task is unrelated to prior work.',
|
|
197
197
|
'',
|
|
198
198
|
'## Context management',
|
|
199
|
-
'- Check
|
|
200
|
-
'- Under 50%: proceed. Over 70%:
|
|
199
|
+
'- Check session_health before sending.',
|
|
200
|
+
'- Under 50%: proceed. Over 70%: compact_context. Over 85%: clear_session.',
|
|
201
201
|
'',
|
|
202
202
|
'## Model selection',
|
|
203
203
|
'- claude-opus-4-6 + high: most coding tasks (default).',
|
package/package.json
CHANGED
|
@@ -1,12 +0,0 @@
|
|
|
1
|
-
import type { ClaudeMetadataSnapshot, ClaudeSettingSource } from '../types/contracts.js';
|
|
2
|
-
import type { ClaudeAgentSdkAdapter } from '../claude/claude-agent-sdk-adapter.js';
|
|
3
|
-
import type { RepoClaudeConfigReader } from './repo-claude-config-reader.js';
|
|
4
|
-
export declare class ClaudeMetadataService {
|
|
5
|
-
private readonly configReader;
|
|
6
|
-
private readonly sdkAdapter;
|
|
7
|
-
constructor(configReader: RepoClaudeConfigReader, sdkAdapter: ClaudeAgentSdkAdapter);
|
|
8
|
-
collect(cwd: string, options?: {
|
|
9
|
-
includeSdkProbe?: boolean;
|
|
10
|
-
settingSources?: ClaudeSettingSource[];
|
|
11
|
-
}): Promise<ClaudeMetadataSnapshot>;
|
|
12
|
-
}
|
|
@@ -1,38 +0,0 @@
|
|
|
1
|
-
export class ClaudeMetadataService {
|
|
2
|
-
configReader;
|
|
3
|
-
sdkAdapter;
|
|
4
|
-
constructor(configReader, sdkAdapter) {
|
|
5
|
-
this.configReader = configReader;
|
|
6
|
-
this.sdkAdapter = sdkAdapter;
|
|
7
|
-
}
|
|
8
|
-
async collect(cwd, options = {}) {
|
|
9
|
-
const baseSnapshot = await this.configReader.read(cwd);
|
|
10
|
-
if (!options.includeSdkProbe) {
|
|
11
|
-
return dedupeSnapshot(baseSnapshot);
|
|
12
|
-
}
|
|
13
|
-
const capabilities = await this.sdkAdapter.probeCapabilities(cwd, options.settingSources);
|
|
14
|
-
return dedupeSnapshot({
|
|
15
|
-
...baseSnapshot,
|
|
16
|
-
commands: [...baseSnapshot.commands, ...capabilities.commands],
|
|
17
|
-
agents: capabilities.agents,
|
|
18
|
-
});
|
|
19
|
-
}
|
|
20
|
-
}
|
|
21
|
-
function dedupeSnapshot(snapshot) {
|
|
22
|
-
return {
|
|
23
|
-
...snapshot,
|
|
24
|
-
commands: dedupeByName(snapshot.commands),
|
|
25
|
-
skills: dedupeByName(snapshot.skills),
|
|
26
|
-
hooks: dedupeByName(snapshot.hooks),
|
|
27
|
-
agents: dedupeByName(snapshot.agents),
|
|
28
|
-
};
|
|
29
|
-
}
|
|
30
|
-
function dedupeByName(items) {
|
|
31
|
-
const seen = new Map();
|
|
32
|
-
for (const item of items) {
|
|
33
|
-
if (!seen.has(item.name)) {
|
|
34
|
-
seen.set(item.name, item);
|
|
35
|
-
}
|
|
36
|
-
}
|
|
37
|
-
return [...seen.values()].sort((left, right) => left.name.localeCompare(right.name));
|
|
38
|
-
}
|
|
@@ -1,154 +0,0 @@
|
|
|
1
|
-
import { promises as fs } from 'node:fs';
|
|
2
|
-
import path from 'node:path';
|
|
3
|
-
import JSON5 from 'json5';
|
|
4
|
-
export class RepoClaudeConfigReader {
|
|
5
|
-
async read(cwd) {
|
|
6
|
-
const claudeDirectory = path.join(cwd, '.claude');
|
|
7
|
-
const skillsDirectory = path.join(claudeDirectory, 'skills');
|
|
8
|
-
const commandsDirectory = path.join(claudeDirectory, 'commands');
|
|
9
|
-
const claudeMdCandidates = [
|
|
10
|
-
path.join(cwd, 'CLAUDE.md'),
|
|
11
|
-
path.join(claudeDirectory, 'CLAUDE.md'),
|
|
12
|
-
];
|
|
13
|
-
const collectedAt = new Date().toISOString();
|
|
14
|
-
const [skills, commands, settingsResult, claudeMdPath] = await Promise.all([
|
|
15
|
-
this.readSkills(skillsDirectory),
|
|
16
|
-
this.readCommands(commandsDirectory),
|
|
17
|
-
this.readSettings(claudeDirectory),
|
|
18
|
-
findFirstExistingPath(claudeMdCandidates),
|
|
19
|
-
]);
|
|
20
|
-
return {
|
|
21
|
-
collectedAt,
|
|
22
|
-
cwd,
|
|
23
|
-
commands: [...skillsToCommands(skills), ...commands],
|
|
24
|
-
skills,
|
|
25
|
-
hooks: settingsResult.hooks,
|
|
26
|
-
agents: [],
|
|
27
|
-
claudeMdPath: claudeMdPath ?? undefined,
|
|
28
|
-
settingsPaths: settingsResult.settingsPaths,
|
|
29
|
-
};
|
|
30
|
-
}
|
|
31
|
-
async readSkills(directory) {
|
|
32
|
-
if (!(await pathExists(directory))) {
|
|
33
|
-
return [];
|
|
34
|
-
}
|
|
35
|
-
const entries = await fs.readdir(directory, { withFileTypes: true });
|
|
36
|
-
const skills = await Promise.all(entries
|
|
37
|
-
.filter((entry) => entry.isDirectory())
|
|
38
|
-
.map(async (entry) => {
|
|
39
|
-
const skillPath = path.join(directory, entry.name, 'SKILL.md');
|
|
40
|
-
if (!(await pathExists(skillPath))) {
|
|
41
|
-
return null;
|
|
42
|
-
}
|
|
43
|
-
const content = await fs.readFile(skillPath, 'utf8');
|
|
44
|
-
return {
|
|
45
|
-
name: entry.name,
|
|
46
|
-
description: extractMarkdownDescription(content),
|
|
47
|
-
path: skillPath,
|
|
48
|
-
source: 'skill',
|
|
49
|
-
};
|
|
50
|
-
}));
|
|
51
|
-
return skills.filter((skill) => skill !== null);
|
|
52
|
-
}
|
|
53
|
-
async readCommands(directory) {
|
|
54
|
-
if (!(await pathExists(directory))) {
|
|
55
|
-
return [];
|
|
56
|
-
}
|
|
57
|
-
const commandFiles = await collectMarkdownFiles(directory);
|
|
58
|
-
const commands = await Promise.all(commandFiles.map(async (commandPath) => {
|
|
59
|
-
const content = await fs.readFile(commandPath, 'utf8');
|
|
60
|
-
return {
|
|
61
|
-
name: path.basename(commandPath, path.extname(commandPath)),
|
|
62
|
-
description: extractMarkdownDescription(content),
|
|
63
|
-
source: 'command',
|
|
64
|
-
path: commandPath,
|
|
65
|
-
};
|
|
66
|
-
}));
|
|
67
|
-
return commands.sort((left, right) => left.name.localeCompare(right.name));
|
|
68
|
-
}
|
|
69
|
-
async readSettings(claudeDirectory) {
|
|
70
|
-
const candidatePaths = [
|
|
71
|
-
path.join(claudeDirectory, 'settings.json'),
|
|
72
|
-
path.join(claudeDirectory, 'settings.local.json'),
|
|
73
|
-
];
|
|
74
|
-
const settingsPaths = [];
|
|
75
|
-
const hooks = [];
|
|
76
|
-
for (const candidatePath of candidatePaths) {
|
|
77
|
-
if (!(await pathExists(candidatePath))) {
|
|
78
|
-
continue;
|
|
79
|
-
}
|
|
80
|
-
settingsPaths.push(candidatePath);
|
|
81
|
-
const content = await fs.readFile(candidatePath, 'utf8');
|
|
82
|
-
const parsed = JSON5.parse(content);
|
|
83
|
-
const hookEntries = Object.entries(parsed.hooks ?? {});
|
|
84
|
-
for (const [hookName, hookValue] of hookEntries) {
|
|
85
|
-
const hookMatchers = Array.isArray(hookValue) ? hookValue : [hookValue];
|
|
86
|
-
for (const hookMatcher of hookMatchers) {
|
|
87
|
-
if (!hookMatcher || typeof hookMatcher !== 'object') {
|
|
88
|
-
continue;
|
|
89
|
-
}
|
|
90
|
-
const matcher = typeof hookMatcher.matcher === 'string'
|
|
91
|
-
? hookMatcher.matcher
|
|
92
|
-
: undefined;
|
|
93
|
-
const commandCount = Array.isArray(hookMatcher.hooks)
|
|
94
|
-
? (hookMatcher.hooks?.length ?? 0)
|
|
95
|
-
: 0;
|
|
96
|
-
hooks.push({
|
|
97
|
-
name: hookName,
|
|
98
|
-
matcher,
|
|
99
|
-
sourcePath: candidatePath,
|
|
100
|
-
commandCount,
|
|
101
|
-
});
|
|
102
|
-
}
|
|
103
|
-
}
|
|
104
|
-
}
|
|
105
|
-
return {
|
|
106
|
-
settingsPaths,
|
|
107
|
-
hooks,
|
|
108
|
-
};
|
|
109
|
-
}
|
|
110
|
-
}
|
|
111
|
-
function extractMarkdownDescription(markdown) {
|
|
112
|
-
const lines = markdown
|
|
113
|
-
.split(/\r?\n/)
|
|
114
|
-
.map((line) => line.trim())
|
|
115
|
-
.filter(Boolean);
|
|
116
|
-
const descriptionLine = lines.find((line) => !line.startsWith('#') && !line.startsWith('---'));
|
|
117
|
-
return descriptionLine ?? 'No description provided.';
|
|
118
|
-
}
|
|
119
|
-
async function collectMarkdownFiles(directory) {
|
|
120
|
-
const entries = await fs.readdir(directory, { withFileTypes: true });
|
|
121
|
-
const files = await Promise.all(entries.map(async (entry) => {
|
|
122
|
-
const resolvedPath = path.join(directory, entry.name);
|
|
123
|
-
if (entry.isDirectory()) {
|
|
124
|
-
return collectMarkdownFiles(resolvedPath);
|
|
125
|
-
}
|
|
126
|
-
return entry.name.endsWith('.md') ? [resolvedPath] : [];
|
|
127
|
-
}));
|
|
128
|
-
return files.flat();
|
|
129
|
-
}
|
|
130
|
-
async function pathExists(candidatePath) {
|
|
131
|
-
try {
|
|
132
|
-
await fs.access(candidatePath);
|
|
133
|
-
return true;
|
|
134
|
-
}
|
|
135
|
-
catch {
|
|
136
|
-
return false;
|
|
137
|
-
}
|
|
138
|
-
}
|
|
139
|
-
async function findFirstExistingPath(candidatePaths) {
|
|
140
|
-
for (const candidatePath of candidatePaths) {
|
|
141
|
-
if (await pathExists(candidatePath)) {
|
|
142
|
-
return candidatePath;
|
|
143
|
-
}
|
|
144
|
-
}
|
|
145
|
-
return null;
|
|
146
|
-
}
|
|
147
|
-
function skillsToCommands(skills) {
|
|
148
|
-
return skills.map((skill) => ({
|
|
149
|
-
name: skill.name,
|
|
150
|
-
description: skill.description,
|
|
151
|
-
source: 'skill',
|
|
152
|
-
path: skill.path,
|
|
153
|
-
}));
|
|
154
|
-
}
|
|
@@ -1,116 +0,0 @@
|
|
|
1
|
-
import { prompts } from '../prompts/registry.js';
|
|
2
|
-
import { evaluateBashCommand, extractBashCommand, } from '../safety/bash-safety.js';
|
|
3
|
-
/**
|
|
4
|
-
* Thin OpenCode orchestrator plugin with Claude Code specialist subagents.
|
|
5
|
-
*
|
|
6
|
-
* - Registers `claude-code` provider via a local shim over ai-sdk-provider-claude-code.
|
|
7
|
-
* - Creates one orchestrator agent (uses the user's default OpenCode model).
|
|
8
|
-
* - Creates 4 Claude Code subagents: planning + build × opus + sonnet.
|
|
9
|
-
* - Enforces bash safety via the permission.ask hook.
|
|
10
|
-
*
|
|
11
|
-
* NOTE: Claude Code `effort` is not configurable through OpenCode provider/model
|
|
12
|
-
* options at this time. The subagent prompts compensate by setting high-quality
|
|
13
|
-
* expectations directly.
|
|
14
|
-
*/
|
|
15
|
-
// Resolve the shim path at module load time so it is stable for the lifetime
|
|
16
|
-
// of the process. The compiled output for this file sits at dist/plugin/ and
|
|
17
|
-
// the shim at dist/providers/, so we walk up one level.
|
|
18
|
-
const claudeCodeShimUrl = new URL('../providers/claude-code-wrapper.js', import.meta.url).href;
|
|
19
|
-
export const OrchestratorPlugin = async () => {
|
|
20
|
-
return {
|
|
21
|
-
config: async (config) => {
|
|
22
|
-
config.provider ??= {};
|
|
23
|
-
config.agent ??= {};
|
|
24
|
-
// ── Provider ──────────────────────────────────────────────────────
|
|
25
|
-
// Uses a file:// shim so OpenCode's factory-finder heuristic sees only
|
|
26
|
-
// createClaudeCode and not createAPICallError from the upstream package.
|
|
27
|
-
config.provider['claude-code'] ??= {
|
|
28
|
-
npm: claudeCodeShimUrl,
|
|
29
|
-
models: {
|
|
30
|
-
opus: {
|
|
31
|
-
id: 'opus',
|
|
32
|
-
name: 'Claude Code Opus 4.6',
|
|
33
|
-
},
|
|
34
|
-
sonnet: {
|
|
35
|
-
id: 'sonnet',
|
|
36
|
-
name: 'Claude Code Sonnet 4.6',
|
|
37
|
-
},
|
|
38
|
-
},
|
|
39
|
-
};
|
|
40
|
-
// ── Orchestrator (uses user's default model — no model set) ───────
|
|
41
|
-
config.agent['opencode-orchestrator'] ??= {
|
|
42
|
-
description: 'CTO-level orchestrator that gathers context and delegates coding to Claude Code specialists.',
|
|
43
|
-
mode: 'primary',
|
|
44
|
-
color: '#D97757',
|
|
45
|
-
prompt: prompts.orchestrator,
|
|
46
|
-
permission: {
|
|
47
|
-
'*': 'deny',
|
|
48
|
-
read: 'allow',
|
|
49
|
-
grep: 'allow',
|
|
50
|
-
glob: 'allow',
|
|
51
|
-
list: 'allow',
|
|
52
|
-
webfetch: 'allow',
|
|
53
|
-
question: 'allow',
|
|
54
|
-
todowrite: 'allow',
|
|
55
|
-
todoread: 'allow',
|
|
56
|
-
task: 'allow',
|
|
57
|
-
bash: 'deny',
|
|
58
|
-
edit: 'deny',
|
|
59
|
-
skill: 'deny',
|
|
60
|
-
},
|
|
61
|
-
};
|
|
62
|
-
// ── Planning subagents ────────────────────────────────────────────
|
|
63
|
-
// Claude Code tools (Bash, Read, Write, Edit, …) are executed internally
|
|
64
|
-
// by the claude CLI subprocess and streamed back with providerExecuted:true.
|
|
65
|
-
// OpenCode's own tools must not be advertised to these agents.
|
|
66
|
-
const claudeCodePermissions = {
|
|
67
|
-
'*': 'deny',
|
|
68
|
-
};
|
|
69
|
-
config.agent['claude-code-planning-opus'] ??= {
|
|
70
|
-
description: 'Claude Code Opus specialist for investigation, architecture, and planning.',
|
|
71
|
-
model: 'claude-code/opus',
|
|
72
|
-
mode: 'subagent',
|
|
73
|
-
color: 'info',
|
|
74
|
-
prompt: prompts.planningAgent,
|
|
75
|
-
permission: { ...claudeCodePermissions },
|
|
76
|
-
};
|
|
77
|
-
config.agent['claude-code-planning-sonnet'] ??= {
|
|
78
|
-
description: 'Claude Code Sonnet specialist for lighter investigation and planning.',
|
|
79
|
-
model: 'claude-code/sonnet',
|
|
80
|
-
mode: 'subagent',
|
|
81
|
-
color: 'info',
|
|
82
|
-
prompt: prompts.planningAgent,
|
|
83
|
-
permission: { ...claudeCodePermissions },
|
|
84
|
-
};
|
|
85
|
-
// ── Build subagents ───────────────────────────────────────────────
|
|
86
|
-
config.agent['claude-code-build-opus'] ??= {
|
|
87
|
-
description: 'Claude Code Opus specialist for implementation and validation.',
|
|
88
|
-
model: 'claude-code/opus',
|
|
89
|
-
mode: 'subagent',
|
|
90
|
-
color: 'success',
|
|
91
|
-
prompt: prompts.buildAgent,
|
|
92
|
-
permission: { ...claudeCodePermissions },
|
|
93
|
-
};
|
|
94
|
-
config.agent['claude-code-build-sonnet'] ??= {
|
|
95
|
-
description: 'Claude Code Sonnet specialist for lighter implementation tasks.',
|
|
96
|
-
model: 'claude-code/sonnet',
|
|
97
|
-
mode: 'subagent',
|
|
98
|
-
color: 'success',
|
|
99
|
-
prompt: prompts.buildAgent,
|
|
100
|
-
permission: { ...claudeCodePermissions },
|
|
101
|
-
};
|
|
102
|
-
},
|
|
103
|
-
// ── Bash safety via permission.ask hook ────────────────────────────
|
|
104
|
-
// Handles both v1 Permission ({ type, pattern }) and v2 PermissionRequest
|
|
105
|
-
// ({ permission, patterns }) via runtime narrowing in extractBashCommand.
|
|
106
|
-
'permission.ask': async (input, output) => {
|
|
107
|
-
const command = extractBashCommand(input);
|
|
108
|
-
if (command === null)
|
|
109
|
-
return;
|
|
110
|
-
const result = evaluateBashCommand(command);
|
|
111
|
-
if (!result.allowed) {
|
|
112
|
-
output.status = 'deny';
|
|
113
|
-
}
|
|
114
|
-
},
|
|
115
|
-
};
|
|
116
|
-
};
|
|
@@ -1,13 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Thin re-export shim for ai-sdk-provider-claude-code.
|
|
3
|
-
*
|
|
4
|
-
* OpenCode's provider loader finds the provider factory by scanning
|
|
5
|
-
* `Object.keys(module).find(key => key.startsWith("create"))`. The upstream
|
|
6
|
-
* package exports `createAPICallError` before `createClaudeCode`, so OpenCode
|
|
7
|
-
* picks the wrong function and `.languageModel` ends up undefined.
|
|
8
|
-
*
|
|
9
|
-
* This shim re-exports only `createClaudeCode`, making it the sole "create*"
|
|
10
|
-
* export. The plugin references this file via a `file://` URL so the upstream
|
|
11
|
-
* package is still the actual implementation.
|
|
12
|
-
*/
|
|
13
|
-
export { createClaudeCode } from 'ai-sdk-provider-claude-code';
|
|
@@ -1,13 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Thin re-export shim for ai-sdk-provider-claude-code.
|
|
3
|
-
*
|
|
4
|
-
* OpenCode's provider loader finds the provider factory by scanning
|
|
5
|
-
* `Object.keys(module).find(key => key.startsWith("create"))`. The upstream
|
|
6
|
-
* package exports `createAPICallError` before `createClaudeCode`, so OpenCode
|
|
7
|
-
* picks the wrong function and `.languageModel` ends up undefined.
|
|
8
|
-
*
|
|
9
|
-
* This shim re-exports only `createClaudeCode`, making it the sole "create*"
|
|
10
|
-
* export. The plugin references this file via a `file://` URL so the upstream
|
|
11
|
-
* package is still the actual implementation.
|
|
12
|
-
*/
|
|
13
|
-
export { createClaudeCode } from 'ai-sdk-provider-claude-code';
|
|
@@ -1,21 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Minimal bash command safety layer.
|
|
3
|
-
* Denies known-dangerous patterns; allows everything else.
|
|
4
|
-
*/
|
|
5
|
-
export type BashSafetyResult = {
|
|
6
|
-
allowed: true;
|
|
7
|
-
} | {
|
|
8
|
-
allowed: false;
|
|
9
|
-
reason: string;
|
|
10
|
-
};
|
|
11
|
-
export declare function evaluateBashCommand(command: string): BashSafetyResult;
|
|
12
|
-
/**
|
|
13
|
-
* Extract the bash command string from a permission hook input,
|
|
14
|
-
* handling both SDK payload shapes:
|
|
15
|
-
*
|
|
16
|
-
* v1 Permission: { type: string, pattern?: string | string[], metadata }
|
|
17
|
-
* v2 PermissionRequest: { permission: string, patterns: string[], metadata }
|
|
18
|
-
*
|
|
19
|
-
* Returns `null` when the input is not a bash permission request.
|
|
20
|
-
*/
|
|
21
|
-
export declare function extractBashCommand(input: Record<string, unknown>): string | null;
|
|
@@ -1,62 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Minimal bash command safety layer.
|
|
3
|
-
* Denies known-dangerous patterns; allows everything else.
|
|
4
|
-
*/
|
|
5
|
-
const DENY_PATTERNS = [
|
|
6
|
-
{ pattern: 'rm -rf /', reason: 'Destructive: rm -rf / is not allowed.' },
|
|
7
|
-
{
|
|
8
|
-
pattern: 'git push --force',
|
|
9
|
-
reason: 'Force push is not allowed.',
|
|
10
|
-
},
|
|
11
|
-
{
|
|
12
|
-
pattern: 'git reset --hard',
|
|
13
|
-
reason: 'git reset --hard is not allowed. Use a safer alternative.',
|
|
14
|
-
},
|
|
15
|
-
];
|
|
16
|
-
export function evaluateBashCommand(command) {
|
|
17
|
-
for (const { pattern, reason } of DENY_PATTERNS) {
|
|
18
|
-
if (command.includes(pattern)) {
|
|
19
|
-
return { allowed: false, reason };
|
|
20
|
-
}
|
|
21
|
-
}
|
|
22
|
-
return { allowed: true };
|
|
23
|
-
}
|
|
24
|
-
/**
|
|
25
|
-
* Extract the bash command string from a permission hook input,
|
|
26
|
-
* handling both SDK payload shapes:
|
|
27
|
-
*
|
|
28
|
-
* v1 Permission: { type: string, pattern?: string | string[], metadata }
|
|
29
|
-
* v2 PermissionRequest: { permission: string, patterns: string[], metadata }
|
|
30
|
-
*
|
|
31
|
-
* Returns `null` when the input is not a bash permission request.
|
|
32
|
-
*/
|
|
33
|
-
export function extractBashCommand(input) {
|
|
34
|
-
// Determine the permission kind from whichever field is present.
|
|
35
|
-
const kind = typeof input['permission'] === 'string'
|
|
36
|
-
? input['permission']
|
|
37
|
-
: typeof input['type'] === 'string'
|
|
38
|
-
? input['type']
|
|
39
|
-
: null;
|
|
40
|
-
if (kind !== 'bash')
|
|
41
|
-
return null;
|
|
42
|
-
// Prefer an explicit command in metadata regardless of shape.
|
|
43
|
-
const meta = input['metadata'];
|
|
44
|
-
if (meta !== null && typeof meta === 'object' && !Array.isArray(meta)) {
|
|
45
|
-
const cmd = meta['command'];
|
|
46
|
-
if (typeof cmd === 'string' && cmd.length > 0)
|
|
47
|
-
return cmd;
|
|
48
|
-
}
|
|
49
|
-
// v2: patterns is always string[]
|
|
50
|
-
const patterns = input['patterns'];
|
|
51
|
-
if (Array.isArray(patterns) && patterns.length > 0) {
|
|
52
|
-
return patterns.join(' ');
|
|
53
|
-
}
|
|
54
|
-
// v1: pattern may be string or string[]
|
|
55
|
-
const pattern = input['pattern'];
|
|
56
|
-
if (typeof pattern === 'string' && pattern.length > 0)
|
|
57
|
-
return pattern;
|
|
58
|
-
if (Array.isArray(pattern) && pattern.length > 0) {
|
|
59
|
-
return pattern.join(' ');
|
|
60
|
-
}
|
|
61
|
-
return null;
|
|
62
|
-
}
|