opencastle 0.27.0 → 0.27.2

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.
Files changed (242) hide show
  1. package/bin/cli.mjs +6 -0
  2. package/dist/cli/agents.d.ts +3 -0
  3. package/dist/cli/agents.d.ts.map +1 -0
  4. package/dist/cli/agents.js +161 -0
  5. package/dist/cli/agents.js.map +1 -0
  6. package/dist/cli/baselines.d.ts +3 -0
  7. package/dist/cli/baselines.d.ts.map +1 -0
  8. package/dist/cli/baselines.js +128 -0
  9. package/dist/cli/baselines.js.map +1 -0
  10. package/dist/cli/convoy/dashboard-types.d.ts +146 -0
  11. package/dist/cli/convoy/dashboard-types.d.ts.map +1 -0
  12. package/dist/cli/convoy/dashboard-types.js +2 -0
  13. package/dist/cli/convoy/dashboard-types.js.map +1 -0
  14. package/dist/cli/convoy/engine.d.ts +67 -2
  15. package/dist/cli/convoy/engine.d.ts.map +1 -1
  16. package/dist/cli/convoy/engine.js +2036 -28
  17. package/dist/cli/convoy/engine.js.map +1 -1
  18. package/dist/cli/convoy/engine.test.js +1659 -70
  19. package/dist/cli/convoy/engine.test.js.map +1 -1
  20. package/dist/cli/convoy/event-schemas.d.ts +9 -0
  21. package/dist/cli/convoy/event-schemas.d.ts.map +1 -0
  22. package/dist/cli/convoy/event-schemas.js +185 -0
  23. package/dist/cli/convoy/event-schemas.js.map +1 -0
  24. package/dist/cli/convoy/events.d.ts +12 -1
  25. package/dist/cli/convoy/events.d.ts.map +1 -1
  26. package/dist/cli/convoy/events.js +186 -13
  27. package/dist/cli/convoy/events.js.map +1 -1
  28. package/dist/cli/convoy/events.test.js +325 -28
  29. package/dist/cli/convoy/events.test.js.map +1 -1
  30. package/dist/cli/convoy/expertise.d.ts +16 -0
  31. package/dist/cli/convoy/expertise.d.ts.map +1 -0
  32. package/dist/cli/convoy/expertise.js +121 -0
  33. package/dist/cli/convoy/expertise.js.map +1 -0
  34. package/dist/cli/convoy/expertise.test.d.ts +2 -0
  35. package/dist/cli/convoy/expertise.test.d.ts.map +1 -0
  36. package/dist/cli/convoy/expertise.test.js +96 -0
  37. package/dist/cli/convoy/expertise.test.js.map +1 -0
  38. package/dist/cli/convoy/export.test.js +1 -0
  39. package/dist/cli/convoy/export.test.js.map +1 -1
  40. package/dist/cli/convoy/formula.d.ts +19 -0
  41. package/dist/cli/convoy/formula.d.ts.map +1 -0
  42. package/dist/cli/convoy/formula.js +142 -0
  43. package/dist/cli/convoy/formula.js.map +1 -0
  44. package/dist/cli/convoy/formula.test.d.ts +2 -0
  45. package/dist/cli/convoy/formula.test.d.ts.map +1 -0
  46. package/dist/cli/convoy/formula.test.js +342 -0
  47. package/dist/cli/convoy/formula.test.js.map +1 -0
  48. package/dist/cli/convoy/gates.d.ts +128 -0
  49. package/dist/cli/convoy/gates.d.ts.map +1 -0
  50. package/dist/cli/convoy/gates.js +606 -0
  51. package/dist/cli/convoy/gates.js.map +1 -0
  52. package/dist/cli/convoy/gates.test.d.ts +2 -0
  53. package/dist/cli/convoy/gates.test.d.ts.map +1 -0
  54. package/dist/cli/convoy/gates.test.js +976 -0
  55. package/dist/cli/convoy/gates.test.js.map +1 -0
  56. package/dist/cli/convoy/health.d.ts +11 -0
  57. package/dist/cli/convoy/health.d.ts.map +1 -1
  58. package/dist/cli/convoy/health.js +54 -0
  59. package/dist/cli/convoy/health.js.map +1 -1
  60. package/dist/cli/convoy/health.test.js +56 -1
  61. package/dist/cli/convoy/health.test.js.map +1 -1
  62. package/dist/cli/convoy/issues.d.ts +8 -0
  63. package/dist/cli/convoy/issues.d.ts.map +1 -0
  64. package/dist/cli/convoy/issues.js +98 -0
  65. package/dist/cli/convoy/issues.js.map +1 -0
  66. package/dist/cli/convoy/issues.test.d.ts +2 -0
  67. package/dist/cli/convoy/issues.test.d.ts.map +1 -0
  68. package/dist/cli/convoy/issues.test.js +107 -0
  69. package/dist/cli/convoy/issues.test.js.map +1 -0
  70. package/dist/cli/convoy/knowledge.d.ts +5 -0
  71. package/dist/cli/convoy/knowledge.d.ts.map +1 -0
  72. package/dist/cli/convoy/knowledge.js +116 -0
  73. package/dist/cli/convoy/knowledge.js.map +1 -0
  74. package/dist/cli/convoy/knowledge.test.d.ts +2 -0
  75. package/dist/cli/convoy/knowledge.test.d.ts.map +1 -0
  76. package/dist/cli/convoy/knowledge.test.js +87 -0
  77. package/dist/cli/convoy/knowledge.test.js.map +1 -0
  78. package/dist/cli/convoy/lessons.d.ts +17 -0
  79. package/dist/cli/convoy/lessons.d.ts.map +1 -0
  80. package/dist/cli/convoy/lessons.js +149 -0
  81. package/dist/cli/convoy/lessons.js.map +1 -0
  82. package/dist/cli/convoy/lessons.test.d.ts +2 -0
  83. package/dist/cli/convoy/lessons.test.d.ts.map +1 -0
  84. package/dist/cli/convoy/lessons.test.js +135 -0
  85. package/dist/cli/convoy/lessons.test.js.map +1 -0
  86. package/dist/cli/convoy/lock.d.ts +13 -0
  87. package/dist/cli/convoy/lock.d.ts.map +1 -0
  88. package/dist/cli/convoy/lock.js +88 -0
  89. package/dist/cli/convoy/lock.js.map +1 -0
  90. package/dist/cli/convoy/lock.test.d.ts +2 -0
  91. package/dist/cli/convoy/lock.test.d.ts.map +1 -0
  92. package/dist/cli/convoy/lock.test.js +136 -0
  93. package/dist/cli/convoy/lock.test.js.map +1 -0
  94. package/dist/cli/convoy/log-merge.test.d.ts +2 -0
  95. package/dist/cli/convoy/log-merge.test.d.ts.map +1 -0
  96. package/dist/cli/convoy/log-merge.test.js +147 -0
  97. package/dist/cli/convoy/log-merge.test.js.map +1 -0
  98. package/dist/cli/convoy/merge.d.ts +4 -0
  99. package/dist/cli/convoy/merge.d.ts.map +1 -1
  100. package/dist/cli/convoy/merge.js +18 -1
  101. package/dist/cli/convoy/merge.js.map +1 -1
  102. package/dist/cli/convoy/merge.test.js +6 -7
  103. package/dist/cli/convoy/merge.test.js.map +1 -1
  104. package/dist/cli/convoy/partition.d.ts +51 -0
  105. package/dist/cli/convoy/partition.d.ts.map +1 -0
  106. package/dist/cli/convoy/partition.js +186 -0
  107. package/dist/cli/convoy/partition.js.map +1 -0
  108. package/dist/cli/convoy/partition.test.d.ts +2 -0
  109. package/dist/cli/convoy/partition.test.d.ts.map +1 -0
  110. package/dist/cli/convoy/partition.test.js +315 -0
  111. package/dist/cli/convoy/partition.test.js.map +1 -0
  112. package/dist/cli/convoy/pipeline.test.js +6 -0
  113. package/dist/cli/convoy/pipeline.test.js.map +1 -1
  114. package/dist/cli/convoy/store.d.ts +99 -7
  115. package/dist/cli/convoy/store.d.ts.map +1 -1
  116. package/dist/cli/convoy/store.js +764 -31
  117. package/dist/cli/convoy/store.js.map +1 -1
  118. package/dist/cli/convoy/store.test.js +1810 -18
  119. package/dist/cli/convoy/store.test.js.map +1 -1
  120. package/dist/cli/convoy/types.d.ts +427 -5
  121. package/dist/cli/convoy/types.d.ts.map +1 -1
  122. package/dist/cli/convoy/types.js +42 -1
  123. package/dist/cli/convoy/types.js.map +1 -1
  124. package/dist/cli/log.d.ts +11 -0
  125. package/dist/cli/log.d.ts.map +1 -1
  126. package/dist/cli/log.js +114 -2
  127. package/dist/cli/log.js.map +1 -1
  128. package/dist/cli/run/adapters/claude.d.ts +2 -0
  129. package/dist/cli/run/adapters/claude.d.ts.map +1 -1
  130. package/dist/cli/run/adapters/claude.js +89 -49
  131. package/dist/cli/run/adapters/claude.js.map +1 -1
  132. package/dist/cli/run/adapters/claude.test.d.ts +2 -0
  133. package/dist/cli/run/adapters/claude.test.d.ts.map +1 -0
  134. package/dist/cli/run/adapters/claude.test.js +205 -0
  135. package/dist/cli/run/adapters/claude.test.js.map +1 -0
  136. package/dist/cli/run/adapters/copilot.d.ts +1 -0
  137. package/dist/cli/run/adapters/copilot.d.ts.map +1 -1
  138. package/dist/cli/run/adapters/copilot.js +84 -46
  139. package/dist/cli/run/adapters/copilot.js.map +1 -1
  140. package/dist/cli/run/adapters/copilot.test.d.ts +2 -0
  141. package/dist/cli/run/adapters/copilot.test.d.ts.map +1 -0
  142. package/dist/cli/run/adapters/copilot.test.js +195 -0
  143. package/dist/cli/run/adapters/copilot.test.js.map +1 -0
  144. package/dist/cli/run/adapters/cursor.d.ts +1 -0
  145. package/dist/cli/run/adapters/cursor.d.ts.map +1 -1
  146. package/dist/cli/run/adapters/cursor.js +83 -47
  147. package/dist/cli/run/adapters/cursor.js.map +1 -1
  148. package/dist/cli/run/adapters/cursor.test.d.ts +2 -0
  149. package/dist/cli/run/adapters/cursor.test.d.ts.map +1 -0
  150. package/dist/cli/run/adapters/cursor.test.js +129 -0
  151. package/dist/cli/run/adapters/cursor.test.js.map +1 -0
  152. package/dist/cli/run/adapters/opencode.d.ts +1 -0
  153. package/dist/cli/run/adapters/opencode.d.ts.map +1 -1
  154. package/dist/cli/run/adapters/opencode.js +81 -47
  155. package/dist/cli/run/adapters/opencode.js.map +1 -1
  156. package/dist/cli/run/adapters/opencode.test.d.ts +2 -0
  157. package/dist/cli/run/adapters/opencode.test.d.ts.map +1 -0
  158. package/dist/cli/run/adapters/opencode.test.js +119 -0
  159. package/dist/cli/run/adapters/opencode.test.js.map +1 -0
  160. package/dist/cli/run/executor.js +1 -1
  161. package/dist/cli/run/executor.js.map +1 -1
  162. package/dist/cli/run/schema.d.ts.map +1 -1
  163. package/dist/cli/run/schema.js +245 -4
  164. package/dist/cli/run/schema.js.map +1 -1
  165. package/dist/cli/run/schema.test.js +669 -0
  166. package/dist/cli/run/schema.test.js.map +1 -1
  167. package/dist/cli/run.d.ts.map +1 -1
  168. package/dist/cli/run.js +362 -22
  169. package/dist/cli/run.js.map +1 -1
  170. package/dist/cli/types.d.ts +85 -2
  171. package/dist/cli/types.d.ts.map +1 -1
  172. package/dist/cli/types.js.map +1 -1
  173. package/dist/cli/watch.d.ts +15 -0
  174. package/dist/cli/watch.d.ts.map +1 -0
  175. package/dist/cli/watch.js +279 -0
  176. package/dist/cli/watch.js.map +1 -0
  177. package/package.json +5 -1
  178. package/src/cli/agents.ts +177 -0
  179. package/src/cli/baselines.ts +143 -0
  180. package/src/cli/convoy/TELEMETRY.md +203 -0
  181. package/src/cli/convoy/dashboard-types.ts +141 -0
  182. package/src/cli/convoy/engine.test.ts +1937 -70
  183. package/src/cli/convoy/engine.ts +2350 -40
  184. package/src/cli/convoy/event-schemas.ts +195 -0
  185. package/src/cli/convoy/events.test.ts +384 -39
  186. package/src/cli/convoy/events.ts +202 -16
  187. package/src/cli/convoy/expertise.test.ts +128 -0
  188. package/src/cli/convoy/expertise.ts +163 -0
  189. package/src/cli/convoy/export.test.ts +1 -0
  190. package/src/cli/convoy/formula.test.ts +405 -0
  191. package/src/cli/convoy/formula.ts +174 -0
  192. package/src/cli/convoy/gates.test.ts +1169 -0
  193. package/src/cli/convoy/gates.ts +774 -0
  194. package/src/cli/convoy/health.test.ts +64 -2
  195. package/src/cli/convoy/health.ts +80 -2
  196. package/src/cli/convoy/issues.test.ts +143 -0
  197. package/src/cli/convoy/issues.ts +136 -0
  198. package/src/cli/convoy/knowledge.test.ts +101 -0
  199. package/src/cli/convoy/knowledge.ts +132 -0
  200. package/src/cli/convoy/lessons.test.ts +188 -0
  201. package/src/cli/convoy/lessons.ts +164 -0
  202. package/src/cli/convoy/lock.test.ts +181 -0
  203. package/src/cli/convoy/lock.ts +103 -0
  204. package/src/cli/convoy/log-merge.test.ts +179 -0
  205. package/src/cli/convoy/merge.test.ts +6 -7
  206. package/src/cli/convoy/merge.ts +19 -1
  207. package/src/cli/convoy/partition.test.ts +423 -0
  208. package/src/cli/convoy/partition.ts +232 -0
  209. package/src/cli/convoy/pipeline.test.ts +6 -0
  210. package/src/cli/convoy/store.test.ts +2041 -20
  211. package/src/cli/convoy/store.ts +945 -46
  212. package/src/cli/convoy/types.ts +278 -4
  213. package/src/cli/log.ts +120 -2
  214. package/src/cli/run/adapters/claude.test.ts +234 -0
  215. package/src/cli/run/adapters/claude.ts +45 -5
  216. package/src/cli/run/adapters/copilot.test.ts +224 -0
  217. package/src/cli/run/adapters/copilot.ts +34 -4
  218. package/src/cli/run/adapters/cursor.test.ts +144 -0
  219. package/src/cli/run/adapters/cursor.ts +33 -2
  220. package/src/cli/run/adapters/opencode.test.ts +135 -0
  221. package/src/cli/run/adapters/opencode.ts +30 -2
  222. package/src/cli/run/executor.ts +1 -1
  223. package/src/cli/run/schema.test.ts +758 -0
  224. package/src/cli/run/schema.ts +300 -25
  225. package/src/cli/run.ts +341 -21
  226. package/src/cli/types.ts +86 -1
  227. package/src/cli/watch.ts +298 -0
  228. package/src/dashboard/dist/_astro/{index.DtnyD8a5.css → index.6L3_HsPT.css} +1 -1
  229. package/src/dashboard/dist/data/.gitkeep +0 -0
  230. package/src/dashboard/dist/data/convoy-list.json +1 -0
  231. package/src/dashboard/dist/data/overall-stats.json +24 -0
  232. package/src/dashboard/dist/index.html +701 -3
  233. package/src/dashboard/node_modules/.vite/deps/_metadata.json +6 -6
  234. package/src/dashboard/public/data/.gitkeep +0 -0
  235. package/src/dashboard/public/data/convoy-list.json +1 -0
  236. package/src/dashboard/public/data/overall-stats.json +24 -0
  237. package/src/dashboard/scripts/etl.test.ts +210 -0
  238. package/src/dashboard/scripts/etl.ts +108 -0
  239. package/src/dashboard/scripts/integration-test.ts +504 -0
  240. package/src/dashboard/src/pages/index.astro +854 -15
  241. package/src/dashboard/src/styles/dashboard.css +557 -1
  242. package/src/orchestrator/prompts/generate-convoy.prompt.md +212 -13
@@ -0,0 +1,144 @@
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
+ // ── CLI mode ──────────────────────────────────────────────────────────────────
42
+
43
+ describe('cursor adapter — MCP support', () => {
44
+ let tmpDir: string
45
+ let mockSpawn: ReturnType<typeof vi.fn>
46
+
47
+ beforeEach(() => {
48
+ vi.resetModules()
49
+ tmpDir = realpathSync(mkdtempSync(join(tmpdir(), 'cursor-test-')))
50
+
51
+ mockSpawn = vi.fn().mockImplementation((cmd: string) => {
52
+ if (cmd === 'which') return makeMockProc(0, '')
53
+ return makeMockProc(0, '{"result":"ok"}')
54
+ })
55
+ vi.doMock('node:child_process', () => ({ spawn: mockSpawn }))
56
+ })
57
+
58
+ afterEach(() => {
59
+ rmSync(tmpDir, { recursive: true, force: true })
60
+ vi.restoreAllMocks()
61
+ })
62
+
63
+ it('writes mcp.json to cwd with correct format when mcpServers provided', async () => {
64
+ let capturedContent: string | null = null
65
+ mockSpawn.mockImplementation((cmd: string) => {
66
+ if (cmd === 'which') return makeMockProc(0, '')
67
+ const mcpPath = join(tmpDir, 'mcp.json')
68
+ if (existsSync(mcpPath)) {
69
+ capturedContent = readFileSync(mcpPath, 'utf8')
70
+ }
71
+ return makeMockProc(0, '{}')
72
+ })
73
+
74
+ const { execute } = await import('./cursor.js')
75
+ const mcpServers = [{ name: 'my-mcp', type: 'local', command: 'node', args: ['server.js'] }]
76
+ await execute(makeTask(), { mcpServers, cwd: tmpDir })
77
+
78
+ expect(capturedContent).not.toBeNull()
79
+ expect(JSON.parse(capturedContent!)).toEqual({
80
+ mcpServers: { 'my-mcp': { command: 'node', args: ['server.js'] } },
81
+ })
82
+ })
83
+
84
+ it('passes --approve-mcps when mcp_approve_all is true', async () => {
85
+ const capturedArgs: string[] = []
86
+ mockSpawn.mockImplementation((cmd: string, args: string[]) => {
87
+ if (cmd === 'which') return makeMockProc(0, '')
88
+ capturedArgs.push(...args)
89
+ return makeMockProc(0, '{}')
90
+ })
91
+ const { execute } = await import('./cursor.js')
92
+ await execute(makeTask(), { mcp_approve_all: true, cwd: tmpDir })
93
+ expect(capturedArgs).toContain('--approve-mcps')
94
+ })
95
+
96
+ it('cleans up mcp.json after successful execution', async () => {
97
+ const { execute } = await import('./cursor.js')
98
+ const mcpServers = [{ name: 'my-mcp', type: 'local', command: 'node', args: ['server.js'] }]
99
+ await execute(makeTask(), { mcpServers, cwd: tmpDir })
100
+ expect(existsSync(join(tmpDir, 'mcp.json'))).toBe(false)
101
+ })
102
+
103
+ it('cleans up mcp.json after failed execution (non-zero exit)', async () => {
104
+ mockSpawn.mockImplementation((cmd: string) => {
105
+ if (cmd === 'which') return makeMockProc(0, '')
106
+ return makeMockProc(1, '')
107
+ })
108
+ const { execute } = await import('./cursor.js')
109
+ const mcpServers = [{ name: 'err-mcp', type: 'local', command: 'node', args: [] }]
110
+ await execute(makeTask(), { mcpServers, cwd: tmpDir })
111
+ expect(existsSync(join(tmpDir, 'mcp.json'))).toBe(false)
112
+ })
113
+
114
+ it('does NOT write mcp.json when mcpServers not configured', async () => {
115
+ const { execute } = await import('./cursor.js')
116
+ await execute(makeTask(), { cwd: tmpDir })
117
+ expect(existsSync(join(tmpDir, 'mcp.json'))).toBe(false)
118
+ })
119
+
120
+ it('does NOT add --approve-mcps when mcp_approve_all is not set', async () => {
121
+ const capturedArgs: string[] = []
122
+ mockSpawn.mockImplementation((cmd: string, args: string[]) => {
123
+ if (cmd === 'which') return makeMockProc(0, '')
124
+ capturedArgs.push(...args)
125
+ return makeMockProc(0, '{}')
126
+ })
127
+ const { execute } = await import('./cursor.js')
128
+ await execute(makeTask(), { cwd: tmpDir })
129
+ expect(capturedArgs).not.toContain('--approve-mcps')
130
+ })
131
+
132
+ it('includes --approve-mcps when mcp_approve_all is true (no mcpServers)', async () => {
133
+ const capturedArgs: string[] = []
134
+ mockSpawn.mockImplementation((cmd: string, args: string[]) => {
135
+ if (cmd === 'which') return makeMockProc(0, '')
136
+ capturedArgs.push(...args)
137
+ return makeMockProc(0, '{}')
138
+ })
139
+ const { execute } = await import('./cursor.js')
140
+ await execute(makeTask(), { mcp_approve_all: true, cwd: tmpDir })
141
+ expect(capturedArgs).toContain('--approve-mcps')
142
+ expect(existsSync(join(tmpDir, 'mcp.json'))).toBe(false)
143
+ })
144
+ })
@@ -1,9 +1,12 @@
1
1
  import { spawn } from 'node:child_process'
2
+ import { writeFileSync, unlinkSync } from 'node:fs'
3
+ import { join } from 'node:path'
2
4
  import type { Task, ExecuteOptions, ExecuteResult, TokenUsage } from '../../types.js'
3
5
 
4
6
  /** Adapter name */
5
7
  export const name = 'cursor'
6
8
 
9
+ export function supportsSessionContinuity(): boolean { return false }
7
10
  /**
8
11
  * Check if the Cursor CLI (`agent`) is available on the system PATH.
9
12
  */
@@ -33,11 +36,34 @@ export async function execute(task: Task, options: ExecuteOptions = {}): Promise
33
36
  'json',
34
37
  ]
35
38
 
36
- return new Promise((resolve) => {
39
+ const cwd = options?.cwd ?? process.cwd()
40
+ const mcpJsonPath = join(cwd, 'mcp.json')
41
+ let wroteJson = false
42
+
43
+ if (options.mcpServers?.length) {
44
+ const mcpJson: Record<string, Record<string, unknown>> = {}
45
+ for (const server of options.mcpServers) {
46
+ const entry: Record<string, unknown> = {}
47
+ if (server.command) entry.command = server.command
48
+ if (server.args) entry.args = server.args
49
+ if (server.url) entry.url = server.url
50
+ if (server.config) Object.assign(entry, server.config)
51
+ mcpJson[server.name] = entry
52
+ }
53
+ writeFileSync(mcpJsonPath, JSON.stringify({ mcpServers: mcpJson }, null, 2), 'utf8')
54
+ wroteJson = true
55
+ }
56
+
57
+ if (options.mcp_approve_all) {
58
+ args.push('--approve-mcps')
59
+ }
60
+
61
+ try {
62
+ return await new Promise<ExecuteResult>((resolve) => {
37
63
  const proc = spawn('agent', args, {
38
64
  stdio: ['ignore', 'pipe', 'pipe'],
39
65
  env: { ...process.env },
40
- cwd: options?.cwd ?? process.cwd(),
66
+ cwd,
41
67
  })
42
68
 
43
69
  let stdout = ''
@@ -89,6 +115,11 @@ export async function execute(task: Task, options: ExecuteOptions = {}): Promise
89
115
  // Store process ref for potential timeout kill
90
116
  task._process = proc
91
117
  })
118
+ } finally {
119
+ if (wroteJson) {
120
+ try { unlinkSync(mcpJsonPath) } catch { /* ignore */ }
121
+ }
122
+ }
92
123
  }
93
124
 
94
125
  /**
@@ -0,0 +1,135 @@
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
+ // ── CLI mode ──────────────────────────────────────────────────────────────────
42
+
43
+ describe('opencode adapter — MCP support', () => {
44
+ let tmpDir: string
45
+ let mockSpawn: ReturnType<typeof vi.fn>
46
+
47
+ beforeEach(() => {
48
+ vi.resetModules()
49
+ tmpDir = realpathSync(mkdtempSync(join(tmpdir(), 'opencode-test-')))
50
+
51
+ mockSpawn = vi.fn().mockImplementation((cmd: string) => {
52
+ if (cmd === 'which') return makeMockProc(0, '')
53
+ return makeMockProc(0, '{"result":"ok"}')
54
+ })
55
+ vi.doMock('node:child_process', () => ({ spawn: mockSpawn }))
56
+ })
57
+
58
+ afterEach(() => {
59
+ rmSync(tmpDir, { recursive: true, force: true })
60
+ vi.restoreAllMocks()
61
+ })
62
+
63
+ it('writes mcp.json to cwd with correct format when mcpServers provided', async () => {
64
+ let capturedContent: string | null = null
65
+ mockSpawn.mockImplementation((cmd: string) => {
66
+ if (cmd === 'which') return makeMockProc(0, '')
67
+ const mcpPath = join(tmpDir, 'mcp.json')
68
+ if (existsSync(mcpPath)) {
69
+ capturedContent = readFileSync(mcpPath, 'utf8')
70
+ }
71
+ return makeMockProc(0, '{}')
72
+ })
73
+
74
+ const { execute } = await import('./opencode.js')
75
+ const mcpServers = [{ name: 'my-mcp', type: 'local', command: 'node', args: ['server.js'] }]
76
+ await execute(makeTask(), { mcpServers, cwd: tmpDir })
77
+
78
+ expect(capturedContent).not.toBeNull()
79
+ expect(JSON.parse(capturedContent!)).toEqual({
80
+ mcpServers: { 'my-mcp': { command: 'node', args: ['server.js'] } },
81
+ })
82
+ })
83
+
84
+ it('passes --mcp-config flag pointing to mcp.json path', async () => {
85
+ const capturedArgs: string[] = []
86
+ mockSpawn.mockImplementation((cmd: string, args: string[]) => {
87
+ if (cmd === 'which') return makeMockProc(0, '')
88
+ capturedArgs.push(...args)
89
+ return makeMockProc(0, '{}')
90
+ })
91
+ const { execute } = await import('./opencode.js')
92
+ const mcpServers = [{ name: 'my-mcp', type: 'local', command: 'node', args: ['server.js'] }]
93
+ await execute(makeTask(), { mcpServers, cwd: tmpDir })
94
+
95
+ const idx = capturedArgs.indexOf('--mcp-config')
96
+ expect(idx).toBeGreaterThanOrEqual(0)
97
+ expect(capturedArgs[idx + 1]).toBe(join(tmpDir, 'mcp.json'))
98
+ })
99
+
100
+ it('cleans up mcp.json after successful execution', async () => {
101
+ const { execute } = await import('./opencode.js')
102
+ const mcpServers = [{ name: 'my-mcp', type: 'local', command: 'node', args: ['server.js'] }]
103
+ await execute(makeTask(), { mcpServers, cwd: tmpDir })
104
+ expect(existsSync(join(tmpDir, 'mcp.json'))).toBe(false)
105
+ })
106
+
107
+ it('cleans up mcp.json after failed execution (non-zero exit)', async () => {
108
+ mockSpawn.mockImplementation((cmd: string) => {
109
+ if (cmd === 'which') return makeMockProc(0, '')
110
+ return makeMockProc(1, '')
111
+ })
112
+ const { execute } = await import('./opencode.js')
113
+ const mcpServers = [{ name: 'err-mcp', type: 'local', command: 'node', args: [] }]
114
+ await execute(makeTask(), { mcpServers, cwd: tmpDir })
115
+ expect(existsSync(join(tmpDir, 'mcp.json'))).toBe(false)
116
+ })
117
+
118
+ it('does NOT write mcp.json when mcpServers not configured', async () => {
119
+ const { execute } = await import('./opencode.js')
120
+ await execute(makeTask(), { cwd: tmpDir })
121
+ expect(existsSync(join(tmpDir, 'mcp.json'))).toBe(false)
122
+ })
123
+
124
+ it('does NOT add --mcp-config when mcpServers not provided', async () => {
125
+ const capturedArgs: string[] = []
126
+ mockSpawn.mockImplementation((cmd: string, args: string[]) => {
127
+ if (cmd === 'which') return makeMockProc(0, '')
128
+ capturedArgs.push(...args)
129
+ return makeMockProc(0, '{}')
130
+ })
131
+ const { execute } = await import('./opencode.js')
132
+ await execute(makeTask(), { cwd: tmpDir })
133
+ expect(capturedArgs).not.toContain('--mcp-config')
134
+ })
135
+ })
@@ -1,9 +1,12 @@
1
1
  import { spawn } from 'node:child_process'
2
+ import { writeFileSync, unlinkSync } from 'node:fs'
3
+ import { join } from 'node:path'
2
4
  import type { Task, ExecuteOptions, ExecuteResult, TokenUsage } from '../../types.js'
3
5
 
4
6
  /** Adapter name */
5
7
  export const name = 'opencode'
6
8
 
9
+ export function supportsSessionContinuity(): boolean { return false }
7
10
  /**
8
11
  * Check if the `opencode` CLI is available on the system PATH.
9
12
  */
@@ -27,11 +30,31 @@ export async function execute(task: Task, options: ExecuteOptions = {}): Promise
27
30
 
28
31
  const args = ['--headless', '-p', prompt]
29
32
 
30
- return new Promise((resolve) => {
33
+ const cwd = options?.cwd ?? process.cwd()
34
+ const mcpJsonPath = join(cwd, 'mcp.json')
35
+ let wroteJson = false
36
+
37
+ if (options.mcpServers?.length) {
38
+ const mcpJson: Record<string, Record<string, unknown>> = {}
39
+ for (const server of options.mcpServers) {
40
+ const entry: Record<string, unknown> = {}
41
+ if (server.command) entry.command = server.command
42
+ if (server.args) entry.args = server.args
43
+ if (server.url) entry.url = server.url
44
+ if (server.config) Object.assign(entry, server.config)
45
+ mcpJson[server.name] = entry
46
+ }
47
+ writeFileSync(mcpJsonPath, JSON.stringify({ mcpServers: mcpJson }, null, 2), 'utf8')
48
+ args.push('--mcp-config', mcpJsonPath)
49
+ wroteJson = true
50
+ }
51
+
52
+ try {
53
+ return await new Promise<ExecuteResult>((resolve) => {
31
54
  const proc = spawn('opencode', args, {
32
55
  stdio: ['ignore', 'pipe', 'pipe'],
33
56
  env: { ...process.env },
34
- cwd: options?.cwd ?? process.cwd(),
57
+ cwd,
35
58
  })
36
59
 
37
60
  let stdout = ''
@@ -83,6 +106,11 @@ export async function execute(task: Task, options: ExecuteOptions = {}): Promise
83
106
  // Store process ref for potential timeout kill
84
107
  task._process = proc
85
108
  })
109
+ } finally {
110
+ if (wroteJson) {
111
+ try { unlinkSync(mcpJsonPath) } catch { /* ignore */ }
112
+ }
113
+ }
86
114
  }
87
115
 
88
116
  /**
@@ -190,7 +190,7 @@ export function createExecutor(
190
190
  reporter.onPhaseStart(phaseIdx + 1, eligible)
191
191
 
192
192
  // Process eligible tasks in batches limited by concurrency
193
- const concurrency = spec.concurrency
193
+ const concurrency = spec.concurrency === 'auto' ? eligible.length : spec.concurrency
194
194
  for (let i = 0; i < eligible.length; i += concurrency) {
195
195
  if (halted) break
196
196
  const batch = eligible.slice(i, i + concurrency)