agenter-app 1.1.0 → 1.3.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/README.md +168 -130
- package/cli.js +43 -0
- package/lib/memory.js +101 -0
- package/lib/terminal.js +127 -0
- package/lib/update.js +69 -0
- package/package.json +1 -1
- package/public/app.js +500 -21
- package/public/index.html +89 -10
- package/public/style.css +119 -10
- package/scripts/pick-folder.ps1 +85 -0
- package/server.js +205 -17
package/server.js
CHANGED
|
@@ -13,6 +13,10 @@ const { truncate, describeTool, readTail, readHead, parseCodexItem, readTranscri
|
|
|
13
13
|
const { normalizeLimits, codexWindowLabel } = require('./lib/limits');
|
|
14
14
|
const { buildDiffs } = require('./lib/diff');
|
|
15
15
|
const { getToken, sameToken } = require('./lib/token');
|
|
16
|
+
const { version: VERSION } = require('./package.json');
|
|
17
|
+
const updates = require('./lib/update');
|
|
18
|
+
const { createTerminals } = require('./lib/terminal');
|
|
19
|
+
const { readMemory, saveMemory, carriedContext } = require('./lib/memory');
|
|
16
20
|
|
|
17
21
|
const PORT = Number(process.env.AGENTER_PORT) || 4317;
|
|
18
22
|
const HOST = '127.0.0.1';
|
|
@@ -36,6 +40,9 @@ const DISCOVER_FULL_SCAN_MS = 5 * 60 * 1000; // when a file watcher catches chan
|
|
|
36
40
|
const CREDENTIALS_FILE = path.join(os.homedir(), '.claude', '.credentials.json');
|
|
37
41
|
const USAGE_URL = 'https://api.anthropic.com/api/oauth/usage';
|
|
38
42
|
const USAGE_INTERVAL_MS = 2 * 60 * 1000;
|
|
43
|
+
// A window opening also checks, so a long-running server still tells you about updates.
|
|
44
|
+
const UPDATE_INTERVAL_MS = 6 * 60 * 60 * 1000;
|
|
45
|
+
const UPDATE_THROTTLE_MS = 60 * 1000;
|
|
39
46
|
|
|
40
47
|
const sessions = new Map(); // session_id -> session
|
|
41
48
|
const pending = new Map(); // pending id -> { item, res, payload }
|
|
@@ -463,6 +470,56 @@ function noteCodexLimits(rl, at) {
|
|
|
463
470
|
codexUsage = { updatedAt: at, plan: rl.plan_type || '', limits, credits: rl.credits || null };
|
|
464
471
|
}
|
|
465
472
|
|
|
473
|
+
// ---------- updates ----------
|
|
474
|
+
|
|
475
|
+
let latestVersion = updates.cachedLatest();
|
|
476
|
+
let updateCheckedAt = 0;
|
|
477
|
+
|
|
478
|
+
async function checkForUpdate() {
|
|
479
|
+
if (!updates.enabled()) return;
|
|
480
|
+
updateCheckedAt = Date.now();
|
|
481
|
+
latestVersion = (await updates.checkLatest({ timeoutMs: 10000 })) || latestVersion;
|
|
482
|
+
broadcast();
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
function updateState() {
|
|
486
|
+
const installed = updates.installedVersion();
|
|
487
|
+
return {
|
|
488
|
+
current: VERSION,
|
|
489
|
+
latest: latestVersion,
|
|
490
|
+
installed,
|
|
491
|
+
available: updates.isNewer(latestVersion, VERSION) && !updates.isNewer(installed, VERSION),
|
|
492
|
+
restart: updates.isNewer(installed, VERSION), // installed, but this server is still the old code
|
|
493
|
+
canSelfUpdate: updates.canSelfUpdate(),
|
|
494
|
+
};
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
// ---------- terminal ----------
|
|
498
|
+
|
|
499
|
+
const TERMINAL_ENABLED = process.platform === 'win32' && process.env.AGENTER_TERMINAL !== '0';
|
|
500
|
+
const terminals = createTerminals((e) => sendAll('terminal', e));
|
|
501
|
+
const terminalId = (cwd) => path.resolve(cwd).toLowerCase();
|
|
502
|
+
|
|
503
|
+
// ---------- folder window ----------
|
|
504
|
+
// The dashboard can't browse this machine's folders itself, so the server opens Windows' own.
|
|
505
|
+
|
|
506
|
+
let pickingFolder = null;
|
|
507
|
+
|
|
508
|
+
function pickFolder(start) {
|
|
509
|
+
return new Promise((resolve) => {
|
|
510
|
+
if (pickingFolder) return resolve({ busy: true });
|
|
511
|
+
if (process.platform !== 'win32') return resolve({ unsupported: true });
|
|
512
|
+
const args = ['-NoProfile', '-ExecutionPolicy', 'Bypass', '-STA', '-File', path.join(__dirname, 'scripts', 'pick-folder.ps1')];
|
|
513
|
+
pickingFolder = execFile('powershell.exe', args, { windowsHide: true, env: { ...process.env, AGENTER_PICK_START: String(start || '') } }, (err, out) => {
|
|
514
|
+
pickingFolder = null;
|
|
515
|
+
onAttentionChanged();
|
|
516
|
+
const folder = String(out || '').trim();
|
|
517
|
+
resolve(err ? { unsupported: true } : folder ? { path: folder } : { cancelled: true });
|
|
518
|
+
});
|
|
519
|
+
onAttentionChanged();
|
|
520
|
+
});
|
|
521
|
+
}
|
|
522
|
+
|
|
466
523
|
function openInVSCode(cwd) {
|
|
467
524
|
if (!cwd || /["&|<>^%]/.test(cwd) || !fs.existsSync(cwd)) return false;
|
|
468
525
|
const child =
|
|
@@ -660,7 +717,8 @@ function respond(id, action, body = {}) {
|
|
|
660
717
|
|
|
661
718
|
// Sessions are only held while a dashboard window has focus. If you're in VS Code,
|
|
662
719
|
// Claude finishes (or asks) there as usual instead of spinning on the dashboard.
|
|
663
|
-
|
|
720
|
+
// The folder window takes focus from the dashboard, but you're still here.
|
|
721
|
+
const inUse = () => !!pickingFolder || [...clients.values()].some((c) => c.focused);
|
|
664
722
|
|
|
665
723
|
let releaseTimer = null;
|
|
666
724
|
function onAttentionChanged() {
|
|
@@ -673,15 +731,24 @@ function onAttentionChanged() {
|
|
|
673
731
|
}
|
|
674
732
|
|
|
675
733
|
// Quit, from the tray. Open windows close themselves on the 'quit' event.
|
|
676
|
-
|
|
677
|
-
|
|
734
|
+
// A restart (to finish an update) leaves them open; they reconnect to the new server.
|
|
735
|
+
function stopServer({ restart = false } = {}) {
|
|
736
|
+
if (!restart) sendAll('quit', {});
|
|
678
737
|
for (const id of [...pending.keys()]) respond(id, 'pass');
|
|
679
738
|
for (const child of runners.values()) {
|
|
680
739
|
child.interrupted = true;
|
|
681
740
|
killRun(child);
|
|
682
741
|
}
|
|
683
|
-
|
|
684
|
-
|
|
742
|
+
terminals.killAll();
|
|
743
|
+
pickingFolder?.kill();
|
|
744
|
+
if (restart) {
|
|
745
|
+
// It waits for this process to let go of the port.
|
|
746
|
+
const env = { ...process.env, AGENTER_RESTART: '1' };
|
|
747
|
+
spawn(process.execPath, [path.join(__dirname, 'server.js'), '--no-open'], { detached: true, stdio: 'ignore', windowsHide: true, env }).unref();
|
|
748
|
+
} else {
|
|
749
|
+
fs.mkdirSync(SETTINGS_DIR, { recursive: true });
|
|
750
|
+
fs.writeFileSync(STOPPED_FILE, '');
|
|
751
|
+
}
|
|
685
752
|
// Long enough for the hooks to receive those answers.
|
|
686
753
|
setTimeout(() => process.exit(0), 300);
|
|
687
754
|
}
|
|
@@ -929,6 +996,64 @@ function startCodexRun(s, text, { fresh = false } = {}) {
|
|
|
929
996
|
});
|
|
930
997
|
}
|
|
931
998
|
|
|
999
|
+
// ---------- images ----------
|
|
1000
|
+
// Hooks and the CLIs only carry text, so pasted images are saved here and their paths go
|
|
1001
|
+
// into the message. Claude opens them with its Read tool, Codex with its image viewer.
|
|
1002
|
+
|
|
1003
|
+
const UPLOADS_DIR = path.join(SETTINGS_DIR, 'uploads');
|
|
1004
|
+
const IMAGE_TYPES = { png: 'png', jpeg: 'jpg', gif: 'gif', webp: 'webp' };
|
|
1005
|
+
const MAX_IMAGES = 8;
|
|
1006
|
+
const MAX_IMAGE_BYTES = 10 * 1024 * 1024;
|
|
1007
|
+
|
|
1008
|
+
// images: data: URLs from the dashboard. Returns the saved files' paths.
|
|
1009
|
+
function saveImages(images) {
|
|
1010
|
+
if (!Array.isArray(images) || !images.length) return [];
|
|
1011
|
+
if (images.length > MAX_IMAGES) throw new Error(`Attach at most ${MAX_IMAGES} images.`);
|
|
1012
|
+
const files = images.map((src) => {
|
|
1013
|
+
const m = /^data:image\/(png|jpeg|gif|webp);base64,([A-Za-z0-9+/=]+)$/.exec(String(src));
|
|
1014
|
+
if (!m) throw new Error('Only PNG, JPEG, GIF and WebP images can be attached.');
|
|
1015
|
+
const data = Buffer.from(m[2], 'base64');
|
|
1016
|
+
if (data.length > MAX_IMAGE_BYTES) throw new Error('Each image must be under 10 MB.');
|
|
1017
|
+
return { data, file: path.join(UPLOADS_DIR, `${crypto.randomUUID()}.${IMAGE_TYPES[m[1]]}`) };
|
|
1018
|
+
});
|
|
1019
|
+
fs.mkdirSync(UPLOADS_DIR, { recursive: true });
|
|
1020
|
+
for (const f of files) fs.writeFileSync(f.file, f.data);
|
|
1021
|
+
return files.map((f) => f.file);
|
|
1022
|
+
}
|
|
1023
|
+
|
|
1024
|
+
function withImages(text, paths) {
|
|
1025
|
+
text = String(text || '').trim();
|
|
1026
|
+
if (!paths.length) return text;
|
|
1027
|
+
const list = paths.map((p) => `- ${p}`).join('\n');
|
|
1028
|
+
const note = `The user attached ${paths.length === 1 ? 'an image' : `${paths.length} images`}. Open ${paths.length === 1 ? 'it' : 'them'} to see:\n${list}`;
|
|
1029
|
+
return text ? `${text}\n\n${note}` : note;
|
|
1030
|
+
}
|
|
1031
|
+
|
|
1032
|
+
// Uploads only matter for the turn they went with; clear out old ones.
|
|
1033
|
+
function pruneUploads(maxAgeMs = 30 * 24 * 3600 * 1000) {
|
|
1034
|
+
let names = [];
|
|
1035
|
+
try {
|
|
1036
|
+
names = fs.readdirSync(UPLOADS_DIR);
|
|
1037
|
+
} catch {
|
|
1038
|
+
return;
|
|
1039
|
+
}
|
|
1040
|
+
for (const n of names) {
|
|
1041
|
+
const f = path.join(UPLOADS_DIR, n);
|
|
1042
|
+
try {
|
|
1043
|
+
if (Date.now() - fs.statSync(f).mtimeMs > maxAgeMs) fs.rmSync(f, { force: true });
|
|
1044
|
+
} catch {}
|
|
1045
|
+
}
|
|
1046
|
+
}
|
|
1047
|
+
|
|
1048
|
+
// ---------- memory ----------
|
|
1049
|
+
|
|
1050
|
+
const memoryContext = (s) => ({ agent: s.agent, cwd: s.cwd, transcriptPath: s.transcriptPath, home: os.homedir(), codexHome: CODEX_HOME });
|
|
1051
|
+
|
|
1052
|
+
function carryFrom(s) {
|
|
1053
|
+
const t = s?.transcriptPath && readTranscript(s.transcriptPath);
|
|
1054
|
+
return t ? carriedContext({ agent: s.agent, cwd: s.cwd, title: s.title, messages: t.messages }) : '';
|
|
1055
|
+
}
|
|
1056
|
+
|
|
932
1057
|
// ---------- hook events ----------
|
|
933
1058
|
|
|
934
1059
|
function handleHook(p, res) {
|
|
@@ -1011,6 +1136,9 @@ function handleHook(p, res) {
|
|
|
1011
1136
|
|
|
1012
1137
|
function snapshot() {
|
|
1013
1138
|
return {
|
|
1139
|
+
version: VERSION,
|
|
1140
|
+
update: updateState(),
|
|
1141
|
+
terminal: TERMINAL_ENABLED,
|
|
1014
1142
|
settings,
|
|
1015
1143
|
usage,
|
|
1016
1144
|
codexUsage,
|
|
@@ -1020,14 +1148,18 @@ function snapshot() {
|
|
|
1020
1148
|
};
|
|
1021
1149
|
}
|
|
1022
1150
|
|
|
1151
|
+
const sendAll = (event, data) => {
|
|
1152
|
+
const msg = `event: ${event}\ndata: ${JSON.stringify(data)}\n\n`;
|
|
1153
|
+
for (const c of clients.keys()) c.write(msg);
|
|
1154
|
+
};
|
|
1155
|
+
|
|
1023
1156
|
let broadcastQueued = false;
|
|
1024
1157
|
function broadcast() {
|
|
1025
1158
|
if (broadcastQueued) return;
|
|
1026
1159
|
broadcastQueued = true;
|
|
1027
1160
|
setTimeout(() => {
|
|
1028
1161
|
broadcastQueued = false;
|
|
1029
|
-
|
|
1030
|
-
for (const c of clients.keys()) c.write(msg);
|
|
1162
|
+
sendAll('state', snapshot());
|
|
1031
1163
|
}, 30);
|
|
1032
1164
|
}
|
|
1033
1165
|
|
|
@@ -1115,6 +1247,7 @@ const server = http.createServer(async (req, res) => {
|
|
|
1115
1247
|
clients.set(res, { id: url.searchParams.get('client') || '', focused: url.searchParams.get('focused') === '1' });
|
|
1116
1248
|
onAttentionChanged();
|
|
1117
1249
|
if (!usage || Date.now() - usage.fetchedAt > 60000) fetchUsage();
|
|
1250
|
+
if (Date.now() - updateCheckedAt > UPDATE_THROTTLE_MS) checkForUpdate();
|
|
1118
1251
|
const ping = setInterval(() => res.write(': ping\n\n'), 20000);
|
|
1119
1252
|
req.on('close', () => {
|
|
1120
1253
|
clearInterval(ping);
|
|
@@ -1141,6 +1274,12 @@ const server = http.createServer(async (req, res) => {
|
|
|
1141
1274
|
return json(res, 200, { apps: await findApps(s.cwd), detects: process.platform === 'win32' });
|
|
1142
1275
|
}
|
|
1143
1276
|
|
|
1277
|
+
if (req.method === 'GET' && url.pathname === '/api/memory') {
|
|
1278
|
+
const s = sessions.get(url.searchParams.get('session'));
|
|
1279
|
+
if (!s) return json(res, 404, { error: 'unknown session' });
|
|
1280
|
+
return json(res, 200, { files: readMemory(memoryContext(s)) });
|
|
1281
|
+
}
|
|
1282
|
+
|
|
1144
1283
|
if (req.method === 'POST' && url.pathname.startsWith('/api/')) {
|
|
1145
1284
|
const body = await readBody(req);
|
|
1146
1285
|
|
|
@@ -1148,7 +1287,44 @@ const server = http.createServer(async (req, res) => {
|
|
|
1148
1287
|
stopServer();
|
|
1149
1288
|
return json(res, 200, {});
|
|
1150
1289
|
}
|
|
1290
|
+
if (url.pathname === '/api/restart') {
|
|
1291
|
+
stopServer({ restart: true });
|
|
1292
|
+
return json(res, 200, {});
|
|
1293
|
+
}
|
|
1294
|
+
if (url.pathname === '/api/update') {
|
|
1295
|
+
if (!TERMINAL_ENABLED) return json(res, 400, { error: 'Run agenter update in a terminal.' });
|
|
1296
|
+
if (!updates.canSelfUpdate()) return json(res, 400, { error: 'This copy is a git clone. Update it with git pull.' });
|
|
1297
|
+
const id = terminalId(os.homedir());
|
|
1298
|
+
const term = terminals.open(id, os.homedir());
|
|
1299
|
+
if (term.running) return json(res, 409, { error: 'That terminal is busy. Wait for it to finish, or press Stop.' });
|
|
1300
|
+
const quote = (p) => `'${p.replace(/'/g, "''")}'`;
|
|
1301
|
+
terminals.run(id, `& ${quote(process.execPath)} ${quote(path.join(__dirname, 'cli.js'))} update`, checkForUpdate);
|
|
1302
|
+
return json(res, 200, { id });
|
|
1303
|
+
}
|
|
1304
|
+
if (url.pathname.startsWith('/api/terminal/')) {
|
|
1305
|
+
if (!TERMINAL_ENABLED) return json(res, 400, { error: 'The terminal is turned off or not available here.' });
|
|
1306
|
+
if (url.pathname === '/api/terminal/open') {
|
|
1307
|
+
const cwd = sessions.get(body.sessionId)?.cwd;
|
|
1308
|
+
const folder = cwd && fs.existsSync(cwd) ? cwd : os.homedir();
|
|
1309
|
+
return json(res, 200, terminals.open(terminalId(folder), folder));
|
|
1310
|
+
}
|
|
1311
|
+
const ok = url.pathname === '/api/terminal/run' ? terminals.run(body.id, String(body.input ?? '')) : url.pathname === '/api/terminal/stop' && terminals.stop(body.id);
|
|
1312
|
+
return json(res, ok ? 200 : 404, {});
|
|
1313
|
+
}
|
|
1314
|
+
if (url.pathname === '/api/pick-folder') {
|
|
1315
|
+
return json(res, 200, await pickFolder(body.start));
|
|
1316
|
+
}
|
|
1317
|
+
if (url.pathname === '/api/memory/save') {
|
|
1318
|
+
const s = sessions.get(body.sessionId);
|
|
1319
|
+
if (!s) return json(res, 404, { error: 'unknown session' });
|
|
1320
|
+
const { status, ...out } = saveMemory(memoryContext(s), body.path, body.text, body.mtime);
|
|
1321
|
+
return json(res, status, out);
|
|
1322
|
+
}
|
|
1151
1323
|
if (url.pathname === '/api/respond') {
|
|
1324
|
+
if (body.action === 'reply' && body.images?.length) {
|
|
1325
|
+
if (!pending.has(body.id)) return json(res, 404, {});
|
|
1326
|
+
body.message = withImages(body.message, saveImages(body.images));
|
|
1327
|
+
}
|
|
1152
1328
|
const s = sessions.get(pending.get(body.id)?.item.sessionId);
|
|
1153
1329
|
const ok = respond(body.id, body.action, body);
|
|
1154
1330
|
// Handing something back to VS Code: bring up that project's window so it's right there.
|
|
@@ -1169,16 +1345,19 @@ const server = http.createServer(async (req, res) => {
|
|
|
1169
1345
|
}
|
|
1170
1346
|
if (url.pathname === '/api/session/steer') {
|
|
1171
1347
|
const s = sessions.get(body.sessionId);
|
|
1172
|
-
const
|
|
1348
|
+
const hasImages = Array.isArray(body.images) && body.images.length > 0;
|
|
1173
1349
|
// A closed Claude session can only be continued by Agenter; Codex queues for its app either way.
|
|
1174
|
-
if (!s || !
|
|
1175
|
-
return json(res, 200, { result: await steer(s,
|
|
1350
|
+
if (!s || !(String(body.message || '').trim() || hasImages) || (s.agent !== 'codex' && driveOf(s) === 'vscode' && (s.status === 'dormant' || s.status === 'ended'))) return json(res, 404, {});
|
|
1351
|
+
return json(res, 200, { result: await steer(s, withImages(body.message, saveImages(body.images))) });
|
|
1176
1352
|
}
|
|
1177
1353
|
if (url.pathname === '/api/session/new') {
|
|
1178
1354
|
const cwd = String(body.cwd || '').trim().replace(/^"(.*)"$/, '$1');
|
|
1179
|
-
const
|
|
1355
|
+
const typed = String(body.message || '').trim();
|
|
1180
1356
|
const codex = body.agent === 'codex';
|
|
1181
|
-
if (!
|
|
1357
|
+
if (!typed && !body.images?.length) return json(res, 400, { error: `Type a first message for ${codex ? 'Codex' : 'Claude'}.` });
|
|
1358
|
+
const text = withImages(typed, saveImages(body.images));
|
|
1359
|
+
const carried = carryFrom(sessions.get(body.fromSessionId));
|
|
1360
|
+
const prompt = carried ? `${text}\n\n${carried}` : text;
|
|
1182
1361
|
let isDir = false;
|
|
1183
1362
|
try {
|
|
1184
1363
|
isDir = path.isAbsolute(cwd) && fs.statSync(cwd).isDirectory();
|
|
@@ -1187,9 +1366,9 @@ const server = http.createServer(async (req, res) => {
|
|
|
1187
1366
|
if (codex) {
|
|
1188
1367
|
if (!findCodex()) return json(res, 400, { error: "Couldn't find Codex. Install the Codex VS Code extension, or set AGENTER_CODEX_BIN to codex.exe." });
|
|
1189
1368
|
// Codex names the session itself, so it's registered once Codex has started it.
|
|
1190
|
-
const draft = Object.assign(newSession(''), { agent: 'codex', title:
|
|
1369
|
+
const draft = Object.assign(newSession(''), { agent: 'codex', title: (typed || 'Image').split('\n')[0].slice(0, 140), lastEventAt: Date.now() });
|
|
1191
1370
|
setCwd(draft, path.resolve(cwd));
|
|
1192
|
-
const started = await Promise.race([startCodexRun(draft,
|
|
1371
|
+
const started = await Promise.race([startCodexRun(draft, prompt, { fresh: true }), new Promise((r) => setTimeout(() => r({ error: 'Codex took too long to start.' }), 60000))]);
|
|
1193
1372
|
if (!started?.id) return json(res, 500, { error: `Codex couldn't start: ${started?.error || 'unknown error'}` });
|
|
1194
1373
|
settings.drive = { ...settings.drive, [started.id]: 'agenter' };
|
|
1195
1374
|
saveSettings();
|
|
@@ -1200,11 +1379,11 @@ const server = http.createServer(async (req, res) => {
|
|
|
1200
1379
|
if (!findClaude()) return json(res, 400, { error: "Couldn't find Claude Code. Install the VS Code extension, or set AGENTER_CLAUDE to claude.exe." });
|
|
1201
1380
|
const s = getOrCreateSession(crypto.randomUUID());
|
|
1202
1381
|
setCwd(s, path.resolve(cwd));
|
|
1203
|
-
s.title =
|
|
1382
|
+
s.title = (typed || 'Image').split('\n')[0].slice(0, 140);
|
|
1204
1383
|
s.lastEventAt = Date.now();
|
|
1205
1384
|
settings.drive = { ...settings.drive, [s.id]: 'agenter' };
|
|
1206
1385
|
saveSettings();
|
|
1207
|
-
if (!startRun(s,
|
|
1386
|
+
if (!startRun(s, prompt, { fresh: true })) {
|
|
1208
1387
|
sessions.delete(s.id);
|
|
1209
1388
|
return json(res, 500, { error: "Couldn't start Claude." });
|
|
1210
1389
|
}
|
|
@@ -1278,7 +1457,9 @@ const server = http.createServer(async (req, res) => {
|
|
|
1278
1457
|
server.requestTimeout = 0; // hooks can legitimately wait a long time for you
|
|
1279
1458
|
// Only listen when run directly; the tests load this file to drive the hook and steering logic.
|
|
1280
1459
|
if (require.main === module) {
|
|
1460
|
+
let portRetries = process.env.AGENTER_RESTART ? 50 : 0;
|
|
1281
1461
|
server.on('error', (err) => {
|
|
1462
|
+
if (err.code === 'EADDRINUSE' && portRetries-- > 0) return void setTimeout(() => server.listen(PORT, HOST), 200);
|
|
1282
1463
|
if (err.code === 'EADDRINUSE') console.error(`Port ${PORT} is busy. Is Agenter already running? (Set AGENTER_PORT to use another port.)`);
|
|
1283
1464
|
else console.error(err);
|
|
1284
1465
|
process.exit(1);
|
|
@@ -1286,12 +1467,19 @@ if (require.main === module) {
|
|
|
1286
1467
|
server.listen(PORT, HOST, () => {
|
|
1287
1468
|
const url = `http://localhost:${PORT}`;
|
|
1288
1469
|
console.log(`Agenter dashboard: ${url}`);
|
|
1470
|
+
delete process.env.AGENTER_RESTART;
|
|
1289
1471
|
fs.rmSync(STOPPED_FILE, { force: true });
|
|
1472
|
+
// Shortcuts start us inside the install folder, and on Windows a folder that's in use
|
|
1473
|
+
// can't be replaced by `npm i -g`. Everything here uses full paths.
|
|
1474
|
+
process.chdir(os.homedir());
|
|
1290
1475
|
startTray();
|
|
1291
1476
|
startDiscovery();
|
|
1477
|
+
pruneUploads();
|
|
1292
1478
|
startCodexDiscovery();
|
|
1293
1479
|
fetchUsage();
|
|
1294
1480
|
setInterval(fetchUsage, USAGE_INTERVAL_MS);
|
|
1481
|
+
checkForUpdate();
|
|
1482
|
+
setInterval(checkForUpdate, UPDATE_INTERVAL_MS);
|
|
1295
1483
|
if (!process.argv.includes('--no-open')) openDashboard(url);
|
|
1296
1484
|
});
|
|
1297
1485
|
}
|
|
@@ -1300,5 +1488,5 @@ module.exports = {
|
|
|
1300
1488
|
sessions, pending, clients, runners, dismissed,
|
|
1301
1489
|
get settings() { return settings; },
|
|
1302
1490
|
handleHook, hold, respond, steer, takeQueue, touchSession, snapshot, driveOf, effectiveHold,
|
|
1303
|
-
findClaude, findCodex, codexCommand, releaseRun,
|
|
1491
|
+
findClaude, findCodex, codexCommand, releaseRun, saveImages, withImages, UPLOADS_DIR,
|
|
1304
1492
|
};
|