@burdenoff/vibe-agent 1.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.
Files changed (100) hide show
  1. package/.env.example +8 -0
  2. package/LICENSE +22 -0
  3. package/README.md +290 -0
  4. package/dist/app.d.ts +15 -0
  5. package/dist/app.d.ts.map +1 -0
  6. package/dist/app.js +445 -0
  7. package/dist/app.js.map +1 -0
  8. package/dist/cli.d.ts +3 -0
  9. package/dist/cli.d.ts.map +1 -0
  10. package/dist/cli.js +1043 -0
  11. package/dist/cli.js.map +1 -0
  12. package/dist/db/schema.d.ts +145 -0
  13. package/dist/db/schema.d.ts.map +1 -0
  14. package/dist/db/schema.js +536 -0
  15. package/dist/db/schema.js.map +1 -0
  16. package/dist/index.d.ts +2 -0
  17. package/dist/index.d.ts.map +1 -0
  18. package/dist/index.js +61 -0
  19. package/dist/index.js.map +1 -0
  20. package/dist/middleware/ModuleAuth.d.ts +61 -0
  21. package/dist/middleware/ModuleAuth.d.ts.map +1 -0
  22. package/dist/middleware/ModuleAuth.js +220 -0
  23. package/dist/middleware/ModuleAuth.js.map +1 -0
  24. package/dist/middleware/auth.d.ts +3 -0
  25. package/dist/middleware/auth.d.ts.map +1 -0
  26. package/dist/middleware/auth.js +11 -0
  27. package/dist/middleware/auth.js.map +1 -0
  28. package/dist/migrations/remove-notes-prompts.d.ts +13 -0
  29. package/dist/migrations/remove-notes-prompts.d.ts.map +1 -0
  30. package/dist/migrations/remove-notes-prompts.js +148 -0
  31. package/dist/migrations/remove-notes-prompts.js.map +1 -0
  32. package/dist/routes/bookmarks.d.ts +3 -0
  33. package/dist/routes/bookmarks.d.ts.map +1 -0
  34. package/dist/routes/bookmarks.js +186 -0
  35. package/dist/routes/bookmarks.js.map +1 -0
  36. package/dist/routes/config.d.ts +3 -0
  37. package/dist/routes/config.d.ts.map +1 -0
  38. package/dist/routes/config.js +108 -0
  39. package/dist/routes/config.js.map +1 -0
  40. package/dist/routes/files.d.ts +3 -0
  41. package/dist/routes/files.d.ts.map +1 -0
  42. package/dist/routes/files.js +471 -0
  43. package/dist/routes/files.js.map +1 -0
  44. package/dist/routes/git.d.ts +3 -0
  45. package/dist/routes/git.d.ts.map +1 -0
  46. package/dist/routes/git.js +498 -0
  47. package/dist/routes/git.js.map +1 -0
  48. package/dist/routes/moduleRegistry.d.ts +41 -0
  49. package/dist/routes/moduleRegistry.d.ts.map +1 -0
  50. package/dist/routes/moduleRegistry.js +356 -0
  51. package/dist/routes/moduleRegistry.js.map +1 -0
  52. package/dist/routes/notifications.d.ts +3 -0
  53. package/dist/routes/notifications.d.ts.map +1 -0
  54. package/dist/routes/notifications.js +250 -0
  55. package/dist/routes/notifications.js.map +1 -0
  56. package/dist/routes/port-forward.d.ts +3 -0
  57. package/dist/routes/port-forward.d.ts.map +1 -0
  58. package/dist/routes/port-forward.js +205 -0
  59. package/dist/routes/port-forward.js.map +1 -0
  60. package/dist/routes/projects.d.ts +3 -0
  61. package/dist/routes/projects.d.ts.map +1 -0
  62. package/dist/routes/projects.js +442 -0
  63. package/dist/routes/projects.js.map +1 -0
  64. package/dist/routes/ssh.d.ts +3 -0
  65. package/dist/routes/ssh.d.ts.map +1 -0
  66. package/dist/routes/ssh.js +192 -0
  67. package/dist/routes/ssh.js.map +1 -0
  68. package/dist/routes/tasks.d.ts +3 -0
  69. package/dist/routes/tasks.d.ts.map +1 -0
  70. package/dist/routes/tasks.js +183 -0
  71. package/dist/routes/tasks.js.map +1 -0
  72. package/dist/routes/tmux.d.ts +3 -0
  73. package/dist/routes/tmux.d.ts.map +1 -0
  74. package/dist/routes/tmux.js +1191 -0
  75. package/dist/routes/tmux.js.map +1 -0
  76. package/dist/routes/tunnel.d.ts +25 -0
  77. package/dist/routes/tunnel.d.ts.map +1 -0
  78. package/dist/routes/tunnel.js +449 -0
  79. package/dist/routes/tunnel.js.map +1 -0
  80. package/dist/services/ModulePermissions.d.ts +100 -0
  81. package/dist/services/ModulePermissions.d.ts.map +1 -0
  82. package/dist/services/ModulePermissions.js +312 -0
  83. package/dist/services/ModulePermissions.js.map +1 -0
  84. package/dist/services/ModuleRegistryService.d.ts +152 -0
  85. package/dist/services/ModuleRegistryService.d.ts.map +1 -0
  86. package/dist/services/ModuleRegistryService.js +522 -0
  87. package/dist/services/ModuleRegistryService.js.map +1 -0
  88. package/dist/services/agent.service.d.ts +19 -0
  89. package/dist/services/agent.service.d.ts.map +1 -0
  90. package/dist/services/agent.service.js +88 -0
  91. package/dist/services/agent.service.js.map +1 -0
  92. package/dist/services/bootstrap.d.ts +22 -0
  93. package/dist/services/bootstrap.d.ts.map +1 -0
  94. package/dist/services/bootstrap.js +206 -0
  95. package/dist/services/bootstrap.js.map +1 -0
  96. package/dist/services/service-manager.d.ts +50 -0
  97. package/dist/services/service-manager.d.ts.map +1 -0
  98. package/dist/services/service-manager.js +382 -0
  99. package/dist/services/service-manager.js.map +1 -0
  100. package/package.json +107 -0
package/dist/cli.js ADDED
@@ -0,0 +1,1043 @@
1
+ #!/usr/bin/env node
2
+ import 'dotenv/config';
3
+ import { Command } from 'commander';
4
+ import { AgentService } from './services/agent.service.js';
5
+ import { ServiceManager } from './services/service-manager.js';
6
+ import { readFileSync } from 'fs';
7
+ import { join, dirname } from 'path';
8
+ import { fileURLToPath } from 'url';
9
+ const __filename = fileURLToPath(import.meta.url);
10
+ const __dirname = dirname(__filename);
11
+ // Read package.json for version
12
+ let packageVersion = '1.0.0';
13
+ try {
14
+ const packageJsonPath = join(__dirname, '..', 'package.json');
15
+ const packageJson = JSON.parse(readFileSync(packageJsonPath, 'utf8'));
16
+ packageVersion = packageJson.version;
17
+ }
18
+ catch {
19
+ // fallback
20
+ }
21
+ const DEFAULT_AGENT_URL = 'http://localhost:3005';
22
+ // ─── Helpers ──────────────────────────────────────────────────────────────────
23
+ async function agentFetch(agentUrl, path, options = {}) {
24
+ const apiKey = process.env.AGENT_API_KEY;
25
+ const headers = {
26
+ ...(options.body ? { 'Content-Type': 'application/json' } : {}),
27
+ ...(apiKey ? { 'x-agent-api-key': apiKey } : {}),
28
+ ...(options.headers ?? {}),
29
+ };
30
+ const res = await fetch(`${agentUrl}${path}`, { ...options, headers });
31
+ const data = await res.json().catch(() => ({}));
32
+ return { ok: res.ok, status: res.status, data };
33
+ }
34
+ function fail(msg) {
35
+ console.error(`\x1b[31mError:\x1b[0m ${msg}`);
36
+ process.exit(1);
37
+ }
38
+ function formatTable(rows) {
39
+ if (rows.length === 0) {
40
+ console.log(' (none)');
41
+ return;
42
+ }
43
+ console.table(rows);
44
+ }
45
+ function timeAgo(dateStr) {
46
+ const diff = Date.now() - new Date(dateStr).getTime();
47
+ const s = Math.floor(diff / 1000);
48
+ if (s < 60)
49
+ return `${s}s ago`;
50
+ const m = Math.floor(s / 60);
51
+ if (m < 60)
52
+ return `${m}m ago`;
53
+ const h = Math.floor(m / 60);
54
+ if (h < 24)
55
+ return `${h}h ago`;
56
+ return `${Math.floor(h / 24)}d ago`;
57
+ }
58
+ // ─── Program ──────────────────────────────────────────────────────────────────
59
+ const program = new Command();
60
+ const agentService = new AgentService();
61
+ const serviceManager = new ServiceManager();
62
+ program
63
+ .name('vibe')
64
+ .description('VibeControls Agent CLI — Remote development environment management')
65
+ .version(packageVersion, '-v, --version');
66
+ // ═══════════════════════════════════════════════════════════════════════════════
67
+ // Core Commands
68
+ // ═══════════════════════════════════════════════════════════════════════════════
69
+ program
70
+ .command('start')
71
+ .description('Start the agent server')
72
+ .option('-p, --port <port>', 'Port to run on', '3005')
73
+ .option('-n, --name <name>', 'Instance name', 'default')
74
+ .option('-d, --daemon', 'Run as background daemon', false)
75
+ .option('--db-path <path>', 'SQLite database path', './vibecontrols-agent.db')
76
+ .action(async (options) => {
77
+ try {
78
+ const config = {
79
+ port: parseInt(options.port),
80
+ name: options.name,
81
+ daemon: options.daemon,
82
+ dbPath: options.dbPath,
83
+ };
84
+ if (options.daemon) {
85
+ await serviceManager.startDaemon(config);
86
+ }
87
+ else {
88
+ await agentService.start(config);
89
+ }
90
+ }
91
+ catch (error) {
92
+ fail(`Failed to start agent: ${error instanceof Error ? error.message : error}`);
93
+ }
94
+ });
95
+ program
96
+ .command('stop')
97
+ .description('Stop a running agent instance')
98
+ .option('-n, --name <name>', 'Instance name', 'default')
99
+ .option('--all', 'Stop all instances', false)
100
+ .action(async (options) => {
101
+ try {
102
+ if (options.all) {
103
+ await serviceManager.stopAll();
104
+ }
105
+ else {
106
+ await serviceManager.stop(options.name);
107
+ }
108
+ }
109
+ catch (error) {
110
+ fail(`Failed to stop: ${error instanceof Error ? error.message : error}`);
111
+ }
112
+ });
113
+ program
114
+ .command('restart')
115
+ .description('Restart an agent instance')
116
+ .option('-n, --name <name>', 'Instance name', 'default')
117
+ .option('-p, --port <port>', 'Port', '3005')
118
+ .option('--db-path <path>', 'Database path', './vibecontrols-agent.db')
119
+ .action(async (options) => {
120
+ try {
121
+ await serviceManager.restart(options.name, {
122
+ port: parseInt(options.port),
123
+ name: options.name,
124
+ daemon: true,
125
+ dbPath: options.dbPath,
126
+ });
127
+ }
128
+ catch (error) {
129
+ fail(`Failed to restart: ${error instanceof Error ? error.message : error}`);
130
+ }
131
+ });
132
+ program
133
+ .command('status')
134
+ .description('Show status of agent instances')
135
+ .option('-n, --name <name>', 'Specific instance name')
136
+ .action(async (options) => {
137
+ try {
138
+ if (options.name) {
139
+ const status = await serviceManager.getStatus(options.name);
140
+ if (!status) {
141
+ console.log(`No agent instance named '${options.name}' found.`);
142
+ }
143
+ else {
144
+ console.log(`\n Agent: ${status.name}`);
145
+ console.log(` Status: ${status.status === 'running' ? '\x1b[32m● running\x1b[0m' : '\x1b[31m○ stopped\x1b[0m'}`);
146
+ console.log(` PID: ${status.pid}`);
147
+ console.log(` Port: ${status.port}`);
148
+ console.log(` Started: ${status.startTime}\n`);
149
+ }
150
+ }
151
+ else {
152
+ const all = await serviceManager.getStatusAll();
153
+ if (all.length === 0) {
154
+ console.log('No agent instances registered.');
155
+ }
156
+ else {
157
+ formatTable(all.map(s => ({
158
+ Name: s.name,
159
+ Status: s.status,
160
+ PID: s.pid,
161
+ Port: s.port,
162
+ Started: s.startTime,
163
+ })));
164
+ }
165
+ }
166
+ }
167
+ catch (error) {
168
+ fail(`Failed to get status: ${error instanceof Error ? error.message : error}`);
169
+ }
170
+ });
171
+ program
172
+ .command('list')
173
+ .description('List all agent instances')
174
+ .action(async () => {
175
+ try {
176
+ const instances = await serviceManager.listInstances();
177
+ if (instances.length === 0) {
178
+ console.log('No agent instances found.');
179
+ }
180
+ else {
181
+ formatTable(instances.map(i => ({
182
+ Name: i.name,
183
+ Status: i.status,
184
+ PID: i.pid,
185
+ Port: i.port,
186
+ Started: i.startTime,
187
+ })));
188
+ }
189
+ }
190
+ catch (error) {
191
+ fail(`Failed to list: ${error instanceof Error ? error.message : error}`);
192
+ }
193
+ });
194
+ program
195
+ .command('kill')
196
+ .description('Force kill an agent instance')
197
+ .option('-n, --name <name>', 'Instance name', 'default')
198
+ .option('--all', 'Kill all instances', false)
199
+ .action(async (options) => {
200
+ try {
201
+ if (options.all) {
202
+ await serviceManager.killAll();
203
+ }
204
+ else {
205
+ await serviceManager.kill(options.name);
206
+ }
207
+ }
208
+ catch (error) {
209
+ fail(`Failed to kill: ${error instanceof Error ? error.message : error}`);
210
+ }
211
+ });
212
+ program
213
+ .command('logs')
214
+ .description('Show logs for an agent instance')
215
+ .option('-n, --name <name>', 'Instance name', 'default')
216
+ .option('-f, --follow', 'Follow log output', false)
217
+ .option('--tail <lines>', 'Number of lines', '100')
218
+ .action(async (options) => {
219
+ try {
220
+ await serviceManager.showLogs(options.name, {
221
+ follow: options.follow,
222
+ tail: parseInt(options.tail),
223
+ });
224
+ }
225
+ catch (error) {
226
+ fail(`Failed to show logs: ${error instanceof Error ? error.message : error}`);
227
+ }
228
+ });
229
+ // ─── Key ──────────────────────────────────────────────────────────────────────
230
+ program
231
+ .command('key')
232
+ .description('Display the current agent API key')
233
+ .option('--agent-url <url>', 'Agent URL', DEFAULT_AGENT_URL)
234
+ .action(async (options) => {
235
+ try {
236
+ const res = await fetch(`${options.agentUrl}/api/agent-api-key`);
237
+ if (!res.ok)
238
+ fail(`Agent returned ${res.status}`);
239
+ const data = await res.json();
240
+ console.log(`\n \x1b[1mAPI Key:\x1b[0m ${data.apiKey}`);
241
+ console.log(` Generated: ${data.generatedAt}`);
242
+ console.log(` Note: ${data.note}\n`);
243
+ }
244
+ catch (error) {
245
+ fail(`Cannot reach agent at ${options.agentUrl}: ${error instanceof Error ? error.message : error}`);
246
+ }
247
+ });
248
+ // ─── Health ───────────────────────────────────────────────────────────────────
249
+ program
250
+ .command('health')
251
+ .description('Check health of the agent')
252
+ .option('-n, --name <name>', 'Instance name (uses local registry)')
253
+ .option('--agent-url <url>', 'Agent URL (direct)', DEFAULT_AGENT_URL)
254
+ .action(async (options) => {
255
+ try {
256
+ if (options.name) {
257
+ const h = await serviceManager.checkHealth(options.name);
258
+ console.log(JSON.stringify(h, null, 2));
259
+ }
260
+ else {
261
+ const res = await fetch(`${options.agentUrl}/health`);
262
+ if (!res.ok)
263
+ fail(`Health check failed: ${res.status}`);
264
+ const data = await res.json();
265
+ console.log(`\n Status: \x1b[32m${data.status}\x1b[0m`);
266
+ console.log(` Version: ${data.version}`);
267
+ console.log(` Uptime: ${Math.floor(data.uptime / 60)}m ${Math.floor(data.uptime % 60)}s`);
268
+ if (data.tunnelUrl)
269
+ console.log(` Tunnel: ${data.tunnelUrl}`);
270
+ console.log();
271
+ }
272
+ }
273
+ catch (error) {
274
+ fail(`Health check failed: ${error instanceof Error ? error.message : error}`);
275
+ }
276
+ });
277
+ // ─── Version (detailed) ──────────────────────────────────────────────────────
278
+ program
279
+ .command('info')
280
+ .description('Show detailed version and system information')
281
+ .option('--agent-url <url>', 'Agent URL', DEFAULT_AGENT_URL)
282
+ .action(async (options) => {
283
+ console.log(`\n \x1b[1mVibeControls Agent\x1b[0m v${packageVersion}`);
284
+ console.log(` Node.js: ${process.version}`);
285
+ console.log(` Platform: ${process.platform}`);
286
+ console.log(` Architecture: ${process.arch}`);
287
+ try {
288
+ const res = await fetch(`${options.agentUrl}/api/version`);
289
+ if (res.ok) {
290
+ const data = await res.json();
291
+ console.log(` Agent Port: ${data.port || 'N/A'}`);
292
+ console.log(` Uptime: ${data.uptime ? `${Math.floor(data.uptime / 60)}m` : 'N/A'}`);
293
+ }
294
+ }
295
+ catch {
296
+ console.log(` Agent: not reachable at ${options.agentUrl}`);
297
+ }
298
+ console.log();
299
+ });
300
+ // ─── Setup ────────────────────────────────────────────────────────────────────
301
+ program
302
+ .command('setup')
303
+ .description('Install or verify system dependencies (tmux, ttyd, cloudflared)')
304
+ .option('--check', 'Only check without installing', false)
305
+ .action(async (options) => {
306
+ try {
307
+ const { bootstrap, checkDependencies } = await import('./services/bootstrap.js');
308
+ if (options.check) {
309
+ console.log('\n Checking dependencies...\n');
310
+ const deps = checkDependencies();
311
+ for (const [name, info] of Object.entries(deps)) {
312
+ const icon = info.available ? '\x1b[32m✓\x1b[0m' : '\x1b[31m✗\x1b[0m';
313
+ console.log(` ${icon} ${name.padEnd(14)} ${info.available ? info.version : 'not installed'}`);
314
+ }
315
+ const missing = Object.entries(deps).filter(([, v]) => !v.available);
316
+ if (missing.length > 0) {
317
+ console.log(`\n Missing: ${missing.map(([k]) => k).join(', ')}`);
318
+ console.log(' Run `vibe setup` to install them.\n');
319
+ process.exit(1);
320
+ }
321
+ else {
322
+ console.log('\n All dependencies are installed.\n');
323
+ }
324
+ }
325
+ else {
326
+ console.log('\n Installing system dependencies...\n');
327
+ const results = await bootstrap({ verbose: true });
328
+ const failed = results.filter(r => r.status === 'failed');
329
+ if (failed.length > 0) {
330
+ console.log(`\n ${failed.length} tool(s) failed: ${failed.map(f => f.tool).join(', ')}`);
331
+ console.log(' You may need to install manually or use sudo.\n');
332
+ process.exit(1);
333
+ }
334
+ else {
335
+ console.log('\n All dependencies installed successfully.\n');
336
+ }
337
+ }
338
+ }
339
+ catch (error) {
340
+ fail(`Setup failed: ${error instanceof Error ? error.message : error}`);
341
+ }
342
+ });
343
+ // ─── Config ───────────────────────────────────────────────────────────────────
344
+ program
345
+ .command('config')
346
+ .description('Manage agent configuration')
347
+ .option('--set <key=value>', 'Set a configuration value')
348
+ .option('--get <key>', 'Get a configuration value')
349
+ .option('--list', 'List all configuration')
350
+ .option('--agent-url <url>', 'Agent URL', DEFAULT_AGENT_URL)
351
+ .action(async (options) => {
352
+ try {
353
+ if (options.set) {
354
+ const eq = options.set.indexOf('=');
355
+ if (eq === -1)
356
+ fail('Use --set key=value format');
357
+ const key = options.set.substring(0, eq);
358
+ const value = options.set.substring(eq + 1);
359
+ const { ok } = await agentFetch(options.agentUrl, `/api/config/${key}`, {
360
+ method: 'PUT',
361
+ body: JSON.stringify({ value }),
362
+ });
363
+ if (ok)
364
+ console.log(` Set: ${key} = ${value}`);
365
+ else
366
+ fail(`Failed to set config key '${key}'`);
367
+ }
368
+ else if (options.get) {
369
+ const { ok, data } = await agentFetch(options.agentUrl, `/api/config/${options.get}`);
370
+ if (ok && data.value !== undefined) {
371
+ console.log(` ${options.get} = ${data.value}`);
372
+ }
373
+ else {
374
+ console.log(` Key '${options.get}' not found`);
375
+ }
376
+ }
377
+ else if (options.list) {
378
+ const { ok, data } = await agentFetch(options.agentUrl, '/api/config/');
379
+ if (ok) {
380
+ const entries = Object.entries(data);
381
+ if (entries.length === 0) {
382
+ console.log(' No configuration set.');
383
+ }
384
+ else {
385
+ for (const [k, v] of entries) {
386
+ console.log(` ${k} = ${v}`);
387
+ }
388
+ }
389
+ }
390
+ else {
391
+ fail('Failed to list config');
392
+ }
393
+ }
394
+ else {
395
+ console.log('Use --set key=value, --get <key>, or --list');
396
+ }
397
+ }
398
+ catch (error) {
399
+ fail(`Config failed: ${error instanceof Error ? error.message : error}`);
400
+ }
401
+ });
402
+ // ═══════════════════════════════════════════════════════════════════════════════
403
+ // Tunnel Commands
404
+ // ═══════════════════════════════════════════════════════════════════════════════
405
+ const tunnelCmd = program
406
+ .command('tunnel')
407
+ .description('Manage cloudflared tunnels');
408
+ tunnelCmd
409
+ .command('list')
410
+ .description('List all tunnels')
411
+ .option('--agent-url <url>', 'Agent URL', DEFAULT_AGENT_URL)
412
+ .action(async (options) => {
413
+ try {
414
+ const { ok, data } = await agentFetch(options.agentUrl, '/api/tunnel');
415
+ if (!ok)
416
+ fail('Failed to list tunnels');
417
+ if (data.tunnels.length === 0) {
418
+ console.log(' No tunnels found.');
419
+ }
420
+ else {
421
+ formatTable(data.tunnels.map(t => ({
422
+ ID: t.id.substring(0, 12) + '...',
423
+ Port: t.localPort,
424
+ 'Public URL': t.publicUrl || '(none)',
425
+ Status: t.status,
426
+ PID: t.pid || 'N/A',
427
+ })));
428
+ }
429
+ }
430
+ catch (error) {
431
+ fail(`Failed: ${error instanceof Error ? error.message : error}`);
432
+ }
433
+ });
434
+ tunnelCmd
435
+ .command('start')
436
+ .description('Start a tunnel for a local port')
437
+ .requiredOption('-p, --port <port>', 'Local port to expose')
438
+ .option('-s, --subdomain <subdomain>', 'Preferred subdomain')
439
+ .option('--agent-url <url>', 'Agent URL', DEFAULT_AGENT_URL)
440
+ .action(async (options) => {
441
+ try {
442
+ console.log(` Starting tunnel for port ${options.port}...`);
443
+ const { ok, data } = await agentFetch(options.agentUrl, '/api/tunnel/start', {
444
+ method: 'POST',
445
+ body: JSON.stringify({
446
+ localPort: parseInt(options.port),
447
+ subdomain: options.subdomain,
448
+ }),
449
+ });
450
+ if (!ok)
451
+ fail(data.error || `Agent returned error`);
452
+ console.log(`\n \x1b[32mTunnel started:\x1b[0m`);
453
+ console.log(` ID: ${data.id}`);
454
+ console.log(` Local Port: ${data.localPort}`);
455
+ console.log(` Public URL: ${data.publicUrl || '(pending...)'}`);
456
+ console.log(` PID: ${data.pid || 'N/A'}`);
457
+ console.log(` Status: ${data.status}\n`);
458
+ }
459
+ catch (error) {
460
+ fail(`Failed: ${error instanceof Error ? error.message : error}`);
461
+ }
462
+ });
463
+ tunnelCmd
464
+ .command('stop')
465
+ .description('Stop a running tunnel')
466
+ .requiredOption('-i, --id <id>', 'Tunnel ID')
467
+ .option('--agent-url <url>', 'Agent URL', DEFAULT_AGENT_URL)
468
+ .action(async (options) => {
469
+ try {
470
+ const { ok, data } = await agentFetch(options.agentUrl, `/api/tunnel/${options.id}/stop`, {
471
+ method: 'POST',
472
+ });
473
+ if (!ok)
474
+ fail(data.error || 'Failed to stop tunnel');
475
+ console.log(' Tunnel stopped.');
476
+ }
477
+ catch (error) {
478
+ fail(`Failed: ${error instanceof Error ? error.message : error}`);
479
+ }
480
+ });
481
+ tunnelCmd
482
+ .command('delete')
483
+ .description('Delete a tunnel')
484
+ .requiredOption('-i, --id <id>', 'Tunnel ID')
485
+ .option('--agent-url <url>', 'Agent URL', DEFAULT_AGENT_URL)
486
+ .action(async (options) => {
487
+ try {
488
+ const { ok, data } = await agentFetch(options.agentUrl, `/api/tunnel/${options.id}`, {
489
+ method: 'DELETE',
490
+ });
491
+ if (!ok)
492
+ fail(data.error || 'Failed to delete tunnel');
493
+ console.log(' Tunnel deleted.');
494
+ }
495
+ catch (error) {
496
+ fail(`Failed: ${error instanceof Error ? error.message : error}`);
497
+ }
498
+ });
499
+ tunnelCmd
500
+ .command('status')
501
+ .description('Get tunnel overview')
502
+ .option('--agent-url <url>', 'Agent URL', DEFAULT_AGENT_URL)
503
+ .action(async (options) => {
504
+ try {
505
+ const { ok, data } = await agentFetch(options.agentUrl, '/api/tunnel/status');
506
+ if (!ok)
507
+ fail('Failed to get tunnel status');
508
+ console.log(`\n Total: ${data.total}`);
509
+ console.log(` Active: \x1b[32m${data.active}\x1b[0m`);
510
+ console.log(` Inactive: ${data.inactive}`);
511
+ console.log(` Errored: \x1b[31m${data.errored}\x1b[0m\n`);
512
+ }
513
+ catch (error) {
514
+ fail(`Failed: ${error instanceof Error ? error.message : error}`);
515
+ }
516
+ });
517
+ // Agent tunnel (the main agent tunnel, not per-port tunnels)
518
+ tunnelCmd
519
+ .command('agent')
520
+ .description('Show/manage the main agent cloudflared tunnel')
521
+ .option('--start', 'Start the agent tunnel')
522
+ .option('--stop', 'Stop the agent tunnel')
523
+ .option('--agent-url <url>', 'Agent URL', DEFAULT_AGENT_URL)
524
+ .action(async (options) => {
525
+ try {
526
+ if (options.start) {
527
+ const { ok, data } = await agentFetch(options.agentUrl, '/api/agent-tunnel/start', { method: 'POST' });
528
+ if (!ok)
529
+ fail('Failed to start agent tunnel');
530
+ console.log(` Agent tunnel started: ${data.tunnelUrl}`);
531
+ }
532
+ else if (options.stop) {
533
+ await agentFetch(options.agentUrl, '/api/agent-tunnel/stop', { method: 'POST' });
534
+ console.log(' Agent tunnel stopped.');
535
+ }
536
+ else {
537
+ const { ok, data } = await agentFetch(options.agentUrl, '/api/agent-tunnel');
538
+ if (!ok)
539
+ fail('Failed to get agent tunnel');
540
+ console.log(`\n Tunnel URL: ${data.tunnelUrl || '(not running)'}`);
541
+ console.log(` Status: ${data.status || 'unknown'}\n`);
542
+ }
543
+ }
544
+ catch (error) {
545
+ fail(`Failed: ${error instanceof Error ? error.message : error}`);
546
+ }
547
+ });
548
+ // ═══════════════════════════════════════════════════════════════════════════════
549
+ // Session Commands (tmux)
550
+ // ═══════════════════════════════════════════════════════════════════════════════
551
+ const sessionCmd = program
552
+ .command('session')
553
+ .description('Manage tmux terminal sessions');
554
+ sessionCmd
555
+ .command('list')
556
+ .description('List all tmux sessions')
557
+ .option('--system', 'Include system (unmanaged) sessions')
558
+ .option('--agent-url <url>', 'Agent URL', DEFAULT_AGENT_URL)
559
+ .action(async (options) => {
560
+ try {
561
+ const path = options.system ? '/api/tmux/system' : '/api/tmux/';
562
+ const { ok, data } = await agentFetch(options.agentUrl, path);
563
+ if (!ok)
564
+ fail('Failed to list sessions');
565
+ const sessions = Array.isArray(data) ? data : (data.sessions ?? []);
566
+ if (sessions.length === 0) {
567
+ console.log(' No sessions found.');
568
+ }
569
+ else {
570
+ formatTable(sessions.map(s => ({
571
+ ID: (s.id || s.sessionId || '').substring(0, 12),
572
+ Name: s.sessionName || s.name || 'N/A',
573
+ Status: s.status || 'unknown',
574
+ Port: s.ttydPort || 'N/A',
575
+ Project: s.projectId || 'N/A',
576
+ })));
577
+ }
578
+ }
579
+ catch (error) {
580
+ fail(`Failed: ${error instanceof Error ? error.message : error}`);
581
+ }
582
+ });
583
+ sessionCmd
584
+ .command('create')
585
+ .description('Create a new tmux session')
586
+ .requiredOption('--name <name>', 'Session name')
587
+ .option('--project <id>', 'Project ID', 'default')
588
+ .option('--command <cmd>', 'Initial command')
589
+ .option('--cwd <dir>', 'Working directory')
590
+ .option('--agent-url <url>', 'Agent URL', DEFAULT_AGENT_URL)
591
+ .action(async (options) => {
592
+ try {
593
+ const { ok, data } = await agentFetch(options.agentUrl, '/api/tmux/create', {
594
+ method: 'POST',
595
+ body: JSON.stringify({
596
+ sessionId: `cli-${Date.now()}`,
597
+ sessionName: options.name,
598
+ projectId: options.project,
599
+ command: options.command,
600
+ startDirectory: options.cwd,
601
+ }),
602
+ });
603
+ if (!ok)
604
+ fail(data.error || 'Failed to create session');
605
+ console.log(` Session created: ${data.sessionName || options.name}`);
606
+ }
607
+ catch (error) {
608
+ fail(`Failed: ${error instanceof Error ? error.message : error}`);
609
+ }
610
+ });
611
+ sessionCmd
612
+ .command('kill')
613
+ .description('Kill a tmux session')
614
+ .requiredOption('-i, --id <id>', 'Session ID')
615
+ .option('--agent-url <url>', 'Agent URL', DEFAULT_AGENT_URL)
616
+ .action(async (options) => {
617
+ try {
618
+ const { ok, data } = await agentFetch(options.agentUrl, `/api/tmux/${options.id}`, {
619
+ method: 'DELETE',
620
+ });
621
+ if (!ok)
622
+ fail(data.error || 'Failed to kill session');
623
+ console.log(' Session killed.');
624
+ }
625
+ catch (error) {
626
+ fail(`Failed: ${error instanceof Error ? error.message : error}`);
627
+ }
628
+ });
629
+ sessionCmd
630
+ .command('exec')
631
+ .description('Execute a command in a tmux session')
632
+ .requiredOption('-i, --id <id>', 'Session ID')
633
+ .requiredOption('-c, --command <cmd>', 'Command to execute')
634
+ .option('--agent-url <url>', 'Agent URL', DEFAULT_AGENT_URL)
635
+ .action(async (options) => {
636
+ try {
637
+ const { ok, data } = await agentFetch(options.agentUrl, `/api/tmux/${options.id}/command`, {
638
+ method: 'POST',
639
+ body: JSON.stringify({ command: options.command }),
640
+ });
641
+ if (!ok)
642
+ fail(data.error || 'Failed to execute command');
643
+ console.log(' Command sent.');
644
+ }
645
+ catch (error) {
646
+ fail(`Failed: ${error instanceof Error ? error.message : error}`);
647
+ }
648
+ });
649
+ sessionCmd
650
+ .command('capture')
651
+ .description('Capture output from a tmux session')
652
+ .requiredOption('-i, --id <id>', 'Session ID')
653
+ .option('--agent-url <url>', 'Agent URL', DEFAULT_AGENT_URL)
654
+ .action(async (options) => {
655
+ try {
656
+ const { ok, data } = await agentFetch(options.agentUrl, `/api/tmux/${options.id}/capture`);
657
+ if (!ok)
658
+ fail('Failed to capture output');
659
+ console.log(data.output || '(empty)');
660
+ }
661
+ catch (error) {
662
+ fail(`Failed: ${error instanceof Error ? error.message : error}`);
663
+ }
664
+ });
665
+ // ═══════════════════════════════════════════════════════════════════════════════
666
+ // SSH Commands
667
+ // ═══════════════════════════════════════════════════════════════════════════════
668
+ const sshCmd = program
669
+ .command('ssh')
670
+ .description('Manage SSH connections');
671
+ sshCmd
672
+ .command('list')
673
+ .description('List saved SSH connections')
674
+ .option('--agent-url <url>', 'Agent URL', DEFAULT_AGENT_URL)
675
+ .action(async (options) => {
676
+ try {
677
+ const { ok, data } = await agentFetch(options.agentUrl, '/api/ssh/connections');
678
+ if (!ok)
679
+ fail('Failed to list SSH connections');
680
+ const conns = Array.isArray(data) ? data : (data.connections ?? []);
681
+ if (conns.length === 0) {
682
+ console.log(' No SSH connections saved.');
683
+ }
684
+ else {
685
+ formatTable(conns.map(c => ({
686
+ ID: (c.id || '').substring(0, 12),
687
+ Name: c.serverName,
688
+ Host: c.host,
689
+ Port: c.port,
690
+ User: c.username,
691
+ })));
692
+ }
693
+ }
694
+ catch (error) {
695
+ fail(`Failed: ${error instanceof Error ? error.message : error}`);
696
+ }
697
+ });
698
+ sshCmd
699
+ .command('add')
700
+ .description('Add an SSH connection')
701
+ .requiredOption('--name <name>', 'Server name')
702
+ .requiredOption('--host <host>', 'Hostname or IP')
703
+ .requiredOption('--user <user>', 'Username')
704
+ .option('--port <port>', 'Port', '22')
705
+ .option('--key <path>', 'Private key path')
706
+ .option('--agent-url <url>', 'Agent URL', DEFAULT_AGENT_URL)
707
+ .action(async (options) => {
708
+ try {
709
+ const { ok, data } = await agentFetch(options.agentUrl, '/api/ssh/connections', {
710
+ method: 'POST',
711
+ body: JSON.stringify({
712
+ serverName: options.name,
713
+ host: options.host,
714
+ port: parseInt(options.port),
715
+ username: options.user,
716
+ privateKeyPath: options.key,
717
+ }),
718
+ });
719
+ if (!ok)
720
+ fail(data.error || 'Failed to add connection');
721
+ console.log(` SSH connection '${options.name}' added.`);
722
+ }
723
+ catch (error) {
724
+ fail(`Failed: ${error instanceof Error ? error.message : error}`);
725
+ }
726
+ });
727
+ sshCmd
728
+ .command('remove')
729
+ .description('Remove an SSH connection')
730
+ .requiredOption('-i, --id <id>', 'Connection ID')
731
+ .option('--agent-url <url>', 'Agent URL', DEFAULT_AGENT_URL)
732
+ .action(async (options) => {
733
+ try {
734
+ const { ok } = await agentFetch(options.agentUrl, `/api/ssh/connections/${options.id}`, {
735
+ method: 'DELETE',
736
+ });
737
+ if (!ok)
738
+ fail('Failed to remove connection');
739
+ console.log(' SSH connection removed.');
740
+ }
741
+ catch (error) {
742
+ fail(`Failed: ${error instanceof Error ? error.message : error}`);
743
+ }
744
+ });
745
+ sshCmd
746
+ .command('test')
747
+ .description('Test an SSH connection')
748
+ .requiredOption('-i, --id <id>', 'Connection ID')
749
+ .option('--agent-url <url>', 'Agent URL', DEFAULT_AGENT_URL)
750
+ .action(async (options) => {
751
+ try {
752
+ const { ok, data } = await agentFetch(options.agentUrl, `/api/ssh/test/${options.id}`, { method: 'POST' });
753
+ if (!ok)
754
+ fail(data.error || 'Connection test failed');
755
+ console.log(' \x1b[32mConnection successful.\x1b[0m');
756
+ }
757
+ catch (error) {
758
+ fail(`Failed: ${error instanceof Error ? error.message : error}`);
759
+ }
760
+ });
761
+ sshCmd
762
+ .command('exec')
763
+ .description('Execute a command on a remote server')
764
+ .requiredOption('-i, --id <id>', 'Connection ID')
765
+ .requiredOption('-c, --command <cmd>', 'Command to execute')
766
+ .option('--agent-url <url>', 'Agent URL', DEFAULT_AGENT_URL)
767
+ .action(async (options) => {
768
+ try {
769
+ const { ok, data } = await agentFetch(options.agentUrl, '/api/ssh/execute', {
770
+ method: 'POST',
771
+ body: JSON.stringify({
772
+ connectionId: options.id,
773
+ command: options.command,
774
+ }),
775
+ });
776
+ if (!ok)
777
+ fail(data.error || 'Execution failed');
778
+ if (data.output)
779
+ console.log(data.output);
780
+ if (data.error)
781
+ console.error(data.error);
782
+ }
783
+ catch (error) {
784
+ fail(`Failed: ${error instanceof Error ? error.message : error}`);
785
+ }
786
+ });
787
+ // ═══════════════════════════════════════════════════════════════════════════════
788
+ // Port Forward Commands
789
+ // ═══════════════════════════════════════════════════════════════════════════════
790
+ const forwardCmd = program
791
+ .command('forward')
792
+ .description('Manage SSH port forwards');
793
+ forwardCmd
794
+ .command('list')
795
+ .description('List all port forwards')
796
+ .option('--agent-url <url>', 'Agent URL', DEFAULT_AGENT_URL)
797
+ .action(async (options) => {
798
+ try {
799
+ const { ok, data } = await agentFetch(options.agentUrl, '/api/port-forward/');
800
+ if (!ok)
801
+ fail('Failed to list forwards');
802
+ const fwds = Array.isArray(data) ? data : (data.forwards ?? []);
803
+ if (fwds.length === 0) {
804
+ console.log(' No port forwards found.');
805
+ }
806
+ else {
807
+ formatTable(fwds.map(f => ({
808
+ ID: (f.id || '').substring(0, 12),
809
+ Local: f.localPort,
810
+ Remote: `${f.remoteHost}:${f.remotePort}`,
811
+ Server: f.serverName,
812
+ Status: f.status,
813
+ })));
814
+ }
815
+ }
816
+ catch (error) {
817
+ fail(`Failed: ${error instanceof Error ? error.message : error}`);
818
+ }
819
+ });
820
+ forwardCmd
821
+ .command('create')
822
+ .description('Create a port forward rule')
823
+ .requiredOption('--local <port>', 'Local port')
824
+ .requiredOption('--remote-host <host>', 'Remote host')
825
+ .requiredOption('--remote-port <port>', 'Remote port')
826
+ .requiredOption('--server <name>', 'SSH server name')
827
+ .option('--agent-url <url>', 'Agent URL', DEFAULT_AGENT_URL)
828
+ .action(async (options) => {
829
+ try {
830
+ const { ok, data } = await agentFetch(options.agentUrl, '/api/port-forward/', {
831
+ method: 'POST',
832
+ body: JSON.stringify({
833
+ localPort: parseInt(options.local),
834
+ remoteHost: options.remoteHost,
835
+ remotePort: parseInt(options.remotePort),
836
+ serverName: options.server,
837
+ }),
838
+ });
839
+ if (!ok)
840
+ fail(data.error || 'Failed to create forward');
841
+ console.log(' Port forward created.');
842
+ }
843
+ catch (error) {
844
+ fail(`Failed: ${error instanceof Error ? error.message : error}`);
845
+ }
846
+ });
847
+ forwardCmd
848
+ .command('start')
849
+ .description('Start a port forward')
850
+ .requiredOption('-i, --id <id>', 'Forward ID')
851
+ .option('--agent-url <url>', 'Agent URL', DEFAULT_AGENT_URL)
852
+ .action(async (options) => {
853
+ try {
854
+ const { ok, data } = await agentFetch(options.agentUrl, `/api/port-forward/${options.id}/start`, {
855
+ method: 'POST',
856
+ });
857
+ if (!ok)
858
+ fail(data.error || 'Failed to start forward');
859
+ console.log(' Port forward started.');
860
+ }
861
+ catch (error) {
862
+ fail(`Failed: ${error instanceof Error ? error.message : error}`);
863
+ }
864
+ });
865
+ forwardCmd
866
+ .command('stop')
867
+ .description('Stop a port forward')
868
+ .requiredOption('-i, --id <id>', 'Forward ID')
869
+ .option('--agent-url <url>', 'Agent URL', DEFAULT_AGENT_URL)
870
+ .action(async (options) => {
871
+ try {
872
+ const { ok, data } = await agentFetch(options.agentUrl, `/api/port-forward/${options.id}/stop`, {
873
+ method: 'POST',
874
+ });
875
+ if (!ok)
876
+ fail(data.error || 'Failed to stop forward');
877
+ console.log(' Port forward stopped.');
878
+ }
879
+ catch (error) {
880
+ fail(`Failed: ${error instanceof Error ? error.message : error}`);
881
+ }
882
+ });
883
+ forwardCmd
884
+ .command('delete')
885
+ .description('Delete a port forward')
886
+ .requiredOption('-i, --id <id>', 'Forward ID')
887
+ .option('--agent-url <url>', 'Agent URL', DEFAULT_AGENT_URL)
888
+ .action(async (options) => {
889
+ try {
890
+ const { ok } = await agentFetch(options.agentUrl, `/api/port-forward/${options.id}`, {
891
+ method: 'DELETE',
892
+ });
893
+ if (!ok)
894
+ fail('Failed to delete forward');
895
+ console.log(' Port forward deleted.');
896
+ }
897
+ catch (error) {
898
+ fail(`Failed: ${error instanceof Error ? error.message : error}`);
899
+ }
900
+ });
901
+ // ═══════════════════════════════════════════════════════════════════════════════
902
+ // Task Commands
903
+ // ═══════════════════════════════════════════════════════════════════════════════
904
+ const taskCmd = program
905
+ .command('task')
906
+ .description('Manage background tasks');
907
+ taskCmd
908
+ .command('list')
909
+ .description('List tasks')
910
+ .option('--status <status>', 'Filter by status (pending, running, completed, failed)')
911
+ .option('--agent-url <url>', 'Agent URL', DEFAULT_AGENT_URL)
912
+ .action(async (options) => {
913
+ try {
914
+ const query = options.status ? `?status=${options.status}` : '';
915
+ const { ok, data } = await agentFetch(options.agentUrl, `/api/tasks/${query}`);
916
+ if (!ok)
917
+ fail('Failed to list tasks');
918
+ const tasks = Array.isArray(data) ? data : [];
919
+ if (tasks.length === 0) {
920
+ console.log(' No tasks found.');
921
+ }
922
+ else {
923
+ formatTable(tasks.map(t => ({
924
+ ID: (t.id || '').substring(0, 12),
925
+ Type: t.type,
926
+ Status: t.status,
927
+ Created: t.createdAt ? timeAgo(t.createdAt) : 'N/A',
928
+ })));
929
+ }
930
+ }
931
+ catch (error) {
932
+ fail(`Failed: ${error instanceof Error ? error.message : error}`);
933
+ }
934
+ });
935
+ taskCmd
936
+ .command('run')
937
+ .description('Run a command as a background task')
938
+ .requiredOption('-c, --command <cmd>', 'Command to execute')
939
+ .option('--cwd <dir>', 'Working directory')
940
+ .option('--agent-url <url>', 'Agent URL', DEFAULT_AGENT_URL)
941
+ .action(async (options) => {
942
+ try {
943
+ const { ok, data } = await agentFetch(options.agentUrl, '/api/tasks/', {
944
+ method: 'POST',
945
+ body: JSON.stringify({
946
+ type: 'command',
947
+ payload: JSON.stringify({
948
+ command: options.command,
949
+ cwd: options.cwd,
950
+ }),
951
+ }),
952
+ });
953
+ if (!ok)
954
+ fail(data.error || 'Failed to create task');
955
+ console.log(` Task created: ${data.id}`);
956
+ }
957
+ catch (error) {
958
+ fail(`Failed: ${error instanceof Error ? error.message : error}`);
959
+ }
960
+ });
961
+ // ═══════════════════════════════════════════════════════════════════════════════
962
+ // Notification Commands
963
+ // ═══════════════════════════════════════════════════════════════════════════════
964
+ const notifyCmd = program
965
+ .command('notify')
966
+ .description('Manage notifications');
967
+ notifyCmd
968
+ .command('list')
969
+ .description('List notifications')
970
+ .option('--unread', 'Show only unread')
971
+ .option('--agent-url <url>', 'Agent URL', DEFAULT_AGENT_URL)
972
+ .action(async (options) => {
973
+ try {
974
+ const path = options.unread ? '/api/notifications/unread' : '/api/notifications/';
975
+ const { ok, data } = await agentFetch(options.agentUrl, path);
976
+ if (!ok)
977
+ fail('Failed to list notifications');
978
+ const notifications = Array.isArray(data) ? data : (data.notifications ?? []);
979
+ if (notifications.length === 0) {
980
+ console.log(' No notifications.');
981
+ }
982
+ else {
983
+ for (const n of notifications) {
984
+ const icon = n.type === 'error' ? '\x1b[31m●\x1b[0m' :
985
+ n.type === 'warning' ? '\x1b[33m●\x1b[0m' :
986
+ n.type === 'success' ? '\x1b[32m●\x1b[0m' : '\x1b[34m●\x1b[0m';
987
+ const unread = n.status === 'unread' ? ' [NEW]' : '';
988
+ console.log(` ${icon} ${n.title}${unread}`);
989
+ console.log(` ${n.message}`);
990
+ if (n.createdAt)
991
+ console.log(` ${timeAgo(n.createdAt)}`);
992
+ console.log();
993
+ }
994
+ }
995
+ }
996
+ catch (error) {
997
+ fail(`Failed: ${error instanceof Error ? error.message : error}`);
998
+ }
999
+ });
1000
+ notifyCmd
1001
+ .command('read-all')
1002
+ .description('Mark all notifications as read')
1003
+ .option('--agent-url <url>', 'Agent URL', DEFAULT_AGENT_URL)
1004
+ .action(async (options) => {
1005
+ try {
1006
+ const { ok } = await agentFetch(options.agentUrl, '/api/notifications/read-all', {
1007
+ method: 'PUT',
1008
+ });
1009
+ if (!ok)
1010
+ fail('Failed to mark as read');
1011
+ console.log(' All notifications marked as read.');
1012
+ }
1013
+ catch (error) {
1014
+ fail(`Failed: ${error instanceof Error ? error.message : error}`);
1015
+ }
1016
+ });
1017
+ // ═══════════════════════════════════════════════════════════════════════════════
1018
+ // System Info
1019
+ // ═══════════════════════════════════════════════════════════════════════════════
1020
+ program
1021
+ .command('system')
1022
+ .description('Show system information from the agent')
1023
+ .option('--agent-url <url>', 'Agent URL', DEFAULT_AGENT_URL)
1024
+ .action(async (options) => {
1025
+ try {
1026
+ const { ok, data } = await agentFetch(options.agentUrl, '/api/config/system/info');
1027
+ if (!ok)
1028
+ fail('Failed to get system info');
1029
+ console.log(`\n Hostname: ${data.hostname}`);
1030
+ console.log(` Platform: ${data.platform}`);
1031
+ console.log(` Architecture: ${data.arch}`);
1032
+ console.log(` CPUs: ${data.cpuCount}`);
1033
+ console.log(` Memory: ${data.totalMemory}`);
1034
+ console.log(` Uptime: ${data.uptime}`);
1035
+ console.log(` Environment: ${data.environment}\n`);
1036
+ }
1037
+ catch (error) {
1038
+ fail(`Failed: ${error instanceof Error ? error.message : error}`);
1039
+ }
1040
+ });
1041
+ // ═══════════════════════════════════════════════════════════════════════════════
1042
+ program.parse();
1043
+ //# sourceMappingURL=cli.js.map