@j0hanz/filesystem-mcp 1.13.2 → 1.14.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.
- package/README.md +162 -145
- package/dist/cli.js +2 -2
- package/dist/completions.js +54 -51
- package/dist/config.d.ts +13 -14
- package/dist/config.js +12 -12
- package/dist/index.js +1 -1
- package/dist/lib/abort.d.ts +7 -0
- package/dist/lib/abort.js +81 -0
- package/dist/lib/constants.d.ts +3 -1
- package/dist/lib/constants.js +8 -2
- package/dist/lib/errors.d.ts +7 -3
- package/dist/lib/errors.js +64 -41
- package/dist/lib/file-operations/core.d.ts +3 -3
- package/dist/lib/file-operations/core.js +23 -20
- package/dist/lib/file-operations/metadata.d.ts +2 -2
- package/dist/lib/file-operations/metadata.js +69 -22
- package/dist/lib/file-operations/search.d.ts +0 -1
- package/dist/lib/file-operations/search.js +87 -95
- package/dist/lib/file-operations/traversal.js +13 -15
- package/dist/lib/fs-helpers.d.ts +3 -10
- package/dist/lib/fs-helpers.js +29 -108
- package/dist/lib/globs.d.ts +2 -0
- package/dist/lib/globs.js +19 -0
- package/dist/lib/logger.d.ts +28 -0
- package/dist/lib/logger.js +91 -0
- package/dist/lib/observability.d.ts +7 -0
- package/dist/lib/observability.js +19 -9
- package/dist/lib/paths.js +55 -55
- package/dist/lib/resource-store.js +4 -4
- package/dist/lib/utils.d.ts +0 -12
- package/dist/lib/utils.js +0 -13
- package/dist/lib/zod-codecs.d.ts +2 -0
- package/dist/lib/zod-codecs.js +18 -0
- package/dist/pkg-info.d.ts +1 -0
- package/dist/pkg-info.js +2 -2
- package/dist/prompts.js +3 -3
- package/dist/resources/generated-instructions.js +41 -41
- package/dist/resources/tool-catalog.js +33 -58
- package/dist/resources/tool-info.d.ts +0 -1
- package/dist/resources/tool-info.js +44 -67
- package/dist/resources/workflows.js +47 -19
- package/dist/resources.d.ts +1 -1
- package/dist/resources.js +4 -4
- package/dist/schemas.d.ts +185 -465
- package/dist/schemas.js +174 -206
- package/dist/server/bootstrap.d.ts +12 -11
- package/dist/server/bootstrap.js +95 -86
- package/dist/server/roots-manager.d.ts +5 -2
- package/dist/server/roots-manager.js +9 -7
- package/dist/server/task-store.d.ts +10 -0
- package/dist/server/task-store.js +73 -0
- package/dist/tools/apply-patch.js +39 -20
- package/dist/tools/calculate-hash.js +14 -27
- package/dist/tools/create-directory.js +11 -9
- package/dist/tools/delete-file.js +19 -19
- package/dist/tools/diff-files.js +16 -18
- package/dist/tools/edit-file.js +11 -5
- package/dist/tools/list-directory.js +16 -21
- package/dist/tools/move-file.js +105 -100
- package/dist/tools/read-multiple.js +15 -10
- package/dist/tools/read.js +6 -7
- package/dist/tools/replace-in-files.js +76 -115
- package/dist/tools/roots.js +3 -7
- package/dist/tools/search-content.js +158 -203
- package/dist/tools/search-files.js +59 -50
- package/dist/tools/shared.d.ts +10 -0
- package/dist/tools/shared.js +105 -36
- package/dist/tools/stat-many.js +15 -9
- package/dist/tools/stat.js +6 -6
- package/dist/tools/task-support.d.ts +10 -9
- package/dist/tools/task-support.js +94 -23
- package/dist/tools/tree.js +4 -4
- package/dist/tools/write-file.js +11 -12
- package/package.json +10 -9
package/dist/lib/fs-helpers.js
CHANGED
|
@@ -50,36 +50,16 @@ var __disposeResources = (this && this.__disposeResources) || (function (Suppres
|
|
|
50
50
|
var e = new Error(message);
|
|
51
51
|
return e.name = "SuppressedError", e.error = error, e.suppressed = suppressed, e;
|
|
52
52
|
});
|
|
53
|
-
import * as fsp from 'node:fs/promises';
|
|
54
|
-
import * as path from 'node:path';
|
|
55
53
|
import { isUtf8 } from 'node:buffer';
|
|
56
54
|
import { randomUUID } from 'node:crypto';
|
|
55
|
+
import { open, rename, stat, unlink, writeFile, } from 'node:fs/promises';
|
|
56
|
+
import { extname } from 'node:path';
|
|
57
|
+
import { assertNotAborted, withAbort } from './abort.js';
|
|
57
58
|
import { BINARY_CHECK_BUFFER_SIZE, KNOWN_BINARY_EXTENSIONS, MAX_TEXT_FILE_SIZE, PARALLEL_CONCURRENCY, } from './constants.js';
|
|
58
59
|
import { ErrorCode, McpError, normalizeUnknownError } from './errors.js';
|
|
59
60
|
import { assertAllowedFileAccess, validateExistingPath } from './paths.js';
|
|
60
|
-
|
|
61
|
-
return new DOMException(message, 'AbortError');
|
|
62
|
-
}
|
|
61
|
+
export { assertNotAborted, createTimedAbortSignal, withAbort, withTimedAbortSignal, } from './abort.js';
|
|
63
62
|
const READ_ONLY_FILE_FLAG = 'r';
|
|
64
|
-
const SHARED_NOOP_SIGNAL = new AbortController().signal;
|
|
65
|
-
function normalizeAbortReason(reason, message) {
|
|
66
|
-
if (reason instanceof Error)
|
|
67
|
-
return reason;
|
|
68
|
-
return createAbortError(message);
|
|
69
|
-
}
|
|
70
|
-
function isFiniteNumber(value) {
|
|
71
|
-
return typeof value === 'number' && Number.isFinite(value);
|
|
72
|
-
}
|
|
73
|
-
export function assertNotAborted(signal, message) {
|
|
74
|
-
if (!signal)
|
|
75
|
-
return;
|
|
76
|
-
try {
|
|
77
|
-
signal.throwIfAborted();
|
|
78
|
-
}
|
|
79
|
-
catch (reason) {
|
|
80
|
-
throw normalizeAbortReason(reason, message);
|
|
81
|
-
}
|
|
82
|
-
}
|
|
83
63
|
function assertPositiveSafeIntegerOption(name, value, message) {
|
|
84
64
|
if (value === undefined)
|
|
85
65
|
return;
|
|
@@ -87,73 +67,15 @@ function assertPositiveSafeIntegerOption(name, value, message) {
|
|
|
87
67
|
!Number.isFinite(value) ||
|
|
88
68
|
!Number.isSafeInteger(value) ||
|
|
89
69
|
value < 1) {
|
|
90
|
-
throw new McpError(ErrorCode.
|
|
70
|
+
throw new McpError(ErrorCode.INVALID_INPUT, message ?? `${name} must be a positive integer`);
|
|
91
71
|
}
|
|
92
72
|
}
|
|
93
73
|
function normalizeConcurrency(concurrency) {
|
|
94
74
|
assertPositiveSafeIntegerOption('concurrency', concurrency);
|
|
95
75
|
return concurrency;
|
|
96
76
|
}
|
|
97
|
-
function getAbortError(signal, message) {
|
|
98
|
-
try {
|
|
99
|
-
signal.throwIfAborted();
|
|
100
|
-
}
|
|
101
|
-
catch (reason) {
|
|
102
|
-
return normalizeAbortReason(reason, message);
|
|
103
|
-
}
|
|
104
|
-
return createAbortError(message);
|
|
105
|
-
}
|
|
106
|
-
export function withAbort(promise, signal) {
|
|
107
|
-
if (!signal)
|
|
108
|
-
return promise;
|
|
109
|
-
signal.throwIfAborted();
|
|
110
|
-
return new Promise((resolve, reject) => {
|
|
111
|
-
const onAbort = () => {
|
|
112
|
-
reject(getAbortError(signal));
|
|
113
|
-
};
|
|
114
|
-
if (signal.aborted) {
|
|
115
|
-
onAbort();
|
|
116
|
-
return;
|
|
117
|
-
}
|
|
118
|
-
signal.addEventListener('abort', onAbort, { once: true });
|
|
119
|
-
promise.then((value) => {
|
|
120
|
-
signal.removeEventListener('abort', onAbort);
|
|
121
|
-
resolve(value);
|
|
122
|
-
}, (error) => {
|
|
123
|
-
signal.removeEventListener('abort', onAbort);
|
|
124
|
-
reject(normalizeUnknownError(error));
|
|
125
|
-
});
|
|
126
|
-
});
|
|
127
|
-
}
|
|
128
|
-
export function createTimedAbortSignal(baseSignal, timeoutMs) {
|
|
129
|
-
const timeoutSignal = isFiniteNumber(timeoutMs)
|
|
130
|
-
? AbortSignal.timeout(timeoutMs)
|
|
131
|
-
: undefined;
|
|
132
|
-
if (baseSignal && timeoutSignal) {
|
|
133
|
-
return {
|
|
134
|
-
signal: AbortSignal.any([baseSignal, timeoutSignal]),
|
|
135
|
-
cleanup: () => { },
|
|
136
|
-
};
|
|
137
|
-
}
|
|
138
|
-
if (baseSignal) {
|
|
139
|
-
return { signal: baseSignal, cleanup: () => { } };
|
|
140
|
-
}
|
|
141
|
-
if (timeoutSignal) {
|
|
142
|
-
return { signal: timeoutSignal, cleanup: () => { } };
|
|
143
|
-
}
|
|
144
|
-
return { signal: SHARED_NOOP_SIGNAL, cleanup: () => { } };
|
|
145
|
-
}
|
|
146
|
-
export async function withTimedAbortSignal(baseSignal, timeoutMs, run) {
|
|
147
|
-
const { signal, cleanup } = createTimedAbortSignal(baseSignal, timeoutMs);
|
|
148
|
-
try {
|
|
149
|
-
return await run(signal);
|
|
150
|
-
}
|
|
151
|
-
finally {
|
|
152
|
-
cleanup();
|
|
153
|
-
}
|
|
154
|
-
}
|
|
155
77
|
function createParallelAbortError() {
|
|
156
|
-
return
|
|
78
|
+
return new DOMException('Operation aborted', 'AbortError');
|
|
157
79
|
}
|
|
158
80
|
export async function processInParallel(items, processor, concurrency = PARALLEL_CONCURRENCY, signal) {
|
|
159
81
|
const itemCount = items.length;
|
|
@@ -218,11 +140,11 @@ export function isHidden(name) {
|
|
|
218
140
|
return name.startsWith('.');
|
|
219
141
|
}
|
|
220
142
|
function hasKnownBinaryExtension(filePath) {
|
|
221
|
-
const ext =
|
|
143
|
+
const ext = extname(filePath).toLowerCase();
|
|
222
144
|
return KNOWN_BINARY_EXTENSIONS.has(ext);
|
|
223
145
|
}
|
|
224
146
|
async function openReadableFileHandle(filePath, signal) {
|
|
225
|
-
return withAbort(
|
|
147
|
+
return withAbort(open(filePath, READ_ONLY_FILE_FLAG), signal);
|
|
226
148
|
}
|
|
227
149
|
async function readProbe(handle, signal) {
|
|
228
150
|
const buffer = Buffer.alloc(BINARY_CHECK_BUFFER_SIZE);
|
|
@@ -282,18 +204,16 @@ function validateReadOptions(options) {
|
|
|
282
204
|
assertPositiveSafeIntegerOption('startLine', options.startLine, 'startLine must be at least 1');
|
|
283
205
|
assertPositiveSafeIntegerOption('endLine', options.endLine, 'endLine must be at least 1');
|
|
284
206
|
if (hasHead && (hasStart || hasEnd)) {
|
|
285
|
-
throw new McpError(ErrorCode.
|
|
207
|
+
throw new McpError(ErrorCode.INVALID_INPUT, 'head cannot be used together with startLine/endLine');
|
|
286
208
|
}
|
|
287
209
|
if (hasTail && (hasHead || hasStart || hasEnd)) {
|
|
288
|
-
throw new McpError(ErrorCode.
|
|
289
|
-
}
|
|
290
|
-
if (hasEnd && !hasStart) {
|
|
291
|
-
throw new McpError(ErrorCode.E_INVALID_INPUT, 'endLine requires startLine');
|
|
210
|
+
throw new McpError(ErrorCode.INVALID_INPUT, 'tail cannot be used together with head/startLine/endLine');
|
|
292
211
|
}
|
|
293
|
-
|
|
294
|
-
options.
|
|
295
|
-
options.endLine
|
|
296
|
-
|
|
212
|
+
{
|
|
213
|
+
const effectiveStart = options.startLine ?? 1;
|
|
214
|
+
if (options.endLine !== undefined && options.endLine < effectiveStart) {
|
|
215
|
+
throw new McpError(ErrorCode.INVALID_INPUT, 'endLine must be greater than or equal to startLine (default: 1)');
|
|
216
|
+
}
|
|
297
217
|
}
|
|
298
218
|
}
|
|
299
219
|
function normalizeOptions(options) {
|
|
@@ -309,12 +229,13 @@ function normalizeOptions(options) {
|
|
|
309
229
|
if (options.tail !== undefined) {
|
|
310
230
|
normalized.tail = options.tail;
|
|
311
231
|
}
|
|
312
|
-
if (options.startLine !== undefined) {
|
|
313
|
-
normalized.startLine = options.startLine;
|
|
314
|
-
}
|
|
315
232
|
if (options.endLine !== undefined) {
|
|
233
|
+
normalized.startLine = options.startLine ?? 1;
|
|
316
234
|
normalized.endLine = options.endLine;
|
|
317
235
|
}
|
|
236
|
+
else if (options.startLine !== undefined) {
|
|
237
|
+
normalized.startLine = options.startLine;
|
|
238
|
+
}
|
|
318
239
|
if (options.signal) {
|
|
319
240
|
normalized.signal = options.signal;
|
|
320
241
|
}
|
|
@@ -340,13 +261,13 @@ function resolveReadMode(options) {
|
|
|
340
261
|
return 'head';
|
|
341
262
|
if (options.tail !== undefined)
|
|
342
263
|
return 'tail';
|
|
343
|
-
if (options.startLine !== undefined)
|
|
264
|
+
if (options.startLine !== undefined || options.endLine !== undefined)
|
|
344
265
|
return 'range';
|
|
345
266
|
return 'full';
|
|
346
267
|
}
|
|
347
268
|
const STREAM_CHUNK_SIZE = 64 * 1024;
|
|
348
269
|
function createTooLargeError(bytesRead, maxSize, requestedPath) {
|
|
349
|
-
return new McpError(ErrorCode.
|
|
270
|
+
return new McpError(ErrorCode.TOO_LARGE, `File exceeds size limit (${bytesRead} > ${maxSize} bytes)`, requestedPath, { size: bytesRead, maxSize });
|
|
350
271
|
}
|
|
351
272
|
async function readFileBufferWithLimit(handle, maxSize, requestedPath, signal) {
|
|
352
273
|
const stream = handle.createReadStream({
|
|
@@ -522,19 +443,19 @@ async function assertNotBinary(validPath, filePath, normalized) {
|
|
|
522
443
|
const isBinary = await isProbablyBinary(validPath, undefined, normalized.signal);
|
|
523
444
|
if (!isBinary)
|
|
524
445
|
return;
|
|
525
|
-
throw new McpError(ErrorCode.
|
|
446
|
+
throw new McpError(ErrorCode.INVALID_INPUT, 'Binary file detected.', filePath);
|
|
526
447
|
}
|
|
527
448
|
function assertSizeWithinLimit(size, maxSize, filePath) {
|
|
528
449
|
if (size <= maxSize)
|
|
529
450
|
return;
|
|
530
|
-
throw new McpError(ErrorCode.
|
|
451
|
+
throw new McpError(ErrorCode.TOO_LARGE, `File too large (${size} > ${maxSize} bytes). Use head to preview.`, filePath, { size, maxSize });
|
|
531
452
|
}
|
|
532
453
|
function requireReadOption(normalized, key, filePath) {
|
|
533
454
|
const value = normalized[key];
|
|
534
455
|
if (value !== undefined) {
|
|
535
456
|
return value;
|
|
536
457
|
}
|
|
537
|
-
throw new McpError(ErrorCode.
|
|
458
|
+
throw new McpError(ErrorCode.INVALID_INPUT, `Missing ${key} option`, filePath);
|
|
538
459
|
}
|
|
539
460
|
async function executeHeadRead(context) {
|
|
540
461
|
const head = requireReadOption(context.normalized, 'head', context.filePath);
|
|
@@ -605,7 +526,7 @@ async function readByMode(context) {
|
|
|
605
526
|
}
|
|
606
527
|
function assertFileStats(filePath, stats) {
|
|
607
528
|
if (!stats.isFile()) {
|
|
608
|
-
throw new McpError(ErrorCode.
|
|
529
|
+
throw new McpError(ErrorCode.NOT_FILE, 'Not a regular file', filePath);
|
|
609
530
|
}
|
|
610
531
|
}
|
|
611
532
|
async function readFileWithStatsInternal(filePath, validPath, stats, normalized) {
|
|
@@ -645,7 +566,7 @@ export async function readFile(filePath, options = {}) {
|
|
|
645
566
|
const normalized = prepareReadOptions(options);
|
|
646
567
|
const validPath = await validateExistingPath(filePath, normalized.signal);
|
|
647
568
|
assertNotAborted(normalized.signal);
|
|
648
|
-
const stats = await withAbort(
|
|
569
|
+
const stats = await withAbort(stat(validPath), normalized.signal);
|
|
649
570
|
return readFileWithStatsInternal(filePath, validPath, stats, normalized);
|
|
650
571
|
}
|
|
651
572
|
export async function atomicWriteFile(filePath, content, options = {}) {
|
|
@@ -653,13 +574,13 @@ export async function atomicWriteFile(filePath, content, options = {}) {
|
|
|
653
574
|
const tempPath = `${filePath}.${randomUUID()}.tmp`;
|
|
654
575
|
try {
|
|
655
576
|
assertNotAborted(signal);
|
|
656
|
-
await
|
|
657
|
-
await withAbort(
|
|
577
|
+
await writeFile(tempPath, content, { encoding, signal });
|
|
578
|
+
await withAbort(rename(tempPath, filePath), signal);
|
|
658
579
|
}
|
|
659
580
|
catch (error) {
|
|
660
581
|
// Attempt cleanup on error, but don't overwrite the original error
|
|
661
582
|
try {
|
|
662
|
-
await
|
|
583
|
+
await unlink(tempPath).catch(() => { });
|
|
663
584
|
}
|
|
664
585
|
catch {
|
|
665
586
|
// Ignore cleanup errors
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { ErrorCode, McpError } from './errors.js';
|
|
2
|
+
const ABSOLUTE_GLOB_RE = /^([/\\]|[A-Za-z]:[/\\]|\\\\)/u;
|
|
3
|
+
const PARENT_SEGMENT_RE = /[\\/]\.\.(?:[/\\]|$)/u;
|
|
4
|
+
export function isSafeGlobPattern(value) {
|
|
5
|
+
if (value.length === 0)
|
|
6
|
+
return false;
|
|
7
|
+
if (value.includes('**/**/**'))
|
|
8
|
+
return false;
|
|
9
|
+
if (ABSOLUTE_GLOB_RE.test(value))
|
|
10
|
+
return false;
|
|
11
|
+
if (value.startsWith('..') || PARENT_SEGMENT_RE.test(value))
|
|
12
|
+
return false;
|
|
13
|
+
return true;
|
|
14
|
+
}
|
|
15
|
+
export function assertSafeGlobPattern(value, message = 'Invalid glob or unsafe path (absolute/.. forbidden)') {
|
|
16
|
+
if (!isSafeGlobPattern(value)) {
|
|
17
|
+
throw new McpError(ErrorCode.INVALID_PATTERN, message);
|
|
18
|
+
}
|
|
19
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
2
|
+
import type { LoggingLevel } from '@modelcontextprotocol/sdk/types.js';
|
|
3
|
+
import { AsyncLocalStorage } from 'node:async_hooks';
|
|
4
|
+
export interface SessionContextData {
|
|
5
|
+
sessionId?: string;
|
|
6
|
+
}
|
|
7
|
+
export declare const SessionContext: AsyncLocalStorage<SessionContextData>;
|
|
8
|
+
export interface LogEvent {
|
|
9
|
+
level: LoggingLevel;
|
|
10
|
+
message: string;
|
|
11
|
+
data?: unknown;
|
|
12
|
+
sessionId?: string;
|
|
13
|
+
}
|
|
14
|
+
export interface LoggingState {
|
|
15
|
+
minimumLevel: LoggingLevel;
|
|
16
|
+
}
|
|
17
|
+
export declare function createLoggingState(minimumLevel?: LoggingLevel): LoggingState;
|
|
18
|
+
export declare function canSendMcpLogs(server: McpServer): boolean;
|
|
19
|
+
export declare function logToMcp(server: McpServer | undefined, level: LoggingLevel, data: string, minLevel?: LoggingLevel): void;
|
|
20
|
+
export declare const Logger: {
|
|
21
|
+
emit(level: LoggingLevel, message: string, data?: unknown): void;
|
|
22
|
+
debug(message: string, data?: unknown): void;
|
|
23
|
+
info(message: string, data?: unknown): void;
|
|
24
|
+
notice(message: string, data?: unknown): void;
|
|
25
|
+
warn(message: string, data?: unknown): void;
|
|
26
|
+
error(message: string, data?: unknown): void;
|
|
27
|
+
critical(message: string, data?: unknown): void;
|
|
28
|
+
};
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import { AsyncLocalStorage } from 'node:async_hooks';
|
|
2
|
+
import { channel } from 'node:diagnostics_channel';
|
|
3
|
+
export const SessionContext = new AsyncLocalStorage();
|
|
4
|
+
const LOG_CHANNEL = channel('filesystem-mcp:log');
|
|
5
|
+
const MCP_LOGGER_NAME = 'filesystem-mcp';
|
|
6
|
+
const LOG_LEVEL_ORDER = {
|
|
7
|
+
debug: 0,
|
|
8
|
+
info: 1,
|
|
9
|
+
notice: 2,
|
|
10
|
+
warning: 3,
|
|
11
|
+
error: 4,
|
|
12
|
+
critical: 5,
|
|
13
|
+
alert: 6,
|
|
14
|
+
emergency: 7,
|
|
15
|
+
};
|
|
16
|
+
export function createLoggingState(minimumLevel = 'debug') {
|
|
17
|
+
return { minimumLevel };
|
|
18
|
+
}
|
|
19
|
+
export function canSendMcpLogs(server) {
|
|
20
|
+
const capabilities = server.server.getClientCapabilities();
|
|
21
|
+
if (!capabilities || typeof capabilities !== 'object')
|
|
22
|
+
return false;
|
|
23
|
+
return 'logging' in capabilities && Boolean(capabilities.logging);
|
|
24
|
+
}
|
|
25
|
+
export function logToMcp(server, level, data, minLevel = 'debug') {
|
|
26
|
+
if (LOG_LEVEL_ORDER[level] < LOG_LEVEL_ORDER[minLevel]) {
|
|
27
|
+
return;
|
|
28
|
+
}
|
|
29
|
+
if (!server || !canSendMcpLogs(server)) {
|
|
30
|
+
console.error(`[${level.toUpperCase()}] ${data}`);
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
const params = {
|
|
34
|
+
level,
|
|
35
|
+
logger: MCP_LOGGER_NAME,
|
|
36
|
+
data,
|
|
37
|
+
};
|
|
38
|
+
void server.sendLoggingMessage(params).catch((error) => {
|
|
39
|
+
console.error(`Failed to send MCP log: ${level} | ${data}`, formatTransportError(error));
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
function formatTransportError(error) {
|
|
43
|
+
if (error instanceof Error)
|
|
44
|
+
return error.message;
|
|
45
|
+
if (typeof error === 'string')
|
|
46
|
+
return error;
|
|
47
|
+
try {
|
|
48
|
+
return JSON.stringify(error);
|
|
49
|
+
}
|
|
50
|
+
catch {
|
|
51
|
+
return String(error);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
export const Logger = {
|
|
55
|
+
emit(level, message, data) {
|
|
56
|
+
const session = SessionContext.getStore();
|
|
57
|
+
const event = {
|
|
58
|
+
level,
|
|
59
|
+
message,
|
|
60
|
+
...(data !== undefined ? { data } : {}),
|
|
61
|
+
...(session?.sessionId !== undefined
|
|
62
|
+
? { sessionId: session.sessionId }
|
|
63
|
+
: {}),
|
|
64
|
+
};
|
|
65
|
+
if (LOG_CHANNEL.hasSubscribers) {
|
|
66
|
+
LOG_CHANNEL.publish(event);
|
|
67
|
+
}
|
|
68
|
+
else {
|
|
69
|
+
// Fallback if no subscribers
|
|
70
|
+
console.error(`[${level.toUpperCase()}] ${message}`, data ?? '');
|
|
71
|
+
}
|
|
72
|
+
},
|
|
73
|
+
debug(message, data) {
|
|
74
|
+
this.emit('debug', message, data);
|
|
75
|
+
},
|
|
76
|
+
info(message, data) {
|
|
77
|
+
this.emit('info', message, data);
|
|
78
|
+
},
|
|
79
|
+
notice(message, data) {
|
|
80
|
+
this.emit('notice', message, data);
|
|
81
|
+
},
|
|
82
|
+
warn(message, data) {
|
|
83
|
+
this.emit('warning', message, data);
|
|
84
|
+
},
|
|
85
|
+
error(message, data) {
|
|
86
|
+
this.emit('error', message, data);
|
|
87
|
+
},
|
|
88
|
+
critical(message, data) {
|
|
89
|
+
this.emit('critical', message, data);
|
|
90
|
+
},
|
|
91
|
+
};
|
|
@@ -5,6 +5,11 @@ interface OpsTraceContext {
|
|
|
5
5
|
path?: string | undefined;
|
|
6
6
|
[key: string]: unknown;
|
|
7
7
|
}
|
|
8
|
+
export interface TraceContext {
|
|
9
|
+
traceparent: string;
|
|
10
|
+
tracestate?: string;
|
|
11
|
+
baggage?: string;
|
|
12
|
+
}
|
|
8
13
|
interface ToolMetrics {
|
|
9
14
|
calls: number;
|
|
10
15
|
errors: number;
|
|
@@ -19,8 +24,10 @@ export declare function getToolContextSnapshot(): {
|
|
|
19
24
|
tool: string;
|
|
20
25
|
path?: string;
|
|
21
26
|
} | undefined;
|
|
27
|
+
export declare function getTraceContext(): TraceContext | undefined;
|
|
22
28
|
export declare function startPerfMeasure(name: string, detail?: Record<string, unknown>): ((ok?: boolean) => void) | undefined;
|
|
23
29
|
export declare function withToolDiagnostics<T>(tool: string, run: () => Promise<T>, options?: {
|
|
24
30
|
path?: string;
|
|
31
|
+
traceContext?: TraceContext;
|
|
25
32
|
}): Promise<T>;
|
|
26
33
|
export {};
|
|
@@ -3,16 +3,18 @@ import { hash } from 'node:crypto';
|
|
|
3
3
|
import { channel, tracingChannel } from 'node:diagnostics_channel';
|
|
4
4
|
import { monitorEventLoopDelay, performance, PerformanceObserver, } from 'node:perf_hooks';
|
|
5
5
|
import { parseTrueEnvFlag } from './constants.js';
|
|
6
|
+
import { Logger } from './logger.js';
|
|
6
7
|
import { isRecord } from './utils.js';
|
|
7
8
|
// --- Configuration ---
|
|
8
9
|
const ENV = process.env;
|
|
9
10
|
let _cachedConfig;
|
|
10
11
|
function readConfig() {
|
|
11
|
-
|
|
12
|
+
_cachedConfig ??= {
|
|
12
13
|
enabled: parseTrueEnvFlag(ENV['FS_CONTEXT_DIAGNOSTICS']),
|
|
13
14
|
detail: parseDetail(ENV['FS_CONTEXT_DIAGNOSTICS_DETAIL']),
|
|
14
15
|
logToolErrors: parseTrueEnvFlag(ENV['FS_CONTEXT_TOOL_LOG_ERRORS']),
|
|
15
|
-
}
|
|
16
|
+
};
|
|
17
|
+
return _cachedConfig;
|
|
16
18
|
}
|
|
17
19
|
function parseDetail(val) {
|
|
18
20
|
if (val === '2')
|
|
@@ -201,6 +203,9 @@ function applyToolContext(context) {
|
|
|
201
203
|
export function getToolContextSnapshot() {
|
|
202
204
|
return toolContext.getStore();
|
|
203
205
|
}
|
|
206
|
+
export function getTraceContext() {
|
|
207
|
+
return toolContext.getStore()?.traceContext;
|
|
208
|
+
}
|
|
204
209
|
function normalizeContext(ctx) {
|
|
205
210
|
if (!ctx.path)
|
|
206
211
|
return ctx;
|
|
@@ -254,16 +259,20 @@ export function startPerfMeasure(name, detail) {
|
|
|
254
259
|
}
|
|
255
260
|
};
|
|
256
261
|
}
|
|
257
|
-
function publishToolStart(tool, pathVal) {
|
|
262
|
+
function publishToolStart(tool, pathVal, traceparent) {
|
|
258
263
|
const event = { phase: 'start', tool };
|
|
259
264
|
if (pathVal)
|
|
260
265
|
event.path = pathVal;
|
|
266
|
+
if (traceparent)
|
|
267
|
+
event.traceparent = traceparent;
|
|
261
268
|
CHANNELS.tool.publish(event);
|
|
262
269
|
}
|
|
263
|
-
function publishToolEnd(tool, ok, durationMs, errorMsg) {
|
|
270
|
+
function publishToolEnd(tool, ok, durationMs, errorMsg, traceparent) {
|
|
264
271
|
const event = { phase: 'end', tool, ok, durationMs };
|
|
265
272
|
if (errorMsg)
|
|
266
273
|
event.error = errorMsg;
|
|
274
|
+
if (traceparent)
|
|
275
|
+
event.traceparent = traceparent;
|
|
267
276
|
CHANNELS.tool.publish(event);
|
|
268
277
|
}
|
|
269
278
|
function publishPerfEnd(tool, durationMs, eluStart, loopMonitor) {
|
|
@@ -281,13 +290,13 @@ function publishPerfEnd(tool, durationMs, eluStart, loopMonitor) {
|
|
|
281
290
|
}
|
|
282
291
|
CHANNELS.perf.publish(event);
|
|
283
292
|
}
|
|
284
|
-
async function runAndObserve(tool, run, pubTool, pubPerf, logErrors, pathVal) {
|
|
293
|
+
async function runAndObserve(tool, run, pubTool, pubPerf, logErrors, pathVal, traceparent) {
|
|
285
294
|
const startMs = performance.now();
|
|
286
295
|
const eluStart = pubPerf ? performance.eventLoopUtilization() : undefined;
|
|
287
296
|
const loopMonitor = pubPerf ? monitorEventLoopDelay() : undefined;
|
|
288
297
|
loopMonitor?.enable();
|
|
289
298
|
if (pubTool)
|
|
290
|
-
publishToolStart(tool, pathVal);
|
|
299
|
+
publishToolStart(tool, pathVal, traceparent);
|
|
291
300
|
let result;
|
|
292
301
|
const obs = { ok: false, errorMsg: undefined };
|
|
293
302
|
try {
|
|
@@ -306,7 +315,7 @@ async function runAndObserve(tool, run, pubTool, pubPerf, logErrors, pathVal) {
|
|
|
306
315
|
if (pubPerf && eluStart)
|
|
307
316
|
publishPerfEnd(tool, durationMs, eluStart, loopMonitor);
|
|
308
317
|
if (pubTool)
|
|
309
|
-
publishToolEnd(tool, obs.ok, durationMs, obs.errorMsg);
|
|
318
|
+
publishToolEnd(tool, obs.ok, durationMs, obs.errorMsg, traceparent);
|
|
310
319
|
updateMetrics(tool, obs.ok, durationMs);
|
|
311
320
|
if (logErrors && !obs.ok)
|
|
312
321
|
logError(tool, durationMs, obs.errorMsg);
|
|
@@ -319,6 +328,7 @@ export async function withToolDiagnostics(tool, run, options) {
|
|
|
319
328
|
const context = {
|
|
320
329
|
tool,
|
|
321
330
|
...(options?.path ? { path: options.path } : {}),
|
|
331
|
+
...(options?.traceContext ? { traceContext: options.traceContext } : {}),
|
|
322
332
|
};
|
|
323
333
|
return toolContext.run(context, async () => {
|
|
324
334
|
if (!config.enabled) {
|
|
@@ -354,10 +364,10 @@ export async function withToolDiagnostics(tool, run, options) {
|
|
|
354
364
|
throw e;
|
|
355
365
|
}
|
|
356
366
|
}
|
|
357
|
-
return runAndObserve(tool, run, pubTool, pubPerf, config.logToolErrors, normalizedPath);
|
|
367
|
+
return runAndObserve(tool, run, pubTool, pubPerf, config.logToolErrors, normalizedPath, options?.traceContext?.traceparent);
|
|
358
368
|
});
|
|
359
369
|
}
|
|
360
370
|
function logError(tool, durationMs, msg) {
|
|
361
371
|
const suffix = msg ? `: ${msg}` : '';
|
|
362
|
-
|
|
372
|
+
Logger.error(`[ToolError] ${tool} failed in ${durationMs.toFixed(1)}ms${suffix}`);
|
|
363
373
|
}
|