@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
@@ -142,6 +142,84 @@ describe('what the operator is told', () => {
142
142
  });
143
143
  });
144
144
 
145
+ describe('linked devices of the device mesh', () => {
146
+ const DEVICE = 'D'.repeat(43);
147
+
148
+ function hostWithDevices(
149
+ peers: readonly TPeerSummary[],
150
+ devices: readonly { deviceId: string; name?: string; locality: 'same-host' | 'another-host' }[],
151
+ ): ICommandHostAdapterAccess {
152
+ return {
153
+ getCommandHostAdapters: () => ({
154
+ localPeers: { list: () => peers, ownSessionId: () => OWN, listDevices: () => devices },
155
+ }),
156
+ } as ICommandHostAdapterAccess;
157
+ }
158
+
159
+ it('lists a linked device beside the sessions on this host, addressable by its id', async () => {
160
+ const result = await executePeersCommand(
161
+ hostWithDevices(
162
+ [
163
+ { sessionId: OWN, liveness: 'alive' },
164
+ { sessionId: 'session-other', liveness: 'alive' },
165
+ ],
166
+ [{ deviceId: DEVICE, name: 'desktop', locality: 'another-host' }],
167
+ ),
168
+ );
169
+ expect(result.success).toBe(true);
170
+ expect(result.message).toContain('session-other');
171
+ expect(result.message).toMatch(new RegExp(`${DEVICE}\\s+desktop\\s+on another machine`));
172
+ expect(result.message).toContain('/peers send <session-or-device-id>');
173
+ });
174
+
175
+ it('lists a linked device even when this is the only session on this host', async () => {
176
+ const result = await executePeersCommand(
177
+ hostWithDevices(
178
+ [{ sessionId: OWN, liveness: 'alive' }],
179
+ [{ deviceId: DEVICE, locality: 'same-host' }],
180
+ ),
181
+ );
182
+ expect(result.message).not.toContain('No other live session');
183
+ expect(result.message).toMatch(new RegExp(`${DEVICE}\\s+on this machine`));
184
+ });
185
+ });
186
+
187
+ describe('when local discovery is off but the device mesh is not', () => {
188
+ const DEVICE = 'D'.repeat(43);
189
+
190
+ function hostWithoutDiscovery(
191
+ devices: readonly { deviceId: string; name?: string; locality: 'same-host' | 'another-host' }[],
192
+ ): ICommandHostAdapterAccess {
193
+ return {
194
+ getCommandHostAdapters: () => ({
195
+ localPeers: {
196
+ list: () => [],
197
+ ownSessionId: () => OWN,
198
+ listDevices: () => devices,
199
+ localDiscoveryOff: 'the rendezvous directory was not admitted',
200
+ },
201
+ }),
202
+ } as ICommandHostAdapterAccess;
203
+ }
204
+
205
+ it('lists the linked devices and says why sessions on this host are not listed', async () => {
206
+ const result = await executePeersCommand(
207
+ hostWithoutDiscovery([{ deviceId: DEVICE, name: 'desktop', locality: 'another-host' }]),
208
+ );
209
+ expect(result.success).toBe(true);
210
+ expect(result.message).toMatch(new RegExp(`${DEVICE}\\s+desktop\\s+on another machine`));
211
+ expect(result.message).toContain('not admitted');
212
+ expect(result.message).not.toContain('Live sessions');
213
+ expect(result.message).toContain('/peers send <device-id>');
214
+ });
215
+
216
+ it('with no device linked, says discovery is off rather than that nobody is there', async () => {
217
+ const result = await executePeersCommand(hostWithoutDiscovery([]));
218
+ expect(result.message).toContain('not admitted');
219
+ expect(result.message).not.toContain('No other live session');
220
+ });
221
+ });
222
+
145
223
  describe('when the host wires no discovery', () => {
146
224
  it('says the feature is unavailable rather than reporting no peers', async () => {
147
225
  // The two are different facts and the difference matters: "nobody is there" invites the operator
@@ -8,7 +8,7 @@ export function createPeersCommandEntry(): ICommand {
8
8
  name: 'peers',
9
9
  displayName: 'Peers',
10
10
  description:
11
- 'List the other live sessions on this host, send one a message, or send one a copy of a file',
11
+ "List the other live Robota sessions on this host and the user's other devices linked over the device mesh, send one a message, or send one a copy of a file. Returns the sessions and devices with the ids to address them by (linked devices even when local discovery is off and sessions on this host cannot be listed), or how a send ended. User-only: the model cannot run it; it answers a peer's message with its reply tool, and when the user wants to reach another session or device, suggest they run `/peers`.",
12
12
  source: 'peers',
13
13
  // The model does not enumerate the operator's other sessions. Discovery is an operator-facing
14
14
  // view of who is at the machine, which is a fact about the person and not about the task.
@@ -33,6 +33,16 @@ function describe(peer: TPeerSummary, ownSessionId: string): string {
33
33
  return ` ${peer.sessionId}${peer.name ? ` ${peer.name}` : ''} status ${status}${self ? '' : workspace(peer)}${liveness}${self}`;
34
34
  }
35
35
 
36
+ type TDeviceSummary = ReturnType<
37
+ NonNullable<NonNullable<ICommandHostAdapters['localPeers']>['listDevices']>
38
+ >[number];
39
+
40
+ /** A linked device: its full id, since that is what `send` takes, and where it runs. */
41
+ function describeDevice(device: TDeviceSummary): string {
42
+ const where = device.locality === 'same-host' ? 'on this machine' : 'on another machine';
43
+ return ` ${device.deviceId}${device.name ? ` ${device.name}` : ''} ${where}`;
44
+ }
45
+
36
46
  /** The relation this session verified. A claim it could not confirm is named as such, not shown. */
37
47
  function workspace(peer: TPeerSummary): string {
38
48
  if (peer.workspaceClaim === 'mismatched') return ' workspace claim mismatched, not believed';
@@ -165,6 +175,21 @@ export async function executePeersCommand(
165
175
  const sendVerb = /^send(?=\s|$)/.exec(trimmed);
166
176
  if (sendVerb !== null) return executeSend(adapter, trimmed.slice(sendVerb[0].length));
167
177
 
178
+ const devices = adapter.listDevices?.() ?? [];
179
+ const discoveryOff = adapter.localDiscoveryOff;
180
+ if (discoveryOff !== undefined) {
181
+ const off = `Sessions on this host are not listed: local peer discovery is off for this session (${discoveryOff}).`;
182
+ return {
183
+ message:
184
+ devices.length === 0
185
+ ? `${off}\nNo device is linked right now.`
186
+ : `Linked devices:\n${devices.map(describeDevice).join('\n')}\n\n${off}` +
187
+ `\n\nSend to one: /peers send <device-id> <message>` +
188
+ `\nSend a file: /peers send-file <device-id> <path>`,
189
+ success: true,
190
+ };
191
+ }
192
+
168
193
  const own = adapter.ownSessionId();
169
194
  // The workspace-judged listing when the host has one; it reads git, so only this view asks for it.
170
195
  const peers = (
@@ -172,7 +197,7 @@ export async function executePeersCommand(
172
197
  ).filter(addressable);
173
198
  const others = peers.filter((peer) => peer.sessionId !== own);
174
199
 
175
- if (others.length === 0) {
200
+ if (others.length === 0 && devices.length === 0) {
176
201
  return {
177
202
  message:
178
203
  'No other live session is announced. Start a second session on this host, as this user, ' +
@@ -181,11 +206,15 @@ export async function executePeersCommand(
181
206
  };
182
207
  }
183
208
 
184
- const lines = peers.map((peer) => describe(peer, own));
209
+ const sections = [`Live sessions:\n${peers.map((peer) => describe(peer, own)).join('\n')}`];
210
+ if (devices.length > 0) {
211
+ sections.push(`Linked devices:\n${devices.map(describeDevice).join('\n')}`);
212
+ }
213
+ const target = devices.length > 0 ? '<session-or-device-id>' : '<session-id>';
185
214
  return {
186
215
  message:
187
- `Live sessions:\n${lines.join('\n')}\n\nSend to one: /peers send <session-id> <message>` +
188
- '\nSend a file: /peers send-file <session-id> <path>',
216
+ `${sections.join('\n\n')}\n\nSend to one: /peers send ${target} <message>` +
217
+ `\nSend a file: /peers send-file ${target} <path>`,
189
218
  success: true,
190
219
  };
191
220
  }
@@ -46,7 +46,10 @@ function createCommandSessionRuntime() {
46
46
  });
47
47
  }
48
48
 
49
- function createCommandHostContext(adapter?: ICommandPluginAdapter) {
49
+ function createCommandHostContext(
50
+ adapter?: ICommandPluginAdapter,
51
+ locality?: 'local' | 'remote',
52
+ ) {
50
53
  const checkpoint = {
51
54
  id: 'checkpoint_1',
52
55
  sessionId: 'session_1',
@@ -66,6 +69,7 @@ function createCommandHostContext(adapter?: ICommandPluginAdapter) {
66
69
  }),
67
70
  getAutoCompactThreshold: () => 0.8,
68
71
  getCommandHostAdapters: () => (adapter === undefined ? {} : { plugin: adapter }),
72
+ ...(locality ? { getCommandSurfaceLocality: () => locality } : {}),
69
73
  compactContext: async () => undefined,
70
74
  getCwd: () => '/workspace',
71
75
  listCommands: () => [],
@@ -186,6 +190,82 @@ describe('executePluginCommand', () => {
186
190
  message: 'Usage: /plugin disable <name>@<marketplace>',
187
191
  });
188
192
  });
193
+
194
+ it('lists installed plugins through the adapter, for the Settings screen (#3282 §4 part b-2)', async () => {
195
+ const adapter = createPluginAdapter({
196
+ listInstalled: vi.fn().mockResolvedValue([
197
+ { name: 'formatter@robota', description: 'Formats code.', enabled: true },
198
+ { name: 'linter@robota', description: 'Lints code.', enabled: false },
199
+ ]),
200
+ });
201
+
202
+ const result = await executePluginCommand(createCommandHostContext(adapter), 'list');
203
+
204
+ expect(result.success).toBe(true);
205
+ expect(result.data).toEqual({
206
+ plugins: [
207
+ { name: 'formatter@robota', description: 'Formats code.', enabled: true },
208
+ { name: 'linter@robota', description: 'Lints code.', enabled: false },
209
+ ],
210
+ });
211
+ expect(result.message).toContain('formatter@robota');
212
+ expect(result.message).toContain('linter@robota (disabled)');
213
+ });
214
+
215
+ it('no plugins installed reports a plain empty list', async () => {
216
+ const adapter = createPluginAdapter();
217
+ const result = await executePluginCommand(createCommandHostContext(adapter), 'list');
218
+ expect(result).toEqual({
219
+ success: true,
220
+ message: 'No plugins are installed.',
221
+ data: { plugins: [] },
222
+ });
223
+ });
224
+
225
+ describe('install/uninstall stay local-surface-only (#3282 §4 part b-2)', () => {
226
+ it('runs install from a local surface', async () => {
227
+ const adapter = createPluginAdapter();
228
+ const result = await executePluginCommand(
229
+ createCommandHostContext(adapter, 'local'),
230
+ 'install demo@community',
231
+ );
232
+ expect(result.success).toBe(true);
233
+ expect(adapter.install).toHaveBeenCalledWith('demo@community');
234
+ });
235
+
236
+ it('refuses install from a remote surface with a plain message, writing nothing', async () => {
237
+ const adapter = createPluginAdapter();
238
+ const result = await executePluginCommand(
239
+ createCommandHostContext(adapter, 'remote'),
240
+ 'install demo@community',
241
+ );
242
+ expect(result.success).toBe(false);
243
+ expect(result.message).toMatch(/only works from the desktop app or the page opened here/);
244
+ expect(result.message).toMatch(/not from a remote device/);
245
+ expect(adapter.install).not.toHaveBeenCalled();
246
+ });
247
+
248
+ it('refuses uninstall from a remote surface with a plain message, writing nothing', async () => {
249
+ const adapter = createPluginAdapter();
250
+ const result = await executePluginCommand(
251
+ createCommandHostContext(adapter, 'remote'),
252
+ 'uninstall demo@community',
253
+ );
254
+ expect(result.success).toBe(false);
255
+ expect(result.message).toMatch(/not from a remote device/);
256
+ expect(adapter.uninstall).not.toHaveBeenCalled();
257
+ });
258
+
259
+ it('never refuses enable/disable/reload from a remote surface — only install/uninstall run new code', async () => {
260
+ const adapter = createPluginAdapter();
261
+ const enableResult = await executePluginCommand(
262
+ createCommandHostContext(adapter, 'remote'),
263
+ 'enable demo@community',
264
+ );
265
+ expect(enableResult.success).toBe(true);
266
+ expect(adapter.enable).toHaveBeenCalledWith('demo@community');
267
+ });
268
+ });
189
269
  });
190
270
 
191
271
  describe('executeReloadPluginsCommand', () => {
@@ -3,9 +3,13 @@ import {
3
3
  resolvePluginCommandAdapter,
4
4
  } from '@robota-sdk/agent-framework';
5
5
 
6
- import type { ICommandHostAdapterAccess } from '@robota-sdk/agent-framework';
6
+ import type { ICommandHostAdapterAccess, ICommandHostWorkspace } from '@robota-sdk/agent-framework';
7
7
  import type { ICommandPluginAdapter, ICommandResult } from '@robota-sdk/agent-interface-command';
8
8
 
9
+ /** What `/plugin` reaches: its adapter port, and — for install/uninstall — this connection's locality. */
10
+ export type TPluginCommandContext = ICommandHostAdapterAccess &
11
+ Partial<Pick<ICommandHostWorkspace, 'getCommandSurfaceLocality'>>;
12
+
9
13
  function getSubcommandParts(args: string): { subcommand: string; subArgs: string } {
10
14
  const parts = args
11
15
  .trim()
@@ -28,6 +32,20 @@ function getPluginAdapter(context: ICommandHostAdapterAccess): ICommandPluginAda
28
32
  return resolvePluginCommandAdapter(context);
29
33
  }
30
34
 
35
+ const REMOTE_INSTALL_REFUSAL =
36
+ 'Installing and uninstalling plugins runs code on this computer, so it only works from the ' +
37
+ 'desktop app or the page opened here — not from a remote device.';
38
+
39
+ /**
40
+ * #3282 §4 part b-2: the same rule the GUI's Settings screen applies before it ever sends the
41
+ * write — installing/uninstalling stays local-surface-only because it runs third-party code.
42
+ * `getCommandSurfaceLocality` absent (an older host, a test double, the in-process TUI) reads as
43
+ * `'local'`, matching the allow-by-default posture the rest of the command layer already uses.
44
+ */
45
+ function isRemoteSurface(context: TPluginCommandContext): boolean {
46
+ return (context.getCommandSurfaceLocality?.() ?? 'local') === 'remote';
47
+ }
48
+
31
49
  async function executePluginOperation(
32
50
  context: ICommandHostAdapterAccess,
33
51
  operation: (adapter: ICommandPluginAdapter) => Promise<string>,
@@ -123,9 +141,10 @@ function executePluginManager(): ICommandResult {
123
141
  }
124
142
 
125
143
  function executeInstallCommand(
126
- context: ICommandHostAdapterAccess,
144
+ context: TPluginCommandContext,
127
145
  pluginId: string,
128
146
  ): Promise<ICommandResult> {
147
+ if (isRemoteSurface(context)) return Promise.resolve(usage(REMOTE_INSTALL_REFUSAL));
129
148
  return executePluginIdOperation(
130
149
  context,
131
150
  pluginId,
@@ -138,9 +157,10 @@ function executeInstallCommand(
138
157
  }
139
158
 
140
159
  function executeUninstallCommand(
141
- context: ICommandHostAdapterAccess,
160
+ context: TPluginCommandContext,
142
161
  pluginId: string,
143
162
  ): Promise<ICommandResult> {
163
+ if (isRemoteSurface(context)) return Promise.resolve(usage(REMOTE_INSTALL_REFUSAL));
144
164
  return executePluginIdOperation(
145
165
  context,
146
166
  pluginId,
@@ -152,6 +172,27 @@ function executeUninstallCommand(
152
172
  );
153
173
  }
154
174
 
175
+ /** #3282 §4 part b-2: the Settings screen's Plugins section reads through this, like `/mcp status`. */
176
+ async function executeListInstalledCommand(
177
+ context: ICommandHostAdapterAccess,
178
+ ): Promise<ICommandResult> {
179
+ const adapter = getPluginAdapter(context);
180
+ if (adapter === undefined) {
181
+ return { success: true, message: 'Plugin management is not available.', data: { plugins: [] } };
182
+ }
183
+ try {
184
+ const plugins = await adapter.listInstalled();
185
+ const message =
186
+ plugins.length === 0
187
+ ? 'No plugins are installed.'
188
+ : `Installed plugins:\n${plugins.map((plugin) => ` ${plugin.name}${plugin.enabled ? '' : ' (disabled)'} — ${plugin.description}`).join('\n')}`;
189
+ return { success: true, message, data: { plugins } };
190
+ } catch (error) {
191
+ const message = error instanceof Error ? error.message : String(error);
192
+ return { success: false, message: `Plugin error: ${message}` };
193
+ }
194
+ }
195
+
155
196
  function executeEnableCommand(
156
197
  context: ICommandHostAdapterAccess,
157
198
  pluginId: string,
@@ -183,7 +224,7 @@ function executeDisableCommand(
183
224
  }
184
225
 
185
226
  export async function executePluginCommand(
186
- context: ICommandHostAdapterAccess,
227
+ context: TPluginCommandContext,
187
228
  args: string,
188
229
  ): Promise<ICommandResult> {
189
230
  const { subcommand, subArgs } = getSubcommandParts(args);
@@ -191,6 +232,8 @@ export async function executePluginCommand(
191
232
  case '':
192
233
  case 'manage':
193
234
  return executePluginManager();
235
+ case 'list':
236
+ return executeListInstalledCommand(context);
194
237
  case 'install':
195
238
  return executeInstallCommand(context, subArgs);
196
239
  case 'uninstall':
@@ -97,6 +97,10 @@ describe('org policy enforcement in provider commands', () => {
97
97
  });
98
98
 
99
99
  it('allows /provider switch when target is in allowedProviders', async () => {
100
+ // #3282: the switch now validates the target profile against `providerDefinitions` before
101
+ // writing anything, the same way the hot-swap it triggers would build the provider — so the
102
+ // profile needs a resolvable credential, same as a real one would.
103
+ vi.stubEnv('OPENAI_API_KEY', 'sk-test');
100
104
  const { adapter } = createSettingsAdapter({
101
105
  currentProvider: 'anthropic',
102
106
  providers: {