@hamp10/agentforge 0.2.21 → 0.2.23
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 +909 -115
- package/package.json +2 -1
- package/scripts/check-task-semantics.js +911 -0
- package/scripts/postinstall.js +20 -5
- package/src/OllamaAgent.js +1178 -246
- package/src/OpenClawCLI.js +5897 -748
- package/src/browser.js +392 -0
- package/src/default-task-guides.js +95 -0
- package/src/resolveOpenclaw.js +38 -7
- package/src/selfUpdate.js +31 -3
- package/src/supervisor.js +88 -20
- package/src/taskSemantics.js +141 -0
- package/src/worker.js +4257 -230
- package/templates/agent/AGENTFORGE.md +151 -53
- package/templates/hooks/agentforge-platform/handler.js +322 -0
- package/src/HampAgentCLI.js +0 -125
- package/src/hampagent/browser.js +0 -321
- package/src/hampagent/runner.js +0 -277
- package/src/hampagent/sessions.js +0 -62
- package/src/hampagent/tools.js +0 -298
package/bin/agentforge.js
CHANGED
|
@@ -4,8 +4,10 @@ import { Command } from 'commander';
|
|
|
4
4
|
import fs from 'fs';
|
|
5
5
|
import path from 'path';
|
|
6
6
|
import os from 'os';
|
|
7
|
+
import crypto from 'crypto';
|
|
7
8
|
import open from 'open';
|
|
8
|
-
import { execSync, spawn } from 'child_process';
|
|
9
|
+
import { execSync, spawn, spawnSync } from 'child_process';
|
|
10
|
+
import http from 'http';
|
|
9
11
|
import { AgentForgeWorker } from '../src/worker.js';
|
|
10
12
|
import { OpenClawCLI } from '../src/OpenClawCLI.js';
|
|
11
13
|
import { checkAndUpdate } from '../src/selfUpdate.js';
|
|
@@ -13,6 +15,8 @@ import { runSupervisor, detachSupervisor, stopSupervisor } from '../src/supervis
|
|
|
13
15
|
|
|
14
16
|
const CONFIG_DIR = path.join(os.homedir(), '.agentforge');
|
|
15
17
|
const CONFIG_FILE = path.join(CONFIG_DIR, 'config.json');
|
|
18
|
+
const SUPERVISOR_PID_FILE = path.join(CONFIG_DIR, 'supervisor.pid');
|
|
19
|
+
const WORKER_PID_FILE = path.join(CONFIG_DIR, 'worker.pid');
|
|
16
20
|
|
|
17
21
|
process.on('unhandledRejection', (reason) => {
|
|
18
22
|
console.error('❌ Unhandled promise rejection:', reason instanceof Error ? reason.message : reason);
|
|
@@ -51,6 +55,206 @@ function saveConfig(config) {
|
|
|
51
55
|
fs.writeFileSync(CONFIG_FILE, JSON.stringify(config, null, 2));
|
|
52
56
|
}
|
|
53
57
|
|
|
58
|
+
function readPidFile(file) {
|
|
59
|
+
try {
|
|
60
|
+
if (!fs.existsSync(file)) return null;
|
|
61
|
+
const pid = Number.parseInt(fs.readFileSync(file, 'utf8').trim(), 10);
|
|
62
|
+
return Number.isFinite(pid) && pid > 0 ? pid : null;
|
|
63
|
+
} catch {
|
|
64
|
+
return null;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function isRunningPid(pid) {
|
|
69
|
+
if (!pid) return false;
|
|
70
|
+
try {
|
|
71
|
+
process.kill(pid, 0);
|
|
72
|
+
return true;
|
|
73
|
+
} catch {
|
|
74
|
+
return false;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function getRuntimeStatus() {
|
|
79
|
+
const supervisorPid = readPidFile(SUPERVISOR_PID_FILE);
|
|
80
|
+
const workerPid = readPidFile(WORKER_PID_FILE);
|
|
81
|
+
return {
|
|
82
|
+
supervisorPid,
|
|
83
|
+
workerPid,
|
|
84
|
+
supervisorRunning: isRunningPid(supervisorPid),
|
|
85
|
+
workerRunning: isRunningPid(workerPid),
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function runQuiet(command, args = []) {
|
|
90
|
+
return spawnSync(command, args, {
|
|
91
|
+
encoding: 'utf8',
|
|
92
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function commandExists(command) {
|
|
97
|
+
return runQuiet('which', [command]).status === 0;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function pathContainsDir(dir) {
|
|
101
|
+
const normalized = path.resolve(dir).replace(/\/$/, '');
|
|
102
|
+
return (process.env.PATH || '')
|
|
103
|
+
.split(path.delimiter)
|
|
104
|
+
.map(entry => {
|
|
105
|
+
try { return path.resolve(entry).replace(/\/$/, ''); } catch { return entry.replace(/\/$/, ''); }
|
|
106
|
+
})
|
|
107
|
+
.includes(normalized);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function getShellProfilePath() {
|
|
111
|
+
const shell = process.env.SHELL || '';
|
|
112
|
+
if (shell.includes('zsh')) return path.join(os.homedir(), '.zshrc');
|
|
113
|
+
if (shell.includes('bash')) {
|
|
114
|
+
const bashrc = path.join(os.homedir(), '.bashrc');
|
|
115
|
+
return fs.existsSync(bashrc) ? bashrc : path.join(os.homedir(), '.bash_profile');
|
|
116
|
+
}
|
|
117
|
+
return path.join(os.homedir(), '.profile');
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function addPathToShellProfile(binDir) {
|
|
121
|
+
if (!pathContainsDir(binDir)) {
|
|
122
|
+
process.env.PATH = `${binDir}${path.delimiter}${process.env.PATH || ''}`;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
const profileFile = getShellProfilePath();
|
|
126
|
+
const exportLine = `export PATH="${binDir}:$PATH" # Added by AgentForge`;
|
|
127
|
+
|
|
128
|
+
try {
|
|
129
|
+
if (fs.existsSync(profileFile)) {
|
|
130
|
+
const existing = fs.readFileSync(profileFile, 'utf8');
|
|
131
|
+
if (existing.includes(binDir)) return { profileFile, changed: false };
|
|
132
|
+
}
|
|
133
|
+
fs.appendFileSync(profileFile, `\n${exportLine}\n`);
|
|
134
|
+
return { profileFile, changed: true };
|
|
135
|
+
} catch (error) {
|
|
136
|
+
return { profileFile, changed: false, error };
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
function npmGlobalBinForPrefix(prefix) {
|
|
141
|
+
return process.platform === 'win32' ? prefix : path.join(prefix, 'bin');
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function npmGlobalRoot() {
|
|
145
|
+
const result = runQuiet('npm', ['root', '-g']);
|
|
146
|
+
return result.status === 0 ? result.stdout.trim() : null;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
function npmPrefix() {
|
|
150
|
+
const result = runQuiet('npm', ['config', 'get', 'prefix']);
|
|
151
|
+
return result.status === 0 ? result.stdout.trim() : null;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
function canWriteNpmRoot(rootDir) {
|
|
155
|
+
if (!rootDir) return false;
|
|
156
|
+
const testDir = path.join(rootDir, `.agentforge-write-test-${process.pid}`);
|
|
157
|
+
try {
|
|
158
|
+
fs.mkdirSync(testDir, { recursive: true });
|
|
159
|
+
fs.rmSync(testDir, { recursive: true, force: true });
|
|
160
|
+
return true;
|
|
161
|
+
} catch {
|
|
162
|
+
try { fs.rmSync(testDir, { recursive: true, force: true }); } catch {}
|
|
163
|
+
return false;
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
function ensureUserOwnedNpmGlobal() {
|
|
168
|
+
if (!commandExists('npm')) {
|
|
169
|
+
return { ok: false, message: 'npm is not installed. Install Node.js LTS from https://nodejs.org.' };
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
let prefix = npmPrefix();
|
|
173
|
+
let root = npmGlobalRoot();
|
|
174
|
+
|
|
175
|
+
if (!canWriteNpmRoot(root)) {
|
|
176
|
+
const userPrefix = path.join(os.homedir(), '.npm-global');
|
|
177
|
+
fs.mkdirSync(path.join(userPrefix, 'bin'), { recursive: true });
|
|
178
|
+
fs.mkdirSync(path.join(userPrefix, 'lib', 'node_modules'), { recursive: true });
|
|
179
|
+
|
|
180
|
+
const setPrefix = spawnSync('npm', ['config', 'set', 'prefix', userPrefix], {
|
|
181
|
+
encoding: 'utf8',
|
|
182
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
183
|
+
});
|
|
184
|
+
if (setPrefix.status !== 0) {
|
|
185
|
+
return {
|
|
186
|
+
ok: false,
|
|
187
|
+
message: `Could not configure npm to use ${userPrefix}: ${setPrefix.stderr || setPrefix.stdout}`.trim(),
|
|
188
|
+
};
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
prefix = userPrefix;
|
|
192
|
+
root = path.join(userPrefix, 'lib', 'node_modules');
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
const binDir = npmGlobalBinForPrefix(prefix);
|
|
196
|
+
fs.mkdirSync(binDir, { recursive: true });
|
|
197
|
+
const pathUpdate = addPathToShellProfile(binDir);
|
|
198
|
+
return { ok: true, prefix, root, binDir, pathUpdate };
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
function installOpenClawRuntime() {
|
|
202
|
+
const npmReady = ensureUserOwnedNpmGlobal();
|
|
203
|
+
if (!npmReady.ok) return npmReady;
|
|
204
|
+
|
|
205
|
+
const result = spawnSync('npm', ['install', '-g', 'openclaw', '--quiet'], {
|
|
206
|
+
encoding: 'utf8',
|
|
207
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
208
|
+
env: { ...process.env, PATH: `${npmReady.binDir}${path.delimiter}${process.env.PATH || ''}` },
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
if (result.status !== 0) {
|
|
212
|
+
return {
|
|
213
|
+
ok: false,
|
|
214
|
+
message: (result.stderr || result.stdout || 'npm install -g openclaw failed').trim(),
|
|
215
|
+
};
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
process.env.PATH = `${npmReady.binDir}${path.delimiter}${process.env.PATH || ''}`;
|
|
219
|
+
return { ok: true, ...npmReady };
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
function printMissingRuntimeHelp() {
|
|
223
|
+
console.error('');
|
|
224
|
+
console.error('❌ Agent runtime not found on this Mac.');
|
|
225
|
+
console.error('');
|
|
226
|
+
console.error(' Your dashboard API key validates model access, but this machine still');
|
|
227
|
+
console.error(' needs a local agent runtime so agents can use the browser, shell, and files.');
|
|
228
|
+
console.error('');
|
|
229
|
+
console.error(' Fix:');
|
|
230
|
+
console.error(' curl -fsSL https://agentforgeai-production.up.railway.app/install.sh | bash');
|
|
231
|
+
console.error('');
|
|
232
|
+
console.error(' Or, if AgentForge is already installed:');
|
|
233
|
+
console.error(' agentforge setup');
|
|
234
|
+
console.error('');
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
function ensureRuntimeAvailableForStart(config) {
|
|
238
|
+
if (config.provider === 'local') return true;
|
|
239
|
+
if (OpenClawCLI.isAvailable()) return true;
|
|
240
|
+
|
|
241
|
+
console.log('');
|
|
242
|
+
console.log('OpenClaw was not found. Installing the local agent runtime...');
|
|
243
|
+
const installed = installOpenClawRuntime();
|
|
244
|
+
if (installed.ok && OpenClawCLI.isAvailable()) {
|
|
245
|
+
console.log('✅ OpenClaw installed');
|
|
246
|
+
console.log('');
|
|
247
|
+
return true;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
if (!installed.ok) {
|
|
251
|
+
console.error('');
|
|
252
|
+
console.error(`OpenClaw install failed: ${installed.message}`);
|
|
253
|
+
}
|
|
254
|
+
printMissingRuntimeHelp();
|
|
255
|
+
return false;
|
|
256
|
+
}
|
|
257
|
+
|
|
54
258
|
const _pkg = JSON.parse(fs.readFileSync(new URL('../package.json', import.meta.url), 'utf8'));
|
|
55
259
|
|
|
56
260
|
program
|
|
@@ -203,18 +407,7 @@ program
|
|
|
203
407
|
process.exit(1);
|
|
204
408
|
}
|
|
205
409
|
|
|
206
|
-
|
|
207
|
-
if (config.provider !== 'local' && !OpenClawCLI.isAvailable()) {
|
|
208
|
-
console.error('');
|
|
209
|
-
console.error('❌ No AI backend configured.');
|
|
210
|
-
console.error('');
|
|
211
|
-
console.error(' AgentForge needs an AI model to run agents.');
|
|
212
|
-
console.error(' Configure a local model server (Ollama, LM Studio, Jan, etc.):');
|
|
213
|
-
console.error('');
|
|
214
|
-
console.error(' agentforge local --url http://localhost:11434 --model llama3.1:8b');
|
|
215
|
-
console.error('');
|
|
216
|
-
console.error(' Then run: agentforge start');
|
|
217
|
-
console.error('');
|
|
410
|
+
if (!ensureRuntimeAvailableForStart(config)) {
|
|
218
411
|
process.exit(1);
|
|
219
412
|
}
|
|
220
413
|
|
|
@@ -254,8 +447,18 @@ program
|
|
|
254
447
|
const MAX_LOG_BYTES = 5 * 1024 * 1024;
|
|
255
448
|
let logStream = fs.createWriteStream(logFile, { flags: 'a' });
|
|
256
449
|
logStream.write(`\n\n===== Worker started ${new Date().toISOString()} =====\n\n`);
|
|
450
|
+
const serializeLogArg = (arg) => {
|
|
451
|
+
if (typeof arg === 'string') return arg;
|
|
452
|
+
if (arg instanceof Error) return arg.stack || `${arg.name || 'Error'}: ${arg.message || ''}`.trim();
|
|
453
|
+
try {
|
|
454
|
+
const json = JSON.stringify(arg);
|
|
455
|
+
return json === undefined ? String(arg) : json;
|
|
456
|
+
} catch {
|
|
457
|
+
return String(arg);
|
|
458
|
+
}
|
|
459
|
+
};
|
|
257
460
|
const writeLog = (level, args) => {
|
|
258
|
-
const line = `[${new Date().toISOString()}] [${level}] ${args.map(
|
|
461
|
+
const line = `[${new Date().toISOString()}] [${level}] ${args.map(serializeLogArg).join(' ')}\n`;
|
|
259
462
|
try {
|
|
260
463
|
logStream.write(line);
|
|
261
464
|
if (fs.statSync(logFile).size > MAX_LOG_BYTES) {
|
|
@@ -277,16 +480,15 @@ program
|
|
|
277
480
|
|
|
278
481
|
// Graceful shutdown
|
|
279
482
|
process.on('SIGINT', () => { console.log('\n[SIGINT received — stopping]'); worker.shutdown(0); }); // Ctrl+C: clean stop
|
|
280
|
-
process.on('SIGTERM', () => { console.log('\n[SIGTERM received —
|
|
483
|
+
process.on('SIGTERM', () => { console.log('\n[SIGTERM received — stopping]'); worker.shutdown(0); }); // agentforge stop / supervisor shutdown
|
|
281
484
|
process.on('SIGHUP', () => { console.log('\n[SIGHUP received — restarting]'); worker.shutdown(1); }); // terminal close: supervisor restarts
|
|
282
485
|
|
|
283
486
|
try {
|
|
284
487
|
await worker.initialize();
|
|
285
488
|
await worker.connect();
|
|
286
489
|
|
|
287
|
-
// Auto-start AgentForge Browser
|
|
288
|
-
// On macOS:
|
|
289
|
-
// On other platforms: skip silently — agents fall back to web_fetch / screenshot_and_describe.
|
|
490
|
+
// Auto-start AgentForge Browser on demand — no LaunchAgent, worker manages lifecycle.
|
|
491
|
+
// On macOS: seed sessions and launch directly. On other platforms: skip silently.
|
|
290
492
|
if (process.platform === 'darwin') {
|
|
291
493
|
try {
|
|
292
494
|
if (isBrowserRunning()) {
|
|
@@ -296,7 +498,7 @@ program
|
|
|
296
498
|
if (browserBin) {
|
|
297
499
|
installAgentBrowserApp();
|
|
298
500
|
seedBrowserProfile();
|
|
299
|
-
|
|
501
|
+
launchBrowserDirect(browserBin);
|
|
300
502
|
console.log('🌐 AgentForge Browser started (port 9223)');
|
|
301
503
|
} else {
|
|
302
504
|
console.warn('⚠️ No browser found — install Chrome for full browser tool support');
|
|
@@ -307,6 +509,62 @@ program
|
|
|
307
509
|
}
|
|
308
510
|
}
|
|
309
511
|
|
|
512
|
+
// Session vault: pull cookies from server so sessions logged in on other
|
|
513
|
+
// machines are immediately available here. Then inject into browser so
|
|
514
|
+
// agents can use them immediately. Finally push updated local sessions.
|
|
515
|
+
try {
|
|
516
|
+
await pullSessionsFromVault(config.token, baseUrl);
|
|
517
|
+
// Await injection so the browser has the cookies before we push back
|
|
518
|
+
if (fs.existsSync(SESSIONS_FILE)) await injectSavedSessions().catch(() => {});
|
|
519
|
+
// Push local browser sessions to vault (captures any newer cookies in the live browser)
|
|
520
|
+
await pushSessionsToVault(config.token, baseUrl);
|
|
521
|
+
} catch {}
|
|
522
|
+
|
|
523
|
+
// Machine login: ensure the dashboard browser is authenticated using the worker token.
|
|
524
|
+
// This runs immediately on startup and every 5 minutes, so session expiry or server
|
|
525
|
+
// restarts with a new SESSION_SECRET are healed automatically — no manual GitHub login needed.
|
|
526
|
+
await ensureDashboardAuth(config.token, baseUrl).catch(() => {});
|
|
527
|
+
setInterval(() => {
|
|
528
|
+
ensureDashboardAuth(config.token, baseUrl).catch(() => {});
|
|
529
|
+
}, 5 * 60 * 1000);
|
|
530
|
+
|
|
531
|
+
// Sync sessions every 5 minutes: push local → vault (macOS only), so other
|
|
532
|
+
// machines always have fresh cookies when they pull on their next startup.
|
|
533
|
+
setInterval(() => {
|
|
534
|
+
pushSessionsToVault(config.token, baseUrl).catch(() => {});
|
|
535
|
+
}, 5 * 60 * 1000);
|
|
536
|
+
|
|
537
|
+
// Session injection watcher: when Chrome appears (started on-demand by openclaw
|
|
538
|
+
// for a task), inject vault sessions into it immediately so sites stay logged in.
|
|
539
|
+
// Does NOT restart Chrome — only reacts when Chrome comes up on its own.
|
|
540
|
+
if (process.platform === 'darwin') {
|
|
541
|
+
let _browserWasRunning = isBrowserRunning();
|
|
542
|
+
let _injecting = false;
|
|
543
|
+
setInterval(async () => {
|
|
544
|
+
try {
|
|
545
|
+
const running = isBrowserRunning();
|
|
546
|
+
if (running && !_browserWasRunning && !_injecting) {
|
|
547
|
+
// Chrome just appeared — inject sessions (set flags FIRST to prevent concurrent runs)
|
|
548
|
+
_browserWasRunning = running;
|
|
549
|
+
_injecting = true;
|
|
550
|
+
try {
|
|
551
|
+
console.log('🌐 Browser appeared — injecting sessions...');
|
|
552
|
+
await new Promise(r => setTimeout(r, 2000)); // let CDP stabilize
|
|
553
|
+
await pullSessionsFromVault(config.token, baseUrl).catch(() => {});
|
|
554
|
+
if (fs.existsSync(SESSIONS_FILE)) await injectSavedSessions().catch(() => {});
|
|
555
|
+
await pushSessionsToVault(config.token, baseUrl).catch(() => {});
|
|
556
|
+
console.log('✅ Sessions restored into new browser');
|
|
557
|
+
} finally {
|
|
558
|
+
_injecting = false;
|
|
559
|
+
}
|
|
560
|
+
} else {
|
|
561
|
+
_browserWasRunning = running;
|
|
562
|
+
}
|
|
563
|
+
} catch {}
|
|
564
|
+
}, 5 * 1000); // check every 5 seconds
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
|
|
310
568
|
console.log('');
|
|
311
569
|
console.log('✅ Worker running');
|
|
312
570
|
console.log(' Press Ctrl+C to stop');
|
|
@@ -334,16 +592,32 @@ program
|
|
|
334
592
|
} else if (OpenClawCLI.isAvailable()) {
|
|
335
593
|
console.log(`AI Backend: ✅ openclaw`);
|
|
336
594
|
} else {
|
|
337
|
-
console.log(`AI Backend: ❌
|
|
595
|
+
console.log(`AI Backend: ❌ OpenClaw not found`);
|
|
338
596
|
}
|
|
339
597
|
|
|
340
598
|
console.log(`Config file: ${CONFIG_FILE}`);
|
|
341
599
|
console.log('');
|
|
342
600
|
|
|
601
|
+
const runtimeStatus = getRuntimeStatus();
|
|
602
|
+
if (runtimeStatus.supervisorRunning || runtimeStatus.workerRunning) {
|
|
603
|
+
const details = [
|
|
604
|
+
runtimeStatus.supervisorRunning ? `supervisor PID ${runtimeStatus.supervisorPid}` : null,
|
|
605
|
+
runtimeStatus.workerRunning ? `worker PID ${runtimeStatus.workerPid}` : null,
|
|
606
|
+
].filter(Boolean).join(', ');
|
|
607
|
+
console.log(`Worker: ✅ Running${details ? ` (${details})` : ''}`);
|
|
608
|
+
} else if (runtimeStatus.supervisorPid || runtimeStatus.workerPid) {
|
|
609
|
+
console.log('Worker: ⚠️ PID file exists, but the process is not running');
|
|
610
|
+
} else {
|
|
611
|
+
console.log('Worker: ⚠️ Not running');
|
|
612
|
+
}
|
|
613
|
+
console.log('');
|
|
614
|
+
|
|
343
615
|
if (!config.token) {
|
|
344
616
|
console.log('Run: agentforge login');
|
|
345
617
|
} else if (config.provider !== 'local' && !OpenClawCLI.isAvailable()) {
|
|
346
|
-
console.log('
|
|
618
|
+
console.log('Run: agentforge setup');
|
|
619
|
+
} else if (runtimeStatus.supervisorRunning || runtimeStatus.workerRunning) {
|
|
620
|
+
console.log('Connected worker process is running.');
|
|
347
621
|
} else {
|
|
348
622
|
console.log('Ready to connect! Run: agentforge start');
|
|
349
623
|
}
|
|
@@ -549,29 +823,30 @@ program
|
|
|
549
823
|
allGood = false;
|
|
550
824
|
}
|
|
551
825
|
} catch {
|
|
552
|
-
console.log('❌
|
|
553
|
-
console.log('
|
|
826
|
+
console.log('❌ OpenClaw runtime not found');
|
|
827
|
+
console.log(' Your dashboard API keys can be valid, but this Mac still needs OpenClaw');
|
|
828
|
+
console.log(' or a configured local model server to execute agents.');
|
|
829
|
+
console.log(' Fix: agentforge setup');
|
|
554
830
|
allGood = false;
|
|
555
831
|
}
|
|
556
832
|
}
|
|
557
833
|
|
|
558
834
|
// 4. Worker running
|
|
559
|
-
const
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
allGood = false;
|
|
571
|
-
}
|
|
835
|
+
const runtimeStatus = getRuntimeStatus();
|
|
836
|
+
if (runtimeStatus.supervisorRunning || runtimeStatus.workerRunning) {
|
|
837
|
+
const details = [
|
|
838
|
+
runtimeStatus.supervisorRunning ? `supervisor PID ${runtimeStatus.supervisorPid}` : null,
|
|
839
|
+
runtimeStatus.workerRunning ? `worker PID ${runtimeStatus.workerPid}` : null,
|
|
840
|
+
].filter(Boolean).join(', ');
|
|
841
|
+
console.log(`✅ Worker running (${details})`);
|
|
842
|
+
} else if (runtimeStatus.supervisorPid || runtimeStatus.workerPid) {
|
|
843
|
+
console.log('⚠️ Worker PID file exists but process is not running');
|
|
844
|
+
console.log(' Fix: agentforge start');
|
|
845
|
+
allGood = false;
|
|
572
846
|
} else {
|
|
573
847
|
console.log('⚠️ Worker not running');
|
|
574
848
|
console.log(' Fix: agentforge start');
|
|
849
|
+
allGood = false;
|
|
575
850
|
}
|
|
576
851
|
|
|
577
852
|
console.log('');
|
|
@@ -586,47 +861,90 @@ program
|
|
|
586
861
|
program
|
|
587
862
|
.command('setup')
|
|
588
863
|
.description('Interactive setup wizard — gets AgentForge running in minutes')
|
|
864
|
+
.option('--url <url>', 'Custom AgentForge URL')
|
|
865
|
+
.option('--token <token>', 'Pre-issued auth token (skips OAuth browser flow)')
|
|
866
|
+
.option('--no-start', 'Do not start the worker after setup')
|
|
589
867
|
.option('--tailscale-key <key>', 'Tailscale auth key (from tailscale.com/admin/settings/keys)')
|
|
590
868
|
.action(async (options) => {
|
|
591
|
-
const { execSync, spawnSync } = await import('child_process');
|
|
592
869
|
const readline = await import('readline');
|
|
593
870
|
|
|
594
|
-
|
|
595
|
-
const ask = (q) =>
|
|
871
|
+
let rl = null;
|
|
872
|
+
const ask = (q) => {
|
|
873
|
+
if (!rl) rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
874
|
+
return new Promise(resolve => rl.question(q, resolve));
|
|
875
|
+
};
|
|
876
|
+
const closeReadline = () => {
|
|
877
|
+
if (rl) {
|
|
878
|
+
rl.close();
|
|
879
|
+
rl = null;
|
|
880
|
+
}
|
|
881
|
+
};
|
|
882
|
+
|
|
883
|
+
const baseUrl = options.url ||
|
|
884
|
+
process.env.AGENTFORGE_URL ||
|
|
885
|
+
process.env.AGENTFORGE_SERVER ||
|
|
886
|
+
loadConfig().url ||
|
|
887
|
+
'https://agentforgeai-production.up.railway.app';
|
|
888
|
+
const preToken = options.token || process.env.AGENTFORGE_TOKEN;
|
|
596
889
|
|
|
597
890
|
console.log('');
|
|
598
891
|
console.log('🚀 AgentForge Setup');
|
|
599
892
|
console.log('================================');
|
|
600
|
-
console.log('Getting
|
|
893
|
+
console.log('Getting this machine ready to run AI agents.\n');
|
|
894
|
+
|
|
895
|
+
// ── Step 1: System checks ───────────────────────────────────────────────
|
|
896
|
+
console.log('Step 1/4: System checks\n');
|
|
897
|
+
|
|
898
|
+
const nodeMajor = Number.parseInt(process.versions.node.split('.')[0], 10);
|
|
899
|
+
if (!Number.isFinite(nodeMajor) || nodeMajor < 18) {
|
|
900
|
+
console.error(`❌ Node.js 18+ is required. Found ${process.version}.`);
|
|
901
|
+
console.error(' Install the Node.js LTS version from https://nodejs.org, then run setup again.');
|
|
902
|
+
process.exit(1);
|
|
903
|
+
}
|
|
904
|
+
console.log(`✅ Node.js ${process.version}`);
|
|
905
|
+
|
|
906
|
+
const npmReady = ensureUserOwnedNpmGlobal();
|
|
907
|
+
if (!npmReady.ok) {
|
|
908
|
+
console.error(`❌ ${npmReady.message}`);
|
|
909
|
+
process.exit(1);
|
|
910
|
+
}
|
|
911
|
+
console.log(`✅ npm global installs use ${npmReady.prefix}`);
|
|
912
|
+
if (npmReady.pathUpdate?.changed) {
|
|
913
|
+
console.log(`✅ Added ${npmReady.binDir} to PATH in ${npmReady.pathUpdate.profileFile}`);
|
|
914
|
+
} else if (npmReady.pathUpdate?.error) {
|
|
915
|
+
console.log(`⚠️ Could not update ${npmReady.pathUpdate.profileFile}; current terminal PATH was updated.`);
|
|
916
|
+
}
|
|
917
|
+
console.log('');
|
|
601
918
|
|
|
602
|
-
// ── Step
|
|
919
|
+
// ── Step 2: Authentication ──────────────────────────────────────────────
|
|
603
920
|
const config = loadConfig();
|
|
604
|
-
|
|
605
|
-
|
|
921
|
+
const alreadyLoggedIn = config.token && config.url === baseUrl && !preToken;
|
|
922
|
+
if (alreadyLoggedIn) {
|
|
923
|
+
console.log('✅ Step 2/4: Already logged in\n');
|
|
606
924
|
} else {
|
|
607
|
-
console.log('Step
|
|
608
|
-
console.log('A browser window will open
|
|
609
|
-
rl.close();
|
|
925
|
+
console.log('Step 2/4: Log in to AgentForge\n');
|
|
926
|
+
if (!preToken) console.log('A browser window will open. Log in there and come back here.\n');
|
|
610
927
|
|
|
611
928
|
// Spawn login as a child process with inherited stdio so the interactive
|
|
612
929
|
// OAuth flow works (readline on stdin, browser opens, polling, etc.)
|
|
613
|
-
const
|
|
614
|
-
|
|
930
|
+
const loginArgs = [process.argv[1], 'login', '--url', baseUrl];
|
|
931
|
+
if (preToken) loginArgs.push('--token', preToken);
|
|
932
|
+
const loginResult = spawnSync(process.execPath, loginArgs, {
|
|
933
|
+
stdio: 'inherit',
|
|
934
|
+
env: { ...process.env, AGENTFORGE_URL: baseUrl },
|
|
935
|
+
});
|
|
615
936
|
if (loginResult.status !== 0) {
|
|
616
937
|
console.error('\n❌ Login failed — run: agentforge login');
|
|
617
938
|
process.exit(1);
|
|
618
939
|
}
|
|
619
|
-
|
|
620
|
-
// Re-open readline for the rest of setup
|
|
621
|
-
const rl2 = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
622
|
-
Object.assign(rl, rl2); // replace for remaining ask() calls (won't be used further)
|
|
623
940
|
console.log('');
|
|
624
941
|
}
|
|
625
942
|
|
|
626
|
-
// ── Step
|
|
627
|
-
console.log('Step
|
|
943
|
+
// ── Step 3: AI Backend ──────────────────────────────────────────────────
|
|
944
|
+
console.log('Step 3/4: AI backend\n');
|
|
628
945
|
|
|
629
946
|
const freshConfig = loadConfig();
|
|
947
|
+
let backendReady = false;
|
|
630
948
|
|
|
631
949
|
if (freshConfig.provider === 'local') {
|
|
632
950
|
const localUrl = freshConfig.localUrl || 'http://localhost:11434';
|
|
@@ -640,12 +958,27 @@ program
|
|
|
640
958
|
console.log(` ⚠️ Model "${freshConfig.localModel}" not found. Available: ${models.slice(0, 5).join(', ')}`);
|
|
641
959
|
}
|
|
642
960
|
console.log('');
|
|
961
|
+
backendReady = true;
|
|
643
962
|
}
|
|
644
963
|
} catch {
|
|
645
|
-
|
|
964
|
+
if (OpenClawCLI.isAvailable()) {
|
|
965
|
+
const openclawConfig = loadConfig();
|
|
966
|
+
delete openclawConfig.provider;
|
|
967
|
+
delete openclawConfig.localUrl;
|
|
968
|
+
delete openclawConfig.localModel;
|
|
969
|
+
saveConfig(openclawConfig);
|
|
970
|
+
console.log(`⚠️ Configured local server at ${localUrl} is not reachable.`);
|
|
971
|
+
console.log('✅ openclaw is available, so setup will use openclaw instead.\n');
|
|
972
|
+
backendReady = true;
|
|
973
|
+
} else {
|
|
974
|
+
console.log(`❌ Configured local server at ${localUrl} is not reachable.`);
|
|
975
|
+
console.log(' Start that model server, or install OpenClaw/Ollama and run setup again.\n');
|
|
976
|
+
process.exit(1);
|
|
977
|
+
}
|
|
646
978
|
}
|
|
647
979
|
} else if (OpenClawCLI.isAvailable()) {
|
|
648
980
|
console.log('✅ openclaw detected and ready\n');
|
|
981
|
+
backendReady = true;
|
|
649
982
|
} else {
|
|
650
983
|
// Probe all common local model servers
|
|
651
984
|
const probes = [
|
|
@@ -671,51 +1004,72 @@ program
|
|
|
671
1004
|
console.log('');
|
|
672
1005
|
|
|
673
1006
|
if (found.length === 0) {
|
|
674
|
-
console.log('');
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
1007
|
+
console.log('\nOpenClaw was not found. Installing the local agent runtime...');
|
|
1008
|
+
const installed = installOpenClawRuntime();
|
|
1009
|
+
if (installed.ok && OpenClawCLI.isAvailable()) {
|
|
1010
|
+
console.log('✅ OpenClaw installed and ready\n');
|
|
1011
|
+
backendReady = true;
|
|
1012
|
+
} else {
|
|
1013
|
+
if (!installed.ok) {
|
|
1014
|
+
console.log(`❌ OpenClaw install failed: ${installed.message}`);
|
|
1015
|
+
}
|
|
1016
|
+
console.log('');
|
|
1017
|
+
console.log('AgentForge needs OpenClaw or a local OpenAI-compatible model server.');
|
|
1018
|
+
console.log('Your dashboard API key validates model access; it does not install');
|
|
1019
|
+
console.log('the local runtime that runs browser, shell, and file tools on this Mac.');
|
|
1020
|
+
console.log('');
|
|
1021
|
+
console.log('Run this again after OpenClaw is installed:');
|
|
1022
|
+
console.log(' agentforge setup');
|
|
1023
|
+
console.log('');
|
|
1024
|
+
console.log('Local model option: install Ollama from https://ollama.ai, then run:');
|
|
1025
|
+
console.log(' ollama pull llama3.1:8b');
|
|
1026
|
+
console.log(' agentforge setup');
|
|
1027
|
+
closeReadline();
|
|
1028
|
+
process.exit(0);
|
|
1029
|
+
}
|
|
684
1030
|
}
|
|
685
1031
|
|
|
686
1032
|
// Let user pick if multiple found, or auto-select if only one
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
1033
|
+
if (!backendReady) {
|
|
1034
|
+
let chosen = found[0];
|
|
1035
|
+
if (found.length > 1) {
|
|
1036
|
+
console.log('\nFound these model servers:\n');
|
|
1037
|
+
found.forEach((b, i) => {
|
|
1038
|
+
console.log(` ${i + 1}. ${b.name} (${b.url}) — ${b.models.length} model(s): ${b.models.slice(0, 3).join(', ')}`);
|
|
1039
|
+
});
|
|
1040
|
+
const ans = await ask(`\nWhich one to use? [1]: `);
|
|
1041
|
+
const idx = parseInt(ans.trim()) - 1;
|
|
1042
|
+
chosen = found[isNaN(idx) || idx < 0 || idx >= found.length ? 0 : idx];
|
|
1043
|
+
} else {
|
|
1044
|
+
console.log(`\nFound: ${chosen.name} (${chosen.url}) with ${chosen.models.length} model(s)`);
|
|
1045
|
+
}
|
|
699
1046
|
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
1047
|
+
// Pick model
|
|
1048
|
+
let model = chosen.models[0] || 'llama3.1:8b';
|
|
1049
|
+
if (chosen.models.length > 1) {
|
|
1050
|
+
console.log(`\nAvailable models: ${chosen.models.join(', ')}`);
|
|
1051
|
+
const ans = await ask(`Model to use [${model}]: `);
|
|
1052
|
+
if (ans.trim()) model = ans.trim();
|
|
1053
|
+
} else if (chosen.models.length === 1) {
|
|
1054
|
+
console.log(`Using model: ${model}`);
|
|
1055
|
+
}
|
|
709
1056
|
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
1057
|
+
const newConfig = loadConfig();
|
|
1058
|
+
newConfig.provider = 'local';
|
|
1059
|
+
newConfig.localUrl = chosen.url;
|
|
1060
|
+
newConfig.localModel = model;
|
|
1061
|
+
saveConfig(newConfig);
|
|
1062
|
+
console.log(`\n✅ Configured ${chosen.name} with model "${model}"\n`);
|
|
1063
|
+
backendReady = true;
|
|
1064
|
+
}
|
|
716
1065
|
}
|
|
717
1066
|
|
|
718
|
-
|
|
1067
|
+
closeReadline();
|
|
1068
|
+
|
|
1069
|
+
if (!backendReady) {
|
|
1070
|
+
console.error('❌ No usable AI backend is configured.');
|
|
1071
|
+
process.exit(1);
|
|
1072
|
+
}
|
|
719
1073
|
|
|
720
1074
|
// ── Optional: Tailscale (only shown if installed or key provided) ────────
|
|
721
1075
|
const tailscaleInstalled = spawnSync('which', ['tailscale'], { encoding: 'utf-8' }).status === 0;
|
|
@@ -731,22 +1085,49 @@ program
|
|
|
731
1085
|
console.log('✅ Tailscale installed (run "sudo tailscale up" to connect remotely)');
|
|
732
1086
|
}
|
|
733
1087
|
|
|
1088
|
+
// ── Step 4: Start worker ────────────────────────────────────────────────
|
|
1089
|
+
console.log('');
|
|
1090
|
+
console.log('Step 4/4: Start worker\n');
|
|
1091
|
+
|
|
1092
|
+
if (options.start === false) {
|
|
1093
|
+
console.log('Skipping worker start because --no-start was provided.\n');
|
|
1094
|
+
} else {
|
|
1095
|
+
const runtimeStatus = getRuntimeStatus();
|
|
1096
|
+
if (runtimeStatus.supervisorRunning || runtimeStatus.workerRunning) {
|
|
1097
|
+
console.log('✅ Worker is already running\n');
|
|
1098
|
+
} else {
|
|
1099
|
+
const startResult = spawnSync(process.execPath, [
|
|
1100
|
+
process.argv[1],
|
|
1101
|
+
'start',
|
|
1102
|
+
'--detach',
|
|
1103
|
+
'--url',
|
|
1104
|
+
baseUrl,
|
|
1105
|
+
], {
|
|
1106
|
+
stdio: 'inherit',
|
|
1107
|
+
env: { ...process.env, AGENTFORGE_URL: baseUrl },
|
|
1108
|
+
});
|
|
1109
|
+
|
|
1110
|
+
if (startResult.status !== 0) {
|
|
1111
|
+
console.error('\n❌ Worker did not start. Run: agentforge doctor');
|
|
1112
|
+
process.exit(startResult.status || 1);
|
|
1113
|
+
}
|
|
1114
|
+
console.log('');
|
|
1115
|
+
}
|
|
1116
|
+
}
|
|
1117
|
+
|
|
734
1118
|
console.log('');
|
|
735
1119
|
console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
|
|
736
1120
|
console.log('✅ Setup complete!');
|
|
737
1121
|
console.log('');
|
|
738
|
-
console.log('
|
|
739
|
-
console.log(
|
|
740
|
-
console.log(' agentforge start');
|
|
741
|
-
console.log('');
|
|
742
|
-
console.log(' Then open your dashboard:');
|
|
1122
|
+
console.log(' Dashboard:');
|
|
1123
|
+
console.log(` ${baseUrl}/dashboard`);
|
|
743
1124
|
console.log('');
|
|
744
|
-
console.log('
|
|
1125
|
+
console.log(' Health check:');
|
|
1126
|
+
console.log(' agentforge doctor');
|
|
745
1127
|
console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
|
|
746
1128
|
console.log('');
|
|
747
1129
|
|
|
748
|
-
|
|
749
|
-
try { execSync('open https://agentforgeai-production.up.railway.app/dashboard'); } catch {}
|
|
1130
|
+
try { await open(`${baseUrl}/dashboard`); } catch {}
|
|
750
1131
|
});
|
|
751
1132
|
|
|
752
1133
|
// ── Browser helpers ──────────────────────────────────────────────────────────
|
|
@@ -758,11 +1139,13 @@ program
|
|
|
758
1139
|
|
|
759
1140
|
const CDP_PORT = 9223;
|
|
760
1141
|
const BROWSER_PROFILE_DIR = path.join(CONFIG_DIR, 'browser');
|
|
1142
|
+
const SESSIONS_FILE = path.join(CONFIG_DIR, 'sessions.json');
|
|
761
1143
|
const LAUNCH_AGENT_LABEL = 'ai.agentforge.browser';
|
|
762
1144
|
const LAUNCH_AGENT_PLIST = path.join(
|
|
763
1145
|
os.homedir(), 'Library', 'LaunchAgents', `${LAUNCH_AGENT_LABEL}.plist`
|
|
764
1146
|
);
|
|
765
1147
|
const CHROME_BIN = '/Applications/Google Chrome.app/Contents/MacOS/Google Chrome';
|
|
1148
|
+
const CHROMIUM_BIN = '/Applications/Chromium.app/Contents/MacOS/Chromium';
|
|
766
1149
|
|
|
767
1150
|
// Returns Google Chrome path if installed — required for cookie seeding
|
|
768
1151
|
// (shared "Chrome Safe Storage" Keychain key makes copied cookies readable).
|
|
@@ -771,11 +1154,14 @@ function findGoogleChrome() {
|
|
|
771
1154
|
}
|
|
772
1155
|
|
|
773
1156
|
// Returns the best available browser for running the agent profile.
|
|
774
|
-
// Prefers
|
|
1157
|
+
// Prefers the browser that matches the user's profile (for Keychain key compatibility).
|
|
1158
|
+
// Falls back to Chrome → Chromium → others.
|
|
775
1159
|
function findBrowser() {
|
|
1160
|
+
// Always prefer Chrome — required for Keychain key compatibility with cookie seeding.
|
|
1161
|
+
// Chrome takes priority even if no Chrome profile exists yet.
|
|
776
1162
|
const candidates = [
|
|
777
1163
|
CHROME_BIN,
|
|
778
|
-
|
|
1164
|
+
CHROMIUM_BIN,
|
|
779
1165
|
'/Applications/Brave Browser.app/Contents/MacOS/Brave Browser',
|
|
780
1166
|
'/Applications/Microsoft Edge.app/Contents/MacOS/Microsoft Edge',
|
|
781
1167
|
];
|
|
@@ -785,6 +1171,22 @@ function findBrowser() {
|
|
|
785
1171
|
return null;
|
|
786
1172
|
}
|
|
787
1173
|
|
|
1174
|
+
// Finds the user's primary browser profile for cookie seeding.
|
|
1175
|
+
// Returns { browserBin, srcDir } where browserBin is the binary that encrypted
|
|
1176
|
+
// the cookies (the agent browser must use the same binary to share the Keychain key).
|
|
1177
|
+
// Priority: Chrome (if profile exists) → Chromium (if profile exists) → null.
|
|
1178
|
+
function findUserBrowserProfile() {
|
|
1179
|
+
const chromeSrcDir = path.join(os.homedir(), 'Library', 'Application Support', 'Google', 'Chrome', 'Default');
|
|
1180
|
+
if (fs.existsSync(CHROME_BIN) && fs.existsSync(chromeSrcDir)) {
|
|
1181
|
+
return { browserBin: CHROME_BIN, srcDir: chromeSrcDir };
|
|
1182
|
+
}
|
|
1183
|
+
const chromiumSrcDir = path.join(os.homedir(), 'Library', 'Application Support', 'Chromium', 'Default');
|
|
1184
|
+
if (fs.existsSync(CHROMIUM_BIN) && fs.existsSync(chromiumSrcDir)) {
|
|
1185
|
+
return { browserBin: CHROMIUM_BIN, srcDir: chromiumSrcDir };
|
|
1186
|
+
}
|
|
1187
|
+
return null;
|
|
1188
|
+
}
|
|
1189
|
+
|
|
788
1190
|
function isBrowserRunning() {
|
|
789
1191
|
try {
|
|
790
1192
|
const out = execSync(`pgrep -f "remote-debugging-port=${CDP_PORT}" 2>/dev/null || true`).toString().trim();
|
|
@@ -813,11 +1215,10 @@ function isGoogleLoggedIn() {
|
|
|
813
1215
|
// Pass force=true to re-copy everything except Google cookies.
|
|
814
1216
|
// Must be called while the agent browser is NOT running (Cookies file is locked).
|
|
815
1217
|
function seedBrowserProfile(force = false) {
|
|
816
|
-
const
|
|
817
|
-
if (!
|
|
818
|
-
const srcDir =
|
|
1218
|
+
const profile = findUserBrowserProfile();
|
|
1219
|
+
if (!profile) return 'no-chrome';
|
|
1220
|
+
const { srcDir } = profile;
|
|
819
1221
|
const destDir = path.join(BROWSER_PROFILE_DIR, 'Default');
|
|
820
|
-
if (!fs.existsSync(srcDir)) return 'no-chrome';
|
|
821
1222
|
const destCookies = path.join(destDir, 'Cookies');
|
|
822
1223
|
if (!force && fs.existsSync(destCookies)) return 'already-seeded';
|
|
823
1224
|
fs.mkdirSync(destDir, { recursive: true });
|
|
@@ -827,6 +1228,35 @@ function seedBrowserProfile(force = false) {
|
|
|
827
1228
|
const src = path.join(srcDir, f);
|
|
828
1229
|
if (fs.existsSync(src)) try { fs.copyFileSync(src, path.join(destDir, f)); } catch {}
|
|
829
1230
|
}
|
|
1231
|
+
|
|
1232
|
+
// Fix bookmarks: flatten any nested 'Bookmarks Bar' folder so items show in bar directly.
|
|
1233
|
+
// Also enable the bookmarks bar in Preferences.
|
|
1234
|
+
const bookmarksPath = path.join(destDir, 'Bookmarks');
|
|
1235
|
+
if (fs.existsSync(bookmarksPath)) {
|
|
1236
|
+
try {
|
|
1237
|
+
const bm = JSON.parse(fs.readFileSync(bookmarksPath, 'utf8'));
|
|
1238
|
+
const barRoot = bm.roots && bm.roots.bookmark_bar;
|
|
1239
|
+
if (barRoot && Array.isArray(barRoot.children)) {
|
|
1240
|
+
const children = barRoot.children;
|
|
1241
|
+
// If the bar's only/first child is a folder named 'Bookmarks Bar' or 'Bookmarks bar', hoist its contents
|
|
1242
|
+
const isNestedBar = children.length > 0 && children[0].type === 'folder' &&
|
|
1243
|
+
/^bookmarks? ?bar$/i.test(children[0].name || '');
|
|
1244
|
+
if (isNestedBar) {
|
|
1245
|
+
barRoot.children = children[0].children || [];
|
|
1246
|
+
fs.writeFileSync(bookmarksPath, JSON.stringify(bm, null, 2));
|
|
1247
|
+
}
|
|
1248
|
+
}
|
|
1249
|
+
} catch {}
|
|
1250
|
+
}
|
|
1251
|
+
const prefsPath = path.join(destDir, 'Preferences');
|
|
1252
|
+
if (fs.existsSync(prefsPath)) {
|
|
1253
|
+
try {
|
|
1254
|
+
const prefs = JSON.parse(fs.readFileSync(prefsPath, 'utf8'));
|
|
1255
|
+
if (!prefs.bookmark_bar) prefs.bookmark_bar = {};
|
|
1256
|
+
prefs.bookmark_bar.show_on_all_tabs = true;
|
|
1257
|
+
fs.writeFileSync(prefsPath, JSON.stringify(prefs, null, 2));
|
|
1258
|
+
} catch {}
|
|
1259
|
+
}
|
|
830
1260
|
for (const dir of ['Local Storage', 'IndexedDB']) {
|
|
831
1261
|
const src = path.join(srcDir, dir);
|
|
832
1262
|
const dest = path.join(destDir, dir);
|
|
@@ -878,9 +1308,9 @@ print('ok')
|
|
|
878
1308
|
// Works while BOTH browsers are running — no restart needed.
|
|
879
1309
|
// Returns the number of cookies synced or -1 on error.
|
|
880
1310
|
function syncSessionCookies() {
|
|
881
|
-
const
|
|
882
|
-
if (!
|
|
883
|
-
const srcDb = path.join(
|
|
1311
|
+
const profile = findUserBrowserProfile();
|
|
1312
|
+
if (!profile) return 0;
|
|
1313
|
+
const srcDb = path.join(profile.srcDir, 'Cookies');
|
|
884
1314
|
const destDb = path.join(BROWSER_PROFILE_DIR, 'Default', 'Cookies');
|
|
885
1315
|
if (!fs.existsSync(srcDb) || !fs.existsSync(destDb)) return 0;
|
|
886
1316
|
|
|
@@ -913,14 +1343,226 @@ print(count)
|
|
|
913
1343
|
} catch { return -1; }
|
|
914
1344
|
}
|
|
915
1345
|
|
|
1346
|
+
// ── Session Vault ─────────────────────────────────────────────────────────────
|
|
1347
|
+
// Cross-machine cookie sync via Railway server.
|
|
1348
|
+
//
|
|
1349
|
+
// Security model (Bitwarden-style):
|
|
1350
|
+
// - Cookies are AES-256-GCM encrypted CLIENT-SIDE before leaving the machine.
|
|
1351
|
+
// - Key is derived from the user's worker token via scrypt — never transmitted.
|
|
1352
|
+
// - The Railway server stores ciphertext it cannot read.
|
|
1353
|
+
// - A DB breach exposes encrypted blobs that are useless without the token.
|
|
1354
|
+
//
|
|
1355
|
+
// Merge model (client owns the merge):
|
|
1356
|
+
// - push: capture live browser cookies → pull vault → decrypt → merge → encrypt → PUT
|
|
1357
|
+
// - pull: GET → decrypt → merge into sessions.json
|
|
1358
|
+
// - Server does a simple replace — it never inspects cookie contents.
|
|
1359
|
+
//
|
|
1360
|
+
// Google cookies are excluded (browser-fingerprint-bound, break on other machines).
|
|
1361
|
+
|
|
1362
|
+
const VAULT_EXCLUDE_DOMAINS = ['google.com', 'googleapis.com', 'gstatic.com', 'youtube.com', 'accounts.google', 'googleadservices'];
|
|
1363
|
+
|
|
1364
|
+
// Derives a 32-byte AES key from the user_id (user-scoped, same across all devices).
|
|
1365
|
+
// user_id is returned by the server GET /api/sessions/vault response.
|
|
1366
|
+
function _vaultKey(userId) {
|
|
1367
|
+
return crypto.scryptSync(String(userId), 'agentforge-session-vault-v1', 32);
|
|
1368
|
+
}
|
|
1369
|
+
|
|
1370
|
+
function _vaultEncrypt(userId, cookies) {
|
|
1371
|
+
const key = _vaultKey(userId);
|
|
1372
|
+
const iv = crypto.randomBytes(12); // 96-bit IV for GCM
|
|
1373
|
+
const cipher = crypto.createCipheriv('aes-256-gcm', key, iv);
|
|
1374
|
+
const data = Buffer.concat([cipher.update(JSON.stringify(cookies), 'utf8'), cipher.final()]);
|
|
1375
|
+
return {
|
|
1376
|
+
v: 1,
|
|
1377
|
+
iv: iv.toString('base64'),
|
|
1378
|
+
tag: cipher.getAuthTag().toString('base64'),
|
|
1379
|
+
data: data.toString('base64'),
|
|
1380
|
+
};
|
|
1381
|
+
}
|
|
1382
|
+
|
|
1383
|
+
function _vaultDecrypt(userId, payload) {
|
|
1384
|
+
if (!payload || payload.v !== 1) return null;
|
|
1385
|
+
try {
|
|
1386
|
+
const key = _vaultKey(userId);
|
|
1387
|
+
const decipher = crypto.createDecipheriv('aes-256-gcm', key, Buffer.from(payload.iv, 'base64'));
|
|
1388
|
+
decipher.setAuthTag(Buffer.from(payload.tag, 'base64'));
|
|
1389
|
+
const plain = Buffer.concat([decipher.update(Buffer.from(payload.data, 'base64')), decipher.final()]);
|
|
1390
|
+
return JSON.parse(plain.toString('utf8'));
|
|
1391
|
+
} catch { return null; }
|
|
1392
|
+
}
|
|
1393
|
+
|
|
1394
|
+
// Merge two cookie arrays by name+domain.
|
|
1395
|
+
// For persistent cookies (with expires), keep the one with the later expiry — this ensures
|
|
1396
|
+
// For persistent cookies, keep the one with the later expiry.
|
|
1397
|
+
// For session cookies (expires -1 or absent), incoming wins.
|
|
1398
|
+
function _mergeCookies(base, incoming) {
|
|
1399
|
+
const merged = [...base];
|
|
1400
|
+
for (const c of incoming) {
|
|
1401
|
+
const idx = merged.findIndex(e => e.name === c.name && e.domain === c.domain);
|
|
1402
|
+
if (idx < 0) { merged.push(c); continue; }
|
|
1403
|
+
const existing = merged[idx];
|
|
1404
|
+
// Both persistent: keep whichever expires later
|
|
1405
|
+
if (c.expires > 0 && existing.expires > 0) {
|
|
1406
|
+
if (c.expires > existing.expires) merged[idx] = c;
|
|
1407
|
+
// else keep existing (it expires later)
|
|
1408
|
+
} else {
|
|
1409
|
+
// Session cookie or incoming persistent over existing session: incoming wins
|
|
1410
|
+
merged[idx] = c;
|
|
1411
|
+
}
|
|
1412
|
+
}
|
|
1413
|
+
return merged;
|
|
1414
|
+
}
|
|
1415
|
+
|
|
1416
|
+
// Ensures the AgentForge dashboard browser tab is authenticated.
|
|
1417
|
+
// Uses the worker token to create a fresh server session via /auth/worker-token.
|
|
1418
|
+
// This heals expired sessions and server restarts with new SESSION_SECRETs automatically.
|
|
1419
|
+
async function ensureDashboardAuth(token, serverUrl) {
|
|
1420
|
+
if (!isBrowserRunning()) return; // nothing to do if browser isn't running
|
|
1421
|
+
try {
|
|
1422
|
+
const tabsRes = await fetch(`http://127.0.0.1:${CDP_PORT}/json`);
|
|
1423
|
+
const tabs = await tabsRes.json();
|
|
1424
|
+
if (!tabs || tabs.length === 0) return;
|
|
1425
|
+
|
|
1426
|
+
// Find the dashboard tab (or any tab on the server domain)
|
|
1427
|
+
const dashboardTab = tabs.find(t => t.url && t.url.includes(serverUrl.replace(/^https?:\/\//, '')));
|
|
1428
|
+
if (!dashboardTab) return;
|
|
1429
|
+
|
|
1430
|
+
const currentUrl = dashboardTab.url || '';
|
|
1431
|
+
|
|
1432
|
+
// If on GitHub login page or login redirect — session is expired, re-auth now
|
|
1433
|
+
// Also check on startup (always navigate to ensure fresh session)
|
|
1434
|
+
const needsAuth = currentUrl.includes('github.com/login') ||
|
|
1435
|
+
currentUrl.includes('/auth/github') ||
|
|
1436
|
+
currentUrl.endsWith('/login') ||
|
|
1437
|
+
currentUrl.endsWith('/');
|
|
1438
|
+
|
|
1439
|
+
if (!needsAuth) return; // Dashboard is loaded and session is valid
|
|
1440
|
+
|
|
1441
|
+
const authUrl = `${serverUrl}/auth/worker-token?token=${encodeURIComponent(token)}`;
|
|
1442
|
+
console.log('🔑 Dashboard session expired — re-authenticating via worker token...');
|
|
1443
|
+
|
|
1444
|
+
const { default: WebSocket } = await import('ws');
|
|
1445
|
+
await new Promise((resolve) => {
|
|
1446
|
+
const ws = new WebSocket(dashboardTab.webSocketDebuggerUrl);
|
|
1447
|
+
const timer = setTimeout(() => { try { ws.close(); } catch {} resolve(); }, 8000);
|
|
1448
|
+
ws.on('open', () => {
|
|
1449
|
+
ws.send(JSON.stringify({
|
|
1450
|
+
id: 1,
|
|
1451
|
+
method: 'Page.navigate',
|
|
1452
|
+
params: { url: authUrl }
|
|
1453
|
+
}));
|
|
1454
|
+
});
|
|
1455
|
+
ws.on('message', (raw) => {
|
|
1456
|
+
try {
|
|
1457
|
+
const m = JSON.parse(raw);
|
|
1458
|
+
if (m.id === 1) { clearTimeout(timer); try { ws.close(); } catch {} resolve(); }
|
|
1459
|
+
} catch {}
|
|
1460
|
+
});
|
|
1461
|
+
ws.on('error', () => { clearTimeout(timer); resolve(); });
|
|
1462
|
+
});
|
|
1463
|
+
|
|
1464
|
+
console.log('✅ Dashboard re-authentication initiated');
|
|
1465
|
+
} catch (err) {
|
|
1466
|
+
// Non-fatal — will retry on next interval
|
|
1467
|
+
}
|
|
1468
|
+
}
|
|
1469
|
+
|
|
1470
|
+
// Reads all plaintext cookies from the running agent browser via CDP Network.getAllCookies.
|
|
1471
|
+
// Returns [] if browser is not running.
|
|
1472
|
+
async function captureAgentBrowserCookies() {
|
|
1473
|
+
if (!isBrowserRunning()) return [];
|
|
1474
|
+
try {
|
|
1475
|
+
const tabsRes = await fetch(`http://127.0.0.1:${CDP_PORT}/json`);
|
|
1476
|
+
const tabs = await tabsRes.json();
|
|
1477
|
+
if (!tabs || !tabs[0]) return [];
|
|
1478
|
+
const { default: WebSocket } = await import('ws');
|
|
1479
|
+
return await new Promise((resolve) => {
|
|
1480
|
+
const ws = new WebSocket(tabs[0].webSocketDebuggerUrl);
|
|
1481
|
+
const timer = setTimeout(() => { try { ws.close(); } catch {} resolve([]); }, 6000);
|
|
1482
|
+
ws.on('open', () => {
|
|
1483
|
+
ws.send(JSON.stringify({ id: 1, method: 'Network.getAllCookies', params: {} }));
|
|
1484
|
+
});
|
|
1485
|
+
ws.on('message', (raw) => {
|
|
1486
|
+
try {
|
|
1487
|
+
const m = JSON.parse(raw);
|
|
1488
|
+
if (m.id === 1) {
|
|
1489
|
+
clearTimeout(timer);
|
|
1490
|
+
try { ws.close(); } catch {}
|
|
1491
|
+
const cookies = (m.result?.cookies || []).filter(c =>
|
|
1492
|
+
!VAULT_EXCLUDE_DOMAINS.some(d => (c.domain || '').includes(d))
|
|
1493
|
+
);
|
|
1494
|
+
resolve(cookies);
|
|
1495
|
+
}
|
|
1496
|
+
} catch { resolve([]); }
|
|
1497
|
+
});
|
|
1498
|
+
ws.on('error', () => { clearTimeout(timer); resolve([]); });
|
|
1499
|
+
});
|
|
1500
|
+
} catch { return []; }
|
|
1501
|
+
}
|
|
1502
|
+
|
|
1503
|
+
// Fetches and decrypts the current vault contents.
|
|
1504
|
+
// Returns { cookies: [], userId: null } if vault is empty or decryption fails.
|
|
1505
|
+
// userId is user-scoped (same across all devices for the same GitHub account).
|
|
1506
|
+
async function _fetchVault(token, serverUrl) {
|
|
1507
|
+
try {
|
|
1508
|
+
const res = await fetch(`${serverUrl}/api/sessions/vault`, {
|
|
1509
|
+
headers: { 'Authorization': `Bearer ${token}` },
|
|
1510
|
+
signal: AbortSignal.timeout(10000),
|
|
1511
|
+
});
|
|
1512
|
+
if (!res.ok) return { cookies: [], userId: null };
|
|
1513
|
+
const data = await res.json();
|
|
1514
|
+
const userId = data.user_id || null;
|
|
1515
|
+
if (!data.payload) return { cookies: [], userId };
|
|
1516
|
+
const cookies = _vaultDecrypt(userId, data.payload) || [];
|
|
1517
|
+
return { cookies, userId };
|
|
1518
|
+
} catch { return { cookies: [], userId: null }; }
|
|
1519
|
+
}
|
|
1520
|
+
|
|
1521
|
+
// Pushes cookies to the vault: capture browser → GET user_id + existing → merge → encrypt with user_id → PUT.
|
|
1522
|
+
// Only runs when the agent browser is available (no point pushing 0 cookies).
|
|
1523
|
+
async function pushSessionsToVault(token, serverUrl) {
|
|
1524
|
+
if (!isBrowserRunning()) return;
|
|
1525
|
+
try {
|
|
1526
|
+
const [local, { cookies: existing, userId }] = await Promise.all([
|
|
1527
|
+
captureAgentBrowserCookies(),
|
|
1528
|
+
_fetchVault(token, serverUrl),
|
|
1529
|
+
]);
|
|
1530
|
+
if (local.length === 0 || !userId) return;
|
|
1531
|
+
const merged = _mergeCookies(existing, local); // local wins on conflict
|
|
1532
|
+
const payload = _vaultEncrypt(userId, merged);
|
|
1533
|
+
const res = await fetch(`${serverUrl}/api/sessions/vault`, {
|
|
1534
|
+
method: 'PUT',
|
|
1535
|
+
headers: { 'Authorization': `Bearer ${token}`, 'Content-Type': 'application/json' },
|
|
1536
|
+
body: JSON.stringify({ payload }),
|
|
1537
|
+
signal: AbortSignal.timeout(15000),
|
|
1538
|
+
});
|
|
1539
|
+
if (res.ok) console.log(`☁️ Session vault updated (${merged.length} cookie(s), encrypted)`);
|
|
1540
|
+
} catch { /* non-fatal */ }
|
|
1541
|
+
}
|
|
1542
|
+
|
|
1543
|
+
// Pulls cookies from the vault, decrypts, and merges into sessions.json.
|
|
1544
|
+
async function pullSessionsFromVault(token, serverUrl) {
|
|
1545
|
+
try {
|
|
1546
|
+
const { cookies: vaultCookies } = await _fetchVault(token, serverUrl);
|
|
1547
|
+
if (vaultCookies.length === 0) return;
|
|
1548
|
+
// Merge vault cookies into local sessions.json
|
|
1549
|
+
saveSession(vaultCookies);
|
|
1550
|
+
console.log(`☁️ Session vault synced (${vaultCookies.length} cookie(s), decrypted)`);
|
|
1551
|
+
} catch { /* non-fatal */ }
|
|
1552
|
+
}
|
|
1553
|
+
|
|
916
1554
|
// Creates /Applications/AgentForge Browser.app — a Chrome wrapper that:
|
|
917
1555
|
// - Appears as a distinct app in the dock (own icon, own name)
|
|
918
1556
|
// - Launches Chrome with the agent profile flags
|
|
919
1557
|
// - Shares Chrome's "Chrome Safe Storage" Keychain key so seeded cookies work
|
|
920
1558
|
// Falls back to ~/Applications if /Applications isn't writable.
|
|
921
1559
|
function installAgentBrowserApp() {
|
|
922
|
-
|
|
923
|
-
|
|
1560
|
+
// Use whichever browser matches the user's profile (Chrome or Chromium) so
|
|
1561
|
+
// the agent browser shares the same "* Safe Storage" Keychain key and can
|
|
1562
|
+
// decrypt seeded cookies. Falls back to any available browser.
|
|
1563
|
+
const profile = findUserBrowserProfile();
|
|
1564
|
+
const chrome = profile?.browserBin || findBrowser();
|
|
1565
|
+
if (!chrome) return null;
|
|
924
1566
|
|
|
925
1567
|
const appName = 'AgentForge Browser.app';
|
|
926
1568
|
let appDir = path.join('/Applications', appName);
|
|
@@ -1048,6 +1690,151 @@ print('ok')
|
|
|
1048
1690
|
try { execSync(`python3 "${pyScript}" 2>/dev/null`); } catch {}
|
|
1049
1691
|
}
|
|
1050
1692
|
|
|
1693
|
+
// Saves cookies to sessions.json for persistence across browser restarts.
|
|
1694
|
+
// Call this after manually logging in to a site to persist that login.
|
|
1695
|
+
function saveSession(cookies) {
|
|
1696
|
+
let sessions = { cookies: [] };
|
|
1697
|
+
if (fs.existsSync(SESSIONS_FILE)) {
|
|
1698
|
+
try { sessions = JSON.parse(fs.readFileSync(SESSIONS_FILE, 'utf8')); } catch {}
|
|
1699
|
+
}
|
|
1700
|
+
// Merge: update existing by name+domain, add new ones
|
|
1701
|
+
for (const c of cookies) {
|
|
1702
|
+
const idx = sessions.cookies.findIndex(s => s.name === c.name && s.domain === c.domain);
|
|
1703
|
+
if (idx >= 0) sessions.cookies[idx] = c;
|
|
1704
|
+
else sessions.cookies.push(c);
|
|
1705
|
+
}
|
|
1706
|
+
fs.writeFileSync(SESSIONS_FILE, JSON.stringify(sessions, null, 2));
|
|
1707
|
+
}
|
|
1708
|
+
|
|
1709
|
+
async function sendPageCdpCommand(webSocketDebuggerUrl, method, params = {}, timeoutMs = 15000) {
|
|
1710
|
+
const { default: WebSocket } = await import('ws');
|
|
1711
|
+
return await new Promise((resolve, reject) => {
|
|
1712
|
+
const ws = new WebSocket(webSocketDebuggerUrl);
|
|
1713
|
+
const id = 1;
|
|
1714
|
+
const timer = setTimeout(() => {
|
|
1715
|
+
try { ws.close(); } catch {}
|
|
1716
|
+
reject(new Error(`${method} timed out after ${timeoutMs}ms`));
|
|
1717
|
+
}, timeoutMs);
|
|
1718
|
+
ws.on('open', () => {
|
|
1719
|
+
ws.send(JSON.stringify({ id, method, params }));
|
|
1720
|
+
});
|
|
1721
|
+
ws.on('message', (raw) => {
|
|
1722
|
+
try {
|
|
1723
|
+
const message = JSON.parse(raw);
|
|
1724
|
+
if (message.id !== id) return;
|
|
1725
|
+
clearTimeout(timer);
|
|
1726
|
+
try { ws.close(); } catch {}
|
|
1727
|
+
if (message.error) {
|
|
1728
|
+
reject(new Error(message.error.message || JSON.stringify(message.error)));
|
|
1729
|
+
} else {
|
|
1730
|
+
resolve(message.result);
|
|
1731
|
+
}
|
|
1732
|
+
} catch (err) {
|
|
1733
|
+
clearTimeout(timer);
|
|
1734
|
+
try { ws.close(); } catch {}
|
|
1735
|
+
reject(err);
|
|
1736
|
+
}
|
|
1737
|
+
});
|
|
1738
|
+
ws.on('error', (err) => {
|
|
1739
|
+
clearTimeout(timer);
|
|
1740
|
+
reject(err);
|
|
1741
|
+
});
|
|
1742
|
+
});
|
|
1743
|
+
}
|
|
1744
|
+
|
|
1745
|
+
// Injects cookies from sessions.json into the running browser via a page-level CDP
|
|
1746
|
+
// target. Avoid Puppeteer here: browser.pages() eagerly enables Network on every
|
|
1747
|
+
// tab, which can time out while Chrome is busy and makes startup look broken.
|
|
1748
|
+
// Page-level Network.setCookies writes to the real cookie store without that scan.
|
|
1749
|
+
async function injectSavedSessions() {
|
|
1750
|
+
if (!fs.existsSync(SESSIONS_FILE)) return;
|
|
1751
|
+
let sessions;
|
|
1752
|
+
try { sessions = JSON.parse(fs.readFileSync(SESSIONS_FILE, 'utf8')); } catch { return; }
|
|
1753
|
+
const cookies = sessions.cookies || [];
|
|
1754
|
+
if (cookies.length === 0) return;
|
|
1755
|
+
|
|
1756
|
+
// Wait for CDP to be ready (up to 15s)
|
|
1757
|
+
let ready = false;
|
|
1758
|
+
for (let i = 0; i < 30; i++) {
|
|
1759
|
+
await new Promise(r => setTimeout(r, 500));
|
|
1760
|
+
try {
|
|
1761
|
+
await new Promise((resolve, reject) => {
|
|
1762
|
+
const req = http.get(`http://127.0.0.1:${CDP_PORT}/json`, r => { r.resume(); resolve(); });
|
|
1763
|
+
req.on('error', reject);
|
|
1764
|
+
req.setTimeout(500, () => { req.destroy(); reject(new Error('timeout')); });
|
|
1765
|
+
});
|
|
1766
|
+
ready = true;
|
|
1767
|
+
break;
|
|
1768
|
+
} catch {}
|
|
1769
|
+
}
|
|
1770
|
+
if (!ready) return;
|
|
1771
|
+
|
|
1772
|
+
await new Promise(r => setTimeout(r, 800));
|
|
1773
|
+
|
|
1774
|
+
try {
|
|
1775
|
+
const tabsRes = await fetch(`http://127.0.0.1:${CDP_PORT}/json`);
|
|
1776
|
+
const tabs = await tabsRes.json();
|
|
1777
|
+
const page = tabs.find(t => t.type === 'page' && t.webSocketDebuggerUrl) || tabs.find(t => t.webSocketDebuggerUrl);
|
|
1778
|
+
if (!page) return;
|
|
1779
|
+
|
|
1780
|
+
// Strip Chrome-internal fields that CDP rejects (sameParty, sourceScheme, sourcePort, partitionKey, etc.)
|
|
1781
|
+
// Use only the fields that Network.setCookies accepts.
|
|
1782
|
+
const VALID_COOKIE_FIELDS = new Set(['name','value','domain','path','secure','httpOnly','sameSite','expires']);
|
|
1783
|
+
const sanitized = cookies.map(c => {
|
|
1784
|
+
const s = {};
|
|
1785
|
+
for (const k of VALID_COOKIE_FIELDS) if (c[k] !== undefined) s[k] = c[k];
|
|
1786
|
+
if (s.expires !== undefined && s.expires <= 0) delete s.expires;
|
|
1787
|
+
return s;
|
|
1788
|
+
}).filter(c => c.name && c.value !== undefined);
|
|
1789
|
+
|
|
1790
|
+
// Try bulk inject first. If Chrome rejects the batch (invalid cookie fields from
|
|
1791
|
+
// public suffix list or partition key issues), fall back to one-by-one so valid
|
|
1792
|
+
// cookies still get set.
|
|
1793
|
+
try {
|
|
1794
|
+
await sendPageCdpCommand(page.webSocketDebuggerUrl, 'Network.setCookies', { cookies: sanitized }, 15000);
|
|
1795
|
+
console.log(`🔑 Sessions injected (${sanitized.length} cookie(s)) via Network.setCookies`);
|
|
1796
|
+
} catch (bulkErr) {
|
|
1797
|
+
console.warn(`⚠️ Bulk cookie inject failed (${bulkErr.message}) — injecting one-by-one`);
|
|
1798
|
+
let injected = 0, skipped = 0;
|
|
1799
|
+
for (const cookie of sanitized) {
|
|
1800
|
+
try {
|
|
1801
|
+
await sendPageCdpCommand(page.webSocketDebuggerUrl, 'Network.setCookies', { cookies: [cookie] }, 5000);
|
|
1802
|
+
injected++;
|
|
1803
|
+
} catch { skipped++; }
|
|
1804
|
+
}
|
|
1805
|
+
console.log(`🔑 Sessions injected one-by-one: ${injected} ok, ${skipped} skipped`);
|
|
1806
|
+
}
|
|
1807
|
+
} catch (err) {
|
|
1808
|
+
console.warn(`⚠️ Session injection failed: ${err.message}`);
|
|
1809
|
+
}
|
|
1810
|
+
}
|
|
1811
|
+
|
|
1812
|
+
function launchBrowserDirect(browserPath, startupUrl = 'https://agentforgeai-production.up.railway.app/dashboard') {
|
|
1813
|
+
const args = [
|
|
1814
|
+
`--remote-debugging-address=0.0.0.0`,
|
|
1815
|
+
`--remote-debugging-port=${CDP_PORT}`,
|
|
1816
|
+
`--user-data-dir=${BROWSER_PROFILE_DIR}`,
|
|
1817
|
+
'--no-first-run',
|
|
1818
|
+
'--no-default-browser-check',
|
|
1819
|
+
'--disable-sync',
|
|
1820
|
+
'--disable-component-update',
|
|
1821
|
+
'--safebrowsing-disable-auto-update',
|
|
1822
|
+
'--disable-background-networking',
|
|
1823
|
+
'--disable-background-timer-throttling',
|
|
1824
|
+
'--disable-backgrounding-occluded-windows',
|
|
1825
|
+
'--disable-renderer-backgrounding',
|
|
1826
|
+
'--disable-client-side-phishing-detection',
|
|
1827
|
+
'--disable-hang-monitor',
|
|
1828
|
+
'--disable-translate',
|
|
1829
|
+
startupUrl,
|
|
1830
|
+
];
|
|
1831
|
+
const logPath = path.join(CONFIG_DIR, 'browser.log');
|
|
1832
|
+
const out = fs.openSync(logPath, 'a');
|
|
1833
|
+
const proc = spawn(browserPath, args, { detached: true, stdio: ['ignore', out, out] });
|
|
1834
|
+
proc.unref();
|
|
1835
|
+
return true;
|
|
1836
|
+
}
|
|
1837
|
+
|
|
1051
1838
|
function installLaunchAgent(browserPath, startupUrl = 'https://agentforgeai-production.up.railway.app/dashboard') {
|
|
1052
1839
|
const plist = `<?xml version="1.0" encoding="UTF-8"?>
|
|
1053
1840
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
@@ -1066,6 +1853,13 @@ function installLaunchAgent(browserPath, startupUrl = 'https://agentforgeai-prod
|
|
|
1066
1853
|
<string>--disable-sync</string>
|
|
1067
1854
|
<string>--disable-component-update</string>
|
|
1068
1855
|
<string>--safebrowsing-disable-auto-update</string>
|
|
1856
|
+
<string>--disable-background-networking</string>
|
|
1857
|
+
<string>--disable-background-timer-throttling</string>
|
|
1858
|
+
<string>--disable-backgrounding-occluded-windows</string>
|
|
1859
|
+
<string>--disable-renderer-backgrounding</string>
|
|
1860
|
+
<string>--disable-client-side-phishing-detection</string>
|
|
1861
|
+
<string>--disable-hang-monitor</string>
|
|
1862
|
+
<string>--disable-translate</string>
|
|
1069
1863
|
<string>${startupUrl}</string>
|
|
1070
1864
|
</array>
|
|
1071
1865
|
<key>RunAtLoad</key>
|
|
@@ -1111,7 +1905,7 @@ program
|
|
|
1111
1905
|
}
|
|
1112
1906
|
if (isBrowserRunning()) {
|
|
1113
1907
|
console.log('Stopping agent browser to reseed...');
|
|
1114
|
-
|
|
1908
|
+
execSync('pkill -9 Chromium 2>/dev/null || pkill -9 "Google Chrome" 2>/dev/null || true');
|
|
1115
1909
|
execSync('sleep 2');
|
|
1116
1910
|
}
|
|
1117
1911
|
const result = seedBrowserProfile(true);
|
|
@@ -1120,7 +1914,7 @@ program
|
|
|
1120
1914
|
} else {
|
|
1121
1915
|
console.log('⚠️ Could not read Chrome profile.');
|
|
1122
1916
|
}
|
|
1123
|
-
|
|
1917
|
+
launchBrowserDirect(browser);
|
|
1124
1918
|
console.log('✅ Agent browser restarted — log into Google in the browser window.');
|
|
1125
1919
|
return;
|
|
1126
1920
|
}
|
|
@@ -1132,7 +1926,7 @@ program
|
|
|
1132
1926
|
|
|
1133
1927
|
installAgentBrowserApp();
|
|
1134
1928
|
const result = seedBrowserProfile();
|
|
1135
|
-
|
|
1929
|
+
launchBrowserDirect(browser);
|
|
1136
1930
|
if (result === 'seeded') {
|
|
1137
1931
|
console.log('✅ Agent browser launched — log into Google in the browser window (one time only).');
|
|
1138
1932
|
} else {
|