@rubytech/create-maxy-code 0.1.111 → 0.1.113

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 (54) hide show
  1. package/dist/__tests__/brew-install.test.js +10 -0
  2. package/dist/__tests__/brew-resolve.test.js +35 -0
  3. package/dist/__tests__/launchd-plist.test.js +46 -0
  4. package/dist/__tests__/macos-darwin-branch.test.js +85 -0
  5. package/dist/brew-install.js +5 -0
  6. package/dist/index.js +26 -6
  7. package/package.json +1 -1
  8. package/payload/platform/plugins/cloudflare/references/manual-setup.md +105 -0
  9. package/payload/platform/plugins/docs/references/deployment.md +22 -4
  10. package/payload/platform/plugins/memory/mcp/dist/index.js +35 -0
  11. package/payload/platform/plugins/memory/mcp/dist/index.js.map +1 -1
  12. package/payload/platform/plugins/memory/mcp/dist/tools/image-fetch.d.ts +15 -0
  13. package/payload/platform/plugins/memory/mcp/dist/tools/image-fetch.d.ts.map +1 -0
  14. package/payload/platform/plugins/memory/mcp/dist/tools/image-fetch.js +64 -0
  15. package/payload/platform/plugins/memory/mcp/dist/tools/image-fetch.js.map +1 -0
  16. package/payload/platform/plugins/memory/references/schema-estate-agent.md +76 -1
  17. package/payload/platform/plugins/venture-studio/PLUGIN.md +13 -4
  18. package/payload/platform/plugins/venture-studio/bin/scaffold.sh +15 -3
  19. package/payload/platform/plugins/venture-studio/skills/brand-pack/SKILL.md +1 -1
  20. package/payload/platform/plugins/venture-studio/skills/investor-data-room/SKILL.md +27 -14
  21. package/payload/platform/plugins/venture-studio/skills/investor-data-room/references/business-plan-template.md +7 -4
  22. package/payload/platform/plugins/venture-studio/skills/investor-data-room/references/data-room-structure.md +16 -5
  23. package/payload/platform/plugins/venture-studio/skills/investor-data-room/references/deck-blueprint-template.md +7 -6
  24. package/payload/platform/plugins/venture-studio/skills/office-hours/SKILL.md +8 -9
  25. package/payload/platform/plugins/venture-studio/skills/prototype-host/SKILL.md +179 -0
  26. package/payload/platform/plugins/venture-studio/skills/prototype-host/references/cloudflared-ingress-edit.md +81 -0
  27. package/payload/platform/plugins/venture-studio/skills/prototype-host/references/scaffold-frameworks.md +60 -0
  28. package/payload/platform/plugins/venture-studio/skills/prototype-host/references/systemd-user-service.md +104 -0
  29. package/payload/platform/plugins/venture-studio/skills/zero-to-prototype/SKILL.md +4 -0
  30. package/payload/platform/templates/agents/public/IDENTITY.md +11 -1
  31. package/payload/premium-plugins/real-agent/BUNDLE.md +3 -2
  32. package/payload/premium-plugins/real-agent/agents/listing-curator.md +152 -0
  33. package/payload/premium-plugins/real-agent/plugins/brochures/skills/a4-print-documents/SKILL.md +1 -1
  34. package/payload/premium-plugins/real-agent/plugins/brochures/skills/brand-design/SKILL.md +5 -11
  35. package/payload/premium-plugins/real-agent/plugins/brochures/skills/make-brochure/SKILL.md +7 -7
  36. package/payload/premium-plugins/real-agent/plugins/brochures/skills/property-extract/SKILL.md +31 -39
  37. package/payload/premium-plugins/real-agent/plugins/brochures/skills/property-market-report/SKILL.md +4 -4
  38. package/payload/premium-plugins/real-agent/plugins/brochures/skills/property-socials/SKILL.md +3 -2
  39. package/payload/premium-plugins/real-agent/plugins/buyers/.claude-plugin/plugin.json +1 -1
  40. package/payload/premium-plugins/real-agent/plugins/buyers/PLUGIN.md +3 -2
  41. package/payload/premium-plugins/real-agent/plugins/buyers/skills/property-recommender/SKILL.md +96 -0
  42. package/payload/premium-plugins/venture-studio/PLUGIN.md +13 -4
  43. package/payload/premium-plugins/venture-studio/bin/scaffold.sh +15 -3
  44. package/payload/premium-plugins/venture-studio/skills/brand-pack/SKILL.md +1 -1
  45. package/payload/premium-plugins/venture-studio/skills/investor-data-room/SKILL.md +27 -14
  46. package/payload/premium-plugins/venture-studio/skills/investor-data-room/references/business-plan-template.md +7 -4
  47. package/payload/premium-plugins/venture-studio/skills/investor-data-room/references/data-room-structure.md +16 -5
  48. package/payload/premium-plugins/venture-studio/skills/investor-data-room/references/deck-blueprint-template.md +7 -6
  49. package/payload/premium-plugins/venture-studio/skills/office-hours/SKILL.md +8 -9
  50. package/payload/premium-plugins/venture-studio/skills/prototype-host/SKILL.md +179 -0
  51. package/payload/premium-plugins/venture-studio/skills/prototype-host/references/cloudflared-ingress-edit.md +81 -0
  52. package/payload/premium-plugins/venture-studio/skills/prototype-host/references/scaffold-frameworks.md +60 -0
  53. package/payload/premium-plugins/venture-studio/skills/prototype-host/references/systemd-user-service.md +104 -0
  54. package/payload/premium-plugins/venture-studio/skills/zero-to-prototype/SKILL.md +4 -0
@@ -73,9 +73,14 @@ test("decideBrewInstallPlan: all pkgs already installed → empty install list,
73
73
  ],
74
74
  });
75
75
  assert.deepEqual(plan.toInstall, []);
76
+ // Each pkg emits the legacy `[homebrew]` line plus the uniform-prefix
77
+ // `[create-maxy] brew-formula=… action=…` mirror Task 297 added for the
78
+ // grep recipe.
76
79
  assert.deepEqual(plan.logs, [
77
80
  " [homebrew] node@22 already installed (v22.18.1)",
81
+ " [create-maxy] brew-formula=node@22 action=present",
78
82
  " [homebrew] neo4j already installed (v5.20.0)",
83
+ " [create-maxy] brew-formula=neo4j action=present",
79
84
  ]);
80
85
  });
81
86
  test("decideBrewInstallPlan: some missing → install list contains only missing formulas, install + already logs interleaved", () => {
@@ -89,8 +94,11 @@ test("decideBrewInstallPlan: some missing → install list contains only missing
89
94
  assert.deepEqual(plan.toInstall, ["neo4j", "cloudflared"]);
90
95
  assert.deepEqual(plan.logs, [
91
96
  " [homebrew] node@22 already installed (v22.18.1)",
97
+ " [create-maxy] brew-formula=node@22 action=present",
92
98
  " [homebrew] installing neo4j …",
99
+ " [create-maxy] brew-formula=neo4j action=install",
93
100
  " [homebrew] installing cloudflared …",
101
+ " [create-maxy] brew-formula=cloudflared action=install",
94
102
  ]);
95
103
  });
96
104
  test("decideBrewInstallPlan: all missing → install list = every formula, only install logs", () => {
@@ -103,7 +111,9 @@ test("decideBrewInstallPlan: all missing → install list = every formula, only
103
111
  assert.deepEqual(plan.toInstall, ["node@22", "neo4j"]);
104
112
  assert.deepEqual(plan.logs, [
105
113
  " [homebrew] installing node@22 …",
114
+ " [create-maxy] brew-formula=node@22 action=install",
106
115
  " [homebrew] installing neo4j …",
116
+ " [create-maxy] brew-formula=neo4j action=install",
107
117
  ]);
108
118
  });
109
119
  // ---------------------------------------------------------------------------
@@ -101,3 +101,38 @@ test("DARWIN_ALIASES contains nodejs → node@22 and is the only entry", () => {
101
101
  assert.equal(DARWIN_ALIASES.nodejs, "node@22");
102
102
  assert.equal(Object.keys(DARWIN_ALIASES).length, 1);
103
103
  });
104
+ // ---------------------------------------------------------------------------
105
+ // DARWIN_BASE_DEPS + neo4j shape lock — every brew dep the darwin install
106
+ // branch feeds into installAllBrewPackages must either (a) be a real Homebrew
107
+ // core formula already, or (b) resolve to one via DARWIN_ALIASES. The list is
108
+ // duplicated here from index.ts:installSystemDeps so a future edit of either
109
+ // surface has to keep them in sync.
110
+ // ---------------------------------------------------------------------------
111
+ // Mirrors `DARWIN_BASE_DEPS` in packages/create-maxy-code/src/index.ts. Every
112
+ // entry must be either a real brew formula (pass-through) or an alias.
113
+ const DARWIN_BASE_DEPS_FIXTURE = ["curl", "git", "unzip", "jq", "poppler", "ffmpeg"];
114
+ // `neo4j` and `cloudflared` are passed to installAllBrewPackages from
115
+ // installNeo4j (index.ts:1265) and installCloudflared (index.ts:1728). Both are
116
+ // concrete brew formula names — no alias hop expected.
117
+ const DARWIN_SOLO_DEPS_FIXTURE = ["neo4j", "cloudflared"];
118
+ test("every DARWIN_BASE_DEPS entry passes through the resolver as the brew formula name", () => {
119
+ for (const pkg of DARWIN_BASE_DEPS_FIXTURE) {
120
+ const r = decideBrewResolution({ pkg, brewInstalled: false });
121
+ assert.equal(r.resolved, pkg, `DARWIN_BASE_DEPS entry '${pkg}' must not be aliased`);
122
+ assert.equal(r.log, null, `DARWIN_BASE_DEPS entry '${pkg}' must not emit an alias log line`);
123
+ }
124
+ });
125
+ test("neo4j and cloudflared pass through unchanged — no alias hop", () => {
126
+ for (const pkg of DARWIN_SOLO_DEPS_FIXTURE) {
127
+ const r = decideBrewResolution({ pkg, brewInstalled: false });
128
+ assert.equal(r.resolved, pkg);
129
+ assert.equal(r.log, null);
130
+ }
131
+ });
132
+ test("nodejs alias hop resolves to node@22 for darwin install", () => {
133
+ // The shared-dep call site (index.ts:988 installNodejs) passes "nodejs";
134
+ // this test pins the brew-formula landing for that single alias.
135
+ const r = decideBrewResolution({ pkg: "nodejs", brewInstalled: false });
136
+ assert.equal(r.resolved, "node@22");
137
+ assert.match(r.log ?? "", /node@22/);
138
+ });
@@ -128,6 +128,52 @@ test("renderPlist escapes `&` once, never double-escapes", () => {
128
128
  // child to "" (current dir = launchd's cwd, /), which silently changes
129
129
  // behaviour from "stay where invoked" to "/" — a different bug than absence.
130
130
  // ---------------------------------------------------------------------------
131
+ // ---------------------------------------------------------------------------
132
+ // Brand-stamped labels — both shipping brands must render distinct labels so a
133
+ // co-resident install does not collide on a single LaunchAgent file.
134
+ // `launchdLabel()` in index.ts returns `com.rubytech.${BRAND.hostname}`; this
135
+ // test pins the brand-input → label contract end-to-end against the renderer.
136
+ // ---------------------------------------------------------------------------
137
+ test("renderPlist maxy-code brand fields land in the plist verbatim", () => {
138
+ // BRAND.hostname=maxy-code, configDir=.maxy-code per brands/maxy-code/brand.json.
139
+ // installServiceDarwin builds ProgramArguments=["/bin/bash", wrapperPath],
140
+ // WorkingDirectory=`${INSTALL_DIR}/server`, std{out,err}Path=`<logsDir>/server.log`.
141
+ const xml = renderPlist({
142
+ label: "com.rubytech.maxy-code",
143
+ programArguments: ["/bin/bash", "/Users/alice/.maxy-code/launchd-wrapper.sh"],
144
+ stdoutPath: "/Users/alice/.maxy-code/logs/server.log",
145
+ stderrPath: "/Users/alice/.maxy-code/logs/server.log",
146
+ keepAlive: true,
147
+ runAtLoad: true,
148
+ workingDirectory: "/Users/alice/maxy-code/server",
149
+ });
150
+ assert.match(xml, /<key>Label<\/key>\s*<string>com\.rubytech\.maxy-code<\/string>/);
151
+ assert.match(xml, /<string>\/bin\/bash<\/string>/);
152
+ assert.match(xml, /<string>\/Users\/alice\/\.maxy-code\/launchd-wrapper\.sh<\/string>/);
153
+ assert.match(xml, /<key>WorkingDirectory<\/key>\s*<string>\/Users\/alice\/maxy-code\/server<\/string>/);
154
+ assert.match(xml, /<key>StandardOutPath<\/key>\s*<string>\/Users\/alice\/\.maxy-code\/logs\/server\.log<\/string>/);
155
+ assert.match(xml, /<key>KeepAlive<\/key>\s*<true\/>/);
156
+ assert.match(xml, /<key>RunAtLoad<\/key>\s*<true\/>/);
157
+ });
158
+ test("renderPlist realagent-code brand fields land in the plist verbatim", () => {
159
+ // BRAND.hostname=realagent-code, configDir=.realagent-code per brands/realagent-code/brand.json.
160
+ // The label must differ from the maxy-code label so a co-resident install
161
+ // produces two distinct ~/Library/LaunchAgents/<label>.plist files.
162
+ const xml = renderPlist({
163
+ label: "com.rubytech.realagent-code",
164
+ programArguments: ["/bin/bash", "/Users/alice/.realagent-code/launchd-wrapper.sh"],
165
+ stdoutPath: "/Users/alice/.realagent-code/logs/server.log",
166
+ stderrPath: "/Users/alice/.realagent-code/logs/server.log",
167
+ keepAlive: true,
168
+ runAtLoad: true,
169
+ workingDirectory: "/Users/alice/realagent-code/server",
170
+ });
171
+ assert.match(xml, /<key>Label<\/key>\s*<string>com\.rubytech\.realagent-code<\/string>/);
172
+ assert.match(xml, /<string>\/Users\/alice\/\.realagent-code\/launchd-wrapper\.sh<\/string>/);
173
+ assert.match(xml, /<key>WorkingDirectory<\/key>\s*<string>\/Users\/alice\/realagent-code\/server<\/string>/);
174
+ // Label collision guard — the maxy-code label must not appear in a realagent plist.
175
+ assert.ok(!/com\.rubytech\.maxy-code/.test(xml), "realagent plist must not carry maxy-code label");
176
+ });
131
177
  test("renderPlist omits WorkingDirectory key when workingDirectory is absent", () => {
132
178
  const xml = renderPlist({
133
179
  label: "com.rubytech.maxy",
@@ -0,0 +1,85 @@
1
+ // Static-grep contract for the darwin install branches in index.ts.
2
+ //
3
+ // Task 297 audited every `platform === "darwin"` block; this test pins the
4
+ // shape so a future refactor that removes a darwin branch without an explicit
5
+ // reason fails loudly here. The test reads src/index.ts at runtime (the
6
+ // authoritative source) and regex-asserts the per-branch markers — keeping
7
+ // the spec adjacent to the code rather than buried in a doc.
8
+ //
9
+ // Why static-grep, not behaviour: the install branches each spawn (sudo,
10
+ // brew, launchctl), and end-to-end darwin reproduction belongs to the manual
11
+ // smoke path in the task brief. The unit suite's job is to ensure the
12
+ // branch points still exist after a refactor.
13
+ import test from "node:test";
14
+ import assert from "node:assert/strict";
15
+ import { readFileSync } from "node:fs";
16
+ import { fileURLToPath } from "node:url";
17
+ import { dirname, resolve } from "node:path";
18
+ // dist/__tests__/macos-darwin-branch.test.js → ../../src/index.ts
19
+ const here = dirname(fileURLToPath(import.meta.url));
20
+ const INDEX_TS = resolve(here, "../../src/index.ts");
21
+ const SRC = readFileSync(INDEX_TS, "utf-8");
22
+ test("installSystemDeps has darwin branch that calls setMacosHostnameViaScutil on --hostname and scutil --get LocalHostName without", () => {
23
+ // The hostname block at index.ts:installSystemDeps must:
24
+ // - call setMacosHostnameViaScutil(HOSTNAME_FLAG) when HOSTNAME_FLAG is set
25
+ // - call scutil --get LocalHostName when HOSTNAME_FLAG is absent
26
+ // The two paths are the contract Task 297 locks in.
27
+ assert.match(SRC, /setMacosHostnameViaScutil\(HOSTNAME_FLAG\)/, "scutil-set path on --hostname must call setMacosHostnameViaScutil");
28
+ assert.match(SRC, /scutil["']?,\s*\[["']--get["']\s*,\s*["']LocalHostName["']\]/, "scutil --get LocalHostName must be the no-flag default on darwin");
29
+ assert.match(SRC, /darwin-hostname-mode=scutil-get/, "scutil-get success path must emit observability log");
30
+ assert.match(SRC, /darwin-hostname-mode=scutil-set/, "scutil-set path must emit observability log");
31
+ assert.match(SRC, /darwin-hostname-mode=brand-fallback/, "brand-fallback path must emit observability log");
32
+ });
33
+ test("installSystemDeps darwin branch installs DARWIN_BASE_DEPS via brew", () => {
34
+ // The shared dep set the darwin path feeds to installAllBrewPackages. If
35
+ // this list shrinks or a name changes, brew-resolve.test.ts (DARWIN_BASE_DEPS_FIXTURE)
36
+ // must move in lockstep — the comment in brew-resolve.test.ts says so.
37
+ assert.match(SRC, /DARWIN_BASE_DEPS\s*=\s*\[\s*"curl"\s*,\s*"git"\s*,\s*"unzip"\s*,\s*"jq"\s*,\s*"poppler"\s*,\s*"ffmpeg"\s*\]/);
38
+ assert.match(SRC, /installAllBrewPackages\(DARWIN_BASE_DEPS,\s*logFile\)/);
39
+ });
40
+ test("installCloudflared darwin branch logs the documented skip and brew-installs cloudflared", () => {
41
+ // Task 297 doctrine: skip tunnel provisioning entirely on darwin. The brew
42
+ // install puts the binary on PATH so the operator can opt in to `cloudflared
43
+ // tunnel login` post-install; the explicit log line is the diagnostic
44
+ // anchor for investigators grepping the install log.
45
+ assert.match(SRC, /darwin-cloudflare-skip=true/);
46
+ assert.match(SRC, /installAllBrewPackages\(\["cloudflared"\],\s*logFile\)/);
47
+ // No `cloudflared service install` or `cloudflared tunnel route dns`
48
+ // invocation in the installer surface — those are operator-driven,
49
+ // post-install steps. Match actual argv shapes (string-literal list to
50
+ // spawnSync/shell) rather than prose: argv arrays carry the args as
51
+ // quoted siblings inside `[...]`. Comment text is filtered by requiring
52
+ // a quote character immediately adjacent.
53
+ assert.ok(!/\[\s*["']cloudflared["'][^\]]*["']service["'][^\]]*["']install["']/.test(SRC), "installer must not invoke `cloudflared service install`");
54
+ assert.ok(!/\[\s*["']cloudflared["'][^\]]*["']tunnel["'][^\]]*["']route["'][^\]]*["']dns["']/.test(SRC), "installer must not invoke `cloudflared tunnel route dns`");
55
+ });
56
+ test("installService darwin branch delegates to installServiceDarwin (launchd path, not systemd)", () => {
57
+ // Two contracts: the platform switch routes darwin to installServiceDarwin,
58
+ // and installServiceDarwin writes the plist + bootstraps it via launchctl.
59
+ assert.match(SRC, /platform\s*===\s*"darwin"\s*\)\s*\{\s*installServiceDarwin\(\)/);
60
+ assert.match(SRC, /launchctl["']?,\s*\[["']bootstrap["']/);
61
+ assert.match(SRC, /launchd-plist=\$\{path\}\s+loaded=true/, "successful bootstrap must emit launchd-plist=… loaded=true");
62
+ assert.match(SRC, /launchd-plist=\$\{path\}\s+loaded=false/, "failed bootstrap must emit launchd-plist=… loaded=false");
63
+ });
64
+ test("installNodejs darwin branch brew-installs the `nodejs` alias name (resolver hops to node@22)", () => {
65
+ // The shared call site uses the apt-style `nodejs` name. brew-resolve.ts
66
+ // hops it to `node@22` via DARWIN_ALIASES — the alias is the deliberate
67
+ // wedge so a single dep name works across both backends.
68
+ assert.match(SRC, /platform\s*===\s*"darwin"[\s\S]{0,400}installAllBrewPackages\(\["nodejs"\]/);
69
+ });
70
+ test("installNeo4j darwin branch brew-installs `neo4j` (no alias hop)", () => {
71
+ // `neo4j` is the concrete brew formula name on darwin, no alias needed.
72
+ assert.match(SRC, /platform\s*===\s*"darwin"[\s\S]{0,800}installAllBrewPackages\(\["neo4j"\]/);
73
+ });
74
+ test("chromium binary-path file write skips darwin (Playwright-managed)", () => {
75
+ // writeChromiumBinaryPathFile early-returns on non-linux. The completion URL
76
+ // and VNC stack on darwin use Playwright/brew-managed Chromium instead of
77
+ // the linux per-brand chromium-binary.path file.
78
+ assert.match(SRC, /writeChromiumBinaryPathFile[\s\S]{0,200}process\.platform\s*!==\s*"linux"/);
79
+ });
80
+ test("uninstall darwin path bootsout the LaunchAgent and removes the plist", () => {
81
+ // The uninstall surface lives in uninstall.ts (separate file); this index.ts
82
+ // sanity check just confirms the launchdLabel helper still exists and is
83
+ // brand-keyed — install + uninstall both depend on the same label shape.
84
+ assert.match(SRC, /function launchdLabel\(\):\s*string\s*\{\s*return\s*`com\.rubytech\.\$\{BRAND\.hostname\}`/);
85
+ });
@@ -71,9 +71,14 @@ export function decideBrewInstallPlan(input) {
71
71
  if (p.installed) {
72
72
  const head = p.versions[0] ?? "?";
73
73
  logs.push(` [homebrew] ${p.resolvedFormula} already installed (v${head})`);
74
+ // Uniform-prefix mirror line so the install-log grep recipe in
75
+ // platform/plugins/docs/references/deployment.md (`grep -E '^\[create-maxy\] …'`)
76
+ // covers every brew step alongside platform=, darwin-hostname-mode=, etc.
77
+ logs.push(` [create-maxy] brew-formula=${p.resolvedFormula} action=present`);
74
78
  }
75
79
  else {
76
80
  logs.push(` [homebrew] installing ${p.resolvedFormula} …`);
81
+ logs.push(` [create-maxy] brew-formula=${p.resolvedFormula} action=install`);
77
82
  toInstall.push(p.resolvedFormula);
78
83
  }
79
84
  }
package/dist/index.js CHANGED
@@ -614,6 +614,7 @@ function installSystemDeps() {
614
614
  // otherwise preserve the existing system hostname.
615
615
  if (HOSTNAME_FLAG) {
616
616
  console.log(` Hostname: ${HOSTNAME_FLAG} (from --hostname flag)`);
617
+ logFile(` [create-maxy] darwin-hostname-mode=scutil-set value=${HOSTNAME_FLAG}`);
617
618
  try {
618
619
  setMacosHostnameViaScutil(HOSTNAME_FLAG);
619
620
  DEVICE_HOSTNAME = HOSTNAME_FLAG;
@@ -623,11 +624,25 @@ function installSystemDeps() {
623
624
  }
624
625
  }
625
626
  else {
626
- try {
627
- DEVICE_HOSTNAME = execFileSync("hostname", [], { encoding: "utf-8" }).trim();
628
- console.log(` Hostname: ${DEVICE_HOSTNAME} (preserved no --hostname flag on darwin)`);
627
+ // No flag — read the live LocalHostName so the printed URL matches mDNS
628
+ // resolution (`<LocalHostName>.local`). The `hostname` command on darwin
629
+ // returns the fully-qualified `<name>.local`, which would render the
630
+ // completion URL with a double `.local` suffix; scutil --get returns the
631
+ // bare token. No sudo, no system mutation.
632
+ const scutilGet = spawnSync("scutil", ["--get", "LocalHostName"], { encoding: "utf-8", stdio: "pipe", timeout: 5_000 });
633
+ const scutilName = ((scutilGet.stdout ?? "").trim());
634
+ if (scutilGet.status === 0 && scutilName.length > 0) {
635
+ DEVICE_HOSTNAME = scutilName;
636
+ console.log(` Hostname: ${DEVICE_HOSTNAME} (from scutil --get LocalHostName — no --hostname flag)`);
637
+ logFile(` [create-maxy] darwin-hostname-mode=scutil-get value=${DEVICE_HOSTNAME}`);
638
+ }
639
+ else {
640
+ // scutil failed or returned empty — fall back to BRAND.hostname so the
641
+ // completion URL at least matches the brand's documented .local name.
642
+ DEVICE_HOSTNAME = BRAND.hostname;
643
+ console.log(` Hostname: ${DEVICE_HOSTNAME} (brand fallback — scutil --get LocalHostName empty)`);
644
+ logFile(` [create-maxy] darwin-hostname-mode=brand-fallback value=${DEVICE_HOSTNAME} scutil-status=${scutilGet.status}`);
629
645
  }
630
- catch { /* fallback to brand — set at declaration */ }
631
646
  }
632
647
  // macOS has no apt analogue for the VNC/WiFi-AP stacks
633
648
  // (kiosk display + hostapd/dnsmasq are Pi-specific's
@@ -1541,8 +1556,11 @@ function installCloudflared() {
1541
1556
  if (platform === "darwin") {
1542
1557
  // Homebrew's `cloudflared` formula tracks the Cloudflare release stream;
1543
1558
  // tunnel-login flow stays unchanged (`feedback_no_api_token_route.md`).
1544
- // `cloudflared service install` is invoked by the launchd supervisor in
1545
- // out of scope here.
1559
+ // `cloudflared service install` and `cloudflared tunnel route dns` are not
1560
+ // invoked by the installer on darwin — public reach is operator-driven
1561
+ // post-install (`cloudflared tunnel login` + follow-up tooling). The brew
1562
+ // install puts the binary on PATH so the operator can opt in later.
1563
+ logFile(` [create-maxy] darwin-cloudflare-skip=true reason="Mac laptop — no public hostname provisioned at install time"`);
1546
1564
  installAllBrewPackages(["cloudflared"], logFile);
1547
1565
  return;
1548
1566
  }
@@ -2592,11 +2610,13 @@ function installServiceDarwin() {
2592
2610
  if (bootstrap.status === 0) {
2593
2611
  console.log(` [launchd] bootstrap ${gui()}/${label} ok`);
2594
2612
  logFile(` [launchd] bootstrap ${gui()}/${label} ok`);
2613
+ logFile(` [create-maxy] launchd-plist=${path} loaded=true`);
2595
2614
  }
2596
2615
  else {
2597
2616
  const stderr = (bootstrap.stderr ?? "").trim();
2598
2617
  console.error(` [launchd] bootstrap returned ${bootstrap.status}: ${stderr}`);
2599
2618
  logFile(` [launchd] bootstrap returned ${bootstrap.status}: ${stderr}`);
2619
+ logFile(` [create-maxy] launchd-plist=${path} loaded=false exit=${bootstrap.status}`);
2600
2620
  throw new Error(`launchctl bootstrap ${gui()} ${path} failed (exit ${bootstrap.status}): ${stderr}`);
2601
2621
  }
2602
2622
  // Wait for the server to come up.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rubytech/create-maxy-code",
3
- "version": "0.1.111",
3
+ "version": "0.1.113",
4
4
  "description": "Install Maxy — AI for Productive People",
5
5
  "bin": {
6
6
  "create-maxy-code": "./dist/index.js"
@@ -468,6 +468,111 @@ Removes the root-level systemd unit, `/etc/cloudflared/config.yml`, and the dupl
468
468
 
469
469
  ---
470
470
 
471
+ ## Multi-ingress: adding hostnames for prototype services
472
+
473
+ Once the brand tunnel is up (admin + public + optionally SSH/SMB), additional service hostnames can be added without re-running the OAuth/cert/create-tunnel steps. This is the pattern the venture-studio `prototype-host` skill uses to expose hosted landing pages and wedge prototypes as `https://<surface>.<BRAND_HOSTNAME>`.
474
+
475
+ The flow is: append an ingress rule to `${CFG_DIR}/config.yml` immediately before the final `http_status:404` catch-all, validate the YAML, create the DNS row, restart the brand cloudflared service.
476
+
477
+ ### 1. Append the ingress rule (awk, not sed)
478
+
479
+ `sed` cannot reliably insert before the last multi-line YAML rule. `awk` matches the literal catch-all line and inserts two lines before each match:
480
+
481
+ ```
482
+ SURFACE="<surface>" # DNS label, e.g. landing or prototype
483
+ PORT="<port>" # local port the dev server / file server binds to
484
+ HOSTNAME="${SURFACE}.${BRAND_HOSTNAME}"
485
+
486
+ # Idempotency — skip if this hostname is already in config.yml.
487
+ if grep -qE "^[[:space:]]*-[[:space:]]+hostname:[[:space:]]+${HOSTNAME}\b" "${CFG_DIR}/config.yml"; then
488
+ echo "ingress-row-exists hostname=${HOSTNAME}"
489
+ else
490
+ awk -v host="${HOSTNAME}" -v port="${PORT}" '
491
+ /^[[:space:]]*-[[:space:]]+service:[[:space:]]+http_status:404[[:space:]]*$/ {
492
+ print " - hostname: " host
493
+ print " service: http://localhost:" port
494
+ }
495
+ { print }
496
+ ' "${CFG_DIR}/config.yml" > "${CFG_DIR}/config.yml.new"
497
+ mv "${CFG_DIR}/config.yml.new" "${CFG_DIR}/config.yml"
498
+ fi
499
+ ```
500
+
501
+ After the edit the file looks like:
502
+
503
+ ```yaml
504
+ tunnel: <TUNNEL_ID>
505
+ credentials-file: <CFG_DIR>/<TUNNEL_ID>.json
506
+ ingress:
507
+ - hostname: admin.<BRAND_HOSTNAME>
508
+ service: http://localhost:<PORT>
509
+ - hostname: public.<BRAND_HOSTNAME>
510
+ service: http://localhost:<PORT>
511
+ - hostname: <surface>.<BRAND_HOSTNAME>
512
+ service: http://localhost:<port>
513
+ - service: http_status:404
514
+ ```
515
+
516
+ ### 2. Validate
517
+
518
+ ```
519
+ cloudflared --origincert "${CFG_DIR}/cert.pem" \
520
+ --config "${CFG_DIR}/config.yml" \
521
+ tunnel ingress validate
522
+ ```
523
+
524
+ Non-zero exit aborts the flow; cloudflared's stderr is the literal diagnostic.
525
+
526
+ ### 3. Create the DNS row
527
+
528
+ ```
529
+ TUNNEL_ID=$(jq -r '.id // .tunnelId' "${CFG_DIR}/tunnel.state")
530
+ cloudflared --origincert "${CFG_DIR}/cert.pem" \
531
+ tunnel route dns --overwrite-dns "${TUNNEL_ID}" "${HOSTNAME}"
532
+ ```
533
+
534
+ `--overwrite-dns` replaces any existing CNAME for the hostname (common after a prior install on the same account). Without it, `tunnel route dns` errors `record already exists`.
535
+
536
+ ### 4. Reload the connector
537
+
538
+ ```
539
+ systemctl --user restart "${BRAND}-cloudflared.service"
540
+ ```
541
+
542
+ Cloudflared does not watch `config.yml` for changes (the brand service unit launches it without `--config-watch`). Restart is the deterministic way to pick up the new rule; the brief ~1s connector blip is the same one every cloudflared upgrade incurs.
543
+
544
+ ### Verification
545
+
546
+ ```
547
+ curl -I https://<surface>.<BRAND_HOSTNAME>
548
+ ```
549
+
550
+ Anything other than `HTTP/2 200` (or the dev server's natural 200/304 response) means either the rule didn't land, the systemd unit serving `<port>` isn't running, or DNS hasn't propagated. Run `cloudflared --origincert "${CFG_DIR}/cert.pem" --config "${CFG_DIR}/config.yml" tunnel ingress validate` and `dig <surface>.<BRAND_HOSTNAME>` to isolate which.
551
+
552
+ ### Removing a prototype hostname
553
+
554
+ Manual for now (no scripted removal — the `prototype-host` skill does not yet ship a remove companion):
555
+
556
+ ```
557
+ # 1. Stop and disable the service unit.
558
+ systemctl --user disable --now "${BRAND}-prototype-<surface>.service"
559
+
560
+ # 2. Strip the ingress row from config.yml — match the literal hostname line plus the following service line.
561
+ awk -v host="${SURFACE}.${BRAND_HOSTNAME}" '
562
+ $0 ~ "^[[:space:]]*-[[:space:]]+hostname:[[:space:]]+" host "[[:space:]]*$" { skip = 2; next }
563
+ skip > 0 { skip--; next }
564
+ { print }
565
+ ' "${CFG_DIR}/config.yml" > "${CFG_DIR}/config.yml.new" && mv "${CFG_DIR}/config.yml.new" "${CFG_DIR}/config.yml"
566
+
567
+ # 3. Delete the DNS row (dashboard, since the cloudflared CLI has no "route dns remove").
568
+ # Cloudflare → DNS → <zone> → delete CNAME for <surface>.<BRAND_HOSTNAME>.
569
+
570
+ # 4. Restart cloudflared.
571
+ systemctl --user restart "${BRAND}-cloudflared.service"
572
+ ```
573
+
574
+ ---
575
+
471
576
  ## Full reset
472
577
 
473
578
  If everything is in a bad state and you want to start over. Assumes Step 0 has been run so `${BRAND}` and `${CFG_DIR}` are set.
@@ -9,15 +9,33 @@
9
9
 
10
10
  ## macOS install
11
11
 
12
- On macOS the installer uses Homebrew + launchd instead of apt + systemd, but the operator command is the same:
12
+ On macOS the installer uses Homebrew + launchd instead of apt + systemd. No flags are required on a laptop:
13
13
 
14
14
  ```bash
15
- npx -y @rubytech/create-maxy
15
+ npx -y @rubytech/create-maxy-code # default brand
16
+ npx -y @rubytech/create-realagent-code # realagent brand
16
17
  ```
17
18
 
18
- **Prerequisite:** Homebrew. If `brew` is missing, the installer refuses with `Homebrew not found. Install from https://brew.sh and re-run.` Install Homebrew once via the official one-liner, then re-run.
19
+ **Prerequisite:** Homebrew. If `brew` is missing, the installer refuses with `Homebrew not found. Install from https://brew.sh and re-run.` Install Homebrew once via the official one-liner, then re-run. The installer never installs Homebrew itself.
20
+
21
+ **Hostname / printed URL.** Without `--hostname`, the installer reads `scutil --get LocalHostName` and prints the completion URL as `http://<that-name>.local:<port>`. No sudo, no system change — your Mac's existing local network name is what mDNS will resolve. With `--hostname <h>`, the installer sets `HostName` / `LocalHostName` / `ComputerName` to `<h>` via `sudo scutil` (one password prompt, all-or-nothing rollback within the three-call batch) so the URL becomes `http://<h>.local:<port>`. Grep `~/.<brand>/logs/install-*.log` for `[create-maxy] darwin-hostname-mode=` to confirm which path ran (`scutil-get`, `scutil-set`, or `brand-fallback`).
22
+
23
+ **LaunchAgent.** The installer registers `{{productName}}` as a launchd LaunchAgent at `~/Library/LaunchAgents/com.rubytech.<brand-hostname>.plist` — for example `com.rubytech.maxy-code.plist`. Survives logout/login and reboot via `KeepAlive=true` and `RunAtLoad=true`. Two brands on the same Mac get two distinct plists (brand-hostname-keyed), so install order is independent. Use `launchctl print gui/$UID/com.rubytech.<brand-hostname>` for service state. The `[create-maxy] launchd-plist=<path> loaded=true` line in the install log confirms `launchctl bootstrap` accepted the plist; `loaded=false exit=<n>` is the failure signal (run `plutil -lint <path>` to diagnose).
24
+
25
+ **Cloudflare on darwin.** The installer brew-installs the `cloudflared` binary so it is on PATH, but does not invoke `cloudflared service install` or `cloudflared tunnel route dns` — public reach is opt-in. After install, the operator runs `cloudflared tunnel login` (browser-driven) followed by the existing tunnel-setup flow if they want a public address. Grep `[create-maxy] darwin-cloudflare-skip=true` in the install log to confirm the installer took the documented skip path.
26
+
27
+ **Uninstall.** `npx -y @rubytech/create-maxy-code uninstall` (or the realagent equivalent) bootsout the LaunchAgent, removes the plist, and deletes `~/.<brand>/`. Homebrew-installed dependencies (curl, git, unzip, jq, poppler, ffmpeg, node@22, neo4j, cloudflared) remain — remove them with `brew uninstall` if you want a clean slate.
28
+
29
+ **Pre-flight.** macOS < 14 is refused at pre-flight via `parseSwVers` (`sw_vers -productVersion` must be `≥ 14`).
30
+
31
+ **Diagnostic grep recipe.** After a Mac install, the canonical log path is `~/.<brand>/logs/install-*.log`. One pass tells you everything:
32
+
33
+ ```bash
34
+ grep -E '^\[create-maxy\] (platform|darwin-hostname-mode|darwin-cloudflare-skip|launchd-plist|init-logging FAILED)=' \
35
+ ~/.<brand>/logs/install-*.log
36
+ ```
19
37
 
20
- The installer registers {{productName}} as a launchd LaunchAgent at `~/Library/LaunchAgents/com.rubytech.maxy.plist` — survives logout/login and reboot. Use `launchctl print gui/$UID/com.rubytech.maxy` for service state. `--hostname <h>` sets HostName / LocalHostName / ComputerName via `sudo scutil`. macOS < 14 is refused at pre-flight.
38
+ Every successful Mac install contains, in order: `platform=darwin`, `darwin-hostname-mode=…`, `darwin-cloudflare-skip=true`, every brew install/verify line, `launchd-plist=… loaded=true`. Absence of any of these is the failure signal.
21
39
 
22
40
  ## Initial Setup
23
41
 
@@ -39,6 +39,7 @@ import { graphPruneDenylistRemove } from "./tools/graph-prune-denylist-remove.js
39
39
  import { conversationMemoryExpunge } from "./tools/conversation-memory-expunge.js";
40
40
  import { conversationArchiveDeriveInsights, } from "./tools/conversation-archive-derive-insights.js";
41
41
  import { conversationArchiveEnrichRejection } from "./tools/conversation-archive-enrich-rejection.js";
42
+ import { fetchImageAsContent } from "./tools/image-fetch.js";
42
43
  import { getSession, closeDriver } from "./lib/neo4j.js";
43
44
  import { embed } from "./lib/embeddings.js";
44
45
  import { notTrashed } from "../../../../lib/graph-trash/dist/index.js";
@@ -1882,6 +1883,40 @@ eagerTool(server, "conversation-search", "Search conversation history using sema
1882
1883
  };
1883
1884
  }
1884
1885
  });
1886
+ // image-fetch — return a remote image URL as a vision-capable content block.
1887
+ // Built for the listing-curator specialist's per-image vision pass; useful to
1888
+ // any caller that has an image URL in hand and a model that can see images.
1889
+ // Specialist tool, not in ADMIN_CORE_TOOLS — registered via server.tool, not
1890
+ // eagerTool (deferred-by-default ToolSearch tax is acceptable because the
1891
+ // curator is the only routine caller).
1892
+ server.tool("image-fetch", [
1893
+ "Fetch a remote image URL and return its bytes as an inline image content block the model can see.",
1894
+ "Guards: http/https only, content-type must start with image/, 10 MB byte cap, 30 s timeout.",
1895
+ "Returns one text block summarising the fetch plus one image block on success; one text block with isError on failure.",
1896
+ ].join(" "), {
1897
+ url: z.string().describe("Absolute http(s) URL of the image to fetch."),
1898
+ }, async ({ url }) => {
1899
+ const result = await fetchImageAsContent(url);
1900
+ if (!result.ok) {
1901
+ return {
1902
+ content: [{ type: "text", text: `image-fetch failed: ${result.reason}` }],
1903
+ isError: true,
1904
+ };
1905
+ }
1906
+ return {
1907
+ content: [
1908
+ {
1909
+ type: "text",
1910
+ text: `Fetched ${result.bytes} bytes (${result.mimeType}) from ${url}`,
1911
+ },
1912
+ {
1913
+ type: "image",
1914
+ data: result.base64,
1915
+ mimeType: result.mimeType,
1916
+ },
1917
+ ],
1918
+ };
1919
+ });
1885
1920
  // Cleanup on exit
1886
1921
  process.on("SIGINT", async () => {
1887
1922
  liveSchemaRuntime.cache.stop();