@vibe-forge/mcp 3.1.1 → 3.2.0

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.
@@ -1,378 +0,0 @@
1
- import { beforeEach, describe, expect, it, vi } from 'vitest'
2
-
3
- import { createToolTester } from './mcp-test-utils.js'
4
-
5
- const mocks = vi.hoisted(() => {
6
- return {
7
- callHook: vi.fn(),
8
- createChildSession: vi.fn(),
9
- getParentSessionId: vi.fn(),
10
- startTask: vi.fn(),
11
- sendTaskMessage: vi.fn(),
12
- submitTaskInput: vi.fn(),
13
- respondToTaskInteraction: vi.fn(),
14
- getTask: vi.fn(),
15
- stopTask: vi.fn(),
16
- getAllTasks: vi.fn(),
17
- uuid: vi.fn()
18
- }
19
- })
20
-
21
- vi.mock('@vibe-forge/hooks', () => ({
22
- callHook: mocks.callHook
23
- }))
24
-
25
- vi.mock('@vibe-forge/utils/uuid', () => ({
26
- uuid: mocks.uuid
27
- }))
28
-
29
- vi.mock('#~/sync.js', () => ({
30
- createChildSession: mocks.createChildSession,
31
- getParentSessionId: mocks.getParentSessionId
32
- }))
33
-
34
- vi.mock('#~/tools/task/manager.js', () => ({
35
- TaskManager: class {
36
- startTask = mocks.startTask
37
- sendTaskMessage = mocks.sendTaskMessage
38
- submitTaskInput = mocks.submitTaskInput
39
- respondToTaskInteraction = mocks.respondToTaskInteraction
40
- getTask = mocks.getTask
41
- stopTask = mocks.stopTask
42
- getAllTasks = mocks.getAllTasks
43
- }
44
- }))
45
-
46
- describe('task tool integration', () => {
47
- beforeEach(() => {
48
- vi.clearAllMocks()
49
- process.env.__VF_PROJECT_AI_SESSION_ID__ = 'sess-1'
50
- delete process.env.__VF_PROJECT_AI_PERMISSION_MODE__
51
- let nextTaskId = 1
52
- mocks.uuid.mockImplementation(() => `task-${nextTaskId++}`)
53
- mocks.callHook.mockResolvedValue({ continue: true })
54
- mocks.getParentSessionId.mockReturnValue(undefined)
55
- mocks.createChildSession.mockResolvedValue({})
56
- mocks.startTask.mockResolvedValue(undefined)
57
- mocks.sendTaskMessage.mockResolvedValue(undefined)
58
- mocks.submitTaskInput.mockResolvedValue(undefined)
59
- mocks.respondToTaskInteraction.mockResolvedValue(undefined)
60
- mocks.getTask.mockImplementation((taskId: string) => ({
61
- taskId,
62
- status: 'completed',
63
- logs: []
64
- }))
65
- mocks.stopTask.mockReturnValue(true)
66
- mocks.getAllTasks.mockReturnValue([])
67
- })
68
-
69
- it('passes resolved task ids to the StartTasks hook', async () => {
70
- const { createTaskRegister } = await import('#~/tools/task/index.js')
71
-
72
- const tester = createToolTester()
73
- createTaskRegister()(tester.mockRegister)
74
-
75
- await tester.callTool('StartTasks', {
76
- tasks: [{
77
- description: 'only output ok',
78
- type: 'default',
79
- background: false
80
- }]
81
- })
82
-
83
- expect(mocks.callHook).toHaveBeenCalledWith(
84
- 'StartTasks',
85
- expect.objectContaining({
86
- sessionId: 'sess-1',
87
- tasks: [expect.objectContaining({
88
- taskId: 'task-1',
89
- description: 'only output ok',
90
- type: 'default',
91
- background: false
92
- })]
93
- })
94
- )
95
- expect(mocks.startTask).toHaveBeenCalledWith(expect.objectContaining({
96
- taskId: 'task-1'
97
- }))
98
- })
99
-
100
- it('accepts workspace tasks without a separate workspace tool', async () => {
101
- const { createTaskRegister } = await import('#~/tools/task/index.js')
102
-
103
- const tester = createToolTester()
104
- createTaskRegister()(tester.mockRegister)
105
-
106
- await tester.callTool('StartTasks', {
107
- tasks: [{
108
- description: 'fix billing',
109
- type: 'workspace',
110
- name: 'billing'
111
- }]
112
- })
113
-
114
- expect(mocks.startTask).toHaveBeenCalledWith(expect.objectContaining({
115
- taskId: 'task-1',
116
- description: 'fix billing',
117
- type: 'workspace',
118
- name: 'billing'
119
- }))
120
- })
121
-
122
- it('passes explicit task model overrides to the hook and task manager', async () => {
123
- const { createTaskRegister } = await import('#~/tools/task/index.js')
124
-
125
- const tester = createToolTester()
126
- createTaskRegister()(tester.mockRegister)
127
-
128
- await tester.callTool('StartTasks', {
129
- tasks: [{
130
- description: 'investigate flaky tests',
131
- type: 'default',
132
- model: 'gpt-5.4-mini'
133
- }]
134
- })
135
-
136
- expect(mocks.callHook).toHaveBeenCalledWith(
137
- 'StartTasks',
138
- expect.objectContaining({
139
- tasks: [expect.objectContaining({
140
- taskId: 'task-1',
141
- model: 'gpt-5.4-mini'
142
- })]
143
- })
144
- )
145
- expect(mocks.startTask).toHaveBeenCalledWith(expect.objectContaining({
146
- taskId: 'task-1',
147
- model: 'gpt-5.4-mini'
148
- }))
149
- })
150
-
151
- it('inherits the parent permission mode when the task does not specify one', async () => {
152
- process.env.__VF_PROJECT_AI_PERMISSION_MODE__ = 'dontAsk'
153
- mocks.getParentSessionId.mockReturnValue('parent-session')
154
-
155
- const { createTaskRegister } = await import('#~/tools/task/index.js')
156
-
157
- const tester = createToolTester()
158
- createTaskRegister()(tester.mockRegister)
159
-
160
- await tester.callTool('StartTasks', {
161
- tasks: [{
162
- description: 'inherit permissions',
163
- type: 'default'
164
- }]
165
- })
166
-
167
- expect(mocks.callHook).toHaveBeenCalledWith(
168
- 'StartTasks',
169
- expect.objectContaining({
170
- tasks: [expect.objectContaining({
171
- taskId: 'task-1',
172
- permissionMode: 'dontAsk'
173
- })]
174
- })
175
- )
176
- expect(mocks.createChildSession).toHaveBeenCalledWith(expect.objectContaining({
177
- id: 'task-1',
178
- parentSessionId: 'parent-session',
179
- permissionMode: 'dontAsk'
180
- }))
181
- expect(mocks.startTask).toHaveBeenCalledWith(expect.objectContaining({
182
- taskId: 'task-1',
183
- permissionMode: 'dontAsk',
184
- enableServerSync: true
185
- }))
186
- })
187
-
188
- it('keeps an explicit task permission mode over the inherited parent mode', async () => {
189
- process.env.__VF_PROJECT_AI_PERMISSION_MODE__ = 'dontAsk'
190
-
191
- const { createTaskRegister } = await import('#~/tools/task/index.js')
192
-
193
- const tester = createToolTester()
194
- createTaskRegister()(tester.mockRegister)
195
-
196
- await tester.callTool('StartTasks', {
197
- tasks: [{
198
- description: 'override permissions',
199
- type: 'default',
200
- permissionMode: 'plan'
201
- }]
202
- })
203
-
204
- expect(mocks.startTask).toHaveBeenCalledWith(expect.objectContaining({
205
- taskId: 'task-1',
206
- permissionMode: 'plan'
207
- }))
208
- })
209
-
210
- it('registers recovery guidance in task tool descriptions', async () => {
211
- const { createTaskRegister } = await import('#~/tools/task/index.js')
212
-
213
- const tester = createToolTester()
214
- createTaskRegister()(tester.mockRegister)
215
-
216
- expect(tester.getRegisteredTools()).toContain('SendTaskMessage')
217
- expect(tester.getRegisteredTools()).toContain('SubmitTaskInput')
218
- expect(tester.getRegisteredTools()).toContain('RespondTaskInteraction')
219
- expect(tester.getToolInfo('StartTasks')?.description).toContain('GetTaskInfo')
220
- expect(tester.getToolInfo('StartTasks')?.description).toContain('SendTaskMessage')
221
- expect(tester.getToolInfo('GetTaskInfo')?.description).toContain('10 most recent logs')
222
- expect(tester.getToolInfo('GetTaskInfo')?.description).toContain('logOrder')
223
- expect(tester.getToolInfo('GetTaskInfo')?.description).toContain('SendTaskMessage')
224
- expect(tester.getToolInfo('SendTaskMessage')?.description).toContain('mode "direct"')
225
- expect(tester.getToolInfo('SendTaskMessage')?.description).toContain('mode "steer"')
226
- expect(tester.getToolInfo('ListTasks')?.description).toContain('10 most recent logs')
227
- expect(tester.getToolInfo('ListTasks')?.description).toContain('SendTaskMessage')
228
- expect(tester.getToolInfo('ListTasks')?.description).toContain('pendingInput')
229
- expect(tester.getToolInfo('SubmitTaskInput')?.description).toContain('SendTaskMessage')
230
- expect(tester.getToolInfo('SubmitTaskInput')?.description).toContain('allow_once')
231
- expect(tester.getToolInfo('RespondTaskInteraction')?.description).toContain('Deprecated alias')
232
- })
233
-
234
- it('returns the 10 most recent logs in descending order by default', async () => {
235
- mocks.getTask.mockReturnValue({
236
- taskId: 'task-1',
237
- status: 'running',
238
- logs: Array.from({ length: 12 }, (_, index) => `log-${index + 1}`)
239
- })
240
-
241
- const { createTaskRegister } = await import('#~/tools/task/index.js')
242
-
243
- const tester = createToolTester()
244
- createTaskRegister()(tester.mockRegister)
245
-
246
- const result = await tester.callTool('GetTaskInfo', {
247
- taskId: 'task-1'
248
- }) as { content: Array<{ text: string }> }
249
- const [task] = JSON.parse(result.content[0].text) as Array<{ logs: string[] }>
250
-
251
- expect(task.logs).toEqual([
252
- 'log-12',
253
- 'log-11',
254
- 'log-10',
255
- 'log-9',
256
- 'log-8',
257
- 'log-7',
258
- 'log-6',
259
- 'log-5',
260
- 'log-4',
261
- 'log-3'
262
- ])
263
- })
264
-
265
- it('supports custom log windows and ascending order in ListTasks', async () => {
266
- mocks.getAllTasks.mockReturnValue([
267
- {
268
- taskId: 'task-1',
269
- status: 'running',
270
- logs: ['a', 'b', 'c', 'd']
271
- },
272
- {
273
- taskId: 'task-2',
274
- status: 'completed',
275
- logs: ['1', '2', '3']
276
- }
277
- ])
278
-
279
- const { createTaskRegister } = await import('#~/tools/task/index.js')
280
-
281
- const tester = createToolTester()
282
- createTaskRegister()(tester.mockRegister)
283
-
284
- const result = await tester.callTool('ListTasks', {
285
- logLimit: 2,
286
- logOrder: 'asc'
287
- }) as { content: Array<{ text: string }> }
288
- const tasks = JSON.parse(result.content[0].text) as Array<{ taskId: string; logs: string[] }>
289
-
290
- expect(tasks).toEqual([
291
- {
292
- taskId: 'task-1',
293
- status: 'running',
294
- logs: ['c', 'd'],
295
- guidance: []
296
- },
297
- {
298
- taskId: 'task-2',
299
- status: 'completed',
300
- logs: ['2', '3'],
301
- guidance: []
302
- }
303
- ])
304
- })
305
-
306
- it('forwards SubmitTaskInput to the task manager', async () => {
307
- mocks.getTask.mockReturnValue({
308
- taskId: 'task-1',
309
- status: 'running',
310
- logs: ['Interaction response submitted: allow_once']
311
- })
312
-
313
- const { createTaskRegister } = await import('#~/tools/task/index.js')
314
-
315
- const tester = createToolTester()
316
- createTaskRegister()(tester.mockRegister)
317
-
318
- await tester.callTool('SubmitTaskInput', {
319
- taskId: 'task-1',
320
- data: 'allow_once'
321
- })
322
-
323
- expect(mocks.submitTaskInput).toHaveBeenCalledWith({
324
- taskId: 'task-1',
325
- interactionId: undefined,
326
- data: 'allow_once'
327
- })
328
- })
329
-
330
- it('forwards SendTaskMessage to the task manager', async () => {
331
- mocks.getTask.mockReturnValue({
332
- taskId: 'task-1',
333
- status: 'running',
334
- logs: ['Queued task message (steer): keep checking logs']
335
- })
336
-
337
- const { createTaskRegister } = await import('#~/tools/task/index.js')
338
-
339
- const tester = createToolTester()
340
- createTaskRegister()(tester.mockRegister)
341
-
342
- await tester.callTool('SendTaskMessage', {
343
- taskId: 'task-1',
344
- message: 'keep checking logs',
345
- mode: 'steer'
346
- })
347
-
348
- expect(mocks.sendTaskMessage).toHaveBeenCalledWith({
349
- taskId: 'task-1',
350
- message: 'keep checking logs',
351
- mode: 'steer'
352
- })
353
- })
354
-
355
- it('keeps RespondTaskInteraction as a deprecated alias', async () => {
356
- mocks.getTask.mockReturnValue({
357
- taskId: 'task-1',
358
- status: 'running',
359
- logs: ['Interaction response submitted: allow_once']
360
- })
361
-
362
- const { createTaskRegister } = await import('#~/tools/task/index.js')
363
-
364
- const tester = createToolTester()
365
- createTaskRegister()(tester.mockRegister)
366
-
367
- await tester.callTool('RespondTaskInteraction', {
368
- taskId: 'task-1',
369
- response: 'allow_once'
370
- })
371
-
372
- expect(mocks.submitTaskInput).toHaveBeenCalledWith({
373
- taskId: 'task-1',
374
- interactionId: undefined,
375
- data: 'allow_once'
376
- })
377
- })
378
- })
package/src/sync.ts DELETED
@@ -1,70 +0,0 @@
1
- import process from 'node:process'
2
-
3
- import type { SessionPermissionMode, WSEvent } from '@vibe-forge/types'
4
- import { extractTextFromMessage } from '@vibe-forge/utils/chat-message'
5
-
6
- const getServerBaseUrl = () => {
7
- const host = process.env.__VF_PROJECT_AI_SERVER_HOST__ ?? 'localhost'
8
- const port = process.env.__VF_PROJECT_AI_SERVER_PORT__ ?? '8787'
9
- return `http://${host}:${port}`
10
- }
11
-
12
- export const getParentSessionId = () => {
13
- const sessionId = process.env.__VF_PROJECT_AI_SESSION_ID__
14
- if (sessionId != null && sessionId !== '') {
15
- return sessionId
16
- }
17
- const ctxId = process.env.__VF_PROJECT_AI_CTX_ID__
18
- return ctxId ?? undefined
19
- }
20
-
21
- export const createChildSession = async (params: {
22
- id: string
23
- title?: string
24
- parentSessionId?: string
25
- permissionMode?: SessionPermissionMode
26
- }) => {
27
- const baseUrl = getServerBaseUrl()
28
- const response = await fetch(`${baseUrl}/api/sessions`, {
29
- method: 'POST',
30
- headers: { 'Content-Type': 'application/json' },
31
- body: JSON.stringify({
32
- id: params.id,
33
- title: params.title,
34
- parentSessionId: params.parentSessionId,
35
- permissionMode: params.permissionMode,
36
- start: false
37
- })
38
- })
39
- if (!response.ok) {
40
- const errorText = await response.text()
41
- throw new Error(`Failed to create session: ${response.statusText} - ${errorText}`)
42
- }
43
- return response.json()
44
- }
45
-
46
- export const postSessionEvent = async (sessionId: string, payload: Record<string, unknown>) => {
47
- const baseUrl = getServerBaseUrl()
48
- const response = await fetch(`${baseUrl}/api/sessions/${sessionId}/events`, {
49
- method: 'POST',
50
- headers: { 'Content-Type': 'application/json' },
51
- body: JSON.stringify(payload)
52
- })
53
- if (!response.ok) {
54
- const errorText = await response.text()
55
- throw new Error(`Failed to post session event: ${response.statusText} - ${errorText}`)
56
- }
57
- }
58
-
59
- export const fetchSessionMessages = async (sessionId: string) => {
60
- const baseUrl = getServerBaseUrl()
61
- const response = await fetch(`${baseUrl}/api/sessions/${sessionId}/messages`)
62
- if (!response.ok) {
63
- const errorText = await response.text()
64
- throw new Error(`Failed to fetch session messages: ${response.statusText} - ${errorText}`)
65
- }
66
- const data = await response.json() as { messages: WSEvent[] }
67
- return data.messages ?? []
68
- }
69
-
70
- export { extractTextFromMessage }
@@ -1,126 +0,0 @@
1
- import process from 'node:process'
2
-
3
- import { callHook } from '@vibe-forge/hooks'
4
- import { uuid } from '@vibe-forge/utils/uuid'
5
- import { z } from 'zod'
6
-
7
- import { createChildSession, getParentSessionId } from '#~/sync.js'
8
- import type { McpManagedTaskInput } from '../../types'
9
- import { defineRegister } from '../types'
10
- import { TaskManager } from './manager'
11
- import {
12
- SESSION_PERMISSION_MODES,
13
- START_TASKS_DESCRIPTION,
14
- TASK_BACKGROUND_DESCRIPTION,
15
- TASK_MODEL_DESCRIPTION,
16
- TASK_PERMISSION_MODE_DESCRIPTION,
17
- resolveInheritedPermissionMode,
18
- serializeTaskInfo
19
- } from './presentation'
20
- import { registerTaskRuntimeTools } from './register-task-runtime-tools'
21
-
22
- export const createTaskRegister = () => {
23
- const taskManager = new TaskManager()
24
-
25
- return defineRegister((server) => {
26
- server.registerTool(
27
- 'StartTasks',
28
- {
29
- title: 'Start Tasks',
30
- description: START_TASKS_DESCRIPTION,
31
- inputSchema: z.object({
32
- tasks: z
33
- .array(
34
- z.object({
35
- description: z
36
- .string()
37
- .describe('The description or prompt for the task'),
38
- type: z
39
- .enum([
40
- 'default',
41
- 'spec',
42
- 'entity',
43
- 'workspace'
44
- ])
45
- .describe('The type of definition to load (default, spec, entity or workspace)'),
46
- name: z
47
- .string()
48
- .describe('The name of the spec or entity to load, if type is spec or entity. Otherwise, ignored.')
49
- .optional(),
50
- adapter: z
51
- .string()
52
- .describe('The adapter to use for the task (e.g. claude-code)')
53
- .optional(),
54
- model: z
55
- .string()
56
- .describe(TASK_MODEL_DESCRIPTION)
57
- .optional(),
58
- permissionMode: z
59
- .enum(SESSION_PERMISSION_MODES)
60
- .describe(TASK_PERMISSION_MODE_DESCRIPTION)
61
- .optional(),
62
- background: z
63
- .boolean()
64
- .describe(TASK_BACKGROUND_DESCRIPTION)
65
- .optional()
66
- })
67
- )
68
- .describe('List of tasks to start')
69
- })
70
- },
71
- async ({ tasks }) => {
72
- const inheritedPermissionMode = resolveInheritedPermissionMode()
73
- const resolvedTasks = tasks.map((task): McpManagedTaskInput & {
74
- taskId: string
75
- type: NonNullable<McpManagedTaskInput['type']>
76
- } => ({
77
- ...task,
78
- permissionMode: task.permissionMode ?? inheritedPermissionMode,
79
- type: task.type ?? 'default',
80
- taskId: uuid()
81
- }))
82
- const parentSessionId = getParentSessionId()
83
-
84
- await callHook('StartTasks', {
85
- cwd: process.cwd(),
86
- sessionId: process.env.__VF_PROJECT_AI_SESSION_ID__!,
87
- tasks: resolvedTasks
88
- })
89
- const syncResults = parentSessionId
90
- ? await Promise.allSettled(resolvedTasks.map(task =>
91
- createChildSession({
92
- id: task.taskId,
93
- title: task.name ?? task.description,
94
- parentSessionId,
95
- permissionMode: task.permissionMode
96
- })
97
- ))
98
- : []
99
- const results = await Promise.allSettled(resolvedTasks
100
- .map((task, idx) =>
101
- taskManager.startTask({
102
- ...task,
103
- enableServerSync: parentSessionId != null && syncResults[idx]?.status === 'fulfilled'
104
- })
105
- ))
106
-
107
- return {
108
- content: [{
109
- type: 'text',
110
- text: JSON.stringify(results.map((r, idx) => {
111
- const { taskId, description } = resolvedTasks[idx]
112
- return serializeTaskInfo({
113
- taskId,
114
- description,
115
- status: r.status === 'rejected' ? 'failed' : 'running',
116
- info: taskManager.getTask(taskId)
117
- })
118
- }))
119
- }]
120
- }
121
- }
122
- )
123
-
124
- registerTaskRuntimeTools(server, taskManager)
125
- })
126
- }