muuuuse 0.1.0 → 0.2.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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 lmtlssss
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,5 +1,117 @@
1
- # muuuuse
1
+ # 🔌Muuuuse
2
2
 
3
- Reserved npm handle for the Muuuuse local multi-seat lane.
3
+ `muuuuse` installs one CLI name:
4
4
 
5
- This package currently exists to reserve the npm namespace for the corresponding lmtlssss project lane.
5
+ - `muuuuse`
6
+
7
+ The visible product brand is always `🔌Muuuuse`, while the terminal command examples use `muuuuse`.
8
+
9
+ ## What It Does
10
+
11
+ `🔌Muuuuse` is the small local-only relay for three tmux terminals:
12
+
13
+ - seat `1` listens in one terminal
14
+ - seat `2` listens in another terminal
15
+ - seat `3` is the controller that auto-pairs them
16
+
17
+ Once seats `1` and `2` are armed, you can launch Codex, Claude, Gemini, or a deterministic script inside those two terminals. `muuuuse 3` then relays only final answers between them by injecting text plus `Enter` into the opposite seat.
18
+
19
+ Remote control is intentionally out of scope here. Use `codeman` or `codemansbot` for remote routing.
20
+
21
+ ## Install
22
+
23
+ ```bash
24
+ npm install -g muuuuse
25
+ ```
26
+
27
+ ## Basic Flow
28
+
29
+ Terminal 1:
30
+
31
+ ```bash
32
+ muuuuse 1
33
+ codex -m gpt-5.4 -c model_reasoning_effort=low --dangerously-bypass-approvals-and-sandbox --no-alt-screen
34
+ ```
35
+
36
+ Terminal 2:
37
+
38
+ ```bash
39
+ muuuuse 2
40
+ claude --dangerously-skip-permissions --permission-mode bypassPermissions
41
+ ```
42
+
43
+ Terminal 3:
44
+
45
+ ```bash
46
+ muuuuse 3 "Start by proposing the first concrete repo task."
47
+ ```
48
+
49
+ That third command auto-pairs seats `1` and `2`, then optionally drops a one-time kickoff prompt into seat `1`.
50
+
51
+ ## Preset Launches
52
+
53
+ `🔌Muuuuse` does not launch the CLIs for you anymore. It arms the terminal, watches the live process, and reads only final answers from the local transcript files.
54
+
55
+ Recommended god-mode launches:
56
+
57
+ ```bash
58
+ codex -m gpt-5.4 -c model_reasoning_effort=low --dangerously-bypass-approvals-and-sandbox --no-alt-screen
59
+ claude --dangerously-skip-permissions --permission-mode bypassPermissions
60
+ gemini --approval-mode yolo --sandbox=false
61
+ ```
62
+
63
+ ## Script Mode
64
+
65
+ Turn an armed seat into a deterministic responder:
66
+
67
+ ```bash
68
+ muuuuse script
69
+ ```
70
+
71
+ That stores one repeating response.
72
+
73
+ For a loop of multiple steps:
74
+
75
+ ```bash
76
+ muuuuse script 4
77
+ ```
78
+
79
+ That collects four prompts and cycles them forever, one per inbound turn.
80
+
81
+ To leave script mode and go back to a live CLI listener:
82
+
83
+ ```bash
84
+ muuuuse live
85
+ ```
86
+
87
+ ## Requirements
88
+
89
+ - `tmux`
90
+ - `git`
91
+ - `npm`
92
+ - at least one local CLI you want to mirror: `codex`, `claude`, `gemini`, or script mode
93
+
94
+ ## Doctor
95
+
96
+ ```bash
97
+ muuuuse doctor
98
+ ```
99
+
100
+ This checks:
101
+
102
+ - `git`
103
+ - `npm`
104
+ - `tmux`
105
+ - `codex`
106
+ - `claude`
107
+ - `gemini`
108
+ - `/root/npm.txt` or the fallback npm token path
109
+
110
+ ## Notes
111
+
112
+ - local only
113
+ - auto-pair, no auth key ceremony
114
+ - only final answers are forwarded
115
+ - no verbose stream forwarding
116
+ - no reasoning forwarding
117
+ - controller exit stops the relay
package/bin/muuse.js ADDED
@@ -0,0 +1,9 @@
1
+ #!/usr/bin/env node
2
+
3
+ const { main } = require("../src/cli");
4
+
5
+ main().catch((error) => {
6
+ const message = error instanceof Error ? error.message : String(error);
7
+ console.error(`🔌Muuuuse error: ${message}`);
8
+ process.exit(1);
9
+ });
package/package.json CHANGED
@@ -1,16 +1,45 @@
1
1
  {
2
2
  "name": "muuuuse",
3
- "version": "0.1.0",
4
- "description": "Reserved npm handle for the Muuuuse local multi-seat lane.",
5
- "license": "MIT",
3
+ "version": "0.2.0",
4
+ "description": "🔌Muuuuse links two local tmux terminals through a third control seat so Codex, Claude, Gemini, or scripts can bounce final answers forever.",
5
+ "type": "commonjs",
6
+ "bin": {
7
+ "muuuuse": "bin/muuse.js"
8
+ },
9
+ "files": [
10
+ "bin",
11
+ "src",
12
+ "README.md",
13
+ "LICENSE"
14
+ ],
15
+ "engines": {
16
+ "node": ">=18"
17
+ },
18
+ "homepage": "https://muuuuse.lmtlssss.fun",
6
19
  "repository": {
7
20
  "type": "git",
8
- "url": "https://github.com/lmtlssss/muuuuse.git"
21
+ "url": "git+https://github.com/lmtlssss/muuuuse.git"
9
22
  },
10
- "homepage": "https://lmtlssss.fun/",
23
+ "bugs": {
24
+ "url": "https://github.com/lmtlssss/muuuuse/issues"
25
+ },
26
+ "publishConfig": {
27
+ "access": "public"
28
+ },
29
+ "license": "MIT",
11
30
  "keywords": [
12
- "lmtlssss",
13
- "reserved-handle"
31
+ "terminal",
32
+ "ai",
33
+ "tmux",
34
+ "relay",
35
+ "local",
36
+ "codex",
37
+ "claude",
38
+ "gemini"
14
39
  ],
15
- "main": "index.js"
40
+ "scripts": {
41
+ "test": "node test/cli.test.js",
42
+ "pack:local": "npm pack",
43
+ "prepublishOnly": "npm test"
44
+ }
16
45
  }
package/src/agents.js ADDED
@@ -0,0 +1,445 @@
1
+ const { createHash } = require("node:crypto");
2
+ const fs = require("node:fs");
3
+ const os = require("node:os");
4
+ const path = require("node:path");
5
+
6
+ const {
7
+ SESSION_MATCH_WINDOW_MS,
8
+ getFileSize,
9
+ hashText,
10
+ readAppendedText,
11
+ sanitizeRelayText,
12
+ } = require("./util");
13
+
14
+ const PRESETS = {
15
+ codex: {
16
+ label: "Codex",
17
+ command: [
18
+ "codex",
19
+ "-m",
20
+ "gpt-5.4",
21
+ "-c",
22
+ "model_reasoning_effort=low",
23
+ "--dangerously-bypass-approvals-and-sandbox",
24
+ "--no-alt-screen",
25
+ ],
26
+ },
27
+ claude: {
28
+ label: "Claude Code",
29
+ command: [
30
+ "claude",
31
+ "--dangerously-skip-permissions",
32
+ "--permission-mode",
33
+ "bypassPermissions",
34
+ ],
35
+ },
36
+ gemini: {
37
+ label: "Gemini CLI",
38
+ command: [
39
+ "gemini",
40
+ "--approval-mode",
41
+ "yolo",
42
+ "--sandbox=false",
43
+ ],
44
+ },
45
+ };
46
+
47
+ const CODEX_ROOT = path.join(os.homedir(), ".codex", "sessions");
48
+ const CLAUDE_ROOT = path.join(os.homedir(), ".claude", "projects");
49
+ const GEMINI_ROOT = path.join(os.homedir(), ".gemini", "tmp");
50
+ const CODEX_SNAPSHOT_ROOT = path.join(os.homedir(), ".codex", "shell_snapshots");
51
+ const codexSnapshotPaneCache = new Map();
52
+
53
+ function walkFiles(rootPath, predicate, results = []) {
54
+ try {
55
+ const entries = fs.readdirSync(rootPath, { withFileTypes: true });
56
+ for (const entry of entries) {
57
+ const absolutePath = path.join(rootPath, entry.name);
58
+ if (entry.isDirectory()) {
59
+ walkFiles(absolutePath, predicate, results);
60
+ } else if (predicate(absolutePath)) {
61
+ results.push(absolutePath);
62
+ }
63
+ }
64
+ } catch (error) {
65
+ return results;
66
+ }
67
+
68
+ return results;
69
+ }
70
+
71
+ function commandMatches(args, command) {
72
+ const pattern = new RegExp(`(^|[\\\\/\\s])${command}(\\s|$)`, "i");
73
+ return pattern.test(args);
74
+ }
75
+
76
+ function detectAgent(processes) {
77
+ const ordered = [...processes].sort((left, right) => left.elapsedSeconds - right.elapsedSeconds);
78
+ for (const process of ordered) {
79
+ if (commandMatches(process.args, "codex")) {
80
+ return buildDetectedAgent("codex", process);
81
+ }
82
+ if (commandMatches(process.args, "claude")) {
83
+ return buildDetectedAgent("claude", process);
84
+ }
85
+ if (commandMatches(process.args, "gemini")) {
86
+ return buildDetectedAgent("gemini", process);
87
+ }
88
+ }
89
+ return null;
90
+ }
91
+
92
+ function buildDetectedAgent(type, process) {
93
+ return {
94
+ type,
95
+ pid: process.pid,
96
+ args: process.args,
97
+ elapsedSeconds: process.elapsedSeconds,
98
+ processStartedAtMs: Date.now() - process.elapsedSeconds * 1000,
99
+ };
100
+ }
101
+
102
+ function readFirstLines(filePath, maxLines = 20) {
103
+ const lines = [];
104
+ const fd = fs.openSync(filePath, "r");
105
+ try {
106
+ const buffer = Buffer.alloc(16384);
107
+ const bytesRead = fs.readSync(fd, buffer, 0, buffer.length, 0);
108
+ if (bytesRead === 0) {
109
+ return lines;
110
+ }
111
+
112
+ for (const line of buffer.toString("utf8", 0, bytesRead).split("\n")) {
113
+ if (line.trim().length === 0) {
114
+ continue;
115
+ }
116
+ lines.push(line.trim());
117
+ if (lines.length >= maxLines) {
118
+ break;
119
+ }
120
+ }
121
+ return lines;
122
+ } finally {
123
+ fs.closeSync(fd);
124
+ }
125
+ }
126
+
127
+ function chooseCandidate(candidates, currentPath, processStartedAtMs) {
128
+ const cwdMatches = candidates.filter((candidate) => candidate.cwd === currentPath);
129
+ if (cwdMatches.length === 0) {
130
+ return null;
131
+ }
132
+
133
+ if (processStartedAtMs !== null) {
134
+ const preciseMatches = cwdMatches
135
+ .map((candidate) => ({
136
+ ...candidate,
137
+ diffMs: Math.abs(candidate.startedAtMs - processStartedAtMs),
138
+ }))
139
+ .filter((candidate) => Number.isFinite(candidate.diffMs) && candidate.diffMs <= SESSION_MATCH_WINDOW_MS)
140
+ .sort((left, right) => left.diffMs - right.diffMs || right.mtimeMs - left.mtimeMs);
141
+
142
+ if (preciseMatches.length > 0) {
143
+ return preciseMatches[0].path;
144
+ }
145
+ }
146
+
147
+ const fallback = cwdMatches.sort((left, right) => right.mtimeMs - left.mtimeMs)[0];
148
+ return fallback ? fallback.path : null;
149
+ }
150
+
151
+ function extractThreadId(filePath) {
152
+ const match = path.basename(filePath).match(/([0-9a-f]{8}-[0-9a-f-]{27})\.jsonl$/i);
153
+ return match ? match[1] : null;
154
+ }
155
+
156
+ function readCodexSnapshotPane(threadId) {
157
+ if (!threadId) {
158
+ return null;
159
+ }
160
+
161
+ if (codexSnapshotPaneCache.has(threadId)) {
162
+ return codexSnapshotPaneCache.get(threadId);
163
+ }
164
+
165
+ const snapshotPath = path.join(CODEX_SNAPSHOT_ROOT, `${threadId}.sh`);
166
+ try {
167
+ const contents = fs.readFileSync(snapshotPath, "utf8");
168
+ const match = contents.match(/declare -x TMUX_PANE="([^"]+)"/);
169
+ const paneId = match ? match[1] : null;
170
+ codexSnapshotPaneCache.set(threadId, paneId);
171
+ return paneId;
172
+ } catch (error) {
173
+ codexSnapshotPaneCache.set(threadId, null);
174
+ return null;
175
+ }
176
+ }
177
+
178
+ function readCodexCandidate(filePath) {
179
+ try {
180
+ const [firstLine] = readFirstLines(filePath, 1);
181
+ if (!firstLine) {
182
+ return null;
183
+ }
184
+ const entry = JSON.parse(firstLine);
185
+ if (entry?.type !== "session_meta" || typeof entry.payload?.cwd !== "string") {
186
+ return null;
187
+ }
188
+
189
+ return {
190
+ path: filePath,
191
+ threadId: extractThreadId(filePath),
192
+ snapshotPaneId: readCodexSnapshotPane(extractThreadId(filePath)),
193
+ cwd: entry.payload.cwd,
194
+ startedAtMs: Date.parse(entry.payload.timestamp),
195
+ mtimeMs: fs.statSync(filePath).mtimeMs,
196
+ };
197
+ } catch (error) {
198
+ return null;
199
+ }
200
+ }
201
+
202
+ function selectCodexSessionFile(currentPath, processStartedAtMs, paneId = null) {
203
+ const candidates = walkFiles(CODEX_ROOT, (filePath) => filePath.endsWith(".jsonl"))
204
+ .map((filePath) => readCodexCandidate(filePath))
205
+ .filter((candidate) => candidate !== null);
206
+
207
+ let scopedCandidates = candidates;
208
+ if (paneId) {
209
+ const exactPaneMatches = scopedCandidates.filter((candidate) => candidate.snapshotPaneId === paneId);
210
+ if (exactPaneMatches.length > 0) {
211
+ scopedCandidates = exactPaneMatches;
212
+ } else {
213
+ scopedCandidates = scopedCandidates.filter((candidate) => candidate.snapshotPaneId === null);
214
+ }
215
+ }
216
+
217
+ return chooseCandidate(scopedCandidates, currentPath, processStartedAtMs);
218
+ }
219
+
220
+ function extractCodexAssistantText(content) {
221
+ if (!Array.isArray(content)) {
222
+ return "";
223
+ }
224
+
225
+ return content
226
+ .flatMap((item) => {
227
+ if (!item || typeof item !== "object") {
228
+ return [];
229
+ }
230
+ if (item.type === "output_text" && typeof item.text === "string") {
231
+ return [item.text.trim()];
232
+ }
233
+ return [];
234
+ })
235
+ .filter((text) => text.length > 0)
236
+ .join("\n");
237
+ }
238
+
239
+ function parseCodexFinalLine(line) {
240
+ try {
241
+ const entry = JSON.parse(line);
242
+ if (entry?.type !== "response_item" || entry.payload?.type !== "message" || entry.payload?.role !== "assistant") {
243
+ return null;
244
+ }
245
+
246
+ const text = sanitizeRelayText(extractCodexAssistantText(entry.payload.content));
247
+ if (!text) {
248
+ return null;
249
+ }
250
+
251
+ return {
252
+ id: entry.payload.id || hashText(line),
253
+ text,
254
+ timestamp: entry.timestamp || entry.payload.timestamp || new Date().toISOString(),
255
+ };
256
+ } catch (error) {
257
+ return null;
258
+ }
259
+ }
260
+
261
+ function readCodexAnswers(filePath, offset) {
262
+ const { nextOffset, text } = readAppendedText(filePath, offset);
263
+ const answers = text
264
+ .split("\n")
265
+ .map((line) => line.trim())
266
+ .filter((line) => line.length > 0)
267
+ .map((line) => parseCodexFinalLine(line))
268
+ .filter((entry) => entry !== null);
269
+
270
+ return { nextOffset, answers };
271
+ }
272
+
273
+ function readClaudeCandidate(filePath) {
274
+ try {
275
+ const lines = readFirstLines(filePath, 12);
276
+ for (const line of lines) {
277
+ const entry = JSON.parse(line);
278
+ if (typeof entry.cwd !== "string") {
279
+ continue;
280
+ }
281
+ return {
282
+ path: filePath,
283
+ cwd: entry.cwd,
284
+ startedAtMs: Date.parse(entry.timestamp || entry.message?.timestamp || 0),
285
+ mtimeMs: fs.statSync(filePath).mtimeMs,
286
+ };
287
+ }
288
+ return null;
289
+ } catch (error) {
290
+ return null;
291
+ }
292
+ }
293
+
294
+ function selectClaudeSessionFile(currentPath, processStartedAtMs) {
295
+ const candidates = walkFiles(CLAUDE_ROOT, (filePath) => filePath.endsWith(".jsonl"))
296
+ .map((filePath) => readClaudeCandidate(filePath))
297
+ .filter((candidate) => candidate !== null);
298
+
299
+ return chooseCandidate(candidates, currentPath, processStartedAtMs);
300
+ }
301
+
302
+ function extractClaudeAssistantText(content) {
303
+ if (!Array.isArray(content)) {
304
+ return "";
305
+ }
306
+
307
+ return content
308
+ .flatMap((item) => {
309
+ if (!item || typeof item !== "object") {
310
+ return [];
311
+ }
312
+ if (item.type === "text" && typeof item.text === "string") {
313
+ return [item.text.trim()];
314
+ }
315
+ return [];
316
+ })
317
+ .filter((text) => text.length > 0)
318
+ .join("\n");
319
+ }
320
+
321
+ function parseClaudeFinalLine(line) {
322
+ try {
323
+ const entry = JSON.parse(line);
324
+ if (entry?.type !== "assistant" || entry.message?.role !== "assistant" || entry.message?.stop_reason !== "end_turn") {
325
+ return null;
326
+ }
327
+
328
+ const text = sanitizeRelayText(extractClaudeAssistantText(entry.message.content));
329
+ if (!text) {
330
+ return null;
331
+ }
332
+
333
+ return {
334
+ id: entry.uuid || entry.message.id || hashText(line),
335
+ text,
336
+ timestamp: entry.timestamp || new Date().toISOString(),
337
+ };
338
+ } catch (error) {
339
+ return null;
340
+ }
341
+ }
342
+
343
+ function readClaudeAnswers(filePath, offset) {
344
+ const { nextOffset, text } = readAppendedText(filePath, offset);
345
+ const answers = text
346
+ .split("\n")
347
+ .map((line) => line.trim())
348
+ .filter((line) => line.length > 0)
349
+ .map((line) => parseClaudeFinalLine(line))
350
+ .filter((entry) => entry !== null);
351
+
352
+ return { nextOffset, answers };
353
+ }
354
+
355
+ function readGeminiCandidate(filePath) {
356
+ try {
357
+ const raw = fs.readFileSync(filePath, "utf8");
358
+ const entry = JSON.parse(raw);
359
+ return {
360
+ path: filePath,
361
+ projectHash: entry.projectHash,
362
+ cwdHash: entry.projectHash,
363
+ startedAtMs: Date.parse(entry.startTime),
364
+ mtimeMs: fs.statSync(filePath).mtimeMs,
365
+ lastUpdatedMs: Date.parse(entry.lastUpdated),
366
+ };
367
+ } catch (error) {
368
+ return null;
369
+ }
370
+ }
371
+
372
+ function selectGeminiSessionFile(currentPath, processStartedAtMs) {
373
+ const projectHash = createHash("sha256").update(currentPath).digest("hex");
374
+ const candidates = walkFiles(GEMINI_ROOT, (filePath) => filePath.endsWith(".json"))
375
+ .map((filePath) => readGeminiCandidate(filePath))
376
+ .filter((candidate) => candidate !== null && candidate.projectHash === projectHash);
377
+
378
+ if (candidates.length === 0) {
379
+ return null;
380
+ }
381
+
382
+ if (processStartedAtMs !== null) {
383
+ const preciseMatches = candidates
384
+ .map((candidate) => ({
385
+ ...candidate,
386
+ diffMs: Math.abs(candidate.startedAtMs - processStartedAtMs),
387
+ }))
388
+ .filter((candidate) => Number.isFinite(candidate.diffMs) && candidate.diffMs <= SESSION_MATCH_WINDOW_MS)
389
+ .sort((left, right) => left.diffMs - right.diffMs || right.lastUpdatedMs - left.lastUpdatedMs);
390
+
391
+ if (preciseMatches.length > 0) {
392
+ return preciseMatches[0].path;
393
+ }
394
+ }
395
+
396
+ return candidates.sort((left, right) => right.lastUpdatedMs - left.lastUpdatedMs || right.mtimeMs - left.mtimeMs)[0].path;
397
+ }
398
+
399
+ function readGeminiAnswers(filePath, lastMessageId = null) {
400
+ try {
401
+ const entry = JSON.parse(fs.readFileSync(filePath, "utf8"));
402
+ const messages = Array.isArray(entry.messages) ? entry.messages : [];
403
+ const finalMessages = messages.filter((message) => {
404
+ const toolCalls = Array.isArray(message.toolCalls) ? message.toolCalls : [];
405
+ return message.type === "gemini" && typeof message.content === "string" && message.content.trim() && toolCalls.length === 0;
406
+ });
407
+
408
+ let startIndex = 0;
409
+ if (lastMessageId) {
410
+ const previousIndex = finalMessages.findIndex((message) => message.id === lastMessageId);
411
+ startIndex = previousIndex === -1 ? finalMessages.length : previousIndex + 1;
412
+ }
413
+
414
+ const answers = finalMessages.slice(startIndex).map((message) => ({
415
+ id: message.id || hashText(JSON.stringify(message)),
416
+ text: sanitizeRelayText(message.content),
417
+ timestamp: message.timestamp || entry.lastUpdated || new Date().toISOString(),
418
+ }));
419
+
420
+ return {
421
+ answers: answers.filter((answer) => answer.text.length > 0),
422
+ lastMessageId: finalMessages.length > 0 ? finalMessages[finalMessages.length - 1].id : lastMessageId,
423
+ fileSize: getFileSize(filePath),
424
+ };
425
+ } catch (error) {
426
+ return {
427
+ answers: [],
428
+ lastMessageId,
429
+ fileSize: 0,
430
+ };
431
+ }
432
+ }
433
+
434
+ module.exports = {
435
+ PRESETS,
436
+ detectAgent,
437
+ parseClaudeFinalLine,
438
+ parseCodexFinalLine,
439
+ readClaudeAnswers,
440
+ readCodexAnswers,
441
+ readGeminiAnswers,
442
+ selectClaudeSessionFile,
443
+ selectCodexSessionFile,
444
+ selectGeminiSessionFile,
445
+ };