airloom 0.1.2 → 0.1.4
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 +266 -54
- package/package.json +2 -1
package/dist/index.js
CHANGED
|
@@ -267,7 +267,7 @@ var Channel = class extends EventEmitter2 {
|
|
|
267
267
|
}
|
|
268
268
|
waitForReady(timeoutMs = 3e4) {
|
|
269
269
|
if (this._ready) return Promise.resolve();
|
|
270
|
-
return new Promise((
|
|
270
|
+
return new Promise((resolve3, reject) => {
|
|
271
271
|
const cleanup = () => {
|
|
272
272
|
clearTimeout(timer);
|
|
273
273
|
this.removeListener("ready", onReady);
|
|
@@ -276,7 +276,7 @@ var Channel = class extends EventEmitter2 {
|
|
|
276
276
|
};
|
|
277
277
|
const onReady = () => {
|
|
278
278
|
cleanup();
|
|
279
|
-
|
|
279
|
+
resolve3();
|
|
280
280
|
};
|
|
281
281
|
const onError = (err) => {
|
|
282
282
|
cleanup();
|
|
@@ -373,14 +373,14 @@ var WebSocketAdapter = class {
|
|
|
373
373
|
}
|
|
374
374
|
async doConnect() {
|
|
375
375
|
const ws = await this.createWebSocket(this.url);
|
|
376
|
-
return new Promise((
|
|
376
|
+
return new Promise((resolve3, reject) => {
|
|
377
377
|
ws.onopen = () => {
|
|
378
378
|
this._connected = true;
|
|
379
379
|
this.shouldReconnect = true;
|
|
380
380
|
this.reconnectAttempts = 0;
|
|
381
381
|
const msg = this.role === "host" ? { type: "create", sessionToken: this.sessionToken } : { type: "join", sessionToken: this.sessionToken };
|
|
382
382
|
ws.send(JSON.stringify(msg));
|
|
383
|
-
|
|
383
|
+
resolve3();
|
|
384
384
|
};
|
|
385
385
|
ws.onmessage = (event) => {
|
|
386
386
|
const raw = event.data;
|
|
@@ -510,8 +510,8 @@ var AblyAdapter = class {
|
|
|
510
510
|
clientOpts.token = this.opts.token;
|
|
511
511
|
}
|
|
512
512
|
this.ably = new Realtime(clientOpts);
|
|
513
|
-
await new Promise((
|
|
514
|
-
this.ably.connection.once("connected", () =>
|
|
513
|
+
await new Promise((resolve3, reject) => {
|
|
514
|
+
this.ably.connection.once("connected", () => resolve3());
|
|
515
515
|
this.ably.connection.once("failed", (stateChange) => {
|
|
516
516
|
reject(new Error(stateChange?.reason?.message ?? "Ably connection failed"));
|
|
517
517
|
});
|
|
@@ -598,14 +598,14 @@ var AblyAdapter = class {
|
|
|
598
598
|
import { sha256 as sha2562 } from "@noble/hashes/sha256";
|
|
599
599
|
import { networkInterfaces } from "os";
|
|
600
600
|
import { fileURLToPath } from "url";
|
|
601
|
-
import { dirname, resolve } from "path";
|
|
602
|
-
import { existsSync as
|
|
601
|
+
import { dirname, resolve as resolve2 } from "path";
|
|
602
|
+
import { existsSync as existsSync3 } from "fs";
|
|
603
603
|
import QRCode from "qrcode";
|
|
604
604
|
|
|
605
605
|
// src/server.ts
|
|
606
606
|
import express from "express";
|
|
607
607
|
import { createServer } from "http";
|
|
608
|
-
import { existsSync } from "fs";
|
|
608
|
+
import { existsSync as existsSync2 } from "fs";
|
|
609
609
|
import { WebSocketServer, WebSocket } from "ws";
|
|
610
610
|
|
|
611
611
|
// src/adapters/anthropic.ts
|
|
@@ -739,85 +739,266 @@ var OpenAIAdapter = class {
|
|
|
739
739
|
|
|
740
740
|
// src/adapters/cli.ts
|
|
741
741
|
import { spawn } from "child_process";
|
|
742
|
+
import { existsSync } from "fs";
|
|
743
|
+
import { delimiter, isAbsolute, join, resolve } from "path";
|
|
742
744
|
var CLI_PRESETS = [
|
|
743
745
|
{
|
|
744
746
|
id: "devin",
|
|
745
747
|
name: "Devin",
|
|
746
|
-
command: "devin
|
|
747
|
-
description: "Devin CLI
|
|
748
|
+
command: "devin",
|
|
749
|
+
description: "Devin CLI (persistent REPL session)",
|
|
750
|
+
mode: "repl",
|
|
751
|
+
silenceTimeout: 8e3
|
|
748
752
|
},
|
|
749
753
|
{
|
|
750
754
|
id: "claude-code",
|
|
751
755
|
name: "Claude Code",
|
|
756
|
+
command: "claude",
|
|
757
|
+
description: "Claude Code (persistent REPL session)",
|
|
758
|
+
mode: "repl",
|
|
759
|
+
silenceTimeout: 8e3
|
|
760
|
+
},
|
|
761
|
+
{
|
|
762
|
+
id: "claude-code-oneshot",
|
|
763
|
+
name: "Claude Code (one-shot)",
|
|
752
764
|
command: "claude -p --output-format text",
|
|
753
|
-
description: "Claude Code in print mode"
|
|
765
|
+
description: "Claude Code in print mode (new process per prompt)",
|
|
766
|
+
mode: "oneshot"
|
|
754
767
|
},
|
|
755
768
|
{
|
|
756
769
|
id: "codex",
|
|
757
770
|
name: "Codex",
|
|
758
771
|
command: "codex exec --full-auto",
|
|
759
|
-
description: "OpenAI Codex CLI in non-interactive exec mode"
|
|
772
|
+
description: "OpenAI Codex CLI in non-interactive exec mode",
|
|
773
|
+
mode: "oneshot"
|
|
760
774
|
},
|
|
761
775
|
{
|
|
762
776
|
id: "aider",
|
|
763
777
|
name: "Aider",
|
|
764
778
|
command: "aider --yes --no-auto-commits --message",
|
|
765
|
-
description: "Aider in scripting mode (prompt via --message)"
|
|
779
|
+
description: "Aider in scripting mode (prompt via --message)",
|
|
780
|
+
mode: "oneshot"
|
|
766
781
|
},
|
|
767
782
|
{
|
|
768
783
|
id: "custom",
|
|
769
784
|
name: "Custom",
|
|
770
785
|
command: "",
|
|
771
|
-
description: "Custom command
|
|
786
|
+
description: "Custom command (default: one-shot, prompt appended as last arg)",
|
|
787
|
+
mode: "oneshot"
|
|
772
788
|
}
|
|
773
789
|
];
|
|
790
|
+
var ANSI_RE = /[\x1b\x9b][[()#;?]*(?:[0-9]{1,4}(?:;[0-9]{0,4})*)?[0-9A-ORZcf-nq-uy=><~]|\x1b\][^\x07\x1b]*(?:\x07|\x1b\\)|\x1b[^[\]()#;?][^\x1b]?|\r/g;
|
|
791
|
+
var stripAnsi = (s) => s.replace(ANSI_RE, "");
|
|
792
|
+
var isDecorativeLine = (line) => {
|
|
793
|
+
const t = line.trim();
|
|
794
|
+
if (!t) return false;
|
|
795
|
+
return /^[╭╰╮╯│├┤┬┴┼─━═┄┈┌┐└┘║╔╗╚╝╠╣╦╩╬▔▁░▒▓█\-+|=*~_\s]+$/.test(t);
|
|
796
|
+
};
|
|
797
|
+
var isNoiseLine = (line) => {
|
|
798
|
+
const t = line.trim();
|
|
799
|
+
if (!t) return false;
|
|
800
|
+
return t === "#" || t.includes("Devin for Terminal") || /^v\d{4}\./.test(t) || t.startsWith("Mode: ") || t.includes("Tip: Use shift+tab") || t.startsWith("Update v") || t.includes("\u2219 Pro");
|
|
801
|
+
};
|
|
802
|
+
function resolveExecutable(command, envPath = process.env.PATH ?? "") {
|
|
803
|
+
if (!command) return null;
|
|
804
|
+
if (isAbsolute(command) && existsSync(command)) return command;
|
|
805
|
+
if (command.includes("/")) {
|
|
806
|
+
const candidate = resolve(process.cwd(), command);
|
|
807
|
+
return existsSync(candidate) ? candidate : null;
|
|
808
|
+
}
|
|
809
|
+
for (const dir of envPath.split(delimiter)) {
|
|
810
|
+
if (!dir) continue;
|
|
811
|
+
const candidate = join(dir.replace(/^~(?=$|\/)/, process.env.HOME ?? "~"), command);
|
|
812
|
+
if (existsSync(candidate)) return candidate;
|
|
813
|
+
}
|
|
814
|
+
return null;
|
|
815
|
+
}
|
|
774
816
|
var CLIAdapter = class {
|
|
775
817
|
name = "cli";
|
|
776
818
|
model;
|
|
777
819
|
command;
|
|
778
820
|
args;
|
|
821
|
+
mode;
|
|
822
|
+
silenceTimeout;
|
|
823
|
+
pty = null;
|
|
824
|
+
ptyState = "starting";
|
|
825
|
+
startupResolve = null;
|
|
826
|
+
startupTimer = null;
|
|
827
|
+
activeStream = null;
|
|
828
|
+
activeResolve = null;
|
|
829
|
+
silenceTimer = null;
|
|
830
|
+
lastInput = "";
|
|
779
831
|
constructor(config) {
|
|
780
832
|
if (!config.command) throw new Error("Command is required for CLI adapter");
|
|
781
|
-
const parts = config.command.
|
|
833
|
+
const parts = config.command.match(/(?:[^\s"]+|"[^"]*")+/g) ?? [config.command];
|
|
782
834
|
this.command = parts[0];
|
|
783
|
-
this.args = parts.slice(1);
|
|
784
|
-
this.
|
|
835
|
+
this.args = parts.slice(1).map((a) => a.replace(/^"|"$/g, ""));
|
|
836
|
+
this.mode = config.mode ?? "oneshot";
|
|
837
|
+
this.silenceTimeout = config.silenceTimeout ?? 5e3;
|
|
838
|
+
this.model = config.model || `${config.command} (${this.mode})`;
|
|
785
839
|
}
|
|
786
840
|
async streamResponse(messages, stream) {
|
|
841
|
+
if (this.mode === "repl") return this.replResponse(messages, stream);
|
|
842
|
+
return this.oneshotResponse(messages, stream);
|
|
843
|
+
}
|
|
844
|
+
destroy() {
|
|
845
|
+
if (this.silenceTimer) clearTimeout(this.silenceTimer);
|
|
846
|
+
if (this.startupTimer) clearTimeout(this.startupTimer);
|
|
847
|
+
try {
|
|
848
|
+
this.pty?.kill();
|
|
849
|
+
} catch {
|
|
850
|
+
}
|
|
851
|
+
this.pty = null;
|
|
852
|
+
this.finishResponse();
|
|
853
|
+
}
|
|
854
|
+
// --- oneshot ----------------------------------------------------------------
|
|
855
|
+
async oneshotResponse(messages, stream) {
|
|
787
856
|
const lastUserMsg = [...messages].reverse().find((m) => m.role === "user");
|
|
788
857
|
if (!lastUserMsg) {
|
|
789
858
|
stream.write("[No user message provided]");
|
|
790
859
|
stream.end();
|
|
791
860
|
return;
|
|
792
861
|
}
|
|
793
|
-
return new Promise((
|
|
862
|
+
return new Promise((resolvePromise) => {
|
|
794
863
|
const args = [...this.args, lastUserMsg.content];
|
|
795
|
-
const proc = spawn(this.command, args, {
|
|
796
|
-
stdio: ["pipe", "pipe", "pipe"]
|
|
797
|
-
});
|
|
798
|
-
const stripAnsi = (s) => s.replace(/\x1b\[[0-9;]*[a-zA-Z]/g, "");
|
|
864
|
+
const proc = spawn(this.command, args, { stdio: ["pipe", "pipe", "pipe"] });
|
|
799
865
|
proc.stdin.end();
|
|
800
866
|
proc.stdout.on("data", (data) => stream.write(stripAnsi(data.toString())));
|
|
801
867
|
proc.stderr.on("data", (data) => stream.write(stripAnsi(data.toString())));
|
|
802
868
|
proc.on("close", () => {
|
|
803
869
|
stream.end();
|
|
804
|
-
|
|
870
|
+
resolvePromise();
|
|
805
871
|
});
|
|
806
872
|
proc.on("error", (err) => {
|
|
807
873
|
stream.write(`[Error: ${err.message}]`);
|
|
808
874
|
stream.end();
|
|
809
|
-
|
|
875
|
+
resolvePromise();
|
|
810
876
|
});
|
|
811
877
|
});
|
|
812
878
|
}
|
|
879
|
+
// --- repl -------------------------------------------------------------------
|
|
880
|
+
async ensurePty() {
|
|
881
|
+
if (this.pty) return this.pty;
|
|
882
|
+
const nodePty = await import("node-pty");
|
|
883
|
+
const executable = resolveExecutable(this.command) ?? this.command;
|
|
884
|
+
console.log(`[cli-repl] Spawning PTY: ${executable} ${this.args.join(" ")}`);
|
|
885
|
+
const pty = nodePty.spawn(executable, this.args, {
|
|
886
|
+
name: "xterm-256color",
|
|
887
|
+
cols: 120,
|
|
888
|
+
rows: 40,
|
|
889
|
+
cwd: process.cwd(),
|
|
890
|
+
env: { ...process.env, NO_COLOR: "1" }
|
|
891
|
+
});
|
|
892
|
+
pty.onData((data) => this.onData(data));
|
|
893
|
+
pty.onExit(({ exitCode }) => {
|
|
894
|
+
console.log(`[cli-repl] PTY exited (code ${exitCode})`);
|
|
895
|
+
this.pty = null;
|
|
896
|
+
this.ptyState = "idle";
|
|
897
|
+
this.finishResponse();
|
|
898
|
+
});
|
|
899
|
+
this.pty = pty;
|
|
900
|
+
this.ptyState = "starting";
|
|
901
|
+
await new Promise((resolvePromise) => {
|
|
902
|
+
this.startupResolve = resolvePromise;
|
|
903
|
+
const maxTimer = setTimeout(() => {
|
|
904
|
+
if (this.ptyState === "starting") {
|
|
905
|
+
this.ptyState = "idle";
|
|
906
|
+
this.startupResolve?.();
|
|
907
|
+
this.startupResolve = null;
|
|
908
|
+
}
|
|
909
|
+
if (this.startupTimer) {
|
|
910
|
+
clearTimeout(this.startupTimer);
|
|
911
|
+
this.startupTimer = null;
|
|
912
|
+
}
|
|
913
|
+
}, 5e3);
|
|
914
|
+
pty.onExit(() => {
|
|
915
|
+
clearTimeout(maxTimer);
|
|
916
|
+
resolvePromise();
|
|
917
|
+
});
|
|
918
|
+
});
|
|
919
|
+
return pty;
|
|
920
|
+
}
|
|
921
|
+
onData(raw) {
|
|
922
|
+
const clean = stripAnsi(raw);
|
|
923
|
+
switch (this.ptyState) {
|
|
924
|
+
case "starting":
|
|
925
|
+
if (this.startupTimer) clearTimeout(this.startupTimer);
|
|
926
|
+
this.startupTimer = setTimeout(() => {
|
|
927
|
+
this.ptyState = "idle";
|
|
928
|
+
this.startupResolve?.();
|
|
929
|
+
this.startupResolve = null;
|
|
930
|
+
}, 2e3);
|
|
931
|
+
break;
|
|
932
|
+
case "idle":
|
|
933
|
+
break;
|
|
934
|
+
case "responding": {
|
|
935
|
+
if (!this.activeStream || !clean) break;
|
|
936
|
+
const filtered = this.filterOutput(clean);
|
|
937
|
+
if (filtered) this.activeStream.write(filtered);
|
|
938
|
+
this.resetSilenceTimer();
|
|
939
|
+
break;
|
|
940
|
+
}
|
|
941
|
+
}
|
|
942
|
+
}
|
|
943
|
+
filterOutput(text) {
|
|
944
|
+
const lines = text.split("\n");
|
|
945
|
+
const kept = [];
|
|
946
|
+
for (const line of lines) {
|
|
947
|
+
const trimmed = line.trim();
|
|
948
|
+
if (!trimmed) {
|
|
949
|
+
kept.push(line);
|
|
950
|
+
continue;
|
|
951
|
+
}
|
|
952
|
+
if (this.lastInput && (trimmed === this.lastInput.trim() || trimmed === `> ${this.lastInput.trim()}`)) continue;
|
|
953
|
+
if (isDecorativeLine(line) || isNoiseLine(line)) continue;
|
|
954
|
+
kept.push(line);
|
|
955
|
+
}
|
|
956
|
+
return kept.join("\n").replace(/^\n+/, "");
|
|
957
|
+
}
|
|
958
|
+
resetSilenceTimer() {
|
|
959
|
+
if (this.silenceTimer) clearTimeout(this.silenceTimer);
|
|
960
|
+
this.silenceTimer = setTimeout(() => {
|
|
961
|
+
this.ptyState = "idle";
|
|
962
|
+
this.finishResponse();
|
|
963
|
+
}, this.silenceTimeout);
|
|
964
|
+
}
|
|
965
|
+
finishResponse() {
|
|
966
|
+
if (this.silenceTimer) {
|
|
967
|
+
clearTimeout(this.silenceTimer);
|
|
968
|
+
this.silenceTimer = null;
|
|
969
|
+
}
|
|
970
|
+
const stream = this.activeStream;
|
|
971
|
+
const resolvePromise = this.activeResolve;
|
|
972
|
+
this.activeStream = null;
|
|
973
|
+
this.activeResolve = null;
|
|
974
|
+
if (stream && !stream.ended) stream.end();
|
|
975
|
+
resolvePromise?.();
|
|
976
|
+
}
|
|
977
|
+
async replResponse(messages, stream) {
|
|
978
|
+
const lastUserMsg = [...messages].reverse().find((m) => m.role === "user");
|
|
979
|
+
if (!lastUserMsg) {
|
|
980
|
+
stream.write("[No user message provided]");
|
|
981
|
+
stream.end();
|
|
982
|
+
return;
|
|
983
|
+
}
|
|
984
|
+
const pty = await this.ensurePty();
|
|
985
|
+
return new Promise((resolvePromise) => {
|
|
986
|
+
this.activeStream = stream;
|
|
987
|
+
this.activeResolve = resolvePromise;
|
|
988
|
+
this.ptyState = "responding";
|
|
989
|
+
this.lastInput = lastUserMsg.content;
|
|
990
|
+
pty.write(lastUserMsg.content + "\r");
|
|
991
|
+
this.resetSilenceTimer();
|
|
992
|
+
});
|
|
993
|
+
}
|
|
813
994
|
};
|
|
814
995
|
|
|
815
996
|
// src/config.ts
|
|
816
997
|
import { readFileSync, writeFileSync, mkdirSync } from "fs";
|
|
817
998
|
import { homedir } from "os";
|
|
818
|
-
import { join } from "path";
|
|
819
|
-
var CONFIG_DIR =
|
|
820
|
-
var CONFIG_PATH =
|
|
999
|
+
import { join as join2 } from "path";
|
|
1000
|
+
var CONFIG_DIR = join2(homedir(), ".config", "airloom");
|
|
1001
|
+
var CONFIG_PATH = join2(CONFIG_DIR, "config.json");
|
|
821
1002
|
function loadConfig() {
|
|
822
1003
|
try {
|
|
823
1004
|
const raw = readFileSync(CONFIG_PATH, "utf-8");
|
|
@@ -858,7 +1039,7 @@ function createHostServer(opts) {
|
|
|
858
1039
|
app.get("/", (_req, res) => {
|
|
859
1040
|
res.type("html").send(HOST_HTML);
|
|
860
1041
|
});
|
|
861
|
-
if (opts.viewerDir &&
|
|
1042
|
+
if (opts.viewerDir && existsSync2(opts.viewerDir)) {
|
|
862
1043
|
app.use("/viewer", express.static(opts.viewerDir));
|
|
863
1044
|
}
|
|
864
1045
|
app.get("/api/status", (_req, res) => {
|
|
@@ -912,7 +1093,13 @@ function createHostServer(opts) {
|
|
|
912
1093
|
res.status(400).json({ error: "CLI adapter requires a command (or set AIRLOOM_CLI_COMMAND env var)" });
|
|
913
1094
|
return;
|
|
914
1095
|
}
|
|
915
|
-
|
|
1096
|
+
const presetInfo = preset ? CLI_PRESETS.find((p) => p.id === preset) : void 0;
|
|
1097
|
+
opts.state.adapter = new CLIAdapter({
|
|
1098
|
+
command: cmd,
|
|
1099
|
+
model,
|
|
1100
|
+
mode: presetInfo?.mode,
|
|
1101
|
+
silenceTimeout: presetInfo?.silenceTimeout
|
|
1102
|
+
});
|
|
916
1103
|
break;
|
|
917
1104
|
}
|
|
918
1105
|
default:
|
|
@@ -957,11 +1144,11 @@ function createHostServer(opts) {
|
|
|
957
1144
|
if (ws.readyState === WebSocket.OPEN) ws.send(msg);
|
|
958
1145
|
}
|
|
959
1146
|
}
|
|
960
|
-
return new Promise((
|
|
1147
|
+
return new Promise((resolve3) => {
|
|
961
1148
|
server.listen(opts.port, "0.0.0.0", () => {
|
|
962
1149
|
const addr = server.address();
|
|
963
1150
|
const actualPort = typeof addr === "object" && addr ? addr.port : opts.port;
|
|
964
|
-
|
|
1151
|
+
resolve3({ server, broadcast, port: actualPort });
|
|
965
1152
|
});
|
|
966
1153
|
server.on("error", (err) => {
|
|
967
1154
|
console.error(`[host] Server error: ${err.message}`);
|
|
@@ -972,19 +1159,25 @@ function createHostServer(opts) {
|
|
|
972
1159
|
async function handleAIResponse(channel, adapter, state, broadcast) {
|
|
973
1160
|
const stream = channel.createStream({ model: adapter.model });
|
|
974
1161
|
let fullResponse = "";
|
|
975
|
-
const
|
|
1162
|
+
const uiBatcher = new Batcher({
|
|
976
1163
|
interval: 100,
|
|
977
1164
|
onFlush: (data) => broadcast({ type: "stream_chunk", data })
|
|
978
1165
|
});
|
|
979
1166
|
const origWrite = stream.write.bind(stream);
|
|
1167
|
+
const relayBatcher = new Batcher({
|
|
1168
|
+
interval: 500,
|
|
1169
|
+
maxBytes: 4096,
|
|
1170
|
+
onFlush: (data) => origWrite(data)
|
|
1171
|
+
});
|
|
980
1172
|
stream.write = (data) => {
|
|
981
1173
|
fullResponse += data;
|
|
982
|
-
|
|
983
|
-
|
|
1174
|
+
uiBatcher.write(data);
|
|
1175
|
+
relayBatcher.write(data);
|
|
984
1176
|
};
|
|
985
1177
|
const origEnd = stream.end.bind(stream);
|
|
986
1178
|
stream.end = () => {
|
|
987
|
-
|
|
1179
|
+
uiBatcher.flush();
|
|
1180
|
+
relayBatcher.flush();
|
|
988
1181
|
broadcast({ type: "stream_end" });
|
|
989
1182
|
state.messages.push({ role: "assistant", content: fullResponse, timestamp: Date.now() });
|
|
990
1183
|
trimMessages(state.messages);
|
|
@@ -998,7 +1191,8 @@ async function handleAIResponse(channel, adapter, state, broadcast) {
|
|
|
998
1191
|
stream.write(`[Error: ${message}]`);
|
|
999
1192
|
stream.end();
|
|
1000
1193
|
}
|
|
1001
|
-
|
|
1194
|
+
uiBatcher.destroy();
|
|
1195
|
+
relayBatcher.destroy();
|
|
1002
1196
|
}
|
|
1003
1197
|
}
|
|
1004
1198
|
var HOST_HTML = `<!DOCTYPE html>
|
|
@@ -1211,6 +1405,7 @@ Environment variables:
|
|
|
1211
1405
|
OPENAI_API_KEY API key for the OpenAI adapter.
|
|
1212
1406
|
ABLY_API_KEY Your own Ably key (overrides default community relay).
|
|
1213
1407
|
RELAY_URL Self-hosted WebSocket relay URL (disables Ably).
|
|
1408
|
+
VIEWER_URL Public viewer URL (default: GitHub Pages).
|
|
1214
1409
|
HOST_PORT Same as --port (CLI flag takes precedence).
|
|
1215
1410
|
`.trimStart());
|
|
1216
1411
|
}
|
|
@@ -1220,6 +1415,8 @@ if (cliArgs.help) {
|
|
|
1220
1415
|
process.exit(0);
|
|
1221
1416
|
}
|
|
1222
1417
|
var DEFAULT_ABLY_KEY = "SfHSAQ.IRTOQQ:FBbi9a7ZV6jIu0Gdo_UeYhIN4rzpMrud5-LldURNh9s";
|
|
1418
|
+
var DEFAULT_VIEWER_URL = "https://bobstrogg.github.io/Airloom/";
|
|
1419
|
+
var VIEWER_URL = process.env.VIEWER_URL ?? DEFAULT_VIEWER_URL;
|
|
1223
1420
|
var RELAY_URL = process.env.RELAY_URL;
|
|
1224
1421
|
var ABLY_API_KEY = process.env.ABLY_API_KEY ?? (RELAY_URL ? void 0 : DEFAULT_ABLY_KEY);
|
|
1225
1422
|
var ABLY_TOKEN_TTL = parseInt(process.env.ABLY_TOKEN_TTL ?? String(24 * 60 * 60 * 1e3), 10);
|
|
@@ -1236,10 +1433,10 @@ function getLanIP() {
|
|
|
1236
1433
|
return void 0;
|
|
1237
1434
|
}
|
|
1238
1435
|
function resolveViewerDir() {
|
|
1239
|
-
const prod =
|
|
1240
|
-
if (
|
|
1241
|
-
const dev =
|
|
1242
|
-
if (
|
|
1436
|
+
const prod = resolve2(__dirname, "viewer");
|
|
1437
|
+
if (existsSync3(prod)) return prod;
|
|
1438
|
+
const dev = resolve2(__dirname, "../../viewer/dist");
|
|
1439
|
+
if (existsSync3(dev)) return dev;
|
|
1243
1440
|
return void 0;
|
|
1244
1441
|
}
|
|
1245
1442
|
async function main() {
|
|
@@ -1305,17 +1502,22 @@ async function main() {
|
|
|
1305
1502
|
};
|
|
1306
1503
|
if (cliArgs.cli || cliArgs.preset) {
|
|
1307
1504
|
let command = cliArgs.cli;
|
|
1308
|
-
|
|
1309
|
-
|
|
1310
|
-
|
|
1505
|
+
let presetInfo;
|
|
1506
|
+
if (cliArgs.preset) {
|
|
1507
|
+
presetInfo = CLI_PRESETS.find((p) => p.id === cliArgs.preset);
|
|
1508
|
+
if (!presetInfo) {
|
|
1311
1509
|
console.error(`[host] Unknown preset "${cliArgs.preset}". Available: ${CLI_PRESETS.map((p) => p.id).join(", ")}`);
|
|
1312
1510
|
process.exit(1);
|
|
1313
1511
|
}
|
|
1314
|
-
command =
|
|
1512
|
+
if (!command) command = presetInfo.command;
|
|
1315
1513
|
}
|
|
1316
1514
|
if (command) {
|
|
1317
|
-
state.adapter = new CLIAdapter({
|
|
1318
|
-
|
|
1515
|
+
state.adapter = new CLIAdapter({
|
|
1516
|
+
command,
|
|
1517
|
+
mode: presetInfo?.mode,
|
|
1518
|
+
silenceTimeout: presetInfo?.silenceTimeout
|
|
1519
|
+
});
|
|
1520
|
+
console.log(`[host] CLI adapter: ${command} (${presetInfo?.mode ?? "oneshot"})`);
|
|
1319
1521
|
}
|
|
1320
1522
|
} else {
|
|
1321
1523
|
const saved = loadConfig();
|
|
@@ -1338,8 +1540,14 @@ async function main() {
|
|
|
1338
1540
|
}
|
|
1339
1541
|
case "cli": {
|
|
1340
1542
|
const cmd = saved.command || process.env.AIRLOOM_CLI_COMMAND;
|
|
1543
|
+
const savedPreset = saved.preset ? CLI_PRESETS.find((p) => p.id === saved.preset) : void 0;
|
|
1341
1544
|
if (cmd) {
|
|
1342
|
-
state.adapter = new CLIAdapter({
|
|
1545
|
+
state.adapter = new CLIAdapter({
|
|
1546
|
+
command: cmd,
|
|
1547
|
+
model: saved.model,
|
|
1548
|
+
mode: savedPreset?.mode,
|
|
1549
|
+
silenceTimeout: savedPreset?.silenceTimeout
|
|
1550
|
+
});
|
|
1343
1551
|
}
|
|
1344
1552
|
break;
|
|
1345
1553
|
}
|
|
@@ -1360,19 +1568,22 @@ async function main() {
|
|
|
1360
1568
|
console.log("[host] Viewer dist not found \u2014 QR will open raw JSON fallback");
|
|
1361
1569
|
}
|
|
1362
1570
|
const { server, broadcast, port } = await createHostServer({ port: HOST_PORT, state, viewerDir });
|
|
1363
|
-
const lanIP = getLanIP();
|
|
1364
|
-
const host = lanIP ?? "localhost";
|
|
1365
|
-
const baseUrl = `http://${host}:${port}`;
|
|
1366
1571
|
const pairingBase64 = Buffer.from(pairingJSON).toString("base64url");
|
|
1367
|
-
const
|
|
1572
|
+
const viewerBase = VIEWER_URL.replace(/\/+$/, "");
|
|
1573
|
+
const qrContent = `${viewerBase}/#${pairingBase64}`;
|
|
1574
|
+
const lanIP = getLanIP();
|
|
1575
|
+
const lanHost = lanIP ?? "localhost";
|
|
1576
|
+
const lanBaseUrl = `http://${lanHost}:${port}`;
|
|
1577
|
+
const lanViewerUrl = viewerDir ? `${lanBaseUrl}/viewer/#${pairingBase64}` : null;
|
|
1368
1578
|
const qrDataUrl = await QRCode.toDataURL(qrContent, { width: 300, margin: 2 });
|
|
1369
1579
|
const qrTerminal = await QRCode.toString(qrContent, { type: "terminal", small: true });
|
|
1370
1580
|
state.pairingQR = qrDataUrl;
|
|
1371
1581
|
console.log("\nPairing QR Code:");
|
|
1372
1582
|
console.log(qrTerminal);
|
|
1373
1583
|
console.log(`Pairing Code: ${displayCode}`);
|
|
1374
|
-
|
|
1375
|
-
|
|
1584
|
+
console.log(`Viewer URL: ${qrContent}`);
|
|
1585
|
+
if (lanViewerUrl) {
|
|
1586
|
+
console.log(`LAN Viewer: ${lanViewerUrl}`);
|
|
1376
1587
|
}
|
|
1377
1588
|
if (!useAbly) console.log(`Relay: ${RELAY_URL}`);
|
|
1378
1589
|
const localUrl = `http://localhost:${port}`;
|
|
@@ -1412,6 +1623,7 @@ async function main() {
|
|
|
1412
1623
|
if (shuttingDown) return;
|
|
1413
1624
|
shuttingDown = true;
|
|
1414
1625
|
console.log("\n[host] Shutting down...");
|
|
1626
|
+
state.adapter?.destroy?.();
|
|
1415
1627
|
try {
|
|
1416
1628
|
channel.close();
|
|
1417
1629
|
} catch {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "airloom",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.4",
|
|
4
4
|
"description": "Run AI on your computer, control it from your phone. E2E encrypted.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -15,6 +15,7 @@
|
|
|
15
15
|
"ably": "^2.20.0",
|
|
16
16
|
"events": "^3.3.0",
|
|
17
17
|
"express": "^5.1.0",
|
|
18
|
+
"node-pty": "^1.1.0",
|
|
18
19
|
"qrcode": "^1.5.0",
|
|
19
20
|
"tweetnacl": "^1.0.3",
|
|
20
21
|
"ws": "^8.18.0"
|