airloom 0.1.9 → 0.1.11
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 +531 -252
- package/package.json +3 -1
package/dist/index.js
CHANGED
|
@@ -609,7 +609,7 @@ import QRCode from "qrcode";
|
|
|
609
609
|
// src/server.ts
|
|
610
610
|
import express from "express";
|
|
611
611
|
import { createServer } from "http";
|
|
612
|
-
import { existsSync as
|
|
612
|
+
import { existsSync as existsSync3 } from "fs";
|
|
613
613
|
import { WebSocketServer, WebSocket } from "ws";
|
|
614
614
|
|
|
615
615
|
// src/adapters/anthropic.ts
|
|
@@ -1025,6 +1025,179 @@ function getConfigPath() {
|
|
|
1025
1025
|
return CONFIG_PATH;
|
|
1026
1026
|
}
|
|
1027
1027
|
|
|
1028
|
+
// src/terminal.ts
|
|
1029
|
+
import { basename, delimiter as delimiter2, isAbsolute as isAbsolute2, join as join3, resolve as resolve2 } from "path";
|
|
1030
|
+
import { existsSync as existsSync2 } from "fs";
|
|
1031
|
+
import { spawn as spawn2 } from "node-pty";
|
|
1032
|
+
function resolveExecutable2(command, envPath = process.env.PATH ?? "") {
|
|
1033
|
+
if (!command) return null;
|
|
1034
|
+
if (isAbsolute2(command) && existsSync2(command)) return command;
|
|
1035
|
+
if (command.includes("/")) {
|
|
1036
|
+
const candidate = resolve2(process.cwd(), command);
|
|
1037
|
+
return existsSync2(candidate) ? candidate : null;
|
|
1038
|
+
}
|
|
1039
|
+
for (const dir of envPath.split(delimiter2)) {
|
|
1040
|
+
if (!dir) continue;
|
|
1041
|
+
const candidate = join3(dir.replace(/^~(?=$|\/)/, process.env.HOME ?? "~"), command);
|
|
1042
|
+
if (existsSync2(candidate)) return candidate;
|
|
1043
|
+
}
|
|
1044
|
+
return null;
|
|
1045
|
+
}
|
|
1046
|
+
function parseCommand(command) {
|
|
1047
|
+
const parts = command.match(/(?:[^\s"]+|"[^"]*")+/g) ?? [command];
|
|
1048
|
+
return {
|
|
1049
|
+
file: parts[0],
|
|
1050
|
+
args: parts.slice(1).map((part) => part.replace(/^"|"$/g, ""))
|
|
1051
|
+
};
|
|
1052
|
+
}
|
|
1053
|
+
function getDefaultTerminalCommand(explicitCommand) {
|
|
1054
|
+
const configured = explicitCommand?.trim() || process.env.AIRLOOM_TERMINAL_COMMAND?.trim();
|
|
1055
|
+
if (configured) return parseCommand(configured);
|
|
1056
|
+
if (process.platform === "win32") {
|
|
1057
|
+
const file = process.env.COMSPEC || "powershell.exe";
|
|
1058
|
+
return { file, args: [] };
|
|
1059
|
+
}
|
|
1060
|
+
const shell = process.env.SHELL || "/bin/bash";
|
|
1061
|
+
const name = basename(shell);
|
|
1062
|
+
if (name === "bash" || name === "zsh" || name === "sh") return { file: shell, args: ["-il"] };
|
|
1063
|
+
return { file: shell, args: ["-i"] };
|
|
1064
|
+
}
|
|
1065
|
+
var AdaptiveOutputBatcher = class {
|
|
1066
|
+
constructor(onFlush, fastInterval = 16, slowInterval = 80, interactiveWindow = 250, maxBytes = 4096) {
|
|
1067
|
+
this.onFlush = onFlush;
|
|
1068
|
+
this.fastInterval = fastInterval;
|
|
1069
|
+
this.slowInterval = slowInterval;
|
|
1070
|
+
this.interactiveWindow = interactiveWindow;
|
|
1071
|
+
this.maxBytes = maxBytes;
|
|
1072
|
+
}
|
|
1073
|
+
buffer = "";
|
|
1074
|
+
timer = null;
|
|
1075
|
+
lastInputAt = 0;
|
|
1076
|
+
noteInput() {
|
|
1077
|
+
this.lastInputAt = Date.now();
|
|
1078
|
+
if (this.timer) {
|
|
1079
|
+
clearTimeout(this.timer);
|
|
1080
|
+
this.timer = null;
|
|
1081
|
+
}
|
|
1082
|
+
if (this.buffer) this.schedule();
|
|
1083
|
+
}
|
|
1084
|
+
write(data) {
|
|
1085
|
+
this.buffer += data;
|
|
1086
|
+
if (this.buffer.length >= this.maxBytes) {
|
|
1087
|
+
this.flush();
|
|
1088
|
+
return;
|
|
1089
|
+
}
|
|
1090
|
+
this.schedule();
|
|
1091
|
+
}
|
|
1092
|
+
flush() {
|
|
1093
|
+
if (this.timer) {
|
|
1094
|
+
clearTimeout(this.timer);
|
|
1095
|
+
this.timer = null;
|
|
1096
|
+
}
|
|
1097
|
+
if (!this.buffer) return;
|
|
1098
|
+
const data = this.buffer;
|
|
1099
|
+
this.buffer = "";
|
|
1100
|
+
this.onFlush(data);
|
|
1101
|
+
}
|
|
1102
|
+
destroy() {
|
|
1103
|
+
this.flush();
|
|
1104
|
+
}
|
|
1105
|
+
schedule() {
|
|
1106
|
+
if (this.timer) return;
|
|
1107
|
+
const recentInput = Date.now() - this.lastInputAt <= this.interactiveWindow;
|
|
1108
|
+
this.timer = setTimeout(() => this.flush(), recentInput ? this.fastInterval : this.slowInterval);
|
|
1109
|
+
}
|
|
1110
|
+
};
|
|
1111
|
+
function getTerminalLaunchDisplay(explicitCommand) {
|
|
1112
|
+
const command = getDefaultTerminalCommand(explicitCommand);
|
|
1113
|
+
return [command.file, ...command.args].join(" ");
|
|
1114
|
+
}
|
|
1115
|
+
var TerminalSession = class {
|
|
1116
|
+
constructor(channel, getLaunchCommand) {
|
|
1117
|
+
this.channel = channel;
|
|
1118
|
+
this.getLaunchCommand = getLaunchCommand;
|
|
1119
|
+
}
|
|
1120
|
+
pty = null;
|
|
1121
|
+
stream = null;
|
|
1122
|
+
batcher = null;
|
|
1123
|
+
cols = 120;
|
|
1124
|
+
rows = 36;
|
|
1125
|
+
handleMessage(message) {
|
|
1126
|
+
switch (message.type) {
|
|
1127
|
+
case "terminal_open":
|
|
1128
|
+
this.open(message);
|
|
1129
|
+
break;
|
|
1130
|
+
case "terminal_input":
|
|
1131
|
+
this.writeInput(message);
|
|
1132
|
+
break;
|
|
1133
|
+
case "terminal_resize":
|
|
1134
|
+
this.resize(message);
|
|
1135
|
+
break;
|
|
1136
|
+
case "terminal_close":
|
|
1137
|
+
this.close();
|
|
1138
|
+
break;
|
|
1139
|
+
case "terminal_exit":
|
|
1140
|
+
break;
|
|
1141
|
+
}
|
|
1142
|
+
}
|
|
1143
|
+
close() {
|
|
1144
|
+
this.batcher?.destroy();
|
|
1145
|
+
this.batcher = null;
|
|
1146
|
+
if (this.stream && !this.stream.ended) this.stream.end();
|
|
1147
|
+
this.stream = null;
|
|
1148
|
+
try {
|
|
1149
|
+
this.pty?.kill();
|
|
1150
|
+
} catch {
|
|
1151
|
+
}
|
|
1152
|
+
this.pty = null;
|
|
1153
|
+
}
|
|
1154
|
+
open(message) {
|
|
1155
|
+
this.cols = Math.max(20, Math.floor(message.cols || this.cols));
|
|
1156
|
+
this.rows = Math.max(5, Math.floor(message.rows || this.rows));
|
|
1157
|
+
if (this.pty) {
|
|
1158
|
+
this.pty.resize(this.cols, this.rows);
|
|
1159
|
+
return;
|
|
1160
|
+
}
|
|
1161
|
+
const command = getDefaultTerminalCommand(this.getLaunchCommand?.());
|
|
1162
|
+
const file = resolveExecutable2(command.file) ?? command.file;
|
|
1163
|
+
const meta = { kind: "terminal", cols: this.cols, rows: this.rows };
|
|
1164
|
+
this.stream = this.channel.createStream(meta);
|
|
1165
|
+
this.batcher = new AdaptiveOutputBatcher((data) => this.stream?.write(data));
|
|
1166
|
+
const env = { ...process.env, TERM: "xterm-256color" };
|
|
1167
|
+
this.pty = spawn2(file, command.args, {
|
|
1168
|
+
name: "xterm-256color",
|
|
1169
|
+
cols: this.cols,
|
|
1170
|
+
rows: this.rows,
|
|
1171
|
+
cwd: process.cwd(),
|
|
1172
|
+
env
|
|
1173
|
+
});
|
|
1174
|
+
this.pty.onData((data) => this.batcher?.write(data));
|
|
1175
|
+
this.pty.onExit(({ exitCode, signal }) => {
|
|
1176
|
+
this.batcher?.flush();
|
|
1177
|
+
this.stream?.end();
|
|
1178
|
+
this.channel.send({ type: "terminal_exit", exitCode, signal });
|
|
1179
|
+
this.stream = null;
|
|
1180
|
+
this.batcher = null;
|
|
1181
|
+
this.pty = null;
|
|
1182
|
+
});
|
|
1183
|
+
}
|
|
1184
|
+
writeInput(message) {
|
|
1185
|
+
if (!this.pty) return;
|
|
1186
|
+
this.batcher?.noteInput();
|
|
1187
|
+
this.pty.write(message.data);
|
|
1188
|
+
}
|
|
1189
|
+
resize(message) {
|
|
1190
|
+
this.cols = Math.max(20, Math.floor(message.cols || this.cols));
|
|
1191
|
+
this.rows = Math.max(5, Math.floor(message.rows || this.rows));
|
|
1192
|
+
this.pty?.resize(this.cols, this.rows);
|
|
1193
|
+
}
|
|
1194
|
+
};
|
|
1195
|
+
function isTerminalMessage(data) {
|
|
1196
|
+
if (!data || typeof data !== "object" || !("type" in data)) return false;
|
|
1197
|
+
const type = data.type;
|
|
1198
|
+
return type === "terminal_open" || type === "terminal_input" || type === "terminal_resize" || type === "terminal_close" || type === "terminal_exit";
|
|
1199
|
+
}
|
|
1200
|
+
|
|
1028
1201
|
// src/server.ts
|
|
1029
1202
|
var MAX_MESSAGES = 200;
|
|
1030
1203
|
function trimMessages(messages) {
|
|
@@ -1043,7 +1216,7 @@ function createHostServer(opts) {
|
|
|
1043
1216
|
app.get("/", (_req, res) => {
|
|
1044
1217
|
res.type("html").send(HOST_HTML);
|
|
1045
1218
|
});
|
|
1046
|
-
if (opts.viewerDir &&
|
|
1219
|
+
if (opts.viewerDir && existsSync3(opts.viewerDir)) {
|
|
1047
1220
|
app.use("/viewer", express.static(opts.viewerDir));
|
|
1048
1221
|
}
|
|
1049
1222
|
app.get("/api/status", (_req, res) => {
|
|
@@ -1093,19 +1266,17 @@ function createHostServer(opts) {
|
|
|
1093
1266
|
break;
|
|
1094
1267
|
}
|
|
1095
1268
|
case "cli": {
|
|
1096
|
-
const
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
});
|
|
1108
|
-
break;
|
|
1269
|
+
const selectedPreset = typeof preset === "string" ? preset : "shell";
|
|
1270
|
+
const presetInfo = CLI_PRESETS.find((p) => p.id === selectedPreset);
|
|
1271
|
+
const cmd = selectedPreset === "shell" ? void 0 : typeof command === "string" && command.trim() ? command.trim() : presetInfo?.command;
|
|
1272
|
+
opts.state.terminalLaunchCommand = cmd;
|
|
1273
|
+
opts.state.terminalLaunch = getTerminalLaunchDisplay(cmd);
|
|
1274
|
+
opts.state.adapter = null;
|
|
1275
|
+
const cfg2 = { type: "terminal", preset: selectedPreset, command: cmd };
|
|
1276
|
+
saveConfig(cfg2);
|
|
1277
|
+
broadcast({ type: "terminal_configured", terminalLaunch: opts.state.terminalLaunch });
|
|
1278
|
+
res.json({ ok: true, terminalLaunch: opts.state.terminalLaunch });
|
|
1279
|
+
return;
|
|
1109
1280
|
}
|
|
1110
1281
|
default:
|
|
1111
1282
|
res.status(400).json({ error: "Unknown adapter type" });
|
|
@@ -1118,7 +1289,7 @@ function createHostServer(opts) {
|
|
|
1118
1289
|
cfg.preset = preset;
|
|
1119
1290
|
}
|
|
1120
1291
|
saveConfig(cfg);
|
|
1121
|
-
broadcast({ type: "configured", adapter: { name: opts.state.adapter
|
|
1292
|
+
broadcast({ type: "configured", adapter: { name: opts.state.adapter?.name ?? "none", model: opts.state.adapter?.model ?? "" } });
|
|
1122
1293
|
res.json({ ok: true });
|
|
1123
1294
|
} catch (err) {
|
|
1124
1295
|
const message = err instanceof Error ? err.message : "Configuration failed";
|
|
@@ -1142,6 +1313,18 @@ function createHostServer(opts) {
|
|
|
1142
1313
|
wss.on("connection", (ws) => {
|
|
1143
1314
|
uiClients.add(ws);
|
|
1144
1315
|
ws.on("close", () => uiClients.delete(ws));
|
|
1316
|
+
ws.on("message", (data) => {
|
|
1317
|
+
try {
|
|
1318
|
+
const message = JSON.parse(data.toString());
|
|
1319
|
+
if (message.type && message.type.startsWith("terminal_")) {
|
|
1320
|
+
if (opts.state.channel) {
|
|
1321
|
+
opts.state.channel.send(message);
|
|
1322
|
+
}
|
|
1323
|
+
}
|
|
1324
|
+
} catch (err) {
|
|
1325
|
+
console.error("[host] Invalid WebSocket message:", err);
|
|
1326
|
+
}
|
|
1327
|
+
});
|
|
1145
1328
|
});
|
|
1146
1329
|
function broadcast(data) {
|
|
1147
1330
|
const msg = JSON.stringify(data);
|
|
@@ -1251,6 +1434,18 @@ var HOST_HTML = `<!DOCTYPE html>
|
|
|
1251
1434
|
@keyframes dot-pulse{0%,80%,100%{opacity:.25;transform:scale(.8)}40%{opacity:1;transform:scale(1)}}
|
|
1252
1435
|
.input-area{display:flex;gap:8px}
|
|
1253
1436
|
.input-area textarea{flex:1;resize:none;min-height:44px;max-height:120px;padding:10px 14px;border-radius:8px;border:1px solid #333;background:#111;color:#e0e0e0;font-family:inherit;font-size:.9rem}
|
|
1437
|
+
|
|
1438
|
+
/* Terminal mode styles */
|
|
1439
|
+
.terminal-mode .chat-only { display: none !important; }
|
|
1440
|
+
.terminal-mode .terminal-only { display: block; }
|
|
1441
|
+
.chat-mode .terminal-only { display: none !important; }
|
|
1442
|
+
.chat-mode .chat-only { display: block; }
|
|
1443
|
+
|
|
1444
|
+
.terminal-container{background:#05070c;border:1px solid #2a2a2a;border-radius:8px;height:400px;overflow:hidden;margin-bottom:12px}
|
|
1445
|
+
#terminal{width:100%;height:100%;padding:8px}
|
|
1446
|
+
.terminal-toolbar{display:flex;gap:8px;margin-bottom:12px}
|
|
1447
|
+
.tool-btn{padding:6px 12px;font-size:.85rem;background:#333;border:none;border-radius:6px;color:#e0e0e0;cursor:pointer}
|
|
1448
|
+
.tool-btn:hover{background:#444}
|
|
1254
1449
|
</style>
|
|
1255
1450
|
</head>
|
|
1256
1451
|
<body>
|
|
@@ -1263,277 +1458,358 @@ var HOST_HTML = `<!DOCTYPE html>
|
|
|
1263
1458
|
<div class="status"><div class="dot wait" id="dot"></div><span id="statusText">Initializing...</span></div>
|
|
1264
1459
|
<p style="color:#888;font-size:.9rem;margin-top:8px" id="launchText">Launch: current shell</p>
|
|
1265
1460
|
</div>
|
|
1266
|
-
|
|
1267
|
-
|
|
1268
|
-
|
|
1269
|
-
|
|
1270
|
-
<
|
|
1271
|
-
<
|
|
1272
|
-
|
|
1273
|
-
|
|
1274
|
-
|
|
1461
|
+
|
|
1462
|
+
<!-- Chat mode UI -->
|
|
1463
|
+
<div id="chatMode" class="chat-mode">
|
|
1464
|
+
<div class="card" id="configCard">
|
|
1465
|
+
<h2>AI Configuration</h2>
|
|
1466
|
+
<div class="config-form">
|
|
1467
|
+
<select id="adapterType">
|
|
1468
|
+
<option value="anthropic">Anthropic Claude</option>
|
|
1469
|
+
<option value="openai">OpenAI GPT</option>
|
|
1470
|
+
<option value="cli">CLI Adapter</option>
|
|
1471
|
+
</select>
|
|
1472
|
+
<input type="text" id="model" placeholder="Model (optional)" />
|
|
1473
|
+
<input type="text" id="apiKey" placeholder="API Key (if not set in environment)" />
|
|
1474
|
+
<button onclick="configureAdapter()">Configure Adapter</button>
|
|
1475
|
+
</div>
|
|
1476
|
+
</div>
|
|
1477
|
+
|
|
1478
|
+
<div class="card chat-only" id="chatCard" style="display:none">
|
|
1479
|
+
<h2>Chat Messages</h2>
|
|
1480
|
+
<div class="messages" id="messages"></div>
|
|
1481
|
+
<div class="input-area chat-only">
|
|
1482
|
+
<textarea id="messageInput" placeholder="Type your message..." rows="1"></textarea>
|
|
1483
|
+
<button onclick="sendMessage()">Send</button>
|
|
1484
|
+
</div>
|
|
1485
|
+
</div>
|
|
1486
|
+
</div>
|
|
1487
|
+
|
|
1488
|
+
<!-- Terminal mode UI -->
|
|
1489
|
+
<div id="terminalMode" class="terminal-mode" style="display:none">
|
|
1490
|
+
<div class="card">
|
|
1491
|
+
<h2>Terminal Configuration</h2>
|
|
1492
|
+
<div class="config-form">
|
|
1493
|
+
<select id="cliPreset"></select>
|
|
1494
|
+
<input type="text" id="command" placeholder="Custom launch command" style="display:none"/>
|
|
1275
1495
|
<p style="color:#666;font-size:.8rem;margin-top:4px" id="presetDesc"></p>
|
|
1496
|
+
<button onclick="configureTerminal()">Apply Launch Target</button>
|
|
1497
|
+
</div>
|
|
1498
|
+
</div>
|
|
1499
|
+
|
|
1500
|
+
<div class="card terminal-only">
|
|
1501
|
+
<h2>Terminal Session</h2>
|
|
1502
|
+
<div class="terminal-toolbar terminal-only">
|
|
1503
|
+
<button class="tool-btn" onclick="focusTerminal()">Focus</button>
|
|
1504
|
+
<button class="tool-btn" onclick="sendCtrlC()">Ctrl+C</button>
|
|
1505
|
+
<button class="tool-btn" onclick="sendEsc()">Esc</button>
|
|
1506
|
+
<button class="tool-btn" onclick="sendTab()">Tab</button>
|
|
1507
|
+
</div>
|
|
1508
|
+
<div class="terminal-container terminal-only">
|
|
1509
|
+
<div id="terminal"></div>
|
|
1276
1510
|
</div>
|
|
1277
|
-
<button onclick="configure()">Configure</button>
|
|
1278
1511
|
</div>
|
|
1279
1512
|
</div>
|
|
1513
|
+
|
|
1280
1514
|
<div class="card pairing" id="pairingCard" style="display:none">
|
|
1281
1515
|
<h2>Connect Your Phone</h2>
|
|
1282
1516
|
<img id="qrCode" alt="QR Code"/>
|
|
1283
1517
|
<div class="pairing-code" id="pairingCode"></div>
|
|
1284
1518
|
<p style="color:#888;font-size:.85rem">Scan QR or enter code in viewer</p>
|
|
1285
1519
|
</div>
|
|
1286
|
-
<div class="card" id="chatCard" style="display:none">
|
|
1287
|
-
<h2>Conversation</h2>
|
|
1288
|
-
<div class="messages" id="messages"></div>
|
|
1289
|
-
<div class="input-area">
|
|
1290
|
-
<textarea id="msgInput" placeholder="Type a message..." onkeydown="if(event.key==='Enter'&&!event.shiftKey){event.preventDefault();sendMsg()}"></textarea>
|
|
1291
|
-
<button onclick="sendMsg()">Send</button>
|
|
1292
|
-
</div>
|
|
1293
|
-
</div>
|
|
1294
1520
|
</div>
|
|
1295
|
-
<script src="https://cdn.jsdelivr.net/npm/marked@15/marked.min.js"></script>
|
|
1296
1521
|
<script>
|
|
1297
|
-
|
|
1298
|
-
|
|
1522
|
+
// Import xterm dynamically to avoid bundling issues
|
|
1523
|
+
const loadTerminal = async () => {
|
|
1524
|
+
if (window.Terminal) return;
|
|
1525
|
+
const [{ Terminal }, { FitAddon }] = await Promise.all([
|
|
1526
|
+
import('@xterm/xterm'),
|
|
1527
|
+
import('@xterm/addon-fit')
|
|
1528
|
+
]);
|
|
1529
|
+
window.Terminal = Terminal;
|
|
1530
|
+
window.FitAddon = FitAddon;
|
|
1531
|
+
};
|
|
1532
|
+
|
|
1299
1533
|
const ws=new WebSocket((location.protocol==='https:'?'wss://':'ws://')+location.host+'/ws');
|
|
1300
|
-
let
|
|
1301
|
-
//
|
|
1534
|
+
let cliPresets=[];
|
|
1535
|
+
let currentMode = null; // 'chat' or 'terminal'
|
|
1536
|
+
let term = null;
|
|
1537
|
+
let fitAddon = null;
|
|
1538
|
+
|
|
1539
|
+
// Mode detection and UI switching
|
|
1540
|
+
function switchToMode(mode) {
|
|
1541
|
+
if (currentMode === mode) return;
|
|
1542
|
+
currentMode = mode;
|
|
1543
|
+
|
|
1544
|
+
const chatMode = document.getElementById('chatMode');
|
|
1545
|
+
const terminalMode = document.getElementById('terminalMode');
|
|
1546
|
+
|
|
1547
|
+
if (mode === 'terminal') {
|
|
1548
|
+
chatMode.style.display = 'none';
|
|
1549
|
+
terminalMode.style.display = 'block';
|
|
1550
|
+
document.body.classList.add('terminal-mode');
|
|
1551
|
+
document.body.classList.remove('chat-mode');
|
|
1552
|
+
initTerminal();
|
|
1553
|
+
} else {
|
|
1554
|
+
chatMode.style.display = 'block';
|
|
1555
|
+
terminalMode.style.display = 'none';
|
|
1556
|
+
document.body.classList.add('chat-mode');
|
|
1557
|
+
document.body.classList.remove('terminal-mode');
|
|
1558
|
+
}
|
|
1559
|
+
}
|
|
1560
|
+
|
|
1561
|
+
// Terminal functionality
|
|
1562
|
+
async function initTerminal() {
|
|
1563
|
+
if (term) return;
|
|
1564
|
+
await loadTerminal();
|
|
1565
|
+
|
|
1566
|
+
term = new window.Terminal({
|
|
1567
|
+
cursorBlink: true,
|
|
1568
|
+
fontFamily: 'ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace',
|
|
1569
|
+
fontSize: 14,
|
|
1570
|
+
lineHeight: 1.25,
|
|
1571
|
+
allowTransparency: true,
|
|
1572
|
+
scrollback: 5000,
|
|
1573
|
+
theme: {
|
|
1574
|
+
background: '#05070c',
|
|
1575
|
+
foreground: '#e6edf3',
|
|
1576
|
+
cursor: '#7c8aff',
|
|
1577
|
+
cursorAccent: '#05070c',
|
|
1578
|
+
selectionBackground: 'rgba(124,138,255,0.28)',
|
|
1579
|
+
black: '#0a0d14',
|
|
1580
|
+
red: '#ff7b72',
|
|
1581
|
+
green: '#3fb950',
|
|
1582
|
+
yellow: '#d29922',
|
|
1583
|
+
blue: '#7c8aff',
|
|
1584
|
+
magenta: '#bc8cff',
|
|
1585
|
+
cyan: '#39c5cf',
|
|
1586
|
+
white: '#c9d1d9',
|
|
1587
|
+
brightBlack: '#6e7681',
|
|
1588
|
+
brightRed: '#ffa198',
|
|
1589
|
+
brightGreen: '#56d364',
|
|
1590
|
+
brightYellow: '#e3b341',
|
|
1591
|
+
brightBlue: '#a5b4ff',
|
|
1592
|
+
brightMagenta: '#d2a8ff',
|
|
1593
|
+
brightCyan: '#56d4dd',
|
|
1594
|
+
brightWhite: '#f0f6fc',
|
|
1595
|
+
},
|
|
1596
|
+
});
|
|
1597
|
+
|
|
1598
|
+
fitAddon = new window.FitAddon();
|
|
1599
|
+
term.loadAddon(fitAddon);
|
|
1600
|
+
term.open(document.getElementById('terminal'));
|
|
1601
|
+
|
|
1602
|
+
term.onData((data) => {
|
|
1603
|
+
if (ws.readyState === WebSocket.OPEN) {
|
|
1604
|
+
ws.send(JSON.stringify({ type: 'terminal_input', data }));
|
|
1605
|
+
}
|
|
1606
|
+
});
|
|
1607
|
+
|
|
1608
|
+
// Handle terminal resize
|
|
1609
|
+
const resizeObserver = new ResizeObserver(() => {
|
|
1610
|
+
if (fitAddon && term) {
|
|
1611
|
+
fitAddon.fit();
|
|
1612
|
+
if (ws.readyState === WebSocket.OPEN) {
|
|
1613
|
+
ws.send(JSON.stringify({
|
|
1614
|
+
type: 'terminal_resize',
|
|
1615
|
+
cols: term.cols,
|
|
1616
|
+
rows: term.rows
|
|
1617
|
+
}));
|
|
1618
|
+
}
|
|
1619
|
+
}
|
|
1620
|
+
});
|
|
1621
|
+
resizeObserver.observe(document.getElementById('terminalContainer'));
|
|
1622
|
+
|
|
1623
|
+
// Auto-focus terminal
|
|
1624
|
+
term.focus();
|
|
1625
|
+
}
|
|
1626
|
+
|
|
1627
|
+
function focusTerminal() {
|
|
1628
|
+
if (term) term.focus();
|
|
1629
|
+
}
|
|
1630
|
+
|
|
1631
|
+
function sendCtrlC() {
|
|
1632
|
+
if (ws.readyState === WebSocket.OPEN) {
|
|
1633
|
+
ws.send(JSON.stringify({ type: 'terminal_input', data: '' }));
|
|
1634
|
+
}
|
|
1635
|
+
if (term) term.focus();
|
|
1636
|
+
}
|
|
1637
|
+
|
|
1638
|
+
function sendEsc() {
|
|
1639
|
+
if (ws.readyState === WebSocket.OPEN) {
|
|
1640
|
+
ws.send(JSON.stringify({ type: 'terminal_input', data: '\x1B' }));
|
|
1641
|
+
}
|
|
1642
|
+
if (term) term.focus();
|
|
1643
|
+
}
|
|
1644
|
+
|
|
1645
|
+
function sendTab() {
|
|
1646
|
+
if (ws.readyState === WebSocket.OPEN) {
|
|
1647
|
+
ws.send(JSON.stringify({ type: 'terminal_input', data: ' ' }));
|
|
1648
|
+
}
|
|
1649
|
+
if (term) term.focus();
|
|
1650
|
+
}
|
|
1651
|
+
|
|
1652
|
+
// Load CLI presets for terminal mode
|
|
1302
1653
|
fetch('/api/cli-presets').then(r=>r.json()).then(presets=>{
|
|
1303
1654
|
cliPresets=presets;
|
|
1304
1655
|
const sel=document.getElementById('cliPreset');
|
|
1656
|
+
const shellOpt=document.createElement('option');
|
|
1657
|
+
shellOpt.value='shell';
|
|
1658
|
+
shellOpt.textContent='Shell (default)';
|
|
1659
|
+
sel.appendChild(shellOpt);
|
|
1305
1660
|
presets.forEach(p=>{const o=document.createElement('option');o.value=p.id;o.textContent=p.name;sel.appendChild(o)});
|
|
1306
1661
|
sel.addEventListener('change',()=>{
|
|
1307
|
-
const
|
|
1308
|
-
|
|
1662
|
+
const id=sel.value;
|
|
1663
|
+
const p=cliPresets.find(x=>x.id===id);
|
|
1664
|
+
const cmd=document.getElementById('command');
|
|
1665
|
+
if(id==='shell'){cmd.style.display='none';cmd.value='';document.getElementById('presetDesc').textContent='Start an interactive login shell. Run commands directly in the phone terminal.';return;}
|
|
1666
|
+
if(id==='custom'){cmd.style.display='';document.getElementById('presetDesc').textContent='Enter the exact launch command.';return;}
|
|
1667
|
+
cmd.style.display='none';
|
|
1668
|
+
document.getElementById('presetDesc').textContent=p?p.description:'';
|
|
1309
1669
|
});
|
|
1310
|
-
|
|
1311
|
-
|
|
1670
|
+
sel.value='shell';
|
|
1671
|
+
sel.dispatchEvent(new Event('change'));
|
|
1312
1672
|
return fetch('/api/config').then(r=>r.json());
|
|
1313
1673
|
}).then(cfg=>{
|
|
1314
1674
|
if(!cfg) return;
|
|
1315
|
-
const {saved
|
|
1316
|
-
if(saved){
|
|
1317
|
-
document.getElementById('
|
|
1318
|
-
document.getElementById('
|
|
1319
|
-
|
|
1320
|
-
if(saved.type==='cli'){
|
|
1321
|
-
if(saved.preset){document.getElementById('cliPreset').value=saved.preset;document.getElementById('cliPreset').dispatchEvent(new Event('change'))}
|
|
1322
|
-
if(saved.command) document.getElementById('command').value=saved.command;
|
|
1323
|
-
}
|
|
1675
|
+
const {saved}=cfg;
|
|
1676
|
+
if(saved && (saved.type==='terminal' || saved.type==='cli')){
|
|
1677
|
+
if(saved.preset) document.getElementById('cliPreset').value=saved.preset;
|
|
1678
|
+
if(saved.command) document.getElementById('command').value=saved.command;
|
|
1679
|
+
document.getElementById('cliPreset').dispatchEvent(new Event('change'));
|
|
1324
1680
|
}
|
|
1325
|
-
// Show hints for env-var API keys
|
|
1326
|
-
if(envKeys.anthropic) document.getElementById('apiKey').placeholder='API Key (ANTHROPIC_API_KEY set)';
|
|
1327
|
-
if(envKeys.openai&&(!saved||saved.type==='openai')) document.getElementById('apiKey').placeholder='API Key (OPENAI_API_KEY set)';
|
|
1328
1681
|
}).catch(()=>{});
|
|
1329
|
-
|
|
1330
|
-
|
|
1331
|
-
document.getElementById('apiKey').style.display=cli?'none':'';
|
|
1332
|
-
document.getElementById('model').style.display=cli?'none':'';
|
|
1333
|
-
document.getElementById('cliConfig').style.display=cli?'':'none';
|
|
1334
|
-
});
|
|
1682
|
+
|
|
1683
|
+
// WebSocket message handling
|
|
1335
1684
|
ws.onmessage=e=>{
|
|
1336
1685
|
const d=JSON.parse(e.data);
|
|
1337
|
-
if(d.type==='
|
|
1338
|
-
|
|
1339
|
-
|
|
1340
|
-
|
|
1341
|
-
else if(d.type==='
|
|
1342
|
-
|
|
1686
|
+
if(d.type==='peer_connected'){
|
|
1687
|
+
document.getElementById('dot').className='dot on';
|
|
1688
|
+
document.getElementById('statusText').textContent='Phone connected';
|
|
1689
|
+
}
|
|
1690
|
+
else if(d.type==='peer_disconnected'){
|
|
1691
|
+
document.getElementById('dot').className='dot wait';
|
|
1692
|
+
document.getElementById('statusText').textContent='Phone disconnected';
|
|
1693
|
+
}
|
|
1694
|
+
else if(d.type==='terminal_configured'){
|
|
1695
|
+
document.getElementById('launchText').textContent='Launch: '+d.terminalLaunch;
|
|
1696
|
+
document.getElementById('statusText').textContent='Launch target updated';
|
|
1697
|
+
}
|
|
1698
|
+
else if(d.type==='configured'){
|
|
1699
|
+
document.getElementById('statusText').textContent='AI adapter configured';
|
|
1700
|
+
}
|
|
1701
|
+
else if(d.type==='stream_chunk' && term){
|
|
1702
|
+
term.write(d.data);
|
|
1703
|
+
}
|
|
1704
|
+
else if(d.type==='stream_end' && term){
|
|
1705
|
+
// Terminal stream ended
|
|
1706
|
+
}
|
|
1707
|
+
else if(d.type==='terminal_exit' && term){
|
|
1708
|
+
const detail = typeof d.exitCode === 'number' ? \`exit \${d.exitCode}\` : 'terminated';
|
|
1709
|
+
term.writeln(\`\r
|
|
1710
|
+
[terminal \${detail}]\`);
|
|
1711
|
+
}
|
|
1712
|
+
else if(d.type==='message'){
|
|
1713
|
+
// Chat message - add to chat UI
|
|
1714
|
+
const messagesDiv = document.getElementById('messages');
|
|
1715
|
+
if (messagesDiv) {
|
|
1716
|
+
const msgDiv = document.createElement('div');
|
|
1717
|
+
msgDiv.className = \`msg \${d.role}\`;
|
|
1718
|
+
msgDiv.textContent = d.content;
|
|
1719
|
+
messagesDiv.appendChild(msgDiv);
|
|
1720
|
+
messagesDiv.scrollTop = messagesDiv.scrollHeight;
|
|
1721
|
+
}
|
|
1722
|
+
}
|
|
1343
1723
|
};
|
|
1724
|
+
|
|
1725
|
+
// Load initial status and determine mode
|
|
1344
1726
|
fetch('/api/status').then(r=>r.json()).then(d=>{
|
|
1345
1727
|
if(d.terminalLaunch) document.getElementById('launchText').textContent='Launch: '+d.terminalLaunch;
|
|
1346
|
-
if(d.pairingCode){
|
|
1347
|
-
|
|
1348
|
-
|
|
1349
|
-
|
|
1350
|
-
|
|
1728
|
+
if(d.pairingCode){
|
|
1729
|
+
document.getElementById('pairingCard').style.display='';
|
|
1730
|
+
document.getElementById('pairingCode').textContent=d.pairingCode;
|
|
1731
|
+
if(d.pairingQR) document.getElementById('qrCode').src=d.pairingQR;
|
|
1732
|
+
}
|
|
1733
|
+
if(d.connected){
|
|
1734
|
+
document.getElementById('dot').className='dot on';
|
|
1735
|
+
document.getElementById('statusText').textContent='Phone connected';
|
|
1736
|
+
} else{
|
|
1737
|
+
document.getElementById('dot').className='dot wait';
|
|
1738
|
+
document.getElementById('statusText').textContent='Waiting for phone...';
|
|
1739
|
+
}
|
|
1740
|
+
|
|
1741
|
+
// Determine mode: terminal mode if no adapter but has terminal launch
|
|
1742
|
+
if (!d.adapter && d.terminalLaunch) {
|
|
1743
|
+
switchToMode('terminal');
|
|
1744
|
+
} else {
|
|
1745
|
+
switchToMode('chat');
|
|
1746
|
+
if (d.adapter) {
|
|
1747
|
+
document.getElementById('chatCard').style.display = 'block';
|
|
1748
|
+
}
|
|
1749
|
+
}
|
|
1351
1750
|
});
|
|
1352
|
-
async function configure(){
|
|
1353
|
-
const r=await fetch('/api/configure',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({type:document.getElementById('adapterType').value,apiKey:document.getElementById('apiKey').value,model:document.getElementById('model').value,command:document.getElementById('command').value,preset:document.getElementById('cliPreset').value})});
|
|
1354
|
-
const d=await r.json();if(d.error)alert(d.error);
|
|
1355
|
-
}
|
|
1356
|
-
async function sendMsg(){
|
|
1357
|
-
const el=document.getElementById('msgInput'),c=el.value.trim();if(!c)return;el.value='';
|
|
1358
|
-
await fetch('/api/send',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({content:c})});
|
|
1359
|
-
}
|
|
1360
|
-
function addMsg(role,content){const el=document.createElement('div');el.className='msg '+role;if(role==='user'||!content){el.textContent=content}else{el.innerHTML=renderMd(content)}document.getElementById('messages').appendChild(el);el.scrollIntoView({block:'end'});return el}
|
|
1361
|
-
</script>
|
|
1362
|
-
</body>
|
|
1363
|
-
</html>`;
|
|
1364
1751
|
|
|
1365
|
-
//
|
|
1366
|
-
|
|
1367
|
-
|
|
1368
|
-
|
|
1369
|
-
|
|
1370
|
-
|
|
1371
|
-
|
|
1372
|
-
|
|
1373
|
-
|
|
1374
|
-
|
|
1375
|
-
}
|
|
1376
|
-
|
|
1377
|
-
|
|
1378
|
-
|
|
1379
|
-
|
|
1752
|
+
// Configuration functions
|
|
1753
|
+
async function configureAdapter() {
|
|
1754
|
+
const type = document.getElementById('adapterType').value;
|
|
1755
|
+
const model = document.getElementById('model').value;
|
|
1756
|
+
const apiKey = document.getElementById('apiKey').value;
|
|
1757
|
+
|
|
1758
|
+
const response = await fetch('/api/configure', {
|
|
1759
|
+
method: 'POST',
|
|
1760
|
+
headers: { 'Content-Type': 'application/json' },
|
|
1761
|
+
body: JSON.stringify({ type, model, apiKey })
|
|
1762
|
+
});
|
|
1763
|
+
const result = await response.json();
|
|
1764
|
+
if (result.error) {
|
|
1765
|
+
alert(result.error);
|
|
1766
|
+
} else {
|
|
1767
|
+
document.getElementById('chatCard').style.display = 'block';
|
|
1380
1768
|
}
|
|
1381
|
-
return null;
|
|
1382
|
-
}
|
|
1383
|
-
function parseCommand(command) {
|
|
1384
|
-
const parts = command.match(/(?:[^\s"]+|"[^"]*")+/g) ?? [command];
|
|
1385
|
-
return {
|
|
1386
|
-
file: parts[0],
|
|
1387
|
-
args: parts.slice(1).map((part) => part.replace(/^"|"$/g, ""))
|
|
1388
|
-
};
|
|
1389
1769
|
}
|
|
1390
|
-
|
|
1391
|
-
|
|
1392
|
-
|
|
1393
|
-
|
|
1394
|
-
|
|
1395
|
-
|
|
1770
|
+
|
|
1771
|
+
async function configureTerminal() {
|
|
1772
|
+
const preset = document.getElementById('cliPreset').value;
|
|
1773
|
+
const command = document.getElementById('command').value;
|
|
1774
|
+
|
|
1775
|
+
const response = await fetch('/api/configure', {
|
|
1776
|
+
method: 'POST',
|
|
1777
|
+
headers: { 'Content-Type': 'application/json' },
|
|
1778
|
+
body: JSON.stringify({ type: 'cli', preset, command })
|
|
1779
|
+
});
|
|
1780
|
+
const result = await response.json();
|
|
1781
|
+
if (result.error) {
|
|
1782
|
+
alert(result.error);
|
|
1396
1783
|
}
|
|
1397
|
-
const shell = process.env.SHELL || "/bin/bash";
|
|
1398
|
-
const name = basename(shell);
|
|
1399
|
-
if (name === "bash" || name === "zsh" || name === "sh") return { file: shell, args: ["-il"] };
|
|
1400
|
-
return { file: shell, args: ["-i"] };
|
|
1401
1784
|
}
|
|
1402
|
-
|
|
1403
|
-
|
|
1404
|
-
|
|
1405
|
-
|
|
1406
|
-
|
|
1407
|
-
|
|
1408
|
-
|
|
1409
|
-
|
|
1410
|
-
|
|
1411
|
-
|
|
1412
|
-
|
|
1413
|
-
|
|
1414
|
-
|
|
1415
|
-
|
|
1416
|
-
|
|
1417
|
-
|
|
1418
|
-
}
|
|
1419
|
-
if (this.buffer) this.schedule();
|
|
1420
|
-
}
|
|
1421
|
-
write(data) {
|
|
1422
|
-
this.buffer += data;
|
|
1423
|
-
if (this.buffer.length >= this.maxBytes) {
|
|
1424
|
-
this.flush();
|
|
1425
|
-
return;
|
|
1426
|
-
}
|
|
1427
|
-
this.schedule();
|
|
1428
|
-
}
|
|
1429
|
-
flush() {
|
|
1430
|
-
if (this.timer) {
|
|
1431
|
-
clearTimeout(this.timer);
|
|
1432
|
-
this.timer = null;
|
|
1433
|
-
}
|
|
1434
|
-
if (!this.buffer) return;
|
|
1435
|
-
const data = this.buffer;
|
|
1436
|
-
this.buffer = "";
|
|
1437
|
-
this.onFlush(data);
|
|
1438
|
-
}
|
|
1439
|
-
destroy() {
|
|
1440
|
-
this.flush();
|
|
1441
|
-
}
|
|
1442
|
-
schedule() {
|
|
1443
|
-
if (this.timer) return;
|
|
1444
|
-
const recentInput = Date.now() - this.lastInputAt <= this.interactiveWindow;
|
|
1445
|
-
this.timer = setTimeout(() => this.flush(), recentInput ? this.fastInterval : this.slowInterval);
|
|
1446
|
-
}
|
|
1447
|
-
};
|
|
1448
|
-
function getTerminalLaunchDisplay(explicitCommand) {
|
|
1449
|
-
const command = getDefaultTerminalCommand(explicitCommand);
|
|
1450
|
-
return [command.file, ...command.args].join(" ");
|
|
1785
|
+
|
|
1786
|
+
// Chat functionality
|
|
1787
|
+
function sendMessage() {
|
|
1788
|
+
const input = document.getElementById('messageInput');
|
|
1789
|
+
const content = input.value.trim();
|
|
1790
|
+
if (!content) return;
|
|
1791
|
+
|
|
1792
|
+
fetch('/api/send', {
|
|
1793
|
+
method: 'POST',
|
|
1794
|
+
headers: { 'Content-Type': 'application/json' },
|
|
1795
|
+
body: JSON.stringify({ content })
|
|
1796
|
+
}).then(() => {
|
|
1797
|
+
input.value = '';
|
|
1798
|
+
}).catch(err => {
|
|
1799
|
+
console.error('Failed to send message:', err);
|
|
1800
|
+
});
|
|
1451
1801
|
}
|
|
1452
|
-
|
|
1453
|
-
|
|
1454
|
-
|
|
1455
|
-
|
|
1456
|
-
|
|
1457
|
-
|
|
1458
|
-
stream = null;
|
|
1459
|
-
batcher = null;
|
|
1460
|
-
cols = 120;
|
|
1461
|
-
rows = 36;
|
|
1462
|
-
handleMessage(message) {
|
|
1463
|
-
switch (message.type) {
|
|
1464
|
-
case "terminal_open":
|
|
1465
|
-
this.open(message);
|
|
1466
|
-
break;
|
|
1467
|
-
case "terminal_input":
|
|
1468
|
-
this.writeInput(message);
|
|
1469
|
-
break;
|
|
1470
|
-
case "terminal_resize":
|
|
1471
|
-
this.resize(message);
|
|
1472
|
-
break;
|
|
1473
|
-
case "terminal_close":
|
|
1474
|
-
this.close();
|
|
1475
|
-
break;
|
|
1476
|
-
case "terminal_exit":
|
|
1477
|
-
break;
|
|
1478
|
-
}
|
|
1479
|
-
}
|
|
1480
|
-
close() {
|
|
1481
|
-
this.batcher?.destroy();
|
|
1482
|
-
this.batcher = null;
|
|
1483
|
-
if (this.stream && !this.stream.ended) this.stream.end();
|
|
1484
|
-
this.stream = null;
|
|
1485
|
-
try {
|
|
1486
|
-
this.pty?.kill();
|
|
1487
|
-
} catch {
|
|
1488
|
-
}
|
|
1489
|
-
this.pty = null;
|
|
1490
|
-
}
|
|
1491
|
-
open(message) {
|
|
1492
|
-
this.cols = Math.max(20, Math.floor(message.cols || this.cols));
|
|
1493
|
-
this.rows = Math.max(5, Math.floor(message.rows || this.rows));
|
|
1494
|
-
if (this.pty) {
|
|
1495
|
-
this.pty.resize(this.cols, this.rows);
|
|
1496
|
-
return;
|
|
1497
|
-
}
|
|
1498
|
-
const command = getDefaultTerminalCommand(this.launchCommand);
|
|
1499
|
-
const file = resolveExecutable2(command.file) ?? command.file;
|
|
1500
|
-
const meta = { kind: "terminal", cols: this.cols, rows: this.rows };
|
|
1501
|
-
this.stream = this.channel.createStream(meta);
|
|
1502
|
-
this.batcher = new AdaptiveOutputBatcher((data) => this.stream?.write(data));
|
|
1503
|
-
const env = { ...process.env, TERM: "xterm-256color" };
|
|
1504
|
-
this.pty = spawn2(file, command.args, {
|
|
1505
|
-
name: "xterm-256color",
|
|
1506
|
-
cols: this.cols,
|
|
1507
|
-
rows: this.rows,
|
|
1508
|
-
cwd: process.cwd(),
|
|
1509
|
-
env
|
|
1510
|
-
});
|
|
1511
|
-
this.pty.onData((data) => this.batcher?.write(data));
|
|
1512
|
-
this.pty.onExit(({ exitCode, signal }) => {
|
|
1513
|
-
this.batcher?.flush();
|
|
1514
|
-
this.stream?.end();
|
|
1515
|
-
this.channel.send({ type: "terminal_exit", exitCode, signal });
|
|
1516
|
-
this.stream = null;
|
|
1517
|
-
this.batcher = null;
|
|
1518
|
-
this.pty = null;
|
|
1519
|
-
});
|
|
1520
|
-
}
|
|
1521
|
-
writeInput(message) {
|
|
1522
|
-
if (!this.pty) return;
|
|
1523
|
-
this.batcher?.noteInput();
|
|
1524
|
-
this.pty.write(message.data);
|
|
1525
|
-
}
|
|
1526
|
-
resize(message) {
|
|
1527
|
-
this.cols = Math.max(20, Math.floor(message.cols || this.cols));
|
|
1528
|
-
this.rows = Math.max(5, Math.floor(message.rows || this.rows));
|
|
1529
|
-
this.pty?.resize(this.cols, this.rows);
|
|
1802
|
+
|
|
1803
|
+
// Handle Enter key in chat input
|
|
1804
|
+
document.getElementById('messageInput')?.addEventListener('keydown', (e) => {
|
|
1805
|
+
if (e.key === 'Enter' && !e.shiftKey) {
|
|
1806
|
+
e.preventDefault();
|
|
1807
|
+
sendMessage();
|
|
1530
1808
|
}
|
|
1531
|
-
};
|
|
1532
|
-
|
|
1533
|
-
|
|
1534
|
-
|
|
1535
|
-
return type === "terminal_open" || type === "terminal_input" || type === "terminal_resize" || type === "terminal_close" || type === "terminal_exit";
|
|
1536
|
-
}
|
|
1809
|
+
});
|
|
1810
|
+
</script>
|
|
1811
|
+
</body>
|
|
1812
|
+
</html>`;
|
|
1537
1813
|
|
|
1538
1814
|
// src/index.ts
|
|
1539
1815
|
function parseArgs(argv) {
|
|
@@ -1673,12 +1949,14 @@ async function main() {
|
|
|
1673
1949
|
});
|
|
1674
1950
|
await channel.connect(session.sessionToken);
|
|
1675
1951
|
console.log("[host] Connected to relay, waiting for phone...");
|
|
1952
|
+
const savedConfig = loadConfig();
|
|
1676
1953
|
const launchPreset = cliArgs.preset ? CLI_PRESETS.find((p) => p.id === cliArgs.preset) : void 0;
|
|
1677
1954
|
if (cliArgs.preset && !launchPreset) {
|
|
1678
1955
|
console.error(`[host] Unknown preset "${cliArgs.preset}". Available: ${CLI_PRESETS.map((p) => p.id).join(", ")}`);
|
|
1679
1956
|
process.exit(1);
|
|
1680
1957
|
}
|
|
1681
|
-
const
|
|
1958
|
+
const savedTerminalCommand = !cliArgs.cli && !cliArgs.preset && savedConfig?.type === "terminal" ? savedConfig.command ?? (savedConfig.preset && savedConfig.preset !== "shell" ? CLI_PRESETS.find((p) => p.id === savedConfig.preset)?.command : void 0) : void 0;
|
|
1959
|
+
const launchCommand = cliArgs.cli ?? launchPreset?.command ?? savedTerminalCommand;
|
|
1682
1960
|
const terminalLaunch = getTerminalLaunchDisplay(launchCommand);
|
|
1683
1961
|
const state = {
|
|
1684
1962
|
channel,
|
|
@@ -1689,6 +1967,7 @@ async function main() {
|
|
|
1689
1967
|
relayUrl: useAbly ? "ably" : RELAY_URL,
|
|
1690
1968
|
connected: false,
|
|
1691
1969
|
terminalLaunch,
|
|
1970
|
+
terminalLaunchCommand: launchCommand,
|
|
1692
1971
|
messages: []
|
|
1693
1972
|
};
|
|
1694
1973
|
console.log(`[host] Terminal launch: ${terminalLaunch}`);
|
|
@@ -1779,7 +2058,7 @@ async function main() {
|
|
|
1779
2058
|
exec(`${cmd} ${localUrl}`);
|
|
1780
2059
|
}).catch(() => {
|
|
1781
2060
|
});
|
|
1782
|
-
const terminal = new TerminalSession(channel,
|
|
2061
|
+
const terminal = new TerminalSession(channel, () => state.terminalLaunchCommand);
|
|
1783
2062
|
channel.on("ready", () => {
|
|
1784
2063
|
console.log("[host] Phone connected! Channel ready.");
|
|
1785
2064
|
state.connected = true;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "airloom",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.11",
|
|
4
4
|
"description": "Run AI on your computer, control it from your phone. E2E encrypted.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -12,6 +12,8 @@
|
|
|
12
12
|
],
|
|
13
13
|
"dependencies": {
|
|
14
14
|
"@noble/hashes": "^1.7.0",
|
|
15
|
+
"@xterm/addon-fit": "^0.11.0",
|
|
16
|
+
"@xterm/xterm": "^6.0.0",
|
|
15
17
|
"ably": "^2.20.0",
|
|
16
18
|
"events": "^3.3.0",
|
|
17
19
|
"express": "^5.1.0",
|