@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.
Files changed (53) hide show
  1. package/LICENSE +661 -21
  2. package/README.md +12 -6
  3. package/dist/node/index.cjs +38 -33
  4. package/dist/node/index.d.ts +68 -6
  5. package/dist/node/index.d.ts.map +1 -1
  6. package/dist/node/index.js +40 -35
  7. package/dist/node/index.js.map +1 -1
  8. package/package.json +7 -7
  9. package/src/agent/agent-command-parser.ts +1 -1
  10. package/src/agent/agent-command.ts +2 -1
  11. package/src/background/__tests__/background-command-module.test.ts +2 -5
  12. package/src/default/__tests__/default-command-modules.test.ts +5 -2
  13. package/src/default/default-command-modules.ts +6 -0
  14. package/src/editor/__tests__/editor-command-functional.test.ts +91 -0
  15. package/src/editor/editor-command-module.ts +47 -0
  16. package/src/editor/editor-command.ts +53 -0
  17. package/src/editor/index.ts +7 -0
  18. package/src/editor/resolve-editor.ts +21 -0
  19. package/src/exit/__tests__/exit-command-module.test.ts +21 -2
  20. package/src/exit/exit-command-module.ts +1 -10
  21. package/src/exit/exit-command.ts +15 -1
  22. package/src/goal/__tests__/goal-command.test.ts +75 -0
  23. package/src/goal/goal-command-module.ts +48 -0
  24. package/src/goal/goal-command.ts +70 -0
  25. package/src/goal/index.ts +6 -0
  26. package/src/index.ts +3 -0
  27. package/src/language/__tests__/language-command-module.test.ts +16 -0
  28. package/src/language/language-command-module.ts +1 -18
  29. package/src/language/language-command.ts +35 -9
  30. package/src/mode/__tests__/mode-command-module.test.ts +34 -0
  31. package/src/mode/mode-command-module.ts +1 -18
  32. package/src/mode/mode-command.ts +31 -8
  33. package/src/preset/__tests__/preset-command-module.test.ts +36 -0
  34. package/src/preset/preset-command-module.ts +1 -18
  35. package/src/preset/preset-command.ts +37 -8
  36. package/src/provider/__tests__/org-policy.test.ts +19 -13
  37. package/src/provider/__tests__/provider-command-module.test.ts +151 -80
  38. package/src/provider/__tests__/scripted-interaction.ts +28 -0
  39. package/src/provider/provider-command-execution.ts +67 -50
  40. package/src/provider/provider-command-module.ts +3 -19
  41. package/src/provider/provider-command-profile-lifecycle.ts +52 -72
  42. package/src/provider/provider-command-profile-operations.ts +15 -51
  43. package/src/provider/provider-command-profile.ts +44 -49
  44. package/src/provider/provider-command-setup.ts +56 -51
  45. package/src/session/__tests__/session-command-module.test.ts +38 -0
  46. package/src/session/session-command-module.ts +1 -10
  47. package/src/session/session-command.ts +14 -1
  48. package/src/shell/__tests__/shell-command-functional.test.ts +96 -0
  49. package/src/shell/index.ts +8 -0
  50. package/src/shell/resolve-shell.ts +25 -0
  51. package/src/shell/shell-command-module.ts +47 -0
  52. package/src/shell/shell-command.ts +44 -0
  53. package/src/shell/spawn-inherited.ts +32 -0
@@ -1,3 +1,4 @@
1
+ import { confirmAction, isConfirmed, selectAction, textAction } from '@robota-sdk/agent-core';
1
2
  import {
2
3
  deleteProviderProfile,
3
4
  sanitizeProviderProfileName,
@@ -6,48 +7,47 @@ import {
6
7
 
7
8
  import { formatProviderChoiceLabel } from './provider-command-profile-operations.js';
8
9
 
10
+ import type { IUserInteraction } from '@robota-sdk/agent-core';
9
11
  import type { IProviderCommandModuleOptions } from '@robota-sdk/agent-framework';
10
- import type { ICommandInteraction, ICommandResult } from '@robota-sdk/agent-interface-transport';
12
+ import type { ICommandResult } from '@robota-sdk/agent-interface-transport';
11
13
 
12
- const YES = 'yes';
13
14
  const MAX_DUPLICATE_PROFILE_SUFFIX = 1000;
14
15
  const PROVIDER_RESTART_EFFECT = {
15
16
  type: 'session-restart-requested',
16
17
  reason: 'other',
17
18
  } as const;
18
19
 
19
- export function buildProviderDuplicate(
20
+ export async function buildProviderDuplicate(
21
+ ui: IUserInteraction,
20
22
  profileName: string,
21
23
  options: IProviderCommandModuleOptions,
22
- ): ICommandResult {
24
+ ): Promise<ICommandResult> {
23
25
  const settings = options.settings.readMergedSettings();
24
26
  if (!settings.providers?.[profileName]) {
25
27
  return { message: `Provider profile "${profileName}" was not found.`, success: false };
26
28
  }
27
29
  const defaultName = suggestDuplicateProfileName(profileName, Object.keys(settings.providers));
28
- return {
29
- message: `Provider duplicate requested: ${profileName}`,
30
- success: true,
31
- interaction: createProviderDuplicateInteraction(profileName, defaultName, options),
32
- };
33
- }
34
-
35
- function createProviderDuplicateInteraction(
36
- profileName: string,
37
- defaultName: string,
38
- options: IProviderCommandModuleOptions,
39
- ): ICommandInteraction {
40
- return {
41
- prompt: {
42
- kind: 'text',
43
- title: `Duplicate ${profileName} as`,
44
- placeholder: defaultName,
45
- allowEmpty: true,
46
- validate: (value) => validateDuplicateProfileName(value, defaultName, options),
47
- },
48
- submit: (value) => completeProviderDuplicate(profileName, value, defaultName, options),
49
- cancel: () => ({ message: 'Provider duplicate cancelled.', success: true }),
50
- };
30
+ let errorMessage: string | undefined;
31
+ for (;;) {
32
+ const response = await ui.ask(
33
+ textAction('provider-duplicate', `Duplicate ${profileName} as`, {
34
+ description: errorMessage,
35
+ placeholder: defaultName,
36
+ allowEmpty: true,
37
+ }),
38
+ );
39
+ if (response.type === 'cancelled') {
40
+ return { message: 'Provider duplicate cancelled.', success: true };
41
+ }
42
+ const value = response.text ?? '';
43
+ // Re-ask on validation failure, surfacing the reason (CMD-004 re-ask loop replaces the closure).
44
+ const validationMessage = validateDuplicateProfileName(value, defaultName, options);
45
+ if (validationMessage !== undefined) {
46
+ errorMessage = validationMessage;
47
+ continue;
48
+ }
49
+ return completeProviderDuplicate(profileName, value, defaultName, options);
50
+ }
51
51
  }
52
52
 
53
53
  function validateDuplicateProfileName(
@@ -90,10 +90,11 @@ function completeProviderDuplicate(
90
90
  };
91
91
  }
92
92
 
93
- export function buildProviderDelete(
93
+ export async function buildProviderDelete(
94
+ ui: IUserInteraction,
94
95
  profileName: string,
95
96
  options: IProviderCommandModuleOptions,
96
- ): ICommandResult {
97
+ ): Promise<ICommandResult> {
97
98
  const settings = options.settings.readMergedSettings();
98
99
  const providers = settings.providers ?? {};
99
100
  if (!providers[profileName]) {
@@ -108,40 +109,20 @@ export function buildProviderDelete(
108
109
  success: false,
109
110
  };
110
111
  }
111
- return {
112
- message: `Provider delete requested: ${profileName}`,
113
- success: true,
114
- interaction: createProviderDeleteConfirmationInteraction(profileName, options),
115
- };
116
- }
117
-
118
- function createProviderDeleteConfirmationInteraction(
119
- profileName: string,
120
- options: IProviderCommandModuleOptions,
121
- ): ICommandInteraction {
122
- return {
123
- prompt: {
124
- kind: 'choice',
125
- title: `Delete provider profile ${profileName}?`,
126
- options: [
127
- { value: YES, label: 'Yes' },
128
- { value: 'no', label: 'No' },
129
- ],
130
- },
131
- submit: (value) => {
132
- if (value !== YES) {
133
- return { message: 'Provider delete cancelled.', success: true };
134
- }
135
- return confirmProviderDelete(profileName, options);
136
- },
137
- cancel: () => ({ message: 'Provider delete cancelled.', success: true }),
138
- };
112
+ const response = await ui.ask(
113
+ confirmAction('provider-delete', `Delete provider profile ${profileName}?`),
114
+ );
115
+ if (!isConfirmed(response)) {
116
+ return { message: 'Provider delete cancelled.', success: true };
117
+ }
118
+ return confirmProviderDelete(ui, profileName, options);
139
119
  }
140
120
 
141
- function confirmProviderDelete(
121
+ async function confirmProviderDelete(
122
+ ui: IUserInteraction,
142
123
  profileName: string,
143
124
  options: IProviderCommandModuleOptions,
144
- ): ICommandResult {
125
+ ): Promise<ICommandResult> {
145
126
  const settings = options.settings.readMergedSettings();
146
127
  if (settings.currentProvider !== profileName) {
147
128
  options.settings.writeTargetSettings(
@@ -155,21 +136,20 @@ function confirmProviderDelete(
155
136
  value: name,
156
137
  label: formatProviderChoiceLabel(name, profile, settings.currentProvider),
157
138
  }));
158
- return {
159
- message: `Select a replacement provider before deleting ${profileName}.`,
160
- success: true,
161
- interaction: {
162
- prompt: {
163
- kind: 'choice',
164
- title: `Replacement provider for ${profileName}`,
165
- options: replacementOptions,
139
+ const response = await ui.ask(
140
+ selectAction(
141
+ 'provider-delete-replacement',
142
+ `Replacement provider for ${profileName}`,
143
+ replacementOptions,
144
+ {
166
145
  maxVisible: 8,
167
146
  },
168
- submit: (replacementName) =>
169
- completeActiveProviderDelete(profileName, replacementName, options),
170
- cancel: () => ({ message: 'Provider delete cancelled.', success: true }),
171
- },
172
- };
147
+ ),
148
+ );
149
+ if (response.type !== 'answer' || response.values[0] === undefined) {
150
+ return { message: 'Provider delete cancelled.', success: true };
151
+ }
152
+ return completeActiveProviderDelete(profileName, response.values[0], options);
173
153
  }
174
154
 
175
155
  function completeActiveProviderDelete(
@@ -6,19 +6,16 @@ import {
6
6
  upsertProviderProfile,
7
7
  } from '@robota-sdk/agent-framework';
8
8
 
9
- import {
10
- createProviderSetupInteraction,
11
- toProviderSetupStepPrompt,
12
- } from './provider-command-setup.js';
13
- import { createProviderSetupFlow, submitProviderSetupValue } from './provider-setup-flow.js';
9
+ import { runProviderSetupAsk } from './provider-command-setup.js';
10
+ import { createProviderSetupFlow } from './provider-setup-flow.js';
14
11
 
15
- import type { IProviderSetupFlowState } from './provider-setup-flow.js';
12
+ import type { IUserInteraction } from '@robota-sdk/agent-core';
16
13
  import type {
17
14
  IProviderCommandModuleOptions,
18
15
  IProviderProfileSettings,
19
16
  IProviderSetupInput,
20
17
  } from '@robota-sdk/agent-framework';
21
- import type { ICommandInteraction, ICommandResult } from '@robota-sdk/agent-interface-transport';
18
+ import type { ICommandResult } from '@robota-sdk/agent-interface-transport';
22
19
 
23
20
  export function formatProviderChoiceLabel(
24
21
  name: string,
@@ -69,10 +66,11 @@ export function buildProviderSwitch(
69
66
  };
70
67
  }
71
68
 
72
- export function buildProviderEdit(
69
+ export async function buildProviderEdit(
70
+ ui: IUserInteraction,
73
71
  profileName: string,
74
72
  options: IProviderCommandModuleOptions,
75
- ): ICommandResult {
73
+ ): Promise<ICommandResult> {
76
74
  const settings = options.settings.readMergedSettings();
77
75
  const profile = settings.providers?.[profileName];
78
76
  if (!profile) {
@@ -81,20 +79,22 @@ export function buildProviderEdit(
81
79
  if (!profile.type) {
82
80
  return { message: `Provider profile "${profileName}" is missing type.`, success: false };
83
81
  }
82
+ let flow;
84
83
  try {
85
- const flow = createProviderSetupFlow(profile.type, options.providerDefinitions, {
84
+ flow = createProviderSetupFlow(profile.type, options.providerDefinitions, {
86
85
  profileName,
87
86
  setCurrent: false,
88
87
  initialValues: getProviderProfileSetupValues(profile),
89
88
  });
90
- return {
91
- message: `Provider edit requested: ${profileName}`,
92
- success: true,
93
- interaction: createProviderEditInteraction(flow, profileName, options),
94
- };
95
89
  } catch (error) {
96
90
  return { message: error instanceof Error ? error.message : String(error), success: false };
97
91
  }
92
+ return runProviderSetupAsk(
93
+ ui,
94
+ flow,
95
+ (input) => completeProviderEdit(input, profileName, options),
96
+ 'Provider edit cancelled.',
97
+ );
98
98
  }
99
99
 
100
100
  function getProviderProfileSetupValues(profile: IProviderProfileSettings): {
@@ -109,42 +109,6 @@ function getProviderProfileSetupValues(profile: IProviderProfileSettings): {
109
109
  };
110
110
  }
111
111
 
112
- function createProviderEditInteraction(
113
- flow: IProviderSetupFlowState,
114
- profileName: string,
115
- options: IProviderCommandModuleOptions,
116
- ): ICommandInteraction {
117
- return {
118
- prompt: toProviderSetupStepPrompt(flow),
119
- submit: (value) => submitProviderEditInteractionValue(flow, profileName, value, options),
120
- cancel: () => ({ message: 'Provider edit cancelled.', success: true }),
121
- };
122
- }
123
-
124
- function submitProviderEditInteractionValue(
125
- flow: IProviderSetupFlowState,
126
- profileName: string,
127
- value: string,
128
- options: IProviderCommandModuleOptions,
129
- ): ICommandResult {
130
- const result = submitProviderSetupValue(flow, value);
131
- if (result.status === 'error') {
132
- return {
133
- message: result.message,
134
- success: false,
135
- interaction: createProviderEditInteraction(flow, profileName, options),
136
- };
137
- }
138
- if (result.status === 'complete') {
139
- return completeProviderEdit(result.input, profileName, options);
140
- }
141
- return {
142
- message: '',
143
- success: true,
144
- interaction: createProviderEditInteraction(result.state, profileName, options),
145
- };
146
- }
147
-
148
112
  function completeProviderEdit(
149
113
  input: IProviderSetupInput,
150
114
  profileName: string,
@@ -1,3 +1,4 @@
1
+ import { selectAction } from '@robota-sdk/agent-core';
1
2
  import { testProviderProfileCommand } from '@robota-sdk/agent-framework';
2
3
 
3
4
  import {
@@ -10,11 +11,12 @@ import {
10
11
  formatProviderChoiceLabel,
11
12
  } from './provider-command-profile-operations.js';
12
13
 
14
+ import type { IUserInteraction } from '@robota-sdk/agent-core';
13
15
  import type {
14
16
  IProviderCommandModuleOptions,
15
17
  IProviderProfileSettings,
16
18
  } from '@robota-sdk/agent-framework';
17
- import type { ICommandInteraction, ICommandResult } from '@robota-sdk/agent-interface-transport';
19
+ import type { ICommandResult } from '@robota-sdk/agent-interface-transport';
18
20
 
19
21
  const ACTION_SWITCH = 'switch';
20
22
  const ACTION_EDIT = 'edit';
@@ -23,64 +25,57 @@ const ACTION_DUPLICATE = 'duplicate';
23
25
  const ACTION_DELETE = 'delete';
24
26
  const ACTION_CANCEL = 'cancel';
25
27
 
26
- export function createProviderProfileSelectionInteraction(
28
+ /**
29
+ * Ask the user to pick a provider profile, then drive its action menu (CMD-004 inline ask). Replaces
30
+ * the former returned choice→submit continuation chain — the picker's own option list is the rendered
31
+ * profile list, so the caller no longer prepends a separate list message.
32
+ */
33
+ export async function askProviderProfileSelection(
34
+ ui: IUserInteraction,
27
35
  currentProvider: string | undefined,
28
36
  providers: Record<string, IProviderProfileSettings> | undefined,
29
37
  options: IProviderCommandModuleOptions,
30
- ): ICommandInteraction {
31
- return {
32
- prompt: {
33
- kind: 'choice',
34
- title: 'Select provider profile',
35
- options: Object.entries(providers ?? {}).map(([name, profile]) => ({
36
- value: name,
37
- label: formatProviderChoiceLabel(name, profile, currentProvider),
38
- })),
39
- maxVisible: 8,
40
- },
41
- submit: (value) => buildProviderProfileActionMenu(value, options),
42
- cancel: () => ({ message: 'Provider profile selection cancelled.', success: true }),
43
- };
38
+ ): Promise<ICommandResult> {
39
+ const profileOptions = Object.entries(providers ?? {}).map(([name, profile]) => ({
40
+ value: name,
41
+ label: formatProviderChoiceLabel(name, profile, currentProvider),
42
+ }));
43
+ const response = await ui.ask(
44
+ selectAction('provider-profile', 'Select provider profile', profileOptions, { maxVisible: 8 }),
45
+ );
46
+ if (response.type !== 'answer' || response.values[0] === undefined) {
47
+ return { message: 'Provider profile selection cancelled.', success: true };
48
+ }
49
+ return askProviderProfileAction(ui, response.values[0], options);
44
50
  }
45
51
 
46
- function buildProviderProfileActionMenu(
52
+ async function askProviderProfileAction(
53
+ ui: IUserInteraction,
47
54
  profileName: string,
48
55
  options: IProviderCommandModuleOptions,
49
- ): ICommandResult {
56
+ ): Promise<ICommandResult> {
50
57
  const settings = options.settings.readMergedSettings();
51
58
  if (!settings.providers?.[profileName]) {
52
59
  return { message: `Provider profile "${profileName}" was not found.`, success: false };
53
60
  }
54
- return {
55
- message: `Provider profile selected: ${profileName}`,
56
- success: true,
57
- interaction: createProviderProfileActionInteraction(profileName, options),
58
- };
59
- }
60
-
61
- function createProviderProfileActionInteraction(
62
- profileName: string,
63
- options: IProviderCommandModuleOptions,
64
- ): ICommandInteraction {
65
- return {
66
- prompt: {
67
- kind: 'choice',
68
- title: `Provider profile: ${profileName}`,
69
- options: [
70
- { value: ACTION_SWITCH, label: 'Switch' },
71
- { value: ACTION_EDIT, label: 'Edit' },
72
- { value: ACTION_TEST, label: 'Test' },
73
- { value: ACTION_DUPLICATE, label: 'Duplicate' },
74
- { value: ACTION_DELETE, label: 'Delete' },
75
- { value: ACTION_CANCEL, label: 'Cancel' },
76
- ],
77
- },
78
- submit: (value) => executeProviderProfileAction(profileName, value, options),
79
- cancel: () => ({ message: 'Provider profile action cancelled.', success: true }),
80
- };
61
+ const response = await ui.ask(
62
+ selectAction('provider-profile-action', `Provider profile: ${profileName}`, [
63
+ { value: ACTION_SWITCH, label: 'Switch' },
64
+ { value: ACTION_EDIT, label: 'Edit' },
65
+ { value: ACTION_TEST, label: 'Test' },
66
+ { value: ACTION_DUPLICATE, label: 'Duplicate' },
67
+ { value: ACTION_DELETE, label: 'Delete' },
68
+ { value: ACTION_CANCEL, label: 'Cancel' },
69
+ ]),
70
+ );
71
+ if (response.type !== 'answer' || response.values[0] === undefined) {
72
+ return { message: 'Provider profile action cancelled.', success: true };
73
+ }
74
+ return executeProviderProfileAction(ui, profileName, response.values[0], options);
81
75
  }
82
76
 
83
77
  async function executeProviderProfileAction(
78
+ ui: IUserInteraction,
84
79
  profileName: string,
85
80
  action: string,
86
81
  options: IProviderCommandModuleOptions,
@@ -90,18 +85,18 @@ async function executeProviderProfileAction(
90
85
  case ACTION_SWITCH:
91
86
  return buildProviderSwitch(settings.providers, profileName, options);
92
87
  case ACTION_EDIT:
93
- return buildProviderEdit(profileName, options);
88
+ return buildProviderEdit(ui, profileName, options);
94
89
  case ACTION_TEST:
95
- return await testProviderProfileCommand(
90
+ return testProviderProfileCommand(
96
91
  settings.currentProvider,
97
92
  settings.providers,
98
93
  profileName,
99
94
  options,
100
95
  );
101
96
  case ACTION_DUPLICATE:
102
- return buildProviderDuplicate(profileName, options);
97
+ return buildProviderDuplicate(ui, profileName, options);
103
98
  case ACTION_DELETE:
104
- return buildProviderDelete(profileName, options);
99
+ return buildProviderDelete(ui, profileName, options);
105
100
  case ACTION_CANCEL:
106
101
  return { message: 'Provider profile action cancelled.', success: true };
107
102
  default:
@@ -1,3 +1,4 @@
1
+ import { textAction } from '@robota-sdk/agent-core';
1
2
  import { buildProviderSetupPatch, mergeProviderPatch } from '@robota-sdk/agent-framework';
2
3
 
3
4
  import {
@@ -5,19 +6,15 @@ import {
5
6
  formatProviderSetupHelpLinks,
6
7
  getProviderSetupStep,
7
8
  submitProviderSetupValue,
8
- validateProviderSetupValue,
9
9
  } from './provider-setup-flow.js';
10
10
 
11
11
  import type { IProviderSetupFlowState } from './provider-setup-flow.js';
12
+ import type { IActionRequest, IUserInteraction } from '@robota-sdk/agent-core';
12
13
  import type {
13
14
  IProviderCommandModuleOptions,
14
15
  IProviderSetupInput,
15
16
  } from '@robota-sdk/agent-framework';
16
- import type {
17
- ICommandInteraction,
18
- ICommandResult,
19
- TCommandInteractionPrompt,
20
- } from '@robota-sdk/agent-interface-transport';
17
+ import type { ICommandResult } from '@robota-sdk/agent-interface-transport';
21
18
 
22
19
  const PROVIDER_RESTART_EFFECT = {
23
20
  type: 'session-restart-requested',
@@ -33,62 +30,70 @@ export function createSetupFlow(
33
30
  });
34
31
  }
35
32
 
36
- export function createProviderSetupInteraction(
33
+ /** Build the per-step `IActionRequest` for the current setup step (CMD-004 inline ask). */
34
+ function toProviderSetupStepRequest(
37
35
  flow: IProviderSetupFlowState,
38
- options: IProviderCommandModuleOptions,
39
- ): ICommandInteraction {
40
- return {
41
- prompt: toProviderSetupStepPrompt(flow),
42
- submit: (value) => submitProviderSetupInteractionValue(flow, value, options),
43
- cancel: () => ({ message: 'Provider setup cancelled.', success: true }),
44
- };
45
- }
46
-
47
- export function toProviderSetupStepPrompt(
48
- flow: IProviderSetupFlowState,
49
- ): TCommandInteractionPrompt {
36
+ errorMessage?: string,
37
+ ): IActionRequest {
50
38
  const step = getProviderSetupStep(flow);
51
39
  const placeholder =
52
40
  step.masked === true && step.defaultValue !== undefined ? '(unchanged)' : step.defaultValue;
53
- return {
54
- kind: 'text',
55
- title: step.title,
56
- ...toProviderSetupPromptDescription(flow),
57
- ...(placeholder !== undefined ? { placeholder } : {}),
58
- ...(step.defaultValue !== undefined ? { allowEmpty: true } : {}),
59
- ...(step.masked !== undefined ? { masked: step.masked } : {}),
60
- validate: (value) => validateProviderSetupValue(step, value),
61
- };
41
+ const helpLinks = formatProviderSetupHelpLinks(flow.setupHelpLinks);
42
+ const description =
43
+ [errorMessage, helpLinks.length > 0 ? helpLinks : undefined]
44
+ .filter((part): part is string => part !== undefined && part.length > 0)
45
+ .join('\n') || undefined;
46
+ return textAction(`provider-setup-${step.key}`, step.title, {
47
+ description,
48
+ placeholder,
49
+ allowEmpty: step.defaultValue !== undefined,
50
+ masked: step.masked,
51
+ });
62
52
  }
63
53
 
64
- function toProviderSetupPromptDescription(
54
+ /**
55
+ * Drive the setup-step engine through inline `ui.ask` calls (CMD-004), re-asking the same step when a
56
+ * step fails validation (the error is surfaced in the request description). The `complete` sink differs
57
+ * between the add and edit paths.
58
+ */
59
+ export async function runProviderSetupAsk(
60
+ ui: IUserInteraction,
65
61
  flow: IProviderSetupFlowState,
66
- ): { description: string } | Record<string, never> {
67
- const description = formatProviderSetupHelpLinks(flow.setupHelpLinks);
68
- return description.length > 0 ? { description } : {};
62
+ complete: (input: IProviderSetupInput) => ICommandResult,
63
+ cancelMessage: string,
64
+ ): Promise<ICommandResult> {
65
+ let state = flow;
66
+ let errorMessage: string | undefined;
67
+ for (;;) {
68
+ const response = await ui.ask(toProviderSetupStepRequest(state, errorMessage));
69
+ if (response.type === 'cancelled') {
70
+ return { message: cancelMessage, success: true };
71
+ }
72
+ const result = submitProviderSetupValue(state, response.text ?? '');
73
+ if (result.status === 'error') {
74
+ errorMessage = result.message;
75
+ continue;
76
+ }
77
+ if (result.status === 'complete') {
78
+ return complete(result.input);
79
+ }
80
+ state = result.state;
81
+ errorMessage = undefined;
82
+ }
69
83
  }
70
84
 
71
- function submitProviderSetupInteractionValue(
85
+ /** Run the `/provider add` setup wizard inline. */
86
+ export function runProviderAddSetup(
87
+ ui: IUserInteraction,
72
88
  flow: IProviderSetupFlowState,
73
- value: string,
74
89
  options: IProviderCommandModuleOptions,
75
- ): ICommandResult {
76
- const result = submitProviderSetupValue(flow, value);
77
- if (result.status === 'error') {
78
- return {
79
- message: result.message,
80
- success: false,
81
- interaction: createProviderSetupInteraction(flow, options),
82
- };
83
- }
84
- if (result.status === 'complete') {
85
- return completeProviderSetup(result.input, options);
86
- }
87
- return {
88
- message: '',
89
- success: true,
90
- interaction: createProviderSetupInteraction(result.state, options),
91
- };
90
+ ): Promise<ICommandResult> {
91
+ return runProviderSetupAsk(
92
+ ui,
93
+ flow,
94
+ (input) => completeProviderSetup(input, options),
95
+ 'Provider setup cancelled.',
96
+ );
92
97
  }
93
98
 
94
99
  function completeProviderSetup(
@@ -201,6 +201,44 @@ describe('createSessionCommandModule', () => {
201
201
  });
202
202
  });
203
203
 
204
+ it('CMD-004: confirms before clearing and proceeds on yes', async () => {
205
+ const clearConversationHistory = vi.fn();
206
+ const context = {
207
+ ...createCommandContext(),
208
+ clearConversationHistory,
209
+ getUserInteraction: () => ({
210
+ ask: async () => ({ type: 'answer' as const, values: ['yes'] }),
211
+ }),
212
+ };
213
+ const executor = new SystemCommandExecutor([
214
+ ...(createSessionCommandModule().systemCommands ?? []),
215
+ ]);
216
+
217
+ const result = await executor.execute('clear', context, '');
218
+
219
+ expect(clearConversationHistory).toHaveBeenCalledTimes(1);
220
+ expect(result?.effects).toEqual([{ type: 'conversation-history-cleared' }]);
221
+ });
222
+
223
+ it('CMD-004: cancels the clear when the user declines', async () => {
224
+ const clearConversationHistory = vi.fn();
225
+ const context = {
226
+ ...createCommandContext(),
227
+ clearConversationHistory,
228
+ getUserInteraction: () => ({
229
+ ask: async () => ({ type: 'answer' as const, values: ['no'] }),
230
+ }),
231
+ };
232
+ const executor = new SystemCommandExecutor([
233
+ ...(createSessionCommandModule().systemCommands ?? []),
234
+ ]);
235
+
236
+ const result = await executor.execute('clear', context, '');
237
+
238
+ expect(clearConversationHistory).not.toHaveBeenCalled();
239
+ expect(result?.message).toBe('Clear cancelled.');
240
+ });
241
+
204
242
  it('falls back to runtime clearHistory when the host has not implemented the richer API', async () => {
205
243
  const runtime = createRuntime();
206
244
  const context = {
@@ -15,11 +15,7 @@ import {
15
15
  } from './session-command.js';
16
16
 
17
17
  import type { ICommandModule, ISystemCommand } from '@robota-sdk/agent-framework';
18
- import type {
19
- ICommand,
20
- ICommandInteractionHint,
21
- ICommandSource,
22
- } from '@robota-sdk/agent-interface-transport';
18
+ import type { ICommand, ICommandSource } from '@robota-sdk/agent-interface-transport';
23
19
 
24
20
  export function createClearCommandEntry(): ICommand {
25
21
  return {
@@ -155,10 +151,6 @@ export class SessionCommandSource implements ICommandSource {
155
151
  }
156
152
  }
157
153
 
158
- const SESSION_INTERACTION_HINTS: Record<string, ICommandInteractionHint> = {
159
- clear: { type: 'confirm', message: 'Clear conversation history?' },
160
- };
161
-
162
154
  export function createSessionCommandModule(): ICommandModule {
163
155
  return {
164
156
  name: 'agent-command-session',
@@ -170,6 +162,5 @@ export function createSessionCommandModule(): ICommandModule {
170
162
  createCostSystemCommand(),
171
163
  createValidateSessionSystemCommand(),
172
164
  ],
173
- interactionHints: SESSION_INTERACTION_HINTS,
174
165
  };
175
166
  }
@@ -1,6 +1,7 @@
1
1
  import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
2
2
  import { join } from 'node:path';
3
3
 
4
+ import { confirmAction, isConfirmed } from '@robota-sdk/agent-core';
4
5
  import {
5
6
  RENAME_COMMAND_USAGE,
6
7
  clearConversationHistory,
@@ -19,7 +20,19 @@ import type { ICommandResult } from '@robota-sdk/agent-interface-transport';
19
20
 
20
21
  export const CLEAR_COMMAND_MESSAGE = 'Conversation cleared.';
21
22
 
22
- export function executeClearCommand(context: ICommandHostContext, _args: string): ICommandResult {
23
+ export async function executeClearCommand(
24
+ context: ICommandHostContext,
25
+ _args: string,
26
+ ): Promise<ICommandResult> {
27
+ // Confirm only when an interactive renderer is attached; with no human the explicit /clear proceeds.
28
+ const ui = context.getUserInteraction?.();
29
+ if (ui) {
30
+ const response = await ui.ask(confirmAction('clear', 'Clear conversation history?'));
31
+ if (!isConfirmed(response)) {
32
+ return { success: true, message: 'Clear cancelled.' };
33
+ }
34
+ }
35
+
23
36
  clearConversationHistory(context);
24
37
  return {
25
38
  success: true,