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.
- package/CHANGELOG.md +86 -0
- package/README.en.md +4 -1
- package/README.md +4 -1
- package/dist/bridge/index.d.ts +5 -1
- package/dist/bridge/index.js +25 -1
- package/dist/card/run-renderer.js +1 -1
- package/dist/card/run-state.js +7 -0
- package/dist/config/dir.d.ts +1 -0
- package/dist/config/dir.js +5 -1
- package/dist/config/index.d.ts +1 -0
- package/dist/config/index.js +2 -0
- package/dist/connector/index.js +20 -0
- package/dist/index.js +54 -2
- package/dist/router/index.d.ts +62 -1
- package/dist/router/index.js +466 -46
- package/dist/runner/codex/argv.d.ts +1 -1
- package/dist/runner/codex/argv.js +1 -1
- package/dist/runner/common/runner-utils.d.ts +12 -0
- package/dist/runner/common/runner-utils.js +21 -0
- package/dist/runner/common/spawning-runner.js +9 -1
- package/dist/session/claude/session-index.d.ts +93 -0
- package/dist/session/claude/session-index.js +374 -0
- package/dist/session/claude/sessions.d.ts +0 -18
- package/dist/session/claude/sessions.js +102 -319
- package/dist/session/codex/rollout-reader.js +3 -3
- package/dist/update/index.d.ts +10 -0
- package/dist/update/index.js +11 -0
- package/dist/update/install.d.ts +29 -0
- package/dist/update/install.js +93 -0
- package/dist/update/startup-hint.d.ts +5 -0
- package/dist/update/startup-hint.js +11 -0
- package/dist/update/version-check.d.ts +31 -0
- package/dist/update/version-check.js +149 -0
- package/dist/workspace/index.d.ts +19 -1
- package/dist/workspace/index.js +51 -5
- package/package.json +1 -14
|
@@ -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.
|
|
18
|
-
*
|
|
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
|
-
*
|
|
26
|
-
*
|
|
27
|
-
*
|
|
28
|
-
*
|
|
29
|
-
*
|
|
30
|
-
*
|
|
31
|
-
*
|
|
32
|
-
*
|
|
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
|
-
|
|
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 (
|
|
81
|
+
if (fileContainsCwd(candidate, cwd))
|
|
112
82
|
return candidate;
|
|
113
83
|
}
|
|
114
84
|
}
|
|
115
85
|
return candidates[0];
|
|
116
86
|
}
|
|
117
87
|
/**
|
|
118
|
-
*
|
|
119
|
-
*
|
|
120
|
-
*
|
|
121
|
-
*
|
|
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
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
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
|
-
|
|
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
|
|
215
|
-
const
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
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
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
//
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
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
|
-
|
|
447
|
-
|
|
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 /
|
|
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
|
-
|
|
629
|
-
|
|
630
|
-
if (
|
|
631
|
-
//
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
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
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
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
|
|
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 (
|
|
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
|
|
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
|
|
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 {};
|