@rubytech/create-maxy-code 0.1.107 → 0.1.109
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.
- package/package.json +1 -1
- package/payload/platform/config/brand.json +1 -0
- package/payload/platform/plugins/.claude-plugin/marketplace.json +5 -0
- package/payload/platform/plugins/business-assistant/PLUGIN.md +4 -1
- package/payload/platform/plugins/cloudflare/PLUGIN.md +1 -1
- package/payload/platform/plugins/cloudflare/scripts/list-cf-domains.sh +12 -12
- package/payload/platform/plugins/cloudflare/scripts/list-cf-domains.ts +9 -43
- package/payload/platform/plugins/cloudflare/scripts/setup-tunnel.sh +27 -29
- package/payload/platform/plugins/cloudflare/skills/setup-tunnel/SKILL.md +14 -9
- package/payload/platform/plugins/docs/references/platform.md +4 -22
- package/payload/platform/plugins/docs/references/plugins-guide.md +9 -2
- package/payload/platform/plugins/docs/references/troubleshooting.md +4 -316
- package/payload/platform/plugins/venture-studio/.claude-plugin/plugin.json +8 -0
- package/payload/platform/plugins/venture-studio/PLUGIN.md +110 -0
- package/payload/platform/plugins/venture-studio/bin/scaffold.sh +104 -0
- package/payload/platform/plugins/venture-studio/skills/brand-pack/SKILL.md +256 -0
- package/payload/platform/plugins/venture-studio/skills/brand-pack/references/color-psychology.md +118 -0
- package/payload/{premium-plugins/founder-pack → platform/plugins/venture-studio}/skills/investor-data-room/SKILL.md +112 -9
- package/payload/{premium-plugins/founder-pack → platform/plugins/venture-studio}/skills/investor-data-room/references/html-pdf-pipeline.md +1 -1
- package/payload/{premium-plugins/founder-pack → platform/plugins/venture-studio}/skills/investor-data-room/references/termsheet-template.md +1 -1
- package/payload/platform/plugins/venture-studio/skills/investor-data-room/templates/prospectus/index.html +1565 -0
- package/payload/platform/plugins/venture-studio/skills/investor-data-room/templates/prospectus/render-pdf.mjs +91 -0
- package/payload/platform/plugins/venture-studio/skills/investor-data-room/templates/prospectus/term_sheet.html +715 -0
- package/payload/platform/plugins/venture-studio/skills/office-hours/SKILL.md +588 -0
- package/payload/platform/plugins/venture-studio/skills/zero-to-prototype/SKILL.md +332 -0
- package/payload/platform/plugins/venture-studio/skills/zero-to-prototype/references/aarrr-metrics.md +275 -0
- package/payload/platform/plugins/venture-studio/skills/zero-to-prototype/references/assumption-testing.md +93 -0
- package/payload/platform/plugins/venture-studio/skills/zero-to-prototype/references/boolean-search.md +308 -0
- package/payload/platform/plugins/venture-studio/skills/zero-to-prototype/references/build-measure-learn.md +262 -0
- package/payload/platform/plugins/venture-studio/skills/zero-to-prototype/references/business-model-canvas.md +171 -0
- package/payload/platform/plugins/venture-studio/skills/zero-to-prototype/references/commitment-signals.md +246 -0
- package/payload/platform/plugins/venture-studio/skills/zero-to-prototype/references/design-thinking.md +183 -0
- package/payload/platform/plugins/venture-studio/skills/zero-to-prototype/references/earlyvangelist.md +190 -0
- package/payload/platform/plugins/venture-studio/skills/zero-to-prototype/references/first-principles.md +58 -0
- package/payload/platform/plugins/venture-studio/skills/zero-to-prototype/references/fishbone.md +114 -0
- package/payload/platform/plugins/venture-studio/skills/zero-to-prototype/references/five-whys.md +43 -0
- package/payload/platform/plugins/venture-studio/skills/zero-to-prototype/references/ice-scoring.md +237 -0
- package/payload/platform/plugins/venture-studio/skills/zero-to-prototype/references/innovation-accounting.md +290 -0
- package/payload/platform/plugins/venture-studio/skills/zero-to-prototype/references/jtbd.md +105 -0
- package/payload/platform/plugins/venture-studio/skills/zero-to-prototype/references/landing-page.md +361 -0
- package/payload/platform/plugins/venture-studio/skills/zero-to-prototype/references/market-type.md +167 -0
- package/payload/platform/plugins/venture-studio/skills/zero-to-prototype/references/mom-test.md +193 -0
- package/payload/platform/plugins/venture-studio/skills/zero-to-prototype/references/mvp-types.md +200 -0
- package/payload/platform/plugins/venture-studio/skills/zero-to-prototype/references/og-images.md +239 -0
- package/payload/platform/plugins/venture-studio/skills/zero-to-prototype/references/pareto.md +103 -0
- package/payload/platform/plugins/venture-studio/skills/zero-to-prototype/references/persona-development.md +291 -0
- package/payload/platform/plugins/venture-studio/skills/zero-to-prototype/references/pivot-types.md +225 -0
- package/payload/platform/plugins/venture-studio/skills/zero-to-prototype/references/positioning-statement.md +179 -0
- package/payload/platform/plugins/venture-studio/skills/zero-to-prototype/references/prd.md +363 -0
- package/payload/platform/plugins/venture-studio/skills/zero-to-prototype/references/pre-mortem.md +74 -0
- package/payload/platform/plugins/venture-studio/skills/zero-to-prototype/references/problem-validation.md +253 -0
- package/payload/platform/plugins/venture-studio/skills/zero-to-prototype/references/product-market-fit.md +256 -0
- package/payload/platform/plugins/venture-studio/skills/zero-to-prototype/references/research-synthesis.md +276 -0
- package/payload/platform/plugins/venture-studio/skills/zero-to-prototype/references/three-engines-of-growth.md +248 -0
- package/payload/platform/plugins/venture-studio/skills/zero-to-prototype/references/validation-tests.md +89 -0
- package/payload/platform/plugins/venture-studio/skills/zero-to-prototype/references/value-proposition-canvas.md +121 -0
- package/payload/platform/plugins/venture-studio/skills/zero-to-prototype/references/win-loss-analysis.md +242 -0
- package/payload/platform/plugins/venture-studio/skills/zero-to-prototype/references/workflow-mapping.md +271 -0
- package/payload/platform/plugins/work/mcp/dist/cli/project-create-cli.d.ts +2 -0
- package/payload/platform/plugins/work/mcp/dist/cli/project-create-cli.d.ts.map +1 -0
- package/payload/platform/plugins/work/mcp/dist/cli/project-create-cli.js +78 -0
- package/payload/platform/plugins/work/mcp/dist/cli/project-create-cli.js.map +1 -0
- package/payload/platform/scripts/vnc.sh +4 -3
- package/payload/premium-plugins/.claude-plugin/marketplace.json +4 -4
- package/payload/premium-plugins/real-agent/plugins/brochures/skills/property-brochure/references/build.md +4 -5
- package/payload/premium-plugins/venture-studio/.claude-plugin/plugin.json +8 -0
- package/payload/premium-plugins/venture-studio/PLUGIN.md +110 -0
- package/payload/premium-plugins/venture-studio/bin/scaffold.sh +104 -0
- package/payload/premium-plugins/venture-studio/skills/brand-pack/SKILL.md +256 -0
- package/payload/premium-plugins/venture-studio/skills/brand-pack/references/color-psychology.md +118 -0
- package/payload/premium-plugins/venture-studio/skills/investor-data-room/SKILL.md +361 -0
- package/payload/premium-plugins/venture-studio/skills/investor-data-room/references/business-plan-template.md +61 -0
- package/payload/premium-plugins/venture-studio/skills/investor-data-room/references/compliance-research-checklist.md +53 -0
- package/payload/premium-plugins/venture-studio/skills/investor-data-room/references/data-room-structure.md +77 -0
- package/payload/premium-plugins/venture-studio/skills/investor-data-room/references/deck-blueprint-template.md +38 -0
- package/payload/premium-plugins/venture-studio/skills/investor-data-room/references/design-tokens-application.md +79 -0
- package/payload/premium-plugins/venture-studio/skills/investor-data-room/references/html-pdf-pipeline.md +236 -0
- package/payload/premium-plugins/venture-studio/skills/investor-data-room/references/internal-workings-scrub.md +33 -0
- package/payload/premium-plugins/venture-studio/skills/investor-data-room/references/termsheet-template.md +88 -0
- package/payload/premium-plugins/venture-studio/skills/investor-data-room/templates/prospectus/index.html +1565 -0
- package/payload/premium-plugins/venture-studio/skills/investor-data-room/templates/prospectus/render-pdf.mjs +91 -0
- package/payload/premium-plugins/venture-studio/skills/investor-data-room/templates/prospectus/term_sheet.html +715 -0
- package/payload/premium-plugins/venture-studio/skills/office-hours/SKILL.md +588 -0
- package/payload/premium-plugins/venture-studio/skills/zero-to-prototype/SKILL.md +332 -0
- package/payload/premium-plugins/venture-studio/skills/zero-to-prototype/references/aarrr-metrics.md +275 -0
- package/payload/premium-plugins/venture-studio/skills/zero-to-prototype/references/assumption-testing.md +93 -0
- package/payload/premium-plugins/venture-studio/skills/zero-to-prototype/references/boolean-search.md +308 -0
- package/payload/premium-plugins/venture-studio/skills/zero-to-prototype/references/build-measure-learn.md +262 -0
- package/payload/premium-plugins/venture-studio/skills/zero-to-prototype/references/business-model-canvas.md +171 -0
- package/payload/premium-plugins/venture-studio/skills/zero-to-prototype/references/commitment-signals.md +246 -0
- package/payload/premium-plugins/venture-studio/skills/zero-to-prototype/references/design-thinking.md +183 -0
- package/payload/premium-plugins/venture-studio/skills/zero-to-prototype/references/earlyvangelist.md +190 -0
- package/payload/premium-plugins/venture-studio/skills/zero-to-prototype/references/first-principles.md +58 -0
- package/payload/premium-plugins/venture-studio/skills/zero-to-prototype/references/fishbone.md +114 -0
- package/payload/premium-plugins/venture-studio/skills/zero-to-prototype/references/five-whys.md +43 -0
- package/payload/premium-plugins/venture-studio/skills/zero-to-prototype/references/ice-scoring.md +237 -0
- package/payload/premium-plugins/venture-studio/skills/zero-to-prototype/references/innovation-accounting.md +290 -0
- package/payload/premium-plugins/venture-studio/skills/zero-to-prototype/references/jtbd.md +105 -0
- package/payload/premium-plugins/venture-studio/skills/zero-to-prototype/references/landing-page.md +361 -0
- package/payload/premium-plugins/venture-studio/skills/zero-to-prototype/references/market-type.md +167 -0
- package/payload/premium-plugins/venture-studio/skills/zero-to-prototype/references/mom-test.md +193 -0
- package/payload/premium-plugins/venture-studio/skills/zero-to-prototype/references/mvp-types.md +200 -0
- package/payload/premium-plugins/venture-studio/skills/zero-to-prototype/references/og-images.md +239 -0
- package/payload/premium-plugins/venture-studio/skills/zero-to-prototype/references/pareto.md +103 -0
- package/payload/premium-plugins/venture-studio/skills/zero-to-prototype/references/persona-development.md +291 -0
- package/payload/premium-plugins/venture-studio/skills/zero-to-prototype/references/pivot-types.md +225 -0
- package/payload/premium-plugins/venture-studio/skills/zero-to-prototype/references/positioning-statement.md +179 -0
- package/payload/premium-plugins/venture-studio/skills/zero-to-prototype/references/prd.md +363 -0
- package/payload/premium-plugins/venture-studio/skills/zero-to-prototype/references/pre-mortem.md +74 -0
- package/payload/premium-plugins/venture-studio/skills/zero-to-prototype/references/problem-validation.md +253 -0
- package/payload/premium-plugins/venture-studio/skills/zero-to-prototype/references/product-market-fit.md +256 -0
- package/payload/premium-plugins/venture-studio/skills/zero-to-prototype/references/research-synthesis.md +276 -0
- package/payload/premium-plugins/venture-studio/skills/zero-to-prototype/references/three-engines-of-growth.md +248 -0
- package/payload/premium-plugins/venture-studio/skills/zero-to-prototype/references/validation-tests.md +89 -0
- package/payload/premium-plugins/venture-studio/skills/zero-to-prototype/references/value-proposition-canvas.md +121 -0
- package/payload/premium-plugins/venture-studio/skills/zero-to-prototype/references/win-loss-analysis.md +242 -0
- package/payload/premium-plugins/venture-studio/skills/zero-to-prototype/references/workflow-mapping.md +271 -0
- package/payload/server/{chunk-TZD23CX6.js → chunk-FSPPVVWM.js} +1347 -505
- package/payload/server/maxy-edge.js +21 -259
- package/payload/server/public/assets/{ChatInput-DJsqm_Gf.js → ChatInput-CsnIedhS.js} +1 -5
- package/payload/server/public/assets/{Checkbox-DGZG9BKc.js → Checkbox-CWugFyFT.js} +1 -1
- package/payload/server/public/assets/admin-D3gZuyUn.js +217 -0
- package/payload/server/public/assets/{architectureDiagram-Q4EWVU46-Dw16BhiX.js → architectureDiagram-Q4EWVU46-CeTRKWDb.js} +1 -1
- package/payload/server/public/assets/{blockDiagram-DXYQGD6D-DIOpmf5Y.js → blockDiagram-DXYQGD6D-DeeIX5U3.js} +1 -1
- package/payload/server/public/assets/{c4Diagram-AHTNJAMY-Tdb_HZeX.js → c4Diagram-AHTNJAMY-CFsqZuil.js} +1 -1
- package/payload/server/public/assets/channel-CrSx5mnG.js +1 -0
- package/payload/server/public/assets/{chunk-336JU56O-CebpwDDe.js → chunk-336JU56O-DpIXuFM0.js} +2 -2
- package/payload/server/public/assets/{chunk-426QAEUC-BtRCmfDU.js → chunk-426QAEUC-Qk8qqrvA.js} +1 -1
- package/payload/server/public/assets/{chunk-4TB4RGXK-BZ3GEWs3.js → chunk-4TB4RGXK-DtM8-CUn.js} +1 -1
- package/payload/server/public/assets/{chunk-5FUZZQ4R-iDI6Xu0U.js → chunk-5FUZZQ4R-CrSQ4ySU.js} +1 -1
- package/payload/server/public/assets/{chunk-5PVQY5BW-SQD_EpYa.js → chunk-5PVQY5BW-Bn2nQwdj.js} +1 -1
- package/payload/server/public/assets/{chunk-EDXVE4YY-CtCcA7_e.js → chunk-EDXVE4YY-CzCPnR0P.js} +1 -1
- package/payload/server/public/assets/{chunk-ENJZ2VHE-pXVGVCbb.js → chunk-ENJZ2VHE-CgZj9RoG.js} +1 -1
- package/payload/server/public/assets/{chunk-ICPOFSXX-Dkzg9o2N.js → chunk-ICPOFSXX-wy-eNjwW.js} +1 -1
- package/payload/server/public/assets/{chunk-OYMX7WX6-1ZZWzf9F.js → chunk-OYMX7WX6-CMmJtL8S.js} +1 -1
- package/payload/server/public/assets/{chunk-U2HBQHQK-CpQ3kzO0.js → chunk-U2HBQHQK-CFCW7OaT.js} +1 -1
- package/payload/server/public/assets/{chunk-X2U36JSP-C2LkxroC.js → chunk-X2U36JSP-Bgh-CJSN.js} +1 -1
- package/payload/server/public/assets/{chunk-YZCP3GAM-Bl5jBOt5.js → chunk-YZCP3GAM-BXKwZ4vN.js} +1 -1
- package/payload/server/public/assets/{chunk-ZZ45TVLE-CHtnptPS.js → chunk-ZZ45TVLE-BiOuK5NP.js} +1 -1
- package/payload/server/public/assets/classDiagram-6PBFFD2Q-C3IDJsqN.js +1 -0
- package/payload/server/public/assets/classDiagram-v2-HSJHXN6E-BOIrJ5Zb.js +1 -0
- package/payload/server/public/assets/clone-P8Fkz7JD.js +1 -0
- package/payload/server/public/assets/{dagre-BziN0Nkh.js → dagre-DyCxa9Q2.js} +1 -1
- package/payload/server/public/assets/{dagre-KV5264BT-B7OG1g1Y.js → dagre-KV5264BT-Cffo_hmf.js} +1 -1
- package/payload/server/public/assets/data-DRfwJPja.js +1 -0
- package/payload/server/public/assets/{diagram-5BDNPKRD-Bf31nIDs.js → diagram-5BDNPKRD-Bt93Du3V.js} +1 -1
- package/payload/server/public/assets/{diagram-G4DWMVQ6-DQu85hhH.js → diagram-G4DWMVQ6-v4r-tBsC.js} +1 -1
- package/payload/server/public/assets/{diagram-MMDJMWI5-5tstbs4y.js → diagram-MMDJMWI5-DKrVPOP-.js} +1 -1
- package/payload/server/public/assets/{diagram-TYMM5635--MOV1U4o.js → diagram-TYMM5635-B1PzWQb9.js} +1 -1
- package/payload/server/public/assets/{erDiagram-SMLLAGMA-BtAnOJmd.js → erDiagram-SMLLAGMA-ClQzsQAs.js} +1 -1
- package/payload/server/public/assets/{flowDiagram-DWJPFMVM-CybiCIih.js → flowDiagram-DWJPFMVM-h3IbpN_7.js} +1 -1
- package/payload/server/public/assets/{ganttDiagram-T4ZO3ILL-B44jN7Cy.js → ganttDiagram-T4ZO3ILL-ClaxOJi8.js} +1 -1
- package/payload/server/public/assets/{gitGraphDiagram-UUTBAWPF-CT7iYcwg.js → gitGraphDiagram-UUTBAWPF-pM-U4ClD.js} +1 -1
- package/payload/server/public/assets/graph-B4FFYwus.js +1 -0
- package/payload/server/public/assets/graph-labels-Cpk9Ktt0.js +1 -0
- package/payload/server/public/assets/{graphlib-DP4o0pYL.js → graphlib-CwimOv_M.js} +1 -1
- package/payload/server/public/assets/{infoDiagram-42DDH7IO-CvxFpFHN.js → infoDiagram-42DDH7IO-DcO3Giwe.js} +1 -1
- package/payload/server/public/assets/{ishikawaDiagram-UXIWVN3A-CUwWLPeR.js → ishikawaDiagram-UXIWVN3A-BcdSL5VS.js} +1 -1
- package/payload/server/public/assets/{journeyDiagram-VCZTEJTY-y-OnGgLi.js → journeyDiagram-VCZTEJTY-CYahHYzf.js} +1 -1
- package/payload/server/public/assets/{kanban-definition-6JOO6SKY-BhrSe8R4.js → kanban-definition-6JOO6SKY-Cl5KhoS4.js} +1 -1
- package/payload/server/public/assets/lib--yuBd0Xi.js +33 -0
- package/payload/server/public/assets/{line-C9uS7z4J.js → line-d_2oTxLp.js} +1 -1
- package/payload/server/public/assets/{mermaid-parser.core-Cg4ZdKp-.js → mermaid-parser.core-CMMDGv3x.js} +1 -1
- package/payload/server/public/assets/{mermaid.core-BHdKOsex.js → mermaid.core-DK9ENGbr.js} +3 -3
- package/payload/server/public/assets/{mindmap-definition-QFDTVHPH-NBYiXHo7.js → mindmap-definition-QFDTVHPH-mA3x3MFG.js} +1 -1
- package/payload/server/public/assets/page-CgDmg_fX.js +1 -0
- package/payload/server/public/assets/{page-D9YpwhIu.js → page-D9lluVl7.js} +2 -2
- package/payload/server/public/assets/{pieDiagram-DEJITSTG-CbojC64C.js → pieDiagram-DEJITSTG-CW1nGFlY.js} +1 -1
- package/payload/server/public/assets/public-DdLMkNdS.js +7 -0
- package/payload/server/public/assets/{quadrantDiagram-34T5L4WZ-CnnoZXcL.js → quadrantDiagram-34T5L4WZ-DI5igJhR.js} +1 -1
- package/payload/server/public/assets/{requirementDiagram-MS252O5E-DFRFRtyJ.js → requirementDiagram-MS252O5E-DyVeI4e_.js} +1 -1
- package/payload/server/public/assets/{sankeyDiagram-XADWPNL6-DU5gcpzO.js → sankeyDiagram-XADWPNL6-CCTTBYKY.js} +1 -1
- package/payload/server/public/assets/{sequenceDiagram-FGHM5R23-BN7HZ6Hq.js → sequenceDiagram-FGHM5R23-hVVf35ly.js} +1 -1
- package/payload/server/public/assets/{stateDiagram-FHFEXIEX-DG6cCjvg.js → stateDiagram-FHFEXIEX-CSLuQDUX.js} +1 -1
- package/payload/server/public/assets/stateDiagram-v2-QKLJ7IA2-C5JejhQU.js +1 -0
- package/payload/server/public/assets/{timeline-definition-GMOUNBTQ-qmV-8SEV.js → timeline-definition-GMOUNBTQ-sxC3W8Q4.js} +1 -1
- package/payload/server/public/assets/{vennDiagram-DHZGUBPP-BMXQ1s3n.js → vennDiagram-DHZGUBPP-Ro-ton6Z.js} +1 -1
- package/payload/server/public/assets/{wardleyDiagram-NUSXRM2D-CoAdP1Gw.js → wardleyDiagram-NUSXRM2D-T-dKD0Zx.js} +1 -1
- package/payload/server/public/assets/{xychartDiagram-5P7HB3ND-CZlfGTEn.js → xychartDiagram-5P7HB3ND-CfProPts.js} +1 -1
- package/payload/server/public/data.html +4 -4
- package/payload/server/public/graph.html +5 -5
- package/payload/server/public/index.html +7 -7
- package/payload/server/public/public.html +4 -4
- package/payload/server/server.js +300 -1393
- package/payload/platform/plugins/cloudflare/scripts/_stream-log.sh +0 -154
- package/payload/premium-plugins/founder-pack/.claude-plugin/plugin.json +0 -8
- package/payload/server/chunk-BDFOTLPW.js +0 -759
- package/payload/server/chunk-JRBCOVA4.js +0 -1305
- package/payload/server/cloudflare-task-tracker-M5ONAGUT.js +0 -22
- package/payload/server/public/assets/admin-BvzMvMGo.js +0 -217
- package/payload/server/public/assets/channel-DThrH4QF.js +0 -1
- package/payload/server/public/assets/classDiagram-6PBFFD2Q-BC6oGTNX.js +0 -1
- package/payload/server/public/assets/classDiagram-v2-HSJHXN6E-COwC5Umh.js +0 -1
- package/payload/server/public/assets/clone-Chp7hvnA.js +0 -1
- package/payload/server/public/assets/data-BAgaPs4j.js +0 -1
- package/payload/server/public/assets/graph-Cma7EArf.js +0 -1
- package/payload/server/public/assets/graph-labels-DyKk6Sxf.js +0 -1
- package/payload/server/public/assets/lib-CpkYtEDz.js +0 -29
- package/payload/server/public/assets/page-B4IWl3aZ.js +0 -1
- package/payload/server/public/assets/public-BtOXjy3A.js +0 -8
- package/payload/server/public/assets/stateDiagram-v2-QKLJ7IA2-CvVd1Q_q.js +0 -1
- /package/payload/{premium-plugins/founder-pack → platform/plugins/venture-studio}/skills/investor-data-room/references/business-plan-template.md +0 -0
- /package/payload/{premium-plugins/founder-pack → platform/plugins/venture-studio}/skills/investor-data-room/references/compliance-research-checklist.md +0 -0
- /package/payload/{premium-plugins/founder-pack → platform/plugins/venture-studio}/skills/investor-data-room/references/data-room-structure.md +0 -0
- /package/payload/{premium-plugins/founder-pack → platform/plugins/venture-studio}/skills/investor-data-room/references/deck-blueprint-template.md +0 -0
- /package/payload/{premium-plugins/founder-pack → platform/plugins/venture-studio}/skills/investor-data-room/references/design-tokens-application.md +0 -0
- /package/payload/{premium-plugins/founder-pack → platform/plugins/venture-studio}/skills/investor-data-room/references/internal-workings-scrub.md +0 -0
|
@@ -1,7 +1,3 @@
|
|
|
1
|
-
import {
|
|
2
|
-
getConversationOwner
|
|
3
|
-
} from "./chunk-JRBCOVA4.js";
|
|
4
|
-
|
|
5
1
|
// node_modules/hono/dist/compose.js
|
|
6
2
|
var compose = (middleware, onError, onNotFound) => {
|
|
7
3
|
return (context, next) => {
|
|
@@ -2632,7 +2628,8 @@ import { resolve, join } from "path";
|
|
|
2632
2628
|
import { existsSync, readFileSync } from "fs";
|
|
2633
2629
|
var configDirName = ".maxy";
|
|
2634
2630
|
var commercialMode = false;
|
|
2635
|
-
var
|
|
2631
|
+
var bundleMode = "none";
|
|
2632
|
+
var bundleAllowlist = [];
|
|
2636
2633
|
var vncDisplayNum = 99;
|
|
2637
2634
|
var rfbPortNum = 5900;
|
|
2638
2635
|
var websockifyPortNum = 6080;
|
|
@@ -2652,7 +2649,20 @@ if (platformRoot) {
|
|
|
2652
2649
|
}
|
|
2653
2650
|
if (typeof brand.configDir === "string") configDirName = brand.configDir;
|
|
2654
2651
|
if (brand.commercialMode === true) commercialMode = true;
|
|
2655
|
-
if (brand.shipsPremiumBundles === true)
|
|
2652
|
+
if (brand.shipsPremiumBundles === true) {
|
|
2653
|
+
bundleMode = "all";
|
|
2654
|
+
} else if (Array.isArray(brand.shipsPremiumBundles)) {
|
|
2655
|
+
const arr = brand.shipsPremiumBundles;
|
|
2656
|
+
for (const entry of arr) {
|
|
2657
|
+
if (typeof entry !== "string" || entry.length === 0) {
|
|
2658
|
+
throw new Error(
|
|
2659
|
+
`brand.json at ${brandPath} shipsPremiumBundles array contains invalid entry: ${JSON.stringify(entry)}`
|
|
2660
|
+
);
|
|
2661
|
+
}
|
|
2662
|
+
}
|
|
2663
|
+
bundleMode = "allowlist";
|
|
2664
|
+
bundleAllowlist = arr;
|
|
2665
|
+
}
|
|
2656
2666
|
if (typeof brand.vncDisplay === "number") vncDisplayNum = brand.vncDisplay;
|
|
2657
2667
|
const brandLabel = configDirName.replace(/^\./, "");
|
|
2658
2668
|
const required = [
|
|
@@ -2676,7 +2686,8 @@ if (platformRoot) {
|
|
|
2676
2686
|
var MAXY_DIR = resolve(homedir(), configDirName);
|
|
2677
2687
|
var BRAND_NAME = configDirName.replace(/^\./, "");
|
|
2678
2688
|
var COMMERCIAL_MODE = commercialMode;
|
|
2679
|
-
var
|
|
2689
|
+
var BUNDLE_MODE = bundleMode;
|
|
2690
|
+
var BUNDLE_ALLOWLIST = bundleAllowlist;
|
|
2680
2691
|
var VNC_DISPLAY = `:${vncDisplayNum}`;
|
|
2681
2692
|
var RFB_PORT = rfbPortNum;
|
|
2682
2693
|
var WEBSOCKIFY_PORT = websockifyPortNum;
|
|
@@ -2739,9 +2750,6 @@ function httpLog(fields) {
|
|
|
2739
2750
|
function websockifyLog(event, fields = {}) {
|
|
2740
2751
|
console.error(`[websockify] ${formatKv({ event, ...fields })}`);
|
|
2741
2752
|
}
|
|
2742
|
-
function browserViewerLog(event, fields = {}) {
|
|
2743
|
-
console.error(`[browser-viewer] ${formatKv({ event, ...fields })}`);
|
|
2744
|
-
}
|
|
2745
2753
|
|
|
2746
2754
|
// app/lib/password-strength.ts
|
|
2747
2755
|
function validatePasswordStrength(password) {
|
|
@@ -3657,8 +3665,24 @@ function readBundleSubPlugins(bundlePath) {
|
|
|
3657
3665
|
return subs;
|
|
3658
3666
|
}
|
|
3659
3667
|
function walkPremiumBundles() {
|
|
3660
|
-
|
|
3661
|
-
|
|
3668
|
+
const TAG = "[premium-auto-deliver]";
|
|
3669
|
+
if (BUNDLE_MODE === "none") {
|
|
3670
|
+
console.log(`${TAG} gate brand=${BRAND_NAME} mode=none bundles=[]`);
|
|
3671
|
+
return [];
|
|
3672
|
+
}
|
|
3673
|
+
const onDisk = walkPremiumBundlesOnDisk();
|
|
3674
|
+
if (BUNDLE_MODE === "all") {
|
|
3675
|
+
console.log(`${TAG} gate brand=${BRAND_NAME} mode=all bundles=[${onDisk.map((b) => b.bundle).join(",")}]`);
|
|
3676
|
+
return onDisk;
|
|
3677
|
+
}
|
|
3678
|
+
const allow = new Set(BUNDLE_ALLOWLIST);
|
|
3679
|
+
const kept = onDisk.filter((b) => allow.has(b.bundle));
|
|
3680
|
+
const onDiskNames = new Set(onDisk.map((b) => b.bundle));
|
|
3681
|
+
const missing = BUNDLE_ALLOWLIST.filter((name) => !onDiskNames.has(name));
|
|
3682
|
+
console.log(
|
|
3683
|
+
`${TAG} gate brand=${BRAND_NAME} mode=allowlist bundles=[${kept.map((b) => b.bundle).join(",")}]` + (missing.length > 0 ? ` missing=[${missing.join(",")}]` : "")
|
|
3684
|
+
);
|
|
3685
|
+
return kept;
|
|
3662
3686
|
}
|
|
3663
3687
|
function walkPremiumBundlesOnDisk() {
|
|
3664
3688
|
const stagingRoot = resolve4(PLATFORM_ROOT2, "../premium-plugins");
|
|
@@ -3733,7 +3757,7 @@ function autoDeliverPremiumPlugins() {
|
|
|
3733
3757
|
}
|
|
3734
3758
|
}
|
|
3735
3759
|
}
|
|
3736
|
-
function reconcileEnabledPlugins(accountDir, config) {
|
|
3760
|
+
function reconcileEnabledPlugins(accountDir, config, accountId) {
|
|
3737
3761
|
const TAG = "[premium-auto-deliver]";
|
|
3738
3762
|
if (!accountDir || !config) return;
|
|
3739
3763
|
const bundles = walkPremiumBundles();
|
|
@@ -3759,7 +3783,9 @@ function reconcileEnabledPlugins(accountDir, config) {
|
|
|
3759
3783
|
currentSet.add(sub);
|
|
3760
3784
|
added.push(sub);
|
|
3761
3785
|
}
|
|
3762
|
-
console.log(
|
|
3786
|
+
console.log(
|
|
3787
|
+
`${TAG} stamp accountId=${accountId ?? "unknown"} brand=${BRAND_NAME} mode=${BUNDLE_MODE} bundles=[${bundleNames.join(",")}] added=[${added.join(",")}]`
|
|
3788
|
+
);
|
|
3763
3789
|
if (added.length === 0) return;
|
|
3764
3790
|
const merged = [...currentSet];
|
|
3765
3791
|
const configPath = resolve4(accountDir, "account.json");
|
|
@@ -3776,11 +3802,13 @@ function reconcileEnabledPlugins(accountDir, config) {
|
|
|
3776
3802
|
function cleanupLeakedPremiumSubs(accountDir, config, accountId) {
|
|
3777
3803
|
const TAG = "[premium-auto-deliver]";
|
|
3778
3804
|
if (!accountDir || !config) return;
|
|
3779
|
-
if (
|
|
3805
|
+
if (BUNDLE_MODE === "all") return;
|
|
3780
3806
|
const current = Array.isArray(config.enabledPlugins) ? config.enabledPlugins : [];
|
|
3781
3807
|
if (current.length === 0) return;
|
|
3808
|
+
const allow = new Set(BUNDLE_ALLOWLIST);
|
|
3782
3809
|
const leakedNames = /* @__PURE__ */ new Set();
|
|
3783
|
-
for (const { subs } of walkPremiumBundlesOnDisk()) {
|
|
3810
|
+
for (const { bundle, subs } of walkPremiumBundlesOnDisk()) {
|
|
3811
|
+
if (BUNDLE_MODE === "allowlist" && allow.has(bundle)) continue;
|
|
3784
3812
|
for (const sub of subs) leakedNames.add(sub);
|
|
3785
3813
|
}
|
|
3786
3814
|
if (leakedNames.size === 0) return;
|
|
@@ -3841,6 +3869,1285 @@ function requirePortEnv(envName, options = {}) {
|
|
|
3841
3869
|
return parsed;
|
|
3842
3870
|
}
|
|
3843
3871
|
|
|
3872
|
+
// app/lib/neo4j-store.ts
|
|
3873
|
+
import neo4j from "neo4j-driver";
|
|
3874
|
+
import { randomUUID } from "crypto";
|
|
3875
|
+
import { readFileSync as readFileSync6, readdirSync as readdirSync3, existsSync as existsSync6, openSync, readSync, closeSync, statSync as statSync3 } from "fs";
|
|
3876
|
+
import { resolve as resolve5 } from "path";
|
|
3877
|
+
var PLATFORM_ROOT3 = process.env.MAXY_PLATFORM_ROOT ?? resolve5(process.cwd(), "..");
|
|
3878
|
+
var driver = null;
|
|
3879
|
+
function readPassword() {
|
|
3880
|
+
if (process.env.NEO4J_PASSWORD) return process.env.NEO4J_PASSWORD;
|
|
3881
|
+
const passwordFile = resolve5(PLATFORM_ROOT3, "config/.neo4j-password");
|
|
3882
|
+
try {
|
|
3883
|
+
return readFileSync6(passwordFile, "utf-8").trim();
|
|
3884
|
+
} catch {
|
|
3885
|
+
throw new Error(
|
|
3886
|
+
`Neo4j password not found. Expected at ${passwordFile} or in NEO4J_PASSWORD env var.`
|
|
3887
|
+
);
|
|
3888
|
+
}
|
|
3889
|
+
}
|
|
3890
|
+
function getDriver() {
|
|
3891
|
+
if (!driver) {
|
|
3892
|
+
const uri = process.env.NEO4J_URI;
|
|
3893
|
+
if (!uri) {
|
|
3894
|
+
throw new Error(
|
|
3895
|
+
"[ui/neo4j-store] NEO4J_URI unset \u2014 refusing to default to bolt://localhost:7687"
|
|
3896
|
+
);
|
|
3897
|
+
}
|
|
3898
|
+
const user = process.env.NEO4J_USER ?? "neo4j";
|
|
3899
|
+
const password = readPassword();
|
|
3900
|
+
console.error(`[ui/neo4j-store] resolved neo4j_uri=${uri}`);
|
|
3901
|
+
driver = neo4j.driver(uri, neo4j.auth.basic(user, password), {
|
|
3902
|
+
maxConnectionPoolSize: 5
|
|
3903
|
+
});
|
|
3904
|
+
}
|
|
3905
|
+
return driver;
|
|
3906
|
+
}
|
|
3907
|
+
function getSession() {
|
|
3908
|
+
return getDriver().session();
|
|
3909
|
+
}
|
|
3910
|
+
async function runAdminUserSelfHeal(args) {
|
|
3911
|
+
const { selfHealAdminUser } = await import("./adminuser-self-heal-QAWOZ3JV.js");
|
|
3912
|
+
return selfHealAdminUser({ driver: getDriver(), ...args });
|
|
3913
|
+
}
|
|
3914
|
+
process.on("SIGINT", async () => {
|
|
3915
|
+
if (driver) {
|
|
3916
|
+
await driver.close();
|
|
3917
|
+
driver = null;
|
|
3918
|
+
}
|
|
3919
|
+
});
|
|
3920
|
+
var OLLAMA_URL = process.env.OLLAMA_URL ?? "http://localhost:11434";
|
|
3921
|
+
var EMBED_MODEL = process.env.EMBED_MODEL ?? "nomic-embed-text";
|
|
3922
|
+
async function embed(text) {
|
|
3923
|
+
const res = await fetch(`${OLLAMA_URL}/api/embed`, {
|
|
3924
|
+
method: "POST",
|
|
3925
|
+
headers: { "Content-Type": "application/json" },
|
|
3926
|
+
body: JSON.stringify({ model: EMBED_MODEL, input: text }),
|
|
3927
|
+
signal: AbortSignal.timeout(5e3)
|
|
3928
|
+
});
|
|
3929
|
+
if (!res.ok) {
|
|
3930
|
+
const body = await res.text();
|
|
3931
|
+
throw new Error(`Ollama embedding failed (${res.status}): ${body}`);
|
|
3932
|
+
}
|
|
3933
|
+
const data = await res.json();
|
|
3934
|
+
return data.embeddings[0];
|
|
3935
|
+
}
|
|
3936
|
+
var sessionStoreRef = null;
|
|
3937
|
+
function getCachedConversationId(cacheKey2) {
|
|
3938
|
+
return sessionStoreRef?.get(cacheKey2)?.conversationId;
|
|
3939
|
+
}
|
|
3940
|
+
function cacheConversationId(cacheKey2, conversationId) {
|
|
3941
|
+
const session = sessionStoreRef?.get(cacheKey2);
|
|
3942
|
+
if (session) {
|
|
3943
|
+
session.conversationId = conversationId;
|
|
3944
|
+
}
|
|
3945
|
+
}
|
|
3946
|
+
var GREETING_DIRECTIVE = "[New session. Greet the visitor.]";
|
|
3947
|
+
async function ensureConversation(accountId, agentType, cacheKey2, visitorId, agentSlug, userId, channelOverride, channelAddressOverride) {
|
|
3948
|
+
const cached = getCachedConversationId(cacheKey2);
|
|
3949
|
+
if (cached) return { conversationId: cached, created: false };
|
|
3950
|
+
const conversationId = randomUUID();
|
|
3951
|
+
const channel = channelOverride ?? (cacheKey2.startsWith("whatsapp:") ? "whatsapp" : cacheKey2.startsWith("telegram:") ? "telegram" : "webchat");
|
|
3952
|
+
const channelAddress = channelAddressOverride ?? (cacheKey2.startsWith("whatsapp:") ? cacheKey2.slice("whatsapp:".length) : cacheKey2.startsWith("telegram:") ? cacheKey2.slice("telegram:".length) : conversationId);
|
|
3953
|
+
const conversationSublabel = agentType === "admin" ? "AdminConversation" : "PublicConversation";
|
|
3954
|
+
const handlerSlug = agentSlug ?? (agentType === "admin" ? "admin" : null);
|
|
3955
|
+
const wantsHandledByEdge = !!handlerSlug;
|
|
3956
|
+
const handledByClause = wantsHandledByEdge ? `WITH c, created
|
|
3957
|
+
OPTIONAL MATCH (a:Agent {accountId: $accountId, slug: $handlerSlug})
|
|
3958
|
+
FOREACH (_ IN CASE WHEN a IS NULL THEN [] ELSE [1] END | MERGE (c)-[:HANDLED_BY]->(a))
|
|
3959
|
+
RETURN c.conversationId AS conversationId, created AS created, a IS NOT NULL AS handledBy` : `RETURN c.conversationId AS conversationId, created AS created, false AS handledBy`;
|
|
3960
|
+
const session = getSession();
|
|
3961
|
+
try {
|
|
3962
|
+
const result = await session.run(
|
|
3963
|
+
`OPTIONAL MATCH (prior:Conversation {accountId: $accountId, channel: $channel, channelAddress: $channelAddress})
|
|
3964
|
+
WITH prior, coalesce(prior.conversationId, $conversationId) AS finalConvId, prior IS NULL AS created
|
|
3965
|
+
MERGE (c:Conversation {conversationId: finalConvId})
|
|
3966
|
+
ON CREATE SET
|
|
3967
|
+
c:${conversationSublabel},
|
|
3968
|
+
c.accountId = $accountId,
|
|
3969
|
+
c.agentType = $agentType,
|
|
3970
|
+
c.channel = $channel,
|
|
3971
|
+
c.channelAddress = $channelAddress,
|
|
3972
|
+
${visitorId ? "c.visitorId = $visitorId," : ""}
|
|
3973
|
+
${agentSlug ? "c.agentSlug = $agentSlug," : ""}
|
|
3974
|
+
${userId ? "c.userId = $userId," : ""}
|
|
3975
|
+
c.createdAt = datetime(),
|
|
3976
|
+
c.updatedAt = datetime()
|
|
3977
|
+
ON MATCH SET
|
|
3978
|
+
c.updatedAt = datetime()
|
|
3979
|
+
${handledByClause}`,
|
|
3980
|
+
{
|
|
3981
|
+
conversationId,
|
|
3982
|
+
accountId,
|
|
3983
|
+
agentType,
|
|
3984
|
+
channel,
|
|
3985
|
+
channelAddress,
|
|
3986
|
+
...visitorId ? { visitorId } : {},
|
|
3987
|
+
...agentSlug ? { agentSlug } : {},
|
|
3988
|
+
...handlerSlug ? { handlerSlug } : {},
|
|
3989
|
+
...userId ? { userId } : {}
|
|
3990
|
+
}
|
|
3991
|
+
);
|
|
3992
|
+
const id = result.records[0]?.get("conversationId");
|
|
3993
|
+
const created = result.records[0]?.get("created") === true;
|
|
3994
|
+
if (id) {
|
|
3995
|
+
cacheConversationId(cacheKey2, id);
|
|
3996
|
+
console.error(`[session] ${(/* @__PURE__ */ new Date()).toISOString()} conversation attributed: conversationId=${id.slice(0, 8)}\u2026 userId=${userId ?? "none"} ${agentType}/${accountId.slice(0, 8)}\u2026 sublabel=${conversationSublabel}`);
|
|
3997
|
+
if (wantsHandledByEdge) {
|
|
3998
|
+
const handled = result.records[0]?.get("handledBy");
|
|
3999
|
+
const id8 = id.slice(0, 8);
|
|
4000
|
+
if (handled === true) {
|
|
4001
|
+
console.error(`[agent-graph] handled-by-write conversationId=${id8} slug=${handlerSlug}`);
|
|
4002
|
+
} else {
|
|
4003
|
+
console.error(`[agent-graph] handled-by-skip reason=no-agent-node conversationId=${id8} slug=${handlerSlug}`);
|
|
4004
|
+
}
|
|
4005
|
+
}
|
|
4006
|
+
}
|
|
4007
|
+
return { conversationId: id ?? null, created };
|
|
4008
|
+
} catch (err) {
|
|
4009
|
+
console.error(`[persist] ensureConversation failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
4010
|
+
return { conversationId: null, created: false };
|
|
4011
|
+
} finally {
|
|
4012
|
+
await session.close();
|
|
4013
|
+
}
|
|
4014
|
+
}
|
|
4015
|
+
var HEX_COLOR_RE = /^#[0-9a-fA-F]{3,8}$/;
|
|
4016
|
+
async function fetchBranding(accountId) {
|
|
4017
|
+
const session = getSession();
|
|
4018
|
+
try {
|
|
4019
|
+
const result = await session.run(
|
|
4020
|
+
`MATCH (b:LocalBusiness {accountId: $accountId})
|
|
4021
|
+
OPTIONAL MATCH (b)-[:HAS_BRAND_ASSET]->(logo:ImageObject {purpose: "logo"})
|
|
4022
|
+
OPTIONAL MATCH (b)-[:HAS_BRAND_ASSET]->(icon:ImageObject {purpose: "icon"})
|
|
4023
|
+
RETURN b.name AS name,
|
|
4024
|
+
b.primaryColor AS primaryColor,
|
|
4025
|
+
b.accentColor AS accentColor,
|
|
4026
|
+
b.backgroundColor AS backgroundColor,
|
|
4027
|
+
b.tagline AS tagline,
|
|
4028
|
+
logo.contentUrl AS logoUrl,
|
|
4029
|
+
icon.contentUrl AS faviconUrl`,
|
|
4030
|
+
{ accountId },
|
|
4031
|
+
{ timeout: 2e3 }
|
|
4032
|
+
);
|
|
4033
|
+
const record = result.records[0];
|
|
4034
|
+
if (!record) return null;
|
|
4035
|
+
const name = record.get("name");
|
|
4036
|
+
if (!name) return null;
|
|
4037
|
+
const primaryColor = record.get("primaryColor");
|
|
4038
|
+
const accentColor = record.get("accentColor");
|
|
4039
|
+
const backgroundColor = record.get("backgroundColor");
|
|
4040
|
+
const tagline = record.get("tagline");
|
|
4041
|
+
const logoUrl = record.get("logoUrl");
|
|
4042
|
+
const faviconUrl = record.get("faviconUrl");
|
|
4043
|
+
const hasBranding = primaryColor || accentColor || backgroundColor || tagline || logoUrl || faviconUrl;
|
|
4044
|
+
if (!hasBranding) return null;
|
|
4045
|
+
const branding = { name };
|
|
4046
|
+
if (primaryColor && HEX_COLOR_RE.test(primaryColor)) branding.primaryColor = primaryColor;
|
|
4047
|
+
if (accentColor && HEX_COLOR_RE.test(accentColor)) branding.accentColor = accentColor;
|
|
4048
|
+
if (backgroundColor && HEX_COLOR_RE.test(backgroundColor)) branding.backgroundColor = backgroundColor;
|
|
4049
|
+
if (tagline) branding.tagline = tagline;
|
|
4050
|
+
if (logoUrl) branding.logoUrl = logoUrl;
|
|
4051
|
+
if (faviconUrl) branding.faviconUrl = faviconUrl;
|
|
4052
|
+
console.error(`[branding] resolved for accountId=${accountId.slice(0, 8)}\u2026: primary=${branding.primaryColor ?? "\u2013"} logo=${branding.logoUrl ? "yes" : "no"}`);
|
|
4053
|
+
return branding;
|
|
4054
|
+
} catch (err) {
|
|
4055
|
+
console.error(`[branding] fetchBranding failed for accountId=${accountId.slice(0, 8)}\u2026: ${err instanceof Error ? err.message : String(err)}`);
|
|
4056
|
+
return null;
|
|
4057
|
+
} finally {
|
|
4058
|
+
await session.close();
|
|
4059
|
+
}
|
|
4060
|
+
}
|
|
4061
|
+
var SUMMARY_MAX_LEN = 200;
|
|
4062
|
+
var persistMessageLocks = /* @__PURE__ */ new Map();
|
|
4063
|
+
async function persistMessage(conversationId, role, content, accountId, tokens, createdAt, sender, components, attachments) {
|
|
4064
|
+
if (!content) return null;
|
|
4065
|
+
const messageId = randomUUID();
|
|
4066
|
+
const summary = role === "user" ? content.slice(0, SUMMARY_MAX_LEN).trim() : "";
|
|
4067
|
+
let embedding = null;
|
|
4068
|
+
try {
|
|
4069
|
+
embedding = await embed(content);
|
|
4070
|
+
} catch (err) {
|
|
4071
|
+
console.error(`[persist] Embedding failed, storing without: ${err instanceof Error ? err.message : String(err)}`);
|
|
4072
|
+
}
|
|
4073
|
+
const prev = persistMessageLocks.get(conversationId);
|
|
4074
|
+
const waited = prev !== void 0;
|
|
4075
|
+
let release;
|
|
4076
|
+
const mine = new Promise((resolve6) => {
|
|
4077
|
+
release = resolve6;
|
|
4078
|
+
});
|
|
4079
|
+
const chained = (prev ?? Promise.resolve()).then(() => mine);
|
|
4080
|
+
persistMessageLocks.set(conversationId, chained);
|
|
4081
|
+
await prev;
|
|
4082
|
+
const session = getSession();
|
|
4083
|
+
try {
|
|
4084
|
+
const result = await session.run(
|
|
4085
|
+
`MATCH (c:Conversation {conversationId: $conversationId})
|
|
4086
|
+
OPTIONAL MATCH (tail:Message)-[:PART_OF]->(c)
|
|
4087
|
+
WHERE NOT (tail)-[:NEXT]->(:Message)
|
|
4088
|
+
AND (tail:UserMessage OR tail:AssistantMessage)
|
|
4089
|
+
// sublabel-scope the tail to the agent-path's own sublabels.
|
|
4090
|
+
// Without this, a Task-857 :WhatsAppMessage row (no outgoing :NEXT)
|
|
4091
|
+
// would be picked as tail and the agent-path would chain
|
|
4092
|
+
// :WhatsAppMessage\u2192:UserMessage, crossing the live-channel chain.
|
|
4093
|
+
// The live writer's tail predicate is sublabel-scoped to
|
|
4094
|
+
// :WhatsAppMessage; this clause is the matching defence on the
|
|
4095
|
+
// agent side.
|
|
4096
|
+
// Capture whether THIS write's guard will fire. Read c.summary
|
|
4097
|
+
// before the SET so the boolean reflects the pre-state, not the
|
|
4098
|
+
// post-state \u2014 a false positive otherwise when a new user message
|
|
4099
|
+
// happens to equal an already-set summary (verbatim retry, short
|
|
4100
|
+
// catchphrase) and fooled a post-state equality check.
|
|
4101
|
+
WITH c, tail, (c.summary IS NULL AND $role = 'user' AND $summary <> '') AS summarySetThisWrite
|
|
4102
|
+
CREATE (m:Message:${role === "user" ? "UserMessage" : "AssistantMessage"} {
|
|
4103
|
+
messageId: $messageId,
|
|
4104
|
+
conversationId: $conversationId,
|
|
4105
|
+
accountId: $accountId,
|
|
4106
|
+
role: $role,
|
|
4107
|
+
content: $content,
|
|
4108
|
+
createdAt: ${createdAt ? "datetime($createdAt)" : "datetime()"}
|
|
4109
|
+
${embedding ? ", embedding: $embedding" : ""}
|
|
4110
|
+
${sender ? ", senderVisitorId: $senderVisitorId, senderName: $senderName" : ""}
|
|
4111
|
+
${tokens?.inputTokens != null ? ", inputTokens: $inputTokens" : ""}
|
|
4112
|
+
${tokens?.outputTokens != null ? ", outputTokens: $outputTokens" : ""}
|
|
4113
|
+
${tokens?.cacheReadTokens != null ? ", cacheReadTokens: $cacheReadTokens" : ""}
|
|
4114
|
+
${tokens?.cacheCreationTokens != null ? ", cacheCreationTokens: $cacheCreationTokens" : ""}
|
|
4115
|
+
})
|
|
4116
|
+
SET c.updatedAt = datetime()
|
|
4117
|
+
CREATE (m)-[:PART_OF]->(c)
|
|
4118
|
+
FOREACH (prev IN CASE WHEN tail IS NULL THEN [] ELSE [tail] END |
|
|
4119
|
+
CREATE (prev)-[:NEXT]->(m)
|
|
4120
|
+
)
|
|
4121
|
+
FOREACH (_ IN CASE WHEN summarySetThisWrite THEN [1] ELSE [] END |
|
|
4122
|
+
SET c.summary = $summary
|
|
4123
|
+
)
|
|
4124
|
+
FOREACH (comp IN $components |
|
|
4125
|
+
CREATE (m)-[:HAS_COMPONENT]->(:Component {
|
|
4126
|
+
componentId: comp.componentId,
|
|
4127
|
+
conversationId: $conversationId,
|
|
4128
|
+
accountId: $accountId,
|
|
4129
|
+
messageId: $messageId,
|
|
4130
|
+
name: comp.name,
|
|
4131
|
+
data: comp.data,
|
|
4132
|
+
ordinal: comp.ordinal,
|
|
4133
|
+
textOffset: comp.textOffset,
|
|
4134
|
+
submitted: false,
|
|
4135
|
+
createdAt: datetime(),
|
|
4136
|
+
// store the artefact attachmentId on the:Component
|
|
4137
|
+
// itself when this is a PERSISTENT_COMPONENTS write whose disk
|
|
4138
|
+
// write succeeded. Field is null for non-persistent / mime-skip
|
|
4139
|
+
// / disk-fail components. Lets the audit + backfill scripts
|
|
4140
|
+
// read the attachmentId directly off the :Component row instead
|
|
4141
|
+
// of re-deriving (the live + heal paths use different sources \u2014
|
|
4142
|
+
// sha256(block.id) live, componentId-fallback backfill \u2014 and
|
|
4143
|
+
// the audit must match either).
|
|
4144
|
+
attachmentId: comp.artefactAttachmentId
|
|
4145
|
+
})
|
|
4146
|
+
)
|
|
4147
|
+
// for PERSISTENT_COMPONENTS whose disk write succeeded
|
|
4148
|
+
// (artefactAttachmentId is non-null), MERGE the sibling
|
|
4149
|
+
// :KnowledgeDocument projection AND a :HAS_KNOWLEDGE_DOCUMENT edge
|
|
4150
|
+
// from the parent :Message in the same Cypher tx. ON CREATE stamps
|
|
4151
|
+
// name+encodingFormat from the live render; ON MATCH only bumps
|
|
4152
|
+
// updatedAt so a heal-on-resume re-run cannot clobber an operator's
|
|
4153
|
+
// subsequent rename via the knowledge-documents API (eng-review \xA72).
|
|
4154
|
+
// The edge makes the projection graph-discoverable from the
|
|
4155
|
+
// conversation timeline so file-delete-cascade and conversation
|
|
4156
|
+
// cleanup can reach it. FOREACH on the filtered list collapses to a
|
|
4157
|
+
// no-op when no component has an attachmentId, preserving the
|
|
4158
|
+
// text-only-turn fast path.
|
|
4159
|
+
FOREACH (kd IN $knowledgeDocs |
|
|
4160
|
+
MERGE (k:KnowledgeDocument {accountId: $accountId, attachmentId: kd.attachmentId})
|
|
4161
|
+
ON CREATE SET k.name = kd.title,
|
|
4162
|
+
k.encodingFormat = kd.mimeType,
|
|
4163
|
+
k.createdAt = datetime(),
|
|
4164
|
+
k.updatedAt = datetime()
|
|
4165
|
+
ON MATCH SET k.updatedAt = datetime()
|
|
4166
|
+
MERGE (m)-[:HAS_KNOWLEDGE_DOCUMENT]->(k)
|
|
4167
|
+
)
|
|
4168
|
+
FOREACH (att IN $attachments |
|
|
4169
|
+
CREATE (m)-[:HAS_ATTACHMENT]->(:Attachment {
|
|
4170
|
+
attachmentId: att.attachmentId,
|
|
4171
|
+
conversationId: $conversationId,
|
|
4172
|
+
accountId: $accountId,
|
|
4173
|
+
messageId: $messageId,
|
|
4174
|
+
filename: att.filename,
|
|
4175
|
+
mimeType: att.mimeType,
|
|
4176
|
+
sizeBytes: att.sizeBytes,
|
|
4177
|
+
storagePath: att.storagePath,
|
|
4178
|
+
ordinal: att.ordinal,
|
|
4179
|
+
createdAt: datetime()
|
|
4180
|
+
})
|
|
4181
|
+
)
|
|
4182
|
+
RETURN tail.messageId AS prevMessageId,
|
|
4183
|
+
summarySetThisWrite,
|
|
4184
|
+
size([(m2:Message)-[:PART_OF]->(c) | m2]) AS chainLen`,
|
|
4185
|
+
{
|
|
4186
|
+
messageId,
|
|
4187
|
+
conversationId,
|
|
4188
|
+
accountId,
|
|
4189
|
+
role,
|
|
4190
|
+
content,
|
|
4191
|
+
summary,
|
|
4192
|
+
...createdAt ? { createdAt } : {},
|
|
4193
|
+
...embedding ? { embedding } : {},
|
|
4194
|
+
...sender ? { senderVisitorId: sender.visitorId, senderName: sender.displayName } : {},
|
|
4195
|
+
...tokens?.inputTokens != null ? { inputTokens: neo4j.int(tokens.inputTokens) } : {},
|
|
4196
|
+
...tokens?.outputTokens != null ? { outputTokens: neo4j.int(tokens.outputTokens) } : {},
|
|
4197
|
+
...tokens?.cacheReadTokens != null ? { cacheReadTokens: neo4j.int(tokens.cacheReadTokens) } : {},
|
|
4198
|
+
...tokens?.cacheCreationTokens != null ? { cacheCreationTokens: neo4j.int(tokens.cacheCreationTokens) } : {},
|
|
4199
|
+
components: (components ?? []).map((comp) => ({
|
|
4200
|
+
componentId: comp.componentId,
|
|
4201
|
+
name: comp.name,
|
|
4202
|
+
data: comp.data,
|
|
4203
|
+
ordinal: neo4j.int(comp.ordinal),
|
|
4204
|
+
textOffset: neo4j.int(comp.textOffset),
|
|
4205
|
+
// null for non-persistent / mime-skip / disk-fail
|
|
4206
|
+
// components; the FOREACH stamps this directly onto the
|
|
4207
|
+
// :Component row so audit + backfill read attachmentId without
|
|
4208
|
+
// re-deriving from componentId.
|
|
4209
|
+
artefactAttachmentId: comp.artefactAttachmentId ?? null
|
|
4210
|
+
})),
|
|
4211
|
+
//:KnowledgeDocument MERGE list. One row per
|
|
4212
|
+
// PERSISTENT_COMPONENTS component whose disk write succeeded;
|
|
4213
|
+
// text-only / mime-skip / disk-fail components are absent so
|
|
4214
|
+
// the FOREACH no-ops on them. attachmentId here matches the
|
|
4215
|
+
// value the stream-parser stamped after writeAttachment.
|
|
4216
|
+
knowledgeDocs: (components ?? []).filter((comp) => typeof comp.artefactAttachmentId === "string").map((comp) => ({
|
|
4217
|
+
attachmentId: comp.artefactAttachmentId,
|
|
4218
|
+
title: comp.artefactTitle ?? "",
|
|
4219
|
+
mimeType: comp.artefactMimeType ?? ""
|
|
4220
|
+
})),
|
|
4221
|
+
attachments: (attachments ?? []).map((att) => ({
|
|
4222
|
+
attachmentId: att.attachmentId,
|
|
4223
|
+
filename: att.filename,
|
|
4224
|
+
mimeType: att.mimeType,
|
|
4225
|
+
sizeBytes: neo4j.int(att.sizeBytes),
|
|
4226
|
+
storagePath: att.storagePath,
|
|
4227
|
+
ordinal: neo4j.int(att.ordinal)
|
|
4228
|
+
}))
|
|
4229
|
+
}
|
|
4230
|
+
);
|
|
4231
|
+
if (result.records.length === 0) {
|
|
4232
|
+
console.error(`[persist] Neo4j write skipped \u2014 conversation not found: ${conversationId.slice(0, 8)}\u2026`);
|
|
4233
|
+
return null;
|
|
4234
|
+
}
|
|
4235
|
+
const record = result.records[0];
|
|
4236
|
+
const prevMessageId = record.get("prevMessageId") ?? null;
|
|
4237
|
+
const summarySetThisWrite = record.get("summarySetThisWrite") === true;
|
|
4238
|
+
const chainLenRaw = record.get("chainLen");
|
|
4239
|
+
const chainLen = typeof chainLenRaw === "bigint" ? Number(chainLenRaw) : typeof chainLenRaw?.toNumber === "function" ? chainLenRaw.toNumber() : Number(chainLenRaw ?? 0);
|
|
4240
|
+
const messageSublabel = role === "user" ? "UserMessage" : "AssistantMessage";
|
|
4241
|
+
console.error(`[neo4j-store] append-message conversationId=${conversationId.slice(0, 8)}\u2026 messageId=${messageId.slice(0, 8)}\u2026 prev=${prevMessageId ? prevMessageId.slice(0, 8) + "\u2026" : "null"} chainLen=${chainLen} waited=${waited} sublabel=${messageSublabel}`);
|
|
4242
|
+
if (summarySetThisWrite) {
|
|
4243
|
+
console.error(`[neo4j-store] conversation-summary-set conversationId=${conversationId.slice(0, 8)}\u2026 len=${summary.length}`);
|
|
4244
|
+
}
|
|
4245
|
+
const componentList = components ?? [];
|
|
4246
|
+
if (componentList.length > 0) {
|
|
4247
|
+
const relsCreated = result.summary.counters.updates().relationshipsCreated;
|
|
4248
|
+
const expectedComponentEdges = componentList.length;
|
|
4249
|
+
const baseEdges = relsCreated - expectedComponentEdges;
|
|
4250
|
+
if (baseEdges < 1) {
|
|
4251
|
+
console.error(`[neo4j-store] persist-component WARN conversationId=${conversationId.slice(0, 8)}\u2026 messageId=${messageId.slice(0, 8)}\u2026 relsCreated=${relsCreated} expected\u2265${1 + expectedComponentEdges} \u2014 component edges may not have been created`);
|
|
4252
|
+
}
|
|
4253
|
+
for (const comp of componentList) {
|
|
4254
|
+
console.error(`[neo4j-store] persist-component conversationId=${conversationId.slice(0, 8)}\u2026 messageId=${messageId.slice(0, 8)}\u2026 componentId=${comp.componentId.slice(0, 8)}\u2026 name=${comp.name} dataLen=${comp.data.length} ordinal=${comp.ordinal} textOffset=${comp.textOffset}`);
|
|
4255
|
+
}
|
|
4256
|
+
}
|
|
4257
|
+
const attachmentList = attachments ?? [];
|
|
4258
|
+
if (attachmentList.length > 0) {
|
|
4259
|
+
const relsCreated = result.summary.counters.updates().relationshipsCreated;
|
|
4260
|
+
const expectedAttachmentEdges = attachmentList.length;
|
|
4261
|
+
const expectedComponentEdges = (components ?? []).length;
|
|
4262
|
+
const baseEdges = relsCreated - expectedComponentEdges - expectedAttachmentEdges;
|
|
4263
|
+
if (baseEdges < 1) {
|
|
4264
|
+
console.error(`[neo4j-store] persist-attachment WARN conversationId=${conversationId.slice(0, 8)}\u2026 messageId=${messageId.slice(0, 8)}\u2026 relsCreated=${relsCreated} expected\u2265${1 + expectedComponentEdges + expectedAttachmentEdges} \u2014 attachment edges may not have been created`);
|
|
4265
|
+
}
|
|
4266
|
+
for (const att of attachmentList) {
|
|
4267
|
+
console.error(`[neo4j-store] persist-attachment conversationId=${conversationId.slice(0, 8)}\u2026 messageId=${messageId.slice(0, 8)}\u2026 attachmentId=${att.attachmentId.slice(0, 8)}\u2026 filename=${att.filename} mimeType=${att.mimeType} sizeBytes=${att.sizeBytes} ordinal=${att.ordinal}`);
|
|
4268
|
+
}
|
|
4269
|
+
}
|
|
4270
|
+
console.error(`[persist] ${(/* @__PURE__ */ new Date()).toISOString()} conversationId=${conversationId.slice(0, 8)}\u2026 role=${role} len=${content.length}${sender ? ` sender=${sender.displayName}` : ""}`);
|
|
4271
|
+
return messageId;
|
|
4272
|
+
} catch (err) {
|
|
4273
|
+
console.error(`[persist] Neo4j write failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
4274
|
+
return null;
|
|
4275
|
+
} finally {
|
|
4276
|
+
release();
|
|
4277
|
+
if (persistMessageLocks.get(conversationId) === chained) {
|
|
4278
|
+
persistMessageLocks.delete(conversationId);
|
|
4279
|
+
}
|
|
4280
|
+
await session.close();
|
|
4281
|
+
}
|
|
4282
|
+
}
|
|
4283
|
+
async function getAgentSessionIdForConversation(conversationId) {
|
|
4284
|
+
const session = getSession();
|
|
4285
|
+
try {
|
|
4286
|
+
const result = await session.run(
|
|
4287
|
+
`MATCH (c:Conversation {conversationId: $conversationId})
|
|
4288
|
+
RETURN c.agentSessionId AS agentSessionId`,
|
|
4289
|
+
{ conversationId }
|
|
4290
|
+
);
|
|
4291
|
+
const value = result.records[0]?.get("agentSessionId");
|
|
4292
|
+
return typeof value === "string" && value.length > 0 ? value : null;
|
|
4293
|
+
} catch (err) {
|
|
4294
|
+
console.error(`[persist] agent-session-id read failed convId=${conversationId.slice(0, 8)}\u2026 error=${err instanceof Error ? err.message : String(err)}`);
|
|
4295
|
+
return null;
|
|
4296
|
+
} finally {
|
|
4297
|
+
await session.close();
|
|
4298
|
+
}
|
|
4299
|
+
}
|
|
4300
|
+
async function getRecentMessages(conversationId, limit = 50) {
|
|
4301
|
+
const session = getSession();
|
|
4302
|
+
try {
|
|
4303
|
+
const result = await session.run(
|
|
4304
|
+
`MATCH (tail:Message {conversationId: $conversationId})
|
|
4305
|
+
WHERE NOT (tail)-[:NEXT]->(:Message)
|
|
4306
|
+
WITH tail ORDER BY tail.createdAt DESC LIMIT 1
|
|
4307
|
+
MATCH path = (m:Message)-[:NEXT*0..]->(tail)
|
|
4308
|
+
WHERE m.conversationId = $conversationId
|
|
4309
|
+
AND length(path) < $limit
|
|
4310
|
+
WITH m, length(path) AS depthFromTail
|
|
4311
|
+
OPTIONAL MATCH (m)-[:HAS_COMPONENT]->(c:Component)
|
|
4312
|
+
WITH m, depthFromTail, c ORDER BY c.ordinal ASC
|
|
4313
|
+
WITH m, depthFromTail,
|
|
4314
|
+
[comp IN collect(c) WHERE comp IS NOT NULL | comp {.*}] AS components
|
|
4315
|
+
OPTIONAL MATCH (m)-[:HAS_ATTACHMENT]->(a:Attachment)
|
|
4316
|
+
WITH m, depthFromTail, components, a ORDER BY a.ordinal ASC
|
|
4317
|
+
WITH m, depthFromTail, components,
|
|
4318
|
+
[att IN collect(a) WHERE att IS NOT NULL | att {.*}] AS attachments
|
|
4319
|
+
RETURN m.messageId AS messageId, m.role AS role, m.content AS content,
|
|
4320
|
+
m.createdAt AS createdAt, components, attachments
|
|
4321
|
+
ORDER BY depthFromTail DESC`,
|
|
4322
|
+
{ conversationId, limit: neo4j.int(limit) }
|
|
4323
|
+
);
|
|
4324
|
+
return result.records.map((r) => {
|
|
4325
|
+
const rawComponents = r.get("components") ?? [];
|
|
4326
|
+
const components = rawComponents.map((c) => {
|
|
4327
|
+
const rawAttachmentId = c.attachmentId;
|
|
4328
|
+
const attachmentId = typeof rawAttachmentId === "string" && rawAttachmentId.length > 0 ? rawAttachmentId : void 0;
|
|
4329
|
+
return {
|
|
4330
|
+
componentId: String(c.componentId ?? ""),
|
|
4331
|
+
name: String(c.name ?? ""),
|
|
4332
|
+
data: String(c.data ?? ""),
|
|
4333
|
+
ordinal: typeof c.ordinal?.toNumber === "function" ? c.ordinal.toNumber() : Number(c.ordinal ?? 0),
|
|
4334
|
+
textOffset: typeof c.textOffset?.toNumber === "function" ? c.textOffset.toNumber() : Number(c.textOffset ?? 0),
|
|
4335
|
+
submitted: c.submitted === true,
|
|
4336
|
+
...attachmentId ? { attachmentId } : {}
|
|
4337
|
+
};
|
|
4338
|
+
});
|
|
4339
|
+
const rawAttachments = r.get("attachments") ?? [];
|
|
4340
|
+
const attachments = rawAttachments.map((a) => ({
|
|
4341
|
+
attachmentId: String(a.attachmentId ?? ""),
|
|
4342
|
+
filename: String(a.filename ?? ""),
|
|
4343
|
+
mimeType: String(a.mimeType ?? ""),
|
|
4344
|
+
sizeBytes: typeof a.sizeBytes?.toNumber === "function" ? a.sizeBytes.toNumber() : Number(a.sizeBytes ?? 0),
|
|
4345
|
+
storagePath: String(a.storagePath ?? ""),
|
|
4346
|
+
ordinal: typeof a.ordinal?.toNumber === "function" ? a.ordinal.toNumber() : Number(a.ordinal ?? 0),
|
|
4347
|
+
accountId: String(a.accountId ?? "")
|
|
4348
|
+
}));
|
|
4349
|
+
return {
|
|
4350
|
+
messageId: r.get("messageId"),
|
|
4351
|
+
role: r.get("role"),
|
|
4352
|
+
content: r.get("content"),
|
|
4353
|
+
createdAt: String(r.get("createdAt")),
|
|
4354
|
+
components,
|
|
4355
|
+
attachments
|
|
4356
|
+
};
|
|
4357
|
+
});
|
|
4358
|
+
} catch (err) {
|
|
4359
|
+
console.error(`[persist] getRecentMessages failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
4360
|
+
return [];
|
|
4361
|
+
} finally {
|
|
4362
|
+
await session.close();
|
|
4363
|
+
}
|
|
4364
|
+
}
|
|
4365
|
+
async function verifyConversationOwnership(conversationId, accountId) {
|
|
4366
|
+
const session = getSession();
|
|
4367
|
+
try {
|
|
4368
|
+
const result = await session.run(
|
|
4369
|
+
`MATCH (c:Conversation {conversationId: $conversationId, accountId: $accountId})
|
|
4370
|
+
RETURN c.conversationId AS id LIMIT 1`,
|
|
4371
|
+
{ conversationId, accountId }
|
|
4372
|
+
);
|
|
4373
|
+
return result.records.length > 0;
|
|
4374
|
+
} catch (err) {
|
|
4375
|
+
console.error(`[persist] verifyConversationOwnership failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
4376
|
+
return false;
|
|
4377
|
+
} finally {
|
|
4378
|
+
await session.close();
|
|
4379
|
+
}
|
|
4380
|
+
}
|
|
4381
|
+
async function getConversationOwner(conversationId) {
|
|
4382
|
+
const session = getSession();
|
|
4383
|
+
try {
|
|
4384
|
+
const result = await session.run(
|
|
4385
|
+
`MATCH (c:Conversation {conversationId: $conversationId})
|
|
4386
|
+
RETURN c.accountId AS accountId, c.userId AS userId LIMIT 1`,
|
|
4387
|
+
{ conversationId }
|
|
4388
|
+
);
|
|
4389
|
+
const record = result.records[0];
|
|
4390
|
+
if (!record) return null;
|
|
4391
|
+
const accountId = record.get("accountId");
|
|
4392
|
+
const userId = record.get("userId");
|
|
4393
|
+
if (!accountId) return null;
|
|
4394
|
+
return { accountId, userId: userId ?? null };
|
|
4395
|
+
} catch (err) {
|
|
4396
|
+
console.error(`[persist] getConversationOwner failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
4397
|
+
return null;
|
|
4398
|
+
} finally {
|
|
4399
|
+
await session.close();
|
|
4400
|
+
}
|
|
4401
|
+
}
|
|
4402
|
+
async function verifyAndGetConversationUpdatedAt(conversationId, accountId) {
|
|
4403
|
+
const session = getSession();
|
|
4404
|
+
try {
|
|
4405
|
+
const result = await session.run(
|
|
4406
|
+
`MATCH (c:Conversation {conversationId: $conversationId, accountId: $accountId})
|
|
4407
|
+
RETURN toString(c.updatedAt) AS updatedAt LIMIT 1`,
|
|
4408
|
+
{ conversationId, accountId }
|
|
4409
|
+
);
|
|
4410
|
+
const record = result.records[0];
|
|
4411
|
+
return record ? record.get("updatedAt") : null;
|
|
4412
|
+
} catch (err) {
|
|
4413
|
+
console.error(`[persist] verifyAndGetConversationUpdatedAt failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
4414
|
+
return null;
|
|
4415
|
+
} finally {
|
|
4416
|
+
await session.close();
|
|
4417
|
+
}
|
|
4418
|
+
}
|
|
4419
|
+
async function listAdminSessions(accountId, userId, limit = 20) {
|
|
4420
|
+
const session = getSession();
|
|
4421
|
+
try {
|
|
4422
|
+
const result = await session.run(
|
|
4423
|
+
`MATCH (c:Conversation {accountId: $accountId, agentType: 'admin', userId: $userId})
|
|
4424
|
+
WHERE NOT c:Trashed
|
|
4425
|
+
RETURN c.conversationId AS conversationId,
|
|
4426
|
+
c.name AS name,
|
|
4427
|
+
c.updatedAt AS updatedAt,
|
|
4428
|
+
c.channel AS channel
|
|
4429
|
+
ORDER BY c.updatedAt DESC
|
|
4430
|
+
LIMIT $limit`,
|
|
4431
|
+
{ accountId, userId, limit: neo4j.int(limit) }
|
|
4432
|
+
);
|
|
4433
|
+
return result.records.map((r) => ({
|
|
4434
|
+
conversationId: r.get("conversationId"),
|
|
4435
|
+
name: r.get("name"),
|
|
4436
|
+
updatedAt: String(r.get("updatedAt")),
|
|
4437
|
+
channel: r.get("channel")
|
|
4438
|
+
}));
|
|
4439
|
+
} catch (err) {
|
|
4440
|
+
console.error(`[persist] listAdminSessions failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
4441
|
+
return [];
|
|
4442
|
+
} finally {
|
|
4443
|
+
await session.close();
|
|
4444
|
+
}
|
|
4445
|
+
}
|
|
4446
|
+
async function deleteConversation(conversationId) {
|
|
4447
|
+
const session = getSession();
|
|
4448
|
+
try {
|
|
4449
|
+
const result = await session.run(
|
|
4450
|
+
`MATCH (c:Conversation {conversationId: $conversationId})
|
|
4451
|
+
OPTIONAL MATCH (m:Message)-[:PART_OF]->(c)
|
|
4452
|
+
OPTIONAL MATCH (m)-[:HAS_COMPONENT]->(comp:Component)
|
|
4453
|
+
OPTIONAL MATCH (m)-[:HAS_ATTACHMENT]->(att:Attachment)
|
|
4454
|
+
OPTIONAL MATCH (c)-[:HAS_TRANSCRIPT]->(t:ClaudeSession)
|
|
4455
|
+
DETACH DELETE att, comp, m, t, c
|
|
4456
|
+
RETURN count(c) AS deleted`,
|
|
4457
|
+
{ conversationId }
|
|
4458
|
+
);
|
|
4459
|
+
const deleted = result.records[0]?.get("deleted");
|
|
4460
|
+
const count = typeof deleted === "object" && deleted !== null ? Number(deleted) : Number(deleted ?? 0);
|
|
4461
|
+
const found = count > 0;
|
|
4462
|
+
console.error(`[persist] deleteConversation ${conversationId.slice(0, 8)}\u2026: ${found ? "deleted" : "not found"}`);
|
|
4463
|
+
return { found };
|
|
4464
|
+
} finally {
|
|
4465
|
+
await session.close();
|
|
4466
|
+
}
|
|
4467
|
+
}
|
|
4468
|
+
async function linkConversationTranscript(conversationId, claudeSessionId, accountId) {
|
|
4469
|
+
if (!conversationId || !claudeSessionId || !accountId) {
|
|
4470
|
+
console.error(`[transcript-link] schema-violation conversationId=${conversationId ? "set" : "EMPTY"} claudeSessionId=${claudeSessionId ? "set" : "EMPTY"} accountId=${accountId ? "set" : "EMPTY"}`);
|
|
4471
|
+
return { linked: false };
|
|
4472
|
+
}
|
|
4473
|
+
const session = getSession();
|
|
4474
|
+
try {
|
|
4475
|
+
const result = await session.run(
|
|
4476
|
+
`MATCH (c:Conversation {conversationId: $conversationId, accountId: $accountId})
|
|
4477
|
+
MERGE (c)-[:HAS_TRANSCRIPT]->(t:ClaudeSession {claudeSessionId: $claudeSessionId})
|
|
4478
|
+
ON CREATE SET
|
|
4479
|
+
t.accountId = $accountId,
|
|
4480
|
+
t.createdAt = datetime()
|
|
4481
|
+
RETURN count(t) AS linked`,
|
|
4482
|
+
{ conversationId, claudeSessionId, accountId }
|
|
4483
|
+
);
|
|
4484
|
+
const linked = Number(result.records[0]?.get("linked") ?? 0) > 0;
|
|
4485
|
+
if (!linked) {
|
|
4486
|
+
console.error(`[transcript-link] convId=${conversationId.slice(0, 8)}\u2026 claudeSessionId=${claudeSessionId.slice(0, 8)}\u2026 status=skipped reason=conversation-not-found`);
|
|
4487
|
+
}
|
|
4488
|
+
return { linked };
|
|
4489
|
+
} catch (err) {
|
|
4490
|
+
console.error(`[transcript-link] convId=${conversationId.slice(0, 8)}\u2026 claudeSessionId=${claudeSessionId.slice(0, 8)}\u2026 status=failed error=${err instanceof Error ? err.message : String(err)}`);
|
|
4491
|
+
return { linked: false };
|
|
4492
|
+
} finally {
|
|
4493
|
+
await session.close();
|
|
4494
|
+
}
|
|
4495
|
+
}
|
|
4496
|
+
async function getConversationClaudeSessionIds(conversationId, accountId) {
|
|
4497
|
+
const session = getSession();
|
|
4498
|
+
try {
|
|
4499
|
+
const result = await session.run(
|
|
4500
|
+
`MATCH (c:Conversation {conversationId: $conversationId, accountId: $accountId})-[:HAS_TRANSCRIPT]->(t:ClaudeSession)
|
|
4501
|
+
RETURN t.claudeSessionId AS claudeSessionId
|
|
4502
|
+
ORDER BY t.createdAt ASC`,
|
|
4503
|
+
{ conversationId, accountId }
|
|
4504
|
+
);
|
|
4505
|
+
return result.records.map((r) => r.get("claudeSessionId")).filter((v) => typeof v === "string" && v.length > 0);
|
|
4506
|
+
} finally {
|
|
4507
|
+
await session.close();
|
|
4508
|
+
}
|
|
4509
|
+
}
|
|
4510
|
+
async function getConversationIdByClaudeSessionId(claudeSessionId, accountId) {
|
|
4511
|
+
const session = getSession();
|
|
4512
|
+
try {
|
|
4513
|
+
const result = await session.run(
|
|
4514
|
+
`MATCH (c:Conversation {accountId: $accountId})-[:HAS_TRANSCRIPT]->(t:ClaudeSession {claudeSessionId: $claudeSessionId})
|
|
4515
|
+
RETURN c.conversationId AS conversationId
|
|
4516
|
+
LIMIT 1`,
|
|
4517
|
+
{ claudeSessionId, accountId }
|
|
4518
|
+
);
|
|
4519
|
+
const value = result.records[0]?.get("conversationId");
|
|
4520
|
+
return typeof value === "string" && value.length > 0 ? value : null;
|
|
4521
|
+
} finally {
|
|
4522
|
+
await session.close();
|
|
4523
|
+
}
|
|
4524
|
+
}
|
|
4525
|
+
async function renameConversation(conversationId, label) {
|
|
4526
|
+
let embedding = null;
|
|
4527
|
+
try {
|
|
4528
|
+
embedding = await embed(label);
|
|
4529
|
+
} catch (err) {
|
|
4530
|
+
console.error(`[persist] manual-label: embedding failed, persisting without: ${err instanceof Error ? err.message : String(err)}`);
|
|
4531
|
+
}
|
|
4532
|
+
const session = getSession();
|
|
4533
|
+
try {
|
|
4534
|
+
await session.run(
|
|
4535
|
+
`MATCH (c:Conversation {conversationId: $conversationId})
|
|
4536
|
+
SET c.name = $label, c.updatedAt = datetime()
|
|
4537
|
+
${embedding ? ", c.embedding = $embedding" : ""}`,
|
|
4538
|
+
{ conversationId, label, ...embedding ? { embedding } : {} }
|
|
4539
|
+
);
|
|
4540
|
+
console.error(`[persist] manual-label: renamed ${conversationId} to "${label}"${embedding ? " (embedded)" : ""}`);
|
|
4541
|
+
} finally {
|
|
4542
|
+
await session.close();
|
|
4543
|
+
}
|
|
4544
|
+
}
|
|
4545
|
+
var DECAY_THRESHOLD_DAYS = 14;
|
|
4546
|
+
var DECAY_RATE_PER_DAY = 0.05;
|
|
4547
|
+
var INJECTION_THRESHOLD = 0.4;
|
|
4548
|
+
var MAX_SUMMARY_PREFERENCES = 30;
|
|
4549
|
+
async function getUserTimezone(accountId, userId) {
|
|
4550
|
+
const session = getSession();
|
|
4551
|
+
try {
|
|
4552
|
+
const query = userId ? `MATCH (up:UserProfile {accountId: $accountId, userId: $userId})
|
|
4553
|
+
RETURN up.timezone AS timezone` : `MATCH (up:UserProfile {accountId: $accountId})
|
|
4554
|
+
RETURN up.timezone AS timezone ORDER BY up.createdAt LIMIT 1`;
|
|
4555
|
+
const result = await session.run(query, { accountId, userId: userId ?? "" });
|
|
4556
|
+
if (result.records.length === 0) return null;
|
|
4557
|
+
const tz = result.records[0].get("timezone");
|
|
4558
|
+
return tz && tz.trim().length > 0 ? tz : null;
|
|
4559
|
+
} catch (err) {
|
|
4560
|
+
console.error(`[datetime] getUserTimezone failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
4561
|
+
return null;
|
|
4562
|
+
} finally {
|
|
4563
|
+
await session.close();
|
|
4564
|
+
}
|
|
4565
|
+
}
|
|
4566
|
+
async function loadAdminUserName(accountId, userId) {
|
|
4567
|
+
const session = getSession();
|
|
4568
|
+
try {
|
|
4569
|
+
const result = await session.run(
|
|
4570
|
+
`MATCH (au:AdminUser {userId: $userId})-[:OWNS]->(p:Person {accountId: $accountId})
|
|
4571
|
+
RETURN p.givenName AS givenName, p.familyName AS familyName, p.avatar AS avatar
|
|
4572
|
+
LIMIT 1`,
|
|
4573
|
+
{ accountId, userId }
|
|
4574
|
+
);
|
|
4575
|
+
if (result.records.length === 0) {
|
|
4576
|
+
return { source: "fallback", reason: "no-person-node" };
|
|
4577
|
+
}
|
|
4578
|
+
const givenName = result.records[0].get("givenName");
|
|
4579
|
+
const familyName = result.records[0].get("familyName");
|
|
4580
|
+
const avatar = result.records[0].get("avatar");
|
|
4581
|
+
if (!givenName || givenName.trim().length === 0) {
|
|
4582
|
+
return { source: "fallback", reason: "no-givenName" };
|
|
4583
|
+
}
|
|
4584
|
+
const trimmedFamily = familyName && familyName.trim().length > 0 ? familyName.trim() : null;
|
|
4585
|
+
const trimmedAvatar = avatar && avatar.trim().length > 0 ? avatar.trim() : null;
|
|
4586
|
+
const joined = trimmedFamily ? `${givenName.trim()} ${trimmedFamily}` : givenName.trim();
|
|
4587
|
+
return { source: "neo4j", joined, givenName: givenName.trim(), familyName: trimmedFamily, avatar: trimmedAvatar };
|
|
4588
|
+
} catch (err) {
|
|
4589
|
+
console.error(`[admin-identity] loadAdminUserName failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
4590
|
+
return { source: "fallback", reason: "neo4j-unreachable" };
|
|
4591
|
+
} finally {
|
|
4592
|
+
await session.close();
|
|
4593
|
+
}
|
|
4594
|
+
}
|
|
4595
|
+
async function writeAdminUserAndPerson(params) {
|
|
4596
|
+
const { userId, fullName, accountId } = params;
|
|
4597
|
+
const trimmed = fullName.trim();
|
|
4598
|
+
if (!trimmed) {
|
|
4599
|
+
throw new Error("writeAdminUserAndPerson: fullName cannot be empty");
|
|
4600
|
+
}
|
|
4601
|
+
const firstSpace = trimmed.search(/\s/);
|
|
4602
|
+
const givenName = firstSpace === -1 ? trimmed : trimmed.slice(0, firstSpace).trim();
|
|
4603
|
+
const familyName = firstSpace === -1 ? null : trimmed.slice(firstSpace + 1).trim() || null;
|
|
4604
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
4605
|
+
const session = getSession();
|
|
4606
|
+
try {
|
|
4607
|
+
const result = await session.run(
|
|
4608
|
+
`MERGE (au:AdminUser {userId: $userId})
|
|
4609
|
+
ON CREATE SET au.name = $fullName, au.createdAt = $now
|
|
4610
|
+
ON MATCH SET au.name = $fullName, au.updatedAt = $now
|
|
4611
|
+
WITH au
|
|
4612
|
+
|
|
4613
|
+
// Case-insensitive Person reuse: same accountId, same givenName, same familyName
|
|
4614
|
+
// (null/empty familyName collapsed to '' so 'Joel' does NOT match 'Joel Smalley').
|
|
4615
|
+
OPTIONAL MATCH (existingPerson:Person {accountId: $accountId})
|
|
4616
|
+
WHERE toLower(existingPerson.givenName) = toLower($givenName)
|
|
4617
|
+
AND coalesce(toLower(existingPerson.familyName), '') = coalesce(toLower($familyName), '')
|
|
4618
|
+
WITH au, existingPerson
|
|
4619
|
+
|
|
4620
|
+
CALL {
|
|
4621
|
+
WITH au, existingPerson
|
|
4622
|
+
WITH au, existingPerson WHERE existingPerson IS NOT NULL
|
|
4623
|
+
MERGE (au)-[:OWNS]->(existingPerson)
|
|
4624
|
+
RETURN existingPerson AS p, true AS reused
|
|
4625
|
+
UNION
|
|
4626
|
+
WITH au, existingPerson
|
|
4627
|
+
WITH au WHERE existingPerson IS NULL
|
|
4628
|
+
CREATE (newPerson:Person {
|
|
4629
|
+
accountId: $accountId,
|
|
4630
|
+
givenName: $givenName,
|
|
4631
|
+
familyName: $familyName,
|
|
4632
|
+
role: 'admin-personal',
|
|
4633
|
+
scope: 'admin',
|
|
4634
|
+
createdAt: $now
|
|
4635
|
+
})
|
|
4636
|
+
MERGE (au)-[:OWNS]->(newPerson)
|
|
4637
|
+
RETURN newPerson AS p, false AS reused
|
|
4638
|
+
}
|
|
4639
|
+
RETURN reused, elementId(p) AS personElementId,
|
|
4640
|
+
p.givenName AS givenName, p.familyName AS familyName`,
|
|
4641
|
+
{ userId, fullName: trimmed, accountId, givenName, familyName, now }
|
|
4642
|
+
);
|
|
4643
|
+
if (result.records.length === 0) {
|
|
4644
|
+
throw new Error("writeAdminUserAndPerson: no record returned");
|
|
4645
|
+
}
|
|
4646
|
+
const record = result.records[0];
|
|
4647
|
+
return {
|
|
4648
|
+
personReused: record.get("reused"),
|
|
4649
|
+
personElementId: record.get("personElementId"),
|
|
4650
|
+
givenName: record.get("givenName"),
|
|
4651
|
+
familyName: record.get("familyName")
|
|
4652
|
+
};
|
|
4653
|
+
} finally {
|
|
4654
|
+
await session.close();
|
|
4655
|
+
}
|
|
4656
|
+
}
|
|
4657
|
+
var PROFILE_CATEGORIES = [
|
|
4658
|
+
"communication",
|
|
4659
|
+
"scheduling",
|
|
4660
|
+
"decision",
|
|
4661
|
+
"workflow",
|
|
4662
|
+
"content",
|
|
4663
|
+
"interaction"
|
|
4664
|
+
];
|
|
4665
|
+
var PROFILE_FIELD_TEMPLATES = {
|
|
4666
|
+
communication: [
|
|
4667
|
+
"preferredChannel",
|
|
4668
|
+
"quietHours",
|
|
4669
|
+
"responseLatencyExpectation",
|
|
4670
|
+
"formalityLevel",
|
|
4671
|
+
"escalationPath"
|
|
4672
|
+
],
|
|
4673
|
+
scheduling: [
|
|
4674
|
+
"workdayStartTime",
|
|
4675
|
+
"workdayEndTime",
|
|
4676
|
+
"weekendAvailability",
|
|
4677
|
+
"meetingBlockPreference",
|
|
4678
|
+
"focusBlockTiming"
|
|
4679
|
+
],
|
|
4680
|
+
decision: [
|
|
4681
|
+
"riskTolerance",
|
|
4682
|
+
"autonomyBoundary",
|
|
4683
|
+
"evidenceThreshold",
|
|
4684
|
+
"reversibilityPreference",
|
|
4685
|
+
"stakeholderConsultation"
|
|
4686
|
+
],
|
|
4687
|
+
workflow: [
|
|
4688
|
+
"batchingPreference",
|
|
4689
|
+
"taskGranularity",
|
|
4690
|
+
"deepWorkBlocks",
|
|
4691
|
+
"statusUpdateCadence",
|
|
4692
|
+
"toolingChoices"
|
|
4693
|
+
],
|
|
4694
|
+
content: [
|
|
4695
|
+
"outputFormat",
|
|
4696
|
+
"lengthPreference",
|
|
4697
|
+
"citationStyle",
|
|
4698
|
+
"tonality"
|
|
4699
|
+
],
|
|
4700
|
+
interaction: [
|
|
4701
|
+
"addressForm",
|
|
4702
|
+
"humourTolerance",
|
|
4703
|
+
"directnessLevel",
|
|
4704
|
+
"emotionalRegisterPreference"
|
|
4705
|
+
]
|
|
4706
|
+
};
|
|
4707
|
+
var TOTAL_PROFILE_FIELDS = Object.values(PROFILE_FIELD_TEMPLATES).reduce(
|
|
4708
|
+
(n, fs) => n + fs.length,
|
|
4709
|
+
0
|
|
4710
|
+
);
|
|
4711
|
+
function computeAbsentFields(preferences, extraTemplates = {}) {
|
|
4712
|
+
const covered = /* @__PURE__ */ new Set();
|
|
4713
|
+
for (const pref of preferences) {
|
|
4714
|
+
const isCovered = pref.notApplicable === true || pref.confidence >= INJECTION_THRESHOLD;
|
|
4715
|
+
if (isCovered) covered.add(`${pref.category}.${pref.key}`);
|
|
4716
|
+
}
|
|
4717
|
+
const absent = [];
|
|
4718
|
+
for (const [category, fields] of Object.entries(PROFILE_FIELD_TEMPLATES)) {
|
|
4719
|
+
for (const field of fields) {
|
|
4720
|
+
const dotted = `${category}.${field}`;
|
|
4721
|
+
if (!covered.has(dotted)) absent.push(dotted);
|
|
4722
|
+
}
|
|
4723
|
+
}
|
|
4724
|
+
for (const [category, fields] of Object.entries(extraTemplates)) {
|
|
4725
|
+
if (category in PROFILE_FIELD_TEMPLATES) continue;
|
|
4726
|
+
for (const field of fields) {
|
|
4727
|
+
const dotted = `${category}.${field}`;
|
|
4728
|
+
if (!covered.has(dotted)) absent.push(dotted);
|
|
4729
|
+
}
|
|
4730
|
+
}
|
|
4731
|
+
return absent;
|
|
4732
|
+
}
|
|
4733
|
+
function parsePluginProfileKeys(manifest) {
|
|
4734
|
+
const fm = manifest.match(/^---\n([\s\S]*?)\n---/);
|
|
4735
|
+
if (!fm) return [];
|
|
4736
|
+
const out = [];
|
|
4737
|
+
let inBlock = false;
|
|
4738
|
+
for (const line of fm[1].split("\n")) {
|
|
4739
|
+
if (/^profileKeys:/.test(line)) {
|
|
4740
|
+
inBlock = true;
|
|
4741
|
+
continue;
|
|
4742
|
+
}
|
|
4743
|
+
if (inBlock) {
|
|
4744
|
+
const m = line.match(/^\s+-\s+(.+?)\s*$/);
|
|
4745
|
+
if (m) out.push(m[1]);
|
|
4746
|
+
else break;
|
|
4747
|
+
}
|
|
4748
|
+
}
|
|
4749
|
+
return out;
|
|
4750
|
+
}
|
|
4751
|
+
function loadEnabledPluginProfileKeys(accountId) {
|
|
4752
|
+
const accountFile = resolve5(PLATFORM_ROOT3, "..", "data/accounts", accountId, "account.json");
|
|
4753
|
+
if (!existsSync6(accountFile)) return {};
|
|
4754
|
+
let enabled;
|
|
4755
|
+
try {
|
|
4756
|
+
const raw2 = readFileSync6(accountFile, "utf-8");
|
|
4757
|
+
const parsed = JSON.parse(raw2);
|
|
4758
|
+
enabled = Array.isArray(parsed.enabledPlugins) ? parsed.enabledPlugins.filter((p) => typeof p === "string") : [];
|
|
4759
|
+
} catch (err) {
|
|
4760
|
+
console.error(
|
|
4761
|
+
`[profile-keys] account.json read failed for ${accountId.slice(0, 8)}\u2026: ${err instanceof Error ? err.message : String(err)}`
|
|
4762
|
+
);
|
|
4763
|
+
return {};
|
|
4764
|
+
}
|
|
4765
|
+
const merged = {};
|
|
4766
|
+
for (const pluginName of enabled) {
|
|
4767
|
+
const manifestPath = resolve5(PLATFORM_ROOT3, "plugins", pluginName, "PLUGIN.md");
|
|
4768
|
+
if (!existsSync6(manifestPath)) continue;
|
|
4769
|
+
let manifest;
|
|
4770
|
+
try {
|
|
4771
|
+
manifest = readFileSync6(manifestPath, "utf-8");
|
|
4772
|
+
} catch {
|
|
4773
|
+
continue;
|
|
4774
|
+
}
|
|
4775
|
+
for (const dotted of parsePluginProfileKeys(manifest)) {
|
|
4776
|
+
const dot = dotted.indexOf(".");
|
|
4777
|
+
if (dot <= 0 || dot === dotted.length - 1) {
|
|
4778
|
+
console.error(`[profile-keys] malformed dotted key in ${pluginName}: ${dotted}`);
|
|
4779
|
+
continue;
|
|
4780
|
+
}
|
|
4781
|
+
const category = dotted.slice(0, dot);
|
|
4782
|
+
const key = dotted.slice(dot + 1);
|
|
4783
|
+
if (!merged[category]) merged[category] = /* @__PURE__ */ new Set();
|
|
4784
|
+
merged[category].add(key);
|
|
4785
|
+
}
|
|
4786
|
+
}
|
|
4787
|
+
const out = {};
|
|
4788
|
+
for (const [cat, set] of Object.entries(merged)) out[cat] = [...set];
|
|
4789
|
+
return out;
|
|
4790
|
+
}
|
|
4791
|
+
var sparseStateLogged = /* @__PURE__ */ new Set();
|
|
4792
|
+
async function loadUserProfile(accountId, userId, conversationId) {
|
|
4793
|
+
console.error(
|
|
4794
|
+
`[profile] loadUserProfile entry accountId=${accountId.slice(0, 8)}\u2026 userId=${userId} conversationId=${conversationId ? `${conversationId.slice(0, 8)}\u2026` : "<none>"}`
|
|
4795
|
+
);
|
|
4796
|
+
const session = getSession();
|
|
4797
|
+
try {
|
|
4798
|
+
const serverTimezone = Intl.DateTimeFormat().resolvedOptions().timeZone;
|
|
4799
|
+
const mergeResult = await session.run(
|
|
4800
|
+
`MATCH (au:AdminUser {userId: $userId})
|
|
4801
|
+
MERGE (au)-[:HAS_PROFILE]->(up:UserProfile {accountId: $accountId, userId: $userId})
|
|
4802
|
+
ON CREATE SET
|
|
4803
|
+
up.createdAt = $now,
|
|
4804
|
+
up.updatedAt = $now,
|
|
4805
|
+
up.profileVersion = 0,
|
|
4806
|
+
up.scope = 'admin',
|
|
4807
|
+
up.timezone = $serverTimezone
|
|
4808
|
+
ON MATCH SET
|
|
4809
|
+
up.timezone = CASE WHEN up.timezone IS NULL OR trim(up.timezone) = '' THEN $serverTimezone ELSE up.timezone END
|
|
4810
|
+
RETURN up.createdAt = $now AS isNew`,
|
|
4811
|
+
{ accountId, userId, now: (/* @__PURE__ */ new Date()).toISOString(), serverTimezone }
|
|
4812
|
+
);
|
|
4813
|
+
if (mergeResult.records.length === 0) {
|
|
4814
|
+
console.error(`[profile] loadUserProfile: AdminUser not found for userId=${userId} accountId=${accountId.slice(0, 8)}\u2026 \u2014 profile not created, invariant violated`);
|
|
4815
|
+
return null;
|
|
4816
|
+
}
|
|
4817
|
+
const isNew = mergeResult.records[0].get("isNew");
|
|
4818
|
+
if (isNew) {
|
|
4819
|
+
console.error(`[profile] created new profile: userId=${userId} accountId=${accountId.slice(0, 8)}\u2026 timezone=${serverTimezone}`);
|
|
4820
|
+
}
|
|
4821
|
+
const nowMs = Date.now();
|
|
4822
|
+
const thresholdMs = DECAY_THRESHOLD_DAYS * 24 * 60 * 60 * 1e3;
|
|
4823
|
+
const staleResult = await session.run(
|
|
4824
|
+
`MATCH (up:UserProfile {accountId: $accountId, userId: $userId})-[:HAS_PREFERENCE]->(pref:Preference)
|
|
4825
|
+
WHERE pref.observedAt IS NOT NULL
|
|
4826
|
+
RETURN pref.preferenceId AS preferenceId,
|
|
4827
|
+
pref.observedAt AS observedAt,
|
|
4828
|
+
pref.confidence AS confidence`,
|
|
4829
|
+
{ accountId, userId }
|
|
4830
|
+
);
|
|
4831
|
+
let decayCount = 0;
|
|
4832
|
+
for (const record of staleResult.records) {
|
|
4833
|
+
const observedAt = record.get("observedAt");
|
|
4834
|
+
const confidence = record.get("confidence");
|
|
4835
|
+
const observedMs = new Date(observedAt).getTime();
|
|
4836
|
+
const ageMs = nowMs - observedMs;
|
|
4837
|
+
if (ageMs > thresholdMs && confidence > 0) {
|
|
4838
|
+
const daysPastThreshold = (ageMs - thresholdMs) / (24 * 60 * 60 * 1e3);
|
|
4839
|
+
const decayedConfidence = Math.max(0, confidence - DECAY_RATE_PER_DAY * daysPastThreshold);
|
|
4840
|
+
if (decayedConfidence !== confidence) {
|
|
4841
|
+
await session.run(
|
|
4842
|
+
`MATCH (pref:Preference {preferenceId: $preferenceId, accountId: $accountId})
|
|
4843
|
+
SET pref.confidence = $confidence`,
|
|
4844
|
+
{
|
|
4845
|
+
preferenceId: record.get("preferenceId"),
|
|
4846
|
+
accountId,
|
|
4847
|
+
confidence: decayedConfidence
|
|
4848
|
+
}
|
|
4849
|
+
);
|
|
4850
|
+
decayCount++;
|
|
4851
|
+
}
|
|
4852
|
+
}
|
|
4853
|
+
}
|
|
4854
|
+
const result = await session.run(
|
|
4855
|
+
`MATCH (up:UserProfile {accountId: $accountId, userId: $userId})
|
|
4856
|
+
OPTIONAL MATCH (au:AdminUser {userId: $userId})-[:OWNS]->(p:Person {accountId: $accountId})
|
|
4857
|
+
OPTIONAL MATCH (up)-[:HAS_PREFERENCE]->(pref:Preference)
|
|
4858
|
+
WHERE pref.confidence >= $threshold OR pref.notApplicable = true
|
|
4859
|
+
WITH up, p, pref
|
|
4860
|
+
ORDER BY pref.confidence DESC
|
|
4861
|
+
WITH up, p, collect(pref) AS allPrefs
|
|
4862
|
+
WITH up, p, allPrefs[0..$limit] AS prefs
|
|
4863
|
+
RETURN up, p AS person, prefs`,
|
|
4864
|
+
{ accountId, userId, threshold: INJECTION_THRESHOLD, limit: MAX_SUMMARY_PREFERENCES }
|
|
4865
|
+
);
|
|
4866
|
+
if (result.records.length === 0 || !result.records[0].get("up")) {
|
|
4867
|
+
return null;
|
|
4868
|
+
}
|
|
4869
|
+
const upNode = result.records[0].get("up");
|
|
4870
|
+
const profileProps = { ...upNode.properties };
|
|
4871
|
+
const personNode = result.records[0].get("person");
|
|
4872
|
+
if (personNode?.properties) {
|
|
4873
|
+
if (personNode.properties.givenName !== void 0) profileProps.givenName = personNode.properties.givenName;
|
|
4874
|
+
if (personNode.properties.familyName !== void 0) profileProps.familyName = personNode.properties.familyName;
|
|
4875
|
+
if (personNode.properties.email !== void 0) profileProps.email = personNode.properties.email;
|
|
4876
|
+
if (personNode.properties.telephone !== void 0) profileProps.telephone = personNode.properties.telephone;
|
|
4877
|
+
}
|
|
4878
|
+
const prefNodes = result.records[0].get("prefs");
|
|
4879
|
+
const preferences = prefNodes.map((p) => ({
|
|
4880
|
+
preferenceId: p.properties.preferenceId,
|
|
4881
|
+
category: p.properties.category,
|
|
4882
|
+
key: p.properties.key,
|
|
4883
|
+
value: p.properties.value,
|
|
4884
|
+
confidence: p.properties.confidence,
|
|
4885
|
+
source: p.properties.source,
|
|
4886
|
+
observedAt: p.properties.observedAt,
|
|
4887
|
+
notApplicable: p.properties.notApplicable === true
|
|
4888
|
+
}));
|
|
4889
|
+
const extraTemplates = loadEnabledPluginProfileKeys(accountId);
|
|
4890
|
+
const extraFieldCount = Object.values(extraTemplates).reduce((n, fs) => n + fs.length, 0);
|
|
4891
|
+
const summary = formatProfileSummary(profileProps, preferences, extraTemplates);
|
|
4892
|
+
const absentFields = computeAbsentFields(preferences, extraTemplates);
|
|
4893
|
+
const totalFields = TOTAL_PROFILE_FIELDS + extraFieldCount;
|
|
4894
|
+
const fieldCoverageCount = totalFields - absentFields.length;
|
|
4895
|
+
const presentCategories = new Set(
|
|
4896
|
+
preferences.filter((p) => p.notApplicable === true || p.confidence >= INJECTION_THRESHOLD).map((p) => p.category)
|
|
4897
|
+
);
|
|
4898
|
+
const absentCategories = PROFILE_CATEGORIES.filter((c) => !presentCategories.has(c));
|
|
4899
|
+
const categoryCoverage = PROFILE_CATEGORIES.length - absentCategories.length;
|
|
4900
|
+
console.error(
|
|
4901
|
+
`[profile] loaded for userId=${userId} accountId=${accountId.slice(0, 8)}\u2026 preferences=${preferences.length} fieldCoverage=${fieldCoverageCount}/${totalFields} categoryCoverage=${categoryCoverage}/${PROFILE_CATEGORIES.length} (decay: ${decayCount} updated)`
|
|
4902
|
+
);
|
|
4903
|
+
const identityKeys = [
|
|
4904
|
+
{ prop: "email", token: "email" },
|
|
4905
|
+
// Schema-canonical name. `phone` is the synonym (rejected at tool
|
|
4906
|
+
// surface); the log uses `telephone` to keep grep aligned with
|
|
4907
|
+
// schema-base.md doctrine the brief enforces.
|
|
4908
|
+
{ prop: "telephone", token: "telephone" },
|
|
4909
|
+
{ prop: "givenName", token: "givenName" },
|
|
4910
|
+
{ prop: "familyName", token: "familyName" },
|
|
4911
|
+
{ prop: "role", token: "role" }
|
|
4912
|
+
];
|
|
4913
|
+
const absentIdentity = identityKeys.filter((k) => {
|
|
4914
|
+
const v = profileProps[k.prop];
|
|
4915
|
+
return typeof v !== "string" || v.trim().length === 0;
|
|
4916
|
+
});
|
|
4917
|
+
const sparseFired = absentCategories.length > 0 || absentFields.length > 0 || absentIdentity.length > 0;
|
|
4918
|
+
if (conversationId && sparseFired) {
|
|
4919
|
+
const dedupeKey = `${accountId}:${userId}:${conversationId}`;
|
|
4920
|
+
if (!sparseStateLogged.has(dedupeKey)) {
|
|
4921
|
+
sparseStateLogged.add(dedupeKey);
|
|
4922
|
+
const FIELDS_LOG_CAP = 10;
|
|
4923
|
+
const absentFieldsTruncated = absentFields.length <= FIELDS_LOG_CAP ? absentFields.join(",") : `${absentFields.slice(0, FIELDS_LOG_CAP).join(",")}+${absentFields.length - FIELDS_LOG_CAP} more`;
|
|
4924
|
+
console.error(
|
|
4925
|
+
`[profile] sparse-state injected accountId=${accountId.slice(0, 8)}\u2026 userId=${userId} conversationId=${conversationId.slice(0, 8)}\u2026 absentCategories=${absentCategories.join(",")} absentFields=${absentFieldsTruncated || "none"} absentIdentity=${absentIdentity.map((k) => k.token).join(",") || "none"}`
|
|
4926
|
+
);
|
|
4927
|
+
}
|
|
4928
|
+
}
|
|
4929
|
+
return summary;
|
|
4930
|
+
} catch (err) {
|
|
4931
|
+
console.error(`[profile] loadUserProfile failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
4932
|
+
return null;
|
|
4933
|
+
} finally {
|
|
4934
|
+
await session.close();
|
|
4935
|
+
}
|
|
4936
|
+
}
|
|
4937
|
+
function formatProfileSummary(profile, preferences, extraTemplates = {}) {
|
|
4938
|
+
const summary = composeOperatorSummary(profile, preferences);
|
|
4939
|
+
return [
|
|
4940
|
+
`This is what you know about the operator: ${summary}.`,
|
|
4941
|
+
`You must seek to MAXIMISE what you know about the operator to do your job effectively.`
|
|
4942
|
+
].join("\n");
|
|
4943
|
+
}
|
|
4944
|
+
function composeOperatorSummary(profile, preferences) {
|
|
4945
|
+
const facts = [];
|
|
4946
|
+
const givenName = typeof profile.givenName === "string" ? profile.givenName.trim() : "";
|
|
4947
|
+
const familyName = typeof profile.familyName === "string" ? profile.familyName.trim() : "";
|
|
4948
|
+
const fullName = [givenName, familyName].filter((s) => s.length > 0).join(" ");
|
|
4949
|
+
if (fullName) facts.push(`name ${fullName}`);
|
|
4950
|
+
if (typeof profile.role === "string" && profile.role.trim()) {
|
|
4951
|
+
facts.push(`role ${profile.role.trim()}`);
|
|
4952
|
+
}
|
|
4953
|
+
if (typeof profile.timezone === "string" && profile.timezone.trim()) {
|
|
4954
|
+
facts.push(`timezone ${profile.timezone.trim()}`);
|
|
4955
|
+
}
|
|
4956
|
+
if (typeof profile.locale === "string" && profile.locale.trim()) {
|
|
4957
|
+
facts.push(`locale ${profile.locale.trim()}`);
|
|
4958
|
+
}
|
|
4959
|
+
if (typeof profile.email === "string" && profile.email.trim()) {
|
|
4960
|
+
facts.push(`email ${profile.email.trim()}`);
|
|
4961
|
+
}
|
|
4962
|
+
if (typeof profile.telephone === "string" && profile.telephone.trim()) {
|
|
4963
|
+
facts.push(`telephone ${profile.telephone.trim()}`);
|
|
4964
|
+
}
|
|
4965
|
+
if (profile.expertise) {
|
|
4966
|
+
const expVal = typeof profile.expertise === "string" ? profile.expertise : JSON.stringify(profile.expertise);
|
|
4967
|
+
if (expVal.trim()) facts.push(`expertise ${expVal.trim()}`);
|
|
4968
|
+
}
|
|
4969
|
+
const byCategory = {};
|
|
4970
|
+
for (const pref of preferences) {
|
|
4971
|
+
if (pref.notApplicable === true) continue;
|
|
4972
|
+
if (typeof pref.value !== "string" || pref.value.trim().length === 0) continue;
|
|
4973
|
+
if (!byCategory[pref.category]) byCategory[pref.category] = [];
|
|
4974
|
+
byCategory[pref.category].push(pref.value.trim());
|
|
4975
|
+
}
|
|
4976
|
+
for (const [cat, vals] of Object.entries(byCategory)) {
|
|
4977
|
+
facts.push(`${cat} ${vals.join(", ")}`);
|
|
4978
|
+
}
|
|
4979
|
+
return facts.length > 0 ? facts.join("; ") : "NOTHING";
|
|
4980
|
+
}
|
|
4981
|
+
var RECENT_FAILURES_TAIL_BYTES = 10 * 1024;
|
|
4982
|
+
var AGENT_FILE_ROLES = [
|
|
4983
|
+
{ filename: "IDENTITY.md", role: "identity" },
|
|
4984
|
+
{ filename: "SOUL.md", role: "soul" },
|
|
4985
|
+
{ filename: "KNOWLEDGE.md", role: "knowledge" },
|
|
4986
|
+
{ filename: "KNOWLEDGE-SUMMARY.md", role: "knowledge-summary" }
|
|
4987
|
+
];
|
|
4988
|
+
var ROLE_TO_EDGE = {
|
|
4989
|
+
identity: "HAS_IDENTITY",
|
|
4990
|
+
soul: "HAS_SOUL",
|
|
4991
|
+
knowledge: "HAS_KNOWLEDGE",
|
|
4992
|
+
"knowledge-summary": "HAS_KNOWLEDGE"
|
|
4993
|
+
};
|
|
4994
|
+
function assertSafeAgentSlug(slug) {
|
|
4995
|
+
if (!slug || slug.includes("/") || slug.includes("\\") || slug.includes("..")) {
|
|
4996
|
+
throw new Error(`[agent-graph] refusing unsafe slug=${JSON.stringify(slug)}`);
|
|
4997
|
+
}
|
|
4998
|
+
}
|
|
4999
|
+
function agentAttachmentId(accountId, slug, role) {
|
|
5000
|
+
return `agent:${accountId}:${slug}:${role}`;
|
|
5001
|
+
}
|
|
5002
|
+
async function projectAgent(accountId, accountDir, slug) {
|
|
5003
|
+
const start = Date.now();
|
|
5004
|
+
const account8 = accountId.slice(0, 8);
|
|
5005
|
+
try {
|
|
5006
|
+
assertSafeAgentSlug(slug);
|
|
5007
|
+
} catch (err) {
|
|
5008
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
5009
|
+
console.error(
|
|
5010
|
+
`[agent-graph] project FAILED slug=${slug} account=${account8} error="${msg}"`
|
|
5011
|
+
);
|
|
5012
|
+
return;
|
|
5013
|
+
}
|
|
5014
|
+
const agentDir = resolve5(accountDir, "agents", slug);
|
|
5015
|
+
const configPath = resolve5(agentDir, "config.json");
|
|
5016
|
+
let config;
|
|
5017
|
+
let presentRoles = [];
|
|
5018
|
+
try {
|
|
5019
|
+
if (!existsSync6(configPath)) {
|
|
5020
|
+
console.error(
|
|
5021
|
+
`[agent-graph] project FAILED slug=${slug} account=${account8} error="config.json missing at ${configPath}"`
|
|
5022
|
+
);
|
|
5023
|
+
return;
|
|
5024
|
+
}
|
|
5025
|
+
config = JSON.parse(readFileSync6(configPath, "utf-8"));
|
|
5026
|
+
for (const { filename, role } of AGENT_FILE_ROLES) {
|
|
5027
|
+
const filePath = resolve5(agentDir, filename);
|
|
5028
|
+
if (!existsSync6(filePath)) continue;
|
|
5029
|
+
const body = readFileSync6(filePath, "utf-8");
|
|
5030
|
+
presentRoles.push({ role, body, edge: ROLE_TO_EDGE[role] });
|
|
5031
|
+
}
|
|
5032
|
+
} catch (err) {
|
|
5033
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
5034
|
+
console.error(
|
|
5035
|
+
`[agent-graph] project FAILED slug=${slug} account=${account8} error="${msg}"`
|
|
5036
|
+
);
|
|
5037
|
+
return;
|
|
5038
|
+
}
|
|
5039
|
+
const session = getSession();
|
|
5040
|
+
try {
|
|
5041
|
+
await session.run(
|
|
5042
|
+
`MERGE (a:Agent {accountId: $accountId, slug: $slug})
|
|
5043
|
+
ON CREATE SET a.createdAt = datetime()
|
|
5044
|
+
SET a.displayName = $displayName,
|
|
5045
|
+
a.status = $status,
|
|
5046
|
+
a.model = $model,
|
|
5047
|
+
a.liveMemory = $liveMemory,
|
|
5048
|
+
a.knowledgeKeywords = $knowledgeKeywords,
|
|
5049
|
+
a.role = 'agent',
|
|
5050
|
+
a.updatedAt = datetime()
|
|
5051
|
+
RETURN a.slug AS slug`,
|
|
5052
|
+
{
|
|
5053
|
+
accountId,
|
|
5054
|
+
slug,
|
|
5055
|
+
displayName: config.displayName ?? slug,
|
|
5056
|
+
status: config.status ?? "unknown",
|
|
5057
|
+
model: config.model ?? "",
|
|
5058
|
+
liveMemory: config.liveMemory ?? false,
|
|
5059
|
+
knowledgeKeywords: config.knowledgeKeywords ?? []
|
|
5060
|
+
}
|
|
5061
|
+
);
|
|
5062
|
+
for (const { role, body, edge } of presentRoles) {
|
|
5063
|
+
const attachmentId = agentAttachmentId(accountId, slug, role);
|
|
5064
|
+
await session.run(
|
|
5065
|
+
`MATCH (a:Agent {accountId: $accountId, slug: $slug})
|
|
5066
|
+
MERGE (k:KnowledgeDocument {attachmentId: $attachmentId})
|
|
5067
|
+
ON CREATE SET k.createdAt = datetime()
|
|
5068
|
+
SET k.accountId = $accountId,
|
|
5069
|
+
k.role = $role,
|
|
5070
|
+
k.name = $name,
|
|
5071
|
+
k.text = $text,
|
|
5072
|
+
k.scope = 'admin',
|
|
5073
|
+
k.updatedAt = datetime()
|
|
5074
|
+
MERGE (a)-[:${edge}]->(k)
|
|
5075
|
+
RETURN k.attachmentId AS attachmentId`,
|
|
5076
|
+
{
|
|
5077
|
+
accountId,
|
|
5078
|
+
slug,
|
|
5079
|
+
attachmentId,
|
|
5080
|
+
role,
|
|
5081
|
+
name: `${slug} ${role}`,
|
|
5082
|
+
text: body
|
|
5083
|
+
}
|
|
5084
|
+
);
|
|
5085
|
+
}
|
|
5086
|
+
const usesResult = await session.run(
|
|
5087
|
+
`MATCH (a:Agent {accountId: $accountId, slug: $slug})
|
|
5088
|
+
MATCH (k:KnowledgeDocument {accountId: $accountId})
|
|
5089
|
+
WHERE $slug IN coalesce(k.agents, [])
|
|
5090
|
+
AND NOT k.attachmentId STARTS WITH $namespacePrefix
|
|
5091
|
+
MERGE (a)-[:USES_KNOWLEDGE]->(k)
|
|
5092
|
+
RETURN count(k) AS uses`,
|
|
5093
|
+
{
|
|
5094
|
+
accountId,
|
|
5095
|
+
slug,
|
|
5096
|
+
namespacePrefix: `agent:${accountId}:${slug}:`
|
|
5097
|
+
}
|
|
5098
|
+
);
|
|
5099
|
+
const uses = usesResult.records[0]?.get("uses");
|
|
5100
|
+
const usesCount = typeof uses === "number" ? uses : uses && typeof uses.toNumber === "function" ? uses.toNumber() : 0;
|
|
5101
|
+
const ms = Date.now() - start;
|
|
5102
|
+
console.error(
|
|
5103
|
+
`[agent-graph] project slug=${slug} account=${account8} docs=${presentRoles.length} uses=${usesCount} ms=${ms}`
|
|
5104
|
+
);
|
|
5105
|
+
} catch (err) {
|
|
5106
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
5107
|
+
console.error(
|
|
5108
|
+
`[agent-graph] project FAILED slug=${slug} account=${account8} error="${msg}"`
|
|
5109
|
+
);
|
|
5110
|
+
throw err;
|
|
5111
|
+
} finally {
|
|
5112
|
+
await session.close();
|
|
5113
|
+
}
|
|
5114
|
+
}
|
|
5115
|
+
async function deleteAgentProjection(accountId, slug) {
|
|
5116
|
+
const start = Date.now();
|
|
5117
|
+
const account8 = accountId.slice(0, 8);
|
|
5118
|
+
assertSafeAgentSlug(slug);
|
|
5119
|
+
const session = getSession();
|
|
5120
|
+
try {
|
|
5121
|
+
await session.run(
|
|
5122
|
+
`MATCH (a:Agent {accountId: $accountId, slug: $slug})
|
|
5123
|
+
DETACH DELETE a
|
|
5124
|
+
WITH 1 AS _
|
|
5125
|
+
UNWIND $attachmentIds AS aid
|
|
5126
|
+
OPTIONAL MATCH (k:KnowledgeDocument {accountId: $accountId, attachmentId: aid})
|
|
5127
|
+
WHERE k.attachmentId STARTS WITH $namespacePrefix
|
|
5128
|
+
DETACH DELETE k`,
|
|
5129
|
+
{
|
|
5130
|
+
accountId,
|
|
5131
|
+
slug,
|
|
5132
|
+
namespacePrefix: `agent:${accountId}:${slug}:`,
|
|
5133
|
+
attachmentIds: ["identity", "soul", "knowledge", "knowledge-summary"].map((role) => agentAttachmentId(accountId, slug, role))
|
|
5134
|
+
}
|
|
5135
|
+
);
|
|
5136
|
+
const ms = Date.now() - start;
|
|
5137
|
+
console.error(
|
|
5138
|
+
`[agent-graph] delete slug=${slug} account=${account8} ms=${ms}`
|
|
5139
|
+
);
|
|
5140
|
+
} catch (err) {
|
|
5141
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
5142
|
+
console.error(
|
|
5143
|
+
`[agent-graph] delete FAILED slug=${slug} account=${account8} error="${msg}"`
|
|
5144
|
+
);
|
|
5145
|
+
throw err;
|
|
5146
|
+
} finally {
|
|
5147
|
+
await session.close();
|
|
5148
|
+
}
|
|
5149
|
+
}
|
|
5150
|
+
|
|
3844
5151
|
// server/routes/_helpers.ts
|
|
3845
5152
|
async function safeJson(c) {
|
|
3846
5153
|
try {
|
|
@@ -3964,486 +5271,6 @@ var clientIpMiddleware = async (c, next) => {
|
|
|
3964
5271
|
await next();
|
|
3965
5272
|
};
|
|
3966
5273
|
|
|
3967
|
-
// server/lib/action-runner.ts
|
|
3968
|
-
import { spawn } from "child_process";
|
|
3969
|
-
import { createReadStream, existsSync as existsSync6, mkdirSync as mkdirSync3, readdirSync as readdirSync3, statSync as statSync3, unlinkSync, watch } from "fs";
|
|
3970
|
-
import { homedir as homedir2 } from "os";
|
|
3971
|
-
import { join as join4, resolve as resolve5 } from "path";
|
|
3972
|
-
import { readFileSync as readFileSync6 } from "fs";
|
|
3973
|
-
import { setTimeout as delay } from "timers/promises";
|
|
3974
|
-
function loadBrandInfo() {
|
|
3975
|
-
const platformRoot2 = process.env.MAXY_PLATFORM_ROOT ?? resolve5(process.cwd(), "..");
|
|
3976
|
-
const brandPath = resolve5(platformRoot2, "config", "brand.json");
|
|
3977
|
-
try {
|
|
3978
|
-
const parsed = JSON.parse(readFileSync6(brandPath, "utf-8"));
|
|
3979
|
-
const hostname = typeof parsed.hostname === "string" && parsed.hostname ? parsed.hostname : "maxy";
|
|
3980
|
-
const configDir = typeof parsed.configDir === "string" && parsed.configDir ? parsed.configDir : ".maxy";
|
|
3981
|
-
return { hostname, configDir };
|
|
3982
|
-
} catch {
|
|
3983
|
-
return { hostname: "maxy", configDir: ".maxy" };
|
|
3984
|
-
}
|
|
3985
|
-
}
|
|
3986
|
-
function actionLogDir() {
|
|
3987
|
-
const { configDir } = loadBrandInfo();
|
|
3988
|
-
return join4(homedir2(), configDir, "logs", "actions");
|
|
3989
|
-
}
|
|
3990
|
-
function actionLogPath(actionId) {
|
|
3991
|
-
return join4(actionLogDir(), `${actionId}.log`);
|
|
3992
|
-
}
|
|
3993
|
-
var WHITELIST = {
|
|
3994
|
-
upgrade: {
|
|
3995
|
-
build: ({ sudoPassword }) => {
|
|
3996
|
-
const { hostname } = loadBrandInfo();
|
|
3997
|
-
const pkg = `@rubytech/create-${hostname}`;
|
|
3998
|
-
const cmd = [
|
|
3999
|
-
// prime: read pw from stdin, silently cache
|
|
4000
|
-
'printf %s "$SUDO_PASSWORD"',
|
|
4001
|
-
"|",
|
|
4002
|
-
"sudo -S -v 2>/dev/null",
|
|
4003
|
-
"&&",
|
|
4004
|
-
// run installer — it will re-prompt only if the cache expires
|
|
4005
|
-
`npx -y ${pkg}@latest`
|
|
4006
|
-
].join(" ");
|
|
4007
|
-
return {
|
|
4008
|
-
argv: ["bash", "-c", cmd],
|
|
4009
|
-
env: sudoPassword ? { SUDO_PASSWORD: sudoPassword } : void 0
|
|
4010
|
-
};
|
|
4011
|
-
}
|
|
4012
|
-
},
|
|
4013
|
-
"cloudflare-setup": {
|
|
4014
|
-
build: ({ positional, streamLogPath, accountDir, tunnelId, tunnelName }) => {
|
|
4015
|
-
if (!positional || positional.length < 3) {
|
|
4016
|
-
throw new Error("cloudflare-setup requires [brand, port, ...hostnames]");
|
|
4017
|
-
}
|
|
4018
|
-
if (!accountDir) {
|
|
4019
|
-
throw new Error("cloudflare-setup requires accountDir");
|
|
4020
|
-
}
|
|
4021
|
-
const idSet = typeof tunnelId === "string" && tunnelId.length > 0;
|
|
4022
|
-
const nameSet = typeof tunnelName === "string" && tunnelName.length > 0;
|
|
4023
|
-
if (idSet && nameSet) {
|
|
4024
|
-
throw new Error("cloudflare-setup: tunnelId and tunnelName are mutually exclusive \u2014 pass exactly one");
|
|
4025
|
-
}
|
|
4026
|
-
if (!idSet && !nameSet) {
|
|
4027
|
-
throw new Error("cloudflare-setup: requires tunnelId (operator-selected) OR tunnelName (operator-created)");
|
|
4028
|
-
}
|
|
4029
|
-
const scriptPath = join4(homedir2(), "setup-tunnel.sh");
|
|
4030
|
-
if (!existsSync6(scriptPath)) {
|
|
4031
|
-
throw new Error(
|
|
4032
|
-
`cloudflare-setup: ~/setup-tunnel.sh not found. Re-run the installer to repair the symlink.`
|
|
4033
|
-
);
|
|
4034
|
-
}
|
|
4035
|
-
const platformRoot2 = process.env.MAXY_PLATFORM_ROOT ?? resolve5(process.cwd(), "..");
|
|
4036
|
-
return {
|
|
4037
|
-
argv: [scriptPath, ...positional],
|
|
4038
|
-
env: {
|
|
4039
|
-
MAXY_PLATFORM_ROOT: platformRoot2,
|
|
4040
|
-
...streamLogPath ? { STREAM_LOG_PATH: streamLogPath } : {},
|
|
4041
|
-
ACCOUNT_DIR: accountDir,
|
|
4042
|
-
...idSet ? { TUNNEL_ID: tunnelId } : {},
|
|
4043
|
-
...nameSet ? { TUNNEL_NAME: tunnelName } : {}
|
|
4044
|
-
}
|
|
4045
|
-
};
|
|
4046
|
-
},
|
|
4047
|
-
reconcileFromLog: reconcileCloudflareSetupFromLog
|
|
4048
|
-
}
|
|
4049
|
-
};
|
|
4050
|
-
var ARMING_LINE_RE = /\bstep=service-restart-armed\b[^\n]*\bexit=0\b/;
|
|
4051
|
-
var DONE_LINE_RE = /\bstep=done\b/;
|
|
4052
|
-
var ERROR_LINE_RE = /\bresult=error\b/;
|
|
4053
|
-
function reconcileCloudflareSetupFromLog(lines) {
|
|
4054
|
-
let hasArming = false;
|
|
4055
|
-
let hasDone = false;
|
|
4056
|
-
let hasError = false;
|
|
4057
|
-
for (const line of lines) {
|
|
4058
|
-
if (!hasArming && ARMING_LINE_RE.test(line)) hasArming = true;
|
|
4059
|
-
if (!hasDone && DONE_LINE_RE.test(line)) hasDone = true;
|
|
4060
|
-
if (!hasError && ERROR_LINE_RE.test(line)) hasError = true;
|
|
4061
|
-
}
|
|
4062
|
-
if (hasDone && hasArming) return { kind: "succeeded", code: 0 };
|
|
4063
|
-
if (hasError) return { kind: "failed", code: 1 };
|
|
4064
|
-
if (hasArming && !hasDone) return { kind: "restart-in-progress", code: null };
|
|
4065
|
-
return null;
|
|
4066
|
-
}
|
|
4067
|
-
function isKnownAction(name) {
|
|
4068
|
-
return Object.prototype.hasOwnProperty.call(WHITELIST, name);
|
|
4069
|
-
}
|
|
4070
|
-
function generateActionId(name) {
|
|
4071
|
-
const ts = Date.now().toString(36);
|
|
4072
|
-
const nonce = Math.floor(Math.random() * 4294967295).toString(16).padStart(8, "0");
|
|
4073
|
-
return `${name}-${ts}-${nonce}`;
|
|
4074
|
-
}
|
|
4075
|
-
function actionNameFromId(actionId) {
|
|
4076
|
-
for (const name of Object.keys(WHITELIST)) {
|
|
4077
|
-
if (actionId.startsWith(`${name}-`)) return name;
|
|
4078
|
-
}
|
|
4079
|
-
return "unknown";
|
|
4080
|
-
}
|
|
4081
|
-
function cleanupStaleLogs() {
|
|
4082
|
-
const dir = actionLogDir();
|
|
4083
|
-
if (!existsSync6(dir)) return;
|
|
4084
|
-
const cutoff = Date.now() - 7 * 24 * 60 * 60 * 1e3;
|
|
4085
|
-
for (const entry of readdirSync3(dir)) {
|
|
4086
|
-
const full = join4(dir, entry);
|
|
4087
|
-
try {
|
|
4088
|
-
const st = statSync3(full);
|
|
4089
|
-
if (st.isFile() && st.mtimeMs < cutoff) unlinkSync(full);
|
|
4090
|
-
} catch {
|
|
4091
|
-
}
|
|
4092
|
-
}
|
|
4093
|
-
}
|
|
4094
|
-
async function isActionActive(actionName) {
|
|
4095
|
-
return new Promise((resolveP) => {
|
|
4096
|
-
const proc = spawn(
|
|
4097
|
-
"systemctl",
|
|
4098
|
-
["--user", "list-units", "--no-legend", "--state=active", `maxy-action-${actionName}-*`],
|
|
4099
|
-
{ stdio: ["ignore", "pipe", "pipe"] }
|
|
4100
|
-
);
|
|
4101
|
-
let out = "";
|
|
4102
|
-
proc.stdout.on("data", (chunk) => {
|
|
4103
|
-
out += chunk.toString();
|
|
4104
|
-
});
|
|
4105
|
-
proc.on("close", () => resolveP(out.trim().length > 0));
|
|
4106
|
-
proc.on("error", () => resolveP(false));
|
|
4107
|
-
});
|
|
4108
|
-
}
|
|
4109
|
-
function buildSystemdArgs(name, args, actionId, logPath) {
|
|
4110
|
-
const unit = `maxy-action-${actionId}`;
|
|
4111
|
-
const built = WHITELIST[name].build(args);
|
|
4112
|
-
const systemdArgs = [
|
|
4113
|
-
"--user",
|
|
4114
|
-
`--unit=${unit}`,
|
|
4115
|
-
"--collect",
|
|
4116
|
-
"--property=RemainAfterExit=false",
|
|
4117
|
-
`--property=StandardOutput=append:${logPath}`,
|
|
4118
|
-
`--property=StandardError=append:${logPath}`,
|
|
4119
|
-
`--description=Maxy action ${name} (${actionId})`
|
|
4120
|
-
];
|
|
4121
|
-
if (built.env) {
|
|
4122
|
-
for (const [k, v] of Object.entries(built.env)) {
|
|
4123
|
-
systemdArgs.push(`--setenv=${k}=${v}`);
|
|
4124
|
-
}
|
|
4125
|
-
}
|
|
4126
|
-
systemdArgs.push("--", ...built.argv);
|
|
4127
|
-
return systemdArgs;
|
|
4128
|
-
}
|
|
4129
|
-
async function launchAction(name, args) {
|
|
4130
|
-
const actionId = generateActionId(name);
|
|
4131
|
-
const dir = actionLogDir();
|
|
4132
|
-
mkdirSync3(dir, { recursive: true });
|
|
4133
|
-
cleanupStaleLogs();
|
|
4134
|
-
const logPath = actionLogPath(actionId);
|
|
4135
|
-
const unit = `maxy-action-${actionId}`;
|
|
4136
|
-
const systemdArgs = buildSystemdArgs(name, args, actionId, logPath);
|
|
4137
|
-
await new Promise((resolveP, rejectP) => {
|
|
4138
|
-
const proc = spawn("systemd-run", systemdArgs, { stdio: ["ignore", "pipe", "pipe"] });
|
|
4139
|
-
let stderr = "";
|
|
4140
|
-
proc.stderr.on("data", (chunk) => {
|
|
4141
|
-
stderr += chunk.toString();
|
|
4142
|
-
});
|
|
4143
|
-
proc.on("close", (code) => {
|
|
4144
|
-
if (code === 0) resolveP();
|
|
4145
|
-
else rejectP(new Error(`systemd-run exited ${code}: ${stderr.trim().slice(0, 400)}`));
|
|
4146
|
-
});
|
|
4147
|
-
proc.on("error", rejectP);
|
|
4148
|
-
});
|
|
4149
|
-
console.log(`[action-runner] launched action=${name} id=${actionId} unit=${unit} log=${logPath}`);
|
|
4150
|
-
return { actionId, unit, logPath };
|
|
4151
|
-
}
|
|
4152
|
-
function parseUnitShow(out) {
|
|
4153
|
-
const kv = {};
|
|
4154
|
-
for (const line of out.split("\n")) {
|
|
4155
|
-
const eq = line.indexOf("=");
|
|
4156
|
-
if (eq > 0) kv[line.slice(0, eq)] = line.slice(eq + 1);
|
|
4157
|
-
}
|
|
4158
|
-
const loadState = kv.LoadState ?? "";
|
|
4159
|
-
if (loadState !== "loaded" || !kv.ActiveState) {
|
|
4160
|
-
return { status: null, loadState };
|
|
4161
|
-
}
|
|
4162
|
-
return {
|
|
4163
|
-
status: {
|
|
4164
|
-
activeState: kv.ActiveState,
|
|
4165
|
-
subState: kv.SubState ?? "",
|
|
4166
|
-
mainPid: kv.MainPID ? Number(kv.MainPID) || null : null,
|
|
4167
|
-
execMainStatus: kv.ExecMainStatus ? Number(kv.ExecMainStatus) : null,
|
|
4168
|
-
execMainSignal: null
|
|
4169
|
-
// systemd reports signal in SIGNAL (SIGTERM etc.), deferred — code is enough for our contract
|
|
4170
|
-
},
|
|
4171
|
-
loadState
|
|
4172
|
-
};
|
|
4173
|
-
}
|
|
4174
|
-
async function showUnit(unit) {
|
|
4175
|
-
return new Promise((resolveP) => {
|
|
4176
|
-
const proc = spawn(
|
|
4177
|
-
"systemctl",
|
|
4178
|
-
["--user", "show", unit, "--property=ActiveState,SubState,MainPID,ExecMainStatus,ExecMainCode,LoadState"],
|
|
4179
|
-
{ stdio: ["ignore", "pipe", "pipe"] }
|
|
4180
|
-
);
|
|
4181
|
-
let out = "";
|
|
4182
|
-
proc.stdout.on("data", (chunk) => {
|
|
4183
|
-
out += chunk.toString();
|
|
4184
|
-
});
|
|
4185
|
-
proc.on("close", () => resolveP(parseUnitShow(out)));
|
|
4186
|
-
proc.on("error", () => resolveP({ status: null, loadState: "error" }));
|
|
4187
|
-
});
|
|
4188
|
-
}
|
|
4189
|
-
async function getUnitStatus(unit) {
|
|
4190
|
-
return (await showUnit(unit)).status;
|
|
4191
|
-
}
|
|
4192
|
-
var HEARTBEAT_INTERVAL_MS = 5e3;
|
|
4193
|
-
var exitEmittedFor = /* @__PURE__ */ new Set();
|
|
4194
|
-
var PHASE_BRACKETED = /\[(\d+)\/(\d+)\]/;
|
|
4195
|
-
var PHASE_STREAM_LOG = /step=([a-z0-9-]+)/;
|
|
4196
|
-
function extractPhase(text) {
|
|
4197
|
-
const m1 = text.match(PHASE_BRACKETED);
|
|
4198
|
-
if (m1) return m1[0];
|
|
4199
|
-
const m2 = text.match(PHASE_STREAM_LOG);
|
|
4200
|
-
if (m2) return m2[1];
|
|
4201
|
-
return null;
|
|
4202
|
-
}
|
|
4203
|
-
function readLogTailLines(path, tailLines) {
|
|
4204
|
-
if (!existsSync6(path)) return [];
|
|
4205
|
-
let raw2;
|
|
4206
|
-
try {
|
|
4207
|
-
raw2 = readFileSync6(path, "utf-8");
|
|
4208
|
-
} catch {
|
|
4209
|
-
return [];
|
|
4210
|
-
}
|
|
4211
|
-
const all = raw2.split("\n");
|
|
4212
|
-
if (all.length <= tailLines) return all;
|
|
4213
|
-
return all.slice(all.length - tailLines);
|
|
4214
|
-
}
|
|
4215
|
-
async function* streamActionEvents(opts) {
|
|
4216
|
-
const { actionId, unit, signal } = opts;
|
|
4217
|
-
const logPath = actionLogPath(actionId);
|
|
4218
|
-
const startedAt = Date.now();
|
|
4219
|
-
let lastLineAt = startedAt;
|
|
4220
|
-
let lastPhase = null;
|
|
4221
|
-
let byteOffset = opts.fromByteOffset ?? 0;
|
|
4222
|
-
const actionName = actionNameFromId(actionId);
|
|
4223
|
-
const reconcileFromLog = isKnownAction(actionName) ? WHITELIST[actionName].reconcileFromLog : void 0;
|
|
4224
|
-
if (existsSync6(logPath)) {
|
|
4225
|
-
const snapshot = statSync3(logPath).size;
|
|
4226
|
-
if (byteOffset < snapshot) {
|
|
4227
|
-
for await (const ev of readLogLines(logPath, byteOffset, snapshot)) {
|
|
4228
|
-
const phase = extractPhase(ev.text);
|
|
4229
|
-
if (phase) lastPhase = phase;
|
|
4230
|
-
lastLineAt = ev.ts;
|
|
4231
|
-
byteOffset = ev.byteOffset;
|
|
4232
|
-
yield ev;
|
|
4233
|
-
if (signal.aborted) return;
|
|
4234
|
-
}
|
|
4235
|
-
}
|
|
4236
|
-
}
|
|
4237
|
-
if (exitEmittedFor.has(actionId)) {
|
|
4238
|
-
return;
|
|
4239
|
-
}
|
|
4240
|
-
let watching = true;
|
|
4241
|
-
let hbTimer = null;
|
|
4242
|
-
const queue = [];
|
|
4243
|
-
let resolveNext = null;
|
|
4244
|
-
function push(ev) {
|
|
4245
|
-
queue.push(ev);
|
|
4246
|
-
if (resolveNext) {
|
|
4247
|
-
const r = resolveNext;
|
|
4248
|
-
resolveNext = null;
|
|
4249
|
-
r();
|
|
4250
|
-
}
|
|
4251
|
-
}
|
|
4252
|
-
const watcher = existsSync6(logPath) ? watch(logPath, async () => {
|
|
4253
|
-
if (!watching) return;
|
|
4254
|
-
try {
|
|
4255
|
-
const size = statSync3(logPath).size;
|
|
4256
|
-
if (size <= byteOffset) return;
|
|
4257
|
-
for await (const ev of readLogLines(logPath, byteOffset, size)) {
|
|
4258
|
-
const phase = extractPhase(ev.text);
|
|
4259
|
-
if (phase) lastPhase = phase;
|
|
4260
|
-
lastLineAt = ev.ts;
|
|
4261
|
-
byteOffset = ev.byteOffset;
|
|
4262
|
-
push(ev);
|
|
4263
|
-
}
|
|
4264
|
-
} catch (err) {
|
|
4265
|
-
console.error(`[action-runner] watcher error action=${actionId}: ${err}`);
|
|
4266
|
-
}
|
|
4267
|
-
}) : null;
|
|
4268
|
-
hbTimer = setInterval(async () => {
|
|
4269
|
-
const probe = await showUnit(unit);
|
|
4270
|
-
const status = probe.status;
|
|
4271
|
-
const now = Date.now();
|
|
4272
|
-
push({
|
|
4273
|
-
type: "heartbeat",
|
|
4274
|
-
ts: now,
|
|
4275
|
-
elapsed_since_last_line_ms: now - lastLineAt,
|
|
4276
|
-
systemd_state: status ? status.activeState : "unknown",
|
|
4277
|
-
pid: status ? status.mainPid : null,
|
|
4278
|
-
last_phase: lastPhase
|
|
4279
|
-
});
|
|
4280
|
-
if (!status) {
|
|
4281
|
-
if (exitEmittedFor.has(actionId)) {
|
|
4282
|
-
watching = false;
|
|
4283
|
-
return;
|
|
4284
|
-
}
|
|
4285
|
-
exitEmittedFor.add(actionId);
|
|
4286
|
-
console.log(`[action-runner] unit-not-loaded action=${actionNameFromId(actionId)} id=${actionId} loadState=${probe.loadState || "empty"}`);
|
|
4287
|
-
const reconciled = reconcileFromLog ? reconcileFromLog(readLogTailLines(logPath, 200)) : null;
|
|
4288
|
-
const reconcileMs = Date.now() - startedAt;
|
|
4289
|
-
if (reconciled) {
|
|
4290
|
-
console.log(
|
|
4291
|
-
`[action-runner] reconcile actionId=${actionId} result=${reconciled.kind} source=persisted-log ms=${reconcileMs}`
|
|
4292
|
-
);
|
|
4293
|
-
push({
|
|
4294
|
-
type: "exit",
|
|
4295
|
-
ts: Date.now(),
|
|
4296
|
-
code: reconciled.code,
|
|
4297
|
-
signal: null,
|
|
4298
|
-
duration_ms: reconcileMs,
|
|
4299
|
-
source: "persisted-log",
|
|
4300
|
-
...reconciled.kind === "restart-in-progress" ? { kind: "restart-in-progress" } : {}
|
|
4301
|
-
});
|
|
4302
|
-
} else {
|
|
4303
|
-
console.log(
|
|
4304
|
-
`[action-runner] reconcile actionId=${actionId} result=unresolved source=persisted-log ms=${reconcileMs}`
|
|
4305
|
-
);
|
|
4306
|
-
push({
|
|
4307
|
-
type: "exit",
|
|
4308
|
-
ts: Date.now(),
|
|
4309
|
-
code: null,
|
|
4310
|
-
signal: null,
|
|
4311
|
-
duration_ms: reconcileMs,
|
|
4312
|
-
source: "persisted-log"
|
|
4313
|
-
});
|
|
4314
|
-
}
|
|
4315
|
-
watching = false;
|
|
4316
|
-
return;
|
|
4317
|
-
}
|
|
4318
|
-
if (status.activeState !== "active" && status.activeState !== "activating") {
|
|
4319
|
-
if (exitEmittedFor.has(actionId)) {
|
|
4320
|
-
watching = false;
|
|
4321
|
-
return;
|
|
4322
|
-
}
|
|
4323
|
-
exitEmittedFor.add(actionId);
|
|
4324
|
-
const finalStatus = await getUnitStatus(unit);
|
|
4325
|
-
if (finalStatus?.execMainStatus == null && reconcileFromLog) {
|
|
4326
|
-
const reconciled = reconcileFromLog(readLogTailLines(logPath, 200));
|
|
4327
|
-
const reconcileMs = Date.now() - startedAt;
|
|
4328
|
-
if (reconciled) {
|
|
4329
|
-
console.log(
|
|
4330
|
-
`[action-runner] reconcile actionId=${actionId} result=${reconciled.kind} source=persisted-log ms=${reconcileMs}`
|
|
4331
|
-
);
|
|
4332
|
-
push({
|
|
4333
|
-
type: "exit",
|
|
4334
|
-
ts: Date.now(),
|
|
4335
|
-
code: reconciled.code,
|
|
4336
|
-
signal: null,
|
|
4337
|
-
duration_ms: reconcileMs,
|
|
4338
|
-
source: "persisted-log",
|
|
4339
|
-
...reconciled.kind === "restart-in-progress" ? { kind: "restart-in-progress" } : {}
|
|
4340
|
-
});
|
|
4341
|
-
watching = false;
|
|
4342
|
-
return;
|
|
4343
|
-
}
|
|
4344
|
-
}
|
|
4345
|
-
const ms = Date.now() - startedAt;
|
|
4346
|
-
const source = finalStatus?.execMainStatus != null ? "systemd" : "persisted-log";
|
|
4347
|
-
const result = finalStatus?.execMainStatus === 0 ? "succeeded" : finalStatus?.execMainStatus != null ? "failed" : "unresolved";
|
|
4348
|
-
console.log(
|
|
4349
|
-
`[action-runner] reconcile actionId=${actionId} result=${result} source=${source} ms=${ms}`
|
|
4350
|
-
);
|
|
4351
|
-
push({
|
|
4352
|
-
type: "exit",
|
|
4353
|
-
ts: Date.now(),
|
|
4354
|
-
code: finalStatus?.execMainStatus ?? null,
|
|
4355
|
-
signal: null,
|
|
4356
|
-
duration_ms: ms,
|
|
4357
|
-
source
|
|
4358
|
-
});
|
|
4359
|
-
watching = false;
|
|
4360
|
-
}
|
|
4361
|
-
}, HEARTBEAT_INTERVAL_MS);
|
|
4362
|
-
try {
|
|
4363
|
-
while (watching && !signal.aborted) {
|
|
4364
|
-
if (queue.length === 0) {
|
|
4365
|
-
await new Promise((r) => {
|
|
4366
|
-
resolveNext = r;
|
|
4367
|
-
});
|
|
4368
|
-
}
|
|
4369
|
-
while (queue.length > 0) {
|
|
4370
|
-
const ev = queue.shift();
|
|
4371
|
-
yield ev;
|
|
4372
|
-
if (ev.type === "exit") {
|
|
4373
|
-
watching = false;
|
|
4374
|
-
break;
|
|
4375
|
-
}
|
|
4376
|
-
}
|
|
4377
|
-
}
|
|
4378
|
-
} finally {
|
|
4379
|
-
if (hbTimer) clearInterval(hbTimer);
|
|
4380
|
-
watcher?.close();
|
|
4381
|
-
}
|
|
4382
|
-
}
|
|
4383
|
-
async function* readLogLines(path, fromOffset, toOffset) {
|
|
4384
|
-
const stream = createReadStream(path, { start: fromOffset, end: toOffset - 1, encoding: "utf-8" });
|
|
4385
|
-
let buffer = "";
|
|
4386
|
-
let offset = fromOffset;
|
|
4387
|
-
for await (const chunk of stream) {
|
|
4388
|
-
buffer += chunk;
|
|
4389
|
-
let nl;
|
|
4390
|
-
while ((nl = buffer.indexOf("\n")) !== -1) {
|
|
4391
|
-
const text = buffer.slice(0, nl);
|
|
4392
|
-
offset += Buffer.byteLength(text, "utf-8") + 1;
|
|
4393
|
-
buffer = buffer.slice(nl + 1);
|
|
4394
|
-
yield { type: "line", ts: Date.now(), stream: "stdout", text, byteOffset: offset };
|
|
4395
|
-
}
|
|
4396
|
-
}
|
|
4397
|
-
}
|
|
4398
|
-
var RATE_LIMIT_WINDOW_MS = 6e4;
|
|
4399
|
-
var RATE_LIMIT_MAX_ATTEMPTS = 5;
|
|
4400
|
-
var rateLimitBuckets = /* @__PURE__ */ new Map();
|
|
4401
|
-
function rateLimitCheck(ip) {
|
|
4402
|
-
const now = Date.now();
|
|
4403
|
-
const bucket = rateLimitBuckets.get(ip);
|
|
4404
|
-
if (!bucket || now - bucket.windowStart > RATE_LIMIT_WINDOW_MS) {
|
|
4405
|
-
rateLimitBuckets.set(ip, { count: 1, windowStart: now });
|
|
4406
|
-
return "ok";
|
|
4407
|
-
}
|
|
4408
|
-
bucket.count += 1;
|
|
4409
|
-
if (bucket.count > RATE_LIMIT_MAX_ATTEMPTS) return "limited";
|
|
4410
|
-
return "ok";
|
|
4411
|
-
}
|
|
4412
|
-
async function primeSudo(password, clientIp) {
|
|
4413
|
-
if (!password) return "invalid";
|
|
4414
|
-
if (rateLimitCheck(clientIp) === "limited") return "limited";
|
|
4415
|
-
return new Promise((resolveP) => {
|
|
4416
|
-
const proc = spawn("sudo", ["-S", "-v"], { stdio: ["pipe", "ignore", "pipe"] });
|
|
4417
|
-
let stderr = "";
|
|
4418
|
-
proc.stderr.on("data", (chunk) => {
|
|
4419
|
-
stderr += chunk.toString();
|
|
4420
|
-
});
|
|
4421
|
-
proc.on("close", (code) => {
|
|
4422
|
-
if (code === 0) return resolveP("ok");
|
|
4423
|
-
if (/incorrect password|authentication failure/i.test(stderr)) return resolveP("invalid");
|
|
4424
|
-
console.error(`[action-runner] sudo-prime failed code=${code} stderr=${stderr.slice(0, 200)}`);
|
|
4425
|
-
resolveP("error");
|
|
4426
|
-
});
|
|
4427
|
-
proc.on("error", (err) => {
|
|
4428
|
-
console.error(`[action-runner] sudo-prime spawn error: ${err}`);
|
|
4429
|
-
resolveP("error");
|
|
4430
|
-
});
|
|
4431
|
-
proc.stdin.write(`${password}
|
|
4432
|
-
`);
|
|
4433
|
-
proc.stdin.end();
|
|
4434
|
-
});
|
|
4435
|
-
}
|
|
4436
|
-
async function waitForExit(unit, timeoutMs = 10 * 60 * 1e3) {
|
|
4437
|
-
const deadline = Date.now() + timeoutMs;
|
|
4438
|
-
while (Date.now() < deadline) {
|
|
4439
|
-
const status = await getUnitStatus(unit);
|
|
4440
|
-
if (!status) return null;
|
|
4441
|
-
if (status.activeState !== "active" && status.activeState !== "activating") return status;
|
|
4442
|
-
await delay(500);
|
|
4443
|
-
}
|
|
4444
|
-
return null;
|
|
4445
|
-
}
|
|
4446
|
-
|
|
4447
5274
|
export {
|
|
4448
5275
|
Hono2 as Hono,
|
|
4449
5276
|
getRequestListener,
|
|
@@ -4465,7 +5292,6 @@ export {
|
|
|
4465
5292
|
sanitizeClientCorrId,
|
|
4466
5293
|
httpLog,
|
|
4467
5294
|
websockifyLog,
|
|
4468
|
-
browserViewerLog,
|
|
4469
5295
|
validatePasswordStrength,
|
|
4470
5296
|
isPasswordValid,
|
|
4471
5297
|
isRemoteAuthConfigured,
|
|
@@ -4505,16 +5331,32 @@ export {
|
|
|
4505
5331
|
emitMissingOnResolve,
|
|
4506
5332
|
stripAttachmentMetaSuffix,
|
|
4507
5333
|
requirePortEnv,
|
|
5334
|
+
getSession,
|
|
5335
|
+
runAdminUserSelfHeal,
|
|
5336
|
+
embed,
|
|
5337
|
+
GREETING_DIRECTIVE,
|
|
5338
|
+
ensureConversation,
|
|
5339
|
+
fetchBranding,
|
|
5340
|
+
persistMessage,
|
|
5341
|
+
getAgentSessionIdForConversation,
|
|
5342
|
+
getRecentMessages,
|
|
5343
|
+
verifyConversationOwnership,
|
|
5344
|
+
verifyAndGetConversationUpdatedAt,
|
|
5345
|
+
listAdminSessions,
|
|
5346
|
+
deleteConversation,
|
|
5347
|
+
linkConversationTranscript,
|
|
5348
|
+
getConversationClaudeSessionIds,
|
|
5349
|
+
getConversationIdByClaudeSessionId,
|
|
5350
|
+
renameConversation,
|
|
5351
|
+
getUserTimezone,
|
|
5352
|
+
loadAdminUserName,
|
|
5353
|
+
writeAdminUserAndPerson,
|
|
5354
|
+
loadUserProfile,
|
|
5355
|
+
projectAgent,
|
|
5356
|
+
deleteAgentProjection,
|
|
4508
5357
|
safeJson,
|
|
4509
5358
|
requireAdminSession,
|
|
4510
5359
|
tryCookieBridgeForConversation,
|
|
4511
5360
|
requireAdminCookie,
|
|
4512
|
-
clientIpMiddleware
|
|
4513
|
-
actionLogPath,
|
|
4514
|
-
isKnownAction,
|
|
4515
|
-
isActionActive,
|
|
4516
|
-
launchAction,
|
|
4517
|
-
streamActionEvents,
|
|
4518
|
-
primeSudo,
|
|
4519
|
-
waitForExit
|
|
5361
|
+
clientIpMiddleware
|
|
4520
5362
|
};
|