@miphamai/cli 0.9.0 → 0.11.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.
- package/dist/mipham +0 -0
- package/package.json +1 -1
- package/src/agent/agent-context.ts +60 -1
- package/src/agent/agent-registry.ts +1 -0
- package/src/agent/background-registry.ts +192 -0
- package/src/agent/message-bus.ts +113 -0
- package/src/agent/sub-agent.ts +101 -3
- package/src/agent/types.ts +6 -0
- package/src/agent-view/agent-view-manager.ts +10 -0
- package/src/agent-view/dashboard.tsx +20 -1
- package/src/core/engine.ts +90 -0
- package/src/core/hooks.ts +48 -0
- package/src/core/output-styles.ts +95 -0
- package/src/core/rules-loader.ts +175 -0
- package/src/mcp/client.ts +9 -0
- package/src/shared/types.ts +16 -1
- package/src/tools/agent/agent.ts +41 -2
- package/src/tools/agent/enter-plan.ts +92 -0
- package/src/tools/agent/exit-plan.ts +54 -0
- package/src/tools/agent/report-findings.ts +186 -0
- package/src/tools/agent/send-message.ts +67 -0
- package/src/tools/agent/workflow.ts +25 -5
- package/src/tools/exec/enter-worktree.ts +142 -0
- package/src/tools/exec/exit-worktree.ts +162 -0
- package/src/tools/exec/task.ts +215 -22
- package/src/tools/index.ts +14 -0
- package/src/tools/system/tool-search.ts +143 -0
- package/src/ui/app.tsx +49 -19
- package/src/ui/commands.ts +172 -55
- package/src/workflow/primitives/agent.ts +77 -15
- package/src/workflow/runtime.ts +84 -7
- package/src/workflow/schema-validator.ts +162 -0
package/src/tools/exec/task.ts
CHANGED
|
@@ -4,13 +4,17 @@ interface Task {
|
|
|
4
4
|
id: string
|
|
5
5
|
subject: string
|
|
6
6
|
description: string
|
|
7
|
-
status: 'pending' | 'in_progress' | 'completed' | 'deleted'
|
|
7
|
+
status: 'pending' | 'in_progress' | 'completed' | 'deleted' | 'failed'
|
|
8
8
|
activeForm?: string
|
|
9
9
|
metadata?: Record<string, unknown>
|
|
10
10
|
blocks: string[]
|
|
11
11
|
blockedBy: string[]
|
|
12
12
|
owner?: string
|
|
13
13
|
createdAt: string
|
|
14
|
+
/** Background task output content (set when a background agent completes). */
|
|
15
|
+
output?: string
|
|
16
|
+
/** Background task error message (set when a background agent fails). */
|
|
17
|
+
outputError?: string
|
|
14
18
|
}
|
|
15
19
|
|
|
16
20
|
const tasks = new Map<string, Task>()
|
|
@@ -20,14 +24,33 @@ function formatTask(t: Task): string {
|
|
|
20
24
|
const meta = t.metadata ? ` ${JSON.stringify(t.metadata)}` : ''
|
|
21
25
|
const blocks = t.blocks.length ? ` blocks:[${t.blocks.join(',')}]` : ''
|
|
22
26
|
const blocked = t.blockedBy.length ? ` waits:[${t.blockedBy.join(',')}]` : ''
|
|
23
|
-
|
|
27
|
+
const owner = t.owner ? ` @${t.owner}` : ''
|
|
28
|
+
return `[${t.status}] #${t.id}: ${t.subject}${owner}${blocks}${blocked}${meta}`
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** Check if a task is blocked — has unresolved dependencies. */
|
|
32
|
+
function isBlocked(task: Task): boolean {
|
|
33
|
+
if (task.blockedBy.length === 0) return false
|
|
34
|
+
return task.blockedBy.some((depId) => {
|
|
35
|
+
const dep = tasks.get(depId)
|
|
36
|
+
return dep && dep.status !== 'completed' && dep.status !== 'deleted'
|
|
37
|
+
})
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** Get the list of blocking task IDs (unresolved dependencies). */
|
|
41
|
+
function getBlockingIds(task: Task): string[] {
|
|
42
|
+
return task.blockedBy.filter((depId) => {
|
|
43
|
+
const dep = tasks.get(depId)
|
|
44
|
+
return dep && dep.status !== 'completed' && dep.status !== 'deleted'
|
|
45
|
+
})
|
|
24
46
|
}
|
|
25
47
|
|
|
26
48
|
export const taskTool: ToolDefinition = {
|
|
27
49
|
name: 'Task',
|
|
28
50
|
description:
|
|
29
51
|
'Create and manage structured task lists for tracking progress. ' +
|
|
30
|
-
'Supports CRUD, dependencies (blocks/blockedBy), metadata, owner assignment,
|
|
52
|
+
'Supports CRUD, dependencies (blocks/blockedBy), metadata, owner assignment, ' +
|
|
53
|
+
'background task output/stop, and status workflow: pending → in_progress → completed. ' +
|
|
31
54
|
'Use for complex multi-step tasks, session tracking, and organizing work.',
|
|
32
55
|
category: 'exec',
|
|
33
56
|
permission: 'auto',
|
|
@@ -36,9 +59,9 @@ export const taskTool: ToolDefinition = {
|
|
|
36
59
|
properties: {
|
|
37
60
|
action: {
|
|
38
61
|
type: 'string',
|
|
39
|
-
enum: ['create', 'list', 'update', 'get', 'delete'],
|
|
62
|
+
enum: ['create', 'list', 'update', 'get', 'delete', 'output', 'stop'],
|
|
40
63
|
description:
|
|
41
|
-
'Action: create a task, list all, update status/fields, get one by ID, or
|
|
64
|
+
'Action: create a task, list all, update status/fields, get one by ID, delete, view output of background task, or stop a running task.',
|
|
42
65
|
},
|
|
43
66
|
subject: { type: 'string', description: 'A brief, actionable title (for create/update).' },
|
|
44
67
|
description: { type: 'string', description: 'What needs to be done (for create/update).' },
|
|
@@ -46,21 +69,24 @@ export const taskTool: ToolDefinition = {
|
|
|
46
69
|
type: 'string',
|
|
47
70
|
description: 'Present continuous form shown during work (for create/update).',
|
|
48
71
|
},
|
|
49
|
-
taskId: { type: 'string', description: 'Task ID (for update/get/delete).' },
|
|
72
|
+
taskId: { type: 'string', description: 'Task ID (for update/get/delete/output/stop).' },
|
|
50
73
|
status: {
|
|
51
74
|
type: 'string',
|
|
52
|
-
enum: ['pending', 'in_progress', 'completed', 'deleted'],
|
|
53
|
-
description:
|
|
75
|
+
enum: ['pending', 'in_progress', 'completed', 'deleted', 'failed'],
|
|
76
|
+
description:
|
|
77
|
+
'New status. "deleted" permanently removes the task, "failed" marks as failed.',
|
|
54
78
|
},
|
|
55
79
|
addBlocks: {
|
|
56
80
|
type: 'array',
|
|
57
81
|
items: { type: 'string' },
|
|
58
|
-
description:
|
|
82
|
+
description:
|
|
83
|
+
'Task IDs that this task blocks (they depend on this one). Use for create or update.',
|
|
59
84
|
},
|
|
60
85
|
addBlockedBy: {
|
|
61
86
|
type: 'array',
|
|
62
87
|
items: { type: 'string' },
|
|
63
|
-
description:
|
|
88
|
+
description:
|
|
89
|
+
'Task IDs that block this task (this one depends on them). Use for create or update.',
|
|
64
90
|
},
|
|
65
91
|
owner: { type: 'string', description: 'Agent or user name assigned to this task.' },
|
|
66
92
|
metadata: {
|
|
@@ -90,8 +116,24 @@ export const taskTool: ToolDefinition = {
|
|
|
90
116
|
owner: (params.owner as string) || undefined,
|
|
91
117
|
createdAt: new Date().toISOString(),
|
|
92
118
|
}
|
|
119
|
+
|
|
120
|
+
// Accept initial dependencies at creation time
|
|
121
|
+
if (Array.isArray(params.addBlocks)) {
|
|
122
|
+
for (const bid of params.addBlocks as string[]) {
|
|
123
|
+
if (!task.blocks.includes(bid)) task.blocks.push(bid)
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
if (Array.isArray(params.addBlockedBy)) {
|
|
127
|
+
for (const bid of params.addBlockedBy as string[]) {
|
|
128
|
+
if (!task.blockedBy.includes(bid)) task.blockedBy.push(bid)
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
93
132
|
tasks.set(id, task)
|
|
94
|
-
|
|
133
|
+
const blockedNote = isBlocked(task)
|
|
134
|
+
? ` (blocked — waiting on: ${getBlockingIds(task).join(', ')})`
|
|
135
|
+
: ''
|
|
136
|
+
return { success: true, content: `Task #${id} created: ${subject}${blockedNote}` }
|
|
95
137
|
}
|
|
96
138
|
|
|
97
139
|
// ── LIST ──
|
|
@@ -99,18 +141,63 @@ export const taskTool: ToolDefinition = {
|
|
|
99
141
|
const all = Array.from(tasks.values()).filter((t) => t.status !== 'deleted')
|
|
100
142
|
if (all.length === 0) return { success: true, content: '(no tasks)' }
|
|
101
143
|
|
|
102
|
-
//
|
|
103
|
-
const
|
|
104
|
-
|
|
144
|
+
// Sort by availability: in_progress → pending(available) → pending(blocked) → completed
|
|
145
|
+
const sortOrder: Record<string, number> = {
|
|
146
|
+
in_progress: 0,
|
|
147
|
+
pending: 1,
|
|
148
|
+
completed: 2,
|
|
149
|
+
failed: 3,
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
const sorted = [...all].sort((a, b) => {
|
|
153
|
+
const orderA = sortOrder[a.status] ?? 3
|
|
154
|
+
const orderB = sortOrder[b.status] ?? 3
|
|
155
|
+
if (orderA !== orderB) return orderA - orderB
|
|
156
|
+
// Within same status: available before blocked
|
|
157
|
+
if (a.status === 'pending' && b.status === 'pending') {
|
|
158
|
+
const aBlocked = isBlocked(a)
|
|
159
|
+
const bBlocked = isBlocked(b)
|
|
160
|
+
if (aBlocked !== bBlocked) return aBlocked ? 1 : -1
|
|
161
|
+
}
|
|
162
|
+
return 0
|
|
163
|
+
})
|
|
105
164
|
|
|
106
165
|
const lines: string[] = []
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
166
|
+
const statusLabels: Record<string, { emoji: string; label: string }> = {
|
|
167
|
+
in_progress: { emoji: '🔄', label: 'In Progress' },
|
|
168
|
+
pending: { emoji: '📋', label: 'Pending' },
|
|
169
|
+
completed: { emoji: '✅', label: 'Completed' },
|
|
170
|
+
failed: { emoji: '❌', label: 'Failed' },
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
let currentStatus = ''
|
|
174
|
+
for (const t of sorted) {
|
|
175
|
+
const statusInfo = statusLabels[t.status]
|
|
176
|
+
if (!statusInfo) continue
|
|
177
|
+
|
|
178
|
+
if (statusInfo.label !== currentStatus) {
|
|
179
|
+
currentStatus = statusInfo.label
|
|
180
|
+
// Count tasks in this status group
|
|
181
|
+
const count = sorted.filter((s) => statusLabels[s.status]?.label === currentStatus).length
|
|
182
|
+
lines.push(`${statusInfo.emoji} ${currentStatus} (${count}):`)
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
const blocked = isBlocked(t)
|
|
186
|
+
const prefix = blocked && t.status === 'pending' ? '🚫 ' : ' '
|
|
187
|
+
const blockingNote = blocked ? ` 🚫 阻塞中 (等待: ${getBlockingIds(t).join(', ')})` : ''
|
|
188
|
+
lines.push(`${prefix}${formatTask(t)}${blockingNote}`)
|
|
113
189
|
}
|
|
190
|
+
|
|
191
|
+
// Summary footer
|
|
192
|
+
const available = all.filter((t) => t.status === 'pending' && !isBlocked(t)).length
|
|
193
|
+
const blockedCount = all.filter((t) => t.status === 'pending' && isBlocked(t)).length
|
|
194
|
+
if (blockedCount > 0) {
|
|
195
|
+
lines.push('')
|
|
196
|
+
lines.push(
|
|
197
|
+
`📊 ${available} available · ${blockedCount} blocked · ${all.filter((t) => t.status === 'completed').length} done`,
|
|
198
|
+
)
|
|
199
|
+
}
|
|
200
|
+
|
|
114
201
|
return { success: true, content: lines.join('\n') }
|
|
115
202
|
}
|
|
116
203
|
|
|
@@ -129,8 +216,27 @@ export const taskTool: ToolDefinition = {
|
|
|
129
216
|
if (task.activeForm) lines.push(`Active form: ${task.activeForm}`)
|
|
130
217
|
if (task.owner) lines.push(`Owner: ${task.owner}`)
|
|
131
218
|
if (task.blocks.length) lines.push(`Blocks: ${task.blocks.join(', ')}`)
|
|
132
|
-
if (task.blockedBy.length)
|
|
219
|
+
if (task.blockedBy.length) {
|
|
220
|
+
const blocking = getBlockingIds(task)
|
|
221
|
+
if (blocking.length > 0) {
|
|
222
|
+
lines.push(
|
|
223
|
+
`Blocked by: ${task.blockedBy.join(', ')} (active blockers: ${blocking.join(', ')})`,
|
|
224
|
+
)
|
|
225
|
+
} else {
|
|
226
|
+
lines.push(`Blocked by: ${task.blockedBy.join(', ')} (all resolved ✓)`)
|
|
227
|
+
}
|
|
228
|
+
}
|
|
133
229
|
if (task.metadata) lines.push(`Metadata: ${JSON.stringify(task.metadata)}`)
|
|
230
|
+
if (task.output) {
|
|
231
|
+
lines.push('')
|
|
232
|
+
lines.push('── Output ──')
|
|
233
|
+
lines.push(task.output.slice(0, 2000))
|
|
234
|
+
}
|
|
235
|
+
if (task.outputError) {
|
|
236
|
+
lines.push('')
|
|
237
|
+
lines.push(`── Error ──`)
|
|
238
|
+
lines.push(task.outputError)
|
|
239
|
+
}
|
|
134
240
|
|
|
135
241
|
return { success: true, content: lines.join('\n') }
|
|
136
242
|
}
|
|
@@ -167,7 +273,14 @@ export const taskTool: ToolDefinition = {
|
|
|
167
273
|
}
|
|
168
274
|
}
|
|
169
275
|
|
|
170
|
-
|
|
276
|
+
const blockedNote = isBlocked(task)
|
|
277
|
+
? ` (blocked — waiting on: ${getBlockingIds(task).join(', ')})`
|
|
278
|
+
: ''
|
|
279
|
+
|
|
280
|
+
return {
|
|
281
|
+
success: true,
|
|
282
|
+
content: `Task #${taskId} updated.\n${formatTask(task)}${blockedNote}`,
|
|
283
|
+
}
|
|
171
284
|
}
|
|
172
285
|
|
|
173
286
|
// ── DELETE ──
|
|
@@ -179,6 +292,86 @@ export const taskTool: ToolDefinition = {
|
|
|
179
292
|
return { success: true, content: `Task #${taskId} deleted.` }
|
|
180
293
|
}
|
|
181
294
|
|
|
295
|
+
// ── OUTPUT ──
|
|
296
|
+
if (action === 'output') {
|
|
297
|
+
const taskId = params.taskId as string
|
|
298
|
+
const task = tasks.get(taskId)
|
|
299
|
+
if (!task) return { success: false, content: '', error: `Task #${taskId} not found` }
|
|
300
|
+
|
|
301
|
+
if (task.status === 'pending') {
|
|
302
|
+
return {
|
|
303
|
+
success: true,
|
|
304
|
+
content: `Task #${taskId} is still pending — no output yet.\nStatus: ${task.status}\nSubject: ${task.subject}`,
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
if (task.status === 'in_progress') {
|
|
309
|
+
return {
|
|
310
|
+
success: true,
|
|
311
|
+
content: `Task #${taskId} is still running — output not yet available.\nStatus: in_progress\nSubject: ${task.subject}\n\nUse Task output again once the task completes.`,
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
if (task.status === 'completed' && task.output) {
|
|
316
|
+
return {
|
|
317
|
+
success: true,
|
|
318
|
+
content: `── Task #${taskId} Output ──\nStatus: completed\nSubject: ${task.subject}\n\n${task.output.slice(0, 5000)}`,
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
if (task.status === 'failed' && task.outputError) {
|
|
323
|
+
return {
|
|
324
|
+
success: false,
|
|
325
|
+
content: '',
|
|
326
|
+
error: `Task #${taskId} failed: ${task.outputError}`,
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
return {
|
|
331
|
+
success: true,
|
|
332
|
+
content: `Task #${taskId} — Status: ${task.status}\nSubject: ${task.subject}\n\n(no output recorded)`,
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
// ── STOP ──
|
|
337
|
+
if (action === 'stop') {
|
|
338
|
+
const taskId = params.taskId as string
|
|
339
|
+
const task = tasks.get(taskId)
|
|
340
|
+
if (!task) return { success: false, content: '', error: `Task #${taskId} not found` }
|
|
341
|
+
|
|
342
|
+
if (task.status === 'completed' || task.status === 'deleted') {
|
|
343
|
+
return {
|
|
344
|
+
success: true,
|
|
345
|
+
content: `Task #${taskId} is already ${task.status}. Nothing to stop.`,
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
if (task.status === 'pending') {
|
|
350
|
+
// Cancel a pending task
|
|
351
|
+
task.status = 'deleted'
|
|
352
|
+
return { success: true, content: `Task #${taskId} cancelled (was pending).` }
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
// For in_progress tasks: mark as failed — actual process termination is handled
|
|
356
|
+
// by BackgroundAgentRegistry.stop() when wired via the engine context
|
|
357
|
+
task.status = 'failed'
|
|
358
|
+
task.outputError = 'Task stopped by user.'
|
|
359
|
+
|
|
360
|
+
// If BackgroundAgentRegistry is available in context, try to abort
|
|
361
|
+
if (_ctx.registry) {
|
|
362
|
+
// Signal to any background registry that this task should stop
|
|
363
|
+
// The background registry will be injected via the engine context
|
|
364
|
+
const bgRegistry = (_ctx as unknown as Record<string, unknown>).backgroundAgentRegistry as
|
|
365
|
+
| { stop(id: string): boolean }
|
|
366
|
+
| undefined
|
|
367
|
+
if (bgRegistry) {
|
|
368
|
+
bgRegistry.stop(taskId)
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
return { success: true, content: `Task #${taskId} stopped.` }
|
|
373
|
+
}
|
|
374
|
+
|
|
182
375
|
return { success: false, content: '', error: `Unknown action: ${action}` }
|
|
183
376
|
},
|
|
184
377
|
}
|
package/src/tools/index.ts
CHANGED
|
@@ -7,16 +7,23 @@ import { grepTool } from './file/grep'
|
|
|
7
7
|
import { bashTool } from './exec/bash'
|
|
8
8
|
import { gitTool } from './exec/git'
|
|
9
9
|
import { taskTool } from './exec/task'
|
|
10
|
+
import { enterWorktreeTool } from './exec/enter-worktree'
|
|
11
|
+
import { exitWorktreeTool } from './exec/exit-worktree'
|
|
10
12
|
import { agentTool } from './agent/agent'
|
|
11
13
|
import { skillTool } from './agent/skill'
|
|
12
14
|
import { planTool } from './agent/plan'
|
|
15
|
+
import { enterPlanModeTool } from './agent/enter-plan'
|
|
16
|
+
import { exitPlanModeTool } from './agent/exit-plan'
|
|
13
17
|
import { memoryTool } from './agent/memory'
|
|
14
18
|
import { workflowTool } from './agent/workflow'
|
|
15
19
|
import { webFetchTool } from './network/web-fetch'
|
|
16
20
|
import { webSearchTool } from './network/web-search'
|
|
17
21
|
import { configTool } from './system/config'
|
|
18
22
|
import { mcpTool } from './system/mcp'
|
|
23
|
+
import { toolSearchTool } from './system/tool-search'
|
|
19
24
|
import { artifactTool } from './artifact/artifact'
|
|
25
|
+
import { reportFindingsTool } from './agent/report-findings'
|
|
26
|
+
import { sendMessageTool } from './agent/send-message'
|
|
20
27
|
import { computerUseTool } from './computer/computer-use'
|
|
21
28
|
import { scheduleWakeupTool } from './scheduling/schedule-wakeup.js'
|
|
22
29
|
import { cronCreateTool, cronDeleteTool, cronListTool } from './scheduling/cron.js'
|
|
@@ -110,18 +117,25 @@ export function createToolRegistry(): Map<string, ToolDefinition> {
|
|
|
110
117
|
withValidation(bashTool),
|
|
111
118
|
withValidation(gitTool),
|
|
112
119
|
withValidation(taskTool),
|
|
120
|
+
withValidation(enterWorktreeTool),
|
|
121
|
+
withValidation(exitWorktreeTool),
|
|
113
122
|
// Agent tools
|
|
114
123
|
withValidation(agentTool),
|
|
115
124
|
withValidation(skillTool),
|
|
116
125
|
withValidation(planTool),
|
|
126
|
+
withValidation(enterPlanModeTool),
|
|
127
|
+
withValidation(exitPlanModeTool),
|
|
117
128
|
withValidation(memoryTool),
|
|
118
129
|
withValidation(workflowTool),
|
|
130
|
+
withValidation(reportFindingsTool),
|
|
131
|
+
withValidation(sendMessageTool),
|
|
119
132
|
// Network tools
|
|
120
133
|
withValidation(webFetchTool),
|
|
121
134
|
withValidation(webSearchTool),
|
|
122
135
|
// System tools
|
|
123
136
|
withValidation(configTool),
|
|
124
137
|
withValidation(mcpTool),
|
|
138
|
+
withValidation(toolSearchTool),
|
|
125
139
|
// Artifact tools
|
|
126
140
|
withValidation(artifactTool),
|
|
127
141
|
// Computer Use tools
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ToolSearch — search MCP server tools by name or description.
|
|
3
|
+
*
|
|
4
|
+
* Instead of loading all MCP tool definitions into the context upfront,
|
|
5
|
+
* the AI can call ToolSearch to discover relevant tools on demand.
|
|
6
|
+
* This saves ~85% of context tokens for large MCP servers (50-100 tools).
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import type { ToolDefinition } from '../../shared/index.ts'
|
|
10
|
+
import { McpClient } from '../../mcp/client'
|
|
11
|
+
|
|
12
|
+
export const toolSearchTool: ToolDefinition = {
|
|
13
|
+
name: 'ToolSearch',
|
|
14
|
+
description:
|
|
15
|
+
'Search available MCP server tools by name or description. ' +
|
|
16
|
+
'Use this to discover tools on demand instead of loading all tool definitions into context. ' +
|
|
17
|
+
'Returns matching tool names with their server and description.',
|
|
18
|
+
category: 'system',
|
|
19
|
+
permission: 'auto',
|
|
20
|
+
parameters: {
|
|
21
|
+
type: 'object',
|
|
22
|
+
properties: {
|
|
23
|
+
query: {
|
|
24
|
+
type: 'string',
|
|
25
|
+
description:
|
|
26
|
+
'Search query — matches against tool names and descriptions. Leave empty to list all available MCP tools.',
|
|
27
|
+
},
|
|
28
|
+
server: {
|
|
29
|
+
type: 'string',
|
|
30
|
+
description: 'Optional: limit search to a specific MCP server name.',
|
|
31
|
+
},
|
|
32
|
+
limit: {
|
|
33
|
+
type: 'integer',
|
|
34
|
+
description: 'Max results to return (default: 20).',
|
|
35
|
+
},
|
|
36
|
+
},
|
|
37
|
+
required: [],
|
|
38
|
+
},
|
|
39
|
+
async execute(params, _ctx) {
|
|
40
|
+
const query = ((params.query as string) || '').toLowerCase()
|
|
41
|
+
const serverFilter = (params.server as string) || ''
|
|
42
|
+
const limit = (params.limit as number) || 20
|
|
43
|
+
|
|
44
|
+
const client = McpClient.getInstance()
|
|
45
|
+
const connectedServers = client.getConnectedServers()
|
|
46
|
+
|
|
47
|
+
if (connectedServers.length === 0) {
|
|
48
|
+
return {
|
|
49
|
+
success: true,
|
|
50
|
+
content:
|
|
51
|
+
'── MCP Tool Search ──\n\n' +
|
|
52
|
+
'No MCP servers connected.\n\n' +
|
|
53
|
+
'Connect MCP servers in .mcp.json or via /mcp add <name> <url>.',
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// Filter by server if specified
|
|
58
|
+
const servers = serverFilter
|
|
59
|
+
? connectedServers.filter((s) => s.toLowerCase().includes(serverFilter))
|
|
60
|
+
: connectedServers
|
|
61
|
+
|
|
62
|
+
if (serverFilter && servers.length === 0) {
|
|
63
|
+
return {
|
|
64
|
+
success: true,
|
|
65
|
+
content:
|
|
66
|
+
`── MCP Tool Search ──\n\n` +
|
|
67
|
+
`No connected server matching "${serverFilter}".\n\n` +
|
|
68
|
+
`Connected servers: ${connectedServers.join(', ')}`,
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// Collect matching tools
|
|
73
|
+
interface Match {
|
|
74
|
+
server: string
|
|
75
|
+
tool: string
|
|
76
|
+
description: string
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const matches: Match[] = []
|
|
80
|
+
|
|
81
|
+
for (const serverName of servers) {
|
|
82
|
+
try {
|
|
83
|
+
const tools = client.getTools(serverName)
|
|
84
|
+
for (const tool of tools) {
|
|
85
|
+
const nameMatch = !query || tool.name.toLowerCase().includes(query)
|
|
86
|
+
const descMatch = !query || (tool.description || '').toLowerCase().includes(query)
|
|
87
|
+
if (nameMatch || descMatch) {
|
|
88
|
+
matches.push({
|
|
89
|
+
server: serverName,
|
|
90
|
+
tool: tool.name,
|
|
91
|
+
description: (tool.description || '').slice(0, 120),
|
|
92
|
+
})
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
} catch {
|
|
96
|
+
// Server disconnected mid-search — skip
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
if (matches.length === 0) {
|
|
101
|
+
return {
|
|
102
|
+
success: true,
|
|
103
|
+
content:
|
|
104
|
+
`── MCP Tool Search ──\n\n` +
|
|
105
|
+
`No tools matching "${query}" across ${servers.length} server(s).\n\n` +
|
|
106
|
+
`Try a different query, or omit the query to list all tools.`,
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// Truncate to limit
|
|
111
|
+
const limited = matches.slice(0, limit)
|
|
112
|
+
|
|
113
|
+
const lines: string[] = [
|
|
114
|
+
`── MCP Tool Search ──`,
|
|
115
|
+
'',
|
|
116
|
+
`${limited.length} of ${matches.length} matching tool(s) across ${servers.length} server(s):`,
|
|
117
|
+
'',
|
|
118
|
+
]
|
|
119
|
+
|
|
120
|
+
// Group by server
|
|
121
|
+
const byServer = new Map<string, Match[]>()
|
|
122
|
+
for (const m of limited) {
|
|
123
|
+
const list = byServer.get(m.server) || []
|
|
124
|
+
list.push(m)
|
|
125
|
+
byServer.set(m.server, list)
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
for (const [server, tools] of byServer) {
|
|
129
|
+
lines.push(` ${server} (${tools.length}):`)
|
|
130
|
+
for (const t of tools) {
|
|
131
|
+
const desc = t.description ? ` — ${t.description}` : ''
|
|
132
|
+
lines.push(` mcp__${server.replace(/[^a-z0-9-]/g, '_')}__${t.tool}${desc}`)
|
|
133
|
+
}
|
|
134
|
+
lines.push('')
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
if (matches.length > limit) {
|
|
138
|
+
lines.push(`... and ${matches.length - limit} more. Use "query" to narrow results.`)
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
return { success: true, content: lines.join('\n') }
|
|
142
|
+
},
|
|
143
|
+
}
|
package/src/ui/app.tsx
CHANGED
|
@@ -15,6 +15,7 @@ import {
|
|
|
15
15
|
handleSwitch,
|
|
16
16
|
type CommandContext,
|
|
17
17
|
} from './commands'
|
|
18
|
+
import type { PermissionMode } from '../shared/index.ts'
|
|
18
19
|
|
|
19
20
|
interface AppProps {
|
|
20
21
|
engine: QueryEngine
|
|
@@ -49,13 +50,29 @@ interface AgentProgress {
|
|
|
49
50
|
// Version is read fresh from package.json at startup via runApp prop
|
|
50
51
|
// (bypasses Bun module caching after npm update)
|
|
51
52
|
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
53
|
+
const PERMISSION_MODES: PermissionMode[] = [
|
|
54
|
+
'default',
|
|
55
|
+
'acceptEdits',
|
|
56
|
+
'plan',
|
|
57
|
+
'auto',
|
|
58
|
+
'dontAsk',
|
|
59
|
+
'bypassPermissions',
|
|
60
|
+
]
|
|
55
61
|
const PERMISSION_LABELS: Record<PermissionMode, string> = {
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
62
|
+
default: 'default · reads free',
|
|
63
|
+
acceptEdits: 'accept edits · reads + edits free',
|
|
64
|
+
plan: 'plan mode · read-only',
|
|
65
|
+
auto: 'auto mode · hooks gate',
|
|
66
|
+
dontAsk: "don't ask · pre-approved only",
|
|
67
|
+
bypassPermissions: 'bypass · skip all checks',
|
|
68
|
+
}
|
|
69
|
+
const PERMISSION_COLORS: Record<PermissionMode, string> = {
|
|
70
|
+
default: 'white',
|
|
71
|
+
acceptEdits: 'blue',
|
|
72
|
+
plan: 'yellow',
|
|
73
|
+
auto: 'green',
|
|
74
|
+
dontAsk: 'cyan',
|
|
75
|
+
bypassPermissions: 'red',
|
|
59
76
|
}
|
|
60
77
|
|
|
61
78
|
export function App({
|
|
@@ -78,7 +95,7 @@ export function App({
|
|
|
78
95
|
const [effort, setEffort] = useState('high')
|
|
79
96
|
const [focusMode, setFocusMode] = useState(false)
|
|
80
97
|
const [goalText, setGoalText] = useState('')
|
|
81
|
-
const [permissionMode, setPermissionMode] = useState<PermissionMode>('
|
|
98
|
+
const [permissionMode, setPermissionMode] = useState<PermissionMode>('default')
|
|
82
99
|
const abortRef = useRef<AbortController | null>(null)
|
|
83
100
|
const [agentProgress, setAgentProgress] = useState<AgentProgress | null>(null)
|
|
84
101
|
const [agentElapsed, setAgentElapsed] = useState(0)
|
|
@@ -304,6 +321,24 @@ export function App({
|
|
|
304
321
|
{ role: 'system', content: `❌ Error: ${chunk.error}` },
|
|
305
322
|
])
|
|
306
323
|
}
|
|
324
|
+
|
|
325
|
+
if (chunk.type === 'task_notification' && chunk.taskNotification) {
|
|
326
|
+
const tn = chunk.taskNotification
|
|
327
|
+
const emoji = tn.status === 'completed' ? '✅' : '❌'
|
|
328
|
+
const label = tn.status === 'completed' ? 'completed' : 'failed'
|
|
329
|
+
const preview = tn.content
|
|
330
|
+
? tn.content.slice(0, 120) + (tn.content.length > 120 ? '...' : '')
|
|
331
|
+
: tn.error
|
|
332
|
+
? `Error: ${tn.error.slice(0, 120)}`
|
|
333
|
+
: '(no output)'
|
|
334
|
+
setMessages((prev) => [
|
|
335
|
+
...prev,
|
|
336
|
+
{
|
|
337
|
+
role: 'system',
|
|
338
|
+
content: `${emoji} Background Agent ${label}: ${tn.description}\n ID: ${tn.taskId}\n ${preview}`,
|
|
339
|
+
},
|
|
340
|
+
])
|
|
341
|
+
}
|
|
307
342
|
}
|
|
308
343
|
} catch (err) {
|
|
309
344
|
setMessages((prev) => [...prev, { role: 'system', content: `Error: ${String(err)}` }])
|
|
@@ -365,13 +400,13 @@ export function App({
|
|
|
365
400
|
})
|
|
366
401
|
return
|
|
367
402
|
}
|
|
368
|
-
// Shift+Tab → cycle
|
|
403
|
+
// Shift+Tab → cycle through all 6 permission modes
|
|
369
404
|
if (key.shift && key.tab) {
|
|
370
405
|
setPermissionMode((prev) => {
|
|
371
406
|
const idx = PERMISSION_MODES.indexOf(prev)
|
|
372
407
|
const next = PERMISSION_MODES[(idx + 1) % PERMISSION_MODES.length]!
|
|
373
|
-
// Sync to engine
|
|
374
|
-
engine.getPermission().
|
|
408
|
+
// Sync to engine — use setMode for proper mode semantics
|
|
409
|
+
engine.getPermission().setMode(next)
|
|
375
410
|
return next
|
|
376
411
|
})
|
|
377
412
|
return
|
|
@@ -441,17 +476,12 @@ export function App({
|
|
|
441
476
|
</Box>
|
|
442
477
|
)}
|
|
443
478
|
<Box flexDirection="row">
|
|
444
|
-
<Text
|
|
445
|
-
color={
|
|
446
|
-
permissionMode === 'auto' ? 'green' : permissionMode === 'ask' ? 'yellow' : 'red'
|
|
447
|
-
}
|
|
448
|
-
>
|
|
479
|
+
<Text color={PERMISSION_COLORS[permissionMode]}>
|
|
449
480
|
● {PERMISSION_LABELS[permissionMode]}
|
|
450
481
|
</Text>
|
|
451
|
-
<Text dimColor> ·
|
|
452
|
-
<Text dimColor>
|
|
453
|
-
<Text dimColor> ·
|
|
454
|
-
<Text dimColor> · ← agents</Text>
|
|
482
|
+
<Text dimColor> · Shift+Tab mode</Text>
|
|
483
|
+
<Text dimColor> · Ctrl+P model · Ctrl+O expand</Text>
|
|
484
|
+
<Text dimColor> · Esc cancel · /help · ← agents</Text>
|
|
455
485
|
</Box>
|
|
456
486
|
</Box>
|
|
457
487
|
</Box>
|