@robota-sdk/agent-command 3.0.0-beta.92 → 3.0.0-beta.93

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.92",
3
+ "version": "3.0.0-beta.93",
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.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"
43
+ "@robota-sdk/agent-core": "3.0.0-beta.93",
44
+ "@robota-sdk/agent-framework": "3.0.0-beta.93",
45
+ "@robota-sdk/agent-interface-execution": "3.0.0-beta.93",
46
+ "@robota-sdk/agent-interface-command": "3.0.0-beta.93",
47
+ "@robota-sdk/agent-interface-session": "3.0.0-beta.93",
48
+ "@robota-sdk/agent-preset": "3.0.0-beta.93"
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 ? settings.providers?.[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 { message: 'No provider profiles configured. Run /provider add to add one.', success: true };
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(selectAction('model', 'Select model', modelOptions, { maxVisible: 8 }));
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 = model.id === snapshot.currentModel && profileName === snapshot.currentProfile ? '* ' : '';
136
- const sharedLabelCount = snapshot.groups.filter((group) => group.providerLabel === providerLabel).length;
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 = settings.providers?.[currentProfileName] ?? target.providers?.[currentProfileName] ?? {};
176
+ const currentProfile =
177
+ settings.providers?.[currentProfileName] ?? target.providers?.[currentProfileName] ?? {};
165
178
  options.settings.writeTargetSettings(
166
- upsertProviderProfile(target, currentProfileName, { ...currentProfile, model: selection.model.id }),
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 { message: `Provider profile "${selection.profileName}" was not found.`, success: false };
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
- validateProviderProfile(selection.profileName, updatedProfile, {
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 ||