morpheus-cli 0.7.2 → 0.7.4

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.
Files changed (36) hide show
  1. package/README.md +119 -0
  2. package/dist/channels/discord.js +109 -0
  3. package/dist/channels/telegram.js +94 -0
  4. package/dist/cli/commands/start.js +12 -0
  5. package/dist/config/manager.js +3 -0
  6. package/dist/config/paths.js +1 -0
  7. package/dist/config/schemas.js +11 -2
  8. package/dist/http/api.js +3 -0
  9. package/dist/http/routers/skills.js +291 -0
  10. package/dist/runtime/__tests__/keymaker.test.js +145 -0
  11. package/dist/runtime/keymaker.js +162 -0
  12. package/dist/runtime/oracle.js +15 -4
  13. package/dist/runtime/scaffold.js +75 -0
  14. package/dist/runtime/skills/__tests__/loader.test.js +187 -0
  15. package/dist/runtime/skills/__tests__/registry.test.js +201 -0
  16. package/dist/runtime/skills/__tests__/tool.test.js +266 -0
  17. package/dist/runtime/skills/index.js +8 -0
  18. package/dist/runtime/skills/loader.js +213 -0
  19. package/dist/runtime/skills/registry.js +141 -0
  20. package/dist/runtime/skills/schema.js +30 -0
  21. package/dist/runtime/skills/tool.js +204 -0
  22. package/dist/runtime/skills/types.js +7 -0
  23. package/dist/runtime/tasks/context.js +16 -0
  24. package/dist/runtime/tasks/worker.js +22 -0
  25. package/dist/runtime/tools/apoc-tool.js +28 -1
  26. package/dist/runtime/tools/morpheus-tools.js +3 -0
  27. package/dist/runtime/tools/neo-tool.js +32 -0
  28. package/dist/runtime/tools/trinity-tool.js +27 -0
  29. package/dist/types/config.js +3 -0
  30. package/dist/ui/assets/index-CsMDzmtQ.js +117 -0
  31. package/dist/ui/assets/index-Dz_qYlIb.css +1 -0
  32. package/dist/ui/index.html +2 -2
  33. package/dist/ui/sw.js +1 -1
  34. package/package.json +4 -1
  35. package/dist/ui/assets/index-7e8TCoiy.js +0 -111
  36. package/dist/ui/assets/index-B9ngtbja.css +0 -1
@@ -0,0 +1,204 @@
1
+ /**
2
+ * Skill Tools - skill_execute (sync) and skill_delegate (async)
3
+ */
4
+ import { tool } from "@langchain/core/tools";
5
+ import { z } from "zod";
6
+ import { TaskRepository } from "../tasks/repository.js";
7
+ import { TaskRequestContext } from "../tasks/context.js";
8
+ import { DisplayManager } from "../display.js";
9
+ import { SkillRegistry } from "./registry.js";
10
+ import { executeKeymakerTask } from "../keymaker.js";
11
+ // ============================================================================
12
+ // skill_execute - Synchronous execution
13
+ // ============================================================================
14
+ /**
15
+ * Generates the skill_execute tool description dynamically with sync skills.
16
+ */
17
+ export function getSkillExecuteDescription() {
18
+ const registry = SkillRegistry.getInstance();
19
+ const syncSkills = registry.getEnabled().filter((s) => s.execution_mode === 'sync');
20
+ const skillList = syncSkills.length > 0
21
+ ? syncSkills.map(s => `- ${s.name}: ${s.description}`).join('\n')
22
+ : '(no sync skills enabled)';
23
+ return `Execute a skill synchronously using Keymaker. The result is returned immediately.
24
+
25
+ Keymaker has access to ALL tools (filesystem, shell, git, MCP, browser, etc.) and will execute the skill instructions.
26
+
27
+ Available sync skills:
28
+ ${skillList}
29
+
30
+ Use this for skills that need immediate results in the conversation.`;
31
+ }
32
+ /**
33
+ * Tool that Oracle uses to execute skills synchronously via Keymaker.
34
+ * Result is returned directly to Oracle for inclusion in the response.
35
+ */
36
+ export const SkillExecuteTool = tool(async ({ skillName, objective }) => {
37
+ const display = DisplayManager.getInstance();
38
+ const registry = SkillRegistry.getInstance();
39
+ // Validate skill exists and is enabled
40
+ const skill = registry.get(skillName);
41
+ if (!skill) {
42
+ const available = registry.getEnabled().map(s => s.name).join(', ');
43
+ return `Error: Skill "${skillName}" not found. Available skills: ${available || 'none'}`;
44
+ }
45
+ if (!skill.enabled) {
46
+ return `Error: Skill "${skillName}" is disabled.`;
47
+ }
48
+ if (skill.execution_mode === 'async') {
49
+ return `Error: Skill "${skillName}" is async-only. Use skill_delegate instead.`;
50
+ }
51
+ display.log(`Executing skill "${skillName}" synchronously...`, {
52
+ source: "SkillExecuteTool",
53
+ level: "info",
54
+ });
55
+ try {
56
+ const ctx = TaskRequestContext.get();
57
+ const sessionId = ctx?.session_id ?? "default";
58
+ const taskContext = {
59
+ origin_channel: ctx?.origin_channel ?? "api",
60
+ session_id: sessionId,
61
+ origin_message_id: ctx?.origin_message_id,
62
+ origin_user_id: ctx?.origin_user_id,
63
+ };
64
+ // Execute Keymaker directly (synchronous)
65
+ const result = await executeKeymakerTask(skillName, objective, taskContext);
66
+ display.log(`Skill "${skillName}" completed successfully.`, {
67
+ source: "SkillExecuteTool",
68
+ level: "info",
69
+ });
70
+ return result;
71
+ }
72
+ catch (err) {
73
+ display.log(`Skill execution error: ${err.message}`, {
74
+ source: "SkillExecuteTool",
75
+ level: "error",
76
+ });
77
+ return `Skill execution failed: ${err.message}`;
78
+ }
79
+ }, {
80
+ name: "skill_execute",
81
+ description: getSkillExecuteDescription(),
82
+ schema: z.object({
83
+ skillName: z.string().describe("Exact name of the sync skill to use"),
84
+ objective: z.string().describe("Clear description of what to accomplish"),
85
+ }),
86
+ });
87
+ // ============================================================================
88
+ // skill_delegate - Asynchronous execution (background task)
89
+ // ============================================================================
90
+ /**
91
+ * Generates the skill_delegate tool description dynamically with async skills.
92
+ */
93
+ export function getSkillDelegateDescription() {
94
+ const registry = SkillRegistry.getInstance();
95
+ const asyncSkills = registry.getEnabled().filter((s) => s.execution_mode === 'async');
96
+ const skillList = asyncSkills.length > 0
97
+ ? asyncSkills.map(s => `- ${s.name}: ${s.description}`).join('\n')
98
+ : '(no async skills enabled)';
99
+ return `Delegate a task to Keymaker as a background job. You will be notified when complete.
100
+
101
+ Keymaker has access to ALL tools (filesystem, shell, git, MCP, browser, etc.) and will execute the skill instructions.
102
+
103
+ Available async skills:
104
+ ${skillList}
105
+
106
+ Use this for long-running skills like builds, deployments, or batch processing.`;
107
+ }
108
+ /**
109
+ * Tool that Oracle uses to delegate tasks to Keymaker via async task queue.
110
+ * Keymaker will execute the skill instructions in background.
111
+ */
112
+ export const SkillDelegateTool = tool(async ({ skillName, objective }) => {
113
+ try {
114
+ const display = DisplayManager.getInstance();
115
+ const registry = SkillRegistry.getInstance();
116
+ // Validate skill exists and is enabled
117
+ const skill = registry.get(skillName);
118
+ if (!skill) {
119
+ const available = registry.getEnabled().map(s => s.name).join(', ');
120
+ return `Error: Skill "${skillName}" not found. Available skills: ${available || 'none'}`;
121
+ }
122
+ if (!skill.enabled) {
123
+ return `Error: Skill "${skillName}" is disabled.`;
124
+ }
125
+ if (skill.execution_mode !== 'async') {
126
+ return `Error: Skill "${skillName}" is sync. Use skill_execute instead for immediate results.`;
127
+ }
128
+ // Check for duplicate delegation
129
+ const existingAck = TaskRequestContext.findDuplicateDelegation("keymaker", `${skillName}:${objective}`);
130
+ if (existingAck) {
131
+ display.log(`Keymaker delegation deduplicated. Reusing task ${existingAck.task_id}.`, {
132
+ source: "SkillDelegateTool",
133
+ level: "info",
134
+ });
135
+ return `Task ${existingAck.task_id} already queued for Keymaker (${skillName}) execution.`;
136
+ }
137
+ if (!TaskRequestContext.canEnqueueDelegation()) {
138
+ display.log(`Keymaker delegation blocked by per-turn limit.`, {
139
+ source: "SkillDelegateTool",
140
+ level: "warning",
141
+ });
142
+ return "Delegation limit reached for this user turn. Wait for current tasks to complete.";
143
+ }
144
+ const ctx = TaskRequestContext.get();
145
+ const repository = TaskRepository.getInstance();
146
+ // Store skill name in context as JSON
147
+ const taskContext = JSON.stringify({ skill: skillName });
148
+ const created = repository.createTask({
149
+ agent: "keymaker",
150
+ input: objective,
151
+ context: taskContext,
152
+ origin_channel: ctx?.origin_channel ?? "api",
153
+ session_id: ctx?.session_id ?? "default",
154
+ origin_message_id: ctx?.origin_message_id ?? null,
155
+ origin_user_id: ctx?.origin_user_id ?? null,
156
+ max_attempts: 3,
157
+ });
158
+ TaskRequestContext.setDelegationAck({
159
+ task_id: created.id,
160
+ agent: "keymaker",
161
+ task: `${skillName}:${objective}`,
162
+ });
163
+ display.log(`Keymaker task created: ${created.id} (skill: ${skillName})`, {
164
+ source: "SkillDelegateTool",
165
+ level: "info",
166
+ meta: {
167
+ agent: created.agent,
168
+ skill: skillName,
169
+ origin_channel: created.origin_channel,
170
+ session_id: created.session_id,
171
+ input: created.input,
172
+ }
173
+ });
174
+ return `Task ${created.id} queued for Keymaker (skill: ${skillName}). You will be notified when complete.`;
175
+ }
176
+ catch (err) {
177
+ const display = DisplayManager.getInstance();
178
+ display.log(`SkillDelegateTool error: ${err.message}`, {
179
+ source: "SkillDelegateTool",
180
+ level: "error",
181
+ });
182
+ return `Keymaker task enqueue failed: ${err.message}`;
183
+ }
184
+ }, {
185
+ name: "skill_delegate",
186
+ description: getSkillDelegateDescription(),
187
+ schema: z.object({
188
+ skillName: z.string().describe("Exact name of the async skill to use"),
189
+ objective: z.string().describe("Clear description of what Keymaker should accomplish"),
190
+ }),
191
+ });
192
+ // ============================================================================
193
+ // Utility functions
194
+ // ============================================================================
195
+ /**
196
+ * Updates both skill tool descriptions with current skill list.
197
+ * Should be called after skills are loaded/reloaded.
198
+ */
199
+ export function updateSkillToolDescriptions() {
200
+ SkillExecuteTool.description = getSkillExecuteDescription();
201
+ SkillDelegateTool.description = getSkillDelegateDescription();
202
+ }
203
+ // Backwards compatibility alias
204
+ export const updateSkillDelegateDescription = updateSkillToolDescriptions;
@@ -0,0 +1,7 @@
1
+ /**
2
+ * Skills System Type Definitions
3
+ *
4
+ * Skills are user-defined behavioral extensions loaded from ~/.morpheus/skills/
5
+ * Each skill is a SKILL.md file with YAML frontmatter containing metadata.
6
+ */
7
+ export {};
@@ -27,6 +27,22 @@ export class TaskRequestContext {
27
27
  static canEnqueueDelegation() {
28
28
  return this.getDelegationAcks().length < this.MAX_DELEGATIONS_PER_TURN;
29
29
  }
30
+ /**
31
+ * Record that a delegation tool executed synchronously (inline).
32
+ * Oracle uses this to know that the tool call was NOT an async enqueue.
33
+ */
34
+ static incrementSyncDelegation() {
35
+ const current = storage.getStore();
36
+ if (!current)
37
+ return;
38
+ current.sync_delegation_count = (current.sync_delegation_count ?? 0) + 1;
39
+ }
40
+ /**
41
+ * Returns the number of delegation tools that executed synchronously this turn.
42
+ */
43
+ static getSyncDelegationCount() {
44
+ return storage.getStore()?.sync_delegation_count ?? 0;
45
+ }
30
46
  static findDuplicateDelegation(agent, task) {
31
47
  const acks = this.getDelegationAcks();
32
48
  if (acks.length === 0)
@@ -3,6 +3,7 @@ import { DisplayManager } from '../display.js';
3
3
  import { Apoc } from '../apoc.js';
4
4
  import { Neo } from '../neo.js';
5
5
  import { Trinity } from '../trinity.js';
6
+ import { executeKeymakerTask } from '../keymaker.js';
6
7
  import { TaskRepository } from './repository.js';
7
8
  export class TaskWorker {
8
9
  workerId;
@@ -71,6 +72,27 @@ export class TaskWorker {
71
72
  output = await trinity.execute(task.input, task.context ?? undefined, task.session_id);
72
73
  break;
73
74
  }
75
+ case 'keymaker': {
76
+ // Parse skill name from context JSON
77
+ let skillName = 'unknown';
78
+ if (task.context) {
79
+ try {
80
+ const parsed = JSON.parse(task.context);
81
+ skillName = parsed.skill || 'unknown';
82
+ }
83
+ catch {
84
+ // context is not JSON, use as skill name directly for backwards compat
85
+ skillName = task.context;
86
+ }
87
+ }
88
+ output = await executeKeymakerTask(skillName, task.input, {
89
+ origin_channel: task.origin_channel,
90
+ session_id: task.session_id,
91
+ origin_message_id: task.origin_message_id ?? undefined,
92
+ origin_user_id: task.origin_user_id ?? undefined,
93
+ });
94
+ break;
95
+ }
74
96
  default: {
75
97
  throw new Error(`Unknown task agent: ${task.agent}`);
76
98
  }
@@ -4,6 +4,15 @@ import { TaskRepository } from "../tasks/repository.js";
4
4
  import { TaskRequestContext } from "../tasks/context.js";
5
5
  import { compositeDelegationError, isLikelyCompositeDelegationTask } from "./delegation-guard.js";
6
6
  import { DisplayManager } from "../display.js";
7
+ import { ConfigManager } from "../../config/manager.js";
8
+ import { Apoc } from "../apoc.js";
9
+ /**
10
+ * Returns true when Apoc is configured to execute synchronously (inline).
11
+ */
12
+ function isApocSync() {
13
+ const config = ConfigManager.getInstance().get();
14
+ return config.apoc?.execution_mode === 'sync';
15
+ }
7
16
  /**
8
17
  * Tool that Oracle uses to delegate devtools tasks to Apoc.
9
18
  * Oracle should call this whenever the user requests operations like:
@@ -25,6 +34,24 @@ export const ApocDelegateTool = tool(async ({ task, context }) => {
25
34
  });
26
35
  return compositeDelegationError();
27
36
  }
37
+ // ── Sync mode: execute inline and return result directly ──
38
+ if (isApocSync()) {
39
+ display.log(`Apoc executing synchronously: ${task.slice(0, 80)}...`, {
40
+ source: "ApocDelegateTool",
41
+ level: "info",
42
+ });
43
+ const ctx = TaskRequestContext.get();
44
+ const sessionId = ctx?.session_id ?? "default";
45
+ const apoc = Apoc.getInstance();
46
+ const result = await apoc.execute(task, context, sessionId);
47
+ TaskRequestContext.incrementSyncDelegation();
48
+ display.log(`Apoc sync execution completed.`, {
49
+ source: "ApocDelegateTool",
50
+ level: "info",
51
+ });
52
+ return result;
53
+ }
54
+ // ── Async mode (default): create background task ──
28
55
  const existingAck = TaskRequestContext.findDuplicateDelegation("apoc", task);
29
56
  if (existingAck) {
30
57
  display.log(`Apoc delegation deduplicated. Reusing task ${existingAck.task_id}.`, {
@@ -72,7 +99,7 @@ export const ApocDelegateTool = tool(async ({ task, context }) => {
72
99
  }
73
100
  }, {
74
101
  name: "apoc_delegate",
75
- description: `Delegate a devtools task to Apoc, the specialized development subagent, asynchronously.
102
+ description: `Delegate a devtools task to Apoc, the specialized development subagent.
76
103
 
77
104
  This tool enqueues a background task and returns an acknowledgement with task id.
78
105
  Do not expect final execution output in the same response.
@@ -29,16 +29,19 @@ const CONFIG_TO_ENV_MAP = {
29
29
  'neo.model': ['MORPHEUS_NEO_MODEL'],
30
30
  'neo.temperature': ['MORPHEUS_NEO_TEMPERATURE'],
31
31
  'neo.api_key': ['MORPHEUS_NEO_API_KEY'],
32
+ 'neo.execution_mode': ['MORPHEUS_NEO_EXECUTION_MODE'],
32
33
  'apoc.provider': ['MORPHEUS_APOC_PROVIDER'],
33
34
  'apoc.model': ['MORPHEUS_APOC_MODEL'],
34
35
  'apoc.temperature': ['MORPHEUS_APOC_TEMPERATURE'],
35
36
  'apoc.api_key': ['MORPHEUS_APOC_API_KEY'],
36
37
  'apoc.working_dir': ['MORPHEUS_APOC_WORKING_DIR'],
37
38
  'apoc.timeout_ms': ['MORPHEUS_APOC_TIMEOUT_MS'],
39
+ 'apoc.execution_mode': ['MORPHEUS_APOC_EXECUTION_MODE'],
38
40
  'trinity.provider': ['MORPHEUS_TRINITY_PROVIDER'],
39
41
  'trinity.model': ['MORPHEUS_TRINITY_MODEL'],
40
42
  'trinity.temperature': ['MORPHEUS_TRINITY_TEMPERATURE'],
41
43
  'trinity.api_key': ['MORPHEUS_TRINITY_API_KEY'],
44
+ 'trinity.execution_mode': ['MORPHEUS_TRINITY_EXECUTION_MODE'],
42
45
  'audio.provider': ['MORPHEUS_AUDIO_PROVIDER'],
43
46
  'audio.model': ['MORPHEUS_AUDIO_MODEL'],
44
47
  'audio.apiKey': ['MORPHEUS_AUDIO_API_KEY'],
@@ -4,6 +4,8 @@ import { TaskRepository } from "../tasks/repository.js";
4
4
  import { TaskRequestContext } from "../tasks/context.js";
5
5
  import { compositeDelegationError, isLikelyCompositeDelegationTask } from "./delegation-guard.js";
6
6
  import { DisplayManager } from "../display.js";
7
+ import { ConfigManager } from "../../config/manager.js";
8
+ import { Neo } from "../neo.js";
7
9
  const NEO_BUILTIN_CAPABILITIES = `
8
10
  Neo built-in capabilities (always available — no MCP required):
9
11
  • Config: morpheus_config_query, morpheus_config_update — read/write Morpheus configuration (LLM, channels, UI, etc.)
@@ -39,6 +41,13 @@ function buildCatalogSection(mcpTools) {
39
41
  }
40
42
  return `\n\nRuntime MCP tools:\n${lines.join("\n")}`;
41
43
  }
44
+ /**
45
+ * Returns true when Neo is configured to execute synchronously (inline).
46
+ */
47
+ function isNeoSync() {
48
+ const config = ConfigManager.getInstance().get();
49
+ return config.neo?.execution_mode === 'sync';
50
+ }
42
51
  export function updateNeoDelegateToolDescription(tools) {
43
52
  const full = `${NEO_BASE_DESCRIPTION}${buildCatalogSection(tools)}`;
44
53
  NeoDelegateTool.description = full;
@@ -53,6 +62,29 @@ export const NeoDelegateTool = tool(async ({ task, context }) => {
53
62
  });
54
63
  return compositeDelegationError();
55
64
  }
65
+ // ── Sync mode: execute inline and return result directly ──
66
+ if (isNeoSync()) {
67
+ display.log(`Neo executing synchronously: ${task.slice(0, 80)}...`, {
68
+ source: "NeoDelegateTool",
69
+ level: "info",
70
+ });
71
+ const ctx = TaskRequestContext.get();
72
+ const sessionId = ctx?.session_id ?? "default";
73
+ const neo = Neo.getInstance();
74
+ const result = await neo.execute(task, context, sessionId, {
75
+ origin_channel: ctx?.origin_channel ?? "api",
76
+ session_id: sessionId,
77
+ origin_message_id: ctx?.origin_message_id,
78
+ origin_user_id: ctx?.origin_user_id,
79
+ });
80
+ TaskRequestContext.incrementSyncDelegation();
81
+ display.log(`Neo sync execution completed.`, {
82
+ source: "NeoDelegateTool",
83
+ level: "info",
84
+ });
85
+ return result;
86
+ }
87
+ // ── Async mode (default): create background task ──
56
88
  const existingAck = TaskRequestContext.findDuplicateDelegation("neo", task);
57
89
  if (existingAck) {
58
90
  display.log(`Neo delegation deduplicated. Reusing task ${existingAck.task_id}.`, {
@@ -4,6 +4,8 @@ import { TaskRepository } from "../tasks/repository.js";
4
4
  import { TaskRequestContext } from "../tasks/context.js";
5
5
  import { compositeDelegationError, isLikelyCompositeDelegationTask } from "./delegation-guard.js";
6
6
  import { DisplayManager } from "../display.js";
7
+ import { ConfigManager } from "../../config/manager.js";
8
+ import { Trinity } from "../trinity.js";
7
9
  const TRINITY_BASE_DESCRIPTION = `Delegate a database task to Trinity, the specialized database subagent, asynchronously.
8
10
 
9
11
  This tool enqueues a background task and returns an acknowledgement with task id.
@@ -30,6 +32,13 @@ export function updateTrinityDelegateToolDescription(databases) {
30
32
  const full = `${TRINITY_BASE_DESCRIPTION}${buildDatabaseCatalog(databases)}`;
31
33
  TrinityDelegateTool.description = full;
32
34
  }
35
+ /**
36
+ * Returns true when Trinity is configured to execute synchronously (inline).
37
+ */
38
+ function isTrinitySync() {
39
+ const config = ConfigManager.getInstance().get();
40
+ return config.trinity?.execution_mode === 'sync';
41
+ }
33
42
  export const TrinityDelegateTool = tool(async ({ task, context }) => {
34
43
  try {
35
44
  const display = DisplayManager.getInstance();
@@ -40,6 +49,24 @@ export const TrinityDelegateTool = tool(async ({ task, context }) => {
40
49
  });
41
50
  return compositeDelegationError();
42
51
  }
52
+ // ── Sync mode: execute inline and return result directly ──
53
+ if (isTrinitySync()) {
54
+ display.log(`Trinity executing synchronously: ${task.slice(0, 80)}...`, {
55
+ source: 'TrinityDelegateTool',
56
+ level: 'info',
57
+ });
58
+ const ctx = TaskRequestContext.get();
59
+ const sessionId = ctx?.session_id ?? 'default';
60
+ const trinity = Trinity.getInstance();
61
+ const result = await trinity.execute(task, context, sessionId);
62
+ TaskRequestContext.incrementSyncDelegation();
63
+ display.log(`Trinity sync execution completed.`, {
64
+ source: 'TrinityDelegateTool',
65
+ level: 'info',
66
+ });
67
+ return result;
68
+ }
69
+ // ── Async mode (default): create background task ──
43
70
  const existingAck = TaskRequestContext.findDuplicateDelegation('trinit', task);
44
71
  if (existingAck) {
45
72
  display.log(`Trinity delegation deduplicated. Reusing task ${existingAck.task_id}.`, {
@@ -51,17 +51,20 @@ export const DEFAULT_CONFIG = {
51
51
  temperature: 0.2,
52
52
  timeout_ms: 30000,
53
53
  personality: 'pragmatic_dev',
54
+ execution_mode: 'async',
54
55
  },
55
56
  neo: {
56
57
  provider: 'openai',
57
58
  model: 'gpt-4',
58
59
  temperature: 0.2,
59
60
  personality: 'analytical_engineer',
61
+ execution_mode: 'async',
60
62
  },
61
63
  trinity: {
62
64
  provider: 'openai',
63
65
  model: 'gpt-4',
64
66
  temperature: 0.2,
65
67
  personality: 'data_specialist',
68
+ execution_mode: 'async',
66
69
  }
67
70
  };