@robota-sdk/agent-command 3.0.0-beta.75 → 3.0.0-beta.77
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/LICENSE +661 -21
- package/README.md +12 -6
- package/dist/node/index.cjs +40 -35
- package/dist/node/index.d.ts +69 -7
- package/dist/node/index.d.ts.map +1 -1
- package/dist/node/index.js +40 -35
- package/dist/node/index.js.map +1 -1
- package/package.json +7 -7
- package/src/agent/agent-command-parser.ts +1 -1
- package/src/agent/agent-command.ts +2 -1
- package/src/background/__tests__/background-command-module.test.ts +2 -5
- package/src/context/context-command.ts +4 -4
- package/src/default/__tests__/default-command-modules.test.ts +5 -2
- package/src/default/default-command-modules.ts +6 -0
- package/src/editor/__tests__/editor-command-functional.test.ts +91 -0
- package/src/editor/editor-command-module.ts +47 -0
- package/src/editor/editor-command.ts +53 -0
- package/src/editor/index.ts +7 -0
- package/src/editor/resolve-editor.ts +21 -0
- package/src/exit/__tests__/exit-command-module.test.ts +21 -2
- package/src/exit/exit-command-module.ts +1 -10
- package/src/exit/exit-command.ts +15 -1
- package/src/goal/__tests__/goal-command.test.ts +75 -0
- package/src/goal/goal-command-module.ts +48 -0
- package/src/goal/goal-command.ts +70 -0
- package/src/goal/index.ts +6 -0
- package/src/index.ts +3 -0
- package/src/language/__tests__/language-command-module.test.ts +16 -0
- package/src/language/language-command-module.ts +1 -18
- package/src/language/language-command.ts +35 -9
- package/src/mode/__tests__/mode-command-module.test.ts +34 -0
- package/src/mode/mode-command-module.ts +1 -18
- package/src/mode/mode-command.ts +31 -8
- package/src/preset/__tests__/preset-command-module.test.ts +43 -7
- package/src/preset/preset-command-module.ts +1 -18
- package/src/preset/preset-command.ts +42 -10
- package/src/provider/__tests__/org-policy.test.ts +19 -13
- package/src/provider/__tests__/provider-command-module.test.ts +151 -80
- package/src/provider/__tests__/scripted-interaction.ts +28 -0
- package/src/provider/provider-command-execution.ts +67 -50
- package/src/provider/provider-command-module.ts +3 -19
- package/src/provider/provider-command-profile-lifecycle.ts +52 -72
- package/src/provider/provider-command-profile-operations.ts +15 -51
- package/src/provider/provider-command-profile.ts +44 -49
- package/src/provider/provider-command-setup.ts +56 -51
- package/src/session/__tests__/session-command-module.test.ts +38 -0
- package/src/session/model-pricing.ts +7 -60
- package/src/session/session-command-module.ts +1 -10
- package/src/session/session-command.ts +14 -1
- package/src/shell/__tests__/shell-command-functional.test.ts +96 -0
- package/src/shell/index.ts +8 -0
- package/src/shell/resolve-shell.ts +25 -0
- package/src/shell/shell-command-module.ts +47 -0
- package/src/shell/shell-command.ts +44 -0
- package/src/shell/spawn-inherited.ts +32 -0
package/src/index.ts
CHANGED
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
export * from './agent/index.js';
|
|
2
2
|
export * from './default/index.js';
|
|
3
3
|
export * from './background/index.js';
|
|
4
|
+
export * from './goal/index.js';
|
|
4
5
|
export * from './compact/index.js';
|
|
5
6
|
export * from './context/index.js';
|
|
7
|
+
export * from './editor/index.js';
|
|
6
8
|
export * from './exit/index.js';
|
|
7
9
|
export * from './help/index.js';
|
|
8
10
|
export * from './language/index.js';
|
|
@@ -11,6 +13,7 @@ export * from './mode/index.js';
|
|
|
11
13
|
export * from './permissions/index.js';
|
|
12
14
|
export * from './plugin/index.js';
|
|
13
15
|
export * from './preset/index.js';
|
|
16
|
+
export * from './shell/index.js';
|
|
14
17
|
export * from './provider/index.js';
|
|
15
18
|
export * from './reset/index.js';
|
|
16
19
|
export * from './rewind/index.js';
|
|
@@ -102,4 +102,20 @@ describe('createLanguageCommandModule', () => {
|
|
|
102
102
|
expect(result?.success).toBe(false);
|
|
103
103
|
expect(result?.message).toBe('Usage: language <code> (e.g., ko, en, ja, zh)');
|
|
104
104
|
});
|
|
105
|
+
|
|
106
|
+
it('asks the user to pick a language when none is provided (CMD-004)', async () => {
|
|
107
|
+
const executor = new SystemCommandExecutor([
|
|
108
|
+
...(createLanguageCommandModule().systemCommands ?? []),
|
|
109
|
+
]);
|
|
110
|
+
const contextWithAsk: ICommandHostContext = {
|
|
111
|
+
...commandHostContext,
|
|
112
|
+
getUserInteraction: () => ({ ask: async () => ({ type: 'answer', values: ['ko'] }) }),
|
|
113
|
+
};
|
|
114
|
+
|
|
115
|
+
const result = await executor.execute('language', contextWithAsk, '');
|
|
116
|
+
|
|
117
|
+
expect(result?.success).toBe(true);
|
|
118
|
+
expect(result?.data?.language).toBe('ko');
|
|
119
|
+
expect(result?.effects).toEqual([{ type: 'language-change-requested', language: 'ko' }]);
|
|
120
|
+
});
|
|
105
121
|
});
|
|
@@ -7,11 +7,7 @@ import {
|
|
|
7
7
|
import { executeLanguageCommand } from './language-command.js';
|
|
8
8
|
|
|
9
9
|
import type { ICommandModule, ISystemCommand } from '@robota-sdk/agent-framework';
|
|
10
|
-
import type {
|
|
11
|
-
ICommand,
|
|
12
|
-
ICommandInteractionHint,
|
|
13
|
-
ICommandSource,
|
|
14
|
-
} from '@robota-sdk/agent-interface-transport';
|
|
10
|
+
import type { ICommand, ICommandSource } from '@robota-sdk/agent-interface-transport';
|
|
15
11
|
|
|
16
12
|
export function createLanguageCommandEntry(): ICommand {
|
|
17
13
|
return {
|
|
@@ -49,23 +45,10 @@ export class LanguageCommandSource implements ICommandSource {
|
|
|
49
45
|
}
|
|
50
46
|
}
|
|
51
47
|
|
|
52
|
-
const LANGUAGE_INTERACTION_HINTS: Record<string, ICommandInteractionHint> = {
|
|
53
|
-
language: {
|
|
54
|
-
type: 'pick',
|
|
55
|
-
getItems: () =>
|
|
56
|
-
buildLanguageCommandSubcommands().map((sub) => ({
|
|
57
|
-
label: `${sub.name} ${sub.description ?? ''}`.trimEnd(),
|
|
58
|
-
value: sub.name,
|
|
59
|
-
description: sub.description,
|
|
60
|
-
})),
|
|
61
|
-
},
|
|
62
|
-
};
|
|
63
|
-
|
|
64
48
|
export function createLanguageCommandModule(): ICommandModule {
|
|
65
49
|
return {
|
|
66
50
|
name: 'agent-command-language',
|
|
67
51
|
commandSources: [new LanguageCommandSource()],
|
|
68
52
|
systemCommands: [createLanguageSystemCommand()],
|
|
69
|
-
interactionHints: LANGUAGE_INTERACTION_HINTS,
|
|
70
53
|
};
|
|
71
54
|
}
|
|
@@ -1,18 +1,44 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { selectAction } from '@robota-sdk/agent-core';
|
|
2
|
+
import {
|
|
3
|
+
buildLanguageCommandSubcommands,
|
|
4
|
+
formatLanguageUsageMessage,
|
|
5
|
+
parseLanguageArgument,
|
|
6
|
+
} from '@robota-sdk/agent-framework';
|
|
2
7
|
|
|
3
8
|
import type { ICommandHostContext } from '@robota-sdk/agent-framework';
|
|
4
9
|
import type { ICommandResult } from '@robota-sdk/agent-interface-transport';
|
|
5
10
|
|
|
6
|
-
|
|
7
|
-
|
|
11
|
+
/**
|
|
12
|
+
* Ask the user to pick a language (CMD-004 inline ask). Returns a validated language, or `undefined`
|
|
13
|
+
* when no interactive renderer is attached or the user cancelled — the caller then shows usage.
|
|
14
|
+
*/
|
|
15
|
+
async function resolveLanguageViaAsk(context: ICommandHostContext): Promise<string | undefined> {
|
|
16
|
+
const ui = context.getUserInteraction?.();
|
|
17
|
+
if (!ui) return undefined;
|
|
18
|
+
const options = buildLanguageCommandSubcommands().map((sub) => ({
|
|
19
|
+
value: sub.name,
|
|
20
|
+
label: sub.name,
|
|
21
|
+
description: sub.description,
|
|
22
|
+
}));
|
|
23
|
+
const response = await ui.ask(selectAction('language', 'Select language', options));
|
|
24
|
+
const picked = response.type === 'answer' ? response.values[0] : undefined;
|
|
25
|
+
return picked === undefined ? undefined : parseLanguageArgument(picked);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export async function executeLanguageCommand(
|
|
29
|
+
context: ICommandHostContext,
|
|
8
30
|
args: string,
|
|
9
|
-
): ICommandResult {
|
|
10
|
-
|
|
31
|
+
): Promise<ICommandResult> {
|
|
32
|
+
let language = parseLanguageArgument(args);
|
|
33
|
+
|
|
11
34
|
if (language === undefined) {
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
35
|
+
language = await resolveLanguageViaAsk(context);
|
|
36
|
+
if (language === undefined) {
|
|
37
|
+
return {
|
|
38
|
+
message: formatLanguageUsageMessage(),
|
|
39
|
+
success: false,
|
|
40
|
+
};
|
|
41
|
+
}
|
|
16
42
|
}
|
|
17
43
|
|
|
18
44
|
return {
|
|
@@ -140,4 +140,38 @@ describe('createModeCommandModule', () => {
|
|
|
140
140
|
);
|
|
141
141
|
expect(context.setPermissionMode).not.toHaveBeenCalled();
|
|
142
142
|
});
|
|
143
|
+
|
|
144
|
+
it('asks the user to pick a mode when no arg is given and a renderer is attached (CMD-004)', async () => {
|
|
145
|
+
const executor = new SystemCommandExecutor([
|
|
146
|
+
...(createModeCommandModule().systemCommands ?? []),
|
|
147
|
+
]);
|
|
148
|
+
const context = createCommandHostContext();
|
|
149
|
+
const contextWithAsk: ICommandHostContext = {
|
|
150
|
+
...context,
|
|
151
|
+
getUserInteraction: () => ({ ask: async () => ({ type: 'answer', values: ['plan'] }) }),
|
|
152
|
+
};
|
|
153
|
+
|
|
154
|
+
const result = await executor.execute('mode', contextWithAsk, '');
|
|
155
|
+
|
|
156
|
+
expect(result?.success).toBe(true);
|
|
157
|
+
expect(result?.message).toBe('Permission mode set to: plan');
|
|
158
|
+
expect(context.setPermissionMode).toHaveBeenCalledWith('plan');
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
it('reports the current mode when the user cancels the pick (CMD-004)', async () => {
|
|
162
|
+
const executor = new SystemCommandExecutor([
|
|
163
|
+
...(createModeCommandModule().systemCommands ?? []),
|
|
164
|
+
]);
|
|
165
|
+
const context = createCommandHostContext();
|
|
166
|
+
const contextWithAsk: ICommandHostContext = {
|
|
167
|
+
...context,
|
|
168
|
+
getUserInteraction: () => ({ ask: async () => ({ type: 'cancelled' }) }),
|
|
169
|
+
};
|
|
170
|
+
|
|
171
|
+
const result = await executor.execute('mode', contextWithAsk, '');
|
|
172
|
+
|
|
173
|
+
expect(result?.success).toBe(true);
|
|
174
|
+
expect(result?.message).toBe('Current mode: default');
|
|
175
|
+
expect(context.setPermissionMode).not.toHaveBeenCalled();
|
|
176
|
+
});
|
|
143
177
|
});
|
|
@@ -7,11 +7,7 @@ import {
|
|
|
7
7
|
import { executeModeCommand } from './mode-command.js';
|
|
8
8
|
|
|
9
9
|
import type { ICommandModule, ISystemCommand } from '@robota-sdk/agent-framework';
|
|
10
|
-
import type {
|
|
11
|
-
ICommand,
|
|
12
|
-
ICommandInteractionHint,
|
|
13
|
-
ICommandSource,
|
|
14
|
-
} from '@robota-sdk/agent-interface-transport';
|
|
10
|
+
import type { ICommand, ICommandSource } from '@robota-sdk/agent-interface-transport';
|
|
15
11
|
|
|
16
12
|
export function createModeCommandEntry(): ICommand {
|
|
17
13
|
return {
|
|
@@ -49,23 +45,10 @@ export class ModeCommandSource implements ICommandSource {
|
|
|
49
45
|
}
|
|
50
46
|
}
|
|
51
47
|
|
|
52
|
-
const MODE_INTERACTION_HINTS: Record<string, ICommandInteractionHint> = {
|
|
53
|
-
mode: {
|
|
54
|
-
type: 'pick',
|
|
55
|
-
getItems: () =>
|
|
56
|
-
buildPermissionModeSubcommands().map((sub) => ({
|
|
57
|
-
label: sub.name,
|
|
58
|
-
value: sub.name,
|
|
59
|
-
description: sub.description,
|
|
60
|
-
})),
|
|
61
|
-
},
|
|
62
|
-
};
|
|
63
|
-
|
|
64
48
|
export function createModeCommandModule(): ICommandModule {
|
|
65
49
|
return {
|
|
66
50
|
name: 'agent-command-mode',
|
|
67
51
|
commandSources: [new ModeCommandSource()],
|
|
68
52
|
systemCommands: [createModeSystemCommand()],
|
|
69
|
-
interactionHints: MODE_INTERACTION_HINTS,
|
|
70
53
|
};
|
|
71
54
|
}
|
package/src/mode/mode-command.ts
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
|
+
import { selectAction } from '@robota-sdk/agent-core';
|
|
1
2
|
import {
|
|
3
|
+
buildPermissionModeSubcommands,
|
|
2
4
|
formatInvalidPermissionModeMessage,
|
|
3
5
|
isPermissionMode,
|
|
4
6
|
parsePermissionModeArgument,
|
|
@@ -9,15 +11,36 @@ import {
|
|
|
9
11
|
import type { ICommandHostContext } from '@robota-sdk/agent-framework';
|
|
10
12
|
import type { ICommandResult } from '@robota-sdk/agent-interface-transport';
|
|
11
13
|
|
|
12
|
-
|
|
13
|
-
|
|
14
|
+
/**
|
|
15
|
+
* Ask the user to pick a permission mode (CMD-004 inline ask). Returns the chosen mode name, or
|
|
16
|
+
* `undefined` when no interactive renderer is attached or the user cancelled — the caller then reports
|
|
17
|
+
* the current mode instead of changing it (never a silent guess).
|
|
18
|
+
*/
|
|
19
|
+
async function resolveModeViaAsk(context: ICommandHostContext): Promise<string | undefined> {
|
|
20
|
+
const ui = context.getUserInteraction?.();
|
|
21
|
+
if (!ui) return undefined;
|
|
22
|
+
const options = buildPermissionModeSubcommands().map((sub) => ({
|
|
23
|
+
value: sub.name,
|
|
24
|
+
label: sub.name,
|
|
25
|
+
description: sub.description,
|
|
26
|
+
}));
|
|
27
|
+
const response = await ui.ask(selectAction('mode', 'Select interaction mode', options));
|
|
28
|
+
return response.type === 'answer' ? response.values[0] : undefined;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export async function executeModeCommand(
|
|
32
|
+
context: ICommandHostContext,
|
|
33
|
+
args: string,
|
|
34
|
+
): Promise<ICommandResult> {
|
|
35
|
+
let arg: string | undefined = parsePermissionModeArgument(args);
|
|
36
|
+
|
|
14
37
|
if (arg === undefined) {
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
data: { mode }
|
|
20
|
-
}
|
|
38
|
+
arg = await resolveModeViaAsk(context);
|
|
39
|
+
if (arg === undefined) {
|
|
40
|
+
// No interactive answer — report the current mode without changing it.
|
|
41
|
+
const mode = readCommandPermissionMode(context);
|
|
42
|
+
return { message: `Current mode: ${mode}`, success: true, data: { mode } };
|
|
43
|
+
}
|
|
21
44
|
}
|
|
22
45
|
|
|
23
46
|
if (!isPermissionMode(arg)) {
|
|
@@ -78,16 +78,16 @@ function createCommandHostContext(
|
|
|
78
78
|
}
|
|
79
79
|
|
|
80
80
|
describe('preset command module', () => {
|
|
81
|
-
it('exposes a single preset system command (structure)', () => {
|
|
81
|
+
it('exposes a single preset system command (structure)', async () => {
|
|
82
82
|
const module = createPresetCommandModule();
|
|
83
83
|
expect(module.systemCommands?.map((command) => command.name)).toContain('preset');
|
|
84
84
|
});
|
|
85
85
|
|
|
86
|
-
it('TC-01: lists every preset and marks the active one', () => {
|
|
86
|
+
it('TC-01: lists every preset and marks the active one', async () => {
|
|
87
87
|
const runtime = createSessionRuntime({ getActivePresetId: () => 'autonomous-builder' });
|
|
88
88
|
const context = createCommandHostContext(runtime);
|
|
89
89
|
|
|
90
|
-
const result = executePresetCommand(context, '');
|
|
90
|
+
const result = await executePresetCommand(context, '');
|
|
91
91
|
|
|
92
92
|
expect(result.success).toBe(true);
|
|
93
93
|
for (const preset of listPresets()) {
|
|
@@ -99,7 +99,7 @@ describe('preset command module', () => {
|
|
|
99
99
|
expect(result.data?.active).toBe('autonomous-builder');
|
|
100
100
|
});
|
|
101
101
|
|
|
102
|
-
it('TC-02: switches to a valid preset and drives the live re-apply seams', () => {
|
|
102
|
+
it('TC-02: switches to a valid preset and drives the live re-apply seams', async () => {
|
|
103
103
|
const setActivePresetId = vi.fn();
|
|
104
104
|
const setPermissionMode = vi.fn();
|
|
105
105
|
const applyModelOptions = vi.fn();
|
|
@@ -110,7 +110,7 @@ describe('preset command module', () => {
|
|
|
110
110
|
});
|
|
111
111
|
const context = createCommandHostContext(runtime);
|
|
112
112
|
|
|
113
|
-
const result = executePresetCommand(context, 'careful-reviewer');
|
|
113
|
+
const result = await executePresetCommand(context, 'careful-reviewer');
|
|
114
114
|
|
|
115
115
|
expect(result.success).toBe(true);
|
|
116
116
|
expect(result.message).toBe('Switched to preset: careful-reviewer');
|
|
@@ -122,12 +122,12 @@ describe('preset command module', () => {
|
|
|
122
122
|
expect(applyModelOptions).toHaveBeenCalledWith(expect.objectContaining({ effort: 'high' }));
|
|
123
123
|
});
|
|
124
124
|
|
|
125
|
-
it('TC-04: rejects an unknown preset id without switching', () => {
|
|
125
|
+
it('TC-04: rejects an unknown preset id without switching', async () => {
|
|
126
126
|
const setActivePresetId = vi.fn();
|
|
127
127
|
const runtime = createSessionRuntime({ setActivePresetId });
|
|
128
128
|
const context = createCommandHostContext(runtime);
|
|
129
129
|
|
|
130
|
-
const result = executePresetCommand(context, '__nope__');
|
|
130
|
+
const result = await executePresetCommand(context, '__nope__');
|
|
131
131
|
|
|
132
132
|
expect(result.success).toBe(false);
|
|
133
133
|
for (const preset of listPresets()) {
|
|
@@ -135,4 +135,40 @@ describe('preset command module', () => {
|
|
|
135
135
|
}
|
|
136
136
|
expect(setActivePresetId).not.toHaveBeenCalled();
|
|
137
137
|
});
|
|
138
|
+
|
|
139
|
+
it('CMD-004: asks the user to pick a preset when none is given, then switches', async () => {
|
|
140
|
+
const setActivePresetId = vi.fn();
|
|
141
|
+
const setPermissionMode = vi.fn();
|
|
142
|
+
const applyModelOptions = vi.fn();
|
|
143
|
+
const runtime = createSessionRuntime({
|
|
144
|
+
setActivePresetId,
|
|
145
|
+
setPermissionMode,
|
|
146
|
+
applyModelOptions,
|
|
147
|
+
});
|
|
148
|
+
const context = createCommandHostContext(runtime, {
|
|
149
|
+
getUserInteraction: () => ({
|
|
150
|
+
ask: async () => ({ type: 'answer', values: ['careful-reviewer'] }),
|
|
151
|
+
}),
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
const result = await executePresetCommand(context, '');
|
|
155
|
+
|
|
156
|
+
expect(result.success).toBe(true);
|
|
157
|
+
expect(result.message).toBe('Switched to preset: careful-reviewer');
|
|
158
|
+
expect(setActivePresetId).toHaveBeenCalledWith('careful-reviewer');
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
it('CMD-004: shows the preset list when the pick is cancelled', async () => {
|
|
162
|
+
const setActivePresetId = vi.fn();
|
|
163
|
+
const runtime = createSessionRuntime({ setActivePresetId });
|
|
164
|
+
const context = createCommandHostContext(runtime, {
|
|
165
|
+
getUserInteraction: () => ({ ask: async () => ({ type: 'cancelled' }) }),
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
const result = await executePresetCommand(context, '');
|
|
169
|
+
|
|
170
|
+
expect(result.success).toBe(true);
|
|
171
|
+
expect(result.message).toContain('Available presets:');
|
|
172
|
+
expect(setActivePresetId).not.toHaveBeenCalled();
|
|
173
|
+
});
|
|
138
174
|
});
|
|
@@ -3,11 +3,7 @@ import { listPresets } from '@robota-sdk/agent-preset';
|
|
|
3
3
|
import { executePresetCommand } from './preset-command.js';
|
|
4
4
|
|
|
5
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';
|
|
6
|
+
import type { ICommand, ICommandSource } from '@robota-sdk/agent-interface-transport';
|
|
11
7
|
|
|
12
8
|
const PRESET_COMMAND_DESCRIPTION = 'List presets or switch the active preset';
|
|
13
9
|
const PRESET_ARGUMENT_HINT = 'list | <preset-id>';
|
|
@@ -57,23 +53,10 @@ export class PresetCommandSource implements ICommandSource {
|
|
|
57
53
|
}
|
|
58
54
|
}
|
|
59
55
|
|
|
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
56
|
export function createPresetCommandModule(): ICommandModule {
|
|
73
57
|
return {
|
|
74
58
|
name: 'agent-command-preset',
|
|
75
59
|
commandSources: [new PresetCommandSource()],
|
|
76
60
|
systemCommands: [createPresetSystemCommand()],
|
|
77
|
-
interactionHints: PRESET_INTERACTION_HINTS,
|
|
78
61
|
};
|
|
79
62
|
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { selectAction } from '@robota-sdk/agent-core';
|
|
1
2
|
import { applyPresetToSession } from '@robota-sdk/agent-framework';
|
|
2
3
|
import { getPreset, listPresets, resolvePreset } from '@robota-sdk/agent-preset';
|
|
3
4
|
|
|
@@ -29,16 +30,47 @@ function formatUnknownPresetMessage(id: string): string {
|
|
|
29
30
|
return `Unknown preset: ${id}. Available: ${ids}`;
|
|
30
31
|
}
|
|
31
32
|
|
|
32
|
-
|
|
33
|
-
|
|
33
|
+
/** The `/preset` (or `/preset list`) listing result. */
|
|
34
|
+
function presetListResult(context: ICommandHostContext): ICommandResult {
|
|
35
|
+
const active = readActivePresetId(context);
|
|
36
|
+
return {
|
|
37
|
+
message: formatPresetList(active),
|
|
38
|
+
success: true,
|
|
39
|
+
data: { presets: listPresets(), active },
|
|
40
|
+
};
|
|
41
|
+
}
|
|
34
42
|
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
43
|
+
/**
|
|
44
|
+
* Ask the user to pick a preset (CMD-004 inline ask). Returns the chosen id, or `undefined` when no
|
|
45
|
+
* interactive renderer is attached or the user cancelled — the caller then shows the preset list.
|
|
46
|
+
*/
|
|
47
|
+
async function resolvePresetViaAsk(context: ICommandHostContext): Promise<string | undefined> {
|
|
48
|
+
const ui = context.getUserInteraction?.();
|
|
49
|
+
if (!ui) return undefined;
|
|
50
|
+
const options = listPresets().map((preset) => ({
|
|
51
|
+
value: preset.id,
|
|
52
|
+
label: preset.id,
|
|
53
|
+
description: preset.description,
|
|
54
|
+
}));
|
|
55
|
+
const response = await ui.ask(selectAction('preset', 'Select a preset', options));
|
|
56
|
+
return response.type === 'answer' ? response.values[0] : undefined;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export async function executePresetCommand(
|
|
60
|
+
context: ICommandHostContext,
|
|
61
|
+
args: string,
|
|
62
|
+
): Promise<ICommandResult> {
|
|
63
|
+
let id: string | undefined = args.trim().split(/\s+/)[0];
|
|
64
|
+
|
|
65
|
+
if (id === 'list') {
|
|
66
|
+
return presetListResult(context);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
if (id === undefined || id.length === 0) {
|
|
70
|
+
id = await resolvePresetViaAsk(context);
|
|
71
|
+
if (id === undefined) {
|
|
72
|
+
return presetListResult(context);
|
|
73
|
+
}
|
|
42
74
|
}
|
|
43
75
|
|
|
44
76
|
if (getPreset(id) === undefined) {
|
|
@@ -49,7 +81,7 @@ export function executePresetCommand(context: ICommandHostContext, args: string)
|
|
|
49
81
|
}
|
|
50
82
|
|
|
51
83
|
const resolved = resolvePreset(id);
|
|
52
|
-
applyPresetToSession(context, id, resolved);
|
|
84
|
+
await applyPresetToSession(context, id, resolved);
|
|
53
85
|
return {
|
|
54
86
|
message: `Switched to preset: ${id}`,
|
|
55
87
|
success: true,
|
|
@@ -8,6 +8,7 @@ import type {
|
|
|
8
8
|
TProviderSettingsDocument,
|
|
9
9
|
} from '@robota-sdk/agent-framework';
|
|
10
10
|
import { createProviderCommandModule } from '../provider-command-module.js';
|
|
11
|
+
import { scriptedContext } from './scripted-interaction.js';
|
|
11
12
|
|
|
12
13
|
const providerDefinitions: readonly IProviderDefinition[] = [
|
|
13
14
|
{
|
|
@@ -62,7 +63,8 @@ function createExecutor(
|
|
|
62
63
|
return new SystemCommandExecutor([...(module.systemCommands ?? [])]);
|
|
63
64
|
}
|
|
64
65
|
|
|
65
|
-
|
|
66
|
+
/** Context with no interactive renderer attached (headless/automation). */
|
|
67
|
+
const headlessContext = {} as ICommandHostContext;
|
|
66
68
|
|
|
67
69
|
describe('org policy enforcement in provider commands', () => {
|
|
68
70
|
afterEach(() => {
|
|
@@ -83,7 +85,7 @@ describe('org policy enforcement in provider commands', () => {
|
|
|
83
85
|
|
|
84
86
|
const result = await createExecutor(adapter, orgPolicy).execute(
|
|
85
87
|
'provider',
|
|
86
|
-
|
|
88
|
+
headlessContext,
|
|
87
89
|
'switch openai',
|
|
88
90
|
);
|
|
89
91
|
|
|
@@ -105,7 +107,7 @@ describe('org policy enforcement in provider commands', () => {
|
|
|
105
107
|
|
|
106
108
|
const result = await createExecutor(adapter, orgPolicy).execute(
|
|
107
109
|
'provider',
|
|
108
|
-
|
|
110
|
+
headlessContext,
|
|
109
111
|
'switch openai',
|
|
110
112
|
);
|
|
111
113
|
|
|
@@ -128,12 +130,14 @@ describe('org policy enforcement in provider commands', () => {
|
|
|
128
130
|
});
|
|
129
131
|
const orgPolicy: IOrgPolicy = { requireApiKeyFromEnv: true, adminContact: 'sec@example.com' };
|
|
130
132
|
|
|
131
|
-
const listed = await createExecutor(adapter, orgPolicy).execute('provider', session, 'list');
|
|
132
|
-
const selected = await listed?.interaction?.submit('anthropic');
|
|
133
|
-
const editRequested = await selected?.interaction?.submit('edit');
|
|
134
133
|
// Anthropic has 2 setup steps: apiKey then model
|
|
135
|
-
const
|
|
136
|
-
|
|
134
|
+
const { context } = scriptedContext([
|
|
135
|
+
{ type: 'answer', values: ['anthropic'] },
|
|
136
|
+
{ type: 'answer', values: ['edit'] },
|
|
137
|
+
{ type: 'answer', values: [], text: 'sk-plaintext-key' },
|
|
138
|
+
{ type: 'answer', values: [], text: 'claude-sonnet-4-6' },
|
|
139
|
+
]);
|
|
140
|
+
const completed = await createExecutor(adapter, orgPolicy).execute('provider', context, 'list');
|
|
137
141
|
|
|
138
142
|
expect(completed?.success).toBe(false);
|
|
139
143
|
expect(completed?.message).toContain('environment variable references');
|
|
@@ -154,11 +158,13 @@ describe('org policy enforcement in provider commands', () => {
|
|
|
154
158
|
});
|
|
155
159
|
const orgPolicy: IOrgPolicy = { requireApiKeyFromEnv: true };
|
|
156
160
|
|
|
157
|
-
const
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
161
|
+
const { context } = scriptedContext([
|
|
162
|
+
{ type: 'answer', values: ['anthropic'] },
|
|
163
|
+
{ type: 'answer', values: ['edit'] },
|
|
164
|
+
{ type: 'answer', values: [], text: '$ENV:ORG_TEST_KEY' },
|
|
165
|
+
{ type: 'answer', values: [], text: 'claude-opus-4-5' },
|
|
166
|
+
]);
|
|
167
|
+
const completed = await createExecutor(adapter, orgPolicy).execute('provider', context, 'list');
|
|
162
168
|
|
|
163
169
|
expect(completed?.success).toBe(true);
|
|
164
170
|
});
|