@novorb/mcp-core 2.1.2 → 3.0.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 +77 -163
- package/bin/mcp-core-jest.mjs +112 -0
- package/dist/auth.d.ts +57 -0
- package/dist/auth.d.ts.map +1 -0
- package/dist/auth.js +74 -0
- package/dist/auth.js.map +1 -0
- package/dist/bootstrap.d.ts +129 -0
- package/dist/bootstrap.d.ts.map +1 -0
- package/dist/bootstrap.js +206 -0
- package/dist/bootstrap.js.map +1 -0
- package/dist/client-input.d.ts +72 -0
- package/dist/client-input.d.ts.map +1 -0
- package/dist/client-input.js +109 -0
- package/dist/client-input.js.map +1 -0
- package/dist/credential-checks.d.ts +80 -0
- package/dist/credential-checks.d.ts.map +1 -0
- package/dist/credential-checks.js +189 -0
- package/dist/credential-checks.js.map +1 -0
- package/dist/credential-health.d.ts +12 -0
- package/dist/credential-health.d.ts.map +1 -0
- package/dist/credential-health.js +12 -0
- package/dist/credential-health.js.map +1 -0
- package/dist/credential-report.d.ts +46 -0
- package/dist/credential-report.d.ts.map +1 -0
- package/dist/credential-report.js +135 -0
- package/dist/credential-report.js.map +1 -0
- package/dist/credential-status-tool.d.ts +20 -0
- package/dist/credential-status-tool.d.ts.map +1 -0
- package/dist/credential-status-tool.js +33 -0
- package/dist/credential-status-tool.js.map +1 -0
- package/dist/credentials.d.ts +288 -0
- package/dist/credentials.d.ts.map +1 -0
- package/dist/credentials.js +652 -0
- package/dist/credentials.js.map +1 -0
- package/dist/express-peers.d.ts +29 -0
- package/dist/express-peers.d.ts.map +1 -0
- package/dist/express-peers.js +36 -0
- package/dist/express-peers.js.map +1 -0
- package/dist/express.d.ts +74 -0
- package/dist/express.d.ts.map +1 -0
- package/dist/express.js +164 -0
- package/dist/express.js.map +1 -0
- package/dist/http-client.d.ts +277 -42
- package/dist/http-client.d.ts.map +1 -1
- package/dist/http-client.js +620 -180
- package/dist/http-client.js.map +1 -1
- package/dist/index.d.ts +27 -6
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +47 -8
- package/dist/index.js.map +1 -1
- package/dist/limits.d.ts +28 -0
- package/dist/limits.d.ts.map +1 -0
- package/dist/limits.js +37 -0
- package/dist/limits.js.map +1 -0
- package/dist/list-pagination.d.ts +41 -0
- package/dist/list-pagination.d.ts.map +1 -0
- package/dist/list-pagination.js +104 -0
- package/dist/list-pagination.js.map +1 -0
- package/dist/net.d.ts +112 -0
- package/dist/net.d.ts.map +1 -0
- package/dist/net.js +490 -0
- package/dist/net.js.map +1 -0
- package/dist/notifications.d.ts +78 -0
- package/dist/notifications.d.ts.map +1 -0
- package/dist/notifications.js +173 -0
- package/dist/notifications.js.map +1 -0
- package/dist/pagination.d.ts +236 -5
- package/dist/pagination.d.ts.map +1 -1
- package/dist/pagination.js +393 -6
- package/dist/pagination.js.map +1 -1
- package/dist/prompts.d.ts +70 -0
- package/dist/prompts.d.ts.map +1 -0
- package/dist/prompts.js +109 -0
- package/dist/prompts.js.map +1 -0
- package/dist/protocol-errors.d.ts +15 -0
- package/dist/protocol-errors.d.ts.map +1 -0
- package/dist/protocol-errors.js +28 -0
- package/dist/protocol-errors.js.map +1 -0
- package/dist/protocol-logging.d.ts +37 -0
- package/dist/protocol-logging.d.ts.map +1 -0
- package/dist/protocol-logging.js +77 -0
- package/dist/protocol-logging.js.map +1 -0
- package/dist/rate-limiter.d.ts +36 -0
- package/dist/rate-limiter.d.ts.map +1 -0
- package/dist/rate-limiter.js +79 -0
- package/dist/rate-limiter.js.map +1 -0
- package/dist/request-state.d.ts +74 -0
- package/dist/request-state.d.ts.map +1 -0
- package/dist/request-state.js +190 -0
- package/dist/request-state.js.map +1 -0
- package/dist/resources.d.ts +181 -0
- package/dist/resources.d.ts.map +1 -0
- package/dist/resources.js +302 -0
- package/dist/resources.js.map +1 -0
- package/dist/result-limits.d.ts +80 -0
- package/dist/result-limits.d.ts.map +1 -0
- package/dist/result-limits.js +248 -0
- package/dist/result-limits.js.map +1 -0
- package/dist/retry.d.ts +79 -23
- package/dist/retry.d.ts.map +1 -1
- package/dist/retry.js +170 -57
- package/dist/retry.js.map +1 -1
- package/dist/sdk/client-stdio.d.ts +6 -0
- package/dist/sdk/client-stdio.d.ts.map +1 -0
- package/dist/sdk/client-stdio.js +6 -0
- package/dist/sdk/client-stdio.js.map +1 -0
- package/dist/sdk/client.d.ts +6 -0
- package/dist/sdk/client.d.ts.map +1 -0
- package/dist/sdk/client.js +6 -0
- package/dist/sdk/client.js.map +1 -0
- package/dist/sdk/express.d.ts +3 -0
- package/dist/sdk/express.d.ts.map +1 -0
- package/dist/sdk/express.js +11 -0
- package/dist/sdk/express.js.map +1 -0
- package/dist/sdk/node.d.ts +6 -0
- package/dist/sdk/node.d.ts.map +1 -0
- package/dist/sdk/node.js +6 -0
- package/dist/sdk/node.js.map +1 -0
- package/dist/sdk/server-stdio.d.ts +6 -0
- package/dist/sdk/server-stdio.d.ts.map +1 -0
- package/dist/sdk/server-stdio.js +6 -0
- package/dist/sdk/server-stdio.js.map +1 -0
- package/dist/sdk/server.d.ts +6 -0
- package/dist/sdk/server.d.ts.map +1 -0
- package/dist/sdk/server.js +6 -0
- package/dist/sdk/server.js.map +1 -0
- package/dist/server.d.ts +88 -35
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +98 -44
- package/dist/server.js.map +1 -1
- package/dist/task-request.d.ts +20 -0
- package/dist/task-request.d.ts.map +1 -0
- package/dist/task-request.js +43 -0
- package/dist/task-request.js.map +1 -0
- package/dist/task-store.d.ts +230 -0
- package/dist/task-store.d.ts.map +1 -0
- package/dist/task-store.js +459 -0
- package/dist/task-store.js.map +1 -0
- package/dist/task-tools.d.ts +115 -0
- package/dist/task-tools.d.ts.map +1 -0
- package/dist/task-tools.js +397 -0
- package/dist/task-tools.js.map +1 -0
- package/dist/testing/client.d.ts +25 -0
- package/dist/testing/client.d.ts.map +1 -0
- package/dist/testing/client.js +11 -0
- package/dist/testing/client.js.map +1 -0
- package/dist/testing/in-memory.d.ts +26 -0
- package/dist/testing/in-memory.d.ts.map +1 -0
- package/dist/testing/in-memory.js +41 -0
- package/dist/testing/in-memory.js.map +1 -0
- package/dist/testing/index.d.ts +20 -0
- package/dist/testing/index.d.ts.map +1 -0
- package/dist/testing/index.js +20 -0
- package/dist/testing/index.js.map +1 -0
- package/dist/testing/isolated-dirs.d.ts +27 -0
- package/dist/testing/isolated-dirs.d.ts.map +1 -0
- package/dist/testing/isolated-dirs.js +67 -0
- package/dist/testing/isolated-dirs.js.map +1 -0
- package/dist/testing/logs.d.ts +61 -0
- package/dist/testing/logs.d.ts.map +1 -0
- package/dist/testing/logs.js +113 -0
- package/dist/testing/logs.js.map +1 -0
- package/dist/testing/raw-http.d.ts +25 -0
- package/dist/testing/raw-http.d.ts.map +1 -0
- package/dist/testing/raw-http.js +41 -0
- package/dist/testing/raw-http.js.map +1 -0
- package/dist/testing/raw-peer.d.ts +43 -0
- package/dist/testing/raw-peer.d.ts.map +1 -0
- package/dist/testing/raw-peer.js +101 -0
- package/dist/testing/raw-peer.js.map +1 -0
- package/dist/testing/recording-server.d.ts +38 -0
- package/dist/testing/recording-server.d.ts.map +1 -0
- package/dist/testing/recording-server.js +38 -0
- package/dist/testing/recording-server.js.map +1 -0
- package/dist/testing/server-process.d.ts +76 -0
- package/dist/testing/server-process.d.ts.map +1 -0
- package/dist/testing/server-process.js +241 -0
- package/dist/testing/server-process.js.map +1 -0
- package/dist/testing/tools.d.ts +53 -0
- package/dist/testing/tools.d.ts.map +1 -0
- package/dist/testing/tools.js +98 -0
- package/dist/testing/tools.js.map +1 -0
- package/dist/testing/unit.d.ts +52 -0
- package/dist/testing/unit.d.ts.map +1 -0
- package/dist/testing/unit.js +95 -0
- package/dist/testing/unit.js.map +1 -0
- package/dist/tools.d.ts +205 -0
- package/dist/tools.d.ts.map +1 -0
- package/dist/tools.js +370 -0
- package/dist/tools.js.map +1 -0
- package/dist/transport.d.ts +143 -52
- package/dist/transport.d.ts.map +1 -1
- package/dist/transport.js +292 -123
- package/dist/transport.js.map +1 -1
- package/dist/url-path.d.ts +49 -0
- package/dist/url-path.d.ts.map +1 -0
- package/dist/url-path.js +165 -0
- package/dist/url-path.js.map +1 -0
- package/dist/utils/dataStore.d.ts +143 -0
- package/dist/utils/dataStore.d.ts.map +1 -0
- package/dist/utils/dataStore.js +429 -0
- package/dist/utils/dataStore.js.map +1 -0
- package/dist/utils/envLoader.d.ts +129 -0
- package/dist/utils/envLoader.d.ts.map +1 -0
- package/dist/utils/envLoader.js +399 -0
- package/dist/utils/envLoader.js.map +1 -0
- package/dist/utils/logger.d.ts +181 -0
- package/dist/utils/logger.d.ts.map +1 -0
- package/dist/utils/logger.js +664 -0
- package/dist/utils/logger.js.map +1 -0
- package/dist/utils/redact.d.ts +50 -0
- package/dist/utils/redact.d.ts.map +1 -0
- package/dist/utils/redact.js +318 -0
- package/dist/utils/redact.js.map +1 -0
- package/docs/api-reference.md +29 -0
- package/docs/building-a-server.md +63 -0
- package/docs/credentials.md +131 -0
- package/docs/data-store.md +37 -0
- package/docs/env.md +38 -0
- package/docs/features.md +28 -0
- package/docs/http-client.md +116 -0
- package/docs/logger.md +100 -0
- package/docs/migrating-to-v3.md +45 -0
- package/docs/outbound-urls.md +104 -0
- package/docs/pagination.md +45 -0
- package/docs/protocol-features.md +62 -0
- package/docs/resources-prompts-tasks.md +95 -0
- package/docs/sdk.md +33 -0
- package/docs/server.md +68 -0
- package/docs/testing-harness.md +139 -0
- package/docs/testing.md +151 -0
- package/docs/tools.md +88 -0
- package/docs/transport.md +85 -0
- package/examples/testing/tools.integration.test.example.ts +312 -0
- package/examples/testing/tools.unit.test.example.ts +149 -0
- package/package.json +130 -17
- package/dist/logger.d.ts +0 -80
- package/dist/logger.d.ts.map +0 -1
- package/dist/logger.js +0 -237
- package/dist/logger.js.map +0 -1
|
@@ -0,0 +1,664 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @novorb/mcp-core - Logger
|
|
3
|
+
*
|
|
4
|
+
* One logging utility for every MCP server. Sinks:
|
|
5
|
+
* - console: process.stderr only (never stdout, the stdio protocol channel)
|
|
6
|
+
* - <name>.log: the main log, every entry at or above the level
|
|
7
|
+
* - exceptions.log: ERROR entries only
|
|
8
|
+
* - debug.log: DEBUG entries only (so only when the level is DEBUG)
|
|
9
|
+
*
|
|
10
|
+
* Log directory: `<rootDir>/.logs` by default (rootDir = the consumer's package root, see
|
|
11
|
+
* `packageRootFrom`). LOG_PATH overrides it (absolute, or relative to rootDir). Without rootDir and
|
|
12
|
+
* without an absolute LOG_PATH the file sinks stay off (one console warning from initLogger); nothing
|
|
13
|
+
* is ever written relative to cwd.
|
|
14
|
+
*
|
|
15
|
+
* Switches (explicit config overrides env, env overrides the default):
|
|
16
|
+
* LOG_LEVEL DEBUG | INFO | WARN | ERROR default INFO
|
|
17
|
+
* LOG_FORMAT text | json default text
|
|
18
|
+
* LOG_CONSOLE true | false default true
|
|
19
|
+
* LOG_FILE_MAIN true | false default true (needs a directory)
|
|
20
|
+
* LOG_FILE_EXCEPTIONS true | false default true (needs a directory)
|
|
21
|
+
* LOG_FILE_DEBUG true | false default true (needs a directory)
|
|
22
|
+
* LOG_PATH log directory default <rootDir>/.logs
|
|
23
|
+
* LOG_FILE main file name (absolute = as-is) default <name>.log
|
|
24
|
+
*
|
|
25
|
+
* Text line: 2026-03-22T13:27:58.513Z | INFO | Server | Starting... | {"meta":"json"}
|
|
26
|
+
* JSON line: {"level":"INFO","timestamp":"...","component":"Server","message":"Starting...",...meta}
|
|
27
|
+
*
|
|
28
|
+
* Every entry is sanitised: sensitive-keyed meta fields become [REDACTED], secret query parameters
|
|
29
|
+
* in strings become name=[REDACTED], and PII in the message and every meta string is masked
|
|
30
|
+
* ([EMAIL] [PHONE] [CARD] [IBAN] [JWT] [TOKEN] [IP]). Text mode also strips control characters.
|
|
31
|
+
*
|
|
32
|
+
* `initLogger` reconfigures the singleton IN PLACE, so a logger obtained with `getLogger()` at import
|
|
33
|
+
* time (before init) writes to the configured sinks after init.
|
|
34
|
+
*/
|
|
35
|
+
import * as fs from 'node:fs';
|
|
36
|
+
import * as path from 'node:path';
|
|
37
|
+
import { fileURLToPath } from 'node:url';
|
|
38
|
+
import { redactSecrets, sanitizeString } from './redact.js';
|
|
39
|
+
export { maskPii, redactSecrets } from './redact.js';
|
|
40
|
+
export var LogLevel;
|
|
41
|
+
(function (LogLevel) {
|
|
42
|
+
LogLevel[LogLevel["DEBUG"] = 0] = "DEBUG";
|
|
43
|
+
LogLevel[LogLevel["INFO"] = 1] = "INFO";
|
|
44
|
+
LogLevel[LogLevel["WARN"] = 2] = "WARN";
|
|
45
|
+
LogLevel[LogLevel["ERROR"] = 3] = "ERROR";
|
|
46
|
+
})(LogLevel || (LogLevel = {}));
|
|
47
|
+
const DEFAULTS = {
|
|
48
|
+
level: LogLevel.INFO,
|
|
49
|
+
format: 'text',
|
|
50
|
+
console: true,
|
|
51
|
+
fileMain: true,
|
|
52
|
+
fileExceptions: true,
|
|
53
|
+
fileDebug: true,
|
|
54
|
+
maxLogSize: 10 * 1024 * 1024,
|
|
55
|
+
maxLogFiles: 5,
|
|
56
|
+
captureProcessErrors: false,
|
|
57
|
+
};
|
|
58
|
+
function parseLogLevel(value) {
|
|
59
|
+
if (!value)
|
|
60
|
+
return undefined;
|
|
61
|
+
const upper = value.toUpperCase();
|
|
62
|
+
const byName = LogLevel;
|
|
63
|
+
return upper in LogLevel && typeof byName[upper] === 'number' ? byName[upper] : undefined;
|
|
64
|
+
}
|
|
65
|
+
function parseFormat(value) {
|
|
66
|
+
const v = value?.toLowerCase();
|
|
67
|
+
return v === 'text' || v === 'json' ? v : undefined;
|
|
68
|
+
}
|
|
69
|
+
function parseBool(value) {
|
|
70
|
+
const v = value?.trim().toLowerCase();
|
|
71
|
+
if (v === 'true' || v === '1' || v === 'yes' || v === 'on')
|
|
72
|
+
return true;
|
|
73
|
+
if (v === 'false' || v === '0' || v === 'no' || v === 'off')
|
|
74
|
+
return false;
|
|
75
|
+
return undefined;
|
|
76
|
+
}
|
|
77
|
+
function defined(obj) {
|
|
78
|
+
const out = {};
|
|
79
|
+
if (!obj)
|
|
80
|
+
return out;
|
|
81
|
+
for (const [k, v] of Object.entries(obj)) {
|
|
82
|
+
if (v !== undefined)
|
|
83
|
+
out[k] = v;
|
|
84
|
+
}
|
|
85
|
+
return out;
|
|
86
|
+
}
|
|
87
|
+
/** Defaults, then env, then explicit options. */
|
|
88
|
+
function buildConfig(options) {
|
|
89
|
+
const env = process.env;
|
|
90
|
+
const fromEnv = defined({
|
|
91
|
+
level: parseLogLevel(env.LOG_LEVEL),
|
|
92
|
+
format: parseFormat(env.LOG_FORMAT),
|
|
93
|
+
console: parseBool(env.LOG_CONSOLE),
|
|
94
|
+
fileMain: parseBool(env.LOG_FILE_MAIN),
|
|
95
|
+
fileExceptions: parseBool(env.LOG_FILE_EXCEPTIONS),
|
|
96
|
+
fileDebug: parseBool(env.LOG_FILE_DEBUG),
|
|
97
|
+
logDir: env.LOG_PATH || undefined,
|
|
98
|
+
logFile: env.LOG_FILE || undefined,
|
|
99
|
+
});
|
|
100
|
+
return { ...DEFAULTS, ...fromEnv, ...defined(options) };
|
|
101
|
+
}
|
|
102
|
+
function sanitizeName(name) {
|
|
103
|
+
const safe = name?.replace(/[^A-Za-z0-9._-]/g, '-').replace(/^\.+/, '');
|
|
104
|
+
return safe ? safe : undefined;
|
|
105
|
+
}
|
|
106
|
+
/**
|
|
107
|
+
* Windows drive-relative (`C:logs`) and root-relative (`\logs`) paths resolve against a current
|
|
108
|
+
* directory, which would break the "never cwd" rule, so they are refused.
|
|
109
|
+
*/
|
|
110
|
+
function isCwdDependent(p) {
|
|
111
|
+
if (/^[A-Za-z]:(?![\\/])/.test(p))
|
|
112
|
+
return true;
|
|
113
|
+
return process.platform === 'win32' && /^[\\/](?![\\/])/.test(p);
|
|
114
|
+
}
|
|
115
|
+
function resolveSinks(c) {
|
|
116
|
+
let dir;
|
|
117
|
+
let dirProblem;
|
|
118
|
+
if (c.logDir) {
|
|
119
|
+
if (isCwdDependent(c.logDir))
|
|
120
|
+
dirProblem = `LOG_PATH "${c.logDir}" is drive- or root-relative`;
|
|
121
|
+
else if (path.isAbsolute(c.logDir))
|
|
122
|
+
dir = c.logDir;
|
|
123
|
+
else if (c.rootDir)
|
|
124
|
+
dir = path.resolve(c.rootDir, c.logDir);
|
|
125
|
+
else
|
|
126
|
+
dirProblem = `LOG_PATH "${c.logDir}" is relative and no rootDir was given`;
|
|
127
|
+
}
|
|
128
|
+
else if (c.rootDir) {
|
|
129
|
+
dir = path.join(path.resolve(c.rootDir), '.logs');
|
|
130
|
+
}
|
|
131
|
+
else {
|
|
132
|
+
dirProblem = 'no rootDir and no LOG_PATH';
|
|
133
|
+
}
|
|
134
|
+
const sinks = { console: c.console };
|
|
135
|
+
const problems = [];
|
|
136
|
+
let missingName = false;
|
|
137
|
+
const badFile = c.logFile !== undefined && isCwdDependent(c.logFile);
|
|
138
|
+
const absoluteMain = !badFile && c.logFile !== undefined && path.isAbsolute(c.logFile);
|
|
139
|
+
if (c.fileMain) {
|
|
140
|
+
if (badFile) {
|
|
141
|
+
problems.push(`main log disabled: LOG_FILE "${c.logFile}" is drive- or root-relative`);
|
|
142
|
+
}
|
|
143
|
+
else if (absoluteMain) {
|
|
144
|
+
sinks.main = c.logFile;
|
|
145
|
+
}
|
|
146
|
+
else if (dir) {
|
|
147
|
+
const file = c.logFile ?? (sanitizeName(c.name) ? `${sanitizeName(c.name)}.log` : undefined);
|
|
148
|
+
if (file)
|
|
149
|
+
sinks.main = path.join(dir, file);
|
|
150
|
+
else
|
|
151
|
+
missingName = true;
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
if (c.fileExceptions && dir)
|
|
155
|
+
sinks.exceptions = path.join(dir, 'exceptions.log');
|
|
156
|
+
if (c.fileDebug && dir)
|
|
157
|
+
sinks.debug = path.join(dir, 'debug.log');
|
|
158
|
+
if (!dir && dirProblem) {
|
|
159
|
+
const mainNeedsDir = c.fileMain && !absoluteMain && !badFile;
|
|
160
|
+
const others = c.fileExceptions || c.fileDebug;
|
|
161
|
+
if (mainNeedsDir)
|
|
162
|
+
problems.unshift(`File logging disabled: ${dirProblem}`);
|
|
163
|
+
else if (others)
|
|
164
|
+
problems.unshift(`exceptions.log and debug.log disabled: ${dirProblem}`);
|
|
165
|
+
}
|
|
166
|
+
const hint = !dir && dirProblem ? ' (pass initLogger({ rootDir }) or set an absolute LOG_PATH)' : '';
|
|
167
|
+
return { sinks, warning: problems.length ? `${problems.join('; ')}${hint}` : undefined, missingName };
|
|
168
|
+
}
|
|
169
|
+
function isBrokenPipe(err) {
|
|
170
|
+
const code = typeof err === 'object' && err !== null ? err.code : undefined;
|
|
171
|
+
return code === 'EPIPE' || code === 'ERR_STREAM_DESTROYED' || code === 'ERR_STREAM_WRITE_AFTER_END';
|
|
172
|
+
}
|
|
173
|
+
function describeError(err) {
|
|
174
|
+
return err instanceof Error ? `${err.name}: ${err.message}` : String(err);
|
|
175
|
+
}
|
|
176
|
+
/** Collapse C0 control chars (incl. CR/LF) + DEL to spaces so a logged value can't forge log lines. */
|
|
177
|
+
function stripControlChars(s) {
|
|
178
|
+
let out = '';
|
|
179
|
+
for (let i = 0; i < s.length; i++) {
|
|
180
|
+
const code = s.charCodeAt(i);
|
|
181
|
+
out += code < 0x20 || code === 0x7f ? ' ' : s[i];
|
|
182
|
+
}
|
|
183
|
+
return out;
|
|
184
|
+
}
|
|
185
|
+
/** Registered secret values shorter than this are ignored (they would mask ordinary words). */
|
|
186
|
+
export const MIN_SECRET_LENGTH = 6;
|
|
187
|
+
const SECRET_MASK = '[SECRET]';
|
|
188
|
+
const secretValues = new Set();
|
|
189
|
+
/**
|
|
190
|
+
* Register secret VALUES (API tokens, passwords) that are masked verbatim as `[SECRET]` wherever they
|
|
191
|
+
* appear in any log entry, in both line formats (raw and JSON-escaped). The env loader calls this for
|
|
192
|
+
* every set `secret: true` variable. Process-global; values shorter than MIN_SECRET_LENGTH are ignored.
|
|
193
|
+
*/
|
|
194
|
+
export function registerSecretValues(values) {
|
|
195
|
+
for (const value of values) {
|
|
196
|
+
if (typeof value !== 'string' || value.length < MIN_SECRET_LENGTH)
|
|
197
|
+
continue;
|
|
198
|
+
secretValues.add(value);
|
|
199
|
+
const escaped = JSON.stringify(value).slice(1, -1);
|
|
200
|
+
if (escaped !== value)
|
|
201
|
+
secretValues.add(escaped);
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
/** Forget every registered secret value (tests). */
|
|
205
|
+
export function clearSecretValues() {
|
|
206
|
+
secretValues.clear();
|
|
207
|
+
}
|
|
208
|
+
/** Replace every registered secret value in `s` with `[SECRET]` (longest first). */
|
|
209
|
+
export function maskSecretValues(s) {
|
|
210
|
+
if (secretValues.size === 0)
|
|
211
|
+
return s;
|
|
212
|
+
let out = s;
|
|
213
|
+
for (const secret of [...secretValues].sort((a, b) => b.length - a.length)) {
|
|
214
|
+
if (out.includes(secret))
|
|
215
|
+
out = out.split(secret).join(SECRET_MASK);
|
|
216
|
+
}
|
|
217
|
+
return out;
|
|
218
|
+
}
|
|
219
|
+
const logListeners = new Set();
|
|
220
|
+
/**
|
|
221
|
+
* Subscribe to accepted log entries (process-global, survives `initLogger` reconfiguration; cleared by
|
|
222
|
+
* `resetLogger`). Returns the unsubscribe function. A throwing listener is ignored, and a listener that
|
|
223
|
+
* logs from inside its own callback is not re-entered.
|
|
224
|
+
*/
|
|
225
|
+
export function addLogListener(listener) {
|
|
226
|
+
logListeners.add(listener);
|
|
227
|
+
return () => {
|
|
228
|
+
logListeners.delete(listener);
|
|
229
|
+
};
|
|
230
|
+
}
|
|
231
|
+
let notifyingListeners = false;
|
|
232
|
+
function sanitizedMeta(meta) {
|
|
233
|
+
if (meta === undefined)
|
|
234
|
+
return undefined;
|
|
235
|
+
const json = JSON.stringify(redactSecrets(meta));
|
|
236
|
+
return json === undefined ? undefined : JSON.parse(maskSecretValues(json));
|
|
237
|
+
}
|
|
238
|
+
function notifyListeners(level, name, message, context, meta) {
|
|
239
|
+
if (logListeners.size === 0 || notifyingListeners)
|
|
240
|
+
return;
|
|
241
|
+
notifyingListeners = true;
|
|
242
|
+
try {
|
|
243
|
+
let entry;
|
|
244
|
+
try {
|
|
245
|
+
entry = {
|
|
246
|
+
level,
|
|
247
|
+
...(name !== undefined ? { logger: name } : {}),
|
|
248
|
+
context: maskSecretValues(sanitizeString(context ?? 'General')),
|
|
249
|
+
message: maskSecretValues(sanitizeString(maskSecretValues(message))),
|
|
250
|
+
meta: sanitizedMeta(meta),
|
|
251
|
+
};
|
|
252
|
+
}
|
|
253
|
+
catch {
|
|
254
|
+
entry = { level, context: 'Logger', message: '[unformattable entry]' };
|
|
255
|
+
}
|
|
256
|
+
for (const listener of logListeners) {
|
|
257
|
+
try {
|
|
258
|
+
listener(entry);
|
|
259
|
+
}
|
|
260
|
+
catch {
|
|
261
|
+
// A listener never breaks logging.
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
finally {
|
|
266
|
+
notifyingListeners = false;
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
const LEVEL_NAMES = {
|
|
270
|
+
[LogLevel.DEBUG]: 'DEBUG',
|
|
271
|
+
[LogLevel.INFO]: 'INFO ',
|
|
272
|
+
[LogLevel.WARN]: 'WARN ',
|
|
273
|
+
[LogLevel.ERROR]: 'ERROR',
|
|
274
|
+
};
|
|
275
|
+
/** A rendered line longer than this is cut (text) or loses its meta (json). */
|
|
276
|
+
const MAX_LINE_CHARS = 256 * 1024;
|
|
277
|
+
const RESERVED_JSON_FIELDS = new Set(['level', 'timestamp', 'component', 'message', 'meta']);
|
|
278
|
+
// stderr and the process are global, so the broken-pipe state, the stderr guard and the fatal
|
|
279
|
+
// re-entrancy flag are too.
|
|
280
|
+
let stderrBroken = false;
|
|
281
|
+
let stderrGuard = null;
|
|
282
|
+
const consoleLoggers = new Set();
|
|
283
|
+
let dirWarningShown = false;
|
|
284
|
+
let fatalInProgress = false;
|
|
285
|
+
/**
|
|
286
|
+
* Keep one stderr `error` listener while any logger has the console on. An EPIPE marks stderr broken
|
|
287
|
+
* (the console sink stops); any other error is re-thrown when no one else listens, exactly as Node
|
|
288
|
+
* would have done without this listener.
|
|
289
|
+
*/
|
|
290
|
+
function updateStderrGuard(logger, consoleOn) {
|
|
291
|
+
if (consoleOn)
|
|
292
|
+
consoleLoggers.add(logger);
|
|
293
|
+
else
|
|
294
|
+
consoleLoggers.delete(logger);
|
|
295
|
+
if (consoleLoggers.size > 0 && !stderrGuard) {
|
|
296
|
+
stderrGuard = (err) => {
|
|
297
|
+
if (isBrokenPipe(err)) {
|
|
298
|
+
stderrBroken = true;
|
|
299
|
+
return;
|
|
300
|
+
}
|
|
301
|
+
if (process.stderr.listenerCount('error') === 1)
|
|
302
|
+
throw err;
|
|
303
|
+
};
|
|
304
|
+
process.stderr.on('error', stderrGuard);
|
|
305
|
+
}
|
|
306
|
+
else if (consoleLoggers.size === 0 && stderrGuard) {
|
|
307
|
+
process.stderr.removeListener('error', stderrGuard);
|
|
308
|
+
stderrGuard = null;
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
export class Logger {
|
|
312
|
+
config = DEFAULTS;
|
|
313
|
+
resolved = { console: true };
|
|
314
|
+
fatalHandlers = null;
|
|
315
|
+
// Known size per file, so rotation needs one stat per file per configuration, not one per line.
|
|
316
|
+
sizes = new Map();
|
|
317
|
+
constructor(options) {
|
|
318
|
+
this.configure(options);
|
|
319
|
+
}
|
|
320
|
+
/**
|
|
321
|
+
* Reconfigure in place: recompute every setting from defaults + env + `options` (exactly as the
|
|
322
|
+
* constructor does), create the log directories, and install or remove the process-error handlers.
|
|
323
|
+
*/
|
|
324
|
+
configure(options) {
|
|
325
|
+
this.config = buildConfig(options);
|
|
326
|
+
this.resolved = resolveSinks(this.config).sinks;
|
|
327
|
+
this.sizes.clear();
|
|
328
|
+
for (const file of [this.resolved.main, this.resolved.exceptions, this.resolved.debug]) {
|
|
329
|
+
if (!file)
|
|
330
|
+
continue;
|
|
331
|
+
try {
|
|
332
|
+
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
333
|
+
}
|
|
334
|
+
catch {
|
|
335
|
+
// Silent: a file sink may fail without affecting the others.
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
updateStderrGuard(this, this.resolved.console);
|
|
339
|
+
if (this.config.captureProcessErrors)
|
|
340
|
+
this.attachProcessHandlers();
|
|
341
|
+
else
|
|
342
|
+
this.detachProcessHandlers();
|
|
343
|
+
}
|
|
344
|
+
/** The active sinks (console flag and absolute file paths). */
|
|
345
|
+
sinks() {
|
|
346
|
+
return { ...this.resolved };
|
|
347
|
+
}
|
|
348
|
+
/** Remove the process-error handlers and this logger's claim on the stderr guard. */
|
|
349
|
+
detach() {
|
|
350
|
+
this.detachProcessHandlers();
|
|
351
|
+
updateStderrGuard(this, false);
|
|
352
|
+
}
|
|
353
|
+
/** Remove the process-error handlers, if installed. */
|
|
354
|
+
detachProcessHandlers() {
|
|
355
|
+
if (!this.fatalHandlers)
|
|
356
|
+
return;
|
|
357
|
+
process.removeListener('uncaughtException', this.fatalHandlers.exception);
|
|
358
|
+
process.removeListener('unhandledRejection', this.fatalHandlers.rejection);
|
|
359
|
+
this.fatalHandlers = null;
|
|
360
|
+
}
|
|
361
|
+
attachProcessHandlers() {
|
|
362
|
+
if (this.fatalHandlers)
|
|
363
|
+
return;
|
|
364
|
+
this.fatalHandlers = {
|
|
365
|
+
exception: (err) => this.onFatal('Uncaught exception', err),
|
|
366
|
+
rejection: (reason) => this.onFatal('Unhandled rejection', reason),
|
|
367
|
+
};
|
|
368
|
+
process.on('uncaughtException', this.fatalHandlers.exception);
|
|
369
|
+
process.on('unhandledRejection', this.fatalHandlers.rejection);
|
|
370
|
+
}
|
|
371
|
+
onFatal(kind, err) {
|
|
372
|
+
// Process-wide: a second capturing logger, or a fault raised while handling one, does nothing.
|
|
373
|
+
if (fatalInProgress)
|
|
374
|
+
return;
|
|
375
|
+
fatalInProgress = true;
|
|
376
|
+
try {
|
|
377
|
+
// An EPIPE means stderr is gone: logging it back to stderr is exactly the runaway loop.
|
|
378
|
+
if (isBrokenPipe(err))
|
|
379
|
+
stderrBroken = true;
|
|
380
|
+
const stack = err instanceof Error && err.stack ? { stack: err.stack } : undefined;
|
|
381
|
+
this.write(LogLevel.ERROR, `${kind}: ${describeError(err)}`, 'Process', stack);
|
|
382
|
+
}
|
|
383
|
+
finally {
|
|
384
|
+
process.exit(1);
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
/** Write one WARN line to the console sink only (used for the directory warning). */
|
|
388
|
+
consoleWarn(message, context) {
|
|
389
|
+
if (this.resolved.console)
|
|
390
|
+
this.writeConsole(this.format(LogLevel.WARN, message, context));
|
|
391
|
+
}
|
|
392
|
+
formatText(level, message, context, meta) {
|
|
393
|
+
const timestamp = new Date().toISOString();
|
|
394
|
+
const levelStr = LEVEL_NAMES[level] ?? LogLevel[level].padEnd(5);
|
|
395
|
+
const contextStr = stripControlChars(sanitizeString(context ?? 'General'));
|
|
396
|
+
const safeMessage = stripControlChars(sanitizeString(message));
|
|
397
|
+
const metaStr = meta !== undefined ? ` | ${JSON.stringify(redactSecrets(meta))}` : '';
|
|
398
|
+
let line = `${timestamp} | ${levelStr} | ${contextStr} | ${safeMessage}${metaStr}`;
|
|
399
|
+
if (line.length > MAX_LINE_CHARS) {
|
|
400
|
+
line = `${line.slice(0, MAX_LINE_CHARS)}...[TRUNCATED ${line.length - MAX_LINE_CHARS} chars]`;
|
|
401
|
+
}
|
|
402
|
+
return `${line}\n`;
|
|
403
|
+
}
|
|
404
|
+
formatJson(level, message, context, meta) {
|
|
405
|
+
const entry = {
|
|
406
|
+
level: LogLevel[level],
|
|
407
|
+
timestamp: new Date().toISOString(),
|
|
408
|
+
component: sanitizeString(context ?? 'General'),
|
|
409
|
+
message: sanitizeString(message),
|
|
410
|
+
};
|
|
411
|
+
if (meta !== undefined) {
|
|
412
|
+
const safeMeta = redactSecrets(meta);
|
|
413
|
+
if (typeof safeMeta === 'object' && safeMeta !== null && !Array.isArray(safeMeta)) {
|
|
414
|
+
// Meta never overwrites the entry's own fields: colliding keys go under `meta`.
|
|
415
|
+
const collisions = {};
|
|
416
|
+
for (const [k, v] of Object.entries(safeMeta)) {
|
|
417
|
+
if (RESERVED_JSON_FIELDS.has(k))
|
|
418
|
+
collisions[k] = v;
|
|
419
|
+
else
|
|
420
|
+
entry[k] = v;
|
|
421
|
+
}
|
|
422
|
+
if (Object.keys(collisions).length > 0)
|
|
423
|
+
entry.meta = collisions;
|
|
424
|
+
}
|
|
425
|
+
else {
|
|
426
|
+
entry.meta = safeMeta;
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
let line = JSON.stringify(entry);
|
|
430
|
+
if (line.length > MAX_LINE_CHARS) {
|
|
431
|
+
line = JSON.stringify({
|
|
432
|
+
level: entry.level,
|
|
433
|
+
timestamp: entry.timestamp,
|
|
434
|
+
component: entry.component,
|
|
435
|
+
message: String(entry.message).slice(0, MAX_LINE_CHARS / 2),
|
|
436
|
+
meta: `[TRUNCATED: meta too large (${line.length} chars)]`,
|
|
437
|
+
});
|
|
438
|
+
}
|
|
439
|
+
return `${line}\n`;
|
|
440
|
+
}
|
|
441
|
+
format(level, message, context, meta) {
|
|
442
|
+
try {
|
|
443
|
+
// Registered secrets are masked in the raw message first (before truncation could split one),
|
|
444
|
+
// then in the whole rendered line (context and every meta string included).
|
|
445
|
+
const safeMessage = maskSecretValues(message);
|
|
446
|
+
const line = this.config.format === 'json'
|
|
447
|
+
? this.formatJson(level, safeMessage, context, meta)
|
|
448
|
+
: this.formatText(level, safeMessage, context, meta);
|
|
449
|
+
return maskSecretValues(line);
|
|
450
|
+
}
|
|
451
|
+
catch {
|
|
452
|
+
// Logging must never throw into the caller: drop the meta and keep the line.
|
|
453
|
+
if (meta === undefined)
|
|
454
|
+
return `${new Date().toISOString()} | ${LEVEL_NAMES[level]} | Logger | [unformattable entry]\n`;
|
|
455
|
+
return this.format(level, message, context, '[unserialisable meta]');
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
write(level, message, context, meta) {
|
|
459
|
+
if (level < this.config.level)
|
|
460
|
+
return;
|
|
461
|
+
const line = this.format(level, message, context, meta);
|
|
462
|
+
const { main, exceptions, debug } = this.resolved;
|
|
463
|
+
if (main)
|
|
464
|
+
this.writeFile(main, line);
|
|
465
|
+
if (exceptions && level === LogLevel.ERROR)
|
|
466
|
+
this.writeFile(exceptions, line);
|
|
467
|
+
if (debug && level === LogLevel.DEBUG)
|
|
468
|
+
this.writeFile(debug, line);
|
|
469
|
+
if (this.resolved.console)
|
|
470
|
+
this.writeConsole(line);
|
|
471
|
+
notifyListeners(level, this.config.name, message, context, meta);
|
|
472
|
+
}
|
|
473
|
+
writeConsole(line) {
|
|
474
|
+
if (stderrBroken)
|
|
475
|
+
return;
|
|
476
|
+
try {
|
|
477
|
+
process.stderr.write(line);
|
|
478
|
+
}
|
|
479
|
+
catch (err) {
|
|
480
|
+
if (isBrokenPipe(err))
|
|
481
|
+
stderrBroken = true;
|
|
482
|
+
}
|
|
483
|
+
}
|
|
484
|
+
knownSize(file) {
|
|
485
|
+
let size = this.sizes.get(file);
|
|
486
|
+
if (size === undefined) {
|
|
487
|
+
try {
|
|
488
|
+
size = fs.statSync(file).size;
|
|
489
|
+
}
|
|
490
|
+
catch {
|
|
491
|
+
size = 0;
|
|
492
|
+
}
|
|
493
|
+
this.sizes.set(file, size);
|
|
494
|
+
}
|
|
495
|
+
return size;
|
|
496
|
+
}
|
|
497
|
+
writeFile(file, line) {
|
|
498
|
+
try {
|
|
499
|
+
if (this.knownSize(file) > this.config.maxLogSize)
|
|
500
|
+
this.rotate(file);
|
|
501
|
+
try {
|
|
502
|
+
fs.appendFileSync(file, line, 'utf8');
|
|
503
|
+
}
|
|
504
|
+
catch (err) {
|
|
505
|
+
// The directory was removed after configuration: recreate it once and retry.
|
|
506
|
+
if (err.code !== 'ENOENT')
|
|
507
|
+
throw err;
|
|
508
|
+
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
509
|
+
fs.appendFileSync(file, line, 'utf8');
|
|
510
|
+
}
|
|
511
|
+
this.sizes.set(file, this.knownSize(file) + Buffer.byteLength(line, 'utf8'));
|
|
512
|
+
}
|
|
513
|
+
catch {
|
|
514
|
+
// Silent: a file sink may fail without affecting the others.
|
|
515
|
+
}
|
|
516
|
+
}
|
|
517
|
+
/** maxLogFiles counts the active file: N keeps the active file plus N-1 rotated ones (minimum 1). */
|
|
518
|
+
rotate(file) {
|
|
519
|
+
try {
|
|
520
|
+
const keep = Math.max(1, Math.floor(this.config.maxLogFiles));
|
|
521
|
+
if (keep === 1) {
|
|
522
|
+
fs.unlinkSync(file);
|
|
523
|
+
}
|
|
524
|
+
else {
|
|
525
|
+
if (fs.existsSync(`${file}.${keep - 1}`))
|
|
526
|
+
fs.unlinkSync(`${file}.${keep - 1}`);
|
|
527
|
+
for (let i = keep - 2; i > 0; i--) {
|
|
528
|
+
if (fs.existsSync(`${file}.${i}`))
|
|
529
|
+
fs.renameSync(`${file}.${i}`, `${file}.${i + 1}`);
|
|
530
|
+
}
|
|
531
|
+
fs.renameSync(file, `${file}.1`);
|
|
532
|
+
}
|
|
533
|
+
}
|
|
534
|
+
catch {
|
|
535
|
+
// Silent
|
|
536
|
+
}
|
|
537
|
+
this.sizes.set(file, 0);
|
|
538
|
+
}
|
|
539
|
+
debug(message, context, meta) {
|
|
540
|
+
this.write(LogLevel.DEBUG, message, context, meta);
|
|
541
|
+
}
|
|
542
|
+
info(message, context, meta) {
|
|
543
|
+
this.write(LogLevel.INFO, message, context, meta);
|
|
544
|
+
}
|
|
545
|
+
warn(message, context, meta) {
|
|
546
|
+
this.write(LogLevel.WARN, message, context, meta);
|
|
547
|
+
}
|
|
548
|
+
error(message, context, meta) {
|
|
549
|
+
this.write(LogLevel.ERROR, message, context, meta);
|
|
550
|
+
}
|
|
551
|
+
setLevel(level) {
|
|
552
|
+
this.config = { ...this.config, level };
|
|
553
|
+
}
|
|
554
|
+
}
|
|
555
|
+
let loggerInstance = null;
|
|
556
|
+
/**
|
|
557
|
+
* Initialise (or reconfigure in place) the logger singleton. Call once at startup with the server
|
|
558
|
+
* `name` and its package `rootDir`. Throws when the main file is on, a directory exists, and neither
|
|
559
|
+
* `name` nor LOG_FILE is given. Warns once on the console when file sinks are on but there is no
|
|
560
|
+
* usable directory.
|
|
561
|
+
*/
|
|
562
|
+
export function initLogger(options) {
|
|
563
|
+
const resolution = resolveSinks(buildConfig(options));
|
|
564
|
+
if (resolution.missingName) {
|
|
565
|
+
throw new Error('initLogger: `name` is required for the main log file (or set LOG_FILE, or fileMain: false)');
|
|
566
|
+
}
|
|
567
|
+
if (loggerInstance)
|
|
568
|
+
loggerInstance.configure(options);
|
|
569
|
+
else
|
|
570
|
+
loggerInstance = new Logger(options);
|
|
571
|
+
if (resolution.warning && !dirWarningShown) {
|
|
572
|
+
dirWarningShown = true;
|
|
573
|
+
loggerInstance.consoleWarn(resolution.warning, 'Logger');
|
|
574
|
+
}
|
|
575
|
+
return loggerInstance;
|
|
576
|
+
}
|
|
577
|
+
/** Get the singleton, creating a default (console-only unless LOG_PATH is absolute) one if needed. */
|
|
578
|
+
export function getLogger(options) {
|
|
579
|
+
if (!loggerInstance)
|
|
580
|
+
loggerInstance = new Logger(options);
|
|
581
|
+
return loggerInstance;
|
|
582
|
+
}
|
|
583
|
+
/** Drop the singleton, its process handlers and the process-global console state (tests). */
|
|
584
|
+
export function resetLogger() {
|
|
585
|
+
loggerInstance?.detach();
|
|
586
|
+
loggerInstance = null;
|
|
587
|
+
consoleLoggers.clear();
|
|
588
|
+
if (stderrGuard)
|
|
589
|
+
process.stderr.removeListener('error', stderrGuard);
|
|
590
|
+
stderrGuard = null;
|
|
591
|
+
stderrBroken = false;
|
|
592
|
+
dirWarningShown = false;
|
|
593
|
+
fatalInProgress = false;
|
|
594
|
+
secretValues.clear();
|
|
595
|
+
logListeners.clear();
|
|
596
|
+
notifyingListeners = false;
|
|
597
|
+
}
|
|
598
|
+
/**
|
|
599
|
+
* The package root of the module at `importMetaUrl` (pass `import.meta.url`): the nearest ancestor
|
|
600
|
+
* directory holding a package.json. Works from `src/` and `dist/` alike and is Windows-safe
|
|
601
|
+
* (`fileURLToPath`, never `URL.pathname`). Also accepts a plain file path.
|
|
602
|
+
*/
|
|
603
|
+
export function packageRootFrom(importMetaUrl) {
|
|
604
|
+
const asString = importMetaUrl instanceof URL ? importMetaUrl.href : importMetaUrl;
|
|
605
|
+
const file = asString.startsWith('file:') ? fileURLToPath(asString) : path.resolve(asString);
|
|
606
|
+
let dir = path.dirname(file);
|
|
607
|
+
for (;;) {
|
|
608
|
+
if (fs.existsSync(path.join(dir, 'package.json')))
|
|
609
|
+
return dir;
|
|
610
|
+
const parent = path.dirname(dir);
|
|
611
|
+
if (parent === dir)
|
|
612
|
+
throw new Error(`packageRootFrom: no package.json above ${file}`);
|
|
613
|
+
dir = parent;
|
|
614
|
+
}
|
|
615
|
+
}
|
|
616
|
+
const MAX_NESTED_KEYS = 20;
|
|
617
|
+
const TOOL_CONTEXT = 'Tools';
|
|
618
|
+
function describeShape(value) {
|
|
619
|
+
if (value === null)
|
|
620
|
+
return 'null';
|
|
621
|
+
if (Array.isArray(value))
|
|
622
|
+
return `array(${value.length})`;
|
|
623
|
+
if (typeof value === 'string')
|
|
624
|
+
return `string(${value.length})`;
|
|
625
|
+
if (typeof value === 'object') {
|
|
626
|
+
const keys = Object.keys(value);
|
|
627
|
+
const shown = keys.slice(0, MAX_NESTED_KEYS).join(',');
|
|
628
|
+
const more = keys.length > MAX_NESTED_KEYS ? `,...+${keys.length - MAX_NESTED_KEYS}` : '';
|
|
629
|
+
return `object{${shown}${more}}`;
|
|
630
|
+
}
|
|
631
|
+
return typeof value;
|
|
632
|
+
}
|
|
633
|
+
/**
|
|
634
|
+
* Summarise tool arguments by shape only, never by value. Per top-level key: `string(<len>)`,
|
|
635
|
+
* `array(<len>)`, `object{<key names, one level, max 20>}`, `number`, `boolean`, `null`, `undefined`,
|
|
636
|
+
* `bigint`, `function`. Non-object args are summarised under `_args`; null/undefined give `{}`.
|
|
637
|
+
*/
|
|
638
|
+
export function summarizeArgs(args) {
|
|
639
|
+
if (args === null || args === undefined)
|
|
640
|
+
return {};
|
|
641
|
+
if (typeof args !== 'object' || Array.isArray(args))
|
|
642
|
+
return { _args: describeShape(args) };
|
|
643
|
+
const out = {};
|
|
644
|
+
for (const [k, v] of Object.entries(args))
|
|
645
|
+
out[k] = describeShape(v);
|
|
646
|
+
return out;
|
|
647
|
+
}
|
|
648
|
+
/** INFO `Tool call: <name>` with meta `{ tool, args: summarizeArgs(args) }`. Never the argument values. */
|
|
649
|
+
export function logToolCall(name, args, context) {
|
|
650
|
+
getLogger().info(`Tool call: ${name}`, context ?? TOOL_CONTEXT, { tool: name, args: summarizeArgs(args) });
|
|
651
|
+
}
|
|
652
|
+
/**
|
|
653
|
+
* `Tool result: <name>` with meta `{ tool, isError, durationMs }` (durationMs rounded). INFO on
|
|
654
|
+
* success, WARN when isError. Never the result content.
|
|
655
|
+
*/
|
|
656
|
+
export function logToolResult(name, outcome, context) {
|
|
657
|
+
const meta = { tool: name, isError: outcome.isError, durationMs: Math.round(outcome.durationMs) };
|
|
658
|
+
if (outcome.isError)
|
|
659
|
+
getLogger().warn(`Tool result: ${name}`, context ?? TOOL_CONTEXT, meta);
|
|
660
|
+
else
|
|
661
|
+
getLogger().info(`Tool result: ${name}`, context ?? TOOL_CONTEXT, meta);
|
|
662
|
+
}
|
|
663
|
+
export default getLogger;
|
|
664
|
+
//# sourceMappingURL=logger.js.map
|