@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.
Files changed (53) hide show
  1. package/LICENSE +661 -21
  2. package/README.md +12 -6
  3. package/dist/node/index.cjs +38 -33
  4. package/dist/node/index.d.ts +68 -6
  5. package/dist/node/index.d.ts.map +1 -1
  6. package/dist/node/index.js +40 -35
  7. package/dist/node/index.js.map +1 -1
  8. package/package.json +7 -7
  9. package/src/agent/agent-command-parser.ts +1 -1
  10. package/src/agent/agent-command.ts +2 -1
  11. package/src/background/__tests__/background-command-module.test.ts +2 -5
  12. package/src/default/__tests__/default-command-modules.test.ts +5 -2
  13. package/src/default/default-command-modules.ts +6 -0
  14. package/src/editor/__tests__/editor-command-functional.test.ts +91 -0
  15. package/src/editor/editor-command-module.ts +47 -0
  16. package/src/editor/editor-command.ts +53 -0
  17. package/src/editor/index.ts +7 -0
  18. package/src/editor/resolve-editor.ts +21 -0
  19. package/src/exit/__tests__/exit-command-module.test.ts +21 -2
  20. package/src/exit/exit-command-module.ts +1 -10
  21. package/src/exit/exit-command.ts +15 -1
  22. package/src/goal/__tests__/goal-command.test.ts +75 -0
  23. package/src/goal/goal-command-module.ts +48 -0
  24. package/src/goal/goal-command.ts +70 -0
  25. package/src/goal/index.ts +6 -0
  26. package/src/index.ts +3 -0
  27. package/src/language/__tests__/language-command-module.test.ts +16 -0
  28. package/src/language/language-command-module.ts +1 -18
  29. package/src/language/language-command.ts +35 -9
  30. package/src/mode/__tests__/mode-command-module.test.ts +34 -0
  31. package/src/mode/mode-command-module.ts +1 -18
  32. package/src/mode/mode-command.ts +31 -8
  33. package/src/preset/__tests__/preset-command-module.test.ts +36 -0
  34. package/src/preset/preset-command-module.ts +1 -18
  35. package/src/preset/preset-command.ts +37 -8
  36. package/src/provider/__tests__/org-policy.test.ts +19 -13
  37. package/src/provider/__tests__/provider-command-module.test.ts +151 -80
  38. package/src/provider/__tests__/scripted-interaction.ts +28 -0
  39. package/src/provider/provider-command-execution.ts +67 -50
  40. package/src/provider/provider-command-module.ts +3 -19
  41. package/src/provider/provider-command-profile-lifecycle.ts +52 -72
  42. package/src/provider/provider-command-profile-operations.ts +15 -51
  43. package/src/provider/provider-command-profile.ts +44 -49
  44. package/src/provider/provider-command-setup.ts +56 -51
  45. package/src/session/__tests__/session-command-module.test.ts +38 -0
  46. package/src/session/session-command-module.ts +1 -10
  47. package/src/session/session-command.ts +14 -1
  48. package/src/shell/__tests__/shell-command-functional.test.ts +96 -0
  49. package/src/shell/index.ts +8 -0
  50. package/src/shell/resolve-shell.ts +25 -0
  51. package/src/shell/shell-command-module.ts +47 -0
  52. package/src/shell/shell-command.ts +44 -0
  53. package/src/shell/spawn-inherited.ts +32 -0
@@ -0,0 +1,96 @@
1
+ /**
2
+ * TERM-003: `/shell` — framework functional test.
3
+ *
4
+ * Drives the command through a REAL InteractiveSession (scripted provider) with an injected fake
5
+ * terminal-handoff capability: the command routes a one-shot shell command through `runWithTerminal`
6
+ * (suspend → spawn shell with inherited stdio → restore) and reports its exit code; and is
7
+ * unavailable when no interactive terminal exists. (Real interactive input is manual — TERM-002.)
8
+ */
9
+ import { afterEach, describe, expect, it } from 'vitest';
10
+
11
+ import { scriptedSession, type ScriptedSessionHarness } from '@robota-sdk/agent-framework/testing';
12
+
13
+ import { createShellCommandModule } from '../shell-command-module.js';
14
+
15
+ import type { ITerminalHandoff } from '@robota-sdk/agent-interface-transport';
16
+
17
+ const TEST_TIMEOUT = 20_000;
18
+
19
+ function fakeHandoff(canHandoff: boolean): ITerminalHandoff & { readonly events: string[] } {
20
+ const events: string[] = [];
21
+ return {
22
+ events,
23
+ canHandoffTerminal: canHandoff,
24
+ async runWithTerminal<T>(fn: () => Promise<T>): Promise<T> {
25
+ events.push('suspend');
26
+ try {
27
+ return await fn();
28
+ } finally {
29
+ events.push('restore');
30
+ }
31
+ },
32
+ };
33
+ }
34
+
35
+ let h: ScriptedSessionHarness | undefined;
36
+ afterEach(async () => {
37
+ await h?.dispose();
38
+ h = undefined;
39
+ });
40
+
41
+ describe('/shell command (framework functional)', () => {
42
+ it(
43
+ 'runs a one-shot command through the handoff and returns exit code 0',
44
+ async () => {
45
+ const handoff = fakeHandoff(true);
46
+ h = scriptedSession({
47
+ turns: [{ text: 'unused' }],
48
+ terminalHandoff: handoff,
49
+ commandModules: [createShellCommandModule()],
50
+ });
51
+
52
+ const result = await h.command('shell', 'exit 0');
53
+
54
+ expect(result?.success).toBe(true);
55
+ expect((result?.data as { exitCode: number }).exitCode).toBe(0);
56
+ // The command went through the transport handoff (suspend → run → restore).
57
+ expect(handoff.events).toEqual(['suspend', 'restore']);
58
+ },
59
+ TEST_TIMEOUT,
60
+ );
61
+
62
+ it(
63
+ 'reports a non-zero exit code from the child',
64
+ async () => {
65
+ const handoff = fakeHandoff(true);
66
+ h = scriptedSession({
67
+ turns: [{ text: 'unused' }],
68
+ terminalHandoff: handoff,
69
+ commandModules: [createShellCommandModule()],
70
+ });
71
+
72
+ const result = await h.command('shell', 'exit 3');
73
+
74
+ expect(result?.success).toBe(false);
75
+ expect((result?.data as { exitCode: number }).exitCode).toBe(3);
76
+ },
77
+ TEST_TIMEOUT,
78
+ );
79
+
80
+ it(
81
+ 'is unavailable when there is no interactive terminal',
82
+ async () => {
83
+ // No terminalHandoff injected → canHandoffTerminal() is false.
84
+ h = scriptedSession({
85
+ turns: [{ text: 'unused' }],
86
+ commandModules: [createShellCommandModule()],
87
+ });
88
+
89
+ const result = await h.command('shell', 'exit 0');
90
+
91
+ expect(result?.success).toBe(false);
92
+ expect(result?.message).toMatch(/unavailable/i);
93
+ },
94
+ TEST_TIMEOUT,
95
+ );
96
+ });
@@ -0,0 +1,8 @@
1
+ export { resolveShell, type IResolvedShell } from './resolve-shell.js';
2
+ export { spawnInherited } from './spawn-inherited.js';
3
+ export { executeShellCommand, SHELL_COMMAND_DESCRIPTION } from './shell-command.js';
4
+ export {
5
+ ShellCommandSource,
6
+ createShellCommandEntry,
7
+ createShellCommandModule,
8
+ } from './shell-command-module.js';
@@ -0,0 +1,25 @@
1
+ /**
2
+ * TERM-003 shell-selection seam, now backed by the cross-platform SSOT resolver in agent-core
3
+ * (TERM-008). Keep all shell choice behind this function so the call sites never change; the
4
+ * per-platform logic (POSIX `$SHELL`/`sh`, Windows PowerShell) lives once in `resolvePlatformShell`.
5
+ */
6
+ import { resolvePlatformShell } from '@robota-sdk/agent-core';
7
+
8
+ export interface IResolvedShell {
9
+ /** Executable to spawn. */
10
+ command: string;
11
+ /** Args for an interactive shell session (drop-to-shell). */
12
+ interactiveArgs: readonly string[];
13
+ /** Args to run a single command string non-interactively. */
14
+ commandArgs(command: string): readonly string[];
15
+ }
16
+
17
+ /** Resolve the interactive shell for the current platform via the agent-core SSOT resolver. */
18
+ export function resolveShell(): IResolvedShell {
19
+ const shell = resolvePlatformShell();
20
+ return {
21
+ command: shell.command,
22
+ interactiveArgs: shell.interactiveArgs,
23
+ commandArgs: (cmd: string) => shell.commandArgs(cmd),
24
+ };
25
+ }
@@ -0,0 +1,47 @@
1
+ /**
2
+ * TERM-003: `/shell` command module — a framework-level consumer of the terminal-handoff capability.
3
+ */
4
+ import { executeShellCommand, SHELL_COMMAND_DESCRIPTION } from './shell-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 createShellCommandEntry(): ICommand {
10
+ return {
11
+ name: 'shell',
12
+ displayName: 'Shell',
13
+ description: SHELL_COMMAND_DESCRIPTION,
14
+ source: 'shell',
15
+ modelInvocable: false,
16
+ };
17
+ }
18
+
19
+ function createShellSystemCommand(): ISystemCommand {
20
+ const entry = createShellCommandEntry();
21
+ return {
22
+ name: entry.name,
23
+ displayName: entry.displayName,
24
+ description: entry.description,
25
+ requiresPermission: true,
26
+ userInvocable: true,
27
+ modelInvocable: false,
28
+ lifecycle: 'inline',
29
+ execute: executeShellCommand,
30
+ };
31
+ }
32
+
33
+ export class ShellCommandSource implements ICommandSource {
34
+ readonly name = 'shell';
35
+
36
+ getCommands(): ICommand[] {
37
+ return [createShellCommandEntry()];
38
+ }
39
+ }
40
+
41
+ export function createShellCommandModule(): ICommandModule {
42
+ return {
43
+ name: 'agent-command-shell',
44
+ commandSources: [new ShellCommandSource()],
45
+ systemCommands: [createShellSystemCommand()],
46
+ };
47
+ }
@@ -0,0 +1,44 @@
1
+ /**
2
+ * TERM-003: `/shell` — hand the real terminal to an interactive subshell (or run a single command
3
+ * interactively), then return to the agent session. Uses the framework terminal-handoff capability;
4
+ * the shell choice goes through the `resolveShell()` seam (macOS/Linux first).
5
+ */
6
+ import { resolveShell } from './resolve-shell.js';
7
+ import { spawnInherited } from './spawn-inherited.js';
8
+
9
+ import type { ICommandHostContext } from '@robota-sdk/agent-framework';
10
+ import type { ICommandResult } from '@robota-sdk/agent-interface-transport';
11
+
12
+ export const SHELL_COMMAND_DESCRIPTION =
13
+ 'Drop to an interactive shell (or run `/shell <command>` interactively), then return to the agent.';
14
+
15
+ export async function executeShellCommand(
16
+ context: ICommandHostContext,
17
+ args: string,
18
+ ): Promise<ICommandResult> {
19
+ if (context.canHandoffTerminal?.() !== true || context.runWithTerminal === undefined) {
20
+ return {
21
+ message: 'An interactive shell is unavailable here (no interactive terminal).',
22
+ success: false,
23
+ };
24
+ }
25
+
26
+ const shell = resolveShell();
27
+ const cwd = context.getCwd();
28
+ const command = args.trim();
29
+
30
+ const exitCode = await context.runWithTerminal(async () =>
31
+ command.length > 0
32
+ ? spawnInherited(shell.command, shell.commandArgs(command), cwd)
33
+ : spawnInherited(shell.command, shell.interactiveArgs, cwd),
34
+ );
35
+
36
+ return {
37
+ message:
38
+ command.length > 0
39
+ ? `Command exited (code ${exitCode}).`
40
+ : `Shell session ended (code ${exitCode}).`,
41
+ success: exitCode === 0,
42
+ data: { exitCode },
43
+ };
44
+ }
@@ -0,0 +1,32 @@
1
+ /**
2
+ * Pure-Node spawn with the real terminal inherited (interactive input + output). Used inside a
3
+ * terminal-handoff `runWithTerminal(fn)` — the framework/transport own the suspend/restore; this just
4
+ * runs the child attached to the real TTY and resolves with its exit code.
5
+ */
6
+ import { spawn } from 'node:child_process';
7
+ import { constants } from 'node:os';
8
+
9
+ export function spawnInherited(
10
+ command: string,
11
+ args: readonly string[],
12
+ cwd: string,
13
+ ): Promise<number> {
14
+ return new Promise<number>((resolve, reject) => {
15
+ const child = spawn(command, [...args], { cwd, stdio: 'inherit', env: process.env });
16
+ child.on('error', reject);
17
+ child.on('exit', (code, signal) => {
18
+ // RUNTIME-53: a signal-terminated child reports code=null; resolving 0 there is a false
19
+ // success. Translate to the shell convention 128 + signal number so callers see the failure.
20
+ if (code !== null) {
21
+ resolve(code);
22
+ return;
23
+ }
24
+ if (signal) {
25
+ const signalNumber = constants.signals[signal as keyof typeof constants.signals];
26
+ resolve(128 + (signalNumber ?? 0));
27
+ return;
28
+ }
29
+ resolve(0);
30
+ });
31
+ });
32
+ }