muuuuse 3.3.2 → 3.3.4
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/package.json +1 -1
- package/src/agents.js +23 -2
- package/src/runtime.js +128 -7
- package/src/util.js +11 -0
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,
|
|
@@ -975,6 +1084,10 @@ class ArmedSeat {
|
|
|
975
1084
|
return;
|
|
976
1085
|
}
|
|
977
1086
|
|
|
1087
|
+
const detectedRelayAgent = this.liveState.type
|
|
1088
|
+
? { type: this.liveState.type }
|
|
1089
|
+
: detectAgent(getChildProcesses(this.childPid));
|
|
1090
|
+
|
|
978
1091
|
const entries = parseContinueEntries(text, this.seatId);
|
|
979
1092
|
for (const entry of entries) {
|
|
980
1093
|
if (this.stopped || this.stopRequested()) {
|
|
@@ -994,7 +1107,10 @@ class ArmedSeat {
|
|
|
994
1107
|
const delivered = await sendTextAndEnter(
|
|
995
1108
|
this.child,
|
|
996
1109
|
payload,
|
|
997
|
-
|
|
1110
|
+
{
|
|
1111
|
+
agentType: detectedRelayAgent?.type || null,
|
|
1112
|
+
shouldAbort: () => this.stopped || this.stopRequested() || !this.child || Boolean(this.childExit),
|
|
1113
|
+
}
|
|
998
1114
|
);
|
|
999
1115
|
if (!delivered) {
|
|
1000
1116
|
this.requestStop("relay_aborted");
|
|
@@ -1179,7 +1295,10 @@ class ArmedSeat {
|
|
|
1179
1295
|
this.liveState.lastMessageId = null;
|
|
1180
1296
|
const sessionStartedAtMs = readSessionFileStartedAtMs(detectedAgent.type, resolvedSessionFile);
|
|
1181
1297
|
if (Number.isFinite(sessionStartedAtMs)) {
|
|
1182
|
-
this.liveState.captureSinceMs = Math.
|
|
1298
|
+
this.liveState.captureSinceMs = Math.max(
|
|
1299
|
+
this.startedAtMs,
|
|
1300
|
+
Math.min(this.liveState.captureSinceMs, sessionStartedAtMs)
|
|
1301
|
+
);
|
|
1183
1302
|
}
|
|
1184
1303
|
}
|
|
1185
1304
|
|
|
@@ -1551,12 +1670,14 @@ function stopAllSessions() {
|
|
|
1551
1670
|
module.exports = {
|
|
1552
1671
|
ArmedSeat,
|
|
1553
1672
|
buildChildEnv,
|
|
1673
|
+
ensureSeatGeminiCliHome,
|
|
1554
1674
|
chunkRelayPayloadForTyping,
|
|
1555
1675
|
getStatusReport,
|
|
1556
1676
|
isBareEscapeInput,
|
|
1557
1677
|
isMeaningfulTerminalInput,
|
|
1558
1678
|
normalizeRelayPayloadForTyping,
|
|
1559
1679
|
resolveSessionName,
|
|
1680
|
+
sendTextAndEnter,
|
|
1560
1681
|
stopAllSessions,
|
|
1561
1682
|
};
|
|
1562
1683
|
|
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
|
}
|
|
@@ -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,
|