@yeaft/webchat-agent 0.1.442 → 0.1.443
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/package.json +1 -1
- package/unify/tools/agent.js +89 -0
- package/unify/tools/apply-patch.js +176 -0
- package/unify/tools/ask-user.js +62 -0
- package/unify/tools/bash.js +189 -0
- package/unify/tools/close-agent.js +58 -0
- package/unify/tools/file-edit.js +120 -0
- package/unify/tools/file-read.js +125 -0
- package/unify/tools/file-write.js +73 -0
- package/unify/tools/glob.js +143 -0
- package/unify/tools/grep.js +268 -0
- package/unify/tools/history-search.js +69 -0
- package/unify/tools/image-generation.js +97 -0
- package/unify/tools/index.js +92 -0
- package/unify/tools/js-repl.js +122 -0
- package/unify/tools/list-agents.js +56 -0
- package/unify/tools/list-dir.js +106 -0
- package/unify/tools/memory-read.js +91 -0
- package/unify/tools/memory-search.js +101 -0
- package/unify/tools/memory-write.js +114 -0
- package/unify/tools/notebook-edit.js +132 -0
- package/unify/tools/request-permissions.js +60 -0
- package/unify/tools/send-message.js +62 -0
- package/unify/tools/task-tools.js +358 -0
- package/unify/tools/tool-search.js +97 -0
- package/unify/tools/view-image.js +117 -0
- package/unify/tools/wait-agent.js +84 -0
- package/unify/tools/web-fetch.js +131 -0
- package/unify/tools/web-search.js +80 -0
- package/unify/tools/write-stdin.js +54 -0
|
@@ -0,0 +1,358 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Task management tools — in-memory task tracking for work mode.
|
|
3
|
+
*
|
|
4
|
+
* Tasks are organized in a simple flat list with status tracking.
|
|
5
|
+
* Persisted only in memory for the session duration.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { defineTool } from './types.js';
|
|
9
|
+
import { randomUUID } from 'crypto';
|
|
10
|
+
|
|
11
|
+
/** In-memory task store. */
|
|
12
|
+
const tasks = new Map();
|
|
13
|
+
|
|
14
|
+
/** Plan text (free-form markdown). */
|
|
15
|
+
let currentPlan = '';
|
|
16
|
+
|
|
17
|
+
/** Get task store for other tools. */
|
|
18
|
+
export function getTaskStore() {
|
|
19
|
+
return tasks;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function getPlan() {
|
|
23
|
+
return currentPlan;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// ─── TaskCreate ─────────────────────────────────────────
|
|
27
|
+
|
|
28
|
+
export const taskCreate = defineTool({
|
|
29
|
+
name: 'TaskCreate',
|
|
30
|
+
description: `Create a new task for tracking work progress.
|
|
31
|
+
|
|
32
|
+
Tasks have a title, description, priority, and status.
|
|
33
|
+
Use this to break down complex work into trackable items.`,
|
|
34
|
+
parameters: {
|
|
35
|
+
type: 'object',
|
|
36
|
+
properties: {
|
|
37
|
+
title: {
|
|
38
|
+
type: 'string',
|
|
39
|
+
description: 'Short task title',
|
|
40
|
+
},
|
|
41
|
+
description: {
|
|
42
|
+
type: 'string',
|
|
43
|
+
description: 'Detailed task description',
|
|
44
|
+
},
|
|
45
|
+
priority: {
|
|
46
|
+
type: 'string',
|
|
47
|
+
enum: ['low', 'medium', 'high', 'critical'],
|
|
48
|
+
description: 'Task priority (default: "medium")',
|
|
49
|
+
},
|
|
50
|
+
parent_id: {
|
|
51
|
+
type: 'string',
|
|
52
|
+
description: 'Parent task ID for subtasks',
|
|
53
|
+
},
|
|
54
|
+
},
|
|
55
|
+
required: ['title'],
|
|
56
|
+
},
|
|
57
|
+
modes: ['work'],
|
|
58
|
+
isConcurrencySafe: () => false,
|
|
59
|
+
isReadOnly: () => false,
|
|
60
|
+
async execute(input, ctx) {
|
|
61
|
+
const { title, description, priority = 'medium', parent_id } = input;
|
|
62
|
+
if (!title) return JSON.stringify({ error: 'title is required' });
|
|
63
|
+
|
|
64
|
+
const id = `task-${randomUUID().slice(0, 8)}`;
|
|
65
|
+
const task = {
|
|
66
|
+
id,
|
|
67
|
+
title,
|
|
68
|
+
description: description || '',
|
|
69
|
+
priority,
|
|
70
|
+
status: 'pending',
|
|
71
|
+
parentId: parent_id || null,
|
|
72
|
+
createdAt: Date.now(),
|
|
73
|
+
updatedAt: Date.now(),
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
tasks.set(id, task);
|
|
77
|
+
|
|
78
|
+
return JSON.stringify({
|
|
79
|
+
success: true,
|
|
80
|
+
task: { id, title, priority, status: 'pending' },
|
|
81
|
+
message: `Task created: ${title} (${id})`,
|
|
82
|
+
});
|
|
83
|
+
},
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
// ─── TaskUpdate ─────────────────────────────────────────
|
|
87
|
+
|
|
88
|
+
export const taskUpdate = defineTool({
|
|
89
|
+
name: 'TaskUpdate',
|
|
90
|
+
description: `Update a task's status, priority, or details.
|
|
91
|
+
|
|
92
|
+
Status values: pending, in_progress, completed, blocked, cancelled`,
|
|
93
|
+
parameters: {
|
|
94
|
+
type: 'object',
|
|
95
|
+
properties: {
|
|
96
|
+
task_id: {
|
|
97
|
+
type: 'string',
|
|
98
|
+
description: 'Task ID to update',
|
|
99
|
+
},
|
|
100
|
+
status: {
|
|
101
|
+
type: 'string',
|
|
102
|
+
enum: ['pending', 'in_progress', 'completed', 'blocked', 'cancelled'],
|
|
103
|
+
description: 'New task status',
|
|
104
|
+
},
|
|
105
|
+
priority: {
|
|
106
|
+
type: 'string',
|
|
107
|
+
enum: ['low', 'medium', 'high', 'critical'],
|
|
108
|
+
description: 'New priority',
|
|
109
|
+
},
|
|
110
|
+
title: {
|
|
111
|
+
type: 'string',
|
|
112
|
+
description: 'Updated title',
|
|
113
|
+
},
|
|
114
|
+
description: {
|
|
115
|
+
type: 'string',
|
|
116
|
+
description: 'Updated description',
|
|
117
|
+
},
|
|
118
|
+
result: {
|
|
119
|
+
type: 'string',
|
|
120
|
+
description: 'Task result or completion notes',
|
|
121
|
+
},
|
|
122
|
+
},
|
|
123
|
+
required: ['task_id'],
|
|
124
|
+
},
|
|
125
|
+
modes: ['work'],
|
|
126
|
+
isConcurrencySafe: () => false,
|
|
127
|
+
isReadOnly: () => false,
|
|
128
|
+
async execute(input, ctx) {
|
|
129
|
+
const { task_id, status, priority, title, description, result } = input;
|
|
130
|
+
if (!task_id) return JSON.stringify({ error: 'task_id is required' });
|
|
131
|
+
|
|
132
|
+
const task = tasks.get(task_id);
|
|
133
|
+
if (!task) return JSON.stringify({ error: `Task not found: ${task_id}` });
|
|
134
|
+
|
|
135
|
+
if (status) task.status = status;
|
|
136
|
+
if (priority) task.priority = priority;
|
|
137
|
+
if (title) task.title = title;
|
|
138
|
+
if (description !== undefined) task.description = description;
|
|
139
|
+
if (result) task.result = result;
|
|
140
|
+
task.updatedAt = Date.now();
|
|
141
|
+
|
|
142
|
+
return JSON.stringify({
|
|
143
|
+
success: true,
|
|
144
|
+
task: { id: task.id, title: task.title, status: task.status, priority: task.priority },
|
|
145
|
+
message: `Task "${task.title}" updated`,
|
|
146
|
+
});
|
|
147
|
+
},
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
// ─── TaskList ───────────────────────────────────────────
|
|
151
|
+
|
|
152
|
+
export const taskList = defineTool({
|
|
153
|
+
name: 'TaskList',
|
|
154
|
+
description: `List all tracked tasks with their status.
|
|
155
|
+
|
|
156
|
+
Shows task IDs, titles, status, and priority. Filter by status if needed.`,
|
|
157
|
+
parameters: {
|
|
158
|
+
type: 'object',
|
|
159
|
+
properties: {
|
|
160
|
+
status: {
|
|
161
|
+
type: 'string',
|
|
162
|
+
enum: ['pending', 'in_progress', 'completed', 'blocked', 'cancelled'],
|
|
163
|
+
description: 'Filter by status (optional)',
|
|
164
|
+
},
|
|
165
|
+
include_completed: {
|
|
166
|
+
type: 'boolean',
|
|
167
|
+
description: 'Include completed tasks (default: true)',
|
|
168
|
+
},
|
|
169
|
+
},
|
|
170
|
+
},
|
|
171
|
+
modes: ['work'],
|
|
172
|
+
isConcurrencySafe: () => true,
|
|
173
|
+
isReadOnly: () => true,
|
|
174
|
+
async execute(input, ctx) {
|
|
175
|
+
const { status, include_completed = true } = input;
|
|
176
|
+
|
|
177
|
+
const taskList = [];
|
|
178
|
+
for (const [, task] of tasks) {
|
|
179
|
+
if (status && task.status !== status) continue;
|
|
180
|
+
if (!include_completed && task.status === 'completed') continue;
|
|
181
|
+
taskList.push({
|
|
182
|
+
id: task.id,
|
|
183
|
+
title: task.title,
|
|
184
|
+
status: task.status,
|
|
185
|
+
priority: task.priority,
|
|
186
|
+
parentId: task.parentId,
|
|
187
|
+
hasResult: !!task.result,
|
|
188
|
+
});
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
// Sort: in_progress first, then pending, then others
|
|
192
|
+
const ORDER = { in_progress: 0, pending: 1, blocked: 2, completed: 3, cancelled: 4 };
|
|
193
|
+
taskList.sort((a, b) => (ORDER[a.status] ?? 5) - (ORDER[b.status] ?? 5));
|
|
194
|
+
|
|
195
|
+
return JSON.stringify({
|
|
196
|
+
tasks: taskList,
|
|
197
|
+
totalCount: taskList.length,
|
|
198
|
+
summary: {
|
|
199
|
+
pending: taskList.filter(t => t.status === 'pending').length,
|
|
200
|
+
in_progress: taskList.filter(t => t.status === 'in_progress').length,
|
|
201
|
+
completed: taskList.filter(t => t.status === 'completed').length,
|
|
202
|
+
blocked: taskList.filter(t => t.status === 'blocked').length,
|
|
203
|
+
},
|
|
204
|
+
}, null, 2);
|
|
205
|
+
},
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
// ─── TaskGet ────────────────────────────────────────────
|
|
209
|
+
|
|
210
|
+
export const taskGet = defineTool({
|
|
211
|
+
name: 'TaskGet',
|
|
212
|
+
description: `Get detailed information about a specific task.`,
|
|
213
|
+
parameters: {
|
|
214
|
+
type: 'object',
|
|
215
|
+
properties: {
|
|
216
|
+
task_id: {
|
|
217
|
+
type: 'string',
|
|
218
|
+
description: 'Task ID to retrieve',
|
|
219
|
+
},
|
|
220
|
+
},
|
|
221
|
+
required: ['task_id'],
|
|
222
|
+
},
|
|
223
|
+
modes: ['work'],
|
|
224
|
+
isConcurrencySafe: () => true,
|
|
225
|
+
isReadOnly: () => true,
|
|
226
|
+
async execute(input, ctx) {
|
|
227
|
+
const { task_id } = input;
|
|
228
|
+
if (!task_id) return JSON.stringify({ error: 'task_id is required' });
|
|
229
|
+
|
|
230
|
+
const task = tasks.get(task_id);
|
|
231
|
+
if (!task) return JSON.stringify({ error: `Task not found: ${task_id}` });
|
|
232
|
+
|
|
233
|
+
// Find subtasks
|
|
234
|
+
const subtasks = [];
|
|
235
|
+
for (const [, t] of tasks) {
|
|
236
|
+
if (t.parentId === task_id) {
|
|
237
|
+
subtasks.push({ id: t.id, title: t.title, status: t.status });
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
return JSON.stringify({
|
|
242
|
+
...task,
|
|
243
|
+
subtasks,
|
|
244
|
+
}, null, 2);
|
|
245
|
+
},
|
|
246
|
+
});
|
|
247
|
+
|
|
248
|
+
// ─── FollowupTask ───────────────────────────────────────
|
|
249
|
+
|
|
250
|
+
export const followupTask = defineTool({
|
|
251
|
+
name: 'FollowupTask',
|
|
252
|
+
description: `Create a follow-up task linked to an existing task.
|
|
253
|
+
|
|
254
|
+
Use when a completed task reveals additional work needed.
|
|
255
|
+
The new task is linked as a child of the original.`,
|
|
256
|
+
parameters: {
|
|
257
|
+
type: 'object',
|
|
258
|
+
properties: {
|
|
259
|
+
parent_task_id: {
|
|
260
|
+
type: 'string',
|
|
261
|
+
description: 'ID of the original task',
|
|
262
|
+
},
|
|
263
|
+
title: {
|
|
264
|
+
type: 'string',
|
|
265
|
+
description: 'Follow-up task title',
|
|
266
|
+
},
|
|
267
|
+
description: {
|
|
268
|
+
type: 'string',
|
|
269
|
+
description: 'Why this follow-up is needed',
|
|
270
|
+
},
|
|
271
|
+
priority: {
|
|
272
|
+
type: 'string',
|
|
273
|
+
enum: ['low', 'medium', 'high', 'critical'],
|
|
274
|
+
},
|
|
275
|
+
},
|
|
276
|
+
required: ['parent_task_id', 'title'],
|
|
277
|
+
},
|
|
278
|
+
modes: ['work'],
|
|
279
|
+
isConcurrencySafe: () => false,
|
|
280
|
+
isReadOnly: () => false,
|
|
281
|
+
async execute(input, ctx) {
|
|
282
|
+
const { parent_task_id, title, description, priority = 'medium' } = input;
|
|
283
|
+
if (!parent_task_id) return JSON.stringify({ error: 'parent_task_id is required' });
|
|
284
|
+
if (!title) return JSON.stringify({ error: 'title is required' });
|
|
285
|
+
|
|
286
|
+
const parent = tasks.get(parent_task_id);
|
|
287
|
+
if (!parent) return JSON.stringify({ error: `Parent task not found: ${parent_task_id}` });
|
|
288
|
+
|
|
289
|
+
const id = `task-${randomUUID().slice(0, 8)}`;
|
|
290
|
+
const task = {
|
|
291
|
+
id,
|
|
292
|
+
title,
|
|
293
|
+
description: description || `Follow-up to: ${parent.title}`,
|
|
294
|
+
priority,
|
|
295
|
+
status: 'pending',
|
|
296
|
+
parentId: parent_task_id,
|
|
297
|
+
createdAt: Date.now(),
|
|
298
|
+
updatedAt: Date.now(),
|
|
299
|
+
};
|
|
300
|
+
|
|
301
|
+
tasks.set(id, task);
|
|
302
|
+
|
|
303
|
+
return JSON.stringify({
|
|
304
|
+
success: true,
|
|
305
|
+
task: { id, title, priority, status: 'pending', parentId: parent_task_id },
|
|
306
|
+
message: `Follow-up task created: ${title} (linked to ${parent.title})`,
|
|
307
|
+
});
|
|
308
|
+
},
|
|
309
|
+
});
|
|
310
|
+
|
|
311
|
+
// ─── UpdatePlan ─────────────────────────────────────────
|
|
312
|
+
|
|
313
|
+
export const updatePlan = defineTool({
|
|
314
|
+
name: 'UpdatePlan',
|
|
315
|
+
description: `Update or view the current execution plan.
|
|
316
|
+
|
|
317
|
+
The plan is a free-form markdown document that describes the overall
|
|
318
|
+
approach, steps, and status of the current work.`,
|
|
319
|
+
parameters: {
|
|
320
|
+
type: 'object',
|
|
321
|
+
properties: {
|
|
322
|
+
action: {
|
|
323
|
+
type: 'string',
|
|
324
|
+
enum: ['view', 'update', 'append'],
|
|
325
|
+
description: '"view" shows current plan, "update" replaces it, "append" adds to it',
|
|
326
|
+
},
|
|
327
|
+
content: {
|
|
328
|
+
type: 'string',
|
|
329
|
+
description: 'Plan content (for "update" and "append" actions)',
|
|
330
|
+
},
|
|
331
|
+
},
|
|
332
|
+
required: ['action'],
|
|
333
|
+
},
|
|
334
|
+
modes: ['work'],
|
|
335
|
+
isConcurrencySafe: () => false,
|
|
336
|
+
isReadOnly: (input) => input?.action === 'view',
|
|
337
|
+
async execute(input, ctx) {
|
|
338
|
+
const { action, content } = input;
|
|
339
|
+
|
|
340
|
+
switch (action) {
|
|
341
|
+
case 'view':
|
|
342
|
+
return currentPlan || '(No plan set yet)';
|
|
343
|
+
|
|
344
|
+
case 'update':
|
|
345
|
+
if (!content) return JSON.stringify({ error: 'content is required for "update"' });
|
|
346
|
+
currentPlan = content;
|
|
347
|
+
return JSON.stringify({ success: true, message: 'Plan updated', length: content.length });
|
|
348
|
+
|
|
349
|
+
case 'append':
|
|
350
|
+
if (!content) return JSON.stringify({ error: 'content is required for "append"' });
|
|
351
|
+
currentPlan = currentPlan ? `${currentPlan}\n\n${content}` : content;
|
|
352
|
+
return JSON.stringify({ success: true, message: 'Plan updated (appended)', length: currentPlan.length });
|
|
353
|
+
|
|
354
|
+
default:
|
|
355
|
+
return JSON.stringify({ error: `Unknown action: ${action}` });
|
|
356
|
+
}
|
|
357
|
+
},
|
|
358
|
+
});
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* tool-search.js — Search available tools by name or description.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { defineTool } from './types.js';
|
|
6
|
+
|
|
7
|
+
export default defineTool({
|
|
8
|
+
name: 'ToolSearch',
|
|
9
|
+
description: `Search available tools by name or description keyword.
|
|
10
|
+
|
|
11
|
+
Use when you're unsure which tool to use for a task.
|
|
12
|
+
Returns matching tools with their descriptions and parameters.`,
|
|
13
|
+
parameters: {
|
|
14
|
+
type: 'object',
|
|
15
|
+
properties: {
|
|
16
|
+
query: {
|
|
17
|
+
type: 'string',
|
|
18
|
+
description: 'Search keyword to match against tool names and descriptions',
|
|
19
|
+
},
|
|
20
|
+
mode: {
|
|
21
|
+
type: 'string',
|
|
22
|
+
enum: ['chat', 'work'],
|
|
23
|
+
description: 'Filter by mode (optional)',
|
|
24
|
+
},
|
|
25
|
+
},
|
|
26
|
+
required: ['query'],
|
|
27
|
+
},
|
|
28
|
+
modes: ['chat', 'work'],
|
|
29
|
+
isConcurrencySafe: () => true,
|
|
30
|
+
isReadOnly: () => true,
|
|
31
|
+
async execute(input, ctx) {
|
|
32
|
+
const { query, mode } = input;
|
|
33
|
+
if (!query) return JSON.stringify({ error: 'query is required' });
|
|
34
|
+
|
|
35
|
+
// Access the tool registry through the engine context
|
|
36
|
+
// Since we don't have direct registry access, list what we know
|
|
37
|
+
const lowerQuery = query.toLowerCase();
|
|
38
|
+
|
|
39
|
+
// Get all tool definitions from the registry if available
|
|
40
|
+
// This is a self-referential tool — it describes the tools available to this engine
|
|
41
|
+
const toolList = [
|
|
42
|
+
{ name: 'AskUser', description: 'Ask the user a question', modes: ['chat', 'work'] },
|
|
43
|
+
{ name: 'MemoryRead', description: 'Read from memory system', modes: ['chat', 'work'] },
|
|
44
|
+
{ name: 'MemoryWrite', description: 'Write to memory system', modes: ['chat', 'work'] },
|
|
45
|
+
{ name: 'MemorySearch', description: 'Search memory entries', modes: ['chat', 'work'] },
|
|
46
|
+
{ name: 'WebSearch', description: 'Search the web', modes: ['chat', 'work'] },
|
|
47
|
+
{ name: 'WebFetch', description: 'Fetch web page content', modes: ['chat', 'work'] },
|
|
48
|
+
{ name: 'HistorySearch', description: 'Search conversation history', modes: ['chat', 'work'] },
|
|
49
|
+
{ name: 'Bash', description: 'Execute shell commands', modes: ['work'] },
|
|
50
|
+
{ name: 'FileRead', description: 'Read file with line numbers', modes: ['work'] },
|
|
51
|
+
{ name: 'FileWrite', description: 'Write/create files', modes: ['work'] },
|
|
52
|
+
{ name: 'FileEdit', description: 'Surgical string replacement in files', modes: ['work'] },
|
|
53
|
+
{ name: 'Glob', description: 'Find files by pattern', modes: ['work'] },
|
|
54
|
+
{ name: 'Grep', description: 'Search file contents', modes: ['work'] },
|
|
55
|
+
{ name: 'ListDir', description: 'List directory contents', modes: ['work'] },
|
|
56
|
+
{ name: 'ApplyPatch', description: 'Apply unified diff patches', modes: ['work'] },
|
|
57
|
+
{ name: 'Agent', description: 'Create sub-agents', modes: ['work'] },
|
|
58
|
+
{ name: 'SendMessage', description: 'Send message to sub-agent', modes: ['work'] },
|
|
59
|
+
{ name: 'WaitAgent', description: 'Wait for sub-agent result', modes: ['work'] },
|
|
60
|
+
{ name: 'CloseAgent', description: 'Close a sub-agent', modes: ['work'] },
|
|
61
|
+
{ name: 'ListAgents', description: 'List all sub-agents', modes: ['work'] },
|
|
62
|
+
{ name: 'TaskCreate', description: 'Create a task', modes: ['work'] },
|
|
63
|
+
{ name: 'TaskUpdate', description: 'Update task status', modes: ['work'] },
|
|
64
|
+
{ name: 'TaskList', description: 'List all tasks', modes: ['work'] },
|
|
65
|
+
{ name: 'TaskGet', description: 'Get task details', modes: ['work'] },
|
|
66
|
+
{ name: 'FollowupTask', description: 'Create follow-up task', modes: ['work'] },
|
|
67
|
+
{ name: 'UpdatePlan', description: 'View/update execution plan', modes: ['work'] },
|
|
68
|
+
{ name: 'JsRepl', description: 'JavaScript REPL evaluation', modes: ['chat', 'work'] },
|
|
69
|
+
{ name: 'JsReplReset', description: 'Reset REPL state', modes: ['chat', 'work'] },
|
|
70
|
+
{ name: 'NotebookEdit', description: 'Edit Jupyter notebooks', modes: ['work'] },
|
|
71
|
+
{ name: 'ImageGeneration', description: 'Generate images from text', modes: ['chat', 'work'] },
|
|
72
|
+
{ name: 'ViewImage', description: 'View image metadata', modes: ['chat', 'work'] },
|
|
73
|
+
{ name: 'RequestPermissions', description: 'Request dangerous operation permissions', modes: ['work'] },
|
|
74
|
+
{ name: 'WriteStdin', description: 'Write to running process stdin', modes: ['work'] },
|
|
75
|
+
{ name: 'Skill', description: 'Load skills from library', modes: ['chat', 'work'] },
|
|
76
|
+
{ name: 'EnterWorktree', description: 'Create git worktree', modes: ['work'] },
|
|
77
|
+
{ name: 'ExitWorktree', description: 'Exit git worktree', modes: ['work'] },
|
|
78
|
+
{ name: 'mcp_list_tools', description: 'List MCP server tools', modes: ['chat', 'work'] },
|
|
79
|
+
{ name: 'mcp_call_tool', description: 'Call MCP server tool', modes: ['chat', 'work'] },
|
|
80
|
+
];
|
|
81
|
+
|
|
82
|
+
let results = toolList.filter(t =>
|
|
83
|
+
t.name.toLowerCase().includes(lowerQuery) ||
|
|
84
|
+
t.description.toLowerCase().includes(lowerQuery)
|
|
85
|
+
);
|
|
86
|
+
|
|
87
|
+
if (mode) {
|
|
88
|
+
results = results.filter(t => t.modes.includes(mode));
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
return JSON.stringify({
|
|
92
|
+
results,
|
|
93
|
+
totalResults: results.length,
|
|
94
|
+
query,
|
|
95
|
+
}, null, 2);
|
|
96
|
+
},
|
|
97
|
+
});
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* view-image.js — Read and describe an image file.
|
|
3
|
+
*
|
|
4
|
+
* Returns image metadata (dimensions, format, size).
|
|
5
|
+
* In a full multimodal integration, would pass the image to the LLM.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { defineTool } from './types.js';
|
|
9
|
+
import { stat, readFile } from 'fs/promises';
|
|
10
|
+
import { existsSync } from 'fs';
|
|
11
|
+
import { resolve, extname } from 'path';
|
|
12
|
+
|
|
13
|
+
/** Supported image formats. */
|
|
14
|
+
const IMAGE_EXTS = new Set(['.png', '.jpg', '.jpeg', '.gif', '.bmp', '.webp', '.svg', '.ico']);
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Parse basic image dimensions from headers.
|
|
18
|
+
*/
|
|
19
|
+
function parseImageDimensions(buffer, ext) {
|
|
20
|
+
try {
|
|
21
|
+
if (ext === '.png') {
|
|
22
|
+
// PNG: width at offset 16, height at 20 (big-endian 32-bit)
|
|
23
|
+
if (buffer.length >= 24) {
|
|
24
|
+
return { width: buffer.readUInt32BE(16), height: buffer.readUInt32BE(20) };
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
if (ext === '.jpg' || ext === '.jpeg') {
|
|
28
|
+
// JPEG: scan for SOF0 marker (0xFF 0xC0)
|
|
29
|
+
for (let i = 0; i < buffer.length - 9; i++) {
|
|
30
|
+
if (buffer[i] === 0xFF && (buffer[i + 1] === 0xC0 || buffer[i + 1] === 0xC2)) {
|
|
31
|
+
return {
|
|
32
|
+
height: buffer.readUInt16BE(i + 5),
|
|
33
|
+
width: buffer.readUInt16BE(i + 7),
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
if (ext === '.gif') {
|
|
39
|
+
// GIF: width at offset 6, height at 8 (little-endian 16-bit)
|
|
40
|
+
if (buffer.length >= 10) {
|
|
41
|
+
return { width: buffer.readUInt16LE(6), height: buffer.readUInt16LE(8) };
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
} catch {
|
|
45
|
+
// Dimension parsing is best-effort
|
|
46
|
+
}
|
|
47
|
+
return null;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export default defineTool({
|
|
51
|
+
name: 'ViewImage',
|
|
52
|
+
description: `View an image file and get its metadata.
|
|
53
|
+
|
|
54
|
+
Returns image format, dimensions, and file size.
|
|
55
|
+
Supports PNG, JPEG, GIF, BMP, WebP, SVG, and ICO.`,
|
|
56
|
+
parameters: {
|
|
57
|
+
type: 'object',
|
|
58
|
+
properties: {
|
|
59
|
+
file_path: {
|
|
60
|
+
type: 'string',
|
|
61
|
+
description: 'Path to the image file',
|
|
62
|
+
},
|
|
63
|
+
},
|
|
64
|
+
required: ['file_path'],
|
|
65
|
+
},
|
|
66
|
+
modes: ['chat', 'work'],
|
|
67
|
+
isConcurrencySafe: () => true,
|
|
68
|
+
isReadOnly: () => true,
|
|
69
|
+
async execute(input, ctx) {
|
|
70
|
+
const { file_path } = input;
|
|
71
|
+
if (!file_path) return JSON.stringify({ error: 'file_path is required' });
|
|
72
|
+
|
|
73
|
+
const cwd = ctx?.cwd || process.cwd();
|
|
74
|
+
const absPath = resolve(cwd, file_path);
|
|
75
|
+
|
|
76
|
+
if (!existsSync(absPath)) {
|
|
77
|
+
return JSON.stringify({ error: `Image not found: ${absPath}` });
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const ext = extname(absPath).toLowerCase();
|
|
81
|
+
if (!IMAGE_EXTS.has(ext)) {
|
|
82
|
+
return JSON.stringify({ error: `Not a recognized image format: ${ext}` });
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
try {
|
|
86
|
+
const fileStat = await stat(absPath);
|
|
87
|
+
const buffer = await readFile(absPath);
|
|
88
|
+
|
|
89
|
+
const dimensions = parseImageDimensions(buffer, ext);
|
|
90
|
+
|
|
91
|
+
const result = {
|
|
92
|
+
path: absPath,
|
|
93
|
+
format: ext.slice(1).toUpperCase(),
|
|
94
|
+
size: fileStat.size,
|
|
95
|
+
sizeFormatted: fileStat.size < 1024 ? `${fileStat.size}B`
|
|
96
|
+
: fileStat.size < 1024 * 1024 ? `${(fileStat.size / 1024).toFixed(1)}KB`
|
|
97
|
+
: `${(fileStat.size / 1024 / 1024).toFixed(1)}MB`,
|
|
98
|
+
modified: fileStat.mtime.toISOString(),
|
|
99
|
+
};
|
|
100
|
+
|
|
101
|
+
if (dimensions) {
|
|
102
|
+
result.width = dimensions.width;
|
|
103
|
+
result.height = dimensions.height;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// For SVG, include a text preview
|
|
107
|
+
if (ext === '.svg') {
|
|
108
|
+
const svgText = buffer.toString('utf-8');
|
|
109
|
+
result.preview = svgText.slice(0, 500);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
return JSON.stringify(result, null, 2);
|
|
113
|
+
} catch (err) {
|
|
114
|
+
return JSON.stringify({ error: `Failed to read image: ${err.message}` });
|
|
115
|
+
}
|
|
116
|
+
},
|
|
117
|
+
});
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* wait-agent.js — Wait for a sub-agent to complete and get its result.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { defineTool } from './types.js';
|
|
6
|
+
import { getAgentRegistry } from './agent.js';
|
|
7
|
+
|
|
8
|
+
export default defineTool({
|
|
9
|
+
name: 'WaitAgent',
|
|
10
|
+
description: `Wait for a sub-agent to complete its task and retrieve the result.
|
|
11
|
+
|
|
12
|
+
Returns the agent's final result or current status if still running.
|
|
13
|
+
Use after sending a task to an agent via SendMessage.`,
|
|
14
|
+
parameters: {
|
|
15
|
+
type: 'object',
|
|
16
|
+
properties: {
|
|
17
|
+
agent_id: {
|
|
18
|
+
type: 'string',
|
|
19
|
+
description: 'The sub-agent ID to wait for',
|
|
20
|
+
},
|
|
21
|
+
timeout_ms: {
|
|
22
|
+
type: 'number',
|
|
23
|
+
description: 'Maximum time to wait in milliseconds (default: 30000)',
|
|
24
|
+
},
|
|
25
|
+
},
|
|
26
|
+
required: ['agent_id'],
|
|
27
|
+
},
|
|
28
|
+
modes: ['work'],
|
|
29
|
+
isConcurrencySafe: () => true,
|
|
30
|
+
isReadOnly: () => true,
|
|
31
|
+
async execute(input, ctx) {
|
|
32
|
+
const { agent_id, timeout_ms = 30000 } = input;
|
|
33
|
+
if (!agent_id) return JSON.stringify({ error: 'agent_id is required' });
|
|
34
|
+
|
|
35
|
+
const agents = getAgentRegistry();
|
|
36
|
+
const agent = agents.get(agent_id);
|
|
37
|
+
|
|
38
|
+
if (!agent) {
|
|
39
|
+
return JSON.stringify({ error: `Agent not found: ${agent_id}` });
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// If already completed, return result immediately
|
|
43
|
+
if (agent.status === 'completed' || agent.status === 'closed') {
|
|
44
|
+
return JSON.stringify({
|
|
45
|
+
agentId: agent_id,
|
|
46
|
+
name: agent.name,
|
|
47
|
+
status: agent.status,
|
|
48
|
+
result: agent.result,
|
|
49
|
+
messages: agent.messages.length,
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// Wait for completion with timeout
|
|
54
|
+
const deadline = Date.now() + timeout_ms;
|
|
55
|
+
while (Date.now() < deadline) {
|
|
56
|
+
if (agent.status === 'completed' || agent.status === 'closed') {
|
|
57
|
+
return JSON.stringify({
|
|
58
|
+
agentId: agent_id,
|
|
59
|
+
name: agent.name,
|
|
60
|
+
status: agent.status,
|
|
61
|
+
result: agent.result,
|
|
62
|
+
messages: agent.messages.length,
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// Check abort signal
|
|
67
|
+
if (ctx?.signal?.aborted) {
|
|
68
|
+
return JSON.stringify({ error: 'Wait cancelled', agentId: agent_id });
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// Poll every 500ms
|
|
72
|
+
await new Promise(r => setTimeout(r, 500));
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
return JSON.stringify({
|
|
76
|
+
agentId: agent_id,
|
|
77
|
+
name: agent.name,
|
|
78
|
+
status: agent.status,
|
|
79
|
+
timedOut: true,
|
|
80
|
+
message: `Agent "${agent.name}" is still running after ${timeout_ms}ms`,
|
|
81
|
+
messages: agent.messages.length,
|
|
82
|
+
});
|
|
83
|
+
},
|
|
84
|
+
});
|