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
package/README.md
CHANGED
|
@@ -153,6 +153,13 @@ The system also supports generic environment variables that apply to all provide
|
|
|
153
153
|
| `MORPHEUS_SATI_MEMORY_LIMIT` | Memory retrieval limit for Sati | santi.memory_limit |
|
|
154
154
|
| `MORPHEUS_SATI_MEMORY_LIMIT` | Memory retrieval limit for Sati | santi.memory_limit |
|
|
155
155
|
| `MORPHEUS_SATI_ENABLED_ARCHIVED_SESSIONS`| Enable/disable retrieval of archived sessions in Sati | santi.enableArchivedSessions |
|
|
156
|
+
| `MORPHEUS_APOC_PROVIDER` | Apoc LLM provider | apoc.provider |
|
|
157
|
+
| `MORPHEUS_APOC_MODEL` | Model name for Apoc | apoc.model |
|
|
158
|
+
| `MORPHEUS_APOC_TEMPERATURE` | Temperature for Apoc | apoc.temperature |
|
|
159
|
+
| `MORPHEUS_APOC_MAX_TOKENS` | Maximum tokens for Apoc | apoc.max_tokens |
|
|
160
|
+
| `MORPHEUS_APOC_API_KEY` | API key for Apoc (falls back to provider-specific key) | apoc.api_key |
|
|
161
|
+
| `MORPHEUS_APOC_WORKING_DIR` | Working directory for Apoc file/shell operations | apoc.working_dir |
|
|
162
|
+
| `MORPHEUS_APOC_TIMEOUT_MS` | Timeout in ms for Apoc shell operations (default: 30000) | apoc.timeout_ms |
|
|
156
163
|
| `MORPHEUS_AUDIO_MODEL` | Model name for audio processing | audio.model |
|
|
157
164
|
| `MORPHEUS_AUDIO_ENABLED` | Enable/disable audio processing | audio.enabled |
|
|
158
165
|
| `MORPHEUS_AUDIO_API_KEY` | Generic API key for audio (lower precedence than provider-specific keys) | audio.apiKey |
|
|
@@ -206,6 +213,28 @@ When enabled:
|
|
|
206
213
|
### 🧩 MCP Support (Model Context Protocol)
|
|
207
214
|
Full integration with [Model Context Protocol](https://modelcontextprotocol.io/), allowing Morpheus to use standardized tools from any MCP-compatible server.
|
|
208
215
|
|
|
216
|
+
### 🛠️ Apoc (DevTools Subagent)
|
|
217
|
+
|
|
218
|
+
Morpheus includes **Apoc**, a specialized subagent invoked by Oracle whenever the user requests developer-level operations. Apoc runs with access to the **DevKit** tool set:
|
|
219
|
+
|
|
220
|
+
| Tool Group | Capabilities |
|
|
221
|
+
|---|---|
|
|
222
|
+
| **Filesystem** | Read, write, append, delete files and directories |
|
|
223
|
+
| **Shell** | Execute shell commands and scripts with timeout control |
|
|
224
|
+
| **Git** | status, log, diff, commit, push, pull, clone, branch |
|
|
225
|
+
| **Packages** | npm/yarn install, update, audit, package.json inspection |
|
|
226
|
+
| **Processes** | List running processes, check ports, terminate processes |
|
|
227
|
+
| **Network** | curl, ping, DNS lookups, HTTP requests |
|
|
228
|
+
| **System** | Environment variables, OS info, disk space, memory usage |
|
|
229
|
+
|
|
230
|
+
Oracle delegates to Apoc via the `apoc_delegate` tool when you ask things like:
|
|
231
|
+
- *"Run npm install and show me any errors"*
|
|
232
|
+
- *"What's the git status of this repo?"*
|
|
233
|
+
- *"Read the contents of config.json"*
|
|
234
|
+
- *"Execute the build script and tell me what happened"*
|
|
235
|
+
|
|
236
|
+
Apoc is independently configurable — use a different (e.g., faster, cheaper) model than Oracle for tool execution tasks.
|
|
237
|
+
|
|
209
238
|
### 🧠 Sati (Long-Term Memory)
|
|
210
239
|
Morpheus features a dedicated middleware system called **Sati** (Mindfulness) that provides long-term memory capabilities.
|
|
211
240
|
- **Automated Storage**: Automatically extracts and saves preferences, project details, and facts from conversations.
|
|
@@ -309,6 +338,12 @@ santi: # Optional: Sati (Long-Term Memory) specific settings
|
|
|
309
338
|
provider: "openai" # defaults to llm.provider
|
|
310
339
|
model: "gpt-4o"
|
|
311
340
|
memory_limit: 1000 # Number of messages/items to retrieve
|
|
341
|
+
apoc: # Optional: Apoc DevTools subagent settings
|
|
342
|
+
provider: "openai" # defaults to llm.provider (can use a cheaper/faster model)
|
|
343
|
+
model: "gpt-4o-mini"
|
|
344
|
+
temperature: 0.2
|
|
345
|
+
working_dir: "/home/user/projects" # root dir for file/shell ops (defaults to process cwd)
|
|
346
|
+
timeout_ms: 30000 # shell command timeout in ms
|
|
312
347
|
channels:
|
|
313
348
|
telegram:
|
|
314
349
|
enabled: true
|
|
@@ -560,6 +595,55 @@ Update the Sati (long-term memory) configuration.
|
|
|
560
595
|
#### DELETE `/api/config/sati`
|
|
561
596
|
Remove the Sati (long-term memory) configuration (falls back to Oracle config).
|
|
562
597
|
|
|
598
|
+
* **Authentication:** Requires `Authorization` header with the password set in `THE_ARCHITECT_PASS`.
|
|
599
|
+
* **Response:**
|
|
600
|
+
```json
|
|
601
|
+
{
|
|
602
|
+
"success": true
|
|
603
|
+
}
|
|
604
|
+
```
|
|
605
|
+
|
|
606
|
+
|
|
607
|
+
#### GET `/api/config/apoc`
|
|
608
|
+
Retrieve the Apoc (DevTools subagent) configuration. Falls back to Oracle (LLM) config if not explicitly set.
|
|
609
|
+
|
|
610
|
+
* **Authentication:** Requires `Authorization` header with the password set in `THE_ARCHITECT_PASS`.
|
|
611
|
+
* **Response:**
|
|
612
|
+
```json
|
|
613
|
+
{
|
|
614
|
+
"provider": "openai",
|
|
615
|
+
"model": "gpt-4o-mini",
|
|
616
|
+
"temperature": 0.2,
|
|
617
|
+
"api_key": "***",
|
|
618
|
+
"working_dir": "/home/user/projects",
|
|
619
|
+
"timeout_ms": 30000
|
|
620
|
+
}
|
|
621
|
+
```
|
|
622
|
+
|
|
623
|
+
#### POST `/api/config/apoc`
|
|
624
|
+
Update the Apoc (DevTools subagent) configuration.
|
|
625
|
+
|
|
626
|
+
* **Authentication:** Requires `Authorization` header with the password set in `THE_ARCHITECT_PASS`.
|
|
627
|
+
* **Body:**
|
|
628
|
+
```json
|
|
629
|
+
{
|
|
630
|
+
"provider": "openai",
|
|
631
|
+
"model": "gpt-4o-mini",
|
|
632
|
+
"temperature": 0.2,
|
|
633
|
+
"working_dir": "/home/user/projects",
|
|
634
|
+
"timeout_ms": 30000
|
|
635
|
+
}
|
|
636
|
+
```
|
|
637
|
+
* **Response:**
|
|
638
|
+
```json
|
|
639
|
+
{
|
|
640
|
+
"success": true
|
|
641
|
+
}
|
|
642
|
+
```
|
|
643
|
+
|
|
644
|
+
#### DELETE `/api/config/apoc`
|
|
645
|
+
Remove the Apoc configuration (falls back to Oracle config).
|
|
646
|
+
|
|
563
647
|
* **Authentication:** Requires `Authorization` header with the password set in `THE_ARCHITECT_PASS`.
|
|
564
648
|
* **Response:**
|
|
565
649
|
```json
|
|
@@ -860,6 +944,10 @@ npm run test:watch
|
|
|
860
944
|
│ ├── cli/ # CLI commands and logic
|
|
861
945
|
│ ├── config/ # Configuration management
|
|
862
946
|
│ ├── runtime/ # Core agent logic, lifecycle, and providers
|
|
947
|
+
│ ├── apoc.ts # Apoc DevTools subagent (filesystem, shell, git, etc.)
|
|
948
|
+
│ ├── oracle.ts # Oracle main agent (LangChain ReactAgent)
|
|
949
|
+
│ └── providers/ # LLM provider factory (createBare for subagents)
|
|
950
|
+
├── devkit/ # DevKit tool factories (filesystem, shell, git, network, packages, processes, system)
|
|
863
951
|
│ ├── types/ # Shared TypeScript definitions
|
|
864
952
|
│ └── ui/ # React Web UI Dashboard
|
|
865
953
|
└── package.json
|
package/dist/config/manager.js
CHANGED
|
@@ -69,6 +69,22 @@ export class ConfigManager {
|
|
|
69
69
|
enabled_archived_sessions: resolveBoolean('MORPHEUS_SATI_ENABLED_ARCHIVED_SESSIONS', config.sati.enabled_archived_sessions, true)
|
|
70
70
|
};
|
|
71
71
|
}
|
|
72
|
+
// Apply precedence to Apoc config
|
|
73
|
+
let apocConfig;
|
|
74
|
+
if (config.apoc) {
|
|
75
|
+
const apocProvider = resolveProvider('MORPHEUS_APOC_PROVIDER', config.apoc.provider, llmConfig.provider);
|
|
76
|
+
apocConfig = {
|
|
77
|
+
provider: apocProvider,
|
|
78
|
+
model: resolveModel(apocProvider, 'MORPHEUS_APOC_MODEL', config.apoc.model || llmConfig.model),
|
|
79
|
+
temperature: resolveNumeric('MORPHEUS_APOC_TEMPERATURE', config.apoc.temperature, llmConfig.temperature),
|
|
80
|
+
max_tokens: config.apoc.max_tokens !== undefined ? resolveNumeric('MORPHEUS_APOC_MAX_TOKENS', config.apoc.max_tokens, config.apoc.max_tokens) : llmConfig.max_tokens,
|
|
81
|
+
api_key: resolveApiKey(apocProvider, 'MORPHEUS_APOC_API_KEY', config.apoc.api_key || llmConfig.api_key),
|
|
82
|
+
base_url: config.apoc.base_url || config.llm.base_url,
|
|
83
|
+
context_window: config.apoc.context_window !== undefined ? resolveNumeric('MORPHEUS_APOC_CONTEXT_WINDOW', config.apoc.context_window, config.apoc.context_window) : llmConfig.context_window,
|
|
84
|
+
working_dir: resolveString('MORPHEUS_APOC_WORKING_DIR', config.apoc.working_dir, process.cwd()),
|
|
85
|
+
timeout_ms: config.apoc.timeout_ms !== undefined ? resolveNumeric('MORPHEUS_APOC_TIMEOUT_MS', config.apoc.timeout_ms, 30_000) : 30_000
|
|
86
|
+
};
|
|
87
|
+
}
|
|
72
88
|
// Apply precedence to audio config
|
|
73
89
|
const audioProvider = resolveString('MORPHEUS_AUDIO_PROVIDER', config.audio.provider, DEFAULT_CONFIG.audio.provider);
|
|
74
90
|
// AudioProvider uses 'google' but resolveApiKey expects LLMProvider which uses 'gemini'
|
|
@@ -112,6 +128,7 @@ export class ConfigManager {
|
|
|
112
128
|
agent: agentConfig,
|
|
113
129
|
llm: llmConfig,
|
|
114
130
|
sati: satiConfig,
|
|
131
|
+
apoc: apocConfig,
|
|
115
132
|
audio: audioConfig,
|
|
116
133
|
channels: channelsConfig,
|
|
117
134
|
ui: uiConfig,
|
|
@@ -153,4 +170,19 @@ export class ConfigManager {
|
|
|
153
170
|
memory_limit: 10 // Default fallback
|
|
154
171
|
};
|
|
155
172
|
}
|
|
173
|
+
getApocConfig() {
|
|
174
|
+
if (this.config.apoc) {
|
|
175
|
+
return {
|
|
176
|
+
working_dir: process.cwd(),
|
|
177
|
+
timeout_ms: 30_000,
|
|
178
|
+
...this.config.apoc
|
|
179
|
+
};
|
|
180
|
+
}
|
|
181
|
+
// Fallback to main LLM config with Apoc defaults
|
|
182
|
+
return {
|
|
183
|
+
...this.config.llm,
|
|
184
|
+
working_dir: process.cwd(),
|
|
185
|
+
timeout_ms: 30_000
|
|
186
|
+
};
|
|
187
|
+
}
|
|
156
188
|
}
|
package/dist/config/schemas.js
CHANGED
|
@@ -22,6 +22,10 @@ export const SatiConfigSchema = LLMConfigSchema.extend({
|
|
|
22
22
|
memory_limit: z.number().int().positive().optional(),
|
|
23
23
|
enabled_archived_sessions: z.boolean().default(true),
|
|
24
24
|
});
|
|
25
|
+
export const ApocConfigSchema = LLMConfigSchema.extend({
|
|
26
|
+
working_dir: z.string().optional(),
|
|
27
|
+
timeout_ms: z.number().int().positive().optional(),
|
|
28
|
+
});
|
|
25
29
|
// Zod Schema matching MorpheusConfig interface
|
|
26
30
|
export const ConfigSchema = z.object({
|
|
27
31
|
agent: z.object({
|
|
@@ -30,6 +34,7 @@ export const ConfigSchema = z.object({
|
|
|
30
34
|
}).default(DEFAULT_CONFIG.agent),
|
|
31
35
|
llm: LLMConfigSchema.default(DEFAULT_CONFIG.llm),
|
|
32
36
|
sati: SatiConfigSchema.optional(),
|
|
37
|
+
apoc: ApocConfigSchema.optional(),
|
|
33
38
|
audio: AudioConfigSchema.default(DEFAULT_CONFIG.audio),
|
|
34
39
|
memory: z.object({
|
|
35
40
|
limit: z.number().int().positive().optional(),
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import { platform } from 'os';
|
|
2
|
+
export class ShellAdapter {
|
|
3
|
+
/**
|
|
4
|
+
* Factory: returns the appropriate adapter for the current OS.
|
|
5
|
+
* Uses direct imports (ESM-compatible, no require()).
|
|
6
|
+
*/
|
|
7
|
+
static create() {
|
|
8
|
+
switch (platform()) {
|
|
9
|
+
case 'win32': return new WindowsAdapter();
|
|
10
|
+
case 'darwin': return new MacAdapter();
|
|
11
|
+
default: return new LinuxAdapter();
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
// ─── Inline implementations (avoids ESM dynamic import issues) ────────────────
|
|
16
|
+
import { spawn } from 'child_process';
|
|
17
|
+
class WindowsAdapter extends ShellAdapter {
|
|
18
|
+
getShell() { return { shell: 'cmd.exe', flag: '/c' }; }
|
|
19
|
+
async run(command, args, options) {
|
|
20
|
+
return spawnCommand(command, args, { ...options, windowsHide: true, shell: true });
|
|
21
|
+
}
|
|
22
|
+
async which(binary) {
|
|
23
|
+
const result = await this.run('where', [binary], { cwd: process.cwd(), timeout_ms: 5000 });
|
|
24
|
+
if (result.exitCode !== 0)
|
|
25
|
+
return null;
|
|
26
|
+
const first = result.stdout.trim().split(/\r?\n/)[0];
|
|
27
|
+
return first || null;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
class LinuxAdapter extends ShellAdapter {
|
|
31
|
+
getShell() { return { shell: '/bin/bash', flag: '-c' }; }
|
|
32
|
+
async run(command, args, options) {
|
|
33
|
+
return spawnCommand(command, args, { ...options, shell: false });
|
|
34
|
+
}
|
|
35
|
+
async which(binary) {
|
|
36
|
+
const result = await this.run('which', [binary], { cwd: process.cwd(), timeout_ms: 5000 });
|
|
37
|
+
if (result.exitCode !== 0)
|
|
38
|
+
return null;
|
|
39
|
+
return result.stdout.trim() || null;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
class MacAdapter extends ShellAdapter {
|
|
43
|
+
getShell() { return { shell: '/bin/zsh', flag: '-c' }; }
|
|
44
|
+
async run(command, args, options) {
|
|
45
|
+
return spawnCommand(command, args, { ...options, shell: false });
|
|
46
|
+
}
|
|
47
|
+
async which(binary) {
|
|
48
|
+
const result = await this.run('which', [binary], { cwd: process.cwd(), timeout_ms: 5000 });
|
|
49
|
+
if (result.exitCode !== 0)
|
|
50
|
+
return null;
|
|
51
|
+
return result.stdout.trim() || null;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
function spawnCommand(command, args, options) {
|
|
55
|
+
return new Promise((resolve) => {
|
|
56
|
+
let stdout = '';
|
|
57
|
+
let stderr = '';
|
|
58
|
+
let timedOut = false;
|
|
59
|
+
const child = spawn(command, args, {
|
|
60
|
+
cwd: options.cwd,
|
|
61
|
+
env: { ...process.env, ...options.env },
|
|
62
|
+
shell: options.shell ?? false,
|
|
63
|
+
windowsHide: options.windowsHide ?? false,
|
|
64
|
+
});
|
|
65
|
+
const timer = setTimeout(() => {
|
|
66
|
+
timedOut = true;
|
|
67
|
+
child.kill('SIGKILL');
|
|
68
|
+
}, options.timeout_ms);
|
|
69
|
+
child.stdout?.on('data', (d) => { stdout += d.toString(); });
|
|
70
|
+
child.stderr?.on('data', (d) => { stderr += d.toString(); });
|
|
71
|
+
child.on('error', (err) => {
|
|
72
|
+
clearTimeout(timer);
|
|
73
|
+
resolve({ exitCode: 1, stdout, stderr: stderr + err.message, timedOut });
|
|
74
|
+
});
|
|
75
|
+
child.on('close', (code) => {
|
|
76
|
+
clearTimeout(timer);
|
|
77
|
+
resolve({ exitCode: code ?? 1, stdout, stderr, timedOut });
|
|
78
|
+
});
|
|
79
|
+
});
|
|
80
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
// Register all DevKit tool factories
|
|
2
|
+
// Import order matters: each import triggers registerToolFactory() as a side effect
|
|
3
|
+
import './tools/filesystem.js';
|
|
4
|
+
import './tools/shell.js';
|
|
5
|
+
import './tools/processes.js';
|
|
6
|
+
import './tools/network.js';
|
|
7
|
+
import './tools/git.js';
|
|
8
|
+
import './tools/packages.js';
|
|
9
|
+
import './tools/system.js';
|
|
10
|
+
export { buildDevKit } from './registry.js';
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
const factories = [];
|
|
2
|
+
export function registerToolFactory(factory) {
|
|
3
|
+
factories.push(factory);
|
|
4
|
+
}
|
|
5
|
+
/**
|
|
6
|
+
* Builds the full DevKit tool set for a given context.
|
|
7
|
+
* Each factory receives the context (working_dir, allowed_commands, etc.)
|
|
8
|
+
* and returns tools with the context captured in closure.
|
|
9
|
+
*/
|
|
10
|
+
export function buildDevKit(ctx) {
|
|
11
|
+
return factories.flatMap(factory => factory(ctx));
|
|
12
|
+
}
|
|
@@ -0,0 +1,219 @@
|
|
|
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
|
+
// Always resolve relative to working_dir
|
|
10
|
+
const resolved = path.isAbsolute(filePath) ? filePath : path.resolve(ctx.working_dir, filePath);
|
|
11
|
+
return resolved;
|
|
12
|
+
}
|
|
13
|
+
function guardPath(ctx, resolved, destructive = false) {
|
|
14
|
+
// If allowed_commands is empty (Merovingian), no path restriction
|
|
15
|
+
if (!destructive)
|
|
16
|
+
return;
|
|
17
|
+
// For Apoc (non-empty allowed_commands) or explicit working_dir set, guard destructive ops
|
|
18
|
+
if (ctx.allowed_commands.length > 0 && !isWithinDir(resolved, ctx.working_dir)) {
|
|
19
|
+
throw new Error(`Path '${resolved}' is outside the working directory '${ctx.working_dir}'. Operation denied.`);
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
export function createFilesystemTools(ctx) {
|
|
23
|
+
return [
|
|
24
|
+
tool(async ({ file_path, encoding, start_line, end_line }) => {
|
|
25
|
+
const resolved = resolveSafe(ctx, file_path);
|
|
26
|
+
const content = await fs.readFile(resolved, encoding ?? 'utf8');
|
|
27
|
+
const lines = content.split('\n');
|
|
28
|
+
const sliced = (start_line || end_line)
|
|
29
|
+
? lines.slice((start_line ?? 1) - 1, end_line).join('\n')
|
|
30
|
+
: content;
|
|
31
|
+
return truncateOutput(sliced);
|
|
32
|
+
}, {
|
|
33
|
+
name: 'read_file',
|
|
34
|
+
description: 'Read the contents of a file. Optionally specify line range.',
|
|
35
|
+
schema: z.object({
|
|
36
|
+
file_path: z.string().describe('Path to the file (absolute or relative to working_dir)'),
|
|
37
|
+
encoding: z.string().optional().describe('File encoding, default utf8'),
|
|
38
|
+
start_line: z.number().int().positive().optional().describe('Start line (1-based)'),
|
|
39
|
+
end_line: z.number().int().positive().optional().describe('End line (inclusive)'),
|
|
40
|
+
}),
|
|
41
|
+
}),
|
|
42
|
+
tool(async ({ file_path, content }) => {
|
|
43
|
+
const resolved = resolveSafe(ctx, file_path);
|
|
44
|
+
guardPath(ctx, resolved, true);
|
|
45
|
+
await fs.ensureDir(path.dirname(resolved));
|
|
46
|
+
await fs.writeFile(resolved, content, 'utf8');
|
|
47
|
+
return JSON.stringify({ success: true, path: resolved });
|
|
48
|
+
}, {
|
|
49
|
+
name: 'write_file',
|
|
50
|
+
description: 'Write content to a file, creating it and parent directories if needed.',
|
|
51
|
+
schema: z.object({
|
|
52
|
+
file_path: z.string(),
|
|
53
|
+
content: z.string().describe('Content to write'),
|
|
54
|
+
}),
|
|
55
|
+
}),
|
|
56
|
+
tool(async ({ file_path, content }) => {
|
|
57
|
+
const resolved = resolveSafe(ctx, file_path);
|
|
58
|
+
guardPath(ctx, resolved, true);
|
|
59
|
+
await fs.ensureDir(path.dirname(resolved));
|
|
60
|
+
await fs.appendFile(resolved, content, 'utf8');
|
|
61
|
+
return JSON.stringify({ success: true, path: resolved });
|
|
62
|
+
}, {
|
|
63
|
+
name: 'append_file',
|
|
64
|
+
description: 'Append content to a file without overwriting existing content.',
|
|
65
|
+
schema: z.object({
|
|
66
|
+
file_path: z.string(),
|
|
67
|
+
content: z.string(),
|
|
68
|
+
}),
|
|
69
|
+
}),
|
|
70
|
+
tool(async ({ file_path }) => {
|
|
71
|
+
const resolved = resolveSafe(ctx, file_path);
|
|
72
|
+
guardPath(ctx, resolved, true);
|
|
73
|
+
await fs.remove(resolved);
|
|
74
|
+
return JSON.stringify({ success: true, deleted: resolved });
|
|
75
|
+
}, {
|
|
76
|
+
name: 'delete_file',
|
|
77
|
+
description: 'Delete a file or directory.',
|
|
78
|
+
schema: z.object({ file_path: z.string() }),
|
|
79
|
+
}),
|
|
80
|
+
tool(async ({ source, destination }) => {
|
|
81
|
+
const src = resolveSafe(ctx, source);
|
|
82
|
+
const dest = resolveSafe(ctx, destination);
|
|
83
|
+
guardPath(ctx, dest, true);
|
|
84
|
+
await fs.ensureDir(path.dirname(dest));
|
|
85
|
+
await fs.move(src, dest, { overwrite: true });
|
|
86
|
+
return JSON.stringify({ success: true, from: src, to: dest });
|
|
87
|
+
}, {
|
|
88
|
+
name: 'move_file',
|
|
89
|
+
description: 'Move or rename a file or directory.',
|
|
90
|
+
schema: z.object({
|
|
91
|
+
source: z.string(),
|
|
92
|
+
destination: z.string(),
|
|
93
|
+
}),
|
|
94
|
+
}),
|
|
95
|
+
tool(async ({ source, destination }) => {
|
|
96
|
+
const src = resolveSafe(ctx, source);
|
|
97
|
+
const dest = resolveSafe(ctx, destination);
|
|
98
|
+
await fs.ensureDir(path.dirname(dest));
|
|
99
|
+
await fs.copy(src, dest);
|
|
100
|
+
return JSON.stringify({ success: true, from: src, to: dest });
|
|
101
|
+
}, {
|
|
102
|
+
name: 'copy_file',
|
|
103
|
+
description: 'Copy a file or directory to a new location.',
|
|
104
|
+
schema: z.object({
|
|
105
|
+
source: z.string(),
|
|
106
|
+
destination: z.string(),
|
|
107
|
+
}),
|
|
108
|
+
}),
|
|
109
|
+
tool(async ({ dir_path, recursive, pattern }) => {
|
|
110
|
+
const resolved = resolveSafe(ctx, dir_path ?? '.');
|
|
111
|
+
const entries = await fs.readdir(resolved, { withFileTypes: true });
|
|
112
|
+
let results = entries.map(e => ({
|
|
113
|
+
name: e.name,
|
|
114
|
+
type: e.isDirectory() ? 'dir' : 'file',
|
|
115
|
+
path: path.join(resolved, e.name),
|
|
116
|
+
}));
|
|
117
|
+
if (pattern) {
|
|
118
|
+
const re = new RegExp(pattern.replace('*', '.*').replace('?', '.'));
|
|
119
|
+
results = results.filter(r => re.test(r.name));
|
|
120
|
+
}
|
|
121
|
+
if (recursive) {
|
|
122
|
+
const subResults = [];
|
|
123
|
+
for (const entry of results.filter(r => r.type === 'dir')) {
|
|
124
|
+
try {
|
|
125
|
+
const subEntries = await fs.readdir(entry.path, { withFileTypes: true });
|
|
126
|
+
subResults.push(...subEntries.map(e => ({
|
|
127
|
+
name: e.name,
|
|
128
|
+
type: e.isDirectory() ? 'dir' : 'file',
|
|
129
|
+
path: path.join(entry.path, e.name),
|
|
130
|
+
})));
|
|
131
|
+
}
|
|
132
|
+
catch { /* skip inaccessible */ }
|
|
133
|
+
}
|
|
134
|
+
results.push(...subResults);
|
|
135
|
+
}
|
|
136
|
+
return truncateOutput(JSON.stringify(results, null, 2));
|
|
137
|
+
}, {
|
|
138
|
+
name: 'list_dir',
|
|
139
|
+
description: 'List files and directories in a path.',
|
|
140
|
+
schema: z.object({
|
|
141
|
+
dir_path: z.string().optional().describe('Directory path, defaults to working_dir'),
|
|
142
|
+
recursive: z.boolean().optional().describe('Include subdirectory contents'),
|
|
143
|
+
pattern: z.string().optional().describe('Filter by name pattern (glob-like)'),
|
|
144
|
+
}),
|
|
145
|
+
}),
|
|
146
|
+
tool(async ({ dir_path }) => {
|
|
147
|
+
const resolved = resolveSafe(ctx, dir_path);
|
|
148
|
+
await fs.ensureDir(resolved);
|
|
149
|
+
return JSON.stringify({ success: true, path: resolved });
|
|
150
|
+
}, {
|
|
151
|
+
name: 'create_dir',
|
|
152
|
+
description: 'Create a directory and all parent directories.',
|
|
153
|
+
schema: z.object({ dir_path: z.string() }),
|
|
154
|
+
}),
|
|
155
|
+
tool(async ({ file_path }) => {
|
|
156
|
+
const resolved = resolveSafe(ctx, file_path);
|
|
157
|
+
const stat = await fs.stat(resolved);
|
|
158
|
+
return JSON.stringify({
|
|
159
|
+
path: resolved,
|
|
160
|
+
size: stat.size,
|
|
161
|
+
isDirectory: stat.isDirectory(),
|
|
162
|
+
isFile: stat.isFile(),
|
|
163
|
+
created: stat.birthtime.toISOString(),
|
|
164
|
+
modified: stat.mtime.toISOString(),
|
|
165
|
+
permissions: stat.mode.toString(8),
|
|
166
|
+
});
|
|
167
|
+
}, {
|
|
168
|
+
name: 'file_info',
|
|
169
|
+
description: 'Get metadata about a file or directory (size, dates, permissions).',
|
|
170
|
+
schema: z.object({ file_path: z.string() }),
|
|
171
|
+
}),
|
|
172
|
+
tool(async ({ pattern, search_path, regex, case_insensitive, max_results }) => {
|
|
173
|
+
const base = resolveSafe(ctx, search_path ?? '.');
|
|
174
|
+
const files = await glob('**/*', { cwd: base, nodir: true, absolute: true });
|
|
175
|
+
const re = new RegExp(pattern, case_insensitive ? 'i' : undefined);
|
|
176
|
+
const results = [];
|
|
177
|
+
for (const file of files) {
|
|
178
|
+
if (results.length >= (max_results ?? 100))
|
|
179
|
+
break;
|
|
180
|
+
try {
|
|
181
|
+
const content = await fs.readFile(file, 'utf8');
|
|
182
|
+
const lines = content.split('\n');
|
|
183
|
+
for (let i = 0; i < lines.length; i++) {
|
|
184
|
+
if (re.test(lines[i])) {
|
|
185
|
+
results.push({ file: path.relative(base, file), line: i + 1, match: lines[i].trim() });
|
|
186
|
+
if (results.length >= (max_results ?? 100))
|
|
187
|
+
break;
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
catch { /* skip binary/unreadable files */ }
|
|
192
|
+
}
|
|
193
|
+
return truncateOutput(JSON.stringify(results, null, 2));
|
|
194
|
+
}, {
|
|
195
|
+
name: 'search_in_files',
|
|
196
|
+
description: 'Search for a pattern (regex) inside file contents.',
|
|
197
|
+
schema: z.object({
|
|
198
|
+
pattern: z.string().describe('Regex pattern to search for'),
|
|
199
|
+
search_path: z.string().optional().describe('Directory to search in, defaults to working_dir'),
|
|
200
|
+
regex: z.boolean().optional().describe('Treat pattern as regex (default true)'),
|
|
201
|
+
case_insensitive: z.boolean().optional(),
|
|
202
|
+
max_results: z.number().int().positive().optional().describe('Max matches to return (default 100)'),
|
|
203
|
+
}),
|
|
204
|
+
}),
|
|
205
|
+
tool(async ({ pattern, search_path }) => {
|
|
206
|
+
const base = resolveSafe(ctx, search_path ?? '.');
|
|
207
|
+
const files = await glob(pattern, { cwd: base, absolute: true });
|
|
208
|
+
return truncateOutput(JSON.stringify(files.map(f => path.relative(base, f)), null, 2));
|
|
209
|
+
}, {
|
|
210
|
+
name: 'find_files',
|
|
211
|
+
description: 'Find files matching a glob pattern.',
|
|
212
|
+
schema: z.object({
|
|
213
|
+
pattern: z.string().describe('Glob pattern e.g. "**/*.ts", "src/**/*.json"'),
|
|
214
|
+
search_path: z.string().optional().describe('Base directory, defaults to working_dir'),
|
|
215
|
+
}),
|
|
216
|
+
}),
|
|
217
|
+
];
|
|
218
|
+
}
|
|
219
|
+
registerToolFactory(createFilesystemTools);
|