@robota-sdk/agent-command 3.0.0-beta.92 → 3.0.0-beta.94
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 +24 -0
- package/dist/node/index.cjs +78 -75
- package/dist/node/index.d.cts +13 -4
- package/dist/node/index.d.cts.map +1 -1
- package/dist/node/index.d.ts +13 -4
- package/dist/node/index.d.ts.map +1 -1
- package/dist/node/index.js +83 -80
- package/dist/node/index.js.map +1 -1
- package/package.json +7 -7
- package/src/default/default-command-modules.ts +15 -1
- package/src/model/model-command.ts +41 -16
- 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/provider-command-execution.ts +23 -5
- package/src/provider/provider-command-module.ts +8 -2
- package/src/provider/provider-command-profile-operations.ts +60 -12
- package/src/provider/provider-command-profile.ts +8 -0
- package/src/provider/provider-command-setup.ts +11 -60
- package/src/provider/provider-connection-setup.ts +287 -0
- package/src/provider/provider-connection-validation.ts +38 -0
- package/src/provider/provider-onboarding.ts +11 -2
- 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 +5 -1
- package/src/provider/provider-startup.ts +63 -8
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.94",
|
|
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-
|
|
44
|
-
"@robota-sdk/agent-
|
|
45
|
-
"@robota-sdk/agent-interface-
|
|
46
|
-
"@robota-sdk/agent-
|
|
47
|
-
"@robota-sdk/agent-interface-
|
|
48
|
-
"@robota-sdk/agent-
|
|
43
|
+
"@robota-sdk/agent-framework": "3.0.0-beta.94",
|
|
44
|
+
"@robota-sdk/agent-core": "3.0.0-beta.94",
|
|
45
|
+
"@robota-sdk/agent-interface-execution": "3.0.0-beta.94",
|
|
46
|
+
"@robota-sdk/agent-preset": "3.0.0-beta.94",
|
|
47
|
+
"@robota-sdk/agent-interface-command": "3.0.0-beta.94",
|
|
48
|
+
"@robota-sdk/agent-interface-session": "3.0.0-beta.94"
|
|
49
49
|
},
|
|
50
50
|
"devDependencies": {
|
|
51
51
|
"@types/node": "^22.19.21",
|
|
@@ -45,13 +45,14 @@ import { createUserLocalCommandModule } from '../user-local/index.js';
|
|
|
45
45
|
import type { IDevicesCommandPort } from '../devices/index.js';
|
|
46
46
|
import type { IDoctorDisplayVocabulary, IDoctorInputs } from '../doctor/index.js';
|
|
47
47
|
import type { IKeybindingsFilePort } from '../keybindings/index.js';
|
|
48
|
-
import type { IProviderDefinition } from '@robota-sdk/agent-core';
|
|
48
|
+
import type { IProviderDefinition, TProviderCredentialResolver } from '@robota-sdk/agent-core';
|
|
49
49
|
import type {
|
|
50
50
|
IOrgPolicy,
|
|
51
51
|
IContributionSource,
|
|
52
52
|
ISkillRootDescriptor,
|
|
53
53
|
ICommandModule,
|
|
54
54
|
IProviderCommandSettingsAdapter,
|
|
55
|
+
IProviderConnectionHost,
|
|
55
56
|
IUnknownCommandModuleName,
|
|
56
57
|
} from '@robota-sdk/agent-framework';
|
|
57
58
|
import type { IThemeCataloguePort } from '@robota-sdk/agent-interface-command';
|
|
@@ -79,6 +80,10 @@ export interface IDefaultCommandModulesOptions {
|
|
|
79
80
|
* snapshot when it withholds its own credentials from `process.env`. Absent, `process.env` is read.
|
|
80
81
|
*/
|
|
81
82
|
providerEnvironment?: Readonly<Record<string, string | undefined>>;
|
|
83
|
+
/** Host-owned connection acquisition and transactional credential persistence. */
|
|
84
|
+
providerConnectionHost?: IProviderConnectionHost;
|
|
85
|
+
/** Host-owned resolution of an opaque saved credential reference. */
|
|
86
|
+
providerCredentialResolver?: TProviderCredentialResolver;
|
|
82
87
|
/** Optional TUI-owned file capability; absent, `/keybindings` says it belongs to an interactive terminal. */
|
|
83
88
|
keybindingsFilePort?: IKeybindingsFilePort;
|
|
84
89
|
/**
|
|
@@ -158,6 +163,8 @@ export function createDefaultCommandModules({
|
|
|
158
163
|
enabledCommandModules,
|
|
159
164
|
disabledCommandModules,
|
|
160
165
|
providerEnvironment,
|
|
166
|
+
providerConnectionHost,
|
|
167
|
+
providerCredentialResolver,
|
|
161
168
|
}: IDefaultCommandModulesOptions): IDefaultCommandModulesResult {
|
|
162
169
|
const modules: readonly ICommandModule[] = [
|
|
163
170
|
createSkillsCommandModule({ contributionSources: contributionSources ?? [], skillRoots }),
|
|
@@ -211,6 +218,10 @@ export function createDefaultCommandModules({
|
|
|
211
218
|
settings: providerSettingsAdapter,
|
|
212
219
|
...(orgPolicy === undefined ? {} : { orgPolicy }),
|
|
213
220
|
...(providerEnvironment === undefined ? {} : { env: providerEnvironment }),
|
|
221
|
+
...(providerConnectionHost === undefined ? {} : { connectionHost: providerConnectionHost }),
|
|
222
|
+
...(providerCredentialResolver === undefined
|
|
223
|
+
? {}
|
|
224
|
+
: { resolveCredential: providerCredentialResolver }),
|
|
214
225
|
}),
|
|
215
226
|
// #3282 §2: shares the provider command's settings adapter and definitions — one settings write
|
|
216
227
|
// owner, never a second copy that could drift from what `/provider switch` reads and writes.
|
|
@@ -219,6 +230,9 @@ export function createDefaultCommandModules({
|
|
|
219
230
|
settings: providerSettingsAdapter,
|
|
220
231
|
...(orgPolicy === undefined ? {} : { orgPolicy }),
|
|
221
232
|
...(providerEnvironment === undefined ? {} : { env: providerEnvironment }),
|
|
233
|
+
...(providerCredentialResolver === undefined
|
|
234
|
+
? {}
|
|
235
|
+
: { resolveCredential: providerCredentialResolver }),
|
|
222
236
|
}),
|
|
223
237
|
];
|
|
224
238
|
const builtModuleNames = modules.map((module) => module.name);
|
|
@@ -5,8 +5,8 @@ import {
|
|
|
5
5
|
resolveModelListSelection,
|
|
6
6
|
setCurrentProvider,
|
|
7
7
|
upsertProviderProfile,
|
|
8
|
-
validateProviderProfile,
|
|
9
8
|
} from '@robota-sdk/agent-framework';
|
|
9
|
+
import { validateProviderConnection } from '../provider/provider-connection-validation.js';
|
|
10
10
|
|
|
11
11
|
import type { IUserInteraction } from '@robota-sdk/agent-core';
|
|
12
12
|
import type {
|
|
@@ -45,7 +45,9 @@ function buildSnapshot(
|
|
|
45
45
|
allowFiltered: boolean,
|
|
46
46
|
): IModelListSnapshot {
|
|
47
47
|
const currentProfile =
|
|
48
|
-
settings.currentProvider !== undefined
|
|
48
|
+
settings.currentProvider !== undefined
|
|
49
|
+
? settings.providers?.[settings.currentProvider]
|
|
50
|
+
: undefined;
|
|
49
51
|
return buildModelListSnapshot(
|
|
50
52
|
settings.providers,
|
|
51
53
|
settings.currentProvider,
|
|
@@ -95,7 +97,10 @@ async function pickModel(
|
|
|
95
97
|
options: IProviderCommandModuleOptions,
|
|
96
98
|
): Promise<ICommandResult> {
|
|
97
99
|
if (snapshot.groups.length === 0) {
|
|
98
|
-
return {
|
|
100
|
+
return {
|
|
101
|
+
message: 'No provider profiles configured. Run /provider add to add one.',
|
|
102
|
+
success: true,
|
|
103
|
+
};
|
|
99
104
|
}
|
|
100
105
|
if (!ui) {
|
|
101
106
|
return { message: formatModelListText(snapshot), success: true };
|
|
@@ -106,7 +111,9 @@ async function pickModel(
|
|
|
106
111
|
label: formatModelOptionLabel(snapshot, group.profileName, group.providerLabel, model),
|
|
107
112
|
})),
|
|
108
113
|
);
|
|
109
|
-
const response = await ui.ask(
|
|
114
|
+
const response = await ui.ask(
|
|
115
|
+
selectAction('model', 'Select model', modelOptions, { maxVisible: 8 }),
|
|
116
|
+
);
|
|
110
117
|
if (response.type !== 'answer' || response.values[0] === undefined) {
|
|
111
118
|
return { message: 'Model selection cancelled.', success: true };
|
|
112
119
|
}
|
|
@@ -119,7 +126,9 @@ function formatModelListText(snapshot: IModelListSnapshot): string {
|
|
|
119
126
|
lines.push(`${group.providerLabel} (${group.profileName})`);
|
|
120
127
|
for (const model of group.models) {
|
|
121
128
|
const marker =
|
|
122
|
-
model.id === snapshot.currentModel && group.profileName === snapshot.currentProfile
|
|
129
|
+
model.id === snapshot.currentModel && group.profileName === snapshot.currentProfile
|
|
130
|
+
? '*'
|
|
131
|
+
: '-';
|
|
123
132
|
lines.push(` ${marker} ${model.label}`);
|
|
124
133
|
}
|
|
125
134
|
}
|
|
@@ -132,8 +141,11 @@ function formatModelOptionLabel(
|
|
|
132
141
|
providerLabel: string,
|
|
133
142
|
model: { id: string; label: string },
|
|
134
143
|
): string {
|
|
135
|
-
const marker =
|
|
136
|
-
|
|
144
|
+
const marker =
|
|
145
|
+
model.id === snapshot.currentModel && profileName === snapshot.currentProfile ? '* ' : '';
|
|
146
|
+
const sharedLabelCount = snapshot.groups.filter(
|
|
147
|
+
(group) => group.providerLabel === providerLabel,
|
|
148
|
+
).length;
|
|
137
149
|
const disambiguator = sharedLabelCount > 1 ? ` (${profileName})` : '';
|
|
138
150
|
return `${marker}${providerLabel}: ${model.label}${disambiguator}`;
|
|
139
151
|
}
|
|
@@ -161,9 +173,13 @@ async function switchModelWithinCurrentProfile(
|
|
|
161
173
|
const target = options.settings.readTargetSettings();
|
|
162
174
|
// Based on MERGED (not target-only) so a profile that lives in a lower-priority settings layer
|
|
163
175
|
// keeps its other fields (type, apiKey, baseURL) — only `model` changes at the target layer.
|
|
164
|
-
const currentProfile =
|
|
176
|
+
const currentProfile =
|
|
177
|
+
settings.providers?.[currentProfileName] ?? target.providers?.[currentProfileName] ?? {};
|
|
165
178
|
options.settings.writeTargetSettings(
|
|
166
|
-
upsertProviderProfile(target, currentProfileName, {
|
|
179
|
+
upsertProviderProfile(target, currentProfileName, {
|
|
180
|
+
...currentProfile,
|
|
181
|
+
model: selection.model.id,
|
|
182
|
+
}),
|
|
167
183
|
);
|
|
168
184
|
return { message: `Model: ${selection.model.label}`, success: true };
|
|
169
185
|
}
|
|
@@ -174,11 +190,11 @@ async function switchModelWithinCurrentProfile(
|
|
|
174
190
|
* settings write, then hot-swap through the existing `provider-hot-swap` host action — which re-reads
|
|
175
191
|
* settings at execution time, so it picks up the model this write just persisted.
|
|
176
192
|
*/
|
|
177
|
-
function switchModelAcrossProfiles(
|
|
193
|
+
async function switchModelAcrossProfiles(
|
|
178
194
|
selection: IModelListSelection,
|
|
179
195
|
settings: TProviderSettingsDocument,
|
|
180
196
|
options: IProviderCommandModuleOptions,
|
|
181
|
-
): ICommandResult {
|
|
197
|
+
): Promise<ICommandResult> {
|
|
182
198
|
const { orgPolicy } = options;
|
|
183
199
|
// Checked BEFORE anything is read or written — exactly buildProviderSwitch's order — so a switch
|
|
184
200
|
// org policy refuses changes nothing on disk, the same guarantee /provider switch already gives.
|
|
@@ -193,14 +209,14 @@ function switchModelAcrossProfiles(
|
|
|
193
209
|
}
|
|
194
210
|
const profile = settings.providers?.[selection.profileName];
|
|
195
211
|
if (profile === undefined) {
|
|
196
|
-
return {
|
|
212
|
+
return {
|
|
213
|
+
message: `Provider profile "${selection.profileName}" was not found.`,
|
|
214
|
+
success: false,
|
|
215
|
+
};
|
|
197
216
|
}
|
|
198
217
|
const updatedProfile: IProviderProfileSettings = { ...profile, model: selection.model.id };
|
|
199
218
|
try {
|
|
200
|
-
|
|
201
|
-
providerDefinitions: options.providerDefinitions,
|
|
202
|
-
...(options.env === undefined ? {} : { env: options.env }),
|
|
203
|
-
});
|
|
219
|
+
await validateProviderConnection(selection.profileName, updatedProfile, options);
|
|
204
220
|
} catch (error) {
|
|
205
221
|
return {
|
|
206
222
|
message: `Could not switch to "${selection.model.label}": ${error instanceof Error ? error.message : String(error)}`,
|
|
@@ -209,6 +225,15 @@ function switchModelAcrossProfiles(
|
|
|
209
225
|
}
|
|
210
226
|
const target = options.settings.readTargetSettings();
|
|
211
227
|
const merged = options.settings.readMergedSettings();
|
|
228
|
+
if (
|
|
229
|
+
merged.currentProvider !== settings.currentProvider ||
|
|
230
|
+
JSON.stringify(merged.providers?.[selection.profileName]) !== JSON.stringify(profile)
|
|
231
|
+
) {
|
|
232
|
+
return {
|
|
233
|
+
success: false,
|
|
234
|
+
message: 'Provider settings changed while switching. Run /model again.',
|
|
235
|
+
};
|
|
236
|
+
}
|
|
212
237
|
const withModel = upsertProviderProfile(target, selection.profileName, updatedProfile);
|
|
213
238
|
const next =
|
|
214
239
|
target.providers?.[selection.profileName] !== undefined ||
|