itwillsync 1.3.8 → 1.5.2

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.
@@ -1,2 +1,2 @@
1
1
  (function(){const t=document.createElement("link").relList;if(t&&t.supports&&t.supports("modulepreload"))return;for(const a of document.querySelectorAll('link[rel="modulepreload"]'))o(a);new MutationObserver(a=>{for(const s of a)if(s.type==="childList")for(const i of s.addedNodes)i.tagName==="LINK"&&i.rel==="modulepreload"&&o(i)}).observe(document,{childList:!0,subtree:!0});function n(a){const s={};return a.integrity&&(s.integrity=a.integrity),a.referrerPolicy&&(s.referrerPolicy=a.referrerPolicy),a.crossOrigin==="use-credentials"?s.credentials="include":a.crossOrigin==="anonymous"?s.credentials="omit":s.credentials="same-origin",s}function o(a){if(a.ep)return;a.ep=!0;const s=n(a);fetch(a.href,s)}})();function B(e){const t=Math.floor(e/1e3);if(t<60)return`${t}s`;const n=Math.floor(t/60);if(n<60)return`${n}m`;const o=Math.floor(n/60),a=n%60;return`${o}h ${a}m`}function z(e){const t=e.match(/^\/(?:Users|home)\/[^/]+/)?.[0];return t?"~"+e.slice(t.length):e}function X(e){return e<1024?`${e} KB`:`${(e/1024).toFixed(1)} MB`}function H(e,t,n){const o=document.createElement("div");o.className=`session-card${e.status==="attention"?" attention":""}`,o.dataset.sessionId=e.id;const a=B(Date.now()-e.connectedAt),s=z(e.cwd),i=document.createElement("div");i.className="card-header";const c=document.createElement("div");c.className="card-agent";const p=document.createElement("div");p.className=`agent-dot ${e.status}`;const P=document.createElement("span");P.className="agent-name",P.textContent=e.name||e.agent,c.appendChild(p),c.appendChild(P);const k=document.createElement("span");k.className="card-uptime",k.textContent=a,i.appendChild(c),i.appendChild(k);const I=document.createElement("div");I.className="card-cwd",I.textContent=s;const h=document.createElement("div");h.className="card-preview";const $=document.createElement("pre");$.className="card-preview-text",$.textContent="Waiting for output...",h.appendChild($),h.addEventListener("click",l=>{l.stopPropagation(),n.onOpen(e)});const g=document.createElement("div");g.className="card-status";const E=document.createElement("span");E.className=`status-badge ${e.status}`,E.textContent=e.status,e.status==="attention"&&(E.style.display="none");const v=document.createElement("span");v.className="attention-badge",v.textContent="Needs your attention",e.status!=="attention"&&(v.style.display="none"),g.appendChild(E),g.appendChild(v);const f=document.createElement("div");f.className="card-actions";const C=document.createElement("button");C.className="action-btn stop",C.textContent="Stop",C.addEventListener("click",l=>{l.stopPropagation(),Q(o,e.id,n.onStop)});const w=document.createElement("button");w.className="action-btn rename",w.textContent="Rename",w.addEventListener("click",l=>{l.stopPropagation(),Z(o,e.id,n.onRename)});const N=document.createElement("button");N.className="action-btn info",N.textContent="Info",N.addEventListener("click",l=>{l.stopPropagation(),n.onInfo(e.id)});const b=document.createElement("button");b.className="action-btn open",b.textContent="Open",b.addEventListener("click",l=>{l.stopPropagation(),n.onOpen(e)}),f.appendChild(b),f.appendChild(w),f.appendChild(N),f.appendChild(C);const R=document.createElement("div");return R.className="card-metadata hidden",o.addEventListener("click",()=>{n.onOpen(e)}),o.appendChild(i),o.appendChild(I),o.appendChild(h),o.appendChild(g),o.appendChild(f),o.appendChild(R),o}function Q(e,t,n){e.querySelector(".confirm-overlay")?.remove();const o=document.createElement("div");o.className="confirm-overlay";const a=document.createElement("span");a.className="confirm-msg",a.textContent="Stop this session?";const s=document.createElement("button");s.className="confirm-btn yes",s.textContent="Yes",s.addEventListener("click",c=>{c.stopPropagation(),n(t),o.remove()});const i=document.createElement("button");i.className="confirm-btn no",i.textContent="No",i.addEventListener("click",c=>{c.stopPropagation(),o.remove()}),o.addEventListener("click",c=>c.stopPropagation()),o.appendChild(a),o.appendChild(s),o.appendChild(i),e.appendChild(o)}function Z(e,t,n){const o=e.querySelector(".agent-name");if(!o)return;const a=o.textContent||"",s=document.createElement("input");s.className="rename-input",s.type="text",s.value=a;const i=()=>{const c=s.value.trim();c&&c!==a&&n(t,c),o.textContent=c||a,o.style.display="",s.remove()};s.addEventListener("keydown",c=>{c.stopPropagation(),c.key==="Enter"?i():c.key==="Escape"&&(o.textContent=a,o.style.display="",s.remove())}),s.addEventListener("blur",i),s.addEventListener("click",c=>c.stopPropagation()),o.style.display="none",o.parentElement?.insertBefore(s,o.nextSibling),s.focus(),s.select()}function G(e,t){const n=e.querySelector(".agent-dot"),o=e.querySelector(".status-badge"),a=e.querySelector(".card-uptime"),s=e.querySelector(".agent-name");n&&(n.className=`agent-dot ${t.status}`),o&&(o.className=`status-badge ${t.status}`,o.textContent=t.status),a&&(a.textContent=B(Date.now()-t.connectedAt)),s&&!s.style.display&&(s.textContent=t.name||t.agent);const i=t.status==="attention";o&&(o.style.display=i?"none":"");const c=e.querySelector(".attention-badge");c&&(c.style.display=i?"":"none"),t.status==="attention"?e.classList.add("attention"):e.classList.remove("attention")}function ee(e,t){const n=e.querySelector(".card-preview-text");n&&(t.length>0?(n.textContent=t.join(`
2
- `),n.classList.remove("empty")):(n.textContent="Waiting for output...",n.classList.add("empty")))}function te(e,t){const n=e.querySelector(".card-metadata");if(!n)return;for(;n.firstChild;)n.removeChild(n.firstChild);const o=[["PID",String(t.pid)],["Agent",t.agent],["Port",String(t.port)],["Directory",t.cwd],["Memory",X(t.memoryKB)],["Uptime",B(t.uptimeMs)]];for(const[a,s]of o){const i=document.createElement("div");i.className="meta-row";const c=document.createElement("span");c.className="meta-label",c.textContent=a;const p=document.createElement("span");p.className="meta-value",p.textContent=s,i.appendChild(c),i.appendChild(p),n.appendChild(i)}n.classList.toggle("hidden")}let r=null,q=!1;function ne(){if(!q){q=!0;try{r=new AudioContext,r.state==="suspended"&&r.resume()}catch{}}}function U(){if(!r)return;r.state==="suspended"&&r.resume();const e=r.currentTime,t=r.createOscillator(),n=r.createGain();t.frequency.value=587.33,t.type="sine",n.gain.setValueAtTime(.3,e),n.gain.exponentialRampToValueAtTime(.01,e+.3),t.connect(n),n.connect(r.destination),t.start(e),t.stop(e+.3);const o=r.createOscillator(),a=r.createGain();o.frequency.value=880,o.type="sine",a.gain.setValueAtTime(.3,e+.15),a.gain.exponentialRampToValueAtTime(.01,e+.45),o.connect(a),a.connect(r.destination),o.start(e+.15),o.stop(e+.45)}const D=120*1e3,u=new Map;function _(e){if(u.has(e))return;U();const t=setTimeout(function n(){U();const o=u.get(e);o&&(o.timerId=setTimeout(n,D))},D);u.set(e,{timerId:t})}function A(e){const t=u.get(e);t&&(clearTimeout(t.timerId),u.delete(e))}function V(){for(const e of u.values())clearTimeout(e.timerId);u.clear()}const oe=new URLSearchParams(window.location.search),J=oe.get("token");if(!J)throw document.body.textContent="Missing authentication token.",new Error("No token in URL");const ae=window.location.protocol==="https:"?"wss:":"ws:",se=`${ae}//${window.location.host}?token=${J}`,Y=window.location.hostname,y=new Map,m=new Map,ce=document.getElementById("session-list"),O=document.getElementById("empty-state"),ie=document.getElementById("session-count"),W=document.getElementById("status-dot");let M=null;function x(){ne(),document.removeEventListener("click",x),document.removeEventListener("touchstart",x)}document.addEventListener("click",x);document.addEventListener("touchstart",x);function S(e){d&&d.readyState===WebSocket.OPEN&&d.send(JSON.stringify(e))}const re={onOpen(e){const t=y.get(e.id)||e;t.status==="attention"&&(S({type:"clear-attention",sessionId:t.id}),A(t.id));const n=window.location.href,o=`http://${Y}:${t.port}?token=${t.token}&hub=${encodeURIComponent(n)}`;window.open(o,"_blank")},onStop(e){S({type:"stop-session",sessionId:e})},onRename(e,t){S({type:"rename-session",sessionId:e,name:t})},onInfo(e){S({type:"get-metadata",sessionId:e})}};function j(){const e=y.size;ie.textContent=`${e} session${e!==1?"s":""}`,e===0?O.style.display="flex":O.style.display="none"}function F(e){y.set(e.id,e);const t=H(e,Y,re);m.set(e.id,t),ce.insertBefore(t,O),j()}function K(e){y.delete(e);const t=m.get(e);t&&(t.remove(),m.delete(e)),j()}function de(e){y.set(e.id,e);const t=m.get(e.id);t&&G(t,e)}function le(){for(const[e,t]of y){const n=m.get(e);n&&G(n,t)}}let d=null,L=0;const me=1e4;function T(){d=new WebSocket(se),d.onopen=()=>{W.className="connected",L=0,M&&clearInterval(M),M=setInterval(le,1e4)},d.onmessage=e=>{try{const t=JSON.parse(e.data);switch(t.type){case"sessions":{V();for(const n of m.keys())K(n);for(const n of t.sessions)F(n),n.status==="attention"&&_(n.id);break}case"session-added":{F(t.session);break}case"session-removed":{const n=t.sessionId;A(n),K(n);break}case"session-updated":{const n=t.session;de(n),n.status==="attention"?_(n.id):A(n.id);break}case"preview":{const n=m.get(t.sessionId);n&&ee(n,t.lines);break}case"metadata":{const n=m.get(t.sessionId);n&&te(n,t.metadata);break}case"operation-error":{console.warn(`Operation "${t.operation}" failed for session ${t.sessionId}: ${t.error}`);break}}}catch{}},d.onclose=()=>{W.className="reconnecting",V(),ue()},d.onerror=()=>{d?.close()}}function ue(){const e=Math.min(1e3*Math.pow(1.5,L),me);L++,setTimeout(T,e)}document.addEventListener("visibilitychange",()=>{document.visibilityState==="visible"&&d?.readyState!==WebSocket.OPEN&&(L=0,T())});T();
2
+ `),n.classList.remove("empty")):(n.textContent="Waiting for output...",n.classList.add("empty")))}function te(e,t){const n=e.querySelector(".card-metadata");if(!n)return;for(;n.firstChild;)n.removeChild(n.firstChild);const o=[["PID",String(t.pid)],["Agent",t.agent],["Port",String(t.port)],["Directory",t.cwd],["Memory",X(t.memoryKB)],["Uptime",B(t.uptimeMs)]];for(const[a,s]of o){const i=document.createElement("div");i.className="meta-row";const c=document.createElement("span");c.className="meta-label",c.textContent=a;const p=document.createElement("span");p.className="meta-value",p.textContent=s,i.appendChild(c),i.appendChild(p),n.appendChild(i)}n.classList.toggle("hidden")}let r=null,q=!1;function ne(){if(!q){q=!0;try{r=new AudioContext,r.state==="suspended"&&r.resume()}catch{}}}function U(){if(!r)return;r.state==="suspended"&&r.resume();const e=r.currentTime,t=r.createOscillator(),n=r.createGain();t.frequency.value=587.33,t.type="sine",n.gain.setValueAtTime(.3,e),n.gain.exponentialRampToValueAtTime(.01,e+.3),t.connect(n),n.connect(r.destination),t.start(e),t.stop(e+.3);const o=r.createOscillator(),a=r.createGain();o.frequency.value=880,o.type="sine",a.gain.setValueAtTime(.3,e+.15),a.gain.exponentialRampToValueAtTime(.01,e+.45),o.connect(a),a.connect(r.destination),o.start(e+.15),o.stop(e+.45)}const D=120*1e3,u=new Map;function V(e){if(u.has(e))return;U();const t=setTimeout(function n(){U();const o=u.get(e);o&&(o.timerId=setTimeout(n,D))},D);u.set(e,{timerId:t})}function A(e){const t=u.get(e);t&&(clearTimeout(t.timerId),u.delete(e))}function W(){for(const e of u.values())clearTimeout(e.timerId);u.clear()}const oe=new URLSearchParams(window.location.search),J=oe.get("token");if(!J)throw document.body.textContent="Missing authentication token.",new Error("No token in URL");const ae=window.location.protocol==="https:"?"wss:":"ws:",se=`${ae}//${window.location.host}?token=${J}`,Y=window.location.hostname,y=new Map,m=new Map,ce=document.getElementById("session-list"),O=document.getElementById("empty-state"),ie=document.getElementById("session-count"),_=document.getElementById("status-dot");let M=null;function x(){ne(),document.removeEventListener("click",x),document.removeEventListener("touchstart",x)}document.addEventListener("click",x);document.addEventListener("touchstart",x);function S(e){d&&d.readyState===WebSocket.OPEN&&d.send(JSON.stringify(e))}const re={onOpen(e){const t=y.get(e.id)||e;t.status==="attention"&&(S({type:"clear-attention",sessionId:t.id}),A(t.id));const n=window.location.href,o=`http://${Y}:${t.port}?token=${t.token}&hub=${encodeURIComponent(n)}`;window.open(o,`session-${t.id}`)},onStop(e){S({type:"stop-session",sessionId:e})},onRename(e,t){S({type:"rename-session",sessionId:e,name:t})},onInfo(e){S({type:"get-metadata",sessionId:e})}};function j(){const e=y.size;ie.textContent=`${e} session${e!==1?"s":""}`,e===0?O.style.display="flex":O.style.display="none"}function F(e){y.set(e.id,e);const t=H(e,Y,re);m.set(e.id,t),ce.insertBefore(t,O),j()}function K(e){y.delete(e);const t=m.get(e);t&&(t.remove(),m.delete(e)),j()}function de(e){y.set(e.id,e);const t=m.get(e.id);t&&G(t,e)}function le(){for(const[e,t]of y){const n=m.get(e);n&&G(n,t)}}let d=null,L=0;const me=1e4;function T(){d=new WebSocket(se),d.onopen=()=>{_.className="connected",L=0,M&&clearInterval(M),M=setInterval(le,1e4)},d.onmessage=e=>{try{const t=JSON.parse(e.data);switch(t.type){case"sessions":{W();for(const n of m.keys())K(n);for(const n of t.sessions)F(n),n.status==="attention"&&V(n.id);break}case"session-added":{F(t.session);break}case"session-removed":{const n=t.sessionId;A(n),K(n);break}case"session-updated":{const n=t.session;de(n),n.status==="attention"?V(n.id):A(n.id);break}case"preview":{const n=m.get(t.sessionId);n&&ee(n,t.lines);break}case"metadata":{const n=m.get(t.sessionId);n&&te(n,t.metadata);break}case"operation-error":{console.warn(`Operation "${t.operation}" failed for session ${t.sessionId}: ${t.error}`);break}}}catch{}},d.onclose=()=>{_.className="reconnecting",W(),ue()},d.onerror=()=>{d?.close()}}function ue(){const e=Math.min(1e3*Math.pow(1.5,L),me);L++,setTimeout(T,e)}document.addEventListener("visibilitychange",()=>{document.visibilityState==="visible"&&d?.readyState!==WebSocket.OPEN&&(L=0,T())});T();
@@ -7,7 +7,7 @@
7
7
  <meta name="apple-mobile-web-app-status-bar-style" content="black-translucent" />
8
8
  <meta name="theme-color" content="#1a1a2e" />
9
9
  <title>itwillsync Dashboard</title>
10
- <script type="module" crossorigin src="/assets/index-DgUZUPW_.js"></script>
10
+ <script type="module" crossorigin src="/assets/index-DdOxsvuU.js"></script>
11
11
  <link rel="stylesheet" crossorigin href="/assets/index-Erqx_a0N.css">
12
12
  </head>
13
13
  <body>
package/dist/index.js CHANGED
@@ -3732,9 +3732,11 @@ var PtyManager = class {
3732
3732
  */
3733
3733
  resize(cols, rows) {
3734
3734
  try {
3735
- this.ptyProcess.resize(cols, rows);
3736
- this._cols = cols;
3737
- this._rows = rows;
3735
+ const clampedCols = Math.max(1, Math.min(500, Math.floor(cols)));
3736
+ const clampedRows = Math.max(1, Math.min(200, Math.floor(rows)));
3737
+ this.ptyProcess.resize(clampedCols, clampedRows);
3738
+ this._cols = clampedCols;
3739
+ this._rows = clampedRows;
3738
3740
  } catch {
3739
3741
  }
3740
3742
  }
@@ -3833,17 +3835,40 @@ async function getTailscaleStatus() {
3833
3835
  }
3834
3836
 
3835
3837
  // src/network.ts
3838
+ var VIRTUAL_INTERFACE_PREFIXES = [
3839
+ "utun",
3840
+ "tun",
3841
+ "tap",
3842
+ "wg",
3843
+ // VPN/tunnel
3844
+ "tailscale",
3845
+ // Tailscale
3846
+ "docker",
3847
+ "br-",
3848
+ "veth",
3849
+ // Docker
3850
+ "virbr",
3851
+ "vboxnet",
3852
+ "vmnet"
3853
+ // VM
3854
+ ];
3855
+ function isVirtualInterface(name) {
3856
+ return VIRTUAL_INTERFACE_PREFIXES.some((prefix) => name.startsWith(prefix));
3857
+ }
3836
3858
  function getLocalIP() {
3837
3859
  const interfaces = networkInterfaces();
3838
- for (const addresses of Object.values(interfaces)) {
3860
+ let fallback = null;
3861
+ for (const [name, addresses] of Object.entries(interfaces)) {
3839
3862
  if (!addresses) continue;
3840
3863
  for (const addr of addresses) {
3841
- if (addr.family === "IPv4" && !addr.internal) {
3864
+ if (addr.family !== "IPv4" || addr.internal) continue;
3865
+ if (!isVirtualInterface(name)) {
3842
3866
  return addr.address;
3843
3867
  }
3868
+ fallback ??= addr.address;
3844
3869
  }
3845
3870
  }
3846
- return "127.0.0.1";
3871
+ return fallback ?? "127.0.0.1";
3847
3872
  }
3848
3873
  async function resolveSessionIP(mode, isLocalhost) {
3849
3874
  if (isLocalhost) return "127.0.0.1";
@@ -3937,12 +3962,25 @@ async function serveStaticFile(webClientPath, filePath, req, res) {
3937
3962
  }
3938
3963
  }
3939
3964
  var PING_INTERVAL_MS = 3e4;
3940
- var SCROLLBACK_BUFFER_SIZE = 5e4;
3965
+ var DEFAULT_SCROLLBACK_SIZE = 10485760;
3966
+ var DEFAULT_CLIENT_BUFFER_LIMIT = 262144;
3941
3967
  function createSyncServer(options) {
3942
- const { ptyManager, token, webClientPath, host, port, localTerminalOwnsResize = false } = options;
3968
+ const {
3969
+ ptyManager,
3970
+ token,
3971
+ webClientPath,
3972
+ host,
3973
+ port,
3974
+ resizePolicy = "last-writer-wins",
3975
+ scrollbackBufferSize = DEFAULT_SCROLLBACK_SIZE,
3976
+ clientBufferLimit = DEFAULT_CLIENT_BUFFER_LIMIT,
3977
+ logger
3978
+ } = options;
3943
3979
  const clients = /* @__PURE__ */ new Set();
3944
3980
  const aliveMap = /* @__PURE__ */ new WeakMap();
3981
+ const dropCounters = /* @__PURE__ */ new WeakMap();
3945
3982
  let scrollbackBuffer = "";
3983
+ let scrollbackBytes = 0;
3946
3984
  let seq = 0;
3947
3985
  const httpServer = createServer2(async (req, res) => {
3948
3986
  const url = new URL(req.url || "/", `http://${req.headers.host}`);
@@ -3979,9 +4017,12 @@ function createSyncServer(options) {
3979
4017
  wssServer.on("connection", (ws) => {
3980
4018
  clients.add(ws);
3981
4019
  aliveMap.set(ws, true);
3982
- if (scrollbackBuffer.length > 0) {
3983
- ws.send(JSON.stringify({ type: "data", data: scrollbackBuffer, seq }));
3984
- }
4020
+ let syncReceived = false;
4021
+ const fallbackTimer = setTimeout(() => {
4022
+ if (!syncReceived && scrollbackBuffer.length > 0) {
4023
+ ws.send(JSON.stringify({ type: "data", data: scrollbackBuffer, seq }));
4024
+ }
4025
+ }, 150);
3985
4026
  ws.send(JSON.stringify({ type: "resize", cols: ptyManager.cols, rows: ptyManager.rows }));
3986
4027
  ws.on("pong", () => {
3987
4028
  aliveMap.set(ws, true);
@@ -3992,10 +4033,32 @@ function createSyncServer(options) {
3992
4033
  if (message.type === "input" && typeof message.data === "string") {
3993
4034
  ptyManager.write(message.data);
3994
4035
  } else if (message.type === "resize" && typeof message.cols === "number" && typeof message.rows === "number") {
3995
- if (!localTerminalOwnsResize) {
4036
+ if (resizePolicy === "last-writer-wins") {
3996
4037
  ptyManager.resize(message.cols, message.rows);
4038
+ const resizeMsg = JSON.stringify({ type: "resize", cols: ptyManager.cols, rows: ptyManager.rows });
4039
+ for (const c of clients) {
4040
+ if (c.readyState === c.OPEN) {
4041
+ c.send(resizeMsg);
4042
+ }
4043
+ }
4044
+ }
4045
+ } else if (message.type === "sync" && typeof message.lastSeq === "number") {
4046
+ syncReceived = true;
4047
+ clearTimeout(fallbackTimer);
4048
+ if (message.lastSeq === -1 || message.lastSeq < seq - scrollbackBuffer.length) {
4049
+ if (scrollbackBuffer.length > 0) {
4050
+ ws.send(JSON.stringify({ type: "data", data: scrollbackBuffer, seq }));
4051
+ }
4052
+ } else {
4053
+ const missed = seq - message.lastSeq;
4054
+ if (missed > 0 && scrollbackBuffer.length > 0) {
4055
+ const delta = missed <= scrollbackBuffer.length ? scrollbackBuffer.slice(-missed) : scrollbackBuffer;
4056
+ ws.send(JSON.stringify({ type: "data", data: delta, seq }));
4057
+ }
3997
4058
  }
3998
4059
  } else if (message.type === "resume" && typeof message.lastSeq === "number") {
4060
+ syncReceived = true;
4061
+ clearTimeout(fallbackTimer);
3999
4062
  const missed = seq - message.lastSeq;
4000
4063
  if (missed > 0 && scrollbackBuffer.length > 0) {
4001
4064
  const delta = missed <= scrollbackBuffer.length ? scrollbackBuffer.slice(-missed) : scrollbackBuffer;
@@ -4013,16 +4076,31 @@ function createSyncServer(options) {
4013
4076
  });
4014
4077
  });
4015
4078
  ptyManager.onData((data) => {
4079
+ if (logger) logger.write(data);
4016
4080
  seq += data.length;
4017
4081
  scrollbackBuffer += data;
4018
- if (scrollbackBuffer.length > SCROLLBACK_BUFFER_SIZE) {
4019
- scrollbackBuffer = scrollbackBuffer.slice(-SCROLLBACK_BUFFER_SIZE);
4082
+ scrollbackBytes += Buffer.byteLength(data, "utf-8");
4083
+ if (scrollbackBytes > scrollbackBufferSize) {
4084
+ const target = Math.floor(scrollbackBufferSize * 0.5);
4085
+ const excessBytes = scrollbackBytes - target;
4086
+ const avgBytesPerChar = scrollbackBytes / scrollbackBuffer.length;
4087
+ const trimChars = Math.min(Math.ceil(excessBytes / avgBytesPerChar), scrollbackBuffer.length);
4088
+ const trimmed = scrollbackBuffer.slice(0, trimChars);
4089
+ scrollbackBuffer = scrollbackBuffer.slice(trimChars);
4090
+ scrollbackBytes -= Buffer.byteLength(trimmed, "utf-8");
4020
4091
  }
4021
4092
  const msg = JSON.stringify({ type: "data", data, seq });
4022
4093
  for (const client of clients) {
4023
- if (client.readyState === client.OPEN) {
4024
- client.send(msg);
4094
+ if (client.readyState !== client.OPEN) continue;
4095
+ if (client.bufferedAmount > clientBufferLimit) {
4096
+ const count = (dropCounters.get(client) || 0) + 1;
4097
+ dropCounters.set(client, count);
4098
+ if (count === 100 || count > 100 && count % 1e3 === 0) {
4099
+ console.warn(` Warning: Dropped ${count} messages for slow client`);
4100
+ }
4101
+ continue;
4025
4102
  }
4103
+ client.send(msg);
4026
4104
  }
4027
4105
  });
4028
4106
  httpServer.listen(port, host);
@@ -4038,8 +4116,9 @@ function createSyncServer(options) {
4038
4116
  wssServer.close();
4039
4117
  httpServer.close();
4040
4118
  },
4041
- broadcastResize(cols, rows) {
4042
- const msg = JSON.stringify({ type: "resize", cols, rows });
4119
+ resizeFromLocal(cols, rows) {
4120
+ ptyManager.resize(cols, rows);
4121
+ const msg = JSON.stringify({ type: "resize", cols: ptyManager.cols, rows: ptyManager.rows });
4043
4122
  for (const client of clients) {
4044
4123
  if (client.readyState === client.OPEN) {
4045
4124
  client.send(msg);
@@ -4072,7 +4151,17 @@ import { existsSync, readFileSync, writeFileSync, mkdirSync } from "fs";
4072
4151
  import { homedir } from "os";
4073
4152
  import { join as join3 } from "path";
4074
4153
  var DEFAULT_CONFIG = {
4075
- networkingMode: "local"
4154
+ networkingMode: "local",
4155
+ scrollbackBufferSize: 10485760,
4156
+ // 10MB
4157
+ maxSessions: 20,
4158
+ idleTimeoutMs: 864e5,
4159
+ // 24 hours
4160
+ logRetentionDays: 30,
4161
+ clientBufferLimit: 262144,
4162
+ // 256KB
4163
+ maxTerminalCols: 500,
4164
+ maxTerminalRows: 200
4076
4165
  };
4077
4166
  function getConfigDir() {
4078
4167
  return process.env.ITWILLSYNC_CONFIG_DIR || join3(homedir(), ".itwillsync");
@@ -4102,6 +4191,65 @@ function saveConfig(config) {
4102
4191
  );
4103
4192
  }
4104
4193
 
4194
+ // src/session-logger.ts
4195
+ import { createWriteStream, mkdirSync as mkdirSync2, unlinkSync } from "fs";
4196
+ import { createGzip } from "zlib";
4197
+ import { pipeline } from "stream/promises";
4198
+ import { createReadStream } from "fs";
4199
+ import { join as join4 } from "path";
4200
+ var FLUSH_INTERVAL_MS = 100;
4201
+ var BUFFER_SIZE = 4096;
4202
+ function getLogsDir() {
4203
+ return join4(getConfigDir(), "logs");
4204
+ }
4205
+ var SessionLogger = class {
4206
+ buffer = "";
4207
+ stream;
4208
+ logPath;
4209
+ flushTimer;
4210
+ closed = false;
4211
+ constructor(sessionId) {
4212
+ const logsDir = getLogsDir();
4213
+ mkdirSync2(logsDir, { recursive: true });
4214
+ this.logPath = join4(logsDir, `${sessionId}.log`);
4215
+ this.stream = createWriteStream(this.logPath, { flags: "a" });
4216
+ this.flushTimer = setInterval(() => this.flush(), FLUSH_INTERVAL_MS);
4217
+ }
4218
+ write(data) {
4219
+ if (this.closed) return;
4220
+ this.buffer += data;
4221
+ if (this.buffer.length >= BUFFER_SIZE) {
4222
+ this.flush();
4223
+ }
4224
+ }
4225
+ flush() {
4226
+ if (this.buffer.length === 0 || this.closed) return;
4227
+ this.stream.write(this.buffer);
4228
+ this.buffer = "";
4229
+ }
4230
+ async close() {
4231
+ if (this.closed) return;
4232
+ this.closed = true;
4233
+ clearInterval(this.flushTimer);
4234
+ this.flush();
4235
+ await new Promise((resolve) => {
4236
+ this.stream.end(() => resolve());
4237
+ });
4238
+ try {
4239
+ const gzPath = this.logPath + ".gz";
4240
+ const source = createReadStream(this.logPath);
4241
+ const gzip = createGzip();
4242
+ const dest = createWriteStream(gzPath);
4243
+ await pipeline(source, gzip, dest);
4244
+ try {
4245
+ unlinkSync(this.logPath);
4246
+ } catch {
4247
+ }
4248
+ } catch {
4249
+ }
4250
+ }
4251
+ };
4252
+
4105
4253
  // src/wizard.ts
4106
4254
  import * as p from "@clack/prompts";
4107
4255
  async function runSetupWizard() {
@@ -4173,23 +4321,23 @@ async function runSetupWizard() {
4173
4321
  // src/cli-options.ts
4174
4322
  import { readFileSync as readFileSync3 } from "fs";
4175
4323
  import { fileURLToPath as fileURLToPath3 } from "url";
4176
- import { dirname as dirname3, join as join5 } from "path";
4324
+ import { dirname as dirname3, join as join6 } from "path";
4177
4325
 
4178
4326
  // src/hub-client.ts
4179
4327
  import { spawn as spawn2 } from "child_process";
4180
- import { readFileSync as readFileSync2, existsSync as existsSync2, unlinkSync } from "fs";
4328
+ import { readFileSync as readFileSync2, existsSync as existsSync2, unlinkSync as unlinkSync2 } from "fs";
4181
4329
  import { homedir as homedir2 } from "os";
4182
- import { join as join4, dirname as dirname2 } from "path";
4330
+ import { join as join5, dirname as dirname2 } from "path";
4183
4331
  import { fileURLToPath as fileURLToPath2 } from "url";
4184
4332
  import { request } from "http";
4185
4333
  var HUB_INTERNAL_PORT = 7963;
4186
4334
  var HUB_EXTERNAL_PORT = 7962;
4187
4335
  var SESSION_PORT_START = 7964;
4188
4336
  function getHubDir() {
4189
- return process.env.ITWILLSYNC_CONFIG_DIR || join4(homedir2(), ".itwillsync");
4337
+ return process.env.ITWILLSYNC_CONFIG_DIR || join5(homedir2(), ".itwillsync");
4190
4338
  }
4191
4339
  function getHubConfigPath() {
4192
- return join4(getHubDir(), "hub.json");
4340
+ return join5(getHubDir(), "hub.json");
4193
4341
  }
4194
4342
  async function discoverHub() {
4195
4343
  return new Promise((resolve) => {
@@ -4272,18 +4420,18 @@ async function killStaleHub() {
4272
4420
  }
4273
4421
  const hubDir = getHubDir();
4274
4422
  try {
4275
- unlinkSync(join4(hubDir, "hub.json"));
4423
+ unlinkSync2(join5(hubDir, "hub.json"));
4276
4424
  } catch {
4277
4425
  }
4278
4426
  try {
4279
- unlinkSync(join4(hubDir, "hub.pid"));
4427
+ unlinkSync2(join5(hubDir, "hub.pid"));
4280
4428
  } catch {
4281
4429
  }
4282
4430
  return true;
4283
4431
  }
4284
4432
  async function spawnHub() {
4285
4433
  const __dirname2 = dirname2(fileURLToPath2(import.meta.url));
4286
- const hubPath = join4(__dirname2, "hub", "daemon.js");
4434
+ const hubPath = join5(__dirname2, "hub", "daemon.js");
4287
4435
  return new Promise((resolve, reject) => {
4288
4436
  const child = spawn2("node", [hubPath], {
4289
4437
  detached: true,
@@ -4478,7 +4626,7 @@ async function sendHeartbeat(sessionId) {
4478
4626
  var __filename = fileURLToPath3(import.meta.url);
4479
4627
  var __dirname = dirname3(__filename);
4480
4628
  var { version } = JSON.parse(
4481
- readFileSync3(join5(__dirname, "..", "package.json"), "utf-8")
4629
+ readFileSync3(join6(__dirname, "..", "package.json"), "utf-8")
4482
4630
  );
4483
4631
  var DEFAULT_PORT = SESSION_PORT_START;
4484
4632
  function parseArgs(argv) {
@@ -4594,7 +4742,7 @@ Hub Management:
4594
4742
 
4595
4743
  // src/index.ts
4596
4744
  import { fileURLToPath as fileURLToPath4 } from "url";
4597
- import { join as join6, dirname as dirname4 } from "path";
4745
+ import { join as join7, dirname as dirname4 } from "path";
4598
4746
  import { spawn as spawn3 } from "child_process";
4599
4747
  function preventSleep() {
4600
4748
  try {
@@ -4729,13 +4877,13 @@ async function main() {
4729
4877
  printHelp();
4730
4878
  process.exit(1);
4731
4879
  }
4880
+ const config = loadConfig();
4732
4881
  let networkingMode = "local";
4733
4882
  if (options.tailscale) {
4734
4883
  networkingMode = "tailscale";
4735
4884
  } else if (options.local) {
4736
4885
  networkingMode = "local";
4737
4886
  } else {
4738
- const config = loadConfig();
4739
4887
  networkingMode = config.networkingMode;
4740
4888
  }
4741
4889
  const [cmd, ...cmdArgs] = options.command;
@@ -4746,15 +4894,20 @@ async function main() {
4746
4894
  const host = options.localhost ? "127.0.0.1" : "0.0.0.0";
4747
4895
  const ip = await resolveSessionIP(networkingMode, options.localhost);
4748
4896
  const __dirname2 = dirname4(fileURLToPath4(import.meta.url));
4749
- const webClientPath = join6(__dirname2, "web-client");
4897
+ const webClientPath = join7(__dirname2, "web-client");
4750
4898
  const ptyManager = new PtyManager(cmd, cmdArgs);
4899
+ const sessionId = `${cmd}-${Date.now().toString(36)}`;
4900
+ const sessionLogger = new SessionLogger(sessionId);
4751
4901
  const server = createSyncServer({
4752
4902
  ptyManager,
4753
4903
  token,
4754
4904
  webClientPath,
4755
4905
  host,
4756
4906
  port,
4757
- localTerminalOwnsResize: true
4907
+ resizePolicy: "last-writer-wins",
4908
+ scrollbackBufferSize: config.scrollbackBufferSize,
4909
+ clientBufferLimit: config.clientBufferLimit,
4910
+ logger: sessionLogger
4758
4911
  });
4759
4912
  let registeredSession = null;
4760
4913
  let heartbeatInterval = null;
@@ -4805,6 +4958,11 @@ async function main() {
4805
4958
  process.stdin.resume();
4806
4959
  process.stdin.setEncoding("utf-8");
4807
4960
  process.stdin.on("data", (data) => {
4961
+ if (process.stdout.columns && process.stdout.rows) {
4962
+ if (process.stdout.columns !== ptyManager.cols || process.stdout.rows !== ptyManager.rows) {
4963
+ server.resizeFromLocal(process.stdout.columns, process.stdout.rows);
4964
+ }
4965
+ }
4808
4966
  ptyManager.write(data);
4809
4967
  });
4810
4968
  ptyManager.onData((data) => {
@@ -4812,8 +4970,7 @@ async function main() {
4812
4970
  });
4813
4971
  function handleResize() {
4814
4972
  if (process.stdout.columns && process.stdout.rows) {
4815
- ptyManager.resize(process.stdout.columns, process.stdout.rows);
4816
- server.broadcastResize(process.stdout.columns, process.stdout.rows);
4973
+ server.resizeFromLocal(process.stdout.columns, process.stdout.rows);
4817
4974
  }
4818
4975
  }
4819
4976
  process.stdout.on("resize", handleResize);
@@ -4835,6 +4992,7 @@ async function main() {
4835
4992
  sleepGuard?.kill();
4836
4993
  server.close();
4837
4994
  ptyManager.kill();
4995
+ await sessionLogger.close();
4838
4996
  }
4839
4997
  ptyManager.onExit(async (exitCode) => {
4840
4998
  console.log(`