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
package/bin/cm-pr.js ADDED
@@ -0,0 +1,248 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * ctxman pr - review context for a branch, the staged changes or the working tree
4
+ *
5
+ * stdout carries only the context (unless -o/--copy); everything else, including notices
6
+ * printed by shared modules with console.log, goes to stderr.
7
+ */
8
+
9
+ import fs from 'fs';
10
+ import path from 'path';
11
+ import { exitOnBrokenPipe } from '../lib/utils/cli-output.js';
12
+
13
+ // `ctxman ... | head` closes stdout early; stop quietly instead of throwing EPIPE
14
+ exitOnBrokenPipe();
15
+
16
+ // Shared modules (ignore-file notices, the logger) print with console.log; keep stdout
17
+ // for the context itself
18
+ const writeOut = (text) => process.stdout.write(text);
19
+ console.log = console.info = console.debug = console.error;
20
+
21
+ const VALUE_OPTIONS = {
22
+ '--base': 'base',
23
+ '--head': 'head',
24
+ '--unified': 'unified',
25
+ '-U': 'unified',
26
+ '--ask': 'ask',
27
+ '--instructions': 'instructions',
28
+ '--budget': 'budget',
29
+ '--style': 'style',
30
+ '--output': 'output',
31
+ '-o': 'output',
32
+ };
33
+
34
+ const FLAG_OPTIONS = {
35
+ '--staged': ['staged', true],
36
+ '--cached': ['staged', true],
37
+ '--working': ['working', true],
38
+ '--no-diff': ['diff', false],
39
+ '--with-tests': ['withTests', true],
40
+ '--with-files': ['withFiles', true],
41
+ '--no-redact': ['redact', false],
42
+ '--copy': ['copy', true],
43
+ '--json': ['style', 'json'],
44
+ '--quiet': ['quiet', true],
45
+ '-q': ['quiet', true],
46
+ '--help': ['help', true],
47
+ '-h': ['help', true],
48
+ };
49
+
50
+ /**
51
+ * Parse the command line
52
+ * @param {string[]} argv
53
+ * @returns {object}
54
+ */
55
+ function parseArgs(argv) {
56
+ const parsed = { paths: [] };
57
+ for (let i = 0; i < argv.length; i++) {
58
+ const arg = argv[i];
59
+ if (arg === '--') {
60
+ parsed.paths.push(...argv.slice(i + 1));
61
+ break;
62
+ }
63
+ const eq = arg.startsWith('--') ? arg.indexOf('=') : -1;
64
+ const name = eq > 0 ? arg.slice(0, eq) : arg;
65
+ if (VALUE_OPTIONS[name]) {
66
+ let value;
67
+ if (eq > 0) value = arg.slice(eq + 1);
68
+ else if (i + 1 < argv.length) value = argv[++i];
69
+ else throw new Error(`${name} needs a value`);
70
+ parsed[VALUE_OPTIONS[name]] = value;
71
+ } else if (FLAG_OPTIONS[name] && eq < 0) {
72
+ const [key, value] = FLAG_OPTIONS[name];
73
+ parsed[key] = value;
74
+ } else if (arg.startsWith('-') && arg !== '-') {
75
+ throw new Error(`Unknown option: ${arg} (see ctxman pr --help)`);
76
+ } else {
77
+ parsed.paths.push(arg);
78
+ }
79
+ }
80
+ if (parsed.paths.length > 1) {
81
+ throw new Error(`Expected one project path, got ${parsed.paths.length}`);
82
+ }
83
+ return parsed;
84
+ }
85
+
86
+ /** "8000", "8k" or "1.5k" → 8000 / 1500 */
87
+ function parseTokenCount(text) {
88
+ const match = /^(\d+(?:\.\d+)?)(k)?$/i.exec(String(text).trim());
89
+ const value = match ? Math.round(Number(match[1]) * (match[2] ? 1000 : 1)) : NaN;
90
+ if (!Number.isInteger(value) || value <= 0) throw new Error(`Invalid --budget: ${text}`);
91
+ return value;
92
+ }
93
+
94
+ function parseCount(text, flag) {
95
+ if (!/^\d+$/.test(String(text))) throw new Error(`Invalid ${flag}: ${text}`);
96
+ return Number(text);
97
+ }
98
+
99
+ const formatNumber = (n) => Number(n).toLocaleString('en-US');
100
+
101
+ function printSummary(result, extras) {
102
+ const { stats } = result;
103
+ const lines = [];
104
+ if (result.mode === 'range') {
105
+ lines.push(
106
+ `ctxman pr: ${result.head.ref} into ${result.base.ref} (merge base ${result.mergeBase.short})`
107
+ );
108
+ } else {
109
+ const what = result.mode === 'staged' ? 'staged changes' : 'working tree';
110
+ lines.push(`ctxman pr: ${what} against ${result.base.ref} (${result.mergeBase.short})`);
111
+ }
112
+ lines.push(
113
+ ` ${stats.commits} commit(s), ${stats.files} file(s) (+${stats.additions} -${stats.deletions}), ` +
114
+ `${stats.changedMethods} changed method(s), ${stats.deletedMethods} deleted, ` +
115
+ `${stats.relatedTests} related test(s)`
116
+ );
117
+ const sections = Object.entries(result.sections)
118
+ .map(([name, tokens]) => `${name} ${formatNumber(tokens)}`)
119
+ .join(', ');
120
+ lines.push(` ${formatNumber(result.tokens)} tokens (${result.style}): ${sections}`);
121
+ if (result.budget) {
122
+ const state = result.budget.over ? 'over budget' : 'within budget';
123
+ lines.push(` Budget ${formatNumber(result.budget.limit)}: ${state}`);
124
+ for (const label of result.budget.dropped) lines.push(` dropped ${label}`);
125
+ }
126
+ const redacted = Object.entries(result.redactions);
127
+ if (redacted.length) {
128
+ const total = redacted.reduce((sum, [, n]) => sum + n, 0);
129
+ lines.push(
130
+ ` Redacted ${total} secret(s): ${redacted.map(([type, n]) => `${n} ${type}`).join(', ')}`
131
+ );
132
+ }
133
+ for (const warning of result.warnings) lines.push(` Warning: ${warning}`);
134
+ lines.push(...extras.map((line) => ` ${line}`));
135
+ process.stderr.write(`${lines.join('\n')}\n`);
136
+ }
137
+
138
+ function printHelp() {
139
+ writeOut(`ctxman pr - pull-request / branch review context for an LLM
140
+
141
+ Usage: ctxman pr [options] [path]
142
+
143
+ Compares the head with the merge base of base and head (what a pull request shows) and
144
+ writes, in this order: a summary, the commit log, the changed files, the full source of
145
+ every changed method (and the list of deleted ones), the unified diff, related tests,
146
+ optionally the changed files' full contents, and your question last.
147
+
148
+ Arguments:
149
+ path Project directory inside a git work tree (default: current
150
+ directory); a subdirectory limits the change to that directory
151
+
152
+ What to compare:
153
+ --base REF Base branch/commit (default: origin/HEAD, else main, else master)
154
+ --head REF Head branch/commit (default: HEAD)
155
+ --staged Staged changes (index) against HEAD, or against --base
156
+ --working Working tree, untracked files included, against HEAD or --base
157
+
158
+ Content:
159
+ -U, --unified N Context lines in the diff (default: 3)
160
+ --no-diff Leave out the unified diff
161
+ --with-tests Include the related tests' contents
162
+ --with-files Include the changed files' full contents
163
+ --ask "QUESTION" Append a question at the end
164
+ --instructions FILE Append the instructions in FILE at the end
165
+ --budget TOKENS Keep the output under TOKENS (e.g. 8000 or 8k): drops full
166
+ files, then test contents, test paths, diffs, method sources,
167
+ the method list, commit bodies, the log, the file list
168
+ --no-redact Do not mask secrets (keys, tokens) in the output
169
+
170
+ Output:
171
+ --style STYLE markdown (default), xml or json
172
+ --json Same as --style json
173
+ -o, --output FILE Write to FILE instead of stdout
174
+ --copy Copy to the clipboard instead of stdout
175
+ -q, --quiet No summary on stderr
176
+ -h, --help Show this help
177
+
178
+ .gitignore, .contextignore and .contextinclude decide which changed files have their
179
+ diff, methods and contents included; excluded files are still listed.
180
+
181
+ Examples:
182
+ ctxman pr # current branch against the default branch
183
+ ctxman pr --base main --head feature/login -o review.md
184
+ ctxman pr --working --ask "Anything risky in these changes?"
185
+ ctxman pr --budget 16k --style xml --with-tests --copy
186
+ `);
187
+ }
188
+
189
+ async function main() {
190
+ let args;
191
+ try {
192
+ args = parseArgs(process.argv.slice(2));
193
+ } catch (error) {
194
+ process.stderr.write(`Error: ${error.message}\n`);
195
+ process.exitCode = 1;
196
+ return;
197
+ }
198
+ if (args.help) {
199
+ printHelp();
200
+ return;
201
+ }
202
+
203
+ try {
204
+ const options = {
205
+ base: args.base,
206
+ head: args.head,
207
+ staged: args.staged,
208
+ working: args.working,
209
+ diff: args.diff,
210
+ withTests: args.withTests,
211
+ withFiles: args.withFiles,
212
+ redact: args.redact,
213
+ style: args.style,
214
+ ask: args.ask,
215
+ };
216
+ if (args.unified !== undefined) options.unified = parseCount(args.unified, '--unified');
217
+ if (args.budget !== undefined) options.budget = parseTokenCount(args.budget);
218
+ if (args.instructions !== undefined) {
219
+ options.instructions = fs.readFileSync(path.resolve(args.instructions), 'utf8');
220
+ }
221
+
222
+ const { buildPrContext } = await import('../lib/pr/pr-context.js');
223
+ const result = await buildPrContext(args.paths[0] || process.cwd(), options);
224
+
225
+ const extras = [];
226
+ let toStdout = !args.output && !args.copy;
227
+ if (args.output) {
228
+ const outputPath = path.resolve(args.output);
229
+ fs.mkdirSync(path.dirname(outputPath), { recursive: true });
230
+ fs.writeFileSync(outputPath, result.output);
231
+ extras.push(`Written to ${outputPath}`);
232
+ }
233
+ if (args.copy) {
234
+ const { default: ClipboardUtils } = await import('../lib/utils/clipboard-utils.js');
235
+ if (!ClipboardUtils.copy(result.output) && !args.output) {
236
+ extras.push('Clipboard unavailable; writing to stdout instead');
237
+ toStdout = true;
238
+ }
239
+ }
240
+ if (toStdout) writeOut(result.output);
241
+ if (!args.quiet) printSummary(result, extras);
242
+ } catch (error) {
243
+ process.stderr.write(`Error: ${error.message}\n`);
244
+ process.exitCode = 1;
245
+ }
246
+ }
247
+
248
+ main();
@@ -0,0 +1,178 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * ctxman search: ranked (BM25) search over the methods and code chunks of a project
5
+ *
6
+ * ctxman search "<query>" [path] [--limit N] [--max-tokens N] [--json] [--full]
7
+ * [--no-redact] [-o FILE] [--copy]
8
+ */
9
+
10
+ import fs from 'fs';
11
+ import path from 'path';
12
+ import { parseArgs } from 'util';
13
+ import { searchProject, DEFAULT_LIMIT, DEFAULT_MAX_TOKENS } from '../lib/search/bm25.js';
14
+ import { getLogger } from '../lib/utils/logger.js';
15
+ import ClipboardUtils from '../lib/utils/clipboard-utils.js';
16
+ import { exitOnBrokenPipe } from '../lib/utils/cli-output.js';
17
+
18
+ // `ctxman ... | head` closes stdout early; stop quietly instead of throwing EPIPE
19
+ exitOnBrokenPipe();
20
+
21
+ const PREVIEW_LINES = 8;
22
+
23
+ const OPTIONS = {
24
+ limit: { type: 'string', short: 'n' },
25
+ 'max-tokens': { type: 'string' },
26
+ json: { type: 'boolean' },
27
+ full: { type: 'boolean' },
28
+ 'no-redact': { type: 'boolean' },
29
+ output: { type: 'string', short: 'o' },
30
+ copy: { type: 'boolean' },
31
+ help: { type: 'boolean', short: 'h' },
32
+ };
33
+
34
+ // stdout carries the results; scanner notices and log lines go to stderr
35
+ const write = (text) => process.stdout.write(text);
36
+ console.log = console.info = console.debug = console.error;
37
+
38
+ function printHelp() {
39
+ write(`ctxman search - Ranked code search (BM25 over methods and code chunks)
40
+
41
+ Usage:
42
+ ctxman search "<query>" [path] [options]
43
+
44
+ The query is a few words or identifiers. Identifiers are split into words (getUserName,
45
+ get_user_name and get-user-name all mean "get user name"), and a chunk is a method or
46
+ function, or a window of up to 40 lines of the code outside them. .gitignore,
47
+ .contextignore and .contextinclude apply.
48
+
49
+ Options:
50
+ -n, --limit <n> Number of results (default: ${DEFAULT_LIMIT})
51
+ --max-tokens <n> Token budget for the returned code (default: ${DEFAULT_MAX_TOKENS})
52
+ --full Print whole chunks instead of the first ${PREVIEW_LINES} lines
53
+ --json Print the results as JSON
54
+ --no-redact Do not redact secrets in the returned code
55
+ -o, --output <file> Write the output to a file
56
+ --copy Copy the output to the clipboard
57
+ -h, --help Show this help
58
+
59
+ Examples:
60
+ ctxman search "parse config file"
61
+ ctxman search getUserName ./services/api --limit 5
62
+ ctxman search "retry backoff" --json | jq '.results[].file'
63
+ `);
64
+ }
65
+
66
+ class UsageError extends Error {}
67
+
68
+ function positiveInteger(value, name) {
69
+ const number = Number(value);
70
+ if (!Number.isInteger(number) || number < 1) {
71
+ throw new UsageError(`${name} must be a positive integer, got "${value}"`);
72
+ }
73
+ return number;
74
+ }
75
+
76
+ /**
77
+ * Human-readable listing
78
+ * @param {ReturnType<typeof searchProject>} result
79
+ * @param {boolean} full - Whole chunks instead of a preview
80
+ * @returns {string}
81
+ */
82
+ function formatResults(result, full) {
83
+ const header = `Ranked search for "${result.query}" in ${result.root} (${result.files} files, ${result.chunks} chunks)\n`;
84
+ if (result.results.length === 0) return `${header}\nNo matches.\n`;
85
+
86
+ const width = String(result.results.length).length;
87
+ const blocks = result.results.map((hit, i) => {
88
+ const label = hit.name
89
+ ? ` ${hit.parent ? `${hit.parent}.` : ''}${hit.name} (${hit.kind})`
90
+ : '';
91
+ const lines = [
92
+ `${String(i + 1).padStart(width)}. ${hit.file}:${hit.startLine}-${hit.endLine}${label} score ${hit.score}`,
93
+ ];
94
+ if (hit.text === null) {
95
+ lines.push(' (code left out: token budget reached)');
96
+ return lines.join('\n');
97
+ }
98
+ const code = hit.text.split('\n');
99
+ const shown = full ? code : code.slice(0, PREVIEW_LINES);
100
+ const numberWidth = String(hit.startLine + shown.length - 1).length;
101
+ shown.forEach((line, j) => {
102
+ lines.push(` ${String(hit.startLine + j).padStart(numberWidth)} | ${line}`);
103
+ });
104
+ const hidden = hit.endLine - hit.startLine + 1 - shown.length;
105
+ if (hidden > 0) {
106
+ lines.push(` ... ${hidden} more line${hidden === 1 ? '' : 's'}`);
107
+ }
108
+ return lines.join('\n');
109
+ });
110
+ return `${header}\n${blocks.join('\n\n')}\n`;
111
+ }
112
+
113
+ function main() {
114
+ let values;
115
+ let positionals;
116
+ try {
117
+ ({ values, positionals } = parseArgs({
118
+ args: process.argv.slice(2),
119
+ options: OPTIONS,
120
+ allowPositionals: true,
121
+ strict: true,
122
+ }));
123
+ } catch (error) {
124
+ throw new UsageError(error.message);
125
+ }
126
+ if (values.help) {
127
+ printHelp();
128
+ return;
129
+ }
130
+
131
+ // A last positional that names a directory is the project; the rest is the query
132
+ let root = process.cwd();
133
+ const last = positionals.at(-1);
134
+ if (positionals.length > 1 && fs.statSync(last, { throwIfNoEntry: false })?.isDirectory()) {
135
+ root = path.resolve(positionals.pop());
136
+ }
137
+ const query = positionals.join(' ').trim();
138
+ if (!query) throw new UsageError('Missing query: ctxman search "<query>" [path]');
139
+
140
+ const limit =
141
+ values.limit === undefined ? DEFAULT_LIMIT : positiveInteger(values.limit, '--limit');
142
+ const maxTokens =
143
+ values['max-tokens'] === undefined
144
+ ? DEFAULT_MAX_TOKENS
145
+ : positiveInteger(values['max-tokens'], '--max-tokens');
146
+
147
+ // Scan and analyzer progress lines are noise here; LOG_LEVEL still overrides
148
+ getLogger().level = process.env.LOG_LEVEL || 'warn';
149
+ const result = searchProject(root, query, {
150
+ limit,
151
+ maxTokens,
152
+ redact: values['no-redact'] !== true,
153
+ });
154
+
155
+ const output = values.json
156
+ ? `${JSON.stringify(result, null, 2)}\n`
157
+ : formatResults(result, values.full === true);
158
+
159
+ if (values.output) {
160
+ fs.writeFileSync(values.output, output);
161
+ console.error(`Wrote ${result.results.length} results to ${values.output}`);
162
+ } else {
163
+ write(output);
164
+ }
165
+ if (values.copy) ClipboardUtils.copy(output);
166
+ }
167
+
168
+ try {
169
+ main();
170
+ } catch (error) {
171
+ console.error(`Error: ${error.message}`);
172
+ if (error instanceof UsageError) {
173
+ console.error('Run `ctxman search --help` for usage.');
174
+ process.exitCode = 2;
175
+ } else {
176
+ process.exitCode = 1;
177
+ }
178
+ }