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

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 (201) hide show
  1. package/README.md +11 -2
  2. package/dist/node/index.cjs +77 -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 +77 -38
  8. package/dist/node/index.js.map +1 -1
  9. package/package.json +39 -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-module.test.ts +24 -0
  87. package/src/handoff/__tests__/handoff-command.test.ts +218 -0
  88. package/src/handoff/handoff-command-module.ts +49 -0
  89. package/src/handoff/handoff-command.ts +187 -0
  90. package/src/handoff/index.ts +6 -0
  91. package/src/help/__tests__/help-command-module.test.ts +63 -40
  92. package/src/help/__tests__/help-command.test.ts +46 -37
  93. package/src/help/help-command-module.ts +2 -1
  94. package/src/help/help-command.ts +3 -3
  95. package/src/index.ts +124 -2
  96. package/src/keybindings/__tests__/keybindings-command-module.test.ts +57 -0
  97. package/src/keybindings/index.ts +6 -0
  98. package/src/keybindings/keybindings-command-module.ts +74 -0
  99. package/src/language/__tests__/language-command-module.test.ts +33 -28
  100. package/src/language/language-command-module.ts +2 -1
  101. package/src/language/language-command.ts +8 -6
  102. package/src/mcp-activation/__tests__/mcp-activation-command.test.ts +630 -0
  103. package/src/mcp-activation/__tests__/mcp-model-view.test.ts +211 -0
  104. package/src/mcp-activation/index.ts +12 -0
  105. package/src/mcp-activation/mcp-activation-command-module.ts +92 -0
  106. package/src/mcp-activation/mcp-activation-command.ts +448 -0
  107. package/src/mcp-activation/mcp-model-notice.ts +85 -0
  108. package/src/mcp-activation/mcp-model-status.ts +99 -0
  109. package/src/memory/__tests__/memory-command-module.test.ts +167 -74
  110. package/src/memory/memory-command-module.ts +18 -1
  111. package/src/memory/memory-command.ts +38 -38
  112. package/src/mode/__tests__/mode-command-module.test.ts +51 -38
  113. package/src/mode/mode-command-module.ts +2 -1
  114. package/src/mode/mode-command.ts +15 -7
  115. package/src/output-style/__tests__/output-style-command.test.ts +96 -0
  116. package/src/output-style/index.ts +6 -0
  117. package/src/output-style/output-style-command-module.ts +143 -0
  118. package/src/peers/__tests__/peers-command.test.ts +321 -0
  119. package/src/peers/index.ts +9 -0
  120. package/src/peers/peers-command-module.ts +49 -0
  121. package/src/peers/peers-command.ts +191 -0
  122. package/src/permissions/__tests__/permissions-command-module.test.ts +149 -37
  123. package/src/permissions/permissions-command-module.ts +2 -1
  124. package/src/permissions/permissions-command.ts +46 -4
  125. package/src/plan/__tests__/plan-command.test.ts +129 -0
  126. package/src/plan/index.ts +9 -0
  127. package/src/plan/plan-command-module.ts +49 -0
  128. package/src/plan/plan-command.ts +92 -0
  129. package/src/plugin/__tests__/plugin-command-module.test.ts +46 -40
  130. package/src/plugin/__tests__/plugin-host-boundary.test.ts +11 -0
  131. package/src/plugin/plugin-command-module.ts +3 -1
  132. package/src/plugin/plugin-command.ts +17 -16
  133. package/src/preset/__tests__/host-supplied-preset-registry.test.ts +98 -0
  134. package/src/preset/__tests__/preset-command-module.test.ts +102 -56
  135. package/src/preset/preset-command-module.ts +26 -8
  136. package/src/preset/preset-command.ts +85 -27
  137. package/src/provider/__tests__/org-policy.test.ts +4 -5
  138. package/src/provider/__tests__/provider-command-module.test.ts +11 -14
  139. package/src/provider/__tests__/provider-startup-host-settings.test.ts +16 -0
  140. package/src/provider/__tests__/scripted-interaction.ts +7 -5
  141. package/src/provider/provider-command-execution.ts +4 -4
  142. package/src/provider/provider-command-module.ts +2 -1
  143. package/src/provider/provider-command-profile-lifecycle.ts +4 -4
  144. package/src/provider/provider-command-profile-operations.ts +3 -3
  145. package/src/provider/provider-command-profile.ts +1 -1
  146. package/src/provider/provider-command-setup.ts +4 -4
  147. package/src/provider/provider-startup.ts +62 -35
  148. package/src/remote-control/__tests__/remote-control-command-module.test.ts +36 -0
  149. package/src/remote-control/__tests__/remote-control-command.test.ts +216 -0
  150. package/src/remote-control/index.ts +8 -0
  151. package/src/remote-control/remote-control-command-module.ts +61 -0
  152. package/src/remote-control/remote-control-command.ts +138 -0
  153. package/src/reset/__tests__/reset-command-module.test.ts +5 -5
  154. package/src/reset/reset-command-module.ts +2 -1
  155. package/src/reset/reset-command.ts +3 -4
  156. package/src/rewind/__tests__/rewind-command-module.test.ts +40 -0
  157. package/src/rewind/rewind-command-module.ts +2 -1
  158. package/src/rewind/rewind-command.ts +73 -6
  159. package/src/sandbox/__tests__/sandbox-command.test.ts +80 -0
  160. package/src/sandbox/index.ts +2 -0
  161. package/src/sandbox/sandbox-command-module.ts +56 -0
  162. package/src/sandbox/sandbox-command.ts +94 -0
  163. package/src/schedule/__tests__/loop-command-functional.test.ts +386 -0
  164. package/src/schedule/__tests__/loop-command.test.ts +360 -0
  165. package/src/schedule/__tests__/loop-jitter.test.ts +25 -0
  166. package/src/schedule/__tests__/monitor-model-permission-functional.test.ts +126 -0
  167. package/src/schedule/__tests__/schedule-command.test.ts +143 -3
  168. package/src/schedule/__tests__/schedule-redos.test.ts +81 -0
  169. package/src/schedule/index.ts +3 -0
  170. package/src/schedule/loop-command.ts +333 -0
  171. package/src/schedule/loop-jitter.ts +41 -0
  172. package/src/schedule/schedule-command-module.ts +92 -14
  173. package/src/schedule/schedule-command.ts +98 -9
  174. package/src/schedule/schedule-spec-parser.ts +5 -1
  175. package/src/session/__tests__/rename-host-persistence.test.ts +72 -0
  176. package/src/session/__tests__/session-command-module.test.ts +210 -45
  177. package/src/session/session-command-module.ts +68 -37
  178. package/src/session/session-command.ts +147 -80
  179. package/src/settings/settings-command-module.ts +3 -2
  180. package/src/shell/__tests__/resolve-shell-host.test.ts +11 -0
  181. package/src/shell/__tests__/shell-command-functional.test.ts +23 -1
  182. package/src/shell/resolve-shell.ts +2 -2
  183. package/src/shell/shell-command-module.ts +6 -5
  184. package/src/shell/shell-command.ts +9 -5
  185. package/src/skills/__tests__/skills-command-module.test.ts +69 -28
  186. package/src/skills/index.ts +1 -0
  187. package/src/skills/skills-command-module.ts +22 -5
  188. package/src/skills/skills-command.ts +14 -13
  189. package/src/statusline/__tests__/statusline-command-module.test.ts +5 -4
  190. package/src/statusline/statusline-command-module.ts +2 -1
  191. package/src/statusline/statusline-command.ts +4 -4
  192. package/src/theme/__tests__/theme-command.test.ts +204 -0
  193. package/src/theme/index.ts +12 -0
  194. package/src/theme/theme-command-module.ts +211 -0
  195. package/src/user-local/__tests__/user-local-command.test.ts +145 -110
  196. package/src/user-local/user-local-command-constants.ts +1 -1
  197. package/src/user-local/user-local-command-module.ts +6 -5
  198. package/src/user-local/user-local-command.ts +12 -6
  199. package/src/user-local/user-local-memory-command.ts +17 -7
  200. package/src/plugins/default-plugin-command-adapter.ts +0 -164
  201. package/src/plugins/default-plugin-command-source-loader.ts +0 -31
@@ -1,21 +1,60 @@
1
1
  import { selectAction } from '@robota-sdk/agent-core';
2
2
  import { applyPresetToSession } from '@robota-sdk/agent-framework';
3
- import { getPreset, listPresets, resolvePreset } from '@robota-sdk/agent-preset';
3
+ import { createPresetRegistry } from '@robota-sdk/agent-preset';
4
4
 
5
- import type { ICommandHostContext } from '@robota-sdk/agent-framework';
6
- import type { ICommandResult } from '@robota-sdk/agent-interface-transport';
5
+ import type {
6
+ ICommandHostAdapterAccess,
7
+ ICommandHostPresetApplication,
8
+ IPresetApplicationOptions,
9
+ ICommandHostSessionAccess,
10
+ ICommandHostUserInteraction,
11
+ } from '@robota-sdk/agent-framework';
12
+ import type { ICommandResult } from '@robota-sdk/agent-interface-command';
13
+ import type { IPresetSummary } from '@robota-sdk/agent-preset';
7
14
 
8
- /** Default active preset id reported when the runtime has no recorded active preset. */
9
- const DEFAULT_ACTIVE_PRESET_ID = 'default';
15
+ /**
16
+ * ARCH-009 — the registry `/preset` discovers through.
17
+ *
18
+ * The host supplies the instance it resolved with through the ADAPTER bag — the same path
19
+ * `/permission-mode` and `/plugin` already take to their host capabilities. A host that supplies none
20
+ * gets a fresh built-ins registry. That order is the point: while discovery read a module-global
21
+ * registry directly, two products in one process shared one mutable list, so ARCH-008's per-instance
22
+ * resolution was only half-true for an embedded host. A command runs with an `ICommandHostContext`
23
+ * and nothing else, so the context is the only path from here to the instance the shell built.
24
+ *
25
+ * The fallback is deliberate rather than transitional, and it is no longer process state: a host that
26
+ * never loads external presets has nothing to hand over, and `/preset` should still list the
27
+ * built-ins. `createPresetRegistry()` with no argument is exactly that, constructed per call.
28
+ */
29
+ /**
30
+ * What `/preset` actually needs from a registry — three members, no assertion.
31
+ *
32
+ * Narrower than `agent-preset`'s `IPresetRegistry` on purpose. Review of ARCH-009 reported the first
33
+ * version reaching that contract with three casts, one of which (`listPresets()`) widened optional
34
+ * `title`/`description` into required ones and would have printed `id — undefined: undefined` for a
35
+ * host that typechecked. The fix is not a better cast: it is asking for the shape the command uses,
36
+ * so both the built-in registry and a host adapter satisfy it STRUCTURALLY and nothing is asserted.
37
+ *
38
+ * `getPreset` returns `unknown` because presence is the whole question here.
39
+ */
40
+ interface IPresetDiscovery {
41
+ listPresets(): readonly IPresetSummary[];
42
+ getPreset(id: string): unknown;
43
+ resolvePreset(id: string, context?: unknown): IPresetApplicationOptions;
44
+ }
45
+
46
+ function presetRegistry(context: ICommandHostAdapterAccess): IPresetDiscovery {
47
+ return context.getCommandHostAdapters?.().presetRegistry ?? createPresetRegistry();
48
+ }
10
49
 
11
50
  /** Read the active preset id from the session, defaulting when the optional seam is absent. */
12
- function readActivePresetId(context: ICommandHostContext): string {
13
- return context.getSession().getActivePresetId?.() ?? DEFAULT_ACTIVE_PRESET_ID;
51
+ function readActivePresetId(context: ICommandHostSessionAccess): string {
52
+ return context.getSession().getActivePresetId();
14
53
  }
15
54
 
16
55
  /** Build the `/preset` listing: one line per preset, marking the active one with a `*` prefix. */
17
- function formatPresetList(active: string): string {
18
- const lines = listPresets().map((preset) => {
56
+ function formatPresetList(active: string, registry: IPresetDiscovery): string {
57
+ const lines = registry.listPresets().map((preset) => {
19
58
  const marker = preset.id === active ? '* ' : ' ';
20
59
  return `${marker}${preset.id} — ${preset.title}: ${preset.description}`;
21
60
  });
@@ -23,20 +62,24 @@ function formatPresetList(active: string): string {
23
62
  }
24
63
 
25
64
  /** Build the rejection message for an unknown preset id, listing the valid ids. */
26
- function formatUnknownPresetMessage(id: string): string {
27
- const ids = listPresets()
65
+ function formatUnknownPresetMessage(id: string, registry: IPresetDiscovery): string {
66
+ const ids = registry
67
+ .listPresets()
28
68
  .map((preset) => preset.id)
29
69
  .join(', ');
30
70
  return `Unknown preset: ${id}. Available: ${ids}`;
31
71
  }
32
72
 
33
73
  /** The `/preset` (or `/preset list`) listing result. */
34
- function presetListResult(context: ICommandHostContext): ICommandResult {
74
+ function presetListResult(
75
+ context: ICommandHostSessionAccess,
76
+ registry: IPresetDiscovery,
77
+ ): ICommandResult {
35
78
  const active = readActivePresetId(context);
36
79
  return {
37
- message: formatPresetList(active),
80
+ message: formatPresetList(active, registry),
38
81
  success: true,
39
- data: { presets: listPresets(), active },
82
+ data: { presets: registry.listPresets(), active },
40
83
  };
41
84
  }
42
85
 
@@ -44,10 +87,13 @@ function presetListResult(context: ICommandHostContext): ICommandResult {
44
87
  * Ask the user to pick a preset (CMD-004 inline ask). Returns the chosen id, or `undefined` when no
45
88
  * interactive renderer is attached or the user cancelled — the caller then shows the preset list.
46
89
  */
47
- async function resolvePresetViaAsk(context: ICommandHostContext): Promise<string | undefined> {
48
- const ui = context.getUserInteraction?.();
90
+ async function resolvePresetViaAsk(
91
+ context: ICommandHostUserInteraction,
92
+ registry: IPresetDiscovery,
93
+ ): Promise<string | undefined> {
94
+ const ui = context.getUserInteraction();
49
95
  if (!ui) return undefined;
50
- const options = listPresets().map((preset) => ({
96
+ const options = registry.listPresets().map((preset) => ({
51
97
  value: preset.id,
52
98
  label: preset.id,
53
99
  description: preset.description,
@@ -57,34 +103,46 @@ async function resolvePresetViaAsk(context: ICommandHostContext): Promise<string
57
103
  }
58
104
 
59
105
  export async function executePresetCommand(
60
- context: ICommandHostContext,
106
+ context: ICommandHostPresetApplication &
107
+ ICommandHostSessionAccess &
108
+ ICommandHostUserInteraction &
109
+ ICommandHostAdapterAccess,
61
110
  args: string,
62
111
  ): Promise<ICommandResult> {
112
+ // ARCH-009: resolved ONCE per invocation, so listing, lookup and resolution cannot disagree — a
113
+ // host that swapped registries mid-command would otherwise report one set and apply another.
114
+ const registry = presetRegistry(context);
63
115
  let id: string | undefined = args.trim().split(/\s+/)[0];
64
116
 
65
117
  if (id === 'list') {
66
- return presetListResult(context);
118
+ return presetListResult(context, registry);
67
119
  }
68
120
 
69
121
  if (id === undefined || id.length === 0) {
70
- id = await resolvePresetViaAsk(context);
122
+ id = await resolvePresetViaAsk(context, registry);
71
123
  if (id === undefined) {
72
- return presetListResult(context);
124
+ return presetListResult(context, registry);
73
125
  }
74
126
  }
75
127
 
76
- if (getPreset(id) === undefined) {
128
+ if (registry.getPreset(id) === undefined) {
77
129
  return {
78
- message: formatUnknownPresetMessage(id),
130
+ message: formatUnknownPresetMessage(id, registry),
79
131
  success: false,
80
132
  };
81
133
  }
82
134
 
83
- const resolved = resolvePreset(id);
84
- await applyPresetToSession(context, id, resolved);
135
+ const resolved = registry.resolvePreset(id);
136
+ const result = await applyPresetToSession(context, id, resolved);
137
+ // INFRA-032: surface any preset command-module name that matched no live module as a non-fatal
138
+ // notice — an in-session `/preset` switch is no longer silent about a short form / typo.
139
+ const noticeLines = result.unknownCommandModules.map(
140
+ ({ name, kind }) =>
141
+ `Preset command-module "${name}" (${kind}) matched no module — expected the agent-command-* form; ignored.`,
142
+ );
85
143
  return {
86
- message: `Switched to preset: ${id}`,
144
+ message: [`Switched to preset: ${id}`, ...noticeLines].join('\n'),
87
145
  success: true,
88
- data: { preset: id },
146
+ data: { preset: id, unknownCommandModules: result.unknownCommandModules },
89
147
  };
90
148
  }
@@ -9,6 +9,7 @@ import type {
9
9
  } from '@robota-sdk/agent-framework';
10
10
  import { createProviderCommandModule } from '../provider-command-module.js';
11
11
  import { scriptedContext } from './scripted-interaction.js';
12
+ import { createTestCommandHost } from '@robota-sdk/agent-framework/testing';
12
13
 
13
14
  const providerDefinitions: readonly IProviderDefinition[] = [
14
15
  {
@@ -64,7 +65,7 @@ function createExecutor(
64
65
  }
65
66
 
66
67
  /** Context with no interactive renderer attached (headless/automation). */
67
- const headlessContext = {} as ICommandHostContext;
68
+ const headlessContext = createTestCommandHost();
68
69
 
69
70
  describe('org policy enforcement in provider commands', () => {
70
71
  afterEach(() => {
@@ -92,7 +93,7 @@ describe('org policy enforcement in provider commands', () => {
92
93
  expect(result?.success).toBe(false);
93
94
  expect(result?.message).toContain('"openai" is not allowed');
94
95
  expect(result?.message).toContain('admin@example.com');
95
- expect(result?.effects).toBeUndefined();
96
+ expect(result?.hostActions).toBeUndefined();
96
97
  });
97
98
 
98
99
  it('allows /provider switch when target is in allowedProviders', async () => {
@@ -112,9 +113,7 @@ describe('org policy enforcement in provider commands', () => {
112
113
  );
113
114
 
114
115
  expect(result?.success).toBe(true);
115
- expect(result?.effects).toEqual([
116
- { type: 'provider-hot-swap-requested', profileName: 'openai' },
117
- ]);
116
+ expect(result?.hostActions).toEqual([{ type: 'provider-hot-swap', profileName: 'openai' }]);
118
117
  });
119
118
 
120
119
  it('rejects plaintext API key during edit when requireApiKeyFromEnv is true', async () => {
@@ -8,6 +8,7 @@ import type {
8
8
  } from '@robota-sdk/agent-framework';
9
9
  import { createProviderCommandModule } from '../provider-command-module.js';
10
10
  import { scriptedContext } from './scripted-interaction.js';
11
+ import { createTestCommandHost } from '@robota-sdk/agent-framework/testing';
11
12
 
12
13
  const providerDefinitions: readonly IProviderDefinition[] = [
13
14
  {
@@ -95,7 +96,7 @@ function createExecutor(adapter: IProviderCommandSettingsAdapter): SystemCommand
95
96
  }
96
97
 
97
98
  /** Context with no interactive renderer attached (headless/automation). */
98
- const headlessContext = {} as ICommandHostContext;
99
+ const headlessContext = createTestCommandHost();
99
100
 
100
101
  describe('createProviderCommandModule', () => {
101
102
  it('contributes /provider metadata and executable command from the provider package', () => {
@@ -190,9 +191,7 @@ describe('createProviderCommandModule', () => {
190
191
  'Switched to openai (supergemma4-26b-uncensored-v2). History preserved.',
191
192
  );
192
193
  expect(readTarget().currentProvider).toBe('openai');
193
- expect(result?.effects).toEqual([
194
- { type: 'provider-hot-swap-requested', profileName: 'openai' },
195
- ]);
194
+ expect(result?.hostActions).toEqual([{ type: 'provider-hot-swap', profileName: 'openai' }]);
196
195
  });
197
196
 
198
197
  it('switches from the provider profile action menu immediately without confirmation', async () => {
@@ -217,9 +216,7 @@ describe('createProviderCommandModule', () => {
217
216
  'Switched to openai (supergemma4-26b-uncensored-v2). History preserved.',
218
217
  );
219
218
  expect(readTarget().currentProvider).toBe('openai');
220
- expect(result?.effects).toEqual([
221
- { type: 'provider-hot-swap-requested', profileName: 'openai' },
222
- ]);
219
+ expect(result?.hostActions).toEqual([{ type: 'provider-hot-swap', profileName: 'openai' }]);
223
220
  });
224
221
 
225
222
  it('edits a provider profile through provider-owned setup metadata without exposing secrets', async () => {
@@ -261,8 +258,8 @@ describe('createProviderCommandModule', () => {
261
258
  },
262
259
  });
263
260
  expect(completed?.message).toBe('Provider anthropic updated. Switching...');
264
- expect(completed?.effects).toEqual([
265
- { type: 'provider-hot-swap-requested', profileName: 'anthropic' },
261
+ expect(completed?.hostActions).toEqual([
262
+ { type: 'provider-hot-swap', profileName: 'anthropic' },
266
263
  ]);
267
264
  });
268
265
 
@@ -296,7 +293,7 @@ describe('createProviderCommandModule', () => {
296
293
  },
297
294
  },
298
295
  });
299
- expect(completed?.effects).toBeUndefined();
296
+ expect(completed?.hostActions).toBeUndefined();
300
297
  });
301
298
 
302
299
  it('deletes inactive provider profiles after confirmation', async () => {
@@ -371,9 +368,9 @@ describe('createProviderCommandModule', () => {
371
368
  openai: { type: 'openai', model: 'supergemma4-26b-uncensored-v2' },
372
369
  },
373
370
  });
374
- expect(completed?.effects).toEqual([
371
+ expect(completed?.hostActions).toEqual([
375
372
  {
376
- type: 'session-restart-requested',
373
+ type: 'session-restart',
377
374
  reason: 'other',
378
375
  message: 'Provider delete restart',
379
376
  },
@@ -437,9 +434,9 @@ describe('createProviderCommandModule', () => {
437
434
  },
438
435
  },
439
436
  });
440
- expect(completed?.effects).toEqual([
437
+ expect(completed?.hostActions).toEqual([
441
438
  {
442
- type: 'session-restart-requested',
439
+ type: 'session-restart',
443
440
  reason: 'other',
444
441
  message: 'Provider setup restart',
445
442
  },
@@ -0,0 +1,16 @@
1
+ import { expect, it } from 'vitest';
2
+
3
+ import { ensureProviderConfig } from '../provider-startup.js';
4
+
5
+ it('requires host settings access before provider startup', async () => {
6
+ await expect(
7
+ ensureProviderConfig(
8
+ process.cwd(),
9
+ {} as never,
10
+ async () => '',
11
+ { writeLine: () => undefined, writeError: () => undefined } as never,
12
+ [],
13
+ { formatError: () => 'Provider is not configured.' },
14
+ ),
15
+ ).rejects.toThrow('Provider startup requires host settings sources and stores.');
16
+ });
@@ -1,5 +1,6 @@
1
+ import { createTestCommandHost } from '@robota-sdk/agent-framework/testing';
2
+
1
3
  import type { IActionRequest, IUserInteraction, TActionResponse } from '@robota-sdk/agent-core';
2
- import type { ICommandHostContext } from '@robota-sdk/agent-framework';
3
4
 
4
5
  /**
5
6
  * Build a command host context whose `getUserInteraction().ask` replays a scripted sequence of
@@ -8,7 +9,7 @@ import type { ICommandHostContext } from '@robota-sdk/agent-framework';
8
9
  * returns `{ type: 'cancelled' }`, mirroring a user dismissing the prompt.
9
10
  */
10
11
  export function scriptedContext(answers: readonly TActionResponse[]): {
11
- context: ICommandHostContext;
12
+ context: ReturnType<typeof createTestCommandHost>;
12
13
  requests: IActionRequest[];
13
14
  } {
14
15
  const requests: IActionRequest[] = [];
@@ -21,8 +22,9 @@ export function scriptedContext(answers: readonly TActionResponse[]): {
21
22
  return Promise.resolve(answer ?? { type: 'cancelled' });
22
23
  },
23
24
  };
24
- const context = {
25
- getUserInteraction: () => ui,
26
- } as Partial<ICommandHostContext> as ICommandHostContext;
25
+ // ARCH-029: the double answers "no capability of that kind" for everything else, so this fixture
26
+ // states exactly the one capability it scripts. The double cast it replaced —
27
+ // `as Partial<…> as ICommandHostContext` — asserted conformance twice over one member.
28
+ const context = createTestCommandHost({ overrides: { getUserInteraction: () => ui } });
27
29
  return { context, requests };
28
30
  }
@@ -12,18 +12,18 @@ import { formatProviderSetupChoiceLabel } from './provider-setup-flow.js';
12
12
 
13
13
  import type { IUserInteraction } from '@robota-sdk/agent-core';
14
14
  import type {
15
- ICommandHostContext,
15
+ ICommandHostUserInteraction,
16
16
  IProviderCommandModuleOptions,
17
17
  IProviderProfileSettings,
18
18
  } from '@robota-sdk/agent-framework';
19
- import type { ICommandResult } from '@robota-sdk/agent-interface-transport';
19
+ import type { ICommandResult } from '@robota-sdk/agent-interface-command';
20
20
 
21
21
  export async function executeProviderCommand(
22
- context: ICommandHostContext,
22
+ context: ICommandHostUserInteraction,
23
23
  args: string,
24
24
  options: IProviderCommandModuleOptions,
25
25
  ): Promise<ICommandResult> {
26
- const ui = context.getUserInteraction?.();
26
+ const ui = context.getUserInteraction();
27
27
  const settings = options.settings.readMergedSettings();
28
28
  const trimmedArgs = args.trim();
29
29
  if (trimmedArgs.length === 0) {
@@ -6,7 +6,7 @@ import type {
6
6
  IProviderCommandSettingsAdapter,
7
7
  ISystemCommand as TSystemCommand,
8
8
  } from '@robota-sdk/agent-framework';
9
- import type { ICommand, ICommandSource } from '@robota-sdk/agent-interface-transport';
9
+ import type { ICommand, ICommandSource } from '@robota-sdk/agent-interface-command';
10
10
  export type { IProviderCommandModuleOptions, IProviderCommandSettingsAdapter };
11
11
 
12
12
  function buildProviderSubcommands(): ICommand[] {
@@ -25,6 +25,7 @@ export function createProviderCommandEntry(): ICommand {
25
25
  displayName: 'Provider Setup',
26
26
  description: 'Manage provider profiles',
27
27
  source: 'provider',
28
+ // User-only: provider profiles hold account credentials; a credential action.
28
29
  modelInvocable: false,
29
30
  argumentHint: 'current | list | switch <profile> | add [type] | test [profile]',
30
31
  subcommands: buildProviderSubcommands(),
@@ -9,11 +9,11 @@ import { formatProviderChoiceLabel } from './provider-command-profile-operations
9
9
 
10
10
  import type { IUserInteraction } from '@robota-sdk/agent-core';
11
11
  import type { IProviderCommandModuleOptions } from '@robota-sdk/agent-framework';
12
- import type { ICommandResult } from '@robota-sdk/agent-interface-transport';
12
+ import type { ICommandResult } from '@robota-sdk/agent-interface-command';
13
13
 
14
14
  const MAX_DUPLICATE_PROFILE_SUFFIX = 1000;
15
- const PROVIDER_RESTART_EFFECT = {
16
- type: 'session-restart-requested',
15
+ const PROVIDER_RESTART_ACTION = {
16
+ type: 'session-restart',
17
17
  reason: 'other',
18
18
  } as const;
19
19
 
@@ -166,7 +166,7 @@ function completeActiveProviderDelete(
166
166
  return {
167
167
  message: `Provider profile deleted: ${profileName}. Restarting with ${replacementName}...`,
168
168
  success: true,
169
- effects: [{ ...PROVIDER_RESTART_EFFECT, message: 'Provider delete restart' }],
169
+ hostActions: [{ ...PROVIDER_RESTART_ACTION, message: 'Provider delete restart' }],
170
170
  };
171
171
  }
172
172
 
@@ -15,7 +15,7 @@ import type {
15
15
  IProviderProfileSettings,
16
16
  IProviderSetupInput,
17
17
  } from '@robota-sdk/agent-framework';
18
- import type { ICommandResult } from '@robota-sdk/agent-interface-transport';
18
+ import type { ICommandResult } from '@robota-sdk/agent-interface-command';
19
19
 
20
20
  export function formatProviderChoiceLabel(
21
21
  name: string,
@@ -62,7 +62,7 @@ export function buildProviderSwitch(
62
62
  return {
63
63
  message: `Switched to ${profileName} (${modelLabel}). History preserved.`,
64
64
  success: true,
65
- effects: [{ type: 'provider-hot-swap-requested', profileName }],
65
+ hostActions: [{ type: 'provider-hot-swap', profileName }],
66
66
  };
67
67
  }
68
68
 
@@ -146,6 +146,6 @@ function completeProviderEdit(
146
146
  ? `Provider ${profileName} updated. Switching...`
147
147
  : `Provider ${profileName} updated.`,
148
148
  success: true,
149
- ...(isCurrent ? { effects: [{ type: 'provider-hot-swap-requested', profileName }] } : {}),
149
+ ...(isCurrent ? { hostActions: [{ type: 'provider-hot-swap' as const, profileName }] } : {}),
150
150
  };
151
151
  }
@@ -16,7 +16,7 @@ import type {
16
16
  IProviderCommandModuleOptions,
17
17
  IProviderProfileSettings,
18
18
  } from '@robota-sdk/agent-framework';
19
- import type { ICommandResult } from '@robota-sdk/agent-interface-transport';
19
+ import type { ICommandResult } from '@robota-sdk/agent-interface-command';
20
20
 
21
21
  const ACTION_SWITCH = 'switch';
22
22
  const ACTION_EDIT = 'edit';
@@ -14,10 +14,10 @@ import type {
14
14
  IProviderCommandModuleOptions,
15
15
  IProviderSetupInput,
16
16
  } from '@robota-sdk/agent-framework';
17
- import type { ICommandResult } from '@robota-sdk/agent-interface-transport';
17
+ import type { ICommandResult } from '@robota-sdk/agent-interface-command';
18
18
 
19
- const PROVIDER_RESTART_EFFECT = {
20
- type: 'session-restart-requested',
19
+ const PROVIDER_RESTART_ACTION = {
20
+ type: 'session-restart',
21
21
  reason: 'other',
22
22
  } as const;
23
23
 
@@ -108,6 +108,6 @@ function completeProviderSetup(
108
108
  return {
109
109
  message: `Provider ${input.profile} configured. Restarting...`,
110
110
  success: true,
111
- effects: [{ ...PROVIDER_RESTART_EFFECT, message: 'Provider setup restart' }],
111
+ hostActions: [{ ...PROVIDER_RESTART_ACTION, message: 'Provider setup restart' }],
112
112
  };
113
113
  }
@@ -1,15 +1,11 @@
1
- import { join } from 'node:path';
2
-
3
1
  import {
4
2
  checkSettingsDocument,
5
3
  ProviderConfigError,
6
4
  readMergedProviderSettings,
7
- readSettings,
8
- writeSettings,
9
5
  resolveEnvDefaultProvider,
10
- resolveSettingsPathForScope,
11
- getProviderSettingsPaths,
12
6
  applyProviderConfiguration,
7
+ resolveProviderSettingsWriteTarget,
8
+ WorkspaceAuthorityRequiredError,
13
9
  } from '@robota-sdk/agent-framework';
14
10
 
15
11
  import { runOnboardingBranch } from './provider-onboarding.js';
@@ -22,11 +18,17 @@ import {
22
18
 
23
19
  import type { IProviderDefinition } from '@robota-sdk/agent-core';
24
20
  import type { ITerminalOutput } from '@robota-sdk/agent-core';
25
- import type { TSettingsScope } from '@robota-sdk/agent-framework';
21
+ import type {
22
+ ISettingsDocumentStore,
23
+ TSettingsScope,
24
+ TSettingsSource,
25
+ } from '@robota-sdk/agent-framework';
26
26
 
27
27
  export interface IProviderStartupContext {
28
28
  provider?: string;
29
29
  settingsScope?: TSettingsScope;
30
+ settingsSources: readonly TSettingsSource[];
31
+ settingsStores: readonly ISettingsDocumentStore[];
30
32
  }
31
33
 
32
34
  export interface IEnsureProviderConfigOptions {
@@ -44,8 +46,11 @@ export async function runProviderStartupSetup(
44
46
  providerDefinitions: readonly IProviderDefinition[],
45
47
  ): Promise<void> {
46
48
  const onboarding = await runOnboardingBranch(promptInput, terminal);
47
- const existingProfileNames = Object.keys(readMergedProviderSettings(cwd).providers ?? {});
48
- const settingsPath = resolveSettingsPathForScope(cwd, ctx.settingsScope);
49
+ const access = resolveSettingsAccess(ctx);
50
+ const existingProfileNames = Object.keys(
51
+ readMergedProviderSettings(access.sources).providers ?? {},
52
+ );
53
+ const settingsStore = selectSettingsStore(access.stores, ctx.settingsScope);
49
54
 
50
55
  let type: string;
51
56
  if (onboarding.preselectedType !== undefined) {
@@ -60,14 +65,14 @@ export async function runProviderStartupSetup(
60
65
  const input = await runProviderSetupPromptFlow(type, promptInput, providerDefinitions, {
61
66
  existingProfileNames,
62
67
  });
63
- applyProviderConfiguration(settingsPath, input, { providerDefinitions });
68
+ applyProviderConfiguration(settingsStore, input, { providerDefinitions });
64
69
  const language = await promptInput(' Response language (ko/en/ja/zh, default: en): ');
65
70
  if (language) {
66
- const settings = readSettings(settingsPath);
71
+ const settings = settingsStore.read();
67
72
  settings.language = language;
68
- writeSettings(settingsPath, settings);
73
+ settingsStore.write(settings);
69
74
  }
70
- terminal.writeLine(`\n Config saved to ${settingsPath}\n`);
75
+ terminal.writeLine(`\n Config saved to ${settingsStore.displayName}\n`);
71
76
  }
72
77
 
73
78
  export async function ensureProviderConfig(
@@ -78,7 +83,8 @@ export async function ensureProviderConfig(
78
83
  providerDefinitions: readonly IProviderDefinition[],
79
84
  options: IEnsureProviderConfigOptions,
80
85
  ): Promise<void> {
81
- const merged = readMergedProviderSettings(cwd);
86
+ const access = resolveSettingsAccess(ctx);
87
+ const merged = readMergedProviderSettings(access.sources);
82
88
  const selectedSettings =
83
89
  ctx.provider !== undefined ? { ...merged, currentProvider: ctx.provider } : merged;
84
90
  if (checkSettingsDocument(selectedSettings, providerDefinitions) === 'valid') {
@@ -98,12 +104,12 @@ export async function ensureProviderConfig(
98
104
  }
99
105
  await runProviderStartupSetup(
100
106
  cwd,
101
- selectStartupContext(cwd, ctx),
107
+ selectStartupContext(ctx, access.stores),
102
108
  promptInput,
103
109
  terminal,
104
110
  providerDefinitions,
105
111
  );
106
- const updated = readMergedProviderSettings(cwd);
112
+ const updated = readMergedProviderSettings(access.sources);
107
113
  const updatedSettings =
108
114
  ctx.provider !== undefined ? { ...updated, currentProvider: ctx.provider } : updated;
109
115
  if (checkSettingsDocument(updatedSettings, providerDefinitions) !== 'valid') {
@@ -111,30 +117,51 @@ export async function ensureProviderConfig(
111
117
  }
112
118
  }
113
119
 
114
- function selectStartupContext(cwd: string, ctx: IProviderStartupContext): IProviderStartupContext {
115
- if (ctx.settingsScope !== undefined || ctx.provider !== undefined) return ctx;
116
- const currentProviderPath = findHighestPriorityCurrentProviderPath(getProviderSettingsPaths(cwd));
117
- if (currentProviderPath === undefined) return ctx;
118
- const projectSettingsPath = join(cwd, '.robota', 'settings.json');
119
- const projectLocalSettingsPath = join(cwd, '.robota', 'settings.local.json');
120
- if (
121
- currentProviderPath === projectSettingsPath ||
122
- currentProviderPath === projectLocalSettingsPath
123
- ) {
124
- return { ...ctx, settingsScope: 'project-local' };
120
+ function resolveSettingsAccess(ctx: IProviderStartupContext): {
121
+ sources: readonly TSettingsSource[];
122
+ stores: readonly ISettingsDocumentStore[];
123
+ } {
124
+ if (ctx.settingsSources === undefined || ctx.settingsStores === undefined) {
125
+ throw new Error('Provider startup requires host settings sources and stores.');
125
126
  }
127
+ return {
128
+ sources: ctx.settingsSources,
129
+ stores: ctx.settingsStores,
130
+ };
131
+ }
132
+
133
+ function selectSettingsStore(
134
+ stores: readonly ISettingsDocumentStore[],
135
+ scope: TSettingsScope | undefined,
136
+ ): ISettingsDocumentStore {
137
+ if (scope === undefined) return resolveProviderSettingsWriteTarget(stores);
138
+ const targetScope = scope === 'user' ? 'user' : 'project-local';
139
+ const store = stores.findLast((candidate) => candidate.scope === targetScope);
140
+ if (store !== undefined) return store;
141
+ throw new WorkspaceAuthorityRequiredError(
142
+ `No authorized ${targetScope} settings store is available.`,
143
+ );
144
+ }
145
+
146
+ function selectStartupContext(
147
+ ctx: IProviderStartupContext,
148
+ stores: readonly ISettingsDocumentStore[],
149
+ ): IProviderStartupContext {
150
+ if (ctx.settingsScope !== undefined || ctx.provider !== undefined) return ctx;
151
+ const currentProviderStore = findHighestPriorityCurrentProviderStore(stores);
152
+ if (currentProviderStore?.kind === 'project') return { ...ctx, settingsScope: 'project-local' };
126
153
  return ctx;
127
154
  }
128
155
 
129
- function findHighestPriorityCurrentProviderPath(
130
- settingsPaths: readonly string[],
131
- ): string | undefined {
132
- for (let index = settingsPaths.length - 1; index >= 0; index -= 1) {
133
- const settingsPath = settingsPaths[index];
134
- if (settingsPath === undefined) continue;
135
- const settings = readSettings(settingsPath);
156
+ function findHighestPriorityCurrentProviderStore(
157
+ stores: readonly ISettingsDocumentStore[],
158
+ ): ISettingsDocumentStore | undefined {
159
+ for (let index = stores.length - 1; index >= 0; index -= 1) {
160
+ const store = stores[index];
161
+ if (store === undefined) continue;
162
+ const settings = store.read();
136
163
  if (typeof settings.currentProvider === 'string') {
137
- return settingsPath;
164
+ return store;
138
165
  }
139
166
  }
140
167
  return undefined;
@@ -0,0 +1,36 @@
1
+ import { describe, expect, it } from 'vitest';
2
+
3
+ import { createTestCommandHost } from '@robota-sdk/agent-framework/testing';
4
+
5
+ import { createRemoteControlCommandModule } from '../remote-control-command-module.js';
6
+
7
+ describe('createRemoteControlCommandModule metadata', () => {
8
+ it('keeps palette metadata aligned with the executable command and status as the safe default', async () => {
9
+ const module = createRemoteControlCommandModule();
10
+ const palette = module.commandSources?.[0]?.getCommands()[0];
11
+ const executable = module.systemCommands?.[0];
12
+
13
+ expect(palette).toBeDefined();
14
+ expect(executable).toBeDefined();
15
+ expect(palette?.argumentHint).toBe('[enable|stop|status|devices|revoke <device-id>]');
16
+ expect(palette?.description).toBe(executable?.description);
17
+ expect(palette?.modelInvocable).toBe(executable?.modelInvocable);
18
+ expect(palette?.argumentHint).toBe(executable?.argumentHint);
19
+ expect(palette?.userInvocable).toBe(executable?.userInvocable);
20
+ expect(palette?.subcommands?.map(({ name }) => name)).toEqual([
21
+ 'status',
22
+ 'devices',
23
+ 'enable',
24
+ 'stop',
25
+ 'revoke',
26
+ ]);
27
+ expect(palette?.subcommands).toEqual(executable?.subcommands);
28
+ expect(executable?.requiresPermission).toBe(false);
29
+ expect(executable?.modelInvocable).toBe(false);
30
+ expect(executable?.userInvocable).toBe(true);
31
+
32
+ const result = await executable!.execute(createTestCommandHost(), 'status');
33
+ expect(result.message).toContain('not available');
34
+ expect(result.hostActions).toBeUndefined();
35
+ });
36
+ });