cc-md-search-cli 1.0.3 → 1.0.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (46) hide show
  1. package/CHANGELOG.md +39 -0
  2. package/CONFIGURATION.md +113 -12
  3. package/README.md +28 -11
  4. package/bun.lock +94 -2
  5. package/package.json +8 -2
  6. package/skills/ccmds-config.md +5 -3
  7. package/src/cache/cache.js +166 -0
  8. package/src/cache/index.js +14 -0
  9. package/src/cli.js +222 -1142
  10. package/src/config/constants.js +63 -0
  11. package/src/config/directories.js +91 -0
  12. package/src/config/generator.js +29 -0
  13. package/src/config/index.js +9 -0
  14. package/src/config/loader.js +143 -0
  15. package/src/config/path-format.js +26 -0
  16. package/src/files/discovery.js +111 -0
  17. package/src/files/glob.js +116 -0
  18. package/src/files/index.js +6 -0
  19. package/src/index-persistence/flexsearch-index.js +571 -0
  20. package/src/index-persistence/index.js +20 -0
  21. package/src/index.js +89 -0
  22. package/src/init/config-builder.js +135 -0
  23. package/src/init/index.js +105 -0
  24. package/src/init/prompts.js +337 -0
  25. package/src/init/ui.js +168 -0
  26. package/src/init/validators.js +226 -0
  27. package/src/output/formatter.js +116 -0
  28. package/src/output/index.js +5 -0
  29. package/src/parsing/context.js +126 -0
  30. package/src/parsing/headings.js +72 -0
  31. package/src/parsing/index.js +8 -0
  32. package/src/parsing/markdown.js +41 -0
  33. package/src/parsing/sections.js +42 -0
  34. package/src/search/fuzzy.js +489 -0
  35. package/src/search/grep.js +80 -0
  36. package/src/search/index.js +11 -0
  37. package/src/version/index.js +5 -0
  38. package/src/version/update.js +44 -0
  39. package/tests/cli-commands.test.js +1 -1
  40. package/tests/cli-config-commands.test.js +30 -6
  41. package/tests/cross-runtime.test.js +7 -108
  42. package/tests/index-persistence.test.js +501 -0
  43. package/tests/init-config-builder.test.js +176 -0
  44. package/tests/init-validators.test.js +228 -0
  45. package/tests/integration.test.js +4 -4
  46. package/tests/search-fuzzy.test.js +53 -43
package/CHANGELOG.md CHANGED
@@ -2,6 +2,44 @@
2
2
 
3
3
  All notable changes to this project will be documented in this file.
4
4
 
5
+ ## [1.0.4] - 2026-01-23
6
+
7
+ ### Added
8
+
9
+ - **Interactive `init` wizard** - `ccmds init` now runs an interactive configuration wizard in TTY environments
10
+ - Prompts for document directories with option to create non-existent paths
11
+ - Named directory support with optional descriptions
12
+ - Common exclude pattern presets via checkbox selection
13
+ - Output mode selection
14
+ - Advanced options: result limit, fuzzy threshold, file extensions, caching
15
+ - **`--no-interactive` flag** - Skip the wizard and use non-interactive mode
16
+ - **`update` command** - `ccmds update` to check for and install latest version
17
+ - **Version check on `--version`** - Shows update availability when checking version
18
+ - **New dependencies** - @inquirer/prompts, chalk, ora for terminal UI
19
+ - **Context-aware previews for `find`** - Previews now show the paragraph or code block where the search term appears, instead of the beginning of the file
20
+ - Uses smart boundary detection (blank lines, headings, code fences)
21
+ - Preserves complete code blocks when match is in code
22
+ - Shows full bullet lists and paragraphs for text matches
23
+ - Falls back to description/file start if no body match found
24
+ - Configurable via `preview.maxLines` (default: 20) to limit context size
25
+ - **Extended query operators** - `'term` for exact substring match, `!term` for exclusion, space-separated terms for AND search
26
+ - **`--clear-cache` flag** - Clear search index cache before running `find` or `grep` commands
27
+
28
+ ### Changed
29
+
30
+ - **Minimal config generation** - Generated configs only include values that differ from defaults
31
+ - **Exclude patterns prompt** - Now asks "Add exclude patterns?" first (default: No) before showing the list
32
+ - **README quickstart** - Simplified to use `ccmds init` interactive wizard
33
+ - **Replaced Fuse.js with FlexSearch** - New search engine with forward tokenization and resolution-based ranking
34
+ - **Index storage format** - Changed from `.ccmds-fuse-index.json` + `.ccmds-fuse-index-meta.json` files to `.ccmds-flexsearch/` directory structure
35
+ - **Search index version** - Bumped to version 5 for automatic cache invalidation
36
+ - **Default index path** - Changed from `.ccmds-fuse-index.json` to `.ccmds-flexsearch/`
37
+
38
+ ### Removed
39
+
40
+ - **Fuse.js dependency** - Replaced by FlexSearch
41
+ - **Fuse.js-specific config options** - `ignoreLocation`, `ignoreFieldNorm`, `distance` no longer apply
42
+
5
43
  ## [1.0.3] - 2026-01-22
6
44
 
7
45
  ### Added
@@ -99,6 +137,7 @@ All notable changes to this project will be documented in this file.
99
137
  - Configurable search options (depth, limit, threshold)
100
138
  - Claude Code skill integration (`ccmds`)
101
139
 
140
+ [1.0.4]: https://github.com/bjeber/cc-md-search-cli/compare/v1.0.3...v1.0.4
102
141
  [1.0.3]: https://github.com/bjeber/cc-md-search-cli/compare/v1.0.2...v1.0.3
103
142
  [1.0.2]: https://github.com/bjeber/cc-md-search-cli/compare/v1.0.1...v1.0.2
104
143
  [1.0.1]: https://github.com/bjeber/cc-md-search-cli/compare/v1.0.0...v1.0.1
package/CONFIGURATION.md CHANGED
@@ -23,17 +23,32 @@ ccmds find "authentication"
23
23
 
24
24
  ### Creating a Config File
25
25
 
26
+ The `init` command creates a `.ccmdsrc` configuration file. By default, it runs an **interactive wizard** when in a terminal (TTY).
27
+
26
28
  ```bash
27
- # Create with default settings
28
- ccmds init
29
+ # Interactive mode (default in terminal)
30
+ ccmds init # Guides you through configuration options
29
31
 
30
- # Create with custom directories
31
- ccmds init -d ./docs ./wiki
32
+ # Non-interactive mode
33
+ ccmds init --no-interactive # Creates config with defaults
34
+ ccmds init -d ./docs ./wiki # Pre-fills directories, still interactive
35
+ ccmds init --no-interactive -d ./docs # Fully non-interactive
32
36
 
33
37
  # Overwrite existing config
34
- ccmds init --force
38
+ ccmds init --force # Works with both interactive/non-interactive
35
39
  ```
36
40
 
41
+ **Interactive mode** prompts for:
42
+ 1. Document directories (with option to create non-existent dirs)
43
+ 2. Exclude patterns (common presets + custom)
44
+ 3. Output mode
45
+ 4. Advanced options (limit, fuzzy threshold, extensions, caching)
46
+
47
+ **Non-interactive mode** is used automatically when:
48
+ - Running in CI/CD pipelines (no TTY)
49
+ - Using `--no-interactive` flag
50
+ - Piping input/output
51
+
37
52
  ### File Locations
38
53
 
39
54
  The CLI looks for config files in this order:
@@ -73,6 +88,7 @@ The CLI looks for config files in this order:
73
88
  }
74
89
  },
75
90
  "preview": {
91
+ "maxLines": 20,
76
92
  "topResults": 600,
77
93
  "midResults": 300,
78
94
  "otherResults": 150
@@ -93,15 +109,19 @@ The CLI looks for config files in this order:
93
109
  | `limit` | `number` | `10` | Default result limit for find |
94
110
  | `fuzzy.threshold` | `number` | `0.4` | Fuzzy match threshold (0=exact, 1=loose) |
95
111
  | `fuzzy.weights` | `object` | See above | Field weights for search scoring |
96
- | `preview.topResults` | `number` | `600` | Preview chars for results 1-3 |
97
- | `preview.midResults` | `number` | `300` | Preview chars for results 4-7 |
98
- | `preview.otherResults` | `number` | `150` | Preview chars for remaining results |
112
+ | `preview.maxLines` | `number` | `20` | Max lines for context-aware previews |
113
+ | `preview.topResults` | `number` | `600` | Fallback preview chars for results 1-3 |
114
+ | `preview.midResults` | `number` | `300` | Fallback preview chars for results 4-7 |
115
+ | `preview.otherResults` | `number` | `150` | Fallback preview chars for remaining |
99
116
  | `frontmatterFields` | `string[]` | See above | Frontmatter fields to include |
100
117
  | `extensions` | `string[]` | `[".md", ".markdown"]` | File extensions to search |
101
118
  | `aliases` | `object` | `{}` | Command shortcuts (planned) |
102
119
  | `cache.enabled` | `boolean` | `false` | Enable result caching |
103
120
  | `cache.ttl` | `number` | `300` | Cache expiration in seconds |
104
121
  | `cache.maxEntries` | `number` | `50` | Maximum cached queries |
122
+ | `index.enabled` | `boolean` | `true` | Enable FlexSearch index caching |
123
+ | `index.path` | `string` | `".ccmds-flexsearch/"` | Index directory location |
124
+ | `index.autoRebuild` | `boolean` | `true` | Auto-rebuild when files change |
105
125
 
106
126
  ---
107
127
 
@@ -382,6 +402,85 @@ Cache is stored in `.ccmds-cache.json` in the project root.
382
402
 
383
403
  ---
384
404
 
405
+ ## Search Index (Performance)
406
+
407
+ The CLI automatically caches the FlexSearch index to dramatically improve search performance (60-80% faster on subsequent searches).
408
+
409
+ ### How It Works
410
+
411
+ 1. **First search**: Builds index, saves to `.ccmds-flexsearch/` directory
412
+ 2. **Subsequent searches**: Loads cached index if files haven't changed
413
+ 3. **Auto-invalidation**: Detects file changes via mtime hashes
414
+
415
+ ### Configuration
416
+
417
+ ```json
418
+ {
419
+ "index": {
420
+ "enabled": true,
421
+ "path": ".ccmds-flexsearch/",
422
+ "autoRebuild": true
423
+ }
424
+ }
425
+ ```
426
+
427
+ | Option | Default | Description |
428
+ |--------|---------|-------------|
429
+ | `enabled` | `true` | Enable index caching |
430
+ | `path` | `".ccmds-flexsearch/"` | Index directory location |
431
+ | `autoRebuild` | `true` | Automatically rebuild when files change |
432
+
433
+ ### Index Commands
434
+
435
+ ```bash
436
+ # View index statistics
437
+ ccmds index stats
438
+
439
+ # Clear cached index
440
+ ccmds index clear
441
+
442
+ # Force rebuild index
443
+ ccmds index rebuild
444
+ ```
445
+
446
+ ### Force Rebuild on Search
447
+
448
+ ```bash
449
+ ccmds find "query" --rebuild-index
450
+ ```
451
+
452
+ ### Clear Cache Before Search
453
+
454
+ ```bash
455
+ ccmds find "query" --clear-cache
456
+ ```
457
+
458
+ ### Query Operators
459
+
460
+ FlexSearch supports extended query operators:
461
+
462
+ | Operator | Example | Description |
463
+ |----------|---------|-------------|
464
+ | (default) | `auth` | Fuzzy/prefix match |
465
+ | `'term` | `'authentication` | Exact substring match |
466
+ | `!term` | `!deprecated` | Exclude results containing term |
467
+ | space | `auth api` | AND search (both terms required) |
468
+
469
+ ### Files Created
470
+
471
+ - `.ccmds-flexsearch/` - FlexSearch index directory containing:
472
+ - `meta.json` - Metadata with version, timestamp, and file hashes
473
+ - Multiple `.json` segment files - Indexed document data
474
+
475
+ Add to `.gitignore`:
476
+
477
+ ```
478
+ .ccmds-flexsearch/
479
+ .ccmds-cache.json
480
+ ```
481
+
482
+ ---
483
+
385
484
  ## Context Efficiency Features
386
485
 
387
486
  The CLI includes optimizations to reduce AI context usage by 30-50%:
@@ -392,10 +491,12 @@ The CLI includes optimizations to reduce AI context usage by 30-50%:
392
491
  - **Heading paths** - Shows `## Setup > ### Prerequisites` for each match
393
492
  - **Deduplication** - Overlapping matches are merged
394
493
 
395
- ### Adaptive Previews (find)
396
- - Top 3 results: 600 characters (configurable via `preview.topResults`)
397
- - Results 4-7: 300 characters (configurable via `preview.midResults`)
398
- - Remaining: 150 characters (configurable via `preview.otherResults`)
494
+ ### Context-Aware Previews (find)
495
+ - **Shows the actual paragraph or code block** where the search term appears
496
+ - Uses smart boundary detection (blank lines, headings, code fences)
497
+ - Preserves complete code blocks when match is inside one
498
+ - Configurable line limit via `preview.maxLines` (default: 20)
499
+ - Falls back to file start when no body match (uses `preview.topResults`/`midResults`/`otherResults`)
399
500
 
400
501
  ### Frontmatter Filtering
401
502
  Only includes useful fields by default: `title`, `description`, `tags`, `category`, `summary`, `keywords`
package/README.md CHANGED
@@ -34,18 +34,10 @@ npm i -g cc-md-search-cli
34
34
 
35
35
  ```bash
36
36
  cd your-project
37
- ccmds init -d ./docs # Creates .ccmdsrc config file
38
- ```
39
-
40
- This creates a `.ccmdsrc` file with `documentDirectories` pointing to your docs folder. **This is the key setting** - it tells ccmds where your documentation lives:
41
-
42
- ```json
43
- {
44
- "documentDirectories": ["./docs"]
45
- }
37
+ ccmds init
46
38
  ```
47
39
 
48
- Adjust the path to match your project's documentation location (e.g., `./documentation`, `./wiki`, or multiple directories).
40
+ This runs an interactive wizard that creates a `.ccmdsrc` configuration file in the current directory. It guides you through setting up your documentation directories and search preferences.
49
41
 
50
42
  #### Project-Level Agent Instructions
51
43
 
@@ -133,6 +125,7 @@ The AI uses these commands behind the scenes when you ask questions like:
133
125
  | `ccmds docs` | List all configured documentations |
134
126
  | `ccmds config` | Show current configuration |
135
127
  | `ccmds init` | Create configuration file |
128
+ | `ccmds update` | Update to latest version |
136
129
 
137
130
  **Common options:**
138
131
 
@@ -148,7 +141,9 @@ See `ccmds --help` or `ccmds <command> --help` for full options.
148
141
  Create a `.ccmdsrc` file in your project root:
149
142
 
150
143
  ```bash
151
- ccmds init
144
+ ccmds init # Interactive wizard (in terminal)
145
+ ccmds init -d ./docs # Pre-fill directories
146
+ ccmds init --no-interactive # Skip wizard, use defaults
152
147
  ```
153
148
 
154
149
  Example configuration:
@@ -209,6 +204,28 @@ bun install
209
204
  npm link
210
205
  ```
211
206
 
207
+ ## Updating
208
+
209
+ Check for and install updates:
210
+
211
+ ```bash
212
+ # Check if update available
213
+ ccmds update --check
214
+
215
+ # Update to latest version
216
+ ccmds update
217
+ ```
218
+
219
+ Or update manually:
220
+
221
+ ```bash
222
+ # Using Bun
223
+ bun update -g cc-md-search-cli
224
+
225
+ # Using npm
226
+ npm update -g cc-md-search-cli
227
+ ```
228
+
212
229
  ## AI Assistant Integration
213
230
 
214
231
  ### Claude Code
package/bun.lock CHANGED
@@ -5,35 +5,127 @@
5
5
  "": {
6
6
  "name": "cc-md-search-cli",
7
7
  "dependencies": {
8
+ "@inquirer/prompts": "^8.2.0",
9
+ "chalk": "^5.6.2",
8
10
  "commander": "^11.1.0",
9
- "fuse.js": "^7.0.0",
11
+ "flexsearch": "^0.8.0",
10
12
  "gray-matter": "^4.0.3",
13
+ "ora": "^9.1.0",
14
+ },
15
+ "devDependencies": {
16
+ "@inquirer/testing": "^3.0.4",
11
17
  },
12
18
  },
13
19
  },
14
20
  "packages": {
21
+ "@inquirer/ansi": ["@inquirer/ansi@2.0.3", "", {}, "sha512-g44zhR3NIKVs0zUesa4iMzExmZpLUdTLRMCStqX3GE5NT6VkPcxQGJ+uC8tDgBUC/vB1rUhUd55cOf++4NZcmw=="],
22
+
23
+ "@inquirer/checkbox": ["@inquirer/checkbox@5.0.4", "", { "dependencies": { "@inquirer/ansi": "^2.0.3", "@inquirer/core": "^11.1.1", "@inquirer/figures": "^2.0.3", "@inquirer/type": "^4.0.3" }, "peerDependencies": { "@types/node": ">=18" }, "optionalPeers": ["@types/node"] }, "sha512-DrAMU3YBGMUAp6ArwTIp/25CNDtDbxk7UjIrrtM25JVVrlVYlVzHh5HR1BDFu9JMyUoZ4ZanzeaHqNDttf3gVg=="],
24
+
25
+ "@inquirer/confirm": ["@inquirer/confirm@6.0.4", "", { "dependencies": { "@inquirer/core": "^11.1.1", "@inquirer/type": "^4.0.3" }, "peerDependencies": { "@types/node": ">=18" }, "optionalPeers": ["@types/node"] }, "sha512-WdaPe7foUnoGYvXzH4jp4wH/3l+dBhZ3uwhKjXjwdrq5tEIFaANxj6zrGHxLdsIA0yKM0kFPVcEalOZXBB5ISA=="],
26
+
27
+ "@inquirer/core": ["@inquirer/core@11.1.1", "", { "dependencies": { "@inquirer/ansi": "^2.0.3", "@inquirer/figures": "^2.0.3", "@inquirer/type": "^4.0.3", "cli-width": "^4.1.0", "mute-stream": "^3.0.0", "signal-exit": "^4.1.0", "wrap-ansi": "^9.0.2" }, "peerDependencies": { "@types/node": ">=18" }, "optionalPeers": ["@types/node"] }, "sha512-hV9o15UxX46OyQAtaoMqAOxGR8RVl1aZtDx1jHbCtSJy1tBdTfKxLPKf7utsE4cRy4tcmCQ4+vdV+ca+oNxqNA=="],
28
+
29
+ "@inquirer/editor": ["@inquirer/editor@5.0.4", "", { "dependencies": { "@inquirer/core": "^11.1.1", "@inquirer/external-editor": "^2.0.3", "@inquirer/type": "^4.0.3" }, "peerDependencies": { "@types/node": ">=18" }, "optionalPeers": ["@types/node"] }, "sha512-QI3Jfqcv6UO2/VJaEFONH8Im1ll++Xn/AJTBn9Xf+qx2M+H8KZAdQ5sAe2vtYlo+mLW+d7JaMJB4qWtK4BG3pw=="],
30
+
31
+ "@inquirer/expand": ["@inquirer/expand@5.0.4", "", { "dependencies": { "@inquirer/core": "^11.1.1", "@inquirer/type": "^4.0.3" }, "peerDependencies": { "@types/node": ">=18" }, "optionalPeers": ["@types/node"] }, "sha512-0I/16YwPPP0Co7a5MsomlZLpch48NzYfToyqYAOWtBmaXSB80RiNQ1J+0xx2eG+Wfxt0nHtpEWSRr6CzNVnOGg=="],
32
+
33
+ "@inquirer/external-editor": ["@inquirer/external-editor@2.0.3", "", { "dependencies": { "chardet": "^2.1.1", "iconv-lite": "^0.7.2" }, "peerDependencies": { "@types/node": ">=18" }, "optionalPeers": ["@types/node"] }, "sha512-LgyI7Agbda74/cL5MvA88iDpvdXI2KuMBCGRkbCl2Dg1vzHeOgs+s0SDcXV7b+WZJrv2+ERpWSM65Fpi9VfY3w=="],
34
+
35
+ "@inquirer/figures": ["@inquirer/figures@2.0.3", "", {}, "sha512-y09iGt3JKoOCBQ3w4YrSJdokcD8ciSlMIWsD+auPu+OZpfxLuyz+gICAQ6GCBOmJJt4KEQGHuZSVff2jiNOy7g=="],
36
+
37
+ "@inquirer/input": ["@inquirer/input@5.0.4", "", { "dependencies": { "@inquirer/core": "^11.1.1", "@inquirer/type": "^4.0.3" }, "peerDependencies": { "@types/node": ">=18" }, "optionalPeers": ["@types/node"] }, "sha512-4B3s3jvTREDFvXWit92Yc6jF1RJMDy2VpSqKtm4We2oVU65YOh2szY5/G14h4fHlyQdpUmazU5MPCFZPRJ0AOw=="],
38
+
39
+ "@inquirer/number": ["@inquirer/number@4.0.4", "", { "dependencies": { "@inquirer/core": "^11.1.1", "@inquirer/type": "^4.0.3" }, "peerDependencies": { "@types/node": ">=18" }, "optionalPeers": ["@types/node"] }, "sha512-CmMp9LF5HwE+G/xWsC333TlCzYYbXMkcADkKzcawh49fg2a1ryLc7JL1NJYYt1lJ+8f4slikNjJM9TEL/AljYQ=="],
40
+
41
+ "@inquirer/password": ["@inquirer/password@5.0.4", "", { "dependencies": { "@inquirer/ansi": "^2.0.3", "@inquirer/core": "^11.1.1", "@inquirer/type": "^4.0.3" }, "peerDependencies": { "@types/node": ">=18" }, "optionalPeers": ["@types/node"] }, "sha512-ZCEPyVYvHK4W4p2Gy6sTp9nqsdHQCfiPXIP9LbJVW4yCinnxL/dDDmPaEZVysGrj8vxVReRnpfS2fOeODe9zjg=="],
42
+
43
+ "@inquirer/prompts": ["@inquirer/prompts@8.2.0", "", { "dependencies": { "@inquirer/checkbox": "^5.0.4", "@inquirer/confirm": "^6.0.4", "@inquirer/editor": "^5.0.4", "@inquirer/expand": "^5.0.4", "@inquirer/input": "^5.0.4", "@inquirer/number": "^4.0.4", "@inquirer/password": "^5.0.4", "@inquirer/rawlist": "^5.2.0", "@inquirer/search": "^4.1.0", "@inquirer/select": "^5.0.4" }, "peerDependencies": { "@types/node": ">=18" }, "optionalPeers": ["@types/node"] }, "sha512-rqTzOprAj55a27jctS3vhvDDJzYXsr33WXTjODgVOru21NvBo9yIgLIAf7SBdSV0WERVly3dR6TWyp7ZHkvKFA=="],
44
+
45
+ "@inquirer/rawlist": ["@inquirer/rawlist@5.2.0", "", { "dependencies": { "@inquirer/core": "^11.1.1", "@inquirer/type": "^4.0.3" }, "peerDependencies": { "@types/node": ">=18" }, "optionalPeers": ["@types/node"] }, "sha512-CciqGoOUMrFo6HxvOtU5uL8fkjCmzyeB6fG7O1vdVAZVSopUBYECOwevDBlqNLyyYmzpm2Gsn/7nLrpruy9RFg=="],
46
+
47
+ "@inquirer/search": ["@inquirer/search@4.1.0", "", { "dependencies": { "@inquirer/core": "^11.1.1", "@inquirer/figures": "^2.0.3", "@inquirer/type": "^4.0.3" }, "peerDependencies": { "@types/node": ">=18" }, "optionalPeers": ["@types/node"] }, "sha512-EAzemfiP4IFvIuWnrHpgZs9lAhWDA0GM3l9F4t4mTQ22IFtzfrk8xbkMLcAN7gmVML9O/i+Hzu8yOUyAaL6BKA=="],
48
+
49
+ "@inquirer/select": ["@inquirer/select@5.0.4", "", { "dependencies": { "@inquirer/ansi": "^2.0.3", "@inquirer/core": "^11.1.1", "@inquirer/figures": "^2.0.3", "@inquirer/type": "^4.0.3" }, "peerDependencies": { "@types/node": ">=18" }, "optionalPeers": ["@types/node"] }, "sha512-s8KoGpPYMEQ6WXc0dT9blX2NtIulMdLOO3LA1UKOiv7KFWzlJ6eLkEYTDBIi+JkyKXyn8t/CD6TinxGjyLt57g=="],
50
+
51
+ "@inquirer/testing": ["@inquirer/testing@3.0.4", "", { "dependencies": { "@inquirer/type": "^4.0.3", "mute-stream": "^3.0.0" }, "peerDependencies": { "@types/node": ">=18" }, "optionalPeers": ["@types/node"] }, "sha512-iSx1+r44QsBWD0o/a3xIM8EoXcyVNncBhZ7cofHbGNtHfJk5X16Ne/gNNYVb0jdYbENSCMH0deWjgJ5P2cjJRg=="],
52
+
53
+ "@inquirer/type": ["@inquirer/type@4.0.3", "", { "peerDependencies": { "@types/node": ">=18" }, "optionalPeers": ["@types/node"] }, "sha512-cKZN7qcXOpj1h+1eTTcGDVLaBIHNMT1Rz9JqJP5MnEJ0JhgVWllx7H/tahUp5YEK1qaByH2Itb8wLG/iScD5kw=="],
54
+
55
+ "ansi-regex": ["ansi-regex@6.2.2", "", {}, "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg=="],
56
+
57
+ "ansi-styles": ["ansi-styles@6.2.3", "", {}, "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg=="],
58
+
15
59
  "argparse": ["argparse@1.0.10", "", { "dependencies": { "sprintf-js": "~1.0.2" } }, "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg=="],
16
60
 
61
+ "chalk": ["chalk@5.6.2", "", {}, "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA=="],
62
+
63
+ "chardet": ["chardet@2.1.1", "", {}, "sha512-PsezH1rqdV9VvyNhxxOW32/d75r01NY7TQCmOqomRo15ZSOKbpTFVsfjghxo6JloQUCGnH4k1LGu0R4yCLlWQQ=="],
64
+
65
+ "cli-cursor": ["cli-cursor@5.0.0", "", { "dependencies": { "restore-cursor": "^5.0.0" } }, "sha512-aCj4O5wKyszjMmDT4tZj93kxyydN/K5zPWSCe6/0AV/AA1pqe5ZBIw0a2ZfPQV7lL5/yb5HsUreJ6UFAF1tEQw=="],
66
+
67
+ "cli-spinners": ["cli-spinners@3.4.0", "", {}, "sha512-bXfOC4QcT1tKXGorxL3wbJm6XJPDqEnij2gQ2m7ESQuE+/z9YFIWnl/5RpTiKWbMq3EVKR4fRLJGn6DVfu0mpw=="],
68
+
69
+ "cli-width": ["cli-width@4.1.0", "", {}, "sha512-ouuZd4/dm2Sw5Gmqy6bGyNNNe1qt9RpmxveLSO7KcgsTnU7RXfsw+/bukWGo1abgBiMAic068rclZsO4IWmmxQ=="],
70
+
17
71
  "commander": ["commander@11.1.0", "", {}, "sha512-yPVavfyCcRhmorC7rWlkHn15b4wDVgVmBA7kV4QVBsF7kv/9TKJAbAXVTxvTnwP8HHKjRCJDClKbciiYS7p0DQ=="],
18
72
 
73
+ "emoji-regex": ["emoji-regex@10.6.0", "", {}, "sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A=="],
74
+
19
75
  "esprima": ["esprima@4.0.1", "", { "bin": { "esparse": "./bin/esparse.js", "esvalidate": "./bin/esvalidate.js" } }, "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A=="],
20
76
 
21
77
  "extend-shallow": ["extend-shallow@2.0.1", "", { "dependencies": { "is-extendable": "^0.1.0" } }, "sha512-zCnTtlxNoAiDc3gqY2aYAWFx7XWWiasuF2K8Me5WbN8otHKTUKBwjPtNpRs/rbUZm7KxWAaNj7P1a/p52GbVug=="],
22
78
 
23
- "fuse.js": ["fuse.js@7.1.0", "", {}, "sha512-trLf4SzuuUxfusZADLINj+dE8clK1frKdmqiJNb1Es75fmI5oY6X2mxLVUciLLjxqw/xr72Dhy+lER6dGd02FQ=="],
79
+ "flexsearch": ["flexsearch@0.8.212", "", {}, "sha512-wSyJr1GUWoOOIISRu+X2IXiOcVfg9qqBRyCPRUdLMIGJqPzMo+jMRlvE83t14v1j0dRMEaBbER/adQjp6Du2pw=="],
80
+
81
+ "get-east-asian-width": ["get-east-asian-width@1.4.0", "", {}, "sha512-QZjmEOC+IT1uk6Rx0sX22V6uHWVwbdbxf1faPqJ1QhLdGgsRGCZoyaQBm/piRdJy/D2um6hM1UP7ZEeQ4EkP+Q=="],
24
82
 
25
83
  "gray-matter": ["gray-matter@4.0.3", "", { "dependencies": { "js-yaml": "^3.13.1", "kind-of": "^6.0.2", "section-matter": "^1.0.0", "strip-bom-string": "^1.0.0" } }, "sha512-5v6yZd4JK3eMI3FqqCouswVqwugaA9r4dNZB1wwcmrD02QkV5H0y7XBQW8QwQqEaZY1pM9aqORSORhJRdNK44Q=="],
26
84
 
85
+ "iconv-lite": ["iconv-lite@0.7.2", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" } }, "sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw=="],
86
+
27
87
  "is-extendable": ["is-extendable@0.1.1", "", {}, "sha512-5BMULNob1vgFX6EjQw5izWDxrecWK9AM72rugNr0TFldMOi0fj6Jk+zeKIt0xGj4cEfQIJth4w3OKWOJ4f+AFw=="],
28
88
 
89
+ "is-interactive": ["is-interactive@2.0.0", "", {}, "sha512-qP1vozQRI+BMOPcjFzrjXuQvdak2pHNUMZoeG2eRbiSqyvbEf/wQtEOTOX1guk6E3t36RkaqiSt8A/6YElNxLQ=="],
90
+
91
+ "is-unicode-supported": ["is-unicode-supported@2.1.0", "", {}, "sha512-mE00Gnza5EEB3Ds0HfMyllZzbBrmLOX3vfWoj9A9PEnTfratQ/BcaJOuMhnkhjXvb2+FkY3VuHqtAGpTPmglFQ=="],
92
+
29
93
  "js-yaml": ["js-yaml@3.14.2", "", { "dependencies": { "argparse": "^1.0.7", "esprima": "^4.0.0" }, "bin": { "js-yaml": "bin/js-yaml.js" } }, "sha512-PMSmkqxr106Xa156c2M265Z+FTrPl+oxd/rgOQy2tijQeK5TxQ43psO1ZCwhVOSdnn+RzkzlRz/eY4BgJBYVpg=="],
30
94
 
31
95
  "kind-of": ["kind-of@6.0.3", "", {}, "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw=="],
32
96
 
97
+ "log-symbols": ["log-symbols@7.0.1", "", { "dependencies": { "is-unicode-supported": "^2.0.0", "yoctocolors": "^2.1.1" } }, "sha512-ja1E3yCr9i/0hmBVaM0bfwDjnGy8I/s6PP4DFp+yP+a+mrHO4Rm7DtmnqROTUkHIkqffC84YY7AeqX6oFk0WFg=="],
98
+
99
+ "mimic-function": ["mimic-function@5.0.1", "", {}, "sha512-VP79XUPxV2CigYP3jWwAUFSku2aKqBH7uTAapFWCBqutsbmDo96KY5o8uh6U+/YSIn5OxJnXp73beVkpqMIGhA=="],
100
+
101
+ "mute-stream": ["mute-stream@3.0.0", "", {}, "sha512-dkEJPVvun4FryqBmZ5KhDo0K9iDXAwn08tMLDinNdRBNPcYEDiWYysLcc6k3mjTMlbP9KyylvRpd4wFtwrT9rw=="],
102
+
103
+ "onetime": ["onetime@7.0.0", "", { "dependencies": { "mimic-function": "^5.0.0" } }, "sha512-VXJjc87FScF88uafS3JllDgvAm+c/Slfz06lorj2uAY34rlUu0Nt+v8wreiImcrgAjjIHp1rXpTDlLOGw29WwQ=="],
104
+
105
+ "ora": ["ora@9.1.0", "", { "dependencies": { "chalk": "^5.6.2", "cli-cursor": "^5.0.0", "cli-spinners": "^3.2.0", "is-interactive": "^2.0.0", "is-unicode-supported": "^2.1.0", "log-symbols": "^7.0.1", "stdin-discarder": "^0.2.2", "string-width": "^8.1.0" } }, "sha512-53uuLsXHOAJl5zLrUrzY9/kE+uIFEx7iaH4g2BIJQK4LZjY4LpCCYZVKDWIkL+F01wAaCg93duQ1whnK/AmY1A=="],
106
+
107
+ "restore-cursor": ["restore-cursor@5.1.0", "", { "dependencies": { "onetime": "^7.0.0", "signal-exit": "^4.1.0" } }, "sha512-oMA2dcrw6u0YfxJQXm342bFKX/E4sG9rbTzO9ptUcR/e8A33cHuvStiYOwH7fszkZlZ1z/ta9AAoPk2F4qIOHA=="],
108
+
109
+ "safer-buffer": ["safer-buffer@2.1.2", "", {}, "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="],
110
+
33
111
  "section-matter": ["section-matter@1.0.0", "", { "dependencies": { "extend-shallow": "^2.0.1", "kind-of": "^6.0.0" } }, "sha512-vfD3pmTzGpufjScBh50YHKzEu2lxBWhVEHsNGoEXmCmn2hKGfeNLYMzCJpe8cD7gqX7TJluOVpBkAequ6dgMmA=="],
34
112
 
113
+ "signal-exit": ["signal-exit@4.1.0", "", {}, "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw=="],
114
+
35
115
  "sprintf-js": ["sprintf-js@1.0.3", "", {}, "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g=="],
36
116
 
117
+ "stdin-discarder": ["stdin-discarder@0.2.2", "", {}, "sha512-UhDfHmA92YAlNnCfhmq0VeNL5bDbiZGg7sZ2IvPsXubGkiNa9EC+tUTsjBRsYUAz87btI6/1wf4XoVvQ3uRnmQ=="],
118
+
119
+ "string-width": ["string-width@8.1.0", "", { "dependencies": { "get-east-asian-width": "^1.3.0", "strip-ansi": "^7.1.0" } }, "sha512-Kxl3KJGb/gxkaUMOjRsQ8IrXiGW75O4E3RPjFIINOVH8AMl2SQ/yWdTzWwF3FevIX9LcMAjJW+GRwAlAbTSXdg=="],
120
+
121
+ "strip-ansi": ["strip-ansi@7.1.2", "", { "dependencies": { "ansi-regex": "^6.0.1" } }, "sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA=="],
122
+
37
123
  "strip-bom-string": ["strip-bom-string@1.0.0", "", {}, "sha512-uCC2VHvQRYu+lMh4My/sFNmF2klFymLX1wHJeXnbEJERpV/ZsVuonzerjfrGpIGF7LBVa1O7i9kjiWvJiFck8g=="],
124
+
125
+ "wrap-ansi": ["wrap-ansi@9.0.2", "", { "dependencies": { "ansi-styles": "^6.2.1", "string-width": "^7.0.0", "strip-ansi": "^7.1.0" } }, "sha512-42AtmgqjV+X1VpdOfyTGOYRi0/zsoLqtXQckTmqTeybT+BDIbM/Guxo7x3pE2vtpr1ok6xRqM9OpBe+Jyoqyww=="],
126
+
127
+ "yoctocolors": ["yoctocolors@2.1.2", "", {}, "sha512-CzhO+pFNo8ajLM2d2IW/R93ipy99LWjtwblvC1RsoSUMZgyLbYFr221TnSNT7GjGdYui6P459mw9JH/g/zW2ug=="],
128
+
129
+ "wrap-ansi/string-width": ["string-width@7.2.0", "", { "dependencies": { "emoji-regex": "^10.3.0", "get-east-asian-width": "^1.0.0", "strip-ansi": "^7.1.0" } }, "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ=="],
38
130
  }
39
131
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cc-md-search-cli",
3
- "version": "1.0.3",
3
+ "version": "1.0.4",
4
4
  "description": "Claude Code Markdown Search - Efficient documentation search CLI for Claude Code integration",
5
5
  "type": "module",
6
6
  "bin": {
@@ -32,8 +32,14 @@
32
32
  "url": "https://github.com/bjeber/cc-md-search-cli/issues"
33
33
  },
34
34
  "dependencies": {
35
+ "@inquirer/prompts": "^8.2.0",
36
+ "chalk": "^5.6.2",
35
37
  "commander": "^11.1.0",
38
+ "flexsearch": "^0.8.0",
36
39
  "gray-matter": "^4.0.3",
37
- "fuse.js": "^7.0.0"
40
+ "ora": "^9.1.0"
41
+ },
42
+ "devDependencies": {
43
+ "@inquirer/testing": "^3.0.4"
38
44
  }
39
45
  }
@@ -52,6 +52,7 @@ The CLI looks for config files in this order:
52
52
  }
53
53
  },
54
54
  "preview": {
55
+ "maxLines": 20,
55
56
  "topResults": 600,
56
57
  "midResults": 300,
57
58
  "otherResults": 150
@@ -73,9 +74,10 @@ The CLI looks for config files in this order:
73
74
  | `limit` | `number` | `10` | Default result limit |
74
75
  | `fuzzy.threshold` | `number` | `0.4` | Match threshold (0=exact, 1=loose) |
75
76
  | `fuzzy.weights` | `object` | See above | Field weights for scoring |
76
- | `preview.topResults` | `number` | `600` | Preview chars for top 3 results |
77
- | `preview.midResults` | `number` | `300` | Preview chars for results 4-7 |
78
- | `preview.otherResults` | `number` | `150` | Preview chars for remaining |
77
+ | `preview.maxLines` | `number` | `20` | Max lines for context-aware previews |
78
+ | `preview.topResults` | `number` | `600` | Fallback preview chars for top 3 results |
79
+ | `preview.midResults` | `number` | `300` | Fallback preview chars for results 4-7 |
80
+ | `preview.otherResults` | `number` | `150` | Fallback preview chars for remaining |
79
81
  | `frontmatterFields` | `string[]` | See above | Frontmatter fields to include |
80
82
  | `extensions` | `string[]` | `[".md", ".markdown"]` | File extensions |
81
83
  | `cache.enabled` | `boolean` | `false` | Enable result caching |
@@ -0,0 +1,166 @@
1
+ /**
2
+ * Search result cache system
3
+ */
4
+
5
+ import { readFileSync, writeFileSync, existsSync, unlinkSync } from 'fs';
6
+ import { join } from 'path';
7
+ import { createHash } from 'crypto';
8
+
9
+ const CACHE_FILE = '.ccmds-cache.json';
10
+ const CACHE_VERSION = 1;
11
+
12
+ /**
13
+ * Generate a cache key from command and options
14
+ * @param {string} command - Command name
15
+ * @param {object} params - Parameters to include in key
16
+ * @returns {string} - Cache key hash
17
+ */
18
+ export function generateCacheKey(command, params) {
19
+ const keyData = JSON.stringify({ command, ...params });
20
+ return createHash('md5').update(keyData).digest('hex').substring(0, 12);
21
+ }
22
+
23
+ /**
24
+ * Get cache file path (in project root or config directory)
25
+ * @param {object} config - Configuration object
26
+ * @returns {string} - Cache file path
27
+ */
28
+ export function getCacheFilePath(config) {
29
+ const baseDir = config._configDir || process.cwd();
30
+ return join(baseDir, CACHE_FILE);
31
+ }
32
+
33
+ /**
34
+ * Read cache from file
35
+ * @param {string} cachePath - Path to cache file
36
+ * @returns {object} - Cache data or empty cache structure
37
+ */
38
+ export function readCache(cachePath) {
39
+ try {
40
+ if (existsSync(cachePath)) {
41
+ const data = JSON.parse(readFileSync(cachePath, 'utf-8'));
42
+ if (data.version === CACHE_VERSION) {
43
+ return data;
44
+ }
45
+ }
46
+ } catch (err) {
47
+ // Ignore corrupted cache
48
+ }
49
+ return { version: CACHE_VERSION, entries: {} };
50
+ }
51
+
52
+ /**
53
+ * Write cache to file
54
+ * @param {string} cachePath - Path to cache file
55
+ * @param {object} cache - Cache data
56
+ */
57
+ export function writeCache(cachePath, cache) {
58
+ try {
59
+ writeFileSync(cachePath, JSON.stringify(cache));
60
+ } catch (err) {
61
+ // Ignore write errors
62
+ }
63
+ }
64
+
65
+ /**
66
+ * Get cached result if valid
67
+ * @param {object} config - Configuration object
68
+ * @param {string} cacheKey - Cache key
69
+ * @returns {object|null} - Cached results or null
70
+ */
71
+ export function getCachedResult(config, cacheKey) {
72
+ if (!config.cache?.enabled) return null;
73
+
74
+ const cachePath = getCacheFilePath(config);
75
+ const cache = readCache(cachePath);
76
+ const entry = cache.entries[cacheKey];
77
+
78
+ if (!entry) return null;
79
+
80
+ // Check TTL
81
+ const ttl = config.cache.ttl || 300;
82
+ const age = (Date.now() - entry.created) / 1000;
83
+ if (age > ttl) {
84
+ // Expired - remove entry
85
+ delete cache.entries[cacheKey];
86
+ writeCache(cachePath, cache);
87
+ return null;
88
+ }
89
+
90
+ return entry.results;
91
+ }
92
+
93
+ /**
94
+ * Store result in cache
95
+ * @param {object} config - Configuration object
96
+ * @param {string} cacheKey - Cache key
97
+ * @param {string} command - Command name
98
+ * @param {any} results - Results to cache
99
+ */
100
+ export function setCachedResult(config, cacheKey, command, results) {
101
+ if (!config.cache?.enabled) return;
102
+
103
+ const cachePath = getCacheFilePath(config);
104
+ const cache = readCache(cachePath);
105
+
106
+ // Add new entry
107
+ cache.entries[cacheKey] = {
108
+ created: Date.now(),
109
+ command,
110
+ results,
111
+ };
112
+
113
+ // Trim old entries if over limit
114
+ const maxEntries = config.cache.maxEntries || 50;
115
+ const entries = Object.entries(cache.entries);
116
+ if (entries.length > maxEntries) {
117
+ // Sort by created time, remove oldest
118
+ entries.sort((a, b) => a[1].created - b[1].created);
119
+ const toRemove = entries.slice(0, entries.length - maxEntries);
120
+ toRemove.forEach(([key]) => delete cache.entries[key]);
121
+ }
122
+
123
+ writeCache(cachePath, cache);
124
+ }
125
+
126
+ /**
127
+ * Clear all cache entries
128
+ * @param {object} config - Configuration object
129
+ * @returns {number} - Number of entries cleared
130
+ */
131
+ export function clearCache(config) {
132
+ const cachePath = getCacheFilePath(config);
133
+ if (existsSync(cachePath)) {
134
+ const cache = readCache(cachePath);
135
+ const count = Object.keys(cache.entries).length;
136
+ unlinkSync(cachePath);
137
+ return count;
138
+ }
139
+ return 0;
140
+ }
141
+
142
+ /**
143
+ * Get cache statistics
144
+ * @param {object} config - Configuration object
145
+ * @returns {object} - Cache stats
146
+ */
147
+ export function getCacheStats(config) {
148
+ const cachePath = getCacheFilePath(config);
149
+ const cache = readCache(cachePath);
150
+ const entries = Object.values(cache.entries);
151
+ const now = Date.now();
152
+ const ttl = (config.cache?.ttl || 300) * 1000;
153
+
154
+ return {
155
+ enabled: config.cache?.enabled || false,
156
+ path: cachePath,
157
+ exists: existsSync(cachePath),
158
+ totalEntries: entries.length,
159
+ validEntries: entries.filter((e) => now - e.created < ttl).length,
160
+ expiredEntries: entries.filter((e) => now - e.created >= ttl).length,
161
+ commands: entries.reduce((acc, e) => {
162
+ acc[e.command] = (acc[e.command] || 0) + 1;
163
+ return acc;
164
+ }, {}),
165
+ };
166
+ }
@@ -0,0 +1,14 @@
1
+ /**
2
+ * Cache module exports
3
+ */
4
+
5
+ export {
6
+ generateCacheKey,
7
+ getCacheFilePath,
8
+ readCache,
9
+ writeCache,
10
+ getCachedResult,
11
+ setCachedResult,
12
+ clearCache,
13
+ getCacheStats,
14
+ } from './cache.js';