k0ntext 3.6.0 → 3.8.0

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 (77) hide show
  1. package/README.md +281 -382
  2. package/dist/agent-system/timestamp-tracker.d.ts +159 -0
  3. package/dist/agent-system/timestamp-tracker.d.ts.map +1 -0
  4. package/dist/agent-system/timestamp-tracker.js +405 -0
  5. package/dist/agent-system/timestamp-tracker.js.map +1 -0
  6. package/dist/agent-system/todolist-manager.d.ts +244 -0
  7. package/dist/agent-system/todolist-manager.d.ts.map +1 -0
  8. package/dist/agent-system/todolist-manager.js +580 -0
  9. package/dist/agent-system/todolist-manager.js.map +1 -0
  10. package/dist/analyzer/intelligent-analyzer.d.ts +7 -0
  11. package/dist/analyzer/intelligent-analyzer.d.ts.map +1 -1
  12. package/dist/analyzer/intelligent-analyzer.js +46 -1
  13. package/dist/analyzer/intelligent-analyzer.js.map +1 -1
  14. package/dist/cli/commands/embeddings-refresh.d.ts.map +1 -1
  15. package/dist/cli/commands/embeddings-refresh.js +4 -1
  16. package/dist/cli/commands/embeddings-refresh.js.map +1 -1
  17. package/dist/cli/commands/migrate.d.ts.map +1 -1
  18. package/dist/cli/commands/migrate.js +8 -0
  19. package/dist/cli/commands/migrate.js.map +1 -1
  20. package/dist/cli/commands/snapshot.d.ts +28 -0
  21. package/dist/cli/commands/snapshot.d.ts.map +1 -0
  22. package/dist/cli/commands/snapshot.js +408 -0
  23. package/dist/cli/commands/snapshot.js.map +1 -0
  24. package/dist/cli/repl/init/wizard.d.ts.map +1 -1
  25. package/dist/cli/repl/init/wizard.js +12 -4
  26. package/dist/cli/repl/init/wizard.js.map +1 -1
  27. package/dist/cli/version/comparator.d.ts +1 -0
  28. package/dist/cli/version/comparator.d.ts.map +1 -1
  29. package/dist/cli/version/comparator.js +1 -0
  30. package/dist/cli/version/comparator.js.map +1 -1
  31. package/dist/db/client.d.ts +5 -0
  32. package/dist/db/client.d.ts.map +1 -1
  33. package/dist/db/client.js +7 -0
  34. package/dist/db/client.js.map +1 -1
  35. package/dist/db/schema.d.ts +1 -1
  36. package/dist/db/schema.js +1 -1
  37. package/dist/embeddings/openrouter.d.ts.map +1 -1
  38. package/dist/embeddings/openrouter.js +8 -3
  39. package/dist/embeddings/openrouter.js.map +1 -1
  40. package/dist/services/snapshot-manager.d.ts +251 -0
  41. package/dist/services/snapshot-manager.d.ts.map +1 -0
  42. package/dist/services/snapshot-manager.js +541 -0
  43. package/dist/services/snapshot-manager.js.map +1 -0
  44. package/dist/utils/chunking.d.ts +38 -0
  45. package/dist/utils/chunking.d.ts.map +1 -0
  46. package/dist/utils/chunking.js +133 -0
  47. package/dist/utils/chunking.js.map +1 -0
  48. package/dist/utils/encoding.d.ts +24 -0
  49. package/dist/utils/encoding.d.ts.map +1 -0
  50. package/dist/utils/encoding.js +32 -0
  51. package/dist/utils/encoding.js.map +1 -0
  52. package/dist/utils/index.d.ts +8 -0
  53. package/dist/utils/index.d.ts.map +1 -0
  54. package/dist/utils/index.js +8 -0
  55. package/dist/utils/index.js.map +1 -0
  56. package/docs/QUICKSTART.md +1 -1
  57. package/docs/TROUBLESHOOTING.md +51 -76
  58. package/docs/plans/2026-02-09-v3.7.0-database-fixes-and-improvements.md +900 -0
  59. package/docs/plans/2026-02-11-context-engineering-enhancement.md +1402 -0
  60. package/package.json +8 -2
  61. package/src/agent-system/timestamp-tracker.ts +520 -0
  62. package/src/agent-system/todolist-manager.ts +753 -0
  63. package/src/analyzer/intelligent-analyzer.ts +58 -1
  64. package/src/cli/commands/embeddings-refresh.ts +4 -1
  65. package/src/cli/commands/migrate.ts +8 -0
  66. package/src/cli/commands/snapshot.ts +471 -0
  67. package/src/cli/repl/init/wizard.ts +12 -4
  68. package/src/cli/version/comparator.ts +1 -0
  69. package/src/db/client.ts +8 -0
  70. package/src/db/migrations/0016_add_context_system_tables.sql +38 -0
  71. package/src/db/migrations/files/0015_add_sync_state_version_tracking.sql +18 -0
  72. package/src/db/schema.ts +1 -1
  73. package/src/embeddings/openrouter.ts +10 -4
  74. package/src/services/snapshot-manager.ts +719 -0
  75. package/src/utils/chunking.ts +152 -0
  76. package/src/utils/encoding.ts +33 -0
  77. package/src/utils/index.ts +8 -0
@@ -0,0 +1,753 @@
1
+ /**
2
+ * Todo List Manager
3
+ *
4
+ * Manages persistent todo lists that survive context compactions.
5
+ * Uses markdown file storage in .claude/todos/ directory.
6
+ */
7
+
8
+ import fs from 'fs/promises';
9
+ import path from 'path';
10
+ import crypto from 'crypto';
11
+ import type { DatabaseClient } from '../db/client.js';
12
+
13
+ /**
14
+ * Task status
15
+ */
16
+ export type TaskStatus = 'pending' | 'in-progress' | 'completed' | 'blocked' | 'cancelled';
17
+
18
+ /**
19
+ * Todo task
20
+ */
21
+ export interface TodoTask {
22
+ /** Unique task ID */
23
+ id: string;
24
+ /** Task title (imperative form) */
25
+ subject: string;
26
+ /** Detailed description */
27
+ description?: string;
28
+ /** Current status */
29
+ status: TaskStatus;
30
+ /** Tasks that must complete before this one */
31
+ dependencies?: string[];
32
+ /** Agent/person assigned to */
33
+ assignedTo?: string;
34
+ /** When task was created */
35
+ createdAt: string;
36
+ /** When task was last updated */
37
+ updatedAt: string;
38
+ /** When task was completed */
39
+ completedAt?: string;
40
+ }
41
+
42
+ /**
43
+ * Todo session
44
+ */
45
+ export interface TodoSession {
46
+ /** Unique session ID */
47
+ id: string;
48
+ /** Session name/title */
49
+ name: string;
50
+ /** Session status */
51
+ status: 'active' | 'completed' | 'archived';
52
+ /** Tasks in this session */
53
+ tasks: TodoTask[];
54
+ /** When session was created */
55
+ createdAt: string;
56
+ /** When session was last updated */
57
+ updatedAt: string;
58
+ /** Parent session ID if this is a continuation */
59
+ parentSession?: string;
60
+ /** Additional metadata */
61
+ metadata?: Record<string, unknown>;
62
+ }
63
+
64
+ /**
65
+ * Todo list manager options
66
+ */
67
+ export interface TodoListManagerOptions {
68
+ /** Base directory for todo storage */
69
+ baseDir?: string;
70
+ /** Verbose logging */
71
+ verbose?: boolean;
72
+ }
73
+
74
+ /**
75
+ * Progress update callback
76
+ */
77
+ export interface ProgressUpdate {
78
+ sessionId: string;
79
+ total: number;
80
+ completed: number;
81
+ percentage: number;
82
+ }
83
+
84
+ /**
85
+ * Todo List Manager
86
+ *
87
+ * Manages persistent todo lists stored in markdown files.
88
+ */
89
+ export class TodoListManager {
90
+ private db: DatabaseClient;
91
+ private baseDir: string;
92
+ private verbose: boolean;
93
+ private readonly TODO_DIR = 'todos';
94
+ private readonly ACTIVE_SUBDIR = 'active';
95
+ private readonly COMPLETED_SUBDIR = 'completed';
96
+ private readonly ARCHIVED_SUBDIR = 'archived';
97
+
98
+ constructor(db: DatabaseClient, options: TodoListManagerOptions = {}) {
99
+ this.db = db;
100
+ this.baseDir = options.baseDir || path.join(process.cwd(), '.claude');
101
+ this.verbose = options.verbose || false;
102
+ }
103
+
104
+ /**
105
+ * Get todos directory path
106
+ */
107
+ private getTodosDir(): string {
108
+ return path.join(this.baseDir, this.TODO_DIR);
109
+ }
110
+
111
+ /**
112
+ * Get active todos directory path
113
+ */
114
+ private getActiveDir(): string {
115
+ return path.join(this.getTodosDir(), this.ACTIVE_SUBDIR);
116
+ }
117
+
118
+ /**
119
+ * Get completed todos directory path
120
+ */
121
+ private getCompletedDir(): string {
122
+ return path.join(this.getTodosDir(), this.COMPLETED_SUBDIR);
123
+ }
124
+
125
+ /**
126
+ * Get archived todos directory path
127
+ */
128
+ private getArchivedDir(): string {
129
+ return path.join(this.getTodosDir(), this.ARCHIVED_SUBDIR);
130
+ }
131
+
132
+ /**
133
+ * Ensure all required directories exist
134
+ */
135
+ private async ensureDirectories(): Promise<void> {
136
+ const dirs = [
137
+ this.getTodosDir(),
138
+ this.getActiveDir(),
139
+ this.getCompletedDir(),
140
+ this.getArchivedDir()
141
+ ];
142
+
143
+ for (const dir of dirs) {
144
+ try {
145
+ await fs.mkdir(dir, { recursive: true });
146
+ } catch {
147
+ // Directory might already exist
148
+ }
149
+ }
150
+ }
151
+
152
+ /**
153
+ * Generate unique session ID
154
+ */
155
+ private generateSessionId(): string {
156
+ const date = new Date().toISOString().split('T')[0].replace(/-/g, '');
157
+ const random = crypto.randomBytes(4).toString('hex').substring(0, 8);
158
+ return `sess-${date}-${random}`;
159
+ }
160
+
161
+ /**
162
+ * Generate unique task ID
163
+ */
164
+ private generateTaskId(): string {
165
+ return crypto.randomBytes(8).toString('hex');
166
+ }
167
+
168
+ /**
169
+ * Get current timestamp
170
+ */
171
+ private now(): string {
172
+ return new Date().toISOString();
173
+ }
174
+
175
+ /**
176
+ * Create a new todo session
177
+ *
178
+ * @param name - Session name
179
+ * @param tasks - Initial tasks (optional)
180
+ * @returns Created session
181
+ */
182
+ async createSession(
183
+ name: string,
184
+ tasks: Array<Omit<TodoTask, 'id' | 'createdAt' | 'updatedAt'>> = []
185
+ ): Promise<TodoSession> {
186
+ await this.ensureDirectories();
187
+
188
+ const sessionId = this.generateSessionId();
189
+ const now = this.now();
190
+
191
+ const sessionTasks: TodoTask[] = tasks.map(task => ({
192
+ ...task,
193
+ id: this.generateTaskId(),
194
+ createdAt: now,
195
+ updatedAt: now
196
+ }));
197
+
198
+ const session: TodoSession = {
199
+ id: sessionId,
200
+ name,
201
+ status: 'active',
202
+ tasks: sessionTasks,
203
+ createdAt: now,
204
+ updatedAt: now
205
+ };
206
+
207
+ // Save to file
208
+ await this.saveSession(session);
209
+
210
+ // Store in database
211
+ this.storeSessionInDatabase(session);
212
+
213
+ if (this.verbose) {
214
+ console.log(`Created session: ${sessionId} with ${tasks.length} tasks`);
215
+ }
216
+
217
+ return session;
218
+ }
219
+
220
+ /**
221
+ * Get active session
222
+ *
223
+ * @returns Active session or null if none exists
224
+ */
225
+ async getActiveSession(): Promise<TodoSession | null> {
226
+ await this.ensureDirectories();
227
+
228
+ const files = await fs.readdir(this.getActiveDir());
229
+ const mdFiles = files.filter(f => f.endsWith('.md') && !f.startsWith('.'));
230
+
231
+ if (mdFiles.length === 0) {
232
+ return null;
233
+ }
234
+
235
+ // Get most recent session
236
+ const latestFile = mdFiles.sort().reverse()[0];
237
+ const content = await fs.readFile(path.join(this.getActiveDir(), latestFile), 'utf-8');
238
+ return this.parseSession(content);
239
+ }
240
+
241
+ /**
242
+ * Get session by ID
243
+ *
244
+ * @param sessionId - Session ID
245
+ * @returns Session or null if not found
246
+ */
247
+ async getSession(sessionId: string): Promise<TodoSession | null> {
248
+ // Try active directory first
249
+ let sessionPath = path.join(this.getActiveDir(), `${sessionId}.md`);
250
+ try {
251
+ const content = await fs.readFile(sessionPath, 'utf-8');
252
+ return this.parseSession(content);
253
+ } catch {
254
+ // Try completed directory
255
+ }
256
+
257
+ sessionPath = path.join(this.getCompletedDir(), `${sessionId}.md`);
258
+ try {
259
+ const content = await fs.readFile(sessionPath, 'utf-8');
260
+ return this.parseSession(content);
261
+ } catch {
262
+ return null;
263
+ }
264
+ }
265
+
266
+ /**
267
+ * List all sessions
268
+ *
269
+ * @param status - Filter by status (optional)
270
+ * @returns Array of sessions
271
+ */
272
+ async listSessions(status?: 'active' | 'completed' | 'archived'): Promise<TodoSession[]> {
273
+ await this.ensureDirectories();
274
+
275
+ const sessions: TodoSession[] = [];
276
+
277
+ if (!status || status === 'active') {
278
+ const activeFiles = await fs.readdir(this.getActiveDir());
279
+ for (const file of activeFiles.filter(f => f.endsWith('.md'))) {
280
+ const content = await fs.readFile(path.join(this.getActiveDir(), file), 'utf-8');
281
+ sessions.push(this.parseSession(content));
282
+ }
283
+ }
284
+
285
+ if (!status || status === 'completed') {
286
+ const completedFiles = await fs.readdir(this.getCompletedDir());
287
+ for (const file of completedFiles.filter(f => f.endsWith('.md'))) {
288
+ const content = await fs.readFile(path.join(this.getCompletedDir(), file), 'utf-8');
289
+ sessions.push(this.parseSession(content));
290
+ }
291
+ }
292
+
293
+ return sessions.sort((a, b) =>
294
+ new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime()
295
+ );
296
+ }
297
+
298
+ /**
299
+ * Update a task in the active session
300
+ *
301
+ * @param taskId - Task ID
302
+ * @param updates - Fields to update
303
+ * @returns Updated task or null if not found
304
+ */
305
+ async updateTask(taskId: string, updates: Partial<TodoTask>): Promise<TodoTask | null> {
306
+ const session = await this.getActiveSession();
307
+ if (!session) return null;
308
+
309
+ const task = session.tasks.find(t => t.id === taskId);
310
+ if (!task) return null;
311
+
312
+ const updatedTask = { ...task, ...updates, updatedAt: this.now() };
313
+ if (updates.status === 'completed' && !task.completedAt) {
314
+ updatedTask.completedAt = this.now();
315
+ }
316
+
317
+ session.tasks = session.tasks.map(t =>
318
+ t.id === taskId ? updatedTask : t
319
+ );
320
+ session.updatedAt = this.now();
321
+
322
+ await this.saveSession(session);
323
+ this.storeSessionInDatabase(session);
324
+
325
+ return updatedTask;
326
+ }
327
+
328
+ /**
329
+ * Add a task to the active session
330
+ *
331
+ * @param task - Task to add (without id)
332
+ * @returns Created task with ID
333
+ */
334
+ async addTask(task: Omit<TodoTask, 'id' | 'createdAt' | 'updatedAt'>): Promise<TodoTask> {
335
+ const session = await this.getActiveSession();
336
+ if (!session) {
337
+ // Create new session if none exists
338
+ const newSession = await this.createSession('New Session', [task]);
339
+ return newSession.tasks[0];
340
+ }
341
+
342
+ const newTask: TodoTask = {
343
+ ...task,
344
+ id: this.generateTaskId(),
345
+ createdAt: this.now(),
346
+ updatedAt: this.now()
347
+ };
348
+
349
+ session.tasks.push(newTask);
350
+ session.updatedAt = this.now();
351
+
352
+ await this.saveSession(session);
353
+ this.storeSessionInDatabase(session);
354
+
355
+ return newTask;
356
+ }
357
+
358
+ /**
359
+ * Complete the active session
360
+ *
361
+ * @returns Completed session or null if no active session
362
+ */
363
+ async completeSession(): Promise<TodoSession | null> {
364
+ const session = await this.getActiveSession();
365
+ if (!session) return null;
366
+
367
+ const now = this.now();
368
+ session.status = 'completed';
369
+ session.updatedAt = now;
370
+
371
+ // Mark incomplete tasks as blocked
372
+ session.tasks = session.tasks.map(t => {
373
+ if (t.status !== 'completed') {
374
+ return { ...t, status: 'blocked' as TaskStatus };
375
+ }
376
+ return t;
377
+ });
378
+
379
+ await this.saveSession(session);
380
+
381
+ // Move from active to completed
382
+ const activePath = path.join(this.getActiveDir(), `${session.id}.md`);
383
+ const completedPath = path.join(this.getCompletedDir(), `${session.id}.md`);
384
+ await fs.rename(activePath, completedPath);
385
+
386
+ this.storeSessionInDatabase(session);
387
+
388
+ return session;
389
+ }
390
+
391
+ /**
392
+ * Archive a session
393
+ *
394
+ * @param sessionId - Session ID to archive
395
+ * @returns True if archived successfully
396
+ */
397
+ async archiveSession(sessionId: string): Promise<boolean> {
398
+ const session = await this.getSession(sessionId);
399
+ if (!session) return false;
400
+
401
+ // Add to archive file
402
+ const archiveDate = new Date().toISOString().split('T')[0];
403
+ const archivePath = path.join(this.getArchivedDir(), `${archiveDate}.md`);
404
+ const archiveContent = await this.generateArchiveContent(session);
405
+
406
+ await fs.appendFile(archivePath, archiveContent + '\n\n---\n\n', 'utf-8');
407
+
408
+ // Remove from completed
409
+ const completedPath = path.join(this.getCompletedDir(), `${sessionId}.md`);
410
+ await fs.unlink(completedPath);
411
+
412
+ return true;
413
+ }
414
+
415
+ /**
416
+ * Get progress for active session
417
+ *
418
+ * @returns Progress update or null if no active session
419
+ */
420
+ async getProgress(): Promise<ProgressUpdate | null> {
421
+ const session = await this.getActiveSession();
422
+ if (!session) return null;
423
+
424
+ const total = session.tasks.length;
425
+ const completed = session.tasks.filter(t => t.status === 'completed').length;
426
+ const percentage = total > 0 ? Math.round((completed / total) * 100) : 0;
427
+
428
+ return {
429
+ sessionId: session.id,
430
+ total,
431
+ completed,
432
+ percentage
433
+ };
434
+ }
435
+
436
+ /**
437
+ * Save session to markdown file
438
+ */
439
+ private async saveSession(session: TodoSession): Promise<void> {
440
+ const content = await this.generateSessionContent(session);
441
+ const filePath = path.join(
442
+ session.status === 'active' ? this.getActiveDir() : this.getCompletedDir(),
443
+ `${session.id}.md`
444
+ );
445
+ await fs.writeFile(filePath, content, 'utf-8');
446
+ }
447
+
448
+ /**
449
+ * Generate session markdown content
450
+ */
451
+ private async generateSessionContent(session: TodoSession): Promise<string> {
452
+ const statusEmoji = {
453
+ active: '🔄',
454
+ completed: '✅',
455
+ archived: '📦'
456
+ };
457
+
458
+ const taskEmoji = (status: TaskStatus) => {
459
+ switch (status) {
460
+ case 'pending': return '[ ]';
461
+ case 'in-progress': return '[~]';
462
+ case 'completed': return '[x]';
463
+ case 'blocked': return '[-]';
464
+ case 'cancelled': return '[✗]';
465
+ }
466
+ };
467
+
468
+ let content = `# Session Todo: ${session.name}
469
+
470
+ **Session ID:** ${session.id}
471
+ **Created:** ${session.createdAt}
472
+ **Status:** ${session.status} ${statusEmoji[session.status]}
473
+
474
+ ## Tasks
475
+
476
+ `;
477
+
478
+ for (const task of session.tasks) {
479
+ content += `${taskEmoji(task.status)} Task ${task.id.substring(0, 8)}: ${task.subject}\n`;
480
+ if (task.description) {
481
+ content += ` ${task.description}\n`;
482
+ }
483
+ if (task.dependencies && task.dependencies.length > 0) {
484
+ content += ` Dependencies: ${task.dependencies.join(', ')}\n`;
485
+ }
486
+ content += '\n';
487
+ }
488
+
489
+ return content;
490
+ }
491
+
492
+ /**
493
+ * Generate archive entry content
494
+ */
495
+ private async generateArchiveContent(session: TodoSession): Promise<string> {
496
+ const total = session.tasks.length;
497
+ const completed = session.tasks.filter(t => t.status === 'completed').length;
498
+ const duration = new Date(session.updatedAt).getTime() - new Date(session.createdAt).getTime();
499
+ const durationMinutes = Math.round(duration / 60000);
500
+
501
+ return `## Session: ${session.name}
502
+
503
+ **Session ID:** ${session.id}
504
+ **Status:** Completed ✅
505
+ **Duration:** ${durationMinutes}m
506
+ **Tasks:** ${completed}/${total} completed (${Math.round((completed / total) * 100)}%)
507
+
508
+ **Summary:**
509
+ All tasks for "${session.name}" completed at ${session.updatedAt}.
510
+ `;
511
+ }
512
+
513
+ /**
514
+ * Parse session from markdown content
515
+ */
516
+ private parseSession(content: string): TodoSession {
517
+ const lines = content.split('\n');
518
+
519
+ let id = '';
520
+ let name = 'Unknown Session';
521
+ let status: TodoSession['status'] = 'active';
522
+ let createdAt = new Date().toISOString();
523
+ let updatedAt = new Date().toISOString();
524
+
525
+ const tasks: TodoTask[] = [];
526
+
527
+ for (const line of lines) {
528
+ // Parse session metadata
529
+ const sessionMatch = line.match(/^\*\*Session ID:\s*(.+)$/);
530
+ if (sessionMatch) id = sessionMatch[1];
531
+
532
+ const nameMatch = line.match(/^#\s+Session Todo:\s*(.+)$/);
533
+ if (nameMatch) name = nameMatch[1];
534
+
535
+ const statusMatch = line.match(/^\*\*Status:\s*(\w+)\s/);
536
+ if (statusMatch) {
537
+ if (statusMatch[1].includes('active')) status = 'active';
538
+ else if (statusMatch[1].includes('completed')) status = 'completed';
539
+ else status = 'archived';
540
+ }
541
+
542
+ const createdMatch = line.match(/^\*\*Created:\s*(.+)$/);
543
+ if (createdMatch) createdAt = createdMatch[1];
544
+
545
+ // Parse tasks
546
+ const taskMatch = line.match(/^[\[\~x\s\-\]]\s+Task\s+([a-f0-9]+):\s*(.+)$/);
547
+ if (taskMatch) {
548
+ const taskId = taskMatch[1];
549
+ const subject = taskMatch[2];
550
+ const taskStatus: TaskStatus = line.includes('[x]') ? 'completed' :
551
+ line.includes('[~]') ? 'in-progress' :
552
+ line.includes('[-]') ? 'blocked' :
553
+ line.includes('[✗]') ? 'cancelled' :
554
+ 'pending';
555
+ tasks.push({
556
+ id: taskId,
557
+ subject,
558
+ status: taskStatus,
559
+ createdAt,
560
+ updatedAt
561
+ });
562
+ }
563
+ }
564
+
565
+ return {
566
+ id,
567
+ name,
568
+ status,
569
+ tasks,
570
+ createdAt,
571
+ updatedAt
572
+ };
573
+ }
574
+
575
+ /**
576
+ * Store session in database
577
+ *
578
+ * Uses todo_sessions and todo_tasks tables for efficient querying.
579
+ */
580
+ private storeSessionInDatabase(session: TodoSession): void {
581
+ try {
582
+ // Check if schema supports todo tables
583
+ this.db.prepare(
584
+ 'INSERT OR REPLACE INTO todo_sessions (id, name, created_at, updated_at, parent_session, metadata) VALUES (?, ?, ?, ?, ?, ?)'
585
+ ).run(session.id, session.name, session.createdAt, session.updatedAt, session.parentSession, JSON.stringify(session.metadata || {}));
586
+
587
+ // Store tasks
588
+ for (const task of session.tasks) {
589
+ this.db.prepare(
590
+ 'INSERT OR REPLACE INTO todo_tasks (id, session_id, subject, description, status, dependencies, assigned_to, created_at, updated_at, completed_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)'
591
+ ).run(task.id, session.id, task.subject, task.description || '', task.status,
592
+ JSON.stringify(task.dependencies || []), task.assignedTo || '',
593
+ task.createdAt, task.updatedAt, task.completedAt);
594
+ }
595
+
596
+ if (this.verbose) {
597
+ console.log(`Stored session ${session.id} in database with ${session.tasks.length} tasks`);
598
+ }
599
+ } catch (error) {
600
+ // Database might not have todo tables yet (migration 0016)
601
+ if (this.verbose) {
602
+ console.warn(`Could not store session in database: ${error}`);
603
+ }
604
+ }
605
+ }
606
+
607
+ /**
608
+ * Get session from database
609
+ *
610
+ * @param sessionId - Session ID
611
+ * @returns Session with tasks or null
612
+ */
613
+ private getSessionFromDatabase(sessionId: string): TodoSession | null {
614
+ try {
615
+ const sessionRow = this.db.prepare(
616
+ 'SELECT * FROM todo_sessions WHERE id = ?'
617
+ ).get(sessionId) as Record<string, unknown> | undefined;
618
+
619
+ if (!sessionRow) return null;
620
+
621
+ const session = sessionRow;
622
+
623
+ const tasksRows = this.db.prepare(
624
+ 'SELECT * FROM todo_tasks WHERE session_id = ? ORDER BY created_at ASC'
625
+ ).all(sessionId) as unknown[];
626
+
627
+ return {
628
+ id: typeof session.id === 'string' ? session.id : sessionId,
629
+ name: typeof session.name === 'string' ? session.name : 'Unknown',
630
+ status: (typeof session.status === 'string' && ['active', 'completed', 'archived'].includes(session.status))
631
+ ? session.status as TodoSession['status'] : 'active',
632
+ createdAt: typeof session.created_at === 'string' ? session.created_at : new Date().toISOString(),
633
+ updatedAt: typeof session.updated_at === 'string' ? session.updated_at : new Date().toISOString(),
634
+ parentSession: typeof session.parent_session === 'string' ? session.parent_session : undefined,
635
+ metadata: typeof session.metadata === 'string' ? JSON.parse(session.metadata) :
636
+ (session.metadata && typeof session.metadata === 'object') ? session.metadata as Record<string, unknown> : undefined,
637
+ tasks: tasksRows.map((t: unknown) => {
638
+ const task = t as Record<string, unknown>;
639
+ return {
640
+ id: typeof task.id === 'string' ? task.id : this.generateTaskId(),
641
+ subject: typeof task.subject === 'string' ? task.subject : '',
642
+ description: typeof task.description === 'string' ? task.description : undefined,
643
+ status: (typeof task.status === 'string' && ['pending', 'in-progress', 'completed', 'blocked', 'cancelled'].includes(task.status))
644
+ ? task.status as TodoTask['status'] : 'pending',
645
+ dependencies: typeof task.dependencies === 'string' ? JSON.parse(task.dependencies) :
646
+ (task.dependencies && Array.isArray(task.dependencies)) ? task.dependencies as string[] : undefined,
647
+ assignedTo: typeof task.assigned_to === 'string' ? task.assigned_to : undefined,
648
+ createdAt: typeof task.created_at === 'string' ? task.created_at : new Date().toISOString(),
649
+ updatedAt: typeof task.updated_at === 'string' ? task.updated_at : new Date().toISOString(),
650
+ completedAt: typeof task.completed_at === 'string' ? task.completed_at : undefined
651
+ };
652
+ })
653
+ };
654
+ } catch (error) {
655
+ if (this.verbose) {
656
+ console.warn(`Could not read session from database: ${error}`);
657
+ }
658
+ return null;
659
+ }
660
+ }
661
+
662
+ /**
663
+ * Export session as markdown
664
+ *
665
+ * @param sessionId - Session ID
666
+ * @returns Markdown content
667
+ */
668
+ async exportSession(sessionId: string): Promise<string | null> {
669
+ const session = await this.getSession(sessionId);
670
+ if (!session) return null;
671
+ return await this.generateSessionContent(session);
672
+ }
673
+
674
+ /**
675
+ * Import tasks from markdown
676
+ *
677
+ * @param markdownContent - Markdown content
678
+ * @returns Created session
679
+ */
680
+ async importTasks(markdownContent: string, sessionName?: string): Promise<TodoSession> {
681
+ const imported = this.parseSession(markdownContent);
682
+ const tasks = imported.tasks.map(t => ({
683
+ subject: t.subject,
684
+ description: t.description,
685
+ status: t.status
686
+ }));
687
+
688
+ return await this.createSession(sessionName || 'Imported Session', tasks);
689
+ }
690
+
691
+ /**
692
+ * Clean up old archived sessions
693
+ *
694
+ * @param daysOld - Remove archives older than this many days
695
+ * @returns Number of archives removed
696
+ */
697
+ async cleanupOldArchives(daysOld: number = 30): Promise<number> {
698
+ const archiveDir = this.getArchivedDir();
699
+ const files = await fs.readdir(archiveDir);
700
+ const cutoff = Date.now() - (daysOld * 24 * 60 * 60 * 1000);
701
+
702
+ let removed = 0;
703
+
704
+ for (const file of files) {
705
+ if (!file.endsWith('.md')) continue;
706
+
707
+ const filePath = path.join(archiveDir, file);
708
+ const stats = await fs.stat(filePath);
709
+
710
+ if (stats.mtime.getTime() < cutoff) {
711
+ await fs.unlink(filePath);
712
+ removed++;
713
+ if (this.verbose) {
714
+ console.log(`Removed old archive: ${file}`);
715
+ }
716
+ }
717
+ }
718
+
719
+ return removed;
720
+ }
721
+
722
+ /**
723
+ * Get all sessions for reporting
724
+ *
725
+ * @returns Summary of all sessions
726
+ */
727
+ async getSummary(): Promise<{
728
+ active: number;
729
+ completed: number;
730
+ totalTasks: number;
731
+ completedTasks: number;
732
+ }> {
733
+ const allSessions = await this.listSessions();
734
+
735
+ const activeCount = allSessions.filter(s => s.status === 'active').length;
736
+ const completedCount = allSessions.filter(s => s.status === 'completed').length;
737
+
738
+ let totalTasks = 0;
739
+ let completedTasks = 0;
740
+
741
+ for (const session of allSessions) {
742
+ totalTasks += session.tasks.length;
743
+ completedTasks += session.tasks.filter(t => t.status === 'completed').length;
744
+ }
745
+
746
+ return {
747
+ active: activeCount,
748
+ completed: completedCount,
749
+ totalTasks,
750
+ completedTasks
751
+ };
752
+ }
753
+ }