neuronlayer 0.1.9 → 0.2.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.

Potentially problematic release.


This version of neuronlayer might be problematic. Click here for more details.

Files changed (81) hide show
  1. package/README.md +3 -2
  2. package/dist/index.js +172 -90
  3. package/dist/index.js.map +7 -0
  4. package/package.json +6 -1
  5. package/esbuild.config.js +0 -26
  6. package/src/cli/commands.ts +0 -573
  7. package/src/core/adr-exporter.ts +0 -253
  8. package/src/core/architecture/architecture-enforcement.ts +0 -228
  9. package/src/core/architecture/duplicate-detector.ts +0 -288
  10. package/src/core/architecture/index.ts +0 -6
  11. package/src/core/architecture/pattern-learner.ts +0 -306
  12. package/src/core/architecture/pattern-library.ts +0 -403
  13. package/src/core/architecture/pattern-validator.ts +0 -324
  14. package/src/core/change-intelligence/bug-correlator.ts +0 -544
  15. package/src/core/change-intelligence/change-intelligence.ts +0 -264
  16. package/src/core/change-intelligence/change-tracker.ts +0 -334
  17. package/src/core/change-intelligence/fix-suggester.ts +0 -340
  18. package/src/core/change-intelligence/index.ts +0 -5
  19. package/src/core/code-verifier.ts +0 -843
  20. package/src/core/confidence/confidence-scorer.ts +0 -251
  21. package/src/core/confidence/conflict-checker.ts +0 -289
  22. package/src/core/confidence/index.ts +0 -5
  23. package/src/core/confidence/source-tracker.ts +0 -263
  24. package/src/core/confidence/warning-detector.ts +0 -241
  25. package/src/core/context-rot/compaction.ts +0 -284
  26. package/src/core/context-rot/context-health.ts +0 -243
  27. package/src/core/context-rot/context-rot-prevention.ts +0 -213
  28. package/src/core/context-rot/critical-context.ts +0 -221
  29. package/src/core/context-rot/drift-detector.ts +0 -255
  30. package/src/core/context-rot/index.ts +0 -7
  31. package/src/core/context.ts +0 -263
  32. package/src/core/decision-extractor.ts +0 -339
  33. package/src/core/decisions.ts +0 -69
  34. package/src/core/deja-vu.ts +0 -421
  35. package/src/core/engine.ts +0 -1646
  36. package/src/core/feature-context.ts +0 -726
  37. package/src/core/ghost-mode.ts +0 -465
  38. package/src/core/learning.ts +0 -519
  39. package/src/core/living-docs/activity-tracker.ts +0 -296
  40. package/src/core/living-docs/architecture-generator.ts +0 -428
  41. package/src/core/living-docs/changelog-generator.ts +0 -348
  42. package/src/core/living-docs/component-generator.ts +0 -230
  43. package/src/core/living-docs/doc-engine.ts +0 -134
  44. package/src/core/living-docs/doc-validator.ts +0 -282
  45. package/src/core/living-docs/index.ts +0 -8
  46. package/src/core/project-manager.ts +0 -301
  47. package/src/core/refresh/activity-gate.ts +0 -256
  48. package/src/core/refresh/git-staleness-checker.ts +0 -108
  49. package/src/core/refresh/index.ts +0 -27
  50. package/src/core/summarizer.ts +0 -290
  51. package/src/core/test-awareness/change-validator.ts +0 -499
  52. package/src/core/test-awareness/index.ts +0 -5
  53. package/src/index.ts +0 -90
  54. package/src/indexing/ast.ts +0 -868
  55. package/src/indexing/embeddings.ts +0 -85
  56. package/src/indexing/indexer.ts +0 -270
  57. package/src/indexing/watcher.ts +0 -78
  58. package/src/server/gateways/aggregator.ts +0 -374
  59. package/src/server/gateways/index.ts +0 -473
  60. package/src/server/gateways/memory-ghost.ts +0 -343
  61. package/src/server/gateways/memory-query.ts +0 -452
  62. package/src/server/gateways/memory-record.ts +0 -346
  63. package/src/server/gateways/memory-review.ts +0 -410
  64. package/src/server/gateways/memory-status.ts +0 -517
  65. package/src/server/gateways/memory-verify.ts +0 -392
  66. package/src/server/gateways/router.ts +0 -434
  67. package/src/server/gateways/types.ts +0 -610
  68. package/src/server/http.ts +0 -228
  69. package/src/server/mcp.ts +0 -154
  70. package/src/server/resources.ts +0 -85
  71. package/src/server/tools.ts +0 -2460
  72. package/src/storage/database.ts +0 -271
  73. package/src/storage/tier1.ts +0 -135
  74. package/src/storage/tier2.ts +0 -972
  75. package/src/storage/tier3.ts +0 -123
  76. package/src/types/documentation.ts +0 -619
  77. package/src/types/index.ts +0 -222
  78. package/src/utils/config.ts +0 -194
  79. package/src/utils/files.ts +0 -117
  80. package/src/utils/time.ts +0 -37
  81. package/src/utils/tokens.ts +0 -52
@@ -1,726 +0,0 @@
1
- import { EventEmitter } from 'events';
2
- import { readFileSync, writeFileSync, existsSync, mkdirSync } from 'fs';
3
- import { dirname, join, basename } from 'path';
4
- import { randomUUID } from 'crypto';
5
- import type { ActiveFeatureContext, FeatureFile, FeatureChange, FeatureQuery, HotContext } from '../types/index.js';
6
-
7
- /**
8
- * Context Resurrection - Restore mental state from last session
9
- * "Last time you worked on auth, you were stuck on X"
10
- */
11
- export interface ResurrectedContext {
12
- /** What files were you working on? */
13
- activeFiles: string[];
14
- /** What were you trying to do? */
15
- lastQueries: string[];
16
- /** What decisions were made during this context? */
17
- sessionDecisions: string[];
18
- /** Where did you leave off? */
19
- lastEditedFile: string | null;
20
- lastEditTime: Date | null;
21
- /** What was the blocker (if any)? */
22
- possibleBlocker: string | null;
23
- /** Suggested next steps */
24
- suggestedActions: string[];
25
- /** Context summary for AI */
26
- summary: string;
27
- /** Time since last activity */
28
- timeSinceLastActive: string;
29
- }
30
-
31
- export interface ContextResurrectionOptions {
32
- /** Specific feature name to resurrect */
33
- featureName?: string;
34
- /** Include file contents in resurrection */
35
- includeFileContents?: boolean;
36
- /** Maximum files to include */
37
- maxFiles?: number;
38
- }
39
-
40
- const MAX_FILES = 20;
41
- const MAX_CHANGES = 50;
42
- const MAX_QUERIES = 20;
43
- const MAX_RECENT_CONTEXTS = 5;
44
- const TTL_MINUTES = 30;
45
- const HOT_CACHE_MAX_FILES = 15;
46
-
47
- export class FeatureContextManager extends EventEmitter {
48
- private current: ActiveFeatureContext | null = null;
49
- private recent: ActiveFeatureContext[] = [];
50
- private fileContents: Map<string, string> = new Map();
51
- private projectPath: string;
52
- private dataDir: string;
53
- private persistPath: string;
54
- private inactivityTimer: NodeJS.Timeout | null = null;
55
-
56
- constructor(projectPath: string, dataDir: string) {
57
- super();
58
- this.projectPath = projectPath;
59
- this.dataDir = dataDir;
60
- this.persistPath = join(dataDir, 'feature-context.json');
61
- this.load();
62
- this.startInactivityTimer();
63
- }
64
-
65
- // ========== FILE TRACKING ==========
66
-
67
- onFileOpened(filePath: string): void {
68
- this.ensureContext();
69
- this.touchFile(filePath);
70
- this.preloadFile(filePath);
71
- }
72
-
73
- onFileEdited(filePath: string, diff: string, linesChanged: number[] = []): void {
74
- this.ensureContext();
75
- this.touchFile(filePath);
76
- this.recordChange(filePath, diff, linesChanged);
77
- }
78
-
79
- private touchFile(filePath: string): void {
80
- if (!this.current) return;
81
-
82
- // Normalize path to relative
83
- const relativePath = this.toRelativePath(filePath);
84
- const existing = this.current.files.find(f => f.path === relativePath);
85
-
86
- if (existing) {
87
- existing.lastTouched = new Date();
88
- existing.touchCount++;
89
- } else {
90
- this.current.files.push({
91
- path: relativePath,
92
- lastTouched: new Date(),
93
- touchCount: 1,
94
- recentLines: []
95
- });
96
-
97
- // Trim if over limit - keep most touched files
98
- if (this.current.files.length > MAX_FILES) {
99
- this.current.files = this.current.files
100
- .sort((a, b) => b.touchCount - a.touchCount)
101
- .slice(0, MAX_FILES);
102
- }
103
- }
104
-
105
- this.current.lastActiveAt = new Date();
106
- this.save();
107
- }
108
-
109
- // ========== CHANGE TRACKING ==========
110
-
111
- private recordChange(filePath: string, diff: string, linesChanged: number[]): void {
112
- if (!this.current) return;
113
-
114
- const relativePath = this.toRelativePath(filePath);
115
-
116
- this.current.changes.unshift({
117
- file: relativePath,
118
- timestamp: new Date(),
119
- diff: diff.slice(0, 200), // Limit diff size
120
- linesChanged
121
- });
122
-
123
- // Trim old changes
124
- if (this.current.changes.length > MAX_CHANGES) {
125
- this.current.changes = this.current.changes.slice(0, MAX_CHANGES);
126
- }
127
-
128
- // Update file's recent lines
129
- const fileEntry = this.current.files.find(f => f.path === relativePath);
130
- if (fileEntry && linesChanged.length > 0) {
131
- fileEntry.recentLines = linesChanged.slice(0, 10);
132
- }
133
-
134
- this.save();
135
- }
136
-
137
- // ========== QUERY TRACKING ==========
138
-
139
- onQuery(query: string, filesUsed: string[]): void {
140
- this.ensureContext();
141
- if (!this.current) return;
142
-
143
- const relativeFiles = filesUsed.map(f => this.toRelativePath(f));
144
-
145
- this.current.queries.unshift({
146
- query,
147
- timestamp: new Date(),
148
- filesUsed: relativeFiles
149
- });
150
-
151
- // Trim old queries
152
- if (this.current.queries.length > MAX_QUERIES) {
153
- this.current.queries = this.current.queries.slice(0, MAX_QUERIES);
154
- }
155
-
156
- // Touch files that were used
157
- relativeFiles.forEach(f => this.touchFile(f));
158
-
159
- this.current.lastActiveAt = new Date();
160
- this.save();
161
- }
162
-
163
- // ========== HOT CACHE ==========
164
-
165
- private async preloadFile(filePath: string): Promise<void> {
166
- const relativePath = this.toRelativePath(filePath);
167
-
168
- if (this.fileContents.has(relativePath)) return;
169
-
170
- try {
171
- const absolutePath = this.toAbsolutePath(relativePath);
172
- const content = readFileSync(absolutePath, 'utf-8');
173
- this.fileContents.set(relativePath, content);
174
-
175
- // Trim cache if too large
176
- if (this.fileContents.size > HOT_CACHE_MAX_FILES && this.current) {
177
- // Find least recently touched file and remove it
178
- const filesInContext = new Set(this.current.files.map(f => f.path));
179
- for (const [path] of this.fileContents) {
180
- if (!filesInContext.has(path)) {
181
- this.fileContents.delete(path);
182
- break;
183
- }
184
- }
185
- }
186
- } catch {
187
- // File might not exist or be unreadable
188
- }
189
- }
190
-
191
- getFileContent(filePath: string): string | null {
192
- const relativePath = this.toRelativePath(filePath);
193
- return this.fileContents.get(relativePath) || null;
194
- }
195
-
196
- // ========== CONTEXT RETRIEVAL ==========
197
-
198
- getHotContext(): HotContext {
199
- if (!this.current) {
200
- return { files: [], changes: [], queries: [], summary: '' };
201
- }
202
-
203
- // Rank files by touchCount * recency
204
- const now = Date.now();
205
- const rankedFiles = this.current.files
206
- .map(f => ({
207
- ...f,
208
- score: f.touchCount * (1 / (now - f.lastTouched.getTime() + 1))
209
- }))
210
- .sort((a, b) => b.score - a.score)
211
- .slice(0, 10);
212
-
213
- return {
214
- files: rankedFiles.map(f => ({
215
- path: f.path,
216
- content: this.fileContents.get(f.path) || null,
217
- touchCount: f.touchCount
218
- })),
219
- changes: this.current.changes.slice(0, 10),
220
- queries: this.current.queries.slice(0, 5),
221
- summary: this.generateSummary()
222
- };
223
- }
224
-
225
- private generateSummary(): string {
226
- if (!this.current) return '';
227
-
228
- const topFiles = this.current.files
229
- .sort((a, b) => b.touchCount - a.touchCount)
230
- .slice(0, 5)
231
- .map(f => basename(f.path))
232
- .join(', ');
233
-
234
- const recentChanges = this.current.changes.length;
235
- const durationMs = Date.now() - this.current.startedAt.getTime();
236
- const durationMin = Math.round(durationMs / 60000);
237
-
238
- if (topFiles) {
239
- return `Working on: ${topFiles} | ${recentChanges} changes | ${durationMin} min`;
240
- }
241
-
242
- return `Session active | ${durationMin} min`;
243
- }
244
-
245
- getCurrentContext(): ActiveFeatureContext | null {
246
- return this.current;
247
- }
248
-
249
- getRecentContexts(): ActiveFeatureContext[] {
250
- return this.recent;
251
- }
252
-
253
- getCurrentSummary(): { name: string; files: number; changes: number; duration: number } | null {
254
- if (!this.current) return null;
255
-
256
- return {
257
- name: this.current.name,
258
- files: this.current.files.length,
259
- changes: this.current.changes.length,
260
- duration: Math.round((Date.now() - this.current.startedAt.getTime()) / 60000)
261
- };
262
- }
263
-
264
- // ========== CONTEXT MANAGEMENT ==========
265
-
266
- private ensureContext(): void {
267
- if (!this.current || this.current.status !== 'active') {
268
- this.startNewContext();
269
- }
270
- }
271
-
272
- startNewContext(name?: string): ActiveFeatureContext {
273
- // Save current context to recent
274
- if (this.current) {
275
- this.current.status = 'paused';
276
- this.recent.unshift(this.current);
277
- this.recent = this.recent.slice(0, MAX_RECENT_CONTEXTS);
278
- }
279
-
280
- this.current = {
281
- id: randomUUID(),
282
- name: name || 'Untitled Feature',
283
- files: [],
284
- changes: [],
285
- queries: [],
286
- startedAt: new Date(),
287
- lastActiveAt: new Date(),
288
- status: 'active'
289
- };
290
-
291
- // Clear hot cache for new context
292
- this.fileContents.clear();
293
- this.save();
294
-
295
- this.emit('context-started', this.current);
296
- return this.current;
297
- }
298
-
299
- setContextName(name: string): boolean {
300
- if (!this.current) return false;
301
- this.current.name = name;
302
- this.save();
303
- return true;
304
- }
305
-
306
- switchToRecent(contextId: string): boolean {
307
- const found = this.recent.find(c => c.id === contextId);
308
- if (!found) return false;
309
-
310
- // Save current to recent
311
- if (this.current) {
312
- this.current.status = 'paused';
313
- this.recent.unshift(this.current);
314
- }
315
-
316
- // Remove found from recent and make it current
317
- this.recent = this.recent.filter(c => c.id !== contextId);
318
- this.current = found;
319
- this.current.status = 'active';
320
- this.current.lastActiveAt = new Date();
321
-
322
- // Reload files into hot cache
323
- this.reloadFiles();
324
- this.save();
325
-
326
- this.emit('context-switched', this.current);
327
- return true;
328
- }
329
-
330
- private reloadFiles(): void {
331
- this.fileContents.clear();
332
- if (!this.current) return;
333
-
334
- // Preload top 10 files
335
- const topFiles = this.current.files
336
- .sort((a, b) => b.touchCount - a.touchCount)
337
- .slice(0, 10);
338
-
339
- for (const file of topFiles) {
340
- this.preloadFile(file.path);
341
- }
342
- }
343
-
344
- completeContext(): boolean {
345
- if (!this.current) return false;
346
- this.current.status = 'completed';
347
- this.recent.unshift(this.current);
348
- this.recent = this.recent.slice(0, MAX_RECENT_CONTEXTS);
349
- this.current = null;
350
- this.fileContents.clear();
351
- this.save();
352
- return true;
353
- }
354
-
355
- // ========== AUTO MANAGEMENT ==========
356
-
357
- private startInactivityTimer(): void {
358
- if (this.inactivityTimer) {
359
- clearInterval(this.inactivityTimer);
360
- }
361
-
362
- this.inactivityTimer = setInterval(() => {
363
- if (!this.current) return;
364
-
365
- const inactiveMs = Date.now() - this.current.lastActiveAt.getTime();
366
- if (inactiveMs > TTL_MINUTES * 60 * 1000) {
367
- this.current.status = 'paused';
368
- this.emit('context-paused', this.current);
369
- this.save();
370
- }
371
- }, 60000); // Check every minute
372
- }
373
-
374
- // ========== PERSISTENCE ==========
375
-
376
- private save(): void {
377
- try {
378
- const dir = dirname(this.persistPath);
379
- if (!existsSync(dir)) {
380
- mkdirSync(dir, { recursive: true });
381
- }
382
-
383
- const data = {
384
- current: this.current,
385
- recent: this.recent
386
- };
387
-
388
- writeFileSync(this.persistPath, JSON.stringify(data, null, 2));
389
- } catch (error) {
390
- console.error('Error saving feature context:', error);
391
- }
392
- }
393
-
394
- private load(): void {
395
- try {
396
- if (existsSync(this.persistPath)) {
397
- const data = JSON.parse(readFileSync(this.persistPath, 'utf-8'));
398
-
399
- // Parse dates in current context
400
- if (data.current) {
401
- this.current = this.parseContext(data.current);
402
- }
403
-
404
- // Parse dates in recent contexts
405
- if (data.recent) {
406
- this.recent = data.recent.map((c: ActiveFeatureContext) => this.parseContext(c));
407
- }
408
-
409
- // Reload files if we have a current context
410
- if (this.current && this.current.status === 'active') {
411
- this.reloadFiles();
412
- }
413
- }
414
- } catch (error) {
415
- console.error('Error loading feature context:', error);
416
- this.current = null;
417
- this.recent = [];
418
- }
419
- }
420
-
421
- private parseContext(data: ActiveFeatureContext): ActiveFeatureContext {
422
- return {
423
- ...data,
424
- startedAt: new Date(data.startedAt),
425
- lastActiveAt: new Date(data.lastActiveAt),
426
- files: data.files.map(f => ({
427
- ...f,
428
- lastTouched: new Date(f.lastTouched)
429
- })),
430
- changes: data.changes.map(c => ({
431
- ...c,
432
- timestamp: new Date(c.timestamp)
433
- })),
434
- queries: data.queries.map(q => ({
435
- ...q,
436
- timestamp: new Date(q.timestamp)
437
- }))
438
- };
439
- }
440
-
441
- // ========== PATH UTILITIES ==========
442
-
443
- private toRelativePath(filePath: string): string {
444
- if (filePath.startsWith(this.projectPath)) {
445
- return filePath.slice(this.projectPath.length).replace(/^[\/\\]/, '');
446
- }
447
- return filePath;
448
- }
449
-
450
- private toAbsolutePath(relativePath: string): string {
451
- if (relativePath.startsWith(this.projectPath)) {
452
- return relativePath;
453
- }
454
- return join(this.projectPath, relativePath);
455
- }
456
-
457
- // ========== CONTEXT RESURRECTION ==========
458
-
459
- /**
460
- * Resurrect context from last session - restore mental state
461
- * Returns what you were working on, where you left off, and possible blockers
462
- */
463
- resurrectContext(options: ContextResurrectionOptions = {}): ResurrectedContext {
464
- const { featureName, maxFiles = 5 } = options;
465
-
466
- // Find the context to resurrect
467
- let contextToResurrect: ActiveFeatureContext | null = null;
468
-
469
- if (featureName) {
470
- // Find by name
471
- contextToResurrect = this.recent.find(c =>
472
- c.name.toLowerCase().includes(featureName.toLowerCase())
473
- ) || null;
474
-
475
- if (!contextToResurrect && this.current?.name.toLowerCase().includes(featureName.toLowerCase())) {
476
- contextToResurrect = this.current;
477
- }
478
- } else {
479
- // Use most recent context (current or first in recent)
480
- contextToResurrect = this.current || this.recent[0] || null;
481
- }
482
-
483
- if (!contextToResurrect) {
484
- return {
485
- activeFiles: [],
486
- lastQueries: [],
487
- sessionDecisions: [],
488
- lastEditedFile: null,
489
- lastEditTime: null,
490
- possibleBlocker: null,
491
- suggestedActions: ['Start a new feature context with memory_record'],
492
- summary: 'No previous context found to resurrect.',
493
- timeSinceLastActive: 'N/A',
494
- };
495
- }
496
-
497
- // Extract information from context
498
- const activeFiles = contextToResurrect.files
499
- .sort((a, b) => b.touchCount - a.touchCount)
500
- .slice(0, maxFiles)
501
- .map(f => f.path);
502
-
503
- const lastQueries = contextToResurrect.queries
504
- .slice(0, 3)
505
- .map(q => q.query);
506
-
507
- // Find last edited file (most recent change)
508
- const lastChange = contextToResurrect.changes[0];
509
- const lastEditedFile = lastChange?.file || null;
510
- const lastEditTime = lastChange?.timestamp || null;
511
-
512
- // Detect possible blocker
513
- const possibleBlocker = this.detectBlocker(contextToResurrect);
514
-
515
- // Generate suggested actions
516
- const suggestedActions = this.suggestNextSteps(contextToResurrect);
517
-
518
- // Calculate time since last active
519
- const timeSinceLastActive = this.formatTimeSince(contextToResurrect.lastActiveAt);
520
-
521
- // Generate summary
522
- const summary = this.generateResurrectionSummary(contextToResurrect, possibleBlocker);
523
-
524
- return {
525
- activeFiles,
526
- lastQueries,
527
- sessionDecisions: [], // Would need to cross-reference with decisions storage
528
- lastEditedFile,
529
- lastEditTime,
530
- possibleBlocker,
531
- suggestedActions,
532
- summary,
533
- timeSinceLastActive,
534
- };
535
- }
536
-
537
- /**
538
- * Detect what might have been blocking progress when session ended
539
- */
540
- private detectBlocker(context: ActiveFeatureContext): string | null {
541
- if (context.queries.length === 0) {
542
- return null;
543
- }
544
-
545
- // Check last few queries for error/problem patterns
546
- const errorPatterns = [
547
- /error/i,
548
- /fix/i,
549
- /bug/i,
550
- /issue/i,
551
- /problem/i,
552
- /not working/i,
553
- /doesn't work/i,
554
- /how to/i,
555
- /why/i,
556
- /failed/i,
557
- /broken/i,
558
- ];
559
-
560
- // Check last 3 queries
561
- for (let i = 0; i < Math.min(3, context.queries.length); i++) {
562
- const query = context.queries[i];
563
- if (!query) continue;
564
-
565
- for (const pattern of errorPatterns) {
566
- if (pattern.test(query.query)) {
567
- return query.query;
568
- }
569
- }
570
- }
571
-
572
- // Check if the same file was touched many times (sign of struggling)
573
- const fileTouchCounts = new Map<string, number>();
574
- for (const change of context.changes.slice(0, 10)) {
575
- const count = fileTouchCounts.get(change.file) || 0;
576
- fileTouchCounts.set(change.file, count + 1);
577
- }
578
-
579
- for (const [file, count] of fileTouchCounts.entries()) {
580
- if (count >= 5) {
581
- return `Multiple edits to ${basename(file)} - possible implementation challenge`;
582
- }
583
- }
584
-
585
- return null;
586
- }
587
-
588
- /**
589
- * Suggest next steps based on context state
590
- */
591
- private suggestNextSteps(context: ActiveFeatureContext): string[] {
592
- const suggestions: string[] = [];
593
-
594
- // If there was a potential blocker, suggest addressing it
595
- const blocker = this.detectBlocker(context);
596
- if (blocker) {
597
- suggestions.push(`Resume investigating: "${blocker.slice(0, 50)}..."`);
598
- }
599
-
600
- // Suggest continuing with most-touched files
601
- const topFiles = context.files
602
- .sort((a, b) => b.touchCount - a.touchCount)
603
- .slice(0, 2);
604
-
605
- if (topFiles.length > 0 && topFiles[0]) {
606
- suggestions.push(`Continue working on ${basename(topFiles[0].path)}`);
607
- }
608
-
609
- // If there were recent changes, suggest reviewing them
610
- if (context.changes.length > 0) {
611
- suggestions.push('Review recent changes before continuing');
612
- }
613
-
614
- // Generic suggestions if nothing specific
615
- if (suggestions.length === 0) {
616
- suggestions.push(`Continue "${context.name}" feature work`);
617
- }
618
-
619
- return suggestions;
620
- }
621
-
622
- /**
623
- * Format time since a date in human-readable form
624
- */
625
- private formatTimeSince(date: Date): string {
626
- const now = new Date();
627
- const diffMs = now.getTime() - date.getTime();
628
- const diffMins = Math.floor(diffMs / (1000 * 60));
629
- const diffHours = Math.floor(diffMs / (1000 * 60 * 60));
630
- const diffDays = Math.floor(diffMs / (1000 * 60 * 60 * 24));
631
-
632
- if (diffMins < 60) {
633
- return `${diffMins} minutes ago`;
634
- } else if (diffHours < 24) {
635
- return `${diffHours} hours ago`;
636
- } else if (diffDays === 1) {
637
- return 'yesterday';
638
- } else if (diffDays < 7) {
639
- return `${diffDays} days ago`;
640
- } else if (diffDays < 30) {
641
- const weeks = Math.floor(diffDays / 7);
642
- return `${weeks} week${weeks > 1 ? 's' : ''} ago`;
643
- } else {
644
- const months = Math.floor(diffDays / 30);
645
- return `${months} month${months > 1 ? 's' : ''} ago`;
646
- }
647
- }
648
-
649
- /**
650
- * Generate a summary of the resurrection context for AI
651
- */
652
- private generateResurrectionSummary(context: ActiveFeatureContext, blocker: string | null): string {
653
- const parts: string[] = [];
654
-
655
- // Feature name and duration
656
- const durationMs = context.lastActiveAt.getTime() - context.startedAt.getTime();
657
- const durationMins = Math.round(durationMs / 60000);
658
- parts.push(`Feature: "${context.name}" (${durationMins} min session)`);
659
-
660
- // Status
661
- parts.push(`Status: ${context.status}`);
662
-
663
- // Files worked on
664
- if (context.files.length > 0) {
665
- const topFiles = context.files
666
- .sort((a, b) => b.touchCount - a.touchCount)
667
- .slice(0, 3)
668
- .map(f => basename(f.path));
669
- parts.push(`Main files: ${topFiles.join(', ')}`);
670
- }
671
-
672
- // Changes made
673
- if (context.changes.length > 0) {
674
- parts.push(`Changes: ${context.changes.length} edits`);
675
- }
676
-
677
- // Blocker
678
- if (blocker) {
679
- parts.push(`Possible blocker: "${blocker.slice(0, 50)}..."`);
680
- }
681
-
682
- // Last activity
683
- parts.push(`Last active: ${this.formatTimeSince(context.lastActiveAt)}`);
684
-
685
- return parts.join(' | ');
686
- }
687
-
688
- /**
689
- * Get all contexts that can be resurrected
690
- */
691
- getResurrectableContexts(): Array<{ id: string; name: string; lastActive: Date; summary: string }> {
692
- const contexts: Array<{ id: string; name: string; lastActive: Date; summary: string }> = [];
693
-
694
- // Include current if paused
695
- if (this.current && this.current.status === 'paused') {
696
- contexts.push({
697
- id: this.current.id,
698
- name: this.current.name,
699
- lastActive: this.current.lastActiveAt,
700
- summary: this.generateResurrectionSummary(this.current, null),
701
- });
702
- }
703
-
704
- // Include recent contexts
705
- for (const ctx of this.recent) {
706
- contexts.push({
707
- id: ctx.id,
708
- name: ctx.name,
709
- lastActive: ctx.lastActiveAt,
710
- summary: this.generateResurrectionSummary(ctx, null),
711
- });
712
- }
713
-
714
- return contexts.sort((a, b) => b.lastActive.getTime() - a.lastActive.getTime());
715
- }
716
-
717
- // ========== CLEANUP ==========
718
-
719
- shutdown(): void {
720
- if (this.inactivityTimer) {
721
- clearInterval(this.inactivityTimer);
722
- this.inactivityTimer = null;
723
- }
724
- this.save();
725
- }
726
- }