@amd-gaia/agent-ui 0.17.1 → 0.17.3
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/gaia-ui.cjs +370 -0
- package/dist/assets/index-B4Qzv7Ys.js +443 -0
- package/dist/assets/index-eQemgF08.css +1 -0
- package/dist/index.html +3 -3
- package/main.cjs +209 -54
- package/package.json +8 -11
- package/preload.cjs +42 -0
- package/services/agent-seeder.cjs +301 -0
- package/services/auto-updater.cjs +437 -0
- package/services/backend-installer-progress-dialog.cjs +429 -0
- package/services/backend-installer.cjs +1082 -0
- package/services/tray-manager.cjs +1 -1
- package/bin/gaia-ui.mjs +0 -571
- package/dist/assets/index-DFaWywBV.js +0 -432
- package/dist/assets/index-TyWv9Ej0.css +0 -1
package/bin/gaia-ui.cjs
ADDED
|
@@ -0,0 +1,370 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// Copyright(C) 2025-2026 Advanced Micro Devices, Inc. All rights reserved.
|
|
4
|
+
// SPDX-License-Identifier: MIT
|
|
5
|
+
|
|
6
|
+
// GAIA Agent UI CLI
|
|
7
|
+
// Usage:
|
|
8
|
+
// gaia-ui Start the app (backend + browser)
|
|
9
|
+
// gaia-ui --serve Serve frontend only (no backend auto-start)
|
|
10
|
+
// gaia-ui --port 4200 Custom backend port
|
|
11
|
+
// gaia-ui --help Show help
|
|
12
|
+
//
|
|
13
|
+
// On first run, automatically installs the Python backend (amd-gaia[ui])
|
|
14
|
+
// using uv + Python 3.12 via the shared services/backend-installer.cjs
|
|
15
|
+
// module. That same module is used by main.cjs for the Electron app's
|
|
16
|
+
// first-run bootstrap.
|
|
17
|
+
//
|
|
18
|
+
// NOTE: This is .cjs (CommonJS) — package.json has `"type": "module"`, so
|
|
19
|
+
// .js files are ESM by default. The shared install module is pure CommonJS
|
|
20
|
+
// and is require()'d from both bin/gaia-ui.cjs and main.cjs.
|
|
21
|
+
|
|
22
|
+
"use strict";
|
|
23
|
+
|
|
24
|
+
const { spawn, exec } = require("child_process");
|
|
25
|
+
const path = require("path");
|
|
26
|
+
const fs = require("fs");
|
|
27
|
+
const { createServer } = require("http");
|
|
28
|
+
const { readFile } = require("fs/promises");
|
|
29
|
+
|
|
30
|
+
const installer = require("../services/backend-installer.cjs");
|
|
31
|
+
|
|
32
|
+
const ROOT_DIR = path.join(__dirname, "..");
|
|
33
|
+
|
|
34
|
+
const args = process.argv.slice(2);
|
|
35
|
+
|
|
36
|
+
function getArg(name, defaultValue) {
|
|
37
|
+
const idx = args.indexOf(name);
|
|
38
|
+
if (idx === -1) return defaultValue;
|
|
39
|
+
return args[idx + 1] || defaultValue;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const hasFlag = (name) => args.includes(name);
|
|
43
|
+
|
|
44
|
+
const PORT = parseInt(getArg("--port", "4200"), 10);
|
|
45
|
+
const SERVE_ONLY = hasFlag("--serve");
|
|
46
|
+
const OPEN_BROWSER = !hasFlag("--no-open");
|
|
47
|
+
const GAIA_VERSION_OVERRIDE = getArg("--gaia-version", null);
|
|
48
|
+
|
|
49
|
+
function readPkg() {
|
|
50
|
+
try {
|
|
51
|
+
return JSON.parse(
|
|
52
|
+
fs.readFileSync(path.join(ROOT_DIR, "package.json"), "utf-8")
|
|
53
|
+
);
|
|
54
|
+
} catch {
|
|
55
|
+
return { version: "unknown" };
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function printHelp() {
|
|
60
|
+
const pkg = readPkg();
|
|
61
|
+
console.log(`
|
|
62
|
+
GAIA - Run AI agents locally on your PC
|
|
63
|
+
Version: ${pkg.version}
|
|
64
|
+
|
|
65
|
+
Usage: gaia-ui [options]
|
|
66
|
+
|
|
67
|
+
Options:
|
|
68
|
+
--port <port> Backend port (default: 4200)
|
|
69
|
+
--gaia-version <ver> Install a specific GAIA version (e.g. 0.16.1)
|
|
70
|
+
--serve Serve frontend only (skip Python backend)
|
|
71
|
+
--no-open Don't auto-open browser
|
|
72
|
+
--help, -h Show this help
|
|
73
|
+
--version, -v Show version
|
|
74
|
+
|
|
75
|
+
On first run, GAIA automatically installs the Python backend
|
|
76
|
+
(uv, Python 3.12, amd-gaia[ui]==${pkg.version}) into ~/.gaia/venv.
|
|
77
|
+
On subsequent runs, it auto-updates if the version doesn't match.
|
|
78
|
+
|
|
79
|
+
Logs: ~/.gaia/electron-install.log
|
|
80
|
+
|
|
81
|
+
Update: npm install -g @amd-gaia/agent-ui@latest
|
|
82
|
+
Uninstall: npm uninstall -g @amd-gaia/agent-ui && rm -rf ~/.gaia
|
|
83
|
+
|
|
84
|
+
Documentation: https://amd-gaia.ai/guides/agent-ui
|
|
85
|
+
`);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function printVersion() {
|
|
89
|
+
const pkg = readPkg();
|
|
90
|
+
console.log(`gaia-ui v${pkg.version}`);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// ── Backend launch ──────────────────────────────────────────────────────────
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Wait for a URL to respond with 200.
|
|
97
|
+
*/
|
|
98
|
+
async function waitForServer(url, timeoutMs = 30000) {
|
|
99
|
+
const start = Date.now();
|
|
100
|
+
while (Date.now() - start < timeoutMs) {
|
|
101
|
+
try {
|
|
102
|
+
const response = await fetch(url);
|
|
103
|
+
if (response.ok) return true;
|
|
104
|
+
} catch {
|
|
105
|
+
// Server not ready yet
|
|
106
|
+
}
|
|
107
|
+
await new Promise((r) => setTimeout(r, 500));
|
|
108
|
+
}
|
|
109
|
+
return false;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Open a URL in the default browser.
|
|
114
|
+
*/
|
|
115
|
+
function openBrowser(url) {
|
|
116
|
+
const platform = process.platform;
|
|
117
|
+
let cmd;
|
|
118
|
+
if (platform === "win32") {
|
|
119
|
+
cmd = `start "" "${url}"`;
|
|
120
|
+
} else if (platform === "darwin") {
|
|
121
|
+
cmd = `open "${url}"`;
|
|
122
|
+
} else {
|
|
123
|
+
cmd = `xdg-open "${url}"`;
|
|
124
|
+
}
|
|
125
|
+
exec(cmd, (err) => {
|
|
126
|
+
if (err) {
|
|
127
|
+
console.log(` Open manually: ${url}`);
|
|
128
|
+
}
|
|
129
|
+
});
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Start the Python backend.
|
|
134
|
+
* Uses "chat --ui" for compatibility with all gaia versions.
|
|
135
|
+
*/
|
|
136
|
+
function startBackend(gaiaBin, port) {
|
|
137
|
+
console.log(`Starting GAIA backend on port ${port}...`);
|
|
138
|
+
|
|
139
|
+
const child = spawn(
|
|
140
|
+
gaiaBin,
|
|
141
|
+
[
|
|
142
|
+
"chat",
|
|
143
|
+
"--ui",
|
|
144
|
+
"--ui-port",
|
|
145
|
+
String(port),
|
|
146
|
+
"--ui-dist",
|
|
147
|
+
path.join(ROOT_DIR, "dist"),
|
|
148
|
+
],
|
|
149
|
+
{
|
|
150
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
151
|
+
detached: false,
|
|
152
|
+
}
|
|
153
|
+
);
|
|
154
|
+
|
|
155
|
+
child.stdout.on("data", (data) => {
|
|
156
|
+
const line = data.toString().trim();
|
|
157
|
+
if (line) console.log(` [backend] ${line}`);
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
child.stderr.on("data", (data) => {
|
|
161
|
+
const line = data.toString().trim();
|
|
162
|
+
if (line) console.log(` [backend] ${line}`);
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
child.on("error", (err) => {
|
|
166
|
+
console.error(`Failed to start backend: ${err.message}`);
|
|
167
|
+
process.exit(1);
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
child.on("exit", (code) => {
|
|
171
|
+
if (code !== 0 && code !== null) {
|
|
172
|
+
console.error(`Backend exited with code ${code}`);
|
|
173
|
+
}
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
return child;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/**
|
|
180
|
+
* Serve the pre-built frontend with a lightweight Node.js HTTP server.
|
|
181
|
+
*/
|
|
182
|
+
async function serveFrontend(port) {
|
|
183
|
+
const distDir = path.join(ROOT_DIR, "dist");
|
|
184
|
+
|
|
185
|
+
if (!fs.existsSync(path.join(distDir, "index.html"))) {
|
|
186
|
+
console.error("Error: Frontend build not found.");
|
|
187
|
+
console.error(`Expected: ${path.join(distDir, "index.html")}`);
|
|
188
|
+
console.error("");
|
|
189
|
+
console.error("The npm package may be corrupted. Try reinstalling:");
|
|
190
|
+
console.error(" npm install -g @amd-gaia/agent-ui@latest");
|
|
191
|
+
process.exit(1);
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
const MIME_TYPES = {
|
|
195
|
+
".html": "text/html",
|
|
196
|
+
".js": "application/javascript",
|
|
197
|
+
".css": "text/css",
|
|
198
|
+
".json": "application/json",
|
|
199
|
+
".png": "image/png",
|
|
200
|
+
".jpg": "image/jpeg",
|
|
201
|
+
".svg": "image/svg+xml",
|
|
202
|
+
".ico": "image/x-icon",
|
|
203
|
+
".woff": "font/woff",
|
|
204
|
+
".woff2": "font/woff2",
|
|
205
|
+
".ttf": "font/ttf",
|
|
206
|
+
};
|
|
207
|
+
|
|
208
|
+
/**
|
|
209
|
+
* Sanitize a URL path and return a safe file path within distDir.
|
|
210
|
+
* Returns the index.html path for invalid or non-file requests (SPA fallback).
|
|
211
|
+
*/
|
|
212
|
+
function safeLookup(urlPath) {
|
|
213
|
+
const indexPath = path.join(distDir, "index.html");
|
|
214
|
+
|
|
215
|
+
if (urlPath.includes("\0")) return indexPath;
|
|
216
|
+
if (urlPath.includes("..")) return indexPath;
|
|
217
|
+
if (!/^[a-zA-Z0-9._\-/]+$/.test(urlPath)) return indexPath;
|
|
218
|
+
|
|
219
|
+
const candidate = path.resolve(distDir, "." + urlPath);
|
|
220
|
+
const resolvedDistDir = path.resolve(distDir);
|
|
221
|
+
|
|
222
|
+
const sep = resolvedDistDir.includes("\\") ? "\\" : "/";
|
|
223
|
+
if (
|
|
224
|
+
!candidate.startsWith(resolvedDistDir + sep) &&
|
|
225
|
+
candidate !== resolvedDistDir
|
|
226
|
+
) {
|
|
227
|
+
return indexPath;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
if (!fs.existsSync(candidate) || !path.extname(candidate)) {
|
|
231
|
+
return indexPath;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
return candidate;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
const server = createServer(async (req, res) => {
|
|
238
|
+
const urlPath = req.url.split("?")[0];
|
|
239
|
+
const safePath =
|
|
240
|
+
urlPath === "/" ? path.join(distDir, "index.html") : safeLookup(urlPath);
|
|
241
|
+
|
|
242
|
+
try {
|
|
243
|
+
const data = await readFile(safePath);
|
|
244
|
+
const ext = path.extname(safePath);
|
|
245
|
+
res.writeHead(200, {
|
|
246
|
+
"Content-Type": MIME_TYPES[ext] || "application/octet-stream",
|
|
247
|
+
});
|
|
248
|
+
res.end(data);
|
|
249
|
+
} catch {
|
|
250
|
+
res.writeHead(404);
|
|
251
|
+
res.end("Not found");
|
|
252
|
+
}
|
|
253
|
+
});
|
|
254
|
+
|
|
255
|
+
server.listen(port, () => {
|
|
256
|
+
console.log(`GAIA Agent UI serving at http://localhost:${port}`);
|
|
257
|
+
});
|
|
258
|
+
|
|
259
|
+
return server;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
// ── Main ──────────────────────────────────────────────────────────────────────
|
|
263
|
+
|
|
264
|
+
async function main() {
|
|
265
|
+
if (hasFlag("--help") || hasFlag("-h")) {
|
|
266
|
+
printHelp();
|
|
267
|
+
process.exit(0);
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
if (hasFlag("--version") || hasFlag("-v")) {
|
|
271
|
+
printVersion();
|
|
272
|
+
process.exit(0);
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
const pkg = readPkg();
|
|
276
|
+
console.log("");
|
|
277
|
+
console.log("========================================");
|
|
278
|
+
console.log(` GAIA Agent UI v${pkg.version}`);
|
|
279
|
+
console.log("========================================");
|
|
280
|
+
console.log("");
|
|
281
|
+
|
|
282
|
+
let backendProcess = null;
|
|
283
|
+
|
|
284
|
+
if (SERVE_ONLY) {
|
|
285
|
+
console.log("Mode: Frontend-only (--serve)");
|
|
286
|
+
console.log(`Port: ${PORT}`);
|
|
287
|
+
console.log("");
|
|
288
|
+
|
|
289
|
+
await serveFrontend(PORT);
|
|
290
|
+
|
|
291
|
+
if (OPEN_BROWSER) {
|
|
292
|
+
openBrowser(`http://localhost:${PORT}`);
|
|
293
|
+
}
|
|
294
|
+
} else {
|
|
295
|
+
// Full mode: ensure backend is installed, start it, open browser.
|
|
296
|
+
// `ensureBackend` uses the shared install module — same code path as
|
|
297
|
+
// the Electron app — and writes logs to ~/.gaia/electron-install.log.
|
|
298
|
+
let gaiaBin;
|
|
299
|
+
try {
|
|
300
|
+
gaiaBin = await installer.ensureBackend({
|
|
301
|
+
version: GAIA_VERSION_OVERRIDE || undefined,
|
|
302
|
+
onProgress: (stage, percent, message) => {
|
|
303
|
+
// Simple CLI progress formatting
|
|
304
|
+
process.stdout.write(
|
|
305
|
+
` [${stage}] ${percent}% ${message}\n`
|
|
306
|
+
);
|
|
307
|
+
},
|
|
308
|
+
});
|
|
309
|
+
} catch (err) {
|
|
310
|
+
console.error("");
|
|
311
|
+
console.error(`Install failed: ${err.message}`);
|
|
312
|
+
if (err.suggestion) {
|
|
313
|
+
console.error("");
|
|
314
|
+
console.error(err.suggestion);
|
|
315
|
+
}
|
|
316
|
+
console.error("");
|
|
317
|
+
console.error(`See log: ${installer.getLogPath()}`);
|
|
318
|
+
process.exit(1);
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
backendProcess = startBackend(gaiaBin, PORT);
|
|
322
|
+
|
|
323
|
+
console.log("Waiting for backend to start...");
|
|
324
|
+
const ready = await waitForServer(
|
|
325
|
+
`http://localhost:${PORT}/api/health`,
|
|
326
|
+
30000
|
|
327
|
+
);
|
|
328
|
+
|
|
329
|
+
if (ready) {
|
|
330
|
+
console.log("Backend is ready!");
|
|
331
|
+
console.log("");
|
|
332
|
+
console.log(` Open: http://localhost:${PORT}`);
|
|
333
|
+
console.log("");
|
|
334
|
+
|
|
335
|
+
if (OPEN_BROWSER) {
|
|
336
|
+
openBrowser(`http://localhost:${PORT}`);
|
|
337
|
+
}
|
|
338
|
+
} else {
|
|
339
|
+
console.log("WARNING: Backend did not respond within 30 seconds.");
|
|
340
|
+
console.log(` Try opening manually: http://localhost:${PORT}`);
|
|
341
|
+
console.log("");
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
// Graceful shutdown
|
|
346
|
+
function cleanup() {
|
|
347
|
+
if (backendProcess) {
|
|
348
|
+
console.log("\nShutting down GAIA...");
|
|
349
|
+
backendProcess.kill("SIGTERM");
|
|
350
|
+
setTimeout(() => {
|
|
351
|
+
try {
|
|
352
|
+
backendProcess.kill("SIGKILL");
|
|
353
|
+
} catch {
|
|
354
|
+
// Already dead
|
|
355
|
+
}
|
|
356
|
+
process.exit(0);
|
|
357
|
+
}, 3000);
|
|
358
|
+
} else {
|
|
359
|
+
process.exit(0);
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
process.on("SIGINT", cleanup);
|
|
364
|
+
process.on("SIGTERM", cleanup);
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
main().catch((err) => {
|
|
368
|
+
console.error("Fatal error:", err && err.stack ? err.stack : err);
|
|
369
|
+
process.exit(1);
|
|
370
|
+
});
|