@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.
@@ -70,7 +70,7 @@ class TrayManager {
70
70
  if (this.tray) return;
71
71
 
72
72
  this.tray = new Tray(this._icon);
73
- this.tray.setToolTip("GAIA Agent UI");
73
+ this.tray.setToolTip("GAIA");
74
74
 
75
75
  // Single-click: show/focus window
76
76
  this.tray.on("click", () => this._showWindow());
package/bin/gaia-ui.mjs DELETED
@@ -1,571 +0,0 @@
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.
15
-
16
- import { spawn, exec, execSync, spawnSync } from "child_process";
17
- import { dirname, join, extname, resolve } from "path";
18
- import { fileURLToPath } from "url";
19
- import { existsSync, readFileSync, mkdirSync } from "fs";
20
- import { readFile } from "fs/promises";
21
- import { createServer } from "http";
22
- import { homedir } from "os";
23
-
24
- const __filename = fileURLToPath(import.meta.url);
25
- const __dirname = dirname(__filename);
26
- const ROOT_DIR = join(__dirname, "..");
27
-
28
- const args = process.argv.slice(2);
29
-
30
- function getArg(name, defaultValue) {
31
- const idx = args.indexOf(name);
32
- if (idx === -1) return defaultValue;
33
- return args[idx + 1] || defaultValue;
34
- }
35
-
36
- const hasFlag = (name) => args.includes(name);
37
-
38
- const PORT = parseInt(getArg("--port", "4200"), 10);
39
- const SERVE_ONLY = hasFlag("--serve");
40
- const OPEN_BROWSER = !hasFlag("--no-open");
41
- const GAIA_VERSION_OVERRIDE = getArg("--gaia-version", null);
42
-
43
- // ── Paths ────────────────────────────────────────────────────────────────────
44
- const IS_WINDOWS = process.platform === "win32";
45
- const GAIA_HOME = join(homedir(), ".gaia");
46
- const GAIA_VENV = join(GAIA_HOME, "venv");
47
-
48
- // Display-friendly paths (use ~ instead of full home directory)
49
- const GAIA_VENV_DISPLAY = "~/.gaia/venv";
50
- const GAIA_BIN = IS_WINDOWS
51
- ? join(GAIA_VENV, "Scripts", "gaia.exe")
52
- : join(GAIA_VENV, "bin", "gaia");
53
-
54
-
55
- function readPkg() {
56
- try {
57
- return JSON.parse(readFileSync(join(ROOT_DIR, "package.json"), "utf-8"));
58
- } catch {
59
- return { version: "unknown" };
60
- }
61
- }
62
-
63
- function printHelp() {
64
- const pkg = readPkg();
65
- console.log(`
66
- GAIA - Run AI agents locally on your PC
67
- Version: ${pkg.version}
68
-
69
- Usage: gaia-ui [options]
70
-
71
- Options:
72
- --port <port> Backend port (default: 4200)
73
- --gaia-version <ver> Install a specific GAIA version (e.g. 0.16.1)
74
- --serve Serve frontend only (skip Python backend)
75
- --no-open Don't auto-open browser
76
- --help, -h Show this help
77
- --version, -v Show version
78
-
79
- On first run, GAIA automatically installs the Python backend
80
- (uv, Python 3.12, amd-gaia[ui]==${pkg.version}) into ~/.gaia/venv.
81
- On subsequent runs, it auto-updates if the version doesn't match.
82
-
83
- Update: npm install -g @amd-gaia/agent-ui@latest
84
- Uninstall: npm uninstall -g @amd-gaia/agent-ui && rm -rf ~/.gaia
85
-
86
- Documentation: https://amd-gaia.ai/guides/agent-ui
87
- `);
88
- }
89
-
90
- function printVersion() {
91
- const pkg = readPkg();
92
- console.log(`gaia-ui v${pkg.version}`);
93
- }
94
-
95
- /**
96
- * Check if a command exists on PATH.
97
- */
98
- function commandExists(cmd) {
99
- try {
100
- const check = IS_WINDOWS ? `where ${cmd}` : `which ${cmd}`;
101
- execSync(check, { stdio: "ignore" });
102
- return true;
103
- } catch {
104
- return false;
105
- }
106
- }
107
-
108
- /**
109
- * Find the gaia binary - check venv first, then PATH.
110
- * Returns the path to the gaia executable, or null if not found.
111
- */
112
- function findGaiaBin() {
113
- // Check the managed venv first
114
- if (existsSync(GAIA_BIN)) {
115
- return GAIA_BIN;
116
- }
117
- // Fall back to PATH
118
- if (commandExists("gaia")) {
119
- return "gaia";
120
- }
121
- return null;
122
- }
123
-
124
- /**
125
- * Ensure uv is available. Install it if not found.
126
- */
127
- function ensureUv() {
128
- if (commandExists("uv")) return;
129
-
130
- console.log("Installing uv (Python package manager)...");
131
-
132
- let result;
133
- if (IS_WINDOWS) {
134
- result = spawnSync(
135
- "powershell",
136
- ["-ExecutionPolicy", "Bypass", "-Command", "irm https://astral.sh/uv/install.ps1 | iex"],
137
- { stdio: "inherit", env: { ...process.env } }
138
- );
139
- } else {
140
- result = spawnSync(
141
- "bash",
142
- ["-c", "curl -LsSf https://astral.sh/uv/install.sh | sh"],
143
- { stdio: "inherit", env: { ...process.env } }
144
- );
145
- }
146
-
147
- if (result.status !== 0 || !commandExists("uv")) {
148
- console.error("");
149
- console.error("Could not install uv automatically.");
150
- console.error("This can happen behind corporate proxies or on restricted systems.");
151
- console.error("");
152
- console.error("Install uv manually, then re-run gaia-ui:");
153
- if (IS_WINDOWS) {
154
- console.error(" powershell -c \"irm https://astral.sh/uv/install.ps1 | iex\"");
155
- } else {
156
- console.error(" curl -LsSf https://astral.sh/uv/install.sh | sh");
157
- }
158
- console.error("");
159
- console.error("Or install the GAIA backend manually:");
160
- console.error(" https://amd-gaia.ai/quickstart#cli-install");
161
- process.exit(1);
162
- }
163
- }
164
-
165
- /**
166
- * Install the exact GAIA Python backend version that matches this npm package.
167
- * Uses uv to create a venv and install the pinned amd-gaia[ui] package.
168
- */
169
- function installBackend() {
170
- const pkg = readPkg();
171
- const gaiaVersion = GAIA_VERSION_OVERRIDE || pkg.version;
172
- const pipPackage = `amd-gaia[ui]==${gaiaVersion}`;
173
-
174
- console.log("========================================");
175
- console.log(" First-time setup: Installing GAIA backend");
176
- console.log("========================================");
177
- console.log("");
178
- console.log(` Package: ${pipPackage}`);
179
- console.log(` Location: ${GAIA_VENV_DISPLAY}`);
180
- console.log("");
181
-
182
- // Step 1: Ensure uv is available
183
- ensureUv();
184
-
185
- // Step 2: Create venv if it doesn't exist
186
- if (!existsSync(GAIA_VENV)) {
187
- console.log("Creating Python environment...");
188
- mkdirSync(GAIA_HOME, { recursive: true });
189
-
190
- const venvResult = spawnSync("uv", ["venv", GAIA_VENV, "--python", "3.12"], {
191
- stdio: "inherit",
192
- });
193
-
194
- if (venvResult.status !== 0) {
195
- console.error("");
196
- console.error("Failed to create Python environment.");
197
- console.error("This may happen if Python 3.12 could not be downloaded.");
198
- console.error("");
199
- console.error("Try creating it manually, then re-run gaia-ui:");
200
- console.error(` uv venv ${GAIA_VENV_DISPLAY} --python 3.12`);
201
- console.error("");
202
- console.error("Full manual install: https://amd-gaia.ai/quickstart#cli-install");
203
- process.exit(1);
204
- }
205
- }
206
-
207
- // Step 3: Install pinned amd-gaia[ui] into the venv
208
- console.log(`Installing ${pipPackage}...`);
209
-
210
- const pipArgs = ["pip", "install", pipPackage, "--refresh", "--python", join(GAIA_VENV, IS_WINDOWS ? "Scripts/python.exe" : "bin/python")];
211
-
212
- // Linux: use CPU-only PyTorch to avoid large CUDA packages
213
- if (!IS_WINDOWS) {
214
- pipArgs.push("--extra-index-url", "https://download.pytorch.org/whl/cpu");
215
- }
216
-
217
- const installResult = spawnSync("uv", pipArgs, {
218
- stdio: "inherit",
219
- env: { ...process.env },
220
- });
221
-
222
- if (installResult.status !== 0) {
223
- console.error("");
224
- console.error(`Failed to install ${pipPackage}.`);
225
- console.error("This can happen if the version is not available on PyPI or due to network issues.");
226
- console.error("");
227
- console.error("Try installing manually, then re-run gaia-ui:");
228
- const pythonBinDisplay = IS_WINDOWS ? `${GAIA_VENV_DISPLAY}/Scripts/python.exe` : `${GAIA_VENV_DISPLAY}/bin/python`;
229
- console.error(` uv pip install ${pipPackage} --python ${pythonBinDisplay}`);
230
- console.error("");
231
- console.error("Full manual install: https://amd-gaia.ai/quickstart#cli-install");
232
- process.exit(1);
233
- }
234
-
235
- // Verify the install worked
236
- if (!existsSync(GAIA_BIN)) {
237
- console.error("");
238
- console.error(`Expected gaia binary at ${GAIA_VENV_DISPLAY} after installation, but not found.`);
239
- console.error("");
240
- console.error("Try installing manually: https://amd-gaia.ai/quickstart#cli-install");
241
- process.exit(1);
242
- }
243
-
244
- console.log("");
245
- console.log("Backend installed successfully!");
246
- console.log("");
247
-
248
- // Run gaia init to install Lemonade Server and download models
249
- console.log("Setting up Lemonade Server and downloading models...");
250
- console.log("(This may take a few minutes on first run)");
251
- console.log("");
252
-
253
- const initResult = spawnSync(GAIA_BIN, ["init", "--profile", "minimal"], {
254
- stdio: "inherit",
255
- env: { ...process.env },
256
- });
257
-
258
- if (initResult.status !== 0) {
259
- console.log("");
260
- console.log("Warning: gaia init did not complete successfully.");
261
- console.log("You can run it manually later: gaia init --profile minimal");
262
- console.log("");
263
- }
264
- }
265
-
266
- /**
267
- * Get the installed Python gaia version by running `gaia --version`.
268
- * Returns the version string (e.g. "0.17.0") or null if unknown.
269
- */
270
- function getInstalledVersion(gaiaBin) {
271
- try {
272
- const result = spawnSync(gaiaBin, ["--version"], {
273
- stdio: ["ignore", "pipe", "pipe"],
274
- timeout: 5000,
275
- });
276
- if (result.status === 0 && result.stdout) {
277
- // Output may be "0.17.0" or "gaia 0.17.0" — extract the version number
278
- const match = result.stdout.toString().trim().match(/(\d+\.\d+\.\d+)/);
279
- return match ? match[1] : null;
280
- }
281
- } catch {
282
- // ignore
283
- }
284
- return null;
285
- }
286
-
287
- /**
288
- * Ensure the GAIA Python backend is available and matches the expected version.
289
- * Installs or upgrades automatically if needed.
290
- */
291
- function ensureBackend() {
292
- const pkg = readPkg();
293
- const expectedVersion = GAIA_VERSION_OVERRIDE || pkg.version;
294
-
295
- const gaiaBin = findGaiaBin();
296
- if (gaiaBin) {
297
- // Check if the installed version matches
298
- const installedVersion = getInstalledVersion(gaiaBin);
299
- if (installedVersion === expectedVersion) {
300
- return gaiaBin;
301
- }
302
-
303
- // Version mismatch — upgrade
304
- if (installedVersion) {
305
- console.log(`Updating GAIA backend: ${installedVersion} → ${expectedVersion}`);
306
- }
307
- installBackend();
308
-
309
- const upgraded = findGaiaBin();
310
- if (upgraded) return upgraded;
311
- } else {
312
- // Not found — install from scratch
313
- installBackend();
314
- }
315
-
316
- // Re-check after install
317
- const installed = findGaiaBin();
318
- if (!installed) {
319
- console.error("Error: GAIA backend not found after installation.");
320
- console.error("");
321
- console.error("Try installing manually:");
322
- console.error(" https://amd-gaia.ai/quickstart");
323
- process.exit(1);
324
- }
325
- return installed;
326
- }
327
-
328
- /**
329
- * Wait for a URL to respond with 200.
330
- */
331
- async function waitForServer(url, timeoutMs = 30000) {
332
- const start = Date.now();
333
- while (Date.now() - start < timeoutMs) {
334
- try {
335
- const response = await fetch(url);
336
- if (response.ok) return true;
337
- } catch {
338
- // Server not ready yet
339
- }
340
- await new Promise((r) => setTimeout(r, 500));
341
- }
342
- return false;
343
- }
344
-
345
- /**
346
- * Open a URL in the default browser.
347
- */
348
- function openBrowser(url) {
349
- const platform = process.platform;
350
- let cmd;
351
- if (platform === "win32") {
352
- cmd = `start "" "${url}"`;
353
- } else if (platform === "darwin") {
354
- cmd = `open "${url}"`;
355
- } else {
356
- cmd = `xdg-open "${url}"`;
357
- }
358
- exec(cmd, (err) => {
359
- if (err) {
360
- console.log(` Open manually: ${url}`);
361
- }
362
- });
363
- }
364
-
365
- /**
366
- * Start the Python backend.
367
- * Uses "chat --ui" for compatibility with all gaia versions.
368
- */
369
- function startBackend(gaiaBin, port) {
370
- console.log(`Starting GAIA backend on port ${port}...`);
371
-
372
- const child = spawn(gaiaBin, ["chat", "--ui", "--ui-port", String(port), "--ui-dist", join(ROOT_DIR, "dist")], {
373
- stdio: ["ignore", "pipe", "pipe"],
374
- detached: false,
375
- });
376
-
377
- child.stdout.on("data", (data) => {
378
- const line = data.toString().trim();
379
- if (line) console.log(` [backend] ${line}`);
380
- });
381
-
382
- child.stderr.on("data", (data) => {
383
- const line = data.toString().trim();
384
- if (line) console.log(` [backend] ${line}`);
385
- });
386
-
387
- child.on("error", (err) => {
388
- console.error(`Failed to start backend: ${err.message}`);
389
- process.exit(1);
390
- });
391
-
392
- child.on("exit", (code) => {
393
- if (code !== 0 && code !== null) {
394
- console.error(`Backend exited with code ${code}`);
395
- }
396
- });
397
-
398
- return child;
399
- }
400
-
401
- /**
402
- * Serve the pre-built frontend with a lightweight Node.js HTTP server.
403
- */
404
- async function serveFrontend(port) {
405
- const distDir = join(ROOT_DIR, "dist");
406
-
407
- if (!existsSync(join(distDir, "index.html"))) {
408
- console.error("Error: Frontend build not found.");
409
- console.error(`Expected: ${join(distDir, "index.html")}`);
410
- console.error("");
411
- console.error("The npm package may be corrupted. Try reinstalling:");
412
- console.error(" npm install -g @amd-gaia/agent-ui@latest");
413
- process.exit(1);
414
- }
415
-
416
- const MIME_TYPES = {
417
- ".html": "text/html",
418
- ".js": "application/javascript",
419
- ".css": "text/css",
420
- ".json": "application/json",
421
- ".png": "image/png",
422
- ".jpg": "image/jpeg",
423
- ".svg": "image/svg+xml",
424
- ".ico": "image/x-icon",
425
- ".woff": "font/woff",
426
- ".woff2": "font/woff2",
427
- ".ttf": "font/ttf",
428
- };
429
-
430
- /**
431
- * Sanitize a URL path and return a safe file path within distDir.
432
- * Returns the index.html path for invalid or non-file requests (SPA fallback).
433
- */
434
- function safeLookup(urlPath) {
435
- const indexPath = join(distDir, "index.html");
436
-
437
- // Reject null bytes
438
- if (urlPath.includes("\0")) return indexPath;
439
-
440
- // Reject path traversal patterns before any path operations
441
- if (urlPath.includes("..")) return indexPath;
442
-
443
- // Only allow safe characters in URL path
444
- if (!/^[a-zA-Z0-9._\-/]+$/.test(urlPath)) return indexPath;
445
-
446
- const candidate = resolve(distDir, "." + urlPath);
447
- const resolvedDistDir = resolve(distDir);
448
-
449
- // Verify the resolved path is within the dist directory.
450
- // Use path.sep for cross-platform safety (Windows uses "\", Unix uses "/").
451
- const sep = resolvedDistDir.includes("\\") ? "\\" : "/";
452
- if (!candidate.startsWith(resolvedDistDir + sep) && candidate !== resolvedDistDir) {
453
- return indexPath;
454
- }
455
-
456
- // Check the file exists and has an extension (not a directory)
457
- if (!existsSync(candidate) || !extname(candidate)) {
458
- return indexPath;
459
- }
460
-
461
- return candidate;
462
- }
463
-
464
- const server = createServer(async (req, res) => {
465
- // Strip query strings
466
- const urlPath = req.url.split("?")[0];
467
-
468
- // Resolve to a safe file path within distDir (never returns paths outside distDir)
469
- const safePath = urlPath === "/" ? join(distDir, "index.html") : safeLookup(urlPath);
470
-
471
- try {
472
- const data = await readFile(safePath);
473
- const ext = extname(safePath);
474
- res.writeHead(200, {
475
- "Content-Type": MIME_TYPES[ext] || "application/octet-stream",
476
- });
477
- res.end(data);
478
- } catch {
479
- res.writeHead(404);
480
- res.end("Not found");
481
- }
482
- });
483
-
484
- server.listen(port, () => {
485
- console.log(`GAIA Agent UI serving at http://localhost:${port}`);
486
- });
487
-
488
- return server;
489
- }
490
-
491
- // ── Main ──────────────────────────────────────────────────────────────────────
492
-
493
- if (hasFlag("--help") || hasFlag("-h")) {
494
- printHelp();
495
- process.exit(0);
496
- }
497
-
498
- if (hasFlag("--version") || hasFlag("-v")) {
499
- printVersion();
500
- process.exit(0);
501
- }
502
-
503
- const pkg = readPkg();
504
- console.log("");
505
- console.log("========================================");
506
- console.log(` GAIA Agent UI v${pkg.version}`);
507
- console.log("========================================");
508
- console.log("");
509
-
510
- let backendProcess = null;
511
-
512
- if (SERVE_ONLY) {
513
- // Serve-only mode: just serve the frontend static files
514
- console.log("Mode: Frontend-only (--serve)");
515
- console.log(`Port: ${PORT}`);
516
- console.log("");
517
-
518
- await serveFrontend(PORT);
519
-
520
- if (OPEN_BROWSER) {
521
- openBrowser(`http://localhost:${PORT}`);
522
- }
523
- } else {
524
- // Full mode: ensure backend is installed, start it, open browser
525
- const gaiaBin = ensureBackend();
526
-
527
- backendProcess = startBackend(gaiaBin, PORT);
528
-
529
- // Wait for the backend to be ready
530
- console.log("Waiting for backend to start...");
531
- const ready = await waitForServer(
532
- `http://localhost:${PORT}/api/health`,
533
- 30000
534
- );
535
-
536
- if (ready) {
537
- console.log("Backend is ready!");
538
- console.log("");
539
- console.log(` Open: http://localhost:${PORT}`);
540
- console.log("");
541
-
542
- if (OPEN_BROWSER) {
543
- openBrowser(`http://localhost:${PORT}`);
544
- }
545
- } else {
546
- console.log("WARNING: Backend did not respond within 30 seconds.");
547
- console.log(` Try opening manually: http://localhost:${PORT}`);
548
- console.log("");
549
- }
550
- }
551
-
552
- // Graceful shutdown
553
- function cleanup() {
554
- if (backendProcess) {
555
- console.log("\nShutting down GAIA...");
556
- backendProcess.kill("SIGTERM");
557
- setTimeout(() => {
558
- try {
559
- backendProcess.kill("SIGKILL");
560
- } catch {
561
- // Already dead
562
- }
563
- process.exit(0);
564
- }, 3000);
565
- } else {
566
- process.exit(0);
567
- }
568
- }
569
-
570
- process.on("SIGINT", cleanup);
571
- process.on("SIGTERM", cleanup);