muuuuse 0.2.0 → 1.3.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
@@ -1,22 +1,26 @@
1
1
  # 🔌Muuuuse
2
2
 
3
- `muuuuse` installs one CLI name:
3
+ `muuuuse` installs one CLI:
4
4
 
5
5
  - `muuuuse`
6
6
 
7
- The visible product brand is always `🔌Muuuuse`, while the terminal command examples use `muuuuse`.
7
+ The public brand stays `🔌Muuuuse`. The terminal command stays `muuuuse`.
8
8
 
9
- ## What It Does
9
+ ## What It Is Now
10
10
 
11
- `🔌Muuuuse` is the small local-only relay for three tmux terminals:
11
+ `🔌Muuuuse` no longer expects you to arm a terminal first and launch something later.
12
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
13
+ The main flow is:
16
14
 
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.
15
+ ```bash
16
+ muuuuse 1 <program...>
17
+ muuuuse 2 <program...>
18
+ muuuuse 3 stop
19
+ ```
20
+
21
+ Seat `1` and seat `2` each launch and own a real local program under a PTY wrapper. Once both seats are alive in the same lane, they automatically bounce final blocks between each other by typing the partner answer plus `Enter` into the wrapped program.
18
22
 
19
- Remote control is intentionally out of scope here. Use `codeman` or `codemansbot` for remote routing.
23
+ `muuuuse 3 stop` is the cleanup command for that lane.
20
24
 
21
25
  ## Install
22
26
 
@@ -24,72 +28,66 @@ Remote control is intentionally out of scope here. Use `codeman` or `codemansbot
24
28
  npm install -g muuuuse
25
29
  ```
26
30
 
27
- ## Basic Flow
31
+ ## Fastest AI Flow
28
32
 
29
33
  Terminal 1:
30
34
 
31
35
  ```bash
32
- muuuuse 1
33
- codex -m gpt-5.4 -c model_reasoning_effort=low --dangerously-bypass-approvals-and-sandbox --no-alt-screen
36
+ muuuuse 1 codex
34
37
  ```
35
38
 
36
39
  Terminal 2:
37
40
 
38
41
  ```bash
39
- muuuuse 2
40
- claude --dangerously-skip-permissions --permission-mode bypassPermissions
42
+ muuuuse 2 gemini
41
43
  ```
42
44
 
43
45
  Terminal 3:
44
46
 
45
47
  ```bash
46
- muuuuse 3 "Start by proposing the first concrete repo task."
48
+ muuuuse 3 stop
47
49
  ```
48
50
 
49
- That third command auto-pairs seats `1` and `2`, then optionally drops a one-time kickoff prompt into seat `1`.
51
+ Known presets expand to recommended flags automatically:
50
52
 
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.
53
+ - `codex`
54
+ - `claude`
55
+ - `gemini`
54
56
 
55
- Recommended god-mode launches:
57
+ So `muuuuse 1 codex` launches the fuller Codex command, not just bare `codex`.
56
58
 
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
- ```
59
+ ## Generic Program Flow
62
60
 
63
- ## Script Mode
61
+ This is not AI-only. Any local program can be wrapped directly.
64
62
 
65
- Turn an armed seat into a deterministic responder:
63
+ Example:
66
64
 
67
65
  ```bash
68
- muuuuse script
66
+ muuuuse 1 bash -lc 'while read line; do printf "left: %s\n\n" "$line"; done'
67
+ muuuuse 2 bash -lc 'while read line; do printf "right: %s\n\n" "$line"; done'
69
68
  ```
70
69
 
71
- That stores one repeating response.
70
+ Type into one seat and the other seat will receive the relayed block.
72
71
 
73
- For a loop of multiple steps:
72
+ For Codex, Claude, and Gemini, `🔌Muuuuse` prefers their structured session logs. For anything else, it falls back to the last stable output block after a turn goes idle.
74
73
 
75
- ```bash
76
- muuuuse script 4
77
- ```
74
+ ## Sessions
78
75
 
79
- That collects four prompts and cycles them forever, one per inbound turn.
76
+ Seats auto-pair by current working directory by default.
80
77
 
81
- To leave script mode and go back to a live CLI listener:
78
+ If you want an explicit lane name, use:
82
79
 
83
80
  ```bash
84
- muuuuse live
81
+ muuuuse 1 --session demo codex
82
+ muuuuse 2 --session demo gemini
83
+ muuuuse 3 --session demo stop
85
84
  ```
86
85
 
87
- ## Requirements
86
+ You can also inspect the lane:
88
87
 
89
- - `tmux`
90
- - `git`
91
- - `npm`
92
- - at least one local CLI you want to mirror: `codex`, `claude`, `gemini`, or script mode
88
+ ```bash
89
+ muuuuse 3 status
90
+ ```
93
91
 
94
92
  ## Doctor
95
93
 
@@ -97,21 +95,11 @@ muuuuse live
97
95
  muuuuse doctor
98
96
  ```
99
97
 
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
98
+ This checks the local runtime plus common agent binaries if you use them.
109
99
 
110
100
  ## Notes
111
101
 
112
102
  - 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
103
+ - no tmux requirement for the main path
104
+ - no remote control surface here; that belongs to `codeman`
105
+ - best with programs that naturally produce turn-shaped output
package/bin/muuse.js CHANGED
File without changes
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "muuuuse",
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.",
3
+ "version": "1.3.0",
4
+ "description": "🔌Muuuuse wraps two local programs and bounces final blocks between them from the terminal you launched.",
5
5
  "type": "commonjs",
6
6
  "bin": {
7
7
  "muuuuse": "bin/muuse.js"
@@ -30,9 +30,11 @@
30
30
  "keywords": [
31
31
  "terminal",
32
32
  "ai",
33
- "tmux",
33
+ "pty",
34
34
  "relay",
35
35
  "local",
36
+ "automation",
37
+ "script",
36
38
  "codex",
37
39
  "claude",
38
40
  "gemini"
@@ -41,5 +43,8 @@
41
43
  "test": "node test/cli.test.js",
42
44
  "pack:local": "npm pack",
43
45
  "prepublishOnly": "npm test"
46
+ },
47
+ "dependencies": {
48
+ "node-pty": "^1.1.0"
44
49
  }
45
50
  }
package/src/agents.js CHANGED
@@ -44,6 +44,33 @@ const PRESETS = {
44
44
  },
45
45
  };
46
46
 
47
+ function expandPresetCommand(commandTokens, usePresets = true) {
48
+ if (!usePresets || !Array.isArray(commandTokens) || commandTokens.length !== 1) {
49
+ return Array.isArray(commandTokens) ? [...commandTokens] : [];
50
+ }
51
+
52
+ const preset = PRESETS[String(commandTokens[0] || "").toLowerCase()];
53
+ return preset ? [...preset.command] : [...commandTokens];
54
+ }
55
+
56
+ function detectAgentTypeFromCommand(commandTokens) {
57
+ const executable = path.basename(String(commandTokens?.[0] || "")).toLowerCase();
58
+ if (!executable) {
59
+ return null;
60
+ }
61
+
62
+ if (executable === "codex") {
63
+ return "codex";
64
+ }
65
+ if (executable === "claude") {
66
+ return "claude";
67
+ }
68
+ if (executable === "gemini") {
69
+ return "gemini";
70
+ }
71
+ return null;
72
+ }
73
+
47
74
  const CODEX_ROOT = path.join(os.homedir(), ".codex", "sessions");
48
75
  const CLAUDE_ROOT = path.join(os.homedir(), ".claude", "projects");
49
76
  const GEMINI_ROOT = path.join(os.homedir(), ".gemini", "tmp");
@@ -434,6 +461,8 @@ function readGeminiAnswers(filePath, lastMessageId = null) {
434
461
  module.exports = {
435
462
  PRESETS,
436
463
  detectAgent,
464
+ detectAgentTypeFromCommand,
465
+ expandPresetCommand,
437
466
  parseClaudeFinalLine,
438
467
  parseCodexFinalLine,
439
468
  readClaudeAnswers,
package/src/cli.js CHANGED
@@ -1,19 +1,17 @@
1
- const fs = require("node:fs");
2
- const path = require("node:path");
3
- const readline = require("node:readline/promises");
4
-
5
- const { PRESETS } = require("./agents");
6
- const { Controller, SeatDaemon, armSeat, configureScript, enableLiveMode } = require("./runtime");
7
- const { getPaneInfo, insideTmux } = require("./tmux");
8
1
  const {
9
2
  BRAND,
10
3
  commandExists,
11
- findFirstExisting,
12
4
  parseFlags,
13
5
  readCommandVersion,
14
- toInt,
15
6
  usage,
16
7
  } = require("./util");
8
+ const {
9
+ SeatProcess,
10
+ readSessionStatus,
11
+ resolveProgramTokens,
12
+ resolveSessionName,
13
+ stopSession,
14
+ } = require("./runtime");
17
15
 
18
16
  async function main(argv = process.argv.slice(2)) {
19
17
  if (argv.length === 0 || argv.includes("--help") || argv.includes("-h")) {
@@ -28,130 +26,67 @@ async function main(argv = process.argv.slice(2)) {
28
26
  return;
29
27
  }
30
28
 
31
- if (command === "daemon") {
32
- const sessionName = argv[1];
33
- const seatId = toInt(argv[2], 0);
34
- if (!sessionName || ![1, 2].includes(seatId)) {
35
- throw new Error("daemon requires <session-name> <1|2>.");
36
- }
37
-
38
- const daemon = new SeatDaemon(sessionName, seatId);
39
- const code = await daemon.run();
40
- process.exit(code);
41
- }
42
-
43
29
  if (command === "1" || command === "2") {
44
- armCurrentPane(Number(command));
45
- return;
46
- }
47
-
48
- if (command === "3") {
49
30
  const { positionals, flags } = parseFlags(argv.slice(1));
50
- const paneInfo = requireTmuxPane();
51
- const seedText = positionals.join(" ").trim();
52
- const controller = new Controller(paneInfo.sessionName, {
53
- seedSeat: flags.seedSeat,
54
- seedText,
31
+ const sessionName = resolveSessionName(flags.session, process.cwd());
32
+ const commandTokens = resolveProgramTokens(positionals, !flags.noPreset);
33
+ const seat = new SeatProcess({
34
+ commandTokens,
35
+ cwd: process.cwd(),
55
36
  maxRelays: flags.maxRelays,
37
+ seatId: Number(command),
38
+ sessionName,
56
39
  });
57
- const code = await controller.run();
40
+ const code = await seat.run();
58
41
  process.exit(code);
59
42
  }
60
43
 
61
- if (command === "script") {
62
- await setScriptMode(argv.slice(1));
63
- return;
64
- }
65
-
66
- if (command === "live") {
67
- const paneInfo = requireTmuxPane();
68
- const seat = enableLiveMode({
69
- sessionName: paneInfo.sessionName,
70
- paneId: paneInfo.paneId,
71
- });
72
- process.stdout.write(`${BRAND} seat ${seat.seatId} is back in live-listen mode.\n`);
73
- return;
74
- }
75
-
76
- throw new Error(`Unknown command '${command}'.`);
77
- }
78
-
79
- function requireTmuxPane() {
80
- if (!insideTmux()) {
81
- throw new Error("muuuuse must run inside tmux so it can arm and inject the current pane.");
82
- }
83
-
84
- const paneInfo = getPaneInfo();
85
- if (!paneInfo) {
86
- throw new Error("tmux is active, but the current pane could not be resolved.");
87
- }
88
- return paneInfo;
89
- }
90
-
91
- function armCurrentPane(seatId) {
92
- const paneInfo = requireTmuxPane();
93
- const binPath = path.resolve(__dirname, "..", "bin", "muuse.js");
94
- const meta = armSeat({
95
- seatId,
96
- paneInfo,
97
- binPath,
98
- });
99
-
100
- process.stdout.write(`${BRAND} armed seat ${seatId} in tmux session ${meta.sessionName}.\n`);
101
- process.stdout.write(`Pane: ${meta.paneId}\n`);
102
- process.stdout.write(`Path: ${meta.cwd}\n`);
103
- process.stdout.write("\nLaunch one of these in this same terminal whenever you're ready:\n");
104
- for (const preset of Object.values(PRESETS)) {
105
- process.stdout.write(`- ${preset.command.join(" ")}\n`);
106
- }
107
- process.stdout.write("\nOr switch this seat to deterministic mode with `muuuuse script`.\n");
108
- }
109
-
110
- async function setScriptMode(argv) {
111
- const paneInfo = requireTmuxPane();
112
- const { positionals, flags } = parseFlags(argv);
113
- const count = Math.max(1, toInt(positionals[0] || 1, 1));
114
- const steps = [...flags.step];
115
-
116
- if (steps.length < count) {
117
- const rl = readline.createInterface({
118
- input: process.stdin,
119
- output: process.stdout,
120
- });
44
+ if (command === "3") {
45
+ const { positionals, flags } = parseFlags(argv.slice(1));
46
+ const sessionName = resolveSessionName(flags.session, process.cwd());
47
+ const action = String(positionals[0] || "status").toLowerCase();
48
+
49
+ if (action === "stop") {
50
+ const result = stopSession(sessionName);
51
+ process.stdout.write(`${BRAND} stop requested for session ${result.sessionName}.\n`);
52
+ for (const seat of result.seats) {
53
+ process.stdout.write(
54
+ `seat ${seat.seatId}: wrapper ${seat.wrapperStopped ? "signaled" : "idle"}`
55
+ + `, child ${seat.childStopped ? "signaled" : "idle"}\n`
56
+ );
57
+ }
58
+ return;
59
+ }
121
60
 
122
- try {
123
- while (steps.length < count) {
124
- const answer = (await rl.question(`Script step ${steps.length + 1}/${count}: `)).trim();
125
- if (!answer) {
126
- process.stdout.write("Step cannot be empty.\n");
61
+ if (action === "status") {
62
+ const status = readSessionStatus(sessionName);
63
+ process.stdout.write(`${BRAND} session ${status.sessionName}\n`);
64
+ for (const seat of status.seats) {
65
+ if (!seat.status) {
66
+ process.stdout.write(`seat ${seat.seatId}: idle\n`);
127
67
  continue;
128
68
  }
129
- steps.push(answer);
69
+
70
+ const state = seat.status.state || "unknown";
71
+ const program = Array.isArray(seat.status.command) ? seat.status.command.join(" ") : "";
72
+ process.stdout.write(`seat ${seat.seatId}: ${state}${program ? ` (${program})` : ""}\n`);
130
73
  }
131
- } finally {
132
- rl.close();
74
+ return;
133
75
  }
134
- }
135
76
 
136
- const result = configureScript({
137
- sessionName: paneInfo.sessionName,
138
- paneId: paneInfo.paneId,
139
- steps: steps.slice(0, count),
140
- });
77
+ throw new Error(`Unknown seat 3 action '${action}'. Try \`muuuuse 3 stop\` or \`muuuuse 3 status\`.`);
78
+ }
141
79
 
142
- process.stdout.write(`${BRAND} seat ${result.seatId} is now in script mode with ${result.steps.length} step`);
143
- process.stdout.write(result.steps.length === 1 ? ".\n" : "s.\n");
80
+ throw new Error(`Unknown command '${command}'.`);
144
81
  }
145
82
 
146
83
  function runDoctor() {
147
84
  const checks = [
148
- checkBinary("git", ["--version"], true),
85
+ checkBinary("node", ["--version"], true),
149
86
  checkBinary("npm", ["--version"], true),
150
- checkBinary("tmux", ["-V"], true),
151
87
  checkBinary("codex", ["--version"], false),
152
88
  checkBinary("claude", ["--version"], false),
153
89
  checkBinary("gemini", ["--version"], false),
154
- checkPath("npm token file", findFirstExisting(["/root/npm.txt", "/root/_ops-bank/credentials/npm.txt"]), true),
155
90
  ];
156
91
 
157
92
  process.stdout.write(`${BRAND} doctor\n\n`);
@@ -159,12 +94,11 @@ function runDoctor() {
159
94
  process.stdout.write(`${item.ok ? "OK " : "MISS"} ${item.label}${item.detail ? `: ${item.detail}` : ""}\n`);
160
95
  }
161
96
 
162
- process.stdout.write("\nLaunch presets\n");
163
- for (const [name, preset] of Object.entries(PRESETS)) {
164
- process.stdout.write(`- ${name}: ${preset.command.join(" ")}\n`);
165
- }
166
-
167
- process.stdout.write("\nRemote routing lives in Codeman / codemansbot. This package stays local-only.\n");
97
+ process.stdout.write("\nKnown presets\n");
98
+ process.stdout.write("- codex\n");
99
+ process.stdout.write("- claude\n");
100
+ process.stdout.write("- gemini\n");
101
+ process.stdout.write("\nAny other local program can be wrapped directly with `muuuuse 1 <program...>` or `muuuuse 2 <program...>`.\n");
168
102
 
169
103
  const missingRequired = checks.some((item) => item.required && !item.ok);
170
104
  if (missingRequired) {
@@ -180,13 +114,6 @@ function checkBinary(command, versionArgs, required) {
180
114
  return { label: command, ok: true, detail: version, required };
181
115
  }
182
116
 
183
- function checkPath(label, filePath, required) {
184
- if (!filePath || !fs.existsSync(filePath)) {
185
- return { label, ok: false, detail: "not found", required };
186
- }
187
- return { label, ok: true, detail: filePath, required };
188
- }
189
-
190
117
  module.exports = {
191
118
  main,
192
119
  runDoctor,