morpheus-cli 0.4.1 → 0.4.3

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.
@@ -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
+ }
@@ -31,15 +31,37 @@ export function isProcessRunning(pid) {
31
31
  return e.code === 'EPERM'; // If EPERM, it exists but we lack permission (still running)
32
32
  }
33
33
  }
34
+ /**
35
+ * Waits until the given PID is no longer running, or until timeout is reached.
36
+ * Returns true if the process died, false if timeout was reached.
37
+ */
38
+ export async function waitForProcessDeath(pid, timeoutMs = 5000, intervalMs = 200) {
39
+ const deadline = Date.now() + timeoutMs;
40
+ while (Date.now() < deadline) {
41
+ if (!isProcessRunning(pid))
42
+ return true;
43
+ await new Promise(resolve => setTimeout(resolve, intervalMs));
44
+ }
45
+ return !isProcessRunning(pid);
46
+ }
34
47
  export async function checkStalePid() {
35
48
  const pid = await readPid();
36
49
  if (pid !== null) {
50
+ // Never treat our own PID as stale — this avoids self-kill loops in containers
51
+ // where a restarted process may inherit the same PID that was previously written.
52
+ if (pid === process.pid) {
53
+ await clearPid();
54
+ return;
55
+ }
37
56
  if (!isProcessRunning(pid)) {
38
57
  await clearPid();
39
58
  }
40
59
  }
41
60
  }
42
61
  export function killProcess(pid) {
62
+ // Safety guard: never kill ourselves
63
+ if (pid === process.pid)
64
+ return false;
43
65
  try {
44
66
  process.kill(pid, 'SIGTERM');
45
67
  return true;
@@ -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 async create(config, tools = []) {
13
- let display = DisplayManager.getInstance();
14
- const toolMonitoringMiddleware = createMiddleware({
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
- let model;
31
- const responseSchema = z.object({
32
- content: z.string().describe("The main response content from the agent"),
33
- });
34
- // Removed direct MCP client instantiation
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
- switch (config.provider) {
37
- case 'openai':
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
- let suggestion = "Check your configuration and API keys.";
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
+ });
@@ -2,3 +2,4 @@
2
2
  export * from './config-tools.js';
3
3
  export * from './diagnostic-tools.js';
4
4
  export * from './analytics-tools.js';
5
+ export * from './apoc-tool.js';
@@ -39,5 +39,11 @@ export const DEFAULT_CONFIG = {
39
39
  context_window: 100,
40
40
  memory_limit: 100,
41
41
  enabled_archived_sessions: true,
42
+ },
43
+ apoc: {
44
+ provider: 'openai',
45
+ model: 'gpt-4',
46
+ temperature: 0.2,
47
+ timeout_ms: 30000,
42
48
  }
43
49
  };