cli4ai 0.8.2 → 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 CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "cli4ai",
3
- "version": "0.8.2",
4
- "description": "The package manager for AI CLI tools - cliforai.com",
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://cliforai.com",
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
- * cliforai.com
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.stdout.write('\r\x1B[K');
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.stdout.write(` ${CYAN}${frame}${RESET}${DIM}${trail}${RESET}`);
38
+ process.stderr.write(` ${CYAN}${frame}${RESET}${DIM}${trail}${RESET}`);
39
39
  await sleep(20);
40
40
  }
41
- process.stdout.write('\r\x1B[K');
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}cliforai.com${RESET}`;
49
+ const line = ` ${BOLD}${CYAN}cli4ai${RESET} ${DIM}─${RESET} ${WHITE}cli4ai.com${RESET}`;
50
50
 
51
51
  for (const face of faces) {
52
- process.stdout.write(`\r ${CYAN}${face}${RESET}${line}`);
52
+ process.stderr.write(`\r ${CYAN}${face}${RESET}${line}`);
53
53
  await sleep(120);
54
54
  }
55
- console.log('');
55
+ console.error('');
56
56
  }
57
57
 
58
58
  async function showBanner(): Promise<void> {
59
- if (!process.stdout.isTTY) return;
59
+ if (!process.stderr.isTTY) return;
60
60
 
61
- console.log('');
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.log(` ${DIM}The package manager for AI CLI tools${RESET}`);
70
- console.log(` ${DIM}v${VERSION}${RESET}`);
71
- console.log('');
72
- console.log(` ${BOLD}Commands${RESET}`);
73
- console.log(` ${DIM}${'─'.repeat(40)}${RESET}`);
74
- console.log(` ${GREEN}browse${RESET} ${DIM}Browse & install packages${RESET}`);
75
- console.log(` ${GREEN}run${RESET} ${CYAN}<pkg> <cmd>${RESET} ${DIM}Run a tool command${RESET}`);
76
- console.log(` ${GREEN}ls${RESET} ${DIM}List installed packages${RESET}`);
77
- console.log(` ${GREEN}update${RESET} ${DIM}Update all packages${RESET}`);
78
- console.log('');
79
- console.log(` ${DIM}Run${RESET} ${WHITE}cli4ai --help${RESET} ${DIM}for all commands${RESET}`);
80
- console.log('');
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
  }
@@ -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) {
@@ -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}cliforai.com${RESET}`);
169
+ log(` ${DIM}cli4ai.com${RESET}`);
170
170
  log('');
171
171
 
172
172
  // Box top
@@ -5,7 +5,7 @@
5
5
  import { output, outputError, log } from '../lib/cli.js';
6
6
  import {
7
7
  loadConfig,
8
- saveConfig,
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 updated = setNestedValue(config, key, parseValue(value));
76
- saveConfig(updated);
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;
@@ -58,32 +58,15 @@ export async function listCommand(options: ListOptions): Promise<void> {
58
58
  }
59
59
  }
60
60
 
61
- if (options.json || !process.stdout.isTTY) {
62
- output({
63
- packages: packages.map(p => ({
64
- name: p.name,
65
- version: p.version,
66
- path: p.path,
67
- source: p.source,
68
- scope: p.scope
69
- })),
70
- count: packages.length
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
- console.log(snippet);
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
- console.log(formatClaudeCodeConfig(config));
65
+ output({
66
+ config,
67
+ formatted: formatClaudeCodeConfig(config)
68
+ });
59
69
  }
@@ -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)) {
@@ -389,5 +389,5 @@ export async function routinesEditCommand(name: string, options: { global?: bool
389
389
  shell: true
390
390
  });
391
391
 
392
- process.exit(result.status ?? 0);
392
+ process.exit(result.status ?? 1);
393
393
  }
@@ -35,11 +35,13 @@ export async function runCommand(
35
35
  env: extraEnv,
36
36
  capture: 'inherit'
37
37
  });
38
- process.exit(result.exitCode);
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
- throw err;
44
+ const message = err instanceof Error ? err.message : String(err);
45
+ outputError('API_ERROR', message);
44
46
  }
45
47
  }