@robota-sdk/agent-command 3.0.0-beta.73 → 3.0.0-beta.75

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 (68) hide show
  1. package/dist/node/index.cjs +35 -34
  2. package/dist/node/index.d.ts +64 -3
  3. package/dist/node/index.d.ts.map +1 -1
  4. package/dist/node/index.js +34 -33
  5. package/dist/node/index.js.map +1 -1
  6. package/package.json +10 -8
  7. package/src/agent/agent-command-module.ts +1 -2
  8. package/src/agent/agent-command-parser.ts +1 -1
  9. package/src/agent/agent-command.ts +2 -5
  10. package/src/background/background-command-module.ts +2 -6
  11. package/src/background/background-command.ts +2 -1
  12. package/src/compact/compact-command-module.ts +2 -6
  13. package/src/compact/compact-command.ts +2 -1
  14. package/src/context/__tests__/context-command-module.test.ts +2 -2
  15. package/src/context/context-command-module.ts +2 -6
  16. package/src/context/context-command.ts +1 -2
  17. package/src/default/__tests__/default-command-modules.test.ts +97 -0
  18. package/src/default/default-command-modules.ts +42 -1
  19. package/src/exit/exit-command-module.ts +2 -3
  20. package/src/exit/exit-command.ts +2 -1
  21. package/src/help/__tests__/help-command.test.ts +2 -5
  22. package/src/help/help-command-module.ts +2 -6
  23. package/src/help/help-command.ts +2 -1
  24. package/src/index.ts +2 -0
  25. package/src/language/language-command-module.ts +2 -3
  26. package/src/language/language-command.ts +2 -1
  27. package/src/memory/memory-command-module.ts +2 -6
  28. package/src/memory/memory-command.ts +1 -2
  29. package/src/mode/__tests__/mode-command-module.test.ts +1 -1
  30. package/src/mode/mode-command-module.ts +2 -3
  31. package/src/mode/mode-command.ts +2 -1
  32. package/src/permissions/__tests__/permissions-command-module.test.ts +1 -1
  33. package/src/permissions/permissions-command-module.ts +2 -6
  34. package/src/permissions/permissions-command.ts +2 -1
  35. package/src/plugin/__tests__/plugin-command-module.test.ts +2 -5
  36. package/src/plugin/plugin-command-module.ts +2 -6
  37. package/src/plugin/plugin-command.ts +2 -5
  38. package/src/plugins/default-plugin-command-adapter.ts +2 -2
  39. package/src/preset/__tests__/preset-command-module.test.ts +138 -0
  40. package/src/preset/index.ts +6 -0
  41. package/src/preset/preset-command-module.ts +79 -0
  42. package/src/preset/preset-command.ts +58 -0
  43. package/src/provider/provider-command-execution.ts +1 -2
  44. package/src/provider/provider-command-module.ts +5 -3
  45. package/src/provider/provider-command-profile-lifecycle.ts +2 -5
  46. package/src/provider/provider-command-profile-operations.ts +2 -3
  47. package/src/provider/provider-command-profile.ts +1 -2
  48. package/src/provider/provider-command-setup.ts +7 -5
  49. package/src/provider/provider-startup.ts +14 -2
  50. package/src/reset/reset-command-module.ts +2 -6
  51. package/src/reset/reset-command.ts +2 -1
  52. package/src/rewind/rewind-command-module.ts +2 -6
  53. package/src/rewind/rewind-command.ts +1 -1
  54. package/src/schedule/__tests__/schedule-command.test.ts +107 -0
  55. package/src/schedule/index.ts +9 -0
  56. package/src/schedule/schedule-command-module.ts +102 -0
  57. package/src/schedule/schedule-command.ts +72 -0
  58. package/src/schedule/schedule-spec-parser.ts +66 -0
  59. package/src/session/session-command-module.ts +2 -3
  60. package/src/session/session-command.ts +2 -1
  61. package/src/settings/settings-command-module.ts +2 -6
  62. package/src/skills/skills-command-module.ts +2 -6
  63. package/src/skills/skills-command.ts +2 -5
  64. package/src/statusline/statusline-command-module.ts +2 -6
  65. package/src/statusline/statusline-command.ts +2 -2
  66. package/src/user-local/user-local-command-module.ts +2 -6
  67. package/src/user-local/user-local-command.ts +2 -1
  68. package/src/user-local/user-local-memory-command.ts +2 -1
@@ -0,0 +1,138 @@
1
+ import { describe, expect, it, vi } from 'vitest';
2
+ import { listPresets } from '@robota-sdk/agent-preset';
3
+ import type { ICommandHostContext, ICommandSessionRuntime } from '@robota-sdk/agent-framework';
4
+ import { createPresetCommandModule, executePresetCommand } from '../index.js';
5
+
6
+ function createSessionRuntime(overrides?: Partial<ICommandSessionRuntime>): ICommandSessionRuntime {
7
+ return {
8
+ clearHistory: () => undefined,
9
+ compact: async () => undefined,
10
+ getContextState: () => ({
11
+ maxTokens: 100,
12
+ usedTokens: 10,
13
+ usedPercentage: 10,
14
+ remainingPercentage: 90,
15
+ }),
16
+ getPermissionMode: () => 'default',
17
+ setPermissionMode: () => undefined,
18
+ getSessionId: () => 'session_1',
19
+ getMessageCount: () => 0,
20
+ getSessionAllowedTools: () => [],
21
+ getAutoCompactThreshold: () => false,
22
+ getFullHistory: () => [],
23
+ getHistory: () => [],
24
+ ...overrides,
25
+ };
26
+ }
27
+
28
+ function createCommandHostContext(
29
+ runtime: ICommandSessionRuntime,
30
+ overrides?: Partial<ICommandHostContext>,
31
+ ): ICommandHostContext {
32
+ return {
33
+ getSession: () => runtime,
34
+ getContextState: () => ({
35
+ maxTokens: 100,
36
+ usedTokens: 10,
37
+ usedPercentage: 10,
38
+ remainingPercentage: 90,
39
+ }),
40
+ getAutoCompactThreshold: () => 0.8,
41
+ compactContext: async () => undefined,
42
+ getCwd: () => '/workspace',
43
+ listEditCheckpoints: () => [],
44
+ restoreEditCheckpoint: async () => ({
45
+ target: {
46
+ id: 'checkpoint_1',
47
+ sessionId: 'session_1',
48
+ sequence: 1,
49
+ prompt: 'edit',
50
+ createdAt: '2026-05-03T00:00:00.000Z',
51
+ fileCount: 0,
52
+ },
53
+ restoredCheckpointCount: 1,
54
+ restoredFileCount: 0,
55
+ removedCheckpointCount: 0,
56
+ }),
57
+ rollbackEditCheckpoint: async () => ({
58
+ target: {
59
+ id: 'checkpoint_1',
60
+ sessionId: 'session_1',
61
+ sequence: 1,
62
+ prompt: 'edit',
63
+ createdAt: '2026-05-03T00:00:00.000Z',
64
+ fileCount: 0,
65
+ },
66
+ restoredCheckpointCount: 1,
67
+ restoredFileCount: 0,
68
+ removedCheckpointCount: 0,
69
+ }),
70
+ getUsedMemoryReferences: () => [],
71
+ recordMemoryEvent: () => undefined,
72
+ listBackgroundTasks: () => [],
73
+ readBackgroundTaskLog: async (taskId) => ({ taskId, lines: [] }),
74
+ cancelBackgroundTask: async () => undefined,
75
+ closeBackgroundTask: async () => undefined,
76
+ ...overrides,
77
+ };
78
+ }
79
+
80
+ describe('preset command module', () => {
81
+ it('exposes a single preset system command (structure)', () => {
82
+ const module = createPresetCommandModule();
83
+ expect(module.systemCommands?.map((command) => command.name)).toContain('preset');
84
+ });
85
+
86
+ it('TC-01: lists every preset and marks the active one', () => {
87
+ const runtime = createSessionRuntime({ getActivePresetId: () => 'autonomous-builder' });
88
+ const context = createCommandHostContext(runtime);
89
+
90
+ const result = executePresetCommand(context, '');
91
+
92
+ expect(result.success).toBe(true);
93
+ for (const preset of listPresets()) {
94
+ expect(result.message).toContain(preset.id);
95
+ }
96
+ // The active preset is marked with the `* ` prefix.
97
+ expect(result.message).toContain('* autonomous-builder');
98
+ expect(result.message).not.toContain('* default');
99
+ expect(result.data?.active).toBe('autonomous-builder');
100
+ });
101
+
102
+ it('TC-02: switches to a valid preset and drives the live re-apply seams', () => {
103
+ const setActivePresetId = vi.fn();
104
+ const setPermissionMode = vi.fn();
105
+ const applyModelOptions = vi.fn();
106
+ const runtime = createSessionRuntime({
107
+ setActivePresetId,
108
+ setPermissionMode,
109
+ applyModelOptions,
110
+ });
111
+ const context = createCommandHostContext(runtime);
112
+
113
+ const result = executePresetCommand(context, 'careful-reviewer');
114
+
115
+ expect(result.success).toBe(true);
116
+ expect(result.message).toBe('Switched to preset: careful-reviewer');
117
+ expect(result.data?.preset).toBe('careful-reviewer');
118
+ expect(setActivePresetId).toHaveBeenCalledWith('careful-reviewer');
119
+ // careful-reviewer resolves autonomy ask-first → default permission posture (PRESET-012)
120
+ // and effort high → applyModelOptions (PRESET-013).
121
+ expect(setPermissionMode).toHaveBeenCalledWith('default');
122
+ expect(applyModelOptions).toHaveBeenCalledWith(expect.objectContaining({ effort: 'high' }));
123
+ });
124
+
125
+ it('TC-04: rejects an unknown preset id without switching', () => {
126
+ const setActivePresetId = vi.fn();
127
+ const runtime = createSessionRuntime({ setActivePresetId });
128
+ const context = createCommandHostContext(runtime);
129
+
130
+ const result = executePresetCommand(context, '__nope__');
131
+
132
+ expect(result.success).toBe(false);
133
+ for (const preset of listPresets()) {
134
+ expect(result.message).toContain(preset.id);
135
+ }
136
+ expect(setActivePresetId).not.toHaveBeenCalled();
137
+ });
138
+ });
@@ -0,0 +1,6 @@
1
+ export {
2
+ createPresetCommandEntry,
3
+ createPresetCommandModule,
4
+ PresetCommandSource,
5
+ } from './preset-command-module.js';
6
+ export { executePresetCommand } from './preset-command.js';
@@ -0,0 +1,79 @@
1
+ import { listPresets } from '@robota-sdk/agent-preset';
2
+
3
+ import { executePresetCommand } from './preset-command.js';
4
+
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';
11
+
12
+ const PRESET_COMMAND_DESCRIPTION = 'List presets or switch the active preset';
13
+ const PRESET_ARGUMENT_HINT = 'list | <preset-id>';
14
+
15
+ /** Build one subcommand per registered preset id (mirrors the permission-mode subcommands). */
16
+ function buildPresetSubcommands(source = 'preset'): ICommand[] {
17
+ return listPresets().map((preset) => ({
18
+ name: preset.id,
19
+ description: preset.description,
20
+ source,
21
+ }));
22
+ }
23
+
24
+ export function createPresetCommandEntry(): ICommand {
25
+ return {
26
+ name: 'preset',
27
+ displayName: 'Agent Preset',
28
+ description: PRESET_COMMAND_DESCRIPTION,
29
+ source: 'preset',
30
+ argumentHint: PRESET_ARGUMENT_HINT,
31
+ subcommands: buildPresetSubcommands('preset'),
32
+ modelInvocable: false,
33
+ };
34
+ }
35
+
36
+ function createPresetSystemCommand(): ISystemCommand {
37
+ const entry = createPresetCommandEntry();
38
+ return {
39
+ name: entry.name,
40
+ displayName: entry.displayName,
41
+ description: entry.description,
42
+ requiresPermission: false,
43
+ userInvocable: true,
44
+ modelInvocable: false,
45
+ argumentHint: entry.argumentHint,
46
+ subcommands: entry.subcommands,
47
+ lifecycle: 'inline',
48
+ execute: executePresetCommand,
49
+ };
50
+ }
51
+
52
+ export class PresetCommandSource implements ICommandSource {
53
+ readonly name = 'preset';
54
+
55
+ getCommands(): ICommand[] {
56
+ return [createPresetCommandEntry()];
57
+ }
58
+ }
59
+
60
+ const PRESET_INTERACTION_HINTS: Record<string, ICommandInteractionHint> = {
61
+ preset: {
62
+ type: 'pick',
63
+ getItems: () =>
64
+ buildPresetSubcommands().map((sub) => ({
65
+ label: sub.name,
66
+ value: sub.name,
67
+ description: sub.description,
68
+ })),
69
+ },
70
+ };
71
+
72
+ export function createPresetCommandModule(): ICommandModule {
73
+ return {
74
+ name: 'agent-command-preset',
75
+ commandSources: [new PresetCommandSource()],
76
+ systemCommands: [createPresetSystemCommand()],
77
+ interactionHints: PRESET_INTERACTION_HINTS,
78
+ };
79
+ }
@@ -0,0 +1,58 @@
1
+ import { applyPresetToSession } from '@robota-sdk/agent-framework';
2
+ import { getPreset, listPresets, resolvePreset } from '@robota-sdk/agent-preset';
3
+
4
+ import type { ICommandHostContext } from '@robota-sdk/agent-framework';
5
+ import type { ICommandResult } from '@robota-sdk/agent-interface-transport';
6
+
7
+ /** Default active preset id reported when the runtime has no recorded active preset. */
8
+ const DEFAULT_ACTIVE_PRESET_ID = 'default';
9
+
10
+ /** Read the active preset id from the session, defaulting when the optional seam is absent. */
11
+ function readActivePresetId(context: ICommandHostContext): string {
12
+ return context.getSession().getActivePresetId?.() ?? DEFAULT_ACTIVE_PRESET_ID;
13
+ }
14
+
15
+ /** Build the `/preset` listing: one line per preset, marking the active one with a `*` prefix. */
16
+ function formatPresetList(active: string): string {
17
+ const lines = listPresets().map((preset) => {
18
+ const marker = preset.id === active ? '* ' : ' ';
19
+ return `${marker}${preset.id} — ${preset.title}: ${preset.description}`;
20
+ });
21
+ return ['Available presets:', ...lines].join('\n');
22
+ }
23
+
24
+ /** Build the rejection message for an unknown preset id, listing the valid ids. */
25
+ function formatUnknownPresetMessage(id: string): string {
26
+ const ids = listPresets()
27
+ .map((preset) => preset.id)
28
+ .join(', ');
29
+ return `Unknown preset: ${id}. Available: ${ids}`;
30
+ }
31
+
32
+ export function executePresetCommand(context: ICommandHostContext, args: string): ICommandResult {
33
+ const id = args.trim().split(/\s+/)[0];
34
+
35
+ if (id === undefined || id.length === 0 || id === 'list') {
36
+ const active = readActivePresetId(context);
37
+ return {
38
+ message: formatPresetList(active),
39
+ success: true,
40
+ data: { presets: listPresets(), active },
41
+ };
42
+ }
43
+
44
+ if (getPreset(id) === undefined) {
45
+ return {
46
+ message: formatUnknownPresetMessage(id),
47
+ success: false,
48
+ };
49
+ }
50
+
51
+ const resolved = resolvePreset(id);
52
+ applyPresetToSession(context, id, resolved);
53
+ return {
54
+ message: `Switched to preset: ${id}`,
55
+ success: true,
56
+ data: { preset: id },
57
+ };
58
+ }
@@ -7,11 +7,10 @@ import { createSetupFlow, createProviderSetupInteraction } from './provider-comm
7
7
  import { formatProviderSetupChoiceLabel } from './provider-setup-flow.js';
8
8
 
9
9
  import type {
10
- ICommandInteraction,
11
- ICommandResult,
12
10
  IProviderCommandModuleOptions,
13
11
  IProviderProfileSettings,
14
12
  } from '@robota-sdk/agent-framework';
13
+ import type { ICommandInteraction, ICommandResult } from '@robota-sdk/agent-interface-transport';
15
14
 
16
15
  export async function executeProviderCommand(
17
16
  args: string,
@@ -1,14 +1,16 @@
1
1
  import { executeProviderCommand } from './provider-command-execution.js';
2
2
 
3
3
  import type {
4
- ICommand,
5
- ICommandInteractionHint,
6
4
  ICommandModule as TCommandModule,
7
- ICommandSource,
8
5
  IProviderCommandModuleOptions,
9
6
  IProviderCommandSettingsAdapter,
10
7
  ISystemCommand as TSystemCommand,
11
8
  } from '@robota-sdk/agent-framework';
9
+ import type {
10
+ ICommand,
11
+ ICommandInteractionHint,
12
+ ICommandSource,
13
+ } from '@robota-sdk/agent-interface-transport';
12
14
  export type { IProviderCommandModuleOptions, IProviderCommandSettingsAdapter };
13
15
 
14
16
  function buildProviderSubcommands(): ICommand[] {
@@ -6,11 +6,8 @@ import {
6
6
 
7
7
  import { formatProviderChoiceLabel } from './provider-command-profile-operations.js';
8
8
 
9
- import type {
10
- ICommandInteraction,
11
- ICommandResult,
12
- IProviderCommandModuleOptions,
13
- } from '@robota-sdk/agent-framework';
9
+ import type { IProviderCommandModuleOptions } from '@robota-sdk/agent-framework';
10
+ import type { ICommandInteraction, ICommandResult } from '@robota-sdk/agent-interface-transport';
14
11
 
15
12
  const YES = 'yes';
16
13
  const MAX_DUPLICATE_PROFILE_SUFFIX = 1000;
@@ -14,12 +14,11 @@ import { createProviderSetupFlow, submitProviderSetupValue } from './provider-se
14
14
 
15
15
  import type { IProviderSetupFlowState } from './provider-setup-flow.js';
16
16
  import type {
17
- ICommandInteraction,
18
- ICommandResult,
19
17
  IProviderCommandModuleOptions,
20
18
  IProviderProfileSettings,
21
19
  IProviderSetupInput,
22
20
  } from '@robota-sdk/agent-framework';
21
+ import type { ICommandInteraction, ICommandResult } from '@robota-sdk/agent-interface-transport';
23
22
 
24
23
  export function formatProviderChoiceLabel(
25
24
  name: string,
@@ -110,7 +109,7 @@ function getProviderProfileSetupValues(profile: IProviderProfileSettings): {
110
109
  };
111
110
  }
112
111
 
113
- export function createProviderEditInteraction(
112
+ function createProviderEditInteraction(
114
113
  flow: IProviderSetupFlowState,
115
114
  profileName: string,
116
115
  options: IProviderCommandModuleOptions,
@@ -11,11 +11,10 @@ import {
11
11
  } from './provider-command-profile-operations.js';
12
12
 
13
13
  import type {
14
- ICommandInteraction,
15
- ICommandResult,
16
14
  IProviderCommandModuleOptions,
17
15
  IProviderProfileSettings,
18
16
  } from '@robota-sdk/agent-framework';
17
+ import type { ICommandInteraction, ICommandResult } from '@robota-sdk/agent-interface-transport';
19
18
 
20
19
  const ACTION_SWITCH = 'switch';
21
20
  const ACTION_EDIT = 'edit';
@@ -10,12 +10,14 @@ import {
10
10
 
11
11
  import type { IProviderSetupFlowState } from './provider-setup-flow.js';
12
12
  import type {
13
- ICommandInteraction,
14
- ICommandResult,
15
13
  IProviderCommandModuleOptions,
16
14
  IProviderSetupInput,
17
- TCommandInteractionPrompt,
18
15
  } from '@robota-sdk/agent-framework';
16
+ import type {
17
+ ICommandInteraction,
18
+ ICommandResult,
19
+ TCommandInteractionPrompt,
20
+ } from '@robota-sdk/agent-interface-transport';
19
21
 
20
22
  const PROVIDER_RESTART_EFFECT = {
21
23
  type: 'session-restart-requested',
@@ -59,7 +61,7 @@ export function toProviderSetupStepPrompt(
59
61
  };
60
62
  }
61
63
 
62
- export function toProviderSetupPromptDescription(
64
+ function toProviderSetupPromptDescription(
63
65
  flow: IProviderSetupFlowState,
64
66
  ): { description: string } | Record<string, never> {
65
67
  const description = formatProviderSetupHelpLinks(flow.setupHelpLinks);
@@ -89,7 +91,7 @@ function submitProviderSetupInteractionValue(
89
91
  };
90
92
  }
91
93
 
92
- export function completeProviderSetup(
94
+ function completeProviderSetup(
93
95
  input: IProviderSetupInput,
94
96
  options: IProviderCommandModuleOptions,
95
97
  ): ICommandResult {
@@ -2,9 +2,11 @@ import { join } from 'node:path';
2
2
 
3
3
  import {
4
4
  checkSettingsDocument,
5
+ ProviderConfigError,
5
6
  readMergedProviderSettings,
6
7
  readSettings,
7
8
  writeSettings,
9
+ resolveEnvDefaultProvider,
8
10
  resolveSettingsPathForScope,
9
11
  getProviderSettingsPaths,
10
12
  applyProviderConfiguration,
@@ -30,6 +32,8 @@ export interface IProviderStartupContext {
30
32
  export interface IEnsureProviderConfigOptions {
31
33
  formatError: (defs: readonly IProviderDefinition[]) => string;
32
34
  isInteractive?: () => boolean;
35
+ /** Environment map for env-default synthesis (test seam, default: process.env). */
36
+ env?: Record<string, string | undefined>;
33
37
  }
34
38
 
35
39
  export async function runProviderStartupSetup(
@@ -80,9 +84,17 @@ export async function ensureProviderConfig(
80
84
  if (checkSettingsDocument(selectedSettings, providerDefinitions) === 'valid') {
81
85
  return;
82
86
  }
87
+ // Zero-config startup: a recognized provider env key with complete definition defaults
88
+ // makes setup unnecessary — resolution will synthesize an env-default config.
89
+ if (
90
+ ctx.provider === undefined &&
91
+ resolveEnvDefaultProvider(providerDefinitions, options.env) !== undefined
92
+ ) {
93
+ return;
94
+ }
83
95
  const checkInteractive = options.isInteractive ?? (() => false);
84
96
  if (!checkInteractive()) {
85
- throw new Error(options.formatError(providerDefinitions));
97
+ throw new ProviderConfigError(options.formatError(providerDefinitions));
86
98
  }
87
99
  await runProviderStartupSetup(
88
100
  cwd,
@@ -95,7 +107,7 @@ export async function ensureProviderConfig(
95
107
  const updatedSettings =
96
108
  ctx.provider !== undefined ? { ...updated, currentProvider: ctx.provider } : updated;
97
109
  if (checkSettingsDocument(updatedSettings, providerDefinitions) !== 'valid') {
98
- throw new Error(options.formatError(providerDefinitions));
110
+ throw new ProviderConfigError(options.formatError(providerDefinitions));
99
111
  }
100
112
  }
101
113
 
@@ -1,11 +1,7 @@
1
1
  import { executeResetCommand } from './reset-command.js';
2
2
 
3
- import type {
4
- ICommand,
5
- ICommandModule,
6
- ICommandSource,
7
- ISystemCommand,
8
- } from '@robota-sdk/agent-framework';
3
+ import type { ICommandModule, ISystemCommand } from '@robota-sdk/agent-framework';
4
+ import type { ICommand, ICommandSource } from '@robota-sdk/agent-interface-transport';
9
5
 
10
6
  const RESET_COMMAND_DESCRIPTION = 'Delete settings';
11
7
 
@@ -1,4 +1,5 @@
1
- import type { ICommandHostContext, ICommandResult } from '@robota-sdk/agent-framework';
1
+ import type { ICommandHostContext } from '@robota-sdk/agent-framework';
2
+ import type { ICommandResult } from '@robota-sdk/agent-interface-transport';
2
3
 
3
4
  export function executeResetCommand(_context: ICommandHostContext, _args: string): ICommandResult {
4
5
  return {
@@ -6,12 +6,8 @@ import {
6
6
 
7
7
  import { executeRewindCommand } from './rewind-command.js';
8
8
 
9
- import type {
10
- ICommand,
11
- ICommandModule,
12
- ICommandSource,
13
- ISystemCommand,
14
- } from '@robota-sdk/agent-framework';
9
+ import type { ICommandModule, ISystemCommand } from '@robota-sdk/agent-framework';
10
+ import type { ICommand, ICommandSource } from '@robota-sdk/agent-interface-transport';
15
11
 
16
12
  export function createRewindCommandEntry(): ICommand {
17
13
  return {
@@ -7,11 +7,11 @@ import {
7
7
 
8
8
  import type {
9
9
  ICommandHostContext,
10
- ICommandResult,
11
10
  IEditCheckpointInspection,
12
11
  IEditCheckpointRestoreResult,
13
12
  IEditCheckpointSummary,
14
13
  } from '@robota-sdk/agent-framework';
14
+ import type { ICommandResult } from '@robota-sdk/agent-interface-transport';
15
15
 
16
16
  const SUBCOMMAND_INDEX = 0;
17
17
  const CHECKPOINT_ID_INDEX = 1;
@@ -0,0 +1,107 @@
1
+ /**
2
+ * FLOW-005: `/schedule` + `/monitor` command surface.
3
+ * - parseScheduleSpec: relative delay → one-shot ISO; cron form; invalid input rejected.
4
+ * - executeScheduleCommand / executeMonitorCommand: build the wake request and spawn it.
5
+ */
6
+
7
+ import { describe, expect, it, vi } from 'vitest';
8
+
9
+ import { executeMonitorCommand, executeScheduleCommand } from '../schedule-command.js';
10
+ import { parseScheduleSpec } from '../schedule-spec-parser.js';
11
+
12
+ import type { IAgentJobHostContext } from '@robota-sdk/agent-framework';
13
+
14
+ function mockHost(): {
15
+ host: IAgentJobHostContext;
16
+ scheduled: ReturnType<typeof vi.fn>;
17
+ monitor: ReturnType<typeof vi.fn>;
18
+ } {
19
+ const scheduled = vi.fn().mockResolvedValue({ id: 'task_sched' });
20
+ const monitor = vi.fn().mockResolvedValue({ id: 'task_mon' });
21
+ const host = {
22
+ spawnScheduledWake: scheduled,
23
+ spawnMonitorWake: monitor,
24
+ } as unknown as IAgentJobHostContext;
25
+ return { host, scheduled, monitor };
26
+ }
27
+
28
+ describe('parseScheduleSpec (FLOW-005)', () => {
29
+ it('parses a relative delay into a one-shot ISO timestamp', () => {
30
+ const result = parseScheduleSpec('in 5m summarize the logs', 0);
31
+ expect(result.ok).toBe(true);
32
+ if (!result.ok) return;
33
+ expect(result.spec.recurring).toBe(false);
34
+ expect(result.spec.cronExpression).toBe(new Date(5 * 60_000).toISOString());
35
+ expect(result.spec.instruction).toBe('summarize the logs');
36
+ });
37
+
38
+ it('parses a quoted cron expression as recurring', () => {
39
+ const result = parseScheduleSpec('cron "0 9 * * *" run standup', 0);
40
+ expect(result.ok).toBe(true);
41
+ if (!result.ok) return;
42
+ expect(result.spec.recurring).toBe(true);
43
+ expect(result.spec.cronExpression).toBe('0 9 * * *');
44
+ expect(result.spec.instruction).toBe('run standup');
45
+ });
46
+
47
+ it('rejects an invalid duration', () => {
48
+ expect(parseScheduleSpec('in 5x do X', 0).ok).toBe(false);
49
+ });
50
+
51
+ it('rejects a missing instruction', () => {
52
+ expect(parseScheduleSpec('in 5m', 0).ok).toBe(false);
53
+ });
54
+
55
+ it('rejects unrecognized input', () => {
56
+ expect(parseScheduleSpec('whenever you feel like it', 0).ok).toBe(false);
57
+ });
58
+ });
59
+
60
+ describe('executeScheduleCommand (FLOW-005)', () => {
61
+ it('TC-01: relative delay spawns a one-shot scheduled wake', async () => {
62
+ const { host, scheduled } = mockHost();
63
+ const now = 1_000_000;
64
+ const result = await executeScheduleCommand(host, 'in 1m summarize', now);
65
+
66
+ expect(result.success).toBe(true);
67
+ expect(scheduled).toHaveBeenCalledTimes(1);
68
+ expect(scheduled).toHaveBeenCalledWith(
69
+ expect.objectContaining({
70
+ cronExpression: new Date(now + 60_000).toISOString(),
71
+ agentInstruction: 'summarize',
72
+ }),
73
+ );
74
+ });
75
+
76
+ it('TC-02: cron form spawns a recurring scheduled wake', async () => {
77
+ const { host, scheduled } = mockHost();
78
+ const result = await executeScheduleCommand(host, 'cron "0 9 * * *" standup', Date.now());
79
+
80
+ expect(result.success).toBe(true);
81
+ expect(scheduled).toHaveBeenCalledWith(
82
+ expect.objectContaining({ cronExpression: '0 9 * * *', agentInstruction: 'standup' }),
83
+ );
84
+ });
85
+
86
+ it('TC-03: invalid input is rejected without spawning', async () => {
87
+ const { host, scheduled } = mockHost();
88
+ const result = await executeScheduleCommand(host, 'garbage', Date.now());
89
+
90
+ expect(result.success).toBe(false);
91
+ expect(scheduled).not.toHaveBeenCalled();
92
+ });
93
+
94
+ it('TC-04: monitor command spawns a process+match wake task', async () => {
95
+ const { host, monitor } = mockHost();
96
+ const result = await executeMonitorCommand(host, '"npm run dev" "ERROR" fix the failure');
97
+
98
+ expect(result.success).toBe(true);
99
+ expect(monitor).toHaveBeenCalledWith(
100
+ expect.objectContaining({
101
+ command: 'npm run dev',
102
+ matchPattern: 'ERROR',
103
+ agentInstruction: 'fix the failure',
104
+ }),
105
+ );
106
+ });
107
+ });
@@ -0,0 +1,9 @@
1
+ export {
2
+ createScheduleCommandEntry,
3
+ createMonitorCommandEntry,
4
+ createScheduleCommandModule,
5
+ ScheduleCommandSource,
6
+ } from './schedule-command-module.js';
7
+ export { executeScheduleCommand, executeMonitorCommand } from './schedule-command.js';
8
+ export { parseScheduleSpec } from './schedule-spec-parser.js';
9
+ export type { IScheduleSpec, TScheduleParseResult } from './schedule-spec-parser.js';