@robota-sdk/agent-command 3.0.0-beta.90 → 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.
@@ -0,0 +1,38 @@
1
+ import { normalizeProviderConfig } from '@robota-sdk/agent-core';
2
+ import { validateProviderProfile } from '@robota-sdk/agent-framework';
3
+
4
+ import type {
5
+ IProviderCommandModuleOptions,
6
+ IProviderProfileSettings,
7
+ } from '@robota-sdk/agent-framework';
8
+
9
+ /** A settings change must not commit a connection its host can no longer resolve. */
10
+ export async function validateProviderConnection(
11
+ profileName: string,
12
+ profile: IProviderProfileSettings,
13
+ options: IProviderCommandModuleOptions,
14
+ ): Promise<void> {
15
+ validateProviderProfile(profileName, profile, {
16
+ providerDefinitions: options.providerDefinitions,
17
+ ...(options.env === undefined ? {} : { env: options.env }),
18
+ });
19
+ if (profile.apiKeyRef === undefined) return;
20
+ try {
21
+ if (options.resolveCredential === undefined)
22
+ throw new Error('Host credential resolution unavailable.');
23
+ const resolved = await options.resolveCredential(
24
+ normalizeProviderConfig(
25
+ {
26
+ name: profile.type!,
27
+ model: profile.model,
28
+ apiKeyRef: profile.apiKeyRef,
29
+ baseURL: profile.baseURL,
30
+ },
31
+ options.providerDefinitions,
32
+ ),
33
+ );
34
+ if (!resolved.apiKey?.trim()) throw new Error('Host credential unavailable.');
35
+ } catch {
36
+ throw new Error(`Saved connection unavailable. Run /provider reconnect ${profileName}.`);
37
+ }
38
+ }
@@ -1,7 +1,7 @@
1
1
  import type { TPromptInput } from './provider-setup-flow.js';
2
2
  import type { ITerminalOutput } from '@robota-sdk/agent-core';
3
3
 
4
- export type TOnboardingPath = 'has-key' | 'free-key' | 'local';
4
+ export type TOnboardingPath = 'has-key' | 'free-key' | 'local' | 'connect';
5
5
 
6
6
  export interface IOnboardingResult {
7
7
  path: TOnboardingPath;
@@ -44,10 +44,19 @@ const LOCAL_MODEL_GUIDE = `
44
44
  export async function runOnboardingBranch(
45
45
  promptInput: TPromptInput,
46
46
  terminal: ITerminalOutput,
47
+ offerOpenRouter = false,
47
48
  ): Promise<IOnboardingResult> {
48
- const raw = await promptInput(ONBOARDING_PROMPT);
49
+ const prompt = offerOpenRouter
50
+ ? ONBOARDING_PROMPT.replace(
51
+ ' Choose [1-3]',
52
+ ' 4. Connect with OpenRouter (Browser or API key)\n\n Choose [1-4]',
53
+ )
54
+ : ONBOARDING_PROMPT;
55
+ const raw = await promptInput(prompt);
49
56
  const choice = raw.trim() || '1';
50
57
 
58
+ if (offerOpenRouter && choice === '4') return { path: 'connect', preselectedType: 'openrouter' };
59
+
51
60
  if (choice === '2') {
52
61
  terminal.writeLine(GEMINI_GUIDE);
53
62
  await promptInput(' Press Enter when you have your API key: ');
@@ -0,0 +1,28 @@
1
+ import type { IUserInteraction } from '@robota-sdk/agent-core';
2
+ import type { TPromptInput } from './provider-setup-flow.js';
3
+
4
+ /** Adapt existing terminal key/model prompts; browser waiting uses the host's abortable interaction. */
5
+ export function createProviderPromptInteraction(promptInput: TPromptInput): IUserInteraction {
6
+ return {
7
+ async ask(request, options) {
8
+ const choices = request.options ?? [];
9
+ const labels = [
10
+ request.description,
11
+ ` ${request.title}`,
12
+ ...choices.map((choice, index) => ` ${index + 1}. ${choice.label}`),
13
+ choices.length > 0 ? ' Choose: ' : ` ${request.placeholder ?? ''}: `,
14
+ ]
15
+ .filter((value) => value !== undefined)
16
+ .join('\n');
17
+ const answer = await promptInput(labels, request.masked, options);
18
+ if (options?.signal?.aborted) return { type: 'cancelled' };
19
+ if (choices.length === 0) return { type: 'answer', values: [], text: answer };
20
+ const selected =
21
+ choices[Number(answer.trim() || '1') - 1] ??
22
+ choices.find((choice) => choice.value === answer.trim());
23
+ return selected === undefined
24
+ ? { type: 'cancelled' }
25
+ : { type: 'answer', values: [selected.value] };
26
+ },
27
+ };
28
+ }
@@ -0,0 +1,67 @@
1
+ import { textAction } from '@robota-sdk/agent-core';
2
+
3
+ import {
4
+ formatProviderSetupHelpLinks,
5
+ getProviderSetupStep,
6
+ submitProviderSetupValue,
7
+ } from './provider-setup-flow.js';
8
+
9
+ import type { IActionRequest, IUserInteraction } from '@robota-sdk/agent-core';
10
+ import type { IProviderSetupInput } from '@robota-sdk/agent-framework';
11
+ import type { ICommandResult } from '@robota-sdk/agent-interface-command';
12
+ import type { IProviderSetupFlowState } from './provider-setup-flow.js';
13
+
14
+ /** Build the per-step `IActionRequest` for the current setup step (CMD-004 inline ask). */
15
+ function toProviderSetupStepRequest(
16
+ flow: IProviderSetupFlowState,
17
+ errorMessage?: string,
18
+ ): IActionRequest {
19
+ const step = getProviderSetupStep(flow);
20
+ const placeholder =
21
+ step.masked === true && step.defaultValue !== undefined ? '(unchanged)' : step.defaultValue;
22
+ const helpLinks = formatProviderSetupHelpLinks(flow.setupHelpLinks);
23
+ const description =
24
+ [errorMessage, helpLinks.length > 0 ? helpLinks : undefined]
25
+ .filter((part): part is string => part !== undefined && part.length > 0)
26
+ .join('\n') || undefined;
27
+ return textAction(`provider-setup-${step.key}`, step.title, {
28
+ description,
29
+ placeholder,
30
+ allowEmpty:
31
+ step.defaultValue !== undefined || (step.editOnly === true && step.required !== true),
32
+ masked: step.masked,
33
+ });
34
+ }
35
+
36
+ /**
37
+ * Drive the setup-step engine through inline `ui.ask` calls (CMD-004), re-asking the same step when a
38
+ * step fails validation (the error is surfaced in the request description). The `complete` sink differs
39
+ * between the add and edit paths.
40
+ */
41
+ export async function runProviderSetupAsk(
42
+ ui: IUserInteraction,
43
+ flow: IProviderSetupFlowState,
44
+ complete: (input: IProviderSetupInput) => ICommandResult | Promise<ICommandResult>,
45
+ cancelMessage: string,
46
+ signal?: AbortSignal,
47
+ ): Promise<ICommandResult> {
48
+ let state = flow;
49
+ let errorMessage: string | undefined;
50
+ for (;;) {
51
+ if (signal?.aborted) return { message: cancelMessage, success: true };
52
+ const response = await ui.ask(toProviderSetupStepRequest(state, errorMessage), { signal });
53
+ if (response.type === 'cancelled') {
54
+ return { message: cancelMessage, success: true };
55
+ }
56
+ const result = submitProviderSetupValue(state, response.text ?? '');
57
+ if (result.status === 'error') {
58
+ errorMessage = result.message;
59
+ continue;
60
+ }
61
+ if (result.status === 'complete') {
62
+ return complete(result.input);
63
+ }
64
+ state = result.state;
65
+ errorMessage = undefined;
66
+ }
67
+ }
@@ -10,7 +10,11 @@ import type {
10
10
  import type { IProviderSetupInput } from '@robota-sdk/agent-framework';
11
11
 
12
12
  export type TProviderSetupType = string;
13
- export type TPromptInput = (label: string, masked?: boolean) => Promise<string>;
13
+ export type TPromptInput = (
14
+ label: string,
15
+ masked?: boolean,
16
+ options?: { signal?: AbortSignal },
17
+ ) => Promise<string>;
14
18
 
15
19
  export interface IProviderSetupPromptStep extends IProviderSetupStepDefinition {
16
20
  key: TProviderSetupField;
@@ -28,6 +32,7 @@ export interface IProviderSetupFlowState {
28
32
  }
29
33
 
30
34
  export interface IProviderSetupFlowOptions {
35
+ includeEditOnlySteps?: boolean;
31
36
  existingProfileNames?: readonly string[];
32
37
  initialValues?: Partial<Record<TProviderSetupField, string>>;
33
38
  profileName?: string;
@@ -58,7 +63,9 @@ export function createProviderSetupFlow(
58
63
  return {
59
64
  type,
60
65
  steps: applyProviderSetupInitialValues(
61
- getProviderSetupSteps(definition),
66
+ getProviderSetupSteps(definition).filter(
67
+ (step) => step.editOnly !== true || options.includeEditOnlySteps === true,
68
+ ),
62
69
  options.initialValues,
63
70
  ),
64
71
  setupHelpLinks: definition.setupHelpLinks ?? [],
@@ -9,6 +9,12 @@ import {
9
9
  } from '@robota-sdk/agent-framework';
10
10
 
11
11
  import { runOnboardingBranch } from './provider-onboarding.js';
12
+ import { createProviderSetupFlow } from './provider-setup-flow.js';
13
+ import {
14
+ hasProviderConnectionMethods,
15
+ runProviderConnectionSetup,
16
+ } from './provider-connection-setup.js';
17
+ import { createProviderPromptInteraction } from './provider-prompt-interaction.js';
12
18
  import {
13
19
  formatProviderSetupSelectionPrompt,
14
20
  resolveProviderSetupSelection,
@@ -16,12 +22,15 @@ import {
16
22
  type TPromptInput,
17
23
  } from './provider-setup-flow.js';
18
24
 
19
- import type { IProviderDefinition } from '@robota-sdk/agent-core';
25
+ import type { IProviderDefinition, IUserInteraction } from '@robota-sdk/agent-core';
20
26
  import type { ITerminalOutput } from '@robota-sdk/agent-core';
21
27
  import type {
22
28
  ISettingsDocumentStore,
23
29
  TSettingsScope,
24
30
  TSettingsSource,
31
+ IProviderConnectionHost,
32
+ IOrgPolicy,
33
+ TProviderSettingsDocument,
25
34
  } from '@robota-sdk/agent-framework';
26
35
 
27
36
  export interface IProviderStartupContext {
@@ -31,6 +40,9 @@ export interface IProviderStartupContext {
31
40
  settingsStores: readonly ISettingsDocumentStore[];
32
41
  /** Selected host snapshot for configure-time credential reference checks. */
33
42
  env?: Record<string, string | undefined>;
43
+ connectionHost?: IProviderConnectionHost;
44
+ orgPolicy?: IOrgPolicy;
45
+ interaction?: IUserInteraction;
34
46
  }
35
47
 
36
48
  export interface IEnsureProviderConfigOptions {
@@ -47,7 +59,11 @@ export async function runProviderStartupSetup(
47
59
  terminal: ITerminalOutput,
48
60
  providerDefinitions: readonly IProviderDefinition[],
49
61
  ): Promise<void> {
50
- const onboarding = await runOnboardingBranch(promptInput, terminal);
62
+ const onboarding = await runOnboardingBranch(
63
+ promptInput,
64
+ terminal,
65
+ providerDefinitions.some((definition) => definition.type === 'openrouter'),
66
+ );
51
67
  const access = resolveSettingsAccess(ctx);
52
68
  const existingProfileNames = Object.keys(
53
69
  readMergedProviderSettings(access.sources).providers ?? {},
@@ -64,10 +80,45 @@ export async function runProviderStartupSetup(
64
80
  type = resolveProviderSetupSelection(providerChoice, providerDefinitions);
65
81
  }
66
82
 
67
- const input = await runProviderSetupPromptFlow(type, promptInput, providerDefinitions, {
68
- existingProfileNames,
69
- });
70
- applyProviderConfiguration(settingsStore, input, { providerDefinitions, env: ctx.env });
83
+ const setupOptions = {
84
+ providerDefinitions,
85
+ env: ctx.env,
86
+ connectionHost: ctx.connectionHost,
87
+ orgPolicy: ctx.orgPolicy,
88
+ settings: {
89
+ readMergedSettings: () => readMergedProviderSettings(access.sources),
90
+ readTargetSettings: () => settingsStore.read() as TProviderSettingsDocument,
91
+ writeTargetSettings: (settings: TProviderSettingsDocument) => settingsStore.write(settings),
92
+ },
93
+ };
94
+ if (hasProviderConnectionMethods(type, setupOptions)) {
95
+ const definitions =
96
+ ctx.interaction === undefined
97
+ ? providerDefinitions.map((definition) => ({
98
+ ...definition,
99
+ connectionMethods: definition.connectionMethods?.filter(
100
+ (method) => method !== 'browser',
101
+ ),
102
+ }))
103
+ : providerDefinitions;
104
+ if (ctx.interaction === undefined)
105
+ terminal.writeLine(
106
+ 'Browser connection is unavailable here. API key setup remains available.',
107
+ );
108
+ const result = await runProviderConnectionSetup(
109
+ ctx.interaction ?? createProviderPromptInteraction(promptInput),
110
+ createProviderSetupFlow(type, definitions, { existingProfileNames }),
111
+ { ...setupOptions, providerDefinitions: definitions },
112
+ false,
113
+ );
114
+ if (!result.success) throw new ProviderConfigError(result.message);
115
+ if (result.hostActions === undefined) return;
116
+ } else {
117
+ const input = await runProviderSetupPromptFlow(type, promptInput, providerDefinitions, {
118
+ existingProfileNames,
119
+ });
120
+ applyProviderConfiguration(settingsStore, input, { providerDefinitions, env: ctx.env });
121
+ }
71
122
  const language = await promptInput(' Response language (ko/en/ja/zh, default: en): ');
72
123
  if (language) {
73
124
  const settings = settingsStore.read();
@@ -89,7 +140,9 @@ export async function ensureProviderConfig(
89
140
  const merged = readMergedProviderSettings(access.sources);
90
141
  const selectedSettings =
91
142
  ctx.provider !== undefined ? { ...merged, currentProvider: ctx.provider } : merged;
92
- if (checkSettingsDocument(selectedSettings, providerDefinitions, ctx.env ?? options.env) === 'valid') {
143
+ if (
144
+ checkSettingsDocument(selectedSettings, providerDefinitions, ctx.env ?? options.env) === 'valid'
145
+ ) {
93
146
  return;
94
147
  }
95
148
  // Zero-config startup: a recognized provider env key with complete definition defaults
@@ -114,7 +167,9 @@ export async function ensureProviderConfig(
114
167
  const updated = readMergedProviderSettings(access.sources);
115
168
  const updatedSettings =
116
169
  ctx.provider !== undefined ? { ...updated, currentProvider: ctx.provider } : updated;
117
- if (checkSettingsDocument(updatedSettings, providerDefinitions, ctx.env ?? options.env) !== 'valid') {
170
+ if (
171
+ checkSettingsDocument(updatedSettings, providerDefinitions, ctx.env ?? options.env) !== 'valid'
172
+ ) {
118
173
  throw new ProviderConfigError(options.formatError(providerDefinitions));
119
174
  }
120
175
  }