@juspay/shooter 1.15.0 → 1.17.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/.claude/hooks/codex-hooks.example.json +75 -0
- package/.claude/hooks/notifier.cjs +158 -8
- package/build/client/_app/immutable/assets/{0.BZLcOr5z.css → 0.B0O0vCnX.css} +1 -1
- package/build/client/_app/immutable/assets/0.B0O0vCnX.css.br +0 -0
- package/build/client/_app/immutable/assets/0.B0O0vCnX.css.gz +0 -0
- package/build/client/_app/immutable/chunks/{X-tVU_3P.js → BctvtE4d.js} +1 -1
- package/build/client/_app/immutable/chunks/BctvtE4d.js.br +0 -0
- package/build/client/_app/immutable/chunks/BctvtE4d.js.gz +0 -0
- package/build/client/_app/immutable/chunks/BxFShcQO.js +1 -0
- package/build/client/_app/immutable/chunks/BxFShcQO.js.br +0 -0
- package/build/client/_app/immutable/chunks/BxFShcQO.js.gz +0 -0
- package/build/client/_app/immutable/chunks/{gxvWeAns.js → ByzqAuXw.js} +1 -1
- package/build/client/_app/immutable/chunks/ByzqAuXw.js.br +0 -0
- package/build/client/_app/immutable/chunks/ByzqAuXw.js.gz +0 -0
- package/build/client/_app/immutable/chunks/{pMo6RVvN.js → CjfxuHdN.js} +1 -1
- package/build/client/_app/immutable/chunks/CjfxuHdN.js.br +0 -0
- package/build/client/_app/immutable/chunks/CjfxuHdN.js.gz +0 -0
- package/build/client/_app/immutable/entry/{app.B0PrrcUG.js → app.CNaTe-zm.js} +2 -2
- package/build/client/_app/immutable/entry/app.CNaTe-zm.js.br +0 -0
- package/build/client/_app/immutable/entry/app.CNaTe-zm.js.gz +0 -0
- package/build/client/_app/immutable/entry/start.hxYnjcDu.js +1 -0
- package/build/client/_app/immutable/entry/start.hxYnjcDu.js.br +0 -0
- package/build/client/_app/immutable/entry/start.hxYnjcDu.js.gz +0 -0
- package/build/client/_app/immutable/nodes/{0.D4GLHqPM.js → 0.C3ELOf4c.js} +1 -1
- package/build/client/_app/immutable/nodes/0.C3ELOf4c.js.br +0 -0
- package/build/client/_app/immutable/nodes/0.C3ELOf4c.js.gz +0 -0
- package/build/client/_app/immutable/nodes/{1.nJde5z5O.js → 1.Fqso94b3.js} +1 -1
- package/build/client/_app/immutable/nodes/1.Fqso94b3.js.br +0 -0
- package/build/client/_app/immutable/nodes/1.Fqso94b3.js.gz +0 -0
- package/build/client/_app/immutable/nodes/{2.CLtsjLeG.js → 2.BusCVJWk.js} +1 -1
- package/build/client/_app/immutable/nodes/2.BusCVJWk.js.br +0 -0
- package/build/client/_app/immutable/nodes/2.BusCVJWk.js.gz +0 -0
- package/build/client/_app/immutable/nodes/{3.CKTUHtnx.js → 3.DUlpocIc.js} +1 -1
- package/build/client/_app/immutable/nodes/3.DUlpocIc.js.br +0 -0
- package/build/client/_app/immutable/nodes/3.DUlpocIc.js.gz +0 -0
- package/build/client/_app/immutable/nodes/6.CG4eKRH0.js +1 -0
- package/build/client/_app/immutable/nodes/6.CG4eKRH0.js.br +0 -0
- package/build/client/_app/immutable/nodes/6.CG4eKRH0.js.gz +0 -0
- package/build/client/_app/immutable/nodes/7.DHilxD1o.js +4 -0
- package/build/client/_app/immutable/nodes/7.DHilxD1o.js.br +0 -0
- package/build/client/_app/immutable/nodes/7.DHilxD1o.js.gz +0 -0
- package/build/client/_app/immutable/nodes/8.BjKgvSie.js +2 -0
- package/build/client/_app/immutable/nodes/8.BjKgvSie.js.br +0 -0
- package/build/client/_app/immutable/nodes/8.BjKgvSie.js.gz +0 -0
- package/build/client/_app/immutable/nodes/9.BRT6HOXB.js +2 -0
- package/build/client/_app/immutable/nodes/9.BRT6HOXB.js.br +0 -0
- package/build/client/_app/immutable/nodes/9.BRT6HOXB.js.gz +0 -0
- package/build/client/_app/version.json +1 -1
- package/build/client/_app/version.json.br +0 -0
- package/build/client/_app/version.json.gz +0 -0
- package/build/server/chunks/{0-BUSWGJr9.js → 0-BWFSL107.js} +3 -3
- package/build/server/chunks/{0-BUSWGJr9.js.map → 0-BWFSL107.js.map} +1 -1
- package/build/server/chunks/{1-DjiQE1K0.js → 1-Bw5KlAjL.js} +2 -2
- package/build/server/chunks/{1-DjiQE1K0.js.map → 1-Bw5KlAjL.js.map} +1 -1
- package/build/server/chunks/{2-ThgVrRKa.js → 2-CQ3yYSVK.js} +2 -2
- package/build/server/chunks/{2-ThgVrRKa.js.map → 2-CQ3yYSVK.js.map} +1 -1
- package/build/server/chunks/{3-G5LiDFQ9.js → 3-DZ4H9hPs.js} +2 -2
- package/build/server/chunks/{3-G5LiDFQ9.js.map → 3-DZ4H9hPs.js.map} +1 -1
- package/build/server/chunks/{6--I7fF3Bx.js → 6-BZ0enR6b.js} +2 -2
- package/build/server/chunks/6-BZ0enR6b.js.map +1 -0
- package/build/server/chunks/{7-BwPLVwOR.js → 7-Lg8imTZn.js} +2 -2
- package/build/server/chunks/7-Lg8imTZn.js.map +1 -0
- package/build/server/chunks/{8-BwOMHaoQ.js → 8-DKs4yOL7.js} +2 -2
- package/build/server/chunks/8-DKs4yOL7.js.map +1 -0
- package/build/server/chunks/{9-DkO6aJIB.js → 9-UNmpUWDY.js} +2 -2
- package/build/server/chunks/9-UNmpUWDY.js.map +1 -0
- package/build/server/chunks/{_server.ts-BuYyCrnF.js → _server.ts-5wx4ZppI.js} +4 -3
- package/build/server/chunks/_server.ts-5wx4ZppI.js.map +1 -0
- package/build/server/chunks/{_server.ts-40c_epk8.js → _server.ts-B1z0q6qZ.js} +10 -8
- package/build/server/chunks/_server.ts-B1z0q6qZ.js.map +1 -0
- package/build/server/chunks/{_server.ts-ByPExYfO.js → _server.ts-BLNDdFWC.js} +3 -3
- package/build/server/chunks/_server.ts-BLNDdFWC.js.map +1 -0
- package/build/server/chunks/_server.ts-BMMTS86y.js +82 -0
- package/build/server/chunks/_server.ts-BMMTS86y.js.map +1 -0
- package/build/server/chunks/{_server.ts-CjpQ10xh.js → _server.ts-Bt7EAfjo.js} +50 -2
- package/build/server/chunks/_server.ts-Bt7EAfjo.js.map +1 -0
- package/build/server/chunks/{_server.ts-0Xr2fWaq.js → _server.ts-CKXVBbwb.js} +18 -8
- package/build/server/chunks/_server.ts-CKXVBbwb.js.map +1 -0
- package/build/server/chunks/{_server.ts-2ixC-X3K.js → _server.ts-CgHc1Zpx.js} +4 -3
- package/build/server/chunks/_server.ts-CgHc1Zpx.js.map +1 -0
- package/build/server/chunks/{_server.ts-CFX-S_8q.js → _server.ts-DZ5naqSL.js} +2 -2
- package/build/server/chunks/{_server.ts-CFX-S_8q.js.map → _server.ts-DZ5naqSL.js.map} +1 -1
- package/build/server/chunks/opencode-db-path-BwaPufWf.js +411 -0
- package/build/server/chunks/opencode-db-path-BwaPufWf.js.map +1 -0
- package/build/server/chunks/{pty-manager-TyMUpDA9.js → pty-manager-RmhVe2Ez.js} +35 -2
- package/build/server/chunks/pty-manager-RmhVe2Ez.js.map +1 -0
- package/build/server/chunks/qwen-reader-2fTFuC_D.js +622 -0
- package/build/server/chunks/qwen-reader-2fTFuC_D.js.map +1 -0
- package/build/server/chunks/{_server.ts-CilRds58.js → registry-DzJj2E6I.js} +95 -92
- package/build/server/chunks/registry-DzJj2E6I.js.map +1 -0
- package/build/server/index.js +1 -1
- package/build/server/index.js.map +1 -1
- package/build/server/manifest.js +17 -17
- package/build/server/manifest.js.map +1 -1
- package/package.json +2 -2
- package/server.ts +12 -0
- package/src/lib/modules/client/common/index.ts +1 -0
- package/src/lib/modules/client/common/provider.ts +43 -0
- package/src/lib/modules/client/terminal/LaunchSheet.svelte +3 -0
- package/src/lib/modules/server/sessions/codex-parser.ts +286 -0
- package/src/lib/modules/server/sessions/codex-reader.ts +294 -0
- package/src/lib/modules/server/sessions/gemini-reader.ts +571 -0
- package/src/lib/modules/server/sessions/opencode-db-path.ts +19 -10
- package/src/lib/modules/server/sessions/process-detector.ts +67 -0
- package/src/lib/modules/server/sessions/qwen-reader.ts +310 -0
- package/src/lib/modules/server/sessions/registry.ts +137 -0
- package/src/lib/modules/server/terminal/codex-watcher.ts +182 -0
- package/src/lib/modules/server/terminal/pty-manager.ts +41 -0
- package/src/lib/modules/server/ws/session-handler.ts +23 -19
- package/src/lib/theme.css +54 -1
- package/src/lib/types/codex.ts +21 -0
- package/src/lib/types/gemini.ts +100 -0
- package/src/lib/types/generated/Sessions.ts +24 -1
- package/src/lib/types/index.ts +2 -0
- package/src/lib/types/server.ts +18 -5
- package/src/lib/types/sessions.ts +23 -2
- package/src/routes/api/device-token/+server.ts +7 -3
- package/src/routes/api/sessions/+server.ts +5 -40
- package/src/routes/api/sessions/connect/+server.ts +22 -11
- package/src/routes/api/terminals/+server.ts +7 -5
- package/src/routes/project/+page.svelte +7 -23
- package/src/routes/session/[id]/+page.svelte +3 -3
- package/src/routes/terminals/+page.svelte +7 -2
- package/src/routes/terminals/[id]/+page.svelte +1 -2
- package/build/client/_app/immutable/assets/0.BZLcOr5z.css.br +0 -0
- package/build/client/_app/immutable/assets/0.BZLcOr5z.css.gz +0 -0
- package/build/client/_app/immutable/chunks/X-tVU_3P.js.br +0 -0
- package/build/client/_app/immutable/chunks/X-tVU_3P.js.gz +0 -0
- package/build/client/_app/immutable/chunks/gxvWeAns.js.br +0 -0
- package/build/client/_app/immutable/chunks/gxvWeAns.js.gz +0 -0
- package/build/client/_app/immutable/chunks/pMo6RVvN.js.br +0 -0
- package/build/client/_app/immutable/chunks/pMo6RVvN.js.gz +0 -0
- package/build/client/_app/immutable/entry/app.B0PrrcUG.js.br +0 -0
- package/build/client/_app/immutable/entry/app.B0PrrcUG.js.gz +0 -0
- package/build/client/_app/immutable/entry/start.B1obDjVk.js +0 -1
- package/build/client/_app/immutable/entry/start.B1obDjVk.js.br +0 -0
- package/build/client/_app/immutable/entry/start.B1obDjVk.js.gz +0 -0
- package/build/client/_app/immutable/nodes/0.D4GLHqPM.js.br +0 -0
- package/build/client/_app/immutable/nodes/0.D4GLHqPM.js.gz +0 -0
- package/build/client/_app/immutable/nodes/1.nJde5z5O.js.br +0 -0
- package/build/client/_app/immutable/nodes/1.nJde5z5O.js.gz +0 -0
- package/build/client/_app/immutable/nodes/2.CLtsjLeG.js.br +0 -0
- package/build/client/_app/immutable/nodes/2.CLtsjLeG.js.gz +0 -0
- package/build/client/_app/immutable/nodes/3.CKTUHtnx.js.br +0 -0
- package/build/client/_app/immutable/nodes/3.CKTUHtnx.js.gz +0 -0
- package/build/client/_app/immutable/nodes/6.Cgc5iTlM.js +0 -1
- package/build/client/_app/immutable/nodes/6.Cgc5iTlM.js.br +0 -0
- package/build/client/_app/immutable/nodes/6.Cgc5iTlM.js.gz +0 -0
- package/build/client/_app/immutable/nodes/7.BXKvUopV.js +0 -4
- package/build/client/_app/immutable/nodes/7.BXKvUopV.js.br +0 -0
- package/build/client/_app/immutable/nodes/7.BXKvUopV.js.gz +0 -0
- package/build/client/_app/immutable/nodes/8.Df0leW0d.js +0 -2
- package/build/client/_app/immutable/nodes/8.Df0leW0d.js.br +0 -0
- package/build/client/_app/immutable/nodes/8.Df0leW0d.js.gz +0 -0
- package/build/client/_app/immutable/nodes/9.C4-N3geF.js +0 -2
- package/build/client/_app/immutable/nodes/9.C4-N3geF.js.br +0 -0
- package/build/client/_app/immutable/nodes/9.C4-N3geF.js.gz +0 -0
- package/build/server/chunks/6--I7fF3Bx.js.map +0 -1
- package/build/server/chunks/7-BwPLVwOR.js.map +0 -1
- package/build/server/chunks/8-BwOMHaoQ.js.map +0 -1
- package/build/server/chunks/9-DkO6aJIB.js.map +0 -1
- package/build/server/chunks/_server.ts-0Xr2fWaq.js.map +0 -1
- package/build/server/chunks/_server.ts-2ixC-X3K.js.map +0 -1
- package/build/server/chunks/_server.ts-40c_epk8.js.map +0 -1
- package/build/server/chunks/_server.ts-BuYyCrnF.js.map +0 -1
- package/build/server/chunks/_server.ts-ByPExYfO.js.map +0 -1
- package/build/server/chunks/_server.ts-CilRds58.js.map +0 -1
- package/build/server/chunks/_server.ts-CjpQ10xh.js.map +0 -1
- package/build/server/chunks/opencode-db-path-DcfhJtJy.js +0 -15
- package/build/server/chunks/opencode-db-path-DcfhJtJy.js.map +0 -1
- package/build/server/chunks/pty-manager-TyMUpDA9.js.map +0 -1
|
@@ -0,0 +1,571 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Gemini CLI session reader — listing + conversation retrieval.
|
|
3
|
+
*
|
|
4
|
+
* Mirrors codex-reader.ts (Codex) for structure/conventions: produces the
|
|
5
|
+
* same SessionInfo / ProjectGroup / ConversationMessage shapes so the rest of
|
|
6
|
+
* the app is provider-agnostic.
|
|
7
|
+
*
|
|
8
|
+
* Data sources (in preference order per session):
|
|
9
|
+
* 1. ~/.gemini/tmp/<projectHash>/chats/session-*.json
|
|
10
|
+
* Full ConversationRecord (user + model + tool calls + thoughts).
|
|
11
|
+
* Present only in newer gemini-cli versions; NOT present on older installs.
|
|
12
|
+
* 2. ~/.gemini/tmp/<projectHash>/logs.json
|
|
13
|
+
* User-messages-only JSON array (LogEntry[]). Always present.
|
|
14
|
+
*
|
|
15
|
+
* Project hash → CWD reverse-lookup:
|
|
16
|
+
* - Read ~/.gemini/projects.json if present (slug → absolute path, new format).
|
|
17
|
+
* - For SHA-256 hash dirs (old format), the hash is irreversible without
|
|
18
|
+
* brute-force; we surface the hash itself as the projectPath in that case.
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
import type {
|
|
22
|
+
ConversationMessage,
|
|
23
|
+
GeminiConversationRecord,
|
|
24
|
+
GeminiLogEntry,
|
|
25
|
+
GeminiMessageRecord,
|
|
26
|
+
GeminiProjectsJson,
|
|
27
|
+
MessagePart,
|
|
28
|
+
ProjectGroup,
|
|
29
|
+
SessionInfo,
|
|
30
|
+
} from '$lib/types';
|
|
31
|
+
|
|
32
|
+
import * as crypto from 'crypto';
|
|
33
|
+
import * as fs from 'fs';
|
|
34
|
+
import { homedir } from 'os';
|
|
35
|
+
import * as path from 'path';
|
|
36
|
+
|
|
37
|
+
// ---------------------------------------------------------------------------
|
|
38
|
+
// Constants
|
|
39
|
+
// ---------------------------------------------------------------------------
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Files above this size are SKIPPED, not truncated — these are single JSON
|
|
43
|
+
* documents/arrays, so a partial read would be invalid JSON. Gemini session
|
|
44
|
+
* files are small in practice; this is only an OOM backstop.
|
|
45
|
+
*/
|
|
46
|
+
const MAX_GEMINI_FILE_BYTES = 64 * 1024 * 1024; // 64 MB
|
|
47
|
+
|
|
48
|
+
/** Gemini tmp root. */
|
|
49
|
+
const GEMINI_TMP = path.join(homedir(), '.gemini', 'tmp');
|
|
50
|
+
|
|
51
|
+
/** Gemini projects registry (slug → absolute path). */
|
|
52
|
+
const GEMINI_PROJECTS_JSON = path.join(homedir(), '.gemini', 'projects.json');
|
|
53
|
+
|
|
54
|
+
// ---------------------------------------------------------------------------
|
|
55
|
+
// Public API
|
|
56
|
+
// ---------------------------------------------------------------------------
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Return sessions whose logs.json or chat file was written within `thresholdMs`
|
|
60
|
+
* of now — i.e. sessions that are currently (or very recently) active.
|
|
61
|
+
*/
|
|
62
|
+
export function detectActiveGeminiSessions(
|
|
63
|
+
thresholdMs: number
|
|
64
|
+
): { cwd: string; id: string; startedAt: number }[] {
|
|
65
|
+
const cutoff = Date.now() - thresholdMs;
|
|
66
|
+
const projectHashToCwd = buildHashToCwdMap();
|
|
67
|
+
const projectDirs = collectProjectHashDirs();
|
|
68
|
+
const out: { cwd: string; id: string; startedAt: number }[] = [];
|
|
69
|
+
|
|
70
|
+
for (const hashDir of projectDirs) {
|
|
71
|
+
const hash = path.basename(hashDir);
|
|
72
|
+
const cwd = projectHashToCwd.get(hash) ?? hash;
|
|
73
|
+
|
|
74
|
+
// Check chats/session-*.json files.
|
|
75
|
+
const chatFiles = collectChatFiles(hashDir);
|
|
76
|
+
for (const chatFile of chatFiles) {
|
|
77
|
+
try {
|
|
78
|
+
const stat = fs.statSync(chatFile);
|
|
79
|
+
if (stat.mtimeMs < cutoff) {
|
|
80
|
+
continue;
|
|
81
|
+
}
|
|
82
|
+
const record = readChatFile(chatFile);
|
|
83
|
+
if (record) {
|
|
84
|
+
out.push({
|
|
85
|
+
cwd,
|
|
86
|
+
id: record.sessionId,
|
|
87
|
+
startedAt: new Date(record.startTime).getTime(),
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
} catch {
|
|
91
|
+
// skip unreadable files
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// Also check logs.json mtime (covers old-format sessions).
|
|
96
|
+
const logsPath = path.join(hashDir, 'logs.json');
|
|
97
|
+
try {
|
|
98
|
+
const stat = fs.statSync(logsPath);
|
|
99
|
+
if (stat.mtimeMs < cutoff) {
|
|
100
|
+
continue;
|
|
101
|
+
}
|
|
102
|
+
// Surface the most recent session in this logs.json.
|
|
103
|
+
const entries = readLogsJson(logsPath);
|
|
104
|
+
if (entries.length > 0) {
|
|
105
|
+
const last = entries[entries.length - 1];
|
|
106
|
+
if (last) {
|
|
107
|
+
// Only add if not already captured from a chat file.
|
|
108
|
+
const alreadyAdded = out.some((o) => o.id === last.sessionId && o.cwd === cwd);
|
|
109
|
+
if (!alreadyAdded) {
|
|
110
|
+
out.push({
|
|
111
|
+
cwd,
|
|
112
|
+
id: last.sessionId,
|
|
113
|
+
startedAt: new Date(entries[0]?.timestamp ?? last.timestamp).getTime(),
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
} catch {
|
|
119
|
+
// logs.json missing or unreadable — skip
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
return out;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Return the conversation messages for a given Gemini session ID.
|
|
128
|
+
* Prefers chats/session-*.json (full record) over logs.json (user-only).
|
|
129
|
+
* Mirrors getCodexConversation() pagination behaviour.
|
|
130
|
+
*/
|
|
131
|
+
export function getGeminiConversation(
|
|
132
|
+
sessionId: string,
|
|
133
|
+
offset = 0,
|
|
134
|
+
limit = 200
|
|
135
|
+
): ConversationMessage[] {
|
|
136
|
+
const projectDirs = collectProjectHashDirs();
|
|
137
|
+
|
|
138
|
+
for (const hashDir of projectDirs) {
|
|
139
|
+
// Try full chat record first.
|
|
140
|
+
const chatFile = findChatFileForSession(hashDir, sessionId);
|
|
141
|
+
if (chatFile) {
|
|
142
|
+
return conversationFromChatFile(chatFile, offset, limit);
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// Fall back to logs.json user-only messages.
|
|
147
|
+
for (const hashDir of projectDirs) {
|
|
148
|
+
const messages = conversationFromLogsJson(hashDir, sessionId);
|
|
149
|
+
if (messages.length > 0) {
|
|
150
|
+
if (offset === 0 && messages.length > limit) {
|
|
151
|
+
return messages.slice(messages.length - limit);
|
|
152
|
+
}
|
|
153
|
+
return messages.slice(offset, offset + limit);
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
return [];
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* List all Gemini CLI sessions grouped by project, sorted by most-recently-
|
|
162
|
+
* modified first. Mirrors listCodexProjects() from codex-reader.ts.
|
|
163
|
+
*/
|
|
164
|
+
export function listGeminiProjects(): ProjectGroup[] {
|
|
165
|
+
const projectHashToCwd = buildHashToCwdMap();
|
|
166
|
+
const projectDirs = collectProjectHashDirs();
|
|
167
|
+
|
|
168
|
+
const byCwd = new Map<string, SessionInfo[]>();
|
|
169
|
+
|
|
170
|
+
for (const hashDir of projectDirs) {
|
|
171
|
+
const hash = path.basename(hashDir);
|
|
172
|
+
const cwd = projectHashToCwd.get(hash) ?? hash;
|
|
173
|
+
|
|
174
|
+
// Prefer full chat records if any exist; fall back to logs.json.
|
|
175
|
+
const chatSessions = collectChatSessions(hashDir, cwd);
|
|
176
|
+
if (chatSessions.length > 0) {
|
|
177
|
+
for (const session of chatSessions) {
|
|
178
|
+
appendToMap(byCwd, cwd, session);
|
|
179
|
+
}
|
|
180
|
+
continue;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
// Fall back: derive sessions from logs.json.
|
|
184
|
+
const logSessions = sessionsFromLogsJson(hashDir, cwd);
|
|
185
|
+
for (const session of logSessions) {
|
|
186
|
+
appendToMap(byCwd, cwd, session);
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
const projects: ProjectGroup[] = [];
|
|
191
|
+
for (const [cwd, sessions] of byCwd) {
|
|
192
|
+
sessions.sort((a, b) => new Date(b.modified).getTime() - new Date(a.modified).getTime());
|
|
193
|
+
const segments = cwd.split('/').filter(Boolean);
|
|
194
|
+
// When the project hash couldn't be reverse-mapped to a real cwd, show a
|
|
195
|
+
// short friendly label instead of the raw 64-char SHA-256 hash.
|
|
196
|
+
const isRawHash = /^[0-9a-f]{64}$/i.test(cwd);
|
|
197
|
+
projects.push({
|
|
198
|
+
fullPath: cwd,
|
|
199
|
+
id: shortHash(cwd),
|
|
200
|
+
lastModified: sessions[0]?.modified ?? '',
|
|
201
|
+
name: isRawHash ? `Gemini (${cwd.slice(0, 8)})` : segments.slice(-2).join('/'),
|
|
202
|
+
sessionCount: sessions.length,
|
|
203
|
+
sessions,
|
|
204
|
+
});
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
return projects.sort(
|
|
208
|
+
(a, b) => new Date(b.lastModified).getTime() - new Date(a.lastModified).getTime()
|
|
209
|
+
);
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
// ---------------------------------------------------------------------------
|
|
213
|
+
// Session building helpers
|
|
214
|
+
// ---------------------------------------------------------------------------
|
|
215
|
+
|
|
216
|
+
function appendToMap<V>(map: Map<string, V[]>, key: string, value: V): void {
|
|
217
|
+
let bucket = map.get(key);
|
|
218
|
+
if (!bucket) {
|
|
219
|
+
bucket = [];
|
|
220
|
+
map.set(key, bucket);
|
|
221
|
+
}
|
|
222
|
+
bucket.push(value);
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
/**
|
|
226
|
+
* Build a SHA-256 projectHash → cwd map by reading ~/.gemini/projects.json.
|
|
227
|
+
* Returns an empty map if the file is absent (old gemini-cli installs).
|
|
228
|
+
*/
|
|
229
|
+
function buildHashToCwdMap(): Map<string, string> {
|
|
230
|
+
const map = new Map<string, string>();
|
|
231
|
+
try {
|
|
232
|
+
const raw = fs.readFileSync(GEMINI_PROJECTS_JSON, 'utf-8');
|
|
233
|
+
const data = JSON.parse(raw) as GeminiProjectsJson;
|
|
234
|
+
for (const [slugOrHash, absolutePath] of Object.entries(data)) {
|
|
235
|
+
// projects.json may use either the slug or the full SHA-256 hash as key.
|
|
236
|
+
map.set(slugOrHash, absolutePath);
|
|
237
|
+
// Pre-compute SHA-256(absolutePath) → absolutePath as well so that
|
|
238
|
+
// old hash-named directories match even when projects.json uses slugs.
|
|
239
|
+
const computed = crypto.createHash('sha256').update(absolutePath).digest('hex');
|
|
240
|
+
map.set(computed, absolutePath);
|
|
241
|
+
}
|
|
242
|
+
} catch {
|
|
243
|
+
// File absent or malformed — that's fine for old gemini-cli installs.
|
|
244
|
+
}
|
|
245
|
+
return map;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
// ---------------------------------------------------------------------------
|
|
249
|
+
// Conversation building helpers
|
|
250
|
+
// ---------------------------------------------------------------------------
|
|
251
|
+
|
|
252
|
+
function cleanTitle(text: string): string {
|
|
253
|
+
const first = text.replace(/\s+/g, ' ').trim().split('\n')[0]?.trim() ?? '';
|
|
254
|
+
if (!first) {
|
|
255
|
+
return 'Untitled Session';
|
|
256
|
+
}
|
|
257
|
+
return first.length > 80 ? `${first.slice(0, 77)}...` : first;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
/** Enumerate all chats/session-*.json files under a project hash dir. */
|
|
261
|
+
function collectChatFiles(hashDir: string): string[] {
|
|
262
|
+
const chatsDir = path.join(hashDir, 'chats');
|
|
263
|
+
try {
|
|
264
|
+
return fs
|
|
265
|
+
.readdirSync(chatsDir, { withFileTypes: true })
|
|
266
|
+
.filter((e) => e.isFile() && e.name.startsWith('session-') && e.name.endsWith('.json'))
|
|
267
|
+
.map((e) => path.join(chatsDir, e.name));
|
|
268
|
+
} catch {
|
|
269
|
+
return [];
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
// ---------------------------------------------------------------------------
|
|
274
|
+
// ConversationRecord → ConversationMessage mapping
|
|
275
|
+
// ---------------------------------------------------------------------------
|
|
276
|
+
|
|
277
|
+
/**
|
|
278
|
+
* Build SessionInfo records for each distinct sessionId found in a
|
|
279
|
+
* chats/session-*.json file under hashDir.
|
|
280
|
+
*/
|
|
281
|
+
function collectChatSessions(hashDir: string, cwd: string): SessionInfo[] {
|
|
282
|
+
const chatFiles = collectChatFiles(hashDir);
|
|
283
|
+
const sessions: SessionInfo[] = [];
|
|
284
|
+
|
|
285
|
+
for (const chatFile of chatFiles) {
|
|
286
|
+
try {
|
|
287
|
+
const stat = fs.statSync(chatFile);
|
|
288
|
+
const record = readChatFile(chatFile);
|
|
289
|
+
if (!record) {
|
|
290
|
+
continue;
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
const userMessages = record.messages.filter((m) => m.type === 'user');
|
|
294
|
+
const firstUserMsg = userMessages[0];
|
|
295
|
+
let title = 'Untitled Session';
|
|
296
|
+
if (firstUserMsg) {
|
|
297
|
+
title = cleanTitle(extractTextFromContent(firstUserMsg.content));
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
sessions.push({
|
|
301
|
+
created: record.startTime,
|
|
302
|
+
gitBranch: '',
|
|
303
|
+
id: record.sessionId,
|
|
304
|
+
messageCount: record.messages.length,
|
|
305
|
+
modified: stat.mtime.toISOString(),
|
|
306
|
+
projectPath: cwd,
|
|
307
|
+
source: 'gemini' as const,
|
|
308
|
+
summary: record.summary ?? '',
|
|
309
|
+
title,
|
|
310
|
+
});
|
|
311
|
+
} catch {
|
|
312
|
+
// skip unreadable chat files
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
return sessions;
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
// ---------------------------------------------------------------------------
|
|
320
|
+
// File I/O helpers
|
|
321
|
+
// ---------------------------------------------------------------------------
|
|
322
|
+
|
|
323
|
+
/** Enumerate all ~/.gemini/tmp/<hash>/ directories. */
|
|
324
|
+
function collectProjectHashDirs(): string[] {
|
|
325
|
+
try {
|
|
326
|
+
return fs
|
|
327
|
+
.readdirSync(GEMINI_TMP, { withFileTypes: true })
|
|
328
|
+
.filter((e) => e.isDirectory())
|
|
329
|
+
.map((e) => path.join(GEMINI_TMP, e.name));
|
|
330
|
+
} catch {
|
|
331
|
+
return [];
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
function conversationFromChatFile(
|
|
336
|
+
chatFile: string,
|
|
337
|
+
offset: number,
|
|
338
|
+
limit: number
|
|
339
|
+
): ConversationMessage[] {
|
|
340
|
+
try {
|
|
341
|
+
const record = readChatFile(chatFile);
|
|
342
|
+
if (!record) {
|
|
343
|
+
return [];
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
const messages = record.messages
|
|
347
|
+
.filter((m) => m.type === 'user' || m.type === 'gemini')
|
|
348
|
+
.map(messageRecordToMessage);
|
|
349
|
+
|
|
350
|
+
if (offset === 0 && messages.length > limit) {
|
|
351
|
+
let startIdx = messages.length - limit;
|
|
352
|
+
while (startIdx > 0 && messages[startIdx]?.role !== 'user') {
|
|
353
|
+
startIdx--;
|
|
354
|
+
}
|
|
355
|
+
return messages.slice(startIdx);
|
|
356
|
+
}
|
|
357
|
+
return messages.slice(offset, offset + limit);
|
|
358
|
+
} catch (err) {
|
|
359
|
+
console.error('[gemini] Failed to read chat file:', err);
|
|
360
|
+
return [];
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
function conversationFromLogsJson(hashDir: string, sessionId: string): ConversationMessage[] {
|
|
365
|
+
const logsPath = path.join(hashDir, 'logs.json');
|
|
366
|
+
const entries = readLogsJson(logsPath);
|
|
367
|
+
return entries
|
|
368
|
+
.filter((e) => e.sessionId === sessionId)
|
|
369
|
+
.map(
|
|
370
|
+
(entry): ConversationMessage => ({
|
|
371
|
+
id: `${entry.sessionId}-${String(entry.messageId)}`,
|
|
372
|
+
parts: [{ content: entry.message, type: 'text' }],
|
|
373
|
+
role: 'user',
|
|
374
|
+
timestamp: entry.timestamp,
|
|
375
|
+
})
|
|
376
|
+
);
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
/** Extract plain-text from a GeminiMessageRecord content field. */
|
|
380
|
+
function extractTextFromContent(
|
|
381
|
+
content: GeminiConversationRecord['messages'][0]['content']
|
|
382
|
+
): string {
|
|
383
|
+
if (typeof content === 'string') {
|
|
384
|
+
return content;
|
|
385
|
+
}
|
|
386
|
+
return content
|
|
387
|
+
.filter((p): p is { text: string } => 'text' in p)
|
|
388
|
+
.map((p) => p.text)
|
|
389
|
+
.join(' ');
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
/**
|
|
393
|
+
* Find the chats/session-*.json file for a specific sessionId.
|
|
394
|
+
* The filename embeds the first 8 chars of the UUID; we fall back to reading
|
|
395
|
+
* every file in the directory if necessary.
|
|
396
|
+
*/
|
|
397
|
+
function findChatFileForSession(hashDir: string, sessionId: string): null | string {
|
|
398
|
+
const shortId = sessionId.slice(0, 8);
|
|
399
|
+
const chatsDir = path.join(hashDir, 'chats');
|
|
400
|
+
try {
|
|
401
|
+
const entries = fs.readdirSync(chatsDir, { withFileTypes: true });
|
|
402
|
+
for (const entry of entries) {
|
|
403
|
+
if (!entry.isFile() || !entry.name.startsWith('session-') || !entry.name.endsWith('.json')) {
|
|
404
|
+
continue;
|
|
405
|
+
}
|
|
406
|
+
// Fast path: filename contains shortId.
|
|
407
|
+
if (entry.name.includes(shortId)) {
|
|
408
|
+
return path.join(chatsDir, entry.name);
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
// Slow path: read each file and check the sessionId field.
|
|
412
|
+
for (const entry of entries) {
|
|
413
|
+
if (!entry.isFile() || !entry.name.startsWith('session-') || !entry.name.endsWith('.json')) {
|
|
414
|
+
continue;
|
|
415
|
+
}
|
|
416
|
+
const record = readChatFile(path.join(chatsDir, entry.name));
|
|
417
|
+
if (record?.sessionId === sessionId) {
|
|
418
|
+
return path.join(chatsDir, entry.name);
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
} catch {
|
|
422
|
+
// chats dir missing or unreadable
|
|
423
|
+
}
|
|
424
|
+
return null;
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
function isGeminiLogEntry(v: unknown): v is GeminiLogEntry {
|
|
428
|
+
if (typeof v !== 'object' || v === null) {
|
|
429
|
+
return false;
|
|
430
|
+
}
|
|
431
|
+
const obj = v as Record<string, unknown>;
|
|
432
|
+
return (
|
|
433
|
+
typeof obj.sessionId === 'string' &&
|
|
434
|
+
typeof obj.messageId === 'number' &&
|
|
435
|
+
typeof obj.message === 'string' &&
|
|
436
|
+
typeof obj.timestamp === 'string'
|
|
437
|
+
);
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
// ---------------------------------------------------------------------------
|
|
441
|
+
// Utility helpers
|
|
442
|
+
// ---------------------------------------------------------------------------
|
|
443
|
+
|
|
444
|
+
function messageRecordToMessage(record: GeminiMessageRecord): ConversationMessage {
|
|
445
|
+
const role: 'assistant' | 'user' = record.type === 'user' ? 'user' : 'assistant';
|
|
446
|
+
const parts: MessagePart[] = [];
|
|
447
|
+
|
|
448
|
+
// 1. Thought summaries (only on 'gemini'-type messages).
|
|
449
|
+
if (record.type === 'gemini' && record.thoughts) {
|
|
450
|
+
for (const thought of record.thoughts) {
|
|
451
|
+
parts.push({ content: thought.summary ?? '', type: 'thinking' });
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
// 2. Content parts.
|
|
456
|
+
const rawContent = record.content;
|
|
457
|
+
const contentParts = typeof rawContent === 'string' ? [{ text: rawContent }] : rawContent;
|
|
458
|
+
for (const part of contentParts) {
|
|
459
|
+
if ('functionCall' in part) {
|
|
460
|
+
parts.push({
|
|
461
|
+
id: part.functionCall.id ?? part.functionCall.name,
|
|
462
|
+
input: part.functionCall.args,
|
|
463
|
+
toolName: part.functionCall.name,
|
|
464
|
+
type: 'tool_use',
|
|
465
|
+
});
|
|
466
|
+
} else if ('thought' in part && part.thought === true) {
|
|
467
|
+
parts.push({ content: part.text, type: 'thinking' });
|
|
468
|
+
} else if ('text' in part) {
|
|
469
|
+
parts.push({ content: part.text, type: 'text' });
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
// 3. Tool calls array (avoid duplicating entries already in content parts).
|
|
474
|
+
if (record.type === 'gemini' && record.toolCalls) {
|
|
475
|
+
for (const tc of record.toolCalls) {
|
|
476
|
+
const alreadyInParts = parts.some((p) => p.type === 'tool_use' && p.id === tc.id);
|
|
477
|
+
if (!alreadyInParts) {
|
|
478
|
+
parts.push({
|
|
479
|
+
id: tc.id,
|
|
480
|
+
input: tc.args,
|
|
481
|
+
toolName: tc.name,
|
|
482
|
+
type: 'tool_use',
|
|
483
|
+
});
|
|
484
|
+
}
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
return {
|
|
489
|
+
id: record.id,
|
|
490
|
+
parts,
|
|
491
|
+
role,
|
|
492
|
+
timestamp: record.timestamp,
|
|
493
|
+
};
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
/** Read and parse a chats/session-*.json file (a single JSON object). */
|
|
497
|
+
function readChatFile(filePath: string): GeminiConversationRecord | null {
|
|
498
|
+
try {
|
|
499
|
+
if (fs.statSync(filePath).size > MAX_GEMINI_FILE_BYTES) {
|
|
500
|
+
return null; // too large to parse safely as one JSON document
|
|
501
|
+
}
|
|
502
|
+
return JSON.parse(fs.readFileSync(filePath, 'utf-8')) as GeminiConversationRecord;
|
|
503
|
+
} catch {
|
|
504
|
+
return null;
|
|
505
|
+
}
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
/** Read and parse ~/.gemini/tmp/<hash>/logs.json (a single JSON array). */
|
|
509
|
+
function readLogsJson(logsPath: string): GeminiLogEntry[] {
|
|
510
|
+
try {
|
|
511
|
+
if (fs.statSync(logsPath).size > MAX_GEMINI_FILE_BYTES) {
|
|
512
|
+
return [];
|
|
513
|
+
}
|
|
514
|
+
const parsed: unknown = JSON.parse(fs.readFileSync(logsPath, 'utf-8'));
|
|
515
|
+
return Array.isArray(parsed) ? parsed.filter(isGeminiLogEntry) : [];
|
|
516
|
+
} catch {
|
|
517
|
+
return [];
|
|
518
|
+
}
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
/**
|
|
522
|
+
* Build SessionInfo records for each distinct sessionId found in logs.json.
|
|
523
|
+
*/
|
|
524
|
+
function sessionsFromLogsJson(hashDir: string, cwd: string): SessionInfo[] {
|
|
525
|
+
const logsPath = path.join(hashDir, 'logs.json');
|
|
526
|
+
let stat: fs.Stats;
|
|
527
|
+
try {
|
|
528
|
+
stat = fs.statSync(logsPath);
|
|
529
|
+
} catch {
|
|
530
|
+
return [];
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
const entries = readLogsJson(logsPath);
|
|
534
|
+
if (entries.length === 0) {
|
|
535
|
+
return [];
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
// Group by sessionId; preserve insertion order (chronological).
|
|
539
|
+
const bySession = new Map<string, GeminiLogEntry[]>();
|
|
540
|
+
for (const entry of entries) {
|
|
541
|
+
let bucket = bySession.get(entry.sessionId);
|
|
542
|
+
if (!bucket) {
|
|
543
|
+
bucket = [];
|
|
544
|
+
bySession.set(entry.sessionId, bucket);
|
|
545
|
+
}
|
|
546
|
+
bucket.push(entry);
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
const sessions: SessionInfo[] = [];
|
|
550
|
+
for (const [sessionId, sessionEntries] of bySession) {
|
|
551
|
+
const first = sessionEntries[0];
|
|
552
|
+
const last = sessionEntries[sessionEntries.length - 1];
|
|
553
|
+
sessions.push({
|
|
554
|
+
created: first?.timestamp ?? stat.birthtime.toISOString(),
|
|
555
|
+
gitBranch: '',
|
|
556
|
+
id: sessionId,
|
|
557
|
+
messageCount: sessionEntries.length,
|
|
558
|
+
modified: last?.timestamp ?? stat.mtime.toISOString(),
|
|
559
|
+
projectPath: cwd,
|
|
560
|
+
source: 'gemini' as const,
|
|
561
|
+
summary: '',
|
|
562
|
+
title: cleanTitle(first?.message ?? ''),
|
|
563
|
+
});
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
return sessions;
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
function shortHash(input: string): string {
|
|
570
|
+
return crypto.createHash('sha256').update(input).digest('hex').slice(0, 8);
|
|
571
|
+
}
|
|
@@ -1,26 +1,35 @@
|
|
|
1
|
+
import { existsSync } from 'fs';
|
|
1
2
|
import * as path from 'path';
|
|
2
3
|
|
|
3
4
|
/**
|
|
4
5
|
* Resolve the path to the OpenCode SQLite database.
|
|
5
6
|
*
|
|
6
|
-
*
|
|
7
|
+
* Probes candidate locations and returns the first that EXISTS (the previous
|
|
8
|
+
* version returned the macOS path unconditionally on darwin, which hid the DB
|
|
9
|
+
* for installs that use the XDG ~/.local/share path — observed in the wild).
|
|
10
|
+
* Candidate order:
|
|
7
11
|
* 1. XDG_DATA_HOME/opencode/opencode.db (honours XDG override)
|
|
8
|
-
* 2.
|
|
9
|
-
* 3.
|
|
12
|
+
* 2. ~/.local/share/opencode/opencode.db (XDG default — common even on macOS)
|
|
13
|
+
* 3. ~/Library/Application Support/opencode/opencode.db (legacy macOS path)
|
|
10
14
|
*/
|
|
11
15
|
export function resolveOpenCodeDbPath(): string {
|
|
12
16
|
const home = process.env.HOME || '';
|
|
17
|
+
const candidates: string[] = [];
|
|
13
18
|
|
|
14
|
-
// 1. If XDG_DATA_HOME is explicitly set, use it
|
|
15
19
|
if (process.env.XDG_DATA_HOME) {
|
|
16
|
-
|
|
20
|
+
candidates.push(path.join(process.env.XDG_DATA_HOME, 'opencode', 'opencode.db'));
|
|
17
21
|
}
|
|
18
|
-
|
|
19
|
-
// 2. Legacy macOS path (~/Library/Application Support/opencode/)
|
|
22
|
+
candidates.push(path.join(home, '.local', 'share', 'opencode', 'opencode.db'));
|
|
20
23
|
if (process.platform === 'darwin') {
|
|
21
|
-
|
|
24
|
+
candidates.push(path.join(home, 'Library', 'Application Support', 'opencode', 'opencode.db'));
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
for (const candidate of candidates) {
|
|
28
|
+
if (existsSync(candidate)) {
|
|
29
|
+
return candidate;
|
|
30
|
+
}
|
|
22
31
|
}
|
|
23
32
|
|
|
24
|
-
//
|
|
25
|
-
return path.join(home, '.local', 'share', 'opencode', 'opencode.db');
|
|
33
|
+
// None exist yet — return the preferred default; callers tolerate a missing file.
|
|
34
|
+
return candidates[0] ?? path.join(home, '.local', 'share', 'opencode', 'opencode.db');
|
|
26
35
|
}
|
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
import type { ClaudeSessionFile, DetectedProcess } from '$lib/types';
|
|
2
2
|
|
|
3
|
+
import { detectActiveCodexSessions } from '$lib/modules/server/sessions/codex-reader';
|
|
4
|
+
import { detectActiveGeminiSessions } from '$lib/modules/server/sessions/gemini-reader';
|
|
3
5
|
import { resolveOpenCodeDbPath } from '$lib/modules/server/sessions/opencode-db-path';
|
|
6
|
+
import { detectActiveQwenSessions } from '$lib/modules/server/sessions/qwen-reader';
|
|
4
7
|
import Database from 'better-sqlite3';
|
|
5
8
|
import { execSync } from 'child_process';
|
|
6
9
|
import { existsSync, readdirSync, readFileSync } from 'fs';
|
|
@@ -45,6 +48,15 @@ const CLAUDE_SESSIONS_DIR = join(homedir(), '.claude', 'sessions');
|
|
|
45
48
|
// OpenCode sessions updated within this window are considered "live"
|
|
46
49
|
const OPENCODE_ACTIVE_THRESHOLD_MS = 3 * 60_000; // 3 minutes
|
|
47
50
|
|
|
51
|
+
// Codex rollout files written within this window are considered "live"
|
|
52
|
+
const CODEX_ACTIVE_THRESHOLD_MS = 3 * 60_000; // 3 minutes
|
|
53
|
+
|
|
54
|
+
// Gemini session files written within this window are considered "live"
|
|
55
|
+
const GEMINI_ACTIVE_THRESHOLD_MS = 3 * 60_000; // 3 minutes
|
|
56
|
+
|
|
57
|
+
// Qwen session files written within this window are considered "live"
|
|
58
|
+
const QWEN_ACTIVE_THRESHOLD_MS = 3 * 60_000; // 3 minutes
|
|
59
|
+
|
|
48
60
|
/**
|
|
49
61
|
* Scan ~/.claude/sessions/*.json to find running Claude Code processes,
|
|
50
62
|
* and query the OpenCode SQLite DB for recently active sessions.
|
|
@@ -137,6 +149,61 @@ export function detectRunningAISessions(): DetectedProcess[] {
|
|
|
137
149
|
}
|
|
138
150
|
}
|
|
139
151
|
|
|
152
|
+
// --- Codex sessions ---
|
|
153
|
+
// Codex has no PID file; a rollout file written in the last few minutes
|
|
154
|
+
// indicates an active session. cwd/id come from its session_meta line.
|
|
155
|
+
try {
|
|
156
|
+
for (const s of detectActiveCodexSessions(CODEX_ACTIVE_THRESHOLD_MS)) {
|
|
157
|
+
results.push({
|
|
158
|
+
command: 'codex',
|
|
159
|
+
cwd: s.cwd,
|
|
160
|
+
kind: 'interactive',
|
|
161
|
+
pid: 0, // Codex doesn't expose a per-session PID
|
|
162
|
+
projectPath: cwdToProjectPath(s.cwd),
|
|
163
|
+
sessionId: s.id,
|
|
164
|
+
startedAt: s.startedAt,
|
|
165
|
+
});
|
|
166
|
+
}
|
|
167
|
+
} catch {
|
|
168
|
+
// ~/.codex/sessions missing or unreadable — skip silently
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// --- Gemini sessions ---
|
|
172
|
+
// Gemini has no PID file; a logs.json / chat file written in the last few
|
|
173
|
+
// minutes indicates an active session. cwd is reverse-mapped where possible.
|
|
174
|
+
try {
|
|
175
|
+
for (const s of detectActiveGeminiSessions(GEMINI_ACTIVE_THRESHOLD_MS)) {
|
|
176
|
+
results.push({
|
|
177
|
+
command: 'gemini',
|
|
178
|
+
cwd: s.cwd,
|
|
179
|
+
kind: 'interactive',
|
|
180
|
+
pid: 0, // Gemini doesn't expose a per-session PID
|
|
181
|
+
projectPath: cwdToProjectPath(s.cwd),
|
|
182
|
+
sessionId: s.id,
|
|
183
|
+
startedAt: s.startedAt,
|
|
184
|
+
});
|
|
185
|
+
}
|
|
186
|
+
} catch {
|
|
187
|
+
// ~/.gemini/tmp missing or unreadable — skip silently
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
// --- Qwen sessions ---
|
|
191
|
+
try {
|
|
192
|
+
for (const s of detectActiveQwenSessions(QWEN_ACTIVE_THRESHOLD_MS)) {
|
|
193
|
+
results.push({
|
|
194
|
+
command: 'qwen',
|
|
195
|
+
cwd: s.cwd,
|
|
196
|
+
kind: 'interactive',
|
|
197
|
+
pid: 0,
|
|
198
|
+
projectPath: cwdToProjectPath(s.cwd),
|
|
199
|
+
sessionId: s.id,
|
|
200
|
+
startedAt: s.startedAt,
|
|
201
|
+
});
|
|
202
|
+
}
|
|
203
|
+
} catch {
|
|
204
|
+
// ~/.qwen/projects missing or unreadable — skip silently
|
|
205
|
+
}
|
|
206
|
+
|
|
140
207
|
// Sort by startedAt descending (most recent first)
|
|
141
208
|
results.sort((a, b) => b.startedAt - a.startedAt);
|
|
142
209
|
|