@robota-sdk/agent-command 3.0.0-beta.78 → 3.0.0-beta.81

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 (200) hide show
  1. package/README.md +11 -2
  2. package/dist/node/index.cjs +76 -38
  3. package/dist/node/index.d.cts +946 -0
  4. package/dist/node/index.d.cts.map +1 -0
  5. package/dist/node/index.d.ts +564 -51
  6. package/dist/node/index.d.ts.map +1 -1
  7. package/dist/node/index.js +76 -38
  8. package/dist/node/index.js.map +1 -1
  9. package/package.json +34 -16
  10. package/src/advisor/advisor-command-module.ts +47 -0
  11. package/src/advisor/advisor-command.test.ts +102 -0
  12. package/src/advisor/advisor-command.ts +55 -0
  13. package/src/advisor/advisor-cost-functional.test.ts +92 -0
  14. package/src/advisor/index.ts +6 -0
  15. package/src/agent/__tests__/agent-command.test.ts +39 -8
  16. package/src/agent/agent-command-module.ts +14 -7
  17. package/src/agent/agent-command-parser.ts +1 -1
  18. package/src/agent/agent-command.ts +3 -3
  19. package/src/background/__tests__/background-command-module.test.ts +59 -52
  20. package/src/background/background-command-module.ts +2 -1
  21. package/src/background/background-command.ts +3 -3
  22. package/src/command-module-utils.ts +33 -0
  23. package/src/compact/__tests__/compact-command-module.test.ts +31 -23
  24. package/src/compact/compact-command-module.ts +10 -1
  25. package/src/compact/compact-command.ts +6 -3
  26. package/src/context/__tests__/context-command-module.test.ts +214 -63
  27. package/src/context/auto-compact-format.ts +25 -0
  28. package/src/context/context-breakdown.ts +295 -0
  29. package/src/context/context-command-module.ts +45 -14
  30. package/src/context/context-command.ts +34 -257
  31. package/src/default/__tests__/__snapshots__/model-exposure.test.ts.snap +110 -0
  32. package/src/default/__tests__/default-command-modules.test.ts +59 -7
  33. package/src/default/__tests__/model-exposure.test.ts +241 -0
  34. package/src/default/__tests__/org-policy-forwarding.test.ts +98 -0
  35. package/src/default/default-command-modules.ts +130 -18
  36. package/src/default/index.ts +4 -1
  37. package/src/devices/__tests__/devices-command-module.test.ts +212 -0
  38. package/src/devices/devices-command-module.ts +224 -0
  39. package/src/devices/devices-command-port.ts +82 -0
  40. package/src/devices/index.ts +11 -0
  41. package/src/doctor/__tests__/doctor-command-module.test.ts +145 -0
  42. package/src/doctor/__tests__/doctor-display-vocabulary.test.ts +38 -0
  43. package/src/doctor/__tests__/doctor-fixture.ts +145 -0
  44. package/src/doctor/__tests__/doctor-redaction.test.ts +29 -0
  45. package/src/doctor/__tests__/doctor-runner.test.ts +464 -0
  46. package/src/doctor/doctor-command-module.ts +132 -0
  47. package/src/doctor/doctor-extensions-probe.ts +299 -0
  48. package/src/doctor/doctor-node-deps.ts +89 -0
  49. package/src/doctor/doctor-redaction.ts +99 -0
  50. package/src/doctor/doctor-render.ts +70 -0
  51. package/src/doctor/doctor-repair.ts +136 -0
  52. package/src/doctor/doctor-runner.ts +122 -0
  53. package/src/doctor/doctor-settings-probe.ts +239 -0
  54. package/src/doctor/doctor-storage-probe.ts +187 -0
  55. package/src/doctor/doctor-types.ts +109 -0
  56. package/src/doctor/index.ts +32 -0
  57. package/src/editor/__tests__/editor-command-functional.test.ts +49 -16
  58. package/src/editor/editor-command-module.ts +6 -5
  59. package/src/editor/editor-command.ts +10 -5
  60. package/src/effort/effort-command-module.ts +51 -0
  61. package/src/effort/effort-command.test.ts +140 -0
  62. package/src/effort/effort-command.ts +122 -0
  63. package/src/effort/index.ts +6 -0
  64. package/src/exit/__tests__/exit-command-module.test.ts +16 -9
  65. package/src/exit/exit-command-module.ts +2 -1
  66. package/src/exit/exit-command.ts +6 -6
  67. package/src/fork/__tests__/fork-command.test.ts +184 -0
  68. package/src/fork/fork-command-module.ts +61 -0
  69. package/src/fork/fork-command.ts +162 -0
  70. package/src/fork/index.ts +8 -0
  71. package/src/git/__tests__/fake-git-port.ts +28 -0
  72. package/src/git/__tests__/git-command-module.test.ts +103 -0
  73. package/src/git/__tests__/git-commit.test.ts +185 -0
  74. package/src/git/__tests__/git-diff.test.ts +125 -0
  75. package/src/git/__tests__/git-process.test.ts +221 -0
  76. package/src/git/__tests__/git-status.test.ts +117 -0
  77. package/src/git/git-command-module.ts +139 -0
  78. package/src/git/git-commit.ts +251 -0
  79. package/src/git/git-diff.ts +129 -0
  80. package/src/git/git-process.ts +169 -0
  81. package/src/git/git-status.ts +134 -0
  82. package/src/git/index.ts +17 -0
  83. package/src/goal/__tests__/goal-command.test.ts +9 -4
  84. package/src/goal/goal-command-module.ts +2 -1
  85. package/src/goal/goal-command.ts +6 -9
  86. package/src/handoff/__tests__/handoff-command.test.ts +218 -0
  87. package/src/handoff/handoff-command-module.ts +48 -0
  88. package/src/handoff/handoff-command.ts +187 -0
  89. package/src/handoff/index.ts +6 -0
  90. package/src/help/__tests__/help-command-module.test.ts +63 -40
  91. package/src/help/__tests__/help-command.test.ts +46 -37
  92. package/src/help/help-command-module.ts +2 -1
  93. package/src/help/help-command.ts +3 -3
  94. package/src/index.ts +124 -2
  95. package/src/keybindings/__tests__/keybindings-command-module.test.ts +57 -0
  96. package/src/keybindings/index.ts +6 -0
  97. package/src/keybindings/keybindings-command-module.ts +74 -0
  98. package/src/language/__tests__/language-command-module.test.ts +33 -28
  99. package/src/language/language-command-module.ts +2 -1
  100. package/src/language/language-command.ts +8 -6
  101. package/src/mcp-activation/__tests__/mcp-activation-command.test.ts +630 -0
  102. package/src/mcp-activation/__tests__/mcp-model-view.test.ts +211 -0
  103. package/src/mcp-activation/index.ts +12 -0
  104. package/src/mcp-activation/mcp-activation-command-module.ts +92 -0
  105. package/src/mcp-activation/mcp-activation-command.ts +448 -0
  106. package/src/mcp-activation/mcp-model-notice.ts +85 -0
  107. package/src/mcp-activation/mcp-model-status.ts +99 -0
  108. package/src/memory/__tests__/memory-command-module.test.ts +167 -74
  109. package/src/memory/memory-command-module.ts +18 -1
  110. package/src/memory/memory-command.ts +38 -38
  111. package/src/mode/__tests__/mode-command-module.test.ts +51 -38
  112. package/src/mode/mode-command-module.ts +2 -1
  113. package/src/mode/mode-command.ts +15 -7
  114. package/src/output-style/__tests__/output-style-command.test.ts +96 -0
  115. package/src/output-style/index.ts +6 -0
  116. package/src/output-style/output-style-command-module.ts +143 -0
  117. package/src/peers/__tests__/peers-command.test.ts +241 -0
  118. package/src/peers/index.ts +9 -0
  119. package/src/peers/peers-command-module.ts +47 -0
  120. package/src/peers/peers-command.ts +142 -0
  121. package/src/permissions/__tests__/permissions-command-module.test.ts +149 -37
  122. package/src/permissions/permissions-command-module.ts +2 -1
  123. package/src/permissions/permissions-command.ts +46 -4
  124. package/src/plan/__tests__/plan-command.test.ts +129 -0
  125. package/src/plan/index.ts +9 -0
  126. package/src/plan/plan-command-module.ts +49 -0
  127. package/src/plan/plan-command.ts +92 -0
  128. package/src/plugin/__tests__/plugin-command-module.test.ts +46 -40
  129. package/src/plugin/__tests__/plugin-host-boundary.test.ts +11 -0
  130. package/src/plugin/plugin-command-module.ts +3 -1
  131. package/src/plugin/plugin-command.ts +17 -16
  132. package/src/preset/__tests__/host-supplied-preset-registry.test.ts +98 -0
  133. package/src/preset/__tests__/preset-command-module.test.ts +102 -56
  134. package/src/preset/preset-command-module.ts +26 -8
  135. package/src/preset/preset-command.ts +85 -27
  136. package/src/provider/__tests__/org-policy.test.ts +4 -5
  137. package/src/provider/__tests__/provider-command-module.test.ts +11 -14
  138. package/src/provider/__tests__/provider-startup-host-settings.test.ts +16 -0
  139. package/src/provider/__tests__/scripted-interaction.ts +7 -5
  140. package/src/provider/provider-command-execution.ts +4 -4
  141. package/src/provider/provider-command-module.ts +2 -1
  142. package/src/provider/provider-command-profile-lifecycle.ts +4 -4
  143. package/src/provider/provider-command-profile-operations.ts +3 -3
  144. package/src/provider/provider-command-profile.ts +1 -1
  145. package/src/provider/provider-command-setup.ts +4 -4
  146. package/src/provider/provider-startup.ts +62 -35
  147. package/src/remote-control/__tests__/remote-control-command-module.test.ts +36 -0
  148. package/src/remote-control/__tests__/remote-control-command.test.ts +216 -0
  149. package/src/remote-control/index.ts +8 -0
  150. package/src/remote-control/remote-control-command-module.ts +61 -0
  151. package/src/remote-control/remote-control-command.ts +138 -0
  152. package/src/reset/__tests__/reset-command-module.test.ts +5 -5
  153. package/src/reset/reset-command-module.ts +2 -1
  154. package/src/reset/reset-command.ts +3 -4
  155. package/src/rewind/__tests__/rewind-command-module.test.ts +40 -0
  156. package/src/rewind/rewind-command-module.ts +2 -1
  157. package/src/rewind/rewind-command.ts +73 -6
  158. package/src/sandbox/__tests__/sandbox-command.test.ts +80 -0
  159. package/src/sandbox/index.ts +2 -0
  160. package/src/sandbox/sandbox-command-module.ts +56 -0
  161. package/src/sandbox/sandbox-command.ts +94 -0
  162. package/src/schedule/__tests__/loop-command-functional.test.ts +386 -0
  163. package/src/schedule/__tests__/loop-command.test.ts +360 -0
  164. package/src/schedule/__tests__/loop-jitter.test.ts +25 -0
  165. package/src/schedule/__tests__/monitor-model-permission-functional.test.ts +126 -0
  166. package/src/schedule/__tests__/schedule-command.test.ts +143 -3
  167. package/src/schedule/__tests__/schedule-redos.test.ts +81 -0
  168. package/src/schedule/index.ts +3 -0
  169. package/src/schedule/loop-command.ts +333 -0
  170. package/src/schedule/loop-jitter.ts +41 -0
  171. package/src/schedule/schedule-command-module.ts +92 -14
  172. package/src/schedule/schedule-command.ts +98 -9
  173. package/src/schedule/schedule-spec-parser.ts +5 -1
  174. package/src/session/__tests__/rename-host-persistence.test.ts +72 -0
  175. package/src/session/__tests__/session-command-module.test.ts +210 -45
  176. package/src/session/session-command-module.ts +68 -37
  177. package/src/session/session-command.ts +147 -80
  178. package/src/settings/settings-command-module.ts +3 -2
  179. package/src/shell/__tests__/resolve-shell-host.test.ts +11 -0
  180. package/src/shell/__tests__/shell-command-functional.test.ts +23 -1
  181. package/src/shell/resolve-shell.ts +2 -2
  182. package/src/shell/shell-command-module.ts +6 -5
  183. package/src/shell/shell-command.ts +9 -5
  184. package/src/skills/__tests__/skills-command-module.test.ts +69 -28
  185. package/src/skills/index.ts +1 -0
  186. package/src/skills/skills-command-module.ts +22 -5
  187. package/src/skills/skills-command.ts +14 -13
  188. package/src/statusline/__tests__/statusline-command-module.test.ts +5 -4
  189. package/src/statusline/statusline-command-module.ts +2 -1
  190. package/src/statusline/statusline-command.ts +4 -4
  191. package/src/theme/__tests__/theme-command.test.ts +204 -0
  192. package/src/theme/index.ts +12 -0
  193. package/src/theme/theme-command-module.ts +211 -0
  194. package/src/user-local/__tests__/user-local-command.test.ts +145 -110
  195. package/src/user-local/user-local-command-constants.ts +1 -1
  196. package/src/user-local/user-local-command-module.ts +6 -5
  197. package/src/user-local/user-local-command.ts +12 -6
  198. package/src/user-local/user-local-memory-command.ts +17 -7
  199. package/src/plugins/default-plugin-command-adapter.ts +0 -164
  200. package/src/plugins/default-plugin-command-source-loader.ts +0 -31
@@ -1,37 +1,68 @@
1
1
  import { executeContextCommand } from './context-command.js';
2
+ import { createSystemCommandFromEntry } from '../command-module-utils.js';
2
3
 
3
4
  import type { ICommandModule, ISystemCommand } from '@robota-sdk/agent-framework';
4
- import type { ICommand, ICommandSource } from '@robota-sdk/agent-interface-transport';
5
+ import type { ICommand, ICommandSource } from '@robota-sdk/agent-interface-command';
5
6
 
6
7
  export function createContextCommandEntry(): ICommand {
7
8
  return {
8
9
  name: 'context',
9
10
  displayName: 'Context References',
10
11
  description: 'Context window info, reference inventory, and auto-compact controls',
12
+ // Model-invocable for the read-only views only: knowing how full the context is lets the model
13
+ // decide when to compact. Pinning, unpinning and clearing references and the auto-compact
14
+ // policy are the user's curation and preference, so they stay user-only.
15
+ modelDescription:
16
+ 'Inspect the context window. Use it before a large task or before deciding to compact, or to ' +
17
+ 'see which files the user pinned into context. Bare returns used/max tokens and percentage, ' +
18
+ 'the auto-compact threshold, the pinned-reference summary and the turn count; `list` returns ' +
19
+ 'the per-category token breakdown and every pinned reference.',
11
20
  source: 'context',
12
- modelInvocable: false,
21
+ modelInvocable: true,
22
+ userInvocable: true,
13
23
  argumentHint: 'list | add <path> | remove <path> | clear | auto ...',
14
24
  subcommands: [
15
- { name: 'list', description: 'List loaded context references', source: 'context' },
16
- { name: 'add', description: 'Add a file to active context references', source: 'context' },
17
- { name: 'remove', description: 'Remove a context reference', source: 'context' },
18
- { name: 'clear', description: 'Clear context references', source: 'context' },
19
- { name: 'auto', description: 'Inspect or change auto-compact policy', source: 'context' },
25
+ {
26
+ name: 'list',
27
+ description: 'List loaded context references',
28
+ source: 'context',
29
+ modelInvocable: true,
30
+ },
31
+ {
32
+ name: 'add',
33
+ description: 'Add a file to active context references',
34
+ source: 'context',
35
+ modelInvocable: false,
36
+ },
37
+ {
38
+ name: 'remove',
39
+ description: 'Remove a context reference',
40
+ source: 'context',
41
+ modelInvocable: false,
42
+ },
43
+ {
44
+ name: 'clear',
45
+ description: 'Clear context references',
46
+ source: 'context',
47
+ modelInvocable: false,
48
+ },
49
+ {
50
+ name: 'auto',
51
+ description: 'Inspect or change auto-compact policy',
52
+ source: 'context',
53
+ modelInvocable: false,
54
+ },
20
55
  ],
21
56
  };
22
57
  }
23
58
 
24
59
  function createContextSystemCommand(): ISystemCommand {
25
60
  const entry = createContextCommandEntry();
26
- return {
27
- name: entry.name,
28
- displayName: entry.displayName,
29
- description: entry.description,
61
+ return createSystemCommandFromEntry(entry, {
30
62
  requiresPermission: false,
31
- userInvocable: true,
32
- modelInvocable: false,
63
+ lifecycle: 'inline',
33
64
  execute: executeContextCommand,
34
- };
65
+ });
35
66
  }
36
67
 
37
68
  export class ContextCommandSource implements ICommandSource {
@@ -1,4 +1,3 @@
1
- import { CONTEXT_ESTIMATE_CHARS_PER_TOKEN } from '@robota-sdk/agent-core';
2
1
  import {
3
2
  addCommandContextReference,
4
3
  clearCommandContextReferences,
@@ -13,13 +12,26 @@ import {
13
12
  writeAutoCompactThresholdSetting,
14
13
  } from '@robota-sdk/agent-framework';
15
14
 
16
- import type { IHistoryEntry, TUniversalMessage } from '@robota-sdk/agent-core';
15
+ import {
16
+ buildToolDisplayList,
17
+ formatContextReferenceLine,
18
+ formatFullContextBreakdown,
19
+ } from './context-breakdown.js';
20
+
17
21
  import type {
18
- ICommandHostContext,
22
+ ICommandHostAdapterAccess,
23
+ ICommandHostContextReferences,
24
+ ICommandHostContextWindow,
25
+ ICommandHostSessionAccess,
19
26
  TAutoCompactThreshold,
20
27
  TAutoCompactThresholdSource,
21
28
  } from '@robota-sdk/agent-framework';
22
- import type { ICommandResult, IContextReferenceItem } from '@robota-sdk/agent-interface-transport';
29
+ import type { ICommandResult } from '@robota-sdk/agent-interface-command';
30
+ import type { IContextReferenceItem } from '@robota-sdk/agent-interface-session';
31
+
32
+ import { formatAutoCompactLine, formatThreshold } from './auto-compact-format.js';
33
+
34
+ export { formatAutoCompactLine } from './auto-compact-format.js';
23
35
 
24
36
  const PERCENT = 100;
25
37
  const USAGE = [
@@ -27,29 +39,23 @@ const USAGE = [
27
39
  'Examples: /context list, /context add AGENTS.md, /context remove AGENTS.md, /context auto 85%',
28
40
  ].join('\n');
29
41
 
30
- function formatThreshold(threshold: TAutoCompactThreshold): string {
31
- if (threshold === false) {
32
- return 'disabled';
33
- }
34
- return `${Math.round(threshold * PERCENT)}%`;
35
- }
36
-
37
- function formatAutoCompactLine(
38
- threshold: TAutoCompactThreshold,
39
- source: TAutoCompactThresholdSource,
40
- ): string {
41
- if (threshold === false) {
42
- return `Auto compact: disabled (${source})`;
43
- }
44
- return `Auto compact: ${formatThreshold(threshold)} (${source})`;
45
- }
46
-
47
42
  function formatPersistenceSuffix(persisted: boolean): string {
48
43
  return persisted ? 'settings' : 'current session only';
49
44
  }
50
45
 
46
+ /** The roles the `/context` surface reads. Named once so five signatures do not restate it. */
47
+ type TContextCommandHost = ICommandHostAdapterAccess &
48
+ ICommandHostContextReferences &
49
+ ICommandHostContextWindow &
50
+ ICommandHostSessionAccess;
51
+
52
+ /** The subset the threshold operations need — no context-reference role. */
53
+ type TContextThresholdHost = ICommandHostAdapterAccess &
54
+ ICommandHostContextWindow &
55
+ ICommandHostSessionAccess;
56
+
51
57
  export async function executeContextCommand(
52
- context: ICommandHostContext,
58
+ context: TContextCommandHost,
53
59
  args: string,
54
60
  ): Promise<ICommandResult> {
55
61
  const parts = args
@@ -87,7 +93,7 @@ export async function executeContextCommand(
87
93
  }
88
94
 
89
95
  async function executeContextSubcommand(
90
- context: ICommandHostContext,
96
+ context: TContextCommandHost,
91
97
  parts: readonly string[],
92
98
  ): Promise<ICommandResult> {
93
99
  const [subcommand, ...rest] = parts;
@@ -117,7 +123,7 @@ async function executeContextSubcommand(
117
123
  }
118
124
 
119
125
  function executeAutoContextSubcommand(
120
- context: ICommandHostContext,
126
+ context: TContextThresholdHost,
121
127
  parts: readonly string[],
122
128
  ): ICommandResult {
123
129
  const [action, extra] = parts;
@@ -160,7 +166,7 @@ function executeAutoContextSubcommand(
160
166
  }
161
167
 
162
168
  async function executeAddContextReference(
163
- context: ICommandHostContext,
169
+ context: ICommandHostContextReferences,
164
170
  args: readonly string[],
165
171
  ): Promise<ICommandResult> {
166
172
  const path = args.join(' ').trim();
@@ -188,7 +194,7 @@ async function executeAddContextReference(
188
194
  }
189
195
 
190
196
  function executeRemoveContextReference(
191
- context: ICommandHostContext,
197
+ context: ICommandHostContextReferences,
192
198
  args: readonly string[],
193
199
  ): ICommandResult {
194
200
  const path = args.join(' ').trim();
@@ -210,7 +216,7 @@ function executeRemoveContextReference(
210
216
  }
211
217
 
212
218
  function applyAutoCompactThreshold(
213
- context: ICommandHostContext,
219
+ context: TContextThresholdHost,
214
220
  threshold: TAutoCompactThreshold,
215
221
  action: 'enabled' | 'disabled' | 'threshold set',
216
222
  ): ICommandResult {
@@ -245,8 +251,7 @@ function formatApplyMessage(
245
251
  }
246
252
 
247
253
  type TParseThresholdResult =
248
- | { success: true; threshold: number }
249
- | { success: false; message: string };
254
+ { success: true; threshold: number } | { success: false; message: string };
250
255
 
251
256
  function parseThreshold(raw: string): TParseThresholdResult {
252
257
  if (raw.endsWith('%')) {
@@ -284,231 +289,3 @@ function formatContextReferenceSummary(references: readonly IContextReferenceIte
284
289
  }
285
290
 
286
291
  // 1 token ≈ 4 chars — sourced from the agent-core estimation SSOT.
287
- const TOOL_ARG_MAX_LEN = 60;
288
-
289
- function estimateTokens(charLength: number): number {
290
- return Math.ceil(charLength / CONTEXT_ESTIMATE_CHARS_PER_TOKEN);
291
- }
292
-
293
- function formatContextReferenceLine(reference: IContextReferenceItem): string {
294
- return [
295
- reference.relativePath,
296
- `[${reference.loadType}, ${reference.status}]`,
297
- `~${estimateTokens(reference.byteLength).toLocaleString()} tokens`,
298
- ].join(' ');
299
- }
300
-
301
- // ── History analysis ────────────────────────────────────────────────────────
302
-
303
- interface IToolResultSummary {
304
- toolName: string;
305
- displayArg: string;
306
- }
307
-
308
- interface IMessageTokensByRole {
309
- systemTokens: number;
310
- userTokens: number;
311
- userCount: number;
312
- assistantTokens: number;
313
- assistantCount: number;
314
- toolTokens: number;
315
- toolCallCount: number;
316
- totalTokens: number;
317
- }
318
-
319
- interface IToolDisplayList {
320
- turnCount: number;
321
- toolResults: IToolResultSummary[];
322
- totalToolCallCount: number;
323
- }
324
-
325
- function parseToolCallArgs(argsJson: string): Record<string, unknown> | null {
326
- try {
327
- return JSON.parse(argsJson) as Record<string, unknown>;
328
- } catch {
329
- // allow-fallback: tool call arguments JSON may be malformed; display-only degradation, not a terminal path
330
- return null;
331
- }
332
- }
333
-
334
- function extractFirstToolArg(argsJson: string): string {
335
- const parsed = parseToolCallArgs(argsJson);
336
- if (parsed === null) return '';
337
- const first = Object.values(parsed)[0];
338
- const raw = typeof first === 'string' ? first : JSON.stringify(first);
339
- return raw.length > TOOL_ARG_MAX_LEN ? `${raw.slice(0, TOOL_ARG_MAX_LEN)}…` : raw;
340
- }
341
-
342
- function computeMessageTokensByRole(rawMessages: TUniversalMessage[]): IMessageTokensByRole {
343
- let systemTokens = 0;
344
- let userTokens = 0;
345
- let userCount = 0;
346
- let assistantTokens = 0;
347
- let assistantCount = 0;
348
- let toolTokens = 0;
349
- let toolCallCount = 0;
350
-
351
- for (const msg of rawMessages) {
352
- const t = Math.ceil(JSON.stringify(msg).length / CONTEXT_ESTIMATE_CHARS_PER_TOKEN);
353
- if (msg.role === 'system') {
354
- systemTokens += t;
355
- } else if (msg.role === 'user') {
356
- userTokens += t;
357
- userCount++;
358
- } else if (msg.role === 'assistant') {
359
- assistantTokens += t;
360
- assistantCount++;
361
- } else if (msg.role === 'tool') {
362
- toolTokens += t;
363
- toolCallCount++;
364
- }
365
- }
366
-
367
- return {
368
- systemTokens,
369
- userTokens,
370
- userCount,
371
- assistantTokens,
372
- assistantCount,
373
- toolTokens,
374
- toolCallCount,
375
- totalTokens: systemTokens + userTokens + assistantTokens + toolTokens,
376
- };
377
- }
378
-
379
- function buildToolDisplayList(history: IHistoryEntry[]): IToolDisplayList {
380
- type TToolCallEntry = { name: string; firstArg: string };
381
- const toolCallMap = new Map<string, TToolCallEntry>();
382
-
383
- for (const entry of history) {
384
- if (entry.category !== 'chat' || entry.type !== 'assistant') continue;
385
- const data = entry.data as {
386
- toolCalls?: Array<{ id: string; function: { name: string; arguments: string } }>;
387
- };
388
- for (const tc of data.toolCalls ?? []) {
389
- toolCallMap.set(tc.id, {
390
- name: tc.function.name,
391
- firstArg: extractFirstToolArg(tc.function.arguments),
392
- });
393
- }
394
- }
395
-
396
- let turnCount = 0;
397
- let totalToolCallCount = 0;
398
- const toolDisplayMap = new Map<string, IToolResultSummary>();
399
-
400
- for (const entry of history) {
401
- if (entry.category !== 'chat') continue;
402
- if (entry.type === 'user') {
403
- turnCount++;
404
- } else if (entry.type === 'tool') {
405
- const data = entry.data as { toolCallId?: string };
406
- const tc = toolCallMap.get(data.toolCallId ?? '');
407
- const toolName = tc?.name ?? 'tool';
408
- const displayArg = tc?.firstArg ?? '';
409
- totalToolCallCount++;
410
- toolDisplayMap.set(`${toolName}:${displayArg}`, { toolName, displayArg });
411
- }
412
- }
413
-
414
- return {
415
- turnCount,
416
- toolResults: [...toolDisplayMap.values()],
417
- totalToolCallCount,
418
- };
419
- }
420
-
421
- // ── Full context breakdown (for /context list) ──────────────────────────────
422
-
423
- function formatSection(title: string, tokens: number, lines: string[]): string {
424
- const tokenLabel = tokens > 0 ? ` — ~${tokens.toLocaleString()} tokens` : '';
425
- const header = `${title}${tokenLabel}:`;
426
- return lines.length === 0
427
- ? `${header}\n (none)`
428
- : [header, ...lines.map((l) => ` ${l}`)].join('\n');
429
- }
430
-
431
- function formatFullContextBreakdown(context: ICommandHostContext): ICommandResult {
432
- const state = readCommandContextState(context);
433
- const autoCompactThreshold = readAutoCompactThreshold(context);
434
- const autoCompactThresholdSource = readAutoCompactThresholdSource(context);
435
- const rawMessages = context.getSession().getHistory();
436
- const msgTokens = computeMessageTokensByRole(rawMessages);
437
- const display = buildToolDisplayList(context.getSession().getFullHistory());
438
- const references = listCommandContextReferences(context);
439
-
440
- const systemRefs = references.filter((r) => r.loadType === 'system');
441
- const manualRefs = references.filter((r) => r.loadType === 'manual');
442
- const promptRefs = references.filter((r) => r.loadType === 'prompt-reference');
443
-
444
- const systemRefTokens = systemRefs.reduce((s, r) => s + estimateTokens(r.byteLength), 0);
445
- const manualTokens = manualRefs.reduce((s, r) => s + estimateTokens(r.byteLength), 0);
446
- const promptRefTokens = promptRefs.reduce((s, r) => s + estimateTokens(r.byteLength), 0);
447
-
448
- const convTurnLabel =
449
- display.turnCount === 0
450
- ? 'Conversation history — 0 turns'
451
- : `Conversation history — ${display.turnCount} turn${display.turnCount !== 1 ? 's' : ''} | ~${msgTokens.totalTokens.toLocaleString()} tokens`;
452
-
453
- const toolResultLines = display.toolResults.map(
454
- (t) => `${t.toolName}${t.displayArg ? `: ${t.displayArg}` : ''}`,
455
- );
456
-
457
- const uniqueToolCount = display.toolResults.length;
458
- const totalToolCallCount = display.totalToolCallCount;
459
- const toolResultsLabel =
460
- totalToolCallCount === 0
461
- ? 'Tool results (0): (none)'
462
- : uniqueToolCount < totalToolCallCount
463
- ? `Tool results (${uniqueToolCount} unique / ${totalToolCallCount} calls, ~${msgTokens.toolTokens.toLocaleString()} tokens):`
464
- : `Tool results (${uniqueToolCount}, ~${msgTokens.toolTokens.toLocaleString()} tokens):`;
465
-
466
- const conversationLines: string[] =
467
- display.turnCount === 0
468
- ? []
469
- : [
470
- `User (${msgTokens.userCount}): ~${msgTokens.userTokens.toLocaleString()} tokens`,
471
- `Assistant (${msgTokens.assistantCount}): ~${msgTokens.assistantTokens.toLocaleString()} tokens`,
472
- ...(msgTokens.systemTokens > 0
473
- ? [`System messages: ~${msgTokens.systemTokens.toLocaleString()} tokens`]
474
- : []),
475
- totalToolCallCount > 0
476
- ? [toolResultsLabel, ...toolResultLines.map((l) => ` ${l}`)].join('\n')
477
- : toolResultsLabel,
478
- ];
479
-
480
- const convSection =
481
- display.turnCount === 0
482
- ? `${convTurnLabel}:\n (none)`
483
- : [`${convTurnLabel}:`, ...conversationLines.map((l) => ` ${l}`)].join('\n');
484
-
485
- const message = [
486
- `Context: ${state.usedTokens.toLocaleString()} / ${state.maxTokens.toLocaleString()} tokens (${Math.round(state.usedPercentage)}%)`,
487
- formatAutoCompactLine(autoCompactThreshold, autoCompactThresholdSource),
488
- '',
489
- formatSection(
490
- 'System prompt (active every turn)',
491
- systemRefTokens,
492
- systemRefs.map(formatContextReferenceLine),
493
- ),
494
- '',
495
- convSection,
496
- '',
497
- formatSection('Manually added', manualTokens, manualRefs.map(formatContextReferenceLine)),
498
- '',
499
- formatSection(
500
- 'Prompt references (@-syntax)',
501
- promptRefTokens,
502
- promptRefs.map(formatContextReferenceLine),
503
- ),
504
- ].join('\n');
505
-
506
- return {
507
- success: true,
508
- message,
509
- data: {
510
- references,
511
- history: { turnCount: display.turnCount, toolResults: display.toolResults },
512
- },
513
- };
514
- }
@@ -0,0 +1,110 @@
1
+ // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
2
+
3
+ exports[`built-in commands offered to the model > pins the descriptor each offered command gets 1`] = `
4
+ {
5
+ "agent": {
6
+ "argumentHint": "PROMPT | AGENT_NAME PROMPT | list | parallel [--wait|--detach] LABEL:"PROMPT" [LABEL=AGENT_NAME:"PROMPT"] | wait GROUP_ID | read AGENT_ID [OFFSET] | send AGENT_ID PROMPT | stop AGENT_ID | close AGENT_ID",
7
+ "description": "Start and manage background subagent jobs. Natural-language arguments start one background agent job. When the user explicitly asks to create, run, spawn, delegate to, or use agents/subagents, start the requested agent command immediately and do not ask a follow-up question unless execution is impossible or unsafe. If the target item is unspecified, include target selection inside the agent prompt instead of delaying execution. The parallel form starts multiple background agent jobs as a wait_all group and returns a consolidated group summary unless --detach is present. list, wait, read, send, stop, close, and open manage existing agent jobs. Returns the started job id (or group summary), the job list, or the requested log page.",
8
+ "kind": "builtin-command",
9
+ "modelInvocable": true,
10
+ "name": "agent",
11
+ "requiresPermission": false,
12
+ "safety": "background-agent",
13
+ "userInvocable": true,
14
+ },
15
+ "compact": {
16
+ "argumentHint": "[instructions]",
17
+ "description": "Summarize older conversation turns to free context-window space. Use it when the context is nearly full (check with the context command) or before starting a large new task; optional instructions say what the summary must keep. Returns how many messages were removed and the context usage before and after.",
18
+ "kind": "builtin-command",
19
+ "modelInvocable": true,
20
+ "name": "compact",
21
+ "requiresPermission": false,
22
+ "safety": "write",
23
+ "userInvocable": true,
24
+ },
25
+ "context": {
26
+ "argumentHint": "[list]",
27
+ "description": "Inspect the context window. Use it before a large task or before deciding to compact, or to see which files the user pinned into context. Bare returns used/max tokens and percentage, the auto-compact threshold, the pinned-reference summary and the turn count; \`list\` returns the per-category token breakdown and every pinned reference.
28
+ Subcommands you may run (bare \`context\` is also allowed):
29
+ - list: List loaded context references",
30
+ "kind": "builtin-command",
31
+ "modelInvocable": true,
32
+ "name": "context",
33
+ "requiresPermission": false,
34
+ "userInvocable": true,
35
+ },
36
+ "cost": {
37
+ "description": "Report this session’s token usage and estimated cost. Use it before an expensive step (a large refactor, many subagents) or when the user asks what the session has cost. Returns the message count, input/output token totals and the estimated USD cost, with the remaining monthly budget when one is set. Setting or clearing the budget is the user’s decision: suggest \`/cost budget <amount>\`.
38
+ Run it with no arguments.",
39
+ "kind": "builtin-command",
40
+ "modelInvocable": true,
41
+ "name": "cost",
42
+ "requiresPermission": false,
43
+ "userInvocable": true,
44
+ },
45
+ "loop": {
46
+ "argumentHint": "[prompt] | <N><s|m|h|d> [prompt] | <prompt> every <N> <unit> | list | stop <id>",
47
+ "description": "Repeat a prompt on a fixed cadence (\`<N><s|m|h|d> [prompt]\`) or self-paced (bare or prompt only: each iteration chooses its next delay or stops). Use it when the user asks for recurring work in this session. \`list\` shows active loops; \`stop <id>\` ends one. Returns the loop id and its next fire time, or the loop list.",
48
+ "kind": "builtin-command",
49
+ "modelInvocable": true,
50
+ "name": "loop",
51
+ "requiresPermission": true,
52
+ "userInvocable": true,
53
+ },
54
+ "mcp": {
55
+ "argumentHint": "[status]",
56
+ "description": "Show which MCP servers are configured and why any of them is unavailable. Use it when an MCP server or its tools are missing, a tool call reports that a server needs sign-in or approval, or the user asks about MCP servers. Returns one line per server: its name, activation state, sign-in state, and the command to suggest to the user when the server needs their approval, sign-in or workspace trust. You cannot approve, sign in or sign out yourself.
57
+ Subcommands you may run (bare \`mcp\` is also allowed):
58
+ - status: Show every MCP server’s name, activation state and sign-in state",
59
+ "kind": "builtin-command",
60
+ "modelInvocable": true,
61
+ "name": "mcp",
62
+ "requiresPermission": false,
63
+ "userInvocable": true,
64
+ },
65
+ "memory": {
66
+ "argumentHint": "[list | show [topic] | add <user|feedback|project|reference> <topic> <text> | pending | used]",
67
+ "description": "Read and write this project’s durable memory. Use \`list\` or \`show [topic]\` to look up stored conventions, preferences or references before asking the user again; \`add <type> <topic> <text>\` to save a durable preference, project convention, feedback item or reference worth reusing across sessions (never secrets, credentials or transient facts); \`pending\` to see candidates awaiting the user’s review; \`used\` to report which memory items informed this turn. Bare lists topics. Returns the requested topics or entries, or a confirmation of what was saved. Approving or rejecting a pending candidate is the user’s review: suggest \`/memory approve <id>\` or \`/memory reject <id>\`.
68
+ Subcommands you may run (bare \`memory\` is also allowed):
69
+ - list: List project memory topics
70
+ - show: Show project memory index or a topic
71
+ - add: Save durable project memory
72
+ - pending: List pending memory candidates
73
+ - used: Show memory references used in the current turn",
74
+ "kind": "builtin-command",
75
+ "modelInvocable": true,
76
+ "name": "memory",
77
+ "requiresPermission": false,
78
+ "safety": "write",
79
+ "userInvocable": true,
80
+ },
81
+ "monitor": {
82
+ "argumentHint": ""<command>" "<pattern>" <instruction>",
83
+ "description": "Run a command in the background and wake yourself when a line of its output matches a pattern. Use it to wait for a server to start, a test to fail or a log line to appear instead of polling. The command is allowed, refused or asked about by the same permission rules as a shell tool call. Returns the monitor task id, or a refusal naming what the user must allow.",
84
+ "kind": "builtin-command",
85
+ "modelInvocable": true,
86
+ "name": "monitor",
87
+ "requiresPermission": false,
88
+ "userInvocable": true,
89
+ },
90
+ "schedule": {
91
+ "argumentHint": "in <N><s|m|h|d> <instruction> | cron "<expr>" <instruction> | list | pause <id> | resume <id> | edit <id> <spec>",
92
+ "description": "Wake yourself later with an instruction. Use it when work must wait (a deploy, a long build, a reply) instead of polling: \`in <N><s|m|h|d> <instruction>\` or \`cron "<expr>" <instruction>\`; \`list\`, \`pause\`, \`resume\` and \`edit\` manage existing schedules. Returns the schedule id, or the schedule list.",
93
+ "kind": "builtin-command",
94
+ "modelInvocable": true,
95
+ "name": "schedule",
96
+ "requiresPermission": false,
97
+ "userInvocable": true,
98
+ },
99
+ "skills": {
100
+ "argumentHint": "[list | <skill-name> [args]]",
101
+ "description": "Skill command. Before following a matching registered skill from the system prompt Skills section, invoke the projected skills command tool with args "<skill-name> [args]". Without arguments, list registered skills. With a skill name, activate that skill; a skill that declares it is not model-invocable is refused. Returns the skill list with each skill's description, or the activated skill's instructions to follow. Slash syntax is a UI input/display concern; the SDK command identity is "skills".",
102
+ "kind": "builtin-command",
103
+ "modelInvocable": true,
104
+ "name": "skills",
105
+ "requiresPermission": false,
106
+ "safety": "read-only",
107
+ "userInvocable": true,
108
+ },
109
+ }
110
+ `;
@@ -26,10 +26,10 @@ const providerSettingsAdapter: IProviderCommandSettingsAdapter = {
26
26
  writeTargetSettings: () => undefined,
27
27
  };
28
28
 
29
- const baseOptions = { cwd: '/tmp', providerDefinitions, providerSettingsAdapter } as const;
29
+ const baseOptions = { cwd: '/tmp', userLocalStorageRoot: '/tmp/robota-test', providerDefinitions, providerSettingsAdapter } as const;
30
30
 
31
31
  function moduleNames(opts: Parameters<typeof createDefaultCommandModules>[0]): string[] {
32
- return createDefaultCommandModules(opts).map((module) => module.name);
32
+ return createDefaultCommandModules(opts).modules.map((module) => module.name);
33
33
  }
34
34
 
35
35
  describe('createDefaultCommandModules — PRESET-004 module-selection delta', () => {
@@ -40,21 +40,34 @@ describe('createDefaultCommandModules — PRESET-004 module-selection delta', ()
40
40
 
41
41
  it('TC-04: neither enabled nor disabled given → full default set unchanged (no-regression)', () => {
42
42
  const names = moduleNames(baseOptions);
43
- // No-regression: the default set length is the documented 24 modules.
44
- expect(names).toHaveLength(24);
43
+ // No-regression: the default set length is the documented 32 modules (SELFHOST-002 added
44
+ // `/plan`; PEER-004 added `/peers`; CLI-1994 added `/fork`; MCP-2520 added `/mcp`; CLI-1988 added
45
+ // `/output-style`; FLOW-008 added `/effort`; `/advisor` joined beside it). The list below is the
46
+ // assertion that matters — a length on its own can be restored by any substitution, and the count
47
+ // exists only to catch an addition that also removed something.
48
+ expect(names).toHaveLength(35);
45
49
  expect(names).toEqual([
46
50
  'agent-command-skills',
47
51
  'agent-command-help',
48
52
  'agent-command-agent',
53
+ 'agent-command-effort',
54
+ 'agent-command-advisor',
49
55
  'agent-command-permissions',
50
56
  'agent-command-mode',
57
+ 'agent-command-sandbox',
51
58
  'agent-command-preset',
59
+ 'agent-command-output-style',
52
60
  'agent-command-language',
53
61
  'agent-command-background',
62
+ // CLI-1994: registered beside `/background`, because a fork IS a background job.
63
+ 'agent-command-fork',
54
64
  'agent-command-goal',
65
+ 'agent-command-plan',
55
66
  'agent-command-shell',
56
67
  'agent-command-editor',
68
+ 'agent-command-git',
57
69
  'agent-command-memory',
70
+ 'agent-command-mcp-activation',
58
71
  'agent-command-user-local',
59
72
  'agent-command-compact',
60
73
  'agent-command-context',
@@ -66,6 +79,9 @@ describe('createDefaultCommandModules — PRESET-004 module-selection delta', ()
66
79
  'agent-command-statusline',
67
80
  'agent-command-plugin',
68
81
  'agent-command-settings',
82
+ 'agent-command-peers',
83
+ 'agent-command-handoff',
84
+ 'agent-command-remote-control',
69
85
  'agent-command-provider',
70
86
  ]);
71
87
  });
@@ -76,6 +92,16 @@ describe('createDefaultCommandModules — PRESET-004 module-selection delta', ()
76
92
  expect(names).toHaveLength(2);
77
93
  });
78
94
 
95
+ it('BEHAVIOR-2003: registers /keybindings only when the file capability is injected', () => {
96
+ expect(moduleNames(baseOptions)).not.toContain('agent-command-keybindings');
97
+ expect(
98
+ moduleNames({
99
+ ...baseOptions,
100
+ keybindingsFilePort: { ensureFile: async () => '/tmp/keybindings.json' },
101
+ }),
102
+ ).toContain('agent-command-keybindings');
103
+ });
104
+
79
105
  it('TC-02: disabledCommandModules blacklist removes the named module', () => {
80
106
  const full = moduleNames(baseOptions);
81
107
  const names = moduleNames({ ...baseOptions, disabledCommandModules: [BACKGROUND] });
@@ -93,8 +119,34 @@ describe('createDefaultCommandModules — PRESET-004 module-selection delta', ()
93
119
  expect(names).not.toContain(AGENT);
94
120
  });
95
121
 
96
- it('whitelist with an unknown name simply yields no module for it', () => {
97
- const names = moduleNames({ ...baseOptions, enabledCommandModules: [HELP, 'does-not-exist'] });
98
- expect(names).toEqual([HELP]);
122
+ it('INFRA-032: an unknown whitelist name yields no module for it AND is reported as unknown', () => {
123
+ const result = createDefaultCommandModules({
124
+ ...baseOptions,
125
+ enabledCommandModules: [HELP, 'does-not-exist'],
126
+ });
127
+ // Known-name filtering is unchanged — only the matched module survives.
128
+ expect(result.modules.map((module) => module.name)).toEqual([HELP]);
129
+ // The unmatched name is surfaced (no longer silently dropped).
130
+ expect(result.unknownModuleNames).toEqual([{ name: 'does-not-exist', kind: 'enabled' }]);
131
+ });
132
+
133
+ it('INFRA-032: an unknown disabled name (short form) is reported as unknown', () => {
134
+ const result = createDefaultCommandModules({
135
+ ...baseOptions,
136
+ disabledCommandModules: ['editor'],
137
+ });
138
+ // The short form "editor" matches no module name (agent-command-editor stays enabled)…
139
+ expect(result.modules.map((module) => module.name)).toContain('agent-command-editor');
140
+ // …and is surfaced rather than silently ignored.
141
+ expect(result.unknownModuleNames).toEqual([{ name: 'editor', kind: 'disabled' }]);
142
+ });
143
+
144
+ it('INFRA-032: all names matching → unknownModuleNames is empty', () => {
145
+ const result = createDefaultCommandModules({
146
+ ...baseOptions,
147
+ enabledCommandModules: [HELP, AGENT],
148
+ disabledCommandModules: [AGENT],
149
+ });
150
+ expect(result.unknownModuleNames).toEqual([]);
99
151
  });
100
152
  });