@robota-sdk/agent-command 3.0.0-beta.82 → 3.0.0-beta.84
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/CHANGELOG.md +1071 -0
- package/README.md +71 -62
- package/dist/node/index.cjs +81 -72
- package/dist/node/index.d.cts +136 -52
- package/dist/node/index.d.cts.map +1 -1
- package/dist/node/index.d.ts +136 -52
- package/dist/node/index.d.ts.map +1 -1
- package/dist/node/index.js +79 -70
- package/dist/node/index.js.map +1 -1
- package/package.json +16 -10
- package/src/agent/__tests__/agent-command.test.ts +62 -0
- package/src/agent/agent-command-parser.ts +2 -2
- package/src/agent/agent-command.ts +20 -0
- package/src/command-module-utils.test.ts +37 -0
- package/src/command-module-utils.ts +2 -0
- package/src/default/__tests__/__snapshots__/model-exposure.test.ts.snap +2 -2
- package/src/default/__tests__/default-command-modules.test.ts +54 -14
- package/src/default/__tests__/model-exposure.test.ts +18 -3
- package/src/default/__tests__/org-policy-forwarding.test.ts +10 -2
- package/src/default/default-command-modules.ts +15 -8
- package/src/devices/__tests__/devices-command-module.test.ts +99 -12
- package/src/devices/devices-command-module.ts +108 -24
- package/src/devices/devices-command-port.ts +62 -1
- package/src/devices/index.ts +3 -0
- package/src/doctor/doctor-node-deps.ts +6 -7
- package/src/editor/editor-command-module.ts +5 -0
- package/src/effort/effort-command.test.ts +65 -2
- package/src/effort/effort-command.ts +19 -5
- package/src/events/__tests__/events-command.test.ts +73 -0
- package/src/events/events-command-module.ts +49 -0
- package/src/events/events-command.ts +69 -0
- package/src/events/index.ts +2 -0
- package/src/git/__tests__/fake-git-port.ts +9 -2
- package/src/git/__tests__/git-command-module.test.ts +10 -2
- package/src/git/git-command-module.ts +3 -4
- package/src/git/git-commit.ts +6 -3
- package/src/git/index.ts +4 -1
- package/src/goal/goal-command.ts +3 -1
- package/src/handoff/__tests__/handoff-command-module.test.ts +2 -1
- package/src/handoff/handoff-command-module.ts +1 -1
- package/src/handoff/handoff-command.ts +2 -2
- package/src/help/__tests__/help-command-module.test.ts +3 -0
- package/src/help/__tests__/help-command.test.ts +3 -0
- package/src/index.ts +14 -0
- package/src/keybindings/keybindings-command-module.ts +17 -3
- package/src/mcp-activation/__tests__/mcp-activation-command.test.ts +172 -0
- package/src/mcp-activation/mcp-activation-command-module.ts +6 -0
- package/src/mcp-activation/mcp-activation-command.ts +62 -4
- package/src/memory/__tests__/memory-command-module.test.ts +40 -2
- package/src/memory/__tests__/memory-command-unavailable.test.ts +63 -0
- package/src/memory/memory-command-module.ts +4 -2
- package/src/memory/memory-command.ts +42 -2
- package/src/mode/__tests__/mode-command-module.test.ts +40 -0
- package/src/mode/mode-command.ts +1 -1
- package/src/model/__tests__/model-command-module.test.ts +80 -0
- package/src/model/__tests__/model-command.test.ts +240 -0
- package/src/model/index.ts +6 -0
- package/src/model/model-command-module.ts +56 -0
- package/src/model/model-command.ts +223 -0
- package/src/peers/__tests__/peers-command-module.test.ts +24 -0
- package/src/peers/__tests__/peers-command.test.ts +78 -0
- package/src/peers/peers-command-module.ts +1 -1
- package/src/peers/peers-command.ts +33 -4
- package/src/plugin/__tests__/plugin-command-module.test.ts +81 -1
- package/src/plugin/plugin-command.ts +47 -4
- package/src/provider/__tests__/org-policy.test.ts +4 -0
- package/src/provider/__tests__/provider-command-module.test.ts +276 -14
- package/src/provider/__tests__/scripted-interaction.ts +12 -2
- package/src/provider/provider-command-execution.ts +54 -8
- package/src/provider/provider-command-profile-lifecycle.ts +48 -2
- package/src/provider/provider-command-profile-operations.ts +14 -0
- package/src/provider/provider-command-profile.ts +7 -4
- package/src/provider/provider-command-setup.ts +14 -1
- package/src/rewind/__tests__/rewind-command-module.test.ts +32 -1
- package/src/rewind/rewind-command.ts +23 -2
- package/src/schedule/__tests__/loop-command-functional.test.ts +13 -0
- package/src/schedule/__tests__/loop-command.test.ts +238 -4
- package/src/schedule/__tests__/schedule-redos.test.ts +33 -0
- package/src/schedule/loop-command.ts +98 -4
- package/src/schedule/schedule-command-module.ts +8 -4
- package/src/settings/settings-command-module.ts +2 -2
- package/src/shell/shell-command-module.ts +5 -0
- package/src/terminal-client/__tests__/terminal-client-commands.test.ts +216 -0
- package/src/terminal-client/index.ts +5 -0
- package/src/terminal-client/terminal-client-commands.ts +109 -0
- package/src/theme/__tests__/theme-command.test.ts +1 -1
- package/src/theme/theme-command-module.ts +12 -2
- package/src/user-local/__tests__/user-local-command.test.ts +2 -2
- package/src/git/__tests__/git-diff.test.ts +0 -125
- package/src/git/__tests__/git-process.test.ts +0 -221
- package/src/git/__tests__/git-status.test.ts +0 -117
- package/src/git/git-diff.ts +0 -129
- package/src/git/git-process.ts +0 -169
- package/src/git/git-status.ts +0 -134
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@robota-sdk/agent-command",
|
|
3
|
-
"version": "3.0.0-beta.
|
|
4
|
-
"description": "
|
|
3
|
+
"version": "3.0.0-beta.84",
|
|
4
|
+
"description": "Slash-command modules for Robota SDK sessions (/help, /permissions, /mcp, /rewind, …)",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/node/index.js",
|
|
7
7
|
"types": "dist/node/index.d.ts",
|
|
@@ -28,19 +28,24 @@
|
|
|
28
28
|
"default": "./dist/node/index.cjs"
|
|
29
29
|
}
|
|
30
30
|
}
|
|
31
|
-
}
|
|
31
|
+
},
|
|
32
|
+
"./package.json": "./package.json"
|
|
33
|
+
},
|
|
34
|
+
"engines": {
|
|
35
|
+
"node": ">=22.12.0"
|
|
32
36
|
},
|
|
33
37
|
"files": [
|
|
34
38
|
"dist",
|
|
35
|
-
"src"
|
|
39
|
+
"src",
|
|
40
|
+
"CHANGELOG.md"
|
|
36
41
|
],
|
|
37
42
|
"dependencies": {
|
|
38
|
-
"@robota-sdk/agent-core": "3.0.0-beta.
|
|
39
|
-
"@robota-sdk/agent-framework": "3.0.0-beta.
|
|
40
|
-
"@robota-sdk/agent-interface-command": "3.0.0-beta.
|
|
41
|
-
"@robota-sdk/agent-interface-execution": "3.0.0-beta.
|
|
42
|
-
"@robota-sdk/agent-interface-session": "3.0.0-beta.
|
|
43
|
-
"@robota-sdk/agent-preset": "3.0.0-beta.
|
|
43
|
+
"@robota-sdk/agent-core": "3.0.0-beta.84",
|
|
44
|
+
"@robota-sdk/agent-framework": "3.0.0-beta.84",
|
|
45
|
+
"@robota-sdk/agent-interface-command": "3.0.0-beta.84",
|
|
46
|
+
"@robota-sdk/agent-interface-execution": "3.0.0-beta.84",
|
|
47
|
+
"@robota-sdk/agent-interface-session": "3.0.0-beta.84",
|
|
48
|
+
"@robota-sdk/agent-preset": "3.0.0-beta.84"
|
|
44
49
|
},
|
|
45
50
|
"devDependencies": {
|
|
46
51
|
"@types/node": "^22.19.21",
|
|
@@ -55,6 +60,7 @@
|
|
|
55
60
|
"url": "https://github.com/woojubb/robota.git",
|
|
56
61
|
"directory": "packages/agent-command"
|
|
57
62
|
},
|
|
63
|
+
"homepage": "https://docs.robota.io/en/packages/agent-command/",
|
|
58
64
|
"publishConfig": {
|
|
59
65
|
"access": "public"
|
|
60
66
|
},
|
|
@@ -15,6 +15,7 @@ function createMockSession(overrides?: Record<string, unknown>) {
|
|
|
15
15
|
{ name: 'general-purpose', description: 'General-purpose task execution agent.' },
|
|
16
16
|
{ name: 'Plan', description: 'Read-only planning agent.' },
|
|
17
17
|
]),
|
|
18
|
+
setDefaultAgentType: vi.fn(),
|
|
18
19
|
listAgentJobs: vi.fn().mockReturnValue([]),
|
|
19
20
|
readBackgroundTaskLog: vi.fn().mockResolvedValue({ taskId: 'agent_1', lines: [] }),
|
|
20
21
|
spawnAgentJob: vi.fn().mockResolvedValue({
|
|
@@ -518,6 +519,67 @@ describe('agent command module', () => {
|
|
|
518
519
|
expect(result?.uiIntents).toEqual([{ type: 'show-agent-switcher' }]);
|
|
519
520
|
});
|
|
520
521
|
|
|
522
|
+
it('#3282 §4: /agent <name> with nothing after it selects a default agent instead of failing', async () => {
|
|
523
|
+
const module = createAgentCommandModule();
|
|
524
|
+
const executor = new SystemCommandExecutor([
|
|
525
|
+
...createSystemCommands(),
|
|
526
|
+
...(module.systemCommands ?? []),
|
|
527
|
+
]);
|
|
528
|
+
const session = createMockSession();
|
|
529
|
+
|
|
530
|
+
const result = await executor.execute('agent', session, 'Plan');
|
|
531
|
+
|
|
532
|
+
expect(result?.success).toBe(true);
|
|
533
|
+
expect(result?.message).toBe('Default agent: Plan');
|
|
534
|
+
expect(result?.data).toEqual({ agentType: 'Plan' });
|
|
535
|
+
expect(
|
|
536
|
+
(session as unknown as { setDefaultAgentType: ReturnType<typeof vi.fn> }).setDefaultAgentType,
|
|
537
|
+
).toHaveBeenCalledWith('Plan');
|
|
538
|
+
expect(
|
|
539
|
+
(session as unknown as { spawnAgentJob: ReturnType<typeof vi.fn> }).spawnAgentJob,
|
|
540
|
+
).not.toHaveBeenCalled();
|
|
541
|
+
});
|
|
542
|
+
|
|
543
|
+
it('#3282 §4: a single unrecognized word is still a prompt for the default agent, not a selection', async () => {
|
|
544
|
+
const module = createAgentCommandModule();
|
|
545
|
+
const executor = new SystemCommandExecutor([
|
|
546
|
+
...createSystemCommands(),
|
|
547
|
+
...(module.systemCommands ?? []),
|
|
548
|
+
]);
|
|
549
|
+
const session = createMockSession();
|
|
550
|
+
|
|
551
|
+
const result = await executor.execute('agent', session, 'NotARealAgent');
|
|
552
|
+
|
|
553
|
+
expect(result?.success).toBe(true);
|
|
554
|
+
expect(
|
|
555
|
+
(session as unknown as { spawnAgentJob: ReturnType<typeof vi.fn> }).spawnAgentJob,
|
|
556
|
+
).toHaveBeenCalledWith({
|
|
557
|
+
agentType: 'general-purpose',
|
|
558
|
+
label: 'general-purpose',
|
|
559
|
+
mode: 'background',
|
|
560
|
+
prompt: 'NotARealAgent',
|
|
561
|
+
});
|
|
562
|
+
expect(
|
|
563
|
+
(session as unknown as { setDefaultAgentType: ReturnType<typeof vi.fn> }).setDefaultAgentType,
|
|
564
|
+
).not.toHaveBeenCalled();
|
|
565
|
+
});
|
|
566
|
+
|
|
567
|
+
it('#3282 §4: a known agent name followed by a prompt still runs a job, not a selection', async () => {
|
|
568
|
+
const module = createAgentCommandModule();
|
|
569
|
+
const executor = new SystemCommandExecutor([
|
|
570
|
+
...createSystemCommands(),
|
|
571
|
+
...(module.systemCommands ?? []),
|
|
572
|
+
]);
|
|
573
|
+
const session = createMockSession();
|
|
574
|
+
|
|
575
|
+
const result = await executor.execute('agent', session, 'Plan "draft architecture"');
|
|
576
|
+
|
|
577
|
+
expect(result?.success).toBe(true);
|
|
578
|
+
expect(
|
|
579
|
+
(session as unknown as { setDefaultAgentType: ReturnType<typeof vi.fn> }).setDefaultAgentType,
|
|
580
|
+
).not.toHaveBeenCalled();
|
|
581
|
+
});
|
|
582
|
+
|
|
521
583
|
it('shows text list when agent list subcommand is used explicitly', async () => {
|
|
522
584
|
const module = createAgentCommandModule();
|
|
523
585
|
const executor = new SystemCommandExecutor([
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import
|
|
1
|
+
import { DEFAULT_AGENT_TYPE } from '@robota-sdk/agent-framework';
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
import type { TBackgroundTaskIsolation } from '@robota-sdk/agent-interface-execution';
|
|
4
4
|
|
|
5
5
|
export type TAgentMode = 'background';
|
|
6
6
|
|
|
@@ -44,6 +44,21 @@ function executeOpenSwitcher(): ICommandResult {
|
|
|
44
44
|
return { message: '', uiIntents: [{ type: 'show-agent-switcher' }], success: true };
|
|
45
45
|
}
|
|
46
46
|
|
|
47
|
+
/**
|
|
48
|
+
* #3282 §4: `/agent <name>` with nothing after the name sets the session's default agent type —
|
|
49
|
+
* the one `/agent <prompt>` falls back to, and the one the agent switcher shows checked. Distinct
|
|
50
|
+
* from `executeRun`, which a name FOLLOWED by a prompt still reaches (a prompt makes it a run, not
|
|
51
|
+
* a selection).
|
|
52
|
+
*/
|
|
53
|
+
function executeSelectDefaultAgent(session: IAgentJobHostContext, agentType: string): ICommandResult {
|
|
54
|
+
session.setDefaultAgentType(agentType);
|
|
55
|
+
return {
|
|
56
|
+
message: `Default agent: ${agentType}`,
|
|
57
|
+
success: true,
|
|
58
|
+
data: { agentType },
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
|
|
47
62
|
async function executeList(session: IAgentJobHostContext): Promise<ICommandResult> {
|
|
48
63
|
const agents = session.listAgentDefinitions();
|
|
49
64
|
const jobs = session.listAgentJobs();
|
|
@@ -223,6 +238,11 @@ export async function executeAgentCommand(
|
|
|
223
238
|
if (action === 'send') return executeSend(session, tokens);
|
|
224
239
|
if (action === 'stop' || action === 'cancel') return executeStop(session, tokens);
|
|
225
240
|
if (action === 'close') return executeClose(session, tokens);
|
|
241
|
+
// `/agent <name>` alone (a known agent, nothing after it) selects a default rather than failing
|
|
242
|
+
// the "run" parse for want of a prompt — the same one-token shape the switcher sends.
|
|
243
|
+
if (tokens.length === 0 && getAvailableAgentNames(session).has(action)) {
|
|
244
|
+
return executeSelectDefaultAgent(session, action);
|
|
245
|
+
}
|
|
226
246
|
return executeRun(session, [action, ...tokens]);
|
|
227
247
|
} catch (error) {
|
|
228
248
|
return { message: formatError(error), success: false };
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
|
|
3
|
+
import { createSystemCommandFromEntry } from './command-module-utils.js';
|
|
4
|
+
|
|
5
|
+
const behavior = {
|
|
6
|
+
lifecycle: 'inline',
|
|
7
|
+
requiresPermission: false,
|
|
8
|
+
execute: () => ({ success: true, message: 'ran' }),
|
|
9
|
+
} as const;
|
|
10
|
+
|
|
11
|
+
describe('createSystemCommandFromEntry', () => {
|
|
12
|
+
it('#3189: carries where the entry says it runs, so the catalog and the palette agree', () => {
|
|
13
|
+
const command = createSystemCommandFromEntry(
|
|
14
|
+
{
|
|
15
|
+
name: 'local',
|
|
16
|
+
description: 'Runs on the terminal',
|
|
17
|
+
source: 'test',
|
|
18
|
+
runner: 'client',
|
|
19
|
+
surfaces: ['terminal'],
|
|
20
|
+
},
|
|
21
|
+
behavior,
|
|
22
|
+
);
|
|
23
|
+
|
|
24
|
+
expect(command.runner).toBe('client');
|
|
25
|
+
expect(command.surfaces).toEqual(['terminal']);
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
it('leaves an unstated placement unstated rather than guessing one', () => {
|
|
29
|
+
const command = createSystemCommandFromEntry(
|
|
30
|
+
{ name: 'plain', description: 'Runs on the runtime', source: 'test' },
|
|
31
|
+
behavior,
|
|
32
|
+
);
|
|
33
|
+
|
|
34
|
+
expect('runner' in command).toBe(false);
|
|
35
|
+
expect('surfaces' in command).toBe(false);
|
|
36
|
+
});
|
|
37
|
+
});
|
|
@@ -25,6 +25,8 @@ export function createSystemCommandFromEntry(
|
|
|
25
25
|
...(entry.argumentHint !== undefined ? { argumentHint: entry.argumentHint } : {}),
|
|
26
26
|
...(entry.safety !== undefined ? { safety: entry.safety } : {}),
|
|
27
27
|
...(entry.subcommands !== undefined ? { subcommands: entry.subcommands } : {}),
|
|
28
|
+
...(entry.runner !== undefined ? { runner: entry.runner } : {}),
|
|
29
|
+
...(entry.surfaces !== undefined ? { surfaces: entry.surfaces } : {}),
|
|
28
30
|
...(behavior.semanticRole !== undefined ? { semanticRole: behavior.semanticRole } : {}),
|
|
29
31
|
lifecycle: behavior.lifecycle,
|
|
30
32
|
requiresPermission: behavior.requiresPermission,
|
|
@@ -44,7 +44,7 @@ Run it with no arguments.",
|
|
|
44
44
|
},
|
|
45
45
|
"loop": {
|
|
46
46
|
"argumentHint": "[prompt] | <N><s|m|h|d> [prompt] | <prompt> every <N> <unit> | list | stop <id>",
|
|
47
|
-
"description": "Repeat a prompt on a fixed cadence (\`<N><s|m|h|d>
|
|
47
|
+
"description": "Repeat a prompt on a fixed cadence (\`<N><s|m|h|d> <prompt>\`) or self-paced (\`<prompt>\`: each iteration chooses its next delay or stops). Use it when the user asks for recurring work in this session, and always give your own prompt: any form that would fall back to the host's default maintenance prompt — bare \`/loop\` or an interval alone (\`<N><s|m|h|d>\`) — needs a person to confirm it and is always refused for you, whether or not one is attached. \`list\` shows active loops; \`stop <id>\` ends one, even mid-turn. Returns the loop id and its next fire time, the loop list, or a refusal naming the command to suggest.",
|
|
48
48
|
"kind": "builtin-command",
|
|
49
49
|
"modelInvocable": true,
|
|
50
50
|
"name": "loop",
|
|
@@ -64,7 +64,7 @@ Subcommands you may run (bare \`mcp\` is also allowed):
|
|
|
64
64
|
},
|
|
65
65
|
"memory": {
|
|
66
66
|
"argumentHint": "[list | show [topic] | add <user|feedback|project|reference> <topic> <text> | pending | used]",
|
|
67
|
-
"description": "Read and write this project’s durable memory. Use \`list\` or \`show [topic]\` to look up stored conventions, preferences or references before asking the user again; \`add <type> <topic> <text>\` to save a durable preference, project convention, feedback item or reference worth reusing across sessions (never secrets, credentials or transient facts); \`pending\` to see candidates awaiting the user’s review; \`used\` to report which memory items informed this turn. Bare lists topics. Returns the requested topics or entries, or a confirmation of what was saved. Approving or rejecting a pending candidate is the user’s review: suggest \`/memory approve <id>\` or \`/memory reject <id>\`.
|
|
67
|
+
"description": "Read and write this project’s durable memory. Use \`list\` or \`show [topic]\` to look up stored conventions, preferences or references before asking the user again; \`add <type> <topic> <text>\` to save a durable preference, project convention, feedback item or reference worth reusing across sessions (never secrets, credentials or transient facts); \`pending\` to see candidates awaiting the user’s review; \`used\` to report which memory items informed this turn. Bare lists topics. In plan mode \`add\` is refused: save after plan mode ends. Returns the requested topics or entries, or a confirmation of what was saved. Approving or rejecting a pending candidate is the user’s review: suggest \`/memory approve <id>\` or \`/memory reject <id>\`.
|
|
68
68
|
Subcommands you may run (bare \`memory\` is also allowed):
|
|
69
69
|
- list: List project memory topics
|
|
70
70
|
- show: Show project memory index or a topic
|
|
@@ -1,4 +1,8 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { mkdtempSync, rmSync } from 'node:fs';
|
|
2
|
+
import { tmpdir } from 'node:os';
|
|
3
|
+
import { join } from 'node:path';
|
|
4
|
+
|
|
5
|
+
import { afterAll, describe, expect, it } from 'vitest';
|
|
2
6
|
|
|
3
7
|
import type { IProviderDefinition } from '@robota-sdk/agent-core';
|
|
4
8
|
import type {
|
|
@@ -8,6 +12,10 @@ import type {
|
|
|
8
12
|
|
|
9
13
|
import { createDefaultCommandModules } from '../default-command-modules.js';
|
|
10
14
|
|
|
15
|
+
/** User-local storage lives in a private per-run directory, never a fixed name under /tmp. */
|
|
16
|
+
const USER_LOCAL_STORAGE_ROOT = mkdtempSync(join(tmpdir(), 'robota-test-'));
|
|
17
|
+
afterAll(() => rmSync(USER_LOCAL_STORAGE_ROOT, { recursive: true, force: true }));
|
|
18
|
+
|
|
11
19
|
const providerDefinitions: readonly IProviderDefinition[] = [
|
|
12
20
|
{
|
|
13
21
|
type: 'anthropic',
|
|
@@ -26,7 +34,7 @@ const providerSettingsAdapter: IProviderCommandSettingsAdapter = {
|
|
|
26
34
|
writeTargetSettings: () => undefined,
|
|
27
35
|
};
|
|
28
36
|
|
|
29
|
-
const baseOptions = { cwd: '/tmp', userLocalStorageRoot:
|
|
37
|
+
const baseOptions = { cwd: '/tmp', userLocalStorageRoot: USER_LOCAL_STORAGE_ROOT, providerDefinitions, providerSettingsAdapter } as const;
|
|
30
38
|
|
|
31
39
|
function moduleNames(opts: Parameters<typeof createDefaultCommandModules>[0]): string[] {
|
|
32
40
|
return createDefaultCommandModules(opts).modules.map((module) => module.name);
|
|
@@ -42,10 +50,10 @@ describe('createDefaultCommandModules — PRESET-004 module-selection delta', ()
|
|
|
42
50
|
const names = moduleNames(baseOptions);
|
|
43
51
|
// No-regression: the default set length is the documented 32 modules (SELFHOST-002 added
|
|
44
52
|
// `/plan`; PEER-004 added `/peers`; CLI-1994 added `/fork`; MCP-2520 added `/mcp`; CLI-1988 added
|
|
45
|
-
// `/output-style`; FLOW-008 added `/effort`; `/advisor` joined beside it
|
|
46
|
-
// assertion that matters — a length on its own can be restored by any
|
|
47
|
-
// exists only to catch an addition that also removed something.
|
|
48
|
-
expect(names).toHaveLength(
|
|
53
|
+
// `/output-style`; FLOW-008 added `/effort`; `/advisor` joined beside it; #3282 §2 added `/model`).
|
|
54
|
+
// The list below is the assertion that matters — a length on its own can be restored by any
|
|
55
|
+
// substitution, and the count exists only to catch an addition that also removed something.
|
|
56
|
+
expect(names).toHaveLength(39);
|
|
49
57
|
expect(names).toEqual([
|
|
50
58
|
'agent-command-skills',
|
|
51
59
|
'agent-command-help',
|
|
@@ -66,6 +74,8 @@ describe('createDefaultCommandModules — PRESET-004 module-selection delta', ()
|
|
|
66
74
|
'agent-command-shell',
|
|
67
75
|
'agent-command-editor',
|
|
68
76
|
'agent-command-git',
|
|
77
|
+
'agent-command-keybindings',
|
|
78
|
+
'agent-command-theme',
|
|
69
79
|
'agent-command-memory',
|
|
70
80
|
'agent-command-mcp-activation',
|
|
71
81
|
'agent-command-user-local',
|
|
@@ -80,9 +90,11 @@ describe('createDefaultCommandModules — PRESET-004 module-selection delta', ()
|
|
|
80
90
|
'agent-command-plugin',
|
|
81
91
|
'agent-command-settings',
|
|
82
92
|
'agent-command-peers',
|
|
93
|
+
'agent-command-events',
|
|
83
94
|
'agent-command-handoff',
|
|
84
95
|
'agent-command-remote-control',
|
|
85
96
|
'agent-command-provider',
|
|
97
|
+
'agent-command-model',
|
|
86
98
|
]);
|
|
87
99
|
});
|
|
88
100
|
|
|
@@ -92,14 +104,42 @@ describe('createDefaultCommandModules — PRESET-004 module-selection delta', ()
|
|
|
92
104
|
expect(names).toHaveLength(2);
|
|
93
105
|
});
|
|
94
106
|
|
|
95
|
-
it('
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
})
|
|
102
|
-
|
|
107
|
+
it('#3186: /keybindings and /theme exist on a host without a terminal, and say where they work', async () => {
|
|
108
|
+
const { modules } = createDefaultCommandModules(baseOptions);
|
|
109
|
+
const commands = modules.flatMap((module) => module.systemCommands ?? []);
|
|
110
|
+
for (const name of ['keybindings', 'theme']) {
|
|
111
|
+
const command = commands.find((candidate) => candidate.name === name);
|
|
112
|
+
expect(command, name).toBeDefined();
|
|
113
|
+
const result = await command!.execute({} as never, '');
|
|
114
|
+
expect(result).toMatchObject({ success: false });
|
|
115
|
+
expect(result.message).toMatch(/robota terminal/);
|
|
116
|
+
}
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
it('#3189: exactly /editor, /keybindings, /shell and /theme run on the terminal client', () => {
|
|
120
|
+
const { modules } = createDefaultCommandModules(baseOptions);
|
|
121
|
+
const TERMINAL_CLIENT = ['editor', 'keybindings', 'shell', 'theme'];
|
|
122
|
+
|
|
123
|
+
// The executable commands: what the runtime keeps in its catalog and what the listing reads.
|
|
124
|
+
const clientSystemCommands = modules
|
|
125
|
+
.flatMap((module) => module.systemCommands ?? [])
|
|
126
|
+
.filter((command) => command.runner === 'client');
|
|
127
|
+
expect(clientSystemCommands.map((command) => command.name).sort()).toEqual(TERMINAL_CLIENT);
|
|
128
|
+
for (const command of clientSystemCommands) {
|
|
129
|
+
expect(command.surfaces, command.name).toEqual(['terminal']);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// The palette entries, which must say the same — the two views cannot disagree.
|
|
133
|
+
const clientEntries = modules
|
|
134
|
+
.flatMap((module) => module.commandSources ?? [])
|
|
135
|
+
.flatMap((source) => source.getCommands())
|
|
136
|
+
.filter((entry) => entry.runner === 'client');
|
|
137
|
+
expect(clientEntries.map((entry) => entry.name).sort()).toEqual(TERMINAL_CLIENT);
|
|
138
|
+
for (const entry of clientEntries) {
|
|
139
|
+
expect(entry.surfaces, entry.name).toEqual(['terminal']);
|
|
140
|
+
// Running on the client does not change who may run it: all four stay user-only.
|
|
141
|
+
expect(entry.modelInvocable, entry.name).toBe(false);
|
|
142
|
+
}
|
|
103
143
|
});
|
|
104
144
|
|
|
105
145
|
it('TC-02: disabledCommandModules blacklist removes the named module', () => {
|
|
@@ -7,7 +7,11 @@
|
|
|
7
7
|
* each command's entry.
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
|
-
import {
|
|
10
|
+
import { mkdtempSync, rmSync } from 'node:fs';
|
|
11
|
+
import { tmpdir } from 'node:os';
|
|
12
|
+
import { join } from 'node:path';
|
|
13
|
+
|
|
14
|
+
import { afterAll, afterEach, describe, expect, it, vi } from 'vitest';
|
|
11
15
|
|
|
12
16
|
import { SystemCommandExecutor } from '@robota-sdk/agent-framework';
|
|
13
17
|
import { createTestCommandHost } from '@robota-sdk/agent-framework/testing';
|
|
@@ -41,6 +45,10 @@ const themeCataloguePort: IThemeCataloguePort = {
|
|
|
41
45
|
}),
|
|
42
46
|
};
|
|
43
47
|
|
|
48
|
+
/** User-local storage lives in a private per-run directory, never a fixed name under /tmp. */
|
|
49
|
+
const USER_LOCAL_STORAGE_ROOT = mkdtempSync(join(tmpdir(), 'robota-test-'));
|
|
50
|
+
afterAll(() => rmSync(USER_LOCAL_STORAGE_ROOT, { recursive: true, force: true }));
|
|
51
|
+
|
|
44
52
|
const fixtures: IDoctorFixture[] = [];
|
|
45
53
|
afterEach(() => {
|
|
46
54
|
for (const fixture of fixtures.splice(0)) fixture.cleanup();
|
|
@@ -52,12 +60,16 @@ function allBuiltInCommands(): ISystemCommand[] {
|
|
|
52
60
|
fixtures.push(fixture);
|
|
53
61
|
const { modules } = createDefaultCommandModules({
|
|
54
62
|
cwd: '/tmp',
|
|
55
|
-
userLocalStorageRoot:
|
|
63
|
+
userLocalStorageRoot: USER_LOCAL_STORAGE_ROOT,
|
|
56
64
|
providerDefinitions,
|
|
57
65
|
providerSettingsAdapter,
|
|
58
66
|
});
|
|
67
|
+
// /keybindings and /theme are in the default set; built here with their capabilities instead.
|
|
68
|
+
const withoutPortless = modules.filter(
|
|
69
|
+
(module) => module.name !== 'agent-command-keybindings' && module.name !== 'agent-command-theme',
|
|
70
|
+
);
|
|
59
71
|
return [
|
|
60
|
-
...
|
|
72
|
+
...withoutPortless,
|
|
61
73
|
createKeybindingsCommandModule({ ensureFile: async () => '/tmp/keybindings.json' }),
|
|
62
74
|
createThemeCommandModule(themeCataloguePort),
|
|
63
75
|
createDoctorCommandModule(fixture.inputs, fixture.deps),
|
|
@@ -94,8 +106,11 @@ const USER_ONLY_COMMANDS = [
|
|
|
94
106
|
'plugin',
|
|
95
107
|
'reload-plugins',
|
|
96
108
|
'remote-control',
|
|
109
|
+
'events',
|
|
97
110
|
// Account and credential actions.
|
|
98
111
|
'provider',
|
|
112
|
+
// Cost and capability decisions: the person chooses, never the model on its own (#3282 §2).
|
|
113
|
+
'model',
|
|
99
114
|
// Permission widening or permission-mode changes.
|
|
100
115
|
'mode',
|
|
101
116
|
'permissions',
|
|
@@ -17,7 +17,11 @@
|
|
|
17
17
|
* type checker's job.
|
|
18
18
|
*/
|
|
19
19
|
|
|
20
|
-
import {
|
|
20
|
+
import { mkdtempSync, rmSync } from 'node:fs';
|
|
21
|
+
import { tmpdir } from 'node:os';
|
|
22
|
+
import { join } from 'node:path';
|
|
23
|
+
|
|
24
|
+
import { afterAll, describe, expect, it } from 'vitest';
|
|
21
25
|
import { SystemCommandExecutor } from '@robota-sdk/agent-framework';
|
|
22
26
|
import { createTestCommandHost } from '@robota-sdk/agent-framework/testing';
|
|
23
27
|
|
|
@@ -55,13 +59,17 @@ const adapter: IProviderCommandSettingsAdapter = {
|
|
|
55
59
|
writeTargetSettings: () => undefined,
|
|
56
60
|
};
|
|
57
61
|
|
|
62
|
+
/** User-local storage lives in a private per-run directory, never a fixed name under /tmp. */
|
|
63
|
+
const USER_LOCAL_STORAGE_ROOT = mkdtempSync(join(tmpdir(), 'robota-test-'));
|
|
64
|
+
afterAll(() => rmSync(USER_LOCAL_STORAGE_ROOT, { recursive: true, force: true }));
|
|
65
|
+
|
|
58
66
|
const POLICY: IOrgPolicy = { allowedProviders: ['anthropic'], adminContact: 'ops@example.com' };
|
|
59
67
|
|
|
60
68
|
/** Build the provider executor the way the PRODUCT does — through the default-modules factory. */
|
|
61
69
|
function executorThroughFactory(orgPolicy?: IOrgPolicy): SystemCommandExecutor {
|
|
62
70
|
const { modules } = createDefaultCommandModules({
|
|
63
71
|
cwd: '/work',
|
|
64
|
-
userLocalStorageRoot:
|
|
72
|
+
userLocalStorageRoot: USER_LOCAL_STORAGE_ROOT,
|
|
65
73
|
providerDefinitions,
|
|
66
74
|
providerSettingsAdapter: adapter,
|
|
67
75
|
...(orgPolicy === undefined ? {} : { orgPolicy }),
|
|
@@ -20,8 +20,10 @@ import { createLanguageCommandModule } from '../language/index.js';
|
|
|
20
20
|
import { createMCPActivationCommandModule } from '../mcp-activation/index.js';
|
|
21
21
|
import { createMemoryCommandModule } from '../memory/index.js';
|
|
22
22
|
import { createModeCommandModule } from '../mode/index.js';
|
|
23
|
+
import { createModelCommandModule } from '../model/index.js';
|
|
23
24
|
import { createSandboxCommandModule } from '../sandbox/index.js';
|
|
24
25
|
import { createOutputStyleCommandModule } from '../output-style/index.js';
|
|
26
|
+
import { createEventsCommandModule } from '../events/index.js';
|
|
25
27
|
import { createPeersCommandModule } from '../peers/index.js';
|
|
26
28
|
import { createPermissionsCommandModule } from '../permissions/index.js';
|
|
27
29
|
import { createPlanCommandModule } from '../plan/index.js';
|
|
@@ -72,12 +74,11 @@ export interface IDefaultCommandModulesOptions {
|
|
|
72
74
|
* red when the producer dropped it.
|
|
73
75
|
*/
|
|
74
76
|
orgPolicy?: IOrgPolicy;
|
|
75
|
-
/** Optional TUI-owned file capability;
|
|
77
|
+
/** Optional TUI-owned file capability; absent, `/keybindings` says it belongs to the robota terminal. */
|
|
76
78
|
keybindingsFilePort?: IKeybindingsFilePort;
|
|
77
79
|
/**
|
|
78
|
-
* SCREEN-2002: the surface's theme catalogue.
|
|
79
|
-
*
|
|
80
|
-
* missing than present-and-failing.
|
|
80
|
+
* SCREEN-2002: the surface's theme catalogue. Absent (print mode, `--serve`), `/theme` is still
|
|
81
|
+
* listed and says it belongs to the robota terminal, which runs it itself when attached.
|
|
81
82
|
*/
|
|
82
83
|
themeCataloguePort?: IThemeCataloguePort;
|
|
83
84
|
/**
|
|
@@ -173,10 +174,8 @@ export function createDefaultCommandModules({
|
|
|
173
174
|
createShellCommandModule(),
|
|
174
175
|
createEditorCommandModule(editorTemporaryDirectoryPrefix),
|
|
175
176
|
createGitCommandModule(),
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
: [createKeybindingsCommandModule(keybindingsFilePort)]),
|
|
179
|
-
...(themeCataloguePort === undefined ? [] : [createThemeCommandModule(themeCataloguePort)]),
|
|
177
|
+
createKeybindingsCommandModule(keybindingsFilePort),
|
|
178
|
+
createThemeCommandModule(themeCataloguePort),
|
|
180
179
|
...(doctorInputs === undefined
|
|
181
180
|
? []
|
|
182
181
|
: [createDoctorCommandModule(doctorInputs, undefined, doctorDisplay)]),
|
|
@@ -194,6 +193,7 @@ export function createDefaultCommandModules({
|
|
|
194
193
|
createPluginCommandModule(),
|
|
195
194
|
createSettingsCommandModule(),
|
|
196
195
|
createPeersCommandModule(),
|
|
196
|
+
createEventsCommandModule(),
|
|
197
197
|
// HANDOFF-001 (issue #1864). Registered even though no product wires the carrier adapter yet:
|
|
198
198
|
// the command's own answer to a host without one is to say so, which is a better state than a
|
|
199
199
|
// capability nobody can see. It is also what makes the carrier's arrival observable.
|
|
@@ -205,6 +205,13 @@ export function createDefaultCommandModules({
|
|
|
205
205
|
settings: providerSettingsAdapter,
|
|
206
206
|
...(orgPolicy === undefined ? {} : { orgPolicy }),
|
|
207
207
|
}),
|
|
208
|
+
// #3282 §2: shares the provider command's settings adapter and definitions — one settings write
|
|
209
|
+
// owner, never a second copy that could drift from what `/provider switch` reads and writes.
|
|
210
|
+
createModelCommandModule({
|
|
211
|
+
providerDefinitions,
|
|
212
|
+
settings: providerSettingsAdapter,
|
|
213
|
+
...(orgPolicy === undefined ? {} : { orgPolicy }),
|
|
214
|
+
}),
|
|
208
215
|
];
|
|
209
216
|
const builtModuleNames = modules.map((module) => module.name);
|
|
210
217
|
return {
|