@rubytech/create-realagent-code 0.1.16 → 0.1.17

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 (122) hide show
  1. package/dist/__tests__/plugin-install.test.js +105 -0
  2. package/dist/index.js +133 -0
  3. package/dist/lib/plugin-install.js +108 -0
  4. package/package.json +1 -1
  5. package/payload/platform/config/brand.json +6 -1
  6. package/payload/platform/plugins/.claude-plugin/marketplace.json +103 -0
  7. package/payload/platform/plugins/admin/.claude-plugin/plugin.json +17 -0
  8. package/payload/platform/plugins/anthropic/.claude-plugin/plugin.json +8 -0
  9. package/payload/platform/plugins/business-assistant/.claude-plugin/plugin.json +8 -0
  10. package/payload/platform/plugins/cloudflare/.claude-plugin/plugin.json +17 -0
  11. package/payload/platform/plugins/contacts/.claude-plugin/plugin.json +17 -0
  12. package/payload/platform/plugins/deep-research/.claude-plugin/plugin.json +8 -0
  13. package/payload/platform/plugins/docs/.claude-plugin/plugin.json +8 -0
  14. package/payload/platform/plugins/docs/references/deployment.md +50 -0
  15. package/payload/platform/plugins/docs/references/platform.md +1 -1
  16. package/payload/platform/plugins/email/.claude-plugin/plugin.json +17 -0
  17. package/payload/platform/plugins/linkedin-import/.claude-plugin/plugin.json +8 -0
  18. package/payload/platform/plugins/memory/.claude-plugin/plugin.json +17 -0
  19. package/payload/platform/plugins/outlook/.claude-plugin/plugin.json +17 -0
  20. package/payload/platform/plugins/projects/.claude-plugin/plugin.json +8 -0
  21. package/payload/platform/plugins/replicate/.claude-plugin/plugin.json +17 -0
  22. package/payload/platform/plugins/sales/.claude-plugin/plugin.json +8 -0
  23. package/payload/platform/plugins/scheduling/.claude-plugin/plugin.json +17 -0
  24. package/payload/platform/plugins/tasks/.claude-plugin/plugin.json +13 -13
  25. package/payload/platform/plugins/waitlist/.claude-plugin/plugin.json +17 -0
  26. package/payload/platform/plugins/whatsapp/.claude-plugin/plugin.json +17 -0
  27. package/payload/platform/plugins/workflows/.claude-plugin/plugin.json +13 -12
  28. package/payload/platform/services/claude-session-manager/dist/http-server.d.ts.map +1 -1
  29. package/payload/platform/services/claude-session-manager/dist/http-server.js +13 -2
  30. package/payload/platform/services/claude-session-manager/dist/http-server.js.map +1 -1
  31. package/payload/platform/services/claude-session-manager/dist/pty-spawner.d.ts +6 -0
  32. package/payload/platform/services/claude-session-manager/dist/pty-spawner.d.ts.map +1 -1
  33. package/payload/platform/services/claude-session-manager/dist/pty-spawner.js +9 -3
  34. package/payload/platform/services/claude-session-manager/dist/pty-spawner.js.map +1 -1
  35. package/payload/premium-plugins/.claude-plugin/marketplace.json +18 -0
  36. package/payload/premium-plugins/real-agency/plugins/.claude-plugin/marketplace.json +63 -0
  37. package/payload/premium-plugins/real-agency/plugins/brochures/.claude-plugin/plugin.json +8 -0
  38. package/payload/premium-plugins/real-agency/plugins/buyers/.claude-plugin/plugin.json +8 -0
  39. package/payload/premium-plugins/real-agency/plugins/estate-business/.claude-plugin/plugin.json +8 -0
  40. package/payload/premium-plugins/real-agency/plugins/estate-coaching/.claude-plugin/plugin.json +8 -0
  41. package/payload/premium-plugins/real-agency/plugins/estate-onboarding/.claude-plugin/plugin.json +8 -0
  42. package/payload/premium-plugins/real-agency/plugins/estate-sales/.claude-plugin/plugin.json +8 -0
  43. package/payload/premium-plugins/real-agency/plugins/estate-teaching/.claude-plugin/plugin.json +8 -0
  44. package/payload/premium-plugins/real-agency/plugins/leads/.claude-plugin/plugin.json +8 -0
  45. package/payload/premium-plugins/real-agency/plugins/listings/.claude-plugin/plugin.json +8 -0
  46. package/payload/premium-plugins/real-agency/plugins/loop/.claude-plugin/plugin.json +17 -0
  47. package/payload/premium-plugins/real-agency/plugins/vendors/.claude-plugin/plugin.json +8 -0
  48. package/payload/premium-plugins/teaching/.claude-plugin/plugin.json +8 -0
  49. package/payload/premium-plugins/writer-craft/.claude-plugin/plugin.json +8 -0
  50. package/payload/server/public/assets/{Checkbox-YcNN7jq1.js → Checkbox-nrf4ISU0.js} +1 -1
  51. package/payload/server/public/assets/{admin-B1JZ0uth.js → admin-BctPxbjB.js} +53 -53
  52. package/payload/server/public/assets/{architectureDiagram-Q4EWVU46-96vgHxqG.js → architectureDiagram-Q4EWVU46-DZI73ap7.js} +1 -1
  53. package/payload/server/public/assets/{blockDiagram-DXYQGD6D-BdH2Cb_g.js → blockDiagram-DXYQGD6D-BQQz9YNZ.js} +1 -1
  54. package/payload/server/public/assets/{c4Diagram-AHTNJAMY-N93PeJeZ.js → c4Diagram-AHTNJAMY-gl8-XVLH.js} +1 -1
  55. package/payload/server/public/assets/channel-BIMyzzFT.js +1 -0
  56. package/payload/server/public/assets/{chunk-336JU56O--ZhC91-8.js → chunk-336JU56O-839etdKR.js} +2 -2
  57. package/payload/server/public/assets/{chunk-426QAEUC-NreYOhuj.js → chunk-426QAEUC-hyvaLE5p.js} +1 -1
  58. package/payload/server/public/assets/{chunk-4TB4RGXK-D8tdHM-B.js → chunk-4TB4RGXK-DEIYWdpd.js} +1 -1
  59. package/payload/server/public/assets/{chunk-5FUZZQ4R-0x7lCysy.js → chunk-5FUZZQ4R-zWevOyjc.js} +1 -1
  60. package/payload/server/public/assets/{chunk-5PVQY5BW-B8aFD0v2.js → chunk-5PVQY5BW-CL9KWSZH.js} +1 -1
  61. package/payload/server/public/assets/{chunk-EDXVE4YY-DcGxS2xv.js → chunk-EDXVE4YY-D91vGp64.js} +1 -1
  62. package/payload/server/public/assets/{chunk-ENJZ2VHE-CGa-VGZR.js → chunk-ENJZ2VHE-DApK4iuk.js} +1 -1
  63. package/payload/server/public/assets/{chunk-ICPOFSXX-jOlAtTf8.js → chunk-ICPOFSXX-BOjV-nTe.js} +1 -1
  64. package/payload/server/public/assets/{chunk-OYMX7WX6-DYDZSTbO.js → chunk-OYMX7WX6-D3Cv5Z_Z.js} +1 -1
  65. package/payload/server/public/assets/{chunk-U2HBQHQK-CZ-7SbAa.js → chunk-U2HBQHQK-C92E-iRU.js} +1 -1
  66. package/payload/server/public/assets/{chunk-X2U36JSP-DE9wpb9Q.js → chunk-X2U36JSP-DohG6qWK.js} +1 -1
  67. package/payload/server/public/assets/{chunk-YZCP3GAM-B6klGA8S.js → chunk-YZCP3GAM-CyeLVSjf.js} +1 -1
  68. package/payload/server/public/assets/{chunk-ZZ45TVLE-BtC_su1V.js → chunk-ZZ45TVLE-D7R-lONY.js} +1 -1
  69. package/payload/server/public/assets/classDiagram-6PBFFD2Q-BAfXNwa9.js +1 -0
  70. package/payload/server/public/assets/classDiagram-v2-HSJHXN6E-D5bE1GaW.js +1 -0
  71. package/payload/server/public/assets/clone-DOH_suVb.js +1 -0
  72. package/payload/server/public/assets/{dagre-DJ6RkHVs.js → dagre-DjhovTZd.js} +1 -1
  73. package/payload/server/public/assets/{dagre-KV5264BT-BL-5O-QR.js → dagre-KV5264BT-DFeRlQuy.js} +1 -1
  74. package/payload/server/public/assets/data-DzTaKq-r.js +1 -0
  75. package/payload/server/public/assets/{device-url-actions-BN_tQerV.js → device-url-actions-CE3A1UDw.js} +1 -1
  76. package/payload/server/public/assets/{diagram-5BDNPKRD-tmZhEAU-.js → diagram-5BDNPKRD-DsmRGnVL.js} +1 -1
  77. package/payload/server/public/assets/{diagram-G4DWMVQ6-Cm8Qa00o.js → diagram-G4DWMVQ6-CVSK-mLR.js} +1 -1
  78. package/payload/server/public/assets/{diagram-MMDJMWI5-gpFqrqSd.js → diagram-MMDJMWI5-DMN94Pe-.js} +1 -1
  79. package/payload/server/public/assets/{diagram-TYMM5635-Dhui1_oR.js → diagram-TYMM5635-DaD4mLMc.js} +1 -1
  80. package/payload/server/public/assets/{erDiagram-SMLLAGMA-DVwK_CfW.js → erDiagram-SMLLAGMA-BUkZ2Iq1.js} +1 -1
  81. package/payload/server/public/assets/{flowDiagram-DWJPFMVM-BElR60Ob.js → flowDiagram-DWJPFMVM-DW8DX_ge.js} +1 -1
  82. package/payload/server/public/assets/{ganttDiagram-T4ZO3ILL-8NGZhLA3.js → ganttDiagram-T4ZO3ILL-DWaRL6__.js} +1 -1
  83. package/payload/server/public/assets/{gitGraphDiagram-UUTBAWPF-DGVw9ymM.js → gitGraphDiagram-UUTBAWPF-BaPTFtVx.js} +1 -1
  84. package/payload/server/public/assets/graph-dgoq2zvY.js +1 -0
  85. package/payload/server/public/assets/{graph-labels-piCPPdFq.js → graph-labels-BRtJE9AE.js} +1 -1
  86. package/payload/server/public/assets/{graphlib-uE-sp1ee.js → graphlib-BUhb3hPU.js} +1 -1
  87. package/payload/server/public/assets/{infoDiagram-42DDH7IO-B-Vuz8L0.js → infoDiagram-42DDH7IO-Ch6CE3GO.js} +1 -1
  88. package/payload/server/public/assets/{ishikawaDiagram-UXIWVN3A-DlcYgX27.js → ishikawaDiagram-UXIWVN3A-DApFr2KO.js} +1 -1
  89. package/payload/server/public/assets/{journeyDiagram-VCZTEJTY-C-19XR7D.js → journeyDiagram-VCZTEJTY-D72xl-VA.js} +1 -1
  90. package/payload/server/public/assets/jsx-runtime-BgXAk35j.css +1 -0
  91. package/payload/server/public/assets/{kanban-definition-6JOO6SKY-CEsBB-l2.js → kanban-definition-6JOO6SKY-TuAvkCJU.js} +1 -1
  92. package/payload/server/public/assets/{line-B_WJJGNO.js → line-Cr3lHgh8.js} +1 -1
  93. package/payload/server/public/assets/{mermaid-parser.core-DHnCk5T5.js → mermaid-parser.core-BvbEd4_6.js} +1 -1
  94. package/payload/server/public/assets/{mermaid.core-DhZqC4uG.js → mermaid.core-CSIEcw1L.js} +3 -3
  95. package/payload/server/public/assets/{mindmap-definition-QFDTVHPH-B2tHowcI.js → mindmap-definition-QFDTVHPH-CNLvqgk-.js} +1 -1
  96. package/payload/server/public/assets/{page-CjxybpTy.js → page-B_80xGrM.js} +1 -1
  97. package/payload/server/public/assets/{page-CBA4FUGo.js → page-BcnqM490.js} +1 -1
  98. package/payload/server/public/assets/{pieDiagram-DEJITSTG-xopsRxKN.js → pieDiagram-DEJITSTG-CSOsdFn6.js} +1 -1
  99. package/payload/server/public/assets/{public-BBqroGXK.js → public-DGrCAqZN.js} +3 -3
  100. package/payload/server/public/assets/{quadrantDiagram-34T5L4WZ-B19ZbtY0.js → quadrantDiagram-34T5L4WZ-Bsxz9S58.js} +1 -1
  101. package/payload/server/public/assets/{requirementDiagram-MS252O5E-BMcM9iam.js → requirementDiagram-MS252O5E-BZRrlQlh.js} +1 -1
  102. package/payload/server/public/assets/{sankeyDiagram-XADWPNL6-DW5Hs7Av.js → sankeyDiagram-XADWPNL6-C-W-Az7g.js} +1 -1
  103. package/payload/server/public/assets/{sequenceDiagram-FGHM5R23-Cn7z1Q3s.js → sequenceDiagram-FGHM5R23-DCZPAj4-.js} +1 -1
  104. package/payload/server/public/assets/{stateDiagram-FHFEXIEX-DR7At7js.js → stateDiagram-FHFEXIEX-C3wODMGb.js} +1 -1
  105. package/payload/server/public/assets/stateDiagram-v2-QKLJ7IA2-Ch6qbhpm.js +1 -0
  106. package/payload/server/public/assets/{timeline-definition-GMOUNBTQ-Bm8JLRpB.js → timeline-definition-GMOUNBTQ-TRzSKinM.js} +1 -1
  107. package/payload/server/public/assets/{vennDiagram-DHZGUBPP-6XqALjnd.js → vennDiagram-DHZGUBPP-DSKZVM8N.js} +1 -1
  108. package/payload/server/public/assets/{wardleyDiagram-NUSXRM2D-DhdYZjeI.js → wardleyDiagram-NUSXRM2D-BRXL08eb.js} +1 -1
  109. package/payload/server/public/assets/{xychartDiagram-5P7HB3ND-CBmEo9Oi.js → xychartDiagram-5P7HB3ND-CgWMOf0l.js} +1 -1
  110. package/payload/server/public/data.html +5 -5
  111. package/payload/server/public/graph.html +6 -6
  112. package/payload/server/public/index.html +8 -8
  113. package/payload/server/public/public.html +5 -5
  114. package/payload/server/public/assets/channel-TAr3X5oz.js +0 -1
  115. package/payload/server/public/assets/classDiagram-6PBFFD2Q-DjmusLvy.js +0 -1
  116. package/payload/server/public/assets/classDiagram-v2-HSJHXN6E-TIJEjUyl.js +0 -1
  117. package/payload/server/public/assets/clone-O8K8fxhJ.js +0 -1
  118. package/payload/server/public/assets/data-DcMPzQFt.js +0 -1
  119. package/payload/server/public/assets/graph-oLjrPnCQ.js +0 -1
  120. package/payload/server/public/assets/jsx-runtime-jZDyLD_2.css +0 -1
  121. package/payload/server/public/assets/stateDiagram-v2-QKLJ7IA2-CvqZ4uTP.js +0 -1
  122. /package/payload/server/public/assets/{jsx-runtime-HrrfyPcI.js → jsx-runtime-Bz7aoCi7.js} +0 -0
@@ -0,0 +1,105 @@
1
+ // Unit tests for the installer's plugin-install pure functions.
2
+ // Tests are ephemeral per the sprint workflow — they prove behaviour
3
+ // during the sprint; the installer's runtime exercise on a real Pi is the
4
+ // permanent verification artifact.
5
+ import test from "node:test";
6
+ import assert from "node:assert/strict";
7
+ import { parsePluginList, computeInstallActions, computeConfigureActions, parseExternalPlugins, } from "../lib/plugin-install.js";
8
+ test("parsePluginList extracts name@marketplace tuples", () => {
9
+ const stdout = `
10
+ ❯ tasks@maxy-platform
11
+ Version: 0.1.0
12
+ Scope: local
13
+
14
+ ❯ telegram@claude-plugins-official
15
+ Version: 1.2.3
16
+ `;
17
+ const out = parsePluginList(stdout);
18
+ assert.deepEqual(out, [
19
+ { name: "tasks", marketplace: "maxy-platform" },
20
+ { name: "telegram", marketplace: "claude-plugins-official" },
21
+ ]);
22
+ });
23
+ test("parsePluginList returns empty array for empty stdout", () => {
24
+ assert.deepEqual(parsePluginList(""), []);
25
+ assert.deepEqual(parsePluginList("No plugins installed.\n"), []);
26
+ });
27
+ test("computeInstallActions: every desired plugin needs installing when none present", () => {
28
+ const desired = [
29
+ { name: "admin", marketplace: "maxy-platform" },
30
+ { name: "telegram", marketplace: "claude-plugins-official" },
31
+ ];
32
+ const { toInstall, alreadyInstalled } = computeInstallActions(desired, []);
33
+ assert.equal(toInstall.length, 2);
34
+ assert.equal(alreadyInstalled.length, 0);
35
+ });
36
+ test("computeInstallActions: idempotent skip for exact name@marketplace match", () => {
37
+ const desired = [
38
+ { name: "admin", marketplace: "maxy-platform" },
39
+ { name: "telegram", marketplace: "claude-plugins-official" },
40
+ ];
41
+ const installed = [
42
+ { name: "admin", marketplace: "maxy-platform" },
43
+ ];
44
+ const { toInstall, alreadyInstalled } = computeInstallActions(desired, installed);
45
+ assert.deepEqual(toInstall.map(p => p.name), ["telegram"]);
46
+ assert.deepEqual(alreadyInstalled.map(p => p.name), ["admin"]);
47
+ });
48
+ test("computeInstallActions: name-only match skips install (legacy install state)", () => {
49
+ const desired = [{ name: "admin", marketplace: "maxy-platform" }];
50
+ const installed = [{ name: "admin", marketplace: "" }];
51
+ const { toInstall, alreadyInstalled } = computeInstallActions(desired, installed);
52
+ assert.equal(toInstall.length, 0);
53
+ assert.equal(alreadyInstalled.length, 1);
54
+ });
55
+ test("computeConfigureActions: skip when no configureSecret declared", () => {
56
+ const installed = [{ name: "admin", marketplace: "maxy-platform" }];
57
+ const actions = computeConfigureActions(installed, {});
58
+ assert.equal(actions.length, 1);
59
+ assert.equal(actions[0].kind, "skip-no-secret-name");
60
+ });
61
+ test("computeConfigureActions: skip when env var absent", () => {
62
+ const installed = [{
63
+ name: "telegram",
64
+ marketplace: "claude-plugins-official",
65
+ configureSecret: "TELEGRAM_BOT_TOKEN",
66
+ }];
67
+ const actions = computeConfigureActions(installed, {});
68
+ assert.equal(actions.length, 1);
69
+ assert.equal(actions[0].kind, "skip-no-secret-value");
70
+ });
71
+ test("computeConfigureActions: configure when env var present", () => {
72
+ const installed = [{
73
+ name: "telegram",
74
+ marketplace: "claude-plugins-official",
75
+ configureSecret: "TELEGRAM_BOT_TOKEN",
76
+ }];
77
+ const actions = computeConfigureActions(installed, { TELEGRAM_BOT_TOKEN: "abc123" });
78
+ assert.equal(actions.length, 1);
79
+ assert.equal(actions[0].kind, "configure");
80
+ if (actions[0].kind === "configure") {
81
+ assert.equal(actions[0].secretValue, "abc123");
82
+ assert.equal(actions[0].plugin.name, "telegram");
83
+ }
84
+ });
85
+ test("parseExternalPlugins: undefined returns []", () => {
86
+ assert.deepEqual(parseExternalPlugins(undefined), []);
87
+ assert.deepEqual(parseExternalPlugins(null), []);
88
+ });
89
+ test("parseExternalPlugins: well-formed array round-trips", () => {
90
+ const raw = [
91
+ { name: "telegram", marketplace: "claude-plugins-official", configureSecret: "TELEGRAM_BOT_TOKEN", channelPlugin: true },
92
+ { name: "discord", marketplace: "claude-plugins-official" },
93
+ ];
94
+ const out = parseExternalPlugins(raw);
95
+ assert.deepEqual(out, [
96
+ { name: "telegram", marketplace: "claude-plugins-official", configureSecret: "TELEGRAM_BOT_TOKEN", channelPlugin: true },
97
+ { name: "discord", marketplace: "claude-plugins-official" },
98
+ ]);
99
+ });
100
+ test("parseExternalPlugins: missing marketplace throws", () => {
101
+ assert.throws(() => parseExternalPlugins([{ name: "telegram" }]));
102
+ });
103
+ test("parseExternalPlugins: missing name throws", () => {
104
+ assert.throws(() => parseExternalPlugins([{ marketplace: "x" }]));
105
+ });
package/dist/index.js CHANGED
@@ -12,6 +12,7 @@ import { installAllBrewPackages } from "./brew-install.js";
12
12
  import { parseSwVers, isSupportedMacosVersion } from "./macos-version.js";
13
13
  import { decideChromiumAction, isSnapConfinedPath } from "./snap-chromium.js";
14
14
  import { classifyPortHolder } from "./preflight-port-classifier.js";
15
+ import { parsePluginList, computeInstallActions, computeConfigureActions, parseExternalPlugins, } from "./lib/plugin-install.js";
15
16
  const PAYLOAD_DIR = resolve(import.meta.dirname, "../payload");
16
17
  // Brand manifest — read from payload to derive all brand-specific installation values.
17
18
  // The bundler stamps brand.json into the payload at build time.
@@ -1875,6 +1876,137 @@ function deployPayload() {
1875
1876
  writeFileSync(join(configDir, `.${BRAND.hostname}-version`), PKG_VERSION, "utf-8");
1876
1877
  console.log(` Deployed to ${INSTALL_DIR}`);
1877
1878
  }
1879
+ // Task 030 — register the local + external Claude Code plugins after the
1880
+ // payload is on disk. The bundler stamps `.claude-plugin/marketplace.json`
1881
+ // at each plugin-tree root (platform/plugins/, premium-plugins/real-agency/
1882
+ // plugins/, premium-plugins/) so this function discovers them generically
1883
+ // from the install directory rather than hardcoding names.
1884
+ //
1885
+ // Idempotent on every step: marketplace add is skipped when already in
1886
+ // `marketplace list`; `plugin install` is skipped when the plugin is
1887
+ // already in `plugin list` (matched by `<name>@<marketplace>` or by name
1888
+ // alone for legacy install state). One `[plugin-install] <name>@<src>
1889
+ // idempotent=<bool>` log line per attempt; failures log
1890
+ // `[plugin-install] ERROR <name>@<src> exit=<n> stderr=<short>` and do
1891
+ // NOT abort — one plugin failing must not block the rest, mirroring the
1892
+ // marketplace-add behaviour at src/index.ts:1018-1019.
1893
+ function registerLocalAndExternalPlugins() {
1894
+ console.log(" Registering local + external Claude Code plugins...");
1895
+ const localTrees = [];
1896
+ // The three known marketplace.json locations the bundler emits. Missing
1897
+ // ones are silently skipped (e.g. a brand that ships no premium plugins).
1898
+ const candidates = [
1899
+ join(INSTALL_DIR, "platform", "plugins"),
1900
+ join(INSTALL_DIR, "premium-plugins", "real-agency", "plugins"),
1901
+ join(INSTALL_DIR, "premium-plugins"),
1902
+ ];
1903
+ for (const dir of candidates) {
1904
+ const mkPath = join(dir, ".claude-plugin", "marketplace.json");
1905
+ if (!existsSync(mkPath))
1906
+ continue;
1907
+ try {
1908
+ const parsed = JSON.parse(readFileSync(mkPath, "utf-8"));
1909
+ if (typeof parsed.name === "string" && parsed.name.length > 0) {
1910
+ localTrees.push({ name: parsed.name, dir });
1911
+ }
1912
+ else {
1913
+ logFile(`[plugin-marketplace] ERROR malformed marketplace at ${mkPath} reason=no-name`);
1914
+ }
1915
+ }
1916
+ catch (err) {
1917
+ const msg = err instanceof Error ? err.message : String(err);
1918
+ logFile(`[plugin-marketplace] ERROR parse ${mkPath} error=${JSON.stringify(msg.slice(0, 200))}`);
1919
+ }
1920
+ }
1921
+ // Add each local marketplace. Idempotence via `marketplace list` grep.
1922
+ const mkList = spawnSync("claude", ["plugin", "marketplace", "list"], { stdio: "pipe", encoding: "utf-8" });
1923
+ const mkListed = mkList.stdout ?? "";
1924
+ for (const { name, dir } of localTrees) {
1925
+ if (mkListed.includes(name)) {
1926
+ logFile(`[plugin-marketplace] added ${name} idempotent=true`);
1927
+ continue;
1928
+ }
1929
+ const add = spawnSync("claude", ["plugin", "marketplace", "add", dir], { stdio: "pipe", encoding: "utf-8", timeout: 60_000 });
1930
+ if (add.status === 0) {
1931
+ logFile(`[plugin-marketplace] added ${name} source=${dir} idempotent=false`);
1932
+ }
1933
+ else {
1934
+ const stderrShort = (add.stderr ?? "").split("\n")[0]?.slice(0, 200) ?? "";
1935
+ logFile(`[plugin-marketplace] ERROR add ${name} source=${dir} exit=${add.status} stderr=${JSON.stringify(stderrShort)}`);
1936
+ }
1937
+ }
1938
+ // Build the desired plugin list = (every local marketplace's plugins) +
1939
+ // (brand.json#externalPlugins). The local entries' `marketplace` field
1940
+ // matches the marketplace.json `name`; the external entries declare
1941
+ // their own.
1942
+ const desired = [];
1943
+ for (const { name: mkName, dir } of localTrees) {
1944
+ const mkPath = join(dir, ".claude-plugin", "marketplace.json");
1945
+ try {
1946
+ const parsed = JSON.parse(readFileSync(mkPath, "utf-8"));
1947
+ for (const p of parsed.plugins ?? []) {
1948
+ if (typeof p.name === "string" && p.name.length > 0) {
1949
+ desired.push({ name: p.name, marketplace: mkName });
1950
+ }
1951
+ }
1952
+ }
1953
+ catch {
1954
+ // Already logged above.
1955
+ }
1956
+ }
1957
+ let externals = [];
1958
+ try {
1959
+ externals = parseExternalPlugins(BRAND.externalPlugins);
1960
+ }
1961
+ catch (err) {
1962
+ const msg = err instanceof Error ? err.message : String(err);
1963
+ logFile(`[plugin-install] ERROR brand.externalPlugins parse error=${JSON.stringify(msg.slice(0, 200))}`);
1964
+ }
1965
+ desired.push(...externals);
1966
+ // Snapshot what's installed to compute the install set.
1967
+ const pluginList = spawnSync("claude", ["plugin", "list"], { stdio: "pipe", encoding: "utf-8" });
1968
+ const installed = parsePluginList(pluginList.stdout ?? "");
1969
+ const { toInstall, alreadyInstalled } = computeInstallActions(desired, installed);
1970
+ for (const ref of alreadyInstalled) {
1971
+ logFile(`[plugin-install] ${ref.name}@${ref.marketplace} idempotent=true`);
1972
+ }
1973
+ for (const ref of toInstall) {
1974
+ const install = spawnSync("claude", ["plugin", "install", `${ref.name}@${ref.marketplace}`, "--scope", "user"], { stdio: "pipe", encoding: "utf-8", timeout: 120_000 });
1975
+ if (install.status === 0) {
1976
+ logFile(`[plugin-install] ${ref.name}@${ref.marketplace} idempotent=false`);
1977
+ }
1978
+ else {
1979
+ const stderrShort = (install.stderr ?? "").split("\n")[0]?.slice(0, 200) ?? "";
1980
+ logFile(`[plugin-install] ERROR ${ref.name}@${ref.marketplace} exit=${install.status} stderr=${JSON.stringify(stderrShort)}`);
1981
+ }
1982
+ }
1983
+ // Configure-token pass — only the externals carry configureSecret. For
1984
+ // each, pipe `/<name>:configure <secret>` into a one-shot `claude`
1985
+ // invocation. Missing env vars log SKIP and continue — pairing is a
1986
+ // manual per-operator step per the channels-reference docs.
1987
+ const configureActions = computeConfigureActions(externals, process.env);
1988
+ for (const action of configureActions) {
1989
+ if (action.kind === "skip-no-secret-name") {
1990
+ // Local plugins and externals without configureSecret — nothing to do.
1991
+ continue;
1992
+ }
1993
+ if (action.kind === "skip-no-secret-value") {
1994
+ logFile(`[plugin-configure] SKIP ${action.plugin.name} reason=no-secret-in-env env-var=${action.plugin.configureSecret}`);
1995
+ continue;
1996
+ }
1997
+ // The piped-stdin form: `echo '/<name>:configure <secret>' | claude --print`
1998
+ // is the documented one-shot configure path. We do not log the secret value.
1999
+ const cmd = `/${action.plugin.name}:configure ${action.secretValue}`;
2000
+ const configure = spawnSync("claude", ["--print", "--input-format", "text"], { input: cmd, stdio: ["pipe", "pipe", "pipe"], encoding: "utf-8", timeout: 60_000 });
2001
+ if (configure.status === 0) {
2002
+ logFile(`[plugin-configure] ${action.plugin.name} ok env-var=${action.plugin.configureSecret}`);
2003
+ }
2004
+ else {
2005
+ const stderrShort = (configure.stderr ?? "").split("\n")[0]?.slice(0, 200) ?? "";
2006
+ logFile(`[plugin-configure] ERROR ${action.plugin.name} exit=${configure.status} stderr=${JSON.stringify(stderrShort)}`);
2007
+ }
2008
+ }
2009
+ }
1878
2010
  function buildPlatform() {
1879
2011
  log("9", TOTAL, "Installing dependencies and building...");
1880
2012
  console.log(` Installing platform dependencies (${join(INSTALL_DIR, "platform")})...`);
@@ -3336,6 +3468,7 @@ try {
3336
3468
  ensureNeo4jPassword(); // Now config/.neo4j-password is available if it existed before
3337
3469
  provisionRemoteSessionSecret(); // Task 653: shared HMAC key readable by maxy-edge + maxy-ui
3338
3470
  buildPlatform();
3471
+ registerLocalAndExternalPlugins(); // Task 030: install-time plugin registration
3339
3472
  setupVncViewer();
3340
3473
  setupAccount();
3341
3474
  installTunnelScripts(); // ~/setup-tunnel.sh, ~/reset-tunnel.sh — the SKILL contract
@@ -0,0 +1,108 @@
1
+ // Pure-function support for the installer's plugin-install loop. The runtime
2
+ // callers in src/index.ts wrap these with the actual `spawnSync` / fs reads;
3
+ // keeping the logic side-effect-free here makes the install / configure
4
+ // branches unit-testable without stubbing every shell invocation.
5
+ /**
6
+ * `claude plugin list` prints rows like:
7
+ * ❯ tasks@maxy-platform
8
+ * Version: 0.1.0
9
+ * Scope: local
10
+ *
11
+ * Parse the `<name>@<marketplace>` line so the installer can skip already-
12
+ * installed plugins. Plugins whose row lacks the `@` suffix (legacy
13
+ * installer state) are returned with marketplace='' so a name-only check
14
+ * still de-dupes.
15
+ */
16
+ export function parsePluginList(stdout) {
17
+ const out = [];
18
+ for (const raw of stdout.split("\n")) {
19
+ const line = raw.replace(/^\s*❯\s+/, "").trim();
20
+ if (!line || !/^[a-z0-9][a-z0-9-]*(@[a-zA-Z0-9-]+)?$/.test(line))
21
+ continue;
22
+ const atIdx = line.indexOf("@");
23
+ if (atIdx < 0)
24
+ out.push({ name: line, marketplace: "" });
25
+ else
26
+ out.push({ name: line.slice(0, atIdx), marketplace: line.slice(atIdx + 1) });
27
+ }
28
+ return out;
29
+ }
30
+ /**
31
+ * Given the desired plugins and what's already installed, return the
32
+ * subset that still needs `claude plugin install <name>@<marketplace>`.
33
+ * Idempotence rule: a plugin is "already installed" if either its
34
+ * exact `<name>@<marketplace>` is in the installed set, OR a row exists
35
+ * with the same name and any marketplace (the spawn would re-attach in
36
+ * the operator's chosen scope). This mirrors the marketplace-add
37
+ * idempotence used in src/index.ts:1012.
38
+ */
39
+ export function computeInstallActions(desired, installed) {
40
+ const byName = new Map();
41
+ for (const i of installed) {
42
+ if (!byName.has(i.name))
43
+ byName.set(i.name, new Set());
44
+ byName.get(i.name).add(i.marketplace);
45
+ }
46
+ const toInstall = [];
47
+ const alreadyInstalled = [];
48
+ for (const d of desired) {
49
+ const marketplaces = byName.get(d.name);
50
+ if (marketplaces && (marketplaces.has(d.marketplace) || marketplaces.has(""))) {
51
+ alreadyInstalled.push(d);
52
+ }
53
+ else {
54
+ toInstall.push(d);
55
+ }
56
+ }
57
+ return { toInstall, alreadyInstalled };
58
+ }
59
+ export function computeConfigureActions(installed, env) {
60
+ const out = [];
61
+ for (const p of installed) {
62
+ if (!p.configureSecret) {
63
+ out.push({ kind: "skip-no-secret-name", plugin: p });
64
+ continue;
65
+ }
66
+ const value = env[p.configureSecret];
67
+ if (typeof value !== "string" || value.length === 0) {
68
+ out.push({ kind: "skip-no-secret-value", plugin: p });
69
+ continue;
70
+ }
71
+ out.push({ kind: "configure", plugin: p, secretValue: value });
72
+ }
73
+ return out;
74
+ }
75
+ /** Brand-config validator. brand.json#externalPlugins is optional; when
76
+ * present, each entry must declare name + marketplace as non-empty
77
+ * strings. Other fields are optional. Returns a normalised array; throws
78
+ * on any malformed entry (loud failure at install time beats silent
79
+ * drop). */
80
+ export function parseExternalPlugins(raw) {
81
+ if (raw === undefined || raw === null)
82
+ return [];
83
+ if (!Array.isArray(raw)) {
84
+ throw new Error("brand.json#externalPlugins must be an array");
85
+ }
86
+ const out = [];
87
+ for (const entry of raw) {
88
+ if (!entry || typeof entry !== "object") {
89
+ throw new Error("brand.json#externalPlugins entries must be objects");
90
+ }
91
+ const e = entry;
92
+ if (typeof e.name !== "string" || e.name.length === 0) {
93
+ throw new Error("brand.json#externalPlugins entry missing 'name'");
94
+ }
95
+ if (typeof e.marketplace !== "string" || e.marketplace.length === 0) {
96
+ throw new Error(`brand.json#externalPlugins[${e.name}] missing 'marketplace'`);
97
+ }
98
+ const ref = { name: e.name, marketplace: e.marketplace };
99
+ if (typeof e.configureSecret === "string" && e.configureSecret.length > 0) {
100
+ ref.configureSecret = e.configureSecret;
101
+ }
102
+ if (e.channelPlugin === true) {
103
+ ref.channelPlugin = true;
104
+ }
105
+ out.push(ref);
106
+ }
107
+ return out;
108
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rubytech/create-realagent-code",
3
- "version": "0.1.16",
3
+ "version": "0.1.17",
4
4
  "description": "Install Real Agent — Built for agents. By agents.",
5
5
  "bin": {
6
6
  "create-realagent-code": "./dist/index.js"
@@ -51,5 +51,10 @@
51
51
  "defaultEnabled": ["sales"],
52
52
  "available": ["deep-research", "waitlist", "projects", "whatsapp", "replicate", "linkedin-import"],
53
53
  "excluded": ["telegram"]
54
- }
54
+ },
55
+
56
+ "externalPlugins": [
57
+ { "name": "discord", "marketplace": "claude-plugins-official", "configureSecret": "DISCORD_BOT_TOKEN", "channelPlugin": true },
58
+ { "name": "imessage", "marketplace": "claude-plugins-official", "channelPlugin": true }
59
+ ]
55
60
  }
@@ -0,0 +1,103 @@
1
+ {
2
+ "name": "maxy-platform",
3
+ "owner": {
4
+ "name": "Rubytech LLC"
5
+ },
6
+ "plugins": [
7
+ {
8
+ "name": "admin",
9
+ "source": "./admin",
10
+ "version": "0.1.0"
11
+ },
12
+ {
13
+ "name": "anthropic",
14
+ "source": "./anthropic",
15
+ "version": "0.1.0"
16
+ },
17
+ {
18
+ "name": "business-assistant",
19
+ "source": "./business-assistant",
20
+ "version": "0.1.0"
21
+ },
22
+ {
23
+ "name": "cloudflare",
24
+ "source": "./cloudflare",
25
+ "version": "0.1.0"
26
+ },
27
+ {
28
+ "name": "contacts",
29
+ "source": "./contacts",
30
+ "version": "0.1.0"
31
+ },
32
+ {
33
+ "name": "deep-research",
34
+ "source": "./deep-research",
35
+ "version": "0.1.0"
36
+ },
37
+ {
38
+ "name": "docs",
39
+ "source": "./docs",
40
+ "version": "0.1.0"
41
+ },
42
+ {
43
+ "name": "email",
44
+ "source": "./email",
45
+ "version": "0.1.0"
46
+ },
47
+ {
48
+ "name": "linkedin-import",
49
+ "source": "./linkedin-import",
50
+ "version": "0.1.0"
51
+ },
52
+ {
53
+ "name": "memory",
54
+ "source": "./memory",
55
+ "version": "0.1.0"
56
+ },
57
+ {
58
+ "name": "outlook",
59
+ "source": "./outlook",
60
+ "version": "0.1.0"
61
+ },
62
+ {
63
+ "name": "projects",
64
+ "source": "./projects",
65
+ "version": "0.1.0"
66
+ },
67
+ {
68
+ "name": "replicate",
69
+ "source": "./replicate",
70
+ "version": "0.1.0"
71
+ },
72
+ {
73
+ "name": "sales",
74
+ "source": "./sales",
75
+ "version": "0.1.0"
76
+ },
77
+ {
78
+ "name": "scheduling",
79
+ "source": "./scheduling",
80
+ "version": "0.1.0"
81
+ },
82
+ {
83
+ "name": "tasks",
84
+ "source": "./tasks",
85
+ "version": "0.1.0"
86
+ },
87
+ {
88
+ "name": "waitlist",
89
+ "source": "./waitlist",
90
+ "version": "0.1.0"
91
+ },
92
+ {
93
+ "name": "whatsapp",
94
+ "source": "./whatsapp",
95
+ "version": "0.1.0"
96
+ },
97
+ {
98
+ "name": "workflows",
99
+ "source": "./workflows",
100
+ "version": "0.1.0"
101
+ }
102
+ ]
103
+ }
@@ -0,0 +1,17 @@
1
+ {
2
+ "name": "admin",
3
+ "description": "Platform administration plugin. Provides system-status, public-hostname (deterministic Cloudflare public-URL resolver — single call returning the operator's canonical hostname so agents never guess property names on :CloudflareHostname nodes), brand-settings, account-manage, account-update, admin-add, admin-remove, admin-list, admin-update-pin, agent-list, agent-config-read, logs-read, plugin-read, skill-load (one-call resolve+read for SKILL.md by skill name — the canonical primitive for loading a named skill; plugin-read remains the reader for references/* and PLUGIN.md), store-skill (deterministic write counterpart to plugin-read; persists operator-authored skills as plugin files under the active account), render-component, session-reset, session-resume, file-attach, wifi, and action-approval tools (action-pending, action-approve, action-reject, action-edit) for managing the Maxy platform.",
4
+ "version": "0.1.0",
5
+ "author": {
6
+ "name": "Rubytech LLC"
7
+ },
8
+ "mcpServers": {
9
+ "admin": {
10
+ "type": "stdio",
11
+ "command": "node",
12
+ "args": [
13
+ "${CLAUDE_PLUGIN_ROOT}/mcp/dist/index.js"
14
+ ]
15
+ }
16
+ }
17
+ }
@@ -0,0 +1,8 @@
1
+ {
2
+ "name": "anthropic",
3
+ "description": "Guide users through obtaining an Anthropic API key to power the public agent.",
4
+ "version": "0.1.0",
5
+ "author": {
6
+ "name": "Rubytech LLC"
7
+ }
8
+ }
@@ -0,0 +1,8 @@
1
+ {
2
+ "name": "business-assistant",
3
+ "description": "Business assistant for small businesses. Handles customer enquiries, appointment booking, quote formatting, invoice generation, and daily briefings. Responds instantly, triages by urgency, and never lets a message fall through the cracks.",
4
+ "version": "0.1.0",
5
+ "author": {
6
+ "name": "Rubytech LLC"
7
+ }
8
+ }
@@ -0,0 +1,17 @@
1
+ {
2
+ "name": "cloudflare",
3
+ "description": "Cloudflare Tunnel operations — setup, reset, dashboard guidance. Zero agent-facing MCP tools; every operation is a shell script or a dashboard click-path the operator performs themselves.",
4
+ "version": "0.1.0",
5
+ "author": {
6
+ "name": "Rubytech LLC"
7
+ },
8
+ "mcpServers": {
9
+ "cloudflare": {
10
+ "type": "stdio",
11
+ "command": "node",
12
+ "args": [
13
+ "${CLAUDE_PLUGIN_ROOT}/mcp/dist/index.js"
14
+ ]
15
+ }
16
+ }
17
+ }
@@ -0,0 +1,17 @@
1
+ {
2
+ "name": "contacts",
3
+ "description": "CRM contacts plugin. Provides contact-create, contact-lookup, contact-update, contact-delete, contact-list, contact-export, contact-erase, group-create, and group-manage tools for managing the customer contact graph and group conversations.",
4
+ "version": "0.1.0",
5
+ "author": {
6
+ "name": "Rubytech LLC"
7
+ },
8
+ "mcpServers": {
9
+ "contacts": {
10
+ "type": "stdio",
11
+ "command": "node",
12
+ "args": [
13
+ "${CLAUDE_PLUGIN_ROOT}/mcp/dist/index.js"
14
+ ]
15
+ }
16
+ }
17
+ }
@@ -0,0 +1,8 @@
1
+ {
2
+ "name": "deep-research",
3
+ "description": "Structured multi-source research workflow — query decomposition, search strategy, source evaluation, synthesis, and citation formatting. Uses Claude Code's native WebSearch and WebFetch tools.",
4
+ "version": "0.1.0",
5
+ "author": {
6
+ "name": "Rubytech LLC"
7
+ }
8
+ }
@@ -0,0 +1,8 @@
1
+ {
2
+ "name": "docs",
3
+ "description": "Comprehensive user guide and platform documentation for Maxy. Load references when users ask how-to, setup, or troubleshooting questions. Also contains platform architecture references for admin use.",
4
+ "version": "0.1.0",
5
+ "author": {
6
+ "name": "Rubytech LLC"
7
+ }
8
+ }
@@ -117,6 +117,56 @@ The post-install acceptance gate at `platform/scripts/test-laptop-vnc-boot.sh` r
117
117
 
118
118
  A separate operator-side harness at `platform/scripts/installer-device-verify.sh <published-version>` runs after every npm publish to confirm the installer reaches a terminal-success marker on each device in the operator's manifest. Two markers are accepted because the installer's CDP probe behaves differently per `DISPLAY_MODE`: `Browser automation ready (CDP connected)` on Pi (virtual display, persistent Chromium) and `[cdp-check] skipped reason=native-display` on laptop (native display, on-demand Chromium). Either is a pass. The harness is operator-only — end users do not run it.
119
119
 
120
+ ## Plugin registration at install time
121
+
122
+ The installer registers Claude Code plugins on the device as the last step before the brand service starts. After registration, `claude plugin list` on the Pi shows every Maxy platform plugin shipped by the brand, every premium sub-plugin shipped by the brand, and any external plugins the brand declares (e.g. Telegram, Discord, iMessage from `claude-plugins-official`). Spawned `claude` sessions inherit those plugins from `~/.claude/` — the session manager passes no `--mcp-config` argv.
123
+
124
+ **Where the manifests come from.** The Maxy plugin source tree uses `PLUGIN.md` (YAML frontmatter) for plugin metadata, not Claude Code's native `.claude-plugin/plugin.json`. At bundle time, `scripts/generate-plugin-manifests.mjs` walks the payload and synthesises a Claude-Code-native `plugin.json` per plugin plus a `marketplace.json` at each tree root. The generator runs in `packages/create-maxy/scripts/bundle.js` after platform + premium plugins are copied into the payload, so the deployed install directory carries:
125
+
126
+ - `<INSTALL_DIR>/platform/plugins/<name>/.claude-plugin/plugin.json` per platform plugin
127
+ - `<INSTALL_DIR>/platform/plugins/.claude-plugin/marketplace.json` (marketplace `maxy-platform`)
128
+ - `<INSTALL_DIR>/premium-plugins/real-agency/plugins/<sub>/.claude-plugin/plugin.json` per sub-plugin
129
+ - `<INSTALL_DIR>/premium-plugins/real-agency/plugins/.claude-plugin/marketplace.json` (`maxy-premium-real-agency`)
130
+ - `<INSTALL_DIR>/premium-plugins/{teaching,writer-craft}/.claude-plugin/plugin.json` for bundle-root plugins
131
+ - `<INSTALL_DIR>/premium-plugins/.claude-plugin/marketplace.json` (`maxy-premium`)
132
+
133
+ Generator schema:
134
+
135
+ | Field | Source | Notes |
136
+ |---|---|---|
137
+ | `name` | directory name (or `PLUGIN.md#name`) | Used as `<name>` in `plugin install` |
138
+ | `description` | `PLUGIN.md` frontmatter `description` | Falls back to "{name} plugin" if absent |
139
+ | `version` | `"0.1.0"` | Single version across all generated manifests |
140
+ | `author` | `{ "name": "Rubytech LLC" }` | Object form required by Claude Code's validator |
141
+ | `mcpServers["<name>"]` | only when `mcp/dist/index.js` exists | `{ "type": "stdio", "command": "node", "args": ["${CLAUDE_PLUGIN_ROOT}/mcp/dist/index.js"] }` |
142
+
143
+ Skills, agents, hooks, and commands directories at the plugin root are auto-discovered by Claude Code — no explicit field needed.
144
+
145
+ **Install flow** (`registerLocalAndExternalPlugins()` in `packages/create-maxy/src/index.ts`):
146
+
147
+ 1. Discover every `.claude-plugin/marketplace.json` under the install directory.
148
+ 2. For each one not already in `claude plugin marketplace list`, run `claude plugin marketplace add <dir>`. Pre-existing entries log `[plugin-marketplace] added <name> idempotent=true`.
149
+ 3. Snapshot `claude plugin list` once.
150
+ 4. Build the desired plugin set = (every local marketplace's plugin entries) + (`brand.json#externalPlugins`).
151
+ 5. For each desired plugin not in the snapshot, run `claude plugin install <name>@<marketplace> --scope user`. Already-installed plugins log `idempotent=true`. Failures log `[plugin-install] ERROR <name>@<src> exit=<n> stderr=<short>` but do not abort the installer — one plugin failing must not block the rest.
152
+ 6. For each external plugin with a `configureSecret` field whose env var is set, pipe `/<name>:configure <secret>` into a one-shot `claude --print` invocation. Missing env vars log `[plugin-configure] SKIP <name> reason=no-secret-in-env env-var=<NAME>` and continue — pairing remains a per-operator manual step.
153
+
154
+ **Brand declaration** — `brands/<brand>/brand.json#externalPlugins`:
155
+
156
+ ```jsonc
157
+ "externalPlugins": [
158
+ { "name": "telegram", "marketplace": "claude-plugins-official",
159
+ "configureSecret": "TELEGRAM_BOT_TOKEN", "channelPlugin": true },
160
+ { "name": "discord", "marketplace": "claude-plugins-official",
161
+ "configureSecret": "DISCORD_BOT_TOKEN", "channelPlugin": true },
162
+ { "name": "imessage", "marketplace": "claude-plugins-official", "channelPlugin": true }
163
+ ]
164
+ ```
165
+
166
+ `channelPlugin: true` signals the session manager to include the entry in the spawn-time `--channels plugin:<name>@<marketplace>` argv. The session manager's `/spawn` and `/resume` HTTP routes accept an optional `channels: string[]` body field that maps directly to those argv flags. When the field is absent or empty, the spawn argv is byte-identical to today's `['--verbose', '--remote-control']` shape.
167
+
168
+ **Diagnostic path** — `grep "\[plugin-install\]" ~/.<brand>/logs/install-*.log | tail -50`; compare row count against `cat brand.json | jq '.externalPlugins | length'` plus the on-disk plugin count under `<INSTALL_DIR>/platform/plugins/` and `<INSTALL_DIR>/premium-plugins/`.
169
+
120
170
  ## Running multiple brands on one device
121
171
 
122
172
  A single Pi or laptop can host more than one brand (for example Maxy and Real Agent) side by side. Each brand runs as its own service on its own port, with its own install directory and its own data. Installing one brand does not touch the other.