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,8 +5,8 @@ import { findJsonlLine, readLastJsonlLine, scanJsonlLines, readJsonlLinesFromOff
5
5
  import { STALE_MS } from '../common/constants.js';
6
6
  import { extractContentBlocks } from '../common/content-blocks.js';
7
7
  import { UsageAccumulator } from '../common/usage-accumulator.js';
8
- import { getLogger } from '../../logger/index.js';
9
8
  import { truncate } from '../../card/card-shared.js';
9
+ import { SessionIndex, extractText, isTaskNotificationText, CLAUDE_MAPPING, } from './session-index.js';
10
10
  /**
11
11
  * Directory Claude Code uses to store session transcripts for a given cwd.
12
12
  * Claude encodes the cwd by replacing `/` with `-` (e.g.
@@ -14,54 +14,24 @@ import { truncate } from '../../card/card-shared.js';
14
14
  *
15
15
  * Note: this encoding is LOSSY (`disk_d/foo` and `disk-d/foo` both encode to
16
16
  * `disk-d-foo`). It is only safe to use the result to **locate** files; never
17
- * to **decode** a directory name back to a cwd. Use `readCwdFromJsonl` for
18
- * cwd (regression 2026-06-21 /resume & /cd 路径错乱).
17
+ * to **decode** a directory name back to a cwd. The cwd must always be read
18
+ * from the JSONL content (regression 2026-06-21 /resume & /cd 路径错乱).
19
19
  */
20
20
  function projectDirForCwd(cwd, projectsDir) {
21
21
  const encoded = cwd.replace(/\//g, '-').replace(/_/g, '-');
22
22
  return path.join(projectsDir, encoded);
23
23
  }
24
24
  /**
25
- * Read the FIRST cwd from a Claude session JSONL. Used for display and
26
- * diagnostics. Claude CLI writes the absolute (symlink-resolved) cwd on
27
- * every event that produces one; older versions wrote it once on init.
28
- * Returns the first non-empty value, or undefined if absent.
29
- *
30
- * For cwd GUARD checks (list/read), use `jsonlContainsCwd` which accepts
31
- * any matching cwd field (required for relocated sessions whose first cwd
32
- * is the pre-relocate path). The directory name under ~/.claude/projects/
33
- * is lossy (encodes `/` AND `_` as `-`), so it cannot be decoded back to
34
- * a real path. See regression 2026-06-21 /resume & /cd 路径错乱.
25
+ * cwd guard for the index-miss fallback (readSessionContent /
26
+ * isClaudeSessionActive / findSessionFileInProjects): the project-dir
27
+ * encoding is lossy (N-to-N), so a file located by directory must be verified
28
+ * to actually belong to the requested cwd. A relocated session contains
29
+ * MULTIPLE cwd values (pre- and post-relocate) — accept when ANY `cwd` field
30
+ * matches. Early-stops on the first match (findJsonlLine), keeping the
31
+ * P2-2 single-pass parse-count anchors intact on the fallback path.
32
+ * Regression 2026-08-04 EnterWorktree relocate.
35
33
  */
36
- export function readCwdFromJsonl(filePath) {
37
- const line = findJsonlLine(filePath, (l) => {
38
- try {
39
- const obj = JSON.parse(l);
40
- return typeof obj.cwd === 'string' && !!obj.cwd;
41
- }
42
- catch {
43
- return false; // skip malformed line
44
- }
45
- });
46
- if (!line)
47
- return undefined;
48
- try {
49
- return JSON.parse(line).cwd;
50
- }
51
- catch {
52
- return undefined;
53
- }
54
- }
55
- /**
56
- * cwd guard for readSessionContent: the project-dir encoding is lossy
57
- * (N-to-N), so a file located by directory must be verified to actually
58
- * belong to the requested cwd. A relocated session contains MULTIPLE cwd
59
- * values (pre- and post-relocate) — accept when ANY `cwd` field matches
60
- * (was: first-cwd-only via readCwdFromJsonl, which rejected relocated
61
- * sessions whose first cwd is the pre-relocate path). Regression
62
- * 2026-08-04 EnterWorktree relocate.
63
- */
64
- function jsonlContainsCwd(filePath, cwd) {
34
+ function fileContainsCwd(filePath, cwd) {
65
35
  return (findJsonlLine(filePath, (l) => {
66
36
  try {
67
37
  const obj = JSON.parse(l);
@@ -108,272 +78,50 @@ function findSessionFileInProjects(sessionId, projectsDir, cwd) {
108
78
  // multiple project dirs). Fall back to the first sorted candidate.
109
79
  if (cwd) {
110
80
  for (const candidate of candidates) {
111
- if (jsonlContainsCwd(candidate, cwd))
81
+ if (fileContainsCwd(candidate, cwd))
112
82
  return candidate;
113
83
  }
114
84
  }
115
85
  return candidates[0];
116
86
  }
117
87
  /**
118
- * P1-19: TTL cache for full project listings (mirror codex getSessionIndex's
119
- * 5s TTL). Repeated /resume pages re-scan the whole project dir per call
120
- * (readdir + 2 opens/file + full reads for files without a user message);
121
- * caching the full list within the TTL removes the redundant scans.
122
- */
123
- const LIST_CACHE_TTL_MS = 5000;
124
- /** Upper bound on cached project listings; FIFO eviction past this. */
125
- const LIST_CACHE_MAX_ENTRIES = 32;
126
- const claudeListCache = new Map();
127
- /**
128
- * Extract a one-line summary from a session JSONL by finding the first
129
- * `type: "user"` record and returning its prompt text. Uses streaming
130
- * early-stop via `findJsonlLine` so only the lines up to the first user
131
- * message are read (not the entire file). Returns a fallback when the
132
- * file is unreadable or contains no user message.
133
- *
134
- * P2-3 optimization: previously used `readJsonlLines` (full-file slurp)
135
- * just to get the first user message. Now uses `findJsonlLine` which
136
- * stops reading as soon as the predicate matches — typically only the
137
- * first few lines of the file, saving ~99% I/O on large sessions.
138
- */
139
- function summarizeSession(filePath) {
140
- const line = findJsonlLine(filePath, (l) => {
141
- try {
142
- const obj = JSON.parse(l);
143
- if (obj.type === 'user' && !!obj.message?.content) {
144
- const text = extractText(obj.message.content);
145
- return !!text && !isTaskNotificationText(text);
146
- }
147
- return false;
148
- }
149
- catch {
150
- return false;
151
- }
152
- });
153
- if (!line)
154
- return '(无摘要)';
155
- try {
156
- const obj = JSON.parse(line);
157
- return extractText(obj.message?.content) ?? '(无摘要)';
158
- }
159
- catch {
160
- return '(无摘要)';
161
- }
162
- }
163
- /** Claude-specific field-name mapping for content block extraction. */
164
- const CLAUDE_MAPPING = {
165
- toolUseType: 'tool_use',
166
- toolResultType: 'tool_result',
167
- toolInputField: 'input',
168
- toolErrorField: 'is_error',
169
- };
170
- /**
171
- * Detect Claude CLI sub-agent completion notification text.
172
- * Claude CLI injects a `type:"user"` record with string content starting
173
- * with `<task-notification>` when a sub-agent (Agent tool) finishes.
174
- * These must be excluded from displayTitle/summary — they are machine
175
- * injections, not real user input. Primary criterion: extracted text
176
- * starts with `<task-notification` (works for both string and array
177
- * content since `extractText` is called first). Secondary (optional):
178
- * `origin.kind === 'task-notification'` (not all CLI versions write it).
88
+ * Lazily-created in-memory session index per projectsDir (session-index.ts).
89
+ * First list/read query builds it (full scan); afterwards refresh() is
90
+ * throttled to a 5s on-demand incremental rescan — no background polling.
91
+ * Keep-alive across calls so the index is actually reused.
179
92
  */
180
- function isTaskNotificationText(text) {
181
- return text.trimStart().startsWith('<task-notification');
182
- }
183
- /**
184
- * Extract first text from a user message.
185
- * Used by readFirstUserMessage.
186
- */
187
- function extractText(content) {
188
- // Handle string content directly (for simple user messages)
189
- if (typeof content === 'string' && content.trim()) {
190
- return content.trim();
93
+ const sessionIndexByProjectsDir = new Map();
94
+ function ensureSessionIndex(projectsDir) {
95
+ let index = sessionIndexByProjectsDir.get(projectsDir);
96
+ if (!index) {
97
+ index = new SessionIndex(projectsDir);
98
+ sessionIndexByProjectsDir.set(projectsDir, index);
191
99
  }
192
- const blocks = extractContentBlocks(content, CLAUDE_MAPPING);
193
- // Prefer text block, skip if all blocks are tool_result (user just ran code)
194
- const textBlock = blocks.find((b) => b.type === 'text');
195
- if (textBlock)
196
- return textBlock.content;
197
- return null;
100
+ return index;
198
101
  }
199
102
  /**
200
103
  * List Claude Code sessions for a given cwd, newest first. Each entry has
201
104
  * the sessionId (= jsonl filename stem), a one-line summary extracted from
202
105
  * the first user message, and the file mtime. Returns an empty array when
203
106
  * the project directory doesn't exist or no jsonl has a matching cwd field.
204
- *
205
- * We locate the directory via `projectDirForCwd` (lossy but OK for locating
206
- * a known cwd) and then **verify** each jsonl's cwd field matches. This
207
- * prevents collision: `disk_d/foo` and `disk-d/foo` both encode to the same
208
- * directory name, but only the matching cwd field is kept.
209
107
  */
210
108
  export function listClaudeSessions(cwd, opts = {}) {
211
109
  // 不传 limit = 返回全集不切片;仅显式传值才 slice(契约迁移 plan §2.1,
212
110
  // reader 拿全集算 total 后再自己按 offset/limit 切片)。
213
111
  const limit = opts.limit;
214
- const dir = projectDirForCwd(cwd, opts.projectsDir ?? defaultProjectsDir());
215
- const cacheKey = `${dir}\u0000${cwd}`;
216
- const cached = claudeListCache.get(cacheKey);
217
- let sessions;
218
- if (cached && Date.now() - cached.builtAt < LIST_CACHE_TTL_MS) {
219
- sessions = cached.sessions;
220
- }
221
- else {
222
- sessions = scanClaudeSessions(cwd, dir, opts.projectsDir);
223
- claudeListCache.set(cacheKey, { builtAt: Date.now(), sessions });
224
- // Bound the cache (map iteration order = insertion order).
225
- if (claudeListCache.size > LIST_CACHE_MAX_ENTRIES) {
226
- const oldest = claudeListCache.keys().next().value;
227
- if (oldest !== undefined)
228
- claudeListCache.delete(oldest);
229
- }
230
- }
112
+ const projectsDir = opts.projectsDir ?? defaultProjectsDir();
113
+ const index = ensureSessionIndex(projectsDir);
114
+ // 首次调用延迟全量建索引;之后 5s 按需节流增量刷新(session-index-manual 拍板 3)。
115
+ index.refresh();
116
+ // 统一 parser 的完整 cwd 集合 → A→B→C 任意中间 cwd 都可列出(核心 bug 修复)。
117
+ // readdir 失败时索引保留旧快照(首次失败则为空),与旧扫描的 readdir 兜底等价。
118
+ const sessions = index.listByCwd(cwd).map((e) => ({
119
+ sessionId: e.sessionId,
120
+ summary: e.summary,
121
+ mtime: e.mtimeMs,
122
+ }));
231
123
  return limit === undefined ? sessions : sessions.slice(0, limit);
232
124
  }
233
- /**
234
- * Scan a single project directory for jsonl files whose cwd field matches
235
- * the requested cwd. Uses `jsonlContainsCwd` (any cwd field matches) rather
236
- * than `readCwdFromJsonl` (first cwd only) so that relocated sessions whose
237
- * first cwd is the pre-relocate path are still found. P1 fix 2026-08-10.
238
- */
239
- function scanDirForSessions(cwd, dir) {
240
- let files;
241
- try {
242
- files = fs.readdirSync(dir).filter((f) => f.endsWith('.jsonl'));
243
- }
244
- catch {
245
- return [];
246
- }
247
- const sessions = [];
248
- for (const f of files) {
249
- const full = path.join(dir, f);
250
- try {
251
- const st = fs.statSync(full);
252
- // P1 fix: use jsonlContainsCwd (any match) instead of readCwdFromJsonl
253
- // (first only). Defends against directory-name collisions AND includes
254
- // relocated sessions whose first cwd is the pre-relocate path.
255
- if (!jsonlContainsCwd(full, cwd))
256
- continue;
257
- const sessionId = f.slice(0, -'.jsonl'.length);
258
- sessions.push({
259
- sessionId,
260
- summary: truncate(summarizeSession(full), 60, { normalizeWhitespace: true }),
261
- mtime: st.mtimeMs,
262
- });
263
- }
264
- catch (err) {
265
- getLogger().warn(`[session] skip ${f}: ${err.message}`);
266
- }
267
- }
268
- return sessions;
269
- }
270
- /**
271
- * Scan OTHER project subdirectories for jsonl files whose first cwd matches
272
- * the requested cwd. Used to find sessions that were MOVED OUT of the primary
273
- * dir by EnterWorktree (which relocates the transcript to the target worktree's
274
- * project dir). Such sessions always have firstCwd == queryCwd because
275
- * EnterWorktree happens after session start.
276
- *
277
- * Uses `readCwdFromJsonl` (first cwd only) instead of `jsonlContainsCwd`
278
- * (full-file scan) because:
279
- * - Relocated-out sessions always have firstCwd == queryCwd → readCwdFromJsonl
280
- * is sufficient and ~10× faster (no full-file streaming).
281
- * - Relocated-IN sessions are found by Phase 1's jsonlContainsCwd in the
282
- * primary dir — no need for Phase 2 to find them again.
283
- *
284
- * Skips the primary dir (already scanned) and any sessionIds already found
285
- * (avoids duplicates). S2 fix 2026-08-10.
286
- */
287
- function scanRelocatedSessions(cwd, primaryDir, projectsDir, seenIds) {
288
- let entries;
289
- try {
290
- entries = fs.readdirSync(projectsDir);
291
- }
292
- catch {
293
- return [];
294
- }
295
- const sessions = [];
296
- for (const entry of entries.sort()) {
297
- const subdir = path.join(projectsDir, entry);
298
- // Skip the primary dir (already scanned) and non-directories
299
- if (subdir === primaryDir)
300
- continue;
301
- try {
302
- if (!fs.statSync(subdir).isDirectory())
303
- continue;
304
- }
305
- catch {
306
- continue;
307
- }
308
- let files;
309
- try {
310
- files = fs
311
- .readdirSync(subdir)
312
- .filter((f) => f.endsWith('.jsonl'))
313
- .sort();
314
- }
315
- catch {
316
- continue;
317
- }
318
- for (const f of files) {
319
- const full = path.join(subdir, f);
320
- const sessionId = f.slice(0, -'.jsonl'.length);
321
- // Skip sessions already found in the primary dir scan
322
- if (seenIds.has(sessionId))
323
- continue;
324
- try {
325
- const st = fs.statSync(full);
326
- // S2: use readCwdFromJsonl (first cwd only) instead of
327
- // jsonlContainsCwd (full-file scan). Relocated-out sessions
328
- // always have firstCwd == queryCwd.
329
- const firstCwd = readCwdFromJsonl(full);
330
- if (firstCwd === undefined || firstCwd !== cwd)
331
- continue;
332
- sessions.push({
333
- sessionId,
334
- summary: truncate(summarizeSession(full), 60, { normalizeWhitespace: true }),
335
- mtime: st.mtimeMs,
336
- });
337
- // Prevent duplicates if the same sessionId exists in multiple dirs
338
- seenIds.add(sessionId);
339
- }
340
- catch (err) {
341
- getLogger().warn(`[session] skip ${f}: ${err.message}`);
342
- }
343
- }
344
- }
345
- return sessions;
346
- }
347
- /** Uncached scan of a claude project dir (the expensive part being cached).
348
- * Phase 1 scans the primary project dir using jsonlContainsCwd (any cwd
349
- * match) to find sessions including relocated-in ones.
350
- * Phase 2 scans all OTHER project dirs using readCwdFromJsonl (first cwd
351
- * only) to find relocated-out sessions (always firstCwd == queryCwd).
352
- * S2 fix 2026-08-10: removed the primarySessions.length === 0 guard so
353
- * relocated-out sessions are visible even when the primary dir has other
354
- * sessions. readCwdFromJsonl is ~10× cheaper than jsonlContainsCwd for
355
- * cross-dir scanning.
356
- */
357
- function scanClaudeSessions(cwd, dir, projectsDir) {
358
- const sessions = [];
359
- // Phase 1: scan the primary project dir (fast path)
360
- const primarySessions = scanDirForSessions(cwd, dir);
361
- sessions.push(...primarySessions);
362
- // Phase 2: cross-dir fallback for relocated sessions whose transcript
363
- // was moved away from the primary dir by EnterWorktree.
364
- // Always executed (S2 fix): relocated-out sessions have firstCwd == queryCwd
365
- // but are invisible to Phase 1 (file moved away). seenIds prevents
366
- // double-counting sessions already found by Phase 1.
367
- const effectiveProjectsDir = projectsDir ?? defaultProjectsDir();
368
- if (effectiveProjectsDir && dir.startsWith(effectiveProjectsDir)) {
369
- const seenIds = new Set(primarySessions.map((s) => s.sessionId));
370
- const crossDirSessions = scanRelocatedSessions(cwd, dir, effectiveProjectsDir, seenIds);
371
- sessions.push(...crossDirSessions);
372
- }
373
- // Same-mtime ties: secondary key keeps the full order deterministic.
374
- sessions.sort((a, b) => b.mtime - a.mtime || a.sessionId.localeCompare(b.sessionId));
375
- return sessions;
376
- }
377
125
  /** Get the newest session for a given cwd. Returns undefined if none exist. */
378
126
  export function getNewestSession(cwd, opts = {}) {
379
127
  const sessions = listClaudeSessions(cwd, { ...opts, limit: 1 });
@@ -427,24 +175,43 @@ function isSessionActive(filePath, mtimeMs, now = Date.now()) {
427
175
  */
428
176
  export function isClaudeSessionActive(sessionId, cwd, opts = {}) {
429
177
  const projectsDir = opts.projectsDir ?? defaultProjectsDir();
430
- const dir = projectDirForCwd(cwd, projectsDir);
431
- let filePath = path.join(dir, `${sessionId}.jsonl`);
432
- if (!fs.existsSync(filePath)) {
433
- // EnterWorktree relocate: transcript moved to the new cwd's project dir.
434
- const relocated = findSessionFileInProjects(sessionId, projectsDir, cwd);
435
- if (!relocated)
436
- return false;
437
- filePath = relocated;
438
- }
439
- // Verify the jsonl belongs to the requested cwd (any cwd field may match).
440
- if (!jsonlContainsCwd(filePath, cwd))
441
- return false;
442
- try {
443
- const st = fs.statSync(filePath);
444
- return isSessionActive(filePath, st.mtimeMs);
178
+ const index = ensureSessionIndex(projectsDir);
179
+ if (index.isBuilt) {
180
+ // 索引定位: findBySessionIdAndCwd 内部 re-stat 比 fingerprint(拍板 3),
181
+ // cwdSet 匹配即守卫通过,无需再全文件扫一遍。
182
+ index.refresh();
183
+ const entry = index.findBySessionIdAndCwd(sessionId, cwd);
184
+ if (entry) {
185
+ try {
186
+ const st = fs.statSync(entry.path);
187
+ return isSessionActive(entry.path, st.mtimeMs);
188
+ }
189
+ catch {
190
+ return false;
191
+ }
192
+ }
445
193
  }
446
- catch {
447
- return false;
194
+ // 索引 miss / 未构建 / 降级 → 精确 sessionId 全目录 fallback + cwd 守卫
195
+ {
196
+ const dir = projectDirForCwd(cwd, projectsDir);
197
+ let filePath = path.join(dir, `${sessionId}.jsonl`);
198
+ if (!fs.existsSync(filePath)) {
199
+ // EnterWorktree relocate: transcript moved to the new cwd's project dir.
200
+ const relocated = findSessionFileInProjects(sessionId, projectsDir, cwd);
201
+ if (!relocated)
202
+ return false;
203
+ filePath = relocated;
204
+ }
205
+ // Verify the jsonl belongs to the requested cwd (any cwd field may match).
206
+ if (!fileContainsCwd(filePath, cwd))
207
+ return false;
208
+ try {
209
+ const st = fs.statSync(filePath);
210
+ return isSessionActive(filePath, st.mtimeMs);
211
+ }
212
+ catch {
213
+ return false;
214
+ }
448
215
  }
449
216
  }
450
217
  /**
@@ -521,7 +288,7 @@ function scalarScan(filePath) {
521
288
  }
522
289
  // --- last user message for displayTitle ---
523
290
  // Skip: 1) tool_result content, 2) isCompactSummary (recap),
524
- // 3) isMeta (skill injection / command echo / CLAUDE.md)
291
+ // 3) isMeta (skill injection / command echo / project rules)
525
292
  if (obj.type === 'user' && !obj.isCompactSummary && !obj.isMeta) {
526
293
  const msg = obj.message;
527
294
  if (msg?.role === 'user' && msg.content) {
@@ -625,19 +392,35 @@ function extractEventsFromTail(tailLines, maxEvents) {
625
392
  export function readSessionContent(sessionId, cwd, opts = {}) {
626
393
  const { maxEvents } = opts;
627
394
  const projectsDir = opts.projectsDir ?? defaultProjectsDir();
628
- const dir = projectDirForCwd(cwd, projectsDir);
629
- let filePath = path.join(dir, `${sessionId}.jsonl`);
630
- if (!fs.existsSync(filePath)) {
631
- // EnterWorktree relocate: transcript moved to the new cwd's project dir.
632
- const relocated = findSessionFileInProjects(sessionId, projectsDir, cwd);
633
- if (!relocated)
634
- return { events: [] };
635
- filePath = relocated;
395
+ let filePath;
396
+ const index = ensureSessionIndex(projectsDir);
397
+ if (index.isBuilt) {
398
+ // 索引定位: findBySessionIdAndCwd 内部 re-stat 比 fingerprint(拍板 3),
399
+ // cwdSet 匹配即守卫通过,无需再全文件扫一遍(不许在指纹一致时全扫)。
400
+ index.refresh();
401
+ const entry = index.findBySessionIdAndCwd(sessionId, cwd);
402
+ if (entry)
403
+ filePath = entry.path;
636
404
  }
637
- // Verify the jsonl actually belongs to the requested cwd (dir encoding is
638
- // lossy). ANY cwd field may match: relocated sessions contain multiple.
639
- if (!jsonlContainsCwd(filePath, cwd)) {
640
- return { events: [] };
405
+ if (filePath === undefined) {
406
+ // 索引 miss / 未构建 / 降级 → 旧 fallback(精确 sessionId 全目录扫描 + cwd 守卫)
407
+ const dir = projectDirForCwd(cwd, projectsDir);
408
+ const direct = path.join(dir, `${sessionId}.jsonl`);
409
+ if (fs.existsSync(direct)) {
410
+ filePath = direct;
411
+ }
412
+ else {
413
+ // EnterWorktree relocate: transcript moved to the new cwd's project dir.
414
+ const relocated = findSessionFileInProjects(sessionId, projectsDir, cwd);
415
+ if (!relocated)
416
+ return { events: [] };
417
+ filePath = relocated;
418
+ }
419
+ // Verify the jsonl actually belongs to the requested cwd (dir encoding is
420
+ // lossy). ANY cwd field may match: relocated sessions contain multiple.
421
+ if (!fileContainsCwd(filePath, cwd)) {
422
+ return { events: [] };
423
+ }
641
424
  }
642
425
  // P2-2 + P3-1 + P2-5: First pass STREAMS the file once via
643
426
  // `scanJsonlLines` (no `string[]` materialized), parsing each line once to
@@ -654,8 +437,8 @@ export function readSessionContent(sessionId, cwd, opts = {}) {
654
437
  // When the session has NO user message, tailOffset stays -1 and the tail
655
438
  // IS the whole file → scan(N) + tail(N) = 2.0× — the known asymptotic
656
439
  // upper bound of the body's two-phase design (still better than pre-P2-2's
657
- // ~3×). Total parse count adds readCwdFromJsonl's fixed overhead (~2
658
- // parses, independent of line count).
440
+ // ~3×). Total parse count adds the index-miss fallback guard's fixed
441
+ // early-stop overhead (~2 parses, independent of line count).
659
442
  const scan = scalarScan(filePath);
660
443
  const usage = scan.usage.inputTokens + scan.usage.outputTokens > 0 ? scan.usage : undefined;
661
444
  // Extract events AFTER the last user message by re-reading + re-parsing
@@ -39,11 +39,11 @@ export function readCodexRollout(filePath) {
39
39
  let sessionMeta = null;
40
40
  // Real user input is identified by a paired `event_msg` whose
41
41
  // payload.type === "user_message" - codex emits this ONLY for text the
42
- // human actually typed. Injected scaffolding (AGENTS.md project rules,
42
+ // human actually typed. Injected scaffolding (project rules,
43
43
  // <environment_context>, permissions) is also written as `role:"user"`
44
44
  // response_items but has NO user_message event, so it must be excluded
45
45
  // from displayTitle/recap/summary. Regression 2026-07-13: the first
46
- // `role:user` message is the injected AGENTS.md, mistakenly shown as
46
+ // `role:user` message is the injected project rules, mistakenly shown as
47
47
  // "最近输入".
48
48
  const realUserMessages = [];
49
49
  // token_count tracking: codex emits cumulative `total_token_usage` and an
@@ -286,7 +286,7 @@ export function readCodexSessionContent(sessionId, opts = {}) {
286
286
  // displayTitle = LAST real user message (matches the
287
287
  // "最近输入" label). recap is undefined: codex has no
288
288
  // compact-summary concept, so never fake one from a user
289
- // message (was: firstUserMessage = injected AGENTS.md).
289
+ // message (was: firstUserMessage = injected project rules).
290
290
  displayTitle: rollout.lastRealUserMessage
291
291
  ? rollout.lastRealUserMessage.slice(0, 50)
292
292
  : undefined,
@@ -0,0 +1,10 @@
1
+ /**
2
+ * Self-update module for lark-remote.
3
+ *
4
+ * Provides version checking (npm registry query + cache),
5
+ * package installation (detect package manager + global install),
6
+ * and the combined workflow for the /update command and --update CLI flag.
7
+ */
8
+ export { isNewer, checkLatestVersion, type VersionCheckResult, type UpdateCache, CACHE_TTL_MS, } from './version-check.js';
9
+ export { detectPackageManager, runInstallLatest, type PackageManager, type InstallResult, } from './install.js';
10
+ export { formatUpdateHint } from './startup-hint.js';
@@ -0,0 +1,11 @@
1
+ /**
2
+ * Self-update module for lark-remote.
3
+ *
4
+ * Provides version checking (npm registry query + cache),
5
+ * package installation (detect package manager + global install),
6
+ * and the combined workflow for the /update command and --update CLI flag.
7
+ */
8
+ export { isNewer, checkLatestVersion, CACHE_TTL_MS, } from './version-check.js';
9
+ export { detectPackageManager, runInstallLatest, } from './install.js';
10
+ export { formatUpdateHint } from './startup-hint.js';
11
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1,29 @@
1
+ /** Supported package managers for global install. */
2
+ export type PackageManager = 'npm' | 'bun' | 'pnpm';
3
+ /** Result of an install attempt. */
4
+ export interface InstallResult {
5
+ success: boolean;
6
+ error?: string;
7
+ }
8
+ /** Type signature matching execFile's callback style. */
9
+ type ExecFileCallback = (error: Error | null, stdout: string | Buffer, stderr: string | Buffer) => void;
10
+ type ExecFileFn = (file: string, args: readonly string[], options: object, callback: ExecFileCallback) => void;
11
+ /**
12
+ * Detect which package manager to use for global install.
13
+ *
14
+ * Priority:
15
+ * 1. Environment variable override (LARK_REMOTE_MANAGED_BY)
16
+ * 2. `which` availability: npm → bun → pnpm
17
+ */
18
+ export declare function detectPackageManager(): PackageManager | null;
19
+ /**
20
+ * Run the global install command to upgrade lark-remote to the latest version.
21
+ *
22
+ * @param opts.packageManager - Override detected package manager
23
+ * @param opts.execFn - Override execFile for testing
24
+ */
25
+ export declare function runInstallLatest(opts?: {
26
+ packageManager?: PackageManager | null;
27
+ execFn?: ExecFileFn;
28
+ }): Promise<InstallResult>;
29
+ export {};