@rubytech/create-realagent 1.0.709 → 1.0.712

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 (41) hide show
  1. package/dist/index.js +38 -3
  2. package/package.json +2 -2
  3. package/payload/platform/lib/mcp-spawn-tee/dist/index.d.ts +53 -0
  4. package/payload/platform/lib/mcp-spawn-tee/dist/index.d.ts.map +1 -0
  5. package/payload/platform/lib/mcp-spawn-tee/dist/index.js +132 -0
  6. package/payload/platform/lib/mcp-spawn-tee/dist/index.js.map +1 -0
  7. package/payload/platform/lib/mcp-spawn-tee/src/index.ts +134 -0
  8. package/payload/platform/lib/mcp-spawn-tee/tsconfig.json +8 -0
  9. package/payload/platform/package.json +2 -2
  10. package/payload/platform/plugins/docs/references/plugins-guide.md +12 -4
  11. package/payload/platform/plugins/linkedin-import/PLUGIN.md +1 -0
  12. package/payload/platform/plugins/linkedin-import/skills/linkedin-import/SKILL.md +26 -5
  13. package/payload/platform/plugins/linkedin-import/skills/linkedin-import/references/connections.md +53 -82
  14. package/payload/platform/plugins/linkedin-import/skills/linkedin-import/references/profile.md +42 -49
  15. package/payload/platform/plugins/memory/PLUGIN.md +1 -0
  16. package/payload/platform/plugins/memory/mcp/dist/index.js +48 -0
  17. package/payload/platform/plugins/memory/mcp/dist/index.js.map +1 -1
  18. package/payload/platform/plugins/memory/mcp/dist/lib/__tests__/schema-loader.test.js +34 -1
  19. package/payload/platform/plugins/memory/mcp/dist/lib/__tests__/schema-loader.test.js.map +1 -1
  20. package/payload/platform/plugins/memory/mcp/dist/lib/schema-loader.d.ts +10 -0
  21. package/payload/platform/plugins/memory/mcp/dist/lib/schema-loader.d.ts.map +1 -1
  22. package/payload/platform/plugins/memory/mcp/dist/lib/schema-loader.js +22 -3
  23. package/payload/platform/plugins/memory/mcp/dist/lib/schema-loader.js.map +1 -1
  24. package/payload/platform/plugins/memory/mcp/dist/tools/memory-archive-write.d.ts +33 -0
  25. package/payload/platform/plugins/memory/mcp/dist/tools/memory-archive-write.d.ts.map +1 -0
  26. package/payload/platform/plugins/memory/mcp/dist/tools/memory-archive-write.js +229 -0
  27. package/payload/platform/plugins/memory/mcp/dist/tools/memory-archive-write.js.map +1 -0
  28. package/payload/platform/plugins/memory/mcp/package.json +3 -1
  29. package/payload/platform/plugins/memory/mcp/scripts/boot-smoke.sh +69 -0
  30. package/payload/platform/plugins/memory/references/graph-primitives.md +22 -0
  31. package/payload/platform/plugins/memory/references/schema-base.md +1 -1
  32. package/payload/platform/scripts/redact-install-logs.sh +85 -0
  33. package/payload/platform/scripts/setup.sh +20 -3
  34. package/payload/platform/scripts/verify-skill-tool-surface.sh +255 -0
  35. package/payload/platform/templates/specialists/agents/database-operator.md +6 -2
  36. package/payload/server/chunk-A5K3CFMI.js +12297 -0
  37. package/payload/server/chunk-U5JPRUYZ.js +12298 -0
  38. package/payload/server/maxy-edge.js +1 -1
  39. package/payload/server/public/assets/{graph-BRD96pKD.js → graph-DJ7IfYHV.js} +12 -12
  40. package/payload/server/public/graph.html +1 -1
  41. package/payload/server/server.js +49 -28
package/dist/index.js CHANGED
@@ -125,7 +125,15 @@ function shell(command, args, options) {
125
125
  const cmd = options?.sudo ? "sudo" : command;
126
126
  const cmdArgs = options?.sudo ? [command, ...args] : args;
127
127
  const start = Date.now();
128
- logFile(`> ${cmd} ${cmdArgs.join(" ")}${options?.cwd ? ` [cwd: ${options.cwd}]` : ""}`);
128
+ // Redaction (Task 744): callers handling secrets pass redact: true so the
129
+ // wrapper records the command name only, not the secret-bearing args. The
130
+ // child process still receives the real args via spawnSync below; only the
131
+ // install log line is sanitised. The grep-able audit shape stays:
132
+ // > sudo neo4j-admin dbms set-initial-password [REDACTED]
133
+ const loggedArgs = options?.redact
134
+ ? `${cmdArgs.slice(0, options?.sudo ? 4 : 3).join(" ")} [REDACTED]`
135
+ : cmdArgs.join(" ");
136
+ logFile(`> ${cmd} ${loggedArgs}${options?.cwd ? ` [cwd: ${options.cwd}]` : ""}`);
129
137
  const result = spawnSync(cmd, cmdArgs, {
130
138
  stdio: "inherit",
131
139
  timeout: options?.timeout ?? 300_000,
@@ -690,7 +698,7 @@ function resetNeo4jAuth(port = DEFAULT_NEO4J_PORT, dataDir = "/var/lib/neo4j") {
690
698
  }
691
699
  else {
692
700
  console.log(" [privileged] neo4j-admin dbms");
693
- shell("neo4j-admin", ["dbms", "set-initial-password", "--", password], { sudo: true });
701
+ shell("neo4j-admin", ["dbms", "set-initial-password", "--", password], { sudo: true, redact: true });
694
702
  }
695
703
  console.log(" [privileged] systemctl start");
696
704
  shell("systemctl", ["start", serviceName], { sudo: true });
@@ -707,6 +715,29 @@ function resetNeo4jAuth(port = DEFAULT_NEO4J_PORT, dataDir = "/var/lib/neo4j") {
707
715
  }
708
716
  return password;
709
717
  }
718
+ /**
719
+ * Task 744 — scrub plaintext neo4j passwords from pre-fix install-*.log files.
720
+ * Calls platform/scripts/redact-install-logs.sh against the installer's LOG_DIR.
721
+ * The script is idempotent; re-running on clean logs is a no-op. Failures here
722
+ * are non-fatal — credential redaction is best-effort cleanup, not a blocker
723
+ * for installation.
724
+ */
725
+ function redactInstallLogs() {
726
+ const script = resolve(INSTALL_DIR, "platform/scripts/redact-install-logs.sh");
727
+ if (!existsSync(script)) {
728
+ logFile("[redact-install-logs] script not found at " + script + " — skipping");
729
+ return;
730
+ }
731
+ const r = spawnSync("bash", [script, "--dir", LOG_DIR], {
732
+ stdio: "pipe",
733
+ encoding: "utf-8",
734
+ timeout: 30_000,
735
+ });
736
+ if (r.stdout)
737
+ logFile(r.stdout.trim());
738
+ if (r.status !== 0 && r.stderr)
739
+ logFile("[redact-install-logs] WARN " + r.stderr.trim());
740
+ }
710
741
  /** Check Neo4j has a working password. Called AFTER deploy so config is in place. */
711
742
  function ensureNeo4jPassword() {
712
743
  const passwordFile = join(INSTALL_DIR, "platform/config/.neo4j-password");
@@ -794,7 +825,7 @@ function installNeo4j() {
794
825
  mkdirSync(configDir, { recursive: true });
795
826
  writeFileSync(join(configDir, ".neo4j-password"), password, { mode: 0o600 });
796
827
  console.log(" [privileged] neo4j-admin dbms");
797
- shell("neo4j-admin", ["dbms", "set-initial-password", "--", password], { sudo: true });
828
+ shell("neo4j-admin", ["dbms", "set-initial-password", "--", password], { sudo: true, redact: true });
798
829
  console.log(" [privileged] systemctl enable");
799
830
  shell("systemctl", ["enable", "neo4j"], { sudo: true });
800
831
  console.log(" [privileged] systemctl start");
@@ -2148,6 +2179,10 @@ try {
2148
2179
  installCloudflared();
2149
2180
  installWhisperCpp();
2150
2181
  deployPayload(); // Must happen before ensureNeo4jPassword — restores config backup
2182
+ // Task 744: scrub plaintext neo4j passwords from any pre-fix install-*.log.
2183
+ // Idempotent — re-running on already-redacted logs is a no-op. Runs after
2184
+ // payload deploy so the bundled redact-install-logs.sh is on disk.
2185
+ redactInstallLogs();
2151
2186
  ensureNeo4jPassword(); // Now config/.neo4j-password is available if it existed before
2152
2187
  provisionRemoteSessionSecret(); // Task 653: shared HMAC key readable by maxy-edge + maxy-ui
2153
2188
  buildPlatform();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rubytech/create-realagent",
3
- "version": "1.0.709",
3
+ "version": "1.0.712",
4
4
  "description": "Install Real Agent — Built for agents. By agents.",
5
5
  "bin": {
6
6
  "create-realagent": "./dist/index.js"
@@ -10,7 +10,7 @@
10
10
  "build": "tsc",
11
11
  "bundle": "node scripts/bundle.js",
12
12
  "test": "npm run build && node --test 'dist/__tests__/*.test.js'",
13
- "prepublishOnly": "node ../../platform/ui/scripts/check-route-wiring.mjs && node ../../platform/ui/scripts/check-edge-admin-routes.mjs && npm run build && node --test 'dist/__tests__/*.test.js' && chmod +x dist/index.js && npm run bundle && node ../../platform/ui/scripts/check-bundle-node-imports.mjs --dir=./payload/server/public/assets"
13
+ "prepublishOnly": "bash ../../platform/scripts/verify-skill-tool-surface.sh && node ../../platform/ui/scripts/check-route-wiring.mjs && node ../../platform/ui/scripts/check-edge-admin-routes.mjs && npm run build && node --test 'dist/__tests__/*.test.js' && chmod +x dist/index.js && npm run bundle && node ../../platform/ui/scripts/check-bundle-node-imports.mjs --dir=./payload/server/public/assets"
14
14
  },
15
15
  "files": [
16
16
  "dist",
@@ -0,0 +1,53 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * MCP spawn-tee — parent-side stderr capture wrapper (Task 743).
4
+ *
5
+ * Claude Code's `--mcp-config` accepts `{command, args, env}` descriptors and
6
+ * spawns each MCP server itself; the platform never holds a ChildProcess
7
+ * handle. The in-process `mcp-stderr-tee` patches `process.stderr.write` from
8
+ * inside the MCP server, but its writes go through `createWriteStream` —
9
+ * async, buffered. A synchronous module-load throw (e.g. schema-loader's
10
+ * line-168 width check on memory) calls `process.exit(1)` before the buffer
11
+ * flushes, so the per-server log file is empty and the platform's
12
+ * `[mcp-init-error] tail="(no stderr file)"` probe is operationally useless.
13
+ * That class shipped as the chronic memory-MCP silent-fail loop fixed by
14
+ * Task 743 (and hit graph in Task 560 — solved there with per-plugin
15
+ * `appendFileSync` discipline that this wrapper now generalises).
16
+ *
17
+ * The wrapper sits between Claude Code and the real MCP server: it spawns
18
+ * the real entry with `stdio: ['inherit', 'inherit', 'pipe']`, then
19
+ * synchronously appends every child stderr chunk to
20
+ * `${LOG_DIR}/mcp-${name}-stderr-<date>.log`. Synchronous writes survive
21
+ * `process.exit` because the kernel queues the syscall before the call
22
+ * returns. The chunk is also written to the wrapper's own stderr so
23
+ * Claude Code's existing stderr consumer is unchanged — the mechanism is
24
+ * additive, not interceptive.
25
+ *
26
+ * Claude Code CLI
27
+ * │ spawns
28
+ * ▼
29
+ * wrapper (this file) — argv[2] = real entry, env.MCP_SPAWN_TEE_NAME = name
30
+ * │ spawns child with stdio:['inherit','inherit','pipe']
31
+ * ▼
32
+ * child = node <real-entry>
33
+ * │
34
+ * ├──▶ stdin/stdout: inherited from wrapper (Claude Code pipe)
35
+ * └──▶ stderr → wrapper.on('data', chunk =>)
36
+ * ├──▶ appendFileSync(${LOG_DIR}/mcp-${name}-stderr-<date>.log)
37
+ * └──▶ process.stderr.write(chunk) → Claude Code consumer
38
+ *
39
+ * The wrapper catches:
40
+ * - MODULE_NOT_FOUND on the entry script itself (success criterion #3:
41
+ * `mv plugins/memory/mcp/dist plugins/memory/mcp/dist-broken && reboot`
42
+ * leaves the cause on disk).
43
+ * - Synchronous throws during module load before the in-process tee runs.
44
+ * - Any stderr a normally-running plugin emits (steady-state telemetry
45
+ * remains visible too — the in-process tee handles per-line stream-log
46
+ * prefixing; this wrapper handles the raw per-server file).
47
+ *
48
+ * SIGTERM/SIGINT propagation: forwarded to the child so a Claude Code
49
+ * shutdown does not orphan the MCP server. Child exit code is propagated
50
+ * verbatim so upstream (Claude Code) sees the real outcome.
51
+ */
52
+ export {};
53
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";AACA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAiDG"}
@@ -0,0 +1,132 @@
1
+ #!/usr/bin/env node
2
+ "use strict";
3
+ /**
4
+ * MCP spawn-tee — parent-side stderr capture wrapper (Task 743).
5
+ *
6
+ * Claude Code's `--mcp-config` accepts `{command, args, env}` descriptors and
7
+ * spawns each MCP server itself; the platform never holds a ChildProcess
8
+ * handle. The in-process `mcp-stderr-tee` patches `process.stderr.write` from
9
+ * inside the MCP server, but its writes go through `createWriteStream` —
10
+ * async, buffered. A synchronous module-load throw (e.g. schema-loader's
11
+ * line-168 width check on memory) calls `process.exit(1)` before the buffer
12
+ * flushes, so the per-server log file is empty and the platform's
13
+ * `[mcp-init-error] tail="(no stderr file)"` probe is operationally useless.
14
+ * That class shipped as the chronic memory-MCP silent-fail loop fixed by
15
+ * Task 743 (and hit graph in Task 560 — solved there with per-plugin
16
+ * `appendFileSync` discipline that this wrapper now generalises).
17
+ *
18
+ * The wrapper sits between Claude Code and the real MCP server: it spawns
19
+ * the real entry with `stdio: ['inherit', 'inherit', 'pipe']`, then
20
+ * synchronously appends every child stderr chunk to
21
+ * `${LOG_DIR}/mcp-${name}-stderr-<date>.log`. Synchronous writes survive
22
+ * `process.exit` because the kernel queues the syscall before the call
23
+ * returns. The chunk is also written to the wrapper's own stderr so
24
+ * Claude Code's existing stderr consumer is unchanged — the mechanism is
25
+ * additive, not interceptive.
26
+ *
27
+ * Claude Code CLI
28
+ * │ spawns
29
+ * ▼
30
+ * wrapper (this file) — argv[2] = real entry, env.MCP_SPAWN_TEE_NAME = name
31
+ * │ spawns child with stdio:['inherit','inherit','pipe']
32
+ * ▼
33
+ * child = node <real-entry>
34
+ * │
35
+ * ├──▶ stdin/stdout: inherited from wrapper (Claude Code pipe)
36
+ * └──▶ stderr → wrapper.on('data', chunk =>)
37
+ * ├──▶ appendFileSync(${LOG_DIR}/mcp-${name}-stderr-<date>.log)
38
+ * └──▶ process.stderr.write(chunk) → Claude Code consumer
39
+ *
40
+ * The wrapper catches:
41
+ * - MODULE_NOT_FOUND on the entry script itself (success criterion #3:
42
+ * `mv plugins/memory/mcp/dist plugins/memory/mcp/dist-broken && reboot`
43
+ * leaves the cause on disk).
44
+ * - Synchronous throws during module load before the in-process tee runs.
45
+ * - Any stderr a normally-running plugin emits (steady-state telemetry
46
+ * remains visible too — the in-process tee handles per-line stream-log
47
+ * prefixing; this wrapper handles the raw per-server file).
48
+ *
49
+ * SIGTERM/SIGINT propagation: forwarded to the child so a Claude Code
50
+ * shutdown does not orphan the MCP server. Child exit code is propagated
51
+ * verbatim so upstream (Claude Code) sees the real outcome.
52
+ */
53
+ Object.defineProperty(exports, "__esModule", { value: true });
54
+ const node_child_process_1 = require("node:child_process");
55
+ const node_fs_1 = require("node:fs");
56
+ const node_path_1 = require("node:path");
57
+ const SERVER_NAME = process.env.MCP_SPAWN_TEE_NAME ?? "unknown";
58
+ const LOG_DIR = process.env.LOG_DIR;
59
+ const ENTRY = process.argv[2];
60
+ // Sync-emit: appendFileSync to per-server log + stderr passthrough. Used for
61
+ // wrapper-internal diagnostics (attach line, errors) and for forwarding child
62
+ // stderr chunks. Each destination wrapped independently — an unwritable log
63
+ // must not mask the primary output.
64
+ function syncEmitLine(line) {
65
+ if (LOG_DIR) {
66
+ try {
67
+ (0, node_fs_1.mkdirSync)(LOG_DIR, { recursive: true });
68
+ const date = new Date().toISOString().slice(0, 10);
69
+ (0, node_fs_1.appendFileSync)((0, node_path_1.resolve)(LOG_DIR, `mcp-${SERVER_NAME}-stderr-${date}.log`), line);
70
+ }
71
+ catch {
72
+ /* unwritable destination — preserve primary failure */
73
+ }
74
+ }
75
+ try {
76
+ process.stderr.write(line);
77
+ }
78
+ catch {
79
+ /* stderr closed — nothing else to do */
80
+ }
81
+ }
82
+ if (!ENTRY) {
83
+ syncEmitLine(`[mcp-spawn-tee-error] server=${SERVER_NAME} reason="no entry given (argv[2] missing)"\n`);
84
+ process.exit(2);
85
+ }
86
+ const child = (0, node_child_process_1.spawn)(process.execPath, [ENTRY], {
87
+ stdio: ["inherit", "inherit", "pipe"],
88
+ env: process.env,
89
+ });
90
+ syncEmitLine(`[mcp-spawn-tee-attached] server=${SERVER_NAME} pid=${child.pid ?? -1} entry=${ENTRY}\n`);
91
+ child.on("error", (err) => {
92
+ const msg = err instanceof Error ? err.message : String(err);
93
+ syncEmitLine(`[mcp-spawn-tee-error] server=${SERVER_NAME} reason=${JSON.stringify(`spawn error: ${msg}`)}\n`);
94
+ process.exit(127);
95
+ });
96
+ if (child.stderr) {
97
+ child.stderr.on("data", (chunk) => {
98
+ if (LOG_DIR) {
99
+ try {
100
+ const date = new Date().toISOString().slice(0, 10);
101
+ (0, node_fs_1.appendFileSync)((0, node_path_1.resolve)(LOG_DIR, `mcp-${SERVER_NAME}-stderr-${date}.log`), chunk);
102
+ }
103
+ catch {
104
+ /* unwritable destination — preserve passthrough */
105
+ }
106
+ }
107
+ try {
108
+ process.stderr.write(chunk);
109
+ }
110
+ catch {
111
+ /* stderr closed — nothing else to do */
112
+ }
113
+ });
114
+ }
115
+ // Forward SIGTERM/SIGINT so Claude Code shutdown doesn't orphan the child.
116
+ const forward = (signal) => {
117
+ if (!child.killed)
118
+ child.kill(signal);
119
+ };
120
+ process.on("SIGTERM", () => forward("SIGTERM"));
121
+ process.on("SIGINT", () => forward("SIGINT"));
122
+ child.on("exit", (code, signal) => {
123
+ if (signal) {
124
+ syncEmitLine(`[mcp-spawn-tee-exit] server=${SERVER_NAME} pid=${child.pid ?? -1} signal=${signal}\n`);
125
+ process.exit(128 + (code ?? 0));
126
+ }
127
+ if (code !== 0) {
128
+ syncEmitLine(`[mcp-spawn-tee-exit] server=${SERVER_NAME} pid=${child.pid ?? -1} code=${code}\n`);
129
+ }
130
+ process.exit(code ?? 0);
131
+ });
132
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,134 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * MCP spawn-tee — parent-side stderr capture wrapper (Task 743).
4
+ *
5
+ * Claude Code's `--mcp-config` accepts `{command, args, env}` descriptors and
6
+ * spawns each MCP server itself; the platform never holds a ChildProcess
7
+ * handle. The in-process `mcp-stderr-tee` patches `process.stderr.write` from
8
+ * inside the MCP server, but its writes go through `createWriteStream` —
9
+ * async, buffered. A synchronous module-load throw (e.g. schema-loader's
10
+ * line-168 width check on memory) calls `process.exit(1)` before the buffer
11
+ * flushes, so the per-server log file is empty and the platform's
12
+ * `[mcp-init-error] tail="(no stderr file)"` probe is operationally useless.
13
+ * That class shipped as the chronic memory-MCP silent-fail loop fixed by
14
+ * Task 743 (and hit graph in Task 560 — solved there with per-plugin
15
+ * `appendFileSync` discipline that this wrapper now generalises).
16
+ *
17
+ * The wrapper sits between Claude Code and the real MCP server: it spawns
18
+ * the real entry with `stdio: ['inherit', 'inherit', 'pipe']`, then
19
+ * synchronously appends every child stderr chunk to
20
+ * `${LOG_DIR}/mcp-${name}-stderr-<date>.log`. Synchronous writes survive
21
+ * `process.exit` because the kernel queues the syscall before the call
22
+ * returns. The chunk is also written to the wrapper's own stderr so
23
+ * Claude Code's existing stderr consumer is unchanged — the mechanism is
24
+ * additive, not interceptive.
25
+ *
26
+ * Claude Code CLI
27
+ * │ spawns
28
+ * ▼
29
+ * wrapper (this file) — argv[2] = real entry, env.MCP_SPAWN_TEE_NAME = name
30
+ * │ spawns child with stdio:['inherit','inherit','pipe']
31
+ * ▼
32
+ * child = node <real-entry>
33
+ * │
34
+ * ├──▶ stdin/stdout: inherited from wrapper (Claude Code pipe)
35
+ * └──▶ stderr → wrapper.on('data', chunk =>)
36
+ * ├──▶ appendFileSync(${LOG_DIR}/mcp-${name}-stderr-<date>.log)
37
+ * └──▶ process.stderr.write(chunk) → Claude Code consumer
38
+ *
39
+ * The wrapper catches:
40
+ * - MODULE_NOT_FOUND on the entry script itself (success criterion #3:
41
+ * `mv plugins/memory/mcp/dist plugins/memory/mcp/dist-broken && reboot`
42
+ * leaves the cause on disk).
43
+ * - Synchronous throws during module load before the in-process tee runs.
44
+ * - Any stderr a normally-running plugin emits (steady-state telemetry
45
+ * remains visible too — the in-process tee handles per-line stream-log
46
+ * prefixing; this wrapper handles the raw per-server file).
47
+ *
48
+ * SIGTERM/SIGINT propagation: forwarded to the child so a Claude Code
49
+ * shutdown does not orphan the MCP server. Child exit code is propagated
50
+ * verbatim so upstream (Claude Code) sees the real outcome.
51
+ */
52
+
53
+ import { spawn } from "node:child_process";
54
+ import { appendFileSync, mkdirSync } from "node:fs";
55
+ import { resolve } from "node:path";
56
+
57
+ const SERVER_NAME = process.env.MCP_SPAWN_TEE_NAME ?? "unknown";
58
+ const LOG_DIR = process.env.LOG_DIR;
59
+ const ENTRY = process.argv[2];
60
+
61
+ // Sync-emit: appendFileSync to per-server log + stderr passthrough. Used for
62
+ // wrapper-internal diagnostics (attach line, errors) and for forwarding child
63
+ // stderr chunks. Each destination wrapped independently — an unwritable log
64
+ // must not mask the primary output.
65
+ function syncEmitLine(line: string): void {
66
+ if (LOG_DIR) {
67
+ try {
68
+ mkdirSync(LOG_DIR, { recursive: true });
69
+ const date = new Date().toISOString().slice(0, 10);
70
+ appendFileSync(resolve(LOG_DIR, `mcp-${SERVER_NAME}-stderr-${date}.log`), line);
71
+ } catch {
72
+ /* unwritable destination — preserve primary failure */
73
+ }
74
+ }
75
+ try {
76
+ process.stderr.write(line);
77
+ } catch {
78
+ /* stderr closed — nothing else to do */
79
+ }
80
+ }
81
+
82
+ if (!ENTRY) {
83
+ syncEmitLine(`[mcp-spawn-tee-error] server=${SERVER_NAME} reason="no entry given (argv[2] missing)"\n`);
84
+ process.exit(2);
85
+ }
86
+
87
+ const child = spawn(process.execPath, [ENTRY], {
88
+ stdio: ["inherit", "inherit", "pipe"],
89
+ env: process.env,
90
+ });
91
+
92
+ syncEmitLine(`[mcp-spawn-tee-attached] server=${SERVER_NAME} pid=${child.pid ?? -1} entry=${ENTRY}\n`);
93
+
94
+ child.on("error", (err) => {
95
+ const msg = err instanceof Error ? err.message : String(err);
96
+ syncEmitLine(`[mcp-spawn-tee-error] server=${SERVER_NAME} reason=${JSON.stringify(`spawn error: ${msg}`)}\n`);
97
+ process.exit(127);
98
+ });
99
+
100
+ if (child.stderr) {
101
+ child.stderr.on("data", (chunk: Buffer) => {
102
+ if (LOG_DIR) {
103
+ try {
104
+ const date = new Date().toISOString().slice(0, 10);
105
+ appendFileSync(resolve(LOG_DIR, `mcp-${SERVER_NAME}-stderr-${date}.log`), chunk);
106
+ } catch {
107
+ /* unwritable destination — preserve passthrough */
108
+ }
109
+ }
110
+ try {
111
+ process.stderr.write(chunk);
112
+ } catch {
113
+ /* stderr closed — nothing else to do */
114
+ }
115
+ });
116
+ }
117
+
118
+ // Forward SIGTERM/SIGINT so Claude Code shutdown doesn't orphan the child.
119
+ const forward = (signal: NodeJS.Signals) => {
120
+ if (!child.killed) child.kill(signal);
121
+ };
122
+ process.on("SIGTERM", () => forward("SIGTERM"));
123
+ process.on("SIGINT", () => forward("SIGINT"));
124
+
125
+ child.on("exit", (code, signal) => {
126
+ if (signal) {
127
+ syncEmitLine(`[mcp-spawn-tee-exit] server=${SERVER_NAME} pid=${child.pid ?? -1} signal=${signal}\n`);
128
+ process.exit(128 + (code ?? 0));
129
+ }
130
+ if (code !== 0) {
131
+ syncEmitLine(`[mcp-spawn-tee-exit] server=${SERVER_NAME} pid=${child.pid ?? -1} code=${code}\n`);
132
+ }
133
+ process.exit(code ?? 0);
134
+ });
@@ -0,0 +1,8 @@
1
+ {
2
+ "extends": "../../tsconfig.base.json",
3
+ "compilerOptions": {
4
+ "outDir": "dist",
5
+ "rootDir": "src"
6
+ },
7
+ "include": ["src"]
8
+ }
@@ -6,8 +6,8 @@
6
6
  "plugins/*/mcp"
7
7
  ],
8
8
  "scripts": {
9
- "build": "tsc -p lib/models/tsconfig.json && tsc -p lib/anthropic-key/tsconfig.json && tsc -p lib/oauth-llm/tsconfig.json && tsc -p lib/mcp-stderr-tee/tsconfig.json && tsc -p lib/graph-mcp/tsconfig.json && tsc -p lib/graph-trash/tsconfig.json && tsc -p lib/graph-search/tsconfig.json && tsc -p lib/graph-write/tsconfig.json && tsc -p lib/screening-patterns/tsconfig.json && tsc -p lib/device-url/tsconfig.json && NODE_OPTIONS='--max-old-space-size=8192' tsc -b plugins/*/mcp/tsconfig.json",
10
- "build:lib": "tsc -p lib/models/tsconfig.json && tsc -p lib/anthropic-key/tsconfig.json && tsc -p lib/oauth-llm/tsconfig.json && tsc -p lib/mcp-stderr-tee/tsconfig.json && tsc -p lib/graph-mcp/tsconfig.json && tsc -p lib/graph-trash/tsconfig.json && tsc -p lib/graph-search/tsconfig.json && tsc -p lib/graph-write/tsconfig.json && tsc -p lib/screening-patterns/tsconfig.json && tsc -p lib/device-url/tsconfig.json",
9
+ "build": "tsc -p lib/models/tsconfig.json && tsc -p lib/anthropic-key/tsconfig.json && tsc -p lib/oauth-llm/tsconfig.json && tsc -p lib/mcp-stderr-tee/tsconfig.json && tsc -p lib/mcp-spawn-tee/tsconfig.json && tsc -p lib/graph-mcp/tsconfig.json && tsc -p lib/graph-trash/tsconfig.json && tsc -p lib/graph-search/tsconfig.json && tsc -p lib/graph-write/tsconfig.json && tsc -p lib/screening-patterns/tsconfig.json && tsc -p lib/device-url/tsconfig.json && NODE_OPTIONS='--max-old-space-size=8192' tsc -b plugins/*/mcp/tsconfig.json",
10
+ "build:lib": "tsc -p lib/models/tsconfig.json && tsc -p lib/anthropic-key/tsconfig.json && tsc -p lib/oauth-llm/tsconfig.json && tsc -p lib/mcp-stderr-tee/tsconfig.json && tsc -p lib/mcp-spawn-tee/tsconfig.json && tsc -p lib/graph-mcp/tsconfig.json && tsc -p lib/graph-trash/tsconfig.json && tsc -p lib/graph-search/tsconfig.json && tsc -p lib/graph-write/tsconfig.json && tsc -p lib/screening-patterns/tsconfig.json && tsc -p lib/device-url/tsconfig.json",
11
11
  "build:memory": "tsc -p plugins/memory/mcp/tsconfig.json",
12
12
  "build:contacts": "tsc -p plugins/contacts/mcp/tsconfig.json",
13
13
  "build:telegram": "tsc -p plugins/telegram/mcp/tsconfig.json",
@@ -115,10 +115,18 @@ After this, every `console.error("[your-tool] ...")` from any tool in the plugin
115
115
 
116
116
  **Main-subprocess stderr (Task 535).** The same teeing pattern applies to the main Claude Code subprocess's stderr — every line lands in the per-conversation stream log as `[subproc-stderr] …`, with lifecycle markers `[subproc-stderr-tee-attached] pid=…` and `[subproc-stderr-tee-detached] pid=… bytes=N lines=N`. A `bytes=0 lines=0` detach means the tee was attached but the subprocess emitted nothing on stderr — which is the normal state today, because the Claude Code CLI is a bundled Bun runtime binary that does not honour Node's `NODE_DEBUG` env var. The platform records this explicitly with one line per spawn: `[subproc-debug-unavailable] reason=bundled-bun-binary-ignores-node-debug pid=… cli=claude`. A reader who finds a `[spawn]` without these markers should treat that as a regression of the tee infrastructure, not as silence.
117
117
 
118
- ## Failure-path observability contract (Task 560)
118
+ ## Failure-path observability contract (Task 560 + Task 743)
119
119
 
120
- The `initStderrTee` wrapper writes to the per-conversation stream log and per-server raw file via `createWriteStream` — async, buffered. Any diagnostic `console.error(…)` followed by an immediate `process.exit(…)` is lost: the event loop never drains the WriteStream before the process terminates. Plugins that call `process.exit()` during module load (rare `graph-mcp` is the only in-tree example today; it spawns a child at boot to proxy upstream stdio) MUST use `fs.appendFileSync` at every exit path to guarantee the cause lands in both log destinations before exit. Lines should follow the `[mcp:<name>] [<plugin-prefix>] <cause>` format so existing `grep '[mcp:<name>]'` investigator paths work. Each destination must be wrapped in its own try/catch an unwritable log must not mask the primary failure.
120
+ The `initStderrTee` wrapper writes to the per-conversation stream log and per-server raw file via `createWriteStream` — async, buffered. Any diagnostic `console.error(…)` followed by an immediate `process.exit(…)` is lost: the event loop never drains the WriteStream before the process terminates. Same race for any synchronous module-load throw: Node's uncaught-exception handler writes the stack to raw fd 2 and exits before the patched async stream flushes. The platform's `[mcp-init-error] tail="(no stderr file)"` line operationally uselessis the public symptom of this race.
121
121
 
122
- A second observability layer closes the same gap from the platform side: when `claude-agent.ts` observes an `init` event with any MCP server reporting `status:"failed"`, it reads the last 512 bytes of `${LOG_DIR}/mcp-<name>-stderr-<date>.log` and emits `[mcp-init-error] server=<name> tail=<quoted>` into the stream log. Absent file → `tail="(no stderr file)"`; empty file → `tail="(empty)"`. This works for every plugin regardless of whether it adopted the sync-write discipline — the tail of whatever landed in the raw stderr file (from whichever destination made it out of the async buffer) is always captured.
122
+ **Two layers now close the gap, each load-bearing on its own:**
123
123
 
124
- Signal inventory after a failed session: `[init] FAILED MCP servers: <names>` (names), `[mcp-init-error] server=<name> tail=…` (cause for each, from platform), optionally `[mcp:<name>] [<plugin>] …` (cause for each, from plugin's own sync-writes when the plugin is disciplined). Their union gives the investigator two independent sources for the same failure.
124
+ 1. **Plugin-side sync-write discipline.** Plugins that call `process.exit()` during module load (rare `graph-mcp` is the in-tree example; it spawns a child at boot to proxy upstream stdio) use `fs.appendFileSync` at every named exit path to guarantee the cause lands in both log destinations before exit. Lines follow the `[mcp:<name>] [<plugin-prefix>] <cause>` format so existing `grep '[mcp:<name>]'` investigator paths work. Each destination is wrapped in its own try/catch an unwritable log must not mask the primary failure. This is the discipline propagated from Task 560 to any plugin author who knows their failure paths.
125
+
126
+ 2. **Parent-side `mcp-spawn-tee` wrapper (Task 743).** Every node-based core MCP server is spawned via the `lib/mcp-spawn-tee` wrapper rather than `node <entry>` directly. The wrapper spawns the real entry with `stdio: ['inherit', 'inherit', 'pipe']` and writes child stderr chunks to `${LOG_DIR}/mcp-${name}-stderr-<date>.log` via `appendFileSync` while passing the same chunks through to its own stderr (Claude Code's consumer is unchanged). Synchronous `appendFileSync` survives `process.exit`, so the per-server file captures even (a) module-load throws before `initStderrTee` runs, (b) `MODULE_NOT_FOUND` on the entry script itself, and (c) anything else a plugin author missed. The wrapper writes `[mcp-spawn-tee-attached] server=<name> pid=<n>` on attach and forwards SIGTERM/SIGINT to the child. This is the layer that makes capture independent of plugin discipline. Playwright stays unwrapped because it spawns via `npx`, not `node`.
127
+
128
+ A third layer closes the same gap from the platform side: when `claude-agent.ts` observes an `init` event with any MCP server reporting `status:"failed"`, it reads the last 512 bytes of `${LOG_DIR}/mcp-<name>-stderr-<date>.log` and emits `[mcp-init-error] server=<name> tail=<quoted>` into the stream log. Absent file → `tail="(no stderr file)"`; empty file → `tail="(empty)"`. With the spawn-tee wrapper now interposing on every core MCP, `tail="(no stderr file)"` post-Task-743 means the wrapper itself is broken — file follow-up.
129
+
130
+ **Signal inventory after a failed session:** `[init] FAILED MCP servers: <names>` (names), `[mcp-init-error] server=<name> tail=…` (cause for each, from the platform's tail probe), `[mcp-spawn-tee-attached] server=<name> pid=<n>` (proof the wrapper attached), `[mcp-spawn-tee-exit] server=<name> code=<n>|signal=<s>` (proof the wrapper saw the exit), and optionally `[mcp:<name>] [<plugin>] …` from plugin-side sync-writes. Their union gives the investigator three independent sources for the same failure.
131
+
132
+ **Boot-smoke as publish-time gate (Task 743).** The memory MCP carries `scripts/boot-smoke.sh` that spawns `dist/index.js` with stub env, sleeps 2s, asserts `kill -0 <pid>`, and reports `[boot-smoke] memory ok|FAILED tail=<n-lines>`. Wired to `prepublish` in `plugins/memory/mcp/package.json`. The pattern is propagatable to other plugin MCPs — it's deliberately not generalised yet because each plugin's stub-env requirements differ (memory needs ACCOUNT_ID + PLATFORM_ROOT + NEO4J_URI + SESSION_ID; others differ).
@@ -4,6 +4,7 @@ description: "Import a LinkedIn Basic Data Export into the Maxy Neo4j graph. Ski
4
4
  tools: []
5
5
  always: false
6
6
  embed: false
7
+ specialist: database-operator
7
8
  metadata: {"platform":{"optional":true,"pluginKey":"linkedin-import"}}
8
9
  ---
9
10
 
@@ -42,7 +42,7 @@ When the owner is an external Person (non-operator archive), the anchor is the c
42
42
 
43
43
  ## Invariants
44
44
 
45
- 1. **Schema first.** The LinkedIn additions (`person_linkedin_url` index, `:Credential` constraint) live in [`platform/neo4j/schema.cypher`](../../../../neo4j/schema.cypher) and are applied by `platform/scripts/seed-neo4j.sh` on every install / upgrade. If running against a Neo4j that hasn't been reseeded since shipping, pipe `schema.cypher` into `cypher-shell` once before startingevery statement is `IF NOT EXISTS`.
45
+ 1. **Schema first.** The LinkedIn additions (`person_linkedin_url` index, `:Credential` constraint) live in [`platform/neo4j/schema.cypher`](../../../../neo4j/schema.cypher) and are applied by `platform/scripts/seed-neo4j.sh` on every install / upgrade. The skill assumes the schema has been seeded; it does not bootstrap schema itself. If a constraint or index is missing, the operator re-runs `seed-neo4j.sh` from the installerschema-bootstrap is installer-side, never agent-side.
46
46
  2. **Owner confirmed first.** No reference runs until `$ownerUserId` (or `$ownerPersonId`) is persisted and echo-confirmed. The reference set is parameterised — no hard-coded owner.
47
47
  3. **Natural edges only.** Every edge written is one the CSV actually expresses. `Connections.csv` encodes "I am connected on LinkedIn to this person" — that becomes `CONNECTED_ON_LINKEDIN`. No synthetic attach-to-owner pattern bolted onto rows that don't describe a relationship to the owner.
48
48
  4. **Reuse Maxy labels.** Schema-extension is last resort. The LinkedIn set maps onto existing labels wherever semantics align:
@@ -60,10 +60,31 @@ When the owner is an external Person (non-operator archive), the anchor is the c
60
60
 
61
61
  ## Execution model
62
62
 
63
- 1. Confirm `schema.cypher` is applied (one-liner: `cypher-shell ... < platform/neo4j/schema.cypher`; safe to re-run).
64
- 2. Run the owner-confirmation flow, persist `$ownerUserId` / `$ownerPersonId`.
65
- 3. For each file the operator approves, load its reference, parse the CSV, batch rows (default 500 per tx), execute the reference's Cypher with `$rows` + owner parameter.
66
- 4. After each file emit `[linkedin-import] file=<name> rows=<n> created=<n> matched=<n> ms=<elapsed>`.
63
+ 1. Run the owner-confirmation flow, persist `$ownerUserId` / `$ownerPersonId`. The owner identity resolves to a single `ownerNodeId` (elementId of the AdminUser or external Person) used in every write call.
64
+ 2. For each file the operator approves, load its reference, parse the CSV into typed `rows[]` matching the reference's row schema.
65
+ 3. **Selective-ingest gate.** Before invoking any write tool, check the parsed row count against the reference's `selectiveIngestThreshold`. If the count exceeds the threshold, pause and ask the operator to filter the import along the natural axes named in the reference (for `Connections.csv`: Company, Position, Connected On). Apply the filter to `rows[]` before continuing. Compress on write, never after — a 5,000-row blanket import is a landfill, a 200-row filtered import is signal. See [§Selective-ingest](#selective-ingest-threshold-bulk-archives).
66
+ 4. Invoke the deterministic write tool the reference names. For all archive references this is `mcp__memory__memory-archive-write` with `{archiveType, ownerNodeId, rows}` the Cypher body is fixed server-side per `archiveType`, so the agent supplies parsed rows, never Cypher. The tool batches rows at 500 per transaction internally.
67
+ 5. After each file emit `[linkedin-import] file=<name> rows=<n> created=<n> matched=<n> ms=<elapsed>` using the counters returned by the write tool.
68
+
69
+ **Doctrine:** raw Cypher and `cypher-shell` invocations are forbidden in this skill and its references. Writes route through `mcp__memory__memory-archive-write` (bulk archives) or `mcp__memory__memory-write` / `mcp__memory__memory-update` (single-node enrichments like `profile.md`). If a CSV needs a write shape no current MCP tool supports, file a task to extend `memory-archive-write` with a new `archiveType` handler — never improvise via Bash. See [database-operator's LOUD-FAIL prerogative](../../../../templates/specialists/agents/database-operator.md#prerogatives).
70
+
71
+ ## Selective-ingest threshold (bulk archives)
72
+
73
+ A LinkedIn export typically contains 3,000–10,000 connections. Writing all of them in one shot defeats compression-on-write — most rows will never be queried, and the noise compounds with every subsequent ingest. The skill compresses by interrogating the operator before bulk writes.
74
+
75
+ **Threshold:** when a parsed reference's `rows[]` exceeds **100 rows**, pause and ask the operator to filter along the reference's natural axes before invoking the write tool.
76
+
77
+ For `Connections.csv` the natural filter axes are:
78
+
79
+ - **Company** — "only people at LargeCorp", "only Female Founders Fund alumni"
80
+ - **Position** — "only Partners", "only Engineering Managers"
81
+ - **Connected On** (date range) — "only my last two years", "since 2024-01-01"
82
+
83
+ The operator picks one axis or a combination. The agent applies the filter to `rows[]` and writes only the filtered subset.
84
+
85
+ **Re-importing is idempotent.** Coming back later with a wider filter (`"add anyone at LargeCorp"`, `"include 2022 too"`) hits the same `linkedinUrl` natural key — existing `:Person` nodes are matched and updated; only the new-only delta is created. The operator can grow the slice over time without dedup work.
86
+
87
+ **Why the threshold lives in the skill, not the server.** Different archive types have different "interesting" thresholds — 100 LinkedIn connections is a lot; 100 LinkedIn skills is small. The MCP tool accepts whatever rows are passed; the conversational gate is the skill's responsibility.
67
88
 
68
89
  ## File roster
69
90