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 +21 -0
- package/README.md +115 -3
- package/bin/muuse.js +9 -0
- package/package.json +37 -8
- package/src/agents.js +445 -0
- package/src/cli.js +193 -0
- package/src/runtime.js +683 -0
- package/src/tmux.js +170 -0
- package/src/util.js +313 -0
- package/index.js +0 -4
package/src/tmux.js
ADDED
|
@@ -0,0 +1,170 @@
|
|
|
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
|
+
};
|
package/src/util.js
ADDED
|
@@ -0,0 +1,313 @@
|
|
|
1
|
+
const { createHash, randomBytes } = require("node:crypto");
|
|
2
|
+
const fs = require("node:fs");
|
|
3
|
+
const os = require("node:os");
|
|
4
|
+
const path = require("node:path");
|
|
5
|
+
const { spawnSync } = require("node:child_process");
|
|
6
|
+
|
|
7
|
+
const BRAND = "🔌Muuuuse";
|
|
8
|
+
const POLL_MS = 900;
|
|
9
|
+
const CONTROLLER_WAIT_MS = 1000;
|
|
10
|
+
const SESSION_MATCH_WINDOW_MS = 5 * 60 * 1000;
|
|
11
|
+
const MAX_RELAY_CHARS = 4000;
|
|
12
|
+
|
|
13
|
+
const FLAG_ALIASES = new Map([
|
|
14
|
+
["--max-relays", "maxRelays"],
|
|
15
|
+
["--seed-seat", "seedSeat"],
|
|
16
|
+
["--step", "step"],
|
|
17
|
+
]);
|
|
18
|
+
|
|
19
|
+
const MULTI_FLAGS = new Set(["step"]);
|
|
20
|
+
|
|
21
|
+
function shellEscape(value) {
|
|
22
|
+
return `'${String(value).replace(/'/g, `'\\''`)}'`;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function createId(length = 10) {
|
|
26
|
+
return randomBytes(Math.ceil(length / 2)).toString("hex").slice(0, length);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function sleep(ms) {
|
|
30
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function ensureDir(dirPath) {
|
|
34
|
+
fs.mkdirSync(dirPath, { recursive: true });
|
|
35
|
+
return dirPath;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function resetDir(dirPath) {
|
|
39
|
+
fs.rmSync(dirPath, { recursive: true, force: true });
|
|
40
|
+
fs.mkdirSync(dirPath, { recursive: true });
|
|
41
|
+
return dirPath;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function writeJson(filePath, value) {
|
|
45
|
+
ensureDir(path.dirname(filePath));
|
|
46
|
+
const tempPath = `${filePath}.${process.pid}.${Date.now()}.tmp`;
|
|
47
|
+
fs.writeFileSync(tempPath, `${JSON.stringify(value, null, 2)}\n`);
|
|
48
|
+
fs.renameSync(tempPath, filePath);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function readJson(filePath, fallback = null) {
|
|
52
|
+
try {
|
|
53
|
+
return JSON.parse(fs.readFileSync(filePath, "utf8"));
|
|
54
|
+
} catch (error) {
|
|
55
|
+
return fallback;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function appendJsonl(filePath, value) {
|
|
60
|
+
ensureDir(path.dirname(filePath));
|
|
61
|
+
fs.appendFileSync(filePath, `${JSON.stringify(value)}\n`);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function readAppendedText(filePath, previousOffset = 0) {
|
|
65
|
+
try {
|
|
66
|
+
const stats = fs.statSync(filePath);
|
|
67
|
+
const startOffset = stats.size < previousOffset ? 0 : previousOffset;
|
|
68
|
+
if (stats.size === startOffset) {
|
|
69
|
+
return {
|
|
70
|
+
nextOffset: startOffset,
|
|
71
|
+
text: "",
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const fd = fs.openSync(filePath, "r");
|
|
76
|
+
try {
|
|
77
|
+
const byteLength = stats.size - startOffset;
|
|
78
|
+
const buffer = Buffer.alloc(byteLength);
|
|
79
|
+
const bytesRead = fs.readSync(fd, buffer, 0, byteLength, startOffset);
|
|
80
|
+
return {
|
|
81
|
+
nextOffset: startOffset + bytesRead,
|
|
82
|
+
text: buffer.toString("utf8", 0, bytesRead),
|
|
83
|
+
};
|
|
84
|
+
} finally {
|
|
85
|
+
fs.closeSync(fd);
|
|
86
|
+
}
|
|
87
|
+
} catch (error) {
|
|
88
|
+
if (error && error.code === "ENOENT") {
|
|
89
|
+
return {
|
|
90
|
+
nextOffset: 0,
|
|
91
|
+
text: "",
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
throw error;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function getFileSize(filePath) {
|
|
99
|
+
try {
|
|
100
|
+
return fs.statSync(filePath).size;
|
|
101
|
+
} catch (error) {
|
|
102
|
+
return 0;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function commandExists(command) {
|
|
107
|
+
const result = spawnSync("bash", ["-lc", `command -v ${shellEscape(command)} >/dev/null 2>&1`], {
|
|
108
|
+
encoding: "utf8",
|
|
109
|
+
});
|
|
110
|
+
return result.status === 0;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function readCommandVersion(command, args = ["--version"]) {
|
|
114
|
+
const result = spawnSync(command, args, {
|
|
115
|
+
encoding: "utf8",
|
|
116
|
+
timeout: 4000,
|
|
117
|
+
});
|
|
118
|
+
if (result.status !== 0) {
|
|
119
|
+
return null;
|
|
120
|
+
}
|
|
121
|
+
return (result.stdout || result.stderr || "").trim().split("\n")[0] || null;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function findFirstExisting(paths) {
|
|
125
|
+
return paths.find((candidate) => fs.existsSync(candidate)) || null;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function stripAnsi(text) {
|
|
129
|
+
return String(text || "").replace(
|
|
130
|
+
// eslint-disable-next-line no-control-regex
|
|
131
|
+
/\u001b\[[0-9;?]*[ -/]*[@-~]|\u001b[@-_]|\u009b[0-9;?]*[ -/]*[@-~]/g,
|
|
132
|
+
""
|
|
133
|
+
);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function sanitizeRelayText(input, maxChars = MAX_RELAY_CHARS) {
|
|
137
|
+
const normalized = stripAnsi(input)
|
|
138
|
+
.replace(/\r/g, "")
|
|
139
|
+
.replace(/\u0000/g, "")
|
|
140
|
+
.replace(/\n{3,}/g, "\n\n")
|
|
141
|
+
.trim();
|
|
142
|
+
|
|
143
|
+
if (normalized.length <= maxChars) {
|
|
144
|
+
return normalized;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
return `${normalized.slice(0, maxChars - 3).trimEnd()}...`;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
function toInt(value, fallback) {
|
|
151
|
+
const parsed = Number.parseInt(String(value), 10);
|
|
152
|
+
return Number.isFinite(parsed) ? parsed : fallback;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
function isPidAlive(pid) {
|
|
156
|
+
if (!Number.isInteger(pid) || pid <= 0) {
|
|
157
|
+
return false;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
try {
|
|
161
|
+
process.kill(pid, 0);
|
|
162
|
+
return true;
|
|
163
|
+
} catch (error) {
|
|
164
|
+
return false;
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
function slugifySegment(value) {
|
|
169
|
+
return String(value || "")
|
|
170
|
+
.trim()
|
|
171
|
+
.replace(/[^a-zA-Z0-9._-]+/g, "-")
|
|
172
|
+
.replace(/^-+|-+$/g, "") || "default";
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
function getStateRoot() {
|
|
176
|
+
return ensureDir(path.join(os.homedir(), ".muuuuse"));
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
function getSessionDir(sessionName) {
|
|
180
|
+
return ensureDir(path.join(getStateRoot(), "sessions", slugifySegment(sessionName)));
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
function getSeatDir(sessionName, seatId) {
|
|
184
|
+
return ensureDir(path.join(getSessionDir(sessionName), `seat-${seatId}`));
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
function getSeatPaths(sessionName, seatId) {
|
|
188
|
+
const dir = getSeatDir(sessionName, seatId);
|
|
189
|
+
return {
|
|
190
|
+
dir,
|
|
191
|
+
metaPath: path.join(dir, "meta.json"),
|
|
192
|
+
daemonPath: path.join(dir, "daemon.json"),
|
|
193
|
+
commandsPath: path.join(dir, "commands.jsonl"),
|
|
194
|
+
eventsPath: path.join(dir, "events.jsonl"),
|
|
195
|
+
scriptPath: path.join(dir, "script.json"),
|
|
196
|
+
statusPath: path.join(dir, "status.json"),
|
|
197
|
+
};
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
function getControllerPath(sessionName) {
|
|
201
|
+
return path.join(getSessionDir(sessionName), "controller.json");
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
function hashText(text) {
|
|
205
|
+
return createHash("sha1").update(String(text || "")).digest("hex");
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
function parseFlags(argv) {
|
|
209
|
+
const positionals = [];
|
|
210
|
+
const flags = {
|
|
211
|
+
step: [],
|
|
212
|
+
maxRelays: Number.POSITIVE_INFINITY,
|
|
213
|
+
seedSeat: 1,
|
|
214
|
+
};
|
|
215
|
+
|
|
216
|
+
for (let index = 0; index < argv.length; index += 1) {
|
|
217
|
+
const token = argv[index];
|
|
218
|
+
if (!token.startsWith("--")) {
|
|
219
|
+
positionals.push(token);
|
|
220
|
+
continue;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
const [rawFlag, inlineValue] = token.split("=", 2);
|
|
224
|
+
const key = FLAG_ALIASES.get(rawFlag);
|
|
225
|
+
if (!key) {
|
|
226
|
+
throw new Error(`Unknown flag: ${rawFlag}`);
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
const next = inlineValue !== undefined ? inlineValue : argv[index + 1];
|
|
230
|
+
if (inlineValue === undefined) {
|
|
231
|
+
index += 1;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
if (next === undefined) {
|
|
235
|
+
throw new Error(`Missing value for ${rawFlag}`);
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
if (MULTI_FLAGS.has(key)) {
|
|
239
|
+
flags[key].push(next);
|
|
240
|
+
continue;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
flags[key] = next;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
flags.maxRelays = flags.maxRelays === Number.POSITIVE_INFINITY
|
|
247
|
+
? Number.POSITIVE_INFINITY
|
|
248
|
+
: toInt(flags.maxRelays, Number.POSITIVE_INFINITY);
|
|
249
|
+
flags.seedSeat = toInt(flags.seedSeat, 1);
|
|
250
|
+
|
|
251
|
+
return {
|
|
252
|
+
positionals,
|
|
253
|
+
flags,
|
|
254
|
+
};
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
function usage() {
|
|
258
|
+
return [
|
|
259
|
+
`${BRAND} is the local-only 3-seat relay for Codex, Claude, Gemini, or deterministic scripts.`,
|
|
260
|
+
"",
|
|
261
|
+
"Usage:",
|
|
262
|
+
" muuuuse 1",
|
|
263
|
+
" muuuuse 2",
|
|
264
|
+
" muuuuse 3 [optional kickoff prompt]",
|
|
265
|
+
" muuuuse script [count] [--step <text>]",
|
|
266
|
+
" muuuuse live",
|
|
267
|
+
" muuuuse doctor",
|
|
268
|
+
"",
|
|
269
|
+
"Flow:",
|
|
270
|
+
" 1. Run `muuuuse 1` in the first tmux terminal.",
|
|
271
|
+
" 2. Run `muuuuse 2` in the second tmux terminal.",
|
|
272
|
+
" 3. Launch Codex, Claude, Gemini, or `muuuuse script` inside those armed seats.",
|
|
273
|
+
" 4. Run `muuuuse 3` in the control terminal to auto-pair the two seats.",
|
|
274
|
+
"",
|
|
275
|
+
"Notes:",
|
|
276
|
+
" - Visible brand: 🔌Muuuuse",
|
|
277
|
+
" - Remote routing belongs to Codeman / codemansbot, not this package.",
|
|
278
|
+
" - Optional kickoff: `muuuuse 3 \"Start by proposing the first concrete repo task.\"`",
|
|
279
|
+
" - Optional script loop: `muuuuse script 4` captures four prompts and cycles them forever.",
|
|
280
|
+
].join("\n");
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
module.exports = {
|
|
284
|
+
BRAND,
|
|
285
|
+
CONTROLLER_WAIT_MS,
|
|
286
|
+
MAX_RELAY_CHARS,
|
|
287
|
+
POLL_MS,
|
|
288
|
+
SESSION_MATCH_WINDOW_MS,
|
|
289
|
+
appendJsonl,
|
|
290
|
+
commandExists,
|
|
291
|
+
createId,
|
|
292
|
+
ensureDir,
|
|
293
|
+
findFirstExisting,
|
|
294
|
+
getControllerPath,
|
|
295
|
+
getFileSize,
|
|
296
|
+
getSeatDir,
|
|
297
|
+
getSeatPaths,
|
|
298
|
+
getSessionDir,
|
|
299
|
+
getStateRoot,
|
|
300
|
+
hashText,
|
|
301
|
+
isPidAlive,
|
|
302
|
+
parseFlags,
|
|
303
|
+
readAppendedText,
|
|
304
|
+
readCommandVersion,
|
|
305
|
+
readJson,
|
|
306
|
+
resetDir,
|
|
307
|
+
sanitizeRelayText,
|
|
308
|
+
shellEscape,
|
|
309
|
+
sleep,
|
|
310
|
+
toInt,
|
|
311
|
+
usage,
|
|
312
|
+
writeJson,
|
|
313
|
+
};
|
package/index.js
DELETED