muuuuse 3.3.1 → 3.3.3

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/README.md CHANGED
@@ -45,7 +45,7 @@ Terminal 2:
45
45
  muuuuse 2 link 1 flow off link 3 flow on link 4 flow on
46
46
  ```
47
47
 
48
- Now both shells are armed in the same cwd and join the same relay graph. Every seat has its own Ed25519 keypair. Each forwarded relay is signed by the sending seat. A target seat only accepts inbound relays from seats it links back to, so the graph can be open-ended without becoming an all-to-all broadcast.
48
+ Now both shells are armed in the same cwd and join the same relay graph. Every seat has its own Ed25519 keypair. Each forwarded relay is signed by the sending seat. A target seat only accepts inbound relays when the sender linked to that target, so the graph can be open-ended without becoming an all-to-all broadcast.
49
49
 
50
50
  `link <seat> flow on` means that outbound edge sends commentary and final answers. `link <seat> flow off` means that outbound edge sends final answers only. This is sender-side routing, not receiver-side filtering.
51
51
 
@@ -79,7 +79,7 @@ muuuuse stop
79
79
 
80
80
  - state lives under `~/.muuuuse`
81
81
  - all armed seats in the same cwd share one relay session graph
82
- - only signed relays from reciprocally linked seats are accepted
82
+ - only signed relays from senders that linked the target seat are accepted
83
83
  - `continue <seat>` is a convenience alias for a single signed outbound link
84
84
  - supported relay detection is built for Codex, Claude, and Gemini
85
85
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "muuuuse",
3
- "version": "3.3.1",
3
+ "version": "3.3.3",
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
@@ -5,6 +5,7 @@ const path = require("node:path");
5
5
 
6
6
  const {
7
7
  getFileSize,
8
+ getSeatGeminiCliHome,
8
9
  hashText,
9
10
  readAppendedText,
10
11
  sanitizeRelayText,
@@ -18,6 +19,23 @@ const GEMINI_ROOT = path.join(os.homedir(), ".gemini", "tmp");
18
19
  const SESSION_START_EARLY_TOLERANCE_MS = 2 * 1000;
19
20
  const STRICT_SINGLE_CANDIDATE_EARLY_TOLERANCE_MS = 250;
20
21
 
22
+ function uniquePaths(paths) {
23
+ return [...new Set(paths.map((entry) => path.resolve(entry)))];
24
+ }
25
+
26
+ function getGeminiRoots(currentPath, options = {}) {
27
+ const roots = [GEMINI_ROOT];
28
+ const seatId = Number.parseInt(String(options.seatId || "").trim(), 10);
29
+ if (Number.isInteger(seatId) && seatId > 0) {
30
+ roots.unshift(path.join(
31
+ getSeatGeminiCliHome(os.homedir(), currentPath, seatId),
32
+ ".gemini",
33
+ "tmp"
34
+ ));
35
+ }
36
+ return uniquePaths(roots);
37
+ }
38
+
21
39
  function walkFiles(rootPath, predicate, results = []) {
22
40
  try {
23
41
  const entries = fs.readdirSync(rootPath, { withFileTypes: true });
@@ -642,14 +660,17 @@ function readGeminiCandidate(filePath) {
642
660
 
643
661
  function selectGeminiSessionFile(currentPath, processStartedAtMs, options = {}) {
644
662
  const projectHash = createHash("sha256").update(currentPath).digest("hex");
645
- const liveCandidates = readOpenSessionCandidates(options.pids ?? options.pid, GEMINI_ROOT, readGeminiCandidate)
663
+ const geminiRoots = getGeminiRoots(currentPath, options);
664
+ const liveCandidates = geminiRoots
665
+ .flatMap((rootPath) => readOpenSessionCandidates(options.pids ?? options.pid, rootPath, readGeminiCandidate))
646
666
  .filter((candidate) => candidate.projectHash === projectHash);
647
667
  const livePath = selectLiveSessionCandidatePath(liveCandidates, projectHash, options.captureSinceMs);
648
668
  if (livePath) {
649
669
  return livePath;
650
670
  }
651
671
 
652
- const candidates = walkFiles(GEMINI_ROOT, (filePath) => filePath.endsWith(".json"))
672
+ const candidates = geminiRoots
673
+ .flatMap((rootPath) => walkFiles(rootPath, (filePath) => filePath.endsWith(".json")))
653
674
  .map((filePath) => readGeminiCandidate(filePath))
654
675
  .filter((candidate) => candidate !== null && candidate.projectHash === projectHash);
655
676
 
package/src/runtime.js CHANGED
@@ -20,6 +20,7 @@ const {
20
20
  ensureDir,
21
21
  getDefaultSessionName,
22
22
  getFileSize,
23
+ getSeatGeminiCliHome,
23
24
  getSeatPaths,
24
25
  getSessionPaths,
25
26
  getStateRoot,
@@ -41,6 +42,14 @@ const {
41
42
  // A short settle delay keeps interactive CLIs from treating submit as another newline.
42
43
  const TYPE_CHUNK_DELAY_MS = 45;
43
44
  const TYPE_CHUNK_SIZE = 24;
45
+ const BRACKETED_PASTE_START = "\u001b[200~";
46
+ const BRACKETED_PASTE_END = "\u001b[201~";
47
+ const GEMINI_SHARED_ENTRY_NAMES = new Set([
48
+ "gemini-credentials.json",
49
+ "google_accounts.json",
50
+ "installation_id",
51
+ "mcp-oauth-tokens-v2.json",
52
+ ]);
44
53
  const MIRROR_SUPPRESSION_WINDOW_MS = 30 * 1000;
45
54
  const PENDING_RELAY_CONTEXT_TTL_MS = 2 * 60 * 1000;
46
55
  const EMITTED_ANSWER_TTL_MS = 5 * 60 * 1000;
@@ -144,6 +153,78 @@ function sanitizeChildPath(pathValue, homeDir) {
144
153
  return entries.join(path.delimiter);
145
154
  }
146
155
 
156
+ function readGeminiApiKeyFromHome(homeDir) {
157
+ const filePath = path.join(homeDir, "gemini.txt");
158
+ try {
159
+ const value = fs.readFileSync(filePath, "utf8").trim();
160
+ return value || null;
161
+ } catch {
162
+ return null;
163
+ }
164
+ }
165
+
166
+ function syncGeminiCliHomeEntry(sourcePath, targetPath) {
167
+ const shouldLink = GEMINI_SHARED_ENTRY_NAMES.has(path.basename(sourcePath));
168
+ try {
169
+ if (fs.lstatSync(targetPath).isSymbolicLink() && fs.realpathSync(targetPath) === sourcePath) {
170
+ return;
171
+ }
172
+ } catch {
173
+ // Recreate the target entry below when missing or mismatched.
174
+ }
175
+
176
+ fs.rmSync(targetPath, { recursive: true, force: true });
177
+
178
+ const sourceStats = fs.lstatSync(sourcePath);
179
+ if (shouldLink) {
180
+ try {
181
+ const linkType = process.platform === "win32"
182
+ ? (sourceStats.isDirectory() ? "junction" : "file")
183
+ : undefined;
184
+ fs.symlinkSync(sourcePath, targetPath, linkType);
185
+ return;
186
+ } catch {
187
+ // Fall through to copying when symlinks are unavailable.
188
+ }
189
+ }
190
+
191
+ if (sourceStats.isDirectory()) {
192
+ fs.cpSync(sourcePath, targetPath, { recursive: true });
193
+ return;
194
+ }
195
+
196
+ fs.copyFileSync(sourcePath, targetPath);
197
+ }
198
+
199
+ function ensureSeatGeminiCliHome(homeDir, cwd, seatId, baseEnv = process.env) {
200
+ const sourceHomeRoot = String(baseEnv.GEMINI_CLI_HOME || homeDir).trim() || homeDir;
201
+ const sourceGeminiDir = path.join(sourceHomeRoot, ".gemini");
202
+ const targetHomeRoot = getSeatGeminiCliHome(homeDir, cwd, seatId);
203
+ const targetGeminiDir = ensureDir(path.join(targetHomeRoot, ".gemini"));
204
+
205
+ let sourceEntries = [];
206
+ try {
207
+ sourceEntries = fs.readdirSync(sourceGeminiDir, { withFileTypes: true });
208
+ } catch {
209
+ ensureDir(path.join(targetGeminiDir, "tmp"));
210
+ return targetHomeRoot;
211
+ }
212
+
213
+ for (const entry of sourceEntries) {
214
+ if (entry.name === "tmp") {
215
+ continue;
216
+ }
217
+
218
+ syncGeminiCliHomeEntry(
219
+ path.join(sourceGeminiDir, entry.name),
220
+ path.join(targetGeminiDir, entry.name)
221
+ );
222
+ }
223
+
224
+ ensureDir(path.join(targetGeminiDir, "tmp"));
225
+ return targetHomeRoot;
226
+ }
227
+
147
228
  function buildChildEnv(seatId, sessionName, cwd, baseEnv = process.env) {
148
229
  const env = { ...baseEnv };
149
230
  for (const key of CHILD_ENV_DROP_KEYS) {
@@ -156,6 +237,13 @@ function buildChildEnv(seatId, sessionName, cwd, baseEnv = process.env) {
156
237
  env.TERM = resolveChildTerm(baseEnv);
157
238
  env.MUUUUSE_SEAT = String(seatId);
158
239
  env.MUUUUSE_SESSION = sessionName;
240
+ if (!String(env.GEMINI_API_KEY || "").trim()) {
241
+ const homeGeminiApiKey = readGeminiApiKeyFromHome(homeDir);
242
+ if (homeGeminiApiKey) {
243
+ env.GEMINI_API_KEY = homeGeminiApiKey;
244
+ }
245
+ }
246
+ env.GEMINI_CLI_HOME = getSeatGeminiCliHome(homeDir, cwd, seatId);
159
247
  return env;
160
248
  }
161
249
 
@@ -566,24 +654,39 @@ function isMeaningfulTerminalInput(input) {
566
654
  }
567
655
 
568
656
  async function sendTextAndEnter(child, text, shouldAbort = () => false) {
657
+ const options = typeof shouldAbort === "function" ? { shouldAbort } : (shouldAbort || {});
658
+ const shouldStop = typeof options.shouldAbort === "function" ? options.shouldAbort : () => false;
659
+ const agentType = String(options.agentType || "").trim().toLowerCase() || null;
569
660
  const payload = normalizeRelayPayloadForTyping(text);
570
661
 
571
662
  if (payload.length > 0) {
572
- for (const chunk of chunkRelayPayloadForTyping(payload)) {
573
- if (shouldAbort() || !child) {
663
+ if (agentType === "codex") {
664
+ if (shouldStop() || !child) {
574
665
  return false;
575
666
  }
576
667
 
577
668
  try {
578
- child.write(chunk);
669
+ child.write(`${BRACKETED_PASTE_START}${payload}${BRACKETED_PASTE_END}`);
579
670
  } catch {
580
671
  return false;
581
672
  }
582
- await sleep(TYPE_CHUNK_DELAY_MS);
673
+ } else {
674
+ for (const chunk of chunkRelayPayloadForTyping(payload)) {
675
+ if (shouldStop() || !child) {
676
+ return false;
677
+ }
678
+
679
+ try {
680
+ child.write(chunk);
681
+ } catch {
682
+ return false;
683
+ }
684
+ await sleep(TYPE_CHUNK_DELAY_MS);
685
+ }
583
686
  }
584
687
  }
585
688
 
586
- if (shouldAbort() || !child) {
689
+ if (shouldStop() || !child) {
587
690
  return false;
588
691
  }
589
692
 
@@ -702,6 +805,12 @@ class ArmedSeat {
702
805
  const shell = resolveShell();
703
806
  const shellArgs = resolveShellArgs(shell);
704
807
  const childEnv = buildChildEnv(this.seatId, this.sessionName, this.cwd);
808
+ ensureSeatGeminiCliHome(
809
+ String(childEnv.HOME || "").trim() || process.env.HOME || "/root",
810
+ this.cwd,
811
+ this.seatId,
812
+ process.env
813
+ );
705
814
  this.child = pty.spawn(shell, shellArgs, {
706
815
  cols: process.stdout.columns || 120,
707
816
  rows: process.stdout.rows || 36,
@@ -872,13 +981,33 @@ class ArmedSeat {
872
981
  return Number.isFinite(requestedAtMs) && requestedAtMs > this.startedAtMs;
873
982
  }
874
983
 
875
- hasAuthorizedSource(sourceSeatId) {
984
+ sourceLinksToTarget(sourceSeatId, targetSeatId = this.seatId) {
876
985
  const desiredSeatId = normalizeSeatId(sourceSeatId);
877
- if (!desiredSeatId) {
986
+ const desiredTargetSeatId = normalizeSeatId(targetSeatId);
987
+ if (!desiredSeatId || !desiredTargetSeatId) {
878
988
  return false;
879
989
  }
880
990
 
881
- return this.getConfiguredTargets().some((target) => target.seatId === desiredSeatId);
991
+ const sourcePaths = getSeatPaths(this.sessionName, desiredSeatId);
992
+ const sourceStatus = readJson(sourcePaths.statusPath, null);
993
+ const sourceMeta = readJson(sourcePaths.metaPath, null);
994
+ const sourceContinueSeatId = sourceStatus?.continueSeatId || sourceMeta?.continueSeatId || null;
995
+ const sourceContinueTargets = normalizeContinueTargets(
996
+ sourceStatus?.continueTargets || sourceMeta?.continueTargets
997
+ );
998
+
999
+ const configuredTargets = [...sourceContinueTargets];
1000
+ if (
1001
+ sourceContinueSeatId &&
1002
+ !configuredTargets.some((target) => target.seatId === normalizeSeatId(sourceContinueSeatId))
1003
+ ) {
1004
+ configuredTargets.push({
1005
+ seatId: normalizeSeatId(sourceContinueSeatId),
1006
+ flowMode: normalizeFlowMode(sourceStatus?.flowMode || sourceMeta?.flowMode),
1007
+ });
1008
+ }
1009
+
1010
+ return configuredTargets.some((target) => target.seatId === desiredTargetSeatId);
882
1011
  }
883
1012
 
884
1013
  readSourcePublicKey(sourceSeatId) {
@@ -922,7 +1051,7 @@ class ArmedSeat {
922
1051
  const sourceSeatId = normalizeSeatId(entry?.sourceSeatId || entry?.seatId);
923
1052
  const targetSeatId = normalizeSeatId(entry?.targetSeatId);
924
1053
  const payload = sanitizeRelayText(entry?.text);
925
- if (!sourceSeatId || targetSeatId !== this.seatId || !payload || !this.hasAuthorizedSource(sourceSeatId)) {
1054
+ if (!sourceSeatId || targetSeatId !== this.seatId || !payload || !this.sourceLinksToTarget(sourceSeatId, targetSeatId)) {
926
1055
  return false;
927
1056
  }
928
1057
 
@@ -955,6 +1084,10 @@ class ArmedSeat {
955
1084
  return;
956
1085
  }
957
1086
 
1087
+ const detectedRelayAgent = this.liveState.type
1088
+ ? { type: this.liveState.type }
1089
+ : detectAgent(getChildProcesses(this.childPid));
1090
+
958
1091
  const entries = parseContinueEntries(text, this.seatId);
959
1092
  for (const entry of entries) {
960
1093
  if (this.stopped || this.stopRequested()) {
@@ -974,7 +1107,10 @@ class ArmedSeat {
974
1107
  const delivered = await sendTextAndEnter(
975
1108
  this.child,
976
1109
  payload,
977
- () => this.stopped || this.stopRequested() || !this.child || Boolean(this.childExit)
1110
+ {
1111
+ agentType: detectedRelayAgent?.type || null,
1112
+ shouldAbort: () => this.stopped || this.stopRequested() || !this.child || Boolean(this.childExit),
1113
+ }
978
1114
  );
979
1115
  if (!delivered) {
980
1116
  this.requestStop("relay_aborted");
@@ -1348,7 +1484,7 @@ class ArmedSeat {
1348
1484
  `Seat ${this.seatId} links signed relay targets: ${configuredTargets.map((target) => `${target.seatId}:${target.flowMode}`).join(", ")}.`
1349
1485
  );
1350
1486
  }
1351
- this.log("Signed relays are accepted only from seats that this seat links back to.");
1487
+ this.log("Signed relays are accepted when the sender linked to this seat.");
1352
1488
  this.log("Run `muuuuse status` or `muuuuse stop` from any terminal.");
1353
1489
 
1354
1490
  try {
@@ -1531,12 +1667,14 @@ function stopAllSessions() {
1531
1667
  module.exports = {
1532
1668
  ArmedSeat,
1533
1669
  buildChildEnv,
1670
+ ensureSeatGeminiCliHome,
1534
1671
  chunkRelayPayloadForTyping,
1535
1672
  getStatusReport,
1536
1673
  isBareEscapeInput,
1537
1674
  isMeaningfulTerminalInput,
1538
1675
  normalizeRelayPayloadForTyping,
1539
1676
  resolveSessionName,
1677
+ sendTextAndEnter,
1540
1678
  stopAllSessions,
1541
1679
  };
1542
1680
 
package/src/util.js CHANGED
@@ -154,6 +154,16 @@ function getDefaultSessionName(currentPath = process.cwd()) {
154
154
  return `${label}-${hashText(resolvedPath).slice(0, 8)}`;
155
155
  }
156
156
 
157
+ function getSeatGeminiCliHome(homeDir = os.homedir(), currentPath = process.cwd(), seatId) {
158
+ return path.join(
159
+ homeDir,
160
+ ".muuuuse",
161
+ "gemini-cli-homes",
162
+ getDefaultSessionName(currentPath),
163
+ `seat-${seatId}`
164
+ );
165
+ }
166
+
157
167
  function getSessionDir(sessionName) {
158
168
  return ensureDir(path.join(getStateRoot(), "sessions", slugifySegment(sessionName)));
159
169
  }
@@ -296,7 +306,7 @@ function usage() {
296
306
  " 4. `flow on` sends commentary and final answers on that edge. `flow off` sends final answers only.",
297
307
  " 5. `continue <seat>` is shorthand for one outbound link that uses the seat's default `flow on|off`.",
298
308
  " 6. Every forwarded relay is signed with the sender seat's key.",
299
- " 7. A seat only accepts signed inbound relays from seats it links back to.",
309
+ " 7. A seat only accepts signed inbound relays when the sender linked to that seat.",
300
310
  " 8. Use those armed shells normally.",
301
311
  " 9. Run `muuuuse status` or `muuuuse stop` from any shell.",
302
312
  "",
@@ -315,6 +325,7 @@ module.exports = {
315
325
  ensureDir,
316
326
  getDefaultSessionName,
317
327
  getFileSize,
328
+ getSeatGeminiCliHome,
318
329
  loadOrCreateSeatIdentity,
319
330
  getSeatPaths,
320
331
  getSessionPaths,