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,288 @@
1
+ import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
2
+ import fs from 'fs';
3
+ import path from 'path';
4
+ import os from 'os';
5
+
6
+ const { GlobalConfig } = await import('./global-config.js');
7
+
8
+ describe('GlobalConfig', () => {
9
+ let tempDir;
10
+ let originalEnv;
11
+
12
+ beforeEach(() => {
13
+ tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'global-config-test-'));
14
+ originalEnv = process.env.TLC_CONFIG_DIR;
15
+ process.env.TLC_CONFIG_DIR = tempDir;
16
+ });
17
+
18
+ afterEach(() => {
19
+ if (originalEnv !== undefined) {
20
+ process.env.TLC_CONFIG_DIR = originalEnv;
21
+ } else {
22
+ delete process.env.TLC_CONFIG_DIR;
23
+ }
24
+ fs.rmSync(tempDir, { recursive: true, force: true });
25
+ });
26
+
27
+ describe('initialization', () => {
28
+ it('creates config directory if not exists', () => {
29
+ const configDir = path.join(tempDir, 'subdir');
30
+ process.env.TLC_CONFIG_DIR = configDir;
31
+
32
+ const config = new GlobalConfig();
33
+ config.load();
34
+
35
+ expect(fs.existsSync(configDir)).toBe(true);
36
+ });
37
+
38
+ it('creates config file with defaults on first access', () => {
39
+ const config = new GlobalConfig();
40
+ const data = config.load();
41
+
42
+ expect(data).toBeDefined();
43
+ expect(data.version).toBe(1);
44
+ expect(data.roots).toEqual([]);
45
+ expect(data.scanDepth).toBe(5);
46
+ });
47
+
48
+ it('respects TLC_CONFIG_DIR environment variable', () => {
49
+ const customDir = path.join(tempDir, 'custom');
50
+ process.env.TLC_CONFIG_DIR = customDir;
51
+
52
+ const config = new GlobalConfig();
53
+ config.load();
54
+
55
+ const configPath = path.join(customDir, 'config.json');
56
+ expect(fs.existsSync(configPath)).toBe(true);
57
+ });
58
+
59
+ it('config schema has version field', () => {
60
+ const config = new GlobalConfig();
61
+ const data = config.load();
62
+
63
+ expect(data.version).toBe(1);
64
+ });
65
+ });
66
+
67
+ describe('getRoots', () => {
68
+ it('returns empty roots when not configured', () => {
69
+ const config = new GlobalConfig();
70
+
71
+ const roots = config.getRoots();
72
+
73
+ expect(roots).toEqual([]);
74
+ });
75
+
76
+ it('returns configured roots', () => {
77
+ const rootPath = fs.mkdtempSync(path.join(os.tmpdir(), 'root-'));
78
+
79
+ try {
80
+ const config = new GlobalConfig();
81
+ config.addRoot(rootPath);
82
+
83
+ const roots = config.getRoots();
84
+
85
+ expect(roots).toContain(rootPath);
86
+ } finally {
87
+ fs.rmSync(rootPath, { recursive: true, force: true });
88
+ }
89
+ });
90
+ });
91
+
92
+ describe('addRoot', () => {
93
+ it('adds root path and persists to disk', () => {
94
+ const rootPath = fs.mkdtempSync(path.join(os.tmpdir(), 'root-'));
95
+
96
+ try {
97
+ const config = new GlobalConfig();
98
+ config.addRoot(rootPath);
99
+
100
+ // Re-read from disk
101
+ const config2 = new GlobalConfig();
102
+ const roots = config2.getRoots();
103
+
104
+ expect(roots).toContain(rootPath);
105
+ } finally {
106
+ fs.rmSync(rootPath, { recursive: true, force: true });
107
+ }
108
+ });
109
+
110
+ it('rejects non-existent directory path', () => {
111
+ const config = new GlobalConfig();
112
+
113
+ expect(() => config.addRoot('/tmp/does-not-exist-xyz-123')).toThrow(/does not exist/i);
114
+ });
115
+
116
+ it('rejects file path (must be directory)', () => {
117
+ const filePath = path.join(tempDir, 'somefile.txt');
118
+ fs.writeFileSync(filePath, 'hello');
119
+
120
+ const config = new GlobalConfig();
121
+
122
+ expect(() => config.addRoot(filePath)).toThrow(/not a directory/i);
123
+ });
124
+
125
+ it('duplicate root paths rejected', () => {
126
+ const rootPath = fs.mkdtempSync(path.join(os.tmpdir(), 'root-'));
127
+
128
+ try {
129
+ const config = new GlobalConfig();
130
+ config.addRoot(rootPath);
131
+
132
+ expect(() => config.addRoot(rootPath)).toThrow(/already configured/i);
133
+ } finally {
134
+ fs.rmSync(rootPath, { recursive: true, force: true });
135
+ }
136
+ });
137
+
138
+ it('multiple roots supported', () => {
139
+ const root1 = fs.mkdtempSync(path.join(os.tmpdir(), 'root1-'));
140
+ const root2 = fs.mkdtempSync(path.join(os.tmpdir(), 'root2-'));
141
+
142
+ try {
143
+ const config = new GlobalConfig();
144
+ config.addRoot(root1);
145
+ config.addRoot(root2);
146
+
147
+ const roots = config.getRoots();
148
+
149
+ expect(roots).toHaveLength(2);
150
+ expect(roots).toContain(root1);
151
+ expect(roots).toContain(root2);
152
+ } finally {
153
+ fs.rmSync(root1, { recursive: true, force: true });
154
+ fs.rmSync(root2, { recursive: true, force: true });
155
+ }
156
+ });
157
+ });
158
+
159
+ describe('removeRoot', () => {
160
+ it('removes root path', () => {
161
+ const rootPath = fs.mkdtempSync(path.join(os.tmpdir(), 'root-'));
162
+
163
+ try {
164
+ const config = new GlobalConfig();
165
+ config.addRoot(rootPath);
166
+ config.removeRoot(rootPath);
167
+
168
+ const roots = config.getRoots();
169
+
170
+ expect(roots).not.toContain(rootPath);
171
+ } finally {
172
+ fs.rmSync(rootPath, { recursive: true, force: true });
173
+ }
174
+ });
175
+
176
+ it('removing non-existent root does not throw', () => {
177
+ const config = new GlobalConfig();
178
+
179
+ expect(() => config.removeRoot('/some/path')).not.toThrow();
180
+ });
181
+ });
182
+
183
+ describe('isConfigured', () => {
184
+ it('returns false with no roots', () => {
185
+ const config = new GlobalConfig();
186
+
187
+ expect(config.isConfigured()).toBe(false);
188
+ });
189
+
190
+ it('returns true with roots', () => {
191
+ const rootPath = fs.mkdtempSync(path.join(os.tmpdir(), 'root-'));
192
+
193
+ try {
194
+ const config = new GlobalConfig();
195
+ config.addRoot(rootPath);
196
+
197
+ expect(config.isConfigured()).toBe(true);
198
+ } finally {
199
+ fs.rmSync(rootPath, { recursive: true, force: true });
200
+ }
201
+ });
202
+ });
203
+
204
+ describe('scanDepth', () => {
205
+ it('defaults to 5', () => {
206
+ const config = new GlobalConfig();
207
+ const data = config.load();
208
+
209
+ expect(data.scanDepth).toBe(5);
210
+ });
211
+
212
+ it('can be updated', () => {
213
+ const config = new GlobalConfig();
214
+ config.setScanDepth(3);
215
+
216
+ const config2 = new GlobalConfig();
217
+ const data = config2.load();
218
+
219
+ expect(data.scanDepth).toBe(3);
220
+ });
221
+ });
222
+
223
+ describe('lastScan tracking', () => {
224
+ it('stores lastScan timestamp per root', () => {
225
+ const rootPath = fs.mkdtempSync(path.join(os.tmpdir(), 'root-'));
226
+
227
+ try {
228
+ const config = new GlobalConfig();
229
+ config.addRoot(rootPath);
230
+
231
+ const now = Date.now();
232
+ config.setLastScan(rootPath, now);
233
+
234
+ const lastScan = config.getLastScan(rootPath);
235
+
236
+ expect(lastScan).toBe(now);
237
+ } finally {
238
+ fs.rmSync(rootPath, { recursive: true, force: true });
239
+ }
240
+ });
241
+
242
+ it('returns null for root that has not been scanned', () => {
243
+ const rootPath = fs.mkdtempSync(path.join(os.tmpdir(), 'root-'));
244
+
245
+ try {
246
+ const config = new GlobalConfig();
247
+ config.addRoot(rootPath);
248
+
249
+ const lastScan = config.getLastScan(rootPath);
250
+
251
+ expect(lastScan).toBeNull();
252
+ } finally {
253
+ fs.rmSync(rootPath, { recursive: true, force: true });
254
+ }
255
+ });
256
+ });
257
+
258
+ describe('error handling', () => {
259
+ it('handles corrupted JSON gracefully (resets to defaults)', () => {
260
+ const configPath = path.join(tempDir, 'config.json');
261
+ fs.writeFileSync(configPath, '{invalid json!!!');
262
+
263
+ const config = new GlobalConfig();
264
+ const data = config.load();
265
+
266
+ expect(data.version).toBe(1);
267
+ expect(data.roots).toEqual([]);
268
+ });
269
+
270
+ it('atomic write prevents partial file corruption', () => {
271
+ const rootPath = fs.mkdtempSync(path.join(os.tmpdir(), 'root-'));
272
+
273
+ try {
274
+ const config = new GlobalConfig();
275
+ config.addRoot(rootPath);
276
+
277
+ // Verify file is valid JSON after write
278
+ const configPath = path.join(tempDir, 'config.json');
279
+ const raw = fs.readFileSync(configPath, 'utf-8');
280
+ const parsed = JSON.parse(raw);
281
+
282
+ expect(parsed.roots).toContain(rootPath);
283
+ } finally {
284
+ fs.rmSync(rootPath, { recursive: true, force: true });
285
+ }
286
+ });
287
+ });
288
+ });
@@ -0,0 +1,184 @@
1
+ /**
2
+ * Inherited Search — wraps semantic-recall with inheritance-aware search
3
+ * that walks from project scope up through workspace scope, adjusting
4
+ * scores and deduplicating results.
5
+ *
6
+ * Workspace results receive a 0.8x score multiplier to prefer local
7
+ * project memories while still surfacing relevant workspace knowledge.
8
+ *
9
+ * Auto-widening: when project scope returns fewer than 3 results,
10
+ * the search automatically widens to include workspace results.
11
+ *
12
+ * @module inherited-search
13
+ */
14
+
15
+ /** Minimum results before auto-widening from project to workspace */
16
+ const AUTO_WIDEN_THRESHOLD = 3;
17
+
18
+ /** Score multiplier for workspace-level results */
19
+ const WORKSPACE_SCORE_MULTIPLIER = 0.8;
20
+
21
+ /**
22
+ * Deduplicate results by id, keeping the entry with the highest score.
23
+ *
24
+ * @param {Array} results - Scored results that may contain duplicates
25
+ * @returns {Array} Deduplicated results
26
+ */
27
+ function deduplicateById(results) {
28
+ const bestById = new Map();
29
+
30
+ for (const result of results) {
31
+ const existing = bestById.get(result.id);
32
+ if (!existing || result.score > existing.score) {
33
+ bestById.set(result.id, result);
34
+ }
35
+ }
36
+
37
+ return [...bestById.values()];
38
+ }
39
+
40
+ /**
41
+ * Apply a score multiplier to an array of results, returning new objects.
42
+ *
43
+ * @param {Array} results - Search results
44
+ * @param {number} multiplier - Score multiplier to apply
45
+ * @returns {Array} Results with adjusted scores
46
+ */
47
+ function applyScoreMultiplier(results, multiplier) {
48
+ return results.map((r) => ({ ...r, score: r.score * multiplier }));
49
+ }
50
+
51
+ /**
52
+ * Create an inherited search instance that wraps semantic-recall with
53
+ * inheritance-aware scope walking.
54
+ *
55
+ * @param {object} deps - Dependencies
56
+ * @param {object} deps.semanticRecall - Semantic recall instance (from Phase 71)
57
+ * @param {object} deps.workspaceDetector - Workspace detector instance (from Task 1)
58
+ * @param {object} deps.vectorIndexer - Vector indexer instance (from Phase 71)
59
+ * @returns {object} Object with search and indexAll methods
60
+ */
61
+ export function createInheritedSearch({ semanticRecall, workspaceDetector, vectorIndexer }) {
62
+ /**
63
+ * Fetch workspace-scope results using the workspace root as the context
64
+ * workspace, applying the 0.8x score multiplier.
65
+ *
66
+ * @param {string} query - Search query
67
+ * @param {object} context - Original context
68
+ * @param {object} options - Search options (without scope)
69
+ * @param {string} workspaceRoot - Workspace root path
70
+ * @returns {Promise<Array>} Workspace results with adjusted scores
71
+ */
72
+ async function fetchWorkspaceResults(query, context, options, workspaceRoot) {
73
+ const wsContext = { ...context, workspace: workspaceRoot };
74
+ const wsOptions = { ...options, scope: 'workspace' };
75
+ const wsResults = await semanticRecall.recall(query, wsContext, wsOptions);
76
+ return applyScoreMultiplier(wsResults, WORKSPACE_SCORE_MULTIPLIER);
77
+ }
78
+
79
+ /**
80
+ * Search with inheritance-aware scope walking.
81
+ *
82
+ * Scope behavior:
83
+ * - 'project': search project only; auto-widen to workspace if < 3 results
84
+ * - 'workspace': search workspace only (delegates directly to semanticRecall)
85
+ * - 'inherited': always search both project and workspace, merge results
86
+ * - 'global': delegates directly to semanticRecall with global scope
87
+ *
88
+ * @param {string} query - Search query text
89
+ * @param {object} context - Current context
90
+ * @param {object} [options] - Search options
91
+ * @param {string} [options.scope='project'] - Search scope
92
+ * @returns {Promise<Array>} Scored and ranked results
93
+ */
94
+ async function search(query, context, options = {}) {
95
+ const { scope = 'project', ...restOptions } = options;
96
+
97
+ // For global or workspace scope, pass through directly
98
+ if (scope === 'global' || scope === 'workspace') {
99
+ return semanticRecall.recall(query, context, { ...restOptions, scope });
100
+ }
101
+
102
+ // Detect workspace info for the current project
103
+ const wsInfo = workspaceDetector.detectWorkspace(context.workspace);
104
+
105
+ // For inherited scope: always search both project and workspace
106
+ if (scope === 'inherited') {
107
+ const projectResults = await semanticRecall.recall(
108
+ query,
109
+ context,
110
+ { ...restOptions, scope: 'project' },
111
+ );
112
+
113
+ // If not in a workspace, return project results only
114
+ if (!wsInfo.isInWorkspace || !wsInfo.workspaceRoot) {
115
+ return projectResults;
116
+ }
117
+
118
+ const wsResults = await fetchWorkspaceResults(
119
+ query,
120
+ context,
121
+ restOptions,
122
+ wsInfo.workspaceRoot,
123
+ );
124
+
125
+ // Merge, deduplicate, and sort
126
+ const merged = deduplicateById([...projectResults, ...wsResults]);
127
+ merged.sort((a, b) => b.score - a.score);
128
+ return merged;
129
+ }
130
+
131
+ // scope === 'project': search project first, auto-widen if needed
132
+ const projectResults = await semanticRecall.recall(
133
+ query,
134
+ context,
135
+ { ...restOptions, scope: 'project' },
136
+ );
137
+
138
+ // If enough project results or not in a workspace, return as-is
139
+ if (
140
+ projectResults.length >= AUTO_WIDEN_THRESHOLD
141
+ || !wsInfo.isInWorkspace
142
+ || !wsInfo.workspaceRoot
143
+ ) {
144
+ return projectResults;
145
+ }
146
+
147
+ // Auto-widen: fetch workspace results and merge
148
+ const wsResults = await fetchWorkspaceResults(
149
+ query,
150
+ context,
151
+ restOptions,
152
+ wsInfo.workspaceRoot,
153
+ );
154
+
155
+ const merged = deduplicateById([...projectResults, ...wsResults]);
156
+ merged.sort((a, b) => b.score - a.score);
157
+ return merged;
158
+ }
159
+
160
+ /**
161
+ * Index all memory for a project, including workspace memory if applicable.
162
+ *
163
+ * @param {string} projectRoot - Absolute path to the project root
164
+ * @returns {Promise<object>} Combined indexing results
165
+ */
166
+ async function indexAll(projectRoot) {
167
+ const projectResult = await vectorIndexer.indexAll(projectRoot);
168
+
169
+ const wsInfo = workspaceDetector.detectWorkspace(projectRoot);
170
+
171
+ if (wsInfo.isInWorkspace && wsInfo.workspaceRoot) {
172
+ const wsResult = await vectorIndexer.indexAll(wsInfo.workspaceRoot);
173
+ return {
174
+ indexed: projectResult.indexed + wsResult.indexed,
175
+ skipped: projectResult.skipped + wsResult.skipped,
176
+ errors: projectResult.errors + wsResult.errors,
177
+ };
178
+ }
179
+
180
+ return projectResult;
181
+ }
182
+
183
+ return { search, indexAll };
184
+ }