cli4ai 1.2.4 → 1.2.6
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/dist/cli.js +31 -0
- package/dist/commands/add.js +5 -1
- package/dist/commands/browse.js +65 -52
- package/dist/commands/download.d.ts +7 -0
- package/dist/commands/download.js +36 -0
- package/dist/commands/routines.js +10 -3
- package/dist/commands/scheduler.js +3 -0
- package/dist/commands/secrets.js +37 -24
- package/dist/commands/serve.d.ts +4 -0
- package/dist/commands/serve.js +9 -0
- package/dist/commands/update.js +6 -2
- package/dist/core/execute.d.ts +4 -0
- package/dist/core/execute.js +37 -12
- package/dist/core/remote-client.d.ts +8 -0
- package/dist/core/remote-client.js +67 -0
- package/dist/core/routine-engine.d.ts +90 -6
- package/dist/core/routine-engine.js +769 -221
- package/dist/core/routines.d.ts +5 -0
- package/dist/core/routines.js +20 -0
- package/dist/core/scheduler.d.ts +19 -0
- package/dist/core/scheduler.js +79 -1
- package/dist/dashboard/api/endpoints.d.ts +14 -0
- package/dist/dashboard/api/endpoints.js +562 -0
- package/dist/dashboard/api/websocket.d.ts +133 -0
- package/dist/dashboard/api/websocket.js +278 -0
- package/dist/dashboard/db/index.d.ts +33 -0
- package/dist/dashboard/db/index.js +69 -0
- package/dist/dashboard/db/runs.d.ts +170 -0
- package/dist/dashboard/db/runs.js +475 -0
- package/dist/dashboard/db/schema.d.ts +64 -0
- package/dist/dashboard/db/schema.js +157 -0
- package/dist/mcp/adapter.js +3 -0
- package/dist/mcp/server.js +3 -1
- package/dist/server/service.d.ts +8 -0
- package/dist/server/service.js +192 -6
- package/package.json +11 -3
- package/src/dashboard/public/assets/index-DN1hIAMO.css +1 -0
- package/src/dashboard/public/assets/index-pZeAAQwj.js +331 -0
- package/src/dashboard/public/index.html +14 -0
package/dist/cli.js
CHANGED
|
@@ -21,6 +21,7 @@ import { routinesListCommand, routinesRunCommand, routinesShowCommand, routinesC
|
|
|
21
21
|
import { schedulerStartCommand, schedulerStopCommand, schedulerStatusCommand, schedulerLogsCommand, schedulerHistoryCommand, schedulerRunCommand } from './commands/scheduler.js';
|
|
22
22
|
import { remotesListCommand, remotesAddCommand, remotesUpdateCommand, remotesRemoveCommand, remotesShowCommand, remotesTestCommand, remotesPackagesCommand } from './commands/remotes.js';
|
|
23
23
|
import { serveCommand } from './commands/serve.js';
|
|
24
|
+
import { downloadCommand } from './commands/download.js';
|
|
24
25
|
export function createProgram() {
|
|
25
26
|
const program = new Command()
|
|
26
27
|
.name('cli4ai')
|
|
@@ -276,6 +277,8 @@ Remote Execution:
|
|
|
276
277
|
.option('-k, --api-key <key>', 'API key for authentication')
|
|
277
278
|
.option('--scope <levels>', 'Comma-separated allowed scopes (read,write,full)')
|
|
278
279
|
.option('--cwd <dir>', 'Working directory for execution')
|
|
280
|
+
.option('--ui', 'Enable the dashboard UI at /ui')
|
|
281
|
+
.option('--allow-path <path>', 'Allow file downloads from path (can be used multiple times)', (val, prev) => prev.concat([val]), [])
|
|
279
282
|
.addHelpText('after', `
|
|
280
283
|
|
|
281
284
|
Examples:
|
|
@@ -283,13 +286,26 @@ Examples:
|
|
|
283
286
|
cli4ai serve --port 8080 # Start on port 8080
|
|
284
287
|
cli4ai serve --api-key mysecretkey # Require API key
|
|
285
288
|
cli4ai serve --scope read,write # Restrict to read/write operations
|
|
289
|
+
cli4ai serve --ui # Enable dashboard at /ui
|
|
290
|
+
cli4ai serve --allow-path /data # Allow file downloads from /data
|
|
286
291
|
|
|
287
292
|
The service exposes these endpoints:
|
|
288
293
|
GET /health - Service health check
|
|
289
294
|
GET /packages - List available packages
|
|
290
295
|
GET /packages/:name - Get package info
|
|
296
|
+
GET /files?path=<path> - Download file (restricted to allowed paths)
|
|
291
297
|
POST /run - Execute a tool
|
|
292
298
|
POST /routines/:name/run - Run a routine
|
|
299
|
+
|
|
300
|
+
By default, file downloads are allowed from: cwd, tmpdir, ~/Downloads.
|
|
301
|
+
Use --allow-path to add additional directories.
|
|
302
|
+
|
|
303
|
+
With --ui flag, additional endpoints are available:
|
|
304
|
+
GET /ui - Dashboard web interface
|
|
305
|
+
GET /api/runs - Run history
|
|
306
|
+
GET /api/routines - Routine management
|
|
307
|
+
GET /api/scheduler - Scheduler status
|
|
308
|
+
GET /api/metrics - Metrics and stats
|
|
293
309
|
`)
|
|
294
310
|
.action(withErrorHandling(serveCommand));
|
|
295
311
|
const remotes = program
|
|
@@ -331,5 +347,20 @@ The service exposes these endpoints:
|
|
|
331
347
|
.command('packages <name>')
|
|
332
348
|
.description('List packages on a remote')
|
|
333
349
|
.action(withErrorHandling(remotesPackagesCommand));
|
|
350
|
+
program
|
|
351
|
+
.command('download <remote> <path>')
|
|
352
|
+
.description('Download a file from a remote service')
|
|
353
|
+
.option('-o, --output <path>', 'Local output path (default: current directory)')
|
|
354
|
+
.addHelpText('after', `
|
|
355
|
+
|
|
356
|
+
Examples:
|
|
357
|
+
cli4ai download myserver /path/to/file.jpg
|
|
358
|
+
cli4ai download myserver /tmp/output.pdf -o ./local.pdf
|
|
359
|
+
cli4ai download mac ~/Downloads/image.png -o ./image.png
|
|
360
|
+
|
|
361
|
+
The remote must be configured via 'cli4ai remotes add' first.
|
|
362
|
+
File access is restricted to allowed paths on the server.
|
|
363
|
+
`)
|
|
364
|
+
.action(withErrorHandling(downloadCommand));
|
|
334
365
|
return program;
|
|
335
366
|
}
|
package/dist/commands/add.js
CHANGED
|
@@ -59,11 +59,15 @@ async function confirm(message) {
|
|
|
59
59
|
input: process.stdin,
|
|
60
60
|
output: process.stderr
|
|
61
61
|
});
|
|
62
|
-
return new Promise((resolve) => {
|
|
62
|
+
return new Promise((resolve, reject) => {
|
|
63
63
|
rl.question(`${message} [y/N] `, (answer) => {
|
|
64
64
|
rl.close();
|
|
65
65
|
resolve(answer.toLowerCase() === 'y' || answer.toLowerCase() === 'yes');
|
|
66
66
|
});
|
|
67
|
+
rl.on('error', (err) => {
|
|
68
|
+
rl.close();
|
|
69
|
+
reject(err);
|
|
70
|
+
});
|
|
67
71
|
});
|
|
68
72
|
}
|
|
69
73
|
/**
|
package/dist/commands/browse.js
CHANGED
|
@@ -190,61 +190,74 @@ async function multiSelect(items) {
|
|
|
190
190
|
log('');
|
|
191
191
|
};
|
|
192
192
|
return new Promise((resolve) => {
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
else if (key === '\x1B[B' || key === 'j') {
|
|
205
|
-
// Down arrow or j
|
|
206
|
-
cursor = Math.min(items.length - 1, cursor + 1);
|
|
207
|
-
render();
|
|
208
|
-
}
|
|
209
|
-
else if (key === ' ') {
|
|
210
|
-
// Space - toggle selection
|
|
211
|
-
if (selected.has(cursor)) {
|
|
212
|
-
selected.delete(cursor);
|
|
193
|
+
try {
|
|
194
|
+
process.stdin.setRawMode(true);
|
|
195
|
+
process.stdin.resume();
|
|
196
|
+
process.stdin.setEncoding('utf8');
|
|
197
|
+
render();
|
|
198
|
+
const onKeypress = (key) => {
|
|
199
|
+
// Handle arrow keys (escape sequences)
|
|
200
|
+
if (key === '\x1B[A' || key === 'k') {
|
|
201
|
+
// Up arrow or k
|
|
202
|
+
cursor = Math.max(0, cursor - 1);
|
|
203
|
+
render();
|
|
213
204
|
}
|
|
214
|
-
else {
|
|
215
|
-
|
|
205
|
+
else if (key === '\x1B[B' || key === 'j') {
|
|
206
|
+
// Down arrow or j
|
|
207
|
+
cursor = Math.min(items.length - 1, cursor + 1);
|
|
208
|
+
render();
|
|
209
|
+
}
|
|
210
|
+
else if (key === ' ') {
|
|
211
|
+
// Space - toggle selection
|
|
212
|
+
if (selected.has(cursor)) {
|
|
213
|
+
selected.delete(cursor);
|
|
214
|
+
}
|
|
215
|
+
else {
|
|
216
|
+
selected.add(cursor);
|
|
217
|
+
}
|
|
218
|
+
render();
|
|
219
|
+
}
|
|
220
|
+
else if (key === '\r' || key === '\n') {
|
|
221
|
+
// Enter - confirm
|
|
222
|
+
process.stdin.setRawMode(false);
|
|
223
|
+
process.stdin.removeListener('data', onKeypress);
|
|
224
|
+
process.stdin.pause();
|
|
225
|
+
process.stderr.write('\x1B[2J\x1B[H'); // Clear screen
|
|
226
|
+
const selectedPackages = Array.from(selected).map(i => items[i].name);
|
|
227
|
+
resolve(selectedPackages);
|
|
228
|
+
}
|
|
229
|
+
else if (key === 'q' || key === '\x1B' || key === '\x03') {
|
|
230
|
+
// q, Escape, or Ctrl+C - quit
|
|
231
|
+
process.stdin.setRawMode(false);
|
|
232
|
+
process.stdin.removeListener('data', onKeypress);
|
|
233
|
+
process.stdin.pause();
|
|
234
|
+
process.stderr.write('\x1B[2J\x1B[H'); // Clear screen
|
|
235
|
+
resolve([]);
|
|
236
|
+
}
|
|
237
|
+
else if (key === 'a') {
|
|
238
|
+
// Select all
|
|
239
|
+
items.forEach((_, i) => selected.add(i));
|
|
240
|
+
render();
|
|
241
|
+
}
|
|
242
|
+
else if (key === 'n') {
|
|
243
|
+
// Select none
|
|
244
|
+
selected.clear();
|
|
245
|
+
render();
|
|
246
|
+
}
|
|
247
|
+
};
|
|
248
|
+
process.stdin.on('data', onKeypress);
|
|
249
|
+
}
|
|
250
|
+
finally {
|
|
251
|
+
// Ensure raw mode is reset even if an error occurs
|
|
252
|
+
if (process.stdin.isTTY && process.stdin.setRawMode) {
|
|
253
|
+
try {
|
|
254
|
+
process.stdin.setRawMode(false);
|
|
255
|
+
}
|
|
256
|
+
catch {
|
|
257
|
+
// Ignore errors when resetting raw mode
|
|
216
258
|
}
|
|
217
|
-
render();
|
|
218
|
-
}
|
|
219
|
-
else if (key === '\r' || key === '\n') {
|
|
220
|
-
// Enter - confirm
|
|
221
|
-
process.stdin.setRawMode(false);
|
|
222
|
-
process.stdin.removeListener('data', onKeypress);
|
|
223
|
-
process.stdin.pause();
|
|
224
|
-
process.stderr.write('\x1B[2J\x1B[H'); // Clear screen
|
|
225
|
-
const selectedPackages = Array.from(selected).map(i => items[i].name);
|
|
226
|
-
resolve(selectedPackages);
|
|
227
|
-
}
|
|
228
|
-
else if (key === 'q' || key === '\x1B' || key === '\x03') {
|
|
229
|
-
// q, Escape, or Ctrl+C - quit
|
|
230
|
-
process.stdin.setRawMode(false);
|
|
231
|
-
process.stdin.removeListener('data', onKeypress);
|
|
232
|
-
process.stdin.pause();
|
|
233
|
-
process.stderr.write('\x1B[2J\x1B[H'); // Clear screen
|
|
234
|
-
resolve([]);
|
|
235
|
-
}
|
|
236
|
-
else if (key === 'a') {
|
|
237
|
-
// Select all
|
|
238
|
-
items.forEach((_, i) => selected.add(i));
|
|
239
|
-
render();
|
|
240
|
-
}
|
|
241
|
-
else if (key === 'n') {
|
|
242
|
-
// Select none
|
|
243
|
-
selected.clear();
|
|
244
|
-
render();
|
|
245
259
|
}
|
|
246
|
-
}
|
|
247
|
-
process.stdin.on('data', onKeypress);
|
|
260
|
+
}
|
|
248
261
|
});
|
|
249
262
|
}
|
|
250
263
|
/**
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Download command - Download files from a remote cli4ai service.
|
|
3
|
+
*/
|
|
4
|
+
export interface DownloadCommandOptions {
|
|
5
|
+
output?: string;
|
|
6
|
+
}
|
|
7
|
+
export declare function downloadCommand(remoteName: string, filePath: string, options: DownloadCommandOptions): Promise<void>;
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Download command - Download files from a remote cli4ai service.
|
|
3
|
+
*/
|
|
4
|
+
import { basename, resolve } from 'path';
|
|
5
|
+
import { output, outputError, log } from '../lib/cli.js';
|
|
6
|
+
import { remoteDownloadFile, RemoteConnectionError, RemoteApiError } from '../core/remote-client.js';
|
|
7
|
+
export async function downloadCommand(remoteName, filePath, options) {
|
|
8
|
+
// Determine output path
|
|
9
|
+
const outputPath = options.output
|
|
10
|
+
? resolve(options.output)
|
|
11
|
+
: resolve(process.cwd(), basename(filePath));
|
|
12
|
+
log(`Downloading from ${remoteName}: ${filePath}`);
|
|
13
|
+
log(`Saving to: ${outputPath}`);
|
|
14
|
+
try {
|
|
15
|
+
const result = await remoteDownloadFile(remoteName, filePath, outputPath);
|
|
16
|
+
output({
|
|
17
|
+
success: true,
|
|
18
|
+
remote: remoteName,
|
|
19
|
+
remotePath: filePath,
|
|
20
|
+
localPath: result.path,
|
|
21
|
+
size: result.size
|
|
22
|
+
});
|
|
23
|
+
}
|
|
24
|
+
catch (err) {
|
|
25
|
+
if (err instanceof RemoteConnectionError) {
|
|
26
|
+
outputError('CONNECTION_ERROR', err.message, {
|
|
27
|
+
remote: remoteName,
|
|
28
|
+
url: err.url
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
if (err instanceof RemoteApiError) {
|
|
32
|
+
outputError(err.code, err.message, err.details);
|
|
33
|
+
}
|
|
34
|
+
outputError('DOWNLOAD_ERROR', err instanceof Error ? err.message : String(err));
|
|
35
|
+
}
|
|
36
|
+
}
|
|
@@ -4,8 +4,10 @@
|
|
|
4
4
|
import { spawn, spawnSync } from 'child_process';
|
|
5
5
|
import { readFileSync, writeFileSync, existsSync, chmodSync, rmSync } from 'fs';
|
|
6
6
|
import { resolve } from 'path';
|
|
7
|
+
import { platform } from 'os';
|
|
7
8
|
import { stringify as stringifyYaml } from 'yaml';
|
|
8
9
|
import { output, outputError, log } from '../lib/cli.js';
|
|
10
|
+
const isWindows = platform() === 'win32';
|
|
9
11
|
import { ensureCli4aiHome, ensureLocalDir, ROUTINES_DIR, LOCAL_ROUTINES_DIR } from '../core/config.js';
|
|
10
12
|
import { getGlobalRoutines, getLocalRoutines, resolveRoutine, validateRoutineName } from '../core/routines.js';
|
|
11
13
|
import { loadRoutineDefinition, dryRunRoutine, runRoutine, RoutineParseError, RoutineValidationError, RoutineTemplateError } from '../core/routine-engine.js';
|
|
@@ -30,7 +32,7 @@ function parseVars(vars) {
|
|
|
30
32
|
return result;
|
|
31
33
|
for (const entry of vars) {
|
|
32
34
|
const eq = entry.indexOf('=');
|
|
33
|
-
if (eq
|
|
35
|
+
if (eq < 0) {
|
|
34
36
|
outputError('INVALID_INPUT', `Invalid --var (expected KEY=value): ${entry}`);
|
|
35
37
|
}
|
|
36
38
|
const key = entry.slice(0, eq).trim();
|
|
@@ -256,6 +258,7 @@ export async function routinesRunCommand(name, args, options) {
|
|
|
256
258
|
const child = spawn('bash', [routine.path, ...args], {
|
|
257
259
|
cwd: process.cwd(),
|
|
258
260
|
stdio: isTTY ? 'inherit' : ['pipe', 'pipe', 'pipe'],
|
|
261
|
+
shell: isWindows,
|
|
259
262
|
env: {
|
|
260
263
|
...process.env,
|
|
261
264
|
...varEnv,
|
|
@@ -272,9 +275,12 @@ export async function routinesRunCommand(name, args, options) {
|
|
|
272
275
|
}
|
|
273
276
|
// Non-TTY: capture and emit single JSON summary.
|
|
274
277
|
const stdoutPromise = child.stdout ? collectStream(child.stdout) : Promise.resolve('');
|
|
275
|
-
|
|
278
|
+
// Stream stderr to process.stderr while collecting it
|
|
279
|
+
const stderrChunks = [];
|
|
276
280
|
if (child.stderr) {
|
|
277
281
|
child.stderr.on('data', (chunk) => {
|
|
282
|
+
const buf = Buffer.isBuffer(chunk) ? chunk : Buffer.from(String(chunk));
|
|
283
|
+
stderrChunks.push(buf);
|
|
278
284
|
process.stderr.write(chunk);
|
|
279
285
|
});
|
|
280
286
|
}
|
|
@@ -282,7 +288,8 @@ export async function routinesRunCommand(name, args, options) {
|
|
|
282
288
|
child.on('close', (code) => resolve(code ?? 0));
|
|
283
289
|
child.on('error', (err) => outputError('API_ERROR', `Failed to execute routine: ${err.message}`));
|
|
284
290
|
});
|
|
285
|
-
const
|
|
291
|
+
const stdout = await stdoutPromise;
|
|
292
|
+
const stderr = Buffer.concat(stderrChunks).toString('utf-8');
|
|
286
293
|
output({
|
|
287
294
|
routine: routine.name,
|
|
288
295
|
kind: routine.kind,
|
|
@@ -12,7 +12,9 @@ import { spawn } from 'child_process';
|
|
|
12
12
|
import { readFileSync, existsSync } from 'fs';
|
|
13
13
|
import { resolve, dirname } from 'path';
|
|
14
14
|
import { fileURLToPath } from 'url';
|
|
15
|
+
import { platform } from 'os';
|
|
15
16
|
import { output, outputError, log } from '../lib/cli.js';
|
|
17
|
+
const isWindows = platform() === 'win32';
|
|
16
18
|
import { isDaemonRunning, getDaemonPid, removeDaemonPid, loadSchedulerState, getRunHistory, Scheduler, SCHEDULER_LOG_FILE, SCHEDULER_PID_FILE } from '../core/scheduler.js';
|
|
17
19
|
import { getScheduledRoutines } from '../core/routines.js';
|
|
18
20
|
export async function schedulerStartCommand(options) {
|
|
@@ -55,6 +57,7 @@ export async function schedulerStartCommand(options) {
|
|
|
55
57
|
const child = spawn('npx', ['tsx', daemonScript, '--project-dir', process.cwd()], {
|
|
56
58
|
detached: true,
|
|
57
59
|
stdio: 'ignore',
|
|
60
|
+
shell: isWindows,
|
|
58
61
|
env: {
|
|
59
62
|
...process.env,
|
|
60
63
|
CLI4AI_DAEMON: 'true'
|
package/dist/commands/secrets.js
CHANGED
|
@@ -33,32 +33,45 @@ async function prompt(message, hidden = false) {
|
|
|
33
33
|
// Hide input for secrets
|
|
34
34
|
process.stderr.write(message);
|
|
35
35
|
let value = '';
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
value
|
|
36
|
+
try {
|
|
37
|
+
process.stdin.setRawMode(true);
|
|
38
|
+
process.stdin.resume();
|
|
39
|
+
process.stdin.setEncoding('utf8');
|
|
40
|
+
const onData = (char) => {
|
|
41
|
+
if (char === '\n' || char === '\r') {
|
|
42
|
+
process.stdin.setRawMode(false);
|
|
43
|
+
process.stdin.removeListener('data', onData);
|
|
44
|
+
process.stderr.write('\n');
|
|
45
|
+
rl.close();
|
|
46
|
+
resolve(value);
|
|
47
|
+
}
|
|
48
|
+
else if (char === '\u0003') {
|
|
49
|
+
// Ctrl+C
|
|
50
|
+
process.exit(1);
|
|
51
|
+
}
|
|
52
|
+
else if (char === '\u007F') {
|
|
53
|
+
// Backspace
|
|
54
|
+
if (value.length > 0) {
|
|
55
|
+
value = value.slice(0, -1);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
else {
|
|
59
|
+
value += char;
|
|
60
|
+
}
|
|
61
|
+
};
|
|
62
|
+
process.stdin.on('data', onData);
|
|
63
|
+
}
|
|
64
|
+
finally {
|
|
65
|
+
// Ensure raw mode is reset even if an error occurs
|
|
66
|
+
if (process.stdin.isTTY && process.stdin.setRawMode) {
|
|
67
|
+
try {
|
|
68
|
+
process.stdin.setRawMode(false);
|
|
69
|
+
}
|
|
70
|
+
catch {
|
|
71
|
+
// Ignore errors when resetting raw mode
|
|
55
72
|
}
|
|
56
73
|
}
|
|
57
|
-
|
|
58
|
-
value += char;
|
|
59
|
-
}
|
|
60
|
-
};
|
|
61
|
-
process.stdin.on('data', onData);
|
|
74
|
+
}
|
|
62
75
|
}
|
|
63
76
|
else {
|
|
64
77
|
rl.question(message, (answer) => {
|
package/dist/commands/serve.d.ts
CHANGED
|
@@ -9,5 +9,9 @@ export interface ServeCommandOptions {
|
|
|
9
9
|
apiKey?: string;
|
|
10
10
|
scope?: string;
|
|
11
11
|
cwd?: string;
|
|
12
|
+
/** Enable the dashboard UI at /ui */
|
|
13
|
+
ui?: boolean;
|
|
14
|
+
/** Additional paths allowed for file downloads */
|
|
15
|
+
allowPath?: string[];
|
|
12
16
|
}
|
|
13
17
|
export declare function serveCommand(options: ServeCommandOptions): Promise<void>;
|
package/dist/commands/serve.js
CHANGED
|
@@ -36,6 +36,15 @@ export async function serveCommand(options) {
|
|
|
36
36
|
}
|
|
37
37
|
serviceOptions.allowedScopes = scopes;
|
|
38
38
|
}
|
|
39
|
+
// Enable dashboard UI
|
|
40
|
+
if (options.ui) {
|
|
41
|
+
serviceOptions.enableDashboard = true;
|
|
42
|
+
}
|
|
43
|
+
// Additional allowed paths for file downloads (adds to defaults)
|
|
44
|
+
if (options.allowPath && options.allowPath.length > 0) {
|
|
45
|
+
// Pass extra paths - service will merge with defaults
|
|
46
|
+
serviceOptions.allowedPaths = options.allowPath;
|
|
47
|
+
}
|
|
39
48
|
try {
|
|
40
49
|
await startService(serviceOptions);
|
|
41
50
|
output({ status: 'stopped' });
|
package/dist/commands/update.js
CHANGED
|
@@ -2,7 +2,9 @@
|
|
|
2
2
|
* cli4ai update - Update installed packages and cli4ai itself
|
|
3
3
|
*/
|
|
4
4
|
import { spawn } from 'child_process';
|
|
5
|
+
import { platform } from 'os';
|
|
5
6
|
import { log } from '../lib/cli.js';
|
|
7
|
+
const isWindows = platform() === 'win32';
|
|
6
8
|
import { getNpmGlobalPackages, getGlobalPackages } from '../core/config.js';
|
|
7
9
|
// ANSI colors
|
|
8
10
|
const RESET = '\x1B[0m';
|
|
@@ -50,7 +52,8 @@ async function updateCli4aiPackage(packageName) {
|
|
|
50
52
|
return new Promise((resolve) => {
|
|
51
53
|
// Use cli4ai add -g to reinstall the package (same location as original install)
|
|
52
54
|
const proc = spawn('cli4ai', ['add', '-g', packageName, '-y'], {
|
|
53
|
-
stdio: 'pipe'
|
|
55
|
+
stdio: 'pipe',
|
|
56
|
+
shell: isWindows
|
|
54
57
|
});
|
|
55
58
|
proc.on('close', (code) => {
|
|
56
59
|
resolve(code === 0);
|
|
@@ -66,7 +69,8 @@ async function updateCli4aiPackage(packageName) {
|
|
|
66
69
|
async function updateNpmPackage(packageName) {
|
|
67
70
|
return new Promise((resolve) => {
|
|
68
71
|
const proc = spawn('npm', ['install', '-g', `${packageName}@latest`], {
|
|
69
|
-
stdio: 'pipe'
|
|
72
|
+
stdio: 'pipe',
|
|
73
|
+
shell: isWindows
|
|
70
74
|
});
|
|
71
75
|
proc.on('close', (code) => {
|
|
72
76
|
resolve(code === 0);
|
package/dist/core/execute.d.ts
CHANGED
|
@@ -27,6 +27,10 @@ export interface ExecuteToolOptions {
|
|
|
27
27
|
scope?: ScopeLevel;
|
|
28
28
|
/** Run in sandboxed environment with restricted file system access */
|
|
29
29
|
sandbox?: boolean;
|
|
30
|
+
/** Callback for each stdout line (for live streaming) */
|
|
31
|
+
onStdoutLine?: (line: string) => void;
|
|
32
|
+
/** Callback for each stderr line (for live streaming) */
|
|
33
|
+
onStderrLine?: (line: string) => void;
|
|
30
34
|
}
|
|
31
35
|
export interface ExecuteToolResult {
|
|
32
36
|
exitCode: number;
|
package/dist/core/execute.js
CHANGED
|
@@ -399,8 +399,8 @@ export async function executeTool(options) {
|
|
|
399
399
|
}
|
|
400
400
|
});
|
|
401
401
|
const exitCode = await new Promise((resolve, reject) => {
|
|
402
|
-
proc.
|
|
403
|
-
proc.
|
|
402
|
+
proc.once('close', (code) => resolve(code ?? 0));
|
|
403
|
+
proc.once('error', (err) => {
|
|
404
404
|
reject(new ExecuteToolError('API_ERROR', `Failed to execute: ${err.message}`));
|
|
405
405
|
});
|
|
406
406
|
});
|
|
@@ -437,13 +437,33 @@ export async function executeTool(options) {
|
|
|
437
437
|
// Don’t block on stdin if nothing is provided
|
|
438
438
|
proc.stdin.end();
|
|
439
439
|
}
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
440
|
+
// Set up line-by-line streaming for callbacks
|
|
441
|
+
const stdoutLines = [];
|
|
442
|
+
const stderrLines = [];
|
|
443
|
+
const readlineInterfaces = [];
|
|
444
|
+
if (proc.stdout) {
|
|
445
|
+
const rl = createInterface({ input: proc.stdout, crlfDelay: Infinity });
|
|
446
|
+
readlineInterfaces.push(rl);
|
|
447
|
+
rl.on('line', (line) => {
|
|
448
|
+
stdoutLines.push(line);
|
|
449
|
+
if (options.onStdoutLine) {
|
|
450
|
+
options.onStdoutLine(line);
|
|
451
|
+
}
|
|
452
|
+
});
|
|
453
|
+
}
|
|
454
|
+
if (proc.stderr) {
|
|
455
|
+
const rl = createInterface({ input: proc.stderr, crlfDelay: Infinity });
|
|
456
|
+
readlineInterfaces.push(rl);
|
|
457
|
+
rl.on('line', (line) => {
|
|
458
|
+
stderrLines.push(line);
|
|
459
|
+
if (teeStderr) {
|
|
460
|
+
process.stderr.write(line + '\n');
|
|
461
|
+
}
|
|
462
|
+
if (options.onStderrLine) {
|
|
463
|
+
options.onStderrLine(line);
|
|
464
|
+
}
|
|
443
465
|
});
|
|
444
466
|
}
|
|
445
|
-
const stdoutPromise = proc.stdout ? collectStream(proc.stdout) : Promise.resolve('');
|
|
446
|
-
const stderrPromise = proc.stderr ? collectStream(proc.stderr) : Promise.resolve('');
|
|
447
467
|
let timeout;
|
|
448
468
|
if (options.timeoutMs && options.timeoutMs > 0) {
|
|
449
469
|
timeout = setTimeout(() => {
|
|
@@ -460,18 +480,23 @@ export async function executeTool(options) {
|
|
|
460
480
|
}, options.timeoutMs);
|
|
461
481
|
}
|
|
462
482
|
const exitCode = await new Promise((resolve, reject) => {
|
|
463
|
-
proc.
|
|
464
|
-
proc.
|
|
483
|
+
proc.once('close', (code) => resolve(code ?? 0));
|
|
484
|
+
proc.once('error', (err) => reject(new ExecuteToolError('API_ERROR', `Failed to execute: ${err.message}`)));
|
|
465
485
|
}).finally(() => {
|
|
466
486
|
if (timeout)
|
|
467
487
|
clearTimeout(timeout);
|
|
488
|
+
// Close all readline interfaces
|
|
489
|
+
for (const rl of readlineInterfaces) {
|
|
490
|
+
rl.close();
|
|
491
|
+
}
|
|
468
492
|
});
|
|
469
|
-
|
|
493
|
+
// Small delay to ensure all readline events are processed
|
|
494
|
+
await new Promise(r => setTimeout(r, 10));
|
|
470
495
|
return {
|
|
471
496
|
exitCode,
|
|
472
497
|
durationMs: Date.now() - startTime,
|
|
473
|
-
stdout,
|
|
474
|
-
stderr,
|
|
498
|
+
stdout: stdoutLines.length > 0 ? stdoutLines.join('\n') + '\n' : '',
|
|
499
|
+
stderr: stderrLines.length > 0 ? stderrLines.join('\n') + '\n' : '',
|
|
475
500
|
packagePath: pkg.path,
|
|
476
501
|
entryPath,
|
|
477
502
|
runtime
|
|
@@ -88,6 +88,14 @@ export declare function remoteRunTool(remoteName: string, options: RemoteRunOpti
|
|
|
88
88
|
* Run a routine on a remote
|
|
89
89
|
*/
|
|
90
90
|
export declare function remoteRunRoutine(remoteName: string, routineName: string, vars?: Record<string, string>): Promise<RoutineRunSummary>;
|
|
91
|
+
/**
|
|
92
|
+
* Download a file from a remote
|
|
93
|
+
*/
|
|
94
|
+
export declare function remoteDownloadFile(remoteName: string, filePath: string, outputPath: string): Promise<{
|
|
95
|
+
success: boolean;
|
|
96
|
+
size: number;
|
|
97
|
+
path: string;
|
|
98
|
+
}>;
|
|
91
99
|
/**
|
|
92
100
|
* Test connection to a remote
|
|
93
101
|
*/
|
|
@@ -214,6 +214,73 @@ export async function remoteRunRoutine(remoteName, routineName, vars) {
|
|
|
214
214
|
throw new RemoteConnectionError(remoteName, remote.url, err instanceof Error ? err.message : String(err));
|
|
215
215
|
}
|
|
216
216
|
}
|
|
217
|
+
/**
|
|
218
|
+
* Download a file from a remote
|
|
219
|
+
*/
|
|
220
|
+
export async function remoteDownloadFile(remoteName, filePath, outputPath) {
|
|
221
|
+
const remote = getRemoteOrThrow(remoteName);
|
|
222
|
+
const url = new URL('/files', remote.url);
|
|
223
|
+
url.searchParams.set('path', filePath);
|
|
224
|
+
const { createWriteStream, promises: fs } = await import('fs');
|
|
225
|
+
const { pipeline } = await import('stream/promises');
|
|
226
|
+
return new Promise((resolve, reject) => {
|
|
227
|
+
const isHttps = url.protocol === 'https:';
|
|
228
|
+
const requestFn = isHttps ? httpsRequest : httpRequest;
|
|
229
|
+
const headers = {};
|
|
230
|
+
if (remote.apiKey) {
|
|
231
|
+
headers['X-API-Key'] = remote.apiKey;
|
|
232
|
+
}
|
|
233
|
+
const options = {
|
|
234
|
+
method: 'GET',
|
|
235
|
+
hostname: url.hostname,
|
|
236
|
+
port: url.port || (isHttps ? 443 : 80),
|
|
237
|
+
path: url.pathname + url.search,
|
|
238
|
+
headers,
|
|
239
|
+
timeout: 300000 // 5 min timeout for file downloads
|
|
240
|
+
};
|
|
241
|
+
const req = requestFn(options, async (res) => {
|
|
242
|
+
if (res.statusCode === 200) {
|
|
243
|
+
try {
|
|
244
|
+
const writeStream = createWriteStream(outputPath);
|
|
245
|
+
await pipeline(res, writeStream);
|
|
246
|
+
const stats = await fs.stat(outputPath);
|
|
247
|
+
updateRemoteLastConnected(remoteName);
|
|
248
|
+
resolve({
|
|
249
|
+
success: true,
|
|
250
|
+
size: stats.size,
|
|
251
|
+
path: outputPath
|
|
252
|
+
});
|
|
253
|
+
}
|
|
254
|
+
catch (err) {
|
|
255
|
+
reject(new Error(`Failed to write file: ${err instanceof Error ? err.message : String(err)}`));
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
else {
|
|
259
|
+
// Read error response body
|
|
260
|
+
const chunks = [];
|
|
261
|
+
res.on('data', (chunk) => chunks.push(Buffer.from(chunk)));
|
|
262
|
+
res.on('end', () => {
|
|
263
|
+
const body = Buffer.concat(chunks).toString('utf-8');
|
|
264
|
+
try {
|
|
265
|
+
const error = JSON.parse(body)?.error;
|
|
266
|
+
reject(new RemoteApiError(remoteName, res.statusCode ?? 500, error?.code ?? 'FILE_ERROR', error?.message ?? 'Failed to download file', error?.details));
|
|
267
|
+
}
|
|
268
|
+
catch {
|
|
269
|
+
reject(new RemoteApiError(remoteName, res.statusCode ?? 500, 'FILE_ERROR', body || 'Failed to download file'));
|
|
270
|
+
}
|
|
271
|
+
});
|
|
272
|
+
}
|
|
273
|
+
});
|
|
274
|
+
req.on('error', (err) => {
|
|
275
|
+
reject(new RemoteConnectionError(remoteName, remote.url, err.message));
|
|
276
|
+
});
|
|
277
|
+
req.on('timeout', () => {
|
|
278
|
+
req.destroy();
|
|
279
|
+
reject(new Error('Download timeout'));
|
|
280
|
+
});
|
|
281
|
+
req.end();
|
|
282
|
+
});
|
|
283
|
+
}
|
|
217
284
|
/**
|
|
218
285
|
* Test connection to a remote
|
|
219
286
|
*/
|