@booklib/core 2.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/.cursor/rules/booklib-standards.mdc +40 -0
- package/.gemini/context.md +372 -0
- package/AGENTS.md +166 -0
- package/CHANGELOG.md +226 -0
- package/CLAUDE.md +81 -0
- package/CODE_OF_CONDUCT.md +31 -0
- package/CONTRIBUTING.md +304 -0
- package/LICENSE +21 -0
- package/PLAN.md +28 -0
- package/README.ja.md +198 -0
- package/README.ko.md +198 -0
- package/README.md +503 -0
- package/README.pt-BR.md +198 -0
- package/README.uk.md +241 -0
- package/README.zh-CN.md +198 -0
- package/SECURITY.md +9 -0
- package/agents/architecture-reviewer.md +136 -0
- package/agents/booklib-reviewer.md +90 -0
- package/agents/data-reviewer.md +107 -0
- package/agents/jvm-reviewer.md +146 -0
- package/agents/python-reviewer.md +128 -0
- package/agents/rust-reviewer.md +115 -0
- package/agents/ts-reviewer.md +110 -0
- package/agents/ui-reviewer.md +117 -0
- package/assets/logo.svg +36 -0
- package/bin/booklib-mcp.js +304 -0
- package/bin/booklib.js +1705 -0
- package/bin/skills.cjs +1292 -0
- package/booklib-router.mdc +36 -0
- package/booklib.config.json +19 -0
- package/commands/animation-at-work.md +10 -0
- package/commands/clean-code-reviewer.md +10 -0
- package/commands/data-intensive-patterns.md +10 -0
- package/commands/data-pipelines.md +10 -0
- package/commands/design-patterns.md +10 -0
- package/commands/domain-driven-design.md +10 -0
- package/commands/effective-java.md +10 -0
- package/commands/effective-kotlin.md +10 -0
- package/commands/effective-python.md +10 -0
- package/commands/effective-typescript.md +10 -0
- package/commands/kotlin-in-action.md +10 -0
- package/commands/lean-startup.md +10 -0
- package/commands/microservices-patterns.md +10 -0
- package/commands/programming-with-rust.md +10 -0
- package/commands/refactoring-ui.md +10 -0
- package/commands/rust-in-action.md +10 -0
- package/commands/skill-router.md +10 -0
- package/commands/spring-boot-in-action.md +10 -0
- package/commands/storytelling-with-data.md +10 -0
- package/commands/system-design-interview.md +10 -0
- package/commands/using-asyncio-python.md +10 -0
- package/commands/web-scraping-python.md +10 -0
- package/community/registry.json +1616 -0
- package/hooks/hooks.json +23 -0
- package/hooks/posttooluse-capture.mjs +67 -0
- package/hooks/suggest.js +153 -0
- package/lib/agent-behaviors.js +40 -0
- package/lib/agent-detector.js +96 -0
- package/lib/config-loader.js +39 -0
- package/lib/conflict-resolver.js +148 -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/ai-features.js +253 -0
- package/lib/engine/auditor.js +103 -0
- package/lib/engine/bm25-index.js +178 -0
- package/lib/engine/capture.js +120 -0
- package/lib/engine/corrections.js +198 -0
- package/lib/engine/doctor.js +195 -0
- package/lib/engine/graph-injector.js +137 -0
- package/lib/engine/graph.js +161 -0
- package/lib/engine/handoff.js +405 -0
- package/lib/engine/indexer.js +242 -0
- package/lib/engine/parser.js +53 -0
- package/lib/engine/query-expander.js +42 -0
- package/lib/engine/reranker.js +40 -0
- package/lib/engine/rrf.js +59 -0
- package/lib/engine/scanner.js +151 -0
- package/lib/engine/searcher.js +139 -0
- package/lib/engine/session-coordinator.js +306 -0
- package/lib/engine/session-manager.js +429 -0
- package/lib/engine/synthesizer.js +70 -0
- package/lib/installer.js +70 -0
- package/lib/instinct-block.js +33 -0
- package/lib/mcp-config-writer.js +88 -0
- package/lib/paths.js +57 -0
- package/lib/profiles/design.md +19 -0
- package/lib/profiles/general.md +16 -0
- package/lib/profiles/research-analysis.md +22 -0
- package/lib/profiles/software-development.md +23 -0
- package/lib/profiles/writing-content.md +19 -0
- package/lib/project-initializer.js +916 -0
- package/lib/registry/skills.js +102 -0
- package/lib/registry-searcher.js +99 -0
- package/lib/rules/rules-manager.js +169 -0
- package/lib/skill-fetcher.js +333 -0
- package/lib/well-known-builder.js +70 -0
- package/lib/wizard/index.js +404 -0
- package/lib/wizard/integration-detector.js +41 -0
- package/lib/wizard/project-detector.js +100 -0
- package/lib/wizard/prompt.js +156 -0
- package/lib/wizard/registry-embeddings.js +107 -0
- package/lib/wizard/skill-recommender.js +69 -0
- package/llms-full.txt +254 -0
- package/llms.txt +70 -0
- package/package.json +45 -0
- package/research-reports/2026-04-01-current-architecture.md +160 -0
- package/research-reports/IDEAS.md +93 -0
- package/rules/common/clean-code.md +42 -0
- package/rules/java/effective-java.md +42 -0
- package/rules/kotlin/effective-kotlin.md +37 -0
- package/rules/python/effective-python.md +38 -0
- package/rules/rust/rust.md +37 -0
- package/rules/typescript/effective-typescript.md +42 -0
- package/scripts/gen-llms-full.mjs +36 -0
- package/scripts/gen-og.mjs +142 -0
- package/scripts/validate-frontmatter.js +25 -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/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
- package/tests/agent-detector.test.js +83 -0
- package/tests/corrections.test.js +245 -0
- package/tests/doctor/hook-installer.test.js +72 -0
- package/tests/doctor/usage-tracker.test.js +140 -0
- package/tests/engine/benchmark-eval.test.js +31 -0
- package/tests/engine/bm25-index.test.js +85 -0
- package/tests/engine/capture-command.test.js +35 -0
- package/tests/engine/capture.test.js +17 -0
- package/tests/engine/graph-augmented-search.test.js +107 -0
- package/tests/engine/graph-injector.test.js +44 -0
- package/tests/engine/graph.test.js +216 -0
- package/tests/engine/hybrid-searcher.test.js +74 -0
- package/tests/engine/indexer-bm25.test.js +37 -0
- package/tests/engine/mcp-tools.test.js +73 -0
- package/tests/engine/project-initializer-mcp.test.js +99 -0
- package/tests/engine/query-expander.test.js +36 -0
- package/tests/engine/reranker.test.js +51 -0
- package/tests/engine/rrf.test.js +49 -0
- package/tests/engine/srag-prefix.test.js +47 -0
- package/tests/instinct-block.test.js +23 -0
- package/tests/mcp-config-writer.test.js +60 -0
- package/tests/project-initializer-new-agents.test.js +48 -0
- package/tests/rules/rules-manager.test.js +230 -0
- package/tests/well-known-builder.test.js +40 -0
- package/tests/wizard/integration-detector.test.js +31 -0
- package/tests/wizard/project-detector.test.js +51 -0
- package/tests/wizard/prompt-session.test.js +61 -0
- package/tests/wizard/prompt.test.js +16 -0
- package/tests/wizard/registry-embeddings.test.js +35 -0
- package/tests/wizard/skill-recommender.test.js +34 -0
- package/tests/wizard/slot-count.test.js +25 -0
- package/vercel.json +21 -0
|
@@ -0,0 +1,405 @@
|
|
|
1
|
+
import fs from 'fs';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import { execSync } from 'child_process';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Manages agent session snapshots for multi-agent handoffs.
|
|
7
|
+
*/
|
|
8
|
+
export class BookLibHandoff {
|
|
9
|
+
constructor(handoffDir = path.join(process.cwd(), '.booklib', 'sessions')) {
|
|
10
|
+
this.handoffDir = handoffDir;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Automatically detects a session ID based on Git branch or folder name + timestamp.
|
|
15
|
+
*/
|
|
16
|
+
getAutoSessionId() {
|
|
17
|
+
try {
|
|
18
|
+
// 1. Try Git Branch
|
|
19
|
+
return execSync('git rev-parse --abbrev-ref HEAD', { stdio: 'pipe' }).toString().trim();
|
|
20
|
+
} catch {
|
|
21
|
+
// 2. Fallback to Folder Name + Date (for non-git or unrelated chats)
|
|
22
|
+
const folder = path.basename(process.cwd());
|
|
23
|
+
const date = new Date().toISOString().split('T')[0];
|
|
24
|
+
return `${folder}-${date}`;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Resolves the file path for a session.
|
|
30
|
+
*/
|
|
31
|
+
getSessionPath(name) {
|
|
32
|
+
const sessionName = name || this.getAutoSessionId();
|
|
33
|
+
return path.join(this.handoffDir, `${sessionName}.md`);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Saves the current agent session state to a file.
|
|
38
|
+
* Enhanced with git state, uncommitted changes, and recent commits tracking.
|
|
39
|
+
* For long-running chats: includes recent commit history as implicit memory.
|
|
40
|
+
*/
|
|
41
|
+
saveState({ name, goal, next, progress, skills }) {
|
|
42
|
+
if (!fs.existsSync(this.handoffDir)) {
|
|
43
|
+
fs.mkdirSync(this.handoffDir, { recursive: true });
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// Capture git state
|
|
47
|
+
let gitInfo = '';
|
|
48
|
+
let recentCommits = '';
|
|
49
|
+
try {
|
|
50
|
+
const branch = execSync('git rev-parse --abbrev-ref HEAD', { stdio: 'pipe' }).toString().trim();
|
|
51
|
+
const lastCommit = execSync('git log -1 --format=%H%n%s', { stdio: 'pipe' }).toString().trim().split('\n');
|
|
52
|
+
const stagedFiles = execSync('git diff --name-only --cached', { stdio: 'pipe' }).toString().trim().split('\n').filter(Boolean);
|
|
53
|
+
const unstagedFiles = execSync('git diff --name-only', { stdio: 'pipe' }).toString().trim().split('\n').filter(Boolean);
|
|
54
|
+
|
|
55
|
+
// Capture last 10 commits (implicit chat memory via commit messages)
|
|
56
|
+
const commits = execSync('git log --oneline -10', { stdio: 'pipe' }).toString().trim().split('\n');
|
|
57
|
+
recentCommits = commits.map(c => ` ${c}`).join('\n');
|
|
58
|
+
|
|
59
|
+
gitInfo = `
|
|
60
|
+
<git_state>
|
|
61
|
+
<branch>${branch}</branch>
|
|
62
|
+
<last_commit_sha>${lastCommit[0]}</last_commit_sha>
|
|
63
|
+
<last_commit_msg>${lastCommit[1] || 'N/A'}</last_commit_msg>
|
|
64
|
+
<uncommitted_changes>
|
|
65
|
+
<staged_files>${stagedFiles.length > 0 ? stagedFiles.join(', ') : 'None'}</staged_files>
|
|
66
|
+
<modified_files>${unstagedFiles.length > 0 ? unstagedFiles.join(', ') : 'None'}</modified_files>
|
|
67
|
+
</uncommitted_changes>
|
|
68
|
+
<recent_commit_history>
|
|
69
|
+
<note>Use this as implicit context: each commit message documents decisions made</note>
|
|
70
|
+
${recentCommits}
|
|
71
|
+
</recent_commit_history>
|
|
72
|
+
</git_state>`;
|
|
73
|
+
} catch (err) {
|
|
74
|
+
gitInfo = `
|
|
75
|
+
<git_state>
|
|
76
|
+
<warning>Could not capture git state. Verify changes are committed before resuming.</warning>
|
|
77
|
+
</git_state>`;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const sessionPath = this.getSessionPath(name);
|
|
81
|
+
const content = `
|
|
82
|
+
<session_handoff>
|
|
83
|
+
<metadata>
|
|
84
|
+
<timestamp>${new Date().toISOString()}</timestamp>
|
|
85
|
+
<session_id>${name || this.getAutoSessionId()}</session_id>
|
|
86
|
+
<working_directory>${process.cwd()}</working_directory>
|
|
87
|
+
</metadata>
|
|
88
|
+
|
|
89
|
+
<context>
|
|
90
|
+
<goal>${goal || 'Not specified'}</goal>
|
|
91
|
+
<progress>${progress || 'Just started'}</progress>
|
|
92
|
+
<pending_tasks>${next || 'Determine next steps'}</pending_tasks>
|
|
93
|
+
<note>For long chats: review recent_commit_history below for detailed reasoning and decisions</note>
|
|
94
|
+
</context>
|
|
95
|
+
|
|
96
|
+
<active_knowledge>
|
|
97
|
+
${(skills || []).map(s => `<skill id="${s}" />`).join('\n ')}
|
|
98
|
+
</active_knowledge>
|
|
99
|
+
${gitInfo}
|
|
100
|
+
|
|
101
|
+
<recovery_instructions>
|
|
102
|
+
<step1>Run: \`node bin/booklib.js resume ${name || this.getAutoSessionId()}\` to load this context</step1>
|
|
103
|
+
<step2>Review the pending_tasks above</step2>
|
|
104
|
+
<step3>CHECK RECENT COMMIT HISTORY (git_state/recent_commit_history) for chat reasoning</step3>
|
|
105
|
+
<step4>If resuming in a different session, ensure you're in the correct working_directory</step4>
|
|
106
|
+
<step5>Run: \`git log --oneline -20\` to see full history if needed</step5>
|
|
107
|
+
</recovery_instructions>
|
|
108
|
+
|
|
109
|
+
<long_chat_recovery_guide>
|
|
110
|
+
<context_source>Since conversation transcripts aren't saved, use these sources:</context_source>
|
|
111
|
+
<source1>Recent commit messages (above) document each decision</source1>
|
|
112
|
+
<source2>Run: \`git show\` on recent commits to see code changes + reasoning</source2>
|
|
113
|
+
<source3>Run: \`git log -p --follow -- <file>\` to see file evolution</source3>
|
|
114
|
+
<source4>Pending_tasks above shows immediate next steps</source4>
|
|
115
|
+
<source5>Active skills tell you which frameworks were being applied</source5>
|
|
116
|
+
</long_chat_recovery_guide>
|
|
117
|
+
</session_handoff>
|
|
118
|
+
`;
|
|
119
|
+
|
|
120
|
+
fs.writeFileSync(sessionPath, content.trim());
|
|
121
|
+
console.log(`✅ Session snapshot saved to ${sessionPath}`);
|
|
122
|
+
console.log(`📝 Git state captured: branch, commits, uncommitted changes`);
|
|
123
|
+
console.log(`📚 Recent 10 commits saved for implicit chat memory`);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Resumes the session by reading the handoff file.
|
|
128
|
+
*/
|
|
129
|
+
resume(name) {
|
|
130
|
+
const sessionPath = this.getSessionPath(name);
|
|
131
|
+
|
|
132
|
+
if (!fs.existsSync(sessionPath)) {
|
|
133
|
+
const sessions = this.listSessions();
|
|
134
|
+
if (sessions.length > 0) {
|
|
135
|
+
return `No snapshot found for "${name || this.getAutoSessionId()}". \nAvailable sessions: ${sessions.join(', ')}`;
|
|
136
|
+
}
|
|
137
|
+
return 'No handoff files found in .booklib/sessions/. Starting a fresh session.';
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
const content = fs.readFileSync(sessionPath, 'utf8');
|
|
141
|
+
return `
|
|
142
|
+
=== RESUMING SESSION [${name || this.getAutoSessionId()}] ===
|
|
143
|
+
An previous agent has left a context snapshot for you.
|
|
144
|
+
Please read the following handoff details and continue the work:
|
|
145
|
+
|
|
146
|
+
${content}
|
|
147
|
+
`;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* Lists all available handoff sessions.
|
|
152
|
+
*/
|
|
153
|
+
listSessions() {
|
|
154
|
+
if (!fs.existsSync(this.handoffDir)) return [];
|
|
155
|
+
return fs.readdirSync(this.handoffDir)
|
|
156
|
+
.filter(f => f.endsWith('.md'))
|
|
157
|
+
.map(f => f.replace('.md', ''));
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* Sets up automatic handoff saving on process exit (SIGINT, SIGTERM, SIGHUP).
|
|
162
|
+
* Catches the case where user forgets to explicitly call save-state.
|
|
163
|
+
*
|
|
164
|
+
* Usage:
|
|
165
|
+
* const handoff = new BookLibHandoff();
|
|
166
|
+
* handoff.setupAutoSave({
|
|
167
|
+
* goal: 'Build payment processor',
|
|
168
|
+
* progress: 'Phase 2 in progress',
|
|
169
|
+
* next: 'Implement webhook handler',
|
|
170
|
+
* skills: ['effective-typescript', 'clean-code-reviewer']
|
|
171
|
+
* });
|
|
172
|
+
*/
|
|
173
|
+
setupAutoSave(options = {}) {
|
|
174
|
+
const { goal, progress, next, skills } = options;
|
|
175
|
+
const sessionId = this.getAutoSessionId();
|
|
176
|
+
|
|
177
|
+
const signals = ['SIGINT', 'SIGTERM', 'SIGHUP'];
|
|
178
|
+
|
|
179
|
+
signals.forEach(signal => {
|
|
180
|
+
process.on(signal, () => {
|
|
181
|
+
try {
|
|
182
|
+
console.log('\n⚠️ Session interrupted. Auto-saving handoff...');
|
|
183
|
+
this.saveState({
|
|
184
|
+
name: sessionId,
|
|
185
|
+
goal: goal || 'Session interrupted - see git log for context',
|
|
186
|
+
progress: progress || 'In progress - check recent commits',
|
|
187
|
+
next: next || 'Resume from last commit',
|
|
188
|
+
skills: skills || []
|
|
189
|
+
});
|
|
190
|
+
console.log(`✅ Auto-saved to ~/.booklib/sessions/${sessionId}.md`);
|
|
191
|
+
console.log(`💡 Next agent can resume with: booklib resume`);
|
|
192
|
+
process.exit(0);
|
|
193
|
+
} catch (err) {
|
|
194
|
+
console.error('⚠️ Auto-save failed:', err.message);
|
|
195
|
+
process.exit(1);
|
|
196
|
+
}
|
|
197
|
+
});
|
|
198
|
+
});
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
/**
|
|
202
|
+
* Recovers handoff state from session files OR git (100% coverage).
|
|
203
|
+
*
|
|
204
|
+
* Priority order:
|
|
205
|
+
* 1. Explicit session file for current branch
|
|
206
|
+
* 2. Parent session (via lineage)
|
|
207
|
+
* 3. Most recent session on same branch
|
|
208
|
+
* 4. Git-based recovery (fallback)
|
|
209
|
+
*
|
|
210
|
+
* Usage:
|
|
211
|
+
* const recovered = handoff.recoverFromSessionOrGit();
|
|
212
|
+
* console.log(recovered);
|
|
213
|
+
*/
|
|
214
|
+
recoverFromSessionOrGit() {
|
|
215
|
+
const branch = this._getCurrentBranch();
|
|
216
|
+
const sessionPath = this.getSessionPath(branch);
|
|
217
|
+
|
|
218
|
+
// Try 1: Explicit session file for current branch
|
|
219
|
+
if (fs.existsSync(sessionPath)) {
|
|
220
|
+
const content = fs.readFileSync(sessionPath, 'utf8');
|
|
221
|
+
return `
|
|
222
|
+
SESSION-BASED RECOVERY (found matching session)
|
|
223
|
+
═══════════════════════════════════════════════════
|
|
224
|
+
|
|
225
|
+
Branch: ${branch}
|
|
226
|
+
Status: Session file found for this branch ✅
|
|
227
|
+
|
|
228
|
+
${content}
|
|
229
|
+
|
|
230
|
+
NEXT STEPS:
|
|
231
|
+
1. Review the context above from the previous agent
|
|
232
|
+
2. Check git status for any uncommitted work
|
|
233
|
+
3. Run: git log --oneline -5 to see recent commits
|
|
234
|
+
4. Continue work as indicated in pending_tasks above
|
|
235
|
+
`;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
// Try 2: Check lineage for parent session
|
|
239
|
+
const parentSession = this._getParentSession(branch);
|
|
240
|
+
if (parentSession) {
|
|
241
|
+
const parentPath = this.getSessionPath(parentSession);
|
|
242
|
+
if (fs.existsSync(parentPath)) {
|
|
243
|
+
const content = fs.readFileSync(parentPath, 'utf8');
|
|
244
|
+
return `
|
|
245
|
+
SESSION-BASED RECOVERY (found parent session in lineage)
|
|
246
|
+
═════════════════════════════════════════════════════════
|
|
247
|
+
|
|
248
|
+
Branch: ${branch}
|
|
249
|
+
Status: No explicit session for this branch, but parent "${parentSession}" found ✅
|
|
250
|
+
|
|
251
|
+
PARENT SESSION CONTEXT:
|
|
252
|
+
${content}
|
|
253
|
+
|
|
254
|
+
RECOVERY INTERPRETATION:
|
|
255
|
+
- This branch was created from: ${parentSession}
|
|
256
|
+
- Parent agent's work provides the base context
|
|
257
|
+
- Any commits since parent session show your additional work
|
|
258
|
+
- Run: git log ${parentSession}..HEAD to see your new commits
|
|
259
|
+
|
|
260
|
+
NEXT STEPS:
|
|
261
|
+
1. Review parent session context above
|
|
262
|
+
2. Run: git diff ${parentSession}..HEAD to see your changes
|
|
263
|
+
3. Review pending_tasks from parent session
|
|
264
|
+
4. Continue or adjust based on what you've added
|
|
265
|
+
`;
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
// Try 3: Most recent session on same branch
|
|
270
|
+
const recentSession = this._getMostRecentSessionOnBranch(branch);
|
|
271
|
+
if (recentSession) {
|
|
272
|
+
const recentPath = this.getSessionPath(recentSession);
|
|
273
|
+
if (fs.existsSync(recentPath)) {
|
|
274
|
+
const content = fs.readFileSync(recentPath, 'utf8');
|
|
275
|
+
return `
|
|
276
|
+
SESSION-BASED RECOVERY (found recent session on branch)
|
|
277
|
+
═══════════════════════════════════════════════════════
|
|
278
|
+
|
|
279
|
+
Branch: ${branch}
|
|
280
|
+
Status: Using most recent session on this branch: "${recentSession}" ✅
|
|
281
|
+
|
|
282
|
+
${content}
|
|
283
|
+
|
|
284
|
+
RECOVERY INTERPRETATION:
|
|
285
|
+
- This is the most recent session saved on this branch
|
|
286
|
+
- Any new commits since this session represent your continued work
|
|
287
|
+
- Run: git log ${recentSession}.. to see what you did after this session
|
|
288
|
+
|
|
289
|
+
NEXT STEPS:
|
|
290
|
+
1. Review the session context above
|
|
291
|
+
2. Check your recent commits for what you accomplished
|
|
292
|
+
3. Review pending_tasks from the session
|
|
293
|
+
4. Continue from where you left off
|
|
294
|
+
`;
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
// Fallback: Git-based recovery
|
|
299
|
+
return this.recoverFromGit();
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
// ─── HELPER METHODS FOR ENHANCED RECOVERY ───
|
|
303
|
+
|
|
304
|
+
/**
|
|
305
|
+
* Gets current git branch.
|
|
306
|
+
*/
|
|
307
|
+
_getCurrentBranch() {
|
|
308
|
+
try {
|
|
309
|
+
return execSync('git rev-parse --abbrev-ref HEAD', { stdio: 'pipe' }).toString().trim();
|
|
310
|
+
} catch {
|
|
311
|
+
return 'unknown';
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
/**
|
|
316
|
+
* Gets parent session from lineage file.
|
|
317
|
+
*/
|
|
318
|
+
_getParentSession(branch) {
|
|
319
|
+
const lineageFile = path.join(this.handoffDir, '_lineage.json');
|
|
320
|
+
if (!fs.existsSync(lineageFile)) return null;
|
|
321
|
+
|
|
322
|
+
try {
|
|
323
|
+
const lineage = JSON.parse(fs.readFileSync(lineageFile, 'utf8'));
|
|
324
|
+
return lineage[branch]?.parent || null;
|
|
325
|
+
} catch {
|
|
326
|
+
return null;
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
/**
|
|
331
|
+
* Finds most recent session saved on current branch.
|
|
332
|
+
*/
|
|
333
|
+
_getMostRecentSessionOnBranch(branch) {
|
|
334
|
+
if (!fs.existsSync(this.handoffDir)) return null;
|
|
335
|
+
|
|
336
|
+
const sessions = fs.readdirSync(this.handoffDir)
|
|
337
|
+
.filter(f => f.endsWith('.md') && f !== '_lineage.json')
|
|
338
|
+
.map(f => {
|
|
339
|
+
const filePath = path.join(this.handoffDir, f);
|
|
340
|
+
const content = fs.readFileSync(filePath, 'utf8');
|
|
341
|
+
const branchMatch = content.match(/<branch>(.*?)<\/branch>/);
|
|
342
|
+
const sessionBranch = branchMatch ? branchMatch[1] : null;
|
|
343
|
+
|
|
344
|
+
return {
|
|
345
|
+
name: f.replace('.md', ''),
|
|
346
|
+
branch: sessionBranch,
|
|
347
|
+
mtime: fs.statSync(filePath).mtime
|
|
348
|
+
};
|
|
349
|
+
})
|
|
350
|
+
.filter(s => s.branch === branch)
|
|
351
|
+
.sort((a, b) => b.mtime - a.mtime);
|
|
352
|
+
|
|
353
|
+
return sessions.length > 0 ? sessions[0].name : null;
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
/**
|
|
357
|
+
* Recovers handoff state from git when explicit save file is missing.
|
|
358
|
+
* Useful if user forgets to save-state before quota hit.
|
|
359
|
+
*
|
|
360
|
+
* Usage:
|
|
361
|
+
* const recovered = handoff.recoverFromGit();
|
|
362
|
+
* console.log(recovered);
|
|
363
|
+
*/
|
|
364
|
+
recoverFromGit() {
|
|
365
|
+
try {
|
|
366
|
+
const branch = this._getCurrentBranch();
|
|
367
|
+
const lastCommit = execSync('git log -1 --format=%H%n%s%n%b', { stdio: 'pipe' }).toString().trim().split('\n');
|
|
368
|
+
const commits = execSync('git log --oneline -10', { stdio: 'pipe' }).toString().trim();
|
|
369
|
+
const status = execSync('git status --short', { stdio: 'pipe' }).toString().trim();
|
|
370
|
+
|
|
371
|
+
return `
|
|
372
|
+
GIT-BASED RECOVERY (no session files found)
|
|
373
|
+
═══════════════════════════════════════════
|
|
374
|
+
|
|
375
|
+
Branch: ${branch}
|
|
376
|
+
|
|
377
|
+
Last Commit: ${lastCommit[0]}
|
|
378
|
+
Message: ${lastCommit[1] || 'N/A'}
|
|
379
|
+
|
|
380
|
+
Recent 10 Commits:
|
|
381
|
+
${commits}
|
|
382
|
+
|
|
383
|
+
Uncommitted Changes:
|
|
384
|
+
${status || 'None'}
|
|
385
|
+
|
|
386
|
+
RECOVERY STEPS:
|
|
387
|
+
1. Review recent commits above to understand what was done
|
|
388
|
+
2. Run: git show <commit-sha> to see code + reasoning
|
|
389
|
+
3. Run: git log -p -- <file> to trace file evolution
|
|
390
|
+
4. Run: git status to see current work in progress
|
|
391
|
+
5. Create explicit handoff for next agent: booklib save-state
|
|
392
|
+
|
|
393
|
+
SAVE FOR NEXT TIME:
|
|
394
|
+
Call "booklib save-state" before quota exhaustion to preserve:
|
|
395
|
+
- Goal statement
|
|
396
|
+
- Progress summary
|
|
397
|
+
- Next tasks
|
|
398
|
+
- Active skills
|
|
399
|
+
- Recovery instructions
|
|
400
|
+
`;
|
|
401
|
+
} catch (err) {
|
|
402
|
+
return `Could not recover from git: ${err.message}. Check manual git history.`;
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
}
|
|
@@ -0,0 +1,242 @@
|
|
|
1
|
+
import fs from 'fs';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import { pipeline } from '@huggingface/transformers';
|
|
4
|
+
import { LocalIndex } from 'vectra';
|
|
5
|
+
import { parseSkillFile } from './parser.js';
|
|
6
|
+
import { resolveBookLibPaths } from '../paths.js';
|
|
7
|
+
import { BM25Index } from './bm25-index.js';
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Builds a structured metadata prefix for SRAG-style embeddings.
|
|
11
|
+
* Prepended to chunk text before vector embedding so the model encodes domain context.
|
|
12
|
+
* @param {object} metadata - Chunk metadata (name/title, type, tags).
|
|
13
|
+
* @returns {string} Prefix string like "[skill:X] [type:Y] [tags:a,b] " or "".
|
|
14
|
+
*/
|
|
15
|
+
export function buildMetadataPrefix(metadata) {
|
|
16
|
+
const parts = [];
|
|
17
|
+
const label = metadata.name ?? metadata.title;
|
|
18
|
+
if (label) parts.push(`[skill:${label}]`);
|
|
19
|
+
if (metadata.type) parts.push(`[type:${metadata.type}]`);
|
|
20
|
+
if (Array.isArray(metadata.tags) && metadata.tags.length > 0) {
|
|
21
|
+
parts.push(`[tags:${metadata.tags.join(',')}]`);
|
|
22
|
+
}
|
|
23
|
+
return parts.length > 0 ? parts.join(' ') + ' ' : '';
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Handles the creation and updating of the semantic index for the BookLib library.
|
|
28
|
+
*/
|
|
29
|
+
export class BookLibIndexer {
|
|
30
|
+
constructor(indexPath) {
|
|
31
|
+
this.indexPath = indexPath ?? resolveBookLibPaths().indexPath;
|
|
32
|
+
fs.mkdirSync(this.indexPath, { recursive: true });
|
|
33
|
+
this.index = new LocalIndex(this.indexPath);
|
|
34
|
+
this.extractor = null;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
get bm25Path() {
|
|
38
|
+
return path.join(path.dirname(this.indexPath), 'bm25.json');
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
_loadOrCreateBM25() {
|
|
42
|
+
return fs.existsSync(this.bm25Path) ? BM25Index.load(this.bm25Path) : new BM25Index();
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Loads the embedding model (lazy-loaded).
|
|
47
|
+
* @param {Object} opts
|
|
48
|
+
* @param {boolean} [opts.quiet=false] - Suppress the "Loading local embedding model..." message.
|
|
49
|
+
*/
|
|
50
|
+
async loadModel(opts = {}) {
|
|
51
|
+
const { quiet = false } = opts;
|
|
52
|
+
if (!this.extractor) {
|
|
53
|
+
const indexExists = await this.index.isIndexCreated().catch(() => false);
|
|
54
|
+
if (!indexExists) {
|
|
55
|
+
console.log('First run: downloading embedding model (~25 MB, ~1 min)...');
|
|
56
|
+
} else if (!quiet) {
|
|
57
|
+
console.log('Loading local embedding model...');
|
|
58
|
+
}
|
|
59
|
+
this.extractor = await pipeline('feature-extraction', 'Xenova/all-MiniLM-L6-v2');
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Generates a vector embedding for a string.
|
|
65
|
+
*/
|
|
66
|
+
async getEmbedding(text) {
|
|
67
|
+
await this.loadModel();
|
|
68
|
+
const output = await this.extractor(text, { pooling: 'mean', normalize: true });
|
|
69
|
+
return Array.from(output.data);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Indexes a directory of skills.
|
|
74
|
+
*
|
|
75
|
+
* @param {string} dirPath - The root directory of the skills library.
|
|
76
|
+
* @param {boolean} clearFirst - Whether to clear the index before starting.
|
|
77
|
+
* @param {Object} opts
|
|
78
|
+
* @param {boolean} [opts.quiet=false] - Suppress per-file output; print a single summary instead.
|
|
79
|
+
*/
|
|
80
|
+
async indexDirectory(dirPath, clearFirst = false, opts = {}) {
|
|
81
|
+
const { quiet = false, onProgress } = opts;
|
|
82
|
+
|
|
83
|
+
if (clearFirst && fs.existsSync(this.indexPath)) {
|
|
84
|
+
fs.rmSync(this.indexPath, { recursive: true, force: true });
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
if (!(await this.index.isIndexCreated())) {
|
|
88
|
+
await this.index.createIndex();
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const files = this.getFiles(dirPath, ['.md', '.mdc']);
|
|
92
|
+
if (!quiet) console.log(`Found ${files.length} skill files to index in ${dirPath}.`);
|
|
93
|
+
|
|
94
|
+
// Pre-warm the model so the load message respects the quiet flag.
|
|
95
|
+
await this.loadModel({ quiet });
|
|
96
|
+
const bm25Chunks = [];
|
|
97
|
+
|
|
98
|
+
let totalFiles = 0;
|
|
99
|
+
let totalChunks = 0;
|
|
100
|
+
let skipped = 0;
|
|
101
|
+
|
|
102
|
+
for (let fileIndex = 0; fileIndex < files.length; fileIndex++) {
|
|
103
|
+
const file = files[fileIndex];
|
|
104
|
+
const content = fs.readFileSync(file, 'utf8');
|
|
105
|
+
const relativePath = path.relative(dirPath, file);
|
|
106
|
+
let chunks;
|
|
107
|
+
try {
|
|
108
|
+
chunks = parseSkillFile(content, relativePath);
|
|
109
|
+
} catch (err) {
|
|
110
|
+
if (quiet) {
|
|
111
|
+
skipped++;
|
|
112
|
+
} else {
|
|
113
|
+
process.stderr.write(`⚠ Skipping ${relativePath}: ${err.message}\n`);
|
|
114
|
+
}
|
|
115
|
+
continue;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
onProgress?.({ current: fileIndex + 1, total: files.length, file: relativePath });
|
|
119
|
+
|
|
120
|
+
if (quiet) {
|
|
121
|
+
totalFiles++;
|
|
122
|
+
totalChunks += chunks.length;
|
|
123
|
+
} else {
|
|
124
|
+
console.log(`Indexing ${relativePath} (${chunks.length} chunks)...`);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
bm25Chunks.push(...chunks);
|
|
128
|
+
|
|
129
|
+
for (const chunk of chunks) {
|
|
130
|
+
const prefixedText = buildMetadataPrefix(chunk.metadata) + chunk.text;
|
|
131
|
+
const vector = await this.getEmbedding(prefixedText);
|
|
132
|
+
await this.index.insertItem({
|
|
133
|
+
vector,
|
|
134
|
+
metadata: { ...chunk.metadata, text: chunk.text }
|
|
135
|
+
});
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
const bm25 = new BM25Index();
|
|
140
|
+
bm25.build(bm25Chunks);
|
|
141
|
+
bm25.save(this.bm25Path);
|
|
142
|
+
|
|
143
|
+
if (quiet) {
|
|
144
|
+
console.log(` Indexed ${totalFiles} files (${totalChunks} chunks)`);
|
|
145
|
+
if (skipped > 0) {
|
|
146
|
+
console.log(` ⚠ ${skipped} file(s) skipped (malformed frontmatter)`);
|
|
147
|
+
}
|
|
148
|
+
} else {
|
|
149
|
+
console.log('Indexing complete.');
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* Indexes a single knowledge node file into the existing index.
|
|
155
|
+
* Safe to call after each capture command — only adds the new node.
|
|
156
|
+
* @param {string} filePath - Absolute path to the node .md file.
|
|
157
|
+
* @param {string} nodesDir - Root nodes directory (used for relative path in metadata).
|
|
158
|
+
*/
|
|
159
|
+
async indexNodeFile(filePath, nodesDir) {
|
|
160
|
+
if (!(await this.index.isIndexCreated())) {
|
|
161
|
+
await this.index.createIndex();
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
const content = fs.readFileSync(filePath, 'utf8');
|
|
165
|
+
const relativePath = path.relative(nodesDir, filePath);
|
|
166
|
+
let chunks;
|
|
167
|
+
try {
|
|
168
|
+
chunks = parseSkillFile(content, relativePath);
|
|
169
|
+
} catch (err) {
|
|
170
|
+
process.stderr.write(`⚠ Skipping node ${relativePath}: ${err.message}\n`);
|
|
171
|
+
return;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
// If body is empty, index the frontmatter fields so the node is still findable by title/tags
|
|
175
|
+
if (chunks.length === 0) {
|
|
176
|
+
const matter = (await import('gray-matter')).default;
|
|
177
|
+
const { data } = matter.read(filePath);
|
|
178
|
+
const fallbackText = [data.title, data.type, ...(data.tags ?? [])].filter(Boolean).join(' ');
|
|
179
|
+
if (!fallbackText) return;
|
|
180
|
+
const prefixedFallback = buildMetadataPrefix(data) + fallbackText;
|
|
181
|
+
const vector = await this.getEmbedding(prefixedFallback);
|
|
182
|
+
await this.index.insertItem({
|
|
183
|
+
vector,
|
|
184
|
+
metadata: { text: fallbackText, id: data.id, title: data.title, type: data.type, nodeKind: 'knowledge', nodeFile: filePath }
|
|
185
|
+
});
|
|
186
|
+
const bm25 = this._loadOrCreateBM25();
|
|
187
|
+
bm25.add({ text: fallbackText, metadata: { id: data.id, title: data.title, type: data.type, nodeKind: 'knowledge', nodeFile: filePath } });
|
|
188
|
+
bm25.save(this.bm25Path);
|
|
189
|
+
return;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
for (const chunk of chunks) {
|
|
193
|
+
const prefixedText = buildMetadataPrefix(chunk.metadata) + chunk.text;
|
|
194
|
+
const vector = await this.getEmbedding(prefixedText);
|
|
195
|
+
await this.index.insertItem({
|
|
196
|
+
vector,
|
|
197
|
+
metadata: { ...chunk.metadata, text: chunk.text, nodeKind: 'knowledge', nodeFile: filePath }
|
|
198
|
+
});
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
const bm25 = this._loadOrCreateBM25();
|
|
202
|
+
for (const chunk of chunks) bm25.add(chunk);
|
|
203
|
+
bm25.save(this.bm25Path);
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
/**
|
|
207
|
+
* Indexes all knowledge nodes from .booklib/knowledge/nodes/.
|
|
208
|
+
* Used by `booklib index` to rebuild the full knowledge portion of the index.
|
|
209
|
+
* @param {string} nodesDir - Path to the nodes directory.
|
|
210
|
+
*/
|
|
211
|
+
async indexKnowledgeNodes(nodesDir) {
|
|
212
|
+
if (!fs.existsSync(nodesDir)) return;
|
|
213
|
+
|
|
214
|
+
const files = this.getFiles(nodesDir, ['.md']);
|
|
215
|
+
if (files.length === 0) return;
|
|
216
|
+
console.log(`Indexing ${files.length} knowledge node(s)...`);
|
|
217
|
+
|
|
218
|
+
for (const file of files) {
|
|
219
|
+
await this.indexNodeFile(file, nodesDir);
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
/**
|
|
224
|
+
* Helper to recursively list files with specific extensions.
|
|
225
|
+
*/
|
|
226
|
+
getFiles(dir, extensions) {
|
|
227
|
+
let results = [];
|
|
228
|
+
const list = fs.readdirSync(dir);
|
|
229
|
+
list.forEach(file => {
|
|
230
|
+
file = path.join(dir, file);
|
|
231
|
+
const stat = fs.statSync(file);
|
|
232
|
+
if (stat && stat.isDirectory()) {
|
|
233
|
+
results = results.concat(this.getFiles(file, extensions));
|
|
234
|
+
} else {
|
|
235
|
+
if (extensions.some(ext => file.endsWith(ext))) {
|
|
236
|
+
results.push(file);
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
});
|
|
240
|
+
return results;
|
|
241
|
+
}
|
|
242
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import matter from 'gray-matter';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Parses a markdown/mdc file and extracts semantic chunks based on XML tags.
|
|
5
|
+
* Each chunk includes the file's frontmatter as metadata.
|
|
6
|
+
*
|
|
7
|
+
* @param {string} content - The raw content of the file.
|
|
8
|
+
* @param {string} filePath - The path to the file (for metadata).
|
|
9
|
+
* @returns {Array<{text: string, metadata: object}>} - Array of semantic chunks.
|
|
10
|
+
*/
|
|
11
|
+
export function parseSkillFile(content, filePath) {
|
|
12
|
+
const { data: frontmatter, content: body } = matter(content);
|
|
13
|
+
const chunks = [];
|
|
14
|
+
|
|
15
|
+
// 1. Extract the full summary/intro (everything before the first XML tag)
|
|
16
|
+
const introMatch = body.split(/<[a-z_]+>/)[0].trim();
|
|
17
|
+
if (introMatch) {
|
|
18
|
+
chunks.push({
|
|
19
|
+
text: introMatch,
|
|
20
|
+
metadata: { ...frontmatter, filePath, type: 'summary' }
|
|
21
|
+
});
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// 2. Extract content from XML tags (Universal & Programming)
|
|
25
|
+
const tagRegex = /<([a-z_]+)>([\s\S]*?)<\/\1>/g;
|
|
26
|
+
let match;
|
|
27
|
+
while ((match = tagRegex.exec(body)) !== null) {
|
|
28
|
+
const tagName = match[1];
|
|
29
|
+
const tagContent = match[2].trim();
|
|
30
|
+
if (tagContent) {
|
|
31
|
+
// Map domain-specific tags to universal categories
|
|
32
|
+
let category = tagName;
|
|
33
|
+
if (tagName === 'core_principles') category = 'framework';
|
|
34
|
+
if (tagName === 'anti_patterns') category = 'pitfalls';
|
|
35
|
+
if (tagName === 'examples') category = 'case_studies';
|
|
36
|
+
|
|
37
|
+
chunks.push({
|
|
38
|
+
text: tagContent,
|
|
39
|
+
metadata: { ...frontmatter, filePath, type: category, originalTag: tagName }
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// 3. If no XML tags were found but there is a body, treat the whole body as a chunk
|
|
45
|
+
if (chunks.length <= 1 && body.trim() && body.trim() !== introMatch) {
|
|
46
|
+
chunks.push({
|
|
47
|
+
text: body.trim(),
|
|
48
|
+
metadata: { ...frontmatter, filePath, type: 'content' }
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
return chunks;
|
|
53
|
+
}
|