@juspay/shooter 1.16.0 → 1.18.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.DEfoFaGR.css → 0.NV8k8wxG.css} +1 -1
- package/build/client/_app/immutable/assets/0.NV8k8wxG.css.br +0 -0
- package/build/client/_app/immutable/assets/0.NV8k8wxG.css.gz +0 -0
- package/build/client/_app/immutable/chunks/8lO1IL7u.js +1 -0
- package/build/client/_app/immutable/chunks/8lO1IL7u.js.br +0 -0
- package/build/client/_app/immutable/chunks/8lO1IL7u.js.gz +0 -0
- package/build/client/_app/immutable/chunks/B9WQy_3X.js +1 -0
- package/build/client/_app/immutable/chunks/B9WQy_3X.js.br +0 -0
- package/build/client/_app/immutable/chunks/B9WQy_3X.js.gz +0 -0
- package/build/client/_app/immutable/chunks/BdtLzPpO.js +1 -0
- package/build/client/_app/immutable/chunks/BdtLzPpO.js.br +0 -0
- package/build/client/_app/immutable/chunks/BdtLzPpO.js.gz +0 -0
- package/build/client/_app/immutable/chunks/{DlS3abGJ.js → DJvX78LW.js} +1 -1
- package/build/client/_app/immutable/chunks/DJvX78LW.js.br +0 -0
- package/build/client/_app/immutable/chunks/DJvX78LW.js.gz +0 -0
- package/build/client/_app/immutable/chunks/nWG9RHyB.js +3 -0
- package/build/client/_app/immutable/chunks/nWG9RHyB.js.br +0 -0
- package/build/client/_app/immutable/chunks/nWG9RHyB.js.gz +0 -0
- package/build/client/_app/immutable/entry/{app.CSJG7N9H.js → app.f46Ko1hu.js} +2 -2
- package/build/client/_app/immutable/entry/app.f46Ko1hu.js.br +0 -0
- package/build/client/_app/immutable/entry/app.f46Ko1hu.js.gz +0 -0
- package/build/client/_app/immutable/entry/start.BVDjNnXt.js +1 -0
- package/build/client/_app/immutable/entry/start.BVDjNnXt.js.br +2 -0
- package/build/client/_app/immutable/entry/start.BVDjNnXt.js.gz +0 -0
- package/build/client/_app/immutable/nodes/{0.qOL7xtFn.js → 0.D_9EwVmq.js} +1 -1
- package/build/client/_app/immutable/nodes/0.D_9EwVmq.js.br +0 -0
- package/build/client/_app/immutable/nodes/0.D_9EwVmq.js.gz +0 -0
- package/build/client/_app/immutable/nodes/{1.Di708Ago.js → 1.C4eFlqSB.js} +1 -1
- package/build/client/_app/immutable/nodes/1.C4eFlqSB.js.br +0 -0
- package/build/client/_app/immutable/nodes/1.C4eFlqSB.js.gz +0 -0
- package/build/client/_app/immutable/nodes/{2.DSM1znqa.js → 2.CdC092Za.js} +1 -1
- package/build/client/_app/immutable/nodes/2.CdC092Za.js.br +0 -0
- package/build/client/_app/immutable/nodes/2.CdC092Za.js.gz +0 -0
- package/build/client/_app/immutable/nodes/{3.BPa5fh75.js → 3.Dhf4ZWW0.js} +1 -1
- package/build/client/_app/immutable/nodes/3.Dhf4ZWW0.js.br +0 -0
- package/build/client/_app/immutable/nodes/3.Dhf4ZWW0.js.gz +0 -0
- package/build/client/_app/immutable/nodes/6.B3SEB_li.js +1 -0
- package/build/client/_app/immutable/nodes/6.B3SEB_li.js.br +0 -0
- package/build/client/_app/immutable/nodes/6.B3SEB_li.js.gz +0 -0
- package/build/client/_app/immutable/nodes/{7.B7UJd8GQ.js → 7.DV8cJ1lX.js} +3 -3
- package/build/client/_app/immutable/nodes/7.DV8cJ1lX.js.br +0 -0
- package/build/client/_app/immutable/nodes/7.DV8cJ1lX.js.gz +0 -0
- package/build/client/_app/immutable/nodes/8.Bs362gyb.js +2 -0
- package/build/client/_app/immutable/nodes/8.Bs362gyb.js.br +0 -0
- package/build/client/_app/immutable/nodes/8.Bs362gyb.js.gz +0 -0
- package/build/client/_app/immutable/nodes/9.Cf7_3uqT.js +2 -0
- package/build/client/_app/immutable/nodes/9.Cf7_3uqT.js.br +0 -0
- package/build/client/_app/immutable/nodes/9.Cf7_3uqT.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-D8uPamNd.js → 0-Cd7jY0a7.js} +3 -3
- package/build/server/chunks/{0-D8uPamNd.js.map → 0-Cd7jY0a7.js.map} +1 -1
- package/build/server/chunks/{1-DhtioHbs.js → 1-C4BOGoJY.js} +2 -2
- package/build/server/chunks/{1-DhtioHbs.js.map → 1-C4BOGoJY.js.map} +1 -1
- package/build/server/chunks/{2-Cgh7ZFgE.js → 2-Ba0mNwJ6.js} +2 -2
- package/build/server/chunks/{2-Cgh7ZFgE.js.map → 2-Ba0mNwJ6.js.map} +1 -1
- package/build/server/chunks/{3-I6hnjssH.js → 3-Pg8t1uJU.js} +2 -2
- package/build/server/chunks/{3-I6hnjssH.js.map → 3-Pg8t1uJU.js.map} +1 -1
- package/build/server/chunks/{6-bPDbH1_W.js → 6-D8xbnTSo.js} +2 -2
- package/build/server/chunks/6-D8xbnTSo.js.map +1 -0
- package/build/server/chunks/{7-CpBrOkxQ.js → 7-CkVK06S0.js} +2 -2
- package/build/server/chunks/7-CkVK06S0.js.map +1 -0
- package/build/server/chunks/{8-BRGAVfze.js → 8-C8qVhrds.js} +2 -2
- package/build/server/chunks/8-C8qVhrds.js.map +1 -0
- package/build/server/chunks/{9-C6xuAb_Y.js → 9-fL5zqN0T.js} +2 -2
- package/build/server/chunks/9-fL5zqN0T.js.map +1 -0
- package/build/server/chunks/{_server.ts-BrRZXr-8.js → _server.ts-BA_uWcPw.js} +9 -9
- package/build/server/chunks/_server.ts-BA_uWcPw.js.map +1 -0
- package/build/server/chunks/{_server.ts-C6xbNz6d.js → _server.ts-Bu3s5hfv.js} +3 -3
- package/build/server/chunks/{_server.ts-C6xbNz6d.js.map → _server.ts-Bu3s5hfv.js.map} +1 -1
- package/build/server/chunks/{_server.ts-Cq9_scaV.js → _server.ts-CwAjt91u.js} +18 -18
- package/build/server/chunks/_server.ts-CwAjt91u.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/{_server.ts-Dekgb6Hx.js → _server.ts-DZP2lhaY.js} +3 -3
- package/build/server/chunks/{_server.ts-Dekgb6Hx.js.map → _server.ts-DZP2lhaY.js.map} +1 -1
- package/build/server/chunks/_server.ts-DZgfQKiH.js +81 -0
- package/build/server/chunks/_server.ts-DZgfQKiH.js.map +1 -0
- package/build/server/chunks/{_server.ts-CjK0g9dO.js → _server.ts-MbnroWEF.js} +25 -16
- package/build/server/chunks/_server.ts-MbnroWEF.js.map +1 -0
- package/build/server/chunks/{pty-manager-aFpChJah.js → pty-manager-DmNSCKAr.js} +99 -2
- package/build/server/chunks/pty-manager-DmNSCKAr.js.map +1 -0
- package/build/server/chunks/qwen-reader-DGfUbKaJ.js +2112 -0
- package/build/server/chunks/qwen-reader-DGfUbKaJ.js.map +1 -0
- package/build/server/chunks/{_server.ts-D--_NXt2.js → registry-Kcw2UCMv.js} +132 -106
- package/build/server/chunks/registry-Kcw2UCMv.js.map +1 -0
- package/build/server/index.js +1 -1
- package/build/server/index.js.map +1 -1
- package/build/server/manifest.js +16 -16
- package/build/server/manifest.js.map +1 -1
- package/package.json +2 -2
- package/scripts/e2e-all-features.sh +165 -0
- package/scripts/e2e-cross-terminal.sh +168 -0
- package/server.ts +12 -0
- package/src/lib/modules/client/common/index.ts +1 -1
- package/src/lib/modules/client/common/provider.ts +11 -0
- package/src/lib/modules/client/terminal/ChatView.svelte +9 -2
- package/src/lib/modules/client/terminal/LaunchSheet.svelte +4 -0
- package/src/lib/modules/server/sessions/amp-reader.ts +439 -0
- package/src/lib/modules/server/sessions/codex-reader.ts +34 -33
- package/src/lib/modules/server/sessions/copilot-reader.ts +542 -0
- package/src/lib/modules/server/sessions/cursor-reader.ts +634 -0
- package/src/lib/modules/server/sessions/gemini-reader.ts +594 -0
- package/src/lib/modules/server/sessions/opencode-db-path.ts +19 -10
- package/src/lib/modules/server/sessions/opencode-reader.ts +13 -12
- package/src/lib/modules/server/sessions/process-detector.ts +39 -18
- package/src/lib/modules/server/sessions/provider-paths.ts +173 -0
- package/src/lib/modules/server/sessions/qwen-reader.ts +336 -0
- package/src/lib/modules/server/sessions/registry.ts +178 -0
- package/src/lib/modules/server/terminal/codex-watcher.ts +4 -1
- package/src/lib/modules/server/terminal/generic-session-watcher.ts +163 -0
- package/src/lib/modules/server/terminal/pty-manager.ts +51 -0
- package/src/lib/modules/server/ws/session-handler.ts +34 -20
- package/src/lib/theme.css +32 -0
- package/src/lib/types/gemini.ts +100 -0
- package/src/lib/types/generated/Sessions.ts +17 -1
- package/src/lib/types/index.ts +1 -0
- package/src/lib/types/server.ts +23 -6
- package/src/lib/types/sessions.ts +14 -2
- package/src/routes/api/sessions/+server.ts +5 -52
- package/src/routes/api/sessions/connect/+server.ts +18 -11
- package/src/routes/api/terminals/+server.ts +7 -5
- package/src/routes/terminals/+page.svelte +7 -2
- package/src/routes/terminals/[id]/+page.svelte +1 -2
- package/build/client/_app/immutable/assets/0.DEfoFaGR.css.br +0 -0
- package/build/client/_app/immutable/assets/0.DEfoFaGR.css.gz +0 -0
- package/build/client/_app/immutable/chunks/Bkqjn62J.js +0 -1
- package/build/client/_app/immutable/chunks/Bkqjn62J.js.br +0 -1
- package/build/client/_app/immutable/chunks/Bkqjn62J.js.gz +0 -0
- package/build/client/_app/immutable/chunks/DOHhmtDH.js +0 -3
- package/build/client/_app/immutable/chunks/DOHhmtDH.js.br +0 -0
- package/build/client/_app/immutable/chunks/DOHhmtDH.js.gz +0 -0
- package/build/client/_app/immutable/chunks/DlS3abGJ.js.br +0 -0
- package/build/client/_app/immutable/chunks/DlS3abGJ.js.gz +0 -0
- package/build/client/_app/immutable/chunks/Pw0jDB7M.js +0 -1
- package/build/client/_app/immutable/chunks/Pw0jDB7M.js.br +0 -0
- package/build/client/_app/immutable/chunks/Pw0jDB7M.js.gz +0 -0
- package/build/client/_app/immutable/entry/app.CSJG7N9H.js.br +0 -0
- package/build/client/_app/immutable/entry/app.CSJG7N9H.js.gz +0 -0
- package/build/client/_app/immutable/entry/start.CTt1901T.js +0 -1
- package/build/client/_app/immutable/entry/start.CTt1901T.js.br +0 -2
- package/build/client/_app/immutable/entry/start.CTt1901T.js.gz +0 -0
- package/build/client/_app/immutable/nodes/0.qOL7xtFn.js.br +0 -0
- package/build/client/_app/immutable/nodes/0.qOL7xtFn.js.gz +0 -0
- package/build/client/_app/immutable/nodes/1.Di708Ago.js.br +0 -0
- package/build/client/_app/immutable/nodes/1.Di708Ago.js.gz +0 -0
- package/build/client/_app/immutable/nodes/2.DSM1znqa.js.br +0 -0
- package/build/client/_app/immutable/nodes/2.DSM1znqa.js.gz +0 -0
- package/build/client/_app/immutable/nodes/3.BPa5fh75.js.br +0 -0
- package/build/client/_app/immutable/nodes/3.BPa5fh75.js.gz +0 -0
- package/build/client/_app/immutable/nodes/6.B1LwwEF-.js +0 -1
- package/build/client/_app/immutable/nodes/6.B1LwwEF-.js.br +0 -0
- package/build/client/_app/immutable/nodes/6.B1LwwEF-.js.gz +0 -0
- package/build/client/_app/immutable/nodes/7.B7UJd8GQ.js.br +0 -0
- package/build/client/_app/immutable/nodes/7.B7UJd8GQ.js.gz +0 -0
- package/build/client/_app/immutable/nodes/8.CG0mrgBU.js +0 -2
- package/build/client/_app/immutable/nodes/8.CG0mrgBU.js.br +0 -0
- package/build/client/_app/immutable/nodes/8.CG0mrgBU.js.gz +0 -0
- package/build/client/_app/immutable/nodes/9.KwzWaMHj.js +0 -2
- package/build/client/_app/immutable/nodes/9.KwzWaMHj.js.br +0 -0
- package/build/client/_app/immutable/nodes/9.KwzWaMHj.js.gz +0 -0
- package/build/server/chunks/6-bPDbH1_W.js.map +0 -1
- package/build/server/chunks/7-CpBrOkxQ.js.map +0 -1
- package/build/server/chunks/8-BRGAVfze.js.map +0 -1
- package/build/server/chunks/9-C6xuAb_Y.js.map +0 -1
- package/build/server/chunks/_server.ts-BrRZXr-8.js.map +0 -1
- package/build/server/chunks/_server.ts-CjK0g9dO.js.map +0 -1
- package/build/server/chunks/_server.ts-Cq9_scaV.js.map +0 -1
- package/build/server/chunks/_server.ts-D--_NXt2.js.map +0 -1
- package/build/server/chunks/opencode-db-path-CRgzBK5U.js +0 -402
- package/build/server/chunks/opencode-db-path-CRgzBK5U.js.map +0 -1
- package/build/server/chunks/pty-manager-aFpChJah.js.map +0 -1
|
@@ -0,0 +1,594 @@
|
|
|
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
|
+
* Parse the full conversation from a single chats/session-*.json file.
|
|
214
|
+
* Returns all messages with no pagination — suitable for bulk processing.
|
|
215
|
+
*/
|
|
216
|
+
export function parseGeminiSessionFile(filePath: string): ConversationMessage[] {
|
|
217
|
+
try {
|
|
218
|
+
return conversationFromChatFile(filePath, 0, Number.MAX_SAFE_INTEGER);
|
|
219
|
+
} catch {
|
|
220
|
+
return [];
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
/**
|
|
225
|
+
* Resolve the absolute path of the chats/session-*.json file backing the
|
|
226
|
+
* given sessionId, or null if no chat file exists (e.g. logs.json-only session).
|
|
227
|
+
*/
|
|
228
|
+
export function resolveGeminiSessionFile(sessionId: string): null | string {
|
|
229
|
+
for (const hashDir of collectProjectHashDirs()) {
|
|
230
|
+
const chatFile = findChatFileForSession(hashDir, sessionId);
|
|
231
|
+
if (chatFile !== null) {
|
|
232
|
+
return chatFile;
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
return null;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
// ---------------------------------------------------------------------------
|
|
239
|
+
// Session building helpers
|
|
240
|
+
// ---------------------------------------------------------------------------
|
|
241
|
+
|
|
242
|
+
function appendToMap<V>(map: Map<string, V[]>, key: string, value: V): void {
|
|
243
|
+
let bucket = map.get(key);
|
|
244
|
+
if (!bucket) {
|
|
245
|
+
bucket = [];
|
|
246
|
+
map.set(key, bucket);
|
|
247
|
+
}
|
|
248
|
+
bucket.push(value);
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
/**
|
|
252
|
+
* Build a SHA-256 projectHash → cwd map by reading ~/.gemini/projects.json.
|
|
253
|
+
* Returns an empty map if the file is absent (old gemini-cli installs).
|
|
254
|
+
*/
|
|
255
|
+
function buildHashToCwdMap(): Map<string, string> {
|
|
256
|
+
const map = new Map<string, string>();
|
|
257
|
+
try {
|
|
258
|
+
const raw = fs.readFileSync(GEMINI_PROJECTS_JSON, 'utf-8');
|
|
259
|
+
const data = JSON.parse(raw) as GeminiProjectsJson;
|
|
260
|
+
for (const [slugOrHash, absolutePath] of Object.entries(data)) {
|
|
261
|
+
// projects.json may use either the slug or the full SHA-256 hash as key.
|
|
262
|
+
map.set(slugOrHash, absolutePath);
|
|
263
|
+
// Pre-compute SHA-256(absolutePath) → absolutePath as well so that
|
|
264
|
+
// old hash-named directories match even when projects.json uses slugs.
|
|
265
|
+
const computed = crypto.createHash('sha256').update(absolutePath).digest('hex');
|
|
266
|
+
map.set(computed, absolutePath);
|
|
267
|
+
}
|
|
268
|
+
} catch {
|
|
269
|
+
// File absent or malformed — that's fine for old gemini-cli installs.
|
|
270
|
+
}
|
|
271
|
+
return map;
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
// ---------------------------------------------------------------------------
|
|
275
|
+
// Conversation building helpers
|
|
276
|
+
// ---------------------------------------------------------------------------
|
|
277
|
+
|
|
278
|
+
function cleanTitle(text: string): string {
|
|
279
|
+
const first = text.replace(/\s+/g, ' ').trim().split('\n')[0]?.trim() ?? '';
|
|
280
|
+
if (!first) {
|
|
281
|
+
return 'Untitled Session';
|
|
282
|
+
}
|
|
283
|
+
return first.length > 80 ? `${first.slice(0, 77)}...` : first;
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
/** Enumerate all chats/session-*.json files under a project hash dir. */
|
|
287
|
+
function collectChatFiles(hashDir: string): string[] {
|
|
288
|
+
const chatsDir = path.join(hashDir, 'chats');
|
|
289
|
+
try {
|
|
290
|
+
return fs
|
|
291
|
+
.readdirSync(chatsDir, { withFileTypes: true })
|
|
292
|
+
.filter((e) => e.isFile() && e.name.startsWith('session-') && e.name.endsWith('.json'))
|
|
293
|
+
.map((e) => path.join(chatsDir, e.name));
|
|
294
|
+
} catch {
|
|
295
|
+
return [];
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
// ---------------------------------------------------------------------------
|
|
300
|
+
// ConversationRecord → ConversationMessage mapping
|
|
301
|
+
// ---------------------------------------------------------------------------
|
|
302
|
+
|
|
303
|
+
/**
|
|
304
|
+
* Build SessionInfo records for each distinct sessionId found in a
|
|
305
|
+
* chats/session-*.json file under hashDir.
|
|
306
|
+
*/
|
|
307
|
+
function collectChatSessions(hashDir: string, cwd: string): SessionInfo[] {
|
|
308
|
+
const chatFiles = collectChatFiles(hashDir);
|
|
309
|
+
const sessions: SessionInfo[] = [];
|
|
310
|
+
|
|
311
|
+
for (const chatFile of chatFiles) {
|
|
312
|
+
try {
|
|
313
|
+
const stat = fs.statSync(chatFile);
|
|
314
|
+
const record = readChatFile(chatFile);
|
|
315
|
+
if (!record) {
|
|
316
|
+
continue;
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
const userMessages = record.messages.filter((m) => m.type === 'user');
|
|
320
|
+
const firstUserMsg = userMessages[0];
|
|
321
|
+
let title = 'Untitled Session';
|
|
322
|
+
if (firstUserMsg) {
|
|
323
|
+
title = cleanTitle(extractTextFromContent(firstUserMsg.content));
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
sessions.push({
|
|
327
|
+
created: record.startTime,
|
|
328
|
+
gitBranch: '',
|
|
329
|
+
id: record.sessionId,
|
|
330
|
+
messageCount: record.messages.length,
|
|
331
|
+
modified: stat.mtime.toISOString(),
|
|
332
|
+
projectPath: cwd,
|
|
333
|
+
source: 'gemini' as const,
|
|
334
|
+
summary: record.summary ?? '',
|
|
335
|
+
title,
|
|
336
|
+
});
|
|
337
|
+
} catch {
|
|
338
|
+
// skip unreadable chat files
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
return sessions;
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
// ---------------------------------------------------------------------------
|
|
346
|
+
// File I/O helpers
|
|
347
|
+
// ---------------------------------------------------------------------------
|
|
348
|
+
|
|
349
|
+
/** Enumerate all ~/.gemini/tmp/<hash>/ directories. */
|
|
350
|
+
function collectProjectHashDirs(): string[] {
|
|
351
|
+
try {
|
|
352
|
+
return fs
|
|
353
|
+
.readdirSync(GEMINI_TMP, { withFileTypes: true })
|
|
354
|
+
.filter((e) => e.isDirectory())
|
|
355
|
+
.map((e) => path.join(GEMINI_TMP, e.name));
|
|
356
|
+
} catch {
|
|
357
|
+
return [];
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
function conversationFromChatFile(
|
|
362
|
+
chatFile: string,
|
|
363
|
+
offset: number,
|
|
364
|
+
limit: number
|
|
365
|
+
): ConversationMessage[] {
|
|
366
|
+
try {
|
|
367
|
+
const record = readChatFile(chatFile);
|
|
368
|
+
if (!record) {
|
|
369
|
+
return [];
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
const messages = record.messages
|
|
373
|
+
.filter((m) => m.type === 'user' || m.type === 'gemini')
|
|
374
|
+
.map(messageRecordToMessage);
|
|
375
|
+
|
|
376
|
+
if (offset === 0 && messages.length > limit) {
|
|
377
|
+
let startIdx = messages.length - limit;
|
|
378
|
+
while (startIdx > 0 && messages[startIdx]?.role !== 'user') {
|
|
379
|
+
startIdx--;
|
|
380
|
+
}
|
|
381
|
+
return messages.slice(startIdx);
|
|
382
|
+
}
|
|
383
|
+
return messages.slice(offset, offset + limit);
|
|
384
|
+
} catch (err) {
|
|
385
|
+
console.error('[gemini] Failed to read chat file:', err);
|
|
386
|
+
return [];
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
function conversationFromLogsJson(hashDir: string, sessionId: string): ConversationMessage[] {
|
|
391
|
+
const logsPath = path.join(hashDir, 'logs.json');
|
|
392
|
+
const entries = readLogsJson(logsPath);
|
|
393
|
+
return entries
|
|
394
|
+
.filter((e) => e.sessionId === sessionId)
|
|
395
|
+
.map(
|
|
396
|
+
(entry): ConversationMessage => ({
|
|
397
|
+
id: `${entry.sessionId}-${String(entry.messageId)}`,
|
|
398
|
+
parts: [{ content: entry.message, type: 'text' }],
|
|
399
|
+
role: 'user',
|
|
400
|
+
timestamp: entry.timestamp,
|
|
401
|
+
})
|
|
402
|
+
);
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
/** Extract plain-text from a GeminiMessageRecord content field. */
|
|
406
|
+
function extractTextFromContent(
|
|
407
|
+
content: GeminiConversationRecord['messages'][0]['content']
|
|
408
|
+
): string {
|
|
409
|
+
if (typeof content === 'string') {
|
|
410
|
+
return content;
|
|
411
|
+
}
|
|
412
|
+
return content
|
|
413
|
+
.filter((p): p is { text: string } => 'text' in p)
|
|
414
|
+
.map((p) => p.text)
|
|
415
|
+
.join(' ');
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
/**
|
|
419
|
+
* Find the chats/session-*.json file for a specific sessionId. The filename
|
|
420
|
+
* embeds the first 8 chars of the UUID, so prefix matches are tried first, but
|
|
421
|
+
* the exact sessionId field is always verified before returning — a short/empty
|
|
422
|
+
* id or a prefix collision must never mis-resolve to the wrong session.
|
|
423
|
+
*/
|
|
424
|
+
function findChatFileForSession(hashDir: string, sessionId: string): null | string {
|
|
425
|
+
if (!sessionId) {
|
|
426
|
+
return null;
|
|
427
|
+
}
|
|
428
|
+
const chatsDir = path.join(hashDir, 'chats');
|
|
429
|
+
let names: string[];
|
|
430
|
+
try {
|
|
431
|
+
names = fs
|
|
432
|
+
.readdirSync(chatsDir, { withFileTypes: true })
|
|
433
|
+
.filter((e) => e.isFile() && e.name.startsWith('session-') && e.name.endsWith('.json'))
|
|
434
|
+
.map((e) => e.name);
|
|
435
|
+
} catch {
|
|
436
|
+
return null; // chats dir missing or unreadable
|
|
437
|
+
}
|
|
438
|
+
const shortId = sessionId.slice(0, 8);
|
|
439
|
+
// Order prefix-matching filenames first (cheap), then verify the exact id.
|
|
440
|
+
names.sort((a, b) => Number(b.includes(shortId)) - Number(a.includes(shortId)));
|
|
441
|
+
for (const name of names) {
|
|
442
|
+
const full = path.join(chatsDir, name);
|
|
443
|
+
if (readChatFile(full)?.sessionId === sessionId) {
|
|
444
|
+
return full;
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
return null;
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
function isGeminiLogEntry(v: unknown): v is GeminiLogEntry {
|
|
451
|
+
if (typeof v !== 'object' || v === null) {
|
|
452
|
+
return false;
|
|
453
|
+
}
|
|
454
|
+
const obj = v as Record<string, unknown>;
|
|
455
|
+
return (
|
|
456
|
+
typeof obj.sessionId === 'string' &&
|
|
457
|
+
typeof obj.messageId === 'number' &&
|
|
458
|
+
typeof obj.message === 'string' &&
|
|
459
|
+
typeof obj.timestamp === 'string'
|
|
460
|
+
);
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
// ---------------------------------------------------------------------------
|
|
464
|
+
// Utility helpers
|
|
465
|
+
// ---------------------------------------------------------------------------
|
|
466
|
+
|
|
467
|
+
function messageRecordToMessage(record: GeminiMessageRecord): ConversationMessage {
|
|
468
|
+
const role: 'assistant' | 'user' = record.type === 'user' ? 'user' : 'assistant';
|
|
469
|
+
const parts: MessagePart[] = [];
|
|
470
|
+
|
|
471
|
+
// 1. Thought summaries (only on 'gemini'-type messages).
|
|
472
|
+
if (record.type === 'gemini' && record.thoughts) {
|
|
473
|
+
for (const thought of record.thoughts) {
|
|
474
|
+
parts.push({ content: thought.summary ?? '', type: 'thinking' });
|
|
475
|
+
}
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
// 2. Content parts.
|
|
479
|
+
const rawContent = record.content;
|
|
480
|
+
const contentParts = typeof rawContent === 'string' ? [{ text: rawContent }] : rawContent;
|
|
481
|
+
for (const part of contentParts) {
|
|
482
|
+
if ('functionCall' in part) {
|
|
483
|
+
parts.push({
|
|
484
|
+
id: part.functionCall.id ?? part.functionCall.name,
|
|
485
|
+
input: part.functionCall.args,
|
|
486
|
+
toolName: part.functionCall.name,
|
|
487
|
+
type: 'tool_use',
|
|
488
|
+
});
|
|
489
|
+
} else if ('thought' in part && part.thought === true) {
|
|
490
|
+
parts.push({ content: part.text, type: 'thinking' });
|
|
491
|
+
} else if ('text' in part) {
|
|
492
|
+
parts.push({ content: part.text, type: 'text' });
|
|
493
|
+
}
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
// 3. Tool calls array (avoid duplicating entries already in content parts).
|
|
497
|
+
if (record.type === 'gemini' && record.toolCalls) {
|
|
498
|
+
for (const tc of record.toolCalls) {
|
|
499
|
+
const alreadyInParts = parts.some((p) => p.type === 'tool_use' && p.id === tc.id);
|
|
500
|
+
if (!alreadyInParts) {
|
|
501
|
+
parts.push({
|
|
502
|
+
id: tc.id,
|
|
503
|
+
input: tc.args,
|
|
504
|
+
toolName: tc.name,
|
|
505
|
+
type: 'tool_use',
|
|
506
|
+
});
|
|
507
|
+
}
|
|
508
|
+
}
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
return {
|
|
512
|
+
id: record.id,
|
|
513
|
+
parts,
|
|
514
|
+
role,
|
|
515
|
+
timestamp: record.timestamp,
|
|
516
|
+
};
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
/** Read and parse a chats/session-*.json file (a single JSON object). */
|
|
520
|
+
function readChatFile(filePath: string): GeminiConversationRecord | null {
|
|
521
|
+
try {
|
|
522
|
+
if (fs.statSync(filePath).size > MAX_GEMINI_FILE_BYTES) {
|
|
523
|
+
return null; // too large to parse safely as one JSON document
|
|
524
|
+
}
|
|
525
|
+
return JSON.parse(fs.readFileSync(filePath, 'utf-8')) as GeminiConversationRecord;
|
|
526
|
+
} catch {
|
|
527
|
+
return null;
|
|
528
|
+
}
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
/** Read and parse ~/.gemini/tmp/<hash>/logs.json (a single JSON array). */
|
|
532
|
+
function readLogsJson(logsPath: string): GeminiLogEntry[] {
|
|
533
|
+
try {
|
|
534
|
+
if (fs.statSync(logsPath).size > MAX_GEMINI_FILE_BYTES) {
|
|
535
|
+
return [];
|
|
536
|
+
}
|
|
537
|
+
const parsed: unknown = JSON.parse(fs.readFileSync(logsPath, 'utf-8'));
|
|
538
|
+
return Array.isArray(parsed) ? parsed.filter(isGeminiLogEntry) : [];
|
|
539
|
+
} catch {
|
|
540
|
+
return [];
|
|
541
|
+
}
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
/**
|
|
545
|
+
* Build SessionInfo records for each distinct sessionId found in logs.json.
|
|
546
|
+
*/
|
|
547
|
+
function sessionsFromLogsJson(hashDir: string, cwd: string): SessionInfo[] {
|
|
548
|
+
const logsPath = path.join(hashDir, 'logs.json');
|
|
549
|
+
let stat: fs.Stats;
|
|
550
|
+
try {
|
|
551
|
+
stat = fs.statSync(logsPath);
|
|
552
|
+
} catch {
|
|
553
|
+
return [];
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
const entries = readLogsJson(logsPath);
|
|
557
|
+
if (entries.length === 0) {
|
|
558
|
+
return [];
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
// Group by sessionId; preserve insertion order (chronological).
|
|
562
|
+
const bySession = new Map<string, GeminiLogEntry[]>();
|
|
563
|
+
for (const entry of entries) {
|
|
564
|
+
let bucket = bySession.get(entry.sessionId);
|
|
565
|
+
if (!bucket) {
|
|
566
|
+
bucket = [];
|
|
567
|
+
bySession.set(entry.sessionId, bucket);
|
|
568
|
+
}
|
|
569
|
+
bucket.push(entry);
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
const sessions: SessionInfo[] = [];
|
|
573
|
+
for (const [sessionId, sessionEntries] of bySession) {
|
|
574
|
+
const first = sessionEntries[0];
|
|
575
|
+
const last = sessionEntries[sessionEntries.length - 1];
|
|
576
|
+
sessions.push({
|
|
577
|
+
created: first?.timestamp ?? stat.birthtime.toISOString(),
|
|
578
|
+
gitBranch: '',
|
|
579
|
+
id: sessionId,
|
|
580
|
+
messageCount: sessionEntries.length,
|
|
581
|
+
modified: last?.timestamp ?? stat.mtime.toISOString(),
|
|
582
|
+
projectPath: cwd,
|
|
583
|
+
source: 'gemini' as const,
|
|
584
|
+
summary: '',
|
|
585
|
+
title: cleanTitle(first?.message ?? ''),
|
|
586
|
+
});
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
return sessions;
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
function shortHash(input: string): string {
|
|
593
|
+
return crypto.createHash('sha256').update(input).digest('hex').slice(0, 8);
|
|
594
|
+
}
|
|
@@ -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
|
}
|
|
@@ -2,16 +2,6 @@ import Database from 'better-sqlite3';
|
|
|
2
2
|
import * as crypto from 'crypto';
|
|
3
3
|
import * as fs from 'fs';
|
|
4
4
|
|
|
5
|
-
function shortHash(input: string): string {
|
|
6
|
-
return crypto.createHash('sha256').update(input).digest('hex').slice(0, 8);
|
|
7
|
-
}
|
|
8
|
-
|
|
9
|
-
import type { ConversationMessage, MessagePart, ProjectGroup, SessionInfo } from '$lib/types';
|
|
10
|
-
|
|
11
|
-
import { resolveOpenCodeDbPath } from './opencode-db-path';
|
|
12
|
-
|
|
13
|
-
const OPENCODE_DB_PATH = resolveOpenCodeDbPath();
|
|
14
|
-
|
|
15
5
|
export function getOpenCodeConversation(
|
|
16
6
|
sessionId: string,
|
|
17
7
|
offset = 0,
|
|
@@ -127,6 +117,10 @@ export function getOpenCodeConversation(
|
|
|
127
117
|
}
|
|
128
118
|
}
|
|
129
119
|
|
|
120
|
+
import type { ConversationMessage, MessagePart, ProjectGroup, SessionInfo } from '$lib/types';
|
|
121
|
+
|
|
122
|
+
import { resolveOpenCodeDbPath } from './opencode-db-path';
|
|
123
|
+
|
|
130
124
|
export function listOpenCodeProjects(): ProjectGroup[] {
|
|
131
125
|
const db = getDb();
|
|
132
126
|
if (!db) {
|
|
@@ -251,12 +245,19 @@ function convertOpenCodePart(data: Record<string, unknown>): MessagePart | null
|
|
|
251
245
|
}
|
|
252
246
|
|
|
253
247
|
function getDb(): Database.Database | null {
|
|
254
|
-
|
|
248
|
+
// Resolve per call (not at module load) so a DB created after server start —
|
|
249
|
+
// or an XDG_DATA_HOME change — is still picked up.
|
|
250
|
+
const dbPath = resolveOpenCodeDbPath();
|
|
251
|
+
if (!fs.existsSync(dbPath)) {
|
|
255
252
|
return null;
|
|
256
253
|
}
|
|
257
254
|
try {
|
|
258
|
-
return new Database(
|
|
255
|
+
return new Database(dbPath, { readonly: true });
|
|
259
256
|
} catch {
|
|
260
257
|
return null;
|
|
261
258
|
}
|
|
262
259
|
}
|
|
260
|
+
|
|
261
|
+
function shortHash(input: string): string {
|
|
262
|
+
return crypto.createHash('sha256').update(input).digest('hex').slice(0, 8);
|
|
263
|
+
}
|