cli4ai 1.2.0 β 1.2.1
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/README.md +39 -0
- package/dist/bin.d.ts +6 -0
- package/dist/bin.js +105 -0
- package/dist/cli.d.ts +5 -0
- package/dist/cli.js +335 -0
- package/dist/commands/add.d.ts +11 -0
- package/dist/commands/add.js +459 -0
- package/dist/commands/browse.d.ts +4 -0
- package/dist/commands/browse.js +379 -0
- package/dist/commands/config.d.ts +10 -0
- package/dist/commands/config.js +121 -0
- package/dist/commands/info.d.ts +9 -0
- package/dist/commands/info.js +122 -0
- package/dist/commands/init.d.ts +10 -0
- package/dist/commands/init.js +458 -0
- package/dist/commands/list.d.ts +10 -0
- package/dist/commands/list.js +76 -0
- package/dist/commands/mcp-config.d.ts +10 -0
- package/dist/commands/mcp-config.js +49 -0
- package/dist/commands/remotes.d.ts +22 -0
- package/dist/commands/remotes.js +196 -0
- package/dist/commands/remove.d.ts +8 -0
- package/dist/commands/remove.js +61 -0
- package/dist/commands/routines.d.ts +29 -0
- package/dist/commands/routines.js +363 -0
- package/dist/commands/run.d.ts +12 -0
- package/dist/commands/run.js +104 -0
- package/dist/commands/scheduler.d.ts +27 -0
- package/dist/commands/scheduler.js +350 -0
- package/dist/commands/search.d.ts +9 -0
- package/dist/commands/search.js +159 -0
- package/dist/commands/secrets.d.ts +28 -0
- package/dist/commands/secrets.js +236 -0
- package/dist/commands/serve.d.ts +13 -0
- package/dist/commands/serve.js +49 -0
- package/dist/commands/start.d.ts +8 -0
- package/dist/commands/start.js +27 -0
- package/dist/commands/update.d.ts +17 -0
- package/dist/commands/update.js +210 -0
- package/dist/core/config.d.ts +91 -0
- package/dist/core/config.js +738 -0
- package/dist/core/execute.d.ts +51 -0
- package/dist/core/execute.js +475 -0
- package/dist/core/link.d.ts +39 -0
- package/dist/core/link.js +214 -0
- package/dist/core/lockfile.d.ts +63 -0
- package/dist/core/lockfile.js +140 -0
- package/dist/core/manifest.d.ts +96 -0
- package/dist/core/manifest.js +224 -0
- package/dist/core/registry.d.ts +74 -0
- package/dist/core/registry.js +116 -0
- package/dist/core/remote-client.d.ts +98 -0
- package/dist/core/remote-client.js +252 -0
- package/dist/core/remotes.d.ts +88 -0
- package/dist/core/remotes.js +206 -0
- package/dist/core/routine-engine.d.ts +124 -0
- package/dist/core/routine-engine.js +699 -0
- package/dist/core/routines.d.ts +36 -0
- package/dist/core/routines.js +132 -0
- package/dist/core/scheduler-daemon.d.ts +10 -0
- package/dist/core/scheduler-daemon.js +77 -0
- package/dist/core/scheduler.d.ts +131 -0
- package/dist/core/scheduler.js +492 -0
- package/dist/core/secrets.d.ts +48 -0
- package/dist/core/secrets.js +384 -0
- package/dist/lib/cli.d.ts +84 -0
- package/dist/lib/cli.js +216 -0
- package/dist/mcp/adapter.d.ts +35 -0
- package/dist/mcp/adapter.js +94 -0
- package/dist/mcp/config-gen.d.ts +31 -0
- package/dist/mcp/config-gen.js +75 -0
- package/dist/mcp/server.d.ts +41 -0
- package/dist/mcp/server.js +296 -0
- package/dist/server/service.d.ts +85 -0
- package/dist/server/service.js +304 -0
- package/package.json +6 -3
- package/src/bin.ts +0 -118
- package/src/cli.ts +0 -412
- package/src/commands/add.ts +0 -562
- package/src/commands/browse.ts +0 -449
- package/src/commands/config.ts +0 -154
- package/src/commands/info.ts +0 -133
- package/src/commands/init.ts +0 -514
- package/src/commands/list.ts +0 -95
- package/src/commands/mcp-config.ts +0 -69
- package/src/commands/remotes.ts +0 -253
- package/src/commands/remove.ts +0 -78
- package/src/commands/routines.ts +0 -427
- package/src/commands/run.ts +0 -127
- package/src/commands/scheduler.ts +0 -438
- package/src/commands/search.ts +0 -185
- package/src/commands/secrets.ts +0 -292
- package/src/commands/serve.ts +0 -66
- package/src/commands/start.ts +0 -40
- package/src/commands/update.ts +0 -252
- package/src/core/config.ts +0 -845
- package/src/core/execute.ts +0 -569
- package/src/core/link.ts +0 -246
- package/src/core/lockfile.ts +0 -187
- package/src/core/manifest.ts +0 -327
- package/src/core/registry.ts +0 -165
- package/src/core/remote-client.ts +0 -419
- package/src/core/remotes.ts +0 -268
- package/src/core/routine-engine.ts +0 -895
- package/src/core/routines.ts +0 -171
- package/src/core/scheduler-daemon.ts +0 -94
- package/src/core/scheduler.ts +0 -606
- package/src/core/secrets.ts +0 -430
- package/src/lib/cli.ts +0 -261
- package/src/mcp/adapter.ts +0 -131
- package/src/mcp/config-gen.ts +0 -106
- package/src/mcp/server.ts +0 -365
- package/src/server/service.ts +0 -434
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tool execution helper used by `cli4ai run` and routines.
|
|
3
|
+
*
|
|
4
|
+
* This refactors the core execution logic so callers can either:
|
|
5
|
+
* - inherit stdio (interactive `cli4ai run`)
|
|
6
|
+
* - capture output (routines / orchestration)
|
|
7
|
+
*/
|
|
8
|
+
export type ExecuteCaptureMode = 'inherit' | 'pipe';
|
|
9
|
+
/**
|
|
10
|
+
* Permission scope levels for tool execution
|
|
11
|
+
* - read: Only allow read operations (no mutations)
|
|
12
|
+
* - write: Allow write operations but no destructive actions
|
|
13
|
+
* - full: Full access (default)
|
|
14
|
+
*/
|
|
15
|
+
export type ScopeLevel = 'read' | 'write' | 'full';
|
|
16
|
+
export interface ExecuteToolOptions {
|
|
17
|
+
packageName: string;
|
|
18
|
+
command?: string;
|
|
19
|
+
args: string[];
|
|
20
|
+
cwd: string;
|
|
21
|
+
env?: Record<string, string>;
|
|
22
|
+
stdin?: string;
|
|
23
|
+
capture: ExecuteCaptureMode;
|
|
24
|
+
timeoutMs?: number;
|
|
25
|
+
teeStderr?: boolean;
|
|
26
|
+
/** Permission scope for the tool */
|
|
27
|
+
scope?: ScopeLevel;
|
|
28
|
+
/** Run in sandboxed environment with restricted file system access */
|
|
29
|
+
sandbox?: boolean;
|
|
30
|
+
}
|
|
31
|
+
export interface ExecuteToolResult {
|
|
32
|
+
exitCode: number;
|
|
33
|
+
durationMs: number;
|
|
34
|
+
stdout?: string;
|
|
35
|
+
stderr?: string;
|
|
36
|
+
packagePath: string;
|
|
37
|
+
entryPath: string;
|
|
38
|
+
runtime: 'node';
|
|
39
|
+
}
|
|
40
|
+
export declare class ExecuteToolError extends Error {
|
|
41
|
+
code: string;
|
|
42
|
+
details?: Record<string, unknown> | undefined;
|
|
43
|
+
constructor(code: string, message: string, details?: Record<string, unknown> | undefined);
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Execute a tool command.
|
|
47
|
+
*
|
|
48
|
+
* - When capture === 'inherit', the child inherits stdio (interactive).
|
|
49
|
+
* - When capture === 'pipe', stdout/stderr can be captured and returned.
|
|
50
|
+
*/
|
|
51
|
+
export declare function executeTool(options: ExecuteToolOptions): Promise<ExecuteToolResult>;
|
|
@@ -0,0 +1,475 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tool execution helper used by `cli4ai run` and routines.
|
|
3
|
+
*
|
|
4
|
+
* This refactors the core execution logic so callers can either:
|
|
5
|
+
* - inherit stdio (interactive `cli4ai run`)
|
|
6
|
+
* - capture output (routines / orchestration)
|
|
7
|
+
*/
|
|
8
|
+
import { spawn, spawnSync } from 'child_process';
|
|
9
|
+
import { resolve } from 'path';
|
|
10
|
+
import { existsSync, readFileSync } from 'fs';
|
|
11
|
+
import { createInterface } from 'readline';
|
|
12
|
+
import { platform, homedir } from 'os';
|
|
13
|
+
import { log } from '../lib/cli.js';
|
|
14
|
+
import { findPackage } from './config.js';
|
|
15
|
+
import { loadManifest } from './manifest.js';
|
|
16
|
+
import { getSecret } from './secrets.js';
|
|
17
|
+
/**
|
|
18
|
+
* Expand ~ to home directory in paths
|
|
19
|
+
*/
|
|
20
|
+
function expandTilde(path) {
|
|
21
|
+
if (path.startsWith('~/')) {
|
|
22
|
+
return resolve(homedir(), path.slice(2));
|
|
23
|
+
}
|
|
24
|
+
if (path === '~') {
|
|
25
|
+
return homedir();
|
|
26
|
+
}
|
|
27
|
+
return path;
|
|
28
|
+
}
|
|
29
|
+
export class ExecuteToolError extends Error {
|
|
30
|
+
code;
|
|
31
|
+
details;
|
|
32
|
+
constructor(code, message, details) {
|
|
33
|
+
super(message);
|
|
34
|
+
this.code = code;
|
|
35
|
+
this.details = details;
|
|
36
|
+
this.name = 'ExecuteToolError';
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
// Known system tools and how to install them
|
|
40
|
+
const INSTALL_COMMANDS = {
|
|
41
|
+
'yt-dlp': {
|
|
42
|
+
check: 'yt-dlp --version',
|
|
43
|
+
install: {
|
|
44
|
+
darwin: 'brew install yt-dlp',
|
|
45
|
+
linux: 'pipx install yt-dlp || sudo apt install -y pipx && pipx install yt-dlp',
|
|
46
|
+
win32: 'pip install yt-dlp'
|
|
47
|
+
},
|
|
48
|
+
description: 'YouTube video/audio downloader'
|
|
49
|
+
},
|
|
50
|
+
'gh': {
|
|
51
|
+
check: 'gh --version',
|
|
52
|
+
install: {
|
|
53
|
+
darwin: 'brew install gh',
|
|
54
|
+
linux: 'sudo apt install gh -y',
|
|
55
|
+
win32: 'winget install GitHub.cli'
|
|
56
|
+
},
|
|
57
|
+
description: 'GitHub CLI'
|
|
58
|
+
},
|
|
59
|
+
'ffmpeg': {
|
|
60
|
+
check: 'ffmpeg -version',
|
|
61
|
+
install: {
|
|
62
|
+
darwin: 'brew install ffmpeg',
|
|
63
|
+
linux: 'sudo apt install ffmpeg -y',
|
|
64
|
+
win32: 'winget install FFmpeg.FFmpeg'
|
|
65
|
+
},
|
|
66
|
+
description: 'Media processing tool'
|
|
67
|
+
},
|
|
68
|
+
'node': {
|
|
69
|
+
check: 'node --version',
|
|
70
|
+
install: {
|
|
71
|
+
darwin: 'brew install node',
|
|
72
|
+
linux: 'curl -fsSL https://deb.nodesource.com/setup_lts.x | sudo -E bash - && sudo apt-get install -y nodejs',
|
|
73
|
+
win32: 'winget install OpenJS.NodeJS.LTS'
|
|
74
|
+
},
|
|
75
|
+
description: 'JavaScript runtime'
|
|
76
|
+
}
|
|
77
|
+
};
|
|
78
|
+
/**
|
|
79
|
+
* Check if a command exists on the system
|
|
80
|
+
*/
|
|
81
|
+
function commandExists(cmd) {
|
|
82
|
+
// Use spawnSync with argument array to prevent command injection
|
|
83
|
+
const os = platform();
|
|
84
|
+
if (os === 'win32') {
|
|
85
|
+
const result = spawnSync('where', [cmd], { stdio: 'pipe' });
|
|
86
|
+
return result.status === 0;
|
|
87
|
+
}
|
|
88
|
+
const result = spawnSync('which', [cmd], { stdio: 'pipe' });
|
|
89
|
+
return result.status === 0;
|
|
90
|
+
}
|
|
91
|
+
/**
|
|
92
|
+
* Prompt user for confirmation
|
|
93
|
+
*/
|
|
94
|
+
async function confirm(message) {
|
|
95
|
+
const rl = createInterface({
|
|
96
|
+
input: process.stdin,
|
|
97
|
+
output: process.stderr
|
|
98
|
+
});
|
|
99
|
+
return new Promise((resolve) => {
|
|
100
|
+
rl.question(`${message} [y/N] `, (answer) => {
|
|
101
|
+
rl.close();
|
|
102
|
+
resolve(answer.toLowerCase() === 'y' || answer.toLowerCase() === 'yes');
|
|
103
|
+
});
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
/**
|
|
107
|
+
* Try to install a system dependency
|
|
108
|
+
*/
|
|
109
|
+
async function installDependency(name) {
|
|
110
|
+
const info = INSTALL_COMMANDS[name];
|
|
111
|
+
if (!info) {
|
|
112
|
+
log(`β οΈ Unknown dependency: ${name}`);
|
|
113
|
+
log(` Please install it manually`);
|
|
114
|
+
return false;
|
|
115
|
+
}
|
|
116
|
+
const os = platform();
|
|
117
|
+
const installCmd = info.install[os];
|
|
118
|
+
if (!installCmd) {
|
|
119
|
+
log(`β οΈ No install command for ${name} on ${os}`);
|
|
120
|
+
return false;
|
|
121
|
+
}
|
|
122
|
+
log(`\nπ¦ ${name} - ${info.description}`);
|
|
123
|
+
log(` Install command: ${installCmd}\n`);
|
|
124
|
+
// SECURITY: Warn about curl|bash pattern
|
|
125
|
+
if (installCmd.includes('curl') && (installCmd.includes('| bash') || installCmd.includes('|bash'))) {
|
|
126
|
+
log(`β οΈ SECURITY WARNING: This command downloads and executes a script from the internet.`);
|
|
127
|
+
log(` Only proceed if you trust the source (${name}).\n`);
|
|
128
|
+
}
|
|
129
|
+
const shouldInstall = await confirm(`Install ${name}?`);
|
|
130
|
+
if (!shouldInstall)
|
|
131
|
+
return false;
|
|
132
|
+
log(`\nInstalling ${name}...`);
|
|
133
|
+
try {
|
|
134
|
+
// Use spawnSync with shell:true for complex install commands (from trusted INSTALL_COMMANDS list)
|
|
135
|
+
// This is safe because installCmd comes from hardcoded list, not user input
|
|
136
|
+
const result = spawnSync(installCmd, {
|
|
137
|
+
stdio: 'inherit',
|
|
138
|
+
shell: true
|
|
139
|
+
});
|
|
140
|
+
if (result.status === 0) {
|
|
141
|
+
log(`β ${name} installed successfully\n`);
|
|
142
|
+
return true;
|
|
143
|
+
}
|
|
144
|
+
log(`β Failed to install ${name}`);
|
|
145
|
+
log(` Try running manually: ${installCmd}\n`);
|
|
146
|
+
return false;
|
|
147
|
+
}
|
|
148
|
+
catch {
|
|
149
|
+
log(`β Failed to install ${name}`);
|
|
150
|
+
log(` Try running manually: ${installCmd}\n`);
|
|
151
|
+
return false;
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
/**
|
|
155
|
+
* Check and install missing peer dependencies
|
|
156
|
+
*/
|
|
157
|
+
async function checkPeerDependencies(pkgPath) {
|
|
158
|
+
// Try to load cli4ai.json for peer dependencies
|
|
159
|
+
const cli4aiPath = resolve(pkgPath, 'cli4ai.json');
|
|
160
|
+
let peerDeps = {};
|
|
161
|
+
if (existsSync(cli4aiPath)) {
|
|
162
|
+
try {
|
|
163
|
+
const cli4ai = JSON.parse(readFileSync(cli4aiPath, 'utf-8'));
|
|
164
|
+
peerDeps = cli4ai.peerDependencies || {};
|
|
165
|
+
}
|
|
166
|
+
catch { }
|
|
167
|
+
}
|
|
168
|
+
const missing = [];
|
|
169
|
+
for (const dep of Object.keys(peerDeps)) {
|
|
170
|
+
// Skip npm packages (they start with @ or contain /)
|
|
171
|
+
if (dep.startsWith('@') || dep.includes('/'))
|
|
172
|
+
continue;
|
|
173
|
+
if (!commandExists(dep)) {
|
|
174
|
+
missing.push(dep);
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
if (missing.length === 0)
|
|
178
|
+
return;
|
|
179
|
+
log(`\nβ οΈ Missing system dependencies: ${missing.join(', ')}\n`);
|
|
180
|
+
for (const dep of missing) {
|
|
181
|
+
const installed = await installDependency(dep);
|
|
182
|
+
if (!installed) {
|
|
183
|
+
throw new ExecuteToolError('MISSING_DEPENDENCY', `Cannot run without ${dep}`, {
|
|
184
|
+
dependency: dep,
|
|
185
|
+
packagePath: pkgPath
|
|
186
|
+
});
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
/**
|
|
191
|
+
* Prompt for a secret value
|
|
192
|
+
*/
|
|
193
|
+
async function promptSecret(key, description) {
|
|
194
|
+
const rl = createInterface({
|
|
195
|
+
input: process.stdin,
|
|
196
|
+
output: process.stderr
|
|
197
|
+
});
|
|
198
|
+
if (description) {
|
|
199
|
+
log(` ${description}`);
|
|
200
|
+
}
|
|
201
|
+
return new Promise((resolve) => {
|
|
202
|
+
if (process.stdin.isTTY) {
|
|
203
|
+
// Hide input
|
|
204
|
+
process.stderr.write(` Enter ${key}: `);
|
|
205
|
+
let value = '';
|
|
206
|
+
process.stdin.setRawMode(true);
|
|
207
|
+
process.stdin.resume();
|
|
208
|
+
process.stdin.setEncoding('utf8');
|
|
209
|
+
const onData = (char) => {
|
|
210
|
+
if (char === '\n' || char === '\r') {
|
|
211
|
+
process.stdin.setRawMode(false);
|
|
212
|
+
process.stdin.removeListener('data', onData);
|
|
213
|
+
process.stderr.write('\n');
|
|
214
|
+
rl.close();
|
|
215
|
+
resolve(value);
|
|
216
|
+
}
|
|
217
|
+
else if (char === '\u0003') {
|
|
218
|
+
process.exit(1);
|
|
219
|
+
}
|
|
220
|
+
else if (char === '\u007F') {
|
|
221
|
+
if (value.length > 0)
|
|
222
|
+
value = value.slice(0, -1);
|
|
223
|
+
}
|
|
224
|
+
else {
|
|
225
|
+
value += char;
|
|
226
|
+
}
|
|
227
|
+
};
|
|
228
|
+
process.stdin.on('data', onData);
|
|
229
|
+
}
|
|
230
|
+
else {
|
|
231
|
+
rl.question(` Enter ${key}: `, (answer) => {
|
|
232
|
+
rl.close();
|
|
233
|
+
resolve(answer);
|
|
234
|
+
});
|
|
235
|
+
}
|
|
236
|
+
});
|
|
237
|
+
}
|
|
238
|
+
/**
|
|
239
|
+
* Check required secrets and prompt for missing ones
|
|
240
|
+
*/
|
|
241
|
+
async function checkAndPromptSecrets(pkgPath, pkgName) {
|
|
242
|
+
const cli4aiPath = resolve(pkgPath, 'cli4ai.json');
|
|
243
|
+
const secretsEnv = {};
|
|
244
|
+
if (!existsSync(cli4aiPath))
|
|
245
|
+
return secretsEnv;
|
|
246
|
+
let envDefs = {};
|
|
247
|
+
try {
|
|
248
|
+
const cli4ai = JSON.parse(readFileSync(cli4aiPath, 'utf-8'));
|
|
249
|
+
envDefs = cli4ai.env || {};
|
|
250
|
+
}
|
|
251
|
+
catch {
|
|
252
|
+
return secretsEnv;
|
|
253
|
+
}
|
|
254
|
+
const missingRequired = [];
|
|
255
|
+
for (const [key, def] of Object.entries(envDefs)) {
|
|
256
|
+
// SECURITY: Use package-scoped secret lookup (tries scoped first, then global)
|
|
257
|
+
const value = getSecret(key, pkgName);
|
|
258
|
+
if (value) {
|
|
259
|
+
secretsEnv[key] = value;
|
|
260
|
+
}
|
|
261
|
+
else if (def.required) {
|
|
262
|
+
missingRequired.push({ key, description: def.description });
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
if (missingRequired.length === 0)
|
|
266
|
+
return secretsEnv;
|
|
267
|
+
log(`\nβ οΈ Missing required secrets for ${pkgName}:\n`);
|
|
268
|
+
// Import setSecret dynamically to avoid circular dependency issues
|
|
269
|
+
const { setSecret } = await import('./secrets.js');
|
|
270
|
+
for (const { key, description } of missingRequired) {
|
|
271
|
+
const value = await promptSecret(key, description);
|
|
272
|
+
if (!value) {
|
|
273
|
+
throw new ExecuteToolError('ENV_MISSING', `${key} is required to run ${pkgName}`, {
|
|
274
|
+
package: pkgName,
|
|
275
|
+
secret: key,
|
|
276
|
+
hint: `Set it with: cli4ai secrets set ${key} --scope ${pkgName}`
|
|
277
|
+
});
|
|
278
|
+
}
|
|
279
|
+
// Expand ~ to home directory for paths
|
|
280
|
+
const expandedValue = expandTilde(value);
|
|
281
|
+
// SECURITY: Store secret scoped to package
|
|
282
|
+
setSecret(key, expandedValue, pkgName);
|
|
283
|
+
secretsEnv[key] = expandedValue;
|
|
284
|
+
log(` β ${key} saved to vault (scoped to ${pkgName})\n`);
|
|
285
|
+
}
|
|
286
|
+
log('');
|
|
287
|
+
return secretsEnv;
|
|
288
|
+
}
|
|
289
|
+
function buildRuntimeCommand(entryPath, cmdArgs) {
|
|
290
|
+
// Use tsx for TypeScript files, node for JavaScript
|
|
291
|
+
if (entryPath.endsWith('.ts') || entryPath.endsWith('.tsx')) {
|
|
292
|
+
return { execCmd: 'npx', execArgs: ['tsx', entryPath, ...cmdArgs], runtime: 'node' };
|
|
293
|
+
}
|
|
294
|
+
return { execCmd: 'node', execArgs: [entryPath, ...cmdArgs], runtime: 'node' };
|
|
295
|
+
}
|
|
296
|
+
/**
|
|
297
|
+
* Build security environment variables for scope and sandbox restrictions
|
|
298
|
+
*/
|
|
299
|
+
function buildSecurityEnv(scope, sandbox, cwd) {
|
|
300
|
+
const env = {};
|
|
301
|
+
// Set scope environment variable for tools to respect
|
|
302
|
+
env.CLI4AI_SCOPE = scope;
|
|
303
|
+
// Sandbox restrictions
|
|
304
|
+
if (sandbox) {
|
|
305
|
+
env.CLI4AI_SANDBOX = '1';
|
|
306
|
+
// Restrict file system access to temp directories and package directory
|
|
307
|
+
// Tools should check these env vars and restrict their operations
|
|
308
|
+
const tmpDir = process.env.TMPDIR || process.env.TMP || process.env.TEMP || '/tmp';
|
|
309
|
+
env.CLI4AI_SANDBOX_ALLOWED_PATHS = [
|
|
310
|
+
tmpDir,
|
|
311
|
+
cwd, // Allow access to current working directory
|
|
312
|
+
].join(':');
|
|
313
|
+
// Restrict network access in sandbox mode
|
|
314
|
+
// Tools should check this and limit network operations
|
|
315
|
+
env.CLI4AI_SANDBOX_NETWORK = 'restricted';
|
|
316
|
+
}
|
|
317
|
+
return env;
|
|
318
|
+
}
|
|
319
|
+
async function ensureRuntimeAvailable() {
|
|
320
|
+
if (!commandExists('node')) {
|
|
321
|
+
log('β οΈ Node.js is required to run this tool\n');
|
|
322
|
+
const installed = await installDependency('node');
|
|
323
|
+
if (!installed) {
|
|
324
|
+
throw new ExecuteToolError('MISSING_DEPENDENCY', 'Node.js is required', {
|
|
325
|
+
hint: 'Install Node.js: https://nodejs.org/en/download/'
|
|
326
|
+
});
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
function collectStream(stream) {
|
|
331
|
+
return new Promise((resolve, reject) => {
|
|
332
|
+
const chunks = [];
|
|
333
|
+
stream.on('data', (chunk) => chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(String(chunk))));
|
|
334
|
+
stream.on('error', reject);
|
|
335
|
+
stream.on('end', () => resolve(Buffer.concat(chunks).toString('utf-8')));
|
|
336
|
+
});
|
|
337
|
+
}
|
|
338
|
+
/**
|
|
339
|
+
* Execute a tool command.
|
|
340
|
+
*
|
|
341
|
+
* - When capture === 'inherit', the child inherits stdio (interactive).
|
|
342
|
+
* - When capture === 'pipe', stdout/stderr can be captured and returned.
|
|
343
|
+
*/
|
|
344
|
+
export async function executeTool(options) {
|
|
345
|
+
const startTime = Date.now();
|
|
346
|
+
const invocationDir = options.cwd;
|
|
347
|
+
const pkg = findPackage(options.packageName, invocationDir);
|
|
348
|
+
if (!pkg) {
|
|
349
|
+
throw new ExecuteToolError('NOT_FOUND', `Package not found: ${options.packageName}`, {
|
|
350
|
+
hint: 'Run "cli4ai list" to see installed packages, or "cli4ai add <package>" to install'
|
|
351
|
+
});
|
|
352
|
+
}
|
|
353
|
+
const manifest = loadManifest(pkg.path);
|
|
354
|
+
await ensureRuntimeAvailable();
|
|
355
|
+
await checkPeerDependencies(pkg.path);
|
|
356
|
+
const secretsEnv = await checkAndPromptSecrets(pkg.path, options.packageName);
|
|
357
|
+
const entryPath = resolve(pkg.path, manifest.entry);
|
|
358
|
+
if (!existsSync(entryPath)) {
|
|
359
|
+
throw new ExecuteToolError('NOT_FOUND', `Entry point not found: ${entryPath}`, {
|
|
360
|
+
manifest: resolve(pkg.path, 'cli4ai.json')
|
|
361
|
+
});
|
|
362
|
+
}
|
|
363
|
+
const cmdArgs = [];
|
|
364
|
+
if (options.command)
|
|
365
|
+
cmdArgs.push(options.command);
|
|
366
|
+
cmdArgs.push(...options.args);
|
|
367
|
+
const { execCmd, execArgs, runtime } = buildRuntimeCommand(entryPath, cmdArgs);
|
|
368
|
+
const teeStderr = options.teeStderr ?? true;
|
|
369
|
+
// Build security environment for scope and sandbox
|
|
370
|
+
const scope = options.scope ?? 'full';
|
|
371
|
+
const sandbox = options.sandbox ?? false;
|
|
372
|
+
const securityEnv = buildSecurityEnv(scope, sandbox, invocationDir);
|
|
373
|
+
// Log security restrictions if active
|
|
374
|
+
if (scope !== 'full' || sandbox) {
|
|
375
|
+
const restrictions = [];
|
|
376
|
+
if (scope !== 'full')
|
|
377
|
+
restrictions.push(`scope=${scope}`);
|
|
378
|
+
if (sandbox)
|
|
379
|
+
restrictions.push('sandbox=enabled');
|
|
380
|
+
log(`π Security: ${restrictions.join(', ')}`);
|
|
381
|
+
}
|
|
382
|
+
if (options.capture === 'inherit') {
|
|
383
|
+
const proc = spawn(execCmd, execArgs, {
|
|
384
|
+
stdio: 'inherit',
|
|
385
|
+
cwd: invocationDir,
|
|
386
|
+
env: {
|
|
387
|
+
...process.env,
|
|
388
|
+
INIT_CWD: process.env.INIT_CWD ?? invocationDir,
|
|
389
|
+
CLI4AI_CWD: invocationDir,
|
|
390
|
+
C4AI_PACKAGE_DIR: pkg.path,
|
|
391
|
+
C4AI_PACKAGE_NAME: pkg.name,
|
|
392
|
+
C4AI_ENTRY: entryPath,
|
|
393
|
+
...secretsEnv,
|
|
394
|
+
...securityEnv,
|
|
395
|
+
...(options.env ?? {})
|
|
396
|
+
}
|
|
397
|
+
});
|
|
398
|
+
const exitCode = await new Promise((resolve, reject) => {
|
|
399
|
+
proc.on('close', (code) => resolve(code ?? 0));
|
|
400
|
+
proc.on('error', (err) => {
|
|
401
|
+
reject(new ExecuteToolError('API_ERROR', `Failed to execute: ${err.message}`));
|
|
402
|
+
});
|
|
403
|
+
});
|
|
404
|
+
return {
|
|
405
|
+
exitCode,
|
|
406
|
+
durationMs: Date.now() - startTime,
|
|
407
|
+
packagePath: pkg.path,
|
|
408
|
+
entryPath,
|
|
409
|
+
runtime
|
|
410
|
+
};
|
|
411
|
+
}
|
|
412
|
+
// capture === 'pipe'
|
|
413
|
+
const proc = spawn(execCmd, execArgs, {
|
|
414
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
415
|
+
cwd: invocationDir,
|
|
416
|
+
env: {
|
|
417
|
+
...process.env,
|
|
418
|
+
INIT_CWD: process.env.INIT_CWD ?? invocationDir,
|
|
419
|
+
CLI4AI_CWD: invocationDir,
|
|
420
|
+
C4AI_PACKAGE_DIR: pkg.path,
|
|
421
|
+
C4AI_PACKAGE_NAME: pkg.name,
|
|
422
|
+
C4AI_ENTRY: entryPath,
|
|
423
|
+
...secretsEnv,
|
|
424
|
+
...securityEnv,
|
|
425
|
+
...(options.env ?? {})
|
|
426
|
+
}
|
|
427
|
+
});
|
|
428
|
+
if (options.stdin !== undefined) {
|
|
429
|
+
proc.stdin.write(options.stdin);
|
|
430
|
+
proc.stdin.end();
|
|
431
|
+
}
|
|
432
|
+
else {
|
|
433
|
+
// Donβt block on stdin if nothing is provided
|
|
434
|
+
proc.stdin.end();
|
|
435
|
+
}
|
|
436
|
+
if (teeStderr && proc.stderr) {
|
|
437
|
+
proc.stderr.on('data', (chunk) => {
|
|
438
|
+
process.stderr.write(chunk);
|
|
439
|
+
});
|
|
440
|
+
}
|
|
441
|
+
const stdoutPromise = proc.stdout ? collectStream(proc.stdout) : Promise.resolve('');
|
|
442
|
+
const stderrPromise = proc.stderr ? collectStream(proc.stderr) : Promise.resolve('');
|
|
443
|
+
let timeout;
|
|
444
|
+
if (options.timeoutMs && options.timeoutMs > 0) {
|
|
445
|
+
timeout = setTimeout(() => {
|
|
446
|
+
try {
|
|
447
|
+
proc.kill('SIGTERM');
|
|
448
|
+
}
|
|
449
|
+
catch { }
|
|
450
|
+
setTimeout(() => {
|
|
451
|
+
try {
|
|
452
|
+
proc.kill('SIGKILL');
|
|
453
|
+
}
|
|
454
|
+
catch { }
|
|
455
|
+
}, 250);
|
|
456
|
+
}, options.timeoutMs);
|
|
457
|
+
}
|
|
458
|
+
const exitCode = await new Promise((resolve, reject) => {
|
|
459
|
+
proc.on('close', (code) => resolve(code ?? 0));
|
|
460
|
+
proc.on('error', (err) => reject(new ExecuteToolError('API_ERROR', `Failed to execute: ${err.message}`)));
|
|
461
|
+
}).finally(() => {
|
|
462
|
+
if (timeout)
|
|
463
|
+
clearTimeout(timeout);
|
|
464
|
+
});
|
|
465
|
+
const [stdout, stderr] = await Promise.all([stdoutPromise, stderrPromise]);
|
|
466
|
+
return {
|
|
467
|
+
exitCode,
|
|
468
|
+
durationMs: Date.now() - startTime,
|
|
469
|
+
stdout,
|
|
470
|
+
stderr,
|
|
471
|
+
packagePath: pkg.path,
|
|
472
|
+
entryPath,
|
|
473
|
+
runtime
|
|
474
|
+
};
|
|
475
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* PATH linking for global packages
|
|
3
|
+
*
|
|
4
|
+
* Creates executable symlinks in ~/.cli4ai/bin/ that can be added to PATH
|
|
5
|
+
*/
|
|
6
|
+
import { type Manifest } from './manifest.js';
|
|
7
|
+
export declare const C4AI_BIN: string;
|
|
8
|
+
/**
|
|
9
|
+
* Ensure bin directory exists
|
|
10
|
+
*/
|
|
11
|
+
export declare function ensureBinDir(): void;
|
|
12
|
+
/**
|
|
13
|
+
* Create executable wrapper script for a package
|
|
14
|
+
*
|
|
15
|
+
* Creates a shell script that invokes `cli4ai run <package> [args]`
|
|
16
|
+
*/
|
|
17
|
+
export declare function linkPackage(manifest: Manifest, packagePath: string): string;
|
|
18
|
+
/**
|
|
19
|
+
* Create direct executable wrapper that runs the tool directly
|
|
20
|
+
*
|
|
21
|
+
* This is faster than going through cli4ai run
|
|
22
|
+
*/
|
|
23
|
+
export declare function linkPackageDirect(manifest: Manifest, packagePath: string): string;
|
|
24
|
+
/**
|
|
25
|
+
* Remove executable link for a package
|
|
26
|
+
*/
|
|
27
|
+
export declare function unlinkPackage(packageName: string): boolean;
|
|
28
|
+
/**
|
|
29
|
+
* Check if a package is linked
|
|
30
|
+
*/
|
|
31
|
+
export declare function isPackageLinked(packageName: string): boolean;
|
|
32
|
+
/**
|
|
33
|
+
* Get PATH setup instructions
|
|
34
|
+
*/
|
|
35
|
+
export declare function getPathInstructions(): string;
|
|
36
|
+
/**
|
|
37
|
+
* Check if bin directory is in PATH
|
|
38
|
+
*/
|
|
39
|
+
export declare function isBinInPath(): boolean;
|