@robota-sdk/agent-command 3.0.0-beta.82 → 3.0.0-beta.84

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 (94) hide show
  1. package/CHANGELOG.md +1071 -0
  2. package/README.md +71 -62
  3. package/dist/node/index.cjs +81 -72
  4. package/dist/node/index.d.cts +136 -52
  5. package/dist/node/index.d.cts.map +1 -1
  6. package/dist/node/index.d.ts +136 -52
  7. package/dist/node/index.d.ts.map +1 -1
  8. package/dist/node/index.js +79 -70
  9. package/dist/node/index.js.map +1 -1
  10. package/package.json +16 -10
  11. package/src/agent/__tests__/agent-command.test.ts +62 -0
  12. package/src/agent/agent-command-parser.ts +2 -2
  13. package/src/agent/agent-command.ts +20 -0
  14. package/src/command-module-utils.test.ts +37 -0
  15. package/src/command-module-utils.ts +2 -0
  16. package/src/default/__tests__/__snapshots__/model-exposure.test.ts.snap +2 -2
  17. package/src/default/__tests__/default-command-modules.test.ts +54 -14
  18. package/src/default/__tests__/model-exposure.test.ts +18 -3
  19. package/src/default/__tests__/org-policy-forwarding.test.ts +10 -2
  20. package/src/default/default-command-modules.ts +15 -8
  21. package/src/devices/__tests__/devices-command-module.test.ts +99 -12
  22. package/src/devices/devices-command-module.ts +108 -24
  23. package/src/devices/devices-command-port.ts +62 -1
  24. package/src/devices/index.ts +3 -0
  25. package/src/doctor/doctor-node-deps.ts +6 -7
  26. package/src/editor/editor-command-module.ts +5 -0
  27. package/src/effort/effort-command.test.ts +65 -2
  28. package/src/effort/effort-command.ts +19 -5
  29. package/src/events/__tests__/events-command.test.ts +73 -0
  30. package/src/events/events-command-module.ts +49 -0
  31. package/src/events/events-command.ts +69 -0
  32. package/src/events/index.ts +2 -0
  33. package/src/git/__tests__/fake-git-port.ts +9 -2
  34. package/src/git/__tests__/git-command-module.test.ts +10 -2
  35. package/src/git/git-command-module.ts +3 -4
  36. package/src/git/git-commit.ts +6 -3
  37. package/src/git/index.ts +4 -1
  38. package/src/goal/goal-command.ts +3 -1
  39. package/src/handoff/__tests__/handoff-command-module.test.ts +2 -1
  40. package/src/handoff/handoff-command-module.ts +1 -1
  41. package/src/handoff/handoff-command.ts +2 -2
  42. package/src/help/__tests__/help-command-module.test.ts +3 -0
  43. package/src/help/__tests__/help-command.test.ts +3 -0
  44. package/src/index.ts +14 -0
  45. package/src/keybindings/keybindings-command-module.ts +17 -3
  46. package/src/mcp-activation/__tests__/mcp-activation-command.test.ts +172 -0
  47. package/src/mcp-activation/mcp-activation-command-module.ts +6 -0
  48. package/src/mcp-activation/mcp-activation-command.ts +62 -4
  49. package/src/memory/__tests__/memory-command-module.test.ts +40 -2
  50. package/src/memory/__tests__/memory-command-unavailable.test.ts +63 -0
  51. package/src/memory/memory-command-module.ts +4 -2
  52. package/src/memory/memory-command.ts +42 -2
  53. package/src/mode/__tests__/mode-command-module.test.ts +40 -0
  54. package/src/mode/mode-command.ts +1 -1
  55. package/src/model/__tests__/model-command-module.test.ts +80 -0
  56. package/src/model/__tests__/model-command.test.ts +240 -0
  57. package/src/model/index.ts +6 -0
  58. package/src/model/model-command-module.ts +56 -0
  59. package/src/model/model-command.ts +223 -0
  60. package/src/peers/__tests__/peers-command-module.test.ts +24 -0
  61. package/src/peers/__tests__/peers-command.test.ts +78 -0
  62. package/src/peers/peers-command-module.ts +1 -1
  63. package/src/peers/peers-command.ts +33 -4
  64. package/src/plugin/__tests__/plugin-command-module.test.ts +81 -1
  65. package/src/plugin/plugin-command.ts +47 -4
  66. package/src/provider/__tests__/org-policy.test.ts +4 -0
  67. package/src/provider/__tests__/provider-command-module.test.ts +276 -14
  68. package/src/provider/__tests__/scripted-interaction.ts +12 -2
  69. package/src/provider/provider-command-execution.ts +54 -8
  70. package/src/provider/provider-command-profile-lifecycle.ts +48 -2
  71. package/src/provider/provider-command-profile-operations.ts +14 -0
  72. package/src/provider/provider-command-profile.ts +7 -4
  73. package/src/provider/provider-command-setup.ts +14 -1
  74. package/src/rewind/__tests__/rewind-command-module.test.ts +32 -1
  75. package/src/rewind/rewind-command.ts +23 -2
  76. package/src/schedule/__tests__/loop-command-functional.test.ts +13 -0
  77. package/src/schedule/__tests__/loop-command.test.ts +238 -4
  78. package/src/schedule/__tests__/schedule-redos.test.ts +33 -0
  79. package/src/schedule/loop-command.ts +98 -4
  80. package/src/schedule/schedule-command-module.ts +8 -4
  81. package/src/settings/settings-command-module.ts +2 -2
  82. package/src/shell/shell-command-module.ts +5 -0
  83. package/src/terminal-client/__tests__/terminal-client-commands.test.ts +216 -0
  84. package/src/terminal-client/index.ts +5 -0
  85. package/src/terminal-client/terminal-client-commands.ts +109 -0
  86. package/src/theme/__tests__/theme-command.test.ts +1 -1
  87. package/src/theme/theme-command-module.ts +12 -2
  88. package/src/user-local/__tests__/user-local-command.test.ts +2 -2
  89. package/src/git/__tests__/git-diff.test.ts +0 -125
  90. package/src/git/__tests__/git-process.test.ts +0 -221
  91. package/src/git/__tests__/git-status.test.ts +0 -117
  92. package/src/git/git-diff.ts +0 -129
  93. package/src/git/git-process.ts +0 -169
  94. package/src/git/git-status.ts +0 -134
@@ -1,5 +1,5 @@
1
1
  import { MODEL_EFFORT_VALUES, selectAction } from '@robota-sdk/agent-core';
2
- import { parseModelEffort, resolveModelEffort } from '@robota-sdk/agent-framework';
2
+ import { EFFORT_LEVEL_LABELS, parseModelEffort, resolveModelEffort } from '@robota-sdk/agent-framework';
3
3
 
4
4
  import type {
5
5
  ICommandHostAdapterAccess,
@@ -14,13 +14,27 @@ import type { ICommandResult } from '@robota-sdk/agent-interface-command';
14
14
 
15
15
  const EFFORT_SELECTIONS: readonly TEffortSelection[] = ['auto', ...MODEL_EFFORT_VALUES];
16
16
 
17
+ /**
18
+ * #3282 §2 — re-exported for callers already importing it from here; the canonical definition moved
19
+ * to `agent-framework` (`effort/effort-resolution.ts`) so headless `/goal` output (in that same
20
+ * package) can use the same labels without a circular dependency on `agent-command`.
21
+ */
22
+ export { EFFORT_LEVEL_LABELS };
23
+
24
+ /**
25
+ * Plain outcome text (#3282 §2), replacing the earlier `Model effort: requested=…, effective=…,
26
+ * source=…, disposition=…` internal-field dump. `not-applied` means the model has no matching native
27
+ * control at all — the earlier fields carried no information a person could act on for that case, so
28
+ * it gets its own plain sentence instead of a label.
29
+ */
17
30
  function formatEffortMessage(resolution: {
18
31
  requested: TEffortSelection;
19
- effective: string;
20
- source: string;
21
32
  disposition: string;
22
33
  }): string {
23
- return `Model effort: requested=${resolution.requested}, effective=${resolution.effective}, source=${resolution.source}, disposition=${resolution.disposition}.`;
34
+ if (resolution.disposition === 'not-applied') {
35
+ return "This model doesn't support effort levels, so it will use its default.";
36
+ }
37
+ return `Effort: ${EFFORT_LEVEL_LABELS[resolution.requested]}`;
24
38
  }
25
39
 
26
40
  async function askForEffort(
@@ -32,7 +46,7 @@ async function askForEffort(
32
46
  selectAction(
33
47
  'effort',
34
48
  'Select model effort',
35
- EFFORT_SELECTIONS.map((value) => ({ value, label: value })),
49
+ EFFORT_SELECTIONS.map((value) => ({ value, label: EFFORT_LEVEL_LABELS[value] })),
36
50
  ),
37
51
  );
38
52
  const selected = response.type === 'answer' ? response.values[0] : undefined;
@@ -0,0 +1,73 @@
1
+ import { describe, expect, it, vi } from 'vitest';
2
+
3
+ import { executeEventsCommand } from '../events-command.js';
4
+ import { createEventsCommandModule } from '../events-command-module.js';
5
+
6
+ import type { ICommandExternalEventsAdapter } from '@robota-sdk/agent-framework';
7
+
8
+ function adapter(): ICommandExternalEventsAdapter & { revoke: ReturnType<typeof vi.fn> } {
9
+ return {
10
+ list: () => [
11
+ {
12
+ grantId: 'ci',
13
+ principal: 'client',
14
+ state: 'open',
15
+ counters: { accepted: 2, refused: { expired: 1 }, settled: { completed: 2 } },
16
+ },
17
+ ],
18
+ revoke: vi.fn((grantId: string) => (grantId === 'ci' ? 'revoked' : 'unknown-grant')),
19
+ };
20
+ }
21
+
22
+ const context = (externalEvents?: ICommandExternalEventsAdapter) => ({
23
+ getCommandHostAdapters: () => (externalEvents ? { externalEvents } : {}),
24
+ });
25
+
26
+ describe('/events command module', () => {
27
+ it('is user-only: never model-invocable, and palette metadata matches the executable', () => {
28
+ const module = createEventsCommandModule();
29
+ const palette = module.commandSources?.[0]?.getCommands()[0];
30
+ const executable = module.systemCommands?.[0];
31
+ expect(palette?.name).toBe('events');
32
+ expect(executable?.modelInvocable).toBe(false);
33
+ expect(palette?.modelInvocable).toBe(false);
34
+ expect(executable?.userInvocable).toBe(true);
35
+ expect(executable?.description).toBe(palette?.description);
36
+ });
37
+
38
+ it('tells the model what it does, what it returns, that it is user-only, and what to suggest', () => {
39
+ const description =
40
+ createEventsCommandModule().commandSources?.[0]?.getCommands()[0]?.description;
41
+ expect(description).toMatch(/external event grants/i);
42
+ expect(description).toMatch(/returns/i);
43
+ expect(description).toMatch(/user-only/i);
44
+ expect(description).toContain('`/events revoke <grant-id>`');
45
+ });
46
+ });
47
+
48
+ describe('/events', () => {
49
+ it('lists grants with their principal kind and counts', async () => {
50
+ const result = await executeEventsCommand(context(adapter()), '');
51
+ expect(result.success).toBe(true);
52
+ expect(result.message).toContain(
53
+ 'ci client open accepted 2; refused: expired 1; settled: completed 2',
54
+ );
55
+ });
56
+
57
+ it('revokes one grant by label and says when the label is unknown', async () => {
58
+ const events = adapter();
59
+ expect((await executeEventsCommand(context(events), 'revoke ci')).message).toMatch(
60
+ /Revoked external event grant ci/,
61
+ );
62
+ expect(events.revoke).toHaveBeenCalledWith('ci');
63
+ const unknown = await executeEventsCommand(context(events), 'revoke nope');
64
+ expect(unknown.success).toBe(false);
65
+ expect(unknown.message).toMatch(/no external event grant nope/);
66
+ expect((await executeEventsCommand(context(events), 'revoke')).success).toBe(false);
67
+ });
68
+
69
+ it('says how grants are given when the session holds none', async () => {
70
+ const result = await executeEventsCommand(context(), '');
71
+ expect(result.message).toMatch(/only when a session starts/);
72
+ });
73
+ });
@@ -0,0 +1,49 @@
1
+ import { executeEventsCommand } from './events-command.js';
2
+
3
+ import type { ICommandModule, ISystemCommand } from '@robota-sdk/agent-framework';
4
+ import type { ICommand, ICommandSource } from '@robota-sdk/agent-interface-command';
5
+
6
+ export function createEventsCommandEntry(): ICommand {
7
+ return {
8
+ name: 'events',
9
+ displayName: 'Events',
10
+ description:
11
+ "List this session's external event grants (each grant's label, whether it pins a subject or a client, whether it is open or revoked, and its counts of accepted, refused and settled events), or revoke one with `/events revoke <grant-id>`. Returns the grants, or whether the revocation took effect. User-only: the model cannot run it; when the user wants to stop events from outside, suggest `/events revoke <grant-id>`.",
12
+ source: 'events',
13
+ argumentHint: '[revoke <grant-id>]',
14
+ // User-only: a grant is the owner's standing decision that someone outside may put turns into
15
+ // this session. Withdrawing it is the owner's call too, and listing it lives on the same command.
16
+ modelInvocable: false,
17
+ };
18
+ }
19
+
20
+ function createEventsSystemCommand(): ISystemCommand {
21
+ const entry = createEventsCommandEntry();
22
+ return {
23
+ name: entry.name,
24
+ displayName: entry.displayName,
25
+ description: entry.description,
26
+ requiresPermission: false,
27
+ userInvocable: true,
28
+ modelInvocable: false,
29
+ ...(entry.argumentHint !== undefined ? { argumentHint: entry.argumentHint } : {}),
30
+ lifecycle: 'inline',
31
+ execute: (context, args) => executeEventsCommand(context, args),
32
+ };
33
+ }
34
+
35
+ export class EventsCommandSource implements ICommandSource {
36
+ readonly name = 'events';
37
+
38
+ getCommands(): ICommand[] {
39
+ return [createEventsCommandEntry()];
40
+ }
41
+ }
42
+
43
+ export function createEventsCommandModule(): ICommandModule {
44
+ return {
45
+ name: 'agent-command-events',
46
+ commandSources: [new EventsCommandSource()],
47
+ systemCommands: [createEventsSystemCommand()],
48
+ };
49
+ }
@@ -0,0 +1,69 @@
1
+ import type {
2
+ ICommandExternalEventGrant,
3
+ ICommandHostAdapterAccess,
4
+ } from '@robota-sdk/agent-framework';
5
+ import type { ICommandResult } from '@robota-sdk/agent-interface-command';
6
+
7
+ const GRANT_ID = /^[a-zA-Z0-9_-]{1,64}$/u;
8
+
9
+ function counts(values: Readonly<Partial<Record<string, number>>>): string {
10
+ const entries = Object.entries(values).filter(([, count]) => (count ?? 0) > 0);
11
+ return entries.length === 0
12
+ ? 'none'
13
+ : entries.map(([key, count]) => `${key} ${count}`).join(', ');
14
+ }
15
+
16
+ /** Label, principal kind, state and counts. The principal itself is never shown. */
17
+ function describe(grant: ICommandExternalEventGrant): string {
18
+ const { counters } = grant;
19
+ return (
20
+ ` ${grant.grantId} ${grant.principal} ${grant.state} accepted ${counters.accepted}; ` +
21
+ `refused: ${counts(counters.refused)}; settled: ${counts(counters.settled)}`
22
+ );
23
+ }
24
+
25
+ /**
26
+ * `/events` — this session's external-event grants, and `/events revoke <grant-id>` to withdraw one.
27
+ * Grants are given only when the session starts; nothing here creates one.
28
+ */
29
+ export async function executeEventsCommand(
30
+ context: ICommandHostAdapterAccess,
31
+ args = '',
32
+ ): Promise<ICommandResult> {
33
+ const adapter = context.getCommandHostAdapters?.().externalEvents;
34
+ if (!adapter) {
35
+ return {
36
+ message:
37
+ 'This session holds no external event grant. Grants are given only when a session starts.',
38
+ success: true,
39
+ };
40
+ }
41
+ const [verb, grantId, extra] = args
42
+ .trim()
43
+ .split(/\s+/u)
44
+ .filter((word) => word !== '');
45
+ if (verb === undefined) {
46
+ const grants = adapter.list();
47
+ return {
48
+ message:
49
+ grants.length === 0
50
+ ? 'This session holds no external event grant.'
51
+ : `External event grants:\n${grants.map(describe).join('\n')}\n\nWithdraw one: /events revoke <grant-id>`,
52
+ success: true,
53
+ };
54
+ }
55
+ if (
56
+ verb !== 'revoke' ||
57
+ grantId === undefined ||
58
+ extra !== undefined ||
59
+ !GRANT_ID.test(grantId)
60
+ ) {
61
+ return { message: 'Usage: /events | /events revoke <grant-id>', success: false };
62
+ }
63
+ return adapter.revoke(grantId) === 'revoked'
64
+ ? {
65
+ message: `Revoked external event grant ${grantId}. Later events for it are refused.`,
66
+ success: true,
67
+ }
68
+ : { message: `This session holds no external event grant ${grantId}.`, success: false };
69
+ }
@@ -0,0 +1,2 @@
1
+ /** Only the module factory leaves this directory; the command is registered by default. */
2
+ export { createEventsCommandModule } from './events-command-module.js';
@@ -1,5 +1,12 @@
1
- /** A scripted `IGitProcessPort`: answers by argv, records every call, throws on an unscripted one. */
2
- import type { IGitProcessPort, TGitProcessOutcome } from '../git-process.js';
1
+ /**
2
+ * A scripted `IGitProcessPort`: answers by argv, records every call, throws on an unscripted one.
3
+ *
4
+ * The port and outcome types moved to `@robota-sdk/agent-framework` with the git process module
5
+ * (#3282 §4c); this double stays here (duplicated, not imported cross-package) because it is a tiny,
6
+ * self-contained test fixture, the same call `agent-framework`'s own `git/__tests__/fake-git-port.ts`
7
+ * makes for its side.
8
+ */
9
+ import type { IGitProcessPort, TGitProcessOutcome } from '@robota-sdk/agent-framework';
3
10
 
4
11
  export interface IFakeGitPort extends IGitProcessPort {
5
12
  readonly calls: readonly (readonly string[])[];
@@ -1,5 +1,9 @@
1
1
  /** BEHAVIOR-2437 TC-05 — registration, the descriptor, and the verb parse through the real session. */
2
- import { afterEach, describe, expect, it } from 'vitest';
2
+ import { mkdtempSync, rmSync } from 'node:fs';
3
+ import { tmpdir } from 'node:os';
4
+ import { join } from 'node:path';
5
+
6
+ import { afterAll, afterEach, describe, expect, it } from 'vitest';
3
7
 
4
8
  import { scriptedSession, type ScriptedSessionHarness } from '@robota-sdk/agent-framework/testing';
5
9
 
@@ -35,7 +39,11 @@ const providerSettingsAdapter: IProviderCommandSettingsAdapter = {
35
39
  readTargetSettings: () => ({}) as TProviderSettingsDocument,
36
40
  writeTargetSettings: () => undefined,
37
41
  };
38
- const BASE_OPTIONS = { cwd: '/tmp', userLocalStorageRoot: '/tmp/robota-test', providerDefinitions, providerSettingsAdapter } as const;
42
+ /** User-local storage lives in a private per-run directory, never a fixed name under /tmp. */
43
+ const USER_LOCAL_STORAGE_ROOT = mkdtempSync(join(tmpdir(), 'robota-test-'));
44
+ afterAll(() => rmSync(USER_LOCAL_STORAGE_ROOT, { recursive: true, force: true }));
45
+
46
+ const BASE_OPTIONS = { cwd: '/tmp', userLocalStorageRoot: USER_LOCAL_STORAGE_ROOT, providerDefinitions, providerSettingsAdapter } as const;
39
47
 
40
48
  const STATUS_FIXTURE = '# branch.head main\0? scratch.log\0';
41
49
 
@@ -5,15 +5,14 @@
5
5
  * on the user path (`SystemCommandExecutor.executeCommand` calls `execute` directly).
6
6
  */
7
7
  import { executeGitCommit } from './git-commit.js';
8
- import { executeGitDiff } from './git-diff.js';
9
- import { createGitProcess } from './git-process.js';
10
- import { executeGitStatus } from './git-status.js';
11
8
 
12
- import type { IGitProcessPort } from './git-process.js';
9
+ import { createGitProcess, executeGitDiff, executeGitStatus } from '@robota-sdk/agent-framework';
10
+
13
11
  import type {
14
12
  ICommandHostUserInteraction,
15
13
  ICommandHostWorkspace,
16
14
  ICommandModule,
15
+ IGitProcessPort,
17
16
  ISystemCommand,
18
17
  } from '@robota-sdk/agent-framework';
19
18
  import type { ICommand, ICommandResult, ICommandSource } from '@robota-sdk/agent-interface-command';
@@ -9,10 +9,13 @@
9
9
  */
10
10
  import { confirmAction, isConfirmed, textAction } from '@robota-sdk/agent-core';
11
11
 
12
- import { gitFailureMessage } from './git-process.js';
13
- import { parseStatusPorcelainV2, GIT_STATUS_ARGS } from './git-status.js';
12
+ import {
13
+ gitFailureMessage,
14
+ parseStatusPorcelainV2,
15
+ GIT_STATUS_ARGS,
16
+ } from '@robota-sdk/agent-framework';
14
17
 
15
- import type { IGitProcessPort } from './git-process.js';
18
+ import type { IGitProcessPort } from '@robota-sdk/agent-framework';
16
19
  import type { IUserInteraction } from '@robota-sdk/agent-core';
17
20
  import type { ICommandResult } from '@robota-sdk/agent-interface-command';
18
21
 
package/src/git/index.ts CHANGED
@@ -1,3 +1,6 @@
1
+ // The git process port and the status/diff readers moved to `@robota-sdk/agent-framework` (#3282
2
+ // §4c) so the Project panel's session-level reads and this command run through the exact same
3
+ // functions; re-exported here so an existing `from '@robota-sdk/agent-command'` import keeps working.
1
4
  export {
2
5
  createGitProcess,
3
6
  gitEnvironment,
@@ -6,7 +9,7 @@ export {
6
9
  type IGitProcessRunOptions,
7
10
  type TGitProcessFailureReason,
8
11
  type TGitProcessOutcome,
9
- } from './git-process.js';
12
+ } from '@robota-sdk/agent-framework';
10
13
  export {
11
14
  createGitCommandEntry,
12
15
  createGitCommandModule,
@@ -4,6 +4,8 @@
4
4
  * command host context; all loop logic lives in agent-framework.
5
5
  */
6
6
 
7
+ import { isGoalCancelVerb } from '@robota-sdk/agent-framework';
8
+
7
9
  import type { ICommandHostGoal } from '@robota-sdk/agent-framework';
8
10
  import type { ICommandResult } from '@robota-sdk/agent-interface-command';
9
11
  import type { IGoalState } from '@robota-sdk/agent-interface-session';
@@ -45,7 +47,7 @@ export async function executeGoalCommand(
45
47
  : { message: 'No goal is set.', success: true };
46
48
  }
47
49
 
48
- if (verb === 'cancel' || verb === 'stop') {
50
+ if (isGoalCancelVerb(trimmed)) {
49
51
  const stopped = context.cancelGoal();
50
52
  return stopped
51
53
  ? { message: `Goal cancelled: ${stopped.objective}`, success: true }
@@ -17,7 +17,8 @@ describe('/handoff command module', () => {
17
17
  it('tells the model what it does and which command to suggest, since it cannot run it', () => {
18
18
  const description =
19
19
  createHandoffCommandModule().commandSources?.[0]?.getCommands()[0]?.description;
20
- expect(description).toContain('/handoff <session-id>');
20
+ expect(description).toContain('/handoff <session-or-device-id>');
21
+ expect(description).toMatch(/linked over the device mesh/);
21
22
  expect(description).toMatch(/user-only/i);
22
23
  expect(description).toContain('saved, not started');
23
24
  });
@@ -8,7 +8,7 @@ export function createHandoffCommandEntry(): ICommand {
8
8
  name: 'handoff',
9
9
  displayName: 'Hand off',
10
10
  description:
11
- 'Push this conversation to another running Robota session of the same user on this machine, after the operator confirms what stays behind (uncommitted changes, running processes; credentials never travel). The receiving operator must also accept; the session arrives saved, not started, and this one ends once it is saved there. With no argument it lists the sessions it could go to. User-only: the model cannot run it; when the user wants to continue this work in another session, suggest they run `/handoff <session-id>`.',
11
+ "Push this conversation to another running Robota session of the same user on this machine, or to another of the user's devices linked over the device mesh, after the operator confirms what stays behind (uncommitted changes, running processes; credentials never travel). The receiving operator must also accept; the session arrives saved, not started, and this one ends once it is saved there. With no argument it lists the sessions and devices it could go to. User-only: the model cannot run it; when the user wants to continue this work in another session or on another device, suggest they run `/handoff <session-or-device-id>`.",
12
12
  source: 'handoff',
13
13
  // User-only: a hand-off moves authority over the operator's work to another place, a decision
14
14
  // about where the person is, not about the task.
@@ -47,8 +47,8 @@ function usage(): ICommandResult {
47
47
  success: true,
48
48
  message: [
49
49
  'Usage:',
50
- ' /handoff list where this session could move to',
51
- ' /handoff <session-id> move it there, after confirming what stays behind',
50
+ ' /handoff list where this session could move to',
51
+ ' /handoff <session-or-device-id> move it there, after confirming what stays behind',
52
52
  ].join('\n'),
53
53
  };
54
54
  }
@@ -55,12 +55,14 @@ function createCommandHostContext() {
55
55
  displayName: 'Help',
56
56
  description: 'Show available commands',
57
57
  modelInvocable: true,
58
+ runner: 'runtime',
58
59
  },
59
60
  {
60
61
  name: 'provider',
61
62
  displayName: 'Provider Setup',
62
63
  description: 'Manage provider profiles',
63
64
  modelInvocable: true,
65
+ runner: 'runtime',
64
66
  },
65
67
  // SEC-008: `plugin` installs and enables code, so it is NOT model-invocable — the fixture says
66
68
  // what the real command says rather than the value that happened to compile.
@@ -69,6 +71,7 @@ function createCommandHostContext() {
69
71
  displayName: 'Plugins',
70
72
  description: 'Manage plugins',
71
73
  modelInvocable: false,
74
+ runner: 'runtime',
72
75
  },
73
76
  ],
74
77
  listEditCheckpoints: () => [],
@@ -86,6 +86,7 @@ describe('formatCommandHelpMessage — example field', () => {
86
86
  description: 'Compress context window',
87
87
  example: '/compact Summarize the current context',
88
88
  modelInvocable: true,
89
+ runner: 'runtime',
89
90
  },
90
91
  ]);
91
92
 
@@ -102,6 +103,7 @@ describe('formatCommandHelpMessage — example field', () => {
102
103
  description: 'Manage provider profiles',
103
104
  example: '/provider switch production',
104
105
  modelInvocable: true,
106
+ runner: 'runtime',
105
107
  },
106
108
  ]);
107
109
 
@@ -117,6 +119,7 @@ describe('formatCommandHelpMessage — example field', () => {
117
119
  displayName: 'Help',
118
120
  description: 'Show available commands',
119
121
  modelInvocable: true,
122
+ runner: 'runtime',
120
123
  },
121
124
  ]);
122
125
 
package/src/index.ts CHANGED
@@ -79,8 +79,11 @@ export {
79
79
  type IDeviceListEntry,
80
80
  type IDevicesCommandPort,
81
81
  type IDevicesInitResult,
82
+ type IDevicesAddResult,
83
+ type IDevicesJoinResult,
82
84
  type IDevicesRecoverResult,
83
85
  type IDevicesRevokeResult,
86
+ type IDevicesMeshStatus,
84
87
  type IDevicesView,
85
88
  type TDevicesOutcome,
86
89
  type TDevicesRefusal,
@@ -120,6 +123,12 @@ export {
120
123
  } from './mcp-activation/mcp-model-notice.js';
121
124
  export type { TMCPUserAction, TMCPUserActionSurface } from './mcp-activation/mcp-model-notice.js';
122
125
  export * from './mode/index.js';
126
+ export {
127
+ createModelCommandEntry,
128
+ createModelCommandModule,
129
+ ModelCommandSource,
130
+ } from './model/model-command-module.js';
131
+ export { executeModelCommand } from './model/model-command.js';
123
132
  export {
124
133
  createSandboxCommandModule,
125
134
  executeSandboxCommand,
@@ -146,4 +155,9 @@ export * from './session/index.js';
146
155
  export * from './settings/index.js';
147
156
  export * from './skills/index.js';
148
157
  export * from './statusline/index.js';
158
+ export {
159
+ createTerminalClientCommands,
160
+ type ITerminalClientCommand,
161
+ type ITerminalClientCommandOptions,
162
+ } from './terminal-client/index.js';
149
163
  export * from './user-local/index.js';
@@ -21,13 +21,23 @@ export function createKeybindingsCommandEntry(): ICommand {
21
21
  source: 'keybindings',
22
22
  // User-only: UI preference.
23
23
  modelInvocable: false,
24
+ // Key bindings belong to the terminal the user sits at, so that terminal runs it, even when
25
+ // attached.
26
+ runner: 'client',
27
+ surfaces: ['terminal'],
24
28
  };
25
29
  }
26
30
 
27
- async function executeKeybindingsCommand(
28
- file: IKeybindingsFilePort,
31
+ // A host without a terminal (the desktop app's sidecar) still answers, so the command is never
32
+ // "unknown" there — it says where key bindings live instead.
33
+ const KEYBINDINGS_UNAVAILABLE =
34
+ 'Key bindings belong to the robota terminal, and this surface has none. Run /keybindings in the robota terminal.';
35
+
36
+ export async function executeKeybindingsCommand(
37
+ file: IKeybindingsFilePort | undefined,
29
38
  context: ICommandHostTerminalHandoff & ICommandHostWorkspace,
30
39
  ): Promise<ICommandResult> {
40
+ if (!file) return { success: false, message: KEYBINDINGS_UNAVAILABLE };
31
41
  if (!context.canHandoffTerminal()) {
32
42
  return { success: false, message: 'Keybindings editor is unavailable here.' };
33
43
  }
@@ -54,7 +64,9 @@ export class KeybindingsCommandSource implements ICommandSource {
54
64
  }
55
65
  }
56
66
 
57
- export function createKeybindingsCommandModule(file: IKeybindingsFilePort): ICommandModule {
67
+ export function createKeybindingsCommandModule(
68
+ file: IKeybindingsFilePort | undefined,
69
+ ): ICommandModule {
58
70
  const entry = createKeybindingsCommandEntry();
59
71
  const command: ISystemCommand = {
60
72
  name: entry.name,
@@ -63,6 +75,8 @@ export function createKeybindingsCommandModule(file: IKeybindingsFilePort): ICom
63
75
  requiresPermission: false,
64
76
  userInvocable: true,
65
77
  modelInvocable: false,
78
+ runner: entry.runner,
79
+ surfaces: entry.surfaces,
66
80
  lifecycle: 'inline',
67
81
  execute: (context) => executeKeybindingsCommand(file, context),
68
82
  };