@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.
Files changed (32) hide show
  1. package/package.json +1 -1
  2. package/payload/platform/config/brand.json +4 -0
  3. package/payload/platform/lib/mcp-stderr-tee/dist/index.d.ts +23 -13
  4. package/payload/platform/lib/mcp-stderr-tee/dist/index.d.ts.map +1 -1
  5. package/payload/platform/lib/mcp-stderr-tee/dist/index.js +86 -89
  6. package/payload/platform/lib/mcp-stderr-tee/dist/index.js.map +1 -1
  7. package/payload/platform/lib/mcp-stderr-tee/src/index.ts +86 -101
  8. package/payload/platform/plugins/admin/mcp/dist/index.js +33 -2
  9. package/payload/platform/plugins/admin/mcp/dist/index.js.map +1 -1
  10. package/payload/platform/plugins/admin/skills/stream-log-review/SKILL.md +22 -8
  11. package/payload/platform/plugins/cloudflare/PLUGIN.md +5 -4
  12. package/payload/platform/plugins/cloudflare/mcp/__tests__/auth-binding.test.ts +196 -0
  13. package/payload/platform/plugins/cloudflare/mcp/__tests__/brand-load.test.ts +81 -0
  14. package/payload/platform/plugins/cloudflare/mcp/__tests__/manifest-scope.test.ts +65 -0
  15. package/payload/platform/plugins/cloudflare/mcp/__tests__/verify-scenario-0.test.ts +70 -0
  16. package/payload/platform/plugins/cloudflare/mcp/__tests__/verify-scenario-B.test.ts +124 -0
  17. package/payload/platform/plugins/cloudflare/mcp/dist/index.js +221 -200
  18. package/payload/platform/plugins/cloudflare/mcp/dist/index.js.map +1 -1
  19. package/payload/platform/plugins/cloudflare/mcp/dist/lib/cloudflared.d.ts +174 -39
  20. package/payload/platform/plugins/cloudflare/mcp/dist/lib/cloudflared.d.ts.map +1 -1
  21. package/payload/platform/plugins/cloudflare/mcp/dist/lib/cloudflared.js +891 -194
  22. package/payload/platform/plugins/cloudflare/mcp/dist/lib/cloudflared.js.map +1 -1
  23. package/payload/platform/plugins/cloudflare/mcp/package.json +5 -2
  24. package/payload/platform/plugins/cloudflare/mcp/vitest.config.ts +10 -0
  25. package/payload/platform/plugins/cloudflare/references/setup-guide.md +31 -32
  26. package/payload/platform/plugins/cloudflare/skills/setup-tunnel/SKILL.md +25 -3
  27. package/payload/platform/plugins/docs/PLUGIN.md +2 -0
  28. package/payload/platform/plugins/docs/references/cloudflare.md +68 -0
  29. package/payload/platform/plugins/docs/references/plugins-guide.md +8 -6
  30. package/payload/platform/scripts/logs-read.sh +114 -54
  31. package/payload/platform/templates/specialists/agents/personal-assistant.md +12 -8
  32. package/payload/server/server.js +387 -71
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rubytech/create-realagent",
3
- "version": "1.0.615",
3
+ "version": "1.0.616",
4
4
  "description": "Install Real Agent — Built for agents. By agents.",
5
5
  "bin": {
6
6
  "create-realagent": "./dist/index.js"
@@ -43,5 +43,9 @@
43
43
  "defaultEnabled": ["sales"],
44
44
  "available": ["deep-research", "waitlist", "projects", "whatsapp", "replicate"],
45
45
  "excluded": ["telegram"]
46
+ },
47
+
48
+ "cloudflare": {
49
+ "zones": ["realagent.network"]
46
50
  }
47
51
  }
@@ -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; Task 362)
18
+ * │ ├──► mcp-{name}-stderr-{YYYY-MM-DD}.log (raw chunks, per-plugin)
19
19
  * │ │
20
- * │ └──► claude-agent-stream-{YYYY-MM-DD}.log (per-line, prefixed; Task 524)
20
+ * │ └──► $STREAM_LOG_PATH (per-line, prefixed)
21
21
  * │ "[<iso>] [mcp:{name}] <line>"
22
- * │ rotates on date boundary
23
22
  * └─────────────────────────┘
24
23
  *
25
- * The stream-log destination puts MCP diagnostic lines in the same file the
26
- * platform already writes agent events to, so a single logs-read returns a
27
- * complete session timeline. The per-server file is retained unchanged
28
- * it remains the grep-one-plugin surface.
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 both a per-server raw log and the
32
- * unified claude-agent-stream log (with a [mcp:<serverName>] prefix).
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
- * Reads LOG_DIR from environment. When absent (dev mode, tool discovery),
35
- * does nothing stderr works as before with no markers emitted.
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. Not safe to call twice —
38
- * repeated calls would stack patches and double-log every chunk.
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;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4BG;AAeH;;;;;;;;;GASG;AACH,wBAAgB,aAAa,CAAC,UAAU,EAAE,MAAM,GAAG,IAAI,CAkKtD"}
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; Task 362)
19
+ * │ ├──► mcp-{name}-stderr-{YYYY-MM-DD}.log (raw chunks, per-plugin)
20
20
  * │ │
21
- * │ └──► claude-agent-stream-{YYYY-MM-DD}.log (per-line, prefixed; Task 524)
21
+ * │ └──► $STREAM_LOG_PATH (per-line, prefixed)
22
22
  * │ "[<iso>] [mcp:{name}] <line>"
23
- * │ rotates on date boundary
24
23
  * └─────────────────────────┘
25
24
  *
26
- * The stream-log destination puts MCP diagnostic lines in the same file the
27
- * platform already writes agent events to, so a single logs-read returns a
28
- * complete session timeline. The per-server file is retained unchanged
29
- * it remains the grep-one-plugin surface.
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 both a per-server raw log and the
42
- * unified claude-agent-stream log (with a [mcp:<serverName>] prefix).
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
- * Reads LOG_DIR from environment. When absent (dev mode, tool discovery),
45
- * does nothing stderr works as before with no markers emitted.
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. Not safe to call twice —
48
- * repeated calls would stack patches and double-log every chunk.
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
- if (!logDir)
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
- // Per-server raw-chunk file (Task 362). Opened once, never rotated —
64
- // preserves existing behaviour.
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
- try {
67
- (0, node_fs_1.mkdirSync)(logDir, { recursive: true });
68
- const date = new Date().toISOString().slice(0, 10);
69
- perServerStream = (0, node_fs_1.createWriteStream)((0, node_path_1.resolve)(logDir, `mcp-${serverName}-stderr-${date}.log`), { flags: "a" });
70
- perServerStream.on("error", (err) => {
71
- originalWrite(`[${new Date().toISOString()}] [platform] mcp stream tee per-server write error server=${serverName} reason=${err.message}\n`);
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
- catch (err) {
75
- const msg = err instanceof Error ? err.message : String(err);
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
- // Stream-log destination (Task 524). Lazily re-opened on date boundary so
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
- let streamLogDate = ""; // YYYY-MM-DD the current stream was opened for
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
- const s = (0, node_fs_1.createWriteStream)(path, { flags: "a" });
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(`[${new Date().toISOString()}] [platform] mcp stream tee stream-log write error server=${serverName} reason=${err.message}\n`);
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
- streamLogDate = today;
113
- // Startup marker lands in the stream log itself so investigators reading
114
- // a session's stream-log file can confirm the tee was wired up.
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
- streamLogDisabledReason = msg;
121
- const line = `[${new Date().toISOString()}] [platform] mcp stream tee disabled reason=${msg} server=${serverName} destination=stream-log\n`;
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
- // Prime the stream log once so the startup marker appears immediately
129
- // rather than waiting for the first stderr chunk.
130
- openStreamLog();
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
- const stream = openStreamLog();
151
- if (!stream || stream.destroyed || stream.writableEnded)
146
+ if (streamLogStream.destroyed || streamLogStream.writableEnded)
152
147
  continue;
153
- stream.write(`[${new Date().toISOString()}] [mcp:${serverName}] ${line}\n`);
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, date-rotating.
162
- try {
163
- const text = typeof chunk === "string"
164
- ? chunk
165
- : utf8.write(Buffer.from(chunk));
166
- emitCompleteLinesToStreamLog(text);
167
- }
168
- catch (err) {
169
- const msg = err instanceof Error ? err.message : String(err);
170
- originalWrite(`[${new Date().toISOString()}] [platform] mcp stream tee emit error server=${serverName} reason=${msg}\n`);
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 (lineBuffer.length === 0)
185
- return;
186
- const stream = openStreamLog();
187
- if (stream && !stream.destroyed && !stream.writableEnded) {
188
- stream.write(`[${new Date().toISOString()}] [mcp:${serverName}] ${lineBuffer}\n`);
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":"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"}
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; Task 362)
18
+ * │ ├──► mcp-{name}-stderr-{YYYY-MM-DD}.log (raw chunks, per-plugin)
19
19
  * │ │
20
- * │ └──► claude-agent-stream-{YYYY-MM-DD}.log (per-line, prefixed; Task 524)
20
+ * │ └──► $STREAM_LOG_PATH (per-line, prefixed)
21
21
  * │ "[<iso>] [mcp:{name}] <line>"
22
- * │ rotates on date boundary
23
22
  * └─────────────────────────┘
24
23
  *
25
- * The stream-log destination puts MCP diagnostic lines in the same file the
26
- * platform already writes agent events to, so a single logs-read returns a
27
- * complete session timeline. The per-server file is retained unchanged
28
- * it remains the grep-one-plugin surface.
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 both a per-server raw log and the
46
- * unified claude-agent-stream log (with a [mcp:<serverName>] prefix).
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
- * Reads LOG_DIR from environment. When absent (dev mode, tool discovery),
49
- * does nothing stderr works as before with no markers emitted.
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. Not safe to call twice —
52
- * repeated calls would stack patches and double-log every chunk.
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
- if (!logDir) return; // Dev mode or tool discovery — stderr only
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
- // Per-server raw-chunk file (Task 362). Opened once, never rotated —
69
- // preserves existing behaviour.
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
- try {
72
- mkdirSync(logDir, { recursive: true });
73
- const date = new Date().toISOString().slice(0, 10);
74
- perServerStream = createWriteStream(
75
- resolve(logDir, `mcp-${serverName}-stderr-${date}.log`),
76
- { flags: "a" },
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
- } catch (err) {
84
- const msg = err instanceof Error ? err.message : String(err);
85
- originalWrite(
86
- `[${new Date().toISOString()}] [platform] mcp stream tee disabled reason=${msg} server=${serverName} destination=per-server\n`,
87
- );
88
- // If the per-server file can't be opened, the LOG_DIR itself is likely
89
- // unusable — give up on the whole tee rather than half-install it.
90
- return;
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
- // Stream-log destination (Task 524). Lazily re-opened on date boundary so
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
- let streamLogDate = ""; // YYYY-MM-DD the current stream was opened for
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
- const s = createWriteStream(path, { flags: "a" });
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
- streamLogDate = today;
126
- // Startup marker lands in the stream log itself so investigators reading
127
- // a session's stream-log file can confirm the tee was wired up.
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
- streamLogDisabledReason = msg;
135
- const line = `[${new Date().toISOString()}] [platform] mcp stream tee disabled reason=${msg} server=${serverName} destination=stream-log\n`;
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
- // Prime the stream log once so the startup marker appears immediately
143
- // rather than waiting for the first stderr chunk.
144
- openStreamLog();
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
- const stream = openStreamLog();
167
- if (!stream || stream.destroyed || stream.writableEnded) continue;
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, date-rotating.
183
- try {
184
- const text = typeof chunk === "string"
185
- ? chunk
186
- : utf8.write(Buffer.from(chunk));
187
- emitCompleteLinesToStreamLog(text);
188
- } catch (err) {
189
- const msg = err instanceof Error ? err.message : String(err);
190
- originalWrite(
191
- `[${new Date().toISOString()}] [platform] mcp stream tee emit error server=${serverName} reason=${msg}\n`,
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 (lineBuffer.length === 0) return;
208
- const stream = openStreamLog();
209
- if (stream && !stream.destroyed && !stream.writableEnded) {
210
- stream.write(
211
- `[${new Date().toISOString()}] [mcp:${serverName}] ${lineBuffer}\n`,
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
  });