sumulige-claude 1.1.2 โ†’ 1.2.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 (102) hide show
  1. package/.claude/hooks/code-formatter.cjs +7 -2
  2. package/.claude/hooks/multi-session.cjs +9 -3
  3. package/.claude/hooks/pre-commit.cjs +0 -0
  4. package/.claude/hooks/pre-push.cjs +0 -0
  5. package/.claude/hooks/project-kickoff.cjs +22 -11
  6. package/.claude/hooks/rag-skill-loader.cjs +7 -0
  7. package/.claude/hooks/thinking-silent.cjs +9 -3
  8. package/.claude/hooks/todo-manager.cjs +19 -13
  9. package/.claude/hooks/verify-work.cjs +10 -4
  10. package/.claude/quality-gate.json +9 -3
  11. package/.claude/settings.local.json +16 -1
  12. package/.claude/templates/hooks/README.md +302 -0
  13. package/.claude/templates/hooks/hook.sh.template +94 -0
  14. package/.claude/templates/hooks/user-prompt-submit.cjs.template +116 -0
  15. package/.claude/templates/hooks/user-response-submit.cjs.template +94 -0
  16. package/.claude/templates/hooks/validate.js +173 -0
  17. package/.claude/workflow/document-scanner.js +426 -0
  18. package/.claude/workflow/knowledge-engine.js +941 -0
  19. package/.claude/workflow/notebooklm/browser.js +1028 -0
  20. package/.claude/workflow/phases/phase1-research.js +578 -0
  21. package/.claude/workflow/phases/phase1-research.ts +465 -0
  22. package/.claude/workflow/phases/phase2-approve.js +722 -0
  23. package/.claude/workflow/phases/phase3-plan.js +1200 -0
  24. package/.claude/workflow/phases/phase4-develop.js +894 -0
  25. package/.claude/workflow/search-cache.js +230 -0
  26. package/.claude/workflow/templates/approval.md +315 -0
  27. package/.claude/workflow/templates/development.md +377 -0
  28. package/.claude/workflow/templates/planning.md +328 -0
  29. package/.claude/workflow/templates/research.md +250 -0
  30. package/.claude/workflow/types.js +37 -0
  31. package/.claude/workflow/web-search.js +278 -0
  32. package/.claude-plugin/marketplace.json +2 -2
  33. package/AGENTS.md +176 -0
  34. package/CHANGELOG.md +7 -14
  35. package/cli.js +20 -0
  36. package/config/quality-gate.json +9 -3
  37. package/development/cache/web-search/search_1193d605f8eb364651fc2f2041b58a31.json +36 -0
  38. package/development/cache/web-search/search_3798bf06960edc125f744a1abb5b72c5.json +36 -0
  39. package/development/cache/web-search/search_37c7d4843a53f0d83f1122a6f908a2a3.json +36 -0
  40. package/development/cache/web-search/search_44166fa0153709ee168485a22aa0ab40.json +36 -0
  41. package/development/cache/web-search/search_4deaebb1f77e86a8ca066dc5a49c59fd.json +36 -0
  42. package/development/cache/web-search/search_94da91789466070a7f545612e73c7372.json +36 -0
  43. package/development/cache/web-search/search_dd5de8491b8b803a3cb01339cd210fb0.json +36 -0
  44. package/development/knowledge-base/.index.clean.json +0 -0
  45. package/development/knowledge-base/.index.json +486 -0
  46. package/development/knowledge-base/test-best-practices.md +29 -0
  47. package/development/projects/proj_mkh1pazz_ixmt1/phase1/feasibility-report.md +160 -0
  48. package/development/projects/proj_mkh4jvnb_z7rwf/phase1/feasibility-report.md +160 -0
  49. package/development/projects/proj_mkh4jxkd_ewz5a/phase1/feasibility-report.md +160 -0
  50. package/development/projects/proj_mkh4k84n_ni73k/phase1/feasibility-report.md +160 -0
  51. package/development/projects/proj_mkh4wfyd_u9w88/phase1/feasibility-report.md +160 -0
  52. package/development/projects/proj_mkh4wsbo_iahvf/development/projects/proj_mkh4xbpg_4na5w/phase1/feasibility-report.md +160 -0
  53. package/development/projects/proj_mkh4wsbo_iahvf/phase1/feasibility-report.md +160 -0
  54. package/development/projects/proj_mkh4xulg_1ka8x/phase1/feasibility-report.md +160 -0
  55. package/development/projects/proj_mkh4xwhj_gch8j/phase1/feasibility-report.md +160 -0
  56. package/development/projects/proj_mkh4y2qk_9lm8z/phase1/feasibility-report.md +160 -0
  57. package/development/projects/proj_mkh4y2qk_9lm8z/phase2/requirements.md +226 -0
  58. package/development/projects/proj_mkh4y2qk_9lm8z/phase3/PRD.md +345 -0
  59. package/development/projects/proj_mkh4y2qk_9lm8z/phase3/TASK_PLAN.md +284 -0
  60. package/development/projects/proj_mkh4y2qk_9lm8z/phase3/prototype/README.md +14 -0
  61. package/development/projects/proj_mkh4y2qk_9lm8z/phase4/DEVELOPMENT_LOG.md +35 -0
  62. package/development/projects/proj_mkh4y2qk_9lm8z/phase4/TASKS.md +34 -0
  63. package/development/projects/proj_mkh4y2qk_9lm8z/phase4/source/.env.example +5 -0
  64. package/development/projects/proj_mkh4y2qk_9lm8z/phase4/source/README.md +60 -0
  65. package/development/projects/proj_mkh4y2qk_9lm8z/phase4/source/package.json +25 -0
  66. package/development/projects/proj_mkh4y2qk_9lm8z/phase4/source/src/index.js +70 -0
  67. package/development/projects/proj_mkh4y2qk_9lm8z/phase4/source/src/routes/index.js +48 -0
  68. package/development/projects/proj_mkh4y2qk_9lm8z/phase4/source/tests/health.test.js +20 -0
  69. package/development/projects/proj_mkh4y2qk_9lm8z/phase4/source/tests/jest.config.js +21 -0
  70. package/development/projects/proj_mkh7veqg_3lypc/phase1/feasibility-report.md +160 -0
  71. package/development/projects/proj_mkh7veqg_3lypc/phase2/requirements.md +226 -0
  72. package/development/projects/proj_mkh7veqg_3lypc/phase3/PRD.md +345 -0
  73. package/development/projects/proj_mkh7veqg_3lypc/phase3/TASK_PLAN.md +284 -0
  74. package/development/projects/proj_mkh7veqg_3lypc/phase3/prototype/README.md +14 -0
  75. package/development/projects/proj_mkh8k8fo_rmqn5/phase1/feasibility-report.md +160 -0
  76. package/development/projects/proj_mkh8xyhy_1vshq/phase1/feasibility-report.md +178 -0
  77. package/development/projects/proj_mkh8zddd_dhamf/phase1/feasibility-report.md +377 -0
  78. package/development/projects/proj_mkh8zddd_dhamf/phase2/requirements.md +442 -0
  79. package/development/projects/proj_mkh8zddd_dhamf/phase3/api-design.md +800 -0
  80. package/development/projects/proj_mkh8zddd_dhamf/phase3/architecture.md +625 -0
  81. package/development/projects/proj_mkh8zddd_dhamf/phase3/data-model.md +830 -0
  82. package/development/projects/proj_mkh8zddd_dhamf/phase3/risks.md +957 -0
  83. package/development/projects/proj_mkh8zddd_dhamf/phase3/wbs.md +381 -0
  84. package/development/todos/.state.json +14 -1
  85. package/development/todos/INDEX.md +31 -73
  86. package/development/todos/completed/develop/local-knowledge-index.md +85 -0
  87. package/development/todos/{active โ†’ completed/develop}/todo-system.md +13 -3
  88. package/development/todos/completed/develop/web-search-integration.md +83 -0
  89. package/development/todos/completed/test/phase1-e2e-test.md +103 -0
  90. package/lib/commands.js +388 -0
  91. package/package.json +3 -2
  92. package/tests/config-manager.test.js +677 -0
  93. package/tests/config-validator.test.js +436 -0
  94. package/tests/errors.test.js +477 -0
  95. package/tests/manual/phase1-e2e.sh +389 -0
  96. package/tests/manual/phase2-test-cases.md +311 -0
  97. package/tests/manual/phase3-test-cases.md +309 -0
  98. package/tests/manual/phase4-test-cases.md +414 -0
  99. package/tests/manual/test-cases.md +417 -0
  100. package/tests/quality-gate.test.js +679 -0
  101. package/tests/quality-rules.test.js +619 -0
  102. package/tests/version-check.test.js +75 -0
@@ -0,0 +1,941 @@
1
+ /**
2
+ * Knowledge Engine (JavaScript version)
3
+ *
4
+ * Integrates NotebookLM knowledge capabilities with local knowledge base
5
+ * and web search for Phase 1 research.
6
+ */
7
+
8
+ const fs = require('fs');
9
+ const path = require('path');
10
+
11
+ // Import DocumentScanner
12
+ const { DocumentScanner } = require('./document-scanner.js');
13
+
14
+ // Import WebSearch and SearchCache
15
+ const { WebSearch } = require('./web-search.js');
16
+ const { SearchCache } = require('./search-cache.js');
17
+
18
+ // Try to import NotebookLM browser module
19
+ let NotebookLMClient = null;
20
+ let getClient = null;
21
+ try {
22
+ const notebooklm = require('./notebooklm/browser.js');
23
+ NotebookLMClient = notebooklm.NotebookLMClient;
24
+ getClient = notebooklm.getClient;
25
+ } catch (e) {
26
+ // NotebookLM not available, will use fallback
27
+ }
28
+
29
+ // ============================================================================
30
+ // Configuration
31
+ // ============================================================================
32
+
33
+ const KNOWLEDGE_BASE_DIR = path.join(process.cwd(), 'development/knowledge-base');
34
+ const PROJECTS_DIR = path.join(process.cwd(), 'development/projects');
35
+ const KNOWLEDGE_INDEX_FILE = path.join(KNOWLEDGE_BASE_DIR, '.index.json');
36
+
37
+ // ============================================================================
38
+ // Knowledge Engine Class
39
+ // ============================================================================
40
+
41
+ class KnowledgeEngine {
42
+ constructor() {
43
+ this.index = { sources: [], lastUpdated: 0 };
44
+ this.indexLoaded = false;
45
+ this.ensureDirectories();
46
+ }
47
+
48
+ // --------------------------------------------------------------------------
49
+ // Initialization
50
+ // --------------------------------------------------------------------------
51
+
52
+ ensureDirectories() {
53
+ [KNOWLEDGE_BASE_DIR, PROJECTS_DIR].forEach(dir => {
54
+ if (!fs.existsSync(dir)) {
55
+ fs.mkdirSync(dir, { recursive: true });
56
+ }
57
+ });
58
+ }
59
+
60
+ loadIndex() {
61
+ if (this.indexLoaded) return;
62
+
63
+ try {
64
+ if (fs.existsSync(KNOWLEDGE_INDEX_FILE)) {
65
+ const content = fs.readFileSync(KNOWLEDGE_INDEX_FILE, 'utf-8');
66
+ this.index = JSON.parse(content);
67
+ }
68
+ this.indexLoaded = true;
69
+ } catch (error) {
70
+ console.warn('Failed to load knowledge index, starting fresh:', error);
71
+ this.index = { sources: [], lastUpdated: Date.now() };
72
+ this.indexLoaded = true;
73
+ }
74
+ }
75
+
76
+ saveIndex() {
77
+ try {
78
+ fs.writeFileSync(
79
+ KNOWLEDGE_INDEX_FILE,
80
+ JSON.stringify(this.index, null, 2),
81
+ 'utf-8'
82
+ );
83
+ this.index.lastUpdated = Date.now();
84
+ } catch (error) {
85
+ console.error('Failed to save knowledge index:', error);
86
+ }
87
+ }
88
+
89
+ // --------------------------------------------------------------------------
90
+ // Knowledge Source Management
91
+ // --------------------------------------------------------------------------
92
+
93
+ generateId() {
94
+ return `src_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
95
+ }
96
+
97
+ getMimeType(ext) {
98
+ const mimeTypes = {
99
+ '.md': 'text/markdown',
100
+ '.txt': 'text/plain',
101
+ '.json': 'application/json',
102
+ '.pdf': 'application/pdf',
103
+ '.html': 'text/html',
104
+ '.js': 'text/javascript',
105
+ '.ts': 'text/typescript',
106
+ '.py': 'text/x-python',
107
+ '.rs': 'text/x-rust'
108
+ };
109
+ return mimeTypes[ext.toLowerCase()] || 'application/octet-stream';
110
+ }
111
+
112
+ /**
113
+ * Add a knowledge source to the index
114
+ */
115
+ addSource(source) {
116
+ this.loadIndex();
117
+
118
+ const id = this.generateId();
119
+ const newSource = {
120
+ ...source,
121
+ id,
122
+ addedAt: Date.now(),
123
+ lastAccessed: Date.now()
124
+ };
125
+
126
+ this.index.sources.push(newSource);
127
+ this.saveIndex();
128
+
129
+ return id;
130
+ }
131
+
132
+ /**
133
+ * Add a local file as knowledge source
134
+ */
135
+ addFile(filePath, tags = [], options = {}) {
136
+ const resolvedPath = path.resolve(filePath);
137
+ if (!fs.existsSync(resolvedPath)) {
138
+ throw new Error(`File not found: ${filePath}`);
139
+ }
140
+
141
+ const stats = fs.statSync(resolvedPath);
142
+ const ext = path.extname(resolvedPath);
143
+
144
+ // Scan file for content and metadata
145
+ const scanResult = DocumentScanner.scanFile(resolvedPath, {
146
+ includeContent: true,
147
+ maxContentSize: options.maxContentSize || 500 * 1024
148
+ });
149
+
150
+ // Build title from front matter or filename
151
+ let title = path.basename(resolvedPath);
152
+ let description = `Local file: ${resolvedPath}`;
153
+
154
+ if (scanResult.frontMatter && scanResult.frontMatter.title) {
155
+ title = scanResult.frontMatter.title;
156
+ }
157
+ if (scanResult.frontMatter && scanResult.frontMatter.description) {
158
+ description = scanResult.frontMatter.description;
159
+ }
160
+
161
+ return this.addSource({
162
+ type: 'local_file',
163
+ path: resolvedPath,
164
+ title,
165
+ description,
166
+ tags: tags.concat(scanResult.frontMatter?.tags?.split(',') || []),
167
+ size: stats.size,
168
+ contentType: scanResult.contentType,
169
+
170
+ // New fields from scanning
171
+ scannable: scanResult.scannable,
172
+ wordCount: scanResult.wordCount,
173
+ lineCount: scanResult.lineCount,
174
+ headings: scanResult.headings,
175
+ links: scanResult.links,
176
+ codeBlocks: scanResult.codeBlocks,
177
+ content: scanResult.content,
178
+ snippet: scanResult.snippet,
179
+ checksum: scanResult.checksum,
180
+ lastModified: scanResult.lastModified,
181
+ indexedAt: Date.now()
182
+ });
183
+ }
184
+
185
+ /**
186
+ * Add a local directory as knowledge source
187
+ * Recursively scans all supported files in the directory
188
+ */
189
+ addDirectory(dirPath, tags = [], options = {}) {
190
+ const resolvedPath = path.resolve(dirPath);
191
+ if (!fs.existsSync(resolvedPath)) {
192
+ throw new Error(`Directory not found: ${dirPath}`);
193
+ }
194
+
195
+ const {
196
+ recursive = true,
197
+ maxDepth = 10,
198
+ includePatterns = [],
199
+ excludePatterns = ['node_modules', '.git', 'dist', 'build', 'coverage', 'sessions', '.claude']
200
+ } = options;
201
+
202
+ // Scan the directory for files
203
+ const scanResults = DocumentScanner.scanDirectory(resolvedPath, {
204
+ recursive,
205
+ maxDepth,
206
+ includePatterns,
207
+ excludePatterns
208
+ });
209
+
210
+ // Add each file as a separate source
211
+ const addedIds = [];
212
+ for (const result of scanResults) {
213
+ if (!result.scannable) continue;
214
+
215
+ let title = path.basename(result.path);
216
+ let description = `File: ${result.path}`;
217
+
218
+ // Use front matter if available
219
+ if (result.frontMatter && result.frontMatter.title) {
220
+ title = result.frontMatter.title;
221
+ }
222
+ if (result.frontMatter && result.frontMatter.description) {
223
+ description = result.frontMatter.description;
224
+ }
225
+
226
+ const id = this.addSource({
227
+ type: 'local_file',
228
+ path: result.path,
229
+ title,
230
+ description,
231
+ tags: tags.concat(result.frontMatter?.tags?.split(',') || []),
232
+ size: result.size,
233
+ contentType: result.contentType,
234
+ scannable: result.scannable,
235
+ wordCount: result.wordCount,
236
+ lineCount: result.lineCount,
237
+ headings: result.headings,
238
+ links: result.links,
239
+ codeBlocks: result.codeBlocks,
240
+ content: result.content,
241
+ snippet: result.snippet,
242
+ checksum: result.checksum,
243
+ lastModified: result.lastModified,
244
+ indexedAt: Date.now()
245
+ });
246
+ addedIds.push(id);
247
+ }
248
+
249
+ // Also add the directory itself as a container
250
+ this.addSource({
251
+ type: 'local_directory',
252
+ path: resolvedPath,
253
+ title: path.basename(resolvedPath),
254
+ description: `Directory with ${addedIds.length} indexed files`,
255
+ tags,
256
+ fileCount: addedIds.length
257
+ });
258
+
259
+ return addedIds;
260
+ }
261
+
262
+ /**
263
+ * Add a NotebookLM notebook as knowledge source
264
+ */
265
+ addNotebook(notebookUrl, title, tags = []) {
266
+ return this.addSource({
267
+ type: 'notebooklm',
268
+ notebookUrl,
269
+ title,
270
+ description: `NotebookLM: ${title}`,
271
+ tags
272
+ });
273
+ }
274
+
275
+ /**
276
+ * List all knowledge sources
277
+ */
278
+ listSources(filter = {}) {
279
+ this.loadIndex();
280
+
281
+ let sources = [...this.index.sources];
282
+
283
+ if (filter.type) {
284
+ sources = sources.filter(s => s.type === filter.type);
285
+ }
286
+
287
+ if (filter.tag) {
288
+ sources = sources.filter(s => s.tags.includes(filter.tag));
289
+ }
290
+
291
+ return sources.sort((a, b) => b.lastAccessed - a.lastAccessed);
292
+ }
293
+
294
+ /**
295
+ * Get a specific knowledge source by ID
296
+ */
297
+ getSource(id) {
298
+ this.loadIndex();
299
+ return this.index.sources.find(s => s.id === id) || null;
300
+ }
301
+
302
+ /**
303
+ * Remove a knowledge source
304
+ */
305
+ removeSource(id) {
306
+ this.loadIndex();
307
+ const initialLength = this.index.sources.length;
308
+ this.index.sources = this.index.sources.filter(s => s.id !== id);
309
+
310
+ if (this.index.sources.length < initialLength) {
311
+ this.saveIndex();
312
+ return true;
313
+ }
314
+ return false;
315
+ }
316
+
317
+ /**
318
+ * Check if a source needs reindexing due to file changes
319
+ */
320
+ needsReindex(source) {
321
+ if (source.type !== 'local_file') return false;
322
+ if (!fs.existsSync(source.path)) return false; // File deleted
323
+
324
+ try {
325
+ const stats = fs.statSync(source.path);
326
+ // Check if file was modified since last index
327
+ if (source.lastModified && stats.mtimeMs > source.lastModified) {
328
+ return true;
329
+ }
330
+ // Also check checksum if available
331
+ if (source.checksum) {
332
+ const currentContent = fs.readFileSync(source.path, 'utf-8');
333
+ const currentChecksum = require('crypto')
334
+ .createHash('md5')
335
+ .update(currentContent)
336
+ .digest('hex');
337
+ return currentChecksum !== source.checksum;
338
+ }
339
+ } catch (error) {
340
+ return false;
341
+ }
342
+
343
+ return false;
344
+ }
345
+
346
+ /**
347
+ * Reindex a single file source
348
+ */
349
+ reindexFile(source) {
350
+ try {
351
+ const scanResult = DocumentScanner.scanFile(source.path, {
352
+ includeContent: true
353
+ });
354
+
355
+ // Update the source with new data
356
+ Object.assign(source, {
357
+ wordCount: scanResult.wordCount,
358
+ lineCount: scanResult.lineCount,
359
+ headings: scanResult.headings,
360
+ links: scanResult.links,
361
+ codeBlocks: scanResult.codeBlocks,
362
+ content: scanResult.content,
363
+ snippet: scanResult.snippet,
364
+ checksum: scanResult.checksum,
365
+ lastModified: scanResult.lastModified,
366
+ indexedAt: Date.now()
367
+ });
368
+
369
+ this.saveIndex();
370
+ return true;
371
+ } catch (error) {
372
+ console.warn(`Failed to reindex ${source.path}:`, error.message);
373
+ return false;
374
+ }
375
+ }
376
+
377
+ /**
378
+ * Update index for all files that have changed
379
+ */
380
+ updateIndex(options = {}) {
381
+ const { progressCallback } = options;
382
+ this.loadIndex();
383
+
384
+ const fileSources = this.index.sources.filter(s => s.type === 'local_file');
385
+ let updatedCount = 0;
386
+
387
+ for (const source of fileSources) {
388
+ if (this.needsReindex(source)) {
389
+ progressCallback?.(`Reindexing: ${source.title}`);
390
+ if (this.reindexFile(source)) {
391
+ updatedCount++;
392
+ }
393
+ }
394
+ }
395
+
396
+ // Remove sources for files that no longer exist
397
+ const initialLength = this.index.sources.length;
398
+ this.index.sources = this.index.sources.filter(s => {
399
+ if (s.type === 'local_file') {
400
+ return fs.existsSync(s.path);
401
+ }
402
+ return true;
403
+ });
404
+
405
+ if (this.index.sources.length < initialLength) {
406
+ this.saveIndex();
407
+ }
408
+
409
+ return { updatedCount, removedCount: initialLength - this.index.sources.length };
410
+ }
411
+
412
+ // --------------------------------------------------------------------------
413
+ // Knowledge Query
414
+ // --------------------------------------------------------------------------
415
+
416
+ calculateRelevance(question, source) {
417
+ const questionLower = question.toLowerCase();
418
+ const questionWords = questionLower.split(/\s+/).filter(w => w.length > 2);
419
+
420
+ let score = 0;
421
+ const weights = {
422
+ title: 0.35,
423
+ tags: 0.25,
424
+ content: 0.30,
425
+ headings: 0.10
426
+ };
427
+
428
+ // 1. Title matching
429
+ const titleLower = (source.title || '').toLowerCase();
430
+ const titleWords = titleLower.split(/\s+/);
431
+ let titleMatches = 0;
432
+ questionWords.forEach(word => {
433
+ if (titleWords.some(t => t.includes(word) || word.includes(t))) {
434
+ titleMatches++;
435
+ }
436
+ });
437
+ score += (titleMatches / Math.max(questionWords.length, 1)) * weights.title;
438
+
439
+ // 2. Tag matching
440
+ if (source.tags && source.tags.length > 0) {
441
+ let tagMatches = 0;
442
+ questionWords.forEach(word => {
443
+ if (source.tags.some(t => t.toLowerCase().includes(word))) {
444
+ tagMatches++;
445
+ }
446
+ });
447
+ score += (tagMatches / Math.max(questionWords.length, 1)) * weights.tags;
448
+ }
449
+
450
+ // 3. Content matching (full-text search)
451
+ if (source.content || source.snippet) {
452
+ const contentLower = (source.content || source.snippet || '').toLowerCase();
453
+ let contentMatches = 0;
454
+ questionWords.forEach(word => {
455
+ if (contentLower.includes(word)) {
456
+ contentMatches++;
457
+ }
458
+ });
459
+ score += (contentMatches / Math.max(questionWords.length, 1)) * weights.content;
460
+
461
+ // Boost for exact phrase match
462
+ if (contentLower.includes(questionLower)) {
463
+ score += 0.2;
464
+ }
465
+ }
466
+
467
+ // 4. Headings matching
468
+ if (source.headings && source.headings.length > 0) {
469
+ let headingMatches = 0;
470
+ questionWords.forEach(word => {
471
+ if (source.headings.some(h => h.text && h.text.toLowerCase().includes(word))) {
472
+ headingMatches++;
473
+ }
474
+ });
475
+ score += (headingMatches / Math.max(questionWords.length, 1)) * weights.headings;
476
+ }
477
+
478
+ return Math.min(score, 1);
479
+ }
480
+
481
+ /**
482
+ * Query the knowledge base with NotebookLM integration
483
+ */
484
+ async query(question, options = {}) {
485
+ this.loadIndex();
486
+
487
+ const {
488
+ includeWeb = false,
489
+ maxSources = 5,
490
+ progressCallback,
491
+ useNotebookLM = true
492
+ } = options;
493
+
494
+ const sources = [];
495
+ const webResults = [];
496
+ let notebooklmAnswer = null;
497
+
498
+ // Step 1: Search local knowledge base
499
+ await progressCallback?.('Searching local knowledge base...', 1, 5);
500
+
501
+ const localSources = this.index.sources.filter(
502
+ s => s.type === 'local_file' || s.type === 'local_directory'
503
+ );
504
+
505
+ for (const source of localSources.slice(0, maxSources)) {
506
+ const relevance = this.calculateRelevance(question, source);
507
+ if (relevance > 0.3) {
508
+ sources.push({
509
+ title: source.title,
510
+ type: source.type,
511
+ relevance,
512
+ excerpt: `Excerpt from ${source.title}`
513
+ });
514
+ source.lastAccessed = Date.now();
515
+ }
516
+ }
517
+
518
+ // Step 2: Query NotebookLM if available and enabled
519
+ await progressCallback?.('Querying NotebookLM...', 2, 5);
520
+
521
+ const notebooklmSources = this.index.sources.filter(s => s.type === 'notebooklm');
522
+
523
+ if (useNotebookLM && notebooklmSources.length > 0 && getClient) {
524
+ try {
525
+ const client = getClient();
526
+
527
+ // Check if authenticated
528
+ const stats = client.getStats();
529
+ if (!stats.authenticated) {
530
+ sources.push({
531
+ title: 'NotebookLM (requires auth)',
532
+ type: 'notebooklm',
533
+ relevance: 0.9,
534
+ excerpt: 'NotebookLM requires authentication. Run: smc notebooklm auth'
535
+ });
536
+ } else {
537
+ // Use the first NotebookLM source
538
+ const notebookUrl = notebooklmSources[0].notebookUrl || null;
539
+ notebooklmAnswer = await client.ask(notebookUrl, question, (msg) => {
540
+ // Forward progress
541
+ });
542
+
543
+ sources.push({
544
+ title: 'NotebookLM',
545
+ type: 'notebooklm',
546
+ relevance: 1.0,
547
+ excerpt: notebooklmAnswer?.substring(0, 200) + '...'
548
+ });
549
+ }
550
+ } catch (error) {
551
+ sources.push({
552
+ title: 'NotebookLM (error)',
553
+ type: 'notebooklm',
554
+ relevance: 0.5,
555
+ excerpt: `NotebookLM query failed: ${error.message}`
556
+ });
557
+ }
558
+ } else if (notebooklmSources.length === 0) {
559
+ sources.push({
560
+ title: 'NotebookLM',
561
+ type: 'notebooklm',
562
+ relevance: 0.5,
563
+ excerpt: 'No NotebookLM sources added. Add one with: smc knowledge notebook <url> <title>'
564
+ });
565
+ }
566
+
567
+ // Step 3: Web search
568
+ if (includeWeb) {
569
+ await progressCallback?.('Searching web for latest information...', 3, 5);
570
+
571
+ try {
572
+ // Check cache first
573
+ const cached = SearchCache.get(question);
574
+ if (cached && cached.length > 0) {
575
+ webResults.push(...cached);
576
+ await progressCallback?.('Using cached web results...', 4, 5);
577
+ } else {
578
+ // Perform fresh search
579
+ const freshResults = await WebSearch.search(question, { maxResults: 5 });
580
+ if (freshResults.length > 0) {
581
+ webResults.push(...freshResults);
582
+ // Cache the results
583
+ SearchCache.set(question, freshResults);
584
+ }
585
+ }
586
+ } catch (error) {
587
+ // Graceful degradation - search failed but continue
588
+ webResults.push({
589
+ title: 'Web Search (unavailable)',
590
+ url: '#',
591
+ excerpt: `Web search is currently unavailable: ${error.message}. Try again later.`,
592
+ source: 'error'
593
+ });
594
+ }
595
+ }
596
+
597
+ // Step 4: Synthesize answer
598
+ await progressCallback?.('Synthesizing answer...', 4, 5);
599
+
600
+ // Use NotebookLM answer if available, otherwise fall back to synthesis
601
+ let finalAnswer;
602
+ if (notebooklmAnswer) {
603
+ finalAnswer = notebooklmAnswer;
604
+ } else {
605
+ finalAnswer = this.synthesizeAnswer(question, sources, webResults);
606
+ }
607
+
608
+ await progressCallback?.('Query complete!', 5, 5);
609
+
610
+ return {
611
+ answer: finalAnswer,
612
+ sources: sources.slice(0, maxSources).sort((a, b) => b.relevance - a.relevance),
613
+ webResults,
614
+ confidence: this.calculateConfidence(sources, webResults),
615
+ notebooklmUsed: !!notebooklmAnswer
616
+ };
617
+ }
618
+
619
+ /**
620
+ * Query NotebookLM directly (bypass local knowledge)
621
+ */
622
+ async queryNotebookLM(question, notebookUrl = null, progressCallback) {
623
+ if (!getClient) {
624
+ throw new Error('NotebookLM module not available. Install patchright: npm install patchright');
625
+ }
626
+
627
+ const client = getClient();
628
+ const stats = client.getStats();
629
+
630
+ if (!stats.authenticated) {
631
+ throw new Error('Not authenticated. Run: smc notebooklm auth');
632
+ }
633
+
634
+ await progressCallback?.('Asking NotebookLM...');
635
+ const answer = await client.ask(notebookUrl, question, (msg) => {
636
+ // Forward progress silently or log
637
+ });
638
+
639
+ return {
640
+ answer,
641
+ notebooklmUsed: true,
642
+ confidence: 0.95
643
+ };
644
+ }
645
+
646
+ synthesizeAnswer(question, sources, webResults) {
647
+ if (sources.length === 0 && (!webResults || webResults.length === 0)) {
648
+ return `No relevant information found in the knowledge base for: "${question}"`;
649
+ }
650
+
651
+ let answer = `Based on the knowledge base, here's what I found regarding "${question}":\n\n`;
652
+
653
+ if (sources.length > 0) {
654
+ answer += `**Local Knowledge:**\n`;
655
+ sources.forEach(source => {
656
+ answer += `- ${source.title} (relevance: ${(source.relevance * 100).toFixed(0)}%)\n`;
657
+ if (source.excerpt && source.excerpt !== 'Excerpt from ' + source.title) {
658
+ answer += ` ${source.excerpt.substring(0, 150)}${source.excerpt.length > 150 ? '...' : ''}\n`;
659
+ }
660
+ });
661
+ answer += '\n';
662
+ }
663
+
664
+ if (webResults && webResults.length > 0) {
665
+ answer += `**Web Sources:**\n`;
666
+ webResults.forEach(result => {
667
+ // Format URL for display (decode if needed)
668
+ let displayUrl = result.url;
669
+ if (displayUrl.startsWith('a1aHR0c')) {
670
+ // URL appears to be still encoded
671
+ try {
672
+ displayUrl = Buffer.from(displayUrl, 'base64').toString('ascii');
673
+ // Extract the actual URL from the encoded string
674
+ const urlMatch = displayUrl.match(/https?:\/\/[^&\s]+/);
675
+ if (urlMatch) displayUrl = urlMatch[0];
676
+ } catch (e) {}
677
+ }
678
+
679
+ answer += `- [${result.title}](${displayUrl})\n`;
680
+ if (result.excerpt && result.excerpt !== 'No description available.') {
681
+ answer += ` ${result.excerpt.substring(0, 200)}${result.excerpt.length > 200 ? '...' : ''}\n`;
682
+ }
683
+ });
684
+ }
685
+
686
+ return answer;
687
+ }
688
+
689
+ calculateConfidence(sources, webResults) {
690
+ const sourceCount = sources.length + (webResults?.length || 0);
691
+ const avgRelevance = sources.reduce((sum, s) => sum + s.relevance, 0) / Math.max(sources.length, 1);
692
+
693
+ return Math.min((sourceCount / 5) * 0.5 + avgRelevance * 0.5, 1);
694
+ }
695
+
696
+ // --------------------------------------------------------------------------
697
+ // Statistics
698
+ // --------------------------------------------------------------------------
699
+
700
+ getStats() {
701
+ this.loadIndex();
702
+
703
+ const sourcesByType = {};
704
+ this.index.sources.forEach(s => {
705
+ sourcesByType[s.type] = (sourcesByType[s.type] || 0) + 1;
706
+ });
707
+
708
+ return {
709
+ totalSources: this.index.sources.length,
710
+ sourcesByType,
711
+ lastUpdated: this.index.lastUpdated
712
+ };
713
+ }
714
+ }
715
+
716
+ // ============================================================================
717
+ // Singleton Instance
718
+ // ============================================================================
719
+
720
+ let knowledgeEngineInstance = null;
721
+
722
+ function getKnowledgeEngine() {
723
+ if (!knowledgeEngineInstance) {
724
+ knowledgeEngineInstance = new KnowledgeEngine();
725
+ }
726
+ return knowledgeEngineInstance;
727
+ }
728
+
729
+ // ============================================================================
730
+ // CLI Helpers
731
+ // ============================================================================
732
+
733
+ async function handleKnowledgeCommand(args) {
734
+ const engine = getKnowledgeEngine();
735
+ const [action, ...rest] = args;
736
+
737
+ switch (action) {
738
+ case 'add': {
739
+ const [filePath, ...restArgs] = rest;
740
+ if (!filePath) {
741
+ console.error('Usage: smc knowledge add <file|directory> [tags...] [--recursive] [--max-depth=N]');
742
+ process.exit(1);
743
+ }
744
+
745
+ try {
746
+ // Parse options
747
+ const tags = restArgs.filter(arg => !arg.startsWith('--'));
748
+ const isRecursive = restArgs.includes('--recursive');
749
+ const maxDepthMatch = restArgs.find(arg => arg.startsWith('--max-depth='));
750
+ const maxDepth = maxDepthMatch ? parseInt(maxDepthMatch.split('=')[1]) : 10;
751
+
752
+ const resolvedPath = path.resolve(filePath);
753
+ const stats = fs.existsSync(resolvedPath) ? fs.statSync(resolvedPath) : null;
754
+
755
+ if (stats && stats.isDirectory()) {
756
+ // Add directory
757
+ console.log(`๐Ÿ“ Scanning directory: ${filePath}`);
758
+ const addedIds = engine.addDirectory(filePath, tags, {
759
+ recursive: isRecursive !== false, // default true
760
+ maxDepth
761
+ });
762
+ console.log(`โœ… Added ${addedIds.length} files from directory`);
763
+ } else if (stats && stats.isFile()) {
764
+ // Add single file
765
+ const id = engine.addFile(filePath, tags);
766
+ const source = engine.getSource(id);
767
+ const scanInfo = source.scannable
768
+ ? ` (${source.wordCount} words, ${source.headings?.length || 0} headings)`
769
+ : ' (not scannable)';
770
+ console.log(`โœ… Added: ${source.title}${scanInfo}`);
771
+ } else {
772
+ console.error(`โŒ Error: File not found: ${filePath}`);
773
+ process.exit(1);
774
+ }
775
+ } catch (error) {
776
+ console.error(`โŒ Error: ${error.message}`);
777
+ process.exit(1);
778
+ }
779
+ break;
780
+ }
781
+
782
+ case 'list': {
783
+ const sources = engine.listSources();
784
+ console.log(`\n๐Ÿ“š Knowledge Base (${sources.length} sources):\n`);
785
+
786
+ if (sources.length === 0) {
787
+ console.log(' No knowledge sources yet.');
788
+ console.log(' Add sources with: smc knowledge add <file|directory> [tags...]');
789
+ } else {
790
+ const icons = {
791
+ local_file: '๐Ÿ“„',
792
+ local_directory: '๐Ÿ“',
793
+ notebooklm: '๐Ÿ““',
794
+ web_search: '๐Ÿ”',
795
+ web_url: '๐ŸŒ'
796
+ };
797
+
798
+ sources.forEach(source => {
799
+ const icon = icons[source.type] || '๐Ÿ“„';
800
+ const tags = source.tags && source.tags.length > 0 ? ` [${source.tags.join(', ')}]` : '';
801
+ console.log(` ${icon} ${source.title}${tags}`);
802
+
803
+ // Show file info
804
+ if (source.scannable) {
805
+ const wordInfo = source.wordCount ? `${source.wordCount} words` : '';
806
+ const headingInfo = source.headings && source.headings.length > 0 ? `${source.headings.length} headings` : '';
807
+ const details = [wordInfo, headingInfo].filter(Boolean).join(', ');
808
+ if (details) {
809
+ console.log(` ๐Ÿ“Š ${details}`);
810
+ }
811
+ }
812
+
813
+ console.log(` Type: ${source.type} | Added: ${new Date(source.addedAt).toLocaleDateString()}`);
814
+ });
815
+ }
816
+ console.log('');
817
+ break;
818
+ }
819
+
820
+ case 'query': {
821
+ const question = rest.join(' ').replace('--web', '').trim();
822
+ if (!question) {
823
+ console.error('Usage: smc knowledge query "<question>" [--web]');
824
+ process.exit(1);
825
+ }
826
+
827
+ const includeWeb = rest.includes('--web');
828
+ const result = await engine.query(question, { includeWeb });
829
+
830
+ console.log(`\n${result.answer}\n`);
831
+ console.log(`Confidence: ${(result.confidence * 100).toFixed(0)}%`);
832
+ break;
833
+ }
834
+
835
+ case 'stats': {
836
+ const stats = engine.getStats();
837
+ console.log('\n๐Ÿ“Š Knowledge Base Statistics:\n');
838
+ console.log(` Total Sources: ${stats.totalSources}`);
839
+ console.log(' By Type:');
840
+ Object.entries(stats.sourcesByType).forEach(([type, count]) => {
841
+ console.log(` - ${type}: ${count}`);
842
+ });
843
+ console.log(` Last Updated: ${new Date(stats.lastUpdated).toLocaleString()}\n`);
844
+ break;
845
+ }
846
+
847
+ case 'remove': {
848
+ const [id] = rest;
849
+ if (!id) {
850
+ console.error('Usage: smc knowledge remove <source-id>');
851
+ process.exit(1);
852
+ }
853
+
854
+ if (engine.removeSource(id)) {
855
+ console.log(`โœ… Removed knowledge source: ${id}`);
856
+ } else {
857
+ console.error(`โŒ Source not found: ${id}`);
858
+ process.exit(1);
859
+ }
860
+ break;
861
+ }
862
+
863
+ case 'update': {
864
+ console.log('๐Ÿ”„ Checking for file changes...\n');
865
+ const result = engine.updateIndex({
866
+ progressCallback: (msg) => console.log(` ${msg}`)
867
+ });
868
+
869
+ if (result.updatedCount === 0 && result.removedCount === 0) {
870
+ console.log('โœ… Index is up to date!');
871
+ } else {
872
+ console.log(`\nโœ… Updated: ${result.updatedCount} files`);
873
+ if (result.removedCount > 0) {
874
+ console.log(` Removed: ${result.removedCount} deleted files`);
875
+ }
876
+ }
877
+ break;
878
+ }
879
+
880
+ case 'cache': {
881
+ const [action] = rest;
882
+ if (action === 'clear') {
883
+ const cleared = SearchCache.clear();
884
+ console.log(`๐Ÿ—‘๏ธ Cleared ${cleared} cached search results`);
885
+ } else if (action === 'clean') {
886
+ const cleaned = SearchCache.clean();
887
+ console.log(`๐Ÿงน Cleaned ${cleaned} expired cache entries`);
888
+ } else if (action === 'stats') {
889
+ const stats = SearchCache.getStats();
890
+ console.log('\n๐Ÿ“Š Search Cache Statistics:\n');
891
+ console.log(` Total Entries: ${stats.totalEntries}`);
892
+ console.log(` Valid Entries: ${stats.validEntries}`);
893
+ console.log(` Cache Size: ${(stats.totalSize / 1024).toFixed(2)} KB`);
894
+ console.log(` Cache Dir: ${stats.cacheDir}\n`);
895
+ } else {
896
+ console.log(`
897
+ Search Cache Commands:
898
+
899
+ smc knowledge cache clear Clear all cached results
900
+ smc knowledge cache clean Remove expired entries
901
+ smc knowledge cache stats Show cache statistics
902
+ `);
903
+ }
904
+ break;
905
+ }
906
+
907
+ case 'sync': {
908
+ console.log('๐Ÿ”„ NotebookLM sync pending - requires notebooklm-mcp integration');
909
+ break;
910
+ }
911
+
912
+ default:
913
+ console.log(`
914
+ Knowledge Base Commands:
915
+
916
+ smc knowledge add <file|directory> [tags...] Add a knowledge source
917
+ smc knowledge list List all sources
918
+ smc knowledge query "<question>" [--web] Query the knowledge base
919
+ smc knowledge remove <source-id> Remove a source
920
+ smc knowledge update Update index for changed files
921
+ smc knowledge cache <clear|clean|stats> Manage search cache
922
+ smc knowledge stats Show statistics
923
+ smc knowledge sync Sync with NotebookLM
924
+
925
+ Examples:
926
+ smc knowledge add ./docs/best-practices.md architecture
927
+ smc knowledge add ./docs --recursive
928
+ smc knowledge list
929
+ smc knowledge query "What are the best practices for API design?"
930
+ smc knowledge query --web "React 19 new features"
931
+ smc knowledge update
932
+ smc knowledge cache stats
933
+ `);
934
+ }
935
+ }
936
+
937
+ module.exports = {
938
+ KnowledgeEngine,
939
+ getKnowledgeEngine,
940
+ handleKnowledgeCommand
941
+ };