@robota-sdk/agent-command 3.0.0-beta.79 → 3.0.0-beta.82

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 (201) hide show
  1. package/README.md +11 -2
  2. package/dist/node/index.cjs +77 -38
  3. package/dist/node/index.d.cts +946 -0
  4. package/dist/node/index.d.cts.map +1 -0
  5. package/dist/node/index.d.ts +564 -51
  6. package/dist/node/index.d.ts.map +1 -1
  7. package/dist/node/index.js +77 -38
  8. package/dist/node/index.js.map +1 -1
  9. package/package.json +39 -16
  10. package/src/advisor/advisor-command-module.ts +47 -0
  11. package/src/advisor/advisor-command.test.ts +102 -0
  12. package/src/advisor/advisor-command.ts +55 -0
  13. package/src/advisor/advisor-cost-functional.test.ts +92 -0
  14. package/src/advisor/index.ts +6 -0
  15. package/src/agent/__tests__/agent-command.test.ts +39 -8
  16. package/src/agent/agent-command-module.ts +14 -7
  17. package/src/agent/agent-command-parser.ts +1 -1
  18. package/src/agent/agent-command.ts +3 -3
  19. package/src/background/__tests__/background-command-module.test.ts +59 -52
  20. package/src/background/background-command-module.ts +2 -1
  21. package/src/background/background-command.ts +3 -3
  22. package/src/command-module-utils.ts +33 -0
  23. package/src/compact/__tests__/compact-command-module.test.ts +31 -23
  24. package/src/compact/compact-command-module.ts +10 -1
  25. package/src/compact/compact-command.ts +6 -3
  26. package/src/context/__tests__/context-command-module.test.ts +214 -63
  27. package/src/context/auto-compact-format.ts +25 -0
  28. package/src/context/context-breakdown.ts +295 -0
  29. package/src/context/context-command-module.ts +45 -14
  30. package/src/context/context-command.ts +34 -257
  31. package/src/default/__tests__/__snapshots__/model-exposure.test.ts.snap +110 -0
  32. package/src/default/__tests__/default-command-modules.test.ts +59 -7
  33. package/src/default/__tests__/model-exposure.test.ts +241 -0
  34. package/src/default/__tests__/org-policy-forwarding.test.ts +98 -0
  35. package/src/default/default-command-modules.ts +130 -18
  36. package/src/default/index.ts +4 -1
  37. package/src/devices/__tests__/devices-command-module.test.ts +212 -0
  38. package/src/devices/devices-command-module.ts +224 -0
  39. package/src/devices/devices-command-port.ts +82 -0
  40. package/src/devices/index.ts +11 -0
  41. package/src/doctor/__tests__/doctor-command-module.test.ts +145 -0
  42. package/src/doctor/__tests__/doctor-display-vocabulary.test.ts +38 -0
  43. package/src/doctor/__tests__/doctor-fixture.ts +145 -0
  44. package/src/doctor/__tests__/doctor-redaction.test.ts +29 -0
  45. package/src/doctor/__tests__/doctor-runner.test.ts +464 -0
  46. package/src/doctor/doctor-command-module.ts +132 -0
  47. package/src/doctor/doctor-extensions-probe.ts +299 -0
  48. package/src/doctor/doctor-node-deps.ts +89 -0
  49. package/src/doctor/doctor-redaction.ts +99 -0
  50. package/src/doctor/doctor-render.ts +70 -0
  51. package/src/doctor/doctor-repair.ts +136 -0
  52. package/src/doctor/doctor-runner.ts +122 -0
  53. package/src/doctor/doctor-settings-probe.ts +239 -0
  54. package/src/doctor/doctor-storage-probe.ts +187 -0
  55. package/src/doctor/doctor-types.ts +109 -0
  56. package/src/doctor/index.ts +32 -0
  57. package/src/editor/__tests__/editor-command-functional.test.ts +49 -16
  58. package/src/editor/editor-command-module.ts +6 -5
  59. package/src/editor/editor-command.ts +10 -5
  60. package/src/effort/effort-command-module.ts +51 -0
  61. package/src/effort/effort-command.test.ts +140 -0
  62. package/src/effort/effort-command.ts +122 -0
  63. package/src/effort/index.ts +6 -0
  64. package/src/exit/__tests__/exit-command-module.test.ts +16 -9
  65. package/src/exit/exit-command-module.ts +2 -1
  66. package/src/exit/exit-command.ts +6 -6
  67. package/src/fork/__tests__/fork-command.test.ts +184 -0
  68. package/src/fork/fork-command-module.ts +61 -0
  69. package/src/fork/fork-command.ts +162 -0
  70. package/src/fork/index.ts +8 -0
  71. package/src/git/__tests__/fake-git-port.ts +28 -0
  72. package/src/git/__tests__/git-command-module.test.ts +103 -0
  73. package/src/git/__tests__/git-commit.test.ts +185 -0
  74. package/src/git/__tests__/git-diff.test.ts +125 -0
  75. package/src/git/__tests__/git-process.test.ts +221 -0
  76. package/src/git/__tests__/git-status.test.ts +117 -0
  77. package/src/git/git-command-module.ts +139 -0
  78. package/src/git/git-commit.ts +251 -0
  79. package/src/git/git-diff.ts +129 -0
  80. package/src/git/git-process.ts +169 -0
  81. package/src/git/git-status.ts +134 -0
  82. package/src/git/index.ts +17 -0
  83. package/src/goal/__tests__/goal-command.test.ts +9 -4
  84. package/src/goal/goal-command-module.ts +2 -1
  85. package/src/goal/goal-command.ts +6 -9
  86. package/src/handoff/__tests__/handoff-command-module.test.ts +24 -0
  87. package/src/handoff/__tests__/handoff-command.test.ts +218 -0
  88. package/src/handoff/handoff-command-module.ts +49 -0
  89. package/src/handoff/handoff-command.ts +187 -0
  90. package/src/handoff/index.ts +6 -0
  91. package/src/help/__tests__/help-command-module.test.ts +63 -40
  92. package/src/help/__tests__/help-command.test.ts +46 -37
  93. package/src/help/help-command-module.ts +2 -1
  94. package/src/help/help-command.ts +3 -3
  95. package/src/index.ts +124 -2
  96. package/src/keybindings/__tests__/keybindings-command-module.test.ts +57 -0
  97. package/src/keybindings/index.ts +6 -0
  98. package/src/keybindings/keybindings-command-module.ts +74 -0
  99. package/src/language/__tests__/language-command-module.test.ts +33 -28
  100. package/src/language/language-command-module.ts +2 -1
  101. package/src/language/language-command.ts +8 -6
  102. package/src/mcp-activation/__tests__/mcp-activation-command.test.ts +630 -0
  103. package/src/mcp-activation/__tests__/mcp-model-view.test.ts +211 -0
  104. package/src/mcp-activation/index.ts +12 -0
  105. package/src/mcp-activation/mcp-activation-command-module.ts +92 -0
  106. package/src/mcp-activation/mcp-activation-command.ts +448 -0
  107. package/src/mcp-activation/mcp-model-notice.ts +85 -0
  108. package/src/mcp-activation/mcp-model-status.ts +99 -0
  109. package/src/memory/__tests__/memory-command-module.test.ts +167 -74
  110. package/src/memory/memory-command-module.ts +18 -1
  111. package/src/memory/memory-command.ts +38 -38
  112. package/src/mode/__tests__/mode-command-module.test.ts +51 -38
  113. package/src/mode/mode-command-module.ts +2 -1
  114. package/src/mode/mode-command.ts +15 -7
  115. package/src/output-style/__tests__/output-style-command.test.ts +96 -0
  116. package/src/output-style/index.ts +6 -0
  117. package/src/output-style/output-style-command-module.ts +143 -0
  118. package/src/peers/__tests__/peers-command.test.ts +321 -0
  119. package/src/peers/index.ts +9 -0
  120. package/src/peers/peers-command-module.ts +49 -0
  121. package/src/peers/peers-command.ts +191 -0
  122. package/src/permissions/__tests__/permissions-command-module.test.ts +149 -37
  123. package/src/permissions/permissions-command-module.ts +2 -1
  124. package/src/permissions/permissions-command.ts +46 -4
  125. package/src/plan/__tests__/plan-command.test.ts +129 -0
  126. package/src/plan/index.ts +9 -0
  127. package/src/plan/plan-command-module.ts +49 -0
  128. package/src/plan/plan-command.ts +92 -0
  129. package/src/plugin/__tests__/plugin-command-module.test.ts +46 -40
  130. package/src/plugin/__tests__/plugin-host-boundary.test.ts +11 -0
  131. package/src/plugin/plugin-command-module.ts +3 -1
  132. package/src/plugin/plugin-command.ts +17 -16
  133. package/src/preset/__tests__/host-supplied-preset-registry.test.ts +98 -0
  134. package/src/preset/__tests__/preset-command-module.test.ts +102 -56
  135. package/src/preset/preset-command-module.ts +26 -8
  136. package/src/preset/preset-command.ts +85 -27
  137. package/src/provider/__tests__/org-policy.test.ts +4 -5
  138. package/src/provider/__tests__/provider-command-module.test.ts +11 -14
  139. package/src/provider/__tests__/provider-startup-host-settings.test.ts +16 -0
  140. package/src/provider/__tests__/scripted-interaction.ts +7 -5
  141. package/src/provider/provider-command-execution.ts +4 -4
  142. package/src/provider/provider-command-module.ts +2 -1
  143. package/src/provider/provider-command-profile-lifecycle.ts +4 -4
  144. package/src/provider/provider-command-profile-operations.ts +3 -3
  145. package/src/provider/provider-command-profile.ts +1 -1
  146. package/src/provider/provider-command-setup.ts +4 -4
  147. package/src/provider/provider-startup.ts +62 -35
  148. package/src/remote-control/__tests__/remote-control-command-module.test.ts +36 -0
  149. package/src/remote-control/__tests__/remote-control-command.test.ts +216 -0
  150. package/src/remote-control/index.ts +8 -0
  151. package/src/remote-control/remote-control-command-module.ts +61 -0
  152. package/src/remote-control/remote-control-command.ts +138 -0
  153. package/src/reset/__tests__/reset-command-module.test.ts +5 -5
  154. package/src/reset/reset-command-module.ts +2 -1
  155. package/src/reset/reset-command.ts +3 -4
  156. package/src/rewind/__tests__/rewind-command-module.test.ts +40 -0
  157. package/src/rewind/rewind-command-module.ts +2 -1
  158. package/src/rewind/rewind-command.ts +73 -6
  159. package/src/sandbox/__tests__/sandbox-command.test.ts +80 -0
  160. package/src/sandbox/index.ts +2 -0
  161. package/src/sandbox/sandbox-command-module.ts +56 -0
  162. package/src/sandbox/sandbox-command.ts +94 -0
  163. package/src/schedule/__tests__/loop-command-functional.test.ts +386 -0
  164. package/src/schedule/__tests__/loop-command.test.ts +360 -0
  165. package/src/schedule/__tests__/loop-jitter.test.ts +25 -0
  166. package/src/schedule/__tests__/monitor-model-permission-functional.test.ts +126 -0
  167. package/src/schedule/__tests__/schedule-command.test.ts +143 -3
  168. package/src/schedule/__tests__/schedule-redos.test.ts +81 -0
  169. package/src/schedule/index.ts +3 -0
  170. package/src/schedule/loop-command.ts +333 -0
  171. package/src/schedule/loop-jitter.ts +41 -0
  172. package/src/schedule/schedule-command-module.ts +92 -14
  173. package/src/schedule/schedule-command.ts +98 -9
  174. package/src/schedule/schedule-spec-parser.ts +5 -1
  175. package/src/session/__tests__/rename-host-persistence.test.ts +72 -0
  176. package/src/session/__tests__/session-command-module.test.ts +210 -45
  177. package/src/session/session-command-module.ts +68 -37
  178. package/src/session/session-command.ts +147 -80
  179. package/src/settings/settings-command-module.ts +3 -2
  180. package/src/shell/__tests__/resolve-shell-host.test.ts +11 -0
  181. package/src/shell/__tests__/shell-command-functional.test.ts +23 -1
  182. package/src/shell/resolve-shell.ts +2 -2
  183. package/src/shell/shell-command-module.ts +6 -5
  184. package/src/shell/shell-command.ts +9 -5
  185. package/src/skills/__tests__/skills-command-module.test.ts +69 -28
  186. package/src/skills/index.ts +1 -0
  187. package/src/skills/skills-command-module.ts +22 -5
  188. package/src/skills/skills-command.ts +14 -13
  189. package/src/statusline/__tests__/statusline-command-module.test.ts +5 -4
  190. package/src/statusline/statusline-command-module.ts +2 -1
  191. package/src/statusline/statusline-command.ts +4 -4
  192. package/src/theme/__tests__/theme-command.test.ts +204 -0
  193. package/src/theme/index.ts +12 -0
  194. package/src/theme/theme-command-module.ts +211 -0
  195. package/src/user-local/__tests__/user-local-command.test.ts +145 -110
  196. package/src/user-local/user-local-command-constants.ts +1 -1
  197. package/src/user-local/user-local-command-module.ts +6 -5
  198. package/src/user-local/user-local-command.ts +12 -6
  199. package/src/user-local/user-local-memory-command.ts +17 -7
  200. package/src/plugins/default-plugin-command-adapter.ts +0 -164
  201. package/src/plugins/default-plugin-command-source-loader.ts +0 -31
@@ -0,0 +1,241 @@
1
+ /**
2
+ * What of the built-in command set the MODEL is offered, and what it is told.
3
+ *
4
+ * Pins, per command: offered or not, the exact model-facing descriptor, and that a user-only
5
+ * subcommand is neither offered nor runnable. The invariant behind it: trust, credential and
6
+ * permission-widening actions are never model-invocable. The reasons for each decision sit beside
7
+ * each command's entry.
8
+ */
9
+
10
+ import { afterEach, describe, expect, it, vi } from 'vitest';
11
+
12
+ import { SystemCommandExecutor } from '@robota-sdk/agent-framework';
13
+ import { createTestCommandHost } from '@robota-sdk/agent-framework/testing';
14
+
15
+ import { createDoctorCommandModule } from '../../doctor/index.js';
16
+ import { createKeybindingsCommandModule } from '../../keybindings/index.js';
17
+ import { createThemeCommandModule } from '../../theme/index.js';
18
+ import { createDoctorFixture } from '../../doctor/__tests__/doctor-fixture.js';
19
+ import { createDefaultCommandModules } from '../default-command-modules.js';
20
+
21
+ import type { IDoctorFixture } from '../../doctor/__tests__/doctor-fixture.js';
22
+ import type { IProviderDefinition } from '@robota-sdk/agent-core';
23
+ import type {
24
+ IProviderCommandSettingsAdapter,
25
+ ISystemCommand,
26
+ TProviderSettingsDocument,
27
+ } from '@robota-sdk/agent-framework';
28
+ import type { IThemeCataloguePort } from '@robota-sdk/agent-interface-command';
29
+
30
+ const providerDefinitions: readonly IProviderDefinition[] = [];
31
+ const providerSettingsAdapter: IProviderCommandSettingsAdapter = {
32
+ readMergedSettings: () => ({}) as TProviderSettingsDocument,
33
+ readTargetSettings: () => ({}) as TProviderSettingsDocument,
34
+ writeTargetSettings: () => undefined,
35
+ };
36
+ const themeCataloguePort: IThemeCataloguePort = {
37
+ listThemes: () => [],
38
+ getTheme: () => undefined,
39
+ getAppearance: () => ({
40
+ settings: { theme: 'dark', syntaxHighlighting: true, reducedMotion: false },
41
+ }),
42
+ };
43
+
44
+ const fixtures: IDoctorFixture[] = [];
45
+ afterEach(() => {
46
+ for (const fixture of fixtures.splice(0)) fixture.cleanup();
47
+ });
48
+
49
+ /** Every built-in command, including the ones only registered when their host port exists. */
50
+ function allBuiltInCommands(): ISystemCommand[] {
51
+ const fixture = createDoctorFixture({ env: {} });
52
+ fixtures.push(fixture);
53
+ const { modules } = createDefaultCommandModules({
54
+ cwd: '/tmp',
55
+ userLocalStorageRoot: '/tmp/robota-test',
56
+ providerDefinitions,
57
+ providerSettingsAdapter,
58
+ });
59
+ return [
60
+ ...modules,
61
+ createKeybindingsCommandModule({ ensureFile: async () => '/tmp/keybindings.json' }),
62
+ createThemeCommandModule(themeCataloguePort),
63
+ createDoctorCommandModule(fixture.inputs, fixture.deps),
64
+ ].flatMap((module) => [...(module.systemCommands ?? [])]);
65
+ }
66
+
67
+ /** Each command with its `execute` replaced, so a test can see whether it ran at all. */
68
+ function spiedExecutor(): {
69
+ executor: SystemCommandExecutor;
70
+ executed: ReturnType<typeof vi.fn>;
71
+ } {
72
+ const executed = vi.fn(() => ({ success: true, message: 'ran' }));
73
+ const commands = allBuiltInCommands().map((command) => ({ ...command, execute: executed }));
74
+ return { executor: new SystemCommandExecutor(commands), executed };
75
+ }
76
+
77
+ /** Offered to the model, each for a reason given beside its entry. */
78
+ const MODEL_INVOCABLE = [
79
+ 'agent',
80
+ 'compact',
81
+ 'context',
82
+ 'cost',
83
+ 'loop',
84
+ 'mcp',
85
+ 'memory',
86
+ 'monitor',
87
+ 'schedule',
88
+ 'skills',
89
+ ];
90
+
91
+ /** Whole commands the model is never offered. */
92
+ const USER_ONLY_COMMANDS = [
93
+ // Trust decisions.
94
+ 'plugin',
95
+ 'reload-plugins',
96
+ 'remote-control',
97
+ // Account and credential actions.
98
+ 'provider',
99
+ // Permission widening or permission-mode changes.
100
+ 'mode',
101
+ 'permissions',
102
+ 'sandbox',
103
+ 'preset',
104
+ 'plan',
105
+ 'goal',
106
+ 'cd',
107
+ // Crossing into another session, machine or process the user owns.
108
+ 'peers',
109
+ 'handoff',
110
+ 'fork',
111
+ 'shell',
112
+ 'git',
113
+ // Exits and destructive session changes.
114
+ 'exit',
115
+ 'clear',
116
+ 'reset',
117
+ 'rewind',
118
+ 'resume',
119
+ // UI-only preferences, and views the user reads.
120
+ 'advisor',
121
+ 'effort',
122
+ 'language',
123
+ 'output-style',
124
+ 'statusline',
125
+ 'theme',
126
+ 'keybindings',
127
+ 'settings',
128
+ 'editor',
129
+ 'rename',
130
+ 'help',
131
+ 'background',
132
+ 'doctor',
133
+ 'user-local',
134
+ 'validate-session',
135
+ ];
136
+
137
+ /** User-only subcommands of commands the model IS offered; `list` is an alias `/mcp` accepts for the user. */
138
+ const USER_ONLY_SUBCOMMANDS: Record<string, readonly string[]> = {
139
+ mcp: ['approve', 'reject', 'revoke', 'logout', 'login', 'list'],
140
+ memory: ['approve', 'reject'],
141
+ context: ['add', 'remove', 'clear', 'auto'],
142
+ cost: ['budget'],
143
+ };
144
+
145
+ describe('built-in commands offered to the model', () => {
146
+ it('offers exactly the model-invocable set', () => {
147
+ const { executor } = spiedExecutor();
148
+ const offered = executor
149
+ .listModelInvocableCommands()
150
+ .map((descriptor) => descriptor.name)
151
+ .sort();
152
+ expect(offered).toEqual([...MODEL_INVOCABLE].sort());
153
+ });
154
+
155
+ it('covers every built-in command with a decision', () => {
156
+ const names = allBuiltInCommands()
157
+ .map((command) => command.name)
158
+ .sort();
159
+ expect(names).toEqual([...MODEL_INVOCABLE, ...USER_ONLY_COMMANDS].sort());
160
+ });
161
+
162
+ it('describes every offered command for the model: what, when, and what it returns', () => {
163
+ for (const command of allBuiltInCommands().filter((c) => c.modelInvocable === true)) {
164
+ expect(command.modelDescription, command.name).toBeDefined();
165
+ expect(command.modelDescription, command.name).toMatch(/\bUse\b|\bBefore\b|\bWhen\b/);
166
+ expect(command.modelDescription, command.name).toMatch(/\b[Rr]eturns\b/);
167
+ }
168
+ });
169
+
170
+ it('pins the descriptor each offered command gets', () => {
171
+ const { executor } = spiedExecutor();
172
+ const descriptors = Object.fromEntries(
173
+ executor
174
+ .listModelInvocableCommands()
175
+ .sort((a, b) => a.name.localeCompare(b.name))
176
+ .map((descriptor) => [descriptor.name, descriptor]),
177
+ );
178
+ expect(descriptors).toMatchSnapshot();
179
+ });
180
+
181
+ it('never offers or runs a user-only command', async () => {
182
+ const { executor, executed } = spiedExecutor();
183
+ const host = createTestCommandHost();
184
+ for (const name of USER_ONLY_COMMANDS) {
185
+ expect(executor.isModelInvocable(name), name).toBe(false);
186
+ expect(await executor.executeModelInvocable(name, host, ''), name).toBeNull();
187
+ }
188
+ expect(executed).not.toHaveBeenCalled();
189
+ });
190
+
191
+ it('never offers a user-only subcommand, and refuses it before the command runs', async () => {
192
+ const { executor, executed } = spiedExecutor();
193
+ const host = createTestCommandHost();
194
+ const descriptors = executor.listModelInvocableCommands();
195
+ for (const [name, subcommands] of Object.entries(USER_ONLY_SUBCOMMANDS)) {
196
+ const descriptor = descriptors.find((candidate) => candidate.name === name);
197
+ const offeredBlock = descriptor?.description.split('Subcommands you may run')[1] ?? '';
198
+ for (const sub of subcommands) {
199
+ expect(descriptor?.argumentHint ?? '', `${name} ${sub}`).not.toMatch(
200
+ new RegExp(`\\b${sub}\\b`),
201
+ );
202
+ expect(offeredBlock, `${name} ${sub}`).not.toContain(`- ${sub}:`);
203
+ const result = await executor.executeModelInvocable(name, host, `${sub} some-arg`);
204
+ expect(result?.success, `${name} ${sub}`).toBe(false);
205
+ }
206
+ }
207
+ expect(executed).not.toHaveBeenCalled();
208
+ });
209
+
210
+ it('runs the read-only subset for the model', async () => {
211
+ const { executor, executed } = spiedExecutor();
212
+ const host = createTestCommandHost();
213
+ for (const [name, args] of [
214
+ ['mcp', ''],
215
+ ['mcp', 'status'],
216
+ ['memory', 'list'],
217
+ ['context', ''],
218
+ ['context', 'list'],
219
+ ['cost', ''],
220
+ ] as const) {
221
+ expect((await executor.executeModelInvocable(name, host, args))?.message).toBe('ran');
222
+ }
223
+ expect(executed).toHaveBeenCalledTimes(6);
224
+ });
225
+
226
+ it('skips the by-name prompt only where the model reaches nothing that needs it', () => {
227
+ const { executor } = spiedExecutor();
228
+ const byName = Object.fromEntries(
229
+ executor.listModelInvocableCommands().map((d) => [d.name, d.requiresPermission]),
230
+ );
231
+ // `/mcp`: the model reaches only `status`. `/monitor`: its command is decided by the shell
232
+ // gate instead (monitor-model-permission-functional.test.ts).
233
+ expect(byName.mcp).toBe(false);
234
+ expect(byName.monitor).toBe(false);
235
+ // Neither becomes read-only for the user or a remote read-only policy.
236
+ for (const name of ['mcp', 'monitor']) {
237
+ const command = executor.getCommand(name)!;
238
+ expect(executor.resolveRequiresPermission(command), name).toBe(true);
239
+ }
240
+ });
241
+ });
@@ -0,0 +1,98 @@
1
+ /**
2
+ * CLI-083 (issue #2287) — the default command modules forward the org policy.
3
+ *
4
+ * `createDefaultCommandModules` used to accept `orgPolicy` and hand it to the provider command
5
+ * module; `92596bc6f` removed the parameter along with its only caller. The consumer never changed:
6
+ * `provider-command-profile-operations.ts` still destructures `orgPolicy` and enforces
7
+ * `allowedProviders` on it.
8
+ *
9
+ * `org-policy.test.ts` already covers that enforcement — by constructing the provider module
10
+ * DIRECTLY with a policy. That test is green whether or not anything upstream forwards one, which
11
+ * is why the cut survived three months. This drives the same enforcement through the factory that
12
+ * lost the parameter, so it fails exactly when the forwarding is missing.
13
+ *
14
+ * There is deliberately no "the factory accepts an orgPolicy" case. At runtime JavaScript accepts an
15
+ * undeclared extra property silently, so that assertion passes whether or not the parameter exists —
16
+ * it was written, it passed against the broken tree, and it proved nothing. The signature is the
17
+ * type checker's job.
18
+ */
19
+
20
+ import { describe, expect, it } from 'vitest';
21
+ import { SystemCommandExecutor } from '@robota-sdk/agent-framework';
22
+ import { createTestCommandHost } from '@robota-sdk/agent-framework/testing';
23
+
24
+ import { createDefaultCommandModules } from '../default-command-modules.js';
25
+
26
+ import type { IProviderDefinition } from '@robota-sdk/agent-core';
27
+ import type {
28
+ IOrgPolicy,
29
+ IProviderCommandSettingsAdapter,
30
+ TProviderSettingsDocument,
31
+ } from '@robota-sdk/agent-framework';
32
+
33
+ const providerDefinitions: readonly IProviderDefinition[] = ['anthropic', 'openai'].map((type) => ({
34
+ type,
35
+ displayName: type,
36
+ defaults: { model: 'm', apiKey: `$ENV:${type.toUpperCase()}_API_KEY` },
37
+ setupSteps: [],
38
+ requiresApiKey: true,
39
+ createProvider: () => {
40
+ throw new Error('not used');
41
+ },
42
+ }));
43
+
44
+ const SETTINGS: TProviderSettingsDocument = {
45
+ currentProvider: 'anthropic',
46
+ providers: {
47
+ anthropic: { type: 'anthropic', model: 'm' },
48
+ openai: { type: 'openai', model: 'm' },
49
+ },
50
+ };
51
+
52
+ const adapter: IProviderCommandSettingsAdapter = {
53
+ readMergedSettings: () => SETTINGS,
54
+ readTargetSettings: () => ({}),
55
+ writeTargetSettings: () => undefined,
56
+ };
57
+
58
+ const POLICY: IOrgPolicy = { allowedProviders: ['anthropic'], adminContact: 'ops@example.com' };
59
+
60
+ /** Build the provider executor the way the PRODUCT does — through the default-modules factory. */
61
+ function executorThroughFactory(orgPolicy?: IOrgPolicy): SystemCommandExecutor {
62
+ const { modules } = createDefaultCommandModules({
63
+ cwd: '/work',
64
+ userLocalStorageRoot: '/tmp/robota-test',
65
+ providerDefinitions,
66
+ providerSettingsAdapter: adapter,
67
+ ...(orgPolicy === undefined ? {} : { orgPolicy }),
68
+ });
69
+ const provider = modules.find((module) => module.name === 'agent-command-provider');
70
+ if (provider === undefined) throw new Error('the provider command module was not built');
71
+ return new SystemCommandExecutor([...(provider.systemCommands ?? [])]);
72
+ }
73
+
74
+ describe('CLI-083: the default command modules forward the org policy', () => {
75
+ it('blocks a disallowed provider switch when a policy is supplied to the FACTORY', async () => {
76
+ const result = await executorThroughFactory(POLICY).execute(
77
+ 'provider',
78
+ createTestCommandHost(),
79
+ 'switch openai',
80
+ );
81
+
82
+ expect(result?.success).toBe(false);
83
+ expect(result?.message).toContain('"openai" is not allowed');
84
+ expect(result?.message).toContain('ops@example.com');
85
+ });
86
+
87
+ it('does not block when no policy is supplied — absence stays distinguishable', async () => {
88
+ // The control. A factory that attached a policy of its own would satisfy the case above and
89
+ // change behaviour for every user who has no org policy at all.
90
+ const result = await executorThroughFactory().execute(
91
+ 'provider',
92
+ createTestCommandHost(),
93
+ 'switch openai',
94
+ );
95
+
96
+ expect(result?.message ?? '').not.toContain('is not allowed');
97
+ });
98
+ });
@@ -1,18 +1,34 @@
1
+ import { findUnknownModuleNames, selectCommandModules } from '@robota-sdk/agent-framework';
2
+
1
3
  import { createAgentCommandModule } from '../agent/index.js';
2
4
  import { createBackgroundCommandModule } from '../background/index.js';
3
5
  import { createCompactCommandModule } from '../compact/index.js';
4
6
  import { createContextCommandModule } from '../context/index.js';
7
+ import { createDevicesCommandModule } from '../devices/index.js';
8
+ import { createDoctorCommandModule } from '../doctor/index.js';
5
9
  import { createEditorCommandModule } from '../editor/index.js';
10
+ import { createAdvisorCommandModule } from '../advisor/index.js';
11
+ import { createEffortCommandModule } from '../effort/index.js';
6
12
  import { createExitCommandModule } from '../exit/index.js';
13
+ import { createForkCommandModule } from '../fork/index.js';
14
+ import { createGitCommandModule } from '../git/index.js';
7
15
  import { createGoalCommandModule } from '../goal/index.js';
16
+ import { createHandoffCommandModule } from '../handoff/index.js';
8
17
  import { createHelpCommandModule } from '../help/index.js';
18
+ import { createKeybindingsCommandModule } from '../keybindings/index.js';
9
19
  import { createLanguageCommandModule } from '../language/index.js';
20
+ import { createMCPActivationCommandModule } from '../mcp-activation/index.js';
10
21
  import { createMemoryCommandModule } from '../memory/index.js';
11
22
  import { createModeCommandModule } from '../mode/index.js';
23
+ import { createSandboxCommandModule } from '../sandbox/index.js';
24
+ import { createOutputStyleCommandModule } from '../output-style/index.js';
25
+ import { createPeersCommandModule } from '../peers/index.js';
12
26
  import { createPermissionsCommandModule } from '../permissions/index.js';
27
+ import { createPlanCommandModule } from '../plan/index.js';
13
28
  import { createPluginCommandModule } from '../plugin/index.js';
14
29
  import { createPresetCommandModule } from '../preset/index.js';
15
30
  import { createProviderCommandModule } from '../provider/index.js';
31
+ import { createRemoteControlCommandModule } from '../remote-control/index.js';
16
32
  import { createResetCommandModule } from '../reset/index.js';
17
33
  import { createRewindCommandModule } from '../rewind/index.js';
18
34
  import { createScheduleCommandModule } from '../schedule/index.js';
@@ -21,15 +37,62 @@ import { createSettingsCommandModule } from '../settings/index.js';
21
37
  import { createShellCommandModule } from '../shell/index.js';
22
38
  import { createSkillsCommandModule } from '../skills/index.js';
23
39
  import { createStatusLineCommandModule } from '../statusline/index.js';
40
+ import { createThemeCommandModule } from '../theme/index.js';
24
41
  import { createUserLocalCommandModule } from '../user-local/index.js';
25
42
 
43
+ import type { IDevicesCommandPort } from '../devices/index.js';
44
+ import type { IDoctorDisplayVocabulary, IDoctorInputs } from '../doctor/index.js';
45
+ import type { IKeybindingsFilePort } from '../keybindings/index.js';
26
46
  import type { IProviderDefinition } from '@robota-sdk/agent-core';
27
- import type { ICommandModule, IProviderCommandSettingsAdapter } from '@robota-sdk/agent-framework';
47
+ import type {
48
+ IOrgPolicy,
49
+ IContributionSource,
50
+ ISkillRootDescriptor,
51
+ ICommandModule,
52
+ IProviderCommandSettingsAdapter,
53
+ IUnknownCommandModuleName,
54
+ } from '@robota-sdk/agent-framework';
55
+ import type { IThemeCataloguePort } from '@robota-sdk/agent-interface-command';
28
56
 
29
57
  export interface IDefaultCommandModulesOptions {
30
58
  cwd: string;
59
+ userLocalStorageRoot: string;
60
+ /** Product-owned prefix for the interactive editor's temporary directory. */
61
+ editorTemporaryDirectoryPrefix?: string;
62
+ contributionSources?: readonly IContributionSource[];
63
+ skillRoots?: readonly ISkillRootDescriptor[];
31
64
  providerDefinitions: readonly IProviderDefinition[];
32
65
  providerSettingsAdapter: IProviderCommandSettingsAdapter;
66
+ /**
67
+ * CLI-083 (issue #2287) — the org policy, so `allowedProviders` and `requireApiKeyFromEnv` are
68
+ * reachable. This parameter existed, was removed by `92596bc6f` along with its only caller, and
69
+ * the consumer never stopped reading it: `provider-command-profile-operations.ts` still
70
+ * destructures `orgPolicy` from its options and enforces on it. Optional, so its absence stays
71
+ * distinguishable from a policy that allows everything — and that optionality is why nothing went
72
+ * red when the producer dropped it.
73
+ */
74
+ orgPolicy?: IOrgPolicy;
75
+ /** Optional TUI-owned file capability; absence means `/keybindings` is not registered. */
76
+ keybindingsFilePort?: IKeybindingsFilePort;
77
+ /**
78
+ * SCREEN-2002: the surface's theme catalogue. Absence means `/theme` is not registered at all —
79
+ * print mode and `--serve` render no themes, and a command that cannot do anything is better
80
+ * missing than present-and-failing.
81
+ */
82
+ themeCataloguePort?: IThemeCataloguePort;
83
+ /**
84
+ * The host that keeps this device's identity (its terminal, credential store and state files).
85
+ * Absence means `/devices` is not registered: without it the command could do nothing.
86
+ */
87
+ devicesPort?: IDevicesCommandPort;
88
+ /** OBSERVABILITY-1991: host-composed doctor inputs; absence means `/doctor` is not registered. */
89
+ doctorInputs?: IDoctorInputs;
90
+ /** Product-owned diagnostic wording; absent keeps `/doctor` product-neutral. */
91
+ doctorDisplay?: IDoctorDisplayVocabulary;
92
+ /** Product-owned command to resume a saved fork; absent yields neutral host guidance. */
93
+ formatForkResumeCommand?: (sessionId: string) => string;
94
+ /** Host-owned fallback text and kill switch for session-local repeat. */
95
+ loopOptions?: { defaultPrompt?: string; resolveDefaultPrompt?: () => string; disabled?: boolean };
33
96
  /**
34
97
  * Whitelist of module `name`s to keep. When provided, only modules whose `name`
35
98
  * appears here survive. Omitted → all modules kept (no-regression).
@@ -48,59 +111,108 @@ export interface IDefaultCommandModulesOptions {
48
111
  * Rules: if `enabled` is provided, keep only modules whose `name` is in it; then
49
112
  * remove any module whose `name` is in `disabled` (deny > allow). Neither given →
50
113
  * the full default set is returned unchanged (no-regression).
114
+ *
115
+ * INFRA-032: delegates to agent-framework's `selectCommandModules` (the allowed command→framework
116
+ * edge) so the filter body exists once — the previously byte-identical copy here is collapsed.
51
117
  */
52
118
  function applyModuleSelection(
53
119
  modules: readonly ICommandModule[],
54
120
  enabled: readonly string[] | undefined,
55
121
  disabled: readonly string[] | undefined,
56
122
  ): readonly ICommandModule[] {
57
- let selected = modules;
58
- if (enabled !== undefined) {
59
- const allow = new Set(enabled);
60
- selected = selected.filter((module) => allow.has(module.name));
61
- }
62
- if (disabled !== undefined) {
63
- const deny = new Set(disabled);
64
- selected = selected.filter((module) => !deny.has(module.name));
65
- }
66
- return selected;
123
+ return selectCommandModules(modules, enabled, disabled);
124
+ }
125
+
126
+ /**
127
+ * Result of {@link createDefaultCommandModules} (INFRA-032): the selected `modules` plus any preset
128
+ * `enabledCommandModules`/`disabledCommandModules` names that matched no built module. Unknown names
129
+ * are returned as data (not dropped silently) so the CLI startup path can surface a non-fatal notice.
130
+ */
131
+ export interface IDefaultCommandModulesResult {
132
+ readonly modules: readonly ICommandModule[];
133
+ readonly unknownModuleNames: readonly IUnknownCommandModuleName[];
67
134
  }
68
135
 
69
136
  export function createDefaultCommandModules({
70
- cwd,
137
+ cwd: _cwd,
138
+ userLocalStorageRoot,
139
+ editorTemporaryDirectoryPrefix,
140
+ contributionSources,
141
+ skillRoots,
71
142
  providerDefinitions,
72
143
  providerSettingsAdapter,
144
+ orgPolicy,
145
+ keybindingsFilePort,
146
+ themeCataloguePort,
147
+ devicesPort,
148
+ doctorInputs,
149
+ doctorDisplay,
150
+ formatForkResumeCommand,
151
+ loopOptions,
73
152
  enabledCommandModules,
74
153
  disabledCommandModules,
75
- }: IDefaultCommandModulesOptions): readonly ICommandModule[] {
154
+ }: IDefaultCommandModulesOptions): IDefaultCommandModulesResult {
76
155
  const modules: readonly ICommandModule[] = [
77
- createSkillsCommandModule({ cwd }),
156
+ createSkillsCommandModule({ contributionSources: contributionSources ?? [], skillRoots }),
78
157
  createHelpCommandModule(),
79
158
  createAgentCommandModule(),
159
+ createEffortCommandModule(),
160
+ createAdvisorCommandModule(),
80
161
  createPermissionsCommandModule(),
81
162
  createModeCommandModule(),
163
+ createSandboxCommandModule(),
82
164
  createPresetCommandModule(),
165
+ createOutputStyleCommandModule(),
83
166
  createLanguageCommandModule(),
84
167
  createBackgroundCommandModule(),
168
+ // CLI-1994: beside `/background`, because a fork IS a background job — the one it starts is
169
+ // listed, peeked at, stopped and attached to through that command and its panel.
170
+ createForkCommandModule(formatForkResumeCommand),
85
171
  createGoalCommandModule(),
172
+ createPlanCommandModule(),
86
173
  createShellCommandModule(),
87
- createEditorCommandModule(),
174
+ createEditorCommandModule(editorTemporaryDirectoryPrefix),
175
+ createGitCommandModule(),
176
+ ...(keybindingsFilePort === undefined
177
+ ? []
178
+ : [createKeybindingsCommandModule(keybindingsFilePort)]),
179
+ ...(themeCataloguePort === undefined ? [] : [createThemeCommandModule(themeCataloguePort)]),
180
+ ...(doctorInputs === undefined
181
+ ? []
182
+ : [createDoctorCommandModule(doctorInputs, undefined, doctorDisplay)]),
88
183
  createMemoryCommandModule(),
89
- createUserLocalCommandModule(),
184
+ createMCPActivationCommandModule(),
185
+ createUserLocalCommandModule(userLocalStorageRoot),
90
186
  createCompactCommandModule(),
91
187
  createContextCommandModule(),
92
188
  createExitCommandModule(),
93
189
  createSessionCommandModule(),
94
190
  createResetCommandModule(),
95
191
  createRewindCommandModule(),
96
- createScheduleCommandModule(),
192
+ createScheduleCommandModule(loopOptions),
97
193
  createStatusLineCommandModule(),
98
194
  createPluginCommandModule(),
99
195
  createSettingsCommandModule(),
196
+ createPeersCommandModule(),
197
+ // HANDOFF-001 (issue #1864). Registered even though no product wires the carrier adapter yet:
198
+ // the command's own answer to a host without one is to say so, which is a better state than a
199
+ // capability nobody can see. It is also what makes the carrier's arrival observable.
200
+ createHandoffCommandModule(),
201
+ createRemoteControlCommandModule(),
202
+ ...(devicesPort === undefined ? [] : [createDevicesCommandModule(devicesPort)]),
100
203
  createProviderCommandModule({
101
204
  providerDefinitions,
102
205
  settings: providerSettingsAdapter,
206
+ ...(orgPolicy === undefined ? {} : { orgPolicy }),
103
207
  }),
104
208
  ];
105
- return applyModuleSelection(modules, enabledCommandModules, disabledCommandModules);
209
+ const builtModuleNames = modules.map((module) => module.name);
210
+ return {
211
+ modules: applyModuleSelection(modules, enabledCommandModules, disabledCommandModules),
212
+ unknownModuleNames: findUnknownModuleNames(
213
+ builtModuleNames,
214
+ enabledCommandModules,
215
+ disabledCommandModules,
216
+ ),
217
+ };
106
218
  }
@@ -1,2 +1,5 @@
1
- export type { IDefaultCommandModulesOptions } from './default-command-modules.js';
1
+ export type {
2
+ IDefaultCommandModulesOptions,
3
+ IDefaultCommandModulesResult,
4
+ } from './default-command-modules.js';
2
5
  export { createDefaultCommandModules } from './default-command-modules.js';