@yeaft/webchat-agent 0.1.441 → 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/claude.js +41 -1
- 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
package/claude.js
CHANGED
|
@@ -5,6 +5,24 @@ import ctx from './context.js';
|
|
|
5
5
|
import { sendConversationList, sendOutput, sendError, handleAskUserQuestion } from './conversation.js';
|
|
6
6
|
import { startSubagentWatcher, stopSubagentWatcher, cleanupSubagentWatchers } from './subagent.js';
|
|
7
7
|
|
|
8
|
+
/**
|
|
9
|
+
* Detect whether a user message is a Claude Code compact summary.
|
|
10
|
+
* These appear after context compaction and should not be displayed in the UI.
|
|
11
|
+
*
|
|
12
|
+
* @param {string} text — user message content
|
|
13
|
+
* @returns {boolean}
|
|
14
|
+
*/
|
|
15
|
+
function isCompactSummary(text) {
|
|
16
|
+
if (!text || text.length < 200) return false;
|
|
17
|
+
// Claude Code compact summary always starts with this exact text
|
|
18
|
+
if (text.includes('This session is being continued from a previous conversation')) return true;
|
|
19
|
+
// Alternate compact summary indicator (Claude Code uses <system-reminder> blocks)
|
|
20
|
+
if (text.includes('The summary below covers the earlier portion of the conversation')) return true;
|
|
21
|
+
// Context compaction with numbered sections (1. Primary Request, 2. Key Technical Concepts, etc.)
|
|
22
|
+
if (/^[\s\S]*Summary:[\s\S]*\d+\.\s+(Primary Request|Key Technical|Current Work)/m.test(text)) return true;
|
|
23
|
+
return false;
|
|
24
|
+
}
|
|
25
|
+
|
|
8
26
|
/**
|
|
9
27
|
* Determine maxContextTokens and autoCompactThreshold from model name.
|
|
10
28
|
* Returns defaults suitable for the model's context window size.
|
|
@@ -423,7 +441,7 @@ async function processClaudeOutput(conversationId, claudeQuery, state) {
|
|
|
423
441
|
|
|
424
442
|
// 过滤 compact summary 消息(compact_boundary 之后的 user 消息)
|
|
425
443
|
if (message.type === 'user' && state._compactSummaryPending) {
|
|
426
|
-
console.log(`[${conversationId}] Filtering compact summary message`);
|
|
444
|
+
console.log(`[${conversationId}] Filtering compact summary message (pending flag)`);
|
|
427
445
|
continue;
|
|
428
446
|
}
|
|
429
447
|
// compact 后的 <local-command-stdout>Compacted </local-command-stdout> 标记 summary 结束
|
|
@@ -431,6 +449,28 @@ async function processClaudeOutput(conversationId, claudeQuery, state) {
|
|
|
431
449
|
state._compactSummaryPending = false;
|
|
432
450
|
}
|
|
433
451
|
|
|
452
|
+
// 兜底过滤: Claude Code 的 compact summary 有时不触发 compact_boundary,
|
|
453
|
+
// 直接以 user 消息形式出现。通过内容特征检测过滤。
|
|
454
|
+
if (message.type === 'user') {
|
|
455
|
+
const userText = typeof message.content === 'string'
|
|
456
|
+
? message.content
|
|
457
|
+
: (Array.isArray(message.content) ? message.content.map(b => b.text || '').join('') : '');
|
|
458
|
+
if (userText && isCompactSummary(userText)) {
|
|
459
|
+
console.log(`[${conversationId}] Filtering compact summary message (content match)`);
|
|
460
|
+
// 补发 compact 完成通知(如果之前没发过)
|
|
461
|
+
if (!state._compactCompleteSent) {
|
|
462
|
+
state._compactCompleteSent = true;
|
|
463
|
+
ctx.sendToServer({
|
|
464
|
+
type: 'compact_status',
|
|
465
|
+
conversationId,
|
|
466
|
+
status: 'completed',
|
|
467
|
+
message: 'Context compacted successfully'
|
|
468
|
+
});
|
|
469
|
+
}
|
|
470
|
+
continue;
|
|
471
|
+
}
|
|
472
|
+
}
|
|
473
|
+
|
|
434
474
|
// 捕获 result 消息中的 usage 信息
|
|
435
475
|
if (message.type === 'result') {
|
|
436
476
|
// Log result message keys for debugging slash command output
|
package/package.json
CHANGED
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* agent.js — Create a sub-agent for parallel task execution.
|
|
3
|
+
*
|
|
4
|
+
* Sub-agents run in isolated contexts and can be assigned
|
|
5
|
+
* independent tasks. They communicate via send-message/wait-agent.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { defineTool } from './types.js';
|
|
9
|
+
import { randomUUID } from 'crypto';
|
|
10
|
+
|
|
11
|
+
/** In-memory sub-agent registry. */
|
|
12
|
+
const agents = new Map();
|
|
13
|
+
|
|
14
|
+
/** Get the global agents map for other tools to access. */
|
|
15
|
+
export function getAgentRegistry() {
|
|
16
|
+
return agents;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export default defineTool({
|
|
20
|
+
name: 'Agent',
|
|
21
|
+
description: `Create a sub-agent to work on an independent task in parallel.
|
|
22
|
+
|
|
23
|
+
Sub-agents run in their own context and can be given specific tasks.
|
|
24
|
+
Use for parallel execution of independent subtasks.
|
|
25
|
+
|
|
26
|
+
Guidelines:
|
|
27
|
+
- Give each agent a clear, focused task description
|
|
28
|
+
- Use unique, descriptive names
|
|
29
|
+
- Sub-agents share the same tools but have independent conversations
|
|
30
|
+
- Use SendMessage to communicate with agents, WaitAgent to collect results
|
|
31
|
+
- Close agents with CloseAgent when done`,
|
|
32
|
+
parameters: {
|
|
33
|
+
type: 'object',
|
|
34
|
+
properties: {
|
|
35
|
+
name: {
|
|
36
|
+
type: 'string',
|
|
37
|
+
description: 'A descriptive name for the sub-agent (e.g. "test-writer", "refactor-auth")',
|
|
38
|
+
},
|
|
39
|
+
task: {
|
|
40
|
+
type: 'string',
|
|
41
|
+
description: 'The task description for the sub-agent',
|
|
42
|
+
},
|
|
43
|
+
cwd: {
|
|
44
|
+
type: 'string',
|
|
45
|
+
description: 'Working directory for the sub-agent (optional, defaults to parent cwd)',
|
|
46
|
+
},
|
|
47
|
+
},
|
|
48
|
+
required: ['name', 'task'],
|
|
49
|
+
},
|
|
50
|
+
modes: ['work'],
|
|
51
|
+
isConcurrencySafe: () => false,
|
|
52
|
+
isReadOnly: () => false,
|
|
53
|
+
async execute(input, ctx) {
|
|
54
|
+
const { name, task, cwd } = input;
|
|
55
|
+
if (!name) return JSON.stringify({ error: 'name is required' });
|
|
56
|
+
if (!task) return JSON.stringify({ error: 'task is required' });
|
|
57
|
+
|
|
58
|
+
// Check for name collision
|
|
59
|
+
for (const [, agent] of agents) {
|
|
60
|
+
if (agent.name === name && agent.status !== 'closed') {
|
|
61
|
+
return JSON.stringify({
|
|
62
|
+
error: `Agent "${name}" already exists. Close it first or use a different name.`,
|
|
63
|
+
agentId: agent.id,
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const agentId = `agent-${randomUUID().slice(0, 8)}`;
|
|
69
|
+
const agent = {
|
|
70
|
+
id: agentId,
|
|
71
|
+
name,
|
|
72
|
+
task,
|
|
73
|
+
cwd: cwd || ctx?.cwd || process.cwd(),
|
|
74
|
+
status: 'created',
|
|
75
|
+
messages: [],
|
|
76
|
+
result: null,
|
|
77
|
+
createdAt: Date.now(),
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
agents.set(agentId, agent);
|
|
81
|
+
|
|
82
|
+
return JSON.stringify({
|
|
83
|
+
success: true,
|
|
84
|
+
agentId,
|
|
85
|
+
name,
|
|
86
|
+
message: `Sub-agent "${name}" created (${agentId}). Use SendMessage to give it work.`,
|
|
87
|
+
});
|
|
88
|
+
},
|
|
89
|
+
});
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* apply-patch.js — Apply a unified diff patch to files.
|
|
3
|
+
*
|
|
4
|
+
* Parses unified diff format and applies changes to the target files.
|
|
5
|
+
* Supports multiple files in a single patch.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { defineTool } from './types.js';
|
|
9
|
+
import { readFile, writeFile, mkdir } from 'fs/promises';
|
|
10
|
+
import { existsSync } from 'fs';
|
|
11
|
+
import { resolve, dirname } from 'path';
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Parse a unified diff into hunks.
|
|
15
|
+
* @param {string} patch
|
|
16
|
+
* @returns {Array<{ file: string, hunks: Array }>}
|
|
17
|
+
*/
|
|
18
|
+
function parsePatch(patch) {
|
|
19
|
+
const files = [];
|
|
20
|
+
const lines = patch.split('\n');
|
|
21
|
+
let currentFile = null;
|
|
22
|
+
let currentHunk = null;
|
|
23
|
+
|
|
24
|
+
for (let i = 0; i < lines.length; i++) {
|
|
25
|
+
const line = lines[i];
|
|
26
|
+
|
|
27
|
+
// File header: --- a/path or +++ b/path
|
|
28
|
+
if (line.startsWith('--- ')) {
|
|
29
|
+
// Next line should be +++
|
|
30
|
+
continue;
|
|
31
|
+
}
|
|
32
|
+
if (line.startsWith('+++ ')) {
|
|
33
|
+
let filePath = line.slice(4).trim();
|
|
34
|
+
// Strip a/ or b/ prefix
|
|
35
|
+
if (filePath.startsWith('b/')) filePath = filePath.slice(2);
|
|
36
|
+
if (filePath === '/dev/null') continue;
|
|
37
|
+
|
|
38
|
+
currentFile = { file: filePath, hunks: [] };
|
|
39
|
+
files.push(currentFile);
|
|
40
|
+
continue;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// Hunk header: @@ -start,count +start,count @@
|
|
44
|
+
const hunkMatch = line.match(/^@@ -(\d+)(?:,(\d+))? \+(\d+)(?:,(\d+))? @@/);
|
|
45
|
+
if (hunkMatch) {
|
|
46
|
+
if (!currentFile) continue;
|
|
47
|
+
currentHunk = {
|
|
48
|
+
oldStart: parseInt(hunkMatch[1], 10),
|
|
49
|
+
oldCount: hunkMatch[2] ? parseInt(hunkMatch[2], 10) : 1,
|
|
50
|
+
newStart: parseInt(hunkMatch[3], 10),
|
|
51
|
+
newCount: hunkMatch[4] ? parseInt(hunkMatch[4], 10) : 1,
|
|
52
|
+
lines: [],
|
|
53
|
+
};
|
|
54
|
+
currentFile.hunks.push(currentHunk);
|
|
55
|
+
continue;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// Diff content lines
|
|
59
|
+
if (currentHunk && (line.startsWith('+') || line.startsWith('-') || line.startsWith(' ') || line === '')) {
|
|
60
|
+
currentHunk.lines.push(line);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
return files;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Apply hunks to file content.
|
|
69
|
+
*/
|
|
70
|
+
function applyHunks(content, hunks) {
|
|
71
|
+
const lines = content.split('\n');
|
|
72
|
+
let offset = 0; // tracks line number shifts from previous hunks
|
|
73
|
+
|
|
74
|
+
for (const hunk of hunks) {
|
|
75
|
+
const startLine = hunk.oldStart - 1 + offset; // 0-based
|
|
76
|
+
const newLines = [];
|
|
77
|
+
let removedCount = 0;
|
|
78
|
+
|
|
79
|
+
for (const diffLine of hunk.lines) {
|
|
80
|
+
if (diffLine.startsWith('+')) {
|
|
81
|
+
newLines.push(diffLine.slice(1));
|
|
82
|
+
} else if (diffLine.startsWith('-')) {
|
|
83
|
+
removedCount++;
|
|
84
|
+
} else if (diffLine.startsWith(' ') || diffLine === '') {
|
|
85
|
+
newLines.push(diffLine.startsWith(' ') ? diffLine.slice(1) : diffLine);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// Replace the old lines with new lines
|
|
90
|
+
lines.splice(startLine, removedCount + (hunk.oldCount - removedCount > 0 ? hunk.oldCount - removedCount : 0), ...newLines);
|
|
91
|
+
offset += newLines.length - hunk.oldCount;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
return lines.join('\n');
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
export default defineTool({
|
|
98
|
+
name: 'ApplyPatch',
|
|
99
|
+
description: `Apply a unified diff patch to files.
|
|
100
|
+
|
|
101
|
+
Parses unified diff format (like git diff output) and applies changes.
|
|
102
|
+
Supports patching multiple files in a single diff.
|
|
103
|
+
|
|
104
|
+
Guidelines:
|
|
105
|
+
- Provide standard unified diff format (--- a/file, +++ b/file, @@ hunks)
|
|
106
|
+
- Ensure the diff matches the current file content exactly
|
|
107
|
+
- New files are created automatically with parent directories`,
|
|
108
|
+
parameters: {
|
|
109
|
+
type: 'object',
|
|
110
|
+
properties: {
|
|
111
|
+
patch: {
|
|
112
|
+
type: 'string',
|
|
113
|
+
description: 'The unified diff patch content',
|
|
114
|
+
},
|
|
115
|
+
},
|
|
116
|
+
required: ['patch'],
|
|
117
|
+
},
|
|
118
|
+
modes: ['work'],
|
|
119
|
+
isConcurrencySafe: () => false,
|
|
120
|
+
isReadOnly: () => false,
|
|
121
|
+
isDestructive: () => false,
|
|
122
|
+
async execute(input, ctx) {
|
|
123
|
+
const { patch } = input;
|
|
124
|
+
if (!patch) return JSON.stringify({ error: 'patch is required' });
|
|
125
|
+
|
|
126
|
+
const cwd = ctx?.cwd || process.cwd();
|
|
127
|
+
|
|
128
|
+
try {
|
|
129
|
+
const fileDiffs = parsePatch(patch);
|
|
130
|
+
|
|
131
|
+
if (fileDiffs.length === 0) {
|
|
132
|
+
return JSON.stringify({ error: 'No valid file diffs found in patch' });
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
const results = [];
|
|
136
|
+
|
|
137
|
+
for (const fileDiff of fileDiffs) {
|
|
138
|
+
const absPath = resolve(cwd, fileDiff.file);
|
|
139
|
+
|
|
140
|
+
try {
|
|
141
|
+
let content;
|
|
142
|
+
if (existsSync(absPath)) {
|
|
143
|
+
content = await readFile(absPath, 'utf-8');
|
|
144
|
+
} else {
|
|
145
|
+
// New file
|
|
146
|
+
await mkdir(dirname(absPath), { recursive: true });
|
|
147
|
+
content = '';
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
const newContent = applyHunks(content, fileDiff.hunks);
|
|
151
|
+
await writeFile(absPath, newContent, 'utf-8');
|
|
152
|
+
|
|
153
|
+
results.push({
|
|
154
|
+
file: fileDiff.file,
|
|
155
|
+
success: true,
|
|
156
|
+
hunks: fileDiff.hunks.length,
|
|
157
|
+
});
|
|
158
|
+
} catch (err) {
|
|
159
|
+
results.push({
|
|
160
|
+
file: fileDiff.file,
|
|
161
|
+
success: false,
|
|
162
|
+
error: err.message,
|
|
163
|
+
});
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
const successCount = results.filter(r => r.success).length;
|
|
168
|
+
return JSON.stringify({
|
|
169
|
+
results,
|
|
170
|
+
summary: `Applied patch to ${successCount}/${results.length} files`,
|
|
171
|
+
}, null, 2);
|
|
172
|
+
} catch (err) {
|
|
173
|
+
return JSON.stringify({ error: `Failed to apply patch: ${err.message}` });
|
|
174
|
+
}
|
|
175
|
+
},
|
|
176
|
+
});
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ask-user.js — Ask the user a question and wait for their response.
|
|
3
|
+
*
|
|
4
|
+
* In Unify mode this tool sends a question through the web-bridge
|
|
5
|
+
* and blocks (via Promise) until the user answers. The answer is
|
|
6
|
+
* returned as the tool result.
|
|
7
|
+
*
|
|
8
|
+
* Reference: yeaft-unify-design.md §8
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { defineTool } from './types.js';
|
|
12
|
+
import { randomUUID } from 'crypto';
|
|
13
|
+
|
|
14
|
+
export default defineTool({
|
|
15
|
+
name: 'AskUser',
|
|
16
|
+
description: `Ask the user a question and wait for their response.
|
|
17
|
+
|
|
18
|
+
Use this tool when you need additional information or clarification from the user.
|
|
19
|
+
The user will see your question in the chat interface and can type a response.
|
|
20
|
+
|
|
21
|
+
Guidelines:
|
|
22
|
+
- Ask specific, focused questions
|
|
23
|
+
- Provide context about why you need the information
|
|
24
|
+
- If presenting options, list them clearly
|
|
25
|
+
- Don't use this for rhetorical questions — only when you genuinely need user input`,
|
|
26
|
+
parameters: {
|
|
27
|
+
type: 'object',
|
|
28
|
+
properties: {
|
|
29
|
+
question: {
|
|
30
|
+
type: 'string',
|
|
31
|
+
description: 'The question to ask the user',
|
|
32
|
+
},
|
|
33
|
+
options: {
|
|
34
|
+
type: 'array',
|
|
35
|
+
items: { type: 'string' },
|
|
36
|
+
description: 'Optional list of choices for the user to pick from',
|
|
37
|
+
},
|
|
38
|
+
},
|
|
39
|
+
required: ['question'],
|
|
40
|
+
},
|
|
41
|
+
modes: ['chat', 'work'],
|
|
42
|
+
isConcurrencySafe: () => false,
|
|
43
|
+
isReadOnly: () => true,
|
|
44
|
+
async execute(input, ctx) {
|
|
45
|
+
const { question, options } = input;
|
|
46
|
+
if (!question) return JSON.stringify({ error: 'question is required' });
|
|
47
|
+
|
|
48
|
+
// Generate a unique request ID for this ask
|
|
49
|
+
const requestId = `ask_${randomUUID().slice(0, 8)}`;
|
|
50
|
+
|
|
51
|
+
// In a full web-bridge integration, this would send an ask_user event
|
|
52
|
+
// and await the answer. For now, return a formatted prompt that the
|
|
53
|
+
// LLM can see — the web-bridge handles the ask flow externally.
|
|
54
|
+
return JSON.stringify({
|
|
55
|
+
type: 'ask_user',
|
|
56
|
+
requestId,
|
|
57
|
+
question,
|
|
58
|
+
...(options ? { options } : {}),
|
|
59
|
+
message: `Question sent to user: "${question}"${options ? ` [Options: ${options.join(', ')}]` : ''}`,
|
|
60
|
+
});
|
|
61
|
+
},
|
|
62
|
+
});
|
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* bash.js — Execute shell commands.
|
|
3
|
+
*
|
|
4
|
+
* Spawns a child process to run shell commands with timeout, output
|
|
5
|
+
* truncation, working directory support, and cancellation via AbortSignal.
|
|
6
|
+
*
|
|
7
|
+
* Modeled after Claude Code's Bash tool implementation.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { defineTool } from './types.js';
|
|
11
|
+
import { spawn } from 'child_process';
|
|
12
|
+
import { existsSync } from 'fs';
|
|
13
|
+
import { resolve } from 'path';
|
|
14
|
+
|
|
15
|
+
/** Max output size in bytes before truncation (256 KB). */
|
|
16
|
+
const MAX_OUTPUT = 256 * 1024;
|
|
17
|
+
|
|
18
|
+
/** Default timeout in ms (2 minutes). */
|
|
19
|
+
const DEFAULT_TIMEOUT_MS = 120_000;
|
|
20
|
+
|
|
21
|
+
/** Max timeout in ms (10 minutes). */
|
|
22
|
+
const MAX_TIMEOUT_MS = 600_000;
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Run a command in a child process.
|
|
26
|
+
* @returns {Promise<{ stdout: string, stderr: string, exitCode: number, timedOut: boolean }>}
|
|
27
|
+
*/
|
|
28
|
+
function runCommand(command, { cwd, timeout, signal }) {
|
|
29
|
+
return new Promise((resolve, reject) => {
|
|
30
|
+
const shell = process.env.SHELL || '/bin/bash';
|
|
31
|
+
const proc = spawn(shell, ['-c', command], {
|
|
32
|
+
cwd,
|
|
33
|
+
env: { ...process.env, TERM: 'dumb', FORCE_COLOR: '0' },
|
|
34
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
35
|
+
timeout,
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
let stdout = '';
|
|
39
|
+
let stderr = '';
|
|
40
|
+
let stdoutTruncated = false;
|
|
41
|
+
let stderrTruncated = false;
|
|
42
|
+
let timedOut = false;
|
|
43
|
+
|
|
44
|
+
proc.stdout.on('data', (chunk) => {
|
|
45
|
+
if (stdout.length < MAX_OUTPUT) {
|
|
46
|
+
stdout += chunk.toString();
|
|
47
|
+
if (stdout.length > MAX_OUTPUT) {
|
|
48
|
+
stdout = stdout.slice(0, MAX_OUTPUT);
|
|
49
|
+
stdoutTruncated = true;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
proc.stderr.on('data', (chunk) => {
|
|
55
|
+
if (stderr.length < MAX_OUTPUT) {
|
|
56
|
+
stderr += chunk.toString();
|
|
57
|
+
if (stderr.length > MAX_OUTPUT) {
|
|
58
|
+
stderr = stderr.slice(0, MAX_OUTPUT);
|
|
59
|
+
stderrTruncated = true;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
// Handle abort signal
|
|
65
|
+
const onAbort = () => {
|
|
66
|
+
try { proc.kill('SIGTERM'); } catch {}
|
|
67
|
+
setTimeout(() => {
|
|
68
|
+
try { proc.kill('SIGKILL'); } catch {}
|
|
69
|
+
}, 2000);
|
|
70
|
+
};
|
|
71
|
+
if (signal) {
|
|
72
|
+
if (signal.aborted) { onAbort(); return; }
|
|
73
|
+
signal.addEventListener('abort', onAbort, { once: true });
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
proc.on('close', (code) => {
|
|
77
|
+
if (signal) signal.removeEventListener('abort', onAbort);
|
|
78
|
+
resolve({
|
|
79
|
+
stdout: stdoutTruncated ? stdout + '\n... (output truncated)' : stdout,
|
|
80
|
+
stderr: stderrTruncated ? stderr + '\n... (stderr truncated)' : stderr,
|
|
81
|
+
exitCode: code ?? 1,
|
|
82
|
+
timedOut,
|
|
83
|
+
});
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
proc.on('error', (err) => {
|
|
87
|
+
if (signal) signal.removeEventListener('abort', onAbort);
|
|
88
|
+
if (err.code === 'ETIMEDOUT' || err.killed) {
|
|
89
|
+
timedOut = true;
|
|
90
|
+
resolve({
|
|
91
|
+
stdout,
|
|
92
|
+
stderr: stderr + `\nProcess timed out after ${timeout}ms`,
|
|
93
|
+
exitCode: 124,
|
|
94
|
+
timedOut: true,
|
|
95
|
+
});
|
|
96
|
+
} else {
|
|
97
|
+
resolve({
|
|
98
|
+
stdout,
|
|
99
|
+
stderr: `Error spawning process: ${err.message}`,
|
|
100
|
+
exitCode: 1,
|
|
101
|
+
timedOut: false,
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
});
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
export default defineTool({
|
|
109
|
+
name: 'Bash',
|
|
110
|
+
description: `Execute a shell command and return its output.
|
|
111
|
+
|
|
112
|
+
Use this tool to run CLI commands, scripts, and system operations.
|
|
113
|
+
|
|
114
|
+
Guidelines:
|
|
115
|
+
- Commands run in the working directory (cwd from context)
|
|
116
|
+
- Timeout defaults to 2 minutes (max 10 minutes)
|
|
117
|
+
- Large outputs are truncated at 256KB
|
|
118
|
+
- Use absolute paths when possible
|
|
119
|
+
- Avoid interactive commands (no stdin support)
|
|
120
|
+
- For long-running tasks, consider redirecting output to a file
|
|
121
|
+
- stderr is captured separately and included in the result`,
|
|
122
|
+
parameters: {
|
|
123
|
+
type: 'object',
|
|
124
|
+
properties: {
|
|
125
|
+
command: {
|
|
126
|
+
type: 'string',
|
|
127
|
+
description: 'The shell command to execute',
|
|
128
|
+
},
|
|
129
|
+
cwd: {
|
|
130
|
+
type: 'string',
|
|
131
|
+
description: 'Working directory for the command (default: engine cwd)',
|
|
132
|
+
},
|
|
133
|
+
timeout_ms: {
|
|
134
|
+
type: 'number',
|
|
135
|
+
description: `Timeout in milliseconds (default: ${DEFAULT_TIMEOUT_MS}, max: ${MAX_TIMEOUT_MS})`,
|
|
136
|
+
},
|
|
137
|
+
},
|
|
138
|
+
required: ['command'],
|
|
139
|
+
},
|
|
140
|
+
modes: ['work'],
|
|
141
|
+
isConcurrencySafe: () => false,
|
|
142
|
+
isReadOnly: () => false,
|
|
143
|
+
isDestructive: (input) => {
|
|
144
|
+
if (!input?.command) return false;
|
|
145
|
+
const cmd = input.command.toLowerCase();
|
|
146
|
+
return cmd.includes('rm ') || cmd.includes('rmdir') ||
|
|
147
|
+
cmd.includes('git reset --hard') || cmd.includes('git clean') ||
|
|
148
|
+
cmd.includes('dd ') || cmd.includes('mkfs') ||
|
|
149
|
+
cmd.includes('> /dev/') || cmd.includes('chmod 000');
|
|
150
|
+
},
|
|
151
|
+
async execute(input, ctx) {
|
|
152
|
+
const { command, cwd: inputCwd, timeout_ms } = input;
|
|
153
|
+
if (!command) return JSON.stringify({ error: 'command is required' });
|
|
154
|
+
|
|
155
|
+
// Resolve working directory
|
|
156
|
+
const cwd = inputCwd
|
|
157
|
+
? resolve(inputCwd)
|
|
158
|
+
: (ctx?.cwd || process.cwd());
|
|
159
|
+
|
|
160
|
+
if (!existsSync(cwd)) {
|
|
161
|
+
return JSON.stringify({ error: `Working directory does not exist: ${cwd}` });
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// Clamp timeout
|
|
165
|
+
const timeout = Math.min(Math.max(timeout_ms || DEFAULT_TIMEOUT_MS, 1000), MAX_TIMEOUT_MS);
|
|
166
|
+
|
|
167
|
+
try {
|
|
168
|
+
const result = await runCommand(command, {
|
|
169
|
+
cwd,
|
|
170
|
+
timeout,
|
|
171
|
+
signal: ctx?.signal,
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
// Format output similar to Claude Code
|
|
175
|
+
const parts = [];
|
|
176
|
+
if (result.stdout) parts.push(result.stdout);
|
|
177
|
+
if (result.stderr) parts.push(`STDERR:\n${result.stderr}`);
|
|
178
|
+
if (result.timedOut) parts.push(`\n(Command timed out after ${timeout}ms)`);
|
|
179
|
+
|
|
180
|
+
const output = parts.join('\n') || '(no output)';
|
|
181
|
+
|
|
182
|
+
return result.exitCode === 0
|
|
183
|
+
? output
|
|
184
|
+
: `Exit code: ${result.exitCode}\n${output}`;
|
|
185
|
+
} catch (err) {
|
|
186
|
+
return JSON.stringify({ error: `Bash execution failed: ${err.message}` });
|
|
187
|
+
}
|
|
188
|
+
},
|
|
189
|
+
});
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* close-agent.js — Close a sub-agent and clean up.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { defineTool } from './types.js';
|
|
6
|
+
import { getAgentRegistry } from './agent.js';
|
|
7
|
+
|
|
8
|
+
export default defineTool({
|
|
9
|
+
name: 'CloseAgent',
|
|
10
|
+
description: `Close a sub-agent and release its resources.
|
|
11
|
+
|
|
12
|
+
Use when a sub-agent's task is complete or no longer needed.
|
|
13
|
+
The agent's result (if any) is returned before closing.`,
|
|
14
|
+
parameters: {
|
|
15
|
+
type: 'object',
|
|
16
|
+
properties: {
|
|
17
|
+
agent_id: {
|
|
18
|
+
type: 'string',
|
|
19
|
+
description: 'The sub-agent ID to close',
|
|
20
|
+
},
|
|
21
|
+
result: {
|
|
22
|
+
type: 'string',
|
|
23
|
+
description: 'Optional final result to set before closing',
|
|
24
|
+
},
|
|
25
|
+
},
|
|
26
|
+
required: ['agent_id'],
|
|
27
|
+
},
|
|
28
|
+
modes: ['work'],
|
|
29
|
+
isConcurrencySafe: () => false,
|
|
30
|
+
isReadOnly: () => false,
|
|
31
|
+
async execute(input, ctx) {
|
|
32
|
+
const { agent_id, result } = 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 (result) {
|
|
43
|
+
agent.result = result;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const finalResult = agent.result;
|
|
47
|
+
agent.status = 'closed';
|
|
48
|
+
|
|
49
|
+
return JSON.stringify({
|
|
50
|
+
success: true,
|
|
51
|
+
agentId: agent_id,
|
|
52
|
+
name: agent.name,
|
|
53
|
+
result: finalResult,
|
|
54
|
+
messages: agent.messages.length,
|
|
55
|
+
message: `Agent "${agent.name}" closed`,
|
|
56
|
+
});
|
|
57
|
+
},
|
|
58
|
+
});
|