airloom 0.1.33 → 0.1.35

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/dist/index.js CHANGED
@@ -652,15 +652,28 @@ var AnthropicAdapter = class {
652
652
  messages: chatMsgs
653
653
  };
654
654
  if (systemMsg) body.system = systemMsg;
655
- const response = await fetch("https://api.anthropic.com/v1/messages", {
656
- method: "POST",
657
- headers: {
658
- "Content-Type": "application/json",
659
- "x-api-key": this.apiKey,
660
- "anthropic-version": "2023-06-01"
661
- },
662
- body: JSON.stringify(body)
663
- });
655
+ const controller = new AbortController();
656
+ const timeout = setTimeout(() => controller.abort(), 6e4);
657
+ let response;
658
+ try {
659
+ response = await fetch("https://api.anthropic.com/v1/messages", {
660
+ method: "POST",
661
+ headers: {
662
+ "Content-Type": "application/json",
663
+ "x-api-key": this.apiKey,
664
+ "anthropic-version": "2023-06-01"
665
+ },
666
+ body: JSON.stringify(body),
667
+ signal: controller.signal
668
+ });
669
+ } catch (err) {
670
+ clearTimeout(timeout);
671
+ const msg = err.name === "AbortError" ? "Request timed out" : err.message;
672
+ stream.write(`[Error: ${msg}]`);
673
+ stream.end();
674
+ return;
675
+ }
676
+ clearTimeout(timeout);
664
677
  if (!response.ok) {
665
678
  const error = await response.text();
666
679
  stream.write(`[Error: ${response.status} ${error}]`);
@@ -714,14 +727,27 @@ var OpenAIAdapter = class {
714
727
  this.baseUrl = config.baseUrl || "https://api.openai.com/v1";
715
728
  }
716
729
  async streamResponse(messages, stream) {
717
- const response = await fetch(`${this.baseUrl}/chat/completions`, {
718
- method: "POST",
719
- headers: {
720
- "Content-Type": "application/json",
721
- Authorization: `Bearer ${this.apiKey}`
722
- },
723
- body: JSON.stringify({ model: this.model, stream: true, messages })
724
- });
730
+ const controller = new AbortController();
731
+ const timeout = setTimeout(() => controller.abort(), 6e4);
732
+ let response;
733
+ try {
734
+ response = await fetch(`${this.baseUrl}/chat/completions`, {
735
+ method: "POST",
736
+ headers: {
737
+ "Content-Type": "application/json",
738
+ Authorization: `Bearer ${this.apiKey}`
739
+ },
740
+ body: JSON.stringify({ model: this.model, stream: true, messages }),
741
+ signal: controller.signal
742
+ });
743
+ } catch (err) {
744
+ clearTimeout(timeout);
745
+ const msg = err.name === "AbortError" ? "Request timed out" : err.message;
746
+ stream.write(`[Error: ${msg}]`);
747
+ stream.end();
748
+ return;
749
+ }
750
+ clearTimeout(timeout);
725
751
  if (!response.ok) {
726
752
  const error = await response.text();
727
753
  stream.write(`[Error: ${response.status} ${error}]`);
@@ -850,7 +876,7 @@ function resolveExecutable(command, envPath = process.env.PATH ?? "") {
850
876
  }
851
877
  for (const dir of envPath.split(delimiter)) {
852
878
  if (!dir) continue;
853
- const candidate = join(dir.replace(/^~(?=$|\/)/, process.env.HOME ?? "~"), command);
879
+ const candidate = join(dir.replace(/^~(?=$|\/)/, process.env.HOME || process.env.USERPROFILE || "~"), command);
854
880
  if (existsSync(candidate)) return candidate;
855
881
  }
856
882
  return null;
@@ -1123,7 +1149,8 @@ function resolveExecutable2(command, envPath = process.env.PATH ?? "") {
1123
1149
  }
1124
1150
  for (const dir of envPath.split(delimiter2)) {
1125
1151
  if (!dir) continue;
1126
- const candidate = join3(dir.replace(/^~(?=$|\/)/, process.env.HOME ?? "~"), command);
1152
+ const home = process.env.HOME || process.env.USERPROFILE || "~";
1153
+ const candidate = join3(dir.replace(/^~(?=$|\/)/, home), command);
1127
1154
  if (existsSync2(candidate)) return candidate;
1128
1155
  }
1129
1156
  return null;
@@ -1138,14 +1165,16 @@ function parseCommand(command) {
1138
1165
  function getDefaultTerminalCommand(explicitCommand) {
1139
1166
  const configured = explicitCommand?.trim() || process.env.AIRLOOM_TERMINAL_COMMAND?.trim();
1140
1167
  if (configured) return parseCommand(configured);
1168
+ const shell = process.env.SHELL;
1169
+ if (shell) {
1170
+ const name = basename(shell);
1171
+ if (name === "bash" || name === "zsh" || name === "sh") return { file: shell, args: ["-il"] };
1172
+ return { file: shell, args: ["-i"] };
1173
+ }
1141
1174
  if (process.platform === "win32") {
1142
- const file = process.env.COMSPEC || "powershell.exe";
1143
- return { file, args: [] };
1175
+ return { file: process.env.COMSPEC || "powershell.exe", args: [] };
1144
1176
  }
1145
- const shell = process.env.SHELL || "/bin/bash";
1146
- const name = basename(shell);
1147
- if (name === "bash" || name === "zsh" || name === "sh") return { file: shell, args: ["-il"] };
1148
- return { file: shell, args: ["-i"] };
1177
+ return { file: "/bin/bash", args: ["-il"] };
1149
1178
  }
1150
1179
  var AdaptiveOutputBatcher = class {
1151
1180
  constructor(onFlush, fastInterval = 16, slowInterval = 80, interactiveWindow = 250, maxBytes = 4096) {
@@ -1220,8 +1249,8 @@ var TerminalSession = class {
1220
1249
  const file = resolveExecutable2(command.file) ?? command.file;
1221
1250
  const cwd = process.cwd();
1222
1251
  log(`[host] PTY spawn: ${file} ${command.args.join(" ")} (${this.cols}x${this.rows}) node=${process.version}`);
1223
- const env2 = { ...process.env, TERM: "xterm-256color" };
1224
- const spawnOpts = { name: "xterm-256color", cols: this.cols, rows: this.rows, cwd, env: env2 };
1252
+ const env = { ...process.env, TERM: "xterm-256color" };
1253
+ const spawnOpts = { name: "xterm-256color", cols: this.cols, rows: this.rows, cwd, env };
1225
1254
  let nodePty;
1226
1255
  try {
1227
1256
  nodePty = requireNodePty();
@@ -2238,13 +2267,213 @@ function loadOrCreateAblySessionToken() {
2238
2267
  return ablySessionToken;
2239
2268
  }
2240
2269
 
2270
+ // src/daemon.ts
2271
+ import { mkdirSync as mkdirSync3, readFileSync as readFileSync3, writeFileSync as writeFileSync3, unlinkSync, readdirSync, openSync, closeSync } from "node:fs";
2272
+ import { homedir as homedir3 } from "node:os";
2273
+ import { join as join5 } from "node:path";
2274
+ import { spawn as spawn2 } from "node:child_process";
2275
+ var SESSIONS_DIR = join5(homedir3(), ".config", "airloom", "sessions");
2276
+ function ensureDir() {
2277
+ mkdirSync3(SESSIONS_DIR, { recursive: true });
2278
+ }
2279
+ function sessionPath(name) {
2280
+ return join5(SESSIONS_DIR, `${name}.json`);
2281
+ }
2282
+ function logFilePath(name) {
2283
+ return join5(SESSIONS_DIR, `${name}.log`);
2284
+ }
2285
+ function validateName(name) {
2286
+ if (!/^[a-zA-Z0-9_-]+$/.test(name)) {
2287
+ throw new Error(`Invalid session name "${name}". Use only letters, numbers, hyphens, and underscores.`);
2288
+ }
2289
+ }
2290
+ function readSession(name) {
2291
+ try {
2292
+ const raw = readFileSync3(sessionPath(name), "utf-8");
2293
+ const data = JSON.parse(raw);
2294
+ if (!data || typeof data.pid !== "number") return null;
2295
+ return data;
2296
+ } catch {
2297
+ return null;
2298
+ }
2299
+ }
2300
+ function writeSession(name, info) {
2301
+ ensureDir();
2302
+ writeFileSync3(sessionPath(name), JSON.stringify(info, null, 2) + "\n", { mode: 384 });
2303
+ }
2304
+ function removeSession(name) {
2305
+ try {
2306
+ unlinkSync(sessionPath(name));
2307
+ } catch {
2308
+ }
2309
+ }
2310
+ function isAlive(pid) {
2311
+ try {
2312
+ process.kill(pid, 0);
2313
+ return true;
2314
+ } catch {
2315
+ return false;
2316
+ }
2317
+ }
2318
+ function listAllSessions() {
2319
+ ensureDir();
2320
+ let files;
2321
+ try {
2322
+ files = readdirSync(SESSIONS_DIR).filter((f) => f.endsWith(".json"));
2323
+ } catch {
2324
+ return [];
2325
+ }
2326
+ const results = [];
2327
+ for (const file of files) {
2328
+ const name = file.replace(/\.json$/, "");
2329
+ const info = readSession(name);
2330
+ if (!info) continue;
2331
+ if (!isAlive(info.pid)) {
2332
+ removeSession(name);
2333
+ continue;
2334
+ }
2335
+ results.push({ name, info });
2336
+ }
2337
+ return results;
2338
+ }
2339
+ function formatAge(ms) {
2340
+ const sec = Math.floor(ms / 1e3);
2341
+ if (sec < 60) return `${sec}s ago`;
2342
+ const min = Math.floor(sec / 60);
2343
+ if (min < 60) return `${min}m ago`;
2344
+ const hr = Math.floor(min / 60);
2345
+ if (hr < 24) return `${hr}h ${min % 60}m ago`;
2346
+ return `${Math.floor(hr / 24)}d ago`;
2347
+ }
2348
+ async function handleStart(name, hostArgs) {
2349
+ validateName(name);
2350
+ const existing = readSession(name);
2351
+ if (existing && isAlive(existing.pid)) {
2352
+ console.error(`Session "${name}" is already running (PID ${existing.pid}, port ${existing.port})`);
2353
+ console.error(`Host UI: ${existing.controlUrl}`);
2354
+ process.exit(1);
2355
+ }
2356
+ if (existing) removeSession(name);
2357
+ ensureDir();
2358
+ const logFile = logFilePath(name);
2359
+ const logFd = openSync(logFile, "w");
2360
+ const child = spawn2(
2361
+ process.execPath,
2362
+ [...process.execArgv, process.argv[1], ...hostArgs, "--_daemon"],
2363
+ {
2364
+ detached: true,
2365
+ stdio: ["ignore", logFd, logFd, "ipc"],
2366
+ cwd: process.cwd(),
2367
+ env: process.env
2368
+ }
2369
+ );
2370
+ try {
2371
+ const info = await new Promise((resolve4, reject) => {
2372
+ const timer = setTimeout(() => {
2373
+ reject(new Error(`Timed out waiting for daemon to start (30s). Check log: ${logFile}`));
2374
+ }, 3e4);
2375
+ child.on("message", (msg) => {
2376
+ const m = msg;
2377
+ if (m.type === "ready") {
2378
+ clearTimeout(timer);
2379
+ resolve4({
2380
+ pid: child.pid,
2381
+ port: m.port,
2382
+ controlUrl: m.controlUrl,
2383
+ viewerUrl: m.viewerUrl,
2384
+ pairingCode: m.pairingCode,
2385
+ cwd: process.cwd(),
2386
+ startedAt: Date.now(),
2387
+ logFile
2388
+ });
2389
+ }
2390
+ });
2391
+ child.on("error", (err) => {
2392
+ clearTimeout(timer);
2393
+ reject(err);
2394
+ });
2395
+ child.on("exit", (code) => {
2396
+ clearTimeout(timer);
2397
+ reject(new Error(`Daemon exited with code ${code}. Check log: ${logFile}`));
2398
+ });
2399
+ });
2400
+ writeSession(name, info);
2401
+ child.disconnect();
2402
+ child.unref();
2403
+ closeSync(logFd);
2404
+ console.log(`
2405
+ Airloom session "${name}" started (PID ${info.pid})
2406
+ `);
2407
+ console.log(`Pairing Code: ${info.pairingCode}`);
2408
+ console.log(`Viewer URL: ${info.viewerUrl}`);
2409
+ console.log(`Host UI: ${info.controlUrl}`);
2410
+ console.log(`Log: ${info.logFile}
2411
+ `);
2412
+ } catch (err) {
2413
+ closeSync(logFd);
2414
+ try {
2415
+ child.kill();
2416
+ } catch {
2417
+ }
2418
+ throw err;
2419
+ }
2420
+ }
2421
+ function handleStop(nameOrNull, all) {
2422
+ if (all) {
2423
+ const sessions = listAllSessions();
2424
+ if (sessions.length === 0) {
2425
+ console.log("No running sessions.");
2426
+ return;
2427
+ }
2428
+ for (const { name, info: info2 } of sessions) {
2429
+ try {
2430
+ process.kill(info2.pid, "SIGTERM");
2431
+ } catch {
2432
+ }
2433
+ removeSession(name);
2434
+ console.log(`Stopped "${name}" (PID ${info2.pid})`);
2435
+ }
2436
+ return;
2437
+ }
2438
+ const target = nameOrNull || "default";
2439
+ const info = readSession(target);
2440
+ if (!info) {
2441
+ console.error(`No session named "${target}" found.`);
2442
+ process.exit(1);
2443
+ }
2444
+ if (!isAlive(info.pid)) {
2445
+ removeSession(target);
2446
+ console.log(`Session "${target}" was not running (cleaned up stale entry).`);
2447
+ return;
2448
+ }
2449
+ try {
2450
+ process.kill(info.pid, "SIGTERM");
2451
+ } catch {
2452
+ }
2453
+ removeSession(target);
2454
+ console.log(`Stopped "${target}" (PID ${info.pid})`);
2455
+ }
2456
+ function handleList() {
2457
+ const sessions = listAllSessions();
2458
+ if (sessions.length === 0) {
2459
+ console.log("No running sessions.");
2460
+ return;
2461
+ }
2462
+ console.log("NAME PORT PID STARTED CWD");
2463
+ for (const { name, info } of sessions) {
2464
+ const age = formatAge(Date.now() - info.startedAt);
2465
+ console.log(
2466
+ name.padEnd(16) + String(info.port).padEnd(7) + String(info.pid).padEnd(9) + age.padEnd(17) + info.cwd
2467
+ );
2468
+ }
2469
+ }
2470
+
2241
2471
  // src/index.ts
2242
2472
  var QRCode = null;
2243
2473
  async function getQRCode() {
2244
2474
  if (!QRCode) QRCode = await import("qrcode");
2245
2475
  return QRCode;
2246
2476
  }
2247
- log("[host] Module loaded");
2248
2477
  function parseArgs(argv) {
2249
2478
  const args = {};
2250
2479
  const rest = argv.slice(2);
@@ -2274,7 +2503,15 @@ function printHelp() {
2274
2503
  Airloom \u2014 Run AI on your computer, control it from your phone.
2275
2504
 
2276
2505
  Usage:
2277
- airloom [options]
2506
+ airloom [options] Start in foreground (default)
2507
+ airloom start [options] Start as a background daemon
2508
+ airloom stop [name] Stop a background session
2509
+ airloom stop --all Stop all background sessions
2510
+ airloom list List running background sessions
2511
+
2512
+ Background options:
2513
+ --name <name> Session name (default: "default").
2514
+ Allows multiple independent sessions.
2278
2515
 
2279
2516
  Options:
2280
2517
  --cli <command> CLI command to use as the AI adapter.
@@ -2299,22 +2536,6 @@ Environment variables:
2299
2536
  HOST_BIND Host bind address (default: 127.0.0.1).
2300
2537
  `.trimStart());
2301
2538
  }
2302
- var cliArgs = parseArgs(process.argv);
2303
- if (cliArgs.help) {
2304
- printHelp();
2305
- process.exit(0);
2306
- }
2307
- var IS_DEV = !process.env.VIEWER_URL && !new URL(import.meta.url).pathname.includes("node_modules");
2308
- var env = parseHostEnv(cliArgs.port, IS_DEV);
2309
- var VIEWER_URL = env.viewerUrl;
2310
- var RELAY_URL = env.relayUrl;
2311
- var ABLY_API_KEY = env.ablyApiKey;
2312
- var ABLY_TOKEN_TTL = env.ablyTokenTtlMs;
2313
- var HOST_PORT = env.hostPort;
2314
- var HOST_BIND = env.hostBind;
2315
- var useAbly = env.useAbly;
2316
- var isDefaultKey = env.isDefaultAblyKey;
2317
- var __dirname = dirname2(fileURLToPath(import.meta.url));
2318
2539
  function getLanIP() {
2319
2540
  for (const ifaces of Object.values(networkInterfaces())) {
2320
2541
  for (const iface of ifaces ?? []) {
@@ -2323,14 +2544,31 @@ function getLanIP() {
2323
2544
  }
2324
2545
  return void 0;
2325
2546
  }
2326
- function resolveViewerDir() {
2327
- const prod = resolve3(__dirname, "viewer");
2547
+ function resolveViewerDir(base) {
2548
+ const prod = resolve3(base, "viewer");
2328
2549
  if (existsSync4(prod)) return prod;
2329
- const dev = resolve3(__dirname, "../../viewer/dist");
2550
+ const dev = resolve3(base, "../../viewer/dist");
2330
2551
  if (existsSync4(dev)) return dev;
2331
2552
  return void 0;
2332
2553
  }
2333
2554
  async function main() {
2555
+ const cliArgs = parseArgs(process.argv);
2556
+ if (cliArgs.help) {
2557
+ printHelp();
2558
+ process.exit(0);
2559
+ }
2560
+ const IS_DEV = !process.env.VIEWER_URL && !new URL(import.meta.url).pathname.includes("node_modules");
2561
+ const env = parseHostEnv(cliArgs.port, IS_DEV);
2562
+ const VIEWER_URL = env.viewerUrl;
2563
+ const RELAY_URL = env.relayUrl;
2564
+ const ABLY_API_KEY = env.ablyApiKey;
2565
+ const ABLY_TOKEN_TTL = env.ablyTokenTtlMs;
2566
+ const HOST_PORT = env.hostPort;
2567
+ const HOST_BIND = env.hostBind;
2568
+ const useAbly = env.useAbly;
2569
+ const isDefaultKey = env.isDefaultAblyKey;
2570
+ const isDaemonChild = process.argv.includes("--_daemon");
2571
+ const __dirname = dirname2(fileURLToPath(import.meta.url));
2334
2572
  console.log("Airloom - Host");
2335
2573
  console.log("==============\n");
2336
2574
  if (useAbly) {
@@ -2482,7 +2720,7 @@ async function main() {
2482
2720
  }
2483
2721
  }
2484
2722
  }
2485
- const viewerDir = resolveViewerDir();
2723
+ const viewerDir = resolveViewerDir(__dirname);
2486
2724
  if (viewerDir) {
2487
2725
  log(`[host] Viewer files: ${viewerDir}`);
2488
2726
  } else {
@@ -2522,16 +2760,20 @@ async function main() {
2522
2760
  const controlBase = isLoopbackBind(HOST_BIND) ? `http://localhost:${port}` : lanBaseUrl;
2523
2761
  const controlUrl = encodeControlUrl(controlBase, controlToken);
2524
2762
  console.log(`Host UI: ${controlUrl}`);
2525
- if (env.isSSH) {
2763
+ if (isDaemonChild && typeof process.send === "function") {
2764
+ process.send({ type: "ready", port, controlUrl, viewerUrl: qrTarget, pairingCode: displayCode });
2765
+ }
2766
+ if (isDaemonChild) {
2767
+ } else if (env.isSSH) {
2526
2768
  if (isLoopbackBind(HOST_BIND)) {
2527
2769
  console.log("\n (SSH session detected but server is bound to localhost \u2014 set HOST_BIND=0.0.0.0 to allow remote access)");
2528
2770
  } else {
2529
2771
  console.log("\n (SSH session \u2014 open the Host UI URL above in a browser on your local machine)");
2530
2772
  }
2531
2773
  } else {
2532
- import("node:child_process").then(({ exec }) => {
2774
+ import("node:child_process").then(({ execFile }) => {
2533
2775
  const cmd = process.platform === "darwin" ? "open" : process.platform === "win32" ? "start" : "xdg-open";
2534
- exec(`${cmd} ${controlUrl}`);
2776
+ execFile(cmd, [controlUrl]);
2535
2777
  }).catch(() => {
2536
2778
  });
2537
2779
  }
@@ -2598,7 +2840,44 @@ async function main() {
2598
2840
  process.on("SIGINT", shutdown);
2599
2841
  process.on("SIGTERM", shutdown);
2600
2842
  }
2601
- main().catch((err) => {
2602
- logError("Fatal error:", err);
2603
- process.exit(1);
2604
- });
2843
+ var _cmd = process.argv[2];
2844
+ if (_cmd === "start" || _cmd === "stop" || _cmd === "list") {
2845
+ (async () => {
2846
+ const _args = process.argv.slice(3);
2847
+ if (_cmd === "list") {
2848
+ handleList();
2849
+ return;
2850
+ }
2851
+ if (_cmd === "stop") {
2852
+ let stopName = null;
2853
+ let stopAll = false;
2854
+ for (const a of _args) {
2855
+ if (a === "--all") stopAll = true;
2856
+ else if (!a.startsWith("-")) stopName = a;
2857
+ }
2858
+ handleStop(stopName, stopAll);
2859
+ return;
2860
+ }
2861
+ let startName = "default";
2862
+ const hostArgs = [];
2863
+ for (let i = 0; i < _args.length; i++) {
2864
+ const a = _args[i];
2865
+ if (a === "--name" && i + 1 < _args.length) {
2866
+ startName = _args[++i];
2867
+ } else if (a.startsWith("--name=")) {
2868
+ startName = a.slice(7);
2869
+ } else {
2870
+ hostArgs.push(a);
2871
+ }
2872
+ }
2873
+ await handleStart(startName, hostArgs);
2874
+ })().catch((err) => {
2875
+ console.error(err.message);
2876
+ process.exit(1);
2877
+ });
2878
+ } else {
2879
+ main().catch((err) => {
2880
+ logError("Fatal error:", err);
2881
+ process.exit(1);
2882
+ });
2883
+ }
@@ -1 +1 @@
1
- import{g as c}from"./index-DwLYzSfq.js";function f(t,i){for(var o=0;o<i.length;o++){const e=i[o];if(typeof e!="string"&&!Array.isArray(e)){for(const r in e)if(r!=="default"&&!(r in t)){const s=Object.getOwnPropertyDescriptor(e,r);s&&Object.defineProperty(t,r,s.get?s:{enumerable:!0,get:()=>e[r]})}}}return Object.freeze(Object.defineProperty(t,Symbol.toStringTag,{value:"Module"}))}var n,a;function b(){return a||(a=1,n=function(){throw new Error("ws does not work in the browser. Browser clients must use the native WebSocket object")}),n}var u=b();const w=c(u),p=f({__proto__:null,default:w},[u]);export{p as b};
1
+ import{g as c}from"./index-RRG33VZY.js";function f(t,i){for(var o=0;o<i.length;o++){const e=i[o];if(typeof e!="string"&&!Array.isArray(e)){for(const r in e)if(r!=="default"&&!(r in t)){const s=Object.getOwnPropertyDescriptor(e,r);s&&Object.defineProperty(t,r,s.get?s:{enumerable:!0,get:()=>e[r]})}}}return Object.freeze(Object.defineProperty(t,Symbol.toStringTag,{value:"Module"}))}var n,a;function b(){return a||(a=1,n=function(){throw new Error("ws does not work in the browser. Browser clients must use the native WebSocket object")}),n}var u=b();const w=c(u),p=f({__proto__:null,default:w},[u]);export{p as b};