tlc-claude-code 1.8.4 → 2.0.1

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 (77) 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/recall.md +87 -0
  4. package/.claude/commands/tlc/remember.md +71 -0
  5. package/CLAUDE.md +84 -201
  6. package/dashboard-web/dist/assets/index-Uhc49PE-.css +1 -0
  7. package/dashboard-web/dist/assets/index-W36XHPC5.js +431 -0
  8. package/dashboard-web/dist/assets/index-W36XHPC5.js.map +1 -0
  9. package/dashboard-web/dist/index.html +2 -2
  10. package/package.json +1 -1
  11. package/server/index.js +29 -4
  12. package/server/lib/bug-writer.js +204 -0
  13. package/server/lib/bug-writer.test.js +279 -0
  14. package/server/lib/claude-cascade.js +247 -0
  15. package/server/lib/claude-cascade.test.js +245 -0
  16. package/server/lib/context-injection.js +121 -0
  17. package/server/lib/context-injection.test.js +340 -0
  18. package/server/lib/conversation-chunker.js +320 -0
  19. package/server/lib/conversation-chunker.test.js +573 -0
  20. package/server/lib/embedding-client.js +160 -0
  21. package/server/lib/embedding-client.test.js +243 -0
  22. package/server/lib/global-config.js +198 -0
  23. package/server/lib/global-config.test.js +288 -0
  24. package/server/lib/inherited-search.js +184 -0
  25. package/server/lib/inherited-search.test.js +343 -0
  26. package/server/lib/memory-api.js +180 -0
  27. package/server/lib/memory-api.test.js +322 -0
  28. package/server/lib/memory-hooks-capture.test.js +350 -0
  29. package/server/lib/memory-hooks.js +101 -0
  30. package/server/lib/memory-inheritance.js +179 -0
  31. package/server/lib/memory-inheritance.test.js +360 -0
  32. package/server/lib/plan-parser.js +33 -7
  33. package/server/lib/plan-writer.js +196 -0
  34. package/server/lib/plan-writer.test.js +298 -0
  35. package/server/lib/project-scanner.js +267 -0
  36. package/server/lib/project-scanner.test.js +389 -0
  37. package/server/lib/project-status.js +302 -0
  38. package/server/lib/project-status.test.js +470 -0
  39. package/server/lib/projects-registry.js +237 -0
  40. package/server/lib/projects-registry.test.js +275 -0
  41. package/server/lib/recall-command.js +207 -0
  42. package/server/lib/recall-command.test.js +306 -0
  43. package/server/lib/remember-command.js +96 -0
  44. package/server/lib/remember-command.test.js +265 -0
  45. package/server/lib/rich-capture.js +221 -0
  46. package/server/lib/rich-capture.test.js +312 -0
  47. package/server/lib/roadmap-api.js +200 -0
  48. package/server/lib/roadmap-api.test.js +318 -0
  49. package/server/lib/semantic-recall.js +242 -0
  50. package/server/lib/semantic-recall.test.js +446 -0
  51. package/server/lib/setup-generator.js +315 -0
  52. package/server/lib/setup-generator.test.js +303 -0
  53. package/server/lib/test-inventory.js +112 -0
  54. package/server/lib/test-inventory.test.js +360 -0
  55. package/server/lib/vector-indexer.js +246 -0
  56. package/server/lib/vector-indexer.test.js +459 -0
  57. package/server/lib/vector-store.js +260 -0
  58. package/server/lib/vector-store.test.js +706 -0
  59. package/server/lib/workspace-api.js +811 -0
  60. package/server/lib/workspace-api.test.js +743 -0
  61. package/server/lib/workspace-bootstrap.js +164 -0
  62. package/server/lib/workspace-bootstrap.test.js +503 -0
  63. package/server/lib/workspace-context.js +129 -0
  64. package/server/lib/workspace-context.test.js +214 -0
  65. package/server/lib/workspace-detector.js +162 -0
  66. package/server/lib/workspace-detector.test.js +193 -0
  67. package/server/lib/workspace-init.js +307 -0
  68. package/server/lib/workspace-init.test.js +244 -0
  69. package/server/lib/workspace-snapshot.js +236 -0
  70. package/server/lib/workspace-snapshot.test.js +444 -0
  71. package/server/lib/workspace-watcher.js +162 -0
  72. package/server/lib/workspace-watcher.test.js +257 -0
  73. package/server/package-lock.json +552 -0
  74. package/server/package.json +4 -0
  75. package/dashboard-web/dist/assets/index-B1I_joSL.js +0 -393
  76. package/dashboard-web/dist/assets/index-B1I_joSL.js.map +0 -1
  77. package/dashboard-web/dist/assets/index-Trhg1C1Y.css +0 -1
@@ -0,0 +1,343 @@
1
+ /**
2
+ * Inherited Search Tests
3
+ *
4
+ * Tests for inheritance-aware semantic search that wraps semantic-recall
5
+ * to search across project and workspace scopes with score adjustment,
6
+ * deduplication, and auto-widening.
7
+ *
8
+ * @module inherited-search.test
9
+ */
10
+
11
+ import { describe, it, beforeEach, afterEach, expect, vi } from 'vitest';
12
+ import { createInheritedSearch } from './inherited-search.js';
13
+
14
+ /**
15
+ * Creates a mock semantic recall instance with vi.fn() methods.
16
+ * @returns {object} Mock semantic recall
17
+ */
18
+ function createMockSemanticRecall() {
19
+ return {
20
+ recall: vi.fn().mockResolvedValue([]),
21
+ recallForContext: vi.fn().mockResolvedValue([]),
22
+ };
23
+ }
24
+
25
+ /**
26
+ * Creates a mock workspace detector instance.
27
+ * @returns {object} Mock workspace detector
28
+ */
29
+ function createMockWorkspaceDetector() {
30
+ return {
31
+ detectWorkspace: vi.fn().mockReturnValue({
32
+ isInWorkspace: true,
33
+ workspaceRoot: '/ws',
34
+ projectPath: '/ws/my-project',
35
+ relativeProjectPath: 'my-project',
36
+ }),
37
+ };
38
+ }
39
+
40
+ /**
41
+ * Creates a mock vector indexer instance.
42
+ * @returns {object} Mock vector indexer
43
+ */
44
+ function createMockVectorIndexer() {
45
+ return {
46
+ indexAll: vi.fn().mockResolvedValue({ indexed: 0, skipped: 0, errors: 0 }),
47
+ indexFile: vi.fn().mockResolvedValue({ success: true }),
48
+ indexChunk: vi.fn().mockResolvedValue({ success: true }),
49
+ isIndexed: vi.fn().mockResolvedValue(false),
50
+ rebuildIndex: vi.fn().mockResolvedValue(undefined),
51
+ };
52
+ }
53
+
54
+ /**
55
+ * Creates a mock search result.
56
+ * @param {object} overrides - Properties to override
57
+ * @returns {object} Mock search result matching semantic-recall output shape
58
+ */
59
+ function createResult(overrides = {}) {
60
+ return {
61
+ id: 'mem-1',
62
+ text: 'Some memory text',
63
+ score: 0.85,
64
+ type: 'decision',
65
+ source: {
66
+ project: 'my-project',
67
+ workspace: '/ws/my-project',
68
+ branch: 'main',
69
+ sourceFile: 'decisions/something.md',
70
+ },
71
+ date: Date.now() - 86400000,
72
+ permanent: false,
73
+ ...overrides,
74
+ };
75
+ }
76
+
77
+ describe('inherited-search', () => {
78
+ let mockSemanticRecall;
79
+ let mockWorkspaceDetector;
80
+ let mockVectorIndexer;
81
+ let search;
82
+
83
+ beforeEach(() => {
84
+ mockSemanticRecall = createMockSemanticRecall();
85
+ mockWorkspaceDetector = createMockWorkspaceDetector();
86
+ mockVectorIndexer = createMockVectorIndexer();
87
+ search = createInheritedSearch({
88
+ semanticRecall: mockSemanticRecall,
89
+ workspaceDetector: mockWorkspaceDetector,
90
+ vectorIndexer: mockVectorIndexer,
91
+ });
92
+ });
93
+
94
+ afterEach(() => {
95
+ vi.restoreAllMocks();
96
+ });
97
+
98
+ describe('search', () => {
99
+ it('project-scope search returns project memories only', async () => {
100
+ const projectResults = [
101
+ createResult({ id: 'p-1', score: 0.90 }),
102
+ createResult({ id: 'p-2', score: 0.80 }),
103
+ createResult({ id: 'p-3', score: 0.70 }),
104
+ ];
105
+ mockSemanticRecall.recall.mockResolvedValue(projectResults);
106
+
107
+ const context = { projectId: 'my-project', workspace: '/ws/my-project' };
108
+ const results = await search.search('query', context, { scope: 'project' });
109
+
110
+ // Should call recall with scope 'project'
111
+ expect(mockSemanticRecall.recall).toHaveBeenCalledWith(
112
+ 'query',
113
+ context,
114
+ expect.objectContaining({ scope: 'project' }),
115
+ );
116
+ // Should return project results unmodified (no score adjustment)
117
+ expect(results).toHaveLength(3);
118
+ expect(results[0].id).toBe('p-1');
119
+ expect(results[0].score).toBe(0.90);
120
+ });
121
+
122
+ it('auto-widens to workspace when project returns < 3 results', async () => {
123
+ const projectResults = [
124
+ createResult({ id: 'p-1', score: 0.90 }),
125
+ createResult({ id: 'p-2', score: 0.80 }),
126
+ ];
127
+ const workspaceResults = [
128
+ createResult({ id: 'ws-1', score: 0.75 }),
129
+ createResult({ id: 'ws-2', score: 0.65 }),
130
+ createResult({ id: 'ws-3', score: 0.55 }),
131
+ ];
132
+
133
+ // First call (project scope) returns < 3 results
134
+ // Second call (workspace scope) returns more results
135
+ mockSemanticRecall.recall
136
+ .mockResolvedValueOnce(projectResults)
137
+ .mockResolvedValueOnce(workspaceResults);
138
+
139
+ const context = { projectId: 'my-project', workspace: '/ws/my-project' };
140
+ const results = await search.search('query', context, { scope: 'project' });
141
+
142
+ // Should have called recall twice: once for project, once for workspace
143
+ expect(mockSemanticRecall.recall).toHaveBeenCalledTimes(2);
144
+ expect(mockSemanticRecall.recall).toHaveBeenNthCalledWith(
145
+ 1,
146
+ 'query',
147
+ context,
148
+ expect.objectContaining({ scope: 'project' }),
149
+ );
150
+ expect(mockSemanticRecall.recall).toHaveBeenNthCalledWith(
151
+ 2,
152
+ 'query',
153
+ expect.anything(),
154
+ expect.objectContaining({ scope: 'workspace' }),
155
+ );
156
+
157
+ // Should include both project results (full score) and workspace results (0.8x)
158
+ expect(results.length).toBeGreaterThan(2);
159
+ });
160
+
161
+ it('workspace results scored 0.8x lower than project results', async () => {
162
+ const projectResults = [
163
+ createResult({ id: 'p-1', score: 0.90 }),
164
+ ];
165
+ const workspaceResults = [
166
+ createResult({ id: 'ws-1', score: 0.80 }),
167
+ ];
168
+
169
+ mockSemanticRecall.recall
170
+ .mockResolvedValueOnce(projectResults)
171
+ .mockResolvedValueOnce(workspaceResults);
172
+
173
+ const context = { projectId: 'my-project', workspace: '/ws/my-project' };
174
+ const results = await search.search('query', context, { scope: 'project' });
175
+
176
+ // Project result keeps original score
177
+ const projectResult = results.find((r) => r.id === 'p-1');
178
+ expect(projectResult.score).toBe(0.90);
179
+
180
+ // Workspace result gets 0.8x multiplier
181
+ const wsResult = results.find((r) => r.id === 'ws-1');
182
+ expect(wsResult.score).toBeCloseTo(0.80 * 0.8, 5);
183
+ });
184
+
185
+ it('explicit inherited scope searches both project and workspace', async () => {
186
+ const projectResults = [
187
+ createResult({ id: 'p-1', score: 0.90 }),
188
+ createResult({ id: 'p-2', score: 0.85 }),
189
+ createResult({ id: 'p-3', score: 0.80 }),
190
+ ];
191
+ const workspaceResults = [
192
+ createResult({ id: 'ws-1', score: 0.75 }),
193
+ ];
194
+
195
+ mockSemanticRecall.recall
196
+ .mockResolvedValueOnce(projectResults)
197
+ .mockResolvedValueOnce(workspaceResults);
198
+
199
+ const context = { projectId: 'my-project', workspace: '/ws/my-project' };
200
+ const results = await search.search('query', context, { scope: 'inherited' });
201
+
202
+ // Should always call recall twice for inherited scope
203
+ expect(mockSemanticRecall.recall).toHaveBeenCalledTimes(2);
204
+ expect(mockSemanticRecall.recall).toHaveBeenNthCalledWith(
205
+ 1,
206
+ 'query',
207
+ context,
208
+ expect.objectContaining({ scope: 'project' }),
209
+ );
210
+ expect(mockSemanticRecall.recall).toHaveBeenNthCalledWith(
211
+ 2,
212
+ 'query',
213
+ expect.anything(),
214
+ expect.objectContaining({ scope: 'workspace' }),
215
+ );
216
+
217
+ // All results should be present (project at full score, workspace at 0.8x)
218
+ expect(results).toHaveLength(4);
219
+ });
220
+
221
+ it('deduplication across scopes keeps higher-scoring entry', async () => {
222
+ // Same id appears in both project (high score) and workspace (lower score after 0.8x)
223
+ const projectResults = [
224
+ createResult({ id: 'shared-1', score: 0.70 }),
225
+ ];
226
+ const workspaceResults = [
227
+ createResult({ id: 'shared-1', score: 0.95 }), // after 0.8x = 0.76
228
+ ];
229
+
230
+ mockSemanticRecall.recall
231
+ .mockResolvedValueOnce(projectResults)
232
+ .mockResolvedValueOnce(workspaceResults);
233
+
234
+ const context = { projectId: 'my-project', workspace: '/ws/my-project' };
235
+ const results = await search.search('query', context, { scope: 'inherited' });
236
+
237
+ // Should have only one entry for shared-1
238
+ const shared = results.filter((r) => r.id === 'shared-1');
239
+ expect(shared).toHaveLength(1);
240
+
241
+ // Workspace version after 0.8x = 0.76, project version = 0.70
242
+ // The workspace version (0.76) should win because it's higher after multiplier
243
+ expect(shared[0].score).toBeCloseTo(0.95 * 0.8, 5);
244
+ });
245
+
246
+ it('standalone project does not search workspace', async () => {
247
+ // Workspace detector returns standalone (not in workspace)
248
+ mockWorkspaceDetector.detectWorkspace.mockReturnValue({
249
+ isInWorkspace: false,
250
+ workspaceRoot: null,
251
+ projectPath: '/standalone-project',
252
+ relativeProjectPath: null,
253
+ });
254
+
255
+ const projectResults = [
256
+ createResult({ id: 'p-1', score: 0.90 }),
257
+ ];
258
+ mockSemanticRecall.recall.mockResolvedValue(projectResults);
259
+
260
+ const context = { projectId: 'my-project', workspace: '/standalone-project' };
261
+ const results = await search.search('query', context, { scope: 'inherited' });
262
+
263
+ // For standalone projects, should only call recall once (project scope only)
264
+ expect(mockSemanticRecall.recall).toHaveBeenCalledTimes(1);
265
+ expect(results).toHaveLength(1);
266
+ expect(results[0].id).toBe('p-1');
267
+ });
268
+
269
+ it('combined ranking sorts correctly across scopes', async () => {
270
+ const projectResults = [
271
+ createResult({ id: 'p-1', score: 0.60 }),
272
+ createResult({ id: 'p-2', score: 0.50 }),
273
+ ];
274
+ const workspaceResults = [
275
+ createResult({ id: 'ws-1', score: 0.90 }), // after 0.8x = 0.72
276
+ createResult({ id: 'ws-2', score: 0.70 }), // after 0.8x = 0.56
277
+ ];
278
+
279
+ mockSemanticRecall.recall
280
+ .mockResolvedValueOnce(projectResults)
281
+ .mockResolvedValueOnce(workspaceResults);
282
+
283
+ const context = { projectId: 'my-project', workspace: '/ws/my-project' };
284
+ const results = await search.search('query', context, { scope: 'inherited' });
285
+
286
+ // Expected order: ws-1(0.72), p-1(0.60), ws-2(0.56), p-2(0.50)
287
+ expect(results).toHaveLength(4);
288
+ for (let i = 0; i < results.length - 1; i++) {
289
+ expect(results[i].score).toBeGreaterThanOrEqual(results[i + 1].score);
290
+ }
291
+ expect(results[0].id).toBe('ws-1');
292
+ expect(results[1].id).toBe('p-1');
293
+ expect(results[2].id).toBe('ws-2');
294
+ expect(results[3].id).toBe('p-2');
295
+ });
296
+
297
+ it('returns empty when no results from any scope', async () => {
298
+ mockSemanticRecall.recall.mockResolvedValue([]);
299
+
300
+ const context = { projectId: 'my-project', workspace: '/ws/my-project' };
301
+ const results = await search.search('query', context, { scope: 'inherited' });
302
+
303
+ expect(results).toEqual([]);
304
+ });
305
+ });
306
+
307
+ describe('indexAll', () => {
308
+ it('processes workspace memory path when in workspace', async () => {
309
+ mockWorkspaceDetector.detectWorkspace.mockReturnValue({
310
+ isInWorkspace: true,
311
+ workspaceRoot: '/ws',
312
+ projectPath: '/ws/my-project',
313
+ relativeProjectPath: 'my-project',
314
+ });
315
+
316
+ mockVectorIndexer.indexAll.mockResolvedValue({ indexed: 5, skipped: 0, errors: 0 });
317
+
318
+ const result = await search.indexAll('/ws/my-project');
319
+
320
+ // Should index both the project path and the workspace root
321
+ expect(mockVectorIndexer.indexAll).toHaveBeenCalledTimes(2);
322
+ expect(mockVectorIndexer.indexAll).toHaveBeenCalledWith('/ws/my-project');
323
+ expect(mockVectorIndexer.indexAll).toHaveBeenCalledWith('/ws');
324
+ });
325
+
326
+ it('processes only project path for standalone', async () => {
327
+ mockWorkspaceDetector.detectWorkspace.mockReturnValue({
328
+ isInWorkspace: false,
329
+ workspaceRoot: null,
330
+ projectPath: '/standalone-project',
331
+ relativeProjectPath: null,
332
+ });
333
+
334
+ mockVectorIndexer.indexAll.mockResolvedValue({ indexed: 3, skipped: 0, errors: 0 });
335
+
336
+ const result = await search.indexAll('/standalone-project');
337
+
338
+ // Should only index the project path (no workspace)
339
+ expect(mockVectorIndexer.indexAll).toHaveBeenCalledTimes(1);
340
+ expect(mockVectorIndexer.indexAll).toHaveBeenCalledWith('/standalone-project');
341
+ });
342
+ });
343
+ });
@@ -0,0 +1,180 @@
1
+ /**
2
+ * Memory API — HTTP handler factory for memory-related endpoints.
3
+ *
4
+ * Factory function `createMemoryApi` accepts injected dependencies and returns
5
+ * handler methods that accept Express-compatible (req, res) objects.
6
+ *
7
+ * Endpoints provided:
8
+ * - handleSearch — semantic search across memory
9
+ * - handleListConversations — paginated conversation list
10
+ * - handleGetConversation — single conversation detail
11
+ * - handleListDecisions — list all decisions
12
+ * - handleListGotchas — list all gotchas
13
+ * - handleGetStats — vector DB statistics
14
+ * - handleRebuild — trigger vector index rebuild
15
+ * - handleRemember — store permanent memory
16
+ *
17
+ * @module memory-api
18
+ */
19
+
20
+ /**
21
+ * Create a memory API handler instance.
22
+ *
23
+ * @param {object} deps
24
+ * @param {object} deps.semanticRecall - Semantic recall with recall(query, context, options)
25
+ * @param {object} deps.vectorIndexer - Vector indexer with indexAll(projectRoot)
26
+ * @param {object} deps.richCapture - Rich capture with processChunk(text, metadata)
27
+ * @param {object} deps.embeddingClient - Embedding client with embed(text)
28
+ * @param {object} deps.memoryStore - Memory store with list/get methods
29
+ * @returns {object} Handler methods for each memory endpoint
30
+ */
31
+ export function createMemoryApi({ semanticRecall, vectorIndexer, richCapture, embeddingClient, memoryStore }) {
32
+
33
+ /**
34
+ * Search memory semantically.
35
+ *
36
+ * Query params: q (search query), scope (project|workspace|global)
37
+ *
38
+ * @param {object} req - Express request
39
+ * @param {object} res - Express response
40
+ */
41
+ async function handleSearch(req, res) {
42
+ const { q = '', scope } = req.query;
43
+
44
+ if (!q) {
45
+ return res.json({ results: [] });
46
+ }
47
+
48
+ const context = {};
49
+ const options = {};
50
+ if (scope) {
51
+ options.scope = scope;
52
+ }
53
+
54
+ const results = await semanticRecall.recall(q, context, options);
55
+ res.json({ results });
56
+ }
57
+
58
+ /**
59
+ * List conversations with pagination.
60
+ *
61
+ * Query params: page, limit, project
62
+ *
63
+ * @param {object} req - Express request
64
+ * @param {object} res - Express response
65
+ */
66
+ async function handleListConversations(req, res) {
67
+ const page = parseInt(req.query.page, 10) || 1;
68
+ const limit = parseInt(req.query.limit, 10) || 20;
69
+ const { project } = req.query;
70
+
71
+ const options = { page, limit };
72
+ if (project) {
73
+ options.project = project;
74
+ }
75
+
76
+ const result = await memoryStore.listConversations(options);
77
+ res.json({ items: result.items, total: result.total });
78
+ }
79
+
80
+ /**
81
+ * Get a single conversation by ID.
82
+ *
83
+ * @param {object} req - Express request with params.id
84
+ * @param {object} res - Express response
85
+ */
86
+ async function handleGetConversation(req, res) {
87
+ const { id } = req.params;
88
+ const conversation = await memoryStore.getConversation(id);
89
+
90
+ if (!conversation) {
91
+ return res.status(404).json({ error: 'Conversation not found' });
92
+ }
93
+
94
+ res.json(conversation);
95
+ }
96
+
97
+ /**
98
+ * List all decisions, optionally filtered by project.
99
+ *
100
+ * @param {object} req - Express request
101
+ * @param {object} res - Express response
102
+ */
103
+ async function handleListDecisions(req, res) {
104
+ const { project } = req.query;
105
+ const options = {};
106
+ if (project) {
107
+ options.project = project;
108
+ }
109
+
110
+ const decisions = await memoryStore.listDecisions(options);
111
+ res.json({ decisions });
112
+ }
113
+
114
+ /**
115
+ * List all gotchas, optionally filtered by project.
116
+ *
117
+ * @param {object} req - Express request
118
+ * @param {object} res - Express response
119
+ */
120
+ async function handleListGotchas(req, res) {
121
+ const { project } = req.query;
122
+ const options = {};
123
+ if (project) {
124
+ options.project = project;
125
+ }
126
+
127
+ const gotchas = await memoryStore.listGotchas(options);
128
+ res.json({ gotchas });
129
+ }
130
+
131
+ /**
132
+ * Get vector DB statistics.
133
+ *
134
+ * @param {object} req - Express request
135
+ * @param {object} res - Express response
136
+ */
137
+ async function handleGetStats(req, res) {
138
+ const stats = await memoryStore.getStats();
139
+ res.json(stats);
140
+ }
141
+
142
+ /**
143
+ * Trigger a full vector index rebuild.
144
+ *
145
+ * Body: { projectRoot }
146
+ *
147
+ * @param {object} req - Express request
148
+ * @param {object} res - Express response
149
+ */
150
+ async function handleRebuild(req, res) {
151
+ const { projectRoot } = req.body;
152
+ const result = await vectorIndexer.indexAll(projectRoot);
153
+ res.json(result);
154
+ }
155
+
156
+ /**
157
+ * Store a permanent memory entry.
158
+ *
159
+ * Body: { text, metadata }
160
+ *
161
+ * @param {object} req - Express request
162
+ * @param {object} res - Express response
163
+ */
164
+ async function handleRemember(req, res) {
165
+ const { text, metadata = {} } = req.body;
166
+ const result = await richCapture.processChunk(text, { ...metadata, permanent: true });
167
+ res.json(result);
168
+ }
169
+
170
+ return {
171
+ handleSearch,
172
+ handleListConversations,
173
+ handleGetConversation,
174
+ handleListDecisions,
175
+ handleListGotchas,
176
+ handleGetStats,
177
+ handleRebuild,
178
+ handleRemember,
179
+ };
180
+ }