muuuuse 1.4.2 → 2.1.0

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
@@ -5,6 +5,7 @@
5
5
  It does one job:
6
6
  - arm terminal one with `muuuuse 1`
7
7
  - arm terminal two with `muuuuse 2`
8
+ - have seat 1 generate a session key and seat 2 sign it
8
9
  - watch Codex, Claude, or Gemini for real final answers
9
10
  - inject that final answer into the other armed terminal
10
11
  - keep looping until you stop it
@@ -32,7 +33,7 @@ Terminal 2:
32
33
  muuuuse 2
33
34
  ```
34
35
 
35
- Now both shells are armed. Use them normally.
36
+ Now both shells are armed. `muuuuse 1` generates the session key, `muuuuse 2` signs it, and only that signed pair relays. Use those shells normally.
36
37
 
37
38
  If you want Codex in one and Gemini in the other, start them inside the armed shells:
38
39
 
@@ -62,6 +63,7 @@ muuuuse stop
62
63
 
63
64
  - no tmux
64
65
  - state lives under `~/.muuuuse`
66
+ - only the signed armed pair can exchange relay events
65
67
  - supported final-answer detection is built for Codex, Claude, and Gemini
66
68
  - `codeman` remains the larger transport/control layer; `muuuuse` stays local and minimal
67
69
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "muuuuse",
3
- "version": "1.4.2",
3
+ "version": "2.1.0",
4
4
  "description": "🔌Muuuuse arms two regular terminals and relays final answers between them.",
5
5
  "type": "commonjs",
6
6
  "bin": {
package/src/agents.js CHANGED
@@ -12,9 +12,11 @@ const {
12
12
  } = require("./util");
13
13
 
14
14
  const CODEX_ROOT = path.join(os.homedir(), ".codex", "sessions");
15
+ const CODEX_SNAPSHOT_ROOT = path.join(os.homedir(), ".codex", "shell_snapshots");
15
16
  const CLAUDE_ROOT = path.join(os.homedir(), ".claude", "projects");
16
17
  const GEMINI_ROOT = path.join(os.homedir(), ".gemini", "tmp");
17
18
  const SESSION_START_EARLY_TOLERANCE_MS = 2 * 1000;
19
+ const STRICT_SINGLE_CANDIDATE_EARLY_TOLERANCE_MS = 250;
18
20
 
19
21
  function walkFiles(rootPath, predicate, results = []) {
20
22
  try {
@@ -51,7 +53,11 @@ function buildDetectedAgent(type, process) {
51
53
  }
52
54
 
53
55
  function detectAgent(processes) {
54
- const ordered = [...processes].sort((left, right) => left.elapsedSeconds - right.elapsedSeconds);
56
+ const ordered = [...processes].sort((left, right) => (
57
+ (left.depth ?? Number.MAX_SAFE_INTEGER) - (right.depth ?? Number.MAX_SAFE_INTEGER) ||
58
+ right.elapsedSeconds - left.elapsedSeconds ||
59
+ left.pid - right.pid
60
+ ));
55
61
  for (const process of ordered) {
56
62
  if (commandMatches(process.args, "codex")) {
57
63
  return buildDetectedAgent("codex", process);
@@ -93,6 +99,33 @@ function readFirstLines(filePath, maxLines = 20) {
93
99
  }
94
100
  }
95
101
 
102
+ function sortSessionCandidates(candidates) {
103
+ return candidates
104
+ .slice()
105
+ .sort((left, right) => {
106
+ const leftDiff = Number.isFinite(left.diffMs) ? left.diffMs : Number.MAX_SAFE_INTEGER;
107
+ const rightDiff = Number.isFinite(right.diffMs) ? right.diffMs : Number.MAX_SAFE_INTEGER;
108
+ return (
109
+ leftDiff - rightDiff ||
110
+ right.startedAtMs - left.startedAtMs ||
111
+ right.mtimeMs - left.mtimeMs ||
112
+ left.path.localeCompare(right.path)
113
+ );
114
+ });
115
+ }
116
+
117
+ function annotateSessionCandidates(candidates, processStartedAtMs) {
118
+ return candidates.map((candidate) => ({
119
+ ...candidate,
120
+ diffMs: Number.isFinite(processStartedAtMs) && Number.isFinite(candidate.startedAtMs)
121
+ ? Math.abs(candidate.startedAtMs - processStartedAtMs)
122
+ : Number.POSITIVE_INFINITY,
123
+ relativeStartMs: Number.isFinite(processStartedAtMs) && Number.isFinite(candidate.startedAtMs)
124
+ ? candidate.startedAtMs - processStartedAtMs
125
+ : Number.NaN,
126
+ }));
127
+ }
128
+
96
129
  function selectSessionCandidatePath(candidates, currentPath, processStartedAtMs) {
97
130
  const cwdMatches = candidates.filter((candidate) => candidate.cwd === currentPath);
98
131
  if (cwdMatches.length === 0) {
@@ -107,12 +140,7 @@ function selectSessionCandidatePath(candidates, currentPath, processStartedAtMs)
107
140
  return null;
108
141
  }
109
142
 
110
- const preciseMatches = cwdMatches
111
- .map((candidate) => ({
112
- ...candidate,
113
- diffMs: Math.abs(candidate.startedAtMs - processStartedAtMs),
114
- relativeStartMs: candidate.startedAtMs - processStartedAtMs,
115
- }))
143
+ const preciseMatches = annotateSessionCandidates(cwdMatches, processStartedAtMs)
116
144
  .filter((candidate) => (
117
145
  Number.isFinite(candidate.diffMs) &&
118
146
  Number.isFinite(candidate.relativeStartMs) &&
@@ -128,6 +156,126 @@ function selectSessionCandidatePath(candidates, currentPath, processStartedAtMs)
128
156
  return null;
129
157
  }
130
158
 
159
+ function readCodexSeatClaim(sessionId) {
160
+ if (!sessionId) {
161
+ return null;
162
+ }
163
+
164
+ const snapshotPath = path.join(CODEX_SNAPSHOT_ROOT, `${sessionId}.sh`);
165
+ try {
166
+ const text = fs.readFileSync(snapshotPath, "utf8");
167
+ const seatMatch = text.match(/declare -x MUUUUSE_SEAT="([^"]+)"/);
168
+ const sessionMatch = text.match(/declare -x MUUUUSE_SESSION="([^"]+)"/);
169
+ if (!seatMatch || !sessionMatch) {
170
+ return null;
171
+ }
172
+
173
+ return {
174
+ seatId: seatMatch[1],
175
+ sessionName: sessionMatch[1],
176
+ };
177
+ } catch {
178
+ return null;
179
+ }
180
+ }
181
+
182
+ function selectClaimedCodexCandidatePath(candidates, options = {}) {
183
+ const seatId = options.seatId == null ? null : String(options.seatId);
184
+ const sessionName = typeof options.sessionName === "string" ? options.sessionName : null;
185
+ if (!seatId || !sessionName || candidates.length <= 1) {
186
+ return null;
187
+ }
188
+
189
+ const annotated = candidates.map((candidate) => ({
190
+ ...candidate,
191
+ claim: readCodexSeatClaim(candidate.sessionId),
192
+ }));
193
+
194
+ const exactMatches = annotated.filter((candidate) => (
195
+ candidate.claim?.seatId === seatId &&
196
+ candidate.claim?.sessionName === sessionName
197
+ ));
198
+ if (exactMatches.length === 1) {
199
+ return exactMatches[0].path;
200
+ }
201
+
202
+ const otherSeatClaims = annotated.filter((candidate) => (
203
+ candidate.claim?.sessionName === sessionName &&
204
+ candidate.claim?.seatId !== seatId
205
+ ));
206
+ if (otherSeatClaims.length === 0) {
207
+ return null;
208
+ }
209
+
210
+ const foreignPaths = new Set(otherSeatClaims.map((candidate) => candidate.path));
211
+ const remaining = annotated.filter((candidate) => !foreignPaths.has(candidate.path));
212
+ if (remaining.length === 1) {
213
+ return remaining[0].path;
214
+ }
215
+
216
+ return null;
217
+ }
218
+
219
+ function listOpenFilePathsForPids(pids, rootPath) {
220
+ const normalizedPids = [...new Set(
221
+ (Array.isArray(pids) ? pids : [pids])
222
+ .map((pid) => Number.parseInt(pid, 10))
223
+ .filter((pid) => Number.isInteger(pid) && pid > 0)
224
+ )];
225
+ if (normalizedPids.length === 0) {
226
+ return [];
227
+ }
228
+
229
+ const rootPrefix = path.resolve(rootPath);
230
+ const openPaths = new Set();
231
+
232
+ for (const pid of normalizedPids) {
233
+ const fdRoot = `/proc/${pid}/fd`;
234
+ try {
235
+ for (const entry of fs.readdirSync(fdRoot)) {
236
+ try {
237
+ const resolved = fs.realpathSync(path.join(fdRoot, entry));
238
+ if (typeof resolved === "string" && resolved.startsWith(rootPrefix)) {
239
+ openPaths.add(resolved);
240
+ }
241
+ } catch {
242
+ // Ignore descriptors that disappear while we are inspecting them.
243
+ }
244
+ }
245
+ } catch {
246
+ // Ignore pids that have already exited.
247
+ }
248
+ }
249
+
250
+ return [...openPaths];
251
+ }
252
+
253
+ function selectLiveSessionCandidatePath(candidates, currentPath, captureSinceMs = null) {
254
+ const cwdMatches = candidates.filter((candidate) => candidate.cwd === currentPath);
255
+ if (cwdMatches.length === 0) {
256
+ return null;
257
+ }
258
+
259
+ const primary = cwdMatches.some((candidate) => candidate.isSubagent === false)
260
+ ? cwdMatches.filter((candidate) => candidate.isSubagent === false)
261
+ : cwdMatches;
262
+
263
+ const recent = Number.isFinite(captureSinceMs)
264
+ ? primary.filter((candidate) => Number.isFinite(candidate.mtimeMs) && candidate.mtimeMs >= captureSinceMs - SESSION_START_EARLY_TOLERANCE_MS)
265
+ : primary;
266
+ const ranked = (recent.length > 0 ? recent : primary)
267
+ .slice()
268
+ .sort((left, right) => right.mtimeMs - left.mtimeMs || right.startedAtMs - left.startedAtMs);
269
+
270
+ return ranked[0]?.path || null;
271
+ }
272
+
273
+ function readOpenSessionCandidates(pids, rootPath, reader) {
274
+ return listOpenFilePathsForPids(pids, rootPath)
275
+ .map((filePath) => reader(filePath))
276
+ .filter((candidate) => candidate !== null);
277
+ }
278
+
131
279
  function readCodexCandidate(filePath) {
132
280
  try {
133
281
  const [firstLine] = readFirstLines(filePath, 1);
@@ -143,6 +291,8 @@ function readCodexCandidate(filePath) {
143
291
  return {
144
292
  path: filePath,
145
293
  cwd: entry.payload.cwd,
294
+ isSubagent: Boolean(entry.payload?.source?.subagent),
295
+ sessionId: entry.payload.id || null,
146
296
  startedAtMs: Date.parse(entry.payload.timestamp),
147
297
  mtimeMs: fs.statSync(filePath).mtimeMs,
148
298
  };
@@ -151,12 +301,143 @@ function readCodexCandidate(filePath) {
151
301
  }
152
302
  }
153
303
 
154
- function selectCodexSessionFile(currentPath, processStartedAtMs) {
304
+ function rankCodexCandidates(candidates, processStartedAtMs) {
305
+ return sortSessionCandidates(annotateSessionCandidates(candidates, processStartedAtMs));
306
+ }
307
+
308
+ function selectExactClaimedCodexCandidate(candidates, options = {}, processStartedAtMs = null) {
309
+ const seatId = options.seatId == null ? null : String(options.seatId);
310
+ const sessionName = typeof options.sessionName === "string" ? options.sessionName : null;
311
+ if (!seatId || !sessionName) {
312
+ return null;
313
+ }
314
+
315
+ const exactMatches = rankCodexCandidates(
316
+ candidates.filter((candidate) => {
317
+ const claim = readCodexSeatClaim(candidate.sessionId);
318
+ return claim?.seatId === seatId && claim?.sessionName === sessionName;
319
+ }),
320
+ processStartedAtMs
321
+ );
322
+
323
+ return exactMatches[0]?.path || null;
324
+ }
325
+
326
+ function filterForeignClaimedCodexCandidates(candidates, options = {}) {
327
+ const seatId = options.seatId == null ? null : String(options.seatId);
328
+ const sessionName = typeof options.sessionName === "string" ? options.sessionName : null;
329
+ if (!seatId || !sessionName) {
330
+ return candidates.slice();
331
+ }
332
+
333
+ return candidates.filter((candidate) => {
334
+ const claim = readCodexSeatClaim(candidate.sessionId);
335
+ return !(claim?.sessionName === sessionName && claim?.seatId && claim.seatId !== seatId);
336
+ });
337
+ }
338
+
339
+ function selectStrictSingleCodexCandidatePath(candidates, processStartedAtMs) {
340
+ if (candidates.length !== 1 || !Number.isFinite(processStartedAtMs)) {
341
+ return null;
342
+ }
343
+
344
+ const [candidate] = annotateSessionCandidates(candidates, processStartedAtMs);
345
+ if (!Number.isFinite(candidate.relativeStartMs)) {
346
+ return null;
347
+ }
348
+
349
+ if (
350
+ candidate.relativeStartMs < -STRICT_SINGLE_CANDIDATE_EARLY_TOLERANCE_MS ||
351
+ candidate.relativeStartMs > SESSION_MATCH_WINDOW_MS
352
+ ) {
353
+ return null;
354
+ }
355
+
356
+ return candidate.path;
357
+ }
358
+
359
+ function selectCodexCandidatePath(candidates, currentPath, processStartedAtMs, options = {}) {
360
+ const cwdMatches = candidates.filter((candidate) => candidate.cwd === currentPath);
361
+ if (cwdMatches.length === 0) {
362
+ return null;
363
+ }
364
+
365
+ const seatId = options.seatId == null ? null : String(options.seatId);
366
+ const sessionName = typeof options.sessionName === "string" ? options.sessionName : null;
367
+ const exactClaimPath = selectExactClaimedCodexCandidate(cwdMatches, options, processStartedAtMs);
368
+ if (exactClaimPath) {
369
+ return exactClaimPath;
370
+ }
371
+
372
+ const foreignClaimsPresent = Boolean(
373
+ seatId &&
374
+ sessionName &&
375
+ cwdMatches.some((candidate) => {
376
+ const claim = readCodexSeatClaim(candidate.sessionId);
377
+ return claim?.sessionName === sessionName && claim?.seatId && claim.seatId !== seatId;
378
+ })
379
+ );
380
+ const allowedMatches = filterForeignClaimedCodexCandidates(cwdMatches, options);
381
+ if (allowedMatches.length === 0) {
382
+ return null;
383
+ }
384
+
385
+ if (!Number.isFinite(processStartedAtMs)) {
386
+ return allowedMatches.length === 1 ? allowedMatches[0].path : null;
387
+ }
388
+
389
+ const preciseMatches = rankCodexCandidates(
390
+ allowedMatches.filter((candidate) => {
391
+ const annotated = annotateSessionCandidates([candidate], processStartedAtMs)[0];
392
+ return (
393
+ Number.isFinite(annotated.diffMs) &&
394
+ Number.isFinite(annotated.relativeStartMs) &&
395
+ annotated.relativeStartMs >= -SESSION_START_EARLY_TOLERANCE_MS &&
396
+ annotated.relativeStartMs <= SESSION_MATCH_WINDOW_MS
397
+ );
398
+ }),
399
+ processStartedAtMs
400
+ );
401
+
402
+ const preciseClaimPath = selectClaimedCodexCandidatePath(preciseMatches, options);
403
+ if (preciseClaimPath) {
404
+ return preciseClaimPath;
405
+ }
406
+
407
+ const pairedSeatSelection = seatId && sessionName;
408
+ if (pairedSeatSelection && options.allowUnclaimedSingleCandidate === false && !foreignClaimsPresent) {
409
+ return null;
410
+ }
411
+
412
+ if (preciseMatches.length === 1) {
413
+ return selectStrictSingleCodexCandidatePath(preciseMatches, processStartedAtMs);
414
+ }
415
+
416
+ if (allowedMatches.length === 1) {
417
+ return selectStrictSingleCodexCandidatePath(allowedMatches, processStartedAtMs);
418
+ }
419
+
420
+ return null;
421
+ }
422
+
423
+ function selectCodexSessionFile(currentPath, processStartedAtMs, options = {}) {
424
+ const liveCandidates = readOpenSessionCandidates(options.pids ?? options.pid, CODEX_ROOT, readCodexCandidate);
425
+ const livePath = selectCodexCandidatePath(liveCandidates, currentPath, processStartedAtMs, {
426
+ ...options,
427
+ allowUnclaimedSingleCandidate: true,
428
+ });
429
+ if (livePath) {
430
+ return livePath;
431
+ }
432
+
155
433
  const candidates = walkFiles(CODEX_ROOT, (filePath) => filePath.endsWith(".jsonl"))
156
434
  .map((filePath) => readCodexCandidate(filePath))
157
435
  .filter((candidate) => candidate !== null);
158
436
 
159
- return selectSessionCandidatePath(candidates, currentPath, processStartedAtMs);
437
+ return selectCodexCandidatePath(candidates, currentPath, processStartedAtMs, {
438
+ ...options,
439
+ allowUnclaimedSingleCandidate: false,
440
+ });
160
441
  }
161
442
 
162
443
  function extractCodexAssistantText(content) {
@@ -252,7 +533,13 @@ function readClaudeCandidate(filePath) {
252
533
  }
253
534
  }
254
535
 
255
- function selectClaudeSessionFile(currentPath, processStartedAtMs) {
536
+ function selectClaudeSessionFile(currentPath, processStartedAtMs, options = {}) {
537
+ const liveCandidates = readOpenSessionCandidates(options.pids ?? options.pid, CLAUDE_ROOT, readClaudeCandidate);
538
+ const livePath = selectLiveSessionCandidatePath(liveCandidates, currentPath, options.captureSinceMs);
539
+ if (livePath) {
540
+ return livePath;
541
+ }
542
+
256
543
  const candidates = walkFiles(CLAUDE_ROOT, (filePath) => filePath.endsWith(".jsonl"))
257
544
  .map((filePath) => readClaudeCandidate(filePath))
258
545
  .filter((candidate) => candidate !== null);
@@ -331,8 +618,15 @@ function readGeminiCandidate(filePath) {
331
618
  }
332
619
  }
333
620
 
334
- function selectGeminiSessionFile(currentPath, processStartedAtMs) {
621
+ function selectGeminiSessionFile(currentPath, processStartedAtMs, options = {}) {
335
622
  const projectHash = createHash("sha256").update(currentPath).digest("hex");
623
+ const liveCandidates = readOpenSessionCandidates(options.pids ?? options.pid, GEMINI_ROOT, readGeminiCandidate)
624
+ .filter((candidate) => candidate.projectHash === projectHash);
625
+ const livePath = selectLiveSessionCandidatePath(liveCandidates, projectHash, options.captureSinceMs);
626
+ if (livePath) {
627
+ return livePath;
628
+ }
629
+
336
630
  const candidates = walkFiles(GEMINI_ROOT, (filePath) => filePath.endsWith(".json"))
337
631
  .map((filePath) => readGeminiCandidate(filePath))
338
632
  .filter((candidate) => candidate !== null && candidate.projectHash === projectHash);
@@ -384,6 +678,7 @@ module.exports = {
384
678
  readClaudeAnswers,
385
679
  readCodexAnswers,
386
680
  readGeminiAnswers,
681
+ selectLiveSessionCandidatePath,
387
682
  selectSessionCandidatePath,
388
683
  selectClaudeSessionFile,
389
684
  selectCodexSessionFile,
package/src/cli.js CHANGED
@@ -82,6 +82,9 @@ function renderSeatStatus(seat) {
82
82
  if (seat.partnerLive) {
83
83
  bits.push("peer live");
84
84
  }
85
+ if (seat.trust) {
86
+ bits.push(`trust ${seat.trust}`);
87
+ }
85
88
  if (seat.lastAnswerAt) {
86
89
  bits.push(`last answer ${seat.lastAnswerAt}`);
87
90
  }