airloom 0.1.3 → 0.1.5

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
@@ -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((resolve2, reject) => {
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
- resolve2();
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((resolve2, reject) => {
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
- resolve2();
383
+ resolve3();
384
384
  };
385
385
  ws.onmessage = (event) => {
386
386
  const raw = event.data;
@@ -503,15 +503,19 @@ var AblyAdapter = class {
503
503
  }
504
504
  async connect(sessionToken, role) {
505
505
  this.clientId = `airloom-${role}-${Date.now()}`;
506
- const clientOpts = { clientId: this.clientId };
506
+ const clientOpts = {
507
+ clientId: this.clientId,
508
+ // Enable server time sync to avoid "Timestamp not current" errors on machines with clock skew
509
+ queryTime: true
510
+ };
507
511
  if (this.opts.key) {
508
512
  clientOpts.key = this.opts.key;
509
513
  } else {
510
514
  clientOpts.token = this.opts.token;
511
515
  }
512
516
  this.ably = new Realtime(clientOpts);
513
- await new Promise((resolve2, reject) => {
514
- this.ably.connection.once("connected", () => resolve2());
517
+ await new Promise((resolve3, reject) => {
518
+ this.ably.connection.once("connected", () => resolve3());
515
519
  this.ably.connection.once("failed", (stateChange) => {
516
520
  reject(new Error(stateChange?.reason?.message ?? "Ably connection failed"));
517
521
  });
@@ -598,14 +602,14 @@ var AblyAdapter = class {
598
602
  import { sha256 as sha2562 } from "@noble/hashes/sha256";
599
603
  import { networkInterfaces } from "os";
600
604
  import { fileURLToPath } from "url";
601
- import { dirname, resolve } from "path";
602
- import { existsSync as existsSync2 } from "fs";
605
+ import { dirname, resolve as resolve2 } from "path";
606
+ import { existsSync as existsSync3 } from "fs";
603
607
  import QRCode from "qrcode";
604
608
 
605
609
  // src/server.ts
606
610
  import express from "express";
607
611
  import { createServer } from "http";
608
- import { existsSync } from "fs";
612
+ import { existsSync as existsSync2 } from "fs";
609
613
  import { WebSocketServer, WebSocket } from "ws";
610
614
 
611
615
  // src/adapters/anthropic.ts
@@ -739,85 +743,266 @@ var OpenAIAdapter = class {
739
743
 
740
744
  // src/adapters/cli.ts
741
745
  import { spawn } from "child_process";
746
+ import { existsSync } from "fs";
747
+ import { delimiter, isAbsolute, join, resolve } from "path";
742
748
  var CLI_PRESETS = [
743
749
  {
744
750
  id: "devin",
745
751
  name: "Devin",
746
- command: "devin --permission-mode dangerous -p --",
747
- description: "Devin CLI in non-interactive print mode"
752
+ command: "devin",
753
+ description: "Devin CLI (persistent REPL session)",
754
+ mode: "repl",
755
+ silenceTimeout: 8e3
748
756
  },
749
757
  {
750
758
  id: "claude-code",
751
759
  name: "Claude Code",
760
+ command: "claude",
761
+ description: "Claude Code (persistent REPL session)",
762
+ mode: "repl",
763
+ silenceTimeout: 8e3
764
+ },
765
+ {
766
+ id: "claude-code-oneshot",
767
+ name: "Claude Code (one-shot)",
752
768
  command: "claude -p --output-format text",
753
- description: "Claude Code in print mode"
769
+ description: "Claude Code in print mode (new process per prompt)",
770
+ mode: "oneshot"
754
771
  },
755
772
  {
756
773
  id: "codex",
757
774
  name: "Codex",
758
775
  command: "codex exec --full-auto",
759
- description: "OpenAI Codex CLI in non-interactive exec mode"
776
+ description: "OpenAI Codex CLI in non-interactive exec mode",
777
+ mode: "oneshot"
760
778
  },
761
779
  {
762
780
  id: "aider",
763
781
  name: "Aider",
764
782
  command: "aider --yes --no-auto-commits --message",
765
- description: "Aider in scripting mode (prompt via --message)"
783
+ description: "Aider in scripting mode (prompt via --message)",
784
+ mode: "oneshot"
766
785
  },
767
786
  {
768
787
  id: "custom",
769
788
  name: "Custom",
770
789
  command: "",
771
- description: "Custom command \u2014 prompt appended as last argument"
790
+ description: "Custom command (default: one-shot, prompt appended as last arg)",
791
+ mode: "oneshot"
772
792
  }
773
793
  ];
794
+ 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;
795
+ var stripAnsi = (s) => s.replace(ANSI_RE, "");
796
+ var isDecorativeLine = (line) => {
797
+ const t = line.trim();
798
+ if (!t) return false;
799
+ return /^[╭╰╮╯│├┤┬┴┼─━═┄┈┌┐└┘║╔╗╚╝╠╣╦╩╬▔▁░▒▓█\-+|=*~_\s]+$/.test(t);
800
+ };
801
+ var isNoiseLine = (line) => {
802
+ const t = line.trim();
803
+ if (!t) return false;
804
+ 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");
805
+ };
806
+ function resolveExecutable(command, envPath = process.env.PATH ?? "") {
807
+ if (!command) return null;
808
+ if (isAbsolute(command) && existsSync(command)) return command;
809
+ if (command.includes("/")) {
810
+ const candidate = resolve(process.cwd(), command);
811
+ return existsSync(candidate) ? candidate : null;
812
+ }
813
+ for (const dir of envPath.split(delimiter)) {
814
+ if (!dir) continue;
815
+ const candidate = join(dir.replace(/^~(?=$|\/)/, process.env.HOME ?? "~"), command);
816
+ if (existsSync(candidate)) return candidate;
817
+ }
818
+ return null;
819
+ }
774
820
  var CLIAdapter = class {
775
821
  name = "cli";
776
822
  model;
777
823
  command;
778
824
  args;
825
+ mode;
826
+ silenceTimeout;
827
+ pty = null;
828
+ ptyState = "starting";
829
+ startupResolve = null;
830
+ startupTimer = null;
831
+ activeStream = null;
832
+ activeResolve = null;
833
+ silenceTimer = null;
834
+ lastInput = "";
779
835
  constructor(config) {
780
836
  if (!config.command) throw new Error("Command is required for CLI adapter");
781
- const parts = config.command.split(" ");
837
+ const parts = config.command.match(/(?:[^\s"]+|"[^"]*")+/g) ?? [config.command];
782
838
  this.command = parts[0];
783
- this.args = parts.slice(1);
784
- this.model = config.model || config.command;
839
+ this.args = parts.slice(1).map((a) => a.replace(/^"|"$/g, ""));
840
+ this.mode = config.mode ?? "oneshot";
841
+ this.silenceTimeout = config.silenceTimeout ?? 5e3;
842
+ this.model = config.model || `${config.command} (${this.mode})`;
785
843
  }
786
844
  async streamResponse(messages, stream) {
845
+ if (this.mode === "repl") return this.replResponse(messages, stream);
846
+ return this.oneshotResponse(messages, stream);
847
+ }
848
+ destroy() {
849
+ if (this.silenceTimer) clearTimeout(this.silenceTimer);
850
+ if (this.startupTimer) clearTimeout(this.startupTimer);
851
+ try {
852
+ this.pty?.kill();
853
+ } catch {
854
+ }
855
+ this.pty = null;
856
+ this.finishResponse();
857
+ }
858
+ // --- oneshot ----------------------------------------------------------------
859
+ async oneshotResponse(messages, stream) {
787
860
  const lastUserMsg = [...messages].reverse().find((m) => m.role === "user");
788
861
  if (!lastUserMsg) {
789
862
  stream.write("[No user message provided]");
790
863
  stream.end();
791
864
  return;
792
865
  }
793
- return new Promise((resolve2) => {
866
+ return new Promise((resolvePromise) => {
794
867
  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, "");
868
+ const proc = spawn(this.command, args, { stdio: ["pipe", "pipe", "pipe"] });
799
869
  proc.stdin.end();
800
870
  proc.stdout.on("data", (data) => stream.write(stripAnsi(data.toString())));
801
871
  proc.stderr.on("data", (data) => stream.write(stripAnsi(data.toString())));
802
872
  proc.on("close", () => {
803
873
  stream.end();
804
- resolve2();
874
+ resolvePromise();
805
875
  });
806
876
  proc.on("error", (err) => {
807
877
  stream.write(`[Error: ${err.message}]`);
808
878
  stream.end();
809
- resolve2();
879
+ resolvePromise();
880
+ });
881
+ });
882
+ }
883
+ // --- repl -------------------------------------------------------------------
884
+ async ensurePty() {
885
+ if (this.pty) return this.pty;
886
+ const nodePty = await import("node-pty");
887
+ const executable = resolveExecutable(this.command) ?? this.command;
888
+ console.log(`[cli-repl] Spawning PTY: ${executable} ${this.args.join(" ")}`);
889
+ const pty = nodePty.spawn(executable, this.args, {
890
+ name: "xterm-256color",
891
+ cols: 120,
892
+ rows: 40,
893
+ cwd: process.cwd(),
894
+ env: { ...process.env, NO_COLOR: "1" }
895
+ });
896
+ pty.onData((data) => this.onData(data));
897
+ pty.onExit(({ exitCode }) => {
898
+ console.log(`[cli-repl] PTY exited (code ${exitCode})`);
899
+ this.pty = null;
900
+ this.ptyState = "idle";
901
+ this.finishResponse();
902
+ });
903
+ this.pty = pty;
904
+ this.ptyState = "starting";
905
+ await new Promise((resolvePromise) => {
906
+ this.startupResolve = resolvePromise;
907
+ const maxTimer = setTimeout(() => {
908
+ if (this.ptyState === "starting") {
909
+ this.ptyState = "idle";
910
+ this.startupResolve?.();
911
+ this.startupResolve = null;
912
+ }
913
+ if (this.startupTimer) {
914
+ clearTimeout(this.startupTimer);
915
+ this.startupTimer = null;
916
+ }
917
+ }, 5e3);
918
+ pty.onExit(() => {
919
+ clearTimeout(maxTimer);
920
+ resolvePromise();
810
921
  });
811
922
  });
923
+ return pty;
924
+ }
925
+ onData(raw) {
926
+ const clean = stripAnsi(raw);
927
+ switch (this.ptyState) {
928
+ case "starting":
929
+ if (this.startupTimer) clearTimeout(this.startupTimer);
930
+ this.startupTimer = setTimeout(() => {
931
+ this.ptyState = "idle";
932
+ this.startupResolve?.();
933
+ this.startupResolve = null;
934
+ }, 2e3);
935
+ break;
936
+ case "idle":
937
+ break;
938
+ case "responding": {
939
+ if (!this.activeStream || !clean) break;
940
+ const filtered = this.filterOutput(clean);
941
+ if (filtered) this.activeStream.write(filtered);
942
+ this.resetSilenceTimer();
943
+ break;
944
+ }
945
+ }
946
+ }
947
+ filterOutput(text) {
948
+ const lines = text.split("\n");
949
+ const kept = [];
950
+ for (const line of lines) {
951
+ const trimmed = line.trim();
952
+ if (!trimmed) {
953
+ kept.push(line);
954
+ continue;
955
+ }
956
+ if (this.lastInput && (trimmed === this.lastInput.trim() || trimmed === `> ${this.lastInput.trim()}`)) continue;
957
+ if (isDecorativeLine(line) || isNoiseLine(line)) continue;
958
+ kept.push(line);
959
+ }
960
+ return kept.join("\n").replace(/^\n+/, "");
961
+ }
962
+ resetSilenceTimer() {
963
+ if (this.silenceTimer) clearTimeout(this.silenceTimer);
964
+ this.silenceTimer = setTimeout(() => {
965
+ this.ptyState = "idle";
966
+ this.finishResponse();
967
+ }, this.silenceTimeout);
968
+ }
969
+ finishResponse() {
970
+ if (this.silenceTimer) {
971
+ clearTimeout(this.silenceTimer);
972
+ this.silenceTimer = null;
973
+ }
974
+ const stream = this.activeStream;
975
+ const resolvePromise = this.activeResolve;
976
+ this.activeStream = null;
977
+ this.activeResolve = null;
978
+ if (stream && !stream.ended) stream.end();
979
+ resolvePromise?.();
980
+ }
981
+ async replResponse(messages, stream) {
982
+ const lastUserMsg = [...messages].reverse().find((m) => m.role === "user");
983
+ if (!lastUserMsg) {
984
+ stream.write("[No user message provided]");
985
+ stream.end();
986
+ return;
987
+ }
988
+ const pty = await this.ensurePty();
989
+ return new Promise((resolvePromise) => {
990
+ this.activeStream = stream;
991
+ this.activeResolve = resolvePromise;
992
+ this.ptyState = "responding";
993
+ this.lastInput = lastUserMsg.content;
994
+ pty.write(lastUserMsg.content + "\r");
995
+ this.resetSilenceTimer();
996
+ });
812
997
  }
813
998
  };
814
999
 
815
1000
  // src/config.ts
816
1001
  import { readFileSync, writeFileSync, mkdirSync } from "fs";
817
1002
  import { homedir } from "os";
818
- import { join } from "path";
819
- var CONFIG_DIR = join(homedir(), ".config", "airloom");
820
- var CONFIG_PATH = join(CONFIG_DIR, "config.json");
1003
+ import { join as join2 } from "path";
1004
+ var CONFIG_DIR = join2(homedir(), ".config", "airloom");
1005
+ var CONFIG_PATH = join2(CONFIG_DIR, "config.json");
821
1006
  function loadConfig() {
822
1007
  try {
823
1008
  const raw = readFileSync(CONFIG_PATH, "utf-8");
@@ -858,7 +1043,7 @@ function createHostServer(opts) {
858
1043
  app.get("/", (_req, res) => {
859
1044
  res.type("html").send(HOST_HTML);
860
1045
  });
861
- if (opts.viewerDir && existsSync(opts.viewerDir)) {
1046
+ if (opts.viewerDir && existsSync2(opts.viewerDir)) {
862
1047
  app.use("/viewer", express.static(opts.viewerDir));
863
1048
  }
864
1049
  app.get("/api/status", (_req, res) => {
@@ -912,7 +1097,13 @@ function createHostServer(opts) {
912
1097
  res.status(400).json({ error: "CLI adapter requires a command (or set AIRLOOM_CLI_COMMAND env var)" });
913
1098
  return;
914
1099
  }
915
- opts.state.adapter = new CLIAdapter({ command: cmd, model });
1100
+ const presetInfo = preset ? CLI_PRESETS.find((p) => p.id === preset) : void 0;
1101
+ opts.state.adapter = new CLIAdapter({
1102
+ command: cmd,
1103
+ model,
1104
+ mode: presetInfo?.mode,
1105
+ silenceTimeout: presetInfo?.silenceTimeout
1106
+ });
916
1107
  break;
917
1108
  }
918
1109
  default:
@@ -957,11 +1148,11 @@ function createHostServer(opts) {
957
1148
  if (ws.readyState === WebSocket.OPEN) ws.send(msg);
958
1149
  }
959
1150
  }
960
- return new Promise((resolve2) => {
1151
+ return new Promise((resolve3) => {
961
1152
  server.listen(opts.port, "0.0.0.0", () => {
962
1153
  const addr = server.address();
963
1154
  const actualPort = typeof addr === "object" && addr ? addr.port : opts.port;
964
- resolve2({ server, broadcast, port: actualPort });
1155
+ resolve3({ server, broadcast, port: actualPort });
965
1156
  });
966
1157
  server.on("error", (err) => {
967
1158
  console.error(`[host] Server error: ${err.message}`);
@@ -972,19 +1163,25 @@ function createHostServer(opts) {
972
1163
  async function handleAIResponse(channel, adapter, state, broadcast) {
973
1164
  const stream = channel.createStream({ model: adapter.model });
974
1165
  let fullResponse = "";
975
- const batcher = new Batcher({
1166
+ const uiBatcher = new Batcher({
976
1167
  interval: 100,
977
1168
  onFlush: (data) => broadcast({ type: "stream_chunk", data })
978
1169
  });
979
1170
  const origWrite = stream.write.bind(stream);
1171
+ const relayBatcher = new Batcher({
1172
+ interval: 500,
1173
+ maxBytes: 4096,
1174
+ onFlush: (data) => origWrite(data)
1175
+ });
980
1176
  stream.write = (data) => {
981
1177
  fullResponse += data;
982
- batcher.write(data);
983
- origWrite(data);
1178
+ uiBatcher.write(data);
1179
+ relayBatcher.write(data);
984
1180
  };
985
1181
  const origEnd = stream.end.bind(stream);
986
1182
  stream.end = () => {
987
- batcher.flush();
1183
+ uiBatcher.flush();
1184
+ relayBatcher.flush();
988
1185
  broadcast({ type: "stream_end" });
989
1186
  state.messages.push({ role: "assistant", content: fullResponse, timestamp: Date.now() });
990
1187
  trimMessages(state.messages);
@@ -998,7 +1195,8 @@ async function handleAIResponse(channel, adapter, state, broadcast) {
998
1195
  stream.write(`[Error: ${message}]`);
999
1196
  stream.end();
1000
1197
  }
1001
- batcher.destroy();
1198
+ uiBatcher.destroy();
1199
+ relayBatcher.destroy();
1002
1200
  }
1003
1201
  }
1004
1202
  var HOST_HTML = `<!DOCTYPE html>
@@ -1239,10 +1437,10 @@ function getLanIP() {
1239
1437
  return void 0;
1240
1438
  }
1241
1439
  function resolveViewerDir() {
1242
- const prod = resolve(__dirname, "viewer");
1243
- if (existsSync2(prod)) return prod;
1244
- const dev = resolve(__dirname, "../../viewer/dist");
1245
- if (existsSync2(dev)) return dev;
1440
+ const prod = resolve2(__dirname, "viewer");
1441
+ if (existsSync3(prod)) return prod;
1442
+ const dev = resolve2(__dirname, "../../viewer/dist");
1443
+ if (existsSync3(dev)) return dev;
1246
1444
  return void 0;
1247
1445
  }
1248
1446
  async function main() {
@@ -1308,17 +1506,22 @@ async function main() {
1308
1506
  };
1309
1507
  if (cliArgs.cli || cliArgs.preset) {
1310
1508
  let command = cliArgs.cli;
1311
- if (!command && cliArgs.preset) {
1312
- const preset = CLI_PRESETS.find((p) => p.id === cliArgs.preset);
1313
- if (!preset) {
1509
+ let presetInfo;
1510
+ if (cliArgs.preset) {
1511
+ presetInfo = CLI_PRESETS.find((p) => p.id === cliArgs.preset);
1512
+ if (!presetInfo) {
1314
1513
  console.error(`[host] Unknown preset "${cliArgs.preset}". Available: ${CLI_PRESETS.map((p) => p.id).join(", ")}`);
1315
1514
  process.exit(1);
1316
1515
  }
1317
- command = preset.command;
1516
+ if (!command) command = presetInfo.command;
1318
1517
  }
1319
1518
  if (command) {
1320
- state.adapter = new CLIAdapter({ command });
1321
- console.log(`[host] CLI adapter: ${command}`);
1519
+ state.adapter = new CLIAdapter({
1520
+ command,
1521
+ mode: presetInfo?.mode,
1522
+ silenceTimeout: presetInfo?.silenceTimeout
1523
+ });
1524
+ console.log(`[host] CLI adapter: ${command} (${presetInfo?.mode ?? "oneshot"})`);
1322
1525
  }
1323
1526
  } else {
1324
1527
  const saved = loadConfig();
@@ -1341,8 +1544,14 @@ async function main() {
1341
1544
  }
1342
1545
  case "cli": {
1343
1546
  const cmd = saved.command || process.env.AIRLOOM_CLI_COMMAND;
1547
+ const savedPreset = saved.preset ? CLI_PRESETS.find((p) => p.id === saved.preset) : void 0;
1344
1548
  if (cmd) {
1345
- state.adapter = new CLIAdapter({ command: cmd, model: saved.model });
1549
+ state.adapter = new CLIAdapter({
1550
+ command: cmd,
1551
+ model: saved.model,
1552
+ mode: savedPreset?.mode,
1553
+ silenceTimeout: savedPreset?.silenceTimeout
1554
+ });
1346
1555
  }
1347
1556
  break;
1348
1557
  }
@@ -1418,6 +1627,7 @@ async function main() {
1418
1627
  if (shuttingDown) return;
1419
1628
  shuttingDown = true;
1420
1629
  console.log("\n[host] Shutting down...");
1630
+ state.adapter?.destroy?.();
1421
1631
  try {
1422
1632
  channel.close();
1423
1633
  } catch {
@@ -1 +1 @@
1
- import{g as c}from"./index-BoCit9Kx.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-DLLnAccl.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};