@rubytech/create-maxy-code 0.1.88 → 0.1.90

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 (127) hide show
  1. package/dist/__tests__/entitlement-flag.test.js +43 -0
  2. package/dist/__tests__/tier-flag.test.js +53 -0
  3. package/dist/index.js +68 -0
  4. package/dist/tier-flag.js +85 -0
  5. package/package.json +1 -1
  6. package/payload/platform/lib/admin-conversation-purge/dist/index.d.ts +54 -0
  7. package/payload/platform/lib/admin-conversation-purge/dist/index.d.ts.map +1 -0
  8. package/payload/platform/lib/admin-conversation-purge/dist/index.js +84 -0
  9. package/payload/platform/lib/admin-conversation-purge/dist/index.js.map +1 -0
  10. package/payload/platform/lib/admin-conversation-purge/src/index.ts +120 -0
  11. package/payload/platform/lib/admin-conversation-purge/tsconfig.json +8 -0
  12. package/payload/platform/lib/graph-trash/dist/index.d.ts +12 -1
  13. package/payload/platform/lib/graph-trash/dist/index.d.ts.map +1 -1
  14. package/payload/platform/lib/graph-trash/dist/index.js +8 -1
  15. package/payload/platform/lib/graph-trash/dist/index.js.map +1 -1
  16. package/payload/platform/lib/graph-trash/src/index.ts +20 -2
  17. package/payload/platform/package.json +2 -2
  18. package/payload/platform/plugins/.claude-plugin/marketplace.json +5 -0
  19. package/payload/platform/plugins/admin/PLUGIN.md +1 -0
  20. package/payload/platform/plugins/admin/hooks/__tests__/askuserquestion-investigate-gate.test.sh +176 -0
  21. package/payload/platform/plugins/admin/hooks/askuserquestion-investigate-gate.sh +257 -0
  22. package/payload/platform/plugins/admin/skills/admin-user-management/SKILL.md +2 -0
  23. package/payload/platform/plugins/docs/references/admin-session.md +5 -1
  24. package/payload/platform/plugins/docs/references/linkedin-extension.md +49 -0
  25. package/payload/platform/plugins/linkedin-extension/.claude-plugin/plugin.json +8 -0
  26. package/payload/platform/plugins/linkedin-extension/PLUGIN.md +58 -0
  27. package/payload/platform/plugins/linkedin-extension/extension/README.md +44 -0
  28. package/payload/platform/plugins/linkedin-extension/extension/__tests__/fixtures/profile.html +34 -0
  29. package/payload/platform/plugins/linkedin-extension/extension/__tests__/fixtures/thread.html +36 -0
  30. package/payload/platform/plugins/linkedin-extension/extension/assets/pill.css +52 -0
  31. package/payload/platform/plugins/linkedin-extension/extension/background/sw.js +60 -0
  32. package/payload/platform/plugins/linkedin-extension/extension/content/extractors.js +127 -0
  33. package/payload/platform/plugins/linkedin-extension/extension/content/profile.js +82 -0
  34. package/payload/platform/plugins/linkedin-extension/extension/content/thread.js +84 -0
  35. package/payload/platform/plugins/linkedin-extension/extension/manifest.json +32 -0
  36. package/payload/platform/plugins/linkedin-extension/extension/options/options.html +33 -0
  37. package/payload/platform/plugins/linkedin-extension/extension/options/options.js +30 -0
  38. package/payload/platform/plugins/linkedin-extension/skills/linkedin-extension/SKILL.md +91 -0
  39. package/payload/platform/plugins/memory/mcp/dist/lib/neo4j.d.ts +14 -0
  40. package/payload/platform/plugins/memory/mcp/dist/lib/neo4j.d.ts.map +1 -1
  41. package/payload/platform/plugins/memory/mcp/dist/lib/neo4j.js +28 -0
  42. package/payload/platform/plugins/memory/mcp/dist/lib/neo4j.js.map +1 -1
  43. package/payload/platform/plugins/memory/mcp/dist/tools/__tests__/memory-empty-trash-admin-conversation.test.d.ts +2 -0
  44. package/payload/platform/plugins/memory/mcp/dist/tools/__tests__/memory-empty-trash-admin-conversation.test.d.ts.map +1 -0
  45. package/payload/platform/plugins/memory/mcp/dist/tools/__tests__/memory-empty-trash-admin-conversation.test.js +168 -0
  46. package/payload/platform/plugins/memory/mcp/dist/tools/__tests__/memory-empty-trash-admin-conversation.test.js.map +1 -0
  47. package/payload/platform/plugins/memory/mcp/dist/tools/memory-empty-trash.d.ts +22 -3
  48. package/payload/platform/plugins/memory/mcp/dist/tools/memory-empty-trash.d.ts.map +1 -1
  49. package/payload/platform/plugins/memory/mcp/dist/tools/memory-empty-trash.js +61 -13
  50. package/payload/platform/plugins/memory/mcp/dist/tools/memory-empty-trash.js.map +1 -1
  51. package/payload/platform/scripts/seed-neo4j.sh +12 -0
  52. package/payload/platform/templates/agents/admin/IDENTITY.md +3 -1
  53. package/payload/platform/templates/specialists/agents/database-operator.md +6 -2
  54. package/payload/server/public/assets/{ChatInput-BzjU6OoA.css → ChatInput-dLWJ0Q8I.css} +1 -1
  55. package/payload/server/public/assets/{Checkbox-BWLS5Isf.js → Checkbox-Bx8mRHQ3.js} +1 -1
  56. package/payload/server/public/assets/{admin-DjU0q63n.js → admin-C57bDUgs.js} +6 -6
  57. package/payload/server/public/assets/{architectureDiagram-Q4EWVU46-lcncWUol.js → architectureDiagram-Q4EWVU46-BEW_bD50.js} +1 -1
  58. package/payload/server/public/assets/{blockDiagram-DXYQGD6D-SqlxCp6m.js → blockDiagram-DXYQGD6D-DFLT98C3.js} +1 -1
  59. package/payload/server/public/assets/{c4Diagram-AHTNJAMY-DWzvXvDc.js → c4Diagram-AHTNJAMY-BemKtROA.js} +1 -1
  60. package/payload/server/public/assets/channel-CfpByn-m.js +1 -0
  61. package/payload/server/public/assets/{chunk-336JU56O-Bz-tPZhS.js → chunk-336JU56O-BL8fGJ7_.js} +2 -2
  62. package/payload/server/public/assets/{chunk-426QAEUC-MYkbqx1u.js → chunk-426QAEUC-BD6by0MP.js} +1 -1
  63. package/payload/server/public/assets/{chunk-4TB4RGXK-DfZRQtaZ.js → chunk-4TB4RGXK-DX-cb6Nd.js} +1 -1
  64. package/payload/server/public/assets/{chunk-5FUZZQ4R-BFMrOiNe.js → chunk-5FUZZQ4R-B2QqtIZS.js} +1 -1
  65. package/payload/server/public/assets/{chunk-5PVQY5BW-64laDovl.js → chunk-5PVQY5BW-CzJWmsmd.js} +1 -1
  66. package/payload/server/public/assets/{chunk-EDXVE4YY-dClAHBNd.js → chunk-EDXVE4YY-DP9vcvEU.js} +1 -1
  67. package/payload/server/public/assets/{chunk-ENJZ2VHE-Bz4gXG1p.js → chunk-ENJZ2VHE-D5xWEjF0.js} +1 -1
  68. package/payload/server/public/assets/{chunk-ICPOFSXX-D5pWrJcN.js → chunk-ICPOFSXX-AKduf9o4.js} +1 -1
  69. package/payload/server/public/assets/{chunk-OYMX7WX6-BCiWtqaK.js → chunk-OYMX7WX6-D6jsVNyQ.js} +1 -1
  70. package/payload/server/public/assets/{chunk-U2HBQHQK-Brql314T.js → chunk-U2HBQHQK-BNAnTYh8.js} +1 -1
  71. package/payload/server/public/assets/{chunk-X2U36JSP-C-79tMym.js → chunk-X2U36JSP-UdQhYDnh.js} +1 -1
  72. package/payload/server/public/assets/{chunk-YZCP3GAM-G4idHEoZ.js → chunk-YZCP3GAM-DSSmSGmS.js} +1 -1
  73. package/payload/server/public/assets/{chunk-ZZ45TVLE-DyHGASIn.js → chunk-ZZ45TVLE-Czm7IDMC.js} +1 -1
  74. package/payload/server/public/assets/classDiagram-6PBFFD2Q-DNc9R1IX.js +1 -0
  75. package/payload/server/public/assets/classDiagram-v2-HSJHXN6E-DM1DQh8N.js +1 -0
  76. package/payload/server/public/assets/clone-BSrNdvLW.js +1 -0
  77. package/payload/server/public/assets/{dagre-KV5264BT--Bba_VRf.js → dagre-KV5264BT-CBuRCJE7.js} +1 -1
  78. package/payload/server/public/assets/{dagre-DfJ8a8tG.js → dagre-hSIMHJcO.js} +1 -1
  79. package/payload/server/public/assets/data-3n20lir5.js +1 -0
  80. package/payload/server/public/assets/{diagram-5BDNPKRD-CfUotMr9.js → diagram-5BDNPKRD-dbMrH0le.js} +1 -1
  81. package/payload/server/public/assets/{diagram-G4DWMVQ6-BWZJChPS.js → diagram-G4DWMVQ6-DBc6hs9a.js} +1 -1
  82. package/payload/server/public/assets/{diagram-MMDJMWI5-BExS6IWb.js → diagram-MMDJMWI5-ubSGybUk.js} +1 -1
  83. package/payload/server/public/assets/{diagram-TYMM5635-DQ4t_wIt.js → diagram-TYMM5635-WxL3cv-9.js} +1 -1
  84. package/payload/server/public/assets/{erDiagram-SMLLAGMA-f1sfB-AQ.js → erDiagram-SMLLAGMA-CrBBOAN8.js} +1 -1
  85. package/payload/server/public/assets/{flowDiagram-DWJPFMVM-CZexxT3v.js → flowDiagram-DWJPFMVM-CXUq5Hzt.js} +1 -1
  86. package/payload/server/public/assets/{ganttDiagram-T4ZO3ILL-BNcFUX4K.js → ganttDiagram-T4ZO3ILL-vTCBnYVS.js} +1 -1
  87. package/payload/server/public/assets/{gitGraphDiagram-UUTBAWPF-soyylUaR.js → gitGraphDiagram-UUTBAWPF-BHrj_bRa.js} +1 -1
  88. package/payload/server/public/assets/graph-ChwMpxn_.js +1 -0
  89. package/payload/server/public/assets/graph-labels-CUgJgIwl.js +1 -0
  90. package/payload/server/public/assets/{graphlib-B2ezMQY4.js → graphlib-CDO75l2X.js} +1 -1
  91. package/payload/server/public/assets/{infoDiagram-42DDH7IO-4xmenbOP.js → infoDiagram-42DDH7IO-CcQdaOhi.js} +1 -1
  92. package/payload/server/public/assets/{ishikawaDiagram-UXIWVN3A-C_D6CqVq.js → ishikawaDiagram-UXIWVN3A-3KvltFgV.js} +1 -1
  93. package/payload/server/public/assets/{journeyDiagram-VCZTEJTY-DUDx0c35.js → journeyDiagram-VCZTEJTY-DZCXaGFE.js} +1 -1
  94. package/payload/server/public/assets/{kanban-definition-6JOO6SKY-BNr7ErhZ.js → kanban-definition-6JOO6SKY-DAIAKy1-.js} +1 -1
  95. package/payload/server/public/assets/{lib-iKom7FFe.js → lib-CCLURO3E.js} +1 -1
  96. package/payload/server/public/assets/{line-Dvs5wrj5.js → line-C8iCE2Wx.js} +1 -1
  97. package/payload/server/public/assets/{mermaid-parser.core-D0ksZQlW.js → mermaid-parser.core-ElvUhnV9.js} +1 -1
  98. package/payload/server/public/assets/{mermaid.core--Btu4mYy.js → mermaid.core-7dSH31Ao.js} +3 -3
  99. package/payload/server/public/assets/{mindmap-definition-QFDTVHPH-DfyZTghB.js → mindmap-definition-QFDTVHPH-FZ0rcfac.js} +1 -1
  100. package/payload/server/public/assets/{page-BnBmdRbo.js → page-D3bFmqF4.js} +1 -1
  101. package/payload/server/public/assets/{page-x6ZXWwMx.js → page-DNjUtT5q.js} +18 -17
  102. package/payload/server/public/assets/{pieDiagram-DEJITSTG-BiG9YoLj.js → pieDiagram-DEJITSTG-CDuvMZbO.js} +1 -1
  103. package/payload/server/public/assets/{public-DW7yINk7.js → public-g_2skbnY.js} +3 -3
  104. package/payload/server/public/assets/{quadrantDiagram-34T5L4WZ-CVufUsie.js → quadrantDiagram-34T5L4WZ-xDjhkqNc.js} +1 -1
  105. package/payload/server/public/assets/{requirementDiagram-MS252O5E-UKCptCPk.js → requirementDiagram-MS252O5E-BPKpyT0n.js} +1 -1
  106. package/payload/server/public/assets/{sankeyDiagram-XADWPNL6-e8iZYL5P.js → sankeyDiagram-XADWPNL6-BvFIrihC.js} +1 -1
  107. package/payload/server/public/assets/{sequenceDiagram-FGHM5R23-CfZqVRxe.js → sequenceDiagram-FGHM5R23-C6G1oAE0.js} +1 -1
  108. package/payload/server/public/assets/{stateDiagram-FHFEXIEX-DrWX8OY8.js → stateDiagram-FHFEXIEX-DbOJT_Xh.js} +1 -1
  109. package/payload/server/public/assets/stateDiagram-v2-QKLJ7IA2-Ce4GFk-H.js +1 -0
  110. package/payload/server/public/assets/{timeline-definition-GMOUNBTQ-Blec2Yhc.js → timeline-definition-GMOUNBTQ-D1DSxHh6.js} +1 -1
  111. package/payload/server/public/assets/{vennDiagram-DHZGUBPP-BtACuGgu.js → vennDiagram-DHZGUBPP-BYoSeMdo.js} +1 -1
  112. package/payload/server/public/assets/{wardleyDiagram-NUSXRM2D-BWT7orYV.js → wardleyDiagram-NUSXRM2D-DH3kZdea.js} +1 -1
  113. package/payload/server/public/assets/{xychartDiagram-5P7HB3ND-DBzoIRiJ.js → xychartDiagram-5P7HB3ND-C6MROMLG.js} +1 -1
  114. package/payload/server/public/data.html +5 -5
  115. package/payload/server/public/graph.html +6 -6
  116. package/payload/server/public/index.html +8 -8
  117. package/payload/server/public/public.html +5 -5
  118. package/payload/server/server.js +531 -118
  119. package/payload/server/public/assets/channel-dZdJT-Ou.js +0 -1
  120. package/payload/server/public/assets/classDiagram-6PBFFD2Q-CP8AWLB9.js +0 -1
  121. package/payload/server/public/assets/classDiagram-v2-HSJHXN6E-bUe6tAo9.js +0 -1
  122. package/payload/server/public/assets/clone-gXHQCPU0.js +0 -1
  123. package/payload/server/public/assets/data-Bt6BTjvP.js +0 -1
  124. package/payload/server/public/assets/graph-labels-BeDsoEVY.js +0 -1
  125. package/payload/server/public/assets/graph-szVcmw8D.js +0 -1
  126. package/payload/server/public/assets/stateDiagram-v2-QKLJ7IA2-CvKXSO97.js +0 -1
  127. /package/payload/server/public/assets/{ChatInput-AR8ym-HW.js → ChatInput-DoS80kaC.js} +0 -0
@@ -0,0 +1,43 @@
1
+ // Acceptance gate for the founder-side `--entitlement-base64 <b64>` installer
2
+ // flag (Task 240). Pure-module coverage: base64 round-trip validation, mode
3
+ // rejection, decoded-payload non-emptiness, path resolution. Live entitlement
4
+ // delivery to a Pi is exercised by manual install per task verification §4.
5
+ import test from "node:test";
6
+ import assert from "node:assert/strict";
7
+ import { validateEntitlementBase64, entitlementPath, } from "../tier-flag.js";
8
+ // A small but realistic envelope shape — looks like what
9
+ // platform/scripts/generate-entitlement-fixture.mjs sign produces.
10
+ const SAMPLE_PAYLOAD = JSON.stringify({
11
+ payload: { accountId: "x", tier: "family" },
12
+ signature: "deadbeef",
13
+ });
14
+ const SAMPLE_B64 = Buffer.from(SAMPLE_PAYLOAD, "utf-8").toString("base64");
15
+ test("validateEntitlementBase64 accepts a well-formed payload when commercialMode is true", () => {
16
+ const decoded = validateEntitlementBase64(SAMPLE_B64, true);
17
+ assert.equal(decoded.toString("utf-8"), SAMPLE_PAYLOAD);
18
+ });
19
+ test("validateEntitlementBase64 tolerates embedded whitespace (newlines, tabs, spaces)", () => {
20
+ // SSH transit and operator-side paste can introduce \r, \n, or stray spaces.
21
+ // The stripper must normalise them before the round-trip check fires.
22
+ const noisy = SAMPLE_B64.slice(0, 8) + "\n" + SAMPLE_B64.slice(8, 16) + " \t" + SAMPLE_B64.slice(16);
23
+ const decoded = validateEntitlementBase64(noisy, true);
24
+ assert.equal(decoded.toString("utf-8"), SAMPLE_PAYLOAD);
25
+ });
26
+ test("validateEntitlementBase64 rejects garbage characters", () => {
27
+ assert.throws(() => validateEntitlementBase64("not-valid-base64@#$", true), /not valid base64/);
28
+ });
29
+ test("validateEntitlementBase64 rejects empty value and flag-shaped value", () => {
30
+ assert.throws(() => validateEntitlementBase64(undefined, true), /requires a base64-encoded value/);
31
+ assert.throws(() => validateEntitlementBase64("", true), /requires a base64-encoded value/);
32
+ assert.throws(() => validateEntitlementBase64("--port", true), /requires a base64-encoded value/);
33
+ });
34
+ test("validateEntitlementBase64 rejects whitespace-only value", () => {
35
+ assert.throws(() => validateEntitlementBase64(" \n\t ", true), /empty after whitespace stripping/);
36
+ });
37
+ test("validateEntitlementBase64 rejects --entitlement-base64 when brand is personal-mode", () => {
38
+ assert.throws(() => validateEntitlementBase64(SAMPLE_B64, false), /Personal mode — use --tier <enum>/);
39
+ });
40
+ test("entitlementPath joins homeDir + configDir + entitlement.json", () => {
41
+ assert.equal(entitlementPath("/home/admin", ".realagent-code"), "/home/admin/.realagent-code/entitlement.json");
42
+ assert.equal(entitlementPath("/home/me", ".maxy-code"), "/home/me/.maxy-code/entitlement.json");
43
+ });
@@ -0,0 +1,53 @@
1
+ // Acceptance gate for the founder-side `--tier <enum>` installer flag (Task 240).
2
+ // Pure-module coverage: validation rules, mode-mismatch rejection, config merge.
3
+ // Wiring through index.ts argv is exercised by manual install per task verification §1.
4
+ import test from "node:test";
5
+ import assert from "node:assert/strict";
6
+ import { validateTierFlag, applyTierToAccountConfig, isValidTier, } from "../tier-flag.js";
7
+ test("validateTierFlag accepts solo|family|pro when commercialMode is false", () => {
8
+ assert.equal(validateTierFlag("solo", false), "solo");
9
+ assert.equal(validateTierFlag("family", false), "family");
10
+ assert.equal(validateTierFlag("pro", false), "pro");
11
+ });
12
+ test("validateTierFlag rejects values outside the enum", () => {
13
+ assert.throws(() => validateTierFlag("enterprise", false), /--tier value must be one of: solo, family, pro/);
14
+ assert.throws(() => validateTierFlag("max", false), /--tier value must be one of: solo, family, pro/);
15
+ });
16
+ test("validateTierFlag rejects empty or flag-shaped value", () => {
17
+ assert.throws(() => validateTierFlag(undefined, false), /--tier requires a value/);
18
+ assert.throws(() => validateTierFlag("", false), /--tier requires a value/);
19
+ assert.throws(() => validateTierFlag("--hostname", false), /--tier requires a value/);
20
+ });
21
+ test("validateTierFlag rejects --tier when brand is commercial-mode", () => {
22
+ assert.throws(() => validateTierFlag("family", true), /Commercial mode — use --entitlement-base64/);
23
+ // Mode mismatch fires before enum check — operator gets the redirect, not "invalid enum".
24
+ assert.throws(() => validateTierFlag("enterprise", true), /Commercial mode — use --entitlement-base64/);
25
+ });
26
+ test("applyTierToAccountConfig stamps tier and preserves every other key", () => {
27
+ const before = {
28
+ accountId: "abc-123",
29
+ enabledPlugins: ["whatsapp", "outlook"],
30
+ outputStyle: "default",
31
+ thinkingView: "default",
32
+ };
33
+ const after = applyTierToAccountConfig(before, "family");
34
+ assert.equal(after.tier, "family");
35
+ assert.equal(after.accountId, "abc-123");
36
+ assert.deepEqual(after.enabledPlugins, ["whatsapp", "outlook"]);
37
+ assert.equal(after.outputStyle, "default");
38
+ assert.equal(after.thinkingView, "default");
39
+ // Input not mutated — caller can safely roundtrip.
40
+ assert.equal(before.tier, undefined);
41
+ });
42
+ test("applyTierToAccountConfig overwrites a previously-stamped tier", () => {
43
+ const after = applyTierToAccountConfig({ tier: "solo", accountId: "x" }, "pro");
44
+ assert.equal(after.tier, "pro");
45
+ assert.equal(after.accountId, "x");
46
+ });
47
+ test("isValidTier narrows correctly", () => {
48
+ assert.equal(isValidTier("solo"), true);
49
+ assert.equal(isValidTier("family"), true);
50
+ assert.equal(isValidTier("pro"), true);
51
+ assert.equal(isValidTier("enterprise"), false);
52
+ assert.equal(isValidTier(""), false);
53
+ });
package/dist/index.js CHANGED
@@ -5,6 +5,7 @@ import { registerSpecialistAgentsAt, SpecialistSymlinkCollision } from "./specia
5
5
  import { resolve, join, dirname } from "node:path";
6
6
  import { randomBytes } from "node:crypto";
7
7
  import { resolveInstallPortFromFs, buildMaxyUnitFile, buildClaudeSessionManagerUnitFile } from "./port-resolution.js";
8
+ import { validateTierFlag, validateEntitlementBase64, applyTierToAccountConfig, entitlementPath } from "./tier-flag.js";
8
9
  import { parseOsRelease, isUbuntuLike as isUbuntuLikePure, parseAptCacheCandidate, decideAptResolution, } from "./apt-resolve.js";
9
10
  import { findPeerBrandOnDefaultNeo4jPort } from "./peer-brand-detect.js";
10
11
  import { runInitLogging } from "./init-logging.js";
@@ -2350,9 +2351,17 @@ function writeInstallDefaults(accountId) {
2350
2351
  config.enabledPlugins = enabledPlugins;
2351
2352
  config.outputStyle = "default";
2352
2353
  config.thinkingView = "default";
2354
+ if (TIER_FLAG) {
2355
+ config = applyTierToAccountConfig(config, TIER_FLAG);
2356
+ }
2353
2357
  writeFileSync(accountJsonPath, JSON.stringify(config, null, 2) + "\n");
2354
2358
  console.log(` [install-defaults] account-json plugins=${enabledPlugins.length} outputStyle=default thinkingView=default`);
2355
2359
  logFile(` [install-defaults] account-json plugins=${enabledPlugins.length} outputStyle=default thinkingView=default`);
2360
+ if (TIER_FLAG) {
2361
+ const tierLine = ` [install-defaults] tier=${TIER_FLAG} mode=personal source=cli-flag`;
2362
+ console.log(tierLine);
2363
+ logFile(tierLine);
2364
+ }
2356
2365
  // Admin SOUL.md is sourced from the bundled template with ${PRODUCT_NAME}
2357
2366
  // substituted at install time. The template carries the Phase 1 standing
2358
2367
  // offer (brochure / preval / Loop CRM) so the agent ships with a stated
@@ -2397,6 +2406,22 @@ function writeInstallDefaults(accountId) {
2397
2406
  logFile(` [install-defaults] public-config path=${publicConfigDst}`);
2398
2407
  }
2399
2408
  }
2409
+ /**
2410
+ * Commercial-mode entitlement delivery (Task 240). Writes the decoded payload
2411
+ * bytes verbatim to ~/<BRAND.configDir>/entitlement.json. Signature and shape
2412
+ * validation happen at the verifier at session start (entitlement lib); the
2413
+ * installer is a dumb delivery channel — a malformed payload that survived
2414
+ * --entitlement-base64 parsing will be caught there and degrade the install
2415
+ * to anonymous-fallback, which is the correct loud-fail per doctrine.
2416
+ */
2417
+ function writeEntitlementFromFlag(payload) {
2418
+ const target = entitlementPath(process.env.HOME ?? "/root", BRAND.configDir);
2419
+ mkdirSync(dirname(target), { recursive: true });
2420
+ writeFileSync(target, payload);
2421
+ const line = ` [install-defaults] entitlement-delivered bytes=${payload.length} path=${target} source=cli-flag`;
2422
+ console.log(line);
2423
+ logFile(line);
2424
+ }
2400
2425
  // ---------------------------------------------------------------------------
2401
2426
  // Tunnel script shortcuts
2402
2427
  //
@@ -3376,6 +3401,46 @@ if (hostnameIdx !== -1) {
3376
3401
  }
3377
3402
  HOSTNAME_FLAG = raw;
3378
3403
  }
3404
+ // ---------------------------------------------------------------------------
3405
+ // Founder-side tier-set flags (Task 240).
3406
+ //
3407
+ // `--tier <solo|family|pro>` stamps account.json.tier directly when the brand
3408
+ // is personal-mode (today's default for both maxy-code and realagent-code).
3409
+ // `--entitlement-base64 <b64>` decodes a signed payload and writes it to
3410
+ // ~/<BRAND.configDir>/entitlement.json when the brand is commercial-mode.
3411
+ // The verifier at session start consumes whichever path the brand is on.
3412
+ //
3413
+ // Mode-mismatch combinations reject loudly at parse time — operator gets a
3414
+ // redirect message pointing at the correct flag. The installer is the sole
3415
+ // doctrine-sanctioned device-mutation channel; the admin agent has no tier-
3416
+ // upgrade surface by design.
3417
+ // ---------------------------------------------------------------------------
3418
+ let TIER_FLAG;
3419
+ let ENTITLEMENT_PAYLOAD;
3420
+ const tierIdx = _args.indexOf("--tier");
3421
+ if (tierIdx !== -1) {
3422
+ try {
3423
+ TIER_FLAG = validateTierFlag(_args[tierIdx + 1], BRAND.commercialMode === true);
3424
+ }
3425
+ catch (err) {
3426
+ const msg = err instanceof Error ? err.message : String(err);
3427
+ console.error(`Setup failed: ${msg}`);
3428
+ console.error(`Error: ${msg}`);
3429
+ process.exit(1);
3430
+ }
3431
+ }
3432
+ const entitlementIdx = _args.indexOf("--entitlement-base64");
3433
+ if (entitlementIdx !== -1) {
3434
+ try {
3435
+ ENTITLEMENT_PAYLOAD = validateEntitlementBase64(_args[entitlementIdx + 1], BRAND.commercialMode === true);
3436
+ }
3437
+ catch (err) {
3438
+ const msg = err instanceof Error ? err.message : String(err);
3439
+ console.error(`Setup failed: ${msg}`);
3440
+ console.error(`Error: ${msg}`);
3441
+ process.exit(1);
3442
+ }
3443
+ }
3379
3444
  let DISPLAY_MODE = "virtual";
3380
3445
  let DISPLAY_MODE_SOURCE = "default";
3381
3446
  const displayIdx = _args.indexOf("--display");
@@ -3638,6 +3703,9 @@ try {
3638
3703
  registerLocalAndExternalPlugins(); // install-time plugin registration
3639
3704
  setupVncViewer();
3640
3705
  setupAccount();
3706
+ if (ENTITLEMENT_PAYLOAD) {
3707
+ writeEntitlementFromFlag(ENTITLEMENT_PAYLOAD);
3708
+ }
3641
3709
  installTunnelScripts(); // ~/setup-tunnel.sh, ~/reset-tunnel.sh — the SKILL contract
3642
3710
  installService();
3643
3711
  // Samba on the LAN. Runs after the brand systemd unit is up so
@@ -0,0 +1,85 @@
1
+ // Pure validation + path helpers for the founder-side tier-set installer flags
2
+ // (Task 240). Two flags wired into the installer at src/index.ts:
3
+ // --tier <solo|family|pro> personal-mode (brand.commercialMode === false)
4
+ // --entitlement-base64 <b64> commercial-mode (brand.commercialMode === true)
5
+ //
6
+ // Both helpers throw on invalid input; the caller exits non-zero and surfaces the
7
+ // thrown message. No filesystem or console side effects — index.ts owns those.
8
+ import { resolve } from "node:path";
9
+ const VALID_TIERS = new Set(["solo", "family", "pro"]);
10
+ export function isValidTier(value) {
11
+ return VALID_TIERS.has(value);
12
+ }
13
+ /**
14
+ * Validate the `--tier <enum>` argv value and the brand mode.
15
+ *
16
+ * Throws when:
17
+ * - raw is missing / starts with `--` (no value supplied after the flag)
18
+ * - raw is not in {solo, family, pro}
19
+ * - brand is commercial-mode (operator must use --entitlement-base64 instead)
20
+ */
21
+ export function validateTierFlag(raw, commercialMode) {
22
+ if (commercialMode) {
23
+ throw new Error("--tier is for personal mode. Commercial mode — use --entitlement-base64 to deliver a signed payload.");
24
+ }
25
+ if (!raw || raw.startsWith("--")) {
26
+ throw new Error("--tier requires a value (one of: solo, family, pro).");
27
+ }
28
+ if (!isValidTier(raw)) {
29
+ throw new Error(`--tier value must be one of: solo, family, pro (got: ${raw}).`);
30
+ }
31
+ return raw;
32
+ }
33
+ /**
34
+ * Validate the `--entitlement-base64 <b64>` argv value and the brand mode.
35
+ *
36
+ * Round-trips through Buffer to detect malformed base64 — Buffer.from is loose
37
+ * (silently drops invalid chars), so an explicit re-encode comparison is the
38
+ * cheapest way to confirm the input is real base64. Whitespace (newlines from
39
+ * SSH transit, stray \r) is stripped first.
40
+ *
41
+ * Throws when:
42
+ * - brand is personal-mode (operator must use --tier instead)
43
+ * - raw is missing / starts with `--`
44
+ * - stripped base64 fails the round-trip check (malformed chars)
45
+ * - decoded bytes are empty
46
+ *
47
+ * Returns the decoded Buffer; caller writes it to entitlement.json verbatim.
48
+ * No JSON parse, no signature validation — that's the verifier's job at session
49
+ * start. The installer is a dumb delivery channel.
50
+ */
51
+ export function validateEntitlementBase64(raw, commercialMode) {
52
+ if (!commercialMode) {
53
+ throw new Error("--entitlement-base64 is for commercial mode. Personal mode — use --tier <enum> to set tier directly.");
54
+ }
55
+ if (!raw || raw.startsWith("--")) {
56
+ throw new Error("--entitlement-base64 requires a base64-encoded value.");
57
+ }
58
+ const stripped = raw.replace(/\s/g, "");
59
+ if (stripped.length === 0) {
60
+ throw new Error("--entitlement-base64 value is empty after whitespace stripping.");
61
+ }
62
+ const decoded = Buffer.from(stripped, "base64");
63
+ if (decoded.toString("base64") !== stripped) {
64
+ throw new Error("--entitlement-base64 value is not valid base64.");
65
+ }
66
+ if (decoded.length === 0) {
67
+ throw new Error("--entitlement-base64 decoded to zero bytes.");
68
+ }
69
+ return decoded;
70
+ }
71
+ /**
72
+ * Build the absolute path where the installer writes entitlement.json. Matches
73
+ * the verifier's `resolve(brand.configDir, "entitlement.json")` (entitlement
74
+ * lib src/index.ts:140) so writer + reader agree on a single location.
75
+ */
76
+ export function entitlementPath(homeDir, configDir) {
77
+ return resolve(homeDir, configDir, "entitlement.json");
78
+ }
79
+ /**
80
+ * Stamp the validated tier onto an account.json config object. Pure merge;
81
+ * preserves every other key (enabledPlugins, outputStyle, thinkingView, etc.).
82
+ */
83
+ export function applyTierToAccountConfig(config, tier) {
84
+ return { ...config, tier };
85
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rubytech/create-maxy-code",
3
- "version": "0.1.88",
3
+ "version": "0.1.90",
4
4
  "description": "Install Maxy — AI for Productive People",
5
5
  "bin": {
6
6
  "create-maxy-code": "./dist/index.js"
@@ -0,0 +1,54 @@
1
+ /**
2
+ * Admin-conversation JSONL purge — Task 237.
3
+ *
4
+ * Shared primitive that fires the claude-session-manager's
5
+ * `DELETE /:sessionId` per attached JSONL. Pure JSONL-only: it does not
6
+ * touch the graph. Callers (the `cascadeAdminConversationDelete` engine
7
+ * and `memory-empty-trash`'s defense-in-depth `onEmpty`) own the graph
8
+ * side and decide whether to proceed after the JSONL outcome lands.
9
+ *
10
+ * Contract:
11
+ * - manager 204 (purged) or 404 (manager never knew the JSONL) both count
12
+ * as "transcript no longer exists" and proceed to the next sessionId.
13
+ * - manager 409 is the structural `pty-still-alive` stop check; the loop
14
+ * halts and the caller surfaces the live sessionId.
15
+ * - any other status or transport failure halts the loop with
16
+ * `jsonl-purge-failed`, naming the failing sessionId and the ids already
17
+ * purged at the failure point.
18
+ *
19
+ * Log lines mirror the Task 217 cascade so a single grep
20
+ * (`[admin-conversation-delete]`) covers both call sites.
21
+ */
22
+ export interface PurgeAdminConversationJsonlsArgs {
23
+ /** Used only for log id8; not for routing. */
24
+ conversationId: string;
25
+ /** Used only for log line context; the helper does not enforce isolation
26
+ * (the caller has already account-scoped the candidate). */
27
+ accountId: string;
28
+ /** Loopback manager base URL — `http://127.0.0.1:${CLAUDE_SESSION_MANAGER_PORT}`. */
29
+ managerBase: string;
30
+ /** Pre-resolved sessionIds to purge. Caller chooses the lookup strategy
31
+ * (`getConversationClaudeSessionIds` for live conversations,
32
+ * elementId-based walk for trashed ones). */
33
+ claudeSessionIds: string[];
34
+ fetchImpl?: typeof fetch;
35
+ logger?: (line: string) => void;
36
+ }
37
+ export type PurgeAdminConversationJsonlsOutcome = {
38
+ ok: true;
39
+ claudeSessionIdsPurged: string[];
40
+ } | {
41
+ ok: false;
42
+ reason: "pty-still-alive";
43
+ claudeSessionId: string;
44
+ claudeSessionIdsPurged: string[];
45
+ } | {
46
+ ok: false;
47
+ reason: "jsonl-purge-failed";
48
+ claudeSessionId: string;
49
+ claudeSessionIdsPurged: string[];
50
+ status: number;
51
+ message: string;
52
+ };
53
+ export declare function purgeAdminConversationJsonls(args: PurgeAdminConversationJsonlsArgs): Promise<PurgeAdminConversationJsonlsOutcome>;
54
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;GAoBG;AAEH,MAAM,WAAW,gCAAgC;IAC/C,8CAA8C;IAC9C,cAAc,EAAE,MAAM,CAAC;IACvB;iEAC6D;IAC7D,SAAS,EAAE,MAAM,CAAC;IAClB,qFAAqF;IACrF,WAAW,EAAE,MAAM,CAAC;IACpB;;kDAE8C;IAC9C,gBAAgB,EAAE,MAAM,EAAE,CAAC;IAC3B,SAAS,CAAC,EAAE,OAAO,KAAK,CAAC;IACzB,MAAM,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,CAAC;CACjC;AAED,MAAM,MAAM,mCAAmC,GAC3C;IAAE,EAAE,EAAE,IAAI,CAAC;IAAC,sBAAsB,EAAE,MAAM,EAAE,CAAA;CAAE,GAC9C;IACE,EAAE,EAAE,KAAK,CAAC;IACV,MAAM,EAAE,iBAAiB,CAAC;IAC1B,eAAe,EAAE,MAAM,CAAC;IACxB,sBAAsB,EAAE,MAAM,EAAE,CAAC;CAClC,GACD;IACE,EAAE,EAAE,KAAK,CAAC;IACV,MAAM,EAAE,oBAAoB,CAAC;IAC7B,eAAe,EAAE,MAAM,CAAC;IACxB,sBAAsB,EAAE,MAAM,EAAE,CAAC;IACjC,MAAM,EAAE,MAAM,CAAC;IACf,OAAO,EAAE,MAAM,CAAC;CACjB,CAAC;AAEN,wBAAsB,4BAA4B,CAChD,IAAI,EAAE,gCAAgC,GACrC,OAAO,CAAC,mCAAmC,CAAC,CA8D9C"}
@@ -0,0 +1,84 @@
1
+ "use strict";
2
+ /**
3
+ * Admin-conversation JSONL purge — Task 237.
4
+ *
5
+ * Shared primitive that fires the claude-session-manager's
6
+ * `DELETE /:sessionId` per attached JSONL. Pure JSONL-only: it does not
7
+ * touch the graph. Callers (the `cascadeAdminConversationDelete` engine
8
+ * and `memory-empty-trash`'s defense-in-depth `onEmpty`) own the graph
9
+ * side and decide whether to proceed after the JSONL outcome lands.
10
+ *
11
+ * Contract:
12
+ * - manager 204 (purged) or 404 (manager never knew the JSONL) both count
13
+ * as "transcript no longer exists" and proceed to the next sessionId.
14
+ * - manager 409 is the structural `pty-still-alive` stop check; the loop
15
+ * halts and the caller surfaces the live sessionId.
16
+ * - any other status or transport failure halts the loop with
17
+ * `jsonl-purge-failed`, naming the failing sessionId and the ids already
18
+ * purged at the failure point.
19
+ *
20
+ * Log lines mirror the Task 217 cascade so a single grep
21
+ * (`[admin-conversation-delete]`) covers both call sites.
22
+ */
23
+ Object.defineProperty(exports, "__esModule", { value: true });
24
+ exports.purgeAdminConversationJsonls = purgeAdminConversationJsonls;
25
+ async function purgeAdminConversationJsonls(args) {
26
+ const fetchImpl = args.fetchImpl ?? fetch;
27
+ const log = args.logger ?? ((line) => console.log(line));
28
+ const id8 = args.conversationId.slice(0, 8);
29
+ const purged = [];
30
+ for (const sessionId of args.claudeSessionIds) {
31
+ const url = `${args.managerBase}/${encodeURIComponent(sessionId)}`;
32
+ let res;
33
+ try {
34
+ res = await fetchImpl(url, { method: "DELETE" });
35
+ }
36
+ catch (err) {
37
+ const message = err instanceof Error ? err.message : String(err);
38
+ log(`[admin-conversation-delete] conversationId=${id8} reason=jsonl-purge-failed claudeSessionId=${sessionId.slice(0, 8)} message=${message}`);
39
+ return {
40
+ ok: false,
41
+ reason: "jsonl-purge-failed",
42
+ claudeSessionId: sessionId,
43
+ claudeSessionIdsPurged: purged,
44
+ status: 0,
45
+ message,
46
+ };
47
+ }
48
+ if (res.status === 204 || res.status === 404) {
49
+ purged.push(sessionId);
50
+ continue;
51
+ }
52
+ if (res.status === 409) {
53
+ log(`[admin-conversation-delete] conversationId=${id8} reason=pty-still-alive claudeSessionId=${sessionId.slice(0, 8)}`);
54
+ return {
55
+ ok: false,
56
+ reason: "pty-still-alive",
57
+ claudeSessionId: sessionId,
58
+ claudeSessionIdsPurged: purged,
59
+ };
60
+ }
61
+ let message = `manager returned ${res.status}`;
62
+ try {
63
+ const body = (await res.json());
64
+ if (typeof body.detail === "string")
65
+ message = body.detail;
66
+ else if (typeof body.error === "string")
67
+ message = body.error;
68
+ }
69
+ catch {
70
+ // body isn't JSON — keep status-coded message
71
+ }
72
+ log(`[admin-conversation-delete] conversationId=${id8} reason=jsonl-purge-failed claudeSessionId=${sessionId.slice(0, 8)} message=${message}`);
73
+ return {
74
+ ok: false,
75
+ reason: "jsonl-purge-failed",
76
+ claudeSessionId: sessionId,
77
+ claudeSessionIdsPurged: purged,
78
+ status: res.status,
79
+ message,
80
+ };
81
+ }
82
+ return { ok: true, claudeSessionIdsPurged: purged };
83
+ }
84
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,120 @@
1
+ /**
2
+ * Admin-conversation JSONL purge — Task 237.
3
+ *
4
+ * Shared primitive that fires the claude-session-manager's
5
+ * `DELETE /:sessionId` per attached JSONL. Pure JSONL-only: it does not
6
+ * touch the graph. Callers (the `cascadeAdminConversationDelete` engine
7
+ * and `memory-empty-trash`'s defense-in-depth `onEmpty`) own the graph
8
+ * side and decide whether to proceed after the JSONL outcome lands.
9
+ *
10
+ * Contract:
11
+ * - manager 204 (purged) or 404 (manager never knew the JSONL) both count
12
+ * as "transcript no longer exists" and proceed to the next sessionId.
13
+ * - manager 409 is the structural `pty-still-alive` stop check; the loop
14
+ * halts and the caller surfaces the live sessionId.
15
+ * - any other status or transport failure halts the loop with
16
+ * `jsonl-purge-failed`, naming the failing sessionId and the ids already
17
+ * purged at the failure point.
18
+ *
19
+ * Log lines mirror the Task 217 cascade so a single grep
20
+ * (`[admin-conversation-delete]`) covers both call sites.
21
+ */
22
+
23
+ export interface PurgeAdminConversationJsonlsArgs {
24
+ /** Used only for log id8; not for routing. */
25
+ conversationId: string;
26
+ /** Used only for log line context; the helper does not enforce isolation
27
+ * (the caller has already account-scoped the candidate). */
28
+ accountId: string;
29
+ /** Loopback manager base URL — `http://127.0.0.1:${CLAUDE_SESSION_MANAGER_PORT}`. */
30
+ managerBase: string;
31
+ /** Pre-resolved sessionIds to purge. Caller chooses the lookup strategy
32
+ * (`getConversationClaudeSessionIds` for live conversations,
33
+ * elementId-based walk for trashed ones). */
34
+ claudeSessionIds: string[];
35
+ fetchImpl?: typeof fetch;
36
+ logger?: (line: string) => void;
37
+ }
38
+
39
+ export type PurgeAdminConversationJsonlsOutcome =
40
+ | { ok: true; claudeSessionIdsPurged: string[] }
41
+ | {
42
+ ok: false;
43
+ reason: "pty-still-alive";
44
+ claudeSessionId: string;
45
+ claudeSessionIdsPurged: string[];
46
+ }
47
+ | {
48
+ ok: false;
49
+ reason: "jsonl-purge-failed";
50
+ claudeSessionId: string;
51
+ claudeSessionIdsPurged: string[];
52
+ status: number;
53
+ message: string;
54
+ };
55
+
56
+ export async function purgeAdminConversationJsonls(
57
+ args: PurgeAdminConversationJsonlsArgs,
58
+ ): Promise<PurgeAdminConversationJsonlsOutcome> {
59
+ const fetchImpl = args.fetchImpl ?? fetch;
60
+ const log = args.logger ?? ((line: string) => console.log(line));
61
+ const id8 = args.conversationId.slice(0, 8);
62
+
63
+ const purged: string[] = [];
64
+ for (const sessionId of args.claudeSessionIds) {
65
+ const url = `${args.managerBase}/${encodeURIComponent(sessionId)}`;
66
+ let res: Response;
67
+ try {
68
+ res = await fetchImpl(url, { method: "DELETE" });
69
+ } catch (err) {
70
+ const message = err instanceof Error ? err.message : String(err);
71
+ log(
72
+ `[admin-conversation-delete] conversationId=${id8} reason=jsonl-purge-failed claudeSessionId=${sessionId.slice(0, 8)} message=${message}`,
73
+ );
74
+ return {
75
+ ok: false,
76
+ reason: "jsonl-purge-failed",
77
+ claudeSessionId: sessionId,
78
+ claudeSessionIdsPurged: purged,
79
+ status: 0,
80
+ message,
81
+ };
82
+ }
83
+ if (res.status === 204 || res.status === 404) {
84
+ purged.push(sessionId);
85
+ continue;
86
+ }
87
+ if (res.status === 409) {
88
+ log(
89
+ `[admin-conversation-delete] conversationId=${id8} reason=pty-still-alive claudeSessionId=${sessionId.slice(0, 8)}`,
90
+ );
91
+ return {
92
+ ok: false,
93
+ reason: "pty-still-alive",
94
+ claudeSessionId: sessionId,
95
+ claudeSessionIdsPurged: purged,
96
+ };
97
+ }
98
+ let message = `manager returned ${res.status}`;
99
+ try {
100
+ const body = (await res.json()) as { error?: string; detail?: string };
101
+ if (typeof body.detail === "string") message = body.detail;
102
+ else if (typeof body.error === "string") message = body.error;
103
+ } catch {
104
+ // body isn't JSON — keep status-coded message
105
+ }
106
+ log(
107
+ `[admin-conversation-delete] conversationId=${id8} reason=jsonl-purge-failed claudeSessionId=${sessionId.slice(0, 8)} message=${message}`,
108
+ );
109
+ return {
110
+ ok: false,
111
+ reason: "jsonl-purge-failed",
112
+ claudeSessionId: sessionId,
113
+ claudeSessionIdsPurged: purged,
114
+ status: res.status,
115
+ message,
116
+ };
117
+ }
118
+
119
+ return { ok: true, claudeSessionIdsPurged: purged };
120
+ }
@@ -0,0 +1,8 @@
1
+ {
2
+ "extends": "../../tsconfig.base.json",
3
+ "compilerOptions": {
4
+ "outDir": "dist",
5
+ "rootDir": "src"
6
+ },
7
+ "include": ["src"]
8
+ }
@@ -59,7 +59,18 @@ export interface EmptyTrashParams {
59
59
  dryRun?: boolean;
60
60
  /** Optional label whitelist — only nodes carrying any of these labels are eligible. */
61
61
  labels?: string[];
62
- /** Side-effect callback invoked per emptied node, e.g. to clean disk attachments. */
62
+ /** Side-effect callback invoked before the `DETACH DELETE` for each
63
+ * emptied node, e.g. to clean disk attachments or purge sibling JSONL
64
+ * transcripts.
65
+ *
66
+ * Contract (Task 237): if the callback **throws**, the candidate is
67
+ * **skipped this run** — no `DETACH DELETE` runs and no `[trash:emptied]`
68
+ * line emits. The thrown message lands in
69
+ * `[trash:empty-run] reason=onEmpty-failed` and the candidate becomes
70
+ * eligible again on the next sweep. Pre-Task-237 consumers
71
+ * (`removeAttachmentDir` for `:KnowledgeDocument`) cannot throw because
72
+ * they wrap `fs.rm({force:true})`, so the contract change is invisible
73
+ * to them. */
63
74
  onEmpty?: (node: TrashCandidate) => Promise<void>;
64
75
  }
65
76
  export interface TrashCandidate {
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;GAqBG;AAEH,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,cAAc,CAAC;AAsC5C,MAAM,WAAW,WAAW;IAC1B,OAAO,EAAE,OAAO,CAAC;IACjB,SAAS,EAAE,MAAM,CAAC;IAClB,SAAS,EAAE,MAAM,CAAC;IAClB,oFAAoF;IACpF,EAAE,EAAE,MAAM,CAAC;IACX,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAED,MAAM,WAAW,WAAW;IAC1B,OAAO,EAAE,OAAO,CAAC;IACjB,cAAc,EAAE,OAAO,CAAC;IACxB,MAAM,EAAE,MAAM,CAAC;IACf,mCAAmC;IACnC,MAAM,EAAE,MAAM,EAAE,CAAC;IACjB,SAAS,EAAE,MAAM,CAAC;IAClB,YAAY,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CACvC;AAED,wBAAsB,SAAS,CAAC,MAAM,EAAE,WAAW,GAAG,OAAO,CAAC,WAAW,CAAC,CAyIzE;AAED,MAAM,WAAW,aAAa;IAC5B,OAAO,EAAE,OAAO,CAAC;IACjB,SAAS,EAAE,MAAM,CAAC;IAClB,SAAS,EAAE,MAAM,CAAC;CACnB;AAED,MAAM,WAAW,aAAa;IAC5B,QAAQ,EAAE,OAAO,CAAC;IAClB,MAAM,EAAE,MAAM,CAAC;IACf,MAAM,EAAE,MAAM,EAAE,CAAC;IACjB,YAAY,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CACvC;AAED,wBAAsB,WAAW,CAAC,MAAM,EAAE,aAAa,GAAG,OAAO,CAAC,aAAa,CAAC,CA6G/E;AAED,MAAM,WAAW,gBAAgB;IAC/B,OAAO,EAAE,OAAO,CAAC;IACjB,SAAS,EAAE,MAAM,CAAC;IAClB,kBAAkB;IAClB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB,uFAAuF;IACvF,MAAM,CAAC,EAAE,MAAM,EAAE,CAAC;IAClB,qFAAqF;IACrF,OAAO,CAAC,EAAE,CAAC,IAAI,EAAE,cAAc,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;CACnD;AAED,MAAM,WAAW,cAAc;IAC7B,SAAS,EAAE,MAAM,CAAC;IAClB,MAAM,EAAE,MAAM,EAAE,CAAC;IACjB,SAAS,EAAE,MAAM,CAAC;IAClB,SAAS,EAAE,MAAM,GAAG,IAAI,CAAC;IACzB,gFAAgF;IAChF,WAAW,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CACtC;AAED,MAAM,WAAW,gBAAgB;IAC/B,SAAS,EAAE,MAAM,CAAC;IAClB,MAAM,EAAE,OAAO,CAAC;IAChB,UAAU,EAAE,cAAc,EAAE,CAAC;IAC7B,OAAO,EAAE,MAAM,CAAC;CACjB;AAED,wBAAsB,UAAU,CAAC,MAAM,EAAE,gBAAgB,GAAG,OAAO,CAAC,gBAAgB,CAAC,CA0EpF;AAED;;;;;;;;;GASG;AACH,wBAAgB,UAAU,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAEhD;AAED,qFAAqF;AACrF,wBAAgB,mBAAmB,CAAC,MAAM,EAAE,MAAM,EAAE,GAAG,MAAM,EAAE,CAI9D;AAED,2FAA2F;AAC3F,eAAO,MAAM,oBAAoB,EAAE,SAAS,MAAM,EAAqB,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;GAqBG;AAEH,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,cAAc,CAAC;AAsC5C,MAAM,WAAW,WAAW;IAC1B,OAAO,EAAE,OAAO,CAAC;IACjB,SAAS,EAAE,MAAM,CAAC;IAClB,SAAS,EAAE,MAAM,CAAC;IAClB,oFAAoF;IACpF,EAAE,EAAE,MAAM,CAAC;IACX,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAED,MAAM,WAAW,WAAW;IAC1B,OAAO,EAAE,OAAO,CAAC;IACjB,cAAc,EAAE,OAAO,CAAC;IACxB,MAAM,EAAE,MAAM,CAAC;IACf,mCAAmC;IACnC,MAAM,EAAE,MAAM,EAAE,CAAC;IACjB,SAAS,EAAE,MAAM,CAAC;IAClB,YAAY,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CACvC;AAED,wBAAsB,SAAS,CAAC,MAAM,EAAE,WAAW,GAAG,OAAO,CAAC,WAAW,CAAC,CAyIzE;AAED,MAAM,WAAW,aAAa;IAC5B,OAAO,EAAE,OAAO,CAAC;IACjB,SAAS,EAAE,MAAM,CAAC;IAClB,SAAS,EAAE,MAAM,CAAC;CACnB;AAED,MAAM,WAAW,aAAa;IAC5B,QAAQ,EAAE,OAAO,CAAC;IAClB,MAAM,EAAE,MAAM,CAAC;IACf,MAAM,EAAE,MAAM,EAAE,CAAC;IACjB,YAAY,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CACvC;AAED,wBAAsB,WAAW,CAAC,MAAM,EAAE,aAAa,GAAG,OAAO,CAAC,aAAa,CAAC,CA6G/E;AAED,MAAM,WAAW,gBAAgB;IAC/B,OAAO,EAAE,OAAO,CAAC;IACjB,SAAS,EAAE,MAAM,CAAC;IAClB,kBAAkB;IAClB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB,uFAAuF;IACvF,MAAM,CAAC,EAAE,MAAM,EAAE,CAAC;IAClB;;;;;;;;;;;mBAWe;IACf,OAAO,CAAC,EAAE,CAAC,IAAI,EAAE,cAAc,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;CACnD;AAED,MAAM,WAAW,cAAc;IAC7B,SAAS,EAAE,MAAM,CAAC;IAClB,MAAM,EAAE,MAAM,EAAE,CAAC;IACjB,SAAS,EAAE,MAAM,CAAC;IAClB,SAAS,EAAE,MAAM,GAAG,IAAI,CAAC;IACzB,gFAAgF;IAChF,WAAW,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CACtC;AAED,MAAM,WAAW,gBAAgB;IAC/B,SAAS,EAAE,MAAM,CAAC;IAClB,MAAM,EAAE,OAAO,CAAC;IAChB,UAAU,EAAE,cAAc,EAAE,CAAC;IAC7B,OAAO,EAAE,MAAM,CAAC;CACjB;AAED,wBAAsB,UAAU,CAAC,MAAM,EAAE,gBAAgB,GAAG,OAAO,CAAC,gBAAgB,CAAC,CAiFpF;AAED;;;;;;;;;GASG;AACH,wBAAgB,UAAU,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAEhD;AAED,qFAAqF;AACrF,wBAAgB,mBAAmB,CAAC,MAAM,EAAE,MAAM,EAAE,GAAG,MAAM,EAAE,CAI9D;AAED,2FAA2F;AAC3F,eAAO,MAAM,oBAAoB,EAAE,SAAS,MAAM,EAAqB,CAAC"}
@@ -291,7 +291,14 @@ async function emptyTrash(params) {
291
291
  await onEmpty(c);
292
292
  }
293
293
  catch (err) {
294
- process.stderr.write(`[trash:empty-run] onEmpty side-effect failed for elementId=${c.elementId}: ${err instanceof Error ? err.message : String(err)}\n`);
294
+ // Task 237 skip the DETACH DELETE so the candidate stays
295
+ // eligible for the next sweep. Surfaces alongside the existing
296
+ // [trash:empty-run] family so an operator's grep catches both
297
+ // candidate listings and skip reasons in one window.
298
+ const id8 = c.elementId.slice(0, 8);
299
+ const message = err instanceof Error ? err.message : String(err);
300
+ process.stderr.write(`[trash:empty-run] reason=onEmpty-failed elementId=${id8} labels=${c.labels.join(",")} message=${message}\n`);
301
+ continue;
295
302
  }
296
303
  }
297
304
  await session.run(`MATCH (n) WHERE elementId(n) = $eid DETACH DELETE n`, { eid: c.elementId });