cli4ai 1.0.3 → 1.1.0

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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cli4ai",
3
- "version": "1.0.3",
3
+ "version": "1.1.0",
4
4
  "description": "The package manager for AI CLI tools - cli4ai.com",
5
5
  "type": "module",
6
6
  "bin": {
@@ -16,7 +16,8 @@
16
16
  "dependencies": {
17
17
  "commander": "^14.0.0",
18
18
  "cron-parser": "^4.9.0",
19
- "semver": "^7.6.0"
19
+ "semver": "^7.6.0",
20
+ "yaml": "^2.8.2"
20
21
  },
21
22
  "devDependencies": {
22
23
  "@types/node": "^22.0.0",
package/src/cli.ts CHANGED
@@ -34,6 +34,16 @@ import {
34
34
  schedulerHistoryCommand,
35
35
  schedulerRunCommand
36
36
  } from './commands/scheduler.js';
37
+ import {
38
+ remotesListCommand,
39
+ remotesAddCommand,
40
+ remotesUpdateCommand,
41
+ remotesRemoveCommand,
42
+ remotesShowCommand,
43
+ remotesTestCommand,
44
+ remotesPackagesCommand
45
+ } from './commands/remotes.js';
46
+ import { serveCommand } from './commands/serve.js';
37
47
 
38
48
  export function createProgram(): Command {
39
49
  const program = new Command()
@@ -94,6 +104,10 @@ export function createProgram(): Command {
94
104
  .command('run <package> [command] [args...]')
95
105
  .description('Run a tool command')
96
106
  .option('-e, --env <vars...>', 'Environment variables (KEY=value)')
107
+ .option('--scope <level>', 'Permission scope: read, write, or full (default: full)')
108
+ .option('--sandbox', 'Run in sandboxed environment with restricted file system access')
109
+ .option('-r, --remote <name>', 'Execute on a remote cli4ai service')
110
+ .option('--timeout <ms>', 'Timeout in milliseconds')
97
111
  // Allow passing tool flags through (e.g. `cli4ai run chrome screenshot --full-page`)
98
112
  .allowUnknownOption(true)
99
113
  .addHelpText('after', `
@@ -101,10 +115,23 @@ export function createProgram(): Command {
101
115
  Examples:
102
116
  cli4ai run github trending
103
117
  cli4ai run chrome screenshot https://example.com --full-page
118
+ cli4ai run github list-issues --scope read
119
+ cli4ai run untrusted-pkg process --sandbox
120
+ cli4ai run browser screenshot --remote chrome-server
104
121
 
105
122
  Pass-through:
106
123
  Use "--" to pass flags that would otherwise be parsed by cli4ai:
107
124
  cli4ai run <pkg> <cmd> -- --help
125
+
126
+ Security:
127
+ --scope read Only allow read operations (no mutations)
128
+ --scope write Allow write operations
129
+ --scope full Full access (default)
130
+ --sandbox Restrict file system access to temp directories
131
+
132
+ Remote Execution:
133
+ --remote <name> Execute on a configured remote service
134
+ Use "cli4ai remotes add" to configure remotes
108
135
  `)
109
136
  .action(withErrorHandling(runCommand));
110
137
 
@@ -236,7 +263,7 @@ Pass-through:
236
263
  .command('create <name>')
237
264
  .description('Create a new routine')
238
265
  .option('-g, --global', 'Create routine globally')
239
- .option('--type <type>', 'Routine type (bash, json)', 'bash')
266
+ .option('--type <type>', 'Routine type (yaml, json, bash)', 'yaml')
240
267
  .action(withErrorHandling(routinesCreateCommand));
241
268
 
242
269
  routines
@@ -302,5 +329,81 @@ Pass-through:
302
329
  .description('Manually trigger a scheduled routine')
303
330
  .action(withErrorHandling(schedulerRunCommand));
304
331
 
332
+ // ═══════════════════════════════════════════════════════════════════════════
333
+ // REMOTE EXECUTION
334
+ // ═══════════════════════════════════════════════════════════════════════════
335
+
336
+ program
337
+ .command('serve')
338
+ .description('Start cli4ai as a remote service')
339
+ .option('-p, --port <port>', 'Port to listen on', '4100')
340
+ .option('-H, --host <host>', 'Host to bind to', '0.0.0.0')
341
+ .option('-k, --api-key <key>', 'API key for authentication')
342
+ .option('--scope <levels>', 'Comma-separated allowed scopes (read,write,full)')
343
+ .option('--cwd <dir>', 'Working directory for execution')
344
+ .addHelpText('after', `
345
+
346
+ Examples:
347
+ cli4ai serve # Start on port 4100
348
+ cli4ai serve --port 8080 # Start on port 8080
349
+ cli4ai serve --api-key mysecretkey # Require API key
350
+ cli4ai serve --scope read,write # Restrict to read/write operations
351
+
352
+ The service exposes these endpoints:
353
+ GET /health - Service health check
354
+ GET /packages - List available packages
355
+ GET /packages/:name - Get package info
356
+ POST /run - Execute a tool
357
+ POST /routines/:name/run - Run a routine
358
+ `)
359
+ .action(withErrorHandling(serveCommand));
360
+
361
+ const remotes = program
362
+ .command('remotes')
363
+ .description('Manage remote cli4ai services');
364
+
365
+ remotes
366
+ .command('list')
367
+ .alias('ls')
368
+ .description('List configured remotes')
369
+ .action(withErrorHandling(remotesListCommand));
370
+
371
+ remotes
372
+ .command('add <name> <url>')
373
+ .description('Add a remote service')
374
+ .option('-k, --api-key <key>', 'API key for authentication')
375
+ .option('-d, --description <desc>', 'Description of this remote')
376
+ .option('--no-test', 'Skip connection test')
377
+ .action(withErrorHandling(remotesAddCommand));
378
+
379
+ remotes
380
+ .command('update <name>')
381
+ .description('Update a remote service')
382
+ .option('-u, --url <url>', 'New URL')
383
+ .option('-k, --api-key <key>', 'New API key')
384
+ .option('-d, --description <desc>', 'New description')
385
+ .action(withErrorHandling(remotesUpdateCommand));
386
+
387
+ remotes
388
+ .command('remove <name>')
389
+ .alias('rm')
390
+ .description('Remove a remote')
391
+ .action(withErrorHandling(remotesRemoveCommand));
392
+
393
+ remotes
394
+ .command('show <name>')
395
+ .description('Show remote details')
396
+ .action(withErrorHandling(remotesShowCommand));
397
+
398
+ remotes
399
+ .command('test <name>')
400
+ .description('Test connection to a remote')
401
+ .action(withErrorHandling(remotesTestCommand));
402
+
403
+ remotes
404
+ .command('packages <name>')
405
+ .description('List packages on a remote')
406
+ .action(withErrorHandling(remotesPackagesCommand));
407
+
305
408
  return program;
306
409
  }
@@ -0,0 +1,253 @@
1
+ /**
2
+ * Remote hosts management command.
3
+ *
4
+ * Manage connections to remote cli4ai instances for distributed execution.
5
+ */
6
+
7
+ import { output, outputError, log } from '../lib/cli.js';
8
+ import {
9
+ getRemotes,
10
+ getRemote,
11
+ addRemote,
12
+ updateRemote,
13
+ removeRemote,
14
+ RemoteNotFoundError,
15
+ RemoteAlreadyExistsError,
16
+ InvalidRemoteUrlError
17
+ } from '../core/remotes.js';
18
+ import { testRemoteConnection, remoteListPackages } from '../core/remote-client.js';
19
+
20
+ // ═══════════════════════════════════════════════════════════════════════════
21
+ // LIST REMOTES
22
+ // ═══════════════════════════════════════════════════════════════════════════
23
+
24
+ export async function remotesListCommand(): Promise<void> {
25
+ const remotes = getRemotes();
26
+
27
+ if (remotes.length === 0) {
28
+ output({ remotes: [], message: 'No remotes configured. Use "cli4ai remotes add <name> <url>" to add one.' });
29
+ return;
30
+ }
31
+
32
+ output({
33
+ remotes: remotes.map(r => ({
34
+ name: r.name,
35
+ url: r.url,
36
+ description: r.description,
37
+ hasApiKey: !!r.apiKey,
38
+ addedAt: r.addedAt,
39
+ lastConnected: r.lastConnected
40
+ }))
41
+ });
42
+ }
43
+
44
+ // ═══════════════════════════════════════════════════════════════════════════
45
+ // ADD REMOTE
46
+ // ═══════════════════════════════════════════════════════════════════════════
47
+
48
+ export interface AddRemoteOptions {
49
+ apiKey?: string;
50
+ description?: string;
51
+ test?: boolean;
52
+ }
53
+
54
+ export async function remotesAddCommand(
55
+ name: string,
56
+ url: string,
57
+ options: AddRemoteOptions
58
+ ): Promise<void> {
59
+ try {
60
+ const remote = addRemote(name, url, {
61
+ apiKey: options.apiKey,
62
+ description: options.description
63
+ });
64
+
65
+ log(`Added remote: ${remote.name} -> ${remote.url}`);
66
+
67
+ // Test connection if requested
68
+ if (options.test !== false) {
69
+ log('Testing connection...');
70
+ const result = await testRemoteConnection(name);
71
+
72
+ if (result.success) {
73
+ log(`Connection successful: ${result.message}`);
74
+ output({
75
+ remote: {
76
+ name: remote.name,
77
+ url: remote.url,
78
+ description: remote.description,
79
+ hasApiKey: !!remote.apiKey
80
+ },
81
+ connectionTest: result
82
+ });
83
+ } else {
84
+ log(`Warning: Connection failed - ${result.message}`);
85
+ log('The remote was added but could not be reached. Check the URL and try again.');
86
+ output({
87
+ remote: {
88
+ name: remote.name,
89
+ url: remote.url,
90
+ description: remote.description,
91
+ hasApiKey: !!remote.apiKey
92
+ },
93
+ connectionTest: result
94
+ });
95
+ }
96
+ } else {
97
+ output({
98
+ remote: {
99
+ name: remote.name,
100
+ url: remote.url,
101
+ description: remote.description,
102
+ hasApiKey: !!remote.apiKey
103
+ }
104
+ });
105
+ }
106
+ } catch (err) {
107
+ if (err instanceof RemoteAlreadyExistsError) {
108
+ outputError('ALREADY_EXISTS', `Remote "${err.remoteName}" already exists. Use "cli4ai remotes update" to modify it.`);
109
+ }
110
+ if (err instanceof InvalidRemoteUrlError) {
111
+ outputError('INVALID_INPUT', `Invalid URL "${err.url}": ${err.reason}`);
112
+ }
113
+ throw err;
114
+ }
115
+ }
116
+
117
+ // ═══════════════════════════════════════════════════════════════════════════
118
+ // UPDATE REMOTE
119
+ // ═══════════════════════════════════════════════════════════════════════════
120
+
121
+ export interface UpdateRemoteOptions {
122
+ url?: string;
123
+ apiKey?: string;
124
+ description?: string;
125
+ }
126
+
127
+ export async function remotesUpdateCommand(
128
+ name: string,
129
+ options: UpdateRemoteOptions
130
+ ): Promise<void> {
131
+ try {
132
+ // Check if any update provided
133
+ if (!options.url && !options.apiKey && !options.description) {
134
+ outputError('INVALID_INPUT', 'No updates provided. Use --url, --api-key, or --description.');
135
+ }
136
+
137
+ const remote = updateRemote(name, options);
138
+
139
+ output({
140
+ updated: true,
141
+ remote: {
142
+ name: remote.name,
143
+ url: remote.url,
144
+ description: remote.description,
145
+ hasApiKey: !!remote.apiKey
146
+ }
147
+ });
148
+ } catch (err) {
149
+ if (err instanceof RemoteNotFoundError) {
150
+ outputError('NOT_FOUND', `Remote "${err.remoteName}" not found`);
151
+ }
152
+ if (err instanceof InvalidRemoteUrlError) {
153
+ outputError('INVALID_INPUT', `Invalid URL "${err.url}": ${err.reason}`);
154
+ }
155
+ throw err;
156
+ }
157
+ }
158
+
159
+ // ═══════════════════════════════════════════════════════════════════════════
160
+ // REMOVE REMOTE
161
+ // ═══════════════════════════════════════════════════════════════════════════
162
+
163
+ export async function remotesRemoveCommand(name: string): Promise<void> {
164
+ try {
165
+ removeRemote(name);
166
+ output({ removed: true, name });
167
+ } catch (err) {
168
+ if (err instanceof RemoteNotFoundError) {
169
+ outputError('NOT_FOUND', `Remote "${err.remoteName}" not found`);
170
+ }
171
+ throw err;
172
+ }
173
+ }
174
+
175
+ // ═══════════════════════════════════════════════════════════════════════════
176
+ // SHOW REMOTE
177
+ // ═══════════════════════════════════════════════════════════════════════════
178
+
179
+ export async function remotesShowCommand(name: string): Promise<void> {
180
+ const remote = getRemote(name);
181
+
182
+ if (!remote) {
183
+ outputError('NOT_FOUND', `Remote "${name}" not found`);
184
+ }
185
+
186
+ output({
187
+ name: remote.name,
188
+ url: remote.url,
189
+ description: remote.description,
190
+ hasApiKey: !!remote.apiKey,
191
+ addedAt: remote.addedAt,
192
+ lastConnected: remote.lastConnected
193
+ });
194
+ }
195
+
196
+ // ═══════════════════════════════════════════════════════════════════════════
197
+ // TEST REMOTE
198
+ // ═══════════════════════════════════════════════════════════════════════════
199
+
200
+ export async function remotesTestCommand(name: string): Promise<void> {
201
+ const remote = getRemote(name);
202
+
203
+ if (!remote) {
204
+ outputError('NOT_FOUND', `Remote "${name}" not found`);
205
+ }
206
+
207
+ log(`Testing connection to ${remote.name} (${remote.url})...`);
208
+
209
+ const result = await testRemoteConnection(name);
210
+
211
+ if (result.success) {
212
+ log(`Connection successful!`);
213
+ output({
214
+ name,
215
+ url: remote.url,
216
+ connected: true,
217
+ ...result.details
218
+ });
219
+ } else {
220
+ log(`Connection failed: ${result.message}`);
221
+ output({
222
+ name,
223
+ url: remote.url,
224
+ connected: false,
225
+ error: result.message
226
+ });
227
+ }
228
+ }
229
+
230
+ // ═══════════════════════════════════════════════════════════════════════════
231
+ // LIST PACKAGES ON REMOTE
232
+ // ═══════════════════════════════════════════════════════════════════════════
233
+
234
+ export async function remotesPackagesCommand(name: string): Promise<void> {
235
+ const remote = getRemote(name);
236
+
237
+ if (!remote) {
238
+ outputError('NOT_FOUND', `Remote "${name}" not found`);
239
+ }
240
+
241
+ try {
242
+ const result = await remoteListPackages(name);
243
+ output({
244
+ remote: name,
245
+ packages: result.packages
246
+ });
247
+ } catch (err) {
248
+ if (err instanceof Error) {
249
+ outputError('API_ERROR', `Failed to list packages on remote "${name}": ${err.message}`);
250
+ }
251
+ throw err;
252
+ }
253
+ }
@@ -5,6 +5,7 @@
5
5
  import { spawn, spawnSync } from 'child_process';
6
6
  import { readFileSync, writeFileSync, existsSync, chmodSync, rmSync } from 'fs';
7
7
  import { resolve } from 'path';
8
+ import { stringify as stringifyYaml } from 'yaml';
8
9
  import { output, outputError, log } from '../lib/cli.js';
9
10
  import { ensureCli4aiHome, ensureLocalDir, ROUTINES_DIR, LOCAL_ROUTINES_DIR } from '../core/config.js';
10
11
  import { getGlobalRoutines, getLocalRoutines, resolveRoutine, validateRoutineName, type RoutineInfo } from '../core/routines.js';
@@ -23,7 +24,7 @@ interface RoutinesRunOptions {
23
24
 
24
25
  interface RoutinesCreateOptions {
25
26
  global?: boolean;
26
- type?: 'json' | 'bash';
27
+ type?: 'yaml' | 'json' | 'bash';
27
28
  }
28
29
 
29
30
  function ensureValidRoutineName(name: string): void {
@@ -139,22 +140,37 @@ function getTargetDir(global: boolean, projectDir: string): string {
139
140
  export async function routinesCreateCommand(name: string, options: RoutinesCreateOptions): Promise<void> {
140
141
  ensureValidRoutineName(name);
141
142
  const projectDir = process.cwd();
142
- const type = options.type ?? 'bash';
143
- if (type !== 'bash' && type !== 'json') {
143
+ const type = options.type ?? 'yaml';
144
+ if (type !== 'yaml' && type !== 'bash' && type !== 'json') {
144
145
  outputError('INVALID_INPUT', `Invalid routine type: ${type}`, {
145
- allowed: ['bash', 'json']
146
+ allowed: ['yaml', 'json', 'bash']
146
147
  });
147
148
  }
148
149
  const dir = getTargetDir(!!options.global, projectDir);
149
150
 
150
- const suffix = type === 'json' ? '.routine.json' : '.routine.sh';
151
+ const suffixMap: Record<string, string> = {
152
+ yaml: '.routine.yaml',
153
+ json: '.routine.json',
154
+ bash: '.routine.sh'
155
+ };
156
+ const suffix = suffixMap[type];
151
157
  const filePath = resolve(dir, `${name}${suffix}`);
152
158
 
153
159
  if (existsSync(filePath)) {
154
160
  outputError('INVALID_INPUT', `Routine already exists: ${name}`, { path: filePath });
155
161
  }
156
162
 
157
- if (type === 'json') {
163
+ if (type === 'yaml') {
164
+ const template = {
165
+ version: 1,
166
+ name,
167
+ description: 'TODO: describe this routine',
168
+ vars: {},
169
+ steps: [],
170
+ result: '{{steps}}'
171
+ };
172
+ writeFileSync(filePath, stringifyYaml(template));
173
+ } else if (type === 'json') {
158
174
  const template = {
159
175
  version: 1,
160
176
  name,
@@ -208,8 +224,11 @@ export async function routinesRunCommand(
208
224
 
209
225
  const vars = parseVars(options.var);
210
226
 
227
+ // Structured routines (YAML or JSON) use the routine engine
228
+ const isStructured = routine.kind === 'yaml' || routine.kind === 'json';
229
+
211
230
  if (options.dryRun) {
212
- if (routine.kind === 'json') {
231
+ if (isStructured) {
213
232
  try {
214
233
  const def = loadRoutineDefinition(routine.path);
215
234
  const plan = await dryRunRoutine(def, vars, process.cwd());
@@ -252,7 +271,7 @@ export async function routinesRunCommand(
252
271
  return;
253
272
  }
254
273
 
255
- if (routine.kind === 'json') {
274
+ if (isStructured) {
256
275
  try {
257
276
  const def = loadRoutineDefinition(routine.path);
258
277
  const summary = await runRoutine(def, vars, process.cwd());
@@ -2,11 +2,17 @@
2
2
  * cli4ai run - Execute a tool command
3
3
  */
4
4
 
5
- import { outputError } from '../lib/cli.js';
6
- import { executeTool, ExecuteToolError } from '../core/execute.js';
5
+ import { output, outputError, log } from '../lib/cli.js';
6
+ import { executeTool, ExecuteToolError, type ScopeLevel } from '../core/execute.js';
7
+ import { remoteRunTool, RemoteConnectionError, RemoteApiError } from '../core/remote-client.js';
8
+ import { getRemote } from '../core/remotes.js';
7
9
 
8
10
  interface RunOptions {
9
11
  env?: string[];
12
+ scope?: string;
13
+ sandbox?: boolean;
14
+ remote?: string;
15
+ timeout?: string;
10
16
  }
11
17
 
12
18
  export async function runCommand(
@@ -26,6 +32,77 @@ export async function runCommand(
26
32
  }
27
33
  }
28
34
 
35
+ // Validate scope option
36
+ let scope: ScopeLevel = 'full';
37
+ if (options.scope) {
38
+ const validScopes: ScopeLevel[] = ['read', 'write', 'full'];
39
+ if (!validScopes.includes(options.scope as ScopeLevel)) {
40
+ outputError('INVALID_INPUT', `Invalid scope: ${options.scope}`, {
41
+ validScopes,
42
+ hint: 'Use --scope read, --scope write, or --scope full'
43
+ });
44
+ }
45
+ scope = options.scope as ScopeLevel;
46
+ }
47
+
48
+ // Parse timeout
49
+ let timeout: number | undefined;
50
+ if (options.timeout) {
51
+ timeout = parseInt(options.timeout, 10);
52
+ if (isNaN(timeout) || timeout < 0) {
53
+ outputError('INVALID_INPUT', 'Timeout must be a positive number (milliseconds)');
54
+ }
55
+ }
56
+
57
+ // Remote execution
58
+ if (options.remote) {
59
+ const remote = getRemote(options.remote);
60
+ if (!remote) {
61
+ outputError('NOT_FOUND', `Remote "${options.remote}" not found`, {
62
+ hint: 'Use "cli4ai remotes add <name> <url>" to configure a remote'
63
+ });
64
+ }
65
+
66
+ log(`Executing on remote: ${remote.name} (${remote.url})`);
67
+
68
+ try {
69
+ const result = await remoteRunTool(options.remote, {
70
+ package: packageName,
71
+ command,
72
+ args,
73
+ env: Object.keys(extraEnv).length > 0 ? extraEnv : undefined,
74
+ timeout,
75
+ scope
76
+ });
77
+
78
+ // Output stdout/stderr
79
+ if (result.stdout) {
80
+ process.stdout.write(result.stdout);
81
+ }
82
+ if (result.stderr) {
83
+ process.stderr.write(result.stderr);
84
+ }
85
+
86
+ process.exitCode = result.exitCode;
87
+
88
+ if (!result.success && result.error) {
89
+ log(`Remote error: ${result.error.message}`);
90
+ }
91
+
92
+ return;
93
+ } catch (err) {
94
+ if (err instanceof RemoteConnectionError) {
95
+ outputError('NETWORK_ERROR', err.message, { remote: options.remote, url: remote.url });
96
+ }
97
+ if (err instanceof RemoteApiError) {
98
+ outputError(err.code, err.message, err.details);
99
+ }
100
+ const message = err instanceof Error ? err.message : String(err);
101
+ outputError('API_ERROR', message);
102
+ }
103
+ }
104
+
105
+ // Local execution
29
106
  try {
30
107
  const result = await executeTool({
31
108
  packageName,
@@ -33,7 +110,10 @@ export async function runCommand(
33
110
  args,
34
111
  cwd: process.cwd(),
35
112
  env: extraEnv,
36
- capture: 'inherit'
113
+ capture: 'inherit',
114
+ timeoutMs: timeout,
115
+ scope,
116
+ sandbox: options.sandbox ?? false
37
117
  });
38
118
  process.exitCode = result.exitCode;
39
119
  return;
@@ -0,0 +1,66 @@
1
+ /**
2
+ * Serve command - Start cli4ai as a remote service.
3
+ *
4
+ * Allows other cli4ai instances to execute tools on this machine.
5
+ */
6
+
7
+ import { output, outputError, log } from '../lib/cli.js';
8
+ import { startService, type StartServiceOptions } from '../server/service.js';
9
+ import type { ScopeLevel } from '../core/execute.js';
10
+
11
+ export interface ServeCommandOptions {
12
+ port?: string;
13
+ host?: string;
14
+ apiKey?: string;
15
+ scope?: string;
16
+ cwd?: string;
17
+ }
18
+
19
+ export async function serveCommand(options: ServeCommandOptions): Promise<void> {
20
+ const serviceOptions: StartServiceOptions = {
21
+ cwd: options.cwd ?? process.cwd()
22
+ };
23
+
24
+ // Parse port
25
+ if (options.port) {
26
+ const port = parseInt(options.port, 10);
27
+ if (isNaN(port) || port < 1 || port > 65535) {
28
+ outputError('INVALID_INPUT', 'Port must be a number between 1 and 65535');
29
+ }
30
+ serviceOptions.port = port;
31
+ }
32
+
33
+ // Parse host
34
+ if (options.host) {
35
+ serviceOptions.host = options.host;
36
+ }
37
+
38
+ // API key
39
+ if (options.apiKey) {
40
+ serviceOptions.apiKey = options.apiKey;
41
+ }
42
+
43
+ // Parse allowed scopes
44
+ if (options.scope) {
45
+ const scopes = options.scope.split(',').map(s => s.trim()) as ScopeLevel[];
46
+ const validScopes: ScopeLevel[] = ['read', 'write', 'full'];
47
+
48
+ for (const scope of scopes) {
49
+ if (!validScopes.includes(scope)) {
50
+ outputError('INVALID_INPUT', `Invalid scope "${scope}". Must be one of: read, write, full`);
51
+ }
52
+ }
53
+
54
+ serviceOptions.allowedScopes = scopes;
55
+ }
56
+
57
+ try {
58
+ await startService(serviceOptions);
59
+ output({ status: 'stopped' });
60
+ } catch (err) {
61
+ if (err instanceof Error) {
62
+ outputError('API_ERROR', err.message);
63
+ }
64
+ throw err;
65
+ }
66
+ }