@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 +390 -44
- package/package.json +1 -1
- package/src/OpenClawCLI.js +204 -46
- package/src/resolveOpenclaw.js +105 -0
- package/src/selfUpdate.js +66 -0
- package/src/supervisor.js +128 -0
- package/src/worker.js +265 -227
- package/templates/agent/AGENTFORGE.md +148 -56
- package/templates/agent/AGENTS.md +0 -212
- package/templates/agent/SOUL.md +0 -36
- package/templates/agent/TOOLS.md +0 -40
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
|
-
//
|
|
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
|
-
|
|
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
|
-
|
|
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(
|
|
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
|
-
|
|
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(
|
|
506
|
-
if (!fs.existsSync(srcDir)) return;
|
|
507
|
-
|
|
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
|
-
|
|
510
|
-
|
|
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
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
try {
|
|
520
|
-
|
|
521
|
-
|
|
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
|
|
526
|
-
const
|
|
527
|
-
if (
|
|
528
|
-
const
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
'
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
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('
|
|
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
|
|
883
|
+
console.log('✅ AgentForge Browser already running (port 9223)');
|
|
548
884
|
return;
|
|
549
885
|
}
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
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();
|