@rubytech/create-maxy 1.0.610 → 1.0.612

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.
@@ -1,51 +1,216 @@
1
1
  /**
2
- * MCP server stderr tee — writes console.error output to both stderr
3
- * (for Claude Code) and a dated log file (for logs-read.sh retrieval).
2
+ * MCP server stderr tee — dual-destination log capture.
4
3
  *
5
- * Claude Code spawns MCP servers as child processes and consumes their
6
- * stderr internally. The platform cannot intercept at the spawn level.
7
- * Each server must tee its own stderr to the account's log directory.
4
+ * Claude Code spawns MCP servers as child processes and consumes their stderr
5
+ * internally. The platform cannot intercept at the spawn level. Each server
6
+ * must tee its own stderr so diagnostic output is retrievable.
7
+ *
8
+ * ┌─────────────────────────┐
9
+ * │ MCP server process │
10
+ * │ │
11
+ * │ console.error("[x] ..") │
12
+ * │ │ │
13
+ * │ ▼ │
14
+ * │ patched stderr.write() │
15
+ * │ │ │
16
+ * │ ├──► original stderr (consumed by Claude Code — opaque)
17
+ * │ │
18
+ * │ ├──► mcp-{name}-stderr-{YYYY-MM-DD}.log (raw chunks; Task 362)
19
+ * │ │
20
+ * │ └──► claude-agent-stream-{YYYY-MM-DD}.log (per-line, prefixed; Task 524)
21
+ * │ "[<iso>] [mcp:{name}] <line>"
22
+ * │ rotates on date boundary
23
+ * └─────────────────────────┘
24
+ *
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.
8
29
  */
9
30
 
10
- import { mkdirSync, createWriteStream, type WriteStream } from "node:fs";
31
+ import {
32
+ mkdirSync,
33
+ createWriteStream,
34
+ type WriteStream,
35
+ } from "node:fs";
11
36
  import { resolve } from "node:path";
37
+ import { StringDecoder } from "node:string_decoder";
38
+
39
+ // Marker on process.stderr.write so repeat calls to initStderrTee() are
40
+ // detected at runtime, not just warned about in the docstring. A second
41
+ // call would stack patches and double-log every chunk.
42
+ const INIT_MARKER = Symbol.for("maxy.mcpStderrTee.installed");
12
43
 
13
44
  /**
14
- * Patch process.stderr.write to tee output to a dated log file.
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).
15
47
  *
16
48
  * Reads LOG_DIR from environment. When absent (dev mode, tool discovery),
17
- * does nothing — stderr continues to work as before.
49
+ * does nothing — stderr works as before with no markers emitted.
18
50
  *
19
- * Log file: {LOG_DIR}/mcp-{serverName}-stderr-YYYY-MM-DD.log (append mode)
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.
20
53
  */
21
54
  export function initStderrTee(serverName: string): void {
22
55
  const logDir = process.env.LOG_DIR;
23
56
  if (!logDir) return; // Dev mode or tool discovery — stderr only
24
57
 
25
- let logStream: WriteStream;
58
+ // Refuse repeat patches — stacking them would double-log every chunk
59
+ // and corrupt the re-entrancy guard (originalWrite would capture the
60
+ // already-patched function on the second call).
61
+ if ((process.stderr.write as unknown as { [k: symbol]: boolean })[INIT_MARKER]) return;
62
+
63
+ // Keep a direct reference to the unpatched writer. All diagnostic output
64
+ // from inside this module MUST go through originalWrite to avoid
65
+ // re-entering the patched writer (which would recurse on a tee failure).
66
+ const originalWrite = process.stderr.write.bind(process.stderr);
67
+
68
+ // Per-server raw-chunk file (Task 362). Opened once, never rotated —
69
+ // preserves existing behaviour.
70
+ let perServerStream: WriteStream | undefined;
26
71
  try {
27
72
  mkdirSync(logDir, { recursive: true });
28
73
  const date = new Date().toISOString().slice(0, 10);
29
- logStream = createWriteStream(
74
+ perServerStream = createWriteStream(
30
75
  resolve(logDir, `mcp-${serverName}-stderr-${date}.log`),
31
76
  { flags: "a" },
32
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`,
81
+ );
82
+ });
33
83
  } catch (err) {
34
84
  const msg = err instanceof Error ? err.message : String(err);
35
- process.stderr.write(
36
- `[${serverName}] WARNING: log file tee failed (${msg}), stderr only\n`,
85
+ originalWrite(
86
+ `[${new Date().toISOString()}] [platform] mcp stream tee disabled reason=${msg} server=${serverName} destination=per-server\n`,
37
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.
38
90
  return;
39
91
  }
40
92
 
41
- const originalWrite = process.stderr.write.bind(process.stderr);
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.
96
+ 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`);
117
+ try {
118
+ const s = createWriteStream(path, { flags: "a" });
119
+ 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
+ );
123
+ });
124
+ 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;
132
+ } catch (err) {
133
+ 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;
139
+ }
140
+ };
141
+
142
+ // Prime the stream log once so the startup marker appears immediately
143
+ // rather than waiting for the first stderr chunk.
144
+ openStreamLog();
145
+
146
+ // Line buffer — accumulates characters written to stderr so complete
147
+ // newline-terminated lines can be prefixed and emitted to the stream log.
148
+ // Partial chunks are held until a newline arrives (or beforeExit flushes).
149
+ let lineBuffer = "";
150
+
151
+ // StringDecoder buffers trailing UTF-8 continuation bytes across chunks.
152
+ // Without it, a multi-byte codepoint that straddles a write boundary would
153
+ // render as two U+FFFD replacement characters in the stream log. Only
154
+ // matters when a caller writes Uint8Array chunks directly — console.error
155
+ // produces strings — but plugins piping child-process stderr can hit this.
156
+ const utf8 = new StringDecoder("utf8");
157
+
158
+ const emitCompleteLinesToStreamLog = (chunk: string): void => {
159
+ lineBuffer += chunk;
160
+ let newlineIndex: number;
161
+ // eslint-disable-next-line no-cond-assign
162
+ while ((newlineIndex = lineBuffer.indexOf("\n")) !== -1) {
163
+ const line = lineBuffer.slice(0, newlineIndex);
164
+ lineBuffer = lineBuffer.slice(newlineIndex + 1);
165
+ 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
+ );
171
+ }
172
+ };
173
+
42
174
  process.stderr.write = (
43
175
  chunk: string | Uint8Array,
44
176
  ...args: unknown[]
45
177
  ): boolean => {
46
- if (!logStream.destroyed) {
47
- logStream.write(chunk);
178
+ // 1. Per-server raw file — preserves Task 362 behaviour (no prefix).
179
+ if (perServerStream && !perServerStream.destroyed) {
180
+ perServerStream.write(chunk);
48
181
  }
49
- return (originalWrite as Function)(chunk, ...args);
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
+ );
193
+ }
194
+ // 3. Original stderr — Claude Code still gets what it's always got.
195
+ return (originalWrite as (chunk: string | Uint8Array, ...a: unknown[]) => boolean)(chunk, ...args);
50
196
  };
197
+ // Mark the patch so a second initStderrTee() call is refused at the top.
198
+ (process.stderr.write as unknown as { [k: symbol]: boolean })[INIT_MARKER] = true;
199
+
200
+ // Flush any trailing unterminated segment on graceful event-loop drain.
201
+ // `beforeExit` fires only when the event loop is empty — it does NOT run
202
+ // on SIGTERM, SIGKILL, or explicit process.exit(). Acceptable: console.error
203
+ // always terminates with \n, so the buffer is effectively always empty at
204
+ // shutdown anyway. This hook exists for the rare caller that uses
205
+ // process.stderr.write without a trailing newline.
206
+ 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
+ );
213
+ }
214
+ lineBuffer = "";
215
+ });
51
216
  }
@@ -52,15 +52,15 @@ Tools are available via the `admin` MCP server.
52
52
 
53
53
  | Task | When to use | Reference |
54
54
  |------|-------------|-----------|
55
- | Manage public agents | User asks to create, edit, clone, list, preview, or delete a public agent | `skills/public-agent-manager/skill.md` |
55
+ | Manage public agents | User asks to create, edit, clone, list, preview, or delete a public agent | `skills/public-agent-manager/SKILL.md` |
56
56
  | Create a new skill | User asks to create, add, or teach the agent a new capability | `skills/skill-builder/SKILL.md` |
57
57
  | Review stream logs | Admin asks to review, diagnose, or analyse a Claude agent stream log | `skills/stream-log-review/SKILL.md` |
58
58
  | Set up business profile | Admin asks to set up, complete, or update business operational data | `skills/business-profile/SKILL.md` |
59
59
  | Generate a QR code | User asks to generate, create, or make a QR code from a URL, Wi-Fi details, contact, or text | `skills/qr-code/SKILL.md` |
60
60
  | Timezone queries | User asks for the time in another city, timezone conversion, UTC offset, or DST status | `skills/datetime/SKILL.md` |
61
61
  | Manage access grants | Admin asks to invite visitors, revoke or extend access, list who has access, or set an agent's access mode | `skills/access-manager/SKILL.md` |
62
- | Manage plugins and settings | User asks to install, enable, disable, or configure plugins, or change account settings | `skills/plugin-management/skill.md` |
63
- | Manage specialists | User asks to install or remove a specialist subagent, or activate/deactivate premium plugin agents | `skills/specialist-management/skill.md` |
62
+ | Manage plugins and settings | User asks to install, enable, disable, or configure plugins, or change account settings | `skills/plugin-management/SKILL.md` |
63
+ | Manage specialists | User asks to install or remove a specialist subagent, or activate/deactivate premium plugin agents | `skills/specialist-management/SKILL.md` |
64
64
  | Generate print-quality PDF | User asks to create a PDF document, one-pager, brochure, or any HTML intended for print/download | `skills/a4-print-documents/SKILL.md` |
65
65
 
66
66
  ## Hooks
@@ -55,7 +55,7 @@ Call `premium-deliver` with `pluginName` set to the plugin name. The tool handle
55
55
  - Scans for public agent templates
56
56
  - Returns a structured result with per-sub-plugin status and available templates
57
57
 
58
- Report the tool's result to the user. If templates are found, present them and ask whether the user wants to create any now. For template creation, load the public agent management skill via `plugin-read` from `admin/skills/public-agent-manager/skill.md` and follow the "Create from Template" instructions, passing the template directory path from the tool's result.
58
+ Report the tool's result to the user. If templates are found, present them and ask whether the user wants to create any now. For template creation, load the public agent management skill via `plugin-read` from `admin/skills/public-agent-manager/SKILL.md` and follow the "Create from Template" instructions, passing the template directory path from the tool's result.
59
59
 
60
60
  If the delivered plugin has specialist agents (files in `agents/` with the `{plugin-name}--{agent-name}.md` naming convention, distinct from template directories), load the specialist management skill and follow the premium plugin agent activation instructions.
61
61
 
@@ -87,3 +87,23 @@ Maxy handles the installation or removal. If the plugin requires any setup (API
87
87
  ## Viewing Your Plugins
88
88
 
89
89
  Ask Maxy: "What plugins do I have?" or "List my plugins."
90
+
91
+ ## MCP Plugin Observability (for plugin authors)
92
+
93
+ Every `console.error` line from a plugin's MCP server can be teed into the unified agent stream log so a single `logs-read` call returns the full session timeline — agent events and plugin diagnostics interleaved in chronological order.
94
+
95
+ **Opt-in (one line at the top of the MCP server's entry file):**
96
+
97
+ ```typescript
98
+ import { initStderrTee } from "../../../../lib/mcp-stderr-tee/dist/index.js";
99
+ initStderrTee("your-plugin-name");
100
+ ```
101
+
102
+ After this, every `console.error("[your-tool] ...")` from any tool in the plugin appears as `[<iso-ts>] [mcp:your-plugin-name] [your-tool] ...` in `claude-agent-stream-YYYY-MM-DD.log`, alongside the usual agent events. The raw per-server file `mcp-your-plugin-name-stderr-YYYY-MM-DD.log` is still produced for deep-dive grep.
103
+
104
+ **Retrieve MCP diagnostic lines for a session:**
105
+
106
+ - All servers: `logs-read { type: "system" }` → grep `[mcp:<name>]` on the returned stream log.
107
+ - One server raw feed: `logs-read { type: "mcp" }` → `mcp-<name>-stderr-*.log`.
108
+
109
+ **Tee-state markers** land in the stream log too — `[platform] attaching mcp stream tee for server=<name> logPath=…` when the tee wires up, `[platform] mcp stream tee disabled reason=… server=<name>` when it bails (missing `LOG_DIR`, unwritable path, etc.). If a server invoked tools but no `[mcp:<name>]` lines appear, look for the disabled marker first.
@@ -18,16 +18,6 @@ ChatGPT helps you think and write. Siri and Alexa respond to commands. Maxy runs
18
18
 
19
19
  The comparison that matters isn't Maxy vs ChatGPT. It's Maxy vs hiring someone — or vs doing it all yourself.
20
20
 
21
- ## How does Maxy earn more responsibility?
22
-
23
- Through the Earned Autonomy model. Maxy tracks your demonstrated competence with each capability independently, progressing through three stages:
24
-
25
- - **Assist** — Maxy shows its work, explains its approach, asks before acting
26
- - **Augment** — Maxy handles things normally, flags exceptions only
27
- - **Orchestrate** — Maxy executes without explanation, escalates only when genuinely unusual
28
-
29
- You never configure this. Maxy observes and adjusts. Tell it "I know how to do this" to advance, or "explain this again" to step back. You're always in control.
30
-
31
21
  ## Can Maxy create documents?
32
22
 
33
23
  Yes. Describe what you need — a quote, a proposal, an invoice, a report, a flyer, a compliance document — and Maxy generates it as a professional A4 PDF. Review it in the conversation, download it, or deliver it directly to a customer via WhatsApp or email. Quotes, proposals, invoices, reports, marketing materials, compliance documents — all created through conversation.
@@ -12,7 +12,6 @@ Includes:
12
12
  - Manages your picture — daily briefings, overdue items, proactive reminders
13
13
  - WhatsApp, Telegram, web, and email access
14
14
  - Memory and context across conversations
15
- - Earned Autonomy — Maxy earns the right to handle more as trust builds
16
15
  - Your data stays on your device. Always.
17
16
 
18
17
  Hardware required: Maxy Pi (from £250, one-time purchase) or your own Linux machine