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.
- package/dashboard/dist/components/ActivityFeed.d.ts +17 -0
- package/dashboard/dist/components/ActivityFeed.js +42 -0
- package/dashboard/dist/components/ActivityFeed.test.d.ts +1 -0
- package/dashboard/dist/components/ActivityFeed.test.js +162 -0
- package/dashboard/dist/components/BranchSelector.d.ts +16 -0
- package/dashboard/dist/components/BranchSelector.js +49 -0
- package/dashboard/dist/components/BranchSelector.test.d.ts +1 -0
- package/dashboard/dist/components/BranchSelector.test.js +166 -0
- package/dashboard/dist/components/CommandPalette.d.ts +17 -0
- package/dashboard/dist/components/CommandPalette.js +118 -0
- package/dashboard/dist/components/CommandPalette.test.d.ts +1 -0
- package/dashboard/dist/components/CommandPalette.test.js +181 -0
- package/dashboard/dist/components/ConnectionStatus.d.ts +16 -0
- package/dashboard/dist/components/ConnectionStatus.js +27 -0
- package/dashboard/dist/components/ConnectionStatus.test.d.ts +1 -0
- package/dashboard/dist/components/ConnectionStatus.test.js +121 -0
- package/dashboard/dist/components/DeviceFrame.d.ts +19 -0
- package/dashboard/dist/components/DeviceFrame.js +52 -0
- package/dashboard/dist/components/DeviceFrame.test.d.ts +1 -0
- package/dashboard/dist/components/DeviceFrame.test.js +118 -0
- package/dashboard/dist/components/EnvironmentBadge.d.ts +11 -0
- package/dashboard/dist/components/EnvironmentBadge.js +16 -0
- package/dashboard/dist/components/EnvironmentBadge.test.d.ts +1 -0
- package/dashboard/dist/components/EnvironmentBadge.test.js +102 -0
- package/dashboard/dist/components/FocusIndicator.d.ts +19 -0
- package/dashboard/dist/components/FocusIndicator.js +47 -0
- package/dashboard/dist/components/FocusIndicator.test.d.ts +1 -0
- package/dashboard/dist/components/FocusIndicator.test.js +117 -0
- package/dashboard/dist/components/KeyboardHelp.d.ts +15 -0
- package/dashboard/dist/components/KeyboardHelp.js +61 -0
- package/dashboard/dist/components/KeyboardHelp.test.d.ts +1 -0
- package/dashboard/dist/components/KeyboardHelp.test.js +131 -0
- package/dashboard/dist/components/LogSearch.d.ts +13 -0
- package/dashboard/dist/components/LogSearch.js +43 -0
- package/dashboard/dist/components/LogSearch.test.d.ts +1 -0
- package/dashboard/dist/components/LogSearch.test.js +100 -0
- package/dashboard/dist/components/LogStream.d.ts +21 -0
- package/dashboard/dist/components/LogStream.js +123 -0
- package/dashboard/dist/components/LogStream.test.d.ts +1 -0
- package/dashboard/dist/components/LogStream.test.js +159 -0
- package/dashboard/dist/components/PlanView.d.ts +7 -0
- package/dashboard/dist/components/PlanView.js +74 -2
- package/dashboard/dist/components/PlanView.test.js +70 -1
- package/dashboard/dist/components/PreviewPanel.d.ts +18 -0
- package/dashboard/dist/components/PreviewPanel.js +73 -0
- package/dashboard/dist/components/PreviewPanel.test.d.ts +1 -0
- package/dashboard/dist/components/PreviewPanel.test.js +124 -0
- package/dashboard/dist/components/ProjectCard.d.ts +18 -0
- package/dashboard/dist/components/ProjectCard.js +19 -0
- package/dashboard/dist/components/ProjectCard.test.d.ts +1 -0
- package/dashboard/dist/components/ProjectCard.test.js +53 -0
- package/dashboard/dist/components/ProjectDetail.d.ts +44 -0
- package/dashboard/dist/components/ProjectDetail.js +65 -0
- package/dashboard/dist/components/ProjectDetail.test.d.ts +1 -0
- package/dashboard/dist/components/ProjectDetail.test.js +196 -0
- package/dashboard/dist/components/ProjectList.d.ts +11 -0
- package/dashboard/dist/components/ProjectList.js +62 -0
- package/dashboard/dist/components/ProjectList.test.d.ts +1 -0
- package/dashboard/dist/components/ProjectList.test.js +93 -0
- package/dashboard/dist/components/SettingsPanel.d.ts +32 -0
- package/dashboard/dist/components/SettingsPanel.js +154 -0
- package/dashboard/dist/components/SettingsPanel.test.d.ts +1 -0
- package/dashboard/dist/components/SettingsPanel.test.js +196 -0
- package/dashboard/dist/components/StatusBar.d.ts +16 -0
- package/dashboard/dist/components/StatusBar.js +47 -0
- package/dashboard/dist/components/StatusBar.test.d.ts +1 -0
- package/dashboard/dist/components/StatusBar.test.js +123 -0
- package/dashboard/dist/components/TaskBoard.d.ts +22 -0
- package/dashboard/dist/components/TaskBoard.js +102 -0
- package/dashboard/dist/components/TaskBoard.test.d.ts +1 -0
- package/dashboard/dist/components/TaskBoard.test.js +113 -0
- package/dashboard/dist/components/TaskCard.d.ts +17 -0
- package/dashboard/dist/components/TaskCard.js +29 -0
- package/dashboard/dist/components/TaskCard.test.d.ts +1 -0
- package/dashboard/dist/components/TaskCard.test.js +109 -0
- package/dashboard/dist/components/TaskDetail.d.ts +36 -0
- package/dashboard/dist/components/TaskDetail.js +41 -0
- package/dashboard/dist/components/TaskDetail.test.d.ts +1 -0
- package/dashboard/dist/components/TaskDetail.test.js +164 -0
- package/dashboard/dist/components/TaskFilter.d.ts +12 -0
- package/dashboard/dist/components/TaskFilter.js +138 -0
- package/dashboard/dist/components/TaskFilter.test.d.ts +1 -0
- package/dashboard/dist/components/TaskFilter.test.js +109 -0
- package/dashboard/dist/components/TeamPanel.d.ts +15 -0
- package/dashboard/dist/components/TeamPanel.js +24 -0
- package/dashboard/dist/components/TeamPanel.test.d.ts +1 -0
- package/dashboard/dist/components/TeamPanel.test.js +109 -0
- package/dashboard/dist/components/TeamPresence.d.ts +14 -0
- package/dashboard/dist/components/TeamPresence.js +31 -0
- package/dashboard/dist/components/TeamPresence.test.d.ts +1 -0
- package/dashboard/dist/components/TeamPresence.test.js +144 -0
- package/dashboard/dist/components/layout/Header.d.ts +9 -0
- package/dashboard/dist/components/layout/Header.js +11 -0
- package/dashboard/dist/components/layout/Header.test.d.ts +1 -0
- package/dashboard/dist/components/layout/Header.test.js +35 -0
- package/dashboard/dist/components/layout/Shell.d.ts +10 -0
- package/dashboard/dist/components/layout/Shell.js +5 -0
- package/dashboard/dist/components/layout/Shell.test.d.ts +1 -0
- package/dashboard/dist/components/layout/Shell.test.js +34 -0
- package/dashboard/dist/components/layout/Sidebar.d.ts +14 -0
- package/dashboard/dist/components/layout/Sidebar.js +8 -0
- package/dashboard/dist/components/layout/Sidebar.test.d.ts +1 -0
- package/dashboard/dist/components/layout/Sidebar.test.js +40 -0
- package/dashboard/dist/components/ui/Badge.d.ts +9 -0
- package/dashboard/dist/components/ui/Badge.js +13 -0
- package/dashboard/dist/components/ui/Badge.test.d.ts +1 -0
- package/dashboard/dist/components/ui/Badge.test.js +69 -0
- package/dashboard/dist/components/ui/Button.d.ts +12 -0
- package/dashboard/dist/components/ui/Button.js +14 -0
- package/dashboard/dist/components/ui/Button.test.d.ts +1 -0
- package/dashboard/dist/components/ui/Button.test.js +81 -0
- package/dashboard/dist/components/ui/Card.d.ts +21 -0
- package/dashboard/dist/components/ui/Card.js +20 -0
- package/dashboard/dist/components/ui/Card.test.d.ts +1 -0
- package/dashboard/dist/components/ui/Card.test.js +82 -0
- package/dashboard/dist/components/ui/Input.d.ts +13 -0
- package/dashboard/dist/components/ui/Input.js +8 -0
- package/dashboard/dist/components/ui/Input.test.d.ts +1 -0
- package/dashboard/dist/components/ui/Input.test.js +68 -0
- package/dashboard/dist/styles/tokens.d.ts +150 -0
- package/dashboard/dist/styles/tokens.js +184 -0
- package/dashboard/dist/styles/tokens.test.d.ts +1 -0
- package/dashboard/dist/styles/tokens.test.js +95 -0
- package/dashboard/dist/test/setup.d.ts +1 -0
- package/dashboard/dist/test/setup.js +1 -0
- package/dashboard/package.json +3 -0
- package/package.json +1 -1
- package/server/dashboard/index.html +157 -2
- package/server/index.js +38 -21
- package/server/lib/adapters/base-adapter.js +114 -0
- package/server/lib/adapters/base-adapter.test.js +90 -0
- package/server/lib/adapters/claude-adapter.js +141 -0
- package/server/lib/adapters/claude-adapter.test.js +180 -0
- package/server/lib/adapters/deepseek-adapter.js +153 -0
- package/server/lib/adapters/deepseek-adapter.test.js +193 -0
- package/server/lib/adapters/openai-adapter.js +190 -0
- package/server/lib/adapters/openai-adapter.test.js +231 -0
- package/server/lib/budget-tracker.js +169 -0
- package/server/lib/budget-tracker.test.js +165 -0
- package/server/lib/claude-injector.js +85 -0
- package/server/lib/claude-injector.test.js +161 -0
- package/server/lib/consensus-engine.js +135 -0
- package/server/lib/consensus-engine.test.js +152 -0
- package/server/lib/context-builder.js +112 -0
- package/server/lib/context-builder.test.js +120 -0
- package/server/lib/file-collector.js +322 -0
- package/server/lib/file-collector.test.js +307 -0
- package/server/lib/memory-classifier.js +175 -0
- package/server/lib/memory-classifier.test.js +169 -0
- package/server/lib/memory-committer.js +138 -0
- package/server/lib/memory-committer.test.js +136 -0
- package/server/lib/memory-hooks.js +127 -0
- package/server/lib/memory-hooks.test.js +136 -0
- package/server/lib/memory-init.js +104 -0
- package/server/lib/memory-init.test.js +119 -0
- package/server/lib/memory-observer.js +149 -0
- package/server/lib/memory-observer.test.js +158 -0
- package/server/lib/memory-reader.js +243 -0
- package/server/lib/memory-reader.test.js +216 -0
- package/server/lib/memory-storage.js +120 -0
- package/server/lib/memory-storage.test.js +136 -0
- package/server/lib/memory-writer.js +176 -0
- package/server/lib/memory-writer.test.js +231 -0
- package/server/lib/overdrive-command.js +30 -6
- package/server/lib/overdrive-command.test.js +8 -1
- package/server/lib/pattern-detector.js +216 -0
- package/server/lib/pattern-detector.test.js +241 -0
- package/server/lib/relevance-scorer.js +175 -0
- package/server/lib/relevance-scorer.test.js +107 -0
- package/server/lib/review-command.js +238 -0
- package/server/lib/review-command.test.js +245 -0
- package/server/lib/review-orchestrator.js +273 -0
- package/server/lib/review-orchestrator.test.js +300 -0
- package/server/lib/review-reporter.js +288 -0
- package/server/lib/review-reporter.test.js +240 -0
- package/server/lib/session-summary.js +90 -0
- 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
|
+
};
|