metame-cli 1.4.12 ā 1.4.15
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 +9 -9
- package/index.js +205 -57
- package/package.json +2 -2
- package/scripts/daemon-admin-commands.js +365 -0
- package/scripts/daemon-agent-commands.js +491 -0
- package/scripts/daemon-agent-tools.js +256 -0
- package/scripts/daemon-bridges.js +236 -0
- package/scripts/daemon-checkpoints.js +89 -0
- package/scripts/daemon-claude-engine.js +909 -0
- package/scripts/daemon-command-router.js +416 -0
- package/scripts/daemon-default.yaml +2 -2
- package/scripts/daemon-exec-commands.js +290 -0
- package/scripts/daemon-file-browser.js +219 -0
- package/scripts/daemon-notify.js +64 -0
- package/scripts/daemon-ops-commands.js +275 -0
- package/scripts/daemon-runtime-lifecycle.js +133 -0
- package/scripts/daemon-session-commands.js +436 -0
- package/scripts/daemon-session-store.js +423 -0
- package/scripts/daemon-task-scheduler.js +539 -0
- package/scripts/daemon.js +555 -4316
- package/scripts/memory-extract.js +15 -9
- package/scripts/session-analytics.js +116 -0
- package/scripts/test_daemon.js +1407 -0
|
@@ -0,0 +1,423 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const crypto = require('crypto');
|
|
4
|
+
|
|
5
|
+
function createSessionStore(deps) {
|
|
6
|
+
const {
|
|
7
|
+
fs,
|
|
8
|
+
path,
|
|
9
|
+
HOME,
|
|
10
|
+
loadState,
|
|
11
|
+
saveState,
|
|
12
|
+
log,
|
|
13
|
+
formatRelativeTime,
|
|
14
|
+
cpExtractTimestamp,
|
|
15
|
+
} = deps;
|
|
16
|
+
|
|
17
|
+
const CLAUDE_PROJECTS_DIR = path.join(HOME, '.claude', 'projects');
|
|
18
|
+
const _sessionFileCache = new Map(); // sessionId -> { path, ts }
|
|
19
|
+
let _sessionCache = null;
|
|
20
|
+
let _sessionCacheTime = 0;
|
|
21
|
+
const SESSION_CACHE_TTL = 10000; // 10s
|
|
22
|
+
|
|
23
|
+
function findSessionFile(sessionId) {
|
|
24
|
+
if (!sessionId || !fs.existsSync(CLAUDE_PROJECTS_DIR)) return null;
|
|
25
|
+
const cached = _sessionFileCache.get(sessionId);
|
|
26
|
+
if (cached && Date.now() - cached.ts < 30000) return cached.path;
|
|
27
|
+
const target = sessionId + '.jsonl';
|
|
28
|
+
try {
|
|
29
|
+
for (const proj of fs.readdirSync(CLAUDE_PROJECTS_DIR)) {
|
|
30
|
+
const candidate = path.join(CLAUDE_PROJECTS_DIR, proj, target);
|
|
31
|
+
if (fs.existsSync(candidate)) {
|
|
32
|
+
_sessionFileCache.set(sessionId, { path: candidate, ts: Date.now() });
|
|
33
|
+
return candidate;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
} catch { /* ignore */ }
|
|
37
|
+
_sessionFileCache.set(sessionId, { path: null, ts: Date.now() });
|
|
38
|
+
return null;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function clearSessionFileCache(sessionId) {
|
|
42
|
+
if (!sessionId) return;
|
|
43
|
+
_sessionFileCache.delete(sessionId);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function truncateSessionLastTurn(sessionId) {
|
|
47
|
+
try {
|
|
48
|
+
const sessionFile = findSessionFile(sessionId);
|
|
49
|
+
if (!sessionFile) return 0;
|
|
50
|
+
const fileContent = fs.readFileSync(sessionFile, 'utf8');
|
|
51
|
+
const lines = fileContent.split('\n').filter(l => l.trim());
|
|
52
|
+
let cutIdx = -1;
|
|
53
|
+
for (let i = lines.length - 1; i >= 0; i--) {
|
|
54
|
+
try {
|
|
55
|
+
const obj = JSON.parse(lines[i]);
|
|
56
|
+
if (obj.type === 'user') { cutIdx = i; break; }
|
|
57
|
+
} catch { /* skip malformed lines */ }
|
|
58
|
+
}
|
|
59
|
+
if (cutIdx <= 0) return 0;
|
|
60
|
+
const kept = lines.slice(0, cutIdx);
|
|
61
|
+
fs.writeFileSync(sessionFile, kept.join('\n') + '\n', 'utf8');
|
|
62
|
+
_sessionFileCache.delete(sessionId);
|
|
63
|
+
const removed = lines.length - kept.length;
|
|
64
|
+
log('INFO', `truncateSessionLastTurn: removed ${removed} lines from ${path.basename(sessionFile)}`);
|
|
65
|
+
return removed;
|
|
66
|
+
} catch (e) {
|
|
67
|
+
log('WARN', `truncateSessionLastTurn failed: ${e.message}`);
|
|
68
|
+
return 0;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function truncateSessionToCheckpoint(sessionId, checkpointMessage) {
|
|
73
|
+
try {
|
|
74
|
+
const cpTs = typeof cpExtractTimestamp === 'function' ? cpExtractTimestamp(checkpointMessage) : null;
|
|
75
|
+
const cpTime = cpTs ? new Date(cpTs).getTime() : 0;
|
|
76
|
+
if (!cpTime) return truncateSessionLastTurn(sessionId);
|
|
77
|
+
|
|
78
|
+
const sessionFile = findSessionFile(sessionId);
|
|
79
|
+
if (!sessionFile) return 0;
|
|
80
|
+
const fileContent = fs.readFileSync(sessionFile, 'utf8');
|
|
81
|
+
const lines = fileContent.split('\n').filter(l => l.trim());
|
|
82
|
+
|
|
83
|
+
let cutIdx = -1;
|
|
84
|
+
for (let i = 0; i < lines.length; i++) {
|
|
85
|
+
try {
|
|
86
|
+
const obj = JSON.parse(lines[i]);
|
|
87
|
+
if (obj.type === 'user' && obj.timestamp) {
|
|
88
|
+
const msgTime = new Date(obj.timestamp).getTime();
|
|
89
|
+
if (msgTime && msgTime >= cpTime) { cutIdx = i; break; }
|
|
90
|
+
}
|
|
91
|
+
} catch { /* skip malformed lines */ }
|
|
92
|
+
}
|
|
93
|
+
if (cutIdx <= 0) return truncateSessionLastTurn(sessionId);
|
|
94
|
+
|
|
95
|
+
const kept = lines.slice(0, cutIdx);
|
|
96
|
+
fs.writeFileSync(sessionFile, kept.join('\n') + '\n', 'utf8');
|
|
97
|
+
_sessionFileCache.delete(sessionId);
|
|
98
|
+
const removed = lines.length - kept.length;
|
|
99
|
+
log('INFO', `truncateSessionToCheckpoint: removed ${removed} lines from ${path.basename(sessionFile)}`);
|
|
100
|
+
return removed;
|
|
101
|
+
} catch (e) {
|
|
102
|
+
log('WARN', `truncateSessionToCheckpoint failed: ${e.message}`);
|
|
103
|
+
return truncateSessionLastTurn(sessionId);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function invalidateSessionCache() { _sessionCache = null; }
|
|
108
|
+
|
|
109
|
+
function scanAllSessions() {
|
|
110
|
+
if (_sessionCache && (Date.now() - _sessionCacheTime < SESSION_CACHE_TTL)) return _sessionCache;
|
|
111
|
+
try {
|
|
112
|
+
if (!fs.existsSync(CLAUDE_PROJECTS_DIR)) { _sessionCache = []; _sessionCacheTime = Date.now(); return []; }
|
|
113
|
+
const projects = fs.readdirSync(CLAUDE_PROJECTS_DIR);
|
|
114
|
+
const sessionMap = new Map();
|
|
115
|
+
const projPathCache = new Map();
|
|
116
|
+
|
|
117
|
+
for (const proj of projects) {
|
|
118
|
+
const projDir = path.join(CLAUDE_PROJECTS_DIR, proj);
|
|
119
|
+
const indexFile = path.join(projDir, 'sessions-index.json');
|
|
120
|
+
try {
|
|
121
|
+
if (fs.existsSync(indexFile)) {
|
|
122
|
+
const data = JSON.parse(fs.readFileSync(indexFile, 'utf8'));
|
|
123
|
+
if (data.entries && data.entries.length > 0) {
|
|
124
|
+
const realPath = data.entries[0].projectPath;
|
|
125
|
+
if (realPath) projPathCache.set(proj, realPath);
|
|
126
|
+
for (const entry of data.entries) {
|
|
127
|
+
if (entry.messageCount >= 1) sessionMap.set(entry.sessionId, entry);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
} catch { /* skip */ }
|
|
132
|
+
|
|
133
|
+
try {
|
|
134
|
+
const files = fs.readdirSync(projDir).filter(f => f.endsWith('.jsonl'));
|
|
135
|
+
for (const file of files) {
|
|
136
|
+
const sessionId = file.replace('.jsonl', '');
|
|
137
|
+
const filePath = path.join(projDir, file);
|
|
138
|
+
const stat = fs.statSync(filePath);
|
|
139
|
+
const fileMtime = stat.mtimeMs;
|
|
140
|
+
const existing = sessionMap.get(sessionId);
|
|
141
|
+
if (!existing || fileMtime > (existing.fileMtime || 0)) {
|
|
142
|
+
const projectPath = projPathCache.get(proj) || proj.slice(1).replace(/-/g, '/');
|
|
143
|
+
sessionMap.set(sessionId, {
|
|
144
|
+
sessionId, projectPath, fileMtime,
|
|
145
|
+
modified: new Date(fileMtime).toISOString(),
|
|
146
|
+
messageCount: 1,
|
|
147
|
+
...(existing || {}),
|
|
148
|
+
fileMtime,
|
|
149
|
+
});
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
} catch { /* skip */ }
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
const all = Array.from(sessionMap.values());
|
|
156
|
+
all.sort((a, b) => {
|
|
157
|
+
const aTime = a.fileMtime || new Date(a.modified).getTime();
|
|
158
|
+
const bTime = b.fileMtime || new Date(b.modified).getTime();
|
|
159
|
+
return bTime - aTime;
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
const ENRICH_LIMIT = 20;
|
|
163
|
+
for (let i = 0; i < Math.min(all.length, ENRICH_LIMIT); i++) {
|
|
164
|
+
const s = all[i];
|
|
165
|
+
if (s.firstPrompt && s.customTitle) continue;
|
|
166
|
+
try {
|
|
167
|
+
const sessionFile = findSessionFile(s.sessionId);
|
|
168
|
+
if (!sessionFile) continue;
|
|
169
|
+
const fd = fs.openSync(sessionFile, 'r');
|
|
170
|
+
const headBuf = Buffer.alloc(8192);
|
|
171
|
+
const headBytes = fs.readSync(fd, headBuf, 0, 8192, 0);
|
|
172
|
+
const headStr = headBuf.toString('utf8', 0, headBytes);
|
|
173
|
+
if (!s.firstPrompt) {
|
|
174
|
+
for (const line of headStr.split('\n')) {
|
|
175
|
+
if (!line) continue;
|
|
176
|
+
try {
|
|
177
|
+
const d = JSON.parse(line);
|
|
178
|
+
if (d.type === 'user' && d.message && d.userType === 'external') {
|
|
179
|
+
const content = d.message.content;
|
|
180
|
+
let raw = '';
|
|
181
|
+
if (typeof content === 'string') raw = content;
|
|
182
|
+
else if (Array.isArray(content)) {
|
|
183
|
+
const txt = content.find(c => c.type === 'text');
|
|
184
|
+
if (txt) raw = txt.text;
|
|
185
|
+
}
|
|
186
|
+
raw = raw.replace(/\n?\[System hints[\s\S]*/i, '').replace(/<system-reminder>[\s\S]*?<\/system-reminder>/g, '').trim();
|
|
187
|
+
if (raw && raw.length > 2) { s.firstPrompt = raw.slice(0, 120); break; }
|
|
188
|
+
}
|
|
189
|
+
} catch { /* skip line */ }
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
if (!s.customTitle) {
|
|
193
|
+
const stat = fs.fstatSync(fd);
|
|
194
|
+
const tailSize = Math.min(4096, stat.size);
|
|
195
|
+
const tailBuf = Buffer.alloc(tailSize);
|
|
196
|
+
fs.readSync(fd, tailBuf, 0, tailSize, stat.size - tailSize);
|
|
197
|
+
const tailStr = tailBuf.toString('utf8');
|
|
198
|
+
const tailLines = tailStr.split('\n').reverse();
|
|
199
|
+
for (const line of tailLines) {
|
|
200
|
+
if (!line) continue;
|
|
201
|
+
try {
|
|
202
|
+
const d = JSON.parse(line);
|
|
203
|
+
if (d.type === 'custom-title' && d.customTitle) { s.customTitle = d.customTitle; break; }
|
|
204
|
+
} catch { /* skip */ }
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
fs.closeSync(fd);
|
|
208
|
+
} catch { /* non-fatal */ }
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
_sessionCache = all;
|
|
212
|
+
_sessionCacheTime = Date.now();
|
|
213
|
+
return all;
|
|
214
|
+
} catch {
|
|
215
|
+
return [];
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
function listRecentSessions(limit, cwd) {
|
|
220
|
+
let all = scanAllSessions();
|
|
221
|
+
if (cwd) {
|
|
222
|
+
const matched = all.filter(s => s.projectPath === cwd);
|
|
223
|
+
if (matched.length > 0) all = matched;
|
|
224
|
+
}
|
|
225
|
+
return all.slice(0, limit || 10);
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
function loadSessionTags() {
|
|
229
|
+
try {
|
|
230
|
+
return JSON.parse(fs.readFileSync(path.join(HOME, '.metame', 'session_tags.json'), 'utf8'));
|
|
231
|
+
} catch { return {}; }
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
function getSessionFileMtime(sessionId) {
|
|
235
|
+
try {
|
|
236
|
+
if (!sessionId) return null;
|
|
237
|
+
const sessionFile = findSessionFile(sessionId);
|
|
238
|
+
if (sessionFile) return fs.statSync(sessionFile).mtimeMs;
|
|
239
|
+
} catch { /* ignore */ }
|
|
240
|
+
return null;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
function sessionLabel(s) {
|
|
244
|
+
const name = s.customTitle;
|
|
245
|
+
const proj = s.projectPath ? path.basename(s.projectPath) : '';
|
|
246
|
+
const realMtime = getSessionFileMtime(s.sessionId, s.projectPath);
|
|
247
|
+
const timeMs = realMtime || s.fileMtime || new Date(s.modified).getTime();
|
|
248
|
+
const ago = formatRelativeTime(new Date(timeMs).toISOString());
|
|
249
|
+
const shortId = s.sessionId.slice(0, 4);
|
|
250
|
+
|
|
251
|
+
if (name) return `${ago} [${name}] ${proj} #${shortId}`;
|
|
252
|
+
|
|
253
|
+
let title = (s.summary || '').slice(0, 20);
|
|
254
|
+
if (!title && s.firstPrompt) {
|
|
255
|
+
title = s.firstPrompt.slice(0, 20);
|
|
256
|
+
if (s.firstPrompt.length > 20) title += '..';
|
|
257
|
+
}
|
|
258
|
+
return `${ago} ${proj ? proj + ': ' : ''}${title || ''} #${shortId}`;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
function sessionDisplayTitle(s, maxLen, sessionTags) {
|
|
262
|
+
maxLen = maxLen || 50;
|
|
263
|
+
const sanitize = (t) => t
|
|
264
|
+
.replace(/\r?\n/g, ' ')
|
|
265
|
+
.replace(/[\x00-\x09\x0B\x0C\x0E-\x1F\x7F\uFFFD\uD800-\uDFFF]/g, '')
|
|
266
|
+
.replace(/\s+/g, ' ')
|
|
267
|
+
.trim();
|
|
268
|
+
if (s.customTitle) return sanitize(s.customTitle).slice(0, maxLen);
|
|
269
|
+
const tagEntry = sessionTags && sessionTags[s.sessionId];
|
|
270
|
+
if (tagEntry && tagEntry.name) return sanitize(tagEntry.name).slice(0, maxLen);
|
|
271
|
+
if (s.firstPrompt) {
|
|
272
|
+
const clean = s.firstPrompt
|
|
273
|
+
.replace(/<system-reminder>[\s\S]*?<\/system-reminder>/g, '')
|
|
274
|
+
.replace(/^<[^>]+>.*?<\/[^>]+>\s*/s, '')
|
|
275
|
+
.replace(/\[System hints[\s\S]*/i, '');
|
|
276
|
+
const firstLine = clean.split('\n').map(l => l.trim()).find(l => l.length > 2) || '';
|
|
277
|
+
const sanitized = sanitize(firstLine);
|
|
278
|
+
if (sanitized && sanitized.length > 2) return sanitized.slice(0, maxLen);
|
|
279
|
+
}
|
|
280
|
+
if (s.summary) return sanitize(s.summary).slice(0, maxLen);
|
|
281
|
+
return '';
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
function sessionRichLabel(s, index, sessionTags) {
|
|
285
|
+
sessionTags = sessionTags || loadSessionTags();
|
|
286
|
+
const title = sessionDisplayTitle(s, 50, sessionTags);
|
|
287
|
+
const proj = s.projectPath ? path.basename(s.projectPath) : '~';
|
|
288
|
+
const realMtime = getSessionFileMtime(s.sessionId, s.projectPath);
|
|
289
|
+
const timeMs = realMtime || s.fileMtime || new Date(s.modified).getTime();
|
|
290
|
+
const ago = formatRelativeTime(new Date(timeMs).toISOString());
|
|
291
|
+
const shortId = s.sessionId.slice(0, 8);
|
|
292
|
+
const tags = (sessionTags[s.sessionId] && sessionTags[s.sessionId].tags || []).slice(0, 3);
|
|
293
|
+
|
|
294
|
+
let line = `${index}. `;
|
|
295
|
+
if (title) line += `${title}${title.length >= 50 ? '..' : ''}`;
|
|
296
|
+
else line += '(unnamed)';
|
|
297
|
+
if (tags.length) line += ` ${tags.map(t => `#${t}`).join(' ')}`;
|
|
298
|
+
line += `\n š${proj} Ā· ${ago}`;
|
|
299
|
+
line += `\n /resume ${shortId}`;
|
|
300
|
+
return line;
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
function buildSessionCardElements(sessions) {
|
|
304
|
+
const sessionTags = loadSessionTags();
|
|
305
|
+
const elements = [];
|
|
306
|
+
sessions.forEach((s, i) => {
|
|
307
|
+
if (i > 0) elements.push({ tag: 'hr' });
|
|
308
|
+
const title = sessionDisplayTitle(s, 60, sessionTags);
|
|
309
|
+
const proj = s.projectPath ? path.basename(s.projectPath) : '~';
|
|
310
|
+
const realMtime = getSessionFileMtime(s.sessionId, s.projectPath);
|
|
311
|
+
const timeMs = realMtime || s.fileMtime || new Date(s.modified).getTime();
|
|
312
|
+
const ago = formatRelativeTime(new Date(timeMs).toISOString());
|
|
313
|
+
const shortId = s.sessionId.slice(0, 6);
|
|
314
|
+
const tags = (sessionTags[s.sessionId] && sessionTags[s.sessionId].tags || []).slice(0, 4);
|
|
315
|
+
let desc = `**${i + 1}. ${title || '(unnamed)'}**\nš${proj} Ā· ${ago}`;
|
|
316
|
+
if (tags.length) desc += `\n${tags.map(t => `\`${t}\``).join(' ')}`;
|
|
317
|
+
elements.push({ tag: 'div', text: { tag: 'lark_md', content: desc } });
|
|
318
|
+
elements.push({ tag: 'action', actions: [{ tag: 'button', text: { tag: 'plain_text', content: `ā¶ļø Switch #${shortId}` }, type: 'primary', value: { cmd: `/resume ${s.sessionId}` } }] });
|
|
319
|
+
});
|
|
320
|
+
return elements;
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
function listProjectDirs() {
|
|
324
|
+
try {
|
|
325
|
+
const all = listRecentSessions(50);
|
|
326
|
+
const seen = new Map();
|
|
327
|
+
for (const s of all) {
|
|
328
|
+
if (!s.projectPath || !fs.existsSync(s.projectPath)) continue;
|
|
329
|
+
const prev = seen.get(s.projectPath);
|
|
330
|
+
if (!prev || new Date(s.modified) > new Date(prev)) seen.set(s.projectPath, s.modified);
|
|
331
|
+
}
|
|
332
|
+
return [...seen.entries()]
|
|
333
|
+
.sort((a, b) => new Date(b[1]) - new Date(a[1]))
|
|
334
|
+
.slice(0, 6)
|
|
335
|
+
.map(([p]) => ({ path: p, label: path.basename(p) }));
|
|
336
|
+
} catch {
|
|
337
|
+
return [];
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
function getSession(chatId) {
|
|
342
|
+
const state = loadState();
|
|
343
|
+
return state.sessions[chatId] || null;
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
function createSession(chatId, cwd, name) {
|
|
347
|
+
const state = loadState();
|
|
348
|
+
const sessionId = crypto.randomUUID();
|
|
349
|
+
state.sessions[chatId] = {
|
|
350
|
+
id: sessionId,
|
|
351
|
+
cwd: cwd || HOME,
|
|
352
|
+
started: false,
|
|
353
|
+
};
|
|
354
|
+
saveState(state);
|
|
355
|
+
invalidateSessionCache();
|
|
356
|
+
if (name) writeSessionName(sessionId, cwd || HOME, name);
|
|
357
|
+
log('INFO', `New session for ${chatId}: ${sessionId}${name ? ' [' + name + ']' : ''} (cwd: ${state.sessions[chatId].cwd})`);
|
|
358
|
+
return { ...state.sessions[chatId], id: sessionId };
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
function getSessionName(sessionId) {
|
|
362
|
+
try {
|
|
363
|
+
if (!fs.existsSync(CLAUDE_PROJECTS_DIR)) return '';
|
|
364
|
+
const projects = fs.readdirSync(CLAUDE_PROJECTS_DIR);
|
|
365
|
+
for (const proj of projects) {
|
|
366
|
+
const indexFile = path.join(CLAUDE_PROJECTS_DIR, proj, 'sessions-index.json');
|
|
367
|
+
if (!fs.existsSync(indexFile)) continue;
|
|
368
|
+
const data = JSON.parse(fs.readFileSync(indexFile, 'utf8'));
|
|
369
|
+
if (data.entries) {
|
|
370
|
+
const entry = data.entries.find(e => e.sessionId === sessionId);
|
|
371
|
+
if (entry && entry.customTitle) return entry.customTitle;
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
} catch { /* ignore */ }
|
|
375
|
+
return '';
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
function writeSessionName(sessionId, cwd, name) {
|
|
379
|
+
void cwd;
|
|
380
|
+
try {
|
|
381
|
+
const sessionFile = findSessionFile(sessionId);
|
|
382
|
+
if (!sessionFile) {
|
|
383
|
+
log('WARN', `writeSessionName: session file not found for ${sessionId.slice(0, 8)}`);
|
|
384
|
+
return;
|
|
385
|
+
}
|
|
386
|
+
const entry = JSON.stringify({ type: 'custom-title', customTitle: name, sessionId }) + '\n';
|
|
387
|
+
fs.appendFileSync(sessionFile, entry, 'utf8');
|
|
388
|
+
log('INFO', `Named session ${sessionId.slice(0, 8)}: ${name}`);
|
|
389
|
+
return true;
|
|
390
|
+
} catch (e) {
|
|
391
|
+
log('WARN', `Failed to write session name: ${e.message}`);
|
|
392
|
+
return false;
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
function markSessionStarted(chatId) {
|
|
397
|
+
const state = loadState();
|
|
398
|
+
if (state.sessions[chatId]) {
|
|
399
|
+
state.sessions[chatId].started = true;
|
|
400
|
+
saveState(state);
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
return {
|
|
405
|
+
findSessionFile,
|
|
406
|
+
clearSessionFileCache,
|
|
407
|
+
truncateSessionToCheckpoint,
|
|
408
|
+
listRecentSessions,
|
|
409
|
+
loadSessionTags,
|
|
410
|
+
getSessionFileMtime,
|
|
411
|
+
sessionLabel,
|
|
412
|
+
sessionRichLabel,
|
|
413
|
+
buildSessionCardElements,
|
|
414
|
+
listProjectDirs,
|
|
415
|
+
getSession,
|
|
416
|
+
createSession,
|
|
417
|
+
getSessionName,
|
|
418
|
+
writeSessionName,
|
|
419
|
+
markSessionStarted,
|
|
420
|
+
};
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
module.exports = { createSessionStore };
|