@robota-sdk/agent-command 3.0.0-beta.78 → 3.0.0-beta.81

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 (200) hide show
  1. package/README.md +11 -2
  2. package/dist/node/index.cjs +76 -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 +76 -38
  8. package/dist/node/index.js.map +1 -1
  9. package/package.json +34 -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.test.ts +218 -0
  87. package/src/handoff/handoff-command-module.ts +48 -0
  88. package/src/handoff/handoff-command.ts +187 -0
  89. package/src/handoff/index.ts +6 -0
  90. package/src/help/__tests__/help-command-module.test.ts +63 -40
  91. package/src/help/__tests__/help-command.test.ts +46 -37
  92. package/src/help/help-command-module.ts +2 -1
  93. package/src/help/help-command.ts +3 -3
  94. package/src/index.ts +124 -2
  95. package/src/keybindings/__tests__/keybindings-command-module.test.ts +57 -0
  96. package/src/keybindings/index.ts +6 -0
  97. package/src/keybindings/keybindings-command-module.ts +74 -0
  98. package/src/language/__tests__/language-command-module.test.ts +33 -28
  99. package/src/language/language-command-module.ts +2 -1
  100. package/src/language/language-command.ts +8 -6
  101. package/src/mcp-activation/__tests__/mcp-activation-command.test.ts +630 -0
  102. package/src/mcp-activation/__tests__/mcp-model-view.test.ts +211 -0
  103. package/src/mcp-activation/index.ts +12 -0
  104. package/src/mcp-activation/mcp-activation-command-module.ts +92 -0
  105. package/src/mcp-activation/mcp-activation-command.ts +448 -0
  106. package/src/mcp-activation/mcp-model-notice.ts +85 -0
  107. package/src/mcp-activation/mcp-model-status.ts +99 -0
  108. package/src/memory/__tests__/memory-command-module.test.ts +167 -74
  109. package/src/memory/memory-command-module.ts +18 -1
  110. package/src/memory/memory-command.ts +38 -38
  111. package/src/mode/__tests__/mode-command-module.test.ts +51 -38
  112. package/src/mode/mode-command-module.ts +2 -1
  113. package/src/mode/mode-command.ts +15 -7
  114. package/src/output-style/__tests__/output-style-command.test.ts +96 -0
  115. package/src/output-style/index.ts +6 -0
  116. package/src/output-style/output-style-command-module.ts +143 -0
  117. package/src/peers/__tests__/peers-command.test.ts +241 -0
  118. package/src/peers/index.ts +9 -0
  119. package/src/peers/peers-command-module.ts +47 -0
  120. package/src/peers/peers-command.ts +142 -0
  121. package/src/permissions/__tests__/permissions-command-module.test.ts +149 -37
  122. package/src/permissions/permissions-command-module.ts +2 -1
  123. package/src/permissions/permissions-command.ts +46 -4
  124. package/src/plan/__tests__/plan-command.test.ts +129 -0
  125. package/src/plan/index.ts +9 -0
  126. package/src/plan/plan-command-module.ts +49 -0
  127. package/src/plan/plan-command.ts +92 -0
  128. package/src/plugin/__tests__/plugin-command-module.test.ts +46 -40
  129. package/src/plugin/__tests__/plugin-host-boundary.test.ts +11 -0
  130. package/src/plugin/plugin-command-module.ts +3 -1
  131. package/src/plugin/plugin-command.ts +17 -16
  132. package/src/preset/__tests__/host-supplied-preset-registry.test.ts +98 -0
  133. package/src/preset/__tests__/preset-command-module.test.ts +102 -56
  134. package/src/preset/preset-command-module.ts +26 -8
  135. package/src/preset/preset-command.ts +85 -27
  136. package/src/provider/__tests__/org-policy.test.ts +4 -5
  137. package/src/provider/__tests__/provider-command-module.test.ts +11 -14
  138. package/src/provider/__tests__/provider-startup-host-settings.test.ts +16 -0
  139. package/src/provider/__tests__/scripted-interaction.ts +7 -5
  140. package/src/provider/provider-command-execution.ts +4 -4
  141. package/src/provider/provider-command-module.ts +2 -1
  142. package/src/provider/provider-command-profile-lifecycle.ts +4 -4
  143. package/src/provider/provider-command-profile-operations.ts +3 -3
  144. package/src/provider/provider-command-profile.ts +1 -1
  145. package/src/provider/provider-command-setup.ts +4 -4
  146. package/src/provider/provider-startup.ts +62 -35
  147. package/src/remote-control/__tests__/remote-control-command-module.test.ts +36 -0
  148. package/src/remote-control/__tests__/remote-control-command.test.ts +216 -0
  149. package/src/remote-control/index.ts +8 -0
  150. package/src/remote-control/remote-control-command-module.ts +61 -0
  151. package/src/remote-control/remote-control-command.ts +138 -0
  152. package/src/reset/__tests__/reset-command-module.test.ts +5 -5
  153. package/src/reset/reset-command-module.ts +2 -1
  154. package/src/reset/reset-command.ts +3 -4
  155. package/src/rewind/__tests__/rewind-command-module.test.ts +40 -0
  156. package/src/rewind/rewind-command-module.ts +2 -1
  157. package/src/rewind/rewind-command.ts +73 -6
  158. package/src/sandbox/__tests__/sandbox-command.test.ts +80 -0
  159. package/src/sandbox/index.ts +2 -0
  160. package/src/sandbox/sandbox-command-module.ts +56 -0
  161. package/src/sandbox/sandbox-command.ts +94 -0
  162. package/src/schedule/__tests__/loop-command-functional.test.ts +386 -0
  163. package/src/schedule/__tests__/loop-command.test.ts +360 -0
  164. package/src/schedule/__tests__/loop-jitter.test.ts +25 -0
  165. package/src/schedule/__tests__/monitor-model-permission-functional.test.ts +126 -0
  166. package/src/schedule/__tests__/schedule-command.test.ts +143 -3
  167. package/src/schedule/__tests__/schedule-redos.test.ts +81 -0
  168. package/src/schedule/index.ts +3 -0
  169. package/src/schedule/loop-command.ts +333 -0
  170. package/src/schedule/loop-jitter.ts +41 -0
  171. package/src/schedule/schedule-command-module.ts +92 -14
  172. package/src/schedule/schedule-command.ts +98 -9
  173. package/src/schedule/schedule-spec-parser.ts +5 -1
  174. package/src/session/__tests__/rename-host-persistence.test.ts +72 -0
  175. package/src/session/__tests__/session-command-module.test.ts +210 -45
  176. package/src/session/session-command-module.ts +68 -37
  177. package/src/session/session-command.ts +147 -80
  178. package/src/settings/settings-command-module.ts +3 -2
  179. package/src/shell/__tests__/resolve-shell-host.test.ts +11 -0
  180. package/src/shell/__tests__/shell-command-functional.test.ts +23 -1
  181. package/src/shell/resolve-shell.ts +2 -2
  182. package/src/shell/shell-command-module.ts +6 -5
  183. package/src/shell/shell-command.ts +9 -5
  184. package/src/skills/__tests__/skills-command-module.test.ts +69 -28
  185. package/src/skills/index.ts +1 -0
  186. package/src/skills/skills-command-module.ts +22 -5
  187. package/src/skills/skills-command.ts +14 -13
  188. package/src/statusline/__tests__/statusline-command-module.test.ts +5 -4
  189. package/src/statusline/statusline-command-module.ts +2 -1
  190. package/src/statusline/statusline-command.ts +4 -4
  191. package/src/theme/__tests__/theme-command.test.ts +204 -0
  192. package/src/theme/index.ts +12 -0
  193. package/src/theme/theme-command-module.ts +211 -0
  194. package/src/user-local/__tests__/user-local-command.test.ts +145 -110
  195. package/src/user-local/user-local-command-constants.ts +1 -1
  196. package/src/user-local/user-local-command-module.ts +6 -5
  197. package/src/user-local/user-local-command.ts +12 -6
  198. package/src/user-local/user-local-memory-command.ts +17 -7
  199. package/src/plugins/default-plugin-command-adapter.ts +0 -164
  200. package/src/plugins/default-plugin-command-source-loader.ts +0 -31
@@ -0,0 +1,211 @@
1
+ import type {
2
+ ICommandModule,
3
+ ISystemCommand,
4
+ TAppearanceSettingsPatch,
5
+ } from '@robota-sdk/agent-framework';
6
+ import type {
7
+ ICommand,
8
+ ICommandResult,
9
+ ICommandSource,
10
+ IThemeAppearanceState,
11
+ IThemeCatalogueEntry,
12
+ IThemeCataloguePort,
13
+ } from '@robota-sdk/agent-interface-command';
14
+
15
+ const THEME_DESCRIPTION =
16
+ 'List, preview or switch the terminal colour theme, and toggle syntax highlighting and motion';
17
+ const THEME_ARGUMENT_HINT = 'list | <theme-id> | syntax on|off | motion on|off';
18
+
19
+ const THEME_UNAVAILABLE = 'Themes are not available in this environment.';
20
+
21
+ function formatToggle(value: boolean): string {
22
+ return value ? 'on' : 'off';
23
+ }
24
+
25
+ function formatCatalogue(catalogue: IThemeCataloguePort): string {
26
+ const { settings, reducedMotionPin } = catalogue.getAppearance();
27
+ const rows = catalogue.listThemes().map((theme) => {
28
+ const marker = theme.id === settings.theme ? '* ' : ' ';
29
+ return `${marker}${theme.id} — ${theme.name} (${theme.appearance}, ${theme.source})`;
30
+ });
31
+ // Two facts, never one: what this run does, and what is saved. Reporting the SAVED value beside
32
+ // the tier reads as a contradiction on a `--no-reduced-motion` run over a `true` setting, and as
33
+ // an outright wrong answer to "is motion reduced right now".
34
+ const motion = reducedMotionPin
35
+ ? `${formatToggle(reducedMotionPin.reducedMotion)} for this run (pinned by ${reducedMotionPin.tier}; saved ${formatToggle(settings.reducedMotion)})`
36
+ : formatToggle(settings.reducedMotion);
37
+ return [
38
+ 'Available themes:',
39
+ ...rows,
40
+ '',
41
+ `syntax highlighting: ${formatToggle(settings.syntaxHighlighting)}`,
42
+ `reduced motion: ${motion}`,
43
+ ].join('\n');
44
+ }
45
+
46
+ function listResult(catalogue: IThemeCataloguePort): ICommandResult {
47
+ return {
48
+ success: true,
49
+ message: formatCatalogue(catalogue),
50
+ data: {
51
+ themes: catalogue.listThemes().map((theme) => ({ ...theme })),
52
+ appearance: { ...catalogue.getAppearance().settings },
53
+ },
54
+ };
55
+ }
56
+
57
+ function unknownThemeResult(id: string, catalogue: IThemeCataloguePort): ICommandResult {
58
+ const known = catalogue
59
+ .listThemes()
60
+ .map((theme) => theme.id)
61
+ .join(', ');
62
+ return {
63
+ success: false,
64
+ message: `Unknown theme "${id}". Available: ${known || '(none)'}`,
65
+ };
66
+ }
67
+
68
+ /** `on` / `off`, and nothing else — a typo must not read as its opposite. */
69
+ function parseToggle(word: string | undefined): boolean | undefined {
70
+ if (word === 'on') return true;
71
+ if (word === 'off') return false;
72
+ return undefined;
73
+ }
74
+
75
+ interface IParsedThemeArgs {
76
+ readonly patch?: TAppearanceSettingsPatch;
77
+ readonly themeId?: string;
78
+ readonly error?: string;
79
+ }
80
+
81
+ /**
82
+ * `/theme <id> [syntax on|off] [motion on|off]`, or a bare `syntax`/`motion` toggle. The theme id is
83
+ * NOT resolved here — parsing answers what was asked for, and the caller decides whether the
84
+ * catalogue holds it, so an unknown id is reported with the catalogue rather than as a parse error.
85
+ */
86
+ function parseThemeArgs(tokens: readonly string[]): IParsedThemeArgs {
87
+ const patch: TAppearanceSettingsPatch = {};
88
+ let index = 0;
89
+ let themeId: string | undefined;
90
+ if (tokens[0] !== 'syntax' && tokens[0] !== 'motion') {
91
+ themeId = tokens[0];
92
+ index = 1;
93
+ }
94
+ while (index < tokens.length) {
95
+ const key = tokens[index];
96
+ const value = parseToggle(tokens[index + 1]);
97
+ if ((key !== 'syntax' && key !== 'motion') || value === undefined) {
98
+ return { error: `Usage: /theme ${THEME_ARGUMENT_HINT}` };
99
+ }
100
+ if (key === 'syntax') patch.syntaxHighlighting = value;
101
+ else patch.reducedMotion = !value;
102
+ index += 2;
103
+ }
104
+ return { patch, ...(themeId === undefined ? {} : { themeId }) };
105
+ }
106
+
107
+ /**
108
+ * `motion on` means "animate", which is `reducedMotion: false`. The inversion lives in one place
109
+ * precisely because reading it backwards is the easy mistake — a user asking for motion and getting
110
+ * stillness would look like the command doing nothing.
111
+ */
112
+ function describeChange(
113
+ patch: TAppearanceSettingsPatch,
114
+ theme: IThemeCatalogueEntry | undefined,
115
+ state: IThemeAppearanceState,
116
+ ): string {
117
+ const parts: string[] = [];
118
+ if (theme) parts.push(`theme ${theme.name}`);
119
+ if (patch.syntaxHighlighting !== undefined) {
120
+ parts.push(`syntax highlighting ${formatToggle(patch.syntaxHighlighting)}`);
121
+ }
122
+ if (patch.reducedMotion !== undefined) {
123
+ parts.push(`motion ${formatToggle(!patch.reducedMotion)}`);
124
+ }
125
+ const applied = parts.length > 0 ? `Applied: ${parts.join(', ')}.` : 'Nothing to change.';
126
+ // The pin is reported, never silently obeyed or silently overridden: the setting IS persisted and
127
+ // takes effect next run, while this run keeps what pinned it.
128
+ if (patch.reducedMotion !== undefined && state.reducedMotionPin) {
129
+ return `${applied} Saved, but this run keeps reduced motion pinned by ${state.reducedMotionPin.tier}.`;
130
+ }
131
+ return applied;
132
+ }
133
+
134
+ export function executeThemeCommand(
135
+ catalogue: IThemeCataloguePort | undefined,
136
+ args: string,
137
+ ): ICommandResult {
138
+ if (!catalogue) return { success: false, message: THEME_UNAVAILABLE };
139
+
140
+ const tokens = args.trim().split(/\s+/u).filter(Boolean);
141
+ if (tokens.length === 0) {
142
+ return {
143
+ success: true,
144
+ message: 'Opening the theme picker...',
145
+ uiIntents: [{ type: 'show-theme-picker' }],
146
+ };
147
+ }
148
+ if (tokens.length === 1 && tokens[0] === 'list') return listResult(catalogue);
149
+
150
+ const parsed = parseThemeArgs(tokens);
151
+ if (parsed.error !== undefined || !parsed.patch) {
152
+ return { success: false, message: parsed.error ?? `Usage: /theme ${THEME_ARGUMENT_HINT}` };
153
+ }
154
+
155
+ const patch = { ...parsed.patch };
156
+ let theme: IThemeCatalogueEntry | undefined;
157
+ if (parsed.themeId !== undefined) {
158
+ theme = catalogue.getTheme(parsed.themeId);
159
+ // An unknown id writes NOTHING — not the theme, and not the toggles submitted beside it.
160
+ if (!theme) return unknownThemeResult(parsed.themeId, catalogue);
161
+ patch.theme = theme.id;
162
+ }
163
+
164
+ return {
165
+ success: true,
166
+ message: describeChange(patch, theme, catalogue.getAppearance()),
167
+ data: { appearance: { ...patch } },
168
+ hostActions: [{ type: 'appearance-settings-patch', patch }],
169
+ };
170
+ }
171
+
172
+ export function createThemeCommandEntry(): ICommand {
173
+ return {
174
+ name: 'theme',
175
+ displayName: 'Theme',
176
+ description: THEME_DESCRIPTION,
177
+ argumentHint: THEME_ARGUMENT_HINT,
178
+ source: 'theme',
179
+ userInvocable: true,
180
+ // User-only: UI preference.
181
+ modelInvocable: false,
182
+ };
183
+ }
184
+
185
+ export class ThemeCommandSource implements ICommandSource {
186
+ readonly name = 'theme';
187
+
188
+ getCommands(): ICommand[] {
189
+ return [createThemeCommandEntry()];
190
+ }
191
+ }
192
+
193
+ export function createThemeCommandModule(catalogue: IThemeCataloguePort): ICommandModule {
194
+ const entry = createThemeCommandEntry();
195
+ const command: ISystemCommand = {
196
+ name: entry.name,
197
+ displayName: entry.displayName,
198
+ description: entry.description,
199
+ argumentHint: entry.argumentHint,
200
+ requiresPermission: false,
201
+ userInvocable: true,
202
+ modelInvocable: false,
203
+ lifecycle: 'inline',
204
+ execute: (_context, args) => executeThemeCommand(catalogue, args),
205
+ };
206
+ return {
207
+ name: 'agent-command-theme',
208
+ commandSources: [new ThemeCommandSource()],
209
+ systemCommands: [command],
210
+ };
211
+ }
@@ -1,8 +1,9 @@
1
1
  import { promises as fs } from 'node:fs';
2
2
  import { tmpdir } from 'node:os';
3
3
  import path from 'node:path';
4
- import { afterEach, describe, expect, it } from 'vitest';
5
- import { executeUserLocalDirectCommand } from '../user-local-command.js';
4
+ import { afterEach, describe, expect, it, vi } from 'vitest';
5
+ import { executeUserLocalCommand, executeUserLocalDirectCommand } from '../user-local-command.js';
6
+ import { createUserLocalCommandModule } from '../user-local-command-module.js';
6
7
 
7
8
  const tempRoots: string[] = [];
8
9
 
@@ -13,50 +14,86 @@ async function createTempRoot(name: string): Promise<string> {
13
14
  }
14
15
 
15
16
  afterEach(async () => {
17
+ vi.unstubAllEnvs();
16
18
  await Promise.all(
17
19
  tempRoots.splice(0).map((root) => fs.rm(root, { recursive: true, force: true })),
18
20
  );
19
21
  });
20
22
 
21
23
  describe('user-local command', () => {
24
+ it('does not advertise a specific product from the neutral command entry', () => {
25
+ const module = createUserLocalCommandModule('/tmp/selected-storage');
26
+ expect(module.commandSources?.[0]?.getCommands()[0]?.description).not.toMatch(/Robota/i);
27
+ });
28
+
29
+ it('fails closed when a direct caller omits the storage root', async () => {
30
+ const workspace = await createTempRoot('robota-user-local-missing-root-');
31
+ const repo = path.join(workspace, 'repo');
32
+ await fs.mkdir(repo);
33
+
34
+ const result = await executeUserLocalDirectCommand({
35
+ cwd: repo,
36
+ argv: ['storage', 'list'],
37
+ } as never);
38
+ expect(result).toMatchObject({ success: false });
39
+ expect(result.message).toContain('userLocalStorageRoot is required');
40
+ });
41
+
42
+ it('uses the explicit root for slash commands', async () => {
43
+ const workspace = await createTempRoot('robota-user-local-slash-root-');
44
+ const repo = path.join(workspace, 'repo');
45
+ const selectedRoot = path.join(workspace, 'selected', '.robota');
46
+ await fs.mkdir(repo);
47
+ vi.stubEnv('HOME', path.join(workspace, 'ambient-home'));
48
+
49
+ const directResult = await executeUserLocalCommand(
50
+ { getCwd: () => repo } as never,
51
+ 'storage list --format=json',
52
+ selectedRoot,
53
+ );
54
+ expect(directResult.success).toBe(true);
55
+ expect(JSON.parse(directResult.message)).toMatchObject({ root: selectedRoot });
56
+
57
+ const slashCommand = createUserLocalCommandModule(selectedRoot).systemCommands?.[0];
58
+ expect(slashCommand).toBeDefined();
59
+ const result = await slashCommand!.execute(
60
+ { getCwd: () => repo } as never,
61
+ 'storage list --format=json',
62
+ );
63
+ expect(result.success).toBe(true);
64
+ expect(JSON.parse(result.message)).toMatchObject({ root: selectedRoot });
65
+ });
66
+
22
67
  it('prints storage inspection JSON without provider configuration', async () => {
23
68
  const workspace = await createTempRoot('robota-user-local-command-');
24
69
  const repo = path.join(workspace, 'repo');
25
70
  const home = path.join(workspace, 'home');
26
71
  await fs.mkdir(repo);
27
72
  await fs.mkdir(home);
28
- const originalHome = process.env.HOME;
29
- process.env.HOME = home;
30
-
31
- try {
32
- const result = await executeUserLocalDirectCommand({
33
- cwd: repo,
34
- argv: ['storage', 'list'],
35
- format: 'json',
36
- });
37
- const parsed = JSON.parse(result.message) as {
38
- root: string;
39
- categories: readonly { category: string; mayExecuteCommands: boolean }[];
40
- };
41
-
42
- expect(result.success).toBe(true);
43
- expect(parsed.root).toBe(path.join(home, '.robota'));
44
- expect(parsed.categories.map((item) => item.category)).toEqual([
45
- 'preferences',
46
- 'view-state',
47
- 'memory-projections',
48
- 'task-associations',
49
- 'workflow-metadata',
50
- 'inspection-index',
51
- ]);
52
- expect(parsed.categories.every((item) => item.mayExecuteCommands === false)).toBe(true);
53
- } finally {
54
- if (originalHome === undefined) {
55
- delete process.env.HOME;
56
- } else {
57
- process.env.HOME = originalHome;
58
- }
59
- }
73
+ vi.stubEnv('HOME', path.join(workspace, 'ambient-home'));
74
+
75
+ const result = await executeUserLocalDirectCommand({
76
+ cwd: repo,
77
+ storageRoot: path.join(home, '.robota'),
78
+ argv: ['storage', 'list'],
79
+ format: 'json',
80
+ });
81
+ const parsed = JSON.parse(result.message) as {
82
+ root: string;
83
+ categories: readonly { category: string; mayExecuteCommands: boolean }[];
84
+ };
85
+
86
+ expect(result.success).toBe(true);
87
+ expect(parsed.root).toBe(path.join(home, '.robota'));
88
+ expect(parsed.categories.map((item) => item.category)).toEqual([
89
+ 'preferences',
90
+ 'view-state',
91
+ 'memory-projections',
92
+ 'task-associations',
93
+ 'workflow-metadata',
94
+ 'inspection-index',
95
+ ]);
96
+ expect(parsed.categories.every((item) => item.mayExecuteCommands === false)).toBe(true);
60
97
  });
61
98
 
62
99
  it('stores, lists, inspects, disables, and deletes user-local memory items', async () => {
@@ -65,81 +102,79 @@ describe('user-local command', () => {
65
102
  const home = path.join(workspace, 'home');
66
103
  await fs.mkdir(repo);
67
104
  await fs.mkdir(home);
68
- const originalHome = process.env.HOME;
69
- process.env.HOME = home;
70
-
71
- try {
72
- const setResult = await executeUserLocalDirectCommand({
73
- cwd: repo,
74
- argv: ['memory', 'set', 'view-preference', 'last-panel', 'background'],
75
- summary: 'Open the background panel',
76
- source: 'user-input',
77
- });
78
- expect(setResult.success).toBe(true);
79
- expect(setResult.message).toContain('view-preference/last-panel');
80
-
81
- const listResult = await executeUserLocalDirectCommand({
82
- cwd: repo,
83
- argv: ['memory', 'list'],
84
- format: 'json',
85
- });
86
- const list = JSON.parse(listResult.message) as {
87
- items: readonly { category: string; key: string; enabled: boolean }[];
88
- };
89
- expect(listResult.success).toBe(true);
90
- expect(list.items).toHaveLength(1);
91
- expect(list.items[0]).toMatchObject({
92
- category: 'view-preference',
93
- key: 'last-panel',
94
- enabled: true,
95
- });
96
-
97
- const inspectResult = await executeUserLocalDirectCommand({
98
- cwd: repo,
99
- argv: ['memory', 'inspect', 'view-preference', 'last-panel'],
100
- format: 'json',
101
- });
102
- const inspected = JSON.parse(inspectResult.message) as {
103
- commandExecutionEffect: string;
104
- displayNavigationRule: string;
105
- };
106
- expect(inspectResult.success).toBe(true);
107
- expect(inspected.commandExecutionEffect).toBe('none');
108
- expect(inspected.displayNavigationRule).toContain('display/navigation only');
109
-
110
- const disableResult = await executeUserLocalDirectCommand({
111
- cwd: repo,
112
- argv: ['memory', 'disable', 'view-preference', 'last-panel'],
113
- });
114
- expect(disableResult.success).toBe(true);
115
-
116
- const disabledInspectResult = await executeUserLocalDirectCommand({
117
- cwd: repo,
118
- argv: ['memory', 'inspect', 'view-preference', 'last-panel'],
119
- format: 'json',
120
- });
121
- const disabled = JSON.parse(disabledInspectResult.message) as { enabled: boolean };
122
- expect(disabled.enabled).toBe(false);
123
-
124
- const deleteResult = await executeUserLocalDirectCommand({
125
- cwd: repo,
126
- argv: ['memory', 'delete', 'view-preference', 'last-panel'],
127
- });
128
- expect(deleteResult.success).toBe(true);
129
-
130
- const missingResult = await executeUserLocalDirectCommand({
131
- cwd: repo,
132
- argv: ['memory', 'inspect', 'view-preference', 'last-panel'],
133
- format: 'json',
134
- });
135
- expect(missingResult.success).toBe(false);
136
- expect(missingResult.message).toBe('User-local memory item not found.');
137
- } finally {
138
- if (originalHome === undefined) {
139
- delete process.env.HOME;
140
- } else {
141
- process.env.HOME = originalHome;
142
- }
143
- }
105
+ vi.stubEnv('HOME', path.join(workspace, 'ambient-home'));
106
+
107
+ const setResult = await executeUserLocalDirectCommand({
108
+ cwd: repo,
109
+ storageRoot: path.join(home, '.robota'),
110
+ argv: ['memory', 'set', 'view-preference', 'last-panel', 'background'],
111
+ summary: 'Open the background panel',
112
+ source: 'user-input',
113
+ });
114
+ expect(setResult.success).toBe(true);
115
+ expect(setResult.message).toContain('view-preference/last-panel');
116
+
117
+ const listResult = await executeUserLocalDirectCommand({
118
+ cwd: repo,
119
+ storageRoot: path.join(home, '.robota'),
120
+ argv: ['memory', 'list'],
121
+ format: 'json',
122
+ });
123
+ const list = JSON.parse(listResult.message) as {
124
+ items: readonly { category: string; key: string; enabled: boolean }[];
125
+ };
126
+ expect(listResult.success).toBe(true);
127
+ expect(list.items).toHaveLength(1);
128
+ expect(list.items[0]).toMatchObject({
129
+ category: 'view-preference',
130
+ key: 'last-panel',
131
+ enabled: true,
132
+ });
133
+
134
+ const inspectResult = await executeUserLocalDirectCommand({
135
+ cwd: repo,
136
+ storageRoot: path.join(home, '.robota'),
137
+ argv: ['memory', 'inspect', 'view-preference', 'last-panel'],
138
+ format: 'json',
139
+ });
140
+ const inspected = JSON.parse(inspectResult.message) as {
141
+ commandExecutionEffect: string;
142
+ displayNavigationRule: string;
143
+ };
144
+ expect(inspectResult.success).toBe(true);
145
+ expect(inspected.commandExecutionEffect).toBe('none');
146
+ expect(inspected.displayNavigationRule).toContain('display/navigation only');
147
+
148
+ const disableResult = await executeUserLocalDirectCommand({
149
+ cwd: repo,
150
+ storageRoot: path.join(home, '.robota'),
151
+ argv: ['memory', 'disable', 'view-preference', 'last-panel'],
152
+ });
153
+ expect(disableResult.success).toBe(true);
154
+
155
+ const disabledInspectResult = await executeUserLocalDirectCommand({
156
+ cwd: repo,
157
+ storageRoot: path.join(home, '.robota'),
158
+ argv: ['memory', 'inspect', 'view-preference', 'last-panel'],
159
+ format: 'json',
160
+ });
161
+ const disabled = JSON.parse(disabledInspectResult.message) as { enabled: boolean };
162
+ expect(disabled.enabled).toBe(false);
163
+
164
+ const deleteResult = await executeUserLocalDirectCommand({
165
+ cwd: repo,
166
+ storageRoot: path.join(home, '.robota'),
167
+ argv: ['memory', 'delete', 'view-preference', 'last-panel'],
168
+ });
169
+ expect(deleteResult.success).toBe(true);
170
+
171
+ const missingResult = await executeUserLocalDirectCommand({
172
+ cwd: repo,
173
+ storageRoot: path.join(home, '.robota'),
174
+ argv: ['memory', 'inspect', 'view-preference', 'last-panel'],
175
+ format: 'json',
176
+ });
177
+ expect(missingResult.success).toBe(false);
178
+ expect(missingResult.message).toBe('User-local memory item not found.');
144
179
  });
145
180
  });
@@ -1,4 +1,4 @@
1
- export const USER_LOCAL_COMMAND_DESCRIPTION = 'Inspect Robota user-local storage and memory state.';
1
+ export const USER_LOCAL_COMMAND_DESCRIPTION = 'Inspect user-local storage and memory state.';
2
2
  export const USER_LOCAL_COMMAND_ARGUMENT_HINT =
3
3
  'storage list [--format json] | memory set/list/inspect/disable/delete';
4
4
  export const USER_LOCAL_COMMAND_USAGE =
@@ -5,7 +5,7 @@ import {
5
5
  } from './user-local-command.js';
6
6
 
7
7
  import type { ICommandModule, ISystemCommand } from '@robota-sdk/agent-framework';
8
- import type { ICommand, ICommandSource } from '@robota-sdk/agent-interface-transport';
8
+ import type { ICommand, ICommandSource } from '@robota-sdk/agent-interface-command';
9
9
 
10
10
  export function createUserLocalCommandEntry(): ICommand {
11
11
  return {
@@ -14,6 +14,7 @@ export function createUserLocalCommandEntry(): ICommand {
14
14
  description: USER_LOCAL_COMMAND_DESCRIPTION,
15
15
  source: 'user-local',
16
16
  argumentHint: USER_LOCAL_COMMAND_ARGUMENT_HINT,
17
+ // User-only: the user's private cross-project storage.
17
18
  modelInvocable: false,
18
19
  safety: 'read-only',
19
20
  subcommands: [
@@ -31,7 +32,7 @@ export function createUserLocalCommandEntry(): ICommand {
31
32
  };
32
33
  }
33
34
 
34
- function createUserLocalSystemCommand(): ISystemCommand {
35
+ function createUserLocalSystemCommand(storageRoot: string): ISystemCommand {
35
36
  const entry = createUserLocalCommandEntry();
36
37
  return {
37
38
  name: entry.name,
@@ -43,7 +44,7 @@ function createUserLocalSystemCommand(): ISystemCommand {
43
44
  argumentHint: entry.argumentHint,
44
45
  safety: entry.safety,
45
46
  subcommands: entry.subcommands,
46
- execute: executeUserLocalCommand,
47
+ execute: (context, rawArgs) => executeUserLocalCommand(context, rawArgs, storageRoot),
47
48
  };
48
49
  }
49
50
 
@@ -55,10 +56,10 @@ export class UserLocalCommandSource implements ICommandSource {
55
56
  }
56
57
  }
57
58
 
58
- export function createUserLocalCommandModule(): ICommandModule {
59
+ export function createUserLocalCommandModule(storageRoot: string): ICommandModule {
59
60
  return {
60
61
  name: 'agent-command-user-local',
61
62
  commandSources: [new UserLocalCommandSource()],
62
- systemCommands: [createUserLocalSystemCommand()],
63
+ systemCommands: [createUserLocalSystemCommand(storageRoot)],
63
64
  };
64
65
  }
@@ -3,8 +3,8 @@ import { inspectUserLocalStorage } from '@robota-sdk/agent-framework';
3
3
  import { USER_LOCAL_COMMAND_USAGE } from './user-local-command-constants.js';
4
4
  import { executeMemoryCommand } from './user-local-memory-command.js';
5
5
 
6
- import type { ICommandHostContext } from '@robota-sdk/agent-framework';
7
- import type { ICommandResult } from '@robota-sdk/agent-interface-transport';
6
+ import type { ICommandHostWorkspace } from '@robota-sdk/agent-framework';
7
+ import type { ICommandResult } from '@robota-sdk/agent-interface-command';
8
8
  export {
9
9
  USER_LOCAL_COMMAND_ARGUMENT_HINT,
10
10
  USER_LOCAL_COMMAND_DESCRIPTION,
@@ -15,6 +15,7 @@ type TUserLocalOutputFormat = 'text' | 'json';
15
15
 
16
16
  export interface IUserLocalDirectCommandOptions {
17
17
  readonly cwd: string;
18
+ readonly storageRoot: string;
18
19
  readonly argv: readonly string[];
19
20
  readonly format?: string;
20
21
  readonly summary?: string;
@@ -123,6 +124,7 @@ function formatStorageInspectionText(
123
124
 
124
125
  async function executeStorageCommand(
125
126
  cwd: string,
127
+ storageRoot: string,
126
128
  parsed: IParsedUserLocalCommand,
127
129
  ): Promise<ICommandResult> {
128
130
  if ((parsed.action ?? 'list') !== 'list') {
@@ -132,7 +134,7 @@ async function executeStorageCommand(
132
134
  };
133
135
  }
134
136
 
135
- const inspection = await inspectUserLocalStorage({ activeRepositoryRoot: cwd });
137
+ const inspection = await inspectUserLocalStorage({ activeRepositoryRoot: cwd, storageRoot });
136
138
  return {
137
139
  message:
138
140
  parsed.format === 'json'
@@ -149,13 +151,14 @@ async function executeStorageCommand(
149
151
 
150
152
  async function executeParsedUserLocalCommand(
151
153
  cwd: string,
154
+ storageRoot: string,
152
155
  parsed: IParsedUserLocalCommand,
153
156
  ): Promise<ICommandResult> {
154
157
  if (parsed.target === 'storage') {
155
- return executeStorageCommand(cwd, parsed);
158
+ return executeStorageCommand(cwd, storageRoot, parsed);
156
159
  }
157
160
  if (parsed.target === 'memory') {
158
- return executeMemoryCommand(cwd, parsed);
161
+ return executeMemoryCommand(cwd, storageRoot, parsed);
159
162
  }
160
163
  return {
161
164
  message: USER_LOCAL_COMMAND_USAGE,
@@ -176,6 +179,7 @@ export async function executeUserLocalDirectCommand(
176
179
  try {
177
180
  return await executeParsedUserLocalCommand(
178
181
  options.cwd,
182
+ options.storageRoot,
179
183
  parseUserLocalArgs(options.argv, {
180
184
  format: options.format,
181
185
  summary: options.summary,
@@ -191,12 +195,14 @@ export async function executeUserLocalDirectCommand(
191
195
  }
192
196
 
193
197
  export async function executeUserLocalCommand(
194
- context: ICommandHostContext,
198
+ context: ICommandHostWorkspace,
195
199
  rawArgs: string,
200
+ storageRoot: string,
196
201
  ): Promise<ICommandResult> {
197
202
  try {
198
203
  return await executeParsedUserLocalCommand(
199
204
  context.getCwd(),
205
+ storageRoot,
200
206
  parseUserLocalArgs(splitRawArgs(rawArgs)),
201
207
  );
202
208
  } catch (error) {