muuuuse 3.3.1 → 3.3.3
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 +2 -2
- package/package.json +1 -1
- package/src/agents.js +23 -2
- package/src/runtime.js +149 -11
- package/src/util.js +12 -1
package/README.md
CHANGED
|
@@ -45,7 +45,7 @@ Terminal 2:
|
|
|
45
45
|
muuuuse 2 link 1 flow off link 3 flow on link 4 flow on
|
|
46
46
|
```
|
|
47
47
|
|
|
48
|
-
Now both shells are armed in the same cwd and join the same relay graph. Every seat has its own Ed25519 keypair. Each forwarded relay is signed by the sending seat. A target seat only accepts inbound relays
|
|
48
|
+
Now both shells are armed in the same cwd and join the same relay graph. Every seat has its own Ed25519 keypair. Each forwarded relay is signed by the sending seat. A target seat only accepts inbound relays when the sender linked to that target, so the graph can be open-ended without becoming an all-to-all broadcast.
|
|
49
49
|
|
|
50
50
|
`link <seat> flow on` means that outbound edge sends commentary and final answers. `link <seat> flow off` means that outbound edge sends final answers only. This is sender-side routing, not receiver-side filtering.
|
|
51
51
|
|
|
@@ -79,7 +79,7 @@ muuuuse stop
|
|
|
79
79
|
|
|
80
80
|
- state lives under `~/.muuuuse`
|
|
81
81
|
- all armed seats in the same cwd share one relay session graph
|
|
82
|
-
- only signed relays from
|
|
82
|
+
- only signed relays from senders that linked the target seat are accepted
|
|
83
83
|
- `continue <seat>` is a convenience alias for a single signed outbound link
|
|
84
84
|
- supported relay detection is built for Codex, Claude, and Gemini
|
|
85
85
|
|
package/package.json
CHANGED
package/src/agents.js
CHANGED
|
@@ -5,6 +5,7 @@ const path = require("node:path");
|
|
|
5
5
|
|
|
6
6
|
const {
|
|
7
7
|
getFileSize,
|
|
8
|
+
getSeatGeminiCliHome,
|
|
8
9
|
hashText,
|
|
9
10
|
readAppendedText,
|
|
10
11
|
sanitizeRelayText,
|
|
@@ -18,6 +19,23 @@ const GEMINI_ROOT = path.join(os.homedir(), ".gemini", "tmp");
|
|
|
18
19
|
const SESSION_START_EARLY_TOLERANCE_MS = 2 * 1000;
|
|
19
20
|
const STRICT_SINGLE_CANDIDATE_EARLY_TOLERANCE_MS = 250;
|
|
20
21
|
|
|
22
|
+
function uniquePaths(paths) {
|
|
23
|
+
return [...new Set(paths.map((entry) => path.resolve(entry)))];
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function getGeminiRoots(currentPath, options = {}) {
|
|
27
|
+
const roots = [GEMINI_ROOT];
|
|
28
|
+
const seatId = Number.parseInt(String(options.seatId || "").trim(), 10);
|
|
29
|
+
if (Number.isInteger(seatId) && seatId > 0) {
|
|
30
|
+
roots.unshift(path.join(
|
|
31
|
+
getSeatGeminiCliHome(os.homedir(), currentPath, seatId),
|
|
32
|
+
".gemini",
|
|
33
|
+
"tmp"
|
|
34
|
+
));
|
|
35
|
+
}
|
|
36
|
+
return uniquePaths(roots);
|
|
37
|
+
}
|
|
38
|
+
|
|
21
39
|
function walkFiles(rootPath, predicate, results = []) {
|
|
22
40
|
try {
|
|
23
41
|
const entries = fs.readdirSync(rootPath, { withFileTypes: true });
|
|
@@ -642,14 +660,17 @@ function readGeminiCandidate(filePath) {
|
|
|
642
660
|
|
|
643
661
|
function selectGeminiSessionFile(currentPath, processStartedAtMs, options = {}) {
|
|
644
662
|
const projectHash = createHash("sha256").update(currentPath).digest("hex");
|
|
645
|
-
const
|
|
663
|
+
const geminiRoots = getGeminiRoots(currentPath, options);
|
|
664
|
+
const liveCandidates = geminiRoots
|
|
665
|
+
.flatMap((rootPath) => readOpenSessionCandidates(options.pids ?? options.pid, rootPath, readGeminiCandidate))
|
|
646
666
|
.filter((candidate) => candidate.projectHash === projectHash);
|
|
647
667
|
const livePath = selectLiveSessionCandidatePath(liveCandidates, projectHash, options.captureSinceMs);
|
|
648
668
|
if (livePath) {
|
|
649
669
|
return livePath;
|
|
650
670
|
}
|
|
651
671
|
|
|
652
|
-
const candidates =
|
|
672
|
+
const candidates = geminiRoots
|
|
673
|
+
.flatMap((rootPath) => walkFiles(rootPath, (filePath) => filePath.endsWith(".json")))
|
|
653
674
|
.map((filePath) => readGeminiCandidate(filePath))
|
|
654
675
|
.filter((candidate) => candidate !== null && candidate.projectHash === projectHash);
|
|
655
676
|
|
package/src/runtime.js
CHANGED
|
@@ -20,6 +20,7 @@ const {
|
|
|
20
20
|
ensureDir,
|
|
21
21
|
getDefaultSessionName,
|
|
22
22
|
getFileSize,
|
|
23
|
+
getSeatGeminiCliHome,
|
|
23
24
|
getSeatPaths,
|
|
24
25
|
getSessionPaths,
|
|
25
26
|
getStateRoot,
|
|
@@ -41,6 +42,14 @@ const {
|
|
|
41
42
|
// A short settle delay keeps interactive CLIs from treating submit as another newline.
|
|
42
43
|
const TYPE_CHUNK_DELAY_MS = 45;
|
|
43
44
|
const TYPE_CHUNK_SIZE = 24;
|
|
45
|
+
const BRACKETED_PASTE_START = "\u001b[200~";
|
|
46
|
+
const BRACKETED_PASTE_END = "\u001b[201~";
|
|
47
|
+
const GEMINI_SHARED_ENTRY_NAMES = new Set([
|
|
48
|
+
"gemini-credentials.json",
|
|
49
|
+
"google_accounts.json",
|
|
50
|
+
"installation_id",
|
|
51
|
+
"mcp-oauth-tokens-v2.json",
|
|
52
|
+
]);
|
|
44
53
|
const MIRROR_SUPPRESSION_WINDOW_MS = 30 * 1000;
|
|
45
54
|
const PENDING_RELAY_CONTEXT_TTL_MS = 2 * 60 * 1000;
|
|
46
55
|
const EMITTED_ANSWER_TTL_MS = 5 * 60 * 1000;
|
|
@@ -144,6 +153,78 @@ function sanitizeChildPath(pathValue, homeDir) {
|
|
|
144
153
|
return entries.join(path.delimiter);
|
|
145
154
|
}
|
|
146
155
|
|
|
156
|
+
function readGeminiApiKeyFromHome(homeDir) {
|
|
157
|
+
const filePath = path.join(homeDir, "gemini.txt");
|
|
158
|
+
try {
|
|
159
|
+
const value = fs.readFileSync(filePath, "utf8").trim();
|
|
160
|
+
return value || null;
|
|
161
|
+
} catch {
|
|
162
|
+
return null;
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
function syncGeminiCliHomeEntry(sourcePath, targetPath) {
|
|
167
|
+
const shouldLink = GEMINI_SHARED_ENTRY_NAMES.has(path.basename(sourcePath));
|
|
168
|
+
try {
|
|
169
|
+
if (fs.lstatSync(targetPath).isSymbolicLink() && fs.realpathSync(targetPath) === sourcePath) {
|
|
170
|
+
return;
|
|
171
|
+
}
|
|
172
|
+
} catch {
|
|
173
|
+
// Recreate the target entry below when missing or mismatched.
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
fs.rmSync(targetPath, { recursive: true, force: true });
|
|
177
|
+
|
|
178
|
+
const sourceStats = fs.lstatSync(sourcePath);
|
|
179
|
+
if (shouldLink) {
|
|
180
|
+
try {
|
|
181
|
+
const linkType = process.platform === "win32"
|
|
182
|
+
? (sourceStats.isDirectory() ? "junction" : "file")
|
|
183
|
+
: undefined;
|
|
184
|
+
fs.symlinkSync(sourcePath, targetPath, linkType);
|
|
185
|
+
return;
|
|
186
|
+
} catch {
|
|
187
|
+
// Fall through to copying when symlinks are unavailable.
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
if (sourceStats.isDirectory()) {
|
|
192
|
+
fs.cpSync(sourcePath, targetPath, { recursive: true });
|
|
193
|
+
return;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
fs.copyFileSync(sourcePath, targetPath);
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
function ensureSeatGeminiCliHome(homeDir, cwd, seatId, baseEnv = process.env) {
|
|
200
|
+
const sourceHomeRoot = String(baseEnv.GEMINI_CLI_HOME || homeDir).trim() || homeDir;
|
|
201
|
+
const sourceGeminiDir = path.join(sourceHomeRoot, ".gemini");
|
|
202
|
+
const targetHomeRoot = getSeatGeminiCliHome(homeDir, cwd, seatId);
|
|
203
|
+
const targetGeminiDir = ensureDir(path.join(targetHomeRoot, ".gemini"));
|
|
204
|
+
|
|
205
|
+
let sourceEntries = [];
|
|
206
|
+
try {
|
|
207
|
+
sourceEntries = fs.readdirSync(sourceGeminiDir, { withFileTypes: true });
|
|
208
|
+
} catch {
|
|
209
|
+
ensureDir(path.join(targetGeminiDir, "tmp"));
|
|
210
|
+
return targetHomeRoot;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
for (const entry of sourceEntries) {
|
|
214
|
+
if (entry.name === "tmp") {
|
|
215
|
+
continue;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
syncGeminiCliHomeEntry(
|
|
219
|
+
path.join(sourceGeminiDir, entry.name),
|
|
220
|
+
path.join(targetGeminiDir, entry.name)
|
|
221
|
+
);
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
ensureDir(path.join(targetGeminiDir, "tmp"));
|
|
225
|
+
return targetHomeRoot;
|
|
226
|
+
}
|
|
227
|
+
|
|
147
228
|
function buildChildEnv(seatId, sessionName, cwd, baseEnv = process.env) {
|
|
148
229
|
const env = { ...baseEnv };
|
|
149
230
|
for (const key of CHILD_ENV_DROP_KEYS) {
|
|
@@ -156,6 +237,13 @@ function buildChildEnv(seatId, sessionName, cwd, baseEnv = process.env) {
|
|
|
156
237
|
env.TERM = resolveChildTerm(baseEnv);
|
|
157
238
|
env.MUUUUSE_SEAT = String(seatId);
|
|
158
239
|
env.MUUUUSE_SESSION = sessionName;
|
|
240
|
+
if (!String(env.GEMINI_API_KEY || "").trim()) {
|
|
241
|
+
const homeGeminiApiKey = readGeminiApiKeyFromHome(homeDir);
|
|
242
|
+
if (homeGeminiApiKey) {
|
|
243
|
+
env.GEMINI_API_KEY = homeGeminiApiKey;
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
env.GEMINI_CLI_HOME = getSeatGeminiCliHome(homeDir, cwd, seatId);
|
|
159
247
|
return env;
|
|
160
248
|
}
|
|
161
249
|
|
|
@@ -566,24 +654,39 @@ function isMeaningfulTerminalInput(input) {
|
|
|
566
654
|
}
|
|
567
655
|
|
|
568
656
|
async function sendTextAndEnter(child, text, shouldAbort = () => false) {
|
|
657
|
+
const options = typeof shouldAbort === "function" ? { shouldAbort } : (shouldAbort || {});
|
|
658
|
+
const shouldStop = typeof options.shouldAbort === "function" ? options.shouldAbort : () => false;
|
|
659
|
+
const agentType = String(options.agentType || "").trim().toLowerCase() || null;
|
|
569
660
|
const payload = normalizeRelayPayloadForTyping(text);
|
|
570
661
|
|
|
571
662
|
if (payload.length > 0) {
|
|
572
|
-
|
|
573
|
-
if (
|
|
663
|
+
if (agentType === "codex") {
|
|
664
|
+
if (shouldStop() || !child) {
|
|
574
665
|
return false;
|
|
575
666
|
}
|
|
576
667
|
|
|
577
668
|
try {
|
|
578
|
-
child.write(
|
|
669
|
+
child.write(`${BRACKETED_PASTE_START}${payload}${BRACKETED_PASTE_END}`);
|
|
579
670
|
} catch {
|
|
580
671
|
return false;
|
|
581
672
|
}
|
|
582
|
-
|
|
673
|
+
} else {
|
|
674
|
+
for (const chunk of chunkRelayPayloadForTyping(payload)) {
|
|
675
|
+
if (shouldStop() || !child) {
|
|
676
|
+
return false;
|
|
677
|
+
}
|
|
678
|
+
|
|
679
|
+
try {
|
|
680
|
+
child.write(chunk);
|
|
681
|
+
} catch {
|
|
682
|
+
return false;
|
|
683
|
+
}
|
|
684
|
+
await sleep(TYPE_CHUNK_DELAY_MS);
|
|
685
|
+
}
|
|
583
686
|
}
|
|
584
687
|
}
|
|
585
688
|
|
|
586
|
-
if (
|
|
689
|
+
if (shouldStop() || !child) {
|
|
587
690
|
return false;
|
|
588
691
|
}
|
|
589
692
|
|
|
@@ -702,6 +805,12 @@ class ArmedSeat {
|
|
|
702
805
|
const shell = resolveShell();
|
|
703
806
|
const shellArgs = resolveShellArgs(shell);
|
|
704
807
|
const childEnv = buildChildEnv(this.seatId, this.sessionName, this.cwd);
|
|
808
|
+
ensureSeatGeminiCliHome(
|
|
809
|
+
String(childEnv.HOME || "").trim() || process.env.HOME || "/root",
|
|
810
|
+
this.cwd,
|
|
811
|
+
this.seatId,
|
|
812
|
+
process.env
|
|
813
|
+
);
|
|
705
814
|
this.child = pty.spawn(shell, shellArgs, {
|
|
706
815
|
cols: process.stdout.columns || 120,
|
|
707
816
|
rows: process.stdout.rows || 36,
|
|
@@ -872,13 +981,33 @@ class ArmedSeat {
|
|
|
872
981
|
return Number.isFinite(requestedAtMs) && requestedAtMs > this.startedAtMs;
|
|
873
982
|
}
|
|
874
983
|
|
|
875
|
-
|
|
984
|
+
sourceLinksToTarget(sourceSeatId, targetSeatId = this.seatId) {
|
|
876
985
|
const desiredSeatId = normalizeSeatId(sourceSeatId);
|
|
877
|
-
|
|
986
|
+
const desiredTargetSeatId = normalizeSeatId(targetSeatId);
|
|
987
|
+
if (!desiredSeatId || !desiredTargetSeatId) {
|
|
878
988
|
return false;
|
|
879
989
|
}
|
|
880
990
|
|
|
881
|
-
|
|
991
|
+
const sourcePaths = getSeatPaths(this.sessionName, desiredSeatId);
|
|
992
|
+
const sourceStatus = readJson(sourcePaths.statusPath, null);
|
|
993
|
+
const sourceMeta = readJson(sourcePaths.metaPath, null);
|
|
994
|
+
const sourceContinueSeatId = sourceStatus?.continueSeatId || sourceMeta?.continueSeatId || null;
|
|
995
|
+
const sourceContinueTargets = normalizeContinueTargets(
|
|
996
|
+
sourceStatus?.continueTargets || sourceMeta?.continueTargets
|
|
997
|
+
);
|
|
998
|
+
|
|
999
|
+
const configuredTargets = [...sourceContinueTargets];
|
|
1000
|
+
if (
|
|
1001
|
+
sourceContinueSeatId &&
|
|
1002
|
+
!configuredTargets.some((target) => target.seatId === normalizeSeatId(sourceContinueSeatId))
|
|
1003
|
+
) {
|
|
1004
|
+
configuredTargets.push({
|
|
1005
|
+
seatId: normalizeSeatId(sourceContinueSeatId),
|
|
1006
|
+
flowMode: normalizeFlowMode(sourceStatus?.flowMode || sourceMeta?.flowMode),
|
|
1007
|
+
});
|
|
1008
|
+
}
|
|
1009
|
+
|
|
1010
|
+
return configuredTargets.some((target) => target.seatId === desiredTargetSeatId);
|
|
882
1011
|
}
|
|
883
1012
|
|
|
884
1013
|
readSourcePublicKey(sourceSeatId) {
|
|
@@ -922,7 +1051,7 @@ class ArmedSeat {
|
|
|
922
1051
|
const sourceSeatId = normalizeSeatId(entry?.sourceSeatId || entry?.seatId);
|
|
923
1052
|
const targetSeatId = normalizeSeatId(entry?.targetSeatId);
|
|
924
1053
|
const payload = sanitizeRelayText(entry?.text);
|
|
925
|
-
if (!sourceSeatId || targetSeatId !== this.seatId || !payload || !this.
|
|
1054
|
+
if (!sourceSeatId || targetSeatId !== this.seatId || !payload || !this.sourceLinksToTarget(sourceSeatId, targetSeatId)) {
|
|
926
1055
|
return false;
|
|
927
1056
|
}
|
|
928
1057
|
|
|
@@ -955,6 +1084,10 @@ class ArmedSeat {
|
|
|
955
1084
|
return;
|
|
956
1085
|
}
|
|
957
1086
|
|
|
1087
|
+
const detectedRelayAgent = this.liveState.type
|
|
1088
|
+
? { type: this.liveState.type }
|
|
1089
|
+
: detectAgent(getChildProcesses(this.childPid));
|
|
1090
|
+
|
|
958
1091
|
const entries = parseContinueEntries(text, this.seatId);
|
|
959
1092
|
for (const entry of entries) {
|
|
960
1093
|
if (this.stopped || this.stopRequested()) {
|
|
@@ -974,7 +1107,10 @@ class ArmedSeat {
|
|
|
974
1107
|
const delivered = await sendTextAndEnter(
|
|
975
1108
|
this.child,
|
|
976
1109
|
payload,
|
|
977
|
-
|
|
1110
|
+
{
|
|
1111
|
+
agentType: detectedRelayAgent?.type || null,
|
|
1112
|
+
shouldAbort: () => this.stopped || this.stopRequested() || !this.child || Boolean(this.childExit),
|
|
1113
|
+
}
|
|
978
1114
|
);
|
|
979
1115
|
if (!delivered) {
|
|
980
1116
|
this.requestStop("relay_aborted");
|
|
@@ -1348,7 +1484,7 @@ class ArmedSeat {
|
|
|
1348
1484
|
`Seat ${this.seatId} links signed relay targets: ${configuredTargets.map((target) => `${target.seatId}:${target.flowMode}`).join(", ")}.`
|
|
1349
1485
|
);
|
|
1350
1486
|
}
|
|
1351
|
-
this.log("Signed relays are accepted
|
|
1487
|
+
this.log("Signed relays are accepted when the sender linked to this seat.");
|
|
1352
1488
|
this.log("Run `muuuuse status` or `muuuuse stop` from any terminal.");
|
|
1353
1489
|
|
|
1354
1490
|
try {
|
|
@@ -1531,12 +1667,14 @@ function stopAllSessions() {
|
|
|
1531
1667
|
module.exports = {
|
|
1532
1668
|
ArmedSeat,
|
|
1533
1669
|
buildChildEnv,
|
|
1670
|
+
ensureSeatGeminiCliHome,
|
|
1534
1671
|
chunkRelayPayloadForTyping,
|
|
1535
1672
|
getStatusReport,
|
|
1536
1673
|
isBareEscapeInput,
|
|
1537
1674
|
isMeaningfulTerminalInput,
|
|
1538
1675
|
normalizeRelayPayloadForTyping,
|
|
1539
1676
|
resolveSessionName,
|
|
1677
|
+
sendTextAndEnter,
|
|
1540
1678
|
stopAllSessions,
|
|
1541
1679
|
};
|
|
1542
1680
|
|
package/src/util.js
CHANGED
|
@@ -154,6 +154,16 @@ function getDefaultSessionName(currentPath = process.cwd()) {
|
|
|
154
154
|
return `${label}-${hashText(resolvedPath).slice(0, 8)}`;
|
|
155
155
|
}
|
|
156
156
|
|
|
157
|
+
function getSeatGeminiCliHome(homeDir = os.homedir(), currentPath = process.cwd(), seatId) {
|
|
158
|
+
return path.join(
|
|
159
|
+
homeDir,
|
|
160
|
+
".muuuuse",
|
|
161
|
+
"gemini-cli-homes",
|
|
162
|
+
getDefaultSessionName(currentPath),
|
|
163
|
+
`seat-${seatId}`
|
|
164
|
+
);
|
|
165
|
+
}
|
|
166
|
+
|
|
157
167
|
function getSessionDir(sessionName) {
|
|
158
168
|
return ensureDir(path.join(getStateRoot(), "sessions", slugifySegment(sessionName)));
|
|
159
169
|
}
|
|
@@ -296,7 +306,7 @@ function usage() {
|
|
|
296
306
|
" 4. `flow on` sends commentary and final answers on that edge. `flow off` sends final answers only.",
|
|
297
307
|
" 5. `continue <seat>` is shorthand for one outbound link that uses the seat's default `flow on|off`.",
|
|
298
308
|
" 6. Every forwarded relay is signed with the sender seat's key.",
|
|
299
|
-
" 7. A seat only accepts signed inbound relays
|
|
309
|
+
" 7. A seat only accepts signed inbound relays when the sender linked to that seat.",
|
|
300
310
|
" 8. Use those armed shells normally.",
|
|
301
311
|
" 9. Run `muuuuse status` or `muuuuse stop` from any shell.",
|
|
302
312
|
"",
|
|
@@ -315,6 +325,7 @@ module.exports = {
|
|
|
315
325
|
ensureDir,
|
|
316
326
|
getDefaultSessionName,
|
|
317
327
|
getFileSize,
|
|
328
|
+
getSeatGeminiCliHome,
|
|
318
329
|
loadOrCreateSeatIdentity,
|
|
319
330
|
getSeatPaths,
|
|
320
331
|
getSessionPaths,
|