@rubytech/create-realagent 1.0.615 → 1.0.616
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/package.json +1 -1
- package/payload/platform/config/brand.json +4 -0
- package/payload/platform/lib/mcp-stderr-tee/dist/index.d.ts +23 -13
- package/payload/platform/lib/mcp-stderr-tee/dist/index.d.ts.map +1 -1
- package/payload/platform/lib/mcp-stderr-tee/dist/index.js +86 -89
- package/payload/platform/lib/mcp-stderr-tee/dist/index.js.map +1 -1
- package/payload/platform/lib/mcp-stderr-tee/src/index.ts +86 -101
- package/payload/platform/plugins/admin/mcp/dist/index.js +33 -2
- package/payload/platform/plugins/admin/mcp/dist/index.js.map +1 -1
- package/payload/platform/plugins/admin/skills/stream-log-review/SKILL.md +22 -8
- package/payload/platform/plugins/cloudflare/PLUGIN.md +5 -4
- package/payload/platform/plugins/cloudflare/mcp/__tests__/auth-binding.test.ts +196 -0
- package/payload/platform/plugins/cloudflare/mcp/__tests__/brand-load.test.ts +81 -0
- package/payload/platform/plugins/cloudflare/mcp/__tests__/manifest-scope.test.ts +65 -0
- package/payload/platform/plugins/cloudflare/mcp/__tests__/verify-scenario-0.test.ts +70 -0
- package/payload/platform/plugins/cloudflare/mcp/__tests__/verify-scenario-B.test.ts +124 -0
- package/payload/platform/plugins/cloudflare/mcp/dist/index.js +221 -200
- package/payload/platform/plugins/cloudflare/mcp/dist/index.js.map +1 -1
- package/payload/platform/plugins/cloudflare/mcp/dist/lib/cloudflared.d.ts +174 -39
- package/payload/platform/plugins/cloudflare/mcp/dist/lib/cloudflared.d.ts.map +1 -1
- package/payload/platform/plugins/cloudflare/mcp/dist/lib/cloudflared.js +891 -194
- package/payload/platform/plugins/cloudflare/mcp/dist/lib/cloudflared.js.map +1 -1
- package/payload/platform/plugins/cloudflare/mcp/package.json +5 -2
- package/payload/platform/plugins/cloudflare/mcp/vitest.config.ts +10 -0
- package/payload/platform/plugins/cloudflare/references/setup-guide.md +31 -32
- package/payload/platform/plugins/cloudflare/skills/setup-tunnel/SKILL.md +25 -3
- package/payload/platform/plugins/docs/PLUGIN.md +2 -0
- package/payload/platform/plugins/docs/references/cloudflare.md +68 -0
- package/payload/platform/plugins/docs/references/plugins-guide.md +8 -6
- package/payload/platform/scripts/logs-read.sh +114 -54
- package/payload/platform/templates/specialists/agents/personal-assistant.md +12 -8
- package/payload/server/server.js +387 -71
package/package.json
CHANGED
|
@@ -15,27 +15,37 @@
|
|
|
15
15
|
* │ │ │
|
|
16
16
|
* │ ├──► original stderr (consumed by Claude Code — opaque)
|
|
17
17
|
* │ │
|
|
18
|
-
* │ ├──► mcp-{name}-stderr-{YYYY-MM-DD}.log (raw chunks
|
|
18
|
+
* │ ├──► mcp-{name}-stderr-{YYYY-MM-DD}.log (raw chunks, per-plugin)
|
|
19
19
|
* │ │
|
|
20
|
-
* │ └──►
|
|
20
|
+
* │ └──► $STREAM_LOG_PATH (per-line, prefixed)
|
|
21
21
|
* │ "[<iso>] [mcp:{name}] <line>"
|
|
22
|
-
* │ rotates on date boundary
|
|
23
22
|
* └─────────────────────────┘
|
|
24
23
|
*
|
|
25
|
-
*
|
|
26
|
-
*
|
|
27
|
-
*
|
|
28
|
-
*
|
|
24
|
+
* $STREAM_LOG_PATH is set by the spawner (`getMcpServers` in `claude-agent.ts`)
|
|
25
|
+
* to the per-conversation stream log file. The MCP server code itself knows
|
|
26
|
+
* nothing about conversations — it just trusts the spawner's path. This is
|
|
27
|
+
* the scope boundary introduced by Task 532: the tee is attached per spawn,
|
|
28
|
+
* not per MCP-server process lifetime. Servers spawned for conversation A
|
|
29
|
+
* write to conversation A's file; servers for B write to B's file.
|
|
30
|
+
*
|
|
31
|
+
* Every decision is logged via `[mcp-tee-*]` markers on both the target file
|
|
32
|
+
* and the original stderr so an investigator can confirm from the stream log
|
|
33
|
+
* which tees were wired up, which were skipped, and why.
|
|
29
34
|
*/
|
|
30
35
|
/**
|
|
31
|
-
* Patch process.stderr.write to tee to
|
|
32
|
-
*
|
|
36
|
+
* Patch process.stderr.write to tee to:
|
|
37
|
+
* 1. Per-server raw log (`mcp-<serverName>-stderr-<date>.log` under LOG_DIR).
|
|
38
|
+
* 2. The per-conversation stream log at `STREAM_LOG_PATH` — per-line, prefixed.
|
|
39
|
+
* 3. The original stderr (consumed by Claude Code) — always preserved.
|
|
33
40
|
*
|
|
34
|
-
*
|
|
35
|
-
*
|
|
41
|
+
* LOG_DIR absent → skip per-server raw file (dev mode / tool discovery).
|
|
42
|
+
* STREAM_LOG_PATH absent → skip stream-log destination (older spawner or
|
|
43
|
+
* standalone invocation). In both absent cases, stderr works unchanged —
|
|
44
|
+
* a `[mcp-tee-skip]` marker is written to original stderr so the skip is
|
|
45
|
+
* visible to journalctl-level readers.
|
|
36
46
|
*
|
|
37
|
-
* Safe to call once at MCP server module load.
|
|
38
|
-
*
|
|
47
|
+
* Safe to call once at MCP server module load. Refused on second call to
|
|
48
|
+
* prevent stacking patches.
|
|
39
49
|
*/
|
|
40
50
|
export declare function initStderrTee(serverName: string): void;
|
|
41
51
|
//# sourceMappingURL=index.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAiCG;AAeH;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,aAAa,CAAC,UAAU,EAAE,MAAM,GAAG,IAAI,CAyItD"}
|
|
@@ -16,17 +16,22 @@
|
|
|
16
16
|
* │ │ │
|
|
17
17
|
* │ ├──► original stderr (consumed by Claude Code — opaque)
|
|
18
18
|
* │ │
|
|
19
|
-
* │ ├──► mcp-{name}-stderr-{YYYY-MM-DD}.log (raw chunks
|
|
19
|
+
* │ ├──► mcp-{name}-stderr-{YYYY-MM-DD}.log (raw chunks, per-plugin)
|
|
20
20
|
* │ │
|
|
21
|
-
* │ └──►
|
|
21
|
+
* │ └──► $STREAM_LOG_PATH (per-line, prefixed)
|
|
22
22
|
* │ "[<iso>] [mcp:{name}] <line>"
|
|
23
|
-
* │ rotates on date boundary
|
|
24
23
|
* └─────────────────────────┘
|
|
25
24
|
*
|
|
26
|
-
*
|
|
27
|
-
*
|
|
28
|
-
*
|
|
29
|
-
*
|
|
25
|
+
* $STREAM_LOG_PATH is set by the spawner (`getMcpServers` in `claude-agent.ts`)
|
|
26
|
+
* to the per-conversation stream log file. The MCP server code itself knows
|
|
27
|
+
* nothing about conversations — it just trusts the spawner's path. This is
|
|
28
|
+
* the scope boundary introduced by Task 532: the tee is attached per spawn,
|
|
29
|
+
* not per MCP-server process lifetime. Servers spawned for conversation A
|
|
30
|
+
* write to conversation A's file; servers for B write to B's file.
|
|
31
|
+
*
|
|
32
|
+
* Every decision is logged via `[mcp-tee-*]` markers on both the target file
|
|
33
|
+
* and the original stderr so an investigator can confirm from the stream log
|
|
34
|
+
* which tees were wired up, which were skipped, and why.
|
|
30
35
|
*/
|
|
31
36
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
32
37
|
exports.initStderrTee = initStderrTee;
|
|
@@ -38,19 +43,23 @@ const node_string_decoder_1 = require("node:string_decoder");
|
|
|
38
43
|
// call would stack patches and double-log every chunk.
|
|
39
44
|
const INIT_MARKER = Symbol.for("maxy.mcpStderrTee.installed");
|
|
40
45
|
/**
|
|
41
|
-
* Patch process.stderr.write to tee to
|
|
42
|
-
*
|
|
46
|
+
* Patch process.stderr.write to tee to:
|
|
47
|
+
* 1. Per-server raw log (`mcp-<serverName>-stderr-<date>.log` under LOG_DIR).
|
|
48
|
+
* 2. The per-conversation stream log at `STREAM_LOG_PATH` — per-line, prefixed.
|
|
49
|
+
* 3. The original stderr (consumed by Claude Code) — always preserved.
|
|
43
50
|
*
|
|
44
|
-
*
|
|
45
|
-
*
|
|
51
|
+
* LOG_DIR absent → skip per-server raw file (dev mode / tool discovery).
|
|
52
|
+
* STREAM_LOG_PATH absent → skip stream-log destination (older spawner or
|
|
53
|
+
* standalone invocation). In both absent cases, stderr works unchanged —
|
|
54
|
+
* a `[mcp-tee-skip]` marker is written to original stderr so the skip is
|
|
55
|
+
* visible to journalctl-level readers.
|
|
46
56
|
*
|
|
47
|
-
* Safe to call once at MCP server module load.
|
|
48
|
-
*
|
|
57
|
+
* Safe to call once at MCP server module load. Refused on second call to
|
|
58
|
+
* prevent stacking patches.
|
|
49
59
|
*/
|
|
50
60
|
function initStderrTee(serverName) {
|
|
51
61
|
const logDir = process.env.LOG_DIR;
|
|
52
|
-
|
|
53
|
-
return; // Dev mode or tool discovery — stderr only
|
|
62
|
+
const streamLogPath = process.env.STREAM_LOG_PATH;
|
|
54
63
|
// Refuse repeat patches — stacking them would double-log every chunk
|
|
55
64
|
// and corrupt the re-entrancy guard (originalWrite would capture the
|
|
56
65
|
// already-patched function on the second call).
|
|
@@ -60,74 +69,59 @@ function initStderrTee(serverName) {
|
|
|
60
69
|
// from inside this module MUST go through originalWrite to avoid
|
|
61
70
|
// re-entering the patched writer (which would recurse on a tee failure).
|
|
62
71
|
const originalWrite = process.stderr.write.bind(process.stderr);
|
|
63
|
-
|
|
64
|
-
|
|
72
|
+
const tsPrefix = () => `[${new Date().toISOString()}]`;
|
|
73
|
+
const skipTee = (reason, destination) => {
|
|
74
|
+
originalWrite(`${tsPrefix()} [platform] [mcp-tee-skip] server=${serverName} destination=${destination} reason=${JSON.stringify(reason)}\n`);
|
|
75
|
+
};
|
|
76
|
+
// --- Destination 1: per-server raw file (optional, existing Task 362 behaviour)
|
|
65
77
|
let perServerStream;
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
78
|
+
if (logDir) {
|
|
79
|
+
try {
|
|
80
|
+
(0, node_fs_1.mkdirSync)(logDir, { recursive: true });
|
|
81
|
+
const date = new Date().toISOString().slice(0, 10);
|
|
82
|
+
perServerStream = (0, node_fs_1.createWriteStream)((0, node_path_1.resolve)(logDir, `mcp-${serverName}-stderr-${date}.log`), { flags: "a" });
|
|
83
|
+
perServerStream.on("error", (err) => {
|
|
84
|
+
originalWrite(`${tsPrefix()} [platform] [mcp-tee-error] server=${serverName} destination=per-server reason=${JSON.stringify(err.message)}\n`);
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
catch (err) {
|
|
88
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
89
|
+
skipTee(msg, "per-server");
|
|
90
|
+
perServerStream = undefined;
|
|
91
|
+
}
|
|
73
92
|
}
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
originalWrite(`[${new Date().toISOString()}] [platform] mcp stream tee disabled reason=${msg} server=${serverName} destination=per-server\n`);
|
|
77
|
-
// If the per-server file can't be opened, the LOG_DIR itself is likely
|
|
78
|
-
// unusable — give up on the whole tee rather than half-install it.
|
|
79
|
-
return;
|
|
93
|
+
else {
|
|
94
|
+
skipTee("LOG_DIR not set", "per-server");
|
|
80
95
|
}
|
|
81
|
-
//
|
|
82
|
-
// a long-lived MCP server started yesterday routes today's output into
|
|
83
|
-
// today's stream-log file, matching the platform's rotation semantics.
|
|
96
|
+
// --- Destination 2: per-conversation stream log (Task 532)
|
|
84
97
|
let streamLogStream;
|
|
85
|
-
|
|
86
|
-
let streamLogDisabledReason;
|
|
87
|
-
const openStreamLog = () => {
|
|
88
|
-
const today = new Date().toISOString().slice(0, 10);
|
|
89
|
-
if (streamLogStream && streamLogDate === today)
|
|
90
|
-
return streamLogStream;
|
|
91
|
-
// Date boundary crossed (or first open). Close the old stream before
|
|
92
|
-
// opening the new one so the file descriptor is released. Also clear
|
|
93
|
-
// any prior disabled reason — the previous failure may have been
|
|
94
|
-
// transient (EMFILE, ENOSPC), and the new day's path is fresh.
|
|
95
|
-
if (streamLogStream && streamLogDate !== today) {
|
|
96
|
-
try {
|
|
97
|
-
streamLogStream.end();
|
|
98
|
-
}
|
|
99
|
-
catch { /* ignore */ }
|
|
100
|
-
streamLogStream = undefined;
|
|
101
|
-
streamLogDisabledReason = undefined;
|
|
102
|
-
}
|
|
103
|
-
if (streamLogDisabledReason)
|
|
104
|
-
return undefined;
|
|
105
|
-
const path = (0, node_path_1.resolve)(logDir, `claude-agent-stream-${today}.log`);
|
|
98
|
+
if (streamLogPath) {
|
|
106
99
|
try {
|
|
107
|
-
|
|
100
|
+
(0, node_fs_1.mkdirSync)((0, node_path_1.dirname)(streamLogPath), { recursive: true });
|
|
101
|
+
const s = (0, node_fs_1.createWriteStream)(streamLogPath, { flags: "a" });
|
|
108
102
|
s.on("error", (err) => {
|
|
109
|
-
originalWrite(
|
|
103
|
+
originalWrite(`${tsPrefix()} [platform] [mcp-tee-error] server=${serverName} destination=stream-log reason=${JSON.stringify(err.message)}\n`);
|
|
110
104
|
});
|
|
111
105
|
streamLogStream = s;
|
|
112
|
-
|
|
113
|
-
//
|
|
114
|
-
|
|
115
|
-
s.write(`[${new Date().toISOString()}] [platform] attaching mcp stream tee for server=${serverName} logPath=${path}\n`);
|
|
116
|
-
return s;
|
|
106
|
+
// Attach marker lands in the stream log itself so investigators can
|
|
107
|
+
// confirm per-conversation the tee was wired up.
|
|
108
|
+
s.write(`${tsPrefix()} [platform] [mcp-tee-attach] server=${serverName} streamLogPath=${streamLogPath}\n`);
|
|
117
109
|
}
|
|
118
110
|
catch (err) {
|
|
119
111
|
const msg = err instanceof Error ? err.message : String(err);
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
originalWrite(line);
|
|
123
|
-
if (perServerStream && !perServerStream.destroyed)
|
|
124
|
-
perServerStream.write(line);
|
|
125
|
-
return undefined;
|
|
112
|
+
skipTee(msg, "stream-log");
|
|
113
|
+
streamLogStream = undefined;
|
|
126
114
|
}
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
115
|
+
}
|
|
116
|
+
else {
|
|
117
|
+
skipTee("STREAM_LOG_PATH not set", "stream-log");
|
|
118
|
+
}
|
|
119
|
+
// If neither tee target is available, leave stderr untouched — patching
|
|
120
|
+
// the writer to a no-op tee would still consume event-loop cycles on
|
|
121
|
+
// every write for zero observability gain.
|
|
122
|
+
if (!perServerStream && !streamLogStream) {
|
|
123
|
+
return;
|
|
124
|
+
}
|
|
131
125
|
// Line buffer — accumulates characters written to stderr so complete
|
|
132
126
|
// newline-terminated lines can be prefixed and emitted to the stream log.
|
|
133
127
|
// Partial chunks are held until a newline arrives (or beforeExit flushes).
|
|
@@ -139,6 +133,8 @@ function initStderrTee(serverName) {
|
|
|
139
133
|
// produces strings — but plugins piping child-process stderr can hit this.
|
|
140
134
|
const utf8 = new node_string_decoder_1.StringDecoder("utf8");
|
|
141
135
|
const emitCompleteLinesToStreamLog = (chunk) => {
|
|
136
|
+
if (!streamLogStream)
|
|
137
|
+
return;
|
|
142
138
|
lineBuffer += chunk;
|
|
143
139
|
let newlineIndex;
|
|
144
140
|
// eslint-disable-next-line no-cond-assign
|
|
@@ -147,10 +143,9 @@ function initStderrTee(serverName) {
|
|
|
147
143
|
lineBuffer = lineBuffer.slice(newlineIndex + 1);
|
|
148
144
|
if (line.length === 0)
|
|
149
145
|
continue; // skip blank lines
|
|
150
|
-
|
|
151
|
-
if (!stream || stream.destroyed || stream.writableEnded)
|
|
146
|
+
if (streamLogStream.destroyed || streamLogStream.writableEnded)
|
|
152
147
|
continue;
|
|
153
|
-
|
|
148
|
+
streamLogStream.write(`${tsPrefix()} [mcp:${serverName}] ${line}\n`);
|
|
154
149
|
}
|
|
155
150
|
};
|
|
156
151
|
process.stderr.write = (chunk, ...args) => {
|
|
@@ -158,16 +153,18 @@ function initStderrTee(serverName) {
|
|
|
158
153
|
if (perServerStream && !perServerStream.destroyed) {
|
|
159
154
|
perServerStream.write(chunk);
|
|
160
155
|
}
|
|
161
|
-
// 2. Stream log — per-line, prefixed
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
156
|
+
// 2. Stream log — per-line, prefixed.
|
|
157
|
+
if (streamLogStream) {
|
|
158
|
+
try {
|
|
159
|
+
const text = typeof chunk === "string"
|
|
160
|
+
? chunk
|
|
161
|
+
: utf8.write(Buffer.from(chunk));
|
|
162
|
+
emitCompleteLinesToStreamLog(text);
|
|
163
|
+
}
|
|
164
|
+
catch (err) {
|
|
165
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
166
|
+
originalWrite(`${tsPrefix()} [platform] [mcp-tee-emit-error] server=${serverName} reason=${JSON.stringify(msg)}\n`);
|
|
167
|
+
}
|
|
171
168
|
}
|
|
172
169
|
// 3. Original stderr — Claude Code still gets what it's always got.
|
|
173
170
|
return originalWrite(chunk, ...args);
|
|
@@ -181,11 +178,11 @@ function initStderrTee(serverName) {
|
|
|
181
178
|
// shutdown anyway. This hook exists for the rare caller that uses
|
|
182
179
|
// process.stderr.write without a trailing newline.
|
|
183
180
|
process.on("beforeExit", () => {
|
|
184
|
-
if (
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
181
|
+
if (streamLogStream && !streamLogStream.destroyed && !streamLogStream.writableEnded) {
|
|
182
|
+
if (lineBuffer.length > 0) {
|
|
183
|
+
streamLogStream.write(`${tsPrefix()} [mcp:${serverName}] ${lineBuffer}\n`);
|
|
184
|
+
}
|
|
185
|
+
streamLogStream.write(`${tsPrefix()} [platform] [mcp-tee-detach] server=${serverName} reason=process-before-exit\n`);
|
|
189
186
|
}
|
|
190
187
|
lineBuffer = "";
|
|
191
188
|
});
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";AAAA
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
|
|
@@ -15,17 +15,22 @@
|
|
|
15
15
|
* │ │ │
|
|
16
16
|
* │ ├──► original stderr (consumed by Claude Code — opaque)
|
|
17
17
|
* │ │
|
|
18
|
-
* │ ├──► mcp-{name}-stderr-{YYYY-MM-DD}.log (raw chunks
|
|
18
|
+
* │ ├──► mcp-{name}-stderr-{YYYY-MM-DD}.log (raw chunks, per-plugin)
|
|
19
19
|
* │ │
|
|
20
|
-
* │ └──►
|
|
20
|
+
* │ └──► $STREAM_LOG_PATH (per-line, prefixed)
|
|
21
21
|
* │ "[<iso>] [mcp:{name}] <line>"
|
|
22
|
-
* │ rotates on date boundary
|
|
23
22
|
* └─────────────────────────┘
|
|
24
23
|
*
|
|
25
|
-
*
|
|
26
|
-
*
|
|
27
|
-
*
|
|
28
|
-
*
|
|
24
|
+
* $STREAM_LOG_PATH is set by the spawner (`getMcpServers` in `claude-agent.ts`)
|
|
25
|
+
* to the per-conversation stream log file. The MCP server code itself knows
|
|
26
|
+
* nothing about conversations — it just trusts the spawner's path. This is
|
|
27
|
+
* the scope boundary introduced by Task 532: the tee is attached per spawn,
|
|
28
|
+
* not per MCP-server process lifetime. Servers spawned for conversation A
|
|
29
|
+
* write to conversation A's file; servers for B write to B's file.
|
|
30
|
+
*
|
|
31
|
+
* Every decision is logged via `[mcp-tee-*]` markers on both the target file
|
|
32
|
+
* and the original stderr so an investigator can confirm from the stream log
|
|
33
|
+
* which tees were wired up, which were skipped, and why.
|
|
29
34
|
*/
|
|
30
35
|
|
|
31
36
|
import {
|
|
@@ -33,7 +38,7 @@ import {
|
|
|
33
38
|
createWriteStream,
|
|
34
39
|
type WriteStream,
|
|
35
40
|
} from "node:fs";
|
|
36
|
-
import { resolve } from "node:path";
|
|
41
|
+
import { resolve, dirname } from "node:path";
|
|
37
42
|
import { StringDecoder } from "node:string_decoder";
|
|
38
43
|
|
|
39
44
|
// Marker on process.stderr.write so repeat calls to initStderrTee() are
|
|
@@ -42,18 +47,23 @@ import { StringDecoder } from "node:string_decoder";
|
|
|
42
47
|
const INIT_MARKER = Symbol.for("maxy.mcpStderrTee.installed");
|
|
43
48
|
|
|
44
49
|
/**
|
|
45
|
-
* Patch process.stderr.write to tee to
|
|
46
|
-
*
|
|
50
|
+
* Patch process.stderr.write to tee to:
|
|
51
|
+
* 1. Per-server raw log (`mcp-<serverName>-stderr-<date>.log` under LOG_DIR).
|
|
52
|
+
* 2. The per-conversation stream log at `STREAM_LOG_PATH` — per-line, prefixed.
|
|
53
|
+
* 3. The original stderr (consumed by Claude Code) — always preserved.
|
|
47
54
|
*
|
|
48
|
-
*
|
|
49
|
-
*
|
|
55
|
+
* LOG_DIR absent → skip per-server raw file (dev mode / tool discovery).
|
|
56
|
+
* STREAM_LOG_PATH absent → skip stream-log destination (older spawner or
|
|
57
|
+
* standalone invocation). In both absent cases, stderr works unchanged —
|
|
58
|
+
* a `[mcp-tee-skip]` marker is written to original stderr so the skip is
|
|
59
|
+
* visible to journalctl-level readers.
|
|
50
60
|
*
|
|
51
|
-
* Safe to call once at MCP server module load.
|
|
52
|
-
*
|
|
61
|
+
* Safe to call once at MCP server module load. Refused on second call to
|
|
62
|
+
* prevent stacking patches.
|
|
53
63
|
*/
|
|
54
64
|
export function initStderrTee(serverName: string): void {
|
|
55
65
|
const logDir = process.env.LOG_DIR;
|
|
56
|
-
|
|
66
|
+
const streamLogPath = process.env.STREAM_LOG_PATH;
|
|
57
67
|
|
|
58
68
|
// Refuse repeat patches — stacking them would double-log every chunk
|
|
59
69
|
// and corrupt the re-entrancy guard (originalWrite would capture the
|
|
@@ -65,83 +75,61 @@ export function initStderrTee(serverName: string): void {
|
|
|
65
75
|
// re-entering the patched writer (which would recurse on a tee failure).
|
|
66
76
|
const originalWrite = process.stderr.write.bind(process.stderr);
|
|
67
77
|
|
|
68
|
-
|
|
69
|
-
|
|
78
|
+
const tsPrefix = () => `[${new Date().toISOString()}]`;
|
|
79
|
+
const skipTee = (reason: string, destination: string) => {
|
|
80
|
+
originalWrite(`${tsPrefix()} [platform] [mcp-tee-skip] server=${serverName} destination=${destination} reason=${JSON.stringify(reason)}\n`);
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
// --- Destination 1: per-server raw file (optional, existing Task 362 behaviour)
|
|
70
84
|
let perServerStream: WriteStream | undefined;
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
perServerStream.on("error", (err) => {
|
|
79
|
-
originalWrite(
|
|
80
|
-
`[${new Date().toISOString()}] [platform] mcp stream tee per-server write error server=${serverName} reason=${err.message}\n`,
|
|
85
|
+
if (logDir) {
|
|
86
|
+
try {
|
|
87
|
+
mkdirSync(logDir, { recursive: true });
|
|
88
|
+
const date = new Date().toISOString().slice(0, 10);
|
|
89
|
+
perServerStream = createWriteStream(
|
|
90
|
+
resolve(logDir, `mcp-${serverName}-stderr-${date}.log`),
|
|
91
|
+
{ flags: "a" },
|
|
81
92
|
);
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
93
|
+
perServerStream.on("error", (err) => {
|
|
94
|
+
originalWrite(`${tsPrefix()} [platform] [mcp-tee-error] server=${serverName} destination=per-server reason=${JSON.stringify(err.message)}\n`);
|
|
95
|
+
});
|
|
96
|
+
} catch (err) {
|
|
97
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
98
|
+
skipTee(msg, "per-server");
|
|
99
|
+
perServerStream = undefined;
|
|
100
|
+
}
|
|
101
|
+
} else {
|
|
102
|
+
skipTee("LOG_DIR not set", "per-server");
|
|
91
103
|
}
|
|
92
104
|
|
|
93
|
-
//
|
|
94
|
-
// a long-lived MCP server started yesterday routes today's output into
|
|
95
|
-
// today's stream-log file, matching the platform's rotation semantics.
|
|
105
|
+
// --- Destination 2: per-conversation stream log (Task 532)
|
|
96
106
|
let streamLogStream: WriteStream | undefined;
|
|
97
|
-
|
|
98
|
-
let streamLogDisabledReason: string | undefined;
|
|
99
|
-
|
|
100
|
-
const openStreamLog = (): WriteStream | undefined => {
|
|
101
|
-
const today = new Date().toISOString().slice(0, 10);
|
|
102
|
-
if (streamLogStream && streamLogDate === today) return streamLogStream;
|
|
103
|
-
|
|
104
|
-
// Date boundary crossed (or first open). Close the old stream before
|
|
105
|
-
// opening the new one so the file descriptor is released. Also clear
|
|
106
|
-
// any prior disabled reason — the previous failure may have been
|
|
107
|
-
// transient (EMFILE, ENOSPC), and the new day's path is fresh.
|
|
108
|
-
if (streamLogStream && streamLogDate !== today) {
|
|
109
|
-
try { streamLogStream.end(); } catch { /* ignore */ }
|
|
110
|
-
streamLogStream = undefined;
|
|
111
|
-
streamLogDisabledReason = undefined;
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
if (streamLogDisabledReason) return undefined;
|
|
115
|
-
|
|
116
|
-
const path = resolve(logDir, `claude-agent-stream-${today}.log`);
|
|
107
|
+
if (streamLogPath) {
|
|
117
108
|
try {
|
|
118
|
-
|
|
109
|
+
mkdirSync(dirname(streamLogPath), { recursive: true });
|
|
110
|
+
const s = createWriteStream(streamLogPath, { flags: "a" });
|
|
119
111
|
s.on("error", (err) => {
|
|
120
|
-
originalWrite(
|
|
121
|
-
`[${new Date().toISOString()}] [platform] mcp stream tee stream-log write error server=${serverName} reason=${err.message}\n`,
|
|
122
|
-
);
|
|
112
|
+
originalWrite(`${tsPrefix()} [platform] [mcp-tee-error] server=${serverName} destination=stream-log reason=${JSON.stringify(err.message)}\n`);
|
|
123
113
|
});
|
|
124
114
|
streamLogStream = s;
|
|
125
|
-
|
|
126
|
-
//
|
|
127
|
-
|
|
128
|
-
s.write(
|
|
129
|
-
`[${new Date().toISOString()}] [platform] attaching mcp stream tee for server=${serverName} logPath=${path}\n`,
|
|
130
|
-
);
|
|
131
|
-
return s;
|
|
115
|
+
// Attach marker lands in the stream log itself so investigators can
|
|
116
|
+
// confirm per-conversation the tee was wired up.
|
|
117
|
+
s.write(`${tsPrefix()} [platform] [mcp-tee-attach] server=${serverName} streamLogPath=${streamLogPath}\n`);
|
|
132
118
|
} catch (err) {
|
|
133
119
|
const msg = err instanceof Error ? err.message : String(err);
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
originalWrite(line);
|
|
137
|
-
if (perServerStream && !perServerStream.destroyed) perServerStream.write(line);
|
|
138
|
-
return undefined;
|
|
120
|
+
skipTee(msg, "stream-log");
|
|
121
|
+
streamLogStream = undefined;
|
|
139
122
|
}
|
|
140
|
-
}
|
|
123
|
+
} else {
|
|
124
|
+
skipTee("STREAM_LOG_PATH not set", "stream-log");
|
|
125
|
+
}
|
|
141
126
|
|
|
142
|
-
//
|
|
143
|
-
//
|
|
144
|
-
|
|
127
|
+
// If neither tee target is available, leave stderr untouched — patching
|
|
128
|
+
// the writer to a no-op tee would still consume event-loop cycles on
|
|
129
|
+
// every write for zero observability gain.
|
|
130
|
+
if (!perServerStream && !streamLogStream) {
|
|
131
|
+
return;
|
|
132
|
+
}
|
|
145
133
|
|
|
146
134
|
// Line buffer — accumulates characters written to stderr so complete
|
|
147
135
|
// newline-terminated lines can be prefixed and emitted to the stream log.
|
|
@@ -156,6 +144,7 @@ export function initStderrTee(serverName: string): void {
|
|
|
156
144
|
const utf8 = new StringDecoder("utf8");
|
|
157
145
|
|
|
158
146
|
const emitCompleteLinesToStreamLog = (chunk: string): void => {
|
|
147
|
+
if (!streamLogStream) return;
|
|
159
148
|
lineBuffer += chunk;
|
|
160
149
|
let newlineIndex: number;
|
|
161
150
|
// eslint-disable-next-line no-cond-assign
|
|
@@ -163,11 +152,8 @@ export function initStderrTee(serverName: string): void {
|
|
|
163
152
|
const line = lineBuffer.slice(0, newlineIndex);
|
|
164
153
|
lineBuffer = lineBuffer.slice(newlineIndex + 1);
|
|
165
154
|
if (line.length === 0) continue; // skip blank lines
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
stream.write(
|
|
169
|
-
`[${new Date().toISOString()}] [mcp:${serverName}] ${line}\n`,
|
|
170
|
-
);
|
|
155
|
+
if (streamLogStream.destroyed || streamLogStream.writableEnded) continue;
|
|
156
|
+
streamLogStream.write(`${tsPrefix()} [mcp:${serverName}] ${line}\n`);
|
|
171
157
|
}
|
|
172
158
|
};
|
|
173
159
|
|
|
@@ -179,17 +165,17 @@ export function initStderrTee(serverName: string): void {
|
|
|
179
165
|
if (perServerStream && !perServerStream.destroyed) {
|
|
180
166
|
perServerStream.write(chunk);
|
|
181
167
|
}
|
|
182
|
-
// 2. Stream log — per-line, prefixed
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
168
|
+
// 2. Stream log — per-line, prefixed.
|
|
169
|
+
if (streamLogStream) {
|
|
170
|
+
try {
|
|
171
|
+
const text = typeof chunk === "string"
|
|
172
|
+
? chunk
|
|
173
|
+
: utf8.write(Buffer.from(chunk));
|
|
174
|
+
emitCompleteLinesToStreamLog(text);
|
|
175
|
+
} catch (err) {
|
|
176
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
177
|
+
originalWrite(`${tsPrefix()} [platform] [mcp-tee-emit-error] server=${serverName} reason=${JSON.stringify(msg)}\n`);
|
|
178
|
+
}
|
|
193
179
|
}
|
|
194
180
|
// 3. Original stderr — Claude Code still gets what it's always got.
|
|
195
181
|
return (originalWrite as (chunk: string | Uint8Array, ...a: unknown[]) => boolean)(chunk, ...args);
|
|
@@ -204,12 +190,11 @@ export function initStderrTee(serverName: string): void {
|
|
|
204
190
|
// shutdown anyway. This hook exists for the rare caller that uses
|
|
205
191
|
// process.stderr.write without a trailing newline.
|
|
206
192
|
process.on("beforeExit", () => {
|
|
207
|
-
if (
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
);
|
|
193
|
+
if (streamLogStream && !streamLogStream.destroyed && !streamLogStream.writableEnded) {
|
|
194
|
+
if (lineBuffer.length > 0) {
|
|
195
|
+
streamLogStream.write(`${tsPrefix()} [mcp:${serverName}] ${lineBuffer}\n`);
|
|
196
|
+
}
|
|
197
|
+
streamLogStream.write(`${tsPrefix()} [platform] [mcp-tee-detach] server=${serverName} reason=process-before-exit\n`);
|
|
213
198
|
}
|
|
214
199
|
lineBuffer = "";
|
|
215
200
|
});
|