@rubytech/create-maxy-code 0.1.164 → 0.1.166

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 (47) hide show
  1. package/dist/index.js +17 -9
  2. package/package.json +1 -1
  3. package/payload/platform/neo4j/schema.cypher +3 -5
  4. package/payload/platform/plugins/email/PLUGIN.md +2 -2
  5. package/payload/platform/plugins/email/mcp/dist/lib/credentials.d.ts +0 -29
  6. package/payload/platform/plugins/email/mcp/dist/lib/credentials.d.ts.map +1 -1
  7. package/payload/platform/plugins/email/mcp/dist/lib/credentials.js +7 -115
  8. package/payload/platform/plugins/email/mcp/dist/lib/credentials.js.map +1 -1
  9. package/payload/platform/plugins/email/mcp/dist/lib/imap.d.ts +7 -44
  10. package/payload/platform/plugins/email/mcp/dist/lib/imap.d.ts.map +1 -1
  11. package/payload/platform/plugins/email/mcp/dist/lib/imap.js +24 -177
  12. package/payload/platform/plugins/email/mcp/dist/lib/imap.js.map +1 -1
  13. package/payload/platform/plugins/email/mcp/dist/tools/email-read.d.ts.map +1 -1
  14. package/payload/platform/plugins/email/mcp/dist/tools/email-read.js +4 -1
  15. package/payload/platform/plugins/email/mcp/dist/tools/email-read.js.map +1 -1
  16. package/payload/platform/plugins/email/mcp/dist/tools/email-search.d.ts.map +1 -1
  17. package/payload/platform/plugins/email/mcp/dist/tools/email-search.js +4 -1
  18. package/payload/platform/plugins/email/mcp/dist/tools/email-search.js.map +1 -1
  19. package/payload/platform/plugins/email/skills/email-composition/SKILL.md +1 -1
  20. package/payload/platform/plugins/memory/references/schema-base.md +1 -1
  21. package/payload/platform/plugins/memory/references/schema-estate-agent.md +1 -1
  22. package/payload/platform/scripts/lib/read-brand-json.sh +1 -1
  23. package/payload/platform/scripts/lib/resolve-account-dir.sh +1 -1
  24. package/payload/platform/scripts/seed-neo4j.sh +29 -421
  25. package/payload/platform/scripts/setup-account.sh +370 -0
  26. package/payload/platform/services/claude-session-manager/dist/config.js +2 -2
  27. package/payload/platform/services/claude-session-manager/dist/config.js.map +1 -1
  28. package/payload/platform/services/claude-session-manager/dist/fs-watcher.d.ts +9 -1
  29. package/payload/platform/services/claude-session-manager/dist/fs-watcher.d.ts.map +1 -1
  30. package/payload/platform/services/claude-session-manager/dist/fs-watcher.js +74 -38
  31. package/payload/platform/services/claude-session-manager/dist/fs-watcher.js.map +1 -1
  32. package/payload/platform/services/claude-session-manager/dist/http-server.d.ts.map +1 -1
  33. package/payload/platform/services/claude-session-manager/dist/http-server.js +16 -0
  34. package/payload/platform/services/claude-session-manager/dist/http-server.js.map +1 -1
  35. package/payload/platform/services/claude-session-manager/dist/pty-spawner.d.ts +16 -1
  36. package/payload/platform/services/claude-session-manager/dist/pty-spawner.d.ts.map +1 -1
  37. package/payload/platform/services/claude-session-manager/dist/pty-spawner.js +21 -7
  38. package/payload/platform/services/claude-session-manager/dist/pty-spawner.js.map +1 -1
  39. package/payload/platform/services/claude-session-manager/dist/session-sidecar.d.ts +7 -0
  40. package/payload/platform/services/claude-session-manager/dist/session-sidecar.d.ts.map +1 -1
  41. package/payload/platform/services/claude-session-manager/dist/session-sidecar.js +10 -0
  42. package/payload/platform/services/claude-session-manager/dist/session-sidecar.js.map +1 -1
  43. package/payload/platform/services/claude-session-manager/dist/systemd-scope.d.ts +52 -10
  44. package/payload/platform/services/claude-session-manager/dist/systemd-scope.d.ts.map +1 -1
  45. package/payload/platform/services/claude-session-manager/dist/systemd-scope.js +73 -13
  46. package/payload/platform/services/claude-session-manager/dist/systemd-scope.js.map +1 -1
  47. package/payload/platform/templates/agents/admin/IDENTITY.md +1 -1
@@ -1,19 +1,18 @@
1
1
  #!/usr/bin/env bash
2
2
  # ============================================================
3
- # seed-neo4j.sh — Phase 0 setup
4
- # Creates the account from templates and applies Neo4j schema.
5
- # Product knowledge is ingested post-installation via the admin
6
- # agent, not seeded as cypher.
3
+ # seed-neo4j.sh — Neo4j schema + root-node seeding
4
+ # Applies the schema (constraints, indexes, vector indexes) and
5
+ # MERGEs the per-account root :LocalBusiness and owner :AdminUser
6
+ # nodes. Account directory creation, Claude Code settings, hooks
7
+ # wiring, and specialist templates live in `setup-account.sh`.
8
+ # This script does no filesystem setup beyond reading what
9
+ # setup-account.sh wrote.
7
10
  # ============================================================
8
11
 
9
12
  set -euo pipefail
10
13
 
11
14
  SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
12
15
  PROJECT_DIR="$(dirname "$SCRIPT_DIR")"
13
- TEMPLATES_DIR="$PROJECT_DIR/templates"
14
- # Accounts live at {installDir}/data/accounts/ — outside the platform/ wipe zone.
15
- INSTALL_DIR="$(dirname "$PROJECT_DIR")"
16
- ACCOUNTS_DIR="$INSTALL_DIR/data/accounts"
17
16
  NEO4J_DIR="$PROJECT_DIR/neo4j"
18
17
 
19
18
  # NEO4J_URI is hard-required. The previous default
@@ -41,426 +40,20 @@ fi
41
40
  CYPHER_SHELL="cypher-shell"
42
41
 
43
42
  # ------------------------------------------------------------------
44
- # 1. Create account from templates
43
+ # 1. Resolve the account dir (must already exist; setup-account.sh
44
+ # is the creator). The resolver is idempotent — calling it here
45
+ # re-finds the same ACCOUNT_DIR / ACCOUNT_ID setup-account.sh
46
+ # minted.
45
47
  # ------------------------------------------------------------------
46
-
47
- # Resolve existing account by identity (users.json[0].userId) or mint a new one on fresh install.
48
- # Orphan siblings under data/accounts/ — non-matching dirs left behind by the userId
49
- # regeneration — are swept to data/accounts/.trash/<uuid>-<ISO8601-ts>/. Multi-candidate with
50
- # no identity match aborts loud; .trash/ is operator-emptied, never auto-cleaned.
51
48
  # shellcheck source=lib/resolve-account-dir.sh
52
49
  . "$SCRIPT_DIR/lib/resolve-account-dir.sh"
53
50
  # shellcheck source=lib/read-brand-json.sh
54
51
  . "$SCRIPT_DIR/lib/read-brand-json.sh"
55
- # Resolve brand-aware persistent users.json path before the resolver runs.
56
- # Mirrors the resolution lower in this file (around the post-Task-904 USERS_FILE
57
- # block); duplicated here because resolve_and_sweep_account_dir runs first.
58
52
  _RESOLVER_CONFIG_DIR_NAME=".maxy"
59
53
  read_brand_json_key "$PROJECT_DIR/config/brand.json" "configDir"
60
54
  [ -n "$BRAND_JSON_VALUE" ] && _RESOLVER_CONFIG_DIR_NAME="$BRAND_JSON_VALUE"
61
55
  USERS_FILE="$HOME/$_RESOLVER_CONFIG_DIR_NAME/users.json" resolve_and_sweep_account_dir
62
56
 
63
- mkdir -p "$ACCOUNT_DIR/agents/admin" "$ACCOUNT_DIR/agents/public" "$ACCOUNT_DIR/.claude" "$ACCOUNT_DIR/specialists/.claude-plugin" "$ACCOUNT_DIR/specialists/agents"
64
-
65
- # Claude Code discovers project-level .claude/ (agents, settings) relative to the
66
- # nearest .git root. Without a .git in the account directory, Claude Code traverses
67
- # up to ~/maxy/.git and misses everything in accountDir/.claude/. Initialise a git
68
- # repo so the account directory IS the project root for Claude Code.
69
- [ -d "$ACCOUNT_DIR/.git" ] || git init -q "$ACCOUNT_DIR"
70
-
71
- # Claude Code permissions — project-level settings now work since the account dir
72
- # has .git. Write permissions to the account's own .claude/settings.json.
73
- ACCOUNT_SETTINGS="$ACCOUNT_DIR/.claude/settings.json"
74
-
75
- # Wildcard permissions for all plugin MCP servers (core + optional).
76
- # Including optional plugins here is safe — permissions without a running
77
- # MCP server are inert. This ensures tools are allowed when the user
78
- # enables an optional plugin, without requiring a re-seed.
79
- MAXY_PERMISSIONS='["Read","Edit","Write","Bash","Glob","Grep","Agent","mcp__memory__*","mcp__contacts__*","mcp__telegram__*","mcp__admin__*","mcp__cloudflare__*","mcp__work__*","mcp__scheduling__*","mcp__email__*","mcp__workflows__*","mcp__web__*","mcp__plugin_playwright_playwright__*"]'
80
-
81
- # Append wildcard permissions for any optional plugins not already covered
82
- PLUGINS_DIR="$(dirname "$0")/../plugins"
83
- if [ -d "$PLUGINS_DIR" ]; then
84
- for PLUGIN_DIR in "$PLUGINS_DIR"/*/; do
85
- PLUGIN_NAME="$(basename "$PLUGIN_DIR")"
86
- # Skip plugins already in the static list
87
- case "$MAXY_PERMISSIONS" in
88
- *"mcp__${PLUGIN_NAME}__"*) continue ;;
89
- esac
90
- # Only add if plugin has an MCP server
91
- if [ -d "$PLUGIN_DIR/mcp" ]; then
92
- MAXY_PERMISSIONS="${MAXY_PERMISSIONS%]},\"mcp__${PLUGIN_NAME}__*\"]"
93
- fi
94
- done
95
- fi
96
-
97
- # Always overwrite — Rubytech-controlled. Project-level settings take
98
- # precedence over global, so each account is properly isolated.
99
- #
100
- # Hook commands use $PLATFORM_ROOT (set by claude-agent.ts at spawn time)
101
- # to locate scripts relative to the platform installation, not the account dir.
102
- #
103
- # Hooks wired:
104
- # PreToolUse — pre-tool-use.sh: admin write boundaries
105
- # playwright-file-guard.sh: intercepts browser_navigate with file:// URLs
106
- # webfetch-preflight.mjs: detects JS-SPA shells before WebFetch's
107
- # 60s extraction timeout. Fail-open on any error;
108
- # on positive SPA detection exits 2 with WEBFETCH_CANNOT_READ_JS_SPA.
109
- # archive-ingest-surface-gate.sh:
110
- # narrows the database-operator subagent's effective surface
111
- # during conversation-archive ingestion to exactly one Bash
112
- # entry (memory/bin/conversation-archive-ingest.sh --source
113
- # <enum>) plus read-only neighbours. Defensive blocks on
114
- # withdrawn MCP tool names (mcp__memory__whatsapp-export-
115
- # {parse,preview,insight-write,insight-pass},
116
- # mcp__memory__memory-archive-write when
117
- # archiveType=whatsapp-export) remain so any future
118
- # reintroduction loud-fails. Preserves the plugin-source
119
- # edit, JS test-runner, and post-parse-error blocks.
120
- # Wired at three points: explicit Edit/Write/NotebookEdit/Bash
121
- # matchers, a no-matcher PreToolUse entry (the
122
- # post-parse-error gate fires on every tool),
123
- # and a PostToolUse regex matcher for parse tools.
124
- # UserPromptSubmit — archive-ingest-surface-gate.sh clears the
125
- # parse-error flag when the operator's next prompt arrives.
126
- # askuserquestion-investigate-gate.sh (Task 241): PreToolUse
127
- # matcher=AskUserQuestion. Blocks the question when no
128
- # read-only investigation tool has fired since the latest
129
- # real user turn in the session JSONL. Fail-open on
130
- # missing transcript or parse error. One [ask-gate]
131
- # decision line per call.
132
- #
133
- # Task 214 — the per-turn Stop-hook registration that fired
134
- # turn-completed-graph-write.sh once per admin turn is no longer written.
135
- # The script, envelope walker, loopback /api/admin/claude-sessions 127.0.0.1
136
- # bypass, [turn-recorder] emitters, recorder-auto-archive subscriber, and
137
- # initialMessage envelope spec are preserved as dormant infrastructure for
138
- # any future autonomous post-turn flow. Admin now delegates graph writes
139
- # to database-operator via the Task tool inside its own session.
140
- #
141
- # Task 282 — Stop hook session-end-retrospective.sh wired below. Fires on
142
- # every assistant end_turn but exits 0 with `trigger-skipped reason=…`
143
- # on every path EXCEPT when the operator's latest real-user message
144
- # carries an end-intent token (/end, /archive, end session,
145
- # archive this session). On that path it blocks (exit 2 + stderr
146
- # instruction block) until the admin agent calls the
147
- # session-retrospective-mark-complete MCP sentinel tool, whose tool_use
148
- # in the JSONL is the release signal. Recursion guard: any session
149
- # carrying MAXY_SPECIALIST=<name> (specialist subagent) skips the gate.
150
- HOOKS_PATH="\$PLATFORM_ROOT/plugins/admin/hooks"
151
- cat > "$ACCOUNT_SETTINGS" << SETTINGS_EOF
152
- {
153
- "permissions": {
154
- "allow": $MAXY_PERMISSIONS
155
- },
156
- "hooks": {
157
- "PreToolUse": [
158
- {
159
- "matcher": "Write",
160
- "hooks": [
161
- { "type": "command", "command": "bash $HOOKS_PATH/pre-tool-use.sh admin" },
162
- { "type": "command", "command": "bash $HOOKS_PATH/archive-ingest-surface-gate.sh" }
163
- ]
164
- },
165
- {
166
- "matcher": "Edit",
167
- "hooks": [
168
- { "type": "command", "command": "bash $HOOKS_PATH/pre-tool-use.sh admin" },
169
- { "type": "command", "command": "bash $HOOKS_PATH/archive-ingest-surface-gate.sh" }
170
- ]
171
- },
172
- {
173
- "matcher": "NotebookEdit",
174
- "hooks": [
175
- { "type": "command", "command": "bash $HOOKS_PATH/archive-ingest-surface-gate.sh" }
176
- ]
177
- },
178
- {
179
- "matcher": "Bash",
180
- "hooks": [
181
- { "type": "command", "command": "bash $HOOKS_PATH/pre-tool-use.sh admin" },
182
- { "type": "command", "command": "bash $HOOKS_PATH/archive-ingest-surface-gate.sh" }
183
- ]
184
- },
185
- {
186
- "matcher": "mcp__plugin_memory_memory__memory-write",
187
- "hooks": [
188
- { "type": "command", "command": "bash $HOOKS_PATH/pre-tool-use.sh admin" }
189
- ]
190
- },
191
- {
192
- "matcher": "mcp__plugin_memory_memory__memory-update",
193
- "hooks": [
194
- { "type": "command", "command": "bash $HOOKS_PATH/pre-tool-use.sh admin" }
195
- ]
196
- },
197
- {
198
- "hooks": [
199
- { "type": "command", "command": "bash $HOOKS_PATH/archive-ingest-surface-gate.sh" }
200
- ]
201
- },
202
- {
203
- "matcher": "mcp__plugin_playwright_playwright__browser_navigate",
204
- "hooks": [
205
- { "type": "command", "command": "bash $HOOKS_PATH/playwright-file-guard.sh" }
206
- ]
207
- },
208
- {
209
- "matcher": "WebFetch",
210
- "hooks": [
211
- { "type": "command", "command": "node $HOOKS_PATH/webfetch-preflight.mjs" }
212
- ]
213
- },
214
- {
215
- "matcher": "AskUserQuestion",
216
- "hooks": [
217
- { "type": "command", "command": "bash $HOOKS_PATH/askuserquestion-investigate-gate.sh" }
218
- ]
219
- }
220
- ],
221
- "PostToolUse": [
222
- {
223
- "matcher": "mcp__.*__.*-(export|import)-parse$",
224
- "hooks": [
225
- { "type": "command", "command": "bash $HOOKS_PATH/archive-ingest-surface-gate.sh" }
226
- ]
227
- }
228
- ],
229
- "UserPromptSubmit": [
230
- {
231
- "hooks": [
232
- { "type": "command", "command": "bash $HOOKS_PATH/archive-ingest-surface-gate.sh" }
233
- ]
234
- }
235
- ],
236
- "Stop": [
237
- {
238
- "hooks": [
239
- { "type": "command", "command": "bash $HOOKS_PATH/session-end-retrospective.sh" }
240
- ]
241
- }
242
- ]
243
- }
244
- }
245
- SETTINGS_EOF
246
-
247
- # Always overwrite IDENTITY.md — Rubytech-controlled, pure behaviour.
248
- cp "$TEMPLATES_DIR/agents/admin/IDENTITY.md" "$ACCOUNT_DIR/agents/admin/IDENTITY.md"
249
- cp "$TEMPLATES_DIR/agents/public/IDENTITY.md" "$ACCOUNT_DIR/agents/public/IDENTITY.md"
250
- echo " agents/public/IDENTITY.md installed"
251
-
252
- # Always overwrite specialist plugin — Rubytech-controlled, not user-editable.
253
- # Specialists are a Claude Code plugin loaded via --plugin-dir. The plugin lives at
254
- # accountDir/specialists/ with .claude-plugin/plugin.json and agents/*.md.
255
- # The admin agent dispatches them as specialists:{name} via the Agent tool.
256
- cp "$TEMPLATES_DIR/specialists/.claude-plugin/plugin.json" "$ACCOUNT_DIR/specialists/.claude-plugin/plugin.json"
257
-
258
- # Remove orphaned core specialist files before copying fresh templates.
259
- # Core specialists (no '--' in filename) are Rubytech-controlled and fully replaced
260
- # on every seed. Premium plugin agents (filename contains '--') are preserved —
261
- # they are managed by the admin agent when plugins are activated/deactivated.
262
- for _existing in "$ACCOUNT_DIR/specialists/agents/"*.md; do
263
- [ -f "$_existing" ] || continue
264
- case "$(basename "$_existing")" in
265
- *--*) continue ;; # Premium plugin agent — preserve
266
- esac
267
- rm -f "$_existing"
268
- done
269
-
270
- for specialist in "$TEMPLATES_DIR/specialists/agents/"*.md; do
271
- [ -f "$specialist" ] && cp "$specialist" "$ACCOUNT_DIR/specialists/agents/$(basename "$specialist")"
272
- done
273
-
274
- # Clean up stale .claude/agents/*.md from previous seed format.
275
- # The old format placed specialists here as slash-command agents — that never worked
276
- # for programmatic dispatch. Safe to remove; .claude/ dir stays for settings.json.
277
- rm -f "$ACCOUNT_DIR/.claude/agents/"*.md 2>/dev/null || true
278
-
279
- # SOUL.md — user-controlled personalisation. Only create if missing. Never overwrite.
280
- [ -f "$ACCOUNT_DIR/agents/admin/SOUL.md" ] || cp "$TEMPLATES_DIR/agents/admin/SOUL.md" "$ACCOUNT_DIR/agents/admin/SOUL.md"
281
-
282
- # Public agent SOUL.md + config.json — create if missing, never overwrite.
283
- # IDENTITY.md is handled in the Rubytech-controlled block above.
284
- [ -f "$ACCOUNT_DIR/agents/public/SOUL.md" ] || cp "$TEMPLATES_DIR/agents/public/SOUL.md" "$ACCOUNT_DIR/agents/public/SOUL.md"
285
- [ -f "$ACCOUNT_DIR/agents/public/config.json" ] || cp "$TEMPLATES_DIR/agents/public/config.json" "$ACCOUNT_DIR/agents/public/config.json"
286
-
287
- # AGENTS.md — specialist registry. Pre-populated from templates, maintained by admin agent at runtime.
288
- # Always overwrite on re-seed (Rubytech-controlled initial state, like IDENTITY.md).
289
- # Dispatch format: specialists:{name} via the Agent tool.
290
- printf '# Installed Roles\n\nDispatch via the Agent tool with `subagent_type: "specialists:{name}"`.\n\n' > "$ACCOUNT_DIR/agents/admin/AGENTS.md"
291
- for specialist in "$ACCOUNT_DIR/specialists/agents/"*.md; do
292
- [ -f "$specialist" ] || continue
293
- # Extract name and description from YAML frontmatter
294
- _name=$(sed -n 's/^name: *//p' "$specialist" | head -1)
295
- _desc=$(sed -n 's/^description: *"*//p' "$specialist" | sed 's/"$//' | head -1)
296
- [ -z "$_name" ] && continue
297
- # Skip Phase 1 stubs — not yet functional
298
- echo "$_desc" | grep -qi "Phase 1" && continue
299
- printf -- '- **specialists:%s**: %s\n' "$_name" "$_desc" >> "$ACCOUNT_DIR/agents/admin/AGENTS.md"
300
- done
301
-
302
- # Create account.json only if it doesn't exist (business config is in the graph).
303
- # Reads defaults from the template so there is a single source of truth.
304
- if [ ! -f "$ACCOUNT_DIR/account.json" ]; then
305
- TEMPLATE="$TEMPLATES_DIR/account.json"
306
- if [ -f "$TEMPLATE" ]; then
307
- sed "s/\"accountId\": *\"[^\"]*\"/\"accountId\":\"$ACCOUNT_ID\"/" "$TEMPLATE" \
308
- | sed "s/\"tier\": *\"[^\"]*\"/\"tier\":\"solo\"/" \
309
- > "$ACCOUNT_DIR/account.json"
310
- else
311
- echo "ERROR: account.json template not found at $TEMPLATE" >&2
312
- exit 1
313
- fi
314
- fi
315
-
316
- # Backfill defaultAgent in account.json if missing (existing accounts earlier).
317
- # Uses the first non-admin agent directory that has a config.json.
318
- if [ -f "$ACCOUNT_DIR/account.json" ] && ! grep -q '"defaultAgent"' "$ACCOUNT_DIR/account.json"; then
319
- _DEFAULT_SLUG=""
320
- for _AGENT_DIR in "$ACCOUNT_DIR/agents"/*/; do
321
- _AGENT_NAME="$(basename "$_AGENT_DIR")"
322
- [ "$_AGENT_NAME" = "admin" ] && continue
323
- [ -f "$_AGENT_DIR/config.json" ] && { _DEFAULT_SLUG="$_AGENT_NAME"; break; }
324
- done
325
- if [ -n "$_DEFAULT_SLUG" ]; then
326
- # Insert defaultAgent before the closing brace
327
- sed -i'' "s/}$/,\"defaultAgent\":\"$_DEFAULT_SLUG\"}/" "$ACCOUNT_DIR/account.json"
328
- echo " Backfilled defaultAgent=$_DEFAULT_SLUG in account.json"
329
- fi
330
- fi
331
-
332
- # ------------------------------------------------------------------
333
- # Seed enabledPlugins from brand.json plugins.defaultEnabled
334
- # ------------------------------------------------------------------
335
- # Fresh accounts: replaces the template's empty [] with defaultEnabled.
336
- # Existing accounts: additive merge — adds missing defaultEnabled entries,
337
- # never removes plugins the user enabled manually.
338
- # Dependency validation: skips any defaultEnabled plugin whose PLUGIN.md
339
- # `requires:` dependencies are not satisfied by core or defaultEnabled.
340
- BRAND_JSON="$PROJECT_DIR/config/brand.json"
341
- if [ -f "$BRAND_JSON" ] && [ -f "$ACCOUNT_DIR/account.json" ]; then
342
- python3 -c "
343
- import json, os, sys
344
-
345
- brand_path = '$BRAND_JSON'
346
- account_path = '$ACCOUNT_DIR/account.json'
347
- plugins_dir = '$PROJECT_DIR/plugins'
348
-
349
- with open(brand_path) as f:
350
- brand = json.load(f)
351
-
352
- plugins_cfg = brand.get('plugins', {})
353
- default_enabled = plugins_cfg.get('defaultEnabled', [])
354
- core = set(plugins_cfg.get('core', []))
355
-
356
- if not default_enabled:
357
- print(' [seed] no defaultEnabled plugins in brand.json — skipping')
358
- sys.exit(0)
359
-
360
- # Dependency validation: check each plugin's PLUGIN.md requires field.
361
- # A dependency is satisfied if it appears in core or in defaultEnabled.
362
- satisfied = core | set(default_enabled)
363
- validated = []
364
- for plugin in default_enabled:
365
- plugin_md = os.path.join(plugins_dir, plugin, 'PLUGIN.md')
366
- requires = []
367
- if os.path.isfile(plugin_md):
368
- in_frontmatter = False
369
- in_requires = False
370
- with open(plugin_md) as f:
371
- for line in f:
372
- stripped = line.rstrip()
373
- if stripped == '---':
374
- if in_frontmatter:
375
- break
376
- in_frontmatter = True
377
- continue
378
- if not in_frontmatter:
379
- continue
380
- if stripped.startswith('requires:'):
381
- # Handle inline list: requires: [a, b]
382
- inline = stripped.split(':', 1)[1].strip()
383
- if inline.startswith('['):
384
- requires = [x.strip().strip('\"').strip(\"'\") for x in inline.strip('[]').split(',') if x.strip()]
385
- in_requires = False
386
- else:
387
- in_requires = True
388
- continue
389
- if in_requires:
390
- if stripped.startswith(' - '):
391
- requires.append(stripped.lstrip(' -').strip())
392
- else:
393
- in_requires = False
394
- unmet = [r for r in requires if r not in satisfied]
395
- if unmet:
396
- print(f' [seed] WARNING: skipping defaultEnabled plugin \"{plugin}\" — unmet requires: {unmet}')
397
- else:
398
- validated.append(plugin)
399
-
400
- with open(account_path) as f:
401
- account = json.load(f)
402
-
403
- existing = account.get('enabledPlugins', [])
404
- existing_set = set(existing)
405
- added = [p for p in validated if p not in existing_set]
406
- account['enabledPlugins'] = existing + added
407
-
408
- with open(account_path, 'w') as f:
409
- json.dump(account, f, indent=2)
410
- f.write('\n')
411
-
412
- if not existing:
413
- print(f' [seed] seeded enabledPlugins: {validated}')
414
- elif added:
415
- print(f' [seed] merged into enabledPlugins: +{added} (existing: {existing})')
416
- else:
417
- print(f' [seed] enabledPlugins already contains all defaultEnabled entries')
418
- "
419
- fi
420
-
421
- echo " Account $ACCOUNT_ID at $ACCOUNT_DIR"
422
-
423
- # ------------------------------------------------------------------
424
- # 1b. Resolve first admin userId from users.json
425
- # ------------------------------------------------------------------
426
- # users.json is created by the set-pin POST during onboarding. The seed
427
- # script reads it to stamp the AdminUser node; it never creates users.json.
428
-
429
- CONFIG_DIR="$PROJECT_DIR/config"
430
-
431
- # Resolve the brand-specific config directory.
432
- # Mirrors the logic in platform/ui/app/lib/paths.ts.
433
- _CONFIG_DIR_NAME=".maxy"
434
- read_brand_json_key "$CONFIG_DIR/brand.json" "configDir"
435
- [ -n "$BRAND_JSON_VALUE" ] && _CONFIG_DIR_NAME="$BRAND_JSON_VALUE"
436
- # users.json lives under $HOME/$_CONFIG_DIR_NAME, not the wipe zone.
437
- USERS_FILE="$HOME/$_CONFIG_DIR_NAME/users.json"
438
- mkdir -p "$HOME/$_CONFIG_DIR_NAME"
439
-
440
- if [ -f "$USERS_FILE" ]; then
441
- USER_ID=$(python3 -c "import json; print(json.load(open('$USERS_FILE'))[0]['userId'])" 2>/dev/null || true)
442
- [ -n "$USER_ID" ] && echo " Existing users.json found, userId=$USER_ID"
443
-
444
- # Ensure account.json admins[] contains the owner. Idempotent.
445
- if [ -n "$USER_ID" ] && [ -f "$ACCOUNT_DIR/account.json" ]; then
446
- python3 -c "
447
- import json
448
- with open('$ACCOUNT_DIR/account.json', 'r') as f:
449
- config = json.load(f)
450
- config.setdefault('admins', [])
451
- if not any(a.get('userId') == '$USER_ID' for a in config['admins']):
452
- config['admins'].append({'userId': '$USER_ID', 'role': 'owner'})
453
- with open('$ACCOUNT_DIR/account.json', 'w') as f:
454
- json.dump(config, f, indent=2)
455
- f.write('\n')
456
- print(' Stamped userId=$USER_ID as owner in account.json admins')
457
- "
458
- fi
459
- else
460
- echo " No users.json found — skipping AdminUser seed (set-pin POST creates it)"
461
- USER_ID=""
462
- fi
463
-
464
57
  # ------------------------------------------------------------------
465
58
  # 2. Apply Neo4j schema (constraints + indexes only)
466
59
  # ------------------------------------------------------------------
@@ -486,7 +79,7 @@ fi
486
79
  echo "$SCHEMA_CYPHER" | "$CYPHER_SHELL" -u "$NEO4J_USER" -p "$NEO4J_PASSWORD" -a "$NEO4J_URI"
487
80
 
488
81
  # ------------------------------------------------------------------
489
- # 2.5 Bootstrap root :LocalBusiness for the account
82
+ # 3. Bootstrap root :LocalBusiness for the account
490
83
  # ------------------------------------------------------------------
491
84
  # Every node in the graph hierarchy (Project, Task, Person, Organisation,
492
85
  # KnowledgeDocument) anchors to a :LocalBusiness via accountId. Without a
@@ -505,6 +98,7 @@ echo "$SCHEMA_CYPHER" | "$CYPHER_SHELL" -u "$NEO4J_USER" -p "$NEO4J_PASSWORD" -a
505
98
  # default filled in on re-seed. createdAt is set ON CREATE only, so re-runs
506
99
  # preserve the original timestamp.
507
100
 
101
+ BRAND_JSON="$PROJECT_DIR/config/brand.json"
508
102
  read_brand_json_key "$BRAND_JSON" "vertical"
509
103
  LOCALBUSINESS_BUSINESS_TYPE=$(derive_business_type "$BRAND_JSON_VALUE")
510
104
  LOCALBUSINESS_BT_CYPHER=$(business_type_cypher "$LOCALBUSINESS_BUSINESS_TYPE")
@@ -547,10 +141,22 @@ fi
547
141
  echo " [install-invariant] localbusiness-bootstrap accountId=${ACCOUNT_ID:0:8} businessType=$LOCALBUSINESS_BT_LOG result=$LOCALBUSINESS_RESULT"
548
142
 
549
143
  # ------------------------------------------------------------------
550
- # 3. Create AdminUser node + ADMIN_OF relationship
144
+ # 4. Create AdminUser node + ADMIN_OF relationship
551
145
  # ------------------------------------------------------------------
146
+ # Resolve owner userId from users.json (set-pin POST creates it).
147
+ # Same brand-aware resolution as setup-account.sh.
148
+ CONFIG_DIR="$PROJECT_DIR/config"
149
+ _CONFIG_DIR_NAME=".maxy"
150
+ read_brand_json_key "$CONFIG_DIR/brand.json" "configDir"
151
+ [ -n "$BRAND_JSON_VALUE" ] && _CONFIG_DIR_NAME="$BRAND_JSON_VALUE"
152
+ USERS_FILE="$HOME/$_CONFIG_DIR_NAME/users.json"
153
+
154
+ USER_ID=""
155
+ if [ -f "$USERS_FILE" ]; then
156
+ USER_ID=$(python3 -c "import json; print(json.load(open('$USERS_FILE'))[0]['userId'])" 2>/dev/null || true)
157
+ fi
552
158
 
553
- if [ -n "${USER_ID:-}" ]; then
159
+ if [ -n "$USER_ID" ]; then
554
160
  # Escape single quotes for Cypher string interpolation (e.g. O'Brien → O''Brien).
555
161
  USER_NAME=$(python3 -c "import json; n=json.load(open('$USERS_FILE'))[0]['name']; print(n.replace(\"'\",\"''\"))" 2>/dev/null || echo "Owner")
556
162
  CREATED_AT=$(date -u +%Y-%m-%dT%H:%M:%SZ)
@@ -576,6 +182,8 @@ ON CREATE SET r.role = 'owner', r.grantedAt = '$CREATED_AT'
576
182
  RETURN au.userId, b.accountId;
577
183
  CYPHER_EOF
578
184
  echo " [seed] AdminUser stamped userId=${USER_ID:0:8} accountId=${ACCOUNT_ID:0:8} role=owner"
185
+ else
186
+ echo " No users.json found — skipping AdminUser seed (set-pin POST creates it)"
579
187
  fi
580
188
 
581
189
  echo " Done."