cli-link 0.0.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.
Files changed (34) hide show
  1. package/README.md +271 -0
  2. package/bin/agentpilot.js +239 -0
  3. package/dist/client/assets/History-DR_K6WbO.js +3 -0
  4. package/dist/client/assets/MarkdownRenderer-D9IwexPM.js +1 -0
  5. package/dist/client/assets/PageTopBar-SnTIrSb5.js +1 -0
  6. package/dist/client/assets/Session-EFYFIC_X.js +11 -0
  7. package/dist/client/assets/Settings-DgmHC_Hw.js +1 -0
  8. package/dist/client/assets/Workspace-CJvVQVzU.js +8 -0
  9. package/dist/client/assets/WorkspaceLinkedText-D6hNg0T9.js +2 -0
  10. package/dist/client/assets/code-highlight-CEcsuMpw.js +1 -0
  11. package/dist/client/assets/index-Bk4_acsd.css +1 -0
  12. package/dist/client/assets/index-C89UCwGk.js +2 -0
  13. package/dist/client/assets/vendor-icons-S_ObYVVf.js +331 -0
  14. package/dist/client/assets/vendor-markdown-BDwu-Ux6.js +35 -0
  15. package/dist/client/assets/vendor-motion-n6Lx6G4a.js +9 -0
  16. package/dist/client/assets/vendor-react-DSV5aFEg.js +67 -0
  17. package/dist/client/assets/vendor-virtual-CcftJrIC.js +4 -0
  18. package/dist/client/favicon.svg +18 -0
  19. package/dist/client/icons/apple-touch-icon.png +0 -0
  20. package/dist/client/icons/icon-192.png +0 -0
  21. package/dist/client/icons/icon-512.png +0 -0
  22. package/dist/client/index.html +34 -0
  23. package/dist/client/manifest.webmanifest +59 -0
  24. package/dist/client/sw.js +143 -0
  25. package/dist/client//344/273/243/347/240/201/351/241/265/351/235/242.png +0 -0
  26. package/dist/client//345/216/206/345/217/262/350/256/260/345/275/225.png +0 -0
  27. package/dist/client//345/257/271/350/257/235/351/241/265/351/235/242.png +0 -0
  28. package/dist/client//350/256/276/347/275/256/351/241/265/351/235/242.png +0 -0
  29. package/dist/server/cli-manager.js +1532 -0
  30. package/dist/server/codex-history.js +280 -0
  31. package/dist/server/index.js +2097 -0
  32. package/dist/server/store.js +594 -0
  33. package/dist/server/terminal-qr.js +317 -0
  34. package/package.json +71 -0
@@ -0,0 +1,594 @@
1
+ import Database from 'better-sqlite3';
2
+ import { mkdirSync, existsSync, statSync } from 'fs';
3
+ import { join, dirname, resolve } from 'path';
4
+ import { homedir } from 'os';
5
+ import { randomUUID } from 'crypto';
6
+ function cleanHistoryTitle(content) {
7
+ const normalized = content
8
+ .replace(/```[\s\S]*?```/g, ' ')
9
+ .replace(/`([^`]+)`/g, '$1')
10
+ .split(/\r?\n/)
11
+ .map(line => line.trim())
12
+ .filter(line => {
13
+ if (!line)
14
+ return false;
15
+ if (/^#+\s*AGENTS\.md/i.test(line))
16
+ return false;
17
+ if (/^<\/?[A-Z_]+>/i.test(line))
18
+ return false;
19
+ if (/^[-*]\s*$/.test(line))
20
+ return false;
21
+ return true;
22
+ })
23
+ .join(' ')
24
+ .replace(/\s+/g, ' ')
25
+ .replace(/^#+\s*/, '')
26
+ .replace(/^[-*]\s+/, '')
27
+ .trim();
28
+ if (!normalized)
29
+ return '会话记录';
30
+ return normalized.length > 48 ? `${normalized.slice(0, 48).trimEnd()}...` : normalized;
31
+ }
32
+ function buildHistoryTitle(messages) {
33
+ const userMessage = messages.find(m => m.type === 'user_message' || m.type === 'user');
34
+ return userMessage ? cleanHistoryTitle(userMessage.content) : '会话记录';
35
+ }
36
+ function normalizeWorkDir(workDir) {
37
+ if (typeof workDir !== 'string')
38
+ return null;
39
+ const trimmed = workDir.trim();
40
+ if (!trimmed)
41
+ return null;
42
+ const expanded = trimmed === '~'
43
+ ? homedir()
44
+ : trimmed.startsWith('~/')
45
+ ? join(homedir(), trimmed.slice(2))
46
+ : trimmed;
47
+ return resolve(expanded);
48
+ }
49
+ function parseConfig(raw) {
50
+ if (!raw)
51
+ return {};
52
+ try {
53
+ const parsed = JSON.parse(raw);
54
+ return parsed && typeof parsed === 'object' ? parsed : {};
55
+ }
56
+ catch {
57
+ return {};
58
+ }
59
+ }
60
+ function getWorkDirFromSessionConfig(raw) {
61
+ const config = parseConfig(raw);
62
+ return normalizeWorkDir(config.workDir);
63
+ }
64
+ function getSessionWorkDir(sessionId) {
65
+ const row = db.prepare(`SELECT cli_config FROM sessions WHERE id = ?`).get(sessionId);
66
+ return getWorkDirFromSessionConfig(row?.cli_config);
67
+ }
68
+ // --- Database ---
69
+ let db;
70
+ const DB_DIR = join(homedir(), '.agentpilot');
71
+ const DB_PATH = join(DB_DIR, 'data.db');
72
+ export function initDb(dbPath) {
73
+ const path = dbPath || DB_PATH;
74
+ const dir = dirname(path);
75
+ if (!existsSync(dir)) {
76
+ mkdirSync(dir, { recursive: true });
77
+ }
78
+ db = new Database(path);
79
+ db.pragma('journal_mode = WAL');
80
+ db.pragma('foreign_keys = ON');
81
+ db.exec(`
82
+ CREATE TABLE IF NOT EXISTS sessions (
83
+ id TEXT PRIMARY KEY,
84
+ status TEXT NOT NULL DEFAULT 'idle',
85
+ cli_config TEXT,
86
+ started_at INTEGER NOT NULL,
87
+ last_seq INTEGER NOT NULL DEFAULT 0
88
+ );
89
+
90
+ CREATE TABLE IF NOT EXISTS messages (
91
+ seq INTEGER PRIMARY KEY AUTOINCREMENT,
92
+ id TEXT NOT NULL,
93
+ type TEXT NOT NULL,
94
+ content TEXT NOT NULL DEFAULT '',
95
+ time TEXT NOT NULL,
96
+ status TEXT,
97
+ toolName TEXT,
98
+ toolDetails TEXT,
99
+ toolUseId TEXT,
100
+ toolResult TEXT,
101
+ permission TEXT,
102
+ details TEXT,
103
+ session_id TEXT NOT NULL
104
+ );
105
+
106
+ CREATE TABLE IF NOT EXISTS history_tasks (
107
+ id TEXT PRIMARY KEY,
108
+ session_id TEXT,
109
+ work_dir TEXT,
110
+ status TEXT NOT NULL,
111
+ title TEXT NOT NULL,
112
+ confirm_count INTEGER NOT NULL DEFAULT 0,
113
+ tool_count INTEGER NOT NULL DEFAULT 0,
114
+ duration TEXT,
115
+ start_time INTEGER NOT NULL,
116
+ end_time INTEGER NOT NULL,
117
+ created_at INTEGER NOT NULL
118
+ );
119
+
120
+ CREATE TABLE IF NOT EXISTS workdir_history (
121
+ path TEXT PRIMARY KEY,
122
+ name TEXT NOT NULL,
123
+ last_used_at INTEGER NOT NULL,
124
+ created_at INTEGER NOT NULL
125
+ );
126
+
127
+ CREATE INDEX IF NOT EXISTS idx_messages_session_seq ON messages(session_id, seq);
128
+ `);
129
+ // Migrate: add 'details' column if it doesn't exist (for existing databases)
130
+ const columns = db.prepare(`PRAGMA table_info(messages)`).all();
131
+ if (!columns.some(c => c.name === 'details')) {
132
+ db.exec(`ALTER TABLE messages ADD COLUMN details TEXT`);
133
+ console.log('[Store] Migrated: added details column to messages table');
134
+ }
135
+ if (!columns.some(c => c.name === 'toolUseId')) {
136
+ db.exec(`ALTER TABLE messages ADD COLUMN toolUseId TEXT`);
137
+ console.log('[Store] Migrated: added toolUseId column to messages table');
138
+ }
139
+ if (!columns.some(c => c.name === 'toolResult')) {
140
+ db.exec(`ALTER TABLE messages ADD COLUMN toolResult TEXT`);
141
+ console.log('[Store] Migrated: added toolResult column to messages table');
142
+ }
143
+ const historyColumns = db.prepare(`PRAGMA table_info(history_tasks)`).all();
144
+ if (!historyColumns.some(c => c.name === 'work_dir')) {
145
+ db.exec(`ALTER TABLE history_tasks ADD COLUMN work_dir TEXT`);
146
+ console.log('[Store] Migrated: added work_dir column to history_tasks table');
147
+ }
148
+ db.exec(`CREATE INDEX IF NOT EXISTS idx_history_tasks_work_dir_created ON history_tasks(work_dir, created_at DESC)`);
149
+ db.exec(`CREATE INDEX IF NOT EXISTS idx_workdir_history_last_used ON workdir_history(last_used_at DESC)`);
150
+ backfillHistoryTaskSummaries();
151
+ console.log(`[Store] Database initialized at ${path}`);
152
+ }
153
+ function backfillHistoryTaskSummaries() {
154
+ const tasks = db.prepare(`SELECT id, session_id, work_dir, title, confirm_count, tool_count
155
+ FROM history_tasks
156
+ WHERE session_id IS NOT NULL`).all();
157
+ if (tasks.length === 0)
158
+ return;
159
+ const firstUserMessage = db.prepare(`SELECT content FROM messages
160
+ WHERE session_id = ? AND type IN ('user_message', 'user') AND content != ''
161
+ ORDER BY seq ASC
162
+ LIMIT 1`);
163
+ const countMessages = db.prepare(`SELECT
164
+ SUM(CASE WHEN type IN ('confirm_request', 'confirm') THEN 1 ELSE 0 END) AS confirm_count,
165
+ SUM(CASE WHEN type IN ('tool_call', 'tool') THEN 1 ELSE 0 END) AS tool_count
166
+ FROM messages
167
+ WHERE session_id = ?`);
168
+ const updateTask = db.prepare(`UPDATE history_tasks
169
+ SET title = ?, confirm_count = ?, tool_count = ?, work_dir = ?
170
+ WHERE id = ?`);
171
+ const getSessionConfig = db.prepare(`SELECT cli_config FROM sessions WHERE id = ?`);
172
+ const backfill = db.transaction((rows) => {
173
+ for (const task of rows) {
174
+ if (!task.session_id)
175
+ continue;
176
+ let title = task.title;
177
+ if (!title || title === '会话记录') {
178
+ const userMessage = firstUserMessage.get(task.session_id);
179
+ title = userMessage ? cleanHistoryTitle(userMessage.content) : '会话记录';
180
+ }
181
+ const counts = countMessages.get(task.session_id);
182
+ const confirmCount = counts.confirm_count || 0;
183
+ const toolCount = counts.tool_count || 0;
184
+ const session = getSessionConfig.get(task.session_id);
185
+ const workDir = task.work_dir || getWorkDirFromSessionConfig(session?.cli_config);
186
+ if (title !== task.title || confirmCount !== task.confirm_count || toolCount !== task.tool_count || workDir !== task.work_dir) {
187
+ updateTask.run(title, confirmCount, toolCount, workDir || null, task.id);
188
+ }
189
+ }
190
+ });
191
+ backfill(tasks);
192
+ }
193
+ // --- Session Operations ---
194
+ export function createSession(config) {
195
+ const id = randomUUID();
196
+ const now = Date.now();
197
+ // If there's an existing active session, archive it first
198
+ const current = getCurrentSession();
199
+ if (current && current.status !== 'archived') {
200
+ archiveSession(current.id, 'completed');
201
+ }
202
+ db.prepare(`INSERT INTO sessions (id, status, cli_config, started_at, last_seq) VALUES (?, ?, ?, ?, 0)`).run(id, 'idle', config ? JSON.stringify(config) : null, now);
203
+ return id;
204
+ }
205
+ export function getCurrentSession() {
206
+ const row = db.prepare(`SELECT * FROM sessions WHERE status != 'archived' ORDER BY started_at DESC LIMIT 1`).get();
207
+ return row || null;
208
+ }
209
+ export function getSessionById(sessionId) {
210
+ const row = db.prepare(`SELECT * FROM sessions WHERE id = ?`).get(sessionId);
211
+ return row || null;
212
+ }
213
+ export function updateSessionStatus(sessionId, status) {
214
+ db.prepare(`UPDATE sessions SET status = ? WHERE id = ?`).run(status, sessionId);
215
+ }
216
+ export function updateSessionConfig(sessionId, config) {
217
+ db.prepare(`UPDATE sessions SET cli_config = ? WHERE id = ?`).run(JSON.stringify(config), sessionId);
218
+ }
219
+ // --- Message Operations ---
220
+ export function appendMessage(sessionId, msg) {
221
+ const id = msg.id || randomUUID();
222
+ const result = db.prepare(`INSERT INTO messages (id, type, content, time, status, toolName, toolDetails, toolUseId, toolResult, permission, details, session_id)
223
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`).run(id, msg.type, msg.content, msg.time, msg.status || null, msg.toolName || null, msg.toolDetails || null, msg.toolUseId || null, msg.toolResult || null, msg.permission ? JSON.stringify(msg.permission) : null, msg.details ? JSON.stringify(msg.details) : null, sessionId);
224
+ const seq = Number(result.lastInsertRowid);
225
+ db.prepare(`UPDATE sessions SET last_seq = ? WHERE id = ?`).run(seq, sessionId);
226
+ return { id, seq };
227
+ }
228
+ export function updateToolCallResult(sessionId, toolUseId, status, toolResult) {
229
+ const result = db.prepare(`UPDATE messages SET status = ?, toolResult = ? WHERE session_id = ? AND toolUseId = ? AND type = 'tool_call'`).run(status, toolResult, sessionId, toolUseId);
230
+ return result.changes > 0;
231
+ }
232
+ export function getSessionMessages(sessionId, afterSeq) {
233
+ if (afterSeq !== undefined && afterSeq > 0) {
234
+ return db.prepare(`SELECT * FROM messages WHERE session_id = ? AND seq > ? ORDER BY seq ASC`).all(sessionId, afterSeq);
235
+ }
236
+ return db.prepare(`SELECT * FROM messages WHERE session_id = ? ORDER BY seq ASC`).all(sessionId);
237
+ }
238
+ export function getRecentSessionMessages(sessionId, limit) {
239
+ const normalizedLimit = Math.max(1, Math.floor(limit));
240
+ const rows = db.prepare(`SELECT * FROM messages WHERE session_id = ? ORDER BY seq DESC LIMIT ?`).all(sessionId, normalizedLimit);
241
+ return rows.reverse();
242
+ }
243
+ export function getSessionMessagesBefore(sessionId, beforeSeq, limit) {
244
+ const normalizedLimit = Math.max(1, Math.floor(limit));
245
+ const rows = db.prepare(`SELECT * FROM messages WHERE session_id = ? AND seq < ? ORDER BY seq DESC LIMIT ?`).all(sessionId, beforeSeq, normalizedLimit);
246
+ return rows.reverse();
247
+ }
248
+ export function hasSessionMessagesBefore(sessionId, beforeSeq) {
249
+ const row = db.prepare(`SELECT 1 FROM messages WHERE session_id = ? AND seq < ? LIMIT 1`).get(sessionId, beforeSeq);
250
+ return !!row;
251
+ }
252
+ export function getSessionDetails(sessionId) {
253
+ const rows = db.prepare(`SELECT details FROM messages WHERE session_id = ? AND details IS NOT NULL AND details != ''`).all(sessionId);
254
+ return rows.map(row => row.details);
255
+ }
256
+ export function getSessionMessageById(sessionId, messageId) {
257
+ const row = db.prepare(`SELECT * FROM messages WHERE session_id = ? AND id = ? ORDER BY seq ASC LIMIT 1`).get(sessionId, messageId);
258
+ return row || null;
259
+ }
260
+ export function copySessionMessagesBeforeSeq(sourceSessionId, targetSessionId, beforeSeq) {
261
+ const messages = db.prepare(`SELECT id, type, content, time, status, toolName, toolDetails, toolUseId, toolResult, permission, details
262
+ FROM messages
263
+ WHERE session_id = ? AND seq < ?
264
+ ORDER BY seq ASC`).all(sourceSessionId, beforeSeq);
265
+ if (messages.length === 0)
266
+ return 0;
267
+ const insertStmt = db.prepare(`INSERT INTO messages (id, type, content, time, status, toolName, toolDetails, toolUseId, toolResult, permission, details, session_id)
268
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`);
269
+ const copyMany = db.transaction((rows) => {
270
+ let lastSeq = 0;
271
+ for (const msg of rows) {
272
+ const result = insertStmt.run(msg.id, msg.type, msg.content, msg.time, msg.status || null, msg.toolName || null, msg.toolDetails || null, msg.toolUseId || null, msg.toolResult || null, msg.permission || null, msg.details || null, targetSessionId);
273
+ lastSeq = Number(result.lastInsertRowid);
274
+ }
275
+ db.prepare(`UPDATE sessions SET last_seq = ? WHERE id = ?`).run(lastSeq, targetSessionId);
276
+ return lastSeq;
277
+ });
278
+ return copyMany(messages);
279
+ }
280
+ export function getMessageCount(sessionId) {
281
+ const row = db.prepare(`SELECT COUNT(*) as count FROM messages WHERE session_id = ?`).get(sessionId);
282
+ return row.count;
283
+ }
284
+ export function getLastSeq(sessionId) {
285
+ const row = db.prepare(`SELECT last_seq FROM sessions WHERE id = ?`).get(sessionId);
286
+ return row?.last_seq || 0;
287
+ }
288
+ // --- History Operations ---
289
+ export function archiveSession(sessionId, status) {
290
+ const messages = getSessionMessages(sessionId);
291
+ if (messages.length === 0) {
292
+ // Just mark as archived, no history task
293
+ db.prepare(`UPDATE sessions SET status = 'archived' WHERE id = ?`).run(sessionId);
294
+ return null;
295
+ }
296
+ const title = buildHistoryTitle(messages);
297
+ const confirmCount = messages.filter(m => m.type === 'confirm_request' || m.type === 'confirm').length;
298
+ const toolCount = messages.filter(m => m.type === 'tool_call' || m.type === 'tool').length;
299
+ const workDir = getSessionWorkDir(sessionId);
300
+ const firstTime = messages[0]?.time;
301
+ const lastTime = messages[messages.length - 1]?.time;
302
+ let startTime = Date.now() - 60_000;
303
+ let endTime = Date.now();
304
+ if (firstTime) {
305
+ const s = new Date(firstTime).getTime();
306
+ if (!isNaN(s))
307
+ startTime = s;
308
+ }
309
+ if (lastTime) {
310
+ const e = new Date(lastTime).getTime();
311
+ if (!isNaN(e))
312
+ endTime = e;
313
+ }
314
+ let duration;
315
+ const diffSec = Math.round((endTime - startTime) / 1000);
316
+ if (diffSec > 0) {
317
+ const m = Math.floor(diffSec / 60);
318
+ const s = diffSec % 60;
319
+ duration = m > 0 ? `${m}m ${String(s).padStart(2, '0')}s` : `${s}s`;
320
+ }
321
+ const taskId = `task-${Date.now()}-${Math.random().toString(36).slice(2, 6)}`;
322
+ db.prepare(`INSERT INTO history_tasks (id, session_id, work_dir, status, title, confirm_count, tool_count, duration, start_time, end_time, created_at)
323
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`).run(taskId, sessionId, workDir, status, title, confirmCount, toolCount, duration || null, startTime, endTime, Date.now());
324
+ db.prepare(`UPDATE sessions SET status = 'archived' WHERE id = ?`).run(sessionId);
325
+ return taskId;
326
+ }
327
+ export function getHistoryTasks(options = {}) {
328
+ const limit = typeof options === 'number' ? options : options.limit ?? 50;
329
+ const workDir = typeof options === 'number' ? null : normalizeWorkDir(options.workDir);
330
+ if (workDir) {
331
+ return db.prepare(`SELECT * FROM history_tasks
332
+ WHERE work_dir = ?
333
+ ORDER BY created_at DESC
334
+ LIMIT ?`).all(workDir, limit);
335
+ }
336
+ return db.prepare(`SELECT * FROM history_tasks ORDER BY created_at DESC LIMIT ?`).all(limit);
337
+ }
338
+ export function getLatestCliSessionIdFromMessages(sessionId, limit = 50) {
339
+ const rows = db.prepare(`SELECT details FROM messages
340
+ WHERE session_id = ? AND details IS NOT NULL
341
+ ORDER BY seq DESC
342
+ LIMIT ?`).all(sessionId, limit);
343
+ for (const row of rows) {
344
+ if (!row.details)
345
+ continue;
346
+ try {
347
+ const details = JSON.parse(row.details);
348
+ const cliSessionId = details?.session_id;
349
+ if (typeof cliSessionId === 'string' && cliSessionId.length > 8) {
350
+ return cliSessionId;
351
+ }
352
+ }
353
+ catch { }
354
+ }
355
+ return undefined;
356
+ }
357
+ export function getHistoryTask(id, workDir) {
358
+ const normalizedWorkDir = normalizeWorkDir(workDir);
359
+ const task = normalizedWorkDir
360
+ ? db.prepare(`SELECT * FROM history_tasks WHERE id = ? AND work_dir = ?`).get(id, normalizedWorkDir)
361
+ : db.prepare(`SELECT * FROM history_tasks WHERE id = ?`).get(id);
362
+ if (!task)
363
+ return null;
364
+ const messages = task.session_id
365
+ ? getSessionMessages(task.session_id)
366
+ : [];
367
+ return { ...task, messages };
368
+ }
369
+ export function resumeHistoryTask(id, currentSessionId, workDir) {
370
+ const task = getHistoryTask(id, workDir);
371
+ if (!task?.session_id)
372
+ return null;
373
+ const session = getSessionById(task.session_id);
374
+ if (!session)
375
+ return null;
376
+ if (currentSessionId && currentSessionId !== session.id) {
377
+ const current = getSessionById(currentSessionId);
378
+ if (current && current.status !== 'archived') {
379
+ archiveSession(current.id, 'completed');
380
+ }
381
+ }
382
+ else {
383
+ const current = getCurrentSession();
384
+ if (current && current.id !== session.id && current.status !== 'archived') {
385
+ archiveSession(current.id, 'completed');
386
+ }
387
+ }
388
+ db.prepare(`DELETE FROM history_tasks WHERE id = ?`).run(id);
389
+ db.prepare(`UPDATE sessions SET status = 'idle' WHERE id = ?`).run(session.id);
390
+ return {
391
+ ...task,
392
+ session: {
393
+ ...session,
394
+ status: 'idle',
395
+ },
396
+ messages: getSessionMessages(session.id),
397
+ };
398
+ }
399
+ export function deleteHistoryTask(id, workDir) {
400
+ const normalizedWorkDir = normalizeWorkDir(workDir);
401
+ const result = normalizedWorkDir
402
+ ? db.prepare(`DELETE FROM history_tasks WHERE id = ? AND work_dir = ?`).run(id, normalizedWorkDir)
403
+ : db.prepare(`DELETE FROM history_tasks WHERE id = ?`).run(id);
404
+ return result.changes > 0;
405
+ }
406
+ export function clearHistory(workDir) {
407
+ const normalizedWorkDir = normalizeWorkDir(workDir);
408
+ if (normalizedWorkDir) {
409
+ db.prepare(`DELETE FROM history_tasks WHERE work_dir = ?`).run(normalizedWorkDir);
410
+ return;
411
+ }
412
+ db.prepare(`DELETE FROM history_tasks`).run();
413
+ }
414
+ export function backfillUnscopedHistoryWorkDir(workDir) {
415
+ const normalizedWorkDir = normalizeWorkDir(workDir);
416
+ if (!normalizedWorkDir)
417
+ return;
418
+ const result = db.prepare(`UPDATE history_tasks
419
+ SET work_dir = ?
420
+ WHERE work_dir IS NULL`).run(normalizedWorkDir);
421
+ if (result.changes > 0) {
422
+ console.log(`[Store] Backfilled ${result.changes} unscoped history tasks to ${normalizedWorkDir}`);
423
+ }
424
+ }
425
+ // --- Work Directory History ---
426
+ function getWorkDirName(workDir) {
427
+ const base = workDir.split(/[\\/]/).filter(Boolean).pop();
428
+ return base || workDir;
429
+ }
430
+ function isReadableDirectory(workDir) {
431
+ try {
432
+ return statSync(workDir).isDirectory();
433
+ }
434
+ catch {
435
+ return false;
436
+ }
437
+ }
438
+ export function recordWorkDir(workDir) {
439
+ const normalized = normalizeWorkDir(workDir);
440
+ if (!normalized || !isReadableDirectory(normalized))
441
+ return null;
442
+ const now = Date.now();
443
+ const name = getWorkDirName(normalized);
444
+ db.prepare(`INSERT INTO workdir_history (path, name, last_used_at, created_at)
445
+ VALUES (?, ?, ?, ?)
446
+ ON CONFLICT(path) DO UPDATE SET
447
+ name = excluded.name,
448
+ last_used_at = excluded.last_used_at`).run(normalized, name, now, now);
449
+ return {
450
+ path: normalized,
451
+ name,
452
+ last_used_at: now,
453
+ created_at: now,
454
+ };
455
+ }
456
+ export function getRecentWorkDirs(limit = 12) {
457
+ const normalizedLimit = Math.max(1, Math.min(50, Math.floor(limit)));
458
+ const rows = db.prepare(`SELECT * FROM workdir_history
459
+ ORDER BY last_used_at DESC
460
+ LIMIT ?`).all(normalizedLimit);
461
+ const stalePaths = [];
462
+ const existing = rows.filter(row => {
463
+ const ok = isReadableDirectory(row.path);
464
+ if (!ok)
465
+ stalePaths.push(row.path);
466
+ return ok;
467
+ });
468
+ if (stalePaths.length > 0) {
469
+ const remove = db.prepare(`DELETE FROM workdir_history WHERE path = ?`);
470
+ const removeMany = db.transaction((paths) => {
471
+ for (const stalePath of paths)
472
+ remove.run(stalePath);
473
+ });
474
+ removeMany(stalePaths);
475
+ }
476
+ return existing;
477
+ }
478
+ // --- Migration (from client localStorage) ---
479
+ export function importLocalData(messages, historyTasks, workDir) {
480
+ const current = getCurrentSession();
481
+ const importWorkDir = normalizeWorkDir(workDir)
482
+ || getWorkDirFromSessionConfig(current?.cli_config)
483
+ || normalizeWorkDir(process.cwd());
484
+ // Import messages into the current session (or create one if none)
485
+ let sessionId;
486
+ if (current && current.status !== 'archived') {
487
+ sessionId = current.id;
488
+ }
489
+ else {
490
+ sessionId = createSession();
491
+ }
492
+ // Only import if current session has no messages
493
+ const existingCount = getMessageCount(sessionId);
494
+ if (existingCount === 0 && messages.length > 0) {
495
+ const insertStmt = db.prepare(`INSERT INTO messages (id, type, content, time, status, toolName, toolDetails, permission, session_id)
496
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`);
497
+ const insertMany = db.transaction((msgs) => {
498
+ for (const msg of msgs) {
499
+ insertStmt.run(String(msg.id || randomUUID()), String(msg.type || 'system'), String(msg.content || ''), String(msg.time || new Date().toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })), msg.status || null, msg.toolName || null, msg.toolDetails || null, msg.permission ? JSON.stringify(msg.permission) : null, sessionId);
500
+ }
501
+ });
502
+ insertMany(messages);
503
+ const lastSeq = getLastSeq(sessionId);
504
+ db.prepare(`UPDATE sessions SET last_seq = ? WHERE id = ?`).run(lastSeq, sessionId);
505
+ }
506
+ // Import history tasks
507
+ if (historyTasks.length > 0) {
508
+ const insertTaskStmt = db.prepare(`INSERT OR IGNORE INTO history_tasks (id, session_id, work_dir, status, title, confirm_count, tool_count, duration, start_time, end_time, created_at)
509
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`);
510
+ const insertTasks = db.transaction((tasks) => {
511
+ for (const task of tasks) {
512
+ insertTaskStmt.run(String(task.id || randomUUID()), null, // session_id not available for imported history
513
+ importWorkDir, String(task.status || 'completed'), String(task.title || '会话记录'), Number(task.confirmCount || 0), Number(task.toolCount || 0), task.duration || null, Number(task.startTime || Date.now()), Number(task.endTime || Date.now()), Number(task.endTime || Date.now()));
514
+ }
515
+ });
516
+ insertTasks(historyTasks);
517
+ }
518
+ console.log(`[Store] Imported ${messages.length} messages and ${historyTasks.length} history tasks`);
519
+ }
520
+ export function importCodexSessions(sessions) {
521
+ const result = {
522
+ scanned: sessions.length,
523
+ imported: 0,
524
+ skipped: 0,
525
+ failed: 0,
526
+ messagesImported: 0,
527
+ errors: [],
528
+ };
529
+ if (sessions.length === 0) {
530
+ return result;
531
+ }
532
+ const sessionExists = db.prepare(`SELECT 1 FROM sessions WHERE id = ? LIMIT 1`);
533
+ const taskExists = db.prepare(`SELECT 1 FROM history_tasks WHERE id = ? LIMIT 1`);
534
+ const insertSession = db.prepare(`INSERT INTO sessions (id, status, cli_config, started_at, last_seq) VALUES (?, ?, ?, ?, 0)`);
535
+ const insertMessage = db.prepare(`INSERT INTO messages (id, type, content, time, status, toolName, toolDetails, toolUseId, toolResult, permission, details, session_id)
536
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`);
537
+ const updateLastSeq = db.prepare(`UPDATE sessions SET last_seq = ? WHERE id = ?`);
538
+ const insertTask = db.prepare(`INSERT INTO history_tasks (id, session_id, work_dir, status, title, confirm_count, tool_count, duration, start_time, end_time, created_at)
539
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`);
540
+ const importOne = db.transaction((session) => {
541
+ const workDir = normalizeWorkDir(session.workDir);
542
+ const messages = session.messages || [];
543
+ const confirmCount = messages.filter(m => m.type === 'confirm_request' || m.type === 'confirm').length;
544
+ const toolCount = messages.filter(m => m.type === 'tool_call' || m.type === 'tool').length;
545
+ const startedAt = Number.isFinite(session.startedAt) && session.startedAt > 0 ? session.startedAt : Date.now();
546
+ const endedAt = Number.isFinite(session.endedAt) && session.endedAt > 0 ? session.endedAt : startedAt;
547
+ const createdAt = Number.isFinite(session.createdAt) && session.createdAt > 0 ? session.createdAt : endedAt;
548
+ const diffSec = Math.max(0, Math.round((endedAt - startedAt) / 1000));
549
+ const duration = diffSec > 0
550
+ ? diffSec >= 60
551
+ ? `${Math.floor(diffSec / 60)}m ${String(diffSec % 60).padStart(2, '0')}s`
552
+ : `${diffSec}s`
553
+ : null;
554
+ insertSession.run(session.sessionId, 'archived', JSON.stringify(session.cliConfig || {}), startedAt);
555
+ let lastSeq = 0;
556
+ for (const message of messages) {
557
+ const inserted = insertMessage.run(message.id, message.type, message.content || '', message.time || '', message.status || null, message.toolName || null, message.toolDetails || null, message.toolUseId || null, message.toolResult || null, message.permission ? JSON.stringify(message.permission) : null, message.details ? JSON.stringify(message.details) : null, session.sessionId);
558
+ lastSeq = Number(inserted.lastInsertRowid);
559
+ }
560
+ if (lastSeq > 0) {
561
+ updateLastSeq.run(lastSeq, session.sessionId);
562
+ }
563
+ insertTask.run(session.taskId, session.sessionId, workDir, session.status || 'completed', session.title || 'Codex 会话记录', confirmCount, toolCount, duration, startedAt, endedAt, createdAt);
564
+ return messages.length;
565
+ });
566
+ for (const session of sessions) {
567
+ try {
568
+ if (!session.sessionId || !session.taskId || session.messages.length === 0) {
569
+ result.skipped += 1;
570
+ continue;
571
+ }
572
+ if (sessionExists.get(session.sessionId) || taskExists.get(session.taskId)) {
573
+ result.skipped += 1;
574
+ continue;
575
+ }
576
+ const importedMessages = importOne(session);
577
+ result.imported += 1;
578
+ result.messagesImported += importedMessages;
579
+ }
580
+ catch (err) {
581
+ result.failed += 1;
582
+ result.errors.push(`${session.sessionId || 'unknown'}: ${err?.message || 'import failed'}`);
583
+ }
584
+ }
585
+ console.log(`[Store] Synced Codex history: imported=${result.imported}, skipped=${result.skipped}, failed=${result.failed}`);
586
+ return result;
587
+ }
588
+ // --- Cleanup ---
589
+ export function closeDb() {
590
+ if (db) {
591
+ db.close();
592
+ console.log('[Store] Database closed');
593
+ }
594
+ }