ctxman 3.0.0 → 3.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (224) 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 +381 -136
  16. package/bin/cli.js +1880 -529
  17. package/bin/cm-ask.js +18 -67
  18. package/bin/cm-audit.js +121 -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 +687 -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 +772 -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 +116 -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 +312 -0
  93. package/lib/policy/enforce.js +125 -0
  94. package/lib/policy/ignore-files.js +169 -0
  95. package/lib/policy/index.js +53 -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/ask.js +122 -0
  108. package/lib/rag/sources/FileDataSource.js +12 -9
  109. package/lib/rag/stores/BaseVectorStore.js +5 -5
  110. package/lib/rag/stores/ChromaDBStore.js +10 -10
  111. package/lib/rag/stores/LanceDBStore.js +29 -19
  112. package/lib/rag/stores/PineconeStore.js +11 -9
  113. package/lib/search/bm25.js +433 -0
  114. package/lib/search/code-units.js +201 -0
  115. package/lib/search/symbols.js +189 -0
  116. package/lib/ui/dashboard.js +120 -89
  117. package/lib/ui/index.js +1 -6
  118. package/lib/ui/init-wizard-ui.js +550 -0
  119. package/lib/ui/progress-bar.js +94 -88
  120. package/lib/ui/select-input.js +40 -32
  121. package/lib/ui/template-selector.js +99 -0
  122. package/lib/ui/wizard.js +374 -329
  123. package/lib/utils/claude-token-counter.js +151 -0
  124. package/lib/utils/cli-output.js +17 -0
  125. package/lib/utils/clipboard-utils.js +49 -50
  126. package/lib/utils/config-utils.js +81 -66
  127. package/lib/utils/context-versioning.js +532 -0
  128. package/lib/utils/error-handler.js +112 -112
  129. package/lib/utils/file-utils.js +153 -69
  130. package/lib/utils/format-converter.js +356 -283
  131. package/lib/utils/git-utils.js +366 -294
  132. package/lib/utils/llm-cost-estimator.js +281 -0
  133. package/lib/utils/llm-detector.js +242 -78
  134. package/lib/utils/logger.js +246 -237
  135. package/lib/utils/messages.js +355 -0
  136. package/lib/utils/multi-repo-manager.js +364 -0
  137. package/lib/utils/profile-manager.js +380 -0
  138. package/lib/utils/safe-read.js +61 -0
  139. package/lib/utils/secret-redactor.js +61 -0
  140. package/lib/utils/snapshot-manager.js +679 -0
  141. package/lib/utils/template-manager.js +378 -0
  142. package/lib/utils/token-utils.js +201 -170
  143. package/lib/utils/tokenizer-adapter.js +421 -377
  144. package/lib/utils/updater.js +539 -506
  145. package/lib/watch/FileWatcher.js +12 -8
  146. package/lib/watch/IncrementalAnalyzer.js +7 -7
  147. package/lib/why/coupling.js +81 -0
  148. package/lib/why/errors.js +23 -0
  149. package/lib/why/git.js +169 -0
  150. package/lib/why/history.js +108 -0
  151. package/lib/why/index.js +720 -0
  152. package/lib/why/ownership.js +79 -0
  153. package/lib/why/paths.js +207 -0
  154. package/lib/why/references.js +259 -0
  155. package/lib/why/render.js +722 -0
  156. package/lib/why/search.js +358 -0
  157. package/lib/why/target.js +389 -0
  158. package/lib/wizards/config-generator.js +257 -0
  159. package/lib/wizards/project-detector.js +572 -0
  160. package/package.json +44 -10
  161. package/.ctxman/logs/ctxman-2026-07-02.log +0 -156
  162. package/.ctxman/self-knowledge/documents.lance/_transactions/0-b3dd1558-f95b-44b9-a154-18e8c330ae7a.txn +0 -0
  163. package/.ctxman/self-knowledge/documents.lance/_transactions/1-44bff6d4-e552-4010-a769-e09ad822698d.txn +0 -0
  164. package/.ctxman/self-knowledge/documents.lance/_transactions/10-37260794-5947-4ecb-b4c8-d732f58f40f1.txn +0 -0
  165. package/.ctxman/self-knowledge/documents.lance/_transactions/11-0d52a883-5518-463c-9b6d-986c3af89dfd.txn +0 -0
  166. package/.ctxman/self-knowledge/documents.lance/_transactions/12-ae354646-52f2-49c9-90fb-2e6ed8932663.txn +0 -0
  167. package/.ctxman/self-knowledge/documents.lance/_transactions/13-5d7d5e57-27da-498b-b734-091b9d38da13.txn +0 -0
  168. package/.ctxman/self-knowledge/documents.lance/_transactions/14-558ce928-810b-4252-a364-fdd1ee6f8a25.txn +0 -0
  169. package/.ctxman/self-knowledge/documents.lance/_transactions/15-27e3a58f-8b64-43b8-a9f4-e3369446968b.txn +0 -0
  170. package/.ctxman/self-knowledge/documents.lance/_transactions/16-a110c390-e5c8-4464-b976-bc7f5195fa15.txn +0 -0
  171. package/.ctxman/self-knowledge/documents.lance/_transactions/17-97513225-53de-4216-b61a-6337bd0cc4ad.txn +0 -0
  172. package/.ctxman/self-knowledge/documents.lance/_transactions/18-5ba450f0-c527-459c-bf4f-6d79b5e4285f.txn +0 -0
  173. package/.ctxman/self-knowledge/documents.lance/_transactions/19-600d3a63-5225-49e9-b8b7-30e037b564ab.txn +0 -0
  174. package/.ctxman/self-knowledge/documents.lance/_transactions/2-48a61b4e-92cf-4342-8376-914c1b99a9f9.txn +0 -0
  175. package/.ctxman/self-knowledge/documents.lance/_transactions/20-121dd912-5410-410e-84ff-82c7a23604a0.txn +0 -0
  176. package/.ctxman/self-knowledge/documents.lance/_transactions/3-bc6844ab-7ea3-4bb3-9ee1-538ce5664a9c.txn +0 -0
  177. package/.ctxman/self-knowledge/documents.lance/_transactions/4-d5e1754f-dbb5-454e-a9be-2a7da8d7bcf9.txn +0 -0
  178. package/.ctxman/self-knowledge/documents.lance/_transactions/5-4fae93df-0568-4b6f-8acf-8bef4bba6047.txn +0 -0
  179. package/.ctxman/self-knowledge/documents.lance/_transactions/6-4b1b8b87-068e-49a3-ba2b-d8dae628b252.txn +0 -0
  180. package/.ctxman/self-knowledge/documents.lance/_transactions/7-63d040fb-d2fa-4c0f-bed0-53411918a7b0.txn +0 -0
  181. package/.ctxman/self-knowledge/documents.lance/_transactions/8-c5628cf6-264c-420f-aab8-bc96a9083591.txn +0 -0
  182. package/.ctxman/self-knowledge/documents.lance/_transactions/9-c94570aa-5e53-4fcb-971f-cecc1d1d5492.txn +0 -0
  183. package/.ctxman/self-knowledge/documents.lance/_versions/1.manifest +0 -0
  184. package/.ctxman/self-knowledge/documents.lance/_versions/10.manifest +0 -0
  185. package/.ctxman/self-knowledge/documents.lance/_versions/11.manifest +0 -0
  186. package/.ctxman/self-knowledge/documents.lance/_versions/12.manifest +0 -0
  187. package/.ctxman/self-knowledge/documents.lance/_versions/13.manifest +0 -0
  188. package/.ctxman/self-knowledge/documents.lance/_versions/14.manifest +0 -0
  189. package/.ctxman/self-knowledge/documents.lance/_versions/15.manifest +0 -0
  190. package/.ctxman/self-knowledge/documents.lance/_versions/16.manifest +0 -0
  191. package/.ctxman/self-knowledge/documents.lance/_versions/17.manifest +0 -0
  192. package/.ctxman/self-knowledge/documents.lance/_versions/18.manifest +0 -0
  193. package/.ctxman/self-knowledge/documents.lance/_versions/19.manifest +0 -0
  194. package/.ctxman/self-knowledge/documents.lance/_versions/2.manifest +0 -0
  195. package/.ctxman/self-knowledge/documents.lance/_versions/20.manifest +0 -0
  196. package/.ctxman/self-knowledge/documents.lance/_versions/21.manifest +0 -0
  197. package/.ctxman/self-knowledge/documents.lance/_versions/3.manifest +0 -0
  198. package/.ctxman/self-knowledge/documents.lance/_versions/4.manifest +0 -0
  199. package/.ctxman/self-knowledge/documents.lance/_versions/5.manifest +0 -0
  200. package/.ctxman/self-knowledge/documents.lance/_versions/6.manifest +0 -0
  201. package/.ctxman/self-knowledge/documents.lance/_versions/7.manifest +0 -0
  202. package/.ctxman/self-knowledge/documents.lance/_versions/8.manifest +0 -0
  203. package/.ctxman/self-knowledge/documents.lance/_versions/9.manifest +0 -0
  204. package/.ctxman/self-knowledge/documents.lance/data/000010010011011101000010de8621405d9d5cb06540dbf02c.lance +0 -0
  205. package/.ctxman/self-knowledge/documents.lance/data/0000101001011100011000003fb62a4aa8978974075c144f1c.lance +0 -0
  206. package/.ctxman/self-knowledge/documents.lance/data/000011010000100111001011c6cc824d628f3bb1f4e04cd5d0.lance +0 -0
  207. package/.ctxman/self-knowledge/documents.lance/data/001000001000011001011100d8f74349ada090b332cd1df669.lance +0 -0
  208. package/.ctxman/self-knowledge/documents.lance/data/01000001111010001000010045ab44467bb1e7e4d8ae06bd58.lance +0 -0
  209. package/.ctxman/self-knowledge/documents.lance/data/01000101101110111110011131ef904241a89e1ffb6ddaf8e3.lance +0 -0
  210. package/.ctxman/self-knowledge/documents.lance/data/010010110000010000011010fbfcc145c6a8a827b28057a07e.lance +0 -0
  211. package/.ctxman/self-knowledge/documents.lance/data/1000011000101010001110003fb4f045c3835982efa1405324.lance +0 -0
  212. package/.ctxman/self-knowledge/documents.lance/data/100111011101110111101011f4396b410fa06af3b028d204e6.lance +0 -0
  213. package/.ctxman/self-knowledge/documents.lance/data/100111100111101000001100596b174e00995d6cc2969cc8dc.lance +0 -0
  214. package/.ctxman/self-knowledge/documents.lance/data/1010001100011000110010102ea16b484d887c7c9725feffd6.lance +0 -0
  215. package/.ctxman/self-knowledge/documents.lance/data/101101100010101111110111c5230944d9b3af7301b6392dda.lance +0 -0
  216. package/.ctxman/self-knowledge/documents.lance/data/1011011100100111111110105892bf41b5a530fedd7cf516f1.lance +0 -0
  217. package/.ctxman/self-knowledge/documents.lance/data/10111110000010001011110164e8774742bade87b483c10f36.lance +0 -0
  218. package/.ctxman/self-knowledge/documents.lance/data/1100000100010110001110103ffaca40a2a42c85067ebaf9ae.lance +0 -0
  219. package/.ctxman/self-knowledge/documents.lance/data/110000010001110110101110664bde4c76a78dfc14b05e83b8.lance +0 -0
  220. package/.ctxman/self-knowledge/documents.lance/data/11001100111000010100110050645c4281b1f35b2022234f7f.lance +0 -0
  221. package/.ctxman/self-knowledge/documents.lance/data/1101111011001100110111017214924a368f5719eab2246619.lance +0 -0
  222. package/.ctxman/self-knowledge/documents.lance/data/1110000111001010001000002150c24dcab653d39d07032e63.lance +0 -0
  223. package/.ctxman/self-knowledge/documents.lance/data/1110100000000100000011014d20534967a18b5c87721659ff.lance +0 -0
  224. package/.ctxman/self-knowledge/documents.lance/data/11110101111010000101011135a0114ef2b35dc6f1297a3381.lance +0 -0
@@ -0,0 +1,151 @@
1
+ /**
2
+ * Claude Token Counter
3
+ * Exact Claude token counts from Anthropic's count_tokens endpoint.
4
+ *
5
+ * tiktoken's encodings are OpenAI's. Claude models use their own tokenizer, which
6
+ * produces more tokens for the same text (especially for code), and Anthropic does not
7
+ * publish an offline tokenizer for current models: the API is the only exact source.
8
+ * Counting generates nothing and is free of charge, but it sends the text to Anthropic,
9
+ * so callers must only use it when the user asked for it.
10
+ */
11
+
12
+ import TokenUtils from './token-utils.js';
13
+
14
+ // Texts are grouped into requests of about this many cl100k_base tokens. Claude counts
15
+ // a little more than that, which still leaves room in the smallest (200K) window.
16
+ const DEFAULT_BATCH_TOKENS = 120000;
17
+
18
+ /**
19
+ * Split a text that is larger than one request into line-aligned pieces
20
+ * @returns {{text: string, tokens: number}[]}
21
+ */
22
+ function splitByLines(text, filePath, maxTokens) {
23
+ const tokens = TokenUtils.calculate(text, filePath);
24
+ if (tokens <= maxTokens) return [{ text, tokens }];
25
+
26
+ const pieces = [];
27
+ let lines = [];
28
+ let pieceTokens = 0;
29
+ for (const line of text.split('\n')) {
30
+ const lineTokens = TokenUtils.calculate(line + '\n', filePath);
31
+ if (lines.length > 0 && pieceTokens + lineTokens > maxTokens) {
32
+ pieces.push({ text: lines.join('\n'), tokens: pieceTokens });
33
+ lines = [];
34
+ pieceTokens = 0;
35
+ }
36
+ lines.push(line);
37
+ pieceTokens += lineTokens;
38
+ }
39
+ if (lines.length > 0) pieces.push({ text: lines.join('\n'), tokens: pieceTokens });
40
+ return pieces;
41
+ }
42
+
43
+ export class ClaudeTokenCounter {
44
+ /**
45
+ * @param {object} options
46
+ * @param {string} options.model - Claude model id, e.g. `claude-opus-5-5`
47
+ * @param {object} options.client - Anthropic SDK client (or a stand-in with
48
+ * `messages.countTokens`)
49
+ */
50
+ constructor({ model, client }) {
51
+ if (!model) throw new Error('A Claude model id is required');
52
+ this.model = model;
53
+ this.client = client;
54
+ this.requests = 0;
55
+ this.framing = null;
56
+ }
57
+
58
+ /**
59
+ * Create a counter with the official SDK, which reads ANTHROPIC_API_KEY (or another
60
+ * configured credential) from the environment
61
+ * @param {object} options
62
+ * @param {string} options.model - Claude model id
63
+ * @param {object} [options.client] - Pre-built client (tests)
64
+ * @returns {Promise<ClaudeTokenCounter>}
65
+ */
66
+ static async create({ model, client } = {}) {
67
+ if (!client) {
68
+ let Anthropic;
69
+ try {
70
+ ({ default: Anthropic } = await import('@anthropic-ai/sdk'));
71
+ } catch (_error) {
72
+ throw new Error(
73
+ 'Counting with the Anthropic API needs the official SDK: npm install @anthropic-ai/sdk'
74
+ );
75
+ }
76
+ client = new Anthropic();
77
+ }
78
+ return new ClaudeTokenCounter({ model, client });
79
+ }
80
+
81
+ /**
82
+ * Count one text as a single user message
83
+ * @param {string} text - Non-empty text
84
+ * @returns {Promise<number>} input_tokens reported by the API
85
+ */
86
+ async countMessage(text) {
87
+ this.requests++;
88
+ const response = await this.client.messages.countTokens({
89
+ model: this.model,
90
+ messages: [{ role: 'user', content: text }],
91
+ });
92
+ return response.input_tokens;
93
+ }
94
+
95
+ /**
96
+ * Tokens the API adds for the message framing, measured once with a one-token text
97
+ * @returns {Promise<number>}
98
+ */
99
+ async getFraming() {
100
+ this.framing ??= Math.max(0, (await this.countMessage('.')) - 1);
101
+ return this.framing;
102
+ }
103
+
104
+ /**
105
+ * Count a text, minus the message framing
106
+ * @param {string} text
107
+ * @returns {Promise<number>}
108
+ */
109
+ async countText(text) {
110
+ if (!text) return 0;
111
+ const framing = await this.getFraming();
112
+ return Math.max(0, (await this.countMessage(text)) - framing);
113
+ }
114
+
115
+ /**
116
+ * Count many documents in as few requests as the batch size allows
117
+ * @param {{path: string, content: string}[]} documents
118
+ * @param {object} [options]
119
+ * @param {number} [options.batchTokens] - cl100k_base tokens per request
120
+ * @param {(done: number, total: number) => void} [options.onProgress]
121
+ * @returns {Promise<{total: number, requests: number, batches: number}>}
122
+ */
123
+ async countDocuments(documents, { batchTokens = DEFAULT_BATCH_TOKENS, onProgress } = {}) {
124
+ const batches = [];
125
+ let current = [];
126
+ let currentTokens = 0;
127
+ for (const doc of documents) {
128
+ if (!doc.content) continue;
129
+ for (const piece of splitByLines(doc.content, doc.path, batchTokens)) {
130
+ if (current.length > 0 && currentTokens + piece.tokens > batchTokens) {
131
+ batches.push(current);
132
+ current = [];
133
+ currentTokens = 0;
134
+ }
135
+ current.push(piece.text);
136
+ currentTokens += piece.tokens;
137
+ }
138
+ }
139
+ if (current.length > 0) batches.push(current);
140
+
141
+ const before = this.requests;
142
+ let total = 0;
143
+ for (let i = 0; i < batches.length; i++) {
144
+ total += await this.countText(batches[i].join('\n'));
145
+ onProgress?.(i + 1, batches.length);
146
+ }
147
+ return { total, requests: this.requests - before, batches: batches.length };
148
+ }
149
+ }
150
+
151
+ export default ClaudeTokenCounter;
@@ -0,0 +1,17 @@
1
+ /**
2
+ * Command-line output helpers
3
+ */
4
+
5
+ /**
6
+ * End the process quietly when the reader of stdout goes away (`ctxman pr | head`): the
7
+ * rest of the output has nowhere to go, and Node's default is an unhandled EPIPE error.
8
+ * @param {import('stream').Writable} [stream=process.stdout]
9
+ */
10
+ export function exitOnBrokenPipe(stream = process.stdout) {
11
+ stream.on('error', (error) => {
12
+ if (error.code === 'EPIPE') process.exit(0);
13
+ throw error;
14
+ });
15
+ }
16
+
17
+ export default exitOnBrokenPipe;
@@ -6,61 +6,60 @@
6
6
  import { execSync } from 'child_process';
7
7
 
8
8
  class ClipboardUtils {
9
- /**
10
- * Copy text to system clipboard
11
- * @param {string} text - Text to copy
12
- * @returns {boolean} Success status
13
- */
14
- static copy(text) {
9
+ /**
10
+ * Copy text to system clipboard
11
+ * @param {string} text - Text to copy
12
+ * @returns {boolean} Success status
13
+ */
14
+ static copy(text) {
15
+ try {
16
+ if (process.platform === 'darwin') {
17
+ // macOS
18
+ execSync('pbcopy', { input: text, encoding: 'utf8' });
19
+ } else if (process.platform === 'linux') {
20
+ // Linux - try xclip first, then xsel
15
21
  try {
16
- if (process.platform === 'darwin') {
17
- // macOS
18
- execSync('pbcopy', { input: text, encoding: 'utf8' });
19
- } else if (process.platform === 'linux') {
20
- // Linux - try xclip first, then xsel
21
- try {
22
- execSync('xclip -selection clipboard', { input: text, encoding: 'utf8' });
23
- } catch {
24
- execSync('xsel --clipboard --input', { input: text, encoding: 'utf8' });
25
- }
26
- } else if (process.platform === 'win32') {
27
- // Windows
28
- execSync('clip', { input: text, encoding: 'utf8' });
29
- } else {
30
- throw new Error('Unsupported platform for clipboard');
31
- }
32
-
33
- console.log('✅ Context copied to clipboard!');
34
- console.log(`📋 ${text.length} characters copied`);
35
- return true;
36
-
37
- } catch (error) {
38
- console.error('❌ Failed to copy to clipboard:', error.message);
39
- return false;
22
+ execSync('xclip -selection clipboard', { input: text, encoding: 'utf8' });
23
+ } catch {
24
+ execSync('xsel --clipboard --input', { input: text, encoding: 'utf8' });
40
25
  }
41
- }
26
+ } else if (process.platform === 'win32') {
27
+ // Windows
28
+ execSync('clip', { input: text, encoding: 'utf8' });
29
+ } else {
30
+ throw new Error('Unsupported platform for clipboard');
31
+ }
42
32
 
43
- /**
44
- * Check if clipboard is available on current platform
45
- * @returns {boolean}
46
- */
47
- static isAvailable() {
48
- const platform = process.platform;
49
- return ['darwin', 'linux', 'win32'].includes(platform);
33
+ console.log('✅ Context copied to clipboard!');
34
+ console.log(`📋 ${text.length} characters copied`);
35
+ return true;
36
+ } catch (error) {
37
+ console.error('❌ Failed to copy to clipboard:', error.message);
38
+ return false;
50
39
  }
40
+ }
51
41
 
52
- /**
53
- * Get clipboard command for current platform
54
- * @returns {string} Command name
55
- */
56
- static getCommand() {
57
- const commands = {
58
- darwin: 'pbcopy',
59
- linux: 'xclip',
60
- win32: 'clip'
61
- };
62
- return commands[process.platform] || 'unknown';
63
- }
42
+ /**
43
+ * Check if clipboard is available on current platform
44
+ * @returns {boolean}
45
+ */
46
+ static isAvailable() {
47
+ const platform = process.platform;
48
+ return ['darwin', 'linux', 'win32'].includes(platform);
49
+ }
50
+
51
+ /**
52
+ * Get clipboard command for current platform
53
+ * @returns {string} Command name
54
+ */
55
+ static getCommand() {
56
+ const commands = {
57
+ darwin: 'pbcopy',
58
+ linux: 'xclip',
59
+ win32: 'clip',
60
+ };
61
+ return commands[process.platform] || 'unknown';
62
+ }
64
63
  }
65
64
 
66
65
  export default ClipboardUtils;
@@ -14,81 +14,96 @@ const __filename = fileURLToPath(import.meta.url);
14
14
  const __dirname = dirname(__filename);
15
15
 
16
16
  class ConfigUtils {
17
- /**
18
- * Find configuration file in standard locations
19
- * @param {string} projectRoot - Project root directory
20
- * @param {string} filename - Config filename
21
- * @returns {string|undefined} Config file path or undefined
22
- */
23
- static findConfigFile(projectRoot, filename) {
24
- const locations = [
25
- path.join(__dirname, '..', '..', filename), // Package root
26
- path.join(projectRoot, filename) // Project root
27
- ];
28
- return locations.find(loc => fs.existsSync(loc));
17
+ /**
18
+ * Find configuration file in standard locations
19
+ * @param {string} projectRoot - Project root directory
20
+ * @param {string} filename - Config filename
21
+ * @returns {string|undefined} Config file path or undefined
22
+ */
23
+ static findConfigFile(projectRoot, filename) {
24
+ const locations = [
25
+ path.join(__dirname, '..', '..', filename), // Package root
26
+ path.join(projectRoot, filename), // Project root
27
+ ];
28
+ return locations.find((loc) => fs.existsSync(loc));
29
+ }
30
+
31
+ /**
32
+ * Initialize method filter parser
33
+ * @param {string} projectRoot - Project root directory
34
+ * @returns {object|null} MethodFilterParser instance or null
35
+ */
36
+ static initMethodFilter(projectRoot) {
37
+ const paths = {
38
+ methodInclude: this.findConfigFile(projectRoot, '.methodinclude'),
39
+ methodIgnore: this.findConfigFile(projectRoot, '.methodignore'),
40
+ };
41
+
42
+ if (!paths.methodInclude && !paths.methodIgnore) {
43
+ return null;
29
44
  }
30
45
 
31
- /**
32
- * Initialize method filter parser
33
- * @param {string} projectRoot - Project root directory
34
- * @returns {object|null} MethodFilterParser instance or null
35
- */
36
- static initMethodFilter(projectRoot) {
37
- const paths = {
38
- methodInclude: this.findConfigFile(projectRoot, '.methodinclude'),
39
- methodIgnore: this.findConfigFile(projectRoot, '.methodignore')
40
- };
46
+ return new MethodFilterParser(paths.methodInclude, paths.methodIgnore);
47
+ }
41
48
 
42
- if (!paths.methodInclude && !paths.methodIgnore) {
43
- return null;
44
- }
49
+ /**
50
+ * Detect if method filtering is enabled
51
+ * @param {string} projectRoot - Project root directory
52
+ * @returns {boolean}
53
+ */
54
+ static detectMethodFilters(projectRoot) {
55
+ const methodIncludePath = this.findConfigFile(projectRoot, '.methodinclude');
56
+ const methodIgnorePath = this.findConfigFile(projectRoot, '.methodignore');
57
+ return !!(methodIncludePath || methodIgnorePath);
58
+ }
45
59
 
46
- return new MethodFilterParser(paths.methodInclude, paths.methodIgnore);
47
- }
60
+ /**
61
+ * Initialize git ignore parser
62
+ * @param {string} projectRoot - Project root directory
63
+ * @returns {object} GitIgnoreParser instance
64
+ */
65
+ static initGitIgnore(projectRoot) {
66
+ const paths = {
67
+ contextIgnore: this.findConfigFile(projectRoot, '.contextignore'),
68
+ contextInclude: this.findConfigFile(projectRoot, '.contextinclude'),
69
+ };
48
70
 
49
- /**
50
- * Detect if method filtering is enabled
51
- * @param {string} projectRoot - Project root directory
52
- * @returns {boolean}
53
- */
54
- static detectMethodFilters(projectRoot) {
55
- const methodIncludePath = this.findConfigFile(projectRoot, '.methodinclude');
56
- const methodIgnorePath = this.findConfigFile(projectRoot, '.methodignore');
57
- return !!(methodIncludePath || methodIgnorePath);
58
- }
71
+ return new GitIgnoreParser(
72
+ path.join(projectRoot, '.gitignore'),
73
+ paths.contextIgnore,
74
+ paths.contextInclude
75
+ );
76
+ }
59
77
 
60
- /**
61
- * Initialize git ignore parser
62
- * @param {string} projectRoot - Project root directory
63
- * @returns {object} GitIgnoreParser instance
64
- */
65
- static initGitIgnore(projectRoot) {
66
- const paths = {
67
- contextIgnore: this.findConfigFile(projectRoot, '.contextignore'),
68
- contextInclude: this.findConfigFile(projectRoot, '.contextinclude')
69
- };
78
+ /**
79
+ * Get all config file paths
80
+ * @param {string} projectRoot - Project root directory
81
+ * @returns {object} Config paths
82
+ */
83
+ /**
84
+ * Load the user configuration (~/.ctxman/config.json, see docs/LOGGING-AND-UPDATES.md)
85
+ * @returns {object|null} Parsed config, or null when missing or invalid
86
+ */
87
+ static loadUserConfig() {
88
+ const home = process.env.HOME || process.env.USERPROFILE;
89
+ if (!home) return null;
70
90
 
71
- return new GitIgnoreParser(
72
- path.join(projectRoot, '.gitignore'),
73
- paths.contextIgnore,
74
- paths.contextInclude
75
- );
91
+ try {
92
+ return JSON.parse(fs.readFileSync(path.join(home, '.ctxman', 'config.json'), 'utf8'));
93
+ } catch (_error) {
94
+ return null;
76
95
  }
96
+ }
77
97
 
78
- /**
79
- * Get all config file paths
80
- * @param {string} projectRoot - Project root directory
81
- * @returns {object} Config paths
82
- */
83
- static getConfigPaths(projectRoot) {
84
- return {
85
- gitignore: path.join(projectRoot, '.gitignore'),
86
- contextIgnore: this.findConfigFile(projectRoot, '.contextignore'),
87
- contextInclude: this.findConfigFile(projectRoot, '.contextinclude'),
88
- methodInclude: this.findConfigFile(projectRoot, '.methodinclude'),
89
- methodIgnore: this.findConfigFile(projectRoot, '.methodignore')
90
- };
91
- }
98
+ static getConfigPaths(projectRoot) {
99
+ return {
100
+ gitignore: path.join(projectRoot, '.gitignore'),
101
+ contextIgnore: this.findConfigFile(projectRoot, '.contextignore'),
102
+ contextInclude: this.findConfigFile(projectRoot, '.contextinclude'),
103
+ methodInclude: this.findConfigFile(projectRoot, '.methodinclude'),
104
+ methodIgnore: this.findConfigFile(projectRoot, '.methodignore'),
105
+ };
106
+ }
92
107
  }
93
108
 
94
109
  export default ConfigUtils;