@akiojin/gwt 3.1.0 → 3.1.2

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 (65) hide show
  1. package/README.ja.md +1 -1
  2. package/README.md +1 -1
  3. package/dist/cli/ui/components/App.d.ts.map +1 -1
  4. package/dist/cli/ui/components/App.js +8 -8
  5. package/dist/cli/ui/components/App.js.map +1 -1
  6. package/dist/cli/ui/components/screens/BranchListScreen.d.ts.map +1 -1
  7. package/dist/cli/ui/components/screens/BranchListScreen.js +9 -5
  8. package/dist/cli/ui/components/screens/BranchListScreen.js.map +1 -1
  9. package/dist/cli/ui/utils/branchFormatter.d.ts.map +1 -1
  10. package/dist/cli/ui/utils/branchFormatter.js +2 -2
  11. package/dist/cli/ui/utils/branchFormatter.js.map +1 -1
  12. package/dist/index.js +1 -1
  13. package/dist/index.js.map +1 -1
  14. package/dist/utils/session/common.d.ts +100 -0
  15. package/dist/utils/session/common.d.ts.map +1 -0
  16. package/dist/utils/session/common.js +417 -0
  17. package/dist/utils/session/common.js.map +1 -0
  18. package/dist/utils/session/index.d.ts +16 -0
  19. package/dist/utils/session/index.d.ts.map +1 -0
  20. package/dist/utils/session/index.js +20 -0
  21. package/dist/utils/session/index.js.map +1 -0
  22. package/dist/utils/session/parsers/claude.d.ts +56 -0
  23. package/dist/utils/session/parsers/claude.d.ts.map +1 -0
  24. package/dist/utils/session/parsers/claude.js +178 -0
  25. package/dist/utils/session/parsers/claude.js.map +1 -0
  26. package/dist/utils/session/parsers/codex.d.ts +37 -0
  27. package/dist/utils/session/parsers/codex.d.ts.map +1 -0
  28. package/dist/utils/session/parsers/codex.js +113 -0
  29. package/dist/utils/session/parsers/codex.js.map +1 -0
  30. package/dist/utils/session/parsers/gemini.d.ts +22 -0
  31. package/dist/utils/session/parsers/gemini.d.ts.map +1 -0
  32. package/dist/utils/session/parsers/gemini.js +81 -0
  33. package/dist/utils/session/parsers/gemini.js.map +1 -0
  34. package/dist/utils/session/parsers/index.d.ts +8 -0
  35. package/dist/utils/session/parsers/index.d.ts.map +1 -0
  36. package/dist/utils/session/parsers/index.js +12 -0
  37. package/dist/utils/session/parsers/index.js.map +1 -0
  38. package/dist/utils/session/parsers/qwen.d.ts +21 -0
  39. package/dist/utils/session/parsers/qwen.d.ts.map +1 -0
  40. package/dist/utils/session/parsers/qwen.js +36 -0
  41. package/dist/utils/session/parsers/qwen.js.map +1 -0
  42. package/dist/utils/session/types.d.ts +38 -0
  43. package/dist/utils/session/types.d.ts.map +1 -0
  44. package/dist/utils/session/types.js +5 -0
  45. package/dist/utils/session/types.js.map +1 -0
  46. package/dist/utils/session.d.ts +14 -79
  47. package/dist/utils/session.d.ts.map +1 -1
  48. package/dist/utils/session.js +14 -585
  49. package/dist/utils/session.js.map +1 -1
  50. package/package.json +1 -1
  51. package/src/cli/ui/__tests__/components/screens/BranchListScreen.test.tsx +40 -1
  52. package/src/cli/ui/__tests__/utils/branchFormatter.test.ts +1 -1
  53. package/src/cli/ui/components/App.tsx +10 -8
  54. package/src/cli/ui/components/screens/BranchListScreen.tsx +8 -6
  55. package/src/cli/ui/utils/branchFormatter.ts +2 -3
  56. package/src/index.ts +1 -1
  57. package/src/utils/session/common.ts +446 -0
  58. package/src/utils/session/index.ts +46 -0
  59. package/src/utils/session/parsers/claude.ts +233 -0
  60. package/src/utils/session/parsers/codex.ts +135 -0
  61. package/src/utils/session/parsers/gemini.ts +94 -0
  62. package/src/utils/session/parsers/index.ts +28 -0
  63. package/src/utils/session/parsers/qwen.ts +54 -0
  64. package/src/utils/session/types.ts +42 -0
  65. package/src/utils/session.ts +14 -755
@@ -0,0 +1,233 @@
1
+ /**
2
+ * Claude Code session parser
3
+ *
4
+ * Handles session detection and management for Claude Code CLI.
5
+ * Session files are stored in ~/.claude/projects/<encoded-path>/sessions/
6
+ */
7
+
8
+ import path from "node:path";
9
+ import { homedir } from "node:os";
10
+
11
+ import type { ClaudeSessionInfo, SessionSearchOptions } from "../types.js";
12
+ import {
13
+ isValidUuidSessionId,
14
+ findNewestSessionIdFromDir,
15
+ matchesCwd,
16
+ readFileContent,
17
+ checkFileStat,
18
+ } from "../common.js";
19
+
20
+ /**
21
+ * Encodes a project path for Claude's directory structure.
22
+ * Normalizes separators and replaces special characters with dashes.
23
+ * @param cwd - The working directory path to encode
24
+ * @returns The encoded path string suitable for Claude's directory naming
25
+ */
26
+ export function encodeClaudeProjectPath(cwd: string): string {
27
+ // Normalize to forward slashes, drop drive colon, replace / and _ with -
28
+ const normalized = cwd.replace(/\\/g, "/").replace(/:/g, "");
29
+ return normalized.replace(/_/g, "-").replace(/\//g, "-");
30
+ }
31
+
32
+ /**
33
+ * Generates candidate paths for Claude project directory.
34
+ * Handles various encoding patterns used by different Claude versions.
35
+ * @param cwd - The working directory path to encode
36
+ * @returns Array of possible encoded directory names
37
+ */
38
+ function generateClaudeProjectPathCandidates(cwd: string): string[] {
39
+ const base = encodeClaudeProjectPath(cwd);
40
+ const dotToDash = cwd
41
+ .replace(/\\/g, "/")
42
+ .replace(/:/g, "")
43
+ .replace(/\./g, "-")
44
+ .replace(/_/g, "-")
45
+ .replace(/\//g, "-");
46
+ const collapsed = dotToDash.replace(/-+/g, "-");
47
+ const candidates = [base, dotToDash, collapsed];
48
+ return Array.from(new Set(candidates));
49
+ }
50
+
51
+ /**
52
+ * Returns the list of possible Claude root directories.
53
+ * Checks CLAUDE_CONFIG_DIR environment variable first, then falls back to
54
+ * standard locations (~/.claude and ~/.config/claude).
55
+ * @returns Array of possible Claude root directory paths
56
+ */
57
+ function getClaudeRootCandidates(): string[] {
58
+ const roots: string[] = [];
59
+ if (process.env.CLAUDE_CONFIG_DIR) {
60
+ roots.push(process.env.CLAUDE_CONFIG_DIR);
61
+ }
62
+ roots.push(
63
+ path.join(homedir(), ".claude"),
64
+ path.join(homedir(), ".config", "claude"),
65
+ );
66
+ return roots;
67
+ }
68
+
69
+ /**
70
+ * Finds the latest Claude session for a given working directory.
71
+ *
72
+ * Search order:
73
+ * 1. ~/.claude/projects/<encoded>/sessions/ (official location)
74
+ * 2. ~/.claude/projects/<encoded>/ (root and subdirs)
75
+ * 3. ~/.claude/history.jsonl (global history fallback)
76
+ *
77
+ * @param cwd - The working directory to find sessions for
78
+ * @param options - Search options (since, until, preferClosestTo, windowMs)
79
+ * @returns Session info with ID and modification time, or null if not found
80
+ */
81
+ export async function findLatestClaudeSession(
82
+ cwd: string,
83
+ options: Omit<SessionSearchOptions, "cwd"> = {},
84
+ ): Promise<ClaudeSessionInfo | null> {
85
+ const rootCandidates = getClaudeRootCandidates();
86
+ const encodedPaths = generateClaudeProjectPathCandidates(cwd);
87
+
88
+ for (const claudeRoot of rootCandidates) {
89
+ for (const encoded of encodedPaths) {
90
+ const projectDir = path.join(claudeRoot, "projects", encoded);
91
+ const sessionsDir = path.join(projectDir, "sessions");
92
+
93
+ // 1) Look under sessions/ (official location)
94
+ const session = await findNewestSessionIdFromDir(
95
+ sessionsDir,
96
+ false,
97
+ options,
98
+ );
99
+ if (session) return session;
100
+
101
+ // 2) Look directly under project dir and subdirs
102
+ const rootSession = await findNewestSessionIdFromDir(
103
+ projectDir,
104
+ true,
105
+ options,
106
+ );
107
+ if (rootSession) return rootSession;
108
+ }
109
+ }
110
+
111
+ // Fallback: parse ~/.claude/history.jsonl
112
+ try {
113
+ const historyPath = path.join(homedir(), ".claude", "history.jsonl");
114
+ const historyStat = await checkFileStat(historyPath);
115
+ if (!historyStat) return null;
116
+
117
+ const content = await readFileContent(historyPath);
118
+ const lines = content.split(/\r?\n/).filter(Boolean);
119
+ for (let i = lines.length - 1; i >= 0; i -= 1) {
120
+ try {
121
+ const line = lines[i] ?? "";
122
+ const parsed = JSON.parse(line) as Record<string, unknown>;
123
+ const project =
124
+ typeof parsed.project === "string" ? parsed.project : null;
125
+ const sessionId =
126
+ typeof parsed.sessionId === "string" ? parsed.sessionId : null;
127
+ if (project && sessionId && matchesCwd(project, cwd)) {
128
+ return { id: sessionId, mtime: historyStat.mtimeMs };
129
+ }
130
+ } catch {
131
+ // ignore malformed lines
132
+ }
133
+ }
134
+ } catch {
135
+ // ignore if history not present
136
+ }
137
+
138
+ return null;
139
+ }
140
+
141
+ /**
142
+ * Finds the latest Claude session ID for a given working directory.
143
+ * @param cwd - The working directory to find sessions for
144
+ * @param options - Search options (since, until, preferClosestTo, windowMs)
145
+ * @returns Session ID string or null if not found
146
+ */
147
+ export async function findLatestClaudeSessionId(
148
+ cwd: string,
149
+ options: Omit<SessionSearchOptions, "cwd"> = {},
150
+ ): Promise<string | null> {
151
+ const found = await findLatestClaudeSession(cwd, options);
152
+ return found?.id ?? null;
153
+ }
154
+
155
+ /**
156
+ * Polls for a Claude session ID until found or timeout.
157
+ * @param cwd - The working directory to find sessions for
158
+ * @param options - Polling options including timeout, interval, and search filters
159
+ * @returns Session ID string or null if timeout reached
160
+ */
161
+ export async function waitForClaudeSessionId(
162
+ cwd: string,
163
+ options: {
164
+ timeoutMs?: number;
165
+ pollIntervalMs?: number;
166
+ since?: number;
167
+ until?: number;
168
+ preferClosestTo?: number;
169
+ windowMs?: number;
170
+ } = {},
171
+ ): Promise<string | null> {
172
+ const timeoutMs = options.timeoutMs ?? 120_000;
173
+ const pollIntervalMs = options.pollIntervalMs ?? 2_000;
174
+ const deadline = Date.now() + timeoutMs;
175
+
176
+ // Build search options once outside the loop
177
+ const searchOptions: Omit<SessionSearchOptions, "cwd"> = {};
178
+ if (options.since !== undefined) searchOptions.since = options.since;
179
+ if (options.until !== undefined) searchOptions.until = options.until;
180
+ if (options.preferClosestTo !== undefined)
181
+ searchOptions.preferClosestTo = options.preferClosestTo;
182
+ if (options.windowMs !== undefined) searchOptions.windowMs = options.windowMs;
183
+
184
+ while (Date.now() < deadline) {
185
+ const found = await findLatestClaudeSession(cwd, searchOptions);
186
+ if (found?.id) return found.id;
187
+ await new Promise((resolve) => setTimeout(resolve, pollIntervalMs));
188
+ }
189
+ return null;
190
+ }
191
+
192
+ /**
193
+ * Checks if a Claude session file exists for the given session ID and worktree path.
194
+ * @param sessionId - The session ID to check
195
+ * @param worktreePath - The worktree path (used to determine project encoding)
196
+ * @returns true if a session file exists for this ID
197
+ */
198
+ export async function claudeSessionFileExists(
199
+ sessionId: string,
200
+ worktreePath: string,
201
+ ): Promise<boolean> {
202
+ if (!isValidUuidSessionId(sessionId)) {
203
+ return false;
204
+ }
205
+
206
+ const encodedPaths = generateClaudeProjectPathCandidates(worktreePath);
207
+ const roots = getClaudeRootCandidates();
208
+
209
+ for (const root of roots) {
210
+ for (const enc of encodedPaths) {
211
+ // Check official sessions/ location first
212
+ const sessionsCandidate = path.join(
213
+ root,
214
+ "projects",
215
+ enc,
216
+ "sessions",
217
+ `${sessionId}.jsonl`,
218
+ );
219
+ const sessionsInfo = await checkFileStat(sessionsCandidate);
220
+ if (sessionsInfo) {
221
+ return true;
222
+ }
223
+
224
+ // Then check project root
225
+ const candidate = path.join(root, "projects", enc, `${sessionId}.jsonl`);
226
+ const info = await checkFileStat(candidate);
227
+ if (info) {
228
+ return true;
229
+ }
230
+ }
231
+ }
232
+ return false;
233
+ }
@@ -0,0 +1,135 @@
1
+ /**
2
+ * Codex CLI session parser
3
+ *
4
+ * Handles session detection and management for Codex CLI.
5
+ * Session files are stored in ~/.codex/sessions/ (or CODEX_HOME/sessions/).
6
+ * Filename pattern: rollout-YYYY-MM-DDTHH-MM-SS-{uuid}.jsonl
7
+ */
8
+
9
+ import path from "node:path";
10
+ import { homedir } from "node:os";
11
+
12
+ import type { CodexSessionInfo, SessionSearchOptions } from "../types.js";
13
+ import {
14
+ UUID_REGEX,
15
+ collectFilesIterative,
16
+ matchesCwd,
17
+ readSessionInfoFromFile,
18
+ } from "../common.js";
19
+
20
+ /**
21
+ * Finds the latest Codex session with optional time filtering and cwd matching.
22
+ *
23
+ * Session ID is extracted from:
24
+ * 1. Filename (rollout-...-{uuid}.jsonl) - most reliable
25
+ * 2. File content (payload.id or sessionId fields)
26
+ *
27
+ * @param options - Search options including time filters and cwd matching
28
+ * @returns Session info with ID and modification time, or null if not found
29
+ */
30
+ export async function findLatestCodexSession(
31
+ options: SessionSearchOptions = {},
32
+ ): Promise<CodexSessionInfo | null> {
33
+ // Codex CLI respects CODEX_HOME. Default is ~/.codex.
34
+ const codexHome = process.env.CODEX_HOME ?? path.join(homedir(), ".codex");
35
+ const baseDir = path.join(codexHome, "sessions");
36
+ const candidates = await collectFilesIterative(
37
+ baseDir,
38
+ (name) => name.endsWith(".json") || name.endsWith(".jsonl"),
39
+ );
40
+ if (!candidates.length) return null;
41
+
42
+ // Apply time filters
43
+ let pool = candidates;
44
+ const sinceVal = options.since;
45
+ const untilVal = options.until;
46
+ if (sinceVal !== undefined) {
47
+ pool = pool.filter((c) => c.mtime >= sinceVal);
48
+ }
49
+ if (untilVal !== undefined) {
50
+ pool = pool.filter((c) => c.mtime <= untilVal);
51
+ }
52
+ if (!pool.length) return null;
53
+
54
+ const ref = options.preferClosestTo;
55
+ const window = options.windowMs ?? 30 * 60 * 1000; // 30 minutes default
56
+ const ordered = [...pool].sort((a, b) => {
57
+ if (typeof ref === "number") {
58
+ const da = Math.abs(a.mtime - ref);
59
+ const db = Math.abs(b.mtime - ref);
60
+ if (da === db) return b.mtime - a.mtime;
61
+ if (da <= window || db <= window) return da - db;
62
+ }
63
+ return b.mtime - a.mtime;
64
+ });
65
+
66
+ for (const file of ordered) {
67
+ // Priority 1: Extract session ID from filename (most reliable for Codex)
68
+ const filenameMatch = path.basename(file.fullPath).match(UUID_REGEX);
69
+ if (filenameMatch) {
70
+ const sessionId = filenameMatch[0];
71
+ // If cwd filtering is needed, read file content to check cwd
72
+ if (options.cwd) {
73
+ const info = await readSessionInfoFromFile(file.fullPath);
74
+ if (matchesCwd(info.cwd, options.cwd)) {
75
+ return { id: sessionId, mtime: file.mtime };
76
+ }
77
+ continue; // cwd doesn't match, try next file
78
+ }
79
+ return { id: sessionId, mtime: file.mtime };
80
+ }
81
+
82
+ // Priority 2: Fallback to reading file content if filename lacks UUID
83
+ const info = await readSessionInfoFromFile(file.fullPath);
84
+ if (!info.id) continue;
85
+ if (options.cwd) {
86
+ if (matchesCwd(info.cwd, options.cwd)) {
87
+ return { id: info.id, mtime: file.mtime };
88
+ }
89
+ continue;
90
+ }
91
+ return { id: info.id, mtime: file.mtime };
92
+ }
93
+
94
+ return null;
95
+ }
96
+
97
+ /**
98
+ * Finds the latest Codex session ID.
99
+ * @param options - Search options including time filters and cwd matching
100
+ * @returns Session ID string or null if not found
101
+ */
102
+ export async function findLatestCodexSessionId(
103
+ options: SessionSearchOptions = {},
104
+ ): Promise<string | null> {
105
+ const found = await findLatestCodexSession(options);
106
+ return found?.id ?? null;
107
+ }
108
+
109
+ /**
110
+ * Polls for a Codex session ID until found or timeout.
111
+ * @param options - Polling options including startedAt time, timeout, and cwd filter
112
+ * @returns Session ID string or null if timeout reached
113
+ */
114
+ export async function waitForCodexSessionId(options: {
115
+ startedAt: number;
116
+ timeoutMs?: number;
117
+ pollIntervalMs?: number;
118
+ cwd?: string | null;
119
+ }): Promise<string | null> {
120
+ const timeoutMs = options.timeoutMs ?? 120_000;
121
+ const pollIntervalMs = options.pollIntervalMs ?? 2_000;
122
+ const deadline = Date.now() + timeoutMs;
123
+
124
+ while (Date.now() < deadline) {
125
+ const found = await findLatestCodexSession({
126
+ since: options.startedAt,
127
+ preferClosestTo: options.startedAt,
128
+ windowMs: 10 * 60 * 1000,
129
+ cwd: options.cwd ?? null,
130
+ });
131
+ if (found?.id) return found.id;
132
+ await new Promise((resolve) => setTimeout(resolve, pollIntervalMs));
133
+ }
134
+ return null;
135
+ }
@@ -0,0 +1,94 @@
1
+ /**
2
+ * Gemini CLI session parser
3
+ *
4
+ * Handles session detection for Gemini CLI.
5
+ * Session files are stored in ~/.gemini/tmp/<project_hash>/
6
+ */
7
+
8
+ import path from "node:path";
9
+ import { homedir } from "node:os";
10
+
11
+ import type { GeminiSessionInfo, SessionSearchOptions } from "../types.js";
12
+ import {
13
+ collectFilesIterative,
14
+ matchesCwd,
15
+ readSessionInfoFromFile,
16
+ } from "../common.js";
17
+
18
+ /**
19
+ * Finds the latest Gemini session with optional time filtering and cwd matching.
20
+ *
21
+ * @param options - Search options including time filters and cwd matching
22
+ * @returns Session info with ID and modification time, or null if not found
23
+ */
24
+ export async function findLatestGeminiSession(
25
+ options: SessionSearchOptions = {},
26
+ ): Promise<GeminiSessionInfo | null> {
27
+ // Gemini stores sessions/logs under ~/.gemini/tmp/<project_hash>/
28
+ const baseDir = path.join(homedir(), ".gemini", "tmp");
29
+ const files = await collectFilesIterative(
30
+ baseDir,
31
+ (name) => name.endsWith(".json") || name.endsWith(".jsonl"),
32
+ );
33
+ if (!files.length) return null;
34
+
35
+ // Apply time filters
36
+ let pool = files;
37
+ const sinceVal = options.since;
38
+ if (sinceVal !== undefined) {
39
+ pool = pool.filter((f) => f.mtime >= sinceVal);
40
+ }
41
+ const untilVal = options.until;
42
+ if (untilVal !== undefined) {
43
+ pool = pool.filter((f) => f.mtime <= untilVal);
44
+ }
45
+ if (!pool.length) return null;
46
+
47
+ // Sort by preference or mtime
48
+ const ref = options.preferClosestTo;
49
+ const window = options.windowMs ?? 30 * 60 * 1000;
50
+ pool = pool.slice().sort((a, b) => {
51
+ if (typeof ref === "number") {
52
+ const da = Math.abs(a.mtime - ref);
53
+ const db = Math.abs(b.mtime - ref);
54
+ if (da === db) return b.mtime - a.mtime;
55
+ if (da <= window || db <= window) return da - db;
56
+ }
57
+ return b.mtime - a.mtime;
58
+ });
59
+
60
+ for (const file of pool) {
61
+ const info = await readSessionInfoFromFile(file.fullPath);
62
+ if (!info.id) continue;
63
+ if (options.cwd) {
64
+ if (matchesCwd(info.cwd, options.cwd)) {
65
+ return { id: info.id, mtime: file.mtime };
66
+ }
67
+ continue;
68
+ }
69
+ return { id: info.id, mtime: file.mtime };
70
+ }
71
+
72
+ return null;
73
+ }
74
+
75
+ /**
76
+ * Finds the latest Gemini session ID.
77
+ * @param cwd - The working directory to find sessions for (used as fallback if options.cwd not set)
78
+ * @param options - Search options including time filters and cwd matching
79
+ * @returns Session ID string or null if not found
80
+ */
81
+ export async function findLatestGeminiSessionId(
82
+ cwd: string,
83
+ options: SessionSearchOptions = {},
84
+ ): Promise<string | null> {
85
+ const searchOptions: SessionSearchOptions = { cwd: options.cwd ?? cwd };
86
+ if (options.since !== undefined) searchOptions.since = options.since;
87
+ if (options.until !== undefined) searchOptions.until = options.until;
88
+ if (options.preferClosestTo !== undefined)
89
+ searchOptions.preferClosestTo = options.preferClosestTo;
90
+ if (options.windowMs !== undefined) searchOptions.windowMs = options.windowMs;
91
+
92
+ const found = await findLatestGeminiSession(searchOptions);
93
+ return found?.id ?? null;
94
+ }
@@ -0,0 +1,28 @@
1
+ /**
2
+ * Session parsers - re-exports all tool-specific parsers
3
+ */
4
+
5
+ // Claude Code
6
+ export {
7
+ encodeClaudeProjectPath,
8
+ findLatestClaudeSession,
9
+ findLatestClaudeSessionId,
10
+ waitForClaudeSessionId,
11
+ claudeSessionFileExists,
12
+ } from "./claude.js";
13
+
14
+ // Codex CLI
15
+ export {
16
+ findLatestCodexSession,
17
+ findLatestCodexSessionId,
18
+ waitForCodexSessionId,
19
+ } from "./codex.js";
20
+
21
+ // Gemini CLI
22
+ export {
23
+ findLatestGeminiSession,
24
+ findLatestGeminiSessionId,
25
+ } from "./gemini.js";
26
+
27
+ // Qwen CLI
28
+ export { findLatestQwenSessionId } from "./qwen.js";
@@ -0,0 +1,54 @@
1
+ /**
2
+ * Qwen CLI session parser
3
+ *
4
+ * Handles session detection for Qwen CLI.
5
+ * Session files are stored in ~/.qwen/tmp/<project_hash>/
6
+ * and checkpoints in ~/.qwen/tmp/<project_hash>/checkpoints/
7
+ */
8
+
9
+ import path from "node:path";
10
+ import { homedir } from "node:os";
11
+
12
+ import {
13
+ findLatestNestedSessionFile,
14
+ readSessionIdFromFile,
15
+ } from "../common.js";
16
+
17
+ /**
18
+ * Finds the latest Qwen session ID.
19
+ *
20
+ * Search order:
21
+ * 1. ~/.qwen/tmp/<hash>/*.json or *.jsonl
22
+ * 2. ~/.qwen/tmp/<hash>/checkpoints/*.json or *.ckpt
23
+ *
24
+ * Falls back to filename (without extension) if no session ID is found in content.
25
+ *
26
+ * @param _cwd - Working directory (currently unused, reserved for future per-project filtering)
27
+ * @returns Session ID string or null if not found
28
+ */
29
+ export async function findLatestQwenSessionId(
30
+ _cwd: string,
31
+ ): Promise<string | null> {
32
+ const baseDir = path.join(homedir(), ".qwen", "tmp");
33
+
34
+ // Try root level first, then checkpoints subdirectory
35
+ const latest =
36
+ (await findLatestNestedSessionFile(
37
+ baseDir,
38
+ [],
39
+ (name) => name.endsWith(".json") || name.endsWith(".jsonl"),
40
+ )) ??
41
+ (await findLatestNestedSessionFile(
42
+ baseDir,
43
+ ["checkpoints"],
44
+ (name) => name.endsWith(".json") || name.endsWith(".ckpt"),
45
+ ));
46
+
47
+ if (!latest) return null;
48
+
49
+ const fromContent = await readSessionIdFromFile(latest);
50
+ if (fromContent) return fromContent;
51
+
52
+ // Fallback: use filename (without extension) as tag
53
+ return path.basename(latest).replace(/\.[^.]+$/, "");
54
+ }
@@ -0,0 +1,42 @@
1
+ /**
2
+ * Session types - common type definitions for session parsers
3
+ */
4
+
5
+ /**
6
+ * Options for session search operations
7
+ */
8
+ export interface SessionSearchOptions {
9
+ /** Minimum mtime (ms since epoch) */
10
+ since?: number;
11
+ /** Maximum mtime (ms since epoch) */
12
+ until?: number;
13
+ /** Reference time for preferring closest match */
14
+ preferClosestTo?: number;
15
+ /** Time window for closest match (default: 30 minutes) */
16
+ windowMs?: number;
17
+ /** Working directory filter */
18
+ cwd?: string | null;
19
+ }
20
+
21
+ /**
22
+ * Base session info with ID and modification time
23
+ */
24
+ export interface SessionInfo {
25
+ id: string;
26
+ mtime: number;
27
+ }
28
+
29
+ /**
30
+ * Claude Code session info
31
+ */
32
+ export type ClaudeSessionInfo = SessionInfo;
33
+
34
+ /**
35
+ * Codex CLI session info
36
+ */
37
+ export type CodexSessionInfo = SessionInfo;
38
+
39
+ /**
40
+ * Gemini CLI session info
41
+ */
42
+ export type GeminiSessionInfo = SessionInfo;