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