@juspay/shooter 1.17.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/build/client/_app/immutable/assets/{0.B0O0vCnX.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.B0O0vCnX.css.gz → 0.NV8k8wxG.css.gz} +0 -0
- package/build/client/_app/immutable/chunks/{BctvtE4d.js → 8lO1IL7u.js} +1 -1
- package/build/client/_app/immutable/chunks/8lO1IL7u.js.br +0 -0
- package/build/client/_app/immutable/chunks/{BctvtE4d.js.gz → 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/{CjfxuHdN.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.CNaTe-zm.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.C3ELOf4c.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.Fqso94b3.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.BusCVJWk.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.DUlpocIc.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.CG4eKRH0.js → 6.B3SEB_li.js} +1 -1
- 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.DHilxD1o.js → 7.DV8cJ1lX.js} +1 -1
- 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.BjKgvSie.js → 8.Bs362gyb.js} +2 -2
- 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.BRT6HOXB.js → 9.Cf7_3uqT.js} +1 -1
- 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-BWFSL107.js → 0-Cd7jY0a7.js} +3 -3
- package/build/server/chunks/{0-BWFSL107.js.map → 0-Cd7jY0a7.js.map} +1 -1
- package/build/server/chunks/{1-Bw5KlAjL.js → 1-C4BOGoJY.js} +2 -2
- package/build/server/chunks/{1-Bw5KlAjL.js.map → 1-C4BOGoJY.js.map} +1 -1
- package/build/server/chunks/{2-CQ3yYSVK.js → 2-Ba0mNwJ6.js} +2 -2
- package/build/server/chunks/{2-CQ3yYSVK.js.map → 2-Ba0mNwJ6.js.map} +1 -1
- package/build/server/chunks/{3-DZ4H9hPs.js → 3-Pg8t1uJU.js} +2 -2
- package/build/server/chunks/{3-DZ4H9hPs.js.map → 3-Pg8t1uJU.js.map} +1 -1
- package/build/server/chunks/{6-BZ0enR6b.js → 6-D8xbnTSo.js} +2 -2
- package/build/server/chunks/{6-BZ0enR6b.js.map → 6-D8xbnTSo.js.map} +1 -1
- package/build/server/chunks/{7-Lg8imTZn.js → 7-CkVK06S0.js} +2 -2
- package/build/server/chunks/{7-Lg8imTZn.js.map → 7-CkVK06S0.js.map} +1 -1
- package/build/server/chunks/{8-DKs4yOL7.js → 8-C8qVhrds.js} +2 -2
- package/build/server/chunks/{8-DKs4yOL7.js.map → 8-C8qVhrds.js.map} +1 -1
- package/build/server/chunks/{9-UNmpUWDY.js → 9-fL5zqN0T.js} +2 -2
- package/build/server/chunks/{9-UNmpUWDY.js.map → 9-fL5zqN0T.js.map} +1 -1
- package/build/server/chunks/{_server.ts-B1z0q6qZ.js → _server.ts-BA_uWcPw.js} +4 -5
- package/build/server/chunks/_server.ts-BA_uWcPw.js.map +1 -0
- package/build/server/chunks/{_server.ts-5wx4ZppI.js → _server.ts-Bu3s5hfv.js} +3 -3
- package/build/server/chunks/{_server.ts-5wx4ZppI.js.map → _server.ts-Bu3s5hfv.js.map} +1 -1
- package/build/server/chunks/{_server.ts-CKXVBbwb.js → _server.ts-CwAjt91u.js} +8 -8
- package/build/server/chunks/_server.ts-CwAjt91u.js.map +1 -0
- package/build/server/chunks/{_server.ts-CgHc1Zpx.js → _server.ts-DZP2lhaY.js} +3 -3
- package/build/server/chunks/{_server.ts-CgHc1Zpx.js.map → _server.ts-DZP2lhaY.js.map} +1 -1
- package/build/server/chunks/{_server.ts-BMMTS86y.js → _server.ts-DZgfQKiH.js} +3 -4
- package/build/server/chunks/{_server.ts-BMMTS86y.js.map → _server.ts-DZgfQKiH.js.map} +1 -1
- package/build/server/chunks/{_server.ts-Bt7EAfjo.js → _server.ts-MbnroWEF.js} +25 -48
- package/build/server/chunks/_server.ts-MbnroWEF.js.map +1 -0
- package/build/server/chunks/{pty-manager-RmhVe2Ez.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/{registry-DzJj2E6I.js → registry-Kcw2UCMv.js} +55 -23
- 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 +15 -15
- 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/provider.ts +0 -2
- package/src/lib/modules/client/terminal/ChatView.svelte +9 -2
- package/src/lib/modules/client/terminal/LaunchSheet.svelte +3 -0
- package/src/lib/modules/server/sessions/amp-reader.ts +439 -0
- 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 +48 -25
- package/src/lib/modules/server/sessions/opencode-reader.ts +13 -12
- package/src/lib/modules/server/sessions/process-detector.ts +37 -60
- package/src/lib/modules/server/sessions/provider-paths.ts +173 -0
- package/src/lib/modules/server/sessions/qwen-reader.ts +41 -15
- package/src/lib/modules/server/sessions/registry.ts +55 -14
- 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 +11 -1
- package/src/lib/theme.css +1 -2
- package/src/lib/types/generated/Sessions.ts +1 -4
- package/src/lib/types/server.ts +23 -6
- package/src/lib/types/sessions.ts +1 -10
- package/src/routes/api/sessions/connect/+server.ts +7 -3
- package/build/client/_app/immutable/assets/0.B0O0vCnX.css.br +0 -0
- package/build/client/_app/immutable/chunks/BctvtE4d.js.br +0 -0
- package/build/client/_app/immutable/chunks/BxFShcQO.js +0 -1
- 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/ByzqAuXw.js +0 -3
- 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/CjfxuHdN.js.br +0 -0
- package/build/client/_app/immutable/chunks/CjfxuHdN.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.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 +0 -1
- 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.C3ELOf4c.js.br +0 -0
- package/build/client/_app/immutable/nodes/0.C3ELOf4c.js.gz +0 -0
- 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.BusCVJWk.js.br +0 -0
- package/build/client/_app/immutable/nodes/2.BusCVJWk.js.gz +0 -0
- 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.br +0 -0
- package/build/client/_app/immutable/nodes/6.CG4eKRH0.js.gz +0 -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.br +0 -0
- package/build/client/_app/immutable/nodes/8.BjKgvSie.js.gz +0 -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/server/chunks/_server.ts-B1z0q6qZ.js.map +0 -1
- package/build/server/chunks/_server.ts-Bt7EAfjo.js.map +0 -1
- package/build/server/chunks/_server.ts-CKXVBbwb.js.map +0 -1
- package/build/server/chunks/opencode-db-path-BwaPufWf.js +0 -411
- package/build/server/chunks/opencode-db-path-BwaPufWf.js.map +0 -1
- package/build/server/chunks/pty-manager-RmhVe2Ez.js.map +0 -1
- package/build/server/chunks/qwen-reader-2fTFuC_D.js +0 -622
- package/build/server/chunks/qwen-reader-2fTFuC_D.js.map +0 -1
- package/build/server/chunks/registry-DzJj2E6I.js.map +0 -1
|
@@ -0,0 +1,2112 @@
|
|
|
1
|
+
import * as crypto from 'crypto';
|
|
2
|
+
import * as fs from 'fs';
|
|
3
|
+
import { existsSync } from 'fs';
|
|
4
|
+
import { homedir } from 'os';
|
|
5
|
+
import * as path from 'path';
|
|
6
|
+
|
|
7
|
+
class TurnBuilder {
|
|
8
|
+
current = null;
|
|
9
|
+
messages = [];
|
|
10
|
+
/** Messages flushed so far (excludes the still-open run). */
|
|
11
|
+
completed() {
|
|
12
|
+
return this.messages;
|
|
13
|
+
}
|
|
14
|
+
flush() {
|
|
15
|
+
if (this.current && this.current.parts.length > 0) {
|
|
16
|
+
this.messages.push({
|
|
17
|
+
id: `codex-${this.messages.length}`,
|
|
18
|
+
parts: this.current.parts,
|
|
19
|
+
role: this.current.role,
|
|
20
|
+
timestamp: this.current.timestamp
|
|
21
|
+
});
|
|
22
|
+
}
|
|
23
|
+
this.current = null;
|
|
24
|
+
}
|
|
25
|
+
push(role, part, timestamp) {
|
|
26
|
+
const category = role === "user" ? "user" : part.type === "tool_result" ? "system" : "assistant";
|
|
27
|
+
const messageRole = category === "system" ? "system" : role;
|
|
28
|
+
if (this.current?.category !== category) {
|
|
29
|
+
this.flush();
|
|
30
|
+
this.current = { category, parts: [], role: messageRole, timestamp };
|
|
31
|
+
}
|
|
32
|
+
this.current.parts.push(part);
|
|
33
|
+
}
|
|
34
|
+
result() {
|
|
35
|
+
this.flush();
|
|
36
|
+
return this.messages;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
function parseCodexMeta(line) {
|
|
40
|
+
try {
|
|
41
|
+
const entry = JSON.parse(line);
|
|
42
|
+
if (!isRecord$3(entry) || entry.type !== "session_meta" || !isRecord$3(entry.payload)) {
|
|
43
|
+
return null;
|
|
44
|
+
}
|
|
45
|
+
const p = entry.payload;
|
|
46
|
+
if (!str(p, "id") || !str(p, "cwd")) {
|
|
47
|
+
return null;
|
|
48
|
+
}
|
|
49
|
+
return {
|
|
50
|
+
cliVersion: str(p, "cli_version"),
|
|
51
|
+
cwd: str(p, "cwd"),
|
|
52
|
+
id: str(p, "id"),
|
|
53
|
+
model: str(p, "model") || str(p, "model_provider"),
|
|
54
|
+
startedAt: str(p, "timestamp") || str(entry, "timestamp")
|
|
55
|
+
};
|
|
56
|
+
} catch {
|
|
57
|
+
return null;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
function parseCodexRollout(text) {
|
|
61
|
+
const builder = new TurnBuilder();
|
|
62
|
+
let meta = null;
|
|
63
|
+
for (const line of text.split("\n")) {
|
|
64
|
+
const trimmed = line.trim();
|
|
65
|
+
if (!trimmed) {
|
|
66
|
+
continue;
|
|
67
|
+
}
|
|
68
|
+
let entry;
|
|
69
|
+
try {
|
|
70
|
+
entry = JSON.parse(trimmed);
|
|
71
|
+
} catch {
|
|
72
|
+
continue;
|
|
73
|
+
}
|
|
74
|
+
if (!isRecord$3(entry)) {
|
|
75
|
+
continue;
|
|
76
|
+
}
|
|
77
|
+
const ts = str(entry, "timestamp");
|
|
78
|
+
if (entry.type === "session_meta") {
|
|
79
|
+
meta = parseCodexMeta(trimmed);
|
|
80
|
+
continue;
|
|
81
|
+
}
|
|
82
|
+
if (entry.type !== "response_item" || !isRecord$3(entry.payload)) {
|
|
83
|
+
continue;
|
|
84
|
+
}
|
|
85
|
+
const mapped = responseItemToPart(entry.payload);
|
|
86
|
+
if (mapped) {
|
|
87
|
+
builder.push(mapped.role, mapped.part, ts);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
return { messages: builder.result(), meta };
|
|
91
|
+
}
|
|
92
|
+
function isRecord$3(value) {
|
|
93
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
94
|
+
}
|
|
95
|
+
function joinMessageText(content) {
|
|
96
|
+
if (!Array.isArray(content)) {
|
|
97
|
+
return typeof content === "string" ? content : "";
|
|
98
|
+
}
|
|
99
|
+
return content.filter((c) => isRecord$3(c)).filter((c) => c.type === "input_text" || c.type === "output_text" || c.type === "text").map((c) => str(c, "text")).join("\n").trim();
|
|
100
|
+
}
|
|
101
|
+
function parseToolInput(raw) {
|
|
102
|
+
if (isRecord$3(raw)) {
|
|
103
|
+
return raw;
|
|
104
|
+
}
|
|
105
|
+
if (typeof raw === "string") {
|
|
106
|
+
try {
|
|
107
|
+
const parsed = JSON.parse(raw);
|
|
108
|
+
return isRecord$3(parsed) ? parsed : { raw };
|
|
109
|
+
} catch {
|
|
110
|
+
return { raw };
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
return {};
|
|
114
|
+
}
|
|
115
|
+
function reasoningText(payload) {
|
|
116
|
+
const summary = payload.summary;
|
|
117
|
+
if (Array.isArray(summary) && summary.length > 0) {
|
|
118
|
+
const text = summary.map((s) => typeof s === "string" ? s : isRecord$3(s) ? str(s, "text") : "").join("\n").trim();
|
|
119
|
+
if (text) {
|
|
120
|
+
return text;
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
return typeof payload.content === "string" ? payload.content.trim() : "";
|
|
124
|
+
}
|
|
125
|
+
function responseItemToPart(p) {
|
|
126
|
+
switch (p.type) {
|
|
127
|
+
case "custom_tool_call":
|
|
128
|
+
case "function_call": {
|
|
129
|
+
const args = p.type === "function_call" ? p.arguments : p.input;
|
|
130
|
+
return {
|
|
131
|
+
part: {
|
|
132
|
+
id: str(p, "call_id"),
|
|
133
|
+
input: parseToolInput(args),
|
|
134
|
+
toolName: str(p, "name") || "tool",
|
|
135
|
+
type: "tool_use"
|
|
136
|
+
},
|
|
137
|
+
role: "assistant"
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
case "custom_tool_call_output":
|
|
141
|
+
case "function_call_output":
|
|
142
|
+
case "tool_search_output": {
|
|
143
|
+
const output = typeof p.output === "string" ? p.output : JSON.stringify(p.output ?? "");
|
|
144
|
+
return {
|
|
145
|
+
part: {
|
|
146
|
+
isError: /exit code:\s*[1-9]/i.test(output),
|
|
147
|
+
output: output.slice(0, 2e3),
|
|
148
|
+
toolUseId: str(p, "call_id"),
|
|
149
|
+
type: "tool_result"
|
|
150
|
+
},
|
|
151
|
+
role: "assistant"
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
case "message": {
|
|
155
|
+
if (p.role === "developer") {
|
|
156
|
+
return null;
|
|
157
|
+
}
|
|
158
|
+
const content = joinMessageText(p.content);
|
|
159
|
+
return content ? { part: { content, type: "text" }, role: p.role === "user" ? "user" : "assistant" } : null;
|
|
160
|
+
}
|
|
161
|
+
case "reasoning": {
|
|
162
|
+
const think = reasoningText(p);
|
|
163
|
+
return think ? { part: { content: think, type: "thinking" }, role: "assistant" } : null;
|
|
164
|
+
}
|
|
165
|
+
case "tool_search_call":
|
|
166
|
+
case "web_search_call":
|
|
167
|
+
return {
|
|
168
|
+
part: {
|
|
169
|
+
id: str(p, "call_id"),
|
|
170
|
+
input: parseToolInput(p.action ?? p.query),
|
|
171
|
+
toolName: str(p, "type"),
|
|
172
|
+
type: "tool_use"
|
|
173
|
+
},
|
|
174
|
+
role: "assistant"
|
|
175
|
+
};
|
|
176
|
+
default:
|
|
177
|
+
return null;
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
function str(obj, key) {
|
|
181
|
+
const v = obj[key];
|
|
182
|
+
return typeof v === "string" ? v : "";
|
|
183
|
+
}
|
|
184
|
+
const LIST_PREFIX_BYTES = 256 * 1024;
|
|
185
|
+
const MAX_FULL_READ_BYTES$2 = 16 * 1024 * 1024;
|
|
186
|
+
const APPROX_BYTES_PER_MESSAGE = 3e3;
|
|
187
|
+
const SYNTHETIC_PROMPT_PREFIXES = ["<environment_context>", "<user_instructions>", "<permissions"];
|
|
188
|
+
function detectActiveCodexSessions(thresholdMs) {
|
|
189
|
+
const cutoff = Date.now() - thresholdMs;
|
|
190
|
+
const out = [];
|
|
191
|
+
for (const filePath of collectRolloutFiles()) {
|
|
192
|
+
try {
|
|
193
|
+
const stat = fs.statSync(filePath);
|
|
194
|
+
if (stat.mtimeMs < cutoff) {
|
|
195
|
+
continue;
|
|
196
|
+
}
|
|
197
|
+
const meta = parseCodexMeta(readPrefix$3(filePath).split("\n")[0] ?? "");
|
|
198
|
+
if (meta) {
|
|
199
|
+
out.push({ cwd: meta.cwd, id: meta.id, startedAt: stat.birthtimeMs });
|
|
200
|
+
}
|
|
201
|
+
} catch {
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
return out;
|
|
205
|
+
}
|
|
206
|
+
function findCodexRolloutById(sessionId) {
|
|
207
|
+
if (!/^[0-9a-f-]+$/i.test(sessionId)) {
|
|
208
|
+
return null;
|
|
209
|
+
}
|
|
210
|
+
const suffix = `-${sessionId}.jsonl`;
|
|
211
|
+
return collectRolloutFiles().find((p) => path.basename(p).endsWith(suffix)) ?? null;
|
|
212
|
+
}
|
|
213
|
+
function findCodexRolloutForCwd(cwd, sinceMs) {
|
|
214
|
+
let best = null;
|
|
215
|
+
for (const filePath of collectRolloutFiles()) {
|
|
216
|
+
try {
|
|
217
|
+
const stat = fs.statSync(filePath);
|
|
218
|
+
if (stat.birthtimeMs <= sinceMs) {
|
|
219
|
+
continue;
|
|
220
|
+
}
|
|
221
|
+
const meta = parseCodexMeta(readPrefix$3(filePath).split("\n")[0] ?? "");
|
|
222
|
+
if (meta?.cwd === cwd && (!best || stat.birthtimeMs > best.birthtime)) {
|
|
223
|
+
best = { birthtime: stat.birthtimeMs, path: filePath };
|
|
224
|
+
}
|
|
225
|
+
} catch {
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
return best?.path ?? null;
|
|
229
|
+
}
|
|
230
|
+
function getCodexConversation(sessionId, offset = 0, limit = 200) {
|
|
231
|
+
const filePath = findCodexRolloutById(sessionId);
|
|
232
|
+
if (!filePath || !fs.existsSync(filePath)) {
|
|
233
|
+
return [];
|
|
234
|
+
}
|
|
235
|
+
try {
|
|
236
|
+
const { messages } = parseCodexRollout(readBoundedRolloutText(filePath));
|
|
237
|
+
if (offset === 0 && messages.length > limit) {
|
|
238
|
+
let startIdx = messages.length - limit;
|
|
239
|
+
while (startIdx > 0 && messages[startIdx].role !== "user") {
|
|
240
|
+
startIdx--;
|
|
241
|
+
}
|
|
242
|
+
return messages.slice(startIdx);
|
|
243
|
+
}
|
|
244
|
+
return messages.slice(offset, offset + limit);
|
|
245
|
+
} catch (error) {
|
|
246
|
+
console.error("[codex] Failed to read conversation:", error);
|
|
247
|
+
return [];
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
function listCodexProjects() {
|
|
251
|
+
const files = collectRolloutFiles();
|
|
252
|
+
const byCwd = /* @__PURE__ */ new Map();
|
|
253
|
+
for (const filePath of files) {
|
|
254
|
+
let stat;
|
|
255
|
+
try {
|
|
256
|
+
stat = fs.statSync(filePath);
|
|
257
|
+
} catch {
|
|
258
|
+
continue;
|
|
259
|
+
}
|
|
260
|
+
let prefix;
|
|
261
|
+
try {
|
|
262
|
+
prefix = readPrefix$3(filePath);
|
|
263
|
+
} catch {
|
|
264
|
+
continue;
|
|
265
|
+
}
|
|
266
|
+
const nlIdx = prefix.indexOf("\n");
|
|
267
|
+
const firstLine = nlIdx === -1 ? prefix : prefix.slice(0, nlIdx);
|
|
268
|
+
const meta = parseCodexMeta(firstLine);
|
|
269
|
+
if (!meta) {
|
|
270
|
+
continue;
|
|
271
|
+
}
|
|
272
|
+
const session = {
|
|
273
|
+
created: meta.startedAt || stat.birthtime.toISOString(),
|
|
274
|
+
gitBranch: "",
|
|
275
|
+
id: meta.id,
|
|
276
|
+
messageCount: Math.max(1, Math.round(stat.size / APPROX_BYTES_PER_MESSAGE)),
|
|
277
|
+
modified: stat.mtime.toISOString(),
|
|
278
|
+
projectPath: meta.cwd,
|
|
279
|
+
source: "codex",
|
|
280
|
+
summary: "",
|
|
281
|
+
title: cleanTitle$2(firstUserPrompt(prefix))
|
|
282
|
+
};
|
|
283
|
+
const bucket = byCwd.get(meta.cwd);
|
|
284
|
+
if (bucket) {
|
|
285
|
+
bucket.push(session);
|
|
286
|
+
} else {
|
|
287
|
+
byCwd.set(meta.cwd, [session]);
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
const projects = [];
|
|
291
|
+
for (const [cwd, sessions] of byCwd) {
|
|
292
|
+
sessions.sort((a, b) => new Date(b.modified).getTime() - new Date(a.modified).getTime());
|
|
293
|
+
const segments = cwd.split("/").filter(Boolean);
|
|
294
|
+
projects.push({
|
|
295
|
+
fullPath: cwd,
|
|
296
|
+
id: shortHash$5(cwd),
|
|
297
|
+
lastModified: sessions[0]?.modified ?? "",
|
|
298
|
+
name: segments.slice(-2).join("/"),
|
|
299
|
+
sessionCount: sessions.length,
|
|
300
|
+
sessions
|
|
301
|
+
});
|
|
302
|
+
}
|
|
303
|
+
return projects.sort(
|
|
304
|
+
(a, b) => new Date(b.lastModified).getTime() - new Date(a.lastModified).getTime()
|
|
305
|
+
);
|
|
306
|
+
}
|
|
307
|
+
function readBoundedRolloutText(filePath) {
|
|
308
|
+
const stat = fs.statSync(filePath);
|
|
309
|
+
if (stat.size <= MAX_FULL_READ_BYTES$2) {
|
|
310
|
+
return fs.readFileSync(filePath, "utf-8");
|
|
311
|
+
}
|
|
312
|
+
const head = readPrefix$3(filePath).split("\n")[0] ?? "";
|
|
313
|
+
const fd = fs.openSync(filePath, "r");
|
|
314
|
+
try {
|
|
315
|
+
const start = stat.size - MAX_FULL_READ_BYTES$2;
|
|
316
|
+
const buf = Buffer.alloc(MAX_FULL_READ_BYTES$2);
|
|
317
|
+
const bytesRead = fs.readSync(fd, buf, 0, MAX_FULL_READ_BYTES$2, start);
|
|
318
|
+
const tail = buf.toString("utf-8", 0, bytesRead);
|
|
319
|
+
return `${head}
|
|
320
|
+
${tail.slice(tail.indexOf("\n") + 1)}`;
|
|
321
|
+
} finally {
|
|
322
|
+
fs.closeSync(fd);
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
function cleanTitle$2(prompt) {
|
|
326
|
+
const firstLine = prompt.replace(/\s+/g, " ").trim().split("\n")[0]?.trim() ?? "";
|
|
327
|
+
if (!firstLine) {
|
|
328
|
+
return "Untitled Session";
|
|
329
|
+
}
|
|
330
|
+
return firstLine.length > 80 ? `${firstLine.slice(0, 77)}...` : firstLine;
|
|
331
|
+
}
|
|
332
|
+
function codexSessionsDirs() {
|
|
333
|
+
const home = homedir();
|
|
334
|
+
return [path.join(home, ".codex", "sessions"), path.join(home, ".codex", "archived_sessions")];
|
|
335
|
+
}
|
|
336
|
+
function collectRolloutFiles() {
|
|
337
|
+
const out = [];
|
|
338
|
+
const walk = (dir) => {
|
|
339
|
+
let entries;
|
|
340
|
+
try {
|
|
341
|
+
entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
342
|
+
} catch {
|
|
343
|
+
return;
|
|
344
|
+
}
|
|
345
|
+
for (const entry of entries) {
|
|
346
|
+
const full = path.join(dir, entry.name);
|
|
347
|
+
if (entry.isDirectory()) {
|
|
348
|
+
walk(full);
|
|
349
|
+
} else if (entry.name.startsWith("rollout-") && entry.name.endsWith(".jsonl")) {
|
|
350
|
+
out.push(full);
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
};
|
|
354
|
+
for (const root of codexSessionsDirs()) {
|
|
355
|
+
walk(root);
|
|
356
|
+
}
|
|
357
|
+
return out;
|
|
358
|
+
}
|
|
359
|
+
function firstUserPrompt(prefixText) {
|
|
360
|
+
for (const line of prefixText.split("\n")) {
|
|
361
|
+
const trimmed = line.trim();
|
|
362
|
+
if (!trimmed || !trimmed.includes('"type":"message"') || !trimmed.includes('"role":"user"')) {
|
|
363
|
+
continue;
|
|
364
|
+
}
|
|
365
|
+
try {
|
|
366
|
+
const entry = JSON.parse(trimmed);
|
|
367
|
+
const text = (entry.payload?.content ?? []).filter((c) => c.type === "input_text" && typeof c.text === "string").map((c) => c.text ?? "").join("\n").trim();
|
|
368
|
+
if (text && !SYNTHETIC_PROMPT_PREFIXES.some((p) => text.startsWith(p))) {
|
|
369
|
+
return text;
|
|
370
|
+
}
|
|
371
|
+
} catch {
|
|
372
|
+
continue;
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
return "";
|
|
376
|
+
}
|
|
377
|
+
function readPrefix$3(filePath) {
|
|
378
|
+
const fd = fs.openSync(filePath, "r");
|
|
379
|
+
try {
|
|
380
|
+
const buf = Buffer.alloc(LIST_PREFIX_BYTES);
|
|
381
|
+
const bytesRead = fs.readSync(fd, buf, 0, LIST_PREFIX_BYTES, 0);
|
|
382
|
+
const text = buf.toString("utf-8", 0, bytesRead);
|
|
383
|
+
const lastNl = text.lastIndexOf("\n");
|
|
384
|
+
return lastNl === -1 ? text : text.slice(0, lastNl);
|
|
385
|
+
} finally {
|
|
386
|
+
fs.closeSync(fd);
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
function shortHash$5(input) {
|
|
390
|
+
return crypto.createHash("sha256").update(input).digest("hex").slice(0, 8);
|
|
391
|
+
}
|
|
392
|
+
const AMP_THREADS = path.join(homedir(), ".local", "share", "amp", "threads");
|
|
393
|
+
const MAX_FILE_BYTES = 64 * 1024 * 1024;
|
|
394
|
+
const THREAD_FILE_RE = /^T-(.+)\.json$/;
|
|
395
|
+
const SESSION_ID_RE$1 = /^[A-Za-z0-9_.-]+$/;
|
|
396
|
+
function detectActiveAmpSessions(thresholdMs) {
|
|
397
|
+
const cutoff = Date.now() - thresholdMs;
|
|
398
|
+
const out = [];
|
|
399
|
+
for (const filePath of collectThreadFiles()) {
|
|
400
|
+
try {
|
|
401
|
+
const stat = fs.statSync(filePath);
|
|
402
|
+
if (stat.mtimeMs < cutoff) {
|
|
403
|
+
continue;
|
|
404
|
+
}
|
|
405
|
+
const doc = readThreadDoc(filePath);
|
|
406
|
+
if (!doc) {
|
|
407
|
+
continue;
|
|
408
|
+
}
|
|
409
|
+
const id = threadId(filePath, doc);
|
|
410
|
+
const cwd = projectCwd(doc);
|
|
411
|
+
if (id && cwd) {
|
|
412
|
+
out.push({ cwd, id, startedAt: stat.birthtimeMs });
|
|
413
|
+
}
|
|
414
|
+
} catch {
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
return out;
|
|
418
|
+
}
|
|
419
|
+
function getAmpConversation(sessionId, offset = 0, limit = 200) {
|
|
420
|
+
if (!SESSION_ID_RE$1.test(sessionId)) {
|
|
421
|
+
return [];
|
|
422
|
+
}
|
|
423
|
+
const filePath = path.join(AMP_THREADS, `T-${sessionId}.json`);
|
|
424
|
+
if (!fs.existsSync(filePath)) {
|
|
425
|
+
return [];
|
|
426
|
+
}
|
|
427
|
+
try {
|
|
428
|
+
const doc = readThreadDoc(filePath);
|
|
429
|
+
if (!doc) {
|
|
430
|
+
return [];
|
|
431
|
+
}
|
|
432
|
+
const rawMessages = Array.isArray(doc.messages) ? doc.messages : [];
|
|
433
|
+
const messages = [];
|
|
434
|
+
let idx = 0;
|
|
435
|
+
for (const raw of rawMessages) {
|
|
436
|
+
const msg = ampMessageToConversationMessage(raw, idx);
|
|
437
|
+
if (msg) {
|
|
438
|
+
messages.push(msg);
|
|
439
|
+
idx++;
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
if (offset === 0 && messages.length > limit) {
|
|
443
|
+
let startIdx = messages.length - limit;
|
|
444
|
+
while (startIdx > 0 && messages[startIdx]?.role !== "user") {
|
|
445
|
+
startIdx--;
|
|
446
|
+
}
|
|
447
|
+
return messages.slice(startIdx);
|
|
448
|
+
}
|
|
449
|
+
return messages.slice(offset, offset + limit);
|
|
450
|
+
} catch (err) {
|
|
451
|
+
console.error("[amp] Failed to read conversation:", err);
|
|
452
|
+
return [];
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
function listAmpProjects() {
|
|
456
|
+
const byCwd = /* @__PURE__ */ new Map();
|
|
457
|
+
for (const filePath of collectThreadFiles()) {
|
|
458
|
+
let stat;
|
|
459
|
+
try {
|
|
460
|
+
stat = fs.statSync(filePath);
|
|
461
|
+
} catch {
|
|
462
|
+
continue;
|
|
463
|
+
}
|
|
464
|
+
const doc = readThreadDoc(filePath);
|
|
465
|
+
if (!doc) {
|
|
466
|
+
continue;
|
|
467
|
+
}
|
|
468
|
+
const id = threadId(filePath, doc);
|
|
469
|
+
if (!id) {
|
|
470
|
+
continue;
|
|
471
|
+
}
|
|
472
|
+
const cwd = projectCwd(doc) || id;
|
|
473
|
+
const modified = lastModified(doc, stat);
|
|
474
|
+
const created = typeof doc.created === "string" && doc.created ? doc.created : stat.birthtime.toISOString();
|
|
475
|
+
const rawMessages = Array.isArray(doc.messages) ? doc.messages : [];
|
|
476
|
+
const session = {
|
|
477
|
+
created,
|
|
478
|
+
gitBranch: "",
|
|
479
|
+
id,
|
|
480
|
+
messageCount: rawMessages.length,
|
|
481
|
+
modified,
|
|
482
|
+
projectPath: cwd,
|
|
483
|
+
source: "amp",
|
|
484
|
+
summary: "",
|
|
485
|
+
title: cleanTitle$1(typeof doc.title === "string" ? doc.title : "")
|
|
486
|
+
};
|
|
487
|
+
const bucket = byCwd.get(cwd);
|
|
488
|
+
if (bucket) {
|
|
489
|
+
bucket.push(session);
|
|
490
|
+
} else {
|
|
491
|
+
byCwd.set(cwd, [session]);
|
|
492
|
+
}
|
|
493
|
+
}
|
|
494
|
+
const projects = [];
|
|
495
|
+
for (const [cwd, sessions] of byCwd) {
|
|
496
|
+
sessions.sort((a, b) => new Date(b.modified).getTime() - new Date(a.modified).getTime());
|
|
497
|
+
const segments = cwd.split("/").filter(Boolean);
|
|
498
|
+
projects.push({
|
|
499
|
+
fullPath: cwd,
|
|
500
|
+
id: shortHash$4(cwd),
|
|
501
|
+
lastModified: sessions[0]?.modified ?? "",
|
|
502
|
+
name: segments.slice(-2).join("/") || cwd,
|
|
503
|
+
sessionCount: sessions.length,
|
|
504
|
+
sessions
|
|
505
|
+
});
|
|
506
|
+
}
|
|
507
|
+
return projects.sort(
|
|
508
|
+
(a, b) => new Date(b.lastModified).getTime() - new Date(a.lastModified).getTime()
|
|
509
|
+
);
|
|
510
|
+
}
|
|
511
|
+
function resolveAmpSessionFile(sessionId) {
|
|
512
|
+
if (!SESSION_ID_RE$1.test(sessionId)) {
|
|
513
|
+
return null;
|
|
514
|
+
}
|
|
515
|
+
const filePath = path.join(AMP_THREADS, `T-${sessionId}.json`);
|
|
516
|
+
return fs.existsSync(filePath) ? filePath : null;
|
|
517
|
+
}
|
|
518
|
+
function ampBlockToPart(block) {
|
|
519
|
+
switch (block.type) {
|
|
520
|
+
case "text":
|
|
521
|
+
return { content: typeof block.text === "string" ? block.text : "", type: "text" };
|
|
522
|
+
case "thinking":
|
|
523
|
+
return {
|
|
524
|
+
content: typeof block.thinking === "string" ? block.thinking : "",
|
|
525
|
+
type: "thinking"
|
|
526
|
+
};
|
|
527
|
+
case "tool_result": {
|
|
528
|
+
const output = extractToolResultText(block.content);
|
|
529
|
+
return {
|
|
530
|
+
isError: typeof block.is_error === "boolean" ? block.is_error : false,
|
|
531
|
+
output: output.slice(0, 2e3),
|
|
532
|
+
toolUseId: typeof block.tool_use_id === "string" ? block.tool_use_id : "",
|
|
533
|
+
type: "tool_result"
|
|
534
|
+
};
|
|
535
|
+
}
|
|
536
|
+
case "tool_use":
|
|
537
|
+
return {
|
|
538
|
+
id: typeof block.id === "string" ? block.id : "",
|
|
539
|
+
input: isRecord$2(block.input) ? block.input : {},
|
|
540
|
+
toolName: typeof block.name === "string" ? block.name : "tool",
|
|
541
|
+
type: "tool_use"
|
|
542
|
+
};
|
|
543
|
+
default:
|
|
544
|
+
return null;
|
|
545
|
+
}
|
|
546
|
+
}
|
|
547
|
+
function ampMessageToConversationMessage(raw, index) {
|
|
548
|
+
if (!isRecord$2(raw)) {
|
|
549
|
+
return null;
|
|
550
|
+
}
|
|
551
|
+
const role = raw.role;
|
|
552
|
+
if (role !== "user" && role !== "assistant") {
|
|
553
|
+
return null;
|
|
554
|
+
}
|
|
555
|
+
const parts = contentToParts(raw.content);
|
|
556
|
+
if (parts.length === 0) {
|
|
557
|
+
return null;
|
|
558
|
+
}
|
|
559
|
+
const msgRole = role === "user" ? "user" : "assistant";
|
|
560
|
+
return {
|
|
561
|
+
id: `amp-${msgRole}-${index}`,
|
|
562
|
+
parts,
|
|
563
|
+
role: msgRole,
|
|
564
|
+
timestamp: ""
|
|
565
|
+
};
|
|
566
|
+
}
|
|
567
|
+
function cleanTitle$1(raw) {
|
|
568
|
+
const first = raw.replace(/\s+/g, " ").trim().split("\n")[0]?.trim() ?? "";
|
|
569
|
+
if (!first) {
|
|
570
|
+
return "Untitled Session";
|
|
571
|
+
}
|
|
572
|
+
return first.length > 80 ? `${first.slice(0, 77)}...` : first;
|
|
573
|
+
}
|
|
574
|
+
function collectThreadFiles() {
|
|
575
|
+
let entries;
|
|
576
|
+
try {
|
|
577
|
+
entries = fs.readdirSync(AMP_THREADS, { withFileTypes: true });
|
|
578
|
+
} catch {
|
|
579
|
+
return [];
|
|
580
|
+
}
|
|
581
|
+
const files = [];
|
|
582
|
+
for (const entry of entries) {
|
|
583
|
+
if (!entry.isFile() || !THREAD_FILE_RE.test(entry.name)) {
|
|
584
|
+
continue;
|
|
585
|
+
}
|
|
586
|
+
const full = path.join(AMP_THREADS, entry.name);
|
|
587
|
+
try {
|
|
588
|
+
const stat = fs.statSync(full);
|
|
589
|
+
files.push({ mtime: stat.mtimeMs, path: full });
|
|
590
|
+
} catch {
|
|
591
|
+
}
|
|
592
|
+
}
|
|
593
|
+
files.sort((a, b) => b.mtime - a.mtime);
|
|
594
|
+
return files.map((f) => f.path);
|
|
595
|
+
}
|
|
596
|
+
function contentToParts(content) {
|
|
597
|
+
if (typeof content === "string" && content) {
|
|
598
|
+
return [{ content, type: "text" }];
|
|
599
|
+
}
|
|
600
|
+
if (!Array.isArray(content)) {
|
|
601
|
+
return [];
|
|
602
|
+
}
|
|
603
|
+
const parts = [];
|
|
604
|
+
for (const item of content) {
|
|
605
|
+
if (!isRecord$2(item)) {
|
|
606
|
+
continue;
|
|
607
|
+
}
|
|
608
|
+
const part = ampBlockToPart(item);
|
|
609
|
+
if (part) {
|
|
610
|
+
parts.push(part);
|
|
611
|
+
}
|
|
612
|
+
}
|
|
613
|
+
return parts;
|
|
614
|
+
}
|
|
615
|
+
function extractToolResultText(content) {
|
|
616
|
+
if (typeof content === "string") {
|
|
617
|
+
return content;
|
|
618
|
+
}
|
|
619
|
+
if (!Array.isArray(content)) {
|
|
620
|
+
return "";
|
|
621
|
+
}
|
|
622
|
+
return content.filter((c) => isRecord$2(c) && c.type === "text").map((c) => typeof c.text === "string" ? c.text : "").join("\n");
|
|
623
|
+
}
|
|
624
|
+
function isRecord$2(value) {
|
|
625
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
626
|
+
}
|
|
627
|
+
function lastModified(doc, stat) {
|
|
628
|
+
if (isRecord$2(doc.meta)) {
|
|
629
|
+
const traces = doc.meta.traces;
|
|
630
|
+
if (Array.isArray(traces) && traces.length > 0) {
|
|
631
|
+
const last = traces[traces.length - 1];
|
|
632
|
+
if (isRecord$2(last)) {
|
|
633
|
+
const et = last.endTime;
|
|
634
|
+
if (typeof et === "string" && et) {
|
|
635
|
+
return et;
|
|
636
|
+
}
|
|
637
|
+
if (typeof et === "number" && et > 0) {
|
|
638
|
+
return new Date(et).toISOString();
|
|
639
|
+
}
|
|
640
|
+
}
|
|
641
|
+
}
|
|
642
|
+
}
|
|
643
|
+
return stat.mtime.toISOString();
|
|
644
|
+
}
|
|
645
|
+
function projectCwd(doc) {
|
|
646
|
+
if (isRecord$2(doc.env) && isRecord$2(doc.env.initial)) {
|
|
647
|
+
const trees = doc.env.initial.trees;
|
|
648
|
+
if (Array.isArray(trees) && trees.length > 0 && isRecord$2(trees[0])) {
|
|
649
|
+
const dn = trees[0].displayName;
|
|
650
|
+
if (typeof dn === "string" && dn) {
|
|
651
|
+
return dn;
|
|
652
|
+
}
|
|
653
|
+
}
|
|
654
|
+
}
|
|
655
|
+
return "";
|
|
656
|
+
}
|
|
657
|
+
function readThreadDoc(filePath) {
|
|
658
|
+
try {
|
|
659
|
+
if (fs.statSync(filePath).size > MAX_FILE_BYTES) {
|
|
660
|
+
return null;
|
|
661
|
+
}
|
|
662
|
+
const parsed = JSON.parse(fs.readFileSync(filePath, "utf-8"));
|
|
663
|
+
return isRecord$2(parsed) ? parsed : null;
|
|
664
|
+
} catch {
|
|
665
|
+
return null;
|
|
666
|
+
}
|
|
667
|
+
}
|
|
668
|
+
function shortHash$4(input) {
|
|
669
|
+
return crypto.createHash("sha256").update(input).digest("hex").slice(0, 8);
|
|
670
|
+
}
|
|
671
|
+
function threadId(filePath, doc) {
|
|
672
|
+
const m = THREAD_FILE_RE.exec(path.basename(filePath));
|
|
673
|
+
if (m?.[1]) {
|
|
674
|
+
return m[1];
|
|
675
|
+
}
|
|
676
|
+
if (typeof doc.id === "string" && doc.id) {
|
|
677
|
+
return doc.id;
|
|
678
|
+
}
|
|
679
|
+
return "";
|
|
680
|
+
}
|
|
681
|
+
const PREFIX_BYTES$2 = 64 * 1024;
|
|
682
|
+
const MAX_FULL_READ_BYTES$1 = 16 * 1024 * 1024;
|
|
683
|
+
const COPILOT_SESSION_STATE = path.join(homedir(), ".copilot", "session-state");
|
|
684
|
+
const SESSION_ID_RE = /^[A-Za-z0-9_.-]+$/;
|
|
685
|
+
function detectActiveCopilotSessions(thresholdMs) {
|
|
686
|
+
const cutoff = Date.now() - thresholdMs;
|
|
687
|
+
const out = [];
|
|
688
|
+
for (const filePath of collectCopilotFiles()) {
|
|
689
|
+
try {
|
|
690
|
+
const stat = fs.statSync(filePath);
|
|
691
|
+
if (stat.mtimeMs < cutoff) {
|
|
692
|
+
continue;
|
|
693
|
+
}
|
|
694
|
+
const meta = readMeta$1(readPrefix$2(filePath), filePath);
|
|
695
|
+
if (meta) {
|
|
696
|
+
out.push({ cwd: meta.cwd, id: meta.id, startedAt: stat.birthtimeMs });
|
|
697
|
+
}
|
|
698
|
+
} catch {
|
|
699
|
+
}
|
|
700
|
+
}
|
|
701
|
+
return out;
|
|
702
|
+
}
|
|
703
|
+
function getCopilotConversation(sessionId, offset = 0, limit = 200) {
|
|
704
|
+
if (!SESSION_ID_RE.test(sessionId)) {
|
|
705
|
+
return [];
|
|
706
|
+
}
|
|
707
|
+
const filePath = resolveCopilotSessionFile(sessionId);
|
|
708
|
+
if (!filePath) {
|
|
709
|
+
return [];
|
|
710
|
+
}
|
|
711
|
+
try {
|
|
712
|
+
const text = readBounded$1(filePath);
|
|
713
|
+
const messages = parseCopilotText(text);
|
|
714
|
+
if (offset === 0 && messages.length > limit) {
|
|
715
|
+
let startIdx = messages.length - limit;
|
|
716
|
+
while (startIdx > 0 && messages[startIdx]?.role !== "user") {
|
|
717
|
+
startIdx--;
|
|
718
|
+
}
|
|
719
|
+
return messages.slice(startIdx);
|
|
720
|
+
}
|
|
721
|
+
return messages.slice(offset, offset + limit);
|
|
722
|
+
} catch (error) {
|
|
723
|
+
console.error("[copilot] Failed to read conversation:", error);
|
|
724
|
+
return [];
|
|
725
|
+
}
|
|
726
|
+
}
|
|
727
|
+
function listCopilotProjects() {
|
|
728
|
+
const byCwd = /* @__PURE__ */ new Map();
|
|
729
|
+
for (const filePath of collectCopilotFiles()) {
|
|
730
|
+
let stat;
|
|
731
|
+
try {
|
|
732
|
+
stat = fs.statSync(filePath);
|
|
733
|
+
} catch {
|
|
734
|
+
continue;
|
|
735
|
+
}
|
|
736
|
+
const prefix = safeReadPrefix(filePath);
|
|
737
|
+
if (!prefix) {
|
|
738
|
+
continue;
|
|
739
|
+
}
|
|
740
|
+
const meta = readMeta$1(prefix, filePath);
|
|
741
|
+
if (!meta) {
|
|
742
|
+
continue;
|
|
743
|
+
}
|
|
744
|
+
const session = {
|
|
745
|
+
created: stat.birthtime.toISOString(),
|
|
746
|
+
gitBranch: meta.gitBranch,
|
|
747
|
+
id: meta.id,
|
|
748
|
+
messageCount: 0,
|
|
749
|
+
modified: stat.mtime.toISOString(),
|
|
750
|
+
projectPath: meta.cwd,
|
|
751
|
+
source: "copilot",
|
|
752
|
+
summary: "",
|
|
753
|
+
title: meta.title || "Untitled Session"
|
|
754
|
+
};
|
|
755
|
+
const bucket = byCwd.get(meta.cwd);
|
|
756
|
+
if (bucket) {
|
|
757
|
+
bucket.push(session);
|
|
758
|
+
} else {
|
|
759
|
+
byCwd.set(meta.cwd, [session]);
|
|
760
|
+
}
|
|
761
|
+
}
|
|
762
|
+
const projects = [];
|
|
763
|
+
for (const [cwd, sessions] of byCwd) {
|
|
764
|
+
sessions.sort((a, b) => new Date(b.modified).getTime() - new Date(a.modified).getTime());
|
|
765
|
+
const segments = cwd.split("/").filter(Boolean);
|
|
766
|
+
projects.push({
|
|
767
|
+
fullPath: cwd,
|
|
768
|
+
id: shortHash$3(cwd),
|
|
769
|
+
lastModified: sessions[0]?.modified ?? "",
|
|
770
|
+
name: segments.slice(-2).join("/"),
|
|
771
|
+
sessionCount: sessions.length,
|
|
772
|
+
sessions
|
|
773
|
+
});
|
|
774
|
+
}
|
|
775
|
+
return projects.sort(
|
|
776
|
+
(a, b) => new Date(b.lastModified).getTime() - new Date(a.lastModified).getTime()
|
|
777
|
+
);
|
|
778
|
+
}
|
|
779
|
+
function resolveCopilotSessionFile(sessionId) {
|
|
780
|
+
if (!SESSION_ID_RE.test(sessionId)) {
|
|
781
|
+
return null;
|
|
782
|
+
}
|
|
783
|
+
for (const filePath of collectCopilotFiles()) {
|
|
784
|
+
if (sessionIdFromFilePath(filePath) === sessionId) {
|
|
785
|
+
return filePath;
|
|
786
|
+
}
|
|
787
|
+
}
|
|
788
|
+
return null;
|
|
789
|
+
}
|
|
790
|
+
function collectCopilotFiles() {
|
|
791
|
+
const out = [];
|
|
792
|
+
let entries;
|
|
793
|
+
try {
|
|
794
|
+
entries = fs.readdirSync(COPILOT_SESSION_STATE, { withFileTypes: true });
|
|
795
|
+
} catch {
|
|
796
|
+
return out;
|
|
797
|
+
}
|
|
798
|
+
for (const entry of entries) {
|
|
799
|
+
const full = path.join(COPILOT_SESSION_STATE, entry.name);
|
|
800
|
+
if (entry.isFile() && entry.name.endsWith(".jsonl")) {
|
|
801
|
+
out.push(full);
|
|
802
|
+
} else if (entry.isDirectory()) {
|
|
803
|
+
const eventsPath = path.join(full, "events.jsonl");
|
|
804
|
+
try {
|
|
805
|
+
fs.statSync(eventsPath);
|
|
806
|
+
out.push(eventsPath);
|
|
807
|
+
} catch {
|
|
808
|
+
}
|
|
809
|
+
}
|
|
810
|
+
}
|
|
811
|
+
return out;
|
|
812
|
+
}
|
|
813
|
+
function copilotEventToMessage(entry, pendingToolUseId) {
|
|
814
|
+
const type = typeof entry.type === "string" ? entry.type : "";
|
|
815
|
+
const ts = typeof entry.timestamp === "string" ? entry.timestamp : "";
|
|
816
|
+
const data = isRecord$1(entry.data) ? entry.data : entry;
|
|
817
|
+
if (type === "user.message") {
|
|
818
|
+
const content = typeof data.content === "string" ? data.content : "";
|
|
819
|
+
if (!content) {
|
|
820
|
+
return null;
|
|
821
|
+
}
|
|
822
|
+
const id = typeof data.id === "string" ? data.id : `copilot-user-${ts}`;
|
|
823
|
+
return {
|
|
824
|
+
id,
|
|
825
|
+
parts: [{ content, type: "text" }],
|
|
826
|
+
role: "user",
|
|
827
|
+
timestamp: ts
|
|
828
|
+
};
|
|
829
|
+
}
|
|
830
|
+
if (type === "assistant.reasoning") {
|
|
831
|
+
const text = typeof data.text === "string" ? data.text : "";
|
|
832
|
+
if (!text) {
|
|
833
|
+
return null;
|
|
834
|
+
}
|
|
835
|
+
const id = typeof data.id === "string" ? data.id : `copilot-reasoning-${ts}`;
|
|
836
|
+
return {
|
|
837
|
+
id,
|
|
838
|
+
parts: [{ content: text, type: "thinking" }],
|
|
839
|
+
role: "assistant",
|
|
840
|
+
timestamp: ts
|
|
841
|
+
};
|
|
842
|
+
}
|
|
843
|
+
if (type === "assistant.message") {
|
|
844
|
+
const content = typeof data.content === "string" ? data.content : "";
|
|
845
|
+
const parts = [];
|
|
846
|
+
const reasoningText2 = typeof data.reasoningText === "string" ? data.reasoningText : "";
|
|
847
|
+
if (reasoningText2) {
|
|
848
|
+
parts.push({ content: reasoningText2, type: "thinking" });
|
|
849
|
+
}
|
|
850
|
+
if (content) {
|
|
851
|
+
parts.push({ content, type: "text" });
|
|
852
|
+
}
|
|
853
|
+
if (Array.isArray(data.toolRequests)) {
|
|
854
|
+
for (const req of data.toolRequests) {
|
|
855
|
+
if (!isRecord$1(req)) {
|
|
856
|
+
continue;
|
|
857
|
+
}
|
|
858
|
+
const toolId = typeof req.toolCallId === "string" ? req.toolCallId : `copilot-tool-${ts}`;
|
|
859
|
+
pendingToolUseId.value = toolId;
|
|
860
|
+
let input = {};
|
|
861
|
+
if (isRecord$1(req.arguments)) {
|
|
862
|
+
input = req.arguments;
|
|
863
|
+
} else if (typeof req.arguments === "string") {
|
|
864
|
+
try {
|
|
865
|
+
const parsed = JSON.parse(req.arguments);
|
|
866
|
+
if (isRecord$1(parsed)) {
|
|
867
|
+
input = parsed;
|
|
868
|
+
}
|
|
869
|
+
} catch {
|
|
870
|
+
}
|
|
871
|
+
}
|
|
872
|
+
parts.push({
|
|
873
|
+
id: toolId,
|
|
874
|
+
input,
|
|
875
|
+
toolName: typeof req.name === "string" ? req.name : "tool",
|
|
876
|
+
type: "tool_use"
|
|
877
|
+
});
|
|
878
|
+
}
|
|
879
|
+
}
|
|
880
|
+
if (parts.length === 0) {
|
|
881
|
+
return null;
|
|
882
|
+
}
|
|
883
|
+
const id = typeof data.id === "string" ? data.id : `copilot-assistant-${ts}`;
|
|
884
|
+
return {
|
|
885
|
+
id,
|
|
886
|
+
parts,
|
|
887
|
+
role: "assistant",
|
|
888
|
+
timestamp: ts
|
|
889
|
+
};
|
|
890
|
+
}
|
|
891
|
+
if (type === "tool.execution_complete") {
|
|
892
|
+
const rawResult = data.result;
|
|
893
|
+
const result = typeof rawResult === "string" ? rawResult : JSON.stringify(rawResult ?? "");
|
|
894
|
+
const isError = data.isError === true || data.success === false;
|
|
895
|
+
const toolCallId = typeof data.toolCallId === "string" ? data.toolCallId : pendingToolUseId.value;
|
|
896
|
+
const toolUseId = toolCallId || pendingToolUseId.value || "unknown";
|
|
897
|
+
const id = typeof data.id === "string" ? data.id : `copilot-tool-result-${ts}`;
|
|
898
|
+
return {
|
|
899
|
+
id,
|
|
900
|
+
parts: [
|
|
901
|
+
{
|
|
902
|
+
isError,
|
|
903
|
+
output: result.slice(0, 2e3),
|
|
904
|
+
toolUseId,
|
|
905
|
+
type: "tool_result"
|
|
906
|
+
}
|
|
907
|
+
],
|
|
908
|
+
role: "system",
|
|
909
|
+
timestamp: ts
|
|
910
|
+
};
|
|
911
|
+
}
|
|
912
|
+
return null;
|
|
913
|
+
}
|
|
914
|
+
function isRecord$1(value) {
|
|
915
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
916
|
+
}
|
|
917
|
+
function parseCopilotText(text) {
|
|
918
|
+
const messages = [];
|
|
919
|
+
const pendingToolUseId = { value: "" };
|
|
920
|
+
for (const line of text.split("\n")) {
|
|
921
|
+
const trimmed = line.trim();
|
|
922
|
+
if (!trimmed) {
|
|
923
|
+
continue;
|
|
924
|
+
}
|
|
925
|
+
try {
|
|
926
|
+
const entry = JSON.parse(trimmed);
|
|
927
|
+
const msg = copilotEventToMessage(entry, pendingToolUseId);
|
|
928
|
+
if (msg) {
|
|
929
|
+
messages.push(msg);
|
|
930
|
+
if (msg.parts[0]?.type === "tool_result") {
|
|
931
|
+
pendingToolUseId.value = "";
|
|
932
|
+
}
|
|
933
|
+
}
|
|
934
|
+
} catch {
|
|
935
|
+
}
|
|
936
|
+
}
|
|
937
|
+
return messages;
|
|
938
|
+
}
|
|
939
|
+
function readBounded$1(filePath) {
|
|
940
|
+
const stat = fs.statSync(filePath);
|
|
941
|
+
if (stat.size <= MAX_FULL_READ_BYTES$1) {
|
|
942
|
+
return fs.readFileSync(filePath, "utf-8");
|
|
943
|
+
}
|
|
944
|
+
const head = readPrefix$2(filePath).split("\n")[0] ?? "";
|
|
945
|
+
const fd = fs.openSync(filePath, "r");
|
|
946
|
+
try {
|
|
947
|
+
const start = stat.size - MAX_FULL_READ_BYTES$1;
|
|
948
|
+
const buf = Buffer.alloc(MAX_FULL_READ_BYTES$1);
|
|
949
|
+
const bytesRead = fs.readSync(fd, buf, 0, MAX_FULL_READ_BYTES$1, start);
|
|
950
|
+
const tail = buf.toString("utf-8", 0, bytesRead);
|
|
951
|
+
return `${head}
|
|
952
|
+
${tail.slice(tail.indexOf("\n") + 1)}`;
|
|
953
|
+
} finally {
|
|
954
|
+
fs.closeSync(fd);
|
|
955
|
+
}
|
|
956
|
+
}
|
|
957
|
+
function readMeta$1(prefix, filePath) {
|
|
958
|
+
let cwd = "";
|
|
959
|
+
let gitBranch = "";
|
|
960
|
+
let title = "";
|
|
961
|
+
const id = sessionIdFromFilePath(filePath);
|
|
962
|
+
for (const line of prefix.split("\n")) {
|
|
963
|
+
const trimmed = line.trim();
|
|
964
|
+
if (!trimmed) {
|
|
965
|
+
continue;
|
|
966
|
+
}
|
|
967
|
+
let entry;
|
|
968
|
+
try {
|
|
969
|
+
entry = JSON.parse(trimmed);
|
|
970
|
+
} catch {
|
|
971
|
+
continue;
|
|
972
|
+
}
|
|
973
|
+
const type = typeof entry.type === "string" ? entry.type : "";
|
|
974
|
+
const data = isRecord$1(entry.data) ? entry.data : entry;
|
|
975
|
+
if (type === "session.start") {
|
|
976
|
+
const context = isRecord$1(data.context) ? data.context : data;
|
|
977
|
+
if (!cwd && typeof context.cwd === "string") {
|
|
978
|
+
cwd = context.cwd;
|
|
979
|
+
}
|
|
980
|
+
if (!gitBranch && typeof context.branch === "string") {
|
|
981
|
+
gitBranch = context.branch;
|
|
982
|
+
}
|
|
983
|
+
}
|
|
984
|
+
if (!title && type === "user.message" && typeof data.content === "string") {
|
|
985
|
+
title = data.content.split("\n")[0]?.slice(0, 80) ?? "";
|
|
986
|
+
}
|
|
987
|
+
if (cwd && title) {
|
|
988
|
+
break;
|
|
989
|
+
}
|
|
990
|
+
}
|
|
991
|
+
return cwd ? { cwd, gitBranch, id, title } : null;
|
|
992
|
+
}
|
|
993
|
+
function readPrefix$2(filePath) {
|
|
994
|
+
const fd = fs.openSync(filePath, "r");
|
|
995
|
+
try {
|
|
996
|
+
const buf = Buffer.alloc(PREFIX_BYTES$2);
|
|
997
|
+
const n = fs.readSync(fd, buf, 0, PREFIX_BYTES$2, 0);
|
|
998
|
+
const text = buf.toString("utf-8", 0, n);
|
|
999
|
+
const lastNl = text.lastIndexOf("\n");
|
|
1000
|
+
return lastNl === -1 ? text : text.slice(0, lastNl);
|
|
1001
|
+
} finally {
|
|
1002
|
+
fs.closeSync(fd);
|
|
1003
|
+
}
|
|
1004
|
+
}
|
|
1005
|
+
function safeReadPrefix(filePath) {
|
|
1006
|
+
try {
|
|
1007
|
+
return readPrefix$2(filePath);
|
|
1008
|
+
} catch {
|
|
1009
|
+
return "";
|
|
1010
|
+
}
|
|
1011
|
+
}
|
|
1012
|
+
function sessionIdFromFilePath(filePath) {
|
|
1013
|
+
const base = path.basename(filePath);
|
|
1014
|
+
if (base === "events.jsonl") {
|
|
1015
|
+
return path.basename(path.dirname(filePath));
|
|
1016
|
+
}
|
|
1017
|
+
return base.replace(/\.jsonl$/, "");
|
|
1018
|
+
}
|
|
1019
|
+
function shortHash$3(input) {
|
|
1020
|
+
return crypto.createHash("sha256").update(input).digest("hex").slice(0, 8);
|
|
1021
|
+
}
|
|
1022
|
+
const CURSOR_PROJECTS = path.join(homedir(), ".cursor", "projects");
|
|
1023
|
+
const PREFIX_BYTES$1 = 64 * 1024;
|
|
1024
|
+
const MAX_FULL_READ_BYTES = 16 * 1024 * 1024;
|
|
1025
|
+
function detectActiveCursorSessions(thresholdMs) {
|
|
1026
|
+
const cutoff = Date.now() - thresholdMs;
|
|
1027
|
+
const out = [];
|
|
1028
|
+
for (const { encodedDir, filePath } of collectTranscriptFiles()) {
|
|
1029
|
+
try {
|
|
1030
|
+
const stat = fs.statSync(filePath);
|
|
1031
|
+
if (stat.mtimeMs < cutoff) {
|
|
1032
|
+
continue;
|
|
1033
|
+
}
|
|
1034
|
+
const id = transcriptId(filePath);
|
|
1035
|
+
if (!id) {
|
|
1036
|
+
continue;
|
|
1037
|
+
}
|
|
1038
|
+
out.push({
|
|
1039
|
+
cwd: decodeCursorProjectDir(encodedDir),
|
|
1040
|
+
id,
|
|
1041
|
+
startedAt: stat.birthtimeMs
|
|
1042
|
+
});
|
|
1043
|
+
} catch {
|
|
1044
|
+
}
|
|
1045
|
+
}
|
|
1046
|
+
return out;
|
|
1047
|
+
}
|
|
1048
|
+
function getCursorConversation(sessionId, offset = 0, limit = 200) {
|
|
1049
|
+
if (!/^[A-Za-z0-9_.-]+$/.test(sessionId)) {
|
|
1050
|
+
return [];
|
|
1051
|
+
}
|
|
1052
|
+
const found = collectTranscriptFiles().find((e) => transcriptId(e.filePath) === sessionId);
|
|
1053
|
+
if (!found) {
|
|
1054
|
+
return [];
|
|
1055
|
+
}
|
|
1056
|
+
try {
|
|
1057
|
+
const messages = parseTranscriptFile(found.filePath);
|
|
1058
|
+
if (offset === 0 && messages.length > limit) {
|
|
1059
|
+
let startIdx = messages.length - limit;
|
|
1060
|
+
while (startIdx > 0 && messages[startIdx]?.role !== "user") {
|
|
1061
|
+
startIdx--;
|
|
1062
|
+
}
|
|
1063
|
+
return messages.slice(startIdx);
|
|
1064
|
+
}
|
|
1065
|
+
return messages.slice(offset, offset + limit);
|
|
1066
|
+
} catch (error) {
|
|
1067
|
+
console.error("[cursor] Failed to read conversation:", error);
|
|
1068
|
+
return [];
|
|
1069
|
+
}
|
|
1070
|
+
}
|
|
1071
|
+
function listCursorProjects() {
|
|
1072
|
+
const byCwd = /* @__PURE__ */ new Map();
|
|
1073
|
+
for (const { encodedDir, filePath } of collectTranscriptFiles()) {
|
|
1074
|
+
let stat;
|
|
1075
|
+
try {
|
|
1076
|
+
stat = fs.statSync(filePath);
|
|
1077
|
+
} catch {
|
|
1078
|
+
continue;
|
|
1079
|
+
}
|
|
1080
|
+
const id = transcriptId(filePath);
|
|
1081
|
+
if (!id) {
|
|
1082
|
+
continue;
|
|
1083
|
+
}
|
|
1084
|
+
const cwd = decodeCursorProjectDir(encodedDir);
|
|
1085
|
+
const title = extractTitle(filePath);
|
|
1086
|
+
const session = {
|
|
1087
|
+
created: stat.birthtime.toISOString(),
|
|
1088
|
+
gitBranch: "",
|
|
1089
|
+
id,
|
|
1090
|
+
messageCount: estimateMessageCount(filePath, stat),
|
|
1091
|
+
modified: stat.mtime.toISOString(),
|
|
1092
|
+
projectPath: cwd,
|
|
1093
|
+
source: "cursor",
|
|
1094
|
+
summary: "",
|
|
1095
|
+
title
|
|
1096
|
+
};
|
|
1097
|
+
const bucket = byCwd.get(cwd);
|
|
1098
|
+
if (bucket) {
|
|
1099
|
+
bucket.push(session);
|
|
1100
|
+
} else {
|
|
1101
|
+
byCwd.set(cwd, [session]);
|
|
1102
|
+
}
|
|
1103
|
+
}
|
|
1104
|
+
const projects = [];
|
|
1105
|
+
for (const [cwd, sessions] of byCwd) {
|
|
1106
|
+
sessions.sort((a, b) => new Date(b.modified).getTime() - new Date(a.modified).getTime());
|
|
1107
|
+
const segments = cwd.split("/").filter(Boolean);
|
|
1108
|
+
projects.push({
|
|
1109
|
+
fullPath: cwd,
|
|
1110
|
+
id: shortHash$2(cwd),
|
|
1111
|
+
lastModified: sessions[0]?.modified ?? "",
|
|
1112
|
+
name: segments.slice(-2).join("/"),
|
|
1113
|
+
sessionCount: sessions.length,
|
|
1114
|
+
sessions
|
|
1115
|
+
});
|
|
1116
|
+
}
|
|
1117
|
+
return projects.sort(
|
|
1118
|
+
(a, b) => new Date(b.lastModified).getTime() - new Date(a.lastModified).getTime()
|
|
1119
|
+
);
|
|
1120
|
+
}
|
|
1121
|
+
function resolveCursorSessionFile(sessionId) {
|
|
1122
|
+
if (!/^[A-Za-z0-9_.-]+$/.test(sessionId)) {
|
|
1123
|
+
return null;
|
|
1124
|
+
}
|
|
1125
|
+
const found = collectTranscriptFiles().find((e) => transcriptId(e.filePath) === sessionId);
|
|
1126
|
+
return found ? found.filePath : null;
|
|
1127
|
+
}
|
|
1128
|
+
function collectTranscriptFiles() {
|
|
1129
|
+
const out = [];
|
|
1130
|
+
let projectDirs;
|
|
1131
|
+
try {
|
|
1132
|
+
projectDirs = fs.readdirSync(CURSOR_PROJECTS, { withFileTypes: true });
|
|
1133
|
+
} catch {
|
|
1134
|
+
return out;
|
|
1135
|
+
}
|
|
1136
|
+
for (const dir of projectDirs) {
|
|
1137
|
+
if (!dir.isDirectory()) {
|
|
1138
|
+
continue;
|
|
1139
|
+
}
|
|
1140
|
+
const transcriptsDir = path.join(CURSOR_PROJECTS, dir.name, "agent-transcripts");
|
|
1141
|
+
let entries;
|
|
1142
|
+
try {
|
|
1143
|
+
entries = fs.readdirSync(transcriptsDir, { withFileTypes: true });
|
|
1144
|
+
} catch {
|
|
1145
|
+
continue;
|
|
1146
|
+
}
|
|
1147
|
+
const jsonlStems = /* @__PURE__ */ new Set();
|
|
1148
|
+
for (const entry of entries) {
|
|
1149
|
+
if (!entry.isFile()) {
|
|
1150
|
+
continue;
|
|
1151
|
+
}
|
|
1152
|
+
if (entry.name.endsWith(".jsonl")) {
|
|
1153
|
+
jsonlStems.add(entry.name.slice(0, -6));
|
|
1154
|
+
out.push({ encodedDir: dir.name, filePath: path.join(transcriptsDir, entry.name) });
|
|
1155
|
+
}
|
|
1156
|
+
}
|
|
1157
|
+
for (const entry of entries) {
|
|
1158
|
+
if (!entry.isFile() || !entry.name.endsWith(".txt")) {
|
|
1159
|
+
continue;
|
|
1160
|
+
}
|
|
1161
|
+
const stem = entry.name.slice(0, -4);
|
|
1162
|
+
if (!jsonlStems.has(stem)) {
|
|
1163
|
+
out.push({ encodedDir: dir.name, filePath: path.join(transcriptsDir, entry.name) });
|
|
1164
|
+
}
|
|
1165
|
+
}
|
|
1166
|
+
}
|
|
1167
|
+
return out;
|
|
1168
|
+
}
|
|
1169
|
+
function decodeCursorProjectDir(encoded) {
|
|
1170
|
+
if (encoded.includes("%2F") || encoded.includes("%2f")) {
|
|
1171
|
+
try {
|
|
1172
|
+
const decoded = decodeURIComponent(encoded.replace(/%2f/gi, "/"));
|
|
1173
|
+
if (decoded.startsWith("/")) {
|
|
1174
|
+
return decoded;
|
|
1175
|
+
}
|
|
1176
|
+
} catch {
|
|
1177
|
+
}
|
|
1178
|
+
}
|
|
1179
|
+
if (encoded.startsWith("-")) {
|
|
1180
|
+
const candidate = encoded.replace(/-/g, "/");
|
|
1181
|
+
return candidate;
|
|
1182
|
+
}
|
|
1183
|
+
return encoded;
|
|
1184
|
+
}
|
|
1185
|
+
function estimateMessageCount(filePath, stat) {
|
|
1186
|
+
const ext = path.extname(filePath);
|
|
1187
|
+
const avgBytesPerLine = ext === ".txt" ? 200 : 500;
|
|
1188
|
+
return Math.max(1, Math.round(stat.size / avgBytesPerLine));
|
|
1189
|
+
}
|
|
1190
|
+
function extractTitle(filePath) {
|
|
1191
|
+
try {
|
|
1192
|
+
const prefix = readPrefix$1(filePath);
|
|
1193
|
+
const ext = path.extname(filePath);
|
|
1194
|
+
if (ext === ".jsonl") {
|
|
1195
|
+
return titleFromJsonl(prefix);
|
|
1196
|
+
}
|
|
1197
|
+
return titleFromTxt(prefix);
|
|
1198
|
+
} catch {
|
|
1199
|
+
return "Untitled Session";
|
|
1200
|
+
}
|
|
1201
|
+
}
|
|
1202
|
+
function isRecord(value) {
|
|
1203
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
1204
|
+
}
|
|
1205
|
+
function jsonlLineToMessage(entry, idx) {
|
|
1206
|
+
const role = resolveRole(entry);
|
|
1207
|
+
if (!role) {
|
|
1208
|
+
return null;
|
|
1209
|
+
}
|
|
1210
|
+
const messageField = entry.message;
|
|
1211
|
+
if (!isRecord(messageField)) {
|
|
1212
|
+
return null;
|
|
1213
|
+
}
|
|
1214
|
+
const rawContent = messageField.content;
|
|
1215
|
+
if (rawContent === void 0 || rawContent === null) {
|
|
1216
|
+
return null;
|
|
1217
|
+
}
|
|
1218
|
+
const parts = [];
|
|
1219
|
+
if (typeof rawContent === "string") {
|
|
1220
|
+
if (!rawContent) {
|
|
1221
|
+
return null;
|
|
1222
|
+
}
|
|
1223
|
+
parts.push({ content: rawContent, type: "text" });
|
|
1224
|
+
} else if (Array.isArray(rawContent)) {
|
|
1225
|
+
for (const block of rawContent) {
|
|
1226
|
+
if (!isRecord(block)) {
|
|
1227
|
+
continue;
|
|
1228
|
+
}
|
|
1229
|
+
const blockType = strOf(block, "type");
|
|
1230
|
+
if (blockType === "text") {
|
|
1231
|
+
const text = strOf(block, "text");
|
|
1232
|
+
if (text) {
|
|
1233
|
+
parts.push({ content: text, type: "text" });
|
|
1234
|
+
}
|
|
1235
|
+
} else if (blockType === "thinking") {
|
|
1236
|
+
const thinking = strOf(block, "thinking");
|
|
1237
|
+
if (thinking) {
|
|
1238
|
+
parts.push({ content: thinking, type: "thinking" });
|
|
1239
|
+
}
|
|
1240
|
+
} else if (blockType === "tool_use") {
|
|
1241
|
+
const input = block.input;
|
|
1242
|
+
parts.push({
|
|
1243
|
+
id: strOf(block, "id"),
|
|
1244
|
+
input: isRecord(input) ? input : {},
|
|
1245
|
+
toolName: strOf(block, "name") || "Unknown",
|
|
1246
|
+
type: "tool_use"
|
|
1247
|
+
});
|
|
1248
|
+
} else if (blockType === "tool_result") {
|
|
1249
|
+
const resultContent = block.content;
|
|
1250
|
+
let output = "";
|
|
1251
|
+
if (typeof resultContent === "string") {
|
|
1252
|
+
output = resultContent;
|
|
1253
|
+
} else if (Array.isArray(resultContent)) {
|
|
1254
|
+
output = resultContent.filter((c) => isRecord(c) && c.type === "text").map((c) => {
|
|
1255
|
+
const t = c.text;
|
|
1256
|
+
return typeof t === "string" ? t : "";
|
|
1257
|
+
}).join("\n");
|
|
1258
|
+
}
|
|
1259
|
+
const isErr = block.is_error;
|
|
1260
|
+
parts.push({
|
|
1261
|
+
isError: typeof isErr === "boolean" ? isErr : false,
|
|
1262
|
+
output: output.slice(0, 2e3),
|
|
1263
|
+
toolUseId: strOf(block, "tool_use_id"),
|
|
1264
|
+
type: "tool_result"
|
|
1265
|
+
});
|
|
1266
|
+
}
|
|
1267
|
+
}
|
|
1268
|
+
} else {
|
|
1269
|
+
return null;
|
|
1270
|
+
}
|
|
1271
|
+
if (parts.length === 0) {
|
|
1272
|
+
return null;
|
|
1273
|
+
}
|
|
1274
|
+
const id = strOf(entry, "id") || strOf(entry, "uuid") || `cursor-${role}-${String(idx)}`;
|
|
1275
|
+
const timestamp = strOf(entry, "timestamp") || strOf(entry, "createdAt") || "";
|
|
1276
|
+
return { id, parts, role, timestamp };
|
|
1277
|
+
}
|
|
1278
|
+
function jsonlToMessages(text) {
|
|
1279
|
+
const messages = [];
|
|
1280
|
+
let idx = 0;
|
|
1281
|
+
for (const raw of text.split("\n")) {
|
|
1282
|
+
const trimmed = raw.trim();
|
|
1283
|
+
if (!trimmed) {
|
|
1284
|
+
continue;
|
|
1285
|
+
}
|
|
1286
|
+
try {
|
|
1287
|
+
const entry = JSON.parse(trimmed);
|
|
1288
|
+
if (!isRecord(entry)) {
|
|
1289
|
+
continue;
|
|
1290
|
+
}
|
|
1291
|
+
const msg = jsonlLineToMessage(entry, idx);
|
|
1292
|
+
if (msg) {
|
|
1293
|
+
messages.push(msg);
|
|
1294
|
+
idx++;
|
|
1295
|
+
}
|
|
1296
|
+
} catch {
|
|
1297
|
+
}
|
|
1298
|
+
}
|
|
1299
|
+
return messages;
|
|
1300
|
+
}
|
|
1301
|
+
function parseTranscriptFile(filePath) {
|
|
1302
|
+
const ext = path.extname(filePath);
|
|
1303
|
+
const text = readBounded(filePath);
|
|
1304
|
+
if (ext === ".jsonl") {
|
|
1305
|
+
return jsonlToMessages(text);
|
|
1306
|
+
}
|
|
1307
|
+
return parseTxtTranscript(text);
|
|
1308
|
+
}
|
|
1309
|
+
function parseTxtTranscript(text) {
|
|
1310
|
+
const messages = [];
|
|
1311
|
+
let currentRole = null;
|
|
1312
|
+
const currentLines = [];
|
|
1313
|
+
let idx = 0;
|
|
1314
|
+
const flush = () => {
|
|
1315
|
+
if (!currentRole || currentLines.length === 0) {
|
|
1316
|
+
return;
|
|
1317
|
+
}
|
|
1318
|
+
const content = currentLines.join("\n").trim();
|
|
1319
|
+
if (content) {
|
|
1320
|
+
messages.push({
|
|
1321
|
+
id: `cursor-${currentRole}-${String(idx)}`,
|
|
1322
|
+
parts: [{ content, type: "text" }],
|
|
1323
|
+
role: currentRole,
|
|
1324
|
+
timestamp: ""
|
|
1325
|
+
});
|
|
1326
|
+
idx++;
|
|
1327
|
+
}
|
|
1328
|
+
currentLines.length = 0;
|
|
1329
|
+
};
|
|
1330
|
+
for (const line of text.split("\n")) {
|
|
1331
|
+
if (line.startsWith("user:")) {
|
|
1332
|
+
flush();
|
|
1333
|
+
currentRole = "user";
|
|
1334
|
+
const rest = line.slice(5).trim();
|
|
1335
|
+
if (rest) {
|
|
1336
|
+
currentLines.push(rest);
|
|
1337
|
+
}
|
|
1338
|
+
} else if (line.startsWith("assistant:")) {
|
|
1339
|
+
flush();
|
|
1340
|
+
currentRole = "assistant";
|
|
1341
|
+
const rest = line.slice(10).trim();
|
|
1342
|
+
if (rest) {
|
|
1343
|
+
currentLines.push(rest);
|
|
1344
|
+
}
|
|
1345
|
+
} else if (currentRole) {
|
|
1346
|
+
currentLines.push(line);
|
|
1347
|
+
}
|
|
1348
|
+
}
|
|
1349
|
+
flush();
|
|
1350
|
+
return messages;
|
|
1351
|
+
}
|
|
1352
|
+
function readBounded(filePath) {
|
|
1353
|
+
const stat = fs.statSync(filePath);
|
|
1354
|
+
if (stat.size <= MAX_FULL_READ_BYTES) {
|
|
1355
|
+
return fs.readFileSync(filePath, "utf-8");
|
|
1356
|
+
}
|
|
1357
|
+
const fd = fs.openSync(filePath, "r");
|
|
1358
|
+
try {
|
|
1359
|
+
const start = stat.size - MAX_FULL_READ_BYTES;
|
|
1360
|
+
const buf = Buffer.alloc(MAX_FULL_READ_BYTES);
|
|
1361
|
+
const n = fs.readSync(fd, buf, 0, MAX_FULL_READ_BYTES, start);
|
|
1362
|
+
const tail = buf.toString("utf-8", 0, n);
|
|
1363
|
+
return tail.slice(tail.indexOf("\n") + 1);
|
|
1364
|
+
} finally {
|
|
1365
|
+
fs.closeSync(fd);
|
|
1366
|
+
}
|
|
1367
|
+
}
|
|
1368
|
+
function readPrefix$1(filePath) {
|
|
1369
|
+
const fd = fs.openSync(filePath, "r");
|
|
1370
|
+
try {
|
|
1371
|
+
const buf = Buffer.alloc(PREFIX_BYTES$1);
|
|
1372
|
+
const n = fs.readSync(fd, buf, 0, PREFIX_BYTES$1, 0);
|
|
1373
|
+
const text = buf.toString("utf-8", 0, n);
|
|
1374
|
+
const lastNl = text.lastIndexOf("\n");
|
|
1375
|
+
return lastNl === -1 ? text : text.slice(0, lastNl);
|
|
1376
|
+
} finally {
|
|
1377
|
+
fs.closeSync(fd);
|
|
1378
|
+
}
|
|
1379
|
+
}
|
|
1380
|
+
function resolveRole(entry) {
|
|
1381
|
+
const raw = strOf(entry, "role") || strOf(entry, "type");
|
|
1382
|
+
switch (raw.toLowerCase()) {
|
|
1383
|
+
case "assistant":
|
|
1384
|
+
case "bot":
|
|
1385
|
+
case "model":
|
|
1386
|
+
return "assistant";
|
|
1387
|
+
case "human":
|
|
1388
|
+
case "user":
|
|
1389
|
+
return "user";
|
|
1390
|
+
default:
|
|
1391
|
+
return null;
|
|
1392
|
+
}
|
|
1393
|
+
}
|
|
1394
|
+
function shortHash$2(input) {
|
|
1395
|
+
return crypto.createHash("sha256").update(input).digest("hex").slice(0, 8);
|
|
1396
|
+
}
|
|
1397
|
+
function strOf(obj, key) {
|
|
1398
|
+
const v = obj[key];
|
|
1399
|
+
return typeof v === "string" ? v : "";
|
|
1400
|
+
}
|
|
1401
|
+
function titleFromJsonl(prefix) {
|
|
1402
|
+
for (const raw of prefix.split("\n")) {
|
|
1403
|
+
const trimmed = raw.trim();
|
|
1404
|
+
if (!trimmed) {
|
|
1405
|
+
continue;
|
|
1406
|
+
}
|
|
1407
|
+
try {
|
|
1408
|
+
const entry = JSON.parse(trimmed);
|
|
1409
|
+
if (!isRecord(entry) || resolveRole(entry) !== "user") {
|
|
1410
|
+
continue;
|
|
1411
|
+
}
|
|
1412
|
+
const messageField = entry.message;
|
|
1413
|
+
if (!isRecord(messageField)) {
|
|
1414
|
+
continue;
|
|
1415
|
+
}
|
|
1416
|
+
const rawContent = messageField.content;
|
|
1417
|
+
let text = "";
|
|
1418
|
+
if (typeof rawContent === "string") {
|
|
1419
|
+
text = rawContent;
|
|
1420
|
+
} else if (Array.isArray(rawContent)) {
|
|
1421
|
+
for (const block of rawContent) {
|
|
1422
|
+
if (isRecord(block) && block.type === "text") {
|
|
1423
|
+
const t = block.text;
|
|
1424
|
+
if (typeof t === "string" && t) {
|
|
1425
|
+
text = t;
|
|
1426
|
+
break;
|
|
1427
|
+
}
|
|
1428
|
+
}
|
|
1429
|
+
}
|
|
1430
|
+
}
|
|
1431
|
+
if (text) {
|
|
1432
|
+
return truncateTitle(text);
|
|
1433
|
+
}
|
|
1434
|
+
} catch {
|
|
1435
|
+
}
|
|
1436
|
+
}
|
|
1437
|
+
return "Untitled Session";
|
|
1438
|
+
}
|
|
1439
|
+
function titleFromTxt(prefix) {
|
|
1440
|
+
for (const line of prefix.split("\n")) {
|
|
1441
|
+
if (line.startsWith("user:")) {
|
|
1442
|
+
const text = line.slice(5).trim();
|
|
1443
|
+
if (text) {
|
|
1444
|
+
return truncateTitle(text);
|
|
1445
|
+
}
|
|
1446
|
+
}
|
|
1447
|
+
}
|
|
1448
|
+
return "Untitled Session";
|
|
1449
|
+
}
|
|
1450
|
+
function transcriptId(filePath) {
|
|
1451
|
+
const base = path.basename(filePath);
|
|
1452
|
+
const stem = base.endsWith(".jsonl") ? base.slice(0, -6) : base.endsWith(".txt") ? base.slice(0, -4) : null;
|
|
1453
|
+
if (!stem || !/^[A-Za-z0-9_.-]+$/.test(stem)) {
|
|
1454
|
+
return null;
|
|
1455
|
+
}
|
|
1456
|
+
return stem;
|
|
1457
|
+
}
|
|
1458
|
+
function truncateTitle(text) {
|
|
1459
|
+
const first = text.replace(/\s+/g, " ").trim().split("\n")[0]?.trim() ?? "";
|
|
1460
|
+
if (!first) {
|
|
1461
|
+
return "Untitled Session";
|
|
1462
|
+
}
|
|
1463
|
+
return first.length > 80 ? `${first.slice(0, 77)}...` : first;
|
|
1464
|
+
}
|
|
1465
|
+
const MAX_GEMINI_FILE_BYTES = 64 * 1024 * 1024;
|
|
1466
|
+
const GEMINI_TMP = path.join(homedir(), ".gemini", "tmp");
|
|
1467
|
+
const GEMINI_PROJECTS_JSON = path.join(homedir(), ".gemini", "projects.json");
|
|
1468
|
+
function detectActiveGeminiSessions(thresholdMs) {
|
|
1469
|
+
const cutoff = Date.now() - thresholdMs;
|
|
1470
|
+
const projectHashToCwd = buildHashToCwdMap();
|
|
1471
|
+
const projectDirs = collectProjectHashDirs();
|
|
1472
|
+
const out = [];
|
|
1473
|
+
for (const hashDir of projectDirs) {
|
|
1474
|
+
const hash = path.basename(hashDir);
|
|
1475
|
+
const cwd = projectHashToCwd.get(hash) ?? hash;
|
|
1476
|
+
const chatFiles = collectChatFiles(hashDir);
|
|
1477
|
+
for (const chatFile of chatFiles) {
|
|
1478
|
+
try {
|
|
1479
|
+
const stat = fs.statSync(chatFile);
|
|
1480
|
+
if (stat.mtimeMs < cutoff) {
|
|
1481
|
+
continue;
|
|
1482
|
+
}
|
|
1483
|
+
const record = readChatFile(chatFile);
|
|
1484
|
+
if (record) {
|
|
1485
|
+
out.push({
|
|
1486
|
+
cwd,
|
|
1487
|
+
id: record.sessionId,
|
|
1488
|
+
startedAt: new Date(record.startTime).getTime()
|
|
1489
|
+
});
|
|
1490
|
+
}
|
|
1491
|
+
} catch {
|
|
1492
|
+
}
|
|
1493
|
+
}
|
|
1494
|
+
const logsPath = path.join(hashDir, "logs.json");
|
|
1495
|
+
try {
|
|
1496
|
+
const stat = fs.statSync(logsPath);
|
|
1497
|
+
if (stat.mtimeMs < cutoff) {
|
|
1498
|
+
continue;
|
|
1499
|
+
}
|
|
1500
|
+
const entries = readLogsJson(logsPath);
|
|
1501
|
+
if (entries.length > 0) {
|
|
1502
|
+
const last = entries[entries.length - 1];
|
|
1503
|
+
if (last) {
|
|
1504
|
+
const alreadyAdded = out.some((o) => o.id === last.sessionId && o.cwd === cwd);
|
|
1505
|
+
if (!alreadyAdded) {
|
|
1506
|
+
out.push({
|
|
1507
|
+
cwd,
|
|
1508
|
+
id: last.sessionId,
|
|
1509
|
+
startedAt: new Date(entries[0]?.timestamp ?? last.timestamp).getTime()
|
|
1510
|
+
});
|
|
1511
|
+
}
|
|
1512
|
+
}
|
|
1513
|
+
}
|
|
1514
|
+
} catch {
|
|
1515
|
+
}
|
|
1516
|
+
}
|
|
1517
|
+
return out;
|
|
1518
|
+
}
|
|
1519
|
+
function getGeminiConversation(sessionId, offset = 0, limit = 200) {
|
|
1520
|
+
const projectDirs = collectProjectHashDirs();
|
|
1521
|
+
for (const hashDir of projectDirs) {
|
|
1522
|
+
const chatFile = findChatFileForSession(hashDir, sessionId);
|
|
1523
|
+
if (chatFile) {
|
|
1524
|
+
return conversationFromChatFile(chatFile, offset, limit);
|
|
1525
|
+
}
|
|
1526
|
+
}
|
|
1527
|
+
for (const hashDir of projectDirs) {
|
|
1528
|
+
const messages = conversationFromLogsJson(hashDir, sessionId);
|
|
1529
|
+
if (messages.length > 0) {
|
|
1530
|
+
if (offset === 0 && messages.length > limit) {
|
|
1531
|
+
return messages.slice(messages.length - limit);
|
|
1532
|
+
}
|
|
1533
|
+
return messages.slice(offset, offset + limit);
|
|
1534
|
+
}
|
|
1535
|
+
}
|
|
1536
|
+
return [];
|
|
1537
|
+
}
|
|
1538
|
+
function listGeminiProjects() {
|
|
1539
|
+
const projectHashToCwd = buildHashToCwdMap();
|
|
1540
|
+
const projectDirs = collectProjectHashDirs();
|
|
1541
|
+
const byCwd = /* @__PURE__ */ new Map();
|
|
1542
|
+
for (const hashDir of projectDirs) {
|
|
1543
|
+
const hash = path.basename(hashDir);
|
|
1544
|
+
const cwd = projectHashToCwd.get(hash) ?? hash;
|
|
1545
|
+
const chatSessions = collectChatSessions(hashDir, cwd);
|
|
1546
|
+
if (chatSessions.length > 0) {
|
|
1547
|
+
for (const session of chatSessions) {
|
|
1548
|
+
appendToMap(byCwd, cwd, session);
|
|
1549
|
+
}
|
|
1550
|
+
continue;
|
|
1551
|
+
}
|
|
1552
|
+
const logSessions = sessionsFromLogsJson(hashDir, cwd);
|
|
1553
|
+
for (const session of logSessions) {
|
|
1554
|
+
appendToMap(byCwd, cwd, session);
|
|
1555
|
+
}
|
|
1556
|
+
}
|
|
1557
|
+
const projects = [];
|
|
1558
|
+
for (const [cwd, sessions] of byCwd) {
|
|
1559
|
+
sessions.sort((a, b) => new Date(b.modified).getTime() - new Date(a.modified).getTime());
|
|
1560
|
+
const segments = cwd.split("/").filter(Boolean);
|
|
1561
|
+
const isRawHash = /^[0-9a-f]{64}$/i.test(cwd);
|
|
1562
|
+
projects.push({
|
|
1563
|
+
fullPath: cwd,
|
|
1564
|
+
id: shortHash$1(cwd),
|
|
1565
|
+
lastModified: sessions[0]?.modified ?? "",
|
|
1566
|
+
name: isRawHash ? `Gemini (${cwd.slice(0, 8)})` : segments.slice(-2).join("/"),
|
|
1567
|
+
sessionCount: sessions.length,
|
|
1568
|
+
sessions
|
|
1569
|
+
});
|
|
1570
|
+
}
|
|
1571
|
+
return projects.sort(
|
|
1572
|
+
(a, b) => new Date(b.lastModified).getTime() - new Date(a.lastModified).getTime()
|
|
1573
|
+
);
|
|
1574
|
+
}
|
|
1575
|
+
function resolveGeminiSessionFile(sessionId) {
|
|
1576
|
+
for (const hashDir of collectProjectHashDirs()) {
|
|
1577
|
+
const chatFile = findChatFileForSession(hashDir, sessionId);
|
|
1578
|
+
if (chatFile !== null) {
|
|
1579
|
+
return chatFile;
|
|
1580
|
+
}
|
|
1581
|
+
}
|
|
1582
|
+
return null;
|
|
1583
|
+
}
|
|
1584
|
+
function appendToMap(map, key, value) {
|
|
1585
|
+
let bucket = map.get(key);
|
|
1586
|
+
if (!bucket) {
|
|
1587
|
+
bucket = [];
|
|
1588
|
+
map.set(key, bucket);
|
|
1589
|
+
}
|
|
1590
|
+
bucket.push(value);
|
|
1591
|
+
}
|
|
1592
|
+
function buildHashToCwdMap() {
|
|
1593
|
+
const map = /* @__PURE__ */ new Map();
|
|
1594
|
+
try {
|
|
1595
|
+
const raw = fs.readFileSync(GEMINI_PROJECTS_JSON, "utf-8");
|
|
1596
|
+
const data = JSON.parse(raw);
|
|
1597
|
+
for (const [slugOrHash, absolutePath] of Object.entries(data)) {
|
|
1598
|
+
map.set(slugOrHash, absolutePath);
|
|
1599
|
+
const computed = crypto.createHash("sha256").update(absolutePath).digest("hex");
|
|
1600
|
+
map.set(computed, absolutePath);
|
|
1601
|
+
}
|
|
1602
|
+
} catch {
|
|
1603
|
+
}
|
|
1604
|
+
return map;
|
|
1605
|
+
}
|
|
1606
|
+
function cleanTitle(text) {
|
|
1607
|
+
const first = text.replace(/\s+/g, " ").trim().split("\n")[0]?.trim() ?? "";
|
|
1608
|
+
if (!first) {
|
|
1609
|
+
return "Untitled Session";
|
|
1610
|
+
}
|
|
1611
|
+
return first.length > 80 ? `${first.slice(0, 77)}...` : first;
|
|
1612
|
+
}
|
|
1613
|
+
function collectChatFiles(hashDir) {
|
|
1614
|
+
const chatsDir = path.join(hashDir, "chats");
|
|
1615
|
+
try {
|
|
1616
|
+
return fs.readdirSync(chatsDir, { withFileTypes: true }).filter((e) => e.isFile() && e.name.startsWith("session-") && e.name.endsWith(".json")).map((e) => path.join(chatsDir, e.name));
|
|
1617
|
+
} catch {
|
|
1618
|
+
return [];
|
|
1619
|
+
}
|
|
1620
|
+
}
|
|
1621
|
+
function collectChatSessions(hashDir, cwd) {
|
|
1622
|
+
const chatFiles = collectChatFiles(hashDir);
|
|
1623
|
+
const sessions = [];
|
|
1624
|
+
for (const chatFile of chatFiles) {
|
|
1625
|
+
try {
|
|
1626
|
+
const stat = fs.statSync(chatFile);
|
|
1627
|
+
const record = readChatFile(chatFile);
|
|
1628
|
+
if (!record) {
|
|
1629
|
+
continue;
|
|
1630
|
+
}
|
|
1631
|
+
const userMessages = record.messages.filter((m) => m.type === "user");
|
|
1632
|
+
const firstUserMsg = userMessages[0];
|
|
1633
|
+
let title = "Untitled Session";
|
|
1634
|
+
if (firstUserMsg) {
|
|
1635
|
+
title = cleanTitle(extractTextFromContent(firstUserMsg.content));
|
|
1636
|
+
}
|
|
1637
|
+
sessions.push({
|
|
1638
|
+
created: record.startTime,
|
|
1639
|
+
gitBranch: "",
|
|
1640
|
+
id: record.sessionId,
|
|
1641
|
+
messageCount: record.messages.length,
|
|
1642
|
+
modified: stat.mtime.toISOString(),
|
|
1643
|
+
projectPath: cwd,
|
|
1644
|
+
source: "gemini",
|
|
1645
|
+
summary: record.summary ?? "",
|
|
1646
|
+
title
|
|
1647
|
+
});
|
|
1648
|
+
} catch {
|
|
1649
|
+
}
|
|
1650
|
+
}
|
|
1651
|
+
return sessions;
|
|
1652
|
+
}
|
|
1653
|
+
function collectProjectHashDirs() {
|
|
1654
|
+
try {
|
|
1655
|
+
return fs.readdirSync(GEMINI_TMP, { withFileTypes: true }).filter((e) => e.isDirectory()).map((e) => path.join(GEMINI_TMP, e.name));
|
|
1656
|
+
} catch {
|
|
1657
|
+
return [];
|
|
1658
|
+
}
|
|
1659
|
+
}
|
|
1660
|
+
function conversationFromChatFile(chatFile, offset, limit) {
|
|
1661
|
+
try {
|
|
1662
|
+
const record = readChatFile(chatFile);
|
|
1663
|
+
if (!record) {
|
|
1664
|
+
return [];
|
|
1665
|
+
}
|
|
1666
|
+
const messages = record.messages.filter((m) => m.type === "user" || m.type === "gemini").map(messageRecordToMessage);
|
|
1667
|
+
if (offset === 0 && messages.length > limit) {
|
|
1668
|
+
let startIdx = messages.length - limit;
|
|
1669
|
+
while (startIdx > 0 && messages[startIdx]?.role !== "user") {
|
|
1670
|
+
startIdx--;
|
|
1671
|
+
}
|
|
1672
|
+
return messages.slice(startIdx);
|
|
1673
|
+
}
|
|
1674
|
+
return messages.slice(offset, offset + limit);
|
|
1675
|
+
} catch (err) {
|
|
1676
|
+
console.error("[gemini] Failed to read chat file:", err);
|
|
1677
|
+
return [];
|
|
1678
|
+
}
|
|
1679
|
+
}
|
|
1680
|
+
function conversationFromLogsJson(hashDir, sessionId) {
|
|
1681
|
+
const logsPath = path.join(hashDir, "logs.json");
|
|
1682
|
+
const entries = readLogsJson(logsPath);
|
|
1683
|
+
return entries.filter((e) => e.sessionId === sessionId).map(
|
|
1684
|
+
(entry) => ({
|
|
1685
|
+
id: `${entry.sessionId}-${String(entry.messageId)}`,
|
|
1686
|
+
parts: [{ content: entry.message, type: "text" }],
|
|
1687
|
+
role: "user",
|
|
1688
|
+
timestamp: entry.timestamp
|
|
1689
|
+
})
|
|
1690
|
+
);
|
|
1691
|
+
}
|
|
1692
|
+
function extractTextFromContent(content) {
|
|
1693
|
+
if (typeof content === "string") {
|
|
1694
|
+
return content;
|
|
1695
|
+
}
|
|
1696
|
+
return content.filter((p) => "text" in p).map((p) => p.text).join(" ");
|
|
1697
|
+
}
|
|
1698
|
+
function findChatFileForSession(hashDir, sessionId) {
|
|
1699
|
+
if (!sessionId) {
|
|
1700
|
+
return null;
|
|
1701
|
+
}
|
|
1702
|
+
const chatsDir = path.join(hashDir, "chats");
|
|
1703
|
+
let names;
|
|
1704
|
+
try {
|
|
1705
|
+
names = fs.readdirSync(chatsDir, { withFileTypes: true }).filter((e) => e.isFile() && e.name.startsWith("session-") && e.name.endsWith(".json")).map((e) => e.name);
|
|
1706
|
+
} catch {
|
|
1707
|
+
return null;
|
|
1708
|
+
}
|
|
1709
|
+
const shortId = sessionId.slice(0, 8);
|
|
1710
|
+
names.sort((a, b) => Number(b.includes(shortId)) - Number(a.includes(shortId)));
|
|
1711
|
+
for (const name of names) {
|
|
1712
|
+
const full = path.join(chatsDir, name);
|
|
1713
|
+
if (readChatFile(full)?.sessionId === sessionId) {
|
|
1714
|
+
return full;
|
|
1715
|
+
}
|
|
1716
|
+
}
|
|
1717
|
+
return null;
|
|
1718
|
+
}
|
|
1719
|
+
function isGeminiLogEntry(v) {
|
|
1720
|
+
if (typeof v !== "object" || v === null) {
|
|
1721
|
+
return false;
|
|
1722
|
+
}
|
|
1723
|
+
const obj = v;
|
|
1724
|
+
return typeof obj.sessionId === "string" && typeof obj.messageId === "number" && typeof obj.message === "string" && typeof obj.timestamp === "string";
|
|
1725
|
+
}
|
|
1726
|
+
function messageRecordToMessage(record) {
|
|
1727
|
+
const role = record.type === "user" ? "user" : "assistant";
|
|
1728
|
+
const parts = [];
|
|
1729
|
+
if (record.type === "gemini" && record.thoughts) {
|
|
1730
|
+
for (const thought of record.thoughts) {
|
|
1731
|
+
parts.push({ content: thought.summary ?? "", type: "thinking" });
|
|
1732
|
+
}
|
|
1733
|
+
}
|
|
1734
|
+
const rawContent = record.content;
|
|
1735
|
+
const contentParts = typeof rawContent === "string" ? [{ text: rawContent }] : rawContent;
|
|
1736
|
+
for (const part of contentParts) {
|
|
1737
|
+
if ("functionCall" in part) {
|
|
1738
|
+
parts.push({
|
|
1739
|
+
id: part.functionCall.id ?? part.functionCall.name,
|
|
1740
|
+
input: part.functionCall.args,
|
|
1741
|
+
toolName: part.functionCall.name,
|
|
1742
|
+
type: "tool_use"
|
|
1743
|
+
});
|
|
1744
|
+
} else if ("thought" in part && part.thought === true) {
|
|
1745
|
+
parts.push({ content: part.text, type: "thinking" });
|
|
1746
|
+
} else if ("text" in part) {
|
|
1747
|
+
parts.push({ content: part.text, type: "text" });
|
|
1748
|
+
}
|
|
1749
|
+
}
|
|
1750
|
+
if (record.type === "gemini" && record.toolCalls) {
|
|
1751
|
+
for (const tc of record.toolCalls) {
|
|
1752
|
+
const alreadyInParts = parts.some((p) => p.type === "tool_use" && p.id === tc.id);
|
|
1753
|
+
if (!alreadyInParts) {
|
|
1754
|
+
parts.push({
|
|
1755
|
+
id: tc.id,
|
|
1756
|
+
input: tc.args,
|
|
1757
|
+
toolName: tc.name,
|
|
1758
|
+
type: "tool_use"
|
|
1759
|
+
});
|
|
1760
|
+
}
|
|
1761
|
+
}
|
|
1762
|
+
}
|
|
1763
|
+
return {
|
|
1764
|
+
id: record.id,
|
|
1765
|
+
parts,
|
|
1766
|
+
role,
|
|
1767
|
+
timestamp: record.timestamp
|
|
1768
|
+
};
|
|
1769
|
+
}
|
|
1770
|
+
function readChatFile(filePath) {
|
|
1771
|
+
try {
|
|
1772
|
+
if (fs.statSync(filePath).size > MAX_GEMINI_FILE_BYTES) {
|
|
1773
|
+
return null;
|
|
1774
|
+
}
|
|
1775
|
+
return JSON.parse(fs.readFileSync(filePath, "utf-8"));
|
|
1776
|
+
} catch {
|
|
1777
|
+
return null;
|
|
1778
|
+
}
|
|
1779
|
+
}
|
|
1780
|
+
function readLogsJson(logsPath) {
|
|
1781
|
+
try {
|
|
1782
|
+
if (fs.statSync(logsPath).size > MAX_GEMINI_FILE_BYTES) {
|
|
1783
|
+
return [];
|
|
1784
|
+
}
|
|
1785
|
+
const parsed = JSON.parse(fs.readFileSync(logsPath, "utf-8"));
|
|
1786
|
+
return Array.isArray(parsed) ? parsed.filter(isGeminiLogEntry) : [];
|
|
1787
|
+
} catch {
|
|
1788
|
+
return [];
|
|
1789
|
+
}
|
|
1790
|
+
}
|
|
1791
|
+
function sessionsFromLogsJson(hashDir, cwd) {
|
|
1792
|
+
const logsPath = path.join(hashDir, "logs.json");
|
|
1793
|
+
let stat;
|
|
1794
|
+
try {
|
|
1795
|
+
stat = fs.statSync(logsPath);
|
|
1796
|
+
} catch {
|
|
1797
|
+
return [];
|
|
1798
|
+
}
|
|
1799
|
+
const entries = readLogsJson(logsPath);
|
|
1800
|
+
if (entries.length === 0) {
|
|
1801
|
+
return [];
|
|
1802
|
+
}
|
|
1803
|
+
const bySession = /* @__PURE__ */ new Map();
|
|
1804
|
+
for (const entry of entries) {
|
|
1805
|
+
let bucket = bySession.get(entry.sessionId);
|
|
1806
|
+
if (!bucket) {
|
|
1807
|
+
bucket = [];
|
|
1808
|
+
bySession.set(entry.sessionId, bucket);
|
|
1809
|
+
}
|
|
1810
|
+
bucket.push(entry);
|
|
1811
|
+
}
|
|
1812
|
+
const sessions = [];
|
|
1813
|
+
for (const [sessionId, sessionEntries] of bySession) {
|
|
1814
|
+
const first = sessionEntries[0];
|
|
1815
|
+
const last = sessionEntries[sessionEntries.length - 1];
|
|
1816
|
+
sessions.push({
|
|
1817
|
+
created: first?.timestamp ?? stat.birthtime.toISOString(),
|
|
1818
|
+
gitBranch: "",
|
|
1819
|
+
id: sessionId,
|
|
1820
|
+
messageCount: sessionEntries.length,
|
|
1821
|
+
modified: last?.timestamp ?? stat.mtime.toISOString(),
|
|
1822
|
+
projectPath: cwd,
|
|
1823
|
+
source: "gemini",
|
|
1824
|
+
summary: "",
|
|
1825
|
+
title: cleanTitle(first?.message ?? "")
|
|
1826
|
+
});
|
|
1827
|
+
}
|
|
1828
|
+
return sessions;
|
|
1829
|
+
}
|
|
1830
|
+
function shortHash$1(input) {
|
|
1831
|
+
return crypto.createHash("sha256").update(input).digest("hex").slice(0, 8);
|
|
1832
|
+
}
|
|
1833
|
+
function resolveOpenCodeDbPath() {
|
|
1834
|
+
const home = process.env.HOME || "";
|
|
1835
|
+
const candidates = [];
|
|
1836
|
+
if (process.env.XDG_DATA_HOME) {
|
|
1837
|
+
candidates.push(path.join(process.env.XDG_DATA_HOME, "opencode", "opencode.db"));
|
|
1838
|
+
}
|
|
1839
|
+
candidates.push(path.join(home, ".local", "share", "opencode", "opencode.db"));
|
|
1840
|
+
if (process.platform === "darwin") {
|
|
1841
|
+
candidates.push(path.join(home, "Library", "Application Support", "opencode", "opencode.db"));
|
|
1842
|
+
}
|
|
1843
|
+
for (const candidate of candidates) {
|
|
1844
|
+
if (existsSync(candidate)) {
|
|
1845
|
+
return candidate;
|
|
1846
|
+
}
|
|
1847
|
+
}
|
|
1848
|
+
return candidates[0] ?? path.join(home, ".local", "share", "opencode", "opencode.db");
|
|
1849
|
+
}
|
|
1850
|
+
const QWEN_PROJECTS = path.join(homedir(), ".qwen", "projects");
|
|
1851
|
+
const PREFIX_BYTES = 64 * 1024;
|
|
1852
|
+
const MAX_QWEN_FILE_BYTES = 16 * 1024 * 1024;
|
|
1853
|
+
const SYSTEM_TAG_PREFIXES = [
|
|
1854
|
+
"<command-name>",
|
|
1855
|
+
"<local-command",
|
|
1856
|
+
"<system-reminder>",
|
|
1857
|
+
"<task-notification>"
|
|
1858
|
+
];
|
|
1859
|
+
function detectActiveQwenSessions(thresholdMs) {
|
|
1860
|
+
const cutoff = Date.now() - thresholdMs;
|
|
1861
|
+
const out = [];
|
|
1862
|
+
for (const filePath of collectQwenFiles()) {
|
|
1863
|
+
try {
|
|
1864
|
+
const stat = fs.statSync(filePath);
|
|
1865
|
+
if (stat.mtimeMs < cutoff) {
|
|
1866
|
+
continue;
|
|
1867
|
+
}
|
|
1868
|
+
const meta = readMeta(readPrefix(filePath));
|
|
1869
|
+
if (meta) {
|
|
1870
|
+
out.push({ cwd: meta.cwd, id: meta.id, startedAt: stat.birthtimeMs });
|
|
1871
|
+
}
|
|
1872
|
+
} catch {
|
|
1873
|
+
}
|
|
1874
|
+
}
|
|
1875
|
+
return out;
|
|
1876
|
+
}
|
|
1877
|
+
function getQwenConversation(sessionId, offset = 0, limit = 200) {
|
|
1878
|
+
if (!/^[A-Za-z0-9_-]+$/.test(sessionId)) {
|
|
1879
|
+
return [];
|
|
1880
|
+
}
|
|
1881
|
+
const filePath = collectQwenFiles().find((p) => path.basename(p) === `${sessionId}.jsonl`);
|
|
1882
|
+
if (!filePath) {
|
|
1883
|
+
return [];
|
|
1884
|
+
}
|
|
1885
|
+
try {
|
|
1886
|
+
const messages = readQwenMessages(filePath);
|
|
1887
|
+
if (offset === 0 && messages.length > limit) {
|
|
1888
|
+
let startIdx = messages.length - limit;
|
|
1889
|
+
while (startIdx > 0 && messages[startIdx].role !== "user") {
|
|
1890
|
+
startIdx--;
|
|
1891
|
+
}
|
|
1892
|
+
return messages.slice(startIdx);
|
|
1893
|
+
}
|
|
1894
|
+
return messages.slice(offset, offset + limit);
|
|
1895
|
+
} catch (error) {
|
|
1896
|
+
console.error("[qwen] Failed to read conversation:", error);
|
|
1897
|
+
return [];
|
|
1898
|
+
}
|
|
1899
|
+
}
|
|
1900
|
+
function listQwenProjects() {
|
|
1901
|
+
const byCwd = /* @__PURE__ */ new Map();
|
|
1902
|
+
for (const filePath of collectQwenFiles()) {
|
|
1903
|
+
let stat;
|
|
1904
|
+
try {
|
|
1905
|
+
stat = fs.statSync(filePath);
|
|
1906
|
+
} catch {
|
|
1907
|
+
continue;
|
|
1908
|
+
}
|
|
1909
|
+
const meta = readMeta(readPrefix(filePath));
|
|
1910
|
+
if (!meta) {
|
|
1911
|
+
continue;
|
|
1912
|
+
}
|
|
1913
|
+
const session = {
|
|
1914
|
+
created: meta.started || stat.birthtime.toISOString(),
|
|
1915
|
+
gitBranch: meta.gitBranch,
|
|
1916
|
+
id: meta.id,
|
|
1917
|
+
messageCount: 0,
|
|
1918
|
+
modified: stat.mtime.toISOString(),
|
|
1919
|
+
projectPath: meta.cwd,
|
|
1920
|
+
source: "qwen",
|
|
1921
|
+
summary: "",
|
|
1922
|
+
title: meta.title || "Untitled Session"
|
|
1923
|
+
};
|
|
1924
|
+
const bucket = byCwd.get(meta.cwd);
|
|
1925
|
+
if (bucket) {
|
|
1926
|
+
bucket.push(session);
|
|
1927
|
+
} else {
|
|
1928
|
+
byCwd.set(meta.cwd, [session]);
|
|
1929
|
+
}
|
|
1930
|
+
}
|
|
1931
|
+
const projects = [];
|
|
1932
|
+
for (const [cwd, sessions] of byCwd) {
|
|
1933
|
+
sessions.sort((a, b) => new Date(b.modified).getTime() - new Date(a.modified).getTime());
|
|
1934
|
+
const segments = cwd.split("/").filter(Boolean);
|
|
1935
|
+
projects.push({
|
|
1936
|
+
fullPath: cwd,
|
|
1937
|
+
id: shortHash(cwd),
|
|
1938
|
+
lastModified: sessions[0]?.modified ?? "",
|
|
1939
|
+
name: segments.slice(-2).join("/"),
|
|
1940
|
+
sessionCount: sessions.length,
|
|
1941
|
+
sessions
|
|
1942
|
+
});
|
|
1943
|
+
}
|
|
1944
|
+
return projects.sort(
|
|
1945
|
+
(a, b) => new Date(b.lastModified).getTime() - new Date(a.lastModified).getTime()
|
|
1946
|
+
);
|
|
1947
|
+
}
|
|
1948
|
+
function resolveQwenSessionFile(sessionId) {
|
|
1949
|
+
if (!/^[A-Za-z0-9_-]+$/.test(sessionId)) {
|
|
1950
|
+
return null;
|
|
1951
|
+
}
|
|
1952
|
+
return collectQwenFiles().find((p) => path.basename(p) === `${sessionId}.jsonl`) ?? null;
|
|
1953
|
+
}
|
|
1954
|
+
function collectQwenFiles() {
|
|
1955
|
+
const out = [];
|
|
1956
|
+
let projectDirs;
|
|
1957
|
+
try {
|
|
1958
|
+
projectDirs = fs.readdirSync(QWEN_PROJECTS, { withFileTypes: true });
|
|
1959
|
+
} catch {
|
|
1960
|
+
return out;
|
|
1961
|
+
}
|
|
1962
|
+
for (const dir of projectDirs) {
|
|
1963
|
+
if (!dir.isDirectory()) {
|
|
1964
|
+
continue;
|
|
1965
|
+
}
|
|
1966
|
+
const chatsDir = path.join(QWEN_PROJECTS, dir.name, "chats");
|
|
1967
|
+
try {
|
|
1968
|
+
for (const f of fs.readdirSync(chatsDir)) {
|
|
1969
|
+
if (f.endsWith(".jsonl")) {
|
|
1970
|
+
out.push(path.join(chatsDir, f));
|
|
1971
|
+
}
|
|
1972
|
+
}
|
|
1973
|
+
} catch {
|
|
1974
|
+
}
|
|
1975
|
+
}
|
|
1976
|
+
return out;
|
|
1977
|
+
}
|
|
1978
|
+
function qwenLineToMessage(entry) {
|
|
1979
|
+
const type = entry.type;
|
|
1980
|
+
if (type !== "user" && type !== "assistant") {
|
|
1981
|
+
return null;
|
|
1982
|
+
}
|
|
1983
|
+
const message = entry.message ?? {};
|
|
1984
|
+
const rawParts = Array.isArray(message.parts) ? message.parts : [];
|
|
1985
|
+
const parts = [];
|
|
1986
|
+
for (const raw of rawParts) {
|
|
1987
|
+
if (typeof raw !== "object" || raw === null) {
|
|
1988
|
+
continue;
|
|
1989
|
+
}
|
|
1990
|
+
const p = raw;
|
|
1991
|
+
if (p.functionCall && typeof p.functionCall === "object") {
|
|
1992
|
+
const fc = p.functionCall;
|
|
1993
|
+
const toolName = typeof fc.name === "string" ? fc.name : "tool";
|
|
1994
|
+
parts.push({
|
|
1995
|
+
id: typeof fc.id === "string" ? fc.id : toolName,
|
|
1996
|
+
input: fc.args ?? {},
|
|
1997
|
+
toolName,
|
|
1998
|
+
type: "tool_use"
|
|
1999
|
+
});
|
|
2000
|
+
} else if (p.thought === true && typeof p.text === "string") {
|
|
2001
|
+
parts.push({ content: p.text, type: "thinking" });
|
|
2002
|
+
} else if (typeof p.text === "string") {
|
|
2003
|
+
parts.push({ content: p.text, type: "text" });
|
|
2004
|
+
}
|
|
2005
|
+
}
|
|
2006
|
+
if (parts.length === 0 && typeof message.content === "string" && message.content) {
|
|
2007
|
+
parts.push({ content: message.content, type: "text" });
|
|
2008
|
+
}
|
|
2009
|
+
if (parts.length === 0) {
|
|
2010
|
+
return null;
|
|
2011
|
+
}
|
|
2012
|
+
return {
|
|
2013
|
+
id: typeof entry.uuid === "string" ? entry.uuid : `qwen-${type}`,
|
|
2014
|
+
parts,
|
|
2015
|
+
role: type === "user" ? "user" : "assistant",
|
|
2016
|
+
timestamp: typeof entry.timestamp === "string" ? entry.timestamp : ""
|
|
2017
|
+
};
|
|
2018
|
+
}
|
|
2019
|
+
function readMeta(prefix) {
|
|
2020
|
+
let cwd = "";
|
|
2021
|
+
let id = "";
|
|
2022
|
+
let gitBranch = "";
|
|
2023
|
+
let started = "";
|
|
2024
|
+
let title = "";
|
|
2025
|
+
for (const line of prefix.split("\n")) {
|
|
2026
|
+
const trimmed = line.trim();
|
|
2027
|
+
if (!trimmed) {
|
|
2028
|
+
continue;
|
|
2029
|
+
}
|
|
2030
|
+
let entry;
|
|
2031
|
+
try {
|
|
2032
|
+
entry = JSON.parse(trimmed);
|
|
2033
|
+
} catch {
|
|
2034
|
+
continue;
|
|
2035
|
+
}
|
|
2036
|
+
if (!cwd && typeof entry.cwd === "string") {
|
|
2037
|
+
cwd = entry.cwd;
|
|
2038
|
+
}
|
|
2039
|
+
if (!id && typeof entry.sessionId === "string") {
|
|
2040
|
+
id = entry.sessionId;
|
|
2041
|
+
}
|
|
2042
|
+
if (!gitBranch && typeof entry.gitBranch === "string") {
|
|
2043
|
+
gitBranch = entry.gitBranch;
|
|
2044
|
+
}
|
|
2045
|
+
if (!started && typeof entry.timestamp === "string") {
|
|
2046
|
+
started = entry.timestamp;
|
|
2047
|
+
}
|
|
2048
|
+
if (!title && entry.type === "user") {
|
|
2049
|
+
const msg = entry.message;
|
|
2050
|
+
let text = typeof msg?.content === "string" ? msg.content : "";
|
|
2051
|
+
if (!text && Array.isArray(msg?.parts)) {
|
|
2052
|
+
text = msg.parts.map(
|
|
2053
|
+
(p) => p && typeof p === "object" && typeof p.text === "string" ? p.text : ""
|
|
2054
|
+
).join(" ").trim();
|
|
2055
|
+
}
|
|
2056
|
+
if (text && !SYSTEM_TAG_PREFIXES.some((p) => text.startsWith(p))) {
|
|
2057
|
+
title = text.split("\n")[0]?.slice(0, 80) ?? "";
|
|
2058
|
+
}
|
|
2059
|
+
}
|
|
2060
|
+
}
|
|
2061
|
+
return id && cwd ? { cwd, gitBranch, id, started, title } : null;
|
|
2062
|
+
}
|
|
2063
|
+
function readPrefix(filePath) {
|
|
2064
|
+
const fd = fs.openSync(filePath, "r");
|
|
2065
|
+
try {
|
|
2066
|
+
const buf = Buffer.alloc(PREFIX_BYTES);
|
|
2067
|
+
const n = fs.readSync(fd, buf, 0, PREFIX_BYTES, 0);
|
|
2068
|
+
const text = buf.toString("utf-8", 0, n);
|
|
2069
|
+
const lastNl = text.lastIndexOf("\n");
|
|
2070
|
+
return lastNl === -1 ? text : text.slice(0, lastNl);
|
|
2071
|
+
} finally {
|
|
2072
|
+
fs.closeSync(fd);
|
|
2073
|
+
}
|
|
2074
|
+
}
|
|
2075
|
+
function readQwenMessages(filePath) {
|
|
2076
|
+
const messages = [];
|
|
2077
|
+
for (const line of readQwenTextBounded(filePath).split("\n")) {
|
|
2078
|
+
const trimmed = line.trim();
|
|
2079
|
+
if (!trimmed) {
|
|
2080
|
+
continue;
|
|
2081
|
+
}
|
|
2082
|
+
try {
|
|
2083
|
+
const msg = qwenLineToMessage(JSON.parse(trimmed));
|
|
2084
|
+
if (msg) {
|
|
2085
|
+
messages.push(msg);
|
|
2086
|
+
}
|
|
2087
|
+
} catch {
|
|
2088
|
+
}
|
|
2089
|
+
}
|
|
2090
|
+
return messages;
|
|
2091
|
+
}
|
|
2092
|
+
function readQwenTextBounded(filePath) {
|
|
2093
|
+
const size = fs.statSync(filePath).size;
|
|
2094
|
+
if (size <= MAX_QWEN_FILE_BYTES) {
|
|
2095
|
+
return fs.readFileSync(filePath, "utf-8");
|
|
2096
|
+
}
|
|
2097
|
+
const fd = fs.openSync(filePath, "r");
|
|
2098
|
+
try {
|
|
2099
|
+
const buf = Buffer.alloc(MAX_QWEN_FILE_BYTES);
|
|
2100
|
+
const n = fs.readSync(fd, buf, 0, MAX_QWEN_FILE_BYTES, size - MAX_QWEN_FILE_BYTES);
|
|
2101
|
+
const tail = buf.toString("utf-8", 0, n);
|
|
2102
|
+
return tail.slice(tail.indexOf("\n") + 1);
|
|
2103
|
+
} finally {
|
|
2104
|
+
fs.closeSync(fd);
|
|
2105
|
+
}
|
|
2106
|
+
}
|
|
2107
|
+
function shortHash(input) {
|
|
2108
|
+
return crypto.createHash("sha256").update(input).digest("hex").slice(0, 8);
|
|
2109
|
+
}
|
|
2110
|
+
|
|
2111
|
+
export { detectActiveGeminiSessions as a, detectActiveQwenSessions as b, detectActiveCursorSessions as c, detectActiveCodexSessions as d, detectActiveCopilotSessions as e, detectActiveAmpSessions as f, getCodexConversation as g, listGeminiProjects as h, getGeminiConversation as i, listQwenProjects as j, getQwenConversation as k, listCodexProjects as l, listCursorProjects as m, getCursorConversation as n, listCopilotProjects as o, getCopilotConversation as p, listAmpProjects as q, resolveOpenCodeDbPath as r, getAmpConversation as s, findCodexRolloutForCwd as t, resolveQwenSessionFile as u, resolveGeminiSessionFile as v, resolveCursorSessionFile as w, resolveCopilotSessionFile as x, resolveAmpSessionFile as y };
|
|
2112
|
+
//# sourceMappingURL=qwen-reader-DGfUbKaJ.js.map
|