ccprofiles-core 0.10.0 → 0.10.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/handoff.d.ts +9 -1
- package/dist/handoff.js +14 -2
- package/dist/sessions.d.ts +11 -1
- package/dist/sessions.js +50 -6
- package/package.json +1 -1
package/dist/handoff.d.ts
CHANGED
|
@@ -1,5 +1,13 @@
|
|
|
1
1
|
import type { ProjectSessions, SessionTranscript } from './sessions.js';
|
|
2
|
-
|
|
2
|
+
import type { OsKind } from './platform.js';
|
|
3
|
+
/**
|
|
4
|
+
* Compare two absolute paths the way the OS would. Windows is case-insensitive and mixes
|
|
5
|
+
* separators freely (`C:\p` / `c:/p` are one path), so a strict === there silently fails to
|
|
6
|
+
* match a session against its own project. POSIX is case-sensitive — normalizing it would
|
|
7
|
+
* merge genuinely distinct paths, so it stays exact.
|
|
8
|
+
*/
|
|
9
|
+
export declare function samePath(a: string, b: string, os: OsKind): boolean;
|
|
10
|
+
export declare function findLastSessionForCwd(scanned: ProjectSessions[], cwd: string, scope: string, agent: 'claude' | 'codex', os?: OsKind): {
|
|
3
11
|
scope: string;
|
|
4
12
|
id: string;
|
|
5
13
|
} | null;
|
package/dist/handoff.js
CHANGED
|
@@ -1,7 +1,19 @@
|
|
|
1
|
-
|
|
1
|
+
/**
|
|
2
|
+
* Compare two absolute paths the way the OS would. Windows is case-insensitive and mixes
|
|
3
|
+
* separators freely (`C:\p` / `c:/p` are one path), so a strict === there silently fails to
|
|
4
|
+
* match a session against its own project. POSIX is case-sensitive — normalizing it would
|
|
5
|
+
* merge genuinely distinct paths, so it stays exact.
|
|
6
|
+
*/
|
|
7
|
+
export function samePath(a, b, os) {
|
|
8
|
+
if (os !== 'win32')
|
|
9
|
+
return a === b;
|
|
10
|
+
const norm = (p) => p.replace(/[\\/]+/g, '\\').replace(/(.)\\+$/, '$1').toLowerCase();
|
|
11
|
+
return norm(a) === norm(b);
|
|
12
|
+
}
|
|
13
|
+
export function findLastSessionForCwd(scanned, cwd, scope, agent, os = 'linux') {
|
|
2
14
|
let newest = null;
|
|
3
15
|
for (const p of scanned) {
|
|
4
|
-
if (p.project
|
|
16
|
+
if (!samePath(p.project, cwd, os) || p.scope !== scope || p.agent !== agent)
|
|
5
17
|
continue;
|
|
6
18
|
for (const s of p.sessions)
|
|
7
19
|
if (!newest || s.mtime > newest.mtime)
|
package/dist/sessions.d.ts
CHANGED
|
@@ -36,8 +36,18 @@ export declare function sessionScanCacheStats(): {
|
|
|
36
36
|
hits: number;
|
|
37
37
|
misses: number;
|
|
38
38
|
};
|
|
39
|
-
/**
|
|
39
|
+
/**
|
|
40
|
+
* Last resort only — lossy, and unfixably so. Claude Code encodes a project path by replacing
|
|
41
|
+
* every '/' with '-', so a segment that legitimately contains a hyphen (`/Users/lp-stf00543`)
|
|
42
|
+
* cannot be told apart from a separator on the way back. Prefer the `cwd` recorded inside the
|
|
43
|
+
* session file; `headCwd` exists so that stays available even for files we decline to read.
|
|
44
|
+
*/
|
|
40
45
|
export declare function decodeProjectDir(name: string): string;
|
|
46
|
+
/**
|
|
47
|
+
* First cwd found in a head slice. A slice cut mid-record leaves a partial trailing line, which
|
|
48
|
+
* simply fails JSON.parse and is skipped — no need to guess which line is incomplete.
|
|
49
|
+
*/
|
|
50
|
+
export declare function headCwd(head: string, pick: (rec: any) => string | undefined): string | null;
|
|
41
51
|
export declare function scanSessions(opts: {
|
|
42
52
|
sharedRoot: string;
|
|
43
53
|
profiles: {
|
package/dist/sessions.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { readdir, readFile, stat, lstat } from 'node:fs/promises';
|
|
1
|
+
import { readdir, readFile, stat, lstat, open } from 'node:fs/promises';
|
|
2
2
|
import { existsSync } from 'node:fs';
|
|
3
3
|
import { join } from 'node:path';
|
|
4
4
|
const SAFE_SESSION_ID = /^[A-Za-z0-9_-]{1,160}$/;
|
|
@@ -7,6 +7,8 @@ const claudeSessionCache = new Map();
|
|
|
7
7
|
const codexSessionCache = new Map();
|
|
8
8
|
/** Per-file cap: a transcript above this size is summarized without reading its content. */
|
|
9
9
|
const MAX_FILE_SCAN_BYTES = 10 * 1024 * 1024;
|
|
10
|
+
// enough to reach the first record carrying cwd, without reading a multi-MB transcript
|
|
11
|
+
const HEAD_SCAN_BYTES = 64 * 1024;
|
|
10
12
|
/** Per-request cap: once this many bytes have been read from disk in one scanSessions() call,
|
|
11
13
|
* remaining (uncached) files are summarized without reading their content either. */
|
|
12
14
|
const MAX_TOTAL_SCAN_BYTES = 50 * 1024 * 1024;
|
|
@@ -17,10 +19,52 @@ let scanCacheMisses = 0;
|
|
|
17
19
|
export function sessionScanCacheStats() {
|
|
18
20
|
return { hits: scanCacheHits, misses: scanCacheMisses };
|
|
19
21
|
}
|
|
20
|
-
/**
|
|
22
|
+
/**
|
|
23
|
+
* Last resort only — lossy, and unfixably so. Claude Code encodes a project path by replacing
|
|
24
|
+
* every '/' with '-', so a segment that legitimately contains a hyphen (`/Users/lp-stf00543`)
|
|
25
|
+
* cannot be told apart from a separator on the way back. Prefer the `cwd` recorded inside the
|
|
26
|
+
* session file; `headCwd` exists so that stays available even for files we decline to read.
|
|
27
|
+
*/
|
|
21
28
|
export function decodeProjectDir(name) {
|
|
22
29
|
return name.replace(/^-/, '/').replace(/-/g, '/');
|
|
23
30
|
}
|
|
31
|
+
/** First `bytes` of a file, for cheap field recovery without reading a multi-MB transcript. */
|
|
32
|
+
async function readHead(file, bytes = HEAD_SCAN_BYTES) {
|
|
33
|
+
let fh = null;
|
|
34
|
+
try {
|
|
35
|
+
fh = await open(file, 'r');
|
|
36
|
+
const buf = Buffer.alloc(bytes);
|
|
37
|
+
const { bytesRead } = await fh.read(buf, 0, bytes, 0);
|
|
38
|
+
return buf.subarray(0, bytesRead).toString('utf8');
|
|
39
|
+
}
|
|
40
|
+
catch {
|
|
41
|
+
return '';
|
|
42
|
+
}
|
|
43
|
+
finally {
|
|
44
|
+
await fh?.close().catch(() => { });
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* First cwd found in a head slice. A slice cut mid-record leaves a partial trailing line, which
|
|
49
|
+
* simply fails JSON.parse and is skipped — no need to guess which line is incomplete.
|
|
50
|
+
*/
|
|
51
|
+
export function headCwd(head, pick) {
|
|
52
|
+
for (const line of head.split('\n')) {
|
|
53
|
+
if (!line.trim())
|
|
54
|
+
continue;
|
|
55
|
+
let rec;
|
|
56
|
+
try {
|
|
57
|
+
rec = JSON.parse(line);
|
|
58
|
+
}
|
|
59
|
+
catch {
|
|
60
|
+
continue;
|
|
61
|
+
}
|
|
62
|
+
const v = pick(rec);
|
|
63
|
+
if (typeof v === 'string' && v)
|
|
64
|
+
return v;
|
|
65
|
+
}
|
|
66
|
+
return null;
|
|
67
|
+
}
|
|
24
68
|
/**
|
|
25
69
|
* Shared mtime+size cache lookup for the two session-file formats. `parseFull` runs only on a
|
|
26
70
|
* cache miss with a fresh read; `makeTruncated` builds a summary-only result when the file is
|
|
@@ -43,12 +87,12 @@ async function cachedParse(file, cache, budget, parseFull, makeTruncated) {
|
|
|
43
87
|
}
|
|
44
88
|
scanCacheMisses++;
|
|
45
89
|
if (st.size > MAX_FILE_SCAN_BYTES) {
|
|
46
|
-
const result = makeTruncated(st);
|
|
90
|
+
const result = makeTruncated(st, await readHead(file));
|
|
47
91
|
cache.set(file, { mtimeMs: st.mtimeMs, size: st.size, result });
|
|
48
92
|
return result;
|
|
49
93
|
}
|
|
50
94
|
if (st.size > budget.bytesLeft)
|
|
51
|
-
return makeTruncated(st); // not cached — budget is per-request
|
|
95
|
+
return makeTruncated(st, await readHead(file)); // not cached — budget is per-request
|
|
52
96
|
let raw;
|
|
53
97
|
try {
|
|
54
98
|
raw = await readFile(file, 'utf8');
|
|
@@ -90,7 +134,7 @@ async function parseClaudeSession(file, budget) {
|
|
|
90
134
|
break;
|
|
91
135
|
}
|
|
92
136
|
return { meta: { id, mtime: st.mtimeMs, messageCount: lines.length, firstPrompt, gitBranch, model, sizeBytes: st.size }, cwd };
|
|
93
|
-
}, st => ({ meta: { id, mtime: st.mtimeMs, messageCount: 0, firstPrompt: null, gitBranch: null, model: null, sizeBytes: st.size, truncated: true }, cwd:
|
|
137
|
+
}, (st, head) => ({ meta: { id, mtime: st.mtimeMs, messageCount: 0, firstPrompt: null, gitBranch: null, model: null, sizeBytes: st.size, truncated: true }, cwd: headCwd(head, r => r.cwd) }));
|
|
94
138
|
}
|
|
95
139
|
async function scanProjectsDir(projectsDir, scope, budget) {
|
|
96
140
|
if (!existsSync(projectsDir))
|
|
@@ -314,7 +358,7 @@ async function parseCodexSession(file, budget) {
|
|
|
314
358
|
}
|
|
315
359
|
id ??= fallbackId;
|
|
316
360
|
return { meta: { id, mtime: st.mtimeMs, messageCount: messageCount || lines.length, firstPrompt, gitBranch, model, sizeBytes: st.size }, cwd };
|
|
317
|
-
}, st => ({ meta: { id: fallbackId, mtime: st.mtimeMs, messageCount: 0, firstPrompt: null, gitBranch: null, model: null, sizeBytes: st.size, truncated: true }, cwd:
|
|
361
|
+
}, (st, head) => ({ meta: { id: fallbackId, mtime: st.mtimeMs, messageCount: 0, firstPrompt: null, gitBranch: null, model: null, sizeBytes: st.size, truncated: true }, cwd: headCwd(head, r => r?.payload?.cwd) }));
|
|
318
362
|
}
|
|
319
363
|
async function codexSessionFiles(dir) {
|
|
320
364
|
let entries;
|
package/package.json
CHANGED