@akiojin/gwt 2.11.1 → 2.12.1

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.
Files changed (76) hide show
  1. package/dist/claude.d.ts +4 -1
  2. package/dist/claude.d.ts.map +1 -1
  3. package/dist/claude.js +51 -7
  4. package/dist/claude.js.map +1 -1
  5. package/dist/cli/ui/components/App.d.ts +7 -0
  6. package/dist/cli/ui/components/App.d.ts.map +1 -1
  7. package/dist/cli/ui/components/App.js +307 -18
  8. package/dist/cli/ui/components/App.js.map +1 -1
  9. package/dist/cli/ui/components/screens/BranchQuickStartScreen.d.ts +21 -0
  10. package/dist/cli/ui/components/screens/BranchQuickStartScreen.d.ts.map +1 -0
  11. package/dist/cli/ui/components/screens/BranchQuickStartScreen.js +145 -0
  12. package/dist/cli/ui/components/screens/BranchQuickStartScreen.js.map +1 -0
  13. package/dist/cli/ui/components/screens/ExecutionModeSelectorScreen.d.ts +2 -1
  14. package/dist/cli/ui/components/screens/ExecutionModeSelectorScreen.d.ts.map +1 -1
  15. package/dist/cli/ui/components/screens/ExecutionModeSelectorScreen.js +4 -2
  16. package/dist/cli/ui/components/screens/ExecutionModeSelectorScreen.js.map +1 -1
  17. package/dist/cli/ui/components/screens/ModelSelectorScreen.js +1 -1
  18. package/dist/cli/ui/components/screens/SessionSelectorScreen.d.ts +10 -2
  19. package/dist/cli/ui/components/screens/SessionSelectorScreen.d.ts.map +1 -1
  20. package/dist/cli/ui/components/screens/SessionSelectorScreen.js +18 -7
  21. package/dist/cli/ui/components/screens/SessionSelectorScreen.js.map +1 -1
  22. package/dist/cli/ui/types.d.ts +1 -1
  23. package/dist/cli/ui/types.d.ts.map +1 -1
  24. package/dist/cli/ui/utils/continueSession.d.ts +18 -0
  25. package/dist/cli/ui/utils/continueSession.d.ts.map +1 -0
  26. package/dist/cli/ui/utils/continueSession.js +67 -0
  27. package/dist/cli/ui/utils/continueSession.js.map +1 -0
  28. package/dist/codex.d.ts +4 -1
  29. package/dist/codex.d.ts.map +1 -1
  30. package/dist/codex.js +70 -5
  31. package/dist/codex.js.map +1 -1
  32. package/dist/config/index.d.ts +9 -1
  33. package/dist/config/index.d.ts.map +1 -1
  34. package/dist/config/index.js +11 -2
  35. package/dist/config/index.js.map +1 -1
  36. package/dist/gemini.d.ts +4 -1
  37. package/dist/gemini.d.ts.map +1 -1
  38. package/dist/gemini.js +146 -32
  39. package/dist/gemini.js.map +1 -1
  40. package/dist/index.d.ts.map +1 -1
  41. package/dist/index.js +119 -48
  42. package/dist/index.js.map +1 -1
  43. package/dist/qwen.d.ts +4 -1
  44. package/dist/qwen.d.ts.map +1 -1
  45. package/dist/qwen.js +45 -4
  46. package/dist/qwen.js.map +1 -1
  47. package/dist/utils/prompt.d.ts +6 -0
  48. package/dist/utils/prompt.d.ts.map +1 -0
  49. package/dist/utils/prompt.js +57 -0
  50. package/dist/utils/prompt.js.map +1 -0
  51. package/dist/utils/session.d.ts +82 -0
  52. package/dist/utils/session.d.ts.map +1 -0
  53. package/dist/utils/session.js +579 -0
  54. package/dist/utils/session.js.map +1 -0
  55. package/package.json +2 -2
  56. package/src/claude.ts +69 -8
  57. package/src/cli/ui/__tests__/components/App.protected-branch.test.tsx +12 -2
  58. package/src/cli/ui/__tests__/components/screens/BranchQuickStartScreen.test.tsx +142 -0
  59. package/src/cli/ui/__tests__/components/screens/ExecutionModeSelectorScreen.test.tsx +14 -0
  60. package/src/cli/ui/__tests__/components/screens/SessionSelectorScreen.test.tsx +29 -10
  61. package/src/cli/ui/__tests__/integration/edgeCases.test.tsx +4 -1
  62. package/src/cli/ui/components/App.tsx +403 -23
  63. package/src/cli/ui/components/screens/BranchQuickStartScreen.tsx +237 -0
  64. package/src/cli/ui/components/screens/ExecutionModeSelectorScreen.tsx +5 -1
  65. package/src/cli/ui/components/screens/ModelSelectorScreen.tsx +1 -1
  66. package/src/cli/ui/components/screens/SessionSelectorScreen.tsx +34 -6
  67. package/src/cli/ui/types.ts +1 -0
  68. package/src/cli/ui/utils/continueSession.ts +106 -0
  69. package/src/codex.ts +91 -6
  70. package/src/config/index.ts +22 -2
  71. package/src/gemini.ts +179 -41
  72. package/src/index.ts +145 -61
  73. package/src/qwen.ts +56 -5
  74. package/src/utils/__tests__/prompt.test.ts +89 -0
  75. package/src/utils/prompt.ts +74 -0
  76. package/src/utils/session.ts +704 -0
@@ -0,0 +1,82 @@
1
+ /**
2
+ * Validates that a string is a properly formatted UUID session ID.
3
+ * @param id - The string to validate
4
+ * @returns true if the string is a valid UUID format
5
+ */
6
+ export declare function isValidUuidSessionId(id: string): boolean;
7
+ export interface CodexSessionInfo {
8
+ id: string;
9
+ mtime: number;
10
+ }
11
+ export interface GeminiSessionInfo {
12
+ id: string;
13
+ mtime: number;
14
+ }
15
+ export declare function findLatestCodexSession(options?: {
16
+ since?: number;
17
+ until?: number;
18
+ preferClosestTo?: number;
19
+ windowMs?: number;
20
+ cwd?: string | null;
21
+ }): Promise<CodexSessionInfo | null>;
22
+ export declare function findLatestCodexSessionId(options?: {
23
+ since?: number;
24
+ until?: number;
25
+ preferClosestTo?: number;
26
+ windowMs?: number;
27
+ cwd?: string | null;
28
+ }): Promise<string | null>;
29
+ export declare function waitForCodexSessionId(options: {
30
+ startedAt: number;
31
+ timeoutMs?: number;
32
+ pollIntervalMs?: number;
33
+ cwd?: string | null;
34
+ }): Promise<string | null>;
35
+ export declare function encodeClaudeProjectPath(cwd: string): string;
36
+ export declare function findLatestClaudeSessionId(cwd: string, options?: {
37
+ since?: number;
38
+ until?: number;
39
+ preferClosestTo?: number;
40
+ windowMs?: number;
41
+ }): Promise<string | null>;
42
+ export interface ClaudeSessionInfo {
43
+ id: string;
44
+ mtime: number;
45
+ }
46
+ export declare function findLatestClaudeSession(cwd: string, options?: {
47
+ since?: number;
48
+ until?: number;
49
+ preferClosestTo?: number;
50
+ windowMs?: number;
51
+ }): Promise<ClaudeSessionInfo | null>;
52
+ export declare function waitForClaudeSessionId(cwd: string, options?: {
53
+ timeoutMs?: number;
54
+ pollIntervalMs?: number;
55
+ since?: number;
56
+ until?: number;
57
+ preferClosestTo?: number;
58
+ windowMs?: number;
59
+ }): Promise<string | null>;
60
+ export declare function findLatestGeminiSession(_cwd: string, options?: {
61
+ since?: number;
62
+ until?: number;
63
+ preferClosestTo?: number;
64
+ windowMs?: number;
65
+ cwd?: string | null;
66
+ }): Promise<GeminiSessionInfo | null>;
67
+ export declare function findLatestGeminiSessionId(cwd: string, options?: {
68
+ since?: number;
69
+ until?: number;
70
+ preferClosestTo?: number;
71
+ windowMs?: number;
72
+ cwd?: string | null;
73
+ }): Promise<string | null>;
74
+ export declare function findLatestQwenSessionId(_cwd: string): Promise<string | null>;
75
+ /**
76
+ * Checks if a Claude session file exists for the given session ID and worktree path.
77
+ * @param sessionId - The session ID to check
78
+ * @param worktreePath - The worktree path (used to determine project encoding)
79
+ * @returns true if a session file exists for this ID
80
+ */
81
+ export declare function claudeSessionFileExists(sessionId: string, worktreePath: string): Promise<boolean>;
82
+ //# sourceMappingURL=session.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"session.d.ts","sourceRoot":"","sources":["../../src/utils/session.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,579 @@
1
+ import path from "node:path";
2
+ import { homedir } from "node:os";
3
+ import { readdir, readFile, stat } from "node:fs/promises";
4
+ const UUID_REGEX = /[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}/i;
5
+ /**
6
+ * Validates that a string is a properly formatted UUID session ID.
7
+ * @param id - The string to validate
8
+ * @returns true if the string is a valid UUID format
9
+ */
10
+ export function isValidUuidSessionId(id) {
11
+ return /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(id);
12
+ }
13
+ function pickSessionIdFromObject(obj) {
14
+ if (!obj || typeof obj !== "object")
15
+ return null;
16
+ const candidate = obj;
17
+ const keys = ["sessionId", "session_id", "id", "conversation_id"];
18
+ for (const key of keys) {
19
+ const value = candidate[key];
20
+ if (typeof value === "string" && value.trim().length > 0) {
21
+ const trimmed = value.trim();
22
+ // Only accept values that are valid UUIDs to avoid picking up arbitrary strings
23
+ if (isValidUuidSessionId(trimmed)) {
24
+ return trimmed;
25
+ }
26
+ }
27
+ }
28
+ return null;
29
+ }
30
+ function pickCwdFromObject(obj) {
31
+ if (!obj || typeof obj !== "object")
32
+ return null;
33
+ const candidate = obj;
34
+ const keys = ["cwd", "workingDirectory", "workdir", "directory", "projectPath"];
35
+ for (const key of keys) {
36
+ const value = candidate[key];
37
+ if (typeof value === "string" && value.trim().length > 0) {
38
+ return value;
39
+ }
40
+ }
41
+ // Check nested payload object (for Codex session format)
42
+ const payload = candidate["payload"];
43
+ if (payload && typeof payload === "object") {
44
+ const nested = pickCwdFromObject(payload);
45
+ if (nested)
46
+ return nested;
47
+ }
48
+ return null;
49
+ }
50
+ function pickSessionIdFromText(content) {
51
+ // Try whole content as JSON
52
+ try {
53
+ const parsed = JSON.parse(content);
54
+ const fromObject = pickSessionIdFromObject(parsed);
55
+ if (fromObject)
56
+ return fromObject;
57
+ }
58
+ catch {
59
+ // ignore
60
+ }
61
+ // Try JSONL lines
62
+ const lines = content.split(/\r?\n/);
63
+ for (const line of lines) {
64
+ const trimmed = line.trim();
65
+ if (!trimmed)
66
+ continue;
67
+ try {
68
+ const parsedLine = JSON.parse(trimmed);
69
+ const fromLine = pickSessionIdFromObject(parsedLine);
70
+ if (fromLine)
71
+ return fromLine;
72
+ }
73
+ catch {
74
+ // ignore
75
+ }
76
+ const match = trimmed.match(UUID_REGEX);
77
+ if (match)
78
+ return match[0];
79
+ }
80
+ // Fallback: find any UUID in the whole text
81
+ const match = content.match(UUID_REGEX);
82
+ return match ? match[0] : null;
83
+ }
84
+ async function findLatestFile(dir, filter) {
85
+ try {
86
+ const entries = await readdir(dir, { withFileTypes: true });
87
+ const files = entries.filter((e) => e.isFile()).map((e) => e.name);
88
+ const filtered = files.filter(filter);
89
+ if (!filtered.length)
90
+ return null;
91
+ const withStats = await Promise.all(filtered.map(async (name) => {
92
+ const fullPath = path.join(dir, name);
93
+ try {
94
+ const info = await stat(fullPath);
95
+ return { fullPath, mtime: info.mtimeMs };
96
+ }
97
+ catch {
98
+ return null;
99
+ }
100
+ }));
101
+ const valid = withStats.filter((entry) => Boolean(entry));
102
+ if (!valid.length)
103
+ return null;
104
+ valid.sort((a, b) => b.mtime - a.mtime);
105
+ return valid[0]?.fullPath ?? null;
106
+ }
107
+ catch {
108
+ return null;
109
+ }
110
+ }
111
+ async function findNewestSessionIdFromDir(dir, recursive, options = {}) {
112
+ try {
113
+ const files = [];
114
+ const processDir = async (currentDir) => {
115
+ const currentEntries = await readdir(currentDir, { withFileTypes: true });
116
+ for (const entry of currentEntries) {
117
+ const fullPath = path.join(currentDir, entry.name);
118
+ if (entry.isDirectory()) {
119
+ if (recursive) {
120
+ await processDir(fullPath);
121
+ }
122
+ continue;
123
+ }
124
+ if (!entry.isFile())
125
+ continue;
126
+ if (!entry.name.endsWith(".json") && !entry.name.endsWith(".jsonl"))
127
+ continue;
128
+ try {
129
+ const info = await stat(fullPath);
130
+ files.push({ fullPath, mtime: info.mtimeMs });
131
+ }
132
+ catch {
133
+ // ignore unreadable
134
+ }
135
+ }
136
+ };
137
+ await processDir(dir);
138
+ // Apply since/until filters clearly
139
+ const filtered = files.filter((f) => {
140
+ if (options.since !== undefined && f.mtime < options.since)
141
+ return false;
142
+ if (options.until !== undefined && f.mtime > options.until)
143
+ return false;
144
+ return true;
145
+ });
146
+ if (!filtered.length)
147
+ return null;
148
+ // Sort by mtime descending (newest first)
149
+ let pool = filtered.sort((a, b) => b.mtime - a.mtime);
150
+ // Apply preferClosestTo window if specified
151
+ const ref = options.preferClosestTo;
152
+ if (typeof ref === "number") {
153
+ const window = options.windowMs ?? 30 * 60 * 1000;
154
+ const withinWindow = pool.filter((f) => Math.abs(f.mtime - ref) <= window);
155
+ if (withinWindow.length) {
156
+ pool = withinWindow.sort((a, b) => b.mtime - a.mtime);
157
+ }
158
+ }
159
+ for (const file of pool) {
160
+ const id = await readSessionIdFromFile(file.fullPath);
161
+ if (id)
162
+ return { id, mtime: file.mtime };
163
+ }
164
+ }
165
+ catch {
166
+ // ignore
167
+ }
168
+ return null;
169
+ }
170
+ async function readSessionIdFromFile(filePath) {
171
+ try {
172
+ // Priority 1: Use filename UUID (most reliable for Claude session files)
173
+ // Claude session files are named with their session ID: {uuid}.jsonl
174
+ const basename = path.basename(filePath);
175
+ const filenameWithoutExt = basename.replace(/\.(json|jsonl)$/i, "");
176
+ if (isValidUuidSessionId(filenameWithoutExt)) {
177
+ return filenameWithoutExt;
178
+ }
179
+ // Priority 2: Extract from file content (for other formats)
180
+ const content = await readFile(filePath, "utf-8");
181
+ const fromContent = pickSessionIdFromText(content);
182
+ if (fromContent)
183
+ return fromContent;
184
+ // Priority 3: Fallback to any UUID in filename
185
+ const filenameMatch = basename.match(UUID_REGEX);
186
+ return filenameMatch ? filenameMatch[0] : null;
187
+ }
188
+ catch {
189
+ return null;
190
+ }
191
+ }
192
+ async function readSessionInfoFromFile(filePath) {
193
+ try {
194
+ const content = await readFile(filePath, "utf-8");
195
+ try {
196
+ const parsed = JSON.parse(content);
197
+ const id = pickSessionIdFromObject(parsed);
198
+ const cwd = pickCwdFromObject(parsed);
199
+ if (id || cwd)
200
+ return { id, cwd };
201
+ }
202
+ catch {
203
+ // ignore
204
+ }
205
+ const lines = content.split(/\r?\n/);
206
+ for (const line of lines) {
207
+ const trimmed = line.trim();
208
+ if (!trimmed)
209
+ continue;
210
+ try {
211
+ const parsedLine = JSON.parse(trimmed);
212
+ const id = pickSessionIdFromObject(parsedLine);
213
+ const cwd = pickCwdFromObject(parsedLine);
214
+ if (id || cwd)
215
+ return { id, cwd };
216
+ }
217
+ catch {
218
+ // ignore
219
+ }
220
+ }
221
+ // Fallback: filename UUID
222
+ const filenameMatch = path.basename(filePath).match(UUID_REGEX);
223
+ if (filenameMatch)
224
+ return { id: filenameMatch[0], cwd: null };
225
+ }
226
+ catch {
227
+ // ignore unreadable
228
+ }
229
+ return { id: null, cwd: null };
230
+ }
231
+ async function collectFilesRecursive(dir, filter) {
232
+ const results = [];
233
+ try {
234
+ const entries = await readdir(dir, { withFileTypes: true });
235
+ for (const entry of entries) {
236
+ const fullPath = path.join(dir, entry.name);
237
+ if (entry.isDirectory()) {
238
+ const nested = await collectFilesRecursive(fullPath, filter);
239
+ results.push(...nested);
240
+ }
241
+ else if (entry.isFile() && filter(entry.name)) {
242
+ try {
243
+ const info = await stat(fullPath);
244
+ results.push({ fullPath, mtime: info.mtimeMs });
245
+ }
246
+ catch {
247
+ // ignore unreadable file
248
+ }
249
+ }
250
+ }
251
+ }
252
+ catch {
253
+ // ignore unreadable directory
254
+ }
255
+ return results;
256
+ }
257
+ export async function findLatestCodexSession(options = {}) {
258
+ // Codex CLI respects CODEX_HOME. Default is ~/.codex.
259
+ const codexHome = process.env.CODEX_HOME ?? path.join(homedir(), ".codex");
260
+ const baseDir = path.join(codexHome, "sessions");
261
+ const candidates = await collectFilesRecursive(baseDir, (name) => name.endsWith(".json") || name.endsWith(".jsonl"));
262
+ if (!candidates.length)
263
+ return null;
264
+ const sinceFiltered = options.since
265
+ ? candidates.filter((c) => c.mtime >= options.since)
266
+ : candidates;
267
+ const bounded = options.until !== undefined
268
+ ? sinceFiltered.filter((c) => c.mtime <= options.until)
269
+ : sinceFiltered;
270
+ const hasWindow = options.since !== undefined || options.until !== undefined;
271
+ const pool = bounded.length ? bounded : hasWindow ? [] : sinceFiltered;
272
+ if (!pool.length)
273
+ return null;
274
+ const ref = options.preferClosestTo;
275
+ const window = options.windowMs ?? 30 * 60 * 1000; // 30 minutes default
276
+ const ordered = [...pool].sort((a, b) => {
277
+ if (typeof ref === "number") {
278
+ const da = Math.abs(a.mtime - ref);
279
+ const db = Math.abs(b.mtime - ref);
280
+ if (da === db)
281
+ return b.mtime - a.mtime;
282
+ if (da <= window || db <= window)
283
+ return da - db;
284
+ }
285
+ return b.mtime - a.mtime;
286
+ });
287
+ for (const file of ordered) {
288
+ // Priority 1: Extract session ID from filename (most reliable for Codex)
289
+ // Codex filenames follow pattern: rollout-YYYY-MM-DDTHH-MM-SS-{uuid}.jsonl
290
+ const filenameMatch = path.basename(file.fullPath).match(UUID_REGEX);
291
+ if (filenameMatch) {
292
+ const sessionId = filenameMatch[0];
293
+ // If cwd filtering is needed, read file content to check cwd
294
+ if (options.cwd) {
295
+ const info = await readSessionInfoFromFile(file.fullPath);
296
+ if (info.cwd &&
297
+ // Match if: exact match, session cwd starts with options.cwd,
298
+ // or options.cwd starts with session cwd (for worktree subdirectories)
299
+ (info.cwd === options.cwd ||
300
+ info.cwd.startsWith(options.cwd) ||
301
+ options.cwd.startsWith(info.cwd))) {
302
+ return { id: sessionId, mtime: file.mtime };
303
+ }
304
+ continue; // cwd doesn't match, try next file
305
+ }
306
+ return { id: sessionId, mtime: file.mtime };
307
+ }
308
+ // Priority 2: Fallback to reading file content if filename lacks UUID
309
+ const info = await readSessionInfoFromFile(file.fullPath);
310
+ if (!info.id)
311
+ continue;
312
+ if (options.cwd) {
313
+ if (info.cwd &&
314
+ // Match if: exact match, session cwd starts with options.cwd,
315
+ // or options.cwd starts with session cwd (for worktree subdirectories)
316
+ (info.cwd === options.cwd ||
317
+ info.cwd.startsWith(options.cwd) ||
318
+ options.cwd.startsWith(info.cwd))) {
319
+ return { id: info.id, mtime: file.mtime };
320
+ }
321
+ continue;
322
+ }
323
+ return { id: info.id, mtime: file.mtime };
324
+ }
325
+ return null;
326
+ }
327
+ export async function findLatestCodexSessionId(options = {}) {
328
+ const found = await findLatestCodexSession(options);
329
+ return found?.id ?? null;
330
+ }
331
+ export async function waitForCodexSessionId(options) {
332
+ const timeoutMs = options.timeoutMs ?? 120_000;
333
+ const pollIntervalMs = options.pollIntervalMs ?? 2_000;
334
+ const deadline = Date.now() + timeoutMs;
335
+ while (Date.now() < deadline) {
336
+ const found = await findLatestCodexSession({
337
+ since: options.startedAt,
338
+ preferClosestTo: options.startedAt,
339
+ windowMs: 10 * 60 * 1000,
340
+ cwd: options.cwd ?? null,
341
+ });
342
+ if (found?.id)
343
+ return found.id;
344
+ await new Promise((resolve) => setTimeout(resolve, pollIntervalMs));
345
+ }
346
+ return null;
347
+ }
348
+ export function encodeClaudeProjectPath(cwd) {
349
+ // Normalize to forward slashes, drop drive colon, replace / and _ with -
350
+ const normalized = cwd.replace(/\\/g, "/").replace(/:/g, "");
351
+ return normalized.replace(/_/g, "-").replace(/\//g, "-");
352
+ }
353
+ function generateClaudeProjectPathCandidates(cwd) {
354
+ const base = encodeClaudeProjectPath(cwd);
355
+ const dotToDash = cwd
356
+ .replace(/\\/g, "/")
357
+ .replace(/:/g, "")
358
+ .replace(/\./g, "-")
359
+ .replace(/_/g, "-")
360
+ .replace(/\//g, "-");
361
+ const collapsed = dotToDash.replace(/-+/g, "-");
362
+ const candidates = [base, dotToDash, collapsed];
363
+ return Array.from(new Set(candidates));
364
+ }
365
+ export async function findLatestClaudeSessionId(cwd, options = {}) {
366
+ const found = await findLatestClaudeSession(cwd, options);
367
+ return found?.id ?? null;
368
+ }
369
+ export async function findLatestClaudeSession(cwd, options = {}) {
370
+ const rootCandidates = [];
371
+ if (process.env.CLAUDE_CONFIG_DIR) {
372
+ rootCandidates.push(process.env.CLAUDE_CONFIG_DIR);
373
+ }
374
+ rootCandidates.push(path.join(homedir(), ".claude"), path.join(homedir(), ".config", "claude"));
375
+ const encodedPaths = generateClaudeProjectPathCandidates(cwd);
376
+ for (const claudeRoot of rootCandidates) {
377
+ for (const encoded of encodedPaths) {
378
+ const projectDir = path.join(claudeRoot, "projects", encoded);
379
+ const sessionsDir = path.join(projectDir, "sessions");
380
+ // 1) Look under sessions/ (official location) - prefer newest file with valid ID
381
+ const session = await findNewestSessionIdFromDir(sessionsDir, false, options);
382
+ if (session)
383
+ return session;
384
+ // 2) Look directly under project dir and subdirs (some versions emit files at root)
385
+ const rootSession = await findNewestSessionIdFromDir(projectDir, true, options);
386
+ if (rootSession)
387
+ return rootSession;
388
+ }
389
+ }
390
+ // Fallback: parse ~/.claude/history.jsonl (Claude Code global history)
391
+ try {
392
+ const historyPath = path.join(homedir(), ".claude", "history.jsonl");
393
+ const content = await readFile(historyPath, "utf-8");
394
+ const lines = content.split(/\r?\n/).filter(Boolean);
395
+ for (let i = lines.length - 1; i >= 0; i -= 1) {
396
+ try {
397
+ const line = lines[i] ?? "";
398
+ const parsed = JSON.parse(line);
399
+ const project = typeof parsed.project === "string" ? parsed.project : null;
400
+ const sessionId = typeof parsed.sessionId === "string" ? parsed.sessionId : null;
401
+ if (project && sessionId && (project === cwd || cwd.startsWith(project))) {
402
+ return { id: sessionId, mtime: Date.now() };
403
+ }
404
+ }
405
+ catch {
406
+ // ignore malformed lines
407
+ }
408
+ }
409
+ }
410
+ catch {
411
+ // ignore if history not present
412
+ }
413
+ return null;
414
+ }
415
+ export async function waitForClaudeSessionId(cwd, options = {}) {
416
+ const timeoutMs = options.timeoutMs ?? 120_000;
417
+ const pollIntervalMs = options.pollIntervalMs ?? 2_000;
418
+ const deadline = Date.now() + timeoutMs;
419
+ while (Date.now() < deadline) {
420
+ const opt = {};
421
+ if (options.since !== undefined)
422
+ opt.since = options.since;
423
+ if (options.until !== undefined)
424
+ opt.until = options.until;
425
+ if (options.preferClosestTo !== undefined)
426
+ opt.preferClosestTo = options.preferClosestTo;
427
+ if (options.windowMs !== undefined)
428
+ opt.windowMs = options.windowMs;
429
+ const found = await findLatestClaudeSession(cwd, opt);
430
+ if (found?.id)
431
+ return found.id;
432
+ await new Promise((resolve) => setTimeout(resolve, pollIntervalMs));
433
+ }
434
+ return null;
435
+ }
436
+ async function findLatestNestedSessionFile(baseDir, subPath, predicate) {
437
+ try {
438
+ const entries = await readdir(baseDir);
439
+ if (!entries.length)
440
+ return null;
441
+ const candidates = [];
442
+ for (const entry of entries) {
443
+ const dirPath = path.join(baseDir, entry, ...subPath);
444
+ const latest = await findLatestFile(dirPath, predicate);
445
+ if (latest) {
446
+ try {
447
+ const info = await stat(latest);
448
+ candidates.push({ fullPath: latest, mtime: info.mtimeMs });
449
+ }
450
+ catch {
451
+ // ignore
452
+ }
453
+ }
454
+ }
455
+ if (!candidates.length)
456
+ return null;
457
+ candidates.sort((a, b) => b.mtime - a.mtime);
458
+ return candidates[0]?.fullPath ?? null;
459
+ }
460
+ catch {
461
+ return null;
462
+ }
463
+ }
464
+ export async function findLatestGeminiSession(_cwd, options = {}) {
465
+ // Gemini stores sessions/logs under ~/.gemini/tmp/<project_hash>/(chats|logs).json
466
+ const baseDir = path.join(homedir(), ".gemini", "tmp");
467
+ const files = await collectFilesRecursive(baseDir, (name) => name.endsWith(".json") || name.endsWith(".jsonl"));
468
+ if (!files.length)
469
+ return null;
470
+ let pool = files;
471
+ if (options.since !== undefined) {
472
+ pool = pool.filter((f) => f.mtime >= options.since);
473
+ }
474
+ if (options.until !== undefined) {
475
+ pool = pool.filter((f) => f.mtime <= options.until);
476
+ }
477
+ const hasWindow = options.since !== undefined || options.until !== undefined;
478
+ if (!pool.length) {
479
+ if (!hasWindow) {
480
+ pool = files;
481
+ }
482
+ else {
483
+ return null;
484
+ }
485
+ }
486
+ const ref = options.preferClosestTo;
487
+ const window = options.windowMs ?? 30 * 60 * 1000;
488
+ pool = pool
489
+ .slice()
490
+ .sort((a, b) => {
491
+ if (typeof ref === "number") {
492
+ const da = Math.abs(a.mtime - ref);
493
+ const db = Math.abs(b.mtime - ref);
494
+ if (da === db)
495
+ return b.mtime - a.mtime;
496
+ if (da <= window || db <= window)
497
+ return da - db;
498
+ }
499
+ return b.mtime - a.mtime;
500
+ });
501
+ for (const file of pool) {
502
+ const info = await readSessionInfoFromFile(file.fullPath);
503
+ if (!info.id)
504
+ continue;
505
+ if (options.cwd) {
506
+ if (info.cwd &&
507
+ (info.cwd === options.cwd || info.cwd.startsWith(options.cwd))) {
508
+ return { id: info.id, mtime: file.mtime };
509
+ }
510
+ continue;
511
+ }
512
+ return { id: info.id, mtime: file.mtime };
513
+ }
514
+ return null;
515
+ }
516
+ export async function findLatestGeminiSessionId(cwd, options = {}) {
517
+ const normalized = {};
518
+ if (options.since !== undefined)
519
+ normalized.since = options.since;
520
+ if (options.until !== undefined)
521
+ normalized.until = options.until;
522
+ if (options.preferClosestTo !== undefined)
523
+ normalized.preferClosestTo = options.preferClosestTo;
524
+ if (options.windowMs !== undefined)
525
+ normalized.windowMs = options.windowMs;
526
+ const found = await findLatestGeminiSession(cwd, { ...normalized, cwd: options.cwd ?? cwd });
527
+ return found?.id ?? null;
528
+ }
529
+ export async function findLatestQwenSessionId(_cwd) {
530
+ // Qwen stores checkpoints/saves under ~/.qwen/tmp/<project_hash>/
531
+ const baseDir = path.join(homedir(), ".qwen", "tmp");
532
+ const latest = (await findLatestNestedSessionFile(baseDir, [], (name) => name.endsWith(".json") || name.endsWith(".jsonl"))) ??
533
+ (await findLatestNestedSessionFile(baseDir, ["checkpoints"], (name) => name.endsWith(".json") || name.endsWith(".ckpt")));
534
+ if (!latest)
535
+ return null;
536
+ const fromContent = await readSessionIdFromFile(latest);
537
+ if (fromContent)
538
+ return fromContent;
539
+ // Fallback: use filename (without extension) as tag
540
+ return path.basename(latest).replace(/\.[^.]+$/, "");
541
+ }
542
+ /**
543
+ * Returns the list of possible Claude root directories.
544
+ */
545
+ function getClaudeRootCandidates() {
546
+ const roots = [];
547
+ if (process.env.CLAUDE_CONFIG_DIR) {
548
+ roots.push(process.env.CLAUDE_CONFIG_DIR);
549
+ }
550
+ roots.push(path.join(homedir(), ".claude"), path.join(homedir(), ".config", "claude"));
551
+ return roots;
552
+ }
553
+ /**
554
+ * Checks if a Claude session file exists for the given session ID and worktree path.
555
+ * @param sessionId - The session ID to check
556
+ * @param worktreePath - The worktree path (used to determine project encoding)
557
+ * @returns true if a session file exists for this ID
558
+ */
559
+ export async function claudeSessionFileExists(sessionId, worktreePath) {
560
+ if (!isValidUuidSessionId(sessionId)) {
561
+ return false;
562
+ }
563
+ const encodedPaths = generateClaudeProjectPathCandidates(worktreePath);
564
+ const roots = getClaudeRootCandidates();
565
+ for (const root of roots) {
566
+ for (const enc of encodedPaths) {
567
+ const candidate = path.join(root, "projects", enc, `${sessionId}.jsonl`);
568
+ try {
569
+ await stat(candidate);
570
+ return true;
571
+ }
572
+ catch {
573
+ // continue to next candidate
574
+ }
575
+ }
576
+ }
577
+ return false;
578
+ }
579
+ //# sourceMappingURL=session.js.map