@robota-sdk/agent-command 3.0.0-beta.100
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 +1351 -0
- package/LICENSE +661 -0
- package/README.md +100 -0
- package/dist/node/index.cjs +121 -0
- package/dist/node/index.d.cts +1053 -0
- package/dist/node/index.d.cts.map +1 -0
- package/dist/node/index.d.ts +1053 -0
- package/dist/node/index.d.ts.map +1 -0
- package/dist/node/index.js +122 -0
- package/dist/node/index.js.map +1 -0
- package/package.json +83 -0
- package/src/advisor/advisor-command-module.ts +47 -0
- package/src/advisor/advisor-command.test.ts +102 -0
- package/src/advisor/advisor-command.ts +55 -0
- package/src/advisor/advisor-cost-functional.test.ts +92 -0
- package/src/advisor/index.ts +6 -0
- package/src/agent/__tests__/agent-command.test.ts +597 -0
- package/src/agent/agent-command-module.ts +89 -0
- package/src/agent/agent-command-parser.ts +180 -0
- package/src/agent/agent-command.ts +255 -0
- package/src/agent/index.ts +7 -0
- package/src/background/__tests__/background-command-module.test.ts +261 -0
- package/src/background/background-command-module.ts +52 -0
- package/src/background/background-command.ts +65 -0
- package/src/background/index.ts +6 -0
- package/src/command-module-utils.test.ts +37 -0
- package/src/command-module-utils.ts +35 -0
- package/src/compact/__tests__/compact-command-module.test.ts +181 -0
- package/src/compact/compact-command-module.ts +59 -0
- package/src/compact/compact-command.ts +33 -0
- package/src/compact/index.ts +6 -0
- package/src/context/__tests__/context-command-module.test.ts +502 -0
- package/src/context/auto-compact-format.ts +25 -0
- package/src/context/context-breakdown.ts +295 -0
- package/src/context/context-command-module.ts +82 -0
- package/src/context/context-command.ts +291 -0
- package/src/context/index.ts +6 -0
- package/src/default/__tests__/__snapshots__/model-exposure.test.ts.snap +121 -0
- package/src/default/__tests__/default-command-modules.test.ts +192 -0
- package/src/default/__tests__/model-exposure.test.ts +269 -0
- package/src/default/__tests__/org-policy-forwarding.test.ts +106 -0
- package/src/default/__tests__/provider-environment-forwarding.test.ts +98 -0
- package/src/default/default-command-modules.ts +247 -0
- package/src/default/index.ts +5 -0
- package/src/devices/__tests__/devices-command-module.test.ts +299 -0
- package/src/devices/devices-command-module.ts +308 -0
- package/src/devices/devices-command-port.ts +143 -0
- package/src/devices/index.ts +14 -0
- package/src/doctor/__tests__/doctor-command-module.test.ts +145 -0
- package/src/doctor/__tests__/doctor-display-vocabulary.test.ts +38 -0
- package/src/doctor/__tests__/doctor-fixture.ts +146 -0
- package/src/doctor/__tests__/doctor-redaction.test.ts +29 -0
- package/src/doctor/__tests__/doctor-runner.test.ts +484 -0
- package/src/doctor/doctor-command-module.ts +132 -0
- package/src/doctor/doctor-extensions-probe.ts +323 -0
- package/src/doctor/doctor-node-deps.ts +88 -0
- package/src/doctor/doctor-redaction.ts +99 -0
- package/src/doctor/doctor-render.ts +70 -0
- package/src/doctor/doctor-repair.ts +136 -0
- package/src/doctor/doctor-runner.ts +122 -0
- package/src/doctor/doctor-settings-probe.ts +239 -0
- package/src/doctor/doctor-storage-probe.ts +187 -0
- package/src/doctor/doctor-types.ts +109 -0
- package/src/doctor/index.ts +32 -0
- package/src/editor/__tests__/editor-command-functional.test.ts +124 -0
- package/src/editor/editor-command-module.ts +53 -0
- package/src/editor/editor-command.ts +58 -0
- package/src/editor/index.ts +7 -0
- package/src/editor/resolve-editor.ts +21 -0
- package/src/effort/effort-command-module.ts +51 -0
- package/src/effort/effort-command.test.ts +203 -0
- package/src/effort/effort-command.ts +136 -0
- package/src/effort/index.ts +6 -0
- 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/exit/__tests__/exit-command-module.test.ts +61 -0
- package/src/exit/exit-command-module.ts +47 -0
- package/src/exit/exit-command.ts +26 -0
- package/src/exit/index.ts +6 -0
- package/src/fork/__tests__/fork-command.test.ts +184 -0
- package/src/fork/fork-command-module.ts +61 -0
- package/src/fork/fork-command.ts +162 -0
- package/src/fork/index.ts +8 -0
- package/src/git/__tests__/fake-git-port.ts +35 -0
- package/src/git/__tests__/git-command-module.test.ts +111 -0
- package/src/git/__tests__/git-commit.test.ts +185 -0
- package/src/git/git-command-module.ts +138 -0
- package/src/git/git-commit.ts +254 -0
- package/src/git/index.ts +20 -0
- package/src/goal/__tests__/goal-command.test.ts +80 -0
- package/src/goal/goal-command-module.ts +49 -0
- package/src/goal/goal-command.ts +69 -0
- package/src/goal/index.ts +6 -0
- package/src/handoff/__tests__/handoff-command-module.test.ts +25 -0
- package/src/handoff/__tests__/handoff-command.test.ts +218 -0
- package/src/handoff/handoff-command-module.ts +49 -0
- package/src/handoff/handoff-command.ts +187 -0
- package/src/handoff/index.ts +6 -0
- package/src/help/__tests__/help-command-module.test.ts +134 -0
- package/src/help/__tests__/help-command.test.ts +130 -0
- package/src/help/help-command-module.ts +47 -0
- package/src/help/help-command.ts +11 -0
- package/src/help/index.ts +6 -0
- package/src/index.ts +163 -0
- package/src/keybindings/__tests__/keybindings-command-module.test.ts +57 -0
- package/src/keybindings/index.ts +6 -0
- package/src/keybindings/keybindings-command-module.ts +88 -0
- package/src/language/__tests__/language-command-module.test.ts +126 -0
- package/src/language/index.ts +6 -0
- package/src/language/language-command-module.ts +55 -0
- package/src/language/language-command.ts +52 -0
- package/src/mcp-activation/__tests__/mcp-activation-command.test.ts +895 -0
- package/src/mcp-activation/__tests__/mcp-model-view.test.ts +242 -0
- package/src/mcp-activation/__tests__/mcp-skill-command.test.ts +141 -0
- package/src/mcp-activation/index.ts +12 -0
- package/src/mcp-activation/mcp-activation-command-module.ts +123 -0
- package/src/mcp-activation/mcp-activation-command.ts +662 -0
- package/src/mcp-activation/mcp-model-notice.ts +99 -0
- package/src/mcp-activation/mcp-model-status.ts +104 -0
- package/src/memory/__tests__/memory-command-module.test.ts +476 -0
- package/src/memory/__tests__/memory-command-unavailable.test.ts +63 -0
- package/src/memory/index.ts +6 -0
- package/src/memory/memory-command-module.ts +77 -0
- package/src/memory/memory-command.ts +326 -0
- package/src/mode/__tests__/mode-command-module.test.ts +230 -0
- package/src/mode/index.ts +6 -0
- package/src/mode/mode-command-module.ts +55 -0
- package/src/mode/mode-command.ts +67 -0
- package/src/model/__tests__/model-command-module.test.ts +80 -0
- package/src/model/__tests__/model-command.test.ts +265 -0
- package/src/model/index.ts +6 -0
- package/src/model/model-command-module.ts +56 -0
- package/src/model/model-command.ts +249 -0
- package/src/output-style/__tests__/output-style-command.test.ts +96 -0
- package/src/output-style/index.ts +6 -0
- package/src/output-style/output-style-command-module.ts +143 -0
- package/src/peers/__tests__/peers-command-module.test.ts +24 -0
- package/src/peers/__tests__/peers-command.test.ts +399 -0
- package/src/peers/index.ts +9 -0
- package/src/peers/peers-command-module.ts +49 -0
- package/src/peers/peers-command.ts +220 -0
- package/src/permissions/__tests__/permissions-command-module.test.ts +276 -0
- package/src/permissions/index.ts +6 -0
- package/src/permissions/permissions-command-module.ts +55 -0
- package/src/permissions/permissions-command.ts +89 -0
- package/src/plan/__tests__/plan-command.test.ts +129 -0
- package/src/plan/index.ts +9 -0
- package/src/plan/plan-command-module.ts +49 -0
- package/src/plan/plan-command.ts +92 -0
- package/src/plugin/__tests__/plugin-active-turn.test.ts +100 -0
- package/src/plugin/__tests__/plugin-command-module.test.ts +300 -0
- package/src/plugin/__tests__/plugin-host-boundary.test.ts +11 -0
- package/src/plugin/index.ts +7 -0
- package/src/plugin/plugin-command-module.ts +85 -0
- package/src/plugin/plugin-command.ts +271 -0
- package/src/preset/__tests__/host-supplied-preset-registry.test.ts +98 -0
- package/src/preset/__tests__/preset-command-module.test.ts +220 -0
- package/src/preset/index.ts +6 -0
- package/src/preset/preset-command-module.ts +80 -0
- package/src/preset/preset-command.ts +148 -0
- package/src/provider/__tests__/optional-provider-credentials.test.ts +130 -0
- package/src/provider/__tests__/org-policy.test.ts +174 -0
- package/src/provider/__tests__/provider-command-module.test.ts +884 -0
- package/src/provider/__tests__/provider-connection-setup.test.ts +567 -0
- package/src/provider/__tests__/provider-reference-switch.test.ts +113 -0
- package/src/provider/__tests__/provider-setup-flow.test.ts +244 -0
- package/src/provider/__tests__/provider-startup-host-settings.test.ts +16 -0
- package/src/provider/__tests__/scripted-interaction.ts +40 -0
- package/src/provider/index.ts +32 -0
- package/src/provider/provider-command-execution.ts +235 -0
- package/src/provider/provider-command-module.ts +82 -0
- package/src/provider/provider-command-profile-lifecycle.ts +239 -0
- package/src/provider/provider-command-profile-operations.ts +218 -0
- package/src/provider/provider-command-profile.ts +116 -0
- package/src/provider/provider-command-setup.ts +79 -0
- package/src/provider/provider-connection-setup.ts +287 -0
- package/src/provider/provider-connection-validation.ts +38 -0
- package/src/provider/provider-onboarding.ts +73 -0
- package/src/provider/provider-prompt-interaction.ts +28 -0
- package/src/provider/provider-setup-ask.ts +67 -0
- package/src/provider/provider-setup-flow.ts +326 -0
- package/src/provider/provider-startup.ts +225 -0
- package/src/remote-control/__tests__/remote-control-command-module.test.ts +36 -0
- package/src/remote-control/__tests__/remote-control-command.test.ts +216 -0
- package/src/remote-control/index.ts +8 -0
- package/src/remote-control/remote-control-command-module.ts +61 -0
- package/src/remote-control/remote-control-command.ts +138 -0
- package/src/reset/__tests__/reset-command-module.test.ts +63 -0
- package/src/reset/index.ts +2 -0
- package/src/reset/reset-command-module.ts +47 -0
- package/src/reset/reset-command.ts +10 -0
- package/src/rewind/__tests__/rewind-command-module.test.ts +287 -0
- package/src/rewind/index.ts +2 -0
- package/src/rewind/rewind-command-module.ts +56 -0
- package/src/rewind/rewind-command.ts +279 -0
- package/src/sandbox/__tests__/sandbox-command.test.ts +80 -0
- package/src/sandbox/index.ts +2 -0
- package/src/sandbox/sandbox-command-module.ts +56 -0
- package/src/sandbox/sandbox-command.ts +94 -0
- package/src/schedule/__tests__/loop-command-functional.test.ts +399 -0
- package/src/schedule/__tests__/loop-command.test.ts +594 -0
- package/src/schedule/__tests__/loop-jitter.test.ts +25 -0
- package/src/schedule/__tests__/monitor-model-permission-functional.test.ts +126 -0
- package/src/schedule/__tests__/schedule-command.test.ts +247 -0
- package/src/schedule/__tests__/schedule-redos.test.ts +114 -0
- package/src/schedule/index.ts +12 -0
- package/src/schedule/loop-command.ts +427 -0
- package/src/schedule/loop-jitter.ts +41 -0
- package/src/schedule/schedule-command-module.ts +184 -0
- package/src/schedule/schedule-command.ts +161 -0
- package/src/schedule/schedule-spec-parser.ts +70 -0
- package/src/session/__tests__/model-pricing.test.ts +66 -0
- package/src/session/__tests__/rename-host-persistence.test.ts +72 -0
- package/src/session/__tests__/session-command-module.test.ts +589 -0
- package/src/session/index.ts +17 -0
- package/src/session/model-pricing.ts +26 -0
- package/src/session/session-command-module.ts +197 -0
- package/src/session/session-command.ts +272 -0
- package/src/settings/index.ts +7 -0
- package/src/settings/settings-command-module.ts +47 -0
- package/src/shell/__tests__/resolve-shell-host.test.ts +11 -0
- package/src/shell/__tests__/shell-command-functional.test.ts +118 -0
- package/src/shell/index.ts +8 -0
- package/src/shell/resolve-shell.ts +25 -0
- package/src/shell/shell-command-module.ts +53 -0
- package/src/shell/shell-command.ts +48 -0
- package/src/shell/spawn-inherited.ts +32 -0
- package/src/skills/__tests__/skill-read-command.test.ts +64 -0
- package/src/skills/__tests__/skills-command-module.test.ts +200 -0
- package/src/skills/index.ts +7 -0
- package/src/skills/skill-read-command.ts +60 -0
- package/src/skills/skills-command-module.ts +76 -0
- package/src/skills/skills-command.ts +108 -0
- package/src/statusline/__tests__/statusline-command-module.test.ts +96 -0
- package/src/statusline/index.ts +6 -0
- package/src/statusline/statusline-command-module.ts +55 -0
- package/src/statusline/statusline-command.ts +80 -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 +204 -0
- package/src/theme/index.ts +12 -0
- package/src/theme/theme-command-module.ts +221 -0
- package/src/user-local/__tests__/user-local-command.test.ts +180 -0
- package/src/user-local/index.ts +13 -0
- package/src/user-local/user-local-command-constants.ts +5 -0
- package/src/user-local/user-local-command-module.ts +65 -0
- package/src/user-local/user-local-command.ts +214 -0
- package/src/user-local/user-local-memory-command.ts +160 -0
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* FLOW-005: `/schedule` and `/monitor` command executors — create agent-wake background tasks.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { MonitorCommandRefusedError } from '@robota-sdk/agent-framework';
|
|
6
|
+
import { parseScheduleSpec } from './schedule-spec-parser.js';
|
|
7
|
+
|
|
8
|
+
import type { IAgentJobHostContext } from '@robota-sdk/agent-framework';
|
|
9
|
+
import type { ICommandResult } from '@robota-sdk/agent-interface-command';
|
|
10
|
+
import type { IBackgroundTaskState } from '@robota-sdk/agent-interface-execution';
|
|
11
|
+
|
|
12
|
+
const MAX_LABEL_LENGTH = 48;
|
|
13
|
+
|
|
14
|
+
function labelFor(prefix: string, instruction: string): string {
|
|
15
|
+
const snippet = instruction.slice(0, MAX_LABEL_LENGTH);
|
|
16
|
+
return `${prefix}: ${snippet}${instruction.length > MAX_LABEL_LENGTH ? '…' : ''}`;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
// SELFHOST-012: management subcommands over the existing scheduler. Create (FLOW-005) stays the default form —
|
|
20
|
+
// a create spec always begins with `in`/`cron`, so these keywords never collide with it.
|
|
21
|
+
const MANAGE_USAGE =
|
|
22
|
+
'Usage: /schedule list | pause <id> | resume <id> | edit <id> <spec> (or a create spec: in <N><s|m|h|d> … | cron "<expr>" …)';
|
|
23
|
+
|
|
24
|
+
/** One-line list view: id, status, cadence, label, and next-fire time. The `[status]` already conveys paused. */
|
|
25
|
+
function formatScheduleList(schedules: IBackgroundTaskState[]): string {
|
|
26
|
+
if (schedules.length === 0) return 'No schedules.';
|
|
27
|
+
return schedules
|
|
28
|
+
.map((s) => {
|
|
29
|
+
// `listSchedules()` only ever returns scheduled-kind tasks; the kind check is what lets
|
|
30
|
+
// TypeScript narrow to the member that carries `schedule`/`nextFireAt` (#2079).
|
|
31
|
+
const cadence =
|
|
32
|
+
s.kind === 'scheduled' ? (s.schedule?.cronExpression ?? '(unknown)') : '(unknown)';
|
|
33
|
+
const when = s.kind === 'scheduled' && s.nextFireAt ? `next ${s.nextFireAt}` : '—';
|
|
34
|
+
return `- ${s.id} [${s.status}] ${cadence} — ${s.label} (${when})`;
|
|
35
|
+
})
|
|
36
|
+
.join('\n');
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
async function executeScheduleManagement(
|
|
40
|
+
host: IAgentJobHostContext,
|
|
41
|
+
action: string,
|
|
42
|
+
rest: string,
|
|
43
|
+
now: number,
|
|
44
|
+
): Promise<ICommandResult | undefined> {
|
|
45
|
+
if (action === 'list') {
|
|
46
|
+
const schedules = host.listSchedules();
|
|
47
|
+
return {
|
|
48
|
+
message: formatScheduleList(schedules),
|
|
49
|
+
success: true,
|
|
50
|
+
data: { count: schedules.length },
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
if (action === 'pause' || action === 'resume') {
|
|
54
|
+
const id = rest.split(/\s+/)[0];
|
|
55
|
+
if (!id) return { message: MANAGE_USAGE, success: false };
|
|
56
|
+
if (action === 'pause') {
|
|
57
|
+
await host.pauseSchedule(id);
|
|
58
|
+
return { message: `Schedule paused: ${id}`, success: true, data: { taskId: id } };
|
|
59
|
+
}
|
|
60
|
+
await host.resumeSchedule(id);
|
|
61
|
+
return { message: `Schedule resumed: ${id}`, success: true, data: { taskId: id } };
|
|
62
|
+
}
|
|
63
|
+
if (action === 'edit') {
|
|
64
|
+
const idSpaceIdx = rest.indexOf(' ');
|
|
65
|
+
const id = idSpaceIdx === -1 ? rest : rest.slice(0, idSpaceIdx);
|
|
66
|
+
const spec = idSpaceIdx === -1 ? '' : rest.slice(idSpaceIdx + 1).trim();
|
|
67
|
+
if (!id || !spec) return { message: MANAGE_USAGE, success: false };
|
|
68
|
+
const parsed = parseScheduleSpec(spec, now);
|
|
69
|
+
if (!parsed.ok) return { message: parsed.error, success: false };
|
|
70
|
+
await host.editSchedule(id, {
|
|
71
|
+
cronExpression: parsed.spec.cronExpression,
|
|
72
|
+
agentInstruction: parsed.spec.instruction,
|
|
73
|
+
// CMD-009: `list` renders the label, so it must follow the instruction it summarizes.
|
|
74
|
+
label: labelFor('Scheduled', parsed.spec.instruction),
|
|
75
|
+
});
|
|
76
|
+
return {
|
|
77
|
+
message: `Schedule updated: ${id} (cron \`${parsed.spec.cronExpression}\`).`,
|
|
78
|
+
success: true,
|
|
79
|
+
data: { taskId: id, cronExpression: parsed.spec.cronExpression },
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
return undefined; // not a management subcommand — fall through to create
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export async function executeScheduleCommand(
|
|
86
|
+
host: IAgentJobHostContext,
|
|
87
|
+
args: string,
|
|
88
|
+
now: number = Date.now(),
|
|
89
|
+
): Promise<ICommandResult> {
|
|
90
|
+
const trimmed = args.trim();
|
|
91
|
+
const spaceIdx = trimmed.indexOf(' ');
|
|
92
|
+
const action = (spaceIdx === -1 ? trimmed : trimmed.slice(0, spaceIdx)).toLowerCase();
|
|
93
|
+
const rest = spaceIdx === -1 ? '' : trimmed.slice(spaceIdx + 1).trim();
|
|
94
|
+
|
|
95
|
+
const managed = await executeScheduleManagement(host, action, rest, now);
|
|
96
|
+
if (managed) return managed;
|
|
97
|
+
|
|
98
|
+
// Default: create (FLOW-005 form). On a parse failure also surface the management usage — a mistyped
|
|
99
|
+
// subcommand (e.g. `pasue <id>`) falls through here, and the create-only error hides list/pause/resume/edit.
|
|
100
|
+
const parsed = parseScheduleSpec(args, now);
|
|
101
|
+
if (!parsed.ok) return { message: `${parsed.error}\n${MANAGE_USAGE}`, success: false };
|
|
102
|
+
|
|
103
|
+
const { cronExpression, instruction, recurring } = parsed.spec;
|
|
104
|
+
const task = await host.spawnScheduledWake({
|
|
105
|
+
label: labelFor('Scheduled', instruction),
|
|
106
|
+
cronExpression,
|
|
107
|
+
agentInstruction: instruction,
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
const when = recurring ? `cron \`${cronExpression}\`` : `once at ${cronExpression}`;
|
|
111
|
+
return {
|
|
112
|
+
message: `Scheduled wake (${when}): "${instruction}" — task ${task.id}.`,
|
|
113
|
+
success: true,
|
|
114
|
+
data: { taskId: task.id, cronExpression, recurring },
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
const MONITOR_USAGE = 'Usage: /monitor "<command>" "<pattern>" <instruction>';
|
|
119
|
+
|
|
120
|
+
/** Parse `"<command>" "<pattern>" <instruction>` (quotes may be single or double). */
|
|
121
|
+
function parseMonitorArgs(
|
|
122
|
+
args: string,
|
|
123
|
+
): { command: string; matchPattern: string; instruction: string } | null {
|
|
124
|
+
// `(\S.*)` — not `(.+)`: `\s+(.+)` is ambiguous (`.` also matches spaces), so a failing input
|
|
125
|
+
// runs in O(n^2). Requiring a non-space first character pins the split point after the greedy
|
|
126
|
+
// `\s+` and makes the match linear, without changing which inputs are accepted.
|
|
127
|
+
const match = /^["']([^"']+)["']\s+["']([^"']+)["']\s+(\S.*)$/.exec(args.trim());
|
|
128
|
+
if (!match) return null;
|
|
129
|
+
const instruction = match[3]!.trim();
|
|
130
|
+
if (instruction.length === 0) return null;
|
|
131
|
+
return { command: match[1]!, matchPattern: match[2]!, instruction };
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
export async function executeMonitorCommand(
|
|
135
|
+
host: IAgentJobHostContext,
|
|
136
|
+
args: string,
|
|
137
|
+
): Promise<ICommandResult> {
|
|
138
|
+
const parsed = parseMonitorArgs(args);
|
|
139
|
+
if (!parsed) return { message: MONITOR_USAGE, success: false };
|
|
140
|
+
|
|
141
|
+
let task: IBackgroundTaskState;
|
|
142
|
+
try {
|
|
143
|
+
task = await host.spawnMonitorWake({
|
|
144
|
+
label: labelFor('Monitor', parsed.instruction),
|
|
145
|
+
command: parsed.command,
|
|
146
|
+
matchPattern: parsed.matchPattern,
|
|
147
|
+
agentInstruction: parsed.instruction,
|
|
148
|
+
});
|
|
149
|
+
} catch (error) {
|
|
150
|
+
if (error instanceof MonitorCommandRefusedError) {
|
|
151
|
+
return { message: error.message, success: false };
|
|
152
|
+
}
|
|
153
|
+
throw error;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
return {
|
|
157
|
+
message: `Monitoring \`${parsed.command}\` for /${parsed.matchPattern}/ — task ${task.id}.`,
|
|
158
|
+
success: true,
|
|
159
|
+
data: { taskId: task.id, matchPattern: parsed.matchPattern },
|
|
160
|
+
};
|
|
161
|
+
}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* FLOW-005: parse the `<when>` portion of `/schedule` into a cron expression croner accepts.
|
|
3
|
+
*
|
|
4
|
+
* Two forms:
|
|
5
|
+
* in <N><unit> <instruction> — one-shot, fires once at now + duration (s|m|h|d)
|
|
6
|
+
* cron "<expr>" <instruction> — recurring, a standard cron expression
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
export interface IScheduleSpec {
|
|
10
|
+
cronExpression: string;
|
|
11
|
+
instruction: string;
|
|
12
|
+
recurring: boolean;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export type TScheduleParseResult = { ok: true; spec: IScheduleSpec } | { ok: false; error: string };
|
|
16
|
+
|
|
17
|
+
const DURATION_MS: Record<string, number> = {
|
|
18
|
+
s: 1_000,
|
|
19
|
+
m: 60_000,
|
|
20
|
+
h: 3_600_000,
|
|
21
|
+
d: 86_400_000,
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
const USAGE =
|
|
25
|
+
'Usage: /schedule in <N><s|m|h|d> <instruction> | /schedule cron "<expr>" <instruction>';
|
|
26
|
+
|
|
27
|
+
export function parseScheduleSpec(args: string, now: number): TScheduleParseResult {
|
|
28
|
+
const trimmed = args.trim();
|
|
29
|
+
if (trimmed.length === 0) return { ok: false, error: USAGE };
|
|
30
|
+
|
|
31
|
+
if (trimmed.startsWith('in ')) {
|
|
32
|
+
const rest = trimmed.slice(3).trim();
|
|
33
|
+
const spaceIdx = rest.indexOf(' ');
|
|
34
|
+
if (spaceIdx === -1) return { ok: false, error: `Missing instruction. ${USAGE}` };
|
|
35
|
+
const durationToken = rest.slice(0, spaceIdx);
|
|
36
|
+
const instruction = rest.slice(spaceIdx + 1).trim();
|
|
37
|
+
const match = /^(\d+)(s|m|h|d)$/.exec(durationToken);
|
|
38
|
+
if (!match) return { ok: false, error: `Invalid duration "${durationToken}". ${USAGE}` };
|
|
39
|
+
if (instruction.length === 0) return { ok: false, error: `Missing instruction. ${USAGE}` };
|
|
40
|
+
const ms = parseInt(match[1]!, 10) * DURATION_MS[match[2]!]!;
|
|
41
|
+
return {
|
|
42
|
+
ok: true,
|
|
43
|
+
spec: {
|
|
44
|
+
cronExpression: new Date(now + ms).toISOString(),
|
|
45
|
+
instruction,
|
|
46
|
+
recurring: false,
|
|
47
|
+
},
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
if (trimmed.startsWith('cron ')) {
|
|
52
|
+
const rest = trimmed.slice(5).trim();
|
|
53
|
+
// `(\S.*)` — not `(.+)`: `\s+(.+)` is ambiguous (`.` also matches spaces), so a failing
|
|
54
|
+
// input runs in O(n^2). Requiring the instruction to start with a non-space pins the split
|
|
55
|
+
// point after the greedy `\s+`, making the match linear. Behaviour is unchanged because the
|
|
56
|
+
// greedy `\s+` already consumed every leading space and the capture is trimmed below.
|
|
57
|
+
const quoted = /^["']([^"']+)["']\s+(\S.*)$/.exec(rest);
|
|
58
|
+
if (!quoted) {
|
|
59
|
+
return { ok: false, error: `cron form needs a quoted expression. ${USAGE}` };
|
|
60
|
+
}
|
|
61
|
+
const instruction = quoted[2]!.trim();
|
|
62
|
+
if (instruction.length === 0) return { ok: false, error: `Missing instruction. ${USAGE}` };
|
|
63
|
+
return {
|
|
64
|
+
ok: true,
|
|
65
|
+
spec: { cronExpression: quoted[1]!.trim(), instruction, recurring: true },
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
return { ok: false, error: USAGE };
|
|
70
|
+
}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import { calculateCost, formatTokens, formatUsd } from '../model-pricing.js';
|
|
3
|
+
|
|
4
|
+
describe('calculateCost', () => {
|
|
5
|
+
it('returns exact cost for known Anthropic Sonnet model', () => {
|
|
6
|
+
const cost = calculateCost('claude-sonnet-4-5', 1_000_000, 1_000_000);
|
|
7
|
+
expect(cost).toBeCloseTo(3 + 15, 5);
|
|
8
|
+
});
|
|
9
|
+
|
|
10
|
+
it('returns exact cost for known Anthropic Opus model', () => {
|
|
11
|
+
const cost = calculateCost('claude-opus-4-7', 1_000_000, 1_000_000);
|
|
12
|
+
expect(cost).toBeCloseTo(15 + 75, 5);
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
it('calculates fractional token costs correctly', () => {
|
|
16
|
+
const cost = calculateCost('claude-sonnet-4-5', 45_000, 12_000);
|
|
17
|
+
expect(cost).toBeDefined();
|
|
18
|
+
expect(cost!).toBeCloseTo((45_000 / 1_000_000) * 3 + (12_000 / 1_000_000) * 15, 8);
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
it('falls back to pattern matching for claude-sonnet variant', () => {
|
|
22
|
+
const cost = calculateCost('claude-sonnet-99-99', 1_000_000, 0);
|
|
23
|
+
expect(cost).toBeCloseTo(3, 5);
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
it.each(['deepseek-v3-turbo', 'deepseek/deepseek-v4.1-flash-1017'])(
|
|
27
|
+
'leaves the cost of unregistered model %s unknown',
|
|
28
|
+
(model) => {
|
|
29
|
+
expect(calculateCost(model, 1_000_000, 0)).toBeUndefined();
|
|
30
|
+
},
|
|
31
|
+
);
|
|
32
|
+
|
|
33
|
+
it('returns undefined for completely unknown model', () => {
|
|
34
|
+
const cost = calculateCost('unknown-model-xyz-123', 1_000, 1_000);
|
|
35
|
+
expect(cost).toBeUndefined();
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
it('returns zero cost when both token counts are zero', () => {
|
|
39
|
+
const cost = calculateCost('claude-sonnet-4-5', 0, 0);
|
|
40
|
+
expect(cost).toBe(0);
|
|
41
|
+
});
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
describe('formatUsd', () => {
|
|
45
|
+
it('formats small amounts with 4 decimal places', () => {
|
|
46
|
+
expect(formatUsd(0.0043)).toBe('$0.0043');
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
it('formats sub-dollar amounts with 3 decimal places', () => {
|
|
50
|
+
expect(formatUsd(0.187)).toBe('$0.187');
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
it('formats dollar+ amounts with 2 decimal places', () => {
|
|
54
|
+
expect(formatUsd(1.24)).toBe('$1.24');
|
|
55
|
+
});
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
describe('formatTokens', () => {
|
|
59
|
+
it('formats token counts with comma separators', () => {
|
|
60
|
+
expect(formatTokens(45000)).toBe('45,000');
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
it('formats small counts without separator', () => {
|
|
64
|
+
expect(formatTokens(999)).toBe('999');
|
|
65
|
+
});
|
|
66
|
+
});
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CMD-004 Phase 2 (TC-10) — red-first `/rename` host-side persistence proof.
|
|
3
|
+
*
|
|
4
|
+
* Pre-Stage-B, `executeRenameCommand` only RETURNS the `session-renamed` effect and the mutation
|
|
5
|
+
* lives in the TUI renderer (`useSideEffects.ts` calls `interactiveSession.setName`), so a session
|
|
6
|
+
* with NO TUI attached renames nothing — these tests were recorded FAILING against that state
|
|
7
|
+
* (evidence in the spec's Evidence Log). Stage B makes the HOST execute the rename inside the
|
|
8
|
+
* `executeCommand` pipeline (direct-on-session action), strips the applied action from the returned
|
|
9
|
+
* result (so the TUI's duplicate application cannot fire — no double rename), and broadcasts
|
|
10
|
+
* `session_renamed` so every attached surface updates its title.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import { describe, expect, it, vi } from 'vitest';
|
|
14
|
+
import { InteractiveSession } from '@robota-sdk/agent-framework';
|
|
15
|
+
import type { ISessionRenamedEvent } from '@robota-sdk/agent-interface-session';
|
|
16
|
+
import { createSessionCommandModule } from '../session-command-module.js';
|
|
17
|
+
|
|
18
|
+
function createRuntimeSession(): Record<string, unknown> {
|
|
19
|
+
return {
|
|
20
|
+
run: vi.fn().mockResolvedValue('answer'),
|
|
21
|
+
abort: vi.fn(),
|
|
22
|
+
clearHistory: vi.fn(),
|
|
23
|
+
getHistory: vi.fn().mockReturnValue([]),
|
|
24
|
+
injectMessage: vi.fn(),
|
|
25
|
+
getContextState: () => ({
|
|
26
|
+
maxTokens: 100,
|
|
27
|
+
usedTokens: 0,
|
|
28
|
+
usedPercentage: 0,
|
|
29
|
+
remainingPercentage: 100,
|
|
30
|
+
}),
|
|
31
|
+
getSessionId: () => 'session_rename_tc10',
|
|
32
|
+
getMessageCount: () => 0,
|
|
33
|
+
getSystemMessage: vi.fn().mockReturnValue('system'),
|
|
34
|
+
getToolSchemas: vi.fn().mockReturnValue([]),
|
|
35
|
+
getEventService: () => ({ subscribe: () => {}, unsubscribe: () => {} }),
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
describe('CMD-004 TC-10 — /rename persists host-side without any TUI handler', () => {
|
|
40
|
+
it('mutates the session name via executeCommand alone and broadcasts session_renamed', async () => {
|
|
41
|
+
const session = new InteractiveSession({
|
|
42
|
+
session: createRuntimeSession() as never,
|
|
43
|
+
commandModules: [createSessionCommandModule()],
|
|
44
|
+
});
|
|
45
|
+
const renamedEvents: ISessionRenamedEvent[] = [];
|
|
46
|
+
session.on('session_renamed', (event) => renamedEvents.push(event));
|
|
47
|
+
|
|
48
|
+
const result = await session.executeCommand('rename', 'My Renamed Session');
|
|
49
|
+
|
|
50
|
+
expect(result?.success).toBe(true);
|
|
51
|
+
expect(result?.message).toBe('Session renamed to "My Renamed Session".');
|
|
52
|
+
// The HOST executed the mutation — no surface handler involved (the defect this TC pins).
|
|
53
|
+
expect(session.getName()).toBe('My Renamed Session');
|
|
54
|
+
// Broadcast so every attached surface (incl. co-driving ones) updates its title.
|
|
55
|
+
expect(renamedEvents).toEqual([{ name: 'My Renamed Session' }]);
|
|
56
|
+
// The applied host action is CONSUMED from the returned result — no surface can double-apply it.
|
|
57
|
+
expect(result?.hostActions).toBeUndefined();
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
it('applies the rename exactly once (single-execution guard)', async () => {
|
|
61
|
+
const session = new InteractiveSession({
|
|
62
|
+
session: createRuntimeSession() as never,
|
|
63
|
+
commandModules: [createSessionCommandModule()],
|
|
64
|
+
});
|
|
65
|
+
const setNameSpy = vi.spyOn(session, 'setName');
|
|
66
|
+
|
|
67
|
+
await session.executeCommand('rename', 'once');
|
|
68
|
+
|
|
69
|
+
expect(setNameSpy).toHaveBeenCalledTimes(1);
|
|
70
|
+
expect(setNameSpy).toHaveBeenCalledWith('once');
|
|
71
|
+
});
|
|
72
|
+
});
|