cli4ai 1.2.0 → 1.2.2

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 (113) hide show
  1. package/README.md +39 -0
  2. package/dist/bin.d.ts +6 -0
  3. package/dist/bin.js +105 -0
  4. package/dist/cli.d.ts +5 -0
  5. package/dist/cli.js +335 -0
  6. package/dist/commands/add.d.ts +11 -0
  7. package/dist/commands/add.js +464 -0
  8. package/dist/commands/browse.d.ts +4 -0
  9. package/dist/commands/browse.js +382 -0
  10. package/dist/commands/config.d.ts +10 -0
  11. package/dist/commands/config.js +121 -0
  12. package/dist/commands/info.d.ts +9 -0
  13. package/dist/commands/info.js +125 -0
  14. package/dist/commands/init.d.ts +10 -0
  15. package/dist/commands/init.js +458 -0
  16. package/dist/commands/list.d.ts +10 -0
  17. package/dist/commands/list.js +76 -0
  18. package/dist/commands/mcp-config.d.ts +10 -0
  19. package/dist/commands/mcp-config.js +49 -0
  20. package/dist/commands/remotes.d.ts +22 -0
  21. package/dist/commands/remotes.js +196 -0
  22. package/dist/commands/remove.d.ts +8 -0
  23. package/dist/commands/remove.js +61 -0
  24. package/dist/commands/routines.d.ts +29 -0
  25. package/dist/commands/routines.js +363 -0
  26. package/dist/commands/run.d.ts +12 -0
  27. package/dist/commands/run.js +104 -0
  28. package/dist/commands/scheduler.d.ts +27 -0
  29. package/dist/commands/scheduler.js +350 -0
  30. package/dist/commands/search.d.ts +9 -0
  31. package/dist/commands/search.js +162 -0
  32. package/dist/commands/secrets.d.ts +28 -0
  33. package/dist/commands/secrets.js +236 -0
  34. package/dist/commands/serve.d.ts +13 -0
  35. package/dist/commands/serve.js +49 -0
  36. package/dist/commands/start.d.ts +8 -0
  37. package/dist/commands/start.js +27 -0
  38. package/dist/commands/update.d.ts +17 -0
  39. package/dist/commands/update.js +210 -0
  40. package/dist/core/config.d.ts +91 -0
  41. package/dist/core/config.js +738 -0
  42. package/dist/core/execute.d.ts +51 -0
  43. package/dist/core/execute.js +475 -0
  44. package/dist/core/link.d.ts +39 -0
  45. package/dist/core/link.js +214 -0
  46. package/dist/core/lockfile.d.ts +63 -0
  47. package/dist/core/lockfile.js +140 -0
  48. package/dist/core/manifest.d.ts +96 -0
  49. package/dist/core/manifest.js +224 -0
  50. package/dist/core/registry.d.ts +74 -0
  51. package/dist/core/registry.js +116 -0
  52. package/dist/core/remote-client.d.ts +98 -0
  53. package/dist/core/remote-client.js +252 -0
  54. package/dist/core/remotes.d.ts +88 -0
  55. package/dist/core/remotes.js +206 -0
  56. package/dist/core/routine-engine.d.ts +124 -0
  57. package/dist/core/routine-engine.js +699 -0
  58. package/dist/core/routines.d.ts +36 -0
  59. package/dist/core/routines.js +132 -0
  60. package/dist/core/scheduler-daemon.d.ts +10 -0
  61. package/dist/core/scheduler-daemon.js +77 -0
  62. package/dist/core/scheduler.d.ts +131 -0
  63. package/dist/core/scheduler.js +492 -0
  64. package/dist/core/secrets.d.ts +48 -0
  65. package/dist/core/secrets.js +384 -0
  66. package/dist/lib/cli.d.ts +84 -0
  67. package/dist/lib/cli.js +216 -0
  68. package/dist/mcp/adapter.d.ts +35 -0
  69. package/dist/mcp/adapter.js +94 -0
  70. package/dist/mcp/config-gen.d.ts +31 -0
  71. package/dist/mcp/config-gen.js +75 -0
  72. package/dist/mcp/server.d.ts +41 -0
  73. package/dist/mcp/server.js +296 -0
  74. package/dist/server/service.d.ts +85 -0
  75. package/dist/server/service.js +304 -0
  76. package/package.json +6 -3
  77. package/src/bin.ts +0 -118
  78. package/src/cli.ts +0 -412
  79. package/src/commands/add.ts +0 -562
  80. package/src/commands/browse.ts +0 -449
  81. package/src/commands/config.ts +0 -154
  82. package/src/commands/info.ts +0 -133
  83. package/src/commands/init.ts +0 -514
  84. package/src/commands/list.ts +0 -95
  85. package/src/commands/mcp-config.ts +0 -69
  86. package/src/commands/remotes.ts +0 -253
  87. package/src/commands/remove.ts +0 -78
  88. package/src/commands/routines.ts +0 -427
  89. package/src/commands/run.ts +0 -127
  90. package/src/commands/scheduler.ts +0 -438
  91. package/src/commands/search.ts +0 -185
  92. package/src/commands/secrets.ts +0 -292
  93. package/src/commands/serve.ts +0 -66
  94. package/src/commands/start.ts +0 -40
  95. package/src/commands/update.ts +0 -252
  96. package/src/core/config.ts +0 -845
  97. package/src/core/execute.ts +0 -569
  98. package/src/core/link.ts +0 -246
  99. package/src/core/lockfile.ts +0 -187
  100. package/src/core/manifest.ts +0 -327
  101. package/src/core/registry.ts +0 -165
  102. package/src/core/remote-client.ts +0 -419
  103. package/src/core/remotes.ts +0 -268
  104. package/src/core/routine-engine.ts +0 -895
  105. package/src/core/routines.ts +0 -171
  106. package/src/core/scheduler-daemon.ts +0 -94
  107. package/src/core/scheduler.ts +0 -606
  108. package/src/core/secrets.ts +0 -430
  109. package/src/lib/cli.ts +0 -261
  110. package/src/mcp/adapter.ts +0 -131
  111. package/src/mcp/config-gen.ts +0 -106
  112. package/src/mcp/server.ts +0 -365
  113. package/src/server/service.ts +0 -434
@@ -0,0 +1,104 @@
1
+ /**
2
+ * cli4ai run - Execute a tool command
3
+ */
4
+ import { outputError, log } from '../lib/cli.js';
5
+ import { executeTool, ExecuteToolError } from '../core/execute.js';
6
+ import { remoteRunTool, RemoteConnectionError, RemoteApiError } from '../core/remote-client.js';
7
+ import { getRemote } from '../core/remotes.js';
8
+ export async function runCommand(packageName, command, args, options) {
9
+ // Parse environment variables from options (-e KEY=value)
10
+ const extraEnv = {};
11
+ if (options.env) {
12
+ for (const envVar of options.env) {
13
+ const eqIndex = envVar.indexOf('=');
14
+ if (eqIndex > 0) {
15
+ extraEnv[envVar.slice(0, eqIndex)] = envVar.slice(eqIndex + 1);
16
+ }
17
+ }
18
+ }
19
+ // Validate scope option
20
+ let scope = 'full';
21
+ if (options.scope) {
22
+ const validScopes = ['read', 'write', 'full'];
23
+ if (!validScopes.includes(options.scope)) {
24
+ outputError('INVALID_INPUT', `Invalid scope: ${options.scope}`, {
25
+ validScopes,
26
+ hint: 'Use --scope read, --scope write, or --scope full'
27
+ });
28
+ }
29
+ scope = options.scope;
30
+ }
31
+ // Parse timeout
32
+ let timeout;
33
+ if (options.timeout) {
34
+ timeout = parseInt(options.timeout, 10);
35
+ if (isNaN(timeout) || timeout < 0) {
36
+ outputError('INVALID_INPUT', 'Timeout must be a positive number (milliseconds)');
37
+ }
38
+ }
39
+ // Remote execution
40
+ if (options.remote) {
41
+ const remote = getRemote(options.remote);
42
+ if (!remote) {
43
+ outputError('NOT_FOUND', `Remote "${options.remote}" not found`, {
44
+ hint: 'Use "cli4ai remotes add <name> <url>" to configure a remote'
45
+ });
46
+ }
47
+ log(`Executing on remote: ${remote.name} (${remote.url})`);
48
+ try {
49
+ const result = await remoteRunTool(options.remote, {
50
+ package: packageName,
51
+ command,
52
+ args,
53
+ env: Object.keys(extraEnv).length > 0 ? extraEnv : undefined,
54
+ timeout,
55
+ scope
56
+ });
57
+ // Output stdout/stderr
58
+ if (result.stdout) {
59
+ process.stdout.write(result.stdout);
60
+ }
61
+ if (result.stderr) {
62
+ process.stderr.write(result.stderr);
63
+ }
64
+ process.exitCode = result.exitCode;
65
+ if (!result.success && result.error) {
66
+ log(`Remote error: ${result.error.message}`);
67
+ }
68
+ return;
69
+ }
70
+ catch (err) {
71
+ if (err instanceof RemoteConnectionError) {
72
+ outputError('NETWORK_ERROR', err.message, { remote: options.remote, url: remote.url });
73
+ }
74
+ if (err instanceof RemoteApiError) {
75
+ outputError(err.code, err.message, err.details);
76
+ }
77
+ const message = err instanceof Error ? err.message : String(err);
78
+ outputError('API_ERROR', message);
79
+ }
80
+ }
81
+ // Local execution
82
+ try {
83
+ const result = await executeTool({
84
+ packageName,
85
+ command,
86
+ args,
87
+ cwd: process.cwd(),
88
+ env: extraEnv,
89
+ capture: 'inherit',
90
+ timeoutMs: timeout,
91
+ scope,
92
+ sandbox: options.sandbox ?? false
93
+ });
94
+ process.exitCode = result.exitCode;
95
+ return;
96
+ }
97
+ catch (err) {
98
+ if (err instanceof ExecuteToolError) {
99
+ outputError(err.code, err.message, err.details);
100
+ }
101
+ const message = err instanceof Error ? err.message : String(err);
102
+ outputError('API_ERROR', message);
103
+ }
104
+ }
@@ -0,0 +1,27 @@
1
+ /**
2
+ * Scheduler CLI commands.
3
+ *
4
+ * cli4ai scheduler start [--foreground] # Start daemon
5
+ * cli4ai scheduler stop # Stop daemon
6
+ * cli4ai scheduler status # Show status + upcoming runs
7
+ * cli4ai scheduler logs [routine] # View logs
8
+ * cli4ai scheduler history [routine] # View execution history
9
+ * cli4ai scheduler run <routine> # Manual trigger
10
+ */
11
+ interface SchedulerStartOptions {
12
+ foreground?: boolean;
13
+ }
14
+ export declare function schedulerStartCommand(options: SchedulerStartOptions): Promise<void>;
15
+ export declare function schedulerStopCommand(): Promise<void>;
16
+ export declare function schedulerStatusCommand(): Promise<void>;
17
+ interface SchedulerLogsOptions {
18
+ follow?: boolean;
19
+ lines?: number;
20
+ }
21
+ export declare function schedulerLogsCommand(options: SchedulerLogsOptions): Promise<void>;
22
+ interface SchedulerHistoryOptions {
23
+ limit?: number;
24
+ }
25
+ export declare function schedulerHistoryCommand(routineName?: string, options?: SchedulerHistoryOptions): Promise<void>;
26
+ export declare function schedulerRunCommand(routineName: string): Promise<void>;
27
+ export {};
@@ -0,0 +1,350 @@
1
+ /**
2
+ * Scheduler CLI commands.
3
+ *
4
+ * cli4ai scheduler start [--foreground] # Start daemon
5
+ * cli4ai scheduler stop # Stop daemon
6
+ * cli4ai scheduler status # Show status + upcoming runs
7
+ * cli4ai scheduler logs [routine] # View logs
8
+ * cli4ai scheduler history [routine] # View execution history
9
+ * cli4ai scheduler run <routine> # Manual trigger
10
+ */
11
+ import { spawn } from 'child_process';
12
+ import { readFileSync, existsSync } from 'fs';
13
+ import { resolve, dirname } from 'path';
14
+ import { fileURLToPath } from 'url';
15
+ import { output, outputError, log } from '../lib/cli.js';
16
+ import { isDaemonRunning, getDaemonPid, removeDaemonPid, loadSchedulerState, getRunHistory, Scheduler, SCHEDULER_LOG_FILE, SCHEDULER_PID_FILE } from '../core/scheduler.js';
17
+ import { getScheduledRoutines } from '../core/routines.js';
18
+ export async function schedulerStartCommand(options) {
19
+ // Check if already running
20
+ if (isDaemonRunning()) {
21
+ const pid = getDaemonPid();
22
+ outputError('INVALID_INPUT', `Scheduler is already running (PID ${pid})`, {
23
+ hint: 'Use "cli4ai scheduler stop" to stop it first'
24
+ });
25
+ return;
26
+ }
27
+ // Clean up stale PID file
28
+ if (existsSync(SCHEDULER_PID_FILE)) {
29
+ removeDaemonPid();
30
+ }
31
+ if (options.foreground) {
32
+ // Run in foreground (blocking)
33
+ log('Starting scheduler in foreground mode...');
34
+ log('Press Ctrl+C to stop');
35
+ const scheduler = new Scheduler({ projectDir: process.cwd() });
36
+ await scheduler.start();
37
+ // Keep running until signal
38
+ await new Promise((resolve) => {
39
+ process.on('SIGINT', async () => {
40
+ log('\nStopping scheduler...');
41
+ await scheduler.stop();
42
+ resolve();
43
+ });
44
+ process.on('SIGTERM', async () => {
45
+ await scheduler.stop();
46
+ resolve();
47
+ });
48
+ });
49
+ output({ status: 'stopped' });
50
+ return;
51
+ }
52
+ // Run as daemon (background)
53
+ const daemonScript = resolve(dirname(fileURLToPath(import.meta.url)), '../core/scheduler-daemon.ts');
54
+ // Spawn with detached mode
55
+ const child = spawn('npx', ['tsx', daemonScript, '--project-dir', process.cwd()], {
56
+ detached: true,
57
+ stdio: 'ignore',
58
+ env: {
59
+ ...process.env,
60
+ CLI4AI_DAEMON: 'true'
61
+ }
62
+ });
63
+ // Unref to allow parent to exit
64
+ child.unref();
65
+ // Wait a moment for the daemon to write its PID
66
+ await new Promise(r => setTimeout(r, 500));
67
+ const pid = getDaemonPid();
68
+ if (pid && isDaemonRunning()) {
69
+ log(`Scheduler daemon started (PID ${pid})`);
70
+ output({ status: 'started', pid });
71
+ }
72
+ else {
73
+ outputError('API_ERROR', 'Failed to start scheduler daemon', {
74
+ hint: 'Check logs with "cli4ai scheduler logs"'
75
+ });
76
+ }
77
+ }
78
+ // ═══════════════════════════════════════════════════════════════════════════
79
+ // SCHEDULER STOP
80
+ // ═══════════════════════════════════════════════════════════════════════════
81
+ export async function schedulerStopCommand() {
82
+ const pid = getDaemonPid();
83
+ if (!pid) {
84
+ outputError('NOT_FOUND', 'Scheduler daemon is not running');
85
+ return;
86
+ }
87
+ if (!isDaemonRunning()) {
88
+ // Clean up stale PID file
89
+ removeDaemonPid();
90
+ log('Scheduler was not running (cleaned up stale PID file)');
91
+ output({ status: 'stopped', pid: null });
92
+ return;
93
+ }
94
+ log(`Stopping scheduler daemon (PID ${pid})...`);
95
+ try {
96
+ // Send SIGTERM for graceful shutdown
97
+ process.kill(pid, 'SIGTERM');
98
+ // Wait for process to exit (up to 10 seconds)
99
+ const maxWait = 10000;
100
+ const checkInterval = 100;
101
+ let waited = 0;
102
+ while (waited < maxWait) {
103
+ await new Promise(r => setTimeout(r, checkInterval));
104
+ waited += checkInterval;
105
+ if (!isDaemonRunning()) {
106
+ break;
107
+ }
108
+ }
109
+ if (isDaemonRunning()) {
110
+ // Force kill if still running
111
+ log('Scheduler did not stop gracefully, sending SIGKILL...');
112
+ process.kill(pid, 'SIGKILL');
113
+ await new Promise(r => setTimeout(r, 500));
114
+ }
115
+ removeDaemonPid();
116
+ log('Scheduler daemon stopped');
117
+ output({ status: 'stopped', pid });
118
+ }
119
+ catch (err) {
120
+ if (err.code === 'ESRCH') {
121
+ // Process doesn't exist
122
+ removeDaemonPid();
123
+ log('Scheduler was not running (cleaned up stale PID file)');
124
+ output({ status: 'stopped', pid: null });
125
+ }
126
+ else {
127
+ outputError('API_ERROR', `Failed to stop scheduler: ${err instanceof Error ? err.message : String(err)}`);
128
+ }
129
+ }
130
+ }
131
+ // ═══════════════════════════════════════════════════════════════════════════
132
+ // SCHEDULER STATUS
133
+ // ═══════════════════════════════════════════════════════════════════════════
134
+ export async function schedulerStatusCommand() {
135
+ const pid = getDaemonPid();
136
+ const running = isDaemonRunning();
137
+ // Get scheduled routines
138
+ const scheduledRoutines = getScheduledRoutines(process.cwd());
139
+ const state = loadSchedulerState();
140
+ if (!running) {
141
+ log('Scheduler: not running');
142
+ if (scheduledRoutines.length === 0) {
143
+ log('\nNo scheduled routines found.');
144
+ log('Add a "schedule" field to your .routine.json files to enable scheduling.');
145
+ }
146
+ else {
147
+ log(`\nFound ${scheduledRoutines.length} scheduled routine(s):`);
148
+ for (const routine of scheduledRoutines) {
149
+ const scheduleStr = routine.schedule.cron
150
+ ? `cron: ${routine.schedule.cron}`
151
+ : `interval: ${routine.schedule.interval}`;
152
+ log(` - ${routine.name} (${scheduleStr})`);
153
+ }
154
+ log('\nStart the scheduler with: cli4ai scheduler start');
155
+ }
156
+ output({
157
+ running: false,
158
+ pid: null,
159
+ routines: scheduledRoutines.map(r => ({
160
+ name: r.name,
161
+ schedule: r.schedule,
162
+ path: r.path
163
+ }))
164
+ });
165
+ return;
166
+ }
167
+ log(`Scheduler: running (PID ${pid})`);
168
+ if (state) {
169
+ log(`Started: ${state.startedAt}`);
170
+ const routineStates = Object.values(state.routines);
171
+ if (routineStates.length === 0) {
172
+ log('\nNo scheduled routines.');
173
+ }
174
+ else {
175
+ log(`\nScheduled routines (${routineStates.length}):`);
176
+ log('');
177
+ for (const routine of routineStates) {
178
+ const scheduleStr = routine.schedule.cron
179
+ ? `cron: ${routine.schedule.cron}`
180
+ : `interval: ${routine.schedule.interval}`;
181
+ const statusIcon = routine.running ? '⏳' :
182
+ routine.lastStatus === 'success' ? '✓' :
183
+ routine.lastStatus === 'failed' ? '✗' : '○';
184
+ log(` ${statusIcon} ${routine.name}`);
185
+ log(` Schedule: ${scheduleStr}`);
186
+ if (routine.nextRunAt) {
187
+ const nextRun = new Date(routine.nextRunAt);
188
+ const now = new Date();
189
+ const diffMs = nextRun.getTime() - now.getTime();
190
+ const diffMins = Math.round(diffMs / 60000);
191
+ if (diffMins < 1) {
192
+ log(` Next run: in < 1 minute`);
193
+ }
194
+ else if (diffMins < 60) {
195
+ log(` Next run: in ${diffMins} minute${diffMins === 1 ? '' : 's'}`);
196
+ }
197
+ else {
198
+ const diffHours = Math.round(diffMins / 60);
199
+ log(` Next run: in ${diffHours} hour${diffHours === 1 ? '' : 's'}`);
200
+ }
201
+ }
202
+ if (routine.lastRunAt) {
203
+ log(` Last run: ${routine.lastRunAt} (${routine.lastStatus})`);
204
+ }
205
+ log('');
206
+ }
207
+ }
208
+ }
209
+ output({
210
+ running: true,
211
+ pid,
212
+ startedAt: state?.startedAt,
213
+ routines: state ? Object.values(state.routines) : []
214
+ });
215
+ }
216
+ export async function schedulerLogsCommand(options) {
217
+ if (!existsSync(SCHEDULER_LOG_FILE)) {
218
+ log('No scheduler logs found.');
219
+ output({ logs: [] });
220
+ return;
221
+ }
222
+ const lines = options.lines ?? 50;
223
+ if (options.follow) {
224
+ // Tail -f mode
225
+ log(`Tailing scheduler logs (${SCHEDULER_LOG_FILE})...`);
226
+ log('Press Ctrl+C to stop\n');
227
+ // Read initial content
228
+ const content = readFileSync(SCHEDULER_LOG_FILE, 'utf-8');
229
+ const allLines = content.split('\n');
230
+ const lastLines = allLines.slice(-lines);
231
+ process.stdout.write(lastLines.join('\n') + '\n');
232
+ // Watch for new content
233
+ const { watch } = await import('fs');
234
+ let lastSize = content.length;
235
+ const watcher = watch(SCHEDULER_LOG_FILE, (eventType) => {
236
+ if (eventType === 'change') {
237
+ const newContent = readFileSync(SCHEDULER_LOG_FILE, 'utf-8');
238
+ if (newContent.length > lastSize) {
239
+ process.stdout.write(newContent.slice(lastSize));
240
+ lastSize = newContent.length;
241
+ }
242
+ }
243
+ });
244
+ // Keep running until interrupted
245
+ await new Promise((resolve) => {
246
+ let closed = false;
247
+ const closeWatcher = () => {
248
+ if (closed)
249
+ return;
250
+ closed = true;
251
+ try {
252
+ watcher.close();
253
+ }
254
+ catch {
255
+ // ignore
256
+ }
257
+ };
258
+ const stop = () => {
259
+ closeWatcher();
260
+ resolve();
261
+ };
262
+ process.once('exit', closeWatcher);
263
+ process.once('SIGINT', stop);
264
+ process.once('SIGTERM', stop);
265
+ });
266
+ }
267
+ else {
268
+ // Read last N lines
269
+ const content = readFileSync(SCHEDULER_LOG_FILE, 'utf-8');
270
+ const allLines = content.split('\n').filter(l => l.trim());
271
+ const lastLines = allLines.slice(-lines);
272
+ for (const line of lastLines) {
273
+ log(line);
274
+ }
275
+ output({ logs: lastLines });
276
+ }
277
+ }
278
+ export async function schedulerHistoryCommand(routineName, options = {}) {
279
+ const limit = options.limit ?? 20;
280
+ const history = getRunHistory(routineName, limit);
281
+ if (history.length === 0) {
282
+ log(routineName
283
+ ? `No execution history found for routine: ${routineName}`
284
+ : 'No execution history found.');
285
+ output({ history: [] });
286
+ return;
287
+ }
288
+ log(routineName
289
+ ? `Execution history for ${routineName} (last ${history.length}):`
290
+ : `Execution history (last ${history.length}):`);
291
+ log('');
292
+ for (const record of history) {
293
+ const statusIcon = record.status === 'success' ? '✓' : '✗';
294
+ const duration = record.durationMs < 1000
295
+ ? `${record.durationMs}ms`
296
+ : `${(record.durationMs / 1000).toFixed(1)}s`;
297
+ log(` ${statusIcon} ${record.routine}`);
298
+ log(` Started: ${record.startedAt}`);
299
+ log(` Duration: ${duration}`);
300
+ if (record.retryAttempt > 0) {
301
+ log(` Retry: #${record.retryAttempt}`);
302
+ }
303
+ if (record.error) {
304
+ log(` Error: ${record.error}`);
305
+ }
306
+ log('');
307
+ }
308
+ output({ history });
309
+ }
310
+ // ═══════════════════════════════════════════════════════════════════════════
311
+ // SCHEDULER RUN
312
+ // ═══════════════════════════════════════════════════════════════════════════
313
+ export async function schedulerRunCommand(routineName) {
314
+ // Check if routine exists and is scheduled
315
+ const scheduledRoutines = getScheduledRoutines(process.cwd());
316
+ const routine = scheduledRoutines.find(r => r.name === routineName);
317
+ if (!routine) {
318
+ // Check if it exists but isn't scheduled
319
+ const { resolveRoutine } = await import('../core/routines.js');
320
+ const exists = resolveRoutine(routineName, process.cwd());
321
+ if (exists) {
322
+ outputError('NOT_FOUND', `Routine "${routineName}" exists but has no schedule configured`, {
323
+ hint: 'Add a "schedule" field to enable scheduling, or use "cli4ai routines run" for one-time execution'
324
+ });
325
+ }
326
+ else {
327
+ outputError('NOT_FOUND', `Routine not found: ${routineName}`);
328
+ }
329
+ return;
330
+ }
331
+ log(`Running routine: ${routineName}`);
332
+ const scheduler = new Scheduler({ projectDir: process.cwd() });
333
+ scheduler.refreshRoutines();
334
+ try {
335
+ const record = await scheduler.runNow(routineName);
336
+ if (record.status === 'success') {
337
+ log(`Routine completed successfully in ${record.durationMs}ms`);
338
+ }
339
+ else {
340
+ log(`Routine failed (exit code ${record.exitCode})`);
341
+ if (record.error) {
342
+ log(`Error: ${record.error}`);
343
+ }
344
+ }
345
+ output(record);
346
+ }
347
+ catch (err) {
348
+ outputError('API_ERROR', `Failed to run routine: ${err instanceof Error ? err.message : String(err)}`);
349
+ }
350
+ }
@@ -0,0 +1,9 @@
1
+ /**
2
+ * cli4ai search - Search for packages
3
+ */
4
+ interface SearchOptions {
5
+ limit?: string;
6
+ remote?: string;
7
+ }
8
+ export declare function searchCommand(query: string, options: SearchOptions): Promise<void>;
9
+ export {};
@@ -0,0 +1,162 @@
1
+ /**
2
+ * cli4ai search - Search for packages
3
+ */
4
+ import { readdirSync, existsSync } from 'fs';
5
+ import { resolve } from 'path';
6
+ import { spawnSync } from 'child_process';
7
+ import { platform } from 'os';
8
+ import { output, outputError, log } from '../lib/cli.js';
9
+ // Windows-safe npm command
10
+ const npmCmd = platform() === 'win32' ? 'npm.cmd' : 'npm';
11
+ import { loadConfig, getGlobalPackages, getLocalPackages } from '../core/config.js';
12
+ import { tryLoadManifest } from '../core/manifest.js';
13
+ import { remoteListPackages, RemoteConnectionError, RemoteApiError } from '../core/remote-client.js';
14
+ export async function searchCommand(query, options) {
15
+ const limit = parseInt(options.limit || '20', 10);
16
+ const queryLower = query.toLowerCase();
17
+ // Handle remote search
18
+ if (options.remote) {
19
+ try {
20
+ const packageList = await remoteListPackages(options.remote);
21
+ const results = packageList.packages
22
+ .filter(pkg => pkg.name.toLowerCase().includes(queryLower) ||
23
+ pkg.path.toLowerCase().includes(queryLower))
24
+ .slice(0, limit)
25
+ .map(pkg => ({
26
+ name: pkg.name,
27
+ version: pkg.version,
28
+ source: 'remote',
29
+ installed: true
30
+ }));
31
+ output({
32
+ remote: options.remote,
33
+ query,
34
+ results,
35
+ count: results.length
36
+ });
37
+ }
38
+ catch (err) {
39
+ if (err instanceof RemoteConnectionError) {
40
+ outputError('NETWORK_ERROR', err.message, { remote: err.remoteName, url: err.url });
41
+ }
42
+ else if (err instanceof RemoteApiError) {
43
+ outputError(err.code, err.message, { remote: err.remoteName, details: err.details });
44
+ }
45
+ else {
46
+ throw err;
47
+ }
48
+ }
49
+ return;
50
+ }
51
+ const results = [];
52
+ const seen = new Set();
53
+ // Get installed packages
54
+ const installedNames = new Set([
55
+ ...getLocalPackages(process.cwd()).map(p => p.name),
56
+ ...getGlobalPackages().map(p => p.name)
57
+ ]);
58
+ // Search local registries
59
+ const config = loadConfig();
60
+ for (const registryPath of config.localRegistries) {
61
+ if (!existsSync(registryPath))
62
+ continue;
63
+ try {
64
+ for (const entry of readdirSync(registryPath, { withFileTypes: true })) {
65
+ if (!entry.isDirectory())
66
+ continue;
67
+ if (seen.has(entry.name))
68
+ continue;
69
+ const pkgPath = resolve(registryPath, entry.name);
70
+ const manifest = tryLoadManifest(pkgPath);
71
+ if (!manifest)
72
+ continue;
73
+ // Check if matches query
74
+ if (matches(manifest, queryLower)) {
75
+ seen.add(manifest.name);
76
+ results.push({
77
+ name: manifest.name,
78
+ version: manifest.version,
79
+ description: manifest.description,
80
+ path: pkgPath,
81
+ source: 'local-registry',
82
+ installed: installedNames.has(manifest.name)
83
+ });
84
+ }
85
+ if (results.length >= limit)
86
+ break;
87
+ }
88
+ }
89
+ catch {
90
+ // Skip inaccessible registries
91
+ }
92
+ if (results.length >= limit)
93
+ break;
94
+ }
95
+ // Search npm for @cli4ai packages
96
+ if (results.length < limit) {
97
+ try {
98
+ log(`Searching npm for @cli4ai packages...`);
99
+ // Use spawnSync with argument array to prevent command injection
100
+ const searchResult = spawnSync(npmCmd, ['search', `@cli4ai/${query}`, '--json'], {
101
+ encoding: 'utf-8',
102
+ timeout: 10000,
103
+ stdio: ['pipe', 'pipe', 'pipe']
104
+ });
105
+ let npmResults = searchResult.stdout || '';
106
+ // Fallback to searching @cli4ai if specific query fails
107
+ if (!npmResults || npmResults === '[]') {
108
+ const fallbackResult = spawnSync(npmCmd, ['search', '@cli4ai', '--json'], {
109
+ encoding: 'utf-8',
110
+ timeout: 10000,
111
+ stdio: ['pipe', 'pipe', 'pipe']
112
+ });
113
+ npmResults = fallbackResult.stdout || '[]';
114
+ }
115
+ const packages = JSON.parse(npmResults || '[]');
116
+ for (const pkg of packages) {
117
+ if (seen.has(pkg.name))
118
+ continue;
119
+ // Filter to only @cli4ai scoped packages
120
+ if (!pkg.name.startsWith('@cli4ai/'))
121
+ continue;
122
+ // Check if matches query
123
+ const shortName = pkg.name.replace('@cli4ai/', '');
124
+ if (shortName.toLowerCase().includes(queryLower) ||
125
+ pkg.description?.toLowerCase().includes(queryLower) ||
126
+ pkg.keywords?.some((k) => k.toLowerCase().includes(queryLower))) {
127
+ seen.add(pkg.name);
128
+ results.push({
129
+ name: shortName,
130
+ version: pkg.version,
131
+ description: pkg.description,
132
+ source: 'npm',
133
+ installed: installedNames.has(shortName) || installedNames.has(pkg.name)
134
+ });
135
+ }
136
+ if (results.length >= limit)
137
+ break;
138
+ }
139
+ }
140
+ catch {
141
+ // npm search failed, continue with local results
142
+ }
143
+ }
144
+ output({
145
+ query,
146
+ results: results.slice(0, limit),
147
+ count: results.length,
148
+ registries: config.localRegistries
149
+ });
150
+ }
151
+ function matches(manifest, query) {
152
+ // Match against name
153
+ if (manifest.name.toLowerCase().includes(query))
154
+ return true;
155
+ // Match against description
156
+ if (manifest.description?.toLowerCase().includes(query))
157
+ return true;
158
+ // Match against keywords
159
+ if (manifest.keywords?.some(k => k.toLowerCase().includes(query)))
160
+ return true;
161
+ return false;
162
+ }
@@ -0,0 +1,28 @@
1
+ /**
2
+ * cli4ai secrets - Manage secrets for CLI tools
3
+ */
4
+ interface SecretsOptions {
5
+ package?: string;
6
+ }
7
+ /**
8
+ * cli4ai secrets set <key> [value]
9
+ */
10
+ export declare function secretsSetCommand(key: string, value?: string): Promise<void>;
11
+ /**
12
+ * cli4ai secrets get <key>
13
+ */
14
+ export declare function secretsGetCommand(key: string): Promise<void>;
15
+ /**
16
+ * cli4ai secrets list
17
+ */
18
+ export declare function secretsListCommand(options: SecretsOptions): Promise<void>;
19
+ /**
20
+ * cli4ai secrets delete <key>
21
+ */
22
+ export declare function secretsDeleteCommand(key: string): Promise<void>;
23
+ /**
24
+ * cli4ai secrets init [package]
25
+ * Interactive setup for a package's required secrets
26
+ */
27
+ export declare function secretsInitCommand(packageName?: string, options?: SecretsOptions): Promise<void>;
28
+ export {};