@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.
- package/.agent/workflows/code-review.md +60 -0
- package/.prettierrc +7 -0
- package/ARCHITECTURE.md +105 -170
- package/CONTRIBUTING.md +32 -113
- package/GEMINI.md +73 -0
- package/LICENSE +21 -21
- package/README.md +161 -54
- package/config.json +876 -75
- package/debug-pids.js +27 -0
- package/eslint.config.js +36 -0
- package/features/ann-config.js +37 -26
- package/features/clear-cache.js +28 -19
- package/features/find-similar-code.js +142 -66
- package/features/hybrid-search.js +253 -93
- package/features/index-codebase.js +1455 -394
- package/features/lifecycle.js +813 -180
- package/features/register.js +58 -52
- package/index.js +450 -306
- package/lib/cache-ops.js +22 -0
- package/lib/cache-utils.js +68 -0
- package/lib/cache.js +1392 -587
- package/lib/call-graph.js +165 -50
- package/lib/cli.js +154 -0
- package/lib/config.js +462 -121
- package/lib/embedding-process.js +77 -0
- package/lib/embedding-worker.js +545 -30
- package/lib/ignore-patterns.js +61 -59
- package/lib/json-worker.js +14 -0
- package/lib/json-writer.js +344 -0
- package/lib/logging.js +88 -0
- package/lib/memory-logger.js +13 -0
- package/lib/project-detector.js +13 -17
- package/lib/server-lifecycle.js +38 -0
- package/lib/settings-editor.js +645 -0
- package/lib/tokenizer.js +207 -104
- package/lib/utils.js +273 -198
- package/lib/vector-store-binary.js +592 -0
- package/mcp_config.example.json +13 -0
- package/package.json +13 -2
- package/scripts/clear-cache.js +6 -17
- package/scripts/download-model.js +14 -9
- package/scripts/postinstall.js +5 -5
- package/search-configs.js +36 -0
- package/test/ann-config.test.js +179 -0
- package/test/ann-fallback.test.js +6 -6
- package/test/binary-store.test.js +69 -0
- package/test/cache-branches.test.js +120 -0
- package/test/cache-errors.test.js +264 -0
- package/test/cache-extra.test.js +300 -0
- package/test/cache-helpers.test.js +205 -0
- package/test/cache-hnsw-failure.test.js +40 -0
- package/test/cache-json-worker.test.js +190 -0
- package/test/cache-worker.test.js +102 -0
- package/test/cache.test.js +443 -0
- package/test/call-graph.test.js +103 -4
- package/test/clear-cache.test.js +69 -68
- package/test/code-review-workflow.test.js +50 -0
- package/test/config.test.js +418 -0
- package/test/coverage-gap.test.js +497 -0
- package/test/coverage-maximizer.test.js +236 -0
- package/test/debug-analysis.js +107 -0
- package/test/embedding-model.test.js +173 -103
- package/test/embedding-worker-extra.test.js +272 -0
- package/test/embedding-worker.test.js +158 -0
- package/test/features.test.js +139 -0
- package/test/final-boost.test.js +271 -0
- package/test/final-polish.test.js +183 -0
- package/test/final.test.js +95 -0
- package/test/find-similar-code.test.js +191 -0
- package/test/helpers.js +92 -11
- package/test/helpers.test.js +46 -0
- package/test/hybrid-search-basic.test.js +62 -0
- package/test/hybrid-search-branch.test.js +202 -0
- package/test/hybrid-search-callgraph.test.js +229 -0
- package/test/hybrid-search-extra.test.js +81 -0
- package/test/hybrid-search.test.js +484 -71
- package/test/index-cli.test.js +520 -0
- package/test/index-codebase-batch.test.js +119 -0
- package/test/index-codebase-branches.test.js +585 -0
- package/test/index-codebase-core.test.js +1032 -0
- package/test/index-codebase-edge-cases.test.js +254 -0
- package/test/index-codebase-errors.test.js +132 -0
- package/test/index-codebase-gap.test.js +239 -0
- package/test/index-codebase-lines.test.js +151 -0
- package/test/index-codebase-watcher.test.js +259 -0
- package/test/index-codebase-zone.test.js +259 -0
- package/test/index-codebase.test.js +371 -69
- package/test/index-memory.test.js +220 -0
- package/test/indexer-detailed.test.js +176 -0
- package/test/integration.test.js +148 -92
- package/test/json-worker.test.js +50 -0
- package/test/lifecycle.test.js +541 -0
- package/test/master.test.js +198 -0
- package/test/perfection.test.js +349 -0
- package/test/project-detector.test.js +65 -0
- package/test/register.test.js +262 -0
- package/test/tokenizer.test.js +55 -93
- package/test/ultra-maximizer.test.js +116 -0
- package/test/utils-branches.test.js +161 -0
- package/test/utils-extra.test.js +116 -0
- package/test/utils.test.js +131 -0
- package/test/verify_fixes.js +76 -0
- package/test/worker-errors.test.js +96 -0
- package/test/worker-init.test.js +102 -0
- package/test/worker_throttling.test.js +93 -0
- package/tools/scripts/benchmark-search.js +95 -0
- package/tools/scripts/cache-stats.js +71 -0
- package/tools/scripts/manual-search.js +34 -0
- package/vitest.config.js +19 -9
package/lib/ignore-patterns.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
+
}
|