airloom 0.1.3 → 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 +254 -48
- 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>
|
|
@@ -1239,10 +1433,10 @@ function getLanIP() {
|
|
|
1239
1433
|
return void 0;
|
|
1240
1434
|
}
|
|
1241
1435
|
function resolveViewerDir() {
|
|
1242
|
-
const prod =
|
|
1243
|
-
if (
|
|
1244
|
-
const dev =
|
|
1245
|
-
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;
|
|
1246
1440
|
return void 0;
|
|
1247
1441
|
}
|
|
1248
1442
|
async function main() {
|
|
@@ -1308,17 +1502,22 @@ async function main() {
|
|
|
1308
1502
|
};
|
|
1309
1503
|
if (cliArgs.cli || cliArgs.preset) {
|
|
1310
1504
|
let command = cliArgs.cli;
|
|
1311
|
-
|
|
1312
|
-
|
|
1313
|
-
|
|
1505
|
+
let presetInfo;
|
|
1506
|
+
if (cliArgs.preset) {
|
|
1507
|
+
presetInfo = CLI_PRESETS.find((p) => p.id === cliArgs.preset);
|
|
1508
|
+
if (!presetInfo) {
|
|
1314
1509
|
console.error(`[host] Unknown preset "${cliArgs.preset}". Available: ${CLI_PRESETS.map((p) => p.id).join(", ")}`);
|
|
1315
1510
|
process.exit(1);
|
|
1316
1511
|
}
|
|
1317
|
-
command =
|
|
1512
|
+
if (!command) command = presetInfo.command;
|
|
1318
1513
|
}
|
|
1319
1514
|
if (command) {
|
|
1320
|
-
state.adapter = new CLIAdapter({
|
|
1321
|
-
|
|
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"})`);
|
|
1322
1521
|
}
|
|
1323
1522
|
} else {
|
|
1324
1523
|
const saved = loadConfig();
|
|
@@ -1341,8 +1540,14 @@ async function main() {
|
|
|
1341
1540
|
}
|
|
1342
1541
|
case "cli": {
|
|
1343
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;
|
|
1344
1544
|
if (cmd) {
|
|
1345
|
-
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
|
+
});
|
|
1346
1551
|
}
|
|
1347
1552
|
break;
|
|
1348
1553
|
}
|
|
@@ -1418,6 +1623,7 @@ async function main() {
|
|
|
1418
1623
|
if (shuttingDown) return;
|
|
1419
1624
|
shuttingDown = true;
|
|
1420
1625
|
console.log("\n[host] Shutting down...");
|
|
1626
|
+
state.adapter?.destroy?.();
|
|
1421
1627
|
try {
|
|
1422
1628
|
channel.close();
|
|
1423
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"
|