@robota-sdk/agent-command 3.0.0-beta.74 → 3.0.0-beta.75

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@robota-sdk/agent-command",
3
- "version": "3.0.0-beta.74",
3
+ "version": "3.0.0-beta.75",
4
4
  "description": "Consolidated command module implementations for Robota SDK CLI",
5
5
  "type": "module",
6
6
  "main": "dist/node/index.js",
@@ -24,9 +24,10 @@
24
24
  "src"
25
25
  ],
26
26
  "dependencies": {
27
- "@robota-sdk/agent-core": "3.0.0-beta.74",
28
- "@robota-sdk/agent-framework": "3.0.0-beta.74",
29
- "@robota-sdk/agent-interface-transport": "3.0.0-beta.74"
27
+ "@robota-sdk/agent-core": "3.0.0-beta.75",
28
+ "@robota-sdk/agent-framework": "3.0.0-beta.75",
29
+ "@robota-sdk/agent-interface-transport": "3.0.0-beta.75",
30
+ "@robota-sdk/agent-preset": "3.0.0-beta.75"
30
31
  },
31
32
  "devDependencies": {
32
33
  "@types/node": "^22.19.21",
@@ -0,0 +1,97 @@
1
+ import { describe, expect, it } from 'vitest';
2
+
3
+ import type { IProviderDefinition } from '@robota-sdk/agent-core';
4
+ import type {
5
+ IProviderCommandSettingsAdapter,
6
+ TProviderSettingsDocument,
7
+ } from '@robota-sdk/agent-framework';
8
+
9
+ import { createDefaultCommandModules } from '../default-command-modules.js';
10
+
11
+ const providerDefinitions: readonly IProviderDefinition[] = [
12
+ {
13
+ type: 'anthropic',
14
+ defaults: { model: 'claude-sonnet-4-6', apiKey: '$ENV:ANTHROPIC_API_KEY' },
15
+ setupSteps: [{ key: 'apiKey', title: 'anthropic API key', masked: true }],
16
+ requiresApiKey: true,
17
+ createProvider: () => {
18
+ throw new Error('not used');
19
+ },
20
+ },
21
+ ];
22
+
23
+ const providerSettingsAdapter: IProviderCommandSettingsAdapter = {
24
+ readMergedSettings: () => ({}) as TProviderSettingsDocument,
25
+ readTargetSettings: () => ({}) as TProviderSettingsDocument,
26
+ writeTargetSettings: () => undefined,
27
+ };
28
+
29
+ const baseOptions = { cwd: '/tmp', providerDefinitions, providerSettingsAdapter } as const;
30
+
31
+ function moduleNames(opts: Parameters<typeof createDefaultCommandModules>[0]): string[] {
32
+ return createDefaultCommandModules(opts).map((module) => module.name);
33
+ }
34
+
35
+ describe('createDefaultCommandModules — PRESET-004 module-selection delta', () => {
36
+ // Module `name`s are the stable ICommandModule ids (the `agent-command-*` form).
37
+ const HELP = 'agent-command-help';
38
+ const AGENT = 'agent-command-agent';
39
+ const BACKGROUND = 'agent-command-background';
40
+
41
+ it('TC-04: neither enabled nor disabled given → full default set unchanged (no-regression)', () => {
42
+ const names = moduleNames(baseOptions);
43
+ // No-regression: the default set length is the documented 21 modules.
44
+ expect(names).toHaveLength(21);
45
+ expect(names).toEqual([
46
+ 'agent-command-skills',
47
+ 'agent-command-help',
48
+ 'agent-command-agent',
49
+ 'agent-command-permissions',
50
+ 'agent-command-mode',
51
+ 'agent-command-preset',
52
+ 'agent-command-language',
53
+ 'agent-command-background',
54
+ 'agent-command-memory',
55
+ 'agent-command-user-local',
56
+ 'agent-command-compact',
57
+ 'agent-command-context',
58
+ 'agent-command-exit',
59
+ 'agent-command-session',
60
+ 'agent-command-reset',
61
+ 'agent-command-rewind',
62
+ 'agent-command-schedule',
63
+ 'agent-command-statusline',
64
+ 'agent-command-plugin',
65
+ 'agent-command-settings',
66
+ 'agent-command-provider',
67
+ ]);
68
+ });
69
+
70
+ it('TC-01: enabledCommandModules whitelist keeps exactly the listed module names', () => {
71
+ const names = moduleNames({ ...baseOptions, enabledCommandModules: [HELP, AGENT] });
72
+ expect(new Set(names)).toEqual(new Set([HELP, AGENT]));
73
+ expect(names).toHaveLength(2);
74
+ });
75
+
76
+ it('TC-02: disabledCommandModules blacklist removes the named module', () => {
77
+ const full = moduleNames(baseOptions);
78
+ const names = moduleNames({ ...baseOptions, disabledCommandModules: [BACKGROUND] });
79
+ expect(names).not.toContain(BACKGROUND);
80
+ expect(names).toHaveLength(full.length - 1);
81
+ });
82
+
83
+ it('TC-03: a name in both enabled and disabled is excluded (deny > allow)', () => {
84
+ const names = moduleNames({
85
+ ...baseOptions,
86
+ enabledCommandModules: [HELP, AGENT],
87
+ disabledCommandModules: [AGENT],
88
+ });
89
+ expect(new Set(names)).toEqual(new Set([HELP]));
90
+ expect(names).not.toContain(AGENT);
91
+ });
92
+
93
+ it('whitelist with an unknown name simply yields no module for it', () => {
94
+ const names = moduleNames({ ...baseOptions, enabledCommandModules: [HELP, 'does-not-exist'] });
95
+ expect(names).toEqual([HELP]);
96
+ });
97
+ });
@@ -9,6 +9,7 @@ import { createMemoryCommandModule } from '../memory/index.js';
9
9
  import { createModeCommandModule } from '../mode/index.js';
10
10
  import { createPermissionsCommandModule } from '../permissions/index.js';
11
11
  import { createPluginCommandModule } from '../plugin/index.js';
12
+ import { createPresetCommandModule } from '../preset/index.js';
12
13
  import { createProviderCommandModule } from '../provider/index.js';
13
14
  import { createResetCommandModule } from '../reset/index.js';
14
15
  import { createRewindCommandModule } from '../rewind/index.js';
@@ -26,19 +27,56 @@ export interface IDefaultCommandModulesOptions {
26
27
  cwd: string;
27
28
  providerDefinitions: readonly IProviderDefinition[];
28
29
  providerSettingsAdapter: IProviderCommandSettingsAdapter;
30
+ /**
31
+ * Whitelist of module `name`s to keep. When provided, only modules whose `name`
32
+ * appears here survive. Omitted → all modules kept (no-regression).
33
+ */
34
+ enabledCommandModules?: readonly string[];
35
+ /**
36
+ * Blacklist of module `name`s to remove. Applied after the whitelist, so a name
37
+ * present in both is removed (deny > allow). Omitted → no modules removed.
38
+ */
39
+ disabledCommandModules?: readonly string[];
40
+ }
41
+
42
+ /**
43
+ * Apply the preset module-selection delta to the default module set.
44
+ *
45
+ * Rules: if `enabled` is provided, keep only modules whose `name` is in it; then
46
+ * remove any module whose `name` is in `disabled` (deny > allow). Neither given →
47
+ * the full default set is returned unchanged (no-regression).
48
+ */
49
+ function applyModuleSelection(
50
+ modules: readonly ICommandModule[],
51
+ enabled: readonly string[] | undefined,
52
+ disabled: readonly string[] | undefined,
53
+ ): readonly ICommandModule[] {
54
+ let selected = modules;
55
+ if (enabled !== undefined) {
56
+ const allow = new Set(enabled);
57
+ selected = selected.filter((module) => allow.has(module.name));
58
+ }
59
+ if (disabled !== undefined) {
60
+ const deny = new Set(disabled);
61
+ selected = selected.filter((module) => !deny.has(module.name));
62
+ }
63
+ return selected;
29
64
  }
30
65
 
31
66
  export function createDefaultCommandModules({
32
67
  cwd,
33
68
  providerDefinitions,
34
69
  providerSettingsAdapter,
70
+ enabledCommandModules,
71
+ disabledCommandModules,
35
72
  }: IDefaultCommandModulesOptions): readonly ICommandModule[] {
36
- return [
73
+ const modules: readonly ICommandModule[] = [
37
74
  createSkillsCommandModule({ cwd }),
38
75
  createHelpCommandModule(),
39
76
  createAgentCommandModule(),
40
77
  createPermissionsCommandModule(),
41
78
  createModeCommandModule(),
79
+ createPresetCommandModule(),
42
80
  createLanguageCommandModule(),
43
81
  createBackgroundCommandModule(),
44
82
  createMemoryCommandModule(),
@@ -58,4 +96,5 @@ export function createDefaultCommandModules({
58
96
  settings: providerSettingsAdapter,
59
97
  }),
60
98
  ];
99
+ return applyModuleSelection(modules, enabledCommandModules, disabledCommandModules);
61
100
  }
package/src/index.ts CHANGED
@@ -10,6 +10,7 @@ export * from './memory/index.js';
10
10
  export * from './mode/index.js';
11
11
  export * from './permissions/index.js';
12
12
  export * from './plugin/index.js';
13
+ export * from './preset/index.js';
13
14
  export * from './provider/index.js';
14
15
  export * from './reset/index.js';
15
16
  export * from './rewind/index.js';
@@ -0,0 +1,138 @@
1
+ import { describe, expect, it, vi } from 'vitest';
2
+ import { listPresets } from '@robota-sdk/agent-preset';
3
+ import type { ICommandHostContext, ICommandSessionRuntime } from '@robota-sdk/agent-framework';
4
+ import { createPresetCommandModule, executePresetCommand } from '../index.js';
5
+
6
+ function createSessionRuntime(overrides?: Partial<ICommandSessionRuntime>): ICommandSessionRuntime {
7
+ return {
8
+ clearHistory: () => undefined,
9
+ compact: async () => undefined,
10
+ getContextState: () => ({
11
+ maxTokens: 100,
12
+ usedTokens: 10,
13
+ usedPercentage: 10,
14
+ remainingPercentage: 90,
15
+ }),
16
+ getPermissionMode: () => 'default',
17
+ setPermissionMode: () => undefined,
18
+ getSessionId: () => 'session_1',
19
+ getMessageCount: () => 0,
20
+ getSessionAllowedTools: () => [],
21
+ getAutoCompactThreshold: () => false,
22
+ getFullHistory: () => [],
23
+ getHistory: () => [],
24
+ ...overrides,
25
+ };
26
+ }
27
+
28
+ function createCommandHostContext(
29
+ runtime: ICommandSessionRuntime,
30
+ overrides?: Partial<ICommandHostContext>,
31
+ ): ICommandHostContext {
32
+ return {
33
+ getSession: () => runtime,
34
+ getContextState: () => ({
35
+ maxTokens: 100,
36
+ usedTokens: 10,
37
+ usedPercentage: 10,
38
+ remainingPercentage: 90,
39
+ }),
40
+ getAutoCompactThreshold: () => 0.8,
41
+ compactContext: async () => undefined,
42
+ getCwd: () => '/workspace',
43
+ listEditCheckpoints: () => [],
44
+ restoreEditCheckpoint: async () => ({
45
+ target: {
46
+ id: 'checkpoint_1',
47
+ sessionId: 'session_1',
48
+ sequence: 1,
49
+ prompt: 'edit',
50
+ createdAt: '2026-05-03T00:00:00.000Z',
51
+ fileCount: 0,
52
+ },
53
+ restoredCheckpointCount: 1,
54
+ restoredFileCount: 0,
55
+ removedCheckpointCount: 0,
56
+ }),
57
+ rollbackEditCheckpoint: async () => ({
58
+ target: {
59
+ id: 'checkpoint_1',
60
+ sessionId: 'session_1',
61
+ sequence: 1,
62
+ prompt: 'edit',
63
+ createdAt: '2026-05-03T00:00:00.000Z',
64
+ fileCount: 0,
65
+ },
66
+ restoredCheckpointCount: 1,
67
+ restoredFileCount: 0,
68
+ removedCheckpointCount: 0,
69
+ }),
70
+ getUsedMemoryReferences: () => [],
71
+ recordMemoryEvent: () => undefined,
72
+ listBackgroundTasks: () => [],
73
+ readBackgroundTaskLog: async (taskId) => ({ taskId, lines: [] }),
74
+ cancelBackgroundTask: async () => undefined,
75
+ closeBackgroundTask: async () => undefined,
76
+ ...overrides,
77
+ };
78
+ }
79
+
80
+ describe('preset command module', () => {
81
+ it('exposes a single preset system command (structure)', () => {
82
+ const module = createPresetCommandModule();
83
+ expect(module.systemCommands?.map((command) => command.name)).toContain('preset');
84
+ });
85
+
86
+ it('TC-01: lists every preset and marks the active one', () => {
87
+ const runtime = createSessionRuntime({ getActivePresetId: () => 'autonomous-builder' });
88
+ const context = createCommandHostContext(runtime);
89
+
90
+ const result = executePresetCommand(context, '');
91
+
92
+ expect(result.success).toBe(true);
93
+ for (const preset of listPresets()) {
94
+ expect(result.message).toContain(preset.id);
95
+ }
96
+ // The active preset is marked with the `* ` prefix.
97
+ expect(result.message).toContain('* autonomous-builder');
98
+ expect(result.message).not.toContain('* default');
99
+ expect(result.data?.active).toBe('autonomous-builder');
100
+ });
101
+
102
+ it('TC-02: switches to a valid preset and drives the live re-apply seams', () => {
103
+ const setActivePresetId = vi.fn();
104
+ const setPermissionMode = vi.fn();
105
+ const applyModelOptions = vi.fn();
106
+ const runtime = createSessionRuntime({
107
+ setActivePresetId,
108
+ setPermissionMode,
109
+ applyModelOptions,
110
+ });
111
+ const context = createCommandHostContext(runtime);
112
+
113
+ const result = executePresetCommand(context, 'careful-reviewer');
114
+
115
+ expect(result.success).toBe(true);
116
+ expect(result.message).toBe('Switched to preset: careful-reviewer');
117
+ expect(result.data?.preset).toBe('careful-reviewer');
118
+ expect(setActivePresetId).toHaveBeenCalledWith('careful-reviewer');
119
+ // careful-reviewer resolves autonomy ask-first → default permission posture (PRESET-012)
120
+ // and effort high → applyModelOptions (PRESET-013).
121
+ expect(setPermissionMode).toHaveBeenCalledWith('default');
122
+ expect(applyModelOptions).toHaveBeenCalledWith(expect.objectContaining({ effort: 'high' }));
123
+ });
124
+
125
+ it('TC-04: rejects an unknown preset id without switching', () => {
126
+ const setActivePresetId = vi.fn();
127
+ const runtime = createSessionRuntime({ setActivePresetId });
128
+ const context = createCommandHostContext(runtime);
129
+
130
+ const result = executePresetCommand(context, '__nope__');
131
+
132
+ expect(result.success).toBe(false);
133
+ for (const preset of listPresets()) {
134
+ expect(result.message).toContain(preset.id);
135
+ }
136
+ expect(setActivePresetId).not.toHaveBeenCalled();
137
+ });
138
+ });
@@ -0,0 +1,6 @@
1
+ export {
2
+ createPresetCommandEntry,
3
+ createPresetCommandModule,
4
+ PresetCommandSource,
5
+ } from './preset-command-module.js';
6
+ export { executePresetCommand } from './preset-command.js';
@@ -0,0 +1,79 @@
1
+ import { listPresets } from '@robota-sdk/agent-preset';
2
+
3
+ import { executePresetCommand } from './preset-command.js';
4
+
5
+ import type { ICommandModule, ISystemCommand } from '@robota-sdk/agent-framework';
6
+ import type {
7
+ ICommand,
8
+ ICommandInteractionHint,
9
+ ICommandSource,
10
+ } from '@robota-sdk/agent-interface-transport';
11
+
12
+ const PRESET_COMMAND_DESCRIPTION = 'List presets or switch the active preset';
13
+ const PRESET_ARGUMENT_HINT = 'list | <preset-id>';
14
+
15
+ /** Build one subcommand per registered preset id (mirrors the permission-mode subcommands). */
16
+ function buildPresetSubcommands(source = 'preset'): ICommand[] {
17
+ return listPresets().map((preset) => ({
18
+ name: preset.id,
19
+ description: preset.description,
20
+ source,
21
+ }));
22
+ }
23
+
24
+ export function createPresetCommandEntry(): ICommand {
25
+ return {
26
+ name: 'preset',
27
+ displayName: 'Agent Preset',
28
+ description: PRESET_COMMAND_DESCRIPTION,
29
+ source: 'preset',
30
+ argumentHint: PRESET_ARGUMENT_HINT,
31
+ subcommands: buildPresetSubcommands('preset'),
32
+ modelInvocable: false,
33
+ };
34
+ }
35
+
36
+ function createPresetSystemCommand(): ISystemCommand {
37
+ const entry = createPresetCommandEntry();
38
+ return {
39
+ name: entry.name,
40
+ displayName: entry.displayName,
41
+ description: entry.description,
42
+ requiresPermission: false,
43
+ userInvocable: true,
44
+ modelInvocable: false,
45
+ argumentHint: entry.argumentHint,
46
+ subcommands: entry.subcommands,
47
+ lifecycle: 'inline',
48
+ execute: executePresetCommand,
49
+ };
50
+ }
51
+
52
+ export class PresetCommandSource implements ICommandSource {
53
+ readonly name = 'preset';
54
+
55
+ getCommands(): ICommand[] {
56
+ return [createPresetCommandEntry()];
57
+ }
58
+ }
59
+
60
+ const PRESET_INTERACTION_HINTS: Record<string, ICommandInteractionHint> = {
61
+ preset: {
62
+ type: 'pick',
63
+ getItems: () =>
64
+ buildPresetSubcommands().map((sub) => ({
65
+ label: sub.name,
66
+ value: sub.name,
67
+ description: sub.description,
68
+ })),
69
+ },
70
+ };
71
+
72
+ export function createPresetCommandModule(): ICommandModule {
73
+ return {
74
+ name: 'agent-command-preset',
75
+ commandSources: [new PresetCommandSource()],
76
+ systemCommands: [createPresetSystemCommand()],
77
+ interactionHints: PRESET_INTERACTION_HINTS,
78
+ };
79
+ }
@@ -0,0 +1,58 @@
1
+ import { applyPresetToSession } from '@robota-sdk/agent-framework';
2
+ import { getPreset, listPresets, resolvePreset } from '@robota-sdk/agent-preset';
3
+
4
+ import type { ICommandHostContext } from '@robota-sdk/agent-framework';
5
+ import type { ICommandResult } from '@robota-sdk/agent-interface-transport';
6
+
7
+ /** Default active preset id reported when the runtime has no recorded active preset. */
8
+ const DEFAULT_ACTIVE_PRESET_ID = 'default';
9
+
10
+ /** Read the active preset id from the session, defaulting when the optional seam is absent. */
11
+ function readActivePresetId(context: ICommandHostContext): string {
12
+ return context.getSession().getActivePresetId?.() ?? DEFAULT_ACTIVE_PRESET_ID;
13
+ }
14
+
15
+ /** Build the `/preset` listing: one line per preset, marking the active one with a `*` prefix. */
16
+ function formatPresetList(active: string): string {
17
+ const lines = listPresets().map((preset) => {
18
+ const marker = preset.id === active ? '* ' : ' ';
19
+ return `${marker}${preset.id} — ${preset.title}: ${preset.description}`;
20
+ });
21
+ return ['Available presets:', ...lines].join('\n');
22
+ }
23
+
24
+ /** Build the rejection message for an unknown preset id, listing the valid ids. */
25
+ function formatUnknownPresetMessage(id: string): string {
26
+ const ids = listPresets()
27
+ .map((preset) => preset.id)
28
+ .join(', ');
29
+ return `Unknown preset: ${id}. Available: ${ids}`;
30
+ }
31
+
32
+ export function executePresetCommand(context: ICommandHostContext, args: string): ICommandResult {
33
+ const id = args.trim().split(/\s+/)[0];
34
+
35
+ if (id === undefined || id.length === 0 || id === 'list') {
36
+ const active = readActivePresetId(context);
37
+ return {
38
+ message: formatPresetList(active),
39
+ success: true,
40
+ data: { presets: listPresets(), active },
41
+ };
42
+ }
43
+
44
+ if (getPreset(id) === undefined) {
45
+ return {
46
+ message: formatUnknownPresetMessage(id),
47
+ success: false,
48
+ };
49
+ }
50
+
51
+ const resolved = resolvePreset(id);
52
+ applyPresetToSession(context, id, resolved);
53
+ return {
54
+ message: `Switched to preset: ${id}`,
55
+ success: true,
56
+ data: { preset: id },
57
+ };
58
+ }