@hamp10/agentforge 0.1.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.
@@ -0,0 +1,558 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { Command } from 'commander';
4
+ import fs from 'fs';
5
+ import path from 'path';
6
+ import os from 'os';
7
+ import open from 'open';
8
+ import { execSync, spawn } from 'child_process';
9
+ import { AgentForgeWorker } from '../src/worker.js';
10
+ import { OpenClawCLI } from '../src/OpenClawCLI.js';
11
+
12
+ const CONFIG_DIR = path.join(os.homedir(), '.agentforge');
13
+ const CONFIG_FILE = path.join(CONFIG_DIR, 'config.json');
14
+
15
+ const program = new Command();
16
+
17
+ function ensureConfigDir() {
18
+ if (!fs.existsSync(CONFIG_DIR)) {
19
+ fs.mkdirSync(CONFIG_DIR, { recursive: true });
20
+ }
21
+ }
22
+
23
+ function loadConfig() {
24
+ ensureConfigDir();
25
+ if (!fs.existsSync(CONFIG_FILE)) {
26
+ return {};
27
+ }
28
+ try {
29
+ return JSON.parse(fs.readFileSync(CONFIG_FILE, 'utf8'));
30
+ } catch (error) {
31
+ console.error('❌ Failed to load config:', error.message);
32
+ return {};
33
+ }
34
+ }
35
+
36
+ function saveConfig(config) {
37
+ ensureConfigDir();
38
+ fs.writeFileSync(CONFIG_FILE, JSON.stringify(config, null, 2));
39
+ }
40
+
41
+ program
42
+ .name('agentforge')
43
+ .description('AgentForge worker - connect your machine to agentforge.ai')
44
+ .version('0.1.0');
45
+
46
+ program
47
+ .command('login')
48
+ .description('Authenticate with AgentForge')
49
+ .option('--url <url>', 'Custom AgentForge URL')
50
+ .option('--token <token>', 'Pre-issued auth token (skips OAuth browser flow)')
51
+ .action(async (options) => {
52
+ const baseUrl = options.url || process.env.AGENTFORGE_URL || 'https://agentforgeai-production.up.railway.app';
53
+ const preToken = options.token || process.env.AGENTFORGE_TOKEN;
54
+
55
+ console.log('');
56
+ console.log('🔐 Authenticating with AgentForge');
57
+ console.log('================================');
58
+ console.log('');
59
+
60
+ try {
61
+ let token;
62
+
63
+ if (preToken) {
64
+ // ── Pre-issued token path — no browser, no polling ──────────────────
65
+ console.log('🔑 Using pre-issued token...');
66
+
67
+ // Validate the token with the server
68
+ const validateResponse = await fetch(`${baseUrl}/api/cli/auth/validate`, {
69
+ method: 'POST',
70
+ headers: { 'Content-Type': 'application/json' },
71
+ body: JSON.stringify({ token: preToken })
72
+ });
73
+
74
+ if (!validateResponse.ok) {
75
+ throw new Error(`Token validation failed: ${validateResponse.statusText}`);
76
+ }
77
+
78
+ const validateData = await validateResponse.json();
79
+ if (!validateData.success) {
80
+ throw new Error(validateData.error || 'Invalid or expired token');
81
+ }
82
+
83
+ token = preToken;
84
+ } else {
85
+ // ── Standard device-code OAuth flow ─────────────────────────────────
86
+
87
+ // Step 1: Request device code
88
+ console.log('📡 Requesting authentication...');
89
+ const deviceResponse = await fetch(`${baseUrl}/api/cli/auth/device`, {
90
+ method: 'POST',
91
+ headers: { 'Content-Type': 'application/json' }
92
+ });
93
+
94
+ if (!deviceResponse.ok) {
95
+ throw new Error(`Failed to get device code: ${deviceResponse.statusText}`);
96
+ }
97
+
98
+ const deviceData = await deviceResponse.json();
99
+ const { device_code, user_code, verification_uri, interval } = deviceData;
100
+
101
+ // Step 2: Show user code and open browser
102
+ console.log('');
103
+ console.log('✨ Your authentication code:');
104
+ console.log('');
105
+ console.log(` ${user_code}`);
106
+ console.log('');
107
+ console.log('🌐 Opening browser for authentication...');
108
+ console.log(` ${verification_uri}?code=${user_code}`);
109
+ console.log('');
110
+
111
+ await open(`${verification_uri}?code=${user_code}`);
112
+
113
+ console.log('⏳ Waiting for approval...');
114
+ console.log('');
115
+
116
+ // Step 3: Poll for approval
117
+ const pollInterval = (interval || 5) * 1000;
118
+ let attempts = 0;
119
+ const maxAttempts = 120; // 10 minutes
120
+
121
+ token = await new Promise((resolve, reject) => {
122
+ const pollTimer = setInterval(async () => {
123
+ attempts++;
124
+
125
+ if (attempts > maxAttempts) {
126
+ clearInterval(pollTimer);
127
+ reject(new Error('Authentication timeout'));
128
+ return;
129
+ }
130
+
131
+ try {
132
+ const pollResponse = await fetch(`${baseUrl}/api/cli/auth/poll`, {
133
+ method: 'POST',
134
+ headers: { 'Content-Type': 'application/json' },
135
+ body: JSON.stringify({ device_code })
136
+ });
137
+
138
+ const pollData = await pollResponse.json();
139
+
140
+ if (pollData.success && pollData.token) {
141
+ clearInterval(pollTimer);
142
+ resolve(pollData.token);
143
+ } else if (pollData.error) {
144
+ clearInterval(pollTimer);
145
+ reject(new Error(pollData.error));
146
+ }
147
+ // Otherwise keep polling
148
+ } catch (error) {
149
+ clearInterval(pollTimer);
150
+ reject(error);
151
+ }
152
+ }, pollInterval);
153
+ });
154
+ }
155
+
156
+ // Save token and URL
157
+ const config = loadConfig();
158
+ config.token = token;
159
+ config.url = baseUrl;
160
+ saveConfig(config);
161
+
162
+ console.log('✅ Authentication successful!');
163
+ console.log('');
164
+ console.log('Next step: agentforge start');
165
+ console.log('');
166
+ } catch (error) {
167
+ console.error('❌ Authentication failed:', error.message);
168
+ process.exit(1);
169
+ }
170
+ });
171
+
172
+ program
173
+ .command('start')
174
+ .description('Start worker and connect to AgentForge')
175
+ .option('-u, --url <url>', 'Custom AgentForge URL (overrides saved config)')
176
+ .action(async (options) => {
177
+ const config = loadConfig();
178
+
179
+ if (!config.token) {
180
+ console.error('❌ Not authenticated. Run: agentforge login');
181
+ process.exit(1);
182
+ }
183
+
184
+ // Check that a working AI backend is configured
185
+ if (config.provider !== 'local' && !OpenClawCLI.isAvailable()) {
186
+ console.error('');
187
+ console.error('❌ No AI backend configured.');
188
+ console.error('');
189
+ console.error(' AgentForge needs an AI model to run agents.');
190
+ console.error(' Configure a local model server (Ollama, LM Studio, Jan, etc.):');
191
+ console.error('');
192
+ console.error(' agentforge local --url http://localhost:11434 --model llama3.1:8b');
193
+ console.error('');
194
+ console.error(' Then run: agentforge start');
195
+ console.error('');
196
+ process.exit(1);
197
+ }
198
+
199
+ // Use saved URL from login, or override with --url flag
200
+ const baseUrl = options.url || config.url || process.env.AGENTFORGE_URL || 'https://agentforgeai-production.up.railway.app';
201
+ const wsUrl = baseUrl.replace(/^http/, 'ws') + '/socket';
202
+
203
+ console.log('');
204
+ console.log('🚀 Starting AgentForge Worker');
205
+ console.log('================================');
206
+ console.log(`📡 Connecting to: ${wsUrl}`);
207
+ console.log('');
208
+
209
+ // Tee all console output to ~/.agentforge/worker.log (rolling at 5MB)
210
+ const logFile = path.join(CONFIG_DIR, 'worker.log');
211
+ const MAX_LOG_BYTES = 5 * 1024 * 1024;
212
+ let logStream = fs.createWriteStream(logFile, { flags: 'a' });
213
+ logStream.write(`\n\n===== Worker started ${new Date().toISOString()} =====\n\n`);
214
+ const writeLog = (level, args) => {
215
+ const line = `[${new Date().toISOString()}] [${level}] ${args.map(a => (typeof a === 'string' ? a : JSON.stringify(a))).join(' ')}\n`;
216
+ try {
217
+ logStream.write(line);
218
+ if (fs.statSync(logFile).size > MAX_LOG_BYTES) {
219
+ logStream.end();
220
+ try { fs.renameSync(logFile, logFile + '.old'); } catch {}
221
+ logStream = fs.createWriteStream(logFile, { flags: 'a' });
222
+ logStream.write(`===== Log rolled ${new Date().toISOString()} =====\n`);
223
+ }
224
+ } catch {} // never crash due to log failure
225
+ };
226
+ const _origLog = console.log.bind(console);
227
+ const _origErr = console.error.bind(console);
228
+ const _origWarn = console.warn.bind(console);
229
+ console.log = (...a) => { _origLog(...a); writeLog('LOG', a); };
230
+ console.error = (...a) => { _origErr(...a); writeLog('ERR', a); };
231
+ console.warn = (...a) => { _origWarn(...a); writeLog('WRN', a); };
232
+
233
+ const worker = new AgentForgeWorker(config.token, wsUrl, config);
234
+
235
+ // Graceful shutdown
236
+ process.on('SIGINT', () => { console.log('\n[SIGINT received]'); worker.shutdown(); });
237
+ process.on('SIGTERM', () => { console.log('\n[SIGTERM received]'); worker.shutdown(); });
238
+ process.on('SIGHUP', () => { console.log('\n[SIGHUP received — terminal closed]'); worker.shutdown(); });
239
+
240
+ try {
241
+ await worker.initialize();
242
+ await worker.connect();
243
+
244
+ // Auto-launch AgentForge browser if not already running
245
+ if (!isBrowserRunning()) {
246
+ const dashboardUrl = baseUrl.replace(/^ws/, 'http') + '/dashboard';
247
+ launchBrowser(dashboardUrl);
248
+ }
249
+
250
+ console.log('');
251
+ console.log('✅ Worker running');
252
+ console.log(' Press Ctrl+C to stop');
253
+ console.log('');
254
+ } catch (error) {
255
+ console.error('❌ Failed to start worker:', error.message);
256
+ process.exit(1);
257
+ }
258
+ });
259
+
260
+ program
261
+ .command('status')
262
+ .description('Check worker configuration')
263
+ .action(() => {
264
+ const config = loadConfig();
265
+
266
+ console.log('');
267
+ console.log('📊 AgentForge Worker Status');
268
+ console.log('================================');
269
+ console.log(`Authenticated: ${config.token ? '✅ Yes' : '❌ No'}`);
270
+
271
+ // Backend status
272
+ if (config.provider === 'local') {
273
+ console.log(`AI Backend: ✅ Local model (${config.localModel || 'llama3.1:8b'} @ ${config.localUrl || 'http://localhost:11434'})`);
274
+ } else if (OpenClawCLI.isAvailable()) {
275
+ console.log(`AI Backend: ✅ openclaw`);
276
+ } else {
277
+ console.log(`AI Backend: ❌ Not configured`);
278
+ }
279
+
280
+ console.log(`Config file: ${CONFIG_FILE}`);
281
+ console.log('');
282
+
283
+ if (!config.token) {
284
+ console.log('Run: agentforge login');
285
+ } else if (config.provider !== 'local' && !OpenClawCLI.isAvailable()) {
286
+ console.log('Configure a backend first: agentforge local --url http://localhost:11434 --model llama3.1:8b');
287
+ } else {
288
+ console.log('Ready to connect! Run: agentforge start');
289
+ }
290
+ console.log('');
291
+ });
292
+
293
+ program
294
+ .command('logout')
295
+ .description('Remove authentication token')
296
+ .action(() => {
297
+ if (fs.existsSync(CONFIG_FILE)) {
298
+ fs.unlinkSync(CONFIG_FILE);
299
+ console.log('✅ Logged out successfully');
300
+ } else {
301
+ console.log('Not currently logged in');
302
+ }
303
+ });
304
+
305
+ program
306
+ .command('local')
307
+ .description('Configure a local model backend (Ollama, LM Studio, Jan, llama.cpp, etc.)')
308
+ .option('--url <url>', 'Base URL of the local server', 'http://localhost:11434')
309
+ .option('--model <model>', 'Model name to use', 'llama3.1:8b')
310
+ .option('--reset', 'Switch back to openclaw (default) backend')
311
+ .action(async (options) => {
312
+ const config = loadConfig();
313
+
314
+ if (options.reset) {
315
+ delete config.provider;
316
+ delete config.localUrl;
317
+ delete config.localModel;
318
+ saveConfig(config);
319
+ console.log('✅ Reset to openclaw backend');
320
+ return;
321
+ }
322
+
323
+ // Validate the server is reachable
324
+ console.log(`\n🦙 Testing connection to ${options.url}...`);
325
+ try {
326
+ const res = await fetch(`${options.url}/v1/models`, { signal: AbortSignal.timeout(5000) });
327
+ if (res.ok) {
328
+ const data = await res.json();
329
+ const models = data.data?.map(m => m.id) ?? [];
330
+ console.log(`✅ Connected! Available models: ${models.slice(0, 5).join(', ') || '(none listed)'}`);
331
+ } else {
332
+ console.log(`⚠️ Server responded with ${res.status} — saving anyway`);
333
+ }
334
+ } catch (err) {
335
+ console.log(`⚠️ Could not reach ${options.url} (${err.message})`);
336
+ console.log(` Make sure your local server is running before starting the worker.`);
337
+ }
338
+
339
+ config.provider = 'local';
340
+ config.localUrl = options.url;
341
+ config.localModel = options.model;
342
+ saveConfig(config);
343
+
344
+ console.log('');
345
+ console.log('✅ Local model backend configured:');
346
+ console.log(` URL: ${options.url}`);
347
+ console.log(` Model: ${options.model}`);
348
+ console.log('');
349
+ console.log('Run: agentforge start');
350
+ console.log('');
351
+ console.log('To switch back to openclaw: agentforge local --reset');
352
+ console.log('');
353
+ });
354
+
355
+ program
356
+ .command('refresh-token')
357
+ .description('Refresh expired Anthropic token across all openclaw agents')
358
+ .action(async () => {
359
+ const { execSync, spawnSync } = await import('child_process');
360
+ const { glob } = await import('fs');
361
+ const { promisify } = await import('util');
362
+ const globAsync = promisify(glob);
363
+
364
+ console.log('');
365
+ console.log('🔑 Refreshing Anthropic token');
366
+ console.log('================================');
367
+ console.log('');
368
+ console.log('Step 1: Run claude setup-token in a NEW terminal, then paste the token here.');
369
+ console.log('');
370
+
371
+ const readline = await import('readline');
372
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
373
+ const newToken = await new Promise(resolve => rl.question('Paste token: ', ans => { rl.close(); resolve(ans.trim()); }));
374
+
375
+ if (!newToken.startsWith('sk-ant-')) {
376
+ console.error('❌ Invalid token format');
377
+ process.exit(1);
378
+ }
379
+
380
+ console.log('');
381
+ console.log('Step 2: Updating all agent auth-profiles...');
382
+
383
+ const agentsDir = path.join(os.homedir(), '.openclaw', 'agents');
384
+ if (!fs.existsSync(agentsDir)) {
385
+ console.error('❌ No openclaw agents directory found');
386
+ process.exit(1);
387
+ }
388
+
389
+ let updated = 0;
390
+ const dirs = fs.readdirSync(agentsDir);
391
+ for (const dir of dirs) {
392
+ const authPath = path.join(agentsDir, dir, 'agent', 'auth-profiles.json');
393
+ if (!fs.existsSync(authPath)) continue;
394
+ try {
395
+ const data = JSON.parse(fs.readFileSync(authPath, 'utf-8'));
396
+ let changed = false;
397
+ for (const profile of Object.values(data?.profiles || {})) {
398
+ if (profile?.provider === 'anthropic' && profile?.token) {
399
+ profile.token = newToken;
400
+ changed = true;
401
+ }
402
+ }
403
+ if (changed) {
404
+ fs.writeFileSync(authPath, JSON.stringify(data, null, 2));
405
+ updated++;
406
+ }
407
+ } catch {}
408
+ }
409
+
410
+ console.log(`✅ Updated ${updated} agent auth files`);
411
+ console.log('');
412
+ console.log('Now run: agentforge start');
413
+ console.log('');
414
+ });
415
+
416
+ program
417
+ .command('setup')
418
+ .description('Full device setup: AgentForge login + Anthropic auth + Tailscale')
419
+ .option('--tailscale-key <key>', 'Tailscale auth key (from tailscale.com/admin/settings/keys)')
420
+ .action(async (options) => {
421
+ const { execSync, spawnSync } = await import('child_process');
422
+
423
+ console.log('');
424
+ console.log('🚀 AgentForge Device Setup');
425
+ console.log('================================');
426
+ console.log('');
427
+
428
+ // Step 1: AgentForge login
429
+ console.log('Step 1/3: AgentForge authentication');
430
+ console.log('Run: agentforge login');
431
+ console.log('(Complete login, then come back here)');
432
+ console.log('');
433
+
434
+ // Step 2: openclaw + Anthropic token
435
+ console.log('Step 2/3: Anthropic token setup');
436
+ console.log('Run in a new terminal: claude setup-token');
437
+ console.log('Then run: agentforge refresh-token');
438
+ console.log('');
439
+
440
+ // Step 3: Tailscale
441
+ console.log('Step 3/3: Tailscale (remote access from any network)');
442
+ console.log('');
443
+
444
+ const tailscaleInstalled = spawnSync('which', ['tailscale'], { encoding: 'utf-8' }).status === 0;
445
+
446
+ if (tailscaleInstalled) {
447
+ console.log('✅ Tailscale already installed');
448
+ } else {
449
+ console.log('Installing Tailscale...');
450
+ try {
451
+ execSync('brew install tailscale', { stdio: 'inherit' });
452
+ console.log('✅ Tailscale installed');
453
+ } catch (e) {
454
+ console.error('❌ Failed to install Tailscale. Install manually: https://tailscale.com/download');
455
+ }
456
+ }
457
+
458
+ if (options.tailscaleKey) {
459
+ try {
460
+ execSync(`sudo tailscale up --authkey=${options.tailscaleKey}`, { stdio: 'inherit' });
461
+ console.log('✅ Tailscale connected');
462
+ const result = spawnSync('tailscale', ['ip', '--4'], { encoding: 'utf-8' });
463
+ if (result.stdout) console.log(` Tailscale IP: ${result.stdout.trim()}`);
464
+ } catch (e) {
465
+ console.error('❌ Tailscale join failed. Run manually: sudo tailscale up');
466
+ }
467
+ } else {
468
+ console.log('To join your Tailscale network:');
469
+ console.log(' sudo tailscale up');
470
+ console.log(' (or: agentforge setup --tailscale-key <key>)');
471
+ console.log(' Get a key at: tailscale.com/admin/settings/keys');
472
+ }
473
+
474
+ console.log('');
475
+ console.log('✅ Setup complete! Run: agentforge start');
476
+ console.log('');
477
+ });
478
+
479
+ // ── Browser helpers ──────────────────────────────────────────────────────────
480
+
481
+ function findChrome() {
482
+ const candidates = [
483
+ '/Applications/Google Chrome.app/Contents/MacOS/Google Chrome',
484
+ '/Applications/Chromium.app/Contents/MacOS/Chromium',
485
+ '/Applications/Brave Browser.app/Contents/MacOS/Brave Browser',
486
+ '/Applications/Microsoft Edge.app/Contents/MacOS/Microsoft Edge',
487
+ ];
488
+ for (const c of candidates) {
489
+ if (fs.existsSync(c)) return c;
490
+ }
491
+ return null;
492
+ }
493
+
494
+ function isBrowserRunning() {
495
+ try {
496
+ const out = execSync('pgrep -f "remote-debugging-port=9223" 2>/dev/null || true').toString().trim();
497
+ return out.length > 0;
498
+ } catch {
499
+ return false;
500
+ }
501
+ }
502
+
503
+ function seedBrowserProfile() {
504
+ const srcDir = path.join(os.homedir(), 'Library', 'Application Support', 'Google', 'Chrome', 'Default');
505
+ const destDir = path.join(CONFIG_DIR, 'browser', 'Default');
506
+ if (!fs.existsSync(srcDir)) return;
507
+ if (fs.existsSync(destDir)) return; // already seeded
508
+ fs.mkdirSync(destDir, { recursive: true });
509
+ const files = ['Cookies', 'Login Data', 'Web Data'];
510
+ for (const f of files) {
511
+ const src = path.join(srcDir, f);
512
+ if (fs.existsSync(src)) {
513
+ try { fs.copyFileSync(src, path.join(destDir, f)); } catch {}
514
+ }
515
+ }
516
+ const lsDir = path.join(srcDir, 'Local Storage');
517
+ const lsDest = path.join(destDir, 'Local Storage');
518
+ if (fs.existsSync(lsDir) && !fs.existsSync(lsDest)) {
519
+ try {
520
+ fs.cpSync(lsDir, lsDest, { recursive: true });
521
+ } catch {}
522
+ }
523
+ }
524
+
525
+ function launchBrowser(dashboardUrl) {
526
+ const chrome = findChrome();
527
+ if (!chrome) return false;
528
+ const profileDir = path.join(CONFIG_DIR, 'browser');
529
+ seedBrowserProfile();
530
+ const proc = spawn(chrome, [
531
+ `--remote-debugging-port=9223`,
532
+ `--user-data-dir=${profileDir}`,
533
+ '--no-first-run',
534
+ '--no-default-browser-check',
535
+ dashboardUrl,
536
+ ], { detached: true, stdio: 'ignore' });
537
+ proc.unref();
538
+ return true;
539
+ }
540
+
541
+ program
542
+ .command('browser')
543
+ .description('Launch the AgentForge browser')
544
+ .option('--url <url>', 'URL to open', 'https://agentforgeai-production.up.railway.app/dashboard')
545
+ .action((options) => {
546
+ if (isBrowserRunning()) {
547
+ console.log('✅ AgentForge browser already running');
548
+ return;
549
+ }
550
+ const ok = launchBrowser(options.url);
551
+ if (!ok) {
552
+ console.error('❌ Could not find Chrome, Chromium, Brave, or Edge.');
553
+ console.error(' Install Chrome from https://www.google.com/chrome');
554
+ process.exit(1);
555
+ }
556
+ });
557
+
558
+ program.parse();
package/package.json ADDED
@@ -0,0 +1,22 @@
1
+ {
2
+ "name": "@hamp10/agentforge",
3
+ "version": "0.1.0",
4
+ "description": "AgentForge worker — connect your machine to agentforge.ai",
5
+ "type": "module",
6
+ "bin": {
7
+ "agentforge": "./bin/agentforge.js"
8
+ },
9
+ "files": [
10
+ "bin",
11
+ "src",
12
+ "templates"
13
+ ],
14
+ "dependencies": {
15
+ "@anthropic-ai/sdk": "^0.80.0",
16
+ "commander": "^12.0.0",
17
+ "open": "^10.0.0",
18
+ "puppeteer-core": "^24.40.0",
19
+ "tree-kill": "^1.2.2",
20
+ "ws": "^8.16.0"
21
+ }
22
+ }