@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
@@ -5,6 +5,7 @@ import type {
5
5
  } from '@robota-sdk/agent-framework';
6
6
  import { SystemCommandExecutor } from '@robota-sdk/agent-framework';
7
7
  import { createModeCommandModule } from '../mode-command-module.js';
8
+ import { createTestCommandHost } from '@robota-sdk/agent-framework/testing';
8
9
 
9
10
  type TPermissionModeName = 'plan' | 'default' | 'acceptEdits' | 'bypassPermissions';
10
11
  type TSetPermissionModeSpy = ReturnType<typeof vi.fn<(nextMode: TPermissionModeName) => void>>;
@@ -25,7 +26,7 @@ function createCheckpointResult(): IEditCheckpointRestoreResult {
25
26
  };
26
27
  }
27
28
 
28
- function createCommandHostContext(): ICommandHostContext & {
29
+ function createCommandHostContext(): ReturnType<typeof createTestCommandHost> & {
29
30
  setPermissionMode: TSetPermissionModeSpy;
30
31
  } {
31
32
  let mode: TPermissionModeName = 'default';
@@ -34,34 +35,41 @@ function createCommandHostContext(): ICommandHostContext & {
34
35
  });
35
36
 
36
37
  return {
37
- getSession: () => {
38
- throw new Error('mode command should use the permission mode adapter');
39
- },
40
- getCommandHostAdapters: () => ({
41
- permissionMode: {
42
- getPermissionMode: () => mode,
43
- setPermissionMode,
44
- listSessionAllowedTools: () => [],
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,
45
71
  },
46
72
  }),
47
- getContextState: () => ({
48
- usedTokens: 0,
49
- maxTokens: 1,
50
- usedPercentage: 0,
51
- remainingPercentage: 100,
52
- }),
53
- getAutoCompactThreshold: () => 0.835,
54
- compactContext: async () => undefined,
55
- getCwd: () => '/workspace',
56
- listEditCheckpoints: () => [],
57
- restoreEditCheckpoint: async () => createCheckpointResult(),
58
- rollbackEditCheckpoint: async () => createCheckpointResult(),
59
- getUsedMemoryReferences: () => [],
60
- recordMemoryEvent: () => undefined,
61
- listBackgroundTasks: () => [],
62
- readBackgroundTaskLog: async () => ({ taskId: 'task_1', lines: [] }),
63
- cancelBackgroundTask: async () => undefined,
64
- closeBackgroundTask: async () => undefined,
65
73
  setPermissionMode,
66
74
  };
67
75
  }
@@ -77,7 +85,7 @@ describe('createModeCommandModule', () => {
77
85
  expect.objectContaining({
78
86
  name: 'mode',
79
87
  description: 'Show/change permission mode',
80
- argumentHint: 'plan | default | acceptEdits | bypassPermissions',
88
+ argumentHint: 'plan | default | acceptEdits | bypassPermissions | auto',
81
89
  source: 'mode',
82
90
  modelInvocable: false,
83
91
  }),
@@ -87,12 +95,13 @@ describe('createModeCommandModule', () => {
87
95
  'default',
88
96
  'acceptEdits',
89
97
  'bypassPermissions',
98
+ 'auto',
90
99
  ]);
91
100
  expect(command).toEqual(
92
101
  expect.objectContaining({
93
102
  name: 'mode',
94
103
  description: 'Show/change permission mode',
95
- argumentHint: 'plan | default | acceptEdits | bypassPermissions',
104
+ argumentHint: 'plan | default | acceptEdits | bypassPermissions | auto',
96
105
  lifecycle: 'inline',
97
106
  modelInvocable: false,
98
107
  }),
@@ -136,7 +145,7 @@ describe('createModeCommandModule', () => {
136
145
 
137
146
  expect(result?.success).toBe(false);
138
147
  expect(result?.message).toBe(
139
- 'Invalid mode. Valid: plan | default | acceptEdits | bypassPermissions',
148
+ 'Invalid mode. Valid: plan | default | acceptEdits | bypassPermissions | auto',
140
149
  );
141
150
  expect(context.setPermissionMode).not.toHaveBeenCalled();
142
151
  });
@@ -146,10 +155,12 @@ describe('createModeCommandModule', () => {
146
155
  ...(createModeCommandModule().systemCommands ?? []),
147
156
  ]);
148
157
  const context = createCommandHostContext();
149
- const contextWithAsk: ICommandHostContext = {
150
- ...context,
151
- getUserInteraction: () => ({ ask: async () => ({ type: 'answer', values: ['plan'] }) }),
152
- };
158
+ const contextWithAsk = createTestCommandHost({
159
+ overrides: {
160
+ ...context,
161
+ getUserInteraction: () => ({ ask: async () => ({ type: 'answer', values: ['plan'] }) }),
162
+ },
163
+ });
153
164
 
154
165
  const result = await executor.execute('mode', contextWithAsk, '');
155
166
 
@@ -163,10 +174,12 @@ describe('createModeCommandModule', () => {
163
174
  ...(createModeCommandModule().systemCommands ?? []),
164
175
  ]);
165
176
  const context = createCommandHostContext();
166
- const contextWithAsk: ICommandHostContext = {
167
- ...context,
168
- getUserInteraction: () => ({ ask: async () => ({ type: 'cancelled' }) }),
169
- };
177
+ const contextWithAsk = createTestCommandHost({
178
+ overrides: {
179
+ ...context,
180
+ getUserInteraction: () => ({ ask: async () => ({ type: 'cancelled' }) }),
181
+ },
182
+ });
170
183
 
171
184
  const result = await executor.execute('mode', contextWithAsk, '');
172
185
 
@@ -7,7 +7,7 @@ import {
7
7
  import { executeModeCommand } from './mode-command.js';
8
8
 
9
9
  import type { ICommandModule, ISystemCommand } from '@robota-sdk/agent-framework';
10
- import type { ICommand, ICommandSource } from '@robota-sdk/agent-interface-transport';
10
+ import type { ICommand, ICommandSource } from '@robota-sdk/agent-interface-command';
11
11
 
12
12
  export function createModeCommandEntry(): ICommand {
13
13
  return {
@@ -17,6 +17,7 @@ export function createModeCommandEntry(): ICommand {
17
17
  source: 'mode',
18
18
  argumentHint: PERMISSION_MODE_ARGUMENT_HINT,
19
19
  subcommands: buildPermissionModeSubcommands('mode'),
20
+ // User-only: changes the permission mode; permission widening is the user's.
20
21
  modelInvocable: false,
21
22
  };
22
23
  }
@@ -5,19 +5,26 @@ import {
5
5
  isPermissionMode,
6
6
  parsePermissionModeArgument,
7
7
  readCommandPermissionMode,
8
- writeCommandPermissionMode,
9
8
  } from '@robota-sdk/agent-framework';
10
9
 
11
- import type { ICommandHostContext } from '@robota-sdk/agent-framework';
12
- import type { ICommandResult } from '@robota-sdk/agent-interface-transport';
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';
13
18
 
14
19
  /**
15
20
  * Ask the user to pick a permission mode (CMD-004 inline ask). Returns the chosen mode name, or
16
21
  * `undefined` when no interactive renderer is attached or the user cancelled — the caller then reports
17
22
  * the current mode instead of changing it (never a silent guess).
18
23
  */
19
- async function resolveModeViaAsk(context: ICommandHostContext): Promise<string | undefined> {
20
- const ui = context.getUserInteraction?.();
24
+ async function resolveModeViaAsk(
25
+ context: ICommandHostUserInteraction,
26
+ ): Promise<string | undefined> {
27
+ const ui = context.getUserInteraction();
21
28
  if (!ui) return undefined;
22
29
  const options = buildPermissionModeSubcommands().map((sub) => ({
23
30
  value: sub.name,
@@ -29,7 +36,7 @@ async function resolveModeViaAsk(context: ICommandHostContext): Promise<string |
29
36
  }
30
37
 
31
38
  export async function executeModeCommand(
32
- context: ICommandHostContext,
39
+ context: ICommandHostAdapterAccess & ICommandHostSessionAccess & ICommandHostUserInteraction,
33
40
  args: string,
34
41
  ): Promise<ICommandResult> {
35
42
  let arg: string | undefined = parsePermissionModeArgument(args);
@@ -50,7 +57,8 @@ export async function executeModeCommand(
50
57
  };
51
58
  }
52
59
 
53
- writeCommandPermissionMode(context, arg);
60
+ const refused = tryWritePermissionMode(context, arg);
61
+ if (refused !== undefined) return refused;
54
62
  return {
55
63
  message: `Permission mode set to: ${arg}`,
56
64
  success: true,
@@ -0,0 +1,96 @@
1
+ import { describe, expect, it, vi } from 'vitest';
2
+
3
+ import { createTestCommandHost } from '@robota-sdk/agent-framework/testing';
4
+
5
+ import {
6
+ createOutputStyleCommandEntry,
7
+ executeOutputStyleCommand,
8
+ } from '../output-style-command-module.js';
9
+
10
+ const styles = [
11
+ {
12
+ id: 'default',
13
+ name: 'Default',
14
+ description: 'Balanced responses.',
15
+ tokenCost: 'baseline',
16
+ source: 'built-in',
17
+ },
18
+ {
19
+ id: 'concise',
20
+ name: 'Concise',
21
+ description: 'Lead with the answer.',
22
+ tokenCost: 'low',
23
+ source: 'built-in',
24
+ },
25
+ ] as const;
26
+
27
+ function context() {
28
+ const ask = vi.fn().mockResolvedValue({ type: 'answer', values: ['concise'] });
29
+ const host = createTestCommandHost({
30
+ overrides: {
31
+ getActiveOutputStyleId: () => 'default',
32
+ getUserInteraction: () => ({ ask }),
33
+ getCommandHostAdapters: () => ({
34
+ outputStyleRegistry: {
35
+ listOutputStyles: () => styles,
36
+ getOutputStyle: (id) =>
37
+ id === 'concise'
38
+ ? {
39
+ id: 'concise',
40
+ name: 'Concise',
41
+ instructions: 'Lead with the answer.',
42
+ keepCodingInstructions: true,
43
+ tokenCost: 'low',
44
+ }
45
+ : undefined,
46
+ },
47
+ }),
48
+ },
49
+ });
50
+ return { host, ask };
51
+ }
52
+
53
+ describe('/output-style', () => {
54
+ it('lists styles with the active marker and token-cost metadata', async () => {
55
+ const { host } = context();
56
+ const result = await executeOutputStyleCommand(host, 'list');
57
+
58
+ expect(result.success).toBe(true);
59
+ expect(result.message).toContain(
60
+ '* default — Default: Balanced responses. [input cost: baseline]',
61
+ );
62
+ expect(result.message).toContain(
63
+ ' concise — Concise: Lead with the answer. [input cost: low]',
64
+ );
65
+ expect(result.data).toEqual({ outputStyles: styles, active: 'default' });
66
+ });
67
+
68
+ it('uses the inline picker when no id is supplied and emits only a style id action', async () => {
69
+ const { host, ask } = context();
70
+ const result = await executeOutputStyleCommand(host, '');
71
+
72
+ expect(ask).toHaveBeenCalledTimes(1);
73
+ expect(result).toMatchObject({
74
+ success: true,
75
+ hostActions: [{ type: 'output-style-change', styleId: 'concise' }],
76
+ data: { outputStyle: 'concise' },
77
+ });
78
+ });
79
+
80
+ it('rejects unknown ids and never emits an action', async () => {
81
+ const { host } = context();
82
+ const result = await executeOutputStyleCommand(host, 'missing');
83
+
84
+ expect(result.success).toBe(false);
85
+ expect(result.message).toContain('Unknown output style');
86
+ expect(result.hostActions).toBeUndefined();
87
+ });
88
+
89
+ it('is operator-only', () => {
90
+ expect(createOutputStyleCommandEntry()).toMatchObject({
91
+ name: 'output-style',
92
+ modelInvocable: false,
93
+ userInvocable: true,
94
+ });
95
+ });
96
+ });
@@ -0,0 +1,6 @@
1
+ export {
2
+ OutputStyleCommandSource,
3
+ createOutputStyleCommandEntry,
4
+ createOutputStyleCommandModule,
5
+ executeOutputStyleCommand,
6
+ } from './output-style-command-module.js';
@@ -0,0 +1,143 @@
1
+ import { selectAction } from '@robota-sdk/agent-core';
2
+
3
+ import type {
4
+ ICommandHostAdapterAccess,
5
+ ICommandHostSessionAccess,
6
+ ICommandHostUserInteraction,
7
+ ICommandModule,
8
+ ISystemCommand,
9
+ ICommandOutputStyleRegistryAdapter,
10
+ } from '@robota-sdk/agent-framework';
11
+ import type { ICommand, ICommandResult, ICommandSource } from '@robota-sdk/agent-interface-command';
12
+
13
+ const OUTPUT_STYLE_DESCRIPTION = 'List or switch the active provider-neutral response style';
14
+ const OUTPUT_STYLE_ARGUMENT_HINT = 'list | <style-id>';
15
+
16
+ function getRegistry(
17
+ context: ICommandHostAdapterAccess,
18
+ ): ICommandOutputStyleRegistryAdapter | undefined {
19
+ return context.getCommandHostAdapters?.().outputStyleRegistry;
20
+ }
21
+
22
+ function formatStyleList(active: string, registry: ICommandOutputStyleRegistryAdapter): string {
23
+ const lines = registry.listOutputStyles().map((style) => {
24
+ const marker = style.id === active ? '* ' : ' ';
25
+ const cost = style.tokenCost ? ` [input cost: ${style.tokenCost}]` : '';
26
+ return `${marker}${style.id} — ${style.name}: ${style.description}${cost}`;
27
+ });
28
+ return ['Available output styles:', ...lines].join('\n');
29
+ }
30
+
31
+ function listResult(
32
+ context: ICommandHostSessionAccess,
33
+ registry: ICommandOutputStyleRegistryAdapter,
34
+ ): ICommandResult {
35
+ const active = context.getActiveOutputStyleId();
36
+ const outputStyles = registry.listOutputStyles();
37
+ return {
38
+ success: true,
39
+ message: formatStyleList(active, registry),
40
+ data: { outputStyles, active },
41
+ };
42
+ }
43
+
44
+ async function chooseStyle(
45
+ context: ICommandHostUserInteraction,
46
+ registry: ICommandOutputStyleRegistryAdapter,
47
+ ): Promise<string | undefined> {
48
+ const interaction = context.getUserInteraction();
49
+ if (!interaction) return undefined;
50
+ const options = registry.listOutputStyles().map((style) => ({
51
+ value: style.id,
52
+ label: style.id,
53
+ description: style.description,
54
+ }));
55
+ const response = await interaction.ask(
56
+ selectAction('output-style', 'Select an output style', options),
57
+ );
58
+ return response.type === 'answer' ? response.values[0] : undefined;
59
+ }
60
+
61
+ export async function executeOutputStyleCommand(
62
+ context: ICommandHostAdapterAccess & ICommandHostSessionAccess & ICommandHostUserInteraction,
63
+ args: string,
64
+ ): Promise<ICommandResult> {
65
+ const registry = getRegistry(context);
66
+ if (!registry) {
67
+ return {
68
+ success: false,
69
+ message: 'Output styles are not available in this environment.',
70
+ };
71
+ }
72
+
73
+ let styleId = args.trim();
74
+ if (styleId === 'list') return listResult(context, registry);
75
+ if (!styleId) {
76
+ styleId = (await chooseStyle(context, registry)) ?? '';
77
+ if (!styleId) return listResult(context, registry);
78
+ }
79
+
80
+ const style = registry.getOutputStyle(styleId);
81
+ if (!style) {
82
+ return {
83
+ success: false,
84
+ message: `Unknown output style "${styleId}". Available: ${
85
+ registry
86
+ .listOutputStyles()
87
+ .map((entry) => entry.id)
88
+ .join(', ') || '(none)'
89
+ }`,
90
+ };
91
+ }
92
+
93
+ return {
94
+ success: true,
95
+ message: `Switching output style to ${style.name}...`,
96
+ data: { outputStyle: style.id },
97
+ hostActions: [{ type: 'output-style-change', styleId: style.id }],
98
+ };
99
+ }
100
+
101
+ export function createOutputStyleCommandEntry(): ICommand {
102
+ return {
103
+ name: 'output-style',
104
+ displayName: 'Output Style',
105
+ description: OUTPUT_STYLE_DESCRIPTION,
106
+ argumentHint: OUTPUT_STYLE_ARGUMENT_HINT,
107
+ source: 'output-style',
108
+ userInvocable: true,
109
+ // User-only: the user's presentation preference.
110
+ modelInvocable: false,
111
+ };
112
+ }
113
+
114
+ function createOutputStyleSystemCommand(): ISystemCommand {
115
+ const entry = createOutputStyleCommandEntry();
116
+ return {
117
+ name: entry.name,
118
+ displayName: entry.displayName,
119
+ description: entry.description,
120
+ argumentHint: entry.argumentHint,
121
+ requiresPermission: false,
122
+ userInvocable: true,
123
+ modelInvocable: false,
124
+ lifecycle: 'inline',
125
+ execute: executeOutputStyleCommand,
126
+ };
127
+ }
128
+
129
+ export class OutputStyleCommandSource implements ICommandSource {
130
+ readonly name = 'output-style';
131
+
132
+ getCommands(): ICommand[] {
133
+ return [createOutputStyleCommandEntry()];
134
+ }
135
+ }
136
+
137
+ export function createOutputStyleCommandModule(): ICommandModule {
138
+ return {
139
+ name: 'agent-command-output-style',
140
+ commandSources: [new OutputStyleCommandSource()],
141
+ systemCommands: [createOutputStyleSystemCommand()],
142
+ };
143
+ }