@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
package/bin/booklib.js
ADDED
|
@@ -0,0 +1,2394 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// Suppress noisy ML model initialisation warnings from @huggingface/transformers
|
|
3
|
+
// The library uses console.warn for dtype/device messages — filter them here.
|
|
4
|
+
const _origWarn = console.warn.bind(console);
|
|
5
|
+
console.warn = (...args) => {
|
|
6
|
+
const msg = typeof args[0] === 'string' ? args[0] : '';
|
|
7
|
+
if (msg.includes('dtype not specified') || msg.includes('Using the default dtype')) return;
|
|
8
|
+
_origWarn(...args);
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
import path from 'path';
|
|
12
|
+
import fs from 'fs';
|
|
13
|
+
import os from 'os';
|
|
14
|
+
import { fileURLToPath } from 'node:url';
|
|
15
|
+
import { createInterface } from 'node:readline';
|
|
16
|
+
|
|
17
|
+
const PACKAGE_ROOT = path.resolve(fileURLToPath(import.meta.url), '..', '..');
|
|
18
|
+
const BUNDLED_SKILLS_DIR = path.join(PACKAGE_ROOT, 'skills');
|
|
19
|
+
import { BookLibIndexer } from '../lib/engine/indexer.js';
|
|
20
|
+
import { BookLibSearcher } from '../lib/engine/searcher.js';
|
|
21
|
+
import { BookLibHandoff } from '../lib/engine/handoff.js';
|
|
22
|
+
import { BookLibAuditor } from '../lib/engine/auditor.js';
|
|
23
|
+
import { BookLibRegistrySearcher } from '../lib/registry-searcher.js';
|
|
24
|
+
import { BookLibInstaller } from '../lib/installer.js';
|
|
25
|
+
import { BookLibScanner } from '../lib/engine/scanner.js';
|
|
26
|
+
import { BookLibSessionCoordinator } from '../lib/engine/session-coordinator.js';
|
|
27
|
+
import { BookLibSessionManager } from '../lib/engine/session-manager.js';
|
|
28
|
+
import { resolveBookLibPaths } from '../lib/paths.js';
|
|
29
|
+
import { SkillFetcher, RequiresConfirmationError, listInstalledSkillNames, countInstalledSlots } from '../lib/skill-fetcher.js';
|
|
30
|
+
import { runWizard } from '../lib/wizard/index.js';
|
|
31
|
+
import { SKILL_LIMIT } from '../lib/wizard/skill-recommender.js';
|
|
32
|
+
import {
|
|
33
|
+
generateNodeId, serializeNode, saveNode, loadNode,
|
|
34
|
+
listNodes, appendEdge, parseNodeFrontmatter, resolveKnowledgePaths,
|
|
35
|
+
resolveNodeRef, EDGE_TYPES, parseCaptureLinkArgs,
|
|
36
|
+
} from '../lib/engine/graph.js';
|
|
37
|
+
import { autoLink, autoLinkReverse } from '../lib/engine/auto-linker.js';
|
|
38
|
+
import { DiscoveryEngine } from '../lib/discovery-engine.js';
|
|
39
|
+
import { ProjectInitializer } from '../lib/project-initializer.js';
|
|
40
|
+
import { ContextBuilder } from '../lib/context-builder.js';
|
|
41
|
+
import {
|
|
42
|
+
buildDictatePrompt, buildSummarizePrompt, callAnthropicAPI,
|
|
43
|
+
openEditor, readStdin, readInteractive,
|
|
44
|
+
} from '../lib/engine/capture.js';
|
|
45
|
+
import { readUsage, summarize } from '../lib/doctor/usage-tracker.js';
|
|
46
|
+
import { installTrackingHook } from '../lib/doctor/hook-installer.js';
|
|
47
|
+
import { listAvailable as listAvailableRules, installRule as installRuleFn, status as rulesStatus } from '../lib/rules/rules-manager.js';
|
|
48
|
+
import { addCorrection, listCorrections, removeCorrection, levelFromMentions } from '../lib/engine/corrections.js';
|
|
49
|
+
import { WellKnownBuilder } from '../lib/well-known-builder.js';
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Remove all BM25 and Vectra chunks belonging to a named source.
|
|
53
|
+
* Shared by the disconnect and refresh commands to avoid duplication.
|
|
54
|
+
*/
|
|
55
|
+
async function removeSourceChunks(sourceName, bm25Path, indexer) {
|
|
56
|
+
// Remove BM25 chunks and rebuild index stats
|
|
57
|
+
if (fs.existsSync(bm25Path)) {
|
|
58
|
+
const { BM25Index: BM25 } = await import('../lib/engine/bm25-index.js');
|
|
59
|
+
const idx = BM25.load(bm25Path);
|
|
60
|
+
const before = idx._docs.length;
|
|
61
|
+
idx._docs = idx._docs.filter(d => d.metadata?.sourceName !== sourceName);
|
|
62
|
+
idx._df = {};
|
|
63
|
+
idx._totalLen = 0;
|
|
64
|
+
for (const doc of idx._docs) {
|
|
65
|
+
for (const term of Object.keys(doc.freq)) {
|
|
66
|
+
idx._df[term] = (idx._df[term] ?? 0) + 1;
|
|
67
|
+
}
|
|
68
|
+
idx._totalLen += doc.len;
|
|
69
|
+
}
|
|
70
|
+
idx._avgLen = idx._docs.length > 0 ? idx._totalLen / idx._docs.length : 0;
|
|
71
|
+
idx.save(bm25Path);
|
|
72
|
+
const removed = before - idx._docs.length;
|
|
73
|
+
if (removed > 0) console.log(`Removed ${removed} BM25 chunk(s).`);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// Remove matching Vectra vectors
|
|
77
|
+
try {
|
|
78
|
+
const items = await indexer.index.listItems();
|
|
79
|
+
let vectraRemoved = 0;
|
|
80
|
+
for (const item of items) {
|
|
81
|
+
if (item.metadata?.sourceName === sourceName) {
|
|
82
|
+
await indexer.index.deleteItem(item.id);
|
|
83
|
+
vectraRemoved++;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
if (vectraRemoved > 0) console.log(`Removed ${vectraRemoved} vector chunk(s).`);
|
|
87
|
+
} catch {
|
|
88
|
+
// Vectra index may not exist yet
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
const args = process.argv.slice(2);
|
|
93
|
+
const command = args[0];
|
|
94
|
+
|
|
95
|
+
// Handle --version / -v before anything else
|
|
96
|
+
if (command === '--version' || command === '-v' || args.includes('--version')) {
|
|
97
|
+
const pkg = JSON.parse(fs.readFileSync(path.join(PACKAGE_ROOT, 'package.json'), 'utf8'));
|
|
98
|
+
console.log(`booklib v${pkg.version}`);
|
|
99
|
+
process.exit(0);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function parseFlag(args, flag) {
|
|
103
|
+
const long = args.find(a => a.startsWith(`--${flag}=`))?.replace(`--${flag}=`, '');
|
|
104
|
+
if (long !== undefined) return long;
|
|
105
|
+
const idx = args.indexOf(`--${flag}`);
|
|
106
|
+
return idx !== -1 ? args[idx + 1] : null;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function parseInterval(str) {
|
|
110
|
+
const match = str.match(/^(\d+)(s|m|h)$/);
|
|
111
|
+
if (!match) throw new Error(`Invalid interval: "${str}". Use: 30s, 5m, 1h`);
|
|
112
|
+
const [, num, unit] = match;
|
|
113
|
+
const multipliers = { s: 1000, m: 60000, h: 3600000 };
|
|
114
|
+
return parseInt(num) * multipliers[unit];
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function formatBytes(bytes) {
|
|
118
|
+
if (bytes < 1024) return `${bytes} B`;
|
|
119
|
+
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
|
|
120
|
+
return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/** Auto-index a freshly saved node so it's immediately searchable. Silently skips on error. */
|
|
124
|
+
async function autoIndexNode(filePath) {
|
|
125
|
+
const { nodesDir } = resolveKnowledgePaths();
|
|
126
|
+
try {
|
|
127
|
+
const indexer = new BookLibIndexer();
|
|
128
|
+
await indexer.indexNodeFile(filePath, nodesDir);
|
|
129
|
+
} catch {
|
|
130
|
+
// Index may not exist yet — user can run `booklib index` to build it
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
const TOOL_MENU = [
|
|
136
|
+
{ num: 1, name: 'Claude Code', target: 'claude', file: 'CLAUDE.md' },
|
|
137
|
+
{ num: 2, name: 'Cursor', target: 'cursor', file: '.cursor/rules/' },
|
|
138
|
+
{ num: 3, name: 'Copilot', target: 'copilot', file: '.github/copilot-instructions.md' },
|
|
139
|
+
{ num: 4, name: 'Gemini CLI', target: 'gemini', file: '.gemini/context.md' },
|
|
140
|
+
{ num: 5, name: 'Codex', target: 'codex', file: 'AGENTS.md' },
|
|
141
|
+
{ num: 6, name: 'Windsurf', target: 'windsurf', file: '.windsurfrules' },
|
|
142
|
+
{ num: 7, name: 'Roo Code', target: 'roo-code', file: '.roo/rules/' },
|
|
143
|
+
{ num: 8, name: 'OpenHands', target: 'openhands', file: '.openhands/instructions.md' },
|
|
144
|
+
{ num: 9, name: 'Junie', target: 'junie', file: '.junie/guidelines.md' },
|
|
145
|
+
{ num: 10, name: 'Goose', target: 'goose', file: '.goose/context.md' },
|
|
146
|
+
{ num: 11, name: 'OpenCode', target: 'opencode', file: 'opencode.toml' },
|
|
147
|
+
{ num: 12, name: 'Letta', target: 'letta', file: '.letta/instructions.md' },
|
|
148
|
+
{ num: 13, name: 'All', target: 'all', file: null },
|
|
149
|
+
];
|
|
150
|
+
|
|
151
|
+
const MCP_TOOL_MENU = [
|
|
152
|
+
{ num: 1, name: 'Claude Code', target: 'claude', file: '.claude/settings.json' },
|
|
153
|
+
{ num: 2, name: 'Cursor', target: 'cursor', file: '.cursor/mcp.json' },
|
|
154
|
+
{ num: 3, name: 'Copilot', target: 'copilot', file: '.vscode/mcp.json' },
|
|
155
|
+
{ num: 4, name: 'Gemini CLI', target: 'gemini', file: '.gemini/settings.json' },
|
|
156
|
+
{ num: 5, name: 'Codex', target: 'codex', file: '.codex/config.toml' },
|
|
157
|
+
{ num: 6, name: 'Roo Code', target: 'roo-code', file: '.roo/mcp.json' },
|
|
158
|
+
{ num: 7, name: 'Windsurf', target: 'windsurf', file: '~/.codeium/windsurf/mcp_config.json' },
|
|
159
|
+
{ num: 8, name: 'Goose', target: 'goose', file: '.goose/config.yaml' },
|
|
160
|
+
{ num: 9, name: 'Zed', target: 'zed', file: '.zed/settings.json' },
|
|
161
|
+
{ num: 10, name: 'Continue', target: 'continue', file: '.continue/mcpServers/booklib.yaml' },
|
|
162
|
+
{ num: 11, name: 'All of the above', target: 'all', file: null },
|
|
163
|
+
];
|
|
164
|
+
|
|
165
|
+
async function promptToolSelection() {
|
|
166
|
+
process.stdout.write('\nWhich AI tool do you use?\n\n');
|
|
167
|
+
for (const t of TOOL_MENU) {
|
|
168
|
+
const fileInfo = t.file ? ` → ${t.file}` : '';
|
|
169
|
+
process.stdout.write(` ${t.num}) ${t.name.padEnd(12)}${fileInfo}\n`);
|
|
170
|
+
}
|
|
171
|
+
process.stdout.write('\nSelect [1-13, or comma-separated for multiple]: ');
|
|
172
|
+
|
|
173
|
+
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
174
|
+
const answer = await new Promise(resolve => {
|
|
175
|
+
rl.once('line', line => { rl.close(); resolve(line.trim()); });
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
if (!answer) return 'all';
|
|
179
|
+
const nums = answer.split(',').map(n => parseInt(n.trim(), 10)).filter(n => !isNaN(n));
|
|
180
|
+
if (nums.length === 0 || nums.includes(13)) return 'all';
|
|
181
|
+
const selected = nums.map(n => TOOL_MENU.find(t => t.num === n)?.target).filter(Boolean);
|
|
182
|
+
return selected.length > 0 ? selected.join(',') : 'all';
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
async function promptMcpToolSelection() {
|
|
186
|
+
const SEP = '━'.repeat(51);
|
|
187
|
+
process.stdout.write(`\n${SEP}\n MCP Server Setup\n${SEP}\n\n`);
|
|
188
|
+
process.stdout.write(' BookLib has an MCP server — your AI tools can call it\n');
|
|
189
|
+
process.stdout.write(' directly to search knowledge, fetch context, and create\n');
|
|
190
|
+
process.stdout.write(' notes without leaving the conversation.\n\n');
|
|
191
|
+
process.stdout.write(' Wire up the MCP server? (Y/n): ');
|
|
192
|
+
|
|
193
|
+
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
194
|
+
const yn = await new Promise(resolve => {
|
|
195
|
+
rl.once('line', line => { rl.close(); resolve(line.trim().toLowerCase()); });
|
|
196
|
+
});
|
|
197
|
+
if (yn === 'n' || yn === 'no') return null;
|
|
198
|
+
|
|
199
|
+
process.stdout.write('\n Which tools should I configure? (select all that apply)\n\n');
|
|
200
|
+
for (const t of MCP_TOOL_MENU) {
|
|
201
|
+
const fileInfo = t.file ? ` → ${t.file}` : '';
|
|
202
|
+
process.stdout.write(` ${t.num}. ${t.name.padEnd(18)}${fileInfo}\n`);
|
|
203
|
+
}
|
|
204
|
+
process.stdout.write('\n Enter numbers separated by commas (1,2,5) or 11 for all: ');
|
|
205
|
+
|
|
206
|
+
const rl2 = createInterface({ input: process.stdin, output: process.stdout });
|
|
207
|
+
const answer = await new Promise(resolve => {
|
|
208
|
+
rl2.once('line', line => { rl2.close(); resolve(line.trim()); });
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
if (!answer) return 'all';
|
|
212
|
+
const nums = answer.split(',').map(n => parseInt(n.trim(), 10)).filter(n => !isNaN(n));
|
|
213
|
+
if (nums.length === 0 || nums.includes(11)) return 'all';
|
|
214
|
+
const selected = nums.map(n => MCP_TOOL_MENU.find(t => t.num === n)?.target).filter(Boolean);
|
|
215
|
+
return selected.length > 0 ? selected.join(',') : 'all';
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
async function main() {
|
|
219
|
+
switch (command) {
|
|
220
|
+
case 'index': {
|
|
221
|
+
const { skillsPath, cachePath } = resolveBookLibPaths();
|
|
222
|
+
const explicitDir = args[1] && !args[1].startsWith('--') ? args[1] : null;
|
|
223
|
+
const indexer = new BookLibIndexer();
|
|
224
|
+
const verboseIndex = args.includes('--verbose');
|
|
225
|
+
const indexStart = Date.now();
|
|
226
|
+
|
|
227
|
+
process.stdout.write('► Building index...\n');
|
|
228
|
+
if (explicitDir) {
|
|
229
|
+
// Explicit directory: just index that one
|
|
230
|
+
await indexer.indexDirectory(explicitDir, true, { quiet: !verboseIndex });
|
|
231
|
+
} else {
|
|
232
|
+
// Always index bundled skills first (clear on first pass)
|
|
233
|
+
await indexer.indexDirectory(BUNDLED_SKILLS_DIR, true, { quiet: !verboseIndex });
|
|
234
|
+
// Add community/user skills — deduplicate to avoid double-indexing same dir
|
|
235
|
+
const communitySkillsDir = path.join(cachePath, 'skills');
|
|
236
|
+
const dirsToAdd = new Set();
|
|
237
|
+
if (skillsPath !== BUNDLED_SKILLS_DIR) dirsToAdd.add(skillsPath);
|
|
238
|
+
if (communitySkillsDir !== skillsPath) dirsToAdd.add(communitySkillsDir);
|
|
239
|
+
for (const dir of dirsToAdd) {
|
|
240
|
+
if (fs.existsSync(dir) && fs.readdirSync(dir).length > 0) {
|
|
241
|
+
const count = fs.readdirSync(dir).length;
|
|
242
|
+
if (verboseIndex) console.log(`Indexing ${count} community skill(s) from ${dir}...`);
|
|
243
|
+
await indexer.indexDirectory(dir, false, { quiet: !verboseIndex });
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
// Index knowledge nodes from .booklib/knowledge/nodes/
|
|
248
|
+
const { resolveKnowledgePaths } = await import('../lib/engine/graph.js');
|
|
249
|
+
const { nodesDir } = resolveKnowledgePaths();
|
|
250
|
+
await indexer.indexKnowledgeNodes(nodesDir);
|
|
251
|
+
const elapsed = ((Date.now() - indexStart) / 1000).toFixed(0);
|
|
252
|
+
console.log(`✅ Index built in ${elapsed}s`);
|
|
253
|
+
console.log(`\n → Now try: booklib search "your query"\n`);
|
|
254
|
+
break;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
case 'lookup':
|
|
258
|
+
case 'search': {
|
|
259
|
+
const autoFetch = args.includes('--auto-fetch');
|
|
260
|
+
const useGraph = args.includes('--graph');
|
|
261
|
+
const roleFilter = (args.find(a => a.startsWith('--role=')) ?? '').replace('--role=', '') || null;
|
|
262
|
+
const query = args.slice(1).filter(a => !a.startsWith('--')).join(' ');
|
|
263
|
+
if (!query) { console.error('Usage: booklib search "<query>" [--auto-fetch] [--role=<role>] [--graph]'); process.exit(1); }
|
|
264
|
+
|
|
265
|
+
const regSearcher = new BookLibRegistrySearcher();
|
|
266
|
+
let { local, suggested, conflicts } = await regSearcher.searchHybrid(query, { useGraph });
|
|
267
|
+
|
|
268
|
+
// Role filter — narrow results to skills tagged for the requested agent role
|
|
269
|
+
if (roleFilter) {
|
|
270
|
+
const roleMatch = s => !s.roles || s.roles.includes(roleFilter);
|
|
271
|
+
local = local.filter(r => roleMatch(r.metadata ?? r));
|
|
272
|
+
suggested = suggested.filter(roleMatch);
|
|
273
|
+
if (local.length === 0 && suggested.length === 0) {
|
|
274
|
+
console.log(`No results for "${query}" in role "${roleFilter}". Try without --role to see all matches.`);
|
|
275
|
+
break;
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
// Auto-fetch: silently fetch trusted suggestions and re-search
|
|
280
|
+
if (autoFetch && suggested.length > 0) {
|
|
281
|
+
const trustedSuggestions = suggested.filter(s => s.trusted);
|
|
282
|
+
if (trustedSuggestions.length > 0) {
|
|
283
|
+
const fetcher = new SkillFetcher();
|
|
284
|
+
for (const skill of trustedSuggestions) {
|
|
285
|
+
if (!fetcher.isCached(skill)) {
|
|
286
|
+
process.stderr.write(`[booklib] Fetching ${skill.name}...\n`);
|
|
287
|
+
try { await fetcher.fetch(skill); } catch { /* non-fatal */ }
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
// Re-index and re-search with newly fetched skills
|
|
291
|
+
const { cachePath } = resolveBookLibPaths();
|
|
292
|
+
const indexer = new BookLibIndexer();
|
|
293
|
+
await indexer.indexDirectory(path.join(cachePath, 'skills'));
|
|
294
|
+
({ local, suggested, conflicts } = await regSearcher.searchHybrid(query, { useGraph }));
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
if (local.length > 0) {
|
|
299
|
+
console.log('\n📚 Local results:\n');
|
|
300
|
+
local.forEach(r => {
|
|
301
|
+
const rationale = r._rationale ? ` ↳ ${r._rationale}` : '';
|
|
302
|
+
const isNode = r.metadata?.nodeKind === 'knowledge';
|
|
303
|
+
const label = isNode
|
|
304
|
+
? `📝 ${r.metadata?.title ?? r.metadata?.filePath ?? '?'} [${r.metadata?.type ?? 'note'}]`
|
|
305
|
+
: `📚 ${r.metadata?.name ?? r.metadata?.filePath ?? '?'} (${r.metadata?.type ?? 'chunk'})`;
|
|
306
|
+
const s = r.score ?? 0;
|
|
307
|
+
const bar = s >= 0.7 ? '████' : s >= 0.5 ? '███░' : s >= 0.35 ? '██░░' : '█░░░';
|
|
308
|
+
console.log(` ${bar} ${label}`);
|
|
309
|
+
if (rationale) console.log(rationale);
|
|
310
|
+
});
|
|
311
|
+
}
|
|
312
|
+
if (suggested.length > 0) {
|
|
313
|
+
console.log('\n💡 Community skills available (not yet indexed):');
|
|
314
|
+
suggested.forEach(s => {
|
|
315
|
+
const stars = s.stars ? ` ★${s.stars.toLocaleString()}` : '';
|
|
316
|
+
const rationale = s._rationale ? `\n ↳ ${s._rationale}` : '';
|
|
317
|
+
console.log(` • ${s.name}${stars} — ${s.description}${rationale}`);
|
|
318
|
+
});
|
|
319
|
+
if (!autoFetch) console.log('\nTip: run with --auto-fetch to install and search in one step');
|
|
320
|
+
}
|
|
321
|
+
if (local.length === 0 && suggested.length === 0) {
|
|
322
|
+
console.log('No results found.');
|
|
323
|
+
}
|
|
324
|
+
break;
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
case 'review':
|
|
328
|
+
case 'audit': {
|
|
329
|
+
const auditor = new BookLibAuditor();
|
|
330
|
+
const skillName = args[1];
|
|
331
|
+
const filePath = args[2];
|
|
332
|
+
if (!skillName || !filePath) { console.error('Usage: booklib audit <skill-name> <file-path>'); process.exit(1); }
|
|
333
|
+
const { skillsPath } = resolveBookLibPaths();
|
|
334
|
+
const candidates = [
|
|
335
|
+
path.join(skillsPath, skillName),
|
|
336
|
+
path.join(BUNDLED_SKILLS_DIR, skillName),
|
|
337
|
+
];
|
|
338
|
+
const skillPath = candidates.find(p => fs.existsSync(p)) ?? candidates[0];
|
|
339
|
+
if (!fs.existsSync(skillPath)) {
|
|
340
|
+
const available = fs.readdirSync(BUNDLED_SKILLS_DIR)
|
|
341
|
+
.filter(d => fs.statSync(path.join(BUNDLED_SKILLS_DIR, d)).isDirectory())
|
|
342
|
+
.sort();
|
|
343
|
+
console.error(` Unknown skill: '${skillName}'`);
|
|
344
|
+
console.error(` Available: ${available.join(', ')}`);
|
|
345
|
+
process.exit(1);
|
|
346
|
+
}
|
|
347
|
+
const report = await auditor.audit(skillPath, filePath);
|
|
348
|
+
const divider = '─'.repeat(60);
|
|
349
|
+
console.log(`\n► Audit prompt — paste into Claude, ChatGPT, or your AI assistant:\n${divider}\n`);
|
|
350
|
+
console.log(report);
|
|
351
|
+
console.log(`\n${divider}`);
|
|
352
|
+
console.log(`Tip: pipe to clipboard → booklib audit ${skillName} ${filePath} | pbcopy (mac)`);
|
|
353
|
+
console.log(` → booklib audit ${skillName} ${filePath} | xclip (linux)\n`);
|
|
354
|
+
break;
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
case 'scan': {
|
|
358
|
+
const scanner = new BookLibScanner();
|
|
359
|
+
const docsMode = args.includes('--docs');
|
|
360
|
+
const scanDir = args.filter(a => !a.startsWith('--'))[1] || process.cwd();
|
|
361
|
+
const report = await scanner.scan(scanDir, { mode: docsMode ? 'docs' : 'code' });
|
|
362
|
+
console.log(report);
|
|
363
|
+
break;
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
case 'context': {
|
|
367
|
+
const promptOnly = args.includes('--prompt-only');
|
|
368
|
+
const task = args.slice(1).filter(a => !a.startsWith('--')).join(' ');
|
|
369
|
+
if (!task) {
|
|
370
|
+
console.error('Usage: booklib context "<task description>" [--prompt-only] [--file=<path>] [--no-graph]');
|
|
371
|
+
console.error('Example: booklib context "implement a payment service in Kotlin with async error handling"');
|
|
372
|
+
process.exit(1);
|
|
373
|
+
}
|
|
374
|
+
const builder = new ContextBuilder();
|
|
375
|
+
const useGraph = !args.includes('--no-graph') && !promptOnly;
|
|
376
|
+
const fileArg = parseFlag(args, 'file');
|
|
377
|
+
const result = useGraph
|
|
378
|
+
? await builder.buildWithGraph(task, fileArg)
|
|
379
|
+
: await builder.build(task, { promptOnly });
|
|
380
|
+
console.log(result);
|
|
381
|
+
|
|
382
|
+
if (fileArg && useGraph && !result.includes('## Knowledge Graph Context')) {
|
|
383
|
+
process.stderr.write(
|
|
384
|
+
`\nTip: no component is mapped to "${fileArg}".\n` +
|
|
385
|
+
` To enable graph context injection: booklib component add <name> "<glob>"\n` +
|
|
386
|
+
` Example: booklib component add auth "src/auth/**"\n`
|
|
387
|
+
);
|
|
388
|
+
}
|
|
389
|
+
break;
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
case 'save-state': {
|
|
393
|
+
const handoff = new BookLibHandoff();
|
|
394
|
+
const parsed = {};
|
|
395
|
+
const stateArgs = args.slice(1);
|
|
396
|
+
for (let i = 0; i < stateArgs.length; i++) {
|
|
397
|
+
const a = stateArgs[i];
|
|
398
|
+
if (!a.startsWith('--')) continue;
|
|
399
|
+
const stripped = a.replace(/^--/, '');
|
|
400
|
+
if (stripped.includes('=')) {
|
|
401
|
+
const [k, ...v] = stripped.split('=');
|
|
402
|
+
parsed[k] = v.join('=');
|
|
403
|
+
} else if (i + 1 < stateArgs.length && !stateArgs[i + 1].startsWith('--')) {
|
|
404
|
+
parsed[stripped] = stateArgs[++i];
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
handoff.saveState(parsed);
|
|
408
|
+
break;
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
case 'resume': {
|
|
412
|
+
const handoff = new BookLibHandoff();
|
|
413
|
+
console.log(handoff.resume(args[1]));
|
|
414
|
+
break;
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
case 'recover-auto': {
|
|
418
|
+
const handoff = new BookLibHandoff();
|
|
419
|
+
console.log(handoff.recoverFromSessionOrGit());
|
|
420
|
+
break;
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
case 'sessions': {
|
|
424
|
+
const mgr = new BookLibSessionManager(process.cwd());
|
|
425
|
+
const subCmd = args[1];
|
|
426
|
+
|
|
427
|
+
if (subCmd === 'cleanup') {
|
|
428
|
+
const beforeDays = parseInt(args[2]?.split('=')[1]) || 90;
|
|
429
|
+
const result = mgr.cleanupSessions({ beforeDays, archive: true });
|
|
430
|
+
console.log(`✅ Archived ${result.archived} sessions, deleted ${result.deleted}`);
|
|
431
|
+
console.log(`Preview: ${JSON.stringify(result.preview.slice(0, 3), null, 2)}`);
|
|
432
|
+
} else if (subCmd === 'diff') {
|
|
433
|
+
const diff = mgr.diffSessions(args[2], args[3]);
|
|
434
|
+
if (diff.error) console.error(diff.error);
|
|
435
|
+
else {
|
|
436
|
+
console.log(`\n📊 Comparing: ${diff.session1} vs ${diff.session2}`);
|
|
437
|
+
console.log(`\nGoal Changed: ${diff.goal.changed}`);
|
|
438
|
+
console.log(` ${diff.session1}: ${diff.goal.s1}`);
|
|
439
|
+
console.log(` ${diff.session2}: ${diff.goal.s2}`);
|
|
440
|
+
console.log(`\nConflicting Tasks: ${diff.tasks.conflicts.length}`);
|
|
441
|
+
diff.tasks.conflicts.forEach(t => console.log(` ⚠️ ${t}`));
|
|
442
|
+
console.log(`\nNew Skills: ${diff.skills.added.join(', ') || 'none'}`);
|
|
443
|
+
}
|
|
444
|
+
} else if (subCmd === 'find') {
|
|
445
|
+
const result = mgr.findSession(args[2], { searchGlobal: true });
|
|
446
|
+
if (result) {
|
|
447
|
+
console.log(`✅ Found: ${result.path} (${result.scope})`);
|
|
448
|
+
} else {
|
|
449
|
+
console.log(`❌ Session not found: ${args[2]}`);
|
|
450
|
+
}
|
|
451
|
+
} else if (subCmd === 'search') {
|
|
452
|
+
const results = mgr.searchSessions(args[2]);
|
|
453
|
+
if (results.length === 0) {
|
|
454
|
+
console.log(`No sessions found matching: ${args[2]}`);
|
|
455
|
+
} else {
|
|
456
|
+
console.log(`\n🔍 Found ${results.length} session(s):`);
|
|
457
|
+
results.forEach(r => {
|
|
458
|
+
console.log(`\n 📝 ${r.name}`);
|
|
459
|
+
console.log(` Goal: ${r.goal}`);
|
|
460
|
+
console.log(` Tags: ${r.tags.join(', ') || 'none'}`);
|
|
461
|
+
});
|
|
462
|
+
}
|
|
463
|
+
} else if (subCmd === 'tag') {
|
|
464
|
+
const sessionId = args[2];
|
|
465
|
+
const tagArg = args.find(a => a.startsWith('--add='));
|
|
466
|
+
if (!tagArg) {
|
|
467
|
+
console.error('Usage: booklib sessions tag <id> --add=tag1,tag2');
|
|
468
|
+
process.exit(1);
|
|
469
|
+
}
|
|
470
|
+
const tags = tagArg.split('=')[1].split(',');
|
|
471
|
+
const result = mgr.tagSession(sessionId, tags, 'add');
|
|
472
|
+
console.log(`✅ Tagged: ${result.session}`);
|
|
473
|
+
console.log(` Tags: ${result.tags.join(', ')}`);
|
|
474
|
+
} else if (subCmd === 'validate') {
|
|
475
|
+
const result = mgr.validateSession(args[2]);
|
|
476
|
+
console.log(`\n${result.valid ? '✅' : '⚠️'} Validation Result:`);
|
|
477
|
+
if (result.errors.length > 0) {
|
|
478
|
+
console.log('Errors:');
|
|
479
|
+
result.errors.forEach(e => console.log(` ❌ ${e}`));
|
|
480
|
+
}
|
|
481
|
+
if (result.warnings.length > 0) {
|
|
482
|
+
console.log('Warnings:');
|
|
483
|
+
result.warnings.forEach(w => console.log(` ⚠️ ${w}`));
|
|
484
|
+
}
|
|
485
|
+
console.log(`Score: ${result.score}/100`);
|
|
486
|
+
} else if (subCmd === 'create') {
|
|
487
|
+
const templateArg = args.find(a => a.startsWith('--template='));
|
|
488
|
+
const template = templateArg?.split('=')[1];
|
|
489
|
+
const sessionName = args[3];
|
|
490
|
+
if (!template || !sessionName) {
|
|
491
|
+
console.error('Usage: booklib sessions create --template=<type> <name>');
|
|
492
|
+
process.exit(1);
|
|
493
|
+
}
|
|
494
|
+
const result = mgr.createFromTemplate(template, sessionName);
|
|
495
|
+
if (result.error) console.error(result.error);
|
|
496
|
+
else console.log(`✅ Created session from template: ${result.created}`);
|
|
497
|
+
} else if (subCmd === 'report') {
|
|
498
|
+
const sinceArg = args.find(a => a.startsWith('--since='));
|
|
499
|
+
const since = sinceArg?.split('=')[1];
|
|
500
|
+
const stats = mgr.generateReport({ since });
|
|
501
|
+
console.log(`\n📊 Session Report`);
|
|
502
|
+
console.log(`Total sessions: ${stats.total_sessions}`);
|
|
503
|
+
console.log(`Pending tasks: ${stats.total_tasks}`);
|
|
504
|
+
console.log(`Active skills: ${stats.unique_skills}`);
|
|
505
|
+
console.log(`\nTop Skills: ${stats.unique_skills.slice(0, 3).join(', ')}`);
|
|
506
|
+
console.log(`\nRecent Activity:`);
|
|
507
|
+
stats.recent_activity.forEach(a => {
|
|
508
|
+
console.log(` 📝 ${a.name}: ${a.goal} (${new Date(a.timestamp).toLocaleDateString()})`);
|
|
509
|
+
});
|
|
510
|
+
} else if (subCmd === 'history') {
|
|
511
|
+
const history = mgr.getVersionHistory(args[2]);
|
|
512
|
+
console.log(`\n📜 Version History: ${args[2]}`);
|
|
513
|
+
console.log(`Total versions: ${history.length}`);
|
|
514
|
+
history.slice(0, 5).forEach(v => {
|
|
515
|
+
console.log(` Version ${v.version}: ${v.timestamp}`);
|
|
516
|
+
});
|
|
517
|
+
}
|
|
518
|
+
break;
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
case 'sessions-list': {
|
|
522
|
+
const coord = new BookLibSessionCoordinator();
|
|
523
|
+
const sessions = coord.listAllSessions();
|
|
524
|
+
if (sessions.length === 0) { console.log('No sessions found.'); break; }
|
|
525
|
+
sessions.forEach(s => console.log(` 📝 ${s.id} — ${s.goal} [${s.branch}]`));
|
|
526
|
+
break;
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
case 'sessions-merge': {
|
|
530
|
+
const coord = new BookLibSessionCoordinator();
|
|
531
|
+
const ids = args[1]?.split(',');
|
|
532
|
+
const output = args[2];
|
|
533
|
+
if (!ids || !output) { console.error('Usage: booklib sessions-merge <id1,id2,...> <output-name>'); process.exit(1); }
|
|
534
|
+
const result = coord.mergeSessions(ids, output);
|
|
535
|
+
console.log(result.message || `✅ Merged into: ${output}`);
|
|
536
|
+
break;
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
case 'sessions-lineage': {
|
|
540
|
+
const coord = new BookLibSessionCoordinator();
|
|
541
|
+
if (args[1] && args[2]) {
|
|
542
|
+
coord.trackLineage(args[1], args[2], args[3] || '');
|
|
543
|
+
console.log(`✅ Lineage tracked: ${args[1]} → ${args[2]}`);
|
|
544
|
+
} else {
|
|
545
|
+
console.log(coord.displayLineageTree());
|
|
546
|
+
}
|
|
547
|
+
break;
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
case 'sessions-compare': {
|
|
551
|
+
const coord = new BookLibSessionCoordinator();
|
|
552
|
+
const ids = args[1]?.split(',');
|
|
553
|
+
const targetFile = args[2];
|
|
554
|
+
const output = args[3];
|
|
555
|
+
if (!ids || !targetFile || !output) { console.error('Usage: booklib sessions-compare <id1,id2,...> <file> <output-name>'); process.exit(1); }
|
|
556
|
+
const result = coord.compareAudits(ids, targetFile, output);
|
|
557
|
+
console.log(result.message || `✅ Comparison saved: ${output}`);
|
|
558
|
+
break;
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
|
|
562
|
+
|
|
563
|
+
case 'init': {
|
|
564
|
+
// Backwards-compat: if legacy flags are passed, run old init flow
|
|
565
|
+
const hasLegacyFlags = args.some(a =>
|
|
566
|
+
a.startsWith('--tool=') || a.startsWith('--skills=') ||
|
|
567
|
+
a.includes('--dry-run') || a.includes('--ecc')
|
|
568
|
+
);
|
|
569
|
+
|
|
570
|
+
if (hasLegacyFlags) {
|
|
571
|
+
// ── Legacy init path ─────────────────────────────────────────────────
|
|
572
|
+
const orchestratorArg = args.find(a => a.startsWith('--orchestrator='))?.split('=')[1] ?? null;
|
|
573
|
+
const dryRun = args.includes('--dry-run');
|
|
574
|
+
const hasToolFlag = args.some(a => a.startsWith('--tool='));
|
|
575
|
+
const targetFlag = args.find(a => a.startsWith('--target='))?.split('=')[1] ?? null;
|
|
576
|
+
let targetArg;
|
|
577
|
+
if (hasToolFlag) {
|
|
578
|
+
targetArg = args.find(a => a.startsWith('--tool='))?.split('=')[1];
|
|
579
|
+
} else if (targetFlag) {
|
|
580
|
+
targetArg = targetFlag;
|
|
581
|
+
} else if (!dryRun) {
|
|
582
|
+
const { configPath } = resolveBookLibPaths();
|
|
583
|
+
let savedConfig = {};
|
|
584
|
+
try { savedConfig = JSON.parse(fs.readFileSync(configPath, 'utf8')); } catch { /* no config yet */ }
|
|
585
|
+
if (savedConfig.tools?.length) {
|
|
586
|
+
targetArg = savedConfig.tools.join(',');
|
|
587
|
+
console.log(`Using saved tool selection: ${targetArg} (pass --tool=X to override)\n`);
|
|
588
|
+
} else {
|
|
589
|
+
targetArg = await promptToolSelection();
|
|
590
|
+
const updatedConfig = { ...savedConfig, tools: targetArg === 'all'
|
|
591
|
+
? ['claude', 'cursor', 'copilot', 'gemini', 'codex', 'windsurf', 'roo-code', 'openhands', 'junie', 'goose', 'opencode', 'letta']
|
|
592
|
+
: targetArg.split(',') };
|
|
593
|
+
try { fs.writeFileSync(configPath, JSON.stringify(updatedConfig, null, 2)); } catch { /* best-effort */ }
|
|
594
|
+
}
|
|
595
|
+
} else {
|
|
596
|
+
targetArg = 'auto';
|
|
597
|
+
}
|
|
598
|
+
const skillsArg = args.find(a => a.startsWith('--skills='))?.split('=')[1];
|
|
599
|
+
const rulesArg = args.find(a => a.startsWith('--rules='))?.split('=')[1];
|
|
600
|
+
const pullEcc = args.includes('--ecc');
|
|
601
|
+
const includeAgents = pullEcc || args.includes('--agents');
|
|
602
|
+
const includeCommands = pullEcc || args.includes('--commands');
|
|
603
|
+
const includeRules = pullEcc || args.includes('--rules') || rulesArg != null;
|
|
604
|
+
const skillList = skillsArg?.split(',').map(s => s.trim());
|
|
605
|
+
const langList = rulesArg ? rulesArg.split(',').map(s => s.trim()) : (includeRules ? null : false);
|
|
606
|
+
const initializer = new ProjectInitializer();
|
|
607
|
+
|
|
608
|
+
if (!skillList) {
|
|
609
|
+
const detected = initializer.detectRelevantSkills();
|
|
610
|
+
if (detected.length === 0 && !includeAgents && !includeCommands && !includeRules) {
|
|
611
|
+
console.log('No skills auto-detected. Specify with --skills=skill1,skill2 or use --ecc to pull agents/commands/rules.');
|
|
612
|
+
process.exit(1);
|
|
613
|
+
}
|
|
614
|
+
if (detected.length > 0) console.log(`Auto-detected skills: ${detected.join(', ')}\n`);
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
if (hasToolFlag && !dryRun) {
|
|
618
|
+
const { configPath } = resolveBookLibPaths();
|
|
619
|
+
let savedConfig = {};
|
|
620
|
+
try { savedConfig = JSON.parse(fs.readFileSync(configPath, 'utf8')); } catch { /* no config yet */ }
|
|
621
|
+
const toolList = targetArg === 'all'
|
|
622
|
+
? ['claude', 'cursor', 'copilot', 'gemini', 'codex', 'windsurf', 'roo-code', 'openhands', 'junie', 'goose', 'opencode', 'letta']
|
|
623
|
+
: targetArg.split(',');
|
|
624
|
+
try { fs.writeFileSync(configPath, JSON.stringify({ ...savedConfig, tools: toolList }, null, 2)); } catch { /* best-effort */ }
|
|
625
|
+
}
|
|
626
|
+
|
|
627
|
+
if (skillList || initializer.detectRelevantSkills().length > 0) {
|
|
628
|
+
console.log(`Generating context files for: ${targetArg === 'all' ? 'claude, cursor, copilot, gemini, codex, windsurf, roo-code, openhands, junie, goose, opencode, letta' : targetArg}\n`);
|
|
629
|
+
const effectiveSkills = skillList ?? initializer.detectRelevantSkills();
|
|
630
|
+
// Load saved config for profile-based rendering (Spec ⑨)
|
|
631
|
+
const { configPath: legacyConfigPath } = resolveBookLibPaths();
|
|
632
|
+
let legacySavedConfig = {};
|
|
633
|
+
try { legacySavedConfig = JSON.parse(fs.readFileSync(legacyConfigPath, 'utf8')); } catch { /* no config yet */ }
|
|
634
|
+
const written = await initializer.init({
|
|
635
|
+
skills: effectiveSkills,
|
|
636
|
+
target: targetArg,
|
|
637
|
+
dryRun,
|
|
638
|
+
profile: legacySavedConfig.profile ?? 'software-development',
|
|
639
|
+
stack: legacySavedConfig.stack ?? effectiveSkills.join(', '),
|
|
640
|
+
});
|
|
641
|
+
if (!dryRun && written.length > 0) console.log('');
|
|
642
|
+
|
|
643
|
+
// Discovery hint: suggest related skills
|
|
644
|
+
const related = initializer.suggestRelatedSkills(effectiveSkills, process.cwd());
|
|
645
|
+
if (related.length > 0) {
|
|
646
|
+
console.log(' \u{1F4A1} Also consider for your stack:');
|
|
647
|
+
related.forEach(s => console.log(` booklib init --skills=${effectiveSkills.join(',')},${s}`));
|
|
648
|
+
}
|
|
649
|
+
}
|
|
650
|
+
|
|
651
|
+
if (includeAgents || includeCommands || includeRules) {
|
|
652
|
+
const pulling = [];
|
|
653
|
+
if (includeRules) pulling.push(langList ? `rules (${langList.join(',')})` : 'rules (all languages)');
|
|
654
|
+
if (includeAgents) pulling.push('agents → .claude/agents/');
|
|
655
|
+
if (includeCommands) pulling.push('commands → .claude/commands/');
|
|
656
|
+
console.log(`Pulling ECC artifacts: ${pulling.join(', ')}\n`);
|
|
657
|
+
try {
|
|
658
|
+
const eccWritten = await initializer.fetchEccArtifacts({ languages: langList, includeAgents, includeCommands, dryRun });
|
|
659
|
+
if (!dryRun && eccWritten.length > 0) console.log(`\nPulled ${eccWritten.length} artifact(s) from ECC.`);
|
|
660
|
+
} catch (err) {
|
|
661
|
+
console.error(`ECC fetch failed: ${err.message}`);
|
|
662
|
+
}
|
|
663
|
+
}
|
|
664
|
+
break;
|
|
665
|
+
}
|
|
666
|
+
|
|
667
|
+
// ── New guided wizard ─────────────────────────────────────────────────
|
|
668
|
+
const reset = args.includes('--reset');
|
|
669
|
+
await runWizard(process.cwd(), { reset });
|
|
670
|
+
break;
|
|
671
|
+
}
|
|
672
|
+
|
|
673
|
+
case 'setup': {
|
|
674
|
+
const engine = new DiscoveryEngine();
|
|
675
|
+
const fetcher = new SkillFetcher();
|
|
676
|
+
console.log('Discovering skills...');
|
|
677
|
+
const skills = await engine.refresh();
|
|
678
|
+
const trusted = skills.filter(s => s.trusted);
|
|
679
|
+
const untrusted = skills.filter(s => !s.trusted);
|
|
680
|
+
if (trusted.length === 0) {
|
|
681
|
+
console.log('No trusted skills found. Check your booklib.config.json sources.');
|
|
682
|
+
break;
|
|
683
|
+
}
|
|
684
|
+
console.log(`Found ${trusted.length} trusted skill(s) to install, ${untrusted.length} require confirmation.\n`);
|
|
685
|
+
let installed = 0;
|
|
686
|
+
for (const skill of trusted) {
|
|
687
|
+
if (fetcher.isCached(skill)) {
|
|
688
|
+
console.log(` ✓ ${skill.name} (already installed)`);
|
|
689
|
+
continue;
|
|
690
|
+
}
|
|
691
|
+
process.stdout.write(` ↓ Fetching ${skill.name}...`);
|
|
692
|
+
try {
|
|
693
|
+
await fetcher.fetch(skill);
|
|
694
|
+
console.log(' done');
|
|
695
|
+
installed++;
|
|
696
|
+
} catch (err) {
|
|
697
|
+
console.log(` failed: ${err.message}`);
|
|
698
|
+
}
|
|
699
|
+
}
|
|
700
|
+
if (installed > 0) {
|
|
701
|
+
console.log(`\nRe-indexing...`);
|
|
702
|
+
const { skillsPath, cachePath } = resolveBookLibPaths();
|
|
703
|
+
const indexer = new BookLibIndexer();
|
|
704
|
+
await indexer.indexDirectory(skillsPath);
|
|
705
|
+
await indexer.indexDirectory(path.join(cachePath, 'skills'));
|
|
706
|
+
}
|
|
707
|
+
console.log('\n✅ Setup complete. Run: booklib search "<query>"');
|
|
708
|
+
console.log(' Skills synced to ~/.claude/skills/ — pair with an orchestrator if needed:');
|
|
709
|
+
console.log(' obra/superpowers: /plugin install superpowers ruflo: npm install -g ruflo');
|
|
710
|
+
if (untrusted.length > 0) {
|
|
711
|
+
console.log(`\nTo install remaining skills, run: booklib install <skill-name>`);
|
|
712
|
+
untrusted.forEach(s => console.log(` • ${s.name}`));
|
|
713
|
+
}
|
|
714
|
+
break;
|
|
715
|
+
}
|
|
716
|
+
|
|
717
|
+
case 'add': {
|
|
718
|
+
console.error('⚠ "booklib add" is deprecated. Use: booklib install <name>');
|
|
719
|
+
const installer = new BookLibInstaller();
|
|
720
|
+
const skillId = args[1];
|
|
721
|
+
if (!skillId) { console.error('Usage: booklib add <skill-id-or-url>'); process.exit(1); }
|
|
722
|
+
await installer.add(skillId);
|
|
723
|
+
break;
|
|
724
|
+
}
|
|
725
|
+
|
|
726
|
+
case 'install': {
|
|
727
|
+
const names = args.slice(1).filter(a => !a.startsWith('--'));
|
|
728
|
+
if (names.length === 0) {
|
|
729
|
+
console.error('Usage: booklib install <skill-name> [skill-name...]');
|
|
730
|
+
process.exit(1);
|
|
731
|
+
}
|
|
732
|
+
const { installSkill } = await import('../lib/skill-fetcher.js');
|
|
733
|
+
for (const name of names) {
|
|
734
|
+
const result = installSkill(name);
|
|
735
|
+
if (result === 'installed') console.log(` ✓ ${name}`);
|
|
736
|
+
else if (result === 'already-installed') console.log(` · ${name} (already installed)`);
|
|
737
|
+
else console.log(` ✗ ${name}: not found in any catalog`);
|
|
738
|
+
}
|
|
739
|
+
break;
|
|
740
|
+
}
|
|
741
|
+
|
|
742
|
+
case 'fetch': {
|
|
743
|
+
console.error('⚠ "booklib fetch" is deprecated. Use: booklib install <name>');
|
|
744
|
+
const { SKILL_REGISTRY } = await import('../lib/registry/skills.js');
|
|
745
|
+
const skillName = args[1];
|
|
746
|
+
if (!skillName) { console.error('Usage: booklib fetch <skill-name>'); process.exit(1); }
|
|
747
|
+
const skill = SKILL_REGISTRY.find(s => s.name === skillName || s.name.endsWith(`/${skillName}`));
|
|
748
|
+
if (!skill) { console.error(`Skill not found in registry: ${skillName}`); process.exit(1); }
|
|
749
|
+
const fetcher = new SkillFetcher();
|
|
750
|
+
try {
|
|
751
|
+
await fetcher.fetch(skill, {
|
|
752
|
+
onPrompt: async (s) => {
|
|
753
|
+
process.stdout.write(`Index "${s.name}" from ${s.source.type} (untrusted)? [y/N] `);
|
|
754
|
+
const answer = await new Promise(r => {
|
|
755
|
+
process.stdin.once('data', d => r(d.toString().trim().toLowerCase()));
|
|
756
|
+
});
|
|
757
|
+
return answer === 'y' || answer === 'yes';
|
|
758
|
+
},
|
|
759
|
+
});
|
|
760
|
+
} catch (err) {
|
|
761
|
+
if (err instanceof RequiresConfirmationError) {
|
|
762
|
+
console.error(err.message);
|
|
763
|
+
process.exit(1);
|
|
764
|
+
}
|
|
765
|
+
throw err;
|
|
766
|
+
}
|
|
767
|
+
// Slot limit warning
|
|
768
|
+
const slotCount = countInstalledSlots();
|
|
769
|
+
if (slotCount >= SKILL_LIMIT) {
|
|
770
|
+
console.log(`\n ⚠ You now have ${slotCount}/${SKILL_LIMIT} skill slots used.`);
|
|
771
|
+
console.log(' Claude may truncate skill descriptions. Run "booklib doctor" to clean up.');
|
|
772
|
+
} else if (slotCount >= SKILL_LIMIT - 4) {
|
|
773
|
+
console.log(`\n ⚠ ${slotCount}/${SKILL_LIMIT} slots used — approaching limit.`);
|
|
774
|
+
console.log(' Run "booklib doctor" to review installed skills.');
|
|
775
|
+
}
|
|
776
|
+
break;
|
|
777
|
+
}
|
|
778
|
+
|
|
779
|
+
case 'sync': {
|
|
780
|
+
// Retroactively sync all already-fetched BookLib skills to ~/.claude/skills/
|
|
781
|
+
const { cachePath } = resolveBookLibPaths();
|
|
782
|
+
const skillsDir = path.join(cachePath, 'skills');
|
|
783
|
+
if (!fs.existsSync(skillsDir)) { console.log('No fetched skills found. Run: booklib setup'); break; }
|
|
784
|
+
const fetcher = new SkillFetcher();
|
|
785
|
+
const dirs = fs.readdirSync(skillsDir).filter(d => fs.existsSync(path.join(skillsDir, d, 'SKILL.md')));
|
|
786
|
+
let synced = 0;
|
|
787
|
+
for (const d of dirs) {
|
|
788
|
+
const skillFile = path.join(skillsDir, d, 'SKILL.md');
|
|
789
|
+
const head = fs.readFileSync(skillFile, 'utf8').split('\n').slice(0, 15).join('\n');
|
|
790
|
+
const nameMatch = head.match(/^name:\s*["']?(.+?)["']?\s*$/m);
|
|
791
|
+
const descMatch = head.match(/^description:\s*(.+)$/m);
|
|
792
|
+
const name = nameMatch ? nameMatch[1].trim() : d;
|
|
793
|
+
const description = descMatch ? descMatch[1].trim().replace(/^["']|["']$/g, '') : '';
|
|
794
|
+
fetcher._syncToClaudeSkills({ name, description }, path.join(skillsDir, d));
|
|
795
|
+
synced++;
|
|
796
|
+
}
|
|
797
|
+
console.log(`Synced ${synced} skills to ~/.claude/skills/ — available via Claude Code Skill tool`);
|
|
798
|
+
console.log(` Pair with an orchestrator: /plugin install superpowers (obra) · npm install -g ruflo (ruflo)`);
|
|
799
|
+
break;
|
|
800
|
+
}
|
|
801
|
+
|
|
802
|
+
case 'discover': {
|
|
803
|
+
const engine = new DiscoveryEngine();
|
|
804
|
+
const flag = args[1];
|
|
805
|
+
if (flag === '--refresh') {
|
|
806
|
+
console.log('Refreshing discovery cache...');
|
|
807
|
+
const skills = await engine.refresh();
|
|
808
|
+
console.log(`Found ${skills.length} skills from external sources.`);
|
|
809
|
+
skills.forEach(s => {
|
|
810
|
+
const stars = s.stars ? ` ★${s.stars.toLocaleString()}` : '';
|
|
811
|
+
const trust = s.trusted ? '' : ' (requires confirmation)';
|
|
812
|
+
console.log(` • ${s.name}${stars} [${s.source.type}]${trust} — ${s.description}`);
|
|
813
|
+
});
|
|
814
|
+
} else {
|
|
815
|
+
const skills = await engine.discover();
|
|
816
|
+
if (skills.length === 0) {
|
|
817
|
+
console.log('No external sources configured. Add sources to booklib.config.json.');
|
|
818
|
+
} else {
|
|
819
|
+
console.log(`Discovered ${skills.length} skills:`);
|
|
820
|
+
skills.forEach(s => {
|
|
821
|
+
const stars = s.stars ? ` ★${s.stars.toLocaleString()}` : '';
|
|
822
|
+
const trust = s.trusted ? '' : ' (requires confirmation)';
|
|
823
|
+
console.log(` • ${s.name}${stars} [${s.source.type}]${trust} — ${s.description}`);
|
|
824
|
+
});
|
|
825
|
+
}
|
|
826
|
+
}
|
|
827
|
+
break;
|
|
828
|
+
}
|
|
829
|
+
|
|
830
|
+
case 'profile': {
|
|
831
|
+
const role = args[1];
|
|
832
|
+
const ALL_ROLES = ['architect', 'coder', 'reviewer', 'tester', 'security', 'frontend', 'optimizer', 'devops', 'ai-engineer', 'manager', 'product', 'legal', 'writer', 'strategist', 'designer'];
|
|
833
|
+
if (!role || role === '--list') {
|
|
834
|
+
console.log('\nAvailable agent roles:\n');
|
|
835
|
+
ALL_ROLES.forEach(r => console.log(` • ${r}`));
|
|
836
|
+
console.log('\nUsage: booklib profile <role>\n');
|
|
837
|
+
break;
|
|
838
|
+
}
|
|
839
|
+
|
|
840
|
+
const engine = new DiscoveryEngine();
|
|
841
|
+
const all = await engine.discover();
|
|
842
|
+
// Merge with registry for role metadata
|
|
843
|
+
const { skills: regSkills } = JSON.parse(
|
|
844
|
+
(await import('fs')).default.readFileSync(
|
|
845
|
+
(await import('path')).default.join(
|
|
846
|
+
(await import('url')).default.fileURLToPath(new URL('.', import.meta.url)),
|
|
847
|
+
'../community/registry.json'
|
|
848
|
+
), 'utf8'
|
|
849
|
+
)
|
|
850
|
+
);
|
|
851
|
+
const roleMap = new Map(regSkills.map(s => [s.name, s.roles ?? []]));
|
|
852
|
+
|
|
853
|
+
const matches = all.filter(s => {
|
|
854
|
+
const roles = roleMap.get(s.name) ?? s.roles ?? [];
|
|
855
|
+
return roles.includes(role);
|
|
856
|
+
});
|
|
857
|
+
|
|
858
|
+
if (matches.length === 0) {
|
|
859
|
+
console.log(`No skills found for role "${role}". Try: booklib profile --list`);
|
|
860
|
+
break;
|
|
861
|
+
}
|
|
862
|
+
|
|
863
|
+
console.log(`\n🤖 Skill profile for agent role: ${role}\n`);
|
|
864
|
+
console.log(` ${matches.length} skills pre-selected from ${all.length} available\n`);
|
|
865
|
+
matches.forEach(s => {
|
|
866
|
+
const stars = s.stars ? ` ★${s.stars.toLocaleString()}` : '';
|
|
867
|
+
console.log(` • ${s.name}${stars}`);
|
|
868
|
+
if (s.description) console.log(` ${s.description.slice(0, 100)}`);
|
|
869
|
+
});
|
|
870
|
+
console.log(`\nTo load all: booklib setup (then each skill is available to inject)`);
|
|
871
|
+
console.log(`To search within role: booklib search "<query>" --role=${role}\n`);
|
|
872
|
+
break;
|
|
873
|
+
}
|
|
874
|
+
|
|
875
|
+
case 'swarm-config': {
|
|
876
|
+
const trigger = args[1];
|
|
877
|
+
|
|
878
|
+
// Trigger → roles → skill domains mapping (extends ruflo's worker-integration concept)
|
|
879
|
+
const SWARM_TRIGGERS = {
|
|
880
|
+
audit: { roles: ['security', 'tester'], phases: ['security-scan', 'coverage', 'vulnerability-check'] },
|
|
881
|
+
refactor: { roles: ['coder', 'reviewer'], phases: ['complexity', 'naming', 'patterns', 'solid'] },
|
|
882
|
+
architect: { roles: ['architect'], phases: ['system-design', 'ddd', 'api-design'] },
|
|
883
|
+
frontend: { roles: ['frontend', 'tester'], phases: ['components', 'state', 'performance', 'a11y'] },
|
|
884
|
+
release: { roles: ['devops', 'security'], phases: ['docker', 'secrets', 'headers', 'changelog'] },
|
|
885
|
+
research: { roles: ['ai-engineer', 'architect'], phases: ['prompt-design', 'rag', 'reliability'] },
|
|
886
|
+
manage: { roles: ['manager'], phases: ['leadership', 'retro', 'process'] },
|
|
887
|
+
product: { roles: ['product', 'writer'], phases: ['requirements', 'user-stories', 'prioritization'] },
|
|
888
|
+
legal: { roles: ['legal'], phases: ['contract-review', 'risk-assessment', 'compliance'] },
|
|
889
|
+
write: { roles: ['writer'], phases: ['outline', 'draft', 'edit', 'review'] },
|
|
890
|
+
strategy: { roles: ['strategist', 'product'], phases: ['discovery', 'positioning', 'roadmap'] },
|
|
891
|
+
design: { roles: ['designer', 'frontend'], phases: ['visual-hierarchy', 'typography', 'brand'] },
|
|
892
|
+
};
|
|
893
|
+
|
|
894
|
+
if (!trigger || trigger === '--list') {
|
|
895
|
+
console.log('\n🐝 BookLib Swarm Trigger Config\n');
|
|
896
|
+
console.log(' Maps swarm triggers → agent roles → skill domains\n');
|
|
897
|
+
console.log(' Usage: booklib swarm-config <trigger>\n');
|
|
898
|
+
Object.entries(SWARM_TRIGGERS).forEach(([t, cfg]) => {
|
|
899
|
+
console.log(` ${t.padEnd(12)} → roles: ${cfg.roles.join(', ')}`);
|
|
900
|
+
});
|
|
901
|
+
console.log('\n booklib swarm-config <trigger> Show skills for a trigger');
|
|
902
|
+
console.log(' booklib profile <role> Show skills for a role\n');
|
|
903
|
+
break;
|
|
904
|
+
}
|
|
905
|
+
|
|
906
|
+
const cfg = SWARM_TRIGGERS[trigger];
|
|
907
|
+
if (!cfg) {
|
|
908
|
+
console.log(`Unknown trigger "${trigger}". Available: ${Object.keys(SWARM_TRIGGERS).join(', ')}`);
|
|
909
|
+
break;
|
|
910
|
+
}
|
|
911
|
+
|
|
912
|
+
const engine = new DiscoveryEngine();
|
|
913
|
+
const all = await engine.discover();
|
|
914
|
+
const { skills: regSkills } = JSON.parse(
|
|
915
|
+
(await import('fs')).default.readFileSync(
|
|
916
|
+
(await import('path')).default.join(
|
|
917
|
+
(await import('url')).default.fileURLToPath(new URL('.', import.meta.url)),
|
|
918
|
+
'../community/registry.json'
|
|
919
|
+
), 'utf8'
|
|
920
|
+
)
|
|
921
|
+
);
|
|
922
|
+
const roleMap = new Map(regSkills.map(s => [s.name, s.roles ?? []]));
|
|
923
|
+
|
|
924
|
+
console.log(`\n🐝 Swarm config for trigger: ${trigger}\n`);
|
|
925
|
+
console.log(` Phases: ${cfg.phases.join(' → ')}\n`);
|
|
926
|
+
|
|
927
|
+
for (const role of cfg.roles) {
|
|
928
|
+
const roleSkills = all.filter(s => (roleMap.get(s.name) ?? s.roles ?? []).includes(role));
|
|
929
|
+
console.log(` Agent role: ${role} (${roleSkills.length} skills)`);
|
|
930
|
+
roleSkills.slice(0, 5).forEach(s => {
|
|
931
|
+
const stars = s.stars ? ` ★${s.stars.toLocaleString()}` : '';
|
|
932
|
+
console.log(` • ${s.name}${stars} — ${(s.description ?? '').slice(0, 75)}`);
|
|
933
|
+
});
|
|
934
|
+
if (roleSkills.length > 5) console.log(` … and ${roleSkills.length - 5} more (booklib profile ${role})`);
|
|
935
|
+
console.log();
|
|
936
|
+
}
|
|
937
|
+
break;
|
|
938
|
+
}
|
|
939
|
+
|
|
940
|
+
case 'note': {
|
|
941
|
+
const title = args.slice(1).join(' ');
|
|
942
|
+
if (!title) { console.error('Usage: booklib note "<title>"'); process.exit(1); }
|
|
943
|
+
const id = generateNodeId('node');
|
|
944
|
+
let body = await readStdin();
|
|
945
|
+
if (!body) body = openEditor('') ?? '';
|
|
946
|
+
if (!body) body = await readInteractive('Enter note content (Ctrl+D to finish):\n');
|
|
947
|
+
const noteContent = serializeNode({ id, type: 'note', title, content: body ?? '' });
|
|
948
|
+
const filePath = saveNode(noteContent, id);
|
|
949
|
+
await autoIndexNode(filePath);
|
|
950
|
+
try {
|
|
951
|
+
const autoLinked = await autoLink({ nodeId: id, title, content: body ?? '' });
|
|
952
|
+
if (autoLinked.length > 0) {
|
|
953
|
+
console.log(` Auto-linked: ${autoLinked.map(l => `${l.to} (${l.type})`).join(', ')}`);
|
|
954
|
+
}
|
|
955
|
+
} catch { /* best-effort */ }
|
|
956
|
+
console.log(`✅ Note created: ${filePath}`);
|
|
957
|
+
console.log(` ID: ${id}`);
|
|
958
|
+
break;
|
|
959
|
+
}
|
|
960
|
+
|
|
961
|
+
case 'component': {
|
|
962
|
+
const subcommand = args[1];
|
|
963
|
+
const name = args[2];
|
|
964
|
+
const glob = args[3];
|
|
965
|
+
if (subcommand !== 'add' || !name || !glob) {
|
|
966
|
+
console.error('Usage: booklib component add <name> "<glob>"');
|
|
967
|
+
process.exit(1);
|
|
968
|
+
}
|
|
969
|
+
const id = `comp_${name.toLowerCase().replace(/[^a-z0-9]/g, '_')}`;
|
|
970
|
+
const content = serializeNode({
|
|
971
|
+
id,
|
|
972
|
+
type: 'component',
|
|
973
|
+
title: name,
|
|
974
|
+
nodePaths: [glob],
|
|
975
|
+
content: '',
|
|
976
|
+
});
|
|
977
|
+
const filePath = saveNode(content, id);
|
|
978
|
+
try {
|
|
979
|
+
const reverseLinks = await autoLinkReverse({ componentId: id, componentTitle: name });
|
|
980
|
+
if (reverseLinks.length > 0) {
|
|
981
|
+
console.log(` Auto-linked ${reverseLinks.length} existing note(s) to this component`);
|
|
982
|
+
}
|
|
983
|
+
} catch { /* best-effort */ }
|
|
984
|
+
console.log(`✅ Component created: ${filePath}`);
|
|
985
|
+
console.log(` ID: ${id} paths: ${glob}`);
|
|
986
|
+
break;
|
|
987
|
+
}
|
|
988
|
+
|
|
989
|
+
case 'link': {
|
|
990
|
+
const [, fromRef, toRef] = args;
|
|
991
|
+
const typeArg = parseFlag(args, 'type');
|
|
992
|
+
const weightArg = parseFlag(args, 'weight');
|
|
993
|
+
if (!fromRef || !toRef || !typeArg) {
|
|
994
|
+
console.error('Usage: booklib link "<title-or-id>" "<title-or-id>" --type <edge-type> [--weight 0.9]');
|
|
995
|
+
process.exit(1);
|
|
996
|
+
}
|
|
997
|
+
if (!EDGE_TYPES.includes(typeArg)) {
|
|
998
|
+
console.error(`Invalid edge type "${typeArg}". Valid: ${EDGE_TYPES.join(', ')}`);
|
|
999
|
+
process.exit(1);
|
|
1000
|
+
}
|
|
1001
|
+
let from, to;
|
|
1002
|
+
try {
|
|
1003
|
+
from = resolveNodeRef(fromRef);
|
|
1004
|
+
to = resolveNodeRef(toRef);
|
|
1005
|
+
} catch (err) {
|
|
1006
|
+
console.error(err.message);
|
|
1007
|
+
process.exit(1);
|
|
1008
|
+
}
|
|
1009
|
+
const edge = {
|
|
1010
|
+
from,
|
|
1011
|
+
to,
|
|
1012
|
+
type: typeArg,
|
|
1013
|
+
weight: weightArg ? parseFloat(weightArg) : 1.0,
|
|
1014
|
+
created: new Date().toISOString().split('T')[0],
|
|
1015
|
+
};
|
|
1016
|
+
appendEdge(edge);
|
|
1017
|
+
console.log(`✅ Edge added: ${from} --[${typeArg}]--> ${to} (weight: ${edge.weight})`);
|
|
1018
|
+
break;
|
|
1019
|
+
}
|
|
1020
|
+
|
|
1021
|
+
case 'nodes': {
|
|
1022
|
+
const subcommand = args[1];
|
|
1023
|
+
if (!subcommand || subcommand === 'list') {
|
|
1024
|
+
const ids = listNodes();
|
|
1025
|
+
if (ids.length === 0) { console.log('No knowledge nodes yet. Try: booklib note "title"'); break; }
|
|
1026
|
+
console.log(`\n📝 Knowledge nodes (${ids.length}):\n`);
|
|
1027
|
+
for (const id of ids) {
|
|
1028
|
+
const raw = loadNode(id);
|
|
1029
|
+
const parsed = raw ? parseNodeFrontmatter(raw) : {};
|
|
1030
|
+
const tags = Array.isArray(parsed.tags) ? parsed.tags.join(', ') : '';
|
|
1031
|
+
console.log(` ${id} [${parsed.type ?? '?'}] ${parsed.title ?? '?'}${tags ? ` (${tags})` : ''}`);
|
|
1032
|
+
}
|
|
1033
|
+
break;
|
|
1034
|
+
}
|
|
1035
|
+
if (subcommand === 'show') {
|
|
1036
|
+
const id = args[2];
|
|
1037
|
+
if (!id) { console.error('Usage: booklib nodes show <id>'); process.exit(1); }
|
|
1038
|
+
const raw = loadNode(id);
|
|
1039
|
+
if (!raw) { console.error(`Node "${id}" not found.`); process.exit(1); }
|
|
1040
|
+
console.log(raw);
|
|
1041
|
+
break;
|
|
1042
|
+
}
|
|
1043
|
+
console.error('Usage: booklib nodes list | booklib nodes show <id>');
|
|
1044
|
+
process.exit(1);
|
|
1045
|
+
}
|
|
1046
|
+
|
|
1047
|
+
case 'dictate': {
|
|
1048
|
+
const isRaw = args.includes('--raw');
|
|
1049
|
+
const titleArg = parseFlag(args, 'title');
|
|
1050
|
+
|
|
1051
|
+
const stdinText = await readStdin();
|
|
1052
|
+
const rawText = stdinText || await readInteractive();
|
|
1053
|
+
|
|
1054
|
+
if (!rawText) { console.error('No input provided.'); process.exit(1); }
|
|
1055
|
+
|
|
1056
|
+
const id = generateNodeId('node');
|
|
1057
|
+
let nodeContent;
|
|
1058
|
+
|
|
1059
|
+
if (isRaw) {
|
|
1060
|
+
const title = titleArg ?? rawText.split('\n')[0].slice(0, 60);
|
|
1061
|
+
nodeContent = serializeNode({ id, type: 'note', title, content: rawText });
|
|
1062
|
+
} else {
|
|
1063
|
+
console.log('Structuring with AI...');
|
|
1064
|
+
let structured;
|
|
1065
|
+
try {
|
|
1066
|
+
structured = await callAnthropicAPI(buildDictatePrompt(rawText));
|
|
1067
|
+
} catch (err) {
|
|
1068
|
+
console.error(`AI structuring failed: ${err.message}`);
|
|
1069
|
+
console.error('Tip: use --raw to save without AI processing.');
|
|
1070
|
+
const title = titleArg ?? rawText.split('\n')[0].slice(0, 60);
|
|
1071
|
+
nodeContent = serializeNode({ id, type: 'note', title, content: rawText });
|
|
1072
|
+
}
|
|
1073
|
+
if (structured) {
|
|
1074
|
+
nodeContent = `---\nid: "${id}"\n` + structured.replace(/^---\n?/, '');
|
|
1075
|
+
}
|
|
1076
|
+
}
|
|
1077
|
+
|
|
1078
|
+
const filePath = saveNode(nodeContent, id);
|
|
1079
|
+
await autoIndexNode(filePath);
|
|
1080
|
+
try {
|
|
1081
|
+
const savedRaw = loadNode(id);
|
|
1082
|
+
const savedParsed = savedRaw ? parseNodeFrontmatter(savedRaw) : {};
|
|
1083
|
+
const autoLinked = await autoLink({
|
|
1084
|
+
nodeId: id,
|
|
1085
|
+
title: savedParsed.title ?? titleArg ?? '',
|
|
1086
|
+
content: savedParsed.body ?? rawText ?? '',
|
|
1087
|
+
});
|
|
1088
|
+
if (autoLinked.length > 0) {
|
|
1089
|
+
console.log(` Auto-linked: ${autoLinked.map(l => `${l.to} (${l.type})`).join(', ')}`);
|
|
1090
|
+
}
|
|
1091
|
+
} catch { /* best-effort */ }
|
|
1092
|
+
console.log(`✅ Note saved: ${filePath}`);
|
|
1093
|
+
console.log(` ID: ${id}`);
|
|
1094
|
+
break;
|
|
1095
|
+
}
|
|
1096
|
+
|
|
1097
|
+
case 'save-chat': {
|
|
1098
|
+
const doSummarize = args.includes('--summarize');
|
|
1099
|
+
const titleArg = parseFlag(args, 'title');
|
|
1100
|
+
// Skip flag values consumed by --flag value pairs so they aren't mistaken for a file path
|
|
1101
|
+
const titleIdx = args.indexOf('--title');
|
|
1102
|
+
const consumedIndices = new Set(titleIdx !== -1 ? [titleIdx, titleIdx + 1] : []);
|
|
1103
|
+
const fileArg = args.slice(1).find((a, i) => !a.startsWith('--') && !consumedIndices.has(i + 1));
|
|
1104
|
+
|
|
1105
|
+
let transcript;
|
|
1106
|
+
if (fileArg) {
|
|
1107
|
+
transcript = fs.readFileSync(fileArg, 'utf8').trim();
|
|
1108
|
+
} else {
|
|
1109
|
+
transcript = await readStdin();
|
|
1110
|
+
}
|
|
1111
|
+
if (!transcript) {
|
|
1112
|
+
transcript = openEditor('# Paste or type the conversation here\n\n');
|
|
1113
|
+
}
|
|
1114
|
+
if (!transcript) { console.error('No conversation content provided.'); process.exit(1); }
|
|
1115
|
+
|
|
1116
|
+
const id = generateNodeId('node');
|
|
1117
|
+
let nodeContent;
|
|
1118
|
+
|
|
1119
|
+
if (doSummarize) {
|
|
1120
|
+
console.log('Summarizing conversation with AI...');
|
|
1121
|
+
try {
|
|
1122
|
+
const summary = await callAnthropicAPI(buildSummarizePrompt(transcript, titleArg ?? ''));
|
|
1123
|
+
nodeContent = `---\nid: "${id}"\n` + summary.replace(/^---\n?/, '');
|
|
1124
|
+
} catch (err) {
|
|
1125
|
+
console.error(`AI summarization failed: ${err.message}`);
|
|
1126
|
+
nodeContent = serializeNode({
|
|
1127
|
+
id, type: 'note',
|
|
1128
|
+
title: titleArg ?? 'Conversation transcript',
|
|
1129
|
+
content: transcript,
|
|
1130
|
+
sources: ['conversation'],
|
|
1131
|
+
});
|
|
1132
|
+
}
|
|
1133
|
+
} else {
|
|
1134
|
+
nodeContent = serializeNode({
|
|
1135
|
+
id, type: 'note',
|
|
1136
|
+
title: titleArg ?? 'Conversation transcript',
|
|
1137
|
+
content: transcript,
|
|
1138
|
+
sources: ['conversation'],
|
|
1139
|
+
});
|
|
1140
|
+
}
|
|
1141
|
+
|
|
1142
|
+
const filePath = saveNode(nodeContent, id);
|
|
1143
|
+
await autoIndexNode(filePath);
|
|
1144
|
+
try {
|
|
1145
|
+
const savedRaw = loadNode(id);
|
|
1146
|
+
const savedParsed = savedRaw ? parseNodeFrontmatter(savedRaw) : {};
|
|
1147
|
+
const autoLinked = await autoLink({
|
|
1148
|
+
nodeId: id,
|
|
1149
|
+
title: savedParsed.title ?? titleArg ?? 'Conversation transcript',
|
|
1150
|
+
content: savedParsed.body ?? transcript ?? '',
|
|
1151
|
+
});
|
|
1152
|
+
if (autoLinked.length > 0) {
|
|
1153
|
+
console.log(` Auto-linked: ${autoLinked.map(l => `${l.to} (${l.type})`).join(', ')}`);
|
|
1154
|
+
}
|
|
1155
|
+
} catch { /* best-effort */ }
|
|
1156
|
+
console.log(`✅ Conversation saved: ${filePath}`);
|
|
1157
|
+
console.log(` ID: ${id}`);
|
|
1158
|
+
break;
|
|
1159
|
+
}
|
|
1160
|
+
|
|
1161
|
+
case 'research': {
|
|
1162
|
+
const topic = args.slice(1).join(' ');
|
|
1163
|
+
if (!topic) { console.error('Usage: booklib research "<topic>"'); process.exit(1); }
|
|
1164
|
+
const id = generateNodeId('node');
|
|
1165
|
+
const template = `## Sources\n\n<!-- Add URLs, papers, docs -->\n\n## Key Findings\n\n<!-- Fill in after researching -->\n\n## Summary\n\n<!-- 2-3 sentence summary -->\n`;
|
|
1166
|
+
const nodeContent = serializeNode({
|
|
1167
|
+
id,
|
|
1168
|
+
type: 'research',
|
|
1169
|
+
title: topic,
|
|
1170
|
+
content: template,
|
|
1171
|
+
confidence: 'low',
|
|
1172
|
+
});
|
|
1173
|
+
const filePath = saveNode(nodeContent, id);
|
|
1174
|
+
await autoIndexNode(filePath);
|
|
1175
|
+
try {
|
|
1176
|
+
const autoLinked = await autoLink({ nodeId: id, title: topic, content: template });
|
|
1177
|
+
if (autoLinked.length > 0) {
|
|
1178
|
+
console.log(` Auto-linked: ${autoLinked.map(l => `${l.to} (${l.type})`).join(', ')}`);
|
|
1179
|
+
}
|
|
1180
|
+
} catch { /* best-effort */ }
|
|
1181
|
+
console.log(`✅ Research template created: ${filePath}`);
|
|
1182
|
+
console.log(` ID: ${id}`);
|
|
1183
|
+
console.log(` Fill in the findings — this node is already indexed and searchable.`);
|
|
1184
|
+
break;
|
|
1185
|
+
}
|
|
1186
|
+
|
|
1187
|
+
case 'uninstall': {
|
|
1188
|
+
const skillName = args[1];
|
|
1189
|
+
if (!skillName) {
|
|
1190
|
+
console.error('Usage: booklib uninstall <skill-name>');
|
|
1191
|
+
process.exit(1);
|
|
1192
|
+
}
|
|
1193
|
+
const fetcher = new SkillFetcher();
|
|
1194
|
+
fetcher.desyncFromClaudeSkills({ name: skillName });
|
|
1195
|
+
const remaining = countInstalledSlots();
|
|
1196
|
+
console.log(`✓ Removed ${skillName} from ~/.claude/skills/`);
|
|
1197
|
+
console.log(` ${remaining}/${SKILL_LIMIT} slots now used`);
|
|
1198
|
+
break;
|
|
1199
|
+
}
|
|
1200
|
+
|
|
1201
|
+
case 'list': {
|
|
1202
|
+
const names = listInstalledSkillNames();
|
|
1203
|
+
const slots = countInstalledSlots();
|
|
1204
|
+
if (names.length === 0) {
|
|
1205
|
+
console.log('No BookLib-managed skills installed. Run "booklib init" to get started.');
|
|
1206
|
+
break;
|
|
1207
|
+
}
|
|
1208
|
+
console.log(`\nInstalled skills (${slots}/${SKILL_LIMIT} slots):\n`);
|
|
1209
|
+
for (const name of names) console.log(` · ${name}`);
|
|
1210
|
+
console.log('');
|
|
1211
|
+
if (slots > SKILL_LIMIT - 4) console.log(' ⚠ Approaching slot limit. Run "booklib doctor" to review.');
|
|
1212
|
+
break;
|
|
1213
|
+
}
|
|
1214
|
+
|
|
1215
|
+
case 'doctor': {
|
|
1216
|
+
const installHook = args.includes('--install-hook');
|
|
1217
|
+
const showUsage = args.includes('--usage');
|
|
1218
|
+
const cure = args.includes('--cure');
|
|
1219
|
+
|
|
1220
|
+
if (installHook) {
|
|
1221
|
+
try {
|
|
1222
|
+
const result = installTrackingHook();
|
|
1223
|
+
if (result.alreadyInstalled) {
|
|
1224
|
+
console.log(' Hook already installed — nothing changed.');
|
|
1225
|
+
} else {
|
|
1226
|
+
console.log('✓ Tracking hook installed');
|
|
1227
|
+
console.log(` Script: ${result.scriptPath}`);
|
|
1228
|
+
console.log(` Hook: ${result.settingsPath} → PreToolUse[Skill]`);
|
|
1229
|
+
console.log('');
|
|
1230
|
+
console.log(' Skill usage will be tracked from now on.');
|
|
1231
|
+
console.log(' Run `booklib doctor` after a few sessions to see your report.');
|
|
1232
|
+
}
|
|
1233
|
+
} catch (err) {
|
|
1234
|
+
console.error(`Failed to install hook: ${err.message}`);
|
|
1235
|
+
process.exit(1);
|
|
1236
|
+
}
|
|
1237
|
+
break;
|
|
1238
|
+
}
|
|
1239
|
+
|
|
1240
|
+
if (showUsage) {
|
|
1241
|
+
// Legacy usage report (moved behind --usage flag)
|
|
1242
|
+
const MS_PER_DAY = 24 * 60 * 60 * 1000;
|
|
1243
|
+
const SKILL_NAME_PAD = 24;
|
|
1244
|
+
const USE_LABEL_PAD = 9;
|
|
1245
|
+
|
|
1246
|
+
const claudeSkillsDir = path.join(os.homedir(), '.claude', 'skills');
|
|
1247
|
+
const usagePath = path.join(os.homedir(), '.booklib', 'usage.json');
|
|
1248
|
+
const installedNames = listInstalledSkillNames();
|
|
1249
|
+
const usageData = readUsage(usagePath);
|
|
1250
|
+
|
|
1251
|
+
if (installedNames.length === 0) {
|
|
1252
|
+
console.log('\n No BookLib-managed skills installed. Run "booklib init" to get started.\n');
|
|
1253
|
+
break;
|
|
1254
|
+
}
|
|
1255
|
+
|
|
1256
|
+
const installDates = {};
|
|
1257
|
+
for (const name of installedNames) {
|
|
1258
|
+
try {
|
|
1259
|
+
const stat = fs.statSync(path.join(claudeSkillsDir, name, '.booklib'));
|
|
1260
|
+
installDates[name] = stat.mtime;
|
|
1261
|
+
} catch { /* unknown install date */ }
|
|
1262
|
+
}
|
|
1263
|
+
|
|
1264
|
+
const summary = summarize(usageData, installedNames, installDates);
|
|
1265
|
+
const suggestions = summary.filter(s => s.suggestion !== null);
|
|
1266
|
+
|
|
1267
|
+
console.log('\n► Skill usage report\n');
|
|
1268
|
+
|
|
1269
|
+
const active = summary.filter(s => s.uses > 0 || s.suggestion !== null);
|
|
1270
|
+
const silentCount = summary.length - active.length;
|
|
1271
|
+
|
|
1272
|
+
const noUsageFile = !fs.existsSync(usagePath);
|
|
1273
|
+
if (active.length === 0 && noUsageFile) {
|
|
1274
|
+
console.log(` ${installedNames.length} community skill${installedNames.length === 1 ? '' : 's'} in ~/.booklib/skills/. No usage data yet.\n`);
|
|
1275
|
+
console.log(' Tip: run `booklib doctor --install-hook` to start tracking usage automatically.');
|
|
1276
|
+
} else if (active.length === 0) {
|
|
1277
|
+
console.log(` ${installedNames.length} community skill${installedNames.length === 1 ? '' : 's'} in ~/.booklib/skills/. No usage data yet.\n`);
|
|
1278
|
+
} else {
|
|
1279
|
+
for (const item of active) {
|
|
1280
|
+
const icon = item.suggestion ? '⚠' : '✓';
|
|
1281
|
+
const useLabel = item.uses === 1 ? '1 use ' : `${item.uses} uses`;
|
|
1282
|
+
let whenLabel;
|
|
1283
|
+
if (item.lastUsed === null) {
|
|
1284
|
+
const days = installDates[item.name]
|
|
1285
|
+
? Math.floor((Date.now() - installDates[item.name].getTime()) / MS_PER_DAY)
|
|
1286
|
+
: null;
|
|
1287
|
+
whenLabel = days !== null ? `never — installed ${days} days ago` : 'never';
|
|
1288
|
+
} else {
|
|
1289
|
+
whenLabel = `${item.daysSinceLastUse} day${item.daysSinceLastUse === 1 ? '' : 's'} ago`;
|
|
1290
|
+
}
|
|
1291
|
+
console.log(` ${icon} ${item.name.padEnd(SKILL_NAME_PAD)} ${useLabel.padEnd(USE_LABEL_PAD)} (${whenLabel})`);
|
|
1292
|
+
}
|
|
1293
|
+
if (silentCount > 0) {
|
|
1294
|
+
console.log(`\n ${silentCount} other skill${silentCount === 1 ? '' : 's'} — no usage recorded`);
|
|
1295
|
+
}
|
|
1296
|
+
}
|
|
1297
|
+
|
|
1298
|
+
if (suggestions.length > 0) {
|
|
1299
|
+
console.log('\n Suggestions:');
|
|
1300
|
+
for (const item of suggestions) {
|
|
1301
|
+
if (item.suggestion === 'remove') {
|
|
1302
|
+
console.log(` · ${item.name}: never used — consider removing (booklib uninstall ${item.name})`);
|
|
1303
|
+
} else {
|
|
1304
|
+
const days = item.daysSinceLastUse ?? 60;
|
|
1305
|
+
console.log(` · ${item.name}: ${item.uses} use${item.uses === 1 ? '' : 's'} in ${days} days — low activity`);
|
|
1306
|
+
}
|
|
1307
|
+
}
|
|
1308
|
+
console.log('\n Run `booklib uninstall <skill>` to free up slots.');
|
|
1309
|
+
}
|
|
1310
|
+
|
|
1311
|
+
if (noUsageFile && active.length > 0) {
|
|
1312
|
+
console.log('\n Tip: run `booklib doctor --install-hook` to start tracking usage.');
|
|
1313
|
+
}
|
|
1314
|
+
|
|
1315
|
+
console.log('');
|
|
1316
|
+
break;
|
|
1317
|
+
}
|
|
1318
|
+
|
|
1319
|
+
// Default: run diagnostics
|
|
1320
|
+
const { runDiagnostics, printDiagnostics } = await import('../lib/engine/doctor.js');
|
|
1321
|
+
|
|
1322
|
+
console.log('\n BookLib Health Check\n');
|
|
1323
|
+
const findings = runDiagnostics(process.cwd());
|
|
1324
|
+
printDiagnostics(findings);
|
|
1325
|
+
|
|
1326
|
+
if (cure && findings.some(f => f.fixable)) {
|
|
1327
|
+
console.log(' Applying fixes...\n');
|
|
1328
|
+
|
|
1329
|
+
for (const f of findings) {
|
|
1330
|
+
if (!f.fixable) continue;
|
|
1331
|
+
|
|
1332
|
+
if (f.check === 'missing-index') {
|
|
1333
|
+
console.log(' Building search index...');
|
|
1334
|
+
const indexer = new BookLibIndexer();
|
|
1335
|
+
const { skillsPath } = resolveBookLibPaths();
|
|
1336
|
+
await indexer.indexDirectory(skillsPath, false, { quiet: true });
|
|
1337
|
+
console.log(' Index built.\n');
|
|
1338
|
+
}
|
|
1339
|
+
}
|
|
1340
|
+
|
|
1341
|
+
// Re-run diagnostics to show updated state
|
|
1342
|
+
const updated = runDiagnostics(process.cwd());
|
|
1343
|
+
const remaining = updated.filter(f => f.fixable).length;
|
|
1344
|
+
if (remaining === 0) {
|
|
1345
|
+
console.log(' All fixable issues resolved.\n');
|
|
1346
|
+
} else {
|
|
1347
|
+
console.log(` ${remaining} issue(s) remain that require manual intervention.\n`);
|
|
1348
|
+
}
|
|
1349
|
+
}
|
|
1350
|
+
|
|
1351
|
+
break;
|
|
1352
|
+
}
|
|
1353
|
+
|
|
1354
|
+
case 'correction': {
|
|
1355
|
+
const sub = args[1];
|
|
1356
|
+
|
|
1357
|
+
if (!sub || sub === 'help') {
|
|
1358
|
+
console.log('\nUsage:');
|
|
1359
|
+
console.log(' booklib correction add "<text>" — record a correction');
|
|
1360
|
+
console.log(' booklib correction list — show all corrections');
|
|
1361
|
+
console.log(' booklib correction remove <id> — delete a correction\n');
|
|
1362
|
+
break;
|
|
1363
|
+
}
|
|
1364
|
+
|
|
1365
|
+
if (sub === 'add') {
|
|
1366
|
+
const text = args.slice(2).join(' ').replace(/^["']|["']$/g, '');
|
|
1367
|
+
if (!text) {
|
|
1368
|
+
console.error(' Usage: booklib correction add "text of the correction"');
|
|
1369
|
+
process.exit(1);
|
|
1370
|
+
}
|
|
1371
|
+
process.stdout.write(' Recording correction (loading embedding model)...\n');
|
|
1372
|
+
const result = await addCorrection(text);
|
|
1373
|
+
const levelUp = result.wasExisting && result.level > levelFromMentions(result.mentions - 1);
|
|
1374
|
+
const action = result.wasExisting ? 'Updated' : 'Recorded';
|
|
1375
|
+
const arrow = levelUp ? ' ↑' : '';
|
|
1376
|
+
console.log(`✓ ${action}: "${result.text}" (mentions: ${result.mentions}, level: ${result.level}${arrow})`);
|
|
1377
|
+
if (levelUp && result.level >= 3) {
|
|
1378
|
+
console.log(` → ~/.claude/CLAUDE.md updated`);
|
|
1379
|
+
}
|
|
1380
|
+
break;
|
|
1381
|
+
}
|
|
1382
|
+
|
|
1383
|
+
if (sub === 'list') {
|
|
1384
|
+
const all = listCorrections();
|
|
1385
|
+
if (all.length === 0) {
|
|
1386
|
+
console.log('\n No corrections recorded yet.\n');
|
|
1387
|
+
break;
|
|
1388
|
+
}
|
|
1389
|
+
console.log(`\n► Learned corrections (${all.length} total)\n`);
|
|
1390
|
+
console.log(` ${'ID'.padEnd(8)} ${'Mentions'.padEnd(10)} ${'Level'.padEnd(7)} Text`);
|
|
1391
|
+
for (const c of all) {
|
|
1392
|
+
const marker = c.level >= 3 ? '●' : ' ';
|
|
1393
|
+
const lvl = `${c.level} ${marker}`;
|
|
1394
|
+
console.log(` ${c.id.padEnd(8)} ${String(c.mentions).padEnd(10)} ${lvl.padEnd(7)} ${c.text.slice(0, 60)}`);
|
|
1395
|
+
}
|
|
1396
|
+
console.log('\n ● = injected into ~/.claude/CLAUDE.md\n');
|
|
1397
|
+
break;
|
|
1398
|
+
}
|
|
1399
|
+
|
|
1400
|
+
if (sub === 'remove') {
|
|
1401
|
+
const id = args[2];
|
|
1402
|
+
if (!id) {
|
|
1403
|
+
console.error(' Usage: booklib correction remove <id>');
|
|
1404
|
+
process.exit(1);
|
|
1405
|
+
}
|
|
1406
|
+
const removed = removeCorrection(id);
|
|
1407
|
+
if (!removed) {
|
|
1408
|
+
console.error(` Not found: ${id}`);
|
|
1409
|
+
process.exit(1);
|
|
1410
|
+
}
|
|
1411
|
+
console.log(`✓ Removed "${removed.text}"`);
|
|
1412
|
+
console.log(` → ~/.claude/CLAUDE.md updated`);
|
|
1413
|
+
break;
|
|
1414
|
+
}
|
|
1415
|
+
|
|
1416
|
+
console.error(` Unknown subcommand: ${sub}`);
|
|
1417
|
+
console.error(' Use: booklib correction add|list|remove');
|
|
1418
|
+
process.exit(1);
|
|
1419
|
+
}
|
|
1420
|
+
|
|
1421
|
+
case 'rules': {
|
|
1422
|
+
const subcommand = args[1];
|
|
1423
|
+
|
|
1424
|
+
switch (subcommand) {
|
|
1425
|
+
case 'list': {
|
|
1426
|
+
const available = listAvailableRules();
|
|
1427
|
+
console.log('\n► Available rule sets\n');
|
|
1428
|
+
console.log(` ${'Bundled:'.padEnd(22)} ${'project'.padEnd(12)} global`);
|
|
1429
|
+
for (const item of available) {
|
|
1430
|
+
const icon = (item.installedProject || item.installedGlobal) ? '✓' : '·';
|
|
1431
|
+
const proj = item.installedProject ? 'installed' : '—';
|
|
1432
|
+
const glob = item.installedGlobal ? 'installed' : '—';
|
|
1433
|
+
console.log(` ${icon} ${item.lang.padEnd(22)} ${proj.padEnd(12)} ${glob}`);
|
|
1434
|
+
}
|
|
1435
|
+
console.log('');
|
|
1436
|
+
console.log(' booklib rules install <lang> → add to .cursor/rules/');
|
|
1437
|
+
console.log(' booklib rules install <lang> --global → add to global agent config');
|
|
1438
|
+
console.log('');
|
|
1439
|
+
break;
|
|
1440
|
+
}
|
|
1441
|
+
|
|
1442
|
+
case 'install': {
|
|
1443
|
+
const lang = args[2];
|
|
1444
|
+
if (!lang || lang.startsWith('--')) {
|
|
1445
|
+
console.error(' Usage: booklib rules install <lang> [--global]');
|
|
1446
|
+
process.exit(1);
|
|
1447
|
+
}
|
|
1448
|
+
const isGlobal = args.includes('--global');
|
|
1449
|
+
try {
|
|
1450
|
+
const written = installRuleFn(lang, { global: isGlobal });
|
|
1451
|
+
if (written.length === 0) {
|
|
1452
|
+
console.log(`\n No rule files found for '${lang}'.\n`);
|
|
1453
|
+
break;
|
|
1454
|
+
}
|
|
1455
|
+
if (isGlobal) {
|
|
1456
|
+
const st = rulesStatus();
|
|
1457
|
+
const entry = st.global.find(g => g.lang === lang);
|
|
1458
|
+
const sizeLabel = entry ? formatBytes(entry.sizeBytes) : '';
|
|
1459
|
+
console.log(`\n✓ Installed ${lang} rules globally`);
|
|
1460
|
+
console.log(` ~/.claude/CLAUDE.md → added ${lang} section (${sizeLabel})\n`);
|
|
1461
|
+
} else {
|
|
1462
|
+
console.log(`\n✓ Installed ${lang} rules`);
|
|
1463
|
+
for (const p of written) {
|
|
1464
|
+
console.log(` ${path.relative(process.cwd(), p)} (${formatBytes(fs.statSync(p).size)})`);
|
|
1465
|
+
}
|
|
1466
|
+
console.log('');
|
|
1467
|
+
}
|
|
1468
|
+
} catch (err) {
|
|
1469
|
+
const msg = err.message;
|
|
1470
|
+
const availIdx = msg.indexOf('. Available:');
|
|
1471
|
+
if (availIdx !== -1) {
|
|
1472
|
+
console.error(` ${msg.slice(0, availIdx)}`);
|
|
1473
|
+
console.error(` ${msg.slice(availIdx + 2)}`);
|
|
1474
|
+
} else {
|
|
1475
|
+
console.error(` ${msg}`);
|
|
1476
|
+
}
|
|
1477
|
+
process.exit(1);
|
|
1478
|
+
}
|
|
1479
|
+
break;
|
|
1480
|
+
}
|
|
1481
|
+
|
|
1482
|
+
case 'status': {
|
|
1483
|
+
const st = rulesStatus();
|
|
1484
|
+
console.log('\n► Rules status\n');
|
|
1485
|
+
|
|
1486
|
+
if (st.cursor.length === 0 && st.global.length === 0) {
|
|
1487
|
+
console.log(' No rules installed in current project.\n');
|
|
1488
|
+
console.log(' Tip: booklib rules install <lang> to add standards.\n');
|
|
1489
|
+
break;
|
|
1490
|
+
}
|
|
1491
|
+
|
|
1492
|
+
if (st.cursor.length > 0) {
|
|
1493
|
+
console.log(' .cursor/rules/ (project)');
|
|
1494
|
+
for (const item of st.cursor) {
|
|
1495
|
+
console.log(` ${path.basename(item.path).padEnd(42)} ${formatBytes(item.sizeBytes)}`);
|
|
1496
|
+
}
|
|
1497
|
+
console.log('');
|
|
1498
|
+
}
|
|
1499
|
+
|
|
1500
|
+
if (st.global.length > 0) {
|
|
1501
|
+
console.log(' ~/.claude/CLAUDE.md (global)');
|
|
1502
|
+
for (const item of st.global) {
|
|
1503
|
+
console.log(` ${item.lang.padEnd(42)} ${formatBytes(item.sizeBytes)}`);
|
|
1504
|
+
}
|
|
1505
|
+
console.log('');
|
|
1506
|
+
}
|
|
1507
|
+
|
|
1508
|
+
const projCount = st.cursor.length;
|
|
1509
|
+
const globCount = st.global.length;
|
|
1510
|
+
console.log(` Total: ${formatBytes(st.totalBytes)} across ${projCount} project + ${globCount} global rule(s)\n`);
|
|
1511
|
+
break;
|
|
1512
|
+
}
|
|
1513
|
+
|
|
1514
|
+
default:
|
|
1515
|
+
console.log('\n booklib rules list — show available rule sets');
|
|
1516
|
+
console.log(' booklib rules install <lang> — install to .cursor/rules/');
|
|
1517
|
+
console.log(' booklib rules install <lang> --global — install to global agent config');
|
|
1518
|
+
console.log(' booklib rules status — show installed rules + sizes\n');
|
|
1519
|
+
}
|
|
1520
|
+
break;
|
|
1521
|
+
}
|
|
1522
|
+
|
|
1523
|
+
case 'remember':
|
|
1524
|
+
case 'capture': {
|
|
1525
|
+
const title = parseFlag(args, 'title');
|
|
1526
|
+
const type = parseFlag(args, 'type') ?? 'insight';
|
|
1527
|
+
const tagsArg = parseFlag(args, 'tags') ?? '';
|
|
1528
|
+
const linksArg = parseFlag(args, 'links') ?? '';
|
|
1529
|
+
|
|
1530
|
+
if (!title) {
|
|
1531
|
+
console.error('Usage: booklib capture --title "<title>" [--type insight] [--tags tag1,tag2] [--links "skill:edge-type,...]"');
|
|
1532
|
+
process.exit(1);
|
|
1533
|
+
}
|
|
1534
|
+
|
|
1535
|
+
const tags = tagsArg ? tagsArg.split(',').map(t => t.trim()).filter(Boolean) : [];
|
|
1536
|
+
const links = parseCaptureLinkArgs(linksArg);
|
|
1537
|
+
|
|
1538
|
+
for (const link of links) {
|
|
1539
|
+
if (!EDGE_TYPES.includes(link.type)) {
|
|
1540
|
+
console.error(`Invalid edge type "${link.type}". Valid: ${EDGE_TYPES.join(', ')}`);
|
|
1541
|
+
process.exit(1);
|
|
1542
|
+
}
|
|
1543
|
+
}
|
|
1544
|
+
|
|
1545
|
+
const id = generateNodeId(type);
|
|
1546
|
+
const nodeContent = serializeNode({ id, type, title, tags });
|
|
1547
|
+
|
|
1548
|
+
const globalBookLibDir = path.join(os.homedir(), '.booklib');
|
|
1549
|
+
const globalNodesDir = path.join(globalBookLibDir, 'knowledge', 'nodes');
|
|
1550
|
+
const globalGraphFile = path.join(globalBookLibDir, 'knowledge', 'graph.jsonl');
|
|
1551
|
+
|
|
1552
|
+
const filePath = saveNode(nodeContent, id, { nodesDir: globalNodesDir });
|
|
1553
|
+
await autoIndexNode(filePath);
|
|
1554
|
+
|
|
1555
|
+
const today = new Date().toISOString().split('T')[0];
|
|
1556
|
+
for (const link of links) {
|
|
1557
|
+
appendEdge({ from: id, to: link.to, type: link.type, weight: 1.0, created: today }, { graphFile: globalGraphFile });
|
|
1558
|
+
}
|
|
1559
|
+
|
|
1560
|
+
try {
|
|
1561
|
+
const autoLinked = await autoLink({
|
|
1562
|
+
nodeId: id,
|
|
1563
|
+
title,
|
|
1564
|
+
content: '',
|
|
1565
|
+
tags,
|
|
1566
|
+
nodesDir: globalNodesDir,
|
|
1567
|
+
graphFile: globalGraphFile,
|
|
1568
|
+
});
|
|
1569
|
+
if (autoLinked.length > 0) {
|
|
1570
|
+
console.log(` Auto-linked: ${autoLinked.map(l => `${l.to} (${l.type})`).join(', ')}`);
|
|
1571
|
+
}
|
|
1572
|
+
} catch { /* best-effort */ }
|
|
1573
|
+
|
|
1574
|
+
console.log(`✅ Knowledge node created: ${filePath}`);
|
|
1575
|
+
console.log(` ID: ${id}`);
|
|
1576
|
+
if (links.length > 0) {
|
|
1577
|
+
console.log(` Linked: ${links.map(l => `${l.to} (${l.type})`).join(', ')}`);
|
|
1578
|
+
}
|
|
1579
|
+
break;
|
|
1580
|
+
}
|
|
1581
|
+
|
|
1582
|
+
case 'benchmark': {
|
|
1583
|
+
const { run } = await import('../benchmark/run-eval.js');
|
|
1584
|
+
await run();
|
|
1585
|
+
break;
|
|
1586
|
+
}
|
|
1587
|
+
|
|
1588
|
+
case 'build-wellknown': {
|
|
1589
|
+
const builder = new WellKnownBuilder();
|
|
1590
|
+
const outPath = await builder.build();
|
|
1591
|
+
console.log(`Generated: ${outPath}`);
|
|
1592
|
+
process.exit(0);
|
|
1593
|
+
}
|
|
1594
|
+
|
|
1595
|
+
case 'connect': {
|
|
1596
|
+
const target = args[1];
|
|
1597
|
+
if (!target) {
|
|
1598
|
+
console.error('Usage: booklib connect <url-or-path> [--type=<type>] [--name=<name>] [--depth=N] [--include=ext1,ext2] [--exclude=dir1,dir2] [--watch]');
|
|
1599
|
+
console.error(' booklib connect github <releases|wiki|discussions> <owner/repo>');
|
|
1600
|
+
console.error(' booklib connect notion <page|database|search> <id-or-query>');
|
|
1601
|
+
process.exit(1);
|
|
1602
|
+
}
|
|
1603
|
+
|
|
1604
|
+
// Notion subcommand — fetch pages, databases, or search results via Notion API
|
|
1605
|
+
if (target === 'notion') {
|
|
1606
|
+
const subcommand = args[2];
|
|
1607
|
+
const targetId = args[3];
|
|
1608
|
+
|
|
1609
|
+
if (!subcommand || !targetId) {
|
|
1610
|
+
console.error('Usage: booklib connect notion <page|database|search> <id-or-query>');
|
|
1611
|
+
process.exit(1);
|
|
1612
|
+
}
|
|
1613
|
+
|
|
1614
|
+
const { NotionConnector } = await import('../lib/connectors/notion.js');
|
|
1615
|
+
const notion = new NotionConnector();
|
|
1616
|
+
|
|
1617
|
+
const auth = notion.checkAuth();
|
|
1618
|
+
if (!auth.ok) {
|
|
1619
|
+
console.error(auth.error);
|
|
1620
|
+
process.exit(1);
|
|
1621
|
+
}
|
|
1622
|
+
|
|
1623
|
+
const sourceName = parseFlag(args, 'name') ?? `notion-${subcommand}-${targetId.slice(0, 12)}`;
|
|
1624
|
+
const outputDir = path.join('.booklib', 'sources', sourceName);
|
|
1625
|
+
|
|
1626
|
+
console.log(`Fetching from Notion (${subcommand})...`);
|
|
1627
|
+
|
|
1628
|
+
let result;
|
|
1629
|
+
try {
|
|
1630
|
+
switch (subcommand) {
|
|
1631
|
+
case 'page':
|
|
1632
|
+
result = await notion.fetchPage(targetId, outputDir);
|
|
1633
|
+
break;
|
|
1634
|
+
case 'database':
|
|
1635
|
+
result = await notion.fetchDatabase(targetId, outputDir);
|
|
1636
|
+
break;
|
|
1637
|
+
case 'search':
|
|
1638
|
+
result = await notion.fetchSearch(targetId, outputDir);
|
|
1639
|
+
break;
|
|
1640
|
+
default:
|
|
1641
|
+
console.error(`Unknown subcommand: ${subcommand}. Use: page, database, search`);
|
|
1642
|
+
process.exit(1);
|
|
1643
|
+
}
|
|
1644
|
+
} catch (err) {
|
|
1645
|
+
console.error(`Notion fetch failed: ${err.message}`);
|
|
1646
|
+
process.exit(1);
|
|
1647
|
+
}
|
|
1648
|
+
|
|
1649
|
+
if (result.pageCount === 0) {
|
|
1650
|
+
console.log('No pages found.');
|
|
1651
|
+
break;
|
|
1652
|
+
}
|
|
1653
|
+
|
|
1654
|
+
console.log(`Fetched ${result.pageCount} page(s).`);
|
|
1655
|
+
|
|
1656
|
+
const { detectSourceType } = await import('../lib/engine/source-detector.js');
|
|
1657
|
+
const detected = detectSourceType(outputDir);
|
|
1658
|
+
const sourceType = parseFlag(args, 'type') ?? detected.type;
|
|
1659
|
+
|
|
1660
|
+
const { SourceManager } = await import('../lib/engine/source-manager.js');
|
|
1661
|
+
const mgr = new SourceManager(path.join(process.cwd(), '.booklib'));
|
|
1662
|
+
mgr.registerSource({ name: sourceName, sourcePath: outputDir, type: sourceType });
|
|
1663
|
+
|
|
1664
|
+
const indexer = new BookLibIndexer();
|
|
1665
|
+
await indexer.indexDirectory(outputDir, false, { sourceName });
|
|
1666
|
+
|
|
1667
|
+
console.log(`Indexed as "${sourceName}" (type: ${sourceType}).`);
|
|
1668
|
+
break;
|
|
1669
|
+
}
|
|
1670
|
+
|
|
1671
|
+
// GitHub subcommand — fetch releases, wiki, or discussions via gh CLI
|
|
1672
|
+
if (target === 'github') {
|
|
1673
|
+
const subcommand = args[2];
|
|
1674
|
+
const repo = args[3];
|
|
1675
|
+
|
|
1676
|
+
if (!subcommand || !repo) {
|
|
1677
|
+
console.error('Usage: booklib connect github <releases|wiki|discussions> <owner/repo>');
|
|
1678
|
+
process.exit(1);
|
|
1679
|
+
}
|
|
1680
|
+
|
|
1681
|
+
const { GitHubConnector } = await import('../lib/connectors/github.js');
|
|
1682
|
+
const gh = new GitHubConnector();
|
|
1683
|
+
|
|
1684
|
+
const auth = gh.checkAuth();
|
|
1685
|
+
if (!auth.ok) {
|
|
1686
|
+
console.error(auth.error);
|
|
1687
|
+
process.exit(1);
|
|
1688
|
+
}
|
|
1689
|
+
|
|
1690
|
+
const sourceName = parseFlag(args, 'name') ?? `github-${repo.replace('/', '-')}-${subcommand}`;
|
|
1691
|
+
const outputDir = path.join('.booklib', 'sources', sourceName);
|
|
1692
|
+
|
|
1693
|
+
console.log(`Fetching ${subcommand} from ${repo}...`);
|
|
1694
|
+
|
|
1695
|
+
let result;
|
|
1696
|
+
try {
|
|
1697
|
+
switch (subcommand) {
|
|
1698
|
+
case 'releases':
|
|
1699
|
+
result = await gh.fetchReleases(repo, outputDir);
|
|
1700
|
+
break;
|
|
1701
|
+
case 'wiki':
|
|
1702
|
+
result = await gh.fetchWiki(repo, outputDir);
|
|
1703
|
+
break;
|
|
1704
|
+
case 'discussions':
|
|
1705
|
+
result = await gh.fetchDiscussions(repo, outputDir);
|
|
1706
|
+
break;
|
|
1707
|
+
default:
|
|
1708
|
+
console.error(`Unknown subcommand: ${subcommand}. Use: releases, wiki, discussions`);
|
|
1709
|
+
process.exit(1);
|
|
1710
|
+
}
|
|
1711
|
+
} catch (err) {
|
|
1712
|
+
console.error(`GitHub fetch failed: ${err.message}`);
|
|
1713
|
+
process.exit(1);
|
|
1714
|
+
}
|
|
1715
|
+
|
|
1716
|
+
if (result.pageCount === 0) {
|
|
1717
|
+
console.log(`No ${subcommand} found for ${repo}.`);
|
|
1718
|
+
break;
|
|
1719
|
+
}
|
|
1720
|
+
|
|
1721
|
+
console.log(`Fetched ${result.pageCount} ${subcommand}.`);
|
|
1722
|
+
|
|
1723
|
+
const { detectSourceType } = await import('../lib/engine/source-detector.js');
|
|
1724
|
+
const detected = detectSourceType(outputDir);
|
|
1725
|
+
const sourceType = parseFlag(args, 'type') ?? detected.type;
|
|
1726
|
+
|
|
1727
|
+
const { SourceManager } = await import('../lib/engine/source-manager.js');
|
|
1728
|
+
const mgr = new SourceManager(path.join(process.cwd(), '.booklib'));
|
|
1729
|
+
mgr.registerSource({ name: sourceName, sourcePath: outputDir, type: sourceType, url: `https://github.com/${repo}` });
|
|
1730
|
+
|
|
1731
|
+
const indexer = new BookLibIndexer();
|
|
1732
|
+
await indexer.indexDirectory(outputDir, false, { sourceName });
|
|
1733
|
+
|
|
1734
|
+
console.log(`Indexed ${result.pageCount} ${subcommand} from ${repo} as "${sourceName}" (type: ${sourceType}).`);
|
|
1735
|
+
break;
|
|
1736
|
+
}
|
|
1737
|
+
|
|
1738
|
+
const isUrl = target.startsWith('http://') || target.startsWith('https://');
|
|
1739
|
+
const typeFlag = parseFlag(args, 'type');
|
|
1740
|
+
const name = parseFlag(args, 'name') ?? undefined;
|
|
1741
|
+
|
|
1742
|
+
const { SourceManager } = await import('../lib/engine/source-manager.js');
|
|
1743
|
+
const booklibDir = path.join(process.cwd(), '.booklib');
|
|
1744
|
+
const mgr = new SourceManager(booklibDir);
|
|
1745
|
+
|
|
1746
|
+
if (isUrl) {
|
|
1747
|
+
// Web connector: scrape URL into local markdown, then index
|
|
1748
|
+
const depth = parseInt(parseFlag(args, 'depth') ?? '1', 10);
|
|
1749
|
+
const { WebConnector } = await import('../lib/connectors/web.js');
|
|
1750
|
+
const wc = new WebConnector({ depth });
|
|
1751
|
+
|
|
1752
|
+
const sourceName = name ?? new URL(target).hostname.replace(/\./g, '-');
|
|
1753
|
+
const outputDir = path.join(booklibDir, 'sources', sourceName);
|
|
1754
|
+
|
|
1755
|
+
console.log(`Scraping ${target} (depth=${depth})...`);
|
|
1756
|
+
const { pageCount } = await wc.scrape(target, outputDir);
|
|
1757
|
+
console.log(`Scraped ${pageCount} page(s) to ${outputDir}`);
|
|
1758
|
+
|
|
1759
|
+
// Auto-detect source type from scraped content when --type not provided
|
|
1760
|
+
let type;
|
|
1761
|
+
if (typeFlag) {
|
|
1762
|
+
type = typeFlag;
|
|
1763
|
+
} else {
|
|
1764
|
+
const { detectSourceType } = await import('../lib/engine/source-detector.js');
|
|
1765
|
+
const detection = detectSourceType(outputDir);
|
|
1766
|
+
type = detection.type;
|
|
1767
|
+
console.log(` Detected source type: ${detection.type} (confidence: ${detection.confidence})`);
|
|
1768
|
+
}
|
|
1769
|
+
|
|
1770
|
+
let source;
|
|
1771
|
+
try {
|
|
1772
|
+
source = mgr.registerSource({ name: sourceName, sourcePath: outputDir, type, url: target });
|
|
1773
|
+
} catch (err) { console.error(err.message); process.exit(1); }
|
|
1774
|
+
console.log(`Registered source "${source.name}" (${outputDir})`);
|
|
1775
|
+
|
|
1776
|
+
// Index the scraped markdown — rollback registration on failure
|
|
1777
|
+
try {
|
|
1778
|
+
const indexer = new BookLibIndexer();
|
|
1779
|
+
console.log('Indexing source...');
|
|
1780
|
+
await indexer.indexDirectory(outputDir, false, { quiet: false, sourceName: source.name });
|
|
1781
|
+
|
|
1782
|
+
const { BM25Index: BM25 } = await import('../lib/engine/bm25-index.js');
|
|
1783
|
+
const bm25File = indexer.bm25Path;
|
|
1784
|
+
let chunkCount = 0;
|
|
1785
|
+
if (fs.existsSync(bm25File)) {
|
|
1786
|
+
const idx = BM25.load(bm25File);
|
|
1787
|
+
chunkCount = idx._docs.filter(d => d.metadata?.sourceName === source.name).length;
|
|
1788
|
+
}
|
|
1789
|
+
mgr.markIndexed(source.name, chunkCount);
|
|
1790
|
+
console.log(`Source "${source.name}" connected and indexed (${chunkCount} chunks).`);
|
|
1791
|
+
} catch (indexErr) {
|
|
1792
|
+
try { mgr.removeSource(source.name); } catch { /* best effort */ }
|
|
1793
|
+
console.error(`Indexing failed for "${source.name}": ${indexErr.message}`);
|
|
1794
|
+
console.error('Source registration rolled back.');
|
|
1795
|
+
process.exit(1);
|
|
1796
|
+
}
|
|
1797
|
+
} else {
|
|
1798
|
+
// Local path connector — with filtering, mtime tracking, and optional watch
|
|
1799
|
+
const resolvedPath = path.resolve(target);
|
|
1800
|
+
if (!fs.existsSync(resolvedPath)) {
|
|
1801
|
+
console.error(`Path does not exist: ${resolvedPath}`);
|
|
1802
|
+
process.exit(1);
|
|
1803
|
+
}
|
|
1804
|
+
|
|
1805
|
+
const includeArg = parseFlag(args, 'include');
|
|
1806
|
+
const excludeArg = parseFlag(args, 'exclude');
|
|
1807
|
+
const include = includeArg ? includeArg.split(',').map(s => s.trim()) : undefined;
|
|
1808
|
+
const exclude = excludeArg ? excludeArg.split(',').map(s => s.trim()) : undefined;
|
|
1809
|
+
|
|
1810
|
+
const { LocalConnector } = await import('../lib/connectors/local.js');
|
|
1811
|
+
const lc = new LocalConnector({ include, exclude });
|
|
1812
|
+
const matchingFiles = lc.listFiles(resolvedPath);
|
|
1813
|
+
console.log(`Found ${matchingFiles.length} file(s) matching filters.`);
|
|
1814
|
+
|
|
1815
|
+
// Auto-detect source type from directory content when --type not provided
|
|
1816
|
+
let type;
|
|
1817
|
+
if (typeFlag) {
|
|
1818
|
+
type = typeFlag;
|
|
1819
|
+
} else {
|
|
1820
|
+
const { detectSourceType } = await import('../lib/engine/source-detector.js');
|
|
1821
|
+
const detection = detectSourceType(resolvedPath);
|
|
1822
|
+
type = detection.type;
|
|
1823
|
+
console.log(` Detected source type: ${detection.type} (confidence: ${detection.confidence})`);
|
|
1824
|
+
}
|
|
1825
|
+
|
|
1826
|
+
let source;
|
|
1827
|
+
try {
|
|
1828
|
+
source = mgr.registerSource({ name, sourcePath: resolvedPath, type });
|
|
1829
|
+
} catch (err) {
|
|
1830
|
+
console.error(err.message);
|
|
1831
|
+
process.exit(1);
|
|
1832
|
+
}
|
|
1833
|
+
console.log(`Registered source "${source.name}" (${resolvedPath})`);
|
|
1834
|
+
|
|
1835
|
+
// Index the source directory — rollback registration on failure
|
|
1836
|
+
try {
|
|
1837
|
+
const indexer = new BookLibIndexer();
|
|
1838
|
+
console.log('Indexing source...');
|
|
1839
|
+
await indexer.indexDirectory(resolvedPath, false, { quiet: false, sourceName: source.name });
|
|
1840
|
+
|
|
1841
|
+
const { BM25Index: BM25 } = await import('../lib/engine/bm25-index.js');
|
|
1842
|
+
const bm25File = indexer.bm25Path;
|
|
1843
|
+
let chunkCount = 0;
|
|
1844
|
+
if (fs.existsSync(bm25File)) {
|
|
1845
|
+
const idx = BM25.load(bm25File);
|
|
1846
|
+
chunkCount = idx._docs.filter(d => d.metadata?.sourceName === source.name).length;
|
|
1847
|
+
}
|
|
1848
|
+
mgr.markIndexed(source.name, chunkCount);
|
|
1849
|
+
|
|
1850
|
+
// Store file mtimes for incremental refresh
|
|
1851
|
+
const mtimes = lc.getFileMtimes(resolvedPath);
|
|
1852
|
+
mgr.updateMtimes(source.name, mtimes);
|
|
1853
|
+
|
|
1854
|
+
console.log(`Source "${source.name}" connected and indexed (${chunkCount} chunks).`);
|
|
1855
|
+
} catch (indexErr) {
|
|
1856
|
+
try { mgr.removeSource(source.name); } catch { /* best effort */ }
|
|
1857
|
+
console.error(`Indexing failed for "${source.name}": ${indexErr.message}`);
|
|
1858
|
+
console.error('Source registration rolled back.');
|
|
1859
|
+
process.exit(1);
|
|
1860
|
+
}
|
|
1861
|
+
|
|
1862
|
+
// Optional: watch for changes and re-index
|
|
1863
|
+
if (args.includes('--watch')) {
|
|
1864
|
+
const indexer = new BookLibIndexer();
|
|
1865
|
+
const watcher = lc.watch(resolvedPath, async (eventType, filename) => {
|
|
1866
|
+
console.log(` ${eventType}: ${filename}`);
|
|
1867
|
+
await indexer.indexDirectory(resolvedPath, false, { quiet: true, sourceName: source.name });
|
|
1868
|
+
const updatedMtimes = lc.getFileMtimes(resolvedPath);
|
|
1869
|
+
mgr.updateMtimes(source.name, updatedMtimes);
|
|
1870
|
+
console.log(` Re-indexed ${source.name}`);
|
|
1871
|
+
});
|
|
1872
|
+
process.on('SIGINT', () => { watcher.close(); process.exit(0); });
|
|
1873
|
+
}
|
|
1874
|
+
}
|
|
1875
|
+
break;
|
|
1876
|
+
}
|
|
1877
|
+
|
|
1878
|
+
case 'disconnect': {
|
|
1879
|
+
const disconnectName = args[1];
|
|
1880
|
+
if (!disconnectName) {
|
|
1881
|
+
console.error('Usage: booklib disconnect <name>');
|
|
1882
|
+
process.exit(1);
|
|
1883
|
+
}
|
|
1884
|
+
const { SourceManager } = await import('../lib/engine/source-manager.js');
|
|
1885
|
+
const booklibDir = path.join(process.cwd(), '.booklib');
|
|
1886
|
+
const mgr = new SourceManager(booklibDir);
|
|
1887
|
+
|
|
1888
|
+
const source = mgr.getSource(disconnectName);
|
|
1889
|
+
if (!source) {
|
|
1890
|
+
console.error(`Source not found: "${disconnectName}". Run 'booklib sources' to see registered sources.`);
|
|
1891
|
+
process.exit(1);
|
|
1892
|
+
}
|
|
1893
|
+
|
|
1894
|
+
const indexer = new BookLibIndexer();
|
|
1895
|
+
await removeSourceChunks(disconnectName, indexer.bm25Path, indexer);
|
|
1896
|
+
|
|
1897
|
+
mgr.removeSource(disconnectName);
|
|
1898
|
+
console.log(`Source "${disconnectName}" disconnected.`);
|
|
1899
|
+
break;
|
|
1900
|
+
}
|
|
1901
|
+
|
|
1902
|
+
case 'sources': {
|
|
1903
|
+
const { SourceManager } = await import('../lib/engine/source-manager.js');
|
|
1904
|
+
const booklibDir = path.join(process.cwd(), '.booklib');
|
|
1905
|
+
const mgr = new SourceManager(booklibDir);
|
|
1906
|
+
const sources = mgr.listSources();
|
|
1907
|
+
|
|
1908
|
+
if (sources.length === 0) {
|
|
1909
|
+
console.log('No sources connected. Use `booklib connect <path>` to add one.');
|
|
1910
|
+
break;
|
|
1911
|
+
}
|
|
1912
|
+
|
|
1913
|
+
console.log(`\n ${'Name'.padEnd(20)} ${'Type'.padEnd(8)} ${'Chunks'.padEnd(8)} Path`);
|
|
1914
|
+
console.log(` ${'─'.repeat(20)} ${'─'.repeat(8)} ${'─'.repeat(8)} ${'─'.repeat(30)}`);
|
|
1915
|
+
for (const s of sources) {
|
|
1916
|
+
const chunks = s.chunk_count != null ? String(s.chunk_count) : '-';
|
|
1917
|
+
console.log(` ${s.name.padEnd(20)} ${s.type.padEnd(8)} ${chunks.padEnd(8)} ${s.sourcePath}`);
|
|
1918
|
+
}
|
|
1919
|
+
console.log();
|
|
1920
|
+
break;
|
|
1921
|
+
}
|
|
1922
|
+
|
|
1923
|
+
case 'refresh': {
|
|
1924
|
+
const refreshName = args[1];
|
|
1925
|
+
if (!refreshName) {
|
|
1926
|
+
console.error('Usage: booklib refresh <name> [--every 5m]');
|
|
1927
|
+
process.exit(1);
|
|
1928
|
+
}
|
|
1929
|
+
const { SourceManager } = await import('../lib/engine/source-manager.js');
|
|
1930
|
+
const booklibDir = path.join(process.cwd(), '.booklib');
|
|
1931
|
+
const mgr = new SourceManager(booklibDir);
|
|
1932
|
+
|
|
1933
|
+
const source = mgr.getSource(refreshName);
|
|
1934
|
+
if (!source) {
|
|
1935
|
+
console.error(`Source not found: "${refreshName}". Run 'booklib sources' to see registered sources.`);
|
|
1936
|
+
process.exit(1);
|
|
1937
|
+
}
|
|
1938
|
+
|
|
1939
|
+
if (!fs.existsSync(source.sourcePath)) {
|
|
1940
|
+
console.error(`Source path no longer exists: ${source.sourcePath}`);
|
|
1941
|
+
process.exit(1);
|
|
1942
|
+
}
|
|
1943
|
+
|
|
1944
|
+
/** Run a single refresh cycle for the source. */
|
|
1945
|
+
const runRefresh = async () => {
|
|
1946
|
+
console.log(`Refreshing source "${refreshName}" from ${source.sourcePath}...`);
|
|
1947
|
+
|
|
1948
|
+
const isLocalSource = source.type === 'local' || (!source.url && fs.statSync(source.sourcePath).isDirectory());
|
|
1949
|
+
const indexer = new BookLibIndexer();
|
|
1950
|
+
|
|
1951
|
+
if (isLocalSource) {
|
|
1952
|
+
const { LocalConnector } = await import('../lib/connectors/local.js');
|
|
1953
|
+
const lc = new LocalConnector();
|
|
1954
|
+
const previousMtimes = mgr.getMtimes(refreshName);
|
|
1955
|
+
const { changed, removed, currentMtimes } = lc.findChanges(source.sourcePath, previousMtimes);
|
|
1956
|
+
|
|
1957
|
+
if (changed.length === 0 && removed.length === 0) {
|
|
1958
|
+
console.log(`Source "${refreshName}" is up to date — no changes detected.`);
|
|
1959
|
+
return;
|
|
1960
|
+
}
|
|
1961
|
+
|
|
1962
|
+
console.log(` ${changed.length} changed, ${removed.length} removed`);
|
|
1963
|
+
|
|
1964
|
+
await removeSourceChunks(refreshName, indexer.bm25Path, indexer);
|
|
1965
|
+
await indexer.indexDirectory(source.sourcePath, false, { quiet: false, sourceName: refreshName });
|
|
1966
|
+
|
|
1967
|
+
mgr.updateMtimes(refreshName, currentMtimes);
|
|
1968
|
+
} else {
|
|
1969
|
+
await removeSourceChunks(refreshName, indexer.bm25Path, indexer);
|
|
1970
|
+
await indexer.indexDirectory(source.sourcePath, false, { quiet: false, sourceName: refreshName });
|
|
1971
|
+
}
|
|
1972
|
+
|
|
1973
|
+
let chunkCount = 0;
|
|
1974
|
+
if (fs.existsSync(indexer.bm25Path)) {
|
|
1975
|
+
const { BM25Index: BM25 } = await import('../lib/engine/bm25-index.js');
|
|
1976
|
+
const idx = BM25.load(indexer.bm25Path);
|
|
1977
|
+
chunkCount = idx._docs.filter(d => d.metadata?.sourceName === refreshName).length;
|
|
1978
|
+
}
|
|
1979
|
+
mgr.markIndexed(refreshName, chunkCount);
|
|
1980
|
+
console.log(`Source "${refreshName}" refreshed (${chunkCount} chunks).`);
|
|
1981
|
+
};
|
|
1982
|
+
|
|
1983
|
+
const every = parseFlag(args, 'every');
|
|
1984
|
+
if (every) {
|
|
1985
|
+
const ms = parseInterval(every);
|
|
1986
|
+
console.log(`Watching "${refreshName}" — refreshing every ${every} (Ctrl+C to stop)`);
|
|
1987
|
+
// Sequential loop: wait for completion before scheduling next run
|
|
1988
|
+
// Prevents overlapping refreshes and handles errors gracefully
|
|
1989
|
+
const loop = async () => {
|
|
1990
|
+
while (true) {
|
|
1991
|
+
try {
|
|
1992
|
+
await runRefresh();
|
|
1993
|
+
console.log(`Refreshed at ${new Date().toLocaleTimeString()}`);
|
|
1994
|
+
} catch (err) {
|
|
1995
|
+
console.error(`Refresh failed: ${err.message} — retrying in ${every}`);
|
|
1996
|
+
}
|
|
1997
|
+
await new Promise(r => setTimeout(r, ms));
|
|
1998
|
+
}
|
|
1999
|
+
};
|
|
2000
|
+
await loop();
|
|
2001
|
+
} else {
|
|
2002
|
+
await runRefresh();
|
|
2003
|
+
}
|
|
2004
|
+
break;
|
|
2005
|
+
}
|
|
2006
|
+
|
|
2007
|
+
case 'gaps': {
|
|
2008
|
+
const { GapDetector } = await import('../lib/engine/gap-detector.js');
|
|
2009
|
+
const detector = new GapDetector();
|
|
2010
|
+
console.log('Scanning project for knowledge gaps...\n');
|
|
2011
|
+
try {
|
|
2012
|
+
const gaps = await detector.detect(process.cwd());
|
|
2013
|
+
|
|
2014
|
+
if (gaps.postTraining.length > 0) {
|
|
2015
|
+
console.log('Post-training dependencies (model may have outdated knowledge):');
|
|
2016
|
+
for (const dep of gaps.postTraining) {
|
|
2017
|
+
const date = dep.publishDate.toISOString().split('T')[0];
|
|
2018
|
+
console.log(` ${dep.name}@${dep.version} (${dep.ecosystem}, published ${date})`);
|
|
2019
|
+
}
|
|
2020
|
+
} else {
|
|
2021
|
+
console.log('No post-training dependencies detected.');
|
|
2022
|
+
}
|
|
2023
|
+
|
|
2024
|
+
if (gaps.uncapturedDocs.length > 0) {
|
|
2025
|
+
console.log('\nUncaptured project docs:');
|
|
2026
|
+
for (const doc of gaps.uncapturedDocs) {
|
|
2027
|
+
const suffix = doc.type === 'directory' ? '/' : '';
|
|
2028
|
+
console.log(` ${doc.path} (${doc.fileCount} file(s))`);
|
|
2029
|
+
console.log(` → booklib connect ./${doc.path}${suffix} --type=team-decision`);
|
|
2030
|
+
}
|
|
2031
|
+
}
|
|
2032
|
+
|
|
2033
|
+
if (gaps.ecosystems.length > 0) {
|
|
2034
|
+
console.log(`\nScanned: ${gaps.totalDeps} dependencies across ${gaps.ecosystems.join(', ')}`);
|
|
2035
|
+
} else {
|
|
2036
|
+
console.log('\nNo dependency ecosystems detected.');
|
|
2037
|
+
}
|
|
2038
|
+
} catch (err) {
|
|
2039
|
+
console.error(`Gap detection failed: ${err.message}`);
|
|
2040
|
+
process.exit(1);
|
|
2041
|
+
}
|
|
2042
|
+
break;
|
|
2043
|
+
}
|
|
2044
|
+
|
|
2045
|
+
case 'fix':
|
|
2046
|
+
case 'resolve-gaps': {
|
|
2047
|
+
const { GapDetector } = await import('../lib/engine/gap-detector.js');
|
|
2048
|
+
const { GapResolver } = await import('../lib/engine/gap-resolver.js');
|
|
2049
|
+
|
|
2050
|
+
console.log('Scanning for gaps...');
|
|
2051
|
+
const detector = new GapDetector();
|
|
2052
|
+
const gaps = await detector.detect(process.cwd());
|
|
2053
|
+
|
|
2054
|
+
if (gaps.postTraining.length === 0) {
|
|
2055
|
+
console.log('No post-training dependencies detected.');
|
|
2056
|
+
break;
|
|
2057
|
+
}
|
|
2058
|
+
|
|
2059
|
+
console.log(`Found ${gaps.postTraining.length} post-training dep(s). Resolving...\n`);
|
|
2060
|
+
|
|
2061
|
+
const resolver = new GapResolver();
|
|
2062
|
+
const results = await resolver.resolveAll(gaps.postTraining, ({ dep, result }) => {
|
|
2063
|
+
const icon = result.resolved ? '\u2713' : '\u2717';
|
|
2064
|
+
console.log(` ${icon} ${dep.name}@${dep.version} \u2014 ${result.source} (${result.pageCount} pages)`);
|
|
2065
|
+
});
|
|
2066
|
+
|
|
2067
|
+
// Index resolved sources
|
|
2068
|
+
const resolvedResults = results.filter(r => r.result.resolved);
|
|
2069
|
+
for (const { result } of resolvedResults) {
|
|
2070
|
+
try {
|
|
2071
|
+
const { SourceManager } = await import('../lib/engine/source-manager.js');
|
|
2072
|
+
const booklibDir = path.join(process.cwd(), '.booklib');
|
|
2073
|
+
const mgr = new SourceManager(booklibDir);
|
|
2074
|
+
const { detectSourceType } = await import('../lib/engine/source-detector.js');
|
|
2075
|
+
const detected = detectSourceType(result.outputDir);
|
|
2076
|
+
mgr.registerSource({ name: result.sourceName, sourcePath: result.outputDir, type: detected.type });
|
|
2077
|
+
|
|
2078
|
+
const indexer = new BookLibIndexer();
|
|
2079
|
+
await indexer.indexDirectory(result.outputDir, false, { sourceName: result.sourceName, quiet: true });
|
|
2080
|
+
} catch (err) {
|
|
2081
|
+
console.warn(` Warning: ${result.sourceName}: ${err.message}`);
|
|
2082
|
+
}
|
|
2083
|
+
}
|
|
2084
|
+
|
|
2085
|
+
// Show suggestions for unresolved
|
|
2086
|
+
const unresolved = results.filter(r => !r.result.resolved);
|
|
2087
|
+
for (const { dep, result } of unresolved) {
|
|
2088
|
+
if (result.suggestion) {
|
|
2089
|
+
console.log(`\n ${dep.name}@${dep.version} \u2014 not resolved\n \u2192 ${result.suggestion}`);
|
|
2090
|
+
}
|
|
2091
|
+
}
|
|
2092
|
+
|
|
2093
|
+
console.log(`\nResolved: ${resolvedResults.length}/${gaps.postTraining.length}`);
|
|
2094
|
+
break;
|
|
2095
|
+
}
|
|
2096
|
+
|
|
2097
|
+
case 'analyze': {
|
|
2098
|
+
const { ProjectAnalyzer } = await import('../lib/engine/project-analyzer.js');
|
|
2099
|
+
console.log('Analyzing project...\n');
|
|
2100
|
+
try {
|
|
2101
|
+
const analyzer = new ProjectAnalyzer();
|
|
2102
|
+
const result = await analyzer.analyze(process.cwd());
|
|
2103
|
+
|
|
2104
|
+
if (result.affected.length === 0) {
|
|
2105
|
+
console.log('No post-training APIs detected in your code.');
|
|
2106
|
+
break;
|
|
2107
|
+
}
|
|
2108
|
+
|
|
2109
|
+
// Group by dep
|
|
2110
|
+
const byDep = new Map();
|
|
2111
|
+
for (const entry of result.affected) {
|
|
2112
|
+
const key = entry.dep.name;
|
|
2113
|
+
if (!byDep.has(key)) byDep.set(key, { dep: entry.dep, files: [] });
|
|
2114
|
+
byDep.get(key).files.push({ file: entry.file, apis: entry.apis });
|
|
2115
|
+
}
|
|
2116
|
+
|
|
2117
|
+
for (const [, { dep, files }] of byDep) {
|
|
2118
|
+
console.log(`\n${dep.name}@${dep.version} (post-training):`);
|
|
2119
|
+
for (const { file, apis } of files) {
|
|
2120
|
+
console.log(` ${file} \u2192 ${apis.join(', ')}`);
|
|
2121
|
+
}
|
|
2122
|
+
}
|
|
2123
|
+
|
|
2124
|
+
console.log(`\n${result.totalFiles} file(s), ${result.totalApis} post-training API(s).`);
|
|
2125
|
+
} catch (err) {
|
|
2126
|
+
console.error(`Analysis failed: ${err.message}`);
|
|
2127
|
+
process.exit(1);
|
|
2128
|
+
}
|
|
2129
|
+
break;
|
|
2130
|
+
}
|
|
2131
|
+
|
|
2132
|
+
case 'guard':
|
|
2133
|
+
case 'check-decisions': {
|
|
2134
|
+
const filePath = args[1];
|
|
2135
|
+
if (!filePath) {
|
|
2136
|
+
console.error('Usage: booklib check-decisions <file>');
|
|
2137
|
+
process.exit(1);
|
|
2138
|
+
}
|
|
2139
|
+
try {
|
|
2140
|
+
const { DecisionChecker } = await import('../lib/engine/decision-checker.js');
|
|
2141
|
+
const { BookLibSearcher } = await import('../lib/engine/searcher.js');
|
|
2142
|
+
const searcher = new BookLibSearcher();
|
|
2143
|
+
const checker = new DecisionChecker({ searcher });
|
|
2144
|
+
const result = await checker.checkFile(filePath);
|
|
2145
|
+
|
|
2146
|
+
if (result.contradictions.length > 0) {
|
|
2147
|
+
console.log(`\u26a0 ${result.contradictions.length} potential contradiction(s):\n`);
|
|
2148
|
+
for (const c of result.contradictions) {
|
|
2149
|
+
console.log(` ${c.identifier} \u2014 contradicts: ${c.source}`);
|
|
2150
|
+
console.log(` "${c.decision.slice(0, 120)}..."`);
|
|
2151
|
+
console.log('');
|
|
2152
|
+
}
|
|
2153
|
+
} else {
|
|
2154
|
+
console.log('No contradictions found.');
|
|
2155
|
+
}
|
|
2156
|
+
console.log(`Checked ${result.checked} identifier(s).`);
|
|
2157
|
+
} catch (err) {
|
|
2158
|
+
console.error(`Decision check failed: ${err.message}`);
|
|
2159
|
+
process.exit(1);
|
|
2160
|
+
}
|
|
2161
|
+
break;
|
|
2162
|
+
}
|
|
2163
|
+
|
|
2164
|
+
case 'verify':
|
|
2165
|
+
case 'check-imports': {
|
|
2166
|
+
const filePath = args[1];
|
|
2167
|
+
if (!filePath) {
|
|
2168
|
+
console.error('Usage: booklib check-imports <file>');
|
|
2169
|
+
process.exit(1);
|
|
2170
|
+
}
|
|
2171
|
+
const { ImportChecker } = await import('../lib/engine/import-checker.js');
|
|
2172
|
+
|
|
2173
|
+
let indexMode = 'manual';
|
|
2174
|
+
try {
|
|
2175
|
+
const cfgPath = path.join(process.cwd(), 'booklib.config.json');
|
|
2176
|
+
if (fs.existsSync(cfgPath)) {
|
|
2177
|
+
const cfg = JSON.parse(fs.readFileSync(cfgPath, 'utf8'));
|
|
2178
|
+
indexMode = cfg.importChecking ?? 'manual';
|
|
2179
|
+
}
|
|
2180
|
+
} catch { /* use default */ }
|
|
2181
|
+
|
|
2182
|
+
const checker = new ImportChecker({
|
|
2183
|
+
searcher: new BookLibSearcher(),
|
|
2184
|
+
indexMode,
|
|
2185
|
+
});
|
|
2186
|
+
|
|
2187
|
+
const resolved = path.resolve(filePath);
|
|
2188
|
+
console.log(`Checking imports in ${filePath}...\n`);
|
|
2189
|
+
try {
|
|
2190
|
+
const result = await checker.checkFile(resolved, process.cwd());
|
|
2191
|
+
|
|
2192
|
+
if (result.unknown.length > 0) {
|
|
2193
|
+
console.log('Unknown APIs (not in BookLib index):');
|
|
2194
|
+
for (const imp of result.unknown) {
|
|
2195
|
+
const eco = imp.language === 'js' ? 'npm' : imp.language;
|
|
2196
|
+
console.log(` ${imp.module} (${eco})`);
|
|
2197
|
+
const docs = await checker.resolveDocsUrl(imp);
|
|
2198
|
+
if (docs.url) {
|
|
2199
|
+
console.log(` \u2192 booklib connect ${docs.url} --type=framework-docs`);
|
|
2200
|
+
}
|
|
2201
|
+
}
|
|
2202
|
+
}
|
|
2203
|
+
|
|
2204
|
+
const knownCount = result.known.length;
|
|
2205
|
+
const unknownCount = result.unknown.length;
|
|
2206
|
+
const skippedCount = result.skipped.length;
|
|
2207
|
+
console.log(`\nKnown: ${knownCount} imports | Unknown: ${unknownCount} imports | Skipped: ${skippedCount} stdlib`);
|
|
2208
|
+
} catch (err) {
|
|
2209
|
+
console.error(`Import check failed: ${err.message}`);
|
|
2210
|
+
process.exit(1);
|
|
2211
|
+
}
|
|
2212
|
+
break;
|
|
2213
|
+
}
|
|
2214
|
+
|
|
2215
|
+
default: {
|
|
2216
|
+
const showAll = args.includes('--all');
|
|
2217
|
+
if (showAll) {
|
|
2218
|
+
console.log(`
|
|
2219
|
+
BookLib — AI Agent Skill Library (full reference)
|
|
2220
|
+
|
|
2221
|
+
CORE:
|
|
2222
|
+
booklib index [dir] [--clear] Build semantic index (skills + knowledge nodes)
|
|
2223
|
+
booklib search "<query>" Search skills and your knowledge nodes
|
|
2224
|
+
booklib audit <skill> <file> Deep-audit a file against a skill
|
|
2225
|
+
booklib scan [dir] [--docs] Project-wide heatmap
|
|
2226
|
+
booklib gaps Detect post-training deps & uncaptured docs
|
|
2227
|
+
booklib resolve-gaps Auto-resolve gaps via Context7/GitHub/manual
|
|
2228
|
+
booklib analyze Show which APIs in your code have post-training gaps
|
|
2229
|
+
booklib check-imports <file> Check if file imports are covered by BookLib
|
|
2230
|
+
booklib check-decisions <file> Check if code contradicts captured team decisions
|
|
2231
|
+
booklib capture --title "<title>" [--type insight] [--tags t1,t2] [--links "skill:edge-type,...]"
|
|
2232
|
+
booklib benchmark Run retrieval quality benchmark (MRR/Recall/NDCG)
|
|
2233
|
+
booklib context "<task>" [--prompt-only] Cross-skill context + conflict resolution
|
|
2234
|
+
booklib context "<task>" --file <path> Also injects graph context for the file's component
|
|
2235
|
+
|
|
2236
|
+
KNOWLEDGE GRAPH:
|
|
2237
|
+
booklib note "<title>" Create a note (pipe content via stdin, or opens editor)
|
|
2238
|
+
booklib dictate [--raw] [--title "<t>"] Type/speak rough thoughts → AI structures → note
|
|
2239
|
+
booklib research "<topic>" Create a research template node to fill in later
|
|
2240
|
+
booklib save-chat [--summarize] [--title "<t>"] Save current conversation as a knowledge node
|
|
2241
|
+
booklib component add <name> "<glob>" Define a project component (e.g. "auth" "src/auth/**")
|
|
2242
|
+
booklib link "<title-or-id>" "<title-or-id>" --type <edge-type> Connect two nodes
|
|
2243
|
+
booklib nodes list List all knowledge nodes
|
|
2244
|
+
booklib nodes show <id> View a specific node
|
|
2245
|
+
|
|
2246
|
+
Edge types: implements · contradicts · extends · applies-to · see-also · inspired-by · supersedes · depends-on
|
|
2247
|
+
|
|
2248
|
+
SKILLS:
|
|
2249
|
+
booklib init [--reset] [--tool=claude|cursor|copilot|gemini|codex|windsurf|roo-code|openhands|junie|goose|opencode|letta|all|auto] [--skills=s1,s2]
|
|
2250
|
+
[--ecc] [--agents] [--commands] [--rules[=kotlin,python]]
|
|
2251
|
+
[--orchestrator=obra|ruflo] [--dry-run]
|
|
2252
|
+
booklib setup Fetch & index all trusted community skills
|
|
2253
|
+
booklib discover [--refresh] List available community skills
|
|
2254
|
+
booklib install <skill-name> Install a skill
|
|
2255
|
+
booklib fetch <skill-name> (deprecated) Use: booklib install
|
|
2256
|
+
booklib add <skill-id-or-url> (deprecated) Use: booklib install
|
|
2257
|
+
booklib rules list|install <lang>|status Manage always-on language rules
|
|
2258
|
+
|
|
2259
|
+
SESSION HANDOFF:
|
|
2260
|
+
booklib save-state --goal=".." --next=".." Save agent context
|
|
2261
|
+
booklib resume [session-name] Resume last session
|
|
2262
|
+
booklib recover-auto Auto-recover from session or git
|
|
2263
|
+
|
|
2264
|
+
SESSION MANAGEMENT:
|
|
2265
|
+
booklib sessions cleanup --before 90days Archive old sessions
|
|
2266
|
+
booklib sessions diff <id1> <id2> Compare two sessions
|
|
2267
|
+
booklib sessions find <name> Find session (local+global)
|
|
2268
|
+
booklib sessions search <query> Search by content
|
|
2269
|
+
booklib sessions tag <id> --add=tag1,tag2 Tag sessions
|
|
2270
|
+
booklib sessions validate [id] Check quality
|
|
2271
|
+
booklib sessions report [--since "2 weeks"] Team report
|
|
2272
|
+
booklib sessions create --template=<t> <n> Create from template
|
|
2273
|
+
booklib sessions history <id> Version history
|
|
2274
|
+
|
|
2275
|
+
SOURCES:
|
|
2276
|
+
booklib connect <path> [--type=<type>] [--name=<name>] Connect a doc source
|
|
2277
|
+
booklib connect github releases <owner/repo> Index GitHub releases
|
|
2278
|
+
booklib connect github wiki <owner/repo> Index GitHub wiki pages
|
|
2279
|
+
booklib connect github discussions <owner/repo> Index GitHub discussions
|
|
2280
|
+
booklib connect notion page <page-id> Index a Notion page
|
|
2281
|
+
booklib connect notion database <database-id> Index a Notion database
|
|
2282
|
+
booklib connect notion search <query> Index Notion search results
|
|
2283
|
+
booklib disconnect <name> Disconnect a source + remove chunks
|
|
2284
|
+
booklib sources List connected sources
|
|
2285
|
+
booklib refresh <name> [--every 5m] Re-index a source (with optional polling)
|
|
2286
|
+
|
|
2287
|
+
ORCHESTRATOR COMPATIBILITY:
|
|
2288
|
+
booklib sync Sync all fetched skills → ~/.claude/skills/
|
|
2289
|
+
|
|
2290
|
+
SWARM / MULTI-AGENT:
|
|
2291
|
+
booklib profile <role>|--list Skill bundle for an agent role
|
|
2292
|
+
booklib swarm-config [trigger] Trigger → roles → skills pipeline
|
|
2293
|
+
booklib sessions-list|merge|lineage|compare Multi-agent session coordination
|
|
2294
|
+
|
|
2295
|
+
`);
|
|
2296
|
+
} else {
|
|
2297
|
+
console.log(`
|
|
2298
|
+
BookLib v3.0.0 — Context engineering for AI coding assistants
|
|
2299
|
+
|
|
2300
|
+
QUICK START:
|
|
2301
|
+
booklib init Guided setup — detects stack, registers MCP, builds index
|
|
2302
|
+
booklib analyze Show which APIs your AI doesn't know about
|
|
2303
|
+
|
|
2304
|
+
EVERYDAY USE:
|
|
2305
|
+
booklib gaps Detect post-training dependencies
|
|
2306
|
+
booklib fix Auto-resolve gaps via Context7/GitHub
|
|
2307
|
+
booklib analyze Show affected files and post-training APIs
|
|
2308
|
+
booklib search "<query>" Search skills and knowledge
|
|
2309
|
+
booklib verify <file> Flag unknown APIs (11 languages)
|
|
2310
|
+
booklib guard <file> Check code against team decisions
|
|
2311
|
+
booklib doctor Health check for skills and config
|
|
2312
|
+
|
|
2313
|
+
KNOWLEDGE:
|
|
2314
|
+
booklib remember --title "<t>" Save a team decision or insight
|
|
2315
|
+
booklib note "<title>" Create a note (pipe or editor)
|
|
2316
|
+
booklib connect <path> Index local documentation
|
|
2317
|
+
booklib connect github releases <repo> Index GitHub changelogs
|
|
2318
|
+
booklib connect notion database <id> Index Notion pages
|
|
2319
|
+
booklib sources List connected sources
|
|
2320
|
+
|
|
2321
|
+
SKILLS:
|
|
2322
|
+
booklib install <skill-name> Install a skill
|
|
2323
|
+
booklib discover Browse the community skill catalog
|
|
2324
|
+
booklib index Rebuild the search index
|
|
2325
|
+
|
|
2326
|
+
booklib --help --all Show all commands including advanced
|
|
2327
|
+
|
|
2328
|
+
`);
|
|
2329
|
+
}
|
|
2330
|
+
break;
|
|
2331
|
+
}
|
|
2332
|
+
}
|
|
2333
|
+
}
|
|
2334
|
+
|
|
2335
|
+
const NO_NUDGE_COMMANDS = new Set(['help', 'search', 'context', 'audit', 'scan', 'nodes', 'sessions', 'sessions-list']);
|
|
2336
|
+
const BOOKLIB_DIR = path.join(os.homedir(), '.booklib');
|
|
2337
|
+
|
|
2338
|
+
function readCounter(file) {
|
|
2339
|
+
try { return parseInt(fs.readFileSync(file, 'utf8'), 10) || 0; } catch { return 0; }
|
|
2340
|
+
}
|
|
2341
|
+
function writeCounter(file, value) {
|
|
2342
|
+
fs.mkdirSync(BOOKLIB_DIR, { recursive: true });
|
|
2343
|
+
fs.writeFileSync(file, String(value));
|
|
2344
|
+
}
|
|
2345
|
+
|
|
2346
|
+
async function maybeAskFeedback() {
|
|
2347
|
+
// Only in interactive terminals, only on action commands
|
|
2348
|
+
if (!process.stderr.isTTY || !command || NO_NUDGE_COMMANDS.has(command) || args.includes('--help')) return;
|
|
2349
|
+
|
|
2350
|
+
const FEEDBACK_EVERY = 25;
|
|
2351
|
+
const counterFile = path.join(BOOKLIB_DIR, 'feedback-count');
|
|
2352
|
+
const count = readCounter(counterFile);
|
|
2353
|
+
const next = count + 1;
|
|
2354
|
+
writeCounter(counterFile, next);
|
|
2355
|
+
if (next % FEEDBACK_EVERY !== 0) return;
|
|
2356
|
+
|
|
2357
|
+
return new Promise(resolve => {
|
|
2358
|
+
const rl = createInterface({ input: process.stdin, output: process.stderr });
|
|
2359
|
+
rl.question('\n Quick question: is BookLib useful to you? [y/n/skip] ', answer => {
|
|
2360
|
+
rl.close();
|
|
2361
|
+
const a = answer.trim().toLowerCase();
|
|
2362
|
+
if (a === 'y' || a === 'yes') {
|
|
2363
|
+
console.error(' Glad to hear it! A ⭐ helps others find it: https://github.com/booklib-ai/booklib\n');
|
|
2364
|
+
} else if (a === 'n' || a === 'no') {
|
|
2365
|
+
console.error(' Thanks for the honesty. Tell us what\'s missing: https://github.com/booklib-ai/booklib/issues\n');
|
|
2366
|
+
}
|
|
2367
|
+
resolve();
|
|
2368
|
+
});
|
|
2369
|
+
});
|
|
2370
|
+
}
|
|
2371
|
+
|
|
2372
|
+
function maybeNudgeStar() {
|
|
2373
|
+
if (!command || NO_NUDGE_COMMANDS.has(command) || args.includes('--help')) return;
|
|
2374
|
+
const NUDGE_EVERY = 50;
|
|
2375
|
+
const counterFile = path.join(BOOKLIB_DIR, 'nudge-count');
|
|
2376
|
+
try {
|
|
2377
|
+
const next = readCounter(counterFile) + 1;
|
|
2378
|
+
writeCounter(counterFile, next);
|
|
2379
|
+
if (next % NUDGE_EVERY === 0) {
|
|
2380
|
+
console.error('\n ⭐ If BookLib is useful, a star helps: https://github.com/booklib-ai/booklib\n');
|
|
2381
|
+
}
|
|
2382
|
+
} catch {
|
|
2383
|
+
// never block the CLI for a nudge
|
|
2384
|
+
}
|
|
2385
|
+
}
|
|
2386
|
+
|
|
2387
|
+
main()
|
|
2388
|
+
.then(() => maybeAskFeedback())
|
|
2389
|
+
.then(() => maybeNudgeStar())
|
|
2390
|
+
.catch(err => {
|
|
2391
|
+
console.error(err.message);
|
|
2392
|
+
console.error('\n If this looks like a bug, please report it: https://github.com/booklib-ai/booklib/issues\n');
|
|
2393
|
+
process.exit(1);
|
|
2394
|
+
});
|