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