mdsel 0.1.2 → 0.1.3

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 (3) hide show
  1. package/README.md +92 -109
  2. package/dist/cli.mjs +349 -14
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -10,38 +10,30 @@ mdsel parses Markdown documents into semantic trees and exposes machine-addressa
10
10
 
11
11
  ```bash
12
12
  $ mdsel README.md
13
- h1 mdsel
14
- h2 Demo
15
- h2 Installation
16
- h2 Quick Start
17
- h2 Commands
18
- h3 index
19
- h3 select
20
- h2 Selectors
21
- h3 Syntax
22
- h3 Node Types
23
- h3 Index Syntax
24
- h3 Examples
25
- h3 Index Semantics
26
- h2 Output Format
27
- h2 Error Handling
28
- h2 Development
29
- h2 License
13
+ h1.0 mdsel
14
+ h2.0 Demo
15
+ h2.1 Installation
16
+ h2.2 Quick Start
17
+ h2.3 Usage
18
+ h3.0 Index (files only)
19
+ h3.1 Select (files + selectors)
20
+ h3.2 Search (fuzzy matching)
21
+ h2.4 Selectors
22
+ h2.5 Output Format
23
+ h2.6 Error Handling
24
+ h2.7 Development
25
+ h2.8 License
30
26
  ---
31
- code:19 para:23 list:5 table:3
27
+ code:29 para:29 list:5 table:4
32
28
  ```
33
29
 
34
30
  **2. Select specific content by selector:**
35
31
 
36
32
  ```bash
37
- $ mdsel README.md h2.1
38
- ```
39
- ```
33
+ $ mdsel h2.1 README.md
40
34
  ## Installation
41
35
 
42
- ​```bash
43
36
  npm install -g mdsel
44
- ​```
45
37
 
46
38
  **Requirements**: Node.js >=18.0.0
47
39
  ```
@@ -49,9 +41,7 @@ npm install -g mdsel
49
41
  **3. Drill into nested content:**
50
42
 
51
43
  ```bash
52
- $ mdsel README.md h2.1/code.0
53
- ```
54
- ```
44
+ $ mdsel "h2.1/code.0" README.md
55
45
  npm install -g mdsel
56
46
  ```
57
47
 
@@ -66,143 +56,134 @@ npm install -g mdsel
66
56
  ## Quick Start
67
57
 
68
58
  ```bash
69
- # Index a document to discover available selectors
59
+ # Index a document to see its structure
70
60
  mdsel README.md
71
61
 
72
- # Select a specific heading (shorthand)
73
- mdsel README.md h1.0
62
+ # Select a specific section by index
63
+ mdsel h2.1 README.md
64
+
65
+ # Select the entire document
66
+ mdsel '*' README.md
74
67
 
75
- # Select the first code block under a heading
76
- mdsel README.md "h2.0/code.0"
68
+ # Select a nested element (first code block under second h2)
69
+ mdsel "h2.1/code.0" README.md
77
70
 
78
- # Select multiple headings with comma syntax
79
- mdsel README.md h2.0,2
71
+ # Select multiple sections at once
72
+ mdsel h2.0 h2.1 README.md
80
73
 
81
- # Select multiple selectors at once
82
- mdsel README.md h2.0 h2.1 code.0
74
+ # Select a range of sections
75
+ mdsel h2.0-2 README.md
83
76
 
84
- # Limit output to first 10 lines
85
- mdsel README.md "h2.0?head=10"
77
+ # Fuzzy search when you don't know the exact selector
78
+ mdsel "installation" README.md
86
79
 
87
- # Limit output to last 5 lines
88
- mdsel README.md "h2.0?tail=5"
80
+ # Limit output to first N lines
81
+ mdsel "h2.0?head=10" README.md
89
82
 
90
- # Use JSON output for programmatic consumption
83
+ # JSON output for programmatic use
91
84
  mdsel --json README.md
92
85
  ```
93
86
 
94
87
  ## Usage
95
88
 
96
- mdsel automatically detects whether arguments are files or selectors:
97
-
98
89
  ```bash
99
- mdsel <files...> [selectors...]
100
90
  mdsel [options] <files...> [selectors...]
101
91
  ```
102
92
 
93
+ Arguments are auto-detected: `.md` files and existing paths are files, everything else is a selector.
94
+
103
95
  **Options**:
104
96
  - `--json` - Output JSON instead of text
97
+ - `--help` - Show help
105
98
 
106
99
  ### Index (files only)
107
100
 
108
- When only files are provided, mdsel outputs the document structure:
101
+ When only files are provided, outputs the document structure:
109
102
 
110
103
  ```bash
111
104
  mdsel README.md
112
- mdsel README.md docs/API.md
113
- mdsel --json README.md
114
105
  ```
115
106
 
116
- **Text Output** (default):
117
107
  ```
118
108
  h1.0 mdsel
119
- h2.0 Installation
120
- h2.1 Quick Start
121
- h2.2 Usage
122
- h3.0 Index
123
- h3.1 Select
109
+ h2.0 Demo
110
+ h2.1 Installation
111
+ h2.2 Quick Start
112
+ h2.3 Usage
113
+ h3.0 Index (files only)
114
+ h3.1 Select (files + selectors)
115
+ h3.2 Search (fuzzy matching)
124
116
  ---
125
- code:19 para:23 list:5 table:3
117
+ code:29 para:29 list:5 table:4
126
118
  ```
127
119
 
128
- **JSON Output** (`--json` flag):
129
- ```json
130
- {
131
- "success": true,
132
- "command": "index",
133
- "timestamp": "2025-01-15T10:30:00.000Z",
134
- "data": {
135
- "documents": [
136
- {
137
- "namespace": "readme",
138
- "file_path": "README.md",
139
- "headings": [...],
140
- "blocks": {
141
- "paragraphs": 5,
142
- "code_blocks": 2,
143
- "lists": 1,
144
- "tables": 0,
145
- "blockquotes": 0
146
- }
147
- }
148
- ],
149
- "summary": {
150
- "total_documents": 1,
151
- "total_nodes": 8,
152
- "total_selectors": 8
153
- }
154
- }
155
- }
156
- ```
120
+ The index shows:
121
+ - Heading hierarchy with selectors (e.g., `h1.0`, `h2.0`)
122
+ - Indentation reflecting document structure
123
+ - Block counts for code, paragraphs, lists, tables
157
124
 
158
125
  ### Select (files + selectors)
159
126
 
160
- When both files and selectors are provided, mdsel retrieves matching content:
127
+ When selectors are provided, retrieves matching content:
161
128
 
162
129
  ```bash
163
- # Single selector
164
- mdsel README.md h2.0
165
-
166
- # Multiple selectors
167
- mdsel README.md h2.0 h2.1 code.0
168
-
169
- # Cross-document selection
170
- mdsel README.md GUIDE.md h1.0
171
-
172
- # With query parameters
173
- mdsel README.md "h2.0?head=10"
174
-
175
- # Range selection
176
- mdsel README.md h2.1-3
130
+ # Single result - content only
131
+ mdsel h2.1 README.md
132
+ ```
177
133
 
178
- # Comma list selection
179
- mdsel README.md h2.0,2,4
180
134
  ```
135
+ ## Installation
181
136
 
182
- **Text Output** (default):
137
+ npm install -g mdsel
138
+
139
+ **Requirements**: Node.js >=18.0.0
183
140
  ```
184
- ## Quick Start
185
141
 
186
- To get started...
142
+ ```bash
143
+ # Multiple results - prefixed with selector
144
+ mdsel h2.0 h2.1 README.md
187
145
  ```
188
146
 
189
- **Multiple Results** (selector prefix):
190
147
  ```
191
148
  heading:h2.0:
192
- ## Installation
193
- content...
149
+ ## Demo
150
+ ...
194
151
  heading:h2.1:
195
- ## Quick Start
196
- content...
152
+ ## Installation
153
+ ...
154
+ ```
155
+
156
+ ```bash
157
+ # Errors show suggestions
158
+ mdsel h2.99 README.md
197
159
  ```
198
160
 
199
- **Error Output**:
200
161
  ```
201
162
  !h2.99
202
- Index out of range: document has 3 h2 headings
163
+ Index out of range: document has 9 h2 headings
203
164
  ~h2.0 ~h2.1 ~h2.2
204
165
  ```
205
166
 
167
+ ### Search (fuzzy matching)
168
+
169
+ When input doesn't look like a selector, mdsel performs fuzzy search:
170
+
171
+ ```bash
172
+ mdsel "installation" README.md
173
+ ```
174
+
175
+ ```
176
+ Search results for "installation":
177
+
178
+ readme::h2.1 (100% match)
179
+ Installation
180
+
181
+ readme::code.9 (74% match)
182
+ ## Installation npm install -g mdsel ...
183
+ ```
184
+
185
+ Search returns selectors you can use directly to fetch the content.
186
+
206
187
  ## Selectors
207
188
 
208
189
  Selectors are path-based, ordinal, stateless, and deterministic. They resemble CSS/XPath conceptually but are purpose-built for Markdown.
@@ -223,6 +204,7 @@ Selectors are path-based, ordinal, stateless, and deterministic. They resemble C
223
204
 
224
205
  | Category | Full Form | Shorthand |
225
206
  |----------|-----------|-----------|
207
+ | Wildcard | `*` | `*` |
226
208
  | Root | `root` | - |
227
209
  | Headings | `heading:h1` ... `heading:h6` | `h1` ... `h6` |
228
210
  | Sections | `section` | - |
@@ -248,6 +230,7 @@ Two equivalent notations are supported:
248
230
 
249
231
  **Basic selection**:
250
232
  ```bash
233
+ * # Entire document (wildcard)
251
234
  root # Document root
252
235
  h1.0 # First h1 heading
253
236
  h2.1 # Second h2 heading
package/dist/cli.mjs CHANGED
@@ -855,6 +855,10 @@ function tokenize(input) {
855
855
  advance();
856
856
  addToken("EQUALS" /* EQUALS */, "=", pos);
857
857
  continue;
858
+ case "*":
859
+ advance();
860
+ addToken("STAR" /* STAR */, "*", pos);
861
+ continue;
858
862
  case '"':
859
863
  case "'": {
860
864
  const strPos = position();
@@ -976,7 +980,9 @@ var Parser = class {
976
980
  let nodeType;
977
981
  let subtype;
978
982
  let index;
979
- if (this.match("ROOT" /* ROOT */)) {
983
+ if (this.match("STAR" /* STAR */)) {
984
+ nodeType = "all";
985
+ } else if (this.match("ROOT" /* ROOT */)) {
980
986
  nodeType = "root";
981
987
  } else if (this.match("HEADING" /* HEADING */)) {
982
988
  nodeType = "heading";
@@ -1420,6 +1426,10 @@ function resolvePathSegments(context, segments) {
1420
1426
  currentNodes = currentNodes.map(({ node, path }) => ({ node, path: [...path, node] }));
1421
1427
  continue;
1422
1428
  }
1429
+ if (segment.nodeType === "all") {
1430
+ currentNodes = currentNodes.map(({ node, path }) => ({ node, path: [...path, node] }));
1431
+ continue;
1432
+ }
1423
1433
  const currentNode = currentNodes[0]?.node;
1424
1434
  const currentPath = currentNodes[0]?.path ?? [];
1425
1435
  const matches = findMatchingChildren(currentNode, segment);
@@ -1558,6 +1568,9 @@ function segmentToStringWithIndex(segment, index) {
1558
1568
  return segStr;
1559
1569
  }
1560
1570
  function segmentToString(segment) {
1571
+ if (segment.nodeType === "all") {
1572
+ return "*";
1573
+ }
1561
1574
  let segStr = segment.nodeType;
1562
1575
  if (segment.subtype) {
1563
1576
  segStr += `:${segment.subtype}`;
@@ -1684,6 +1697,193 @@ function selectorToString2(selector) {
1684
1697
  return result;
1685
1698
  }
1686
1699
 
1700
+ // src/resolver/search.ts
1701
+ import { toString as toString2 } from "mdast-util-to-string";
1702
+ function searchDocument(tree, namespace, query, options = {}) {
1703
+ const { maxResults = 10, minScore = 0.3, includeCode = true } = options;
1704
+ const results = [];
1705
+ const normalizedQuery = query.toLowerCase().trim();
1706
+ if (!normalizedQuery) {
1707
+ return [];
1708
+ }
1709
+ const headingIndices = { 1: 0, 2: 0, 3: 0, 4: 0, 5: 0, 6: 0 };
1710
+ const blockIndices = {
1711
+ paragraph: 0,
1712
+ code: 0,
1713
+ list: 0,
1714
+ table: 0,
1715
+ blockquote: 0
1716
+ };
1717
+ for (let i = 0; i < tree.children.length; i++) {
1718
+ const node = tree.children[i];
1719
+ if (!node) continue;
1720
+ const nodeType = node.type;
1721
+ const text = toString2(node);
1722
+ const normalizedText = text.toLowerCase();
1723
+ const matchResult = calculateMatchScore(normalizedQuery, normalizedText, text);
1724
+ if (matchResult.score >= minScore) {
1725
+ let selector;
1726
+ let type;
1727
+ if (nodeType === "heading" && "depth" in node) {
1728
+ const depth = node.depth;
1729
+ const idx = headingIndices[depth] ?? 0;
1730
+ selector = `${namespace}::h${depth}.${idx}`;
1731
+ type = `heading:h${depth}`;
1732
+ headingIndices[depth] = idx + 1;
1733
+ } else if (nodeType in blockIndices) {
1734
+ const idx = blockIndices[nodeType] ?? 0;
1735
+ const shorthand = getBlockShorthand(nodeType);
1736
+ selector = `${namespace}::${shorthand}.${idx}`;
1737
+ type = nodeType;
1738
+ blockIndices[nodeType] = idx + 1;
1739
+ if (nodeType === "code" && !includeCode) {
1740
+ continue;
1741
+ }
1742
+ } else {
1743
+ continue;
1744
+ }
1745
+ results.push({
1746
+ selector,
1747
+ type,
1748
+ preview: createPreview(text, normalizedQuery, 100),
1749
+ content: text,
1750
+ score: matchResult.score,
1751
+ matchType: matchResult.matchType
1752
+ });
1753
+ } else {
1754
+ if (nodeType === "heading" && "depth" in node) {
1755
+ const depth = node.depth;
1756
+ headingIndices[depth] = (headingIndices[depth] ?? 0) + 1;
1757
+ } else if (nodeType in blockIndices) {
1758
+ blockIndices[nodeType] = (blockIndices[nodeType] ?? 0) + 1;
1759
+ }
1760
+ }
1761
+ }
1762
+ const sectionResults = searchHeadingSections(tree, namespace, normalizedQuery, minScore);
1763
+ results.push(...sectionResults);
1764
+ return results.sort((a, b) => b.score - a.score).slice(0, maxResults);
1765
+ }
1766
+ function searchHeadingSections(tree, namespace, normalizedQuery, minScore) {
1767
+ const results = [];
1768
+ const headingIndices = { 1: 0, 2: 0, 3: 0, 4: 0, 5: 0, 6: 0 };
1769
+ for (let i = 0; i < tree.children.length; i++) {
1770
+ const node = tree.children[i];
1771
+ if (!node || node.type !== "heading" || !("depth" in node)) {
1772
+ continue;
1773
+ }
1774
+ const depth = node.depth;
1775
+ const idx = headingIndices[depth] ?? 0;
1776
+ headingIndices[depth] = idx + 1;
1777
+ const sectionParts = [toString2(node)];
1778
+ for (let j = i + 1; j < tree.children.length; j++) {
1779
+ const sibling = tree.children[j];
1780
+ if (!sibling) break;
1781
+ if (sibling.type === "heading" && "depth" in sibling && sibling.depth <= depth) {
1782
+ break;
1783
+ }
1784
+ sectionParts.push(toString2(sibling));
1785
+ }
1786
+ const sectionText = sectionParts.join("\n");
1787
+ const normalizedSection = sectionText.toLowerCase();
1788
+ const matchResult = calculateMatchScore(normalizedQuery, normalizedSection, sectionText);
1789
+ if (matchResult.score >= minScore) {
1790
+ const selector = `${namespace}::h${depth}.${idx}`;
1791
+ const existing = results.find((r) => r.selector === selector);
1792
+ if (!existing) {
1793
+ results.push({
1794
+ selector,
1795
+ type: `heading:h${depth}`,
1796
+ preview: createPreview(sectionText, normalizedQuery, 100),
1797
+ content: sectionText,
1798
+ score: matchResult.score * 0.9,
1799
+ // Slightly lower score for section matches
1800
+ matchType: matchResult.matchType
1801
+ });
1802
+ }
1803
+ }
1804
+ }
1805
+ return results;
1806
+ }
1807
+ function calculateMatchScore(normalizedQuery, normalizedText, originalText) {
1808
+ if (normalizedText === normalizedQuery) {
1809
+ return { score: 1, matchType: "exact" };
1810
+ }
1811
+ if (normalizedText.includes(normalizedQuery)) {
1812
+ const coverage = normalizedQuery.length / normalizedText.length;
1813
+ const score = 0.7 + coverage * 0.25;
1814
+ return { score, matchType: "substring" };
1815
+ }
1816
+ const queryWords = normalizedQuery.split(/\s+/).filter((w) => w.length > 0);
1817
+ const textWords = normalizedText.split(/\s+/).filter((w) => w.length > 0);
1818
+ if (queryWords.length > 0) {
1819
+ let matchedWords = 0;
1820
+ for (const queryWord of queryWords) {
1821
+ for (const textWord of textWords) {
1822
+ if (textWord === queryWord) {
1823
+ matchedWords++;
1824
+ break;
1825
+ }
1826
+ if (textWord.includes(queryWord) || queryWord.includes(textWord)) {
1827
+ matchedWords += 0.7;
1828
+ break;
1829
+ }
1830
+ const distance = levenshteinDistance(queryWord, textWord);
1831
+ const maxLen = Math.max(queryWord.length, textWord.length);
1832
+ const similarity = (maxLen - distance) / maxLen;
1833
+ if (similarity > 0.7) {
1834
+ matchedWords += similarity * 0.5;
1835
+ break;
1836
+ }
1837
+ }
1838
+ }
1839
+ const wordMatchRatio = matchedWords / queryWords.length;
1840
+ if (wordMatchRatio > 0.3) {
1841
+ return { score: wordMatchRatio * 0.6, matchType: "fuzzy" };
1842
+ }
1843
+ }
1844
+ return { score: 0, matchType: "fuzzy" };
1845
+ }
1846
+ function createPreview(text, query, maxLength) {
1847
+ const normalizedText = text.toLowerCase();
1848
+ const index = normalizedText.indexOf(query);
1849
+ if (index === -1) {
1850
+ const cleaned = text.replace(/\s+/g, " ").trim();
1851
+ if (cleaned.length <= maxLength) {
1852
+ return cleaned;
1853
+ }
1854
+ return cleaned.slice(0, maxLength - 3) + "...";
1855
+ }
1856
+ const start = Math.max(0, index - Math.floor((maxLength - query.length) / 2));
1857
+ const end = Math.min(text.length, start + maxLength);
1858
+ let preview = text.slice(start, end).replace(/\s+/g, " ");
1859
+ if (start > 0) {
1860
+ preview = "..." + preview;
1861
+ }
1862
+ if (end < text.length) {
1863
+ preview = preview + "...";
1864
+ }
1865
+ return preview.trim();
1866
+ }
1867
+ function getBlockShorthand(type) {
1868
+ switch (type) {
1869
+ case "paragraph":
1870
+ return "para";
1871
+ case "blockquote":
1872
+ return "quote";
1873
+ default:
1874
+ return type;
1875
+ }
1876
+ }
1877
+ function searchMultipleDocuments(documents, query, options = {}) {
1878
+ const allResults = [];
1879
+ for (const doc of documents) {
1880
+ const docResults = searchDocument(doc.tree, doc.namespace, query, options);
1881
+ allResults.push(...docResults);
1882
+ }
1883
+ const maxResults = options.maxResults ?? 10;
1884
+ return allResults.sort((a, b) => b.score - a.score).slice(0, maxResults);
1885
+ }
1886
+
1687
1887
  // src/cli/commands/select-command.ts
1688
1888
  async function selectCommand(selector, files, options = {}) {
1689
1889
  const useJson = options.json === true;
@@ -1698,22 +1898,32 @@ async function selectCommand(selector, files, options = {}) {
1698
1898
  return;
1699
1899
  }
1700
1900
  let selectorAst;
1901
+ let parseError = null;
1701
1902
  try {
1702
1903
  selectorAst = parseSelector(selector);
1703
1904
  } catch (error) {
1704
1905
  if (error instanceof SelectorParseError) {
1705
- const errorEntry = createErrorEntry(
1706
- "INVALID_SELECTOR",
1707
- error.code,
1708
- error.message,
1709
- void 0,
1710
- selector
1711
- );
1712
- outputError2([errorEntry], useJson);
1713
- exitWithCode(ExitCode.ERROR);
1906
+ parseError = error;
1907
+ } else {
1908
+ throw error;
1909
+ }
1910
+ }
1911
+ if (parseError) {
1912
+ const looksLikeSearch = isLikelySearchQuery(selector);
1913
+ if (looksLikeSearch) {
1914
+ await performSearchFallback(selector, files, options);
1714
1915
  return;
1715
1916
  }
1716
- throw error;
1917
+ const errorEntry = createErrorEntry(
1918
+ "INVALID_SELECTOR",
1919
+ parseError.code,
1920
+ parseError.message,
1921
+ void 0,
1922
+ selector
1923
+ );
1924
+ outputError2([errorEntry], useJson);
1925
+ exitWithCode(ExitCode.ERROR);
1926
+ return;
1717
1927
  }
1718
1928
  const truncateOptions = {};
1719
1929
  if (selectorAst.queryParams) {
@@ -1945,6 +2155,121 @@ async function selectMultiCommand(selectors, files, options = {}) {
1945
2155
  }
1946
2156
  exitWithCode(allUnresolved.length > 0 ? ExitCode.ERROR : ExitCode.SUCCESS);
1947
2157
  }
2158
+ function isLikelySearchQuery(input) {
2159
+ if (!input.trim()) {
2160
+ return false;
2161
+ }
2162
+ if (input.includes("::")) {
2163
+ return false;
2164
+ }
2165
+ if (input.includes("[") && !input.includes("]") || !input.includes("[") && input.includes("]")) {
2166
+ return false;
2167
+ }
2168
+ if (/^(h[1-6]|code|para|paragraph|list|table|quote|blockquote|root|section|heading|block)[\.\[]/.test(input)) {
2169
+ return false;
2170
+ }
2171
+ if (/^(heading|block):/.test(input)) {
2172
+ return false;
2173
+ }
2174
+ return true;
2175
+ }
2176
+ async function performSearchFallback(query, files, options) {
2177
+ const useJson = options.json === true;
2178
+ const documents = [];
2179
+ const parseErrors = [];
2180
+ for (const file of files) {
2181
+ try {
2182
+ const result = await parseFile(file);
2183
+ const namespace = deriveNamespace(file);
2184
+ documents.push({
2185
+ namespace,
2186
+ tree: result.ast
2187
+ });
2188
+ } catch (error) {
2189
+ if (error instanceof ParserError) {
2190
+ parseErrors.push(
2191
+ createErrorEntry(
2192
+ error.code,
2193
+ error.code,
2194
+ error.message,
2195
+ error.filePath
2196
+ )
2197
+ );
2198
+ } else if (error instanceof Error) {
2199
+ parseErrors.push(createErrorEntry("PROCESSING_ERROR", "UNKNOWN", error.message, file));
2200
+ }
2201
+ }
2202
+ }
2203
+ if (documents.length === 0) {
2204
+ outputError2(parseErrors, useJson);
2205
+ exitWithCode(ExitCode.ERROR);
2206
+ return;
2207
+ }
2208
+ const searchResults = searchMultipleDocuments(documents, query, {
2209
+ maxResults: 10,
2210
+ minScore: 0.3
2211
+ });
2212
+ if (searchResults.length === 0) {
2213
+ const unresolved = [
2214
+ {
2215
+ selector: query,
2216
+ reason: `No matches found for search query "${query}"`,
2217
+ suggestions: []
2218
+ }
2219
+ ];
2220
+ if (useJson) {
2221
+ const response = formatSelectResponse([], unresolved);
2222
+ console.log(JSON.stringify(response));
2223
+ } else {
2224
+ console.log(formatSearchText(query, []));
2225
+ }
2226
+ exitWithCode(ExitCode.ERROR);
2227
+ return;
2228
+ }
2229
+ if (useJson) {
2230
+ const matches = formatSearchResultsAsMatches(searchResults);
2231
+ const response = {
2232
+ success: true,
2233
+ command: "search",
2234
+ query,
2235
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
2236
+ data: {
2237
+ matches,
2238
+ unresolved: []
2239
+ }
2240
+ };
2241
+ console.log(JSON.stringify(response));
2242
+ } else {
2243
+ console.log(formatSearchText(query, searchResults));
2244
+ }
2245
+ exitWithCode(ExitCode.SUCCESS);
2246
+ }
2247
+ function formatSearchResultsAsMatches(results) {
2248
+ return results.map((result) => ({
2249
+ selector: result.selector,
2250
+ type: result.type,
2251
+ content: result.content,
2252
+ truncated: false,
2253
+ children_available: [],
2254
+ search_score: result.score,
2255
+ match_type: result.matchType
2256
+ }));
2257
+ }
2258
+ function formatSearchText(query, results) {
2259
+ if (results.length === 0) {
2260
+ return `No matches found for: ${query}`;
2261
+ }
2262
+ const parts = [];
2263
+ parts.push(`Search results for "${query}":`);
2264
+ parts.push("");
2265
+ for (const result of results) {
2266
+ const score = Math.round(result.score * 100);
2267
+ parts.push(`${result.selector} (${score}% match)`);
2268
+ parts.push(` ${result.preview}`);
2269
+ parts.push("");
2270
+ }
2271
+ return parts.join("\n").trimEnd();
2272
+ }
1948
2273
 
1949
2274
  // src/cli/commands/format-command.ts
1950
2275
  function formatCommand(command, options = {}) {
@@ -2023,7 +2348,17 @@ function partitionArgs(args) {
2023
2348
  return { files, selectors };
2024
2349
  }
2025
2350
  var program = new Command();
2026
- program.name("mdsel").description(pkg.description).version(pkg.version).option("--json", "Output JSON instead of minimal text").argument("[args...]", "Files and/or selectors").action(async (args) => {
2351
+ program.name("mdsel").description(
2352
+ `${pkg.description}
2353
+
2354
+ Examples:
2355
+ mdsel README.md Index document structure
2356
+ mdsel h2.1 README.md Select second h2 section
2357
+ mdsel '*' README.md Select entire document
2358
+ mdsel "h2.1/code.0" README.md Select nested content
2359
+ mdsel "installation" README.md Fuzzy search
2360
+ mdsel --json README.md Output as JSON`
2361
+ ).version(pkg.version).option("--json", "Output JSON instead of text").argument("[args...]", "Markdown files and selectors (auto-detected)").action(async (args) => {
2027
2362
  try {
2028
2363
  const globalOpts = program.opts();
2029
2364
  if (args.length === 0) {
@@ -2057,7 +2392,7 @@ program.name("mdsel").description(pkg.description).version(pkg.version).option("
2057
2392
  program.command("format").description("Output format specification for tool descriptions").argument("[command]", "Command to describe (index, select, or omit for all)").option("--example", "Show example output instead of terse spec").action((command, options) => {
2058
2393
  formatCommand(command, options);
2059
2394
  });
2060
- program.command("index").description("Index markdown files (optional - same as just providing files)").argument("<files...>", "Markdown files to index").action(async (files) => {
2395
+ program.command("index").description("Index markdown files (alternative to: mdsel <files>)").argument("<files...>", "Markdown files to index").action(async (files) => {
2061
2396
  try {
2062
2397
  const globalOpts = program.opts();
2063
2398
  await indexCommand(files, { json: globalOpts.json });
@@ -2066,7 +2401,7 @@ program.command("index").description("Index markdown files (optional - same as j
2066
2401
  process.exit(ExitCode.ERROR);
2067
2402
  }
2068
2403
  });
2069
- program.command("select").description("Select content from markdown files (optional - same as providing files + selectors)").argument("<selector>", "Selector to match").argument("<files...>", "Markdown files to search").action(async (selector, files) => {
2404
+ program.command("select").description("Select content (alternative to: mdsel <selector> <files>)").argument("<selector>", "Selector to match").argument("<files...>", "Markdown files to search").action(async (selector, files) => {
2070
2405
  try {
2071
2406
  const globalOpts = program.opts();
2072
2407
  const selectors = splitSelectorList(selector);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mdsel",
3
- "version": "0.1.2",
3
+ "version": "0.1.3",
4
4
  "description": "Declarative Markdown semantic selection CLI for LLM agents",
5
5
  "type": "module",
6
6
  "bin": {