ccakashic 0.2.4 → 0.2.6
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 +6 -2
- package/dist/bin/ccakashic.js +248 -0
- package/dist/discover.js +230 -0
- package/dist/html-generator.js +554 -0
- package/{lib → dist}/pages.js +65 -80
- package/dist/parser.js +465 -0
- package/{lib → dist}/template-assets.js +6 -7
- package/package.json +14 -5
- package/bin/ccakashic.js +0 -225
- package/lib/discover.js +0 -221
- package/lib/html-generator.js +0 -586
- package/lib/parser.js +0 -496
package/README.md
CHANGED
|
@@ -1,10 +1,14 @@
|
|
|
1
|
+
<p align="center">
|
|
2
|
+
<img src="https://raw.githubusercontent.com/ashimon83/ccakashic/main/assets/logo.png" alt="ccakashic logo" width="160" />
|
|
3
|
+
</p>
|
|
4
|
+
|
|
1
5
|
# ccakashic
|
|
2
6
|
|
|
3
7
|
An Akashic Record of your Claude Code sessions — browse Claude Code session logs (`~/.claude/projects/`) as beautiful HTML in your browser.
|
|
4
8
|
|
|
5
|
-

|
|
9
|
+

|
|
6
10
|
|
|
7
|
-

|
|
11
|
+

|
|
8
12
|
|
|
9
13
|
## Usage
|
|
10
14
|
|
|
@@ -0,0 +1,248 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
"use strict";
|
|
3
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
4
|
+
if (k2 === undefined) k2 = k;
|
|
5
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
6
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
7
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
8
|
+
}
|
|
9
|
+
Object.defineProperty(o, k2, desc);
|
|
10
|
+
}) : (function(o, m, k, k2) {
|
|
11
|
+
if (k2 === undefined) k2 = k;
|
|
12
|
+
o[k2] = m[k];
|
|
13
|
+
}));
|
|
14
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
15
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
16
|
+
}) : function(o, v) {
|
|
17
|
+
o["default"] = v;
|
|
18
|
+
});
|
|
19
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
20
|
+
var ownKeys = function(o) {
|
|
21
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
22
|
+
var ar = [];
|
|
23
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
24
|
+
return ar;
|
|
25
|
+
};
|
|
26
|
+
return ownKeys(o);
|
|
27
|
+
};
|
|
28
|
+
return function (mod) {
|
|
29
|
+
if (mod && mod.__esModule) return mod;
|
|
30
|
+
var result = {};
|
|
31
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
32
|
+
__setModuleDefault(result, mod);
|
|
33
|
+
return result;
|
|
34
|
+
};
|
|
35
|
+
})();
|
|
36
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
37
|
+
const http = __importStar(require("http"));
|
|
38
|
+
const fs = __importStar(require("fs"));
|
|
39
|
+
const os = __importStar(require("os"));
|
|
40
|
+
const path = __importStar(require("path"));
|
|
41
|
+
const child_process_1 = require("child_process");
|
|
42
|
+
const discover_1 = require("../discover");
|
|
43
|
+
const parser_1 = require("../parser");
|
|
44
|
+
const html_generator_1 = require("../html-generator");
|
|
45
|
+
const pages_1 = require("../pages");
|
|
46
|
+
// Published at dist/bin/ccakashic.js, so ../../package.json resolves from dist/
|
|
47
|
+
const pkg = __importStar(require("../../package.json"));
|
|
48
|
+
function openInBrowser(url) {
|
|
49
|
+
const cmd = process.platform === 'darwin' ? 'open'
|
|
50
|
+
: process.platform === 'win32' ? 'start'
|
|
51
|
+
: 'xdg-open';
|
|
52
|
+
(0, child_process_1.exec)(`${cmd} "${url}"`);
|
|
53
|
+
}
|
|
54
|
+
const PORT = parseInt(process.env.CCAKASHIC_PORT || '') || 3333;
|
|
55
|
+
const MAX_PORT_TRIES = 20;
|
|
56
|
+
const LOCK_FILE = path.join(os.tmpdir(), `ccakashic-${os.userInfo().username || 'user'}.json`);
|
|
57
|
+
const server = http.createServer(async (req, res) => {
|
|
58
|
+
try {
|
|
59
|
+
const url = new URL(req.url || '/', `http://localhost`);
|
|
60
|
+
const pathname = url.pathname;
|
|
61
|
+
if (pathname === '/__ccakashic') {
|
|
62
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
63
|
+
res.end(JSON.stringify({ name: 'ccakashic', version: pkg.version }));
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
if (pathname === '/' || pathname === '') {
|
|
67
|
+
const projects = (0, discover_1.listProjects)();
|
|
68
|
+
res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
|
|
69
|
+
res.end((0, pages_1.generateIndex)(projects));
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
const projectMatch = pathname.match(/^\/project\/(.+)$/);
|
|
73
|
+
if (projectMatch && !pathname.includes('/session/')) {
|
|
74
|
+
const rawName = decodeURIComponent(projectMatch[1]);
|
|
75
|
+
const projects = (0, discover_1.listProjects)();
|
|
76
|
+
const project = projects.find((p) => p.rawName === rawName);
|
|
77
|
+
if (!project) {
|
|
78
|
+
res.writeHead(404, { 'Content-Type': 'text/plain' });
|
|
79
|
+
res.end('Project not found');
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
const sessions = await (0, discover_1.listSessions)(project.dir);
|
|
83
|
+
res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
|
|
84
|
+
res.end((0, pages_1.generateSessionList)(project, sessions));
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
const sessionMatch = pathname.match(/^\/project\/(.+)\/session\/(.+)$/);
|
|
88
|
+
if (sessionMatch) {
|
|
89
|
+
const rawName = decodeURIComponent(sessionMatch[1]);
|
|
90
|
+
const sessionId = decodeURIComponent(sessionMatch[2]);
|
|
91
|
+
const projects = (0, discover_1.listProjects)();
|
|
92
|
+
const project = projects.find((p) => p.rawName === rawName);
|
|
93
|
+
if (!project) {
|
|
94
|
+
res.writeHead(404, { 'Content-Type': 'text/plain' });
|
|
95
|
+
res.end('Project not found');
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
98
|
+
const sessionPath = path.join(project.dir, `${sessionId}.jsonl`);
|
|
99
|
+
if (!fs.existsSync(sessionPath)) {
|
|
100
|
+
res.writeHead(404, { 'Content-Type': 'text/plain' });
|
|
101
|
+
res.end('Session not found');
|
|
102
|
+
return;
|
|
103
|
+
}
|
|
104
|
+
const sessions = await (0, discover_1.listSessions)(project.dir);
|
|
105
|
+
const session = sessions.find((s) => s.id === sessionId) || { id: sessionId, path: sessionPath };
|
|
106
|
+
const parsed = await (0, parser_1.parseSession)(sessionPath);
|
|
107
|
+
const html = (0, html_generator_1.generate)(parsed, { projectName: project.name, session, backUrl: `/project/${encodeURIComponent(rawName)}` });
|
|
108
|
+
res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
|
|
109
|
+
res.end(html);
|
|
110
|
+
return;
|
|
111
|
+
}
|
|
112
|
+
res.writeHead(404, { 'Content-Type': 'text/plain' });
|
|
113
|
+
res.end('Not found');
|
|
114
|
+
}
|
|
115
|
+
catch (err) {
|
|
116
|
+
console.error(err);
|
|
117
|
+
res.writeHead(500, { 'Content-Type': 'text/plain' });
|
|
118
|
+
res.end('Internal server error');
|
|
119
|
+
}
|
|
120
|
+
});
|
|
121
|
+
async function buildOpenUrl(baseUrl) {
|
|
122
|
+
try {
|
|
123
|
+
const match = await (0, discover_1.findSessionForCwd)(process.cwd());
|
|
124
|
+
if (match) {
|
|
125
|
+
console.log(`Detected session for ${process.cwd()} → opening at bottom`);
|
|
126
|
+
return `${baseUrl}/project/${encodeURIComponent(match.projectRawName)}/session/${encodeURIComponent(match.sessionId)}#session-bottom`;
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
catch (err) {
|
|
130
|
+
console.error('Failed to auto-detect session:', err?.message);
|
|
131
|
+
}
|
|
132
|
+
return baseUrl;
|
|
133
|
+
}
|
|
134
|
+
function probeCcakashic(port) {
|
|
135
|
+
return new Promise((resolve) => {
|
|
136
|
+
const req = http.request({
|
|
137
|
+
host: '127.0.0.1',
|
|
138
|
+
port,
|
|
139
|
+
path: '/__ccakashic',
|
|
140
|
+
method: 'GET',
|
|
141
|
+
timeout: 500,
|
|
142
|
+
}, (res) => {
|
|
143
|
+
let data = '';
|
|
144
|
+
res.on('data', (chunk) => { data += chunk; });
|
|
145
|
+
res.on('end', () => {
|
|
146
|
+
try {
|
|
147
|
+
const parsed = JSON.parse(data);
|
|
148
|
+
resolve(parsed && parsed.name === 'ccakashic');
|
|
149
|
+
}
|
|
150
|
+
catch {
|
|
151
|
+
resolve(false);
|
|
152
|
+
}
|
|
153
|
+
});
|
|
154
|
+
});
|
|
155
|
+
req.on('error', () => resolve(false));
|
|
156
|
+
req.on('timeout', () => { req.destroy(); resolve(false); });
|
|
157
|
+
req.end();
|
|
158
|
+
});
|
|
159
|
+
}
|
|
160
|
+
function readLockPort() {
|
|
161
|
+
try {
|
|
162
|
+
const data = JSON.parse(fs.readFileSync(LOCK_FILE, 'utf-8'));
|
|
163
|
+
return typeof data.port === 'number' ? data.port : null;
|
|
164
|
+
}
|
|
165
|
+
catch {
|
|
166
|
+
return null;
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
function writeLockFile(port) {
|
|
170
|
+
try {
|
|
171
|
+
fs.writeFileSync(LOCK_FILE, JSON.stringify({ port, pid: process.pid, startedAt: Date.now() }));
|
|
172
|
+
}
|
|
173
|
+
catch {
|
|
174
|
+
// best-effort
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
function cleanupLockFile() {
|
|
178
|
+
try {
|
|
179
|
+
fs.unlinkSync(LOCK_FILE);
|
|
180
|
+
}
|
|
181
|
+
catch { }
|
|
182
|
+
}
|
|
183
|
+
function listenOnPort(port) {
|
|
184
|
+
return new Promise((resolve, reject) => {
|
|
185
|
+
const onError = (err) => { server.off('listening', onListening); reject(err); };
|
|
186
|
+
const onListening = () => { server.off('error', onError); resolve(); };
|
|
187
|
+
server.once('error', onError);
|
|
188
|
+
server.once('listening', onListening);
|
|
189
|
+
server.listen(port, '127.0.0.1');
|
|
190
|
+
});
|
|
191
|
+
}
|
|
192
|
+
async function findExistingCcakashic(startPort) {
|
|
193
|
+
const lockPort = readLockPort();
|
|
194
|
+
if (lockPort && await probeCcakashic(lockPort))
|
|
195
|
+
return lockPort;
|
|
196
|
+
if (startPort !== lockPort && await probeCcakashic(startPort))
|
|
197
|
+
return startPort;
|
|
198
|
+
return null;
|
|
199
|
+
}
|
|
200
|
+
async function startServer(startPort) {
|
|
201
|
+
for (let i = 0; i < MAX_PORT_TRIES; i++) {
|
|
202
|
+
const port = startPort + i;
|
|
203
|
+
try {
|
|
204
|
+
await listenOnPort(port);
|
|
205
|
+
return port;
|
|
206
|
+
}
|
|
207
|
+
catch (err) {
|
|
208
|
+
if (err?.code !== 'EADDRINUSE')
|
|
209
|
+
throw err;
|
|
210
|
+
if (await probeCcakashic(port))
|
|
211
|
+
return -port;
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
throw new Error(`No available port after ${MAX_PORT_TRIES} tries starting at ${startPort}`);
|
|
215
|
+
}
|
|
216
|
+
async function main() {
|
|
217
|
+
const existing = await findExistingCcakashic(PORT);
|
|
218
|
+
if (existing) {
|
|
219
|
+
const url = `http://127.0.0.1:${existing}`;
|
|
220
|
+
console.log(`Reusing existing ccakashic at ${url}`);
|
|
221
|
+
writeLockFile(existing);
|
|
222
|
+
openInBrowser(await buildOpenUrl(url));
|
|
223
|
+
return;
|
|
224
|
+
}
|
|
225
|
+
const result = await startServer(PORT);
|
|
226
|
+
if (result < 0) {
|
|
227
|
+
const port = -result;
|
|
228
|
+
const url = `http://127.0.0.1:${port}`;
|
|
229
|
+
console.log(`Reusing existing ccakashic at ${url}`);
|
|
230
|
+
writeLockFile(port);
|
|
231
|
+
openInBrowser(await buildOpenUrl(url));
|
|
232
|
+
return;
|
|
233
|
+
}
|
|
234
|
+
const port = result;
|
|
235
|
+
const url = `http://127.0.0.1:${port}`;
|
|
236
|
+
console.log(`ccakashic running at ${url}`);
|
|
237
|
+
console.log('Press Ctrl+C to stop');
|
|
238
|
+
writeLockFile(port);
|
|
239
|
+
const cleanup = () => { cleanupLockFile(); process.exit(0); };
|
|
240
|
+
process.on('SIGINT', cleanup);
|
|
241
|
+
process.on('SIGTERM', cleanup);
|
|
242
|
+
process.on('exit', cleanupLockFile);
|
|
243
|
+
openInBrowser(await buildOpenUrl(url));
|
|
244
|
+
}
|
|
245
|
+
main().catch((err) => {
|
|
246
|
+
console.error(err);
|
|
247
|
+
process.exit(1);
|
|
248
|
+
});
|
package/dist/discover.js
ADDED
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.CLAUDE_DIR = void 0;
|
|
37
|
+
exports.decodeDirName = decodeDirName;
|
|
38
|
+
exports.listProjects = listProjects;
|
|
39
|
+
exports.listSessions = listSessions;
|
|
40
|
+
exports.findSessionForCwd = findSessionForCwd;
|
|
41
|
+
const fs = __importStar(require("fs"));
|
|
42
|
+
const os = __importStar(require("os"));
|
|
43
|
+
const path = __importStar(require("path"));
|
|
44
|
+
const readline = __importStar(require("readline"));
|
|
45
|
+
exports.CLAUDE_DIR = path.join(os.homedir(), '.claude', 'projects');
|
|
46
|
+
function decodeDirName(dirName) {
|
|
47
|
+
// Directory names encode paths: /Users/foo/bar → -Users-foo-bar
|
|
48
|
+
// This is lossy (dots become dashes too), but good enough for display
|
|
49
|
+
if (dirName.startsWith('-')) {
|
|
50
|
+
return '/' + dirName.slice(1).replace(/-/g, '/');
|
|
51
|
+
}
|
|
52
|
+
return dirName;
|
|
53
|
+
}
|
|
54
|
+
function listProjects() {
|
|
55
|
+
if (!fs.existsSync(exports.CLAUDE_DIR)) {
|
|
56
|
+
return [];
|
|
57
|
+
}
|
|
58
|
+
const entries = fs.readdirSync(exports.CLAUDE_DIR, { withFileTypes: true });
|
|
59
|
+
const projects = [];
|
|
60
|
+
for (const entry of entries) {
|
|
61
|
+
if (!entry.isDirectory())
|
|
62
|
+
continue;
|
|
63
|
+
const projectDir = path.join(exports.CLAUDE_DIR, entry.name);
|
|
64
|
+
const jsonlFiles = fs.readdirSync(projectDir).filter((f) => f.endsWith('.jsonl'));
|
|
65
|
+
if (jsonlFiles.length === 0)
|
|
66
|
+
continue;
|
|
67
|
+
let lastModified = 0;
|
|
68
|
+
for (const f of jsonlFiles) {
|
|
69
|
+
const stat = fs.statSync(path.join(projectDir, f));
|
|
70
|
+
if (stat.mtimeMs > lastModified)
|
|
71
|
+
lastModified = stat.mtimeMs;
|
|
72
|
+
}
|
|
73
|
+
projects.push({
|
|
74
|
+
name: decodeDirName(entry.name),
|
|
75
|
+
rawName: entry.name,
|
|
76
|
+
dir: projectDir,
|
|
77
|
+
sessionCount: jsonlFiles.length,
|
|
78
|
+
lastModified: new Date(lastModified),
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
projects.sort((a, b) => b.lastModified.getTime() - a.lastModified.getTime());
|
|
82
|
+
return projects;
|
|
83
|
+
}
|
|
84
|
+
async function getSessionPreview(filePath) {
|
|
85
|
+
return new Promise((resolve) => {
|
|
86
|
+
const result = {
|
|
87
|
+
id: path.basename(filePath, '.jsonl'),
|
|
88
|
+
path: filePath,
|
|
89
|
+
timestamp: null,
|
|
90
|
+
lastModified: fs.statSync(filePath).mtimeMs,
|
|
91
|
+
preview: '',
|
|
92
|
+
gitBranch: null,
|
|
93
|
+
slug: null,
|
|
94
|
+
model: null,
|
|
95
|
+
hasSubagents: false,
|
|
96
|
+
totalTokens: 0,
|
|
97
|
+
outputTokens: 0,
|
|
98
|
+
};
|
|
99
|
+
const sessionDir = path.join(path.dirname(filePath), result.id);
|
|
100
|
+
if (fs.existsSync(path.join(sessionDir, 'subagents'))) {
|
|
101
|
+
result.hasSubagents = true;
|
|
102
|
+
}
|
|
103
|
+
const rl = readline.createInterface({
|
|
104
|
+
input: fs.createReadStream(filePath, { encoding: 'utf-8' }),
|
|
105
|
+
crlfDelay: Infinity,
|
|
106
|
+
});
|
|
107
|
+
let foundPreview = false;
|
|
108
|
+
rl.on('line', (line) => {
|
|
109
|
+
try {
|
|
110
|
+
const obj = JSON.parse(line);
|
|
111
|
+
if (!result.timestamp && obj.timestamp) {
|
|
112
|
+
result.timestamp = obj.timestamp;
|
|
113
|
+
}
|
|
114
|
+
if (!result.gitBranch && obj.gitBranch) {
|
|
115
|
+
result.gitBranch = obj.gitBranch;
|
|
116
|
+
}
|
|
117
|
+
if (!result.slug && obj.slug) {
|
|
118
|
+
result.slug = obj.slug;
|
|
119
|
+
}
|
|
120
|
+
if (!result.model && obj.type === 'assistant' && obj.message?.model) {
|
|
121
|
+
result.model = obj.message.model;
|
|
122
|
+
}
|
|
123
|
+
if (obj.type === 'assistant' && obj.message?.usage) {
|
|
124
|
+
const u = obj.message.usage;
|
|
125
|
+
result.totalTokens += (u.input_tokens || 0) + (u.output_tokens || 0)
|
|
126
|
+
+ (u.cache_creation_input_tokens || 0) + (u.cache_read_input_tokens || 0);
|
|
127
|
+
result.outputTokens += u.output_tokens || 0;
|
|
128
|
+
}
|
|
129
|
+
if (!foundPreview && obj.type === 'user' && obj.message) {
|
|
130
|
+
const content = obj.message.content;
|
|
131
|
+
let text = '';
|
|
132
|
+
if (typeof content === 'string') {
|
|
133
|
+
text = content;
|
|
134
|
+
}
|
|
135
|
+
else if (Array.isArray(content)) {
|
|
136
|
+
for (const block of content) {
|
|
137
|
+
if (block.type === 'text' && block.text) {
|
|
138
|
+
text = block.text;
|
|
139
|
+
break;
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
if (text) {
|
|
144
|
+
result.preview = text.replace(/\n/g, ' ').slice(0, 100);
|
|
145
|
+
foundPreview = true;
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
catch {
|
|
150
|
+
// skip malformed lines
|
|
151
|
+
}
|
|
152
|
+
});
|
|
153
|
+
rl.on('close', () => resolve(result));
|
|
154
|
+
rl.on('error', () => resolve(result));
|
|
155
|
+
});
|
|
156
|
+
}
|
|
157
|
+
async function listSessions(projectDir) {
|
|
158
|
+
const jsonlFiles = fs
|
|
159
|
+
.readdirSync(projectDir)
|
|
160
|
+
.filter((f) => f.endsWith('.jsonl'))
|
|
161
|
+
.map((f) => path.join(projectDir, f));
|
|
162
|
+
const sessions = await Promise.all(jsonlFiles.map(getSessionPreview));
|
|
163
|
+
sessions.sort((a, b) => b.lastModified - a.lastModified);
|
|
164
|
+
return sessions;
|
|
165
|
+
}
|
|
166
|
+
function readCwdFromSession(filePath) {
|
|
167
|
+
return new Promise((resolve) => {
|
|
168
|
+
const rl = readline.createInterface({
|
|
169
|
+
input: fs.createReadStream(filePath, { encoding: 'utf-8' }),
|
|
170
|
+
crlfDelay: Infinity,
|
|
171
|
+
});
|
|
172
|
+
let found = null;
|
|
173
|
+
rl.on('line', (line) => {
|
|
174
|
+
if (found)
|
|
175
|
+
return;
|
|
176
|
+
try {
|
|
177
|
+
const obj = JSON.parse(line);
|
|
178
|
+
if (obj.cwd) {
|
|
179
|
+
found = obj.cwd;
|
|
180
|
+
rl.close();
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
catch {
|
|
184
|
+
// skip
|
|
185
|
+
}
|
|
186
|
+
});
|
|
187
|
+
rl.on('close', () => resolve(found));
|
|
188
|
+
rl.on('error', () => resolve(found));
|
|
189
|
+
});
|
|
190
|
+
}
|
|
191
|
+
function latestSessionFile(projectDir) {
|
|
192
|
+
const files = fs.readdirSync(projectDir).filter((f) => f.endsWith('.jsonl'));
|
|
193
|
+
if (!files.length)
|
|
194
|
+
return null;
|
|
195
|
+
let best = null;
|
|
196
|
+
let bestMtime = 0;
|
|
197
|
+
for (const f of files) {
|
|
198
|
+
const mtime = fs.statSync(path.join(projectDir, f)).mtimeMs;
|
|
199
|
+
if (mtime > bestMtime) {
|
|
200
|
+
bestMtime = mtime;
|
|
201
|
+
best = f;
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
return best ? { file: best, mtimeMs: bestMtime } : null;
|
|
205
|
+
}
|
|
206
|
+
async function findSessionForCwd(cwd) {
|
|
207
|
+
// Pick the project whose cwd is the most specific (longest) match for the
|
|
208
|
+
// given cwd. A brand-new session in a specific subdir should win over an
|
|
209
|
+
// older, more-active ancestor project.
|
|
210
|
+
const projects = listProjects();
|
|
211
|
+
let bestMatch = null;
|
|
212
|
+
let bestLen = -1;
|
|
213
|
+
for (const project of projects) {
|
|
214
|
+
const latest = latestSessionFile(project.dir);
|
|
215
|
+
if (!latest)
|
|
216
|
+
continue;
|
|
217
|
+
const projectCwd = await readCwdFromSession(path.join(project.dir, latest.file));
|
|
218
|
+
if (!projectCwd)
|
|
219
|
+
continue;
|
|
220
|
+
const isMatch = cwd === projectCwd || cwd.startsWith(projectCwd + path.sep);
|
|
221
|
+
if (isMatch && projectCwd.length > bestLen) {
|
|
222
|
+
bestMatch = {
|
|
223
|
+
projectRawName: project.rawName,
|
|
224
|
+
sessionId: path.basename(latest.file, '.jsonl'),
|
|
225
|
+
};
|
|
226
|
+
bestLen = projectCwd.length;
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
return bestMatch;
|
|
230
|
+
}
|