@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
@@ -0,0 +1,216 @@
1
+ import { describe, expect, it } from 'vitest';
2
+
3
+ import { createRemoteControlCommandEntry } from '../remote-control-command-module.js';
4
+ import { executeRemoteControlCommand } from '../remote-control-command.js';
5
+ import { createTestCommandHost } from '@robota-sdk/agent-framework/testing';
6
+
7
+ import type {
8
+ ICommandRemoteControlAdapter,
9
+ TRemoteControlStatus,
10
+ } from '@robota-sdk/agent-framework';
11
+
12
+ /**
13
+ * REMOTE-008 — `/remote-control` command: a declarative trigger (enable/stop return host actions) + a
14
+ * status read over the injected `remoteControl` adapter.
15
+ */
16
+
17
+ function ctx(status?: TRemoteControlStatus) {
18
+ const adapter: ICommandRemoteControlAdapter | undefined =
19
+ status === undefined ? undefined : { getStatus: () => status };
20
+ return createTestCommandHost({
21
+ overrides: {
22
+ getCommandHostAdapters: () => (adapter ? { remoteControl: adapter } : {}),
23
+ },
24
+ });
25
+ }
26
+
27
+ describe('executeRemoteControlCommand (REMOTE-008)', () => {
28
+ it('default (no args) requests enable via a host effect (no transport touched by the command)', () => {
29
+ const result = executeRemoteControlCommand(ctx(), '');
30
+ expect(result.success).toBe(true);
31
+ expect(result.hostActions).toEqual([{ type: 'remote-control-enable' }]);
32
+ });
33
+
34
+ it('`enable` / `on` also request enable', () => {
35
+ expect(executeRemoteControlCommand(ctx(), 'enable').hostActions).toEqual([
36
+ { type: 'remote-control-enable' },
37
+ ]);
38
+ expect(executeRemoteControlCommand(ctx(), 'ON').hostActions).toEqual([
39
+ { type: 'remote-control-enable' },
40
+ ]);
41
+ });
42
+
43
+ it('`stop` / `off` request the stop host action', () => {
44
+ expect(executeRemoteControlCommand(ctx(), 'stop').hostActions).toEqual([
45
+ { type: 'remote-control-stop' },
46
+ ]);
47
+ expect(executeRemoteControlCommand(ctx(), 'off').hostActions).toEqual([
48
+ { type: 'remote-control-stop' },
49
+ ]);
50
+ });
51
+
52
+ it('`status` reads the adapter and reports the awaiting-pairing URL', () => {
53
+ const result = executeRemoteControlCommand(
54
+ ctx({ state: 'awaiting-pairing', pairingUrl: 'https://x/#r=a&s=b' }),
55
+ 'status',
56
+ );
57
+ expect(result.success).toBe(true);
58
+ expect(result.message).toContain('https://x/#r=a&s=b');
59
+ expect(result.hostActions).toBeUndefined(); // status is read-only, no action
60
+ });
61
+
62
+ it('`status` with no relay reports the missing-relay guidance', () => {
63
+ const result = executeRemoteControlCommand(ctx({ state: 'no-relay' }), 'status');
64
+ expect(result.message).toMatch(/relayUrl/);
65
+ });
66
+
67
+ it('`status` with no adapter reports unavailability (never throws)', () => {
68
+ const result = executeRemoteControlCommand(ctx(), 'status');
69
+ expect(result.success).toBe(true);
70
+ expect(result.message).toMatch(/not available/i);
71
+ });
72
+
73
+ it('an unknown argument is a usage error (no effect)', () => {
74
+ const result = executeRemoteControlCommand(ctx(), 'frobnicate');
75
+ expect(result.success).toBe(false);
76
+ expect(result.hostActions).toBeUndefined();
77
+ });
78
+
79
+ // REMOTE-012 E3 — trusted-device management verbs.
80
+ function e3ctx(over: Partial<ICommandRemoteControlAdapter>) {
81
+ const adapter: ICommandRemoteControlAdapter = { getStatus: () => ({ state: 'off' }), ...over };
82
+ return createTestCommandHost({
83
+ overrides: { getCommandHostAdapters: () => ({ remoteControl: adapter }) },
84
+ });
85
+ }
86
+
87
+ it('`devices` lists enrolled trusted devices', () => {
88
+ const result = executeRemoteControlCommand(
89
+ e3ctx({
90
+ listDevices: () => [
91
+ { deviceId: 'AbC-123', label: 'phone', lastSeenAt: '2026-07-11T00:00:00Z' },
92
+ ],
93
+ }),
94
+ 'devices',
95
+ );
96
+ expect(result.success).toBe(true);
97
+ expect(result.message).toContain('AbC-123');
98
+ expect(result.message).toContain('phone');
99
+ });
100
+
101
+ it('`devices` with none enrolled says so', () => {
102
+ const result = executeRemoteControlCommand(e3ctx({ listDevices: () => [] }), 'devices');
103
+ expect(result.message).toMatch(/no trusted devices/i);
104
+ });
105
+
106
+ it('`revoke <id>` removes a device (case-preserving id) and reports it', () => {
107
+ let revoked: string | undefined;
108
+ const result = executeRemoteControlCommand(
109
+ e3ctx({
110
+ revokeDevice: (id) => {
111
+ revoked = id;
112
+ return true;
113
+ },
114
+ }),
115
+ 'revoke AbC-123',
116
+ );
117
+ expect(revoked).toBe('AbC-123'); // NOT lowercased
118
+ expect(result.success).toBe(true);
119
+ expect(result.message).toMatch(/revoked/i);
120
+ });
121
+
122
+ it('`revoke` of an unknown id is a failure notice', () => {
123
+ const result = executeRemoteControlCommand(
124
+ e3ctx({ revokeDevice: () => false }),
125
+ 'revoke ghost',
126
+ );
127
+ expect(result.success).toBe(false);
128
+ expect(result.message).toMatch(/no trusted device/i);
129
+ });
130
+
131
+ it('`revoke` with no id is a usage error', () => {
132
+ const result = executeRemoteControlCommand(e3ctx({ revokeDevice: () => true }), 'revoke');
133
+ expect(result.success).toBe(false);
134
+ expect(result.message).toMatch(/usage/i);
135
+ });
136
+
137
+ it('`status` names where the host key is kept', () => {
138
+ const result = executeRemoteControlCommand(
139
+ e3ctx({
140
+ describeKeyStorage: () =>
141
+ 'owner-only file in /h/.robota/credentials (OS keychain unavailable: absent)',
142
+ }),
143
+ 'status',
144
+ );
145
+ expect(result.message).toContain(
146
+ 'Host key storage: owner-only file in /h/.robota/credentials (OS keychain unavailable: absent)',
147
+ );
148
+ });
149
+
150
+ it('`status` says the key storage is still to be chosen before any key was needed', () => {
151
+ const result = executeRemoteControlCommand(
152
+ e3ctx({ describeKeyStorage: () => undefined }),
153
+ 'status',
154
+ );
155
+ expect(result.message).toMatch(/Host key storage: chosen when remote control is first enabled/);
156
+ });
157
+
158
+ // Pairing and trust are the local operator's. A surface that is already connected must not be able
159
+ // to mint a new pairing link, read one, or change which devices are trusted.
160
+ function remoteCtx(over: Partial<ICommandRemoteControlAdapter> = {}) {
161
+ const adapter: ICommandRemoteControlAdapter = { getStatus: () => ({ state: 'off' }), ...over };
162
+ return createTestCommandHost({
163
+ overrides: {
164
+ getCommandHostAdapters: () => ({ remoteControl: adapter }),
165
+ getCommandInvocationSource: () => 'remote',
166
+ },
167
+ });
168
+ }
169
+
170
+ it.each(['', 'enable', 'on'])(
171
+ 'refuses `%s` from a remote surface without requesting enable',
172
+ (verb) => {
173
+ const result = executeRemoteControlCommand(remoteCtx(), verb);
174
+ expect(result.success).toBe(false);
175
+ expect(result.hostActions).toBeUndefined();
176
+ expect(result.message).toMatch(/operator at this terminal/);
177
+ },
178
+ );
179
+
180
+ it('never shows a remote surface the pairing link', () => {
181
+ const result = executeRemoteControlCommand(
182
+ remoteCtx({
183
+ getStatus: () => ({ state: 'awaiting-pairing', pairingUrl: 'https://x/#r=a&s=b' }),
184
+ }),
185
+ 'status',
186
+ );
187
+ expect(result.success).toBe(true);
188
+ expect(result.message).not.toContain('https://x/#r=a&s=b');
189
+ expect(result.message).not.toContain('#r=');
190
+ expect(result.message).toMatch(/waiting for a device to pair/);
191
+ });
192
+
193
+ it('refuses `revoke` from a remote surface without touching the trusted devices', () => {
194
+ const revokeDevice = (): boolean => {
195
+ throw new Error('must not be called');
196
+ };
197
+ const result = executeRemoteControlCommand(remoteCtx({ revokeDevice }), 'revoke AbC-123');
198
+ expect(result.success).toBe(false);
199
+ expect(result.message).toMatch(/operator at this terminal/);
200
+ });
201
+
202
+ it('still lets a remote surface stop remote control and read the other states', () => {
203
+ expect(executeRemoteControlCommand(remoteCtx(), 'stop').hostActions).toEqual([
204
+ { type: 'remote-control-stop' },
205
+ ]);
206
+ const paired = executeRemoteControlCommand(
207
+ remoteCtx({ getStatus: () => ({ state: 'paired' }) }),
208
+ 'status',
209
+ );
210
+ expect(paired.message).toMatch(/a device is paired/);
211
+ });
212
+
213
+ it('stays user-only for the model', () => {
214
+ expect(createRemoteControlCommandEntry().modelInvocable).toBe(false);
215
+ });
216
+ });
@@ -0,0 +1,8 @@
1
+ // @robota-sdk/agent-command-remote-control
2
+
3
+ export {
4
+ createRemoteControlCommandModule,
5
+ createRemoteControlCommandEntry,
6
+ RemoteControlCommandSource,
7
+ } from './remote-control-command-module.js';
8
+ export { executeRemoteControlCommand } from './remote-control-command.js';
@@ -0,0 +1,61 @@
1
+ import { executeRemoteControlCommand } from './remote-control-command.js';
2
+ import { createSystemCommandFromEntry } from '../command-module-utils.js';
3
+
4
+ import type { ICommandModule, ISystemCommand } from '@robota-sdk/agent-framework';
5
+ import type { ICommand, ICommandSource } from '@robota-sdk/agent-interface-command';
6
+
7
+ export function createRemoteControlCommandEntry(): ICommand {
8
+ return {
9
+ name: 'remote-control',
10
+ displayName: 'Remote control',
11
+ description:
12
+ 'Enable P2P remote control (pair a device to co-drive this session), or check status. Pairing and revoking run only for the operator at the host terminal; a connected surface can stop remote control and read its status, without the pairing link.',
13
+ source: 'remote-control',
14
+ // User-only: pairing and revoking devices are trust decisions. The command also refuses them when
15
+ // they come from a connected surface rather than the operator at this terminal.
16
+ modelInvocable: false,
17
+ userInvocable: true,
18
+ argumentHint: '[enable|stop|status|devices|revoke <device-id>]',
19
+ subcommands: [
20
+ { name: 'status', description: 'Show remote-control status', source: 'remote-control' },
21
+ { name: 'devices', description: 'List trusted devices', source: 'remote-control' },
22
+ {
23
+ name: 'enable',
24
+ description: 'Enable remote control and pair a device',
25
+ source: 'remote-control',
26
+ },
27
+ { name: 'stop', description: 'Stop remote control', source: 'remote-control' },
28
+ {
29
+ name: 'revoke',
30
+ description: 'Revoke a trusted device',
31
+ source: 'remote-control',
32
+ argumentHint: '<device-id>',
33
+ },
34
+ ],
35
+ };
36
+ }
37
+
38
+ function createRemoteControlSystemCommand(): ISystemCommand {
39
+ const entry = createRemoteControlCommandEntry();
40
+ return createSystemCommandFromEntry(entry, {
41
+ requiresPermission: false,
42
+ lifecycle: 'inline',
43
+ execute: (context, args) => executeRemoteControlCommand(context, args),
44
+ });
45
+ }
46
+
47
+ export class RemoteControlCommandSource implements ICommandSource {
48
+ readonly name = 'remote-control';
49
+
50
+ getCommands(): ICommand[] {
51
+ return [createRemoteControlCommandEntry()];
52
+ }
53
+ }
54
+
55
+ export function createRemoteControlCommandModule(): ICommandModule {
56
+ return {
57
+ name: 'agent-command-remote-control',
58
+ commandSources: [new RemoteControlCommandSource()],
59
+ systemCommands: [createRemoteControlSystemCommand()],
60
+ };
61
+ }
@@ -0,0 +1,138 @@
1
+ import type {
2
+ ICommandHostAdapterAccess,
3
+ ICommandHostWorkspace,
4
+ TRemoteControlStatus,
5
+ } from '@robota-sdk/agent-framework';
6
+ import type { ICommandResult } from '@robota-sdk/agent-interface-command';
7
+
8
+ /**
9
+ * `/remote-control` (REMOTE-008) — enable/stop P2P remote control and report status.
10
+ *
11
+ * The command is a declarative trigger: `enable` and `stop` return typed HOST ACTIONS
12
+ * (`remote-control-enable` / `remote-control-stop`) executed by the session via the composition-root
13
+ * adapter (commands never construct transports). `status` reads the injected
14
+ * `ICommandHostAdapters.remoteControl.getStatus()` view — absent ⇒ the feature is unavailable in this host.
15
+ *
16
+ * Pairing and trust belong to the operator at this terminal. A surface that is already connected
17
+ * may stop remote control and read its state, but it cannot mint a pairing link, read the one on
18
+ * offer, or revoke a trusted device: otherwise one connected surface could admit the next.
19
+ */
20
+
21
+ const OPERATOR_ONLY =
22
+ 'only the operator at this terminal can do this; a connected surface cannot pair, enroll or revoke devices. ' +
23
+ 'Run `/remote-control` on the host terminal.';
24
+
25
+ function formatStatus(
26
+ status: TRemoteControlStatus | undefined,
27
+ showPairingLink: boolean,
28
+ ): ICommandResult {
29
+ if (!status) {
30
+ return { message: 'Remote control is not available in this environment.', success: true };
31
+ }
32
+ switch (status.state) {
33
+ case 'off':
34
+ return {
35
+ message: 'Remote control is off. Use `/remote-control` to enable it.',
36
+ success: true,
37
+ };
38
+ case 'no-relay':
39
+ return {
40
+ message:
41
+ 'Remote control is unavailable: no signaling relay is configured ' +
42
+ '(set `transports.webrtc.options.relayUrl`).',
43
+ success: true,
44
+ };
45
+ case 'awaiting-pairing':
46
+ return {
47
+ message: showPairingLink
48
+ ? `Remote control is waiting for a device to pair.\nOpen: ${status.pairingUrl}`
49
+ : 'Remote control is waiting for a device to pair. The pairing link is shown only on the host terminal.',
50
+ success: true,
51
+ };
52
+ case 'paired':
53
+ return { message: 'Remote control is active — a device is paired.', success: true };
54
+ default:
55
+ return { message: 'Remote control status is unknown.', success: true };
56
+ }
57
+ }
58
+
59
+ export function executeRemoteControlCommand(
60
+ context: ICommandHostAdapterAccess & Pick<ICommandHostWorkspace, 'getCommandInvocationSource'>,
61
+ args: string,
62
+ ): ICommandResult {
63
+ const operator = context.getCommandInvocationSource() === 'user';
64
+ const trimmed = args.trim();
65
+ // Split into a lowercased verb + the original-case remainder (a deviceId is case-sensitive base64url).
66
+ const spaceAt = trimmed.indexOf(' ');
67
+ const verb = (spaceAt === -1 ? trimmed : trimmed.slice(0, spaceAt)).toLowerCase();
68
+ const rest = spaceAt === -1 ? '' : trimmed.slice(spaceAt + 1).trim();
69
+
70
+ if (verb === 'status') {
71
+ const adapter = context.getCommandHostAdapters?.().remoteControl;
72
+ const result = formatStatus(adapter?.getStatus(), operator);
73
+ if (!adapter?.describeKeyStorage) return result;
74
+ const storage = adapter.describeKeyStorage() ?? 'chosen when remote control is first enabled';
75
+ return { ...result, message: `${result.message}\nHost key storage: ${storage}` };
76
+ }
77
+
78
+ if (verb === 'devices') {
79
+ const adapter = context.getCommandHostAdapters?.().remoteControl;
80
+ const devices = adapter?.listDevices?.();
81
+ if (!devices) {
82
+ return {
83
+ message: 'Trusted-device reconnect is not available in this environment.',
84
+ success: true,
85
+ };
86
+ }
87
+ if (devices.length === 0) {
88
+ return { message: 'No trusted devices are enrolled yet.', success: true };
89
+ }
90
+ const lines = devices.map((d) => ` ${d.deviceId} ${d.label} (last seen ${d.lastSeenAt})`);
91
+ return { message: `Trusted devices:\n${lines.join('\n')}`, success: true };
92
+ }
93
+
94
+ if (verb === 'revoke') {
95
+ if (!operator) {
96
+ return { message: `\`/remote-control revoke\`: ${OPERATOR_ONLY}`, success: false };
97
+ }
98
+ if (!rest) {
99
+ return { message: 'Usage: /remote-control revoke <deviceId>', success: false };
100
+ }
101
+ const adapter = context.getCommandHostAdapters?.().remoteControl;
102
+ if (!adapter?.revokeDevice) {
103
+ return {
104
+ message: 'Trusted-device reconnect is not available in this environment.',
105
+ success: true,
106
+ };
107
+ }
108
+ const removed = adapter.revokeDevice(rest);
109
+ return removed
110
+ ? { message: `Revoked trusted device ${rest}. It must re-pair to reconnect.`, success: true }
111
+ : { message: `No trusted device with id ${rest}.`, success: false };
112
+ }
113
+
114
+ if (verb === 'stop' || verb === 'off') {
115
+ return {
116
+ message: 'Stopping remote control...',
117
+ success: true,
118
+ hostActions: [{ type: 'remote-control-stop' as const }],
119
+ };
120
+ }
121
+
122
+ // Default (empty / `enable` / `on`): request enable. The host reports the pairing QR/link.
123
+ if (verb === '' || verb === 'enable' || verb === 'on') {
124
+ if (!operator) {
125
+ return { message: `\`/remote-control enable\`: ${OPERATOR_ONLY}`, success: false };
126
+ }
127
+ return {
128
+ message: 'Enabling remote control...',
129
+ success: true,
130
+ hostActions: [{ type: 'remote-control-enable' as const }],
131
+ };
132
+ }
133
+
134
+ return {
135
+ message: `Unknown argument "${verb}". Usage: /remote-control [enable|stop|status|devices|revoke <id>]`,
136
+ success: false,
137
+ };
138
+ }
@@ -1,5 +1,5 @@
1
1
  import { describe, expect, it } from 'vitest';
2
- import type { ICommandHostContext } from '@robota-sdk/agent-framework';
2
+ import { createTestCommandHost } from '@robota-sdk/agent-framework/testing';
3
3
  import { SystemCommandExecutor } from '@robota-sdk/agent-framework';
4
4
  import {
5
5
  createResetCommandEntry,
@@ -7,7 +7,7 @@ import {
7
7
  executeResetCommand,
8
8
  } from '../index.js';
9
9
 
10
- const COMMAND_CONTEXT = {} as ICommandHostContext;
10
+ const COMMAND_CONTEXT = createTestCommandHost();
11
11
 
12
12
  describe('createResetCommandModule', () => {
13
13
  it('provides reset metadata and a user-only executable command from one module owner', () => {
@@ -36,13 +36,13 @@ describe('createResetCommandModule', () => {
36
36
  });
37
37
 
38
38
  it('emits a typed settings reset effect without host file I/O', () => {
39
- const result = executeResetCommand(COMMAND_CONTEXT, '');
39
+ const result = executeResetCommand();
40
40
 
41
41
  expect(result).toEqual({
42
42
  success: true,
43
43
  message: 'Reset requested.',
44
44
  data: { resetRequested: true },
45
- effects: [{ type: 'settings-reset-requested' }],
45
+ hostActions: [{ type: 'settings-reset' }],
46
46
  });
47
47
  });
48
48
 
@@ -57,7 +57,7 @@ describe('createResetCommandModule', () => {
57
57
  success: true,
58
58
  message: 'Reset requested.',
59
59
  data: { resetRequested: true },
60
- effects: [{ type: 'settings-reset-requested' }],
60
+ hostActions: [{ type: 'settings-reset' }],
61
61
  });
62
62
  });
63
63
  });
@@ -1,7 +1,7 @@
1
1
  import { executeResetCommand } from './reset-command.js';
2
2
 
3
3
  import type { ICommandModule, ISystemCommand } from '@robota-sdk/agent-framework';
4
- import type { ICommand, ICommandSource } from '@robota-sdk/agent-interface-transport';
4
+ import type { ICommand, ICommandSource } from '@robota-sdk/agent-interface-command';
5
5
 
6
6
  const RESET_COMMAND_DESCRIPTION = 'Delete settings';
7
7
 
@@ -11,6 +11,7 @@ export function createResetCommandEntry(): ICommand {
11
11
  displayName: 'Reset Settings',
12
12
  description: RESET_COMMAND_DESCRIPTION,
13
13
  source: 'reset',
14
+ // User-only: discards the user's settings.
14
15
  modelInvocable: false,
15
16
  };
16
17
  }
@@ -1,11 +1,10 @@
1
- import type { ICommandHostContext } from '@robota-sdk/agent-framework';
2
- import type { ICommandResult } from '@robota-sdk/agent-interface-transport';
1
+ import type { ICommandResult } from '@robota-sdk/agent-interface-command';
3
2
 
4
- export function executeResetCommand(_context: ICommandHostContext, _args: string): ICommandResult {
3
+ export function executeResetCommand(): ICommandResult {
5
4
  return {
6
5
  success: true,
7
6
  message: 'Reset requested.',
8
7
  data: { resetRequested: true },
9
- effects: [{ type: 'settings-reset-requested' }],
8
+ hostActions: [{ type: 'settings-reset' }],
10
9
  };
11
10
  }
@@ -93,6 +93,9 @@ describe('createRewindCommandModule', () => {
93
93
  'restore',
94
94
  'code',
95
95
  'rollback',
96
+ 'fork',
97
+ 'switch',
98
+ 'branches',
96
99
  ]);
97
100
  expect(module.systemCommands?.map((command) => command.name)).toEqual(['rewind']);
98
101
  expect(module.systemCommands?.[0]?.userInvocable).toBe(true);
@@ -186,6 +189,43 @@ describe('executeRewindCommand', () => {
186
189
  expect(result?.message).toContain('Rolled back code through turn-0001.');
187
190
  });
188
191
 
192
+ // SELFHOST-007 TC-05: branching subcommands route through the host context.
193
+ it('delegates fork through session.executeCommand (non-destructive)', async () => {
194
+ const session = createInteractiveSession();
195
+ const forkCheckpointBranch = vi
196
+ .spyOn(session, 'forkCheckpointBranch')
197
+ .mockResolvedValue(createRestoreResult());
198
+
199
+ const result = await session.executeCommand('rewind', 'fork turn-0001');
200
+
201
+ expect(result?.success).toBe(true);
202
+ expect(forkCheckpointBranch).toHaveBeenCalledWith('turn-0001');
203
+ expect(result?.message).toContain('Forked a new branch from turn-0001');
204
+ });
205
+
206
+ it('delegates switch through session.executeCommand', async () => {
207
+ const session = createInteractiveSession();
208
+ const switchCheckpointBranch = vi.spyOn(session, 'switchCheckpointBranch').mockReturnValue();
209
+
210
+ const result = await session.executeCommand('rewind', 'switch turn-0002');
211
+
212
+ expect(result?.success).toBe(true);
213
+ expect(switchCheckpointBranch).toHaveBeenCalledWith('turn-0002');
214
+ expect(result?.message).toContain('Switched to checkpoint branch turn-0002.');
215
+ });
216
+
217
+ it('lists branch tips via the branches subcommand', async () => {
218
+ const session = createInteractiveSession();
219
+ vi.spyOn(session, 'listCheckpointBranches').mockReturnValue(['turn-0003', 'turn-0005']);
220
+
221
+ const result = await session.executeCommand('rewind', 'branches');
222
+
223
+ expect(result?.success).toBe(true);
224
+ expect(result?.message).toContain('turn-0003');
225
+ expect(result?.message).toContain('turn-0005');
226
+ expect(result?.data?.branches).toEqual(['turn-0003', 'turn-0005']);
227
+ });
228
+
189
229
  it('returns usage for invalid arguments without mutating state', async () => {
190
230
  const session = createInteractiveSession();
191
231
  const restoreEditCheckpoint = vi.spyOn(session, 'restoreEditCheckpoint');
@@ -7,7 +7,7 @@ import {
7
7
  import { executeRewindCommand } from './rewind-command.js';
8
8
 
9
9
  import type { ICommandModule, ISystemCommand } from '@robota-sdk/agent-framework';
10
- import type { ICommand, ICommandSource } from '@robota-sdk/agent-interface-transport';
10
+ import type { ICommand, ICommandSource } from '@robota-sdk/agent-interface-command';
11
11
 
12
12
  export function createRewindCommandEntry(): ICommand {
13
13
  return {
@@ -16,6 +16,7 @@ export function createRewindCommandEntry(): ICommand {
16
16
  description: REWIND_COMMAND_DESCRIPTION,
17
17
  source: 'rewind',
18
18
  argumentHint: REWIND_COMMAND_ARGUMENT_HINT,
19
+ // User-only: rewrites the user's history and files; the user decides what to undo.
19
20
  modelInvocable: false,
20
21
  safety: 'write',
21
22
  subcommands: buildRewindCommandSubcommands(),