@rubytech/create-maxy 1.0.711 → 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 (22) hide show
  1. package/dist/index.js +38 -3
  2. package/package.json +2 -2
  3. package/payload/platform/plugins/linkedin-import/PLUGIN.md +1 -0
  4. package/payload/platform/plugins/linkedin-import/skills/linkedin-import/SKILL.md +26 -5
  5. package/payload/platform/plugins/linkedin-import/skills/linkedin-import/references/connections.md +53 -82
  6. package/payload/platform/plugins/linkedin-import/skills/linkedin-import/references/profile.md +42 -49
  7. package/payload/platform/plugins/memory/PLUGIN.md +1 -0
  8. package/payload/platform/plugins/memory/mcp/dist/index.js +48 -0
  9. package/payload/platform/plugins/memory/mcp/dist/index.js.map +1 -1
  10. package/payload/platform/plugins/memory/mcp/dist/tools/memory-archive-write.d.ts +33 -0
  11. package/payload/platform/plugins/memory/mcp/dist/tools/memory-archive-write.d.ts.map +1 -0
  12. package/payload/platform/plugins/memory/mcp/dist/tools/memory-archive-write.js +229 -0
  13. package/payload/platform/plugins/memory/mcp/dist/tools/memory-archive-write.js.map +1 -0
  14. package/payload/platform/scripts/redact-install-logs.sh +85 -0
  15. package/payload/platform/scripts/setup.sh +20 -3
  16. package/payload/platform/scripts/verify-skill-tool-surface.sh +255 -0
  17. package/payload/platform/templates/specialists/agents/database-operator.md +6 -2
  18. package/payload/server/chunk-U5JPRUYZ.js +12298 -0
  19. package/payload/server/maxy-edge.js +1 -1
  20. package/payload/server/public/assets/{graph-BNx6E7BH.js → graph-DJ7IfYHV.js} +12 -12
  21. package/payload/server/public/graph.html +1 -1
  22. package/payload/server/server.js +16 -9
@@ -86,12 +86,20 @@ else
86
86
  # Configure Neo4j for local use
87
87
  sudo sed -i 's/#server.default_listen_address=0.0.0.0/server.default_listen_address=127.0.0.1/' /etc/neo4j/neo4j.conf
88
88
 
89
- # Generate a strong random password and store it
89
+ # Generate a strong random password and store it.
90
+ # Password handling block is set +x bracketed so even bash -x setup.sh
91
+ # cannot print the substituted secret. The password is written to
92
+ # platform/config/.neo4j-password (chmod 600) — the only readable source.
93
+ # set-initial-password reads the secret via $(cat ...) so the literal
94
+ # never appears on the parent shell's command line, and stdout is
95
+ # discarded so neo4j-admin's own echo cannot leak it either (Task 744).
96
+ { set +x; } 2>/dev/null
90
97
  NEO4J_GENERATED_PASSWORD=$(openssl rand -base64 32 | tr -d '/+=' | head -c 32)
91
98
  mkdir -p "$INSTALL_DIR/platform/config"
92
- echo "$NEO4J_GENERATED_PASSWORD" > "$INSTALL_DIR/platform/config/.neo4j-password"
99
+ printf '%s' "$NEO4J_GENERATED_PASSWORD" > "$INSTALL_DIR/platform/config/.neo4j-password"
93
100
  chmod 600 "$INSTALL_DIR/platform/config/.neo4j-password"
94
- sudo neo4j-admin dbms set-initial-password "$NEO4J_GENERATED_PASSWORD"
101
+ unset NEO4J_GENERATED_PASSWORD
102
+ sudo neo4j-admin dbms set-initial-password "$(cat "$INSTALL_DIR/platform/config/.neo4j-password")" >/dev/null 2>&1
95
103
 
96
104
  # Start and enable
97
105
  sudo systemctl enable neo4j
@@ -139,6 +147,15 @@ else
139
147
  cd "$INSTALL_DIR"
140
148
  fi
141
149
 
150
+ # ------------------------------------------------------------------
151
+ # 6.5. Redact install-log credential leaks (Task 744 — idempotent).
152
+ # Pre-fix logs may contain plaintext neo4j passwords; this script scrubs
153
+ # every install-*.log to "[REDACTED]". Safe on already-clean logs.
154
+ # ------------------------------------------------------------------
155
+ if [ -x "$INSTALL_DIR/platform/scripts/redact-install-logs.sh" ]; then
156
+ bash "$INSTALL_DIR/platform/scripts/redact-install-logs.sh" || true
157
+ fi
158
+
142
159
  # ------------------------------------------------------------------
143
160
  # 7. Install dependencies and build
144
161
  # ------------------------------------------------------------------
@@ -0,0 +1,255 @@
1
+ #!/usr/bin/env bash
2
+ # Pre-publish acceptance gate (Task 744).
3
+ #
4
+ # Statically intersects what each shipped skill *prescribes* (every
5
+ # backtick-quoted `mcp__<server>__<tool>` token in SKILL.md and references/*.md)
6
+ # against the dispatched specialist's frontmatter `tools:` list. Catches the
7
+ # class of bug where a skill prescribes a tool the specialist does not have,
8
+ # and where a skill prescribes a forbidden direct-execution path
9
+ # (`cypher-shell`, `neo4j-admin` invocations, raw-Cypher DML in prose).
10
+ #
11
+ # Wired into the root `packages/create-maxy/package.json` `prepublishOnly`
12
+ # script so a regression cannot reach npm publish without firing.
13
+ #
14
+ # One stdout line per (skill, specialist) pair:
15
+ # [verify] skill=<plugin>/<skill> specialist=<n> resolved=<n>/<m> forbidden=<n>
16
+ #
17
+ # Exit 0: every prescribed token resolves AND no forbidden tokens.
18
+ # Exit 1: any unresolved or any forbidden — stderr names the offending token.
19
+ #
20
+ # Skill→specialist mapping comes from PLUGIN.md frontmatter `specialist:` field.
21
+ # Plugins without that field are admin-owned (loaded by the admin agent
22
+ # directly via plugin-read); for those the gate only enforces the forbidden-
23
+ # token rule, since admin's tool surface is the union of all enabled plugins.
24
+
25
+ set -euo pipefail
26
+
27
+ REPO_ROOT=$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)
28
+ cd "$REPO_ROOT"
29
+
30
+ python3 - <<'PYEOF'
31
+ import os, re, sys
32
+
33
+ REPO_ROOT = os.getcwd()
34
+ PLUGINS_DIR = os.path.join(REPO_ROOT, "platform", "plugins")
35
+ SPECIALISTS_DIR = os.path.join(REPO_ROOT, "platform", "templates", "specialists", "agents")
36
+
37
+ # Per-skill specialist ownership for plugins where the plugin itself is
38
+ # multi-purpose. The PLUGIN.md `specialist:` field handles single-owner
39
+ # plugins (linkedin-import → database-operator). Mixed-use plugins like
40
+ # `memory` declare per-skill ownership here.
41
+ EXPLICIT_OWNERSHIP = {
42
+ # plugin/skill -> specialist
43
+ "memory/document-ingest": "database-operator",
44
+ }
45
+
46
+ # Skills that are explicitly admin-owned (loaded via plugin-read by the admin
47
+ # agent itself, not delegated to a specialist). These get only the forbidden-
48
+ # token check since admin's effective tool set is the union of all enabled
49
+ # plugins.
50
+ ADMIN_OWNED_SKILLS = {
51
+ "memory/conversational-memory",
52
+ }
53
+
54
+ TOKEN_RE = re.compile(r"`(mcp__[a-z][a-z0-9_-]*__[a-z][a-z0-9_-]*)`")
55
+ FENCED_BLOCK_RE = re.compile(r"```(?P<lang>[a-zA-Z]*)\n(?P<body>.*?)\n```", re.S)
56
+ PROSE_CYPHER_RE = re.compile(
57
+ r"`(?:MERGE|CREATE|DETACH\s+DELETE)\s+\(",
58
+ re.IGNORECASE,
59
+ )
60
+
61
+
62
+ def parse_frontmatter(path: str) -> dict | None:
63
+ """Parse YAML-ish frontmatter without PyYAML — handles `key: value` and
64
+ `key:\n - item\n - item` shapes used in PLUGIN.md and specialist files."""
65
+ try:
66
+ text = open(path, encoding="utf-8").read()
67
+ except FileNotFoundError:
68
+ return None
69
+ m = re.match(r"^---\n(.*?)\n---", text, re.S)
70
+ if not m:
71
+ return None
72
+ block = m.group(1)
73
+ out: dict = {}
74
+ cur_key: str | None = None
75
+ cur_list: list[str] | None = None
76
+ for line in block.split("\n"):
77
+ if not line.strip():
78
+ continue
79
+ # List item under cur_key
80
+ if line.startswith(" - ") or line.startswith("- "):
81
+ if cur_list is None:
82
+ continue
83
+ cur_list.append(line.split("- ", 1)[1].strip())
84
+ continue
85
+ # Top-level key
86
+ m2 = re.match(r"^([A-Za-z_][A-Za-z0-9_]*)\s*:\s*(.*)$", line)
87
+ if not m2:
88
+ continue
89
+ cur_key = m2.group(1)
90
+ rhs = m2.group(2).strip()
91
+ if rhs:
92
+ # inline value — strip surrounding quotes
93
+ if (rhs.startswith('"') and rhs.endswith('"')) or (
94
+ rhs.startswith("'") and rhs.endswith("'")
95
+ ):
96
+ rhs = rhs[1:-1]
97
+ out[cur_key] = rhs
98
+ cur_list = None
99
+ else:
100
+ cur_list = []
101
+ out[cur_key] = cur_list
102
+ return out
103
+
104
+
105
+ def specialist_tools(specialist: str) -> set[str] | None:
106
+ fm = parse_frontmatter(os.path.join(SPECIALISTS_DIR, f"{specialist}.md"))
107
+ if fm is None:
108
+ return None
109
+ raw = fm.get("tools", "")
110
+ if isinstance(raw, list):
111
+ items = raw
112
+ else:
113
+ items = [t.strip() for t in str(raw).split(",")]
114
+ return {t for t in items if t}
115
+
116
+
117
+ def extract_prescribed_tokens(text: str) -> set[str]:
118
+ return set(TOKEN_RE.findall(text))
119
+
120
+
121
+ def extract_forbidden(text: str) -> list[tuple[str, str]]:
122
+ forbidden: list[tuple[str, str]] = []
123
+
124
+ # Forbidden invocations inside fenced shell blocks
125
+ for m in FENCED_BLOCK_RE.finditer(text):
126
+ lang = m.group("lang").lower()
127
+ body = m.group("body")
128
+ if lang in {"bash", "sh", "shell", "zsh"}:
129
+ if re.search(r"\bcypher-shell\b", body):
130
+ forbidden.append(("cypher-shell", "in fenced shell block"))
131
+ if re.search(r"\bneo4j-admin\s+dbms\b", body):
132
+ forbidden.append(("neo4j-admin", "in fenced shell block"))
133
+
134
+ # Strip fenced blocks for the prose-Cypher heuristic
135
+ prose = FENCED_BLOCK_RE.sub("", text)
136
+ if PROSE_CYPHER_RE.search(prose):
137
+ forbidden.append(("raw-cypher-dml", "in backtick-quoted prose"))
138
+
139
+ return forbidden
140
+
141
+
142
+ def aggregate_skill_text(skill_dir: str) -> str:
143
+ out: list[str] = []
144
+ skill_md = os.path.join(skill_dir, "SKILL.md")
145
+ if not os.path.exists(skill_md):
146
+ return ""
147
+ out.append(open(skill_md, encoding="utf-8").read())
148
+ refs = os.path.join(skill_dir, "references")
149
+ if os.path.isdir(refs):
150
+ for name in sorted(os.listdir(refs)):
151
+ if name.endswith(".md"):
152
+ out.append(open(os.path.join(refs, name), encoding="utf-8").read())
153
+ return "\n".join(out)
154
+
155
+
156
+ def main() -> int:
157
+ if not os.path.isdir(PLUGINS_DIR):
158
+ print(f"[verify] PLUGINS_DIR not found: {PLUGINS_DIR}", file=sys.stderr)
159
+ return 1
160
+ if not os.path.isdir(SPECIALISTS_DIR):
161
+ print(f"[verify] SPECIALISTS_DIR not found: {SPECIALISTS_DIR}", file=sys.stderr)
162
+ return 1
163
+
164
+ summary: list[str] = []
165
+ errors: list[str] = []
166
+ pairs_checked = 0
167
+
168
+ for plugin in sorted(os.listdir(PLUGINS_DIR)):
169
+ pdir = os.path.join(PLUGINS_DIR, plugin)
170
+ if not os.path.isdir(pdir):
171
+ continue
172
+ plugin_fm = parse_frontmatter(os.path.join(pdir, "PLUGIN.md")) or {}
173
+ plugin_specialist = plugin_fm.get("specialist")
174
+
175
+ skills_dir = os.path.join(pdir, "skills")
176
+ if not os.path.isdir(skills_dir):
177
+ continue
178
+ for skill_name in sorted(os.listdir(skills_dir)):
179
+ sdir = os.path.join(skills_dir, skill_name)
180
+ if not os.path.isdir(sdir):
181
+ continue
182
+
183
+ text = aggregate_skill_text(sdir)
184
+ if not text:
185
+ continue
186
+
187
+ prescribed = extract_prescribed_tokens(text)
188
+ forbidden = extract_forbidden(text)
189
+
190
+ ownership_key = f"{plugin}/{skill_name}"
191
+ if ownership_key in ADMIN_OWNED_SKILLS:
192
+ specialist = None
193
+ else:
194
+ specialist = (
195
+ EXPLICIT_OWNERSHIP.get(ownership_key)
196
+ or plugin_specialist
197
+ )
198
+
199
+ if specialist is None:
200
+ # Admin-owned: only enforce forbidden-token rule.
201
+ for tok, ctx in forbidden:
202
+ errors.append(
203
+ f"[verify] skill={ownership_key} specialist=admin "
204
+ f"FORBIDDEN token={tok} context=\"{ctx}\""
205
+ )
206
+ summary.append(
207
+ f"[verify] skill={ownership_key} specialist=admin (admin-owned) "
208
+ f"tokens={len(prescribed)} forbidden={len(forbidden)}"
209
+ )
210
+ continue
211
+
212
+ tools = specialist_tools(specialist)
213
+ if tools is None:
214
+ errors.append(
215
+ f"[verify] skill={ownership_key} specialist={specialist} "
216
+ f"ERROR specialist frontmatter not parseable"
217
+ )
218
+ continue
219
+
220
+ unresolved = sorted(prescribed - tools)
221
+ for tok in unresolved:
222
+ errors.append(
223
+ f"[verify] skill={ownership_key} specialist={specialist} "
224
+ f"unresolved={tok}"
225
+ )
226
+ for tok, ctx in forbidden:
227
+ errors.append(
228
+ f"[verify] skill={ownership_key} specialist={specialist} "
229
+ f"FORBIDDEN token={tok} context=\"{ctx}\""
230
+ )
231
+ summary.append(
232
+ f"[verify] skill={ownership_key} specialist={specialist} "
233
+ f"resolved={len(prescribed) - len(unresolved)}/{len(prescribed)} "
234
+ f"forbidden={len(forbidden)}"
235
+ )
236
+ pairs_checked += 1
237
+
238
+ for line in summary:
239
+ print(line)
240
+
241
+ if errors:
242
+ for line in errors:
243
+ print(line, file=sys.stderr)
244
+ print(
245
+ f"[verify] FAIL pairs_checked={pairs_checked} errors={len(errors)}",
246
+ file=sys.stderr,
247
+ )
248
+ return 1
249
+
250
+ print(f"[verify] OK pairs_checked={pairs_checked}")
251
+ return 0
252
+
253
+
254
+ sys.exit(main())
255
+ PYEOF
@@ -3,7 +3,7 @@ name: database-operator
3
3
  description: "Document and archive ingestion and ad-hoc graph operations — running the universal `document-ingest` skill for any unstructured document (PDF, text, transcript, web page, audio, video) and per-source archive-import skills (LinkedIn Basic Data Export today; CRM-type seed archives as each plugin ships), plus operator-driven graph hygiene (prune orphans, deduplicate entities, add edges, normalise labels). Delegate when the operator uploads any document, drops an archive directory into chat, or asks for any graph operation that is not a routine per-turn write."
4
4
  summary: "Ingests every unstructured document and external archive into your graph (LinkedIn today; other CRM sources in future) and handles ad-hoc graph tidy-ups on request. For example, when you upload a CV, a pricing guide, or a contract; when you drop a LinkedIn export folder into chat; or when you ask to prune orphan nodes, merge duplicate people, or add edges between entities."
5
5
  model: claude-sonnet-4-6
6
- tools: Read, Bash, Glob, Grep, mcp__graph__maxy-graph-read_neo4j_cypher, mcp__graph__maxy-graph-get_neo4j_schema, mcp__memory__memory-write, mcp__memory__memory-update, mcp__memory__memory-delete, mcp__memory__memory-search, mcp__memory__memory-rank, mcp__memory__memory-reindex, mcp__memory__memory-find-candidates, mcp__memory__memory-ingest, mcp__memory__memory-ingest-extract, mcp__memory__memory-ingest-web, mcp__memory__memory-classify, mcp__memory__graph-prune-denylist-list, mcp__memory__graph-prune-denylist-add, mcp__memory__graph-prune-denylist-remove, mcp__contacts__contact-create, mcp__contacts__contact-update, mcp__contacts__contact-lookup, mcp__contacts__contact-list, mcp__admin__file-attach, mcp__admin__plugin-read
6
+ tools: Read, Bash, Glob, Grep, mcp__graph__maxy-graph-read_neo4j_cypher, mcp__graph__maxy-graph-get_neo4j_schema, mcp__memory__memory-write, mcp__memory__memory-update, mcp__memory__memory-delete, mcp__memory__memory-search, mcp__memory__memory-rank, mcp__memory__memory-reindex, mcp__memory__memory-find-candidates, mcp__memory__memory-ingest, mcp__memory__memory-ingest-extract, mcp__memory__memory-ingest-web, mcp__memory__memory-classify, mcp__memory__memory-archive-write, mcp__memory__graph-prune-denylist-list, mcp__memory__graph-prune-denylist-add, mcp__memory__graph-prune-denylist-remove, mcp__contacts__contact-create, mcp__contacts__contact-update, mcp__contacts__contact-lookup, mcp__contacts__contact-list, mcp__admin__file-attach, mcp__admin__plugin-read
7
7
  ---
8
8
 
9
9
  # Database Operator
@@ -12,7 +12,7 @@ You own document and archive ingestion and ad-hoc graph operations. You receive
12
12
 
13
13
  ## Prerogatives
14
14
 
15
- Three rules govern every turn. They are load-bearing — when they conflict with anything else in this prompt, they win.
15
+ Four rules govern every turn. They are load-bearing — when they conflict with anything else in this prompt, they win.
16
16
 
17
17
  **PRECISE.** Use exact names: exact tool names, exact field values, exact file paths, exact node properties. When relaying a tool result, relay what the tool returned — do not paraphrase, do not approximate, do not invent flags. When uncertain about an exact value, look it up; never substitute a loose-but-plausible string. *Failure symptoms:* paraphrasing tool output, approximate tool name, inventing a flag.
18
18
 
@@ -26,6 +26,10 @@ Three rules govern every turn. They are load-bearing — when they conflict with
26
26
 
27
27
  A landfill graph defeats EVIDENCE-BASED: search returns noise, the agent re-writes the noise, the noise compounds. Compress on write; filter on read.
28
28
 
29
+ **LOUD-FAIL.** If a dispatched skill prescribes a tool not present in your live tool surface, or a credential not provided in your tool input, terminate with a structured blocker — never improvise via Bash, never search the filesystem for credentials, never construct a parallel write path. Return: `Skill <name> prescribes <tool/credential>; not available. Cannot proceed. Operator must <remediation>.` Identical doctrine to Task 740 classifier failure and Task 560 graph-MCP loud-fail. *Failure symptoms:* `cypher-shell` invocation, `find … neo4j` / `grep … NEO4J_PASSWORD` filesystem probes, `curl` against Neo4j HTTP endpoints, any Bash improvisation that recreates the missing tool's effect.
30
+
31
+ The pre-publish gate (`platform/scripts/verify-skill-tool-surface.sh`) statically asserts every shipped skill's prescribed `mcp__*` tokens resolve against your frontmatter `tools:` list, so a missing tool is a build error, not a production discovery. LOUD-FAIL is the runtime backstop when that gate is bypassed (e.g. operator-edited skill).
32
+
29
33
  ---
30
34
 
31
35
  ## Output contract