@tom2012/cc-web 2026.4.19-n → 2026.4.19-p
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/README.md +2 -2
- package/backend/dist/adapters/claude-adapter.d.ts +1 -2
- package/backend/dist/adapters/claude-adapter.d.ts.map +1 -1
- package/backend/dist/adapters/claude-adapter.js +0 -22
- package/backend/dist/adapters/claude-adapter.js.map +1 -1
- package/backend/dist/adapters/codex-adapter.d.ts +3 -4
- package/backend/dist/adapters/codex-adapter.d.ts.map +1 -1
- package/backend/dist/adapters/codex-adapter.js +2 -40
- package/backend/dist/adapters/codex-adapter.js.map +1 -1
- package/backend/dist/adapters/gemini-adapter.d.ts +1 -7
- package/backend/dist/adapters/gemini-adapter.d.ts.map +1 -1
- package/backend/dist/adapters/gemini-adapter.js +0 -8
- package/backend/dist/adapters/gemini-adapter.js.map +1 -1
- package/backend/dist/adapters/opencode-adapter.d.ts +1 -2
- package/backend/dist/adapters/opencode-adapter.d.ts.map +1 -1
- package/backend/dist/adapters/opencode-adapter.js +0 -1
- package/backend/dist/adapters/opencode-adapter.js.map +1 -1
- package/backend/dist/adapters/qwen-adapter.d.ts +1 -2
- package/backend/dist/adapters/qwen-adapter.d.ts.map +1 -1
- package/backend/dist/adapters/qwen-adapter.js +0 -1
- package/backend/dist/adapters/qwen-adapter.js.map +1 -1
- package/backend/dist/adapters/terminal-adapter.d.ts +1 -2
- package/backend/dist/adapters/terminal-adapter.d.ts.map +1 -1
- package/backend/dist/adapters/terminal-adapter.js +0 -1
- package/backend/dist/adapters/terminal-adapter.js.map +1 -1
- package/backend/dist/adapters/types.d.ts +1 -3
- package/backend/dist/adapters/types.d.ts.map +1 -1
- package/backend/dist/config.d.ts +0 -1
- package/backend/dist/config.d.ts.map +1 -1
- package/backend/dist/config.js +0 -4
- package/backend/dist/config.js.map +1 -1
- package/backend/dist/index.d.ts.map +1 -1
- package/backend/dist/index.js +14 -31
- package/backend/dist/index.js.map +1 -1
- package/backend/dist/routes/hooks.d.ts.map +1 -1
- package/backend/dist/routes/hooks.js +0 -21
- package/backend/dist/routes/hooks.js.map +1 -1
- package/backend/dist/routes/projects.d.ts.map +1 -1
- package/backend/dist/routes/projects.js +23 -193
- package/backend/dist/routes/projects.js.map +1 -1
- package/backend/dist/session-manager.d.ts +48 -35
- package/backend/dist/session-manager.d.ts.map +1 -1
- package/backend/dist/session-manager.js +162 -263
- package/backend/dist/session-manager.js.map +1 -1
- package/frontend/dist/assets/{AssistantMessageContent-Cb3vWWdt.js → AssistantMessageContent-wJrH2meJ.js} +1 -1
- package/frontend/dist/assets/{GraphPreview-DLCHQmiL.js → GraphPreview-Dc0doluU.js} +1 -1
- package/frontend/dist/assets/MobilePage-BpP-L6oO.js +14 -0
- package/frontend/dist/assets/{OfficePreview-Dp3pb_je.js → OfficePreview-CuLXXJtC.js} +2 -2
- package/frontend/dist/assets/{PlanPanel-ZyFJbOGU.js → PlanPanel-CxCXowAR.js} +1 -1
- package/frontend/dist/assets/{ProjectPage-8jEnQIBP.js → ProjectPage-B2D-WoYj.js} +5 -5
- package/frontend/dist/assets/{SettingsPage--XfgMw_S.js → SettingsPage-BRdp1y0m.js} +1 -1
- package/frontend/dist/assets/{SkillHubPage-8akrua_r.js → SkillHubPage-jirxSrmL.js} +3 -3
- package/frontend/dist/assets/{chevron-down-O78Rv4X9.js → chevron-down-B6rODcyf.js} +1 -1
- package/frontend/dist/assets/{chevron-up-CmZ9PR-r.js → chevron-up-CVjkTdMA.js} +1 -1
- package/frontend/dist/assets/{index-Cz6H099h.js → index-BzaacjGy.js} +9 -9
- package/frontend/dist/assets/{index-C1vsGo1Z.js → index-DD7aR8Sa.js} +1 -1
- package/frontend/dist/assets/index-D_umrsZK.css +1 -0
- package/frontend/dist/assets/{index-B5gXUoQK.js → index-Qcpg6uVd.js} +1 -1
- package/frontend/dist/assets/{jszip.min-Bay3QEoh.js → jszip.min-C_V8wSYG.js} +1 -1
- package/frontend/dist/assets/{maximize-2-x-gvOFji.js → maximize-2-C6Y5UA8p.js} +1 -1
- package/frontend/dist/assets/{search-Cp-3z5TH.js → search-BEl0zazi.js} +1 -1
- package/frontend/dist/index.html +2 -2
- package/package.json +1 -1
- package/frontend/dist/assets/MobilePage-ALW_i1_8.js +0 -14
- package/frontend/dist/assets/ShareViewPage-gVIeDo2t.js +0 -1
- package/frontend/dist/assets/bot-DL3ewTz_.js +0 -7
- package/frontend/dist/assets/index-DXR3Cuy1.css +0 -1
- package/frontend/dist/assets/user-B84sgmht.js +0 -7
|
@@ -1,27 +1,22 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* SessionManager — reads conversation history directly from
|
|
3
|
-
* native JSONL files
|
|
2
|
+
* SessionManager — reads conversation history directly from the CLI's
|
|
3
|
+
* native JSONL files (e.g. ~/.claude/projects/{encoded-path}/{uuid}.jsonl).
|
|
4
4
|
*
|
|
5
|
-
* No PTY parsing, no ANSI stripping, no heuristics
|
|
5
|
+
* No PTY parsing, no ANSI stripping, no heuristics. The JSONL is the single
|
|
6
|
+
* source of truth; ccweb maintains no separate conversation store.
|
|
6
7
|
*/
|
|
7
8
|
import { EventEmitter } from 'events';
|
|
8
9
|
import type { CliTool } from './types';
|
|
9
|
-
export interface SessionMessage {
|
|
10
|
-
role: 'user' | 'assistant';
|
|
11
|
-
content: string;
|
|
12
|
-
timestamp: string;
|
|
13
|
-
}
|
|
14
|
-
export interface Session {
|
|
15
|
-
id: string;
|
|
16
|
-
projectId: string;
|
|
17
|
-
startedAt: string;
|
|
18
|
-
messages: SessionMessage[];
|
|
19
|
-
}
|
|
20
10
|
export interface ChatBlockItem {
|
|
21
11
|
type: 'text' | 'thinking' | 'tool_use' | 'tool_result';
|
|
22
12
|
content: string;
|
|
23
13
|
}
|
|
24
14
|
export interface ChatBlock {
|
|
15
|
+
/** Stable block id for dedup between WS replay and HTTP history. Derived
|
|
16
|
+
* from sha1(jsonlPath + source) so it's idempotent across restarts and
|
|
17
|
+
* unique per entry. Optional on older code paths; always populated by
|
|
18
|
+
* session-manager. */
|
|
19
|
+
id?: string;
|
|
25
20
|
role: 'user' | 'assistant';
|
|
26
21
|
timestamp: string;
|
|
27
22
|
blocks: ChatBlockItem[];
|
|
@@ -41,13 +36,45 @@ declare class SessionManager extends EventEmitter {
|
|
|
41
36
|
getAllSemanticStatus(): Record<string, SemanticStatus>;
|
|
42
37
|
/** Return the JSONL file path currently being tailed for this project. */
|
|
43
38
|
getJsonlPath(projectId: string): string | null;
|
|
44
|
-
/** Return all parsed ChatBlocks from the
|
|
39
|
+
/** Return all parsed ChatBlocks from the project's latest JSONL file.
|
|
40
|
+
*
|
|
41
|
+
* Resolution order:
|
|
42
|
+
* 1. Active watcher already has `jsonlPath` discovered via hooks → use it.
|
|
43
|
+
* 2. Watcher exists but hasn't discovered the file yet → lazily scan the
|
|
44
|
+
* adapter's session dir, pick the latest JSONL, cache on the watcher.
|
|
45
|
+
* 3. No watcher (stopped project, user just navigated in) → resolve
|
|
46
|
+
* project config on-the-fly and pick the latest JSONL. Do NOT
|
|
47
|
+
* persist to `this.watchers` because a watcher without a PTY
|
|
48
|
+
* would drift (no hooks, no triggerRead).
|
|
49
|
+
*
|
|
50
|
+
* This indirection exists because chat history has to be loadable without
|
|
51
|
+
* a hook ever firing (e.g. immediately after ccweb restart — triggerRead
|
|
52
|
+
* only runs when Claude Code's hooks call `/api/hooks`).
|
|
53
|
+
*/
|
|
45
54
|
getChatHistory(projectId: string): ChatBlock[];
|
|
55
|
+
/** Parse a JSONL/JSON session file into ChatBlocks with stable ids. */
|
|
56
|
+
private parseJsonlFile;
|
|
57
|
+
/** Find the latest JSONL file for a project (single source of truth for
|
|
58
|
+
* both chat-history HTTP and hook-driven tail paths).
|
|
59
|
+
*
|
|
60
|
+
* Strategy: pick the newest file (by mtime) in the adapter's session dir.
|
|
61
|
+
* For adapters that store sessions in a shared tree (e.g. Codex's
|
|
62
|
+
* date-partitioned dir), use `getSessionFilesForProject` to scope by cwd.
|
|
63
|
+
*
|
|
64
|
+
* Deliberately has NO startedAt/recency filter — historical requirement
|
|
65
|
+
* was for "the file THIS session is writing", but that caused HTTP and
|
|
66
|
+
* WS paths to disagree on which file to read, breaking block-id dedup.
|
|
67
|
+
* Using "newest" works because Claude --continue updates the mtime of
|
|
68
|
+
* the continued file, and any new file it creates has the highest mtime.
|
|
69
|
+
* The caller (triggerRead) handles mid-session file switches by re-
|
|
70
|
+
* checking on each call.
|
|
71
|
+
*/
|
|
72
|
+
private findLatestJsonlForProject;
|
|
46
73
|
registerChatListener(projectId: string, cb: (msg: ChatBlock) => void): void;
|
|
47
74
|
unregisterChatListener(projectId: string, cb: (msg: ChatBlock) => void): void;
|
|
48
|
-
/** Call when a new PTY starts for a project
|
|
75
|
+
/** Call when a new PTY starts for a project. Registers a watcher so that
|
|
76
|
+
* subsequent hook-driven `triggerRead()` calls can tail the JSONL. */
|
|
49
77
|
startSession(projectId: string, folderPath: string, cliTool?: CliTool): void;
|
|
50
|
-
private pruneOldSessions;
|
|
51
78
|
/** Stop the session poller for a project (public for cleanup on terminal stop) */
|
|
52
79
|
stopWatcherForProject(projectId: string): void;
|
|
53
80
|
private stopWatcher;
|
|
@@ -55,33 +82,19 @@ declare class SessionManager extends EventEmitter {
|
|
|
55
82
|
* Updates semantic status directly from env var — does NOT read JSONL
|
|
56
83
|
* (JSONL has not been written yet at this point). */
|
|
57
84
|
handleHookPreTool(projectId: string, toolName: string): void;
|
|
58
|
-
/** Called by hooks route on PostToolUse/Stop.
|
|
59
|
-
*
|
|
85
|
+
/** Called by hooks route on PostToolUse/Stop. Discovers the latest JSONL
|
|
86
|
+
* file if not already cached, detects mid-session file switches (e.g.
|
|
87
|
+
* Claude --continue creating a fresh JSONL), then reads new content and
|
|
88
|
+
* emits block events to chat listeners. */
|
|
60
89
|
triggerRead(projectId: string): void;
|
|
61
90
|
/** Called by hooks route on Stop — clears semantic status before reading final text. */
|
|
62
91
|
clearSemanticStatus(projectId: string): void;
|
|
63
|
-
/** Find the newest session file created after startedAt in the tool's session dir */
|
|
64
|
-
private findJsonl;
|
|
65
92
|
/** Read new data from the session file and extract messages */
|
|
66
93
|
private readNewLines;
|
|
67
94
|
/** Read whole-file JSON session (Gemini etc.) — re-parse on each trigger, diff against last known state */
|
|
68
95
|
private readWholeFileSession;
|
|
69
96
|
/** Incremental JSONL reading (Claude, Codex) */
|
|
70
97
|
private readJsonlIncremental;
|
|
71
|
-
private appendMessages;
|
|
72
|
-
/** Overwrite all messages (for whole-file JSON tools that re-parse the entire session) */
|
|
73
|
-
private overwriteMessages;
|
|
74
|
-
/** Resolve folderPath for a project — from active watcher or project config */
|
|
75
|
-
private resolveFolderPath;
|
|
76
|
-
/** Read session files from a directory */
|
|
77
|
-
private readSessionsFromDir;
|
|
78
|
-
listSessions(projectId: string): (Omit<Session, 'messages'> & {
|
|
79
|
-
messageCount: number;
|
|
80
|
-
isCurrent: boolean;
|
|
81
|
-
})[];
|
|
82
|
-
/** Validate sessionId to prevent path traversal */
|
|
83
|
-
private isValidSessionId;
|
|
84
|
-
getSession(projectId: string, sessionId: string): Session | null;
|
|
85
98
|
}
|
|
86
99
|
export declare const sessionManager: SessionManager;
|
|
87
100
|
export {};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"session-manager.d.ts","sourceRoot":"","sources":["../src/session-manager.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"session-manager.d.ts","sourceRoot":"","sources":["../src/session-manager.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAKH,OAAO,EAAE,YAAY,EAAE,MAAM,QAAQ,CAAC;AAGtC,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,SAAS,CAAC;AAIvC,MAAM,WAAW,aAAa;IAC5B,IAAI,EAAE,MAAM,GAAG,UAAU,GAAG,UAAU,GAAG,aAAa,CAAC;IACvD,OAAO,EAAE,MAAM,CAAC;CACjB;AAED,MAAM,WAAW,SAAS;IACxB;;;2BAGuB;IACvB,EAAE,CAAC,EAAE,MAAM,CAAC;IACZ,IAAI,EAAE,MAAM,GAAG,WAAW,CAAC;IAC3B,SAAS,EAAE,MAAM,CAAC;IAClB,MAAM,EAAE,aAAa,EAAE,CAAC;CACzB;AAWD,MAAM,MAAM,aAAa,GAAG,UAAU,GAAG,UAAU,GAAG,aAAa,GAAG,MAAM,CAAC;AAE7E,MAAM,WAAW,cAAc;IAC7B,KAAK,EAAE,aAAa,CAAC;IACrB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,SAAS,EAAE,MAAM,CAAC;CACnB;AAeD,cAAM,cAAe,SAAQ,YAAY;IACvC,OAAO,CAAC,QAAQ,CAAiC;IACjD,OAAO,CAAC,aAAa,CAAoD;IACzE,OAAO,CAAC,cAAc,CAAqC;;IAM3D,iBAAiB,CAAC,SAAS,EAAE,MAAM,GAAG,cAAc,GAAG,IAAI;IAI3D,oBAAoB,IAAI,MAAM,CAAC,MAAM,EAAE,cAAc,CAAC;IAQtD,0EAA0E;IAC1E,YAAY,CAAC,SAAS,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI;IAI9C;;;;;;;;;;;;;;OAcG;IACH,cAAc,CAAC,SAAS,EAAE,MAAM,GAAG,SAAS,EAAE;IA0B9C,uEAAuE;IACvE,OAAO,CAAC,cAAc;IA2BtB;;;;;;;;;;;;;;OAcG;IACH,OAAO,CAAC,yBAAyB;IA6BjC,oBAAoB,CAAC,SAAS,EAAE,MAAM,EAAE,EAAE,EAAE,CAAC,GAAG,EAAE,SAAS,KAAK,IAAI,GAAG,IAAI;IAK3E,sBAAsB,CAAC,SAAS,EAAE,MAAM,EAAE,EAAE,EAAE,CAAC,GAAG,EAAE,SAAS,KAAK,IAAI,GAAG,IAAI;IAO7E;2EACuE;IACvE,YAAY,CAAC,SAAS,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,EAAE,OAAO,GAAE,OAAkB,GAAG,IAAI;IAYtF,kFAAkF;IAClF,qBAAqB,CAAC,SAAS,EAAE,MAAM,GAAG,IAAI;IAI9C,OAAO,CAAC,WAAW;IAKnB;;0DAEsD;IACtD,iBAAiB,CAAC,SAAS,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,IAAI;IAU5D;;;gDAG4C;IAC5C,WAAW,CAAC,SAAS,EAAE,MAAM,GAAG,IAAI;IA2CpC,wFAAwF;IACxF,mBAAmB,CAAC,SAAS,EAAE,MAAM,GAAG,IAAI;IAM5C,+DAA+D;IAC/D,OAAO,CAAC,YAAY;IAepB,2GAA2G;IAC3G,OAAO,CAAC,oBAAoB;IAmC5B,gDAAgD;IAChD,OAAO,CAAC,oBAAoB;CAgD7B;AAED,eAAO,MAAM,cAAc,gBAAuB,CAAC"}
|
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
/**
|
|
3
|
-
* SessionManager — reads conversation history directly from
|
|
4
|
-
* native JSONL files
|
|
3
|
+
* SessionManager — reads conversation history directly from the CLI's
|
|
4
|
+
* native JSONL files (e.g. ~/.claude/projects/{encoded-path}/{uuid}.jsonl).
|
|
5
5
|
*
|
|
6
|
-
* No PTY parsing, no ANSI stripping, no heuristics
|
|
6
|
+
* No PTY parsing, no ANSI stripping, no heuristics. The JSONL is the single
|
|
7
|
+
* source of truth; ccweb maintains no separate conversation store.
|
|
7
8
|
*/
|
|
8
9
|
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
9
10
|
if (k2 === undefined) k2 = k;
|
|
@@ -42,25 +43,15 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
42
43
|
exports.sessionManager = void 0;
|
|
43
44
|
const fs = __importStar(require("fs"));
|
|
44
45
|
const path = __importStar(require("path"));
|
|
46
|
+
const crypto = __importStar(require("crypto"));
|
|
45
47
|
const events_1 = require("events");
|
|
46
|
-
const uuid_1 = require("uuid");
|
|
47
48
|
const config_1 = require("./config");
|
|
48
49
|
const adapters_1 = require("./adapters");
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
function
|
|
53
|
-
return (0
|
|
54
|
-
}
|
|
55
|
-
function projectSessionFile(folderPath, sessionId) {
|
|
56
|
-
return path.join(projectSessionsDir(folderPath), `${sessionId}.json`);
|
|
57
|
-
}
|
|
58
|
-
/** Legacy location: data/sessions/{projectId}/ */
|
|
59
|
-
function legacySessionsDir(projectId) {
|
|
60
|
-
return path.join(LEGACY_SESSIONS_DIR, projectId);
|
|
61
|
-
}
|
|
62
|
-
function legacySessionFile(projectId, sessionId) {
|
|
63
|
-
return path.join(legacySessionsDir(projectId), `${sessionId}.json`);
|
|
50
|
+
/** Generate a stable 16-hex-char id for a chat block.
|
|
51
|
+
* `source` is the original JSONL line for line-based tools (Claude/Codex),
|
|
52
|
+
* or `timestamp + JSON.stringify(blocks)` for whole-file tools (Gemini). */
|
|
53
|
+
function makeBlockId(jsonlPath, source) {
|
|
54
|
+
return crypto.createHash('sha1').update(jsonlPath + '\0' + source).digest('hex').slice(0, 16);
|
|
64
55
|
}
|
|
65
56
|
// ── SessionManager ────────────────────────────────────────────────────────────
|
|
66
57
|
class SessionManager extends events_1.EventEmitter {
|
|
@@ -84,17 +75,57 @@ class SessionManager extends events_1.EventEmitter {
|
|
|
84
75
|
getJsonlPath(projectId) {
|
|
85
76
|
return this.watchers.get(projectId)?.jsonlPath ?? null;
|
|
86
77
|
}
|
|
87
|
-
/** Return all parsed ChatBlocks from the
|
|
78
|
+
/** Return all parsed ChatBlocks from the project's latest JSONL file.
|
|
79
|
+
*
|
|
80
|
+
* Resolution order:
|
|
81
|
+
* 1. Active watcher already has `jsonlPath` discovered via hooks → use it.
|
|
82
|
+
* 2. Watcher exists but hasn't discovered the file yet → lazily scan the
|
|
83
|
+
* adapter's session dir, pick the latest JSONL, cache on the watcher.
|
|
84
|
+
* 3. No watcher (stopped project, user just navigated in) → resolve
|
|
85
|
+
* project config on-the-fly and pick the latest JSONL. Do NOT
|
|
86
|
+
* persist to `this.watchers` because a watcher without a PTY
|
|
87
|
+
* would drift (no hooks, no triggerRead).
|
|
88
|
+
*
|
|
89
|
+
* This indirection exists because chat history has to be loadable without
|
|
90
|
+
* a hook ever firing (e.g. immediately after ccweb restart — triggerRead
|
|
91
|
+
* only runs when Claude Code's hooks call `/api/hooks`).
|
|
92
|
+
*/
|
|
88
93
|
getChatHistory(projectId) {
|
|
89
94
|
const state = this.watchers.get(projectId);
|
|
90
|
-
if (
|
|
95
|
+
if (state) {
|
|
96
|
+
if (!state.jsonlPath) {
|
|
97
|
+
state.jsonlPath = this.findLatestJsonlForProject(state.folderPath, state.cliTool);
|
|
98
|
+
}
|
|
99
|
+
if (!state.jsonlPath)
|
|
100
|
+
return [];
|
|
101
|
+
return this.parseJsonlFile(state.jsonlPath, state.cliTool);
|
|
102
|
+
}
|
|
103
|
+
// No active watcher — fall back to project config for stopped projects
|
|
104
|
+
const project = (0, config_1.getProject)(projectId);
|
|
105
|
+
if (!project) {
|
|
106
|
+
console.warn(`[SessionManager] getChatHistory(${projectId}): project not in registry`);
|
|
91
107
|
return [];
|
|
92
|
-
|
|
108
|
+
}
|
|
109
|
+
const cliTool = project.cliTool ?? 'claude';
|
|
110
|
+
const jsonlPath = this.findLatestJsonlForProject(project.folderPath, cliTool);
|
|
111
|
+
if (!jsonlPath) {
|
|
112
|
+
console.warn(`[SessionManager] getChatHistory(${projectId}): no JSONL found for ${project.folderPath} (${cliTool})`);
|
|
113
|
+
return [];
|
|
114
|
+
}
|
|
115
|
+
return this.parseJsonlFile(jsonlPath, cliTool);
|
|
116
|
+
}
|
|
117
|
+
/** Parse a JSONL/JSON session file into ChatBlocks with stable ids. */
|
|
118
|
+
parseJsonlFile(jsonlPath, cliTool) {
|
|
119
|
+
const adapter = (0, adapters_1.getAdapter)(cliTool);
|
|
93
120
|
try {
|
|
94
|
-
const content = fs.readFileSync(
|
|
121
|
+
const content = fs.readFileSync(jsonlPath, 'utf-8');
|
|
95
122
|
// Whole-file JSON tools (e.g. Gemini): parse entire file at once
|
|
96
123
|
if (typeof adapter.parseSessionFile === 'function') {
|
|
97
|
-
|
|
124
|
+
const blocks = adapter.parseSessionFile(content);
|
|
125
|
+
return blocks.map((b) => ({
|
|
126
|
+
...b,
|
|
127
|
+
id: b.id ?? makeBlockId(jsonlPath, b.timestamp + '|' + JSON.stringify(b.blocks)),
|
|
128
|
+
}));
|
|
98
129
|
}
|
|
99
130
|
// JSONL tools (Claude, Codex): parse line by line
|
|
100
131
|
const lines = content.split('\n').filter((l) => l.trim());
|
|
@@ -102,7 +133,7 @@ class SessionManager extends events_1.EventEmitter {
|
|
|
102
133
|
for (const line of lines) {
|
|
103
134
|
const block = adapter.parseLineBlocks(line);
|
|
104
135
|
if (block)
|
|
105
|
-
blocks.push(block);
|
|
136
|
+
blocks.push({ ...block, id: makeBlockId(jsonlPath, line) });
|
|
106
137
|
}
|
|
107
138
|
return blocks;
|
|
108
139
|
}
|
|
@@ -110,6 +141,53 @@ class SessionManager extends events_1.EventEmitter {
|
|
|
110
141
|
return [];
|
|
111
142
|
}
|
|
112
143
|
}
|
|
144
|
+
/** Find the latest JSONL file for a project (single source of truth for
|
|
145
|
+
* both chat-history HTTP and hook-driven tail paths).
|
|
146
|
+
*
|
|
147
|
+
* Strategy: pick the newest file (by mtime) in the adapter's session dir.
|
|
148
|
+
* For adapters that store sessions in a shared tree (e.g. Codex's
|
|
149
|
+
* date-partitioned dir), use `getSessionFilesForProject` to scope by cwd.
|
|
150
|
+
*
|
|
151
|
+
* Deliberately has NO startedAt/recency filter — historical requirement
|
|
152
|
+
* was for "the file THIS session is writing", but that caused HTTP and
|
|
153
|
+
* WS paths to disagree on which file to read, breaking block-id dedup.
|
|
154
|
+
* Using "newest" works because Claude --continue updates the mtime of
|
|
155
|
+
* the continued file, and any new file it creates has the highest mtime.
|
|
156
|
+
* The caller (triggerRead) handles mid-session file switches by re-
|
|
157
|
+
* checking on each call.
|
|
158
|
+
*/
|
|
159
|
+
findLatestJsonlForProject(folderPath, cliTool) {
|
|
160
|
+
const adapter = (0, adapters_1.getAdapter)(cliTool);
|
|
161
|
+
if (typeof adapter.getSessionFilesForProject === 'function') {
|
|
162
|
+
const files = adapter.getSessionFilesForProject(folderPath);
|
|
163
|
+
if (files.length === 0)
|
|
164
|
+
return null;
|
|
165
|
+
try {
|
|
166
|
+
return files
|
|
167
|
+
.map((f) => ({ f, mtime: fs.statSync(f).mtimeMs }))
|
|
168
|
+
.sort((a, b) => b.mtime - a.mtime)[0].f;
|
|
169
|
+
}
|
|
170
|
+
catch {
|
|
171
|
+
return null;
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
const dir = adapter.getSessionDir(folderPath);
|
|
175
|
+
if (!dir || !fs.existsSync(dir))
|
|
176
|
+
return null;
|
|
177
|
+
const ext = typeof adapter.getSessionFileExtension === 'function'
|
|
178
|
+
? adapter.getSessionFileExtension()
|
|
179
|
+
: '.jsonl';
|
|
180
|
+
try {
|
|
181
|
+
const files = fs.readdirSync(dir)
|
|
182
|
+
.filter((f) => f.endsWith(ext))
|
|
183
|
+
.map((f) => ({ f, mtime: fs.statSync(path.join(dir, f)).mtimeMs }))
|
|
184
|
+
.sort((a, b) => b.mtime - a.mtime);
|
|
185
|
+
return files.length > 0 ? path.join(dir, files[0].f) : null;
|
|
186
|
+
}
|
|
187
|
+
catch {
|
|
188
|
+
return null;
|
|
189
|
+
}
|
|
190
|
+
}
|
|
113
191
|
registerChatListener(projectId, cb) {
|
|
114
192
|
if (!this.chatListeners.has(projectId))
|
|
115
193
|
this.chatListeners.set(projectId, new Set());
|
|
@@ -123,51 +201,18 @@ class SessionManager extends events_1.EventEmitter {
|
|
|
123
201
|
if (listeners.size === 0)
|
|
124
202
|
this.chatListeners.delete(projectId);
|
|
125
203
|
}
|
|
126
|
-
/** Call when a new PTY starts for a project
|
|
204
|
+
/** Call when a new PTY starts for a project. Registers a watcher so that
|
|
205
|
+
* subsequent hook-driven `triggerRead()` calls can tail the JSONL. */
|
|
127
206
|
startSession(projectId, folderPath, cliTool = 'claude') {
|
|
128
|
-
// Stop any previous watcher
|
|
129
207
|
this.stopWatcher(projectId);
|
|
130
|
-
|
|
131
|
-
const startedAt = Date.now();
|
|
132
|
-
// Create our session file in .ccweb/sessions/
|
|
133
|
-
fs.mkdirSync(projectSessionsDir(folderPath), { recursive: true });
|
|
134
|
-
const session = { id: sessionId, projectId, startedAt: new Date(startedAt).toISOString(), messages: [] };
|
|
135
|
-
fs.writeFileSync(projectSessionFile(folderPath, sessionId), JSON.stringify(session, null, 2), 'utf-8');
|
|
136
|
-
const state = {
|
|
137
|
-
sessionId,
|
|
208
|
+
this.watchers.set(projectId, {
|
|
138
209
|
folderPath,
|
|
139
210
|
cliTool,
|
|
140
211
|
jsonlPath: null,
|
|
141
212
|
fileOffset: 0,
|
|
142
|
-
startedAt,
|
|
143
|
-
};
|
|
144
|
-
|
|
145
|
-
// Prune old sessions (keep latest 20)
|
|
146
|
-
this.pruneOldSessions(folderPath, projectId);
|
|
147
|
-
console.log(`[SessionManager] Started session ${sessionId} for project ${projectId}`);
|
|
148
|
-
}
|
|
149
|
-
pruneOldSessions(folderPath, projectId, keep = 20) {
|
|
150
|
-
const dirs = [projectSessionsDir(folderPath), legacySessionsDir(projectId)];
|
|
151
|
-
for (const dir of dirs) {
|
|
152
|
-
if (!fs.existsSync(dir))
|
|
153
|
-
continue;
|
|
154
|
-
try {
|
|
155
|
-
const files = fs.readdirSync(dir)
|
|
156
|
-
.filter((f) => f.endsWith('.json'))
|
|
157
|
-
.sort(); // session filenames start with timestamp, so sort = chronological
|
|
158
|
-
if (files.length <= keep)
|
|
159
|
-
continue;
|
|
160
|
-
const toDelete = files.slice(0, files.length - keep);
|
|
161
|
-
for (const f of toDelete) {
|
|
162
|
-
try {
|
|
163
|
-
fs.unlinkSync(path.join(dir, f));
|
|
164
|
-
}
|
|
165
|
-
catch { /**/ }
|
|
166
|
-
}
|
|
167
|
-
console.log(`[SessionManager] Pruned ${toDelete.length} old sessions in ${dir}`);
|
|
168
|
-
}
|
|
169
|
-
catch { /**/ }
|
|
170
|
-
}
|
|
213
|
+
startedAt: Date.now(),
|
|
214
|
+
});
|
|
215
|
+
console.log(`[SessionManager] Started watcher for project ${projectId}`);
|
|
171
216
|
}
|
|
172
217
|
/** Stop the session poller for a project (public for cleanup on terminal stop) */
|
|
173
218
|
stopWatcherForProject(projectId) {
|
|
@@ -189,49 +234,55 @@ class SessionManager extends events_1.EventEmitter {
|
|
|
189
234
|
this.semanticStatus.set(projectId, newStatus);
|
|
190
235
|
this.emit('semantic', { projectId, status: newStatus });
|
|
191
236
|
}
|
|
192
|
-
/** Called by hooks route on PostToolUse/Stop.
|
|
193
|
-
*
|
|
237
|
+
/** Called by hooks route on PostToolUse/Stop. Discovers the latest JSONL
|
|
238
|
+
* file if not already cached, detects mid-session file switches (e.g.
|
|
239
|
+
* Claude --continue creating a fresh JSONL), then reads new content and
|
|
240
|
+
* emits block events to chat listeners. */
|
|
194
241
|
triggerRead(projectId) {
|
|
195
242
|
const state = this.watchers.get(projectId);
|
|
196
243
|
if (!state)
|
|
197
244
|
return;
|
|
198
|
-
//
|
|
245
|
+
// Re-check latest file each call: shares the single `findLatestJsonlForProject`
|
|
246
|
+
// path with getChatHistory so HTTP and WS paths always agree on which file
|
|
247
|
+
// is authoritative (→ consistent block ids → frontend dedup works).
|
|
248
|
+
const latest = this.findLatestJsonlForProject(state.folderPath, state.cliTool);
|
|
249
|
+
if (latest && latest !== state.jsonlPath) {
|
|
250
|
+
state.jsonlPath = latest;
|
|
251
|
+
state.fileOffset = 0;
|
|
252
|
+
}
|
|
199
253
|
if (!state.jsonlPath) {
|
|
200
|
-
|
|
201
|
-
if (
|
|
202
|
-
// Guard: skip if a retry chain is already running for this project
|
|
203
|
-
if (state.retrying)
|
|
204
|
-
return;
|
|
205
|
-
state.retrying = true;
|
|
206
|
-
const delays = [500, 1000, 2000];
|
|
207
|
-
const retry = (attempt) => {
|
|
208
|
-
setTimeout(() => {
|
|
209
|
-
const s = this.watchers.get(projectId);
|
|
210
|
-
if (!s)
|
|
211
|
-
return;
|
|
212
|
-
if (s.jsonlPath) {
|
|
213
|
-
s.retrying = false;
|
|
214
|
-
return;
|
|
215
|
-
}
|
|
216
|
-
s.jsonlPath = this.findJsonl(s.folderPath, s.startedAt, s.cliTool);
|
|
217
|
-
if (s.jsonlPath) {
|
|
218
|
-
s.retrying = false;
|
|
219
|
-
s.fileOffset = 0;
|
|
220
|
-
this.readNewLines(projectId, s);
|
|
221
|
-
}
|
|
222
|
-
else if (attempt + 1 < delays.length) {
|
|
223
|
-
retry(attempt + 1);
|
|
224
|
-
}
|
|
225
|
-
else {
|
|
226
|
-
s.retrying = false;
|
|
227
|
-
console.warn(`[SessionManager] JSONL file not found for project ${projectId} after ${delays.length} retries — chat history unavailable`);
|
|
228
|
-
}
|
|
229
|
-
}, delays[attempt]);
|
|
230
|
-
};
|
|
231
|
-
retry(0);
|
|
254
|
+
// Brand-new project or session dir not yet created — retry a few times
|
|
255
|
+
if (state.retrying)
|
|
232
256
|
return;
|
|
233
|
-
|
|
234
|
-
|
|
257
|
+
state.retrying = true;
|
|
258
|
+
const delays = [500, 1000, 2000];
|
|
259
|
+
const retry = (attempt) => {
|
|
260
|
+
setTimeout(() => {
|
|
261
|
+
const s = this.watchers.get(projectId);
|
|
262
|
+
if (!s)
|
|
263
|
+
return;
|
|
264
|
+
if (s.jsonlPath) {
|
|
265
|
+
s.retrying = false;
|
|
266
|
+
return;
|
|
267
|
+
}
|
|
268
|
+
const later = this.findLatestJsonlForProject(s.folderPath, s.cliTool);
|
|
269
|
+
if (later) {
|
|
270
|
+
s.retrying = false;
|
|
271
|
+
s.jsonlPath = later;
|
|
272
|
+
s.fileOffset = 0;
|
|
273
|
+
this.readNewLines(projectId, s);
|
|
274
|
+
}
|
|
275
|
+
else if (attempt + 1 < delays.length) {
|
|
276
|
+
retry(attempt + 1);
|
|
277
|
+
}
|
|
278
|
+
else {
|
|
279
|
+
s.retrying = false;
|
|
280
|
+
console.warn(`[SessionManager] JSONL file not found for project ${projectId} after ${delays.length} retries — chat history unavailable`);
|
|
281
|
+
}
|
|
282
|
+
}, delays[attempt]);
|
|
283
|
+
};
|
|
284
|
+
retry(0);
|
|
285
|
+
return;
|
|
235
286
|
}
|
|
236
287
|
this.readNewLines(projectId, state);
|
|
237
288
|
}
|
|
@@ -242,27 +293,6 @@ class SessionManager extends events_1.EventEmitter {
|
|
|
242
293
|
this.semanticStatus.delete(projectId);
|
|
243
294
|
this.emit('semantic', { projectId, status: null });
|
|
244
295
|
}
|
|
245
|
-
/** Find the newest session file created after startedAt in the tool's session dir */
|
|
246
|
-
findJsonl(folderPath, startedAt, cliTool = 'claude') {
|
|
247
|
-
const adapter = (0, adapters_1.getAdapter)(cliTool);
|
|
248
|
-
const dir = adapter.getSessionDir(folderPath);
|
|
249
|
-
if (!dir || !fs.existsSync(dir))
|
|
250
|
-
return null;
|
|
251
|
-
const ext = typeof adapter.getSessionFileExtension === 'function'
|
|
252
|
-
? adapter.getSessionFileExtension()
|
|
253
|
-
: '.jsonl';
|
|
254
|
-
try {
|
|
255
|
-
const files = fs.readdirSync(dir)
|
|
256
|
-
.filter((f) => f.endsWith(ext))
|
|
257
|
-
.map((f) => ({ f, mtime: fs.statSync(path.join(dir, f)).mtimeMs }))
|
|
258
|
-
.filter(({ mtime }) => mtime >= startedAt - 5000) // 5s grace
|
|
259
|
-
.sort((a, b) => b.mtime - a.mtime);
|
|
260
|
-
return files.length > 0 ? path.join(dir, files[0].f) : null;
|
|
261
|
-
}
|
|
262
|
-
catch {
|
|
263
|
-
return null;
|
|
264
|
-
}
|
|
265
|
-
}
|
|
266
296
|
/** Read new data from the session file and extract messages */
|
|
267
297
|
readNewLines(projectId, state) {
|
|
268
298
|
if (!state.jsonlPath)
|
|
@@ -287,22 +317,14 @@ class SessionManager extends events_1.EventEmitter {
|
|
|
287
317
|
const blocks = adapter.parseSessionFile(content);
|
|
288
318
|
if (blocks.length === 0)
|
|
289
319
|
return;
|
|
290
|
-
// Extract SessionMessages for our ccweb session file
|
|
291
|
-
const newMsgs = [];
|
|
292
|
-
for (const block of blocks) {
|
|
293
|
-
const text = block.blocks.filter(b => b.type === 'text').map(b => b.content).join('\n').trim();
|
|
294
|
-
if (text) {
|
|
295
|
-
newMsgs.push({ role: block.role, content: text, timestamp: block.timestamp });
|
|
296
|
-
}
|
|
297
|
-
}
|
|
298
|
-
if (newMsgs.length > 0) {
|
|
299
|
-
// Overwrite (not append) since we re-parsed the whole file
|
|
300
|
-
this.overwriteMessages(state.folderPath, state.sessionId, newMsgs);
|
|
301
|
-
}
|
|
302
320
|
// Emit latest blocks to chat listeners + update semantic status
|
|
303
321
|
for (const block of blocks) {
|
|
304
|
-
|
|
305
|
-
|
|
322
|
+
const blockWithId = {
|
|
323
|
+
...block,
|
|
324
|
+
id: block.id ?? makeBlockId(state.jsonlPath, block.timestamp + '|' + JSON.stringify(block.blocks)),
|
|
325
|
+
};
|
|
326
|
+
if (blockWithId.role === 'assistant' && blockWithId.blocks.length > 0) {
|
|
327
|
+
const lastBlock = blockWithId.blocks[blockWithId.blocks.length - 1];
|
|
306
328
|
const detail = lastBlock.type === 'tool_use' ? lastBlock.content.split('(')[0] : undefined;
|
|
307
329
|
const newStatus = { phase: lastBlock.type, detail, updatedAt: Date.now() };
|
|
308
330
|
this.semanticStatus.set(projectId, newStatus);
|
|
@@ -312,7 +334,7 @@ class SessionManager extends events_1.EventEmitter {
|
|
|
312
334
|
if (listeners) {
|
|
313
335
|
for (const cb of listeners) {
|
|
314
336
|
try {
|
|
315
|
-
cb(
|
|
337
|
+
cb(blockWithId);
|
|
316
338
|
}
|
|
317
339
|
catch { /**/ }
|
|
318
340
|
}
|
|
@@ -336,21 +358,11 @@ class SessionManager extends events_1.EventEmitter {
|
|
|
336
358
|
fs.readSync(fd, buf, 0, toRead, state.fileOffset);
|
|
337
359
|
state.fileOffset = stat.size;
|
|
338
360
|
const lines = buf.toString('utf-8').split('\n').filter((l) => l.trim());
|
|
339
|
-
let changed = false;
|
|
340
|
-
const newMsgs = [];
|
|
341
|
-
for (const line of lines) {
|
|
342
|
-
const msg = adapter.parseLine(line);
|
|
343
|
-
if (msg)
|
|
344
|
-
newMsgs.push(msg);
|
|
345
|
-
}
|
|
346
|
-
if (newMsgs.length > 0) {
|
|
347
|
-
this.appendMessages(state.folderPath, state.sessionId, newMsgs);
|
|
348
|
-
changed = true;
|
|
349
|
-
}
|
|
350
361
|
// Emit to chat listeners + update semantic status
|
|
351
362
|
for (const line of lines) {
|
|
352
|
-
const
|
|
353
|
-
if (
|
|
363
|
+
const parsed = adapter.parseLineBlocks(line);
|
|
364
|
+
if (parsed) {
|
|
365
|
+
const block = { ...parsed, id: makeBlockId(state.jsonlPath, line) };
|
|
354
366
|
if (block.role === 'assistant' && block.blocks.length > 0) {
|
|
355
367
|
const lastBlock = block.blocks[block.blocks.length - 1];
|
|
356
368
|
const detail = lastBlock.type === 'tool_use'
|
|
@@ -375,9 +387,6 @@ class SessionManager extends events_1.EventEmitter {
|
|
|
375
387
|
}
|
|
376
388
|
}
|
|
377
389
|
}
|
|
378
|
-
if (changed) {
|
|
379
|
-
console.log(`[SessionManager] Updated session ${state.sessionId}`);
|
|
380
|
-
}
|
|
381
390
|
}
|
|
382
391
|
catch {
|
|
383
392
|
// file may be temporarily locked or missing — try again next poll
|
|
@@ -390,116 +399,6 @@ class SessionManager extends events_1.EventEmitter {
|
|
|
390
399
|
catch { /**/ }
|
|
391
400
|
}
|
|
392
401
|
}
|
|
393
|
-
appendMessages(folderPath, sessionId, msgs) {
|
|
394
|
-
const file = projectSessionFile(folderPath, sessionId);
|
|
395
|
-
try {
|
|
396
|
-
const session = JSON.parse(fs.readFileSync(file, 'utf-8'));
|
|
397
|
-
session.messages.push(...msgs);
|
|
398
|
-
const tmpPath = file + `.tmp.${process.pid}`;
|
|
399
|
-
fs.writeFileSync(tmpPath, JSON.stringify(session, null, 2), 'utf-8');
|
|
400
|
-
fs.renameSync(tmpPath, file);
|
|
401
|
-
}
|
|
402
|
-
catch (err) {
|
|
403
|
-
console.error(`[SessionManager] Failed to append messages to session ${sessionId}:`, err);
|
|
404
|
-
}
|
|
405
|
-
}
|
|
406
|
-
/** Overwrite all messages (for whole-file JSON tools that re-parse the entire session) */
|
|
407
|
-
overwriteMessages(folderPath, sessionId, msgs) {
|
|
408
|
-
const file = projectSessionFile(folderPath, sessionId);
|
|
409
|
-
try {
|
|
410
|
-
const session = JSON.parse(fs.readFileSync(file, 'utf-8'));
|
|
411
|
-
session.messages = msgs;
|
|
412
|
-
const tmpPath = file + `.tmp.${process.pid}`;
|
|
413
|
-
fs.writeFileSync(tmpPath, JSON.stringify(session, null, 2), 'utf-8');
|
|
414
|
-
fs.renameSync(tmpPath, file);
|
|
415
|
-
}
|
|
416
|
-
catch (err) {
|
|
417
|
-
console.error(`[SessionManager] Failed to overwrite messages for session ${sessionId}:`, err);
|
|
418
|
-
}
|
|
419
|
-
}
|
|
420
|
-
// ── Query API ──────────────────────────────────────────────────────────────
|
|
421
|
-
/** Resolve folderPath for a project — from active watcher or project config */
|
|
422
|
-
resolveFolderPath(projectId) {
|
|
423
|
-
const watcher = this.watchers.get(projectId);
|
|
424
|
-
if (watcher)
|
|
425
|
-
return watcher.folderPath;
|
|
426
|
-
const project = (0, config_1.getProject)(projectId);
|
|
427
|
-
return project?.folderPath ?? null;
|
|
428
|
-
}
|
|
429
|
-
/** Read session files from a directory */
|
|
430
|
-
readSessionsFromDir(dir, currentId) {
|
|
431
|
-
if (!fs.existsSync(dir))
|
|
432
|
-
return [];
|
|
433
|
-
try {
|
|
434
|
-
return fs.readdirSync(dir)
|
|
435
|
-
.filter((f) => f.endsWith('.json'))
|
|
436
|
-
.map((f) => {
|
|
437
|
-
try {
|
|
438
|
-
const s = JSON.parse(fs.readFileSync(path.join(dir, f), 'utf-8'));
|
|
439
|
-
return {
|
|
440
|
-
id: s.id,
|
|
441
|
-
projectId: s.projectId,
|
|
442
|
-
startedAt: s.startedAt,
|
|
443
|
-
messageCount: s.messages.length,
|
|
444
|
-
isCurrent: s.id === currentId,
|
|
445
|
-
};
|
|
446
|
-
}
|
|
447
|
-
catch {
|
|
448
|
-
return null;
|
|
449
|
-
}
|
|
450
|
-
})
|
|
451
|
-
.filter((s) => s !== null);
|
|
452
|
-
}
|
|
453
|
-
catch {
|
|
454
|
-
return [];
|
|
455
|
-
}
|
|
456
|
-
}
|
|
457
|
-
listSessions(projectId) {
|
|
458
|
-
const currentId = this.watchers.get(projectId)?.sessionId;
|
|
459
|
-
const folderPath = this.resolveFolderPath(projectId);
|
|
460
|
-
// Collect from .ccweb/sessions/ (primary) and legacy data/sessions/ (fallback)
|
|
461
|
-
const results = [];
|
|
462
|
-
const seenIds = new Set();
|
|
463
|
-
if (folderPath) {
|
|
464
|
-
for (const s of this.readSessionsFromDir(projectSessionsDir(folderPath), currentId)) {
|
|
465
|
-
results.push(s);
|
|
466
|
-
seenIds.add(s.id);
|
|
467
|
-
}
|
|
468
|
-
}
|
|
469
|
-
// Legacy fallback — include sessions not already in .ccweb/
|
|
470
|
-
for (const s of this.readSessionsFromDir(legacySessionsDir(projectId), currentId)) {
|
|
471
|
-
if (!seenIds.has(s.id)) {
|
|
472
|
-
results.push(s);
|
|
473
|
-
}
|
|
474
|
-
}
|
|
475
|
-
return results.sort((a, b) => b.startedAt.localeCompare(a.startedAt));
|
|
476
|
-
}
|
|
477
|
-
/** Validate sessionId to prevent path traversal */
|
|
478
|
-
isValidSessionId(sessionId) {
|
|
479
|
-
return /^[\w-]+$/.test(sessionId) && !sessionId.includes('..');
|
|
480
|
-
}
|
|
481
|
-
getSession(projectId, sessionId) {
|
|
482
|
-
if (!this.isValidSessionId(sessionId))
|
|
483
|
-
return null;
|
|
484
|
-
const folderPath = this.resolveFolderPath(projectId);
|
|
485
|
-
// Try .ccweb/ first
|
|
486
|
-
if (folderPath) {
|
|
487
|
-
const file = projectSessionFile(folderPath, sessionId);
|
|
488
|
-
try {
|
|
489
|
-
if (fs.existsSync(file)) {
|
|
490
|
-
return JSON.parse(fs.readFileSync(file, 'utf-8'));
|
|
491
|
-
}
|
|
492
|
-
}
|
|
493
|
-
catch { /* fall through */ }
|
|
494
|
-
}
|
|
495
|
-
// Legacy fallback
|
|
496
|
-
try {
|
|
497
|
-
return JSON.parse(fs.readFileSync(legacySessionFile(projectId, sessionId), 'utf-8'));
|
|
498
|
-
}
|
|
499
|
-
catch {
|
|
500
|
-
return null;
|
|
501
|
-
}
|
|
502
|
-
}
|
|
503
402
|
}
|
|
504
403
|
exports.sessionManager = new SessionManager();
|
|
505
404
|
//# sourceMappingURL=session-manager.js.map
|