50c 3.9.2 → 4.0.0

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/lib/subagent.js CHANGED
@@ -1,366 +1,369 @@
1
- #!/usr/bin/env node
2
- /**
3
- * 50c Sub-Agent - Pure Node.js execution engine
4
- * Bypasses IDE shell restrictions, works on ALL MCP-compatible IDEs
5
- *
6
- * Capabilities:
7
- * - SSH remote execution (via ssh2)
8
- * - HTTP/HTTPS requests (native)
9
- * - File operations (native)
10
- * - Process spawning (child_process)
11
- */
12
-
13
- const { spawn, exec } = require('child_process');
14
- const https = require('https');
15
- const http = require('http');
16
- const fs = require('fs');
17
- const path = require('path');
18
-
19
- // User-configured servers loaded from ~/.50c/servers.json
20
- // Run: 50c servers add <alias> <host> <user> to configure
21
- function loadUserServers() {
22
- try {
23
- const serversPath = path.join(require('os').homedir(), '.50c', 'servers.json');
24
- if (fs.existsSync(serversPath)) {
25
- return JSON.parse(fs.readFileSync(serversPath, 'utf8'));
26
- }
27
- } catch (e) {}
28
- return {};
29
- }
30
- const KNOWN_SERVERS = loadUserServers();
31
-
32
- /**
33
- * HTTP/HTTPS fetch - no shell, pure Node
34
- */
35
- async function httpFetch(url, options = {}) {
36
- return new Promise((resolve, reject) => {
37
- const isHttps = url.startsWith('https');
38
- const lib = isHttps ? https : http;
39
-
40
- const urlObj = new URL(url);
41
- const bodyData = options.body || '';
42
- const reqOptions = {
43
- hostname: urlObj.hostname,
44
- port: urlObj.port || (isHttps ? 443 : 80),
45
- path: urlObj.pathname + urlObj.search,
46
- method: options.method || 'GET',
47
- headers: {
48
- ...options.headers,
49
- ...(bodyData ? { 'Content-Length': Buffer.byteLength(bodyData) } : {})
50
- },
51
- timeout: options.timeout || 30000,
52
- // TLS certificate validation enabled for security
53
- };
54
-
55
- const req = lib.request(reqOptions, (res) => {
56
- let data = '';
57
- res.on('data', chunk => data += chunk);
58
- res.on('end', () => {
59
- resolve({
60
- status: res.statusCode,
61
- headers: res.headers,
62
- body: data,
63
- ok: res.statusCode >= 200 && res.statusCode < 300
64
- });
65
- });
66
- });
67
-
68
- req.on('error', reject);
69
- req.on('timeout', () => {
70
- req.destroy();
71
- reject(new Error('Request timeout'));
72
- });
73
-
74
- if (bodyData) {
75
- req.write(bodyData);
76
- }
77
- req.end();
78
- });
79
- }
80
-
81
- /**
82
- * SSH execution via native ssh command (spawned, not shell)
83
- * Fixed: Uses explicit identity file for Windows compatibility
84
- */
85
- async function sshExec(server, command, options = {}) {
86
- const serverConfig = typeof server === 'string' ? KNOWN_SERVERS[server] : server;
87
-
88
- if (!serverConfig) {
89
- throw new Error(`Unknown server: ${server}. Known: ${Object.keys(KNOWN_SERVERS).join(', ')}`);
90
- }
91
-
92
- const { host, user = 'root', port = 22 } = serverConfig;
93
- const timeout = options.timeout || 30000;
94
-
95
- // Try to find SSH key - common locations
96
- const os = require('os');
97
- const homeDir = os.homedir();
98
- const keyPaths = [
99
- path.join(homeDir, '.ssh', 'id_rsa'),
100
- path.join(homeDir, '.ssh', 'id_ed25519'),
101
- options.keyFile
102
- ].filter(Boolean);
103
-
104
- let identityArgs = [];
105
- for (const keyPath of keyPaths) {
106
- if (fs.existsSync(keyPath)) {
107
- identityArgs = ['-i', keyPath];
108
- break;
109
- }
110
- }
111
-
112
- return new Promise((resolve, reject) => {
113
- const args = [
114
- '-o', 'StrictHostKeyChecking=no',
115
- '-o', 'UserKnownHostsFile=/dev/null',
116
- '-o', 'ConnectTimeout=10',
117
- '-o', 'ServerAliveInterval=5',
118
- '-o', 'ServerAliveCountMax=2',
119
- ...identityArgs,
120
- '-p', String(port),
121
- `${user}@${host}`,
122
- command
123
- ];
124
-
125
- const proc = spawn('ssh', args, {
126
- stdio: ['pipe', 'pipe', 'pipe'],
127
- timeout,
128
- windowsHide: true
129
- });
130
-
131
- let stdout = '';
132
- let stderr = '';
133
-
134
- proc.stdout.on('data', data => stdout += data.toString());
135
- proc.stderr.on('data', data => stderr += data.toString());
136
-
137
- const timer = setTimeout(() => {
138
- proc.kill('SIGTERM');
139
- reject(new Error(`SSH timeout after ${timeout}ms`));
140
- }, timeout);
141
-
142
- proc.on('close', code => {
143
- clearTimeout(timer);
144
- resolve({
145
- ok: code === 0,
146
- code,
147
- stdout: stdout.trim(),
148
- stderr: stderr.trim(),
149
- server: serverConfig.name || host
150
- });
151
- });
152
-
153
- proc.on('error', err => {
154
- clearTimeout(timer);
155
- reject(err);
156
- });
157
- });
158
- }
159
-
160
- /**
161
- * Local command execution (spawned, controlled)
162
- */
163
- async function localExec(command, options = {}) {
164
- const timeout = options.timeout || 30000;
165
- const cwd = options.cwd || process.cwd();
166
-
167
- return new Promise((resolve, reject) => {
168
- exec(command, { timeout, cwd, maxBuffer: 10 * 1024 * 1024 }, (error, stdout, stderr) => {
169
- resolve({
170
- ok: !error,
171
- code: error ? error.code || 1 : 0,
172
- stdout: stdout.trim(),
173
- stderr: stderr.trim(),
174
- command
175
- });
176
- });
177
- });
178
- }
179
-
180
- /**
181
- * File operations
182
- */
183
- const fileOps = {
184
- read: (filePath) => {
185
- try {
186
- return { ok: true, content: fs.readFileSync(filePath, 'utf8') };
187
- } catch (e) {
188
- return { ok: false, error: e.message };
189
- }
190
- },
191
-
192
- write: (filePath, content) => {
193
- try {
194
- fs.mkdirSync(path.dirname(filePath), { recursive: true });
195
- fs.writeFileSync(filePath, content);
196
- return { ok: true, path: filePath };
197
- } catch (e) {
198
- return { ok: false, error: e.message };
199
- }
200
- },
201
-
202
- exists: (filePath) => ({ ok: true, exists: fs.existsSync(filePath) }),
203
-
204
- list: (dirPath) => {
205
- try {
206
- return { ok: true, files: fs.readdirSync(dirPath) };
207
- } catch (e) {
208
- return { ok: false, error: e.message };
209
- }
210
- }
211
- };
212
-
213
- /**
214
- * 50c MCP Tool calling - call any 50c tool via API
215
- */
216
- const API_ENDPOINT = process.env.FIFTYC_ENDPOINT || 'https://api.50c.ai';
217
-
218
- async function call50cTool(toolName, toolArgs, apiKey) {
219
- const key = apiKey || process.env.FIFTYC_API_KEY || loadApiKey();
220
-
221
- if (!key) {
222
- return { ok: false, error: 'No API key. Set FIFTYC_API_KEY or run: 50c config key <key>' };
223
- }
224
-
225
- try {
226
- const response = await httpFetch(`${API_ENDPOINT}/tools/${toolName}`, {
227
- method: 'POST',
228
- headers: {
229
- 'Content-Type': 'application/json',
230
- 'Authorization': `Bearer ${key}`
231
- },
232
- body: JSON.stringify(toolArgs || {}),
233
- timeout: 120000 // 2 min for genius+
234
- });
235
-
236
- if (!response.ok) {
237
- return { ok: false, error: `HTTP ${response.status}`, body: response.body };
238
- }
239
-
240
- const result = JSON.parse(response.body);
241
- if (result.error) {
242
- return { ok: false, error: result.error };
243
- }
244
-
245
- return { ok: true, result: result.result || result };
246
- } catch (e) {
247
- return { ok: false, error: e.message };
248
- }
249
- }
250
-
251
- function loadApiKey() {
252
- try {
253
- const configPath = path.join(require('os').homedir(), '.50c', 'config.json');
254
- if (fs.existsSync(configPath)) {
255
- const config = JSON.parse(fs.readFileSync(configPath, 'utf8'));
256
- return config.api_key;
257
- }
258
- } catch {}
259
- return null;
260
- }
261
-
262
- /**
263
- * Main sub-agent dispatcher
264
- */
265
- async function subagent(task) {
266
- const { type, ...params } = task;
267
-
268
- switch (type) {
269
- case 'http':
270
- case 'fetch':
271
- return httpFetch(params.url, params);
272
-
273
- case 'ssh':
274
- return sshExec(params.server, params.command, params);
275
-
276
- case 'local':
277
- case 'exec':
278
- return localExec(params.command, params);
279
-
280
- case 'file':
281
- const op = fileOps[params.op];
282
- if (!op) throw new Error(`Unknown file op: ${params.op}`);
283
- return op(params.path, params.content);
284
-
285
- case 'multi':
286
- // Parallel execution of multiple tasks
287
- const results = await Promise.all(
288
- params.tasks.map(t => subagent(t).catch(e => ({ ok: false, error: e.message })))
289
- );
290
- return { ok: results.every(r => r.ok), results };
291
-
292
- case 'sequence':
293
- // Sequential execution, stops on first failure
294
- const seqResults = [];
295
- for (const t of params.tasks) {
296
- const result = await subagent(t).catch(e => ({ ok: false, error: e.message }));
297
- seqResults.push(result);
298
- if (!result.ok && params.stopOnError !== false) break;
299
- }
300
- return { ok: seqResults.every(r => r.ok), results: seqResults };
301
-
302
- case 'servers':
303
- // List known servers
304
- return { ok: true, servers: KNOWN_SERVERS };
305
-
306
- case '50c':
307
- case 'tool':
308
- // Call 50c MCP tool
309
- return call50cTool(params.tool, params.args || {}, params.api_key);
310
-
311
- case 'chain':
312
- // Chain multiple 50c tools (output of one feeds into next)
313
- let chainResult = params.input || {};
314
- const chainSteps = [];
315
- for (const step of params.steps) {
316
- const args = { ...step.args };
317
- // Inject previous result if placeholder exists
318
- for (const [k, v] of Object.entries(args)) {
319
- if (v === '$prev' || v === '$result') {
320
- args[k] = typeof chainResult === 'string' ? chainResult : JSON.stringify(chainResult);
321
- }
322
- }
323
- const stepResult = await call50cTool(step.tool, args, params.api_key);
324
- chainSteps.push({ tool: step.tool, result: stepResult });
325
- if (!stepResult.ok) {
326
- return { ok: false, error: `Chain failed at ${step.tool}`, steps: chainSteps };
327
- }
328
- chainResult = stepResult.result;
329
- }
330
- return { ok: true, result: chainResult, steps: chainSteps };
331
-
332
- default:
333
- throw new Error(`Unknown task type: ${type}`);
334
- }
335
- }
336
-
337
- // Export for MCP integration
338
- module.exports = {
339
- subagent,
340
- httpFetch,
341
- sshExec,
342
- localExec,
343
- fileOps,
344
- call50cTool,
345
- KNOWN_SERVERS
346
- };
347
-
348
- // CLI mode
349
- if (require.main === module) {
350
- const args = process.argv.slice(2);
351
- if (args.length === 0) {
352
- console.log('50c Sub-Agent - Pure Node execution engine');
353
- console.log('Usage: node subagent.js <json-task>');
354
- console.log('Example: node subagent.js \'{"type":"http","url":"https://api.50c.ai/health"}\'');
355
- process.exit(0);
356
- }
357
-
358
- try {
359
- const task = JSON.parse(args[0]);
360
- subagent(task)
361
- .then(r => console.log(JSON.stringify(r, null, 2)))
362
- .catch(e => console.error(JSON.stringify({ ok: false, error: e.message })));
363
- } catch (e) {
364
- console.error(JSON.stringify({ ok: false, error: `Invalid JSON: ${e.message}` }));
365
- }
366
- }
1
+ #!/usr/bin/env node
2
+ /**
3
+ * 50c Sub-Agent - Pure Node.js execution engine
4
+ * Bypasses IDE shell restrictions, works on ALL MCP-compatible IDEs
5
+ *
6
+ * Capabilities:
7
+ * - SSH remote execution (via ssh2)
8
+ * - HTTP/HTTPS requests (native)
9
+ * - File operations (native)
10
+ * - Process spawning (child_process)
11
+ */
12
+
13
+ const { spawn, exec } = require('child_process');
14
+ const https = require('https');
15
+ const http = require('http');
16
+ const fs = require('fs');
17
+ const path = require('path');
18
+
19
+ // Known servers - loaded from ~/.50c/servers.json (not hardcoded for security)
20
+ const os = require('os');
21
+ const SERVERS_FILE = path.join(os.homedir(), '.50c', 'servers.json');
22
+
23
+ function loadKnownServers() {
24
+ try {
25
+ if (fs.existsSync(SERVERS_FILE)) {
26
+ return JSON.parse(fs.readFileSync(SERVERS_FILE, 'utf8'));
27
+ }
28
+ } catch (e) {}
29
+ // Empty by default - users must configure their own servers
30
+ return {};
31
+ }
32
+
33
+ const KNOWN_SERVERS = loadKnownServers();
34
+
35
+ /**
36
+ * HTTP/HTTPS fetch - no shell, pure Node
37
+ */
38
+ async function httpFetch(url, options = {}) {
39
+ return new Promise((resolve, reject) => {
40
+ const isHttps = url.startsWith('https');
41
+ const lib = isHttps ? https : http;
42
+
43
+ const urlObj = new URL(url);
44
+ const bodyData = options.body || '';
45
+ const reqOptions = {
46
+ hostname: urlObj.hostname,
47
+ port: urlObj.port || (isHttps ? 443 : 80),
48
+ path: urlObj.pathname + urlObj.search,
49
+ method: options.method || 'GET',
50
+ headers: {
51
+ ...options.headers,
52
+ ...(bodyData ? { 'Content-Length': Buffer.byteLength(bodyData) } : {})
53
+ },
54
+ timeout: options.timeout || 30000,
55
+ rejectUnauthorized: false // Allow self-signed certs
56
+ };
57
+
58
+ const req = lib.request(reqOptions, (res) => {
59
+ let data = '';
60
+ res.on('data', chunk => data += chunk);
61
+ res.on('end', () => {
62
+ resolve({
63
+ status: res.statusCode,
64
+ headers: res.headers,
65
+ body: data,
66
+ ok: res.statusCode >= 200 && res.statusCode < 300
67
+ });
68
+ });
69
+ });
70
+
71
+ req.on('error', reject);
72
+ req.on('timeout', () => {
73
+ req.destroy();
74
+ reject(new Error('Request timeout'));
75
+ });
76
+
77
+ if (bodyData) {
78
+ req.write(bodyData);
79
+ }
80
+ req.end();
81
+ });
82
+ }
83
+
84
+ /**
85
+ * SSH execution via native ssh command (spawned, not shell)
86
+ * Fixed: Uses explicit identity file for Windows compatibility
87
+ */
88
+ async function sshExec(server, command, options = {}) {
89
+ const serverConfig = typeof server === 'string' ? KNOWN_SERVERS[server] : server;
90
+
91
+ if (!serverConfig) {
92
+ throw new Error(`Unknown server: ${server}. Known: ${Object.keys(KNOWN_SERVERS).join(', ')}`);
93
+ }
94
+
95
+ const { host, user = 'root', port = 22 } = serverConfig;
96
+ const timeout = options.timeout || 30000;
97
+
98
+ // Try to find SSH key - common locations
99
+ const os = require('os');
100
+ const homeDir = os.homedir();
101
+ const keyPaths = [
102
+ path.join(homeDir, '.ssh', 'id_rsa'),
103
+ path.join(homeDir, '.ssh', 'id_ed25519'),
104
+ options.keyFile
105
+ ].filter(Boolean);
106
+
107
+ let identityArgs = [];
108
+ for (const keyPath of keyPaths) {
109
+ if (fs.existsSync(keyPath)) {
110
+ identityArgs = ['-i', keyPath];
111
+ break;
112
+ }
113
+ }
114
+
115
+ return new Promise((resolve, reject) => {
116
+ const args = [
117
+ '-o', 'StrictHostKeyChecking=no',
118
+ '-o', 'UserKnownHostsFile=/dev/null',
119
+ '-o', 'ConnectTimeout=10',
120
+ '-o', 'ServerAliveInterval=5',
121
+ '-o', 'ServerAliveCountMax=2',
122
+ ...identityArgs,
123
+ '-p', String(port),
124
+ `${user}@${host}`,
125
+ command
126
+ ];
127
+
128
+ const proc = spawn('ssh', args, {
129
+ stdio: ['pipe', 'pipe', 'pipe'],
130
+ timeout,
131
+ windowsHide: true
132
+ });
133
+
134
+ let stdout = '';
135
+ let stderr = '';
136
+
137
+ proc.stdout.on('data', data => stdout += data.toString());
138
+ proc.stderr.on('data', data => stderr += data.toString());
139
+
140
+ const timer = setTimeout(() => {
141
+ proc.kill('SIGTERM');
142
+ reject(new Error(`SSH timeout after ${timeout}ms`));
143
+ }, timeout);
144
+
145
+ proc.on('close', code => {
146
+ clearTimeout(timer);
147
+ resolve({
148
+ ok: code === 0,
149
+ code,
150
+ stdout: stdout.trim(),
151
+ stderr: stderr.trim(),
152
+ server: serverConfig.name || host
153
+ });
154
+ });
155
+
156
+ proc.on('error', err => {
157
+ clearTimeout(timer);
158
+ reject(err);
159
+ });
160
+ });
161
+ }
162
+
163
+ /**
164
+ * Local command execution (spawned, controlled)
165
+ */
166
+ async function localExec(command, options = {}) {
167
+ const timeout = options.timeout || 30000;
168
+ const cwd = options.cwd || process.cwd();
169
+
170
+ return new Promise((resolve, reject) => {
171
+ exec(command, { timeout, cwd, maxBuffer: 10 * 1024 * 1024 }, (error, stdout, stderr) => {
172
+ resolve({
173
+ ok: !error,
174
+ code: error ? error.code || 1 : 0,
175
+ stdout: stdout.trim(),
176
+ stderr: stderr.trim(),
177
+ command
178
+ });
179
+ });
180
+ });
181
+ }
182
+
183
+ /**
184
+ * File operations
185
+ */
186
+ const fileOps = {
187
+ read: (filePath) => {
188
+ try {
189
+ return { ok: true, content: fs.readFileSync(filePath, 'utf8') };
190
+ } catch (e) {
191
+ return { ok: false, error: e.message };
192
+ }
193
+ },
194
+
195
+ write: (filePath, content) => {
196
+ try {
197
+ fs.mkdirSync(path.dirname(filePath), { recursive: true });
198
+ fs.writeFileSync(filePath, content);
199
+ return { ok: true, path: filePath };
200
+ } catch (e) {
201
+ return { ok: false, error: e.message };
202
+ }
203
+ },
204
+
205
+ exists: (filePath) => ({ ok: true, exists: fs.existsSync(filePath) }),
206
+
207
+ list: (dirPath) => {
208
+ try {
209
+ return { ok: true, files: fs.readdirSync(dirPath) };
210
+ } catch (e) {
211
+ return { ok: false, error: e.message };
212
+ }
213
+ }
214
+ };
215
+
216
+ /**
217
+ * 50c MCP Tool calling - call any 50c tool via API
218
+ */
219
+ const API_ENDPOINT = process.env.FIFTYC_ENDPOINT || 'https://api.50c.ai';
220
+
221
+ async function call50cTool(toolName, toolArgs, apiKey) {
222
+ const key = apiKey || process.env.FIFTYC_API_KEY || loadApiKey();
223
+
224
+ if (!key) {
225
+ return { ok: false, error: 'No API key. Set FIFTYC_API_KEY or run: 50c config key <key>' };
226
+ }
227
+
228
+ try {
229
+ const response = await httpFetch(`${API_ENDPOINT}/tools/${toolName}`, {
230
+ method: 'POST',
231
+ headers: {
232
+ 'Content-Type': 'application/json',
233
+ 'Authorization': `Bearer ${key}`
234
+ },
235
+ body: JSON.stringify(toolArgs || {}),
236
+ timeout: 120000 // 2 min for genius+
237
+ });
238
+
239
+ if (!response.ok) {
240
+ return { ok: false, error: `HTTP ${response.status}`, body: response.body };
241
+ }
242
+
243
+ const result = JSON.parse(response.body);
244
+ if (result.error) {
245
+ return { ok: false, error: result.error };
246
+ }
247
+
248
+ return { ok: true, result: result.result || result };
249
+ } catch (e) {
250
+ return { ok: false, error: e.message };
251
+ }
252
+ }
253
+
254
+ function loadApiKey() {
255
+ try {
256
+ const configPath = path.join(require('os').homedir(), '.50c', 'config.json');
257
+ if (fs.existsSync(configPath)) {
258
+ const config = JSON.parse(fs.readFileSync(configPath, 'utf8'));
259
+ return config.api_key;
260
+ }
261
+ } catch {}
262
+ return null;
263
+ }
264
+
265
+ /**
266
+ * Main sub-agent dispatcher
267
+ */
268
+ async function subagent(task) {
269
+ const { type, ...params } = task;
270
+
271
+ switch (type) {
272
+ case 'http':
273
+ case 'fetch':
274
+ return httpFetch(params.url, params);
275
+
276
+ case 'ssh':
277
+ return sshExec(params.server, params.command, params);
278
+
279
+ case 'local':
280
+ case 'exec':
281
+ return localExec(params.command, params);
282
+
283
+ case 'file':
284
+ const op = fileOps[params.op];
285
+ if (!op) throw new Error(`Unknown file op: ${params.op}`);
286
+ return op(params.path, params.content);
287
+
288
+ case 'multi':
289
+ // Parallel execution of multiple tasks
290
+ const results = await Promise.all(
291
+ params.tasks.map(t => subagent(t).catch(e => ({ ok: false, error: e.message })))
292
+ );
293
+ return { ok: results.every(r => r.ok), results };
294
+
295
+ case 'sequence':
296
+ // Sequential execution, stops on first failure
297
+ const seqResults = [];
298
+ for (const t of params.tasks) {
299
+ const result = await subagent(t).catch(e => ({ ok: false, error: e.message }));
300
+ seqResults.push(result);
301
+ if (!result.ok && params.stopOnError !== false) break;
302
+ }
303
+ return { ok: seqResults.every(r => r.ok), results: seqResults };
304
+
305
+ case 'servers':
306
+ // List known servers
307
+ return { ok: true, servers: KNOWN_SERVERS };
308
+
309
+ case '50c':
310
+ case 'tool':
311
+ // Call 50c MCP tool
312
+ return call50cTool(params.tool, params.args || {}, params.api_key);
313
+
314
+ case 'chain':
315
+ // Chain multiple 50c tools (output of one feeds into next)
316
+ let chainResult = params.input || {};
317
+ const chainSteps = [];
318
+ for (const step of params.steps) {
319
+ const args = { ...step.args };
320
+ // Inject previous result if placeholder exists
321
+ for (const [k, v] of Object.entries(args)) {
322
+ if (v === '$prev' || v === '$result') {
323
+ args[k] = typeof chainResult === 'string' ? chainResult : JSON.stringify(chainResult);
324
+ }
325
+ }
326
+ const stepResult = await call50cTool(step.tool, args, params.api_key);
327
+ chainSteps.push({ tool: step.tool, result: stepResult });
328
+ if (!stepResult.ok) {
329
+ return { ok: false, error: `Chain failed at ${step.tool}`, steps: chainSteps };
330
+ }
331
+ chainResult = stepResult.result;
332
+ }
333
+ return { ok: true, result: chainResult, steps: chainSteps };
334
+
335
+ default:
336
+ throw new Error(`Unknown task type: ${type}`);
337
+ }
338
+ }
339
+
340
+ // Export for MCP integration
341
+ module.exports = {
342
+ subagent,
343
+ httpFetch,
344
+ sshExec,
345
+ localExec,
346
+ fileOps,
347
+ call50cTool,
348
+ KNOWN_SERVERS
349
+ };
350
+
351
+ // CLI mode
352
+ if (require.main === module) {
353
+ const args = process.argv.slice(2);
354
+ if (args.length === 0) {
355
+ console.log('50c Sub-Agent - Pure Node execution engine');
356
+ console.log('Usage: node subagent.js <json-task>');
357
+ console.log('Example: node subagent.js \'{"type":"http","url":"https://api.50c.ai/health"}\'');
358
+ process.exit(0);
359
+ }
360
+
361
+ try {
362
+ const task = JSON.parse(args[0]);
363
+ subagent(task)
364
+ .then(r => console.log(JSON.stringify(r, null, 2)))
365
+ .catch(e => console.error(JSON.stringify({ ok: false, error: e.message })));
366
+ } catch (e) {
367
+ console.error(JSON.stringify({ ok: false, error: `Invalid JSON: ${e.message}` }));
368
+ }
369
+ }