@robota-sdk/agent-command 3.0.0-beta.90 → 3.0.0-beta.92
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 +23 -0
- package/dist/node/index.cjs +3 -3
- package/dist/node/index.d.cts +1 -0
- package/dist/node/index.d.cts.map +1 -1
- package/dist/node/index.d.ts +1 -0
- package/dist/node/index.d.ts.map +1 -1
- package/dist/node/index.js +3 -3
- package/dist/node/index.js.map +1 -1
- package/package.json +7 -7
- package/src/provider/__tests__/optional-provider-credentials.test.ts +130 -0
- package/src/provider/provider-command-module.ts +9 -2
- package/src/provider/provider-command-profile-operations.ts +1 -0
- package/src/provider/provider-command-setup.ts +2 -1
- package/src/provider/provider-setup-flow.ts +4 -1
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.92",
|
|
4
4
|
"description": "Slash-command modules for Robota SDK sessions (/help, /permissions, /mcp, /rewind, …)",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/node/index.js",
|
|
@@ -40,12 +40,12 @@
|
|
|
40
40
|
"CHANGELOG.md"
|
|
41
41
|
],
|
|
42
42
|
"dependencies": {
|
|
43
|
-
"@robota-sdk/agent-core": "3.0.0-beta.
|
|
44
|
-
"@robota-sdk/agent-framework": "3.0.0-beta.
|
|
45
|
-
"@robota-sdk/agent-interface-command": "3.0.0-beta.
|
|
46
|
-
"@robota-sdk/agent-interface-execution": "3.0.0-beta.
|
|
47
|
-
"@robota-sdk/agent-interface-session": "3.0.0-beta.
|
|
48
|
-
"@robota-sdk/agent-preset": "3.0.0-beta.
|
|
43
|
+
"@robota-sdk/agent-core": "3.0.0-beta.92",
|
|
44
|
+
"@robota-sdk/agent-framework": "3.0.0-beta.92",
|
|
45
|
+
"@robota-sdk/agent-interface-command": "3.0.0-beta.92",
|
|
46
|
+
"@robota-sdk/agent-interface-execution": "3.0.0-beta.92",
|
|
47
|
+
"@robota-sdk/agent-interface-session": "3.0.0-beta.92",
|
|
48
|
+
"@robota-sdk/agent-preset": "3.0.0-beta.92"
|
|
49
49
|
},
|
|
50
50
|
"devDependencies": {
|
|
51
51
|
"@types/node": "^22.19.21",
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import type { IProviderDefinition } from '@robota-sdk/agent-core';
|
|
3
|
+
import { SystemCommandExecutor } from '@robota-sdk/agent-framework';
|
|
4
|
+
import type { TProviderSettingsDocument } from '@robota-sdk/agent-framework';
|
|
5
|
+
import { createProviderCommandModule } from '../provider-command-module.js';
|
|
6
|
+
import { scriptedContext } from './scripted-interaction.js';
|
|
7
|
+
|
|
8
|
+
const localDefinition: IProviderDefinition = {
|
|
9
|
+
type: 'local',
|
|
10
|
+
defaults: { model: 'installed-model', baseURL: 'http://localhost:1234/v1' },
|
|
11
|
+
setupSteps: [
|
|
12
|
+
{ key: 'baseURL', title: 'Server URL', defaultValue: 'http://localhost:1234/v1' },
|
|
13
|
+
{ key: 'model', title: 'Installed model', defaultValue: 'installed-model' },
|
|
14
|
+
{ key: 'apiKey', title: 'Server API key (optional)', masked: true, editOnly: true },
|
|
15
|
+
],
|
|
16
|
+
requiresApiKey: false,
|
|
17
|
+
createProvider: () => {
|
|
18
|
+
throw new Error('not used');
|
|
19
|
+
},
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
function commands(initial: TProviderSettingsDocument = {}) {
|
|
23
|
+
let settings = initial;
|
|
24
|
+
const module = createProviderCommandModule({
|
|
25
|
+
providerDefinitions: [localDefinition],
|
|
26
|
+
env: { LOCAL_TOKEN: 'synthetic-selected-token' },
|
|
27
|
+
settings: {
|
|
28
|
+
readMergedSettings: () => settings,
|
|
29
|
+
readTargetSettings: () => settings,
|
|
30
|
+
writeTargetSettings: (next) => {
|
|
31
|
+
settings = next;
|
|
32
|
+
},
|
|
33
|
+
},
|
|
34
|
+
});
|
|
35
|
+
return {
|
|
36
|
+
executor: new SystemCommandExecutor([...(module.systemCommands ?? [])]),
|
|
37
|
+
read: () => settings,
|
|
38
|
+
module,
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
describe('optional provider credentials', () => {
|
|
43
|
+
it('adds a keyless profile after only endpoint and model answers', async () => {
|
|
44
|
+
const { executor, read } = commands();
|
|
45
|
+
const { context, requests } = scriptedContext([
|
|
46
|
+
{ type: 'answer', values: [], text: '' },
|
|
47
|
+
{ type: 'answer', values: [], text: '' },
|
|
48
|
+
]);
|
|
49
|
+
|
|
50
|
+
const result = await executor.execute('provider', context, 'add local');
|
|
51
|
+
|
|
52
|
+
expect(result?.success).toBe(true);
|
|
53
|
+
expect(requests.map((request) => request.id)).toEqual([
|
|
54
|
+
'provider-setup-baseURL',
|
|
55
|
+
'provider-setup-model',
|
|
56
|
+
]);
|
|
57
|
+
expect(read().providers?.local).toEqual({
|
|
58
|
+
type: 'local',
|
|
59
|
+
model: 'installed-model',
|
|
60
|
+
baseURL: 'http://localhost:1234/v1',
|
|
61
|
+
});
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
it.each([undefined, '$ENV:LOCAL_TOKEN', 'synthetic-existing-token'])(
|
|
65
|
+
'allows blank optional key editing and preserves the previous credential %s',
|
|
66
|
+
async (apiKey) => {
|
|
67
|
+
const profile = {
|
|
68
|
+
type: 'local',
|
|
69
|
+
model: 'installed-model',
|
|
70
|
+
baseURL: 'http://localhost:1234/v1',
|
|
71
|
+
...(apiKey === undefined ? {} : { apiKey }),
|
|
72
|
+
};
|
|
73
|
+
const { executor, read } = commands({ providers: { local: profile } });
|
|
74
|
+
const { context, requests } = scriptedContext([
|
|
75
|
+
{ type: 'answer', values: [], text: '' },
|
|
76
|
+
{ type: 'answer', values: [], text: 'another-installed-model' },
|
|
77
|
+
{ type: 'answer', values: [], text: '' },
|
|
78
|
+
]);
|
|
79
|
+
|
|
80
|
+
const result = await executor.execute('provider', context, 'edit local');
|
|
81
|
+
|
|
82
|
+
expect(result?.success).toBe(true);
|
|
83
|
+
expect(requests[2]).toMatchObject({
|
|
84
|
+
id: 'provider-setup-apiKey',
|
|
85
|
+
masked: true,
|
|
86
|
+
allowEmpty: true,
|
|
87
|
+
});
|
|
88
|
+
expect(JSON.stringify(requests)).not.toContain('synthetic-existing-token');
|
|
89
|
+
expect(read().providers?.local).toEqual({ ...profile, model: 'another-installed-model' });
|
|
90
|
+
},
|
|
91
|
+
);
|
|
92
|
+
|
|
93
|
+
it('keeps credential management user-only and advertises the edit recovery action', () => {
|
|
94
|
+
const { module } = commands();
|
|
95
|
+
const entry = module.commandSources?.[0]?.getCommands()[0];
|
|
96
|
+
expect(entry).toMatchObject({ modelInvocable: false });
|
|
97
|
+
expect(module.systemCommands?.[0]).toMatchObject({
|
|
98
|
+
modelInvocable: false,
|
|
99
|
+
userInvocable: true,
|
|
100
|
+
});
|
|
101
|
+
expect(entry?.argumentHint).toContain('edit <profile>');
|
|
102
|
+
expect(entry?.description).toContain('credentials');
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
it.each(['$ENV:MISSING_SERVER_TOKEN', '$ENV:'])(
|
|
106
|
+
'leaves the working profile untouched when an edit supplies %s',
|
|
107
|
+
async (apiKey) => {
|
|
108
|
+
const initial = {
|
|
109
|
+
providers: {
|
|
110
|
+
local: {
|
|
111
|
+
type: 'local',
|
|
112
|
+
model: 'installed-model',
|
|
113
|
+
baseURL: 'http://localhost:1234/v1',
|
|
114
|
+
apiKey: 'synthetic-working-token',
|
|
115
|
+
},
|
|
116
|
+
},
|
|
117
|
+
};
|
|
118
|
+
const { executor, read } = commands(initial);
|
|
119
|
+
const { context } = scriptedContext([
|
|
120
|
+
{ type: 'answer', values: [], text: '' },
|
|
121
|
+
{ type: 'answer', values: [], text: '' },
|
|
122
|
+
{ type: 'answer', values: [], text: apiKey },
|
|
123
|
+
]);
|
|
124
|
+
await expect(executor.execute('provider', context, 'edit local')).rejects.toThrow(
|
|
125
|
+
'missing apiKey',
|
|
126
|
+
);
|
|
127
|
+
expect(read()).toEqual(initial);
|
|
128
|
+
},
|
|
129
|
+
);
|
|
130
|
+
});
|
|
@@ -15,6 +15,11 @@ function buildProviderSubcommands(): ICommand[] {
|
|
|
15
15
|
{ name: 'list', description: 'List provider profiles', source: 'provider' },
|
|
16
16
|
{ name: 'switch', description: 'Hot-swap to another provider profile', source: 'provider' },
|
|
17
17
|
{ name: 'add', description: 'Configure a provider profile', source: 'provider' },
|
|
18
|
+
{
|
|
19
|
+
name: 'edit',
|
|
20
|
+
description: 'Edit a profile, including optional server credentials',
|
|
21
|
+
source: 'provider',
|
|
22
|
+
},
|
|
18
23
|
{ name: 'test', description: 'Test provider profile', source: 'provider' },
|
|
19
24
|
];
|
|
20
25
|
}
|
|
@@ -23,11 +28,13 @@ export function createProviderCommandEntry(): ICommand {
|
|
|
23
28
|
return {
|
|
24
29
|
name: 'provider',
|
|
25
30
|
displayName: 'Provider Setup',
|
|
26
|
-
description:
|
|
31
|
+
description:
|
|
32
|
+
'Show, configure or switch provider profiles. Suggest editing a profile when its URL, model or credentials need correction; returns profile details or the result of the change.',
|
|
27
33
|
source: 'provider',
|
|
28
34
|
// User-only: provider profiles hold account credentials; a credential action.
|
|
29
35
|
modelInvocable: false,
|
|
30
|
-
argumentHint:
|
|
36
|
+
argumentHint:
|
|
37
|
+
'current | list | switch <profile> | add [type] | edit <profile> | test [profile]',
|
|
31
38
|
subcommands: buildProviderSubcommands(),
|
|
32
39
|
example: '/provider switch production',
|
|
33
40
|
};
|
|
@@ -99,6 +99,7 @@ export async function buildProviderEdit(
|
|
|
99
99
|
let flow;
|
|
100
100
|
try {
|
|
101
101
|
flow = createProviderSetupFlow(profile.type, options.providerDefinitions, {
|
|
102
|
+
includeEditOnlySteps: true,
|
|
102
103
|
profileName,
|
|
103
104
|
setCurrent: false,
|
|
104
105
|
initialValues: getProviderProfileSetupValues(profile),
|
|
@@ -46,7 +46,8 @@ function toProviderSetupStepRequest(
|
|
|
46
46
|
return textAction(`provider-setup-${step.key}`, step.title, {
|
|
47
47
|
description,
|
|
48
48
|
placeholder,
|
|
49
|
-
allowEmpty:
|
|
49
|
+
allowEmpty:
|
|
50
|
+
step.defaultValue !== undefined || (step.editOnly === true && step.required !== true),
|
|
50
51
|
masked: step.masked,
|
|
51
52
|
});
|
|
52
53
|
}
|
|
@@ -28,6 +28,7 @@ export interface IProviderSetupFlowState {
|
|
|
28
28
|
}
|
|
29
29
|
|
|
30
30
|
export interface IProviderSetupFlowOptions {
|
|
31
|
+
includeEditOnlySteps?: boolean;
|
|
31
32
|
existingProfileNames?: readonly string[];
|
|
32
33
|
initialValues?: Partial<Record<TProviderSetupField, string>>;
|
|
33
34
|
profileName?: string;
|
|
@@ -58,7 +59,9 @@ export function createProviderSetupFlow(
|
|
|
58
59
|
return {
|
|
59
60
|
type,
|
|
60
61
|
steps: applyProviderSetupInitialValues(
|
|
61
|
-
getProviderSetupSteps(definition)
|
|
62
|
+
getProviderSetupSteps(definition).filter(
|
|
63
|
+
(step) => step.editOnly !== true || options.includeEditOnlySteps === true,
|
|
64
|
+
),
|
|
62
65
|
options.initialValues,
|
|
63
66
|
),
|
|
64
67
|
setupHelpLinks: definition.setupHelpLinks ?? [],
|