tlc-claude-code 1.2.27 → 1.2.29

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 (179) hide show
  1. package/README.md +9 -4
  2. package/dashboard/dist/components/ActivityFeed.d.ts +17 -0
  3. package/dashboard/dist/components/ActivityFeed.js +42 -0
  4. package/dashboard/dist/components/ActivityFeed.test.d.ts +1 -0
  5. package/dashboard/dist/components/ActivityFeed.test.js +162 -0
  6. package/dashboard/dist/components/BranchSelector.d.ts +16 -0
  7. package/dashboard/dist/components/BranchSelector.js +49 -0
  8. package/dashboard/dist/components/BranchSelector.test.d.ts +1 -0
  9. package/dashboard/dist/components/BranchSelector.test.js +166 -0
  10. package/dashboard/dist/components/CommandPalette.d.ts +17 -0
  11. package/dashboard/dist/components/CommandPalette.js +118 -0
  12. package/dashboard/dist/components/CommandPalette.test.d.ts +1 -0
  13. package/dashboard/dist/components/CommandPalette.test.js +181 -0
  14. package/dashboard/dist/components/ConnectionStatus.d.ts +16 -0
  15. package/dashboard/dist/components/ConnectionStatus.js +27 -0
  16. package/dashboard/dist/components/ConnectionStatus.test.d.ts +1 -0
  17. package/dashboard/dist/components/ConnectionStatus.test.js +121 -0
  18. package/dashboard/dist/components/DeviceFrame.d.ts +19 -0
  19. package/dashboard/dist/components/DeviceFrame.js +52 -0
  20. package/dashboard/dist/components/DeviceFrame.test.d.ts +1 -0
  21. package/dashboard/dist/components/DeviceFrame.test.js +118 -0
  22. package/dashboard/dist/components/EnvironmentBadge.d.ts +11 -0
  23. package/dashboard/dist/components/EnvironmentBadge.js +16 -0
  24. package/dashboard/dist/components/EnvironmentBadge.test.d.ts +1 -0
  25. package/dashboard/dist/components/EnvironmentBadge.test.js +102 -0
  26. package/dashboard/dist/components/FocusIndicator.d.ts +19 -0
  27. package/dashboard/dist/components/FocusIndicator.js +47 -0
  28. package/dashboard/dist/components/FocusIndicator.test.d.ts +1 -0
  29. package/dashboard/dist/components/FocusIndicator.test.js +117 -0
  30. package/dashboard/dist/components/KeyboardHelp.d.ts +15 -0
  31. package/dashboard/dist/components/KeyboardHelp.js +61 -0
  32. package/dashboard/dist/components/KeyboardHelp.test.d.ts +1 -0
  33. package/dashboard/dist/components/KeyboardHelp.test.js +131 -0
  34. package/dashboard/dist/components/LogSearch.d.ts +13 -0
  35. package/dashboard/dist/components/LogSearch.js +43 -0
  36. package/dashboard/dist/components/LogSearch.test.d.ts +1 -0
  37. package/dashboard/dist/components/LogSearch.test.js +100 -0
  38. package/dashboard/dist/components/LogStream.d.ts +21 -0
  39. package/dashboard/dist/components/LogStream.js +123 -0
  40. package/dashboard/dist/components/LogStream.test.d.ts +1 -0
  41. package/dashboard/dist/components/LogStream.test.js +159 -0
  42. package/dashboard/dist/components/PreviewPanel.d.ts +18 -0
  43. package/dashboard/dist/components/PreviewPanel.js +73 -0
  44. package/dashboard/dist/components/PreviewPanel.test.d.ts +1 -0
  45. package/dashboard/dist/components/PreviewPanel.test.js +124 -0
  46. package/dashboard/dist/components/ProjectCard.d.ts +18 -0
  47. package/dashboard/dist/components/ProjectCard.js +19 -0
  48. package/dashboard/dist/components/ProjectCard.test.d.ts +1 -0
  49. package/dashboard/dist/components/ProjectCard.test.js +53 -0
  50. package/dashboard/dist/components/ProjectDetail.d.ts +44 -0
  51. package/dashboard/dist/components/ProjectDetail.js +65 -0
  52. package/dashboard/dist/components/ProjectDetail.test.d.ts +1 -0
  53. package/dashboard/dist/components/ProjectDetail.test.js +196 -0
  54. package/dashboard/dist/components/ProjectList.d.ts +11 -0
  55. package/dashboard/dist/components/ProjectList.js +62 -0
  56. package/dashboard/dist/components/ProjectList.test.d.ts +1 -0
  57. package/dashboard/dist/components/ProjectList.test.js +93 -0
  58. package/dashboard/dist/components/SettingsPanel.d.ts +32 -0
  59. package/dashboard/dist/components/SettingsPanel.js +154 -0
  60. package/dashboard/dist/components/SettingsPanel.test.d.ts +1 -0
  61. package/dashboard/dist/components/SettingsPanel.test.js +196 -0
  62. package/dashboard/dist/components/StatusBar.d.ts +16 -0
  63. package/dashboard/dist/components/StatusBar.js +47 -0
  64. package/dashboard/dist/components/StatusBar.test.d.ts +1 -0
  65. package/dashboard/dist/components/StatusBar.test.js +123 -0
  66. package/dashboard/dist/components/TaskBoard.d.ts +22 -0
  67. package/dashboard/dist/components/TaskBoard.js +102 -0
  68. package/dashboard/dist/components/TaskBoard.test.d.ts +1 -0
  69. package/dashboard/dist/components/TaskBoard.test.js +113 -0
  70. package/dashboard/dist/components/TaskCard.d.ts +17 -0
  71. package/dashboard/dist/components/TaskCard.js +29 -0
  72. package/dashboard/dist/components/TaskCard.test.d.ts +1 -0
  73. package/dashboard/dist/components/TaskCard.test.js +109 -0
  74. package/dashboard/dist/components/TaskDetail.d.ts +36 -0
  75. package/dashboard/dist/components/TaskDetail.js +41 -0
  76. package/dashboard/dist/components/TaskDetail.test.d.ts +1 -0
  77. package/dashboard/dist/components/TaskDetail.test.js +164 -0
  78. package/dashboard/dist/components/TaskFilter.d.ts +12 -0
  79. package/dashboard/dist/components/TaskFilter.js +138 -0
  80. package/dashboard/dist/components/TaskFilter.test.d.ts +1 -0
  81. package/dashboard/dist/components/TaskFilter.test.js +109 -0
  82. package/dashboard/dist/components/TeamPanel.d.ts +15 -0
  83. package/dashboard/dist/components/TeamPanel.js +24 -0
  84. package/dashboard/dist/components/TeamPanel.test.d.ts +1 -0
  85. package/dashboard/dist/components/TeamPanel.test.js +109 -0
  86. package/dashboard/dist/components/TeamPresence.d.ts +14 -0
  87. package/dashboard/dist/components/TeamPresence.js +31 -0
  88. package/dashboard/dist/components/TeamPresence.test.d.ts +1 -0
  89. package/dashboard/dist/components/TeamPresence.test.js +144 -0
  90. package/dashboard/dist/components/layout/Header.d.ts +9 -0
  91. package/dashboard/dist/components/layout/Header.js +11 -0
  92. package/dashboard/dist/components/layout/Header.test.d.ts +1 -0
  93. package/dashboard/dist/components/layout/Header.test.js +35 -0
  94. package/dashboard/dist/components/layout/Shell.d.ts +10 -0
  95. package/dashboard/dist/components/layout/Shell.js +5 -0
  96. package/dashboard/dist/components/layout/Shell.test.d.ts +1 -0
  97. package/dashboard/dist/components/layout/Shell.test.js +34 -0
  98. package/dashboard/dist/components/layout/Sidebar.d.ts +14 -0
  99. package/dashboard/dist/components/layout/Sidebar.js +8 -0
  100. package/dashboard/dist/components/layout/Sidebar.test.d.ts +1 -0
  101. package/dashboard/dist/components/layout/Sidebar.test.js +40 -0
  102. package/dashboard/dist/components/ui/Badge.d.ts +9 -0
  103. package/dashboard/dist/components/ui/Badge.js +13 -0
  104. package/dashboard/dist/components/ui/Badge.test.d.ts +1 -0
  105. package/dashboard/dist/components/ui/Badge.test.js +69 -0
  106. package/dashboard/dist/components/ui/Button.d.ts +12 -0
  107. package/dashboard/dist/components/ui/Button.js +14 -0
  108. package/dashboard/dist/components/ui/Button.test.d.ts +1 -0
  109. package/dashboard/dist/components/ui/Button.test.js +81 -0
  110. package/dashboard/dist/components/ui/Card.d.ts +21 -0
  111. package/dashboard/dist/components/ui/Card.js +20 -0
  112. package/dashboard/dist/components/ui/Card.test.d.ts +1 -0
  113. package/dashboard/dist/components/ui/Card.test.js +82 -0
  114. package/dashboard/dist/components/ui/Input.d.ts +13 -0
  115. package/dashboard/dist/components/ui/Input.js +8 -0
  116. package/dashboard/dist/components/ui/Input.test.d.ts +1 -0
  117. package/dashboard/dist/components/ui/Input.test.js +68 -0
  118. package/dashboard/dist/styles/tokens.d.ts +150 -0
  119. package/dashboard/dist/styles/tokens.js +184 -0
  120. package/dashboard/dist/styles/tokens.test.d.ts +1 -0
  121. package/dashboard/dist/styles/tokens.test.js +95 -0
  122. package/dashboard/dist/test/setup.d.ts +1 -0
  123. package/dashboard/dist/test/setup.js +1 -0
  124. package/dashboard/package.json +3 -0
  125. package/package.json +15 -4
  126. package/scripts/capture-screenshots.js +170 -0
  127. package/scripts/docs-update.js +253 -0
  128. package/scripts/generate-screenshots.js +321 -0
  129. package/scripts/project-docs.js +377 -0
  130. package/scripts/vps-setup.sh +477 -0
  131. package/server/lib/adapters/base-adapter.js +114 -0
  132. package/server/lib/adapters/base-adapter.test.js +90 -0
  133. package/server/lib/adapters/claude-adapter.js +141 -0
  134. package/server/lib/adapters/claude-adapter.test.js +180 -0
  135. package/server/lib/adapters/deepseek-adapter.js +153 -0
  136. package/server/lib/adapters/deepseek-adapter.test.js +193 -0
  137. package/server/lib/adapters/openai-adapter.js +190 -0
  138. package/server/lib/adapters/openai-adapter.test.js +231 -0
  139. package/server/lib/budget-tracker.js +169 -0
  140. package/server/lib/budget-tracker.test.js +165 -0
  141. package/server/lib/claude-injector.js +85 -0
  142. package/server/lib/claude-injector.test.js +161 -0
  143. package/server/lib/consensus-engine.js +135 -0
  144. package/server/lib/consensus-engine.test.js +152 -0
  145. package/server/lib/context-builder.js +112 -0
  146. package/server/lib/context-builder.test.js +120 -0
  147. package/server/lib/file-collector.js +322 -0
  148. package/server/lib/file-collector.test.js +307 -0
  149. package/server/lib/memory-classifier.js +175 -0
  150. package/server/lib/memory-classifier.test.js +169 -0
  151. package/server/lib/memory-committer.js +138 -0
  152. package/server/lib/memory-committer.test.js +136 -0
  153. package/server/lib/memory-hooks.js +127 -0
  154. package/server/lib/memory-hooks.test.js +136 -0
  155. package/server/lib/memory-init.js +104 -0
  156. package/server/lib/memory-init.test.js +119 -0
  157. package/server/lib/memory-observer.js +149 -0
  158. package/server/lib/memory-observer.test.js +158 -0
  159. package/server/lib/memory-reader.js +243 -0
  160. package/server/lib/memory-reader.test.js +216 -0
  161. package/server/lib/memory-storage.js +120 -0
  162. package/server/lib/memory-storage.test.js +136 -0
  163. package/server/lib/memory-writer.js +176 -0
  164. package/server/lib/memory-writer.test.js +231 -0
  165. package/server/lib/overdrive-command.js +30 -6
  166. package/server/lib/overdrive-command.test.js +8 -1
  167. package/server/lib/pattern-detector.js +216 -0
  168. package/server/lib/pattern-detector.test.js +241 -0
  169. package/server/lib/relevance-scorer.js +175 -0
  170. package/server/lib/relevance-scorer.test.js +107 -0
  171. package/server/lib/review-command.js +238 -0
  172. package/server/lib/review-command.test.js +245 -0
  173. package/server/lib/review-orchestrator.js +273 -0
  174. package/server/lib/review-orchestrator.test.js +300 -0
  175. package/server/lib/review-reporter.js +288 -0
  176. package/server/lib/review-reporter.test.js +240 -0
  177. package/server/lib/session-summary.js +90 -0
  178. package/server/lib/session-summary.test.js +156 -0
  179. package/templates/docs-sync.yml +91 -0
@@ -0,0 +1,138 @@
1
+ /**
2
+ * Memory Committer - Auto-commit team memory with conventional commits
3
+ */
4
+
5
+ const fs = require('fs').promises;
6
+ const path = require('path');
7
+ const { exec } = require('child_process');
8
+ const { promisify } = require('util');
9
+
10
+ const execAsync = promisify(exec);
11
+
12
+ /**
13
+ * Detect uncommitted memory files in team directory
14
+ * @param {string} projectRoot - Project root directory
15
+ * @returns {Promise<string[]>} List of uncommitted file paths
16
+ */
17
+ async function detectUncommittedMemory(projectRoot) {
18
+ const teamDir = path.join(projectRoot, '.tlc', 'memory', 'team');
19
+
20
+ try {
21
+ await fs.access(teamDir);
22
+ } catch {
23
+ return [];
24
+ }
25
+
26
+ // Get all files in team directory
27
+ const files = [];
28
+
29
+ async function walkDir(dir) {
30
+ const entries = await fs.readdir(dir, { withFileTypes: true });
31
+ for (const entry of entries) {
32
+ const fullPath = path.join(dir, entry.name);
33
+ if (entry.isDirectory()) {
34
+ await walkDir(fullPath);
35
+ } else if (entry.name.endsWith('.json') || entry.name.endsWith('.md')) {
36
+ // Skip template files like conventions.md
37
+ if (entry.name === 'conventions.md') continue;
38
+
39
+ // Get path relative to projectRoot
40
+ const relativePath = path.relative(projectRoot, fullPath);
41
+ files.push(relativePath);
42
+ }
43
+ }
44
+ }
45
+
46
+ await walkDir(teamDir);
47
+
48
+ return files;
49
+ }
50
+
51
+ /**
52
+ * Generate conventional commit message for memory files
53
+ * @param {string[]} files - List of file paths
54
+ * @returns {string} Commit message
55
+ */
56
+ function generateCommitMessage(files) {
57
+ if (!files || files.length === 0) return '';
58
+
59
+ const types = new Set();
60
+
61
+ for (const file of files) {
62
+ if (file.includes('decisions')) {
63
+ types.add('decision');
64
+ } else if (file.includes('gotchas')) {
65
+ types.add('gotcha');
66
+ }
67
+ }
68
+
69
+ const typeList = Array.from(types);
70
+ const typeStr = typeList.length === 1
71
+ ? typeList[0]
72
+ : typeList.slice(0, -1).join(', ') + ' and ' + typeList[typeList.length - 1];
73
+
74
+ return `docs(memory): add ${typeStr}${typeList.length > 1 || files.length > 1 ? 's' : ''}`;
75
+ }
76
+
77
+ /**
78
+ * Commit team memory files
79
+ * @param {string} projectRoot - Project root directory
80
+ * @param {Object} options - Options
81
+ * @param {boolean} options.dryRun - If true, don't actually commit
82
+ * @returns {Promise<Object>} Commit result
83
+ */
84
+ async function commitTeamMemory(projectRoot, options = {}) {
85
+ const { dryRun = false } = options;
86
+
87
+ const files = await detectUncommittedMemory(projectRoot);
88
+
89
+ if (files.length === 0) {
90
+ return {
91
+ success: false,
92
+ reason: 'nothing to commit',
93
+ files: [],
94
+ };
95
+ }
96
+
97
+ const message = generateCommitMessage(files);
98
+
99
+ if (dryRun) {
100
+ return {
101
+ success: true,
102
+ dryRun: true,
103
+ committed: false,
104
+ files,
105
+ message,
106
+ };
107
+ }
108
+
109
+ try {
110
+ // Stage memory files
111
+ for (const file of files) {
112
+ await execAsync(`git add "${file}"`, { cwd: projectRoot });
113
+ }
114
+
115
+ // Commit
116
+ await execAsync(`git commit -m "${message}"`, { cwd: projectRoot });
117
+
118
+ return {
119
+ success: true,
120
+ committed: true,
121
+ files,
122
+ message,
123
+ };
124
+ } catch (err) {
125
+ return {
126
+ success: false,
127
+ reason: err.message,
128
+ files,
129
+ message,
130
+ };
131
+ }
132
+ }
133
+
134
+ module.exports = {
135
+ detectUncommittedMemory,
136
+ generateCommitMessage,
137
+ commitTeamMemory,
138
+ };
@@ -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 { detectUncommittedMemory, generateCommitMessage, commitTeamMemory } from './memory-committer.js';
6
+ import { initMemoryStructure } from './memory-storage.js';
7
+ import { writeTeamDecision, writeTeamGotcha } from './memory-writer.js';
8
+
9
+ describe('memory-committer', () => {
10
+ let testDir;
11
+
12
+ beforeEach(async () => {
13
+ testDir = fs.mkdtempSync(path.join(os.tmpdir(), 'tlc-committer-test-'));
14
+ await initMemoryStructure(testDir);
15
+ });
16
+
17
+ afterEach(() => {
18
+ fs.rmSync(testDir, { recursive: true, force: true });
19
+ });
20
+
21
+ describe('detectUncommittedMemory', () => {
22
+ it('returns empty array for no memory files', async () => {
23
+ const uncommitted = await detectUncommittedMemory(testDir);
24
+ expect(uncommitted).toHaveLength(0);
25
+ });
26
+
27
+ it('detects new decision files', async () => {
28
+ await writeTeamDecision(testDir, {
29
+ title: 'Use PostgreSQL',
30
+ reasoning: 'JSONB support',
31
+ });
32
+
33
+ const uncommitted = await detectUncommittedMemory(testDir);
34
+
35
+ expect(uncommitted.length).toBeGreaterThan(0);
36
+ expect(uncommitted.some(f => f.includes('decisions'))).toBe(true);
37
+ });
38
+
39
+ it('detects new gotcha files', async () => {
40
+ await writeTeamGotcha(testDir, {
41
+ title: 'Auth delay',
42
+ issue: 'warmup needed',
43
+ });
44
+
45
+ const uncommitted = await detectUncommittedMemory(testDir);
46
+
47
+ expect(uncommitted.length).toBeGreaterThan(0);
48
+ expect(uncommitted.some(f => f.includes('gotchas'))).toBe(true);
49
+ });
50
+
51
+ it('excludes .local files', async () => {
52
+ // .local files should never be in uncommitted list
53
+ const localFile = path.join(testDir, '.tlc', 'memory', '.local', 'test.json');
54
+ fs.mkdirSync(path.dirname(localFile), { recursive: true });
55
+ fs.writeFileSync(localFile, '{}');
56
+
57
+ const uncommitted = await detectUncommittedMemory(testDir);
58
+
59
+ expect(uncommitted.every(f => !f.includes('.local'))).toBe(true);
60
+ });
61
+ });
62
+
63
+ describe('generateCommitMessage', () => {
64
+ it('generates message for decisions', () => {
65
+ const files = ['.tlc/memory/team/decisions/001.json'];
66
+ const message = generateCommitMessage(files);
67
+
68
+ expect(message).toContain('memory');
69
+ expect(message).toContain('decision');
70
+ });
71
+
72
+ it('generates message for gotchas', () => {
73
+ const files = ['.tlc/memory/team/gotchas/001.json'];
74
+ const message = generateCommitMessage(files);
75
+
76
+ expect(message).toContain('gotcha');
77
+ });
78
+
79
+ it('generates message for mixed types', () => {
80
+ const files = [
81
+ '.tlc/memory/team/decisions/001.json',
82
+ '.tlc/memory/team/gotchas/001.json',
83
+ ];
84
+ const message = generateCommitMessage(files);
85
+
86
+ expect(message).toContain('decision');
87
+ expect(message).toContain('gotcha');
88
+ });
89
+
90
+ it('uses conventional commit format', () => {
91
+ const files = ['.tlc/memory/team/decisions/001.json'];
92
+ const message = generateCommitMessage(files);
93
+
94
+ // Should start with docs: or chore: or similar (with optional scope)
95
+ expect(message).toMatch(/^(docs|chore|feat)(\([\w-]+\))?:/);
96
+ });
97
+
98
+ it('handles empty files array', () => {
99
+ const message = generateCommitMessage([]);
100
+ expect(message).toBe('');
101
+ });
102
+ });
103
+
104
+ describe('commitTeamMemory', () => {
105
+ it('returns success false for no uncommitted files', async () => {
106
+ const result = await commitTeamMemory(testDir, { dryRun: true });
107
+
108
+ expect(result.success).toBe(false);
109
+ expect(result.reason).toContain('nothing');
110
+ });
111
+
112
+ it('returns files that would be committed', async () => {
113
+ await writeTeamDecision(testDir, {
114
+ title: 'Test decision',
115
+ reasoning: 'Test',
116
+ });
117
+
118
+ const result = await commitTeamMemory(testDir, { dryRun: true });
119
+
120
+ expect(result.files.length).toBeGreaterThan(0);
121
+ expect(result.message).toBeTruthy();
122
+ });
123
+
124
+ it('respects dryRun option', async () => {
125
+ await writeTeamDecision(testDir, {
126
+ title: 'Test decision',
127
+ reasoning: 'Test',
128
+ });
129
+
130
+ const result = await commitTeamMemory(testDir, { dryRun: true });
131
+
132
+ expect(result.dryRun).toBe(true);
133
+ expect(result.committed).toBe(false);
134
+ });
135
+ });
136
+ });
@@ -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
+ };