ctxman 3.0.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 (190) hide show
  1. package/.ctxman/custom-profiles.example.json +20 -0
  2. package/.ctxman/llm-profiles.json +131 -0
  3. package/.ctxman/logs/ctxman-2026-07-02.log +156 -0
  4. package/.ctxman/self-knowledge/documents.lance/_transactions/0-b3dd1558-f95b-44b9-a154-18e8c330ae7a.txn +0 -0
  5. package/.ctxman/self-knowledge/documents.lance/_transactions/1-44bff6d4-e552-4010-a769-e09ad822698d.txn +0 -0
  6. package/.ctxman/self-knowledge/documents.lance/_transactions/10-37260794-5947-4ecb-b4c8-d732f58f40f1.txn +0 -0
  7. package/.ctxman/self-knowledge/documents.lance/_transactions/11-0d52a883-5518-463c-9b6d-986c3af89dfd.txn +0 -0
  8. package/.ctxman/self-knowledge/documents.lance/_transactions/12-ae354646-52f2-49c9-90fb-2e6ed8932663.txn +0 -0
  9. package/.ctxman/self-knowledge/documents.lance/_transactions/13-5d7d5e57-27da-498b-b734-091b9d38da13.txn +0 -0
  10. package/.ctxman/self-knowledge/documents.lance/_transactions/14-558ce928-810b-4252-a364-fdd1ee6f8a25.txn +0 -0
  11. package/.ctxman/self-knowledge/documents.lance/_transactions/15-27e3a58f-8b64-43b8-a9f4-e3369446968b.txn +0 -0
  12. package/.ctxman/self-knowledge/documents.lance/_transactions/16-a110c390-e5c8-4464-b976-bc7f5195fa15.txn +0 -0
  13. package/.ctxman/self-knowledge/documents.lance/_transactions/17-97513225-53de-4216-b61a-6337bd0cc4ad.txn +0 -0
  14. package/.ctxman/self-knowledge/documents.lance/_transactions/18-5ba450f0-c527-459c-bf4f-6d79b5e4285f.txn +0 -0
  15. package/.ctxman/self-knowledge/documents.lance/_transactions/19-600d3a63-5225-49e9-b8b7-30e037b564ab.txn +0 -0
  16. package/.ctxman/self-knowledge/documents.lance/_transactions/2-48a61b4e-92cf-4342-8376-914c1b99a9f9.txn +0 -0
  17. package/.ctxman/self-knowledge/documents.lance/_transactions/20-121dd912-5410-410e-84ff-82c7a23604a0.txn +0 -0
  18. package/.ctxman/self-knowledge/documents.lance/_transactions/3-bc6844ab-7ea3-4bb3-9ee1-538ce5664a9c.txn +0 -0
  19. package/.ctxman/self-knowledge/documents.lance/_transactions/4-d5e1754f-dbb5-454e-a9be-2a7da8d7bcf9.txn +0 -0
  20. package/.ctxman/self-knowledge/documents.lance/_transactions/5-4fae93df-0568-4b6f-8acf-8bef4bba6047.txn +0 -0
  21. package/.ctxman/self-knowledge/documents.lance/_transactions/6-4b1b8b87-068e-49a3-ba2b-d8dae628b252.txn +0 -0
  22. package/.ctxman/self-knowledge/documents.lance/_transactions/7-63d040fb-d2fa-4c0f-bed0-53411918a7b0.txn +0 -0
  23. package/.ctxman/self-knowledge/documents.lance/_transactions/8-c5628cf6-264c-420f-aab8-bc96a9083591.txn +0 -0
  24. package/.ctxman/self-knowledge/documents.lance/_transactions/9-c94570aa-5e53-4fcb-971f-cecc1d1d5492.txn +0 -0
  25. package/.ctxman/self-knowledge/documents.lance/_versions/1.manifest +0 -0
  26. package/.ctxman/self-knowledge/documents.lance/_versions/10.manifest +0 -0
  27. package/.ctxman/self-knowledge/documents.lance/_versions/11.manifest +0 -0
  28. package/.ctxman/self-knowledge/documents.lance/_versions/12.manifest +0 -0
  29. package/.ctxman/self-knowledge/documents.lance/_versions/13.manifest +0 -0
  30. package/.ctxman/self-knowledge/documents.lance/_versions/14.manifest +0 -0
  31. package/.ctxman/self-knowledge/documents.lance/_versions/15.manifest +0 -0
  32. package/.ctxman/self-knowledge/documents.lance/_versions/16.manifest +0 -0
  33. package/.ctxman/self-knowledge/documents.lance/_versions/17.manifest +0 -0
  34. package/.ctxman/self-knowledge/documents.lance/_versions/18.manifest +0 -0
  35. package/.ctxman/self-knowledge/documents.lance/_versions/19.manifest +0 -0
  36. package/.ctxman/self-knowledge/documents.lance/_versions/2.manifest +0 -0
  37. package/.ctxman/self-knowledge/documents.lance/_versions/20.manifest +0 -0
  38. package/.ctxman/self-knowledge/documents.lance/_versions/21.manifest +0 -0
  39. package/.ctxman/self-knowledge/documents.lance/_versions/3.manifest +0 -0
  40. package/.ctxman/self-knowledge/documents.lance/_versions/4.manifest +0 -0
  41. package/.ctxman/self-knowledge/documents.lance/_versions/5.manifest +0 -0
  42. package/.ctxman/self-knowledge/documents.lance/_versions/6.manifest +0 -0
  43. package/.ctxman/self-knowledge/documents.lance/_versions/7.manifest +0 -0
  44. package/.ctxman/self-knowledge/documents.lance/_versions/8.manifest +0 -0
  45. package/.ctxman/self-knowledge/documents.lance/_versions/9.manifest +0 -0
  46. package/.ctxman/self-knowledge/documents.lance/data/000010010011011101000010de8621405d9d5cb06540dbf02c.lance +0 -0
  47. package/.ctxman/self-knowledge/documents.lance/data/0000101001011100011000003fb62a4aa8978974075c144f1c.lance +0 -0
  48. package/.ctxman/self-knowledge/documents.lance/data/000011010000100111001011c6cc824d628f3bb1f4e04cd5d0.lance +0 -0
  49. package/.ctxman/self-knowledge/documents.lance/data/001000001000011001011100d8f74349ada090b332cd1df669.lance +0 -0
  50. package/.ctxman/self-knowledge/documents.lance/data/01000001111010001000010045ab44467bb1e7e4d8ae06bd58.lance +0 -0
  51. package/.ctxman/self-knowledge/documents.lance/data/01000101101110111110011131ef904241a89e1ffb6ddaf8e3.lance +0 -0
  52. package/.ctxman/self-knowledge/documents.lance/data/010010110000010000011010fbfcc145c6a8a827b28057a07e.lance +0 -0
  53. package/.ctxman/self-knowledge/documents.lance/data/1000011000101010001110003fb4f045c3835982efa1405324.lance +0 -0
  54. package/.ctxman/self-knowledge/documents.lance/data/100111011101110111101011f4396b410fa06af3b028d204e6.lance +0 -0
  55. package/.ctxman/self-knowledge/documents.lance/data/100111100111101000001100596b174e00995d6cc2969cc8dc.lance +0 -0
  56. package/.ctxman/self-knowledge/documents.lance/data/1010001100011000110010102ea16b484d887c7c9725feffd6.lance +0 -0
  57. package/.ctxman/self-knowledge/documents.lance/data/101101100010101111110111c5230944d9b3af7301b6392dda.lance +0 -0
  58. package/.ctxman/self-knowledge/documents.lance/data/1011011100100111111110105892bf41b5a530fedd7cf516f1.lance +0 -0
  59. package/.ctxman/self-knowledge/documents.lance/data/10111110000010001011110164e8774742bade87b483c10f36.lance +0 -0
  60. package/.ctxman/self-knowledge/documents.lance/data/1100000100010110001110103ffaca40a2a42c85067ebaf9ae.lance +0 -0
  61. package/.ctxman/self-knowledge/documents.lance/data/110000010001110110101110664bde4c76a78dfc14b05e83b8.lance +0 -0
  62. package/.ctxman/self-knowledge/documents.lance/data/11001100111000010100110050645c4281b1f35b2022234f7f.lance +0 -0
  63. package/.ctxman/self-knowledge/documents.lance/data/1101111011001100110111017214924a368f5719eab2246619.lance +0 -0
  64. package/.ctxman/self-knowledge/documents.lance/data/1110000111001010001000002150c24dcab653d39d07032e63.lance +0 -0
  65. package/.ctxman/self-knowledge/documents.lance/data/1110100000000100000011014d20534967a18b5c87721659ff.lance +0 -0
  66. package/.ctxman/self-knowledge/documents.lance/data/11110101111010000101011135a0114ef2b35dc6f1297a3381.lance +0 -0
  67. package/.ctxman/wizard-profiles/code-review/.contextignore +41 -0
  68. package/.ctxman/wizard-profiles/code-review/.contextinclude +30 -0
  69. package/.ctxman/wizard-profiles/code-review/.methodignore +26 -0
  70. package/.ctxman/wizard-profiles/code-review/.methodinclude +47 -0
  71. package/.ctxman/wizard-profiles/code-review/profile.json +26 -0
  72. package/.ctxman/wizard-profiles/documentation/.contextignore +21 -0
  73. package/.ctxman/wizard-profiles/documentation/.contextinclude +24 -0
  74. package/.ctxman/wizard-profiles/documentation/.methodignore +19 -0
  75. package/.ctxman/wizard-profiles/documentation/.methodinclude +44 -0
  76. package/.ctxman/wizard-profiles/documentation/profile.json +27 -0
  77. package/.ctxman/wizard-profiles/full/.contextignore +23 -0
  78. package/.ctxman/wizard-profiles/full/.contextinclude +38 -0
  79. package/.ctxman/wizard-profiles/full/.methodignore +5 -0
  80. package/.ctxman/wizard-profiles/full/.methodinclude +5 -0
  81. package/.ctxman/wizard-profiles/full/profile.json +28 -0
  82. package/.ctxman/wizard-profiles/llm-explain/.contextignore +25 -0
  83. package/.ctxman/wizard-profiles/llm-explain/.contextinclude +33 -0
  84. package/.ctxman/wizard-profiles/llm-explain/.methodignore +19 -0
  85. package/.ctxman/wizard-profiles/llm-explain/.methodinclude +48 -0
  86. package/.ctxman/wizard-profiles/llm-explain/profile.json +27 -0
  87. package/.ctxman/wizard-profiles/minimal/.contextignore +27 -0
  88. package/.ctxman/wizard-profiles/minimal/.contextinclude +14 -0
  89. package/.ctxman/wizard-profiles/minimal/.methodignore +22 -0
  90. package/.ctxman/wizard-profiles/minimal/.methodinclude +18 -0
  91. package/.ctxman/wizard-profiles/minimal/profile.json +26 -0
  92. package/.ctxman/wizard-profiles/security-audit/.contextignore +36 -0
  93. package/.ctxman/wizard-profiles/security-audit/.contextinclude +37 -0
  94. package/.ctxman/wizard-profiles/security-audit/.methodignore +29 -0
  95. package/.ctxman/wizard-profiles/security-audit/.methodinclude +60 -0
  96. package/.ctxman/wizard-profiles/security-audit/profile.json +34 -0
  97. package/README.md +1167 -0
  98. package/bin/cli.js +650 -0
  99. package/bin/cm-ask.js +95 -0
  100. package/bin/cm-gitingest.js +134 -0
  101. package/bin/cm-update.js +175 -0
  102. package/bin/mcp-server.js +31 -0
  103. package/ctxman.js +236 -0
  104. package/examples/README.md +298 -0
  105. package/examples/custom-llm-profiles.example.json +19 -0
  106. package/examples/wizard-profiles/code-review/.contextignore +41 -0
  107. package/examples/wizard-profiles/code-review/.contextinclude +30 -0
  108. package/examples/wizard-profiles/code-review/.methodignore +26 -0
  109. package/examples/wizard-profiles/code-review/.methodinclude +47 -0
  110. package/examples/wizard-profiles/code-review/profile.json +26 -0
  111. package/examples/wizard-profiles/documentation/.contextignore +21 -0
  112. package/examples/wizard-profiles/documentation/.contextinclude +24 -0
  113. package/examples/wizard-profiles/documentation/.methodignore +19 -0
  114. package/examples/wizard-profiles/documentation/.methodinclude +44 -0
  115. package/examples/wizard-profiles/documentation/profile.json +27 -0
  116. package/examples/wizard-profiles/full/.contextignore +23 -0
  117. package/examples/wizard-profiles/full/.contextinclude +38 -0
  118. package/examples/wizard-profiles/full/.methodignore +5 -0
  119. package/examples/wizard-profiles/full/.methodinclude +5 -0
  120. package/examples/wizard-profiles/full/profile.json +28 -0
  121. package/examples/wizard-profiles/llm-explain/.contextignore +25 -0
  122. package/examples/wizard-profiles/llm-explain/.contextinclude +33 -0
  123. package/examples/wizard-profiles/llm-explain/.methodignore +19 -0
  124. package/examples/wizard-profiles/llm-explain/.methodinclude +48 -0
  125. package/examples/wizard-profiles/llm-explain/profile.json +27 -0
  126. package/examples/wizard-profiles/minimal/.contextignore +27 -0
  127. package/examples/wizard-profiles/minimal/.contextinclude +14 -0
  128. package/examples/wizard-profiles/minimal/.methodignore +22 -0
  129. package/examples/wizard-profiles/minimal/.methodinclude +18 -0
  130. package/examples/wizard-profiles/minimal/profile.json +26 -0
  131. package/examples/wizard-profiles/security-audit/.contextignore +36 -0
  132. package/examples/wizard-profiles/security-audit/.contextinclude +37 -0
  133. package/examples/wizard-profiles/security-audit/.methodignore +29 -0
  134. package/examples/wizard-profiles/security-audit/.methodinclude +60 -0
  135. package/examples/wizard-profiles/security-audit/profile.json +34 -0
  136. package/index.js +68 -0
  137. package/lib/agents/pr-failure-agent.js +201 -0
  138. package/lib/analyzers/go-method-analyzer.js +126 -0
  139. package/lib/analyzers/method-analyzer.js +457 -0
  140. package/lib/analyzers/token-calculator.js +577 -0
  141. package/lib/api/mcp/prompts.js +319 -0
  142. package/lib/api/mcp/resources.js +269 -0
  143. package/lib/api/mcp/server.js +339 -0
  144. package/lib/api/mcp/transports/HttpSseTransport.js +63 -0
  145. package/lib/api/rest/server.js +405 -0
  146. package/lib/cache/CacheManager.js +388 -0
  147. package/lib/core/Analyzer.js +180 -0
  148. package/lib/core/ContextBuilder.js +325 -0
  149. package/lib/core/Reporter.js +270 -0
  150. package/lib/core/Scanner.js +129 -0
  151. package/lib/formatters/format-registry.js +419 -0
  152. package/lib/formatters/gitingest-formatter.js +746 -0
  153. package/lib/formatters/toon-formatter-v1.3.js +731 -0
  154. package/lib/formatters/toon-formatter.js +291 -0
  155. package/lib/integrations/git/BlameTracker.js +142 -0
  156. package/lib/integrations/git/DiffAnalyzer.js +260 -0
  157. package/lib/integrations/git/GitClient.js +336 -0
  158. package/lib/parsers/gitignore-parser.js +127 -0
  159. package/lib/parsers/method-filter-parser.js +50 -0
  160. package/lib/plugins/ExporterPlugin.js +102 -0
  161. package/lib/plugins/LanguagePlugin.js +100 -0
  162. package/lib/plugins/PluginManager.js +302 -0
  163. package/lib/rag/DataSourcePlugin.js +31 -0
  164. package/lib/rag/EmbeddingProvider.js +75 -0
  165. package/lib/rag/Indexer.js +83 -0
  166. package/lib/rag/VectorStoreFactory.js +24 -0
  167. package/lib/rag/sources/FileDataSource.js +70 -0
  168. package/lib/rag/stores/BaseVectorStore.js +43 -0
  169. package/lib/rag/stores/ChromaDBStore.js +85 -0
  170. package/lib/rag/stores/LanceDBStore.js +109 -0
  171. package/lib/rag/stores/PineconeStore.js +74 -0
  172. package/lib/ui/dashboard.js +109 -0
  173. package/lib/ui/index.js +15 -0
  174. package/lib/ui/progress-bar.js +110 -0
  175. package/lib/ui/select-input.js +51 -0
  176. package/lib/ui/wizard.js +359 -0
  177. package/lib/utils/clipboard-utils.js +66 -0
  178. package/lib/utils/config-utils.js +94 -0
  179. package/lib/utils/error-handler.js +145 -0
  180. package/lib/utils/file-utils.js +89 -0
  181. package/lib/utils/format-converter.js +325 -0
  182. package/lib/utils/git-utils.js +337 -0
  183. package/lib/utils/llm-detector.js +409 -0
  184. package/lib/utils/logger.js +278 -0
  185. package/lib/utils/token-utils.js +200 -0
  186. package/lib/utils/tokenizer-adapter.js +454 -0
  187. package/lib/utils/updater.js +565 -0
  188. package/lib/watch/FileWatcher.js +208 -0
  189. package/lib/watch/IncrementalAnalyzer.js +206 -0
  190. package/package.json +118 -0
@@ -0,0 +1,302 @@
1
+ /**
2
+ * PluginManager - Plugin System Core
3
+ * v3.0.0 - Plugin architecture
4
+ *
5
+ * Responsibilities:
6
+ * - Load and manage plugins
7
+ * - Plugin lifecycle management
8
+ * - Plugin dependency resolution
9
+ * - Event-driven plugin communication
10
+ */
11
+
12
+ import fs from 'fs';
13
+ import path from 'path';
14
+ import { EventEmitter } from 'events';
15
+ import { getLogger } from '../utils/logger.js';
16
+
17
+ const logger = getLogger('PluginManager');
18
+
19
+ export class PluginManager extends EventEmitter {
20
+ constructor(options = {}) {
21
+ super();
22
+
23
+ this.options = {
24
+ pluginPaths: [
25
+ './lib/languages',
26
+ './lib/exporters',
27
+ './plugins'
28
+ ],
29
+ autoLoad: true,
30
+ lazy: true, // Lazy load plugins
31
+ ...options
32
+ };
33
+
34
+ this.plugins = new Map();
35
+ this.loaded = new Map();
36
+ this.stats = {
37
+ registered: 0,
38
+ loaded: 0,
39
+ errors: 0
40
+ };
41
+ }
42
+
43
+ /**
44
+ * Initialize plugin manager
45
+ */
46
+ async initialize() {
47
+ logger.info('Initializing plugin manager...');
48
+
49
+ if (this.options.autoLoad) {
50
+ await this.discoverPlugins();
51
+ }
52
+
53
+ this.emit('initialized');
54
+ }
55
+
56
+ /**
57
+ * Discover plugins from plugin paths
58
+ */
59
+ async discoverPlugins() {
60
+ for (const pluginPath of this.options.pluginPaths) {
61
+ const fullPath = path.resolve(pluginPath);
62
+
63
+ if (!fs.existsSync(fullPath)) {
64
+ logger.debug(`Plugin path not found: ${pluginPath}`);
65
+ continue;
66
+ }
67
+
68
+ await this.discoverFromDirectory(fullPath);
69
+ }
70
+
71
+ logger.info(`Discovered ${this.plugins.size} plugins`);
72
+ }
73
+
74
+ /**
75
+ * Discover plugins from a directory
76
+ * @param {string} dirPath
77
+ */
78
+ async discoverFromDirectory(dirPath) {
79
+ try {
80
+ const entries = fs.readdirSync(dirPath, { withFileTypes: true });
81
+
82
+ for (const entry of entries) {
83
+ if (entry.isFile() && entry.name.endsWith('.js')) {
84
+ const pluginPath = path.join(dirPath, entry.name);
85
+ await this.registerPluginFromFile(pluginPath);
86
+ }
87
+ }
88
+ } catch (error) {
89
+ logger.error(`Error discovering plugins in ${dirPath}: ${error.message}`);
90
+ }
91
+ }
92
+
93
+ /**
94
+ * Register plugin from file
95
+ * @param {string} pluginPath
96
+ */
97
+ async registerPluginFromFile(pluginPath) {
98
+ try {
99
+ // Register without loading (lazy)
100
+ const pluginName = path.basename(pluginPath, '.js');
101
+
102
+ this.plugins.set(pluginName, {
103
+ name: pluginName,
104
+ path: pluginPath,
105
+ loaded: false,
106
+ instance: null
107
+ });
108
+
109
+ this.stats.registered++;
110
+ logger.debug(`Registered plugin: ${pluginName}`);
111
+
112
+ } catch (error) {
113
+ logger.error(`Failed to register plugin ${pluginPath}: ${error.message}`);
114
+ this.stats.errors++;
115
+ }
116
+ }
117
+
118
+ /**
119
+ * Load a plugin
120
+ * @param {string} pluginName
121
+ * @returns {Promise<Plugin>}
122
+ */
123
+ async load(pluginName) {
124
+ const plugin = this.plugins.get(pluginName);
125
+
126
+ if (!plugin) {
127
+ throw new Error(`Plugin not found: ${pluginName}`);
128
+ }
129
+
130
+ // Return cached instance if already loaded
131
+ if (plugin.loaded && plugin.instance) {
132
+ logger.debug(`Plugin already loaded: ${pluginName}`);
133
+ return plugin.instance;
134
+ }
135
+
136
+ // Load plugin
137
+ logger.info(`Loading plugin: ${pluginName}`);
138
+ const startTime = Date.now();
139
+
140
+ try {
141
+ const module = await import(plugin.path);
142
+ const PluginClass = module.default || module[Object.keys(module)[0]];
143
+
144
+ let instance;
145
+ if (typeof PluginClass === 'function') {
146
+ instance = new PluginClass();
147
+ } else {
148
+ instance = PluginClass;
149
+ }
150
+
151
+ plugin.instance = instance;
152
+ plugin.loaded = true;
153
+
154
+ const elapsed = Date.now() - startTime;
155
+ this.stats.loaded++;
156
+
157
+ logger.info(`Plugin loaded: ${pluginName} (${elapsed}ms)`);
158
+ this.emit('plugin:loaded', { name: pluginName, elapsed });
159
+
160
+ return instance;
161
+
162
+ } catch (error) {
163
+ logger.error(`Failed to load plugin ${pluginName}: ${error.message}`);
164
+ this.stats.errors++;
165
+ this.emit('plugin:error', { name: pluginName, error });
166
+ throw error;
167
+ }
168
+ }
169
+
170
+ /**
171
+ * Load multiple plugins
172
+ * @param {Array<string>} pluginNames
173
+ * @returns {Promise<Array<Plugin>>}
174
+ */
175
+ async loadMultiple(pluginNames) {
176
+ const promises = pluginNames.map(name => this.load(name));
177
+ return Promise.all(promises);
178
+ }
179
+
180
+ /**
181
+ * Get loaded plugin
182
+ * @param {string} pluginName
183
+ * @returns {Plugin|null}
184
+ */
185
+ get(pluginName) {
186
+ const plugin = this.plugins.get(pluginName);
187
+ return plugin?.instance || null;
188
+ }
189
+
190
+ /**
191
+ * Check if plugin is loaded
192
+ * @param {string} pluginName
193
+ * @returns {boolean}
194
+ */
195
+ isLoaded(pluginName) {
196
+ const plugin = this.plugins.get(pluginName);
197
+ return plugin?.loaded || false;
198
+ }
199
+
200
+ /**
201
+ * Unload a plugin
202
+ * @param {string} pluginName
203
+ */
204
+ unload(pluginName) {
205
+ const plugin = this.plugins.get(pluginName);
206
+
207
+ if (!plugin || !plugin.loaded) {
208
+ return;
209
+ }
210
+
211
+ // Call cleanup if available
212
+ if (plugin.instance && typeof plugin.instance.cleanup === 'function') {
213
+ plugin.instance.cleanup();
214
+ }
215
+
216
+ plugin.loaded = false;
217
+ plugin.instance = null;
218
+ this.stats.loaded--;
219
+
220
+ logger.info(`Plugin unloaded: ${pluginName}`);
221
+ this.emit('plugin:unloaded', { name: pluginName });
222
+ }
223
+
224
+ /**
225
+ * Unload all plugins
226
+ */
227
+ unloadAll() {
228
+ for (const [name, plugin] of this.plugins) {
229
+ if (plugin.loaded) {
230
+ this.unload(name);
231
+ }
232
+ }
233
+ }
234
+
235
+ /**
236
+ * List all registered plugins
237
+ * @returns {Array<string>}
238
+ */
239
+ list() {
240
+ return Array.from(this.plugins.keys());
241
+ }
242
+
243
+ /**
244
+ * List loaded plugins
245
+ * @returns {Array<string>}
246
+ */
247
+ listLoaded() {
248
+ return Array.from(this.plugins.entries())
249
+ .filter(([, plugin]) => plugin.loaded)
250
+ .map(([name]) => name);
251
+ }
252
+
253
+ /**
254
+ * Get plugin info
255
+ * @param {string} pluginName
256
+ * @returns {object|null}
257
+ */
258
+ getInfo(pluginName) {
259
+ const plugin = this.plugins.get(pluginName);
260
+
261
+ if (!plugin) {
262
+ return null;
263
+ }
264
+
265
+ return {
266
+ name: plugin.name,
267
+ loaded: plugin.loaded,
268
+ path: plugin.path,
269
+ hasInstance: plugin.instance !== null
270
+ };
271
+ }
272
+
273
+ /**
274
+ * Get statistics
275
+ * @returns {object}
276
+ */
277
+ getStats() {
278
+ return { ...this.stats };
279
+ }
280
+
281
+ /**
282
+ * Register plugin programmatically
283
+ * @param {string} name
284
+ * @param {Plugin} instance
285
+ */
286
+ register(name, instance) {
287
+ this.plugins.set(name, {
288
+ name,
289
+ path: null,
290
+ loaded: true,
291
+ instance
292
+ });
293
+
294
+ this.stats.registered++;
295
+ this.stats.loaded++;
296
+
297
+ logger.info(`Plugin registered: ${name}`);
298
+ this.emit('plugin:registered', { name });
299
+ }
300
+ }
301
+
302
+ export default PluginManager;
@@ -0,0 +1,31 @@
1
+ /**
2
+ * Base class for all data source plugins (e.g., GitHub, Slack, Linear)
3
+ */
4
+ export class DataSourcePlugin {
5
+ /**
6
+ * @param {string} name - Unique name of the data source
7
+ */
8
+ constructor(name) {
9
+ this.name = name;
10
+ }
11
+
12
+ /**
13
+ * Validates if the necessary configuration (tokens, urls) is present.
14
+ * @param {Object} config
15
+ * @returns {Promise<boolean>}
16
+ */
17
+ async validateConfig(config) {
18
+ return true;
19
+ }
20
+
21
+ /**
22
+ * Collects data from the source.
23
+ * Generator function that yields documents to be indexed.
24
+ *
25
+ * @param {Object} options
26
+ * @yields {import('./Indexer').IndexableDocument}
27
+ */
28
+ async *collect(options = {}) {
29
+ throw new Error('Not implemented');
30
+ }
31
+ }
@@ -0,0 +1,75 @@
1
+ import { pipeline } from '@xenova/transformers';
2
+
3
+ /**
4
+ * Abstract base class for embedding providers
5
+ */
6
+ export class EmbeddingProvider {
7
+ /**
8
+ * Generates an embedding for the given text.
9
+ * @param {string} text
10
+ * @returns {Promise<number[]>}
11
+ */
12
+ async embed(text) {
13
+ throw new Error('Not implemented');
14
+ }
15
+ }
16
+
17
+ /**
18
+ * Uses @xenova/transformers to generate embeddings locally using ONNX models.
19
+ * Default model: Xenova/all-MiniLM-L6-v2 (384 dimensions)
20
+ */
21
+ export class TransformersEmbeddingProvider extends EmbeddingProvider {
22
+ constructor(modelName = 'Xenova/all-MiniLM-L6-v2') {
23
+ super();
24
+ this.modelName = modelName;
25
+ this.pipe = null;
26
+ }
27
+
28
+ async init() {
29
+ if (!this.pipe) {
30
+ // Load the pipeline. This will download the model if not present.
31
+ this.pipe = await pipeline('feature-extraction', this.modelName);
32
+ }
33
+ }
34
+
35
+ async embed(text) {
36
+ if (!this.pipe) {
37
+ await this.init();
38
+ }
39
+ // pooling: 'mean' and normalize: true are standard for sentence embeddings
40
+ const result = await this.pipe(text, { pooling: 'mean', normalize: true });
41
+ return Array.from(result.data);
42
+ }
43
+ }
44
+
45
+ /**
46
+ * Mock provider for testing without loading heavy models.
47
+ */
48
+ export class MockEmbeddingProvider extends EmbeddingProvider {
49
+ constructor(dimension = 384) {
50
+ super();
51
+ this.dimension = dimension;
52
+ }
53
+
54
+ async embed(text) {
55
+ // Generate a deterministic-ish vector based on text length for consistent testing
56
+ // or just random if text is ignored.
57
+ // Let's make it pseudo-random based on text hash to be stable.
58
+ let hash = 0;
59
+ for (let i = 0; i < text.length; i++) {
60
+ hash = ((hash << 5) - hash) + text.charCodeAt(i);
61
+ hash |= 0;
62
+ }
63
+
64
+ const vector = [];
65
+ for (let i = 0; i < this.dimension; i++) {
66
+ // Simple pseudo-random generator
67
+ const val = Math.sin(hash + i);
68
+ vector.push(val);
69
+ }
70
+
71
+ // Normalize
72
+ const norm = Math.sqrt(vector.reduce((sum, val) => sum + val * val, 0));
73
+ return vector.map(val => val / norm);
74
+ }
75
+ }
@@ -0,0 +1,83 @@
1
+ import { getLogger } from '../utils/logger.js';
2
+
3
+ const logger = getLogger('Indexer');
4
+
5
+ /**
6
+ * @typedef {Object} IndexableDocument
7
+ * @property {string} text - Content to embed
8
+ * @property {string} id - Unique ID from source
9
+ * @property {Object} metadata - Additional context (source, author, url, etc.)
10
+ */
11
+
12
+ export class Indexer {
13
+ /**
14
+ * @param {import('./stores/BaseVectorStore').BaseVectorStore} vectorStore
15
+ */
16
+ constructor(vectorStore) {
17
+ this.vectorStore = vectorStore;
18
+ this.sources = new Map();
19
+ }
20
+
21
+ /**
22
+ * Register a data source plugin
23
+ * @param {import('./DataSourcePlugin').DataSourcePlugin} sourcePlugin
24
+ */
25
+ registerSource(sourcePlugin) {
26
+ this.sources.set(sourcePlugin.name, sourcePlugin);
27
+ logger.info(`Registered data source: ${sourcePlugin.name}`);
28
+ }
29
+
30
+ /**
31
+ * Run indexing for a specific source or all sources
32
+ * @param {string} [sourceName] - Optional source name to run specific indexer
33
+ * @param {Object} options - Options passed to collect method
34
+ */
35
+ async index(sourceName, options = {}) {
36
+ const sourcesToRun = sourceName
37
+ ? [this.sources.get(sourceName)].filter(Boolean)
38
+ : Array.from(this.sources.values());
39
+
40
+ if (sourcesToRun.length === 0) {
41
+ logger.warn(`No sources found to index (requested: ${sourceName || 'all'})`);
42
+ return;
43
+ }
44
+
45
+ for (const source of sourcesToRun) {
46
+ logger.info(`Starting indexing for source: ${source.name}`);
47
+ let count = 0;
48
+ const batch = [];
49
+ const BATCH_SIZE = 10; // Process in small batches
50
+
51
+ try {
52
+ for await (const doc of source.collect(options)) {
53
+ batch.push({
54
+ text: doc.text,
55
+ metadata: {
56
+ ...doc.metadata,
57
+ sourceId: doc.id,
58
+ source: source.name,
59
+ indexedAt: new Date().toISOString()
60
+ }
61
+ });
62
+
63
+ if (batch.length >= BATCH_SIZE) {
64
+ await this.vectorStore.addDocuments([...batch]);
65
+ count += batch.length;
66
+ batch.length = 0; // Clear batch
67
+ logger.debug(`Indexed ${count} documents from ${source.name}`);
68
+ }
69
+ }
70
+
71
+ // Flush remaining
72
+ if (batch.length > 0) {
73
+ await this.vectorStore.addDocuments(batch);
74
+ count += batch.length;
75
+ }
76
+
77
+ logger.info(`Completed indexing ${source.name}. Total documents: ${count}`);
78
+ } catch (error) {
79
+ logger.error(`Error indexing ${source.name}: ${error.message}`);
80
+ }
81
+ }
82
+ }
83
+ }
@@ -0,0 +1,24 @@
1
+ import { LanceDBStore } from './stores/LanceDBStore.js';
2
+ import { ChromaDBStore } from './stores/ChromaDBStore.js';
3
+ import { PineconeStore } from './stores/PineconeStore.js';
4
+
5
+ export class VectorStoreFactory {
6
+ /**
7
+ * @param {string} type
8
+ * @param {import('./EmbeddingProvider').EmbeddingProvider} embeddingProvider
9
+ * @param {Object} config
10
+ */
11
+ static create(type, embeddingProvider, config = {}) {
12
+ switch (type.toLowerCase()) {
13
+ case 'lancedb':
14
+ return new LanceDBStore(embeddingProvider, config.path);
15
+ case 'chroma':
16
+ case 'chromadb':
17
+ return new ChromaDBStore(embeddingProvider, config);
18
+ case 'pinecone':
19
+ return new PineconeStore(embeddingProvider, config);
20
+ default:
21
+ throw new Error(`Unknown VectorStore type: ${type}`);
22
+ }
23
+ }
24
+ }
@@ -0,0 +1,70 @@
1
+ import fs from 'fs/promises';
2
+ import path from 'path';
3
+ import { DataSourcePlugin } from '../DataSourcePlugin.js';
4
+ import { TokenizerManager } from '../../utils/tokenizer-adapter.js';
5
+
6
+ export class FileDataSource extends DataSourcePlugin {
7
+ constructor() {
8
+ super('local-files');
9
+ this.tokenizer = new TokenizerManager();
10
+ }
11
+
12
+ async validateConfig(config) {
13
+ // Requires a target directory
14
+ return !!config.targetDir;
15
+ }
16
+
17
+ async *collect(options = {}) {
18
+ const targetDir = options.targetDir || process.cwd();
19
+ const maxDepth = options.maxDepth || 5;
20
+ const include = options.include || ['.js', '.md', '.ts', '.json'];
21
+ const exclude = options.exclude || ['node_modules', '.git', 'dist', 'coverage'];
22
+
23
+ yield* this.walkDirectory(targetDir, maxDepth, include, exclude);
24
+ }
25
+
26
+ async *walkDirectory(dir, depth, include, exclude) {
27
+ if (depth < 0) return;
28
+
29
+ let entries;
30
+ try {
31
+ entries = await fs.readdir(dir, { withFileTypes: true });
32
+ } catch (error) {
33
+ console.warn(`[FileDataSource] Skipping ${dir}: ${error.message}`);
34
+ return;
35
+ }
36
+
37
+ for (const entry of entries) {
38
+ const fullPath = path.join(dir, entry.name);
39
+
40
+ if (exclude.some(ex => fullPath.includes(ex))) continue;
41
+
42
+ if (entry.isDirectory()) {
43
+ yield* this.walkDirectory(fullPath, depth - 1, include, exclude);
44
+ } else if (entry.isFile()) {
45
+ const ext = path.extname(entry.name);
46
+ if (include.length > 0 && !include.includes(ext)) continue;
47
+
48
+ try {
49
+ const content = await fs.readFile(fullPath, 'utf-8');
50
+ // Basic text check (skip binaries)
51
+ if (content.includes('\0')) continue;
52
+
53
+ yield {
54
+ id: fullPath,
55
+ text: content,
56
+ metadata: {
57
+ source: 'file-system',
58
+ path: fullPath,
59
+ extension: ext,
60
+ filename: entry.name,
61
+ size: content.length
62
+ }
63
+ };
64
+ } catch (error) {
65
+ console.warn(`[FileDataSource] Failed to read ${fullPath}: ${error.message}`);
66
+ }
67
+ }
68
+ }
69
+ }
70
+ }
@@ -0,0 +1,43 @@
1
+ /**
2
+ * @typedef {Object} VectorDocument
3
+ * @property {string} text
4
+ * @property {Array<number>} vector
5
+ * @property {string} id
6
+ * @property {Object} metadata
7
+ */
8
+
9
+ /**
10
+ * @typedef {Object} SearchResult
11
+ * @property {string} text
12
+ * @property {number} score
13
+ * @property {Object} metadata
14
+ */
15
+
16
+ export class BaseVectorStore {
17
+ /**
18
+ * @param {import('../EmbeddingProvider').EmbeddingProvider} embeddingProvider
19
+ */
20
+ constructor(embeddingProvider) {
21
+ this.embeddingProvider = embeddingProvider;
22
+ }
23
+
24
+ async init() {
25
+ throw new Error('Method not implemented');
26
+ }
27
+
28
+ /**
29
+ * @param {Array<{text: string, metadata: Object}>} docs
30
+ */
31
+ async addDocuments(docs) {
32
+ throw new Error('Method not implemented');
33
+ }
34
+
35
+ /**
36
+ * @param {string} queryText
37
+ * @param {number} limit
38
+ * @returns {Promise<Array<SearchResult>>}
39
+ */
40
+ async search(queryText, limit = 5) {
41
+ throw new Error('Method not implemented');
42
+ }
43
+ }
@@ -0,0 +1,85 @@
1
+ import { BaseVectorStore } from './BaseVectorStore.js';
2
+
3
+ export class ChromaDBStore extends BaseVectorStore {
4
+ /**
5
+ * @param {import('../EmbeddingProvider').EmbeddingProvider} embeddingProvider
6
+ * @param {Object} config
7
+ * @param {string} [config.collectionName]
8
+ * @param {string} [config.url]
9
+ */
10
+ constructor(embeddingProvider, config = {}) {
11
+ super(embeddingProvider);
12
+ this.collectionName = config.collectionName || 'ctxman-docs';
13
+ this.url = config.url || 'http://localhost:8000';
14
+ this.client = null;
15
+ this.collection = null;
16
+ }
17
+
18
+ async init() {
19
+ if (this.client) return;
20
+
21
+ try {
22
+ const { ChromaClient } = await import('chromadb');
23
+ this.client = new ChromaClient({ path: this.url });
24
+
25
+ this.collection = await this.client.getOrCreateCollection({
26
+ name: this.collectionName,
27
+ });
28
+ } catch (error) {
29
+ if (error.code === 'ERR_MODULE_NOT_FOUND') {
30
+ throw new Error('Please install chromadb to use ChromaDBStore: npm install chromadb');
31
+ }
32
+ throw error;
33
+ }
34
+ }
35
+
36
+ async addDocuments(docs) {
37
+ if (!this.collection) await this.init();
38
+ if (docs.length === 0) return;
39
+
40
+ const ids = docs.map(d => d.id || crypto.randomUUID());
41
+ const metadatas = docs.map(d => d.metadata || {});
42
+ const documents = docs.map(d => d.text);
43
+
44
+ // Chroma can handle embeddings, but we have a provider.
45
+ // If we want to use our provider:
46
+ const embeddings = [];
47
+ for (const doc of docs) {
48
+ embeddings.push(await this.embeddingProvider.embed(doc.text));
49
+ }
50
+
51
+ await this.collection.add({
52
+ ids,
53
+ embeddings,
54
+ metadatas,
55
+ documents
56
+ });
57
+ }
58
+
59
+ async search(queryText, limit = 5) {
60
+ if (!this.collection) await this.init();
61
+
62
+ const queryEmbedding = await this.embeddingProvider.embed(queryText);
63
+
64
+ const results = await this.collection.query({
65
+ queryEmbeddings: [queryEmbedding],
66
+ nResults: limit
67
+ });
68
+
69
+ // Chroma returns arrays of arrays
70
+ if (!results.ids || results.ids.length === 0) return [];
71
+
72
+ const hits = [];
73
+ const count = results.ids[0].length;
74
+
75
+ for (let i = 0; i < count; i++) {
76
+ hits.push({
77
+ text: results.documents[0][i],
78
+ score: results.distances ? results.distances[0][i] : 0,
79
+ metadata: results.metadatas[0][i]
80
+ });
81
+ }
82
+
83
+ return hits;
84
+ }
85
+ }