@robota-sdk/agent-command 3.0.0-beta.75 → 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 (55) hide show
  1. package/LICENSE +661 -21
  2. package/README.md +12 -6
  3. package/dist/node/index.cjs +40 -35
  4. package/dist/node/index.d.ts +69 -7
  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/context/context-command.ts +4 -4
  13. package/src/default/__tests__/default-command-modules.test.ts +5 -2
  14. package/src/default/default-command-modules.ts +6 -0
  15. package/src/editor/__tests__/editor-command-functional.test.ts +91 -0
  16. package/src/editor/editor-command-module.ts +47 -0
  17. package/src/editor/editor-command.ts +53 -0
  18. package/src/editor/index.ts +7 -0
  19. package/src/editor/resolve-editor.ts +21 -0
  20. package/src/exit/__tests__/exit-command-module.test.ts +21 -2
  21. package/src/exit/exit-command-module.ts +1 -10
  22. package/src/exit/exit-command.ts +15 -1
  23. package/src/goal/__tests__/goal-command.test.ts +75 -0
  24. package/src/goal/goal-command-module.ts +48 -0
  25. package/src/goal/goal-command.ts +70 -0
  26. package/src/goal/index.ts +6 -0
  27. package/src/index.ts +3 -0
  28. package/src/language/__tests__/language-command-module.test.ts +16 -0
  29. package/src/language/language-command-module.ts +1 -18
  30. package/src/language/language-command.ts +35 -9
  31. package/src/mode/__tests__/mode-command-module.test.ts +34 -0
  32. package/src/mode/mode-command-module.ts +1 -18
  33. package/src/mode/mode-command.ts +31 -8
  34. package/src/preset/__tests__/preset-command-module.test.ts +43 -7
  35. package/src/preset/preset-command-module.ts +1 -18
  36. package/src/preset/preset-command.ts +42 -10
  37. package/src/provider/__tests__/org-policy.test.ts +19 -13
  38. package/src/provider/__tests__/provider-command-module.test.ts +151 -80
  39. package/src/provider/__tests__/scripted-interaction.ts +28 -0
  40. package/src/provider/provider-command-execution.ts +67 -50
  41. package/src/provider/provider-command-module.ts +3 -19
  42. package/src/provider/provider-command-profile-lifecycle.ts +52 -72
  43. package/src/provider/provider-command-profile-operations.ts +15 -51
  44. package/src/provider/provider-command-profile.ts +44 -49
  45. package/src/provider/provider-command-setup.ts +56 -51
  46. package/src/session/__tests__/session-command-module.test.ts +38 -0
  47. package/src/session/model-pricing.ts +7 -60
  48. package/src/session/session-command-module.ts +1 -10
  49. package/src/session/session-command.ts +14 -1
  50. package/src/shell/__tests__/shell-command-functional.test.ts +96 -0
  51. package/src/shell/index.ts +8 -0
  52. package/src/shell/resolve-shell.ts +25 -0
  53. package/src/shell/shell-command-module.ts +47 -0
  54. package/src/shell/shell-command.ts +44 -0
  55. package/src/shell/spawn-inherited.ts +32 -0
@@ -1,69 +1,16 @@
1
- /** USD prices per 1,000,000 tokens (as of May 2026 — update when providers change rates). */
2
- interface IModelPrice {
3
- inputPerMillion: number;
4
- outputPerMillion: number;
5
- }
6
-
7
- const MODEL_PRICES: Record<string, IModelPrice> = {
8
- // Anthropic Claude 4
9
- 'claude-opus-4-7': { inputPerMillion: 15, outputPerMillion: 75 },
10
- 'claude-opus-4-5': { inputPerMillion: 15, outputPerMillion: 75 },
11
- 'claude-sonnet-4-6': { inputPerMillion: 3, outputPerMillion: 15 },
12
- 'claude-sonnet-4-5': { inputPerMillion: 3, outputPerMillion: 15 },
13
- 'claude-haiku-4-5': { inputPerMillion: 0.8, outputPerMillion: 4 },
14
- // Anthropic Claude 3
15
- 'claude-3-5-sonnet-20241022': { inputPerMillion: 3, outputPerMillion: 15 },
16
- 'claude-3-5-haiku-20241022': { inputPerMillion: 0.8, outputPerMillion: 4 },
17
- 'claude-3-opus-20240229': { inputPerMillion: 15, outputPerMillion: 75 },
18
- // OpenAI
19
- 'gpt-4o': { inputPerMillion: 2.5, outputPerMillion: 10 },
20
- 'gpt-4o-mini': { inputPerMillion: 0.15, outputPerMillion: 0.6 },
21
- o1: { inputPerMillion: 15, outputPerMillion: 60 },
22
- 'o1-mini': { inputPerMillion: 3, outputPerMillion: 12 },
23
- o3: { inputPerMillion: 10, outputPerMillion: 40 },
24
- 'o3-mini': { inputPerMillion: 1.1, outputPerMillion: 4.4 },
25
- // DeepSeek
26
- 'deepseek-chat': { inputPerMillion: 0.14, outputPerMillion: 0.28 },
27
- 'deepseek-reasoner': { inputPerMillion: 0.55, outputPerMillion: 2.19 },
28
- // Google Gemini
29
- 'gemini-2.0-flash': { inputPerMillion: 0.1, outputPerMillion: 0.4 },
30
- 'gemini-2.0-flash-thinking': { inputPerMillion: 0.35, outputPerMillion: 3.5 },
31
- 'gemini-1.5-pro': { inputPerMillion: 1.25, outputPerMillion: 5 },
32
- 'gemini-1.5-flash': { inputPerMillion: 0.075, outputPerMillion: 0.3 },
33
- };
34
-
35
- const PATTERN_PRICES: Array<{ pattern: RegExp; price: IModelPrice }> = [
36
- { pattern: /claude-opus/i, price: { inputPerMillion: 15, outputPerMillion: 75 } },
37
- { pattern: /claude-sonnet/i, price: { inputPerMillion: 3, outputPerMillion: 15 } },
38
- { pattern: /claude-haiku/i, price: { inputPerMillion: 0.8, outputPerMillion: 4 } },
39
- { pattern: /gpt-4o-mini/i, price: { inputPerMillion: 0.15, outputPerMillion: 0.6 } },
40
- { pattern: /gpt-4/i, price: { inputPerMillion: 2.5, outputPerMillion: 10 } },
41
- { pattern: /deepseek/i, price: { inputPerMillion: 0.14, outputPerMillion: 0.28 } },
42
- { pattern: /gemini-2/i, price: { inputPerMillion: 0.1, outputPerMillion: 0.4 } },
43
- { pattern: /gemini-1/i, price: { inputPerMillion: 1.25, outputPerMillion: 5 } },
44
- ];
45
-
46
- function lookupPrice(modelId: string): IModelPrice | undefined {
47
- const exact = MODEL_PRICES[modelId];
48
- if (exact) return exact;
49
- for (const { pattern, price } of PATTERN_PRICES) {
50
- if (pattern.test(modelId)) return price;
51
- }
52
- return undefined;
53
- }
1
+ import { calculateModelCost } from '@robota-sdk/agent-core';
54
2
 
55
- /** Returns USD cost, or undefined if the model is not in the pricing table. */
3
+ /**
4
+ * Returns USD cost, or undefined if the model is not in the pricing table.
5
+ * Delegates to the agent-core pricing SSOT (`calculateModelCost`); this package owns only the
6
+ * command-facing display helpers below.
7
+ */
56
8
  export function calculateCost(
57
9
  modelId: string,
58
10
  inputTokens: number,
59
11
  outputTokens: number,
60
12
  ): number | undefined {
61
- const price = lookupPrice(modelId);
62
- if (!price) return undefined;
63
- return (
64
- (inputTokens / 1_000_000) * price.inputPerMillion +
65
- (outputTokens / 1_000_000) * price.outputPerMillion
66
- );
13
+ return calculateModelCost(modelId, inputTokens, outputTokens);
67
14
  }
68
15
 
69
16
  /** Format a USD amount for display (e.g. "$0.0043"). */
@@ -15,11 +15,7 @@ import {
15
15
  } from './session-command.js';
16
16
 
17
17
  import type { ICommandModule, ISystemCommand } from '@robota-sdk/agent-framework';
18
- import type {
19
- ICommand,
20
- ICommandInteractionHint,
21
- ICommandSource,
22
- } from '@robota-sdk/agent-interface-transport';
18
+ import type { ICommand, ICommandSource } from '@robota-sdk/agent-interface-transport';
23
19
 
24
20
  export function createClearCommandEntry(): ICommand {
25
21
  return {
@@ -155,10 +151,6 @@ export class SessionCommandSource implements ICommandSource {
155
151
  }
156
152
  }
157
153
 
158
- const SESSION_INTERACTION_HINTS: Record<string, ICommandInteractionHint> = {
159
- clear: { type: 'confirm', message: 'Clear conversation history?' },
160
- };
161
-
162
154
  export function createSessionCommandModule(): ICommandModule {
163
155
  return {
164
156
  name: 'agent-command-session',
@@ -170,6 +162,5 @@ export function createSessionCommandModule(): ICommandModule {
170
162
  createCostSystemCommand(),
171
163
  createValidateSessionSystemCommand(),
172
164
  ],
173
- interactionHints: SESSION_INTERACTION_HINTS,
174
165
  };
175
166
  }
@@ -1,6 +1,7 @@
1
1
  import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
2
2
  import { join } from 'node:path';
3
3
 
4
+ import { confirmAction, isConfirmed } from '@robota-sdk/agent-core';
4
5
  import {
5
6
  RENAME_COMMAND_USAGE,
6
7
  clearConversationHistory,
@@ -19,7 +20,19 @@ import type { ICommandResult } from '@robota-sdk/agent-interface-transport';
19
20
 
20
21
  export const CLEAR_COMMAND_MESSAGE = 'Conversation cleared.';
21
22
 
22
- export function executeClearCommand(context: ICommandHostContext, _args: string): ICommandResult {
23
+ export async function executeClearCommand(
24
+ context: ICommandHostContext,
25
+ _args: string,
26
+ ): Promise<ICommandResult> {
27
+ // Confirm only when an interactive renderer is attached; with no human the explicit /clear proceeds.
28
+ const ui = context.getUserInteraction?.();
29
+ if (ui) {
30
+ const response = await ui.ask(confirmAction('clear', 'Clear conversation history?'));
31
+ if (!isConfirmed(response)) {
32
+ return { success: true, message: 'Clear cancelled.' };
33
+ }
34
+ }
35
+
23
36
  clearConversationHistory(context);
24
37
  return {
25
38
  success: true,
@@ -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
+ }