mu-harness 0.17.5 → 0.18.0

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.
@@ -1,3 +1,4 @@
1
1
  export { createAgentsCommand, createHelpCommand, createQuitCommand, createSessionsCommand, createSkillsCommand, } from './defaults.js';
2
2
  export { createCommandRegistry } from './registry.js';
3
+ export { createSkillCommand, type SkillCommandDeps } from './skill.js';
3
4
  export type { Command, CommandContext, CommandRegistry, CommandResult } from './types.js';
@@ -1,2 +1,3 @@
1
1
  export { createAgentsCommand, createHelpCommand, createQuitCommand, createSessionsCommand, createSkillsCommand, } from './defaults.js';
2
2
  export { createCommandRegistry } from './registry.js';
3
+ export { createSkillCommand } from './skill.js';
@@ -0,0 +1,12 @@
1
+ import type { Agent } from '../agents/index.js';
2
+ import { type RunSkillDeps, type Skill } from '../skills/index.js';
3
+ import type { Command } from './types.js';
4
+ export type SkillCommandDeps = RunSkillDeps & {
5
+ activeAgent?: () => Agent | undefined;
6
+ };
7
+ /**
8
+ * Builds a slash command that runs a skill — the opt-in counterpart to a skill's
9
+ * `command` field. Invoking it runs the skill (as the active agent, or the first
10
+ * registered one) with the command args as its task, and returns the output.
11
+ */
12
+ export declare const createSkillCommand: (skill: Skill, deps: SkillCommandDeps) => Command;
@@ -0,0 +1,21 @@
1
+ import { runSkill } from '../skills/index.js';
2
+ /**
3
+ * Builds a slash command that runs a skill — the opt-in counterpart to a skill's
4
+ * `command` field. Invoking it runs the skill (as the active agent, or the first
5
+ * registered one) with the command args as its task, and returns the output.
6
+ */
7
+ export const createSkillCommand = (skill, deps) => ({
8
+ name: skill.command,
9
+ description: skill.description || `Run the "${skill.name}" skill`,
10
+ run: async (args) => {
11
+ const agent = deps.activeAgent?.()?.name ?? deps.agents?.list()[0]?.name;
12
+ if (!agent)
13
+ return { ok: false, error: 'no agent available to run the skill' };
14
+ try {
15
+ return { ok: true, output: await runSkill(deps, { skill: skill.name, task: args, agent }) };
16
+ }
17
+ catch (error) {
18
+ return { ok: false, error: error instanceof Error ? error.message : String(error) };
19
+ }
20
+ },
21
+ });
@@ -2,7 +2,7 @@ import os from 'node:os';
2
2
  import { join } from 'node:path';
3
3
  import process from 'node:process';
4
4
  import { createAgentRegistry, createAgentWriterTool, grantArg, loadAgents, toolDecision, toolNames } from '../agents/index.js';
5
- import { createAgentsCommand, createCommandRegistry, createHelpCommand, createSessionsCommand, createSkillsCommand, } from '../commands/index.js';
5
+ import { createAgentsCommand, createCommandRegistry, createHelpCommand, createSessionsCommand, createSkillCommand, createSkillsCommand, } from '../commands/index.js';
6
6
  import { createHarnessConfig } from '../config/index.js';
7
7
  import { mergeHooks } from '../hooks/index.js';
8
8
  import { allowList } from '../permissions/index.js';
@@ -173,6 +173,22 @@ export const createHarness = async (options) => {
173
173
  ...(tasks ? [createTasksCommand(tasks)] : []),
174
174
  ]);
175
175
  commands.register(createHelpCommand(() => commands.list()));
176
+ // Register slash commands for skills that opt in via their `command` field,
177
+ // and keep them in sync across hot-reloads. A name already taken by another
178
+ // command is left alone (built-ins win).
179
+ const skillCommandNames = new Set();
180
+ const syncSkillCommands = () => {
181
+ for (const name of skillCommandNames)
182
+ commands.unregister(name);
183
+ skillCommandNames.clear();
184
+ for (const skill of skills.list()) {
185
+ if (!skill.command || commands.get(skill.command))
186
+ continue;
187
+ commands.register(createSkillCommand(skill, { skills, agents, spawn, runs, activeAgent: () => approvals?.activeAgent() }));
188
+ skillCommandNames.add(skill.command);
189
+ }
190
+ };
191
+ syncSkillCommands();
176
192
  if (scheduler)
177
193
  await scheduler.start();
178
194
  return {
@@ -192,6 +208,7 @@ export const createHarness = async (options) => {
192
208
  reloadDefinitions: async () => {
193
209
  agents.replaceAll(await mergedAgents());
194
210
  skills.replaceAll(await mergedSkills());
211
+ syncSkillCommands();
195
212
  },
196
213
  scheduler,
197
214
  tasks,
@@ -12,5 +12,8 @@ export const parseSkill = (source, fallbackName, dir) => {
12
12
  skill.dir = dir;
13
13
  if (platforms.length)
14
14
  skill.platforms = platforms;
15
+ const command = str(fields.command);
16
+ if (command)
17
+ skill.command = command;
15
18
  return skill;
16
19
  };
@@ -5,4 +5,9 @@ export interface Skill {
5
5
  dir?: string;
6
6
  /** OS allow-list (Hermes/agentskills.io `platforms`): macos | linux | windows. Empty/absent = all. */
7
7
  platforms?: string[];
8
+ /**
9
+ * Opt-in slash command name. When set, the harness registers a command that
10
+ * runs this skill (args become its task). Absent = no command for this skill.
11
+ */
12
+ command?: string;
8
13
  }
@@ -62,6 +62,21 @@ export interface ChatHost {
62
62
  * in the status line) leave this unset (the default).
63
63
  */
64
64
  minimal?: boolean;
65
+ /**
66
+ * Harness slash commands to surface in the TUI alongside the built-in ones
67
+ * (e.g. `/agents`, skill commands). Called fresh so hot-reloaded commands show
68
+ * up. A name that collides with a built-in TUI command is ignored.
69
+ */
70
+ commands?(): {
71
+ name: string;
72
+ description: string;
73
+ }[];
74
+ /** Runs a harness command (e.g. "/agents foo"); its output is shown in the transcript. */
75
+ runCommand?(input: string): Promise<{
76
+ ok: boolean;
77
+ output?: unknown;
78
+ error?: string;
79
+ }>;
65
80
  onExit(code: number): void;
66
81
  }
67
82
  export declare class ChatApp {
@@ -145,6 +160,8 @@ export declare class ChatApp {
145
160
  private cycleAgent;
146
161
  private onEscape;
147
162
  private cancelGeneration;
163
+ private harnessCommands;
164
+ private allCommands;
148
165
  private paletteItems;
149
166
  private paletteMove;
150
167
  private paletteComplete;
@@ -691,8 +691,35 @@ export class ChatApp {
691
691
  this.onTurnComplete();
692
692
  this.tui.requestRender();
693
693
  }
694
+ // Built-in TUI commands plus any harness commands (computed fresh so hot-reloaded
695
+ // skill commands appear). Harness names that collide with a TUI command are dropped.
696
+ harnessCommands() {
697
+ if (!this.host.commands || !this.host.runCommand)
698
+ return [];
699
+ const taken = new Set(this.commands.map((c) => c.name));
700
+ return this.host.commands()
701
+ .filter((c) => !taken.has(c.name))
702
+ .map((c) => ({
703
+ name: c.name,
704
+ description: c.description,
705
+ run: async (args) => {
706
+ const res = await this.host.runCommand(`/${c.name}${args ? ` ${args}` : ''}`);
707
+ if (res.ok) {
708
+ if (res.output != null)
709
+ this.transcript.note(String(res.output));
710
+ }
711
+ else {
712
+ this.transcript.note(res.error ?? 'command failed', true);
713
+ }
714
+ this.tui.requestRender();
715
+ },
716
+ }));
717
+ }
718
+ allCommands() {
719
+ return [...this.commands, ...this.harnessCommands()];
720
+ }
694
721
  paletteItems() {
695
- return filterCommands(this.commands, this.editor.getValue(), this.paletteDismissedFor).slice(0, MAX_LIST_ROWS);
722
+ return filterCommands(this.allCommands(), this.editor.getValue(), this.paletteDismissedFor).slice(0, MAX_LIST_ROWS);
696
723
  }
697
724
  paletteMove(delta) {
698
725
  const items = this.paletteItems();
@@ -726,7 +753,7 @@ export class ChatApp {
726
753
  const space = body.indexOf(' ');
727
754
  const name = (space === -1 ? body : body.slice(0, space)).toLowerCase();
728
755
  const args = space === -1 ? '' : body.slice(space + 1).trim();
729
- const command = this.commands.find((c) => c.name === name);
756
+ const command = this.allCommands().find((c) => c.name === name);
730
757
  if (!command) {
731
758
  this.transcript.note(`Unknown command: /${name}`, true);
732
759
  this.tui.requestRender();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mu-harness",
3
- "version": "0.17.5",
3
+ "version": "0.18.0",
4
4
  "description": "Agent harness: createHarness wires mu-core into a host — XDG paths, model registry, plugins, disk-loaded agents & skills, sub-agents, sessions (JSONL + SQLite catalog), slash commands, permission/approval hooks, an optional scheduler, and a composable TUI chat app",
5
5
  "license": "MIT",
6
6
  "main": "./script/index.js",
@@ -23,8 +23,8 @@
23
23
  "@swc/wasm-typescript": "^1.15.0",
24
24
  "cli-highlight": "^2.1.11",
25
25
  "croner": "^9.0.0",
26
- "mu-core": "^0.17.5",
27
- "mu-tui": "^0.17.5"
26
+ "mu-core": "^0.18.0",
27
+ "mu-tui": "^0.18.0"
28
28
  },
29
29
  "_generatedBy": "dnt@dev",
30
30
  "types": "./esm/index.d.ts"
@@ -1,3 +1,4 @@
1
1
  export { createAgentsCommand, createHelpCommand, createQuitCommand, createSessionsCommand, createSkillsCommand, } from './defaults.js';
2
2
  export { createCommandRegistry } from './registry.js';
3
+ export { createSkillCommand, type SkillCommandDeps } from './skill.js';
3
4
  export type { Command, CommandContext, CommandRegistry, CommandResult } from './types.js';
@@ -1,6 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.createCommandRegistry = exports.createSkillsCommand = exports.createSessionsCommand = exports.createQuitCommand = exports.createHelpCommand = exports.createAgentsCommand = void 0;
3
+ exports.createSkillCommand = exports.createCommandRegistry = exports.createSkillsCommand = exports.createSessionsCommand = exports.createQuitCommand = exports.createHelpCommand = exports.createAgentsCommand = void 0;
4
4
  var defaults_js_1 = require("./defaults.js");
5
5
  Object.defineProperty(exports, "createAgentsCommand", { enumerable: true, get: function () { return defaults_js_1.createAgentsCommand; } });
6
6
  Object.defineProperty(exports, "createHelpCommand", { enumerable: true, get: function () { return defaults_js_1.createHelpCommand; } });
@@ -9,3 +9,5 @@ Object.defineProperty(exports, "createSessionsCommand", { enumerable: true, get:
9
9
  Object.defineProperty(exports, "createSkillsCommand", { enumerable: true, get: function () { return defaults_js_1.createSkillsCommand; } });
10
10
  var registry_js_1 = require("./registry.js");
11
11
  Object.defineProperty(exports, "createCommandRegistry", { enumerable: true, get: function () { return registry_js_1.createCommandRegistry; } });
12
+ var skill_js_1 = require("./skill.js");
13
+ Object.defineProperty(exports, "createSkillCommand", { enumerable: true, get: function () { return skill_js_1.createSkillCommand; } });
@@ -0,0 +1,12 @@
1
+ import type { Agent } from '../agents/index.js';
2
+ import { type RunSkillDeps, type Skill } from '../skills/index.js';
3
+ import type { Command } from './types.js';
4
+ export type SkillCommandDeps = RunSkillDeps & {
5
+ activeAgent?: () => Agent | undefined;
6
+ };
7
+ /**
8
+ * Builds a slash command that runs a skill — the opt-in counterpart to a skill's
9
+ * `command` field. Invoking it runs the skill (as the active agent, or the first
10
+ * registered one) with the command args as its task, and returns the output.
11
+ */
12
+ export declare const createSkillCommand: (skill: Skill, deps: SkillCommandDeps) => Command;
@@ -0,0 +1,25 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.createSkillCommand = void 0;
4
+ const index_js_1 = require("../skills/index.js");
5
+ /**
6
+ * Builds a slash command that runs a skill — the opt-in counterpart to a skill's
7
+ * `command` field. Invoking it runs the skill (as the active agent, or the first
8
+ * registered one) with the command args as its task, and returns the output.
9
+ */
10
+ const createSkillCommand = (skill, deps) => ({
11
+ name: skill.command,
12
+ description: skill.description || `Run the "${skill.name}" skill`,
13
+ run: async (args) => {
14
+ const agent = deps.activeAgent?.()?.name ?? deps.agents?.list()[0]?.name;
15
+ if (!agent)
16
+ return { ok: false, error: 'no agent available to run the skill' };
17
+ try {
18
+ return { ok: true, output: await (0, index_js_1.runSkill)(deps, { skill: skill.name, task: args, agent }) };
19
+ }
20
+ catch (error) {
21
+ return { ok: false, error: error instanceof Error ? error.message : String(error) };
22
+ }
23
+ },
24
+ });
25
+ exports.createSkillCommand = createSkillCommand;
@@ -179,6 +179,22 @@ const createHarness = async (options) => {
179
179
  ...(tasks ? [(0, index_js_7.createTasksCommand)(tasks)] : []),
180
180
  ]);
181
181
  commands.register((0, index_js_2.createHelpCommand)(() => commands.list()));
182
+ // Register slash commands for skills that opt in via their `command` field,
183
+ // and keep them in sync across hot-reloads. A name already taken by another
184
+ // command is left alone (built-ins win).
185
+ const skillCommandNames = new Set();
186
+ const syncSkillCommands = () => {
187
+ for (const name of skillCommandNames)
188
+ commands.unregister(name);
189
+ skillCommandNames.clear();
190
+ for (const skill of skills.list()) {
191
+ if (!skill.command || commands.get(skill.command))
192
+ continue;
193
+ commands.register((0, index_js_2.createSkillCommand)(skill, { skills, agents, spawn, runs, activeAgent: () => approvals?.activeAgent() }));
194
+ skillCommandNames.add(skill.command);
195
+ }
196
+ };
197
+ syncSkillCommands();
182
198
  if (scheduler)
183
199
  await scheduler.start();
184
200
  return {
@@ -198,6 +214,7 @@ const createHarness = async (options) => {
198
214
  reloadDefinitions: async () => {
199
215
  agents.replaceAll(await mergedAgents());
200
216
  skills.replaceAll(await mergedSkills());
217
+ syncSkillCommands();
201
218
  },
202
219
  scheduler,
203
220
  tasks,
@@ -15,6 +15,9 @@ const parseSkill = (source, fallbackName, dir) => {
15
15
  skill.dir = dir;
16
16
  if (platforms.length)
17
17
  skill.platforms = platforms;
18
+ const command = (0, index_js_1.str)(fields.command);
19
+ if (command)
20
+ skill.command = command;
18
21
  return skill;
19
22
  };
20
23
  exports.parseSkill = parseSkill;
@@ -5,4 +5,9 @@ export interface Skill {
5
5
  dir?: string;
6
6
  /** OS allow-list (Hermes/agentskills.io `platforms`): macos | linux | windows. Empty/absent = all. */
7
7
  platforms?: string[];
8
+ /**
9
+ * Opt-in slash command name. When set, the harness registers a command that
10
+ * runs this skill (args become its task). Absent = no command for this skill.
11
+ */
12
+ command?: string;
8
13
  }
@@ -62,6 +62,21 @@ export interface ChatHost {
62
62
  * in the status line) leave this unset (the default).
63
63
  */
64
64
  minimal?: boolean;
65
+ /**
66
+ * Harness slash commands to surface in the TUI alongside the built-in ones
67
+ * (e.g. `/agents`, skill commands). Called fresh so hot-reloaded commands show
68
+ * up. A name that collides with a built-in TUI command is ignored.
69
+ */
70
+ commands?(): {
71
+ name: string;
72
+ description: string;
73
+ }[];
74
+ /** Runs a harness command (e.g. "/agents foo"); its output is shown in the transcript. */
75
+ runCommand?(input: string): Promise<{
76
+ ok: boolean;
77
+ output?: unknown;
78
+ error?: string;
79
+ }>;
65
80
  onExit(code: number): void;
66
81
  }
67
82
  export declare class ChatApp {
@@ -145,6 +160,8 @@ export declare class ChatApp {
145
160
  private cycleAgent;
146
161
  private onEscape;
147
162
  private cancelGeneration;
163
+ private harnessCommands;
164
+ private allCommands;
148
165
  private paletteItems;
149
166
  private paletteMove;
150
167
  private paletteComplete;
@@ -694,8 +694,35 @@ class ChatApp {
694
694
  this.onTurnComplete();
695
695
  this.tui.requestRender();
696
696
  }
697
+ // Built-in TUI commands plus any harness commands (computed fresh so hot-reloaded
698
+ // skill commands appear). Harness names that collide with a TUI command are dropped.
699
+ harnessCommands() {
700
+ if (!this.host.commands || !this.host.runCommand)
701
+ return [];
702
+ const taken = new Set(this.commands.map((c) => c.name));
703
+ return this.host.commands()
704
+ .filter((c) => !taken.has(c.name))
705
+ .map((c) => ({
706
+ name: c.name,
707
+ description: c.description,
708
+ run: async (args) => {
709
+ const res = await this.host.runCommand(`/${c.name}${args ? ` ${args}` : ''}`);
710
+ if (res.ok) {
711
+ if (res.output != null)
712
+ this.transcript.note(String(res.output));
713
+ }
714
+ else {
715
+ this.transcript.note(res.error ?? 'command failed', true);
716
+ }
717
+ this.tui.requestRender();
718
+ },
719
+ }));
720
+ }
721
+ allCommands() {
722
+ return [...this.commands, ...this.harnessCommands()];
723
+ }
697
724
  paletteItems() {
698
- return (0, commands_js_1.filterCommands)(this.commands, this.editor.getValue(), this.paletteDismissedFor).slice(0, MAX_LIST_ROWS);
725
+ return (0, commands_js_1.filterCommands)(this.allCommands(), this.editor.getValue(), this.paletteDismissedFor).slice(0, MAX_LIST_ROWS);
699
726
  }
700
727
  paletteMove(delta) {
701
728
  const items = this.paletteItems();
@@ -729,7 +756,7 @@ class ChatApp {
729
756
  const space = body.indexOf(' ');
730
757
  const name = (space === -1 ? body : body.slice(0, space)).toLowerCase();
731
758
  const args = space === -1 ? '' : body.slice(space + 1).trim();
732
- const command = this.commands.find((c) => c.name === name);
759
+ const command = this.allCommands().find((c) => c.name === name);
733
760
  if (!command) {
734
761
  this.transcript.note(`Unknown command: /${name}`, true);
735
762
  this.tui.requestRender();