cli4ai 1.1.5 → 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.
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 +459 -0
  8. package/dist/commands/browse.d.ts +4 -0
  9. package/dist/commands/browse.js +379 -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 +122 -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 +159 -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 -409
  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 -102
  83. package/src/commands/init.ts +0 -514
  84. package/src/commands/list.ts +0 -72
  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 -148
  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,35 @@
1
+ /**
2
+ * MCP Adapter - Converts CLI tools to MCP tools
3
+ */
4
+ import { type Manifest, type CommandDef } from '../core/manifest.js';
5
+ export interface McpTool {
6
+ name: string;
7
+ description: string;
8
+ inputSchema: {
9
+ type: 'object';
10
+ properties: Record<string, {
11
+ type: string;
12
+ description?: string;
13
+ }>;
14
+ required: string[];
15
+ };
16
+ }
17
+ export interface McpToolResult {
18
+ content: Array<{
19
+ type: 'text';
20
+ text: string;
21
+ }>;
22
+ isError?: boolean;
23
+ }
24
+ /**
25
+ * Convert a CLI command definition to MCP tool format
26
+ */
27
+ export declare function commandToMcpTool(manifest: Manifest, cmdName: string, cmd: CommandDef): McpTool;
28
+ /**
29
+ * Convert all commands in a manifest to MCP tools
30
+ */
31
+ export declare function manifestToMcpTools(manifest: Manifest): McpTool[];
32
+ /**
33
+ * Execute a CLI tool command and return MCP-formatted result
34
+ */
35
+ export declare function executeTool(entryPath: string, runtime: string, command: string, args: Record<string, string>, argOrder?: string[]): Promise<McpToolResult>;
@@ -0,0 +1,94 @@
1
+ /**
2
+ * MCP Adapter - Converts CLI tools to MCP tools
3
+ */
4
+ import { spawn } from 'child_process';
5
+ /**
6
+ * Convert a CLI command definition to MCP tool format
7
+ */
8
+ export function commandToMcpTool(manifest, cmdName, cmd) {
9
+ const properties = {};
10
+ const required = [];
11
+ // Convert args to JSON Schema properties
12
+ if (cmd.args) {
13
+ for (const arg of cmd.args) {
14
+ properties[arg.name] = {
15
+ type: 'string',
16
+ description: arg.description
17
+ };
18
+ if (arg.required) {
19
+ required.push(arg.name);
20
+ }
21
+ }
22
+ }
23
+ return {
24
+ name: `${manifest.name}_${cmdName}`,
25
+ description: `${manifest.name}: ${cmd.description || cmdName}`,
26
+ inputSchema: {
27
+ type: 'object',
28
+ properties,
29
+ required
30
+ }
31
+ };
32
+ }
33
+ /**
34
+ * Convert all commands in a manifest to MCP tools
35
+ */
36
+ export function manifestToMcpTools(manifest) {
37
+ const tools = [];
38
+ if (manifest.commands) {
39
+ for (const [cmdName, cmdDef] of Object.entries(manifest.commands)) {
40
+ tools.push(commandToMcpTool(manifest, cmdName, cmdDef));
41
+ }
42
+ }
43
+ return tools;
44
+ }
45
+ /**
46
+ * Execute a CLI tool command and return MCP-formatted result
47
+ */
48
+ export async function executeTool(entryPath, runtime, command, args, argOrder) {
49
+ return new Promise((resolve) => {
50
+ // Build command arguments
51
+ const cmdArgs = [command];
52
+ // Prefer manifest-defined arg order (positional), fall back to stable ordering.
53
+ const orderedKeys = argOrder ?? Object.keys(args).sort();
54
+ for (const key of orderedKeys) {
55
+ const value = args[key];
56
+ if (value !== undefined && value !== '')
57
+ cmdArgs.push(value);
58
+ }
59
+ const runtimeArgs = runtime === 'node'
60
+ ? [entryPath, ...cmdArgs]
61
+ : ['run', entryPath, ...cmdArgs];
62
+ const proc = spawn(runtime, runtimeArgs, {
63
+ stdio: ['pipe', 'pipe', 'pipe'],
64
+ env: { ...process.env }
65
+ });
66
+ let stdout = '';
67
+ let stderr = '';
68
+ proc.stdout.on('data', (data) => {
69
+ stdout += data.toString();
70
+ });
71
+ proc.stderr.on('data', (data) => {
72
+ stderr += data.toString();
73
+ });
74
+ proc.on('close', (code) => {
75
+ if (code !== 0) {
76
+ resolve({
77
+ content: [{ type: 'text', text: stderr || `Command failed with exit code ${code}` }],
78
+ isError: true
79
+ });
80
+ }
81
+ else {
82
+ resolve({
83
+ content: [{ type: 'text', text: stdout || 'Command completed successfully' }]
84
+ });
85
+ }
86
+ });
87
+ proc.on('error', (err) => {
88
+ resolve({
89
+ content: [{ type: 'text', text: `Failed to execute: ${err.message}` }],
90
+ isError: true
91
+ });
92
+ });
93
+ });
94
+ }
@@ -0,0 +1,31 @@
1
+ /**
2
+ * MCP Config Generator - Generate Claude Code MCP configuration
3
+ */
4
+ import { type Manifest } from '../core/manifest.js';
5
+ export interface McpServerConfig {
6
+ command: string;
7
+ args: string[];
8
+ env?: Record<string, string>;
9
+ }
10
+ export interface ClaudeCodeConfig {
11
+ mcpServers: Record<string, McpServerConfig>;
12
+ }
13
+ /**
14
+ * Generate MCP server config for a single package
15
+ */
16
+ export declare function generateServerConfig(manifest: Manifest, _packagePath: string): McpServerConfig;
17
+ /**
18
+ * Generate Claude Code config for installed packages
19
+ */
20
+ export declare function generateClaudeCodeConfig(cwd: string, options?: {
21
+ global?: boolean;
22
+ packages?: string[];
23
+ }): ClaudeCodeConfig;
24
+ /**
25
+ * Format config as JSON for Claude Code
26
+ */
27
+ export declare function formatClaudeCodeConfig(config: ClaudeCodeConfig): string;
28
+ /**
29
+ * Generate config snippet for adding to existing claude_desktop_config.json
30
+ */
31
+ export declare function generateConfigSnippet(manifest: Manifest, packagePath: string): string;
@@ -0,0 +1,75 @@
1
+ /**
2
+ * MCP Config Generator - Generate Claude Code MCP configuration
3
+ */
4
+ import { tryLoadManifest } from '../core/manifest.js';
5
+ import { findPackage, getGlobalPackages, getLocalPackages } from '../core/config.js';
6
+ /**
7
+ * Generate MCP server config for a single package
8
+ */
9
+ export function generateServerConfig(manifest, _packagePath) {
10
+ // Use cli4ai start command which handles the MCP server
11
+ return {
12
+ command: 'cli4ai',
13
+ args: ['start', manifest.name]
14
+ };
15
+ }
16
+ /**
17
+ * Load manifest for an installed package
18
+ */
19
+ function loadPackageWithManifest(pkg) {
20
+ const manifest = tryLoadManifest(pkg.path);
21
+ if (!manifest)
22
+ return null;
23
+ return { name: pkg.name, path: pkg.path, manifest };
24
+ }
25
+ /**
26
+ * Generate Claude Code config for installed packages
27
+ */
28
+ export function generateClaudeCodeConfig(cwd, options = {}) {
29
+ const mcpServers = {};
30
+ // Get installed packages
31
+ let installedPackages = [];
32
+ if (options.packages && options.packages.length > 0) {
33
+ // Specific packages requested
34
+ for (const pkgName of options.packages) {
35
+ const pkg = findPackage(pkgName, cwd);
36
+ if (pkg) {
37
+ installedPackages.push(pkg);
38
+ }
39
+ }
40
+ }
41
+ else {
42
+ // All installed packages
43
+ if (options.global) {
44
+ installedPackages = getGlobalPackages();
45
+ }
46
+ else {
47
+ installedPackages = [
48
+ ...getLocalPackages(cwd),
49
+ ...getGlobalPackages()
50
+ ];
51
+ }
52
+ }
53
+ // Load manifests and filter to MCP-enabled packages
54
+ for (const pkg of installedPackages) {
55
+ const pkgWithManifest = loadPackageWithManifest(pkg);
56
+ if (pkgWithManifest && pkgWithManifest.manifest.mcp?.enabled) {
57
+ mcpServers[`cli4ai-${pkgWithManifest.name}`] = generateServerConfig(pkgWithManifest.manifest, pkgWithManifest.path);
58
+ }
59
+ }
60
+ return { mcpServers };
61
+ }
62
+ /**
63
+ * Format config as JSON for Claude Code
64
+ */
65
+ export function formatClaudeCodeConfig(config) {
66
+ return JSON.stringify(config, null, 2);
67
+ }
68
+ /**
69
+ * Generate config snippet for adding to existing claude_desktop_config.json
70
+ */
71
+ export function generateConfigSnippet(manifest, packagePath) {
72
+ const serverConfig = generateServerConfig(manifest, packagePath);
73
+ const serverName = `cli4ai-${manifest.name}`;
74
+ return `"${serverName}": ${JSON.stringify(serverConfig, null, 2)}`;
75
+ }
@@ -0,0 +1,41 @@
1
+ /**
2
+ * MCP Server - Expose CLI tools as MCP tools
3
+ *
4
+ * Implements the Model Context Protocol (MCP) over stdio
5
+ * https://modelcontextprotocol.io/
6
+ *
7
+ * SECURITY: Includes execution safeguards:
8
+ * - Audit logging of all tool calls
9
+ * - Rate limiting to prevent abuse
10
+ * - Trust level tracking for packages
11
+ */
12
+ import { type Manifest } from '../core/manifest.js';
13
+ /**
14
+ * MCP Server that wraps a CLI tool
15
+ */
16
+ export declare class McpServer {
17
+ private manifest;
18
+ private packagePath;
19
+ private tools;
20
+ private rateLimits;
21
+ private totalCallCount;
22
+ constructor(manifest: Manifest, packagePath: string);
23
+ /**
24
+ * SECURITY: Check if a tool call should be rate limited
25
+ */
26
+ private checkRateLimit;
27
+ /**
28
+ * Start the MCP server (stdio mode)
29
+ */
30
+ start(): Promise<void>;
31
+ private handleMessage;
32
+ private handleRequest;
33
+ private handleToolCall;
34
+ private executeCommand;
35
+ private sendResult;
36
+ private sendError;
37
+ }
38
+ /**
39
+ * Start MCP server for a package
40
+ */
41
+ export declare function startMcpServer(manifest: Manifest, packagePath: string): Promise<void>;
@@ -0,0 +1,296 @@
1
+ /**
2
+ * MCP Server - Expose CLI tools as MCP tools
3
+ *
4
+ * Implements the Model Context Protocol (MCP) over stdio
5
+ * https://modelcontextprotocol.io/
6
+ *
7
+ * SECURITY: Includes execution safeguards:
8
+ * - Audit logging of all tool calls
9
+ * - Rate limiting to prevent abuse
10
+ * - Trust level tracking for packages
11
+ */
12
+ import { spawn } from 'child_process';
13
+ import { resolve } from 'path';
14
+ import { appendFileSync, existsSync, mkdirSync } from 'fs';
15
+ import { homedir } from 'os';
16
+ import { manifestToMcpTools } from './adapter.js';
17
+ import { getSecret } from '../core/secrets.js';
18
+ import { loadConfig } from '../core/config.js';
19
+ // ═══════════════════════════════════════════════════════════════════════════
20
+ // SECURITY: Audit logging and rate limiting
21
+ // ═══════════════════════════════════════════════════════════════════════════
22
+ const AUDIT_LOG_DIR = resolve(homedir(), '.cli4ai', 'logs');
23
+ const RATE_LIMIT_WINDOW_MS = 60000; // 1 minute
24
+ const RATE_LIMIT_MAX_CALLS = 100; // Max calls per minute per tool
25
+ /**
26
+ * Audit log an MCP tool call for security tracking
27
+ * Can be disabled via `cli4ai config set audit.enabled false`
28
+ */
29
+ function auditLog(packageName, toolName, args, result, errorMessage) {
30
+ try {
31
+ // Check if audit logging is enabled
32
+ const config = loadConfig();
33
+ if (!config.audit?.enabled) {
34
+ return;
35
+ }
36
+ if (!existsSync(AUDIT_LOG_DIR)) {
37
+ mkdirSync(AUDIT_LOG_DIR, { recursive: true });
38
+ }
39
+ const logEntry = {
40
+ timestamp: new Date().toISOString(),
41
+ package: packageName,
42
+ tool: toolName,
43
+ // Redact potentially sensitive argument values, keep keys
44
+ argKeys: Object.keys(args),
45
+ result,
46
+ error: errorMessage,
47
+ pid: process.pid
48
+ };
49
+ const logFile = resolve(AUDIT_LOG_DIR, `mcp-audit-${new Date().toISOString().slice(0, 10)}.log`);
50
+ appendFileSync(logFile, JSON.stringify(logEntry) + '\n');
51
+ }
52
+ catch {
53
+ // Don't fail tool execution if logging fails
54
+ }
55
+ }
56
+ /**
57
+ * MCP Server that wraps a CLI tool
58
+ */
59
+ export class McpServer {
60
+ manifest;
61
+ packagePath;
62
+ tools;
63
+ rateLimits = new Map();
64
+ totalCallCount = 0;
65
+ constructor(manifest, packagePath) {
66
+ this.manifest = manifest;
67
+ this.packagePath = packagePath;
68
+ this.tools = manifestToMcpTools(manifest);
69
+ }
70
+ /**
71
+ * SECURITY: Check if a tool call should be rate limited
72
+ */
73
+ checkRateLimit(toolName) {
74
+ const now = Date.now();
75
+ const entry = this.rateLimits.get(toolName);
76
+ if (!entry || now - entry.windowStart >= RATE_LIMIT_WINDOW_MS) {
77
+ // Start new window
78
+ this.rateLimits.set(toolName, { count: 1, windowStart: now });
79
+ return { allowed: true };
80
+ }
81
+ if (entry.count >= RATE_LIMIT_MAX_CALLS) {
82
+ const retryAfterMs = RATE_LIMIT_WINDOW_MS - (now - entry.windowStart);
83
+ return { allowed: false, retryAfterMs };
84
+ }
85
+ entry.count++;
86
+ return { allowed: true };
87
+ }
88
+ /**
89
+ * Start the MCP server (stdio mode)
90
+ */
91
+ async start() {
92
+ process.stdin.setEncoding('utf8');
93
+ let buffer = '';
94
+ process.stdin.on('data', (chunk) => {
95
+ buffer += chunk;
96
+ // Try to parse complete JSON-RPC messages
97
+ const lines = buffer.split('\n');
98
+ buffer = lines.pop() || '';
99
+ for (const line of lines) {
100
+ if (line.trim()) {
101
+ this.handleMessage(line.trim());
102
+ }
103
+ }
104
+ });
105
+ process.stdin.on('end', () => {
106
+ process.exit(0);
107
+ });
108
+ }
109
+ handleMessage(message) {
110
+ try {
111
+ const request = JSON.parse(message);
112
+ this.handleRequest(request);
113
+ }
114
+ catch (err) {
115
+ this.sendError(null, -32700, 'Parse error');
116
+ }
117
+ }
118
+ async handleRequest(request) {
119
+ const { id, method, params } = request;
120
+ try {
121
+ switch (method) {
122
+ case 'initialize':
123
+ this.sendResult(id, {
124
+ protocolVersion: '2024-11-05',
125
+ capabilities: {
126
+ tools: {}
127
+ },
128
+ serverInfo: {
129
+ name: `cli4ai-${this.manifest.name}`,
130
+ version: this.manifest.version
131
+ }
132
+ });
133
+ break;
134
+ case 'initialized':
135
+ // No response needed
136
+ break;
137
+ case 'tools/list':
138
+ this.sendResult(id, {
139
+ tools: this.tools.map(t => ({
140
+ name: t.name,
141
+ description: t.description,
142
+ inputSchema: t.inputSchema
143
+ }))
144
+ });
145
+ break;
146
+ case 'tools/call':
147
+ await this.handleToolCall(id, params);
148
+ break;
149
+ case 'ping':
150
+ this.sendResult(id, {});
151
+ break;
152
+ default:
153
+ this.sendError(id, -32601, `Method not found: ${method}`);
154
+ }
155
+ }
156
+ catch (err) {
157
+ this.sendError(id, -32603, err.message);
158
+ }
159
+ }
160
+ async handleToolCall(id, params) {
161
+ const { name, arguments: args = {} } = params;
162
+ // SECURITY: Rate limiting
163
+ const rateCheck = this.checkRateLimit(name);
164
+ if (!rateCheck.allowed) {
165
+ auditLog(this.manifest.name, name, args, 'rate_limited');
166
+ this.sendError(id, -32000, `Rate limit exceeded. Retry after ${Math.ceil((rateCheck.retryAfterMs || 0) / 1000)}s`);
167
+ return;
168
+ }
169
+ // Track total calls for monitoring
170
+ this.totalCallCount++;
171
+ // Parse tool name: package_command
172
+ const parts = name.split('_');
173
+ if (parts.length < 2 || parts[0] !== this.manifest.name) {
174
+ auditLog(this.manifest.name, name, args, 'error', 'Unknown tool');
175
+ this.sendError(id, -32602, `Unknown tool: ${name}`);
176
+ return;
177
+ }
178
+ const command = parts.slice(1).join('_');
179
+ const cmdDef = this.manifest.commands?.[command];
180
+ if (!cmdDef) {
181
+ auditLog(this.manifest.name, name, args, 'error', 'Unknown command');
182
+ this.sendError(id, -32602, `Unknown command: ${command}`);
183
+ return;
184
+ }
185
+ // Build command arguments in order
186
+ const cmdArgs = [command];
187
+ if (cmdDef.args) {
188
+ for (const argDef of cmdDef.args) {
189
+ const value = args[argDef.name];
190
+ if (value !== undefined && value !== '') {
191
+ cmdArgs.push(String(value));
192
+ }
193
+ else if (argDef.required) {
194
+ auditLog(this.manifest.name, name, args, 'error', `Missing required argument: ${argDef.name}`);
195
+ this.sendError(id, -32602, `Missing required argument: ${argDef.name}`);
196
+ return;
197
+ }
198
+ }
199
+ }
200
+ // Execute the CLI tool
201
+ const entryPath = resolve(this.packagePath, this.manifest.entry);
202
+ try {
203
+ const result = await this.executeCommand(entryPath, cmdArgs);
204
+ auditLog(this.manifest.name, name, args, 'success');
205
+ this.sendResult(id, {
206
+ content: [{ type: 'text', text: result }]
207
+ });
208
+ }
209
+ catch (err) {
210
+ const errorMessage = err.message;
211
+ auditLog(this.manifest.name, name, args, 'error', errorMessage);
212
+ this.sendResult(id, {
213
+ content: [{ type: 'text', text: errorMessage }],
214
+ isError: true
215
+ });
216
+ }
217
+ }
218
+ executeCommand(entryPath, args) {
219
+ return new Promise((resolve, reject) => {
220
+ // Use tsx for TypeScript files, node for JavaScript
221
+ let cmd;
222
+ let cmdArgs;
223
+ if (entryPath.endsWith('.ts') || entryPath.endsWith('.tsx')) {
224
+ cmd = 'npx';
225
+ cmdArgs = ['tsx', entryPath, ...args];
226
+ }
227
+ else {
228
+ cmd = 'node';
229
+ cmdArgs = [entryPath, ...args];
230
+ }
231
+ // Inject secrets from manifest env definitions
232
+ // SECURITY: Use package-scoped secret lookup (tries scoped first, then global)
233
+ const secretsEnv = {};
234
+ if (this.manifest.env) {
235
+ for (const key of Object.keys(this.manifest.env)) {
236
+ const value = getSecret(key, this.manifest.name);
237
+ if (value) {
238
+ secretsEnv[key] = value;
239
+ }
240
+ }
241
+ }
242
+ const proc = spawn(cmd, cmdArgs, {
243
+ stdio: ['pipe', 'pipe', 'pipe'],
244
+ env: { ...process.env, ...secretsEnv }
245
+ });
246
+ let stdout = '';
247
+ let stderr = '';
248
+ proc.stdout.on('data', (data) => {
249
+ stdout += data.toString();
250
+ });
251
+ proc.stderr.on('data', (data) => {
252
+ stderr += data.toString();
253
+ });
254
+ proc.on('close', (code) => {
255
+ if (code !== 0) {
256
+ reject(new Error(stderr || `Exit code ${code}`));
257
+ }
258
+ else {
259
+ resolve(stdout);
260
+ }
261
+ });
262
+ proc.on('error', (err) => {
263
+ reject(err);
264
+ });
265
+ });
266
+ }
267
+ sendResult(id, result) {
268
+ if (id === null)
269
+ return;
270
+ const response = {
271
+ jsonrpc: '2.0',
272
+ id,
273
+ result
274
+ };
275
+ console.log(JSON.stringify(response));
276
+ }
277
+ sendError(id, code, message) {
278
+ // Per JSON-RPC 2.0 spec, error responses for parse errors should include id: null
279
+ // For other errors where id is null (shouldn't happen), we skip the response
280
+ if (id === null && code !== -32700)
281
+ return;
282
+ const response = {
283
+ jsonrpc: '2.0',
284
+ id: id, // For parse errors (-32700), this will be cast from null
285
+ error: { code, message }
286
+ };
287
+ console.log(JSON.stringify(response));
288
+ }
289
+ }
290
+ /**
291
+ * Start MCP server for a package
292
+ */
293
+ export async function startMcpServer(manifest, packagePath) {
294
+ const server = new McpServer(manifest, packagePath);
295
+ await server.start();
296
+ }
@@ -0,0 +1,85 @@
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 { type ScopeLevel } from '../core/execute.js';
9
+ export interface ServiceConfig {
10
+ /** Port to listen on (default: 4100) */
11
+ port: number;
12
+ /** Host to bind to (default: 0.0.0.0) */
13
+ host: string;
14
+ /** API key for authentication (optional but recommended) */
15
+ apiKey?: string;
16
+ /** Working directory for command execution */
17
+ cwd: string;
18
+ /** Allowed scope levels (defaults to ['read', 'write', 'full']) */
19
+ allowedScopes?: ScopeLevel[];
20
+ }
21
+ export interface RunToolRequest {
22
+ /** Package name */
23
+ package: string;
24
+ /** Command within the package */
25
+ command?: string;
26
+ /** Arguments to pass */
27
+ args?: string[];
28
+ /** Environment variables */
29
+ env?: Record<string, string>;
30
+ /** Standard input to pass to the tool */
31
+ stdin?: string;
32
+ /** Timeout in milliseconds */
33
+ timeout?: number;
34
+ /** Scope level for execution */
35
+ scope?: ScopeLevel;
36
+ }
37
+ export interface RunToolResponse {
38
+ success: boolean;
39
+ exitCode: number;
40
+ stdout?: string;
41
+ stderr?: string;
42
+ durationMs: number;
43
+ error?: {
44
+ code: string;
45
+ message: string;
46
+ details?: Record<string, unknown>;
47
+ };
48
+ }
49
+ export interface RunRoutineRequest {
50
+ /** Routine name */
51
+ routine: string;
52
+ /** Variables to pass to the routine */
53
+ vars?: Record<string, string>;
54
+ }
55
+ export interface ListPackagesResponse {
56
+ packages: Array<{
57
+ name: string;
58
+ version: string;
59
+ path: string;
60
+ source: 'local' | 'registry';
61
+ }>;
62
+ }
63
+ export interface PackageInfoResponse {
64
+ name: string;
65
+ version: string;
66
+ description?: string;
67
+ commands?: Record<string, {
68
+ description: string;
69
+ }>;
70
+ }
71
+ export interface HealthResponse {
72
+ status: 'ok';
73
+ hostname: string;
74
+ version: string;
75
+ uptime: number;
76
+ }
77
+ export declare function createService(config: ServiceConfig): ReturnType<typeof createServer>;
78
+ export interface StartServiceOptions {
79
+ port?: number;
80
+ host?: string;
81
+ apiKey?: string;
82
+ cwd?: string;
83
+ allowedScopes?: ScopeLevel[];
84
+ }
85
+ export declare function startService(options?: StartServiceOptions): Promise<void>;