morpheus-cli 0.4.0 → 0.4.2
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/README.md +88 -0
- package/dist/config/manager.js +32 -0
- package/dist/config/schemas.js +5 -0
- package/dist/devkit/adapters/shell.js +80 -0
- package/dist/devkit/index.js +10 -0
- package/dist/devkit/registry.js +12 -0
- package/dist/devkit/tools/filesystem.js +219 -0
- package/dist/devkit/tools/git.js +210 -0
- package/dist/devkit/tools/network.js +158 -0
- package/dist/devkit/tools/packages.js +73 -0
- package/dist/devkit/tools/processes.js +130 -0
- package/dist/devkit/tools/shell.js +94 -0
- package/dist/devkit/tools/system.js +132 -0
- package/dist/devkit/types.js +1 -0
- package/dist/devkit/utils.js +45 -0
- package/dist/http/api.js +122 -0
- package/dist/runtime/apoc.js +110 -0
- package/dist/runtime/memory/sati/index.js +2 -2
- package/dist/runtime/memory/sati/service.js +3 -2
- package/dist/runtime/memory/sqlite.js +98 -28
- package/dist/runtime/oracle.js +4 -1
- package/dist/runtime/providers/factory.js +85 -80
- package/dist/runtime/telephonist.js +19 -1
- package/dist/runtime/tools/apoc-tool.js +43 -0
- package/dist/runtime/tools/index.js +1 -0
- package/dist/types/config.js +6 -0
- package/dist/ui/assets/index-CjlkpcsE.js +109 -0
- package/dist/ui/assets/index-LrqT6MpO.css +1 -0
- package/dist/ui/index.html +2 -2
- package/dist/ui/sw.js +1 -1
- package/package.json +1 -1
- package/dist/ui/assets/index-CwvCMGLo.css +0 -1
- package/dist/ui/assets/index-D9REy_tK.js +0 -109
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
import { tool } from '@langchain/core/tools';
|
|
2
|
+
import { z } from 'zod';
|
|
3
|
+
import { ShellAdapter } from '../adapters/shell.js';
|
|
4
|
+
import { truncateOutput, isCommandAllowed } from '../utils.js';
|
|
5
|
+
import { registerToolFactory } from '../registry.js';
|
|
6
|
+
export function createGitTools(ctx) {
|
|
7
|
+
const shell = ShellAdapter.create();
|
|
8
|
+
async function git(args, cwd) {
|
|
9
|
+
if (!isCommandAllowed('git', ctx.allowed_commands)) {
|
|
10
|
+
return { success: false, output: `'git' is not in the allowed_commands list.` };
|
|
11
|
+
}
|
|
12
|
+
const result = await shell.run('git', args, {
|
|
13
|
+
cwd: cwd ?? ctx.working_dir,
|
|
14
|
+
timeout_ms: ctx.timeout_ms ?? 30_000,
|
|
15
|
+
});
|
|
16
|
+
return {
|
|
17
|
+
success: result.exitCode === 0,
|
|
18
|
+
output: truncateOutput(result.stdout + (result.stderr ? '\n' + result.stderr : '')),
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
return [
|
|
22
|
+
tool(async ({ short }) => {
|
|
23
|
+
const r = await git(short ? ['status', '-s'] : ['status']);
|
|
24
|
+
return r.output;
|
|
25
|
+
}, {
|
|
26
|
+
name: 'git_status',
|
|
27
|
+
description: 'Show the working tree status.',
|
|
28
|
+
schema: z.object({ short: z.boolean().optional().describe('Short format') }),
|
|
29
|
+
}),
|
|
30
|
+
tool(async ({ staged, file, base_branch }) => {
|
|
31
|
+
const args = ['diff'];
|
|
32
|
+
if (staged)
|
|
33
|
+
args.push('--staged');
|
|
34
|
+
if (base_branch)
|
|
35
|
+
args.push(base_branch);
|
|
36
|
+
if (file)
|
|
37
|
+
args.push('--', file);
|
|
38
|
+
const r = await git(args);
|
|
39
|
+
return r.output;
|
|
40
|
+
}, {
|
|
41
|
+
name: 'git_diff',
|
|
42
|
+
description: 'Show changes between commits, commit and working tree, etc.',
|
|
43
|
+
schema: z.object({
|
|
44
|
+
staged: z.boolean().optional().describe('Show staged changes'),
|
|
45
|
+
file: z.string().optional().describe('Specific file'),
|
|
46
|
+
base_branch: z.string().optional().describe('Compare against this branch'),
|
|
47
|
+
}),
|
|
48
|
+
}),
|
|
49
|
+
tool(async ({ max_count, oneline, author, since }) => {
|
|
50
|
+
const args = ['log'];
|
|
51
|
+
if (max_count)
|
|
52
|
+
args.push(`-${max_count}`);
|
|
53
|
+
if (oneline)
|
|
54
|
+
args.push('--oneline');
|
|
55
|
+
if (author)
|
|
56
|
+
args.push(`--author=${author}`);
|
|
57
|
+
if (since)
|
|
58
|
+
args.push(`--since=${since}`);
|
|
59
|
+
const r = await git(args);
|
|
60
|
+
return r.output;
|
|
61
|
+
}, {
|
|
62
|
+
name: 'git_log',
|
|
63
|
+
description: 'Show commit logs.',
|
|
64
|
+
schema: z.object({
|
|
65
|
+
max_count: z.number().int().optional().describe('Limit number of commits'),
|
|
66
|
+
oneline: z.boolean().optional(),
|
|
67
|
+
author: z.string().optional(),
|
|
68
|
+
since: z.string().optional().describe('e.g. "2 weeks ago"'),
|
|
69
|
+
}),
|
|
70
|
+
}),
|
|
71
|
+
tool(async ({ files }) => {
|
|
72
|
+
const args = ['add', ...(files ?? ['.'])];
|
|
73
|
+
const r = await git(args);
|
|
74
|
+
return JSON.stringify({ success: r.success, output: r.output });
|
|
75
|
+
}, {
|
|
76
|
+
name: 'git_add',
|
|
77
|
+
description: 'Stage files for commit.',
|
|
78
|
+
schema: z.object({
|
|
79
|
+
files: z.array(z.string()).optional().describe('Files to stage, defaults to all (".")'),
|
|
80
|
+
}),
|
|
81
|
+
}),
|
|
82
|
+
tool(async ({ message, allow_empty }) => {
|
|
83
|
+
const args = ['commit', '-m', message];
|
|
84
|
+
if (allow_empty)
|
|
85
|
+
args.push('--allow-empty');
|
|
86
|
+
const r = await git(args);
|
|
87
|
+
return JSON.stringify({ success: r.success, output: r.output });
|
|
88
|
+
}, {
|
|
89
|
+
name: 'git_commit',
|
|
90
|
+
description: 'Create a commit with the staged changes.',
|
|
91
|
+
schema: z.object({
|
|
92
|
+
message: z.string().describe('Commit message'),
|
|
93
|
+
allow_empty: z.boolean().optional(),
|
|
94
|
+
}),
|
|
95
|
+
}),
|
|
96
|
+
tool(async ({ remote, branch, force }) => {
|
|
97
|
+
const args = ['push'];
|
|
98
|
+
if (remote)
|
|
99
|
+
args.push(remote);
|
|
100
|
+
if (branch)
|
|
101
|
+
args.push(branch);
|
|
102
|
+
if (force)
|
|
103
|
+
args.push('--force-with-lease');
|
|
104
|
+
const r = await git(args);
|
|
105
|
+
return JSON.stringify({ success: r.success, output: r.output });
|
|
106
|
+
}, {
|
|
107
|
+
name: 'git_push',
|
|
108
|
+
description: 'Push commits to the remote repository.',
|
|
109
|
+
schema: z.object({
|
|
110
|
+
remote: z.string().optional().describe('Remote name, default origin'),
|
|
111
|
+
branch: z.string().optional().describe('Branch to push'),
|
|
112
|
+
force: z.boolean().optional().describe('Force push with lease (safer)'),
|
|
113
|
+
}),
|
|
114
|
+
}),
|
|
115
|
+
tool(async ({ remote, branch, rebase }) => {
|
|
116
|
+
const args = ['pull'];
|
|
117
|
+
if (remote)
|
|
118
|
+
args.push(remote);
|
|
119
|
+
if (branch)
|
|
120
|
+
args.push(branch);
|
|
121
|
+
if (rebase)
|
|
122
|
+
args.push('--rebase');
|
|
123
|
+
const r = await git(args);
|
|
124
|
+
return JSON.stringify({ success: r.success, output: r.output });
|
|
125
|
+
}, {
|
|
126
|
+
name: 'git_pull',
|
|
127
|
+
description: 'Fetch and merge changes from remote.',
|
|
128
|
+
schema: z.object({
|
|
129
|
+
remote: z.string().optional(),
|
|
130
|
+
branch: z.string().optional(),
|
|
131
|
+
rebase: z.boolean().optional(),
|
|
132
|
+
}),
|
|
133
|
+
}),
|
|
134
|
+
tool(async ({ target, create }) => {
|
|
135
|
+
const args = ['checkout'];
|
|
136
|
+
if (create)
|
|
137
|
+
args.push('-b');
|
|
138
|
+
args.push(target);
|
|
139
|
+
const r = await git(args);
|
|
140
|
+
return JSON.stringify({ success: r.success, output: r.output });
|
|
141
|
+
}, {
|
|
142
|
+
name: 'git_checkout',
|
|
143
|
+
description: 'Switch branches or restore files.',
|
|
144
|
+
schema: z.object({
|
|
145
|
+
target: z.string().describe('Branch name or file path'),
|
|
146
|
+
create: z.boolean().optional().describe('Create the branch if it does not exist'),
|
|
147
|
+
}),
|
|
148
|
+
}),
|
|
149
|
+
tool(async ({ branch_name, from }) => {
|
|
150
|
+
const args = ['checkout', '-b', branch_name];
|
|
151
|
+
if (from)
|
|
152
|
+
args.push(from);
|
|
153
|
+
const r = await git(args);
|
|
154
|
+
return JSON.stringify({ success: r.success, output: r.output });
|
|
155
|
+
}, {
|
|
156
|
+
name: 'git_create_branch',
|
|
157
|
+
description: 'Create a new git branch.',
|
|
158
|
+
schema: z.object({
|
|
159
|
+
branch_name: z.string(),
|
|
160
|
+
from: z.string().optional().describe('Base branch or commit'),
|
|
161
|
+
}),
|
|
162
|
+
}),
|
|
163
|
+
tool(async ({ message, pop }) => {
|
|
164
|
+
const args = pop ? ['stash', 'pop'] : ['stash', 'push'];
|
|
165
|
+
if (!pop && message)
|
|
166
|
+
args.push('-m', message);
|
|
167
|
+
const r = await git(args);
|
|
168
|
+
return JSON.stringify({ success: r.success, output: r.output });
|
|
169
|
+
}, {
|
|
170
|
+
name: 'git_stash',
|
|
171
|
+
description: 'Stash working directory changes or pop the last stash.',
|
|
172
|
+
schema: z.object({
|
|
173
|
+
message: z.string().optional(),
|
|
174
|
+
pop: z.boolean().optional().describe('Pop the last stash instead of creating'),
|
|
175
|
+
}),
|
|
176
|
+
}),
|
|
177
|
+
tool(async ({ url, destination, depth }) => {
|
|
178
|
+
const args = ['clone', url];
|
|
179
|
+
if (destination)
|
|
180
|
+
args.push(destination);
|
|
181
|
+
if (depth)
|
|
182
|
+
args.push('--depth', String(depth));
|
|
183
|
+
const r = await git(args);
|
|
184
|
+
return JSON.stringify({ success: r.success, output: r.output });
|
|
185
|
+
}, {
|
|
186
|
+
name: 'git_clone',
|
|
187
|
+
description: 'Clone a git repository.',
|
|
188
|
+
schema: z.object({
|
|
189
|
+
url: z.string().describe('Repository URL'),
|
|
190
|
+
destination: z.string().optional().describe('Target directory'),
|
|
191
|
+
depth: z.number().int().optional().describe('Shallow clone depth'),
|
|
192
|
+
}),
|
|
193
|
+
}),
|
|
194
|
+
tool(async ({ path: worktreePath, branch }) => {
|
|
195
|
+
const args = ['worktree', 'add', worktreePath];
|
|
196
|
+
if (branch)
|
|
197
|
+
args.push('-b', branch);
|
|
198
|
+
const r = await git(args);
|
|
199
|
+
return JSON.stringify({ success: r.success, output: r.output });
|
|
200
|
+
}, {
|
|
201
|
+
name: 'git_worktree_add',
|
|
202
|
+
description: 'Add a new git worktree for parallel development.',
|
|
203
|
+
schema: z.object({
|
|
204
|
+
path: z.string().describe('Path for the new worktree'),
|
|
205
|
+
branch: z.string().optional().describe('New branch to create in the worktree'),
|
|
206
|
+
}),
|
|
207
|
+
}),
|
|
208
|
+
];
|
|
209
|
+
}
|
|
210
|
+
registerToolFactory(createGitTools);
|
|
@@ -0,0 +1,158 @@
|
|
|
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 } 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: 'Check if a host is reachable on a given port (TCP connect check).',
|
|
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
|
+
await fs.ensureDir(path.dirname(destPath));
|
|
125
|
+
const controller = new AbortController();
|
|
126
|
+
const timer = setTimeout(() => controller.abort(), timeout_ms ?? 60_000);
|
|
127
|
+
try {
|
|
128
|
+
const response = await fetch(url, { signal: controller.signal });
|
|
129
|
+
if (!response.ok) {
|
|
130
|
+
return JSON.stringify({ success: false, error: `HTTP ${response.status}: ${response.statusText}` });
|
|
131
|
+
}
|
|
132
|
+
const buffer = await response.arrayBuffer();
|
|
133
|
+
await fs.writeFile(destPath, Buffer.from(buffer));
|
|
134
|
+
return JSON.stringify({
|
|
135
|
+
success: true,
|
|
136
|
+
url,
|
|
137
|
+
destination: destPath,
|
|
138
|
+
size_bytes: buffer.byteLength,
|
|
139
|
+
});
|
|
140
|
+
}
|
|
141
|
+
catch (err) {
|
|
142
|
+
return JSON.stringify({ success: false, error: err.message });
|
|
143
|
+
}
|
|
144
|
+
finally {
|
|
145
|
+
clearTimeout(timer);
|
|
146
|
+
}
|
|
147
|
+
}, {
|
|
148
|
+
name: 'download_file',
|
|
149
|
+
description: 'Download a file from a URL to a local path.',
|
|
150
|
+
schema: z.object({
|
|
151
|
+
url: z.string().describe('URL to download from'),
|
|
152
|
+
destination: z.string().describe('Local path to save the file'),
|
|
153
|
+
timeout_ms: z.number().optional().describe('Timeout in ms, default 60000'),
|
|
154
|
+
}),
|
|
155
|
+
}),
|
|
156
|
+
];
|
|
157
|
+
}
|
|
158
|
+
registerToolFactory(createNetworkTools);
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { tool } from '@langchain/core/tools';
|
|
2
|
+
import { z } from 'zod';
|
|
3
|
+
import { ShellAdapter } from '../adapters/shell.js';
|
|
4
|
+
import { truncateOutput, isCommandAllowed } from '../utils.js';
|
|
5
|
+
import { registerToolFactory } from '../registry.js';
|
|
6
|
+
export function createPackageTools(ctx) {
|
|
7
|
+
const shell = ShellAdapter.create();
|
|
8
|
+
async function run(binary, args, timeout_ms) {
|
|
9
|
+
if (!isCommandAllowed(binary, ctx.allowed_commands)) {
|
|
10
|
+
return `'${binary}' is not in the allowed_commands list.`;
|
|
11
|
+
}
|
|
12
|
+
const result = await shell.run(binary, args, {
|
|
13
|
+
cwd: ctx.working_dir,
|
|
14
|
+
timeout_ms: timeout_ms ?? ctx.timeout_ms ?? 120_000,
|
|
15
|
+
});
|
|
16
|
+
return truncateOutput(result.stdout + (result.stderr ? '\n' + result.stderr : ''));
|
|
17
|
+
}
|
|
18
|
+
return [
|
|
19
|
+
tool(async ({ packages, save_dev, timeout_ms }) => {
|
|
20
|
+
const args = ['install'];
|
|
21
|
+
if (packages?.length)
|
|
22
|
+
args.push(...packages);
|
|
23
|
+
if (save_dev)
|
|
24
|
+
args.push('--save-dev');
|
|
25
|
+
return run('npm', args, timeout_ms);
|
|
26
|
+
}, {
|
|
27
|
+
name: 'npm_install',
|
|
28
|
+
description: 'Install npm packages. Runs "npm install [packages]".',
|
|
29
|
+
schema: z.object({
|
|
30
|
+
packages: z.array(z.string()).optional().describe('Specific packages to install, omit to install all from package.json'),
|
|
31
|
+
save_dev: z.boolean().optional().describe('Install as devDependency'),
|
|
32
|
+
timeout_ms: z.number().optional(),
|
|
33
|
+
}),
|
|
34
|
+
}),
|
|
35
|
+
tool(async ({ script, args, timeout_ms }) => {
|
|
36
|
+
const npmArgs = ['run', script];
|
|
37
|
+
if (args?.length)
|
|
38
|
+
npmArgs.push('--', ...args);
|
|
39
|
+
return run('npm', npmArgs, timeout_ms);
|
|
40
|
+
}, {
|
|
41
|
+
name: 'npm_run',
|
|
42
|
+
description: 'Run an npm script defined in package.json.',
|
|
43
|
+
schema: z.object({
|
|
44
|
+
script: z.string().describe('Script name from package.json'),
|
|
45
|
+
args: z.array(z.string()).optional().describe('Additional arguments after --'),
|
|
46
|
+
timeout_ms: z.number().optional(),
|
|
47
|
+
}),
|
|
48
|
+
}),
|
|
49
|
+
tool(async ({ packages, upgrade, timeout_ms }) => {
|
|
50
|
+
const args = [upgrade ? 'install' : 'install', '--upgrade', ...packages];
|
|
51
|
+
return run('pip3', packages.length ? ['install', ...packages] : ['install', '-r', 'requirements.txt'], timeout_ms);
|
|
52
|
+
}, {
|
|
53
|
+
name: 'pip_install',
|
|
54
|
+
description: 'Install Python packages with pip3.',
|
|
55
|
+
schema: z.object({
|
|
56
|
+
packages: z.array(z.string()).default([]).describe('Package names, omit to use requirements.txt'),
|
|
57
|
+
upgrade: z.boolean().optional().describe('Upgrade packages if already installed'),
|
|
58
|
+
timeout_ms: z.number().optional(),
|
|
59
|
+
}),
|
|
60
|
+
}),
|
|
61
|
+
tool(async ({ args, timeout_ms }) => {
|
|
62
|
+
return run('cargo', ['build', ...(args ?? [])], timeout_ms);
|
|
63
|
+
}, {
|
|
64
|
+
name: 'cargo_build',
|
|
65
|
+
description: 'Build a Rust project using cargo.',
|
|
66
|
+
schema: z.object({
|
|
67
|
+
args: z.array(z.string()).optional().describe('Additional cargo build arguments'),
|
|
68
|
+
timeout_ms: z.number().optional(),
|
|
69
|
+
}),
|
|
70
|
+
}),
|
|
71
|
+
];
|
|
72
|
+
}
|
|
73
|
+
registerToolFactory(createPackageTools);
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
import { tool } from '@langchain/core/tools';
|
|
2
|
+
import { z } from 'zod';
|
|
3
|
+
import os from 'os';
|
|
4
|
+
import { ShellAdapter } from '../adapters/shell.js';
|
|
5
|
+
import { truncateOutput } from '../utils.js';
|
|
6
|
+
import { registerToolFactory } from '../registry.js';
|
|
7
|
+
import { platform } from 'os';
|
|
8
|
+
export function createProcessTools(ctx) {
|
|
9
|
+
const shell = ShellAdapter.create();
|
|
10
|
+
const isWindows = platform() === 'win32';
|
|
11
|
+
return [
|
|
12
|
+
tool(async ({ filter }) => {
|
|
13
|
+
let result;
|
|
14
|
+
if (isWindows) {
|
|
15
|
+
result = await shell.run('tasklist', filter ? ['/FI', `IMAGENAME eq ${filter}*`] : [], {
|
|
16
|
+
cwd: ctx.working_dir, timeout_ms: 10_000,
|
|
17
|
+
});
|
|
18
|
+
}
|
|
19
|
+
else {
|
|
20
|
+
result = await shell.run('ps', ['aux'], { cwd: ctx.working_dir, timeout_ms: 10_000 });
|
|
21
|
+
if (filter && result.exitCode === 0) {
|
|
22
|
+
result.stdout = result.stdout
|
|
23
|
+
.split('\n')
|
|
24
|
+
.filter((l, i) => i === 0 || l.toLowerCase().includes(filter.toLowerCase()))
|
|
25
|
+
.join('\n');
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
return truncateOutput(result.stdout || result.stderr);
|
|
29
|
+
}, {
|
|
30
|
+
name: 'list_processes',
|
|
31
|
+
description: 'List running processes, optionally filtered by name.',
|
|
32
|
+
schema: z.object({
|
|
33
|
+
filter: z.string().optional().describe('Filter by process name (partial match)'),
|
|
34
|
+
}),
|
|
35
|
+
}),
|
|
36
|
+
tool(async ({ pid, name }) => {
|
|
37
|
+
let result;
|
|
38
|
+
if (isWindows) {
|
|
39
|
+
const filter = pid ? `/FI "PID eq ${pid}"` : `/FI "IMAGENAME eq ${name}*"`;
|
|
40
|
+
result = await shell.run('tasklist', ['/FI', filter.replace(/"/g, '').split(' ').join(' ')], {
|
|
41
|
+
cwd: ctx.working_dir, timeout_ms: 5_000,
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
else {
|
|
45
|
+
const query = pid ? String(pid) : name ?? '';
|
|
46
|
+
result = await shell.run('ps', ['-p', query, '-o', 'pid,ppid,cmd,%cpu,%mem'], {
|
|
47
|
+
cwd: ctx.working_dir, timeout_ms: 5_000,
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
return truncateOutput(result.stdout || result.stderr);
|
|
51
|
+
}, {
|
|
52
|
+
name: 'get_process',
|
|
53
|
+
description: 'Get info about a specific process by PID or name.',
|
|
54
|
+
schema: z.object({
|
|
55
|
+
pid: z.number().int().optional().describe('Process ID'),
|
|
56
|
+
name: z.string().optional().describe('Process name'),
|
|
57
|
+
}),
|
|
58
|
+
}),
|
|
59
|
+
tool(async ({ pid, force }) => {
|
|
60
|
+
let result;
|
|
61
|
+
if (isWindows) {
|
|
62
|
+
const args = ['taskkill', '/PID', String(pid)];
|
|
63
|
+
if (force)
|
|
64
|
+
args.push('/F');
|
|
65
|
+
result = await shell.run('taskkill', ['/PID', String(pid), ...(force ? ['/F'] : [])], {
|
|
66
|
+
cwd: ctx.working_dir, timeout_ms: 10_000,
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
else {
|
|
70
|
+
const signal = force ? '-9' : '-15';
|
|
71
|
+
result = await shell.run('kill', [signal, String(pid)], {
|
|
72
|
+
cwd: ctx.working_dir, timeout_ms: 10_000,
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
return JSON.stringify({
|
|
76
|
+
success: result.exitCode === 0,
|
|
77
|
+
stdout: result.stdout.trim(),
|
|
78
|
+
stderr: result.stderr.trim(),
|
|
79
|
+
});
|
|
80
|
+
}, {
|
|
81
|
+
name: 'kill_process',
|
|
82
|
+
description: 'Kill a process by PID. Use force=true for SIGKILL.',
|
|
83
|
+
schema: z.object({
|
|
84
|
+
pid: z.number().int().describe('Process ID to kill'),
|
|
85
|
+
force: z.boolean().optional().describe('Force kill (SIGKILL / /F), default false'),
|
|
86
|
+
}),
|
|
87
|
+
}),
|
|
88
|
+
tool(async () => {
|
|
89
|
+
const cpus = os.cpus();
|
|
90
|
+
return JSON.stringify({
|
|
91
|
+
platform: os.platform(),
|
|
92
|
+
arch: os.arch(),
|
|
93
|
+
release: os.release(),
|
|
94
|
+
hostname: os.hostname(),
|
|
95
|
+
cpus: cpus.length,
|
|
96
|
+
cpu_model: cpus[0]?.model ?? 'unknown',
|
|
97
|
+
total_memory_mb: Math.round(os.totalmem() / 1024 / 1024),
|
|
98
|
+
free_memory_mb: Math.round(os.freemem() / 1024 / 1024),
|
|
99
|
+
uptime_seconds: Math.round(os.uptime()),
|
|
100
|
+
load_avg: os.loadavg(),
|
|
101
|
+
home_dir: os.homedir(),
|
|
102
|
+
tmp_dir: os.tmpdir(),
|
|
103
|
+
});
|
|
104
|
+
}, {
|
|
105
|
+
name: 'system_info',
|
|
106
|
+
description: 'Get system information (OS, CPU, RAM, uptime).',
|
|
107
|
+
schema: z.object({}),
|
|
108
|
+
}),
|
|
109
|
+
tool(async ({ name, all }) => {
|
|
110
|
+
if (all)
|
|
111
|
+
return JSON.stringify(process.env);
|
|
112
|
+
if (name)
|
|
113
|
+
return JSON.stringify({ [name]: process.env[name] ?? null });
|
|
114
|
+
// Return non-sensitive vars
|
|
115
|
+
const safe = Object.fromEntries(Object.entries(process.env).filter(([k]) => !k.toLowerCase().includes('key') &&
|
|
116
|
+
!k.toLowerCase().includes('token') &&
|
|
117
|
+
!k.toLowerCase().includes('secret') &&
|
|
118
|
+
!k.toLowerCase().includes('password')));
|
|
119
|
+
return truncateOutput(JSON.stringify(safe, null, 2));
|
|
120
|
+
}, {
|
|
121
|
+
name: 'env_read',
|
|
122
|
+
description: 'Read environment variables. Sensitive keys (API_KEY, TOKEN, etc.) are filtered unless all=true.',
|
|
123
|
+
schema: z.object({
|
|
124
|
+
name: z.string().optional().describe('Specific env var name'),
|
|
125
|
+
all: z.boolean().optional().describe('Include all vars including sensitive ones'),
|
|
126
|
+
}),
|
|
127
|
+
}),
|
|
128
|
+
];
|
|
129
|
+
}
|
|
130
|
+
registerToolFactory(createProcessTools);
|
|
@@ -0,0 +1,94 @@
|
|
|
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 os from 'os';
|
|
6
|
+
import { randomUUID } from 'crypto';
|
|
7
|
+
import { ShellAdapter } from '../adapters/shell.js';
|
|
8
|
+
import { truncateOutput, isCommandAllowed } from '../utils.js';
|
|
9
|
+
import { registerToolFactory } from '../registry.js';
|
|
10
|
+
export function createShellTools(ctx) {
|
|
11
|
+
const shell = ShellAdapter.create();
|
|
12
|
+
return [
|
|
13
|
+
tool(async ({ command, args, timeout_ms, cwd }) => {
|
|
14
|
+
if (!isCommandAllowed(command, ctx.allowed_commands)) {
|
|
15
|
+
return JSON.stringify({
|
|
16
|
+
success: false,
|
|
17
|
+
error: `Command '${command}' is not in the allowed_commands list for this project. Allowed: [${ctx.allowed_commands.join(', ')}]`,
|
|
18
|
+
});
|
|
19
|
+
}
|
|
20
|
+
const result = await shell.run(command, args ?? [], {
|
|
21
|
+
cwd: cwd ?? ctx.working_dir,
|
|
22
|
+
timeout_ms: timeout_ms ?? ctx.timeout_ms ?? 30_000,
|
|
23
|
+
});
|
|
24
|
+
return JSON.stringify({
|
|
25
|
+
success: result.exitCode === 0,
|
|
26
|
+
stdout: truncateOutput(result.stdout),
|
|
27
|
+
stderr: truncateOutput(result.stderr),
|
|
28
|
+
exitCode: result.exitCode,
|
|
29
|
+
timedOut: result.timedOut,
|
|
30
|
+
});
|
|
31
|
+
}, {
|
|
32
|
+
name: 'run_command',
|
|
33
|
+
description: 'Run a shell command. The command binary must be in the project allowlist.',
|
|
34
|
+
schema: z.object({
|
|
35
|
+
command: z.string().describe('Command/binary to run'),
|
|
36
|
+
args: z.array(z.string()).optional().describe('Arguments array'),
|
|
37
|
+
timeout_ms: z.number().optional().describe('Override timeout in milliseconds'),
|
|
38
|
+
cwd: z.string().optional().describe('Override working directory'),
|
|
39
|
+
}),
|
|
40
|
+
}),
|
|
41
|
+
tool(async ({ script, language, timeout_ms }) => {
|
|
42
|
+
const lang = language ?? 'bash';
|
|
43
|
+
const ext = lang === 'python' ? 'py' : lang === 'node' ? 'js' : 'sh';
|
|
44
|
+
const tmpFile = path.join(os.tmpdir(), `morpheus-script-${randomUUID()}.${ext}`);
|
|
45
|
+
try {
|
|
46
|
+
await fs.writeFile(tmpFile, script, 'utf8');
|
|
47
|
+
const binaryMap = {
|
|
48
|
+
bash: 'bash',
|
|
49
|
+
python: 'python3',
|
|
50
|
+
node: 'node',
|
|
51
|
+
sh: 'sh',
|
|
52
|
+
};
|
|
53
|
+
const binary = binaryMap[lang] ?? lang;
|
|
54
|
+
if (!isCommandAllowed(binary, ctx.allowed_commands)) {
|
|
55
|
+
return JSON.stringify({
|
|
56
|
+
success: false,
|
|
57
|
+
error: `Script runtime '${binary}' is not in the allowed_commands list.`,
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
const result = await shell.run(binary, [tmpFile], {
|
|
61
|
+
cwd: ctx.working_dir,
|
|
62
|
+
timeout_ms: timeout_ms ?? ctx.timeout_ms ?? 60_000,
|
|
63
|
+
});
|
|
64
|
+
return JSON.stringify({
|
|
65
|
+
success: result.exitCode === 0,
|
|
66
|
+
stdout: truncateOutput(result.stdout),
|
|
67
|
+
stderr: truncateOutput(result.stderr),
|
|
68
|
+
exitCode: result.exitCode,
|
|
69
|
+
timedOut: result.timedOut,
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
finally {
|
|
73
|
+
await fs.remove(tmpFile).catch(() => { });
|
|
74
|
+
}
|
|
75
|
+
}, {
|
|
76
|
+
name: 'run_script',
|
|
77
|
+
description: 'Write and execute an inline script (bash, python, node, sh).',
|
|
78
|
+
schema: z.object({
|
|
79
|
+
script: z.string().describe('Script content to execute'),
|
|
80
|
+
language: z.enum(['bash', 'python', 'node', 'sh']).optional().describe('Script language, default bash'),
|
|
81
|
+
timeout_ms: z.number().optional(),
|
|
82
|
+
}),
|
|
83
|
+
}),
|
|
84
|
+
tool(async ({ binary }) => {
|
|
85
|
+
const location = await shell.which(binary);
|
|
86
|
+
return JSON.stringify({ found: Boolean(location), path: location });
|
|
87
|
+
}, {
|
|
88
|
+
name: 'which',
|
|
89
|
+
description: 'Find the location of a binary in the system PATH.',
|
|
90
|
+
schema: z.object({ binary: z.string() }),
|
|
91
|
+
}),
|
|
92
|
+
];
|
|
93
|
+
}
|
|
94
|
+
registerToolFactory(createShellTools);
|