ctxman 3.0.0 → 3.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (223) hide show
  1. package/.ctxman/llm-profiles.json +1067 -39
  2. package/.ctxman/profiles/backend-team.json +13 -0
  3. package/.ctxman/profiles/default.json +12 -0
  4. package/.ctxman/profiles/devops-team.json +21 -0
  5. package/.ctxman/profiles/frontend-team.json +20 -0
  6. package/.ctxman/templates/bug-fix.json +17 -0
  7. package/.ctxman/templates/code-review.json +9 -0
  8. package/.ctxman/templates/documentation.json +9 -0
  9. package/.ctxman/templates/feature.json +9 -0
  10. package/.ctxman/templates/full-context.json +9 -0
  11. package/.ctxman/templates/refactoring.json +18 -0
  12. package/.ctxman/templates/testing.json +9 -0
  13. package/.ctxman/wizard-profiles/security-audit/profile.json +7 -1
  14. package/LICENSE +21 -0
  15. package/README.md +380 -136
  16. package/bin/cli.js +1880 -529
  17. package/bin/cm-ask.js +18 -13
  18. package/bin/cm-audit.js +124 -0
  19. package/bin/cm-count.js +151 -0
  20. package/bin/cm-gitingest.js +128 -115
  21. package/bin/cm-map.js +242 -0
  22. package/bin/cm-mcp.js +293 -0
  23. package/bin/cm-pack.js +405 -0
  24. package/bin/cm-policy.js +170 -0
  25. package/bin/cm-pr.js +248 -0
  26. package/bin/cm-search.js +178 -0
  27. package/bin/cm-update.js +128 -128
  28. package/bin/cm-why.js +197 -0
  29. package/bin/mcp-server.js +8 -24
  30. package/ctxman.js +237 -183
  31. package/examples/README.md +19 -4
  32. package/examples/github-actions/ctxman-check.yml +49 -0
  33. package/examples/wizard-profiles/security-audit/profile.json +7 -1
  34. package/index.d.ts +1422 -0
  35. package/index.js +55 -31
  36. package/lib/agents/pr-failure-agent.js +48 -45
  37. package/lib/analyzers/ai-suggester.js +451 -0
  38. package/lib/analyzers/dependency-scanner.js +505 -0
  39. package/lib/analyzers/duplicate-detector.js +264 -0
  40. package/lib/analyzers/go-method-analyzer.js +151 -110
  41. package/lib/analyzers/import-tracker.js +431 -0
  42. package/lib/analyzers/method-analyzer.js +1055 -434
  43. package/lib/analyzers/performance-dashboard.js +683 -0
  44. package/lib/analyzers/token-calculator.js +895 -504
  45. package/lib/analyzers/unused-exports-detector.js +336 -0
  46. package/lib/api/mcp/install.js +424 -0
  47. package/lib/api/mcp/prompts.js +281 -281
  48. package/lib/api/mcp/resources.js +300 -241
  49. package/lib/api/mcp/sandbox.js +89 -0
  50. package/lib/api/mcp/server.js +675 -312
  51. package/lib/api/mcp/start.js +125 -0
  52. package/lib/api/mcp/transports/HttpSseTransport.js +60 -52
  53. package/lib/api/rest/server.js +105 -28
  54. package/lib/cache/CacheManager.js +9 -17
  55. package/lib/core/Analyzer.js +16 -10
  56. package/lib/core/ContextBuilder.js +19 -28
  57. package/lib/core/Reporter.js +15 -14
  58. package/lib/core/Scanner.js +81 -12
  59. package/lib/count/index.js +100 -0
  60. package/lib/formatters/format-registry.js +394 -372
  61. package/lib/formatters/gitingest-formatter.js +656 -621
  62. package/lib/formatters/toon-formatter-v1.3.js +637 -632
  63. package/lib/formatters/toon-formatter.js +239 -240
  64. package/lib/integrations/git/BlameTracker.js +4 -4
  65. package/lib/integrations/git/DiffAnalyzer.js +38 -31
  66. package/lib/integrations/git/GitClient.js +71 -53
  67. package/lib/lsp/server.js +311 -0
  68. package/lib/map/graph.js +299 -0
  69. package/lib/map/imports.js +233 -0
  70. package/lib/map/index.js +33 -0
  71. package/lib/map/rank.js +253 -0
  72. package/lib/map/render.js +346 -0
  73. package/lib/map/resolver.js +574 -0
  74. package/lib/pack/budget.js +111 -0
  75. package/lib/pack/code-lexer.js +360 -0
  76. package/lib/pack/compress.js +414 -0
  77. package/lib/pack/index.js +354 -0
  78. package/lib/pack/renderers/common.js +134 -0
  79. package/lib/pack/renderers/index.js +24 -0
  80. package/lib/pack/renderers/json.js +47 -0
  81. package/lib/pack/renderers/markdown.js +54 -0
  82. package/lib/pack/renderers/plain.js +38 -0
  83. package/lib/pack/renderers/xml.js +85 -0
  84. package/lib/pack/select.js +177 -0
  85. package/lib/pack/split.js +182 -0
  86. package/lib/pack/tree.js +35 -0
  87. package/lib/parsers/gitignore-parser.js +113 -107
  88. package/lib/parsers/method-filter-parser.js +50 -33
  89. package/lib/plugins/ExporterPlugin.js +3 -3
  90. package/lib/plugins/LanguagePlugin.js +3 -3
  91. package/lib/plugins/PluginManager.js +7 -13
  92. package/lib/policy/audit.js +294 -0
  93. package/lib/policy/enforce.js +125 -0
  94. package/lib/policy/ignore-files.js +169 -0
  95. package/lib/policy/index.js +52 -0
  96. package/lib/policy/policy.js +489 -0
  97. package/lib/pr/diff-parser.js +267 -0
  98. package/lib/pr/method-mapper.js +148 -0
  99. package/lib/pr/pr-context.js +829 -0
  100. package/lib/pr/pr-git.js +149 -0
  101. package/lib/pr/pr-renderer.js +644 -0
  102. package/lib/pr/related-tests.js +296 -0
  103. package/lib/rag/DataSourcePlugin.js +5 -5
  104. package/lib/rag/EmbeddingProvider.js +19 -8
  105. package/lib/rag/Indexer.js +14 -5
  106. package/lib/rag/VectorStoreFactory.js +3 -3
  107. package/lib/rag/sources/FileDataSource.js +6 -6
  108. package/lib/rag/stores/BaseVectorStore.js +5 -5
  109. package/lib/rag/stores/ChromaDBStore.js +10 -10
  110. package/lib/rag/stores/LanceDBStore.js +29 -19
  111. package/lib/rag/stores/PineconeStore.js +11 -9
  112. package/lib/search/bm25.js +433 -0
  113. package/lib/search/code-units.js +201 -0
  114. package/lib/search/symbols.js +189 -0
  115. package/lib/ui/dashboard.js +120 -89
  116. package/lib/ui/index.js +1 -6
  117. package/lib/ui/init-wizard-ui.js +550 -0
  118. package/lib/ui/progress-bar.js +94 -88
  119. package/lib/ui/select-input.js +40 -32
  120. package/lib/ui/template-selector.js +99 -0
  121. package/lib/ui/wizard.js +374 -329
  122. package/lib/utils/claude-token-counter.js +151 -0
  123. package/lib/utils/cli-output.js +17 -0
  124. package/lib/utils/clipboard-utils.js +49 -50
  125. package/lib/utils/config-utils.js +81 -66
  126. package/lib/utils/context-versioning.js +532 -0
  127. package/lib/utils/error-handler.js +112 -112
  128. package/lib/utils/file-utils.js +153 -69
  129. package/lib/utils/format-converter.js +356 -283
  130. package/lib/utils/git-utils.js +366 -294
  131. package/lib/utils/llm-cost-estimator.js +281 -0
  132. package/lib/utils/llm-detector.js +242 -78
  133. package/lib/utils/logger.js +243 -237
  134. package/lib/utils/messages.js +355 -0
  135. package/lib/utils/multi-repo-manager.js +364 -0
  136. package/lib/utils/profile-manager.js +380 -0
  137. package/lib/utils/safe-read.js +61 -0
  138. package/lib/utils/secret-redactor.js +61 -0
  139. package/lib/utils/snapshot-manager.js +679 -0
  140. package/lib/utils/template-manager.js +378 -0
  141. package/lib/utils/token-utils.js +201 -170
  142. package/lib/utils/tokenizer-adapter.js +421 -377
  143. package/lib/utils/updater.js +539 -506
  144. package/lib/watch/FileWatcher.js +12 -8
  145. package/lib/watch/IncrementalAnalyzer.js +7 -7
  146. package/lib/why/coupling.js +81 -0
  147. package/lib/why/errors.js +23 -0
  148. package/lib/why/git.js +169 -0
  149. package/lib/why/history.js +108 -0
  150. package/lib/why/index.js +720 -0
  151. package/lib/why/ownership.js +79 -0
  152. package/lib/why/paths.js +207 -0
  153. package/lib/why/references.js +259 -0
  154. package/lib/why/render.js +722 -0
  155. package/lib/why/search.js +358 -0
  156. package/lib/why/target.js +389 -0
  157. package/lib/wizards/config-generator.js +255 -0
  158. package/lib/wizards/project-detector.js +571 -0
  159. package/package.json +44 -10
  160. package/.ctxman/logs/ctxman-2026-07-02.log +0 -156
  161. package/.ctxman/self-knowledge/documents.lance/_transactions/0-b3dd1558-f95b-44b9-a154-18e8c330ae7a.txn +0 -0
  162. package/.ctxman/self-knowledge/documents.lance/_transactions/1-44bff6d4-e552-4010-a769-e09ad822698d.txn +0 -0
  163. package/.ctxman/self-knowledge/documents.lance/_transactions/10-37260794-5947-4ecb-b4c8-d732f58f40f1.txn +0 -0
  164. package/.ctxman/self-knowledge/documents.lance/_transactions/11-0d52a883-5518-463c-9b6d-986c3af89dfd.txn +0 -0
  165. package/.ctxman/self-knowledge/documents.lance/_transactions/12-ae354646-52f2-49c9-90fb-2e6ed8932663.txn +0 -0
  166. package/.ctxman/self-knowledge/documents.lance/_transactions/13-5d7d5e57-27da-498b-b734-091b9d38da13.txn +0 -0
  167. package/.ctxman/self-knowledge/documents.lance/_transactions/14-558ce928-810b-4252-a364-fdd1ee6f8a25.txn +0 -0
  168. package/.ctxman/self-knowledge/documents.lance/_transactions/15-27e3a58f-8b64-43b8-a9f4-e3369446968b.txn +0 -0
  169. package/.ctxman/self-knowledge/documents.lance/_transactions/16-a110c390-e5c8-4464-b976-bc7f5195fa15.txn +0 -0
  170. package/.ctxman/self-knowledge/documents.lance/_transactions/17-97513225-53de-4216-b61a-6337bd0cc4ad.txn +0 -0
  171. package/.ctxman/self-knowledge/documents.lance/_transactions/18-5ba450f0-c527-459c-bf4f-6d79b5e4285f.txn +0 -0
  172. package/.ctxman/self-knowledge/documents.lance/_transactions/19-600d3a63-5225-49e9-b8b7-30e037b564ab.txn +0 -0
  173. package/.ctxman/self-knowledge/documents.lance/_transactions/2-48a61b4e-92cf-4342-8376-914c1b99a9f9.txn +0 -0
  174. package/.ctxman/self-knowledge/documents.lance/_transactions/20-121dd912-5410-410e-84ff-82c7a23604a0.txn +0 -0
  175. package/.ctxman/self-knowledge/documents.lance/_transactions/3-bc6844ab-7ea3-4bb3-9ee1-538ce5664a9c.txn +0 -0
  176. package/.ctxman/self-knowledge/documents.lance/_transactions/4-d5e1754f-dbb5-454e-a9be-2a7da8d7bcf9.txn +0 -0
  177. package/.ctxman/self-knowledge/documents.lance/_transactions/5-4fae93df-0568-4b6f-8acf-8bef4bba6047.txn +0 -0
  178. package/.ctxman/self-knowledge/documents.lance/_transactions/6-4b1b8b87-068e-49a3-ba2b-d8dae628b252.txn +0 -0
  179. package/.ctxman/self-knowledge/documents.lance/_transactions/7-63d040fb-d2fa-4c0f-bed0-53411918a7b0.txn +0 -0
  180. package/.ctxman/self-knowledge/documents.lance/_transactions/8-c5628cf6-264c-420f-aab8-bc96a9083591.txn +0 -0
  181. package/.ctxman/self-knowledge/documents.lance/_transactions/9-c94570aa-5e53-4fcb-971f-cecc1d1d5492.txn +0 -0
  182. package/.ctxman/self-knowledge/documents.lance/_versions/1.manifest +0 -0
  183. package/.ctxman/self-knowledge/documents.lance/_versions/10.manifest +0 -0
  184. package/.ctxman/self-knowledge/documents.lance/_versions/11.manifest +0 -0
  185. package/.ctxman/self-knowledge/documents.lance/_versions/12.manifest +0 -0
  186. package/.ctxman/self-knowledge/documents.lance/_versions/13.manifest +0 -0
  187. package/.ctxman/self-knowledge/documents.lance/_versions/14.manifest +0 -0
  188. package/.ctxman/self-knowledge/documents.lance/_versions/15.manifest +0 -0
  189. package/.ctxman/self-knowledge/documents.lance/_versions/16.manifest +0 -0
  190. package/.ctxman/self-knowledge/documents.lance/_versions/17.manifest +0 -0
  191. package/.ctxman/self-knowledge/documents.lance/_versions/18.manifest +0 -0
  192. package/.ctxman/self-knowledge/documents.lance/_versions/19.manifest +0 -0
  193. package/.ctxman/self-knowledge/documents.lance/_versions/2.manifest +0 -0
  194. package/.ctxman/self-knowledge/documents.lance/_versions/20.manifest +0 -0
  195. package/.ctxman/self-knowledge/documents.lance/_versions/21.manifest +0 -0
  196. package/.ctxman/self-knowledge/documents.lance/_versions/3.manifest +0 -0
  197. package/.ctxman/self-knowledge/documents.lance/_versions/4.manifest +0 -0
  198. package/.ctxman/self-knowledge/documents.lance/_versions/5.manifest +0 -0
  199. package/.ctxman/self-knowledge/documents.lance/_versions/6.manifest +0 -0
  200. package/.ctxman/self-knowledge/documents.lance/_versions/7.manifest +0 -0
  201. package/.ctxman/self-knowledge/documents.lance/_versions/8.manifest +0 -0
  202. package/.ctxman/self-knowledge/documents.lance/_versions/9.manifest +0 -0
  203. package/.ctxman/self-knowledge/documents.lance/data/000010010011011101000010de8621405d9d5cb06540dbf02c.lance +0 -0
  204. package/.ctxman/self-knowledge/documents.lance/data/0000101001011100011000003fb62a4aa8978974075c144f1c.lance +0 -0
  205. package/.ctxman/self-knowledge/documents.lance/data/000011010000100111001011c6cc824d628f3bb1f4e04cd5d0.lance +0 -0
  206. package/.ctxman/self-knowledge/documents.lance/data/001000001000011001011100d8f74349ada090b332cd1df669.lance +0 -0
  207. package/.ctxman/self-knowledge/documents.lance/data/01000001111010001000010045ab44467bb1e7e4d8ae06bd58.lance +0 -0
  208. package/.ctxman/self-knowledge/documents.lance/data/01000101101110111110011131ef904241a89e1ffb6ddaf8e3.lance +0 -0
  209. package/.ctxman/self-knowledge/documents.lance/data/010010110000010000011010fbfcc145c6a8a827b28057a07e.lance +0 -0
  210. package/.ctxman/self-knowledge/documents.lance/data/1000011000101010001110003fb4f045c3835982efa1405324.lance +0 -0
  211. package/.ctxman/self-knowledge/documents.lance/data/100111011101110111101011f4396b410fa06af3b028d204e6.lance +0 -0
  212. package/.ctxman/self-knowledge/documents.lance/data/100111100111101000001100596b174e00995d6cc2969cc8dc.lance +0 -0
  213. package/.ctxman/self-knowledge/documents.lance/data/1010001100011000110010102ea16b484d887c7c9725feffd6.lance +0 -0
  214. package/.ctxman/self-knowledge/documents.lance/data/101101100010101111110111c5230944d9b3af7301b6392dda.lance +0 -0
  215. package/.ctxman/self-knowledge/documents.lance/data/1011011100100111111110105892bf41b5a530fedd7cf516f1.lance +0 -0
  216. package/.ctxman/self-knowledge/documents.lance/data/10111110000010001011110164e8774742bade87b483c10f36.lance +0 -0
  217. package/.ctxman/self-knowledge/documents.lance/data/1100000100010110001110103ffaca40a2a42c85067ebaf9ae.lance +0 -0
  218. package/.ctxman/self-knowledge/documents.lance/data/110000010001110110101110664bde4c76a78dfc14b05e83b8.lance +0 -0
  219. package/.ctxman/self-knowledge/documents.lance/data/11001100111000010100110050645c4281b1f35b2022234f7f.lance +0 -0
  220. package/.ctxman/self-knowledge/documents.lance/data/1101111011001100110111017214924a368f5719eab2246619.lance +0 -0
  221. package/.ctxman/self-knowledge/documents.lance/data/1110000111001010001000002150c24dcab653d39d07032e63.lance +0 -0
  222. package/.ctxman/self-knowledge/documents.lance/data/1110100000000100000011014d20534967a18b5c87721659ff.lance +0 -0
  223. package/.ctxman/self-knowledge/documents.lance/data/11110101111010000101011135a0114ef2b35dc6f1297a3381.lance +0 -0
@@ -0,0 +1,85 @@
1
+ /**
2
+ * XML style: Anthropic's long-context document format
3
+ *
4
+ * <documents>
5
+ * <document index="1">
6
+ * <source>path</source>
7
+ * <document_content>
8
+ * ...raw file content...
9
+ * </document_content>
10
+ * </document>
11
+ * </documents>
12
+ *
13
+ * File content is not XML-escaped (Claude reads raw code, and escaping every `<` and `&`
14
+ * costs tokens). Only a closing tag of the wrapper itself (`</document_content>`,
15
+ * `</document>`, `</documents>`) inside a file is neutralised by writing its `<` as `&lt;`,
16
+ * so a file cannot end its own document early. Paths and the tree are escaped.
17
+ */
18
+
19
+ import {
20
+ joinSections,
21
+ omittedLines,
22
+ omittedSummary,
23
+ sourceLabel,
24
+ withoutFinalNewline,
25
+ } from './common.js';
26
+
27
+ const escapeXml = (text) => text.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;');
28
+
29
+ // `</document_content>`, `</document>` and `</documents>`, in any case, spaces before `>`
30
+ const WRAPPER_CLOSING_TAG = /<(?=\/(?:document_content|documents?)\s*>)/gi;
31
+
32
+ /**
33
+ * Neutralise wrapper closing tags inside file content
34
+ * @param {string} content
35
+ * @returns {string}
36
+ */
37
+ export function protectContent(content) {
38
+ return content.replace(WRAPPER_CLOSING_TAG, '&lt;');
39
+ }
40
+
41
+ /**
42
+ * One `<document>` element
43
+ * @param {object} file - Document model file
44
+ * @param {number} index - Document number
45
+ * @returns {string}
46
+ */
47
+ export function renderFile(file, index) {
48
+ return [
49
+ `<document index="${index}">`,
50
+ `<source>${escapeXml(sourceLabel(file))}</source>`,
51
+ '<document_content>',
52
+ protectContent(withoutFinalNewline(file.content)),
53
+ '</document_content>',
54
+ '</document>',
55
+ ].join('\n');
56
+ }
57
+
58
+ /**
59
+ * @param {object} doc - Document model (see common.js)
60
+ * @returns {string}
61
+ */
62
+ export function render(doc) {
63
+ const documents = [
64
+ '<documents>',
65
+ ...doc.files.map((file, i) => renderFile(file, doc.firstIndex + i)),
66
+ '</documents>',
67
+ ].join('\n');
68
+
69
+ return joinSections([
70
+ doc.header,
71
+ doc.partLabel,
72
+ doc.tree !== null && `<directory_structure>\n${escapeXml(doc.tree)}\n</directory_structure>`,
73
+ documents,
74
+ doc.omitted &&
75
+ [
76
+ `<omitted_files count="${doc.omitted.count}" tokens="${doc.omitted.tokens}">`,
77
+ `${omittedSummary(doc.omitted)}:`,
78
+ ...omittedLines(doc.omitted).map(escapeXml),
79
+ '</omitted_files>',
80
+ ].join('\n'),
81
+ doc.instructions && `<instructions>\n${doc.instructions}\n</instructions>`,
82
+ ]);
83
+ }
84
+
85
+ export default { name: 'xml', extension: '.xml', render, renderFile };
@@ -0,0 +1,177 @@
1
+ /**
2
+ * File selection for `pack`: the project scan (with .gitignore, .contextignore and
3
+ * .contextinclude) or an explicit list, narrowed by --include/--exclude globs
4
+ */
5
+
6
+ import fs from 'fs';
7
+ import path from 'path';
8
+ import { Scanner } from '../core/Scanner.js';
9
+ import GitIgnoreParser from '../parsers/gitignore-parser.js';
10
+ import FileUtils from '../utils/file-utils.js';
11
+
12
+ /** Error with a machine-readable code (PATH_OUTSIDE_ROOT, ROOT_NOT_FOUND, ...) */
13
+ export class PackError extends Error {
14
+ constructor(code, message) {
15
+ super(message);
16
+ this.name = 'PackError';
17
+ this.code = code;
18
+ }
19
+ }
20
+
21
+ const toPosix = (relativePath) => relativePath.split(path.sep).join('/');
22
+
23
+ /**
24
+ * Path of `target` relative to `root` when it lies strictly below it, else null
25
+ * (`..config` is a file name, `../config` is outside)
26
+ * @param {string} root - Absolute directory
27
+ * @param {string} target - Absolute path
28
+ * @returns {string|null}
29
+ */
30
+ function relativeInside(root, target) {
31
+ const relative = path.relative(root, target);
32
+ if (relative === '' || path.isAbsolute(relative)) return null;
33
+ if (relative === '..' || relative.startsWith(`..${path.sep}`)) return null;
34
+ return relative;
35
+ }
36
+
37
+ /**
38
+ * Compile gitignore-style globs with the same rules as .contextinclude/.contextignore
39
+ * @param {string[]} globs
40
+ * @param {boolean} includeMode - true: a `!glob` excludes (as in .contextinclude)
41
+ * @returns {{test: function(string): boolean}|null} null when there are no globs
42
+ */
43
+ function globMatcher(globs, includeMode) {
44
+ const patterns = globs.map((glob) => glob.trim()).filter(Boolean);
45
+ if (!patterns.length) return null;
46
+ // Only the pattern helpers are used: missing ignore files load nothing
47
+ const parser = new GitIgnoreParser('', '', '');
48
+ const compiled = patterns.map((pattern) => parser.convertToRegex(pattern));
49
+ return {
50
+ test: (relativePath) =>
51
+ includeMode
52
+ ? parser.testPatternsWithNegation(compiled, relativePath)
53
+ : parser.testPatterns(compiled, relativePath, 'exclude'),
54
+ };
55
+ }
56
+
57
+ /**
58
+ * Pick the files to pack
59
+ * @param {string} root - Project root
60
+ * @param {object} options
61
+ * @param {string[]} [options.files] - Explicit list (replaces the scan); relative entries
62
+ * are resolved against `options.cwd`. Every entry must stay inside the root, and the
63
+ * project's ignore files still apply to it.
64
+ * @param {string} [options.cwd] - Base for relative `files` entries (default: root)
65
+ * @param {string[]} [options.include] - Keep only matching files
66
+ * @param {string[]} [options.exclude] - Leave matching files out
67
+ * @param {string[]} [options.excludeFiles] - Absolute paths to leave out (the output file)
68
+ * @param {number} [options.maxFileSize] - Larger files are skipped
69
+ * @returns {{root: string, files: Array<{absolutePath: string, path: string, size: number,
70
+ * modified: Date}>, skipped: Array<{path: string, reason: string}>}}
71
+ */
72
+ export function selectFiles(root, options = {}) {
73
+ const rootPath = path.resolve(root);
74
+ if (!fs.statSync(rootPath, { throwIfNoEntry: false })?.isDirectory()) {
75
+ throw new PackError('ROOT_NOT_FOUND', `Not a directory: ${root}`);
76
+ }
77
+ const skipped = [];
78
+ const candidates = options.files
79
+ ? listedFiles(rootPath, options.files, options.cwd ?? rootPath, skipped)
80
+ : scannedFiles(rootPath);
81
+
82
+ const include = globMatcher(options.include ?? [], true);
83
+ const exclude = globMatcher(options.exclude ?? [], false);
84
+ const excluded = new Set((options.excludeFiles ?? []).map((file) => path.resolve(file)));
85
+ const maxFileSize = options.maxFileSize ?? Infinity;
86
+
87
+ const files = [];
88
+ for (const file of candidates) {
89
+ if (include && !include.test(file.path)) continue;
90
+ if (exclude?.test(file.path)) continue;
91
+ if (excluded.has(file.absolutePath)) continue;
92
+ if (file.size > maxFileSize) {
93
+ skipped.push({ path: file.path, reason: 'too-large' });
94
+ continue;
95
+ }
96
+ files.push(file);
97
+ }
98
+ return { root: rootPath, files, skipped };
99
+ }
100
+
101
+ /** Files found by the Scanner, in path order */
102
+ function scannedFiles(rootPath) {
103
+ return new Scanner(rootPath)
104
+ .scan()
105
+ .map((file) => ({
106
+ absolutePath: file.path,
107
+ path: toPosix(file.relativePath),
108
+ size: file.size,
109
+ modified: file.modified,
110
+ }))
111
+ .sort((a, b) => (a.path < b.path ? -1 : a.path > b.path ? 1 : 0));
112
+ }
113
+
114
+ /**
115
+ * Whether the scan would leave a listed file out: a skipped directory (node_modules, .git,
116
+ * ...) on its path, or a .gitignore/.contextignore/.contextinclude rule on the file or one
117
+ * of its directories, checked the way the Scanner walks down to it
118
+ */
119
+ function isIgnored(scanner, rootPath, relativePath) {
120
+ const segments = relativePath.split(path.sep);
121
+ for (let depth = 1; depth < segments.length; depth++) {
122
+ const directory = segments.slice(0, depth).join(path.sep);
123
+ if (FileUtils.isSkippedDirectory(segments[depth - 1])) return true;
124
+ if (scanner.shouldIgnore(directory, path.join(rootPath, directory))) return true;
125
+ }
126
+ return scanner.shouldIgnore(relativePath, path.join(rootPath, relativePath));
127
+ }
128
+
129
+ /**
130
+ * An explicit list, in the given order. A path that leaves the root (`..`, an absolute path
131
+ * elsewhere, a symlink pointing out) is an error; files the ignore rules exclude are skipped.
132
+ */
133
+ function listedFiles(rootPath, entries, cwd, skipped) {
134
+ const realRoot = fs.realpathSync(rootPath);
135
+ const scanner = new Scanner(rootPath);
136
+ const files = [];
137
+ const seen = new Set();
138
+ for (const entry of entries) {
139
+ if (typeof entry !== 'string' || entry.trim() === '') continue;
140
+ const absolute = path.resolve(cwd, entry);
141
+ // The root as given, or its real path (`find "$(pwd -P)"` under a symlinked root)
142
+ let relative = relativeInside(rootPath, absolute);
143
+ if (relative === null && realRoot !== rootPath) relative = relativeInside(realRoot, absolute);
144
+ if (relative === null) {
145
+ throw new PackError('PATH_OUTSIDE_ROOT', `Path is outside the project root: ${entry}`);
146
+ }
147
+ const absolutePath = path.join(rootPath, relative);
148
+ const relativePath = toPosix(relative);
149
+ if (seen.has(relativePath)) continue;
150
+ seen.add(relativePath);
151
+
152
+ let stats;
153
+ try {
154
+ // Symlinks are followed, but must not lead out of the root
155
+ if (relativeInside(realRoot, fs.realpathSync(absolutePath)) === null) {
156
+ throw new PackError('PATH_OUTSIDE_ROOT', `Path is outside the project root: ${entry}`);
157
+ }
158
+ stats = fs.statSync(absolutePath);
159
+ } catch (error) {
160
+ if (error instanceof PackError) throw error;
161
+ skipped.push({ path: relativePath, reason: 'missing' });
162
+ continue;
163
+ }
164
+ if (!stats.isFile()) {
165
+ skipped.push({ path: relativePath, reason: 'not-a-file' });
166
+ continue;
167
+ }
168
+ if (isIgnored(scanner, rootPath, relative)) {
169
+ skipped.push({ path: relativePath, reason: 'ignored' });
170
+ continue;
171
+ }
172
+ files.push({ absolutePath, path: relativePath, size: stats.size, modified: stats.mtime });
173
+ }
174
+ return files;
175
+ }
176
+
177
+ export default { selectFiles, PackError };
@@ -0,0 +1,182 @@
1
+ /**
2
+ * `pack --split`: cut the packed files into numbered parts of at most `limit` tokens
3
+ *
4
+ * Every part is a complete document in the chosen style. Part 1 carries the header and the
5
+ * directory tree, the last part the omitted-files list and the instructions, and document
6
+ * numbers continue across parts. Files are never cut, except a file that does not fit in a
7
+ * part on its own: it is split by lines into pieces labelled "(lines a-b of n)".
8
+ */
9
+
10
+ import { PackError } from './select.js';
11
+
12
+ // Label of a middle part when estimating: as long as any real label can be
13
+ const PLACEHOLDER_PART = { index: 9999, count: 9999 };
14
+
15
+ /**
16
+ * @param {Array} entries - Packed entries in output order
17
+ * @param {number} limit - Token limit per part
18
+ * @param {object} context
19
+ * @param {object} context.renderer - Style renderer
20
+ * @param {function(string): number} context.count - Token counter
21
+ * @param {object} context.doc - Whole-document fields: header, tree, omitted, instructions
22
+ * @returns {{parts: Array<{index: number, text: string, tokens: number, files: string[]}>,
23
+ * splitFiles: Array<{path: string, pieces: number}>}}
24
+ */
25
+ export function splitIntoParts(entries, limit, { renderer, count, doc }) {
26
+ const hasFirstExtras = Boolean(doc.header) || doc.tree !== null;
27
+ const hasLastExtras = Boolean(doc.omitted) || Boolean(doc.instructions);
28
+
29
+ const partDoc = (units, index, total, firstIndex) => ({
30
+ header: index === 0 ? doc.header : null,
31
+ partLabel: `Part ${index + 1} of ${total}`,
32
+ part: { index: index + 1, count: total },
33
+ tree: index === 0 ? doc.tree : null,
34
+ files: units,
35
+ firstIndex,
36
+ omitted: index === total - 1 ? doc.omitted : null,
37
+ instructions: index === total - 1 ? doc.instructions : null,
38
+ });
39
+ const middleDoc = (units) => ({
40
+ ...partDoc(units, 1, 3, 99999),
41
+ part: PLACEHOLDER_PART,
42
+ partLabel: 'Part 9999 of 9999',
43
+ });
44
+ const middleBase = count(renderer.render(middleDoc([])));
45
+ const unitCost = (unit) => count(renderer.renderFile(unit, 1)) + 1;
46
+
47
+ // Files too large for a part of their own are cut into pieces by lines
48
+ const units = [];
49
+ const splitFiles = [];
50
+ for (const entry of entries) {
51
+ // Estimates near the limit are checked exactly
52
+ const estimate = middleBase + unitCost(entry);
53
+ if (estimate <= limit * 0.9 || count(renderer.render(middleDoc([entry]))) <= limit) {
54
+ units.push(entry);
55
+ continue;
56
+ }
57
+ const pieces = splitByLines(entry, limit, { renderer, count, middleDoc });
58
+ splitFiles.push({ path: entry.path, pieces: pieces.length });
59
+ units.push(...pieces);
60
+ }
61
+
62
+ // Fill parts greedily with the estimates
63
+ let parts = [[]];
64
+ let used = count(renderer.render({ ...middleDoc([]), header: doc.header, tree: doc.tree }));
65
+ for (const unit of units) {
66
+ const cost = unitCost(unit);
67
+ if (parts[parts.length - 1].length && used + cost > limit) {
68
+ parts.push([]);
69
+ used = middleBase;
70
+ }
71
+ parts[parts.length - 1].push(unit);
72
+ used += cost;
73
+ }
74
+
75
+ // Then count each part exactly and move files forward until every part fits. A move
76
+ // changes two parts (all of them when the number of parts changes): the others are
77
+ // counted once
78
+ const counted = new Map();
79
+ const countOnce = (text) => {
80
+ if (!counted.has(text)) counted.set(text, count(text));
81
+ return counted.get(text);
82
+ };
83
+ const maxIterations = (units.length + 5) * 4;
84
+ for (let iteration = 0; ; iteration++) {
85
+ if (iteration > maxIterations) {
86
+ throw new PackError('SPLIT_TOO_SMALL', `Could not split into parts of ${limit} tokens`);
87
+ }
88
+ const rendered = renderParts(parts, partDoc, renderer, countOnce);
89
+ const over = rendered.findIndex((part) => part.tokens > limit);
90
+ if (over === -1) {
91
+ return {
92
+ parts: rendered.map((part, i) => ({ ...part, files: parts[i].map(labelOf) })),
93
+ splitFiles,
94
+ };
95
+ }
96
+
97
+ const isLast = over === parts.length - 1;
98
+ if (isLast && hasLastExtras && parts[over].length) {
99
+ parts.push([]); // the omitted list and instructions get a part of their own
100
+ } else if (parts[over].length > 1 || (over === 0 && hasFirstExtras && parts[over].length)) {
101
+ if (isLast) parts.push([]);
102
+ parts[over + 1].unshift(parts[over].pop());
103
+ } else {
104
+ throw new PackError(
105
+ 'SPLIT_TOO_SMALL',
106
+ `Part ${over + 1} needs ${rendered[over].tokens} tokens; raise --split` +
107
+ (over === 0 && doc.tree !== null ? ' or use --no-tree' : '')
108
+ );
109
+ }
110
+ parts = parts.filter(
111
+ (units, i) =>
112
+ units.length ||
113
+ (i === 0 && hasFirstExtras) ||
114
+ (i === parts.length - 1 && hasLastExtras && parts.length > 1)
115
+ );
116
+ if (!parts.length) parts = [[]];
117
+ }
118
+ }
119
+
120
+ function renderParts(parts, partDoc, renderer, count) {
121
+ let firstIndex = 1;
122
+ return parts.map((units, i) => {
123
+ const text = renderer.render(partDoc(units, i, parts.length, firstIndex));
124
+ firstIndex += units.length;
125
+ return { index: i + 1, text, tokens: count(text) };
126
+ });
127
+ }
128
+
129
+ const labelOf = (unit) =>
130
+ unit.lines ? `${unit.path} (lines ${unit.lines[0]}-${unit.lines[1]})` : unit.path;
131
+
132
+ /**
133
+ * Cut one entry into pieces of whole lines, each fitting in a part on its own
134
+ * @returns {Array} Entries with `lines: [first, last]` and `totalLines`
135
+ */
136
+ function splitByLines(entry, limit, { renderer, count, middleDoc }) {
137
+ const endsWithNewline = entry.content.endsWith('\n');
138
+ const lines = (endsWithNewline ? entry.content.slice(0, -1) : entry.content).split('\n');
139
+ const numberOf = (i) => (entry.lineNumbers ? entry.lineNumbers[i] : i + 1);
140
+ const lineTokens = lines.map((line) => count(`${line}\n`));
141
+ const fits = (piece) => count(renderer.render(middleDoc([piece]))) <= limit;
142
+
143
+ const piece = (from, to) => {
144
+ const content = lines.slice(from, to).join('\n') + (endsWithNewline ? '\n' : '');
145
+ return {
146
+ ...entry,
147
+ content,
148
+ tokens: count(content),
149
+ lines: [numberOf(from), numberOf(to - 1)],
150
+ totalLines: entry.totalLines,
151
+ };
152
+ };
153
+
154
+ const empty = count(renderer.render(middleDoc([piece(0, 1)]))) - lineTokens[0];
155
+ let margin = Math.max(16, Math.ceil(limit * 0.02));
156
+ for (let attempt = 0; attempt < 6; attempt++, margin *= 2) {
157
+ const room = limit - empty - margin;
158
+ const pieces = [];
159
+ let start = 0;
160
+ let used = 0;
161
+ for (let i = 0; i < lines.length; i++) {
162
+ if (lineTokens[i] > room) {
163
+ throw new PackError(
164
+ 'SPLIT_TOO_SMALL',
165
+ `${entry.path}: line ${numberOf(i)} alone has ${lineTokens[i]} tokens, more than ` +
166
+ `a ${limit}-token part can hold`
167
+ );
168
+ }
169
+ if (used + lineTokens[i] > room && i > start) {
170
+ pieces.push(piece(start, i));
171
+ start = i;
172
+ used = 0;
173
+ }
174
+ used += lineTokens[i];
175
+ }
176
+ pieces.push(piece(start, lines.length));
177
+ if (pieces.every(fits)) return pieces;
178
+ }
179
+ throw new PackError('SPLIT_TOO_SMALL', `Could not split ${entry.path} into ${limit}-token parts`);
180
+ }
181
+
182
+ export default { splitIntoParts };
@@ -0,0 +1,35 @@
1
+ /**
2
+ * Directory tree for the top of a packed document
3
+ */
4
+
5
+ /**
6
+ * Render project-relative paths as an indented tree, directories first
7
+ * @param {string[]} paths - Paths with `/` separators
8
+ * @returns {string} One entry per line, two spaces per level, directories end with `/`
9
+ */
10
+ export function buildTree(paths) {
11
+ const root = { dirs: new Map(), files: [] };
12
+ for (const filePath of paths) {
13
+ const parts = filePath.split('/');
14
+ let node = root;
15
+ for (const dir of parts.slice(0, -1)) {
16
+ if (!node.dirs.has(dir)) node.dirs.set(dir, { dirs: new Map(), files: [] });
17
+ node = node.dirs.get(dir);
18
+ }
19
+ node.files.push(parts[parts.length - 1]);
20
+ }
21
+
22
+ const lines = [];
23
+ const walk = (node, depth) => {
24
+ const indent = ' '.repeat(depth);
25
+ for (const name of [...node.dirs.keys()].sort()) {
26
+ lines.push(`${indent}${name}/`);
27
+ walk(node.dirs.get(name), depth + 1);
28
+ }
29
+ for (const name of [...node.files].sort()) lines.push(`${indent}${name}`);
30
+ };
31
+ walk(root, 0);
32
+ return lines.join('\n');
33
+ }
34
+
35
+ export default buildTree;