@robota-sdk/agent-command 3.0.0-beta.76 → 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 +38 -33
- package/dist/node/index.d.ts +68 -6
- 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/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 +36 -0
- package/src/preset/preset-command-module.ts +1 -18
- package/src/preset/preset-command.ts +37 -8
- 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/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
|
@@ -7,6 +7,7 @@ import type {
|
|
|
7
7
|
TProviderSettingsDocument,
|
|
8
8
|
} from '@robota-sdk/agent-framework';
|
|
9
9
|
import { createProviderCommandModule } from '../provider-command-module.js';
|
|
10
|
+
import { scriptedContext } from './scripted-interaction.js';
|
|
10
11
|
|
|
11
12
|
const providerDefinitions: readonly IProviderDefinition[] = [
|
|
12
13
|
{
|
|
@@ -93,7 +94,8 @@ function createExecutor(adapter: IProviderCommandSettingsAdapter): SystemCommand
|
|
|
93
94
|
return new SystemCommandExecutor([...(module.systemCommands ?? [])]);
|
|
94
95
|
}
|
|
95
96
|
|
|
96
|
-
|
|
97
|
+
/** Context with no interactive renderer attached (headless/automation). */
|
|
98
|
+
const headlessContext = {} as ICommandHostContext;
|
|
97
99
|
|
|
98
100
|
describe('createProviderCommandModule', () => {
|
|
99
101
|
it('contributes /provider metadata and executable command from the provider package', () => {
|
|
@@ -107,7 +109,7 @@ describe('createProviderCommandModule', () => {
|
|
|
107
109
|
expect(module.systemCommands?.map((command) => command.name)).toEqual(['provider']);
|
|
108
110
|
});
|
|
109
111
|
|
|
110
|
-
it('
|
|
112
|
+
it('asks the user to pick a provider profile from injected merged settings', async () => {
|
|
111
113
|
const { adapter } = createSettingsAdapter({
|
|
112
114
|
currentProvider: 'openai',
|
|
113
115
|
providers: {
|
|
@@ -116,18 +118,21 @@ describe('createProviderCommandModule', () => {
|
|
|
116
118
|
},
|
|
117
119
|
});
|
|
118
120
|
|
|
119
|
-
const
|
|
121
|
+
const { context, requests } = scriptedContext([{ type: 'cancelled' }]);
|
|
122
|
+
const result = await createExecutor(adapter).execute('provider', context, 'list');
|
|
120
123
|
|
|
121
124
|
expect(result?.success).toBe(true);
|
|
122
|
-
|
|
123
|
-
expect(
|
|
124
|
-
expect(
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
125
|
+
const picker = requests[0];
|
|
126
|
+
expect(picker?.title).toBe('Select provider profile');
|
|
127
|
+
expect(picker?.options?.map((option) => option.label)).toEqual(
|
|
128
|
+
expect.arrayContaining([
|
|
129
|
+
expect.stringContaining('* openai'),
|
|
130
|
+
expect.stringContaining('anthropic'),
|
|
131
|
+
]),
|
|
132
|
+
);
|
|
128
133
|
});
|
|
129
134
|
|
|
130
|
-
it('
|
|
135
|
+
it('returns a plain text list when no interactive renderer is attached', async () => {
|
|
131
136
|
const { adapter } = createSettingsAdapter({
|
|
132
137
|
currentProvider: 'openai',
|
|
133
138
|
providers: {
|
|
@@ -136,14 +141,31 @@ describe('createProviderCommandModule', () => {
|
|
|
136
141
|
},
|
|
137
142
|
});
|
|
138
143
|
|
|
139
|
-
const result = await createExecutor(adapter).execute('provider',
|
|
140
|
-
const selected = await result?.interaction?.submit('anthropic');
|
|
144
|
+
const result = await createExecutor(adapter).execute('provider', headlessContext, 'list');
|
|
141
145
|
|
|
146
|
+
expect(result?.success).toBe(true);
|
|
142
147
|
expect(result?.message).toContain('* openai');
|
|
143
|
-
expect(
|
|
144
|
-
|
|
145
|
-
|
|
148
|
+
expect(result?.message).toContain('anthropic');
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
it('opens a provider profile action menu from /provider after picking a profile', async () => {
|
|
152
|
+
const { adapter } = createSettingsAdapter({
|
|
153
|
+
currentProvider: 'openai',
|
|
154
|
+
providers: {
|
|
155
|
+
openai: { type: 'openai', model: 'supergemma4-26b-uncensored-v2' },
|
|
156
|
+
anthropic: { type: 'anthropic', model: 'claude-sonnet-4-6' },
|
|
157
|
+
},
|
|
146
158
|
});
|
|
159
|
+
|
|
160
|
+
const { context, requests } = scriptedContext([
|
|
161
|
+
{ type: 'answer', values: ['anthropic'] },
|
|
162
|
+
{ type: 'cancelled' },
|
|
163
|
+
]);
|
|
164
|
+
const result = await createExecutor(adapter).execute('provider', context, '');
|
|
165
|
+
|
|
166
|
+
expect(result?.success).toBe(true);
|
|
167
|
+
expect(requests[0]?.title).toBe('Select provider profile');
|
|
168
|
+
expect(requests[1]?.title).toBe('Provider profile: anthropic');
|
|
147
169
|
});
|
|
148
170
|
|
|
149
171
|
it('switches provider immediately via /provider switch without confirmation dialog', async () => {
|
|
@@ -158,9 +180,12 @@ describe('createProviderCommandModule', () => {
|
|
|
158
180
|
{},
|
|
159
181
|
);
|
|
160
182
|
|
|
161
|
-
const result = await createExecutor(adapter).execute(
|
|
183
|
+
const result = await createExecutor(adapter).execute(
|
|
184
|
+
'provider',
|
|
185
|
+
headlessContext,
|
|
186
|
+
'switch openai',
|
|
187
|
+
);
|
|
162
188
|
|
|
163
|
-
expect(result?.interaction).toBeUndefined();
|
|
164
189
|
expect(result?.message).toBe(
|
|
165
190
|
'Switched to openai (supergemma4-26b-uncensored-v2). History preserved.',
|
|
166
191
|
);
|
|
@@ -182,16 +207,17 @@ describe('createProviderCommandModule', () => {
|
|
|
182
207
|
{},
|
|
183
208
|
);
|
|
184
209
|
|
|
185
|
-
const
|
|
186
|
-
|
|
187
|
-
|
|
210
|
+
const { context } = scriptedContext([
|
|
211
|
+
{ type: 'answer', values: ['openai'] },
|
|
212
|
+
{ type: 'answer', values: ['switch'] },
|
|
213
|
+
]);
|
|
214
|
+
const result = await createExecutor(adapter).execute('provider', context, 'list');
|
|
188
215
|
|
|
189
|
-
expect(
|
|
216
|
+
expect(result?.message).toBe(
|
|
190
217
|
'Switched to openai (supergemma4-26b-uncensored-v2). History preserved.',
|
|
191
218
|
);
|
|
192
|
-
expect(switchRequested?.interaction).toBeUndefined();
|
|
193
219
|
expect(readTarget().currentProvider).toBe('openai');
|
|
194
|
-
expect(
|
|
220
|
+
expect(result?.effects).toEqual([
|
|
195
221
|
{ type: 'provider-hot-swap-requested', profileName: 'openai' },
|
|
196
222
|
]);
|
|
197
223
|
});
|
|
@@ -211,19 +237,19 @@ describe('createProviderCommandModule', () => {
|
|
|
211
237
|
{},
|
|
212
238
|
);
|
|
213
239
|
|
|
214
|
-
const
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
placeholder: '(unchanged)',
|
|
222
|
-
masked: true,
|
|
223
|
-
});
|
|
240
|
+
const { context, requests } = scriptedContext([
|
|
241
|
+
{ type: 'answer', values: ['anthropic'] },
|
|
242
|
+
{ type: 'answer', values: ['edit'] },
|
|
243
|
+
{ type: 'answer', values: [], text: '' },
|
|
244
|
+
{ type: 'answer', values: [], text: 'claude-opus-4-5' },
|
|
245
|
+
]);
|
|
246
|
+
const completed = await createExecutor(adapter).execute('provider', context, 'list');
|
|
224
247
|
|
|
225
|
-
const
|
|
226
|
-
|
|
248
|
+
const apiKeyRequest = requests[2];
|
|
249
|
+
expect(apiKeyRequest?.title).toBe('anthropic API key');
|
|
250
|
+
expect(apiKeyRequest?.allowFreeText).toBe(true);
|
|
251
|
+
expect(apiKeyRequest?.placeholder).toBe('(unchanged)');
|
|
252
|
+
expect(apiKeyRequest?.masked).toBe(true);
|
|
227
253
|
|
|
228
254
|
expect(readTarget()).toMatchObject({
|
|
229
255
|
providers: {
|
|
@@ -252,16 +278,15 @@ describe('createProviderCommandModule', () => {
|
|
|
252
278
|
},
|
|
253
279
|
});
|
|
254
280
|
|
|
255
|
-
const
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
281
|
+
const { context, requests } = scriptedContext([
|
|
282
|
+
{ type: 'answer', values: ['openai'] },
|
|
283
|
+
{ type: 'answer', values: ['duplicate'] },
|
|
284
|
+
{ type: 'answer', values: [], text: '' },
|
|
285
|
+
]);
|
|
286
|
+
const completed = await createExecutor(adapter).execute('provider', context, 'list');
|
|
259
287
|
|
|
260
|
-
expect(
|
|
261
|
-
|
|
262
|
-
title: 'Duplicate openai as',
|
|
263
|
-
placeholder: 'openai-copy',
|
|
264
|
-
});
|
|
288
|
+
expect(requests[2]?.title).toBe('Duplicate openai as');
|
|
289
|
+
expect(requests[2]?.placeholder).toBe('openai-copy');
|
|
265
290
|
expect(readTarget()).toMatchObject({
|
|
266
291
|
providers: {
|
|
267
292
|
'openai-copy': {
|
|
@@ -292,11 +317,16 @@ describe('createProviderCommandModule', () => {
|
|
|
292
317
|
},
|
|
293
318
|
);
|
|
294
319
|
|
|
295
|
-
const
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
320
|
+
const { context, requests } = scriptedContext([
|
|
321
|
+
{ type: 'answer', values: ['anthropic'] },
|
|
322
|
+
{ type: 'answer', values: ['delete'] },
|
|
323
|
+
{ type: 'answer', values: ['yes'] },
|
|
324
|
+
]);
|
|
325
|
+
const completed = await createExecutor(adapter).execute('provider', context, 'list');
|
|
299
326
|
|
|
327
|
+
// The confirm prompt must actually be issued — guards against a silent drop of the
|
|
328
|
+
// confirmation step that would still "delete and pass".
|
|
329
|
+
expect(requests[2]?.title).toBe('Delete provider profile anthropic?');
|
|
300
330
|
expect(completed?.message).toBe('Provider profile deleted: anthropic.');
|
|
301
331
|
expect(readTarget()).toEqual({
|
|
302
332
|
currentProvider: 'openai',
|
|
@@ -324,16 +354,17 @@ describe('createProviderCommandModule', () => {
|
|
|
324
354
|
},
|
|
325
355
|
);
|
|
326
356
|
|
|
327
|
-
const
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
357
|
+
const { context, requests } = scriptedContext([
|
|
358
|
+
{ type: 'answer', values: ['anthropic'] },
|
|
359
|
+
{ type: 'answer', values: ['delete'] },
|
|
360
|
+
{ type: 'answer', values: ['yes'] },
|
|
361
|
+
{ type: 'answer', values: ['openai'] },
|
|
362
|
+
]);
|
|
363
|
+
const completed = await createExecutor(adapter).execute('provider', context, 'list');
|
|
332
364
|
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
});
|
|
365
|
+
// The confirm precedes the replacement picker — assert both so neither step can be dropped silently.
|
|
366
|
+
expect(requests[2]?.title).toBe('Delete provider profile anthropic?');
|
|
367
|
+
expect(requests[3]?.title).toBe('Replacement provider for anthropic');
|
|
337
368
|
expect(readTarget()).toEqual({
|
|
338
369
|
currentProvider: 'openai',
|
|
339
370
|
providers: {
|
|
@@ -361,30 +392,39 @@ describe('createProviderCommandModule', () => {
|
|
|
361
392
|
{},
|
|
362
393
|
);
|
|
363
394
|
|
|
364
|
-
const
|
|
365
|
-
|
|
366
|
-
|
|
395
|
+
const { context } = scriptedContext([
|
|
396
|
+
{ type: 'answer', values: ['anthropic'] },
|
|
397
|
+
{ type: 'answer', values: ['delete'] },
|
|
398
|
+
]);
|
|
399
|
+
const result = await createExecutor(adapter).execute('provider', context, 'list');
|
|
367
400
|
|
|
368
|
-
expect(
|
|
369
|
-
expect(
|
|
401
|
+
expect(result?.success).toBe(false);
|
|
402
|
+
expect(result?.message).toContain('not stored in the active write target');
|
|
370
403
|
});
|
|
371
404
|
|
|
372
405
|
it('owns provider setup flow and writes settings after generic prompt submissions', async () => {
|
|
373
406
|
const { adapter, readTarget } = createSettingsAdapter({}, {});
|
|
374
|
-
const first = await createExecutor(adapter).execute('provider', session, 'add openai');
|
|
375
|
-
|
|
376
|
-
expect(first?.interaction?.prompt).toMatchObject({
|
|
377
|
-
kind: 'text',
|
|
378
|
-
title: 'OpenAI-compatible base URL',
|
|
379
|
-
description:
|
|
380
|
-
' Setup help: Official: OpenAI-compatible local server docs - https://lmstudio.ai/docs/developer',
|
|
381
|
-
placeholder: 'http://localhost:1234/v1',
|
|
382
|
-
allowEmpty: true,
|
|
383
|
-
});
|
|
384
407
|
|
|
385
|
-
const
|
|
386
|
-
|
|
387
|
-
|
|
408
|
+
const { context, requests } = scriptedContext([
|
|
409
|
+
{ type: 'answer', values: [], text: '' },
|
|
410
|
+
{ type: 'answer', values: [], text: '' },
|
|
411
|
+
{ type: 'answer', values: [], text: '' },
|
|
412
|
+
]);
|
|
413
|
+
const completed = await createExecutor(adapter).execute('provider', context, 'add openai');
|
|
414
|
+
|
|
415
|
+
expect(requests[0]?.title).toBe('OpenAI-compatible base URL');
|
|
416
|
+
expect(requests[0]?.description).toBe(
|
|
417
|
+
' Setup help: Official: OpenAI-compatible local server docs - https://lmstudio.ai/docs/developer',
|
|
418
|
+
);
|
|
419
|
+
expect(requests[0]?.placeholder).toBe('http://localhost:1234/v1');
|
|
420
|
+
expect(requests[0]?.allowEmpty).toBe(true);
|
|
421
|
+
// All three setup steps must be asked in order — a dropped step would otherwise be hidden by the
|
|
422
|
+
// scripted double returning the surplus answers to whatever remains.
|
|
423
|
+
expect(requests.map((request) => request.title)).toEqual([
|
|
424
|
+
'OpenAI-compatible base URL',
|
|
425
|
+
'OpenAI-compatible model',
|
|
426
|
+
'OpenAI-compatible API key',
|
|
427
|
+
]);
|
|
388
428
|
|
|
389
429
|
expect(readTarget()).toMatchObject({
|
|
390
430
|
currentProvider: 'openai',
|
|
@@ -406,6 +446,35 @@ describe('createProviderCommandModule', () => {
|
|
|
406
446
|
]);
|
|
407
447
|
});
|
|
408
448
|
|
|
449
|
+
it('asks the user to pick a provider type when /provider add is called without one', async () => {
|
|
450
|
+
const { adapter, readTarget } = createSettingsAdapter({}, {});
|
|
451
|
+
|
|
452
|
+
const { context, requests } = scriptedContext([
|
|
453
|
+
{ type: 'answer', values: ['openai'] },
|
|
454
|
+
{ type: 'answer', values: [], text: '' },
|
|
455
|
+
{ type: 'answer', values: [], text: '' },
|
|
456
|
+
{ type: 'answer', values: [], text: '' },
|
|
457
|
+
]);
|
|
458
|
+
const completed = await createExecutor(adapter).execute('provider', context, 'add');
|
|
459
|
+
|
|
460
|
+
expect(requests[0]?.title).toBe('Select provider');
|
|
461
|
+
expect(requests[0]?.options?.map((option) => option.value)).toEqual(['openai', 'anthropic']);
|
|
462
|
+
expect(readTarget()).toMatchObject({
|
|
463
|
+
currentProvider: 'openai',
|
|
464
|
+
providers: { openai: { type: 'openai' } },
|
|
465
|
+
});
|
|
466
|
+
expect(completed?.success).toBe(true);
|
|
467
|
+
});
|
|
468
|
+
|
|
469
|
+
it('reports usage for /provider add without a type when no renderer is attached', async () => {
|
|
470
|
+
const { adapter } = createSettingsAdapter({}, {});
|
|
471
|
+
|
|
472
|
+
const result = await createExecutor(adapter).execute('provider', headlessContext, 'add');
|
|
473
|
+
|
|
474
|
+
expect(result?.success).toBe(false);
|
|
475
|
+
expect(result?.message).toContain('Usage: provider add <type>');
|
|
476
|
+
});
|
|
477
|
+
|
|
409
478
|
it('creates another profile when provider type already exists', async () => {
|
|
410
479
|
const { adapter, readTarget } = createSettingsAdapter(
|
|
411
480
|
{
|
|
@@ -421,11 +490,13 @@ describe('createProviderCommandModule', () => {
|
|
|
421
490
|
},
|
|
422
491
|
{},
|
|
423
492
|
);
|
|
424
|
-
const first = await createExecutor(adapter).execute('provider', session, 'add openai');
|
|
425
|
-
const second = await first?.interaction?.submit('');
|
|
426
|
-
const third = await second?.interaction?.submit('');
|
|
427
493
|
|
|
428
|
-
|
|
494
|
+
const { context } = scriptedContext([
|
|
495
|
+
{ type: 'answer', values: [], text: '' },
|
|
496
|
+
{ type: 'answer', values: [], text: '' },
|
|
497
|
+
{ type: 'answer', values: [], text: '' },
|
|
498
|
+
]);
|
|
499
|
+
await createExecutor(adapter).execute('provider', context, 'add openai');
|
|
429
500
|
|
|
430
501
|
expect(readTarget()).toMatchObject({
|
|
431
502
|
currentProvider: 'openai-2',
|
|
@@ -465,7 +536,7 @@ describe('createProviderCommandModule', () => {
|
|
|
465
536
|
});
|
|
466
537
|
const result = await new SystemCommandExecutor([...(module.systemCommands ?? [])]).execute(
|
|
467
538
|
'provider',
|
|
468
|
-
|
|
539
|
+
headlessContext,
|
|
469
540
|
'test openai',
|
|
470
541
|
);
|
|
471
542
|
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import type { IActionRequest, IUserInteraction, TActionResponse } from '@robota-sdk/agent-core';
|
|
2
|
+
import type { ICommandHostContext } from '@robota-sdk/agent-framework';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Build a command host context whose `getUserInteraction().ask` replays a scripted sequence of
|
|
6
|
+
* responses (CMD-004 test double). Each `ask` records the request it received (so tests can assert on
|
|
7
|
+
* the rendered action shape) and returns the next scripted answer; once the script is exhausted it
|
|
8
|
+
* returns `{ type: 'cancelled' }`, mirroring a user dismissing the prompt.
|
|
9
|
+
*/
|
|
10
|
+
export function scriptedContext(answers: readonly TActionResponse[]): {
|
|
11
|
+
context: ICommandHostContext;
|
|
12
|
+
requests: IActionRequest[];
|
|
13
|
+
} {
|
|
14
|
+
const requests: IActionRequest[] = [];
|
|
15
|
+
let index = 0;
|
|
16
|
+
const ui: IUserInteraction = {
|
|
17
|
+
ask: (request) => {
|
|
18
|
+
requests.push(request);
|
|
19
|
+
const answer = answers[index];
|
|
20
|
+
index += 1;
|
|
21
|
+
return Promise.resolve(answer ?? { type: 'cancelled' });
|
|
22
|
+
},
|
|
23
|
+
};
|
|
24
|
+
const context = {
|
|
25
|
+
getUserInteraction: () => ui,
|
|
26
|
+
} as Partial<ICommandHostContext> as ICommandHostContext;
|
|
27
|
+
return { context, requests };
|
|
28
|
+
}
|
|
@@ -1,30 +1,38 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {
|
|
2
|
+
findProviderDefinition,
|
|
3
|
+
formatSupportedProviderTypes,
|
|
4
|
+
selectAction,
|
|
5
|
+
} from '@robota-sdk/agent-core';
|
|
2
6
|
import { testProviderProfileCommand } from '@robota-sdk/agent-framework';
|
|
3
7
|
|
|
4
8
|
import { buildProviderSwitch } from './provider-command-profile-operations.js';
|
|
5
|
-
import {
|
|
6
|
-
import { createSetupFlow,
|
|
9
|
+
import { askProviderProfileSelection } from './provider-command-profile.js';
|
|
10
|
+
import { createSetupFlow, runProviderAddSetup } from './provider-command-setup.js';
|
|
7
11
|
import { formatProviderSetupChoiceLabel } from './provider-setup-flow.js';
|
|
8
12
|
|
|
13
|
+
import type { IUserInteraction } from '@robota-sdk/agent-core';
|
|
9
14
|
import type {
|
|
15
|
+
ICommandHostContext,
|
|
10
16
|
IProviderCommandModuleOptions,
|
|
11
17
|
IProviderProfileSettings,
|
|
12
18
|
} from '@robota-sdk/agent-framework';
|
|
13
|
-
import type {
|
|
19
|
+
import type { ICommandResult } from '@robota-sdk/agent-interface-transport';
|
|
14
20
|
|
|
15
21
|
export async function executeProviderCommand(
|
|
22
|
+
context: ICommandHostContext,
|
|
16
23
|
args: string,
|
|
17
24
|
options: IProviderCommandModuleOptions,
|
|
18
25
|
): Promise<ICommandResult> {
|
|
26
|
+
const ui = context.getUserInteraction?.();
|
|
19
27
|
const settings = options.settings.readMergedSettings();
|
|
20
28
|
const trimmedArgs = args.trim();
|
|
21
29
|
if (trimmedArgs.length === 0) {
|
|
22
|
-
return buildProviderProfilePicker(settings.currentProvider, settings.providers, options);
|
|
30
|
+
return buildProviderProfilePicker(ui, settings.currentProvider, settings.providers, options);
|
|
23
31
|
}
|
|
24
32
|
const [subcommand = 'current', profileArg] = trimmedArgs.split(/\s+/);
|
|
25
33
|
|
|
26
34
|
if (subcommand === 'list') {
|
|
27
|
-
return buildProviderProfilePicker(settings.currentProvider, settings.providers, options);
|
|
35
|
+
return buildProviderProfilePicker(ui, settings.currentProvider, settings.providers, options);
|
|
28
36
|
}
|
|
29
37
|
if (subcommand === 'current' || subcommand === '') {
|
|
30
38
|
return {
|
|
@@ -36,7 +44,7 @@ export async function executeProviderCommand(
|
|
|
36
44
|
return buildProviderSwitch(settings.providers, profileArg, options);
|
|
37
45
|
}
|
|
38
46
|
if (subcommand === 'test') {
|
|
39
|
-
return
|
|
47
|
+
return testProviderProfileCommand(
|
|
40
48
|
settings.currentProvider,
|
|
41
49
|
settings.providers,
|
|
42
50
|
profileArg,
|
|
@@ -44,7 +52,7 @@ export async function executeProviderCommand(
|
|
|
44
52
|
);
|
|
45
53
|
}
|
|
46
54
|
if (subcommand === 'add') {
|
|
47
|
-
return buildProviderSetup(profileArg, options);
|
|
55
|
+
return buildProviderSetup(ui, profileArg, options);
|
|
48
56
|
}
|
|
49
57
|
|
|
50
58
|
return {
|
|
@@ -53,20 +61,20 @@ export async function executeProviderCommand(
|
|
|
53
61
|
};
|
|
54
62
|
}
|
|
55
63
|
|
|
64
|
+
/**
|
|
65
|
+
* List the provider profiles. With an interactive renderer attached, drive the inline profile picker
|
|
66
|
+
* (CMD-004); without one (headless/automation) or with no profiles, return the formatted list as text.
|
|
67
|
+
*/
|
|
56
68
|
function buildProviderProfilePicker(
|
|
69
|
+
ui: IUserInteraction | undefined,
|
|
57
70
|
currentProvider: string | undefined,
|
|
58
71
|
providers: Record<string, IProviderProfileSettings> | undefined,
|
|
59
72
|
options: IProviderCommandModuleOptions,
|
|
60
|
-
): ICommandResult {
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
return { message, success: true };
|
|
73
|
+
): Promise<ICommandResult> | ICommandResult {
|
|
74
|
+
if (!ui || Object.keys(providers ?? {}).length === 0) {
|
|
75
|
+
return { message: formatProviderList(currentProvider, providers), success: true };
|
|
64
76
|
}
|
|
65
|
-
return
|
|
66
|
-
message,
|
|
67
|
-
success: true,
|
|
68
|
-
interaction: createProviderProfileSelectionInteraction(currentProvider, providers, options),
|
|
69
|
-
};
|
|
77
|
+
return askProviderProfileSelection(ui, currentProvider, providers, options);
|
|
70
78
|
}
|
|
71
79
|
|
|
72
80
|
function formatProviderList(
|
|
@@ -104,16 +112,24 @@ function formatCurrentProvider(
|
|
|
104
112
|
].join('\n');
|
|
105
113
|
}
|
|
106
114
|
|
|
115
|
+
/**
|
|
116
|
+
* Configure a provider profile. With an explicit `type`, run the setup wizard directly; without one,
|
|
117
|
+
* ask the user to pick a provider type first (CMD-004). Without an interactive renderer, setup cannot
|
|
118
|
+
* proceed — return usage text instead of a silent guess.
|
|
119
|
+
*/
|
|
107
120
|
function buildProviderSetup(
|
|
121
|
+
ui: IUserInteraction | undefined,
|
|
108
122
|
type: string | undefined,
|
|
109
123
|
options: IProviderCommandModuleOptions,
|
|
110
|
-
): ICommandResult {
|
|
124
|
+
): Promise<ICommandResult> | ICommandResult {
|
|
111
125
|
if (type === undefined || type.length === 0) {
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
126
|
+
if (!ui) {
|
|
127
|
+
return {
|
|
128
|
+
message: `Usage: provider add <type>. Supported: ${formatSupportedProviderTypes(options.providerDefinitions)}`,
|
|
129
|
+
success: false,
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
return askProviderSetupType(ui, options);
|
|
117
133
|
}
|
|
118
134
|
if (findProviderDefinition(options.providerDefinitions, type) === undefined) {
|
|
119
135
|
return {
|
|
@@ -121,34 +137,35 @@ function buildProviderSetup(
|
|
|
121
137
|
success: false,
|
|
122
138
|
};
|
|
123
139
|
}
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
140
|
+
if (!ui) {
|
|
141
|
+
return {
|
|
142
|
+
message: `Provider setup for "${type}" requires an interactive session.`,
|
|
143
|
+
success: false,
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
return runProviderAddSetup(ui, createSetupFlow(type, options), options);
|
|
129
147
|
}
|
|
130
148
|
|
|
131
|
-
function
|
|
149
|
+
async function askProviderSetupType(
|
|
150
|
+
ui: IUserInteraction,
|
|
132
151
|
options: IProviderCommandModuleOptions,
|
|
133
|
-
):
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
cancel: () => ({ message: 'Provider setup cancelled.', success: true }),
|
|
153
|
-
};
|
|
152
|
+
): Promise<ICommandResult> {
|
|
153
|
+
const typeOptions = options.providerDefinitions.map((definition) => ({
|
|
154
|
+
value: definition.type,
|
|
155
|
+
label: formatProviderSetupChoiceLabel(definition),
|
|
156
|
+
}));
|
|
157
|
+
const response = await ui.ask(
|
|
158
|
+
selectAction('provider-type', 'Select provider', typeOptions, { maxVisible: 6 }),
|
|
159
|
+
);
|
|
160
|
+
if (response.type !== 'answer' || response.values[0] === undefined) {
|
|
161
|
+
return { message: 'Provider setup cancelled.', success: true };
|
|
162
|
+
}
|
|
163
|
+
const type = response.values[0];
|
|
164
|
+
if (findProviderDefinition(options.providerDefinitions, type) === undefined) {
|
|
165
|
+
return {
|
|
166
|
+
message: `Usage: provider add <type>. Supported: ${formatSupportedProviderTypes(options.providerDefinitions)}`,
|
|
167
|
+
success: false,
|
|
168
|
+
};
|
|
169
|
+
}
|
|
170
|
+
return runProviderAddSetup(ui, createSetupFlow(type, options), options);
|
|
154
171
|
}
|
|
@@ -6,11 +6,7 @@ import type {
|
|
|
6
6
|
IProviderCommandSettingsAdapter,
|
|
7
7
|
ISystemCommand as TSystemCommand,
|
|
8
8
|
} from '@robota-sdk/agent-framework';
|
|
9
|
-
import type {
|
|
10
|
-
ICommand,
|
|
11
|
-
ICommandInteractionHint,
|
|
12
|
-
ICommandSource,
|
|
13
|
-
} from '@robota-sdk/agent-interface-transport';
|
|
9
|
+
import type { ICommand, ICommandSource } from '@robota-sdk/agent-interface-transport';
|
|
14
10
|
export type { IProviderCommandModuleOptions, IProviderCommandSettingsAdapter };
|
|
15
11
|
|
|
16
12
|
function buildProviderSubcommands(): ICommand[] {
|
|
@@ -56,22 +52,11 @@ function createProviderSystemCommand(options: IProviderCommandModuleOptions): TS
|
|
|
56
52
|
modelInvocable: false,
|
|
57
53
|
argumentHint: entry.argumentHint,
|
|
58
54
|
subcommands: entry.subcommands,
|
|
59
|
-
|
|
55
|
+
lifecycle: 'inline',
|
|
56
|
+
execute: (context, args) => executeProviderCommand(context, args, options),
|
|
60
57
|
};
|
|
61
58
|
}
|
|
62
59
|
|
|
63
|
-
const PROVIDER_INTERACTION_HINTS: Record<string, ICommandInteractionHint> = {
|
|
64
|
-
provider: {
|
|
65
|
-
type: 'pick',
|
|
66
|
-
getItems: () =>
|
|
67
|
-
buildProviderSubcommands().map((sub) => ({
|
|
68
|
-
label: sub.name,
|
|
69
|
-
value: sub.name,
|
|
70
|
-
description: sub.description,
|
|
71
|
-
})),
|
|
72
|
-
},
|
|
73
|
-
};
|
|
74
|
-
|
|
75
60
|
export function createProviderCommandModule(
|
|
76
61
|
options: IProviderCommandModuleOptions,
|
|
77
62
|
): TCommandModule {
|
|
@@ -79,6 +64,5 @@ export function createProviderCommandModule(
|
|
|
79
64
|
name: 'agent-command-provider',
|
|
80
65
|
commandSources: [new ProviderCommandSource()],
|
|
81
66
|
systemCommands: [createProviderSystemCommand(options)],
|
|
82
|
-
interactionHints: PROVIDER_INTERACTION_HINTS,
|
|
83
67
|
};
|
|
84
68
|
}
|