@vibe-forge/mcp 0.11.0 → 0.11.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/__tests__/sync.spec.ts +23 -0
- package/__tests__/task-manager.spec.ts +319 -1
- package/__tests__/task-tool.spec.ts +130 -0
- package/package.json +6 -6
- package/src/sync.ts +7 -1
- package/src/tools/task/index.ts +22 -78
- package/src/tools/task/manager.ts +493 -69
- package/src/tools/task/permission-recovery.ts +172 -0
- package/src/tools/task/permission-state.ts +200 -0
- package/src/tools/task/presentation.ts +95 -0
- package/src/tools/task/register-task-runtime-tools.ts +145 -0
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { afterEach, describe, expect, it } from 'vitest'
|
|
2
|
+
|
|
3
|
+
import { getParentSessionId } from '#~/sync.js'
|
|
4
|
+
|
|
5
|
+
describe('mcp sync helpers', () => {
|
|
6
|
+
afterEach(() => {
|
|
7
|
+
delete process.env.__VF_PROJECT_AI_SESSION_ID__
|
|
8
|
+
delete process.env.__VF_PROJECT_AI_CTX_ID__
|
|
9
|
+
})
|
|
10
|
+
|
|
11
|
+
it('prefers the current session id when resolving the parent session id', () => {
|
|
12
|
+
process.env.__VF_PROJECT_AI_SESSION_ID__ = 'session-parent'
|
|
13
|
+
process.env.__VF_PROJECT_AI_CTX_ID__ = 'ctx-parent'
|
|
14
|
+
|
|
15
|
+
expect(getParentSessionId()).toBe('session-parent')
|
|
16
|
+
})
|
|
17
|
+
|
|
18
|
+
it('falls back to ctx id when the session id is missing', () => {
|
|
19
|
+
process.env.__VF_PROJECT_AI_CTX_ID__ = 'ctx-parent'
|
|
20
|
+
|
|
21
|
+
expect(getParentSessionId()).toBe('ctx-parent')
|
|
22
|
+
})
|
|
23
|
+
})
|
|
@@ -4,11 +4,16 @@ const mocks = vi.hoisted(() => ({
|
|
|
4
4
|
run: vi.fn(),
|
|
5
5
|
generateAdapterQueryOptions: vi.fn(),
|
|
6
6
|
callHook: vi.fn(),
|
|
7
|
+
buildConfigJsonVariables: vi.fn(),
|
|
8
|
+
loadConfig: vi.fn(),
|
|
9
|
+
updateConfigFile: vi.fn(),
|
|
7
10
|
loadInjectDefaultSystemPromptValue: vi.fn(),
|
|
8
11
|
mergeSystemPrompts: vi.fn(),
|
|
9
12
|
extractTextFromMessage: vi.fn(),
|
|
10
13
|
postSessionEvent: vi.fn(),
|
|
11
|
-
fetchSessionMessages: vi.fn()
|
|
14
|
+
fetchSessionMessages: vi.fn(),
|
|
15
|
+
mkdir: vi.fn(),
|
|
16
|
+
writeFile: vi.fn()
|
|
12
17
|
}))
|
|
13
18
|
|
|
14
19
|
vi.mock('@vibe-forge/task', () => ({
|
|
@@ -21,6 +26,9 @@ vi.mock('@vibe-forge/hooks', () => ({
|
|
|
21
26
|
}))
|
|
22
27
|
|
|
23
28
|
vi.mock('@vibe-forge/config', () => ({
|
|
29
|
+
buildConfigJsonVariables: mocks.buildConfigJsonVariables,
|
|
30
|
+
loadConfig: mocks.loadConfig,
|
|
31
|
+
updateConfigFile: mocks.updateConfigFile,
|
|
24
32
|
loadInjectDefaultSystemPromptValue: mocks.loadInjectDefaultSystemPromptValue,
|
|
25
33
|
mergeSystemPrompts: mocks.mergeSystemPrompts
|
|
26
34
|
}))
|
|
@@ -29,6 +37,11 @@ vi.mock('@vibe-forge/utils/chat-message', () => ({
|
|
|
29
37
|
extractTextFromMessage: mocks.extractTextFromMessage
|
|
30
38
|
}))
|
|
31
39
|
|
|
40
|
+
vi.mock('node:fs/promises', () => ({
|
|
41
|
+
mkdir: mocks.mkdir,
|
|
42
|
+
writeFile: mocks.writeFile
|
|
43
|
+
}))
|
|
44
|
+
|
|
32
45
|
vi.mock('#~/sync.js', () => ({
|
|
33
46
|
postSessionEvent: mocks.postSessionEvent,
|
|
34
47
|
fetchSessionMessages: mocks.fetchSessionMessages
|
|
@@ -46,12 +59,17 @@ describe('taskManager fatal error scenarios', () => {
|
|
|
46
59
|
mcpServers: undefined
|
|
47
60
|
}
|
|
48
61
|
])
|
|
62
|
+
mocks.buildConfigJsonVariables.mockReturnValue({})
|
|
63
|
+
mocks.loadConfig.mockResolvedValue([undefined, undefined])
|
|
64
|
+
mocks.updateConfigFile.mockResolvedValue(undefined)
|
|
49
65
|
mocks.callHook.mockResolvedValue(undefined)
|
|
50
66
|
mocks.loadInjectDefaultSystemPromptValue.mockResolvedValue(true)
|
|
51
67
|
mocks.mergeSystemPrompts.mockReturnValue(undefined)
|
|
52
68
|
mocks.postSessionEvent.mockResolvedValue(undefined)
|
|
53
69
|
mocks.fetchSessionMessages.mockResolvedValue([])
|
|
54
70
|
mocks.extractTextFromMessage.mockReturnValue('')
|
|
71
|
+
mocks.mkdir.mockResolvedValue(undefined)
|
|
72
|
+
mocks.writeFile.mockResolvedValue(undefined)
|
|
55
73
|
})
|
|
56
74
|
|
|
57
75
|
it('keeps the task failed when a fatal error is followed by stop', async () => {
|
|
@@ -88,4 +106,304 @@ describe('taskManager fatal error scenarios', () => {
|
|
|
88
106
|
expect(task?.status).toBe('failed')
|
|
89
107
|
expect(task?.logs).toContain('Incomplete response returned')
|
|
90
108
|
})
|
|
109
|
+
|
|
110
|
+
it('surfaces pending interactions in logs and task state', async () => {
|
|
111
|
+
const { TaskManager } = await import('#~/tools/task/manager.js')
|
|
112
|
+
|
|
113
|
+
mocks.run.mockImplementationOnce(async (_options: unknown, adapterOptions: any) => {
|
|
114
|
+
const session = {
|
|
115
|
+
emit: vi.fn(() => {
|
|
116
|
+
adapterOptions.onEvent({
|
|
117
|
+
type: 'interaction_request',
|
|
118
|
+
data: {
|
|
119
|
+
id: 'interaction-1',
|
|
120
|
+
payload: {
|
|
121
|
+
sessionId: 'task-waiting',
|
|
122
|
+
kind: 'permission',
|
|
123
|
+
question: 'Allow editing files?',
|
|
124
|
+
options: [
|
|
125
|
+
{ label: 'Allow once', value: 'allow_once' },
|
|
126
|
+
{ label: 'Deny once', value: 'deny_once' }
|
|
127
|
+
]
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
})
|
|
131
|
+
}),
|
|
132
|
+
kill: vi.fn(),
|
|
133
|
+
respondInteraction: vi.fn()
|
|
134
|
+
}
|
|
135
|
+
return { session }
|
|
136
|
+
})
|
|
137
|
+
|
|
138
|
+
const managedTaskManager = new TaskManager()
|
|
139
|
+
const result = await managedTaskManager.startTask({
|
|
140
|
+
taskId: 'task-waiting',
|
|
141
|
+
description: 'trigger',
|
|
142
|
+
background: false
|
|
143
|
+
})
|
|
144
|
+
|
|
145
|
+
const task = managedTaskManager.getTask('task-waiting')
|
|
146
|
+
expect(task?.status).toBe('waiting_input')
|
|
147
|
+
expect(task?.pendingInteraction).toEqual({
|
|
148
|
+
id: 'interaction-1',
|
|
149
|
+
payload: expect.objectContaining({
|
|
150
|
+
question: 'Allow editing files?'
|
|
151
|
+
}),
|
|
152
|
+
source: 'adapter'
|
|
153
|
+
})
|
|
154
|
+
expect(result.logs).toContain(
|
|
155
|
+
'Waiting for permission input: Allow editing files? Available responses: allow_once, deny_once.'
|
|
156
|
+
)
|
|
157
|
+
})
|
|
158
|
+
|
|
159
|
+
it('responds to pending interactions and syncs the response', async () => {
|
|
160
|
+
const { TaskManager } = await import('#~/tools/task/manager.js')
|
|
161
|
+
const respondInteraction = vi.fn()
|
|
162
|
+
|
|
163
|
+
mocks.run.mockImplementationOnce(async (_options: unknown, adapterOptions: any) => {
|
|
164
|
+
const session = {
|
|
165
|
+
emit: vi.fn(() => {
|
|
166
|
+
adapterOptions.onEvent({
|
|
167
|
+
type: 'interaction_request',
|
|
168
|
+
data: {
|
|
169
|
+
id: 'interaction-2',
|
|
170
|
+
payload: {
|
|
171
|
+
sessionId: 'task-respond',
|
|
172
|
+
kind: 'permission',
|
|
173
|
+
question: 'Allow bash?',
|
|
174
|
+
options: [
|
|
175
|
+
{ label: 'Allow once', value: 'allow_once' }
|
|
176
|
+
]
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
})
|
|
180
|
+
}),
|
|
181
|
+
kill: vi.fn(),
|
|
182
|
+
respondInteraction
|
|
183
|
+
}
|
|
184
|
+
return { session }
|
|
185
|
+
})
|
|
186
|
+
|
|
187
|
+
const managedTaskManager = new TaskManager()
|
|
188
|
+
await managedTaskManager.startTask({
|
|
189
|
+
taskId: 'task-respond',
|
|
190
|
+
description: 'trigger',
|
|
191
|
+
enableServerSync: true
|
|
192
|
+
})
|
|
193
|
+
|
|
194
|
+
await managedTaskManager.respondToTaskInteraction({
|
|
195
|
+
taskId: 'task-respond',
|
|
196
|
+
data: 'allow_once'
|
|
197
|
+
})
|
|
198
|
+
|
|
199
|
+
const task = managedTaskManager.getTask('task-respond')
|
|
200
|
+
expect(respondInteraction).toHaveBeenCalledWith('interaction-2', 'allow_once')
|
|
201
|
+
expect(task?.status).toBe('running')
|
|
202
|
+
expect(task?.pendingInteraction).toBeUndefined()
|
|
203
|
+
expect(task?.logs).toContain('Interaction response submitted: allow_once')
|
|
204
|
+
expect(mocks.postSessionEvent).toHaveBeenCalledWith('task-respond', {
|
|
205
|
+
type: 'interaction_response',
|
|
206
|
+
id: 'interaction-2',
|
|
207
|
+
data: 'allow_once'
|
|
208
|
+
})
|
|
209
|
+
})
|
|
210
|
+
|
|
211
|
+
it('builds synthetic permission recovery for claude-code and resumes after SubmitTaskInput', async () => {
|
|
212
|
+
const { TaskManager } = await import('#~/tools/task/manager.js')
|
|
213
|
+
const resumedEmit = vi.fn()
|
|
214
|
+
|
|
215
|
+
mocks.run
|
|
216
|
+
.mockImplementationOnce(async (_options: unknown, adapterOptions: any) => {
|
|
217
|
+
const session = {
|
|
218
|
+
emit: vi.fn(() => {
|
|
219
|
+
adapterOptions.onEvent({
|
|
220
|
+
type: 'message',
|
|
221
|
+
data: {
|
|
222
|
+
id: 'assistant-tool-use',
|
|
223
|
+
role: 'assistant',
|
|
224
|
+
content: [{
|
|
225
|
+
type: 'tool_use',
|
|
226
|
+
id: 'tool-use-1',
|
|
227
|
+
name: 'adapter:claude-code:Write',
|
|
228
|
+
input: {
|
|
229
|
+
file_path: '/tmp/demo.txt',
|
|
230
|
+
content: 'ok'
|
|
231
|
+
}
|
|
232
|
+
}],
|
|
233
|
+
createdAt: Date.now()
|
|
234
|
+
}
|
|
235
|
+
})
|
|
236
|
+
adapterOptions.onEvent({
|
|
237
|
+
type: 'error',
|
|
238
|
+
data: {
|
|
239
|
+
message: 'Permission required to continue',
|
|
240
|
+
code: 'permission_required',
|
|
241
|
+
details: {
|
|
242
|
+
toolUseId: 'tool-use-1',
|
|
243
|
+
permissionDenials: [{
|
|
244
|
+
message: 'Write requires approval',
|
|
245
|
+
deniedTools: []
|
|
246
|
+
}]
|
|
247
|
+
},
|
|
248
|
+
fatal: true
|
|
249
|
+
}
|
|
250
|
+
})
|
|
251
|
+
adapterOptions.onEvent({
|
|
252
|
+
type: 'exit',
|
|
253
|
+
data: {
|
|
254
|
+
exitCode: 1,
|
|
255
|
+
stderr: 'permission blocked'
|
|
256
|
+
}
|
|
257
|
+
})
|
|
258
|
+
}),
|
|
259
|
+
kill: vi.fn()
|
|
260
|
+
}
|
|
261
|
+
return {
|
|
262
|
+
session,
|
|
263
|
+
resolvedAdapter: 'claude-code'
|
|
264
|
+
}
|
|
265
|
+
})
|
|
266
|
+
.mockResolvedValueOnce({
|
|
267
|
+
session: {
|
|
268
|
+
emit: resumedEmit,
|
|
269
|
+
kill: vi.fn()
|
|
270
|
+
},
|
|
271
|
+
resolvedAdapter: 'claude-code'
|
|
272
|
+
})
|
|
273
|
+
|
|
274
|
+
const managedTaskManager = new TaskManager()
|
|
275
|
+
await managedTaskManager.startTask({
|
|
276
|
+
taskId: 'task-claude-recovery',
|
|
277
|
+
description: 'trigger',
|
|
278
|
+
adapter: 'claude-code'
|
|
279
|
+
})
|
|
280
|
+
|
|
281
|
+
const waitingTask = managedTaskManager.getTask('task-claude-recovery')
|
|
282
|
+
expect(waitingTask?.status).toBe('waiting_input')
|
|
283
|
+
expect(waitingTask?.pendingInteraction).toMatchObject({
|
|
284
|
+
source: 'permission_recovery',
|
|
285
|
+
subjectKeys: ['Write'],
|
|
286
|
+
payload: {
|
|
287
|
+
question: '当前任务需要使用 Write 才能继续,请选择处理方式。',
|
|
288
|
+
kind: 'permission',
|
|
289
|
+
permissionContext: expect.objectContaining({
|
|
290
|
+
currentMode: undefined,
|
|
291
|
+
deniedTools: ['Write'],
|
|
292
|
+
subjectKey: 'Write',
|
|
293
|
+
subjectLabel: 'Write',
|
|
294
|
+
projectConfigPath: '.ai.config.json'
|
|
295
|
+
})
|
|
296
|
+
}
|
|
297
|
+
})
|
|
298
|
+
|
|
299
|
+
await managedTaskManager.submitTaskInput({
|
|
300
|
+
taskId: 'task-claude-recovery',
|
|
301
|
+
data: 'allow_session'
|
|
302
|
+
})
|
|
303
|
+
|
|
304
|
+
const resumedTask = managedTaskManager.getTask('task-claude-recovery')
|
|
305
|
+
expect(resumedTask?.status).toBe('running')
|
|
306
|
+
expect(resumedTask?.pendingInteraction).toBeUndefined()
|
|
307
|
+
expect(resumedTask?.permissionState).toEqual(expect.objectContaining({
|
|
308
|
+
allow: ['Write']
|
|
309
|
+
}))
|
|
310
|
+
expect(resumedTask?.logs).toContain('Permission decision applied: allow_session. Restarting task.')
|
|
311
|
+
expect(mocks.run).toHaveBeenCalledTimes(2)
|
|
312
|
+
expect(resumedEmit).toHaveBeenCalledWith({
|
|
313
|
+
type: 'message',
|
|
314
|
+
content: [{
|
|
315
|
+
type: 'text',
|
|
316
|
+
text: '权限规则已更新。请继续刚才被权限拦截的工作,并重试被阻止的操作。'
|
|
317
|
+
}]
|
|
318
|
+
})
|
|
319
|
+
expect(mocks.writeFile).toHaveBeenCalled()
|
|
320
|
+
})
|
|
321
|
+
|
|
322
|
+
it('stops blocked tasks even after the failed session has already exited', async () => {
|
|
323
|
+
const { TaskManager } = await import('#~/tools/task/manager.js')
|
|
324
|
+
|
|
325
|
+
mocks.run.mockImplementationOnce(async (_options: unknown, adapterOptions: any) => {
|
|
326
|
+
const session = {
|
|
327
|
+
emit: vi.fn(() => {
|
|
328
|
+
adapterOptions.onEvent({
|
|
329
|
+
type: 'message',
|
|
330
|
+
data: {
|
|
331
|
+
id: 'assistant-tool-use-stop',
|
|
332
|
+
role: 'assistant',
|
|
333
|
+
content: [{
|
|
334
|
+
type: 'tool_use',
|
|
335
|
+
id: 'tool-use-stop-1',
|
|
336
|
+
name: 'adapter:claude-code:Write',
|
|
337
|
+
input: {
|
|
338
|
+
file_path: '/tmp/demo.txt',
|
|
339
|
+
content: 'blocked'
|
|
340
|
+
}
|
|
341
|
+
}],
|
|
342
|
+
createdAt: Date.now()
|
|
343
|
+
}
|
|
344
|
+
})
|
|
345
|
+
adapterOptions.onEvent({
|
|
346
|
+
type: 'error',
|
|
347
|
+
data: {
|
|
348
|
+
message: 'Permission required to continue',
|
|
349
|
+
code: 'permission_required',
|
|
350
|
+
details: {
|
|
351
|
+
toolUseId: 'tool-use-stop-1',
|
|
352
|
+
permissionDenials: [{
|
|
353
|
+
message: 'Write requires approval',
|
|
354
|
+
deniedTools: []
|
|
355
|
+
}]
|
|
356
|
+
},
|
|
357
|
+
fatal: true
|
|
358
|
+
}
|
|
359
|
+
})
|
|
360
|
+
adapterOptions.onEvent({
|
|
361
|
+
type: 'exit',
|
|
362
|
+
data: {
|
|
363
|
+
exitCode: 1,
|
|
364
|
+
stderr: 'permission blocked'
|
|
365
|
+
}
|
|
366
|
+
})
|
|
367
|
+
}),
|
|
368
|
+
kill: vi.fn()
|
|
369
|
+
}
|
|
370
|
+
return {
|
|
371
|
+
session,
|
|
372
|
+
resolvedAdapter: 'claude-code'
|
|
373
|
+
}
|
|
374
|
+
})
|
|
375
|
+
|
|
376
|
+
const managedTaskManager = new TaskManager()
|
|
377
|
+
await managedTaskManager.startTask({
|
|
378
|
+
taskId: 'task-stop-waiting',
|
|
379
|
+
description: 'trigger',
|
|
380
|
+
adapter: 'claude-code',
|
|
381
|
+
enableServerSync: true
|
|
382
|
+
})
|
|
383
|
+
|
|
384
|
+
const waitingTask = managedTaskManager.getTask('task-stop-waiting')
|
|
385
|
+
expect(waitingTask?.status).toBe('waiting_input')
|
|
386
|
+
expect(waitingTask?.session).toBeUndefined()
|
|
387
|
+
expect(waitingTask?.pendingInteraction).toBeDefined()
|
|
388
|
+
|
|
389
|
+
expect(managedTaskManager.stopTask('task-stop-waiting')).toBe(true)
|
|
390
|
+
await new Promise(resolve => setTimeout(resolve, 0))
|
|
391
|
+
|
|
392
|
+
const stoppedTask = managedTaskManager.getTask('task-stop-waiting')
|
|
393
|
+
expect(stoppedTask?.status).toBe('failed')
|
|
394
|
+
expect(stoppedTask?.pendingInteraction).toBeUndefined()
|
|
395
|
+
expect(stoppedTask?.logs).toContain('Task stopped by user')
|
|
396
|
+
expect(mocks.postSessionEvent).toHaveBeenCalledWith('task-stop-waiting', {
|
|
397
|
+
type: 'interaction_response',
|
|
398
|
+
id: expect.stringContaining('task-recovery:task-stop-waiting:'),
|
|
399
|
+
data: 'cancel'
|
|
400
|
+
})
|
|
401
|
+
expect(mocks.postSessionEvent).toHaveBeenCalledWith('task-stop-waiting', {
|
|
402
|
+
type: 'error',
|
|
403
|
+
data: {
|
|
404
|
+
message: 'Task stopped by user',
|
|
405
|
+
fatal: true
|
|
406
|
+
}
|
|
407
|
+
})
|
|
408
|
+
})
|
|
91
409
|
})
|
|
@@ -8,6 +8,8 @@ const mocks = vi.hoisted(() => {
|
|
|
8
8
|
createChildSession: vi.fn(),
|
|
9
9
|
getParentSessionId: vi.fn(),
|
|
10
10
|
startTask: vi.fn(),
|
|
11
|
+
submitTaskInput: vi.fn(),
|
|
12
|
+
respondToTaskInteraction: vi.fn(),
|
|
11
13
|
getTask: vi.fn(),
|
|
12
14
|
stopTask: vi.fn(),
|
|
13
15
|
getAllTasks: vi.fn(),
|
|
@@ -31,6 +33,8 @@ vi.mock('#~/sync.js', () => ({
|
|
|
31
33
|
vi.mock('#~/tools/task/manager.js', () => ({
|
|
32
34
|
TaskManager: class {
|
|
33
35
|
startTask = mocks.startTask
|
|
36
|
+
submitTaskInput = mocks.submitTaskInput
|
|
37
|
+
respondToTaskInteraction = mocks.respondToTaskInteraction
|
|
34
38
|
getTask = mocks.getTask
|
|
35
39
|
stopTask = mocks.stopTask
|
|
36
40
|
getAllTasks = mocks.getAllTasks
|
|
@@ -41,11 +45,15 @@ describe('task tool integration', () => {
|
|
|
41
45
|
beforeEach(() => {
|
|
42
46
|
vi.clearAllMocks()
|
|
43
47
|
process.env.__VF_PROJECT_AI_SESSION_ID__ = 'sess-1'
|
|
48
|
+
delete process.env.__VF_PROJECT_AI_PERMISSION_MODE__
|
|
44
49
|
let nextTaskId = 1
|
|
45
50
|
mocks.uuid.mockImplementation(() => `task-${nextTaskId++}`)
|
|
46
51
|
mocks.callHook.mockResolvedValue({ continue: true })
|
|
47
52
|
mocks.getParentSessionId.mockReturnValue(undefined)
|
|
53
|
+
mocks.createChildSession.mockResolvedValue({})
|
|
48
54
|
mocks.startTask.mockResolvedValue(undefined)
|
|
55
|
+
mocks.submitTaskInput.mockResolvedValue(undefined)
|
|
56
|
+
mocks.respondToTaskInteraction.mockResolvedValue(undefined)
|
|
49
57
|
mocks.getTask.mockImplementation((taskId: string) => ({
|
|
50
58
|
taskId,
|
|
51
59
|
status: 'completed',
|
|
@@ -85,4 +93,126 @@ describe('task tool integration', () => {
|
|
|
85
93
|
taskId: 'task-1'
|
|
86
94
|
}))
|
|
87
95
|
})
|
|
96
|
+
|
|
97
|
+
it('inherits the parent permission mode when the task does not specify one', async () => {
|
|
98
|
+
process.env.__VF_PROJECT_AI_PERMISSION_MODE__ = 'dontAsk'
|
|
99
|
+
mocks.getParentSessionId.mockReturnValue('parent-session')
|
|
100
|
+
|
|
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: 'inherit permissions',
|
|
109
|
+
type: 'default'
|
|
110
|
+
}]
|
|
111
|
+
})
|
|
112
|
+
|
|
113
|
+
expect(mocks.callHook).toHaveBeenCalledWith(
|
|
114
|
+
'StartTasks',
|
|
115
|
+
expect.objectContaining({
|
|
116
|
+
tasks: [expect.objectContaining({
|
|
117
|
+
taskId: 'task-1',
|
|
118
|
+
permissionMode: 'dontAsk'
|
|
119
|
+
})]
|
|
120
|
+
})
|
|
121
|
+
)
|
|
122
|
+
expect(mocks.createChildSession).toHaveBeenCalledWith(expect.objectContaining({
|
|
123
|
+
id: 'task-1',
|
|
124
|
+
parentSessionId: 'parent-session',
|
|
125
|
+
permissionMode: 'dontAsk'
|
|
126
|
+
}))
|
|
127
|
+
expect(mocks.startTask).toHaveBeenCalledWith(expect.objectContaining({
|
|
128
|
+
taskId: 'task-1',
|
|
129
|
+
permissionMode: 'dontAsk',
|
|
130
|
+
enableServerSync: true
|
|
131
|
+
}))
|
|
132
|
+
})
|
|
133
|
+
|
|
134
|
+
it('keeps an explicit task permission mode over the inherited parent mode', async () => {
|
|
135
|
+
process.env.__VF_PROJECT_AI_PERMISSION_MODE__ = 'dontAsk'
|
|
136
|
+
|
|
137
|
+
const { createTaskRegister } = await import('#~/tools/task/index.js')
|
|
138
|
+
|
|
139
|
+
const tester = createToolTester()
|
|
140
|
+
createTaskRegister()(tester.mockRegister)
|
|
141
|
+
|
|
142
|
+
await tester.callTool('StartTasks', {
|
|
143
|
+
tasks: [{
|
|
144
|
+
description: 'override permissions',
|
|
145
|
+
type: 'default',
|
|
146
|
+
permissionMode: 'plan'
|
|
147
|
+
}]
|
|
148
|
+
})
|
|
149
|
+
|
|
150
|
+
expect(mocks.startTask).toHaveBeenCalledWith(expect.objectContaining({
|
|
151
|
+
taskId: 'task-1',
|
|
152
|
+
permissionMode: 'plan'
|
|
153
|
+
}))
|
|
154
|
+
})
|
|
155
|
+
|
|
156
|
+
it('registers recovery guidance in task tool descriptions', async () => {
|
|
157
|
+
const { createTaskRegister } = await import('#~/tools/task/index.js')
|
|
158
|
+
|
|
159
|
+
const tester = createToolTester()
|
|
160
|
+
createTaskRegister()(tester.mockRegister)
|
|
161
|
+
|
|
162
|
+
expect(tester.getRegisteredTools()).toContain('SubmitTaskInput')
|
|
163
|
+
expect(tester.getRegisteredTools()).toContain('RespondTaskInteraction')
|
|
164
|
+
expect(tester.getToolInfo('StartTasks')?.description).toContain('GetTaskInfo')
|
|
165
|
+
expect(tester.getToolInfo('GetTaskInfo')?.description).toContain('SubmitTaskInput')
|
|
166
|
+
expect(tester.getToolInfo('ListTasks')?.description).toContain('pendingInput')
|
|
167
|
+
expect(tester.getToolInfo('SubmitTaskInput')?.description).toContain('allow_once')
|
|
168
|
+
expect(tester.getToolInfo('RespondTaskInteraction')?.description).toContain('Deprecated alias')
|
|
169
|
+
})
|
|
170
|
+
|
|
171
|
+
it('forwards SubmitTaskInput to the task manager', async () => {
|
|
172
|
+
mocks.getTask.mockReturnValue({
|
|
173
|
+
taskId: 'task-1',
|
|
174
|
+
status: 'running',
|
|
175
|
+
logs: ['Interaction response submitted: allow_once']
|
|
176
|
+
})
|
|
177
|
+
|
|
178
|
+
const { createTaskRegister } = await import('#~/tools/task/index.js')
|
|
179
|
+
|
|
180
|
+
const tester = createToolTester()
|
|
181
|
+
createTaskRegister()(tester.mockRegister)
|
|
182
|
+
|
|
183
|
+
await tester.callTool('SubmitTaskInput', {
|
|
184
|
+
taskId: 'task-1',
|
|
185
|
+
data: 'allow_once'
|
|
186
|
+
})
|
|
187
|
+
|
|
188
|
+
expect(mocks.submitTaskInput).toHaveBeenCalledWith({
|
|
189
|
+
taskId: 'task-1',
|
|
190
|
+
interactionId: undefined,
|
|
191
|
+
data: 'allow_once'
|
|
192
|
+
})
|
|
193
|
+
})
|
|
194
|
+
|
|
195
|
+
it('keeps RespondTaskInteraction as a deprecated alias', async () => {
|
|
196
|
+
mocks.getTask.mockReturnValue({
|
|
197
|
+
taskId: 'task-1',
|
|
198
|
+
status: 'running',
|
|
199
|
+
logs: ['Interaction response submitted: allow_once']
|
|
200
|
+
})
|
|
201
|
+
|
|
202
|
+
const { createTaskRegister } = await import('#~/tools/task/index.js')
|
|
203
|
+
|
|
204
|
+
const tester = createToolTester()
|
|
205
|
+
createTaskRegister()(tester.mockRegister)
|
|
206
|
+
|
|
207
|
+
await tester.callTool('RespondTaskInteraction', {
|
|
208
|
+
taskId: 'task-1',
|
|
209
|
+
response: 'allow_once'
|
|
210
|
+
})
|
|
211
|
+
|
|
212
|
+
expect(mocks.submitTaskInput).toHaveBeenCalledWith({
|
|
213
|
+
taskId: 'task-1',
|
|
214
|
+
interactionId: undefined,
|
|
215
|
+
data: 'allow_once'
|
|
216
|
+
})
|
|
217
|
+
})
|
|
88
218
|
})
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@vibe-forge/mcp",
|
|
3
|
-
"version": "0.11.
|
|
3
|
+
"version": "0.11.1",
|
|
4
4
|
"description": "Vibe Forge MCP server",
|
|
5
5
|
"imports": {
|
|
6
6
|
"#~/*.js": {
|
|
@@ -33,13 +33,13 @@
|
|
|
33
33
|
"@modelcontextprotocol/sdk": "^1.25.3",
|
|
34
34
|
"commander": "^12.1.0",
|
|
35
35
|
"zod": "^3.24.1",
|
|
36
|
+
"@vibe-forge/hooks": "^0.11.1",
|
|
37
|
+
"@vibe-forge/config": "^0.11.0",
|
|
36
38
|
"@vibe-forge/cli-helper": "^0.11.0",
|
|
39
|
+
"@vibe-forge/types": "^0.11.1",
|
|
40
|
+
"@vibe-forge/utils": "^0.11.1",
|
|
37
41
|
"@vibe-forge/register": "^0.11.0",
|
|
38
|
-
"@vibe-forge/
|
|
39
|
-
"@vibe-forge/hooks": "^0.11.0",
|
|
40
|
-
"@vibe-forge/task": "^0.11.0",
|
|
41
|
-
"@vibe-forge/types": "^0.11.0",
|
|
42
|
-
"@vibe-forge/utils": "^0.11.0"
|
|
42
|
+
"@vibe-forge/task": "^0.11.2"
|
|
43
43
|
},
|
|
44
44
|
"scripts": {
|
|
45
45
|
"test": "pnpm -C ../.. exec vitest run --workspace vitest.workspace.ts --project bundler packages/mcp/__tests__"
|
package/src/sync.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import process from 'node:process'
|
|
2
2
|
|
|
3
|
-
import type { WSEvent } from '@vibe-forge/types'
|
|
3
|
+
import type { SessionPermissionMode, WSEvent } from '@vibe-forge/types'
|
|
4
4
|
import { extractTextFromMessage } from '@vibe-forge/utils/chat-message'
|
|
5
5
|
|
|
6
6
|
const getServerBaseUrl = () => {
|
|
@@ -10,6 +10,10 @@ const getServerBaseUrl = () => {
|
|
|
10
10
|
}
|
|
11
11
|
|
|
12
12
|
export const getParentSessionId = () => {
|
|
13
|
+
const sessionId = process.env.__VF_PROJECT_AI_SESSION_ID__
|
|
14
|
+
if (sessionId != null && sessionId !== '') {
|
|
15
|
+
return sessionId
|
|
16
|
+
}
|
|
13
17
|
const ctxId = process.env.__VF_PROJECT_AI_CTX_ID__
|
|
14
18
|
return ctxId ?? undefined
|
|
15
19
|
}
|
|
@@ -18,6 +22,7 @@ export const createChildSession = async (params: {
|
|
|
18
22
|
id: string
|
|
19
23
|
title?: string
|
|
20
24
|
parentSessionId?: string
|
|
25
|
+
permissionMode?: SessionPermissionMode
|
|
21
26
|
}) => {
|
|
22
27
|
const baseUrl = getServerBaseUrl()
|
|
23
28
|
const response = await fetch(`${baseUrl}/api/sessions`, {
|
|
@@ -27,6 +32,7 @@ export const createChildSession = async (params: {
|
|
|
27
32
|
id: params.id,
|
|
28
33
|
title: params.title,
|
|
29
34
|
parentSessionId: params.parentSessionId,
|
|
35
|
+
permissionMode: params.permissionMode,
|
|
30
36
|
start: false
|
|
31
37
|
})
|
|
32
38
|
})
|