@mseep/core 3.0.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.
- package/CHANGELOG.md +285 -0
- package/LICENSE +21 -0
- package/README.ja.md +14 -0
- package/README.ko.md +14 -0
- package/README.md +227 -0
- package/README.pt-BR.md +14 -0
- package/README.skills.md +50 -0
- package/README.uk.md +14 -0
- package/README.zh-CN.md +14 -0
- package/bin/booklib-mcp.js +458 -0
- package/bin/booklib.js +2394 -0
- package/bin/skills.cjs +1292 -0
- package/community/registry.json +1616 -0
- package/hooks/hooks.json +52 -0
- package/hooks/posttooluse-capture.mjs +67 -0
- package/hooks/posttooluse-contradict.mjs +76 -0
- package/hooks/posttooluse-imports.mjs +67 -0
- package/hooks/pretooluse-inject.mjs +82 -0
- package/hooks/suggest.js +153 -0
- package/lib/agent-detector.js +96 -0
- package/lib/config-loader.js +39 -0
- package/lib/conflict-resolver.js +148 -0
- package/lib/connectors/context7.js +167 -0
- package/lib/connectors/github.js +223 -0
- package/lib/connectors/local.js +120 -0
- package/lib/connectors/notion.js +436 -0
- package/lib/connectors/web.js +134 -0
- package/lib/context-builder.js +574 -0
- package/lib/discovery-engine.js +298 -0
- package/lib/doctor/hook-installer.js +83 -0
- package/lib/doctor/usage-tracker.js +87 -0
- package/lib/engine/auditor.js +103 -0
- package/lib/engine/auto-linker.js +177 -0
- package/lib/engine/bm25-index.js +178 -0
- package/lib/engine/capture.js +120 -0
- package/lib/engine/context-map.js +641 -0
- package/lib/engine/corrections.js +194 -0
- package/lib/engine/decision-checker.js +203 -0
- package/lib/engine/doctor.js +207 -0
- package/lib/engine/embedding-provider.js +72 -0
- package/lib/engine/gap-detector.js +138 -0
- package/lib/engine/gap-resolver.js +135 -0
- package/lib/engine/graph-injector.js +137 -0
- package/lib/engine/graph-search.js +183 -0
- package/lib/engine/graph.js +170 -0
- package/lib/engine/handoff.js +411 -0
- package/lib/engine/import-checker.js +249 -0
- package/lib/engine/import-parser.js +145 -0
- package/lib/engine/indexer.js +334 -0
- package/lib/engine/lookup-priority.js +15 -0
- package/lib/engine/parser.js +257 -0
- package/lib/engine/principle-extractor.js +116 -0
- package/lib/engine/project-analyzer.js +353 -0
- package/lib/engine/query-expander.js +42 -0
- package/lib/engine/reasoning-modes.js +353 -0
- package/lib/engine/registries.js +524 -0
- package/lib/engine/reranker.js +45 -0
- package/lib/engine/rrf.js +59 -0
- package/lib/engine/scanner.js +151 -0
- package/lib/engine/searcher.js +223 -0
- package/lib/engine/session-coordinator.js +291 -0
- package/lib/engine/session-manager.js +375 -0
- package/lib/engine/source-detector.js +240 -0
- package/lib/engine/source-manager.js +142 -0
- package/lib/engine/structured-response.js +47 -0
- package/lib/engine/synthesis-templates.js +364 -0
- package/lib/installer.js +70 -0
- package/lib/instinct-block.js +21 -0
- package/lib/mcp-config-writer.js +107 -0
- package/lib/paths.js +62 -0
- package/lib/project-initializer.js +856 -0
- package/lib/registry/skills.js +102 -0
- package/lib/registry-searcher.js +107 -0
- package/lib/rules/rules-manager.js +169 -0
- package/lib/skill-fetcher.js +333 -0
- package/lib/well-known-builder.js +74 -0
- package/lib/wizard/index.js +1389 -0
- package/lib/wizard/integration-detector.js +41 -0
- package/lib/wizard/project-detector.js +146 -0
- package/lib/wizard/prompt.js +221 -0
- package/lib/wizard/registry-embeddings.js +107 -0
- package/lib/wizard/skill-recommender.js +69 -0
- package/package.json +70 -0
- package/skills/animation-at-work/SKILL.md +270 -0
- package/skills/animation-at-work/assets/example_asset.txt +1 -0
- package/skills/animation-at-work/evals/evals.json +44 -0
- package/skills/animation-at-work/evals/results.json +13 -0
- package/skills/animation-at-work/examples/after.md +64 -0
- package/skills/animation-at-work/examples/before.md +35 -0
- package/skills/animation-at-work/references/api_reference.md +369 -0
- package/skills/animation-at-work/references/review-checklist.md +79 -0
- package/skills/animation-at-work/scripts/audit_animations.py +295 -0
- package/skills/animation-at-work/scripts/example.py +1 -0
- package/skills/booklib-mcp-guide/SKILL.md +129 -0
- package/skills/booklib-mcp-guide/evals/evals.json +37 -0
- package/skills/booklib-mcp-guide/examples/after.md +34 -0
- package/skills/booklib-mcp-guide/examples/before.md +27 -0
- package/skills/booklib-mcp-guide/references/tool-catalog.md +9 -0
- package/skills/clean-code-reviewer/SKILL.md +444 -0
- package/skills/clean-code-reviewer/audit.json +35 -0
- package/skills/clean-code-reviewer/evals/evals.json +185 -0
- package/skills/clean-code-reviewer/evals/results.json +13 -0
- package/skills/clean-code-reviewer/examples/after.md +48 -0
- package/skills/clean-code-reviewer/examples/before.md +33 -0
- package/skills/clean-code-reviewer/references/api_reference.md +158 -0
- package/skills/clean-code-reviewer/references/practices-catalog.md +282 -0
- package/skills/clean-code-reviewer/references/review-checklist.md +254 -0
- package/skills/clean-code-reviewer/scripts/pre-review.py +206 -0
- package/skills/data-intensive-patterns/SKILL.md +267 -0
- package/skills/data-intensive-patterns/assets/example_asset.txt +1 -0
- package/skills/data-intensive-patterns/evals/evals.json +54 -0
- package/skills/data-intensive-patterns/evals/results.json +13 -0
- package/skills/data-intensive-patterns/examples/after.md +61 -0
- package/skills/data-intensive-patterns/examples/before.md +38 -0
- package/skills/data-intensive-patterns/references/api_reference.md +34 -0
- package/skills/data-intensive-patterns/references/patterns-catalog.md +551 -0
- package/skills/data-intensive-patterns/references/review-checklist.md +193 -0
- package/skills/data-intensive-patterns/scripts/adr.py +213 -0
- package/skills/data-intensive-patterns/scripts/example.py +1 -0
- package/skills/data-pipelines/SKILL.md +259 -0
- package/skills/data-pipelines/assets/example_asset.txt +1 -0
- package/skills/data-pipelines/evals/evals.json +45 -0
- package/skills/data-pipelines/evals/results.json +13 -0
- package/skills/data-pipelines/examples/after.md +97 -0
- package/skills/data-pipelines/examples/before.md +37 -0
- package/skills/data-pipelines/references/api_reference.md +301 -0
- package/skills/data-pipelines/references/review-checklist.md +181 -0
- package/skills/data-pipelines/scripts/example.py +1 -0
- package/skills/data-pipelines/scripts/new_pipeline.py +444 -0
- package/skills/design-patterns/SKILL.md +271 -0
- package/skills/design-patterns/assets/example_asset.txt +1 -0
- package/skills/design-patterns/evals/evals.json +46 -0
- package/skills/design-patterns/evals/results.json +13 -0
- package/skills/design-patterns/examples/after.md +52 -0
- package/skills/design-patterns/examples/before.md +29 -0
- package/skills/design-patterns/references/api_reference.md +1 -0
- package/skills/design-patterns/references/patterns-catalog.md +726 -0
- package/skills/design-patterns/references/review-checklist.md +173 -0
- package/skills/design-patterns/scripts/example.py +1 -0
- package/skills/design-patterns/scripts/scaffold.py +807 -0
- package/skills/domain-driven-design/SKILL.md +142 -0
- package/skills/domain-driven-design/assets/example_asset.txt +1 -0
- package/skills/domain-driven-design/evals/evals.json +48 -0
- package/skills/domain-driven-design/evals/results.json +13 -0
- package/skills/domain-driven-design/examples/after.md +80 -0
- package/skills/domain-driven-design/examples/before.md +43 -0
- package/skills/domain-driven-design/references/api_reference.md +1 -0
- package/skills/domain-driven-design/references/patterns-catalog.md +545 -0
- package/skills/domain-driven-design/references/review-checklist.md +158 -0
- package/skills/domain-driven-design/scripts/example.py +1 -0
- package/skills/domain-driven-design/scripts/scaffold.py +421 -0
- package/skills/effective-java/SKILL.md +227 -0
- package/skills/effective-java/assets/example_asset.txt +1 -0
- package/skills/effective-java/evals/evals.json +46 -0
- package/skills/effective-java/evals/results.json +13 -0
- package/skills/effective-java/examples/after.md +83 -0
- package/skills/effective-java/examples/before.md +37 -0
- package/skills/effective-java/references/api_reference.md +1 -0
- package/skills/effective-java/references/items-catalog.md +955 -0
- package/skills/effective-java/references/review-checklist.md +216 -0
- package/skills/effective-java/scripts/checkstyle_setup.py +211 -0
- package/skills/effective-java/scripts/example.py +1 -0
- package/skills/effective-kotlin/SKILL.md +271 -0
- package/skills/effective-kotlin/assets/example_asset.txt +1 -0
- package/skills/effective-kotlin/audit.json +29 -0
- package/skills/effective-kotlin/evals/evals.json +45 -0
- package/skills/effective-kotlin/evals/results.json +13 -0
- package/skills/effective-kotlin/examples/after.md +36 -0
- package/skills/effective-kotlin/examples/before.md +38 -0
- package/skills/effective-kotlin/references/api_reference.md +1 -0
- package/skills/effective-kotlin/references/practices-catalog.md +1228 -0
- package/skills/effective-kotlin/references/review-checklist.md +126 -0
- package/skills/effective-kotlin/scripts/example.py +1 -0
- package/skills/effective-python/SKILL.md +441 -0
- package/skills/effective-python/evals/evals.json +44 -0
- package/skills/effective-python/evals/results.json +13 -0
- package/skills/effective-python/examples/after.md +56 -0
- package/skills/effective-python/examples/before.md +40 -0
- package/skills/effective-python/ref-01-pythonic-thinking.md +202 -0
- package/skills/effective-python/ref-02-lists-and-dicts.md +146 -0
- package/skills/effective-python/ref-03-functions.md +186 -0
- package/skills/effective-python/ref-04-comprehensions-generators.md +211 -0
- package/skills/effective-python/ref-05-classes-interfaces.md +188 -0
- package/skills/effective-python/ref-06-metaclasses-attributes.md +209 -0
- package/skills/effective-python/ref-07-concurrency.md +213 -0
- package/skills/effective-python/ref-08-robustness-performance.md +248 -0
- package/skills/effective-python/ref-09-testing-debugging.md +253 -0
- package/skills/effective-python/ref-10-collaboration.md +175 -0
- package/skills/effective-python/references/api_reference.md +218 -0
- package/skills/effective-python/references/practices-catalog.md +483 -0
- package/skills/effective-python/references/review-checklist.md +190 -0
- package/skills/effective-python/scripts/lint.py +173 -0
- package/skills/effective-typescript/SKILL.md +262 -0
- package/skills/effective-typescript/audit.json +29 -0
- package/skills/effective-typescript/evals/evals.json +37 -0
- package/skills/effective-typescript/evals/results.json +13 -0
- package/skills/effective-typescript/examples/after.md +70 -0
- package/skills/effective-typescript/examples/before.md +47 -0
- package/skills/effective-typescript/references/api_reference.md +118 -0
- package/skills/effective-typescript/references/practices-catalog.md +371 -0
- package/skills/effective-typescript/scripts/review.py +169 -0
- package/skills/kotlin-in-action/SKILL.md +261 -0
- package/skills/kotlin-in-action/assets/example_asset.txt +1 -0
- package/skills/kotlin-in-action/evals/evals.json +43 -0
- package/skills/kotlin-in-action/evals/results.json +13 -0
- package/skills/kotlin-in-action/examples/after.md +53 -0
- package/skills/kotlin-in-action/examples/before.md +39 -0
- package/skills/kotlin-in-action/references/api_reference.md +1 -0
- package/skills/kotlin-in-action/references/practices-catalog.md +436 -0
- package/skills/kotlin-in-action/references/review-checklist.md +204 -0
- package/skills/kotlin-in-action/scripts/example.py +1 -0
- package/skills/kotlin-in-action/scripts/setup_detekt.py +224 -0
- package/skills/lean-startup/SKILL.md +160 -0
- package/skills/lean-startup/assets/example_asset.txt +1 -0
- package/skills/lean-startup/evals/evals.json +43 -0
- package/skills/lean-startup/evals/results.json +13 -0
- package/skills/lean-startup/examples/after.md +80 -0
- package/skills/lean-startup/examples/before.md +34 -0
- package/skills/lean-startup/references/api_reference.md +319 -0
- package/skills/lean-startup/references/review-checklist.md +137 -0
- package/skills/lean-startup/scripts/example.py +1 -0
- package/skills/lean-startup/scripts/new_experiment.py +286 -0
- package/skills/microservices-patterns/SKILL.md +384 -0
- package/skills/microservices-patterns/evals/evals.json +45 -0
- package/skills/microservices-patterns/evals/results.json +13 -0
- package/skills/microservices-patterns/examples/after.md +69 -0
- package/skills/microservices-patterns/examples/before.md +40 -0
- package/skills/microservices-patterns/references/patterns-catalog.md +391 -0
- package/skills/microservices-patterns/references/review-checklist.md +169 -0
- package/skills/microservices-patterns/scripts/new_service.py +583 -0
- package/skills/programming-with-rust/SKILL.md +209 -0
- package/skills/programming-with-rust/evals/evals.json +37 -0
- package/skills/programming-with-rust/evals/results.json +13 -0
- package/skills/programming-with-rust/examples/after.md +107 -0
- package/skills/programming-with-rust/examples/before.md +59 -0
- package/skills/programming-with-rust/references/api_reference.md +152 -0
- package/skills/programming-with-rust/references/practices-catalog.md +335 -0
- package/skills/programming-with-rust/scripts/review.py +142 -0
- package/skills/refactoring-ui/SKILL.md +362 -0
- package/skills/refactoring-ui/assets/example_asset.txt +1 -0
- package/skills/refactoring-ui/evals/evals.json +45 -0
- package/skills/refactoring-ui/evals/results.json +13 -0
- package/skills/refactoring-ui/examples/after.md +85 -0
- package/skills/refactoring-ui/examples/before.md +58 -0
- package/skills/refactoring-ui/references/api_reference.md +355 -0
- package/skills/refactoring-ui/references/review-checklist.md +114 -0
- package/skills/refactoring-ui/scripts/audit_css.py +250 -0
- package/skills/refactoring-ui/scripts/example.py +1 -0
- package/skills/rust-in-action/SKILL.md +350 -0
- package/skills/rust-in-action/evals/evals.json +38 -0
- package/skills/rust-in-action/evals/results.json +13 -0
- package/skills/rust-in-action/examples/after.md +156 -0
- package/skills/rust-in-action/examples/before.md +56 -0
- package/skills/rust-in-action/references/practices-catalog.md +346 -0
- package/skills/rust-in-action/scripts/review.py +147 -0
- package/skills/skill-router/SKILL.md +186 -0
- package/skills/skill-router/evals/evals.json +38 -0
- package/skills/skill-router/evals/results.json +13 -0
- package/skills/skill-router/examples/after.md +63 -0
- package/skills/skill-router/examples/before.md +39 -0
- package/skills/skill-router/references/api_reference.md +24 -0
- package/skills/skill-router/references/routing-heuristics.md +89 -0
- package/skills/skill-router/references/skill-catalog.md +174 -0
- package/skills/skill-router/scripts/route.py +266 -0
- package/skills/spring-boot-in-action/SKILL.md +340 -0
- package/skills/spring-boot-in-action/evals/evals.json +39 -0
- package/skills/spring-boot-in-action/evals/results.json +13 -0
- package/skills/spring-boot-in-action/examples/after.md +185 -0
- package/skills/spring-boot-in-action/examples/before.md +84 -0
- package/skills/spring-boot-in-action/references/practices-catalog.md +403 -0
- package/skills/spring-boot-in-action/scripts/review.py +184 -0
- package/skills/storytelling-with-data/SKILL.md +241 -0
- package/skills/storytelling-with-data/assets/example_asset.txt +1 -0
- package/skills/storytelling-with-data/evals/evals.json +47 -0
- package/skills/storytelling-with-data/evals/results.json +13 -0
- package/skills/storytelling-with-data/examples/after.md +50 -0
- package/skills/storytelling-with-data/examples/before.md +33 -0
- package/skills/storytelling-with-data/references/api_reference.md +379 -0
- package/skills/storytelling-with-data/references/review-checklist.md +111 -0
- package/skills/storytelling-with-data/scripts/chart_review.py +301 -0
- package/skills/storytelling-with-data/scripts/example.py +1 -0
- package/skills/system-design-interview/SKILL.md +233 -0
- package/skills/system-design-interview/assets/example_asset.txt +1 -0
- package/skills/system-design-interview/evals/evals.json +46 -0
- package/skills/system-design-interview/evals/results.json +13 -0
- package/skills/system-design-interview/examples/after.md +94 -0
- package/skills/system-design-interview/examples/before.md +27 -0
- package/skills/system-design-interview/references/api_reference.md +582 -0
- package/skills/system-design-interview/references/review-checklist.md +201 -0
- package/skills/system-design-interview/scripts/example.py +1 -0
- package/skills/system-design-interview/scripts/new_design.py +421 -0
- package/skills/using-asyncio-python/SKILL.md +290 -0
- package/skills/using-asyncio-python/assets/example_asset.txt +1 -0
- package/skills/using-asyncio-python/evals/evals.json +43 -0
- package/skills/using-asyncio-python/evals/results.json +13 -0
- package/skills/using-asyncio-python/examples/after.md +68 -0
- package/skills/using-asyncio-python/examples/before.md +39 -0
- package/skills/using-asyncio-python/references/api_reference.md +267 -0
- package/skills/using-asyncio-python/references/review-checklist.md +149 -0
- package/skills/using-asyncio-python/scripts/check_blocking.py +270 -0
- package/skills/using-asyncio-python/scripts/example.py +1 -0
- package/skills/web-scraping-python/SKILL.md +280 -0
- package/skills/web-scraping-python/assets/example_asset.txt +1 -0
- package/skills/web-scraping-python/evals/evals.json +46 -0
- package/skills/web-scraping-python/evals/results.json +13 -0
- package/skills/web-scraping-python/examples/after.md +109 -0
- package/skills/web-scraping-python/examples/before.md +40 -0
- package/skills/web-scraping-python/references/api_reference.md +393 -0
- package/skills/web-scraping-python/references/review-checklist.md +163 -0
- package/skills/web-scraping-python/scripts/example.py +1 -0
- package/skills/web-scraping-python/scripts/new_scraper.py +231 -0
- package/skills/writing-plans/audit.json +34 -0
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import {
|
|
4
|
+
listNodes, loadNode, parseNodeFrontmatter,
|
|
5
|
+
appendEdge, loadEdges,
|
|
6
|
+
} from './graph.js';
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Auto-links a newly created knowledge node to:
|
|
10
|
+
* 1. Matching project components (by keyword in title/content)
|
|
11
|
+
* 2. Related existing knowledge nodes (by semantic similarity via search)
|
|
12
|
+
*
|
|
13
|
+
* @param {object} opts
|
|
14
|
+
* @param {string} opts.nodeId - the new node's ID
|
|
15
|
+
* @param {string} opts.title - node title
|
|
16
|
+
* @param {string} [opts.content] - node content
|
|
17
|
+
* @param {string[]} [opts.tags] - node tags
|
|
18
|
+
* @param {string} [opts.nodesDir] - knowledge nodes directory
|
|
19
|
+
* @param {string} [opts.graphFile] - graph.jsonl path
|
|
20
|
+
* @returns {Promise<Array<{from: string, to: string, type: string, reason: string}>>}
|
|
21
|
+
*/
|
|
22
|
+
export async function autoLink({ nodeId, title, content = '', tags = [], nodesDir, graphFile }) {
|
|
23
|
+
const links = [];
|
|
24
|
+
const existingEdges = loadEdges({ graphFile });
|
|
25
|
+
const allNodeIds = listNodes({ nodesDir });
|
|
26
|
+
const today = new Date().toISOString().split('T')[0];
|
|
27
|
+
|
|
28
|
+
// Step 1: Component matching — find components whose name appears in title/content
|
|
29
|
+
const searchText = `${title} ${content} ${tags.join(' ')}`.toLowerCase();
|
|
30
|
+
|
|
31
|
+
for (const id of allNodeIds) {
|
|
32
|
+
if (id === nodeId) continue;
|
|
33
|
+
const raw = loadNode(id, { nodesDir });
|
|
34
|
+
if (!raw) continue;
|
|
35
|
+
const parsed = parseNodeFrontmatter(raw);
|
|
36
|
+
|
|
37
|
+
// Only match against components
|
|
38
|
+
if (parsed.type !== 'component') continue;
|
|
39
|
+
|
|
40
|
+
const componentName = (parsed.title ?? '').toLowerCase();
|
|
41
|
+
if (!componentName) continue;
|
|
42
|
+
|
|
43
|
+
// Check if component name (or significant words from it) appears in the new node's text
|
|
44
|
+
const nameWords = componentName.split(/[\s\-_]+/).filter(w => w.length > 2);
|
|
45
|
+
const matches = nameWords.some(word => searchText.includes(word));
|
|
46
|
+
|
|
47
|
+
if (matches && !edgeExists(existingEdges, nodeId, id)) {
|
|
48
|
+
const edge = { from: nodeId, to: id, type: 'applies-to', weight: 1.0, created: today };
|
|
49
|
+
appendEdge(edge, { graphFile });
|
|
50
|
+
links.push({ from: nodeId, to: id, type: 'applies-to', reason: `component name match: "${parsed.title}"` });
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// Step 2: Knowledge matching — find related existing notes by title similarity
|
|
55
|
+
// Simple approach: check if significant words from the new title appear in existing node titles
|
|
56
|
+
const titleWords = title.toLowerCase().split(/[\s\-_]+/).filter(w => w.length > 3);
|
|
57
|
+
|
|
58
|
+
for (const id of allNodeIds) {
|
|
59
|
+
if (id === nodeId) continue;
|
|
60
|
+
if (links.length >= 3) break; // cap at 3 auto-links
|
|
61
|
+
|
|
62
|
+
const raw = loadNode(id, { nodesDir });
|
|
63
|
+
if (!raw) continue;
|
|
64
|
+
const parsed = parseNodeFrontmatter(raw);
|
|
65
|
+
|
|
66
|
+
// Skip components (already handled above)
|
|
67
|
+
if (parsed.type === 'component') continue;
|
|
68
|
+
|
|
69
|
+
const existingTitle = (parsed.title ?? '').toLowerCase();
|
|
70
|
+
if (!existingTitle) continue;
|
|
71
|
+
|
|
72
|
+
// Check for word overlap between titles
|
|
73
|
+
const existingWords = existingTitle.split(/[\s\-_]+/).filter(w => w.length > 3);
|
|
74
|
+
const overlap = titleWords.filter(w => existingWords.includes(w));
|
|
75
|
+
|
|
76
|
+
if (overlap.length >= 1 && !edgeExists(existingEdges, nodeId, id)) {
|
|
77
|
+
const edge = { from: nodeId, to: id, type: 'see-also', weight: 1.0, created: today };
|
|
78
|
+
appendEdge(edge, { graphFile });
|
|
79
|
+
links.push({ from: nodeId, to: id, type: 'see-also', reason: `related knowledge: "${parsed.title}"` });
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
return links;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Reverse auto-link: when a new component is created, find existing notes
|
|
88
|
+
* that mention the component name and link them.
|
|
89
|
+
*/
|
|
90
|
+
export async function autoLinkReverse({ componentId, componentTitle, nodesDir, graphFile }) {
|
|
91
|
+
const links = [];
|
|
92
|
+
const existingEdges = loadEdges({ graphFile });
|
|
93
|
+
const allNodeIds = listNodes({ nodesDir });
|
|
94
|
+
const today = new Date().toISOString().split('T')[0];
|
|
95
|
+
const nameWords = componentTitle.toLowerCase().split(/[\s\-_]+/).filter(w => w.length > 2);
|
|
96
|
+
|
|
97
|
+
for (const id of allNodeIds) {
|
|
98
|
+
if (id === componentId) continue;
|
|
99
|
+
const raw = loadNode(id, { nodesDir });
|
|
100
|
+
if (!raw) continue;
|
|
101
|
+
const parsed = parseNodeFrontmatter(raw);
|
|
102
|
+
if (parsed.type === 'component') continue;
|
|
103
|
+
|
|
104
|
+
const nodeText = `${parsed.title ?? ''} ${parsed.body ?? ''}`.toLowerCase();
|
|
105
|
+
const matches = nameWords.some(word => nodeText.includes(word));
|
|
106
|
+
|
|
107
|
+
if (matches && !edgeExists(existingEdges, id, componentId)) {
|
|
108
|
+
const edge = { from: id, to: componentId, type: 'applies-to', weight: 1.0, created: today };
|
|
109
|
+
appendEdge(edge, { graphFile });
|
|
110
|
+
links.push({ from: id, to: componentId, type: 'applies-to', reason: `mentions component: "${componentTitle}"` });
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
return links;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Creates see-also edges between knowledge nodes and skills when their tags overlap.
|
|
119
|
+
* Called during `booklib index` after indexing knowledge nodes.
|
|
120
|
+
*
|
|
121
|
+
* @param {object} opts
|
|
122
|
+
* @param {Array<{id: string, title: string, tags: string[]}>} opts.knowledgeNodes
|
|
123
|
+
* @param {Array<{name: string, tags: string[]}>} opts.skillTags
|
|
124
|
+
* @param {string} [opts.graphFile]
|
|
125
|
+
* @returns {Promise<Array<{from: string, to: string, type: string, reason: string}>>}
|
|
126
|
+
*/
|
|
127
|
+
export async function autoLinkSkills({ knowledgeNodes, skillTags, graphFile }) {
|
|
128
|
+
const links = [];
|
|
129
|
+
const existingEdges = loadEdges({ graphFile });
|
|
130
|
+
const today = new Date().toISOString().split('T')[0];
|
|
131
|
+
|
|
132
|
+
// For each knowledge node, check tag overlap with each skill
|
|
133
|
+
for (const node of knowledgeNodes) {
|
|
134
|
+
const nodeTagSet = new Set(node.tags || []);
|
|
135
|
+
|
|
136
|
+
// Extract significant words (>3 chars) from title
|
|
137
|
+
const titleWords = node.title.toLowerCase().split(/[\s\-_]+/).filter(w => w.length > 3);
|
|
138
|
+
for (const word of titleWords) {
|
|
139
|
+
nodeTagSet.add(word);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// Check overlap with each skill
|
|
143
|
+
for (const skill of skillTags) {
|
|
144
|
+
const skillTagSet = new Set(skill.tags || []);
|
|
145
|
+
|
|
146
|
+
// Count overlapping tags
|
|
147
|
+
const overlap = Array.from(nodeTagSet).filter(tag => skillTagSet.has(tag));
|
|
148
|
+
|
|
149
|
+
// Create edge if ≥2 tags overlap and edge doesn't already exist
|
|
150
|
+
if (overlap.length >= 2 && !edgeExists(existingEdges, node.id, skill.name)) {
|
|
151
|
+
const edge = {
|
|
152
|
+
from: node.id,
|
|
153
|
+
to: skill.name,
|
|
154
|
+
type: 'see-also',
|
|
155
|
+
weight: 1.0,
|
|
156
|
+
created: today,
|
|
157
|
+
};
|
|
158
|
+
appendEdge(edge, { graphFile });
|
|
159
|
+
links.push({
|
|
160
|
+
from: node.id,
|
|
161
|
+
to: skill.name,
|
|
162
|
+
type: 'see-also',
|
|
163
|
+
reason: `tag overlap: [${overlap.join(', ')}]`,
|
|
164
|
+
});
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
return links;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
function edgeExists(edges, from, to) {
|
|
173
|
+
return edges.some(e =>
|
|
174
|
+
(e.from === from && e.to === to) ||
|
|
175
|
+
(e.from === to && e.to === from)
|
|
176
|
+
);
|
|
177
|
+
}
|
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
|
|
3
|
+
const K1 = 1.5;
|
|
4
|
+
const B = 0.75;
|
|
5
|
+
const MIN_TOKEN_LENGTH = 2;
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Tokenizes text into lowercase alphanumeric terms of at least 2 characters.
|
|
9
|
+
* @param {string} text
|
|
10
|
+
* @returns {string[]}
|
|
11
|
+
*/
|
|
12
|
+
function tokenize(text) {
|
|
13
|
+
return text
|
|
14
|
+
.toLowerCase()
|
|
15
|
+
.split(/[^a-z0-9]+/)
|
|
16
|
+
.filter(token => token.length >= MIN_TOKEN_LENGTH);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Counts term frequencies in a token array.
|
|
21
|
+
* @param {string[]} tokens
|
|
22
|
+
* @returns {Map<string, number>}
|
|
23
|
+
*/
|
|
24
|
+
function countTermFrequencies(tokens) {
|
|
25
|
+
const freq = new Map();
|
|
26
|
+
for (const token of tokens) {
|
|
27
|
+
freq.set(token, (freq.get(token) ?? 0) + 1);
|
|
28
|
+
}
|
|
29
|
+
return freq;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Computes BM25 IDF for a term.
|
|
34
|
+
* Formula: log((N - df + 0.5) / (df + 0.5) + 1)
|
|
35
|
+
* @param {number} docCount - Total number of documents
|
|
36
|
+
* @param {number} docFrequency - Number of documents containing the term
|
|
37
|
+
* @returns {number}
|
|
38
|
+
*/
|
|
39
|
+
function computeIdf(docCount, docFrequency) {
|
|
40
|
+
return Math.log((docCount - docFrequency + 0.5) / (docFrequency + 0.5) + 1);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Scores a single document against a set of query terms using BM25.
|
|
45
|
+
* @param {Object} doc - Document with freq map and len
|
|
46
|
+
* @param {string[]} queryTerms
|
|
47
|
+
* @param {Map<string, number>} df - Document frequency map
|
|
48
|
+
* @param {number} docCount
|
|
49
|
+
* @param {number} avgDocLen
|
|
50
|
+
* @returns {number}
|
|
51
|
+
*/
|
|
52
|
+
function scoreBm25(doc, queryTerms, df, docCount, avgDocLen) {
|
|
53
|
+
let score = 0;
|
|
54
|
+
for (const term of queryTerms) {
|
|
55
|
+
const termFreq = doc.freq[term] ?? 0;
|
|
56
|
+
if (termFreq === 0) continue;
|
|
57
|
+
|
|
58
|
+
const docFrequency = df[term] ?? 0;
|
|
59
|
+
const idf = computeIdf(docCount, docFrequency);
|
|
60
|
+
const normalizedTf =
|
|
61
|
+
(termFreq * (K1 + 1)) /
|
|
62
|
+
(termFreq + K1 * (1 - B + B * (doc.len / avgDocLen)));
|
|
63
|
+
|
|
64
|
+
score += idf * normalizedTf;
|
|
65
|
+
}
|
|
66
|
+
return score;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* BM25 full-text search index.
|
|
71
|
+
*
|
|
72
|
+
* Supports incremental document addition, JSON persistence, and
|
|
73
|
+
* Robertson BM25 ranking.
|
|
74
|
+
*/
|
|
75
|
+
export class BM25Index {
|
|
76
|
+
constructor() {
|
|
77
|
+
this._docs = [];
|
|
78
|
+
this._df = {};
|
|
79
|
+
this._avgLen = 0;
|
|
80
|
+
this._totalLen = 0;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Builds the index from scratch from an array of chunks.
|
|
85
|
+
* @param {{ text: string, metadata: object }[]} chunks
|
|
86
|
+
*/
|
|
87
|
+
build(chunks) {
|
|
88
|
+
this._docs = [];
|
|
89
|
+
this._df = {};
|
|
90
|
+
this._avgLen = 0;
|
|
91
|
+
this._totalLen = 0;
|
|
92
|
+
|
|
93
|
+
for (const chunk of chunks) {
|
|
94
|
+
this._appendDoc(chunk);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Incrementally adds one document to the index, updating avgLen and df.
|
|
100
|
+
* @param {{ text: string, metadata: object }} chunk
|
|
101
|
+
*/
|
|
102
|
+
add(chunk) {
|
|
103
|
+
this._appendDoc(chunk);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Searches the index and returns top-K results sorted by score descending.
|
|
108
|
+
* @param {string} query
|
|
109
|
+
* @param {number} topK
|
|
110
|
+
* @returns {{ score: number, text: string, metadata: object }[]}
|
|
111
|
+
*/
|
|
112
|
+
search(query, topK = 20) {
|
|
113
|
+
if (this._docs.length === 0) return [];
|
|
114
|
+
|
|
115
|
+
const queryTerms = tokenize(query);
|
|
116
|
+
if (queryTerms.length === 0) return [];
|
|
117
|
+
|
|
118
|
+
const scoredDocs = this._docs.map(doc => ({
|
|
119
|
+
score: scoreBm25(doc, queryTerms, this._df, this._docs.length, this._avgLen),
|
|
120
|
+
text: doc.text,
|
|
121
|
+
metadata: doc.metadata,
|
|
122
|
+
}));
|
|
123
|
+
|
|
124
|
+
const matchingDocs = scoredDocs.filter(r => r.score > 0);
|
|
125
|
+
return matchingDocs
|
|
126
|
+
.sort((a, b) => b.score - a.score)
|
|
127
|
+
.slice(0, topK);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Persists the index to a JSON file.
|
|
132
|
+
* @param {string} filePath
|
|
133
|
+
*/
|
|
134
|
+
save(filePath) {
|
|
135
|
+
const serialized = JSON.stringify({
|
|
136
|
+
docs: this._docs,
|
|
137
|
+
df: this._df,
|
|
138
|
+
avgLen: this._avgLen,
|
|
139
|
+
});
|
|
140
|
+
fs.writeFileSync(filePath, serialized, 'utf8');
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* Restores a BM25Index instance from a JSON file.
|
|
145
|
+
* @param {string} filePath
|
|
146
|
+
* @returns {BM25Index}
|
|
147
|
+
*/
|
|
148
|
+
static load(filePath) {
|
|
149
|
+
const raw = fs.readFileSync(filePath, 'utf8');
|
|
150
|
+
const { docs, df, avgLen } = JSON.parse(raw);
|
|
151
|
+
const idx = new BM25Index();
|
|
152
|
+
idx._docs = docs;
|
|
153
|
+
idx._df = df;
|
|
154
|
+
idx._avgLen = avgLen;
|
|
155
|
+
idx._totalLen = docs.reduce((sum, d) => sum + d.len, 0);
|
|
156
|
+
return idx;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* Internal: adds one document and updates df and avgLen.
|
|
161
|
+
* @param {{ text: string, metadata: object }} chunk
|
|
162
|
+
*/
|
|
163
|
+
_appendDoc(chunk) {
|
|
164
|
+
const tokens = tokenize(chunk.text);
|
|
165
|
+
const freqMap = countTermFrequencies(tokens);
|
|
166
|
+
const freqObj = Object.fromEntries(freqMap);
|
|
167
|
+
|
|
168
|
+
for (const term of freqMap.keys()) {
|
|
169
|
+
this._df[term] = (this._df[term] ?? 0) + 1;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
const len = tokens.length;
|
|
173
|
+
this._docs.push({ text: chunk.text, metadata: chunk.metadata, freq: freqObj, len });
|
|
174
|
+
|
|
175
|
+
this._totalLen += len;
|
|
176
|
+
this._avgLen = this._totalLen / this._docs.length;
|
|
177
|
+
}
|
|
178
|
+
}
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
// lib/engine/capture.js
|
|
2
|
+
import { spawnSync } from 'node:child_process';
|
|
3
|
+
import { writeFileSync, readFileSync, unlinkSync } from 'node:fs';
|
|
4
|
+
import { tmpdir } from 'node:os';
|
|
5
|
+
import { join } from 'node:path';
|
|
6
|
+
|
|
7
|
+
// ── AI prompt builders (exported for testing) ─────────────────────────────────
|
|
8
|
+
|
|
9
|
+
/** Returns the prompt used to ask Claude to structure raw dictation into a clean note. */
|
|
10
|
+
export function buildDictatePrompt(rawText) {
|
|
11
|
+
return `You are a knowledge management assistant. Structure the following raw notes into a clean markdown knowledge note.
|
|
12
|
+
|
|
13
|
+
Raw input:
|
|
14
|
+
${rawText}
|
|
15
|
+
|
|
16
|
+
Return ONLY valid YAML frontmatter + markdown. The frontmatter must include:
|
|
17
|
+
- title: (concise, descriptive title extracted or inferred from the content)
|
|
18
|
+
- tags: (2-5 relevant tags as a YAML list)
|
|
19
|
+
- type: note
|
|
20
|
+
|
|
21
|
+
Fix grammar and typos. Preserve all meaning. Do not add information not present in the input.
|
|
22
|
+
Return nothing except the markdown document starting with ---.`;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/** Returns the prompt used to summarize a conversation transcript into a structured note. */
|
|
26
|
+
export function buildSummarizePrompt(transcript, title = '') {
|
|
27
|
+
const titleLine = title ? `Title: ${title}\n\n` : '';
|
|
28
|
+
return `You are a knowledge management assistant. Summarize this AI conversation into a structured knowledge note.
|
|
29
|
+
|
|
30
|
+
${titleLine}Conversation:
|
|
31
|
+
${transcript}
|
|
32
|
+
|
|
33
|
+
Return ONLY a markdown document starting with ---. Include this frontmatter:
|
|
34
|
+
- title: (descriptive title${title ? ` — use "${title}" as basis` : ''})
|
|
35
|
+
- tags: (2-5 relevant tags)
|
|
36
|
+
- type: note
|
|
37
|
+
|
|
38
|
+
And these sections in the body:
|
|
39
|
+
## Key Decisions
|
|
40
|
+
(bullet list of decisions made)
|
|
41
|
+
|
|
42
|
+
## Findings
|
|
43
|
+
(bullet list of key findings or conclusions)
|
|
44
|
+
|
|
45
|
+
## Context
|
|
46
|
+
(1-2 sentences of background)
|
|
47
|
+
|
|
48
|
+
Then append the full transcript inside a details element:
|
|
49
|
+
<details>
|
|
50
|
+
<summary>Full conversation transcript</summary>
|
|
51
|
+
|
|
52
|
+
${transcript}
|
|
53
|
+
|
|
54
|
+
</details>`;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// ── AI call (requires ANTHROPIC_API_KEY env var) ──────────────────────────────
|
|
58
|
+
|
|
59
|
+
/** Calls claude-haiku via the Anthropic Messages API. Requires ANTHROPIC_API_KEY. */
|
|
60
|
+
export async function callAnthropicAPI(prompt) {
|
|
61
|
+
const apiKey = process.env.ANTHROPIC_API_KEY;
|
|
62
|
+
if (!apiKey) {
|
|
63
|
+
throw new Error('ANTHROPIC_API_KEY not set. Export it or add to .env.local');
|
|
64
|
+
}
|
|
65
|
+
const response = await fetch('https://api.anthropic.com/v1/messages', {
|
|
66
|
+
method: 'POST',
|
|
67
|
+
headers: {
|
|
68
|
+
'Content-Type': 'application/json',
|
|
69
|
+
'x-api-key': apiKey,
|
|
70
|
+
'anthropic-version': '2023-06-01',
|
|
71
|
+
},
|
|
72
|
+
body: JSON.stringify({
|
|
73
|
+
model: 'claude-haiku-4-5-20251001',
|
|
74
|
+
max_tokens: 2048,
|
|
75
|
+
messages: [{ role: 'user', content: prompt }],
|
|
76
|
+
}),
|
|
77
|
+
});
|
|
78
|
+
if (!response.ok) {
|
|
79
|
+
const body = await response.text();
|
|
80
|
+
throw new Error(`Anthropic API ${response.status}: ${body}`);
|
|
81
|
+
}
|
|
82
|
+
const data = await response.json();
|
|
83
|
+
return data.content[0].text;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// ── Input helpers ─────────────────────────────────────────────────────────────
|
|
87
|
+
|
|
88
|
+
/** Opens $EDITOR (or vi) with optional initial content. Returns edited content. */
|
|
89
|
+
export function openEditor(initialContent = '') {
|
|
90
|
+
const tmpFile = join(tmpdir(), `booklib-edit-${Date.now()}.md`);
|
|
91
|
+
writeFileSync(tmpFile, initialContent, 'utf8');
|
|
92
|
+
const editor = process.env.EDITOR ?? 'vi';
|
|
93
|
+
spawnSync(editor, [tmpFile], { stdio: 'inherit' });
|
|
94
|
+
const content = readFileSync(tmpFile, 'utf8');
|
|
95
|
+
unlinkSync(tmpFile);
|
|
96
|
+
return content.trim();
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/** Reads all of stdin when piped. Returns empty string when stdin is a TTY. */
|
|
100
|
+
export async function readStdin() {
|
|
101
|
+
if (process.stdin.isTTY) return '';
|
|
102
|
+
return new Promise(resolve => {
|
|
103
|
+
let data = '';
|
|
104
|
+
process.stdin.setEncoding('utf8');
|
|
105
|
+
process.stdin.on('data', chunk => { data += chunk; });
|
|
106
|
+
process.stdin.on('end', () => resolve(data.trim()));
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/** Prompts user to type input interactively until Ctrl+D. */
|
|
111
|
+
export async function readInteractive(prompt = 'Type your note (Ctrl+D when done):') {
|
|
112
|
+
process.stdout.write(prompt + '\n');
|
|
113
|
+
return new Promise(resolve => {
|
|
114
|
+
let data = '';
|
|
115
|
+
process.stdin.setEncoding('utf8');
|
|
116
|
+
process.stdin.resume();
|
|
117
|
+
process.stdin.on('data', chunk => { data += chunk; });
|
|
118
|
+
process.stdin.on('end', () => resolve(data.trim()));
|
|
119
|
+
});
|
|
120
|
+
}
|