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,73 +0,0 @@
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, 'packages');
@@ -1,130 +0,0 @@
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, 'processes');
@@ -1,106 +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 os from 'os';
6
- import { randomUUID } from 'crypto';
7
- import { ShellAdapter } from '../adapters/shell.js';
8
- import { truncateOutput, isCommandAllowed, isWithinDir } 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
- // Enforce sandbox_dir: override cwd to stay within sandbox
21
- let effectiveCwd = cwd ?? ctx.working_dir;
22
- if (ctx.sandbox_dir) {
23
- const resolvedCwd = path.isAbsolute(effectiveCwd) ? effectiveCwd : path.resolve(ctx.sandbox_dir, effectiveCwd);
24
- if (!isWithinDir(resolvedCwd, ctx.sandbox_dir)) {
25
- return JSON.stringify({
26
- success: false,
27
- error: `Working directory '${resolvedCwd}' is outside the sandbox directory '${ctx.sandbox_dir}'. Operation denied.`,
28
- });
29
- }
30
- effectiveCwd = resolvedCwd;
31
- }
32
- const result = await shell.run(command, args ?? [], {
33
- cwd: effectiveCwd,
34
- timeout_ms: timeout_ms ?? ctx.timeout_ms ?? 30_000,
35
- });
36
- return JSON.stringify({
37
- success: result.exitCode === 0,
38
- stdout: truncateOutput(result.stdout),
39
- stderr: truncateOutput(result.stderr),
40
- exitCode: result.exitCode,
41
- timedOut: result.timedOut,
42
- });
43
- }, {
44
- name: 'run_command',
45
- description: 'Run a shell command. The command binary must be in the project allowlist.',
46
- schema: z.object({
47
- command: z.string().describe('Command/binary to run'),
48
- args: z.array(z.string()).optional().describe('Arguments array'),
49
- timeout_ms: z.number().optional().describe('Override timeout in milliseconds'),
50
- cwd: z.string().optional().describe('Override working directory'),
51
- }),
52
- }),
53
- tool(async ({ script, language, timeout_ms }) => {
54
- const lang = language ?? 'bash';
55
- const ext = lang === 'python' ? 'py' : lang === 'node' ? 'js' : 'sh';
56
- const tmpFile = path.join(os.tmpdir(), `morpheus-script-${randomUUID()}.${ext}`);
57
- try {
58
- await fs.writeFile(tmpFile, script, 'utf8');
59
- const binaryMap = {
60
- bash: 'bash',
61
- python: 'python3',
62
- node: 'node',
63
- sh: 'sh',
64
- };
65
- const binary = binaryMap[lang] ?? lang;
66
- if (!isCommandAllowed(binary, ctx.allowed_commands)) {
67
- return JSON.stringify({
68
- success: false,
69
- error: `Script runtime '${binary}' is not in the allowed_commands list.`,
70
- });
71
- }
72
- const result = await shell.run(binary, [tmpFile], {
73
- cwd: ctx.working_dir,
74
- timeout_ms: timeout_ms ?? ctx.timeout_ms ?? 60_000,
75
- });
76
- return JSON.stringify({
77
- success: result.exitCode === 0,
78
- stdout: truncateOutput(result.stdout),
79
- stderr: truncateOutput(result.stderr),
80
- exitCode: result.exitCode,
81
- timedOut: result.timedOut,
82
- });
83
- }
84
- finally {
85
- await fs.remove(tmpFile).catch(() => { });
86
- }
87
- }, {
88
- name: 'run_script',
89
- description: 'Write and execute an inline script (bash, python, node, sh).',
90
- schema: z.object({
91
- script: z.string().describe('Script content to execute'),
92
- language: z.enum(['bash', 'python', 'node', 'sh']).optional().describe('Script language, default bash'),
93
- timeout_ms: z.number().optional(),
94
- }),
95
- }),
96
- tool(async ({ binary }) => {
97
- const location = await shell.which(binary);
98
- return JSON.stringify({ found: Boolean(location), path: location });
99
- }, {
100
- name: 'which',
101
- description: 'Find the location of a binary in the system PATH.',
102
- schema: z.object({ binary: z.string() }),
103
- }),
104
- ];
105
- }
106
- registerToolFactory(createShellTools, 'shell');
@@ -1,132 +0,0 @@
1
- import { tool } from '@langchain/core/tools';
2
- import { z } from 'zod';
3
- import { ShellAdapter } from '../adapters/shell.js';
4
- import { registerToolFactory } from '../registry.js';
5
- import { platform } from 'os';
6
- export function createSystemTools(ctx) {
7
- const shell = ShellAdapter.create();
8
- const isWindows = platform() === 'win32';
9
- const isMac = platform() === 'darwin';
10
- return [
11
- tool(async ({ title, message, urgency }) => {
12
- try {
13
- if (isWindows) {
14
- // PowerShell toast notification
15
- const ps = `[Windows.UI.Notifications.ToastNotificationManager, Windows.UI.Notifications, ContentType=WindowsRuntime] | Out-Null; $template = [Windows.UI.Notifications.ToastNotificationManager]::GetTemplateContent([Windows.UI.Notifications.ToastTemplateType]::ToastText02); $template.GetElementsByTagName('text')[0].AppendChild($template.CreateTextNode('${title}')); $template.GetElementsByTagName('text')[1].AppendChild($template.CreateTextNode('${message}')); [Windows.UI.Notifications.ToastNotificationManager]::CreateToastNotifier('Morpheus').Show([Windows.UI.Notifications.ToastNotification]::new($template))`;
16
- await shell.run('powershell', ['-Command', ps], { cwd: ctx.working_dir, timeout_ms: 5_000 });
17
- }
18
- else if (isMac) {
19
- await shell.run('osascript', ['-e', `display notification "${message}" with title "${title}"`], {
20
- cwd: ctx.working_dir, timeout_ms: 5_000,
21
- });
22
- }
23
- else {
24
- // Linux — notify-send
25
- const args = [title, message];
26
- if (urgency)
27
- args.unshift(`-u`, urgency);
28
- await shell.run('notify-send', args, { cwd: ctx.working_dir, timeout_ms: 5_000 });
29
- }
30
- return JSON.stringify({ success: true });
31
- }
32
- catch (err) {
33
- return JSON.stringify({ success: false, error: err.message });
34
- }
35
- }, {
36
- name: 'notify',
37
- description: 'Send a desktop notification.',
38
- schema: z.object({
39
- title: z.string(),
40
- message: z.string(),
41
- urgency: z.enum(['low', 'normal', 'critical']).optional().describe('Linux urgency level'),
42
- }),
43
- }),
44
- tool(async () => {
45
- try {
46
- let result;
47
- if (isWindows) {
48
- result = await shell.run('powershell', ['-Command', 'Get-Clipboard'], {
49
- cwd: ctx.working_dir, timeout_ms: 5_000,
50
- });
51
- }
52
- else if (isMac) {
53
- result = await shell.run('pbpaste', [], { cwd: ctx.working_dir, timeout_ms: 5_000 });
54
- }
55
- else {
56
- result = await shell.run('xclip', ['-selection', 'clipboard', '-o'], {
57
- cwd: ctx.working_dir, timeout_ms: 5_000,
58
- });
59
- }
60
- return JSON.stringify({ success: result.exitCode === 0, content: result.stdout });
61
- }
62
- catch (err) {
63
- return JSON.stringify({ success: false, error: err.message });
64
- }
65
- }, {
66
- name: 'read_clipboard',
67
- description: 'Read the current clipboard contents.',
68
- schema: z.object({}),
69
- }),
70
- tool(async ({ content }) => {
71
- try {
72
- let result;
73
- if (isWindows) {
74
- result = await shell.run('powershell', ['-Command', `Set-Clipboard -Value '${content.replace(/'/g, "''")}'`], {
75
- cwd: ctx.working_dir, timeout_ms: 5_000,
76
- });
77
- }
78
- else if (isMac) {
79
- result = await shell.run('sh', ['-c', `printf '%s' '${content.replace(/'/g, "'\\''")}' | pbcopy`], {
80
- cwd: ctx.working_dir, timeout_ms: 5_000,
81
- });
82
- }
83
- else {
84
- result = await shell.run('sh', ['-c', `printf '%s' '${content.replace(/'/g, "'\\''")}' | xclip -selection clipboard`], {
85
- cwd: ctx.working_dir, timeout_ms: 5_000,
86
- });
87
- }
88
- return JSON.stringify({ success: result.exitCode === 0 });
89
- }
90
- catch (err) {
91
- return JSON.stringify({ success: false, error: err.message });
92
- }
93
- }, {
94
- name: 'write_clipboard',
95
- description: 'Write content to the clipboard.',
96
- schema: z.object({ content: z.string() }),
97
- }),
98
- tool(async ({ url }) => {
99
- try {
100
- const open = isWindows ? 'start' : isMac ? 'open' : 'xdg-open';
101
- const result = await shell.run(isWindows ? 'cmd' : open, isWindows ? ['/c', 'start', url] : [url], {
102
- cwd: ctx.working_dir, timeout_ms: 5_000,
103
- });
104
- return JSON.stringify({ success: result.exitCode === 0, url });
105
- }
106
- catch (err) {
107
- return JSON.stringify({ success: false, error: err.message });
108
- }
109
- }, {
110
- name: 'open_url',
111
- description: 'Open a URL in the default browser.',
112
- schema: z.object({ url: z.string() }),
113
- }),
114
- tool(async ({ file_path }) => {
115
- try {
116
- const open = isWindows ? 'start' : isMac ? 'open' : 'xdg-open';
117
- const result = await shell.run(isWindows ? 'cmd' : open, isWindows ? ['/c', 'start', '""', file_path] : [file_path], {
118
- cwd: ctx.working_dir, timeout_ms: 5_000,
119
- });
120
- return JSON.stringify({ success: result.exitCode === 0, file_path });
121
- }
122
- catch (err) {
123
- return JSON.stringify({ success: false, error: err.message });
124
- }
125
- }, {
126
- name: 'open_file',
127
- description: 'Open a file with the default application.',
128
- schema: z.object({ file_path: z.string() }),
129
- }),
130
- ];
131
- }
132
- registerToolFactory(createSystemTools, 'system');
@@ -1 +0,0 @@
1
- export const MAX_OUTPUT_BYTES = 50 * 1024; // 50 KB
@@ -1,45 +0,0 @@
1
- import path from 'path';
2
- import { MAX_OUTPUT_BYTES } from './types.js';
3
- /**
4
- * Truncates a string to MAX_OUTPUT_BYTES (50 KB) if needed.
5
- * Returns a UTF-8-safe truncation with a note when truncated.
6
- */
7
- export function truncateOutput(output) {
8
- const bytes = Buffer.byteLength(output, 'utf8');
9
- if (bytes <= MAX_OUTPUT_BYTES)
10
- return output;
11
- const truncated = Buffer.from(output).subarray(0, MAX_OUTPUT_BYTES).toString('utf8');
12
- return truncated + `\n\n[OUTPUT TRUNCATED: ${bytes} bytes total, showing first ${MAX_OUTPUT_BYTES} bytes]`;
13
- }
14
- /**
15
- * Returns true if filePath is inside dir (or equal to dir).
16
- * Both paths are resolved before comparison.
17
- */
18
- export function isWithinDir(filePath, dir) {
19
- const resolved = path.resolve(filePath);
20
- const resolvedDir = path.resolve(dir);
21
- return resolved === resolvedDir || resolved.startsWith(resolvedDir + path.sep);
22
- }
23
- /**
24
- * Extracts the binary base name from a command string.
25
- * Handles full paths (/usr/bin/node, C:\bin\node.exe) and plain names.
26
- */
27
- export function extractBinaryName(command) {
28
- // Take first token (before any space), then get the basename, strip extension
29
- const firstToken = command.split(/\s+/)[0] ?? command;
30
- const base = path.basename(firstToken);
31
- return base.replace(/\.(exe|cmd|bat|sh|ps1)$/i, '').toLowerCase();
32
- }
33
- /**
34
- * Checks if a command is allowed based on the allowlist.
35
- * Empty allowlist means ALL commands are allowed (Merovingian mode).
36
- */
37
- export function isCommandAllowed(command, allowedCommands) {
38
- if (allowedCommands.length === 0)
39
- return true;
40
- const binary = extractBinaryName(command);
41
- return allowedCommands.some(allowed => {
42
- const allowedBinary = extractBinaryName(allowed);
43
- return allowedBinary === binary;
44
- });
45
- }