@softerist/heuristic-mcp 2.1.47 → 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 (109) hide show
  1. package/.agent/workflows/code-review.md +60 -0
  2. package/.prettierrc +7 -0
  3. package/ARCHITECTURE.md +105 -170
  4. package/CONTRIBUTING.md +32 -113
  5. package/GEMINI.md +73 -0
  6. package/LICENSE +21 -21
  7. package/README.md +161 -54
  8. package/config.json +876 -75
  9. package/debug-pids.js +27 -0
  10. package/eslint.config.js +36 -0
  11. package/features/ann-config.js +37 -26
  12. package/features/clear-cache.js +28 -19
  13. package/features/find-similar-code.js +142 -66
  14. package/features/hybrid-search.js +253 -93
  15. package/features/index-codebase.js +1455 -394
  16. package/features/lifecycle.js +813 -180
  17. package/features/register.js +58 -52
  18. package/index.js +450 -306
  19. package/lib/cache-ops.js +22 -0
  20. package/lib/cache-utils.js +68 -0
  21. package/lib/cache.js +1392 -587
  22. package/lib/call-graph.js +165 -50
  23. package/lib/cli.js +154 -0
  24. package/lib/config.js +462 -121
  25. package/lib/embedding-process.js +77 -0
  26. package/lib/embedding-worker.js +545 -30
  27. package/lib/ignore-patterns.js +61 -59
  28. package/lib/json-worker.js +14 -0
  29. package/lib/json-writer.js +344 -0
  30. package/lib/logging.js +88 -0
  31. package/lib/memory-logger.js +13 -0
  32. package/lib/project-detector.js +13 -17
  33. package/lib/server-lifecycle.js +38 -0
  34. package/lib/settings-editor.js +645 -0
  35. package/lib/tokenizer.js +207 -104
  36. package/lib/utils.js +273 -198
  37. package/lib/vector-store-binary.js +592 -0
  38. package/mcp_config.example.json +13 -0
  39. package/package.json +13 -2
  40. package/scripts/clear-cache.js +6 -17
  41. package/scripts/download-model.js +14 -9
  42. package/scripts/postinstall.js +5 -5
  43. package/search-configs.js +36 -0
  44. package/test/ann-config.test.js +179 -0
  45. package/test/ann-fallback.test.js +6 -6
  46. package/test/binary-store.test.js +69 -0
  47. package/test/cache-branches.test.js +120 -0
  48. package/test/cache-errors.test.js +264 -0
  49. package/test/cache-extra.test.js +300 -0
  50. package/test/cache-helpers.test.js +205 -0
  51. package/test/cache-hnsw-failure.test.js +40 -0
  52. package/test/cache-json-worker.test.js +190 -0
  53. package/test/cache-worker.test.js +102 -0
  54. package/test/cache.test.js +443 -0
  55. package/test/call-graph.test.js +103 -4
  56. package/test/clear-cache.test.js +69 -68
  57. package/test/code-review-workflow.test.js +50 -0
  58. package/test/config.test.js +418 -0
  59. package/test/coverage-gap.test.js +497 -0
  60. package/test/coverage-maximizer.test.js +236 -0
  61. package/test/debug-analysis.js +107 -0
  62. package/test/embedding-model.test.js +173 -103
  63. package/test/embedding-worker-extra.test.js +272 -0
  64. package/test/embedding-worker.test.js +158 -0
  65. package/test/features.test.js +139 -0
  66. package/test/final-boost.test.js +271 -0
  67. package/test/final-polish.test.js +183 -0
  68. package/test/final.test.js +95 -0
  69. package/test/find-similar-code.test.js +191 -0
  70. package/test/helpers.js +92 -11
  71. package/test/helpers.test.js +46 -0
  72. package/test/hybrid-search-basic.test.js +62 -0
  73. package/test/hybrid-search-branch.test.js +202 -0
  74. package/test/hybrid-search-callgraph.test.js +229 -0
  75. package/test/hybrid-search-extra.test.js +81 -0
  76. package/test/hybrid-search.test.js +484 -71
  77. package/test/index-cli.test.js +520 -0
  78. package/test/index-codebase-batch.test.js +119 -0
  79. package/test/index-codebase-branches.test.js +585 -0
  80. package/test/index-codebase-core.test.js +1032 -0
  81. package/test/index-codebase-edge-cases.test.js +254 -0
  82. package/test/index-codebase-errors.test.js +132 -0
  83. package/test/index-codebase-gap.test.js +239 -0
  84. package/test/index-codebase-lines.test.js +151 -0
  85. package/test/index-codebase-watcher.test.js +259 -0
  86. package/test/index-codebase-zone.test.js +259 -0
  87. package/test/index-codebase.test.js +371 -69
  88. package/test/index-memory.test.js +220 -0
  89. package/test/indexer-detailed.test.js +176 -0
  90. package/test/integration.test.js +148 -92
  91. package/test/json-worker.test.js +50 -0
  92. package/test/lifecycle.test.js +541 -0
  93. package/test/master.test.js +198 -0
  94. package/test/perfection.test.js +349 -0
  95. package/test/project-detector.test.js +65 -0
  96. package/test/register.test.js +262 -0
  97. package/test/tokenizer.test.js +55 -93
  98. package/test/ultra-maximizer.test.js +116 -0
  99. package/test/utils-branches.test.js +161 -0
  100. package/test/utils-extra.test.js +116 -0
  101. package/test/utils.test.js +131 -0
  102. package/test/verify_fixes.js +76 -0
  103. package/test/worker-errors.test.js +96 -0
  104. package/test/worker-init.test.js +102 -0
  105. package/test/worker_throttling.test.js +93 -0
  106. package/tools/scripts/benchmark-search.js +95 -0
  107. package/tools/scripts/cache-stats.js +71 -0
  108. package/tools/scripts/manual-search.js +34 -0
  109. package/vitest.config.js +19 -9
@@ -19,7 +19,11 @@ export const IGNORE_PATTERNS = {
19
19
  '**/yarn-debug.log*',
20
20
  '**/yarn-error.log*',
21
21
  '**/.pnpm-store/**',
22
- '**/.turbo/**'
22
+ '**/.turbo/**',
23
+ '**/package-lock.json',
24
+ '**/pnpm-lock.yaml',
25
+ '**/bun.lockb',
26
+ '**/yarn.lock',
23
27
  ],
24
28
 
25
29
  // Python
@@ -52,7 +56,7 @@ export const IGNORE_PATTERNS = {
52
56
  '**/.coverage',
53
57
  '**/.hypothesis/**',
54
58
  '**/.mypy_cache/**',
55
- '**/.ruff_cache/**'
59
+ '**/.ruff_cache/**',
56
60
  ],
57
61
 
58
62
  // Java/Maven
@@ -72,7 +76,7 @@ export const IGNORE_PATTERNS = {
72
76
  '**/*.class',
73
77
  '**/*.jar',
74
78
  '**/*.war',
75
- '**/*.ear'
79
+ '**/*.ear',
76
80
  ],
77
81
 
78
82
  // Android
@@ -91,7 +95,7 @@ export const IGNORE_PATTERNS = {
91
95
  '**/*.dex',
92
96
  '**/google-services.json',
93
97
  '**/gradle-app.setting',
94
- '**/.navigation/**'
98
+ '**/.navigation/**',
95
99
  ],
96
100
 
97
101
  // iOS/Swift
@@ -112,41 +116,20 @@ export const IGNORE_PATTERNS = {
112
116
  '**/*.moved-aside',
113
117
  '**/*.xcuserstate',
114
118
  '**/*.hmap',
115
- '**/*.ipa'
119
+ '**/*.ipa',
116
120
  ],
117
121
 
118
122
  // Go
119
- go: [
120
- '**/vendor/**',
121
- '**/bin/**',
122
- '**/pkg/**',
123
- '**/*.exe',
124
- '**/*.test',
125
- '**/*.prof'
126
- ],
123
+ go: ['**/vendor/**', '**/bin/**', '**/pkg/**', '**/*.exe', '**/*.test', '**/*.prof'],
127
124
 
128
125
  // PHP
129
- php: [
130
- '**/vendor/**',
131
- '**/composer.phar',
132
- '**/composer.lock',
133
- '**/.phpunit.result.cache'
134
- ],
126
+ php: ['**/vendor/**', '**/composer.phar', '**/composer.lock', '**/.phpunit.result.cache'],
135
127
 
136
128
  // Rust
137
- rust: [
138
- '**/target/**',
139
- '**/Cargo.lock',
140
- '**/*.rs.bk'
141
- ],
129
+ rust: ['**/target/**', '**/Cargo.lock', '**/*.rs.bk'],
142
130
 
143
131
  // Ruby
144
- ruby: [
145
- '**/vendor/bundle/**',
146
- '**/.bundle/**',
147
- '**/Gemfile.lock',
148
- '**/.byebug_history'
149
- ],
132
+ ruby: ['**/vendor/bundle/**', '**/.bundle/**', '**/Gemfile.lock', '**/.byebug_history'],
150
133
 
151
134
  // .NET/C#
152
135
  dotnet: [
@@ -156,7 +139,7 @@ export const IGNORE_PATTERNS = {
156
139
  '**/*.user',
157
140
  '**/*.suo',
158
141
  '**/.vs/**',
159
- '**/node_modules/**'
142
+ '**/node_modules/**',
160
143
  ],
161
144
 
162
145
  // Common (IDE, OS, Build tools)
@@ -166,13 +149,13 @@ export const IGNORE_PATTERNS = {
166
149
  '**/.svn/**',
167
150
  '**/.hg/**',
168
151
  '**/.bzr/**',
169
-
152
+
170
153
  // OS files
171
154
  '**/.DS_Store',
172
155
  '**/Thumbs.db',
173
156
  '**/desktop.ini',
174
157
  '**/$RECYCLE.BIN/**',
175
-
158
+
176
159
  // Backup files
177
160
  '**/*.bak',
178
161
  '**/*.backup',
@@ -182,16 +165,16 @@ export const IGNORE_PATTERNS = {
182
165
  '**/*.swn',
183
166
  '**/#*#',
184
167
  '**/.#*',
185
-
168
+
186
169
  // Lock files (editor/runtime, not package managers)
187
170
  '**/*.lock',
188
171
  '**/.~lock*',
189
-
172
+
190
173
  // Logs
191
174
  '**/*.log',
192
175
  '**/logs/**',
193
176
  '**/*.log.*',
194
-
177
+
195
178
  // IDEs and Editors
196
179
  '**/.vscode/**',
197
180
  '**/.idea/**',
@@ -207,19 +190,19 @@ export const IGNORE_PATTERNS = {
207
190
  '**/*.tmproj',
208
191
  '**/*.tmproject',
209
192
  '**/tmtags',
210
-
193
+
211
194
  // Vim
212
195
  '**/*~',
213
196
  '**/*.swp',
214
197
  '**/*.swo',
215
198
  '**/.*.sw?',
216
199
  '**/Session.vim',
217
-
200
+
218
201
  // Emacs
219
202
  '**/*~',
220
203
  '**/#*#',
221
204
  '**/.#*',
222
-
205
+
223
206
  // Environment files (secrets)
224
207
  '**/.env',
225
208
  '**/.env.local',
@@ -236,21 +219,21 @@ export const IGNORE_PATTERNS = {
236
219
  '**/*.cer',
237
220
  '**/*.p12',
238
221
  '**/*.pfx',
239
-
222
+
240
223
  // Temporary files
241
224
  '**/tmp/**',
242
225
  '**/temp/**',
243
226
  '**/*.tmp',
244
227
  '**/*.temp',
245
228
  '**/.cache/**',
246
-
229
+
247
230
  // Session & runtime
248
231
  '**/.sass-cache/**',
249
232
  '**/connect.lock',
250
233
  '**/*.pid',
251
234
  '**/*.seed',
252
235
  '**/*.pid.lock',
253
-
236
+
254
237
  // Coverage & test output
255
238
  '**/coverage/**',
256
239
  '**/.nyc_output/**',
@@ -258,16 +241,16 @@ export const IGNORE_PATTERNS = {
258
241
  '**/*.cover',
259
242
  '**/*.coverage',
260
243
  '**/htmlcov/**',
261
-
244
+
262
245
  // Documentation builds
263
246
  '**/docs/_build/**',
264
247
  '**/site/**',
265
-
248
+
266
249
  // Misc
267
250
  '**/*.orig',
268
251
  '**/core',
269
- '**/*.core'
270
- ]
252
+ '**/*.core',
253
+ ],
271
254
  };
272
255
 
273
256
  // Map marker files to project types
@@ -277,38 +260,57 @@ export const FILE_TYPE_MAP = {
277
260
  'package-lock.json': 'javascript',
278
261
  'yarn.lock': 'javascript',
279
262
  'pnpm-lock.yaml': 'javascript',
280
-
263
+
281
264
  // Python
282
265
  'requirements.txt': 'python',
283
- 'Pipfile': 'python',
266
+ Pipfile: 'python',
284
267
  'pyproject.toml': 'python',
285
268
  'setup.py': 'python',
286
-
269
+
287
270
  // Android
288
271
  'build.gradle': 'android',
289
272
  'build.gradle.kts': 'android',
290
273
  'settings.gradle': 'android',
291
-
274
+
292
275
  // Java
293
276
  'pom.xml': 'java',
294
-
277
+
295
278
  // iOS
296
- 'Podfile': 'ios',
279
+ Podfile: 'ios',
297
280
  'Package.swift': 'ios',
298
-
281
+
299
282
  // Go
300
283
  'go.mod': 'go',
301
-
284
+
302
285
  // PHP
303
286
  'composer.json': 'php',
304
-
287
+
305
288
  // Rust
306
289
  'Cargo.toml': 'rust',
307
-
290
+
308
291
  // Ruby
309
- 'Gemfile': 'ruby',
310
-
292
+ Gemfile: 'ruby',
293
+
311
294
  // .NET
312
295
  '*.csproj': 'dotnet',
313
- '*.sln': 'dotnet'
296
+ '*.sln': 'dotnet',
314
297
  };
298
+
299
+ // Directories to skip during project detection (recursion)
300
+ export const SKIP_DIRECTORIES = [
301
+ 'node_modules',
302
+ 'dist',
303
+ 'build',
304
+ 'target',
305
+ 'vendor',
306
+ 'coverage',
307
+ 'htmlcov',
308
+ 'typings',
309
+ 'nltk_data',
310
+ 'secrets',
311
+ 'venv',
312
+ 'env',
313
+ '__pycache__',
314
+ 'eggs',
315
+ '.eggs',
316
+ ];
@@ -0,0 +1,14 @@
1
+ import fs from 'fs/promises';
2
+ import { parentPort, workerData } from 'worker_threads';
3
+
4
+ async function loadJson() {
5
+ try {
6
+ const data = await fs.readFile(workerData.filePath, 'utf-8');
7
+ const parsed = JSON.parse(data);
8
+ parentPort?.postMessage({ ok: true, data: parsed });
9
+ } catch (error) {
10
+ parentPort?.postMessage({ ok: false, error: error.message });
11
+ }
12
+ }
13
+
14
+ void loadJson();
@@ -0,0 +1,344 @@
1
+ import fs from 'fs';
2
+
3
+ function isTypedArray(x) {
4
+ return x && ArrayBuffer.isView(x) && !(x instanceof DataView);
5
+ }
6
+
7
+ function onceDrainOrError(stream) {
8
+ return new Promise((resolve, reject) => {
9
+ const onDrain = () => cleanup(resolve);
10
+ const onError = (err) => cleanup(() => reject(err));
11
+
12
+ const cleanup = (fn) => {
13
+ stream.off('drain', onDrain);
14
+ stream.off('error', onError);
15
+ fn();
16
+ };
17
+
18
+ stream.once('drain', onDrain);
19
+ stream.once('error', onError);
20
+ });
21
+ }
22
+
23
+ /**
24
+ * Streaming JSON array writer optimized for:
25
+ * - TypedArray vectors streamed (no per-item vector allocation)
26
+ * - backpressure safety
27
+ * - configurable float rounding + flush threshold
28
+ * - compact mode when indent === '' (no forced newlines)
29
+ * - safe cleanup on failure (abort)
30
+ * - optional native TypedArray.join(',') fast-path when rounding is disabled
31
+ */
32
+ export class StreamingJsonWriter {
33
+ /**
34
+ * @param {string} filePath
35
+ * @param {object} [opts]
36
+ * @param {number} [opts.highWaterMark] Stream internal buffer size.
37
+ * @param {number|null} [opts.floatDigits] Round floats to N digits. null disables rounding.
38
+ * @param {number} [opts.flushChars] Flush threshold for the internal string buffer.
39
+ * @param {string} [opts.indent] Indent prefix per item ("" for compact, " " for pretty).
40
+ * @param {boolean} [opts.assumeFinite] Skip NaN/Infinity checks (unsafe if false data).
41
+ * @param {boolean} [opts.checkFinite] If set, overrides assumeFinite (true = check, false = skip).
42
+ * @param {boolean} [opts.noMutation] Avoid temporary mutation when stripping vector.
43
+ * @param {number} [opts.joinThreshold] Max elements to use single join() string.
44
+ * @param {number} [opts.joinChunkSize] Elements per join() chunk when chunking.
45
+ */
46
+ constructor(
47
+ filePath,
48
+ {
49
+ highWaterMark = 256 * 1024,
50
+ floatDigits = 6,
51
+ flushChars = 256 * 1024,
52
+ indent = '',
53
+ assumeFinite,
54
+ checkFinite,
55
+ noMutation = false,
56
+ joinThreshold = 8192,
57
+ joinChunkSize = 2048,
58
+ } = {},
59
+ ) {
60
+ this.filePath = filePath;
61
+ this.highWaterMark = Number.isInteger(highWaterMark) && highWaterMark > 8 * 1024
62
+ ? highWaterMark
63
+ : 256 * 1024;
64
+ this.flushChars = Number.isInteger(flushChars) && flushChars > 8 * 1024
65
+ ? flushChars
66
+ : 256 * 1024;
67
+ this.indent = typeof indent === 'string' ? indent : '';
68
+ this.pretty = this.indent.length > 0;
69
+ this.assumeFinite =
70
+ typeof checkFinite === 'boolean' ? !checkFinite : !!assumeFinite;
71
+ this.noMutation = !!noMutation;
72
+ this.joinThreshold = Number.isInteger(joinThreshold) && joinThreshold > 0
73
+ ? joinThreshold
74
+ : 8192;
75
+ this.joinChunkSize = Number.isInteger(joinChunkSize) && joinChunkSize > 0
76
+ ? joinChunkSize
77
+ : 2048;
78
+
79
+ this._prefixFirst = this.pretty ? this.indent : '';
80
+ this._prefixNext = this.pretty ? ',\n' + this.indent : ',';
81
+
82
+ this.stream = null;
83
+ this.first = true;
84
+ this._streamError = null;
85
+
86
+ // Formatter + fast-path flag
87
+ this._useJoinFastPath = floatDigits === null;
88
+
89
+ if (!this._useJoinFastPath) {
90
+ const digitsOk = Number.isInteger(floatDigits) && floatDigits >= 0 && floatDigits <= 12;
91
+ const d = digitsOk ? floatDigits : 6;
92
+ const scale = 10 ** d;
93
+ if (this.assumeFinite) {
94
+ this._formatFn = (x) => String(Math.round(x * scale) / scale);
95
+ } else {
96
+ this._formatFn = (x) => {
97
+ if (!Number.isFinite(x)) return '0';
98
+ return String(Math.round(x * scale) / scale);
99
+ };
100
+ }
101
+ } else {
102
+ if (this.assumeFinite) {
103
+ this._formatFn = (x) => String(x);
104
+ } else {
105
+ this._formatFn = (x) => {
106
+ if (!Number.isFinite(x)) return '0';
107
+ return String(x);
108
+ };
109
+ }
110
+ }
111
+ }
112
+
113
+ async writeStart() {
114
+ if (this.stream) return;
115
+
116
+ this.stream = fs.createWriteStream(this.filePath, {
117
+ flags: 'w',
118
+ encoding: 'utf8',
119
+ highWaterMark: this.highWaterMark,
120
+ });
121
+
122
+ this.stream.on('error', (err) => {
123
+ this._streamError = err;
124
+ });
125
+
126
+ await new Promise((resolve, reject) => {
127
+ if (this.stream.fd !== null) return resolve();
128
+ this.stream.once('open', resolve);
129
+ this.stream.once('error', reject);
130
+ });
131
+
132
+ const p = this._writeRaw(this.pretty ? '[\n' : '[');
133
+ if (p) await p;
134
+ this.first = true;
135
+ }
136
+
137
+ /**
138
+ * Best-effort early shutdown (use in catch/finally blocks).
139
+ * Destroys the stream to avoid fd leaks when writeEnd() is not reached.
140
+ */
141
+ abort(err) {
142
+ if (!this.stream) return;
143
+ try {
144
+ this._streamError = err || this._streamError || new Error('StreamingJsonWriter aborted');
145
+ this.stream.destroy(this._streamError);
146
+ } catch {
147
+ // ignore
148
+ } finally {
149
+ this.stream = null;
150
+ }
151
+ }
152
+
153
+ async drain() {
154
+ if (!this.stream || !this.stream.writableNeedDrain) return;
155
+ await onceDrainOrError(this.stream);
156
+ }
157
+
158
+ writeItem(item) {
159
+ if (!this.stream) throw new Error('StreamingJsonWriter not started. Call writeStart() first.');
160
+ if (this._streamError) throw this._streamError;
161
+
162
+ const prefix = this.first ? this._prefixFirst : this._prefixNext;
163
+ this.first = false;
164
+
165
+ const vec = item?.vector;
166
+
167
+ if (isTypedArray(vec)) {
168
+ const base = this.noMutation
169
+ ? this._stringifyWithoutMutation(item, vec)
170
+ : this._stringifyWithoutVector(item, vec);
171
+ const hasBase = typeof base === 'string' && base.length > 0 && base !== '{}';
172
+ const header = hasBase
173
+ ? `${prefix}${base.slice(0, -1)},"vector":`
174
+ : `${prefix}{"vector":`;
175
+
176
+ return this._chain(this._writeRaw(header), () =>
177
+ this._chain(this._writeTypedArray(vec), () => this._writeRaw('}')),
178
+ );
179
+ }
180
+
181
+ return this._writeRaw(prefix + JSON.stringify(item));
182
+ }
183
+
184
+ async writeEnd() {
185
+ if (!this.stream) return;
186
+ if (this._streamError) throw this._streamError;
187
+
188
+ const p = this._writeRaw(this.pretty ? '\n]\n' : ']\n');
189
+ if (p) await p;
190
+
191
+ await new Promise((resolve, reject) => {
192
+ this.stream.once('error', reject);
193
+ this.stream.end(resolve);
194
+ });
195
+
196
+ this.stream = null;
197
+ this._streamError = null;
198
+ }
199
+
200
+ _chain(promise, next) {
201
+ if (promise) return promise.then(() => next());
202
+ return next();
203
+ }
204
+
205
+ _stringifyWithoutVector(item, vec) {
206
+ let base;
207
+ let restored = false;
208
+
209
+ try {
210
+ const prev = item.vector;
211
+ item.vector = undefined;
212
+ base = JSON.stringify(item);
213
+ item.vector = prev;
214
+ restored = true;
215
+ } catch {
216
+ base = JSON.stringify(item, (key, val) =>
217
+ key === 'vector' && val === vec ? undefined : val,
218
+ );
219
+ } finally {
220
+ if (!restored) {
221
+ try {
222
+ item.vector = vec;
223
+ } catch {
224
+ // ignore
225
+ }
226
+ }
227
+ }
228
+
229
+ return base;
230
+ }
231
+
232
+ _stringifyWithoutMutation(item, vec) {
233
+ try {
234
+ const rest = { ...item };
235
+ delete rest.vector;
236
+ return JSON.stringify(rest);
237
+ } catch {
238
+ return JSON.stringify(item, (key, val) =>
239
+ key === 'vector' && val === vec ? undefined : val,
240
+ );
241
+ }
242
+ }
243
+
244
+ /**
245
+ * Core write method.
246
+ * Returns null on synchronous success (fast path).
247
+ * Returns a Promise only when backpressure is hit (slow path).
248
+ */
249
+ _writeRaw(str) {
250
+ if (this._streamError) throw this._streamError;
251
+
252
+ const ok = this.stream.write(str);
253
+ if (ok) return null;
254
+
255
+ return onceDrainOrError(this.stream).then(() => {
256
+ if (this._streamError) throw this._streamError;
257
+ });
258
+ }
259
+
260
+ _writeTypedArray(vec) {
261
+ return this._chain(this._writeRaw('['), () => this._writeTypedArrayBody(vec));
262
+ }
263
+
264
+ _writeTypedArrayBody(vec) {
265
+ if (this._useJoinFastPath) {
266
+ if (!this.assumeFinite && !this._allFinite(vec)) {
267
+ return this._writeFormatted(vec);
268
+ }
269
+
270
+ if (vec.length <= this.joinThreshold) {
271
+ return this._chain(this._writeRaw(vec.join(',')), () => this._writeRaw(']'));
272
+ }
273
+
274
+ return this._writeJoinChunks(vec);
275
+ }
276
+
277
+ return this._writeFormatted(vec);
278
+ }
279
+
280
+ _allFinite(vec) {
281
+ for (let i = 0; i < vec.length; i++) {
282
+ if (!Number.isFinite(vec[i])) return false;
283
+ }
284
+ return true;
285
+ }
286
+
287
+ _writeJoinChunks(vec) {
288
+ const len = vec.length;
289
+ if (len === 0) return this._writeRaw(']');
290
+
291
+ let i = 0;
292
+ const chunkSize = this.joinChunkSize;
293
+
294
+ const writeNext = () => {
295
+ while (i < len) {
296
+ const end = Math.min(len, i + chunkSize);
297
+ let chunk = vec.subarray(i, end).join(',');
298
+ if (i !== 0) chunk = ',' + chunk;
299
+ i = end;
300
+
301
+ const pending = this._writeRaw(chunk);
302
+ if (pending) return pending.then(writeNext);
303
+ }
304
+
305
+ return this._writeRaw(']');
306
+ };
307
+
308
+ return writeNext();
309
+ }
310
+
311
+ _writeFormatted(vec) {
312
+ const len = vec.length;
313
+ if (len === 0) return this._writeRaw(']');
314
+
315
+ let i = 0;
316
+ let buf = '';
317
+ const FLUSH_AT = this.flushChars;
318
+ const format = this._formatFn;
319
+
320
+ const writeNext = () => {
321
+ while (i < len) {
322
+ if (i) buf += ',';
323
+ buf += format(vec[i]);
324
+ i += 1;
325
+
326
+ if (buf.length >= FLUSH_AT) {
327
+ const pending = this._writeRaw(buf);
328
+ buf = '';
329
+ if (pending) return pending.then(writeNext);
330
+ }
331
+ }
332
+
333
+ if (buf) {
334
+ const pending = this._writeRaw(buf);
335
+ buf = '';
336
+ if (pending) return pending.then(() => this._writeRaw(']'));
337
+ }
338
+
339
+ return this._writeRaw(']');
340
+ };
341
+
342
+ return writeNext();
343
+ }
344
+ }
package/lib/logging.js ADDED
@@ -0,0 +1,88 @@
1
+ import fs from 'fs/promises';
2
+ import { createWriteStream } from 'fs';
3
+ import path from 'path';
4
+ import util from 'util';
5
+
6
+ let logStream = null;
7
+ const originalConsole = {
8
+ log: console.info,
9
+ warn: console.warn,
10
+ error: console.error,
11
+ info: console.info,
12
+ };
13
+
14
+ export function enableStderrOnlyLogging() {
15
+ // Keep MCP stdout clean by routing all console output to stderr.
16
+ const redirect = (...args) => originalConsole.error(...args);
17
+ // eslint-disable-next-line no-console
18
+ console.log = redirect; console.info = redirect;
19
+ console.warn = redirect;
20
+ console.error = redirect;
21
+ // eslint-disable-next-line no-console
22
+ console.log = redirect; console.info = redirect;
23
+ }
24
+
25
+ export async function setupFileLogging(config) {
26
+ if (process.env.VITEST === 'true' || process.env.NODE_ENV === 'test') {
27
+ return null;
28
+ }
29
+
30
+ try {
31
+ const logPath = await ensureLogDirectory(config);
32
+ logStream = createWriteStream(logPath, { flags: 'a' });
33
+
34
+ const writeLine = (level, args) => {
35
+ if (!logStream) return;
36
+ const message = util.format(...args);
37
+ // Skip empty lines (spacers) in log files
38
+ if (!message.trim()) return;
39
+
40
+ const timestamp = new Date().toISOString();
41
+ const lines = message
42
+ .split(/\r?\n/)
43
+ .map((line) => line.trimEnd())
44
+ .filter((line) => line.length > 0);
45
+ if (lines.length === 0) return;
46
+ const payload = lines.map((line) => `${timestamp} [${level}] ${line}`).join('\n') + '\n';
47
+ logStream.write(payload);
48
+ };
49
+
50
+ const wrap = (method, level) => {
51
+ const originalError = originalConsole.error;
52
+ // eslint-disable-next-line no-console
53
+ console[method] = (...args) => {
54
+ // Always send to original stderr to avoid MCP protocol pollution on stdout
55
+ originalError(...args);
56
+ writeLine(level, args);
57
+ };
58
+ };
59
+
60
+ wrap('log', 'INFO');
61
+ wrap('warn', 'WARN');
62
+ wrap('error', 'ERROR');
63
+ wrap('info', 'INFO');
64
+
65
+ logStream.on('error', (err) => {
66
+ originalConsole.error(`[Logs] Failed to write log file: ${err.message}`);
67
+ });
68
+
69
+ process.on('exit', () => {
70
+ if (logStream) logStream.end();
71
+ });
72
+
73
+ return logPath;
74
+ } catch (err) {
75
+ originalConsole.error(`[Logs] Failed to initialize log file: ${err.message}`);
76
+ return null;
77
+ }
78
+ }
79
+
80
+ export function getLogFilePath(config) {
81
+ return path.join(config.cacheDirectory, 'logs', 'server.log');
82
+ }
83
+
84
+ export async function ensureLogDirectory(config) {
85
+ const logPath = getLogFilePath(config);
86
+ await fs.mkdir(path.dirname(logPath), { recursive: true });
87
+ return logPath;
88
+ }