@rubytech/create-realagent 1.0.614 → 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 (46) hide show
  1. package/dist/index.js +42 -8
  2. package/package.json +1 -1
  3. package/payload/platform/config/brand.json +4 -0
  4. package/payload/platform/lib/mcp-stderr-tee/dist/index.d.ts +23 -13
  5. package/payload/platform/lib/mcp-stderr-tee/dist/index.d.ts.map +1 -1
  6. package/payload/platform/lib/mcp-stderr-tee/dist/index.js +86 -89
  7. package/payload/platform/lib/mcp-stderr-tee/dist/index.js.map +1 -1
  8. package/payload/platform/lib/mcp-stderr-tee/src/index.ts +86 -101
  9. package/payload/platform/plugins/admin/mcp/dist/index.js +33 -2
  10. package/payload/platform/plugins/admin/mcp/dist/index.js.map +1 -1
  11. package/payload/platform/plugins/admin/mcp/dist/lib/review-tools.d.ts.map +1 -1
  12. package/payload/platform/plugins/admin/mcp/dist/lib/review-tools.js +2 -0
  13. package/payload/platform/plugins/admin/mcp/dist/lib/review-tools.js.map +1 -1
  14. package/payload/platform/plugins/admin/skills/stream-log-review/SKILL.md +22 -8
  15. package/payload/platform/plugins/cloudflare/PLUGIN.md +5 -4
  16. package/payload/platform/plugins/cloudflare/mcp/__tests__/auth-binding.test.ts +196 -0
  17. package/payload/platform/plugins/cloudflare/mcp/__tests__/brand-load.test.ts +81 -0
  18. package/payload/platform/plugins/cloudflare/mcp/__tests__/manifest-scope.test.ts +65 -0
  19. package/payload/platform/plugins/cloudflare/mcp/__tests__/verify-scenario-0.test.ts +70 -0
  20. package/payload/platform/plugins/cloudflare/mcp/__tests__/verify-scenario-B.test.ts +124 -0
  21. package/payload/platform/plugins/cloudflare/mcp/dist/index.js +232 -183
  22. package/payload/platform/plugins/cloudflare/mcp/dist/index.js.map +1 -1
  23. package/payload/platform/plugins/cloudflare/mcp/dist/lib/cloudflared.d.ts +181 -30
  24. package/payload/platform/plugins/cloudflare/mcp/dist/lib/cloudflared.d.ts.map +1 -1
  25. package/payload/platform/plugins/cloudflare/mcp/dist/lib/cloudflared.js +938 -154
  26. package/payload/platform/plugins/cloudflare/mcp/dist/lib/cloudflared.js.map +1 -1
  27. package/payload/platform/plugins/cloudflare/mcp/package.json +5 -2
  28. package/payload/platform/plugins/cloudflare/mcp/vitest.config.ts +10 -0
  29. package/payload/platform/plugins/cloudflare/references/setup-guide.md +32 -27
  30. package/payload/platform/plugins/cloudflare/skills/setup-tunnel/SKILL.md +25 -3
  31. package/payload/platform/plugins/docs/PLUGIN.md +2 -0
  32. package/payload/platform/plugins/docs/references/cloudflare.md +68 -0
  33. package/payload/platform/plugins/docs/references/plugins-guide.md +8 -6
  34. package/payload/platform/plugins/docs/references/troubleshooting.md +2 -0
  35. package/payload/platform/plugins/email/mcp/dist/lib/providers.d.ts +9 -2
  36. package/payload/platform/plugins/email/mcp/dist/lib/providers.d.ts.map +1 -1
  37. package/payload/platform/plugins/email/mcp/dist/lib/providers.js +545 -92
  38. package/payload/platform/plugins/email/mcp/dist/lib/providers.js.map +1 -1
  39. package/payload/platform/scripts/logs-read.sh +114 -54
  40. package/payload/platform/templates/agents/admin/IDENTITY.md +6 -0
  41. package/payload/platform/templates/agents/public/IDENTITY.md +1 -0
  42. package/payload/platform/templates/specialists/agents/content-producer.md +4 -0
  43. package/payload/platform/templates/specialists/agents/personal-assistant.md +16 -8
  44. package/payload/platform/templates/specialists/agents/project-manager.md +4 -0
  45. package/payload/platform/templates/specialists/agents/research-assistant.md +4 -0
  46. package/payload/server/server.js +714 -125
@@ -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
  });
@@ -938,13 +938,44 @@ server.tool("agent-list", "List all public (non-admin) agents with their full co
938
938
  };
939
939
  }
940
940
  });
941
- server.tool("logs-read", "Read recent logs. type=system: raw Claude stream-json. type=session: SSE events sent to client. type=error: Claude subprocess stderr. type=heartbeat: platform event dispatcher (check-due-events cron). type=public: public agent diagnostic log (user messages, knowledge source, memory context, component events, responses). type=server: platform server log (session lifecycle, plugin loading, persistence, routing). type=mcp: MCP server stderr (Loop API calls, status codes, durations). type=vnc: consolidated VNC browser viewer lifecycle log (Xtigervnc/websockify/Chromium boot, HTTP viewer fetches, WebSocket upgrade decisions, proxy pipe open/close/bytes, MCP browser tool calls, client disconnect reasons, ensureVnc/ensureCdp recovery). For any 'browser' issue on the Pi this is the first-stop log. type=review: proactive log-review detector log (Task 385) every scan cycle, rule match, suppression, rate-limit decision, admin-tool rule mutation, and watchdog event. Use this to audit whether the detector observed a known-bad condition. When sessionKey is provided, greps all log files (or just the specified type) for lines containing that session key and returns a unified timeline.", {
941
+ server.tool("logs-read", "Read recent logs. Task 532: the stream logs (type=system/error/session/public) are now per-conversation — pass `conversationId` to retrieve a single conversation's log from first [spawn] to final [process-exit]. type=system: raw Claude stream-json + agent events + [tool-wait]/[tool-wait-diag]/[tool-wait-proc] telemetry + MCP server stderr via tee. type=session: SSE events sent to client. type=error: Claude subprocess stderr (raw — NODE_DEBUG HTTP/NET/UNDICI traces land in system via the stream tee, not here). type=heartbeat: platform event dispatcher (check-due-events cron). type=public: public agent diagnostic log. type=server: platform server log. type=mcp: MCP server stderr (per-plugin raw). type=vnc: VNC browser viewer lifecycle. type=review: log-review detector decisions. sessionKey: grep legacy sessionKey-tagged lines across all logs (useful for pre-Task-532 artefacts). When conversationId is provided, reads the single per-conversation file for the requested type (or dumps all type files for that conversationId if type is omitted).", {
942
942
  type: z.enum(["system", "session", "error", "heartbeat", "public", "server", "mcp", "vnc", "review"]).optional(),
943
943
  lines: z.number().optional(),
944
944
  sessionKey: z.string().optional(),
945
- }, async ({ type, lines = 50, sessionKey }) => {
945
+ conversationId: z.string().optional(),
946
+ }, async ({ type, lines = 50, sessionKey, conversationId }) => {
946
947
  try {
947
948
  const LOG_DIR = resolve(getAccountDir(), "logs");
949
+ // Task 532: conversationId mode — reads the exact per-conversation file.
950
+ // Precedes the sessionKey grep path because a conversationId is the
951
+ // tightest identity and answers single-conversation questions without
952
+ // a grep pass across every log file.
953
+ if (conversationId) {
954
+ if (!existsSync(LOG_DIR)) {
955
+ return { content: [{ type: "text", text: `Log directory does not exist: ${LOG_DIR}` }] };
956
+ }
957
+ const prefixMap = {
958
+ system: "claude-agent-stream",
959
+ error: "claude-agent-stderr",
960
+ session: "sse-events",
961
+ public: "public-agent-stream",
962
+ };
963
+ const resolvedType = type ?? "system";
964
+ const prefix = prefixMap[resolvedType];
965
+ if (!prefix) {
966
+ return {
967
+ content: [{ type: "text", text: `type=${resolvedType} is not per-conversation. Valid per-conversation types: system, error, session, public. For platform-scoped types (server, vnc, review, heartbeat, mcp) omit conversationId.` }],
968
+ isError: true,
969
+ };
970
+ }
971
+ const fileName = `${prefix}-${conversationId}.log`;
972
+ const filePath = resolve(LOG_DIR, fileName);
973
+ if (!existsSync(filePath)) {
974
+ return { content: [{ type: "text", text: `No log file found for conversationId=${conversationId} type=${resolvedType} at ${filePath}. If the conversation has not yet spawned a subprocess (first turn mid-init) the file will not exist.` }] };
975
+ }
976
+ const result = execFileSync("tail", ["-n", String(lines), filePath], { timeout: 5000 }).toString();
977
+ return { content: [{ type: "text", text: `# ${fileName}\n\n${result}` }] };
978
+ }
948
979
  if (!existsSync(LOG_DIR)) {
949
980
  return { content: [{ type: "text", text: `Log directory does not exist: ${LOG_DIR}` }] };
950
981
  }