@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,239 @@
1
+ import { confirmAction, isConfirmed, selectAction, textAction } from '@robota-sdk/agent-core';
2
+ import {
3
+ deleteProviderProfile,
4
+ sanitizeProviderProfileName,
5
+ upsertProviderProfile,
6
+ } from '@robota-sdk/agent-framework';
7
+
8
+ import { formatProviderChoiceLabel } from './provider-command-profile-operations.js';
9
+
10
+ import type { IUserInteraction } from '@robota-sdk/agent-core';
11
+ import type {
12
+ IProviderCommandModuleOptions,
13
+ IProviderProfileSettings,
14
+ } from '@robota-sdk/agent-framework';
15
+ import type { ICommandResult } from '@robota-sdk/agent-interface-command';
16
+
17
+ const MAX_DUPLICATE_PROFILE_SUFFIX = 1000;
18
+ const PROVIDER_RESTART_ACTION = {
19
+ type: 'session-restart',
20
+ reason: 'other',
21
+ } as const;
22
+
23
+ export async function buildProviderDuplicate(
24
+ ui: IUserInteraction,
25
+ profileName: string,
26
+ options: IProviderCommandModuleOptions,
27
+ ): Promise<ICommandResult> {
28
+ const settings = options.settings.readMergedSettings();
29
+ if (!settings.providers?.[profileName]) {
30
+ return { message: `Provider profile "${profileName}" was not found.`, success: false };
31
+ }
32
+ const defaultName = suggestDuplicateProfileName(profileName, Object.keys(settings.providers));
33
+ let errorMessage: string | undefined;
34
+ for (;;) {
35
+ const response = await ui.ask(
36
+ textAction('provider-duplicate', `Duplicate ${profileName} as`, {
37
+ description: errorMessage,
38
+ placeholder: defaultName,
39
+ allowEmpty: true,
40
+ }),
41
+ );
42
+ if (response.type === 'cancelled') {
43
+ return { message: 'Provider duplicate cancelled.', success: true };
44
+ }
45
+ const value = response.text ?? '';
46
+ // Re-ask on validation failure, surfacing the reason (CMD-004 re-ask loop replaces the closure).
47
+ const validationMessage = validateDuplicateProfileName(value, defaultName, options);
48
+ if (validationMessage !== undefined) {
49
+ errorMessage = validationMessage;
50
+ continue;
51
+ }
52
+ return completeProviderDuplicate(profileName, value, defaultName, options);
53
+ }
54
+ }
55
+
56
+ function validateDuplicateProfileName(
57
+ value: string,
58
+ defaultName: string,
59
+ options: IProviderCommandModuleOptions,
60
+ ): string | undefined {
61
+ const name = normalizeProfileName(value, defaultName);
62
+ if (name.length === 0) {
63
+ return 'Required';
64
+ }
65
+ if (options.settings.readMergedSettings().providers?.[name] !== undefined) {
66
+ return `Provider profile "${name}" already exists`;
67
+ }
68
+ return undefined;
69
+ }
70
+
71
+ function completeProviderDuplicate(
72
+ profileName: string,
73
+ value: string,
74
+ defaultName: string,
75
+ options: IProviderCommandModuleOptions,
76
+ ): ICommandResult {
77
+ const settings = options.settings.readMergedSettings();
78
+ const sourceProfile = settings.providers?.[profileName];
79
+ if (!sourceProfile) {
80
+ return { message: `Provider profile "${profileName}" was not found.`, success: false };
81
+ }
82
+ const targetName = normalizeProfileName(value, defaultName);
83
+ const validationMessage = validateDuplicateProfileName(targetName, defaultName, options);
84
+ if (validationMessage !== undefined) {
85
+ return { message: validationMessage, success: false };
86
+ }
87
+ options.settings.writeTargetSettings(
88
+ upsertProviderProfile(options.settings.readTargetSettings(), targetName, { ...sourceProfile }),
89
+ );
90
+ return {
91
+ message: `Provider profile duplicated: ${profileName} -> ${targetName}.`,
92
+ success: true,
93
+ };
94
+ }
95
+
96
+ /**
97
+ * #3282 §4b: the direct, non-interactive delete itself — no replacement ask (the interactive
98
+ * per-profile menu's Delete, `buildProviderDelete` below, still offers one); refuses instead when the
99
+ * profile is the one in use. Never asks to CONFIRM either — its caller in `provider-command-execution`
100
+ * does that first, when a human can answer, before reaching this function; this function itself is
101
+ * called only once a delete is already decided (confirmed by a human, or `--confirmed` from the
102
+ * Settings screen's own ConfirmDialog).
103
+ */
104
+ export function buildProviderProfileDelete(
105
+ providers: Record<string, IProviderProfileSettings> | undefined,
106
+ currentProvider: string | undefined,
107
+ profileName: string | undefined,
108
+ options: IProviderCommandModuleOptions,
109
+ ): ICommandResult {
110
+ if (!profileName) {
111
+ return { message: 'Usage: /provider delete <profile>', success: false };
112
+ }
113
+ if (!providers?.[profileName]) {
114
+ return { message: `Provider profile "${profileName}" was not found.`, success: false };
115
+ }
116
+ if (Object.keys(providers).length <= 1) {
117
+ return { message: 'Cannot delete the only provider profile.', success: false };
118
+ }
119
+ if (currentProvider === profileName) {
120
+ return {
121
+ message: 'Cannot delete the profile in use. Switch to another profile first.',
122
+ success: false,
123
+ };
124
+ }
125
+ if (options.settings.readTargetSettings().providers?.[profileName] === undefined) {
126
+ return {
127
+ message: `Provider profile "${profileName}" is not stored in the active write target; edit its source settings file or override it before deleting.`,
128
+ success: false,
129
+ };
130
+ }
131
+ options.settings.writeTargetSettings(
132
+ deleteProviderProfile(options.settings.readTargetSettings(), profileName),
133
+ );
134
+ return { message: `Provider profile deleted: ${profileName}.`, success: true };
135
+ }
136
+
137
+ export async function buildProviderDelete(
138
+ ui: IUserInteraction,
139
+ profileName: string,
140
+ options: IProviderCommandModuleOptions,
141
+ ): Promise<ICommandResult> {
142
+ const settings = options.settings.readMergedSettings();
143
+ const providers = settings.providers ?? {};
144
+ if (!providers[profileName]) {
145
+ return { message: `Provider profile "${profileName}" was not found.`, success: false };
146
+ }
147
+ if (Object.keys(providers).length <= 1) {
148
+ return { message: 'Cannot delete the only provider profile.', success: false };
149
+ }
150
+ if (options.settings.readTargetSettings().providers?.[profileName] === undefined) {
151
+ return {
152
+ message: `Provider profile "${profileName}" is not stored in the active write target; edit its source settings file or override it before deleting.`,
153
+ success: false,
154
+ };
155
+ }
156
+ // #3282 §2: the GUI renders this confirm on its shared ConfirmDialog (id 'provider-delete') with
157
+ // this exact title — kept in the quoted-name form the TUI's inline picker shows the same way.
158
+ const response = await ui.ask(
159
+ confirmAction('provider-delete', `Delete profile "${profileName}"?`),
160
+ );
161
+ if (!isConfirmed(response)) {
162
+ return { message: 'Provider delete cancelled.', success: true };
163
+ }
164
+ return confirmProviderDelete(ui, profileName, options);
165
+ }
166
+
167
+ async function confirmProviderDelete(
168
+ ui: IUserInteraction,
169
+ profileName: string,
170
+ options: IProviderCommandModuleOptions,
171
+ ): Promise<ICommandResult> {
172
+ const settings = options.settings.readMergedSettings();
173
+ if (settings.currentProvider !== profileName) {
174
+ options.settings.writeTargetSettings(
175
+ deleteProviderProfile(options.settings.readTargetSettings(), profileName),
176
+ );
177
+ return { message: `Provider profile deleted: ${profileName}.`, success: true };
178
+ }
179
+ const replacementOptions = Object.entries(settings.providers ?? {})
180
+ .filter(([name]) => name !== profileName)
181
+ .map(([name, profile]) => ({
182
+ value: name,
183
+ label: formatProviderChoiceLabel(name, profile, settings.currentProvider),
184
+ }));
185
+ const response = await ui.ask(
186
+ selectAction(
187
+ 'provider-delete-replacement',
188
+ `Replacement provider for ${profileName}`,
189
+ replacementOptions,
190
+ {
191
+ maxVisible: 8,
192
+ },
193
+ ),
194
+ );
195
+ if (response.type !== 'answer' || response.values[0] === undefined) {
196
+ return { message: 'Provider delete cancelled.', success: true };
197
+ }
198
+ return completeActiveProviderDelete(profileName, response.values[0], options);
199
+ }
200
+
201
+ function completeActiveProviderDelete(
202
+ profileName: string,
203
+ replacementName: string,
204
+ options: IProviderCommandModuleOptions,
205
+ ): ICommandResult {
206
+ const settings = options.settings.readMergedSettings();
207
+ if (settings.providers?.[replacementName] === undefined || replacementName === profileName) {
208
+ return { message: `Provider profile "${replacementName}" was not found.`, success: false };
209
+ }
210
+ const target = deleteProviderProfile(options.settings.readTargetSettings(), profileName);
211
+ options.settings.writeTargetSettings({ ...target, currentProvider: replacementName });
212
+ return {
213
+ message: `Provider profile deleted: ${profileName}. Restarting with ${replacementName}...`,
214
+ success: true,
215
+ hostActions: [{ ...PROVIDER_RESTART_ACTION, message: 'Provider delete restart' }],
216
+ };
217
+ }
218
+
219
+ function suggestDuplicateProfileName(
220
+ profileName: string,
221
+ existingProfileNames: readonly string[],
222
+ ): string {
223
+ const base = sanitizeProviderProfileName(`${profileName}-copy`) ?? 'provider-copy';
224
+ if (!existingProfileNames.includes(base)) {
225
+ return base;
226
+ }
227
+ for (let index = 2; index < MAX_DUPLICATE_PROFILE_SUFFIX; index += 1) {
228
+ const candidate = `${base}-${index}`;
229
+ if (!existingProfileNames.includes(candidate)) {
230
+ return candidate;
231
+ }
232
+ }
233
+ return `${base}-${Date.now()}`;
234
+ }
235
+
236
+ function normalizeProfileName(value: string, defaultName: string): string {
237
+ const raw = value.trim() || defaultName;
238
+ return sanitizeProviderProfileName(raw) ?? '';
239
+ }
@@ -0,0 +1,218 @@
1
+ import {
2
+ buildProviderSetupPatch,
3
+ formatOrgPolicyViolationMessage,
4
+ isApiKeyPlaintext,
5
+ setCurrentProvider,
6
+ upsertProviderProfile,
7
+ } from '@robota-sdk/agent-framework';
8
+
9
+ import { runProviderSetupAsk } from './provider-command-setup.js';
10
+ import { createProviderSetupFlow } from './provider-setup-flow.js';
11
+ import {
12
+ hasProviderConnectionMethods,
13
+ runProviderConnectionSetup,
14
+ } from './provider-connection-setup.js';
15
+ import { validateProviderConnection } from './provider-connection-validation.js';
16
+
17
+ import type { IUserInteraction } from '@robota-sdk/agent-core';
18
+ import type {
19
+ IProviderCommandModuleOptions,
20
+ IProviderProfileSettings,
21
+ IProviderSetupInput,
22
+ } from '@robota-sdk/agent-framework';
23
+ import type { ICommandResult } from '@robota-sdk/agent-interface-command';
24
+
25
+ export function formatProviderChoiceLabel(
26
+ name: string,
27
+ profile: IProviderProfileSettings,
28
+ currentProvider: string | undefined,
29
+ ): string {
30
+ const marker = name === currentProvider ? '* ' : '';
31
+ return `${marker}${name}: ${profile.type ?? 'unknown'} ${profile.model ?? '(no model)'}`;
32
+ }
33
+
34
+ export async function buildProviderSwitch(
35
+ providers: Record<string, IProviderProfileSettings> | undefined,
36
+ profileName: string | undefined,
37
+ options: IProviderCommandModuleOptions,
38
+ ): Promise<ICommandResult> {
39
+ if (!profileName) {
40
+ return { message: 'Usage: /provider switch <profile>', success: false };
41
+ }
42
+ if (!providers?.[profileName]) {
43
+ return { message: `Provider profile "${profileName}" was not found.`, success: false };
44
+ }
45
+ const { orgPolicy } = options;
46
+ if (orgPolicy?.allowedProviders && !orgPolicy.allowedProviders.includes(profileName)) {
47
+ return {
48
+ message: formatOrgPolicyViolationMessage(
49
+ `Provider "${profileName}" is not allowed by your organization policy. Allowed: ${orgPolicy.allowedProviders.join(', ')}.`,
50
+ orgPolicy.adminContact,
51
+ ),
52
+ success: false,
53
+ };
54
+ }
55
+ const initial = options.settings.readMergedSettings();
56
+ if (initial.currentProvider === profileName) {
57
+ return { message: `Already using provider "${profileName}".`, success: true };
58
+ }
59
+ const profile = providers[profileName];
60
+ // #3282: the hot-swap this returns as a `hostActions` entry builds this exact profile against
61
+ // these exact definitions (`resolveUserSettingsProviderSwitch` in agent-framework). Validating it
62
+ // HERE, before anything is written, means a switch that would fail changes nothing on disk — the
63
+ // previous behavior wrote `currentProvider` unconditionally and let a downstream hot-swap failure
64
+ // (e.g. "Unknown provider: anthropic. Currently supported: ", an empty list) stand uncorrected.
65
+ try {
66
+ await validateProviderConnection(profileName, profile, options);
67
+ } catch (error) {
68
+ return {
69
+ message: `Failed to switch to "${profileName}": ${error instanceof Error ? error.message : String(error)}`,
70
+ success: false,
71
+ };
72
+ }
73
+ const target = options.settings.readTargetSettings();
74
+ const merged = options.settings.readMergedSettings();
75
+ if (
76
+ merged.currentProvider !== initial.currentProvider ||
77
+ JSON.stringify(merged.providers?.[profileName]) !== JSON.stringify(profile)
78
+ ) {
79
+ return {
80
+ success: false,
81
+ message: 'Provider settings changed while switching. Run /provider switch again.',
82
+ };
83
+ }
84
+ const next =
85
+ target.providers?.[profileName] !== undefined || merged.providers?.[profileName] !== undefined
86
+ ? { ...target, currentProvider: profileName }
87
+ : setCurrentProvider(target, profileName);
88
+ options.settings.writeTargetSettings(next);
89
+ const modelLabel = profile.model ?? 'unknown model';
90
+ return {
91
+ message: `Switched to ${profileName} (${modelLabel}). History preserved.`,
92
+ success: true,
93
+ hostActions: [{ type: 'provider-hot-swap', profileName }],
94
+ };
95
+ }
96
+
97
+ export async function buildProviderEdit(
98
+ ui: IUserInteraction,
99
+ profileName: string,
100
+ options: IProviderCommandModuleOptions,
101
+ ): Promise<ICommandResult> {
102
+ const settings = options.settings.readMergedSettings();
103
+ const profile = settings.providers?.[profileName];
104
+ if (!profile) {
105
+ return { message: `Provider profile "${profileName}" was not found.`, success: false };
106
+ }
107
+ if (!profile.type) {
108
+ return { message: `Provider profile "${profileName}" is missing type.`, success: false };
109
+ }
110
+ let flow;
111
+ try {
112
+ flow = createProviderSetupFlow(profile.type, options.providerDefinitions, {
113
+ includeEditOnlySteps: true,
114
+ profileName,
115
+ setCurrent: false,
116
+ initialValues: getProviderProfileSetupValues(profile),
117
+ });
118
+ if (hasProviderConnectionMethods(profile.type, options)) {
119
+ flow = { ...flow, steps: flow.steps.filter((step) => step.key !== 'apiKey') };
120
+ }
121
+ } catch (error) {
122
+ return { message: error instanceof Error ? error.message : String(error), success: false };
123
+ }
124
+ return runProviderSetupAsk(
125
+ ui,
126
+ flow,
127
+ (input) => completeProviderEdit(input, profileName, options),
128
+ 'Provider edit cancelled.',
129
+ );
130
+ }
131
+
132
+ /** Explicitly replace this profile's connection; ordinary editing preserves its credential. */
133
+ export async function buildProviderReconnect(
134
+ ui: IUserInteraction,
135
+ profileName: string,
136
+ options: IProviderCommandModuleOptions,
137
+ ): Promise<ICommandResult> {
138
+ const profile = options.settings.readMergedSettings().providers?.[profileName];
139
+ if (!profile?.type)
140
+ return { success: false, message: `Provider profile "${profileName}" was not found.` };
141
+ if (!hasProviderConnectionMethods(profile.type, options)) {
142
+ return {
143
+ success: false,
144
+ message: `Use /provider edit ${profileName} to update this provider's credentials.`,
145
+ };
146
+ }
147
+ const flow = createProviderSetupFlow(profile.type, options.providerDefinitions, {
148
+ profileName,
149
+ setCurrent: false,
150
+ initialValues: { ...(profile.model === undefined ? {} : { model: profile.model }) },
151
+ });
152
+ return runProviderConnectionSetup(ui, flow, options, false, true);
153
+ }
154
+
155
+ function getProviderProfileSetupValues(profile: IProviderProfileSettings): {
156
+ model?: string;
157
+ apiKey?: string;
158
+ baseURL?: string;
159
+ } {
160
+ return {
161
+ ...(typeof profile.model === 'string' ? { model: profile.model } : {}),
162
+ ...(typeof profile.apiKey === 'string' ? { apiKey: profile.apiKey } : {}),
163
+ ...(typeof profile.baseURL === 'string' ? { baseURL: profile.baseURL } : {}),
164
+ };
165
+ }
166
+
167
+ function completeProviderEdit(
168
+ input: IProviderSetupInput,
169
+ profileName: string,
170
+ options: IProviderCommandModuleOptions,
171
+ ): ICommandResult {
172
+ const merged = options.settings.readMergedSettings();
173
+ const currentProfile = merged.providers?.[profileName];
174
+ if (!currentProfile) {
175
+ return { message: `Provider profile "${profileName}" was not found.`, success: false };
176
+ }
177
+ const { orgPolicy } = options;
178
+ if (orgPolicy?.requireApiKeyFromEnv && isApiKeyPlaintext(input.apiKey)) {
179
+ return {
180
+ message: formatOrgPolicyViolationMessage(
181
+ 'Your organization policy requires API keys to be stored as environment variable references ($ENV:VAR_NAME), not as plaintext.',
182
+ orgPolicy.adminContact,
183
+ ),
184
+ success: false,
185
+ };
186
+ }
187
+ const target = options.settings.readTargetSettings();
188
+ const patch = buildProviderSetupPatch(
189
+ {
190
+ ...input,
191
+ ...(input.apiKey === undefined && currentProfile.apiKeyRef !== undefined
192
+ ? { apiKeyRef: currentProfile.apiKeyRef }
193
+ : {}),
194
+ ...(input.apiKey === undefined && currentProfile.apiKey !== undefined
195
+ ? { apiKey: currentProfile.apiKey }
196
+ : {}),
197
+ },
198
+ {
199
+ providerDefinitions: options.providerDefinitions,
200
+ ...(options.env === undefined ? {} : { env: options.env }),
201
+ },
202
+ );
203
+ const updatedProfile = patch.providers[profileName];
204
+ if (!updatedProfile) {
205
+ return { message: `Provider profile "${profileName}" was not updated.`, success: false };
206
+ }
207
+ options.settings.writeTargetSettings(
208
+ upsertProviderProfile(target, profileName, { ...currentProfile, ...updatedProfile }),
209
+ );
210
+ const isCurrent = merged.currentProvider === profileName;
211
+ return {
212
+ message: isCurrent
213
+ ? `Provider ${profileName} updated. Switching...`
214
+ : `Provider ${profileName} updated.`,
215
+ success: true,
216
+ ...(isCurrent ? { hostActions: [{ type: 'provider-hot-swap' as const, profileName }] } : {}),
217
+ };
218
+ }
@@ -0,0 +1,116 @@
1
+ import { selectAction } from '@robota-sdk/agent-core';
2
+ import { testProviderProfileCommand } from '@robota-sdk/agent-framework';
3
+
4
+ import {
5
+ buildProviderDelete,
6
+ buildProviderDuplicate,
7
+ } from './provider-command-profile-lifecycle.js';
8
+ import {
9
+ buildProviderEdit,
10
+ buildProviderReconnect,
11
+ buildProviderSwitch,
12
+ formatProviderChoiceLabel,
13
+ } from './provider-command-profile-operations.js';
14
+ import { hasProviderConnectionMethods } from './provider-connection-setup.js';
15
+
16
+ import type { IUserInteraction } from '@robota-sdk/agent-core';
17
+ import type {
18
+ IProviderCommandModuleOptions,
19
+ IProviderProfileSettings,
20
+ } from '@robota-sdk/agent-framework';
21
+ import type { ICommandResult } from '@robota-sdk/agent-interface-command';
22
+
23
+ const ACTION_SWITCH = 'switch';
24
+ const ACTION_EDIT = 'edit';
25
+ const ACTION_TEST = 'test';
26
+ const ACTION_DUPLICATE = 'duplicate';
27
+ const ACTION_DELETE = 'delete';
28
+ const ACTION_RECONNECT = 'reconnect';
29
+
30
+ /**
31
+ * Ask the user to pick a provider profile, then drive its action menu (CMD-004 inline ask). Replaces
32
+ * the former returned choice→submit continuation chain — the picker's own option list is the rendered
33
+ * profile list, so the caller no longer prepends a separate list message.
34
+ */
35
+ export async function askProviderProfileSelection(
36
+ ui: IUserInteraction,
37
+ currentProvider: string | undefined,
38
+ providers: Record<string, IProviderProfileSettings> | undefined,
39
+ options: IProviderCommandModuleOptions,
40
+ ): Promise<ICommandResult> {
41
+ const profileOptions = Object.entries(providers ?? {}).map(([name, profile]) => ({
42
+ value: name,
43
+ label: formatProviderChoiceLabel(name, profile, currentProvider),
44
+ }));
45
+ const response = await ui.ask(
46
+ selectAction('provider-profile', 'Select provider profile', profileOptions, { maxVisible: 8 }),
47
+ );
48
+ if (response.type !== 'answer' || response.values[0] === undefined) {
49
+ return { message: 'Provider profile selection cancelled.', success: true };
50
+ }
51
+ return askProviderProfileAction(ui, response.values[0], options);
52
+ }
53
+
54
+ async function askProviderProfileAction(
55
+ ui: IUserInteraction,
56
+ profileName: string,
57
+ options: IProviderCommandModuleOptions,
58
+ ): Promise<ICommandResult> {
59
+ const settings = options.settings.readMergedSettings();
60
+ if (!settings.providers?.[profileName]) {
61
+ return { message: `Provider profile "${profileName}" was not found.`, success: false };
62
+ }
63
+ // #3282 §2: no separate 'cancel' option here — the picker's own persistent Cancel affordance
64
+ // (every ask renderer offers one, e.g. `PermissionPrompt`'s ghost Cancel button in agent-ui-web)
65
+ // is the only Cancel a person sees. A second one, indistinguishable from Switch/Edit/Test/Duplicate
66
+ // in a flat list, was the audited bug (#3282 issue text): "Delete looks the same as Switch... plus
67
+ // a second Cancel." Order matters too — Switch first as the primary action, then Edit/Test/
68
+ // Duplicate, with Delete last: a client (the GUI's provider-action menu) that wants Delete
69
+ // separated and styled destructive can rely on it always being the final entry.
70
+ const response = await ui.ask(
71
+ selectAction('provider-profile-action', `Provider profile: ${profileName}`, [
72
+ { value: ACTION_SWITCH, label: 'Switch' },
73
+ { value: ACTION_EDIT, label: 'Edit' },
74
+ ...(hasProviderConnectionMethods(settings.providers[profileName]?.type ?? '', options)
75
+ ? [{ value: ACTION_RECONNECT, label: 'Reconnect' }]
76
+ : []),
77
+ { value: ACTION_TEST, label: 'Test' },
78
+ { value: ACTION_DUPLICATE, label: 'Duplicate' },
79
+ { value: ACTION_DELETE, label: 'Delete' },
80
+ ]),
81
+ );
82
+ if (response.type !== 'answer' || response.values[0] === undefined) {
83
+ return { message: 'Provider profile action cancelled.', success: true };
84
+ }
85
+ return executeProviderProfileAction(ui, profileName, response.values[0], options);
86
+ }
87
+
88
+ async function executeProviderProfileAction(
89
+ ui: IUserInteraction,
90
+ profileName: string,
91
+ action: string,
92
+ options: IProviderCommandModuleOptions,
93
+ ): Promise<ICommandResult> {
94
+ const settings = options.settings.readMergedSettings();
95
+ switch (action) {
96
+ case ACTION_SWITCH:
97
+ return buildProviderSwitch(settings.providers, profileName, options);
98
+ case ACTION_EDIT:
99
+ return buildProviderEdit(ui, profileName, options);
100
+ case ACTION_RECONNECT:
101
+ return buildProviderReconnect(ui, profileName, options);
102
+ case ACTION_TEST:
103
+ return testProviderProfileCommand(
104
+ settings.currentProvider,
105
+ settings.providers,
106
+ profileName,
107
+ options,
108
+ );
109
+ case ACTION_DUPLICATE:
110
+ return buildProviderDuplicate(ui, profileName, options);
111
+ case ACTION_DELETE:
112
+ return buildProviderDelete(ui, profileName, options);
113
+ default:
114
+ return { message: `Unknown provider profile action "${action}".`, success: false };
115
+ }
116
+ }
@@ -0,0 +1,79 @@
1
+ import { buildProviderSetupPatch, mergeProviderPatch } from '@robota-sdk/agent-framework';
2
+
3
+ import { createProviderSetupFlow } from './provider-setup-flow.js';
4
+ import { runProviderSetupAsk } from './provider-setup-ask.js';
5
+ import {
6
+ hasProviderConnectionMethods,
7
+ runProviderConnectionSetup,
8
+ } from './provider-connection-setup.js';
9
+
10
+ import type { IProviderSetupFlowState } from './provider-setup-flow.js';
11
+ import type { IUserInteraction } from '@robota-sdk/agent-core';
12
+ import type {
13
+ IProviderCommandModuleOptions,
14
+ IProviderSetupInput,
15
+ } from '@robota-sdk/agent-framework';
16
+ import type { ICommandResult } from '@robota-sdk/agent-interface-command';
17
+
18
+ export { runProviderSetupAsk } from './provider-setup-ask.js';
19
+
20
+ const PROVIDER_RESTART_ACTION = {
21
+ type: 'session-restart',
22
+ reason: 'other',
23
+ } as const;
24
+
25
+ export function createSetupFlow(
26
+ type: string,
27
+ options: IProviderCommandModuleOptions,
28
+ ): IProviderSetupFlowState {
29
+ return createProviderSetupFlow(type, options.providerDefinitions, {
30
+ existingProfileNames: Object.keys(options.settings.readMergedSettings().providers ?? {}),
31
+ });
32
+ }
33
+
34
+ /** Run the `/provider add` setup wizard inline. */
35
+ export function runProviderAddSetup(
36
+ ui: IUserInteraction,
37
+ flow: IProviderSetupFlowState,
38
+ options: IProviderCommandModuleOptions,
39
+ isSetupRequired: boolean,
40
+ ): Promise<ICommandResult> {
41
+ if (hasProviderConnectionMethods(flow.type, options)) {
42
+ return runProviderConnectionSetup(ui, flow, options, isSetupRequired);
43
+ }
44
+ return runProviderSetupAsk(
45
+ ui,
46
+ flow,
47
+ (input) => completeProviderSetup(input, options, isSetupRequired),
48
+ 'Provider setup cancelled.',
49
+ );
50
+ }
51
+
52
+ function completeProviderSetup(
53
+ input: IProviderSetupInput,
54
+ options: IProviderCommandModuleOptions,
55
+ isSetupRequired: boolean,
56
+ ): ICommandResult {
57
+ const target = options.settings.readTargetSettings();
58
+ const patch = buildProviderSetupPatch(input, {
59
+ providerDefinitions: options.providerDefinitions,
60
+ ...(options.env === undefined ? {} : { env: options.env }),
61
+ });
62
+ options.settings.writeTargetSettings(mergeProviderPatch(target, patch));
63
+ // #3282 §3: the session's own live setup-mode state (never settings — an ordinary env-default
64
+ // session also has no persisted `currentProvider`, and restarts like any other session). Only a
65
+ // session that itself started with a placeholder provider hot-swaps the real one in; every other
66
+ // `/provider add`, even a session's first EVER persisted profile, restarts as before.
67
+ if (isSetupRequired) {
68
+ return {
69
+ message: `Provider ${input.profile} configured.`,
70
+ success: true,
71
+ hostActions: [{ type: 'provider-hot-swap', profileName: input.profile }],
72
+ };
73
+ }
74
+ return {
75
+ message: `Provider ${input.profile} configured. Restarting...`,
76
+ success: true,
77
+ hostActions: [{ ...PROVIDER_RESTART_ACTION, message: 'Provider setup restart' }],
78
+ };
79
+ }