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-map.js ADDED
@@ -0,0 +1,242 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * ctxman map - ranked repository map trimmed to a token budget
4
+ */
5
+
6
+ import fs from 'fs';
7
+ import path from 'path';
8
+ import { getLogger } from '../lib/utils/logger.js';
9
+ import { exitOnBrokenPipe } from '../lib/utils/cli-output.js';
10
+
11
+ // `ctxman ... | head` closes stdout early; stop quietly instead of throwing EPIPE
12
+ exitOnBrokenPipe();
13
+
14
+ const STYLES = ['text', 'markdown', 'json', 'dot'];
15
+ const ORDERS = ['rank', 'path'];
16
+
17
+ function printHelp() {
18
+ console.log(`Repository map - the most important files and their signatures, within a token budget
19
+
20
+ Usage: ctxman map [path] [options]
21
+
22
+ Files are ranked by PageRank over the import graph (importer -> imported), blended with
23
+ git change frequency and how often other files mention their functions. The best files
24
+ are listed with one line per function (line, signature, tokens) until the budget is full.
25
+
26
+ Options:
27
+ -t, --tokens N Token budget for the map (default: 2048)
28
+ -f, --focus PATH Bias the ranking toward a file or folder and its neighbours
29
+ (repeatable)
30
+ --history N Commits of git history to mine for change frequency
31
+ (default: 300, 0 disables)
32
+ --order rank|path Order of the files in the output (default: rank)
33
+ --important-last Highest-ranked file at the end (rank order only)
34
+ --style STYLE text (default), markdown, json, dot (Graphviz)
35
+ --json Same as --style json
36
+ --graph Print the dependency edges instead of the map
37
+ -o, --output FILE Write to FILE instead of stdout
38
+ --copy Copy to the clipboard instead of stdout
39
+ --no-redact Do not mask secrets in signatures
40
+ -q, --quiet No summary on stderr
41
+ -h, --help Show this help
42
+
43
+ Score:
44
+ 0.70 x PageRank + 0.25 x change frequency + 0.05 x symbol references, each scaled to 0..1
45
+ (without git history: the other two, renormalised). See docs/commands/map.md.
46
+
47
+ Examples:
48
+ ctxman map # map of the current directory, 2048 tokens
49
+ ctxman map --tokens 4096 --style markdown
50
+ ctxman map src --focus src/server.js # around the file you are working on
51
+ ctxman map --important-last -o map.txt # best files last, next to your question
52
+ ctxman map --graph --style dot | dot -Tsvg > deps.svg
53
+ `);
54
+ }
55
+
56
+ /**
57
+ * Parse the command line
58
+ * @param {string[]} argv
59
+ * @returns {Object}
60
+ */
61
+ function parseArgs(argv) {
62
+ const options = {
63
+ root: null,
64
+ tokens: undefined,
65
+ focus: [],
66
+ history: undefined,
67
+ order: undefined,
68
+ importantLast: false,
69
+ style: undefined,
70
+ graph: false,
71
+ output: null,
72
+ copy: false,
73
+ redact: true,
74
+ quiet: false,
75
+ help: false,
76
+ };
77
+ const integer = (name, value, minimum) => {
78
+ if (!/^\d+$/.test(value ?? '') || Number(value) < minimum) {
79
+ throw new Error(`${name} needs an integer >= ${minimum}, got ${JSON.stringify(value)}`);
80
+ }
81
+ return Number(value);
82
+ };
83
+
84
+ for (let i = 0; i < argv.length; i++) {
85
+ let arg = argv[i];
86
+ let inline;
87
+ if (arg.startsWith('--') && arg.includes('=')) {
88
+ inline = arg.slice(arg.indexOf('=') + 1);
89
+ arg = arg.slice(0, arg.indexOf('='));
90
+ }
91
+ const value = () => {
92
+ if (inline !== undefined) return inline;
93
+ if (i + 1 >= argv.length) throw new Error(`${arg} needs a value`);
94
+ return argv[++i];
95
+ };
96
+
97
+ switch (arg) {
98
+ case '-h':
99
+ case '--help':
100
+ options.help = true;
101
+ break;
102
+ case '-t':
103
+ case '--tokens':
104
+ options.tokens = integer(arg, value(), 1);
105
+ break;
106
+ case '-f':
107
+ case '--focus':
108
+ options.focus.push(value());
109
+ break;
110
+ case '--history':
111
+ options.history = integer(arg, value(), 0);
112
+ break;
113
+ case '--order':
114
+ options.order = value();
115
+ if (!ORDERS.includes(options.order)) {
116
+ throw new Error(`--order must be one of ${ORDERS.join(', ')}`);
117
+ }
118
+ break;
119
+ case '--important-last':
120
+ options.importantLast = true;
121
+ break;
122
+ case '--style':
123
+ options.style = value();
124
+ if (!STYLES.includes(options.style)) {
125
+ throw new Error(`--style must be one of ${STYLES.join(', ')}`);
126
+ }
127
+ break;
128
+ case '--json':
129
+ options.style = 'json';
130
+ break;
131
+ case '--graph':
132
+ options.graph = true;
133
+ break;
134
+ case '-o':
135
+ case '--output':
136
+ options.output = value();
137
+ break;
138
+ case '--copy':
139
+ options.copy = true;
140
+ break;
141
+ case '--no-redact':
142
+ options.redact = false;
143
+ break;
144
+ case '-q':
145
+ case '--quiet':
146
+ options.quiet = true;
147
+ break;
148
+ default:
149
+ if (arg.startsWith('-')) throw new Error(`Unknown option: ${arg}`);
150
+ if (options.root !== null) throw new Error(`Unexpected argument: ${arg}`);
151
+ options.root = arg;
152
+ }
153
+ }
154
+ if (options.importantLast && options.order === 'path') {
155
+ throw new Error('--important-last cannot be combined with --order path');
156
+ }
157
+ return options;
158
+ }
159
+
160
+ /**
161
+ * A focus path is project-relative when it exists under the project, otherwise taken
162
+ * from the current directory (`ctxman map ../app --focus ../app/src/a.js`)
163
+ */
164
+ function focusPath(root, entry) {
165
+ if (path.isAbsolute(entry) || fs.existsSync(path.resolve(root, entry))) return entry;
166
+ const fromCwd = path.resolve(entry);
167
+ return fs.existsSync(fromCwd) ? fromCwd : entry;
168
+ }
169
+
170
+ function formatMs(ms) {
171
+ return ms >= 1000 ? `${(ms / 1000).toFixed(1)}s` : `${Math.round(ms)}ms`;
172
+ }
173
+
174
+ async function main() {
175
+ let options;
176
+ try {
177
+ options = parseArgs(process.argv.slice(2));
178
+ } catch (error) {
179
+ console.error(`Error: ${error.message}`);
180
+ console.error('Run "ctxman map --help" for usage.');
181
+ process.exitCode = 1;
182
+ return;
183
+ }
184
+ if (options.help) {
185
+ printHelp();
186
+ return;
187
+ }
188
+
189
+ // stdout carries only the map: logger lines and ignore-file notices go to stderr, and
190
+ // the map writes no log files into the project
191
+ getLogger({ level: process.env.LOG_LEVEL || 'warn', logToFile: false });
192
+ console.log = console.info = console.debug = console.error;
193
+ const { renderRepoMap } = await import('../lib/map/index.js');
194
+ const { default: ClipboardUtils } = await import('../lib/utils/clipboard-utils.js');
195
+
196
+ const root = path.resolve(options.root ?? '.');
197
+ const result = renderRepoMap(root, {
198
+ tokens: options.tokens,
199
+ focus: options.focus.map((entry) => focusPath(root, entry)),
200
+ history: options.history,
201
+ order: options.order,
202
+ importantLast: options.importantLast,
203
+ style: options.style,
204
+ graph: options.graph,
205
+ redact: options.redact,
206
+ });
207
+
208
+ if (options.output) {
209
+ fs.writeFileSync(options.output, result.output);
210
+ }
211
+ if (options.copy && !ClipboardUtils.copy(result.output)) {
212
+ process.exitCode = 1;
213
+ }
214
+ if (!options.output && !options.copy) {
215
+ process.stdout.write(result.output);
216
+ }
217
+
218
+ if (options.quiet) return;
219
+ for (const warning of result.warnings) console.error(`warning: ${warning}`);
220
+ const { stats } = result;
221
+ const parts = [];
222
+ if (options.graph || result.style === 'dot') {
223
+ parts.push(`${stats.edges} edges between ${stats.files} files`);
224
+ } else {
225
+ parts.push(`${result.files.length} of ${stats.files} files`);
226
+ parts.push(`${result.tokens}/${result.budget} tokens`);
227
+ parts.push(`${stats.edges} import edges`);
228
+ }
229
+ const history = result.graph.history;
230
+ if (history.available) parts.push(`history ${history.commits} commits`);
231
+ if (stats.skipped.length > 0) parts.push(`${stats.skipped.length} large/minified files left out`);
232
+ const redacted = Object.values(stats.redactions).reduce((sum, count) => sum + count, 0);
233
+ if (redacted > 0) parts.push(`${redacted} secrets redacted`);
234
+ parts.push(formatMs(stats.timings.total));
235
+ console.error(`ctxman map: ${parts.join(' · ')}`);
236
+ if (options.output) console.error(`Map written to ${options.output}`);
237
+ }
238
+
239
+ main().catch((error) => {
240
+ console.error(`Error: ${error.message}`);
241
+ process.exitCode = 1;
242
+ });
package/bin/cm-mcp.js ADDED
@@ -0,0 +1,293 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * ctxman mcp: start the MCP server, or add it to an MCP client's configuration
5
+ *
6
+ * ctxman mcp [--transport stdio|sse] [--port N] [--host H] [--api-key K] [--cors] [--root DIR]
7
+ * ctxman mcp install --client NAME [--scope user|project] [--name N] [--dry-run] [--print]
8
+ * ctxman mcp uninstall --client NAME [--scope user|project] [--name N] [--dry-run]
9
+ * ctxman mcp config --client NAME [--scope user|project] [--name N]
10
+ */
11
+
12
+ import fs from 'fs';
13
+ import path from 'path';
14
+ import { parseArgs } from 'util';
15
+ import { parseServerArgs, startMcpServer } from '../lib/api/mcp/start.js';
16
+ import {
17
+ CLIENTS,
18
+ DEFAULT_SERVER_NAME,
19
+ SCOPES,
20
+ buildServerEntry,
21
+ buildSnippet,
22
+ getClient,
23
+ installServer,
24
+ manualCommands,
25
+ resolveConfigPath,
26
+ uninstallServer,
27
+ } from '../lib/api/mcp/install.js';
28
+
29
+ const SETUP_COMMANDS = ['install', 'uninstall', 'config'];
30
+
31
+ const SETUP_OPTIONS = {
32
+ client: { type: 'string' },
33
+ scope: { type: 'string' },
34
+ name: { type: 'string' },
35
+ project: { type: 'string' },
36
+ file: { type: 'string' },
37
+ root: { type: 'string' },
38
+ npx: { type: 'boolean' },
39
+ force: { type: 'boolean' },
40
+ 'dry-run': { type: 'boolean' },
41
+ print: { type: 'boolean' },
42
+ json: { type: 'boolean' },
43
+ help: { type: 'boolean', short: 'h' },
44
+ };
45
+
46
+ // What to do after the config file changed
47
+ const NEXT_STEPS = {
48
+ 'claude-desktop': 'Restart Claude Desktop to load the server.',
49
+ 'claude-code':
50
+ 'Claude Code asks you to approve servers from .mcp.json on its next start; `claude mcp list` shows them.',
51
+ cursor: 'Restart Cursor (or toggle the server in Settings > MCP) to load it.',
52
+ vscode: 'Start the server from the .vscode/mcp.json editor or the "MCP: List Servers" command.',
53
+ windsurf: 'Refresh the MCP servers in Cascade to load it.',
54
+ };
55
+
56
+ function printHelp() {
57
+ const clientLines = Object.entries(CLIENTS).map(([id, info]) => {
58
+ const scopes = SCOPES.filter((scope) => [...info.scopes, ...info.manualScopes].includes(scope))
59
+ .map((scope) => {
60
+ const mark = scope === info.defaultScope ? `${scope}*` : scope;
61
+ return info.scopes.includes(scope) ? mark : `${mark} (prints instructions)`;
62
+ })
63
+ .join(', ');
64
+ return ` ${id.padEnd(16)}${scopes}`;
65
+ });
66
+
67
+ console.log(`ctxman mcp - Model Context Protocol server for AI assistants
68
+
69
+ Usage:
70
+ ctxman mcp [options] Start the server (stdio by default)
71
+ ctxman mcp install --client <name> Add the server to an MCP client's configuration
72
+ ctxman mcp uninstall --client <name> Remove it again
73
+ ctxman mcp config --client <name> Print the configuration snippet
74
+
75
+ Server options:
76
+ --transport <stdio|sse> Transport (default: stdio); sse serves streamable HTTP on /mcp
77
+ --port <n> HTTP port (default: 3000)
78
+ --host <address> HTTP address (default: 127.0.0.1)
79
+ --api-key <key> Require this key (x-api-key header) on HTTP
80
+ --cors Send CORS headers on HTTP (off by default)
81
+ --root <dir> Confine every tool path to <dir> (or set CTXMAN_MCP_ROOT)
82
+
83
+ Client setup options:
84
+ --client <name> ${Object.keys(CLIENTS).join(', ')}
85
+ --scope <user|project> Which configuration (default: the starred scope below)
86
+ --name <name> Server name in the configuration (default: ${DEFAULT_SERVER_NAME})
87
+ --project <dir> Project directory for --scope project (default: current directory)
88
+ --file <path> Use this configuration file instead of the client's default
89
+ --npx Run \`npx -y ctxman mcp\` instead of this installation
90
+ --root <dir> Start the server with --root <dir>
91
+ --force Replace an existing, different entry of the same name
92
+ --dry-run Show the result without writing anything
93
+ --print Print the snippet instead of writing (same as \`config\`)
94
+ --json Print the result as JSON (install, uninstall)
95
+
96
+ Clients and scopes:
97
+ ${clientLines.join('\n')}
98
+
99
+ Before writing, the previous file is kept as <file>.bak. Other servers stay as they are.
100
+
101
+ Examples:
102
+ ctxman mcp install --client claude-desktop
103
+ ctxman mcp install --client claude-code --scope project --npx
104
+ ctxman mcp install --client cursor --dry-run
105
+ ctxman mcp config --client vscode
106
+ ctxman mcp --transport sse --port 3333 --api-key secret
107
+ ctxman mcp --root ~/code/my-project`);
108
+ }
109
+
110
+ class UsageError extends Error {}
111
+
112
+ /**
113
+ * Describe where the entry goes when ctxman does not write the file itself
114
+ */
115
+ function printManual(client, scope, name, entry, target, uninstall = false) {
116
+ const info = getClient(client);
117
+ console.error(`${info.label} (${scope} scope): ${target.reason}`);
118
+ if (uninstall) {
119
+ if (client === 'claude-code') {
120
+ console.log(`claude mcp remove --scope user ${name}`);
121
+ } else {
122
+ console.error(`Remove the "${name}" entry under "${info.key}" by hand.`);
123
+ }
124
+ return;
125
+ }
126
+ const commands = manualCommands(client, scope, name, entry);
127
+ if (commands.length > 0) {
128
+ console.error('Run:');
129
+ for (const command of commands) console.log(command);
130
+ console.error('Or add this to the configuration:');
131
+ } else {
132
+ console.error('Add this to the configuration:');
133
+ }
134
+ console.log(JSON.stringify(buildSnippet(client, name, entry), null, 2));
135
+ }
136
+
137
+ function runSetup(command, argv) {
138
+ let values;
139
+ try {
140
+ ({ values } = parseArgs({ args: argv, options: SETUP_OPTIONS, strict: true }));
141
+ } catch (error) {
142
+ throw new UsageError(error.message);
143
+ }
144
+ if (values.help) {
145
+ printHelp();
146
+ return 0;
147
+ }
148
+ if (!values.client) throw new UsageError('--client is required');
149
+ let info;
150
+ try {
151
+ info = getClient(values.client);
152
+ } catch (error) {
153
+ throw new UsageError(error.message);
154
+ }
155
+ const client = values.client;
156
+ const scope = values.scope ?? info.defaultScope;
157
+ if (!SCOPES.includes(scope)) {
158
+ throw new UsageError(`Unknown scope "${scope}" (expected user or project)`);
159
+ }
160
+ if (!values.file && ![...info.scopes, ...info.manualScopes].includes(scope)) {
161
+ throw new UsageError(`${info.label} has no ${scope} configuration`);
162
+ }
163
+ const name = values.name ?? DEFAULT_SERVER_NAME;
164
+ if (!/^[\w.-]+$/.test(name)) {
165
+ throw new UsageError(`Invalid server name "${name}" (letters, digits, _ . - only)`);
166
+ }
167
+ const projectDir = path.resolve(values.project ?? process.cwd());
168
+ if (scope === 'project' && !fs.statSync(projectDir, { throwIfNoEntry: false })?.isDirectory()) {
169
+ throw new UsageError(`Project directory does not exist: ${projectDir}`);
170
+ }
171
+ let root;
172
+ if (values.root) {
173
+ root = path.resolve(values.root);
174
+ if (!fs.statSync(root, { throwIfNoEntry: false })?.isDirectory()) {
175
+ throw new UsageError(`--root is not an existing directory: ${root}`);
176
+ }
177
+ }
178
+
179
+ const entry = buildServerEntry(client, { npx: values.npx, root });
180
+ const target = values.file
181
+ ? { path: path.resolve(values.file) }
182
+ : resolveConfigPath(client, scope, { projectDir });
183
+
184
+ if (command === 'config' || values.print) {
185
+ if (target.path) {
186
+ console.error(`Add this to ${target.path} (${info.label}, ${scope} scope):`);
187
+ console.log(JSON.stringify(buildSnippet(client, name, entry), null, 2));
188
+ } else {
189
+ printManual(client, scope, name, entry, target);
190
+ }
191
+ return 0;
192
+ }
193
+
194
+ if (!target.path) {
195
+ printManual(client, scope, name, entry, target, command === 'uninstall');
196
+ return 0;
197
+ }
198
+
199
+ const dryRun = values['dry-run'] === true;
200
+ const result =
201
+ command === 'install'
202
+ ? installServer({ client, file: target.path, entry, name, force: values.force, dryRun })
203
+ : uninstallServer({ client, file: target.path, name, dryRun });
204
+
205
+ if (values.json) {
206
+ console.log(JSON.stringify({ client, scope, name, dryRun, ...result }, null, 2));
207
+ return result.status === 'conflict' ? 1 : 0;
208
+ }
209
+
210
+ const where = `${target.path} (${info.label}, ${scope} scope)`;
211
+ switch (result.status) {
212
+ case 'conflict':
213
+ console.error(`${where} already has a different "${name}" entry:`);
214
+ console.error(JSON.stringify(result.existing, null, 2));
215
+ console.error('Use --force to replace it (the old file is kept as .bak), or --name.');
216
+ return 1;
217
+ case 'unchanged':
218
+ console.log(`"${name}" is already set up in ${where}; nothing changed.`);
219
+ return 0;
220
+ case 'absent':
221
+ console.log(`No "${name}" entry in ${where}; nothing changed.`);
222
+ return 0;
223
+ default:
224
+ break;
225
+ }
226
+
227
+ const verb = { created: 'Created', added: 'Added', updated: 'Replaced', removed: 'Removed' }[
228
+ result.status
229
+ ];
230
+ if (dryRun) {
231
+ console.log(`Dry run: would write ${target.path}:`);
232
+ console.log(result.content.trimEnd());
233
+ return 0;
234
+ }
235
+ const subject = result.status === 'created' ? where : `"${name}" in ${where}`;
236
+ console.log(`${verb} ${subject}`);
237
+ if (result.backup) console.log(`Previous version: ${result.backup}`);
238
+ if (command === 'install') {
239
+ if (scope === 'project' && !values.npx) {
240
+ console.log(
241
+ 'The entry holds absolute paths of this machine; use --npx for a file you commit.'
242
+ );
243
+ }
244
+ console.log(NEXT_STEPS[client]);
245
+ }
246
+ return 0;
247
+ }
248
+
249
+ async function runServer(argv) {
250
+ let options;
251
+ try {
252
+ options = parseServerArgs(argv, { strict: true });
253
+ } catch (error) {
254
+ throw new UsageError(error.message);
255
+ }
256
+ if (options.help) {
257
+ printHelp();
258
+ return;
259
+ }
260
+ try {
261
+ await startMcpServer(options);
262
+ } catch (error) {
263
+ console.error('Fatal error running MCP server:', error.message);
264
+ process.exit(1);
265
+ }
266
+ }
267
+
268
+ async function main() {
269
+ const argv = process.argv.slice(2);
270
+ const [first, ...rest] = argv;
271
+ try {
272
+ if (first === 'help') {
273
+ printHelp();
274
+ } else if (SETUP_COMMANDS.includes(first)) {
275
+ process.exitCode = runSetup(first, rest);
276
+ } else if (first !== undefined && !first.startsWith('-')) {
277
+ throw new UsageError(`Unknown mcp command "${first}"`);
278
+ } else {
279
+ await runServer(argv);
280
+ }
281
+ } catch (error) {
282
+ if (error instanceof UsageError) {
283
+ console.error(`Error: ${error.message}`);
284
+ console.error('Run `ctxman mcp --help` for usage.');
285
+ process.exitCode = 2;
286
+ return;
287
+ }
288
+ console.error(`Error: ${error.message}`);
289
+ process.exitCode = 1;
290
+ }
291
+ }
292
+
293
+ main();