opencastle 0.27.0 → 0.27.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/cli.mjs +6 -0
- package/dist/cli/agents.d.ts +3 -0
- package/dist/cli/agents.d.ts.map +1 -0
- package/dist/cli/agents.js +161 -0
- package/dist/cli/agents.js.map +1 -0
- package/dist/cli/baselines.d.ts +3 -0
- package/dist/cli/baselines.d.ts.map +1 -0
- package/dist/cli/baselines.js +128 -0
- package/dist/cli/baselines.js.map +1 -0
- package/dist/cli/convoy/engine.d.ts +68 -2
- package/dist/cli/convoy/engine.d.ts.map +1 -1
- package/dist/cli/convoy/engine.js +2102 -26
- package/dist/cli/convoy/engine.js.map +1 -1
- package/dist/cli/convoy/engine.test.js +1572 -70
- package/dist/cli/convoy/engine.test.js.map +1 -1
- package/dist/cli/convoy/events.d.ts +4 -1
- package/dist/cli/convoy/events.d.ts.map +1 -1
- package/dist/cli/convoy/events.js +74 -13
- package/dist/cli/convoy/events.js.map +1 -1
- package/dist/cli/convoy/events.test.js +154 -27
- package/dist/cli/convoy/events.test.js.map +1 -1
- package/dist/cli/convoy/expertise.d.ts +16 -0
- package/dist/cli/convoy/expertise.d.ts.map +1 -0
- package/dist/cli/convoy/expertise.js +121 -0
- package/dist/cli/convoy/expertise.js.map +1 -0
- package/dist/cli/convoy/expertise.test.d.ts +2 -0
- package/dist/cli/convoy/expertise.test.d.ts.map +1 -0
- package/dist/cli/convoy/expertise.test.js +96 -0
- package/dist/cli/convoy/expertise.test.js.map +1 -0
- package/dist/cli/convoy/export.test.js +1 -0
- package/dist/cli/convoy/export.test.js.map +1 -1
- package/dist/cli/convoy/formula.d.ts +19 -0
- package/dist/cli/convoy/formula.d.ts.map +1 -0
- package/dist/cli/convoy/formula.js +142 -0
- package/dist/cli/convoy/formula.js.map +1 -0
- package/dist/cli/convoy/formula.test.d.ts +2 -0
- package/dist/cli/convoy/formula.test.d.ts.map +1 -0
- package/dist/cli/convoy/formula.test.js +342 -0
- package/dist/cli/convoy/formula.test.js.map +1 -0
- package/dist/cli/convoy/gates.d.ts +128 -0
- package/dist/cli/convoy/gates.d.ts.map +1 -0
- package/dist/cli/convoy/gates.js +606 -0
- package/dist/cli/convoy/gates.js.map +1 -0
- package/dist/cli/convoy/gates.test.d.ts +2 -0
- package/dist/cli/convoy/gates.test.d.ts.map +1 -0
- package/dist/cli/convoy/gates.test.js +976 -0
- package/dist/cli/convoy/gates.test.js.map +1 -0
- package/dist/cli/convoy/health.d.ts +11 -0
- package/dist/cli/convoy/health.d.ts.map +1 -1
- package/dist/cli/convoy/health.js +54 -0
- package/dist/cli/convoy/health.js.map +1 -1
- package/dist/cli/convoy/health.test.js +56 -1
- package/dist/cli/convoy/health.test.js.map +1 -1
- package/dist/cli/convoy/issues.d.ts +8 -0
- package/dist/cli/convoy/issues.d.ts.map +1 -0
- package/dist/cli/convoy/issues.js +98 -0
- package/dist/cli/convoy/issues.js.map +1 -0
- package/dist/cli/convoy/issues.test.d.ts +2 -0
- package/dist/cli/convoy/issues.test.d.ts.map +1 -0
- package/dist/cli/convoy/issues.test.js +107 -0
- package/dist/cli/convoy/issues.test.js.map +1 -0
- package/dist/cli/convoy/knowledge.d.ts +5 -0
- package/dist/cli/convoy/knowledge.d.ts.map +1 -0
- package/dist/cli/convoy/knowledge.js +116 -0
- package/dist/cli/convoy/knowledge.js.map +1 -0
- package/dist/cli/convoy/knowledge.test.d.ts +2 -0
- package/dist/cli/convoy/knowledge.test.d.ts.map +1 -0
- package/dist/cli/convoy/knowledge.test.js +87 -0
- package/dist/cli/convoy/knowledge.test.js.map +1 -0
- package/dist/cli/convoy/lessons.d.ts +17 -0
- package/dist/cli/convoy/lessons.d.ts.map +1 -0
- package/dist/cli/convoy/lessons.js +149 -0
- package/dist/cli/convoy/lessons.js.map +1 -0
- package/dist/cli/convoy/lessons.test.d.ts +2 -0
- package/dist/cli/convoy/lessons.test.d.ts.map +1 -0
- package/dist/cli/convoy/lessons.test.js +135 -0
- package/dist/cli/convoy/lessons.test.js.map +1 -0
- package/dist/cli/convoy/lock.d.ts +13 -0
- package/dist/cli/convoy/lock.d.ts.map +1 -0
- package/dist/cli/convoy/lock.js +88 -0
- package/dist/cli/convoy/lock.js.map +1 -0
- package/dist/cli/convoy/lock.test.d.ts +2 -0
- package/dist/cli/convoy/lock.test.d.ts.map +1 -0
- package/dist/cli/convoy/lock.test.js +136 -0
- package/dist/cli/convoy/lock.test.js.map +1 -0
- package/dist/cli/convoy/merge.d.ts +4 -0
- package/dist/cli/convoy/merge.d.ts.map +1 -1
- package/dist/cli/convoy/merge.js +18 -1
- package/dist/cli/convoy/merge.js.map +1 -1
- package/dist/cli/convoy/merge.test.js +6 -7
- package/dist/cli/convoy/merge.test.js.map +1 -1
- package/dist/cli/convoy/partition.d.ts +51 -0
- package/dist/cli/convoy/partition.d.ts.map +1 -0
- package/dist/cli/convoy/partition.js +186 -0
- package/dist/cli/convoy/partition.js.map +1 -0
- package/dist/cli/convoy/partition.test.d.ts +2 -0
- package/dist/cli/convoy/partition.test.d.ts.map +1 -0
- package/dist/cli/convoy/partition.test.js +315 -0
- package/dist/cli/convoy/partition.test.js.map +1 -0
- package/dist/cli/convoy/pipeline.test.js +6 -0
- package/dist/cli/convoy/pipeline.test.js.map +1 -1
- package/dist/cli/convoy/store.d.ts +47 -5
- package/dist/cli/convoy/store.d.ts.map +1 -1
- package/dist/cli/convoy/store.js +525 -19
- package/dist/cli/convoy/store.js.map +1 -1
- package/dist/cli/convoy/store.test.js +1345 -12
- package/dist/cli/convoy/store.test.js.map +1 -1
- package/dist/cli/convoy/types.d.ts +156 -2
- package/dist/cli/convoy/types.d.ts.map +1 -1
- package/dist/cli/run/adapters/claude.d.ts +2 -0
- package/dist/cli/run/adapters/claude.d.ts.map +1 -1
- package/dist/cli/run/adapters/claude.js +89 -49
- package/dist/cli/run/adapters/claude.js.map +1 -1
- package/dist/cli/run/adapters/claude.test.d.ts +2 -0
- package/dist/cli/run/adapters/claude.test.d.ts.map +1 -0
- package/dist/cli/run/adapters/claude.test.js +205 -0
- package/dist/cli/run/adapters/claude.test.js.map +1 -0
- package/dist/cli/run/adapters/copilot.d.ts +1 -0
- package/dist/cli/run/adapters/copilot.d.ts.map +1 -1
- package/dist/cli/run/adapters/copilot.js +84 -46
- package/dist/cli/run/adapters/copilot.js.map +1 -1
- package/dist/cli/run/adapters/copilot.test.d.ts +2 -0
- package/dist/cli/run/adapters/copilot.test.d.ts.map +1 -0
- package/dist/cli/run/adapters/copilot.test.js +195 -0
- package/dist/cli/run/adapters/copilot.test.js.map +1 -0
- package/dist/cli/run/adapters/cursor.d.ts +1 -0
- package/dist/cli/run/adapters/cursor.d.ts.map +1 -1
- package/dist/cli/run/adapters/cursor.js +83 -47
- package/dist/cli/run/adapters/cursor.js.map +1 -1
- package/dist/cli/run/adapters/cursor.test.d.ts +2 -0
- package/dist/cli/run/adapters/cursor.test.d.ts.map +1 -0
- package/dist/cli/run/adapters/cursor.test.js +129 -0
- package/dist/cli/run/adapters/cursor.test.js.map +1 -0
- package/dist/cli/run/adapters/opencode.d.ts +1 -0
- package/dist/cli/run/adapters/opencode.d.ts.map +1 -1
- package/dist/cli/run/adapters/opencode.js +81 -47
- package/dist/cli/run/adapters/opencode.js.map +1 -1
- package/dist/cli/run/adapters/opencode.test.d.ts +2 -0
- package/dist/cli/run/adapters/opencode.test.d.ts.map +1 -0
- package/dist/cli/run/adapters/opencode.test.js +119 -0
- package/dist/cli/run/adapters/opencode.test.js.map +1 -0
- package/dist/cli/run/executor.js +1 -1
- package/dist/cli/run/executor.js.map +1 -1
- package/dist/cli/run/schema.d.ts.map +1 -1
- package/dist/cli/run/schema.js +245 -4
- package/dist/cli/run/schema.js.map +1 -1
- package/dist/cli/run/schema.test.js +669 -0
- package/dist/cli/run/schema.test.js.map +1 -1
- package/dist/cli/run.d.ts.map +1 -1
- package/dist/cli/run.js +362 -22
- package/dist/cli/run.js.map +1 -1
- package/dist/cli/types.d.ts +85 -2
- package/dist/cli/types.d.ts.map +1 -1
- package/dist/cli/types.js.map +1 -1
- package/dist/cli/watch.d.ts +15 -0
- package/dist/cli/watch.d.ts.map +1 -0
- package/dist/cli/watch.js +279 -0
- package/dist/cli/watch.js.map +1 -0
- package/package.json +1 -1
- package/src/cli/agents.ts +177 -0
- package/src/cli/baselines.ts +143 -0
- package/src/cli/convoy/engine.test.ts +1839 -70
- package/src/cli/convoy/engine.ts +2417 -38
- package/src/cli/convoy/events.test.ts +179 -38
- package/src/cli/convoy/events.ts +88 -16
- package/src/cli/convoy/expertise.test.ts +128 -0
- package/src/cli/convoy/expertise.ts +163 -0
- package/src/cli/convoy/export.test.ts +1 -0
- package/src/cli/convoy/formula.test.ts +405 -0
- package/src/cli/convoy/formula.ts +174 -0
- package/src/cli/convoy/gates.test.ts +1169 -0
- package/src/cli/convoy/gates.ts +774 -0
- package/src/cli/convoy/health.test.ts +64 -2
- package/src/cli/convoy/health.ts +80 -2
- package/src/cli/convoy/issues.test.ts +143 -0
- package/src/cli/convoy/issues.ts +136 -0
- package/src/cli/convoy/knowledge.test.ts +101 -0
- package/src/cli/convoy/knowledge.ts +132 -0
- package/src/cli/convoy/lessons.test.ts +188 -0
- package/src/cli/convoy/lessons.ts +164 -0
- package/src/cli/convoy/lock.test.ts +181 -0
- package/src/cli/convoy/lock.ts +103 -0
- package/src/cli/convoy/merge.test.ts +6 -7
- package/src/cli/convoy/merge.ts +19 -1
- package/src/cli/convoy/partition.test.ts +423 -0
- package/src/cli/convoy/partition.ts +232 -0
- package/src/cli/convoy/pipeline.test.ts +6 -0
- package/src/cli/convoy/store.test.ts +1512 -14
- package/src/cli/convoy/store.ts +676 -30
- package/src/cli/convoy/types.ts +170 -1
- package/src/cli/run/adapters/claude.test.ts +234 -0
- package/src/cli/run/adapters/claude.ts +45 -5
- package/src/cli/run/adapters/copilot.test.ts +224 -0
- package/src/cli/run/adapters/copilot.ts +34 -4
- package/src/cli/run/adapters/cursor.test.ts +144 -0
- package/src/cli/run/adapters/cursor.ts +33 -2
- package/src/cli/run/adapters/opencode.test.ts +135 -0
- package/src/cli/run/adapters/opencode.ts +30 -2
- package/src/cli/run/executor.ts +1 -1
- package/src/cli/run/schema.test.ts +758 -0
- package/src/cli/run/schema.ts +300 -25
- package/src/cli/run.ts +341 -21
- package/src/cli/types.ts +86 -1
- package/src/cli/watch.ts +298 -0
- package/src/dashboard/node_modules/.vite/deps/_metadata.json +6 -6
package/src/cli/convoy/types.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export type ConvoyStatus = 'pending' | 'running' | 'done' | 'failed' | 'gate-failed'
|
|
1
|
+
export type ConvoyStatus = 'pending' | 'running' | 'done' | 'failed' | 'gate-failed' | 'hook-failed'
|
|
2
2
|
|
|
3
3
|
export type ConvoyTaskStatus =
|
|
4
4
|
| 'pending'
|
|
@@ -6,8 +6,13 @@ export type ConvoyTaskStatus =
|
|
|
6
6
|
| 'running'
|
|
7
7
|
| 'done'
|
|
8
8
|
| 'failed'
|
|
9
|
+
| 'gate-failed'
|
|
10
|
+
| 'review-blocked'
|
|
9
11
|
| 'timed-out'
|
|
10
12
|
| 'skipped'
|
|
13
|
+
| 'hook-failed'
|
|
14
|
+
| 'disputed'
|
|
15
|
+
| 'wait-for-input'
|
|
11
16
|
|
|
12
17
|
export type WorkerStatus = 'spawned' | 'running' | 'done' | 'failed' | 'killed'
|
|
13
18
|
|
|
@@ -26,6 +31,9 @@ export interface ConvoyRecord {
|
|
|
26
31
|
total_tokens: number | null
|
|
27
32
|
total_cost_usd: string | null
|
|
28
33
|
pipeline_id: string | null
|
|
34
|
+
circuit_state: string | null
|
|
35
|
+
review_tokens_total: number | null
|
|
36
|
+
review_budget: number | null
|
|
29
37
|
}
|
|
30
38
|
|
|
31
39
|
export interface TaskRecord {
|
|
@@ -52,6 +60,24 @@ export interface TaskRecord {
|
|
|
52
60
|
completion_tokens: number | null
|
|
53
61
|
total_tokens: number | null
|
|
54
62
|
cost_usd: string | null
|
|
63
|
+
gates: string | null
|
|
64
|
+
on_exhausted: 'dlq' | 'skip' | 'stop'
|
|
65
|
+
injected: number
|
|
66
|
+
provenance: string | null
|
|
67
|
+
idempotency_key: string | null
|
|
68
|
+
current_step: number | null
|
|
69
|
+
total_steps: number | null
|
|
70
|
+
review_level: string | null
|
|
71
|
+
review_verdict: string | null
|
|
72
|
+
review_tokens: number | null
|
|
73
|
+
review_model: string | null
|
|
74
|
+
panel_attempts: number
|
|
75
|
+
dispute_id: string | null
|
|
76
|
+
drift_score: number | null
|
|
77
|
+
drift_retried: number
|
|
78
|
+
outputs?: string | null // JSON array of TaskOutput
|
|
79
|
+
inputs?: string | null // JSON array of TaskInput
|
|
80
|
+
discovered_issues?: string | null // JSON array
|
|
55
81
|
}
|
|
56
82
|
|
|
57
83
|
export interface WorkerRecord {
|
|
@@ -90,3 +116,146 @@ export interface PipelineRecord {
|
|
|
90
116
|
total_tokens: number | null
|
|
91
117
|
total_cost_usd: string | null
|
|
92
118
|
}
|
|
119
|
+
|
|
120
|
+
export interface BuiltInGatesConfig {
|
|
121
|
+
secret_scan?: boolean
|
|
122
|
+
blast_radius?: boolean
|
|
123
|
+
dependency_audit?: 'auto' | boolean
|
|
124
|
+
regression_test?: 'auto' | boolean
|
|
125
|
+
browser_test?: 'auto' | boolean
|
|
126
|
+
gate_timeout?: number
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
export interface BrowserTestConfig {
|
|
131
|
+
urls: string[]
|
|
132
|
+
check_console_errors?: boolean
|
|
133
|
+
visual_diff_threshold?: number
|
|
134
|
+
a11y?: boolean
|
|
135
|
+
severity_threshold?: 'critical' | 'serious' | 'moderate' | 'minor'
|
|
136
|
+
baselines_dir?: string
|
|
137
|
+
}
|
|
138
|
+
export interface GuardConfig {
|
|
139
|
+
enabled?: boolean // default: true
|
|
140
|
+
agent?: string // optional agent name (e.g. 'session-guard')
|
|
141
|
+
checks?: string[] // e.g. ['observability', 'cleanup', 'cost-report']
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
export interface DlqRecord {
|
|
145
|
+
id: string
|
|
146
|
+
convoy_id: string
|
|
147
|
+
task_id: string
|
|
148
|
+
agent: string
|
|
149
|
+
failure_type: string
|
|
150
|
+
error_output: string | null
|
|
151
|
+
attempts: number
|
|
152
|
+
tokens_spent: number | null
|
|
153
|
+
escalation_task_id: string | null
|
|
154
|
+
resolved: number
|
|
155
|
+
resolution: string | null
|
|
156
|
+
created_at: string
|
|
157
|
+
resolved_at: string | null
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
export interface CircuitBreakerConfig {
|
|
161
|
+
threshold?: number // failures before Open (default: 3)
|
|
162
|
+
cooldown_ms?: number // ms in Open before Half-Open (default: 300000 = 5min)
|
|
163
|
+
fallback_agent?: string // reassign pending tasks when circuit opens
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
export interface TaskOutput {
|
|
167
|
+
name: string
|
|
168
|
+
type: 'file' | 'summary' | 'json'
|
|
169
|
+
description?: string
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
export interface TaskInput {
|
|
173
|
+
from: string
|
|
174
|
+
name: string
|
|
175
|
+
as?: string
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
export interface ArtifactRecord {
|
|
179
|
+
id: string
|
|
180
|
+
convoy_id: string
|
|
181
|
+
task_id: string
|
|
182
|
+
name: string
|
|
183
|
+
type: 'file' | 'summary' | 'json'
|
|
184
|
+
content: string
|
|
185
|
+
created_at: string
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
export interface AgentIdentityRecord {
|
|
189
|
+
id: string
|
|
190
|
+
agent: string
|
|
191
|
+
convoy_id: string
|
|
192
|
+
task_id: string
|
|
193
|
+
summary: string
|
|
194
|
+
created_at: string
|
|
195
|
+
retention_days: number
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
export interface StepCondition {
|
|
199
|
+
step: string // reference previous step by id
|
|
200
|
+
exitCode?: { eq?: number; ne?: number; gt?: number; lt?: number }
|
|
201
|
+
fileExists?: { path: string }
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
export interface TaskStep {
|
|
205
|
+
id?: string
|
|
206
|
+
prompt: string
|
|
207
|
+
gates?: string[]
|
|
208
|
+
max_retries?: number // inherits from task if omitted
|
|
209
|
+
if?: StepCondition
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
export interface Hook {
|
|
213
|
+
type: 'review' | 'guard' | 'agent' | 'command' | 'validate'
|
|
214
|
+
name?: string
|
|
215
|
+
prompt?: string // for agent hooks
|
|
216
|
+
command?: string // for command hooks
|
|
217
|
+
on?: 'pre_task' | 'post_task' | 'post_convoy'
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
export interface TaskStepRecord {
|
|
221
|
+
id: number
|
|
222
|
+
task_id: string
|
|
223
|
+
step_index: number
|
|
224
|
+
prompt: string
|
|
225
|
+
gates: string | null
|
|
226
|
+
status: string
|
|
227
|
+
exit_code: number | null
|
|
228
|
+
output: string | null
|
|
229
|
+
started_at: string | null
|
|
230
|
+
finished_at: string | null
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
export interface WatchTrigger {
|
|
234
|
+
type: 'file-change' | 'cron' | 'git-push'
|
|
235
|
+
glob?: string // for file-change: glob pattern to watch
|
|
236
|
+
schedule?: string // for cron: 5-field cron expression
|
|
237
|
+
branch?: string // for git-push: branch name pattern
|
|
238
|
+
debounce_ms?: number // file-change debounce (default: 500ms)
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
export interface WatchConfig {
|
|
242
|
+
triggers: WatchTrigger[]
|
|
243
|
+
clear_scratchpad?: boolean // clear scratchpad on watch start
|
|
244
|
+
scratchpad_retention_days?: number // auto-clear scratchpad entries older than N days
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
export interface ScratchpadRecord {
|
|
248
|
+
key: string
|
|
249
|
+
value: string
|
|
250
|
+
updated_at: string
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
export interface MCPServerConfig {
|
|
254
|
+
name: string
|
|
255
|
+
type: string
|
|
256
|
+
local?: boolean
|
|
257
|
+
command?: string
|
|
258
|
+
args?: string[]
|
|
259
|
+
url?: string
|
|
260
|
+
config?: Record<string, unknown>
|
|
261
|
+
}
|
|
@@ -0,0 +1,234 @@
|
|
|
1
|
+
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'
|
|
2
|
+
import { mkdtempSync, rmSync, existsSync, readFileSync, realpathSync } from 'node:fs'
|
|
3
|
+
import { tmpdir } from 'node:os'
|
|
4
|
+
import { join } from 'node:path'
|
|
5
|
+
import { EventEmitter } from 'node:events'
|
|
6
|
+
import type { Task } from '../../types.js'
|
|
7
|
+
|
|
8
|
+
// ── Helpers ───────────────────────────────────────────────────────────────────
|
|
9
|
+
|
|
10
|
+
function makeTask(): Task {
|
|
11
|
+
return {
|
|
12
|
+
id: 'test-task',
|
|
13
|
+
agent: 'developer',
|
|
14
|
+
prompt: 'Do something',
|
|
15
|
+
files: [],
|
|
16
|
+
timeout: '5m',
|
|
17
|
+
depends_on: [],
|
|
18
|
+
description: 'test task',
|
|
19
|
+
max_retries: 0,
|
|
20
|
+
} as unknown as Task
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function makeMockProc(exitCode = 0, stdoutData = '{"result":"ok"}') {
|
|
24
|
+
const proc = new EventEmitter() as EventEmitter & {
|
|
25
|
+
stdout: EventEmitter
|
|
26
|
+
stderr: EventEmitter
|
|
27
|
+
killed: boolean
|
|
28
|
+
kill: ReturnType<typeof vi.fn>
|
|
29
|
+
}
|
|
30
|
+
proc.stdout = new EventEmitter()
|
|
31
|
+
proc.stderr = new EventEmitter()
|
|
32
|
+
proc.killed = false
|
|
33
|
+
proc.kill = vi.fn()
|
|
34
|
+
process.nextTick(() => {
|
|
35
|
+
if (stdoutData) proc.stdout.emit('data', Buffer.from(stdoutData))
|
|
36
|
+
proc.emit('close', exitCode)
|
|
37
|
+
})
|
|
38
|
+
return proc
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// ── SDK mode ──────────────────────────────────────────────────────────────────
|
|
42
|
+
|
|
43
|
+
describe('claude adapter — SDK mode', () => {
|
|
44
|
+
let mockCreateSession: ReturnType<typeof vi.fn>
|
|
45
|
+
let mockSession: {
|
|
46
|
+
sendAndWait: ReturnType<typeof vi.fn>
|
|
47
|
+
on: ReturnType<typeof vi.fn>
|
|
48
|
+
destroy: ReturnType<typeof vi.fn>
|
|
49
|
+
abort: ReturnType<typeof vi.fn>
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
beforeEach(() => {
|
|
53
|
+
vi.resetModules()
|
|
54
|
+
mockSession = {
|
|
55
|
+
sendAndWait: vi.fn().mockResolvedValue({ data: { content: 'I did the task' } }),
|
|
56
|
+
on: vi.fn(),
|
|
57
|
+
destroy: vi.fn().mockResolvedValue(undefined),
|
|
58
|
+
abort: vi.fn().mockResolvedValue(undefined),
|
|
59
|
+
}
|
|
60
|
+
mockCreateSession = vi.fn().mockResolvedValue(mockSession)
|
|
61
|
+
vi.doMock('@anthropic-ai/agent-sdk', () => {
|
|
62
|
+
// Must use a regular function (not arrow) so `new AgentClient()` works
|
|
63
|
+
function MockAgentClient(this: Record<string, unknown>) {
|
|
64
|
+
this.start = vi.fn().mockResolvedValue(undefined)
|
|
65
|
+
this.createSession = mockCreateSession
|
|
66
|
+
}
|
|
67
|
+
return {
|
|
68
|
+
AgentClient: MockAgentClient,
|
|
69
|
+
approveAll: vi.fn(),
|
|
70
|
+
}
|
|
71
|
+
})
|
|
72
|
+
})
|
|
73
|
+
|
|
74
|
+
afterEach(() => {
|
|
75
|
+
vi.restoreAllMocks()
|
|
76
|
+
})
|
|
77
|
+
|
|
78
|
+
it('passes mcpServers to createSession when provided', async () => {
|
|
79
|
+
const { execute } = await import('./claude.js')
|
|
80
|
+
const mcpServers = [{ name: 'my-mcp', type: 'local', command: 'node', args: ['server.js'] }]
|
|
81
|
+
await execute(makeTask(), { mcpServers })
|
|
82
|
+
expect(mockCreateSession).toHaveBeenCalledWith(
|
|
83
|
+
expect.objectContaining({ mcpServers }),
|
|
84
|
+
)
|
|
85
|
+
})
|
|
86
|
+
|
|
87
|
+
it('does NOT include mcpServers in createSession when not provided', async () => {
|
|
88
|
+
const { execute } = await import('./claude.js')
|
|
89
|
+
await execute(makeTask(), {})
|
|
90
|
+
const callArg = mockCreateSession.mock.calls[0]?.[0] as Record<string, unknown>
|
|
91
|
+
expect(callArg).not.toHaveProperty('mcpServers')
|
|
92
|
+
})
|
|
93
|
+
|
|
94
|
+
it('does NOT include mcpServers when mcpServers is empty array', async () => {
|
|
95
|
+
const { execute } = await import('./claude.js')
|
|
96
|
+
await execute(makeTask(), { mcpServers: [] })
|
|
97
|
+
const callArg = mockCreateSession.mock.calls[0]?.[0] as Record<string, unknown>
|
|
98
|
+
expect(callArg).not.toHaveProperty('mcpServers')
|
|
99
|
+
})
|
|
100
|
+
})
|
|
101
|
+
|
|
102
|
+
// ── CLI mode ──────────────────────────────────────────────────────────────────
|
|
103
|
+
|
|
104
|
+
describe('claude adapter — CLI mode', () => {
|
|
105
|
+
let tmpDir: string
|
|
106
|
+
let mockSpawn: ReturnType<typeof vi.fn>
|
|
107
|
+
|
|
108
|
+
beforeEach(() => {
|
|
109
|
+
vi.resetModules()
|
|
110
|
+
tmpDir = realpathSync(mkdtempSync(join(tmpdir(), 'claude-test-')))
|
|
111
|
+
|
|
112
|
+
mockSpawn = vi.fn().mockImplementation((cmd: string) => {
|
|
113
|
+
if (cmd === 'which') return makeMockProc(0, '')
|
|
114
|
+
return makeMockProc(0, '{"result":"ok"}')
|
|
115
|
+
})
|
|
116
|
+
vi.doMock('node:child_process', () => ({ spawn: mockSpawn }))
|
|
117
|
+
})
|
|
118
|
+
|
|
119
|
+
afterEach(() => {
|
|
120
|
+
rmSync(tmpDir, { recursive: true, force: true })
|
|
121
|
+
vi.restoreAllMocks()
|
|
122
|
+
})
|
|
123
|
+
|
|
124
|
+
it('writes mcp.json to cwd with correct format when mcpServers provided', async () => {
|
|
125
|
+
let capturedContent: string | null = null
|
|
126
|
+
mockSpawn.mockImplementation((cmd: string) => {
|
|
127
|
+
if (cmd === 'which') return makeMockProc(0, '')
|
|
128
|
+
const mcpPath = join(tmpDir, 'mcp.json')
|
|
129
|
+
if (existsSync(mcpPath)) {
|
|
130
|
+
capturedContent = readFileSync(mcpPath, 'utf8')
|
|
131
|
+
}
|
|
132
|
+
return makeMockProc(0, '{}')
|
|
133
|
+
})
|
|
134
|
+
|
|
135
|
+
const { executeViaCli } = await import('./claude.js')
|
|
136
|
+
const mcpServers = [{ name: 'my-mcp', type: 'local', command: 'node', args: ['server.js'] }]
|
|
137
|
+
await executeViaCli(makeTask(), { mcpServers, cwd: tmpDir })
|
|
138
|
+
|
|
139
|
+
expect(capturedContent).not.toBeNull()
|
|
140
|
+
expect(JSON.parse(capturedContent!)).toEqual({
|
|
141
|
+
mcpServers: { 'my-mcp': { command: 'node', args: ['server.js'] } },
|
|
142
|
+
})
|
|
143
|
+
})
|
|
144
|
+
|
|
145
|
+
it('passes --mcp-config flag pointing to mcp.json path', async () => {
|
|
146
|
+
const capturedArgs: string[] = []
|
|
147
|
+
mockSpawn.mockImplementation((cmd: string, args: string[]) => {
|
|
148
|
+
if (cmd === 'which') return makeMockProc(0, '')
|
|
149
|
+
capturedArgs.push(...args)
|
|
150
|
+
return makeMockProc(0, '{}')
|
|
151
|
+
})
|
|
152
|
+
const { executeViaCli } = await import('./claude.js')
|
|
153
|
+
const mcpServers = [{ name: 'my-mcp', type: 'local', command: 'node', args: ['server.js'] }]
|
|
154
|
+
await executeViaCli(makeTask(), { mcpServers, cwd: tmpDir })
|
|
155
|
+
|
|
156
|
+
const idx = capturedArgs.indexOf('--mcp-config')
|
|
157
|
+
expect(idx).toBeGreaterThanOrEqual(0)
|
|
158
|
+
expect(capturedArgs[idx + 1]).toBe(join(tmpDir, 'mcp.json'))
|
|
159
|
+
})
|
|
160
|
+
|
|
161
|
+
it('cleans up mcp.json after successful execution', async () => {
|
|
162
|
+
const { executeViaCli } = await import('./claude.js')
|
|
163
|
+
const mcpServers = [{ name: 'my-mcp', type: 'local', command: 'node', args: ['server.js'] }]
|
|
164
|
+
await executeViaCli(makeTask(), { mcpServers, cwd: tmpDir })
|
|
165
|
+
expect(existsSync(join(tmpDir, 'mcp.json'))).toBe(false)
|
|
166
|
+
})
|
|
167
|
+
|
|
168
|
+
it('cleans up mcp.json after failed execution (non-zero exit)', async () => {
|
|
169
|
+
mockSpawn.mockImplementation((cmd: string) => {
|
|
170
|
+
if (cmd === 'which') return makeMockProc(0, '')
|
|
171
|
+
return makeMockProc(1, '')
|
|
172
|
+
})
|
|
173
|
+
const { executeViaCli } = await import('./claude.js')
|
|
174
|
+
const mcpServers = [{ name: 'err-mcp', type: 'local', command: 'node', args: [] }]
|
|
175
|
+
await executeViaCli(makeTask(), { mcpServers, cwd: tmpDir })
|
|
176
|
+
expect(existsSync(join(tmpDir, 'mcp.json'))).toBe(false)
|
|
177
|
+
})
|
|
178
|
+
|
|
179
|
+
it('includes --approve-mcps flag when mcp_approve_all is true', async () => {
|
|
180
|
+
const capturedArgs: string[] = []
|
|
181
|
+
mockSpawn.mockImplementation((cmd: string, args: string[]) => {
|
|
182
|
+
if (cmd === 'which') return makeMockProc(0, '')
|
|
183
|
+
capturedArgs.push(...args)
|
|
184
|
+
return makeMockProc(0, '{}')
|
|
185
|
+
})
|
|
186
|
+
const { executeViaCli } = await import('./claude.js')
|
|
187
|
+
await executeViaCli(makeTask(), { mcp_approve_all: true, cwd: tmpDir })
|
|
188
|
+
expect(capturedArgs).toContain('--approve-mcps')
|
|
189
|
+
})
|
|
190
|
+
|
|
191
|
+
it('does NOT write mcp.json when mcpServers not configured', async () => {
|
|
192
|
+
const { executeViaCli } = await import('./claude.js')
|
|
193
|
+
await executeViaCli(makeTask(), { cwd: tmpDir })
|
|
194
|
+
expect(existsSync(join(tmpDir, 'mcp.json'))).toBe(false)
|
|
195
|
+
})
|
|
196
|
+
|
|
197
|
+
it('does NOT add --approve-mcps when mcp_approve_all is not set', async () => {
|
|
198
|
+
const capturedArgs: string[] = []
|
|
199
|
+
mockSpawn.mockImplementation((cmd: string, args: string[]) => {
|
|
200
|
+
if (cmd === 'which') return makeMockProc(0, '')
|
|
201
|
+
capturedArgs.push(...args)
|
|
202
|
+
return makeMockProc(0, '{}')
|
|
203
|
+
})
|
|
204
|
+
const { executeViaCli } = await import('./claude.js')
|
|
205
|
+
await executeViaCli(makeTask(), { cwd: tmpDir })
|
|
206
|
+
expect(capturedArgs).not.toContain('--approve-mcps')
|
|
207
|
+
})
|
|
208
|
+
|
|
209
|
+
it('maps mcpServers with url and config into mcp.json', async () => {
|
|
210
|
+
let capturedContent: string | null = null
|
|
211
|
+
mockSpawn.mockImplementation((cmd: string) => {
|
|
212
|
+
if (cmd === 'which') return makeMockProc(0, '')
|
|
213
|
+
const mcpPath = join(tmpDir, 'mcp.json')
|
|
214
|
+
if (existsSync(mcpPath)) capturedContent = readFileSync(mcpPath, 'utf8')
|
|
215
|
+
return makeMockProc(0, '{}')
|
|
216
|
+
})
|
|
217
|
+
const { executeViaCli } = await import('./claude.js')
|
|
218
|
+
const mcpServers = [
|
|
219
|
+
{
|
|
220
|
+
name: 'remote-mcp',
|
|
221
|
+
type: 'remote',
|
|
222
|
+
url: 'http://localhost:9000',
|
|
223
|
+
config: { token: 'abc' },
|
|
224
|
+
},
|
|
225
|
+
]
|
|
226
|
+
await executeViaCli(makeTask(), { mcpServers, cwd: tmpDir })
|
|
227
|
+
expect(capturedContent).not.toBeNull()
|
|
228
|
+
const parsed = JSON.parse(capturedContent!) as { mcpServers: Record<string, Record<string, unknown>> }
|
|
229
|
+
expect(parsed.mcpServers['remote-mcp']).toMatchObject({
|
|
230
|
+
url: 'http://localhost:9000',
|
|
231
|
+
token: 'abc',
|
|
232
|
+
})
|
|
233
|
+
})
|
|
234
|
+
})
|
|
@@ -1,10 +1,13 @@
|
|
|
1
1
|
import { spawn } from 'node:child_process'
|
|
2
|
+
import { writeFileSync, unlinkSync } from 'node:fs'
|
|
3
|
+
import { join } from 'node:path'
|
|
2
4
|
import { parseTimeout } from '../schema.js'
|
|
3
5
|
import type { Task, ExecuteOptions, ExecuteResult, TokenUsage } from '../../types.js'
|
|
4
6
|
|
|
5
7
|
// Adapter name
|
|
6
8
|
export const name = 'claude'
|
|
7
9
|
|
|
10
|
+
export function supportsSessionContinuity(): boolean { return false }
|
|
8
11
|
// Module-level state for mode selection
|
|
9
12
|
let mode: 'sdk' | 'cli' | null = null
|
|
10
13
|
|
|
@@ -100,6 +103,8 @@ async function executeViaSdk(task: Task, options: ExecuteOptions = {}): Promise<
|
|
|
100
103
|
},
|
|
101
104
|
infiniteSessions: { enabled: false },
|
|
102
105
|
...(options.verbose ? { streaming: true } : {}),
|
|
106
|
+
// mcpServers is forward-compatible: field will be recognised by future SDK versions
|
|
107
|
+
...(options.mcpServers?.length ? { mcpServers: options.mcpServers } : {}),
|
|
103
108
|
})
|
|
104
109
|
activeSessions.set(task.id, session)
|
|
105
110
|
if (options.verbose) {
|
|
@@ -108,12 +113,21 @@ async function executeViaSdk(task: Task, options: ExecuteOptions = {}): Promise<
|
|
|
108
113
|
process.stdout.write(event.data.deltaContent)
|
|
109
114
|
})
|
|
110
115
|
}
|
|
116
|
+
interface SdkResponse {
|
|
117
|
+
data?: {
|
|
118
|
+
content?: string
|
|
119
|
+
usage?: Record<string, number>
|
|
120
|
+
}
|
|
121
|
+
usage?: Record<string, number>
|
|
122
|
+
}
|
|
123
|
+
|
|
111
124
|
try {
|
|
112
125
|
const timeoutMs = parseTimeout(task.timeout)
|
|
113
126
|
const response = await session.sendAndWait({ prompt }, timeoutMs)
|
|
114
|
-
const
|
|
127
|
+
const typed = response as SdkResponse
|
|
128
|
+
const data = typed?.data
|
|
115
129
|
const output = (data?.content as string | undefined) ?? ''
|
|
116
|
-
const rawUsage = data?.usage ??
|
|
130
|
+
const rawUsage = data?.usage ?? typed?.usage
|
|
117
131
|
const u = rawUsage as Record<string, number> | undefined
|
|
118
132
|
const usageResult = u
|
|
119
133
|
? {
|
|
@@ -150,7 +164,7 @@ function killSdk(task: Task): void {
|
|
|
150
164
|
}
|
|
151
165
|
|
|
152
166
|
// --- CLI implementation (from claude-code.ts) ---
|
|
153
|
-
async function executeViaCli(task: Task, options: ExecuteOptions = {}): Promise<ExecuteResult> {
|
|
167
|
+
export async function executeViaCli(task: Task, options: ExecuteOptions = {}): Promise<ExecuteResult> {
|
|
154
168
|
let prompt = `You are a ${task.agent}. ${task.prompt}`
|
|
155
169
|
if (task.files && task.files.length > 0) {
|
|
156
170
|
prompt += `\n\nOnly modify files under: ${task.files.join(', ')}`
|
|
@@ -163,11 +177,32 @@ async function executeViaCli(task: Task, options: ExecuteOptions = {}): Promise<
|
|
|
163
177
|
'--max-turns',
|
|
164
178
|
'50',
|
|
165
179
|
]
|
|
166
|
-
|
|
180
|
+
const cwd = options?.cwd ?? process.cwd()
|
|
181
|
+
const mcpJsonPath = join(cwd, 'mcp.json')
|
|
182
|
+
let wroteJson = false
|
|
183
|
+
if (options.mcpServers?.length) {
|
|
184
|
+
const mcpJson: Record<string, Record<string, unknown>> = {}
|
|
185
|
+
for (const server of options.mcpServers) {
|
|
186
|
+
const entry: Record<string, unknown> = {}
|
|
187
|
+
if (server.command) entry.command = server.command
|
|
188
|
+
if (server.args) entry.args = server.args
|
|
189
|
+
if (server.url) entry.url = server.url
|
|
190
|
+
if (server.config) Object.assign(entry, server.config)
|
|
191
|
+
mcpJson[server.name] = entry
|
|
192
|
+
}
|
|
193
|
+
writeFileSync(mcpJsonPath, JSON.stringify({ mcpServers: mcpJson }, null, 2), 'utf8')
|
|
194
|
+
args.push('--mcp-config', mcpJsonPath)
|
|
195
|
+
wroteJson = true
|
|
196
|
+
}
|
|
197
|
+
if (options.mcp_approve_all) {
|
|
198
|
+
args.push('--approve-mcps')
|
|
199
|
+
}
|
|
200
|
+
try {
|
|
201
|
+
return await new Promise<ExecuteResult>((resolve) => {
|
|
167
202
|
const proc = spawn('claude', args, {
|
|
168
203
|
stdio: ['ignore', 'pipe', 'pipe'],
|
|
169
204
|
env: { ...process.env },
|
|
170
|
-
cwd
|
|
205
|
+
cwd,
|
|
171
206
|
})
|
|
172
207
|
let stdout = ''
|
|
173
208
|
let stderr = ''
|
|
@@ -212,6 +247,11 @@ async function executeViaCli(task: Task, options: ExecuteOptions = {}): Promise<
|
|
|
212
247
|
})
|
|
213
248
|
task._process = proc
|
|
214
249
|
})
|
|
250
|
+
} finally {
|
|
251
|
+
if (wroteJson) {
|
|
252
|
+
try { unlinkSync(mcpJsonPath) } catch { /* ignore */ }
|
|
253
|
+
}
|
|
254
|
+
}
|
|
215
255
|
}
|
|
216
256
|
|
|
217
257
|
function killCli(task: Task): void {
|