muuuuse 1.3.2 → 1.4.1
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 +33 -66
- package/package.json +2 -3
- package/src/agents.js +76 -224
- package/src/cli.js +47 -119
- package/src/runtime.js +625 -373
- package/src/util.js +43 -123
- package/src/tmux.js +0 -170
package/src/util.js
CHANGED
|
@@ -2,24 +2,11 @@ const { createHash, randomBytes } = require("node:crypto");
|
|
|
2
2
|
const fs = require("node:fs");
|
|
3
3
|
const os = require("node:os");
|
|
4
4
|
const path = require("node:path");
|
|
5
|
-
const { spawnSync } = require("node:child_process");
|
|
6
5
|
|
|
7
6
|
const BRAND = "🔌Muuuuse";
|
|
8
|
-
const POLL_MS =
|
|
9
|
-
const SESSION_MATCH_WINDOW_MS = 5 * 60 * 1000;
|
|
7
|
+
const POLL_MS = 220;
|
|
10
8
|
const MAX_RELAY_CHARS = 4000;
|
|
11
|
-
|
|
12
|
-
const FLAG_ALIASES = new Map([
|
|
13
|
-
["--max-relays", "maxRelays"],
|
|
14
|
-
["--no-preset", "noPreset"],
|
|
15
|
-
["--session", "session"],
|
|
16
|
-
]);
|
|
17
|
-
|
|
18
|
-
const BOOLEAN_FLAGS = new Set(["noPreset"]);
|
|
19
|
-
|
|
20
|
-
function shellEscape(value) {
|
|
21
|
-
return `'${String(value).replace(/'/g, `'\\''`)}'`;
|
|
22
|
-
}
|
|
9
|
+
const SESSION_MATCH_WINDOW_MS = 5 * 60 * 1000;
|
|
23
10
|
|
|
24
11
|
function createId(length = 10) {
|
|
25
12
|
return randomBytes(Math.ceil(length / 2)).toString("hex").slice(0, length);
|
|
@@ -34,12 +21,6 @@ function ensureDir(dirPath) {
|
|
|
34
21
|
return dirPath;
|
|
35
22
|
}
|
|
36
23
|
|
|
37
|
-
function resetDir(dirPath) {
|
|
38
|
-
fs.rmSync(dirPath, { recursive: true, force: true });
|
|
39
|
-
fs.mkdirSync(dirPath, { recursive: true });
|
|
40
|
-
return dirPath;
|
|
41
|
-
}
|
|
42
|
-
|
|
43
24
|
function writeJson(filePath, value) {
|
|
44
25
|
ensureDir(path.dirname(filePath));
|
|
45
26
|
const tempPath = `${filePath}.${process.pid}.${Date.now()}.tmp`;
|
|
@@ -50,7 +31,7 @@ function writeJson(filePath, value) {
|
|
|
50
31
|
function readJson(filePath, fallback = null) {
|
|
51
32
|
try {
|
|
52
33
|
return JSON.parse(fs.readFileSync(filePath, "utf8"));
|
|
53
|
-
} catch
|
|
34
|
+
} catch {
|
|
54
35
|
return fallback;
|
|
55
36
|
}
|
|
56
37
|
}
|
|
@@ -65,10 +46,7 @@ function readAppendedText(filePath, previousOffset = 0) {
|
|
|
65
46
|
const stats = fs.statSync(filePath);
|
|
66
47
|
const startOffset = stats.size < previousOffset ? 0 : previousOffset;
|
|
67
48
|
if (stats.size === startOffset) {
|
|
68
|
-
return {
|
|
69
|
-
nextOffset: startOffset,
|
|
70
|
-
text: "",
|
|
71
|
-
};
|
|
49
|
+
return { nextOffset: startOffset, text: "" };
|
|
72
50
|
}
|
|
73
51
|
|
|
74
52
|
const fd = fs.openSync(filePath, "r");
|
|
@@ -85,10 +63,7 @@ function readAppendedText(filePath, previousOffset = 0) {
|
|
|
85
63
|
}
|
|
86
64
|
} catch (error) {
|
|
87
65
|
if (error && error.code === "ENOENT") {
|
|
88
|
-
return {
|
|
89
|
-
nextOffset: 0,
|
|
90
|
-
text: "",
|
|
91
|
-
};
|
|
66
|
+
return { nextOffset: 0, text: "" };
|
|
92
67
|
}
|
|
93
68
|
throw error;
|
|
94
69
|
}
|
|
@@ -97,29 +72,11 @@ function readAppendedText(filePath, previousOffset = 0) {
|
|
|
97
72
|
function getFileSize(filePath) {
|
|
98
73
|
try {
|
|
99
74
|
return fs.statSync(filePath).size;
|
|
100
|
-
} catch
|
|
75
|
+
} catch {
|
|
101
76
|
return 0;
|
|
102
77
|
}
|
|
103
78
|
}
|
|
104
79
|
|
|
105
|
-
function commandExists(command) {
|
|
106
|
-
const result = spawnSync("bash", ["-lc", `command -v ${shellEscape(command)} >/dev/null 2>&1`], {
|
|
107
|
-
encoding: "utf8",
|
|
108
|
-
});
|
|
109
|
-
return result.status === 0;
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
function readCommandVersion(command, args = ["--version"]) {
|
|
113
|
-
const result = spawnSync(command, args, {
|
|
114
|
-
encoding: "utf8",
|
|
115
|
-
timeout: 4000,
|
|
116
|
-
});
|
|
117
|
-
if (result.status !== 0) {
|
|
118
|
-
return null;
|
|
119
|
-
}
|
|
120
|
-
return (result.stdout || result.stderr || "").trim().split("\n")[0] || null;
|
|
121
|
-
}
|
|
122
|
-
|
|
123
80
|
function stripAnsi(text) {
|
|
124
81
|
return String(text || "").replace(
|
|
125
82
|
// eslint-disable-next-line no-control-regex
|
|
@@ -132,6 +89,7 @@ function sanitizeRelayText(input, maxChars = MAX_RELAY_CHARS) {
|
|
|
132
89
|
const normalized = stripAnsi(input)
|
|
133
90
|
.replace(/\r/g, "")
|
|
134
91
|
.replace(/\u0000/g, "")
|
|
92
|
+
.replace(/\u0007/g, "")
|
|
135
93
|
.replace(/\n{3,}/g, "\n\n")
|
|
136
94
|
.trim();
|
|
137
95
|
|
|
@@ -142,11 +100,6 @@ function sanitizeRelayText(input, maxChars = MAX_RELAY_CHARS) {
|
|
|
142
100
|
return `${normalized.slice(0, maxChars - 3).trimEnd()}...`;
|
|
143
101
|
}
|
|
144
102
|
|
|
145
|
-
function toInt(value, fallback) {
|
|
146
|
-
const parsed = Number.parseInt(String(value), 10);
|
|
147
|
-
return Number.isFinite(parsed) ? parsed : fallback;
|
|
148
|
-
}
|
|
149
|
-
|
|
150
103
|
function isPidAlive(pid) {
|
|
151
104
|
if (!Number.isInteger(pid) || pid <= 0) {
|
|
152
105
|
return false;
|
|
@@ -155,7 +108,7 @@ function isPidAlive(pid) {
|
|
|
155
108
|
try {
|
|
156
109
|
process.kill(pid, 0);
|
|
157
110
|
return true;
|
|
158
|
-
} catch
|
|
111
|
+
} catch {
|
|
159
112
|
return false;
|
|
160
113
|
}
|
|
161
114
|
}
|
|
@@ -179,7 +132,7 @@ function getDefaultSessionName(currentPath = process.cwd()) {
|
|
|
179
132
|
const resolvedPath = (() => {
|
|
180
133
|
try {
|
|
181
134
|
return fs.realpathSync(currentPath);
|
|
182
|
-
} catch
|
|
135
|
+
} catch {
|
|
183
136
|
return path.resolve(currentPath);
|
|
184
137
|
}
|
|
185
138
|
})();
|
|
@@ -192,6 +145,15 @@ function getSessionDir(sessionName) {
|
|
|
192
145
|
return ensureDir(path.join(getStateRoot(), "sessions", slugifySegment(sessionName)));
|
|
193
146
|
}
|
|
194
147
|
|
|
148
|
+
function getSessionPaths(sessionName) {
|
|
149
|
+
const dir = getSessionDir(sessionName);
|
|
150
|
+
return {
|
|
151
|
+
dir,
|
|
152
|
+
controllerPath: path.join(dir, "controller.json"),
|
|
153
|
+
stopPath: path.join(dir, "stop.json"),
|
|
154
|
+
};
|
|
155
|
+
}
|
|
156
|
+
|
|
195
157
|
function getSeatDir(sessionName, seatId) {
|
|
196
158
|
return ensureDir(path.join(getSessionDir(sessionName), `seat-${seatId}`));
|
|
197
159
|
}
|
|
@@ -200,111 +162,69 @@ function getSeatPaths(sessionName, seatId) {
|
|
|
200
162
|
const dir = getSeatDir(sessionName, seatId);
|
|
201
163
|
return {
|
|
202
164
|
dir,
|
|
165
|
+
daemonPath: path.join(dir, "daemon.json"),
|
|
203
166
|
eventsPath: path.join(dir, "events.jsonl"),
|
|
204
167
|
metaPath: path.join(dir, "meta.json"),
|
|
168
|
+
pipePath: path.join(dir, "pipe.log"),
|
|
205
169
|
statusPath: path.join(dir, "status.json"),
|
|
206
170
|
};
|
|
207
171
|
}
|
|
208
172
|
|
|
209
|
-
function
|
|
210
|
-
const
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
const token = argv[index];
|
|
219
|
-
if (!token.startsWith("--")) {
|
|
220
|
-
positionals.push(token);
|
|
221
|
-
continue;
|
|
222
|
-
}
|
|
223
|
-
|
|
224
|
-
const [rawFlag, inlineValue] = token.split("=", 2);
|
|
225
|
-
const key = FLAG_ALIASES.get(rawFlag);
|
|
226
|
-
if (!key) {
|
|
227
|
-
throw new Error(`Unknown flag: ${rawFlag}`);
|
|
228
|
-
}
|
|
229
|
-
|
|
230
|
-
if (BOOLEAN_FLAGS.has(key)) {
|
|
231
|
-
flags[key] = true;
|
|
232
|
-
continue;
|
|
233
|
-
}
|
|
234
|
-
|
|
235
|
-
const next = inlineValue !== undefined ? inlineValue : argv[index + 1];
|
|
236
|
-
if (inlineValue === undefined) {
|
|
237
|
-
index += 1;
|
|
238
|
-
}
|
|
239
|
-
|
|
240
|
-
if (next === undefined) {
|
|
241
|
-
throw new Error(`Missing value for ${rawFlag}`);
|
|
242
|
-
}
|
|
243
|
-
|
|
244
|
-
flags[key] = next;
|
|
173
|
+
function listSessionNames() {
|
|
174
|
+
const sessionsRoot = path.join(getStateRoot(), "sessions");
|
|
175
|
+
try {
|
|
176
|
+
return fs.readdirSync(sessionsRoot, { withFileTypes: true })
|
|
177
|
+
.filter((entry) => entry.isDirectory())
|
|
178
|
+
.map((entry) => entry.name)
|
|
179
|
+
.sort();
|
|
180
|
+
} catch {
|
|
181
|
+
return [];
|
|
245
182
|
}
|
|
246
|
-
|
|
247
|
-
flags.maxRelays = flags.maxRelays === Number.POSITIVE_INFINITY
|
|
248
|
-
? Number.POSITIVE_INFINITY
|
|
249
|
-
: toInt(flags.maxRelays, Number.POSITIVE_INFINITY);
|
|
250
|
-
|
|
251
|
-
return {
|
|
252
|
-
positionals,
|
|
253
|
-
flags,
|
|
254
|
-
};
|
|
255
183
|
}
|
|
256
184
|
|
|
257
185
|
function usage() {
|
|
258
186
|
return [
|
|
259
|
-
`${BRAND}
|
|
187
|
+
`${BRAND} arms two regular terminals and relays final answers between them.`,
|
|
260
188
|
"",
|
|
261
189
|
"Usage:",
|
|
262
|
-
" muuuuse 1
|
|
263
|
-
" muuuuse 2
|
|
190
|
+
" muuuuse 1",
|
|
191
|
+
" muuuuse 2",
|
|
264
192
|
" muuuuse stop",
|
|
265
193
|
" muuuuse status",
|
|
266
|
-
" muuuuse doctor",
|
|
267
194
|
"",
|
|
268
|
-
"
|
|
269
|
-
" muuuuse 1
|
|
270
|
-
" muuuuse 2
|
|
271
|
-
"
|
|
272
|
-
"
|
|
195
|
+
"Flow:",
|
|
196
|
+
" 1. Run `muuuuse 1` in terminal one.",
|
|
197
|
+
" 2. Run `muuuuse 2` in terminal two.",
|
|
198
|
+
" 3. Use those armed shells normally.",
|
|
199
|
+
" 4. Codex, Claude, and Gemini final answers relay automatically from their local session logs.",
|
|
200
|
+
" 5. Run `muuuuse status` or `muuuuse stop` from any shell.",
|
|
273
201
|
"",
|
|
274
202
|
"Notes:",
|
|
275
|
-
" -
|
|
276
|
-
" -
|
|
277
|
-
" -
|
|
278
|
-
" - Any other program runs as-is inside the current terminal under a PTY wrapper.",
|
|
203
|
+
" - No tmux.",
|
|
204
|
+
" - `muuuuse stop` and `muuuuse status` work from another terminal or the same one.",
|
|
205
|
+
" - State lives under `~/.muuuuse`.",
|
|
279
206
|
].join("\n");
|
|
280
207
|
}
|
|
281
208
|
|
|
282
209
|
module.exports = {
|
|
283
210
|
BRAND,
|
|
284
|
-
MAX_RELAY_CHARS,
|
|
285
211
|
POLL_MS,
|
|
286
212
|
SESSION_MATCH_WINDOW_MS,
|
|
287
213
|
appendJsonl,
|
|
288
|
-
commandExists,
|
|
289
214
|
createId,
|
|
290
215
|
ensureDir,
|
|
291
216
|
getDefaultSessionName,
|
|
292
217
|
getFileSize,
|
|
293
|
-
getSeatDir,
|
|
294
218
|
getSeatPaths,
|
|
295
|
-
|
|
219
|
+
getSessionPaths,
|
|
296
220
|
getStateRoot,
|
|
297
221
|
hashText,
|
|
298
222
|
isPidAlive,
|
|
299
|
-
|
|
223
|
+
listSessionNames,
|
|
300
224
|
readAppendedText,
|
|
301
|
-
readCommandVersion,
|
|
302
225
|
readJson,
|
|
303
|
-
resetDir,
|
|
304
226
|
sanitizeRelayText,
|
|
305
|
-
shellEscape,
|
|
306
227
|
sleep,
|
|
307
|
-
toInt,
|
|
308
228
|
usage,
|
|
309
229
|
writeJson,
|
|
310
230
|
};
|
package/src/tmux.js
DELETED
|
@@ -1,170 +0,0 @@
|
|
|
1
|
-
const { execFileSync } = require("node:child_process");
|
|
2
|
-
const SLEEP_BUFFER = new SharedArrayBuffer(4);
|
|
3
|
-
const SLEEP_VIEW = new Int32Array(SLEEP_BUFFER);
|
|
4
|
-
|
|
5
|
-
function runTmux(args) {
|
|
6
|
-
return execFileSync("tmux", args, { encoding: "utf8" });
|
|
7
|
-
}
|
|
8
|
-
|
|
9
|
-
function insideTmux() {
|
|
10
|
-
return Boolean(process.env.TMUX && process.env.TMUX_PANE);
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
function getPaneInfo(target = process.env.TMUX_PANE) {
|
|
14
|
-
if (!target) {
|
|
15
|
-
return null;
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
try {
|
|
19
|
-
const output = runTmux([
|
|
20
|
-
"display-message",
|
|
21
|
-
"-p",
|
|
22
|
-
"-t",
|
|
23
|
-
target,
|
|
24
|
-
"#{session_name}\t#{window_index}\t#{window_name}\t#{pane_id}\t#{pane_current_path}\t#{pane_pid}",
|
|
25
|
-
]).trim();
|
|
26
|
-
|
|
27
|
-
if (!output) {
|
|
28
|
-
return null;
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
const [sessionName = "", windowIndex = "", windowName = "", paneId = "", currentPath = "", panePid = ""] =
|
|
32
|
-
output.split("\t");
|
|
33
|
-
|
|
34
|
-
return {
|
|
35
|
-
sessionName,
|
|
36
|
-
windowIndex: Number.parseInt(windowIndex, 10),
|
|
37
|
-
windowName,
|
|
38
|
-
paneId,
|
|
39
|
-
currentPath,
|
|
40
|
-
panePid: Number.parseInt(panePid, 10),
|
|
41
|
-
};
|
|
42
|
-
} catch (error) {
|
|
43
|
-
return null;
|
|
44
|
-
}
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
function paneExists(paneId) {
|
|
48
|
-
if (!paneId) {
|
|
49
|
-
return false;
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
try {
|
|
53
|
-
const output = runTmux(["display-message", "-p", "-t", paneId, "#{pane_id}"]).trim();
|
|
54
|
-
return output.length > 0;
|
|
55
|
-
} catch (error) {
|
|
56
|
-
return false;
|
|
57
|
-
}
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
function setPaneTitle(paneId, title) {
|
|
61
|
-
try {
|
|
62
|
-
runTmux(["select-pane", "-t", paneId, "-T", title]);
|
|
63
|
-
return true;
|
|
64
|
-
} catch (error) {
|
|
65
|
-
return false;
|
|
66
|
-
}
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
function sendLiteral(paneId, text) {
|
|
70
|
-
const chunkSize = 800;
|
|
71
|
-
for (let start = 0; start < text.length; start += chunkSize) {
|
|
72
|
-
const chunk = text.slice(start, start + chunkSize);
|
|
73
|
-
if (chunk.length > 0) {
|
|
74
|
-
runTmux(["send-keys", "-t", paneId, "-l", chunk]);
|
|
75
|
-
}
|
|
76
|
-
}
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
function sleepSync(ms) {
|
|
80
|
-
Atomics.wait(SLEEP_VIEW, 0, 0, ms);
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
function sendTextAndEnter(paneId, text) {
|
|
84
|
-
const lines = String(text || "").replace(/\r/g, "").split("\n");
|
|
85
|
-
|
|
86
|
-
for (let index = 0; index < lines.length; index += 1) {
|
|
87
|
-
const line = lines[index];
|
|
88
|
-
if (line.length > 0) {
|
|
89
|
-
sendLiteral(paneId, line);
|
|
90
|
-
sleepSync(120);
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
if (index < lines.length - 1) {
|
|
94
|
-
runTmux(["send-keys", "-t", paneId, "Enter"]);
|
|
95
|
-
}
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
sleepSync(120);
|
|
99
|
-
runTmux(["send-keys", "-t", paneId, "Enter"]);
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
function capturePaneText(paneId, lines = 220) {
|
|
103
|
-
try {
|
|
104
|
-
return runTmux(["capture-pane", "-p", "-J", "-S", `-${lines}`, "-t", paneId]);
|
|
105
|
-
} catch (error) {
|
|
106
|
-
return "";
|
|
107
|
-
}
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
function getPaneChildProcesses(paneId) {
|
|
111
|
-
const info = getPaneInfo(paneId);
|
|
112
|
-
if (!info || !Number.isInteger(info.panePid)) {
|
|
113
|
-
return [];
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
try {
|
|
117
|
-
const output = execFileSync("ps", ["-axo", "pid=,ppid=,etimes=,command="], {
|
|
118
|
-
encoding: "utf8",
|
|
119
|
-
});
|
|
120
|
-
|
|
121
|
-
const processes = output
|
|
122
|
-
.split("\n")
|
|
123
|
-
.map((line) => line.trim())
|
|
124
|
-
.filter((line) => line.length > 0)
|
|
125
|
-
.map((line) => {
|
|
126
|
-
const match = line.match(/^(\d+)\s+(\d+)\s+(\d+)\s+(.*)$/);
|
|
127
|
-
if (!match) {
|
|
128
|
-
return null;
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
return {
|
|
132
|
-
pid: Number.parseInt(match[1], 10),
|
|
133
|
-
ppid: Number.parseInt(match[2], 10),
|
|
134
|
-
elapsedSeconds: Number.parseInt(match[3], 10),
|
|
135
|
-
args: match[4],
|
|
136
|
-
};
|
|
137
|
-
})
|
|
138
|
-
.filter((entry) => entry !== null);
|
|
139
|
-
|
|
140
|
-
const descendants = [];
|
|
141
|
-
const queue = [info.panePid];
|
|
142
|
-
const seen = new Set(queue);
|
|
143
|
-
|
|
144
|
-
while (queue.length > 0) {
|
|
145
|
-
const parentPid = queue.shift();
|
|
146
|
-
for (const process of processes) {
|
|
147
|
-
if (process.ppid !== parentPid || seen.has(process.pid)) {
|
|
148
|
-
continue;
|
|
149
|
-
}
|
|
150
|
-
seen.add(process.pid);
|
|
151
|
-
queue.push(process.pid);
|
|
152
|
-
descendants.push(process);
|
|
153
|
-
}
|
|
154
|
-
}
|
|
155
|
-
|
|
156
|
-
return descendants.sort((left, right) => left.elapsedSeconds - right.elapsedSeconds);
|
|
157
|
-
} catch (error) {
|
|
158
|
-
return [];
|
|
159
|
-
}
|
|
160
|
-
}
|
|
161
|
-
|
|
162
|
-
module.exports = {
|
|
163
|
-
capturePaneText,
|
|
164
|
-
getPaneChildProcesses,
|
|
165
|
-
getPaneInfo,
|
|
166
|
-
insideTmux,
|
|
167
|
-
paneExists,
|
|
168
|
-
sendTextAndEnter,
|
|
169
|
-
setPaneTitle,
|
|
170
|
-
};
|