morpheus-cli 0.4.1 → 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 +46 -0
- package/dist/runtime/apoc.js +110 -0
- package/dist/runtime/providers/factory.js +85 -80
- 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/index.html +1 -1
- package/dist/ui/sw.js +1 -1
- package/package.json +1 -1
- package/dist/ui/assets/index-CovGlIO5.js +0 -109
|
@@ -0,0 +1,132 @@
|
|
|
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);
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export const MAX_OUTPUT_BYTES = 50 * 1024; // 50 KB
|
|
@@ -0,0 +1,45 @@
|
|
|
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
|
+
}
|
package/dist/http/api.js
CHANGED
|
@@ -386,6 +386,52 @@ export function createApiRouter(oracle) {
|
|
|
386
386
|
res.status(500).json({ error: error.message });
|
|
387
387
|
}
|
|
388
388
|
});
|
|
389
|
+
// Apoc config endpoints
|
|
390
|
+
router.get('/config/apoc', (req, res) => {
|
|
391
|
+
try {
|
|
392
|
+
const apocConfig = configManager.getApocConfig();
|
|
393
|
+
res.json(apocConfig);
|
|
394
|
+
}
|
|
395
|
+
catch (error) {
|
|
396
|
+
res.status(500).json({ error: error.message });
|
|
397
|
+
}
|
|
398
|
+
});
|
|
399
|
+
router.post('/config/apoc', async (req, res) => {
|
|
400
|
+
try {
|
|
401
|
+
const config = configManager.get();
|
|
402
|
+
await configManager.save({ ...config, apoc: req.body });
|
|
403
|
+
const display = DisplayManager.getInstance();
|
|
404
|
+
display.log('Apoc configuration updated via UI', {
|
|
405
|
+
source: 'Zaion',
|
|
406
|
+
level: 'info'
|
|
407
|
+
});
|
|
408
|
+
res.json({ success: true });
|
|
409
|
+
}
|
|
410
|
+
catch (error) {
|
|
411
|
+
if (error.name === 'ZodError') {
|
|
412
|
+
res.status(400).json({ error: 'Validation failed', details: error.errors });
|
|
413
|
+
}
|
|
414
|
+
else {
|
|
415
|
+
res.status(500).json({ error: error.message });
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
});
|
|
419
|
+
router.delete('/config/apoc', async (req, res) => {
|
|
420
|
+
try {
|
|
421
|
+
const config = configManager.get();
|
|
422
|
+
const { apoc: _apoc, ...restConfig } = config;
|
|
423
|
+
await configManager.save(restConfig);
|
|
424
|
+
const display = DisplayManager.getInstance();
|
|
425
|
+
display.log('Apoc configuration removed via UI (falling back to Oracle config)', {
|
|
426
|
+
source: 'Zaion',
|
|
427
|
+
level: 'info'
|
|
428
|
+
});
|
|
429
|
+
res.json({ success: true });
|
|
430
|
+
}
|
|
431
|
+
catch (error) {
|
|
432
|
+
res.status(500).json({ error: error.message });
|
|
433
|
+
}
|
|
434
|
+
});
|
|
389
435
|
// Sati memories endpoints
|
|
390
436
|
router.get('/sati/memories', async (req, res) => {
|
|
391
437
|
try {
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
import { HumanMessage, SystemMessage } from "@langchain/core/messages";
|
|
2
|
+
import { ConfigManager } from "../config/manager.js";
|
|
3
|
+
import { ProviderFactory } from "./providers/factory.js";
|
|
4
|
+
import { ProviderError } from "./errors.js";
|
|
5
|
+
import { DisplayManager } from "./display.js";
|
|
6
|
+
import { buildDevKit } from "../devkit/index.js";
|
|
7
|
+
/**
|
|
8
|
+
* Apoc is a subagent of Oracle specialized in devtools operations.
|
|
9
|
+
* It receives delegated tasks from Oracle and executes them using DevKit tools
|
|
10
|
+
* (filesystem, shell, git, network, processes, packages, system).
|
|
11
|
+
*
|
|
12
|
+
* Oracle calls Apoc via the `apoc_delegate` tool when the user requests
|
|
13
|
+
* dev-related tasks such as running commands, reading/writing files,
|
|
14
|
+
* managing git, or inspecting system state.
|
|
15
|
+
*/
|
|
16
|
+
export class Apoc {
|
|
17
|
+
static instance = null;
|
|
18
|
+
agent;
|
|
19
|
+
config;
|
|
20
|
+
display = DisplayManager.getInstance();
|
|
21
|
+
constructor(config) {
|
|
22
|
+
this.config = config || ConfigManager.getInstance().get();
|
|
23
|
+
}
|
|
24
|
+
static getInstance(config) {
|
|
25
|
+
if (!Apoc.instance) {
|
|
26
|
+
Apoc.instance = new Apoc(config);
|
|
27
|
+
}
|
|
28
|
+
return Apoc.instance;
|
|
29
|
+
}
|
|
30
|
+
static resetInstance() {
|
|
31
|
+
Apoc.instance = null;
|
|
32
|
+
}
|
|
33
|
+
async initialize() {
|
|
34
|
+
const apocConfig = this.config.apoc || this.config.llm;
|
|
35
|
+
console.log(`Apoc configuration: ${JSON.stringify(apocConfig)}`);
|
|
36
|
+
const working_dir = this.config.apoc?.working_dir || process.cwd();
|
|
37
|
+
const timeout_ms = this.config.apoc?.timeout_ms || 30_000;
|
|
38
|
+
// Import all devkit tool factories (side-effect registration)
|
|
39
|
+
await import("../devkit/index.js");
|
|
40
|
+
const tools = buildDevKit({
|
|
41
|
+
working_dir,
|
|
42
|
+
allowed_commands: [], // no restriction — Oracle is trusted orchestrator
|
|
43
|
+
timeout_ms,
|
|
44
|
+
});
|
|
45
|
+
this.display.log(`Apoc initialized with ${tools.length} DevKit tools (working_dir: ${working_dir})`, { source: "Apoc" });
|
|
46
|
+
try {
|
|
47
|
+
this.agent = await ProviderFactory.createBare(apocConfig, tools);
|
|
48
|
+
}
|
|
49
|
+
catch (err) {
|
|
50
|
+
throw new ProviderError(apocConfig.provider, err, "Apoc subagent initialization failed");
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Execute a devtools task delegated by Oracle.
|
|
55
|
+
* @param task Natural language task description
|
|
56
|
+
* @param context Optional additional context from the ongoing conversation
|
|
57
|
+
*/
|
|
58
|
+
async execute(task, context) {
|
|
59
|
+
if (!this.agent) {
|
|
60
|
+
await this.initialize();
|
|
61
|
+
}
|
|
62
|
+
this.display.log(`Executing delegated task: ${task.slice(0, 80)}...`, {
|
|
63
|
+
source: "Apoc",
|
|
64
|
+
});
|
|
65
|
+
const systemMessage = new SystemMessage(`
|
|
66
|
+
You are Apoc, a specialized devtools subagent within the Morpheus system.
|
|
67
|
+
|
|
68
|
+
You are called by Oracle when the user needs dev operations performed.
|
|
69
|
+
Your job is to execute the requested task accurately using your available tools.
|
|
70
|
+
|
|
71
|
+
Available capabilities:
|
|
72
|
+
- Read, write, append, and delete files
|
|
73
|
+
- Execute shell commands
|
|
74
|
+
- Inspect and manage processes
|
|
75
|
+
- Run git operations (status, log, diff, clone, commit, etc.)
|
|
76
|
+
- Perform network operations (curl, DNS, ping)
|
|
77
|
+
- Manage packages (npm, yarn)
|
|
78
|
+
- Inspect system information
|
|
79
|
+
|
|
80
|
+
OPERATING RULES:
|
|
81
|
+
1. Use tools to accomplish the task. Do not speculate.
|
|
82
|
+
2. Always verify results after execution.
|
|
83
|
+
3. Report clearly what was done and what the result was.
|
|
84
|
+
4. If something fails, report the error and what you tried.
|
|
85
|
+
5. Stay focused on the delegated task only.
|
|
86
|
+
|
|
87
|
+
${context ? `CONTEXT FROM ORACLE:\n${context}` : ""}
|
|
88
|
+
`);
|
|
89
|
+
const userMessage = new HumanMessage(task);
|
|
90
|
+
const messages = [systemMessage, userMessage];
|
|
91
|
+
try {
|
|
92
|
+
const response = await this.agent.invoke({ messages });
|
|
93
|
+
const lastMessage = response.messages[response.messages.length - 1];
|
|
94
|
+
const content = typeof lastMessage.content === "string"
|
|
95
|
+
? lastMessage.content
|
|
96
|
+
: JSON.stringify(lastMessage.content);
|
|
97
|
+
this.display.log("Apoc task completed.", { source: "Apoc" });
|
|
98
|
+
return content;
|
|
99
|
+
}
|
|
100
|
+
catch (err) {
|
|
101
|
+
throw new ProviderError(this.config.apoc?.provider || this.config.llm.provider, err, "Apoc task execution failed");
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
/** Reload with updated config (called when settings change) */
|
|
105
|
+
async reload() {
|
|
106
|
+
this.config = ConfigManager.getInstance().get();
|
|
107
|
+
this.agent = undefined;
|
|
108
|
+
await this.initialize();
|
|
109
|
+
}
|
|
110
|
+
}
|
|
@@ -4,14 +4,12 @@ import { ChatOllama } from "@langchain/ollama";
|
|
|
4
4
|
import { ChatGoogleGenerativeAI } from "@langchain/google-genai";
|
|
5
5
|
import { ProviderError } from "../errors.js";
|
|
6
6
|
import { createAgent, createMiddleware } from "langchain";
|
|
7
|
-
// import { MultiServerMCPClient, } from "@langchain/mcp-adapters"; // REMOVED
|
|
8
|
-
import { z } from "zod";
|
|
9
7
|
import { DisplayManager } from "../display.js";
|
|
10
|
-
import { ConfigQueryTool, ConfigUpdateTool, DiagnosticTool, MessageCountTool, TokenUsageTool, ProviderModelUsageTool } from "../tools/index.js";
|
|
8
|
+
import { ConfigQueryTool, ConfigUpdateTool, DiagnosticTool, MessageCountTool, TokenUsageTool, ProviderModelUsageTool, ApocDelegateTool } from "../tools/index.js";
|
|
11
9
|
export class ProviderFactory {
|
|
12
|
-
static
|
|
13
|
-
|
|
14
|
-
|
|
10
|
+
static buildMonitoringMiddleware() {
|
|
11
|
+
const display = DisplayManager.getInstance();
|
|
12
|
+
return createMiddleware({
|
|
15
13
|
name: "ToolMonitoringMiddleware",
|
|
16
14
|
wrapToolCall: (request, handler) => {
|
|
17
15
|
display.log(`Executing tool: ${request.toolCall.name}`, { level: "warning", source: 'ConstructLoad' });
|
|
@@ -27,55 +25,84 @@ export class ProviderFactory {
|
|
|
27
25
|
}
|
|
28
26
|
},
|
|
29
27
|
});
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
28
|
+
}
|
|
29
|
+
static buildModel(config) {
|
|
30
|
+
switch (config.provider) {
|
|
31
|
+
case 'openai':
|
|
32
|
+
return new ChatOpenAI({
|
|
33
|
+
modelName: config.model,
|
|
34
|
+
temperature: config.temperature,
|
|
35
|
+
apiKey: process.env.OPENAI_API_KEY || config.api_key,
|
|
36
|
+
});
|
|
37
|
+
case 'anthropic':
|
|
38
|
+
return new ChatAnthropic({
|
|
39
|
+
modelName: config.model,
|
|
40
|
+
temperature: config.temperature,
|
|
41
|
+
apiKey: process.env.ANTHROPIC_API_KEY || config.api_key,
|
|
42
|
+
});
|
|
43
|
+
case 'openrouter':
|
|
44
|
+
return new ChatOpenAI({
|
|
45
|
+
modelName: config.model,
|
|
46
|
+
temperature: config.temperature,
|
|
47
|
+
apiKey: process.env.OPENROUTER_API_KEY || config.api_key,
|
|
48
|
+
configuration: {
|
|
49
|
+
baseURL: config.base_url || 'https://openrouter.ai/api/v1'
|
|
50
|
+
}
|
|
51
|
+
});
|
|
52
|
+
case 'ollama':
|
|
53
|
+
return new ChatOllama({
|
|
54
|
+
model: config.model,
|
|
55
|
+
temperature: config.temperature,
|
|
56
|
+
baseUrl: config.base_url || config.api_key,
|
|
57
|
+
});
|
|
58
|
+
case 'gemini':
|
|
59
|
+
return new ChatGoogleGenerativeAI({
|
|
60
|
+
model: config.model,
|
|
61
|
+
temperature: config.temperature,
|
|
62
|
+
apiKey: process.env.GOOGLE_API_KEY || config.api_key
|
|
63
|
+
});
|
|
64
|
+
default:
|
|
65
|
+
throw new Error(`Unsupported provider: ${config.provider}`);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
static handleProviderError(config, error) {
|
|
69
|
+
let suggestion = "Check your configuration and API keys.";
|
|
70
|
+
const msg = error.message?.toLowerCase() || '';
|
|
71
|
+
if (msg.includes("api key") && (msg.includes("missing") || msg.includes("not found"))) {
|
|
72
|
+
suggestion = `API Key is missing for ${config.provider}. Run 'morpheus config' or set it in .env.`;
|
|
73
|
+
}
|
|
74
|
+
else if (msg.includes("401") || msg.includes("unauthorized")) {
|
|
75
|
+
suggestion = `Run 'morpheus config' to update your ${config.provider} API key.`;
|
|
76
|
+
}
|
|
77
|
+
else if ((msg.includes("econnrefused") || msg.includes("fetch failed")) && config.provider === 'ollama') {
|
|
78
|
+
suggestion = "Is Ollama running? Try 'ollama serve'.";
|
|
79
|
+
}
|
|
80
|
+
else if (msg.includes("model not found") || msg.includes("404")) {
|
|
81
|
+
suggestion = `Model '${config.model}' may not be available. Check provider docs.`;
|
|
82
|
+
}
|
|
83
|
+
else if (msg.includes("unsupported provider")) {
|
|
84
|
+
suggestion = "Edit your config file to use a supported provider (openai, anthropic, openrouter, ollama, gemini).";
|
|
85
|
+
}
|
|
86
|
+
throw new ProviderError(config.provider, error, suggestion);
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* Creates a ReactAgent with only the provided tools — no internal Oracle tools injected.
|
|
90
|
+
* Used by subagents like Apoc that need a clean, isolated tool context.
|
|
91
|
+
*/
|
|
92
|
+
static async createBare(config, tools = []) {
|
|
93
|
+
try {
|
|
94
|
+
const model = ProviderFactory.buildModel(config);
|
|
95
|
+
const middleware = ProviderFactory.buildMonitoringMiddleware();
|
|
96
|
+
return createAgent({ model, tools, middleware: [middleware] });
|
|
97
|
+
}
|
|
98
|
+
catch (error) {
|
|
99
|
+
ProviderFactory.handleProviderError(config, error);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
static async create(config, tools = []) {
|
|
35
103
|
try {
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
model = new ChatOpenAI({
|
|
39
|
-
modelName: config.model,
|
|
40
|
-
temperature: config.temperature,
|
|
41
|
-
apiKey: process.env.OPENAI_API_KEY || config.api_key, // Check env var first, then config
|
|
42
|
-
});
|
|
43
|
-
break;
|
|
44
|
-
case 'anthropic':
|
|
45
|
-
model = new ChatAnthropic({
|
|
46
|
-
modelName: config.model,
|
|
47
|
-
temperature: config.temperature,
|
|
48
|
-
apiKey: process.env.ANTHROPIC_API_KEY || config.api_key, // Check env var first, then config
|
|
49
|
-
});
|
|
50
|
-
break;
|
|
51
|
-
case 'openrouter':
|
|
52
|
-
model = new ChatOpenAI({
|
|
53
|
-
modelName: config.model,
|
|
54
|
-
temperature: config.temperature,
|
|
55
|
-
apiKey: process.env.OPENROUTER_API_KEY || config.api_key, // Check env var first, then config
|
|
56
|
-
configuration: {
|
|
57
|
-
baseURL: config.base_url || 'https://openrouter.ai/api/v1'
|
|
58
|
-
}
|
|
59
|
-
});
|
|
60
|
-
break;
|
|
61
|
-
case 'ollama':
|
|
62
|
-
// Ollama usually runs locally, api_key optional
|
|
63
|
-
model = new ChatOllama({
|
|
64
|
-
model: config.model,
|
|
65
|
-
temperature: config.temperature,
|
|
66
|
-
baseUrl: config.api_key, // Sometimes users might overload api_key for base URL or similar, but simplified here
|
|
67
|
-
});
|
|
68
|
-
break;
|
|
69
|
-
case 'gemini':
|
|
70
|
-
model = new ChatGoogleGenerativeAI({
|
|
71
|
-
model: config.model,
|
|
72
|
-
temperature: config.temperature,
|
|
73
|
-
apiKey: process.env.GOOGLE_API_KEY || config.api_key // Check env var first, then config
|
|
74
|
-
});
|
|
75
|
-
break;
|
|
76
|
-
default:
|
|
77
|
-
throw new Error(`Unsupported provider: ${config.provider}`);
|
|
78
|
-
}
|
|
104
|
+
const model = ProviderFactory.buildModel(config);
|
|
105
|
+
const middleware = ProviderFactory.buildMonitoringMiddleware();
|
|
79
106
|
const toolsForAgent = [
|
|
80
107
|
...tools,
|
|
81
108
|
ConfigQueryTool,
|
|
@@ -83,35 +110,13 @@ export class ProviderFactory {
|
|
|
83
110
|
DiagnosticTool,
|
|
84
111
|
MessageCountTool,
|
|
85
112
|
TokenUsageTool,
|
|
86
|
-
ProviderModelUsageTool
|
|
113
|
+
ProviderModelUsageTool,
|
|
114
|
+
ApocDelegateTool
|
|
87
115
|
];
|
|
88
|
-
return createAgent({
|
|
89
|
-
model: model,
|
|
90
|
-
tools: toolsForAgent,
|
|
91
|
-
middleware: [toolMonitoringMiddleware]
|
|
92
|
-
});
|
|
116
|
+
return createAgent({ model, tools: toolsForAgent, middleware: [middleware] });
|
|
93
117
|
}
|
|
94
118
|
catch (error) {
|
|
95
|
-
|
|
96
|
-
const msg = error.message?.toLowerCase() || '';
|
|
97
|
-
// Constructor validation errors (Missing Keys)
|
|
98
|
-
if (msg.includes("api key") && (msg.includes("missing") || msg.includes("not found"))) {
|
|
99
|
-
suggestion = `API Key is missing for ${config.provider}. Run 'morpheus config' or set it in .env.`;
|
|
100
|
-
}
|
|
101
|
-
// Network/Auth errors (unlikely in constructor, but possible if pre-validation exists)
|
|
102
|
-
else if (msg.includes("401") || msg.includes("unauthorized")) {
|
|
103
|
-
suggestion = `Run 'morpheus config' to update your ${config.provider} API key.`;
|
|
104
|
-
}
|
|
105
|
-
else if ((msg.includes("econnrefused") || msg.includes("fetch failed")) && config.provider === 'ollama') {
|
|
106
|
-
suggestion = "Is Ollama running? Try 'ollama serve'.";
|
|
107
|
-
}
|
|
108
|
-
else if (msg.includes("model not found") || msg.includes("404")) {
|
|
109
|
-
suggestion = `Model '${config.model}' may not be available. Check provider docs.`;
|
|
110
|
-
}
|
|
111
|
-
else if (msg.includes("unsupported provider")) {
|
|
112
|
-
suggestion = "Edit your config file to use a supported provider (openai, anthropic, openrouter, ollama, gemini).";
|
|
113
|
-
}
|
|
114
|
-
throw new ProviderError(config.provider, error, suggestion);
|
|
119
|
+
ProviderFactory.handleProviderError(config, error);
|
|
115
120
|
}
|
|
116
121
|
}
|
|
117
122
|
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { tool } from "@langchain/core/tools";
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
import { Apoc } from "../apoc.js";
|
|
4
|
+
/**
|
|
5
|
+
* Tool that Oracle uses to delegate devtools tasks to Apoc.
|
|
6
|
+
* Oracle should call this whenever the user requests operations like:
|
|
7
|
+
* - Reading/writing/listing files
|
|
8
|
+
* - Running shell commands or scripts
|
|
9
|
+
* - Git operations (status, log, commit, push, etc.)
|
|
10
|
+
* - Package management (npm install, etc.)
|
|
11
|
+
* - Process inspection or management
|
|
12
|
+
* - Network diagnostics (ping, curl, DNS)
|
|
13
|
+
* - System information queries
|
|
14
|
+
*/
|
|
15
|
+
export const ApocDelegateTool = tool(async ({ task, context }) => {
|
|
16
|
+
try {
|
|
17
|
+
const apoc = Apoc.getInstance();
|
|
18
|
+
const result = await apoc.execute(task, context);
|
|
19
|
+
return result;
|
|
20
|
+
}
|
|
21
|
+
catch (err) {
|
|
22
|
+
return `Apoc execution failed: ${err.message}`;
|
|
23
|
+
}
|
|
24
|
+
}, {
|
|
25
|
+
name: "apoc_delegate",
|
|
26
|
+
description: `Delegate a devtools task to Apoc, the specialized development subagent.
|
|
27
|
+
|
|
28
|
+
Use this tool when the user asks for ANY of the following:
|
|
29
|
+
- File operations: read, write, create, delete files or directories
|
|
30
|
+
- Shell commands: run scripts, execute commands, check output
|
|
31
|
+
- Git: status, log, diff, commit, push, pull, clone, branch
|
|
32
|
+
- Package management: npm install/update/audit, yarn, package.json inspection
|
|
33
|
+
- Process management: list processes, kill processes, check ports
|
|
34
|
+
- Network: ping hosts, curl URLs, DNS lookups
|
|
35
|
+
- System info: environment variables, OS info, disk space, memory
|
|
36
|
+
|
|
37
|
+
Provide a clear natural language task description. Optionally provide context
|
|
38
|
+
from the current conversation to help Apoc understand the broader goal.`,
|
|
39
|
+
schema: z.object({
|
|
40
|
+
task: z.string().describe("Clear description of the devtools task to execute"),
|
|
41
|
+
context: z.string().optional().describe("Optional context from the conversation to help Apoc understand the goal"),
|
|
42
|
+
}),
|
|
43
|
+
});
|
package/dist/types/config.js
CHANGED