@robota-sdk/agent-command 3.0.0-beta.66 → 3.0.0-beta.68
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/README.md +0 -1
- package/dist/node/index.cjs +57 -28
- package/dist/node/index.d.ts +3 -16
- package/dist/node/index.d.ts.map +1 -1
- package/dist/node/index.js +57 -28
- package/dist/node/index.js.map +1 -1
- package/package.json +3 -3
- package/src/compact/__tests__/compact-command-module.test.ts +13 -4
- package/src/compact/compact-command-module.ts +2 -0
- package/src/compact/compact-command.ts +11 -4
- package/src/default/default-command-modules.ts +0 -5
- package/src/help/__tests__/help-command.test.ts +119 -0
- package/src/index.ts +0 -1
- package/src/provider/__tests__/org-policy.test.ts +165 -0
- package/src/provider/__tests__/provider-command-module.test.ts +18 -29
- package/src/provider/__tests__/provider-setup-flow.test.ts +2 -2
- package/src/provider/provider-command-execution.ts +2 -2
- package/src/provider/provider-command-module.ts +4 -2
- package/src/provider/provider-command-profile-operations.ts +37 -49
- package/src/provider/provider-onboarding.ts +64 -0
- package/src/provider/provider-setup-flow.ts +11 -2
- package/src/provider/provider-startup.ts +15 -3
- package/src/session/__tests__/model-pricing.test.ts +64 -0
- package/src/session/__tests__/session-command-module.test.ts +51 -7
- package/src/session/model-pricing.ts +79 -0
- package/src/session/session-command.ts +124 -8
- package/src/model/__tests__/model-command-module.test.ts +0 -273
- package/src/model/index.ts +0 -6
- package/src/model/model-command-module.ts +0 -70
- package/src/model/model-command.ts +0 -41
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@robota-sdk/agent-command",
|
|
3
|
-
"version": "3.0.0-beta.
|
|
3
|
+
"version": "3.0.0-beta.68",
|
|
4
4
|
"description": "Consolidated command module implementations for Robota SDK CLI",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/node/index.js",
|
|
@@ -24,8 +24,8 @@
|
|
|
24
24
|
"src"
|
|
25
25
|
],
|
|
26
26
|
"dependencies": {
|
|
27
|
-
"@robota-sdk/agent-
|
|
28
|
-
"@robota-sdk/agent-
|
|
27
|
+
"@robota-sdk/agent-core": "3.0.0-beta.68",
|
|
28
|
+
"@robota-sdk/agent-framework": "3.0.0-beta.68"
|
|
29
29
|
},
|
|
30
30
|
"devDependencies": {
|
|
31
31
|
"@types/node": "^22.14.0",
|
|
@@ -20,8 +20,9 @@ const AFTER_CONTEXT: TContextWindowState = {
|
|
|
20
20
|
remainingPercentage: 65,
|
|
21
21
|
};
|
|
22
22
|
|
|
23
|
-
function createRuntime(): ICommandSessionRuntime {
|
|
23
|
+
function createRuntime(beforeCount = 10, afterCount = 3): ICommandSessionRuntime {
|
|
24
24
|
let mode: TPermissionMode = 'default';
|
|
25
|
+
const getMessageCount = vi.fn().mockReturnValueOnce(beforeCount).mockReturnValue(afterCount);
|
|
25
26
|
return {
|
|
26
27
|
clearHistory: vi.fn(),
|
|
27
28
|
compact: vi.fn().mockResolvedValue(undefined),
|
|
@@ -31,7 +32,7 @@ function createRuntime(): ICommandSessionRuntime {
|
|
|
31
32
|
mode = nextMode;
|
|
32
33
|
},
|
|
33
34
|
getSessionId: () => 'session_1',
|
|
34
|
-
getMessageCount
|
|
35
|
+
getMessageCount,
|
|
35
36
|
getSessionAllowedTools: () => [],
|
|
36
37
|
getAutoCompactThreshold: () => 0.835,
|
|
37
38
|
};
|
|
@@ -104,6 +105,7 @@ describe('createCompactCommandModule', () => {
|
|
|
104
105
|
modelInvocable: true,
|
|
105
106
|
argumentHint: '[instructions]',
|
|
106
107
|
safety: 'write',
|
|
108
|
+
requiresPermission: false,
|
|
107
109
|
},
|
|
108
110
|
]);
|
|
109
111
|
});
|
|
@@ -117,8 +119,15 @@ describe('createCompactCommandModule', () => {
|
|
|
117
119
|
const result = await executor.execute('compact', context, ' focus on tests ');
|
|
118
120
|
|
|
119
121
|
expect(result?.success).toBe(true);
|
|
120
|
-
expect(result?.message).toBe(
|
|
121
|
-
|
|
122
|
+
expect(result?.message).toBe(
|
|
123
|
+
'Context compacted.\n Removed messages: 7 (70% of total)\n Context: 80% → 35%',
|
|
124
|
+
);
|
|
125
|
+
expect(result?.data).toEqual({
|
|
126
|
+
before: BEFORE_CONTEXT,
|
|
127
|
+
after: AFTER_CONTEXT,
|
|
128
|
+
beforeMessageCount: 10,
|
|
129
|
+
afterMessageCount: 3,
|
|
130
|
+
});
|
|
122
131
|
expect(context.compactContext).toHaveBeenCalledWith('focus on tests');
|
|
123
132
|
});
|
|
124
133
|
|
|
@@ -16,6 +16,7 @@ export function createCompactCommandEntry(): ICommand {
|
|
|
16
16
|
modelInvocable: true,
|
|
17
17
|
argumentHint: '[instructions]',
|
|
18
18
|
safety: 'write',
|
|
19
|
+
example: '/compact Summarize the current context',
|
|
19
20
|
};
|
|
20
21
|
}
|
|
21
22
|
|
|
@@ -25,6 +26,7 @@ function createCompactSystemCommand(): ISystemCommand {
|
|
|
25
26
|
name: entry.name,
|
|
26
27
|
displayName: entry.displayName,
|
|
27
28
|
description: entry.description,
|
|
29
|
+
example: entry.example,
|
|
28
30
|
requiresPermission: false,
|
|
29
31
|
userInvocable: true,
|
|
30
32
|
modelInvocable: entry.modelInvocable,
|
|
@@ -12,11 +12,18 @@ export async function executeCompactCommand(
|
|
|
12
12
|
args: string,
|
|
13
13
|
): Promise<ICommandResult> {
|
|
14
14
|
const result = await compactCommandContext(context, parseInstructions(args));
|
|
15
|
-
const before = result
|
|
16
|
-
const
|
|
15
|
+
const { before, after, beforeMessageCount, afterMessageCount } = result;
|
|
16
|
+
const removedMessages = beforeMessageCount - afterMessageCount;
|
|
17
|
+
const removedPercent =
|
|
18
|
+
beforeMessageCount > 0 ? Math.round((removedMessages / beforeMessageCount) * 100) : 0;
|
|
19
|
+
const message = [
|
|
20
|
+
'Context compacted.',
|
|
21
|
+
` Removed messages: ${removedMessages} (${removedPercent}% of total)`,
|
|
22
|
+
` Context: ${Math.round(before.usedPercentage)}% → ${Math.round(after.usedPercentage)}%`,
|
|
23
|
+
].join('\n');
|
|
17
24
|
return {
|
|
18
|
-
message
|
|
25
|
+
message,
|
|
19
26
|
success: true,
|
|
20
|
-
data: { before, after },
|
|
27
|
+
data: { before, after, beforeMessageCount, afterMessageCount },
|
|
21
28
|
};
|
|
22
29
|
}
|
|
@@ -7,7 +7,6 @@ import { createHelpCommandModule } from '../help/index.js';
|
|
|
7
7
|
import { createLanguageCommandModule } from '../language/index.js';
|
|
8
8
|
import { createMemoryCommandModule } from '../memory/index.js';
|
|
9
9
|
import { createModeCommandModule } from '../mode/index.js';
|
|
10
|
-
import { createModelCommandModule } from '../model/index.js';
|
|
11
10
|
import { createPermissionsCommandModule } from '../permissions/index.js';
|
|
12
11
|
import { createPluginCommandModule } from '../plugin/index.js';
|
|
13
12
|
import { createProviderCommandModule } from '../provider/index.js';
|
|
@@ -37,10 +36,6 @@ export function createDefaultCommandModules({
|
|
|
37
36
|
createSkillsCommandModule({ cwd }),
|
|
38
37
|
createHelpCommandModule(),
|
|
39
38
|
createAgentCommandModule(),
|
|
40
|
-
createModelCommandModule({
|
|
41
|
-
providerDefinitions,
|
|
42
|
-
settings: { readMergedSettings: () => providerSettingsAdapter.readMergedSettings() },
|
|
43
|
-
}),
|
|
44
39
|
createPermissionsCommandModule(),
|
|
45
40
|
createModeCommandModule(),
|
|
46
41
|
createLanguageCommandModule(),
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
ICommandHostContext,
|
|
3
|
+
ICommandListEntry,
|
|
4
|
+
ICommandSessionRuntime,
|
|
5
|
+
} from '@robota-sdk/agent-framework';
|
|
6
|
+
import { formatCommandHelpMessage } from '@robota-sdk/agent-framework';
|
|
7
|
+
import { describe, expect, it } from 'vitest';
|
|
8
|
+
|
|
9
|
+
function createCommandSessionRuntime(): ICommandSessionRuntime {
|
|
10
|
+
return {
|
|
11
|
+
clearHistory: () => undefined,
|
|
12
|
+
compact: async () => undefined,
|
|
13
|
+
getContextState: () => ({
|
|
14
|
+
maxTokens: 100,
|
|
15
|
+
usedTokens: 10,
|
|
16
|
+
usedPercentage: 10,
|
|
17
|
+
remainingPercentage: 90,
|
|
18
|
+
}),
|
|
19
|
+
getPermissionMode: () => 'default',
|
|
20
|
+
setPermissionMode: () => undefined,
|
|
21
|
+
getSessionId: () => 'session_1',
|
|
22
|
+
getMessageCount: () => 0,
|
|
23
|
+
getSessionAllowedTools: () => [],
|
|
24
|
+
getAutoCompactThreshold: () => false,
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function createCheckpoint() {
|
|
29
|
+
return {
|
|
30
|
+
id: 'checkpoint_1',
|
|
31
|
+
sessionId: 'session_1',
|
|
32
|
+
sequence: 1,
|
|
33
|
+
prompt: 'prompt',
|
|
34
|
+
createdAt: '2026-05-25T00:00:00.000Z',
|
|
35
|
+
fileCount: 0,
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function buildContext(commands: ICommandListEntry[]): ICommandHostContext {
|
|
40
|
+
const checkpoint = createCheckpoint();
|
|
41
|
+
return {
|
|
42
|
+
getSession: () => createCommandSessionRuntime(),
|
|
43
|
+
getContextState: () => ({
|
|
44
|
+
maxTokens: 100,
|
|
45
|
+
usedTokens: 10,
|
|
46
|
+
usedPercentage: 10,
|
|
47
|
+
remainingPercentage: 90,
|
|
48
|
+
}),
|
|
49
|
+
getAutoCompactThreshold: () => 0.8,
|
|
50
|
+
compactContext: async () => undefined,
|
|
51
|
+
getCwd: () => '/workspace',
|
|
52
|
+
listCommands: () => commands,
|
|
53
|
+
listEditCheckpoints: () => [],
|
|
54
|
+
restoreEditCheckpoint: async () => ({
|
|
55
|
+
target: checkpoint,
|
|
56
|
+
restoredCheckpointCount: 0,
|
|
57
|
+
restoredFileCount: 0,
|
|
58
|
+
removedCheckpointCount: 0,
|
|
59
|
+
}),
|
|
60
|
+
rollbackEditCheckpoint: async () => ({
|
|
61
|
+
target: checkpoint,
|
|
62
|
+
restoredCheckpointCount: 0,
|
|
63
|
+
restoredFileCount: 0,
|
|
64
|
+
removedCheckpointCount: 0,
|
|
65
|
+
}),
|
|
66
|
+
getUsedMemoryReferences: () => [],
|
|
67
|
+
recordMemoryEvent: () => undefined,
|
|
68
|
+
listBackgroundTasks: () => [],
|
|
69
|
+
readBackgroundTaskLog: async (taskId) => ({ taskId, lines: [] }),
|
|
70
|
+
cancelBackgroundTask: async () => undefined,
|
|
71
|
+
closeBackgroundTask: async () => undefined,
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
describe('formatCommandHelpMessage — example field', () => {
|
|
76
|
+
it('TC-01: includes Example line for compact command', () => {
|
|
77
|
+
const context = buildContext([
|
|
78
|
+
{
|
|
79
|
+
name: 'compact',
|
|
80
|
+
displayName: 'Compact Context',
|
|
81
|
+
description: 'Compress context window',
|
|
82
|
+
example: '/compact Summarize the current context',
|
|
83
|
+
},
|
|
84
|
+
]);
|
|
85
|
+
|
|
86
|
+
const output = formatCommandHelpMessage(context);
|
|
87
|
+
|
|
88
|
+
expect(output).toContain('Example: /compact Summarize the current context');
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
it('TC-02: includes Example line for provider command', () => {
|
|
92
|
+
const context = buildContext([
|
|
93
|
+
{
|
|
94
|
+
name: 'provider',
|
|
95
|
+
displayName: 'Provider Setup',
|
|
96
|
+
description: 'Manage provider profiles',
|
|
97
|
+
example: '/provider switch production',
|
|
98
|
+
},
|
|
99
|
+
]);
|
|
100
|
+
|
|
101
|
+
const output = formatCommandHelpMessage(context);
|
|
102
|
+
|
|
103
|
+
expect(output).toContain('Example: /provider switch production');
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
it('TC-03: omits Example line for commands without example', () => {
|
|
107
|
+
const context = buildContext([
|
|
108
|
+
{
|
|
109
|
+
name: 'help',
|
|
110
|
+
displayName: 'Help',
|
|
111
|
+
description: 'Show available commands',
|
|
112
|
+
},
|
|
113
|
+
]);
|
|
114
|
+
|
|
115
|
+
const output = formatCommandHelpMessage(context);
|
|
116
|
+
|
|
117
|
+
expect(output).not.toContain('Example:');
|
|
118
|
+
});
|
|
119
|
+
});
|
package/src/index.ts
CHANGED
|
@@ -8,7 +8,6 @@ export * from './help/index.js';
|
|
|
8
8
|
export * from './language/index.js';
|
|
9
9
|
export * from './memory/index.js';
|
|
10
10
|
export * from './mode/index.js';
|
|
11
|
-
export * from './model/index.js';
|
|
12
11
|
export * from './permissions/index.js';
|
|
13
12
|
export * from './plugin/index.js';
|
|
14
13
|
export * from './provider/index.js';
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
import { afterEach, describe, expect, it, vi } from 'vitest';
|
|
2
|
+
import type { IProviderDefinition } from '@robota-sdk/agent-core';
|
|
3
|
+
import { SystemCommandExecutor } from '@robota-sdk/agent-framework';
|
|
4
|
+
import type {
|
|
5
|
+
ICommandHostContext,
|
|
6
|
+
IOrgPolicy,
|
|
7
|
+
IProviderCommandSettingsAdapter,
|
|
8
|
+
TProviderSettingsDocument,
|
|
9
|
+
} from '@robota-sdk/agent-framework';
|
|
10
|
+
import { createProviderCommandModule } from '../provider-command-module.js';
|
|
11
|
+
|
|
12
|
+
const providerDefinitions: readonly IProviderDefinition[] = [
|
|
13
|
+
{
|
|
14
|
+
type: 'anthropic',
|
|
15
|
+
displayName: 'Anthropic',
|
|
16
|
+
defaults: { model: 'claude-sonnet-4-6', apiKey: '$ENV:ANTHROPIC_API_KEY' },
|
|
17
|
+
setupSteps: [
|
|
18
|
+
{ key: 'apiKey', title: 'Anthropic API key', masked: true },
|
|
19
|
+
{ key: 'model', title: 'Anthropic model', defaultValue: 'claude-sonnet-4-6' },
|
|
20
|
+
],
|
|
21
|
+
requiresApiKey: true,
|
|
22
|
+
createProvider: () => {
|
|
23
|
+
throw new Error('not used');
|
|
24
|
+
},
|
|
25
|
+
},
|
|
26
|
+
{
|
|
27
|
+
type: 'openai',
|
|
28
|
+
displayName: 'OpenAI',
|
|
29
|
+
defaults: { model: 'gpt-4o', apiKey: '$ENV:OPENAI_API_KEY' },
|
|
30
|
+
setupSteps: [
|
|
31
|
+
{ key: 'apiKey', title: 'OpenAI API key', masked: true },
|
|
32
|
+
{ key: 'model', title: 'OpenAI model', defaultValue: 'gpt-4o' },
|
|
33
|
+
],
|
|
34
|
+
requiresApiKey: true,
|
|
35
|
+
createProvider: () => {
|
|
36
|
+
throw new Error('not used');
|
|
37
|
+
},
|
|
38
|
+
},
|
|
39
|
+
];
|
|
40
|
+
|
|
41
|
+
function createSettingsAdapter(
|
|
42
|
+
merged: TProviderSettingsDocument,
|
|
43
|
+
target: TProviderSettingsDocument = {},
|
|
44
|
+
): { adapter: IProviderCommandSettingsAdapter } {
|
|
45
|
+
let writable = target;
|
|
46
|
+
return {
|
|
47
|
+
adapter: {
|
|
48
|
+
readMergedSettings: () => merged,
|
|
49
|
+
readTargetSettings: () => writable,
|
|
50
|
+
writeTargetSettings: (next) => {
|
|
51
|
+
writable = next;
|
|
52
|
+
},
|
|
53
|
+
},
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function createExecutor(
|
|
58
|
+
adapter: IProviderCommandSettingsAdapter,
|
|
59
|
+
orgPolicy?: IOrgPolicy,
|
|
60
|
+
): SystemCommandExecutor {
|
|
61
|
+
const module = createProviderCommandModule({ providerDefinitions, settings: adapter, orgPolicy });
|
|
62
|
+
return new SystemCommandExecutor([...(module.systemCommands ?? [])]);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const session = {} as ICommandHostContext;
|
|
66
|
+
|
|
67
|
+
describe('org policy enforcement in provider commands', () => {
|
|
68
|
+
afterEach(() => {
|
|
69
|
+
vi.unstubAllEnvs();
|
|
70
|
+
});
|
|
71
|
+
it('blocks /provider switch when target is not in allowedProviders', async () => {
|
|
72
|
+
const { adapter } = createSettingsAdapter({
|
|
73
|
+
currentProvider: 'anthropic',
|
|
74
|
+
providers: {
|
|
75
|
+
anthropic: { type: 'anthropic', model: 'claude-sonnet-4-6' },
|
|
76
|
+
openai: { type: 'openai', model: 'gpt-4o' },
|
|
77
|
+
},
|
|
78
|
+
});
|
|
79
|
+
const orgPolicy: IOrgPolicy = {
|
|
80
|
+
allowedProviders: ['anthropic'],
|
|
81
|
+
adminContact: 'admin@example.com',
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
const result = await createExecutor(adapter, orgPolicy).execute(
|
|
85
|
+
'provider',
|
|
86
|
+
session,
|
|
87
|
+
'switch openai',
|
|
88
|
+
);
|
|
89
|
+
|
|
90
|
+
expect(result?.success).toBe(false);
|
|
91
|
+
expect(result?.message).toContain('"openai" is not allowed');
|
|
92
|
+
expect(result?.message).toContain('admin@example.com');
|
|
93
|
+
expect(result?.effects).toBeUndefined();
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
it('allows /provider switch when target is in allowedProviders', async () => {
|
|
97
|
+
const { adapter } = createSettingsAdapter({
|
|
98
|
+
currentProvider: 'anthropic',
|
|
99
|
+
providers: {
|
|
100
|
+
anthropic: { type: 'anthropic', model: 'claude-sonnet-4-6' },
|
|
101
|
+
openai: { type: 'openai', model: 'gpt-4o' },
|
|
102
|
+
},
|
|
103
|
+
});
|
|
104
|
+
const orgPolicy: IOrgPolicy = { allowedProviders: ['anthropic', 'openai'] };
|
|
105
|
+
|
|
106
|
+
const result = await createExecutor(adapter, orgPolicy).execute(
|
|
107
|
+
'provider',
|
|
108
|
+
session,
|
|
109
|
+
'switch openai',
|
|
110
|
+
);
|
|
111
|
+
|
|
112
|
+
expect(result?.success).toBe(true);
|
|
113
|
+
expect(result?.effects).toEqual([
|
|
114
|
+
{ type: 'provider-hot-swap-requested', profileName: 'openai' },
|
|
115
|
+
]);
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
it('rejects plaintext API key during edit when requireApiKeyFromEnv is true', async () => {
|
|
119
|
+
const { adapter } = createSettingsAdapter({
|
|
120
|
+
currentProvider: 'anthropic',
|
|
121
|
+
providers: {
|
|
122
|
+
anthropic: {
|
|
123
|
+
type: 'anthropic',
|
|
124
|
+
model: 'claude-sonnet-4-6',
|
|
125
|
+
apiKey: 'sk-existing-key',
|
|
126
|
+
},
|
|
127
|
+
},
|
|
128
|
+
});
|
|
129
|
+
const orgPolicy: IOrgPolicy = { requireApiKeyFromEnv: true, adminContact: 'sec@example.com' };
|
|
130
|
+
|
|
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
|
+
// Anthropic has 2 setup steps: apiKey then model
|
|
135
|
+
const modelPrompt = await editRequested?.interaction?.submit('sk-plaintext-key');
|
|
136
|
+
const completed = await modelPrompt?.interaction?.submit('claude-sonnet-4-6');
|
|
137
|
+
|
|
138
|
+
expect(completed?.success).toBe(false);
|
|
139
|
+
expect(completed?.message).toContain('environment variable references');
|
|
140
|
+
expect(completed?.message).toContain('sec@example.com');
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
it('allows env-ref API key during edit when requireApiKeyFromEnv is true', async () => {
|
|
144
|
+
vi.stubEnv('ORG_TEST_KEY', 'sk-test-resolved-value');
|
|
145
|
+
const { adapter } = createSettingsAdapter({
|
|
146
|
+
currentProvider: 'anthropic',
|
|
147
|
+
providers: {
|
|
148
|
+
anthropic: {
|
|
149
|
+
type: 'anthropic',
|
|
150
|
+
model: 'claude-sonnet-4-6',
|
|
151
|
+
apiKey: 'sk-existing-key',
|
|
152
|
+
},
|
|
153
|
+
},
|
|
154
|
+
});
|
|
155
|
+
const orgPolicy: IOrgPolicy = { requireApiKeyFromEnv: true };
|
|
156
|
+
|
|
157
|
+
const listed = await createExecutor(adapter, orgPolicy).execute('provider', session, 'list');
|
|
158
|
+
const selected = await listed?.interaction?.submit('anthropic');
|
|
159
|
+
const editRequested = await selected?.interaction?.submit('edit');
|
|
160
|
+
const modelPrompt = await editRequested?.interaction?.submit('$ENV:ORG_TEST_KEY');
|
|
161
|
+
const completed = await modelPrompt?.interaction?.submit('claude-opus-4-5');
|
|
162
|
+
|
|
163
|
+
expect(completed?.success).toBe(true);
|
|
164
|
+
});
|
|
165
|
+
});
|
|
@@ -103,7 +103,7 @@ describe('createProviderCommandModule', () => {
|
|
|
103
103
|
|
|
104
104
|
expect(module.name).toBe('agent-command-provider');
|
|
105
105
|
expect(commands.map((command) => command.name)).toEqual(['provider']);
|
|
106
|
-
expect(commands[0]?.subcommands?.map((command) => command.name)).toContain('
|
|
106
|
+
expect(commands[0]?.subcommands?.map((command) => command.name)).toContain('switch');
|
|
107
107
|
expect(module.systemCommands?.map((command) => command.name)).toEqual(['provider']);
|
|
108
108
|
});
|
|
109
109
|
|
|
@@ -146,7 +146,7 @@ describe('createProviderCommandModule', () => {
|
|
|
146
146
|
});
|
|
147
147
|
});
|
|
148
148
|
|
|
149
|
-
it('
|
|
149
|
+
it('switches provider immediately via /provider switch without confirmation dialog', async () => {
|
|
150
150
|
const { adapter, readTarget } = createSettingsAdapter(
|
|
151
151
|
{
|
|
152
152
|
currentProvider: 'anthropic',
|
|
@@ -158,24 +158,19 @@ describe('createProviderCommandModule', () => {
|
|
|
158
158
|
{},
|
|
159
159
|
);
|
|
160
160
|
|
|
161
|
-
const result = await createExecutor(adapter).execute('provider', session, '
|
|
162
|
-
const submitted = await result?.interaction?.submit('yes');
|
|
161
|
+
const result = await createExecutor(adapter).execute('provider', session, 'switch openai');
|
|
163
162
|
|
|
164
|
-
expect(result?.interaction
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
163
|
+
expect(result?.interaction).toBeUndefined();
|
|
164
|
+
expect(result?.message).toBe(
|
|
165
|
+
'Switched to openai (supergemma4-26b-uncensored-v2). History preserved.',
|
|
166
|
+
);
|
|
168
167
|
expect(readTarget().currentProvider).toBe('openai');
|
|
169
|
-
expect(
|
|
170
|
-
{
|
|
171
|
-
type: 'session-restart-requested',
|
|
172
|
-
reason: 'other',
|
|
173
|
-
message: 'Provider change restart',
|
|
174
|
-
},
|
|
168
|
+
expect(result?.effects).toEqual([
|
|
169
|
+
{ type: 'provider-hot-swap-requested', profileName: 'openai' },
|
|
175
170
|
]);
|
|
176
171
|
});
|
|
177
172
|
|
|
178
|
-
it('switches from the provider profile action menu
|
|
173
|
+
it('switches from the provider profile action menu immediately without confirmation', async () => {
|
|
179
174
|
const { adapter, readTarget } = createSettingsAdapter(
|
|
180
175
|
{
|
|
181
176
|
currentProvider: 'anthropic',
|
|
@@ -190,16 +185,14 @@ describe('createProviderCommandModule', () => {
|
|
|
190
185
|
const listed = await createExecutor(adapter).execute('provider', session, 'list');
|
|
191
186
|
const selected = await listed?.interaction?.submit('openai');
|
|
192
187
|
const switchRequested = await selected?.interaction?.submit('switch');
|
|
193
|
-
const submitted = await switchRequested?.interaction?.submit('yes');
|
|
194
188
|
|
|
195
|
-
expect(switchRequested?.message).toBe(
|
|
189
|
+
expect(switchRequested?.message).toBe(
|
|
190
|
+
'Switched to openai (supergemma4-26b-uncensored-v2). History preserved.',
|
|
191
|
+
);
|
|
192
|
+
expect(switchRequested?.interaction).toBeUndefined();
|
|
196
193
|
expect(readTarget().currentProvider).toBe('openai');
|
|
197
|
-
expect(
|
|
198
|
-
{
|
|
199
|
-
type: 'session-restart-requested',
|
|
200
|
-
reason: 'other',
|
|
201
|
-
message: 'Provider change restart',
|
|
202
|
-
},
|
|
194
|
+
expect(switchRequested?.effects).toEqual([
|
|
195
|
+
{ type: 'provider-hot-swap-requested', profileName: 'openai' },
|
|
203
196
|
]);
|
|
204
197
|
});
|
|
205
198
|
|
|
@@ -241,13 +234,9 @@ describe('createProviderCommandModule', () => {
|
|
|
241
234
|
},
|
|
242
235
|
},
|
|
243
236
|
});
|
|
244
|
-
expect(completed?.message).toBe('Provider anthropic updated.
|
|
237
|
+
expect(completed?.message).toBe('Provider anthropic updated. Switching...');
|
|
245
238
|
expect(completed?.effects).toEqual([
|
|
246
|
-
{
|
|
247
|
-
type: 'session-restart-requested',
|
|
248
|
-
reason: 'other',
|
|
249
|
-
message: 'Provider edit restart',
|
|
250
|
-
},
|
|
239
|
+
{ type: 'provider-hot-swap-requested', profileName: 'anthropic' },
|
|
251
240
|
]);
|
|
252
241
|
});
|
|
253
242
|
|
|
@@ -82,8 +82,8 @@ describe('provider setup flow', () => {
|
|
|
82
82
|
expect(formatProviderSetupSelectionPrompt(providerDefinitions)).toBe(
|
|
83
83
|
[
|
|
84
84
|
' Select provider:',
|
|
85
|
-
' 1. OpenAI (openai)
|
|
86
|
-
' 2. Anthropic (anthropic)
|
|
85
|
+
' 1. OpenAI (openai) — Use OpenAI Responses API',
|
|
86
|
+
' 2. Anthropic (anthropic) — Use Claude models through Anthropic',
|
|
87
87
|
' Provider [1-2] (default: 1): ',
|
|
88
88
|
].join('\n'),
|
|
89
89
|
);
|
|
@@ -33,7 +33,7 @@ export async function executeProviderCommand(
|
|
|
33
33
|
success: true,
|
|
34
34
|
};
|
|
35
35
|
}
|
|
36
|
-
if (subcommand === '
|
|
36
|
+
if (subcommand === 'switch') {
|
|
37
37
|
return buildProviderSwitch(settings.providers, profileArg, options);
|
|
38
38
|
}
|
|
39
39
|
if (subcommand === 'test') {
|
|
@@ -49,7 +49,7 @@ export async function executeProviderCommand(
|
|
|
49
49
|
}
|
|
50
50
|
|
|
51
51
|
return {
|
|
52
|
-
message: 'Usage: provider [current|list|
|
|
52
|
+
message: 'Usage: provider [current|list|switch <profile>|add <type>|test [profile]]',
|
|
53
53
|
success: false,
|
|
54
54
|
};
|
|
55
55
|
}
|
|
@@ -14,7 +14,7 @@ function buildProviderSubcommands(): ICommand[] {
|
|
|
14
14
|
return [
|
|
15
15
|
{ name: 'current', description: 'Show current provider', source: 'provider' },
|
|
16
16
|
{ name: 'list', description: 'List provider profiles', source: 'provider' },
|
|
17
|
-
{ name: '
|
|
17
|
+
{ name: 'switch', description: 'Hot-swap to another provider profile', source: 'provider' },
|
|
18
18
|
{ name: 'add', description: 'Configure a provider profile', source: 'provider' },
|
|
19
19
|
{ name: 'test', description: 'Test provider profile', source: 'provider' },
|
|
20
20
|
];
|
|
@@ -27,8 +27,9 @@ export function createProviderCommandEntry(): ICommand {
|
|
|
27
27
|
description: 'Manage provider profiles',
|
|
28
28
|
source: 'provider',
|
|
29
29
|
modelInvocable: false,
|
|
30
|
-
argumentHint: 'current | list |
|
|
30
|
+
argumentHint: 'current | list | switch <profile> | add [type] | test [profile]',
|
|
31
31
|
subcommands: buildProviderSubcommands(),
|
|
32
|
+
example: '/provider switch production',
|
|
32
33
|
};
|
|
33
34
|
}
|
|
34
35
|
|
|
@@ -46,6 +47,7 @@ function createProviderSystemCommand(options: IProviderCommandModuleOptions): TS
|
|
|
46
47
|
name: entry.name,
|
|
47
48
|
displayName: entry.displayName,
|
|
48
49
|
description: entry.description,
|
|
50
|
+
example: entry.example,
|
|
49
51
|
requiresPermission: false,
|
|
50
52
|
userInvocable: true,
|
|
51
53
|
modelInvocable: false,
|