morpheus-cli 0.8.8 → 0.9.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.
@@ -1,235 +0,0 @@
1
- import { tool } from '@langchain/core/tools';
2
- import { z } from 'zod';
3
- import fs from 'fs-extra';
4
- import path from 'path';
5
- import { glob } from 'glob';
6
- import { truncateOutput, isWithinDir } from '../utils.js';
7
- import { registerToolFactory } from '../registry.js';
8
- function resolveSafe(ctx, filePath) {
9
- // Resolve relative to sandbox_dir (preferred) or working_dir
10
- const base = ctx.sandbox_dir || ctx.working_dir;
11
- const resolved = path.isAbsolute(filePath) ? filePath : path.resolve(base, filePath);
12
- return resolved;
13
- }
14
- /**
15
- * Guards a resolved path against the sandbox directory.
16
- * When sandbox_dir is set, ALL paths (read and write) must be within it.
17
- * When readonly_mode is true, destructive operations are blocked.
18
- */
19
- function guardPath(ctx, resolved, destructive = false) {
20
- // Enforce readonly_mode for destructive operations
21
- if (destructive && ctx.readonly_mode) {
22
- throw new Error(`Operation denied: DevKit is in read-only mode. Write/delete operations are blocked.`);
23
- }
24
- // Enforce sandbox_dir for ALL operations (read and write)
25
- if (ctx.sandbox_dir && !isWithinDir(resolved, ctx.sandbox_dir)) {
26
- throw new Error(`Path '${resolved}' is outside the sandbox directory '${ctx.sandbox_dir}'. Operation denied.`);
27
- }
28
- }
29
- export function createFilesystemTools(ctx) {
30
- return [
31
- tool(async ({ file_path, encoding, start_line, end_line }) => {
32
- const resolved = resolveSafe(ctx, file_path);
33
- guardPath(ctx, resolved);
34
- const content = await fs.readFile(resolved, encoding ?? 'utf8');
35
- const lines = content.split('\n');
36
- const sliced = (start_line || end_line)
37
- ? lines.slice((start_line ?? 1) - 1, end_line).join('\n')
38
- : content;
39
- return truncateOutput(sliced);
40
- }, {
41
- name: 'read_file',
42
- description: 'Read the contents of a file. Optionally specify line range.',
43
- schema: z.object({
44
- file_path: z.string().describe('Path to the file (absolute or relative to working_dir)'),
45
- encoding: z.string().optional().describe('File encoding, default utf8'),
46
- start_line: z.number().int().positive().optional().describe('Start line (1-based)'),
47
- end_line: z.number().int().positive().optional().describe('End line (inclusive)'),
48
- }),
49
- }),
50
- tool(async ({ file_path, content }) => {
51
- const resolved = resolveSafe(ctx, file_path);
52
- guardPath(ctx, resolved, true);
53
- await fs.ensureDir(path.dirname(resolved));
54
- await fs.writeFile(resolved, content, 'utf8');
55
- return JSON.stringify({ success: true, path: resolved });
56
- }, {
57
- name: 'write_file',
58
- description: 'Write content to a file, creating it and parent directories if needed.',
59
- schema: z.object({
60
- file_path: z.string(),
61
- content: z.string().describe('Content to write'),
62
- }),
63
- }),
64
- tool(async ({ file_path, content }) => {
65
- const resolved = resolveSafe(ctx, file_path);
66
- guardPath(ctx, resolved, true);
67
- await fs.ensureDir(path.dirname(resolved));
68
- await fs.appendFile(resolved, content, 'utf8');
69
- return JSON.stringify({ success: true, path: resolved });
70
- }, {
71
- name: 'append_file',
72
- description: 'Append content to a file without overwriting existing content.',
73
- schema: z.object({
74
- file_path: z.string(),
75
- content: z.string(),
76
- }),
77
- }),
78
- tool(async ({ file_path }) => {
79
- const resolved = resolveSafe(ctx, file_path);
80
- guardPath(ctx, resolved, true);
81
- await fs.remove(resolved);
82
- return JSON.stringify({ success: true, deleted: resolved });
83
- }, {
84
- name: 'delete_file',
85
- description: 'Delete a file or directory.',
86
- schema: z.object({ file_path: z.string() }),
87
- }),
88
- tool(async ({ source, destination }) => {
89
- const src = resolveSafe(ctx, source);
90
- const dest = resolveSafe(ctx, destination);
91
- guardPath(ctx, src, true);
92
- guardPath(ctx, dest, true);
93
- await fs.ensureDir(path.dirname(dest));
94
- await fs.move(src, dest, { overwrite: true });
95
- return JSON.stringify({ success: true, from: src, to: dest });
96
- }, {
97
- name: 'move_file',
98
- description: 'Move or rename a file or directory.',
99
- schema: z.object({
100
- source: z.string(),
101
- destination: z.string(),
102
- }),
103
- }),
104
- tool(async ({ source, destination }) => {
105
- const src = resolveSafe(ctx, source);
106
- const dest = resolveSafe(ctx, destination);
107
- guardPath(ctx, src);
108
- guardPath(ctx, dest, true);
109
- await fs.ensureDir(path.dirname(dest));
110
- await fs.copy(src, dest);
111
- return JSON.stringify({ success: true, from: src, to: dest });
112
- }, {
113
- name: 'copy_file',
114
- description: 'Copy a file or directory to a new location.',
115
- schema: z.object({
116
- source: z.string(),
117
- destination: z.string(),
118
- }),
119
- }),
120
- tool(async ({ dir_path, recursive, pattern }) => {
121
- const resolved = resolveSafe(ctx, dir_path ?? '.');
122
- guardPath(ctx, resolved);
123
- const entries = await fs.readdir(resolved, { withFileTypes: true });
124
- let results = entries.map(e => ({
125
- name: e.name,
126
- type: e.isDirectory() ? 'dir' : 'file',
127
- path: path.join(resolved, e.name),
128
- }));
129
- if (pattern) {
130
- const re = new RegExp(pattern.replace('*', '.*').replace('?', '.'));
131
- results = results.filter(r => re.test(r.name));
132
- }
133
- if (recursive) {
134
- const subResults = [];
135
- for (const entry of results.filter(r => r.type === 'dir')) {
136
- try {
137
- const subEntries = await fs.readdir(entry.path, { withFileTypes: true });
138
- subResults.push(...subEntries.map(e => ({
139
- name: e.name,
140
- type: e.isDirectory() ? 'dir' : 'file',
141
- path: path.join(entry.path, e.name),
142
- })));
143
- }
144
- catch { /* skip inaccessible */ }
145
- }
146
- results.push(...subResults);
147
- }
148
- return truncateOutput(JSON.stringify(results, null, 2));
149
- }, {
150
- name: 'list_dir',
151
- description: 'List files and directories in a path.',
152
- schema: z.object({
153
- dir_path: z.string().optional().describe('Directory path, defaults to working_dir'),
154
- recursive: z.boolean().optional().describe('Include subdirectory contents'),
155
- pattern: z.string().optional().describe('Filter by name pattern (glob-like)'),
156
- }),
157
- }),
158
- tool(async ({ dir_path }) => {
159
- const resolved = resolveSafe(ctx, dir_path);
160
- guardPath(ctx, resolved, true);
161
- await fs.ensureDir(resolved);
162
- return JSON.stringify({ success: true, path: resolved });
163
- }, {
164
- name: 'create_dir',
165
- description: 'Create a directory and all parent directories.',
166
- schema: z.object({ dir_path: z.string() }),
167
- }),
168
- tool(async ({ file_path }) => {
169
- const resolved = resolveSafe(ctx, file_path);
170
- guardPath(ctx, resolved);
171
- const stat = await fs.stat(resolved);
172
- return JSON.stringify({
173
- path: resolved,
174
- size: stat.size,
175
- isDirectory: stat.isDirectory(),
176
- isFile: stat.isFile(),
177
- created: stat.birthtime.toISOString(),
178
- modified: stat.mtime.toISOString(),
179
- permissions: stat.mode.toString(8),
180
- });
181
- }, {
182
- name: 'file_info',
183
- description: 'Get metadata about a file or directory (size, dates, permissions).',
184
- schema: z.object({ file_path: z.string() }),
185
- }),
186
- tool(async ({ pattern, search_path, regex, case_insensitive, max_results }) => {
187
- const base = resolveSafe(ctx, search_path ?? '.');
188
- guardPath(ctx, base);
189
- const files = await glob('**/*', { cwd: base, nodir: true, absolute: true });
190
- const re = new RegExp(pattern, case_insensitive ? 'i' : undefined);
191
- const results = [];
192
- for (const file of files) {
193
- if (results.length >= (max_results ?? 100))
194
- break;
195
- try {
196
- const content = await fs.readFile(file, 'utf8');
197
- const lines = content.split('\n');
198
- for (let i = 0; i < lines.length; i++) {
199
- if (re.test(lines[i])) {
200
- results.push({ file: path.relative(base, file), line: i + 1, match: lines[i].trim() });
201
- if (results.length >= (max_results ?? 100))
202
- break;
203
- }
204
- }
205
- }
206
- catch { /* skip binary/unreadable files */ }
207
- }
208
- return truncateOutput(JSON.stringify(results, null, 2));
209
- }, {
210
- name: 'search_in_files',
211
- description: 'Search for a pattern (regex) inside file contents.',
212
- schema: z.object({
213
- pattern: z.string().describe('Regex pattern to search for'),
214
- search_path: z.string().optional().describe('Directory to search in, defaults to working_dir'),
215
- regex: z.boolean().optional().describe('Treat pattern as regex (default true)'),
216
- case_insensitive: z.boolean().optional(),
217
- max_results: z.number().int().positive().optional().describe('Max matches to return (default 100)'),
218
- }),
219
- }),
220
- tool(async ({ pattern, search_path }) => {
221
- const base = resolveSafe(ctx, search_path ?? '.');
222
- guardPath(ctx, base);
223
- const files = await glob(pattern, { cwd: base, absolute: true });
224
- return truncateOutput(JSON.stringify(files.map(f => path.relative(base, f)), null, 2));
225
- }, {
226
- name: 'find_files',
227
- description: 'Find files matching a glob pattern.',
228
- schema: z.object({
229
- pattern: z.string().describe('Glob pattern e.g. "**/*.ts", "src/**/*.json"'),
230
- search_path: z.string().optional().describe('Base directory, defaults to working_dir'),
231
- }),
232
- }),
233
- ];
234
- }
235
- registerToolFactory(createFilesystemTools, 'filesystem');
@@ -1,226 +0,0 @@
1
- import { tool } from '@langchain/core/tools';
2
- import { z } from 'zod';
3
- import path from 'path';
4
- import { ShellAdapter } from '../adapters/shell.js';
5
- import { truncateOutput, isCommandAllowed, isWithinDir } from '../utils.js';
6
- import { registerToolFactory } from '../registry.js';
7
- export function createGitTools(ctx) {
8
- const shell = ShellAdapter.create();
9
- async function git(args, cwd) {
10
- if (!isCommandAllowed('git', ctx.allowed_commands)) {
11
- return { success: false, output: `'git' is not in the allowed_commands list.` };
12
- }
13
- const result = await shell.run('git', args, {
14
- cwd: cwd ?? ctx.working_dir,
15
- timeout_ms: ctx.timeout_ms ?? 30_000,
16
- });
17
- return {
18
- success: result.exitCode === 0,
19
- output: truncateOutput(result.stdout + (result.stderr ? '\n' + result.stderr : '')),
20
- };
21
- }
22
- return [
23
- tool(async ({ short }) => {
24
- const r = await git(short ? ['status', '-s'] : ['status']);
25
- return r.output;
26
- }, {
27
- name: 'git_status',
28
- description: 'Show the working tree status.',
29
- schema: z.object({ short: z.boolean().optional().describe('Short format') }),
30
- }),
31
- tool(async ({ staged, file, base_branch }) => {
32
- const args = ['diff'];
33
- if (staged)
34
- args.push('--staged');
35
- if (base_branch)
36
- args.push(base_branch);
37
- if (file)
38
- args.push('--', file);
39
- const r = await git(args);
40
- return r.output;
41
- }, {
42
- name: 'git_diff',
43
- description: 'Show changes between commits, commit and working tree, etc.',
44
- schema: z.object({
45
- staged: z.boolean().optional().describe('Show staged changes'),
46
- file: z.string().optional().describe('Specific file'),
47
- base_branch: z.string().optional().describe('Compare against this branch'),
48
- }),
49
- }),
50
- tool(async ({ max_count, oneline, author, since }) => {
51
- const args = ['log'];
52
- if (max_count)
53
- args.push(`-${max_count}`);
54
- if (oneline)
55
- args.push('--oneline');
56
- if (author)
57
- args.push(`--author=${author}`);
58
- if (since)
59
- args.push(`--since=${since}`);
60
- const r = await git(args);
61
- return r.output;
62
- }, {
63
- name: 'git_log',
64
- description: 'Show commit logs.',
65
- schema: z.object({
66
- max_count: z.number().int().optional().describe('Limit number of commits'),
67
- oneline: z.boolean().optional(),
68
- author: z.string().optional(),
69
- since: z.string().optional().describe('e.g. "2 weeks ago"'),
70
- }),
71
- }),
72
- tool(async ({ files }) => {
73
- const args = ['add', ...(files ?? ['.'])];
74
- const r = await git(args);
75
- return JSON.stringify({ success: r.success, output: r.output });
76
- }, {
77
- name: 'git_add',
78
- description: 'Stage files for commit.',
79
- schema: z.object({
80
- files: z.array(z.string()).optional().describe('Files to stage, defaults to all (".")'),
81
- }),
82
- }),
83
- tool(async ({ message, allow_empty }) => {
84
- const args = ['commit', '-m', message];
85
- if (allow_empty)
86
- args.push('--allow-empty');
87
- const r = await git(args);
88
- return JSON.stringify({ success: r.success, output: r.output });
89
- }, {
90
- name: 'git_commit',
91
- description: 'Create a commit with the staged changes.',
92
- schema: z.object({
93
- message: z.string().describe('Commit message'),
94
- allow_empty: z.boolean().optional(),
95
- }),
96
- }),
97
- tool(async ({ remote, branch, force }) => {
98
- const args = ['push'];
99
- if (remote)
100
- args.push(remote);
101
- if (branch)
102
- args.push(branch);
103
- if (force)
104
- args.push('--force-with-lease');
105
- const r = await git(args);
106
- return JSON.stringify({ success: r.success, output: r.output });
107
- }, {
108
- name: 'git_push',
109
- description: 'Push commits to the remote repository.',
110
- schema: z.object({
111
- remote: z.string().optional().describe('Remote name, default origin'),
112
- branch: z.string().optional().describe('Branch to push'),
113
- force: z.boolean().optional().describe('Force push with lease (safer)'),
114
- }),
115
- }),
116
- tool(async ({ remote, branch, rebase }) => {
117
- const args = ['pull'];
118
- if (remote)
119
- args.push(remote);
120
- if (branch)
121
- args.push(branch);
122
- if (rebase)
123
- args.push('--rebase');
124
- const r = await git(args);
125
- return JSON.stringify({ success: r.success, output: r.output });
126
- }, {
127
- name: 'git_pull',
128
- description: 'Fetch and merge changes from remote.',
129
- schema: z.object({
130
- remote: z.string().optional(),
131
- branch: z.string().optional(),
132
- rebase: z.boolean().optional(),
133
- }),
134
- }),
135
- tool(async ({ target, create }) => {
136
- const args = ['checkout'];
137
- if (create)
138
- args.push('-b');
139
- args.push(target);
140
- const r = await git(args);
141
- return JSON.stringify({ success: r.success, output: r.output });
142
- }, {
143
- name: 'git_checkout',
144
- description: 'Switch branches or restore files.',
145
- schema: z.object({
146
- target: z.string().describe('Branch name or file path'),
147
- create: z.boolean().optional().describe('Create the branch if it does not exist'),
148
- }),
149
- }),
150
- tool(async ({ branch_name, from }) => {
151
- const args = ['checkout', '-b', branch_name];
152
- if (from)
153
- args.push(from);
154
- const r = await git(args);
155
- return JSON.stringify({ success: r.success, output: r.output });
156
- }, {
157
- name: 'git_create_branch',
158
- description: 'Create a new git branch.',
159
- schema: z.object({
160
- branch_name: z.string(),
161
- from: z.string().optional().describe('Base branch or commit'),
162
- }),
163
- }),
164
- tool(async ({ message, pop }) => {
165
- const args = pop ? ['stash', 'pop'] : ['stash', 'push'];
166
- if (!pop && message)
167
- args.push('-m', message);
168
- const r = await git(args);
169
- return JSON.stringify({ success: r.success, output: r.output });
170
- }, {
171
- name: 'git_stash',
172
- description: 'Stash working directory changes or pop the last stash.',
173
- schema: z.object({
174
- message: z.string().optional(),
175
- pop: z.boolean().optional().describe('Pop the last stash instead of creating'),
176
- }),
177
- }),
178
- tool(async ({ url, destination, depth }) => {
179
- const args = ['clone', url];
180
- if (destination) {
181
- // Enforce sandbox_dir on clone destination
182
- if (ctx.sandbox_dir) {
183
- const resolvedDest = path.isAbsolute(destination) ? destination : path.resolve(ctx.working_dir, destination);
184
- if (!isWithinDir(resolvedDest, ctx.sandbox_dir)) {
185
- return JSON.stringify({ success: false, output: `Clone destination '${resolvedDest}' is outside the sandbox directory '${ctx.sandbox_dir}'. Operation denied.` });
186
- }
187
- }
188
- args.push(destination);
189
- }
190
- if (depth)
191
- args.push('--depth', String(depth));
192
- const r = await git(args);
193
- return JSON.stringify({ success: r.success, output: r.output });
194
- }, {
195
- name: 'git_clone',
196
- description: 'Clone a git repository.',
197
- schema: z.object({
198
- url: z.string().describe('Repository URL'),
199
- destination: z.string().optional().describe('Target directory'),
200
- depth: z.number().int().optional().describe('Shallow clone depth'),
201
- }),
202
- }),
203
- tool(async ({ path: worktreePath, branch }) => {
204
- // Enforce sandbox_dir on worktree path
205
- if (ctx.sandbox_dir) {
206
- const resolvedPath = path.isAbsolute(worktreePath) ? worktreePath : path.resolve(ctx.working_dir, worktreePath);
207
- if (!isWithinDir(resolvedPath, ctx.sandbox_dir)) {
208
- return JSON.stringify({ success: false, output: `Worktree path '${resolvedPath}' is outside the sandbox directory '${ctx.sandbox_dir}'. Operation denied.` });
209
- }
210
- }
211
- const args = ['worktree', 'add', worktreePath];
212
- if (branch)
213
- args.push('-b', branch);
214
- const r = await git(args);
215
- return JSON.stringify({ success: r.success, output: r.output });
216
- }, {
217
- name: 'git_worktree_add',
218
- description: 'Add a new git worktree for parallel development.',
219
- schema: z.object({
220
- path: z.string().describe('Path for the new worktree'),
221
- branch: z.string().optional().describe('New branch to create in the worktree'),
222
- }),
223
- }),
224
- ];
225
- }
226
- registerToolFactory(createGitTools, 'git');
@@ -1,165 +0,0 @@
1
- import { tool } from '@langchain/core/tools';
2
- import { z } from 'zod';
3
- import net from 'net';
4
- import dns from 'dns';
5
- import fs from 'fs-extra';
6
- import path from 'path';
7
- import { truncateOutput, isWithinDir } from '../utils.js';
8
- import { registerToolFactory } from '../registry.js';
9
- export function createNetworkTools(ctx) {
10
- return [
11
- tool(async ({ url, method, headers, body, timeout_ms }) => {
12
- const controller = new AbortController();
13
- const timer = setTimeout(() => controller.abort(), timeout_ms ?? 30_000);
14
- try {
15
- const response = await fetch(url, {
16
- method: method ?? 'GET',
17
- headers: headers,
18
- body: body ? JSON.stringify(body) : undefined,
19
- signal: controller.signal,
20
- });
21
- const text = await response.text();
22
- return JSON.stringify({
23
- success: response.ok,
24
- status: response.status,
25
- statusText: response.statusText,
26
- headers: Object.fromEntries(response.headers.entries()),
27
- body: truncateOutput(text),
28
- });
29
- }
30
- catch (err) {
31
- return JSON.stringify({ success: false, error: err.message });
32
- }
33
- finally {
34
- clearTimeout(timer);
35
- }
36
- }, {
37
- name: 'http_request',
38
- description: 'Make an HTTP request (GET, POST, PUT, DELETE, PATCH).',
39
- schema: z.object({
40
- url: z.string().describe('Full URL to request'),
41
- method: z.enum(['GET', 'POST', 'PUT', 'DELETE', 'PATCH']).optional().describe('HTTP method, default GET'),
42
- headers: z.record(z.string(), z.string()).optional().describe('Request headers'),
43
- body: z.any().optional().describe('Request body (will be JSON-stringified)'),
44
- timeout_ms: z.number().optional().describe('Timeout in ms, default 30000'),
45
- }),
46
- }),
47
- tool(async ({ host, port, timeout_ms }) => {
48
- const checkPort = port ?? 80;
49
- return new Promise((resolve) => {
50
- const socket = net.createConnection(checkPort, host);
51
- const timer = setTimeout(() => {
52
- socket.destroy();
53
- resolve(JSON.stringify({ success: false, host, port: checkPort, error: 'timeout' }));
54
- }, timeout_ms ?? 5_000);
55
- socket.on('connect', () => {
56
- clearTimeout(timer);
57
- socket.destroy();
58
- resolve(JSON.stringify({ success: true, host, port: checkPort, reachable: true }));
59
- });
60
- socket.on('error', (err) => {
61
- clearTimeout(timer);
62
- resolve(JSON.stringify({ success: false, host, port: checkPort, error: err.message }));
63
- });
64
- });
65
- }, {
66
- name: 'ping',
67
- description: 'Preferred connectivity check tool. Verify if a host is reachable on a given port (TCP connect check). Use this instead of shell ping for routine reachability checks.',
68
- schema: z.object({
69
- host: z.string().describe('Hostname or IP'),
70
- port: z.number().int().optional().describe('Port to check, default 80'),
71
- timeout_ms: z.number().optional().describe('Timeout in ms, default 5000'),
72
- }),
73
- }),
74
- tool(async ({ host, port, timeout_ms }) => {
75
- return new Promise((resolve) => {
76
- const socket = net.createConnection(port, host);
77
- const timer = setTimeout(() => {
78
- socket.destroy();
79
- resolve(JSON.stringify({ open: false, host, port, reason: 'timeout' }));
80
- }, timeout_ms ?? 5_000);
81
- socket.on('connect', () => {
82
- clearTimeout(timer);
83
- socket.destroy();
84
- resolve(JSON.stringify({ open: true, host, port }));
85
- });
86
- socket.on('error', (err) => {
87
- clearTimeout(timer);
88
- resolve(JSON.stringify({ open: false, host, port, reason: err.message }));
89
- });
90
- });
91
- }, {
92
- name: 'port_check',
93
- description: 'Check if a specific port is open on a host.',
94
- schema: z.object({
95
- host: z.string(),
96
- port: z.number().int(),
97
- timeout_ms: z.number().optional(),
98
- }),
99
- }),
100
- tool(async ({ hostname, record_type }) => {
101
- return new Promise((resolve) => {
102
- const rtype = (record_type ?? 'A');
103
- dns.resolve(hostname, rtype, (err, addresses) => {
104
- if (err) {
105
- resolve(JSON.stringify({ success: false, hostname, error: err.message }));
106
- }
107
- else {
108
- resolve(JSON.stringify({ success: true, hostname, type: record_type ?? 'A', addresses }));
109
- }
110
- });
111
- });
112
- }, {
113
- name: 'dns_lookup',
114
- description: 'Resolve a hostname to IP addresses.',
115
- schema: z.object({
116
- hostname: z.string(),
117
- record_type: z.enum(['A', 'AAAA', 'MX', 'TXT', 'CNAME']).optional().describe('DNS record type, default A'),
118
- }),
119
- }),
120
- tool(async ({ url, destination, timeout_ms }) => {
121
- const destPath = path.isAbsolute(destination)
122
- ? destination
123
- : path.resolve(ctx.working_dir, destination);
124
- // Enforce sandbox_dir on download destination
125
- if (ctx.sandbox_dir && !isWithinDir(destPath, ctx.sandbox_dir)) {
126
- return JSON.stringify({
127
- success: false,
128
- error: `Download destination '${destPath}' is outside the sandbox directory '${ctx.sandbox_dir}'. Operation denied.`,
129
- });
130
- }
131
- await fs.ensureDir(path.dirname(destPath));
132
- const controller = new AbortController();
133
- const timer = setTimeout(() => controller.abort(), timeout_ms ?? 60_000);
134
- try {
135
- const response = await fetch(url, { signal: controller.signal });
136
- if (!response.ok) {
137
- return JSON.stringify({ success: false, error: `HTTP ${response.status}: ${response.statusText}` });
138
- }
139
- const buffer = await response.arrayBuffer();
140
- await fs.writeFile(destPath, Buffer.from(buffer));
141
- return JSON.stringify({
142
- success: true,
143
- url,
144
- destination: destPath,
145
- size_bytes: buffer.byteLength,
146
- });
147
- }
148
- catch (err) {
149
- return JSON.stringify({ success: false, error: err.message });
150
- }
151
- finally {
152
- clearTimeout(timer);
153
- }
154
- }, {
155
- name: 'download_file',
156
- description: 'Download a file from a URL to a local path.',
157
- schema: z.object({
158
- url: z.string().describe('URL to download from'),
159
- destination: z.string().describe('Local path to save the file'),
160
- timeout_ms: z.number().optional().describe('Timeout in ms, default 60000'),
161
- }),
162
- }),
163
- ];
164
- }
165
- registerToolFactory(createNetworkTools, 'network');