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.
- package/README.md +9 -4
- 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/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 +15 -4
- package/scripts/capture-screenshots.js +170 -0
- package/scripts/docs-update.js +253 -0
- package/scripts/generate-screenshots.js +321 -0
- package/scripts/project-docs.js +377 -0
- package/scripts/vps-setup.sh +477 -0
- 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
- 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
|
+
};
|