@micsushi/agent-hotline 1.0.0 → 1.0.1

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@micsushi/agent-hotline",
3
- "version": "1.0.0",
3
+ "version": "1.0.1",
4
4
  "description": "Local read-aloud hooks and tray app for AI coding agents.",
5
5
  "bin": {
6
6
  "ah": "packages/backend/bin/agent-hotline.js",
@@ -14,6 +14,7 @@
14
14
  "packages/backend/bin/",
15
15
  "packages/backend/src/",
16
16
  "packages/backend/skills/",
17
+ "packages/backend/web/",
17
18
  "packages/backend/package.json"
18
19
  ],
19
20
  "publishConfig": {
@@ -33,6 +34,8 @@
33
34
  "install-hook": "node scripts/install-hook.js",
34
35
  "install-skill": "node scripts/install-skill.js",
35
36
  "install-hotline": "node packages/backend/bin/agent-hotline.js install",
37
+ "stage-web": "node scripts/stage-web.mjs",
38
+ "prepack": "node scripts/stage-web.mjs",
36
39
  "test": "npm --prefix packages/backend test && npm --workspace @agent-hotline/desktop run test",
37
40
  "lint": "eslint . && npm run rust:clippy",
38
41
  "lint:fix": "eslint . --fix",
@@ -463,6 +463,68 @@ function getPathname(req) {
463
463
  return new URL(req.url, "http://127.0.0.1").pathname;
464
464
  }
465
465
 
466
+ // Built desktop UI, served so `agent-hotline run` opens the full app in a
467
+ // browser. The npm package ships a pruned copy under web/ (no heavy local-TTS
468
+ // assets); a source checkout uses the full desktop/dist build. Absent in either
469
+ // case, routes fall back to the inline page() console.
470
+ const WEB_CANDIDATES = [
471
+ path.resolve(__dirname, "../web"),
472
+ path.resolve(__dirname, "../../desktop/dist")
473
+ ];
474
+
475
+ function webDir() {
476
+ for (const dir of WEB_CANDIDATES) {
477
+ try {
478
+ if (fs.statSync(path.join(dir, "index.html")).isFile()) return dir;
479
+ } catch {
480
+ // try next candidate
481
+ }
482
+ }
483
+ return null;
484
+ }
485
+
486
+ const STATIC_CONTENT_TYPES = {
487
+ ".html": "text/html; charset=utf-8",
488
+ ".js": "text/javascript; charset=utf-8",
489
+ ".mjs": "text/javascript; charset=utf-8",
490
+ ".css": "text/css; charset=utf-8",
491
+ ".json": "application/json; charset=utf-8",
492
+ ".svg": "image/svg+xml",
493
+ ".png": "image/png",
494
+ ".jpg": "image/jpeg",
495
+ ".jpeg": "image/jpeg",
496
+ ".webp": "image/webp",
497
+ ".ico": "image/x-icon",
498
+ ".woff": "font/woff",
499
+ ".woff2": "font/woff2",
500
+ ".ttf": "font/ttf",
501
+ ".wasm": "application/wasm",
502
+ ".map": "application/json; charset=utf-8"
503
+ };
504
+
505
+ // Resolve a request path to a file inside the active web dir, refusing anything
506
+ // that escapes the directory. Returns the absolute path or null.
507
+ function resolveStaticFile(pathname) {
508
+ const root = webDir();
509
+ if (!root) return null;
510
+ const rel = decodeURIComponent(pathname).replace(/^\/+/, "");
511
+ const target = path.resolve(root, rel);
512
+ if (target !== root && !target.startsWith(root + path.sep)) return null;
513
+ try {
514
+ if (fs.statSync(target).isFile()) return target;
515
+ } catch {
516
+ return null;
517
+ }
518
+ return null;
519
+ }
520
+
521
+ function serveStaticFile(res, filePath) {
522
+ const type =
523
+ STATIC_CONTENT_TYPES[path.extname(filePath).toLowerCase()] || "application/octet-stream";
524
+ res.writeHead(200, { "Content-Type": type });
525
+ fs.createReadStream(filePath).pipe(res);
526
+ }
527
+
466
528
  function page() {
467
529
  return `<!doctype html>
468
530
  <html lang="en">
@@ -634,11 +696,25 @@ function createServer(options = {}) {
634
696
  const pathname = getPathname(req);
635
697
 
636
698
  if (req.method === "GET" && pathname === "/") {
699
+ const index = resolveStaticFile("/index.html");
700
+ if (index) {
701
+ serveStaticFile(res, index);
702
+ return;
703
+ }
637
704
  res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" });
638
705
  res.end(page());
639
706
  return;
640
707
  }
641
708
 
709
+ // Tell the browser-served UI to call this same origin for the API, so a
710
+ // custom --port still works. The bundled static config.json (used by the
711
+ // packaged desktop app) is overridden here only for HTTP requests.
712
+ if (req.method === "GET" && pathname === "/config.json") {
713
+ const host = req.headers.host || `${HOST}:${PORT}`;
714
+ sendJson(res, 200, { backendUrl: `http://${host}` });
715
+ return;
716
+ }
717
+
642
718
  if (req.method === "GET" && (pathname === "/health" || pathname === "/api/health")) {
643
719
  sendJson(res, 200, { ok: true, service: "agent-hotline", host: HOST });
644
720
  return;
@@ -901,6 +977,16 @@ function createServer(options = {}) {
901
977
  return;
902
978
  }
903
979
 
980
+ // Static assets for the built UI (JS/CSS/fonts/etc). API paths never reach
981
+ // here, so this only serves the desktop dist bundle.
982
+ if (req.method === "GET" && !pathname.startsWith("/api/")) {
983
+ const file = resolveStaticFile(pathname);
984
+ if (file) {
985
+ serveStaticFile(res, file);
986
+ return;
987
+ }
988
+ }
989
+
904
990
  throw createHttpError(404, "not_found", "Not found");
905
991
  } catch (error) {
906
992
  if (!error.status && /Queue item not found/.test(error.message)) {