@robota-sdk/agent-command 3.0.0-beta.100

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.
Files changed (251) hide show
  1. package/CHANGELOG.md +1351 -0
  2. package/LICENSE +661 -0
  3. package/README.md +100 -0
  4. package/dist/node/index.cjs +121 -0
  5. package/dist/node/index.d.cts +1053 -0
  6. package/dist/node/index.d.cts.map +1 -0
  7. package/dist/node/index.d.ts +1053 -0
  8. package/dist/node/index.d.ts.map +1 -0
  9. package/dist/node/index.js +122 -0
  10. package/dist/node/index.js.map +1 -0
  11. package/package.json +83 -0
  12. package/src/advisor/advisor-command-module.ts +47 -0
  13. package/src/advisor/advisor-command.test.ts +102 -0
  14. package/src/advisor/advisor-command.ts +55 -0
  15. package/src/advisor/advisor-cost-functional.test.ts +92 -0
  16. package/src/advisor/index.ts +6 -0
  17. package/src/agent/__tests__/agent-command.test.ts +597 -0
  18. package/src/agent/agent-command-module.ts +89 -0
  19. package/src/agent/agent-command-parser.ts +180 -0
  20. package/src/agent/agent-command.ts +255 -0
  21. package/src/agent/index.ts +7 -0
  22. package/src/background/__tests__/background-command-module.test.ts +261 -0
  23. package/src/background/background-command-module.ts +52 -0
  24. package/src/background/background-command.ts +65 -0
  25. package/src/background/index.ts +6 -0
  26. package/src/command-module-utils.test.ts +37 -0
  27. package/src/command-module-utils.ts +35 -0
  28. package/src/compact/__tests__/compact-command-module.test.ts +181 -0
  29. package/src/compact/compact-command-module.ts +59 -0
  30. package/src/compact/compact-command.ts +33 -0
  31. package/src/compact/index.ts +6 -0
  32. package/src/context/__tests__/context-command-module.test.ts +502 -0
  33. package/src/context/auto-compact-format.ts +25 -0
  34. package/src/context/context-breakdown.ts +295 -0
  35. package/src/context/context-command-module.ts +82 -0
  36. package/src/context/context-command.ts +291 -0
  37. package/src/context/index.ts +6 -0
  38. package/src/default/__tests__/__snapshots__/model-exposure.test.ts.snap +121 -0
  39. package/src/default/__tests__/default-command-modules.test.ts +192 -0
  40. package/src/default/__tests__/model-exposure.test.ts +269 -0
  41. package/src/default/__tests__/org-policy-forwarding.test.ts +106 -0
  42. package/src/default/__tests__/provider-environment-forwarding.test.ts +98 -0
  43. package/src/default/default-command-modules.ts +247 -0
  44. package/src/default/index.ts +5 -0
  45. package/src/devices/__tests__/devices-command-module.test.ts +299 -0
  46. package/src/devices/devices-command-module.ts +308 -0
  47. package/src/devices/devices-command-port.ts +143 -0
  48. package/src/devices/index.ts +14 -0
  49. package/src/doctor/__tests__/doctor-command-module.test.ts +145 -0
  50. package/src/doctor/__tests__/doctor-display-vocabulary.test.ts +38 -0
  51. package/src/doctor/__tests__/doctor-fixture.ts +146 -0
  52. package/src/doctor/__tests__/doctor-redaction.test.ts +29 -0
  53. package/src/doctor/__tests__/doctor-runner.test.ts +484 -0
  54. package/src/doctor/doctor-command-module.ts +132 -0
  55. package/src/doctor/doctor-extensions-probe.ts +323 -0
  56. package/src/doctor/doctor-node-deps.ts +88 -0
  57. package/src/doctor/doctor-redaction.ts +99 -0
  58. package/src/doctor/doctor-render.ts +70 -0
  59. package/src/doctor/doctor-repair.ts +136 -0
  60. package/src/doctor/doctor-runner.ts +122 -0
  61. package/src/doctor/doctor-settings-probe.ts +239 -0
  62. package/src/doctor/doctor-storage-probe.ts +187 -0
  63. package/src/doctor/doctor-types.ts +109 -0
  64. package/src/doctor/index.ts +32 -0
  65. package/src/editor/__tests__/editor-command-functional.test.ts +124 -0
  66. package/src/editor/editor-command-module.ts +53 -0
  67. package/src/editor/editor-command.ts +58 -0
  68. package/src/editor/index.ts +7 -0
  69. package/src/editor/resolve-editor.ts +21 -0
  70. package/src/effort/effort-command-module.ts +51 -0
  71. package/src/effort/effort-command.test.ts +203 -0
  72. package/src/effort/effort-command.ts +136 -0
  73. package/src/effort/index.ts +6 -0
  74. package/src/events/__tests__/events-command.test.ts +73 -0
  75. package/src/events/events-command-module.ts +49 -0
  76. package/src/events/events-command.ts +69 -0
  77. package/src/events/index.ts +2 -0
  78. package/src/exit/__tests__/exit-command-module.test.ts +61 -0
  79. package/src/exit/exit-command-module.ts +47 -0
  80. package/src/exit/exit-command.ts +26 -0
  81. package/src/exit/index.ts +6 -0
  82. package/src/fork/__tests__/fork-command.test.ts +184 -0
  83. package/src/fork/fork-command-module.ts +61 -0
  84. package/src/fork/fork-command.ts +162 -0
  85. package/src/fork/index.ts +8 -0
  86. package/src/git/__tests__/fake-git-port.ts +35 -0
  87. package/src/git/__tests__/git-command-module.test.ts +111 -0
  88. package/src/git/__tests__/git-commit.test.ts +185 -0
  89. package/src/git/git-command-module.ts +138 -0
  90. package/src/git/git-commit.ts +254 -0
  91. package/src/git/index.ts +20 -0
  92. package/src/goal/__tests__/goal-command.test.ts +80 -0
  93. package/src/goal/goal-command-module.ts +49 -0
  94. package/src/goal/goal-command.ts +69 -0
  95. package/src/goal/index.ts +6 -0
  96. package/src/handoff/__tests__/handoff-command-module.test.ts +25 -0
  97. package/src/handoff/__tests__/handoff-command.test.ts +218 -0
  98. package/src/handoff/handoff-command-module.ts +49 -0
  99. package/src/handoff/handoff-command.ts +187 -0
  100. package/src/handoff/index.ts +6 -0
  101. package/src/help/__tests__/help-command-module.test.ts +134 -0
  102. package/src/help/__tests__/help-command.test.ts +130 -0
  103. package/src/help/help-command-module.ts +47 -0
  104. package/src/help/help-command.ts +11 -0
  105. package/src/help/index.ts +6 -0
  106. package/src/index.ts +163 -0
  107. package/src/keybindings/__tests__/keybindings-command-module.test.ts +57 -0
  108. package/src/keybindings/index.ts +6 -0
  109. package/src/keybindings/keybindings-command-module.ts +88 -0
  110. package/src/language/__tests__/language-command-module.test.ts +126 -0
  111. package/src/language/index.ts +6 -0
  112. package/src/language/language-command-module.ts +55 -0
  113. package/src/language/language-command.ts +52 -0
  114. package/src/mcp-activation/__tests__/mcp-activation-command.test.ts +895 -0
  115. package/src/mcp-activation/__tests__/mcp-model-view.test.ts +242 -0
  116. package/src/mcp-activation/__tests__/mcp-skill-command.test.ts +141 -0
  117. package/src/mcp-activation/index.ts +12 -0
  118. package/src/mcp-activation/mcp-activation-command-module.ts +123 -0
  119. package/src/mcp-activation/mcp-activation-command.ts +662 -0
  120. package/src/mcp-activation/mcp-model-notice.ts +99 -0
  121. package/src/mcp-activation/mcp-model-status.ts +104 -0
  122. package/src/memory/__tests__/memory-command-module.test.ts +476 -0
  123. package/src/memory/__tests__/memory-command-unavailable.test.ts +63 -0
  124. package/src/memory/index.ts +6 -0
  125. package/src/memory/memory-command-module.ts +77 -0
  126. package/src/memory/memory-command.ts +326 -0
  127. package/src/mode/__tests__/mode-command-module.test.ts +230 -0
  128. package/src/mode/index.ts +6 -0
  129. package/src/mode/mode-command-module.ts +55 -0
  130. package/src/mode/mode-command.ts +67 -0
  131. package/src/model/__tests__/model-command-module.test.ts +80 -0
  132. package/src/model/__tests__/model-command.test.ts +265 -0
  133. package/src/model/index.ts +6 -0
  134. package/src/model/model-command-module.ts +56 -0
  135. package/src/model/model-command.ts +249 -0
  136. package/src/output-style/__tests__/output-style-command.test.ts +96 -0
  137. package/src/output-style/index.ts +6 -0
  138. package/src/output-style/output-style-command-module.ts +143 -0
  139. package/src/peers/__tests__/peers-command-module.test.ts +24 -0
  140. package/src/peers/__tests__/peers-command.test.ts +399 -0
  141. package/src/peers/index.ts +9 -0
  142. package/src/peers/peers-command-module.ts +49 -0
  143. package/src/peers/peers-command.ts +220 -0
  144. package/src/permissions/__tests__/permissions-command-module.test.ts +276 -0
  145. package/src/permissions/index.ts +6 -0
  146. package/src/permissions/permissions-command-module.ts +55 -0
  147. package/src/permissions/permissions-command.ts +89 -0
  148. package/src/plan/__tests__/plan-command.test.ts +129 -0
  149. package/src/plan/index.ts +9 -0
  150. package/src/plan/plan-command-module.ts +49 -0
  151. package/src/plan/plan-command.ts +92 -0
  152. package/src/plugin/__tests__/plugin-active-turn.test.ts +100 -0
  153. package/src/plugin/__tests__/plugin-command-module.test.ts +300 -0
  154. package/src/plugin/__tests__/plugin-host-boundary.test.ts +11 -0
  155. package/src/plugin/index.ts +7 -0
  156. package/src/plugin/plugin-command-module.ts +85 -0
  157. package/src/plugin/plugin-command.ts +271 -0
  158. package/src/preset/__tests__/host-supplied-preset-registry.test.ts +98 -0
  159. package/src/preset/__tests__/preset-command-module.test.ts +220 -0
  160. package/src/preset/index.ts +6 -0
  161. package/src/preset/preset-command-module.ts +80 -0
  162. package/src/preset/preset-command.ts +148 -0
  163. package/src/provider/__tests__/optional-provider-credentials.test.ts +130 -0
  164. package/src/provider/__tests__/org-policy.test.ts +174 -0
  165. package/src/provider/__tests__/provider-command-module.test.ts +884 -0
  166. package/src/provider/__tests__/provider-connection-setup.test.ts +567 -0
  167. package/src/provider/__tests__/provider-reference-switch.test.ts +113 -0
  168. package/src/provider/__tests__/provider-setup-flow.test.ts +244 -0
  169. package/src/provider/__tests__/provider-startup-host-settings.test.ts +16 -0
  170. package/src/provider/__tests__/scripted-interaction.ts +40 -0
  171. package/src/provider/index.ts +32 -0
  172. package/src/provider/provider-command-execution.ts +235 -0
  173. package/src/provider/provider-command-module.ts +82 -0
  174. package/src/provider/provider-command-profile-lifecycle.ts +239 -0
  175. package/src/provider/provider-command-profile-operations.ts +218 -0
  176. package/src/provider/provider-command-profile.ts +116 -0
  177. package/src/provider/provider-command-setup.ts +79 -0
  178. package/src/provider/provider-connection-setup.ts +287 -0
  179. package/src/provider/provider-connection-validation.ts +38 -0
  180. package/src/provider/provider-onboarding.ts +73 -0
  181. package/src/provider/provider-prompt-interaction.ts +28 -0
  182. package/src/provider/provider-setup-ask.ts +67 -0
  183. package/src/provider/provider-setup-flow.ts +326 -0
  184. package/src/provider/provider-startup.ts +225 -0
  185. package/src/remote-control/__tests__/remote-control-command-module.test.ts +36 -0
  186. package/src/remote-control/__tests__/remote-control-command.test.ts +216 -0
  187. package/src/remote-control/index.ts +8 -0
  188. package/src/remote-control/remote-control-command-module.ts +61 -0
  189. package/src/remote-control/remote-control-command.ts +138 -0
  190. package/src/reset/__tests__/reset-command-module.test.ts +63 -0
  191. package/src/reset/index.ts +2 -0
  192. package/src/reset/reset-command-module.ts +47 -0
  193. package/src/reset/reset-command.ts +10 -0
  194. package/src/rewind/__tests__/rewind-command-module.test.ts +287 -0
  195. package/src/rewind/index.ts +2 -0
  196. package/src/rewind/rewind-command-module.ts +56 -0
  197. package/src/rewind/rewind-command.ts +279 -0
  198. package/src/sandbox/__tests__/sandbox-command.test.ts +80 -0
  199. package/src/sandbox/index.ts +2 -0
  200. package/src/sandbox/sandbox-command-module.ts +56 -0
  201. package/src/sandbox/sandbox-command.ts +94 -0
  202. package/src/schedule/__tests__/loop-command-functional.test.ts +399 -0
  203. package/src/schedule/__tests__/loop-command.test.ts +594 -0
  204. package/src/schedule/__tests__/loop-jitter.test.ts +25 -0
  205. package/src/schedule/__tests__/monitor-model-permission-functional.test.ts +126 -0
  206. package/src/schedule/__tests__/schedule-command.test.ts +247 -0
  207. package/src/schedule/__tests__/schedule-redos.test.ts +114 -0
  208. package/src/schedule/index.ts +12 -0
  209. package/src/schedule/loop-command.ts +427 -0
  210. package/src/schedule/loop-jitter.ts +41 -0
  211. package/src/schedule/schedule-command-module.ts +184 -0
  212. package/src/schedule/schedule-command.ts +161 -0
  213. package/src/schedule/schedule-spec-parser.ts +70 -0
  214. package/src/session/__tests__/model-pricing.test.ts +66 -0
  215. package/src/session/__tests__/rename-host-persistence.test.ts +72 -0
  216. package/src/session/__tests__/session-command-module.test.ts +589 -0
  217. package/src/session/index.ts +17 -0
  218. package/src/session/model-pricing.ts +26 -0
  219. package/src/session/session-command-module.ts +197 -0
  220. package/src/session/session-command.ts +272 -0
  221. package/src/settings/index.ts +7 -0
  222. package/src/settings/settings-command-module.ts +47 -0
  223. package/src/shell/__tests__/resolve-shell-host.test.ts +11 -0
  224. package/src/shell/__tests__/shell-command-functional.test.ts +118 -0
  225. package/src/shell/index.ts +8 -0
  226. package/src/shell/resolve-shell.ts +25 -0
  227. package/src/shell/shell-command-module.ts +53 -0
  228. package/src/shell/shell-command.ts +48 -0
  229. package/src/shell/spawn-inherited.ts +32 -0
  230. package/src/skills/__tests__/skill-read-command.test.ts +64 -0
  231. package/src/skills/__tests__/skills-command-module.test.ts +200 -0
  232. package/src/skills/index.ts +7 -0
  233. package/src/skills/skill-read-command.ts +60 -0
  234. package/src/skills/skills-command-module.ts +76 -0
  235. package/src/skills/skills-command.ts +108 -0
  236. package/src/statusline/__tests__/statusline-command-module.test.ts +96 -0
  237. package/src/statusline/index.ts +6 -0
  238. package/src/statusline/statusline-command-module.ts +55 -0
  239. package/src/statusline/statusline-command.ts +80 -0
  240. package/src/terminal-client/__tests__/terminal-client-commands.test.ts +216 -0
  241. package/src/terminal-client/index.ts +5 -0
  242. package/src/terminal-client/terminal-client-commands.ts +109 -0
  243. package/src/theme/__tests__/theme-command.test.ts +204 -0
  244. package/src/theme/index.ts +12 -0
  245. package/src/theme/theme-command-module.ts +221 -0
  246. package/src/user-local/__tests__/user-local-command.test.ts +180 -0
  247. package/src/user-local/index.ts +13 -0
  248. package/src/user-local/user-local-command-constants.ts +5 -0
  249. package/src/user-local/user-local-command-module.ts +65 -0
  250. package/src/user-local/user-local-command.ts +214 -0
  251. package/src/user-local/user-local-memory-command.ts +160 -0
@@ -0,0 +1,287 @@
1
+ import {
2
+ ENV_REFERENCE_PREFIX,
3
+ findProviderDefinition,
4
+ isEnvReference,
5
+ selectAction,
6
+ textAction,
7
+ } from '@robota-sdk/agent-core';
8
+ import {
9
+ buildProviderSetupPatch,
10
+ formatOrgPolicyViolationMessage,
11
+ mergeProviderPatch,
12
+ suggestProviderProfileName,
13
+ } from '@robota-sdk/agent-framework';
14
+
15
+ import { runProviderSetupAsk } from './provider-setup-ask.js';
16
+
17
+ import type { ICredentialKey, IUserInteraction } from '@robota-sdk/agent-core';
18
+ import type {
19
+ IProviderCommandModuleOptions,
20
+ IProviderSetupInput,
21
+ TProviderConnectionStage,
22
+ } from '@robota-sdk/agent-framework';
23
+ import type { ICommandResult } from '@robota-sdk/agent-interface-command';
24
+ import type { IProviderSetupFlowState } from './provider-setup-flow.js';
25
+
26
+ let nextAttempt = 0;
27
+ const STAGE_LABELS: Record<TProviderConnectionStage, string> = {
28
+ 'awaiting-approval': 'Waiting for browser approval',
29
+ exchanging: 'Completing browser authentication',
30
+ validating: 'Checking the connection',
31
+ saving: 'Finishing connection setup',
32
+ };
33
+
34
+ export function hasProviderConnectionMethods(
35
+ type: string,
36
+ options: IProviderCommandModuleOptions,
37
+ ): boolean {
38
+ return (
39
+ (findProviderDefinition(options.providerDefinitions, type)?.connectionMethods?.length ?? 0) > 0
40
+ );
41
+ }
42
+
43
+ /** Both frontends acquire new keys through the host; settings retain references to stored keys. */
44
+ export async function runProviderConnectionSetup(
45
+ ui: IUserInteraction,
46
+ flow: IProviderSetupFlowState,
47
+ options: IProviderCommandModuleOptions,
48
+ isSetupRequired: boolean,
49
+ replace = false,
50
+ ): Promise<ICommandResult> {
51
+ const definition = findProviderDefinition(options.providerDefinitions, flow.type);
52
+ const profile =
53
+ flow.profileName ??
54
+ suggestProviderProfileName(
55
+ { type: flow.type },
56
+ { existingProfileNames: flow.existingProfileNames },
57
+ );
58
+ const initial = options.settings.readMergedSettings();
59
+ const previous = initial.providers?.[profile];
60
+ const previousSnapshot = JSON.stringify(previous);
61
+ if (
62
+ options.orgPolicy?.allowedProviders &&
63
+ !options.orgPolicy.allowedProviders.includes(profile)
64
+ ) {
65
+ return {
66
+ success: false,
67
+ message: formatOrgPolicyViolationMessage(
68
+ `Provider profile "${profile}" is not allowed by your organization policy.`,
69
+ options.orgPolicy.adminContact,
70
+ ),
71
+ };
72
+ }
73
+ const methods = (definition?.connectionMethods ?? []).filter(
74
+ (method) => options.orgPolicy?.requireApiKeyFromEnv !== true || method === 'api-key',
75
+ );
76
+ let description = options.orgPolicy?.requireApiKeyFromEnv
77
+ ? formatOrgPolicyViolationMessage(
78
+ 'Your organization requires an environment reference such as $ENV:OPENROUTER_API_KEY.',
79
+ options.orgPolicy.adminContact,
80
+ )
81
+ : undefined;
82
+ for (;;) {
83
+ const methodAnswer = await ui.ask(
84
+ selectAction(
85
+ 'provider-connection-method',
86
+ `Connect with ${definition?.displayName ?? flow.type}`,
87
+ methods.map((method) => ({
88
+ value: method,
89
+ label: method === 'api-key' ? 'API key' : 'Browser',
90
+ })),
91
+ { description },
92
+ ),
93
+ );
94
+ if (methodAnswer.type !== 'answer') return cancelled();
95
+ const method = methodAnswer.values[0];
96
+ if ((method !== 'api-key' && method !== 'browser') || !methods.includes(method))
97
+ return cancelled();
98
+ let apiKey: string | undefined;
99
+ if (method === 'api-key') {
100
+ const keyAnswer = await ui.ask(
101
+ textAction('provider-connection-key', 'API key or $ENV:VARIABLE_NAME', {
102
+ masked: true,
103
+ allowEmpty: false,
104
+ }),
105
+ );
106
+ if (keyAnswer.type !== 'answer') return cancelled();
107
+ apiKey = keyAnswer.text?.trim();
108
+ if (!apiKey || (options.orgPolicy?.requireApiKeyFromEnv && !isEnvReference(apiKey))) {
109
+ description = options.orgPolicy?.requireApiKeyFromEnv
110
+ ? formatOrgPolicyViolationMessage(
111
+ 'Enter an environment reference such as $ENV:OPENROUTER_API_KEY.',
112
+ options.orgPolicy.adminContact,
113
+ )
114
+ : 'An API key is required.';
115
+ continue;
116
+ }
117
+ if (isEnvReference(apiKey)) {
118
+ const variable = apiKey.slice(ENV_REFERENCE_PREFIX.length).trim();
119
+ if (!variable || !(options.env ?? process.env)[variable]?.trim()) {
120
+ description =
121
+ 'The environment reference is unset or invalid. Set its variable, then choose API key to try again.';
122
+ continue;
123
+ }
124
+ }
125
+ }
126
+ const controller = new AbortController();
127
+ let progress: AbortController | undefined;
128
+ let sequence = 0;
129
+ const attempt = ++nextAttempt;
130
+ const stopProgress = (): void => {
131
+ progress?.abort();
132
+ progress = undefined;
133
+ };
134
+ const onProgress = (stage: TProviderConnectionStage): void => {
135
+ stopProgress();
136
+ const prompt = new AbortController();
137
+ progress = prompt;
138
+ void ui
139
+ .ask(
140
+ selectAction(
141
+ `provider-connection-progress-${attempt}-${++sequence}`,
142
+ STAGE_LABELS[stage],
143
+ [{ value: 'cancel', label: 'Cancel connection' }],
144
+ { description: 'Keep this session open. You can cancel and choose API key setup.' },
145
+ ),
146
+ { signal: prompt.signal },
147
+ )
148
+ .then((response) => {
149
+ if (response.type === 'answer' || !prompt.signal.aborted) controller.abort();
150
+ })
151
+ .catch(() => {
152
+ if (!prompt.signal.aborted) controller.abort();
153
+ });
154
+ };
155
+ let result: ICommandResult | undefined;
156
+ const persist = async (reference?: ICredentialKey, hostSignal?: AbortSignal): Promise<void> => {
157
+ stopProgress();
158
+ const signal = hostSignal ?? controller.signal;
159
+ let input: IProviderSetupInput | undefined;
160
+ const modelFlow = {
161
+ ...flow,
162
+ profileName: profile,
163
+ steps: flow.steps.filter((step) => step.key !== 'apiKey' && step.key !== 'baseURL'),
164
+ };
165
+ if (modelFlow.steps.length === 0) {
166
+ input = {
167
+ profile,
168
+ type: flow.type,
169
+ model: definition?.defaults?.model,
170
+ setCurrent: flow.setCurrent ?? true,
171
+ };
172
+ } else {
173
+ await runProviderSetupAsk(
174
+ ui,
175
+ modelFlow,
176
+ (completed) => {
177
+ input = completed;
178
+ return { success: true, message: '' };
179
+ },
180
+ 'Provider connection cancelled.',
181
+ signal,
182
+ );
183
+ }
184
+ if (!input || signal.aborted || controller.signal.aborted) {
185
+ controller.abort();
186
+ throw new Error('Provider connection cancelled.');
187
+ }
188
+ const current = options.settings.readMergedSettings();
189
+ if (
190
+ JSON.stringify(current.providers?.[profile]) !== previousSnapshot ||
191
+ current.currentProvider !== initial.currentProvider
192
+ ) {
193
+ throw new Error('Provider connection changed while setup was pending.');
194
+ }
195
+ const credential =
196
+ reference === undefined
197
+ ? { apiKeyEnv: apiKey?.slice(ENV_REFERENCE_PREFIX.length).trim() }
198
+ : { apiKeyRef: reference };
199
+ const patch = buildProviderSetupPatch(
200
+ { ...input, ...credential },
201
+ {
202
+ providerDefinitions: options.providerDefinitions,
203
+ env: options.env,
204
+ },
205
+ );
206
+ if (replace && previous !== undefined) {
207
+ const retained = { ...previous };
208
+ delete retained.apiKey;
209
+ delete retained.apiKeyRef;
210
+ patch.providers[profile] = { ...retained, ...patch.providers[profile] };
211
+ }
212
+ options.settings.writeTargetSettings(
213
+ mergeProviderPatch(options.settings.readTargetSettings(), patch),
214
+ );
215
+ const active = initial.currentProvider === profile;
216
+ result = {
217
+ success: true,
218
+ message: `Provider ${profile} connected.`,
219
+ ...(replace
220
+ ? active
221
+ ? {
222
+ hostActions: [
223
+ {
224
+ type: 'session-restart',
225
+ reason: 'other',
226
+ message: 'Provider connection restart',
227
+ } as const,
228
+ ],
229
+ }
230
+ : {}
231
+ : {
232
+ hostActions: isSetupRequired
233
+ ? [{ type: 'provider-hot-swap', profileName: profile } as const]
234
+ : [
235
+ {
236
+ type: 'session-restart',
237
+ reason: 'other',
238
+ message: 'Provider setup restart',
239
+ } as const,
240
+ ],
241
+ }),
242
+ };
243
+ };
244
+ try {
245
+ if (apiKey !== undefined && isEnvReference(apiKey)) {
246
+ await persist();
247
+ } else if (options.connectionHost === undefined) {
248
+ description =
249
+ 'Host connection support is unavailable. Choose API key and use an environment reference.';
250
+ continue;
251
+ } else {
252
+ await options.connectionHost.connect(
253
+ {
254
+ type: flow.type,
255
+ profile,
256
+ method,
257
+ ...(apiKey === undefined ? {} : { apiKey }),
258
+ signal: controller.signal,
259
+ ...(method === 'browser' ? { onProgress } : {}),
260
+ },
261
+ persist,
262
+ );
263
+ }
264
+ if (result === undefined) throw new Error('The host did not save a connection.');
265
+ return result;
266
+ } catch {
267
+ const recovery =
268
+ method === 'browser' && flow.type === 'openrouter'
269
+ ? ' If OpenRouter issued a key during this attempt, it may remain in your account. Review OpenRouter Keys at https://openrouter.ai/keys.'
270
+ : '';
271
+ if (controller.signal.aborted) {
272
+ description = `Connection cancelled. Choose a connection method to try again.${recovery}`;
273
+ continue;
274
+ }
275
+ return {
276
+ success: false,
277
+ message: `Connection failed. Retry with ${replace ? `/provider reconnect ${profile}` : `/provider add ${flow.type}`} and choose ${flow.type === 'openrouter' ? 'an ordinary OpenRouter API key or Browser' : 'API key or Browser'}.${recovery}`,
278
+ };
279
+ } finally {
280
+ stopProgress();
281
+ }
282
+ }
283
+ }
284
+
285
+ function cancelled(): ICommandResult {
286
+ return { success: true, message: 'Provider connection cancelled.' };
287
+ }
@@ -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
+ }
@@ -0,0 +1,73 @@
1
+ import type { TPromptInput } from './provider-setup-flow.js';
2
+ import type { ITerminalOutput } from '@robota-sdk/agent-core';
3
+
4
+ export type TOnboardingPath = 'has-key' | 'free-key' | 'local' | 'connect';
5
+
6
+ export interface IOnboardingResult {
7
+ path: TOnboardingPath;
8
+ preselectedType?: string;
9
+ }
10
+
11
+ const ONBOARDING_PROMPT = `
12
+ Do you have an API key for an AI provider?
13
+
14
+ 1. Yes, I have an API key
15
+ 2. No — get a free key (Google Gemini, takes ~2 min)
16
+ 3. No — use a local model (LM Studio, no API key needed)
17
+
18
+ Choose [1-3] (default: 1): `;
19
+
20
+ const GEMINI_GUIDE = `
21
+ ── Get a free Gemini API key ────────────────────────────────────────────────
22
+
23
+ 1. Open https://aistudio.google.com/apikey in your browser
24
+ 2. Sign in with your Google account
25
+ 3. Click "Create API key" (takes ~30 seconds)
26
+ 4. Copy the key starting with "AIza..."
27
+
28
+ ─────────────────────────────────────────────────────────────────────────────
29
+ `;
30
+
31
+ const LOCAL_MODEL_GUIDE = `
32
+ ── Set up a local model with LM Studio ──────────────────────────────────────
33
+
34
+ 1. Download LM Studio from https://lmstudio.ai
35
+ 2. Open LM Studio → search for a model (e.g. "llama" or "phi") → Download
36
+ 3. Go to Developer tab → click Start Server
37
+ (default address: http://localhost:1234)
38
+
39
+ When the server is running, come back here and press Enter to continue.
40
+
41
+ ─────────────────────────────────────────────────────────────────────────────
42
+ `;
43
+
44
+ export async function runOnboardingBranch(
45
+ promptInput: TPromptInput,
46
+ terminal: ITerminalOutput,
47
+ offerOpenRouter = false,
48
+ ): Promise<IOnboardingResult> {
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);
56
+ const choice = raw.trim() || '1';
57
+
58
+ if (offerOpenRouter && choice === '4') return { path: 'connect', preselectedType: 'openrouter' };
59
+
60
+ if (choice === '2') {
61
+ terminal.writeLine(GEMINI_GUIDE);
62
+ await promptInput(' Press Enter when you have your API key: ');
63
+ return { path: 'free-key', preselectedType: 'gemini' };
64
+ }
65
+
66
+ if (choice === '3') {
67
+ terminal.writeLine(LOCAL_MODEL_GUIDE);
68
+ await promptInput(' Press Enter when LM Studio server is running: ');
69
+ return { path: 'local', preselectedType: 'gemma' };
70
+ }
71
+
72
+ return { path: 'has-key' };
73
+ }
@@ -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
+ }