create-principles-disciple 1.147.7 → 1.147.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (84) hide show
  1. package/_release/product-identity.json +1 -1
  2. package/core/dist/host/host-installer.d.ts +15 -0
  3. package/core/dist/host/host-installer.d.ts.map +1 -1
  4. package/core/dist/host/host-installer.js.map +1 -1
  5. package/core/dist/runtime-v2/feature-flags/feature-flag-contract.js +1 -1
  6. package/core/dist/runtime-v2/feature-flags/feature-flag-contract.js.map +1 -1
  7. package/core/dist/runtime-v2/feature-flags/feature-flag-lifecycle.js +2 -2
  8. package/core/dist/runtime-v2/feature-flags/feature-flag-lifecycle.js.map +1 -1
  9. package/core/dist/runtime-v2/pain-recall-capability.d.ts.map +1 -1
  10. package/core/dist/runtime-v2/pain-recall-capability.js +53 -2
  11. package/core/dist/runtime-v2/pain-recall-capability.js.map +1 -1
  12. package/core/dist/runtime-v2/pain-recall-read-model.d.ts.map +1 -1
  13. package/core/dist/runtime-v2/pain-recall-read-model.js +60 -5
  14. package/core/dist/runtime-v2/pain-recall-read-model.js.map +1 -1
  15. package/core/dist/runtime-v2/signal-collector/keyword-stage.d.ts.map +1 -1
  16. package/core/dist/runtime-v2/signal-collector/keyword-stage.js +35 -0
  17. package/core/dist/runtime-v2/signal-collector/keyword-stage.js.map +1 -1
  18. package/core/package.json +1 -1
  19. package/dist/index.d.ts.map +1 -1
  20. package/dist/index.js +6 -29
  21. package/dist/index.js.map +1 -1
  22. package/dist/install-summary.d.ts +38 -0
  23. package/dist/install-summary.d.ts.map +1 -0
  24. package/dist/install-summary.js +115 -0
  25. package/dist/install-summary.js.map +1 -0
  26. package/dist/installer.d.ts +36 -0
  27. package/dist/installer.d.ts.map +1 -1
  28. package/dist/installer.js +120 -26
  29. package/dist/installer.js.map +1 -1
  30. package/dist/installers/workbuddy-host-installer.d.ts +51 -17
  31. package/dist/installers/workbuddy-host-installer.d.ts.map +1 -1
  32. package/dist/installers/workbuddy-host-installer.js +510 -186
  33. package/dist/installers/workbuddy-host-installer.js.map +1 -1
  34. package/dist/prompts.js +1 -1
  35. package/dist/prompts.js.map +1 -1
  36. package/host-runtime/dist/governance-signal-admission.d.ts +2 -2
  37. package/host-runtime/dist/governance-signal-admission.js +2 -6
  38. package/host-runtime/package.json +1 -1
  39. package/package.json +3 -3
  40. package/pd-cli/dist/commands/health-mcp.d.ts +19 -0
  41. package/pd-cli/dist/commands/health-mcp.d.ts.map +1 -1
  42. package/pd-cli/dist/commands/health-mcp.js +226 -0
  43. package/pd-cli/dist/commands/health-mcp.js.map +1 -1
  44. package/pd-cli/dist/commands/health.d.ts.map +1 -1
  45. package/pd-cli/dist/commands/health.js +18 -1
  46. package/pd-cli/dist/commands/health.js.map +1 -1
  47. package/pd-cli/dist/commands/mcp-logs-export.d.ts +7 -0
  48. package/pd-cli/dist/commands/mcp-logs-export.d.ts.map +1 -0
  49. package/pd-cli/dist/commands/mcp-logs-export.js +267 -0
  50. package/pd-cli/dist/commands/mcp-logs-export.js.map +1 -0
  51. package/pd-cli/dist/commands/workbuddy-worker.d.ts.map +1 -1
  52. package/pd-cli/dist/commands/workbuddy-worker.js +2 -1
  53. package/pd-cli/dist/commands/workbuddy-worker.js.map +1 -1
  54. package/pd-cli/dist/index.js +35 -0
  55. package/pd-cli/dist/index.js.map +1 -1
  56. package/pd-cli/dist/services/zip-writer.d.ts +6 -0
  57. package/pd-cli/dist/services/zip-writer.d.ts.map +1 -0
  58. package/pd-cli/dist/services/zip-writer.js +97 -0
  59. package/pd-cli/dist/services/zip-writer.js.map +1 -0
  60. package/pd-mcp-server/dist/index.d.ts +1 -0
  61. package/pd-mcp-server/dist/index.js +167 -10
  62. package/pd-mcp-server/dist/log-file.d.ts +26 -0
  63. package/pd-mcp-server/dist/log-file.js +312 -0
  64. package/pd-mcp-server/dist/logging.d.ts +2 -1
  65. package/pd-mcp-server/dist/logging.js +25 -14
  66. package/pd-mcp-server/dist/server.js +78 -24
  67. package/pd-mcp-server/dist/version.d.ts +1 -1
  68. package/pd-mcp-server/dist/version.js +1 -1
  69. package/pd-mcp-server/dist/workspace.d.ts +3 -0
  70. package/pd-mcp-server/dist/workspace.js +5 -0
  71. package/pd-mcp-server/package.json +1 -1
  72. package/plugin/dist/bundle.js +274 -273
  73. package/plugin/dist/openclaw.plugin.json +1 -1
  74. package/plugin/mcp/launcher.cjs +561 -0
  75. package/plugin/openclaw.plugin.json +1 -1
  76. package/plugin/package.json +1 -1
  77. package/release-manager/dist/installer.js +120 -26
  78. package/release-manager/dist/installers/workbuddy-host-installer.js +510 -186
  79. package/release-manager/package.json +1 -1
  80. package/workbuddy-adapter/dist/pd-hook.js +3 -2
  81. package/workbuddy-adapter/package.json +1 -1
  82. package/workbuddy-adapter/plugin/.codebuddy-plugin/plugin.json +1 -1
  83. package/workbuddy-adapter/plugin/mcp/launcher.cjs +215 -1
  84. package/workbuddy-adapter/plugin/skills/pd-pain-signal/SKILL.md +4 -0
@@ -0,0 +1,97 @@
1
+ /**
2
+ * Minimal ZIP writer (deflate + UTF-8 names) for the MCP log export command.
3
+ *
4
+ * The repository ships no zip dependency and the export must work offline on
5
+ * an Owner machine; the format needed here is the small, well-specified subset:
6
+ * local headers, one central directory, end-of-central-directory. Archives are
7
+ * bounded by the caller (≤20 MiB) and contain only text files.
8
+ */
9
+ import { deflateRawSync } from 'node:zlib';
10
+ const LOCAL_FILE_HEADER = 0x04034b50;
11
+ const CENTRAL_DIRECTORY_HEADER = 0x02014b50;
12
+ const END_OF_CENTRAL_DIRECTORY = 0x06054b50;
13
+ const UTF8_FLAG = 0x0800;
14
+ const DEFLATE_METHOD = 8;
15
+ const VERSION_NEEDED = 20;
16
+ const CRC_TABLE = (() => {
17
+ const table = new Uint32Array(256);
18
+ for (let i = 0; i < 256; i += 1) {
19
+ let value = i;
20
+ for (let bit = 0; bit < 8; bit += 1) {
21
+ value = value & 1 ? 0xedb88320 ^ (value >>> 1) : value >>> 1;
22
+ }
23
+ table[i] = value >>> 0;
24
+ }
25
+ return table;
26
+ })();
27
+ function crc32(buffer) {
28
+ let crc = 0xffffffff;
29
+ for (const byte of buffer) {
30
+ const table = CRC_TABLE[(crc ^ byte) & 0xff] ?? 0;
31
+ crc = table ^ (crc >>> 8);
32
+ }
33
+ return (crc ^ 0xffffffff) >>> 0;
34
+ }
35
+ /** DOS date/time (local); fixed to the generation time. */
36
+ function dosDateTime(date) {
37
+ const time = (date.getHours() << 11) | (date.getMinutes() << 5) | Math.floor(date.getSeconds() / 2);
38
+ const dosDate = ((date.getFullYear() - 1980) << 9) | ((date.getMonth() + 1) << 5) | date.getDate();
39
+ return { time, date: dosDate };
40
+ }
41
+ export function buildZip(entries, generatedAt = new Date()) {
42
+ const stamp = dosDateTime(generatedAt);
43
+ const chunks = [];
44
+ const central = [];
45
+ let offset = 0;
46
+ for (const entry of entries) {
47
+ const name = Buffer.from(entry.name, 'utf8');
48
+ const compressed = deflateRawSync(entry.data);
49
+ const crc = crc32(entry.data);
50
+ const local = Buffer.alloc(30);
51
+ local.writeUInt32LE(LOCAL_FILE_HEADER, 0);
52
+ local.writeUInt16LE(VERSION_NEEDED, 4);
53
+ local.writeUInt16LE(UTF8_FLAG, 6);
54
+ local.writeUInt16LE(DEFLATE_METHOD, 8);
55
+ local.writeUInt16LE(stamp.time, 10);
56
+ local.writeUInt16LE(stamp.date, 12);
57
+ local.writeUInt32LE(crc, 14);
58
+ local.writeUInt32LE(compressed.length, 18);
59
+ local.writeUInt32LE(entry.data.length, 22);
60
+ local.writeUInt16LE(name.length, 26);
61
+ local.writeUInt16LE(0, 28);
62
+ chunks.push(local, name, compressed);
63
+ const directory = Buffer.alloc(46);
64
+ directory.writeUInt32LE(CENTRAL_DIRECTORY_HEADER, 0);
65
+ directory.writeUInt16LE(VERSION_NEEDED, 4);
66
+ directory.writeUInt16LE(VERSION_NEEDED, 6);
67
+ directory.writeUInt16LE(UTF8_FLAG, 8);
68
+ directory.writeUInt16LE(DEFLATE_METHOD, 10);
69
+ directory.writeUInt16LE(stamp.time, 12);
70
+ directory.writeUInt16LE(stamp.date, 14);
71
+ directory.writeUInt32LE(crc, 16);
72
+ directory.writeUInt32LE(compressed.length, 20);
73
+ directory.writeUInt32LE(entry.data.length, 24);
74
+ directory.writeUInt16LE(name.length, 28);
75
+ directory.writeUInt16LE(0, 30);
76
+ directory.writeUInt16LE(0, 32);
77
+ directory.writeUInt16LE(0, 34);
78
+ directory.writeUInt16LE(0, 36);
79
+ directory.writeUInt32LE(0, 38);
80
+ directory.writeUInt32LE(offset, 42);
81
+ central.push(directory, name);
82
+ offset += local.length + name.length + compressed.length;
83
+ }
84
+ const centralOffset = offset;
85
+ const centralSize = central.reduce((size, chunk) => size + chunk.length, 0);
86
+ const end = Buffer.alloc(22);
87
+ end.writeUInt32LE(END_OF_CENTRAL_DIRECTORY, 0);
88
+ end.writeUInt16LE(0, 4);
89
+ end.writeUInt16LE(0, 6);
90
+ end.writeUInt16LE(entries.length, 8);
91
+ end.writeUInt16LE(entries.length, 10);
92
+ end.writeUInt32LE(centralSize, 12);
93
+ end.writeUInt32LE(centralOffset, 16);
94
+ end.writeUInt16LE(0, 20);
95
+ return Buffer.concat([...chunks, ...central, end]);
96
+ }
97
+ //# sourceMappingURL=zip-writer.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"zip-writer.js","sourceRoot":"","sources":["../../src/services/zip-writer.ts"],"names":[],"mappings":"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"}
@@ -3,6 +3,7 @@ export { resolveWorkspaceBinding, WORKSPACE_DIR_ENV, HOST_ENV } from './workspac
3
3
  export { RESPONSE_BUDGET_BYTES, withinBudget, measureTextBytes } from './budget.js';
4
4
  export { DEFAULT_DEADLINE_MS, MAX_TRACKED_ORPHANS, runWithDeadline, OrphanLimiter } from './deadline.js';
5
5
  export { SERVER_NAME, SERVER_VERSION, MCP_SDK_PIN } from './version.js';
6
+ export { LOG_ENV, LOG_DIR_ENV, LOG_FILE_ENV, LOG_MAX_FILE_BYTES, LOG_MAX_DIR_BYTES, LOG_RETENTION_MS, createMcpFileLog, combineSinks, } from './log-file.js';
6
7
  /**
7
8
  * Run the stdio server against the real process env. Returns the intended
8
9
  * process exit code: 0 after a clean shutdown, 1 for a fail-closed startup.
@@ -11,7 +11,10 @@
11
11
  * suppressed), release resources, exit 0.
12
12
  *
13
13
  * stdout carries MCP protocol messages ONLY (§5.3). This module never
14
- * console.log's; logs go through the sanitized stderr logger.
14
+ * console.log's; logs go through the sanitized stderr logger and, when
15
+ * enabled, the durable JSONL file sink (`~/.pd/logs/mcp`). File logging is
16
+ * strictly observational: every failure is swallowed and can never change a
17
+ * result, the protocol stream, the 32 KiB budget or the 10 s deadline.
15
18
  */
16
19
  import { StdioServerTransport } from '@modelcontextprotocol/server/stdio';
17
20
  import { createHash } from 'node:crypto';
@@ -19,27 +22,166 @@ import * as path from 'node:path';
19
22
  import { pathToFileURL } from 'node:url';
20
23
  import { createPdMcpServer } from './server.js';
21
24
  import { resolveWorkspaceBinding } from './workspace.js';
22
- import { stderrLog } from './logging.js';
25
+ import { combineSinks, createMcpFileLog } from './log-file.js';
26
+ import { createLogger, stderrSink } from './logging.js';
23
27
  export { createPdMcpServer, TOOL_NAME, toolInputSchema } from './server.js';
24
28
  export { resolveWorkspaceBinding, WORKSPACE_DIR_ENV, HOST_ENV } from './workspace.js';
25
29
  export { RESPONSE_BUDGET_BYTES, withinBudget, measureTextBytes } from './budget.js';
26
30
  export { DEFAULT_DEADLINE_MS, MAX_TRACKED_ORPHANS, runWithDeadline, OrphanLimiter } from './deadline.js';
27
31
  export { SERVER_NAME, SERVER_VERSION, MCP_SDK_PIN } from './version.js';
32
+ export { LOG_ENV, LOG_DIR_ENV, LOG_FILE_ENV, LOG_MAX_FILE_BYTES, LOG_MAX_DIR_BYTES, LOG_RETENTION_MS, createMcpFileLog, combineSinks, } from './log-file.js';
33
+ function isRecord(value) {
34
+ return value !== null && typeof value === 'object' && !Array.isArray(value);
35
+ }
36
+ /** Bounded, path-free label for client names / versions / protocol ids. */
37
+ function safeLabel(value, max = 48) {
38
+ if (typeof value !== 'string')
39
+ return undefined;
40
+ const cleaned = value.replace(/[^\x20-\x7e]/g, '').replace(/[^\w.#/+:@-]/g, '').slice(0, max);
41
+ return cleaned.length > 0 ? cleaned : undefined;
42
+ }
43
+ function safeId(value) {
44
+ if (typeof value === 'string')
45
+ return safeLabel(value, 40);
46
+ if (typeof value === 'number' && Number.isFinite(value))
47
+ return String(value);
48
+ return undefined;
49
+ }
50
+ function byteLength(value) {
51
+ try {
52
+ return Buffer.byteLength(JSON.stringify(value) ?? '', 'utf8');
53
+ }
54
+ catch {
55
+ return 0;
56
+ }
57
+ }
58
+ const OBSERVED_METHODS = ['initialize', 'tools/list'];
59
+ /**
60
+ * Stdio transport that observes — never alters — the JSON-RPC stream.
61
+ * `initialize` / `tools/list` are otherwise invisible to the server layer:
62
+ * MCP only exposes tool callbacks, so a handshake failure would leave no
63
+ * evidence at all. Observation is wrapped in try/catch and always forwards
64
+ * the untouched message to the SDK.
65
+ */
66
+ class ObservingStdioTransport extends StdioServerTransport {
67
+ observeLog;
68
+ pending = new Map();
69
+ constructor(observeLog) {
70
+ super();
71
+ this.observeLog = observeLog;
72
+ }
73
+ async start() {
74
+ const inner = this.onmessage;
75
+ this.onmessage = (message) => {
76
+ try {
77
+ this.observeInbound(message);
78
+ }
79
+ catch {
80
+ // Observation must never break the protocol.
81
+ }
82
+ inner?.(message);
83
+ };
84
+ return super.start();
85
+ }
86
+ async send(message) {
87
+ try {
88
+ this.observeOutbound(message);
89
+ }
90
+ catch {
91
+ // Observation must never break the protocol.
92
+ }
93
+ return super.send(message);
94
+ }
95
+ observeInbound(message) {
96
+ if (!isRecord(message) || typeof message.method !== 'string')
97
+ return;
98
+ const { method } = message;
99
+ if (!OBSERVED_METHODS.includes(method))
100
+ return;
101
+ const id = safeId(message.id);
102
+ if (id === undefined)
103
+ return;
104
+ const params = isRecord(message.params) ? message.params : {};
105
+ const clientInfo = isRecord(params.clientInfo) ? params.clientInfo : {};
106
+ const capabilities = isRecord(params.capabilities) ? params.capabilities : {};
107
+ this.pending.set(id, {
108
+ method: method === 'initialize' ? 'initialize' : 'tools/list',
109
+ startedAt: Date.now(),
110
+ });
111
+ if (method === 'initialize') {
112
+ this.observeLog('info', 'initialize', {
113
+ phase: 'request',
114
+ requestId: id,
115
+ clientName: safeLabel(clientInfo.name) ?? 'unknown',
116
+ clientVersion: safeLabel(clientInfo.version) ?? 'unknown',
117
+ protocolVersion: safeLabel(params.protocolVersion) ?? 'unknown',
118
+ capabilityKeysCount: Object.keys(capabilities).length,
119
+ });
120
+ }
121
+ else {
122
+ this.observeLog('info', 'tools_list', { phase: 'request', requestId: id });
123
+ }
124
+ }
125
+ observeOutbound(message) {
126
+ if (!isRecord(message))
127
+ return;
128
+ const id = safeId(message.id);
129
+ if (id === undefined)
130
+ return;
131
+ const pending = this.pending.get(id);
132
+ if (pending === undefined)
133
+ return;
134
+ this.pending.delete(id);
135
+ const error = isRecord(message.error) ? message.error : undefined;
136
+ const result = isRecord(message.result) ? message.result : undefined;
137
+ const ok = error === undefined;
138
+ const baseFields = {
139
+ phase: 'response',
140
+ requestId: id,
141
+ ok,
142
+ durationMs: Date.now() - pending.startedAt,
143
+ responseBytes: byteLength(message),
144
+ };
145
+ if (pending.method === 'initialize') {
146
+ const fields = { ...baseFields };
147
+ if (!ok)
148
+ fields.errorCode = safeId(error.code) ?? 'unknown_error';
149
+ this.observeLog(ok ? 'info' : 'error', 'initialize', fields);
150
+ }
151
+ else {
152
+ const tools = Array.isArray(result?.tools) ? result?.tools : undefined;
153
+ const fields = {
154
+ ...baseFields,
155
+ toolCount: tools?.length ?? 0,
156
+ };
157
+ if (!ok)
158
+ fields.errorCode = safeId(error.code) ?? 'unknown_error';
159
+ this.observeLog(ok ? 'info' : 'error', 'tools_list', fields);
160
+ }
161
+ }
162
+ }
28
163
  /**
29
164
  * Run the stdio server against the real process env. Returns the intended
30
165
  * process exit code: 0 after a clean shutdown, 1 for a fail-closed startup.
31
166
  */
32
167
  export async function main(env = process.env) {
168
+ const fileLog = createMcpFileLog(env);
169
+ const sink = fileLog.enabled ? combineSinks(stderrSink, fileLog.sink) : stderrSink;
170
+ const log = createLogger(sink);
33
171
  const binding = resolveWorkspaceBinding(env);
34
172
  if (!binding.ok) {
35
173
  // Fail-closed BEFORE initialize: one sanitized stderr line with the
36
- // reason and the next action; nothing about values or paths.
37
- stderrLog('error', 'startup_binding_failed', {
38
- envVar: binding.envVar,
174
+ // reason and the next action; nothing about values or paths. The same
175
+ // fact lands on disk as `startup_failure` so a host that cannot even
176
+ // reach `initialize` still leaves PD-side evidence.
177
+ log('error', 'startup_failure', {
178
+ stage: 'server',
39
179
  reasonCategory: 'invalid_binding',
40
- reason: binding.reason,
180
+ reasonCode: binding.reasonCode,
181
+ envVar: binding.envVar,
41
182
  nextAction: binding.nextAction,
42
183
  });
184
+ fileLog.flushSync();
43
185
  return 1;
44
186
  }
45
187
  // Workspace fingerprint (NOT the path): lets operators and acceptance runs
@@ -50,16 +192,31 @@ export async function main(env = process.env) {
50
192
  .update(path.resolve(binding.binding.workspaceDir))
51
193
  .digest('hex')
52
194
  .slice(0, 16);
53
- stderrLog('info', 'startup', { host: binding.binding.host ?? 'unspecified', workspaceFingerprint });
54
- const handle = createPdMcpServer({ workspaceDir: binding.binding.workspaceDir, host: binding.binding.host });
55
- const transport = new StdioServerTransport();
195
+ log('info', 'startup', {
196
+ host: binding.binding.host ?? 'unspecified',
197
+ workspaceFingerprint,
198
+ nodeVersion: process.version,
199
+ platform: process.platform,
200
+ });
201
+ const handle = createPdMcpServer({
202
+ workspaceDir: binding.binding.workspaceDir,
203
+ host: binding.binding.host,
204
+ logSink: sink,
205
+ });
206
+ const transport = new ObservingStdioTransport(log);
56
207
  await handle.mcpServer.connect(transport);
57
208
  // Host-driven shutdown: stdin close → SDK closes the transport → we stop
58
209
  // accepting new work, let in-flight calls drain (bounded by the deadline)
59
210
  // and exit cleanly. The transport close callback is the single trigger.
60
211
  transport.onclose = () => {
61
212
  handle.beginShutdown();
62
- stderrLog('info', 'transport_closed', { orphans: handle.orphans.size });
213
+ log('info', 'shutdown', {
214
+ reason: 'stdin_closed',
215
+ orphans: handle.orphans.size,
216
+ active: handle.admissions.active,
217
+ uptimeMs: Math.round(process.uptime() * 1000),
218
+ });
219
+ fileLog.flushSync();
63
220
  };
64
221
  return 0;
65
222
  }
@@ -0,0 +1,26 @@
1
+ export declare const LOG_ENV: 'PD_MCP_LOG';
2
+ export declare const LOG_DIR_ENV: 'PD_MCP_LOG_DIR';
3
+ /** Internal launcher → server contract (basename only, never a path). */
4
+ export declare const LOG_FILE_ENV: 'PD_MCP_LOG_FILE';
5
+ export declare const LOG_RUN_ID_ENV: 'PD_MCP_LOG_RUN_ID';
6
+ export declare const LOG_SEQ_ENV: 'PD_MCP_LOG_SEQ';
7
+ export declare const LOG_MAX_FILE_BYTES: number;
8
+ export declare const LOG_MAX_DIR_BYTES: number;
9
+ export declare const LOG_RETENTION_MS: number;
10
+ export declare const LOG_MAX_SHARDS = 3;
11
+ export interface McpFileLog {
12
+ /** False when `PD_MCP_LOG=0` or the log directory cannot be resolved. */
13
+ readonly enabled: boolean;
14
+ readonly sink: (line: string) => void;
15
+ /** Flush pending lines synchronously; used on startup failure/shutdown. */
16
+ flushSync(): void;
17
+ readonly dir: string | null;
18
+ readonly file: string | null;
19
+ }
20
+ /**
21
+ * Create the process log sink. Never throws: an unusable directory degrades to
22
+ * a running-but-silent sink (stderr logging stays untouched).
23
+ */
24
+ export declare function createMcpFileLog(env?: Record<string, unknown>, clock?: () => Date): McpFileLog;
25
+ /** Compose sinks; a broken sink never prevents the others from running. */
26
+ export declare function combineSinks(...sinks: readonly ((line: string) => void)[]): (line: string) => void;
@@ -0,0 +1,312 @@
1
+ /**
2
+ * Durable JSONL sink for PD MCP observability (default ON).
3
+ *
4
+ * Every entry the process already emits through `createLogger` is also appended
5
+ * to `~/.pd/logs/mcp/mcp-<host>-<pid>-<stamp>.jsonl`. This module is
6
+ * deliberately defensive: no export ever throws, and a failure to write logs
7
+ * can only lose log lines — it can never change a tool result, the stdio
8
+ * protocol stream, the 32 KiB budget or the 10 s deadline.
9
+ *
10
+ * Privacy: the sink only enriches entries the existing scalar-whitelist logger
11
+ * produced. It never accepts bodies, paths or env values; fields are counts,
12
+ * enums, booleans and fingerprints.
13
+ *
14
+ * Limits: one file per process (the launcher opens the file first and passes
15
+ * its basename through the internal env so the server appends instead of
16
+ * opening a second one), 2 MiB per file, at most 3 shards per process, 64 MiB
17
+ * per directory, 14 days retention. Maintenance runs best-effort at open and
18
+ * rotation time.
19
+ */
20
+ import * as fs from 'node:fs';
21
+ import * as os from 'node:os';
22
+ import * as path from 'node:path';
23
+ import { randomBytes } from 'node:crypto';
24
+ export const LOG_ENV = 'PD_MCP_LOG';
25
+ export const LOG_DIR_ENV = 'PD_MCP_LOG_DIR';
26
+ /** Internal launcher → server contract (basename only, never a path). */
27
+ export const LOG_FILE_ENV = 'PD_MCP_LOG_FILE';
28
+ export const LOG_RUN_ID_ENV = 'PD_MCP_LOG_RUN_ID';
29
+ export const LOG_SEQ_ENV = 'PD_MCP_LOG_SEQ';
30
+ export const LOG_MAX_FILE_BYTES = 2 * 1024 * 1024;
31
+ export const LOG_MAX_DIR_BYTES = 64 * 1024 * 1024;
32
+ export const LOG_RETENTION_MS = 14 * 24 * 60 * 60 * 1000;
33
+ export const LOG_MAX_SHARDS = 3;
34
+ const MAX_BUFFER_LINES = 200;
35
+ const MAX_BUFFER_BYTES = 256 * 1024;
36
+ const DISABLED_VALUES = ['0', 'false', 'off'];
37
+ const ADOPTED_FILE = /^mcp-[^\\/:*?"<>|\s]{1,120}\.jsonl$/;
38
+ const RUN_ID = /^[0-9a-f]{8}$/;
39
+ function isRecord(value) {
40
+ return value !== null && typeof value === 'object' && !Array.isArray(value);
41
+ }
42
+ function envString(env, key) {
43
+ const value = env[key];
44
+ return typeof value === 'string' ? value : undefined;
45
+ }
46
+ function isEnabled(env) {
47
+ const value = envString(env, LOG_ENV);
48
+ if (value === undefined)
49
+ return true;
50
+ return !DISABLED_VALUES.includes(value.trim().toLowerCase());
51
+ }
52
+ function resolveDir(env) {
53
+ const override = envString(env, LOG_DIR_ENV);
54
+ if (override !== undefined && override.trim().length > 0)
55
+ return path.resolve(override);
56
+ try {
57
+ return path.join(os.homedir(), '.pd', 'logs', 'mcp');
58
+ }
59
+ catch {
60
+ return null;
61
+ }
62
+ }
63
+ function resolveAdoptedFile(env) {
64
+ const name = envString(env, LOG_FILE_ENV);
65
+ return name !== undefined && ADOPTED_FILE.test(name) ? name : null;
66
+ }
67
+ function resolveRunId(env) {
68
+ const given = envString(env, LOG_RUN_ID_ENV);
69
+ if (given !== undefined && RUN_ID.test(given))
70
+ return given;
71
+ return randomBytes(4).toString('hex');
72
+ }
73
+ function resolveSeq(env) {
74
+ const raw = envString(env, LOG_SEQ_ENV);
75
+ const parsed = raw === undefined ? Number.NaN : Number.parseInt(raw, 10);
76
+ return Number.isInteger(parsed) && parsed >= 0 && parsed < 1_000_000 ? parsed : 0;
77
+ }
78
+ function pad(value, width = 2) {
79
+ return String(value).padStart(width, '0');
80
+ }
81
+ /** RFC3339 with a numeric local offset, plus the offset in minutes. */
82
+ function localTimestamp(date) {
83
+ const offsetMinutes = -date.getTimezoneOffset();
84
+ const sign = offsetMinutes >= 0 ? '+' : '-';
85
+ const abs = Math.abs(offsetMinutes);
86
+ const ts = `${pad(date.getFullYear(), 4)}-${pad(date.getMonth() + 1)}-${pad(date.getDate())}` +
87
+ `T${pad(date.getHours())}:${pad(date.getMinutes())}:${pad(date.getSeconds())}.${pad(date.getMilliseconds(), 3)}` +
88
+ `${sign}${pad(Math.floor(abs / 60))}:${pad(abs % 60)}`;
89
+ return { ts, tzOffsetMinutes: offsetMinutes };
90
+ }
91
+ function utcStamp(date) {
92
+ return (`${pad(date.getUTCFullYear(), 4)}${pad(date.getUTCMonth() + 1)}${pad(date.getUTCDate())}` +
93
+ `T${pad(date.getUTCHours())}${pad(date.getUTCMinutes())}${pad(date.getUTCSeconds())}Z`);
94
+ }
95
+ function listOwnLogFiles(dir) {
96
+ try {
97
+ const entries = fs.readdirSync(dir, { withFileTypes: true });
98
+ const out = [];
99
+ for (const entry of entries) {
100
+ if (!entry.isFile() || !/^mcp-.*\.jsonl$/.test(entry.name))
101
+ continue;
102
+ const fullPath = path.join(dir, entry.name);
103
+ try {
104
+ const stat = fs.statSync(fullPath);
105
+ out.push({ fullPath, size: stat.size, mtimeMs: stat.mtimeMs });
106
+ }
107
+ catch {
108
+ // Concurrent cleanup removed it — ignore.
109
+ }
110
+ }
111
+ return out;
112
+ }
113
+ catch {
114
+ return [];
115
+ }
116
+ }
117
+ /** Best-effort retention + directory cap. Never throws, never deletes non-PD files. */
118
+ function maintainDirectory(dir, nowMs, keepPath) {
119
+ try {
120
+ const files = listOwnLogFiles(dir);
121
+ const survivors = [];
122
+ for (const file of files) {
123
+ if (nowMs - file.mtimeMs > LOG_RETENTION_MS) {
124
+ if (file.fullPath === keepPath) {
125
+ survivors.push(file);
126
+ continue;
127
+ }
128
+ try {
129
+ fs.unlinkSync(file.fullPath);
130
+ }
131
+ catch {
132
+ survivors.push(file);
133
+ }
134
+ }
135
+ else {
136
+ survivors.push(file);
137
+ }
138
+ }
139
+ let total = survivors.reduce((sum, file) => sum + file.size, 0);
140
+ if (total <= LOG_MAX_DIR_BYTES)
141
+ return;
142
+ const removable = survivors
143
+ .filter((file) => file.fullPath !== keepPath)
144
+ .sort((left, right) => left.mtimeMs - right.mtimeMs);
145
+ for (const file of removable) {
146
+ if (total <= LOG_MAX_DIR_BYTES)
147
+ break;
148
+ try {
149
+ fs.unlinkSync(file.fullPath);
150
+ total -= file.size;
151
+ }
152
+ catch {
153
+ // Another process owns it or it vanished — leave it.
154
+ }
155
+ }
156
+ }
157
+ catch {
158
+ // Maintenance is advisory only.
159
+ }
160
+ }
161
+ function sanitizeSegment(value) {
162
+ const cleaned = value.replace(/[^A-Za-z0-9_-]/g, '').slice(0, 40);
163
+ return cleaned.length > 0 ? cleaned : 'unknown';
164
+ }
165
+ const NOOP_LOG = {
166
+ enabled: false,
167
+ sink: () => undefined,
168
+ flushSync: () => undefined,
169
+ dir: null,
170
+ file: null,
171
+ };
172
+ /**
173
+ * Create the process log sink. Never throws: an unusable directory degrades to
174
+ * a running-but-silent sink (stderr logging stays untouched).
175
+ */
176
+ export function createMcpFileLog(env = process.env, clock = () => new Date()) {
177
+ try {
178
+ if (!isEnabled(env))
179
+ return NOOP_LOG;
180
+ const dir = resolveDir(env);
181
+ if (dir === null)
182
+ return NOOP_LOG;
183
+ const openedAt = clock();
184
+ try {
185
+ fs.mkdirSync(dir, { recursive: true, mode: 0o700 });
186
+ if (!fs.statSync(dir).isDirectory())
187
+ return { ...NOOP_LOG, enabled: true, dir };
188
+ }
189
+ catch {
190
+ // Directory unavailable (e.g. a file occupies the path): stay silent.
191
+ return { ...NOOP_LOG, enabled: true, dir };
192
+ }
193
+ maintainDirectory(dir, openedAt.getTime(), null);
194
+ const host = sanitizeSegment(envString(env, 'PD_MCP_HOST') ?? 'unknown');
195
+ const adopted = resolveAdoptedFile(env);
196
+ const baseName = adopted ?? `mcp-${host}-${process.pid}-${utcStamp(openedAt)}.jsonl`;
197
+ const runId = resolveRunId(env);
198
+ let seq = resolveSeq(env);
199
+ let shard = 1;
200
+ let currentPath = shard === 1 ? path.join(dir, baseName) : path.join(dir, baseName.replace(/\.jsonl$/, `-${shard}.jsonl`));
201
+ let currentBytes = 0;
202
+ try {
203
+ currentBytes = fs.statSync(currentPath).size;
204
+ }
205
+ catch {
206
+ currentBytes = 0;
207
+ }
208
+ if (currentBytes >= LOG_MAX_FILE_BYTES) {
209
+ shard = 2;
210
+ currentPath = path.join(dir, baseName.replace(/\.jsonl$/, `-${shard}.jsonl`));
211
+ currentBytes = 0;
212
+ }
213
+ let buffer = [];
214
+ let bufferBytes = 0;
215
+ let scheduled = false;
216
+ let exhausted = shard > LOG_MAX_SHARDS;
217
+ function rotate() {
218
+ if (shard >= LOG_MAX_SHARDS) {
219
+ exhausted = true;
220
+ return;
221
+ }
222
+ shard += 1;
223
+ currentPath = path.join(dir, baseName.replace(/\.jsonl$/, `-${shard}.jsonl`));
224
+ currentBytes = 0;
225
+ maintainDirectory(dir, clock().getTime(), currentPath);
226
+ }
227
+ function flush() {
228
+ scheduled = false;
229
+ if (buffer.length === 0)
230
+ return;
231
+ const batch = buffer.join('');
232
+ buffer = [];
233
+ bufferBytes = 0;
234
+ try {
235
+ const bytes = Buffer.byteLength(batch, 'utf8');
236
+ if (currentBytes + bytes > LOG_MAX_FILE_BYTES)
237
+ rotate();
238
+ if (exhausted || currentBytes + bytes > LOG_MAX_FILE_BYTES)
239
+ return;
240
+ fs.appendFileSync(currentPath, batch, { mode: 0o600 });
241
+ currentBytes += bytes;
242
+ }
243
+ catch {
244
+ // Drop the batch. Logging must never affect the caller.
245
+ }
246
+ }
247
+ function flushSync() {
248
+ try {
249
+ flush();
250
+ }
251
+ catch {
252
+ // Swallow even a synchronous flush defect.
253
+ }
254
+ }
255
+ function sink(line) {
256
+ try {
257
+ const parsed = JSON.parse(line);
258
+ if (!isRecord(parsed))
259
+ return;
260
+ const stamp = localTimestamp(clock());
261
+ parsed.ts = stamp.ts;
262
+ parsed.tzOffsetMinutes = stamp.tzOffsetMinutes;
263
+ parsed.runId = runId;
264
+ parsed.seq = (seq += 1);
265
+ const enriched = JSON.stringify(parsed);
266
+ buffer.push(enriched + '\n');
267
+ bufferBytes += Buffer.byteLength(enriched, 'utf8');
268
+ if (buffer.length >= MAX_BUFFER_LINES || bufferBytes >= MAX_BUFFER_BYTES)
269
+ flushSync();
270
+ else if (!scheduled) {
271
+ scheduled = true;
272
+ setImmediate(() => {
273
+ try {
274
+ flush();
275
+ }
276
+ catch {
277
+ // Swallow.
278
+ }
279
+ });
280
+ }
281
+ }
282
+ catch {
283
+ // Swallow: enrichment/serialization errors must not reach the server.
284
+ }
285
+ }
286
+ return {
287
+ enabled: true,
288
+ sink,
289
+ flushSync,
290
+ dir,
291
+ get file() {
292
+ return currentPath;
293
+ },
294
+ };
295
+ }
296
+ catch {
297
+ return { ...NOOP_LOG, enabled: true };
298
+ }
299
+ }
300
+ /** Compose sinks; a broken sink never prevents the others from running. */
301
+ export function combineSinks(...sinks) {
302
+ return (line) => {
303
+ for (const sink of sinks) {
304
+ try {
305
+ sink(line);
306
+ }
307
+ catch {
308
+ // Isolate sinks from each other.
309
+ }
310
+ }
311
+ };
312
+ }