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.
- 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 +380 -136
- package/bin/cli.js +1880 -529
- package/bin/cm-ask.js +18 -13
- package/bin/cm-audit.js +124 -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 +683 -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 +675 -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 +113 -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 +294 -0
- package/lib/policy/enforce.js +125 -0
- package/lib/policy/ignore-files.js +169 -0
- package/lib/policy/index.js +52 -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/sources/FileDataSource.js +6 -6
- 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 +243 -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 +255 -0
- package/lib/wizards/project-detector.js +571 -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
|
@@ -0,0 +1,424 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MCP client configuration: where each client keeps its MCP servers, the entry that starts
|
|
3
|
+
* ctxman, and merging that entry into (or removing it from) the client's JSON file.
|
|
4
|
+
*
|
|
5
|
+
* Locations follow the clients' documentation (see docs/commands/mcp.md). Everything that
|
|
6
|
+
* depends on the machine (platform, home directory, environment, project directory) is
|
|
7
|
+
* passed in, so tests never touch the real home directory.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import fs from 'fs';
|
|
11
|
+
import os from 'os';
|
|
12
|
+
import path from 'path';
|
|
13
|
+
import { fileURLToPath } from 'url';
|
|
14
|
+
|
|
15
|
+
/** Absolute path of bin/cli.js, which `ctxman mcp` runs */
|
|
16
|
+
export const CLI_PATH = fileURLToPath(new URL('../../../bin/cli.js', import.meta.url));
|
|
17
|
+
export const DEFAULT_SERVER_NAME = 'ctxman';
|
|
18
|
+
export const SCOPES = ['user', 'project'];
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Supported clients.
|
|
22
|
+
* - key: object in the config file that holds the servers
|
|
23
|
+
* - scopes: scopes with a config file ctxman writes
|
|
24
|
+
* - manualScopes: scopes for which ctxman prints a command or instructions instead
|
|
25
|
+
* - stdioType: whether entries carry `"type": "stdio"`
|
|
26
|
+
*/
|
|
27
|
+
export const CLIENTS = {
|
|
28
|
+
'claude-desktop': {
|
|
29
|
+
label: 'Claude Desktop',
|
|
30
|
+
key: 'mcpServers',
|
|
31
|
+
defaultScope: 'user',
|
|
32
|
+
scopes: ['user'],
|
|
33
|
+
manualScopes: [],
|
|
34
|
+
stdioType: false,
|
|
35
|
+
docs: 'https://modelcontextprotocol.io/docs/2026-07-28/develop/connect-local-servers',
|
|
36
|
+
},
|
|
37
|
+
'claude-code': {
|
|
38
|
+
label: 'Claude Code',
|
|
39
|
+
key: 'mcpServers',
|
|
40
|
+
defaultScope: 'project',
|
|
41
|
+
scopes: ['project'],
|
|
42
|
+
manualScopes: ['user'],
|
|
43
|
+
stdioType: true,
|
|
44
|
+
docs: 'https://code.claude.com/docs/en/mcp',
|
|
45
|
+
},
|
|
46
|
+
cursor: {
|
|
47
|
+
label: 'Cursor',
|
|
48
|
+
key: 'mcpServers',
|
|
49
|
+
defaultScope: 'user',
|
|
50
|
+
scopes: ['user', 'project'],
|
|
51
|
+
manualScopes: [],
|
|
52
|
+
stdioType: false,
|
|
53
|
+
docs: 'https://cursor.com/docs/mcp',
|
|
54
|
+
},
|
|
55
|
+
vscode: {
|
|
56
|
+
label: 'VS Code',
|
|
57
|
+
key: 'servers',
|
|
58
|
+
defaultScope: 'project',
|
|
59
|
+
scopes: ['project'],
|
|
60
|
+
manualScopes: ['user'],
|
|
61
|
+
stdioType: true,
|
|
62
|
+
docs: 'https://code.visualstudio.com/docs/agents/reference/mcp-configuration',
|
|
63
|
+
},
|
|
64
|
+
windsurf: {
|
|
65
|
+
label: 'Windsurf',
|
|
66
|
+
key: 'mcpServers',
|
|
67
|
+
defaultScope: 'user',
|
|
68
|
+
// The documented location differs between the Cascade docs and the editor's discovery
|
|
69
|
+
// file, so ctxman only writes it when given --file
|
|
70
|
+
scopes: [],
|
|
71
|
+
manualScopes: ['user'],
|
|
72
|
+
stdioType: false,
|
|
73
|
+
docs: 'https://docs.windsurf.com/windsurf/cascade/mcp',
|
|
74
|
+
},
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Validate a client name
|
|
79
|
+
* @param {string} client
|
|
80
|
+
* @returns {Object} The client description
|
|
81
|
+
* @throws {Error} For an unknown client
|
|
82
|
+
*/
|
|
83
|
+
export function getClient(client) {
|
|
84
|
+
const info = CLIENTS[client];
|
|
85
|
+
if (!info) {
|
|
86
|
+
throw new Error(
|
|
87
|
+
`Unknown client "${client ?? ''}" (expected one of: ${Object.keys(CLIENTS).join(', ')})`
|
|
88
|
+
);
|
|
89
|
+
}
|
|
90
|
+
return info;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Quote an argument for the user's shell when it needs quoting
|
|
95
|
+
* @param {string} arg
|
|
96
|
+
* @param {string} [platform=process.platform]
|
|
97
|
+
* @returns {string}
|
|
98
|
+
*/
|
|
99
|
+
export function shellQuote(arg, platform = process.platform) {
|
|
100
|
+
if (/^[\w@%+=:,./\\-]+$/.test(arg)) return arg;
|
|
101
|
+
if (platform === 'win32') {
|
|
102
|
+
// Windows argument parsing: backslashes are literal unless a quote follows them, so
|
|
103
|
+
// only the runs before a quote, or before the closing quote, are doubled
|
|
104
|
+
return `"${arg.replace(/(\\*)"/g, '$1$1\\"').replace(/(\\+)$/, '$1$1')}"`;
|
|
105
|
+
}
|
|
106
|
+
return `'${arg.replace(/'/g, "'\\''")}'`;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* The server entry for a client's config file.
|
|
111
|
+
*
|
|
112
|
+
* @param {string} client
|
|
113
|
+
* @param {Object} [options]
|
|
114
|
+
* @param {boolean} [options.npx=false] - Start with `npx -y ctxman mcp` instead of this install
|
|
115
|
+
* @param {string} [options.execPath=process.execPath] - Absolute path of node
|
|
116
|
+
* @param {string} [options.cliPath=CLI_PATH] - Absolute path of bin/cli.js
|
|
117
|
+
* @param {string} [options.root] - Pass `--root <dir>` to the server
|
|
118
|
+
* @param {string} [options.platform=process.platform] - npx goes through `cmd /c` on Windows
|
|
119
|
+
* @returns {{type?: string, command: string, args: string[]}}
|
|
120
|
+
*/
|
|
121
|
+
export function buildServerEntry(
|
|
122
|
+
client,
|
|
123
|
+
{
|
|
124
|
+
npx = false,
|
|
125
|
+
execPath = process.execPath,
|
|
126
|
+
cliPath = CLI_PATH,
|
|
127
|
+
root,
|
|
128
|
+
platform = process.platform,
|
|
129
|
+
} = {}
|
|
130
|
+
) {
|
|
131
|
+
const info = getClient(client);
|
|
132
|
+
let command;
|
|
133
|
+
let args;
|
|
134
|
+
if (!npx) {
|
|
135
|
+
command = execPath;
|
|
136
|
+
args = [cliPath, 'mcp'];
|
|
137
|
+
} else if (platform === 'win32') {
|
|
138
|
+
// npx is a .cmd script on Windows, which clients cannot spawn directly
|
|
139
|
+
command = 'cmd';
|
|
140
|
+
args = ['/c', 'npx', '-y', 'ctxman', 'mcp'];
|
|
141
|
+
} else {
|
|
142
|
+
command = 'npx';
|
|
143
|
+
args = ['-y', 'ctxman', 'mcp'];
|
|
144
|
+
}
|
|
145
|
+
if (root) args.push('--root', root);
|
|
146
|
+
return info.stdioType ? { type: 'stdio', command, args } : { command, args };
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* Where a client keeps its MCP servers for a scope.
|
|
151
|
+
*
|
|
152
|
+
* @param {string} client
|
|
153
|
+
* @param {string} scope - 'user' or 'project'
|
|
154
|
+
* @param {Object} [context]
|
|
155
|
+
* @param {string} [context.platform=process.platform]
|
|
156
|
+
* @param {string} [context.homeDir=os.homedir()]
|
|
157
|
+
* @param {Object} [context.env=process.env] - APPDATA (Windows), XDG_CONFIG_HOME (Linux)
|
|
158
|
+
* @param {string} [context.projectDir=process.cwd()]
|
|
159
|
+
* @returns {{path: string|null, reason?: string}} `path` is null when ctxman does not write
|
|
160
|
+
* that configuration; `reason` then says why
|
|
161
|
+
*/
|
|
162
|
+
export function resolveConfigPath(
|
|
163
|
+
client,
|
|
164
|
+
scope,
|
|
165
|
+
{
|
|
166
|
+
platform = process.platform,
|
|
167
|
+
homeDir = os.homedir(),
|
|
168
|
+
env = process.env,
|
|
169
|
+
projectDir = process.cwd(),
|
|
170
|
+
} = {}
|
|
171
|
+
) {
|
|
172
|
+
getClient(client);
|
|
173
|
+
if (!SCOPES.includes(scope)) {
|
|
174
|
+
throw new Error(`Unknown scope "${scope}" (expected user or project)`);
|
|
175
|
+
}
|
|
176
|
+
const p = platform === 'win32' ? path.win32 : path.posix;
|
|
177
|
+
|
|
178
|
+
switch (client) {
|
|
179
|
+
case 'claude-desktop': {
|
|
180
|
+
if (scope === 'project') {
|
|
181
|
+
return { path: null, reason: 'Claude Desktop has no project configuration.' };
|
|
182
|
+
}
|
|
183
|
+
let dir;
|
|
184
|
+
if (platform === 'darwin') {
|
|
185
|
+
dir = p.join(homeDir, 'Library', 'Application Support', 'Claude');
|
|
186
|
+
} else if (platform === 'win32') {
|
|
187
|
+
dir = p.join(env.APPDATA || p.join(homeDir, 'AppData', 'Roaming'), 'Claude');
|
|
188
|
+
} else {
|
|
189
|
+
dir = p.join(env.XDG_CONFIG_HOME || p.join(homeDir, '.config'), 'Claude');
|
|
190
|
+
}
|
|
191
|
+
return { path: p.join(dir, 'claude_desktop_config.json') };
|
|
192
|
+
}
|
|
193
|
+
case 'claude-code':
|
|
194
|
+
if (scope === 'project') return { path: p.join(projectDir, '.mcp.json') };
|
|
195
|
+
return {
|
|
196
|
+
path: null,
|
|
197
|
+
reason:
|
|
198
|
+
'Claude Code keeps user-scope servers in ~/.claude.json together with its other ' +
|
|
199
|
+
'state; ctxman does not edit that file. Add the server with the claude CLI instead.',
|
|
200
|
+
};
|
|
201
|
+
case 'cursor':
|
|
202
|
+
return {
|
|
203
|
+
path:
|
|
204
|
+
scope === 'project'
|
|
205
|
+
? p.join(projectDir, '.cursor', 'mcp.json')
|
|
206
|
+
: p.join(homeDir, '.cursor', 'mcp.json'),
|
|
207
|
+
};
|
|
208
|
+
case 'vscode':
|
|
209
|
+
if (scope === 'project') return { path: p.join(projectDir, '.vscode', 'mcp.json') };
|
|
210
|
+
return {
|
|
211
|
+
path: null,
|
|
212
|
+
reason:
|
|
213
|
+
'VS Code keeps user-level MCP servers in the user profile; add the server with ' +
|
|
214
|
+
'`code --add-mcp` or the "MCP: Open User Configuration" command.',
|
|
215
|
+
};
|
|
216
|
+
case 'windsurf':
|
|
217
|
+
default:
|
|
218
|
+
if (scope === 'project') {
|
|
219
|
+
return { path: null, reason: 'Windsurf has no project configuration.' };
|
|
220
|
+
}
|
|
221
|
+
return {
|
|
222
|
+
path: null,
|
|
223
|
+
reason:
|
|
224
|
+
'The Windsurf docs name ~/.config/devin/mcp_config.json ($XDG_CONFIG_HOME/devin, ' +
|
|
225
|
+
'%APPDATA%\\devin on Windows) for Cascade, while the editor reads ' +
|
|
226
|
+
'~/.codeium/windsurf/mcp_config.json; open the file from Cascade > MCPs > ' +
|
|
227
|
+
'"Open MCP config file", or pass it with --file.',
|
|
228
|
+
};
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
/**
|
|
233
|
+
* The fragment of a config file that holds the entry: `{ [key]: { [name]: entry } }`
|
|
234
|
+
* @param {string} client
|
|
235
|
+
* @param {string} name
|
|
236
|
+
* @param {Object} entry
|
|
237
|
+
* @returns {Object}
|
|
238
|
+
*/
|
|
239
|
+
export function buildSnippet(client, name, entry) {
|
|
240
|
+
return { [getClient(client).key]: { [name]: entry } };
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
/**
|
|
244
|
+
* Commands that add the server where ctxman does not write the file itself
|
|
245
|
+
* @param {string} client
|
|
246
|
+
* @param {string} scope
|
|
247
|
+
* @param {string} name
|
|
248
|
+
* @param {Object} entry
|
|
249
|
+
* @param {string} [platform=process.platform]
|
|
250
|
+
* @returns {string[]} Shell commands (empty when there is none)
|
|
251
|
+
*/
|
|
252
|
+
export function manualCommands(client, scope, name, entry, platform = process.platform) {
|
|
253
|
+
const q = (arg) => shellQuote(arg, platform);
|
|
254
|
+
if (client === 'claude-code' && scope === 'user') {
|
|
255
|
+
return [
|
|
256
|
+
[
|
|
257
|
+
'claude mcp add --scope user --transport stdio',
|
|
258
|
+
q(name),
|
|
259
|
+
'--',
|
|
260
|
+
q(entry.command),
|
|
261
|
+
...entry.args.map(q),
|
|
262
|
+
].join(' '),
|
|
263
|
+
];
|
|
264
|
+
}
|
|
265
|
+
if (client === 'vscode' && scope === 'user') {
|
|
266
|
+
return [`code --add-mcp ${q(JSON.stringify({ name, ...entry }))}`];
|
|
267
|
+
}
|
|
268
|
+
return [];
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
/**
|
|
272
|
+
* Key-order-independent equality of JSON values
|
|
273
|
+
* @private
|
|
274
|
+
*/
|
|
275
|
+
function sameJson(a, b) {
|
|
276
|
+
const canonical = (value) => {
|
|
277
|
+
if (Array.isArray(value)) return value.map(canonical);
|
|
278
|
+
if (value && typeof value === 'object') {
|
|
279
|
+
return Object.fromEntries(
|
|
280
|
+
Object.keys(value)
|
|
281
|
+
.sort()
|
|
282
|
+
.map((k) => [k, canonical(value[k])])
|
|
283
|
+
);
|
|
284
|
+
}
|
|
285
|
+
return value;
|
|
286
|
+
};
|
|
287
|
+
return JSON.stringify(canonical(a)) === JSON.stringify(canonical(b));
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
const isPlainObject = (value) =>
|
|
291
|
+
value !== null && typeof value === 'object' && !Array.isArray(value);
|
|
292
|
+
|
|
293
|
+
/**
|
|
294
|
+
* Read a client config file
|
|
295
|
+
* @param {string} file
|
|
296
|
+
* @param {string} key - Object that holds the servers
|
|
297
|
+
* @returns {{exists: boolean, config: Object}}
|
|
298
|
+
* @throws {Error} When the file is not a JSON object (comments are not supported)
|
|
299
|
+
*/
|
|
300
|
+
export function readConfigFile(file, key) {
|
|
301
|
+
let text;
|
|
302
|
+
try {
|
|
303
|
+
text = fs.readFileSync(file, 'utf-8');
|
|
304
|
+
} catch (error) {
|
|
305
|
+
if (error.code === 'ENOENT') return { exists: false, config: {} };
|
|
306
|
+
throw error;
|
|
307
|
+
}
|
|
308
|
+
if (!text.trim()) return { exists: true, config: {} };
|
|
309
|
+
|
|
310
|
+
let config;
|
|
311
|
+
try {
|
|
312
|
+
config = JSON.parse(text);
|
|
313
|
+
} catch (error) {
|
|
314
|
+
throw new Error(
|
|
315
|
+
`${file} is not valid JSON (${error.message}); ctxman does not rewrite files with ` +
|
|
316
|
+
'comments or syntax errors. Fix it, or add the entry by hand (ctxman mcp config).'
|
|
317
|
+
);
|
|
318
|
+
}
|
|
319
|
+
if (!isPlainObject(config)) {
|
|
320
|
+
throw new Error(`${file} does not hold a JSON object`);
|
|
321
|
+
}
|
|
322
|
+
if (config[key] !== undefined && !isPlainObject(config[key])) {
|
|
323
|
+
throw new Error(`"${key}" in ${file} is not an object`);
|
|
324
|
+
}
|
|
325
|
+
return { exists: true, config };
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
/**
|
|
329
|
+
* Write a config file as 2-space JSON: back up the previous version to `<file>.bak`, create
|
|
330
|
+
* missing directories, replace the file in one rename
|
|
331
|
+
* @param {string} file
|
|
332
|
+
* @param {Object} config
|
|
333
|
+
* @param {boolean} existed - Whether there is a previous version to back up
|
|
334
|
+
* @returns {string|null} Path of the backup
|
|
335
|
+
*/
|
|
336
|
+
export function writeConfigFile(file, config, existed) {
|
|
337
|
+
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
338
|
+
let backup = null;
|
|
339
|
+
// Keep the permissions of the file being replaced
|
|
340
|
+
let mode = 0o644;
|
|
341
|
+
if (existed) {
|
|
342
|
+
backup = `${file}.bak`;
|
|
343
|
+
fs.copyFileSync(file, backup);
|
|
344
|
+
mode = fs.statSync(file).mode & 0o777;
|
|
345
|
+
}
|
|
346
|
+
const temp = `${file}.${process.pid}.tmp`;
|
|
347
|
+
fs.writeFileSync(temp, `${JSON.stringify(config, null, 2)}\n`, { mode });
|
|
348
|
+
fs.renameSync(temp, file);
|
|
349
|
+
return backup;
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
/**
|
|
353
|
+
* Add the ctxman entry to a client config file.
|
|
354
|
+
*
|
|
355
|
+
* @param {Object} options
|
|
356
|
+
* @param {string} options.client
|
|
357
|
+
* @param {string} options.file - Config file to change
|
|
358
|
+
* @param {Object} options.entry - From buildServerEntry()
|
|
359
|
+
* @param {string} [options.name='ctxman']
|
|
360
|
+
* @param {boolean} [options.force=false] - Replace a different existing entry of that name
|
|
361
|
+
* @param {boolean} [options.dryRun=false] - Report without writing
|
|
362
|
+
* @returns {{status: 'created'|'added'|'updated'|'unchanged'|'conflict', file: string,
|
|
363
|
+
* content: string, backup: string|null, existing?: Object}} `content` is the file as it
|
|
364
|
+
* is (or would be) written; `conflict` means an entry exists and force was not given
|
|
365
|
+
*/
|
|
366
|
+
export function installServer({
|
|
367
|
+
client,
|
|
368
|
+
file,
|
|
369
|
+
entry,
|
|
370
|
+
name = DEFAULT_SERVER_NAME,
|
|
371
|
+
force = false,
|
|
372
|
+
dryRun = false,
|
|
373
|
+
}) {
|
|
374
|
+
const { key } = getClient(client);
|
|
375
|
+
const { exists, config } = readConfigFile(file, key);
|
|
376
|
+
const servers = config[key] ?? {};
|
|
377
|
+
const existing = servers[name];
|
|
378
|
+
|
|
379
|
+
let status;
|
|
380
|
+
if (existing === undefined) status = exists ? 'added' : 'created';
|
|
381
|
+
else if (sameJson(existing, entry)) status = 'unchanged';
|
|
382
|
+
else if (force) status = 'updated';
|
|
383
|
+
else status = 'conflict';
|
|
384
|
+
|
|
385
|
+
if (status === 'unchanged' || status === 'conflict') {
|
|
386
|
+
return {
|
|
387
|
+
status,
|
|
388
|
+
file,
|
|
389
|
+
content: `${JSON.stringify(config, null, 2)}\n`,
|
|
390
|
+
backup: null,
|
|
391
|
+
...(existing !== undefined ? { existing } : {}),
|
|
392
|
+
};
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
// Assigning to an existing key keeps its position among the other servers
|
|
396
|
+
const next = { ...config, [key]: { ...servers, [name]: entry } };
|
|
397
|
+
const content = `${JSON.stringify(next, null, 2)}\n`;
|
|
398
|
+
const backup = dryRun ? null : writeConfigFile(file, next, exists);
|
|
399
|
+
return { status, file, content, backup };
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
/**
|
|
403
|
+
* Remove the ctxman entry from a client config file.
|
|
404
|
+
*
|
|
405
|
+
* @param {Object} options
|
|
406
|
+
* @param {string} options.client
|
|
407
|
+
* @param {string} options.file
|
|
408
|
+
* @param {string} [options.name='ctxman']
|
|
409
|
+
* @param {boolean} [options.dryRun=false]
|
|
410
|
+
* @returns {{status: 'removed'|'absent', file: string, content: string|null,
|
|
411
|
+
* backup: string|null}}
|
|
412
|
+
*/
|
|
413
|
+
export function uninstallServer({ client, file, name = DEFAULT_SERVER_NAME, dryRun = false }) {
|
|
414
|
+
const { key } = getClient(client);
|
|
415
|
+
const { exists, config } = readConfigFile(file, key);
|
|
416
|
+
if (!exists || !config[key] || !(name in config[key])) {
|
|
417
|
+
return { status: 'absent', file, content: null, backup: null };
|
|
418
|
+
}
|
|
419
|
+
const { [name]: _removed, ...servers } = config[key];
|
|
420
|
+
const next = { ...config, [key]: servers };
|
|
421
|
+
const content = `${JSON.stringify(next, null, 2)}\n`;
|
|
422
|
+
const backup = dryRun ? null : writeConfigFile(file, next, true);
|
|
423
|
+
return { status: 'removed', file, content, backup };
|
|
424
|
+
}
|