ctxman 3.0.0 → 3.1.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.
Files changed (223) hide show
  1. package/.ctxman/llm-profiles.json +1067 -39
  2. package/.ctxman/profiles/backend-team.json +13 -0
  3. package/.ctxman/profiles/default.json +12 -0
  4. package/.ctxman/profiles/devops-team.json +21 -0
  5. package/.ctxman/profiles/frontend-team.json +20 -0
  6. package/.ctxman/templates/bug-fix.json +17 -0
  7. package/.ctxman/templates/code-review.json +9 -0
  8. package/.ctxman/templates/documentation.json +9 -0
  9. package/.ctxman/templates/feature.json +9 -0
  10. package/.ctxman/templates/full-context.json +9 -0
  11. package/.ctxman/templates/refactoring.json +18 -0
  12. package/.ctxman/templates/testing.json +9 -0
  13. package/.ctxman/wizard-profiles/security-audit/profile.json +7 -1
  14. package/LICENSE +21 -0
  15. package/README.md +380 -136
  16. package/bin/cli.js +1880 -529
  17. package/bin/cm-ask.js +18 -13
  18. package/bin/cm-audit.js +124 -0
  19. package/bin/cm-count.js +151 -0
  20. package/bin/cm-gitingest.js +128 -115
  21. package/bin/cm-map.js +242 -0
  22. package/bin/cm-mcp.js +293 -0
  23. package/bin/cm-pack.js +405 -0
  24. package/bin/cm-policy.js +170 -0
  25. package/bin/cm-pr.js +248 -0
  26. package/bin/cm-search.js +178 -0
  27. package/bin/cm-update.js +128 -128
  28. package/bin/cm-why.js +197 -0
  29. package/bin/mcp-server.js +8 -24
  30. package/ctxman.js +237 -183
  31. package/examples/README.md +19 -4
  32. package/examples/github-actions/ctxman-check.yml +49 -0
  33. package/examples/wizard-profiles/security-audit/profile.json +7 -1
  34. package/index.d.ts +1422 -0
  35. package/index.js +55 -31
  36. package/lib/agents/pr-failure-agent.js +48 -45
  37. package/lib/analyzers/ai-suggester.js +451 -0
  38. package/lib/analyzers/dependency-scanner.js +505 -0
  39. package/lib/analyzers/duplicate-detector.js +264 -0
  40. package/lib/analyzers/go-method-analyzer.js +151 -110
  41. package/lib/analyzers/import-tracker.js +431 -0
  42. package/lib/analyzers/method-analyzer.js +1055 -434
  43. package/lib/analyzers/performance-dashboard.js +683 -0
  44. package/lib/analyzers/token-calculator.js +895 -504
  45. package/lib/analyzers/unused-exports-detector.js +336 -0
  46. package/lib/api/mcp/install.js +424 -0
  47. package/lib/api/mcp/prompts.js +281 -281
  48. package/lib/api/mcp/resources.js +300 -241
  49. package/lib/api/mcp/sandbox.js +89 -0
  50. package/lib/api/mcp/server.js +675 -312
  51. package/lib/api/mcp/start.js +125 -0
  52. package/lib/api/mcp/transports/HttpSseTransport.js +60 -52
  53. package/lib/api/rest/server.js +105 -28
  54. package/lib/cache/CacheManager.js +9 -17
  55. package/lib/core/Analyzer.js +16 -10
  56. package/lib/core/ContextBuilder.js +19 -28
  57. package/lib/core/Reporter.js +15 -14
  58. package/lib/core/Scanner.js +81 -12
  59. package/lib/count/index.js +100 -0
  60. package/lib/formatters/format-registry.js +394 -372
  61. package/lib/formatters/gitingest-formatter.js +656 -621
  62. package/lib/formatters/toon-formatter-v1.3.js +637 -632
  63. package/lib/formatters/toon-formatter.js +239 -240
  64. package/lib/integrations/git/BlameTracker.js +4 -4
  65. package/lib/integrations/git/DiffAnalyzer.js +38 -31
  66. package/lib/integrations/git/GitClient.js +71 -53
  67. package/lib/lsp/server.js +311 -0
  68. package/lib/map/graph.js +299 -0
  69. package/lib/map/imports.js +233 -0
  70. package/lib/map/index.js +33 -0
  71. package/lib/map/rank.js +253 -0
  72. package/lib/map/render.js +346 -0
  73. package/lib/map/resolver.js +574 -0
  74. package/lib/pack/budget.js +111 -0
  75. package/lib/pack/code-lexer.js +360 -0
  76. package/lib/pack/compress.js +414 -0
  77. package/lib/pack/index.js +354 -0
  78. package/lib/pack/renderers/common.js +134 -0
  79. package/lib/pack/renderers/index.js +24 -0
  80. package/lib/pack/renderers/json.js +47 -0
  81. package/lib/pack/renderers/markdown.js +54 -0
  82. package/lib/pack/renderers/plain.js +38 -0
  83. package/lib/pack/renderers/xml.js +85 -0
  84. package/lib/pack/select.js +177 -0
  85. package/lib/pack/split.js +182 -0
  86. package/lib/pack/tree.js +35 -0
  87. package/lib/parsers/gitignore-parser.js +113 -107
  88. package/lib/parsers/method-filter-parser.js +50 -33
  89. package/lib/plugins/ExporterPlugin.js +3 -3
  90. package/lib/plugins/LanguagePlugin.js +3 -3
  91. package/lib/plugins/PluginManager.js +7 -13
  92. package/lib/policy/audit.js +294 -0
  93. package/lib/policy/enforce.js +125 -0
  94. package/lib/policy/ignore-files.js +169 -0
  95. package/lib/policy/index.js +52 -0
  96. package/lib/policy/policy.js +489 -0
  97. package/lib/pr/diff-parser.js +267 -0
  98. package/lib/pr/method-mapper.js +148 -0
  99. package/lib/pr/pr-context.js +829 -0
  100. package/lib/pr/pr-git.js +149 -0
  101. package/lib/pr/pr-renderer.js +644 -0
  102. package/lib/pr/related-tests.js +296 -0
  103. package/lib/rag/DataSourcePlugin.js +5 -5
  104. package/lib/rag/EmbeddingProvider.js +19 -8
  105. package/lib/rag/Indexer.js +14 -5
  106. package/lib/rag/VectorStoreFactory.js +3 -3
  107. package/lib/rag/sources/FileDataSource.js +6 -6
  108. package/lib/rag/stores/BaseVectorStore.js +5 -5
  109. package/lib/rag/stores/ChromaDBStore.js +10 -10
  110. package/lib/rag/stores/LanceDBStore.js +29 -19
  111. package/lib/rag/stores/PineconeStore.js +11 -9
  112. package/lib/search/bm25.js +433 -0
  113. package/lib/search/code-units.js +201 -0
  114. package/lib/search/symbols.js +189 -0
  115. package/lib/ui/dashboard.js +120 -89
  116. package/lib/ui/index.js +1 -6
  117. package/lib/ui/init-wizard-ui.js +550 -0
  118. package/lib/ui/progress-bar.js +94 -88
  119. package/lib/ui/select-input.js +40 -32
  120. package/lib/ui/template-selector.js +99 -0
  121. package/lib/ui/wizard.js +374 -329
  122. package/lib/utils/claude-token-counter.js +151 -0
  123. package/lib/utils/cli-output.js +17 -0
  124. package/lib/utils/clipboard-utils.js +49 -50
  125. package/lib/utils/config-utils.js +81 -66
  126. package/lib/utils/context-versioning.js +532 -0
  127. package/lib/utils/error-handler.js +112 -112
  128. package/lib/utils/file-utils.js +153 -69
  129. package/lib/utils/format-converter.js +356 -283
  130. package/lib/utils/git-utils.js +366 -294
  131. package/lib/utils/llm-cost-estimator.js +281 -0
  132. package/lib/utils/llm-detector.js +242 -78
  133. package/lib/utils/logger.js +243 -237
  134. package/lib/utils/messages.js +355 -0
  135. package/lib/utils/multi-repo-manager.js +364 -0
  136. package/lib/utils/profile-manager.js +380 -0
  137. package/lib/utils/safe-read.js +61 -0
  138. package/lib/utils/secret-redactor.js +61 -0
  139. package/lib/utils/snapshot-manager.js +679 -0
  140. package/lib/utils/template-manager.js +378 -0
  141. package/lib/utils/token-utils.js +201 -170
  142. package/lib/utils/tokenizer-adapter.js +421 -377
  143. package/lib/utils/updater.js +539 -506
  144. package/lib/watch/FileWatcher.js +12 -8
  145. package/lib/watch/IncrementalAnalyzer.js +7 -7
  146. package/lib/why/coupling.js +81 -0
  147. package/lib/why/errors.js +23 -0
  148. package/lib/why/git.js +169 -0
  149. package/lib/why/history.js +108 -0
  150. package/lib/why/index.js +720 -0
  151. package/lib/why/ownership.js +79 -0
  152. package/lib/why/paths.js +207 -0
  153. package/lib/why/references.js +259 -0
  154. package/lib/why/render.js +722 -0
  155. package/lib/why/search.js +358 -0
  156. package/lib/why/target.js +389 -0
  157. package/lib/wizards/config-generator.js +255 -0
  158. package/lib/wizards/project-detector.js +571 -0
  159. package/package.json +44 -10
  160. package/.ctxman/logs/ctxman-2026-07-02.log +0 -156
  161. package/.ctxman/self-knowledge/documents.lance/_transactions/0-b3dd1558-f95b-44b9-a154-18e8c330ae7a.txn +0 -0
  162. package/.ctxman/self-knowledge/documents.lance/_transactions/1-44bff6d4-e552-4010-a769-e09ad822698d.txn +0 -0
  163. package/.ctxman/self-knowledge/documents.lance/_transactions/10-37260794-5947-4ecb-b4c8-d732f58f40f1.txn +0 -0
  164. package/.ctxman/self-knowledge/documents.lance/_transactions/11-0d52a883-5518-463c-9b6d-986c3af89dfd.txn +0 -0
  165. package/.ctxman/self-knowledge/documents.lance/_transactions/12-ae354646-52f2-49c9-90fb-2e6ed8932663.txn +0 -0
  166. package/.ctxman/self-knowledge/documents.lance/_transactions/13-5d7d5e57-27da-498b-b734-091b9d38da13.txn +0 -0
  167. package/.ctxman/self-knowledge/documents.lance/_transactions/14-558ce928-810b-4252-a364-fdd1ee6f8a25.txn +0 -0
  168. package/.ctxman/self-knowledge/documents.lance/_transactions/15-27e3a58f-8b64-43b8-a9f4-e3369446968b.txn +0 -0
  169. package/.ctxman/self-knowledge/documents.lance/_transactions/16-a110c390-e5c8-4464-b976-bc7f5195fa15.txn +0 -0
  170. package/.ctxman/self-knowledge/documents.lance/_transactions/17-97513225-53de-4216-b61a-6337bd0cc4ad.txn +0 -0
  171. package/.ctxman/self-knowledge/documents.lance/_transactions/18-5ba450f0-c527-459c-bf4f-6d79b5e4285f.txn +0 -0
  172. package/.ctxman/self-knowledge/documents.lance/_transactions/19-600d3a63-5225-49e9-b8b7-30e037b564ab.txn +0 -0
  173. package/.ctxman/self-knowledge/documents.lance/_transactions/2-48a61b4e-92cf-4342-8376-914c1b99a9f9.txn +0 -0
  174. package/.ctxman/self-knowledge/documents.lance/_transactions/20-121dd912-5410-410e-84ff-82c7a23604a0.txn +0 -0
  175. package/.ctxman/self-knowledge/documents.lance/_transactions/3-bc6844ab-7ea3-4bb3-9ee1-538ce5664a9c.txn +0 -0
  176. package/.ctxman/self-knowledge/documents.lance/_transactions/4-d5e1754f-dbb5-454e-a9be-2a7da8d7bcf9.txn +0 -0
  177. package/.ctxman/self-knowledge/documents.lance/_transactions/5-4fae93df-0568-4b6f-8acf-8bef4bba6047.txn +0 -0
  178. package/.ctxman/self-knowledge/documents.lance/_transactions/6-4b1b8b87-068e-49a3-ba2b-d8dae628b252.txn +0 -0
  179. package/.ctxman/self-knowledge/documents.lance/_transactions/7-63d040fb-d2fa-4c0f-bed0-53411918a7b0.txn +0 -0
  180. package/.ctxman/self-knowledge/documents.lance/_transactions/8-c5628cf6-264c-420f-aab8-bc96a9083591.txn +0 -0
  181. package/.ctxman/self-knowledge/documents.lance/_transactions/9-c94570aa-5e53-4fcb-971f-cecc1d1d5492.txn +0 -0
  182. package/.ctxman/self-knowledge/documents.lance/_versions/1.manifest +0 -0
  183. package/.ctxman/self-knowledge/documents.lance/_versions/10.manifest +0 -0
  184. package/.ctxman/self-knowledge/documents.lance/_versions/11.manifest +0 -0
  185. package/.ctxman/self-knowledge/documents.lance/_versions/12.manifest +0 -0
  186. package/.ctxman/self-knowledge/documents.lance/_versions/13.manifest +0 -0
  187. package/.ctxman/self-knowledge/documents.lance/_versions/14.manifest +0 -0
  188. package/.ctxman/self-knowledge/documents.lance/_versions/15.manifest +0 -0
  189. package/.ctxman/self-knowledge/documents.lance/_versions/16.manifest +0 -0
  190. package/.ctxman/self-knowledge/documents.lance/_versions/17.manifest +0 -0
  191. package/.ctxman/self-knowledge/documents.lance/_versions/18.manifest +0 -0
  192. package/.ctxman/self-knowledge/documents.lance/_versions/19.manifest +0 -0
  193. package/.ctxman/self-knowledge/documents.lance/_versions/2.manifest +0 -0
  194. package/.ctxman/self-knowledge/documents.lance/_versions/20.manifest +0 -0
  195. package/.ctxman/self-knowledge/documents.lance/_versions/21.manifest +0 -0
  196. package/.ctxman/self-knowledge/documents.lance/_versions/3.manifest +0 -0
  197. package/.ctxman/self-knowledge/documents.lance/_versions/4.manifest +0 -0
  198. package/.ctxman/self-knowledge/documents.lance/_versions/5.manifest +0 -0
  199. package/.ctxman/self-knowledge/documents.lance/_versions/6.manifest +0 -0
  200. package/.ctxman/self-knowledge/documents.lance/_versions/7.manifest +0 -0
  201. package/.ctxman/self-knowledge/documents.lance/_versions/8.manifest +0 -0
  202. package/.ctxman/self-knowledge/documents.lance/_versions/9.manifest +0 -0
  203. package/.ctxman/self-knowledge/documents.lance/data/000010010011011101000010de8621405d9d5cb06540dbf02c.lance +0 -0
  204. package/.ctxman/self-knowledge/documents.lance/data/0000101001011100011000003fb62a4aa8978974075c144f1c.lance +0 -0
  205. package/.ctxman/self-knowledge/documents.lance/data/000011010000100111001011c6cc824d628f3bb1f4e04cd5d0.lance +0 -0
  206. package/.ctxman/self-knowledge/documents.lance/data/001000001000011001011100d8f74349ada090b332cd1df669.lance +0 -0
  207. package/.ctxman/self-knowledge/documents.lance/data/01000001111010001000010045ab44467bb1e7e4d8ae06bd58.lance +0 -0
  208. package/.ctxman/self-knowledge/documents.lance/data/01000101101110111110011131ef904241a89e1ffb6ddaf8e3.lance +0 -0
  209. package/.ctxman/self-knowledge/documents.lance/data/010010110000010000011010fbfcc145c6a8a827b28057a07e.lance +0 -0
  210. package/.ctxman/self-knowledge/documents.lance/data/1000011000101010001110003fb4f045c3835982efa1405324.lance +0 -0
  211. package/.ctxman/self-knowledge/documents.lance/data/100111011101110111101011f4396b410fa06af3b028d204e6.lance +0 -0
  212. package/.ctxman/self-knowledge/documents.lance/data/100111100111101000001100596b174e00995d6cc2969cc8dc.lance +0 -0
  213. package/.ctxman/self-knowledge/documents.lance/data/1010001100011000110010102ea16b484d887c7c9725feffd6.lance +0 -0
  214. package/.ctxman/self-knowledge/documents.lance/data/101101100010101111110111c5230944d9b3af7301b6392dda.lance +0 -0
  215. package/.ctxman/self-knowledge/documents.lance/data/1011011100100111111110105892bf41b5a530fedd7cf516f1.lance +0 -0
  216. package/.ctxman/self-knowledge/documents.lance/data/10111110000010001011110164e8774742bade87b483c10f36.lance +0 -0
  217. package/.ctxman/self-knowledge/documents.lance/data/1100000100010110001110103ffaca40a2a42c85067ebaf9ae.lance +0 -0
  218. package/.ctxman/self-knowledge/documents.lance/data/110000010001110110101110664bde4c76a78dfc14b05e83b8.lance +0 -0
  219. package/.ctxman/self-knowledge/documents.lance/data/11001100111000010100110050645c4281b1f35b2022234f7f.lance +0 -0
  220. package/.ctxman/self-knowledge/documents.lance/data/1101111011001100110111017214924a368f5719eab2246619.lance +0 -0
  221. package/.ctxman/self-knowledge/documents.lance/data/1110000111001010001000002150c24dcab653d39d07032e63.lance +0 -0
  222. package/.ctxman/self-knowledge/documents.lance/data/1110100000000100000011014d20534967a18b5c87721659ff.lance +0 -0
  223. package/.ctxman/self-knowledge/documents.lance/data/11110101111010000101011135a0114ef2b35dc6f1297a3381.lance +0 -0
@@ -0,0 +1,189 @@
1
+ /**
2
+ * Symbol lookup: the full source of a named method, function or class.
3
+ *
4
+ * Names are matched exactly first, then ignoring case; when neither finds anything, similar
5
+ * names are suggested. `Class.method` (also `Class::method`, `Class#method`) narrows the
6
+ * lookup to methods of that class.
7
+ */
8
+
9
+ import path from 'path';
10
+ import TokenUtils from '../utils/token-utils.js';
11
+ import { redactSecrets } from '../utils/secret-redactor.js';
12
+ import { compareStrings, fitToTokenBudget, getProjectIndex, sliceLines } from './bm25.js';
13
+
14
+ export const DEFAULT_SYMBOL_LIMIT = 10;
15
+ export const DEFAULT_SYMBOL_MAX_TOKENS = 20000;
16
+ const MAX_SUGGESTIONS = 10;
17
+
18
+ /**
19
+ * Name without an accessor prefix: `get value` -> `value`
20
+ * @param {string} name
21
+ * @returns {string}
22
+ */
23
+ function bareName(name) {
24
+ return name.replace(/^(?:get|set)\s+/, '');
25
+ }
26
+
27
+ /**
28
+ * Edit distance between two strings, or `max + 1` once it is certain to exceed `max`
29
+ * @param {string} a
30
+ * @param {string} b
31
+ * @param {number} [max=Infinity]
32
+ * @returns {number}
33
+ */
34
+ export function editDistance(a, b, max = Infinity) {
35
+ if (Math.abs(a.length - b.length) > max) return max + 1;
36
+ let previous = Array.from({ length: b.length + 1 }, (_, j) => j);
37
+ for (let i = 1; i <= a.length; i++) {
38
+ const current = [i];
39
+ let rowMin = i;
40
+ for (let j = 1; j <= b.length; j++) {
41
+ const cost = a[i - 1] === b[j - 1] ? 0 : 1;
42
+ current[j] = Math.min(previous[j] + 1, current[j - 1] + 1, previous[j - 1] + cost);
43
+ rowMin = Math.min(rowMin, current[j]);
44
+ }
45
+ if (rowMin > max) return max + 1;
46
+ previous = current;
47
+ }
48
+ return previous[b.length];
49
+ }
50
+
51
+ /**
52
+ * Split a qualified name: `Store.get`, `Store::get` and `Store#get` give
53
+ * `{ parent: 'Store', name: 'get' }`
54
+ * @param {string} query
55
+ * @returns {{parent: string|null, name: string}}
56
+ */
57
+ export function parseSymbolQuery(query) {
58
+ const match = /^(.+?)(?:\.|::|#)([^.:#]+)$/.exec(query);
59
+ return match ? { parent: match[1], name: match[2] } : { parent: null, name: query };
60
+ }
61
+
62
+ /**
63
+ * Similar names for a query that matched nothing
64
+ * @private
65
+ */
66
+ function suggest(candidates, name) {
67
+ const query = name.toLowerCase();
68
+ const threshold = Math.max(2, Math.floor(query.length / 3));
69
+ const best = new Map();
70
+ for (const { entry, unit } of candidates) {
71
+ const candidate = bareName(unit.name);
72
+ const lower = candidate.toLowerCase();
73
+ let distance = editDistance(lower, query, threshold);
74
+ // `user` suggests `getUserName`: containment counts as close
75
+ const shorter = Math.min(lower.length, query.length);
76
+ if (shorter >= 3 && (lower.includes(query) || query.includes(lower))) {
77
+ distance = Math.min(distance, 1);
78
+ }
79
+ if (distance > threshold || best.has(candidate)) continue;
80
+ best.set(candidate, {
81
+ name: unit.name,
82
+ kind: unit.kind,
83
+ ...(unit.parent ? { parent: unit.parent } : {}),
84
+ file: entry.relativePath,
85
+ startLine: unit.startLine,
86
+ distance,
87
+ });
88
+ }
89
+ return [...best.values()]
90
+ .sort(
91
+ (a, b) =>
92
+ a.distance - b.distance ||
93
+ Math.abs(a.name.length - query.length) - Math.abs(b.name.length - query.length) ||
94
+ compareStrings(a.name, b.name)
95
+ )
96
+ .slice(0, MAX_SUGGESTIONS)
97
+ .map(({ distance: _distance, ...rest }) => rest);
98
+ }
99
+
100
+ /**
101
+ * Find the units named by a query
102
+ * @param {Array<{entry: Object, unit: Object}>} candidates - Units with their file entries
103
+ * @param {string} query - `name` or `Parent.name`
104
+ * @returns {{match: 'exact'|'case-insensitive'|'none', found: Array, suggestions: Array}}
105
+ */
106
+ export function lookupSymbols(candidates, query) {
107
+ const { parent, name } = parseSymbolQuery(query);
108
+ const tiers = [
109
+ ['exact', (a, b) => a === b],
110
+ ['case-insensitive', (a, b) => a.toLowerCase() === b.toLowerCase()],
111
+ ];
112
+
113
+ for (const [match, same] of tiers) {
114
+ const named = (unitName, wanted) => same(unitName, wanted) || same(bareName(unitName), wanted);
115
+ const found = candidates.filter(
116
+ ({ unit }) =>
117
+ named(unit.name, query) ||
118
+ (parent !== null && unit.parent && same(unit.parent, parent) && named(unit.name, name))
119
+ );
120
+ if (found.length > 0) return { match, found, suggestions: [] };
121
+ }
122
+ return { match: 'none', found: [], suggestions: suggest(candidates, name) };
123
+ }
124
+
125
+ /**
126
+ * Source of the methods, functions or classes with a given name in a project.
127
+ *
128
+ * @param {string} root - Project directory (files found with the Scanner, so the ignore
129
+ * rules apply)
130
+ * @param {string} query - Symbol name, or `Class.method`
131
+ * @param {Object} [options]
132
+ * @param {string} [options.file] - Only this project-relative file
133
+ * @param {number} [options.limit=10] - Maximum number of symbols returned with source
134
+ * @param {number} [options.maxTokens=20000] - Token budget for the sources; sources past it
135
+ * are cut (`truncated: true`)
136
+ * @param {boolean} [options.redact=true] - Redact secrets in the sources
137
+ * @returns {{query: string, match: string, total: number, symbols: Array,
138
+ * suggestions?: Array}}
139
+ * @throws {Error} When the query is empty or `file` is not a project file
140
+ */
141
+ export function getSymbol(
142
+ root,
143
+ query,
144
+ { file, limit = DEFAULT_SYMBOL_LIMIT, maxTokens = DEFAULT_SYMBOL_MAX_TOKENS, redact = true } = {}
145
+ ) {
146
+ const wanted = typeof query === 'string' ? query.trim() : '';
147
+ if (!wanted) throw new Error('The symbol name is empty');
148
+
149
+ const project = getProjectIndex(root);
150
+ let entries = [...project.files.values()];
151
+ if (file) {
152
+ const target = path.normalize(file);
153
+ entries = entries.filter((entry) => path.normalize(entry.relativePath) === target);
154
+ if (entries.length === 0) throw new Error(`File not found in project: ${file}`);
155
+ }
156
+
157
+ const candidates = entries.flatMap((entry) => entry.units.map((unit) => ({ entry, unit })));
158
+ const { match, found, suggestions } = lookupSymbols(candidates, wanted);
159
+
160
+ let used = 0;
161
+ const symbols = found.slice(0, Math.max(0, limit)).map(({ entry, unit }) => {
162
+ const raw = sliceLines(entry.content, unit.startLine, unit.endLine);
163
+ let source = redact ? redactSecrets(raw).text : raw;
164
+ let tokens = TokenUtils.calculate(source, entry.path);
165
+ const symbol = {
166
+ name: unit.name,
167
+ kind: unit.kind,
168
+ ...(unit.parent ? { parent: unit.parent } : {}),
169
+ file: entry.relativePath,
170
+ startLine: unit.startLine,
171
+ endLine: unit.endLine,
172
+ };
173
+ if (used + tokens > maxTokens) {
174
+ source = fitToTokenBudget(source, maxTokens - used, entry.path);
175
+ tokens = source ? TokenUtils.calculate(source, entry.path) : 0;
176
+ symbol.truncated = true;
177
+ }
178
+ used += tokens;
179
+ return { ...symbol, tokens, source };
180
+ });
181
+
182
+ return {
183
+ query: wanted,
184
+ match,
185
+ total: found.length,
186
+ symbols,
187
+ ...(match === 'none' ? { suggestions } : {}),
188
+ };
189
+ }
@@ -6,104 +6,135 @@ import { Box, Text, useInput, useStdin } from 'ink';
6
6
  * Real-time statistics and progress display
7
7
  */
8
8
  export default function Dashboard({ stats, topFiles = [], status = 'analyzing' }) {
9
- const [shouldRefresh, setShouldRefresh] = useState(false);
10
- const { isRawModeSupported } = useStdin();
9
+ const [shouldRefresh, setShouldRefresh] = useState(false);
10
+ const { isRawModeSupported } = useStdin();
11
11
 
12
- // Keyboard controls
13
- useInput((input, key) => {
14
- if (input === 'r' || input === 'R') {
15
- setShouldRefresh(!shouldRefresh);
16
- }
17
- if (input === 'q' || input === 'Q') {
18
- process.exit(0);
19
- }
20
- }, { isActive: isRawModeSupported });
12
+ // Keyboard controls
13
+ useInput(
14
+ (input, _key) => {
15
+ if (input === 'r' || input === 'R') {
16
+ setShouldRefresh(!shouldRefresh);
17
+ }
18
+ if (input === 'q' || input === 'Q') {
19
+ process.exit(0);
20
+ }
21
+ },
22
+ { isActive: isRawModeSupported }
23
+ );
21
24
 
22
- const {
23
- totalFiles = 0,
24
- totalTokens = 0,
25
- totalBytes = 0,
26
- totalLines = 0,
27
- byExtension = {}
28
- } = stats;
25
+ const {
26
+ totalFiles = 0,
27
+ totalTokens = 0,
28
+ totalBytes = 0,
29
+ totalLines = 0,
30
+ byExtension = {},
31
+ } = stats;
29
32
 
30
- const avgTokensPerFile = totalFiles > 0 ? Math.round(totalTokens / totalFiles) : 0;
31
- const sizeInMB = (totalBytes / (1024 * 1024)).toFixed(2);
33
+ const avgTokensPerFile = totalFiles > 0 ? Math.round(totalTokens / totalFiles) : 0;
34
+ const sizeInMB = (totalBytes / (1024 * 1024)).toFixed(2);
32
35
 
33
- // Get dominant language
34
- const extensions = Object.entries(byExtension);
35
- const dominant = extensions.length > 0
36
- ? extensions.sort((a, b) => b[1].count - a[1].count)[0]
37
- : null;
36
+ // Get dominant language
37
+ const extensions = Object.entries(byExtension);
38
+ const dominant =
39
+ extensions.length > 0 ? extensions.sort((a, b) => b[1].count - a[1].count)[0] : null;
38
40
 
39
- return React.createElement(Box, {
40
- flexDirection: 'column',
41
- padding: 1,
42
- borderStyle: 'round',
43
- borderColor: 'green',
44
- width: 76 // Max width to prevent scrollbar overlap (reduced from 80)
45
- },
46
- React.createElement(Box, { marginBottom: 1 },
47
- React.createElement(Text, { bold: true, color: 'green' }, '📊 Live Analysis Dashboard'),
48
- React.createElement(Text, { color: 'gray' }, ` (${status})`)
49
- ),
41
+ return React.createElement(
42
+ Box,
43
+ {
44
+ flexDirection: 'column',
45
+ padding: 1,
46
+ borderStyle: 'round',
47
+ borderColor: 'green',
48
+ width: 76, // Max width to prevent scrollbar overlap (reduced from 80)
49
+ },
50
+ React.createElement(
51
+ Box,
52
+ { marginBottom: 1 },
53
+ React.createElement(Text, { bold: true, color: 'green' }, '📊 Live Analysis Dashboard'),
54
+ React.createElement(Text, { color: 'gray' }, ` (${status})`)
55
+ ),
50
56
 
51
- // Summary Stats
52
- React.createElement(Box, { flexDirection: 'column', marginBottom: 1 },
53
- React.createElement(Box, null,
54
- React.createElement(Text, null, 'Files: '),
55
- React.createElement(Text, { bold: true, color: 'cyan' }, totalFiles.toLocaleString()),
56
- React.createElement(Text, null, ' Methods: '),
57
- React.createElement(Text, { bold: true, color: 'cyan' }, '-'),
58
- React.createElement(Text, null, ' Tokens: '),
59
- React.createElement(Text, { bold: true, color: 'cyan' }, totalTokens.toLocaleString())
60
- ),
57
+ // Summary Stats
58
+ React.createElement(
59
+ Box,
60
+ { flexDirection: 'column', marginBottom: 1 },
61
+ React.createElement(
62
+ Box,
63
+ null,
64
+ React.createElement(Text, null, 'Files: '),
65
+ React.createElement(Text, { bold: true, color: 'cyan' }, totalFiles.toLocaleString()),
66
+ React.createElement(Text, null, ' Methods: '),
67
+ React.createElement(Text, { bold: true, color: 'cyan' }, '-'),
68
+ React.createElement(Text, null, ' Tokens: '),
69
+ React.createElement(Text, { bold: true, color: 'cyan' }, totalTokens.toLocaleString())
70
+ ),
61
71
 
62
- React.createElement(Box, null,
63
- React.createElement(Text, null, 'Size: '),
64
- React.createElement(Text, { bold: true, color: 'cyan' }, `${sizeInMB} MB`),
65
- React.createElement(Text, null, ' Lines: '),
66
- React.createElement(Text, { bold: true, color: 'cyan' }, totalLines.toLocaleString()),
67
- React.createElement(Text, null, ' Avg: '),
68
- React.createElement(Text, { bold: true, color: 'cyan' }, `${avgTokensPerFile.toLocaleString()} tok/file`)
69
- )
70
- ),
72
+ React.createElement(
73
+ Box,
74
+ null,
75
+ React.createElement(Text, null, 'Size: '),
76
+ React.createElement(Text, { bold: true, color: 'cyan' }, `${sizeInMB} MB`),
77
+ React.createElement(Text, null, ' Lines: '),
78
+ React.createElement(Text, { bold: true, color: 'cyan' }, totalLines.toLocaleString()),
79
+ React.createElement(Text, null, ' Avg: '),
80
+ React.createElement(
81
+ Text,
82
+ { bold: true, color: 'cyan' },
83
+ `${avgTokensPerFile.toLocaleString()} tok/file`
84
+ )
85
+ )
86
+ ),
71
87
 
72
- // Top Languages
73
- dominant && React.createElement(Box, { flexDirection: 'column', marginBottom: 1 },
74
- React.createElement(Text, { bold: true }, 'Top Languages:'),
75
- React.createElement(Box, { marginLeft: 2 },
76
- React.createElement(Text, { color: 'green' }, '▓'.repeat(30)),
77
- React.createElement(Text, null, ` ${dominant[0]} (${dominant[1].count} files)`)
78
- )
79
- ),
88
+ // Top Languages
89
+ dominant &&
90
+ React.createElement(
91
+ Box,
92
+ { flexDirection: 'column', marginBottom: 1 },
93
+ React.createElement(Text, { bold: true }, 'Top Languages:'),
94
+ React.createElement(
95
+ Box,
96
+ { marginLeft: 2 },
97
+ React.createElement(Text, { color: 'green' }, '▓'.repeat(30)),
98
+ React.createElement(Text, null, ` ${dominant[0]} (${dominant[1].count} files)`)
99
+ )
100
+ ),
80
101
 
81
- // Largest Files
82
- topFiles.length > 0 && React.createElement(Box, { flexDirection: 'column', marginBottom: 1 },
83
- React.createElement(Text, { bold: true }, 'Largest Files:'),
84
- ...topFiles.slice(0, 3).map((file, index) => {
85
- const barLength = Math.round((file.tokens / topFiles[0].tokens) * 20);
86
- const percentage = ((file.tokens / totalTokens) * 100).toFixed(1);
87
- const fileName = (file.name || file.relativePath || file.path || 'unknown').substring(0, 30);
102
+ // Largest Files
103
+ topFiles.length > 0 &&
104
+ React.createElement(
105
+ Box,
106
+ { flexDirection: 'column', marginBottom: 1 },
107
+ React.createElement(Text, { bold: true }, 'Largest Files:'),
108
+ ...topFiles.slice(0, 3).map((file, index) => {
109
+ const barLength = Math.round((file.tokens / topFiles[0].tokens) * 20);
110
+ const percentage = ((file.tokens / totalTokens) * 100).toFixed(1);
111
+ const fileName = (file.name || file.relativePath || file.path || 'unknown').substring(
112
+ 0,
113
+ 30
114
+ );
88
115
 
89
- return React.createElement(Box, { key: index, marginLeft: 2 },
90
- React.createElement(Text, null, '• '),
91
- React.createElement(Text, { color: 'cyan' }, fileName),
92
- React.createElement(Text, null, ` ${file.tokens.toLocaleString()} tokens `),
93
- React.createElement(Text, { color: 'green' }, '▓'.repeat(barLength)),
94
- React.createElement(Text, { color: 'gray' }, ` (${percentage}%)`)
95
- );
96
- })
97
- ),
116
+ return React.createElement(
117
+ Box,
118
+ { key: index, marginLeft: 2 },
119
+ React.createElement(Text, null, '• '),
120
+ React.createElement(Text, { color: 'cyan' }, fileName),
121
+ React.createElement(Text, null, ` ${file.tokens.toLocaleString()} tokens `),
122
+ React.createElement(Text, { color: 'green' }, '▓'.repeat(barLength)),
123
+ React.createElement(Text, { color: 'gray' }, ` (${percentage}%)`)
124
+ );
125
+ })
126
+ ),
98
127
 
99
- // Controls
100
- React.createElement(Box, {
101
- marginTop: 1,
102
- borderStyle: 'single',
103
- borderColor: 'gray',
104
- padding: 1
105
- },
106
- React.createElement(Text, { dimColor: true }, '[R] Refresh [S] Save [E] Export [Q] Quit')
107
- )
108
- );
128
+ // Controls
129
+ React.createElement(
130
+ Box,
131
+ {
132
+ marginTop: 1,
133
+ borderStyle: 'single',
134
+ borderColor: 'gray',
135
+ padding: 1,
136
+ },
137
+ React.createElement(Text, { dimColor: true }, '[R] Refresh [S] Save [E] Export [Q] Quit')
138
+ )
139
+ );
109
140
  }
package/lib/ui/index.js CHANGED
@@ -7,9 +7,4 @@ import { ProgressBar, SpinnerWithText } from './progress-bar.js';
7
7
  import Wizard from './wizard.js';
8
8
  import Dashboard from './dashboard.js';
9
9
 
10
- export {
11
- ProgressBar,
12
- SpinnerWithText,
13
- Wizard,
14
- Dashboard
15
- };
10
+ export { ProgressBar, SpinnerWithText, Wizard, Dashboard };