cli4ai 0.8.1 → 0.8.3
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 +4 -3
- package/src/bin.ts +21 -21
- package/src/cli.ts +50 -0
- package/src/commands/add.ts +43 -0
- package/src/commands/browse.ts +1 -1
- package/src/commands/config.ts +31 -3
- package/src/commands/list.ts +11 -28
- package/src/commands/mcp-config.ts +14 -4
- package/src/commands/remove.ts +6 -0
- package/src/commands/routines.ts +1 -1
- package/src/commands/run.ts +4 -2
- package/src/commands/scheduler.ts +438 -0
- package/src/commands/secrets.ts +21 -2
- package/src/commands/update.ts +7 -8
- package/src/core/config.test.ts +1 -1
- package/src/core/config.ts +200 -37
- package/src/core/execute.ts +56 -15
- package/src/core/link.ts +52 -9
- package/src/core/lockfile.ts +4 -2
- package/src/core/routine-engine.ts +103 -1
- package/src/core/routines.ts +58 -1
- package/src/core/scheduler-daemon.ts +94 -0
- package/src/core/scheduler.test.ts +291 -0
- package/src/core/scheduler.ts +601 -0
- package/src/lib/cli.ts +25 -5
- package/src/mcp/adapter.ts +14 -6
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "cli4ai",
|
|
3
|
-
"version": "0.8.
|
|
4
|
-
"description": "The package manager for AI CLI tools -
|
|
3
|
+
"version": "0.8.3",
|
|
4
|
+
"description": "The package manager for AI CLI tools - cli4ai.com",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
7
7
|
"cli4ai": "./src/bin.ts"
|
|
@@ -16,6 +16,7 @@
|
|
|
16
16
|
},
|
|
17
17
|
"dependencies": {
|
|
18
18
|
"commander": "^14.0.0",
|
|
19
|
+
"cron-parser": "^4.9.0",
|
|
19
20
|
"semver": "^7.6.0"
|
|
20
21
|
},
|
|
21
22
|
"devDependencies": {
|
|
@@ -30,7 +31,7 @@
|
|
|
30
31
|
"type": "git",
|
|
31
32
|
"url": "https://github.com/cliforai/framework"
|
|
32
33
|
},
|
|
33
|
-
"homepage": "https://
|
|
34
|
+
"homepage": "https://cli4ai.com",
|
|
34
35
|
"keywords": [
|
|
35
36
|
"cli",
|
|
36
37
|
"ai",
|
package/src/bin.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env bun
|
|
2
2
|
/**
|
|
3
3
|
* cli4ai - The package manager for AI CLI tools
|
|
4
|
-
*
|
|
4
|
+
* cli4ai.com
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
7
|
import { createProgram } from './cli.js';
|
|
@@ -32,13 +32,13 @@ async function showAnimation(): Promise<void> {
|
|
|
32
32
|
const food = '·';
|
|
33
33
|
|
|
34
34
|
for (let pos = 0; pos <= width; pos++) {
|
|
35
|
-
process.
|
|
35
|
+
process.stderr.write('\r\x1B[K');
|
|
36
36
|
const frame = robot[pos % robot.length];
|
|
37
37
|
const trail = ' '.repeat(pos) + food.repeat(width - pos);
|
|
38
|
-
process.
|
|
38
|
+
process.stderr.write(` ${CYAN}${frame}${RESET}${DIM}${trail}${RESET}`);
|
|
39
39
|
await sleep(20);
|
|
40
40
|
}
|
|
41
|
-
process.
|
|
41
|
+
process.stderr.write('\r\x1B[K');
|
|
42
42
|
}
|
|
43
43
|
|
|
44
44
|
/**
|
|
@@ -46,19 +46,19 @@ async function showAnimation(): Promise<void> {
|
|
|
46
46
|
*/
|
|
47
47
|
async function animateRobotFace(): Promise<void> {
|
|
48
48
|
const faces = ['[•_•]', '[•_•]', '[-_-]', '[•_•]', '[•_•]', '[°_°]', '[•_•]'];
|
|
49
|
-
const line = ` ${BOLD}${CYAN}cli4ai${RESET} ${DIM}─${RESET} ${WHITE}
|
|
49
|
+
const line = ` ${BOLD}${CYAN}cli4ai${RESET} ${DIM}─${RESET} ${WHITE}cli4ai.com${RESET}`;
|
|
50
50
|
|
|
51
51
|
for (const face of faces) {
|
|
52
|
-
process.
|
|
52
|
+
process.stderr.write(`\r ${CYAN}${face}${RESET}${line}`);
|
|
53
53
|
await sleep(120);
|
|
54
54
|
}
|
|
55
|
-
console.
|
|
55
|
+
console.error('');
|
|
56
56
|
}
|
|
57
57
|
|
|
58
58
|
async function showBanner(): Promise<void> {
|
|
59
|
-
if (!process.
|
|
59
|
+
if (!process.stderr.isTTY) return;
|
|
60
60
|
|
|
61
|
-
console.
|
|
61
|
+
console.error('');
|
|
62
62
|
|
|
63
63
|
// Fun robot eating animation
|
|
64
64
|
await showAnimation();
|
|
@@ -66,18 +66,18 @@ async function showBanner(): Promise<void> {
|
|
|
66
66
|
// Animated robot branding (blinking)
|
|
67
67
|
await animateRobotFace();
|
|
68
68
|
|
|
69
|
-
console.
|
|
70
|
-
console.
|
|
71
|
-
console.
|
|
72
|
-
console.
|
|
73
|
-
console.
|
|
74
|
-
console.
|
|
75
|
-
console.
|
|
76
|
-
console.
|
|
77
|
-
console.
|
|
78
|
-
console.
|
|
79
|
-
console.
|
|
80
|
-
console.
|
|
69
|
+
console.error(` ${DIM}The package manager for AI CLI tools${RESET}`);
|
|
70
|
+
console.error(` ${DIM}v${VERSION}${RESET}`);
|
|
71
|
+
console.error('');
|
|
72
|
+
console.error(` ${BOLD}Commands${RESET}`);
|
|
73
|
+
console.error(` ${DIM}${'─'.repeat(40)}${RESET}`);
|
|
74
|
+
console.error(` ${GREEN}browse${RESET} ${DIM}Browse & install packages${RESET}`);
|
|
75
|
+
console.error(` ${GREEN}run${RESET} ${CYAN}<pkg> <cmd>${RESET} ${DIM}Run a tool command${RESET}`);
|
|
76
|
+
console.error(` ${GREEN}ls${RESET} ${DIM}List installed packages${RESET}`);
|
|
77
|
+
console.error(` ${GREEN}update${RESET} ${DIM}Update all packages${RESET}`);
|
|
78
|
+
console.error('');
|
|
79
|
+
console.error(` ${DIM}Run${RESET} ${WHITE}cli4ai --help${RESET} ${DIM}for all commands${RESET}`);
|
|
80
|
+
console.error('');
|
|
81
81
|
|
|
82
82
|
// Check for updates in background (non-blocking)
|
|
83
83
|
checkUpdatesInBackground();
|
package/src/cli.ts
CHANGED
|
@@ -26,6 +26,14 @@ import {
|
|
|
26
26
|
import { browseCommand } from './commands/browse.js';
|
|
27
27
|
import { updateCommand } from './commands/update.js';
|
|
28
28
|
import { routinesListCommand, routinesRunCommand, routinesShowCommand, routinesCreateCommand, routinesEditCommand, routinesRemoveCommand } from './commands/routines.js';
|
|
29
|
+
import {
|
|
30
|
+
schedulerStartCommand,
|
|
31
|
+
schedulerStopCommand,
|
|
32
|
+
schedulerStatusCommand,
|
|
33
|
+
schedulerLogsCommand,
|
|
34
|
+
schedulerHistoryCommand,
|
|
35
|
+
schedulerRunCommand
|
|
36
|
+
} from './commands/scheduler.js';
|
|
29
37
|
|
|
30
38
|
export function createProgram(): Command {
|
|
31
39
|
const program = new Command()
|
|
@@ -252,5 +260,47 @@ Pass-through:
|
|
|
252
260
|
.option('--dry-run', 'Show execution plan without running')
|
|
253
261
|
.action(withErrorHandling(routinesRunCommand));
|
|
254
262
|
|
|
263
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
264
|
+
// SCHEDULER
|
|
265
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
266
|
+
|
|
267
|
+
const scheduler = program
|
|
268
|
+
.command('scheduler')
|
|
269
|
+
.description('Manage scheduled routine execution');
|
|
270
|
+
|
|
271
|
+
scheduler
|
|
272
|
+
.command('start')
|
|
273
|
+
.description('Start the scheduler daemon')
|
|
274
|
+
.option('-f, --foreground', 'Run in foreground (blocking)')
|
|
275
|
+
.action(withErrorHandling(schedulerStartCommand));
|
|
276
|
+
|
|
277
|
+
scheduler
|
|
278
|
+
.command('stop')
|
|
279
|
+
.description('Stop the scheduler daemon')
|
|
280
|
+
.action(withErrorHandling(schedulerStopCommand));
|
|
281
|
+
|
|
282
|
+
scheduler
|
|
283
|
+
.command('status')
|
|
284
|
+
.description('Show scheduler status and upcoming runs')
|
|
285
|
+
.action(withErrorHandling(schedulerStatusCommand));
|
|
286
|
+
|
|
287
|
+
scheduler
|
|
288
|
+
.command('logs')
|
|
289
|
+
.description('View scheduler logs')
|
|
290
|
+
.option('-f, --follow', 'Follow log output')
|
|
291
|
+
.option('-n, --lines <n>', 'Number of lines to show', '50')
|
|
292
|
+
.action(withErrorHandling(schedulerLogsCommand));
|
|
293
|
+
|
|
294
|
+
scheduler
|
|
295
|
+
.command('history [routine]')
|
|
296
|
+
.description('View routine execution history')
|
|
297
|
+
.option('-n, --limit <n>', 'Number of records to show', '20')
|
|
298
|
+
.action(withErrorHandling(schedulerHistoryCommand));
|
|
299
|
+
|
|
300
|
+
scheduler
|
|
301
|
+
.command('run <routine>')
|
|
302
|
+
.description('Manually trigger a scheduled routine')
|
|
303
|
+
.action(withErrorHandling(schedulerRunCommand));
|
|
304
|
+
|
|
255
305
|
return program;
|
|
256
306
|
}
|
package/src/commands/add.ts
CHANGED
|
@@ -45,6 +45,47 @@ interface InstallPlan {
|
|
|
45
45
|
fromNpm?: boolean;
|
|
46
46
|
}
|
|
47
47
|
|
|
48
|
+
const PKG_NAME_PATTERN = /^[a-z0-9]([a-z0-9-]*[a-z0-9])?$/;
|
|
49
|
+
const CLI4AI_SCOPED_PKG_PATTERN = /^@cli4ai\/[a-z0-9]([a-z0-9-]*[a-z0-9])?$/;
|
|
50
|
+
const URL_LIKE_PATTERN = /^[a-zA-Z][a-zA-Z0-9+.-]*:\/\//;
|
|
51
|
+
|
|
52
|
+
function validatePackageSpecifier(pkg: string): void {
|
|
53
|
+
if (URL_LIKE_PATTERN.test(pkg)) {
|
|
54
|
+
let parsed: URL;
|
|
55
|
+
try {
|
|
56
|
+
parsed = new URL(pkg);
|
|
57
|
+
} catch {
|
|
58
|
+
outputError('INVALID_INPUT', 'Invalid URL', { url: pkg });
|
|
59
|
+
}
|
|
60
|
+
if (parsed.protocol !== 'https:') {
|
|
61
|
+
outputError('INVALID_INPUT', 'Unsupported URL protocol', {
|
|
62
|
+
url: pkg,
|
|
63
|
+
protocol: parsed.protocol,
|
|
64
|
+
allowed: ['https:']
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
outputError('INVALID_INPUT', 'Installing from URLs is not supported', {
|
|
68
|
+
url: pkg,
|
|
69
|
+
hint: 'Use a local path (./path) or a package name (e.g. github, @cli4ai/github)'
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
if (pkg.includes('\\') || pkg.includes('..')) {
|
|
74
|
+
outputError('INVALID_INPUT', 'Invalid package specifier', { package: pkg });
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
if (pkg.startsWith('@cli4ai/')) {
|
|
78
|
+
if (!CLI4AI_SCOPED_PKG_PATTERN.test(pkg)) {
|
|
79
|
+
outputError('INVALID_INPUT', 'Invalid package name', { package: pkg });
|
|
80
|
+
}
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
if (!PKG_NAME_PATTERN.test(pkg)) {
|
|
85
|
+
outputError('INVALID_INPUT', 'Invalid package name', { package: pkg });
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
48
89
|
/**
|
|
49
90
|
* Prompt user for confirmation
|
|
50
91
|
*/
|
|
@@ -446,6 +487,8 @@ async function resolvePackage(
|
|
|
446
487
|
return { manifest, path: absolutePath };
|
|
447
488
|
}
|
|
448
489
|
|
|
490
|
+
validatePackageSpecifier(pkg);
|
|
491
|
+
|
|
449
492
|
// Check local registries first
|
|
450
493
|
const config = loadConfig();
|
|
451
494
|
for (const registryPath of config.localRegistries) {
|
package/src/commands/browse.ts
CHANGED
|
@@ -166,7 +166,7 @@ async function multiSelect(items: BrowseItem[]): Promise<string[]> {
|
|
|
166
166
|
// Robot header
|
|
167
167
|
log('');
|
|
168
168
|
log(` ${CYAN}${BOLD}[•_•]${RESET} ${BOLD}cli4ai${RESET} ${DIM}─${RESET} ${WHITE}Package Browser${RESET}`);
|
|
169
|
-
log(` ${DIM}
|
|
169
|
+
log(` ${DIM}cli4ai.com${RESET}`);
|
|
170
170
|
log('');
|
|
171
171
|
|
|
172
172
|
// Box top
|
package/src/commands/config.ts
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
import { output, outputError, log } from '../lib/cli.js';
|
|
6
6
|
import {
|
|
7
7
|
loadConfig,
|
|
8
|
-
|
|
8
|
+
updateConfig,
|
|
9
9
|
addLocalRegistry,
|
|
10
10
|
removeLocalRegistry,
|
|
11
11
|
CLI4AI_HOME,
|
|
@@ -72,8 +72,13 @@ export async function configCommand(
|
|
|
72
72
|
|
|
73
73
|
// Set key=value
|
|
74
74
|
if (key && value) {
|
|
75
|
-
const
|
|
76
|
-
|
|
75
|
+
const parsed = parseValue(value);
|
|
76
|
+
|
|
77
|
+
if (key === 'registry') {
|
|
78
|
+
validateRegistryUrl(parsed);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const updated = updateConfig((current) => setNestedValue(current, key, parsed));
|
|
77
82
|
log(`Set ${key} = ${value}`);
|
|
78
83
|
output({
|
|
79
84
|
action: 'set',
|
|
@@ -84,6 +89,29 @@ export async function configCommand(
|
|
|
84
89
|
}
|
|
85
90
|
}
|
|
86
91
|
|
|
92
|
+
function validateRegistryUrl(value: unknown): void {
|
|
93
|
+
if (typeof value !== 'string' || value.trim().length === 0) {
|
|
94
|
+
outputError('INVALID_INPUT', 'Registry must be a non-empty URL string', {
|
|
95
|
+
got: value
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
let url: URL;
|
|
100
|
+
try {
|
|
101
|
+
url = new URL(value);
|
|
102
|
+
} catch {
|
|
103
|
+
outputError('INVALID_INPUT', `Invalid registry URL: ${value}`, {
|
|
104
|
+
hint: 'Use a valid http(s) URL, e.g. https://registry.cli4ai.com'
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
if (url.protocol !== 'http:' && url.protocol !== 'https:') {
|
|
109
|
+
outputError('INVALID_INPUT', `Invalid registry URL protocol: ${url.protocol}`, {
|
|
110
|
+
hint: 'Registry URL must start with http:// or https://'
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
87
115
|
function getNestedValue(obj: Record<string, unknown>, path: string): unknown {
|
|
88
116
|
const parts = path.split('.');
|
|
89
117
|
let current: unknown = obj;
|
package/src/commands/list.ts
CHANGED
|
@@ -58,32 +58,15 @@ export async function listCommand(options: ListOptions): Promise<void> {
|
|
|
58
58
|
}
|
|
59
59
|
}
|
|
60
60
|
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
} else {
|
|
73
|
-
// Human-readable output
|
|
74
|
-
if (packages.length === 0) {
|
|
75
|
-
console.log('No packages installed');
|
|
76
|
-
console.log('\nRun "cli4ai browse" to find and install packages');
|
|
77
|
-
console.log('Or: npm install -g @cli4ai/<package>');
|
|
78
|
-
} else {
|
|
79
|
-
console.log(`\nInstalled packages (${packages.length}):\n`);
|
|
80
|
-
for (const pkg of packages) {
|
|
81
|
-
const scopeTag = pkg.scope === 'local' ? '' :
|
|
82
|
-
pkg.scope === 'npm' ? ' (npm)' : ' (cli4ai)';
|
|
83
|
-
console.log(` ${pkg.name}@${pkg.version}${scopeTag}`);
|
|
84
|
-
console.log(` ${pkg.path}`);
|
|
85
|
-
}
|
|
86
|
-
console.log('');
|
|
87
|
-
}
|
|
88
|
-
}
|
|
61
|
+
output({
|
|
62
|
+
packages: packages.map(p => ({
|
|
63
|
+
name: p.name,
|
|
64
|
+
version: p.version,
|
|
65
|
+
path: p.path,
|
|
66
|
+
source: p.source,
|
|
67
|
+
scope: p.scope
|
|
68
|
+
})),
|
|
69
|
+
count: packages.length,
|
|
70
|
+
location: options.global ? 'global' : 'all'
|
|
71
|
+
});
|
|
89
72
|
}
|
|
@@ -2,11 +2,12 @@
|
|
|
2
2
|
* cli4ai mcp-config - Generate MCP configuration for Claude Code
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
|
-
import { outputError } from '../lib/cli.js';
|
|
5
|
+
import { output, outputError } from '../lib/cli.js';
|
|
6
6
|
import {
|
|
7
7
|
generateClaudeCodeConfig,
|
|
8
8
|
formatClaudeCodeConfig,
|
|
9
|
-
generateConfigSnippet
|
|
9
|
+
generateConfigSnippet,
|
|
10
|
+
generateServerConfig
|
|
10
11
|
} from '../mcp/config-gen.js';
|
|
11
12
|
import { findPackage } from '../core/config.js';
|
|
12
13
|
import { tryLoadManifest } from '../core/manifest.js';
|
|
@@ -36,8 +37,14 @@ export async function mcpConfigCommand(options: McpConfigOptions): Promise<void>
|
|
|
36
37
|
outputError('INVALID_INPUT', `Package ${options.package} does not have MCP enabled`);
|
|
37
38
|
}
|
|
38
39
|
|
|
40
|
+
const serverName = `cli4ai-${manifest!.name}`;
|
|
41
|
+
const serverConfig = generateServerConfig(manifest!, pkg!.path);
|
|
39
42
|
const snippet = generateConfigSnippet(manifest!, pkg!.path);
|
|
40
|
-
|
|
43
|
+
output({
|
|
44
|
+
serverName,
|
|
45
|
+
serverConfig,
|
|
46
|
+
snippet
|
|
47
|
+
});
|
|
41
48
|
return;
|
|
42
49
|
}
|
|
43
50
|
|
|
@@ -55,5 +62,8 @@ export async function mcpConfigCommand(options: McpConfigOptions): Promise<void>
|
|
|
55
62
|
}
|
|
56
63
|
|
|
57
64
|
// Output formatted config
|
|
58
|
-
|
|
65
|
+
output({
|
|
66
|
+
config,
|
|
67
|
+
formatted: formatClaudeCodeConfig(config)
|
|
68
|
+
});
|
|
59
69
|
}
|
package/src/commands/remove.ts
CHANGED
|
@@ -18,6 +18,8 @@ interface RemoveResult {
|
|
|
18
18
|
path: string;
|
|
19
19
|
}
|
|
20
20
|
|
|
21
|
+
const PKG_NAME_PATTERN = /^[a-z0-9]([a-z0-9-]*[a-z0-9])?$/;
|
|
22
|
+
|
|
21
23
|
export async function removeCommand(packages: string[], options: RemoveOptions): Promise<void> {
|
|
22
24
|
const results: RemoveResult[] = [];
|
|
23
25
|
const errors: { package: string; error: string }[] = [];
|
|
@@ -26,6 +28,10 @@ export async function removeCommand(packages: string[], options: RemoveOptions):
|
|
|
26
28
|
const projectDir = process.cwd();
|
|
27
29
|
|
|
28
30
|
for (const pkg of packages) {
|
|
31
|
+
if (!PKG_NAME_PATTERN.test(pkg)) {
|
|
32
|
+
errors.push({ package: pkg, error: 'Invalid package name' });
|
|
33
|
+
continue;
|
|
34
|
+
}
|
|
29
35
|
const pkgPath = resolve(targetDir, pkg);
|
|
30
36
|
|
|
31
37
|
if (!existsSync(pkgPath)) {
|
package/src/commands/routines.ts
CHANGED
package/src/commands/run.ts
CHANGED
|
@@ -35,11 +35,13 @@ export async function runCommand(
|
|
|
35
35
|
env: extraEnv,
|
|
36
36
|
capture: 'inherit'
|
|
37
37
|
});
|
|
38
|
-
process.
|
|
38
|
+
process.exitCode = result.exitCode;
|
|
39
|
+
return;
|
|
39
40
|
} catch (err) {
|
|
40
41
|
if (err instanceof ExecuteToolError) {
|
|
41
42
|
outputError(err.code, err.message, err.details);
|
|
42
43
|
}
|
|
43
|
-
|
|
44
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
45
|
+
outputError('API_ERROR', message);
|
|
44
46
|
}
|
|
45
47
|
}
|