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
package/README.md CHANGED
@@ -22,6 +22,7 @@ It runs as a daemon and orchestrates LLMs, MCP tools, DevKit tools, memory, and
22
22
  - `Sati`: long-term memory retrieval/evaluation.
23
23
  - `Trinity`: database specialist. Executes queries, introspects schemas, and manages registered databases (PostgreSQL, MySQL, SQLite, MongoDB).
24
24
  - `Chronos`: temporal scheduler. Runs Oracle prompts on a recurring or one-time schedule.
25
+ - `Keymaker`: skill executor. Runs user-defined skills with full tool access (DevKit + MCP + internal tools).
25
26
 
26
27
  ## Installation
27
28
 
@@ -441,6 +442,124 @@ Precedence order:
441
442
  3. `zaion.yaml`
442
443
  4. Defaults
443
444
 
445
+ ## Skills
446
+
447
+ Skills are user-defined capabilities that extend Morpheus. Each skill is a folder in `~/.morpheus/skills/` containing a single `SKILL.md` file with YAML frontmatter for metadata.
448
+
449
+ ### Execution Modes
450
+
451
+ Skills support two execution modes:
452
+
453
+ - **`sync`** (default) - Executes immediately via `skill_execute`, returns result inline
454
+ - **`async`** - Runs as background task via `skill_delegate`, notifies when complete
455
+
456
+ ### Creating a Skill
457
+
458
+ ```bash
459
+ mkdir -p ~/.morpheus/skills/my-skill
460
+ ```
461
+
462
+ **SKILL.md:**
463
+ ```markdown
464
+ ---
465
+ name: my-skill
466
+ description: Brief description of what this skill does
467
+ version: 1.0.0
468
+ author: your-name
469
+ enabled: true
470
+ execution_mode: sync
471
+ tags:
472
+ - automation
473
+ - example
474
+ examples:
475
+ - "Example prompt that triggers this skill"
476
+ ---
477
+
478
+ # My Skill
479
+
480
+ You are an expert at [domain]. Follow these instructions...
481
+
482
+ ## Your Task
483
+ [Instructions for Keymaker to execute]
484
+ ```
485
+
486
+ ### Async Skill Example
487
+
488
+ For long-running tasks like deployments or builds:
489
+
490
+ ```markdown
491
+ ---
492
+ name: deploy-staging
493
+ description: Deploy application to staging environment
494
+ execution_mode: async
495
+ tags:
496
+ - deployment
497
+ - devops
498
+ ---
499
+
500
+ # Deploy to Staging
501
+
502
+ Execute a full deployment to the staging environment...
503
+ ```
504
+
505
+ ### Using Skills
506
+
507
+ Once a skill is loaded, Oracle will automatically suggest delegating matching tasks to Keymaker:
508
+
509
+ **Sync Skills (immediate result):**
510
+ ```
511
+ User: "Review the code in src/auth.ts"
512
+ Oracle: [executes code-reviewer skill via skill_execute]
513
+ Keymaker: [returns detailed review immediately]
514
+ ```
515
+
516
+ **Async Skills (background task):**
517
+ ```
518
+ User: "Deploy to staging"
519
+ Oracle: [delegates via skill_delegate, task queued]
520
+ Morpheus: [notifies via Telegram/Discord when complete]
521
+ ```
522
+
523
+ ### Managing Skills
524
+
525
+ **CLI Commands:**
526
+ ```bash
527
+ # View loaded skills
528
+ morpheus skills
529
+
530
+ # Reload skills from disk
531
+ morpheus skills --reload
532
+ ```
533
+
534
+ **API Endpoints:**
535
+ ```
536
+ GET /api/skills - List all skills
537
+ GET /api/skills/:name - Get skill details
538
+ POST /api/skills/reload - Reload from filesystem
539
+ POST /api/skills/:name/enable - Enable skill
540
+ POST /api/skills/:name/disable - Disable skill
541
+ ```
542
+
543
+ **Telegram/Discord Commands:**
544
+ ```
545
+ /skills - List skills
546
+ /skill_reload - Reload skills
547
+ /skill_enable <name> - Enable a skill
548
+ /skill_disable <name> - Disable a skill
549
+ ```
550
+
551
+ ### Sample Skills
552
+
553
+ Example skills are available in `examples/skills/`:
554
+ - `code-reviewer` - Reviews code for issues and best practices (sync)
555
+ - `git-helper` - Assists with Git operations (sync)
556
+ - `deploy-staging` - Deploy to staging environment (async)
557
+
558
+ Copy them to your skills directory:
559
+ ```bash
560
+ cp -r examples/skills/* ~/.morpheus/skills/
561
+ ```
562
+
444
563
  ## MCP Configuration
445
564
 
446
565
  Configure MCP servers in `~/.morpheus/mcps.json`.
@@ -56,6 +56,24 @@ const SLASH_COMMANDS = [
56
56
  .setDescription('Delete a Chronos job')
57
57
  .addStringOption(opt => opt.setName('id').setDescription('Job ID').setRequired(true))
58
58
  .setDMPermission(true),
59
+ new SlashCommandBuilder()
60
+ .setName('skills')
61
+ .setDescription('List all available skills')
62
+ .setDMPermission(true),
63
+ new SlashCommandBuilder()
64
+ .setName('skill_reload')
65
+ .setDescription('Reload skills from filesystem')
66
+ .setDMPermission(true),
67
+ new SlashCommandBuilder()
68
+ .setName('skill_enable')
69
+ .setDescription('Enable a skill')
70
+ .addStringOption(opt => opt.setName('name').setDescription('Skill name').setRequired(true))
71
+ .setDMPermission(true),
72
+ new SlashCommandBuilder()
73
+ .setName('skill_disable')
74
+ .setDescription('Disable a skill')
75
+ .addStringOption(opt => opt.setName('name').setDescription('Skill name').setRequired(true))
76
+ .setDMPermission(true),
59
77
  ].map(cmd => cmd.toJSON());
60
78
  // ─── Adapter ──────────────────────────────────────────────────────────────────
61
79
  export class DiscordAdapter {
@@ -337,6 +355,18 @@ export class DiscordAdapter {
337
355
  case 'chronos_delete':
338
356
  await this.cmdChronosDelete(interaction);
339
357
  break;
358
+ case 'skills':
359
+ await this.cmdSkills(interaction);
360
+ break;
361
+ case 'skill_reload':
362
+ await this.cmdSkillReload(interaction);
363
+ break;
364
+ case 'skill_enable':
365
+ await this.cmdSkillEnable(interaction);
366
+ break;
367
+ case 'skill_disable':
368
+ await this.cmdSkillDisable(interaction);
369
+ break;
340
370
  }
341
371
  }
342
372
  async cmdHelp(interaction) {
@@ -356,6 +386,12 @@ export class DiscordAdapter {
356
386
  '`/chronos_enable id:` — Enable a job',
357
387
  '`/chronos_delete id:` — Delete a job',
358
388
  '',
389
+ '**Skills**',
390
+ '`/skills` — List all available skills',
391
+ '`/skill_reload` — Reload skills from filesystem',
392
+ '`/skill_enable name:` — Enable a skill',
393
+ '`/skill_disable name:` — Disable a skill',
394
+ '',
359
395
  'You can also send text or voice messages to chat with the Oracle.',
360
396
  ].join('\n');
361
397
  await interaction.reply({ content });
@@ -548,6 +584,79 @@ export class DiscordAdapter {
548
584
  await interaction.reply({ content: `Error: ${err.message}` });
549
585
  }
550
586
  }
587
+ // ─── Skills Commands ─────────────────────────────────────────────────────
588
+ async cmdSkills(interaction) {
589
+ try {
590
+ const { SkillRegistry } = await import('../runtime/skills/index.js');
591
+ const registry = SkillRegistry.getInstance();
592
+ const skills = registry.getAll();
593
+ if (!skills.length) {
594
+ await interaction.reply({ content: 'No skills found. Add skills to `~/.morpheus/skills/`' });
595
+ return;
596
+ }
597
+ const lines = skills.map(s => {
598
+ const status = s.enabled ? '🟢' : '🔴';
599
+ const tags = s.tags?.length ? ` [${s.tags.join(', ')}]` : '';
600
+ return `${status} **${s.name}**${tags}\n _${s.description.slice(0, 50)}${s.description.length > 50 ? '…' : ''}_`;
601
+ });
602
+ const enabled = skills.filter(s => s.enabled).length;
603
+ await interaction.reply({
604
+ content: `**Skills** (${enabled}/${skills.length} enabled)\n\n${lines.join('\n')}`
605
+ });
606
+ }
607
+ catch (err) {
608
+ await interaction.reply({ content: `Error: ${err.message}` });
609
+ }
610
+ }
611
+ async cmdSkillReload(interaction) {
612
+ try {
613
+ const { SkillRegistry, updateSkillDelegateDescription } = await import('../runtime/skills/index.js');
614
+ const registry = SkillRegistry.getInstance();
615
+ const result = await registry.reload();
616
+ updateSkillDelegateDescription();
617
+ const msg = result.errors.length > 0
618
+ ? `Reloaded ${result.skills.length} skills with ${result.errors.length} error(s).`
619
+ : `Reloaded ${result.skills.length} skill(s).`;
620
+ await interaction.reply({ content: msg });
621
+ }
622
+ catch (err) {
623
+ await interaction.reply({ content: `Error: ${err.message}` });
624
+ }
625
+ }
626
+ async cmdSkillEnable(interaction) {
627
+ const name = interaction.options.getString('name', true);
628
+ try {
629
+ const { SkillRegistry, updateSkillDelegateDescription } = await import('../runtime/skills/index.js');
630
+ const registry = SkillRegistry.getInstance();
631
+ const success = registry.enable(name);
632
+ if (!success) {
633
+ await interaction.reply({ content: `Skill "${name}" not found.` });
634
+ return;
635
+ }
636
+ updateSkillDelegateDescription();
637
+ await interaction.reply({ content: `Skill \`${name}\` enabled.` });
638
+ }
639
+ catch (err) {
640
+ await interaction.reply({ content: `Error: ${err.message}` });
641
+ }
642
+ }
643
+ async cmdSkillDisable(interaction) {
644
+ const name = interaction.options.getString('name', true);
645
+ try {
646
+ const { SkillRegistry, updateSkillDelegateDescription } = await import('../runtime/skills/index.js');
647
+ const registry = SkillRegistry.getInstance();
648
+ const success = registry.disable(name);
649
+ if (!success) {
650
+ await interaction.reply({ content: `Skill "${name}" not found.` });
651
+ return;
652
+ }
653
+ updateSkillDelegateDescription();
654
+ await interaction.reply({ content: `Skill \`${name}\` disabled.` });
655
+ }
656
+ catch (err) {
657
+ await interaction.reply({ content: `Error: ${err.message}` });
658
+ }
659
+ }
551
660
  // ─── Helpers ──────────────────────────────────────────────────────────────
552
661
  isAuthorized(userId) {
553
662
  return this.allowedUsers.includes(userId);
@@ -801,6 +801,22 @@ export class TelegramAdapter {
801
801
  await this.handleChronosDelete(ctx, id);
802
802
  break;
803
803
  }
804
+ case '/skills':
805
+ await this.handleSkillsList(ctx);
806
+ break;
807
+ case '/skill_reload':
808
+ await this.handleSkillsReload(ctx);
809
+ break;
810
+ case '/skill_enable': {
811
+ const name = args[0] ?? '';
812
+ await this.handleSkillEnable(ctx, name);
813
+ break;
814
+ }
815
+ case '/skill_disable': {
816
+ const name = args[0] ?? '';
817
+ await this.handleSkillDisable(ctx, name);
818
+ break;
819
+ }
804
820
  default:
805
821
  await this.handleDefaultCommand(ctx, user, command);
806
822
  }
@@ -1000,6 +1016,84 @@ export class TelegramAdapter {
1000
1016
  }
1001
1017
  }
1002
1018
  // ─── End Chronos ──────────────────────────────────────────────────────────────
1019
+ // ─── Skills Command Handlers ─────────────────────────────────────────────────
1020
+ async handleSkillsList(ctx) {
1021
+ try {
1022
+ const { SkillRegistry } = await import('../runtime/skills/index.js');
1023
+ const registry = SkillRegistry.getInstance();
1024
+ const skills = registry.getAll();
1025
+ if (!skills.length) {
1026
+ await ctx.reply('No skills found. Add skills to `~/.morpheus/skills/`', { parse_mode: 'Markdown' });
1027
+ return;
1028
+ }
1029
+ const lines = skills.map(s => {
1030
+ const status = s.enabled ? '🟢' : '🔴';
1031
+ const tags = s.tags?.length ? ` [${s.tags.join(', ')}]` : '';
1032
+ return `${status} *${s.name}*${tags}\n _${s.description.slice(0, 50)}${s.description.length > 50 ? '…' : ''}_`;
1033
+ });
1034
+ const enabled = skills.filter(s => s.enabled).length;
1035
+ await ctx.reply(`*Skills* (${enabled}/${skills.length} enabled)\n\n${lines.join('\n')}`, { parse_mode: 'Markdown' });
1036
+ }
1037
+ catch (err) {
1038
+ await ctx.reply(`Error: ${err.message}`);
1039
+ }
1040
+ }
1041
+ async handleSkillsReload(ctx) {
1042
+ try {
1043
+ const { SkillRegistry, updateSkillDelegateDescription } = await import('../runtime/skills/index.js');
1044
+ const registry = SkillRegistry.getInstance();
1045
+ const result = await registry.reload();
1046
+ updateSkillDelegateDescription();
1047
+ const msg = result.errors.length > 0
1048
+ ? `Reloaded ${result.skills.length} skills with ${result.errors.length} error(s).`
1049
+ : `Reloaded ${result.skills.length} skill(s).`;
1050
+ await ctx.reply(msg);
1051
+ }
1052
+ catch (err) {
1053
+ await ctx.reply(`Error: ${err.message}`);
1054
+ }
1055
+ }
1056
+ async handleSkillEnable(ctx, name) {
1057
+ if (!name) {
1058
+ await ctx.reply('Usage: /skill_enable <name>');
1059
+ return;
1060
+ }
1061
+ try {
1062
+ const { SkillRegistry, updateSkillDelegateDescription } = await import('../runtime/skills/index.js');
1063
+ const registry = SkillRegistry.getInstance();
1064
+ const success = registry.enable(name);
1065
+ if (!success) {
1066
+ await ctx.reply(`Skill "${name}" not found.`);
1067
+ return;
1068
+ }
1069
+ updateSkillDelegateDescription();
1070
+ await ctx.reply(`Skill \`${name}\` enabled.`, { parse_mode: 'Markdown' });
1071
+ }
1072
+ catch (err) {
1073
+ await ctx.reply(`Error: ${err.message}`);
1074
+ }
1075
+ }
1076
+ async handleSkillDisable(ctx, name) {
1077
+ if (!name) {
1078
+ await ctx.reply('Usage: /skill_disable <name>');
1079
+ return;
1080
+ }
1081
+ try {
1082
+ const { SkillRegistry, updateSkillDelegateDescription } = await import('../runtime/skills/index.js');
1083
+ const registry = SkillRegistry.getInstance();
1084
+ const success = registry.disable(name);
1085
+ if (!success) {
1086
+ await ctx.reply(`Skill "${name}" not found.`);
1087
+ return;
1088
+ }
1089
+ updateSkillDelegateDescription();
1090
+ await ctx.reply(`Skill \`${name}\` disabled.`, { parse_mode: 'Markdown' });
1091
+ }
1092
+ catch (err) {
1093
+ await ctx.reply(`Error: ${err.message}`);
1094
+ }
1095
+ }
1096
+ // ─── End Skills ───────────────────────────────────────────────────────────────
1003
1097
  async handleNewSessionCommand(ctx, user) {
1004
1098
  try {
1005
1099
  await ctx.reply("Are you ready to start a new session\\? Please confirm\\.", {
@@ -22,6 +22,7 @@ import { TaskWorker } from '../../runtime/tasks/worker.js';
22
22
  import { TaskNotifier } from '../../runtime/tasks/notifier.js';
23
23
  import { ChronosWorker } from '../../runtime/chronos/worker.js';
24
24
  import { ChronosRepository } from '../../runtime/chronos/repository.js';
25
+ import { SkillRegistry } from '../../runtime/skills/index.js';
25
26
  // Load .env file explicitly in start command
26
27
  const envPath = path.join(process.cwd(), '.env');
27
28
  if (fs.existsSync(envPath)) {
@@ -126,6 +127,17 @@ export const startCommand = new Command('start')
126
127
  if (options.ui) {
127
128
  display.log(chalk.blue(`Web UI enabled to port ${options.port}`), { source: 'Zaion' });
128
129
  }
130
+ // Initialize SkillRegistry before Oracle (so skills are available in system prompt)
131
+ try {
132
+ const skillRegistry = SkillRegistry.getInstance();
133
+ await skillRegistry.load();
134
+ const loadedSkills = skillRegistry.getAll();
135
+ const enabledCount = skillRegistry.getEnabled().length;
136
+ display.log(chalk.green(`✓ Skills loaded: ${loadedSkills.length} total, ${enabledCount} enabled`), { source: 'Skills' });
137
+ }
138
+ catch (err) {
139
+ display.log(chalk.yellow(`Skills initialization warning: ${err.message}`), { source: 'Skills' });
140
+ }
129
141
  // Initialize Oracle
130
142
  const oracle = new Oracle(config);
131
143
  try {
@@ -169,6 +169,7 @@ export class ConfigManager {
169
169
  working_dir: resolveString('MORPHEUS_APOC_WORKING_DIR', config.apoc.working_dir, process.cwd()),
170
170
  timeout_ms: config.apoc.timeout_ms !== undefined ? resolveNumeric('MORPHEUS_APOC_TIMEOUT_MS', config.apoc.timeout_ms, 30_000) : 30_000,
171
171
  personality: resolveString('MORPHEUS_APOC_PERSONALITY', config.apoc.personality, 'pragmatic_dev'),
172
+ execution_mode: resolveString('MORPHEUS_APOC_EXECUTION_MODE', config.apoc.execution_mode, 'async'),
172
173
  };
173
174
  }
174
175
  // Apply precedence to Neo config
@@ -209,6 +210,7 @@ export class ConfigManager {
209
210
  base_url: neoBaseUrl || undefined,
210
211
  context_window: resolveOptionalNumeric('MORPHEUS_NEO_CONTEXT_WINDOW', config.neo?.context_window, neoContextWindowFallback),
211
212
  personality: resolveString('MORPHEUS_NEO_PERSONALITY', config.neo?.personality, 'analytical_engineer'),
213
+ execution_mode: resolveString('MORPHEUS_NEO_EXECUTION_MODE', config.neo?.execution_mode, 'async'),
212
214
  };
213
215
  }
214
216
  // Apply precedence to Trinity config
@@ -233,6 +235,7 @@ export class ConfigManager {
233
235
  base_url: config.trinity?.base_url || config.llm.base_url,
234
236
  context_window: resolveOptionalNumeric('MORPHEUS_TRINITY_CONTEXT_WINDOW', config.trinity?.context_window, trinityContextWindowFallback),
235
237
  personality: resolveString('MORPHEUS_TRINITY_PERSONALITY', config.trinity?.personality, 'data_specialist'),
238
+ execution_mode: resolveString('MORPHEUS_TRINITY_EXECUTION_MODE', config.trinity?.execution_mode, 'async'),
236
239
  };
237
240
  }
238
241
  // Apply precedence to audio config
@@ -14,4 +14,5 @@ export const PATHS = {
14
14
  browser: path.join(MORPHEUS_ROOT, 'cache', 'browser'),
15
15
  commands: path.join(MORPHEUS_ROOT, 'commands'),
16
16
  mcps: path.join(MORPHEUS_ROOT, 'mcps.json'),
17
+ skills: path.join(MORPHEUS_ROOT, 'skills'),
17
18
  };
@@ -25,9 +25,17 @@ export const SatiConfigSchema = LLMConfigSchema.extend({
25
25
  export const ApocConfigSchema = LLMConfigSchema.extend({
26
26
  working_dir: z.string().optional(),
27
27
  timeout_ms: z.number().int().positive().optional(),
28
+ execution_mode: z.enum(['sync', 'async']).default('async'),
29
+ });
30
+ export const NeoConfigSchema = LLMConfigSchema.extend({
31
+ execution_mode: z.enum(['sync', 'async']).default('async'),
32
+ });
33
+ export const TrinityConfigSchema = LLMConfigSchema.extend({
34
+ execution_mode: z.enum(['sync', 'async']).default('async'),
35
+ });
36
+ export const KeymakerConfigSchema = LLMConfigSchema.extend({
37
+ skills_dir: z.string().optional(),
28
38
  });
29
- export const NeoConfigSchema = LLMConfigSchema;
30
- export const TrinityConfigSchema = LLMConfigSchema;
31
39
  export const WebhookConfigSchema = z.object({
32
40
  telegram_notify_all: z.boolean().optional(),
33
41
  }).optional();
@@ -47,6 +55,7 @@ export const ConfigSchema = z.object({
47
55
  neo: NeoConfigSchema.optional(),
48
56
  apoc: ApocConfigSchema.optional(),
49
57
  trinity: TrinityConfigSchema.optional(),
58
+ keymaker: KeymakerConfigSchema.optional(),
50
59
  webhooks: WebhookConfigSchema,
51
60
  audio: AudioConfigSchema.default(DEFAULT_CONFIG.audio),
52
61
  memory: z.object({
package/dist/http/api.js CHANGED
@@ -18,6 +18,7 @@ import { Trinity } from '../runtime/trinity.js';
18
18
  import { ChronosRepository } from '../runtime/chronos/repository.js';
19
19
  import { ChronosWorker } from '../runtime/chronos/worker.js';
20
20
  import { createChronosJobRouter, createChronosConfigRouter } from './routers/chronos.js';
21
+ import { createSkillsRouter } from './routers/skills.js';
21
22
  import { getActiveEnvOverrides } from '../config/precedence.js';
22
23
  async function readLastLines(filePath, n) {
23
24
  try {
@@ -41,6 +42,8 @@ export function createApiRouter(oracle, chronosWorker) {
41
42
  router.use('/chronos', createChronosJobRouter(chronosRepo, worker));
42
43
  router.use('/config/chronos', createChronosConfigRouter(worker));
43
44
  }
45
+ // Mount Skills router
46
+ router.use('/skills', createSkillsRouter());
44
47
  // --- Session Management ---
45
48
  router.get('/sessions', async (req, res) => {
46
49
  try {