@doingdev/opencode-claude-manager-plugin 0.1.33 → 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 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
- - `engineer_send` — send a message to the persistent Claude Code session. Auto-creates on first call, resumes on subsequent calls.
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
- - `engineer_compact` — compress the active session context while preserving session state. Use before clearing when context is high but salvageable.
63
- - `engineer_clear` — drop the active session entirely; next send starts fresh.
64
- - `engineer_status` — get current session health: context %, turns, cost, session ID.
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
- - `engineer_metadata` — inspect available Claude commands, skills, hooks, and settings.
75
- - `engineer_sessions` — list Claude sessions or inspect a saved transcript.
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 (`engineer_*`, `git_*`, `approval_*`) plus read/search/web tools for investigation.
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. Inspect Claude capabilities with `engineer_metadata`.
99
- 2. Delegate work with `engineer_send`.
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 `engineer_sessions` or prior orchestration records with `engineer_runs`.
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 engineer_send to implement the new validation logic in src/auth.ts, then review with git_diff.
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 engineer_send with freshSession:true to investigate the failing CI test in test/api.test.ts using mode:"plan".
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 engineer_compact to free up context, then continue with the next implementation step.
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(String(value), max);
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 String(value);
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 ["engineer_send", "engineer_send_plan", "engineer_send_build", "engineer_compact", "engineer_clear", "engineer_status", "engineer_sessions", "engineer_runs", "git_diff", "git_commit", "git_reset", "approval_policy", "approval_decisions", "approval_update"];
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
- 'engineer_compact',
21
- 'engineer_clear',
22
- 'engineer_status',
23
- 'engineer_sessions',
24
- 'engineer_runs',
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 = ['engineer_send_plan', ...ENGINEER_SHARED_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 = ['engineer_send_build', ...ENGINEER_SHARED_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 + engineer_send_plan + shared session tools. */
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 + engineer_send_build + shared session tools. */
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) {
@@ -126,7 +121,7 @@ function buildEngineerBuildPermissions() {
126
121
  // ---------------------------------------------------------------------------
127
122
  export function buildCtoAgentConfig(prompts) {
128
123
  return {
129
- description: 'Pure orchestrator that investigates, spawns engineers for planning and building, reviews diffs, and commits.',
124
+ description: 'Delegates by default with minimal spot-checks, spawns engineers for exploration and implementation, reviews diffs, and commits.',
130
125
  mode: 'primary',
131
126
  color: '#D97757',
132
127
  permission: buildCtoPermissions(),
@@ -135,7 +130,7 @@ export function buildCtoAgentConfig(prompts) {
135
130
  }
136
131
  export function buildEngineerPlanAgentConfig(prompts) {
137
132
  return {
138
- description: 'Engineer that manages a Claude Code session in plan mode for read-only investigation and analysis.',
133
+ description: 'Thin high-judgment wrapper that frames work quickly and dispatches to Claude Code in plan mode for read-only investigation.',
139
134
  mode: 'subagent',
140
135
  color: '#D97757',
141
136
  permission: buildEngineerPlanPermissions(),
@@ -144,7 +139,7 @@ export function buildEngineerPlanAgentConfig(prompts) {
144
139
  }
145
140
  export function buildEngineerBuildAgentConfig(prompts) {
146
141
  return {
147
- description: 'Engineer that manages a Claude Code session in free mode for implementation and execution.',
142
+ description: 'Thin high-judgment wrapper that frames work quickly and dispatches to Claude Code in free mode for implementation.',
148
143
  mode: 'subagent',
149
144
  color: '#D97757',
150
145
  permission: buildEngineerBuildPermissions(),
@@ -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 executeEngineerSend(args, context) {
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 ? 'Claude Code: Resuming session...' : 'Claude Code: Initializing...',
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: `Claude Code: Running ${toolName}...${usageSuffix}`,
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: `Claude Code: Thinking...${usageSuffix}`,
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: `Claude Code: Session started`,
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: `Claude Code: Tool result${usageSuffix}`,
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: `Claude Code: ${toolName} running ${elapsed > 0 ? `(${elapsed.toFixed(0)}s)` : ''}...${usageSuffix}`,
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: `Claude Code: Tool done${usageSuffix}`,
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: `Claude Code: Writing...${usageSuffix}`,
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: `Claude Code: Error`,
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: `Claude Code: Context at ${result.context.estimatedContextPercent}% (${turns} turns)`,
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: `Claude Code: Complete (${turns} turns, ${costLabel})`,
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,28 +218,26 @@ export const ClaudeManagerPlugin = async ({ worktree }) => {
196
218
  config.agent[AGENT_ENGINEER_BUILD] ??= buildEngineerBuildAgentConfig(derivedPrompts);
197
219
  },
198
220
  tool: {
199
- engineer_send: tool({
200
- description: 'Send a message to the persistent Claude Code session with explicit mode control. ' +
201
- 'Most agents should use engineer_send_plan or engineer_send_build instead.',
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 executeEngineerSend(args, context);
235
+ return executeDelegate({ ...args, mode: 'plan' }, context);
214
236
  },
215
237
  }),
216
- engineer_send_plan: tool({
217
- description: 'Send a read-only investigation message to the Claude Code session in plan mode. ' +
218
- 'The engineer will analyze code without making edits. ' +
219
- 'Auto-creates a session on first call. Resumes the existing session on subsequent calls. ' +
220
- '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.',
221
241
  args: {
222
242
  message: tool.schema.string().min(1),
223
243
  model: tool.schema
@@ -228,34 +248,12 @@ export const ClaudeManagerPlugin = async ({ worktree }) => {
228
248
  cwd: tool.schema.string().optional(),
229
249
  },
230
250
  async execute(args, context) {
231
- return executeEngineerSend({ ...args, mode: 'plan' }, context);
251
+ return executeDelegate({ ...args, mode: 'free' }, context);
232
252
  },
233
253
  }),
234
- engineer_send_build: tool({
235
- description: 'Send an implementation message to the Claude Code session in free mode. ' +
236
- 'The engineer can read, edit, and create files. ' +
237
- 'Auto-creates a session on first call. Resumes the existing session on subsequent calls. ' +
238
- 'Returns the assistant response and current context health snapshot. ' +
239
- 'Prefer claude-opus-4-6 (default) for most coding work; use a Sonnet model for faster/lighter tasks. ' +
240
- 'Prefer effort "high" (default) for most work; use "medium" for lighter tasks and "max" for especially hard problems.',
241
- args: {
242
- message: tool.schema.string().min(1),
243
- model: tool.schema
244
- .enum(['claude-opus-4-6', 'claude-sonnet-4-6', 'claude-sonnet-4-5'])
245
- .optional(),
246
- effort: tool.schema.enum(['low', 'medium', 'high', 'max']).default('high'),
247
- freshSession: tool.schema.boolean().default(false),
248
- cwd: tool.schema.string().optional(),
249
- },
250
- async execute(args, context) {
251
- return executeEngineerSend({ ...args, mode: 'free' }, context);
252
- },
253
- }),
254
- engineer_compact: tool({
255
- description: 'Compact the active Claude Code session to reclaim context space. ' +
256
- 'Sends /compact to the session, which compresses prior conversation while preserving state. ' +
257
- 'Use before clearing when context is high but the session still has useful state. ' +
258
- '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.',
259
257
  args: {
260
258
  cwd: tool.schema.string().optional(),
261
259
  },
@@ -267,9 +265,12 @@ export const ClaudeManagerPlugin = async ({ worktree }) => {
267
265
  const contextWarning = formatContextWarning(snap);
268
266
  context.metadata({
269
267
  title: contextWarning
270
- ? `Claude Code: Compacted — context at ${snap.estimatedContextPercent}%`
271
- : `Claude Code: Compacted (${snap.totalTurns} turns, $${(snap.totalCostUsd ?? 0).toFixed(4)})`,
272
- metadata: { sessionId: result.sessionId },
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
+ },
273
274
  });
274
275
  return JSON.stringify({
275
276
  sessionId: result.sessionId,
@@ -317,9 +318,9 @@ export const ClaudeManagerPlugin = async ({ worktree }) => {
317
318
  return JSON.stringify(result, null, 2);
318
319
  },
319
320
  }),
320
- engineer_clear: tool({
321
- description: 'Clear the active Claude Code session. The next send will start a fresh session. ' +
322
- 'Use when context is full, the session is confused, or starting a different task.',
321
+ clear_session: tool({
322
+ description: 'Clear the active session to start fresh. ' +
323
+ 'Use when context is full or starting a new task.',
323
324
  args: {
324
325
  cwd: tool.schema.string().optional(),
325
326
  reason: tool.schema.string().optional(),
@@ -332,8 +333,8 @@ export const ClaudeManagerPlugin = async ({ worktree }) => {
332
333
  return JSON.stringify({ clearedSessionId: clearedId });
333
334
  },
334
335
  }),
335
- engineer_status: tool({
336
- description: 'Get the current persistent session status: context usage %, turns, cost, active session ID.',
336
+ session_health: tool({
337
+ description: 'Check session health metrics: context usage %, turn count, cost, and session ID.',
337
338
  args: {
338
339
  cwd: tool.schema.string().optional(),
339
340
  },
@@ -349,9 +350,8 @@ export const ClaudeManagerPlugin = async ({ worktree }) => {
349
350
  }, null, 2);
350
351
  },
351
352
  }),
352
- engineer_sessions: tool({
353
- description: 'List Claude sessions or inspect a saved transcript. ' +
354
- '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.',
355
355
  args: {
356
356
  cwd: tool.schema.string().optional(),
357
357
  sessionId: tool.schema.string().optional(),
@@ -373,8 +373,8 @@ export const ClaudeManagerPlugin = async ({ worktree }) => {
373
373
  return JSON.stringify(sessions, null, 2);
374
374
  },
375
375
  }),
376
- engineer_runs: tool({
377
- description: 'List persistent manager run records.',
376
+ list_history: tool({
377
+ description: 'List persistent run records from the manager or inspect a specific run.',
378
378
  args: {
379
379
  cwd: tool.schema.string().optional(),
380
380
  runId: tool.schema.string().optional(),
@@ -480,8 +480,12 @@ export const ClaudeManagerPlugin = async ({ worktree }) => {
480
480
  },
481
481
  };
482
482
  };
483
- function annotateToolRun(context, title, metadata) {
484
- context.metadata({ title, metadata });
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
+ });
485
489
  }
486
490
  function formatLiveUsage(turns, cost) {
487
491
  if (turns === undefined && cost === undefined) {
@@ -489,10 +493,10 @@ function formatLiveUsage(turns, cost) {
489
493
  }
490
494
  const parts = [];
491
495
  if (turns !== undefined) {
492
- parts.push(`${turns} turns`);
496
+ parts.push(`🔄 ${turns} turns`);
493
497
  }
494
498
  if (cost !== undefined) {
495
- parts.push(`$${cost.toFixed(4)}`);
499
+ parts.push(`💰 $${cost.toFixed(4)}`);
496
500
  }
497
501
  return ` (${parts.join(', ')})`;
498
502
  }
@@ -512,3 +516,47 @@ function formatContextWarning(context) {
512
516
  .replace('{turns}', String(totalTurns))
513
517
  .replace('{cost}', totalCostUsd.toFixed(2));
514
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
+ }
@@ -18,6 +18,15 @@ export const managerPromptRegistry = {
18
18
  'You are a staff+ technical owner who uses Claude Code better than anyone.',
19
19
  'You own the outcome — discover the right problem before solving it.',
20
20
  '',
21
+ '## Core principle: delegation-first',
22
+ 'Your default action is to delegate. Do not do broad repo exploration yourself',
23
+ 'when an engineer can do it. Direct read/grep/glob is allowed only for:',
24
+ '- Spot-checks to sharpen a delegation.',
25
+ '- Verifying a result after an engineer returns.',
26
+ '- Resolving one high-leverage ambiguity before dispatching.',
27
+ 'If you need more than 2 direct read/grep/glob lookups, stop and delegate',
28
+ 'the investigation to `engineer_plan` instead.',
29
+ '',
21
30
  '## Core principle: technical ownership',
22
31
  'You are not a ticket-taker. Before acting, look for:',
23
32
  '- Hidden assumptions and missing constraints.',
@@ -42,11 +51,12 @@ export const managerPromptRegistry = {
42
51
  ' 2. What is underspecified or conflicting.',
43
52
  ' 3. What the cleanest architecture is.',
44
53
  ' 4. What should be clarified before proceeding.',
45
- ' Read relevant code yourself. Understand the current state.',
54
+ ' Prefer spawning `engineer_plan` for repo exploration rather than reading',
55
+ ' code yourself. Use at most 1-2 spot-check reads to sharpen the delegation.',
46
56
  ' Then delegate with file paths, line numbers, patterns, and verification.',
47
57
  '',
48
58
  '**Complex tasks** (multi-file feature, large refactor):',
49
- ' 1. Investigate: read code, grep for patterns, spawn `engineer_plan` to explore.',
59
+ ' 1. Spawn `engineer_plan` to explore the repo, map dependencies, and analyze impact.',
50
60
  ' 2. If requirements are unclear, ask the user ONE high-leverage question —',
51
61
  ' only when it materially changes architecture, ownership, or destructive behavior.',
52
62
  ' Prefer the question tool when discrete options exist.',
@@ -133,18 +143,18 @@ export const managerPromptRegistry = {
133
143
  '- Ask ONE clarification first if it materially improves architecture.',
134
144
  '',
135
145
  '## Repo-context investigation',
136
- '- You MAY use read, grep, glob, and other search tools to investigate',
137
- ' the repo before delegating. Build context so your delegation is precise.',
146
+ '- Use read/grep/glob sparingly only for spot-checks to sharpen a delegation.',
147
+ '- If more than 2 lookups are needed, send the investigation to the engineer.',
138
148
  '- Do NOT implement changes yourself — investigation only.',
139
149
  '',
140
150
  '## Behavior',
141
- '- Send the objective to the engineer using engineer_send_plan.',
151
+ '- Send the objective to the engineer using explore.',
142
152
  "- Return the engineer's response verbatim. Do not summarize.",
143
- '- Use freshSession:true on engineer_send_plan when the task is unrelated to prior work.',
153
+ '- Use freshSession:true on explore when the task is unrelated to prior work.',
144
154
  '',
145
155
  '## Context management',
146
- '- Check engineer_status before sending.',
147
- '- Under 50%: proceed. Over 70%: engineer_compact. Over 85%: engineer_clear.',
156
+ '- Check session_health before sending.',
157
+ '- Under 50%: proceed. Over 70%: compact_context. Over 85%: clear_session.',
148
158
  '',
149
159
  '## Model selection',
150
160
  '- claude-opus-4-6 + high: complex analysis (default).',
@@ -176,18 +186,18 @@ export const managerPromptRegistry = {
176
186
  '- Ask ONE clarification first if it materially improves architecture.',
177
187
  '',
178
188
  '## Repo-context investigation',
179
- '- You MAY use read, grep, glob, and other search tools to investigate',
180
- ' the repo before delegating. Build context so your delegation is precise.',
189
+ '- Use read/grep/glob sparingly only for spot-checks to sharpen a delegation.',
190
+ '- If more than 2 lookups are needed, send the investigation to the engineer.',
181
191
  '- Do NOT implement changes yourself — investigation only.',
182
192
  '',
183
193
  '## Behavior',
184
- '- Send the objective to the engineer using engineer_send_build.',
194
+ '- Send the objective to the engineer using implement.',
185
195
  "- Return the engineer's response verbatim. Do not summarize.",
186
- '- Use freshSession:true on engineer_send_build when the task is unrelated to prior work.',
196
+ '- Use freshSession:true on implement when the task is unrelated to prior work.',
187
197
  '',
188
198
  '## Context management',
189
- '- Check engineer_status before sending.',
190
- '- Under 50%: proceed. Over 70%: engineer_compact. Over 85%: engineer_clear.',
199
+ '- Check session_health before sending.',
200
+ '- Under 50%: proceed. Over 70%: compact_context. Over 85%: clear_session.',
191
201
  '',
192
202
  '## Model selection',
193
203
  '- claude-opus-4-6 + high: most coding tasks (default).',
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@doingdev/opencode-claude-manager-plugin",
3
- "version": "0.1.33",
3
+ "version": "0.1.35",
4
4
  "description": "OpenCode plugin that orchestrates Claude Code sessions.",
5
5
  "keywords": [
6
6
  "opencode",
@@ -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,7 +0,0 @@
1
- import type { ClaudeMetadataSnapshot } from '../types/contracts.js';
2
- export declare class RepoClaudeConfigReader {
3
- read(cwd: string): Promise<ClaudeMetadataSnapshot>;
4
- private readSkills;
5
- private readCommands;
6
- private readSettings;
7
- }
@@ -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,2 +0,0 @@
1
- import type { Plugin } from '@opencode-ai/plugin';
2
- export declare const OrchestratorPlugin: Plugin;
@@ -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
- }