@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.
Files changed (251) hide show
  1. package/CHANGELOG.md +1351 -0
  2. package/LICENSE +661 -0
  3. package/README.md +100 -0
  4. package/dist/node/index.cjs +121 -0
  5. package/dist/node/index.d.cts +1053 -0
  6. package/dist/node/index.d.cts.map +1 -0
  7. package/dist/node/index.d.ts +1053 -0
  8. package/dist/node/index.d.ts.map +1 -0
  9. package/dist/node/index.js +122 -0
  10. package/dist/node/index.js.map +1 -0
  11. package/package.json +83 -0
  12. package/src/advisor/advisor-command-module.ts +47 -0
  13. package/src/advisor/advisor-command.test.ts +102 -0
  14. package/src/advisor/advisor-command.ts +55 -0
  15. package/src/advisor/advisor-cost-functional.test.ts +92 -0
  16. package/src/advisor/index.ts +6 -0
  17. package/src/agent/__tests__/agent-command.test.ts +597 -0
  18. package/src/agent/agent-command-module.ts +89 -0
  19. package/src/agent/agent-command-parser.ts +180 -0
  20. package/src/agent/agent-command.ts +255 -0
  21. package/src/agent/index.ts +7 -0
  22. package/src/background/__tests__/background-command-module.test.ts +261 -0
  23. package/src/background/background-command-module.ts +52 -0
  24. package/src/background/background-command.ts +65 -0
  25. package/src/background/index.ts +6 -0
  26. package/src/command-module-utils.test.ts +37 -0
  27. package/src/command-module-utils.ts +35 -0
  28. package/src/compact/__tests__/compact-command-module.test.ts +181 -0
  29. package/src/compact/compact-command-module.ts +59 -0
  30. package/src/compact/compact-command.ts +33 -0
  31. package/src/compact/index.ts +6 -0
  32. package/src/context/__tests__/context-command-module.test.ts +502 -0
  33. package/src/context/auto-compact-format.ts +25 -0
  34. package/src/context/context-breakdown.ts +295 -0
  35. package/src/context/context-command-module.ts +82 -0
  36. package/src/context/context-command.ts +291 -0
  37. package/src/context/index.ts +6 -0
  38. package/src/default/__tests__/__snapshots__/model-exposure.test.ts.snap +121 -0
  39. package/src/default/__tests__/default-command-modules.test.ts +192 -0
  40. package/src/default/__tests__/model-exposure.test.ts +269 -0
  41. package/src/default/__tests__/org-policy-forwarding.test.ts +106 -0
  42. package/src/default/__tests__/provider-environment-forwarding.test.ts +98 -0
  43. package/src/default/default-command-modules.ts +247 -0
  44. package/src/default/index.ts +5 -0
  45. package/src/devices/__tests__/devices-command-module.test.ts +299 -0
  46. package/src/devices/devices-command-module.ts +308 -0
  47. package/src/devices/devices-command-port.ts +143 -0
  48. package/src/devices/index.ts +14 -0
  49. package/src/doctor/__tests__/doctor-command-module.test.ts +145 -0
  50. package/src/doctor/__tests__/doctor-display-vocabulary.test.ts +38 -0
  51. package/src/doctor/__tests__/doctor-fixture.ts +146 -0
  52. package/src/doctor/__tests__/doctor-redaction.test.ts +29 -0
  53. package/src/doctor/__tests__/doctor-runner.test.ts +484 -0
  54. package/src/doctor/doctor-command-module.ts +132 -0
  55. package/src/doctor/doctor-extensions-probe.ts +323 -0
  56. package/src/doctor/doctor-node-deps.ts +88 -0
  57. package/src/doctor/doctor-redaction.ts +99 -0
  58. package/src/doctor/doctor-render.ts +70 -0
  59. package/src/doctor/doctor-repair.ts +136 -0
  60. package/src/doctor/doctor-runner.ts +122 -0
  61. package/src/doctor/doctor-settings-probe.ts +239 -0
  62. package/src/doctor/doctor-storage-probe.ts +187 -0
  63. package/src/doctor/doctor-types.ts +109 -0
  64. package/src/doctor/index.ts +32 -0
  65. package/src/editor/__tests__/editor-command-functional.test.ts +124 -0
  66. package/src/editor/editor-command-module.ts +53 -0
  67. package/src/editor/editor-command.ts +58 -0
  68. package/src/editor/index.ts +7 -0
  69. package/src/editor/resolve-editor.ts +21 -0
  70. package/src/effort/effort-command-module.ts +51 -0
  71. package/src/effort/effort-command.test.ts +203 -0
  72. package/src/effort/effort-command.ts +136 -0
  73. package/src/effort/index.ts +6 -0
  74. package/src/events/__tests__/events-command.test.ts +73 -0
  75. package/src/events/events-command-module.ts +49 -0
  76. package/src/events/events-command.ts +69 -0
  77. package/src/events/index.ts +2 -0
  78. package/src/exit/__tests__/exit-command-module.test.ts +61 -0
  79. package/src/exit/exit-command-module.ts +47 -0
  80. package/src/exit/exit-command.ts +26 -0
  81. package/src/exit/index.ts +6 -0
  82. package/src/fork/__tests__/fork-command.test.ts +184 -0
  83. package/src/fork/fork-command-module.ts +61 -0
  84. package/src/fork/fork-command.ts +162 -0
  85. package/src/fork/index.ts +8 -0
  86. package/src/git/__tests__/fake-git-port.ts +35 -0
  87. package/src/git/__tests__/git-command-module.test.ts +111 -0
  88. package/src/git/__tests__/git-commit.test.ts +185 -0
  89. package/src/git/git-command-module.ts +138 -0
  90. package/src/git/git-commit.ts +254 -0
  91. package/src/git/index.ts +20 -0
  92. package/src/goal/__tests__/goal-command.test.ts +80 -0
  93. package/src/goal/goal-command-module.ts +49 -0
  94. package/src/goal/goal-command.ts +69 -0
  95. package/src/goal/index.ts +6 -0
  96. package/src/handoff/__tests__/handoff-command-module.test.ts +25 -0
  97. package/src/handoff/__tests__/handoff-command.test.ts +218 -0
  98. package/src/handoff/handoff-command-module.ts +49 -0
  99. package/src/handoff/handoff-command.ts +187 -0
  100. package/src/handoff/index.ts +6 -0
  101. package/src/help/__tests__/help-command-module.test.ts +134 -0
  102. package/src/help/__tests__/help-command.test.ts +130 -0
  103. package/src/help/help-command-module.ts +47 -0
  104. package/src/help/help-command.ts +11 -0
  105. package/src/help/index.ts +6 -0
  106. package/src/index.ts +163 -0
  107. package/src/keybindings/__tests__/keybindings-command-module.test.ts +57 -0
  108. package/src/keybindings/index.ts +6 -0
  109. package/src/keybindings/keybindings-command-module.ts +88 -0
  110. package/src/language/__tests__/language-command-module.test.ts +126 -0
  111. package/src/language/index.ts +6 -0
  112. package/src/language/language-command-module.ts +55 -0
  113. package/src/language/language-command.ts +52 -0
  114. package/src/mcp-activation/__tests__/mcp-activation-command.test.ts +895 -0
  115. package/src/mcp-activation/__tests__/mcp-model-view.test.ts +242 -0
  116. package/src/mcp-activation/__tests__/mcp-skill-command.test.ts +141 -0
  117. package/src/mcp-activation/index.ts +12 -0
  118. package/src/mcp-activation/mcp-activation-command-module.ts +123 -0
  119. package/src/mcp-activation/mcp-activation-command.ts +662 -0
  120. package/src/mcp-activation/mcp-model-notice.ts +99 -0
  121. package/src/mcp-activation/mcp-model-status.ts +104 -0
  122. package/src/memory/__tests__/memory-command-module.test.ts +476 -0
  123. package/src/memory/__tests__/memory-command-unavailable.test.ts +63 -0
  124. package/src/memory/index.ts +6 -0
  125. package/src/memory/memory-command-module.ts +77 -0
  126. package/src/memory/memory-command.ts +326 -0
  127. package/src/mode/__tests__/mode-command-module.test.ts +230 -0
  128. package/src/mode/index.ts +6 -0
  129. package/src/mode/mode-command-module.ts +55 -0
  130. package/src/mode/mode-command.ts +67 -0
  131. package/src/model/__tests__/model-command-module.test.ts +80 -0
  132. package/src/model/__tests__/model-command.test.ts +265 -0
  133. package/src/model/index.ts +6 -0
  134. package/src/model/model-command-module.ts +56 -0
  135. package/src/model/model-command.ts +249 -0
  136. package/src/output-style/__tests__/output-style-command.test.ts +96 -0
  137. package/src/output-style/index.ts +6 -0
  138. package/src/output-style/output-style-command-module.ts +143 -0
  139. package/src/peers/__tests__/peers-command-module.test.ts +24 -0
  140. package/src/peers/__tests__/peers-command.test.ts +399 -0
  141. package/src/peers/index.ts +9 -0
  142. package/src/peers/peers-command-module.ts +49 -0
  143. package/src/peers/peers-command.ts +220 -0
  144. package/src/permissions/__tests__/permissions-command-module.test.ts +276 -0
  145. package/src/permissions/index.ts +6 -0
  146. package/src/permissions/permissions-command-module.ts +55 -0
  147. package/src/permissions/permissions-command.ts +89 -0
  148. package/src/plan/__tests__/plan-command.test.ts +129 -0
  149. package/src/plan/index.ts +9 -0
  150. package/src/plan/plan-command-module.ts +49 -0
  151. package/src/plan/plan-command.ts +92 -0
  152. package/src/plugin/__tests__/plugin-active-turn.test.ts +100 -0
  153. package/src/plugin/__tests__/plugin-command-module.test.ts +300 -0
  154. package/src/plugin/__tests__/plugin-host-boundary.test.ts +11 -0
  155. package/src/plugin/index.ts +7 -0
  156. package/src/plugin/plugin-command-module.ts +85 -0
  157. package/src/plugin/plugin-command.ts +271 -0
  158. package/src/preset/__tests__/host-supplied-preset-registry.test.ts +98 -0
  159. package/src/preset/__tests__/preset-command-module.test.ts +220 -0
  160. package/src/preset/index.ts +6 -0
  161. package/src/preset/preset-command-module.ts +80 -0
  162. package/src/preset/preset-command.ts +148 -0
  163. package/src/provider/__tests__/optional-provider-credentials.test.ts +130 -0
  164. package/src/provider/__tests__/org-policy.test.ts +174 -0
  165. package/src/provider/__tests__/provider-command-module.test.ts +884 -0
  166. package/src/provider/__tests__/provider-connection-setup.test.ts +567 -0
  167. package/src/provider/__tests__/provider-reference-switch.test.ts +113 -0
  168. package/src/provider/__tests__/provider-setup-flow.test.ts +244 -0
  169. package/src/provider/__tests__/provider-startup-host-settings.test.ts +16 -0
  170. package/src/provider/__tests__/scripted-interaction.ts +40 -0
  171. package/src/provider/index.ts +32 -0
  172. package/src/provider/provider-command-execution.ts +235 -0
  173. package/src/provider/provider-command-module.ts +82 -0
  174. package/src/provider/provider-command-profile-lifecycle.ts +239 -0
  175. package/src/provider/provider-command-profile-operations.ts +218 -0
  176. package/src/provider/provider-command-profile.ts +116 -0
  177. package/src/provider/provider-command-setup.ts +79 -0
  178. package/src/provider/provider-connection-setup.ts +287 -0
  179. package/src/provider/provider-connection-validation.ts +38 -0
  180. package/src/provider/provider-onboarding.ts +73 -0
  181. package/src/provider/provider-prompt-interaction.ts +28 -0
  182. package/src/provider/provider-setup-ask.ts +67 -0
  183. package/src/provider/provider-setup-flow.ts +326 -0
  184. package/src/provider/provider-startup.ts +225 -0
  185. package/src/remote-control/__tests__/remote-control-command-module.test.ts +36 -0
  186. package/src/remote-control/__tests__/remote-control-command.test.ts +216 -0
  187. package/src/remote-control/index.ts +8 -0
  188. package/src/remote-control/remote-control-command-module.ts +61 -0
  189. package/src/remote-control/remote-control-command.ts +138 -0
  190. package/src/reset/__tests__/reset-command-module.test.ts +63 -0
  191. package/src/reset/index.ts +2 -0
  192. package/src/reset/reset-command-module.ts +47 -0
  193. package/src/reset/reset-command.ts +10 -0
  194. package/src/rewind/__tests__/rewind-command-module.test.ts +287 -0
  195. package/src/rewind/index.ts +2 -0
  196. package/src/rewind/rewind-command-module.ts +56 -0
  197. package/src/rewind/rewind-command.ts +279 -0
  198. package/src/sandbox/__tests__/sandbox-command.test.ts +80 -0
  199. package/src/sandbox/index.ts +2 -0
  200. package/src/sandbox/sandbox-command-module.ts +56 -0
  201. package/src/sandbox/sandbox-command.ts +94 -0
  202. package/src/schedule/__tests__/loop-command-functional.test.ts +399 -0
  203. package/src/schedule/__tests__/loop-command.test.ts +594 -0
  204. package/src/schedule/__tests__/loop-jitter.test.ts +25 -0
  205. package/src/schedule/__tests__/monitor-model-permission-functional.test.ts +126 -0
  206. package/src/schedule/__tests__/schedule-command.test.ts +247 -0
  207. package/src/schedule/__tests__/schedule-redos.test.ts +114 -0
  208. package/src/schedule/index.ts +12 -0
  209. package/src/schedule/loop-command.ts +427 -0
  210. package/src/schedule/loop-jitter.ts +41 -0
  211. package/src/schedule/schedule-command-module.ts +184 -0
  212. package/src/schedule/schedule-command.ts +161 -0
  213. package/src/schedule/schedule-spec-parser.ts +70 -0
  214. package/src/session/__tests__/model-pricing.test.ts +66 -0
  215. package/src/session/__tests__/rename-host-persistence.test.ts +72 -0
  216. package/src/session/__tests__/session-command-module.test.ts +589 -0
  217. package/src/session/index.ts +17 -0
  218. package/src/session/model-pricing.ts +26 -0
  219. package/src/session/session-command-module.ts +197 -0
  220. package/src/session/session-command.ts +272 -0
  221. package/src/settings/index.ts +7 -0
  222. package/src/settings/settings-command-module.ts +47 -0
  223. package/src/shell/__tests__/resolve-shell-host.test.ts +11 -0
  224. package/src/shell/__tests__/shell-command-functional.test.ts +118 -0
  225. package/src/shell/index.ts +8 -0
  226. package/src/shell/resolve-shell.ts +25 -0
  227. package/src/shell/shell-command-module.ts +53 -0
  228. package/src/shell/shell-command.ts +48 -0
  229. package/src/shell/spawn-inherited.ts +32 -0
  230. package/src/skills/__tests__/skill-read-command.test.ts +64 -0
  231. package/src/skills/__tests__/skills-command-module.test.ts +200 -0
  232. package/src/skills/index.ts +7 -0
  233. package/src/skills/skill-read-command.ts +60 -0
  234. package/src/skills/skills-command-module.ts +76 -0
  235. package/src/skills/skills-command.ts +108 -0
  236. package/src/statusline/__tests__/statusline-command-module.test.ts +96 -0
  237. package/src/statusline/index.ts +6 -0
  238. package/src/statusline/statusline-command-module.ts +55 -0
  239. package/src/statusline/statusline-command.ts +80 -0
  240. package/src/terminal-client/__tests__/terminal-client-commands.test.ts +216 -0
  241. package/src/terminal-client/index.ts +5 -0
  242. package/src/terminal-client/terminal-client-commands.ts +109 -0
  243. package/src/theme/__tests__/theme-command.test.ts +204 -0
  244. package/src/theme/index.ts +12 -0
  245. package/src/theme/theme-command-module.ts +221 -0
  246. package/src/user-local/__tests__/user-local-command.test.ts +180 -0
  247. package/src/user-local/index.ts +13 -0
  248. package/src/user-local/user-local-command-constants.ts +5 -0
  249. package/src/user-local/user-local-command-module.ts +65 -0
  250. package/src/user-local/user-local-command.ts +214 -0
  251. package/src/user-local/user-local-memory-command.ts +160 -0
@@ -0,0 +1,197 @@
1
+ import {
2
+ CLEAR_COMMAND_DESCRIPTION,
3
+ COST_COMMAND_DESCRIPTION,
4
+ RENAME_COMMAND_DESCRIPTION,
5
+ RESUME_COMMAND_DESCRIPTION,
6
+ VALIDATE_SESSION_COMMAND_DESCRIPTION,
7
+ } from '@robota-sdk/agent-framework';
8
+
9
+ import {
10
+ CD_COMMAND_DESCRIPTION,
11
+ executeCdCommand,
12
+ executeClearCommand,
13
+ executeCostCommand,
14
+ executeRenameCommand,
15
+ executeResumeCommand,
16
+ executeValidateSessionCommand,
17
+ } from './session-command.js';
18
+ import { createSystemCommandFromEntry } from '../command-module-utils.js';
19
+
20
+ import type { ICommandModule, ISystemCommand } from '@robota-sdk/agent-framework';
21
+ import type { ICommand, ICommandSource } from '@robota-sdk/agent-interface-command';
22
+
23
+ export function createClearCommandEntry(): ICommand {
24
+ return {
25
+ name: 'clear',
26
+ displayName: 'Clear History',
27
+ description: CLEAR_COMMAND_DESCRIPTION,
28
+ source: 'session',
29
+ // User-only: wiping the conversation is the user's decision.
30
+ modelInvocable: false,
31
+ userInvocable: true,
32
+ };
33
+ }
34
+
35
+ export function createRenameCommandEntry(): ICommand {
36
+ return {
37
+ name: 'rename',
38
+ displayName: 'Rename Session',
39
+ description: RENAME_COMMAND_DESCRIPTION,
40
+ argumentHint: '<name>',
41
+ source: 'session',
42
+ // User-only: the session's name is the user's label; UI-only.
43
+ modelInvocable: false,
44
+ userInvocable: true,
45
+ };
46
+ }
47
+
48
+ export function createCdCommandEntry(): ICommand {
49
+ return {
50
+ name: 'cd',
51
+ displayName: 'Change Directory',
52
+ description: CD_COMMAND_DESCRIPTION,
53
+ argumentHint: '<directory>',
54
+ source: 'session',
55
+ // A move re-roots what every later tool call may touch; only the user decides it.
56
+ modelInvocable: false,
57
+ userInvocable: true,
58
+ };
59
+ }
60
+
61
+ export function createResumeCommandEntry(): ICommand {
62
+ return {
63
+ name: 'resume',
64
+ displayName: 'Resume Session',
65
+ description: RESUME_COMMAND_DESCRIPTION,
66
+ source: 'session',
67
+ // User-only: switching to another session is the user's decision.
68
+ modelInvocable: false,
69
+ userInvocable: true,
70
+ };
71
+ }
72
+
73
+ export function createCostCommandEntry(): ICommand {
74
+ return {
75
+ name: 'cost',
76
+ displayName: 'Session Cost',
77
+ description: COST_COMMAND_DESCRIPTION,
78
+ // Model-invocable for the read-only report only: the model can weigh cost before an expensive
79
+ // step. The monthly budget is the user's spending decision, so `budget` stays user-only.
80
+ modelDescription:
81
+ 'Report this session’s token usage and estimated cost. Use it before an expensive step (a ' +
82
+ 'large refactor, many subagents) or when the user asks what the session has cost. Returns ' +
83
+ 'the message count, input/output token totals and the estimated USD cost, with the remaining ' +
84
+ 'monthly budget when one is set. Setting or clearing the budget is the user’s decision: suggest `/cost budget <amount>`.',
85
+ argumentHint: '[budget [<amount>|clear]]',
86
+ // The bare `/cost` is the report, so choosing it from a menu runs it.
87
+ runsBare: true,
88
+ source: 'session',
89
+ modelInvocable: true,
90
+ userInvocable: true,
91
+ subcommands: [
92
+ {
93
+ name: 'budget',
94
+ description: 'Show, set, or clear the monthly budget',
95
+ argumentHint: '[<amount>|clear]',
96
+ source: 'session',
97
+ modelInvocable: false,
98
+ },
99
+ ],
100
+ };
101
+ }
102
+
103
+ export function createValidateSessionCommandEntry(): ICommand {
104
+ return {
105
+ name: 'validate-session',
106
+ displayName: 'Validate Session',
107
+ description: VALIDATE_SESSION_COMMAND_DESCRIPTION,
108
+ source: 'session',
109
+ // User-only: a diagnostic of the replay log for the user; nothing the model acts on.
110
+ modelInvocable: false,
111
+ userInvocable: true,
112
+ };
113
+ }
114
+
115
+ function createClearSystemCommand(): ISystemCommand {
116
+ const entry = createClearCommandEntry();
117
+ return createSystemCommandFromEntry(entry, {
118
+ requiresPermission: false,
119
+ lifecycle: 'inline',
120
+ execute: executeClearCommand,
121
+ });
122
+ }
123
+
124
+ function createRenameSystemCommand(): ISystemCommand {
125
+ const entry = createRenameCommandEntry();
126
+ return createSystemCommandFromEntry(entry, {
127
+ requiresPermission: false,
128
+ lifecycle: 'inline',
129
+ execute: executeRenameCommand,
130
+ });
131
+ }
132
+
133
+ function createCdSystemCommand(): ISystemCommand {
134
+ const entry = createCdCommandEntry();
135
+ return createSystemCommandFromEntry(entry, {
136
+ requiresPermission: false,
137
+ lifecycle: 'inline',
138
+ execute: executeCdCommand,
139
+ });
140
+ }
141
+
142
+ function createResumeSystemCommand(): ISystemCommand {
143
+ const entry = createResumeCommandEntry();
144
+ return createSystemCommandFromEntry(entry, {
145
+ requiresPermission: false,
146
+ lifecycle: 'inline',
147
+ execute: executeResumeCommand,
148
+ });
149
+ }
150
+
151
+ function createCostSystemCommand(): ISystemCommand {
152
+ const entry = createCostCommandEntry();
153
+ return createSystemCommandFromEntry(entry, {
154
+ requiresPermission: false,
155
+ lifecycle: 'inline',
156
+ execute: executeCostCommand,
157
+ });
158
+ }
159
+
160
+ function createValidateSessionSystemCommand(): ISystemCommand {
161
+ const entry = createValidateSessionCommandEntry();
162
+ return createSystemCommandFromEntry(entry, {
163
+ requiresPermission: false,
164
+ lifecycle: 'inline',
165
+ execute: executeValidateSessionCommand,
166
+ });
167
+ }
168
+
169
+ export class SessionCommandSource implements ICommandSource {
170
+ readonly name = 'session';
171
+
172
+ getCommands(): ICommand[] {
173
+ return [
174
+ createClearCommandEntry(),
175
+ createRenameCommandEntry(),
176
+ createCdCommandEntry(),
177
+ createResumeCommandEntry(),
178
+ createCostCommandEntry(),
179
+ createValidateSessionCommandEntry(),
180
+ ];
181
+ }
182
+ }
183
+
184
+ export function createSessionCommandModule(): ICommandModule {
185
+ return {
186
+ name: 'agent-command-session',
187
+ commandSources: [new SessionCommandSource()],
188
+ systemCommands: [
189
+ createClearSystemCommand(),
190
+ createRenameSystemCommand(),
191
+ createCdSystemCommand(),
192
+ createResumeSystemCommand(),
193
+ createCostSystemCommand(),
194
+ createValidateSessionSystemCommand(),
195
+ ],
196
+ };
197
+ }
@@ -0,0 +1,272 @@
1
+ import { confirmAction, isConfirmed } from '@robota-sdk/agent-core';
2
+ import {
3
+ RENAME_COMMAND_USAGE,
4
+ clearConversationHistory,
5
+ createShowSessionPickerIntent,
6
+ createSessionRenameHostAction,
7
+ formatCommandSessionReplayValidationReport,
8
+ parseSessionNameArgument,
9
+ readCommandSessionInfo,
10
+ validateCommandSessionReplayLog,
11
+ } from '@robota-sdk/agent-framework';
12
+
13
+ import { calculateCost, formatTokens, formatUsd } from './model-pricing.js';
14
+
15
+ import type {
16
+ ICommandCostBudgetAdapter,
17
+ ICommandHostAdapterAccess,
18
+ ICommandHostNoCapability,
19
+ ICommandHostSessionAccess,
20
+ ICommandHostUserInteraction,
21
+ ICommandHostWorkspace,
22
+ ISessionUsageRecord,
23
+ } from '@robota-sdk/agent-framework';
24
+ import type { ICommandResult } from '@robota-sdk/agent-interface-command';
25
+
26
+ export const CLEAR_COMMAND_MESSAGE = 'Conversation cleared.';
27
+
28
+ export const CD_COMMAND_DESCRIPTION = 'Move this session to another working directory';
29
+ export const CD_COMMAND_USAGE = 'Usage: /cd <directory>';
30
+
31
+ /**
32
+ * `/cd <directory>` (issue #3081) requests the host-executed move. The checks — no turn running, no
33
+ * live background task, the target exists, no `Cd(...)` deny rule — run where the session state is,
34
+ * when the host applies the action.
35
+ */
36
+ export function executeCdCommand(_context: ICommandHostNoCapability, args: string): ICommandResult {
37
+ const path = args.trim();
38
+ if (path === '') return { success: false, message: CD_COMMAND_USAGE };
39
+ return { success: true, message: '', hostActions: [{ type: 'workspace-move', path }] };
40
+ }
41
+
42
+ export async function executeClearCommand(
43
+ context: ICommandHostSessionAccess & ICommandHostUserInteraction,
44
+ _args: string,
45
+ ): Promise<ICommandResult> {
46
+ // Confirm only when an interactive renderer is attached; with no human the explicit /clear proceeds.
47
+ const ui = context.getUserInteraction();
48
+ if (ui) {
49
+ const response = await ui.ask(confirmAction('clear', 'Clear conversation history?'));
50
+ if (!isConfirmed(response)) {
51
+ return { success: true, message: 'Clear cancelled.' };
52
+ }
53
+ }
54
+
55
+ // CMD-004 Stage E: the clear mutation runs host-side and the session broadcasts `history_cleared`
56
+ // to every attached surface — the result carries no separate notification.
57
+ clearConversationHistory(context);
58
+ return { success: true, message: CLEAR_COMMAND_MESSAGE };
59
+ }
60
+
61
+ export function executeRenameCommand(
62
+ _context: ICommandHostNoCapability,
63
+ args: string,
64
+ ): ICommandResult {
65
+ const name = parseSessionNameArgument(args);
66
+ if (name === undefined) {
67
+ return { success: false, message: RENAME_COMMAND_USAGE };
68
+ }
69
+
70
+ return {
71
+ success: true,
72
+ message: `Session renamed to "${name}".`,
73
+ data: { name },
74
+ hostActions: [createSessionRenameHostAction(name)],
75
+ };
76
+ }
77
+
78
+ export function executeResumeCommand(): ICommandResult {
79
+ return {
80
+ success: true,
81
+ message: 'Opening session picker...',
82
+ data: { triggerResumePicker: true },
83
+ uiIntents: [createShowSessionPickerIntent()],
84
+ };
85
+ }
86
+
87
+ type TCostCommandContext = ICommandHostSessionAccess &
88
+ ICommandHostWorkspace &
89
+ ICommandHostAdapterAccess;
90
+
91
+ /**
92
+ * CMD-007 (issue #2058): the budget lives behind `ICommandCostBudgetAdapter`, composed by the shell.
93
+ * This package owns no path literal and no filesystem call for it any more — atomic/symlink-safe
94
+ * writing is the adapter's policy (agent-cli's file adapter keeps the O_NOFOLLOW guard).
95
+ */
96
+ function costBudgetAdapter(context: TCostCommandContext): ICommandCostBudgetAdapter | undefined {
97
+ return context.getCommandHostAdapters?.().costBudget;
98
+ }
99
+
100
+ const BUDGET_UNAVAILABLE_MESSAGE =
101
+ 'Budget persistence is not available on this host (no cost-budget adapter was composed).';
102
+
103
+ /** What to tell the operator when the adapter refused the write; the adapter names the cause. */
104
+ function describeBudgetWriteFailure(error: unknown): string {
105
+ return `Could not write the budget: ${error instanceof Error ? error.message : String(error)}`;
106
+ }
107
+
108
+ interface ICostTotals {
109
+ inputTokens: number;
110
+ outputTokens: number;
111
+ /** Absent when nothing could be priced. */
112
+ costUsd?: number;
113
+ /** Some usage could not be priced, so the cost is a lower bound. */
114
+ partial: boolean;
115
+ /** The models the cost was priced on. */
116
+ models: string[];
117
+ }
118
+
119
+ /**
120
+ * Total the session's usage records. A record priced on its own model (the advisor's) keeps that
121
+ * price; a main-thread record without one is priced on the session's model. A consulted model whose
122
+ * price is unknown is left unpriced rather than priced as the main model.
123
+ */
124
+ function totalSessionUsage(
125
+ records: readonly ISessionUsageRecord[],
126
+ modelId: string | undefined,
127
+ ): ICostTotals | undefined {
128
+ if (records.length === 0) return undefined;
129
+ const totals: ICostTotals = { inputTokens: 0, outputTokens: 0, partial: false, models: [] };
130
+ const models = new Set<string>();
131
+ let cost = 0;
132
+ let priced = false;
133
+ for (const record of records) {
134
+ totals.inputTokens += record.promptTokens;
135
+ totals.outputTokens += record.completionTokens;
136
+ const consulted = record.source?.scope === 'tool';
137
+ const recordCost =
138
+ record.costUsd ??
139
+ (!consulted && modelId
140
+ ? calculateCost(modelId, record.promptTokens, record.completionTokens)
141
+ : undefined);
142
+ if (recordCost === undefined) {
143
+ totals.partial = true;
144
+ continue;
145
+ }
146
+ cost += recordCost;
147
+ priced = true;
148
+ models.add(consulted ? (record.source?.label ?? 'another model') : (modelId ?? 'main model'));
149
+ }
150
+ if (priced) totals.costUsd = cost;
151
+ totals.models = [...models];
152
+ return totals;
153
+ }
154
+
155
+ function buildCostOutput(context: TCostCommandContext): {
156
+ lines: string[];
157
+ data: Record<string, unknown>;
158
+ } {
159
+ const session = context.getSession();
160
+ const sessionInfo = readCommandSessionInfo(context);
161
+ const modelId = session.getModelId();
162
+ const totals = totalSessionUsage(context.getSessionUsage(), modelId);
163
+ const lines: string[] = [
164
+ `Session: ${sessionInfo.sessionId}`,
165
+ `Messages: ${sessionInfo.messageCount}`,
166
+ ];
167
+ const data: Record<string, unknown> = {
168
+ sessionId: sessionInfo.sessionId,
169
+ messageCount: sessionInfo.messageCount,
170
+ };
171
+
172
+ if (totals) {
173
+ lines.push(
174
+ `Tokens: ${formatTokens(totals.inputTokens)} input / ${formatTokens(totals.outputTokens)} output`,
175
+ );
176
+ data.inputTokens = totals.inputTokens;
177
+ data.outputTokens = totals.outputTokens;
178
+
179
+ const cost = totals.costUsd;
180
+ if (cost !== undefined) {
181
+ const priced =
182
+ totals.models.length > 1 ? `mixed: ${totals.models.join(', ')}` : (totals.models[0] ?? '');
183
+ const note = totals.partial ? '; some usage could not be priced' : '';
184
+ lines.push(`Cost: ${formatUsd(cost)} (${priced}${note})`);
185
+ data.estimatedCostUsd = cost;
186
+ data.costModels = totals.models;
187
+ if (totals.partial) data.costPartial = true;
188
+
189
+ const budget = costBudgetAdapter(context)?.read();
190
+ if (budget?.monthly) {
191
+ const remaining = budget.monthly - cost;
192
+ const pct = Math.min(100, Math.round((cost / budget.monthly) * 100));
193
+ lines.push(
194
+ `Budget: ${formatUsd(remaining)} remaining of ${formatUsd(budget.monthly)}/mo (${pct}% used)`,
195
+ );
196
+ data.budgetMonthly = budget.monthly;
197
+ data.budgetRemainingUsd = remaining;
198
+ }
199
+ }
200
+ } else {
201
+ lines.push('Tokens: not yet available (no turns completed)');
202
+ }
203
+
204
+ return { lines, data };
205
+ }
206
+
207
+ export function executeCostCommand(context: TCostCommandContext, args: string): ICommandResult {
208
+ const trimmed = args.trim();
209
+
210
+ if (trimmed.startsWith('budget')) {
211
+ const budgetArg = trimmed.slice('budget'.length).trim();
212
+ // Argument shape is judged before the host is asked for persistence: a malformed amount is a
213
+ // usage error on every host, not a symptom of the adapter that would have stored it.
214
+ const amount = budgetArg === 'clear' || budgetArg === '' ? undefined : parseFloat(budgetArg);
215
+ if (amount !== undefined && (!Number.isFinite(amount) || amount <= 0)) {
216
+ return {
217
+ success: false,
218
+ message: 'Usage: /cost budget <amount> (e.g. /cost budget 5.00)',
219
+ };
220
+ }
221
+ const adapter = costBudgetAdapter(context);
222
+ if (adapter === undefined) return { success: false, message: BUDGET_UNAVAILABLE_MESSAGE };
223
+
224
+ if (budgetArg === 'clear') {
225
+ try {
226
+ adapter.clear();
227
+ } catch (error) {
228
+ return { success: false, message: describeBudgetWriteFailure(error) };
229
+ }
230
+ return { success: true, message: 'Monthly budget cleared.' };
231
+ }
232
+
233
+ if (budgetArg === '') {
234
+ const current = adapter.read();
235
+ if (!current?.monthly) {
236
+ return {
237
+ success: true,
238
+ message: 'No budget set. Use: /cost budget <amount>',
239
+ };
240
+ }
241
+ return { success: true, message: `Monthly budget: ${formatUsd(current.monthly)}` };
242
+ }
243
+
244
+ if (amount === undefined) return { success: false, message: BUDGET_UNAVAILABLE_MESSAGE };
245
+ try {
246
+ adapter.write({ monthly: amount });
247
+ } catch (error) {
248
+ return { success: false, message: describeBudgetWriteFailure(error) };
249
+ }
250
+ return { success: true, message: `Monthly budget set to ${formatUsd(amount)}.` };
251
+ }
252
+
253
+ const { lines, data } = buildCostOutput(context);
254
+ return { success: true, message: lines.join('\n'), data };
255
+ }
256
+
257
+ export function executeValidateSessionCommand(
258
+ context: ICommandHostSessionAccess & ICommandHostWorkspace,
259
+ _args: string,
260
+ ): ICommandResult {
261
+ const report = validateCommandSessionReplayLog(context);
262
+ return {
263
+ success: report.validation.ok,
264
+ message: formatCommandSessionReplayValidationReport(report),
265
+ data: {
266
+ logFile: report.logFile,
267
+ entryCount: report.entryCount,
268
+ issueCount: report.validation.issues.length,
269
+ ok: report.validation.ok,
270
+ },
271
+ };
272
+ }
@@ -0,0 +1,7 @@
1
+ // @robota-sdk/agent-command-settings
2
+
3
+ export {
4
+ createSettingsCommandModule,
5
+ createSettingsCommandEntry,
6
+ SettingsCommandSource,
7
+ } from './settings-command-module.js';
@@ -0,0 +1,47 @@
1
+ import type { ICommandModule, ISystemCommand } from '@robota-sdk/agent-framework';
2
+ import type { ICommand, ICommandSource } from '@robota-sdk/agent-interface-command';
3
+
4
+ export function createSettingsCommandEntry(): ICommand {
5
+ return {
6
+ name: 'settings',
7
+ displayName: 'Settings',
8
+ description: 'Open settings — language, output style, preset, permissions and more',
9
+ source: 'settings',
10
+ // User-only: opens a screen; nothing here is a model action.
11
+ modelInvocable: false,
12
+ };
13
+ }
14
+
15
+ function createSettingsSystemCommand(): ISystemCommand {
16
+ const entry = createSettingsCommandEntry();
17
+ return {
18
+ name: entry.name,
19
+ displayName: entry.displayName,
20
+ description: entry.description,
21
+ requiresPermission: false,
22
+ userInvocable: true,
23
+ modelInvocable: false,
24
+ lifecycle: 'inline',
25
+ execute: async () => ({
26
+ success: true,
27
+ message: 'Opening settings...',
28
+ uiIntents: [{ type: 'show-settings' as const }],
29
+ }),
30
+ };
31
+ }
32
+
33
+ export class SettingsCommandSource implements ICommandSource {
34
+ readonly name = 'settings';
35
+
36
+ getCommands(): ICommand[] {
37
+ return [createSettingsCommandEntry()];
38
+ }
39
+ }
40
+
41
+ export function createSettingsCommandModule(): ICommandModule {
42
+ return {
43
+ name: 'agent-command-settings',
44
+ commandSources: [new SettingsCommandSource()],
45
+ systemCommands: [createSettingsSystemCommand()],
46
+ };
47
+ }
@@ -0,0 +1,11 @@
1
+ import { describe, expect, it } from 'vitest';
2
+
3
+ import { resolveShell } from '../resolve-shell.js';
4
+
5
+ describe('interactive shell host selection', () => {
6
+ it('keeps the executable and argument family together', () => {
7
+ const shell = resolveShell('C:\\Windows\\System32\\cmd.exe');
8
+ expect(shell.command).toBe('C:\\Windows\\System32\\cmd.exe');
9
+ expect(shell.commandArgs('echo ok')).toEqual(['/d', '/s', '/c', 'echo ok']);
10
+ });
11
+ });
@@ -0,0 +1,118 @@
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-session';
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
+ 'uses the host-selected executable captured by its module',
44
+ async () => {
45
+ if (process.platform === 'win32') return;
46
+ const previousShell = process.env['SHELL'];
47
+ process.env['SHELL'] = '/bin/false';
48
+ try {
49
+ h = scriptedSession({
50
+ turns: [{ text: 'unused' }],
51
+ terminalHandoff: fakeHandoff(true),
52
+ commandModules: [createShellCommandModule('/bin/bash')],
53
+ });
54
+ const result = await h.command('shell', 'exit 0');
55
+ expect(result?.success).toBe(true);
56
+ } finally {
57
+ if (previousShell === undefined) delete process.env['SHELL'];
58
+ else process.env['SHELL'] = previousShell;
59
+ }
60
+ },
61
+ TEST_TIMEOUT,
62
+ );
63
+
64
+ it(
65
+ 'runs a one-shot command through the handoff and returns exit code 0',
66
+ async () => {
67
+ const handoff = fakeHandoff(true);
68
+ h = scriptedSession({
69
+ turns: [{ text: 'unused' }],
70
+ terminalHandoff: handoff,
71
+ commandModules: [createShellCommandModule()],
72
+ });
73
+
74
+ const result = await h.command('shell', 'exit 0');
75
+
76
+ expect(result?.success).toBe(true);
77
+ expect((result?.data as { exitCode: number }).exitCode).toBe(0);
78
+ // The command went through the transport handoff (suspend → run → restore).
79
+ expect(handoff.events).toEqual(['suspend', 'restore']);
80
+ },
81
+ TEST_TIMEOUT,
82
+ );
83
+
84
+ it(
85
+ 'reports a non-zero exit code from the child',
86
+ async () => {
87
+ const handoff = fakeHandoff(true);
88
+ h = scriptedSession({
89
+ turns: [{ text: 'unused' }],
90
+ terminalHandoff: handoff,
91
+ commandModules: [createShellCommandModule()],
92
+ });
93
+
94
+ const result = await h.command('shell', 'exit 3');
95
+
96
+ expect(result?.success).toBe(false);
97
+ expect((result?.data as { exitCode: number }).exitCode).toBe(3);
98
+ },
99
+ TEST_TIMEOUT,
100
+ );
101
+
102
+ it(
103
+ 'is unavailable when there is no interactive terminal',
104
+ async () => {
105
+ // No terminalHandoff injected → canHandoffTerminal() is false.
106
+ h = scriptedSession({
107
+ turns: [{ text: 'unused' }],
108
+ commandModules: [createShellCommandModule()],
109
+ });
110
+
111
+ const result = await h.command('shell', 'exit 0');
112
+
113
+ expect(result?.success).toBe(false);
114
+ expect(result?.message).toMatch(/unavailable/i);
115
+ },
116
+ TEST_TIMEOUT,
117
+ );
118
+ });
@@ -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(executable?: string): IResolvedShell {
19
+ const shell = resolvePlatformShell({ executable });
20
+ return {
21
+ command: shell.command,
22
+ interactiveArgs: shell.interactiveArgs,
23
+ commandArgs: (cmd: string) => shell.commandArgs(cmd),
24
+ };
25
+ }