@hamp10/agentforge 0.1.0 → 0.2.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/bin/agentforge.js CHANGED
@@ -8,10 +8,23 @@ import open from 'open';
8
8
  import { execSync, spawn } from 'child_process';
9
9
  import { AgentForgeWorker } from '../src/worker.js';
10
10
  import { OpenClawCLI } from '../src/OpenClawCLI.js';
11
+ import { checkAndUpdate } from '../src/selfUpdate.js';
12
+ import { runSupervisor, detachSupervisor, stopSupervisor } from '../src/supervisor.js';
11
13
 
12
14
  const CONFIG_DIR = path.join(os.homedir(), '.agentforge');
13
15
  const CONFIG_FILE = path.join(CONFIG_DIR, 'config.json');
14
16
 
17
+ process.on('unhandledRejection', (reason) => {
18
+ console.error('❌ Unhandled promise rejection:', reason instanceof Error ? reason.message : reason);
19
+ if (reason instanceof Error && reason.stack) {
20
+ console.error(reason.stack.split('\n').slice(0, 4).join('\n'));
21
+ }
22
+ });
23
+ process.on('uncaughtException', (err) => {
24
+ console.error('❌ Uncaught exception:', err.message);
25
+ console.error(err.stack?.split('\n').slice(0, 4).join('\n') || '');
26
+ });
27
+
15
28
  const program = new Command();
16
29
 
17
30
  function ensureConfigDir() {
@@ -173,7 +186,14 @@ program
173
186
  .command('start')
174
187
  .description('Start worker and connect to AgentForge')
175
188
  .option('-u, --url <url>', 'Custom AgentForge URL (overrides saved config)')
189
+ .option('--detach', 'Run worker in background (returns shell prompt immediately)')
190
+ .option('--no-daemon', 'Run worker directly without supervisor (internal use)')
176
191
  .action(async (options) => {
192
+ // Self-update: check for newer package version before doing anything else.
193
+ // Skipped when AGENTFORGE_SKIP_UPDATE=1 (set by supervisor on re-exec).
194
+ const pkg = JSON.parse(fs.readFileSync(new URL('../package.json', import.meta.url), 'utf8'));
195
+ await checkAndUpdate(pkg.name, pkg.version);
196
+
177
197
  const config = loadConfig();
178
198
 
179
199
  if (!config.token) {
@@ -196,6 +216,27 @@ program
196
216
  process.exit(1);
197
217
  }
198
218
 
219
+ // Supervisor routing:
220
+ // --detach → spawn background supervisor and return shell immediately
221
+ // (default) → run foreground supervisor that auto-restarts on crash
222
+ // --no-daemon → skip supervisor entirely (used by supervisor internally)
223
+ if (options.detach) {
224
+ // Build the argv for the background supervisor process (no --detach flag so it
225
+ // enters runSupervisor and manages workers; AGENTFORGE_SKIP_UPDATE=1 is set by detachSupervisor)
226
+ const supervisorArgv = ['start', ...(options.url ? ['--url', options.url] : [])];
227
+ detachSupervisor(supervisorArgv);
228
+ process.exit(0);
229
+ }
230
+
231
+ if (options.daemon !== false) {
232
+ // Foreground supervisor mode — re-exec self with --no-daemon, restart on crash
233
+ const innerArgv = ['start', '--no-daemon', ...(options.url ? ['--url', options.url] : [])];
234
+ await runSupervisor(innerArgv);
235
+ return; // runSupervisor exits the process — this is just a safety return
236
+ }
237
+
238
+ // --no-daemon: run the actual worker directly (no supervision wrapper)
239
+
199
240
  // Use saved URL from login, or override with --url flag
200
241
  const baseUrl = options.url || config.url || process.env.AGENTFORGE_URL || 'https://agentforgeai-production.up.railway.app';
201
242
  const wsUrl = baseUrl.replace(/^http/, 'ws') + '/socket';
@@ -241,11 +282,7 @@ program
241
282
  await worker.initialize();
242
283
  await worker.connect();
243
284
 
244
- // Auto-launch AgentForge browser if not already running
245
- if (!isBrowserRunning()) {
246
- const dashboardUrl = baseUrl.replace(/^ws/, 'http') + '/dashboard';
247
- launchBrowser(dashboardUrl);
248
- }
285
+ // Agent browser starts on-demand when an agent needs browser tools not at worker start.
249
286
 
250
287
  console.log('');
251
288
  console.log('✅ Worker running');
@@ -477,10 +514,31 @@ program
477
514
  });
478
515
 
479
516
  // ── Browser helpers ──────────────────────────────────────────────────────────
517
+ // Agents use a dedicated browser profile on port 9223 — isolated from the
518
+ // user's real browser, but seeded with Chrome cookies when Chrome is available
519
+ // (both share the same "Chrome Safe Storage" Keychain key, so cookies decrypt).
520
+ // Non-Chrome users get the same isolated profile but log in manually once.
521
+ // A LaunchAgent keeps the agent browser running on every login.
522
+
523
+ const CDP_PORT = 9223;
524
+ const BROWSER_PROFILE_DIR = path.join(CONFIG_DIR, 'browser');
525
+ const LAUNCH_AGENT_LABEL = 'ai.agentforge.browser';
526
+ const LAUNCH_AGENT_PLIST = path.join(
527
+ os.homedir(), 'Library', 'LaunchAgents', `${LAUNCH_AGENT_LABEL}.plist`
528
+ );
529
+ const CHROME_BIN = '/Applications/Google Chrome.app/Contents/MacOS/Google Chrome';
530
+
531
+ // Returns Google Chrome path if installed — required for cookie seeding
532
+ // (shared "Chrome Safe Storage" Keychain key makes copied cookies readable).
533
+ function findGoogleChrome() {
534
+ return fs.existsSync(CHROME_BIN) ? CHROME_BIN : null;
535
+ }
480
536
 
481
- function findChrome() {
537
+ // Returns the best available browser for running the agent profile.
538
+ // Prefers Chrome (for seeding compatibility) but falls back to others.
539
+ function findBrowser() {
482
540
  const candidates = [
483
- '/Applications/Google Chrome.app/Contents/MacOS/Google Chrome',
541
+ CHROME_BIN,
484
542
  '/Applications/Chromium.app/Contents/MacOS/Chromium',
485
543
  '/Applications/Brave Browser.app/Contents/MacOS/Brave Browser',
486
544
  '/Applications/Microsoft Edge.app/Contents/MacOS/Microsoft Edge',
@@ -493,66 +551,354 @@ function findChrome() {
493
551
 
494
552
  function isBrowserRunning() {
495
553
  try {
496
- const out = execSync('pgrep -f "remote-debugging-port=9223" 2>/dev/null || true').toString().trim();
554
+ const out = execSync(`pgrep -f "remote-debugging-port=${CDP_PORT}" 2>/dev/null || true`).toString().trim();
497
555
  return out.length > 0;
498
556
  } catch {
499
557
  return false;
500
558
  }
501
559
  }
502
560
 
503
- function seedBrowserProfile() {
561
+ // Check if the agent browser profile has an active Google session.
562
+ function isGoogleLoggedIn() {
563
+ const cookiePath = path.join(BROWSER_PROFILE_DIR, 'Default', 'Cookies');
564
+ if (!fs.existsSync(cookiePath)) return false;
565
+ try {
566
+ const count = execSync(
567
+ `sqlite3 "${cookiePath}" "SELECT COUNT(*) FROM cookies WHERE host_key LIKE '%.google.com' AND name='SID';" 2>/dev/null`
568
+ ).toString().trim();
569
+ return parseInt(count) > 0;
570
+ } catch { return false; }
571
+ }
572
+
573
+ // Copies Chrome session data into the agent browser profile so agents start
574
+ // already logged in. Only works with Google Chrome (shared Keychain key).
575
+ // NEVER overwrites Google cookies — Google session-binds cookies to browser
576
+ // fingerprint, so any manually-logged-in Google session must be preserved.
577
+ // Pass force=true to re-copy everything except Google cookies.
578
+ // Must be called while the agent browser is NOT running (Cookies file is locked).
579
+ function seedBrowserProfile(force = false) {
580
+ const chrome = findGoogleChrome();
581
+ if (!chrome) return 'no-chrome';
504
582
  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
583
+ const destDir = path.join(BROWSER_PROFILE_DIR, 'Default');
584
+ if (!fs.existsSync(srcDir)) return 'no-chrome';
585
+ const destCookies = path.join(destDir, 'Cookies');
586
+ if (!force && fs.existsSync(destCookies)) return 'already-seeded';
508
587
  fs.mkdirSync(destDir, { recursive: true });
509
- const files = ['Cookies', 'Login Data', 'Web Data'];
510
- for (const f of files) {
588
+
589
+ // Non-cookie files: always copy
590
+ for (const f of ['Login Data', 'Web Data', 'Preferences', 'Bookmarks', 'Bookmarks.bak']) {
511
591
  const src = path.join(srcDir, f);
512
- if (fs.existsSync(src)) {
513
- try { fs.copyFileSync(src, path.join(destDir, f)); } catch {}
514
- }
592
+ if (fs.existsSync(src)) try { fs.copyFileSync(src, path.join(destDir, f)); } catch {}
515
593
  }
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 {}
594
+ for (const dir of ['Local Storage', 'IndexedDB']) {
595
+ const src = path.join(srcDir, dir);
596
+ const dest = path.join(destDir, dir);
597
+ if (fs.existsSync(src)) try { fs.cpSync(src, dest, { recursive: true, force: true }); } catch {}
598
+ }
599
+
600
+ // Cookies: if agent browser already has a Google session, preserve it.
601
+ // Copy Chrome cookies but restore any existing Google cookies on top.
602
+ const srcCookies = path.join(srcDir, 'Cookies');
603
+ if (!fs.existsSync(srcCookies)) return 'seeded';
604
+
605
+ if (fs.existsSync(destCookies) && isGoogleLoggedIn()) {
606
+ // Preserve Google session: copy Chrome cookies via Python, skip google.com rows
607
+ const py = `
608
+ import sqlite3, shutil, os, sys
609
+ src = sqlite3.connect("${srcCookies.replace(/"/g, '\\"')}")
610
+ dst_path = "${destCookies.replace(/"/g, '\\"')}"
611
+ tmp = dst_path + ".new"
612
+ shutil.copy2("${srcCookies.replace(/"/g, '\\"')}", tmp)
613
+ # Load saved Google cookies from existing agent browser db
614
+ old = sqlite3.connect(dst_path)
615
+ old.row_factory = sqlite3.Row
616
+ google_rows = old.execute("SELECT * FROM cookies WHERE host_key LIKE '%.google.com' OR host_key='accounts.google.com'").fetchall()
617
+ old.close()
618
+ # Write them into the new cookie db
619
+ new_db = sqlite3.connect(tmp)
620
+ for row in google_rows:
621
+ cols = row.keys()
622
+ new_db.execute(f"DELETE FROM cookies WHERE host_key=? AND name=?", (row['host_key'], row['name']))
623
+ ph = ','.join(['?' for _ in cols])
624
+ try: new_db.execute(f"INSERT INTO cookies ({','.join(cols)}) VALUES ({ph})", tuple(row[c] for c in cols))
625
+ except: pass
626
+ new_db.commit(); new_db.close()
627
+ os.replace(tmp, dst_path)
628
+ print('ok')
629
+ `;
630
+ const pyFile = path.join(os.tmpdir(), 'af_seed_cookies.py');
631
+ fs.writeFileSync(pyFile, py);
632
+ try { execSync(`python3 "${pyFile}" 2>/dev/null`); } catch {}
633
+ } else {
634
+ // No existing Google session — copy cookies wholesale (first-time setup)
635
+ try { fs.copyFileSync(srcCookies, destCookies); } catch {}
636
+ }
637
+
638
+ return 'seeded';
639
+ }
640
+
641
+ // Live-syncs session cookies from Chrome to the agent browser using Python/sqlite3.
642
+ // Works while BOTH browsers are running — no restart needed.
643
+ // Returns the number of cookies synced or -1 on error.
644
+ function syncSessionCookies() {
645
+ const chrome = findGoogleChrome();
646
+ if (!chrome) return 0;
647
+ const srcDb = path.join(os.homedir(), 'Library', 'Application Support', 'Google', 'Chrome', 'Default', 'Cookies');
648
+ const destDb = path.join(BROWSER_PROFILE_DIR, 'Default', 'Cookies');
649
+ if (!fs.existsSync(srcDb) || !fs.existsSync(destDb)) return 0;
650
+
651
+ const py = `
652
+ import sqlite3, sys
653
+ src = sqlite3.connect("${srcDb.replace(/"/g, '\\"')}")
654
+ dst = sqlite3.connect("${destDb.replace(/"/g, '\\"')}")
655
+ src.row_factory = sqlite3.Row
656
+ rows = src.execute("SELECT * FROM cookies WHERE host_key LIKE '%agentforgeai%' OR host_key='localhost'").fetchall()
657
+ count = 0
658
+ for row in rows:
659
+ cols = row.keys()
660
+ vals = tuple(row[c] for c in cols)
661
+ try:
662
+ dst.execute(f"DELETE FROM cookies WHERE host_key=? AND name=?", (row['host_key'], row['name']))
663
+ ph = ','.join(['?' for _ in cols])
664
+ dst.execute(f"INSERT INTO cookies ({','.join(cols)}) VALUES ({ph})", vals)
665
+ count += 1
666
+ except Exception as e:
667
+ pass
668
+ dst.commit()
669
+ src.close(); dst.close()
670
+ print(count)
671
+ `;
672
+ try {
673
+ const pyFile = path.join(os.tmpdir(), 'af_cookie_sync.py');
674
+ fs.writeFileSync(pyFile, py);
675
+ const result = execSync(`python3 "${pyFile}" 2>/dev/null`).toString().trim();
676
+ return parseInt(result) || 0;
677
+ } catch { return -1; }
678
+ }
679
+
680
+ // Creates /Applications/AgentForge Browser.app — a Chrome wrapper that:
681
+ // - Appears as a distinct app in the dock (own icon, own name)
682
+ // - Launches Chrome with the agent profile flags
683
+ // - Shares Chrome's "Chrome Safe Storage" Keychain key so seeded cookies work
684
+ // Falls back to ~/Applications if /Applications isn't writable.
685
+ function installAgentBrowserApp() {
686
+ const chrome = findGoogleChrome();
687
+ if (!chrome) return null; // Chrome required — Chromium can't share Keychain
688
+
689
+ const appName = 'AgentForge Browser.app';
690
+ let appDir = path.join('/Applications', appName);
691
+ try {
692
+ fs.mkdirSync(path.join(appDir, 'Contents', 'MacOS'), { recursive: true });
693
+ fs.mkdirSync(path.join(appDir, 'Contents', 'Resources'), { recursive: true });
694
+ } catch {
695
+ appDir = path.join(os.homedir(), 'Applications', appName);
696
+ fs.mkdirSync(path.join(appDir, 'Contents', 'MacOS'), { recursive: true });
697
+ fs.mkdirSync(path.join(appDir, 'Contents', 'Resources'), { recursive: true });
522
698
  }
699
+
700
+ // Info.plist — distinct bundle ID so macOS treats this as a separate app
701
+ fs.writeFileSync(path.join(appDir, 'Contents', 'Info.plist'), `<?xml version="1.0" encoding="UTF-8"?>
702
+ <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
703
+ <plist version="1.0">
704
+ <dict>
705
+ <key>CFBundleName</key><string>AgentForge Browser</string>
706
+ <key>CFBundleDisplayName</key><string>AgentForge Browser</string>
707
+ <key>CFBundleIdentifier</key><string>ai.agentforge.browser-app</string>
708
+ <key>CFBundleExecutable</key><string>AgentForge Browser</string>
709
+ <key>CFBundleIconFile</key><string>AppIcon</string>
710
+ <key>CFBundlePackageType</key><string>APPL</string>
711
+ <key>CFBundleVersion</key><string>1.0</string>
712
+ <key>NSHighResolutionCapable</key><true/>
713
+ <key>LSMinimumSystemVersion</key><string>12.0</string>
714
+ </dict>
715
+ </plist>`);
716
+
717
+ // Executable — shell script that launches Chrome with agent flags.
718
+ // Uses spawn (not exec) so macOS associates the process with this app bundle.
719
+ const execPath = path.join(appDir, 'Contents', 'MacOS', 'AgentForge Browser');
720
+ fs.writeFileSync(execPath, `#!/bin/bash
721
+ CHROME="${chrome}"
722
+ PROFILE_DIR="$HOME/.agentforge/browser"
723
+
724
+ if [ ! -f "$CHROME" ]; then
725
+ osascript -e 'display alert "AgentForge Browser" message "Google Chrome is required but was not found."'
726
+ exit 1
727
+ fi
728
+
729
+ "$CHROME" \\
730
+ --remote-debugging-port=${CDP_PORT} \\
731
+ --user-data-dir="$PROFILE_DIR" \\
732
+ --no-first-run \\
733
+ --no-default-browser-check \\
734
+ --disable-sync \\
735
+ --disable-component-update \\
736
+ --safebrowsing-disable-auto-update \\
737
+ "$@"
738
+ `);
739
+ fs.chmodSync(execPath, 0o755);
740
+
741
+ // Generate icon with Python (graceful no-op if unavailable)
742
+ _generateAppIcon(path.join(appDir, 'Contents', 'Resources'));
743
+
744
+ // Ad-hoc sign + remove quarantine
745
+ try { execSync(`codesign --sign - --force "${appDir}" 2>/dev/null`); } catch {}
746
+ try { execSync(`xattr -rd com.apple.quarantine "${appDir}" 2>/dev/null`); } catch {}
747
+
748
+ // Register with Launch Services so it appears in Spotlight/dock
749
+ const lsreg = '/System/Library/Frameworks/CoreServices.framework/Frameworks/LaunchServices.framework/Support/lsregister';
750
+ try { execSync(`"${lsreg}" -f "${appDir}" 2>/dev/null`); } catch {}
751
+
752
+ return appDir;
523
753
  }
524
754
 
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();
755
+ function _generateAppIcon(resourcesDir) {
756
+ const icnsPath = path.join(resourcesDir, 'AppIcon.icns');
757
+ if (fs.existsSync(icnsPath)) return; // already generated
758
+ const pyScript = path.join(os.tmpdir(), 'af_browser_icon.py');
759
+ fs.writeFileSync(pyScript, `
760
+ try:
761
+ from PIL import Image, ImageDraw
762
+ except ImportError:
763
+ import subprocess, sys
764
+ subprocess.check_call([sys.executable, '-m', 'pip', 'install', 'Pillow', '-q'])
765
+ from PIL import Image, ImageDraw
766
+ import os, subprocess, tempfile
767
+
768
+ size = 1024
769
+ img = Image.new('RGBA', (size, size), (0, 0, 0, 0))
770
+ draw = ImageDraw.Draw(img)
771
+ cx, cy = size // 2, size // 2
772
+
773
+ draw.ellipse([cx-460, cy-460, cx+460, cy+460], fill=(10, 10, 12, 255))
774
+
775
+ bar_top, bar_h = cy - 260, 90
776
+ bar_l, bar_r = cx - 290, cx + 290
777
+ draw.rounded_rectangle([bar_l, bar_top, bar_r, bar_top+bar_h], radius=18, fill=(255, 107, 53, 255))
778
+ for i, col in enumerate([(232,70,60,255),(255,185,30,255),(39,199,109,255)]):
779
+ dx = bar_l + 38 + i*38
780
+ draw.ellipse([dx-11, cy-260+45-11, dx+11, cy-260+45+11], fill=col)
781
+ draw.rounded_rectangle([bar_l+130, bar_top+18, bar_r-18, bar_top+bar_h-18], radius=10, fill=(30,30,35,220))
782
+
783
+ vp_top, vp_bot = bar_top+bar_h+14, cy+280
784
+ draw.rounded_rectangle([bar_l, vp_top, bar_r, vp_bot], radius=18, fill=(20,20,25,255))
785
+
786
+ af = (255,107,53,255)
787
+ mx, my, s = cx, (vp_top+vp_bot)//2-10, 22
788
+ draw.polygon([(mx-90,my+90),(mx-90+s,my+90),(mx,my-90),(mx-s//2,my-90)], fill=af)
789
+ draw.polygon([(mx+90,my+90),(mx+90-s,my+90),(mx,my-90),(mx+s//2,my-90)], fill=af)
790
+ draw.rectangle([mx-52, my-8, mx+52, my+8+s], fill=af)
791
+
792
+ png = os.path.join(tempfile.gettempdir(), 'af_icon_gen.png')
793
+ iconset = os.path.join(tempfile.gettempdir(), 'af_icon.iconset')
794
+ img.save(png)
795
+ os.makedirs(iconset, exist_ok=True)
796
+ for sz in [16,32,128,256,512]:
797
+ subprocess.run(['sips','-z',str(sz),str(sz),png,'--out',f'{iconset}/icon_{sz}x{sz}.png'], capture_output=True)
798
+ subprocess.run(['sips','-z',str(sz*2),str(sz*2),png,'--out',f'{iconset}/icon_{sz}x{sz}@2x.png'], capture_output=True)
799
+ subprocess.run(['iconutil','-c','icns',iconset,'-o','${icnsPath}'], capture_output=True)
800
+ print('ok')
801
+ `);
802
+ try { execSync(`python3 "${pyScript}" 2>/dev/null`); } catch {}
803
+ }
804
+
805
+ function installLaunchAgent(browserPath, startupUrl = 'https://agentforgeai-production.up.railway.app/dashboard') {
806
+ const plist = `<?xml version="1.0" encoding="UTF-8"?>
807
+ <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
808
+ <plist version="1.0">
809
+ <dict>
810
+ <key>Label</key>
811
+ <string>${LAUNCH_AGENT_LABEL}</string>
812
+ <key>ProgramArguments</key>
813
+ <array>
814
+ <string>${browserPath}</string>
815
+ <string>--remote-debugging-address=0.0.0.0</string>
816
+ <string>--remote-debugging-port=${CDP_PORT}</string>
817
+ <string>--user-data-dir=${BROWSER_PROFILE_DIR}</string>
818
+ <string>--no-first-run</string>
819
+ <string>--no-default-browser-check</string>
820
+ <string>--disable-sync</string>
821
+ <string>--disable-component-update</string>
822
+ <string>--safebrowsing-disable-auto-update</string>
823
+ <string>${startupUrl}</string>
824
+ </array>
825
+ <key>RunAtLoad</key>
826
+ <true/>
827
+ <key>KeepAlive</key>
828
+ <true/>
829
+ <key>StandardErrorPath</key>
830
+ <string>${path.join(CONFIG_DIR, 'browser.log')}</string>
831
+ </dict>
832
+ </plist>`;
833
+ fs.mkdirSync(path.dirname(LAUNCH_AGENT_PLIST), { recursive: true });
834
+ fs.writeFileSync(LAUNCH_AGENT_PLIST, plist);
835
+ try { execSync(`launchctl load "${LAUNCH_AGENT_PLIST}" 2>/dev/null`); } catch {}
538
836
  return true;
539
837
  }
540
838
 
541
839
  program
542
840
  .command('browser')
543
- .description('Launch the AgentForge browser')
544
- .option('--url <url>', 'URL to open', 'https://agentforgeai-production.up.railway.app/dashboard')
841
+ .description('Manage the AgentForge agent browser')
842
+ .option('--url <url>', 'URL to open on launch', 'https://agentforgeai-production.up.railway.app/dashboard')
843
+ .option('--reseed', 'Re-copy Chrome sessions into agent browser to refresh expired logins (Chrome only)')
844
+ .option('--sync', 'Live-sync session cookies from Chrome to agent browser (no restart)')
545
845
  .action((options) => {
846
+ const browser = findBrowser();
847
+ if (!browser) {
848
+ console.error('❌ No browser found. Install Chrome: https://www.google.com/chrome');
849
+ process.exit(1);
850
+ }
851
+
852
+ if (options.sync) {
853
+ const n = syncSessionCookies();
854
+ if (n >= 0) console.log(`✅ Synced ${n} session cookie(s) from Chrome to agent browser.`);
855
+ else console.log('⚠️ Could not sync cookies — is Chrome installed?');
856
+ return;
857
+ }
858
+
859
+ if (options.reseed) {
860
+ const chrome = findGoogleChrome();
861
+ if (!chrome) {
862
+ console.error('❌ --reseed requires Google Chrome.');
863
+ console.error(' Without Chrome, log in manually in the agent browser window.');
864
+ process.exit(1);
865
+ }
866
+ if (isBrowserRunning()) {
867
+ console.log('Stopping agent browser to reseed...');
868
+ try { execSync(`launchctl unload "${LAUNCH_AGENT_PLIST}" 2>/dev/null`); } catch {}
869
+ execSync('sleep 2');
870
+ }
871
+ const result = seedBrowserProfile(true);
872
+ if (result === 'seeded') {
873
+ console.log('✅ Sessions refreshed. Log into Google in the browser window — everything else is ready.');
874
+ } else {
875
+ console.log('⚠️ Could not read Chrome profile.');
876
+ }
877
+ installLaunchAgent(browser);
878
+ console.log('✅ Agent browser restarted — log into Google in the browser window.');
879
+ return;
880
+ }
881
+
546
882
  if (isBrowserRunning()) {
547
- console.log('✅ AgentForge browser already running');
883
+ console.log('✅ AgentForge Browser already running (port 9223)');
548
884
  return;
549
885
  }
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);
886
+
887
+ installAgentBrowserApp();
888
+ const result = seedBrowserProfile();
889
+ installLaunchAgent(browser);
890
+ if (result === 'seeded') {
891
+ console.log('✅ Agent browser launched — log into Google in the browser window (one time only).');
892
+ } else {
893
+ console.log('✅ Agent browser launched.');
555
894
  }
556
895
  });
557
896
 
897
+ program
898
+ .command('stop')
899
+ .description('Stop a running background supervisor (started with --detach)')
900
+ .action(() => {
901
+ stopSupervisor();
902
+ });
903
+
558
904
  program.parse();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hamp10/agentforge",
3
- "version": "0.1.0",
3
+ "version": "0.2.0",
4
4
  "description": "AgentForge worker — connect your machine to agentforge.ai",
5
5
  "type": "module",
6
6
  "bin": {