ctxman 3.0.0 → 3.1.1
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.
- package/.ctxman/llm-profiles.json +1067 -39
- package/.ctxman/profiles/backend-team.json +13 -0
- package/.ctxman/profiles/default.json +12 -0
- package/.ctxman/profiles/devops-team.json +21 -0
- package/.ctxman/profiles/frontend-team.json +20 -0
- package/.ctxman/templates/bug-fix.json +17 -0
- package/.ctxman/templates/code-review.json +9 -0
- package/.ctxman/templates/documentation.json +9 -0
- package/.ctxman/templates/feature.json +9 -0
- package/.ctxman/templates/full-context.json +9 -0
- package/.ctxman/templates/refactoring.json +18 -0
- package/.ctxman/templates/testing.json +9 -0
- package/.ctxman/wizard-profiles/security-audit/profile.json +7 -1
- package/LICENSE +21 -0
- package/README.md +381 -136
- package/bin/cli.js +1880 -529
- package/bin/cm-ask.js +18 -67
- package/bin/cm-audit.js +121 -0
- package/bin/cm-count.js +151 -0
- package/bin/cm-gitingest.js +128 -115
- package/bin/cm-map.js +242 -0
- package/bin/cm-mcp.js +293 -0
- package/bin/cm-pack.js +405 -0
- package/bin/cm-policy.js +170 -0
- package/bin/cm-pr.js +248 -0
- package/bin/cm-search.js +178 -0
- package/bin/cm-update.js +128 -128
- package/bin/cm-why.js +197 -0
- package/bin/mcp-server.js +8 -24
- package/ctxman.js +237 -183
- package/examples/README.md +19 -4
- package/examples/github-actions/ctxman-check.yml +49 -0
- package/examples/wizard-profiles/security-audit/profile.json +7 -1
- package/index.d.ts +1422 -0
- package/index.js +55 -31
- package/lib/agents/pr-failure-agent.js +48 -45
- package/lib/analyzers/ai-suggester.js +451 -0
- package/lib/analyzers/dependency-scanner.js +505 -0
- package/lib/analyzers/duplicate-detector.js +264 -0
- package/lib/analyzers/go-method-analyzer.js +151 -110
- package/lib/analyzers/import-tracker.js +431 -0
- package/lib/analyzers/method-analyzer.js +1055 -434
- package/lib/analyzers/performance-dashboard.js +687 -0
- package/lib/analyzers/token-calculator.js +895 -504
- package/lib/analyzers/unused-exports-detector.js +336 -0
- package/lib/api/mcp/install.js +424 -0
- package/lib/api/mcp/prompts.js +281 -281
- package/lib/api/mcp/resources.js +300 -241
- package/lib/api/mcp/sandbox.js +89 -0
- package/lib/api/mcp/server.js +772 -312
- package/lib/api/mcp/start.js +125 -0
- package/lib/api/mcp/transports/HttpSseTransport.js +60 -52
- package/lib/api/rest/server.js +105 -28
- package/lib/cache/CacheManager.js +9 -17
- package/lib/core/Analyzer.js +16 -10
- package/lib/core/ContextBuilder.js +19 -28
- package/lib/core/Reporter.js +15 -14
- package/lib/core/Scanner.js +81 -12
- package/lib/count/index.js +100 -0
- package/lib/formatters/format-registry.js +394 -372
- package/lib/formatters/gitingest-formatter.js +656 -621
- package/lib/formatters/toon-formatter-v1.3.js +637 -632
- package/lib/formatters/toon-formatter.js +239 -240
- package/lib/integrations/git/BlameTracker.js +4 -4
- package/lib/integrations/git/DiffAnalyzer.js +38 -31
- package/lib/integrations/git/GitClient.js +71 -53
- package/lib/lsp/server.js +311 -0
- package/lib/map/graph.js +299 -0
- package/lib/map/imports.js +233 -0
- package/lib/map/index.js +33 -0
- package/lib/map/rank.js +253 -0
- package/lib/map/render.js +346 -0
- package/lib/map/resolver.js +574 -0
- package/lib/pack/budget.js +111 -0
- package/lib/pack/code-lexer.js +360 -0
- package/lib/pack/compress.js +414 -0
- package/lib/pack/index.js +354 -0
- package/lib/pack/renderers/common.js +134 -0
- package/lib/pack/renderers/index.js +24 -0
- package/lib/pack/renderers/json.js +47 -0
- package/lib/pack/renderers/markdown.js +54 -0
- package/lib/pack/renderers/plain.js +38 -0
- package/lib/pack/renderers/xml.js +85 -0
- package/lib/pack/select.js +177 -0
- package/lib/pack/split.js +182 -0
- package/lib/pack/tree.js +35 -0
- package/lib/parsers/gitignore-parser.js +116 -107
- package/lib/parsers/method-filter-parser.js +50 -33
- package/lib/plugins/ExporterPlugin.js +3 -3
- package/lib/plugins/LanguagePlugin.js +3 -3
- package/lib/plugins/PluginManager.js +7 -13
- package/lib/policy/audit.js +312 -0
- package/lib/policy/enforce.js +125 -0
- package/lib/policy/ignore-files.js +169 -0
- package/lib/policy/index.js +53 -0
- package/lib/policy/policy.js +489 -0
- package/lib/pr/diff-parser.js +267 -0
- package/lib/pr/method-mapper.js +148 -0
- package/lib/pr/pr-context.js +829 -0
- package/lib/pr/pr-git.js +149 -0
- package/lib/pr/pr-renderer.js +644 -0
- package/lib/pr/related-tests.js +296 -0
- package/lib/rag/DataSourcePlugin.js +5 -5
- package/lib/rag/EmbeddingProvider.js +19 -8
- package/lib/rag/Indexer.js +14 -5
- package/lib/rag/VectorStoreFactory.js +3 -3
- package/lib/rag/ask.js +122 -0
- package/lib/rag/sources/FileDataSource.js +12 -9
- package/lib/rag/stores/BaseVectorStore.js +5 -5
- package/lib/rag/stores/ChromaDBStore.js +10 -10
- package/lib/rag/stores/LanceDBStore.js +29 -19
- package/lib/rag/stores/PineconeStore.js +11 -9
- package/lib/search/bm25.js +433 -0
- package/lib/search/code-units.js +201 -0
- package/lib/search/symbols.js +189 -0
- package/lib/ui/dashboard.js +120 -89
- package/lib/ui/index.js +1 -6
- package/lib/ui/init-wizard-ui.js +550 -0
- package/lib/ui/progress-bar.js +94 -88
- package/lib/ui/select-input.js +40 -32
- package/lib/ui/template-selector.js +99 -0
- package/lib/ui/wizard.js +374 -329
- package/lib/utils/claude-token-counter.js +151 -0
- package/lib/utils/cli-output.js +17 -0
- package/lib/utils/clipboard-utils.js +49 -50
- package/lib/utils/config-utils.js +81 -66
- package/lib/utils/context-versioning.js +532 -0
- package/lib/utils/error-handler.js +112 -112
- package/lib/utils/file-utils.js +153 -69
- package/lib/utils/format-converter.js +356 -283
- package/lib/utils/git-utils.js +366 -294
- package/lib/utils/llm-cost-estimator.js +281 -0
- package/lib/utils/llm-detector.js +242 -78
- package/lib/utils/logger.js +246 -237
- package/lib/utils/messages.js +355 -0
- package/lib/utils/multi-repo-manager.js +364 -0
- package/lib/utils/profile-manager.js +380 -0
- package/lib/utils/safe-read.js +61 -0
- package/lib/utils/secret-redactor.js +61 -0
- package/lib/utils/snapshot-manager.js +679 -0
- package/lib/utils/template-manager.js +378 -0
- package/lib/utils/token-utils.js +201 -170
- package/lib/utils/tokenizer-adapter.js +421 -377
- package/lib/utils/updater.js +539 -506
- package/lib/watch/FileWatcher.js +12 -8
- package/lib/watch/IncrementalAnalyzer.js +7 -7
- package/lib/why/coupling.js +81 -0
- package/lib/why/errors.js +23 -0
- package/lib/why/git.js +169 -0
- package/lib/why/history.js +108 -0
- package/lib/why/index.js +720 -0
- package/lib/why/ownership.js +79 -0
- package/lib/why/paths.js +207 -0
- package/lib/why/references.js +259 -0
- package/lib/why/render.js +722 -0
- package/lib/why/search.js +358 -0
- package/lib/why/target.js +389 -0
- package/lib/wizards/config-generator.js +257 -0
- package/lib/wizards/project-detector.js +572 -0
- package/package.json +44 -10
- package/.ctxman/logs/ctxman-2026-07-02.log +0 -156
- package/.ctxman/self-knowledge/documents.lance/_transactions/0-b3dd1558-f95b-44b9-a154-18e8c330ae7a.txn +0 -0
- package/.ctxman/self-knowledge/documents.lance/_transactions/1-44bff6d4-e552-4010-a769-e09ad822698d.txn +0 -0
- package/.ctxman/self-knowledge/documents.lance/_transactions/10-37260794-5947-4ecb-b4c8-d732f58f40f1.txn +0 -0
- package/.ctxman/self-knowledge/documents.lance/_transactions/11-0d52a883-5518-463c-9b6d-986c3af89dfd.txn +0 -0
- package/.ctxman/self-knowledge/documents.lance/_transactions/12-ae354646-52f2-49c9-90fb-2e6ed8932663.txn +0 -0
- package/.ctxman/self-knowledge/documents.lance/_transactions/13-5d7d5e57-27da-498b-b734-091b9d38da13.txn +0 -0
- package/.ctxman/self-knowledge/documents.lance/_transactions/14-558ce928-810b-4252-a364-fdd1ee6f8a25.txn +0 -0
- package/.ctxman/self-knowledge/documents.lance/_transactions/15-27e3a58f-8b64-43b8-a9f4-e3369446968b.txn +0 -0
- package/.ctxman/self-knowledge/documents.lance/_transactions/16-a110c390-e5c8-4464-b976-bc7f5195fa15.txn +0 -0
- package/.ctxman/self-knowledge/documents.lance/_transactions/17-97513225-53de-4216-b61a-6337bd0cc4ad.txn +0 -0
- package/.ctxman/self-knowledge/documents.lance/_transactions/18-5ba450f0-c527-459c-bf4f-6d79b5e4285f.txn +0 -0
- package/.ctxman/self-knowledge/documents.lance/_transactions/19-600d3a63-5225-49e9-b8b7-30e037b564ab.txn +0 -0
- package/.ctxman/self-knowledge/documents.lance/_transactions/2-48a61b4e-92cf-4342-8376-914c1b99a9f9.txn +0 -0
- package/.ctxman/self-knowledge/documents.lance/_transactions/20-121dd912-5410-410e-84ff-82c7a23604a0.txn +0 -0
- package/.ctxman/self-knowledge/documents.lance/_transactions/3-bc6844ab-7ea3-4bb3-9ee1-538ce5664a9c.txn +0 -0
- package/.ctxman/self-knowledge/documents.lance/_transactions/4-d5e1754f-dbb5-454e-a9be-2a7da8d7bcf9.txn +0 -0
- package/.ctxman/self-knowledge/documents.lance/_transactions/5-4fae93df-0568-4b6f-8acf-8bef4bba6047.txn +0 -0
- package/.ctxman/self-knowledge/documents.lance/_transactions/6-4b1b8b87-068e-49a3-ba2b-d8dae628b252.txn +0 -0
- package/.ctxman/self-knowledge/documents.lance/_transactions/7-63d040fb-d2fa-4c0f-bed0-53411918a7b0.txn +0 -0
- package/.ctxman/self-knowledge/documents.lance/_transactions/8-c5628cf6-264c-420f-aab8-bc96a9083591.txn +0 -0
- package/.ctxman/self-knowledge/documents.lance/_transactions/9-c94570aa-5e53-4fcb-971f-cecc1d1d5492.txn +0 -0
- package/.ctxman/self-knowledge/documents.lance/_versions/1.manifest +0 -0
- package/.ctxman/self-knowledge/documents.lance/_versions/10.manifest +0 -0
- package/.ctxman/self-knowledge/documents.lance/_versions/11.manifest +0 -0
- package/.ctxman/self-knowledge/documents.lance/_versions/12.manifest +0 -0
- package/.ctxman/self-knowledge/documents.lance/_versions/13.manifest +0 -0
- package/.ctxman/self-knowledge/documents.lance/_versions/14.manifest +0 -0
- package/.ctxman/self-knowledge/documents.lance/_versions/15.manifest +0 -0
- package/.ctxman/self-knowledge/documents.lance/_versions/16.manifest +0 -0
- package/.ctxman/self-knowledge/documents.lance/_versions/17.manifest +0 -0
- package/.ctxman/self-knowledge/documents.lance/_versions/18.manifest +0 -0
- package/.ctxman/self-knowledge/documents.lance/_versions/19.manifest +0 -0
- package/.ctxman/self-knowledge/documents.lance/_versions/2.manifest +0 -0
- package/.ctxman/self-knowledge/documents.lance/_versions/20.manifest +0 -0
- package/.ctxman/self-knowledge/documents.lance/_versions/21.manifest +0 -0
- package/.ctxman/self-knowledge/documents.lance/_versions/3.manifest +0 -0
- package/.ctxman/self-knowledge/documents.lance/_versions/4.manifest +0 -0
- package/.ctxman/self-knowledge/documents.lance/_versions/5.manifest +0 -0
- package/.ctxman/self-knowledge/documents.lance/_versions/6.manifest +0 -0
- package/.ctxman/self-knowledge/documents.lance/_versions/7.manifest +0 -0
- package/.ctxman/self-knowledge/documents.lance/_versions/8.manifest +0 -0
- package/.ctxman/self-knowledge/documents.lance/_versions/9.manifest +0 -0
- package/.ctxman/self-knowledge/documents.lance/data/000010010011011101000010de8621405d9d5cb06540dbf02c.lance +0 -0
- package/.ctxman/self-knowledge/documents.lance/data/0000101001011100011000003fb62a4aa8978974075c144f1c.lance +0 -0
- package/.ctxman/self-knowledge/documents.lance/data/000011010000100111001011c6cc824d628f3bb1f4e04cd5d0.lance +0 -0
- package/.ctxman/self-knowledge/documents.lance/data/001000001000011001011100d8f74349ada090b332cd1df669.lance +0 -0
- package/.ctxman/self-knowledge/documents.lance/data/01000001111010001000010045ab44467bb1e7e4d8ae06bd58.lance +0 -0
- package/.ctxman/self-knowledge/documents.lance/data/01000101101110111110011131ef904241a89e1ffb6ddaf8e3.lance +0 -0
- package/.ctxman/self-knowledge/documents.lance/data/010010110000010000011010fbfcc145c6a8a827b28057a07e.lance +0 -0
- package/.ctxman/self-knowledge/documents.lance/data/1000011000101010001110003fb4f045c3835982efa1405324.lance +0 -0
- package/.ctxman/self-knowledge/documents.lance/data/100111011101110111101011f4396b410fa06af3b028d204e6.lance +0 -0
- package/.ctxman/self-knowledge/documents.lance/data/100111100111101000001100596b174e00995d6cc2969cc8dc.lance +0 -0
- package/.ctxman/self-knowledge/documents.lance/data/1010001100011000110010102ea16b484d887c7c9725feffd6.lance +0 -0
- package/.ctxman/self-knowledge/documents.lance/data/101101100010101111110111c5230944d9b3af7301b6392dda.lance +0 -0
- package/.ctxman/self-knowledge/documents.lance/data/1011011100100111111110105892bf41b5a530fedd7cf516f1.lance +0 -0
- package/.ctxman/self-knowledge/documents.lance/data/10111110000010001011110164e8774742bade87b483c10f36.lance +0 -0
- package/.ctxman/self-knowledge/documents.lance/data/1100000100010110001110103ffaca40a2a42c85067ebaf9ae.lance +0 -0
- package/.ctxman/self-knowledge/documents.lance/data/110000010001110110101110664bde4c76a78dfc14b05e83b8.lance +0 -0
- package/.ctxman/self-knowledge/documents.lance/data/11001100111000010100110050645c4281b1f35b2022234f7f.lance +0 -0
- package/.ctxman/self-knowledge/documents.lance/data/1101111011001100110111017214924a368f5719eab2246619.lance +0 -0
- package/.ctxman/self-knowledge/documents.lance/data/1110000111001010001000002150c24dcab653d39d07032e63.lance +0 -0
- package/.ctxman/self-knowledge/documents.lance/data/1110100000000100000011014d20534967a18b5c87721659ff.lance +0 -0
- 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();
|