cli4ai 0.8.3 ā 0.9.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/package.json +8 -8
- package/src/cli.ts +1 -1
- package/src/commands/add.ts +4 -6
- package/src/commands/info.ts +1 -1
- package/src/commands/init.test.ts +61 -13
- package/src/commands/init.ts +177 -223
- package/src/commands/routines.ts +18 -3
- package/src/commands/scheduler.ts +1 -1
- package/src/commands/update.ts +22 -6
- package/src/core/config.test.ts +2 -2
- package/src/core/config.ts +13 -2
- package/src/core/execute.ts +33 -50
- package/src/core/link.test.ts +5 -5
- package/src/core/link.ts +19 -22
- package/src/core/lockfile.test.ts +1 -1
- package/src/core/manifest.test.ts +2 -2
- package/src/core/manifest.ts +12 -4
- package/src/core/routine-engine.test.ts +1 -1
- package/src/core/scheduler.test.ts +3 -3
- package/src/core/scheduler.ts +7 -2
- package/src/core/secrets.test.ts +1 -1
- package/src/lib/cli.ts +1 -1
- package/src/mcp/adapter.test.ts +1 -1
- package/src/mcp/config-gen.test.ts +1 -1
- package/src/mcp/server.ts +11 -17
package/src/core/config.test.ts
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* Tests for config.ts
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
|
-
import { describe, test, expect, beforeEach, afterEach } from '
|
|
5
|
+
import { describe, test, expect, beforeEach, afterEach } from 'vitest';
|
|
6
6
|
import { mkdtempSync, rmSync, writeFileSync, mkdirSync, existsSync, readFileSync } from 'fs';
|
|
7
7
|
import { join } from 'path';
|
|
8
8
|
import { tmpdir } from 'os';
|
|
@@ -31,7 +31,7 @@ describe('config', () => {
|
|
|
31
31
|
test('has expected defaults', () => {
|
|
32
32
|
expect(DEFAULT_CONFIG.registry).toBe('https://registry.cli4ai.com');
|
|
33
33
|
expect(DEFAULT_CONFIG.localRegistries).toEqual([]);
|
|
34
|
-
expect(DEFAULT_CONFIG.defaultRuntime).toBe('
|
|
34
|
+
expect(DEFAULT_CONFIG.defaultRuntime).toBe('node');
|
|
35
35
|
expect(DEFAULT_CONFIG.mcp.transport).toBe('stdio');
|
|
36
36
|
expect(DEFAULT_CONFIG.telemetry).toBe(false);
|
|
37
37
|
});
|
package/src/core/config.ts
CHANGED
|
@@ -96,7 +96,7 @@ export interface Config {
|
|
|
96
96
|
localRegistries: string[];
|
|
97
97
|
|
|
98
98
|
// Runtime defaults
|
|
99
|
-
defaultRuntime: '
|
|
99
|
+
defaultRuntime: 'node';
|
|
100
100
|
|
|
101
101
|
// MCP defaults
|
|
102
102
|
mcp: {
|
|
@@ -123,7 +123,7 @@ export interface InstalledPackage {
|
|
|
123
123
|
export const DEFAULT_CONFIG: Config = {
|
|
124
124
|
registry: 'https://registry.cli4ai.com',
|
|
125
125
|
localRegistries: [],
|
|
126
|
-
defaultRuntime: '
|
|
126
|
+
defaultRuntime: 'node',
|
|
127
127
|
mcp: {
|
|
128
128
|
transport: 'stdio',
|
|
129
129
|
port: 3100
|
|
@@ -743,7 +743,18 @@ export function getNpmGlobalPackages(): InstalledPackage[] {
|
|
|
743
743
|
* Try to find a package in a global directory
|
|
744
744
|
*/
|
|
745
745
|
function findPackageInGlobalDir(globalDir: string, name: string): InstalledPackage | null {
|
|
746
|
+
// SECURITY: Validate name to prevent path traversal
|
|
747
|
+
if (name.includes('..') || name.includes('/') || name.includes('\\') || name.startsWith('.')) {
|
|
748
|
+
return null;
|
|
749
|
+
}
|
|
750
|
+
|
|
746
751
|
const scopedPath = resolve(globalDir, '@cli4ai', name);
|
|
752
|
+
|
|
753
|
+
// SECURITY: Verify resolved path is under globalDir
|
|
754
|
+
if (!scopedPath.startsWith(resolve(globalDir))) {
|
|
755
|
+
return null;
|
|
756
|
+
}
|
|
757
|
+
|
|
747
758
|
if (!existsSync(scopedPath)) return null;
|
|
748
759
|
|
|
749
760
|
const manifestPath = resolve(scopedPath, 'cli4ai.json');
|
package/src/core/execute.ts
CHANGED
|
@@ -51,7 +51,7 @@ export interface ExecuteToolResult {
|
|
|
51
51
|
stderr?: string;
|
|
52
52
|
packagePath: string;
|
|
53
53
|
entryPath: string;
|
|
54
|
-
runtime: '
|
|
54
|
+
runtime: 'node';
|
|
55
55
|
}
|
|
56
56
|
|
|
57
57
|
export class ExecuteToolError extends Error {
|
|
@@ -94,12 +94,12 @@ const INSTALL_COMMANDS: Record<string, { check: string; install: Record<string,
|
|
|
94
94
|
},
|
|
95
95
|
description: 'Media processing tool'
|
|
96
96
|
},
|
|
97
|
-
'
|
|
98
|
-
check: '
|
|
97
|
+
'node': {
|
|
98
|
+
check: 'node --version',
|
|
99
99
|
install: {
|
|
100
|
-
darwin: '
|
|
101
|
-
linux: 'curl -fsSL https://
|
|
102
|
-
win32: '
|
|
100
|
+
darwin: 'brew install node',
|
|
101
|
+
linux: 'curl -fsSL https://deb.nodesource.com/setup_lts.x | sudo -E bash - && sudo apt-get install -y nodejs',
|
|
102
|
+
win32: 'winget install OpenJS.NodeJS.LTS'
|
|
103
103
|
},
|
|
104
104
|
description: 'JavaScript runtime'
|
|
105
105
|
}
|
|
@@ -158,6 +158,12 @@ async function installDependency(name: string): Promise<boolean> {
|
|
|
158
158
|
log(`\nš¦ ${name} - ${info.description}`);
|
|
159
159
|
log(` Install command: ${installCmd}\n`);
|
|
160
160
|
|
|
161
|
+
// SECURITY: Warn about curl|bash pattern
|
|
162
|
+
if (installCmd.includes('curl') && (installCmd.includes('| bash') || installCmd.includes('|bash'))) {
|
|
163
|
+
log(`ā ļø SECURITY WARNING: This command downloads and executes a script from the internet.`);
|
|
164
|
+
log(` Only proceed if you trust the source (${name}).\n`);
|
|
165
|
+
}
|
|
166
|
+
|
|
161
167
|
const shouldInstall = await confirm(`Install ${name}?`);
|
|
162
168
|
if (!shouldInstall) return false;
|
|
163
169
|
|
|
@@ -295,7 +301,8 @@ async function checkAndPromptSecrets(pkgPath: string, pkgName: string): Promise<
|
|
|
295
301
|
const missingRequired: Array<{ key: string; description?: string }> = [];
|
|
296
302
|
|
|
297
303
|
for (const [key, def] of Object.entries(envDefs)) {
|
|
298
|
-
|
|
304
|
+
// SECURITY: Use package-scoped secret lookup (tries scoped first, then global)
|
|
305
|
+
const value = getSecret(key, pkgName);
|
|
299
306
|
|
|
300
307
|
if (value) {
|
|
301
308
|
secretsEnv[key] = value;
|
|
@@ -318,62 +325,39 @@ async function checkAndPromptSecrets(pkgPath: string, pkgName: string): Promise<
|
|
|
318
325
|
throw new ExecuteToolError('ENV_MISSING', `${key} is required to run ${pkgName}`, {
|
|
319
326
|
package: pkgName,
|
|
320
327
|
secret: key,
|
|
321
|
-
hint: `Set it with: cli4ai secrets set ${key}`
|
|
328
|
+
hint: `Set it with: cli4ai secrets set ${key} --scope ${pkgName}`
|
|
322
329
|
});
|
|
323
330
|
}
|
|
324
331
|
|
|
325
332
|
// Expand ~ to home directory for paths
|
|
326
333
|
const expandedValue = expandTilde(value);
|
|
327
|
-
|
|
334
|
+
// SECURITY: Store secret scoped to package
|
|
335
|
+
setSecret(key, expandedValue, pkgName);
|
|
328
336
|
secretsEnv[key] = expandedValue;
|
|
329
|
-
log(` ā ${key} saved to vault\n`);
|
|
337
|
+
log(` ā ${key} saved to vault (scoped to ${pkgName})\n`);
|
|
330
338
|
}
|
|
331
339
|
|
|
332
340
|
log('');
|
|
333
341
|
return secretsEnv;
|
|
334
342
|
}
|
|
335
343
|
|
|
336
|
-
function buildRuntimeCommand(entryPath: string,
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
case 'bun':
|
|
341
|
-
default:
|
|
342
|
-
return { execCmd: 'bun', execArgs: ['run', entryPath, ...cmdArgs], runtime: 'bun' };
|
|
344
|
+
function buildRuntimeCommand(entryPath: string, cmdArgs: string[]): { execCmd: string; execArgs: string[]; runtime: 'node' } {
|
|
345
|
+
// Use tsx for TypeScript files, node for JavaScript
|
|
346
|
+
if (entryPath.endsWith('.ts') || entryPath.endsWith('.tsx')) {
|
|
347
|
+
return { execCmd: 'npx', execArgs: ['tsx', entryPath, ...cmdArgs], runtime: 'node' };
|
|
343
348
|
}
|
|
349
|
+
return { execCmd: 'node', execArgs: [entryPath, ...cmdArgs], runtime: 'node' };
|
|
344
350
|
}
|
|
345
351
|
|
|
346
|
-
function
|
|
347
|
-
if (manifest.runtime === 'node') return 'node';
|
|
348
|
-
if (manifest.runtime === 'bun') return 'bun';
|
|
349
|
-
|
|
350
|
-
// Unspecified runtime: prefer bun if installed, otherwise fall back to node if available.
|
|
351
|
-
if (commandExists('bun')) return 'bun';
|
|
352
|
-
if (commandExists('node')) return 'node';
|
|
353
|
-
|
|
354
|
-
// Default to bun (will prompt install below).
|
|
355
|
-
return 'bun';
|
|
356
|
-
}
|
|
357
|
-
|
|
358
|
-
async function ensureRuntimeAvailable(runtime: 'bun' | 'node'): Promise<void> {
|
|
359
|
-
if (runtime === 'bun') {
|
|
360
|
-
if (!commandExists('bun')) {
|
|
361
|
-
log('ā ļø bun is required to run this tool\n');
|
|
362
|
-
const installed = await installDependency('bun');
|
|
363
|
-
if (!installed) {
|
|
364
|
-
throw new ExecuteToolError('MISSING_DEPENDENCY', 'bun is required', {
|
|
365
|
-
hint: 'Install bun: curl -fsSL https://bun.sh/install | bash'
|
|
366
|
-
});
|
|
367
|
-
}
|
|
368
|
-
}
|
|
369
|
-
return;
|
|
370
|
-
}
|
|
371
|
-
|
|
372
|
-
// runtime === 'node'
|
|
352
|
+
async function ensureRuntimeAvailable(): Promise<void> {
|
|
373
353
|
if (!commandExists('node')) {
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
354
|
+
log('ā ļø Node.js is required to run this tool\n');
|
|
355
|
+
const installed = await installDependency('node');
|
|
356
|
+
if (!installed) {
|
|
357
|
+
throw new ExecuteToolError('MISSING_DEPENDENCY', 'Node.js is required', {
|
|
358
|
+
hint: 'Install Node.js: https://nodejs.org/en/download/'
|
|
359
|
+
});
|
|
360
|
+
}
|
|
377
361
|
}
|
|
378
362
|
}
|
|
379
363
|
|
|
@@ -426,8 +410,7 @@ export async function executeTool(options: ExecuteToolOptions): Promise<ExecuteT
|
|
|
426
410
|
log('');
|
|
427
411
|
}
|
|
428
412
|
|
|
429
|
-
|
|
430
|
-
await ensureRuntimeAvailable(runtime);
|
|
413
|
+
await ensureRuntimeAvailable();
|
|
431
414
|
|
|
432
415
|
await checkPeerDependencies(pkg.path);
|
|
433
416
|
|
|
@@ -444,7 +427,7 @@ export async function executeTool(options: ExecuteToolOptions): Promise<ExecuteT
|
|
|
444
427
|
if (options.command) cmdArgs.push(options.command);
|
|
445
428
|
cmdArgs.push(...options.args);
|
|
446
429
|
|
|
447
|
-
const { execCmd, execArgs } = buildRuntimeCommand(entryPath,
|
|
430
|
+
const { execCmd, execArgs, runtime } = buildRuntimeCommand(entryPath, cmdArgs);
|
|
448
431
|
|
|
449
432
|
const teeStderr = options.teeStderr ?? true;
|
|
450
433
|
|
package/src/core/link.test.ts
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* Tests for link.ts
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
|
-
import { describe, test, expect, beforeEach, afterEach } from '
|
|
5
|
+
import { describe, test, expect, beforeEach, afterEach } from 'vitest';
|
|
6
6
|
import { mkdtempSync, rmSync, readFileSync, writeFileSync, existsSync, statSync, mkdirSync } from 'fs';
|
|
7
7
|
import { join, resolve } from 'path';
|
|
8
8
|
import { tmpdir, homedir } from 'os';
|
|
@@ -36,7 +36,7 @@ describe('link', () => {
|
|
|
36
36
|
name,
|
|
37
37
|
version,
|
|
38
38
|
entry: 'run.ts',
|
|
39
|
-
runtime: '
|
|
39
|
+
runtime: 'node'
|
|
40
40
|
});
|
|
41
41
|
|
|
42
42
|
describe('C4AI_BIN constant', () => {
|
|
@@ -101,7 +101,7 @@ describe('link', () => {
|
|
|
101
101
|
expect(existsSync(binPath)).toBe(true);
|
|
102
102
|
const content = readFileSync(binPath, 'utf-8');
|
|
103
103
|
expect(content).toContain('#!/bin/sh');
|
|
104
|
-
expect(content).toContain('
|
|
104
|
+
expect(content).toContain('npx tsx');
|
|
105
105
|
expect(content).toContain('run.ts');
|
|
106
106
|
});
|
|
107
107
|
|
|
@@ -123,7 +123,7 @@ describe('link', () => {
|
|
|
123
123
|
expect(content).not.toContain('node run');
|
|
124
124
|
});
|
|
125
125
|
|
|
126
|
-
test('defaults to
|
|
126
|
+
test('defaults to node runtime with tsx for TypeScript', () => {
|
|
127
127
|
const manifest: Manifest = {
|
|
128
128
|
name: 'no-runtime',
|
|
129
129
|
version: '1.0.0',
|
|
@@ -135,7 +135,7 @@ describe('link', () => {
|
|
|
135
135
|
const binPath = linkPackageDirect(manifest, packagePath);
|
|
136
136
|
|
|
137
137
|
const content = readFileSync(binPath, 'utf-8');
|
|
138
|
-
expect(content).toContain('
|
|
138
|
+
expect(content).toContain('npx tsx');
|
|
139
139
|
});
|
|
140
140
|
|
|
141
141
|
test('includes full path to entry', () => {
|
package/src/core/link.ts
CHANGED
|
@@ -115,25 +115,18 @@ export function linkPackageDirect(manifest: Manifest, packagePath: string): stri
|
|
|
115
115
|
|
|
116
116
|
const binPath = join(C4AI_BIN, manifest.name);
|
|
117
117
|
const entryPath = resolve(packagePath, manifest.entry);
|
|
118
|
-
const runtime = manifest.runtime || 'bun';
|
|
119
118
|
|
|
120
119
|
// SECURITY: Shell-escape the entry path to prevent injection
|
|
121
120
|
const safeEntryPath = shellEscape(entryPath);
|
|
122
121
|
const safeName = shellEscape(manifest.name);
|
|
123
122
|
const safeVersion = shellEscape(manifest.version);
|
|
124
123
|
|
|
125
|
-
// Build runtime command
|
|
126
|
-
// - bun: bun run <file>
|
|
127
|
-
// - node: node <file>
|
|
124
|
+
// Build runtime command - use tsx for TypeScript, node for JavaScript
|
|
128
125
|
let execCommand: string;
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
case 'bun':
|
|
134
|
-
default:
|
|
135
|
-
execCommand = `bun run ${safeEntryPath}`;
|
|
136
|
-
break;
|
|
126
|
+
if (entryPath.endsWith('.ts') || entryPath.endsWith('.tsx')) {
|
|
127
|
+
execCommand = `npx tsx ${safeEntryPath}`;
|
|
128
|
+
} else {
|
|
129
|
+
execCommand = `node ${safeEntryPath}`;
|
|
137
130
|
}
|
|
138
131
|
|
|
139
132
|
// Create wrapper script that runs the tool directly
|
|
@@ -152,18 +145,17 @@ exec ${execCommand} "$@"
|
|
|
152
145
|
// Windows compatibility: generate .cmd and .ps1 launchers
|
|
153
146
|
if (process.platform === 'win32') {
|
|
154
147
|
const quotedEntry = `"${entryPath.replaceAll('"', '""')}"`;
|
|
155
|
-
const runtimeCmd = runtime === 'node' ? 'node' : 'bun';
|
|
156
148
|
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
149
|
+
let cmdContent: string;
|
|
150
|
+
let ps1Content: string;
|
|
151
|
+
if (entryPath.endsWith('.ts') || entryPath.endsWith('.tsx')) {
|
|
152
|
+
cmdContent = `@echo off\r\nnpx tsx ${quotedEntry} %*\r\nexit /b %errorlevel%\r\n`;
|
|
153
|
+
ps1Content = `& npx tsx ${quotedEntry} @args\nexit $LASTEXITCODE\n`;
|
|
154
|
+
} else {
|
|
155
|
+
cmdContent = `@echo off\r\nnode ${quotedEntry} %*\r\nexit /b %errorlevel%\r\n`;
|
|
156
|
+
ps1Content = `& node ${quotedEntry} @args\nexit $LASTEXITCODE\n`;
|
|
157
|
+
}
|
|
161
158
|
writeFileSync(binPath + '.cmd', cmdContent);
|
|
162
|
-
|
|
163
|
-
const ps1Content =
|
|
164
|
-
runtime === 'node'
|
|
165
|
-
? `& "${runtimeCmd}" ${quotedEntry} @args\nexit $LASTEXITCODE\n`
|
|
166
|
-
: `& "${runtimeCmd}" run ${quotedEntry} @args\nexit $LASTEXITCODE\n`;
|
|
167
159
|
writeFileSync(binPath + '.ps1', ps1Content);
|
|
168
160
|
}
|
|
169
161
|
|
|
@@ -174,6 +166,11 @@ exec ${execCommand} "$@"
|
|
|
174
166
|
* Remove executable link for a package
|
|
175
167
|
*/
|
|
176
168
|
export function unlinkPackage(packageName: string): boolean {
|
|
169
|
+
// SECURITY: Validate package name to prevent path traversal
|
|
170
|
+
if (!isShellSafe(packageName) || packageName.includes('..')) {
|
|
171
|
+
throw new Error(`Invalid package name: ${packageName}`);
|
|
172
|
+
}
|
|
173
|
+
|
|
177
174
|
const basePath = join(C4AI_BIN, packageName);
|
|
178
175
|
const candidates = process.platform === 'win32'
|
|
179
176
|
? [basePath, basePath + '.cmd', basePath + '.ps1']
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* Tests for lockfile.ts
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
|
-
import { describe, test, expect, beforeEach, afterEach } from '
|
|
5
|
+
import { describe, test, expect, beforeEach, afterEach } from 'vitest';
|
|
6
6
|
import { mkdtempSync, rmSync, readFileSync, writeFileSync } from 'fs';
|
|
7
7
|
import { join } from 'path';
|
|
8
8
|
import { tmpdir } from 'os';
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* Tests for manifest.ts
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
|
-
import { describe, test, expect, beforeEach, afterEach } from '
|
|
5
|
+
import { describe, test, expect, beforeEach, afterEach } from 'vitest';
|
|
6
6
|
import { mkdtempSync, rmSync, writeFileSync, mkdirSync } from 'fs';
|
|
7
7
|
import { join } from 'path';
|
|
8
8
|
import { tmpdir } from 'os';
|
|
@@ -304,7 +304,7 @@ describe('manifest', () => {
|
|
|
304
304
|
expect(manifest.name).toBe('my-tool');
|
|
305
305
|
expect(manifest.version).toBe('1.0.0');
|
|
306
306
|
expect(manifest.entry).toBe('run.ts');
|
|
307
|
-
expect(manifest.runtime).toBe('
|
|
307
|
+
expect(manifest.runtime).toBe('node');
|
|
308
308
|
});
|
|
309
309
|
|
|
310
310
|
test('normalizes name', () => {
|
package/src/core/manifest.ts
CHANGED
|
@@ -73,8 +73,8 @@ export interface Manifest {
|
|
|
73
73
|
homepage?: string;
|
|
74
74
|
keywords?: string[];
|
|
75
75
|
|
|
76
|
-
// Runtime (
|
|
77
|
-
runtime?: '
|
|
76
|
+
// Runtime (node is default, bun kept for backwards compatibility)
|
|
77
|
+
runtime?: 'node' | 'bun';
|
|
78
78
|
|
|
79
79
|
// Commands (for MCP generation)
|
|
80
80
|
commands?: Record<string, CommandDef>;
|
|
@@ -134,6 +134,14 @@ export function validateManifest(manifest: unknown, source?: string): Manifest {
|
|
|
134
134
|
});
|
|
135
135
|
}
|
|
136
136
|
|
|
137
|
+
// SECURITY: Validate entry is a relative path that stays within package directory
|
|
138
|
+
if (m.entry.startsWith('/') || m.entry.startsWith('\\') || m.entry.includes('..')) {
|
|
139
|
+
throw new ManifestValidationError(
|
|
140
|
+
'Invalid "entry" - must be a relative path without ".." (security: path traversal)',
|
|
141
|
+
{ source, got: m.entry }
|
|
142
|
+
);
|
|
143
|
+
}
|
|
144
|
+
|
|
137
145
|
// Optional: runtime (deno not supported)
|
|
138
146
|
if (m.runtime !== undefined && !['bun', 'node'].includes(m.runtime as string)) {
|
|
139
147
|
throw new ManifestValidationError('Invalid "runtime" (must be bun or node)', {
|
|
@@ -230,7 +238,7 @@ export function loadFromPackageJson(dir: string): Manifest | null {
|
|
|
230
238
|
description: pkg.description,
|
|
231
239
|
author: pkg.author,
|
|
232
240
|
license: pkg.license,
|
|
233
|
-
runtime: '
|
|
241
|
+
runtime: 'node',
|
|
234
242
|
keywords: pkg.keywords
|
|
235
243
|
};
|
|
236
244
|
} catch {
|
|
@@ -312,7 +320,7 @@ export function createManifest(name: string, options: Partial<Manifest> = {}): M
|
|
|
312
320
|
name: name.toLowerCase().replace(/[^a-z0-9-]/g, '-'),
|
|
313
321
|
version: '1.0.0',
|
|
314
322
|
entry: 'run.ts',
|
|
315
|
-
runtime: '
|
|
323
|
+
runtime: 'node',
|
|
316
324
|
description: options.description || `${name} tool`,
|
|
317
325
|
...options
|
|
318
326
|
};
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* Tests for routine-engine.ts
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
|
-
import { describe, test, expect, beforeEach, afterEach } from '
|
|
5
|
+
import { describe, test, expect, beforeEach, afterEach } from 'vitest';
|
|
6
6
|
import { mkdtempSync, rmSync, writeFileSync } from 'fs';
|
|
7
7
|
import { join } from 'path';
|
|
8
8
|
import { tmpdir } from 'os';
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* Tests for scheduler core functionality.
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
|
-
import { describe, test, expect, beforeEach, afterEach } from '
|
|
5
|
+
import { describe, test, expect, beforeEach, afterEach } from 'vitest';
|
|
6
6
|
import { rmSync, existsSync, readdirSync } from 'fs';
|
|
7
7
|
import { join } from 'path';
|
|
8
8
|
import {
|
|
@@ -82,8 +82,8 @@ describe('getNextRunTime', () => {
|
|
|
82
82
|
const nextRun = getNextRunTime(schedule, now);
|
|
83
83
|
|
|
84
84
|
expect(nextRun).not.toBeNull();
|
|
85
|
-
expect(nextRun!.
|
|
86
|
-
expect(nextRun!.
|
|
85
|
+
expect(nextRun!.getUTCMinutes()).toBe(0);
|
|
86
|
+
expect(nextRun!.getUTCHours()).toBe(11); // Next hour in UTC
|
|
87
87
|
});
|
|
88
88
|
|
|
89
89
|
test('returns earliest when both cron and interval specified', () => {
|
package/src/core/scheduler.ts
CHANGED
|
@@ -141,8 +141,13 @@ export function getDaemonPid(): number | null {
|
|
|
141
141
|
}
|
|
142
142
|
|
|
143
143
|
try {
|
|
144
|
-
const
|
|
145
|
-
|
|
144
|
+
const content = readFileSync(SCHEDULER_PID_FILE, 'utf-8').trim();
|
|
145
|
+
// SECURITY: Validate PID format and bounds
|
|
146
|
+
if (!/^\d+$/.test(content)) return null;
|
|
147
|
+
const pid = parseInt(content, 10);
|
|
148
|
+
// PIDs must be positive integers within reasonable bounds
|
|
149
|
+
// Max PID varies by OS but is typically 32768-4194304
|
|
150
|
+
if (!Number.isInteger(pid) || pid < 1 || pid > 4194304) return null;
|
|
146
151
|
return pid;
|
|
147
152
|
} catch {
|
|
148
153
|
return null;
|
package/src/core/secrets.test.ts
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* Tests for secrets.ts
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
|
-
import { describe, test, expect, beforeEach, afterEach } from '
|
|
5
|
+
import { describe, test, expect, beforeEach, afterEach } from 'vitest';
|
|
6
6
|
import { mkdtempSync, rmSync, writeFileSync, existsSync, readFileSync } from 'fs';
|
|
7
7
|
import { join } from 'path';
|
|
8
8
|
import { tmpdir, hostname, userInfo } from 'os';
|
package/src/lib/cli.ts
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
import { Command } from 'commander';
|
|
7
7
|
|
|
8
8
|
export const BRAND = 'cli4ai - cli4ai.com';
|
|
9
|
-
export const VERSION = '0.
|
|
9
|
+
export const VERSION = '0.9.1';
|
|
10
10
|
|
|
11
11
|
// āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
|
|
12
12
|
// TYPES
|
package/src/mcp/adapter.test.ts
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* Tests for mcp/config-gen.ts
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
|
-
import { describe, test, expect, beforeEach, afterEach } from '
|
|
5
|
+
import { describe, test, expect, beforeEach, afterEach } from 'vitest';
|
|
6
6
|
import { mkdtempSync, rmSync, writeFileSync, mkdirSync } from 'fs';
|
|
7
7
|
import { join } from 'path';
|
|
8
8
|
import { tmpdir } from 'os';
|
package/src/mcp/server.ts
CHANGED
|
@@ -251,10 +251,9 @@ export class McpServer {
|
|
|
251
251
|
|
|
252
252
|
// Execute the CLI tool
|
|
253
253
|
const entryPath = resolve(this.packagePath, this.manifest.entry);
|
|
254
|
-
const runtime = this.manifest.runtime || 'bun';
|
|
255
254
|
|
|
256
255
|
try {
|
|
257
|
-
const result = await this.executeCommand(
|
|
256
|
+
const result = await this.executeCommand(entryPath, cmdArgs);
|
|
258
257
|
auditLog(this.manifest.name, name, args, 'success');
|
|
259
258
|
this.sendResult(id, {
|
|
260
259
|
content: [{ type: 'text', text: result }]
|
|
@@ -269,30 +268,25 @@ export class McpServer {
|
|
|
269
268
|
}
|
|
270
269
|
}
|
|
271
270
|
|
|
272
|
-
private executeCommand(
|
|
271
|
+
private executeCommand(entryPath: string, args: string[]): Promise<string> {
|
|
273
272
|
return new Promise((resolve, reject) => {
|
|
274
|
-
//
|
|
275
|
-
// - bun: bun run <file> [args]
|
|
276
|
-
// - node: node <file> [args]
|
|
273
|
+
// Use tsx for TypeScript files, node for JavaScript
|
|
277
274
|
let cmd: string;
|
|
278
275
|
let cmdArgs: string[];
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
default:
|
|
286
|
-
cmd = 'bun';
|
|
287
|
-
cmdArgs = ['run', entryPath, ...args];
|
|
288
|
-
break;
|
|
276
|
+
if (entryPath.endsWith('.ts') || entryPath.endsWith('.tsx')) {
|
|
277
|
+
cmd = 'npx';
|
|
278
|
+
cmdArgs = ['tsx', entryPath, ...args];
|
|
279
|
+
} else {
|
|
280
|
+
cmd = 'node';
|
|
281
|
+
cmdArgs = [entryPath, ...args];
|
|
289
282
|
}
|
|
290
283
|
|
|
291
284
|
// Inject secrets from manifest env definitions
|
|
285
|
+
// SECURITY: Use package-scoped secret lookup (tries scoped first, then global)
|
|
292
286
|
const secretsEnv: Record<string, string> = {};
|
|
293
287
|
if (this.manifest.env) {
|
|
294
288
|
for (const key of Object.keys(this.manifest.env)) {
|
|
295
|
-
const value = getSecret(key);
|
|
289
|
+
const value = getSecret(key, this.manifest.name);
|
|
296
290
|
if (value) {
|
|
297
291
|
secretsEnv[key] = value;
|
|
298
292
|
}
|