lark-remote 0.1.3 → 0.1.5

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.
@@ -5,7 +5,7 @@
5
5
  * - `approval_policy="never"` eliminates the approval handshake (root cause ①).
6
6
  * - Prompt goes via stdin (`-` flag) to avoid argv escaping issues.
7
7
  * - `--skip-git-repo-check`: lark-remote's cwd may not be a git repo.
8
- * - User's `~/.codex/config.toml` and AGENTS.md/CLAUDE.md rules are preserved
8
+ * - User's `~/.codex/config.toml` and project rules are preserved
9
9
  * (no `--ignore-user-config` / `--ignore-rules`).
10
10
  */
11
11
  interface BuildCodexExecArgsInput {
@@ -5,7 +5,7 @@
5
5
  * - `approval_policy="never"` eliminates the approval handshake (root cause ①).
6
6
  * - Prompt goes via stdin (`-` flag) to avoid argv escaping issues.
7
7
  * - `--skip-git-repo-check`: lark-remote's cwd may not be a git repo.
8
- * - User's `~/.codex/config.toml` and AGENTS.md/CLAUDE.md rules are preserved
8
+ * - User's `~/.codex/config.toml` and project rules are preserved
9
9
  * (no `--ignore-user-config` / `--ignore-rules`).
10
10
  */
11
11
  /**
@@ -11,6 +11,18 @@ export declare function pipeAllStdio(): ('ignore' | 'pipe')[];
11
11
  * rather than argv.
12
12
  */
13
13
  export declare function endStdinWithPrompt(proc: ChildProcess, message: string): void;
14
+ /**
15
+ * Build a synthetic system.init event for pre-spawn failure paths.
16
+ *
17
+ * When the runner exits before spawning a process (validateBeforeRun failure,
18
+ * ENOENT spawn failure), no real system.init arrives from stdout. Without a
19
+ * synthetic init, the bridge's pre-init result guard (§9.22) and the run-state
20
+ * reducer's sessionId check would silently drop the error result, leaving the
21
+ * card showing "输出流已结束,但未收到 result 事件" instead of the actual
22
+ * error message. The synthetic init satisfies both guards so the real error
23
+ * result is processed normally.
24
+ */
25
+ export declare function syntheticInitEvent(sessionId?: string): AgentEvent;
14
26
  /**
15
27
  * Build a standardized auth/error result event for yield.
16
28
  *
@@ -46,6 +46,27 @@ export function endStdinWithPrompt(proc, message) {
46
46
  }
47
47
  }
48
48
  }
49
+ /**
50
+ * Build a synthetic system.init event for pre-spawn failure paths.
51
+ *
52
+ * When the runner exits before spawning a process (validateBeforeRun failure,
53
+ * ENOENT spawn failure), no real system.init arrives from stdout. Without a
54
+ * synthetic init, the bridge's pre-init result guard (§9.22) and the run-state
55
+ * reducer's sessionId check would silently drop the error result, leaving the
56
+ * card showing "输出流已结束,但未收到 result 事件" instead of the actual
57
+ * error message. The synthetic init satisfies both guards so the real error
58
+ * result is processed normally.
59
+ */
60
+ export function syntheticInitEvent(sessionId = '') {
61
+ return {
62
+ type: 'system',
63
+ subtype: 'init',
64
+ session_id: sessionId,
65
+ cwd: '',
66
+ model: '',
67
+ timestamp: new Date().toISOString(),
68
+ };
69
+ }
49
70
  /**
50
71
  * Build a standardized auth/error result event for yield.
51
72
  *
@@ -6,7 +6,7 @@ import { getLogger } from '../../logger/index.js';
6
6
  import { ProcessStopper } from './process-stopper.js';
7
7
  import { SpawnHeartbeat } from './spawn-heartbeat.js';
8
8
  import { createJSONLStream } from './jsonl-stream.js';
9
- import { authErrorEvent } from './runner-utils.js';
9
+ import { authErrorEvent, syntheticInitEvent } from './runner-utils.js';
10
10
  import { DEFAULT_STOP_GRACE_MS } from '../../config/index.js';
11
11
  /**
12
12
  * Magic-number constants (Clean Code P3-1, G25 Replace Magic Numbers with
@@ -255,6 +255,11 @@ export class SpawningRunner {
255
255
  // Pre-spawn validation hook (e.g. codex API key check)
256
256
  const validationError = this.validateBeforeRun(opts);
257
257
  if (validationError) {
258
+ // §9.22: yield synthetic init so the bridge's pre-init result guard and
259
+ // the run-state reducer's sessionId check don't silently drop the error
260
+ // result. Without init, the card would show a generic "输出流已结束"
261
+ // instead of the specific validation error message.
262
+ yield syntheticInitEvent(opts.sessionId);
258
263
  yield validationError;
259
264
  return;
260
265
  }
@@ -287,6 +292,9 @@ export class SpawningRunner {
287
292
  // so the user is not misdiagnosed into reinstalling the binary.
288
293
  const baseMsg = `${this.binary} 命令不可用(未找到或不可执行),请检查是否已安装或在 PATH 中`;
289
294
  const msg = spawnErr ? `${baseMsg}:${spawnErr.message}` : baseMsg;
295
+ // §9.22: yield synthetic init before the error result so bridge/run-state
296
+ // guards don't silently drop it (same rationale as validateBeforeRun).
297
+ yield syntheticInitEvent(opts.sessionId);
290
298
  yield authErrorEvent(msg);
291
299
  return;
292
300
  }
@@ -0,0 +1,93 @@
1
+ import { type ContentBlockMapping } from '../common/content-blocks.js';
2
+ export interface SessionIndexEntry {
3
+ sessionId: string;
4
+ path: string;
5
+ fingerprint: string;
6
+ mtimeMs: number;
7
+ summary: string;
8
+ cwdSet: Set<string>;
9
+ }
10
+ export interface SessionIndexOptions {
11
+ /** Minimum interval between refresh scans (ms). Default 5000. */
12
+ refreshIntervalMs?: number;
13
+ }
14
+ /** Claude-specific field-name mapping for content block extraction. */
15
+ export declare const CLAUDE_MAPPING: ContentBlockMapping;
16
+ /**
17
+ * Detect Claude CLI sub-agent completion notification text.
18
+ * Claude CLI injects a `type:"user"` record with string content starting
19
+ * with `<task-notification>` when a sub-agent (Agent tool) finishes.
20
+ * These must be excluded from displayTitle/summary — they are machine
21
+ * injections, not real user input. Primary criterion: extracted text
22
+ * starts with `<task-notification` (works for both string and array
23
+ * content since `extractText` is called first). Secondary (optional):
24
+ * `origin.kind === 'task-notification'` (not all CLI versions write it).
25
+ */
26
+ export declare function isTaskNotificationText(text: string): boolean;
27
+ /**
28
+ * Extract first text from a user message.
29
+ * Used by parseSessionJsonl and sessions.ts readSessionContent.
30
+ */
31
+ export declare function extractText(content: unknown): string | null;
32
+ export interface ParsedSession {
33
+ cwdSet: Set<string>;
34
+ summary: string;
35
+ }
36
+ /**
37
+ * Unified parser: single streaming scan of a JSONL file producing both
38
+ * the complete set of cwd values AND the first real user message summary.
39
+ * Replaces the split readCwdFromJsonl (first-cwd) + jsonlContainsCwd (any-cwd).
40
+ */
41
+ export declare function parseSessionJsonl(filePath: string): ParsedSession;
42
+ /**
43
+ * Build a fingerprint string from file stat metadata.
44
+ * Uses `size + mtimeNs + ctimeNs + dev + ino` (bigint stat).
45
+ * `size`/`mtime` are required; the rest are best-effort.
46
+ */
47
+ export declare function buildFingerprint(filePath: string): string;
48
+ /**
49
+ * In-memory session index with three query views:
50
+ * byPath: path → entry
51
+ * bySessionId: sessionId → Set<path>
52
+ * byCwd: cwd → Set<path>
53
+ *
54
+ * Build once, refresh on demand (throttled). Single-file parse failure
55
+ * doesn't affect other files; readdir failure preserves old index.
56
+ */
57
+ export declare class SessionIndex {
58
+ private readonly projectsDir;
59
+ private readonly refreshIntervalMs;
60
+ private readonly byPath;
61
+ private readonly bySessionId;
62
+ private readonly byCwd;
63
+ private lastRefreshAt;
64
+ private built;
65
+ /** Test-only: count of parseSessionJsonl calls since creation */
66
+ parseCount: number;
67
+ /** Test-only: count of directory scan operations */
68
+ scanCount: number;
69
+ constructor(projectsDir: string, opts?: SessionIndexOptions);
70
+ /** Whether a full scan has ever completed successfully (readdir succeeded). */
71
+ get isBuilt(): boolean;
72
+ build(): void;
73
+ refresh(): void;
74
+ listByCwd(cwd: string, opts?: {
75
+ limit?: number;
76
+ offset?: number;
77
+ }): SessionIndexEntry[];
78
+ /**
79
+ * Find a session entry by sessionId and cwd.
80
+ * Re-stats the file to verify fingerprint before returning (拍板 3):
81
+ * - fingerprint unchanged → trust index cwdSet
82
+ * - fingerprint changed → re-parse that one file, update index, re-check
83
+ * Returns undefined if not found or cwd doesn't match.
84
+ *
85
+ * Note: we re-stat ALL paths for the sessionId regardless of the old cwdSet,
86
+ * because a fingerprint change may have added the target cwd to the set.
87
+ */
88
+ findBySessionIdAndCwd(sessionId: string, cwd: string): SessionIndexEntry | undefined;
89
+ private fullScan;
90
+ private updateEntry;
91
+ private removeEntry;
92
+ private removeCrossRefs;
93
+ }
@@ -0,0 +1,374 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ import { scanJsonlLines } from '../common/jsonl.js';
4
+ import { extractContentBlocks } from '../common/content-blocks.js';
5
+ import { truncate } from '../../card/card-shared.js';
6
+ import { getLogger } from '../../logger/index.js';
7
+ // ─── Claude content block mapping (shared with sessions.ts) ─
8
+ /** Claude-specific field-name mapping for content block extraction. */
9
+ export const CLAUDE_MAPPING = {
10
+ toolUseType: 'tool_use',
11
+ toolResultType: 'tool_result',
12
+ toolInputField: 'input',
13
+ toolErrorField: 'is_error',
14
+ };
15
+ /**
16
+ * Detect Claude CLI sub-agent completion notification text.
17
+ * Claude CLI injects a `type:"user"` record with string content starting
18
+ * with `<task-notification>` when a sub-agent (Agent tool) finishes.
19
+ * These must be excluded from displayTitle/summary — they are machine
20
+ * injections, not real user input. Primary criterion: extracted text
21
+ * starts with `<task-notification` (works for both string and array
22
+ * content since `extractText` is called first). Secondary (optional):
23
+ * `origin.kind === 'task-notification'` (not all CLI versions write it).
24
+ */
25
+ export function isTaskNotificationText(text) {
26
+ return text.trimStart().startsWith('<task-notification');
27
+ }
28
+ /**
29
+ * Extract first text from a user message.
30
+ * Used by parseSessionJsonl and sessions.ts readSessionContent.
31
+ */
32
+ export function extractText(content) {
33
+ if (typeof content === 'string' && content.trim()) {
34
+ return content.trim();
35
+ }
36
+ const blocks = extractContentBlocks(content, CLAUDE_MAPPING);
37
+ const textBlock = blocks.find((b) => b.type === 'text');
38
+ if (textBlock)
39
+ return textBlock.content;
40
+ return null;
41
+ }
42
+ /**
43
+ * Unified parser: single streaming scan of a JSONL file producing both
44
+ * the complete set of cwd values AND the first real user message summary.
45
+ * Replaces the split readCwdFromJsonl (first-cwd) + jsonlContainsCwd (any-cwd).
46
+ */
47
+ export function parseSessionJsonl(filePath) {
48
+ const cwdSet = new Set();
49
+ let summary = '(无摘要)';
50
+ let foundSummary = false;
51
+ const ok = scanJsonlLines(filePath, (line) => {
52
+ let obj;
53
+ try {
54
+ obj = JSON.parse(line);
55
+ }
56
+ catch {
57
+ return;
58
+ }
59
+ // Collect cwd values
60
+ if (typeof obj.cwd === 'string' && obj.cwd) {
61
+ cwdSet.add(obj.cwd);
62
+ }
63
+ // Find first real user message for summary (same logic as summarizeSession)
64
+ if (!foundSummary && obj.type === 'user' && !!obj.message) {
65
+ const msg = obj.message;
66
+ if (msg.content) {
67
+ const text = extractText(msg.content);
68
+ if (text && !isTaskNotificationText(text)) {
69
+ summary = text;
70
+ foundSummary = true;
71
+ }
72
+ }
73
+ }
74
+ });
75
+ // If file couldn't be opened, return empty result
76
+ if (!ok) {
77
+ return { cwdSet: new Set(), summary: '(无摘要)' };
78
+ }
79
+ return { cwdSet, summary };
80
+ }
81
+ // ─── buildFingerprint ───────────────────────────────────────
82
+ /**
83
+ * Build a fingerprint string from file stat metadata.
84
+ * Uses `size + mtimeNs + ctimeNs + dev + ino` (bigint stat).
85
+ * `size`/`mtime` are required; the rest are best-effort.
86
+ */
87
+ export function buildFingerprint(filePath) {
88
+ let st;
89
+ try {
90
+ st = fs.statSync(filePath, { bigint: true });
91
+ }
92
+ catch {
93
+ return '';
94
+ }
95
+ const parts = [String(st.size), String(st.mtimeNs)];
96
+ // ctimeNs, dev, ino: include if available
97
+ try {
98
+ parts.push(String(st.ctimeNs));
99
+ }
100
+ catch {
101
+ /* skip */
102
+ }
103
+ try {
104
+ parts.push(String(st.dev));
105
+ }
106
+ catch {
107
+ /* skip */
108
+ }
109
+ try {
110
+ parts.push(String(st.ino));
111
+ }
112
+ catch {
113
+ /* skip */
114
+ }
115
+ return parts.join('|');
116
+ }
117
+ // ─── SessionIndex ───────────────────────────────────────────
118
+ /**
119
+ * In-memory session index with three query views:
120
+ * byPath: path → entry
121
+ * bySessionId: sessionId → Set<path>
122
+ * byCwd: cwd → Set<path>
123
+ *
124
+ * Build once, refresh on demand (throttled). Single-file parse failure
125
+ * doesn't affect other files; readdir failure preserves old index.
126
+ */
127
+ export class SessionIndex {
128
+ projectsDir;
129
+ refreshIntervalMs;
130
+ byPath = new Map();
131
+ bySessionId = new Map();
132
+ byCwd = new Map();
133
+ lastRefreshAt = 0;
134
+ built = false;
135
+ /** Test-only: count of parseSessionJsonl calls since creation */
136
+ parseCount = 0;
137
+ /** Test-only: count of directory scan operations */
138
+ scanCount = 0;
139
+ constructor(projectsDir, opts = {}) {
140
+ this.projectsDir = projectsDir;
141
+ this.refreshIntervalMs = opts.refreshIntervalMs ?? 5000;
142
+ }
143
+ /** Whether a full scan has ever completed successfully (readdir succeeded). */
144
+ get isBuilt() {
145
+ return this.built;
146
+ }
147
+ // ── Build (initial full scan) ───────────────────────────
148
+ build() {
149
+ this.scanCount++;
150
+ this.fullScan();
151
+ this.lastRefreshAt = Date.now();
152
+ }
153
+ // ── Refresh (throttled incremental) ─────────────────────
154
+ refresh() {
155
+ const now = Date.now();
156
+ if (now - this.lastRefreshAt < this.refreshIntervalMs) {
157
+ return; // throttled
158
+ }
159
+ this.scanCount++;
160
+ this.fullScan();
161
+ this.lastRefreshAt = now;
162
+ }
163
+ // ── Query: listByCwd ────────────────────────────────────
164
+ listByCwd(cwd, opts = {}) {
165
+ const paths = this.byCwd.get(cwd);
166
+ if (!paths || paths.size === 0)
167
+ return [];
168
+ // Gather entries, deduplicate by sessionId (keep mtime newest)
169
+ const byId = new Map();
170
+ for (const p of paths) {
171
+ const entry = this.byPath.get(p);
172
+ if (!entry)
173
+ continue;
174
+ const existing = byId.get(entry.sessionId);
175
+ if (!existing || entry.mtimeMs > existing.mtimeMs) {
176
+ byId.set(entry.sessionId, entry);
177
+ }
178
+ }
179
+ const sorted = [...byId.values()].sort((a, b) => b.mtimeMs - a.mtimeMs || a.sessionId.localeCompare(b.sessionId));
180
+ const offset = opts.offset ?? 0;
181
+ const limit = opts.limit;
182
+ const sliced = limit === undefined ? sorted.slice(offset) : sorted.slice(offset, offset + limit);
183
+ return sliced;
184
+ }
185
+ // ── Query: findBySessionIdAndCwd ────────────────────────
186
+ /**
187
+ * Find a session entry by sessionId and cwd.
188
+ * Re-stats the file to verify fingerprint before returning (拍板 3):
189
+ * - fingerprint unchanged → trust index cwdSet
190
+ * - fingerprint changed → re-parse that one file, update index, re-check
191
+ * Returns undefined if not found or cwd doesn't match.
192
+ *
193
+ * Note: we re-stat ALL paths for the sessionId regardless of the old cwdSet,
194
+ * because a fingerprint change may have added the target cwd to the set.
195
+ */
196
+ findBySessionIdAndCwd(sessionId, cwd) {
197
+ const paths = this.bySessionId.get(sessionId);
198
+ if (!paths || paths.size === 0)
199
+ return undefined;
200
+ for (const p of paths) {
201
+ const entry = this.byPath.get(p);
202
+ if (!entry)
203
+ continue;
204
+ // Re-stat to verify fingerprint (拍板 3)
205
+ const currentFp = buildFingerprint(p);
206
+ if (currentFp === '') {
207
+ // File disappeared — remove from index
208
+ this.removeEntry(p);
209
+ continue;
210
+ }
211
+ if (currentFp === entry.fingerprint) {
212
+ // Fingerprint matches → trust index
213
+ if (entry.cwdSet.has(cwd))
214
+ return entry;
215
+ continue;
216
+ }
217
+ // Fingerprint changed → re-parse this single file
218
+ const parsed = parseSessionJsonl(p);
219
+ this.parseCount++;
220
+ try {
221
+ const st = fs.statSync(p, { bigint: true });
222
+ const updated = {
223
+ sessionId: entry.sessionId,
224
+ path: p,
225
+ fingerprint: currentFp,
226
+ mtimeMs: Number(st.mtimeMs),
227
+ summary: truncate(parsed.summary, 60, { normalizeWhitespace: true }),
228
+ cwdSet: parsed.cwdSet,
229
+ };
230
+ this.updateEntry(p, updated);
231
+ // Re-check cwd after update
232
+ if (updated.cwdSet.has(cwd))
233
+ return updated;
234
+ }
235
+ catch {
236
+ // File gone
237
+ this.removeEntry(p);
238
+ }
239
+ }
240
+ return undefined;
241
+ }
242
+ // ── Internal: full scan ─────────────────────────────────
243
+ fullScan() {
244
+ let entries;
245
+ try {
246
+ entries = fs.readdirSync(this.projectsDir);
247
+ }
248
+ catch {
249
+ // readdir failure → preserve old index, degrade to fallback
250
+ getLogger().warn('[session-index] readdir failed, preserving old index');
251
+ return;
252
+ }
253
+ // readdir succeeded → the snapshot is authoritative for what we could read.
254
+ this.built = true;
255
+ const currentPaths = new Set();
256
+ for (const entry of entries) {
257
+ const subdir = path.join(this.projectsDir, entry);
258
+ let st;
259
+ try {
260
+ st = fs.statSync(subdir);
261
+ if (!st.isDirectory())
262
+ continue;
263
+ }
264
+ catch {
265
+ continue;
266
+ }
267
+ let files;
268
+ try {
269
+ files = fs.readdirSync(subdir).filter((f) => f.endsWith('.jsonl'));
270
+ }
271
+ catch {
272
+ continue;
273
+ }
274
+ for (const f of files) {
275
+ const fullPath = path.join(subdir, f);
276
+ currentPaths.add(fullPath);
277
+ // Check fingerprint
278
+ const fp = buildFingerprint(fullPath);
279
+ if (fp === '')
280
+ continue; // can't stat, skip
281
+ const existing = this.byPath.get(fullPath);
282
+ if (existing && existing.fingerprint === fp) {
283
+ // Unchanged, keep as-is
284
+ continue;
285
+ }
286
+ // New file or fingerprint changed → parse
287
+ // Concurrent-write guard: stat before and after parse, compare fingerprints
288
+ const fpBefore = fp;
289
+ const parsed = parseSessionJsonl(fullPath);
290
+ this.parseCount++;
291
+ const fpAfter = buildFingerprint(fullPath);
292
+ if (fpBefore !== fpAfter) {
293
+ // File was modified during parse → skip this round, retry next refresh
294
+ getLogger().info(`[session-index] concurrent write detected for ${f}, skipping`);
295
+ continue;
296
+ }
297
+ try {
298
+ const fileStat = fs.statSync(fullPath, { bigint: true });
299
+ const sessionId = f.slice(0, -'.jsonl'.length);
300
+ const newEntry = {
301
+ sessionId,
302
+ path: fullPath,
303
+ fingerprint: fpAfter,
304
+ mtimeMs: Number(fileStat.mtimeMs),
305
+ summary: truncate(parsed.summary, 60, { normalizeWhitespace: true }),
306
+ cwdSet: parsed.cwdSet,
307
+ };
308
+ this.updateEntry(fullPath, newEntry);
309
+ }
310
+ catch (err) {
311
+ getLogger().warn(`[session-index] skip ${f}: ${err.message}`);
312
+ }
313
+ }
314
+ }
315
+ // Remove entries for files that no longer exist
316
+ for (const p of [...this.byPath.keys()]) {
317
+ if (!currentPaths.has(p)) {
318
+ this.removeEntry(p);
319
+ }
320
+ }
321
+ }
322
+ // ── Internal: index mutation ────────────────────────────
323
+ updateEntry(filePath, entry) {
324
+ const old = this.byPath.get(filePath);
325
+ // Remove old entry's cross-references if it exists
326
+ if (old) {
327
+ this.removeCrossRefs(filePath, old);
328
+ }
329
+ // Set new entry
330
+ this.byPath.set(filePath, entry);
331
+ // bySessionId
332
+ let idPaths = this.bySessionId.get(entry.sessionId);
333
+ if (!idPaths) {
334
+ idPaths = new Set();
335
+ this.bySessionId.set(entry.sessionId, idPaths);
336
+ }
337
+ idPaths.add(filePath);
338
+ // byCwd
339
+ for (const cwd of entry.cwdSet) {
340
+ let cwdPaths = this.byCwd.get(cwd);
341
+ if (!cwdPaths) {
342
+ cwdPaths = new Set();
343
+ this.byCwd.set(cwd, cwdPaths);
344
+ }
345
+ cwdPaths.add(filePath);
346
+ }
347
+ }
348
+ removeEntry(filePath) {
349
+ const entry = this.byPath.get(filePath);
350
+ if (!entry)
351
+ return;
352
+ this.removeCrossRefs(filePath, entry);
353
+ this.byPath.delete(filePath);
354
+ }
355
+ removeCrossRefs(filePath, entry) {
356
+ // bySessionId
357
+ const idPaths = this.bySessionId.get(entry.sessionId);
358
+ if (idPaths) {
359
+ idPaths.delete(filePath);
360
+ if (idPaths.size === 0)
361
+ this.bySessionId.delete(entry.sessionId);
362
+ }
363
+ // byCwd
364
+ for (const cwd of entry.cwdSet) {
365
+ const cwdPaths = this.byCwd.get(cwd);
366
+ if (cwdPaths) {
367
+ cwdPaths.delete(filePath);
368
+ if (cwdPaths.size === 0)
369
+ this.byCwd.delete(cwd);
370
+ }
371
+ }
372
+ }
373
+ }
374
+ //# sourceMappingURL=session-index.js.map
@@ -1,27 +1,9 @@
1
1
  import type { AgentSession, AgentSessionContentEvent, AgentSessionUsage } from '../../runner/index.js';
2
- /**
3
- * Read the FIRST cwd from a Claude session JSONL. Used for display and
4
- * diagnostics. Claude CLI writes the absolute (symlink-resolved) cwd on
5
- * every event that produces one; older versions wrote it once on init.
6
- * Returns the first non-empty value, or undefined if absent.
7
- *
8
- * For cwd GUARD checks (list/read), use `jsonlContainsCwd` which accepts
9
- * any matching cwd field (required for relocated sessions whose first cwd
10
- * is the pre-relocate path). The directory name under ~/.claude/projects/
11
- * is lossy (encodes `/` AND `_` as `-`), so it cannot be decoded back to
12
- * a real path. See regression 2026-06-21 /resume & /cd 路径错乱.
13
- */
14
- export declare function readCwdFromJsonl(filePath: string): string | undefined;
15
2
  /**
16
3
  * List Claude Code sessions for a given cwd, newest first. Each entry has
17
4
  * the sessionId (= jsonl filename stem), a one-line summary extracted from
18
5
  * the first user message, and the file mtime. Returns an empty array when
19
6
  * the project directory doesn't exist or no jsonl has a matching cwd field.
20
- *
21
- * We locate the directory via `projectDirForCwd` (lossy but OK for locating
22
- * a known cwd) and then **verify** each jsonl's cwd field matches. This
23
- * prevents collision: `disk_d/foo` and `disk-d/foo` both encode to the same
24
- * directory name, but only the matching cwd field is kept.
25
7
  */
26
8
  export declare function listClaudeSessions(cwd: string, opts?: {
27
9
  limit?: number;