cli4ai 1.2.0 → 1.2.2

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.
Files changed (113) hide show
  1. package/README.md +39 -0
  2. package/dist/bin.d.ts +6 -0
  3. package/dist/bin.js +105 -0
  4. package/dist/cli.d.ts +5 -0
  5. package/dist/cli.js +335 -0
  6. package/dist/commands/add.d.ts +11 -0
  7. package/dist/commands/add.js +464 -0
  8. package/dist/commands/browse.d.ts +4 -0
  9. package/dist/commands/browse.js +382 -0
  10. package/dist/commands/config.d.ts +10 -0
  11. package/dist/commands/config.js +121 -0
  12. package/dist/commands/info.d.ts +9 -0
  13. package/dist/commands/info.js +125 -0
  14. package/dist/commands/init.d.ts +10 -0
  15. package/dist/commands/init.js +458 -0
  16. package/dist/commands/list.d.ts +10 -0
  17. package/dist/commands/list.js +76 -0
  18. package/dist/commands/mcp-config.d.ts +10 -0
  19. package/dist/commands/mcp-config.js +49 -0
  20. package/dist/commands/remotes.d.ts +22 -0
  21. package/dist/commands/remotes.js +196 -0
  22. package/dist/commands/remove.d.ts +8 -0
  23. package/dist/commands/remove.js +61 -0
  24. package/dist/commands/routines.d.ts +29 -0
  25. package/dist/commands/routines.js +363 -0
  26. package/dist/commands/run.d.ts +12 -0
  27. package/dist/commands/run.js +104 -0
  28. package/dist/commands/scheduler.d.ts +27 -0
  29. package/dist/commands/scheduler.js +350 -0
  30. package/dist/commands/search.d.ts +9 -0
  31. package/dist/commands/search.js +162 -0
  32. package/dist/commands/secrets.d.ts +28 -0
  33. package/dist/commands/secrets.js +236 -0
  34. package/dist/commands/serve.d.ts +13 -0
  35. package/dist/commands/serve.js +49 -0
  36. package/dist/commands/start.d.ts +8 -0
  37. package/dist/commands/start.js +27 -0
  38. package/dist/commands/update.d.ts +17 -0
  39. package/dist/commands/update.js +210 -0
  40. package/dist/core/config.d.ts +91 -0
  41. package/dist/core/config.js +738 -0
  42. package/dist/core/execute.d.ts +51 -0
  43. package/dist/core/execute.js +475 -0
  44. package/dist/core/link.d.ts +39 -0
  45. package/dist/core/link.js +214 -0
  46. package/dist/core/lockfile.d.ts +63 -0
  47. package/dist/core/lockfile.js +140 -0
  48. package/dist/core/manifest.d.ts +96 -0
  49. package/dist/core/manifest.js +224 -0
  50. package/dist/core/registry.d.ts +74 -0
  51. package/dist/core/registry.js +116 -0
  52. package/dist/core/remote-client.d.ts +98 -0
  53. package/dist/core/remote-client.js +252 -0
  54. package/dist/core/remotes.d.ts +88 -0
  55. package/dist/core/remotes.js +206 -0
  56. package/dist/core/routine-engine.d.ts +124 -0
  57. package/dist/core/routine-engine.js +699 -0
  58. package/dist/core/routines.d.ts +36 -0
  59. package/dist/core/routines.js +132 -0
  60. package/dist/core/scheduler-daemon.d.ts +10 -0
  61. package/dist/core/scheduler-daemon.js +77 -0
  62. package/dist/core/scheduler.d.ts +131 -0
  63. package/dist/core/scheduler.js +492 -0
  64. package/dist/core/secrets.d.ts +48 -0
  65. package/dist/core/secrets.js +384 -0
  66. package/dist/lib/cli.d.ts +84 -0
  67. package/dist/lib/cli.js +216 -0
  68. package/dist/mcp/adapter.d.ts +35 -0
  69. package/dist/mcp/adapter.js +94 -0
  70. package/dist/mcp/config-gen.d.ts +31 -0
  71. package/dist/mcp/config-gen.js +75 -0
  72. package/dist/mcp/server.d.ts +41 -0
  73. package/dist/mcp/server.js +296 -0
  74. package/dist/server/service.d.ts +85 -0
  75. package/dist/server/service.js +304 -0
  76. package/package.json +6 -3
  77. package/src/bin.ts +0 -118
  78. package/src/cli.ts +0 -412
  79. package/src/commands/add.ts +0 -562
  80. package/src/commands/browse.ts +0 -449
  81. package/src/commands/config.ts +0 -154
  82. package/src/commands/info.ts +0 -133
  83. package/src/commands/init.ts +0 -514
  84. package/src/commands/list.ts +0 -95
  85. package/src/commands/mcp-config.ts +0 -69
  86. package/src/commands/remotes.ts +0 -253
  87. package/src/commands/remove.ts +0 -78
  88. package/src/commands/routines.ts +0 -427
  89. package/src/commands/run.ts +0 -127
  90. package/src/commands/scheduler.ts +0 -438
  91. package/src/commands/search.ts +0 -185
  92. package/src/commands/secrets.ts +0 -292
  93. package/src/commands/serve.ts +0 -66
  94. package/src/commands/start.ts +0 -40
  95. package/src/commands/update.ts +0 -252
  96. package/src/core/config.ts +0 -845
  97. package/src/core/execute.ts +0 -569
  98. package/src/core/link.ts +0 -246
  99. package/src/core/lockfile.ts +0 -187
  100. package/src/core/manifest.ts +0 -327
  101. package/src/core/registry.ts +0 -165
  102. package/src/core/remote-client.ts +0 -419
  103. package/src/core/remotes.ts +0 -268
  104. package/src/core/routine-engine.ts +0 -895
  105. package/src/core/routines.ts +0 -171
  106. package/src/core/scheduler-daemon.ts +0 -94
  107. package/src/core/scheduler.ts +0 -606
  108. package/src/core/secrets.ts +0 -430
  109. package/src/lib/cli.ts +0 -261
  110. package/src/mcp/adapter.ts +0 -131
  111. package/src/mcp/config-gen.ts +0 -106
  112. package/src/mcp/server.ts +0 -365
  113. package/src/server/service.ts +0 -434
@@ -0,0 +1,304 @@
1
+ /**
2
+ * cli4ai Remote Service
3
+ *
4
+ * HTTP server that exposes cli4ai functionality for remote execution.
5
+ * Run with `cli4ai serve` to start the service.
6
+ */
7
+ import { createServer } from 'http';
8
+ import { hostname } from 'os';
9
+ import { log } from '../lib/cli.js';
10
+ import { executeTool, ExecuteToolError } from '../core/execute.js';
11
+ import { findPackage, getGlobalPackages, getLocalPackages } from '../core/config.js';
12
+ import { loadManifest } from '../core/manifest.js';
13
+ import { loadRoutineDefinition, runRoutine } from '../core/routine-engine.js';
14
+ import { resolveRoutine } from '../core/routines.js';
15
+ // ═══════════════════════════════════════════════════════════════════════════
16
+ // HELPERS
17
+ // ═══════════════════════════════════════════════════════════════════════════
18
+ function parseBody(req) {
19
+ return new Promise((resolve, reject) => {
20
+ const chunks = [];
21
+ req.on('data', (chunk) => chunks.push(Buffer.from(chunk)));
22
+ req.on('error', reject);
23
+ req.on('end', () => resolve(Buffer.concat(chunks).toString('utf-8')));
24
+ });
25
+ }
26
+ function sendJson(res, status, data) {
27
+ const body = JSON.stringify(data);
28
+ res.writeHead(status, {
29
+ 'Content-Type': 'application/json',
30
+ 'Content-Length': Buffer.byteLength(body)
31
+ });
32
+ res.end(body);
33
+ }
34
+ function sendError(res, status, code, message, details) {
35
+ sendJson(res, status, { error: { code, message, details } });
36
+ }
37
+ // ═══════════════════════════════════════════════════════════════════════════
38
+ // REQUEST HANDLERS
39
+ // ═══════════════════════════════════════════════════════════════════════════
40
+ async function handleHealth(config, startTime) {
41
+ return {
42
+ status: 'ok',
43
+ hostname: hostname(),
44
+ version: '1.0.0',
45
+ uptime: Math.floor((Date.now() - startTime) / 1000)
46
+ };
47
+ }
48
+ async function handleListPackages(config) {
49
+ const localPkgs = getLocalPackages(config.cwd);
50
+ const globalPkgs = getGlobalPackages();
51
+ const packages = [
52
+ ...localPkgs.map(p => ({ name: p.name, version: p.version, path: p.path, source: p.source })),
53
+ ...globalPkgs.map(p => ({ name: p.name, version: p.version, path: p.path, source: p.source }))
54
+ ];
55
+ // Deduplicate by name (local takes precedence)
56
+ const seen = new Set();
57
+ const unique = packages.filter(p => {
58
+ if (seen.has(p.name))
59
+ return false;
60
+ seen.add(p.name);
61
+ return true;
62
+ });
63
+ return { packages: unique };
64
+ }
65
+ async function handlePackageInfo(config, packageName) {
66
+ const pkg = findPackage(packageName, config.cwd);
67
+ if (!pkg)
68
+ return null;
69
+ const manifest = loadManifest(pkg.path);
70
+ const commands = {};
71
+ if (manifest.commands) {
72
+ for (const [name, cmd] of Object.entries(manifest.commands)) {
73
+ commands[name] = { description: cmd.description };
74
+ }
75
+ }
76
+ return {
77
+ name: manifest.name,
78
+ version: manifest.version,
79
+ description: manifest.description,
80
+ commands
81
+ };
82
+ }
83
+ async function handleRunTool(config, request) {
84
+ const startTime = Date.now();
85
+ // Validate scope
86
+ const scope = request.scope ?? 'full';
87
+ const allowedScopes = config.allowedScopes ?? ['read', 'write', 'full'];
88
+ if (!allowedScopes.includes(scope)) {
89
+ return {
90
+ success: false,
91
+ exitCode: 1,
92
+ durationMs: Date.now() - startTime,
93
+ error: {
94
+ code: 'FORBIDDEN',
95
+ message: `Scope "${scope}" is not allowed on this remote`,
96
+ details: { allowedScopes }
97
+ }
98
+ };
99
+ }
100
+ try {
101
+ const result = await executeTool({
102
+ packageName: request.package,
103
+ command: request.command,
104
+ args: request.args ?? [],
105
+ cwd: config.cwd,
106
+ env: request.env,
107
+ stdin: request.stdin,
108
+ capture: 'pipe',
109
+ timeoutMs: request.timeout,
110
+ scope,
111
+ teeStderr: false
112
+ });
113
+ return {
114
+ success: result.exitCode === 0,
115
+ exitCode: result.exitCode,
116
+ stdout: result.stdout,
117
+ stderr: result.stderr,
118
+ durationMs: result.durationMs
119
+ };
120
+ }
121
+ catch (err) {
122
+ const durationMs = Date.now() - startTime;
123
+ if (err instanceof ExecuteToolError) {
124
+ return {
125
+ success: false,
126
+ exitCode: 1,
127
+ durationMs,
128
+ error: {
129
+ code: err.code,
130
+ message: err.message,
131
+ details: err.details
132
+ }
133
+ };
134
+ }
135
+ return {
136
+ success: false,
137
+ exitCode: 1,
138
+ durationMs,
139
+ error: {
140
+ code: 'API_ERROR',
141
+ message: err instanceof Error ? err.message : String(err)
142
+ }
143
+ };
144
+ }
145
+ }
146
+ async function handleRunRoutine(config, request) {
147
+ const resolved = resolveRoutine(request.routine, config.cwd);
148
+ if (!resolved)
149
+ return null;
150
+ const def = loadRoutineDefinition(resolved.path);
151
+ const result = await runRoutine(def, request.vars ?? {}, config.cwd);
152
+ return result;
153
+ }
154
+ // ═══════════════════════════════════════════════════════════════════════════
155
+ // MAIN SERVER
156
+ // ═══════════════════════════════════════════════════════════════════════════
157
+ export function createService(config) {
158
+ const startTime = Date.now();
159
+ const server = createServer(async (req, res) => {
160
+ const method = req.method ?? 'GET';
161
+ const url = new URL(req.url ?? '/', `http://${req.headers.host ?? 'localhost'}`);
162
+ const path = url.pathname;
163
+ // CORS headers for cross-origin requests
164
+ res.setHeader('Access-Control-Allow-Origin', '*');
165
+ res.setHeader('Access-Control-Allow-Methods', 'GET, POST, OPTIONS');
166
+ res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization, X-API-Key');
167
+ // Handle preflight
168
+ if (method === 'OPTIONS') {
169
+ res.writeHead(204);
170
+ res.end();
171
+ return;
172
+ }
173
+ // Authentication check
174
+ if (config.apiKey) {
175
+ const providedKey = req.headers['x-api-key'] ||
176
+ req.headers['authorization']?.replace(/^Bearer\s+/i, '');
177
+ if (providedKey !== config.apiKey) {
178
+ sendError(res, 401, 'UNAUTHORIZED', 'Invalid or missing API key');
179
+ return;
180
+ }
181
+ }
182
+ try {
183
+ // Route: GET /health
184
+ if (method === 'GET' && path === '/health') {
185
+ const data = await handleHealth(config, startTime);
186
+ sendJson(res, 200, data);
187
+ return;
188
+ }
189
+ // Route: GET /packages
190
+ if (method === 'GET' && path === '/packages') {
191
+ const data = await handleListPackages(config);
192
+ sendJson(res, 200, data);
193
+ return;
194
+ }
195
+ // Route: GET /packages/:name
196
+ if (method === 'GET' && path.startsWith('/packages/')) {
197
+ const packageName = path.slice('/packages/'.length);
198
+ const data = await handlePackageInfo(config, packageName);
199
+ if (!data) {
200
+ sendError(res, 404, 'NOT_FOUND', `Package not found: ${packageName}`);
201
+ return;
202
+ }
203
+ sendJson(res, 200, data);
204
+ return;
205
+ }
206
+ // Route: POST /run
207
+ if (method === 'POST' && path === '/run') {
208
+ const body = await parseBody(req);
209
+ let request;
210
+ try {
211
+ request = JSON.parse(body);
212
+ }
213
+ catch {
214
+ sendError(res, 400, 'PARSE_ERROR', 'Invalid JSON body');
215
+ return;
216
+ }
217
+ if (!request.package || typeof request.package !== 'string') {
218
+ sendError(res, 400, 'INVALID_INPUT', 'Missing required field: package');
219
+ return;
220
+ }
221
+ const data = await handleRunTool(config, request);
222
+ sendJson(res, data.success ? 200 : 500, data);
223
+ return;
224
+ }
225
+ // Route: POST /routines/:name/run
226
+ if (method === 'POST' && path.match(/^\/routines\/[^/]+\/run$/)) {
227
+ const routineName = path.split('/')[2];
228
+ const body = await parseBody(req);
229
+ let request;
230
+ try {
231
+ request = body ? JSON.parse(body) : {};
232
+ request.routine = routineName;
233
+ }
234
+ catch {
235
+ sendError(res, 400, 'PARSE_ERROR', 'Invalid JSON body');
236
+ return;
237
+ }
238
+ const data = await handleRunRoutine(config, request);
239
+ if (!data) {
240
+ sendError(res, 404, 'NOT_FOUND', `Routine not found: ${routineName}`);
241
+ return;
242
+ }
243
+ sendJson(res, data.status === 'success' ? 200 : 500, data);
244
+ return;
245
+ }
246
+ // 404 for unknown routes
247
+ sendError(res, 404, 'NOT_FOUND', `Unknown route: ${method} ${path}`);
248
+ }
249
+ catch (err) {
250
+ console.error('Request error:', err);
251
+ sendError(res, 500, 'API_ERROR', err instanceof Error ? err.message : String(err));
252
+ }
253
+ });
254
+ return server;
255
+ }
256
+ export async function startService(options = {}) {
257
+ const config = {
258
+ port: options.port ?? 4100,
259
+ host: options.host ?? '0.0.0.0',
260
+ apiKey: options.apiKey,
261
+ cwd: options.cwd ?? process.cwd(),
262
+ allowedScopes: options.allowedScopes
263
+ };
264
+ const server = createService(config);
265
+ return new Promise((resolve, reject) => {
266
+ server.on('error', (err) => {
267
+ if (err.code === 'EADDRINUSE') {
268
+ reject(new Error(`Port ${config.port} is already in use`));
269
+ }
270
+ else {
271
+ reject(err);
272
+ }
273
+ });
274
+ server.listen(config.port, config.host, () => {
275
+ log(`cli4ai service running on http://${config.host}:${config.port}`);
276
+ log(`Hostname: ${hostname()}`);
277
+ log(`Working directory: ${config.cwd}`);
278
+ if (config.apiKey) {
279
+ log(`Authentication: API key required`);
280
+ }
281
+ else {
282
+ log(`Authentication: None (not recommended for production)`);
283
+ }
284
+ log('');
285
+ log('Endpoints:');
286
+ log(' GET /health - Service health check');
287
+ log(' GET /packages - List available packages');
288
+ log(' GET /packages/:name - Get package info');
289
+ log(' POST /run - Execute a tool');
290
+ log(' POST /routines/:name/run - Run a routine');
291
+ log('');
292
+ log('Press Ctrl+C to stop');
293
+ });
294
+ // Handle graceful shutdown
295
+ const shutdown = () => {
296
+ log('\nShutting down...');
297
+ server.close(() => {
298
+ resolve();
299
+ });
300
+ };
301
+ process.on('SIGINT', shutdown);
302
+ process.on('SIGTERM', shutdown);
303
+ });
304
+ }
package/package.json CHANGED
@@ -1,13 +1,15 @@
1
1
  {
2
2
  "name": "cli4ai",
3
- "version": "1.2.0",
3
+ "version": "1.2.2",
4
4
  "description": "The package manager for AI CLI tools - cli4ai.com",
5
5
  "type": "module",
6
6
  "bin": {
7
- "cli4ai": "./src/bin.ts"
7
+ "cli4ai": "./dist/bin.js"
8
8
  },
9
9
  "scripts": {
10
10
  "dev": "npx tsx src/bin.ts",
11
+ "build": "tsc",
12
+ "prepublishOnly": "npm run build",
11
13
  "typecheck": "tsc --noEmit",
12
14
  "test": "vitest run",
13
15
  "test:watch": "vitest",
@@ -42,7 +44,8 @@
42
44
  "cli4ai"
43
45
  ],
44
46
  "files": [
45
- "src/**/*.ts",
47
+ "dist/**/*.js",
48
+ "dist/**/*.d.ts",
46
49
  "templates/**/*"
47
50
  ],
48
51
  "publishConfig": {
package/src/bin.ts DELETED
@@ -1,118 +0,0 @@
1
- #!/usr/bin/env npx tsx
2
- /**
3
- * cli4ai - The package manager for AI CLI tools
4
- * cli4ai.com
5
- */
6
-
7
- import { createProgram } from './cli.js';
8
- import { VERSION } from './lib/cli.js';
9
- import { getNpmGlobalPackages } from './core/config.js';
10
-
11
- // ANSI codes
12
- const RESET = '\x1B[0m';
13
- const BOLD = '\x1B[1m';
14
- const DIM = '\x1B[2m';
15
- const CYAN = '\x1B[36m';
16
- const WHITE = '\x1B[37m';
17
- const GREEN = '\x1B[32m';
18
- const YELLOW = '\x1B[33m';
19
- const MAGENTA = '\x1B[35m';
20
-
21
- /**
22
- * Sleep helper
23
- */
24
- const sleep = (ms: number) => new Promise(r => setTimeout(r, ms));
25
-
26
- /**
27
- * Robot eating dots animation
28
- */
29
- async function showAnimation(): Promise<void> {
30
- const width = 35;
31
- const robot = ['[•_•]', '[•_•]', '[°_°]', '[•_•]'];
32
- const food = '·';
33
-
34
- for (let pos = 0; pos <= width; pos++) {
35
- process.stderr.write('\r\x1B[K');
36
- const frame = robot[pos % robot.length];
37
- const trail = ' '.repeat(pos) + food.repeat(width - pos);
38
- process.stderr.write(` ${CYAN}${frame}${RESET}${DIM}${trail}${RESET}`);
39
- await sleep(20);
40
- }
41
- process.stderr.write('\r\x1B[K');
42
- }
43
-
44
- /**
45
- * Animate the robot face (blinking)
46
- */
47
- async function animateRobotFace(): Promise<void> {
48
- const faces = ['[•_•]', '[•_•]', '[-_-]', '[•_•]', '[•_•]', '[°_°]', '[•_•]'];
49
- const line = ` ${BOLD}${CYAN}cli4ai${RESET} ${DIM}─${RESET} ${WHITE}cli4ai.com${RESET}`;
50
-
51
- for (const face of faces) {
52
- process.stderr.write(`\r ${CYAN}${face}${RESET}${line}`);
53
- await sleep(120);
54
- }
55
- console.error('');
56
- }
57
-
58
- async function showBanner(): Promise<void> {
59
- if (!process.stderr.isTTY) return;
60
-
61
- console.error('');
62
-
63
- // Fun robot eating animation
64
- await showAnimation();
65
-
66
- // Animated robot branding (blinking)
67
- await animateRobotFace();
68
-
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
-
82
- // Check for updates in background (non-blocking)
83
- checkUpdatesInBackground();
84
- }
85
-
86
- /**
87
- * Check for updates without blocking - spawns detached process
88
- */
89
- function checkUpdatesInBackground(): void {
90
- // Quick local check only - don't hit network
91
- try {
92
- const packages = getNpmGlobalPackages();
93
- if (packages.length > 0) {
94
- // Spawn a background process to check updates and cache results
95
- const { spawn } = require('child_process');
96
- const child = spawn('sh', ['-c', 'timeout 3 npm view cli4ai version 2>/dev/null > /tmp/.cli4ai-update-check 2>&1 &'], {
97
- detached: true,
98
- stdio: ['ignore', 'ignore', 'ignore']
99
- });
100
- child.unref();
101
- }
102
- } catch {
103
- // Ignore errors - this is best effort
104
- }
105
- }
106
-
107
- // Check if running without command (just `cli4ai`)
108
- const args = process.argv.slice(2);
109
- const hasCommand = args.length > 0 && !args[0].startsWith('-');
110
- const isHelp = args.includes('--help') || args.includes('-h');
111
- const isVersion = args.includes('--version') || args.includes('-v');
112
-
113
- if (!hasCommand && !isHelp && !isVersion) {
114
- showBanner().then(() => process.exit(0));
115
- } else {
116
- const program = createProgram();
117
- program.parse(process.argv);
118
- }