@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,230 @@
1
+ import { describe, expect, it, vi } from 'vitest';
2
+ import type {
3
+ ICommandHostContext,
4
+ IEditCheckpointRestoreResult,
5
+ } from '@robota-sdk/agent-framework';
6
+ import { SystemCommandExecutor } from '@robota-sdk/agent-framework';
7
+ import { createModeCommandModule } from '../mode-command-module.js';
8
+ import { createTestCommandHost } from '@robota-sdk/agent-framework/testing';
9
+
10
+ type TPermissionModeName = 'plan' | 'default' | 'acceptEdits' | 'bypassPermissions';
11
+ type TSetPermissionModeSpy = ReturnType<typeof vi.fn<(nextMode: TPermissionModeName) => void>>;
12
+
13
+ function createCheckpointResult(): IEditCheckpointRestoreResult {
14
+ return {
15
+ target: {
16
+ id: 'checkpoint_1',
17
+ sessionId: 'session_1',
18
+ sequence: 1,
19
+ prompt: 'edit files',
20
+ createdAt: '2026-05-03T00:00:00.000Z',
21
+ fileCount: 1,
22
+ },
23
+ restoredCheckpointCount: 0,
24
+ restoredFileCount: 0,
25
+ removedCheckpointCount: 0,
26
+ };
27
+ }
28
+
29
+ function createCommandHostContext(): ReturnType<typeof createTestCommandHost> & {
30
+ setPermissionMode: TSetPermissionModeSpy;
31
+ } {
32
+ let mode: TPermissionModeName = 'default';
33
+ const setPermissionMode = vi.fn((nextMode: TPermissionModeName) => {
34
+ mode = nextMode;
35
+ });
36
+
37
+ return {
38
+ ...createTestCommandHost({
39
+ overrides: {
40
+ getSession: () => {
41
+ throw new Error('mode command should use the permission mode adapter');
42
+ },
43
+ getCommandHostAdapters: () => ({
44
+ permissionMode: {
45
+ getPermissionMode: () => mode,
46
+ setPermissionMode,
47
+ listSessionAllowedTools: () => [],
48
+ getPermissionRules: () => ({ allow: [], deny: [], ask: [] }),
49
+ listRecentDenials: () => [],
50
+ retryDenial: () => undefined,
51
+ },
52
+ }),
53
+ getContextState: () => ({
54
+ usedTokens: 0,
55
+ maxTokens: 1,
56
+ usedPercentage: 0,
57
+ remainingPercentage: 100,
58
+ }),
59
+ getAutoCompactThreshold: () => 0.835,
60
+ compactContext: async () => undefined,
61
+ getCwd: () => '/workspace',
62
+ listEditCheckpoints: () => [],
63
+ restoreEditCheckpoint: async () => createCheckpointResult(),
64
+ rollbackEditCheckpoint: async () => createCheckpointResult(),
65
+ getUsedMemoryReferences: () => [],
66
+ recordMemoryEvent: () => undefined,
67
+ listBackgroundTasks: () => [],
68
+ readBackgroundTaskLog: async () => ({ taskId: 'task_1', lines: [] }),
69
+ cancelBackgroundTask: async () => undefined,
70
+ closeBackgroundTask: async () => undefined,
71
+ },
72
+ }),
73
+ setPermissionMode,
74
+ };
75
+ }
76
+
77
+ describe('createModeCommandModule', () => {
78
+ it('provides mode metadata and executable command from one module owner', () => {
79
+ const module = createModeCommandModule();
80
+ const command = module.systemCommands?.[0];
81
+ const entry = module.commandSources?.[0]?.getCommands()[0];
82
+
83
+ expect(module.name).toBe('agent-command-mode');
84
+ expect(entry).toEqual(
85
+ expect.objectContaining({
86
+ name: 'mode',
87
+ description: 'Show/change permission mode',
88
+ argumentHint: 'plan | default | acceptEdits | bypassPermissions | auto',
89
+ source: 'mode',
90
+ modelInvocable: false,
91
+ }),
92
+ );
93
+ expect(entry?.subcommands?.map((subcommand) => subcommand.name)).toEqual([
94
+ 'plan',
95
+ 'default',
96
+ 'acceptEdits',
97
+ 'bypassPermissions',
98
+ 'auto',
99
+ ]);
100
+ // #3282 §2: a plain label beside every description, so a client never shows the raw id.
101
+ expect(entry?.subcommands?.map((subcommand) => subcommand.displayName)).toEqual([
102
+ 'Plan only',
103
+ 'Ask first',
104
+ 'Accept edits',
105
+ 'Skip all checks',
106
+ 'Auto',
107
+ ]);
108
+ expect(entry?.subcommands?.every((subcommand) => subcommand.description.length > 0)).toBe(true);
109
+ expect(command).toEqual(
110
+ expect.objectContaining({
111
+ name: 'mode',
112
+ description: 'Show/change permission mode',
113
+ argumentHint: 'plan | default | acceptEdits | bypassPermissions | auto',
114
+ lifecycle: 'inline',
115
+ modelInvocable: false,
116
+ }),
117
+ );
118
+ expect(command?.subcommands).toEqual(entry?.subcommands);
119
+ });
120
+
121
+ it('reports the current permission mode through the SDK command adapter', async () => {
122
+ const executor = new SystemCommandExecutor([
123
+ ...(createModeCommandModule().systemCommands ?? []),
124
+ ]);
125
+
126
+ const result = await executor.execute('mode', createCommandHostContext(), '');
127
+
128
+ expect(result?.success).toBe(true);
129
+ expect(result?.message).toBe('Current mode: default');
130
+ expect(result?.data?.mode).toBe('default');
131
+ });
132
+
133
+ it('updates valid permission modes through the SDK command adapter', async () => {
134
+ const executor = new SystemCommandExecutor([
135
+ ...(createModeCommandModule().systemCommands ?? []),
136
+ ]);
137
+ const context = createCommandHostContext();
138
+
139
+ const result = await executor.execute('mode', context, 'plan');
140
+
141
+ expect(result?.success).toBe(true);
142
+ expect(result?.message).toBe('Permission mode set to: plan');
143
+ expect(result?.data?.mode).toBe('plan');
144
+ expect(context.setPermissionMode).toHaveBeenCalledWith('plan');
145
+ });
146
+
147
+ it('rejects invalid permission modes without writing state', async () => {
148
+ const executor = new SystemCommandExecutor([
149
+ ...(createModeCommandModule().systemCommands ?? []),
150
+ ]);
151
+ const context = createCommandHostContext();
152
+
153
+ const result = await executor.execute('mode', context, 'invalid');
154
+
155
+ expect(result?.success).toBe(false);
156
+ expect(result?.message).toBe(
157
+ 'Invalid mode. Valid: plan | default | acceptEdits | bypassPermissions | auto',
158
+ );
159
+ expect(context.setPermissionMode).not.toHaveBeenCalled();
160
+ });
161
+
162
+ it('#3282 §2: the picker shows plain labels with their descriptions, never a raw id', async () => {
163
+ const executor = new SystemCommandExecutor([
164
+ ...(createModeCommandModule().systemCommands ?? []),
165
+ ]);
166
+ const context = createCommandHostContext();
167
+ const ask = vi.fn().mockResolvedValue({ type: 'answer', values: ['plan'] });
168
+ const contextWithAsk = createTestCommandHost({
169
+ overrides: { ...context, getUserInteraction: () => ({ ask }) },
170
+ });
171
+
172
+ await executor.execute('mode', contextWithAsk, '');
173
+
174
+ expect(ask).toHaveBeenCalledTimes(1);
175
+ const request = ask.mock.calls[0]![0];
176
+ expect(request.options).toEqual([
177
+ { value: 'plan', label: 'Plan only', description: 'Plan only, no execution' },
178
+ { value: 'default', label: 'Ask first', description: 'Ask before risky actions' },
179
+ { value: 'acceptEdits', label: 'Accept edits', description: 'Auto-approve file edits' },
180
+ {
181
+ value: 'bypassPermissions',
182
+ label: 'Skip all checks',
183
+ description: 'Skip all permission checks',
184
+ },
185
+ {
186
+ value: 'auto',
187
+ label: 'Auto',
188
+ description: 'A model classifier approves or blocks risky actions',
189
+ },
190
+ ]);
191
+ });
192
+
193
+ it('asks the user to pick a mode when no arg is given and a renderer is attached (CMD-004)', async () => {
194
+ const executor = new SystemCommandExecutor([
195
+ ...(createModeCommandModule().systemCommands ?? []),
196
+ ]);
197
+ const context = createCommandHostContext();
198
+ const contextWithAsk = createTestCommandHost({
199
+ overrides: {
200
+ ...context,
201
+ getUserInteraction: () => ({ ask: async () => ({ type: 'answer', values: ['plan'] }) }),
202
+ },
203
+ });
204
+
205
+ const result = await executor.execute('mode', contextWithAsk, '');
206
+
207
+ expect(result?.success).toBe(true);
208
+ expect(result?.message).toBe('Permission mode set to: plan');
209
+ expect(context.setPermissionMode).toHaveBeenCalledWith('plan');
210
+ });
211
+
212
+ it('reports the current mode when the user cancels the pick (CMD-004)', async () => {
213
+ const executor = new SystemCommandExecutor([
214
+ ...(createModeCommandModule().systemCommands ?? []),
215
+ ]);
216
+ const context = createCommandHostContext();
217
+ const contextWithAsk = createTestCommandHost({
218
+ overrides: {
219
+ ...context,
220
+ getUserInteraction: () => ({ ask: async () => ({ type: 'cancelled' }) }),
221
+ },
222
+ });
223
+
224
+ const result = await executor.execute('mode', contextWithAsk, '');
225
+
226
+ expect(result?.success).toBe(true);
227
+ expect(result?.message).toBe('Current mode: default');
228
+ expect(context.setPermissionMode).not.toHaveBeenCalled();
229
+ });
230
+ });
@@ -0,0 +1,6 @@
1
+ export {
2
+ createModeCommandEntry,
3
+ createModeCommandModule,
4
+ ModeCommandSource,
5
+ } from './mode-command-module.js';
6
+ export { executeModeCommand } from './mode-command.js';
@@ -0,0 +1,55 @@
1
+ import {
2
+ buildPermissionModeSubcommands,
3
+ PERMISSION_MODE_ARGUMENT_HINT,
4
+ PERMISSION_MODE_COMMAND_DESCRIPTION,
5
+ } from '@robota-sdk/agent-framework';
6
+
7
+ import { executeModeCommand } from './mode-command.js';
8
+
9
+ import type { ICommandModule, ISystemCommand } from '@robota-sdk/agent-framework';
10
+ import type { ICommand, ICommandSource } from '@robota-sdk/agent-interface-command';
11
+
12
+ export function createModeCommandEntry(): ICommand {
13
+ return {
14
+ name: 'mode',
15
+ displayName: 'Interaction Mode',
16
+ description: PERMISSION_MODE_COMMAND_DESCRIPTION,
17
+ source: 'mode',
18
+ argumentHint: PERMISSION_MODE_ARGUMENT_HINT,
19
+ subcommands: buildPermissionModeSubcommands('mode'),
20
+ // User-only: changes the permission mode; permission widening is the user's.
21
+ modelInvocable: false,
22
+ };
23
+ }
24
+
25
+ function createModeSystemCommand(): ISystemCommand {
26
+ const entry = createModeCommandEntry();
27
+ return {
28
+ name: entry.name,
29
+ displayName: entry.displayName,
30
+ description: entry.description,
31
+ requiresPermission: false,
32
+ userInvocable: true,
33
+ modelInvocable: false,
34
+ argumentHint: entry.argumentHint,
35
+ subcommands: entry.subcommands,
36
+ lifecycle: 'inline',
37
+ execute: executeModeCommand,
38
+ };
39
+ }
40
+
41
+ export class ModeCommandSource implements ICommandSource {
42
+ readonly name = 'mode';
43
+
44
+ getCommands(): ICommand[] {
45
+ return [createModeCommandEntry()];
46
+ }
47
+ }
48
+
49
+ export function createModeCommandModule(): ICommandModule {
50
+ return {
51
+ name: 'agent-command-mode',
52
+ commandSources: [new ModeCommandSource()],
53
+ systemCommands: [createModeSystemCommand()],
54
+ };
55
+ }
@@ -0,0 +1,67 @@
1
+ import { selectAction } from '@robota-sdk/agent-core';
2
+ import {
3
+ buildPermissionModeSubcommands,
4
+ formatInvalidPermissionModeMessage,
5
+ isPermissionMode,
6
+ parsePermissionModeArgument,
7
+ readCommandPermissionMode,
8
+ } from '@robota-sdk/agent-framework';
9
+
10
+ import { tryWritePermissionMode } from '../permissions/permissions-command.js';
11
+
12
+ import type {
13
+ ICommandHostAdapterAccess,
14
+ ICommandHostSessionAccess,
15
+ ICommandHostUserInteraction,
16
+ } from '@robota-sdk/agent-framework';
17
+ import type { ICommandResult } from '@robota-sdk/agent-interface-command';
18
+
19
+ /**
20
+ * Ask the user to pick a permission mode (CMD-004 inline ask). Returns the chosen mode name, or
21
+ * `undefined` when no interactive renderer is attached or the user cancelled — the caller then reports
22
+ * the current mode instead of changing it (never a silent guess).
23
+ */
24
+ async function resolveModeViaAsk(
25
+ context: ICommandHostUserInteraction,
26
+ ): Promise<string | undefined> {
27
+ const ui = context.getUserInteraction();
28
+ if (!ui) return undefined;
29
+ const options = buildPermissionModeSubcommands().map((sub) => ({
30
+ value: sub.name,
31
+ label: sub.displayName ?? sub.name,
32
+ description: sub.description,
33
+ }));
34
+ const response = await ui.ask(selectAction('mode', 'Select interaction mode', options));
35
+ return response.type === 'answer' ? response.values[0] : undefined;
36
+ }
37
+
38
+ export async function executeModeCommand(
39
+ context: ICommandHostAdapterAccess & ICommandHostSessionAccess & ICommandHostUserInteraction,
40
+ args: string,
41
+ ): Promise<ICommandResult> {
42
+ let arg: string | undefined = parsePermissionModeArgument(args);
43
+
44
+ if (arg === undefined) {
45
+ arg = await resolveModeViaAsk(context);
46
+ if (arg === undefined) {
47
+ // No interactive answer — report the current mode without changing it.
48
+ const mode = readCommandPermissionMode(context);
49
+ return { message: `Current mode: ${mode}`, success: true, data: { mode } };
50
+ }
51
+ }
52
+
53
+ if (!isPermissionMode(arg)) {
54
+ return {
55
+ message: formatInvalidPermissionModeMessage(),
56
+ success: false,
57
+ };
58
+ }
59
+
60
+ const refused = tryWritePermissionMode(context, arg);
61
+ if (refused !== undefined) return refused;
62
+ return {
63
+ message: `Permission mode set to: ${arg}`,
64
+ success: true,
65
+ data: { mode: arg },
66
+ };
67
+ }
@@ -0,0 +1,80 @@
1
+ import { describe, expect, it, vi } from 'vitest';
2
+
3
+ import { SystemCommandExecutor } from '@robota-sdk/agent-framework';
4
+ import { createTestCommandHost } from '@robota-sdk/agent-framework/testing';
5
+
6
+ import { createModelCommandEntry, createModelCommandModule } from '../model-command-module.js';
7
+
8
+ import type { IProviderCommandModuleOptions, IProviderProfileSettings } from '@robota-sdk/agent-framework';
9
+ import type { IProviderDefinition } from '@robota-sdk/agent-core';
10
+
11
+ function buildOptions(): IProviderCommandModuleOptions {
12
+ const providers: Record<string, IProviderProfileSettings> = {
13
+ anthropic: { type: 'anthropic', model: 'claude-sonnet-4-6' },
14
+ };
15
+ const definitions: readonly IProviderDefinition[] = [
16
+ {
17
+ type: 'anthropic',
18
+ displayName: 'Anthropic',
19
+ modelCatalog: { status: 'unavailable' },
20
+ createProvider: () => {
21
+ throw new Error('not used');
22
+ },
23
+ },
24
+ ];
25
+ const document = { currentProvider: 'anthropic', providers };
26
+ return {
27
+ providerDefinitions: definitions,
28
+ settings: {
29
+ readMergedSettings: () => document,
30
+ readTargetSettings: () => document,
31
+ writeTargetSettings: vi.fn(),
32
+ },
33
+ };
34
+ }
35
+
36
+ describe('createModelCommandModule (#3282 §2)', () => {
37
+ it('is user-only: never model-invocable', () => {
38
+ const entry = createModelCommandEntry();
39
+ expect(entry.modelInvocable).toBe(false);
40
+
41
+ const module = createModelCommandModule(buildOptions());
42
+ const command = module.systemCommands?.[0];
43
+ expect(command?.modelInvocable).toBe(false);
44
+ });
45
+
46
+ it('carries a model-facing description, name and argument hint', () => {
47
+ const entry = createModelCommandEntry();
48
+ expect(entry).toEqual(
49
+ expect.objectContaining({
50
+ name: 'model',
51
+ displayName: 'Model',
52
+ description: 'Show/change the model',
53
+ argumentHint: '[<id>]',
54
+ source: 'model',
55
+ }),
56
+ );
57
+ });
58
+
59
+ it('provides the metadata and the executable command from one module owner', () => {
60
+ const module = createModelCommandModule(buildOptions());
61
+ expect(module.name).toBe('agent-command-model');
62
+ const entry = module.commandSources?.[0]?.getCommands()[0];
63
+ const command = module.systemCommands?.[0];
64
+ expect(entry?.name).toBe('model');
65
+ expect(command?.name).toBe('model');
66
+ expect(command?.lifecycle).toBe('inline');
67
+ });
68
+
69
+ it('runs "/model <id>" through the SDK command executor', async () => {
70
+ const executor = new SystemCommandExecutor([
71
+ ...(createModelCommandModule(buildOptions()).systemCommands ?? []),
72
+ ]);
73
+ const host = createTestCommandHost({ session: { getModelId: () => 'claude-sonnet-4-6' } });
74
+
75
+ const result = await executor.execute('model', host, 'not-a-real-model');
76
+
77
+ expect(result?.success).toBe(false);
78
+ expect(result?.message).toBe('Unknown model "not-a-real-model". Run /model to see the choices.');
79
+ });
80
+ });