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