wormclaude 1.0.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/tools.js ADDED
@@ -0,0 +1,1136 @@
1
+ // Araçlar — WormClaude araç setinin birebir uyarlaması (WormClaude'a göre).
2
+ // OpenAI function şemaları + Node executor'ları. Açıklamalar ve davranışlar
3
+ // WormClaude'un gerçek prompt.ts/şemalarından alınmıştır; sadece marka adı ve
4
+ // WormClaude'da bulunmayan araç referansları (Agent tool, sandbox) uyarlandı.
5
+ import { execSync, spawn } from 'node:child_process';
6
+ import * as fs from 'node:fs';
7
+ import * as os from 'node:os';
8
+ import * as path from 'node:path';
9
+ import { loadConfig } from './api.js';
10
+ import { runAgentLoop } from './agent.js';
11
+ import { tasks } from './tasks.js';
12
+ import { getMcpToolSchemas, callMcpTool } from './mcp.js';
13
+ import { getAutoSkills, getSkill, buildSkillPrompt } from './skills.js';
14
+ // Agent/alt-agent araçlarının backend'e ulaşması için config. cli.tsx başlangıçta
15
+ // setToolConfig ile aynı (mutable) config nesnesini verir → /config değişiklikleri görülür.
16
+ let toolConfig = null;
17
+ export function setToolConfig(c) { toolConfig = c; }
18
+ function cfg() { return toolConfig ?? loadConfig(); }
19
+ let todosStore = []; // TodoWrite durumu
20
+ export function getTodos() { return todosStore; }
21
+ const MAX_LINES_TO_READ = 2000;
22
+ const DEFAULT_BASH_TIMEOUT_MS = 120000;
23
+ const MAX_BASH_TIMEOUT_MS = 600000;
24
+ // WormClaude davranışı: bir dosyayı düzenlemeden/üzerine yazmadan önce
25
+ // en az bir kez okumuş olman gerekir. Okunan dosyaları burada izliyoruz.
26
+ const readFiles = new Set();
27
+ function norm(p) {
28
+ try {
29
+ return path.resolve(p).toLowerCase();
30
+ }
31
+ catch {
32
+ return p;
33
+ }
34
+ }
35
+ // ── Araç açıklamaları (WormClaude prompt.ts'lerinden birebir) ────────────────
36
+ const BASH_DESCRIPTION = `Executes a given bash command and returns its output.
37
+
38
+ The working directory persists between commands, but shell state does not. The shell environment is initialized from the user's profile (bash or zsh).
39
+
40
+ IMPORTANT: Avoid using this tool to run \`find\`, \`grep\`, \`cat\`, \`head\`, \`tail\`, \`sed\`, \`awk\`, or \`echo\` commands, unless explicitly instructed or after you have verified that a dedicated tool cannot accomplish your task. Instead, use the appropriate dedicated tool as this will provide a much better experience for the user:
41
+
42
+ - File search: Use Glob (NOT find or ls)
43
+ - Content search: Use Grep (NOT grep or rg)
44
+ - Read files: Use Read (NOT cat/head/tail)
45
+ - Edit files: Use Edit (NOT sed/awk)
46
+ - Write files: Use Write (NOT echo >/cat <<EOF)
47
+ - Communication: Output text directly (NOT echo/printf)
48
+
49
+ While the Bash tool can do similar things, it's better to use the built-in tools as they provide a better user experience and make it easier to review tool calls and give permission.
50
+
51
+ # Instructions
52
+ - If your command will create new directories or files, first use this tool to run \`ls\` to verify the parent directory exists and is the correct location.
53
+ - Always quote file paths that contain spaces with double quotes in your command (e.g., cd "path with spaces/file.txt")
54
+ - Try to maintain your current working directory throughout the session by using absolute paths and avoiding usage of \`cd\`. You may use \`cd\` if the User explicitly requests it.
55
+ - You may specify an optional timeout in milliseconds (up to ${MAX_BASH_TIMEOUT_MS}ms / ${MAX_BASH_TIMEOUT_MS / 60000} minutes). By default, your command will timeout after ${DEFAULT_BASH_TIMEOUT_MS}ms (${DEFAULT_BASH_TIMEOUT_MS / 60000} minutes).
56
+ - When issuing multiple commands:
57
+ - If the commands depend on each other and must run sequentially, use a single Bash call with '&&' to chain them together.
58
+ - Use ';' only when you need to run commands sequentially but don't care if earlier commands fail.
59
+ - DO NOT use newlines to separate commands (newlines are ok in quoted strings).
60
+ - For git commands:
61
+ - Prefer to create a new commit rather than amending an existing commit.
62
+ - Before running destructive operations (e.g., git reset --hard, git push --force, git checkout --), consider whether there is a safer alternative that achieves the same goal. Only use destructive operations when they are truly the best approach.
63
+ - Never skip hooks (--no-verify) or bypass signing unless the user has explicitly asked for it. If a hook fails, investigate and fix the underlying issue.
64
+ - Avoid unnecessary \`sleep\` commands: do not sleep between commands that can run immediately — just run them.`;
65
+ const READ_DESCRIPTION = `Reads a file from the local filesystem. You can access any file directly by using this tool.
66
+ Assume this tool is able to read all files on the machine. If the User provides a path to a file assume that path is valid. It is okay to read a file that does not exist; an error will be returned.
67
+
68
+ Usage:
69
+ - The file_path parameter must be an absolute path, not a relative path
70
+ - By default, it reads up to ${MAX_LINES_TO_READ} lines starting from the beginning of the file
71
+ - You can optionally specify a line offset and limit (especially handy for long files), but it's recommended to read the whole file by not providing these parameters
72
+ - Results are returned using cat -n format, with line numbers starting at 1
73
+ - This tool can only read files, not directories. To read a directory, use an ls command via the Bash tool.
74
+ - If you read a file that exists but has empty contents you will receive a system reminder warning in place of file contents.`;
75
+ const WRITE_DESCRIPTION = `Writes a file to the local filesystem.
76
+
77
+ Usage:
78
+ - This tool will overwrite the existing file if there is one at the provided path.
79
+ - If this is an existing file, you MUST use the Read tool first to read the file's contents. This tool will fail if you did not read the file first.
80
+ - Prefer the Edit tool for modifying existing files — it only sends the diff. Only use this tool to create new files or for complete rewrites.
81
+ - NEVER create documentation files (*.md) or README files unless explicitly requested by the User.
82
+ - Only use emojis if the user explicitly requests it. Avoid writing emojis to files unless asked.`;
83
+ const EDIT_DESCRIPTION = `Performs exact string replacements in files.
84
+
85
+ Usage:
86
+ - You must use your \`Read\` tool at least once in the conversation before editing. This tool will error if you attempt an edit without reading the file.
87
+ - When editing text from Read tool output, ensure you preserve the exact indentation (tabs/spaces) as it appears AFTER the line number prefix. The line number prefix format is: spaces + line number + tab. Everything after that is the actual file content to match. Never include any part of the line number prefix in the old_string or new_string.
88
+ - ALWAYS prefer editing existing files in the codebase. NEVER write new files unless explicitly required.
89
+ - Only use emojis if the user explicitly requests it. Avoid adding emojis to files unless asked.
90
+ - The edit will FAIL if \`old_string\` is not unique in the file. Either provide a larger string with more surrounding context to make it unique or use \`replace_all\` to change every instance of \`old_string\`.
91
+ - Use \`replace_all\` for replacing and renaming strings across the file. This parameter is useful if you want to rename a variable for instance.`;
92
+ const GLOB_DESCRIPTION = `- Fast file pattern matching tool that works with any codebase size
93
+ - Supports glob patterns like "**/*.js" or "src/**/*.ts"
94
+ - Returns matching file paths sorted by modification time
95
+ - Use this tool when you need to find files by name patterns`;
96
+ const GREP_DESCRIPTION = `A powerful search tool built on ripgrep
97
+
98
+ Usage:
99
+ - ALWAYS use Grep for search tasks. NEVER invoke \`grep\` or \`rg\` as a Bash command. The Grep tool has been optimized for correct permissions and access.
100
+ - Supports full regex syntax (e.g., "log.*Error", "function\\s+\\w+")
101
+ - Filter files with glob parameter (e.g., "*.js", "**/*.tsx") or type parameter (e.g., "js", "py", "rust")
102
+ - Output modes: "content" shows matching lines, "files_with_matches" shows only file paths (default), "count" shows match counts
103
+ - Pattern syntax: literal braces need escaping (use \`interface\\{\\}\` to find \`interface{}\` in Go code)
104
+ - Multiline matching: By default patterns match within single lines only. For cross-line patterns like \`struct \\{[\\s\\S]*?field\`, use \`multiline: true\``;
105
+ const AGENT_DESCRIPTION = `Launch a new sub-agent to handle a complex, multi-step task autonomously.
106
+
107
+ The sub-agent runs its own tool-calling loop (Bash, Read, Write, Edit, Glob, Grep, WebFetch) against the same model, then returns a final summary. Use this to:
108
+ - Delegate a self-contained subtask (e.g. "find and fix all usages of X", "investigate why Y fails")
109
+ - Parallelize independent work by launching several background agents at once (coordinator pattern)
110
+
111
+ Usage:
112
+ - description: a short (3-5 word) description of the task
113
+ - prompt: the detailed, self-contained instruction for the sub-agent (it has no memory of this conversation — include all needed context, file paths, and exactly what to return)
114
+ - run_in_background: if true, returns a task id immediately and the agent works in the background; read its result later with TaskOutput. If false (default), blocks until the sub-agent finishes and returns its result directly.
115
+ - The sub-agent CANNOT launch further sub-agents (no nesting).`;
116
+ const TASKOUTPUT_DESCRIPTION = `Read the current output and status of a background task (shell command or sub-agent) previously started with run_in_background. Returns the task status and its accumulated output.`;
117
+ const WEBFETCH_DESCRIPTION = `- Fetches content from a specified URL
118
+ - Takes a URL and a prompt as input
119
+ - Fetches the URL content, converts HTML to text
120
+ - Returns the page content for analysis against the prompt
121
+ - Use this tool when you need to retrieve and analyze web content
122
+
123
+ Usage notes:
124
+ - The URL must be a fully-formed valid URL
125
+ - HTTP URLs will be automatically upgraded to HTTPS
126
+ - The prompt should describe what information you want to extract from the page
127
+ - This tool is read-only and does not modify any files
128
+ - For GitHub URLs, prefer using the gh CLI via Bash instead (e.g., gh pr view, gh issue view, gh api).`;
129
+ // ── Şemalar (OpenAI function-calling formatı) ─────────────────────────────────
130
+ export const toolSchemas = [
131
+ {
132
+ type: 'function',
133
+ function: {
134
+ name: 'Bash',
135
+ description: BASH_DESCRIPTION,
136
+ parameters: {
137
+ type: 'object',
138
+ properties: {
139
+ command: { type: 'string', description: 'The command to execute' },
140
+ timeout: { type: 'number', description: `Optional timeout in milliseconds (max ${MAX_BASH_TIMEOUT_MS})` },
141
+ description: {
142
+ type: 'string',
143
+ description: 'Clear, concise description of what this command does in active voice. Never use words like "complex" or "risk" in the description - just describe what it does.',
144
+ },
145
+ run_in_background: {
146
+ type: 'boolean',
147
+ description: "Set to true to run this command in the background. Returns a task id immediately; read output later with TaskOutput. You don't need '&' at the end.",
148
+ },
149
+ },
150
+ required: ['command'],
151
+ },
152
+ },
153
+ },
154
+ {
155
+ type: 'function',
156
+ function: {
157
+ name: 'Read',
158
+ description: READ_DESCRIPTION,
159
+ parameters: {
160
+ type: 'object',
161
+ properties: {
162
+ file_path: { type: 'string', description: 'The absolute path to the file to read' },
163
+ offset: {
164
+ type: 'number',
165
+ description: 'The line number to start reading from. Only provide if the file is too large to read at once',
166
+ },
167
+ limit: {
168
+ type: 'number',
169
+ description: 'The number of lines to read. Only provide if the file is too large to read at once.',
170
+ },
171
+ },
172
+ required: ['file_path'],
173
+ },
174
+ },
175
+ },
176
+ {
177
+ type: 'function',
178
+ function: {
179
+ name: 'Write',
180
+ description: WRITE_DESCRIPTION,
181
+ parameters: {
182
+ type: 'object',
183
+ properties: {
184
+ file_path: {
185
+ type: 'string',
186
+ description: 'The absolute path to the file to write (must be absolute, not relative)',
187
+ },
188
+ content: { type: 'string', description: 'The content to write to the file' },
189
+ },
190
+ required: ['file_path', 'content'],
191
+ },
192
+ },
193
+ },
194
+ {
195
+ type: 'function',
196
+ function: {
197
+ name: 'Edit',
198
+ description: EDIT_DESCRIPTION,
199
+ parameters: {
200
+ type: 'object',
201
+ properties: {
202
+ file_path: { type: 'string', description: 'The absolute path to the file to modify' },
203
+ old_string: { type: 'string', description: 'The text to replace' },
204
+ new_string: {
205
+ type: 'string',
206
+ description: 'The text to replace it with (must be different from old_string)',
207
+ },
208
+ replace_all: {
209
+ type: 'boolean',
210
+ description: 'Replace all occurrences of old_string (default false)',
211
+ },
212
+ },
213
+ required: ['file_path', 'old_string', 'new_string'],
214
+ },
215
+ },
216
+ },
217
+ {
218
+ type: 'function',
219
+ function: {
220
+ name: 'Glob',
221
+ description: GLOB_DESCRIPTION,
222
+ parameters: {
223
+ type: 'object',
224
+ properties: {
225
+ pattern: { type: 'string', description: 'The glob pattern to match files against' },
226
+ path: {
227
+ type: 'string',
228
+ description: 'The directory to search in. If not specified, the current working directory will be used. IMPORTANT: Omit this field to use the default directory. DO NOT enter "undefined" or "null" - simply omit it for the default behavior. Must be a valid directory path if provided.',
229
+ },
230
+ },
231
+ required: ['pattern'],
232
+ },
233
+ },
234
+ },
235
+ {
236
+ type: 'function',
237
+ function: {
238
+ name: 'Grep',
239
+ description: GREP_DESCRIPTION,
240
+ parameters: {
241
+ type: 'object',
242
+ properties: {
243
+ pattern: { type: 'string', description: 'The regular expression pattern to search for in file contents' },
244
+ path: {
245
+ type: 'string',
246
+ description: 'File or directory to search in. Defaults to current working directory.',
247
+ },
248
+ glob: {
249
+ type: 'string',
250
+ description: 'Glob pattern to filter files (e.g. "*.js", "*.{ts,tsx}")',
251
+ },
252
+ output_mode: {
253
+ type: 'string',
254
+ enum: ['content', 'files_with_matches', 'count'],
255
+ description: 'Output mode: "content" shows matching lines (supports -A/-B/-C context, -n line numbers, head_limit), "files_with_matches" shows file paths (supports head_limit), "count" shows match counts. Defaults to "files_with_matches".',
256
+ },
257
+ '-B': { type: 'number', description: 'Number of lines to show before each match. Requires output_mode: "content".' },
258
+ '-A': { type: 'number', description: 'Number of lines to show after each match. Requires output_mode: "content".' },
259
+ '-C': { type: 'number', description: 'Alias for context.' },
260
+ context: { type: 'number', description: 'Number of lines to show before and after each match. Requires output_mode: "content".' },
261
+ '-n': { type: 'boolean', description: 'Show line numbers in output. Requires output_mode: "content". Defaults to true.' },
262
+ '-i': { type: 'boolean', description: 'Case insensitive search' },
263
+ type: {
264
+ type: 'string',
265
+ description: 'File type to search. Common types: js, ts, py, rust, go, java, etc.',
266
+ },
267
+ head_limit: {
268
+ type: 'number',
269
+ description: 'Limit output to first N lines/entries. Works across all output modes. Defaults to 250 when unspecified.',
270
+ },
271
+ offset: { type: 'number', description: 'Skip first N lines/entries before applying head_limit. Defaults to 0.' },
272
+ multiline: {
273
+ type: 'boolean',
274
+ description: 'Enable multiline mode where . matches newlines and patterns can span lines. Default: false.',
275
+ },
276
+ },
277
+ required: ['pattern'],
278
+ },
279
+ },
280
+ },
281
+ {
282
+ type: 'function',
283
+ function: {
284
+ name: 'WebFetch',
285
+ description: WEBFETCH_DESCRIPTION,
286
+ parameters: {
287
+ type: 'object',
288
+ properties: {
289
+ url: { type: 'string', description: 'The URL to fetch content from' },
290
+ prompt: { type: 'string', description: 'The prompt to run on the fetched content' },
291
+ },
292
+ required: ['url', 'prompt'],
293
+ },
294
+ },
295
+ },
296
+ {
297
+ type: 'function',
298
+ function: {
299
+ name: 'Agent',
300
+ description: AGENT_DESCRIPTION,
301
+ parameters: {
302
+ type: 'object',
303
+ properties: {
304
+ description: { type: 'string', description: 'A short (3-5 word) description of the task' },
305
+ prompt: { type: 'string', description: 'The detailed, self-contained task for the sub-agent to perform' },
306
+ run_in_background: { type: 'boolean', description: 'Run the sub-agent in the background and return a task id (default false)' },
307
+ },
308
+ required: ['description', 'prompt'],
309
+ },
310
+ },
311
+ },
312
+ {
313
+ type: 'function',
314
+ function: {
315
+ name: 'TaskOutput',
316
+ description: TASKOUTPUT_DESCRIPTION,
317
+ parameters: {
318
+ type: 'object',
319
+ properties: {
320
+ task_id: { type: 'string', description: 'The id of the background task to read (e.g. shell_1, agent_2)' },
321
+ },
322
+ required: ['task_id'],
323
+ },
324
+ },
325
+ },
326
+ {
327
+ type: 'function',
328
+ function: {
329
+ name: 'WebSearch',
330
+ description: 'Search the web and return top results (title, url, snippet). Use for current info, docs, or finding pages to WebFetch.',
331
+ parameters: {
332
+ type: 'object',
333
+ properties: {
334
+ query: { type: 'string', description: 'The search query' },
335
+ max_results: { type: 'number', description: 'Max results to return (default 5)' },
336
+ },
337
+ required: ['query'],
338
+ },
339
+ },
340
+ },
341
+ {
342
+ type: 'function',
343
+ function: {
344
+ name: 'TodoWrite',
345
+ description: 'Create/update a structured task list for the current work. Use for multi-step tasks to track progress. Pass the FULL list each time.',
346
+ parameters: {
347
+ type: 'object',
348
+ properties: {
349
+ todos: {
350
+ type: 'array',
351
+ description: 'The full todo list',
352
+ items: {
353
+ type: 'object',
354
+ properties: {
355
+ content: { type: 'string', description: 'Task description' },
356
+ status: { type: 'string', enum: ['pending', 'in_progress', 'completed'], description: 'Task status' },
357
+ },
358
+ required: ['content', 'status'],
359
+ },
360
+ },
361
+ },
362
+ required: ['todos'],
363
+ },
364
+ },
365
+ },
366
+ {
367
+ type: 'function',
368
+ function: {
369
+ name: 'PowerShell',
370
+ description: 'Execute a Windows PowerShell command and return its output. Use for Windows-specific tasks; otherwise prefer Bash.',
371
+ parameters: {
372
+ type: 'object',
373
+ properties: {
374
+ command: { type: 'string', description: 'The PowerShell command to execute' },
375
+ timeout: { type: 'number', description: `Optional timeout in ms (max ${MAX_BASH_TIMEOUT_MS})` },
376
+ },
377
+ required: ['command'],
378
+ },
379
+ },
380
+ },
381
+ {
382
+ type: 'function',
383
+ function: {
384
+ name: 'NotebookEdit',
385
+ description: 'Edit a Jupyter notebook (.ipynb) cell. edit_mode: replace (default), insert, or delete.',
386
+ parameters: {
387
+ type: 'object',
388
+ properties: {
389
+ notebook_path: { type: 'string', description: 'Absolute path to the .ipynb file' },
390
+ cell_index: { type: 'number', description: 'Zero-based index of the cell to edit/insert at' },
391
+ new_source: { type: 'string', description: 'New cell source (for replace/insert)' },
392
+ cell_type: { type: 'string', enum: ['code', 'markdown'], description: 'Cell type for insert' },
393
+ edit_mode: { type: 'string', enum: ['replace', 'insert', 'delete'], description: 'Edit mode (default replace)' },
394
+ },
395
+ required: ['notebook_path', 'cell_index'],
396
+ },
397
+ },
398
+ },
399
+ {
400
+ type: 'function',
401
+ function: {
402
+ name: 'REPL',
403
+ description: 'Run a code snippet in node or python and return stdout/stderr. One-shot (no persisted state).',
404
+ parameters: {
405
+ type: 'object',
406
+ properties: {
407
+ language: { type: 'string', enum: ['node', 'python'], description: 'Runtime' },
408
+ code: { type: 'string', description: 'Code to execute' },
409
+ },
410
+ required: ['language', 'code'],
411
+ },
412
+ },
413
+ },
414
+ {
415
+ type: 'function',
416
+ function: {
417
+ name: 'LSP',
418
+ description: 'Find a symbol definition or references in the codebase (heuristic, ripgrep-based — not a full language server).',
419
+ parameters: {
420
+ type: 'object',
421
+ properties: {
422
+ action: { type: 'string', enum: ['definition', 'references'], description: 'What to find' },
423
+ symbol: { type: 'string', description: 'The symbol name (function/class/variable)' },
424
+ path: { type: 'string', description: 'Base directory (default cwd)' },
425
+ },
426
+ required: ['action', 'symbol'],
427
+ },
428
+ },
429
+ },
430
+ {
431
+ type: 'function',
432
+ function: {
433
+ name: 'Sleep',
434
+ description: 'Pause for N seconds (max 60). Use sparingly, e.g. waiting for a process to settle.',
435
+ parameters: {
436
+ type: 'object',
437
+ properties: { seconds: { type: 'number', description: 'Seconds to wait (max 60)' } },
438
+ required: ['seconds'],
439
+ },
440
+ },
441
+ },
442
+ {
443
+ type: 'function',
444
+ function: {
445
+ name: 'AskUserQuestion',
446
+ description: 'Ask the user a multiple-choice question and get their answer. Use when you need a decision only the user can make.',
447
+ parameters: {
448
+ type: 'object',
449
+ properties: {
450
+ question: { type: 'string', description: 'The question to ask' },
451
+ options: {
452
+ type: 'array',
453
+ description: '2-4 options',
454
+ items: { type: 'object', properties: { label: { type: 'string' }, description: { type: 'string' } }, required: ['label'] },
455
+ },
456
+ },
457
+ required: ['question', 'options'],
458
+ },
459
+ },
460
+ },
461
+ {
462
+ type: 'function',
463
+ function: {
464
+ name: 'EnterPlanMode',
465
+ description: 'Enter read-only planning mode: investigate and write a plan; file edits and commands are blocked until ExitPlanMode is approved.',
466
+ parameters: { type: 'object', properties: {}, required: [] },
467
+ },
468
+ },
469
+ {
470
+ type: 'function',
471
+ function: {
472
+ name: 'ExitPlanMode',
473
+ description: 'Present your plan to the user for approval and exit plan mode. On approval you may proceed with edits/commands.',
474
+ parameters: {
475
+ type: 'object',
476
+ properties: { plan: { type: 'string', description: 'The plan to present (markdown)' } },
477
+ required: ['plan'],
478
+ },
479
+ },
480
+ },
481
+ ];
482
+ // autoInvoke skill'leri model'e tek bir 'Skill' aracı olarak sunar.
483
+ function skillToolSchema() {
484
+ const auto = getAutoSkills();
485
+ if (!auto.length)
486
+ return null;
487
+ const list = auto.map((s) => `- ${s.name}: ${s.description}${s.whenToUse ? ` (${s.whenToUse})` : ''}`).join('\n');
488
+ return {
489
+ type: 'function',
490
+ function: {
491
+ name: 'Skill',
492
+ description: 'Invoke a specialized skill (an expert prompt pack) when the task matches one. ' +
493
+ 'The skill runs as a focused sub-agent and returns its result. Available skills:\n' + list,
494
+ parameters: {
495
+ type: 'object',
496
+ properties: {
497
+ name: { type: 'string', enum: auto.map((s) => s.name), description: 'The skill to invoke' },
498
+ args: { type: 'string', description: 'Optional context/arguments for the skill' },
499
+ },
500
+ required: ['name'],
501
+ },
502
+ },
503
+ };
504
+ }
505
+ // Yerleşik + MCP + autoInvoke-skill araçlarının tümü. Ana döngü ve alt-agent'lar kullanır.
506
+ export function allToolSchemas() {
507
+ const sk = skillToolSchema();
508
+ return [...toolSchemas, ...getMcpToolSchemas(), ...(sk ? [sk] : [])];
509
+ }
510
+ const TOOL_META = {
511
+ Read: { readOnly: true, concurrencySafe: true },
512
+ Glob: { readOnly: true, concurrencySafe: true },
513
+ Grep: { readOnly: true, concurrencySafe: true },
514
+ WebFetch: { readOnly: true, needsPermission: true, validate: (a) => (a && a.url ? null : 'url gerekli') },
515
+ TaskOutput: { readOnly: true, concurrencySafe: true },
516
+ Bash: { needsPermission: true, validate: (a) => (a && a.command ? null : 'command gerekli') },
517
+ Write: { needsPermission: true, validate: (a) => (a && a.file_path ? null : 'file_path gerekli') },
518
+ Edit: { needsPermission: true, validate: (a) => (a && a.file_path && a.old_string != null ? null : 'file_path ve old_string gerekli') },
519
+ Agent: { validate: (a) => (a && a.prompt ? null : 'prompt gerekli') },
520
+ WebSearch: { readOnly: true, needsPermission: true, validate: (a) => (a && a.query ? null : 'query gerekli') },
521
+ TodoWrite: { readOnly: true, validate: (a) => (a && Array.isArray(a.todos) ? null : 'todos dizisi gerekli') },
522
+ PowerShell: { needsPermission: true, validate: (a) => (a && a.command ? null : 'command gerekli') },
523
+ NotebookEdit: { needsPermission: true, validate: (a) => (a && a.notebook_path ? null : 'notebook_path gerekli') },
524
+ REPL: { needsPermission: true, validate: (a) => (a && a.language && a.code ? null : 'language ve code gerekli') },
525
+ LSP: { readOnly: true, concurrencySafe: true, validate: (a) => (a && a.symbol ? null : 'symbol gerekli') },
526
+ Sleep: { readOnly: true, concurrencySafe: true },
527
+ AskUserQuestion: { readOnly: true, validate: (a) => (a && a.question && Array.isArray(a.options) ? null : 'question ve options gerekli') },
528
+ EnterPlanMode: { readOnly: true },
529
+ ExitPlanMode: { readOnly: true, validate: (a) => (a && a.plan ? null : 'plan gerekli') },
530
+ };
531
+ // Plan modu: açıkken yazma/komut araçları engellenir (ExitPlanMode onayına kadar).
532
+ let planMode = false;
533
+ export function isPlanMode() { return planMode; }
534
+ const MAX_TOOL_CONCURRENCY = Number(process.env.WORMCLAUDE_MAX_TOOL_CONCURRENCY) || 10;
535
+ export function isConcurrencySafe(name) {
536
+ return TOOL_META[name]?.concurrencySafe ?? false; // bilinmeyen/MCP → sıralı (güvenli taraf)
537
+ }
538
+ export function isReadOnly(name) {
539
+ return TOOL_META[name]?.readOnly ?? false;
540
+ }
541
+ export function needsPermission(name) {
542
+ return TOOL_META[name]?.needsPermission ?? false; // MCP/diğer → varsayılan izin gerekmez
543
+ }
544
+ export function validateToolInput(name, args) {
545
+ return TOOL_META[name]?.validate?.(args) ?? null;
546
+ }
547
+ async function execOne(call, hooks) {
548
+ let args = {};
549
+ try {
550
+ args = JSON.parse(call.args || '{}');
551
+ }
552
+ catch { }
553
+ // 1) Girdi doğrulama
554
+ const verr = validateToolInput(call.name, args);
555
+ if (verr)
556
+ return { ok: false, output: `Geçersiz girdi: ${verr}`, args };
557
+ // 2) İnteraktif/plan araçları
558
+ if (call.name === 'AskUserQuestion') {
559
+ const opts = Array.isArray(args.options) ? args.options : [];
560
+ if (hooks?.ask) {
561
+ const ch = await hooks.ask({ question: String(args.question), options: opts });
562
+ return { ok: true, output: ch, args };
563
+ }
564
+ return { ok: true, output: opts[0]?.label || '(no UI)', args };
565
+ }
566
+ if (call.name === 'EnterPlanMode') {
567
+ planMode = true;
568
+ return { ok: true, output: 'Plan modu açık. Araştır ve planı yaz; yazma/komut engelli. ExitPlanMode(plan) ile onay iste.', args };
569
+ }
570
+ if (call.name === 'ExitPlanMode') {
571
+ let approved = true;
572
+ if (hooks?.ask) {
573
+ const ch = await hooks.ask({ question: String(args.plan || ''), options: [{ label: 'Onayla / Approve' }, { label: 'Reddet / Reject' }] });
574
+ approved = /onayla|approve/i.test(ch);
575
+ }
576
+ if (approved) {
577
+ planMode = false;
578
+ return { ok: true, output: 'Plan onaylandı — uygulamaya geçebilirsin.', args };
579
+ }
580
+ return { ok: true, output: 'Plan reddedildi — plan modunda kal ve düzelt.', args };
581
+ }
582
+ // 3) Plan modu zorlaması: yazma/komut araçları engelli
583
+ if (planMode && !isReadOnly(call.name)) {
584
+ return { ok: false, output: 'Plan modunda — yazma/komut engellendi. Önce ExitPlanMode ile planı onaylat.', args };
585
+ }
586
+ // 4) İzin (gerekiyorsa)
587
+ if (needsPermission(call.name) && hooks?.confirm) {
588
+ const decision = await hooks.confirm(call, args);
589
+ if (decision !== 'allow') {
590
+ const fb = typeof decision === 'object' && decision.deny ? decision.deny : '';
591
+ return { ok: false, output: fb ? `Kullanıcı bu aracı reddetti. Bunun yerine şunu yap: ${fb}` : 'Kullanıcı bu aracı reddetti.', args };
592
+ }
593
+ }
594
+ // 5) Çalıştır
595
+ const res = await executeTool(call.name, args);
596
+ return { ...res, args };
597
+ }
598
+ // Araç çağrılarını ardışık güvenlik durumuna göre batch'ler; güvenli batch'leri
599
+ // paralel (limit MAX), diğerlerini sıralı çalıştırır. Sonuçları ORİJİNAL sırada döndürür.
600
+ export async function executeToolCalls(calls, hooks) {
601
+ const results = new Array(calls.length);
602
+ const batches = [];
603
+ calls.forEach((c, i) => {
604
+ const safe = isConcurrencySafe(c.name);
605
+ const last = batches[batches.length - 1];
606
+ if (last && last.safe === safe)
607
+ last.items.push({ c, i });
608
+ else
609
+ batches.push({ safe, items: [{ c, i }] });
610
+ });
611
+ for (const b of batches) {
612
+ if (b.safe && b.items.length > 1) {
613
+ let p = 0;
614
+ const worker = async () => {
615
+ while (p < b.items.length) {
616
+ const { c, i } = b.items[p++];
617
+ hooks?.onStart?.(c, i);
618
+ results[i] = await execOne(c, hooks);
619
+ hooks?.onResult?.(c, i, results[i]);
620
+ }
621
+ };
622
+ await Promise.all(Array.from({ length: Math.min(MAX_TOOL_CONCURRENCY, b.items.length) }, worker));
623
+ }
624
+ else {
625
+ for (const { c, i } of b.items) {
626
+ hooks?.onStart?.(c, i);
627
+ results[i] = await execOne(c, hooks);
628
+ hooks?.onResult?.(c, i, results[i]);
629
+ }
630
+ }
631
+ }
632
+ return results;
633
+ }
634
+ // Alt-agent'a verilecek araç seti (özyineleme/iç içe agent engellenir).
635
+ function subAgentTools() {
636
+ return allToolSchemas().filter((t) => t.function.name !== 'Agent' && t.function.name !== 'Skill');
637
+ }
638
+ const SUBAGENT_SYSTEM = 'You are a WormClaude sub-agent: an autonomous worker spawned to complete one specific task. ' +
639
+ 'Use your tools (Bash, Read, Write, Edit, Glob, Grep, WebFetch, TaskOutput) to do the work, ' +
640
+ 'then return a concise final report of what you did and any results requested. ' +
641
+ 'You have no memory beyond this task. Be thorough and finish the task end-to-end.';
642
+ export function toolLabel(name, args) {
643
+ try {
644
+ if (name === 'Bash')
645
+ return `Bash(${String(args.command).slice(0, 80)})`;
646
+ if (name === 'Read')
647
+ return `Read(${args.file_path})`;
648
+ if (name === 'Write')
649
+ return `Write(${args.file_path})`;
650
+ if (name === 'Edit')
651
+ return `Edit(${args.file_path})`;
652
+ if (name === 'Glob')
653
+ return `Glob(${args.pattern})`;
654
+ if (name === 'Grep')
655
+ return `Grep(${args.pattern})`;
656
+ if (name === 'WebFetch')
657
+ return `WebFetch(${args.url})`;
658
+ if (name === 'Agent')
659
+ return `Agent(${args.description || ''}${args.run_in_background ? ', bg' : ''})`;
660
+ if (name === 'TaskOutput')
661
+ return `TaskOutput(${args.task_id})`;
662
+ if (name === 'Skill')
663
+ return `Skill(${args.name})`;
664
+ if (name === 'WebSearch')
665
+ return `WebSearch(${args.query})`;
666
+ if (name === 'TodoWrite')
667
+ return `TodoWrite(${(args.todos || []).length} öğe)`;
668
+ if (name === 'PowerShell')
669
+ return `PowerShell(${String(args.command).slice(0, 60)})`;
670
+ if (name === 'NotebookEdit')
671
+ return `NotebookEdit(${args.notebook_path}, ${args.edit_mode || 'replace'})`;
672
+ if (name === 'REPL')
673
+ return `REPL(${args.language})`;
674
+ if (name === 'LSP')
675
+ return `LSP(${args.action}: ${args.symbol})`;
676
+ if (name === 'Sleep')
677
+ return `Sleep(${args.seconds}s)`;
678
+ if (name === 'AskUserQuestion')
679
+ return `AskUserQuestion(${String(args.question).slice(0, 50)})`;
680
+ if (name === 'EnterPlanMode')
681
+ return 'EnterPlanMode()';
682
+ if (name === 'ExitPlanMode')
683
+ return 'ExitPlanMode()';
684
+ if (name.startsWith('mcp__')) {
685
+ const parts = name.split('__');
686
+ return `MCP·${parts[1]}(${parts.slice(2).join('__')})`;
687
+ }
688
+ }
689
+ catch { }
690
+ return `${name}(...)`;
691
+ }
692
+ // ── Yardımcılar ───────────────────────────────────────────────────────────────
693
+ function walk(dir, out, depth = 0) {
694
+ if (depth > 12 || out.length > 20000)
695
+ return;
696
+ let entries;
697
+ try {
698
+ entries = fs.readdirSync(dir, { withFileTypes: true });
699
+ }
700
+ catch {
701
+ return;
702
+ }
703
+ for (const e of entries) {
704
+ if (e.name === 'node_modules' || e.name === '.git')
705
+ continue;
706
+ const full = path.join(dir, e.name);
707
+ if (e.isDirectory())
708
+ walk(full, out, depth + 1);
709
+ else
710
+ out.push(full);
711
+ }
712
+ }
713
+ function globToRegex(pattern) {
714
+ let re = pattern
715
+ .replace(/[.+^${}()|[\]\\]/g, '\\$&')
716
+ .replace(/\*\*/g, '')
717
+ .replace(/\*/g, '[^/\\\\]*')
718
+ .replace(//g, '.*')
719
+ .replace(/\?/g, '.');
720
+ return new RegExp(re + '$', 'i');
721
+ }
722
+ const TYPE_EXT = {
723
+ js: ['js', 'jsx', 'mjs', 'cjs'],
724
+ ts: ['ts', 'tsx', 'mts', 'cts'],
725
+ py: ['py', 'pyi'],
726
+ rust: ['rs'],
727
+ go: ['go'],
728
+ java: ['java'],
729
+ c: ['c', 'h'],
730
+ cpp: ['cpp', 'cc', 'cxx', 'hpp', 'hh'],
731
+ json: ['json'],
732
+ md: ['md', 'markdown'],
733
+ html: ['html', 'htm'],
734
+ css: ['css', 'scss', 'sass'],
735
+ sh: ['sh', 'bash', 'zsh'],
736
+ };
737
+ // ── Executor ──────────────────────────────────────────────────────────────────
738
+ export async function executeTool(name, args) {
739
+ try {
740
+ if (name === 'Bash') {
741
+ if (args.run_in_background) {
742
+ const task = tasks.create('shell', String(args.command).slice(0, 60));
743
+ const child = spawn(String(args.command), { shell: true, windowsHide: true });
744
+ task.child = child;
745
+ child.stdout?.on('data', (d) => tasks.append(task.id, d.toString()));
746
+ child.stderr?.on('data', (d) => tasks.append(task.id, d.toString()));
747
+ child.on('close', (code) => {
748
+ tasks.append(task.id, `\n[exit ${code}]`);
749
+ tasks.finish(task.id, code === 0 ? 'done' : 'error');
750
+ });
751
+ child.on('error', (e) => { tasks.append(task.id, `\n[spawn error: ${e.message}]`); tasks.finish(task.id, 'error'); });
752
+ return { ok: true, output: `Background task started: ${task.id}. Read output later with TaskOutput("${task.id}").` };
753
+ }
754
+ let timeout = Number(args.timeout) || DEFAULT_BASH_TIMEOUT_MS;
755
+ if (timeout > MAX_BASH_TIMEOUT_MS)
756
+ timeout = MAX_BASH_TIMEOUT_MS;
757
+ const out = execSync(String(args.command), {
758
+ encoding: 'utf8', timeout, maxBuffer: 10 * 1024 * 1024, windowsHide: true,
759
+ });
760
+ return { ok: true, output: (out || '(no output)').slice(0, 20000) };
761
+ }
762
+ if (name === 'Agent') {
763
+ const subMessages = [
764
+ { role: 'system', content: SUBAGENT_SYSTEM },
765
+ { role: 'user', content: String(args.prompt || '') },
766
+ ];
767
+ if (args.run_in_background) {
768
+ const task = tasks.create('agent', String(args.description || 'agent'));
769
+ (async () => {
770
+ try {
771
+ const { finalText } = await runAgentLoop({
772
+ config: cfg(),
773
+ messages: subMessages,
774
+ tools: subAgentTools(),
775
+ executeTool,
776
+ hooks: {
777
+ onText: (t) => tasks.append(task.id, t),
778
+ onToolResult: (n, _a, ok, _o) => tasks.append(task.id, `\n[tool ${n}: ${ok ? 'ok' : 'err'}]\n`),
779
+ },
780
+ });
781
+ tasks.append(task.id, `\n\n=== FINAL ===\n${finalText}`);
782
+ tasks.finish(task.id, 'done');
783
+ }
784
+ catch (e) {
785
+ tasks.append(task.id, `\n[error: ${e?.message || e}]`);
786
+ tasks.finish(task.id, 'error');
787
+ }
788
+ })();
789
+ return { ok: true, output: `Background sub-agent started: ${task.id}. Read its result with TaskOutput("${task.id}").` };
790
+ }
791
+ const { finalText } = await runAgentLoop({
792
+ config: cfg(),
793
+ messages: subMessages,
794
+ tools: subAgentTools(),
795
+ executeTool,
796
+ });
797
+ return { ok: true, output: finalText || '(sub-agent returned no text)' };
798
+ }
799
+ if (name === 'TaskOutput') {
800
+ const t = tasks.get(String(args.task_id));
801
+ if (!t)
802
+ return { ok: false, output: `No such task: ${args.task_id}. Use /tasks to list.` };
803
+ const body = t.output.slice(-9000) || '(no output yet)';
804
+ return { ok: true, output: `[${t.id}] ${t.kind} · ${t.status} · ${t.label}\n\n${body}` };
805
+ }
806
+ if (name === 'Read') {
807
+ const fp = args.file_path;
808
+ if (!fs.existsSync(fp))
809
+ return { ok: false, output: `Error: file does not exist: ${fp}` };
810
+ if (fs.statSync(fp).isDirectory())
811
+ return { ok: false, output: 'Error: path is a directory. Use an ls command via Bash to read a directory.' };
812
+ const raw = fs.readFileSync(fp, 'utf8');
813
+ readFiles.add(norm(fp));
814
+ if (raw.length === 0)
815
+ return { ok: true, output: '<system-reminder>File exists but has empty contents.</system-reminder>' };
816
+ const allLines = raw.split('\n');
817
+ const offset = Math.max(0, (Number(args.offset) || 1) - 1); // 1-tabanlı
818
+ const limit = Number(args.limit) || MAX_LINES_TO_READ;
819
+ const slice = allLines.slice(offset, offset + limit);
820
+ // cat -n formatı: sağa hizalı satır numarası + tab
821
+ const numbered = slice
822
+ .map((line, i) => `${String(offset + i + 1).padStart(6, ' ')}\t${line}`)
823
+ .join('\n');
824
+ return { ok: true, output: numbered.slice(0, 40000) };
825
+ }
826
+ if (name === 'Write') {
827
+ const fp = args.file_path;
828
+ if (fs.existsSync(fp) && !readFiles.has(norm(fp)))
829
+ return { ok: false, output: 'Error: existing file must be read first. Use the Read tool before overwriting.' };
830
+ fs.mkdirSync(path.dirname(path.resolve(fp)), { recursive: true });
831
+ fs.writeFileSync(fp, args.content ?? '');
832
+ readFiles.add(norm(fp));
833
+ return { ok: true, output: `Wrote ${fp} (${(args.content || '').length} chars)` };
834
+ }
835
+ if (name === 'Edit') {
836
+ const fp = args.file_path;
837
+ if (!fs.existsSync(fp))
838
+ return { ok: false, output: `Error: file does not exist: ${fp}` };
839
+ if (!readFiles.has(norm(fp)))
840
+ return { ok: false, output: 'Error: you must use the Read tool on this file before editing it.' };
841
+ const oldStr = String(args.old_string ?? '');
842
+ const newStr = String(args.new_string ?? '');
843
+ if (oldStr === newStr)
844
+ return { ok: false, output: 'Error: new_string must differ from old_string.' };
845
+ let c = fs.readFileSync(fp, 'utf8');
846
+ const count = oldStr ? c.split(oldStr).length - 1 : 0;
847
+ if (count === 0)
848
+ return { ok: false, output: 'Error: old_string not found in file.' };
849
+ if (count > 1 && !args.replace_all)
850
+ return {
851
+ ok: false,
852
+ output: `Error: old_string is not unique (${count} matches). Provide more surrounding context or set replace_all: true.`,
853
+ };
854
+ c = args.replace_all ? c.split(oldStr).join(newStr) : c.replace(oldStr, newStr);
855
+ fs.writeFileSync(fp, c);
856
+ return { ok: true, output: `Edited ${fp}${args.replace_all ? ` (${count} occurrences)` : ''}` };
857
+ }
858
+ if (name === 'Glob') {
859
+ const base = args.path || process.cwd();
860
+ const all = [];
861
+ walk(base, all);
862
+ const rx = globToRegex(args.pattern);
863
+ let matches = all.filter((f) => rx.test(f.replace(/\\/g, '/')));
864
+ // Değiştirilme zamanına göre sırala (yeni → eski), WormClaude gibi
865
+ matches = matches
866
+ .map((f) => { let m = 0; try {
867
+ m = fs.statSync(f).mtimeMs;
868
+ }
869
+ catch { } return { f, m }; })
870
+ .sort((a, b) => b.m - a.m)
871
+ .map((x) => x.f);
872
+ const truncated = matches.length > 100;
873
+ const shown = matches.slice(0, 100);
874
+ if (!shown.length)
875
+ return { ok: true, output: '(no matches)' };
876
+ return { ok: true, output: shown.join('\n') + (truncated ? '\n(results truncated to 100 files)' : '') };
877
+ }
878
+ if (name === 'Grep') {
879
+ const base = args.path && fs.existsSync(args.path) ? args.path : process.cwd();
880
+ const mode = args.output_mode || 'files_with_matches';
881
+ const flags = (args['-i'] ? 'i' : '') + (args.multiline ? 's' : '');
882
+ let rx;
883
+ try {
884
+ rx = new RegExp(args.pattern, flags + 'g');
885
+ }
886
+ catch (e) {
887
+ return { ok: false, output: `Error: invalid regex: ${e?.message || e}` };
888
+ }
889
+ const files = [];
890
+ if (fs.existsSync(base) && fs.statSync(base).isFile())
891
+ files.push(base);
892
+ else
893
+ walk(base, files);
894
+ // type / glob filtresi
895
+ let filtered = files;
896
+ if (args.type && TYPE_EXT[args.type]) {
897
+ const exts = TYPE_EXT[args.type];
898
+ filtered = filtered.filter((f) => exts.includes(path.extname(f).slice(1).toLowerCase()));
899
+ }
900
+ if (args.glob) {
901
+ const grx = globToRegex(args.glob);
902
+ filtered = filtered.filter((f) => grx.test(f.replace(/\\/g, '/')) || grx.test(path.basename(f)));
903
+ }
904
+ const showLineNo = args['-n'] !== false; // content modunda varsayılan true
905
+ const after = Number(args['-A'] ?? args['-C'] ?? args.context ?? 0);
906
+ const before = Number(args['-B'] ?? args['-C'] ?? args.context ?? 0);
907
+ const headLimit = args.head_limit === 0 ? Infinity : Number(args.head_limit) || 250;
908
+ const offset = Number(args.offset) || 0;
909
+ const reset = () => { rx.lastIndex = 0; return rx; };
910
+ const entries = [];
911
+ let fileCount = 0;
912
+ for (const f of filtered) {
913
+ let txt;
914
+ try {
915
+ txt = fs.readFileSync(f, 'utf8');
916
+ }
917
+ catch {
918
+ continue;
919
+ }
920
+ if (args.multiline) {
921
+ // çok satırlı: tüm metinde ara
922
+ if (mode === 'files_with_matches') {
923
+ if (reset().test(txt)) {
924
+ entries.push(f);
925
+ fileCount++;
926
+ }
927
+ }
928
+ else if (mode === 'count') {
929
+ const n = (txt.match(rx) || []).length;
930
+ if (n) {
931
+ entries.push(`${f}:${n}`);
932
+ fileCount++;
933
+ }
934
+ }
935
+ else {
936
+ const m = txt.match(rx);
937
+ if (m) {
938
+ entries.push(`${f}: ${m.length} match(es)`);
939
+ fileCount++;
940
+ }
941
+ }
942
+ continue;
943
+ }
944
+ const lines = txt.split('\n');
945
+ const lineRx = new RegExp(args.pattern, args['-i'] ? 'i' : '');
946
+ const hitIdx = [];
947
+ for (let i = 0; i < lines.length; i++)
948
+ if (lineRx.test(lines[i]))
949
+ hitIdx.push(i);
950
+ if (!hitIdx.length)
951
+ continue;
952
+ fileCount++;
953
+ if (mode === 'files_with_matches') {
954
+ entries.push(f);
955
+ continue;
956
+ }
957
+ if (mode === 'count') {
958
+ entries.push(`${f}:${hitIdx.length}`);
959
+ continue;
960
+ }
961
+ // content modu
962
+ const printed = new Set();
963
+ for (const i of hitIdx) {
964
+ for (let j = Math.max(0, i - before); j <= Math.min(lines.length - 1, i + after); j++) {
965
+ if (printed.has(j))
966
+ continue;
967
+ printed.add(j);
968
+ entries.push(showLineNo ? `${f}:${j + 1}: ${lines[j]}` : `${f}: ${lines[j]}`);
969
+ }
970
+ }
971
+ }
972
+ const sliced = entries.slice(offset, offset + headLimit);
973
+ if (!sliced.length)
974
+ return { ok: true, output: '(no matches)' };
975
+ const note = entries.length > sliced.length ? `\n(${entries.length - sliced.length} more, raise head_limit to see)` : '';
976
+ return { ok: true, output: sliced.join('\n').slice(0, 30000) + note + `\n\n[${fileCount} file(s) with matches]` };
977
+ }
978
+ if (name === 'WebFetch') {
979
+ let url = String(args.url);
980
+ if (url.startsWith('http://'))
981
+ url = 'https://' + url.slice(7);
982
+ const res = await fetch(url, { signal: AbortSignal.timeout(20000) });
983
+ let txt = await res.text();
984
+ txt = txt
985
+ .replace(/<script[\s\S]*?<\/script>/gi, '')
986
+ .replace(/<style[\s\S]*?<\/style>/gi, '')
987
+ .replace(/<[^>]+>/g, ' ')
988
+ .replace(/\s+/g, ' ')
989
+ .trim();
990
+ const header = args.prompt ? `[prompt: ${String(args.prompt).slice(0, 200)}]\n\n` : '';
991
+ return { ok: true, output: header + (txt || '(empty)').slice(0, 15000) };
992
+ }
993
+ if (name === 'WebSearch') {
994
+ const q = encodeURIComponent(String(args.query || ''));
995
+ const max = Number(args.max_results) || 5;
996
+ const res = await fetch(`https://html.duckduckgo.com/html/?q=${q}`, {
997
+ headers: { 'User-Agent': 'Mozilla/5.0' }, signal: AbortSignal.timeout(20000),
998
+ });
999
+ const html = await res.text();
1000
+ const out = [];
1001
+ const re = /<a[^>]*class="result__a"[^>]*href="([^"]+)"[^>]*>([\s\S]*?)<\/a>/g;
1002
+ let m;
1003
+ while ((m = re.exec(html)) && out.length < max) {
1004
+ let url = m[1];
1005
+ const ud = url.match(/uddg=([^&]+)/);
1006
+ if (ud)
1007
+ url = decodeURIComponent(ud[1]);
1008
+ const title = m[2].replace(/<[^>]+>/g, '').replace(/\s+/g, ' ').trim();
1009
+ out.push(`${out.length + 1}. ${title}\n ${url}`);
1010
+ }
1011
+ return { ok: true, output: out.length ? out.join('\n') : '(sonuç yok)' };
1012
+ }
1013
+ if (name === 'TodoWrite') {
1014
+ todosStore = Array.isArray(args.todos) ? args.todos : [];
1015
+ const icon = (s) => (s === 'completed' ? '[x]' : s === 'in_progress' ? '[~]' : '[ ]');
1016
+ const txt = todosStore.map((tdo) => `${icon(tdo.status)} ${tdo.content}`).join('\n');
1017
+ return { ok: true, output: txt || '(boş)' };
1018
+ }
1019
+ if (name === 'PowerShell') {
1020
+ let timeout = Number(args.timeout) || DEFAULT_BASH_TIMEOUT_MS;
1021
+ if (timeout > MAX_BASH_TIMEOUT_MS)
1022
+ timeout = MAX_BASH_TIMEOUT_MS;
1023
+ const cmd = String(args.command).replace(/"/g, '\\"');
1024
+ const out = execSync(`powershell -NoProfile -NonInteractive -Command "${cmd}"`, {
1025
+ encoding: 'utf8', timeout, maxBuffer: 10 * 1024 * 1024, windowsHide: true,
1026
+ });
1027
+ return { ok: true, output: (out || '(no output)').slice(0, 20000) };
1028
+ }
1029
+ if (name === 'NotebookEdit') {
1030
+ const fp = args.notebook_path;
1031
+ if (!fs.existsSync(fp))
1032
+ return { ok: false, output: `Error: file does not exist: ${fp}` };
1033
+ const nb = JSON.parse(fs.readFileSync(fp, 'utf8'));
1034
+ nb.cells = nb.cells || [];
1035
+ const idx = Number(args.cell_index) || 0;
1036
+ const mode = args.edit_mode || 'replace';
1037
+ const src = String(args.new_source ?? '').split('\n').map((l, i, a) => (i < a.length - 1 ? l + '\n' : l));
1038
+ if (mode === 'delete') {
1039
+ if (!nb.cells[idx])
1040
+ return { ok: false, output: 'cell index out of range' };
1041
+ nb.cells.splice(idx, 1);
1042
+ }
1043
+ else if (mode === 'insert') {
1044
+ const cell = { cell_type: args.cell_type || 'code', metadata: {}, source: src };
1045
+ if (cell.cell_type === 'code') {
1046
+ cell.outputs = [];
1047
+ cell.execution_count = null;
1048
+ }
1049
+ nb.cells.splice(idx, 0, cell);
1050
+ }
1051
+ else {
1052
+ if (!nb.cells[idx])
1053
+ return { ok: false, output: 'cell index out of range' };
1054
+ nb.cells[idx].source = src;
1055
+ }
1056
+ fs.writeFileSync(fp, JSON.stringify(nb, null, 1));
1057
+ return { ok: true, output: `Notebook ${mode} @ cell ${idx} (${fp})` };
1058
+ }
1059
+ if (name === 'REPL') {
1060
+ const lang = args.language;
1061
+ const tmp = path.join(os.tmpdir(), `wc-repl-${Date.now()}.${lang === 'python' ? 'py' : 'js'}`);
1062
+ fs.writeFileSync(tmp, String(args.code || ''));
1063
+ try {
1064
+ const cmd = lang === 'python' ? `python "${tmp}"` : `node "${tmp}"`;
1065
+ const out = execSync(cmd, { encoding: 'utf8', timeout: 60000, maxBuffer: 10 * 1024 * 1024, windowsHide: true });
1066
+ return { ok: true, output: (out || '(no output)').slice(0, 15000) };
1067
+ }
1068
+ catch (e) {
1069
+ return { ok: false, output: ((e?.stdout || '') + (e?.stderr || e?.message || '')).slice(0, 15000) };
1070
+ }
1071
+ finally {
1072
+ try {
1073
+ fs.unlinkSync(tmp);
1074
+ }
1075
+ catch { }
1076
+ }
1077
+ }
1078
+ if (name === 'LSP') {
1079
+ const base = args.path && fs.existsSync(args.path) ? args.path : process.cwd();
1080
+ const sym = String(args.symbol);
1081
+ const esc = sym.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
1082
+ const defRe = new RegExp(`\\b(function|class|def|const|let|var|interface|type|enum|struct|fn|func)\\s+${esc}\\b`);
1083
+ const refRe = new RegExp(`\\b${esc}\\b`);
1084
+ const rx = args.action === 'definition' ? defRe : refRe;
1085
+ const files = [];
1086
+ walk(base, files);
1087
+ const hits = [];
1088
+ for (const f of files) {
1089
+ if (hits.length > 100)
1090
+ break;
1091
+ let txt;
1092
+ try {
1093
+ txt = fs.readFileSync(f, 'utf8');
1094
+ }
1095
+ catch {
1096
+ continue;
1097
+ }
1098
+ const lines = txt.split('\n');
1099
+ for (let i = 0; i < lines.length; i++) {
1100
+ if (rx.test(lines[i])) {
1101
+ hits.push(`${f}:${i + 1}: ${lines[i].trim().slice(0, 160)}`);
1102
+ if (hits.length > 100)
1103
+ break;
1104
+ }
1105
+ }
1106
+ }
1107
+ return { ok: true, output: hits.length ? hits.join('\n') : '(bulunamadı)' };
1108
+ }
1109
+ if (name === 'Sleep') {
1110
+ const s = Math.min(Math.max(0, Number(args.seconds) || 0), 60);
1111
+ await new Promise((r) => setTimeout(r, s * 1000));
1112
+ return { ok: true, output: `Slept ${s}s` };
1113
+ }
1114
+ if (name === 'Skill') {
1115
+ const sk = getSkill(String(args.name));
1116
+ if (!sk)
1117
+ return { ok: false, output: `Unknown skill: ${args.name}` };
1118
+ const prompt = buildSkillPrompt(sk, String(args.args || ''));
1119
+ let tools = subAgentTools();
1120
+ if (sk.tools && sk.tools.length)
1121
+ tools = tools.filter((t) => sk.tools.includes(t.function.name));
1122
+ const messages = [
1123
+ { role: 'system', content: SUBAGENT_SYSTEM },
1124
+ { role: 'user', content: prompt },
1125
+ ];
1126
+ const { finalText } = await runAgentLoop({ config: cfg(), messages, tools, executeTool });
1127
+ return { ok: true, output: finalText || '(skill returned no text)' };
1128
+ }
1129
+ if (name.startsWith('mcp__'))
1130
+ return await callMcpTool(name, args);
1131
+ return { ok: false, output: `Unknown tool: ${name}` };
1132
+ }
1133
+ catch (e) {
1134
+ return { ok: false, output: `Error: ${e?.message || String(e)}` };
1135
+ }
1136
+ }