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
|
@@ -0,0 +1,354 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pack: file contents in one LLM-ready document
|
|
3
|
+
*
|
|
4
|
+
* Pipeline per file: read -> skip binary -> compress (optional) -> line numbers (optional)
|
|
5
|
+
* -> redact secrets (default) -> count tokens. Then the budget picks files (optional), the
|
|
6
|
+
* renderer for the style builds the document, and --split cuts it into parts (optional).
|
|
7
|
+
*
|
|
8
|
+
* Every count uses one tiktoken encoding: the target model's (o200k_base for recent OpenAI
|
|
9
|
+
* models) when one is given, cl100k_base otherwise.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import fs from 'fs';
|
|
13
|
+
import TokenUtils from '../utils/token-utils.js';
|
|
14
|
+
import { LLMDetector } from '../utils/llm-detector.js';
|
|
15
|
+
import { redactSecrets } from '../utils/secret-redactor.js';
|
|
16
|
+
import { selectFiles, PackError } from './select.js';
|
|
17
|
+
import { compressCode } from './compress.js';
|
|
18
|
+
import { buildTree } from './tree.js';
|
|
19
|
+
import { rankEntries, fitToBudget } from './budget.js';
|
|
20
|
+
import { splitIntoParts } from './split.js';
|
|
21
|
+
import { getRenderer, STYLES } from './renderers/index.js';
|
|
22
|
+
|
|
23
|
+
export { PackError, STYLES };
|
|
24
|
+
|
|
25
|
+
/** Files larger than this are skipped unless `maxFileSize` says otherwise */
|
|
26
|
+
export const DEFAULT_MAX_FILE_SIZE = 1024 * 1024;
|
|
27
|
+
/** Budget of the MCP tool when the client gives none: whole projects would flood its context */
|
|
28
|
+
export const TOOL_DEFAULT_BUDGET = 20000;
|
|
29
|
+
// Omitted files listed by name in the document; the rest are counted
|
|
30
|
+
const OMITTED_LISTED = 20;
|
|
31
|
+
// A NUL byte in the first bytes marks a binary file
|
|
32
|
+
const BINARY_SNIFF_BYTES = 8000;
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Prefix each line with its number, right-aligned
|
|
36
|
+
* @param {string} text
|
|
37
|
+
* @param {number[]|null} numbers - Number of each line (null: 1, 2, 3, ...)
|
|
38
|
+
* @returns {string}
|
|
39
|
+
*/
|
|
40
|
+
export function addLineNumbers(text, numbers = null) {
|
|
41
|
+
const lines = text.split('\n');
|
|
42
|
+
const count = lines.length - (text.endsWith('\n') ? 1 : 0);
|
|
43
|
+
const numberOf = (i) => (numbers ? numbers[i] : i + 1);
|
|
44
|
+
let widest = 0;
|
|
45
|
+
for (let i = 0; i < count; i++) widest = Math.max(widest, numberOf(i));
|
|
46
|
+
const width = String(widest).length;
|
|
47
|
+
return lines
|
|
48
|
+
.map((line, i) => {
|
|
49
|
+
if (i >= count) return line;
|
|
50
|
+
const number = String(numberOf(i)).padStart(width);
|
|
51
|
+
return line === '' || line === '\r' ? `${number} |${line}` : `${number} | ${line}`;
|
|
52
|
+
})
|
|
53
|
+
.join('\n');
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const positiveInteger = (value, name) => {
|
|
57
|
+
if (value === undefined || value === null) return null;
|
|
58
|
+
const number = Number(value);
|
|
59
|
+
if (!Number.isInteger(number) || number <= 0) {
|
|
60
|
+
throw new PackError('INVALID_OPTION', `${name} must be a positive integer, got ${value}`);
|
|
61
|
+
}
|
|
62
|
+
return number;
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
const stringList = (value, name) => {
|
|
66
|
+
if (value === undefined || value === null) return [];
|
|
67
|
+
const list = Array.isArray(value) ? value : [value];
|
|
68
|
+
if (!list.every((item) => typeof item === 'string')) {
|
|
69
|
+
throw new PackError('INVALID_OPTION', `${name} must be a list of strings`);
|
|
70
|
+
}
|
|
71
|
+
return list;
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
/** Profile of the target model, or null without one; unknown ids are an error */
|
|
75
|
+
function targetProfile(model) {
|
|
76
|
+
if (model === undefined || model === null || model === '') return null;
|
|
77
|
+
const id = LLMDetector.resolveModelId(String(model));
|
|
78
|
+
if (!id) {
|
|
79
|
+
throw new PackError(
|
|
80
|
+
'UNKNOWN_MODEL',
|
|
81
|
+
`Unknown model "${model}" (ctxman --list-llms lists the known ids)`
|
|
82
|
+
);
|
|
83
|
+
}
|
|
84
|
+
return LLMDetector.getProfile(id);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function normalizeOptions(options) {
|
|
88
|
+
const style = options.style ?? 'xml';
|
|
89
|
+
if (!STYLES.includes(style)) {
|
|
90
|
+
throw new PackError('INVALID_OPTION', `Unknown style "${style}" (use ${STYLES.join(', ')})`);
|
|
91
|
+
}
|
|
92
|
+
const profile = targetProfile(options.targetModel);
|
|
93
|
+
return {
|
|
94
|
+
style,
|
|
95
|
+
include: stringList(options.include, 'include'),
|
|
96
|
+
exclude: stringList(options.exclude, 'exclude'),
|
|
97
|
+
files: options.files ? stringList(options.files, 'files') : null,
|
|
98
|
+
cwd: options.cwd,
|
|
99
|
+
excludeFiles: stringList(options.excludeFiles, 'excludeFiles'),
|
|
100
|
+
maxFileSize: positiveInteger(options.maxFileSize, 'maxFileSize') ?? DEFAULT_MAX_FILE_SIZE,
|
|
101
|
+
tree: options.tree !== false,
|
|
102
|
+
header: options.header ? String(options.header).trimEnd() : null,
|
|
103
|
+
instructions: options.instructions ? String(options.instructions).trimEnd() : null,
|
|
104
|
+
lineNumbers: Boolean(options.lineNumbers),
|
|
105
|
+
compress: Boolean(options.compress),
|
|
106
|
+
redact: options.redact !== false,
|
|
107
|
+
budget: positiveInteger(options.budget, 'budget'),
|
|
108
|
+
split: positiveInteger(options.split, 'split'),
|
|
109
|
+
profile,
|
|
110
|
+
encoding: TokenUtils.encodingFor(profile?.id),
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Read and transform one file
|
|
116
|
+
* @returns {object|{skipped: string}} Entry, or the reason it is skipped
|
|
117
|
+
*/
|
|
118
|
+
function prepareFile(file, options) {
|
|
119
|
+
let buffer;
|
|
120
|
+
try {
|
|
121
|
+
buffer = fs.readFileSync(file.absolutePath);
|
|
122
|
+
} catch (_error) {
|
|
123
|
+
return { skipped: 'unreadable' };
|
|
124
|
+
}
|
|
125
|
+
if (buffer.subarray(0, BINARY_SNIFF_BYTES).includes(0)) return { skipped: 'binary' };
|
|
126
|
+
|
|
127
|
+
let content = buffer.toString('utf8');
|
|
128
|
+
if (content.charCodeAt(0) === 0xfeff) content = content.slice(1);
|
|
129
|
+
const totalLines = content.split('\n').length - (content.endsWith('\n') ? 1 : 0);
|
|
130
|
+
const count = (text) => TokenUtils.calculate(text, file.path, options.encoding);
|
|
131
|
+
|
|
132
|
+
let lineNumbers = null;
|
|
133
|
+
let compression = null;
|
|
134
|
+
if (options.compress) {
|
|
135
|
+
const result = compressCode(content, file.path);
|
|
136
|
+
if (result.elided > 0) {
|
|
137
|
+
compression = {
|
|
138
|
+
bodies: result.elided,
|
|
139
|
+
originalTokens: count(content),
|
|
140
|
+
compressedTokens: count(result.text),
|
|
141
|
+
};
|
|
142
|
+
content = result.text;
|
|
143
|
+
lineNumbers = result.lineMap;
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// Numbers first, so a secret masked below cannot shift the numbers of later lines
|
|
148
|
+
if (options.lineNumbers) content = addLineNumbers(content, lineNumbers);
|
|
149
|
+
|
|
150
|
+
let findings = [];
|
|
151
|
+
if (options.redact) {
|
|
152
|
+
const redacted = redactSecrets(content);
|
|
153
|
+
if (redacted.findings.length) {
|
|
154
|
+
// A masked private key block becomes one line: line positions no longer line up
|
|
155
|
+
if (redacted.text.split('\n').length !== content.split('\n').length) lineNumbers = null;
|
|
156
|
+
content = redacted.text;
|
|
157
|
+
findings = redacted.findings;
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
return {
|
|
162
|
+
path: file.path,
|
|
163
|
+
absolutePath: file.absolutePath,
|
|
164
|
+
modified: file.modified,
|
|
165
|
+
content,
|
|
166
|
+
tokens: count(content),
|
|
167
|
+
totalLines,
|
|
168
|
+
lineNumbers,
|
|
169
|
+
compression,
|
|
170
|
+
findings,
|
|
171
|
+
};
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/** Mask secrets in user text (header, instructions) and note what was found */
|
|
175
|
+
function redactText(text, options, findings) {
|
|
176
|
+
if (!text || !options.redact) return text;
|
|
177
|
+
const redacted = redactSecrets(text);
|
|
178
|
+
findings.push(...redacted.findings);
|
|
179
|
+
return redacted.text;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
function omittedSection(omitted) {
|
|
183
|
+
if (!omitted.length) return null;
|
|
184
|
+
const rest = omitted.slice(OMITTED_LISTED);
|
|
185
|
+
const sum = (entries) => entries.reduce((total, entry) => total + entry.tokens, 0);
|
|
186
|
+
return {
|
|
187
|
+
files: omitted.slice(0, OMITTED_LISTED).map(({ path, tokens }) => ({ path, tokens })),
|
|
188
|
+
count: omitted.length,
|
|
189
|
+
tokens: sum(omitted),
|
|
190
|
+
more: rest.length,
|
|
191
|
+
moreTokens: sum(rest),
|
|
192
|
+
};
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
/** How the document compares with the target model's context window */
|
|
196
|
+
function modelFit(profile, tokens, encoding) {
|
|
197
|
+
if (!profile) return null;
|
|
198
|
+
return {
|
|
199
|
+
id: profile.id,
|
|
200
|
+
name: profile.name,
|
|
201
|
+
vendor: profile.vendor,
|
|
202
|
+
encoding: TokenUtils.isExact() ? encoding : 'estimate',
|
|
203
|
+
// tiktoken is the model's own tokenizer only for OpenAI models with a published encoding
|
|
204
|
+
exact: TokenUtils.isExact() && profile.tokenizer?.exact === true,
|
|
205
|
+
contextWindow: profile.contextWindow,
|
|
206
|
+
maxRecommendedInput: profile.maxRecommendedInput ?? null,
|
|
207
|
+
fits: tokens <= profile.contextWindow,
|
|
208
|
+
percentOfWindow: (tokens / profile.contextWindow) * 100,
|
|
209
|
+
};
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
/**
|
|
213
|
+
* Pack the contents of a project's files into one document
|
|
214
|
+
* @param {string} root - Project root
|
|
215
|
+
* @param {object} [options]
|
|
216
|
+
* @param {'xml'|'markdown'|'plain'|'json'} [options.style='xml']
|
|
217
|
+
* @param {string[]} [options.include] - Gitignore-style globs: keep only matching files
|
|
218
|
+
* @param {string[]} [options.exclude] - Gitignore-style globs: leave matching files out
|
|
219
|
+
* @param {string[]} [options.files] - Explicit file list instead of scanning the root
|
|
220
|
+
* @param {string} [options.cwd] - Base for relative `files` entries (default: root)
|
|
221
|
+
* @param {string[]} [options.excludeFiles] - Absolute paths to leave out
|
|
222
|
+
* @param {number} [options.maxFileSize=1048576] - Skip larger files (bytes)
|
|
223
|
+
* @param {boolean} [options.tree=true] - Directory tree at the top
|
|
224
|
+
* @param {string} [options.header] - Text at the top
|
|
225
|
+
* @param {string} [options.instructions] - Text at the end, after the files
|
|
226
|
+
* @param {boolean} [options.lineNumbers=false] - Number every line
|
|
227
|
+
* @param {boolean} [options.compress=false] - Replace function bodies with placeholders
|
|
228
|
+
* @param {boolean} [options.redact=true] - Mask secrets
|
|
229
|
+
* @param {number} [options.budget] - Token limit for the document; highest-ranked files first
|
|
230
|
+
* @param {number} [options.split] - Token limit per part; adds `parts` to the result
|
|
231
|
+
* @param {string} [options.targetModel] - Count with this model's encoding and report fit
|
|
232
|
+
* @returns {Promise<object>} {text, style, encoding, totalTokens, files, omitted, skipped,
|
|
233
|
+
* redactions, compression, budget, model, parts, splitFiles}
|
|
234
|
+
*/
|
|
235
|
+
export async function packContext(root, options = {}) {
|
|
236
|
+
const opts = normalizeOptions(options);
|
|
237
|
+
const renderer = getRenderer(opts.style);
|
|
238
|
+
const count = (text) => TokenUtils.calculate(text, `pack${renderer.extension}`, opts.encoding);
|
|
239
|
+
|
|
240
|
+
const selection = selectFiles(root, opts);
|
|
241
|
+
const skipped = [...selection.skipped];
|
|
242
|
+
const entries = [];
|
|
243
|
+
for (const file of selection.files) {
|
|
244
|
+
const entry = prepareFile(file, opts);
|
|
245
|
+
if (entry.skipped) skipped.push({ path: file.path, reason: entry.skipped });
|
|
246
|
+
else entries.push(entry);
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
const textFindings = [];
|
|
250
|
+
const header = redactText(opts.header, opts, textFindings);
|
|
251
|
+
const instructions = redactText(opts.instructions, opts, textFindings);
|
|
252
|
+
|
|
253
|
+
const docFor = (packed, omitted) => ({
|
|
254
|
+
header,
|
|
255
|
+
partLabel: null,
|
|
256
|
+
part: null,
|
|
257
|
+
tree: opts.tree ? buildTree(packed.map((entry) => entry.path)) : null,
|
|
258
|
+
files: packed,
|
|
259
|
+
firstIndex: 1,
|
|
260
|
+
omitted: omittedSection(omitted),
|
|
261
|
+
instructions,
|
|
262
|
+
});
|
|
263
|
+
const measure = (packed, omitted) => {
|
|
264
|
+
const text = renderer.render(docFor(packed, omitted));
|
|
265
|
+
return { text, tokens: count(text) };
|
|
266
|
+
};
|
|
267
|
+
|
|
268
|
+
let packed = entries;
|
|
269
|
+
let omitted = [];
|
|
270
|
+
let rendered;
|
|
271
|
+
if (opts.budget) {
|
|
272
|
+
const { ranked } = rankEntries(selection.root, entries);
|
|
273
|
+
const separator = count('\n\n');
|
|
274
|
+
const wrapperCost = (entry) =>
|
|
275
|
+
opts.style === 'json'
|
|
276
|
+
? count(renderer.renderFile(entry, entries.length))
|
|
277
|
+
: entry.tokens + count(renderer.renderFile({ ...entry, content: '' }, entries.length));
|
|
278
|
+
const cost = (entry) =>
|
|
279
|
+
(entry.cost ??= wrapperCost(entry) + separator + (opts.tree ? count(entry.path) + 1 : 0));
|
|
280
|
+
({ packed, omitted, ...rendered } = fitToBudget(entries, ranked, opts.budget, {
|
|
281
|
+
measure,
|
|
282
|
+
cost,
|
|
283
|
+
}));
|
|
284
|
+
} else {
|
|
285
|
+
rendered = measure(entries, []);
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
let parts = null;
|
|
289
|
+
let splitFiles = [];
|
|
290
|
+
if (opts.split) {
|
|
291
|
+
const doc = docFor(packed, omitted);
|
|
292
|
+
({ parts, splitFiles } = splitIntoParts(packed, opts.split, { renderer, count, doc }));
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
const redactionCounts = {};
|
|
296
|
+
const findings = [...textFindings, ...packed.flatMap((entry) => entry.findings)];
|
|
297
|
+
for (const type of findings) redactionCounts[type] = (redactionCounts[type] ?? 0) + 1;
|
|
298
|
+
|
|
299
|
+
const compressed = packed.filter((entry) => entry.compression);
|
|
300
|
+
const sum = (key) => compressed.reduce((total, entry) => total + entry.compression[key], 0);
|
|
301
|
+
const compression = opts.compress
|
|
302
|
+
? {
|
|
303
|
+
files: compressed.length,
|
|
304
|
+
bodies: sum('bodies'),
|
|
305
|
+
originalTokens: sum('originalTokens'),
|
|
306
|
+
compressedTokens: sum('compressedTokens'),
|
|
307
|
+
savedTokens: sum('originalTokens') - sum('compressedTokens'),
|
|
308
|
+
}
|
|
309
|
+
: null;
|
|
310
|
+
|
|
311
|
+
return {
|
|
312
|
+
text: rendered.text,
|
|
313
|
+
style: opts.style,
|
|
314
|
+
encoding: TokenUtils.isExact() ? opts.encoding : 'estimate',
|
|
315
|
+
totalTokens: rendered.tokens,
|
|
316
|
+
files: packed.map((entry) => ({
|
|
317
|
+
path: entry.path,
|
|
318
|
+
tokens: entry.tokens,
|
|
319
|
+
lines: entry.totalLines,
|
|
320
|
+
compressed: Boolean(entry.compression),
|
|
321
|
+
})),
|
|
322
|
+
omitted: omitted.map(({ path, tokens }) => ({ path, tokens })),
|
|
323
|
+
skipped,
|
|
324
|
+
redactions: { total: findings.length, byType: redactionCounts },
|
|
325
|
+
compression,
|
|
326
|
+
budget: opts.budget ? { limit: opts.budget, used: rendered.tokens } : null,
|
|
327
|
+
model: modelFit(opts.profile, rendered.tokens, opts.encoding),
|
|
328
|
+
parts,
|
|
329
|
+
splitFiles,
|
|
330
|
+
};
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
/**
|
|
334
|
+
* One sentence about a pack result: size, files left out, compression, redactions, model fit
|
|
335
|
+
* @param {object} result - From packContext()
|
|
336
|
+
* @returns {string}
|
|
337
|
+
*/
|
|
338
|
+
export function describePack(result) {
|
|
339
|
+
const parts = [`${result.files.length} file(s), ${result.totalTokens} tokens (${result.style})`];
|
|
340
|
+
if (result.omitted.length) parts.push(`${result.omitted.length} file(s) left out by the budget`);
|
|
341
|
+
if (result.compression) {
|
|
342
|
+
parts.push(`${result.compression.savedTokens} tokens saved by compression`);
|
|
343
|
+
}
|
|
344
|
+
if (result.redactions.total) parts.push(`${result.redactions.total} secret(s) redacted`);
|
|
345
|
+
if (result.model) {
|
|
346
|
+
const fit = result.model.fits ? 'fits' : "doesn't fit";
|
|
347
|
+
parts.push(
|
|
348
|
+
`${result.model.percentOfWindow.toFixed(1)}% of ${result.model.id}'s window, ${fit}`
|
|
349
|
+
);
|
|
350
|
+
}
|
|
351
|
+
return `Packed ${parts.join('; ')}.`;
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
export default packContext;
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Helpers shared by the pack renderers
|
|
3
|
+
*
|
|
4
|
+
* A renderer turns a document model into text:
|
|
5
|
+
* header text for the top, or null
|
|
6
|
+
* partLabel "Part 2 of 3" when the output is split, or null
|
|
7
|
+
* part {index, count} when the output is split, or null
|
|
8
|
+
* tree directory tree text, or null
|
|
9
|
+
* files [{path, content, tokens, lines?: [first, last], totalLines?}]
|
|
10
|
+
* firstIndex index of the first file (documents are numbered across parts)
|
|
11
|
+
* omitted {files: [{path, tokens}], count, tokens, more, moreTokens}, or null
|
|
12
|
+
* instructions text for the end, or null
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
/** @param {number} value */
|
|
16
|
+
export const formatNumber = (value) => value.toLocaleString('en-US');
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Display name of a packed file; a piece of a file split by lines says which lines
|
|
20
|
+
* @param {{path: string, lines?: number[], totalLines?: number}} file
|
|
21
|
+
* @returns {string}
|
|
22
|
+
*/
|
|
23
|
+
export function sourceLabel(file) {
|
|
24
|
+
if (!file.lines) return file.path;
|
|
25
|
+
return `${file.path} (lines ${file.lines[0]}-${file.lines[1]} of ${file.totalLines})`;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Content without its final line break (the wrapper adds one)
|
|
30
|
+
* @param {string} content
|
|
31
|
+
* @returns {string}
|
|
32
|
+
*/
|
|
33
|
+
export function withoutFinalNewline(content) {
|
|
34
|
+
return content.endsWith('\n') ? content.slice(0, -1) : content;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Lines of the omitted-files section: the listed files, then a count of the rest
|
|
39
|
+
* @param {object} omitted - Document model `omitted`
|
|
40
|
+
* @param {string} bullet - Prefix of each line
|
|
41
|
+
* @returns {string[]}
|
|
42
|
+
*/
|
|
43
|
+
export function omittedLines(omitted, bullet = '') {
|
|
44
|
+
const lines = omitted.files.map(
|
|
45
|
+
(file) => `${bullet}${file.path} (${formatNumber(file.tokens)} tokens)`
|
|
46
|
+
);
|
|
47
|
+
if (omitted.more > 0) {
|
|
48
|
+
lines.push(
|
|
49
|
+
`${bullet}… and ${formatNumber(omitted.more)} more files (${formatNumber(omitted.moreTokens)} tokens)`
|
|
50
|
+
);
|
|
51
|
+
}
|
|
52
|
+
return lines;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* One-line description of the omitted files
|
|
57
|
+
* @param {object} omitted - Document model `omitted`
|
|
58
|
+
* @returns {string}
|
|
59
|
+
*/
|
|
60
|
+
export function omittedSummary(omitted) {
|
|
61
|
+
const files = omitted.count === 1 ? '1 file' : `${formatNumber(omitted.count)} files`;
|
|
62
|
+
return `${files} (${formatNumber(omitted.tokens)} tokens) did not fit in the token budget`;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Join the non-empty sections of a document with blank lines
|
|
67
|
+
* @param {Array<string|null|false>} sections
|
|
68
|
+
* @returns {string}
|
|
69
|
+
*/
|
|
70
|
+
export function joinSections(sections) {
|
|
71
|
+
return `${sections.filter((section) => section).join('\n\n')}\n`;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// Fenced-code language per extension (Markdown style)
|
|
75
|
+
const LANGUAGES = {
|
|
76
|
+
js: 'javascript',
|
|
77
|
+
mjs: 'javascript',
|
|
78
|
+
cjs: 'javascript',
|
|
79
|
+
jsx: 'jsx',
|
|
80
|
+
ts: 'typescript',
|
|
81
|
+
mts: 'typescript',
|
|
82
|
+
cts: 'typescript',
|
|
83
|
+
tsx: 'tsx',
|
|
84
|
+
py: 'python',
|
|
85
|
+
rb: 'ruby',
|
|
86
|
+
go: 'go',
|
|
87
|
+
rs: 'rust',
|
|
88
|
+
java: 'java',
|
|
89
|
+
kt: 'kotlin',
|
|
90
|
+
kts: 'kotlin',
|
|
91
|
+
swift: 'swift',
|
|
92
|
+
cs: 'csharp',
|
|
93
|
+
c: 'c',
|
|
94
|
+
h: 'c',
|
|
95
|
+
cc: 'cpp',
|
|
96
|
+
cpp: 'cpp',
|
|
97
|
+
cxx: 'cpp',
|
|
98
|
+
hpp: 'cpp',
|
|
99
|
+
scala: 'scala',
|
|
100
|
+
php: 'php',
|
|
101
|
+
json: 'json',
|
|
102
|
+
md: 'markdown',
|
|
103
|
+
yml: 'yaml',
|
|
104
|
+
yaml: 'yaml',
|
|
105
|
+
html: 'html',
|
|
106
|
+
vue: 'vue',
|
|
107
|
+
css: 'css',
|
|
108
|
+
scss: 'scss',
|
|
109
|
+
sass: 'sass',
|
|
110
|
+
less: 'less',
|
|
111
|
+
xml: 'xml',
|
|
112
|
+
svg: 'xml',
|
|
113
|
+
sh: 'bash',
|
|
114
|
+
bash: 'bash',
|
|
115
|
+
zsh: 'zsh',
|
|
116
|
+
sql: 'sql',
|
|
117
|
+
toml: 'toml',
|
|
118
|
+
ini: 'ini',
|
|
119
|
+
conf: 'ini',
|
|
120
|
+
txt: 'text',
|
|
121
|
+
};
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Fenced-code language of a file, from its extension ('' when unknown)
|
|
125
|
+
* @param {string} filePath
|
|
126
|
+
* @returns {string}
|
|
127
|
+
*/
|
|
128
|
+
export function languageOf(filePath) {
|
|
129
|
+
const name = filePath.split('/').pop().toLowerCase();
|
|
130
|
+
if (name === 'dockerfile') return 'dockerfile';
|
|
131
|
+
if (name === 'makefile') return 'makefile';
|
|
132
|
+
const dot = name.lastIndexOf('.');
|
|
133
|
+
return dot > 0 ? (LANGUAGES[name.slice(dot + 1)] ?? '') : '';
|
|
134
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pack renderers by style name
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import xml from './xml.js';
|
|
6
|
+
import markdown from './markdown.js';
|
|
7
|
+
import plain from './plain.js';
|
|
8
|
+
import json from './json.js';
|
|
9
|
+
|
|
10
|
+
export const RENDERERS = { xml, markdown, plain, json };
|
|
11
|
+
|
|
12
|
+
export const STYLES = Object.keys(RENDERERS);
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* @param {string} style - xml, markdown, plain or json
|
|
16
|
+
* @returns {{name: string, extension: string, render: Function, renderFile: Function}}
|
|
17
|
+
*/
|
|
18
|
+
export function getRenderer(style) {
|
|
19
|
+
const renderer = RENDERERS[style];
|
|
20
|
+
if (!renderer) throw new Error(`Unknown style "${style}" (use ${STYLES.join(', ')})`);
|
|
21
|
+
return renderer;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export default { RENDERERS, STYLES, getRenderer };
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* JSON style: {header?, part?, parts?, tree?, files: [{path, tokens, content}], omitted?,
|
|
3
|
+
* instructions?} with the instructions last
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
const fileEntry = (file) => ({
|
|
7
|
+
path: file.path,
|
|
8
|
+
...(file.lines && { lines: file.lines, totalLines: file.totalLines }),
|
|
9
|
+
tokens: file.tokens,
|
|
10
|
+
content: file.content,
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* One file entry as it appears in the `files` array (for size estimates)
|
|
15
|
+
* @param {object} file - Document model file
|
|
16
|
+
* @returns {string}
|
|
17
|
+
*/
|
|
18
|
+
export function renderFile(file) {
|
|
19
|
+
return JSON.stringify(fileEntry(file), null, 2).replace(/\n/g, '\n ');
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* @param {object} doc - Document model (see renderers/common.js)
|
|
24
|
+
* @returns {string}
|
|
25
|
+
*/
|
|
26
|
+
export function render(doc) {
|
|
27
|
+
const output = {};
|
|
28
|
+
if (doc.header) output.header = doc.header;
|
|
29
|
+
if (doc.part) {
|
|
30
|
+
output.part = doc.part.index;
|
|
31
|
+
output.parts = doc.part.count;
|
|
32
|
+
}
|
|
33
|
+
if (doc.tree !== null) output.tree = doc.tree;
|
|
34
|
+
output.files = doc.files.map(fileEntry);
|
|
35
|
+
if (doc.omitted) {
|
|
36
|
+
output.omitted = {
|
|
37
|
+
count: doc.omitted.count,
|
|
38
|
+
tokens: doc.omitted.tokens,
|
|
39
|
+
files: doc.omitted.files.map(({ path, tokens }) => ({ path, tokens })),
|
|
40
|
+
...(doc.omitted.more > 0 && { more: doc.omitted.more, moreTokens: doc.omitted.moreTokens }),
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
if (doc.instructions) output.instructions = doc.instructions;
|
|
44
|
+
return `${JSON.stringify(output, null, 2)}\n`;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export default { name: 'json', extension: '.json', render, renderFile };
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Markdown style: a heading per file and a fenced code block whose fence is longer than any
|
|
3
|
+
* backtick run in the file, so the content cannot close it
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import {
|
|
7
|
+
joinSections,
|
|
8
|
+
languageOf,
|
|
9
|
+
omittedLines,
|
|
10
|
+
omittedSummary,
|
|
11
|
+
sourceLabel,
|
|
12
|
+
withoutFinalNewline,
|
|
13
|
+
} from './common.js';
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Backtick fence for a block of text: at least three, one more than its longest run
|
|
17
|
+
* @param {string} text
|
|
18
|
+
* @returns {string}
|
|
19
|
+
*/
|
|
20
|
+
export function fenceFor(text) {
|
|
21
|
+
let longest = 0;
|
|
22
|
+
for (const run of text.match(/`+/g) || []) longest = Math.max(longest, run.length);
|
|
23
|
+
return '`'.repeat(Math.max(3, longest + 1));
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* One file: heading and fenced content
|
|
28
|
+
* @param {object} file - Document model file
|
|
29
|
+
* @returns {string}
|
|
30
|
+
*/
|
|
31
|
+
export function renderFile(file) {
|
|
32
|
+
const content = withoutFinalNewline(file.content);
|
|
33
|
+
const fence = fenceFor(content);
|
|
34
|
+
return `### ${sourceLabel(file)}\n\n${fence}${languageOf(file.path)}\n${content}\n${fence}`;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* @param {object} doc - Document model (see renderers/common.js)
|
|
39
|
+
* @returns {string}
|
|
40
|
+
*/
|
|
41
|
+
export function render(doc) {
|
|
42
|
+
const treeFence = doc.tree !== null && fenceFor(doc.tree);
|
|
43
|
+
return joinSections([
|
|
44
|
+
doc.header,
|
|
45
|
+
doc.partLabel,
|
|
46
|
+
doc.tree !== null && `## Directory structure\n\n${treeFence}\n${doc.tree}\n${treeFence}`,
|
|
47
|
+
doc.files.length > 0 && `## Files\n\n${doc.files.map((file) => renderFile(file)).join('\n\n')}`,
|
|
48
|
+
doc.omitted &&
|
|
49
|
+
`## Omitted files\n\n${omittedSummary(doc.omitted)}:\n\n${omittedLines(doc.omitted, '- ').join('\n')}`,
|
|
50
|
+
doc.instructions && `## Instructions\n\n${doc.instructions}`,
|
|
51
|
+
]);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export default { name: 'markdown', extension: '.md', render, renderFile };
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Plain style: `=== path ===` separators and raw content
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import {
|
|
6
|
+
joinSections,
|
|
7
|
+
omittedLines,
|
|
8
|
+
omittedSummary,
|
|
9
|
+
sourceLabel,
|
|
10
|
+
withoutFinalNewline,
|
|
11
|
+
} from './common.js';
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* One file: separator line and content
|
|
15
|
+
* @param {object} file - Document model file
|
|
16
|
+
* @returns {string}
|
|
17
|
+
*/
|
|
18
|
+
export function renderFile(file) {
|
|
19
|
+
return `=== ${sourceLabel(file)} ===\n${withoutFinalNewline(file.content)}`;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* @param {object} doc - Document model (see renderers/common.js)
|
|
24
|
+
* @returns {string}
|
|
25
|
+
*/
|
|
26
|
+
export function render(doc) {
|
|
27
|
+
return joinSections([
|
|
28
|
+
doc.header,
|
|
29
|
+
doc.partLabel,
|
|
30
|
+
doc.tree !== null && `Directory structure:\n${doc.tree}`,
|
|
31
|
+
doc.files.length > 0 && doc.files.map((file) => renderFile(file)).join('\n\n'),
|
|
32
|
+
doc.omitted &&
|
|
33
|
+
`Omitted files: ${omittedSummary(doc.omitted)}.\n${omittedLines(doc.omitted, ' ').join('\n')}`,
|
|
34
|
+
doc.instructions && `Instructions:\n${doc.instructions}`,
|
|
35
|
+
]);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export default { name: 'plain', extension: '.txt', render, renderFile };
|