@robota-sdk/agent-command 3.0.0-beta.100
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/CHANGELOG.md +1351 -0
- package/LICENSE +661 -0
- package/README.md +100 -0
- package/dist/node/index.cjs +121 -0
- package/dist/node/index.d.cts +1053 -0
- package/dist/node/index.d.cts.map +1 -0
- package/dist/node/index.d.ts +1053 -0
- package/dist/node/index.d.ts.map +1 -0
- package/dist/node/index.js +122 -0
- package/dist/node/index.js.map +1 -0
- package/package.json +83 -0
- package/src/advisor/advisor-command-module.ts +47 -0
- package/src/advisor/advisor-command.test.ts +102 -0
- package/src/advisor/advisor-command.ts +55 -0
- package/src/advisor/advisor-cost-functional.test.ts +92 -0
- package/src/advisor/index.ts +6 -0
- package/src/agent/__tests__/agent-command.test.ts +597 -0
- package/src/agent/agent-command-module.ts +89 -0
- package/src/agent/agent-command-parser.ts +180 -0
- package/src/agent/agent-command.ts +255 -0
- package/src/agent/index.ts +7 -0
- package/src/background/__tests__/background-command-module.test.ts +261 -0
- package/src/background/background-command-module.ts +52 -0
- package/src/background/background-command.ts +65 -0
- package/src/background/index.ts +6 -0
- package/src/command-module-utils.test.ts +37 -0
- package/src/command-module-utils.ts +35 -0
- package/src/compact/__tests__/compact-command-module.test.ts +181 -0
- package/src/compact/compact-command-module.ts +59 -0
- package/src/compact/compact-command.ts +33 -0
- package/src/compact/index.ts +6 -0
- package/src/context/__tests__/context-command-module.test.ts +502 -0
- package/src/context/auto-compact-format.ts +25 -0
- package/src/context/context-breakdown.ts +295 -0
- package/src/context/context-command-module.ts +82 -0
- package/src/context/context-command.ts +291 -0
- package/src/context/index.ts +6 -0
- package/src/default/__tests__/__snapshots__/model-exposure.test.ts.snap +121 -0
- package/src/default/__tests__/default-command-modules.test.ts +192 -0
- package/src/default/__tests__/model-exposure.test.ts +269 -0
- package/src/default/__tests__/org-policy-forwarding.test.ts +106 -0
- package/src/default/__tests__/provider-environment-forwarding.test.ts +98 -0
- package/src/default/default-command-modules.ts +247 -0
- package/src/default/index.ts +5 -0
- package/src/devices/__tests__/devices-command-module.test.ts +299 -0
- package/src/devices/devices-command-module.ts +308 -0
- package/src/devices/devices-command-port.ts +143 -0
- package/src/devices/index.ts +14 -0
- package/src/doctor/__tests__/doctor-command-module.test.ts +145 -0
- package/src/doctor/__tests__/doctor-display-vocabulary.test.ts +38 -0
- package/src/doctor/__tests__/doctor-fixture.ts +146 -0
- package/src/doctor/__tests__/doctor-redaction.test.ts +29 -0
- package/src/doctor/__tests__/doctor-runner.test.ts +484 -0
- package/src/doctor/doctor-command-module.ts +132 -0
- package/src/doctor/doctor-extensions-probe.ts +323 -0
- package/src/doctor/doctor-node-deps.ts +88 -0
- package/src/doctor/doctor-redaction.ts +99 -0
- package/src/doctor/doctor-render.ts +70 -0
- package/src/doctor/doctor-repair.ts +136 -0
- package/src/doctor/doctor-runner.ts +122 -0
- package/src/doctor/doctor-settings-probe.ts +239 -0
- package/src/doctor/doctor-storage-probe.ts +187 -0
- package/src/doctor/doctor-types.ts +109 -0
- package/src/doctor/index.ts +32 -0
- package/src/editor/__tests__/editor-command-functional.test.ts +124 -0
- package/src/editor/editor-command-module.ts +53 -0
- package/src/editor/editor-command.ts +58 -0
- package/src/editor/index.ts +7 -0
- package/src/editor/resolve-editor.ts +21 -0
- package/src/effort/effort-command-module.ts +51 -0
- package/src/effort/effort-command.test.ts +203 -0
- package/src/effort/effort-command.ts +136 -0
- package/src/effort/index.ts +6 -0
- package/src/events/__tests__/events-command.test.ts +73 -0
- package/src/events/events-command-module.ts +49 -0
- package/src/events/events-command.ts +69 -0
- package/src/events/index.ts +2 -0
- package/src/exit/__tests__/exit-command-module.test.ts +61 -0
- package/src/exit/exit-command-module.ts +47 -0
- package/src/exit/exit-command.ts +26 -0
- package/src/exit/index.ts +6 -0
- package/src/fork/__tests__/fork-command.test.ts +184 -0
- package/src/fork/fork-command-module.ts +61 -0
- package/src/fork/fork-command.ts +162 -0
- package/src/fork/index.ts +8 -0
- package/src/git/__tests__/fake-git-port.ts +35 -0
- package/src/git/__tests__/git-command-module.test.ts +111 -0
- package/src/git/__tests__/git-commit.test.ts +185 -0
- package/src/git/git-command-module.ts +138 -0
- package/src/git/git-commit.ts +254 -0
- package/src/git/index.ts +20 -0
- package/src/goal/__tests__/goal-command.test.ts +80 -0
- package/src/goal/goal-command-module.ts +49 -0
- package/src/goal/goal-command.ts +69 -0
- package/src/goal/index.ts +6 -0
- package/src/handoff/__tests__/handoff-command-module.test.ts +25 -0
- package/src/handoff/__tests__/handoff-command.test.ts +218 -0
- package/src/handoff/handoff-command-module.ts +49 -0
- package/src/handoff/handoff-command.ts +187 -0
- package/src/handoff/index.ts +6 -0
- package/src/help/__tests__/help-command-module.test.ts +134 -0
- package/src/help/__tests__/help-command.test.ts +130 -0
- package/src/help/help-command-module.ts +47 -0
- package/src/help/help-command.ts +11 -0
- package/src/help/index.ts +6 -0
- package/src/index.ts +163 -0
- package/src/keybindings/__tests__/keybindings-command-module.test.ts +57 -0
- package/src/keybindings/index.ts +6 -0
- package/src/keybindings/keybindings-command-module.ts +88 -0
- package/src/language/__tests__/language-command-module.test.ts +126 -0
- package/src/language/index.ts +6 -0
- package/src/language/language-command-module.ts +55 -0
- package/src/language/language-command.ts +52 -0
- package/src/mcp-activation/__tests__/mcp-activation-command.test.ts +895 -0
- package/src/mcp-activation/__tests__/mcp-model-view.test.ts +242 -0
- package/src/mcp-activation/__tests__/mcp-skill-command.test.ts +141 -0
- package/src/mcp-activation/index.ts +12 -0
- package/src/mcp-activation/mcp-activation-command-module.ts +123 -0
- package/src/mcp-activation/mcp-activation-command.ts +662 -0
- package/src/mcp-activation/mcp-model-notice.ts +99 -0
- package/src/mcp-activation/mcp-model-status.ts +104 -0
- package/src/memory/__tests__/memory-command-module.test.ts +476 -0
- package/src/memory/__tests__/memory-command-unavailable.test.ts +63 -0
- package/src/memory/index.ts +6 -0
- package/src/memory/memory-command-module.ts +77 -0
- package/src/memory/memory-command.ts +326 -0
- package/src/mode/__tests__/mode-command-module.test.ts +230 -0
- package/src/mode/index.ts +6 -0
- package/src/mode/mode-command-module.ts +55 -0
- package/src/mode/mode-command.ts +67 -0
- package/src/model/__tests__/model-command-module.test.ts +80 -0
- package/src/model/__tests__/model-command.test.ts +265 -0
- package/src/model/index.ts +6 -0
- package/src/model/model-command-module.ts +56 -0
- package/src/model/model-command.ts +249 -0
- package/src/output-style/__tests__/output-style-command.test.ts +96 -0
- package/src/output-style/index.ts +6 -0
- package/src/output-style/output-style-command-module.ts +143 -0
- package/src/peers/__tests__/peers-command-module.test.ts +24 -0
- package/src/peers/__tests__/peers-command.test.ts +399 -0
- package/src/peers/index.ts +9 -0
- package/src/peers/peers-command-module.ts +49 -0
- package/src/peers/peers-command.ts +220 -0
- package/src/permissions/__tests__/permissions-command-module.test.ts +276 -0
- package/src/permissions/index.ts +6 -0
- package/src/permissions/permissions-command-module.ts +55 -0
- package/src/permissions/permissions-command.ts +89 -0
- package/src/plan/__tests__/plan-command.test.ts +129 -0
- package/src/plan/index.ts +9 -0
- package/src/plan/plan-command-module.ts +49 -0
- package/src/plan/plan-command.ts +92 -0
- package/src/plugin/__tests__/plugin-active-turn.test.ts +100 -0
- package/src/plugin/__tests__/plugin-command-module.test.ts +300 -0
- package/src/plugin/__tests__/plugin-host-boundary.test.ts +11 -0
- package/src/plugin/index.ts +7 -0
- package/src/plugin/plugin-command-module.ts +85 -0
- package/src/plugin/plugin-command.ts +271 -0
- package/src/preset/__tests__/host-supplied-preset-registry.test.ts +98 -0
- package/src/preset/__tests__/preset-command-module.test.ts +220 -0
- package/src/preset/index.ts +6 -0
- package/src/preset/preset-command-module.ts +80 -0
- package/src/preset/preset-command.ts +148 -0
- package/src/provider/__tests__/optional-provider-credentials.test.ts +130 -0
- package/src/provider/__tests__/org-policy.test.ts +174 -0
- package/src/provider/__tests__/provider-command-module.test.ts +884 -0
- package/src/provider/__tests__/provider-connection-setup.test.ts +567 -0
- package/src/provider/__tests__/provider-reference-switch.test.ts +113 -0
- package/src/provider/__tests__/provider-setup-flow.test.ts +244 -0
- package/src/provider/__tests__/provider-startup-host-settings.test.ts +16 -0
- package/src/provider/__tests__/scripted-interaction.ts +40 -0
- package/src/provider/index.ts +32 -0
- package/src/provider/provider-command-execution.ts +235 -0
- package/src/provider/provider-command-module.ts +82 -0
- package/src/provider/provider-command-profile-lifecycle.ts +239 -0
- package/src/provider/provider-command-profile-operations.ts +218 -0
- package/src/provider/provider-command-profile.ts +116 -0
- package/src/provider/provider-command-setup.ts +79 -0
- package/src/provider/provider-connection-setup.ts +287 -0
- package/src/provider/provider-connection-validation.ts +38 -0
- package/src/provider/provider-onboarding.ts +73 -0
- package/src/provider/provider-prompt-interaction.ts +28 -0
- package/src/provider/provider-setup-ask.ts +67 -0
- package/src/provider/provider-setup-flow.ts +326 -0
- package/src/provider/provider-startup.ts +225 -0
- package/src/remote-control/__tests__/remote-control-command-module.test.ts +36 -0
- package/src/remote-control/__tests__/remote-control-command.test.ts +216 -0
- package/src/remote-control/index.ts +8 -0
- package/src/remote-control/remote-control-command-module.ts +61 -0
- package/src/remote-control/remote-control-command.ts +138 -0
- package/src/reset/__tests__/reset-command-module.test.ts +63 -0
- package/src/reset/index.ts +2 -0
- package/src/reset/reset-command-module.ts +47 -0
- package/src/reset/reset-command.ts +10 -0
- package/src/rewind/__tests__/rewind-command-module.test.ts +287 -0
- package/src/rewind/index.ts +2 -0
- package/src/rewind/rewind-command-module.ts +56 -0
- package/src/rewind/rewind-command.ts +279 -0
- package/src/sandbox/__tests__/sandbox-command.test.ts +80 -0
- package/src/sandbox/index.ts +2 -0
- package/src/sandbox/sandbox-command-module.ts +56 -0
- package/src/sandbox/sandbox-command.ts +94 -0
- package/src/schedule/__tests__/loop-command-functional.test.ts +399 -0
- package/src/schedule/__tests__/loop-command.test.ts +594 -0
- package/src/schedule/__tests__/loop-jitter.test.ts +25 -0
- package/src/schedule/__tests__/monitor-model-permission-functional.test.ts +126 -0
- package/src/schedule/__tests__/schedule-command.test.ts +247 -0
- package/src/schedule/__tests__/schedule-redos.test.ts +114 -0
- package/src/schedule/index.ts +12 -0
- package/src/schedule/loop-command.ts +427 -0
- package/src/schedule/loop-jitter.ts +41 -0
- package/src/schedule/schedule-command-module.ts +184 -0
- package/src/schedule/schedule-command.ts +161 -0
- package/src/schedule/schedule-spec-parser.ts +70 -0
- package/src/session/__tests__/model-pricing.test.ts +66 -0
- package/src/session/__tests__/rename-host-persistence.test.ts +72 -0
- package/src/session/__tests__/session-command-module.test.ts +589 -0
- package/src/session/index.ts +17 -0
- package/src/session/model-pricing.ts +26 -0
- package/src/session/session-command-module.ts +197 -0
- package/src/session/session-command.ts +272 -0
- package/src/settings/index.ts +7 -0
- package/src/settings/settings-command-module.ts +47 -0
- package/src/shell/__tests__/resolve-shell-host.test.ts +11 -0
- package/src/shell/__tests__/shell-command-functional.test.ts +118 -0
- package/src/shell/index.ts +8 -0
- package/src/shell/resolve-shell.ts +25 -0
- package/src/shell/shell-command-module.ts +53 -0
- package/src/shell/shell-command.ts +48 -0
- package/src/shell/spawn-inherited.ts +32 -0
- package/src/skills/__tests__/skill-read-command.test.ts +64 -0
- package/src/skills/__tests__/skills-command-module.test.ts +200 -0
- package/src/skills/index.ts +7 -0
- package/src/skills/skill-read-command.ts +60 -0
- package/src/skills/skills-command-module.ts +76 -0
- package/src/skills/skills-command.ts +108 -0
- package/src/statusline/__tests__/statusline-command-module.test.ts +96 -0
- package/src/statusline/index.ts +6 -0
- package/src/statusline/statusline-command-module.ts +55 -0
- package/src/statusline/statusline-command.ts +80 -0
- package/src/terminal-client/__tests__/terminal-client-commands.test.ts +216 -0
- package/src/terminal-client/index.ts +5 -0
- package/src/terminal-client/terminal-client-commands.ts +109 -0
- package/src/theme/__tests__/theme-command.test.ts +204 -0
- package/src/theme/index.ts +12 -0
- package/src/theme/theme-command-module.ts +221 -0
- package/src/user-local/__tests__/user-local-command.test.ts +180 -0
- package/src/user-local/index.ts +13 -0
- package/src/user-local/user-local-command-constants.ts +5 -0
- package/src/user-local/user-local-command-module.ts +65 -0
- package/src/user-local/user-local-command.ts +214 -0
- package/src/user-local/user-local-memory-command.ts +160 -0
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* TERM-003: `/shell` command module — a framework-level consumer of the terminal-handoff capability.
|
|
3
|
+
*/
|
|
4
|
+
import { executeShellCommand, SHELL_COMMAND_DESCRIPTION } from './shell-command.js';
|
|
5
|
+
|
|
6
|
+
import type { ICommandModule, ISystemCommand } from '@robota-sdk/agent-framework';
|
|
7
|
+
import type { ICommand, ICommandSource } from '@robota-sdk/agent-interface-command';
|
|
8
|
+
|
|
9
|
+
export function createShellCommandEntry(): ICommand {
|
|
10
|
+
return {
|
|
11
|
+
name: 'shell',
|
|
12
|
+
displayName: 'Shell',
|
|
13
|
+
description: SHELL_COMMAND_DESCRIPTION,
|
|
14
|
+
source: 'shell',
|
|
15
|
+
// User-only: the user's own shell passthrough; the model has its permission-gated shell tool.
|
|
16
|
+
modelInvocable: false,
|
|
17
|
+
// It takes over the terminal the user sits at, so that terminal runs it, even when attached.
|
|
18
|
+
runner: 'client',
|
|
19
|
+
surfaces: ['terminal'],
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function createShellSystemCommand(executable?: string): ISystemCommand {
|
|
24
|
+
const entry = createShellCommandEntry();
|
|
25
|
+
return {
|
|
26
|
+
name: entry.name,
|
|
27
|
+
displayName: entry.displayName,
|
|
28
|
+
description: entry.description,
|
|
29
|
+
requiresPermission: true,
|
|
30
|
+
userInvocable: true,
|
|
31
|
+
modelInvocable: false,
|
|
32
|
+
runner: entry.runner,
|
|
33
|
+
surfaces: entry.surfaces,
|
|
34
|
+
lifecycle: 'inline',
|
|
35
|
+
execute: (context, args) => executeShellCommand(context, args, executable),
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export class ShellCommandSource implements ICommandSource {
|
|
40
|
+
readonly name = 'shell';
|
|
41
|
+
|
|
42
|
+
getCommands(): ICommand[] {
|
|
43
|
+
return [createShellCommandEntry()];
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export function createShellCommandModule(executable?: string): ICommandModule {
|
|
48
|
+
return {
|
|
49
|
+
name: 'agent-command-shell',
|
|
50
|
+
commandSources: [new ShellCommandSource()],
|
|
51
|
+
systemCommands: [createShellSystemCommand(executable)],
|
|
52
|
+
};
|
|
53
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* TERM-003: `/shell` — hand the real terminal to an interactive subshell (or run a single command
|
|
3
|
+
* interactively), then return to the agent session. Uses the framework terminal-handoff capability;
|
|
4
|
+
* the shell choice goes through the `resolveShell()` seam (macOS/Linux first).
|
|
5
|
+
*/
|
|
6
|
+
import { resolveShell } from './resolve-shell.js';
|
|
7
|
+
import { spawnInherited } from './spawn-inherited.js';
|
|
8
|
+
|
|
9
|
+
import type {
|
|
10
|
+
ICommandHostTerminalHandoff,
|
|
11
|
+
ICommandHostWorkspace,
|
|
12
|
+
} from '@robota-sdk/agent-framework';
|
|
13
|
+
import type { ICommandResult } from '@robota-sdk/agent-interface-command';
|
|
14
|
+
|
|
15
|
+
export const SHELL_COMMAND_DESCRIPTION =
|
|
16
|
+
'Drop to an interactive shell (or run `/shell <command>` interactively), then return to the agent.';
|
|
17
|
+
|
|
18
|
+
export async function executeShellCommand(
|
|
19
|
+
context: ICommandHostTerminalHandoff & ICommandHostWorkspace,
|
|
20
|
+
args: string,
|
|
21
|
+
executable?: string,
|
|
22
|
+
): Promise<ICommandResult> {
|
|
23
|
+
if (!context.canHandoffTerminal()) {
|
|
24
|
+
return {
|
|
25
|
+
message: 'An interactive shell is unavailable here (no interactive terminal).',
|
|
26
|
+
success: false,
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const shell = resolveShell(executable);
|
|
31
|
+
const cwd = context.getCwd();
|
|
32
|
+
const command = args.trim();
|
|
33
|
+
|
|
34
|
+
const exitCode = await context.runWithTerminal(async () =>
|
|
35
|
+
command.length > 0
|
|
36
|
+
? spawnInherited(shell.command, shell.commandArgs(command), cwd)
|
|
37
|
+
: spawnInherited(shell.command, shell.interactiveArgs, cwd),
|
|
38
|
+
);
|
|
39
|
+
|
|
40
|
+
return {
|
|
41
|
+
message:
|
|
42
|
+
command.length > 0
|
|
43
|
+
? `Command exited (code ${exitCode}).`
|
|
44
|
+
: `Shell session ended (code ${exitCode}).`,
|
|
45
|
+
success: exitCode === 0,
|
|
46
|
+
data: { exitCode },
|
|
47
|
+
};
|
|
48
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pure-Node spawn with the real terminal inherited (interactive input + output). Used inside a
|
|
3
|
+
* terminal-handoff `runWithTerminal(fn)` — the framework/transport own the suspend/restore; this just
|
|
4
|
+
* runs the child attached to the real TTY and resolves with its exit code.
|
|
5
|
+
*/
|
|
6
|
+
import { spawn } from 'node:child_process';
|
|
7
|
+
import { constants } from 'node:os';
|
|
8
|
+
|
|
9
|
+
export function spawnInherited(
|
|
10
|
+
command: string,
|
|
11
|
+
args: readonly string[],
|
|
12
|
+
cwd: string,
|
|
13
|
+
): Promise<number> {
|
|
14
|
+
return new Promise<number>((resolve, reject) => {
|
|
15
|
+
const child = spawn(command, [...args], { cwd, stdio: 'inherit', env: process.env });
|
|
16
|
+
child.on('error', reject);
|
|
17
|
+
child.on('exit', (code, signal) => {
|
|
18
|
+
// RUNTIME-53: a signal-terminated child reports code=null; resolving 0 there is a false
|
|
19
|
+
// success. Translate to the shell convention 128 + signal number so callers see the failure.
|
|
20
|
+
if (code !== null) {
|
|
21
|
+
resolve(code);
|
|
22
|
+
return;
|
|
23
|
+
}
|
|
24
|
+
if (signal) {
|
|
25
|
+
const signalNumber = constants.signals[signal as keyof typeof constants.signals];
|
|
26
|
+
resolve(128 + (signalNumber ?? 0));
|
|
27
|
+
return;
|
|
28
|
+
}
|
|
29
|
+
resolve(0);
|
|
30
|
+
});
|
|
31
|
+
});
|
|
32
|
+
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { expect, it, vi } from 'vitest';
|
|
2
|
+
import { SystemCommandExecutor } from '@robota-sdk/agent-framework';
|
|
3
|
+
import { createTestCommandHost } from '@robota-sdk/agent-framework/testing';
|
|
4
|
+
import { createSkillsCommandModule } from '../skills-command-module.js';
|
|
5
|
+
|
|
6
|
+
it('offers supporting reads to the model under the composed semantic command identity', async () => {
|
|
7
|
+
const module = createSkillsCommandModule({ contributionSources: [] });
|
|
8
|
+
const executor = new SystemCommandExecutor([...(module.systemCommands ?? [])]);
|
|
9
|
+
expect(executor.getSemanticRoles().skillResourceRead).toBe('skill-read');
|
|
10
|
+
expect(executor.isModelInvocable('skill-read')).toBe(true);
|
|
11
|
+
expect(
|
|
12
|
+
executor.listModelInvocableCommands().find((command) => command.name === 'skill-read')
|
|
13
|
+
?.description,
|
|
14
|
+
).toContain('already activated');
|
|
15
|
+
const resource = {
|
|
16
|
+
uri: 'skill://demo/reference%20file.txt',
|
|
17
|
+
digest: 'sha256:verified',
|
|
18
|
+
size: 7,
|
|
19
|
+
text: 'content',
|
|
20
|
+
};
|
|
21
|
+
const readSkillResource = vi.fn(async () => resource);
|
|
22
|
+
const context = createTestCommandHost({ overrides: { readSkillResource } });
|
|
23
|
+
const result = await executor.executeModelInvocable(
|
|
24
|
+
'skill-read',
|
|
25
|
+
context,
|
|
26
|
+
JSON.stringify(['demo', resource.uri]),
|
|
27
|
+
);
|
|
28
|
+
expect(readSkillResource).toHaveBeenCalledWith('demo', resource.uri);
|
|
29
|
+
expect(result).toMatchObject({ success: true, data: resource });
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
it.each(['', '{}', '["demo"]', '["demo", 3]', '["demo", "uri", "extra"]', '["", "uri"]'])(
|
|
33
|
+
'refuses malformed resource arguments without calling the host: %s',
|
|
34
|
+
async (args) => {
|
|
35
|
+
const executor = new SystemCommandExecutor([
|
|
36
|
+
...(createSkillsCommandModule({ contributionSources: [] }).systemCommands ?? []),
|
|
37
|
+
]);
|
|
38
|
+
const readSkillResource = vi.fn();
|
|
39
|
+
const result = await executor.executeModelInvocable(
|
|
40
|
+
'skill-read',
|
|
41
|
+
createTestCommandHost({ overrides: { readSkillResource } }),
|
|
42
|
+
args,
|
|
43
|
+
);
|
|
44
|
+
expect(result?.success).toBe(false);
|
|
45
|
+
expect(readSkillResource).not.toHaveBeenCalled();
|
|
46
|
+
},
|
|
47
|
+
);
|
|
48
|
+
|
|
49
|
+
it('reports an unavailable host instead of acquiring a skill or falling back to filesystem reads', async () => {
|
|
50
|
+
const executor = new SystemCommandExecutor([
|
|
51
|
+
...(createSkillsCommandModule({ contributionSources: [] }).systemCommands ?? []),
|
|
52
|
+
]);
|
|
53
|
+
const executeSkillCommandByName = vi.fn();
|
|
54
|
+
const result = await executor.executeModelInvocable(
|
|
55
|
+
'skill-read',
|
|
56
|
+
createTestCommandHost({ overrides: { executeSkillCommandByName } }),
|
|
57
|
+
'["demo", "skill://demo/file"]',
|
|
58
|
+
);
|
|
59
|
+
expect(result).toMatchObject({
|
|
60
|
+
success: false,
|
|
61
|
+
message: expect.stringContaining('does not provide'),
|
|
62
|
+
});
|
|
63
|
+
expect(executeSkillCommandByName).not.toHaveBeenCalled();
|
|
64
|
+
});
|
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
import { describe, expect, it, vi } from 'vitest';
|
|
2
|
+
import { mkdirSync, mkdtempSync, realpathSync, writeFileSync } from 'node:fs';
|
|
3
|
+
import { tmpdir } from 'node:os';
|
|
4
|
+
import { join } from 'node:path';
|
|
5
|
+
import {
|
|
6
|
+
createNodeHostContributionSource,
|
|
7
|
+
InteractiveSession,
|
|
8
|
+
SystemCommandExecutor,
|
|
9
|
+
WorkspaceTrustService,
|
|
10
|
+
} from '@robota-sdk/agent-framework';
|
|
11
|
+
import { createSkillsCommandModule } from '../skills-command-module.js';
|
|
12
|
+
import {
|
|
13
|
+
createTestCommandHost,
|
|
14
|
+
type ICreateTestCommandHostOptions,
|
|
15
|
+
} from '@robota-sdk/agent-framework/testing';
|
|
16
|
+
|
|
17
|
+
import type { IWorkspaceIdentity, IWorkspaceTrustStore } from '@robota-sdk/agent-framework';
|
|
18
|
+
|
|
19
|
+
async function createTrustedProjectAccess(cwd: string) {
|
|
20
|
+
const root = realpathSync(cwd);
|
|
21
|
+
const identity: IWorkspaceIdentity = {
|
|
22
|
+
repositoryKey: `skills-command-test:${root}`,
|
|
23
|
+
displayPath: root,
|
|
24
|
+
worktreeRoot: root,
|
|
25
|
+
};
|
|
26
|
+
const store: IWorkspaceTrustStore = {
|
|
27
|
+
inspect: () => Promise.resolve({ state: 'trusted', generation: 1 }),
|
|
28
|
+
grant: () => Promise.resolve({ state: 'trusted', generation: 1 }),
|
|
29
|
+
revoke: () => Promise.resolve({ state: 'revoked', generation: 2 }),
|
|
30
|
+
};
|
|
31
|
+
return new WorkspaceTrustService({
|
|
32
|
+
identityResolver: { resolve: () => identity },
|
|
33
|
+
store,
|
|
34
|
+
}).inspect(root);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function createTempSkill(cwd: string): void {
|
|
38
|
+
const skillDir = join(cwd, '.agents', 'skills', 'audit');
|
|
39
|
+
mkdirSync(skillDir, { recursive: true });
|
|
40
|
+
writeFileSync(
|
|
41
|
+
join(skillDir, 'SKILL.md'),
|
|
42
|
+
['---', 'name: audit', 'description: Audit code', '---', 'Audit $ARGUMENTS'].join('\n'),
|
|
43
|
+
'utf8',
|
|
44
|
+
);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function makeParentSession() {
|
|
48
|
+
return {
|
|
49
|
+
run: vi.fn().mockResolvedValue('parent response'),
|
|
50
|
+
abort: vi.fn(),
|
|
51
|
+
assertInputAllowed: vi.fn(),
|
|
52
|
+
getHistory: vi.fn().mockReturnValue([]),
|
|
53
|
+
getContextState: vi.fn().mockReturnValue({
|
|
54
|
+
usedPercentage: 10,
|
|
55
|
+
usedTokens: 100,
|
|
56
|
+
maxTokens: 1000,
|
|
57
|
+
}),
|
|
58
|
+
injectMessage: vi.fn(),
|
|
59
|
+
getSessionId: vi.fn().mockReturnValue('parent-session-id'),
|
|
60
|
+
// SELFHOST-004 P6: the span collector subscribes to the session bus each turn.
|
|
61
|
+
getEventService: vi.fn().mockReturnValue({ subscribe: vi.fn(), unsubscribe: vi.fn() }),
|
|
62
|
+
getSystemMessage: vi.fn().mockReturnValue('# system'),
|
|
63
|
+
getToolSchemas: vi.fn().mockReturnValue([]),
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function createMockContext(overrides?: ICreateTestCommandHostOptions['overrides']) {
|
|
68
|
+
// ARCH-029: overrides over a conformant host rather than a 20-member literal asserting it IS one.
|
|
69
|
+
return createTestCommandHost({
|
|
70
|
+
overrides: {
|
|
71
|
+
getSession: vi.fn(),
|
|
72
|
+
getContextState: vi.fn(),
|
|
73
|
+
getAutoCompactThreshold: vi.fn().mockReturnValue(0.835),
|
|
74
|
+
compactContext: vi.fn(),
|
|
75
|
+
getCwd: vi.fn().mockReturnValue('/workspace'),
|
|
76
|
+
listEditCheckpoints: vi.fn().mockReturnValue([]),
|
|
77
|
+
restoreEditCheckpoint: vi.fn(),
|
|
78
|
+
rollbackEditCheckpoint: vi.fn(),
|
|
79
|
+
getUsedMemoryReferences: vi.fn().mockReturnValue([]),
|
|
80
|
+
recordMemoryEvent: vi.fn(),
|
|
81
|
+
listBackgroundTasks: vi.fn().mockReturnValue([]),
|
|
82
|
+
readBackgroundTaskLog: vi.fn(),
|
|
83
|
+
cancelBackgroundTask: vi.fn(),
|
|
84
|
+
closeBackgroundTask: vi.fn(),
|
|
85
|
+
...overrides,
|
|
86
|
+
},
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
describe('createSkillsCommandModule', () => {
|
|
91
|
+
it('exposes skills as a normal model-invocable command module', () => {
|
|
92
|
+
const module = createSkillsCommandModule({ contributionSources: [] });
|
|
93
|
+
const command = module.systemCommands?.[0];
|
|
94
|
+
|
|
95
|
+
expect(module.name).toBe('agent-command-skills');
|
|
96
|
+
expect(module.commandSources?.[0]?.getCommands().map((entry) => entry.name)).toEqual([
|
|
97
|
+
'skills',
|
|
98
|
+
'skill-read',
|
|
99
|
+
]);
|
|
100
|
+
expect(command).toMatchObject({
|
|
101
|
+
name: 'skills',
|
|
102
|
+
semanticRole: 'skillActivation',
|
|
103
|
+
userInvocable: true,
|
|
104
|
+
modelInvocable: true,
|
|
105
|
+
lifecycle: 'inline',
|
|
106
|
+
argumentHint: '[list | <skill-name> [args]]',
|
|
107
|
+
safety: 'read-only',
|
|
108
|
+
});
|
|
109
|
+
expect(command?.modelDescription).toContain('projected skills command tool');
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
it('lists skill metadata from the SDK host context', async () => {
|
|
113
|
+
const executor = new SystemCommandExecutor([
|
|
114
|
+
...(createSkillsCommandModule({ contributionSources: [] }).systemCommands ?? []),
|
|
115
|
+
]);
|
|
116
|
+
|
|
117
|
+
const result = await executor.execute(
|
|
118
|
+
'skills',
|
|
119
|
+
createMockContext({
|
|
120
|
+
listSkills: vi.fn().mockReturnValue([
|
|
121
|
+
{
|
|
122
|
+
name: 'repo-writing',
|
|
123
|
+
description: 'Repository writing rules',
|
|
124
|
+
source: 'skill',
|
|
125
|
+
modelInvocable: true,
|
|
126
|
+
userInvocable: true,
|
|
127
|
+
},
|
|
128
|
+
]),
|
|
129
|
+
}),
|
|
130
|
+
'',
|
|
131
|
+
);
|
|
132
|
+
|
|
133
|
+
expect(result?.message).toContain('repo-writing: Repository writing rules');
|
|
134
|
+
expect(result?.message).toContain('Use /skills <skill-name> [args]');
|
|
135
|
+
expect(result?.data?.['activationContract']).toMatchObject({
|
|
136
|
+
activateWith: '/skills <skill-name> [args]',
|
|
137
|
+
activationRequiredBeforeWorkflow: true,
|
|
138
|
+
});
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
it('activates skills through the SDK host skill activation API', async () => {
|
|
142
|
+
const executeSkillCommandByName = vi.fn().mockResolvedValue({
|
|
143
|
+
success: true,
|
|
144
|
+
message: 'Skill activated: repo-writing',
|
|
145
|
+
data: { skill: 'repo-writing' },
|
|
146
|
+
});
|
|
147
|
+
const executor = new SystemCommandExecutor([
|
|
148
|
+
...(createSkillsCommandModule({ contributionSources: [] }).systemCommands ?? []),
|
|
149
|
+
]);
|
|
150
|
+
|
|
151
|
+
const result = await executor.execute(
|
|
152
|
+
'skills',
|
|
153
|
+
createMockContext({
|
|
154
|
+
getCommandInvocationSource: vi.fn().mockReturnValue('model'),
|
|
155
|
+
executeSkillCommandByName,
|
|
156
|
+
}),
|
|
157
|
+
'repo-writing update docs',
|
|
158
|
+
);
|
|
159
|
+
|
|
160
|
+
expect(executeSkillCommandByName).toHaveBeenCalledWith('repo-writing', 'update docs', {
|
|
161
|
+
invocationSource: 'model',
|
|
162
|
+
displayInput: '/repo-writing update docs',
|
|
163
|
+
rawInput: '/repo-writing update docs',
|
|
164
|
+
});
|
|
165
|
+
expect(result?.message).toBe('Skill activated: repo-writing');
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
it('lets the SDK normalize virtual /skill-name commands into the composed skills command', async () => {
|
|
169
|
+
const cwd = realpathSync(mkdtempSync(join(tmpdir(), 'agent-test-skills-command-module-')));
|
|
170
|
+
createTempSkill(cwd);
|
|
171
|
+
const parentSession = makeParentSession();
|
|
172
|
+
const session = new InteractiveSession({
|
|
173
|
+
session: parentSession as never,
|
|
174
|
+
cwd,
|
|
175
|
+
projectAccess: await createTrustedProjectAccess(cwd),
|
|
176
|
+
contributionSources: [createNodeHostContributionSource(cwd)],
|
|
177
|
+
skillRoots: [{ root: join('.agents', 'skills'), kind: 'skills' }],
|
|
178
|
+
commandModules: [
|
|
179
|
+
createSkillsCommandModule({
|
|
180
|
+
contributionSources: [createNodeHostContributionSource(cwd)],
|
|
181
|
+
skillRoots: [{ root: join('.agents', 'skills'), kind: 'skills' }],
|
|
182
|
+
}),
|
|
183
|
+
],
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
const result = await session.executeCommand('audit', 'src/index.ts');
|
|
187
|
+
|
|
188
|
+
expect(result).toMatchObject({
|
|
189
|
+
success: true,
|
|
190
|
+
data: expect.objectContaining({
|
|
191
|
+
skill: 'audit',
|
|
192
|
+
sessionExecution: true,
|
|
193
|
+
}),
|
|
194
|
+
});
|
|
195
|
+
expect(parentSession.run).toHaveBeenCalledWith(
|
|
196
|
+
expect.stringContaining('Audit src/index.ts'),
|
|
197
|
+
'/audit src/index.ts',
|
|
198
|
+
);
|
|
199
|
+
});
|
|
200
|
+
});
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
export {
|
|
2
|
+
createSkillsCommandEntry,
|
|
3
|
+
createSkillsCommandModule,
|
|
4
|
+
SkillsCommandSource,
|
|
5
|
+
} from './skills-command-module.js';
|
|
6
|
+
export type { ISkillsCommandModuleOptions } from './skills-command-module.js';
|
|
7
|
+
export { executeSkillsCommand, SKILLS_COMMAND_DESCRIPTION } from './skills-command.js';
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import type { ICommand, ICommandResult } from '@robota-sdk/agent-interface-command';
|
|
2
|
+
import type { ICommandHostCatalog, ISystemCommand } from '@robota-sdk/agent-framework';
|
|
3
|
+
|
|
4
|
+
export function createSkillReadCommandEntry(): ICommand {
|
|
5
|
+
return {
|
|
6
|
+
name: 'skill-read',
|
|
7
|
+
description: 'Read a supporting file from a skill active in this execution',
|
|
8
|
+
modelDescription:
|
|
9
|
+
'Read verified supporting bytes from a skill already activated in this execution. Use when its instructions refer to a resource in the returned manifest. Args are a JSON array ["<skill-name>", "<resource-uri>"]. Returns the verified URI, digest, size and text or base64 blob with optional MIME type. This does not activate nested skills or grant consent; unactivated skills, undeclared resources and ended executions are refused. Binary blobs are returned as encoded data, not a rendered image.',
|
|
10
|
+
argumentHint: '["<skill-name>", "<resource-uri>"]',
|
|
11
|
+
source: 'skills',
|
|
12
|
+
userInvocable: true,
|
|
13
|
+
modelInvocable: true,
|
|
14
|
+
safety: 'read-only',
|
|
15
|
+
};
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export async function executeSkillReadCommand(
|
|
19
|
+
context: Pick<ICommandHostCatalog, 'readSkillResource'>,
|
|
20
|
+
args = '',
|
|
21
|
+
): Promise<ICommandResult> {
|
|
22
|
+
let input: unknown;
|
|
23
|
+
try {
|
|
24
|
+
input = JSON.parse(args);
|
|
25
|
+
} catch {
|
|
26
|
+
return { success: false, message: 'Use /skill-read ["<skill-name>", "<resource-uri>"]' };
|
|
27
|
+
}
|
|
28
|
+
if (
|
|
29
|
+
!Array.isArray(input) ||
|
|
30
|
+
input.length !== 2 ||
|
|
31
|
+
input.some((value) => typeof value !== 'string' || !value.length)
|
|
32
|
+
)
|
|
33
|
+
return { success: false, message: 'Use /skill-read ["<skill-name>", "<resource-uri>"]' };
|
|
34
|
+
if (!context.readSkillResource)
|
|
35
|
+
return { success: false, message: 'This host does not provide active skill resource reads.' };
|
|
36
|
+
try {
|
|
37
|
+
const resource = await context.readSkillResource(input[0], input[1]);
|
|
38
|
+
return {
|
|
39
|
+
success: true,
|
|
40
|
+
message: `Read verified skill resource: ${resource.uri}`,
|
|
41
|
+
data: { ...resource },
|
|
42
|
+
};
|
|
43
|
+
} catch (error) {
|
|
44
|
+
return {
|
|
45
|
+
success: false,
|
|
46
|
+
message: `Skill resource read refused: ${error instanceof Error ? error.message : 'host refused the read'}`,
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export function createSkillReadSystemCommand(): ISystemCommand {
|
|
52
|
+
const entry = createSkillReadCommandEntry();
|
|
53
|
+
return {
|
|
54
|
+
...entry,
|
|
55
|
+
semanticRole: 'skillResourceRead',
|
|
56
|
+
requiresPermission: false,
|
|
57
|
+
lifecycle: 'inline',
|
|
58
|
+
execute: executeSkillReadCommand,
|
|
59
|
+
};
|
|
60
|
+
}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import { SkillCommandSource } from '@robota-sdk/agent-framework';
|
|
2
|
+
import { createSkillReadCommandEntry, createSkillReadSystemCommand } from './skill-read-command.js';
|
|
3
|
+
|
|
4
|
+
import {
|
|
5
|
+
executeSkillsCommand,
|
|
6
|
+
SKILLS_COMMAND_DESCRIPTION,
|
|
7
|
+
SKILLS_COMMAND_MODEL_DESCRIPTION,
|
|
8
|
+
} from './skills-command.js';
|
|
9
|
+
|
|
10
|
+
import type {
|
|
11
|
+
ICommandModule,
|
|
12
|
+
IContributionSource,
|
|
13
|
+
ISystemCommand,
|
|
14
|
+
ISkillRootDescriptor,
|
|
15
|
+
} from '@robota-sdk/agent-framework';
|
|
16
|
+
import type { ICommand, ICommandSource } from '@robota-sdk/agent-interface-command';
|
|
17
|
+
|
|
18
|
+
export interface ISkillsCommandModuleOptions {
|
|
19
|
+
readonly contributionSources: readonly IContributionSource[];
|
|
20
|
+
readonly skillRoots?: readonly ISkillRootDescriptor[];
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function createSkillsCommandEntry(): ICommand {
|
|
24
|
+
return {
|
|
25
|
+
name: 'skills',
|
|
26
|
+
displayName: 'Skills',
|
|
27
|
+
description: SKILLS_COMMAND_DESCRIPTION,
|
|
28
|
+
// Model-invocable: activating a skill is how the model follows one; a skill's own
|
|
29
|
+
// `disable-model-invocation` frontmatter still refuses it per skill.
|
|
30
|
+
modelDescription: SKILLS_COMMAND_MODEL_DESCRIPTION,
|
|
31
|
+
source: 'skills',
|
|
32
|
+
modelInvocable: true,
|
|
33
|
+
userInvocable: true,
|
|
34
|
+
argumentHint: '[list | <skill-name> [args]]',
|
|
35
|
+
safety: 'read-only',
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function createSkillsSystemCommand(): ISystemCommand {
|
|
40
|
+
const entry = createSkillsCommandEntry();
|
|
41
|
+
return {
|
|
42
|
+
name: entry.name,
|
|
43
|
+
semanticRole: 'skillActivation',
|
|
44
|
+
...(entry.modelDescription !== undefined ? { modelDescription: entry.modelDescription } : {}),
|
|
45
|
+
displayName: entry.displayName,
|
|
46
|
+
description: entry.description,
|
|
47
|
+
requiresPermission: false,
|
|
48
|
+
userInvocable: true,
|
|
49
|
+
modelInvocable: true,
|
|
50
|
+
argumentHint: entry.argumentHint,
|
|
51
|
+
safety: entry.safety,
|
|
52
|
+
lifecycle: 'inline',
|
|
53
|
+
execute: executeSkillsCommand,
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export class SkillsCommandSource implements ICommandSource {
|
|
58
|
+
readonly name = 'skills';
|
|
59
|
+
|
|
60
|
+
getCommands(): ICommand[] {
|
|
61
|
+
return [createSkillsCommandEntry(), createSkillReadCommandEntry()];
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export function createSkillsCommandModule(options: ISkillsCommandModuleOptions): ICommandModule {
|
|
66
|
+
const commandSources: ICommandSource[] = [new SkillsCommandSource()];
|
|
67
|
+
commandSources.push(
|
|
68
|
+
new SkillCommandSource(options.contributionSources, options.skillRoots ?? []),
|
|
69
|
+
);
|
|
70
|
+
|
|
71
|
+
return {
|
|
72
|
+
name: 'agent-command-skills',
|
|
73
|
+
commandSources,
|
|
74
|
+
systemCommands: [createSkillsSystemCommand(), createSkillReadSystemCommand()],
|
|
75
|
+
};
|
|
76
|
+
}
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
ICommandHostCatalog,
|
|
3
|
+
ICommandHostWorkspace,
|
|
4
|
+
ICommandSkillListEntry,
|
|
5
|
+
} from '@robota-sdk/agent-framework';
|
|
6
|
+
import type { ICommandResult } from '@robota-sdk/agent-interface-command';
|
|
7
|
+
|
|
8
|
+
export const SKILLS_COMMAND_DESCRIPTION = 'List registered skills, or activate one by name';
|
|
9
|
+
|
|
10
|
+
/** What the model is told about `skills`; `SKILLS_COMMAND_DESCRIPTION` is the `/help` line. */
|
|
11
|
+
export const SKILLS_COMMAND_MODEL_DESCRIPTION =
|
|
12
|
+
'Skill command. Before following a matching registered skill from the system prompt Skills section, invoke the projected skills command tool with args "<skill-name> [args]". Without arguments, list registered skills. With a skill name, activate that skill; a skill that declares it is not model-invocable is refused. Returns the skill list with each skill\'s description, or the activated skill\'s instructions to follow. Slash syntax is a UI input/display concern; the SDK command identity is "skills".';
|
|
13
|
+
|
|
14
|
+
interface IParsedSkillsArgs {
|
|
15
|
+
readonly action: 'list' | 'activate';
|
|
16
|
+
readonly skillName?: string;
|
|
17
|
+
readonly skillArgs: string;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function formatSkillFlags(skill: ICommandSkillListEntry): string {
|
|
21
|
+
const flags: string[] = [];
|
|
22
|
+
if (!skill.modelInvocable) flags.push('model-disabled');
|
|
23
|
+
if (!skill.userInvocable) flags.push('model-only');
|
|
24
|
+
if (skill.context) flags.push(`context:${skill.context}`);
|
|
25
|
+
if (skill.agent) flags.push(`agent:${skill.agent}`);
|
|
26
|
+
return flags.length > 0 ? ` [${flags.join(', ')}]` : '';
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function formatSkillLine(skill: ICommandSkillListEntry): string {
|
|
30
|
+
const hint = skill.argumentHint ? ` ${skill.argumentHint}` : '';
|
|
31
|
+
return `- ${skill.name}${hint}: ${skill.description}${formatSkillFlags(skill)}`;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function formatSkillsMessage(skills: readonly ICommandSkillListEntry[]): string {
|
|
35
|
+
if (skills.length === 0) {
|
|
36
|
+
return [
|
|
37
|
+
'No skills are registered for this session.',
|
|
38
|
+
'',
|
|
39
|
+
'Skills are metadata until activated. Do not invent or imitate a skill workflow when no matching registered skill exists.',
|
|
40
|
+
].join('\n');
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
return [
|
|
44
|
+
'Registered skills:',
|
|
45
|
+
...skills.map(formatSkillLine),
|
|
46
|
+
'',
|
|
47
|
+
'Activation contract:',
|
|
48
|
+
'- Use /skills <skill-name> [args] to activate a matching skill.',
|
|
49
|
+
'- Treat /<skill-name> as a virtual alias for /skills <skill-name>.',
|
|
50
|
+
'- The system prompt Skills section is skill selection metadata.',
|
|
51
|
+
'- Treat descriptions as selection metadata only, not as loaded SKILL.md content.',
|
|
52
|
+
'- Do not answer by merely naming, recommending, or imitating a matching skill.',
|
|
53
|
+
'- If no listed skill matches the task, continue without claiming a skill was activated.',
|
|
54
|
+
].join('\n');
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function parseSkillsArgs(args: string): IParsedSkillsArgs {
|
|
58
|
+
const trimmed = args.trim();
|
|
59
|
+
if (trimmed.length === 0 || trimmed === 'list') {
|
|
60
|
+
return { action: 'list', skillArgs: '' };
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const [skillName = '', ...rest] = trimmed.split(/\s+/);
|
|
64
|
+
if (skillName.length === 0) {
|
|
65
|
+
return { action: 'list', skillArgs: '' };
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
return {
|
|
69
|
+
action: 'activate',
|
|
70
|
+
skillName,
|
|
71
|
+
skillArgs: rest.join(' '),
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export async function executeSkillsCommand(
|
|
76
|
+
context: ICommandHostCatalog & ICommandHostWorkspace,
|
|
77
|
+
args = '',
|
|
78
|
+
): Promise<ICommandResult> {
|
|
79
|
+
const parsed = parseSkillsArgs(args);
|
|
80
|
+
if (parsed.action === 'activate' && parsed.skillName !== undefined) {
|
|
81
|
+
const displayInput = `/${parsed.skillName}${parsed.skillArgs ? ` ${parsed.skillArgs}` : ''}`;
|
|
82
|
+
const result = await context.executeSkillCommandByName(parsed.skillName, parsed.skillArgs, {
|
|
83
|
+
invocationSource: context.getCommandInvocationSource(),
|
|
84
|
+
displayInput,
|
|
85
|
+
rawInput: displayInput,
|
|
86
|
+
});
|
|
87
|
+
return (
|
|
88
|
+
result ?? {
|
|
89
|
+
success: false,
|
|
90
|
+
message: `Unknown skill: ${parsed.skillName}`,
|
|
91
|
+
}
|
|
92
|
+
);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
const skills = context.listSkills();
|
|
96
|
+
return {
|
|
97
|
+
success: true,
|
|
98
|
+
message: formatSkillsMessage(skills),
|
|
99
|
+
data: {
|
|
100
|
+
skills,
|
|
101
|
+
activationContract: {
|
|
102
|
+
activateWith: '/skills <skill-name> [args]',
|
|
103
|
+
activationRequiredBeforeWorkflow: true,
|
|
104
|
+
metadataIsNotSkillContent: true,
|
|
105
|
+
},
|
|
106
|
+
},
|
|
107
|
+
};
|
|
108
|
+
}
|