tlc-claude-code 2.0.1 → 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 (86) hide show
  1. package/.claude/commands/tlc/deploy.md +194 -2
  2. package/.claude/commands/tlc/e2e-verify.md +214 -0
  3. package/.claude/commands/tlc/guard.md +191 -0
  4. package/.claude/commands/tlc/help.md +32 -0
  5. package/.claude/commands/tlc/init.md +73 -37
  6. package/.claude/commands/tlc/llm.md +19 -4
  7. package/.claude/commands/tlc/preflight.md +134 -0
  8. package/.claude/commands/tlc/review.md +17 -4
  9. package/.claude/commands/tlc/watchci.md +159 -0
  10. package/.claude/hooks/tlc-block-tools.sh +41 -0
  11. package/.claude/hooks/tlc-capture-exchange.sh +50 -0
  12. package/.claude/hooks/tlc-post-build.sh +38 -0
  13. package/.claude/hooks/tlc-post-push.sh +22 -0
  14. package/.claude/hooks/tlc-prompt-guard.sh +69 -0
  15. package/.claude/hooks/tlc-session-init.sh +123 -0
  16. package/CLAUDE.md +12 -0
  17. package/bin/install.js +171 -2
  18. package/bin/postinstall.js +45 -26
  19. package/dashboard-web/dist/assets/index-CdS5CHqu.css +1 -0
  20. package/dashboard-web/dist/assets/index-CwNPPVpg.js +483 -0
  21. package/dashboard-web/dist/assets/index-CwNPPVpg.js.map +1 -0
  22. package/dashboard-web/dist/index.html +2 -2
  23. package/docker-compose.dev.yml +18 -12
  24. package/package.json +3 -1
  25. package/server/index.js +228 -2
  26. package/server/lib/capture-bridge.js +242 -0
  27. package/server/lib/capture-bridge.test.js +363 -0
  28. package/server/lib/capture-guard.js +140 -0
  29. package/server/lib/capture-guard.test.js +182 -0
  30. package/server/lib/command-runner.js +159 -0
  31. package/server/lib/command-runner.test.js +92 -0
  32. package/server/lib/deploy/runners/dependency-runner.js +106 -0
  33. package/server/lib/deploy/runners/dependency-runner.test.js +148 -0
  34. package/server/lib/deploy/runners/secrets-runner.js +174 -0
  35. package/server/lib/deploy/runners/secrets-runner.test.js +127 -0
  36. package/server/lib/deploy/security-gates.js +11 -24
  37. package/server/lib/deploy/security-gates.test.js +9 -2
  38. package/server/lib/deploy-engine.js +182 -0
  39. package/server/lib/deploy-engine.test.js +147 -0
  40. package/server/lib/docker-api.js +137 -0
  41. package/server/lib/docker-api.test.js +202 -0
  42. package/server/lib/docker-client.js +297 -0
  43. package/server/lib/docker-client.test.js +308 -0
  44. package/server/lib/input-sanitizer.js +86 -0
  45. package/server/lib/input-sanitizer.test.js +117 -0
  46. package/server/lib/launchd-agent.js +225 -0
  47. package/server/lib/launchd-agent.test.js +185 -0
  48. package/server/lib/memory-api.js +3 -1
  49. package/server/lib/memory-api.test.js +3 -5
  50. package/server/lib/memory-bridge-e2e.test.js +160 -0
  51. package/server/lib/memory-committer.js +18 -4
  52. package/server/lib/memory-committer.test.js +21 -0
  53. package/server/lib/memory-hooks-capture.test.js +69 -4
  54. package/server/lib/memory-hooks-integration.test.js +98 -0
  55. package/server/lib/memory-hooks.js +42 -4
  56. package/server/lib/memory-store-adapter.js +105 -0
  57. package/server/lib/memory-store-adapter.test.js +141 -0
  58. package/server/lib/memory-wiring-e2e.test.js +93 -0
  59. package/server/lib/nginx-config.js +114 -0
  60. package/server/lib/nginx-config.test.js +82 -0
  61. package/server/lib/ollama-health.js +91 -0
  62. package/server/lib/ollama-health.test.js +74 -0
  63. package/server/lib/port-guard.js +44 -0
  64. package/server/lib/port-guard.test.js +65 -0
  65. package/server/lib/project-scanner.js +37 -2
  66. package/server/lib/project-scanner.test.js +152 -0
  67. package/server/lib/remember-command.js +2 -0
  68. package/server/lib/remember-command.test.js +23 -0
  69. package/server/lib/security/crypto-utils.test.js +2 -2
  70. package/server/lib/semantic-recall.js +1 -1
  71. package/server/lib/semantic-recall.test.js +17 -0
  72. package/server/lib/ssh-client.js +184 -0
  73. package/server/lib/ssh-client.test.js +127 -0
  74. package/server/lib/vps-api.js +184 -0
  75. package/server/lib/vps-api.test.js +208 -0
  76. package/server/lib/vps-bootstrap.js +124 -0
  77. package/server/lib/vps-bootstrap.test.js +79 -0
  78. package/server/lib/vps-monitor.js +126 -0
  79. package/server/lib/vps-monitor.test.js +98 -0
  80. package/server/lib/workspace-api.js +182 -1
  81. package/server/lib/workspace-api.test.js +474 -0
  82. package/server/package-lock.json +737 -0
  83. package/server/package.json +3 -0
  84. package/dashboard-web/dist/assets/index-Uhc49PE-.css +0 -1
  85. package/dashboard-web/dist/assets/index-W36XHPC5.js +0 -431
  86. package/dashboard-web/dist/assets/index-W36XHPC5.js.map +0 -1
@@ -16,6 +16,8 @@ const { createTestInventory } = require('./test-inventory');
16
16
  const { createRoadmapApi } = require('./roadmap-api');
17
17
  const { createPlanWriter } = require('./plan-writer');
18
18
  const { createBugWriter } = require('./bug-writer');
19
+ const { createMemoryStoreAdapter } = require('./memory-store-adapter');
20
+ const { createCaptureGuard } = require('./capture-guard');
19
21
 
20
22
  /**
21
23
  * Encode a project path to a URL-safe project ID
@@ -62,6 +64,7 @@ function readProjectStatus(projectPath) {
62
64
  phaseName: null,
63
65
  totalPhases: 0,
64
66
  completedPhases: 0,
67
+ coverage: null,
65
68
  };
66
69
 
67
70
  if (!status.exists) {
@@ -128,6 +131,19 @@ function readProjectStatus(projectPath) {
128
131
  }
129
132
  }
130
133
 
134
+ // Read coverage from Istanbul coverage-summary.json
135
+ const coveragePath = path.join(projectPath, 'coverage', 'coverage-summary.json');
136
+ if (fs.existsSync(coveragePath)) {
137
+ try {
138
+ const covData = JSON.parse(fs.readFileSync(coveragePath, 'utf-8'));
139
+ if (covData.total && covData.total.lines && typeof covData.total.lines.pct === 'number') {
140
+ status.coverage = covData.total.lines.pct;
141
+ }
142
+ } catch {
143
+ // Malformed coverage file — leave as null
144
+ }
145
+ }
146
+
131
147
  return status;
132
148
  }
133
149
 
@@ -292,7 +308,7 @@ function readProjectBugs(projectPath) {
292
308
  * @returns {express.Router} Express router with workspace endpoints
293
309
  */
294
310
  function createWorkspaceRouter(options = {}) {
295
- const { globalConfig, projectScanner } = options;
311
+ const { globalConfig, projectScanner, memoryApi, memoryDeps = {} } = options;
296
312
 
297
313
  if (!globalConfig) {
298
314
  throw new Error('globalConfig is required');
@@ -302,6 +318,7 @@ function createWorkspaceRouter(options = {}) {
302
318
  }
303
319
 
304
320
  const router = express.Router();
321
+ const captureGuard = createCaptureGuard();
305
322
 
306
323
  // =========================================================================
307
324
  // GET /config - Returns workspace configuration
@@ -805,6 +822,170 @@ function createWorkspaceRouter(options = {}) {
805
822
  }
806
823
  });
807
824
 
825
+ // =========================================================================
826
+ // Memory API routes (Phase 77, fixed per-project in Phase 78)
827
+ // =========================================================================
828
+ if (memoryApi) {
829
+ router.get('/projects/:projectId/memory/decisions', async (req, res) => {
830
+ try {
831
+ const roots = globalConfig.getRoots();
832
+ const project = findProjectById(projectScanner, roots, req.params.projectId);
833
+ if (!project) return res.status(404).json({ error: 'Project not found' });
834
+ const adapter = createMemoryStoreAdapter(project.path);
835
+ const decisions = await adapter.listDecisions();
836
+ res.json({ decisions });
837
+ } catch (err) {
838
+ res.status(500).json({ error: err.message });
839
+ }
840
+ });
841
+
842
+ router.get('/projects/:projectId/memory/gotchas', async (req, res) => {
843
+ try {
844
+ const roots = globalConfig.getRoots();
845
+ const project = findProjectById(projectScanner, roots, req.params.projectId);
846
+ if (!project) return res.status(404).json({ error: 'Project not found' });
847
+ const adapter = createMemoryStoreAdapter(project.path);
848
+ const gotchas = await adapter.listGotchas();
849
+ res.json({ gotchas });
850
+ } catch (err) {
851
+ res.status(500).json({ error: err.message });
852
+ }
853
+ });
854
+
855
+ router.get('/projects/:projectId/memory/stats', async (req, res) => {
856
+ try {
857
+ const roots = globalConfig.getRoots();
858
+ const project = findProjectById(projectScanner, roots, req.params.projectId);
859
+ if (!project) return res.status(404).json({ error: 'Project not found' });
860
+ const adapter = createMemoryStoreAdapter(project.path);
861
+ const stats = await adapter.getStats();
862
+ res.json(stats);
863
+ } catch (err) {
864
+ res.status(500).json({ error: err.message });
865
+ }
866
+ });
867
+ }
868
+
869
+ // =========================================================================
870
+ // Memory capture endpoint (Phase 79 Task 5)
871
+ // =========================================================================
872
+ router.post('/projects/:projectId/memory/capture', async (req, res) => {
873
+ try {
874
+ const projectId = req.params.projectId;
875
+
876
+ // Rate limit check
877
+ const rateCheck = captureGuard.checkRateLimit(projectId);
878
+ if (!rateCheck.ok) {
879
+ return res.status(rateCheck.status).json({ error: rateCheck.error });
880
+ }
881
+
882
+ // Payload validation (size + structure)
883
+ const validation = captureGuard.validate(req.body, projectId);
884
+ if (!validation.ok) {
885
+ return res.status(validation.status).json({ error: validation.error });
886
+ }
887
+
888
+ const roots = globalConfig.getRoots();
889
+ const project = findProjectById(projectScanner, roots, projectId);
890
+ if (!project) return res.status(404).json({ error: 'Project not found' });
891
+
892
+ // Deduplicate exchanges
893
+ const exchanges = captureGuard.deduplicate(req.body.exchanges, projectId);
894
+
895
+ if (exchanges.length === 0) {
896
+ return res.json({ captured: 0, deduplicated: true });
897
+ }
898
+
899
+ // Process in background — respond immediately
900
+ let captured = 0;
901
+ const { observeAndRemember, vectorIndexer } = memoryDeps;
902
+
903
+ for (const exchange of exchanges) {
904
+ try {
905
+ if (typeof observeAndRemember === 'function') {
906
+ await observeAndRemember(project.path, exchange);
907
+ }
908
+ if (vectorIndexer && typeof vectorIndexer.indexChunk === 'function') {
909
+ await vectorIndexer.indexChunk(exchange);
910
+ }
911
+ captured++;
912
+ } catch {
913
+ // Individual exchange failures don't stop the batch
914
+ }
915
+ }
916
+
917
+ res.json({ captured });
918
+ } catch (err) {
919
+ res.status(500).json({ error: err.message });
920
+ }
921
+ });
922
+
923
+ // =========================================================================
924
+ // Memory search endpoint (Phase 79 Task 6)
925
+ // =========================================================================
926
+ router.get('/projects/:projectId/memory/search', async (req, res) => {
927
+ try {
928
+ const roots = globalConfig.getRoots();
929
+ const project = findProjectById(projectScanner, roots, req.params.projectId);
930
+ if (!project) return res.status(404).json({ error: 'Project not found' });
931
+
932
+ const query = req.query.q;
933
+ if (!query) {
934
+ return res.status(400).json({ error: 'Query parameter q is required' });
935
+ }
936
+
937
+ const { semanticRecall } = memoryDeps;
938
+
939
+ // Try vector-based semantic recall first
940
+ if (semanticRecall && typeof semanticRecall.recall === 'function') {
941
+ try {
942
+ const results = await semanticRecall.recall(query, { projectRoot: project.path });
943
+ return res.json({ results: results || [], source: 'vector' });
944
+ } catch {
945
+ // Fall through to file-based search
946
+ }
947
+ }
948
+
949
+ // Fallback: file-based text search
950
+ try {
951
+ const { searchMemory } = require('./memory-reader');
952
+ const results = await searchMemory(project.path, query);
953
+ return res.json({ results: results || [], source: 'file' });
954
+ } catch {
955
+ return res.json({ results: [], source: 'file' });
956
+ }
957
+ } catch (err) {
958
+ res.status(500).json({ error: err.message });
959
+ }
960
+ });
961
+
962
+ // =========================================================================
963
+ // Project file endpoint (Phase 77)
964
+ // =========================================================================
965
+ router.get('/projects/:projectId/files/:filename', (req, res) => {
966
+ try {
967
+ const roots = globalConfig.getRoots();
968
+ const project = findProjectById(projectScanner, roots, req.params.projectId);
969
+ if (!project) return res.status(404).json({ error: 'Project not found' });
970
+
971
+ const filename = req.params.filename;
972
+ // Reject path traversal
973
+ if (filename.includes('..') || filename.includes('/') || filename.includes('\\')) {
974
+ return res.status(400).json({ error: 'Invalid filename' });
975
+ }
976
+
977
+ const filePath = path.join(project.path, '.planning', filename);
978
+ if (!fs.existsSync(filePath)) {
979
+ return res.status(404).json({ error: 'File not found' });
980
+ }
981
+
982
+ const content = fs.readFileSync(filePath, 'utf-8');
983
+ res.json({ filename, content });
984
+ } catch (err) {
985
+ res.status(500).json({ error: err.message });
986
+ }
987
+ });
988
+
808
989
  return router;
809
990
  }
810
991