@robota-sdk/agent-command 3.0.0-beta.64

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 (95) hide show
  1. package/LICENSE +21 -0
  2. package/dist/node/index.cjs +30 -0
  3. package/dist/node/index.d.ts +293 -0
  4. package/dist/node/index.d.ts.map +1 -0
  5. package/dist/node/index.js +31 -0
  6. package/dist/node/index.js.map +1 -0
  7. package/package.json +48 -0
  8. package/src/agent/__tests__/agent-command.test.ts +504 -0
  9. package/src/agent/agent-command-module.ts +82 -0
  10. package/src/agent/agent-command-parser.ts +180 -0
  11. package/src/agent/agent-command.ts +235 -0
  12. package/src/agent/index.ts +7 -0
  13. package/src/background/__tests__/background-command-module.test.ts +255 -0
  14. package/src/background/background-command-module.ts +53 -0
  15. package/src/background/background-command.ts +63 -0
  16. package/src/background/index.ts +6 -0
  17. package/src/compact/__tests__/compact-command-module.test.ts +162 -0
  18. package/src/compact/compact-command-module.ts +51 -0
  19. package/src/compact/compact-command.ts +21 -0
  20. package/src/compact/index.ts +6 -0
  21. package/src/context/__tests__/context-command-module.test.ts +294 -0
  22. package/src/context/context-command-module.ts +54 -0
  23. package/src/context/context-command.ts +298 -0
  24. package/src/context/index.ts +6 -0
  25. package/src/exit/__tests__/exit-command-module.test.ts +35 -0
  26. package/src/exit/exit-command-module.ts +48 -0
  27. package/src/exit/exit-command.ts +10 -0
  28. package/src/exit/index.ts +6 -0
  29. package/src/help/__tests__/help-command-module.test.ts +106 -0
  30. package/src/help/help-command-module.ts +48 -0
  31. package/src/help/help-command.ts +9 -0
  32. package/src/help/index.ts +6 -0
  33. package/src/index.ts +20 -0
  34. package/src/language/__tests__/language-command-module.test.ts +105 -0
  35. package/src/language/index.ts +6 -0
  36. package/src/language/language-command-module.ts +56 -0
  37. package/src/language/language-command.ts +22 -0
  38. package/src/memory/__tests__/memory-command-module.test.ts +272 -0
  39. package/src/memory/index.ts +6 -0
  40. package/src/memory/memory-command-module.ts +57 -0
  41. package/src/memory/memory-command.ts +234 -0
  42. package/src/mode/__tests__/mode-command-module.test.ts +143 -0
  43. package/src/mode/index.ts +6 -0
  44. package/src/mode/mode-command-module.ts +56 -0
  45. package/src/mode/mode-command.ts +34 -0
  46. package/src/model/__tests__/model-command-module.test.ts +273 -0
  47. package/src/model/index.ts +6 -0
  48. package/src/model/model-command-module.ts +68 -0
  49. package/src/model/model-command.ts +40 -0
  50. package/src/permissions/__tests__/permissions-command-module.test.ts +164 -0
  51. package/src/permissions/index.ts +6 -0
  52. package/src/permissions/permissions-command-module.ts +56 -0
  53. package/src/permissions/permissions-command.ts +45 -0
  54. package/src/plugin/__tests__/plugin-command-module.test.ts +214 -0
  55. package/src/plugin/index.ts +7 -0
  56. package/src/plugin/plugin-command-module.ts +81 -0
  57. package/src/plugin/plugin-command.ts +230 -0
  58. package/src/provider/__tests__/provider-command-module.test.ts +488 -0
  59. package/src/provider/__tests__/provider-setup-flow.test.ts +43 -0
  60. package/src/provider/index.ts +30 -0
  61. package/src/provider/provider-command-execution.ts +150 -0
  62. package/src/provider/provider-command-module.ts +65 -0
  63. package/src/provider/provider-command-profile-lifecycle.ts +211 -0
  64. package/src/provider/provider-command-profile-operations.ts +198 -0
  65. package/src/provider/provider-command-profile.ts +109 -0
  66. package/src/provider/provider-command-setup.ts +104 -0
  67. package/src/provider/provider-setup-flow.ts +309 -0
  68. package/src/reset/__tests__/reset-command-module.test.ts +63 -0
  69. package/src/reset/index.ts +2 -0
  70. package/src/reset/reset-command-module.ts +49 -0
  71. package/src/reset/reset-command.ts +10 -0
  72. package/src/rewind/__tests__/rewind-command-module.test.ts +215 -0
  73. package/src/rewind/index.ts +2 -0
  74. package/src/rewind/rewind-command-module.ts +57 -0
  75. package/src/rewind/rewind-command.ts +184 -0
  76. package/src/session/__tests__/session-command-module.test.ts +339 -0
  77. package/src/session/index.ts +17 -0
  78. package/src/session/session-command-module.ts +168 -0
  79. package/src/session/session-command.ts +74 -0
  80. package/src/settings/index.ts +7 -0
  81. package/src/settings/settings-command-module.ts +50 -0
  82. package/src/skills/__tests__/skills-command-module.test.ts +157 -0
  83. package/src/skills/index.ts +6 -0
  84. package/src/skills/skills-command-module.ts +62 -0
  85. package/src/skills/skills-command.ts +110 -0
  86. package/src/statusline/__tests__/statusline-command-module.test.ts +95 -0
  87. package/src/statusline/index.ts +6 -0
  88. package/src/statusline/statusline-command-module.ts +56 -0
  89. package/src/statusline/statusline-command.ts +79 -0
  90. package/src/user-local/__tests__/user-local-command.test.ts +145 -0
  91. package/src/user-local/index.ts +13 -0
  92. package/src/user-local/user-local-command-constants.ts +5 -0
  93. package/src/user-local/user-local-command-module.ts +67 -0
  94. package/src/user-local/user-local-command.ts +205 -0
  95. package/src/user-local/user-local-memory-command.ts +147 -0
@@ -0,0 +1,273 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import type {
3
+ ICommandHostContext,
4
+ IEditCheckpointRestoreResult,
5
+ IModelCommandModuleOptions,
6
+ TProviderSettingsDocument,
7
+ } from '@robota-sdk/agent-framework';
8
+ import { SystemCommandExecutor } from '@robota-sdk/agent-framework';
9
+ import { createModelCommandModule } from '../model-command-module.js';
10
+
11
+ type TTestProviderDefinition = NonNullable<
12
+ IModelCommandModuleOptions['providerDefinitions']
13
+ >[number];
14
+
15
+ function createCheckpointResult(): IEditCheckpointRestoreResult {
16
+ return {
17
+ target: {
18
+ id: 'checkpoint_1',
19
+ sessionId: 'session_1',
20
+ sequence: 1,
21
+ prompt: 'edit files',
22
+ createdAt: '2026-05-03T00:00:00.000Z',
23
+ fileCount: 1,
24
+ },
25
+ restoredCheckpointCount: 0,
26
+ restoredFileCount: 0,
27
+ removedCheckpointCount: 0,
28
+ };
29
+ }
30
+
31
+ const commandHostContext: ICommandHostContext = {
32
+ getSession: () => {
33
+ throw new Error('model command should not read session runtime');
34
+ },
35
+ getContextState: () => ({
36
+ usedTokens: 0,
37
+ maxTokens: 1,
38
+ usedPercentage: 0,
39
+ remainingPercentage: 100,
40
+ }),
41
+ getAutoCompactThreshold: () => 0.835,
42
+ compactContext: async () => undefined,
43
+ getCwd: () => '/workspace',
44
+ listEditCheckpoints: () => [],
45
+ restoreEditCheckpoint: async () => createCheckpointResult(),
46
+ rollbackEditCheckpoint: async () => createCheckpointResult(),
47
+ getUsedMemoryReferences: () => [],
48
+ recordMemoryEvent: () => undefined,
49
+ listBackgroundTasks: () => [],
50
+ readBackgroundTaskLog: async () => ({ taskId: 'task_1', lines: [] }),
51
+ cancelBackgroundTask: async () => undefined,
52
+ closeBackgroundTask: async () => undefined,
53
+ };
54
+
55
+ const providerDefinitions: readonly TTestProviderDefinition[] = [
56
+ {
57
+ type: 'anthropic',
58
+ defaults: { model: 'claude-sonnet-4-6' },
59
+ modelCatalog: {
60
+ status: 'fallback',
61
+ lastVerifiedAt: '2026-05-04',
62
+ sourceUrl: 'https://platform.claude.com/docs/en/api/models/list',
63
+ entries: [
64
+ {
65
+ id: 'claude-sonnet-4-6',
66
+ displayName: 'Claude Sonnet 4.6',
67
+ contextWindow: 1_000_000,
68
+ lifecycle: 'active',
69
+ },
70
+ ],
71
+ },
72
+ createProvider: () => {
73
+ throw new Error('not used');
74
+ },
75
+ },
76
+ {
77
+ type: 'qwen',
78
+ defaults: { model: 'qwen-plus' },
79
+ modelCatalog: {
80
+ status: 'fallback',
81
+ lastVerifiedAt: '2026-05-04',
82
+ sourceUrl:
83
+ 'https://www.alibabacloud.com/help/en/model-studio/compatibility-of-openai-with-dashscope',
84
+ entries: [
85
+ {
86
+ id: 'qwen-plus',
87
+ displayName: 'Qwen Plus',
88
+ lifecycle: 'active',
89
+ },
90
+ {
91
+ id: 'qwen-max',
92
+ displayName: 'Qwen Max',
93
+ lifecycle: 'active',
94
+ },
95
+ ],
96
+ },
97
+ createProvider: () => {
98
+ throw new Error('not used');
99
+ },
100
+ },
101
+ {
102
+ type: 'openai',
103
+ modelCatalog: {
104
+ status: 'unavailable',
105
+ sourceUrl: 'https://platform.openai.com/docs/api-reference/models/list',
106
+ message: 'OpenAI models should be discovered live.',
107
+ },
108
+ createProvider: () => {
109
+ throw new Error('not used');
110
+ },
111
+ },
112
+ ];
113
+
114
+ const refreshableProviderDefinitions: readonly TTestProviderDefinition[] = [
115
+ {
116
+ type: 'openai',
117
+ modelCatalog: {
118
+ status: 'unavailable',
119
+ sourceUrl: 'https://platform.openai.com/docs/api-reference/models/list',
120
+ message: 'OpenAI models should be discovered live.',
121
+ },
122
+ refreshModelCatalog: async () => ({
123
+ status: 'live',
124
+ sourceUrl: 'https://platform.openai.com/docs/api-reference/models/list',
125
+ lastVerifiedAt: '2026-05-05T00:00:00.000Z',
126
+ entries: [{ id: 'gpt-5.1', displayName: 'gpt-5.1', lifecycle: 'active' }],
127
+ }),
128
+ createProvider: () => {
129
+ throw new Error('not used');
130
+ },
131
+ },
132
+ ];
133
+
134
+ function createModelModuleForSettings(settings: TProviderSettingsDocument) {
135
+ return createModelCommandModule({
136
+ providerDefinitions,
137
+ settings: {
138
+ readMergedSettings: () => settings,
139
+ },
140
+ });
141
+ }
142
+
143
+ describe('createModelCommandModule', () => {
144
+ it('provides model metadata and executable command from one module owner', () => {
145
+ const module = createModelCommandModule();
146
+ const command = module.systemCommands?.[0];
147
+ const entry = module.commandSources?.[0]?.getCommands()[0];
148
+
149
+ expect(module.name).toBe('agent-command-model');
150
+ expect(entry).toEqual(
151
+ expect.objectContaining({
152
+ name: 'model',
153
+ description: 'Change AI model',
154
+ argumentHint: '<model-id>',
155
+ source: 'model',
156
+ }),
157
+ );
158
+ expect(entry?.subcommands?.map((subcommand) => subcommand.name)).toContain('claude-sonnet-4-6');
159
+ expect(command).toEqual(
160
+ expect.objectContaining({
161
+ name: 'model',
162
+ description: 'Change AI model',
163
+ argumentHint: '<model-id>',
164
+ lifecycle: 'inline',
165
+ }),
166
+ );
167
+ expect(command?.subcommands).toEqual(entry?.subcommands);
168
+ });
169
+
170
+ it('deduplicates date-suffixed model variants in descriptor subcommands', () => {
171
+ const entry = createModelCommandModule().commandSources?.[0]?.getCommands()[0];
172
+ const names = entry?.subcommands?.map((subcommand) => subcommand.name) ?? [];
173
+
174
+ expect(names).toContain('claude-opus-4-6');
175
+ expect(names).toContain('claude-sonnet-4-6');
176
+ expect(names).toContain('claude-haiku-4-5');
177
+ expect(names).not.toContain('claude-haiku-4-5-20251001');
178
+ expect(names).not.toContain('claude-sonnet-4-5-20250929');
179
+ expect(names).not.toContain('claude-opus-4-5-20251101');
180
+ });
181
+
182
+ it('formats subcommand descriptions with human-readable names and context windows', () => {
183
+ const entry = createModelCommandModule().commandSources?.[0]?.getCommands()[0];
184
+ const subcommands = entry?.subcommands ?? [];
185
+
186
+ expect(
187
+ subcommands.find((subcommand) => subcommand.name === 'claude-opus-4-6')?.description,
188
+ ).toBe('Claude Opus 4.6 (1M)');
189
+ expect(
190
+ subcommands.find((subcommand) => subcommand.name === 'claude-haiku-4-5')?.description,
191
+ ).toBe('Claude Haiku 4.5 (200K)');
192
+ });
193
+
194
+ it('lists models for the effective active provider instead of Claude defaults', () => {
195
+ const entry = createModelModuleForSettings({
196
+ currentProvider: 'qwen',
197
+ providers: {
198
+ qwen: { type: 'qwen', model: 'qwen-plus' },
199
+ anthropic: { type: 'anthropic', model: 'claude-sonnet-4-6' },
200
+ },
201
+ }).commandSources?.[0]?.getCommands()[0];
202
+
203
+ const names = entry?.subcommands?.map((subcommand) => subcommand.name) ?? [];
204
+ expect(names).toEqual(['qwen-plus', 'qwen-max']);
205
+ expect(names).not.toContain('claude-sonnet-4-6');
206
+ });
207
+
208
+ it('keeps manual model input available when the active provider has no catalog', async () => {
209
+ const executor = new SystemCommandExecutor([
210
+ ...(createModelModuleForSettings({
211
+ currentProvider: 'openai',
212
+ providers: {
213
+ openai: { type: 'openai', model: 'gpt-5.1' },
214
+ },
215
+ }).systemCommands ?? []),
216
+ ]);
217
+
218
+ const usage = await executor.execute('model', commandHostContext, '');
219
+ const manual = await executor.execute('model', commandHostContext, 'gpt-5.1');
220
+
221
+ expect(usage?.success).toBe(false);
222
+ expect(usage?.message).toContain('No model catalog available for provider openai');
223
+ expect(manual?.success).toBe(true);
224
+ expect(manual?.effects).toEqual([{ type: 'model-change-requested', modelId: 'gpt-5.1' }]);
225
+ });
226
+
227
+ it('surfaces refreshed catalog freshness when usage is requested', async () => {
228
+ const executor = new SystemCommandExecutor([
229
+ ...(createModelCommandModule({
230
+ providerDefinitions: refreshableProviderDefinitions,
231
+ settings: {
232
+ readMergedSettings: () => ({
233
+ currentProvider: 'openai',
234
+ providers: {
235
+ openai: { type: 'openai', model: 'gpt-5.1', apiKey: 'sk-test' },
236
+ },
237
+ }),
238
+ },
239
+ }).systemCommands ?? []),
240
+ ]);
241
+
242
+ const usage = await executor.execute('model', commandHostContext, '');
243
+
244
+ expect(usage?.success).toBe(false);
245
+ expect(usage?.message).toContain('Catalog: live; 1 model(s)');
246
+ expect(usage?.message).toContain('verified 2026-05-05T00:00:00.000Z');
247
+ });
248
+
249
+ it('requests model changes through a typed command effect', async () => {
250
+ const executor = new SystemCommandExecutor([
251
+ ...(createModelCommandModule().systemCommands ?? []),
252
+ ]);
253
+
254
+ const result = await executor.execute('model', commandHostContext, 'claude-sonnet-4-6');
255
+
256
+ expect(result?.success).toBe(true);
257
+ expect(result?.data?.modelId).toBe('claude-sonnet-4-6');
258
+ expect(result?.effects).toEqual([
259
+ { type: 'model-change-requested', modelId: 'claude-sonnet-4-6' },
260
+ ]);
261
+ });
262
+
263
+ it('shows usage when no model id is provided', async () => {
264
+ const executor = new SystemCommandExecutor([
265
+ ...(createModelCommandModule().systemCommands ?? []),
266
+ ]);
267
+
268
+ const result = await executor.execute('model', commandHostContext, '');
269
+
270
+ expect(result?.success).toBe(false);
271
+ expect(result?.message).toBe('Usage: model <model-id>');
272
+ });
273
+ });
@@ -0,0 +1,6 @@
1
+ export {
2
+ ModelCommandSource,
3
+ createModelCommandEntry,
4
+ createModelCommandModule,
5
+ } from './model-command-module.js';
6
+ export { executeModelCommand } from './model-command.js';
@@ -0,0 +1,68 @@
1
+ import type {
2
+ ICommand,
3
+ ICommandModule,
4
+ ICommandSource,
5
+ IModelCommandModuleOptions,
6
+ ISystemCommand,
7
+ } from '@robota-sdk/agent-framework';
8
+ import {
9
+ buildModelCommandSubcommands,
10
+ MODEL_COMMAND_ARGUMENT_HINT,
11
+ MODEL_COMMAND_DESCRIPTION,
12
+ } from '@robota-sdk/agent-framework';
13
+ import { executeModelCommand } from './model-command.js';
14
+
15
+ export function createModelCommandEntry(options?: IModelCommandModuleOptions): ICommand {
16
+ return {
17
+ name: 'model',
18
+ displayName: 'Change Model',
19
+ description: MODEL_COMMAND_DESCRIPTION,
20
+ source: 'model',
21
+ argumentHint: MODEL_COMMAND_ARGUMENT_HINT,
22
+ subcommands: buildModelSubcommands(options),
23
+ };
24
+ }
25
+
26
+ function createModelSystemCommand(options?: IModelCommandModuleOptions): ISystemCommand {
27
+ const entry = createModelCommandEntry(options);
28
+ return {
29
+ name: entry.name,
30
+ displayName: entry.displayName,
31
+ description: entry.description,
32
+ requiresPermission: true,
33
+ userInvocable: true,
34
+ argumentHint: entry.argumentHint,
35
+ subcommands: entry.subcommands,
36
+ lifecycle: 'inline',
37
+ execute: (context, args) => executeModelCommand(context, args, options),
38
+ };
39
+ }
40
+
41
+ export class ModelCommandSource implements ICommandSource {
42
+ readonly name = 'model';
43
+
44
+ constructor(private readonly options?: IModelCommandModuleOptions) {}
45
+
46
+ getCommands(): ICommand[] {
47
+ return [createModelCommandEntry(this.options)];
48
+ }
49
+ }
50
+
51
+ export function createModelCommandModule(options?: IModelCommandModuleOptions): ICommandModule {
52
+ return {
53
+ name: 'agent-command-model',
54
+ commandSources: [new ModelCommandSource(options)],
55
+ systemCommands: [createModelSystemCommand(options)],
56
+ };
57
+ }
58
+
59
+ function buildModelSubcommands(options?: IModelCommandModuleOptions): ICommand[] {
60
+ if (options === undefined) {
61
+ return buildModelCommandSubcommands('model');
62
+ }
63
+ return buildModelCommandSubcommands({
64
+ source: 'model',
65
+ providerDefinitions: options.providerDefinitions,
66
+ settings: options.settings.readMergedSettings(),
67
+ });
68
+ }
@@ -0,0 +1,40 @@
1
+ import type {
2
+ ICommandHostContext,
3
+ ICommandResult,
4
+ IModelCommandModuleOptions,
5
+ } from '@robota-sdk/agent-framework';
6
+ import { formatModelCommandUsageMessageAsync } from '@robota-sdk/agent-framework';
7
+
8
+ function parseModelId(args: string): string | undefined {
9
+ const modelId = args.trim().split(/\s+/)[0];
10
+ return modelId !== undefined && modelId.length > 0 ? modelId : undefined;
11
+ }
12
+
13
+ export async function executeModelCommand(
14
+ _context: ICommandHostContext,
15
+ args: string,
16
+ options?: IModelCommandModuleOptions,
17
+ ): Promise<ICommandResult> {
18
+ const modelId = parseModelId(args);
19
+ if (modelId === undefined) {
20
+ return {
21
+ message: await formatModelCommandUsageMessageAsync({
22
+ ...(options?.settings !== undefined
23
+ ? { settings: options.settings.readMergedSettings() }
24
+ : {}),
25
+ ...(options?.providerDefinitions !== undefined
26
+ ? { providerDefinitions: options.providerDefinitions }
27
+ : {}),
28
+ refresh: true,
29
+ }),
30
+ success: false,
31
+ };
32
+ }
33
+
34
+ return {
35
+ message: `Model change requested: ${modelId}`,
36
+ success: true,
37
+ data: { modelId },
38
+ effects: [{ type: 'model-change-requested', modelId }],
39
+ };
40
+ }
@@ -0,0 +1,164 @@
1
+ import { describe, expect, it, vi } from 'vitest';
2
+ import type {
3
+ ICommandHostContext,
4
+ IEditCheckpointRestoreResult,
5
+ } from '@robota-sdk/agent-framework';
6
+ import { SystemCommandExecutor } from '@robota-sdk/agent-framework';
7
+ import { createPermissionsCommandModule } from '../permissions-command-module.js';
8
+
9
+ type TPermissionModeName = 'plan' | 'default' | 'acceptEdits' | 'bypassPermissions';
10
+ type TSetPermissionModeSpy = ReturnType<typeof vi.fn<[nextMode: TPermissionModeName], void>>;
11
+
12
+ function createCheckpointResult(): IEditCheckpointRestoreResult {
13
+ return {
14
+ target: {
15
+ id: 'checkpoint_1',
16
+ sessionId: 'session_1',
17
+ sequence: 1,
18
+ prompt: 'edit files',
19
+ createdAt: '2026-05-03T00:00:00.000Z',
20
+ fileCount: 1,
21
+ },
22
+ restoredCheckpointCount: 0,
23
+ restoredFileCount: 0,
24
+ removedCheckpointCount: 0,
25
+ };
26
+ }
27
+
28
+ function createCommandHostContext(options?: {
29
+ mode?: TPermissionModeName;
30
+ sessionAllowed?: readonly string[];
31
+ }): ICommandHostContext & { setPermissionMode: TSetPermissionModeSpy } {
32
+ let mode = options?.mode ?? 'default';
33
+ const setPermissionMode = vi.fn((nextMode: TPermissionModeName) => {
34
+ mode = nextMode;
35
+ });
36
+
37
+ return {
38
+ getSession: () => {
39
+ throw new Error('permissions command should use the permission mode adapter');
40
+ },
41
+ getCommandHostAdapters: () => ({
42
+ permissionMode: {
43
+ getPermissionMode: () => mode,
44
+ setPermissionMode,
45
+ listSessionAllowedTools: () => options?.sessionAllowed ?? [],
46
+ },
47
+ }),
48
+ getContextState: () => ({
49
+ usedTokens: 0,
50
+ maxTokens: 1,
51
+ usedPercentage: 0,
52
+ remainingPercentage: 100,
53
+ }),
54
+ getAutoCompactThreshold: () => 0.835,
55
+ compactContext: async () => undefined,
56
+ getCwd: () => '/workspace',
57
+ listEditCheckpoints: () => [],
58
+ restoreEditCheckpoint: async () => createCheckpointResult(),
59
+ rollbackEditCheckpoint: async () => createCheckpointResult(),
60
+ getUsedMemoryReferences: () => [],
61
+ recordMemoryEvent: () => undefined,
62
+ listBackgroundTasks: () => [],
63
+ readBackgroundTaskLog: async () => ({ taskId: 'task_1', lines: [] }),
64
+ cancelBackgroundTask: async () => undefined,
65
+ closeBackgroundTask: async () => undefined,
66
+ setPermissionMode,
67
+ };
68
+ }
69
+
70
+ describe('createPermissionsCommandModule', () => {
71
+ it('provides permissions metadata and user-only executable command from one module owner', () => {
72
+ const module = createPermissionsCommandModule();
73
+ const command = module.systemCommands?.[0];
74
+ const entry = module.commandSources?.[0]?.getCommands()[0];
75
+
76
+ expect(module.name).toBe('agent-command-permissions');
77
+ expect(entry).toEqual(
78
+ expect.objectContaining({
79
+ name: 'permissions',
80
+ description: 'Show/change permission mode and permission rules',
81
+ argumentHint: 'plan | default | acceptEdits | bypassPermissions',
82
+ source: 'permissions',
83
+ modelInvocable: false,
84
+ }),
85
+ );
86
+ expect(entry?.subcommands?.map((subcommand) => subcommand.name)).toEqual([
87
+ 'plan',
88
+ 'default',
89
+ 'acceptEdits',
90
+ 'bypassPermissions',
91
+ ]);
92
+ expect(command).toEqual(
93
+ expect.objectContaining({
94
+ name: 'permissions',
95
+ description: 'Show/change permission mode and permission rules',
96
+ argumentHint: 'plan | default | acceptEdits | bypassPermissions',
97
+ lifecycle: 'inline',
98
+ modelInvocable: false,
99
+ }),
100
+ );
101
+ expect(command?.subcommands).toEqual(entry?.subcommands);
102
+ });
103
+
104
+ it('reports no session-approved tools when the command allowlist is empty', async () => {
105
+ const executor = new SystemCommandExecutor([
106
+ ...(createPermissionsCommandModule().systemCommands ?? []),
107
+ ]);
108
+
109
+ const result = await executor.execute('permissions', createCommandHostContext(), '');
110
+
111
+ expect(result?.success).toBe(true);
112
+ expect(result?.message).toBe('Permission mode: default\nNo session-approved tools.');
113
+ expect(result?.data).toEqual({ mode: 'default', sessionAllowed: [] });
114
+ });
115
+
116
+ it('reports session-approved tools when present', async () => {
117
+ const executor = new SystemCommandExecutor([
118
+ ...(createPermissionsCommandModule().systemCommands ?? []),
119
+ ]);
120
+
121
+ const result = await executor.execute(
122
+ 'permissions',
123
+ createCommandHostContext({ mode: 'acceptEdits', sessionAllowed: ['Bash', 'Read'] }),
124
+ '',
125
+ );
126
+
127
+ expect(result?.success).toBe(true);
128
+ expect(result?.message).toBe(
129
+ 'Permission mode: acceptEdits\nSession-approved tools: Bash, Read',
130
+ );
131
+ expect(result?.data).toEqual({ mode: 'acceptEdits', sessionAllowed: ['Bash', 'Read'] });
132
+ });
133
+
134
+ it('updates valid permission modes through the SDK command adapter', async () => {
135
+ const executor = new SystemCommandExecutor([
136
+ ...(createPermissionsCommandModule().systemCommands ?? []),
137
+ ]);
138
+ const context = createCommandHostContext();
139
+
140
+ const result = await executor.execute('permissions', context, 'plan');
141
+
142
+ expect(result?.success).toBe(true);
143
+ expect(result?.message).toBe(
144
+ 'Permission mode set to: plan\nPermission mode: plan\nNo session-approved tools.',
145
+ );
146
+ expect(result?.data).toEqual({ mode: 'plan', sessionAllowed: [] });
147
+ expect(context.setPermissionMode).toHaveBeenCalledWith('plan');
148
+ });
149
+
150
+ it('rejects invalid permission modes without writing state', async () => {
151
+ const executor = new SystemCommandExecutor([
152
+ ...(createPermissionsCommandModule().systemCommands ?? []),
153
+ ]);
154
+ const context = createCommandHostContext();
155
+
156
+ const result = await executor.execute('permissions', context, 'invalid');
157
+
158
+ expect(result?.success).toBe(false);
159
+ expect(result?.message).toBe(
160
+ 'Invalid mode. Valid: plan | default | acceptEdits | bypassPermissions',
161
+ );
162
+ expect(context.setPermissionMode).not.toHaveBeenCalled();
163
+ });
164
+ });
@@ -0,0 +1,6 @@
1
+ export {
2
+ createPermissionsCommandEntry,
3
+ createPermissionsCommandModule,
4
+ PermissionsCommandSource,
5
+ } from './permissions-command-module.js';
6
+ export { executePermissionsCommand } from './permissions-command.js';
@@ -0,0 +1,56 @@
1
+ import type {
2
+ ICommand,
3
+ ICommandModule,
4
+ ICommandSource,
5
+ ISystemCommand,
6
+ } from '@robota-sdk/agent-framework';
7
+ import {
8
+ buildPermissionModeSubcommands,
9
+ PERMISSION_MODE_ARGUMENT_HINT,
10
+ PERMISSIONS_COMMAND_DESCRIPTION,
11
+ } from '@robota-sdk/agent-framework';
12
+ import { executePermissionsCommand } from './permissions-command.js';
13
+
14
+ export function createPermissionsCommandEntry(): ICommand {
15
+ return {
16
+ name: 'permissions',
17
+ displayName: 'Permissions',
18
+ description: PERMISSIONS_COMMAND_DESCRIPTION,
19
+ source: 'permissions',
20
+ argumentHint: PERMISSION_MODE_ARGUMENT_HINT,
21
+ subcommands: buildPermissionModeSubcommands('permissions'),
22
+ modelInvocable: false,
23
+ };
24
+ }
25
+
26
+ function createPermissionsSystemCommand(): ISystemCommand {
27
+ const entry = createPermissionsCommandEntry();
28
+ return {
29
+ name: entry.name,
30
+ displayName: entry.displayName,
31
+ description: entry.description,
32
+ requiresPermission: true,
33
+ userInvocable: true,
34
+ modelInvocable: false,
35
+ argumentHint: entry.argumentHint,
36
+ subcommands: entry.subcommands,
37
+ lifecycle: 'inline',
38
+ execute: executePermissionsCommand,
39
+ };
40
+ }
41
+
42
+ export class PermissionsCommandSource implements ICommandSource {
43
+ readonly name = 'permissions';
44
+
45
+ getCommands(): ICommand[] {
46
+ return [createPermissionsCommandEntry()];
47
+ }
48
+ }
49
+
50
+ export function createPermissionsCommandModule(): ICommandModule {
51
+ return {
52
+ name: 'agent-command-permissions',
53
+ commandSources: [new PermissionsCommandSource()],
54
+ systemCommands: [createPermissionsSystemCommand()],
55
+ };
56
+ }
@@ -0,0 +1,45 @@
1
+ import type { ICommandHostContext, ICommandResult } from '@robota-sdk/agent-framework';
2
+ import {
3
+ formatCommandPermissionsMessage,
4
+ formatInvalidPermissionModeMessage,
5
+ isPermissionMode,
6
+ parsePermissionModeArgument,
7
+ readCommandPermissionsState,
8
+ writeCommandPermissionMode,
9
+ } from '@robota-sdk/agent-framework';
10
+
11
+ export function executePermissionsCommand(
12
+ context: ICommandHostContext,
13
+ args: string,
14
+ ): ICommandResult {
15
+ const arg = parsePermissionModeArgument(args);
16
+ if (arg !== undefined) {
17
+ if (!isPermissionMode(arg)) {
18
+ return {
19
+ message: formatInvalidPermissionModeMessage(),
20
+ success: false,
21
+ };
22
+ }
23
+
24
+ writeCommandPermissionMode(context, arg);
25
+ const state = readCommandPermissionsState(context);
26
+ return {
27
+ message: `Permission mode set to: ${arg}\n${formatCommandPermissionsMessage(state)}`,
28
+ success: true,
29
+ data: {
30
+ mode: state.mode,
31
+ sessionAllowed: state.sessionAllowed,
32
+ },
33
+ };
34
+ }
35
+
36
+ const state = readCommandPermissionsState(context);
37
+ return {
38
+ message: formatCommandPermissionsMessage(state),
39
+ success: true,
40
+ data: {
41
+ mode: state.mode,
42
+ sessionAllowed: state.sessionAllowed,
43
+ },
44
+ };
45
+ }