tlc-claude-code 1.2.26 → 1.2.28

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 (177) hide show
  1. package/dashboard/dist/components/ActivityFeed.d.ts +17 -0
  2. package/dashboard/dist/components/ActivityFeed.js +42 -0
  3. package/dashboard/dist/components/ActivityFeed.test.d.ts +1 -0
  4. package/dashboard/dist/components/ActivityFeed.test.js +162 -0
  5. package/dashboard/dist/components/BranchSelector.d.ts +16 -0
  6. package/dashboard/dist/components/BranchSelector.js +49 -0
  7. package/dashboard/dist/components/BranchSelector.test.d.ts +1 -0
  8. package/dashboard/dist/components/BranchSelector.test.js +166 -0
  9. package/dashboard/dist/components/CommandPalette.d.ts +17 -0
  10. package/dashboard/dist/components/CommandPalette.js +118 -0
  11. package/dashboard/dist/components/CommandPalette.test.d.ts +1 -0
  12. package/dashboard/dist/components/CommandPalette.test.js +181 -0
  13. package/dashboard/dist/components/ConnectionStatus.d.ts +16 -0
  14. package/dashboard/dist/components/ConnectionStatus.js +27 -0
  15. package/dashboard/dist/components/ConnectionStatus.test.d.ts +1 -0
  16. package/dashboard/dist/components/ConnectionStatus.test.js +121 -0
  17. package/dashboard/dist/components/DeviceFrame.d.ts +19 -0
  18. package/dashboard/dist/components/DeviceFrame.js +52 -0
  19. package/dashboard/dist/components/DeviceFrame.test.d.ts +1 -0
  20. package/dashboard/dist/components/DeviceFrame.test.js +118 -0
  21. package/dashboard/dist/components/EnvironmentBadge.d.ts +11 -0
  22. package/dashboard/dist/components/EnvironmentBadge.js +16 -0
  23. package/dashboard/dist/components/EnvironmentBadge.test.d.ts +1 -0
  24. package/dashboard/dist/components/EnvironmentBadge.test.js +102 -0
  25. package/dashboard/dist/components/FocusIndicator.d.ts +19 -0
  26. package/dashboard/dist/components/FocusIndicator.js +47 -0
  27. package/dashboard/dist/components/FocusIndicator.test.d.ts +1 -0
  28. package/dashboard/dist/components/FocusIndicator.test.js +117 -0
  29. package/dashboard/dist/components/KeyboardHelp.d.ts +15 -0
  30. package/dashboard/dist/components/KeyboardHelp.js +61 -0
  31. package/dashboard/dist/components/KeyboardHelp.test.d.ts +1 -0
  32. package/dashboard/dist/components/KeyboardHelp.test.js +131 -0
  33. package/dashboard/dist/components/LogSearch.d.ts +13 -0
  34. package/dashboard/dist/components/LogSearch.js +43 -0
  35. package/dashboard/dist/components/LogSearch.test.d.ts +1 -0
  36. package/dashboard/dist/components/LogSearch.test.js +100 -0
  37. package/dashboard/dist/components/LogStream.d.ts +21 -0
  38. package/dashboard/dist/components/LogStream.js +123 -0
  39. package/dashboard/dist/components/LogStream.test.d.ts +1 -0
  40. package/dashboard/dist/components/LogStream.test.js +159 -0
  41. package/dashboard/dist/components/PlanView.d.ts +7 -0
  42. package/dashboard/dist/components/PlanView.js +74 -2
  43. package/dashboard/dist/components/PlanView.test.js +70 -1
  44. package/dashboard/dist/components/PreviewPanel.d.ts +18 -0
  45. package/dashboard/dist/components/PreviewPanel.js +73 -0
  46. package/dashboard/dist/components/PreviewPanel.test.d.ts +1 -0
  47. package/dashboard/dist/components/PreviewPanel.test.js +124 -0
  48. package/dashboard/dist/components/ProjectCard.d.ts +18 -0
  49. package/dashboard/dist/components/ProjectCard.js +19 -0
  50. package/dashboard/dist/components/ProjectCard.test.d.ts +1 -0
  51. package/dashboard/dist/components/ProjectCard.test.js +53 -0
  52. package/dashboard/dist/components/ProjectDetail.d.ts +44 -0
  53. package/dashboard/dist/components/ProjectDetail.js +65 -0
  54. package/dashboard/dist/components/ProjectDetail.test.d.ts +1 -0
  55. package/dashboard/dist/components/ProjectDetail.test.js +196 -0
  56. package/dashboard/dist/components/ProjectList.d.ts +11 -0
  57. package/dashboard/dist/components/ProjectList.js +62 -0
  58. package/dashboard/dist/components/ProjectList.test.d.ts +1 -0
  59. package/dashboard/dist/components/ProjectList.test.js +93 -0
  60. package/dashboard/dist/components/SettingsPanel.d.ts +32 -0
  61. package/dashboard/dist/components/SettingsPanel.js +154 -0
  62. package/dashboard/dist/components/SettingsPanel.test.d.ts +1 -0
  63. package/dashboard/dist/components/SettingsPanel.test.js +196 -0
  64. package/dashboard/dist/components/StatusBar.d.ts +16 -0
  65. package/dashboard/dist/components/StatusBar.js +47 -0
  66. package/dashboard/dist/components/StatusBar.test.d.ts +1 -0
  67. package/dashboard/dist/components/StatusBar.test.js +123 -0
  68. package/dashboard/dist/components/TaskBoard.d.ts +22 -0
  69. package/dashboard/dist/components/TaskBoard.js +102 -0
  70. package/dashboard/dist/components/TaskBoard.test.d.ts +1 -0
  71. package/dashboard/dist/components/TaskBoard.test.js +113 -0
  72. package/dashboard/dist/components/TaskCard.d.ts +17 -0
  73. package/dashboard/dist/components/TaskCard.js +29 -0
  74. package/dashboard/dist/components/TaskCard.test.d.ts +1 -0
  75. package/dashboard/dist/components/TaskCard.test.js +109 -0
  76. package/dashboard/dist/components/TaskDetail.d.ts +36 -0
  77. package/dashboard/dist/components/TaskDetail.js +41 -0
  78. package/dashboard/dist/components/TaskDetail.test.d.ts +1 -0
  79. package/dashboard/dist/components/TaskDetail.test.js +164 -0
  80. package/dashboard/dist/components/TaskFilter.d.ts +12 -0
  81. package/dashboard/dist/components/TaskFilter.js +138 -0
  82. package/dashboard/dist/components/TaskFilter.test.d.ts +1 -0
  83. package/dashboard/dist/components/TaskFilter.test.js +109 -0
  84. package/dashboard/dist/components/TeamPanel.d.ts +15 -0
  85. package/dashboard/dist/components/TeamPanel.js +24 -0
  86. package/dashboard/dist/components/TeamPanel.test.d.ts +1 -0
  87. package/dashboard/dist/components/TeamPanel.test.js +109 -0
  88. package/dashboard/dist/components/TeamPresence.d.ts +14 -0
  89. package/dashboard/dist/components/TeamPresence.js +31 -0
  90. package/dashboard/dist/components/TeamPresence.test.d.ts +1 -0
  91. package/dashboard/dist/components/TeamPresence.test.js +144 -0
  92. package/dashboard/dist/components/layout/Header.d.ts +9 -0
  93. package/dashboard/dist/components/layout/Header.js +11 -0
  94. package/dashboard/dist/components/layout/Header.test.d.ts +1 -0
  95. package/dashboard/dist/components/layout/Header.test.js +35 -0
  96. package/dashboard/dist/components/layout/Shell.d.ts +10 -0
  97. package/dashboard/dist/components/layout/Shell.js +5 -0
  98. package/dashboard/dist/components/layout/Shell.test.d.ts +1 -0
  99. package/dashboard/dist/components/layout/Shell.test.js +34 -0
  100. package/dashboard/dist/components/layout/Sidebar.d.ts +14 -0
  101. package/dashboard/dist/components/layout/Sidebar.js +8 -0
  102. package/dashboard/dist/components/layout/Sidebar.test.d.ts +1 -0
  103. package/dashboard/dist/components/layout/Sidebar.test.js +40 -0
  104. package/dashboard/dist/components/ui/Badge.d.ts +9 -0
  105. package/dashboard/dist/components/ui/Badge.js +13 -0
  106. package/dashboard/dist/components/ui/Badge.test.d.ts +1 -0
  107. package/dashboard/dist/components/ui/Badge.test.js +69 -0
  108. package/dashboard/dist/components/ui/Button.d.ts +12 -0
  109. package/dashboard/dist/components/ui/Button.js +14 -0
  110. package/dashboard/dist/components/ui/Button.test.d.ts +1 -0
  111. package/dashboard/dist/components/ui/Button.test.js +81 -0
  112. package/dashboard/dist/components/ui/Card.d.ts +21 -0
  113. package/dashboard/dist/components/ui/Card.js +20 -0
  114. package/dashboard/dist/components/ui/Card.test.d.ts +1 -0
  115. package/dashboard/dist/components/ui/Card.test.js +82 -0
  116. package/dashboard/dist/components/ui/Input.d.ts +13 -0
  117. package/dashboard/dist/components/ui/Input.js +8 -0
  118. package/dashboard/dist/components/ui/Input.test.d.ts +1 -0
  119. package/dashboard/dist/components/ui/Input.test.js +68 -0
  120. package/dashboard/dist/styles/tokens.d.ts +150 -0
  121. package/dashboard/dist/styles/tokens.js +184 -0
  122. package/dashboard/dist/styles/tokens.test.d.ts +1 -0
  123. package/dashboard/dist/styles/tokens.test.js +95 -0
  124. package/dashboard/dist/test/setup.d.ts +1 -0
  125. package/dashboard/dist/test/setup.js +1 -0
  126. package/dashboard/package.json +3 -0
  127. package/package.json +1 -1
  128. package/server/dashboard/index.html +157 -2
  129. package/server/index.js +38 -21
  130. package/server/lib/adapters/base-adapter.js +114 -0
  131. package/server/lib/adapters/base-adapter.test.js +90 -0
  132. package/server/lib/adapters/claude-adapter.js +141 -0
  133. package/server/lib/adapters/claude-adapter.test.js +180 -0
  134. package/server/lib/adapters/deepseek-adapter.js +153 -0
  135. package/server/lib/adapters/deepseek-adapter.test.js +193 -0
  136. package/server/lib/adapters/openai-adapter.js +190 -0
  137. package/server/lib/adapters/openai-adapter.test.js +231 -0
  138. package/server/lib/budget-tracker.js +169 -0
  139. package/server/lib/budget-tracker.test.js +165 -0
  140. package/server/lib/claude-injector.js +85 -0
  141. package/server/lib/claude-injector.test.js +161 -0
  142. package/server/lib/consensus-engine.js +135 -0
  143. package/server/lib/consensus-engine.test.js +152 -0
  144. package/server/lib/context-builder.js +112 -0
  145. package/server/lib/context-builder.test.js +120 -0
  146. package/server/lib/file-collector.js +322 -0
  147. package/server/lib/file-collector.test.js +307 -0
  148. package/server/lib/memory-classifier.js +175 -0
  149. package/server/lib/memory-classifier.test.js +169 -0
  150. package/server/lib/memory-committer.js +138 -0
  151. package/server/lib/memory-committer.test.js +136 -0
  152. package/server/lib/memory-hooks.js +127 -0
  153. package/server/lib/memory-hooks.test.js +136 -0
  154. package/server/lib/memory-init.js +104 -0
  155. package/server/lib/memory-init.test.js +119 -0
  156. package/server/lib/memory-observer.js +149 -0
  157. package/server/lib/memory-observer.test.js +158 -0
  158. package/server/lib/memory-reader.js +243 -0
  159. package/server/lib/memory-reader.test.js +216 -0
  160. package/server/lib/memory-storage.js +120 -0
  161. package/server/lib/memory-storage.test.js +136 -0
  162. package/server/lib/memory-writer.js +176 -0
  163. package/server/lib/memory-writer.test.js +231 -0
  164. package/server/lib/overdrive-command.js +30 -6
  165. package/server/lib/overdrive-command.test.js +8 -1
  166. package/server/lib/pattern-detector.js +216 -0
  167. package/server/lib/pattern-detector.test.js +241 -0
  168. package/server/lib/relevance-scorer.js +175 -0
  169. package/server/lib/relevance-scorer.test.js +107 -0
  170. package/server/lib/review-command.js +238 -0
  171. package/server/lib/review-command.test.js +245 -0
  172. package/server/lib/review-orchestrator.js +273 -0
  173. package/server/lib/review-orchestrator.test.js +300 -0
  174. package/server/lib/review-reporter.js +288 -0
  175. package/server/lib/review-reporter.test.js +240 -0
  176. package/server/lib/session-summary.js +90 -0
  177. package/server/lib/session-summary.test.js +156 -0
@@ -0,0 +1,127 @@
1
+ /**
2
+ * Memory Hooks - Hook memory system into TLC command lifecycle
3
+ */
4
+
5
+ const { buildSessionContext } = require('./context-builder.js');
6
+ const { observeAndRemember, processExchange } = require('./memory-observer.js');
7
+ const { generateSessionSummary, formatSummary } = require('./session-summary.js');
8
+ const { appendSessionLog } = require('./memory-writer.js');
9
+
10
+ /**
11
+ * MemoryHooks class for stateful hook management
12
+ */
13
+ class MemoryHooks {
14
+ constructor(projectRoot) {
15
+ this.projectRoot = projectRoot;
16
+ this.sessionStartTime = null;
17
+ this.responseCount = 0;
18
+ }
19
+
20
+ /**
21
+ * Called when session starts - returns context for injection
22
+ * @returns {Promise<{context: string}>}
23
+ */
24
+ async onSessionStart() {
25
+ this.sessionStartTime = Date.now();
26
+ this.responseCount = 0;
27
+
28
+ const context = await buildSessionContext(this.projectRoot);
29
+
30
+ return { context };
31
+ }
32
+
33
+ /**
34
+ * Called after each response - observes for patterns
35
+ * @param {string} response - The response text
36
+ * @returns {Promise<{detected: boolean}>}
37
+ */
38
+ async onResponse(response) {
39
+ this.responseCount++;
40
+
41
+ // Create exchange object from response
42
+ const exchange = { assistant: response };
43
+
44
+ // Process the exchange for patterns (synchronous detection)
45
+ const classified = await processExchange(exchange);
46
+
47
+ // Fire and forget the storage (non-blocking)
48
+ observeAndRemember(this.projectRoot, exchange);
49
+
50
+ const detected = classified.decisions.length > 0 ||
51
+ classified.preferences.length > 0 ||
52
+ classified.gotchas.length > 0 ||
53
+ classified.reasoning.length > 0;
54
+
55
+ return { detected };
56
+ }
57
+
58
+ /**
59
+ * Called when session ends - returns summary
60
+ * @returns {Promise<{summary: string}>}
61
+ */
62
+ async onSessionEnd() {
63
+ const summaryData = await generateSessionSummary(this.projectRoot);
64
+ const summary = formatSummary(summaryData);
65
+
66
+ // Log session end
67
+ await appendSessionLog(this.projectRoot, {
68
+ type: 'session_end',
69
+ responseCount: this.responseCount,
70
+ duration: Date.now() - this.sessionStartTime,
71
+ });
72
+
73
+ return { summary };
74
+ }
75
+
76
+ /**
77
+ * Called before a command runs
78
+ * @param {string} command - Command name
79
+ * @returns {Promise<{command: string}>}
80
+ */
81
+ async beforeCommand(command) {
82
+ await appendSessionLog(this.projectRoot, {
83
+ type: 'command_start',
84
+ command,
85
+ });
86
+
87
+ return { command };
88
+ }
89
+
90
+ /**
91
+ * Called after a command runs
92
+ * @param {string} command - Command name
93
+ * @param {Object} result - Command result
94
+ * @returns {Promise<{logged: boolean}>}
95
+ */
96
+ async afterCommand(command, result) {
97
+ await appendSessionLog(this.projectRoot, {
98
+ type: 'command_end',
99
+ command,
100
+ success: result?.success,
101
+ });
102
+
103
+ return { logged: true };
104
+ }
105
+ }
106
+
107
+ /**
108
+ * Create memory hooks for a project
109
+ * @param {string} projectRoot - Project root directory
110
+ * @returns {Object} Hook functions
111
+ */
112
+ function createMemoryHooks(projectRoot) {
113
+ const hooks = new MemoryHooks(projectRoot);
114
+
115
+ return {
116
+ onSessionStart: () => hooks.onSessionStart(),
117
+ onResponse: (response) => hooks.onResponse(response),
118
+ onSessionEnd: () => hooks.onSessionEnd(),
119
+ beforeCommand: (command) => hooks.beforeCommand(command),
120
+ afterCommand: (command, result) => hooks.afterCommand(command, result),
121
+ };
122
+ }
123
+
124
+ module.exports = {
125
+ createMemoryHooks,
126
+ MemoryHooks,
127
+ };
@@ -0,0 +1,136 @@
1
+ import { describe, it, beforeEach, afterEach, expect, vi } from 'vitest';
2
+ import fs from 'fs';
3
+ import path from 'path';
4
+ import os from 'os';
5
+ import { createMemoryHooks, MemoryHooks } from './memory-hooks.js';
6
+ import { initMemoryStructure } from './memory-storage.js';
7
+
8
+ describe('memory-hooks', () => {
9
+ let testDir;
10
+
11
+ beforeEach(async () => {
12
+ testDir = fs.mkdtempSync(path.join(os.tmpdir(), 'tlc-hooks-test-'));
13
+ await initMemoryStructure(testDir);
14
+ });
15
+
16
+ afterEach(() => {
17
+ fs.rmSync(testDir, { recursive: true, force: true });
18
+ });
19
+
20
+ describe('createMemoryHooks', () => {
21
+ it('returns hooks object', () => {
22
+ const hooks = createMemoryHooks(testDir);
23
+
24
+ expect(hooks).toHaveProperty('onSessionStart');
25
+ expect(hooks).toHaveProperty('onResponse');
26
+ expect(hooks).toHaveProperty('onSessionEnd');
27
+ expect(hooks).toHaveProperty('beforeCommand');
28
+ expect(hooks).toHaveProperty('afterCommand');
29
+ });
30
+
31
+ it('hooks are callable functions', () => {
32
+ const hooks = createMemoryHooks(testDir);
33
+
34
+ expect(typeof hooks.onSessionStart).toBe('function');
35
+ expect(typeof hooks.onResponse).toBe('function');
36
+ expect(typeof hooks.onSessionEnd).toBe('function');
37
+ });
38
+ });
39
+
40
+ describe('onSessionStart', () => {
41
+ it('returns context for injection', async () => {
42
+ const hooks = createMemoryHooks(testDir);
43
+ const context = await hooks.onSessionStart();
44
+
45
+ expect(context).toHaveProperty('context');
46
+ });
47
+
48
+ it('returns empty context for new project', async () => {
49
+ const hooks = createMemoryHooks(testDir);
50
+ const result = await hooks.onSessionStart();
51
+
52
+ // New project has no memory, so context should be empty
53
+ expect(result.context).toBe('');
54
+ });
55
+ });
56
+
57
+ describe('onResponse', () => {
58
+ it('observes response for patterns', async () => {
59
+ const hooks = createMemoryHooks(testDir);
60
+
61
+ // Should not throw
62
+ await expect(hooks.onResponse('let\'s use PostgreSQL instead of MySQL')).resolves.not.toThrow();
63
+ });
64
+
65
+ it('returns detection results', async () => {
66
+ const hooks = createMemoryHooks(testDir);
67
+ const result = await hooks.onResponse('let\'s use PostgreSQL instead of MySQL');
68
+
69
+ expect(result).toHaveProperty('detected');
70
+ });
71
+ });
72
+
73
+ describe('onSessionEnd', () => {
74
+ it('returns session summary', async () => {
75
+ const hooks = createMemoryHooks(testDir);
76
+ const summary = await hooks.onSessionEnd();
77
+
78
+ expect(summary).toHaveProperty('summary');
79
+ });
80
+
81
+ it('includes formatted summary text', async () => {
82
+ const hooks = createMemoryHooks(testDir);
83
+ const result = await hooks.onSessionEnd();
84
+
85
+ expect(typeof result.summary).toBe('string');
86
+ });
87
+ });
88
+
89
+ describe('beforeCommand', () => {
90
+ it('runs without error', async () => {
91
+ const hooks = createMemoryHooks(testDir);
92
+
93
+ await expect(hooks.beforeCommand('build')).resolves.not.toThrow();
94
+ });
95
+
96
+ it('returns command context', async () => {
97
+ const hooks = createMemoryHooks(testDir);
98
+ const result = await hooks.beforeCommand('build');
99
+
100
+ expect(result).toHaveProperty('command');
101
+ expect(result.command).toBe('build');
102
+ });
103
+ });
104
+
105
+ describe('afterCommand', () => {
106
+ it('runs without error', async () => {
107
+ const hooks = createMemoryHooks(testDir);
108
+
109
+ await expect(hooks.afterCommand('build', { success: true })).resolves.not.toThrow();
110
+ });
111
+
112
+ it('logs command result', async () => {
113
+ const hooks = createMemoryHooks(testDir);
114
+ const result = await hooks.afterCommand('build', { success: true });
115
+
116
+ expect(result).toHaveProperty('logged');
117
+ });
118
+ });
119
+
120
+ describe('MemoryHooks class', () => {
121
+ it('can be instantiated', () => {
122
+ const hooks = new MemoryHooks(testDir);
123
+ expect(hooks).toBeInstanceOf(MemoryHooks);
124
+ });
125
+
126
+ it('shares state across calls', async () => {
127
+ const hooks = new MemoryHooks(testDir);
128
+
129
+ await hooks.onSessionStart();
130
+ await hooks.onResponse('test response');
131
+ const result = await hooks.onSessionEnd();
132
+
133
+ expect(result).toBeTruthy();
134
+ });
135
+ });
136
+ });
@@ -0,0 +1,104 @@
1
+ /**
2
+ * Memory Init - Initialize memory system directories
3
+ */
4
+
5
+ const fs = require('fs').promises;
6
+ const path = require('path');
7
+
8
+ /**
9
+ * Directory structure for memory system
10
+ */
11
+ const MEMORY_STRUCTURE = {
12
+ team: ['decisions', 'gotchas'],
13
+ local: ['preferences', 'sessions'],
14
+ };
15
+
16
+ /**
17
+ * Initialize the memory system directories
18
+ * @param {string} projectRoot - Project root directory
19
+ * @returns {Promise<{created: boolean, directories: string[]}>}
20
+ */
21
+ async function initMemorySystem(projectRoot) {
22
+ const memoryRoot = path.join(projectRoot, '.tlc', 'memory');
23
+ const created = [];
24
+
25
+ // Create team directories
26
+ for (const subdir of MEMORY_STRUCTURE.team) {
27
+ const dirPath = path.join(memoryRoot, 'team', subdir);
28
+ try {
29
+ await fs.mkdir(dirPath, { recursive: true });
30
+ created.push(dirPath);
31
+ } catch (err) {
32
+ if (err.code !== 'EEXIST') throw err;
33
+ }
34
+ }
35
+
36
+ // Create local directories
37
+ for (const subdir of MEMORY_STRUCTURE.local) {
38
+ const dirPath = path.join(memoryRoot, '.local', subdir);
39
+ try {
40
+ await fs.mkdir(dirPath, { recursive: true });
41
+ created.push(dirPath);
42
+ } catch (err) {
43
+ if (err.code !== 'EEXIST') throw err;
44
+ }
45
+ }
46
+
47
+ // Ensure .gitignore has .local entry
48
+ const gitignorePath = path.join(memoryRoot, '.gitignore');
49
+ let gitignoreContent = '';
50
+ try {
51
+ gitignoreContent = await fs.readFile(gitignorePath, 'utf-8');
52
+ } catch (err) {
53
+ if (err.code !== 'ENOENT') throw err;
54
+ }
55
+
56
+ if (!gitignoreContent.includes('.local')) {
57
+ const newContent = gitignoreContent
58
+ ? gitignoreContent.trim() + '\n.local\n'
59
+ : '.local\n';
60
+ await fs.writeFile(gitignorePath, newContent, 'utf-8');
61
+ }
62
+
63
+ return {
64
+ created: created.length > 0,
65
+ directories: created,
66
+ };
67
+ }
68
+
69
+ /**
70
+ * Check if memory system is initialized
71
+ * @param {string} projectRoot - Project root directory
72
+ * @returns {Promise<boolean>}
73
+ */
74
+ async function isMemoryInitialized(projectRoot) {
75
+ const memoryRoot = path.join(projectRoot, '.tlc', 'memory');
76
+
77
+ // Check team directories
78
+ for (const subdir of MEMORY_STRUCTURE.team) {
79
+ const dirPath = path.join(memoryRoot, 'team', subdir);
80
+ try {
81
+ await fs.access(dirPath);
82
+ } catch {
83
+ return false;
84
+ }
85
+ }
86
+
87
+ // Check local directories
88
+ for (const subdir of MEMORY_STRUCTURE.local) {
89
+ const dirPath = path.join(memoryRoot, '.local', subdir);
90
+ try {
91
+ await fs.access(dirPath);
92
+ } catch {
93
+ return false;
94
+ }
95
+ }
96
+
97
+ return true;
98
+ }
99
+
100
+ module.exports = {
101
+ initMemorySystem,
102
+ isMemoryInitialized,
103
+ MEMORY_STRUCTURE,
104
+ };
@@ -0,0 +1,119 @@
1
+ import { describe, it, beforeEach, afterEach, expect } from 'vitest';
2
+ import fs from 'fs';
3
+ import path from 'path';
4
+ import os from 'os';
5
+ import { initMemorySystem, isMemoryInitialized } from './memory-init.js';
6
+
7
+ describe('memory-init', () => {
8
+ let testDir;
9
+
10
+ beforeEach(() => {
11
+ testDir = fs.mkdtempSync(path.join(os.tmpdir(), 'tlc-memory-init-test-'));
12
+ });
13
+
14
+ afterEach(() => {
15
+ fs.rmSync(testDir, { recursive: true, force: true });
16
+ });
17
+
18
+ describe('initMemorySystem', () => {
19
+ it('creates .tlc/memory/team directory', async () => {
20
+ await initMemorySystem(testDir);
21
+
22
+ const teamDir = path.join(testDir, '.tlc', 'memory', 'team');
23
+ expect(fs.existsSync(teamDir)).toBe(true);
24
+ });
25
+
26
+ it('creates .tlc/memory/.local directory', async () => {
27
+ await initMemorySystem(testDir);
28
+
29
+ const localDir = path.join(testDir, '.tlc', 'memory', '.local');
30
+ expect(fs.existsSync(localDir)).toBe(true);
31
+ });
32
+
33
+ it('creates subdirectories in team', async () => {
34
+ await initMemorySystem(testDir);
35
+
36
+ const decisionsDir = path.join(testDir, '.tlc', 'memory', 'team', 'decisions');
37
+ const gotchasDir = path.join(testDir, '.tlc', 'memory', 'team', 'gotchas');
38
+ expect(fs.existsSync(decisionsDir)).toBe(true);
39
+ expect(fs.existsSync(gotchasDir)).toBe(true);
40
+ });
41
+
42
+ it('creates subdirectories in .local', async () => {
43
+ await initMemorySystem(testDir);
44
+
45
+ const preferencesDir = path.join(testDir, '.tlc', 'memory', '.local', 'preferences');
46
+ const sessionsDir = path.join(testDir, '.tlc', 'memory', '.local', 'sessions');
47
+ expect(fs.existsSync(preferencesDir)).toBe(true);
48
+ expect(fs.existsSync(sessionsDir)).toBe(true);
49
+ });
50
+
51
+ it('adds .local to .gitignore if not present', async () => {
52
+ await initMemorySystem(testDir);
53
+
54
+ const gitignorePath = path.join(testDir, '.tlc', 'memory', '.gitignore');
55
+ expect(fs.existsSync(gitignorePath)).toBe(true);
56
+ const content = fs.readFileSync(gitignorePath, 'utf-8');
57
+ expect(content).toContain('.local');
58
+ });
59
+
60
+ it('does not duplicate .local in .gitignore', async () => {
61
+ const gitignorePath = path.join(testDir, '.tlc', 'memory', '.gitignore');
62
+ fs.mkdirSync(path.dirname(gitignorePath), { recursive: true });
63
+ fs.writeFileSync(gitignorePath, '.local\n');
64
+
65
+ await initMemorySystem(testDir);
66
+
67
+ const content = fs.readFileSync(gitignorePath, 'utf-8');
68
+ const matches = content.match(/\.local/g);
69
+ expect(matches.length).toBe(1);
70
+ });
71
+
72
+ it('skips creation if already exists', async () => {
73
+ const teamDir = path.join(testDir, '.tlc', 'memory', 'team', 'decisions');
74
+ fs.mkdirSync(teamDir, { recursive: true });
75
+ fs.writeFileSync(path.join(teamDir, 'test.json'), '{"test": true}');
76
+
77
+ await initMemorySystem(testDir);
78
+
79
+ // File should still exist
80
+ expect(fs.existsSync(path.join(teamDir, 'test.json'))).toBe(true);
81
+ });
82
+
83
+ it('returns initialization status', async () => {
84
+ const result = await initMemorySystem(testDir);
85
+
86
+ expect(result).toHaveProperty('created');
87
+ expect(result).toHaveProperty('directories');
88
+ });
89
+
90
+ it('handles missing .tlc directory', async () => {
91
+ // Should create .tlc directory if not present
92
+ await initMemorySystem(testDir);
93
+
94
+ expect(fs.existsSync(path.join(testDir, '.tlc'))).toBe(true);
95
+ });
96
+ });
97
+
98
+ describe('isMemoryInitialized', () => {
99
+ it('returns false for empty directory', async () => {
100
+ const result = await isMemoryInitialized(testDir);
101
+ expect(result).toBe(false);
102
+ });
103
+
104
+ it('returns true after initialization', async () => {
105
+ await initMemorySystem(testDir);
106
+
107
+ const result = await isMemoryInitialized(testDir);
108
+ expect(result).toBe(true);
109
+ });
110
+
111
+ it('returns false for partial initialization', async () => {
112
+ // Only create team dir, not local
113
+ fs.mkdirSync(path.join(testDir, '.tlc', 'memory', 'team'), { recursive: true });
114
+
115
+ const result = await isMemoryInitialized(testDir);
116
+ expect(result).toBe(false);
117
+ });
118
+ });
119
+ });
@@ -0,0 +1,149 @@
1
+ /**
2
+ * Memory Observer - Non-blocking capture of memorable patterns from exchanges
3
+ */
4
+
5
+ const { detectPatterns } = require('./pattern-detector.js');
6
+ const { classifyMemory, CLASSIFICATION } = require('./memory-classifier.js');
7
+ const { writeTeamDecision, writeTeamGotcha, writePersonalPreference, appendSessionLog } = require('./memory-writer.js');
8
+
9
+ /**
10
+ * Process an exchange and extract patterns with classification
11
+ * @param {Object} exchange - Conversation exchange
12
+ * @returns {Object} Extracted patterns with classification
13
+ */
14
+ async function processExchange(exchange) {
15
+ const patterns = detectPatterns(exchange);
16
+
17
+ // Add classification to each pattern
18
+ const classified = {
19
+ decisions: patterns.decisions.map(d => ({
20
+ ...d,
21
+ classification: classifyMemory(d),
22
+ })),
23
+ preferences: patterns.preferences.map(p => ({
24
+ ...p,
25
+ classification: classifyMemory(p),
26
+ })),
27
+ gotchas: patterns.gotchas.map(g => ({
28
+ ...g,
29
+ classification: classifyMemory(g),
30
+ })),
31
+ reasoning: patterns.reasoning.map(r => ({
32
+ ...r,
33
+ classification: classifyMemory(r),
34
+ })),
35
+ };
36
+
37
+ return classified;
38
+ }
39
+
40
+ /**
41
+ * Store extracted patterns to appropriate storage
42
+ * @param {string} projectRoot - Project root directory
43
+ * @param {Object} classified - Classified patterns
44
+ */
45
+ async function storePatterns(projectRoot, classified) {
46
+ // Store team decisions
47
+ for (const decision of classified.decisions) {
48
+ if (decision.classification === CLASSIFICATION.TEAM) {
49
+ try {
50
+ await writeTeamDecision(projectRoot, {
51
+ title: decision.choice || 'Decision',
52
+ reasoning: decision.reasoning || decision.raw || '',
53
+ context: decision.over ? `Chosen over ${decision.over}` : undefined,
54
+ });
55
+ } catch (e) {
56
+ console.error('Failed to write team decision:', e.message);
57
+ }
58
+ }
59
+ }
60
+
61
+ // Store team gotchas
62
+ for (const gotcha of classified.gotchas) {
63
+ if (gotcha.classification === CLASSIFICATION.TEAM) {
64
+ try {
65
+ await writeTeamGotcha(projectRoot, {
66
+ title: gotcha.subject || 'Gotcha',
67
+ issue: gotcha.issue || gotcha.raw || '',
68
+ severity: 'medium',
69
+ });
70
+ } catch (e) {
71
+ console.error('Failed to write team gotcha:', e.message);
72
+ }
73
+ }
74
+ }
75
+
76
+ // Store personal preferences
77
+ for (const pref of classified.preferences) {
78
+ if (pref.classification === CLASSIFICATION.PERSONAL) {
79
+ try {
80
+ const key = pref.category || 'learned';
81
+ await writePersonalPreference(projectRoot, key, {
82
+ preference: pref.preference,
83
+ antiPreference: pref.antiPreference,
84
+ raw: pref.raw,
85
+ });
86
+ } catch (e) {
87
+ console.error('Failed to write personal preference:', e.message);
88
+ }
89
+ }
90
+ }
91
+ }
92
+
93
+ /**
94
+ * Log the exchange to session log
95
+ * @param {string} projectRoot - Project root directory
96
+ * @param {Object} classified - Classified patterns
97
+ */
98
+ async function logToSession(projectRoot, classified) {
99
+ const hasContent =
100
+ classified.decisions.length > 0 ||
101
+ classified.preferences.length > 0 ||
102
+ classified.gotchas.length > 0 ||
103
+ classified.reasoning.length > 0;
104
+
105
+ if (!hasContent) return;
106
+
107
+ try {
108
+ await appendSessionLog(projectRoot, {
109
+ type: 'memory_capture',
110
+ decisions: classified.decisions.length,
111
+ preferences: classified.preferences.length,
112
+ gotchas: classified.gotchas.length,
113
+ reasoning: classified.reasoning.length,
114
+ });
115
+ } catch (e) {
116
+ console.error('Failed to log to session:', e.message);
117
+ }
118
+ }
119
+
120
+ /**
121
+ * Observe an exchange and remember memorable patterns
122
+ * Fire-and-forget async - does not block
123
+ * @param {string} projectRoot - Project root directory
124
+ * @param {Object} exchange - Conversation exchange
125
+ */
126
+ async function observeAndRemember(projectRoot, exchange) {
127
+ // Fire and forget - don't await the full processing
128
+ setImmediate(async () => {
129
+ try {
130
+ const classified = await processExchange(exchange);
131
+
132
+ // Store patterns (errors are caught inside)
133
+ await storePatterns(projectRoot, classified);
134
+
135
+ // Log to session
136
+ await logToSession(projectRoot, classified);
137
+ } catch (e) {
138
+ // Silently fail - memory is nice-to-have, not critical
139
+ console.error('Memory observation failed:', e.message);
140
+ }
141
+ });
142
+ }
143
+
144
+ module.exports = {
145
+ observeAndRemember,
146
+ processExchange,
147
+ storePatterns,
148
+ logToSession,
149
+ };