tlc-claude-code 1.8.5 → 2.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (138) hide show
  1. package/.claude/commands/tlc/bootstrap.md +77 -0
  2. package/.claude/commands/tlc/build.md +20 -6
  3. package/.claude/commands/tlc/deploy.md +194 -2
  4. package/.claude/commands/tlc/e2e-verify.md +214 -0
  5. package/.claude/commands/tlc/guard.md +191 -0
  6. package/.claude/commands/tlc/help.md +32 -0
  7. package/.claude/commands/tlc/init.md +73 -37
  8. package/.claude/commands/tlc/llm.md +19 -4
  9. package/.claude/commands/tlc/preflight.md +134 -0
  10. package/.claude/commands/tlc/recall.md +87 -0
  11. package/.claude/commands/tlc/remember.md +71 -0
  12. package/.claude/commands/tlc/review.md +17 -4
  13. package/.claude/commands/tlc/watchci.md +159 -0
  14. package/.claude/hooks/tlc-block-tools.sh +41 -0
  15. package/.claude/hooks/tlc-capture-exchange.sh +50 -0
  16. package/.claude/hooks/tlc-post-build.sh +38 -0
  17. package/.claude/hooks/tlc-post-push.sh +22 -0
  18. package/.claude/hooks/tlc-prompt-guard.sh +69 -0
  19. package/.claude/hooks/tlc-session-init.sh +123 -0
  20. package/CLAUDE.md +96 -201
  21. package/bin/install.js +171 -2
  22. package/bin/postinstall.js +45 -26
  23. package/dashboard-web/dist/assets/index-CdS5CHqu.css +1 -0
  24. package/dashboard-web/dist/assets/index-CwNPPVpg.js +483 -0
  25. package/dashboard-web/dist/assets/index-CwNPPVpg.js.map +1 -0
  26. package/dashboard-web/dist/index.html +2 -2
  27. package/docker-compose.dev.yml +18 -12
  28. package/package.json +3 -1
  29. package/server/index.js +240 -1
  30. package/server/lib/bug-writer.js +204 -0
  31. package/server/lib/bug-writer.test.js +279 -0
  32. package/server/lib/capture-bridge.js +242 -0
  33. package/server/lib/capture-bridge.test.js +363 -0
  34. package/server/lib/capture-guard.js +140 -0
  35. package/server/lib/capture-guard.test.js +182 -0
  36. package/server/lib/claude-cascade.js +247 -0
  37. package/server/lib/claude-cascade.test.js +245 -0
  38. package/server/lib/command-runner.js +159 -0
  39. package/server/lib/command-runner.test.js +92 -0
  40. package/server/lib/context-injection.js +121 -0
  41. package/server/lib/context-injection.test.js +340 -0
  42. package/server/lib/conversation-chunker.js +320 -0
  43. package/server/lib/conversation-chunker.test.js +573 -0
  44. package/server/lib/deploy/runners/dependency-runner.js +106 -0
  45. package/server/lib/deploy/runners/dependency-runner.test.js +148 -0
  46. package/server/lib/deploy/runners/secrets-runner.js +174 -0
  47. package/server/lib/deploy/runners/secrets-runner.test.js +127 -0
  48. package/server/lib/deploy/security-gates.js +11 -24
  49. package/server/lib/deploy/security-gates.test.js +9 -2
  50. package/server/lib/deploy-engine.js +182 -0
  51. package/server/lib/deploy-engine.test.js +147 -0
  52. package/server/lib/docker-api.js +137 -0
  53. package/server/lib/docker-api.test.js +202 -0
  54. package/server/lib/docker-client.js +297 -0
  55. package/server/lib/docker-client.test.js +308 -0
  56. package/server/lib/embedding-client.js +160 -0
  57. package/server/lib/embedding-client.test.js +243 -0
  58. package/server/lib/global-config.js +198 -0
  59. package/server/lib/global-config.test.js +288 -0
  60. package/server/lib/inherited-search.js +184 -0
  61. package/server/lib/inherited-search.test.js +343 -0
  62. package/server/lib/input-sanitizer.js +86 -0
  63. package/server/lib/input-sanitizer.test.js +117 -0
  64. package/server/lib/launchd-agent.js +225 -0
  65. package/server/lib/launchd-agent.test.js +185 -0
  66. package/server/lib/memory-api.js +182 -0
  67. package/server/lib/memory-api.test.js +320 -0
  68. package/server/lib/memory-bridge-e2e.test.js +160 -0
  69. package/server/lib/memory-committer.js +18 -4
  70. package/server/lib/memory-committer.test.js +21 -0
  71. package/server/lib/memory-hooks-capture.test.js +415 -0
  72. package/server/lib/memory-hooks-integration.test.js +98 -0
  73. package/server/lib/memory-hooks.js +139 -0
  74. package/server/lib/memory-inheritance.js +179 -0
  75. package/server/lib/memory-inheritance.test.js +360 -0
  76. package/server/lib/memory-store-adapter.js +105 -0
  77. package/server/lib/memory-store-adapter.test.js +141 -0
  78. package/server/lib/memory-wiring-e2e.test.js +93 -0
  79. package/server/lib/nginx-config.js +114 -0
  80. package/server/lib/nginx-config.test.js +82 -0
  81. package/server/lib/ollama-health.js +91 -0
  82. package/server/lib/ollama-health.test.js +74 -0
  83. package/server/lib/plan-writer.js +196 -0
  84. package/server/lib/plan-writer.test.js +298 -0
  85. package/server/lib/port-guard.js +44 -0
  86. package/server/lib/port-guard.test.js +65 -0
  87. package/server/lib/project-scanner.js +302 -0
  88. package/server/lib/project-scanner.test.js +541 -0
  89. package/server/lib/project-status.js +302 -0
  90. package/server/lib/project-status.test.js +470 -0
  91. package/server/lib/projects-registry.js +237 -0
  92. package/server/lib/projects-registry.test.js +275 -0
  93. package/server/lib/recall-command.js +207 -0
  94. package/server/lib/recall-command.test.js +306 -0
  95. package/server/lib/remember-command.js +98 -0
  96. package/server/lib/remember-command.test.js +288 -0
  97. package/server/lib/rich-capture.js +221 -0
  98. package/server/lib/rich-capture.test.js +312 -0
  99. package/server/lib/roadmap-api.js +200 -0
  100. package/server/lib/roadmap-api.test.js +318 -0
  101. package/server/lib/security/crypto-utils.test.js +2 -2
  102. package/server/lib/semantic-recall.js +242 -0
  103. package/server/lib/semantic-recall.test.js +463 -0
  104. package/server/lib/setup-generator.js +315 -0
  105. package/server/lib/setup-generator.test.js +303 -0
  106. package/server/lib/ssh-client.js +184 -0
  107. package/server/lib/ssh-client.test.js +127 -0
  108. package/server/lib/test-inventory.js +112 -0
  109. package/server/lib/test-inventory.test.js +360 -0
  110. package/server/lib/vector-indexer.js +246 -0
  111. package/server/lib/vector-indexer.test.js +459 -0
  112. package/server/lib/vector-store.js +260 -0
  113. package/server/lib/vector-store.test.js +706 -0
  114. package/server/lib/vps-api.js +184 -0
  115. package/server/lib/vps-api.test.js +208 -0
  116. package/server/lib/vps-bootstrap.js +124 -0
  117. package/server/lib/vps-bootstrap.test.js +79 -0
  118. package/server/lib/vps-monitor.js +126 -0
  119. package/server/lib/vps-monitor.test.js +98 -0
  120. package/server/lib/workspace-api.js +992 -0
  121. package/server/lib/workspace-api.test.js +1217 -0
  122. package/server/lib/workspace-bootstrap.js +164 -0
  123. package/server/lib/workspace-bootstrap.test.js +503 -0
  124. package/server/lib/workspace-context.js +129 -0
  125. package/server/lib/workspace-context.test.js +214 -0
  126. package/server/lib/workspace-detector.js +162 -0
  127. package/server/lib/workspace-detector.test.js +193 -0
  128. package/server/lib/workspace-init.js +307 -0
  129. package/server/lib/workspace-init.test.js +244 -0
  130. package/server/lib/workspace-snapshot.js +236 -0
  131. package/server/lib/workspace-snapshot.test.js +444 -0
  132. package/server/lib/workspace-watcher.js +162 -0
  133. package/server/lib/workspace-watcher.test.js +257 -0
  134. package/server/package-lock.json +1306 -17
  135. package/server/package.json +7 -0
  136. package/dashboard-web/dist/assets/index-B1I_joSL.js +0 -393
  137. package/dashboard-web/dist/assets/index-B1I_joSL.js.map +0 -1
  138. package/dashboard-web/dist/assets/index-Trhg1C1Y.css +0 -1
@@ -0,0 +1,302 @@
1
+ /**
2
+ * Project Scanner - Recursively discovers TLC projects within configured root paths
3
+ *
4
+ * Scans directory trees looking for:
5
+ * - TLC projects (.tlc.json present)
6
+ * - Planning-only projects (.planning/ directory present)
7
+ * - Candidate projects (package.json + .git/ present, not yet initialized with TLC)
8
+ *
9
+ * Returns structured project metadata including phase info from ROADMAP.md.
10
+ */
11
+
12
+ const fs = require('fs');
13
+ const path = require('path');
14
+
15
+ /**
16
+ * Directories to skip during recursive scanning
17
+ */
18
+ const IGNORED_DIRS = new Set([
19
+ 'node_modules',
20
+ '.git',
21
+ 'dist',
22
+ 'build',
23
+ 'coverage',
24
+ 'vendor',
25
+ '.next',
26
+ '.nuxt',
27
+ ]);
28
+
29
+ /**
30
+ * Parse phase information from a ROADMAP.md file
31
+ * @param {string} roadmapPath - Absolute path to ROADMAP.md
32
+ * @returns {{ phase: number|null, phaseName: string|null, totalPhases: number, completedPhases: number }}
33
+ */
34
+ function parseRoadmap(roadmapPath) {
35
+ const result = {
36
+ phase: null,
37
+ phaseName: null,
38
+ totalPhases: 0,
39
+ completedPhases: 0,
40
+ };
41
+
42
+ let content;
43
+ try {
44
+ content = fs.readFileSync(roadmapPath, 'utf-8');
45
+ } catch {
46
+ return result;
47
+ }
48
+
49
+ // Format 1: Heading format — ### Phase N: Name [x] / [ ] / [>]
50
+ const headingRegex = /###\s+Phase\s+(\d+)(?:\.\d+)?[:\s]+(.+?)\s*\[([x >])\]\s*$/gm;
51
+ let headingMatch;
52
+ let foundHeadings = false;
53
+ let firstIncomplete = null;
54
+
55
+ while ((headingMatch = headingRegex.exec(content)) !== null) {
56
+ foundHeadings = true;
57
+ result.totalPhases++;
58
+ const phaseNum = parseInt(headingMatch[1], 10);
59
+ const phaseName = headingMatch[2].trim();
60
+ const marker = headingMatch[3];
61
+
62
+ if (marker === 'x') {
63
+ result.completedPhases++;
64
+ } else if (!firstIncomplete) {
65
+ firstIncomplete = { phase: phaseNum, phaseName };
66
+ }
67
+ }
68
+
69
+ if (foundHeadings) {
70
+ if (firstIncomplete) {
71
+ result.phase = firstIncomplete.phase;
72
+ result.phaseName = firstIncomplete.phaseName;
73
+ }
74
+ return result;
75
+ }
76
+
77
+ // Format 2: Table format — | N | [Name](link) | status |
78
+ const tableRegex = /\|\s*(\d+)\s*\|\s*\[([^\]]+)\][^|]*\|\s*(\w+)\s*\|/g;
79
+ let tableMatch;
80
+
81
+ while ((tableMatch = tableRegex.exec(content)) !== null) {
82
+ result.totalPhases++;
83
+ const phaseNum = parseInt(tableMatch[1], 10);
84
+ const phaseName = tableMatch[2].trim();
85
+ const status = tableMatch[3].trim().toLowerCase();
86
+ const completed = status === 'complete' || status === 'done' || status === 'verified';
87
+
88
+ if (completed) {
89
+ result.completedPhases++;
90
+ } else if (!firstIncomplete) {
91
+ firstIncomplete = { phase: phaseNum, phaseName };
92
+ }
93
+ }
94
+
95
+ if (firstIncomplete) {
96
+ result.phase = firstIncomplete.phase;
97
+ result.phaseName = firstIncomplete.phaseName;
98
+ }
99
+
100
+ return result;
101
+ }
102
+
103
+ /**
104
+ * Read project metadata from a project directory
105
+ * @param {string} projectDir - Absolute path to the project directory
106
+ * @returns {object} Project metadata
107
+ */
108
+ function readProjectMetadata(projectDir) {
109
+ const hasTlc = fs.existsSync(path.join(projectDir, '.tlc.json'));
110
+ const hasPlanning = fs.existsSync(path.join(projectDir, '.planning'));
111
+
112
+ // Read name, version, and workspaces from package.json if present
113
+ let name = path.basename(projectDir);
114
+ let version = null;
115
+ let isMonorepo = false;
116
+ let workspaces = [];
117
+
118
+ const pkgPath = path.join(projectDir, 'package.json');
119
+ if (fs.existsSync(pkgPath)) {
120
+ try {
121
+ const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf-8'));
122
+ if (pkg.name) {
123
+ name = pkg.name;
124
+ }
125
+ if (pkg.version) {
126
+ version = pkg.version;
127
+ }
128
+
129
+ // Detect monorepo workspaces (npm array or yarn object format)
130
+ let workspacePatterns = null;
131
+ if (Array.isArray(pkg.workspaces)) {
132
+ workspacePatterns = pkg.workspaces;
133
+ } else if (pkg.workspaces && Array.isArray(pkg.workspaces.packages)) {
134
+ workspacePatterns = pkg.workspaces.packages;
135
+ }
136
+
137
+ if (workspacePatterns) {
138
+ isMonorepo = true;
139
+ // Resolve glob patterns to actual directories
140
+ for (const pattern of workspacePatterns) {
141
+ try {
142
+ const globDir = path.join(projectDir, path.dirname(pattern));
143
+ if (fs.existsSync(globDir)) {
144
+ const entries = fs.readdirSync(globDir, { withFileTypes: true });
145
+ for (const entry of entries) {
146
+ if (entry.isDirectory()) {
147
+ workspaces.push(path.join(path.dirname(pattern), entry.name));
148
+ }
149
+ }
150
+ }
151
+ } catch {
152
+ // Ignore glob resolution errors
153
+ }
154
+ }
155
+ }
156
+ } catch {
157
+ // Ignore malformed package.json
158
+ }
159
+ }
160
+
161
+ // Parse phase info from ROADMAP.md if .planning exists
162
+ let phaseInfo = { phase: null, phaseName: null, totalPhases: 0, completedPhases: 0 };
163
+ if (hasPlanning) {
164
+ const roadmapPath = path.join(projectDir, '.planning', 'ROADMAP.md');
165
+ if (fs.existsSync(roadmapPath)) {
166
+ phaseInfo = parseRoadmap(roadmapPath);
167
+ }
168
+ }
169
+
170
+ return {
171
+ name,
172
+ path: projectDir,
173
+ hasTlc,
174
+ hasPlanning,
175
+ version,
176
+ phase: phaseInfo.phase,
177
+ phaseName: phaseInfo.phaseName,
178
+ totalPhases: phaseInfo.totalPhases,
179
+ completedPhases: phaseInfo.completedPhases,
180
+ isMonorepo,
181
+ workspaces,
182
+ };
183
+ }
184
+
185
+ /**
186
+ * ProjectScanner - Recursively discovers TLC projects within configured root paths
187
+ */
188
+ class ProjectScanner {
189
+ /**
190
+ * @param {object} [options]
191
+ * @param {number} [options.scanDepth=5] - Maximum recursion depth
192
+ * @param {number} [options.cacheTTL=60000] - Cache time-to-live in milliseconds
193
+ */
194
+ constructor(options = {}) {
195
+ this.scanDepth = options.scanDepth || 5;
196
+ this.cacheTTL = options.cacheTTL || 60000;
197
+ this._cache = null;
198
+ this._cacheTime = 0;
199
+ }
200
+
201
+ /**
202
+ * Scan root directories for TLC projects
203
+ * @param {string[]} roots - Array of root directory paths to scan
204
+ * @param {object} [options]
205
+ * @param {boolean} [options.force=false] - Force re-scan bypassing cache
206
+ * @param {function} [options.onProgress] - Progress callback receiving discovered count
207
+ * @returns {object[]} Array of project metadata objects, sorted by name
208
+ */
209
+ scan(roots, options = {}) {
210
+ const { force = false, onProgress } = options;
211
+
212
+ // Check cache
213
+ if (!force && this._cache !== null) {
214
+ const age = Date.now() - this._cacheTime;
215
+ if (age < this.cacheTTL) {
216
+ return this._cache;
217
+ }
218
+ }
219
+
220
+ const projectsByPath = new Map();
221
+
222
+ for (const root of roots) {
223
+ // Check that root exists
224
+ if (!fs.existsSync(root)) {
225
+ console.warn(`ProjectScanner: root path does not exist: ${root}`);
226
+ continue;
227
+ }
228
+
229
+ this._scanDir(root, 0, projectsByPath, onProgress);
230
+ }
231
+
232
+ const projects = Array.from(projectsByPath.values());
233
+ projects.sort((a, b) => a.name.localeCompare(b.name));
234
+
235
+ // Update cache
236
+ this._cache = projects;
237
+ this._cacheTime = Date.now();
238
+
239
+ return projects;
240
+ }
241
+
242
+ /**
243
+ * Recursively scan a directory for projects
244
+ * @param {string} dir - Directory to scan
245
+ * @param {number} depth - Current recursion depth
246
+ * @param {Map} projectsByPath - Accumulated projects (keyed by absolute path for dedup)
247
+ * @param {function} [onProgress] - Progress callback
248
+ * @private
249
+ */
250
+ _scanDir(dir, depth, projectsByPath, onProgress) {
251
+ if (depth > this.scanDepth) {
252
+ return;
253
+ }
254
+
255
+ // Check if this directory IS a project
256
+ const hasTlc = fs.existsSync(path.join(dir, '.tlc.json'));
257
+ const hasPlanning = fs.existsSync(path.join(dir, '.planning'));
258
+ const hasPackageJson = fs.existsSync(path.join(dir, 'package.json'));
259
+ const hasGit = fs.existsSync(path.join(dir, '.git'));
260
+
261
+ const isProject = hasTlc || hasPlanning || (hasPackageJson && hasGit);
262
+
263
+ if (isProject && !projectsByPath.has(dir)) {
264
+ const metadata = readProjectMetadata(dir);
265
+ projectsByPath.set(dir, metadata);
266
+
267
+ if (typeof onProgress === 'function') {
268
+ onProgress(projectsByPath.size);
269
+ }
270
+
271
+ // Stop recursion: a project's children are not separate projects
272
+ return;
273
+ }
274
+
275
+ // Recurse into subdirectories (only for non-project directories)
276
+ let entries;
277
+ try {
278
+ entries = fs.readdirSync(dir, { withFileTypes: true });
279
+ } catch (err) {
280
+ if (err.code === 'EACCES' || err.code === 'EPERM') {
281
+ console.warn(`ProjectScanner: permission denied reading directory: ${dir}`);
282
+ return;
283
+ }
284
+ throw err;
285
+ }
286
+
287
+ for (const entry of entries) {
288
+ if (!entry.isDirectory()) {
289
+ continue;
290
+ }
291
+
292
+ if (IGNORED_DIRS.has(entry.name)) {
293
+ continue;
294
+ }
295
+
296
+ const childPath = path.join(dir, entry.name);
297
+ this._scanDir(childPath, depth + 1, projectsByPath, onProgress);
298
+ }
299
+ }
300
+ }
301
+
302
+ module.exports = { ProjectScanner };