@robota-sdk/agent-command 3.0.0-beta.76 → 3.0.0-beta.77
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/LICENSE +661 -21
- package/README.md +12 -6
- package/dist/node/index.cjs +38 -33
- package/dist/node/index.d.ts +68 -6
- package/dist/node/index.d.ts.map +1 -1
- package/dist/node/index.js +40 -35
- package/dist/node/index.js.map +1 -1
- package/package.json +7 -7
- package/src/agent/agent-command-parser.ts +1 -1
- package/src/agent/agent-command.ts +2 -1
- package/src/background/__tests__/background-command-module.test.ts +2 -5
- package/src/default/__tests__/default-command-modules.test.ts +5 -2
- package/src/default/default-command-modules.ts +6 -0
- package/src/editor/__tests__/editor-command-functional.test.ts +91 -0
- package/src/editor/editor-command-module.ts +47 -0
- package/src/editor/editor-command.ts +53 -0
- package/src/editor/index.ts +7 -0
- package/src/editor/resolve-editor.ts +21 -0
- package/src/exit/__tests__/exit-command-module.test.ts +21 -2
- package/src/exit/exit-command-module.ts +1 -10
- package/src/exit/exit-command.ts +15 -1
- package/src/goal/__tests__/goal-command.test.ts +75 -0
- package/src/goal/goal-command-module.ts +48 -0
- package/src/goal/goal-command.ts +70 -0
- package/src/goal/index.ts +6 -0
- package/src/index.ts +3 -0
- package/src/language/__tests__/language-command-module.test.ts +16 -0
- package/src/language/language-command-module.ts +1 -18
- package/src/language/language-command.ts +35 -9
- package/src/mode/__tests__/mode-command-module.test.ts +34 -0
- package/src/mode/mode-command-module.ts +1 -18
- package/src/mode/mode-command.ts +31 -8
- package/src/preset/__tests__/preset-command-module.test.ts +36 -0
- package/src/preset/preset-command-module.ts +1 -18
- package/src/preset/preset-command.ts +37 -8
- package/src/provider/__tests__/org-policy.test.ts +19 -13
- package/src/provider/__tests__/provider-command-module.test.ts +151 -80
- package/src/provider/__tests__/scripted-interaction.ts +28 -0
- package/src/provider/provider-command-execution.ts +67 -50
- package/src/provider/provider-command-module.ts +3 -19
- package/src/provider/provider-command-profile-lifecycle.ts +52 -72
- package/src/provider/provider-command-profile-operations.ts +15 -51
- package/src/provider/provider-command-profile.ts +44 -49
- package/src/provider/provider-command-setup.ts +56 -51
- package/src/session/__tests__/session-command-module.test.ts +38 -0
- package/src/session/session-command-module.ts +1 -10
- package/src/session/session-command.ts +14 -1
- package/src/shell/__tests__/shell-command-functional.test.ts +96 -0
- package/src/shell/index.ts +8 -0
- package/src/shell/resolve-shell.ts +25 -0
- package/src/shell/shell-command-module.ts +47 -0
- package/src/shell/shell-command.ts +44 -0
- package/src/shell/spawn-inherited.ts +32 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@robota-sdk/agent-command",
|
|
3
|
-
"version": "3.0.0-beta.
|
|
3
|
+
"version": "3.0.0-beta.77",
|
|
4
4
|
"description": "Consolidated command module implementations for Robota SDK CLI",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/node/index.js",
|
|
@@ -24,19 +24,19 @@
|
|
|
24
24
|
"src"
|
|
25
25
|
],
|
|
26
26
|
"dependencies": {
|
|
27
|
-
"@robota-sdk/agent-core": "3.0.0-beta.
|
|
28
|
-
"@robota-sdk/agent-framework": "3.0.0-beta.
|
|
29
|
-
"@robota-sdk/agent-interface-transport": "3.0.0-beta.
|
|
30
|
-
"@robota-sdk/agent-preset": "3.0.0-beta.
|
|
27
|
+
"@robota-sdk/agent-core": "3.0.0-beta.77",
|
|
28
|
+
"@robota-sdk/agent-framework": "3.0.0-beta.77",
|
|
29
|
+
"@robota-sdk/agent-interface-transport": "3.0.0-beta.77",
|
|
30
|
+
"@robota-sdk/agent-preset": "3.0.0-beta.77"
|
|
31
31
|
},
|
|
32
32
|
"devDependencies": {
|
|
33
33
|
"@types/node": "^22.19.21",
|
|
34
|
-
"rimraf": "^
|
|
34
|
+
"rimraf": "^5.0.10",
|
|
35
35
|
"tsdown": "^0.22.2",
|
|
36
36
|
"typescript": "^5.9.3",
|
|
37
37
|
"vitest": "^3.2.6"
|
|
38
38
|
},
|
|
39
|
-
"license": "
|
|
39
|
+
"license": "AGPL-3.0-only OR LicenseRef-Commercial",
|
|
40
40
|
"publishConfig": {
|
|
41
41
|
"access": "public"
|
|
42
42
|
},
|
|
@@ -3,8 +3,9 @@ import { summarizeBackgroundJobGroup } from '@robota-sdk/agent-framework';
|
|
|
3
3
|
import { parseParallelRequests, parseRunRequest, tokenizeArgs } from './agent-command-parser.js';
|
|
4
4
|
|
|
5
5
|
import type { IAgentRunRequest } from './agent-command-parser.js';
|
|
6
|
-
import type { IAgentJobHostContext
|
|
6
|
+
import type { IAgentJobHostContext } from '@robota-sdk/agent-framework';
|
|
7
7
|
import type { ICommandResult } from '@robota-sdk/agent-interface-transport';
|
|
8
|
+
import type { ISubagentJobState } from '@robota-sdk/agent-interface-transport';
|
|
8
9
|
|
|
9
10
|
function formatError<TError>(error: TError): string {
|
|
10
11
|
return error instanceof Error ? error.message : String(error);
|
|
@@ -1,15 +1,12 @@
|
|
|
1
1
|
import { describe, expect, it, vi } from 'vitest';
|
|
2
|
-
import type {
|
|
3
|
-
IBackgroundTaskState,
|
|
4
|
-
ICommandHostContext,
|
|
5
|
-
ICommandSessionRuntime,
|
|
6
|
-
} from '@robota-sdk/agent-framework';
|
|
2
|
+
import type { ICommandHostContext, ICommandSessionRuntime } from '@robota-sdk/agent-framework';
|
|
7
3
|
import {
|
|
8
4
|
BackgroundCommandSource,
|
|
9
5
|
createBackgroundCommandEntry,
|
|
10
6
|
createBackgroundCommandModule,
|
|
11
7
|
executeBackgroundCommand,
|
|
12
8
|
} from '../index.js';
|
|
9
|
+
import type { IBackgroundTaskState } from '@robota-sdk/agent-interface-transport';
|
|
13
10
|
|
|
14
11
|
function createSessionRuntime(): ICommandSessionRuntime {
|
|
15
12
|
return {
|
|
@@ -40,8 +40,8 @@ describe('createDefaultCommandModules — PRESET-004 module-selection delta', ()
|
|
|
40
40
|
|
|
41
41
|
it('TC-04: neither enabled nor disabled given → full default set unchanged (no-regression)', () => {
|
|
42
42
|
const names = moduleNames(baseOptions);
|
|
43
|
-
// No-regression: the default set length is the documented
|
|
44
|
-
expect(names).toHaveLength(
|
|
43
|
+
// No-regression: the default set length is the documented 24 modules.
|
|
44
|
+
expect(names).toHaveLength(24);
|
|
45
45
|
expect(names).toEqual([
|
|
46
46
|
'agent-command-skills',
|
|
47
47
|
'agent-command-help',
|
|
@@ -51,6 +51,9 @@ describe('createDefaultCommandModules — PRESET-004 module-selection delta', ()
|
|
|
51
51
|
'agent-command-preset',
|
|
52
52
|
'agent-command-language',
|
|
53
53
|
'agent-command-background',
|
|
54
|
+
'agent-command-goal',
|
|
55
|
+
'agent-command-shell',
|
|
56
|
+
'agent-command-editor',
|
|
54
57
|
'agent-command-memory',
|
|
55
58
|
'agent-command-user-local',
|
|
56
59
|
'agent-command-compact',
|
|
@@ -2,7 +2,9 @@ import { createAgentCommandModule } from '../agent/index.js';
|
|
|
2
2
|
import { createBackgroundCommandModule } from '../background/index.js';
|
|
3
3
|
import { createCompactCommandModule } from '../compact/index.js';
|
|
4
4
|
import { createContextCommandModule } from '../context/index.js';
|
|
5
|
+
import { createEditorCommandModule } from '../editor/index.js';
|
|
5
6
|
import { createExitCommandModule } from '../exit/index.js';
|
|
7
|
+
import { createGoalCommandModule } from '../goal/index.js';
|
|
6
8
|
import { createHelpCommandModule } from '../help/index.js';
|
|
7
9
|
import { createLanguageCommandModule } from '../language/index.js';
|
|
8
10
|
import { createMemoryCommandModule } from '../memory/index.js';
|
|
@@ -16,6 +18,7 @@ import { createRewindCommandModule } from '../rewind/index.js';
|
|
|
16
18
|
import { createScheduleCommandModule } from '../schedule/index.js';
|
|
17
19
|
import { createSessionCommandModule } from '../session/index.js';
|
|
18
20
|
import { createSettingsCommandModule } from '../settings/index.js';
|
|
21
|
+
import { createShellCommandModule } from '../shell/index.js';
|
|
19
22
|
import { createSkillsCommandModule } from '../skills/index.js';
|
|
20
23
|
import { createStatusLineCommandModule } from '../statusline/index.js';
|
|
21
24
|
import { createUserLocalCommandModule } from '../user-local/index.js';
|
|
@@ -79,6 +82,9 @@ export function createDefaultCommandModules({
|
|
|
79
82
|
createPresetCommandModule(),
|
|
80
83
|
createLanguageCommandModule(),
|
|
81
84
|
createBackgroundCommandModule(),
|
|
85
|
+
createGoalCommandModule(),
|
|
86
|
+
createShellCommandModule(),
|
|
87
|
+
createEditorCommandModule(),
|
|
82
88
|
createMemoryCommandModule(),
|
|
83
89
|
createUserLocalCommandModule(),
|
|
84
90
|
createCompactCommandModule(),
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* TERM-004: `/editor` — framework functional test.
|
|
3
|
+
*
|
|
4
|
+
* Drives the command through a REAL InteractiveSession with an injected fake handoff and a FAKE
|
|
5
|
+
* editor (a tiny script that writes known content to its file arg). Verifies the round-trip: the
|
|
6
|
+
* command opens the editor via `runWithTerminal`, captures the saved text, and cleans up — without a
|
|
7
|
+
* real interactive editor (which is a manual gate, TERM-002).
|
|
8
|
+
*/
|
|
9
|
+
import { chmodSync, mkdtempSync, writeFileSync } from 'node:fs';
|
|
10
|
+
import { tmpdir } from 'node:os';
|
|
11
|
+
import { join } from 'node:path';
|
|
12
|
+
|
|
13
|
+
import { afterEach, describe, expect, it } from 'vitest';
|
|
14
|
+
|
|
15
|
+
import { scriptedSession, type ScriptedSessionHarness } from '@robota-sdk/agent-framework/testing';
|
|
16
|
+
|
|
17
|
+
import { createEditorCommandModule } from '../editor-command-module.js';
|
|
18
|
+
|
|
19
|
+
import type { ITerminalHandoff } from '@robota-sdk/agent-interface-transport';
|
|
20
|
+
|
|
21
|
+
const TEST_TIMEOUT = 20_000;
|
|
22
|
+
|
|
23
|
+
function fakeHandoff(canHandoff: boolean): ITerminalHandoff {
|
|
24
|
+
return {
|
|
25
|
+
canHandoffTerminal: canHandoff,
|
|
26
|
+
async runWithTerminal<T>(fn: () => Promise<T>): Promise<T> {
|
|
27
|
+
return fn();
|
|
28
|
+
},
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** Write a fake `$EDITOR`: a script that writes `content` into the file it is given as $1. */
|
|
33
|
+
function installFakeEditor(content: string): string {
|
|
34
|
+
const dir = mkdtempSync(join(tmpdir(), 'robota-fake-editor-'));
|
|
35
|
+
const script = join(dir, 'fake-editor.sh');
|
|
36
|
+
writeFileSync(script, `#!/bin/sh\nprintf '%s' '${content}' > "$1"\n`, 'utf8');
|
|
37
|
+
chmodSync(script, 0o755);
|
|
38
|
+
return script;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const origEditor = process.env.EDITOR;
|
|
42
|
+
const origVisual = process.env.VISUAL;
|
|
43
|
+
|
|
44
|
+
let h: ScriptedSessionHarness | undefined;
|
|
45
|
+
afterEach(async () => {
|
|
46
|
+
await h?.dispose();
|
|
47
|
+
h = undefined;
|
|
48
|
+
if (origEditor === undefined) delete process.env.EDITOR;
|
|
49
|
+
else process.env.EDITOR = origEditor;
|
|
50
|
+
if (origVisual === undefined) delete process.env.VISUAL;
|
|
51
|
+
else process.env.VISUAL = origVisual;
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
describe('/editor command (framework functional)', () => {
|
|
55
|
+
it(
|
|
56
|
+
'opens the editor via the handoff and returns the saved text',
|
|
57
|
+
async () => {
|
|
58
|
+
delete process.env.VISUAL;
|
|
59
|
+
process.env.EDITOR = installFakeEditor('composed in editor');
|
|
60
|
+
h = scriptedSession({
|
|
61
|
+
turns: [{ text: 'unused' }],
|
|
62
|
+
terminalHandoff: fakeHandoff(true),
|
|
63
|
+
commandModules: [createEditorCommandModule()],
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
const result = await h.command('editor', '');
|
|
67
|
+
|
|
68
|
+
expect(result?.success).toBe(true);
|
|
69
|
+
expect(result?.message).toBe('composed in editor');
|
|
70
|
+
expect((result?.data as { content: string }).content).toBe('composed in editor');
|
|
71
|
+
},
|
|
72
|
+
TEST_TIMEOUT,
|
|
73
|
+
);
|
|
74
|
+
|
|
75
|
+
it(
|
|
76
|
+
'is unavailable when there is no interactive terminal',
|
|
77
|
+
async () => {
|
|
78
|
+
process.env.EDITOR = installFakeEditor('unused');
|
|
79
|
+
h = scriptedSession({
|
|
80
|
+
turns: [{ text: 'unused' }],
|
|
81
|
+
commandModules: [createEditorCommandModule()],
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
const result = await h.command('editor', '');
|
|
85
|
+
|
|
86
|
+
expect(result?.success).toBe(false);
|
|
87
|
+
expect(result?.message).toMatch(/unavailable/i);
|
|
88
|
+
},
|
|
89
|
+
TEST_TIMEOUT,
|
|
90
|
+
);
|
|
91
|
+
});
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* TERM-004: `/editor` command module — a framework-level consumer of the terminal-handoff capability.
|
|
3
|
+
*/
|
|
4
|
+
import { EDITOR_COMMAND_DESCRIPTION, executeEditorCommand } from './editor-command.js';
|
|
5
|
+
|
|
6
|
+
import type { ICommandModule, ISystemCommand } from '@robota-sdk/agent-framework';
|
|
7
|
+
import type { ICommand, ICommandSource } from '@robota-sdk/agent-interface-transport';
|
|
8
|
+
|
|
9
|
+
export function createEditorCommandEntry(): ICommand {
|
|
10
|
+
return {
|
|
11
|
+
name: 'editor',
|
|
12
|
+
displayName: 'Editor',
|
|
13
|
+
description: EDITOR_COMMAND_DESCRIPTION,
|
|
14
|
+
source: 'editor',
|
|
15
|
+
modelInvocable: false,
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function createEditorSystemCommand(): ISystemCommand {
|
|
20
|
+
const entry = createEditorCommandEntry();
|
|
21
|
+
return {
|
|
22
|
+
name: entry.name,
|
|
23
|
+
displayName: entry.displayName,
|
|
24
|
+
description: entry.description,
|
|
25
|
+
requiresPermission: false,
|
|
26
|
+
userInvocable: true,
|
|
27
|
+
modelInvocable: false,
|
|
28
|
+
lifecycle: 'inline',
|
|
29
|
+
execute: executeEditorCommand,
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export class EditorCommandSource implements ICommandSource {
|
|
34
|
+
readonly name = 'editor';
|
|
35
|
+
|
|
36
|
+
getCommands(): ICommand[] {
|
|
37
|
+
return [createEditorCommandEntry()];
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export function createEditorCommandModule(): ICommandModule {
|
|
42
|
+
return {
|
|
43
|
+
name: 'agent-command-editor',
|
|
44
|
+
commandSources: [new EditorCommandSource()],
|
|
45
|
+
systemCommands: [createEditorSystemCommand()],
|
|
46
|
+
};
|
|
47
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* TERM-004: `/editor` — compose a message/prompt in `$EDITOR` via the terminal-handoff capability,
|
|
3
|
+
* then return the saved text. The temp file is pre-filled with any args and removed afterward.
|
|
4
|
+
*/
|
|
5
|
+
import { mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
|
|
6
|
+
import { tmpdir } from 'node:os';
|
|
7
|
+
import { join } from 'node:path';
|
|
8
|
+
|
|
9
|
+
import { resolveEditor } from './resolve-editor.js';
|
|
10
|
+
import { spawnInherited } from '../shell/spawn-inherited.js';
|
|
11
|
+
|
|
12
|
+
import type { ICommandHostContext } from '@robota-sdk/agent-framework';
|
|
13
|
+
import type { ICommandResult } from '@robota-sdk/agent-interface-transport';
|
|
14
|
+
|
|
15
|
+
export const EDITOR_COMMAND_DESCRIPTION =
|
|
16
|
+
'Compose a message in $EDITOR (optionally pre-filled with `/editor <text>`), then return it.';
|
|
17
|
+
|
|
18
|
+
export async function executeEditorCommand(
|
|
19
|
+
context: ICommandHostContext,
|
|
20
|
+
args: string,
|
|
21
|
+
): Promise<ICommandResult> {
|
|
22
|
+
if (context.canHandoffTerminal?.() !== true || context.runWithTerminal === undefined) {
|
|
23
|
+
return {
|
|
24
|
+
message: 'An editor is unavailable here (no interactive terminal).',
|
|
25
|
+
success: false,
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const editor = resolveEditor();
|
|
30
|
+
const dir = mkdtempSync(join(tmpdir(), 'robota-editor-'));
|
|
31
|
+
const file = join(dir, 'message.md');
|
|
32
|
+
writeFileSync(file, args ?? '', 'utf8');
|
|
33
|
+
|
|
34
|
+
try {
|
|
35
|
+
const exitCode = await context.runWithTerminal(async () =>
|
|
36
|
+
spawnInherited(editor.command, [...editor.args, file], context.getCwd()),
|
|
37
|
+
);
|
|
38
|
+
if (exitCode !== 0) {
|
|
39
|
+
return {
|
|
40
|
+
message: `Editor exited without saving (code ${exitCode}).`,
|
|
41
|
+
success: false,
|
|
42
|
+
data: { exitCode },
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
const content = readFileSync(file, 'utf8').replace(/\s+$/u, '');
|
|
46
|
+
if (content.length === 0) {
|
|
47
|
+
return { message: 'Editor closed with empty content; nothing composed.', success: false };
|
|
48
|
+
}
|
|
49
|
+
return { message: content, success: true, data: { content } };
|
|
50
|
+
} finally {
|
|
51
|
+
rmSync(dir, { recursive: true, force: true, maxRetries: 3, retryDelay: 20 });
|
|
52
|
+
}
|
|
53
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
export { resolveEditor, type IResolvedEditor } from './resolve-editor.js';
|
|
2
|
+
export { executeEditorCommand, EDITOR_COMMAND_DESCRIPTION } from './editor-command.js';
|
|
3
|
+
export {
|
|
4
|
+
EditorCommandSource,
|
|
5
|
+
createEditorCommandEntry,
|
|
6
|
+
createEditorCommandModule,
|
|
7
|
+
} from './editor-command-module.js';
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* TERM-004: editor-selection seam. Resolves `$VISUAL` → `$EDITOR` → `vi` (POSIX-first; a Windows
|
|
3
|
+
* default is a TERM-007 concern). Splits on whitespace so `"code -w"` / `"subl -w"` style values work.
|
|
4
|
+
*/
|
|
5
|
+
export interface IResolvedEditor {
|
|
6
|
+
command: string;
|
|
7
|
+
args: readonly string[];
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export function resolveEditor(): IResolvedEditor {
|
|
11
|
+
const visual = process.env.VISUAL?.trim();
|
|
12
|
+
const editor = process.env.EDITOR?.trim();
|
|
13
|
+
const chosen =
|
|
14
|
+
visual !== undefined && visual.length > 0
|
|
15
|
+
? visual
|
|
16
|
+
: editor !== undefined && editor.length > 0
|
|
17
|
+
? editor
|
|
18
|
+
: 'vi';
|
|
19
|
+
const parts = chosen.split(/\s+/).filter((p) => p.length > 0);
|
|
20
|
+
return { command: parts[0] ?? 'vi', args: parts.slice(1) };
|
|
21
|
+
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import type { ICommandHostContext } from '@robota-sdk/agent-framework';
|
|
2
3
|
import {
|
|
3
4
|
ExitCommandSource,
|
|
4
5
|
createExitCommandEntry,
|
|
@@ -6,6 +7,12 @@ import {
|
|
|
6
7
|
executeExitCommand,
|
|
7
8
|
} from '../index.js';
|
|
8
9
|
|
|
10
|
+
function contextWithAnswer(value: string): ICommandHostContext {
|
|
11
|
+
return {
|
|
12
|
+
getUserInteraction: () => ({ ask: async () => ({ type: 'answer', values: [value] }) }),
|
|
13
|
+
} as unknown as ICommandHostContext;
|
|
14
|
+
}
|
|
15
|
+
|
|
9
16
|
describe('exit command module', () => {
|
|
10
17
|
it('provides command metadata and executable registration from one module', () => {
|
|
11
18
|
const entry = createExitCommandEntry();
|
|
@@ -23,8 +30,8 @@ describe('exit command module', () => {
|
|
|
23
30
|
expect(module.commandSources?.flatMap((source) => source.getCommands())).toEqual([entry]);
|
|
24
31
|
});
|
|
25
32
|
|
|
26
|
-
it('
|
|
27
|
-
const result = executeExitCommand({} as never, '');
|
|
33
|
+
it('proceeds to exit with no renderer attached (no human to confirm)', async () => {
|
|
34
|
+
const result = await executeExitCommand({} as never, '');
|
|
28
35
|
|
|
29
36
|
expect(result).toEqual({
|
|
30
37
|
success: true,
|
|
@@ -32,4 +39,16 @@ describe('exit command module', () => {
|
|
|
32
39
|
effects: [{ type: 'session-exit-requested' }],
|
|
33
40
|
});
|
|
34
41
|
});
|
|
42
|
+
|
|
43
|
+
it('confirms before exiting and proceeds on yes (CMD-004)', async () => {
|
|
44
|
+
const result = await executeExitCommand(contextWithAnswer('yes'), '');
|
|
45
|
+
expect(result.effects).toEqual([{ type: 'session-exit-requested' }]);
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
it('cancels the exit when the user declines (CMD-004)', async () => {
|
|
49
|
+
const result = await executeExitCommand(contextWithAnswer('no'), '');
|
|
50
|
+
expect(result.success).toBe(true);
|
|
51
|
+
expect(result.message).toBe('Exit cancelled.');
|
|
52
|
+
expect(result.effects).toBeUndefined();
|
|
53
|
+
});
|
|
35
54
|
});
|
|
@@ -3,11 +3,7 @@ import { EXIT_COMMAND_DESCRIPTION } from '@robota-sdk/agent-framework';
|
|
|
3
3
|
import { executeExitCommand } from './exit-command.js';
|
|
4
4
|
|
|
5
5
|
import type { ICommandModule, ISystemCommand } from '@robota-sdk/agent-framework';
|
|
6
|
-
import type {
|
|
7
|
-
ICommand,
|
|
8
|
-
ICommandInteractionHint,
|
|
9
|
-
ICommandSource,
|
|
10
|
-
} from '@robota-sdk/agent-interface-transport';
|
|
6
|
+
import type { ICommand, ICommandSource } from '@robota-sdk/agent-interface-transport';
|
|
11
7
|
|
|
12
8
|
export function createExitCommandEntry(): ICommand {
|
|
13
9
|
return {
|
|
@@ -41,15 +37,10 @@ export class ExitCommandSource implements ICommandSource {
|
|
|
41
37
|
}
|
|
42
38
|
}
|
|
43
39
|
|
|
44
|
-
const EXIT_INTERACTION_HINTS: Record<string, ICommandInteractionHint> = {
|
|
45
|
-
exit: { type: 'confirm', message: 'Exit the session?' },
|
|
46
|
-
};
|
|
47
|
-
|
|
48
40
|
export function createExitCommandModule(): ICommandModule {
|
|
49
41
|
return {
|
|
50
42
|
name: 'agent-command-exit',
|
|
51
43
|
commandSources: [new ExitCommandSource()],
|
|
52
44
|
systemCommands: [createExitSystemCommand()],
|
|
53
|
-
interactionHints: EXIT_INTERACTION_HINTS,
|
|
54
45
|
};
|
|
55
46
|
}
|
package/src/exit/exit-command.ts
CHANGED
|
@@ -1,9 +1,23 @@
|
|
|
1
|
+
import { confirmAction, isConfirmed } from '@robota-sdk/agent-core';
|
|
1
2
|
import { createSessionExitRequestedEffect } from '@robota-sdk/agent-framework';
|
|
2
3
|
|
|
3
4
|
import type { ICommandHostContext } from '@robota-sdk/agent-framework';
|
|
4
5
|
import type { ICommandResult } from '@robota-sdk/agent-interface-transport';
|
|
5
6
|
|
|
6
|
-
export function executeExitCommand(
|
|
7
|
+
export async function executeExitCommand(
|
|
8
|
+
context: ICommandHostContext,
|
|
9
|
+
_args: string,
|
|
10
|
+
): Promise<ICommandResult> {
|
|
11
|
+
// Confirm only when an interactive renderer is attached. With no human (headless/automation) the
|
|
12
|
+
// user explicitly invoked /exit, so proceed — the confirm is an interactive safety prompt, not a gate.
|
|
13
|
+
const ui = context.getUserInteraction?.();
|
|
14
|
+
if (ui) {
|
|
15
|
+
const response = await ui.ask(confirmAction('exit', 'Exit the session?'));
|
|
16
|
+
if (!isConfirmed(response)) {
|
|
17
|
+
return { success: true, message: 'Exit cancelled.' };
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
7
21
|
return {
|
|
8
22
|
success: true,
|
|
9
23
|
message: 'Exit requested.',
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import { describe, it, expect, vi } from 'vitest';
|
|
2
|
+
|
|
3
|
+
import { executeGoalCommand } from '../goal-command.js';
|
|
4
|
+
|
|
5
|
+
import type { ICommandHostContext } from '@robota-sdk/agent-framework';
|
|
6
|
+
import type { IGoalState } from '@robota-sdk/agent-interface-transport';
|
|
7
|
+
|
|
8
|
+
function goal(overrides: Partial<IGoalState> = {}): IGoalState {
|
|
9
|
+
return {
|
|
10
|
+
id: 'g1',
|
|
11
|
+
objective: 'write a file',
|
|
12
|
+
status: 'active',
|
|
13
|
+
iterations: 1,
|
|
14
|
+
maxIterations: 25,
|
|
15
|
+
startedAt: '2026-06-27T00:00:00.000Z',
|
|
16
|
+
progress: [{ iteration: 1, signal: 'continue', reason: 'started' }],
|
|
17
|
+
...overrides,
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function host(overrides: Partial<ICommandHostContext> = {}): ICommandHostContext {
|
|
22
|
+
return overrides as unknown as ICommandHostContext;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
describe('executeGoalCommand', () => {
|
|
26
|
+
it('assigns a goal from the objective text', async () => {
|
|
27
|
+
const setGoal = vi.fn().mockResolvedValue(goal());
|
|
28
|
+
const result = await executeGoalCommand(host({ setGoal }), 'write a file');
|
|
29
|
+
expect(setGoal).toHaveBeenCalledWith('write a file');
|
|
30
|
+
expect(result.success).toBe(true);
|
|
31
|
+
expect(result.message).toContain('pursuing autonomously');
|
|
32
|
+
expect(result.data).toMatchObject({ goalId: 'g1' });
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
it('shows status when given "status"', async () => {
|
|
36
|
+
const result = await executeGoalCommand(host({ getGoalState: () => goal() }), 'status');
|
|
37
|
+
expect(result.success).toBe(true);
|
|
38
|
+
expect(result.message).toContain('write a file');
|
|
39
|
+
expect(result.message).toContain('Iterations: 1 / 25');
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
it('reports no goal when status is requested with none set', async () => {
|
|
43
|
+
const result = await executeGoalCommand(host({ getGoalState: () => null }), 'status');
|
|
44
|
+
expect(result.message).toBe('No goal is set.');
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
it('cancels an active goal', async () => {
|
|
48
|
+
const cancelGoal = vi
|
|
49
|
+
.fn()
|
|
50
|
+
.mockReturnValue(goal({ status: 'stopped', stopReason: 'cancelled' }));
|
|
51
|
+
const result = await executeGoalCommand(host({ cancelGoal }), 'cancel');
|
|
52
|
+
expect(cancelGoal).toHaveBeenCalled();
|
|
53
|
+
expect(result.success).toBe(true);
|
|
54
|
+
expect(result.message).toContain('Goal cancelled');
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
it('reports failure when cancelling with no active goal', async () => {
|
|
58
|
+
const result = await executeGoalCommand(host({ cancelGoal: () => null }), 'stop');
|
|
59
|
+
expect(result.success).toBe(false);
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
it('shows usage on an empty argument', async () => {
|
|
63
|
+
const result = await executeGoalCommand(host({}), '');
|
|
64
|
+
expect(result.message).toContain('Usage');
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
it('surfaces an invalid-objective error as a failed result (not a crash)', async () => {
|
|
68
|
+
const setGoal = vi
|
|
69
|
+
.fn()
|
|
70
|
+
.mockRejectedValue(new Error('A goal objective must be a non-empty string.'));
|
|
71
|
+
const result = await executeGoalCommand(host({ setGoal }), 'x');
|
|
72
|
+
expect(result.success).toBe(false);
|
|
73
|
+
expect(result.message).toContain('non-empty');
|
|
74
|
+
});
|
|
75
|
+
});
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* GOAL-001: `/goal` command module registration.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { executeGoalCommand, GOAL_COMMAND_DESCRIPTION } from './goal-command.js';
|
|
6
|
+
|
|
7
|
+
import type { ICommandModule, ISystemCommand } from '@robota-sdk/agent-framework';
|
|
8
|
+
import type { ICommand, ICommandSource } from '@robota-sdk/agent-interface-transport';
|
|
9
|
+
|
|
10
|
+
export function createGoalCommandEntry(): ICommand {
|
|
11
|
+
return {
|
|
12
|
+
name: 'goal',
|
|
13
|
+
displayName: 'Autonomous Goal',
|
|
14
|
+
description: GOAL_COMMAND_DESCRIPTION,
|
|
15
|
+
source: 'goal',
|
|
16
|
+
modelInvocable: false,
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function createGoalSystemCommand(): ISystemCommand {
|
|
21
|
+
const entry = createGoalCommandEntry();
|
|
22
|
+
return {
|
|
23
|
+
name: entry.name,
|
|
24
|
+
displayName: entry.displayName,
|
|
25
|
+
description: entry.description,
|
|
26
|
+
requiresPermission: false,
|
|
27
|
+
userInvocable: true,
|
|
28
|
+
modelInvocable: false,
|
|
29
|
+
lifecycle: 'inline',
|
|
30
|
+
execute: executeGoalCommand,
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export class GoalCommandSource implements ICommandSource {
|
|
35
|
+
readonly name = 'goal';
|
|
36
|
+
|
|
37
|
+
getCommands(): ICommand[] {
|
|
38
|
+
return [createGoalCommandEntry()];
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export function createGoalCommandModule(): ICommandModule {
|
|
43
|
+
return {
|
|
44
|
+
name: 'agent-command-goal',
|
|
45
|
+
commandSources: [new GoalCommandSource()],
|
|
46
|
+
systemCommands: [createGoalSystemCommand()],
|
|
47
|
+
};
|
|
48
|
+
}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* GOAL-001: the `/goal` slash command. Assigns an autonomous objective the agent pursues across
|
|
3
|
+
* turns until satisfied or a bound fires. Delegates to the framework-owned goal controller via the
|
|
4
|
+
* command host context; all loop logic lives in agent-framework.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import type { ICommandHostContext } from '@robota-sdk/agent-framework';
|
|
8
|
+
import type { ICommandResult, IGoalState } from '@robota-sdk/agent-interface-transport';
|
|
9
|
+
|
|
10
|
+
export const GOAL_COMMAND_DESCRIPTION =
|
|
11
|
+
'Assign an autonomous goal the agent pursues across turns until satisfied.';
|
|
12
|
+
|
|
13
|
+
const GOAL_COMMAND_USAGE =
|
|
14
|
+
'Usage:\n /goal <objective> assign a goal and pursue it autonomously\n' +
|
|
15
|
+
' /goal status show the current goal and progress\n' +
|
|
16
|
+
' /goal cancel stop the current goal';
|
|
17
|
+
|
|
18
|
+
function formatGoalState(goal: IGoalState): string {
|
|
19
|
+
const lines = [
|
|
20
|
+
`Goal: ${goal.objective}`,
|
|
21
|
+
`Status: ${goal.status}${goal.stopReason ? ` (${goal.stopReason})` : ''}`,
|
|
22
|
+
`Iterations: ${goal.iterations} / ${goal.maxIterations}`,
|
|
23
|
+
];
|
|
24
|
+
const last = goal.progress[goal.progress.length - 1];
|
|
25
|
+
if (last?.reason) lines.push(`Latest: ${last.reason}`);
|
|
26
|
+
return lines.join('\n');
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export async function executeGoalCommand(
|
|
30
|
+
context: ICommandHostContext,
|
|
31
|
+
args: string,
|
|
32
|
+
): Promise<ICommandResult> {
|
|
33
|
+
const trimmed = args.trim();
|
|
34
|
+
const verb = trimmed.split(/\s+/)[0]?.toLowerCase() ?? '';
|
|
35
|
+
|
|
36
|
+
if (trimmed.length === 0 || verb === 'help') {
|
|
37
|
+
return { message: GOAL_COMMAND_USAGE, success: true };
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
if (verb === 'status') {
|
|
41
|
+
const goal = context.getGoalState?.() ?? null;
|
|
42
|
+
return goal
|
|
43
|
+
? { message: formatGoalState(goal), success: true, data: { status: goal.status } }
|
|
44
|
+
: { message: 'No goal is set.', success: true };
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
if (verb === 'cancel' || verb === 'stop') {
|
|
48
|
+
const stopped = context.cancelGoal?.() ?? null;
|
|
49
|
+
return stopped
|
|
50
|
+
? { message: `Goal cancelled: ${stopped.objective}`, success: true }
|
|
51
|
+
: { message: 'No active goal to cancel.', success: false };
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
if (!context.setGoal) {
|
|
55
|
+
return { message: 'Goal pursuit is not available in this session.', success: false };
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
let goal: IGoalState;
|
|
59
|
+
try {
|
|
60
|
+
goal = await context.setGoal(trimmed);
|
|
61
|
+
} catch (error) {
|
|
62
|
+
// allow-fallback: surface an invalid-objective error to the user as a failed command result, not a crash
|
|
63
|
+
return { message: error instanceof Error ? error.message : String(error), success: false };
|
|
64
|
+
}
|
|
65
|
+
return {
|
|
66
|
+
message: `Goal set — pursuing autonomously (up to ${goal.maxIterations} iterations):\n${goal.objective}`,
|
|
67
|
+
success: true,
|
|
68
|
+
data: { goalId: goal.id },
|
|
69
|
+
};
|
|
70
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
export * from './agent/index.js';
|
|
2
2
|
export * from './default/index.js';
|
|
3
3
|
export * from './background/index.js';
|
|
4
|
+
export * from './goal/index.js';
|
|
4
5
|
export * from './compact/index.js';
|
|
5
6
|
export * from './context/index.js';
|
|
7
|
+
export * from './editor/index.js';
|
|
6
8
|
export * from './exit/index.js';
|
|
7
9
|
export * from './help/index.js';
|
|
8
10
|
export * from './language/index.js';
|
|
@@ -11,6 +13,7 @@ export * from './mode/index.js';
|
|
|
11
13
|
export * from './permissions/index.js';
|
|
12
14
|
export * from './plugin/index.js';
|
|
13
15
|
export * from './preset/index.js';
|
|
16
|
+
export * from './shell/index.js';
|
|
14
17
|
export * from './provider/index.js';
|
|
15
18
|
export * from './reset/index.js';
|
|
16
19
|
export * from './rewind/index.js';
|