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
@@ -4,334 +4,406 @@
4
4
  * v2.3.6+
5
5
  */
6
6
 
7
- import { execSync } from 'child_process';
7
+ import { execSync, execFileSync } from 'child_process';
8
8
  import fs from 'fs';
9
9
  import path from 'path';
10
10
  import https from 'https';
11
11
  import TokenCalculator from '../analyzers/token-calculator.js';
12
12
  import GitIngestFormatter from '../formatters/gitingest-formatter.js';
13
+ import { isValidGitRef } from '../integrations/git/GitClient.js';
14
+ import { enforceExport, PolicyViolationError } from '../policy/enforce.js';
15
+ import { recordExport, getGitHead } from '../policy/audit.js';
13
16
 
14
17
  class GitUtils {
15
- constructor(options = {}) {
16
- this.tempDir = options.tempDir || path.join(process.cwd(), '.ctxman', 'temp');
17
- this.outputDir = options.outputDir || path.join(process.cwd(), 'docs');
18
- this.verbose = options.verbose || false;
18
+ constructor(options = {}) {
19
+ this.tempDir = options.tempDir || path.join(process.cwd(), '.ctxman', 'temp');
20
+ this.outputDir = options.outputDir || path.join(process.cwd(), 'docs');
21
+ this.verbose = options.verbose || false;
22
+ }
23
+
24
+ /**
25
+ * Parse GitHub URL and extract repo info
26
+ */
27
+ parseGitHubURL(url) {
28
+ // Support various GitHub URL formats:
29
+ // - https://github.com/owner/repo
30
+ // - https://github.com/owner/repo.git
31
+ // - git@github.com:owner/repo.git
32
+ // - github.com/owner/repo
33
+ // - owner/repo
34
+
35
+ let cleanUrl = url.trim();
36
+
37
+ // Remove git@ prefix
38
+ if (cleanUrl.startsWith('git@')) {
39
+ cleanUrl = cleanUrl.replace('git@github.com:', 'https://github.com/');
19
40
  }
20
41
 
21
- /**
22
- * Parse GitHub URL and extract repo info
23
- */
24
- parseGitHubURL(url) {
25
- // Support various GitHub URL formats:
26
- // - https://github.com/owner/repo
27
- // - https://github.com/owner/repo.git
28
- // - git@github.com:owner/repo.git
29
- // - github.com/owner/repo
30
- // - owner/repo
31
-
32
- let cleanUrl = url.trim();
33
-
34
- // Remove git@ prefix
35
- if (cleanUrl.startsWith('git@')) {
36
- cleanUrl = cleanUrl.replace('git@github.com:', 'https://github.com/');
37
- }
38
-
39
- // Add https if missing
40
- if (!cleanUrl.startsWith('http')) {
41
- if (cleanUrl.includes('/')) {
42
- cleanUrl = `https://github.com/${cleanUrl}`;
43
- }
44
- }
45
-
46
- // Remove .git suffix
47
- cleanUrl = cleanUrl.replace(/\.git$/, '');
48
-
49
- // Parse URL
50
- try {
51
- const urlObj = new URL(cleanUrl);
52
- const pathParts = urlObj.pathname.split('/').filter(p => p);
53
-
54
- if (pathParts.length < 2) {
55
- throw new Error('Invalid GitHub URL: must include owner and repo');
56
- }
57
-
58
- const owner = pathParts[0];
59
- const repo = pathParts[1];
60
- const branch = pathParts[3] || 'main'; // If /tree/branch specified
61
-
62
- return {
63
- owner,
64
- repo,
65
- branch,
66
- fullName: `${owner}/${repo}`,
67
- cloneUrl: `https://github.com/${owner}/${repo}.git`,
68
- apiUrl: `https://api.github.com/repos/${owner}/${repo}`,
69
- rawUrl: `https://raw.githubusercontent.com/${owner}/${repo}/${branch}`,
70
- originalUrl: url
71
- };
72
- } catch (error) {
73
- throw new Error(`Failed to parse GitHub URL: ${error.message}`);
74
- }
42
+ // Add https if missing
43
+ if (!cleanUrl.startsWith('http')) {
44
+ // "github.com/owner/repo" only lacks the scheme; prefixing the host again would
45
+ // turn "github.com" into the owner
46
+ if (/^github\.com\//i.test(cleanUrl)) {
47
+ cleanUrl = `https://${cleanUrl}`;
48
+ } else if (cleanUrl.includes('/')) {
49
+ cleanUrl = `https://github.com/${cleanUrl}`;
50
+ }
75
51
  }
76
52
 
77
- /**
78
- * Check if git is installed
79
- */
80
- isGitInstalled() {
81
- try {
82
- execSync('git --version', { stdio: 'pipe' });
83
- return true;
84
- } catch (error) {
85
- return false;
86
- }
53
+ // Remove .git suffix
54
+ cleanUrl = cleanUrl.replace(/\.git$/, '');
55
+
56
+ // Parse URL
57
+ try {
58
+ const urlObj = new URL(cleanUrl);
59
+ const pathParts = urlObj.pathname.split('/').filter((p) => p);
60
+
61
+ if (pathParts.length < 2) {
62
+ throw new Error('Invalid GitHub URL: must include owner and repo');
63
+ }
64
+
65
+ const owner = pathParts[0];
66
+ const repo = pathParts[1];
67
+ const branchFromUrl = Boolean(pathParts[3]); // /owner/repo/tree/<branch>
68
+ const branch = pathParts[3] || 'main';
69
+
70
+ return {
71
+ owner,
72
+ repo,
73
+ branch,
74
+ branchFromUrl,
75
+ fullName: `${owner}/${repo}`,
76
+ cloneUrl: `https://github.com/${owner}/${repo}.git`,
77
+ apiUrl: `https://api.github.com/repos/${owner}/${repo}`,
78
+ rawUrl: `https://raw.githubusercontent.com/${owner}/${repo}/${branch}`,
79
+ originalUrl: url,
80
+ };
81
+ } catch (error) {
82
+ throw new Error(`Failed to parse GitHub URL: ${error.message}`);
83
+ }
84
+ }
85
+
86
+ /**
87
+ * Check if git is installed
88
+ */
89
+ isGitInstalled() {
90
+ try {
91
+ execSync('git --version', { stdio: 'pipe' });
92
+ return true;
93
+ } catch (_error) {
94
+ return false;
95
+ }
96
+ }
97
+
98
+ /**
99
+ * Clone repository to temp directory
100
+ */
101
+ cloneRepository(repoInfo, options = {}) {
102
+ if (!this.isGitInstalled()) {
103
+ throw new Error('Git is not installed. Please install git to use this feature.');
87
104
  }
88
105
 
89
- /**
90
- * Clone repository to temp directory
91
- */
92
- cloneRepository(repoInfo, options = {}) {
93
- if (!this.isGitInstalled()) {
94
- throw new Error('Git is not installed. Please install git to use this feature.');
95
- }
96
-
97
- const { shallow = true, depth = 1 } = options;
98
- const repoPath = path.join(this.tempDir, repoInfo.fullName.replace('/', '-'));
99
-
100
- // Create temp directory
101
- if (!fs.existsSync(this.tempDir)) {
102
- fs.mkdirSync(this.tempDir, { recursive: true });
103
- }
104
-
105
- // Remove existing clone if present
106
- if (fs.existsSync(repoPath)) {
107
- if (this.verbose) {
108
- console.log(`♻️ Removing existing clone: ${repoPath}`);
109
- }
110
- fs.rmSync(repoPath, { recursive: true, force: true });
111
- }
112
-
113
- // Clone repository
114
- if (this.verbose) {
115
- console.log(`📥 Cloning ${repoInfo.fullName}...`);
116
- }
106
+ const { shallow = true, depth = 1 } = options;
107
+ const repoPath = path.join(this.tempDir, repoInfo.fullName.replace('/', '-'));
117
108
 
118
- const cloneCommand = shallow
119
- ? `git clone --depth ${depth} --single-branch --branch ${repoInfo.branch} ${repoInfo.cloneUrl} "${repoPath}"`
120
- : `git clone ${repoInfo.cloneUrl} "${repoPath}"`;
109
+ // Create temp directory
110
+ if (!fs.existsSync(this.tempDir)) {
111
+ fs.mkdirSync(this.tempDir, { recursive: true });
112
+ }
121
113
 
122
- try {
123
- execSync(cloneCommand, {
124
- stdio: this.verbose ? 'inherit' : 'pipe'
125
- });
114
+ // Remove existing clone if present
115
+ if (fs.existsSync(repoPath)) {
116
+ if (this.verbose) {
117
+ console.log(`♻️ Removing existing clone: ${repoPath}`);
118
+ }
119
+ fs.rmSync(repoPath, { recursive: true, force: true });
120
+ }
126
121
 
127
- if (this.verbose) {
128
- console.log(`✅ Repository cloned to: ${repoPath}`);
129
- }
122
+ // Clone repository
123
+ if (this.verbose) {
124
+ console.log(`📥 Cloning ${repoInfo.fullName}...`);
125
+ }
130
126
 
131
- return repoPath;
132
- } catch (error) {
133
- throw new Error(`Failed to clone repository: ${error.message}`);
134
- }
127
+ if (repoInfo.branch && !isValidGitRef(repoInfo.branch)) {
128
+ throw new Error(`Invalid branch name: ${JSON.stringify(repoInfo.branch)}`);
135
129
  }
136
130
 
137
- /**
138
- * Generate GitIngest digest from GitHub URL
139
- */
140
- async generateFromGitHub(url, options = {}) {
141
- const {
142
- outputFile,
143
- cleanup = true,
144
- shallow = true,
145
- analyzerOptions = {}
146
- } = options;
147
-
148
- // Parse GitHub URL
149
- const repoInfo = this.parseGitHubURL(url);
150
- console.log(`\n📦 Processing GitHub Repository: ${repoInfo.fullName}`);
151
- console.log(` Branch: ${repoInfo.branch}`);
152
-
153
- // Clone repository
154
- console.log(`\n📥 Cloning repository...`);
155
- const repoPath = this.cloneRepository(repoInfo, { shallow });
156
-
157
- // Generate GitIngest digest
158
- console.log(`\n🔍 Analyzing repository...`);
159
-
160
- const analyzer = new TokenCalculator(repoPath, {
161
- verbose: this.verbose,
162
- ...analyzerOptions
131
+ // Argument list, no shell. Option values are attached (`--branch=x`) so none of them
132
+ // is a standalone argument, and `--` ends option parsing before the URL and path
133
+ // Without a branch, git clones the remote's default branch
134
+ const branchArgs = repoInfo.branch ? [`--branch=${repoInfo.branch}`] : [];
135
+ const shallowArgs = shallow
136
+ ? [`--depth=${Number.parseInt(depth, 10) || 1}`, '--single-branch', ...branchArgs]
137
+ : branchArgs;
138
+ const cloneArgs = ['clone', ...shallowArgs, '--', repoInfo.cloneUrl, repoPath];
139
+
140
+ try {
141
+ execFileSync('git', cloneArgs, {
142
+ stdio: this.verbose ? 'inherit' : 'pipe',
143
+ });
144
+
145
+ if (this.verbose) {
146
+ console.log(`✅ Repository cloned to: ${repoPath}`);
147
+ }
148
+
149
+ return repoPath;
150
+ } catch (error) {
151
+ throw new Error(`Failed to clone repository: ${error.message}`);
152
+ }
153
+ }
154
+
155
+ /**
156
+ * Generate GitIngest digest from GitHub URL
157
+ */
158
+ async generateFromGitHub(url, options = {}) {
159
+ const { outputFile, cleanup = true, shallow = true, analyzerOptions = {} } = options;
160
+ // Export policy from loadPolicy() (optional): deny rules, limits and audit log
161
+ const policy = options.policy?.active ? options.policy : null;
162
+
163
+ // Parse GitHub URL. An explicit branch wins, then one named in the URL (/tree/<branch>);
164
+ // otherwise the clone uses the remote's default branch
165
+ const repoInfo = this.parseGitHubURL(url);
166
+ repoInfo.branch = options.branch || (repoInfo.branchFromUrl ? repoInfo.branch : null);
167
+ console.log(`\n📦 Processing GitHub Repository: ${repoInfo.fullName}`);
168
+ console.log(` Branch: ${repoInfo.branch || '(default)'}`);
169
+
170
+ // Clone repository
171
+ console.log(`\n📥 Cloning repository...`);
172
+ const repoPath = this.cloneRepository(repoInfo, { shallow });
173
+
174
+ // Generate GitIngest digest
175
+ console.log(`\n🔍 Analyzing repository...`);
176
+
177
+ const analyzer = new TokenCalculator(repoPath, {
178
+ verbose: this.verbose,
179
+ policy,
180
+ ...analyzerOptions,
181
+ });
182
+
183
+ // Run analysis
184
+ const results = analyzer.analyze();
185
+
186
+ const audit = {
187
+ command: 'github',
188
+ root: `https://github.com/${repoInfo.fullName}`,
189
+ gitHead: policy ? getGitHead(repoPath) : null,
190
+ format: 'gitingest',
191
+ files: results,
192
+ tokens: analyzer.stats.totalTokens,
193
+ removedByPolicy: analyzer.stats.policyDeniedFiles ?? 0,
194
+ };
195
+ if (policy) {
196
+ if (audit.removedByPolicy > 0) {
197
+ console.log(`🛡️ Removed ${audit.removedByPolicy} file(s) denied by the export policy`);
198
+ }
199
+ try {
200
+ enforceExport(policy, {
201
+ ...audit,
202
+ redact: options.formatterOptions?.redactSecrets !== false,
163
203
  });
164
-
165
- // Run analysis
166
- const results = analyzer.analyze();
167
-
168
- // Generate digest
169
- const formatter = new GitIngestFormatter(
170
- repoPath,
171
- analyzer.stats,
172
- results,
173
- options.formatterOptions || {}
174
- );
175
-
176
- const digest = formatter.generateDigest();
177
-
178
- // Determine output file
179
- const outputFileName = outputFile ||
180
- `${repoInfo.fullName.replace('/', '-')}-gitingest-${Date.now().toString(36)}.txt`;
181
-
182
- const outputPath = path.isAbsolute(outputFileName)
183
- ? outputFileName
184
- : path.join(this.outputDir, outputFileName);
185
-
186
- // Ensure output directory exists
187
- const outputDirPath = path.dirname(outputPath);
188
- if (!fs.existsSync(outputDirPath)) {
189
- fs.mkdirSync(outputDirPath, { recursive: true });
190
- }
191
-
192
- // Save digest
193
- fs.writeFileSync(outputPath, digest, 'utf8');
194
-
195
- const stats = {
196
- repository: repoInfo.fullName,
197
- branch: repoInfo.branch,
198
- url: repoInfo.originalUrl,
199
- localPath: repoPath,
200
- outputFile: outputPath,
201
- digestSize: digest.length,
202
- digestSizeKB: (digest.length / 1024).toFixed(2),
203
- files: analyzer.stats.totalFiles,
204
- tokens: analyzer.stats.totalTokens,
205
- lines: analyzer.stats.totalLines
206
- };
207
-
208
- // Cleanup temp directory if requested
209
- if (cleanup) {
210
- console.log(`\n🧹 Cleaning up temporary files...`);
211
- fs.rmSync(repoPath, { recursive: true, force: true });
204
+ } catch (error) {
205
+ if (error instanceof PolicyViolationError) {
206
+ recordExport(policy, { ...audit, outcome: 'refused', violations: error.violations });
207
+ if (cleanup) fs.rmSync(repoPath, { recursive: true, force: true });
212
208
  }
213
-
214
- console.log(`\n✅ GitIngest digest generated!`);
215
- console.log(` Output: ${outputPath}`);
216
- console.log(` Size: ${stats.digestSizeKB} KB`);
217
- console.log(` Files: ${stats.files}`);
218
- console.log(` Tokens: ${stats.tokens.toLocaleString()}`);
219
-
220
- return stats;
209
+ throw error;
210
+ }
221
211
  }
222
212
 
223
- /**
224
- * Fetch file from GitHub raw URL (without cloning)
225
- */
226
- async fetchFileFromGitHub(url) {
227
- return new Promise((resolve, reject) => {
228
- https.get(url, (res) => {
229
- let data = '';
230
-
231
- res.on('data', (chunk) => {
232
- data += chunk;
233
- });
234
-
235
- res.on('end', () => {
236
- if (res.statusCode === 200) {
237
- resolve(data);
238
- } else {
239
- reject(new Error(`HTTP ${res.statusCode}: ${data}`));
240
- }
241
- });
242
- }).on('error', reject);
243
- });
213
+ // Generate digest
214
+ const formatter = new GitIngestFormatter(
215
+ repoPath,
216
+ analyzer.stats,
217
+ results,
218
+ options.formatterOptions || {}
219
+ );
220
+
221
+ // Determine output file
222
+ const outputFileName =
223
+ outputFile ||
224
+ `${repoInfo.fullName.replace('/', '-')}-gitingest-${Date.now().toString(36)}.txt`;
225
+
226
+ const outputPath = path.isAbsolute(outputFileName)
227
+ ? outputFileName
228
+ : path.join(this.outputDir, outputFileName);
229
+
230
+ // Ensure output directory exists
231
+ const outputDirPath = path.dirname(outputPath);
232
+ if (!fs.existsSync(outputDirPath)) {
233
+ fs.mkdirSync(outputDirPath, { recursive: true });
244
234
  }
245
235
 
246
- /**
247
- * Get repository info from GitHub API
248
- */
249
- async getRepositoryInfo(repoInfo) {
250
- return new Promise((resolve, reject) => {
251
- const options = {
252
- headers: {
253
- 'User-Agent': 'ctxman-git-utils'
254
- }
255
- };
256
-
257
- https.get(repoInfo.apiUrl, options, (res) => {
258
- let data = '';
259
-
260
- res.on('data', (chunk) => {
261
- data += chunk;
262
- });
263
-
264
- res.on('end', () => {
265
- try {
266
- const repo = JSON.parse(data);
267
- resolve({
268
- name: repo.name,
269
- fullName: repo.full_name,
270
- description: repo.description,
271
- stars: repo.stargazers_count,
272
- forks: repo.forks_count,
273
- defaultBranch: repo.default_branch,
274
- size: repo.size,
275
- language: repo.language,
276
- updatedAt: repo.updated_at
277
- });
278
- } catch (error) {
279
- reject(new Error(`Failed to parse GitHub API response: ${error.message}`));
280
- }
281
- });
282
- }).on('error', reject);
283
- });
236
+ // Save digest (a chunked digest is written as one file per chunk)
237
+ const digestLength = formatter.saveToFile(outputPath);
238
+
239
+ if (policy?.audit.enabled) {
240
+ const written = formatter.chunkFiles.length ? formatter.chunkFiles : [outputPath];
241
+ recordExport(policy, {
242
+ ...audit,
243
+ destination: outputPath,
244
+ outputFiles: written.length > 1 ? written : undefined,
245
+ redactions: formatter.redactions,
246
+ text: written.map((file) => fs.readFileSync(file)),
247
+ });
284
248
  }
285
249
 
286
- /**
287
- * Cleanup temp directory
288
- */
289
- cleanupTemp() {
290
- if (fs.existsSync(this.tempDir)) {
291
- fs.rmSync(this.tempDir, { recursive: true, force: true });
292
- if (this.verbose) {
293
- console.log(`🧹 Cleaned up: ${this.tempDir}`);
294
- }
295
- }
250
+ const stats = {
251
+ repository: repoInfo.fullName,
252
+ branch: repoInfo.branch,
253
+ url: repoInfo.originalUrl,
254
+ localPath: repoPath,
255
+ outputFile: formatter.chunkFiles.length ? formatter.chunkFiles[0] : outputPath,
256
+ chunkFiles: formatter.chunkFiles,
257
+ digestSize: digestLength,
258
+ digestSizeKB: (digestLength / 1024).toFixed(2),
259
+ files: analyzer.stats.totalFiles,
260
+ tokens: analyzer.stats.totalTokens,
261
+ lines: analyzer.stats.totalLines,
262
+ };
263
+
264
+ // Cleanup temp directory if requested
265
+ if (cleanup) {
266
+ console.log(`\n🧹 Cleaning up temporary files...`);
267
+ fs.rmSync(repoPath, { recursive: true, force: true });
296
268
  }
297
269
 
298
- /**
299
- * List cached repositories
300
- */
301
- listCachedRepos() {
302
- if (!fs.existsSync(this.tempDir)) {
303
- return [];
304
- }
305
-
306
- return fs.readdirSync(this.tempDir)
307
- .filter(name => fs.statSync(path.join(this.tempDir, name)).isDirectory())
308
- .map(name => ({
309
- name,
310
- path: path.join(this.tempDir, name),
311
- size: this.getDirectorySize(path.join(this.tempDir, name))
312
- }));
270
+ console.log(`\n✅ GitIngest digest generated!`);
271
+ if (stats.chunkFiles.length) {
272
+ console.log(` Output: ${stats.chunkFiles.length} chunk files`);
273
+ stats.chunkFiles.forEach((file) => console.log(` ${file}`));
274
+ } else {
275
+ console.log(` Output: ${outputPath}`);
313
276
  }
314
-
315
- /**
316
- * Get directory size recursively
317
- */
318
- getDirectorySize(dir) {
319
- let size = 0;
320
-
321
- const files = fs.readdirSync(dir);
322
- for (const file of files) {
323
- const filePath = path.join(dir, file);
324
- const stats = fs.statSync(filePath);
325
-
326
- if (stats.isDirectory()) {
327
- size += this.getDirectorySize(filePath);
277
+ console.log(` Size: ${stats.digestSizeKB} KB`);
278
+ console.log(` Files: ${stats.files}`);
279
+ console.log(` Tokens: ${stats.tokens.toLocaleString()}`);
280
+
281
+ return stats;
282
+ }
283
+
284
+ /**
285
+ * Fetch file from GitHub raw URL (without cloning)
286
+ */
287
+ async fetchFileFromGitHub(url) {
288
+ return new Promise((resolve, reject) => {
289
+ https
290
+ .get(url, (res) => {
291
+ let data = '';
292
+
293
+ res.on('data', (chunk) => {
294
+ data += chunk;
295
+ });
296
+
297
+ res.on('end', () => {
298
+ if (res.statusCode === 200) {
299
+ resolve(data);
328
300
  } else {
329
- size += stats.size;
301
+ reject(new Error(`HTTP ${res.statusCode}: ${data}`));
330
302
  }
331
- }
303
+ });
304
+ })
305
+ .on('error', reject);
306
+ });
307
+ }
308
+
309
+ /**
310
+ * Get repository info from GitHub API
311
+ */
312
+ async getRepositoryInfo(repoInfo) {
313
+ return new Promise((resolve, reject) => {
314
+ const options = {
315
+ headers: {
316
+ 'User-Agent': 'ctxman-git-utils',
317
+ },
318
+ };
319
+
320
+ https
321
+ .get(repoInfo.apiUrl, options, (res) => {
322
+ let data = '';
323
+
324
+ res.on('data', (chunk) => {
325
+ data += chunk;
326
+ });
327
+
328
+ res.on('end', () => {
329
+ try {
330
+ const repo = JSON.parse(data);
331
+ if (res.statusCode !== 200) {
332
+ reject(
333
+ new Error(`GitHub API returned ${res.statusCode}: ${repo.message || 'error'}`)
334
+ );
335
+ return;
336
+ }
337
+ resolve({
338
+ name: repo.name,
339
+ fullName: repo.full_name,
340
+ description: repo.description,
341
+ stars: repo.stargazers_count,
342
+ forks: repo.forks_count,
343
+ defaultBranch: repo.default_branch,
344
+ size: repo.size,
345
+ language: repo.language,
346
+ updatedAt: repo.updated_at,
347
+ });
348
+ } catch (error) {
349
+ reject(new Error(`Failed to parse GitHub API response: ${error.message}`));
350
+ }
351
+ });
352
+ })
353
+ .on('error', reject);
354
+ });
355
+ }
356
+
357
+ /**
358
+ * Cleanup temp directory
359
+ */
360
+ cleanupTemp() {
361
+ if (fs.existsSync(this.tempDir)) {
362
+ fs.rmSync(this.tempDir, { recursive: true, force: true });
363
+ if (this.verbose) {
364
+ console.log(`🧹 Cleaned up: ${this.tempDir}`);
365
+ }
366
+ }
367
+ }
368
+
369
+ /**
370
+ * List cached repositories
371
+ */
372
+ listCachedRepos() {
373
+ if (!fs.existsSync(this.tempDir)) {
374
+ return [];
375
+ }
332
376
 
333
- return size;
377
+ return fs
378
+ .readdirSync(this.tempDir)
379
+ .filter((name) => fs.statSync(path.join(this.tempDir, name)).isDirectory())
380
+ .map((name) => ({
381
+ name,
382
+ path: path.join(this.tempDir, name),
383
+ size: this.getDirectorySize(path.join(this.tempDir, name)),
384
+ }));
385
+ }
386
+
387
+ /**
388
+ * Get directory size recursively
389
+ */
390
+ getDirectorySize(dir) {
391
+ let size = 0;
392
+
393
+ const files = fs.readdirSync(dir);
394
+ for (const file of files) {
395
+ const filePath = path.join(dir, file);
396
+ const stats = fs.statSync(filePath);
397
+
398
+ if (stats.isDirectory()) {
399
+ size += this.getDirectorySize(filePath);
400
+ } else {
401
+ size += stats.size;
402
+ }
334
403
  }
404
+
405
+ return size;
406
+ }
335
407
  }
336
408
 
337
409
  export default GitUtils;