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
package/bin/cm-ask.js CHANGED
@@ -1,11 +1,10 @@
1
1
  #!/usr/bin/env node
2
2
  import { VectorStoreFactory } from '../lib/rag/VectorStoreFactory.js';
3
- import { EmbeddingProvider, TransformersEmbeddingProvider } from '../lib/rag/EmbeddingProvider.js';
3
+ import { TransformersEmbeddingProvider } from '../lib/rag/EmbeddingProvider.js';
4
4
  import { Indexer } from '../lib/rag/Indexer.js';
5
5
  import { FileDataSource } from '../lib/rag/sources/FileDataSource.js';
6
6
  import path from 'path';
7
7
  import fs from 'fs/promises';
8
- import { TokenizerManager } from '../lib/utils/tokenizer-adapter.js';
9
8
 
10
9
  async function main() {
11
10
  const args = process.argv.slice(2);
@@ -33,7 +32,7 @@ async function main() {
33
32
 
34
33
  // 2. Check if indexed
35
34
  const isIndexed = await checkIndexExists(storePath);
36
-
35
+
37
36
  if (!isIndexed) {
38
37
  console.log('šŸ“š Index not found. Auto-indexing current project...');
39
38
  await indexProject(store);
@@ -50,16 +49,16 @@ async function main() {
50
49
 
51
50
  // 4. Display Results (or feed to LLM if we had one configured)
52
51
  console.log(`\nFound ${results.length} relevant snippets:\n`);
53
-
52
+
54
53
  results.forEach((r, i) => {
55
- console.log(`[${i+1}] ${r.metadata.filename} (Score: ${r.score?.toFixed(4)})`);
54
+ console.log(`[${i + 1}] ${r.metadata.filename} (Score: ${r.score?.toFixed(4)})`);
56
55
  console.log('─'.repeat(40));
57
56
  // Show a small snippet around the match if possible, but we stored full text.
58
57
  // Let's just show the first 200 chars or so.
59
58
  console.log(r.text.substring(0, 300).replace(/\n/g, ' ') + '...');
60
59
  console.log('\n');
61
60
  });
62
-
61
+
63
62
  // Future: Pass `results` to an LLM API to generate a real answer.
64
63
  // For now, this validates the retrieval pipeline works from CLI.
65
64
  }
@@ -81,15 +80,21 @@ async function indexProject(store) {
81
80
  indexer.registerSource(fileSource);
82
81
 
83
82
  console.log('ā³ Indexing files (this may take a moment)...');
84
- await indexer.index('local-files', {
85
- targetDir: process.cwd(),
86
- include: ['.js', '.md', '.ts', '.json'], // Configurable?
87
- exclude: ['node_modules', '.git', 'dist', 'coverage', '.ctxman']
88
- });
83
+ try {
84
+ await indexer.index('local-files', {
85
+ targetDir: process.cwd(),
86
+ include: ['.js', '.md', '.ts', '.json'], // Configurable?
87
+ exclude: ['node_modules', '.git', 'dist', 'coverage', '.ctxman'],
88
+ });
89
+ } catch (error) {
90
+ console.error(`āŒ ${error.message}`);
91
+ process.exit(1);
92
+ }
89
93
  console.log('āœ… Indexing complete.');
90
94
  }
91
95
 
92
- main().catch(err => {
93
- console.error('āŒ Error:', err);
96
+ main().catch((err) => {
97
+ // Message only: missing optional dependencies surface here with an install hint
98
+ console.error('āŒ Error:', err.message);
94
99
  process.exit(1);
95
100
  });
@@ -0,0 +1,124 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Audit Command
4
+ * Print the export audit log written while a policy enables `audit`
5
+ */
6
+
7
+ import path from 'path';
8
+ import {
9
+ loadPolicy,
10
+ readAuditLog,
11
+ filterAuditEvents,
12
+ formatAuditTable,
13
+ parseSince,
14
+ DEFAULT_AUDIT_PATH,
15
+ POLICY_FILE,
16
+ } from '../lib/policy/index.js';
17
+
18
+ const DEFAULT_TABLE_LIMIT = 50;
19
+
20
+ function main() {
21
+ const args = process.argv.slice(2);
22
+
23
+ if (args.includes('--help') || args.includes('-h')) {
24
+ printHelp();
25
+ return;
26
+ }
27
+
28
+ const json = args.includes('--json');
29
+ let since = null;
30
+ let limit = json ? 0 : DEFAULT_TABLE_LIMIT;
31
+ let file = null;
32
+
33
+ try {
34
+ const sinceValue = valueOf(args, '--since');
35
+ if (sinceValue !== undefined) since = parseSince(sinceValue);
36
+
37
+ const limitValue = valueOf(args, '--limit');
38
+ if (limitValue !== undefined) {
39
+ if (!/^\d+$/.test(limitValue)) {
40
+ throw new Error(`Invalid --limit value ${JSON.stringify(limitValue)}: expected 0 or more`);
41
+ }
42
+ limit = Number(limitValue);
43
+ }
44
+
45
+ file = valueOf(args, '--file') ?? null;
46
+ } catch (error) {
47
+ console.error(`āŒ ${error.message}`);
48
+ process.exitCode = 2;
49
+ return;
50
+ }
51
+
52
+ const files = file ? [path.resolve(file)] : auditFiles();
53
+ const { events, skipped, missing } = readAuditLog(files);
54
+ const selected = filterAuditEvents(events, { since, limit });
55
+
56
+ if (json) {
57
+ console.log(JSON.stringify(selected, null, 2));
58
+ } else if (missing.length === files.length) {
59
+ console.log(`No audit log found (${files.join(', ')}).`);
60
+ console.log(
61
+ `Exports are logged while ${POLICY_FILE} has "audit": {"enabled": true} (ctxman policy init).`
62
+ );
63
+ } else if (selected.length === 0) {
64
+ console.log(
65
+ since ? `No exports recorded since ${since.toISOString()}.` : 'No exports recorded.'
66
+ );
67
+ } else {
68
+ console.log(formatAuditTable(selected));
69
+ console.log();
70
+ const matching = filterAuditEvents(events, { since }).length;
71
+ const shown =
72
+ selected.length < matching
73
+ ? `Showing the last ${selected.length} of ${matching} events (--limit 0 shows all)`
74
+ : `${selected.length} event${selected.length === 1 ? '' : 's'}`;
75
+ console.log(`${shown} from ${files.filter((f) => !missing.includes(f)).join(', ')}`);
76
+ }
77
+
78
+ if (skipped > 0) {
79
+ console.error(
80
+ `āš ļø Skipped ${skipped} line${skipped === 1 ? '' : 's'} that are not JSON events`
81
+ );
82
+ }
83
+ }
84
+
85
+ // The policy's audit logs; the default location when no policy names one
86
+ function auditFiles() {
87
+ const root = process.cwd();
88
+ try {
89
+ const policy = loadPolicy(root);
90
+ if (policy.audit.paths.length > 0) return policy.audit.paths;
91
+ } catch (error) {
92
+ console.error(`āš ļø ${error.message}`);
93
+ console.error(` Reading the default log ${DEFAULT_AUDIT_PATH} instead.`);
94
+ }
95
+ return [path.join(root, DEFAULT_AUDIT_PATH)];
96
+ }
97
+
98
+ function valueOf(args, flag) {
99
+ const index = args.indexOf(flag);
100
+ if (index === -1) return undefined;
101
+ const value = args[index + 1];
102
+ if (value === undefined || value.startsWith('--')) {
103
+ throw new Error(`${flag} needs a value`);
104
+ }
105
+ return value;
106
+ }
107
+
108
+ function printHelp() {
109
+ console.log('ctxman audit - show the export audit log\n');
110
+ console.log('Usage: ctxman audit [options]\n');
111
+ console.log('Options:');
112
+ console.log(' --since DATE Only exports at or after DATE: a date (2026-10-01), an ISO');
113
+ console.log(' time, or an age such as 30m, 12h, 7d, 2w');
114
+ console.log(` --limit N Show the last N exports (default: ${DEFAULT_TABLE_LIMIT};`);
115
+ console.log(' with --json: all; 0 = all)');
116
+ console.log(' --json Print the events as a JSON array');
117
+ console.log(" --file PATH Read this log instead of the policy's audit log(s)");
118
+ console.log(' -h, --help Show this help\n');
119
+ console.log(`The log is written while ${POLICY_FILE} (or $CTXMAN_POLICY) enables audit;`);
120
+ console.log(`default location: ${DEFAULT_AUDIT_PATH}. One JSON object per line; each export`);
121
+ console.log('records who, when, where to, which files and a SHA-256 of the exported text.');
122
+ }
123
+
124
+ main();
@@ -0,0 +1,151 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * ctxman count — token count for a target model, exact for Claude through the API
4
+ */
5
+
6
+ import fs from 'fs';
7
+ import path from 'path';
8
+ import { countForModel } from '../lib/count/index.js';
9
+ import { LLMDetector } from '../lib/utils/llm-detector.js';
10
+ import { getLogger } from '../lib/utils/logger.js';
11
+ import { exitOnBrokenPipe } from '../lib/utils/cli-output.js';
12
+
13
+ // `ctxman ... | head` closes stdout early; stop quietly instead of throwing EPIPE
14
+ exitOnBrokenPipe();
15
+
16
+ // stdout carries only the result (it may be JSON); progress, notices and warnings go to stderr
17
+ const print = (line = '') => process.stdout.write(`${line}\n`);
18
+ console.log = console.error;
19
+ getLogger().level = 'warn';
20
+
21
+ const HELP = `Usage: ctxman count [path | -] [options]
22
+
23
+ Count the tokens of a project (or of text piped to stdin with "-") for a target model,
24
+ and check them against the model's context window.
25
+
26
+ Options:
27
+ --model, -m ID Target model (default: ${LLMDetector.getDefaultModelId()}; see ctxman --list-llms)
28
+ --api Claude models: count exactly with Anthropic's count_tokens API.
29
+ Sends the (secret-redacted) text to Anthropic; needs ANTHROPIC_API_KEY
30
+ and the @anthropic-ai/sdk package. Counting is free and generates nothing.
31
+ --top N Show the N largest files (default 5, 0 to hide)
32
+ --json Print the result as JSON
33
+ -h, --help Show this help
34
+
35
+ Without --api, OpenAI models are counted exactly with their tiktoken encoding
36
+ (o200k_base or cl100k_base); other vendors' models use their own tokenizers, so
37
+ their counts are approximate.
38
+
39
+ Examples:
40
+ ctxman count --model gpt-6.1-sol
41
+ ctxman count --model claude-opus-5-5 --api
42
+ ctxman pack | ctxman count - --model claude-sonnet-5-5 --api`;
43
+
44
+ function parseArgs(argv) {
45
+ const options = { top: 5 };
46
+ for (let i = 0; i < argv.length; i++) {
47
+ const arg = argv[i];
48
+ if (arg === '--model' || arg === '-m') options.model = argv[++i];
49
+ else if (arg === '--api') options.api = true;
50
+ else if (arg === '--json') options.json = true;
51
+ else if (arg === '--top') options.top = Number.parseInt(argv[++i], 10);
52
+ else if (arg === '--help' || arg === '-h') options.help = true;
53
+ else if (arg === '-' || !arg.startsWith('-')) options.target = arg;
54
+ else throw new Error(`Unknown option: ${arg}`);
55
+ }
56
+ if (options.model === undefined && argv.includes('--model')) {
57
+ throw new Error('--model needs a model id');
58
+ }
59
+ if (!Number.isInteger(options.top) || options.top < 0) {
60
+ throw new Error('--top needs a number of files');
61
+ }
62
+ return options;
63
+ }
64
+
65
+ const n = (value) => value.toLocaleString('en-US');
66
+
67
+ function printResult(result, top) {
68
+ const label =
69
+ result.method === 'anthropic-api'
70
+ ? `exact, Anthropic count_tokens API (${result.requests} request${result.requests === 1 ? '' : 's'})`
71
+ : result.encoding === 'estimate'
72
+ ? 'estimate (install tiktoken for exact counts)'
73
+ : result.exact
74
+ ? `exact, tiktoken ${result.encoding}`
75
+ : `approximate, tiktoken ${result.encoding}; ${result.vendor} models use their own tokenizer`;
76
+
77
+ print(`Model: ${result.modelName} (${result.model})`);
78
+ print(`Files: ${n(result.files)}`);
79
+ print(`Tokens: ${n(result.tokens)} [${label}]`);
80
+ if (result.method === 'anthropic-api' && result.ratio) {
81
+ print(
82
+ ` ${n(result.localTokens)} with cl100k_base; Claude counts ${result.ratio.toFixed(2)}x as many`
83
+ );
84
+ }
85
+ print(
86
+ `Window: ${n(result.contextWindow)} tokens — ${result.percentOfWindow.toFixed(1)}% used, ${result.fits ? 'fits' : "doesn't fit"}`
87
+ );
88
+ if (result.method === 'local' && !result.exact && result.vendor === 'Anthropic') {
89
+ print(` For an exact count: ctxman count --model ${result.model} --api`);
90
+ }
91
+
92
+ const largest = result.largestFiles.slice(0, top);
93
+ if (largest.length > 1) {
94
+ const scale = result.method === 'anthropic-api' && result.ratio ? result.ratio : 1;
95
+ print(`\nLargest files${scale !== 1 ? ' (ā‰ˆ, scaled to the API count)' : ''}:`);
96
+ for (const file of largest) {
97
+ print(` ${n(Math.round(file.tokens * scale)).padStart(9)} ${file.path}`);
98
+ }
99
+ }
100
+ }
101
+
102
+ async function main() {
103
+ let options;
104
+ try {
105
+ options = parseArgs(process.argv.slice(2));
106
+ } catch (error) {
107
+ console.error(`āŒ ${error.message}\n\n${HELP}`);
108
+ process.exit(1);
109
+ }
110
+ if (options.help) {
111
+ print(HELP);
112
+ return;
113
+ }
114
+
115
+ const root = path.resolve(options.target && options.target !== '-' ? options.target : '.');
116
+ const text = options.target === '-' ? fs.readFileSync(0, 'utf8') : undefined;
117
+ if (text === undefined && !fs.statSync(root, { throwIfNoEntry: false })?.isDirectory()) {
118
+ console.error(`āŒ Not a directory: ${root}`);
119
+ process.exit(1);
120
+ }
121
+
122
+ const model = options.model || LLMDetector.getDefaultModelId();
123
+ if (options.api && LLMDetector.getProfile(model).vendor === 'Anthropic') {
124
+ console.error('Sending the redacted text to the Anthropic API to count tokens...');
125
+ }
126
+
127
+ try {
128
+ const result = await countForModel(root, {
129
+ model,
130
+ api: options.api,
131
+ text,
132
+ onProgress: (done, total) => {
133
+ if (total > 1) process.stderr.write(`\r request ${done}/${total}`);
134
+ if (total > 1 && done === total) process.stderr.write('\n');
135
+ },
136
+ });
137
+ if (options.json) {
138
+ const { largestFiles, ...summary } = result;
139
+ print(
140
+ JSON.stringify({ ...summary, largestFiles: largestFiles.slice(0, options.top) }, null, 2)
141
+ );
142
+ } else {
143
+ printResult(result, options.top);
144
+ }
145
+ } catch (error) {
146
+ console.error(`āŒ ${error.message}`);
147
+ process.exit(1);
148
+ }
149
+ }
150
+
151
+ main();
@@ -4,131 +4,144 @@
4
4
  * Generate GitIngest digest from GitHub repositories
5
5
  */
6
6
 
7
- const GitUtils = require('../lib/utils/git-utils');
8
- const { getLogger } = require('../lib/utils/logger');
7
+ import GitUtils from '../lib/utils/git-utils.js';
8
+ import { getLogger } from '../lib/utils/logger.js';
9
+ import { loadPolicy } from '../lib/policy/policy.js';
9
10
 
10
11
  const logger = getLogger({ level: 'info' });
11
12
 
12
13
  async function main() {
13
- const args = process.argv.slice(2);
14
-
15
- if (args.length === 0 || args.includes('--help') || args.includes('-h')) {
16
- printHelp();
17
- return;
18
- }
19
-
20
- const url = args[0];
21
- const verbose = args.includes('--verbose') || args.includes('-v');
22
- const keepClone = args.includes('--keep-clone');
23
- const shallow = !args.includes('--full-clone');
24
-
25
- // Parse options
26
- const outputIndex = args.findIndex(arg => arg === '--output' || arg === '-o');
27
- const outputFile = outputIndex !== -1 ? args[outputIndex + 1] : null;
28
-
29
- const branchIndex = args.findIndex(arg => arg === '--branch' || arg === '-b');
30
- const branch = branchIndex !== -1 ? args[branchIndex + 1] : 'main';
31
-
32
- const chunkSize = args.includes('--chunk-size')
33
- ? parseInt(args[args.findIndex(arg => arg === '--chunk-size') + 1])
34
- : null;
35
-
14
+ const args = process.argv.slice(2);
15
+
16
+ if (args.length === 0 || args.includes('--help') || args.includes('-h')) {
17
+ printHelp();
18
+ return;
19
+ }
20
+
21
+ const url = args[0];
22
+ const verbose = args.includes('--verbose') || args.includes('-v');
23
+ const keepClone = args.includes('--keep-clone');
24
+ const shallow = !args.includes('--full-clone');
25
+
26
+ // Parse options
27
+ const outputIndex = args.findIndex((arg) => arg === '--output' || arg === '-o');
28
+ const outputFile = outputIndex !== -1 ? args[outputIndex + 1] : null;
29
+
30
+ const branchIndex = args.findIndex((arg) => arg === '--branch' || arg === '-b');
31
+ const branch = branchIndex !== -1 ? args[branchIndex + 1] : null;
32
+
33
+ const chunkSize = args.includes('--chunk-size')
34
+ ? parseInt(args[args.findIndex((arg) => arg === '--chunk-size') + 1])
35
+ : null;
36
+
37
+ try {
38
+ console.log('╔════════════════════════════════════════════════════════╗');
39
+ console.log('ā•‘ GitHub GitIngest Generator v2.3.6 ā•‘');
40
+ console.log('ā•šā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•\n');
41
+
42
+ const gitUtils = new GitUtils({
43
+ verbose,
44
+ outputDir: 'docs',
45
+ });
46
+
47
+ // Repository info is informational: an API error (rate limit, renamed repo) must not
48
+ // stop the clone
49
+ console.log('šŸ“‹ Fetching repository information...');
50
+ const repoInfo = gitUtils.parseGitHubURL(url);
36
51
  try {
37
- console.log('╔════════════════════════════════════════════════════════╗');
38
- console.log('ā•‘ GitHub GitIngest Generator v2.3.6 ā•‘');
39
- console.log('ā•šā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•\n');
40
-
41
- const gitUtils = new GitUtils({
42
- verbose,
43
- outputDir: 'docs'
44
- });
45
-
46
- // Get repository info first
47
- console.log('šŸ“‹ Fetching repository information...');
48
- const repoInfo = gitUtils.parseGitHubURL(url);
49
- repoInfo.branch = branch; // Override branch if specified
50
-
51
- const repoData = await gitUtils.getRepositoryInfo(repoInfo);
52
- console.log(`\nšŸ“Š Repository Info:`);
53
- console.log(` Name: ${repoData.fullName}`);
54
- console.log(` Description: ${repoData.description || 'N/A'}`);
55
- console.log(` Stars: ⭐ ${repoData.stars.toLocaleString()}`);
56
- console.log(` Language: ${repoData.language || 'Multiple'}`);
57
- console.log(` Default Branch: ${repoData.defaultBranch}`);
58
-
59
- // Generate GitIngest
60
- const options = {
61
- outputFile,
62
- cleanup: !keepClone,
63
- shallow,
64
- formatterOptions: chunkSize ? {
65
- chunking: {
66
- enabled: true,
67
- maxTokensPerChunk: chunkSize
68
- }
69
- } : {}
70
- };
71
-
72
- const stats = await gitUtils.generateFromGitHub(url, options);
73
-
74
- // Log to file
75
- logger.info('GitHub GitIngest generated', {
76
- repository: stats.repository,
77
- outputFile: stats.outputFile,
78
- files: stats.files,
79
- tokens: stats.tokens
80
- });
81
-
82
- console.log('\nāœ… Complete!\n');
83
-
84
- // Show next steps
85
- console.log('šŸ“ Next steps:');
86
- console.log(` 1. View digest: cat "${stats.outputFile}"`);
87
- console.log(` 2. Use in LLM: Copy to clipboard`);
88
- console.log(` 3. Cleanup: npm run clean\n`);
89
-
52
+ const repoData = await gitUtils.getRepositoryInfo(repoInfo);
53
+ console.log(`\nšŸ“Š Repository Info:`);
54
+ console.log(` Name: ${repoData.fullName}`);
55
+ console.log(` Description: ${repoData.description || 'N/A'}`);
56
+ console.log(` Stars: ⭐ ${(repoData.stars ?? 0).toLocaleString()}`);
57
+ console.log(` Language: ${repoData.language || 'Multiple'}`);
58
+ console.log(` Default Branch: ${repoData.defaultBranch}`);
90
59
  } catch (error) {
91
- console.error('\nāŒ Error:', error.message);
92
- logger.error('GitHub GitIngest failed', { error: error.message });
93
- process.exit(1);
60
+ console.log(`āš ļø Could not fetch repository info (${error.message}); continuing`);
94
61
  }
62
+
63
+ // Generate GitIngest
64
+ const options = {
65
+ outputFile,
66
+ branch,
67
+ cleanup: !keepClone,
68
+ shallow,
69
+ // Policy of the current directory and CTXMAN_POLICY (deny rules apply to the clone)
70
+ policy: loadPolicy(process.cwd()),
71
+ formatterOptions: chunkSize
72
+ ? {
73
+ chunking: {
74
+ enabled: true,
75
+ maxTokensPerChunk: chunkSize,
76
+ },
77
+ }
78
+ : {},
79
+ };
80
+
81
+ const stats = await gitUtils.generateFromGitHub(url, options);
82
+
83
+ // Log to file
84
+ logger.info('GitHub GitIngest generated', {
85
+ repository: stats.repository,
86
+ outputFile: stats.outputFile,
87
+ files: stats.files,
88
+ tokens: stats.tokens,
89
+ });
90
+
91
+ console.log('\nāœ… Complete!\n');
92
+
93
+ // Show next steps
94
+ console.log('šŸ“ Next steps:');
95
+ console.log(` 1. View digest: cat "${stats.outputFile}"`);
96
+ console.log(` 2. Use in LLM: Copy to clipboard`);
97
+ console.log(` 3. Cleanup: npm run clean\n`);
98
+ } catch (error) {
99
+ console.error('\nāŒ Error:', error.message);
100
+ logger.error('GitHub GitIngest failed', { error: error.message });
101
+ // 2: invalid policy file, 3: export refused by the policy
102
+ process.exit(error.exitCode ?? 1);
103
+ }
95
104
  }
96
105
 
97
106
  function printHelp() {
98
- console.log('GitHub GitIngest Generator - Generate digest from GitHub repositories\n');
99
- console.log('Usage: ctxman github <url> [options]\n');
100
- console.log('Arguments:');
101
- console.log(' url GitHub repository URL or owner/repo\n');
102
- console.log('Options:');
103
- console.log(' -o, --output FILE Output file path (default: docs/{repo}-gitingest-{hash}.txt)');
104
- console.log(' -b, --branch BRANCH Branch to clone (default: main)');
105
- console.log(' -v, --verbose Verbose output');
106
- console.log(' --keep-clone Keep cloned repository (don\'t cleanup)');
107
- console.log(' --full-clone Full clone (default: shallow)');
108
- console.log(' --chunk-size TOKENS Enable chunking with max tokens per chunk');
109
- console.log(' -h, --help Show this help\n');
110
- console.log('URL Formats:');
111
- console.log(' https://github.com/owner/repo');
112
- console.log(' https://github.com/owner/repo.git');
113
- console.log(' git@github.com:owner/repo.git');
114
- console.log(' github.com/owner/repo');
115
- console.log(' owner/repo\n');
116
- console.log('Examples:');
117
- console.log(' # Basic usage');
118
- console.log(' ctxman github https://github.com/facebook/react\n');
119
- console.log(' # Short format');
120
- console.log(' ctxman github facebook/react\n');
121
- console.log(' # Specific branch');
122
- console.log(' ctxman github vercel/next.js --branch canary\n');
123
- console.log(' # Custom output location');
124
- console.log(' ctxman github angular/angular -o docs/angular-digest.txt\n');
125
- console.log(' # With chunking');
126
- console.log(' ctxman github microsoft/vscode --chunk-size 100000\n');
127
- console.log(' # Keep clone for inspection');
128
- console.log(' ctxman github nodejs/node --keep-clone --verbose\n');
129
- console.log('Output:');
130
- console.log(' Generated digest file in docs/ directory');
131
- console.log(' Automatic cleanup of temporary clone (unless --keep-clone)\n');
107
+ console.log('GitHub GitIngest Generator - Generate digest from GitHub repositories\n');
108
+ console.log('Usage: ctxman github <url> [options]\n');
109
+ console.log('Arguments:');
110
+ console.log(' url GitHub repository URL or owner/repo\n');
111
+ console.log('Options:');
112
+ console.log(
113
+ ' -o, --output FILE Output file path (default: docs/{repo}-gitingest-{hash}.txt)'
114
+ );
115
+ console.log(
116
+ " -b, --branch BRANCH Branch to clone (default: the repository's default branch)"
117
+ );
118
+ console.log(' -v, --verbose Verbose output');
119
+ console.log(" --keep-clone Keep cloned repository (don't cleanup)");
120
+ console.log(' --full-clone Full clone (default: shallow)');
121
+ console.log(' --chunk-size TOKENS Enable chunking with max tokens per chunk');
122
+ console.log(' -h, --help Show this help\n');
123
+ console.log('URL Formats:');
124
+ console.log(' https://github.com/owner/repo');
125
+ console.log(' https://github.com/owner/repo.git');
126
+ console.log(' git@github.com:owner/repo.git');
127
+ console.log(' github.com/owner/repo');
128
+ console.log(' owner/repo\n');
129
+ console.log('Examples:');
130
+ console.log(' # Basic usage');
131
+ console.log(' ctxman github https://github.com/facebook/react\n');
132
+ console.log(' # Short format');
133
+ console.log(' ctxman github facebook/react\n');
134
+ console.log(' # Specific branch');
135
+ console.log(' ctxman github vercel/next.js --branch canary\n');
136
+ console.log(' # Custom output location');
137
+ console.log(' ctxman github angular/angular -o docs/angular-digest.txt\n');
138
+ console.log(' # With chunking');
139
+ console.log(' ctxman github microsoft/vscode --chunk-size 100000\n');
140
+ console.log(' # Keep clone for inspection');
141
+ console.log(' ctxman github nodejs/node --keep-clone --verbose\n');
142
+ console.log('Output:');
143
+ console.log(' Generated digest file in docs/ directory');
144
+ console.log(' Automatic cleanup of temporary clone (unless --keep-clone)\n');
132
145
  }
133
146
 
134
147
  main();