mu-harness 0.17.3 → 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.
- package/esm/agents/registry.d.ts +5 -0
- package/esm/agents/registry.js +13 -6
- package/esm/commands/index.d.ts +1 -0
- package/esm/commands/index.js +1 -0
- package/esm/commands/skill.d.ts +12 -0
- package/esm/commands/skill.js +21 -0
- package/esm/harness/create.js +39 -8
- package/esm/harness/types.d.ts +11 -0
- package/esm/skills/parser.js +3 -0
- package/esm/skills/registry.d.ts +5 -0
- package/esm/skills/registry.js +8 -3
- package/esm/skills/types.d.ts +5 -0
- package/esm/tui/chat/ChatApp.d.ts +17 -0
- package/esm/tui/chat/ChatApp.js +29 -2
- package/package.json +3 -3
- package/script/agents/registry.d.ts +5 -0
- package/script/agents/registry.js +13 -6
- package/script/commands/index.d.ts +1 -0
- package/script/commands/index.js +3 -1
- package/script/commands/skill.d.ts +12 -0
- package/script/commands/skill.js +25 -0
- package/script/harness/create.js +38 -7
- package/script/harness/types.d.ts +11 -0
- package/script/skills/parser.js +3 -0
- package/script/skills/registry.d.ts +5 -0
- package/script/skills/registry.js +8 -3
- package/script/skills/types.d.ts +5 -0
- package/script/tui/chat/ChatApp.d.ts +17 -0
- package/script/tui/chat/ChatApp.js +29 -2
package/esm/agents/registry.d.ts
CHANGED
|
@@ -9,6 +9,11 @@ export interface AgentRegistry {
|
|
|
9
9
|
* re-resolved so they pick up the change.
|
|
10
10
|
*/
|
|
11
11
|
add(agent: Agent): void;
|
|
12
|
+
/**
|
|
13
|
+
* Replace the entire set in place (rebuild) — used by hot-reload to reflect
|
|
14
|
+
* created, edited, and deleted definitions. Existing references see the new set.
|
|
15
|
+
*/
|
|
16
|
+
replaceAll(agents: Agent[]): void;
|
|
12
17
|
}
|
|
13
18
|
export declare const grantArg: (tool: string, input: unknown) => string | undefined;
|
|
14
19
|
export declare const toolDecision: (agent: Agent, tool: string, arg?: string) => ToolDecision;
|
package/esm/agents/registry.js
CHANGED
|
@@ -64,9 +64,7 @@ const merge = (base, child) => ({
|
|
|
64
64
|
});
|
|
65
65
|
export const createAgentRegistry = (agents = []) => {
|
|
66
66
|
const raw = new Map();
|
|
67
|
-
|
|
68
|
-
if (!raw.has(agent.name))
|
|
69
|
-
raw.set(agent.name, agent);
|
|
67
|
+
const byName = new Map();
|
|
70
68
|
const resolve = (name, seen) => {
|
|
71
69
|
const agent = raw.get(name);
|
|
72
70
|
if (!agent.extends)
|
|
@@ -78,9 +76,17 @@ export const createAgentRegistry = (agents = []) => {
|
|
|
78
76
|
throw new Error(`AgentRegistry: "${name}" extends unknown agent "${agent.extends}"`);
|
|
79
77
|
return merge(resolve(agent.extends, new Set(seen).add(name)), agent);
|
|
80
78
|
};
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
79
|
+
// Rebuild both maps in place so existing holders of this registry see the new set.
|
|
80
|
+
const load = (list) => {
|
|
81
|
+
raw.clear();
|
|
82
|
+
for (const agent of list)
|
|
83
|
+
if (!raw.has(agent.name))
|
|
84
|
+
raw.set(agent.name, agent);
|
|
85
|
+
byName.clear();
|
|
86
|
+
for (const name of raw.keys())
|
|
87
|
+
byName.set(name, resolve(name, new Set()));
|
|
88
|
+
};
|
|
89
|
+
load(agents);
|
|
84
90
|
return {
|
|
85
91
|
list: () => [...byName.values()],
|
|
86
92
|
get: (name) => byName.get(name),
|
|
@@ -91,5 +97,6 @@ export const createAgentRegistry = (agents = []) => {
|
|
|
91
97
|
if (a.extends === agent.name)
|
|
92
98
|
byName.set(name, resolve(name, new Set()));
|
|
93
99
|
},
|
|
100
|
+
replaceAll: (list) => load(list),
|
|
94
101
|
};
|
|
95
102
|
};
|
package/esm/commands/index.d.ts
CHANGED
|
@@ -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';
|
package/esm/commands/index.js
CHANGED
|
@@ -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
|
+
});
|
package/esm/harness/create.js
CHANGED
|
@@ -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';
|
|
@@ -20,7 +20,7 @@ const TITLE_AGENT = {
|
|
|
20
20
|
tools: [],
|
|
21
21
|
};
|
|
22
22
|
export const createHarness = async (options) => {
|
|
23
|
-
const { hostName, xdg, providers, model, agents: hostAgents = [], skills: hostSkills = [], skillScope, agentScope, agentDirs, title, titleModel, scheduler: enableScheduler = false, approvals, ...sessionDefaults } = options;
|
|
23
|
+
const { hostName, xdg, providers, model, agents: hostAgents = [], defaultAgents = [], skills: hostSkills = [], skillScope, agentScope, agentDirs, title, titleModel, scheduler: enableScheduler = false, approvals, ...sessionDefaults } = options;
|
|
24
24
|
const cwd = options.cwd ?? process.cwd();
|
|
25
25
|
const config = createHarnessConfig({ hostName, xdg });
|
|
26
26
|
const models = createModelRegistry({ providers, default: model });
|
|
@@ -30,9 +30,15 @@ export const createHarness = async (options) => {
|
|
|
30
30
|
const plugins = createPluginStore({ dir: pluginsDir });
|
|
31
31
|
const newId = () => crypto.randomUUID();
|
|
32
32
|
const pluginAgents = (sessionDefaults.plugins ?? []).flatMap((plugin) => plugin.agents ?? []);
|
|
33
|
-
const
|
|
34
|
-
|
|
35
|
-
const
|
|
33
|
+
const loadDiskAgents = async () => [...await loadAgents(localAgentsDir), ...await loadAgents(agentsDir)];
|
|
34
|
+
// Priority (first wins): host > plugin > local dir > config dir > default fallback.
|
|
35
|
+
const mergedAgents = async () => [
|
|
36
|
+
...hostAgents,
|
|
37
|
+
...pluginAgents,
|
|
38
|
+
...await loadDiskAgents(),
|
|
39
|
+
...defaultAgents,
|
|
40
|
+
];
|
|
41
|
+
const agents = createAgentRegistry(await mergedAgents());
|
|
36
42
|
const skillsDir = join(config.configDir, 'skills');
|
|
37
43
|
const cwdSkillsDir = join(cwd, 'skills');
|
|
38
44
|
const envBlock = environmentBlock({
|
|
@@ -48,9 +54,13 @@ export const createHarness = async (options) => {
|
|
|
48
54
|
prepareRequest: ({ system }) => ({ system: system ? `${system}\n\n${envBlock}` : envBlock }),
|
|
49
55
|
};
|
|
50
56
|
const pluginSkills = (sessionDefaults.plugins ?? []).flatMap((plugin) => plugin.skills ?? []);
|
|
51
|
-
const
|
|
52
|
-
|
|
53
|
-
|
|
57
|
+
const mergedSkills = async () => [
|
|
58
|
+
...hostSkills,
|
|
59
|
+
...pluginSkills,
|
|
60
|
+
...await loadSkills(cwdSkillsDir),
|
|
61
|
+
...await loadSkills(skillsDir),
|
|
62
|
+
];
|
|
63
|
+
const skills = createSkillRegistry(await mergedSkills());
|
|
54
64
|
const skillWriterTool = createSkillWriterTool({
|
|
55
65
|
dirs: { local: cwdSkillsDir, config: skillsDir },
|
|
56
66
|
registry: skills,
|
|
@@ -163,6 +173,22 @@ export const createHarness = async (options) => {
|
|
|
163
173
|
...(tasks ? [createTasksCommand(tasks)] : []),
|
|
164
174
|
]);
|
|
165
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();
|
|
166
192
|
if (scheduler)
|
|
167
193
|
await scheduler.start();
|
|
168
194
|
return {
|
|
@@ -179,6 +205,11 @@ export const createHarness = async (options) => {
|
|
|
179
205
|
throw new Error(`unknown sub-agent "${agent}"`);
|
|
180
206
|
return await runSubAgent(def, task, { spawn, runs, parentId });
|
|
181
207
|
},
|
|
208
|
+
reloadDefinitions: async () => {
|
|
209
|
+
agents.replaceAll(await mergedAgents());
|
|
210
|
+
skills.replaceAll(await mergedSkills());
|
|
211
|
+
syncSkillCommands();
|
|
212
|
+
},
|
|
182
213
|
scheduler,
|
|
183
214
|
tasks,
|
|
184
215
|
commands,
|
package/esm/harness/types.d.ts
CHANGED
|
@@ -14,6 +14,11 @@ export type HarnessOptions = HarnessConfigOptions & Omit<AgentSessionConfig, 'pr
|
|
|
14
14
|
providers: Record<string, Provider>;
|
|
15
15
|
model: string;
|
|
16
16
|
agents?: Agent[];
|
|
17
|
+
/**
|
|
18
|
+
* Lowest-priority fallback agents (e.g. a host's built-in primary). Merged
|
|
19
|
+
* last, so disk- or host-defined agents of the same name override them.
|
|
20
|
+
*/
|
|
21
|
+
defaultAgents?: Agent[];
|
|
17
22
|
skills?: Skill[];
|
|
18
23
|
/**
|
|
19
24
|
* Forces the save location of `create_skill`. When set, the model's `scope`
|
|
@@ -57,6 +62,12 @@ export interface Harness {
|
|
|
57
62
|
readonly skills: SkillRegistry;
|
|
58
63
|
readonly subAgents: SubAgentRegistry;
|
|
59
64
|
dispatchSubAgent(agent: string, task: string, parentId: string): Promise<SubAgentResult>;
|
|
65
|
+
/**
|
|
66
|
+
* Re-read the on-disk agent and skill directories and refresh the registries
|
|
67
|
+
* in place (create/edit/delete), preserving host/plugin/default contributions.
|
|
68
|
+
* Lets a host hot-reload definitions without a restart.
|
|
69
|
+
*/
|
|
70
|
+
reloadDefinitions(): Promise<void>;
|
|
60
71
|
readonly commands: CommandRegistry;
|
|
61
72
|
readonly scheduler?: Scheduler;
|
|
62
73
|
readonly tasks?: TaskStore;
|
package/esm/skills/parser.js
CHANGED
package/esm/skills/registry.d.ts
CHANGED
|
@@ -3,6 +3,11 @@ export interface SkillRegistry {
|
|
|
3
3
|
list(): Skill[];
|
|
4
4
|
get(name: string): Skill | undefined;
|
|
5
5
|
add(skill: Skill): void;
|
|
6
|
+
/**
|
|
7
|
+
* Replace the entire set in place (rebuild) — used by hot-reload to reflect
|
|
8
|
+
* created, edited, and deleted skills. Existing references see the new set.
|
|
9
|
+
*/
|
|
10
|
+
replaceAll(skills: Skill[]): void;
|
|
6
11
|
select(names: string[]): SkillRegistry;
|
|
7
12
|
}
|
|
8
13
|
export declare const createSkillRegistry: (skills?: Skill[]) => SkillRegistry;
|
package/esm/skills/registry.js
CHANGED
|
@@ -1,8 +1,12 @@
|
|
|
1
1
|
export const createSkillRegistry = (skills = []) => {
|
|
2
2
|
const byName = new Map();
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
3
|
+
const load = (list) => {
|
|
4
|
+
byName.clear();
|
|
5
|
+
for (const skill of list)
|
|
6
|
+
if (!byName.has(skill.name))
|
|
7
|
+
byName.set(skill.name, skill);
|
|
8
|
+
};
|
|
9
|
+
load(skills);
|
|
6
10
|
const view = (allow) => ({
|
|
7
11
|
list: () => [...byName.values()].filter((skill) => !allow || allow.has(skill.name)),
|
|
8
12
|
get: (name) => (!allow || allow.has(name) ? byName.get(name) : undefined),
|
|
@@ -10,6 +14,7 @@ export const createSkillRegistry = (skills = []) => {
|
|
|
10
14
|
byName.set(skill.name, skill);
|
|
11
15
|
allow?.add(skill.name);
|
|
12
16
|
},
|
|
17
|
+
replaceAll: (list) => load(list),
|
|
13
18
|
select: (names) => view(new Set(allow ? names.filter((name) => allow.has(name)) : names)),
|
|
14
19
|
});
|
|
15
20
|
return view();
|
package/esm/skills/types.d.ts
CHANGED
|
@@ -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;
|
package/esm/tui/chat/ChatApp.js
CHANGED
|
@@ -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.
|
|
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.
|
|
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.
|
|
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.
|
|
27
|
-
"mu-tui": "^0.
|
|
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"
|
|
@@ -9,6 +9,11 @@ export interface AgentRegistry {
|
|
|
9
9
|
* re-resolved so they pick up the change.
|
|
10
10
|
*/
|
|
11
11
|
add(agent: Agent): void;
|
|
12
|
+
/**
|
|
13
|
+
* Replace the entire set in place (rebuild) — used by hot-reload to reflect
|
|
14
|
+
* created, edited, and deleted definitions. Existing references see the new set.
|
|
15
|
+
*/
|
|
16
|
+
replaceAll(agents: Agent[]): void;
|
|
12
17
|
}
|
|
13
18
|
export declare const grantArg: (tool: string, input: unknown) => string | undefined;
|
|
14
19
|
export declare const toolDecision: (agent: Agent, tool: string, arg?: string) => ToolDecision;
|
|
@@ -70,9 +70,7 @@ const merge = (base, child) => ({
|
|
|
70
70
|
});
|
|
71
71
|
const createAgentRegistry = (agents = []) => {
|
|
72
72
|
const raw = new Map();
|
|
73
|
-
|
|
74
|
-
if (!raw.has(agent.name))
|
|
75
|
-
raw.set(agent.name, agent);
|
|
73
|
+
const byName = new Map();
|
|
76
74
|
const resolve = (name, seen) => {
|
|
77
75
|
const agent = raw.get(name);
|
|
78
76
|
if (!agent.extends)
|
|
@@ -84,9 +82,17 @@ const createAgentRegistry = (agents = []) => {
|
|
|
84
82
|
throw new Error(`AgentRegistry: "${name}" extends unknown agent "${agent.extends}"`);
|
|
85
83
|
return merge(resolve(agent.extends, new Set(seen).add(name)), agent);
|
|
86
84
|
};
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
85
|
+
// Rebuild both maps in place so existing holders of this registry see the new set.
|
|
86
|
+
const load = (list) => {
|
|
87
|
+
raw.clear();
|
|
88
|
+
for (const agent of list)
|
|
89
|
+
if (!raw.has(agent.name))
|
|
90
|
+
raw.set(agent.name, agent);
|
|
91
|
+
byName.clear();
|
|
92
|
+
for (const name of raw.keys())
|
|
93
|
+
byName.set(name, resolve(name, new Set()));
|
|
94
|
+
};
|
|
95
|
+
load(agents);
|
|
90
96
|
return {
|
|
91
97
|
list: () => [...byName.values()],
|
|
92
98
|
get: (name) => byName.get(name),
|
|
@@ -97,6 +103,7 @@ const createAgentRegistry = (agents = []) => {
|
|
|
97
103
|
if (a.extends === agent.name)
|
|
98
104
|
byName.set(name, resolve(name, new Set()));
|
|
99
105
|
},
|
|
106
|
+
replaceAll: (list) => load(list),
|
|
100
107
|
};
|
|
101
108
|
};
|
|
102
109
|
exports.createAgentRegistry = createAgentRegistry;
|
|
@@ -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';
|
package/script/commands/index.js
CHANGED
|
@@ -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;
|
package/script/harness/create.js
CHANGED
|
@@ -26,7 +26,7 @@ const TITLE_AGENT = {
|
|
|
26
26
|
tools: [],
|
|
27
27
|
};
|
|
28
28
|
const createHarness = async (options) => {
|
|
29
|
-
const { hostName, xdg, providers, model, agents: hostAgents = [], skills: hostSkills = [], skillScope, agentScope, agentDirs, title, titleModel, scheduler: enableScheduler = false, approvals, ...sessionDefaults } = options;
|
|
29
|
+
const { hostName, xdg, providers, model, agents: hostAgents = [], defaultAgents = [], skills: hostSkills = [], skillScope, agentScope, agentDirs, title, titleModel, scheduler: enableScheduler = false, approvals, ...sessionDefaults } = options;
|
|
30
30
|
const cwd = options.cwd ?? node_process_1.default.cwd();
|
|
31
31
|
const config = (0, index_js_3.createHarnessConfig)({ hostName, xdg });
|
|
32
32
|
const models = (0, models_js_1.createModelRegistry)({ providers, default: model });
|
|
@@ -36,9 +36,15 @@ const createHarness = async (options) => {
|
|
|
36
36
|
const plugins = (0, index_js_6.createPluginStore)({ dir: pluginsDir });
|
|
37
37
|
const newId = () => crypto.randomUUID();
|
|
38
38
|
const pluginAgents = (sessionDefaults.plugins ?? []).flatMap((plugin) => plugin.agents ?? []);
|
|
39
|
-
const
|
|
40
|
-
|
|
41
|
-
const
|
|
39
|
+
const loadDiskAgents = async () => [...await (0, index_js_1.loadAgents)(localAgentsDir), ...await (0, index_js_1.loadAgents)(agentsDir)];
|
|
40
|
+
// Priority (first wins): host > plugin > local dir > config dir > default fallback.
|
|
41
|
+
const mergedAgents = async () => [
|
|
42
|
+
...hostAgents,
|
|
43
|
+
...pluginAgents,
|
|
44
|
+
...await loadDiskAgents(),
|
|
45
|
+
...defaultAgents,
|
|
46
|
+
];
|
|
47
|
+
const agents = (0, index_js_1.createAgentRegistry)(await mergedAgents());
|
|
42
48
|
const skillsDir = (0, node_path_1.join)(config.configDir, 'skills');
|
|
43
49
|
const cwdSkillsDir = (0, node_path_1.join)(cwd, 'skills');
|
|
44
50
|
const envBlock = (0, environment_js_1.environmentBlock)({
|
|
@@ -54,9 +60,13 @@ const createHarness = async (options) => {
|
|
|
54
60
|
prepareRequest: ({ system }) => ({ system: system ? `${system}\n\n${envBlock}` : envBlock }),
|
|
55
61
|
};
|
|
56
62
|
const pluginSkills = (sessionDefaults.plugins ?? []).flatMap((plugin) => plugin.skills ?? []);
|
|
57
|
-
const
|
|
58
|
-
|
|
59
|
-
|
|
63
|
+
const mergedSkills = async () => [
|
|
64
|
+
...hostSkills,
|
|
65
|
+
...pluginSkills,
|
|
66
|
+
...await (0, index_js_9.loadSkills)(cwdSkillsDir),
|
|
67
|
+
...await (0, index_js_9.loadSkills)(skillsDir),
|
|
68
|
+
];
|
|
69
|
+
const skills = (0, index_js_9.createSkillRegistry)(await mergedSkills());
|
|
60
70
|
const skillWriterTool = (0, index_js_9.createSkillWriterTool)({
|
|
61
71
|
dirs: { local: cwdSkillsDir, config: skillsDir },
|
|
62
72
|
registry: skills,
|
|
@@ -169,6 +179,22 @@ const createHarness = async (options) => {
|
|
|
169
179
|
...(tasks ? [(0, index_js_7.createTasksCommand)(tasks)] : []),
|
|
170
180
|
]);
|
|
171
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();
|
|
172
198
|
if (scheduler)
|
|
173
199
|
await scheduler.start();
|
|
174
200
|
return {
|
|
@@ -185,6 +211,11 @@ const createHarness = async (options) => {
|
|
|
185
211
|
throw new Error(`unknown sub-agent "${agent}"`);
|
|
186
212
|
return await (0, index_js_10.runSubAgent)(def, task, { spawn, runs, parentId });
|
|
187
213
|
},
|
|
214
|
+
reloadDefinitions: async () => {
|
|
215
|
+
agents.replaceAll(await mergedAgents());
|
|
216
|
+
skills.replaceAll(await mergedSkills());
|
|
217
|
+
syncSkillCommands();
|
|
218
|
+
},
|
|
188
219
|
scheduler,
|
|
189
220
|
tasks,
|
|
190
221
|
commands,
|
|
@@ -14,6 +14,11 @@ export type HarnessOptions = HarnessConfigOptions & Omit<AgentSessionConfig, 'pr
|
|
|
14
14
|
providers: Record<string, Provider>;
|
|
15
15
|
model: string;
|
|
16
16
|
agents?: Agent[];
|
|
17
|
+
/**
|
|
18
|
+
* Lowest-priority fallback agents (e.g. a host's built-in primary). Merged
|
|
19
|
+
* last, so disk- or host-defined agents of the same name override them.
|
|
20
|
+
*/
|
|
21
|
+
defaultAgents?: Agent[];
|
|
17
22
|
skills?: Skill[];
|
|
18
23
|
/**
|
|
19
24
|
* Forces the save location of `create_skill`. When set, the model's `scope`
|
|
@@ -57,6 +62,12 @@ export interface Harness {
|
|
|
57
62
|
readonly skills: SkillRegistry;
|
|
58
63
|
readonly subAgents: SubAgentRegistry;
|
|
59
64
|
dispatchSubAgent(agent: string, task: string, parentId: string): Promise<SubAgentResult>;
|
|
65
|
+
/**
|
|
66
|
+
* Re-read the on-disk agent and skill directories and refresh the registries
|
|
67
|
+
* in place (create/edit/delete), preserving host/plugin/default contributions.
|
|
68
|
+
* Lets a host hot-reload definitions without a restart.
|
|
69
|
+
*/
|
|
70
|
+
reloadDefinitions(): Promise<void>;
|
|
60
71
|
readonly commands: CommandRegistry;
|
|
61
72
|
readonly scheduler?: Scheduler;
|
|
62
73
|
readonly tasks?: TaskStore;
|
package/script/skills/parser.js
CHANGED
|
@@ -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;
|
|
@@ -3,6 +3,11 @@ export interface SkillRegistry {
|
|
|
3
3
|
list(): Skill[];
|
|
4
4
|
get(name: string): Skill | undefined;
|
|
5
5
|
add(skill: Skill): void;
|
|
6
|
+
/**
|
|
7
|
+
* Replace the entire set in place (rebuild) — used by hot-reload to reflect
|
|
8
|
+
* created, edited, and deleted skills. Existing references see the new set.
|
|
9
|
+
*/
|
|
10
|
+
replaceAll(skills: Skill[]): void;
|
|
6
11
|
select(names: string[]): SkillRegistry;
|
|
7
12
|
}
|
|
8
13
|
export declare const createSkillRegistry: (skills?: Skill[]) => SkillRegistry;
|
|
@@ -3,9 +3,13 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
3
3
|
exports.createSkillRegistry = void 0;
|
|
4
4
|
const createSkillRegistry = (skills = []) => {
|
|
5
5
|
const byName = new Map();
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
6
|
+
const load = (list) => {
|
|
7
|
+
byName.clear();
|
|
8
|
+
for (const skill of list)
|
|
9
|
+
if (!byName.has(skill.name))
|
|
10
|
+
byName.set(skill.name, skill);
|
|
11
|
+
};
|
|
12
|
+
load(skills);
|
|
9
13
|
const view = (allow) => ({
|
|
10
14
|
list: () => [...byName.values()].filter((skill) => !allow || allow.has(skill.name)),
|
|
11
15
|
get: (name) => (!allow || allow.has(name) ? byName.get(name) : undefined),
|
|
@@ -13,6 +17,7 @@ const createSkillRegistry = (skills = []) => {
|
|
|
13
17
|
byName.set(skill.name, skill);
|
|
14
18
|
allow?.add(skill.name);
|
|
15
19
|
},
|
|
20
|
+
replaceAll: (list) => load(list),
|
|
16
21
|
select: (names) => view(new Set(allow ? names.filter((name) => allow.has(name)) : names)),
|
|
17
22
|
});
|
|
18
23
|
return view();
|
package/script/skills/types.d.ts
CHANGED
|
@@ -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.
|
|
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.
|
|
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();
|