muuuuse 3.3.4 → 3.3.6

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "muuuuse",
3
- "version": "3.3.4",
3
+ "version": "3.3.6",
4
4
  "description": "🔌Muuuuse arms regular terminals and relays assistant output across signed terminal links.",
5
5
  "type": "commonjs",
6
6
  "bin": {
package/src/agents.js CHANGED
@@ -179,22 +179,73 @@ function readCodexSeatClaim(sessionId) {
179
179
  return null;
180
180
  }
181
181
 
182
- const snapshotPath = path.join(CODEX_SNAPSHOT_ROOT, `${sessionId}.sh`);
182
+ const claims = readCodexSeatClaims(sessionId);
183
+ return claims[0] || null;
184
+ }
185
+
186
+ function readCodexSeatClaims(sessionId) {
187
+ if (!sessionId) {
188
+ return [];
189
+ }
190
+
191
+ const candidatePaths = listCodexSnapshotPaths(sessionId);
192
+ return candidatePaths
193
+ .map((snapshotPath) => {
194
+ let text;
195
+ try {
196
+ text = fs.readFileSync(snapshotPath, "utf8");
197
+ } catch {
198
+ return null;
199
+ }
200
+
201
+ const seatMatch = text.match(/declare -x MUUUUSE_SEAT="([^"]+)"/);
202
+ const sessionMatch = text.match(/declare -x MUUUUSE_SESSION="([^"]+)"/);
203
+ if (!seatMatch || !sessionMatch) {
204
+ return null;
205
+ }
206
+
207
+ let mtimeMs = 0;
208
+ try {
209
+ mtimeMs = fs.statSync(snapshotPath).mtimeMs;
210
+ } catch {
211
+ // Best effort only.
212
+ }
213
+
214
+ return {
215
+ seatId: seatMatch[1],
216
+ sessionName: sessionMatch[1],
217
+ snapshotPath,
218
+ mtimeMs,
219
+ };
220
+ })
221
+ .filter((claim) => claim !== null)
222
+ .sort((left, right) => right.mtimeMs - left.mtimeMs || left.snapshotPath.localeCompare(right.snapshotPath));
223
+ }
224
+
225
+ function listCodexSnapshotPaths(sessionId) {
226
+ const exactName = `${sessionId}.sh`;
227
+ const prefix = `${sessionId}.`;
228
+ const candidateNames = [];
229
+
183
230
  try {
184
- const text = fs.readFileSync(snapshotPath, "utf8");
185
- const seatMatch = text.match(/declare -x MUUUUSE_SEAT="([^"]+)"/);
186
- const sessionMatch = text.match(/declare -x MUUUUSE_SESSION="([^"]+)"/);
187
- if (!seatMatch || !sessionMatch) {
188
- return null;
189
- }
231
+ for (const entry of fs.readdirSync(CODEX_SNAPSHOT_ROOT, { withFileTypes: true })) {
232
+ if (!entry.isFile()) {
233
+ continue;
234
+ }
190
235
 
191
- return {
192
- seatId: seatMatch[1],
193
- sessionName: sessionMatch[1],
194
- };
236
+ if (entry.name === exactName || (entry.name.startsWith(prefix) && entry.name.endsWith(".sh"))) {
237
+ candidateNames.push(entry.name);
238
+ }
239
+ }
195
240
  } catch {
196
- return null;
241
+ // Snapshot directory may not exist yet.
197
242
  }
243
+
244
+ return uniquePaths(candidateNames.map((name) => path.join(CODEX_SNAPSHOT_ROOT, name)));
245
+ }
246
+
247
+ function candidateHasClaim(candidate, predicate) {
248
+ return readCodexSeatClaims(candidate.sessionId).some((claim) => predicate(claim));
198
249
  }
199
250
 
200
251
  function selectClaimedCodexCandidatePath(candidates, options = {}) {
@@ -204,29 +255,28 @@ function selectClaimedCodexCandidatePath(candidates, options = {}) {
204
255
  return null;
205
256
  }
206
257
 
207
- const annotated = candidates.map((candidate) => ({
208
- ...candidate,
209
- claim: readCodexSeatClaim(candidate.sessionId),
210
- }));
211
-
212
- const exactMatches = annotated.filter((candidate) => (
213
- candidate.claim?.seatId === seatId &&
214
- candidate.claim?.sessionName === sessionName
258
+ const exactMatches = candidates.filter((candidate) => (
259
+ candidateHasClaim(candidate, (claim) => (
260
+ claim.seatId === seatId &&
261
+ claim.sessionName === sessionName
262
+ ))
215
263
  ));
216
264
  if (exactMatches.length === 1) {
217
265
  return exactMatches[0].path;
218
266
  }
219
267
 
220
- const otherSeatClaims = annotated.filter((candidate) => (
221
- candidate.claim?.sessionName === sessionName &&
222
- candidate.claim?.seatId !== seatId
268
+ const otherSeatClaims = candidates.filter((candidate) => (
269
+ candidateHasClaim(candidate, (claim) => (
270
+ claim.sessionName === sessionName &&
271
+ claim.seatId !== seatId
272
+ ))
223
273
  ));
224
274
  if (otherSeatClaims.length === 0) {
225
275
  return null;
226
276
  }
227
277
 
228
278
  const foreignPaths = new Set(otherSeatClaims.map((candidate) => candidate.path));
229
- const remaining = annotated.filter((candidate) => !foreignPaths.has(candidate.path));
279
+ const remaining = candidates.filter((candidate) => !foreignPaths.has(candidate.path));
230
280
  if (remaining.length === 1) {
231
281
  return remaining[0].path;
232
282
  }
@@ -332,8 +382,10 @@ function selectExactClaimedCodexCandidate(candidates, options = {}, processStart
332
382
 
333
383
  const exactMatches = rankCodexCandidates(
334
384
  candidates.filter((candidate) => {
335
- const claim = readCodexSeatClaim(candidate.sessionId);
336
- return claim?.seatId === seatId && claim?.sessionName === sessionName;
385
+ return candidateHasClaim(candidate, (claim) => (
386
+ claim.seatId === seatId &&
387
+ claim.sessionName === sessionName
388
+ ));
337
389
  }),
338
390
  processStartedAtMs
339
391
  );
@@ -349,8 +401,11 @@ function filterForeignClaimedCodexCandidates(candidates, options = {}) {
349
401
  }
350
402
 
351
403
  return candidates.filter((candidate) => {
352
- const claim = readCodexSeatClaim(candidate.sessionId);
353
- return !(claim?.sessionName === sessionName && claim?.seatId && claim.seatId !== seatId);
404
+ return !candidateHasClaim(candidate, (claim) => (
405
+ claim.sessionName === sessionName &&
406
+ claim.seatId &&
407
+ claim.seatId !== seatId
408
+ ));
354
409
  });
355
410
  }
356
411
 
@@ -391,8 +446,11 @@ function selectCodexCandidatePath(candidates, currentPath, processStartedAtMs, o
391
446
  seatId &&
392
447
  sessionName &&
393
448
  cwdMatches.some((candidate) => {
394
- const claim = readCodexSeatClaim(candidate.sessionId);
395
- return claim?.sessionName === sessionName && claim?.seatId && claim.seatId !== seatId;
449
+ return candidateHasClaim(candidate, (claim) => (
450
+ claim.sessionName === sessionName &&
451
+ claim.seatId &&
452
+ claim.seatId !== seatId
453
+ ));
396
454
  })
397
455
  );
398
456
  const allowedMatches = filterForeignClaimedCodexCandidates(cwdMatches, options);
package/src/runtime.js CHANGED
@@ -42,6 +42,7 @@ const {
42
42
  // A short settle delay keeps interactive CLIs from treating submit as another newline.
43
43
  const TYPE_CHUNK_DELAY_MS = 45;
44
44
  const TYPE_CHUNK_SIZE = 24;
45
+ const GEMINI_PASTE_SETTLE_MS = 200;
45
46
  const BRACKETED_PASTE_START = "\u001b[200~";
46
47
  const BRACKETED_PASTE_END = "\u001b[201~";
47
48
  const GEMINI_SHARED_ENTRY_NAMES = new Set([
@@ -56,8 +57,11 @@ const EMITTED_ANSWER_TTL_MS = 5 * 60 * 1000;
56
57
  const MAX_RECENT_INBOUND_RELAYS = 12;
57
58
  const MAX_RECENT_EMITTED_ANSWERS = 48;
58
59
  const STOP_FORCE_KILL_MS = 1200;
60
+ const STOP_PURGE_WAIT_MS = STOP_FORCE_KILL_MS + 1200;
61
+ const STOP_PURGE_POLL_MS = 60;
59
62
  const SEAT_JOIN_WAIT_MS = 3000;
60
63
  const SEAT_JOIN_POLL_MS = 60;
64
+ const MAX_PENDING_PASSIVE_INPUT_CHARS = 512;
61
65
  const CHILD_ENV_DROP_KEYS = [
62
66
  "CODEX_CI",
63
67
  "CODEX_MANAGED_BY_NPM",
@@ -200,6 +204,7 @@ function ensureSeatGeminiCliHome(homeDir, cwd, seatId, baseEnv = process.env) {
200
204
  const sourceHomeRoot = String(baseEnv.GEMINI_CLI_HOME || homeDir).trim() || homeDir;
201
205
  const sourceGeminiDir = path.join(sourceHomeRoot, ".gemini");
202
206
  const targetHomeRoot = getSeatGeminiCliHome(homeDir, cwd, seatId);
207
+ fs.rmSync(targetHomeRoot, { recursive: true, force: true });
203
208
  const targetGeminiDir = ensureDir(path.join(targetHomeRoot, ".gemini"));
204
209
 
205
210
  let sourceEntries = [];
@@ -653,6 +658,103 @@ function isMeaningfulTerminalInput(input) {
653
658
  return stripPassiveTerminalInput(input).length > 0;
654
659
  }
655
660
 
661
+ function consumeTerminalProxyInput(input, pendingPassiveInput = "") {
662
+ const combined = `${pendingPassiveInput}${String(input || "")}`;
663
+ let forwardText = "";
664
+ let index = 0;
665
+
666
+ while (index < combined.length) {
667
+ const current = combined[index];
668
+ if (current !== "\u001b") {
669
+ forwardText += current;
670
+ index += 1;
671
+ continue;
672
+ }
673
+
674
+ if (index + 1 >= combined.length) {
675
+ forwardText += current;
676
+ index += 1;
677
+ continue;
678
+ }
679
+
680
+ const next = combined[index + 1];
681
+ if (next === "]") {
682
+ const belIndex = combined.indexOf("\u0007", index + 2);
683
+ const stIndex = combined.indexOf("\u001b\\", index + 2);
684
+ const endIndex = (
685
+ belIndex !== -1 && stIndex !== -1 ? Math.min(belIndex, stIndex) :
686
+ belIndex !== -1 ? belIndex :
687
+ stIndex
688
+ );
689
+
690
+ if (endIndex === -1) {
691
+ const pending = combined.slice(index).slice(0, MAX_PENDING_PASSIVE_INPUT_CHARS);
692
+ return {
693
+ forwardText,
694
+ meaningful: isMeaningfulTerminalInput(forwardText),
695
+ pendingPassiveInput: pending,
696
+ };
697
+ }
698
+
699
+ index = endIndex + (endIndex === stIndex ? 2 : 1);
700
+ continue;
701
+ }
702
+
703
+ if (next === "[") {
704
+ if (index + 2 >= combined.length) {
705
+ return {
706
+ forwardText,
707
+ meaningful: isMeaningfulTerminalInput(forwardText),
708
+ pendingPassiveInput: combined.slice(index).slice(0, MAX_PENDING_PASSIVE_INPUT_CHARS),
709
+ };
710
+ }
711
+
712
+ let endIndex = index + 2;
713
+ while (endIndex < combined.length && !/[@-~]/.test(combined[endIndex])) {
714
+ endIndex += 1;
715
+ }
716
+
717
+ if (endIndex >= combined.length) {
718
+ const pending = combined.slice(index);
719
+ if (/^\u001b\[(?:\??[0-9;]*)?$/.test(pending)) {
720
+ return {
721
+ forwardText,
722
+ meaningful: isMeaningfulTerminalInput(forwardText),
723
+ pendingPassiveInput: pending.slice(0, MAX_PENDING_PASSIVE_INPUT_CHARS),
724
+ };
725
+ }
726
+
727
+ forwardText += pending;
728
+ break;
729
+ }
730
+
731
+ const sequence = combined.slice(index, endIndex + 1);
732
+ if (
733
+ sequence === "\u001b[I" ||
734
+ sequence === "\u001b[O" ||
735
+ /^\u001b\[\d+;\d+R$/.test(sequence) ||
736
+ /^\u001b\[\?[0-9;]*c$/.test(sequence)
737
+ ) {
738
+ index = endIndex + 1;
739
+ continue;
740
+ }
741
+
742
+ forwardText += sequence;
743
+ index = endIndex + 1;
744
+ continue;
745
+ }
746
+
747
+ forwardText += current;
748
+ index += 1;
749
+ }
750
+
751
+ return {
752
+ forwardText,
753
+ meaningful: isMeaningfulTerminalInput(forwardText),
754
+ pendingPassiveInput: "",
755
+ };
756
+ }
757
+
656
758
  async function sendTextAndEnter(child, text, shouldAbort = () => false) {
657
759
  const options = typeof shouldAbort === "function" ? { shouldAbort } : (shouldAbort || {});
658
760
  const shouldStop = typeof options.shouldAbort === "function" ? options.shouldAbort : () => false;
@@ -660,7 +762,7 @@ async function sendTextAndEnter(child, text, shouldAbort = () => false) {
660
762
  const payload = normalizeRelayPayloadForTyping(text);
661
763
 
662
764
  if (payload.length > 0) {
663
- if (agentType === "codex") {
765
+ if (agentType === "codex" || agentType === "gemini") {
664
766
  if (shouldStop() || !child) {
665
767
  return false;
666
768
  }
@@ -670,6 +772,10 @@ async function sendTextAndEnter(child, text, shouldAbort = () => false) {
670
772
  } catch {
671
773
  return false;
672
774
  }
775
+
776
+ if (agentType === "gemini") {
777
+ await sleep(GEMINI_PASTE_SETTLE_MS);
778
+ }
673
779
  } else {
674
780
  for (const chunk of chunkRelayPayloadForTyping(payload)) {
675
781
  if (shouldStop() || !child) {
@@ -730,6 +836,7 @@ class ArmedSeat {
730
836
  this.forceKillTimer = null;
731
837
  this.identity = loadOrCreateSeatIdentity(this.paths);
732
838
  this.lastUserInputAtMs = 0;
839
+ this.pendingPassiveInput = "";
733
840
  this.pendingInboundContext = null;
734
841
  this.recentInboundRelays = [];
735
842
  this.recentEmittedAnswers = [];
@@ -843,14 +950,16 @@ class ArmedSeat {
843
950
  installStdinProxy() {
844
951
  const handleData = (chunk) => {
845
952
  const chunkText = chunk.toString("utf8");
846
- if (isMeaningfulTerminalInput(chunkText)) {
953
+ const proxyInput = consumeTerminalProxyInput(chunkText, this.pendingPassiveInput);
954
+ this.pendingPassiveInput = proxyInput.pendingPassiveInput;
955
+ if (proxyInput.meaningful) {
847
956
  this.lastUserInputAtMs = Date.now();
848
957
  this.pendingInboundContext = null;
849
958
  }
850
- if (!this.child) {
959
+ if (!this.child || proxyInput.forwardText.length === 0) {
851
960
  return;
852
961
  }
853
- this.child.write(chunkText);
962
+ this.child.write(proxyInput.forwardText);
854
963
  };
855
964
 
856
965
  const handleEnd = () => {
@@ -1537,6 +1646,8 @@ class ArmedSeat {
1537
1646
  exitedAt: new Date().toISOString(),
1538
1647
  state: "exited",
1539
1648
  });
1649
+
1650
+ purgeSeatTransientState(this.sessionPaths.dir, this.paths.dir, this.cwd, this.seatId);
1540
1651
  }
1541
1652
  }
1542
1653
 
@@ -1661,6 +1772,10 @@ function stopAllSessions() {
1661
1772
  }
1662
1773
  }
1663
1774
 
1775
+ for (const session of report.sessions) {
1776
+ finalizeStoppedSession(session);
1777
+ }
1778
+
1664
1779
  return {
1665
1780
  requestedAt,
1666
1781
  sessions: report.sessions,
@@ -1670,6 +1785,7 @@ function stopAllSessions() {
1670
1785
  module.exports = {
1671
1786
  ArmedSeat,
1672
1787
  buildChildEnv,
1788
+ consumeTerminalProxyInput,
1673
1789
  ensureSeatGeminiCliHome,
1674
1790
  chunkRelayPayloadForTyping,
1675
1791
  getStatusReport,
@@ -1694,6 +1810,77 @@ function signalPid(pid, signal) {
1694
1810
  }
1695
1811
  }
1696
1812
 
1813
+ function finalizeStoppedSession(session, timeoutMs = STOP_PURGE_WAIT_MS) {
1814
+ if (!session || !Array.isArray(session.seats) || session.seats.length === 0) {
1815
+ return;
1816
+ }
1817
+
1818
+ const deadline = Date.now() + timeoutMs;
1819
+ while (Date.now() <= deadline) {
1820
+ const pendingSeats = session.seats.filter((seat) => isPidAlive(seat.wrapperPid) || isPidAlive(seat.childPid));
1821
+ if (pendingSeats.length === 0) {
1822
+ break;
1823
+ }
1824
+ sleepSync(STOP_PURGE_POLL_MS);
1825
+ }
1826
+
1827
+ for (const seat of session.seats) {
1828
+ if (isPidAlive(seat.childPid)) {
1829
+ signalProcessFamily(seat.childPid, "SIGKILL");
1830
+ }
1831
+ if (isPidAlive(seat.wrapperPid)) {
1832
+ signalPid(seat.wrapperPid, "SIGKILL");
1833
+ }
1834
+ }
1835
+
1836
+ sleepSync(STOP_PURGE_POLL_MS);
1837
+
1838
+ const sessionDir = getSessionPaths(session.sessionName).dir;
1839
+ for (const seat of session.seats) {
1840
+ purgeSeatTransientState(sessionDir, getSeatPaths(session.sessionName, seat.seatId).dir, seat.cwd, seat.seatId);
1841
+ }
1842
+ }
1843
+
1844
+ function purgeSeatTransientState(sessionDir, seatDir, cwd, seatId) {
1845
+ let geminiSessionDir = null;
1846
+ try {
1847
+ fs.rmSync(seatDir, { recursive: true, force: true });
1848
+ } catch {
1849
+ // Best effort only.
1850
+ }
1851
+
1852
+ try {
1853
+ const homeDir = String(process.env.HOME || "").trim() || "/root";
1854
+ const geminiSeatHome = getSeatGeminiCliHome(homeDir, cwd, seatId);
1855
+ geminiSessionDir = path.dirname(geminiSeatHome);
1856
+ fs.rmSync(geminiSeatHome, { recursive: true, force: true });
1857
+ } catch {
1858
+ // Best effort only.
1859
+ }
1860
+
1861
+ if (geminiSessionDir) {
1862
+ try {
1863
+ const remainingGeminiSeatDirs = fs.readdirSync(geminiSessionDir, { withFileTypes: true })
1864
+ .filter((entry) => entry.isDirectory() && /^seat-\d+$/.test(entry.name));
1865
+ if (remainingGeminiSeatDirs.length === 0) {
1866
+ fs.rmSync(geminiSessionDir, { recursive: true, force: true });
1867
+ }
1868
+ } catch {
1869
+ // Best effort only.
1870
+ }
1871
+ }
1872
+
1873
+ try {
1874
+ const remainingSeatDirs = fs.readdirSync(sessionDir, { withFileTypes: true })
1875
+ .filter((entry) => entry.isDirectory() && /^seat-\d+$/.test(entry.name));
1876
+ if (remainingSeatDirs.length === 0) {
1877
+ fs.rmSync(sessionDir, { recursive: true, force: true });
1878
+ }
1879
+ } catch {
1880
+ // Best effort only.
1881
+ }
1882
+ }
1883
+
1697
1884
  function signalProcessTree(rootPid, signal) {
1698
1885
  const descendants = getChildProcesses(rootPid);
1699
1886
  let delivered = 0;