@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.
- package/README.md +11 -2
- package/dist/node/index.cjs +77 -38
- package/dist/node/index.d.cts +946 -0
- package/dist/node/index.d.cts.map +1 -0
- package/dist/node/index.d.ts +564 -51
- package/dist/node/index.d.ts.map +1 -1
- package/dist/node/index.js +77 -38
- package/dist/node/index.js.map +1 -1
- package/package.json +39 -16
- package/src/advisor/advisor-command-module.ts +47 -0
- package/src/advisor/advisor-command.test.ts +102 -0
- package/src/advisor/advisor-command.ts +55 -0
- package/src/advisor/advisor-cost-functional.test.ts +92 -0
- package/src/advisor/index.ts +6 -0
- package/src/agent/__tests__/agent-command.test.ts +39 -8
- package/src/agent/agent-command-module.ts +14 -7
- package/src/agent/agent-command-parser.ts +1 -1
- package/src/agent/agent-command.ts +3 -3
- package/src/background/__tests__/background-command-module.test.ts +59 -52
- package/src/background/background-command-module.ts +2 -1
- package/src/background/background-command.ts +3 -3
- package/src/command-module-utils.ts +33 -0
- package/src/compact/__tests__/compact-command-module.test.ts +31 -23
- package/src/compact/compact-command-module.ts +10 -1
- package/src/compact/compact-command.ts +6 -3
- package/src/context/__tests__/context-command-module.test.ts +214 -63
- package/src/context/auto-compact-format.ts +25 -0
- package/src/context/context-breakdown.ts +295 -0
- package/src/context/context-command-module.ts +45 -14
- package/src/context/context-command.ts +34 -257
- package/src/default/__tests__/__snapshots__/model-exposure.test.ts.snap +110 -0
- package/src/default/__tests__/default-command-modules.test.ts +59 -7
- package/src/default/__tests__/model-exposure.test.ts +241 -0
- package/src/default/__tests__/org-policy-forwarding.test.ts +98 -0
- package/src/default/default-command-modules.ts +130 -18
- package/src/default/index.ts +4 -1
- package/src/devices/__tests__/devices-command-module.test.ts +212 -0
- package/src/devices/devices-command-module.ts +224 -0
- package/src/devices/devices-command-port.ts +82 -0
- package/src/devices/index.ts +11 -0
- package/src/doctor/__tests__/doctor-command-module.test.ts +145 -0
- package/src/doctor/__tests__/doctor-display-vocabulary.test.ts +38 -0
- package/src/doctor/__tests__/doctor-fixture.ts +145 -0
- package/src/doctor/__tests__/doctor-redaction.test.ts +29 -0
- package/src/doctor/__tests__/doctor-runner.test.ts +464 -0
- package/src/doctor/doctor-command-module.ts +132 -0
- package/src/doctor/doctor-extensions-probe.ts +299 -0
- package/src/doctor/doctor-node-deps.ts +89 -0
- package/src/doctor/doctor-redaction.ts +99 -0
- package/src/doctor/doctor-render.ts +70 -0
- package/src/doctor/doctor-repair.ts +136 -0
- package/src/doctor/doctor-runner.ts +122 -0
- package/src/doctor/doctor-settings-probe.ts +239 -0
- package/src/doctor/doctor-storage-probe.ts +187 -0
- package/src/doctor/doctor-types.ts +109 -0
- package/src/doctor/index.ts +32 -0
- package/src/editor/__tests__/editor-command-functional.test.ts +49 -16
- package/src/editor/editor-command-module.ts +6 -5
- package/src/editor/editor-command.ts +10 -5
- package/src/effort/effort-command-module.ts +51 -0
- package/src/effort/effort-command.test.ts +140 -0
- package/src/effort/effort-command.ts +122 -0
- package/src/effort/index.ts +6 -0
- package/src/exit/__tests__/exit-command-module.test.ts +16 -9
- package/src/exit/exit-command-module.ts +2 -1
- package/src/exit/exit-command.ts +6 -6
- package/src/fork/__tests__/fork-command.test.ts +184 -0
- package/src/fork/fork-command-module.ts +61 -0
- package/src/fork/fork-command.ts +162 -0
- package/src/fork/index.ts +8 -0
- package/src/git/__tests__/fake-git-port.ts +28 -0
- package/src/git/__tests__/git-command-module.test.ts +103 -0
- package/src/git/__tests__/git-commit.test.ts +185 -0
- package/src/git/__tests__/git-diff.test.ts +125 -0
- package/src/git/__tests__/git-process.test.ts +221 -0
- package/src/git/__tests__/git-status.test.ts +117 -0
- package/src/git/git-command-module.ts +139 -0
- package/src/git/git-commit.ts +251 -0
- package/src/git/git-diff.ts +129 -0
- package/src/git/git-process.ts +169 -0
- package/src/git/git-status.ts +134 -0
- package/src/git/index.ts +17 -0
- package/src/goal/__tests__/goal-command.test.ts +9 -4
- package/src/goal/goal-command-module.ts +2 -1
- package/src/goal/goal-command.ts +6 -9
- package/src/handoff/__tests__/handoff-command-module.test.ts +24 -0
- package/src/handoff/__tests__/handoff-command.test.ts +218 -0
- package/src/handoff/handoff-command-module.ts +49 -0
- package/src/handoff/handoff-command.ts +187 -0
- package/src/handoff/index.ts +6 -0
- package/src/help/__tests__/help-command-module.test.ts +63 -40
- package/src/help/__tests__/help-command.test.ts +46 -37
- package/src/help/help-command-module.ts +2 -1
- package/src/help/help-command.ts +3 -3
- package/src/index.ts +124 -2
- package/src/keybindings/__tests__/keybindings-command-module.test.ts +57 -0
- package/src/keybindings/index.ts +6 -0
- package/src/keybindings/keybindings-command-module.ts +74 -0
- package/src/language/__tests__/language-command-module.test.ts +33 -28
- package/src/language/language-command-module.ts +2 -1
- package/src/language/language-command.ts +8 -6
- package/src/mcp-activation/__tests__/mcp-activation-command.test.ts +630 -0
- package/src/mcp-activation/__tests__/mcp-model-view.test.ts +211 -0
- package/src/mcp-activation/index.ts +12 -0
- package/src/mcp-activation/mcp-activation-command-module.ts +92 -0
- package/src/mcp-activation/mcp-activation-command.ts +448 -0
- package/src/mcp-activation/mcp-model-notice.ts +85 -0
- package/src/mcp-activation/mcp-model-status.ts +99 -0
- package/src/memory/__tests__/memory-command-module.test.ts +167 -74
- package/src/memory/memory-command-module.ts +18 -1
- package/src/memory/memory-command.ts +38 -38
- package/src/mode/__tests__/mode-command-module.test.ts +51 -38
- package/src/mode/mode-command-module.ts +2 -1
- package/src/mode/mode-command.ts +15 -7
- package/src/output-style/__tests__/output-style-command.test.ts +96 -0
- package/src/output-style/index.ts +6 -0
- package/src/output-style/output-style-command-module.ts +143 -0
- package/src/peers/__tests__/peers-command.test.ts +321 -0
- package/src/peers/index.ts +9 -0
- package/src/peers/peers-command-module.ts +49 -0
- package/src/peers/peers-command.ts +191 -0
- package/src/permissions/__tests__/permissions-command-module.test.ts +149 -37
- package/src/permissions/permissions-command-module.ts +2 -1
- package/src/permissions/permissions-command.ts +46 -4
- package/src/plan/__tests__/plan-command.test.ts +129 -0
- package/src/plan/index.ts +9 -0
- package/src/plan/plan-command-module.ts +49 -0
- package/src/plan/plan-command.ts +92 -0
- package/src/plugin/__tests__/plugin-command-module.test.ts +46 -40
- package/src/plugin/__tests__/plugin-host-boundary.test.ts +11 -0
- package/src/plugin/plugin-command-module.ts +3 -1
- package/src/plugin/plugin-command.ts +17 -16
- package/src/preset/__tests__/host-supplied-preset-registry.test.ts +98 -0
- package/src/preset/__tests__/preset-command-module.test.ts +102 -56
- package/src/preset/preset-command-module.ts +26 -8
- package/src/preset/preset-command.ts +85 -27
- package/src/provider/__tests__/org-policy.test.ts +4 -5
- package/src/provider/__tests__/provider-command-module.test.ts +11 -14
- package/src/provider/__tests__/provider-startup-host-settings.test.ts +16 -0
- package/src/provider/__tests__/scripted-interaction.ts +7 -5
- package/src/provider/provider-command-execution.ts +4 -4
- package/src/provider/provider-command-module.ts +2 -1
- package/src/provider/provider-command-profile-lifecycle.ts +4 -4
- package/src/provider/provider-command-profile-operations.ts +3 -3
- package/src/provider/provider-command-profile.ts +1 -1
- package/src/provider/provider-command-setup.ts +4 -4
- package/src/provider/provider-startup.ts +62 -35
- package/src/remote-control/__tests__/remote-control-command-module.test.ts +36 -0
- package/src/remote-control/__tests__/remote-control-command.test.ts +216 -0
- package/src/remote-control/index.ts +8 -0
- package/src/remote-control/remote-control-command-module.ts +61 -0
- package/src/remote-control/remote-control-command.ts +138 -0
- package/src/reset/__tests__/reset-command-module.test.ts +5 -5
- package/src/reset/reset-command-module.ts +2 -1
- package/src/reset/reset-command.ts +3 -4
- package/src/rewind/__tests__/rewind-command-module.test.ts +40 -0
- package/src/rewind/rewind-command-module.ts +2 -1
- package/src/rewind/rewind-command.ts +73 -6
- package/src/sandbox/__tests__/sandbox-command.test.ts +80 -0
- package/src/sandbox/index.ts +2 -0
- package/src/sandbox/sandbox-command-module.ts +56 -0
- package/src/sandbox/sandbox-command.ts +94 -0
- package/src/schedule/__tests__/loop-command-functional.test.ts +386 -0
- package/src/schedule/__tests__/loop-command.test.ts +360 -0
- package/src/schedule/__tests__/loop-jitter.test.ts +25 -0
- package/src/schedule/__tests__/monitor-model-permission-functional.test.ts +126 -0
- package/src/schedule/__tests__/schedule-command.test.ts +143 -3
- package/src/schedule/__tests__/schedule-redos.test.ts +81 -0
- package/src/schedule/index.ts +3 -0
- package/src/schedule/loop-command.ts +333 -0
- package/src/schedule/loop-jitter.ts +41 -0
- package/src/schedule/schedule-command-module.ts +92 -14
- package/src/schedule/schedule-command.ts +98 -9
- package/src/schedule/schedule-spec-parser.ts +5 -1
- package/src/session/__tests__/rename-host-persistence.test.ts +72 -0
- package/src/session/__tests__/session-command-module.test.ts +210 -45
- package/src/session/session-command-module.ts +68 -37
- package/src/session/session-command.ts +147 -80
- package/src/settings/settings-command-module.ts +3 -2
- package/src/shell/__tests__/resolve-shell-host.test.ts +11 -0
- package/src/shell/__tests__/shell-command-functional.test.ts +23 -1
- package/src/shell/resolve-shell.ts +2 -2
- package/src/shell/shell-command-module.ts +6 -5
- package/src/shell/shell-command.ts +9 -5
- package/src/skills/__tests__/skills-command-module.test.ts +69 -28
- package/src/skills/index.ts +1 -0
- package/src/skills/skills-command-module.ts +22 -5
- package/src/skills/skills-command.ts +14 -13
- package/src/statusline/__tests__/statusline-command-module.test.ts +5 -4
- package/src/statusline/statusline-command-module.ts +2 -1
- package/src/statusline/statusline-command.ts +4 -4
- package/src/theme/__tests__/theme-command.test.ts +204 -0
- package/src/theme/index.ts +12 -0
- package/src/theme/theme-command-module.ts +211 -0
- package/src/user-local/__tests__/user-local-command.test.ts +145 -110
- package/src/user-local/user-local-command-constants.ts +1 -1
- package/src/user-local/user-local-command-module.ts +6 -5
- package/src/user-local/user-local-command.ts +12 -6
- package/src/user-local/user-local-memory-command.ts +17 -7
- package/src/plugins/default-plugin-command-adapter.ts +0 -164
- 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
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
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
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
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
|
|
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-
|
|
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 {
|
|
7
|
-
import type { ICommandResult } from '@robota-sdk/agent-interface-
|
|
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:
|
|
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) {
|