@rubytech/create-realagent 1.0.817 → 1.0.818

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (35) hide show
  1. package/package.json +1 -1
  2. package/payload/platform/lib/graph-search/src/__tests__/fulltext-coverage.test.ts +1 -1
  3. package/payload/platform/lib/graph-write/dist/index.d.ts +5 -0
  4. package/payload/platform/lib/graph-write/dist/index.d.ts.map +1 -1
  5. package/payload/platform/lib/graph-write/dist/index.js +14 -0
  6. package/payload/platform/lib/graph-write/dist/index.js.map +1 -1
  7. package/payload/platform/lib/graph-write/src/index.ts +25 -0
  8. package/payload/platform/neo4j/edge-annotations.json +0 -8
  9. package/payload/platform/neo4j/migrations/004-prune-alien-accounts.ts +3 -4
  10. package/payload/platform/neo4j/migrations/005-removed-review-feature.ts +102 -0
  11. package/payload/platform/neo4j/schema.cypher +1 -22
  12. package/payload/platform/plugins/admin/PLUGIN.md +1 -8
  13. package/payload/platform/plugins/admin/mcp/dist/index.js +6 -44
  14. package/payload/platform/plugins/admin/mcp/dist/index.js.map +1 -1
  15. package/payload/platform/plugins/cloudflare/mcp/dist/lib/cloudflared.d.ts.map +1 -1
  16. package/payload/platform/plugins/cloudflare/mcp/dist/lib/cloudflared.js +2 -3
  17. package/payload/platform/plugins/cloudflare/mcp/dist/lib/cloudflared.js.map +1 -1
  18. package/payload/platform/plugins/cloudflare/mcp/dist/lib/setup-orchestrator.js +1 -1
  19. package/payload/platform/plugins/cloudflare/mcp/dist/lib/setup-orchestrator.js.map +1 -1
  20. package/payload/platform/plugins/docs/references/memory-guide.md +1 -1
  21. package/payload/platform/plugins/memory/mcp/dist/lib/graph-write-gate.js +1 -1
  22. package/payload/platform/plugins/memory/mcp/dist/lib/graph-write-gate.js.map +1 -1
  23. package/payload/platform/scripts/logs-read.sh +8 -38
  24. package/payload/server/chunk-AJLGI7Y3.js +10067 -0
  25. package/payload/server/chunk-ON3LBL2Y.js +1114 -0
  26. package/payload/server/chunk-PXQA2MA3.js +2518 -0
  27. package/payload/server/client-pool-GBY5I2KQ.js +31 -0
  28. package/payload/server/maxy-edge.js +3 -3
  29. package/payload/server/neo4j-migrations-STCKDWAL.js +364 -0
  30. package/payload/server/public/assets/{admin-2w0XSMC6.js → admin-CdVYoqKD.js} +1 -1
  31. package/payload/server/public/assets/{graph-C4-jEPDE.js → graph-DeH6ulGh.js} +1 -1
  32. package/payload/server/public/assets/{page-zuI00fuC.js → page-WIAWD2Oi.js} +1 -1
  33. package/payload/server/public/graph.html +2 -2
  34. package/payload/server/public/index.html +2 -2
  35. package/payload/server/server.js +305 -1890
@@ -51,7 +51,7 @@ import {
51
51
  vncLog,
52
52
  waitForExit,
53
53
  writeChromiumWrapper
54
- } from "./chunk-P3HTEK33.js";
54
+ } from "./chunk-AJLGI7Y3.js";
55
55
  import {
56
56
  agentLogStream,
57
57
  clearSessionHistory,
@@ -79,7 +79,7 @@ import {
79
79
  sigtermFlushStreamLogs,
80
80
  unregisterSession,
81
81
  validateSession
82
- } from "./chunk-UYLZDEMC.js";
82
+ } from "./chunk-ON3LBL2Y.js";
83
83
  import {
84
84
  ACCOUNTS_DIR,
85
85
  GREETING_DIRECTIVE,
@@ -119,7 +119,7 @@ import {
119
119
  verifyAndGetConversationUpdatedAt,
120
120
  verifyConversationOwnership,
121
121
  writeAdminUserAndPerson
122
- } from "./chunk-TQTMKIW6.js";
122
+ } from "./chunk-PXQA2MA3.js";
123
123
 
124
124
  // ../lib/graph-trash/dist/index.js
125
125
  var require_dist = __commonJS({
@@ -618,15 +618,15 @@ var serveStatic = (options = { root: "" }) => {
618
618
  };
619
619
 
620
620
  // server/index.ts
621
- import { readFileSync as readFileSync18, existsSync as existsSync24, watchFile } from "fs";
622
- import { resolve as resolve25, join as join10, basename as basename7 } from "path";
621
+ import { readFileSync as readFileSync15, existsSync as existsSync21, watchFile } from "fs";
622
+ import { resolve as resolve21, join as join9, basename as basename5 } from "path";
623
623
  import { homedir as homedir2 } from "os";
624
624
 
625
625
  // app/lib/agent-slug-pattern.ts
626
626
  var AGENT_SLUG_PATTERN = /^\/([a-z][a-z0-9-]{2,49})$/;
627
627
 
628
628
  // server/routes/health.ts
629
- import { existsSync as existsSync6, readFileSync as readFileSync6 } from "fs";
629
+ import { existsSync as existsSync3, readFileSync as readFileSync3 } from "fs";
630
630
  import { createConnection } from "net";
631
631
 
632
632
  // app/lib/network.ts
@@ -647,1562 +647,6 @@ function getLanIp() {
647
647
  return fallback;
648
648
  }
649
649
 
650
- // app/lib/review-detector/boot.ts
651
- import { basename as basename2 } from "path";
652
-
653
- // app/lib/review-detector/rules.ts
654
- import { readFileSync, writeFileSync, existsSync as existsSync2, statSync as statSync2, mkdirSync, renameSync } from "fs";
655
- import { resolve, dirname } from "path";
656
- var DEFAULT_SCAN_INTERVAL_MS = 5e3;
657
- var RATE_LIMIT_PATTERN = "rate[- ]?limit(?:ed| reached| hit)|(?:HTTP|status)[^a-z]{0,3}429|too many requests";
658
- var RATE_LIMIT_PATTERN_V1 = "\\b429\\b|rate.?limit|too.?many.?requests";
659
- var VALID_TYPES = /* @__PURE__ */ new Set([
660
- "reconnect-loop",
661
- "repeated-error",
662
- "silent-catch",
663
- "file-write-storm",
664
- "stale-log",
665
- "rate-limit",
666
- "absent-followup"
667
- ]);
668
- var MAX_FOLLOWUP_WINDOW_MS = 6e5;
669
- var VALID_SOURCES = /* @__PURE__ */ new Set([
670
- "any",
671
- "server",
672
- "vnc",
673
- "system",
674
- "error",
675
- "session",
676
- "public",
677
- "mcp",
678
- "cloudflared",
679
- "config-dir"
680
- ]);
681
- var VALID_SCOPES = /* @__PURE__ */ new Set(["global", "session"]);
682
- function defaultRules() {
683
- return [
684
- {
685
- id: "whatsapp-reconnect-loop",
686
- name: "WhatsApp Baileys reconnect loop",
687
- type: "reconnect-loop",
688
- logSource: "server",
689
- pattern: "\\[whatsapp:baileys\\] ERROR",
690
- thresholdCount: 5,
691
- thresholdWindowMinutes: 10,
692
- suggestedAction: "Check Baileys init-queries error. Run `/investigate` on the WhatsApp subsystem, or pause WhatsApp and re-pair the account from chat."
693
- },
694
- {
695
- id: "repeated-error-generic",
696
- name: "Repeated error signature",
697
- type: "repeated-error",
698
- // Match generic ERROR lines with a bracketed prefix. Fingerprinting by
699
- // first 80 chars happens in the evaluator.
700
- logSource: "server",
701
- pattern: "\\] ERROR ",
702
- thresholdCount: 20,
703
- thresholdWindowMinutes: 60,
704
- suggestedAction: "Repeated error burst. Tail the relevant log via `logs-read` and identify the upstream cause."
705
- },
706
- {
707
- id: "silent-catch-fingerprint",
708
- name: "Silent catch-block fingerprint",
709
- type: "silent-catch",
710
- // Patterns that historically indicated a silent catch. Every match is
711
- // worth an alert — never suppressed by frequency.
712
- logSource: "any",
713
- pattern: "(UnhandledPromiseRejection|catch.*ignored|swallowed error|silent failure)",
714
- thresholdCount: 0,
715
- thresholdWindowMinutes: 0,
716
- suggestedAction: "A silent-failure fingerprint appeared in logs. Read the surrounding context and turn the catch into loud failure per CLAUDE.md rule 'Observability Is Non-Negotiable'."
717
- },
718
- {
719
- id: "credentials-write-storm",
720
- name: "Credentials directory write storm",
721
- type: "file-write-storm",
722
- logSource: "config-dir",
723
- pattern: "",
724
- watchPath: "credentials",
725
- thresholdCount: 5,
726
- thresholdWindowMinutes: 5,
727
- suggestedAction: "The credentials directory is being rewritten repeatedly. This usually means a subsystem is stuck in an auth-retry loop. Check which file is being rewritten and trace the caller."
728
- },
729
- // The stale-log rule type is fully supported by the evaluator but the
730
- // default seed does not ship an instance of it. Choosing the right file
731
- // to watch is subsystem-specific (e.g. a plugin-specific log that stops
732
- // when the plugin dies); server.log is the wrong target because it is
733
- // written continuously by the detector's own cycle events, so it never
734
- // goes stale. Users can add a targeted stale-log rule via
735
- // `review-rules-add` when they know which file matters for their
736
- // subsystem.
737
- {
738
- id: "http-rate-limit-429",
739
- name: "HTTP 429 rate-limit hit",
740
- type: "rate-limit",
741
- logSource: "any",
742
- pattern: RATE_LIMIT_PATTERN,
743
- thresholdCount: 0,
744
- thresholdWindowMinutes: 0,
745
- suggestedAction: "An external API returned 429 (rate-limited). Identify the caller and add backoff, or check the quota on the relevant API key."
746
- },
747
- {
748
- id: "approval-bypass-detected",
749
- name: "Approval gating bypass",
750
- type: "repeated-error",
751
- logSource: "error",
752
- pattern: "\\[persist\\].*(?:email-send|email-reply|whatsapp-send|whatsapp-send-document|message|contact-erase).*approval=auto-executed",
753
- thresholdCount: 0,
754
- thresholdWindowMinutes: 0,
755
- suggestedAction: "An external-facing tool executed with auto-executed approval state. Check account.json approvalPolicy \u2014 if this tool should require review, the gating hook may have been bypassed or the policy was changed."
756
- },
757
- {
758
- // Task 530: catches the bridgeai-style class where a single conversation
759
- // sees the same tool error repeatedly and the agent silently falls back.
760
- // Session-scoped so cross-conversation coincidence doesn't trigger it.
761
- id: "tool-result-recurring-errors",
762
- name: "Tool result errors recurring in a conversation",
763
- type: "repeated-error",
764
- logSource: "system",
765
- pattern: "\\[tool-result\\].*error=true",
766
- thresholdCount: 2,
767
- thresholdWindowMinutes: 5,
768
- scope: "session",
769
- suggestedAction: 'The same conversation has logged multiple tool failures. Use the admin `logs-read` MCP tool with `type: "system"` (or the `logs-read.sh` script with the conversationId) to retrieve the adjacent [tool-failure-diag] lines and identify whether the cause is DNS, TCP, HTTP, or the tool\'s internal pipeline \u2014 then adapt the next attempt to match. Never retry the same tool against the same target without diagnostic-grounded reasoning.'
770
- },
771
- {
772
- // Task 532: closes the "60-second black-box tool wait" class. Fires when
773
- // a tool hits the 30-second mark of the mid-flight heartbeat. The
774
- // pattern anchors on elapsed=30s specifically (the tool-wait tick emits
775
- // one line per 5s per tool, so matching every tick would be noisy) and
776
- // excludes tools whose long runtime is expected: `Task`/`Agent` subagent
777
- // dispatch, `Bash` with explicit long timeouts.
778
- id: "tool-wait-long-stall",
779
- name: "Tool wait exceeds 30 seconds (possible stall)",
780
- type: "repeated-error",
781
- logSource: "system",
782
- pattern: "\\[tool-wait\\][^\\n]*name=(?!Task\\b|Agent\\b|Bash\\b)[A-Za-z0-9_]+[^\\n]*elapsed=30s",
783
- thresholdCount: 0,
784
- thresholdWindowMinutes: 0,
785
- scope: "session",
786
- suggestedAction: "A tool call has been pending for 30 seconds without a result. Read the adjacent [tool-wait-diag] and [tool-wait-proc] lines in the conversation's stream log to determine whether the network remained healthy, the subprocess held active sockets, and the HTTP request reached the wire. If diag shows a healthy network but the subprocess has no [subproc-stderr] UNDICI/HTTP activity during the wait window, the tool's internal pipeline is stalled \u2014 do not retry the same request against the same target without a change in approach."
787
- },
788
- {
789
- // Task 536: detect agents ignoring the WEBFETCH_CANNOT_READ_JS_SPA
790
- // structured failure. A single SPA short-circuit per conversation is
791
- // expected — the hook is doing its job. Two or more in the same
792
- // conversation within 5 minutes means either (a) the agent retried
793
- // WebFetch on the same SPA URL despite the directive, or (b) the
794
- // owner is asking about multiple SPA URLs in one session and the
795
- // pattern needs surfacing as a recurring class. Both signal that the
796
- // IDENTITY.md "Tool Failure Discipline" guidance is not landing in the
797
- // prompt — revise the copy rather than add mechanical enforcement.
798
- id: "webfetch-spa-short-circuit-recurring",
799
- name: "WebFetch JS-SPA short-circuit fired repeatedly in conversation",
800
- type: "repeated-error",
801
- logSource: "system",
802
- pattern: "WEBFETCH_CANNOT_READ_JS_SPA",
803
- thresholdCount: 2,
804
- thresholdWindowMinutes: 5,
805
- scope: "session",
806
- suggestedAction: "The WebFetch SPA preflight has fired more than once in this conversation. Either the agent is ignoring the loud-failure directive (retrying WebFetch after seeing WEBFETCH_CANNOT_READ_JS_SPA), or multiple SPA URLs are being asked about. Read the conversation's stream log for the [tool-use] / [tool-result] sequence around each occurrence \u2014 if the agent dispatched WebFetch on the same URL or substituted Playwright silently, revisit the IDENTITY.md `Tool Failure Discipline` paragraph that names structured-error handling."
807
- },
808
- {
809
- // Task 538: fires when a [spawn] line appears in a conversation's stream
810
- // log but no subprocess-lifecycle marker follows within 10s. The three
811
- // acceptable followups are Task 535's contract — at least one must be
812
- // emitted immediately at every spawn site. Their absence means
813
- // `teeProcStderrToStreamLog` regressed, the markers drifted, or the
814
- // spawn site was added without wiring them up. Session scope so a single
815
- // broken conversation fires exactly once, not N times for every spawn.
816
- id: "subproc-tee-silent-spawn",
817
- name: "Subprocess spawn without a stderr-tee lifecycle marker",
818
- type: "absent-followup",
819
- logSource: "system",
820
- pattern: "\\[spawn\\] pid=\\d+",
821
- followupPattern: "\\[subproc-stderr-tee-attached\\]|\\[subproc-debug-unavailable\\]|\\[subproc-stderr-skip\\]",
822
- followupWindowMs: 1e4,
823
- thresholdCount: 0,
824
- thresholdWindowMinutes: 0,
825
- scope: "session",
826
- suggestedAction: "The main-subprocess tee infrastructure has regressed \u2014 a spawn produced no lifecycle marker. Re-check `teeProcStderrToStreamLog` is invoked at the spawn site and that `[subproc-debug-unavailable]` or `[subproc-stderr-skip]` is written immediately when the tee cannot attach (Task 535 contract)."
827
- },
828
- {
829
- // Task 533: surface every Cloudflare-plugin refusal. The plugin emits
830
- // exactly one [cloudflare:refuse] line per refusal with a structured
831
- // reason field; any single occurrence on a previously-clean device
832
- // means the bound Cloudflare account does not match the operator's
833
- // intent (or the post-flight FQDN drifted) and the operator needs to
834
- // act in the dashboard.
835
- id: "cloudflare-refuse",
836
- name: "Cloudflare plugin refusal",
837
- type: "silent-catch",
838
- logSource: "any",
839
- pattern: "\\[cloudflare:refuse\\]|\\[cloudflare:post-flight-mismatch\\]",
840
- thresholdCount: 0,
841
- thresholdWindowMinutes: 0,
842
- suggestedAction: "The Cloudflare plugin refused an operation. Read the refusal `reason` field in the adjacent log line. For `account-drift` or `unbound-device`, run `tunnel-login force=true` while the operator is signed into the correct Cloudflare account in the browser. For `hostname-zone-not-routable`, the domain is not on Cloudflare yet \u2014 guide the operator to add it via the Cloudflare dashboard. For `post-flight-fqdn-mismatch` or `bound-account-does-not-own-hostname`, the laptop is signed into the wrong Cloudflare account \u2014 guide the operator to switch accounts in the dashboard, then re-run `tunnel-login force=true`."
843
- },
844
- {
845
- // Task 540: the single highest-priority refusal — surface it immediately
846
- // and independently of the generic cloudflare-refuse rule so the admin
847
- // agent sees it on the very next turn. This is the exact class that
848
- // burned the operator for 8 days across 9+ sessions (Apr 11–18, 2026):
849
- // tunnel running locally, dashboard serving the wrong account, nothing
850
- // from the internet reaches the laptop, and no prior telemetry surfaced
851
- // it in time for the agent to self-correct.
852
- id: "cloudflare-bound-account-mismatch",
853
- name: "Cloudflare bound account does not own the configured hostnames",
854
- type: "silent-catch",
855
- logSource: "any",
856
- pattern: '"reason":"bound-account-does-not-own-hostname"',
857
- thresholdCount: 0,
858
- thresholdWindowMinutes: 0,
859
- suggestedAction: "This laptop is signed into a Cloudflare account that does not own the hostnames the tunnel is configured to serve. Run `tunnel-status` to confirm, then tell the operator verbatim: 'The tunnel is running on this laptop but nothing from the internet is reaching it. The Cloudflare account this laptop is signed into doesn't own your domain. Open Cloudflare in your browser \u2014 is the account name in the top-left the one that owns your domain? If not, switch to the correct one, then tell me and I will re-sign-in.' When the operator confirms the correct account is selected, run `tunnel-login force=true`."
860
- },
861
- {
862
- // Task 545: tunnel-login's terminal-failure class — cloudflared's
863
- // login process died without writing cert.pem. Covers every reason
864
- // the handler emits on the `failed` branch: either an unknown exit
865
- // (`-without-cert`), an exit preceded by the courtesy browser-launch
866
- // marker (`-with-marker`), auth URL never produced (`-timeout`), or
867
- // crashed before producing it at all. Task 541's original pattern
868
- // matched `reason=browser-launch-fetch-error` — Task 545 retired
869
- // that reason because the marker alone is no longer terminal
870
- // (cloudflared keeps its OAuth-callback loop alive after emitting
871
- // it). Use this pattern for any new terminal reason the handler
872
- // gains: extend the alternation rather than adding parallel rules.
873
- id: "cloudflare-tunnel-login-failed",
874
- name: "Cloudflare tunnel-login process terminated without writing cert",
875
- type: "silent-catch",
876
- logSource: "any",
877
- pattern: "\\[cloudflare:tunnel-login:failed\\] reason=(login-process-exited-without-cert|login-process-exited-with-marker|auth-url-timeout|process-exited-before-auth-url)",
878
- thresholdCount: 0,
879
- thresholdWindowMinutes: 0,
880
- suggestedAction: "The cloudflared login process died before cert.pem landed on disk. For `login-process-exited-*` reasons the tunnel-login tool detected the dead process on the next call and has already respawned it \u2014 relay the new sign-in URL and wait for the operator to authorize in the VNC browser. For `auth-url-timeout` / `process-exited-before-auth-url`, the tool's most recent call returned an error and did not respawn \u2014 call `tunnel-login` again to spawn a fresh attempt. Never open the Cloudflare dashboard in any other surface; the only auth path is the sign-in URL the tool produces."
881
- },
882
- {
883
- // Task 545: non-terminal advisory. cloudflared's browser-launch
884
- // subcommand failed (DISPLAY unreachable, xdg-open absent, etc.) but
885
- // its OAuth-callback listener is still running — the login can still
886
- // complete if a human opens the URL. This rule fires so the admin
887
- // agent can relay "open the URL yourself" to the operator the moment
888
- // the condition appears, rather than waiting for the operator to
889
- // notice nothing is happening in their browser. Task 546 will
890
- // obsolete the advisory by rendering auth URLs that auto-open in
891
- // the VNC browser.
892
- id: "cloudflare-tunnel-login-browser-launch-failed",
893
- name: "cloudflared couldn't open the sign-in URL (login still live)",
894
- type: "silent-catch",
895
- logSource: "any",
896
- pattern: "\\[cloudflare:tunnel-login:browser-launch-failed\\]",
897
- thresholdCount: 0,
898
- thresholdWindowMinutes: 0,
899
- suggestedAction: "cloudflared's browser-launch courtesy failed on this laptop, but the sign-in URL is still live \u2014 the process is waiting for the OAuth callback. Tell the operator to open the sign-in URL in the VNC browser themselves and complete the authorization. The tunnel-login tool already includes the URL and the advisory in its most recent response; do not restart the login (that would invalidate the URL the operator is about to open)."
900
- },
901
- {
902
- // Task 545: raw-log surface. cloudflared-login.log is now read by
903
- // the review-detector's `cloudflared` source (see sources.ts). The
904
- // literal "Failed to fetch resource" is emitted by cloudflared
905
- // itself when its browser-launch subcommand can't reach a display.
906
- // This rule catches the cloudflared-side event even if the MCP
907
- // handler's classification drifts or the platform is restarting
908
- // mid-login and the advisory log line is not yet written. Keeping
909
- // this rule on a different source (log-line, not MCP stderr) makes
910
- // the detection redundant in the "defence in depth" sense — if the
911
- // MCP classification ever regresses, this still fires.
912
- id: "cloudflared-login-browser-launch-failed-raw",
913
- name: "cloudflared login \u2014 browser-launch fetch error in log",
914
- type: "silent-catch",
915
- logSource: "cloudflared",
916
- pattern: "Failed to fetch resource",
917
- thresholdCount: 0,
918
- thresholdWindowMinutes: 0,
919
- suggestedAction: "cloudflared-login.log shows the browser-launch courtesy failed. The sign-in URL from the last tunnel-login call is still live \u2014 the cloudflared process waits for the OAuth callback regardless of whether it could open the browser itself. Tell the operator to open the sign-in URL manually in the VNC browser on the device."
920
- },
921
- {
922
- // Task 540: cloudflared.log is the one file most likely to carry the
923
- // "tunnel is having real-world connectivity problems" signal — QUIC
924
- // connection failures, connector drops, edge unreachability. Prior to
925
- // this rule it was written but never read (the review-detector had no
926
- // rule coverage for it). A single ERR line is worth surfacing; the
927
- // tee'd output is typically noise-free.
928
- // Task 862: setup-tunnel.sh emits `[script:setup-tunnel]
929
- // step=onboarding-persist result=skipped reason=no-account-dir` via
930
- // phase_line when ACCOUNT_DIR is unset. Pre-Task-862, the form-driven
931
- // action runner threaded STREAM_LOG_PATH but not ACCOUNT_DIR, so the
932
- // line landed in the agent's stream log (system source) and the user
933
- // looped on currentStep=6 indefinitely.
934
- // The agent-via-Bash path also threads STREAM_LOG_PATH; operator-SSH
935
- // does not — that's the disambiguator. If this pattern reappears in
936
- // a system log, a future invocation surface forgot to declare
937
- // ACCOUNT_DIR. Fix at action-runner.ts WHITELIST['cloudflare-setup'],
938
- // not in the script.
939
- id: "cloudflare-setup-account-dir-missing",
940
- name: "cloudflare-setup ran without ACCOUNT_DIR \u2014 onboarding step-7 will not persist",
941
- type: "silent-catch",
942
- logSource: "system",
943
- pattern: "\\[script:setup-tunnel\\] step=onboarding-persist result=skipped reason=no-account-dir",
944
- thresholdCount: 0,
945
- thresholdWindowMinutes: 0,
946
- suggestedAction: "An invocation surface for setup-tunnel.sh failed to declare ACCOUNT_DIR. Without it the script's step-7 persist block (Task 562) skips, leaving OnboardingState.currentStep=6 forever. Inspect platform/ui/server/lib/action-runner.ts WHITELIST['cloudflare-setup'].build (Task 862 thread) and platform/ui/app/lib/claude-agent/spawn-env.ts buildSpawnEnv (Task 562 thread) \u2014 confirm ACCOUNT_DIR is in the env map for whichever surface the action-id prefix indicates. Do NOT change setup-tunnel.sh; the skipped branch is correct for operator-SSH reconfigure flows."
947
- },
948
- {
949
- id: "cloudflared-edge-errors",
950
- name: "cloudflared edge connectivity errors",
951
- type: "silent-catch",
952
- logSource: "cloudflared",
953
- pattern: "^\\S+ ERR (Failed to refresh protocol|no more connections active|Failed to dial a quic connection)",
954
- thresholdCount: 0,
955
- thresholdWindowMinutes: 0,
956
- suggestedAction: "cloudflared is reporting edge connectivity errors in its daemon log. Read the last 20 lines of `cloudflared.log` to see the surrounding context. Transient QUIC drops are normal; sustained failures (more than a handful in a minute) point to either a network issue on this laptop or an edge-side routing problem. Run `tunnel-status` to check whether end-to-end probing still succeeds."
957
- },
958
- {
959
- // Task 543: fires when an agent turn contains an opposing-axis choice-fork
960
- // question ("Want me to X, or Y?"). Every occurrence is a violation of the
961
- // IDENTITY § Questions rule — Rule A (one-sided questions) catches them
962
- // all, and the sharper sub-class (Rule B — no menu when a tool returned a
963
- // deterministic signal) is isolated by Task 544's `preceded-by` tightening.
964
- // logSource is "any" so the rule catches violations on both the admin
965
- // stream (claude-agent-stream-*) and the public agent stream
966
- // (public-agent-stream-*); the regex is agent-phrasing-specific and has
967
- // not been observed in other log contexts. Session scope groups matches
968
- // by conversationId so a single offending turn fires exactly once.
969
- id: "agent-choice-fork",
970
- name: "Agent emitted a choice-fork question",
971
- type: "repeated-error",
972
- logSource: "any",
973
- pattern: "Want me to [^\\n]+, or [^\\n]+\\?",
974
- thresholdCount: 1,
975
- thresholdWindowMinutes: 60,
976
- scope: "session",
977
- suggestedAction: 'Agent emitted a choice-fork question ("Want me to X, or Y?") instead of a one-sided question or the prescribed action. Review Task 543 IDENTITY \xA7 Questions \u2014 the agent asked an opposing-axis question, or degraded a deterministic tool signal into a menu. The log sample shows the offending turn verbatim.'
978
- },
979
- {
980
- // Task 546: fires when the operator clicks a device-bound URL affordance
981
- // and the chat UI cannot drive the device browser — either CDP is
982
- // unreachable, the navigation timed out, or CDP returned an error. The
983
- // log line carries intent, hostname, and navigateResult so the admin
984
- // agent can name the affected flow and hostname verbatim on its next
985
- // turn. Every occurrence is worth surfacing (thresholdCount: 0) because
986
- // this is the exact class of silent failure Task 546 exists to close:
987
- // the operator clicked, nothing happened on the device, and if we don't
988
- // review the click telemetry the agent has no way to know the flow is
989
- // stuck.
990
- id: "device-url-click-failed",
991
- name: "Device-bound URL click failed to drive the VNC browser",
992
- type: "silent-catch",
993
- logSource: "server",
994
- // Enumerate the NavigateResult union explicitly rather than relying
995
- // on a (?!ok) negative lookahead anchored to a specific token order.
996
- // If a new member is added to the union in cdp-client.ts, this rule
997
- // must be updated in the same commit — the pattern is order-agnostic
998
- // (browser= and navigateResult= can appear in either order) and the
999
- // enumerated list compile-fails the source if it ever drifts from
1000
- // the shared type in device-url-schema.ts.
1001
- pattern: "\\[device-url:click\\][^\\n]*(?:browser=fallback|navigateResult=(?:timeout|cdp-unreachable|error))",
1002
- thresholdCount: 0,
1003
- thresholdWindowMinutes: 0,
1004
- suggestedAction: "A device-bound URL click failed to drive Chromium on the device's VNC display. Identify the `intent` and `hostname` from the log line, then check the VNC surface: read `vnc-boot.log` and confirm Chromium on :99 is responding on CDP port 9222. If CDP is unreachable, the operator needs to restart the VNC stack; if CDP is reachable but navigation errored, the URL itself may be malformed upstream \u2014 grep the stream log for the originating `[device-url:render]` line."
1005
- },
1006
- {
1007
- // Task 554: fires when an agent turn emits a synthesized localhost/127.0.0.1
1008
- // `__remote-auth/setup` URL. The only legitimate source for that URL is
1009
- // `remote-auth-status`, which emits it inside a `maxy-device-url` affordance
1010
- // block with the device's real hostname. A synthesized localhost variant is
1011
- // the exact failure mode Task 554 closed: the removed "direct the user to
1012
- // /__remote-auth/setup" clause in the password-setter's tool description
1013
- // invited URL synthesis from a partial path. Session scope groups matches
1014
- // by conversationId so a single offending turn fires exactly once, even if
1015
- // the URL appears multiple times in the streamed response.
1016
- id: "invented-remote-auth-url",
1017
- name: "Invented localhost/127.0.0.1 remote-auth setup URL",
1018
- type: "silent-catch",
1019
- logSource: "any",
1020
- pattern: "http(s)?://(localhost|127\\.0\\.0\\.1)[:\\w/.-]*__remote-auth/setup",
1021
- thresholdCount: 0,
1022
- thresholdWindowMinutes: 0,
1023
- scope: "session",
1024
- suggestedAction: "[Task 554 regression] An assistant turn emitted a synthesized localhost/127.0.0.1 remote-auth setup URL. The only authority for the browser-setup URL is `remote-auth-status`, which emits a `maxy-device-url` affordance block with the device's real hostname. Review the offending turn, confirm the `remote-auth-set-password` tool description and onboarding SKILL.md step 3 still lack URL-synthesis language, and patch whichever prose surface invited the synthesis."
1025
- },
1026
- {
1027
- // Task 553: fires when `anthropic-setup` auto-resets a revoked API key.
1028
- // The auto-reset is silent to the agent (the tool falls through to
1029
- // awaiting_signin in the same call), but it is operator-visible — the
1030
- // user's previously-stored key was just deleted because Anthropic
1031
- // rejected it. Surfacing the event lets the admin agent explain on the
1032
- // next turn why the user is being asked to sign in again instead of
1033
- // continuing normally. The matching string is the exact stable prefix
1034
- // emitted by the state machine immediately before `deleteKey()` is
1035
- // invoked.
1036
- id: "anthropic-setup-auth-error-auto-reset",
1037
- name: "Anthropic API key auto-reset on auth_error",
1038
- type: "silent-catch",
1039
- logSource: "any",
1040
- pattern: "\\[anthropic-setup\\] auth_error",
1041
- thresholdCount: 0,
1042
- thresholdWindowMinutes: 0,
1043
- suggestedAction: "The stored Anthropic API key was rejected by console.anthropic.com (invalid, revoked, or expired) and `anthropic-setup` auto-deleted it. On the next operator interaction, explain that the key was cleared and walk them through sign-in again via the onboarding skill \u2014 the tool already returned `awaiting_signin` with the correct `browser_evaluate` action on the same call."
1044
- },
1045
- {
1046
- // Task 562: setup-tunnel.sh persists step-7 completion to a filesystem
1047
- // flag before arming the service restart. The flag is consumed by the
1048
- // next session's `loadOnboardingStep` and `getOnboardingState` calls,
1049
- // so any runtime failure to write it means the next admin session
1050
- // will re-ask the Cloudflare question the user just answered. Every
1051
- // occurrence is loud-surface-worthy: the cause is either a permission
1052
- // issue on the onboarding directory or a filesystem problem on the
1053
- // device, and the recovery is deterministic (the operator can re-run
1054
- // setup-tunnel.sh or call `onboarding-complete-step` explicitly).
1055
- id: "setup-tunnel-onboarding-persist-failed",
1056
- name: "setup-tunnel.sh failed to persist step-7 completion",
1057
- type: "silent-catch",
1058
- logSource: "any",
1059
- pattern: "\\[script:setup-tunnel\\] step=onboarding-persist result=error",
1060
- thresholdCount: 0,
1061
- thresholdWindowMinutes: 0,
1062
- suggestedAction: "[Task 562] setup-tunnel.sh could not persist step-7 completion before arming the service restart. Read the `reason` field on the failing phase line: `fs-flag-dir-failed` means `${ACCOUNT_DIR}/onboarding/` is not writable (chmod issue or disk full); `fs-flag-write-failed` means the flag file itself could not be written. Without the flag, the next admin session will re-ask the Cloudflare question the user just answered. Recovery: fix the permission/disk issue, then either re-run `~/setup-tunnel.sh` (the flag-write is idempotent) or call `onboarding-complete-step` with step 7 from the admin chat to mark step 7 complete explicitly."
1063
- },
1064
- {
1065
- // Task 570: every `[llm-call] adminModel resolution FAILED:` line
1066
- // is a workflow LLM step that could not resolve a model because
1067
- // readAdminModel returned null. The stderr line carries `reason=`
1068
- // (enoent/eacces/fs_error/parse_error/field_missing/
1069
- // field_wrong_type/field_empty) so the admin agent can act on the
1070
- // specific failure mode rather than reading the generic persisted
1071
- // error and misdiagnosing. Every occurrence is worth surfacing —
1072
- // the workflow already failed when this line was emitted.
1073
- id: "workflow-admin-model-resolution-failed",
1074
- name: "Workflow readAdminModel returned null (reason code in log)",
1075
- type: "silent-catch",
1076
- logSource: "any",
1077
- pattern: "\\[llm-call\\] adminModel resolution FAILED:",
1078
- thresholdCount: 0,
1079
- thresholdWindowMinutes: 0,
1080
- suggestedAction: "A workflow LLM step could not resolve the account's adminModel. Read the log line's `reason=` field: `enoent` means the account.json path is wrong or the account is unprovisioned \u2014 inspect `path=` and confirm it exists. `eacces` means permission drift \u2014 check file ownership. `parse_error` means the file is malformed, likely mid-write or truncated \u2014 read the file and repair. `field_missing` / `field_wrong_type` / `field_empty` mean the `adminModel` field needs to be set to a valid model ID. Never edit the persisted WorkflowRun.error string; fix the underlying file or permissions and re-run the workflow."
1081
- },
1082
- {
1083
- // Task 561: fires when an admin-agent Bash tool call installs
1084
- // `bind9-dnsutils` at runtime. Post-Task-561 the Maxy installer
1085
- // (platform/scripts/setup.sh) provisions the package on every fresh
1086
- // and upgrade install, so `dig` is always in PATH before
1087
- // setup-tunnel.sh runs. An admin apt-install of bind9-dnsutils is
1088
- // therefore evidence the installer regressed (or the host is on a
1089
- // pre-Task-561 image); in either case the fix is to re-run the
1090
- // installer, not to patch apt-state from chat. Session scope so a
1091
- // single offending turn fires exactly once.
1092
- id: "admin-agent-apt-bind9-install",
1093
- name: "Admin agent installed bind9-dnsutils at runtime (installer regression)",
1094
- type: "repeated-error",
1095
- logSource: "any",
1096
- pattern: "\\[tool-use\\][^\\n]*name=Bash[^\\n]*apt-get install[^\\n]*bind9-dnsutils",
1097
- thresholdCount: 1,
1098
- thresholdWindowMinutes: 60,
1099
- scope: "session",
1100
- suggestedAction: "[Task 561 regression] The admin agent ran `apt-get install bind9-dnsutils` \u2014 the exact workaround Task 561 eliminated. The Maxy installer provisions `bind9-dnsutils` in `platform/scripts/setup.sh` so `dig` is always in PATH before `setup-tunnel.sh` runs. Do not patch apt-state from chat \u2014 re-run the installer (`npx -y @rubytech/create-maxy`) and verify the installer's apt-get line still includes `bind9-dnsutils`. If the package is present in setup.sh but missing on the device, the installer did not re-run step 1 on the current image."
1101
- }
1102
- ];
1103
- }
1104
- function rulesFilePath(configDir2) {
1105
- return resolve(configDir2, "review-rules.json");
1106
- }
1107
- function ensureRulesFile(configDir2) {
1108
- const path2 = rulesFilePath(configDir2);
1109
- if (existsSync2(path2)) return { created: false, path: path2 };
1110
- mkdirSync(dirname(path2), { recursive: true });
1111
- const body = {
1112
- scanIntervalMs: DEFAULT_SCAN_INTERVAL_MS,
1113
- rules: defaultRules()
1114
- };
1115
- atomicWriteJson(path2, body);
1116
- return { created: true, path: path2 };
1117
- }
1118
- function loadRules(configDir2) {
1119
- const path2 = rulesFilePath(configDir2);
1120
- if (!existsSync2(path2)) {
1121
- throw new Error(`rules file missing at ${path2}`);
1122
- }
1123
- const raw = readFileSync(path2, "utf-8");
1124
- let parsed;
1125
- try {
1126
- parsed = JSON.parse(raw);
1127
- } catch (err) {
1128
- throw new Error(`rules file ${path2} is not valid JSON: ${err instanceof Error ? err.message : String(err)}`);
1129
- }
1130
- return validateRulesFile(parsed, path2);
1131
- }
1132
- function rulesFileMtime(configDir2) {
1133
- const path2 = rulesFilePath(configDir2);
1134
- try {
1135
- return statSync2(path2).mtimeMs;
1136
- } catch {
1137
- return null;
1138
- }
1139
- }
1140
- function saveRules(configDir2, file) {
1141
- validateRulesFile(file, rulesFilePath(configDir2));
1142
- atomicWriteJson(rulesFilePath(configDir2), file);
1143
- }
1144
- function atomicWriteJson(path2, body) {
1145
- const tmp = `${path2}.tmp.${process.pid}.${Date.now()}`;
1146
- writeFileSync(tmp, JSON.stringify(body, null, 2) + "\n", "utf-8");
1147
- renameSync(tmp, path2);
1148
- }
1149
- function validateRulesFile(input, sourceLabel) {
1150
- if (!input || typeof input !== "object") {
1151
- throw new Error(`${sourceLabel}: top-level must be an object`);
1152
- }
1153
- const obj = input;
1154
- const scanIntervalMs = obj.scanIntervalMs;
1155
- if (typeof scanIntervalMs !== "number" || scanIntervalMs < 500 || scanIntervalMs > 3e5) {
1156
- throw new Error(`${sourceLabel}: scanIntervalMs must be a number between 500 and 300000`);
1157
- }
1158
- const rulesRaw = obj.rules;
1159
- if (!Array.isArray(rulesRaw)) {
1160
- throw new Error(`${sourceLabel}: rules must be an array`);
1161
- }
1162
- const ids = /* @__PURE__ */ new Set();
1163
- const rules = rulesRaw.map((r, i) => validateRule(r, `${sourceLabel}#rules[${i}]`, ids));
1164
- return { scanIntervalMs, rules };
1165
- }
1166
- function addMissingDefaultRules(rulesFile) {
1167
- const existingIds = new Set(rulesFile.rules.map((r) => r.id));
1168
- const defaults = defaultRules();
1169
- let mutated = false;
1170
- for (const rule of defaults) {
1171
- if (!existingIds.has(rule.id)) {
1172
- rulesFile.rules.push(rule);
1173
- mutated = true;
1174
- }
1175
- }
1176
- return mutated;
1177
- }
1178
- function migrateRateLimitPattern(rulesFile) {
1179
- const rule = rulesFile.rules.find((r) => r.id === "http-rate-limit-429");
1180
- if (!rule) return false;
1181
- if (rule.pattern !== RATE_LIMIT_PATTERN_V1) return false;
1182
- rule.pattern = RATE_LIMIT_PATTERN;
1183
- return true;
1184
- }
1185
- function validateRule(input, label, seenIds) {
1186
- if (!input || typeof input !== "object") {
1187
- throw new Error(`${label}: rule must be an object`);
1188
- }
1189
- const r = input;
1190
- const id = r.id;
1191
- if (typeof id !== "string" || id.length === 0) {
1192
- throw new Error(`${label}: id must be a non-empty string`);
1193
- }
1194
- if (seenIds.has(id)) {
1195
- throw new Error(`${label}: duplicate rule id "${id}"`);
1196
- }
1197
- seenIds.add(id);
1198
- const name = r.name;
1199
- if (typeof name !== "string" || name.length === 0) {
1200
- throw new Error(`${label}: name must be a non-empty string`);
1201
- }
1202
- const type = r.type;
1203
- if (typeof type !== "string" || !VALID_TYPES.has(type)) {
1204
- throw new Error(`${label}: type must be one of ${[...VALID_TYPES].join(", ")}`);
1205
- }
1206
- const logSource = r.logSource;
1207
- if (typeof logSource !== "string" || !VALID_SOURCES.has(logSource)) {
1208
- throw new Error(`${label}: logSource must be one of ${[...VALID_SOURCES].join(", ")}`);
1209
- }
1210
- const pattern = r.pattern;
1211
- if (typeof pattern !== "string") {
1212
- throw new Error(`${label}: pattern must be a string (may be empty for stale-log/file-write-storm)`);
1213
- }
1214
- if (pattern.length > 0) {
1215
- try {
1216
- new RegExp(pattern);
1217
- } catch (err) {
1218
- throw new Error(`${label}: pattern is not a valid regex: ${err instanceof Error ? err.message : String(err)}`);
1219
- }
1220
- }
1221
- const thresholdCount = r.thresholdCount;
1222
- if (typeof thresholdCount !== "number" || thresholdCount < 0) {
1223
- throw new Error(`${label}: thresholdCount must be a non-negative number`);
1224
- }
1225
- const thresholdWindowMinutes = r.thresholdWindowMinutes;
1226
- if (typeof thresholdWindowMinutes !== "number" || thresholdWindowMinutes < 0) {
1227
- throw new Error(`${label}: thresholdWindowMinutes must be a non-negative number`);
1228
- }
1229
- const suggestedAction = r.suggestedAction;
1230
- if (typeof suggestedAction !== "string" || suggestedAction.length === 0) {
1231
- throw new Error(`${label}: suggestedAction must be a non-empty string`);
1232
- }
1233
- const rule = {
1234
- id,
1235
- name,
1236
- type,
1237
- logSource,
1238
- pattern,
1239
- thresholdCount,
1240
- thresholdWindowMinutes,
1241
- suggestedAction
1242
- };
1243
- if (typeof r.watchPath === "string") rule.watchPath = r.watchPath;
1244
- if (typeof r.staleHours === "number") rule.staleHours = r.staleHours;
1245
- if (typeof r.followupPattern === "string") {
1246
- if (r.followupPattern.length > 0) {
1247
- try {
1248
- new RegExp(r.followupPattern);
1249
- } catch (err) {
1250
- throw new Error(`${label}: followupPattern is not a valid regex: ${err instanceof Error ? err.message : String(err)}`);
1251
- }
1252
- }
1253
- rule.followupPattern = r.followupPattern;
1254
- }
1255
- if (typeof r.followupWindowMs === "number") rule.followupWindowMs = r.followupWindowMs;
1256
- if (typeof r.suppressedUntil === "string") rule.suppressedUntil = r.suppressedUntil;
1257
- if (r.scope !== void 0) {
1258
- if (typeof r.scope !== "string" || !VALID_SCOPES.has(r.scope)) {
1259
- throw new Error(`${label}: scope must be one of ${[...VALID_SCOPES].join(", ")}`);
1260
- }
1261
- rule.scope = r.scope;
1262
- }
1263
- if (rule.type === "file-write-storm" || rule.type === "stale-log") {
1264
- if (!rule.watchPath) {
1265
- throw new Error(`${label}: ${rule.type} rules require watchPath`);
1266
- }
1267
- }
1268
- if (rule.type === "stale-log") {
1269
- if (typeof rule.staleHours !== "number" || rule.staleHours <= 0) {
1270
- throw new Error(`${label}: stale-log rules require a positive staleHours`);
1271
- }
1272
- }
1273
- if (rule.type === "absent-followup") {
1274
- if (rule.pattern.length === 0) {
1275
- throw new Error(`${label}: absent-followup rules require a non-empty pattern`);
1276
- }
1277
- if (typeof rule.followupPattern !== "string" || rule.followupPattern.length === 0) {
1278
- throw new Error(`${label}: absent-followup rules require a non-empty followupPattern`);
1279
- }
1280
- if (typeof rule.followupWindowMs !== "number" || rule.followupWindowMs <= 0 || rule.followupWindowMs > MAX_FOLLOWUP_WINDOW_MS) {
1281
- throw new Error(`${label}: absent-followup rules require followupWindowMs in (0, ${MAX_FOLLOWUP_WINDOW_MS}]`);
1282
- }
1283
- }
1284
- return rule;
1285
- }
1286
-
1287
- // app/lib/review-detector/sources.ts
1288
- import { existsSync as existsSync3, readdirSync, statSync as statSync3, writeFileSync as writeFileSync2, renameSync as renameSync2, mkdirSync as mkdirSync2, openSync, readSync, closeSync, readFileSync as readFileSync2 } from "fs";
1289
- import { resolve as resolve2, join as join2, basename, dirname as dirname2 } from "path";
1290
- function tailStatePath(configDir2) {
1291
- return resolve2(configDir2, "review-state.json");
1292
- }
1293
- function loadTailState(configDir2) {
1294
- const path2 = tailStatePath(configDir2);
1295
- if (!existsSync3(path2)) return {};
1296
- try {
1297
- const raw = readFileSync2(path2, "utf-8");
1298
- const parsed = JSON.parse(raw);
1299
- if (!parsed || typeof parsed !== "object") return {};
1300
- const clean = {};
1301
- for (const [key, value] of Object.entries(parsed)) {
1302
- const entry = value;
1303
- if (entry && typeof entry.offset === "number" && typeof entry.size === "number" && typeof entry.inode === "number") {
1304
- clean[key] = entry;
1305
- }
1306
- }
1307
- return clean;
1308
- } catch (err) {
1309
- console.error(`[review] tail state corrupt at ${path2}, starting fresh: ${err instanceof Error ? err.message : String(err)}`);
1310
- return {};
1311
- }
1312
- }
1313
- function saveTailState(configDir2, state) {
1314
- const path2 = tailStatePath(configDir2);
1315
- mkdirSync2(dirname2(path2), { recursive: true });
1316
- const tmp = `${path2}.tmp.${process.pid}.${Date.now()}`;
1317
- writeFileSync2(tmp, JSON.stringify(state, null, 2) + "\n", "utf-8");
1318
- renameSync2(tmp, path2);
1319
- }
1320
- function discoverSourceFiles(configDir2, accountLogDir2, logicalSource) {
1321
- if (logicalSource === "server") {
1322
- const p = resolve2(configDir2, "logs", "server.log");
1323
- return existsSync3(p) ? [{ logicalSource: "server", filepath: p }] : [];
1324
- }
1325
- if (logicalSource === "vnc") {
1326
- const p = resolve2(configDir2, "logs", "vnc-boot.log");
1327
- return existsSync3(p) ? [{ logicalSource: "vnc", filepath: p }] : [];
1328
- }
1329
- if (logicalSource === "cloudflared") {
1330
- const files2 = [];
1331
- const daemon = resolve2(configDir2, "logs", "cloudflared.log");
1332
- if (existsSync3(daemon)) files2.push({ logicalSource: "cloudflared", filepath: daemon });
1333
- const login = resolve2(configDir2, "logs", "cloudflared-login.log");
1334
- if (existsSync3(login)) files2.push({ logicalSource: "cloudflared", filepath: login });
1335
- return files2;
1336
- }
1337
- const prefix = {
1338
- system: "claude-agent-stream-",
1339
- error: "claude-agent-stderr-",
1340
- session: "sse-events-",
1341
- public: "public-agent-stream-",
1342
- mcp: "mcp-"
1343
- }[logicalSource];
1344
- if (!existsSync3(accountLogDir2)) return [];
1345
- const files = [];
1346
- let scanned = 0;
1347
- let skippedPrefixMismatch = 0;
1348
- let skippedNotLog = 0;
1349
- for (const entry of readdirSync(accountLogDir2)) {
1350
- scanned += 1;
1351
- const matchesPrefix = entry.startsWith(prefix);
1352
- const isLog = entry.endsWith(".log");
1353
- if (matchesPrefix && isLog) {
1354
- files.push({ logicalSource, filepath: join2(accountLogDir2, entry) });
1355
- } else if (!matchesPrefix) {
1356
- skippedPrefixMismatch += 1;
1357
- } else {
1358
- skippedNotLog += 1;
1359
- }
1360
- }
1361
- files.sort((a, b) => {
1362
- try {
1363
- return statSync3(b.filepath).mtimeMs - statSync3(a.filepath).mtimeMs;
1364
- } catch {
1365
- return a.filepath.localeCompare(b.filepath);
1366
- }
1367
- });
1368
- if (skippedPrefixMismatch > 0 || skippedNotLog > 0) {
1369
- console.error(`[review-scan-skip] dir=${accountLogDir2} source=${logicalSource} prefix=${prefix} scanned=${scanned} matched=${files.length} skipped_prefix=${skippedPrefixMismatch} skipped_non_log=${skippedNotLog}`);
1370
- }
1371
- return files;
1372
- }
1373
- function discoverAllSources(configDir2, accountLogDir2) {
1374
- return [
1375
- ...discoverSourceFiles(configDir2, accountLogDir2, "server"),
1376
- ...discoverSourceFiles(configDir2, accountLogDir2, "vnc"),
1377
- ...discoverSourceFiles(configDir2, accountLogDir2, "system"),
1378
- ...discoverSourceFiles(configDir2, accountLogDir2, "error"),
1379
- ...discoverSourceFiles(configDir2, accountLogDir2, "session"),
1380
- ...discoverSourceFiles(configDir2, accountLogDir2, "public"),
1381
- ...discoverSourceFiles(configDir2, accountLogDir2, "mcp"),
1382
- ...discoverSourceFiles(configDir2, accountLogDir2, "cloudflared")
1383
- ];
1384
- }
1385
- function readNewLines(filepath, prev) {
1386
- if (!existsSync3(filepath)) return null;
1387
- const stat7 = statSync3(filepath);
1388
- const size = stat7.size;
1389
- const inode = stat7.ino;
1390
- let startOffset = 0;
1391
- let rotated = false;
1392
- let truncated = false;
1393
- if (prev) {
1394
- if (prev.inode !== inode) {
1395
- rotated = true;
1396
- startOffset = 0;
1397
- } else if (size < prev.offset) {
1398
- truncated = true;
1399
- startOffset = 0;
1400
- } else {
1401
- startOffset = prev.offset;
1402
- }
1403
- }
1404
- if (startOffset >= size) {
1405
- return {
1406
- lines: [],
1407
- entry: { offset: size, size, inode },
1408
- rotated,
1409
- truncated
1410
- };
1411
- }
1412
- const fd = openSync(filepath, "r");
1413
- try {
1414
- const bufSize = Math.max(0, size - startOffset);
1415
- const buf = Buffer.alloc(bufSize);
1416
- if (bufSize > 0) {
1417
- readSync(fd, buf, 0, bufSize, startOffset);
1418
- }
1419
- const text = buf.toString("utf-8");
1420
- const lines = text.length > 0 ? text.split("\n") : [];
1421
- let newOffset = size;
1422
- if (lines.length > 0 && !text.endsWith("\n")) {
1423
- const partial = lines.pop();
1424
- newOffset = size - Buffer.byteLength(partial, "utf-8");
1425
- } else if (lines.length > 0 && text.endsWith("\n")) {
1426
- if (lines[lines.length - 1] === "") lines.pop();
1427
- }
1428
- return {
1429
- lines,
1430
- entry: { offset: newOffset, size, inode },
1431
- rotated,
1432
- truncated
1433
- };
1434
- } finally {
1435
- closeSync(fd);
1436
- }
1437
- }
1438
- function countRecentWrites(dir, sinceMs) {
1439
- if (!existsSync3(dir)) return 0;
1440
- let count = 0;
1441
- for (const entry of readdirSync(dir, { withFileTypes: true })) {
1442
- if (!entry.isFile()) continue;
1443
- try {
1444
- const st = statSync3(join2(dir, entry.name));
1445
- if (st.mtimeMs >= sinceMs) count += 1;
1446
- } catch {
1447
- }
1448
- }
1449
- return count;
1450
- }
1451
- function fileLastWriteMs(path2) {
1452
- if (!existsSync3(path2)) return null;
1453
- try {
1454
- return statSync3(path2).mtimeMs;
1455
- } catch {
1456
- return null;
1457
- }
1458
- }
1459
- function accountLogDir(accountDir) {
1460
- return resolve2(accountDir, "logs");
1461
- }
1462
- function sourceKey(file) {
1463
- return `${file.logicalSource}:${basename(file.filepath)}`;
1464
- }
1465
-
1466
- // app/lib/review-detector/writer.ts
1467
- import { appendFileSync, existsSync as existsSync4, mkdirSync as mkdirSync3, readFileSync as readFileSync3, writeFileSync as writeFileSync3, renameSync as renameSync3, statSync as statSync4 } from "fs";
1468
- import { resolve as resolve3, dirname as dirname3 } from "path";
1469
- import { randomUUID } from "crypto";
1470
- function reviewLogPath(configDir2) {
1471
- return resolve3(configDir2, "logs", "review.log");
1472
- }
1473
- function pendingAlertsPath(configDir2) {
1474
- return resolve3(configDir2, "review-pending-alerts.jsonl");
1475
- }
1476
- function reviewLog(configDir2, event) {
1477
- const path2 = reviewLogPath(configDir2);
1478
- try {
1479
- mkdirSync3(dirname3(path2), { recursive: true });
1480
- const line = `${new Date(
1481
- typeof event.ts === "number" ? event.ts : Date.now()
1482
- ).toISOString()} [review] ${JSON.stringify(event)}
1483
- `;
1484
- appendFileSync(path2, line, "utf-8");
1485
- } catch (err) {
1486
- console.error(`[review] failed to write review log at ${path2}: ${err instanceof Error ? err.message : String(err)}`);
1487
- }
1488
- }
1489
- async function ensureReviewAlertIndex() {
1490
- const session = getSession();
1491
- try {
1492
- await session.run(
1493
- `CREATE INDEX review_alert_lookup IF NOT EXISTS
1494
- FOR (a:ReviewAlert)
1495
- ON (a.accountId, a.resolvedAt, a.lastMatchAt)`
1496
- );
1497
- } finally {
1498
- await session.close();
1499
- }
1500
- }
1501
- async function ensureReviewDigestSchedule(accountId) {
1502
- const eventId = `review-digest-${accountId}`;
1503
- const now = (/* @__PURE__ */ new Date()).toISOString();
1504
- const next = /* @__PURE__ */ new Date();
1505
- if (next.getHours() >= 8) {
1506
- next.setDate(next.getDate() + 1);
1507
- }
1508
- next.setHours(8, 0, 0, 0);
1509
- const nextRun = next.toISOString();
1510
- const session = getSession();
1511
- try {
1512
- await session.run(
1513
- `MERGE (e:Event { eventId: $eventId })
1514
- ON CREATE SET
1515
- e.accountId = $accountId,
1516
- e.name = 'Daily review digest',
1517
- e.description = 'Task 385 \u2014 composes the review cadence digest from the last 24 hours of review.log and active ReviewAlert records. Scheduled via check-due-events.',
1518
- e.startDate = $now,
1519
- e.eventStatus = 'scheduled',
1520
- e.recurrence = '0 8 * * *',
1521
- e.nextRun = datetime($nextRun),
1522
- e.sourcePlugin = 'admin',
1523
- e.actionPlugin = 'admin',
1524
- e.actionTool = 'review-digest-compose',
1525
- e.actionArgs = '{}',
1526
- e.createdAt = $now,
1527
- e.updatedAt = $now
1528
- ON MATCH SET
1529
- e.actionPlugin = 'admin',
1530
- e.actionTool = 'review-digest-compose',
1531
- e.updatedAt = $now`,
1532
- { eventId, accountId, now, nextRun }
1533
- );
1534
- } finally {
1535
- await session.close();
1536
- }
1537
- }
1538
- async function countActiveReviewAlerts(accountId) {
1539
- const session = getSession();
1540
- try {
1541
- const result = await session.run(
1542
- `MATCH (a:ReviewAlert {accountId: $accountId})
1543
- WHERE a.resolvedAt IS NULL
1544
- AND (a.suppressedUntil IS NULL OR a.suppressedUntil < datetime())
1545
- RETURN count(a) AS n`,
1546
- { accountId }
1547
- );
1548
- const n = result.records[0]?.get("n");
1549
- if (typeof n === "number") return n;
1550
- if (n && typeof n.toNumber === "function") {
1551
- return n.toNumber();
1552
- }
1553
- return 0;
1554
- } finally {
1555
- await session.close();
1556
- }
1557
- }
1558
- async function upsertReviewAlert(accountId, match) {
1559
- const session = getSession();
1560
- try {
1561
- await session.run(
1562
- `MERGE (a:ReviewAlert { ruleId: $ruleId, accountId: $accountId })
1563
- ON CREATE SET
1564
- a.alertId = $alertId,
1565
- a.ruleName = $ruleName,
1566
- a.firstMatchAt = datetime($matchedAt),
1567
- a.lastMatchAt = datetime($matchedAt),
1568
- a.cumulativeMatchCount = 1,
1569
- a.sampleEvidence = $sampleEvidence,
1570
- a.suggestedAction = $suggestedAction,
1571
- a.suppressedUntil = null,
1572
- a.resolvedAt = null
1573
- ON MATCH SET
1574
- a.lastMatchAt = datetime($matchedAt),
1575
- a.cumulativeMatchCount = a.cumulativeMatchCount + 1,
1576
- a.sampleEvidence = $sampleEvidence,
1577
- a.suggestedAction = $suggestedAction,
1578
- a.resolvedAt = null`,
1579
- {
1580
- ruleId: match.ruleId,
1581
- accountId,
1582
- alertId: randomUUID(),
1583
- ruleName: match.ruleName,
1584
- matchedAt: new Date(match.matchedAt).toISOString(),
1585
- sampleEvidence: match.sampleEvidence,
1586
- suggestedAction: match.suggestedAction
1587
- }
1588
- );
1589
- } finally {
1590
- await session.close();
1591
- }
1592
- }
1593
- function queueAlert(configDir2, accountId, match) {
1594
- const path2 = pendingAlertsPath(configDir2);
1595
- try {
1596
- mkdirSync3(dirname3(path2), { recursive: true });
1597
- const line = JSON.stringify({ accountId, match }) + "\n";
1598
- appendFileSync(path2, line, "utf-8");
1599
- } catch (err) {
1600
- console.error(`[review] failed to queue alert at ${path2}: ${err instanceof Error ? err.message : String(err)}`);
1601
- }
1602
- }
1603
- async function drainPendingAlerts(configDir2) {
1604
- const path2 = pendingAlertsPath(configDir2);
1605
- if (!existsSync4(path2)) return { drained: 0, remaining: 0 };
1606
- const raw = readFileSync3(path2, "utf-8");
1607
- const lines = raw.split("\n").filter((l) => l.trim().length > 0);
1608
- if (lines.length === 0) return { drained: 0, remaining: 0 };
1609
- const remaining = [];
1610
- let drained = 0;
1611
- for (const line of lines) {
1612
- let entry = null;
1613
- try {
1614
- entry = JSON.parse(line);
1615
- } catch {
1616
- continue;
1617
- }
1618
- try {
1619
- await upsertReviewAlert(entry.accountId, entry.match);
1620
- drained += 1;
1621
- } catch {
1622
- remaining.push(line);
1623
- }
1624
- }
1625
- const tmp = `${path2}.tmp.${process.pid}.${Date.now()}`;
1626
- if (remaining.length > 0) {
1627
- writeFileSync3(tmp, remaining.join("\n") + "\n", "utf-8");
1628
- } else {
1629
- writeFileSync3(tmp, "", "utf-8");
1630
- }
1631
- renameSync3(tmp, path2);
1632
- return { drained, remaining: remaining.length };
1633
- }
1634
-
1635
- // app/lib/review-detector/boot.ts
1636
- async function bootDetector() {
1637
- const account = resolveAccount();
1638
- if (!account) {
1639
- console.error("[review] boot: no account resolved \u2014 detector not starting (Phase 0 expects exactly one account)");
1640
- return null;
1641
- }
1642
- const configDir2 = MAXY_DIR;
1643
- const accountId = account.accountId;
1644
- const accountDir = account.accountDir;
1645
- const ensured = ensureRulesFile(configDir2);
1646
- if (ensured.created) {
1647
- reviewLog(configDir2, { event: "rules-defaults-created", path: ensured.path });
1648
- }
1649
- let rulesFile;
1650
- try {
1651
- rulesFile = loadRules(configDir2);
1652
- } catch (err) {
1653
- reviewLog(configDir2, {
1654
- event: "boot-failed",
1655
- reason: "rules-invalid",
1656
- error: err instanceof Error ? err.message : String(err)
1657
- });
1658
- console.error(`[review] boot: rules file invalid \u2014 ${err instanceof Error ? err.message : String(err)}`);
1659
- return null;
1660
- }
1661
- if (addMissingDefaultRules(rulesFile)) {
1662
- saveRules(configDir2, rulesFile);
1663
- reviewLog(configDir2, { event: "rules-updated", update: "added-missing-defaults" });
1664
- console.error("[review] boot: added missing default rules to existing rules file");
1665
- }
1666
- if (migrateRateLimitPattern(rulesFile)) {
1667
- saveRules(configDir2, rulesFile);
1668
- reviewLog(configDir2, { event: "rules-migrated", migration: "rate-limit-pattern-v2" });
1669
- console.error("[review] boot: migrated http-rate-limit-429 pattern to v2 (Task 408)");
1670
- }
1671
- try {
1672
- await ensureReviewAlertIndex();
1673
- reviewLog(configDir2, { event: "neo4j-index-ensured" });
1674
- } catch (err) {
1675
- reviewLog(configDir2, {
1676
- event: "neo4j-index-failed",
1677
- error: err instanceof Error ? err.message : String(err)
1678
- });
1679
- console.error(`[review] boot: ensureReviewAlertIndex failed (continuing): ${err instanceof Error ? err.message : String(err)}`);
1680
- }
1681
- try {
1682
- await ensureReviewDigestSchedule(accountId);
1683
- reviewLog(configDir2, { event: "digest-schedule-ensured" });
1684
- } catch (err) {
1685
- reviewLog(configDir2, {
1686
- event: "digest-schedule-failed",
1687
- error: err instanceof Error ? err.message : String(err)
1688
- });
1689
- console.error(`[review] boot: ensureReviewDigestSchedule failed (continuing): ${err instanceof Error ? err.message : String(err)}`);
1690
- }
1691
- const tailState = loadTailState(configDir2);
1692
- const logDir = accountLogDir(accountDir);
1693
- const initialSources = discoverAllSources(configDir2, logDir);
1694
- const snapshot = {
1695
- state: "running",
1696
- startedAt: Date.now(),
1697
- lastScanAt: null,
1698
- lastScanDurationMs: null,
1699
- scanCycles: 0,
1700
- rulesLoaded: rulesFile.rules.length,
1701
- sourcesTracked: initialSources.length,
1702
- activeAlerts: 0,
1703
- lastError: null
1704
- };
1705
- reviewLog(configDir2, {
1706
- event: "detector-started",
1707
- configDir: basename2(configDir2),
1708
- accountId: accountId.slice(0, 8),
1709
- rulesLoaded: rulesFile.rules.length,
1710
- sourcesDiscovered: initialSources.length,
1711
- tailStateEntries: Object.keys(tailState).length,
1712
- scanIntervalMs: rulesFile.scanIntervalMs
1713
- });
1714
- const runtime = {
1715
- configDir: configDir2,
1716
- accountId,
1717
- accountDir,
1718
- rulesFile,
1719
- rulesFileMtimeMs: rulesFileMtime(configDir2) ?? Date.now(),
1720
- ruleState: /* @__PURE__ */ new Map(),
1721
- tailState,
1722
- snapshot,
1723
- stopped: false
1724
- };
1725
- return runtime;
1726
- }
1727
-
1728
- // app/lib/review-detector/scan-loop.ts
1729
- import { resolve as resolve4 } from "path";
1730
-
1731
- // app/lib/review-detector/evaluator.ts
1732
- var SAMPLE_MAX_CHARS = 500;
1733
- var CONV_ID_REGEX = /conversationId=([a-f0-9]{8})/;
1734
- function scopeKeyFor(rule, line) {
1735
- if (rule.scope !== "session") return "";
1736
- const m = CONV_ID_REGEX.exec(line);
1737
- return m ? m[1] : "";
1738
- }
1739
- var compiledRegexCache = /* @__PURE__ */ new Map();
1740
- function compileRegex(pattern) {
1741
- let re = compiledRegexCache.get(pattern);
1742
- if (re === void 0) {
1743
- re = new RegExp(pattern, "i");
1744
- compiledRegexCache.set(pattern, re);
1745
- }
1746
- return re;
1747
- }
1748
- function isSuppressed(rule, nowMs) {
1749
- if (!rule.suppressedUntil) return false;
1750
- const until = Date.parse(rule.suppressedUntil);
1751
- if (Number.isNaN(until)) return false;
1752
- return nowMs < until;
1753
- }
1754
- function toSample(line) {
1755
- if (line.length <= SAMPLE_MAX_CHARS) return line;
1756
- return line.slice(0, SAMPLE_MAX_CHARS) + "\u2026";
1757
- }
1758
- function newRuleState() {
1759
- return {
1760
- matchTimestamps: [],
1761
- matchTimestampsByScope: /* @__PURE__ */ new Map(),
1762
- lastAlertAt: null,
1763
- cumulativeSinceLastAlert: 0,
1764
- lastSeenAt: null,
1765
- pendingFollowups: []
1766
- };
1767
- }
1768
- function evaluateTextRule(rule, lines, state, nowMs) {
1769
- if (isSuppressed(rule, nowMs)) return { match: null, state };
1770
- if (rule.pattern.length === 0) return { match: null, state };
1771
- const regex = compileRegex(rule.pattern);
1772
- const matchesByScope = /* @__PURE__ */ new Map();
1773
- let firstSample = null;
1774
- for (const line of lines) {
1775
- if (!regex.test(line)) continue;
1776
- if (!firstSample) firstSample = line;
1777
- const key = scopeKeyFor(rule, line);
1778
- const existing = matchesByScope.get(key) ?? [];
1779
- existing.push(line);
1780
- matchesByScope.set(key, existing);
1781
- }
1782
- if (matchesByScope.size === 0) return { match: null, state };
1783
- const windowStart = rule.thresholdWindowMinutes > 0 ? nowMs - rule.thresholdWindowMinutes * 6e4 : -Infinity;
1784
- const updated = {
1785
- ...state,
1786
- matchTimestamps: [...state.matchTimestamps],
1787
- matchTimestampsByScope: new Map(state.matchTimestampsByScope ?? [])
1788
- };
1789
- let firingSample = null;
1790
- let fires = false;
1791
- const isCountZero = rule.thresholdCount === 0;
1792
- if (rule.scope === "session") {
1793
- for (const [key, hits] of matchesByScope) {
1794
- const prior = updated.matchTimestampsByScope.get(key) ?? [];
1795
- const merged = [...prior, ...hits.map(() => nowMs)].filter((t) => t >= windowStart);
1796
- if (merged.length === 0) {
1797
- updated.matchTimestampsByScope.delete(key);
1798
- } else {
1799
- updated.matchTimestampsByScope.set(key, merged);
1800
- }
1801
- if (!fires && (isCountZero || merged.length >= rule.thresholdCount)) {
1802
- fires = true;
1803
- firingSample = hits[0];
1804
- }
1805
- }
1806
- } else {
1807
- for (const hits of matchesByScope.values()) {
1808
- for (const _ of hits) updated.matchTimestamps.push(nowMs);
1809
- }
1810
- updated.matchTimestamps = updated.matchTimestamps.filter((t) => t >= windowStart);
1811
- fires = isCountZero || updated.matchTimestamps.length >= rule.thresholdCount;
1812
- if (fires) firingSample = firstSample;
1813
- }
1814
- if (!fires) {
1815
- return { match: null, state: updated };
1816
- }
1817
- const match = {
1818
- ruleId: rule.id,
1819
- ruleName: rule.name,
1820
- matchedAt: nowMs,
1821
- sampleEvidence: toSample(firingSample ?? firstSample ?? lines[0] ?? ""),
1822
- suggestedAction: rule.suggestedAction
1823
- };
1824
- return { match, state: updated };
1825
- }
1826
- function evaluateFileWriteStormRule(rule, recentWriteCount, state, nowMs) {
1827
- if (isSuppressed(rule, nowMs)) return { match: null, state };
1828
- if (recentWriteCount < rule.thresholdCount) {
1829
- return { match: null, state };
1830
- }
1831
- const match = {
1832
- ruleId: rule.id,
1833
- ruleName: rule.name,
1834
- matchedAt: nowMs,
1835
- sampleEvidence: `${recentWriteCount} file writes in ${rule.thresholdWindowMinutes}m at ${rule.watchPath}`,
1836
- suggestedAction: rule.suggestedAction
1837
- };
1838
- return { match, state };
1839
- }
1840
- function evaluateStaleLogRule(rule, lastMtimeMs, state, nowMs) {
1841
- if (isSuppressed(rule, nowMs)) return { match: null, state };
1842
- let lastSeenAt = state.lastSeenAt;
1843
- if (lastMtimeMs !== null) {
1844
- lastSeenAt = Math.max(lastSeenAt ?? 0, lastMtimeMs);
1845
- }
1846
- const updated = { ...state, lastSeenAt };
1847
- if (lastMtimeMs === null || lastSeenAt === null) {
1848
- return { match: null, state: updated };
1849
- }
1850
- const staleMs = (rule.staleHours ?? 24) * 60 * 60 * 1e3;
1851
- if (nowMs - lastSeenAt < staleMs) {
1852
- return { match: null, state: updated };
1853
- }
1854
- const match = {
1855
- ruleId: rule.id,
1856
- ruleName: rule.name,
1857
- matchedAt: nowMs,
1858
- sampleEvidence: `last write at ${new Date(lastSeenAt).toISOString()} (${rule.watchPath})`,
1859
- suggestedAction: rule.suggestedAction
1860
- };
1861
- return { match, state: updated };
1862
- }
1863
- function evaluateAbsentFollowupRule(rule, lines, state, nowMs) {
1864
- if (isSuppressed(rule, nowMs)) return { matches: [], state };
1865
- if (!rule.pattern || !rule.followupPattern || !rule.followupWindowMs) {
1866
- return { matches: [], state };
1867
- }
1868
- const triggerRegex = compileRegex(rule.pattern);
1869
- const followupRegex = compileRegex(rule.followupPattern);
1870
- const pending = [...state.pendingFollowups ?? []];
1871
- for (const line of lines) {
1872
- if (triggerRegex.test(line)) {
1873
- pending.push({
1874
- scope: scopeKeyFor(rule, line),
1875
- timestamp: nowMs,
1876
- line,
1877
- fulfilled: false
1878
- });
1879
- continue;
1880
- }
1881
- if (followupRegex.test(line)) {
1882
- const scope = scopeKeyFor(rule, line);
1883
- for (const entry of pending) {
1884
- if (!entry.fulfilled && entry.scope === scope) {
1885
- entry.fulfilled = true;
1886
- break;
1887
- }
1888
- }
1889
- }
1890
- }
1891
- const matches = [];
1892
- const kept = [];
1893
- for (const entry of pending) {
1894
- const age = nowMs - entry.timestamp;
1895
- if (age >= rule.followupWindowMs) {
1896
- if (!entry.fulfilled) {
1897
- matches.push({
1898
- ruleId: rule.id,
1899
- ruleName: rule.name,
1900
- matchedAt: nowMs,
1901
- sampleEvidence: toSample(entry.line),
1902
- suggestedAction: rule.suggestedAction,
1903
- missedForMs: age
1904
- });
1905
- }
1906
- continue;
1907
- }
1908
- kept.push(entry);
1909
- }
1910
- return {
1911
- matches,
1912
- state: { ...state, pendingFollowups: kept }
1913
- };
1914
- }
1915
- var ALERT_WINDOW_MS = 60 * 60 * 1e3;
1916
- function rateLimitDecision(state, nowMs) {
1917
- const since = state.lastAlertAt === null ? Infinity : nowMs - state.lastAlertAt;
1918
- if (since >= ALERT_WINDOW_MS) {
1919
- return {
1920
- surface: true,
1921
- state: { ...state, lastAlertAt: nowMs, cumulativeSinceLastAlert: 0 }
1922
- };
1923
- }
1924
- return {
1925
- surface: false,
1926
- state: { ...state, cumulativeSinceLastAlert: state.cumulativeSinceLastAlert + 1 }
1927
- };
1928
- }
1929
-
1930
- // app/lib/review-detector/scan-loop.ts
1931
- async function runScanCycle(runtime) {
1932
- const cycleStart = Date.now();
1933
- runtime.snapshot.scanCycles += 1;
1934
- try {
1935
- const currentMtime = rulesFileMtime(runtime.configDir);
1936
- if (currentMtime !== null && currentMtime !== runtime.rulesFileMtimeMs) {
1937
- try {
1938
- runtime.rulesFile = loadRules(runtime.configDir);
1939
- runtime.rulesFileMtimeMs = currentMtime;
1940
- runtime.snapshot.rulesLoaded = runtime.rulesFile.rules.length;
1941
- reviewLog(runtime.configDir, {
1942
- event: "rules-reloaded",
1943
- rulesLoaded: runtime.rulesFile.rules.length,
1944
- mtime: new Date(currentMtime).toISOString()
1945
- });
1946
- } catch (err) {
1947
- reviewLog(runtime.configDir, {
1948
- event: "rules-reload-failed",
1949
- error: err instanceof Error ? err.message : String(err)
1950
- });
1951
- runtime.snapshot.state = "degraded";
1952
- runtime.snapshot.lastError = err instanceof Error ? err.message : String(err);
1953
- }
1954
- }
1955
- const logDir = accountLogDir(runtime.accountDir);
1956
- const files = discoverAllSources(runtime.configDir, logDir);
1957
- runtime.snapshot.sourcesTracked = files.length;
1958
- const linesBySource = /* @__PURE__ */ new Map();
1959
- for (const file of files) {
1960
- const key = sourceKey(file);
1961
- const prev = runtime.tailState[key];
1962
- const result = readNewLines(file.filepath, prev);
1963
- if (result === null) {
1964
- if (prev) {
1965
- reviewLog(runtime.configDir, {
1966
- event: "source-vanished",
1967
- source: file.logicalSource,
1968
- filepath: file.filepath
1969
- });
1970
- delete runtime.tailState[key];
1971
- }
1972
- continue;
1973
- }
1974
- if (result.rotated) {
1975
- reviewLog(runtime.configDir, {
1976
- event: "source-rotated",
1977
- source: file.logicalSource,
1978
- filepath: file.filepath
1979
- });
1980
- }
1981
- if (result.truncated) {
1982
- reviewLog(runtime.configDir, {
1983
- event: "source-truncated",
1984
- source: file.logicalSource,
1985
- filepath: file.filepath
1986
- });
1987
- }
1988
- runtime.tailState[key] = result.entry;
1989
- if (result.lines.length > 0) {
1990
- const bucket = linesBySource.get(file.logicalSource) ?? [];
1991
- bucket.push(...result.lines);
1992
- linesBySource.set(file.logicalSource, bucket);
1993
- }
1994
- }
1995
- const matches = [];
1996
- for (const rule of runtime.rulesFile.rules) {
1997
- let state = runtime.ruleState.get(rule.id) ?? newRuleState();
1998
- if (isSuppressed(rule, cycleStart)) {
1999
- reviewLog(runtime.configDir, {
2000
- event: "rule-suppressed",
2001
- ruleId: rule.id,
2002
- suppressedUntil: rule.suppressedUntil
2003
- });
2004
- runtime.ruleState.set(rule.id, state);
2005
- continue;
2006
- }
2007
- let match = null;
2008
- if (rule.type === "reconnect-loop" || rule.type === "repeated-error" || rule.type === "silent-catch" || rule.type === "rate-limit") {
2009
- let inputLines = [];
2010
- if (rule.logSource === "any") {
2011
- for (const [src, lines] of linesBySource.entries()) {
2012
- if (src !== "config-dir") inputLines.push(...lines);
2013
- }
2014
- } else {
2015
- inputLines = linesBySource.get(rule.logSource) ?? [];
2016
- }
2017
- if (inputLines.length > 0) {
2018
- const result = evaluateTextRule(rule, inputLines, state, cycleStart);
2019
- state = result.state;
2020
- match = result.match;
2021
- }
2022
- } else if (rule.type === "file-write-storm") {
2023
- const dir = resolve4(runtime.configDir, rule.watchPath ?? "");
2024
- const sinceMs = cycleStart - rule.thresholdWindowMinutes * 6e4;
2025
- const count = countRecentWrites(dir, sinceMs);
2026
- const result = evaluateFileWriteStormRule(rule, count, state, cycleStart);
2027
- state = result.state;
2028
- match = result.match;
2029
- } else if (rule.type === "stale-log") {
2030
- const trackedPath = resolve4(runtime.configDir, rule.watchPath ?? "");
2031
- const lastMs = fileLastWriteMs(trackedPath);
2032
- const result = evaluateStaleLogRule(rule, lastMs, state, cycleStart);
2033
- state = result.state;
2034
- match = result.match;
2035
- } else if (rule.type === "absent-followup") {
2036
- let inputLines = [];
2037
- if (rule.logSource === "any") {
2038
- for (const [src, lines] of linesBySource.entries()) {
2039
- if (src !== "config-dir") inputLines.push(...lines);
2040
- }
2041
- } else {
2042
- inputLines = linesBySource.get(rule.logSource) ?? [];
2043
- }
2044
- const result = evaluateAbsentFollowupRule(rule, inputLines, state, cycleStart);
2045
- state = result.state;
2046
- for (const m of result.matches) {
2047
- const safeTrigger = m.sampleEvidence.replace(/\\/g, "\\\\").replace(/"/g, '\\"');
2048
- console.error(
2049
- `[review-detector] absent-followup rule=${rule.id} trigger="${safeTrigger}" missed_for_ms=${m.missedForMs ?? ""}`
2050
- );
2051
- matches.push(m);
2052
- }
2053
- }
2054
- runtime.ruleState.set(rule.id, state);
2055
- if (match) matches.push(match);
2056
- }
2057
- for (const match of matches) {
2058
- const state = runtime.ruleState.get(match.ruleId) ?? newRuleState();
2059
- const decision = rateLimitDecision(state, cycleStart);
2060
- runtime.ruleState.set(match.ruleId, decision.state);
2061
- if (!decision.surface) {
2062
- reviewLog(runtime.configDir, {
2063
- event: "rate-limit-deferred",
2064
- ruleId: match.ruleId,
2065
- cumulativeSinceLastAlert: decision.state.cumulativeSinceLastAlert
2066
- });
2067
- continue;
2068
- }
2069
- reviewLog(runtime.configDir, {
2070
- event: "match",
2071
- ruleId: match.ruleId,
2072
- ruleName: match.ruleName,
2073
- sampleEvidence: match.sampleEvidence,
2074
- suggestedAction: match.suggestedAction
2075
- });
2076
- try {
2077
- await upsertReviewAlert(runtime.accountId, match);
2078
- reviewLog(runtime.configDir, { event: "alert-persisted", ruleId: match.ruleId });
2079
- } catch (err) {
2080
- queueAlert(runtime.configDir, runtime.accountId, match);
2081
- reviewLog(runtime.configDir, {
2082
- event: "alert-queued",
2083
- ruleId: match.ruleId,
2084
- error: err instanceof Error ? err.message : String(err)
2085
- });
2086
- }
2087
- }
2088
- try {
2089
- const drain = await drainPendingAlerts(runtime.configDir);
2090
- if (drain.drained > 0 || drain.remaining > 0) {
2091
- reviewLog(runtime.configDir, {
2092
- event: "queue-drain",
2093
- drained: drain.drained,
2094
- remaining: drain.remaining
2095
- });
2096
- }
2097
- } catch (err) {
2098
- reviewLog(runtime.configDir, {
2099
- event: "queue-drain-failed",
2100
- error: err instanceof Error ? err.message : String(err)
2101
- });
2102
- }
2103
- try {
2104
- runtime.snapshot.activeAlerts = await countActiveReviewAlerts(runtime.accountId);
2105
- } catch (err) {
2106
- reviewLog(runtime.configDir, {
2107
- event: "active-alerts-count-failed",
2108
- error: err instanceof Error ? err.message : String(err)
2109
- });
2110
- }
2111
- saveTailState(runtime.configDir, runtime.tailState);
2112
- const cycleDuration = Date.now() - cycleStart;
2113
- runtime.snapshot.lastScanAt = cycleStart;
2114
- runtime.snapshot.lastScanDurationMs = cycleDuration;
2115
- if (runtime.snapshot.state !== "degraded" && runtime.snapshot.state !== "failed") {
2116
- runtime.snapshot.state = "running";
2117
- }
2118
- runtime.snapshot.lastError = null;
2119
- reviewLog(runtime.configDir, {
2120
- event: "cycle",
2121
- cycles: runtime.snapshot.scanCycles,
2122
- sources: files.length,
2123
- rules: runtime.rulesFile.rules.length,
2124
- matches: matches.length,
2125
- durationMs: cycleDuration
2126
- });
2127
- } catch (err) {
2128
- runtime.snapshot.state = "degraded";
2129
- runtime.snapshot.lastError = err instanceof Error ? err.message : String(err);
2130
- reviewLog(runtime.configDir, {
2131
- event: "cycle-failed",
2132
- error: err instanceof Error ? err.message : String(err)
2133
- });
2134
- }
2135
- }
2136
- function startScanLoop(runtime) {
2137
- let inFlight = null;
2138
- const interval = setInterval(async () => {
2139
- if (runtime.stopped) return;
2140
- if (inFlight) return;
2141
- inFlight = runScanCycle(runtime);
2142
- try {
2143
- await inFlight;
2144
- } finally {
2145
- inFlight = null;
2146
- }
2147
- }, runtime.rulesFile.scanIntervalMs);
2148
- inFlight = runScanCycle(runtime);
2149
- return async () => {
2150
- runtime.stopped = true;
2151
- clearInterval(interval);
2152
- if (inFlight) {
2153
- try {
2154
- await inFlight;
2155
- } catch {
2156
- }
2157
- }
2158
- runtime.snapshot.state = "stopped";
2159
- reviewLog(runtime.configDir, { event: "detector-stopped", cycles: runtime.snapshot.scanCycles });
2160
- };
2161
- }
2162
-
2163
- // app/lib/review-detector/index.ts
2164
- var activeRuntime = null;
2165
- var stopFn = null;
2166
- async function startReviewDetector() {
2167
- if (stopFn) {
2168
- console.error("[review] startReviewDetector called twice \u2014 ignoring second call");
2169
- return;
2170
- }
2171
- try {
2172
- activeRuntime = await bootDetector();
2173
- if (!activeRuntime) return;
2174
- stopFn = startScanLoop(activeRuntime);
2175
- } catch (err) {
2176
- console.error(`[review] detector start failed: ${err instanceof Error ? err.message : String(err)}`);
2177
- }
2178
- }
2179
- async function shutdownReviewDetector() {
2180
- if (!stopFn) return;
2181
- try {
2182
- await stopFn();
2183
- } catch (err) {
2184
- console.error(`[review] detector shutdown error: ${err instanceof Error ? err.message : String(err)}`);
2185
- }
2186
- stopFn = null;
2187
- activeRuntime = null;
2188
- }
2189
- function getReviewDetectorSnapshot() {
2190
- if (!activeRuntime) {
2191
- return {
2192
- state: "stopped",
2193
- startedAt: null,
2194
- lastScanAt: null,
2195
- lastScanDurationMs: null,
2196
- scanCycles: 0,
2197
- rulesLoaded: 0,
2198
- sourcesTracked: 0,
2199
- activeAlerts: 0,
2200
- lastError: null
2201
- };
2202
- }
2203
- return activeRuntime.snapshot;
2204
- }
2205
-
2206
650
  // app/lib/whatsapp/schema.ts
2207
651
  import { z } from "zod";
2208
652
  var DmPolicySchema = z.enum(["open", "allowlist", "disabled"]);
@@ -2267,20 +711,20 @@ var WhatsAppConfigSchema = z.object({
2267
711
  });
2268
712
 
2269
713
  // app/lib/whatsapp/config-persist.ts
2270
- import { readFileSync as readFileSync4, writeFileSync as writeFileSync4, existsSync as existsSync5 } from "fs";
2271
- import { resolve as resolve5, join as join3 } from "path";
714
+ import { readFileSync, writeFileSync, existsSync as existsSync2 } from "fs";
715
+ import { resolve, join as join2 } from "path";
2272
716
  var TAG = "[whatsapp:config]";
2273
717
  function configPath(accountDir) {
2274
- return resolve5(accountDir, "account.json");
718
+ return resolve(accountDir, "account.json");
2275
719
  }
2276
720
  function readConfig(accountDir) {
2277
721
  const path2 = configPath(accountDir);
2278
- if (!existsSync5(path2)) throw new Error(`account.json not found at ${path2}`);
2279
- return JSON.parse(readFileSync4(path2, "utf-8"));
722
+ if (!existsSync2(path2)) throw new Error(`account.json not found at ${path2}`);
723
+ return JSON.parse(readFileSync(path2, "utf-8"));
2280
724
  }
2281
725
  function writeConfig(accountDir, config) {
2282
726
  const path2 = configPath(accountDir);
2283
- writeFileSync4(path2, JSON.stringify(config, null, 2) + "\n", "utf-8");
727
+ writeFileSync(path2, JSON.stringify(config, null, 2) + "\n", "utf-8");
2284
728
  }
2285
729
  function reloadManagerConfig(accountDir) {
2286
730
  try {
@@ -2450,8 +894,8 @@ function setPublicAgent(accountDir, slug) {
2450
894
  if (!trimmed) {
2451
895
  return { ok: false, error: "Agent slug cannot be empty." };
2452
896
  }
2453
- const agentConfigPath = join3(accountDir, "agents", trimmed, "config.json");
2454
- if (!existsSync5(agentConfigPath)) {
897
+ const agentConfigPath = join2(accountDir, "agents", trimmed, "config.json");
898
+ if (!existsSync2(agentConfigPath)) {
2455
899
  return { ok: false, error: `Agent "${trimmed}" not found \u2014 no config.json at ${agentConfigPath}. Check the agent slug and try again.` };
2456
900
  }
2457
901
  try {
@@ -2514,8 +958,8 @@ function setGroupPublicAgent(accountDir, accountId, groupJid, slug) {
2514
958
  if (!trimmedSlug) return { ok: false, error: "Agent slug cannot be empty." };
2515
959
  if (!trimmedGroup) return { ok: false, error: "Group JID cannot be empty." };
2516
960
  if (!trimmedAccount) return { ok: false, error: "Account ID cannot be empty." };
2517
- const agentConfigPath = join3(accountDir, "agents", trimmedSlug, "config.json");
2518
- if (!existsSync5(agentConfigPath)) {
961
+ const agentConfigPath = join2(accountDir, "agents", trimmedSlug, "config.json");
962
+ if (!existsSync2(agentConfigPath)) {
2519
963
  return { ok: false, error: `Agent "${trimmedSlug}" not found \u2014 no config.json at ${agentConfigPath}. Check the agent slug and try again.` };
2520
964
  }
2521
965
  try {
@@ -2744,7 +1188,7 @@ function listCredentialAccountIds(configDir2) {
2744
1188
  }
2745
1189
 
2746
1190
  // app/lib/whatsapp/session.ts
2747
- import { randomUUID as randomUUID2 } from "crypto";
1191
+ import { randomUUID } from "crypto";
2748
1192
  import fsSync2 from "fs";
2749
1193
  import fs2 from "fs/promises";
2750
1194
  import { inspect } from "util";
@@ -2819,7 +1263,7 @@ var credsSaveQueue = Promise.resolve();
2819
1263
  async function drainCredsSaveQueue(timeoutMs = 5e3) {
2820
1264
  console.error(`${TAG3} draining credential save queue\u2026`);
2821
1265
  const timer2 = new Promise(
2822
- (resolve26) => setTimeout(() => resolve26("timeout"), timeoutMs)
1266
+ (resolve22) => setTimeout(() => resolve22("timeout"), timeoutMs)
2823
1267
  );
2824
1268
  const result = await Promise.race([
2825
1269
  credsSaveQueue.then(() => "drained"),
@@ -2947,11 +1391,11 @@ async function createWaSocket(opts) {
2947
1391
  return sock;
2948
1392
  }
2949
1393
  async function waitForConnection(sock) {
2950
- return new Promise((resolve26, reject) => {
1394
+ return new Promise((resolve22, reject) => {
2951
1395
  const handler = (update) => {
2952
1396
  if (update.connection === "open") {
2953
1397
  sock.ev.off("connection.update", handler);
2954
- resolve26();
1398
+ resolve22();
2955
1399
  }
2956
1400
  if (update.connection === "close") {
2957
1401
  sock.ev.off("connection.update", handler);
@@ -3065,14 +1509,14 @@ ${inspected}`;
3065
1509
  return inspect2(err, INSPECT_OPTS2);
3066
1510
  }
3067
1511
  function withTimeout(label, promise, timeoutMs) {
3068
- return new Promise((resolve26, reject) => {
1512
+ return new Promise((resolve22, reject) => {
3069
1513
  const timer2 = setTimeout(() => {
3070
1514
  reject(new Error(`${label} timed out after ${timeoutMs}ms`));
3071
1515
  }, timeoutMs);
3072
1516
  promise.then(
3073
1517
  (value) => {
3074
1518
  clearTimeout(timer2);
3075
- resolve26(value);
1519
+ resolve22(value);
3076
1520
  },
3077
1521
  (err) => {
3078
1522
  clearTimeout(timer2);
@@ -3607,8 +2051,8 @@ async function persistWhatsAppMessage(input) {
3607
2051
  const { givenName, familyName } = splitName(input.pushName);
3608
2052
  const prev = sessionWriteLocks.get(input.sessionKey);
3609
2053
  let release;
3610
- const mine = new Promise((resolve26) => {
3611
- release = resolve26;
2054
+ const mine = new Promise((resolve22) => {
2055
+ release = resolve22;
3612
2056
  });
3613
2057
  const chained = (prev ?? Promise.resolve()).then(() => mine);
3614
2058
  sessionWriteLocks.set(input.sessionKey, chained);
@@ -3793,8 +2237,8 @@ async function ensureWhatsAppConversation(input) {
3793
2237
  }
3794
2238
 
3795
2239
  // app/lib/whatsapp/platform-account-id.ts
3796
- import { readdirSync as readdirSync2, readFileSync as readFileSync5 } from "fs";
3797
- import { resolve as resolve6 } from "path";
2240
+ import { readdirSync, readFileSync as readFileSync2 } from "fs";
2241
+ import { resolve as resolve2 } from "path";
3798
2242
  var UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
3799
2243
  var cached = null;
3800
2244
  var cachedAccountsDir = null;
@@ -3818,7 +2262,7 @@ function resolvePlatformAccountId(accountsDir = ACCOUNTS_DIR) {
3818
2262
  function enumerateValidAccountIds(accountsDir) {
3819
2263
  let names;
3820
2264
  try {
3821
- names = readdirSync2(accountsDir);
2265
+ names = readdirSync(accountsDir);
3822
2266
  } catch (err) {
3823
2267
  if (err.code === "ENOENT") return [];
3824
2268
  throw err;
@@ -3826,9 +2270,9 @@ function enumerateValidAccountIds(accountsDir) {
3826
2270
  const valid = [];
3827
2271
  for (const name of names) {
3828
2272
  if (!UUID_RE.test(name)) continue;
3829
- const configPath2 = resolve6(accountsDir, name, "account.json");
2273
+ const configPath2 = resolve2(accountsDir, name, "account.json");
3830
2274
  try {
3831
- JSON.parse(readFileSync5(configPath2, "utf-8"));
2275
+ JSON.parse(readFileSync2(configPath2, "utf-8"));
3832
2276
  valid.push(name);
3833
2277
  } catch (err) {
3834
2278
  const code = err.code;
@@ -3839,9 +2283,9 @@ function enumerateValidAccountIds(accountsDir) {
3839
2283
  }
3840
2284
 
3841
2285
  // app/lib/whatsapp/inbound/media.ts
3842
- import { randomUUID as randomUUID3 } from "crypto";
2286
+ import { randomUUID as randomUUID2 } from "crypto";
3843
2287
  import { writeFile, mkdir } from "fs/promises";
3844
- import { join as join4 } from "path";
2288
+ import { join as join3 } from "path";
3845
2289
  import {
3846
2290
  downloadMediaMessage,
3847
2291
  downloadContentFromMessage,
@@ -3926,8 +2370,8 @@ async function downloadInboundMedia(msg, sock, opts) {
3926
2370
  }
3927
2371
  await mkdir(MEDIA_DIR, { recursive: true });
3928
2372
  const ext = mimeToExt(mimetype ?? "application/octet-stream");
3929
- const filename = `${randomUUID3()}.${ext}`;
3930
- const filePath = join4(MEDIA_DIR, filename);
2373
+ const filename = `${randomUUID2()}.${ext}`;
2374
+ const filePath = join3(MEDIA_DIR, filename);
3931
2375
  await writeFile(filePath, buffer);
3932
2376
  const sizeKB = (buffer.length / 1024).toFixed(0);
3933
2377
  console.error(`${TAG9} media downloaded type=${mimetype ?? "unknown"} size=${sizeKB}KB path=${filePath}`);
@@ -4615,11 +3059,11 @@ async function connectWithReconnect(conn) {
4615
3059
  console.error(
4616
3060
  `${TAG13} reconnecting account=${conn.accountId} in ${delay}ms (attempt ${decision.nextAttempts}/${maxAttempts})`
4617
3061
  );
4618
- await new Promise((resolve26) => {
4619
- const timer2 = setTimeout(resolve26, delay);
3062
+ await new Promise((resolve22) => {
3063
+ const timer2 = setTimeout(resolve22, delay);
4620
3064
  conn.abortController.signal.addEventListener("abort", () => {
4621
3065
  clearTimeout(timer2);
4622
- resolve26();
3066
+ resolve22();
4623
3067
  }, { once: true });
4624
3068
  });
4625
3069
  }
@@ -4627,16 +3071,16 @@ async function connectWithReconnect(conn) {
4627
3071
  }
4628
3072
  }
4629
3073
  function waitForDisconnectEvent(conn) {
4630
- return new Promise((resolve26) => {
3074
+ return new Promise((resolve22) => {
4631
3075
  if (!conn.sock) {
4632
- resolve26();
3076
+ resolve22();
4633
3077
  return;
4634
3078
  }
4635
3079
  const sock = conn.sock;
4636
3080
  const handler = (update) => {
4637
3081
  if (update.connection === "close") {
4638
3082
  sock.ev.off("connection.update", handler);
4639
- resolve26();
3083
+ resolve22();
4640
3084
  }
4641
3085
  };
4642
3086
  sock.ev.on("connection.update", handler);
@@ -4897,8 +3341,8 @@ async function handleInboundMessage(conn, msg) {
4897
3341
  const conversationKey = isGroup ? remoteJid : senderPhone;
4898
3342
  const debounceKey = `${conn.accountId}:${conversationKey}:${senderPhone}`;
4899
3343
  let resolvePending;
4900
- const sttPending = new Promise((resolve26) => {
4901
- resolvePending = resolve26;
3344
+ const sttPending = new Promise((resolve22) => {
3345
+ resolvePending = resolve22;
4902
3346
  });
4903
3347
  if (conn.debouncer) conn.debouncer.registerPending(debounceKey, sttPending);
4904
3348
  try {
@@ -5019,20 +3463,20 @@ async function probeApiKey() {
5019
3463
  return result.status;
5020
3464
  }
5021
3465
  function checkPort(port2, timeoutMs = 500) {
5022
- return new Promise((resolve26) => {
3466
+ return new Promise((resolve22) => {
5023
3467
  const socket = createConnection(port2, "127.0.0.1");
5024
3468
  socket.setTimeout(timeoutMs);
5025
3469
  socket.once("connect", () => {
5026
3470
  socket.destroy();
5027
- resolve26(true);
3471
+ resolve22(true);
5028
3472
  });
5029
3473
  socket.once("error", () => {
5030
3474
  socket.destroy();
5031
- resolve26(false);
3475
+ resolve22(false);
5032
3476
  });
5033
3477
  socket.once("timeout", () => {
5034
3478
  socket.destroy();
5035
- resolve26(false);
3479
+ resolve22(false);
5036
3480
  });
5037
3481
  });
5038
3482
  }
@@ -5041,8 +3485,8 @@ app.get("/", async (c) => {
5041
3485
  const browserTransport = resolveBrowserTransport(c.req.raw, c.env?.incoming?.socket?.remoteAddress);
5042
3486
  let pinConfigured = false;
5043
3487
  try {
5044
- if (existsSync6(USERS_FILE)) {
5045
- const raw = readFileSync6(USERS_FILE, "utf-8").trim();
3488
+ if (existsSync3(USERS_FILE)) {
3489
+ const raw = readFileSync3(USERS_FILE, "utf-8").trim();
5046
3490
  if (raw) {
5047
3491
  const users = JSON.parse(raw);
5048
3492
  pinConfigured = Array.isArray(users) && users.length > 0;
@@ -5061,7 +3505,7 @@ app.get("/", async (c) => {
5061
3505
  const vncRunning = await checkPort(6080);
5062
3506
  let apiKeyConfigured = false;
5063
3507
  try {
5064
- apiKeyConfigured = existsSync6(keyFilePath());
3508
+ apiKeyConfigured = existsSync3(keyFilePath());
5065
3509
  } catch {
5066
3510
  }
5067
3511
  let apiKeyStatus = "missing";
@@ -5094,7 +3538,6 @@ app.get("/", async (c) => {
5094
3538
  const step = await loadOnboardingStep(account.accountId);
5095
3539
  if (step !== null) onboardingComplete = step >= 6;
5096
3540
  }
5097
- const reviewDetector = getReviewDetectorSnapshot();
5098
3541
  return c.json({
5099
3542
  pin_configured: pinConfigured,
5100
3543
  claude_authenticated: claudeAuthenticated,
@@ -5110,31 +3553,20 @@ app.get("/", async (c) => {
5110
3553
  any_connected: whatsappAnyConnected,
5111
3554
  any_stuck: whatsappAnyStuck,
5112
3555
  accounts: whatsappAccounts
5113
- },
5114
- review_detector: {
5115
- state: reviewDetector.state,
5116
- started_at: reviewDetector.startedAt,
5117
- last_scan_at: reviewDetector.lastScanAt,
5118
- last_scan_duration_ms: reviewDetector.lastScanDurationMs,
5119
- scan_cycles: reviewDetector.scanCycles,
5120
- rules_loaded: reviewDetector.rulesLoaded,
5121
- sources_tracked: reviewDetector.sourcesTracked,
5122
- active_alerts: reviewDetector.activeAlerts,
5123
- last_error: reviewDetector.lastError
5124
3556
  }
5125
3557
  });
5126
3558
  });
5127
3559
  var health_default = app;
5128
3560
 
5129
3561
  // server/routes/session.ts
5130
- import { resolve as resolve7 } from "path";
5131
- import { existsSync as existsSync7, writeFileSync as writeFileSync5, mkdirSync as mkdirSync4 } from "fs";
3562
+ import { resolve as resolve3 } from "path";
3563
+ import { existsSync as existsSync4, writeFileSync as writeFileSync2, mkdirSync } from "fs";
5132
3564
  var UUID_RE2 = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
5133
3565
  function writeBrandingCache(accountId, agentSlug, branding) {
5134
3566
  try {
5135
- const cacheDir = resolve7(MAXY_DIR, "branding-cache", accountId);
5136
- mkdirSync4(cacheDir, { recursive: true });
5137
- writeFileSync5(resolve7(cacheDir, `${agentSlug}.json`), JSON.stringify(branding), "utf-8");
3567
+ const cacheDir = resolve3(MAXY_DIR, "branding-cache", accountId);
3568
+ mkdirSync(cacheDir, { recursive: true });
3569
+ writeFileSync2(resolve3(cacheDir, `${agentSlug}.json`), JSON.stringify(branding), "utf-8");
5138
3570
  } catch (err) {
5139
3571
  console.error(`[branding] cache write failed: ${err instanceof Error ? err.message : String(err)}`);
5140
3572
  }
@@ -5204,9 +3636,9 @@ app2.post("/", async (c) => {
5204
3636
  }
5205
3637
  let agentConfig = null;
5206
3638
  if (account) {
5207
- const agentDir = resolve7(account.accountDir, "agents", agentSlug);
5208
- const agentConfigPath = resolve7(agentDir, "config.json");
5209
- if (!existsSync7(agentDir) || !existsSync7(agentConfigPath)) {
3639
+ const agentDir = resolve3(account.accountDir, "agents", agentSlug);
3640
+ const agentConfigPath = resolve3(agentDir, "config.json");
3641
+ if (!existsSync4(agentDir) || !existsSync4(agentConfigPath)) {
5210
3642
  return c.json({ error: "Agent not found" }, 404);
5211
3643
  }
5212
3644
  agentConfig = resolveAgentConfig(account.accountDir, agentSlug);
@@ -5456,12 +3888,12 @@ ${raw}`;
5456
3888
  }
5457
3889
 
5458
3890
  // app/lib/attachments.ts
5459
- import { randomUUID as randomUUID4 } from "crypto";
3891
+ import { randomUUID as randomUUID3 } from "crypto";
5460
3892
  import { mkdir as mkdir2, readFile, stat as stat2, writeFile as writeFile2 } from "fs/promises";
5461
3893
  import { realpathSync } from "fs";
5462
- import { resolve as resolve8, extname, basename as basename3 } from "path";
5463
- var PLATFORM_ROOT2 = process.env.MAXY_PLATFORM_ROOT ?? resolve8(process.cwd(), "../platform");
5464
- var ATTACHMENTS_ROOT = resolve8(PLATFORM_ROOT2, "..", "data/uploads");
3894
+ import { resolve as resolve4, extname, basename } from "path";
3895
+ var PLATFORM_ROOT2 = process.env.MAXY_PLATFORM_ROOT ?? resolve4(process.cwd(), "../platform");
3896
+ var ATTACHMENTS_ROOT = resolve4(PLATFORM_ROOT2, "..", "data/uploads");
5465
3897
  var SUPPORTED_MIME_TYPES = /* @__PURE__ */ new Set([
5466
3898
  "image/jpeg",
5467
3899
  "image/png",
@@ -5487,12 +3919,12 @@ function assertSupportedMime(mimeType) {
5487
3919
  }
5488
3920
  }
5489
3921
  async function writeAttachment(scope, filename, mimeType, sizeBytes, buffer) {
5490
- const attachmentId = randomUUID4();
5491
- const dir = resolve8(ATTACHMENTS_ROOT, scope, attachmentId);
3922
+ const attachmentId = randomUUID3();
3923
+ const dir = resolve4(ATTACHMENTS_ROOT, scope, attachmentId);
5492
3924
  await mkdir2(dir, { recursive: true });
5493
3925
  const ext = extname(filename) || "";
5494
- const storagePath = resolve8(dir, `${attachmentId}${ext}`);
5495
- const metaPath = resolve8(dir, `${attachmentId}.meta.json`);
3926
+ const storagePath = resolve4(dir, `${attachmentId}${ext}`);
3927
+ const metaPath = resolve4(dir, `${attachmentId}.meta.json`);
5496
3928
  const meta = {
5497
3929
  attachmentId,
5498
3930
  scope,
@@ -5566,7 +3998,7 @@ async function storeGeneratedFile(accountId, filePath) {
5566
3998
  `File exceeds the 20 MB limit (${(fileStat.size / 1024 / 1024).toFixed(1)} MB).`
5567
3999
  );
5568
4000
  }
5569
- const filename = basename3(filePath);
4001
+ const filename = basename(filePath);
5570
4002
  const mimeType = detectMimeType(filePath);
5571
4003
  const buffer = await readFile(filePath);
5572
4004
  return writeAttachment(accountId, filename, mimeType, fileStat.size, buffer);
@@ -5575,7 +4007,7 @@ async function storeGeneratedFile(accountId, filePath) {
5575
4007
  // app/lib/stt/voice-note.ts
5576
4008
  import { writeFile as writeFile3, mkdtemp, rm } from "fs/promises";
5577
4009
  import { tmpdir } from "os";
5578
- import { join as join5 } from "path";
4010
+ import { join as join4 } from "path";
5579
4011
  var TAG14 = "[voice]";
5580
4012
  var AUDIO_MIME_TYPES = /* @__PURE__ */ new Set([
5581
4013
  "audio/ogg",
@@ -5613,9 +4045,9 @@ async function transcribeVoiceNote(file, source) {
5613
4045
  let tempDir;
5614
4046
  let tempPath;
5615
4047
  try {
5616
- tempDir = await mkdtemp(join5(tmpdir(), "voice-"));
4048
+ tempDir = await mkdtemp(join4(tmpdir(), "voice-"));
5617
4049
  const ext = audioExtension(mimeType);
5618
- tempPath = join5(tempDir, `recording${ext}`);
4050
+ tempPath = join4(tempDir, `recording${ext}`);
5619
4051
  const buffer = Buffer.from(await file.arrayBuffer());
5620
4052
  await writeFile3(tempPath, buffer);
5621
4053
  } catch (err) {
@@ -6170,16 +4602,16 @@ var group_default = app4;
6170
4602
 
6171
4603
  // app/lib/access-gate.ts
6172
4604
  import neo4j from "neo4j-driver";
6173
- import { readFileSync as readFileSync7 } from "fs";
6174
- import { resolve as resolve9 } from "path";
6175
- import { randomUUID as randomUUID5, randomInt } from "crypto";
6176
- var PLATFORM_ROOT3 = process.env.MAXY_PLATFORM_ROOT ?? resolve9(process.cwd(), "..");
4605
+ import { readFileSync as readFileSync4 } from "fs";
4606
+ import { resolve as resolve5 } from "path";
4607
+ import { randomUUID as randomUUID4, randomInt } from "crypto";
4608
+ var PLATFORM_ROOT3 = process.env.MAXY_PLATFORM_ROOT ?? resolve5(process.cwd(), "..");
6177
4609
  var driver = null;
6178
4610
  function readPassword() {
6179
4611
  if (process.env.NEO4J_PASSWORD) return process.env.NEO4J_PASSWORD;
6180
- const passwordFile = resolve9(PLATFORM_ROOT3, "config/.neo4j-password");
4612
+ const passwordFile = resolve5(PLATFORM_ROOT3, "config/.neo4j-password");
6181
4613
  try {
6182
- return readFileSync7(passwordFile, "utf-8").trim();
4614
+ return readFileSync4(passwordFile, "utf-8").trim();
6183
4615
  } catch {
6184
4616
  throw new Error(
6185
4617
  `Neo4j password not found. Expected at ${passwordFile} or in NEO4J_PASSWORD env var.`
@@ -6428,7 +4860,7 @@ async function setGrantPassword(grantId, passwordHash) {
6428
4860
  }
6429
4861
  }
6430
4862
  async function generateNewMagicToken(grantId) {
6431
- const token = randomUUID5();
4863
+ const token = randomUUID4();
6432
4864
  const session = getSession2();
6433
4865
  try {
6434
4866
  await session.run(
@@ -6490,19 +4922,19 @@ async function findActiveGrantByContact(contactValue, agentSlug, accountId) {
6490
4922
  }
6491
4923
 
6492
4924
  // app/lib/brevo-sms.ts
6493
- import { readFileSync as readFileSync8, writeFileSync as writeFileSync6, mkdirSync as mkdirSync5, existsSync as existsSync8, chmodSync } from "fs";
6494
- import { dirname as dirname4 } from "path";
6495
- import { resolve as resolve10 } from "path";
6496
- var BREVO_API_KEY_FILE = resolve10(MAXY_DIR, ".brevo-api-key");
4925
+ import { readFileSync as readFileSync5, writeFileSync as writeFileSync3, mkdirSync as mkdirSync2, existsSync as existsSync5, chmodSync } from "fs";
4926
+ import { dirname } from "path";
4927
+ import { resolve as resolve6 } from "path";
4928
+ var BREVO_API_KEY_FILE = resolve6(MAXY_DIR, ".brevo-api-key");
6497
4929
  var BREVO_API_URL = "https://api.brevo.com/v3/transactionalSMS/sms";
6498
4930
  var BREVO_TIMEOUT_MS = 1e4;
6499
4931
  var BREVO_SENDER = "Maxy";
6500
4932
  var platformRoot = process.env.MAXY_PLATFORM_ROOT;
6501
4933
  if (platformRoot) {
6502
4934
  try {
6503
- const brandPath = resolve10(platformRoot, "config", "brand.json");
6504
- if (existsSync8(brandPath)) {
6505
- const brand = JSON.parse(readFileSync8(brandPath, "utf-8"));
4935
+ const brandPath = resolve6(platformRoot, "config", "brand.json");
4936
+ if (existsSync5(brandPath)) {
4937
+ const brand = JSON.parse(readFileSync5(brandPath, "utf-8"));
6506
4938
  if (brand.productName) BREVO_SENDER = brand.productName;
6507
4939
  }
6508
4940
  } catch {
@@ -6510,7 +4942,7 @@ if (platformRoot) {
6510
4942
  }
6511
4943
  function readBrevoApiKey() {
6512
4944
  try {
6513
- const key = readFileSync8(BREVO_API_KEY_FILE, "utf-8").trim();
4945
+ const key = readFileSync5(BREVO_API_KEY_FILE, "utf-8").trim();
6514
4946
  if (!key) {
6515
4947
  throw new Error(`Brevo API key file is empty: ${BREVO_API_KEY_FILE}`);
6516
4948
  }
@@ -6525,7 +4957,7 @@ function readBrevoApiKey() {
6525
4957
  }
6526
4958
  }
6527
4959
  function hasBrevoApiKey() {
6528
- return existsSync8(BREVO_API_KEY_FILE);
4960
+ return existsSync5(BREVO_API_KEY_FILE);
6529
4961
  }
6530
4962
  async function sendSms(recipient, content, opts) {
6531
4963
  let apiKey;
@@ -6941,7 +5373,7 @@ app5.post("/send-otp", async (c) => {
6941
5373
  var access_default = app5;
6942
5374
 
6943
5375
  // server/routes/telegram.ts
6944
- import { existsSync as existsSync9, readFileSync as readFileSync9 } from "fs";
5376
+ import { existsSync as existsSync6, readFileSync as readFileSync6 } from "fs";
6945
5377
  import { timingSafeEqual } from "crypto";
6946
5378
 
6947
5379
  // app/lib/telegram/access-control.ts
@@ -6978,8 +5410,8 @@ var TELEGRAM_API = "https://api.telegram.org";
6978
5410
  function getWebhookSecret(botType) {
6979
5411
  const filePath = botType === "admin" ? TELEGRAM_ADMIN_WEBHOOK_SECRET_FILE : TELEGRAM_WEBHOOK_SECRET_FILE;
6980
5412
  try {
6981
- if (!existsSync9(filePath)) return null;
6982
- const secret = readFileSync9(filePath, "utf-8").trim();
5413
+ if (!existsSync6(filePath)) return null;
5414
+ const secret = readFileSync6(filePath, "utf-8").trim();
6983
5415
  return secret || null;
6984
5416
  } catch {
6985
5417
  return null;
@@ -7137,12 +5569,12 @@ app6.post("/webhook", async (c) => {
7137
5569
  var telegram_default = app6;
7138
5570
 
7139
5571
  // server/routes/whatsapp.ts
7140
- import { join as join6, resolve as resolve11, basename as basename4 } from "path";
5572
+ import { join as join5, resolve as resolve7, basename as basename2 } from "path";
7141
5573
  import { readFile as readFile2, stat as stat3 } from "fs/promises";
7142
- import { realpathSync as realpathSync2, readdirSync as readdirSync3, readFileSync as readFileSync10, existsSync as existsSync10 } from "fs";
5574
+ import { realpathSync as realpathSync2, readdirSync as readdirSync2, readFileSync as readFileSync7, existsSync as existsSync7 } from "fs";
7143
5575
 
7144
5576
  // app/lib/whatsapp/login.ts
7145
- import { randomUUID as randomUUID6 } from "crypto";
5577
+ import { randomUUID as randomUUID5 } from "crypto";
7146
5578
  var TAG17 = "[whatsapp:login]";
7147
5579
  var ACTIVE_LOGIN_TTL_MS = 3 * 6e4;
7148
5580
  var activeLogins = /* @__PURE__ */ new Map();
@@ -7245,8 +5677,8 @@ async function startLogin(opts) {
7245
5677
  resetActiveLogin(accountId);
7246
5678
  let resolveQr = null;
7247
5679
  let rejectQr = null;
7248
- const qrPromise = new Promise((resolve26, reject) => {
7249
- resolveQr = resolve26;
5680
+ const qrPromise = new Promise((resolve22, reject) => {
5681
+ resolveQr = resolve22;
7250
5682
  rejectQr = reject;
7251
5683
  });
7252
5684
  const qrTimer = setTimeout(
@@ -7281,7 +5713,7 @@ async function startLogin(opts) {
7281
5713
  const login = {
7282
5714
  accountId,
7283
5715
  authDir,
7284
- id: randomUUID6(),
5716
+ id: randomUUID5(),
7285
5717
  sock,
7286
5718
  startedAt: Date.now(),
7287
5719
  connected: false
@@ -7480,7 +5912,7 @@ app7.post("/login/start", async (c) => {
7480
5912
  const body = await c.req.json().catch(() => ({}));
7481
5913
  const accountId = validateAccountId(body.accountId);
7482
5914
  const force = body.force ?? false;
7483
- const authDir = join6(MAXY_DIR, "credentials", "whatsapp", accountId);
5915
+ const authDir = join5(MAXY_DIR, "credentials", "whatsapp", accountId);
7484
5916
  const result = await startLogin({ accountId, authDir, force });
7485
5917
  console.error(`${TAG18} login/start result account=${accountId} hasQr=${!!result.qrRaw}${result.selfPhone ? ` phone=${result.selfPhone}` : ""}`);
7486
5918
  return c.json(result);
@@ -7640,17 +6072,17 @@ app7.post("/config", async (c) => {
7640
6072
  return c.json(result, result.ok ? 200 : 400);
7641
6073
  }
7642
6074
  case "list-public-agents": {
7643
- const agentsDir = resolve11(account.accountDir, "agents");
6075
+ const agentsDir = resolve7(account.accountDir, "agents");
7644
6076
  const agents = [];
7645
- if (existsSync10(agentsDir)) {
6077
+ if (existsSync7(agentsDir)) {
7646
6078
  try {
7647
- const entries = readdirSync3(agentsDir, { withFileTypes: true });
6079
+ const entries = readdirSync2(agentsDir, { withFileTypes: true });
7648
6080
  for (const entry of entries.sort((a, b) => a.name.localeCompare(b.name))) {
7649
6081
  if (!entry.isDirectory() || entry.name === "admin") continue;
7650
- const configPath2 = resolve11(agentsDir, entry.name, "config.json");
7651
- if (!existsSync10(configPath2)) continue;
6082
+ const configPath2 = resolve7(agentsDir, entry.name, "config.json");
6083
+ if (!existsSync7(configPath2)) continue;
7652
6084
  try {
7653
- const config = JSON.parse(readFileSync10(configPath2, "utf-8"));
6085
+ const config = JSON.parse(readFileSync7(configPath2, "utf-8"));
7654
6086
  agents.push({ slug: entry.name, displayName: config.displayName ?? entry.name });
7655
6087
  } catch {
7656
6088
  console.error(`${TAG18} config action=list-public-agents error="failed to parse config.json for agent ${entry.name}" \u2014 skipping`);
@@ -7725,7 +6157,7 @@ app7.post("/send-document", async (c) => {
7725
6157
  if (!maxyAccountId || !PLATFORM_ROOT4) {
7726
6158
  return c.json({ error: "Cannot validate file path: missing account or platform context" }, 400);
7727
6159
  }
7728
- const accountDir = resolve11(PLATFORM_ROOT4, "..", "data/accounts", maxyAccountId);
6160
+ const accountDir = resolve7(PLATFORM_ROOT4, "..", "data/accounts", maxyAccountId);
7729
6161
  let resolvedPath;
7730
6162
  try {
7731
6163
  resolvedPath = realpathSync2(filePath);
@@ -7749,7 +6181,7 @@ app7.post("/send-document", async (c) => {
7749
6181
  return c.json({ error: `File exceeds 20 MB limit (${(fileStat.size / 1024 / 1024).toFixed(1)} MB)` }, 400);
7750
6182
  }
7751
6183
  const buffer = Buffer.from(await readFile2(resolvedPath));
7752
- const filename = basename4(resolvedPath);
6184
+ const filename = basename2(resolvedPath);
7753
6185
  const mimetype = detectMimeType(resolvedPath);
7754
6186
  const sock = getSocket(accountId);
7755
6187
  if (!sock) {
@@ -7949,16 +6381,16 @@ var whatsapp_default = app7;
7949
6381
 
7950
6382
  // server/routes/onboarding.ts
7951
6383
  import { spawn, execFileSync } from "child_process";
7952
- import { openSync as openSync2, closeSync as closeSync2, writeFileSync as writeFileSync7, writeSync, existsSync as existsSync11, mkdirSync as mkdirSync6, readFileSync as readFileSync11, unlinkSync } from "fs";
7953
- import { resolve as resolve12, dirname as dirname5 } from "path";
7954
- import { createHash, randomUUID as randomUUID7 } from "crypto";
6384
+ import { openSync, closeSync, writeFileSync as writeFileSync4, writeSync, existsSync as existsSync8, mkdirSync as mkdirSync3, readFileSync as readFileSync8, unlinkSync } from "fs";
6385
+ import { resolve as resolve8, dirname as dirname2 } from "path";
6386
+ import { createHash, randomUUID as randomUUID6 } from "crypto";
7955
6387
  var PLATFORM_ROOT5 = process.env.MAXY_PLATFORM_ROOT || "";
7956
6388
  function hashPin(pin) {
7957
6389
  return createHash("sha256").update(pin).digest("hex");
7958
6390
  }
7959
6391
  function readUsersFile() {
7960
- if (!existsSync11(USERS_FILE)) return null;
7961
- const raw = readFileSync11(USERS_FILE, "utf-8").trim();
6392
+ if (!existsSync8(USERS_FILE)) return null;
6393
+ const raw = readFileSync8(USERS_FILE, "utf-8").trim();
7962
6394
  if (!raw) return [];
7963
6395
  return JSON.parse(raw);
7964
6396
  }
@@ -8024,11 +6456,11 @@ app8.post("/claude-auth", async (c) => {
8024
6456
  if (!vncReady) return c.json({ error: "VNC display failed to start" }, 500);
8025
6457
  }
8026
6458
  await ensureCdp(transport);
8027
- writeFileSync7(logPath("claude-auth"), "");
6459
+ writeFileSync4(logPath("claude-auth"), "");
8028
6460
  const chromiumWrapper = writeChromiumWrapper();
8029
6461
  const x11Env = buildX11Env(chromiumWrapper, transport);
8030
6462
  vncLog("claude-auth", { action: "start", transport });
8031
- const claudeAuthLogFd = openSync2(logPath("claude-auth"), "a");
6463
+ const claudeAuthLogFd = openSync(logPath("claude-auth"), "a");
8032
6464
  const claudeProc = spawn("claude", ["auth", "login"], {
8033
6465
  env: x11Env,
8034
6466
  stdio: ["ignore", "pipe", "pipe"]
@@ -8037,7 +6469,7 @@ app8.post("/claude-auth", async (c) => {
8037
6469
  const onClaudeOutput = (chunk) => writeSync(claudeAuthLogFd, chunk);
8038
6470
  claudeProc.stdout?.on("data", onClaudeOutput);
8039
6471
  claudeProc.stderr?.on("data", onClaudeOutput);
8040
- claudeProc.once("close", () => closeSync2(claudeAuthLogFd));
6472
+ claudeProc.once("close", () => closeSync(claudeAuthLogFd));
8041
6473
  await waitForAuthPage(2e4);
8042
6474
  return c.json({ started: true, transport });
8043
6475
  });
@@ -8068,22 +6500,22 @@ app8.post("/set-pin", async (c) => {
8068
6500
  const hash = hashPin(body.pin);
8069
6501
  const account = resolveAccount();
8070
6502
  const existingOwnerUserId = account?.config.admins?.find((a) => a.role === "owner")?.userId;
8071
- const userId = existingOwnerUserId ?? randomUUID7();
6503
+ const userId = existingOwnerUserId ?? randomUUID6();
8072
6504
  if (existingOwnerUserId) {
8073
6505
  console.log(`[set-pin] reusing existing owner userId=${userId.slice(0, 8)}\u2026 (change-PIN preserves identity)`);
8074
6506
  } else {
8075
6507
  console.log(`[set-pin] minted new userId=${userId.slice(0, 8)}\u2026 (first-time install)`);
8076
6508
  }
8077
- mkdirSync6(dirname5(USERS_FILE), { recursive: true });
8078
- writeFileSync7(USERS_FILE, JSON.stringify([{ userId, pin: hash }]), { mode: 384 });
6509
+ mkdirSync3(dirname2(USERS_FILE), { recursive: true });
6510
+ writeFileSync4(USERS_FILE, JSON.stringify([{ userId, pin: hash }]), { mode: 384 });
8079
6511
  console.log(`[set-pin] wrote users.json: userId=${userId.slice(0, 8)}\u2026 hash=${hash.slice(0, 8)}\u2026`);
8080
6512
  if (account) {
8081
6513
  try {
8082
- const config = JSON.parse(readFileSync11(`${account.accountDir}/account.json`, "utf-8"));
6514
+ const config = JSON.parse(readFileSync8(`${account.accountDir}/account.json`, "utf-8"));
8083
6515
  if (!config.admins) config.admins = [];
8084
6516
  if (!config.admins.some((a) => a.userId === userId)) {
8085
6517
  config.admins.push({ userId, role: "owner" });
8086
- writeFileSync7(`${account.accountDir}/account.json`, JSON.stringify(config, null, 2) + "\n");
6518
+ writeFileSync4(`${account.accountDir}/account.json`, JSON.stringify(config, null, 2) + "\n");
8087
6519
  console.log(`[set-pin] added userId=${userId.slice(0, 8)}\u2026 to account.json admins`);
8088
6520
  }
8089
6521
  } catch (err) {
@@ -8136,7 +6568,7 @@ app8.delete("/set-pin", async (c) => {
8136
6568
  unlinkSync(USERS_FILE);
8137
6569
  console.log(`[set-pin] cleared users.json (last entry removed): userId=${matchedUser.userId.slice(0, 8)}\u2026`);
8138
6570
  } else {
8139
- writeFileSync7(USERS_FILE, JSON.stringify(remaining), { mode: 384 });
6571
+ writeFileSync4(USERS_FILE, JSON.stringify(remaining), { mode: 384 });
8140
6572
  console.log(`[set-pin] removed entry from users.json: userId=${matchedUser.userId.slice(0, 8)}\u2026 remaining=${remaining.length}`);
8141
6573
  }
8142
6574
  return c.json({ ok: true });
@@ -8155,19 +6587,19 @@ app8.post("/skip", async (c) => {
8155
6587
  }
8156
6588
  const { accountId, accountDir } = account;
8157
6589
  let agentName = "Maxy";
8158
- const brandPath = PLATFORM_ROOT5 ? resolve12(PLATFORM_ROOT5, "config", "brand.json") : "";
8159
- if (brandPath && existsSync11(brandPath)) {
6590
+ const brandPath = PLATFORM_ROOT5 ? resolve8(PLATFORM_ROOT5, "config", "brand.json") : "";
6591
+ if (brandPath && existsSync8(brandPath)) {
8160
6592
  try {
8161
- const brand = JSON.parse(readFileSync11(brandPath, "utf-8"));
6593
+ const brand = JSON.parse(readFileSync8(brandPath, "utf-8"));
8162
6594
  if (brand.productName) agentName = brand.productName;
8163
6595
  } catch (err) {
8164
6596
  console.error(`[onboarding-skip] brand.json read failed: ${err instanceof Error ? err.message : String(err)}`);
8165
6597
  }
8166
6598
  }
8167
- const soulPath = resolve12(accountDir, "agents", "admin", "SOUL.md");
6599
+ const soulPath = resolve8(accountDir, "agents", "admin", "SOUL.md");
8168
6600
  try {
8169
- mkdirSync6(dirname5(soulPath), { recursive: true });
8170
- writeFileSync7(soulPath, `You are ${agentName}, an AI operations manager.
6601
+ mkdirSync3(dirname2(soulPath), { recursive: true });
6602
+ writeFileSync4(soulPath, `You are ${agentName}, an AI operations manager.
8171
6603
  `);
8172
6604
  console.log(`[onboarding-skip] wrote SOUL.md: ${soulPath}`);
8173
6605
  } catch (err) {
@@ -8211,9 +6643,9 @@ app8.post("/skip", async (c) => {
8211
6643
  var onboarding_default = app8;
8212
6644
 
8213
6645
  // server/routes/client-error.ts
8214
- import { appendFileSync as appendFileSync2, existsSync as existsSync12, renameSync as renameSync4, statSync as statSync5 } from "fs";
8215
- import { join as join7 } from "path";
8216
- var CLIENT_ERRORS_LOG = join7(LOG_DIR, "client-errors.log");
6646
+ import { appendFileSync, existsSync as existsSync9, renameSync, statSync as statSync2 } from "fs";
6647
+ import { join as join6 } from "path";
6648
+ var CLIENT_ERRORS_LOG = join6(LOG_DIR, "client-errors.log");
8217
6649
  var MAX_LOG_SIZE = 10 * 1024 * 1024;
8218
6650
  var MAX_BODY_SIZE = 8 * 1024;
8219
6651
  var MAX_STACK_LEN = 2e3;
@@ -8256,10 +6688,10 @@ function stackHead(stack) {
8256
6688
  }
8257
6689
  function rotateIfNeeded() {
8258
6690
  try {
8259
- if (!existsSync12(CLIENT_ERRORS_LOG)) return;
8260
- const stats = statSync5(CLIENT_ERRORS_LOG);
6691
+ if (!existsSync9(CLIENT_ERRORS_LOG)) return;
6692
+ const stats = statSync2(CLIENT_ERRORS_LOG);
8261
6693
  if (stats.size < MAX_LOG_SIZE) return;
8262
- renameSync4(CLIENT_ERRORS_LOG, CLIENT_ERRORS_LOG + ".1");
6694
+ renameSync(CLIENT_ERRORS_LOG, CLIENT_ERRORS_LOG + ".1");
8263
6695
  } catch (err) {
8264
6696
  console.error(`[client-error] log rotation failed: ${err instanceof Error ? err.message : String(err)}`);
8265
6697
  }
@@ -8360,7 +6792,7 @@ app9.post("/", async (c) => {
8360
6792
  tag: typeof body.tag === "string" ? truncate(body.tag, 32) : void 0,
8361
6793
  status: typeof body.status === "number" ? body.status : void 0
8362
6794
  };
8363
- appendFileSync2(CLIENT_ERRORS_LOG, JSON.stringify(payload) + "\n", "utf-8");
6795
+ appendFileSync(CLIENT_ERRORS_LOG, JSON.stringify(payload) + "\n", "utf-8");
8364
6796
  } catch (err) {
8365
6797
  console.error(`[client-error] append failed: ${err instanceof Error ? err.message : String(err)}`);
8366
6798
  }
@@ -8370,15 +6802,15 @@ app9.post("/", async (c) => {
8370
6802
  var client_error_default = app9;
8371
6803
 
8372
6804
  // server/routes/admin/session.ts
8373
- import { readFileSync as readFileSync12, writeFileSync as writeFileSync8, existsSync as existsSync13 } from "fs";
6805
+ import { readFileSync as readFileSync9, writeFileSync as writeFileSync5, existsSync as existsSync10 } from "fs";
8374
6806
  import { createHash as createHash2 } from "crypto";
8375
6807
  var deprecationLogged = /* @__PURE__ */ new Set();
8376
6808
  function hashPin2(pin) {
8377
6809
  return createHash2("sha256").update(pin).digest("hex");
8378
6810
  }
8379
6811
  function readUsersFile2() {
8380
- if (!existsSync13(USERS_FILE)) return null;
8381
- const raw = readFileSync12(USERS_FILE, "utf-8").trim();
6812
+ if (!existsSync10(USERS_FILE)) return null;
6813
+ const raw = readFileSync9(USERS_FILE, "utf-8").trim();
8382
6814
  if (!raw) return [];
8383
6815
  return JSON.parse(raw);
8384
6816
  }
@@ -8395,7 +6827,7 @@ function stripLegacyNameField(users) {
8395
6827
  }
8396
6828
  }
8397
6829
  try {
8398
- writeFileSync8(USERS_FILE, JSON.stringify(users), { mode: 384 });
6830
+ writeFileSync5(USERS_FILE, JSON.stringify(users), { mode: 384 });
8399
6831
  } catch (err) {
8400
6832
  console.error(`[admin-identity] users-json strip failed: ${err instanceof Error ? err.message : String(err)}`);
8401
6833
  }
@@ -8552,13 +6984,13 @@ app10.post("/", async (c) => {
8552
6984
  var session_default2 = app10;
8553
6985
 
8554
6986
  // server/routes/admin/chat.ts
8555
- import { resolve as resolve13 } from "path";
8556
- import { appendFileSync as appendFileSync4 } from "fs";
6987
+ import { resolve as resolve9 } from "path";
6988
+ import { appendFileSync as appendFileSync3 } from "fs";
8557
6989
 
8558
6990
  // app/lib/script-stream-tailer.ts
8559
6991
  import * as childProcess from "child_process";
8560
- import { appendFileSync as appendFileSync3, createReadStream as createReadStream2, mkdirSync as mkdirSync7, statSync as statSync6 } from "fs";
8561
- import { dirname as dirname6 } from "path";
6992
+ import { appendFileSync as appendFileSync2, createReadStream as createReadStream2, mkdirSync as mkdirSync4, statSync as statSync3 } from "fs";
6993
+ import { dirname as dirname3 } from "path";
8562
6994
  import { StringDecoder } from "string_decoder";
8563
6995
  var SCRIPT_STREAM_RE = /^\[([^\]]+)\] \[script:([a-z][a-z0-9-]*)((?::[a-z0-9:_-]+)?)\] (.*)$/;
8564
6996
  function parseLine(line) {
@@ -8576,7 +7008,7 @@ function startScriptStreamTailer(opts) {
8576
7008
  const { path: path2, onEvent, onError } = opts;
8577
7009
  let offset;
8578
7010
  try {
8579
- offset = statSync6(path2).size;
7011
+ offset = statSync3(path2).size;
8580
7012
  } catch {
8581
7013
  offset = 0;
8582
7014
  }
@@ -8595,7 +7027,7 @@ function startScriptStreamTailer(opts) {
8595
7027
  try {
8596
7028
  let size;
8597
7029
  try {
8598
- size = statSync6(path2).size;
7030
+ size = statSync3(path2).size;
8599
7031
  } catch {
8600
7032
  return;
8601
7033
  }
@@ -8667,8 +7099,8 @@ function writeRouteMilestone(streamLogPath, scope, line) {
8667
7099
  }
8668
7100
  const ts = (/* @__PURE__ */ new Date()).toISOString();
8669
7101
  try {
8670
- mkdirSync7(dirname6(streamLogPath), { recursive: true });
8671
- appendFileSync3(streamLogPath, `[${ts}] [script:${scope}] ${line}
7102
+ mkdirSync4(dirname3(streamLogPath), { recursive: true });
7103
+ appendFileSync2(streamLogPath, `[${ts}] [script:${scope}] ${line}
8672
7104
  `);
8673
7105
  } catch (err) {
8674
7106
  console.error(
@@ -8843,7 +7275,7 @@ var app11 = new Hono();
8843
7275
  app11.post("/cancel", requireAdminSession, async (c) => {
8844
7276
  const session_key = c.var.sessionKey;
8845
7277
  try {
8846
- const { interruptClient: interruptClient2 } = await import("./client-pool-BMPFHXHB.js");
7278
+ const { interruptClient: interruptClient2 } = await import("./client-pool-GBY5I2KQ.js");
8847
7279
  await interruptClient2(session_key);
8848
7280
  return c.json({ ok: true });
8849
7281
  } catch (err) {
@@ -9007,10 +7439,10 @@ app11.post("/", requireAdminSession, async (c) => {
9007
7439
  function resolveTeeStreamLogPath() {
9008
7440
  const liveConvId = getConversationIdForSession(session_key);
9009
7441
  const key = liveConvId ?? preflushStreamLogKey(session_key);
9010
- return resolve13(account.accountDir, "logs", `claude-agent-stream-${key}.log`);
7442
+ return resolve9(account.accountDir, "logs", `claude-agent-stream-${key}.log`);
9011
7443
  }
9012
7444
  try {
9013
- appendFileSync4(resolveTeeStreamLogPath(), `[${(/* @__PURE__ */ new Date()).toISOString()}] [chat-route-version=task606-tee-path-resolve] sessionKey=${session_key.slice(0, 12)}\u2026
7445
+ appendFileSync3(resolveTeeStreamLogPath(), `[${(/* @__PURE__ */ new Date()).toISOString()}] [chat-route-version=task606-tee-path-resolve] sessionKey=${session_key.slice(0, 12)}\u2026
9014
7446
  `);
9015
7447
  } catch {
9016
7448
  }
@@ -9019,7 +7451,7 @@ app11.post("/", requireAdminSession, async (c) => {
9019
7451
  incoming.on("close", () => {
9020
7452
  const tsClose = (/* @__PURE__ */ new Date()).toISOString();
9021
7453
  try {
9022
- appendFileSync4(resolveTeeStreamLogPath(), `[${tsClose}] [incoming-close] sessionKey=${session_key.slice(0, 12)}\u2026 complete=${incoming.complete}
7454
+ appendFileSync3(resolveTeeStreamLogPath(), `[${tsClose}] [incoming-close] sessionKey=${session_key.slice(0, 12)}\u2026 complete=${incoming.complete}
9023
7455
  `);
9024
7456
  } catch {
9025
7457
  }
@@ -9027,7 +7459,7 @@ app11.post("/", requireAdminSession, async (c) => {
9027
7459
  console.error(`[${tsClose}] [incoming-close] DISCONNECT sessionKey=${session_key.slice(0, 12)}\u2026`);
9028
7460
  interruptClient(session_key).catch((err) => {
9029
7461
  try {
9030
- appendFileSync4(resolveTeeStreamLogPath(), `[${(/* @__PURE__ */ new Date()).toISOString()}] [incoming-close] interrupt-failed: ${err instanceof Error ? err.message : String(err)}
7462
+ appendFileSync3(resolveTeeStreamLogPath(), `[${(/* @__PURE__ */ new Date()).toISOString()}] [incoming-close] interrupt-failed: ${err instanceof Error ? err.message : String(err)}
9031
7463
  `);
9032
7464
  } catch {
9033
7465
  }
@@ -9036,7 +7468,7 @@ app11.post("/", requireAdminSession, async (c) => {
9036
7468
  });
9037
7469
  } else {
9038
7470
  try {
9039
- appendFileSync4(resolveTeeStreamLogPath(), `[${(/* @__PURE__ */ new Date()).toISOString()}] [incoming-close] UNAVAILABLE \u2014 c.env.incoming is not an EventEmitter
7471
+ appendFileSync3(resolveTeeStreamLogPath(), `[${(/* @__PURE__ */ new Date()).toISOString()}] [incoming-close] UNAVAILABLE \u2014 c.env.incoming is not an EventEmitter
9040
7472
  `);
9041
7473
  } catch {
9042
7474
  }
@@ -9048,7 +7480,7 @@ app11.post("/", requireAdminSession, async (c) => {
9048
7480
  } catch {
9049
7481
  }
9050
7482
  try {
9051
- appendFileSync4(resolveTeeStreamLogPath(), line);
7483
+ appendFileSync3(resolveTeeStreamLogPath(), line);
9052
7484
  } catch {
9053
7485
  }
9054
7486
  return true;
@@ -9109,7 +7541,7 @@ app11.post("/", requireAdminSession, async (c) => {
9109
7541
  try {
9110
7542
  registerAdminSSE(sseEntry);
9111
7543
  if (sseConvId) {
9112
- const streamLogPath = resolve13(account.accountDir, "logs", `claude-agent-stream-${sseConvId}.log`);
7544
+ const streamLogPath = resolve9(account.accountDir, "logs", `claude-agent-stream-${sseConvId}.log`);
9113
7545
  tailer = startScriptStreamTailer({
9114
7546
  path: streamLogPath,
9115
7547
  onEvent: (event) => {
@@ -9238,22 +7670,22 @@ app12.post("/", requireAdminSession, async (c) => {
9238
7670
  var compact_default = app12;
9239
7671
 
9240
7672
  // server/routes/admin/logs.ts
9241
- import { existsSync as existsSync15, readdirSync as readdirSync4, readFileSync as readFileSync13, statSync as statSync7 } from "fs";
9242
- import { resolve as resolve14, basename as basename5 } from "path";
7673
+ import { existsSync as existsSync12, readdirSync as readdirSync3, readFileSync as readFileSync10, statSync as statSync4 } from "fs";
7674
+ import { resolve as resolve10, basename as basename3 } from "path";
9243
7675
 
9244
7676
  // app/lib/logs-read-resolve.ts
9245
- import { existsSync as existsSync14 } from "fs";
9246
- import { join as join8 } from "path";
7677
+ import { existsSync as existsSync11 } from "fs";
7678
+ import { join as join7 } from "path";
9247
7679
  function resolveConversationLogPaths(fullFilename, preflushFilename, logDirs) {
9248
7680
  const tried = [fullFilename, preflushFilename];
9249
7681
  const hits = [];
9250
7682
  const stalePreflushPaths = [];
9251
7683
  for (const dir of logDirs) {
9252
- const fullPath = join8(dir, fullFilename);
9253
- if (existsSync14(fullPath)) {
7684
+ const fullPath = join7(dir, fullFilename);
7685
+ if (existsSync11(fullPath)) {
9254
7686
  hits.push({ path: fullPath, shape: "full", dir });
9255
- const preflushSibling = join8(dir, preflushFilename);
9256
- if (existsSync14(preflushSibling)) {
7687
+ const preflushSibling = join7(dir, preflushFilename);
7688
+ if (existsSync11(preflushSibling)) {
9257
7689
  stalePreflushPaths.push(preflushSibling);
9258
7690
  }
9259
7691
  }
@@ -9262,8 +7694,8 @@ function resolveConversationLogPaths(fullFilename, preflushFilename, logDirs) {
9262
7694
  return { hits, stalePreflushPaths, tried };
9263
7695
  }
9264
7696
  for (const dir of logDirs) {
9265
- const preflushPath = join8(dir, preflushFilename);
9266
- if (existsSync14(preflushPath)) {
7697
+ const preflushPath = join7(dir, preflushFilename);
7698
+ if (existsSync11(preflushPath)) {
9267
7699
  hits.push({ path: preflushPath, shape: "preflush", dir });
9268
7700
  }
9269
7701
  }
@@ -9283,19 +7715,19 @@ app13.get("/", async (c) => {
9283
7715
  const sessionKeyParam = c.req.query("sessionKey");
9284
7716
  const download = c.req.query("download") === "1";
9285
7717
  const account = resolveAccount();
9286
- const accountLogDir2 = account ? resolve14(account.accountDir, "logs") : null;
7718
+ const accountLogDir = account ? resolve10(account.accountDir, "logs") : null;
9287
7719
  const logDirs = [];
9288
- if (accountLogDir2) logDirs.push(accountLogDir2);
7720
+ if (accountLogDir) logDirs.push(accountLogDir);
9289
7721
  logDirs.push(LOG_DIR);
9290
7722
  if (fileParam) {
9291
- const safe = basename5(fileParam);
7723
+ const safe = basename3(fileParam);
9292
7724
  const searched = [];
9293
7725
  for (const dir of logDirs) {
9294
- const filePath = resolve14(dir, safe);
7726
+ const filePath = resolve10(dir, safe);
9295
7727
  searched.push(filePath);
9296
7728
  try {
9297
- const buffer = readFileSync13(filePath);
9298
- const onDiskBytes = statSync7(filePath).size;
7729
+ const buffer = readFileSync10(filePath);
7730
+ const onDiskBytes = statSync4(filePath).size;
9299
7731
  const headers = {
9300
7732
  "Content-Type": "text/plain; charset=utf-8",
9301
7733
  "Content-Length": String(buffer.byteLength)
@@ -9367,9 +7799,9 @@ app13.get("/", async (c) => {
9367
7799
  const hit = hits[0];
9368
7800
  console.info(`[admin/logs] resolved sessionKey=${sessionKeySlice} conversationId=${conversationIdSlice} shape=${hit.shape} stalePreflushCount=${stalePreflushCount}`);
9369
7801
  try {
9370
- const filename = basename5(hit.path);
7802
+ const filename = basename3(hit.path);
9371
7803
  if (stalePreflushCount > 0 && !download) {
9372
- const content = readFileSync13(hit.path, "utf-8");
7804
+ const content = readFileSync10(hit.path, "utf-8");
9373
7805
  return c.json({
9374
7806
  log: content,
9375
7807
  filename,
@@ -9377,8 +7809,8 @@ app13.get("/", async (c) => {
9377
7809
  warnings: stalePreflushPaths.map((path2) => ({ kind: "stale-preflush", path: path2 }))
9378
7810
  });
9379
7811
  }
9380
- const buffer = readFileSync13(hit.path);
9381
- const onDiskBytes = statSync7(hit.path).size;
7812
+ const buffer = readFileSync10(hit.path);
7813
+ const onDiskBytes = statSync4(hit.path).size;
9382
7814
  const headers = {
9383
7815
  "Content-Type": "text/plain; charset=utf-8",
9384
7816
  "Content-Length": String(buffer.byteLength)
@@ -9411,19 +7843,19 @@ app13.get("/", async (c) => {
9411
7843
  const seen = /* @__PURE__ */ new Set();
9412
7844
  const logs = {};
9413
7845
  for (const dir of logDirs) {
9414
- if (!existsSync15(dir)) continue;
7846
+ if (!existsSync12(dir)) continue;
9415
7847
  let files;
9416
7848
  try {
9417
- files = readdirSync4(dir).filter((f) => f.endsWith(".log"));
7849
+ files = readdirSync3(dir).filter((f) => f.endsWith(".log"));
9418
7850
  } catch (err) {
9419
7851
  const reason = err instanceof Error ? err.message : String(err);
9420
7852
  console.warn(`[admin/logs] readdir-fail dir=${dir} reason=${reason}`);
9421
7853
  continue;
9422
7854
  }
9423
- files.filter((f) => !seen.has(f)).map((f) => ({ name: f, mtime: statSync7(resolve14(dir, f)).mtimeMs })).sort((a, b) => b.mtime - a.mtime).forEach(({ name }) => {
7855
+ files.filter((f) => !seen.has(f)).map((f) => ({ name: f, mtime: statSync4(resolve10(dir, f)).mtimeMs })).sort((a, b) => b.mtime - a.mtime).forEach(({ name }) => {
9424
7856
  seen.add(name);
9425
7857
  try {
9426
- const content = readFileSync13(resolve14(dir, name));
7858
+ const content = readFileSync10(resolve10(dir, name));
9427
7859
  const tail = content.length > TAIL_BYTES ? content.subarray(content.length - TAIL_BYTES).toString("utf-8") : content.toString("utf-8");
9428
7860
  logs[name] = tail.trim() || "(empty)";
9429
7861
  } catch (err) {
@@ -9462,8 +7894,8 @@ var claude_info_default = app14;
9462
7894
 
9463
7895
  // server/routes/admin/attachment.ts
9464
7896
  import { readFile as readFile3, readdir } from "fs/promises";
9465
- import { existsSync as existsSync16 } from "fs";
9466
- import { resolve as resolve15 } from "path";
7897
+ import { existsSync as existsSync13 } from "fs";
7898
+ import { resolve as resolve11 } from "path";
9467
7899
  var app15 = new Hono();
9468
7900
  app15.get("/:attachmentId", requireAdminSession, async (c) => {
9469
7901
  const attachmentId = c.req.param("attachmentId");
@@ -9475,12 +7907,12 @@ app15.get("/:attachmentId", requireAdminSession, async (c) => {
9475
7907
  if (!/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/.test(attachmentId)) {
9476
7908
  return new Response("Not found", { status: 404 });
9477
7909
  }
9478
- const dir = resolve15(ATTACHMENTS_ROOT, accountId, attachmentId);
9479
- if (!existsSync16(dir)) {
7910
+ const dir = resolve11(ATTACHMENTS_ROOT, accountId, attachmentId);
7911
+ if (!existsSync13(dir)) {
9480
7912
  return new Response("Not found", { status: 404 });
9481
7913
  }
9482
- const metaPath = resolve15(dir, `${attachmentId}.meta.json`);
9483
- if (!existsSync16(metaPath)) {
7914
+ const metaPath = resolve11(dir, `${attachmentId}.meta.json`);
7915
+ if (!existsSync13(metaPath)) {
9484
7916
  return new Response("Not found", { status: 404 });
9485
7917
  }
9486
7918
  let meta;
@@ -9494,7 +7926,7 @@ app15.get("/:attachmentId", requireAdminSession, async (c) => {
9494
7926
  if (!dataFile) {
9495
7927
  return new Response("Not found", { status: 404 });
9496
7928
  }
9497
- const filePath = resolve15(dir, dataFile);
7929
+ const filePath = resolve11(dir, dataFile);
9498
7930
  const buffer = await readFile3(filePath);
9499
7931
  return new Response(new Uint8Array(buffer), {
9500
7932
  headers: {
@@ -9507,24 +7939,24 @@ app15.get("/:attachmentId", requireAdminSession, async (c) => {
9507
7939
  var attachment_default = app15;
9508
7940
 
9509
7941
  // server/routes/admin/agents.ts
9510
- import { resolve as resolve16 } from "path";
9511
- import { readdirSync as readdirSync5, readFileSync as readFileSync14, existsSync as existsSync17, rmSync } from "fs";
7942
+ import { resolve as resolve12 } from "path";
7943
+ import { readdirSync as readdirSync4, readFileSync as readFileSync11, existsSync as existsSync14, rmSync } from "fs";
9512
7944
  var app16 = new Hono();
9513
7945
  app16.get("/", (c) => {
9514
7946
  const account = resolveAccount();
9515
7947
  if (!account) return c.json({ agents: [] });
9516
- const agentsDir = resolve16(account.accountDir, "agents");
9517
- if (!existsSync17(agentsDir)) return c.json({ agents: [] });
7948
+ const agentsDir = resolve12(account.accountDir, "agents");
7949
+ if (!existsSync14(agentsDir)) return c.json({ agents: [] });
9518
7950
  const agents = [];
9519
7951
  try {
9520
- const entries = readdirSync5(agentsDir, { withFileTypes: true });
7952
+ const entries = readdirSync4(agentsDir, { withFileTypes: true });
9521
7953
  for (const entry of entries.sort((a, b) => a.name.localeCompare(b.name))) {
9522
7954
  if (!entry.isDirectory()) continue;
9523
7955
  if (entry.name === "admin") continue;
9524
- const configPath2 = resolve16(agentsDir, entry.name, "config.json");
9525
- if (!existsSync17(configPath2)) continue;
7956
+ const configPath2 = resolve12(agentsDir, entry.name, "config.json");
7957
+ if (!existsSync14(configPath2)) continue;
9526
7958
  try {
9527
- const config = JSON.parse(readFileSync14(configPath2, "utf-8"));
7959
+ const config = JSON.parse(readFileSync11(configPath2, "utf-8"));
9528
7960
  agents.push({
9529
7961
  slug: entry.name,
9530
7962
  displayName: config.displayName ?? entry.name,
@@ -9550,8 +7982,8 @@ app16.delete("/:slug", async (c) => {
9550
7982
  if (slug.includes("/") || slug.includes("..") || slug.includes("\\")) {
9551
7983
  return c.json({ error: "Invalid agent slug" }, 400);
9552
7984
  }
9553
- const agentDir = resolve16(account.accountDir, "agents", slug);
9554
- if (!existsSync17(agentDir)) {
7985
+ const agentDir = resolve12(account.accountDir, "agents", slug);
7986
+ if (!existsSync14(agentDir)) {
9555
7987
  return c.json({ error: "Agent not found" }, 404);
9556
7988
  }
9557
7989
  try {
@@ -9580,8 +8012,8 @@ app16.post("/:slug/project", async (c) => {
9580
8012
  if (slug.includes("/") || slug.includes("..") || slug.includes("\\")) {
9581
8013
  return c.json({ error: "Invalid agent slug" }, 400);
9582
8014
  }
9583
- const agentDir = resolve16(account.accountDir, "agents", slug);
9584
- if (!existsSync17(agentDir)) {
8015
+ const agentDir = resolve12(account.accountDir, "agents", slug);
8016
+ if (!existsSync14(agentDir)) {
9585
8017
  return c.json({ error: "Agent not found on disk" }, 404);
9586
8018
  }
9587
8019
  try {
@@ -9597,7 +8029,7 @@ var agents_default = app16;
9597
8029
  // server/routes/admin/sessions.ts
9598
8030
  import crypto2 from "crypto";
9599
8031
  import { resolve as resolvePath } from "path";
9600
- import { appendFileSync as appendFileSync5, existsSync as existsSync18 } from "fs";
8032
+ import { appendFileSync as appendFileSync4, existsSync as existsSync15 } from "fs";
9601
8033
  function validateAndShapeAttachments(raws, conversationAccountId, conversationId, messageId, streamLogPath) {
9602
8034
  const chips = [];
9603
8035
  let valid = 0;
@@ -9606,11 +8038,11 @@ function validateAndShapeAttachments(raws, conversationAccountId, conversationId
9606
8038
  let reason = null;
9607
8039
  if (!a.attachmentId || !a.filename || !a.mimeType || !a.storagePath) reason = "schema-fail";
9608
8040
  else if (a.accountId !== conversationAccountId) reason = "account-mismatch";
9609
- else if (!existsSync18(a.storagePath)) reason = "missing-file";
8041
+ else if (!existsSync15(a.storagePath)) reason = "missing-file";
9610
8042
  if (reason) {
9611
8043
  invalid++;
9612
8044
  try {
9613
- appendFileSync5(streamLogPath, `[${(/* @__PURE__ */ new Date()).toISOString()}] [attachment-rehydrate-invalid] conversationId=${conversationId.slice(0, 8)} messageId=${messageId.slice(0, 8)} attachmentId=${(a.attachmentId || "").slice(0, 8)} reason=${reason}
8045
+ appendFileSync4(streamLogPath, `[${(/* @__PURE__ */ new Date()).toISOString()}] [attachment-rehydrate-invalid] conversationId=${conversationId.slice(0, 8)} messageId=${messageId.slice(0, 8)} attachmentId=${(a.attachmentId || "").slice(0, 8)} reason=${reason}
9614
8046
  `);
9615
8047
  } catch {
9616
8048
  }
@@ -9673,7 +8105,7 @@ function reconstructAssistantEvents(content, components, conversationId, message
9673
8105
  const line = `[${(/* @__PURE__ */ new Date()).toISOString()}] [component-rehydrate-invalid] conversationId=${conversationId.slice(0, 8)} messageId=${messageId.slice(0, 8)} name=${c.name || "<unnamed>"} reason=${invalidReason}
9674
8106
  `;
9675
8107
  try {
9676
- appendFileSync5(streamLogPath, line);
8108
+ appendFileSync4(streamLogPath, line);
9677
8109
  } catch {
9678
8110
  }
9679
8111
  continue;
@@ -9876,14 +8308,14 @@ app17.post("/:id/resume", async (c) => {
9876
8308
  const userMessageCount = rehydrated.filter((m) => m.role !== "assistant").length;
9877
8309
  const reason = bridged ? "post-restart" : "page-refresh";
9878
8310
  try {
9879
- appendFileSync5(streamLogPath, `[${(/* @__PURE__ */ new Date()).toISOString()}] [admin-resume] reason=${reason} sessionKey=${sessionKey.slice(0, 8)} conversationId=${conversationId.slice(0, 8)} ${tag} loadedMessages=${messages.length} componentCount=${totalComponents} userAttachmentCount=${totalAttachments}
8311
+ appendFileSync4(streamLogPath, `[${(/* @__PURE__ */ new Date()).toISOString()}] [admin-resume] reason=${reason} sessionKey=${sessionKey.slice(0, 8)} conversationId=${conversationId.slice(0, 8)} ${tag} loadedMessages=${messages.length} componentCount=${totalComponents} userAttachmentCount=${totalAttachments}
9880
8312
  `);
9881
8313
  if (totalComponents > 0) {
9882
- appendFileSync5(streamLogPath, `[${(/* @__PURE__ */ new Date()).toISOString()}] [component-rehydrate] conversationId=${conversationId.slice(0, 8)} count=${totalComponents} valid=${totalValid} invalid=${totalInvalid} textRuns=${textRuns}
8314
+ appendFileSync4(streamLogPath, `[${(/* @__PURE__ */ new Date()).toISOString()}] [component-rehydrate] conversationId=${conversationId.slice(0, 8)} count=${totalComponents} valid=${totalValid} invalid=${totalInvalid} textRuns=${textRuns}
9883
8315
  `);
9884
8316
  }
9885
8317
  if (totalAttachments > 0 || totalAttachmentInvalid > 0) {
9886
- appendFileSync5(streamLogPath, `[${(/* @__PURE__ */ new Date()).toISOString()}] [attachment-rehydrate] conversationId=${conversationId.slice(0, 8)} userMessages=${userMessageCount} attachments=${totalAttachments} invalid=${totalAttachmentInvalid}
8318
+ appendFileSync4(streamLogPath, `[${(/* @__PURE__ */ new Date()).toISOString()}] [attachment-rehydrate] conversationId=${conversationId.slice(0, 8)} userMessages=${userMessageCount} attachments=${totalAttachments} invalid=${totalAttachmentInvalid}
9887
8319
  `);
9888
8320
  }
9889
8321
  } catch {
@@ -10147,8 +8579,8 @@ var events_default = app20;
10147
8579
 
10148
8580
  // server/routes/admin/cloudflare.ts
10149
8581
  import { homedir } from "os";
10150
- import { resolve as resolve18 } from "path";
10151
- import { readFileSync as readFileSync16 } from "fs";
8582
+ import { resolve as resolve14 } from "path";
8583
+ import { readFileSync as readFileSync13 } from "fs";
10152
8584
 
10153
8585
  // app/lib/dns-label.ts
10154
8586
  var VALID_LABEL = /^[a-z0-9]([a-z0-9-]{0,61}[a-z0-9])?$/;
@@ -10164,14 +8596,14 @@ function isValidDomain(value) {
10164
8596
  }
10165
8597
 
10166
8598
  // app/lib/alias-domains.ts
10167
- import { existsSync as existsSync19, mkdirSync as mkdirSync8, readFileSync as readFileSync15, writeFileSync as writeFileSync9 } from "fs";
10168
- import { dirname as dirname7 } from "path";
10169
- import { resolve as resolve17 } from "path";
10170
- var ALIAS_DOMAINS_PATH = resolve17(MAXY_DIR, "alias-domains.json");
8599
+ import { existsSync as existsSync16, mkdirSync as mkdirSync5, readFileSync as readFileSync12, writeFileSync as writeFileSync6 } from "fs";
8600
+ import { dirname as dirname4 } from "path";
8601
+ import { resolve as resolve13 } from "path";
8602
+ var ALIAS_DOMAINS_PATH = resolve13(MAXY_DIR, "alias-domains.json");
10171
8603
  function readExisting() {
10172
- if (!existsSync19(ALIAS_DOMAINS_PATH)) return /* @__PURE__ */ new Set();
8604
+ if (!existsSync16(ALIAS_DOMAINS_PATH)) return /* @__PURE__ */ new Set();
10173
8605
  try {
10174
- const parsed = JSON.parse(readFileSync15(ALIAS_DOMAINS_PATH, "utf-8"));
8606
+ const parsed = JSON.parse(readFileSync12(ALIAS_DOMAINS_PATH, "utf-8"));
10175
8607
  if (!Array.isArray(parsed)) return /* @__PURE__ */ new Set();
10176
8608
  return new Set(parsed.filter((h) => typeof h === "string"));
10177
8609
  } catch {
@@ -10182,18 +8614,18 @@ function addAliasDomain(hostname2) {
10182
8614
  const existing = readExisting();
10183
8615
  if (existing.has(hostname2)) return;
10184
8616
  existing.add(hostname2);
10185
- mkdirSync8(dirname7(ALIAS_DOMAINS_PATH), { recursive: true });
10186
- writeFileSync9(ALIAS_DOMAINS_PATH, JSON.stringify([...existing], null, 2) + "\n", "utf-8");
8617
+ mkdirSync5(dirname4(ALIAS_DOMAINS_PATH), { recursive: true });
8618
+ writeFileSync6(ALIAS_DOMAINS_PATH, JSON.stringify([...existing], null, 2) + "\n", "utf-8");
10187
8619
  }
10188
8620
 
10189
8621
  // server/routes/admin/cloudflare.ts
10190
8622
  var SETUP_TIMEOUT_MS = 10 * 60 * 1e3;
10191
8623
  var DOMAINS_TIMEOUT_MS = 40 * 1e3;
10192
8624
  function loadBrandInfo() {
10193
- const platformRoot2 = process.env.MAXY_PLATFORM_ROOT ?? resolve18(process.cwd(), "..");
10194
- const brandPath = resolve18(platformRoot2, "config", "brand.json");
8625
+ const platformRoot2 = process.env.MAXY_PLATFORM_ROOT ?? resolve14(process.cwd(), "..");
8626
+ const brandPath = resolve14(platformRoot2, "config", "brand.json");
10195
8627
  try {
10196
- const parsed = JSON.parse(readFileSync16(brandPath, "utf-8"));
8628
+ const parsed = JSON.parse(readFileSync13(brandPath, "utf-8"));
10197
8629
  const hostname2 = typeof parsed.hostname === "string" && parsed.hostname ? parsed.hostname : "maxy";
10198
8630
  const configDir2 = typeof parsed.configDir === "string" && parsed.configDir ? parsed.configDir : ".maxy";
10199
8631
  return { hostname: hostname2, configDir: configDir2 };
@@ -10296,7 +8728,7 @@ app21.get("/domains", requireAdminSession, async (c) => {
10296
8728
  streamLogPath = streamLogPathFor(accountId, correlationId).streamLogPath;
10297
8729
  log(`phase=stream-log-resolved path=${streamLogPath}`);
10298
8730
  const brand = loadBrandInfo();
10299
- const scriptPath = resolve18(homedir(), "list-cf-domains.sh");
8731
+ const scriptPath = resolve14(homedir(), "list-cf-domains.sh");
10300
8732
  const result = await runFormSpawn({
10301
8733
  scriptPath,
10302
8734
  args: [brand.hostname],
@@ -10495,17 +8927,17 @@ var cloudflare_default = app21;
10495
8927
  import { createReadStream as createReadStream3 } from "fs";
10496
8928
  import { readdir as readdir2, readFile as readFile4, stat as stat4, mkdir as mkdir3, writeFile as writeFile4, unlink as unlink2 } from "fs/promises";
10497
8929
  import { realpathSync as realpathSync4 } from "fs";
10498
- import { basename as basename6, dirname as dirname8, join as join9, resolve as resolve20, sep as sep2 } from "path";
8930
+ import { basename as basename4, dirname as dirname5, join as join8, resolve as resolve16, sep as sep2 } from "path";
10499
8931
  import { Readable as Readable2 } from "stream";
10500
8932
 
10501
8933
  // app/lib/data-path.ts
10502
8934
  import { realpathSync as realpathSync3 } from "fs";
10503
- import { resolve as resolve19, normalize, sep, relative } from "path";
10504
- var PLATFORM_ROOT6 = process.env.MAXY_PLATFORM_ROOT ?? resolve19(process.cwd(), "../platform");
10505
- var DATA_ROOT = resolve19(PLATFORM_ROOT6, "..", "data");
8935
+ import { resolve as resolve15, normalize, sep, relative } from "path";
8936
+ var PLATFORM_ROOT6 = process.env.MAXY_PLATFORM_ROOT ?? resolve15(process.cwd(), "../platform");
8937
+ var DATA_ROOT = resolve15(PLATFORM_ROOT6, "..", "data");
10506
8938
  function resolveDataPath(raw) {
10507
8939
  const cleaned = normalize("/" + (raw ?? "").replace(/\\/g, "/")).replace(/^\/+/, "");
10508
- const absolute = resolve19(DATA_ROOT, cleaned);
8940
+ const absolute = resolve15(DATA_ROOT, cleaned);
10509
8941
  let dataRootReal;
10510
8942
  try {
10511
8943
  dataRootReal = realpathSync3(DATA_ROOT);
@@ -10853,7 +9285,7 @@ async function cascadeDeleteDocument(params) {
10853
9285
  var UUID_RE4 = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
10854
9286
  async function readMeta(absDir, baseName) {
10855
9287
  try {
10856
- const raw = await readFile4(join9(absDir, `${baseName}.meta.json`), "utf8");
9288
+ const raw = await readFile4(join8(absDir, `${baseName}.meta.json`), "utf8");
10857
9289
  const parsed = JSON.parse(raw);
10858
9290
  if (typeof parsed?.filename === "string") {
10859
9291
  return { filename: parsed.filename, mimeType: typeof parsed.mimeType === "string" ? parsed.mimeType : void 0 };
@@ -10864,7 +9296,7 @@ async function readMeta(absDir, baseName) {
10864
9296
  }
10865
9297
  async function readAccountNames() {
10866
9298
  const map = /* @__PURE__ */ new Map();
10867
- const accountsDir = resolve20(DATA_ROOT, "accounts");
9299
+ const accountsDir = resolve16(DATA_ROOT, "accounts");
10868
9300
  let names;
10869
9301
  try {
10870
9302
  names = await readdir2(accountsDir);
@@ -10873,7 +9305,7 @@ async function readAccountNames() {
10873
9305
  }
10874
9306
  for (const name of names) {
10875
9307
  if (!UUID_RE4.test(name)) continue;
10876
- const configPath2 = resolve20(accountsDir, name, "account.json");
9308
+ const configPath2 = resolve16(accountsDir, name, "account.json");
10877
9309
  try {
10878
9310
  const raw = await readFile4(configPath2, "utf8");
10879
9311
  const parsed = JSON.parse(raw);
@@ -10891,7 +9323,7 @@ async function readAccountNames() {
10891
9323
  }
10892
9324
  async function enrich(absolute, entry, accountNames) {
10893
9325
  if (entry.kind === "directory" && UUID_RE4.test(entry.name)) {
10894
- const meta = await readMeta(join9(absolute, entry.name), entry.name);
9326
+ const meta = await readMeta(join8(absolute, entry.name), entry.name);
10895
9327
  if (meta?.filename) {
10896
9328
  entry.displayName = meta.filename;
10897
9329
  entry.mimeType = meta.mimeType;
@@ -10950,7 +9382,7 @@ app22.get("/", requireAdminSession, async (c) => {
10950
9382
  continue;
10951
9383
  }
10952
9384
  try {
10953
- const entryPath = join9(absolute, name);
9385
+ const entryPath = join8(absolute, name);
10954
9386
  const s = await stat4(entryPath);
10955
9387
  entries.push({
10956
9388
  name,
@@ -11005,7 +9437,7 @@ app22.get("/download", requireAdminSession, async (c) => {
11005
9437
  if (!info.isFile()) {
11006
9438
  return c.json({ error: "Path is not a file" }, 400);
11007
9439
  }
11008
- const filename = basename6(absolute);
9440
+ const filename = basename4(absolute);
11009
9441
  const mimeType = detectMimeType(absolute);
11010
9442
  const nodeStream = createReadStream3(absolute);
11011
9443
  const webStream = Readable2.toWeb(nodeStream);
@@ -11062,10 +9494,10 @@ app22.post("/upload", requireAdminSession, async (c) => {
11062
9494
  error: `Unsupported file type: "${file.type}". Supported: ${[...SUPPORTED_MIME_TYPES].join(", ")}.`
11063
9495
  }, 422);
11064
9496
  }
11065
- const safeName = basename6(file.name).replace(/[\0/\\]/g, "_");
9497
+ const safeName = basename4(file.name).replace(/[\0/\\]/g, "_");
11066
9498
  const finalName = `${Date.now()}-${safeName}`;
11067
- const destDir = resolve20(DATA_ROOT, "uploads", accountId);
11068
- const destPath = resolve20(destDir, finalName);
9499
+ const destDir = resolve16(DATA_ROOT, "uploads", accountId);
9500
+ const destPath = resolve16(destDir, finalName);
11069
9501
  try {
11070
9502
  await mkdir3(destDir, { recursive: true });
11071
9503
  const dataRootReal = realpathSync4(DATA_ROOT);
@@ -11107,7 +9539,7 @@ app22.delete("/", requireAdminSession, async (c) => {
11107
9539
  return c.json({ error: resolution.error }, resolution.status);
11108
9540
  }
11109
9541
  const { absolute, relative: relPath2 } = resolution;
11110
- const base = basename6(absolute);
9542
+ const base = basename4(absolute);
11111
9543
  const segments = relPath2.split("/").filter(Boolean);
11112
9544
  if (base === "account.json" || segments.includes(".git")) {
11113
9545
  console.error(`[data] file-delete blocked path="${relPath2}" reason="protected"`);
@@ -11123,7 +9555,7 @@ app22.delete("/", requireAdminSession, async (c) => {
11123
9555
  }
11124
9556
  const dot = base.lastIndexOf(".");
11125
9557
  const stem = dot === -1 ? base : base.slice(0, dot);
11126
- const sidecarPath = UUID_RE4.test(stem) && base !== `${stem}.meta.json` ? join9(dirname8(absolute), `${stem}.meta.json`) : null;
9558
+ const sidecarPath = UUID_RE4.test(stem) && base !== `${stem}.meta.json` ? join8(dirname5(absolute), `${stem}.meta.json`) : null;
11127
9559
  await unlink2(absolute);
11128
9560
  if (sidecarPath) {
11129
9561
  try {
@@ -11674,11 +10106,6 @@ var GRAPH_LABEL_COLOURS = {
11674
10106
  // confusion doesn't apply, but kept distinguishable for the legend)
11675
10107
  Email: "#6F7F4A",
11676
10108
  EmailAccount: "#91A063",
11677
- // Review signals (Task 626 — previously written by review-detector but
11678
- // unregistered here, producing an `unknown label` 400 whenever the
11679
- // filter popover advertised the label. Muted brick — still reads as
11680
- // alert against the cream background but doesn't shout fire-engine red.)
11681
- ReviewAlert: "#A85C5C",
11682
10109
  // Public-agent projection (Task 837) — burnished bronze sits between
11683
10110
  // people-terracotta and email-moss but shares neither hue, signalling
11684
10111
  // "operator-defined persona that traverses to KnowledgeDocuments,
@@ -12603,8 +11030,8 @@ var adherence_default = app30;
12603
11030
  // server/routes/admin/sidebar-artefacts.ts
12604
11031
  import neo4j3 from "neo4j-driver";
12605
11032
  import { readFile as readFile5, readdir as readdir3, stat as stat5 } from "fs/promises";
12606
- import { resolve as resolve21, relative as relative2, isAbsolute } from "path";
12607
- import { existsSync as existsSync20 } from "fs";
11033
+ import { resolve as resolve17, relative as relative2, isAbsolute } from "path";
11034
+ import { existsSync as existsSync17 } from "fs";
12608
11035
  var LIMIT = 50;
12609
11036
  var TEXT_MIME_PREFIXES = ["text/", "application/json", "application/markdown"];
12610
11037
  var ADMIN_AGENT_FILES = ["IDENTITY.md", "SOUL.md", "KNOWLEDGE.md"];
@@ -12620,7 +11047,7 @@ app31.get("/", requireAdminSession, async (c) => {
12620
11047
  if (docs === null) {
12621
11048
  return c.json({ error: "Failed to load artefacts" }, 500);
12622
11049
  }
12623
- const accountDir = resolve21(ACCOUNTS_DIR, accountId);
11050
+ const accountDir = resolve17(ACCOUNTS_DIR, accountId);
12624
11051
  const agents = await fetchAgentTemplateRows(accountDir);
12625
11052
  const artefacts = [...docs, ...agents].sort(
12626
11053
  (a, b) => (b.updatedAt ?? "").localeCompare(a.updatedAt ?? "")
@@ -12683,8 +11110,8 @@ async function readArtefactContent(accountId, attachmentId, mimeType, displayNam
12683
11110
  logSkip(displayName, "non-text-mime", mimeType);
12684
11111
  return { content: "", skipReason: "non-text-mime" };
12685
11112
  }
12686
- const accountDir = resolve21(ATTACHMENTS_ROOT, accountId);
12687
- const dir = resolve21(accountDir, attachmentId);
11113
+ const accountDir = resolve17(ATTACHMENTS_ROOT, accountId);
11114
+ const dir = resolve17(accountDir, attachmentId);
12688
11115
  try {
12689
11116
  validateFilePathInAccount(dir, accountDir);
12690
11117
  } catch {
@@ -12698,7 +11125,7 @@ async function readArtefactContent(accountId, attachmentId, mimeType, displayNam
12698
11125
  logSkip(displayName, "missing-on-disk", mimeType);
12699
11126
  return { content: "", skipReason: "missing-on-disk" };
12700
11127
  }
12701
- return { content: await readFile5(resolve21(dir, dataFile), "utf-8"), skipReason: null };
11128
+ return { content: await readFile5(resolve17(dir, dataFile), "utf-8"), skipReason: null };
12702
11129
  } catch (err) {
12703
11130
  const message = err instanceof Error ? err.message : String(err);
12704
11131
  console.error(`[admin/sidebar-artefacts] read-failed attachmentId=${attachmentId.slice(0, 8)} error="${message}"`);
@@ -12714,8 +11141,8 @@ function logSkip(name, reason, mimeType) {
12714
11141
  async function fetchAgentTemplateRows(accountDir) {
12715
11142
  const rows = [];
12716
11143
  for (const filename of ADMIN_AGENT_FILES) {
12717
- const overridePath = resolve21(accountDir, "agents", "admin", filename);
12718
- const bundledPath = resolve21(PLATFORM_ROOT, "templates", "agents", "admin", filename);
11144
+ const overridePath = resolve17(accountDir, "agents", "admin", filename);
11145
+ const bundledPath = resolve17(PLATFORM_ROOT, "templates", "agents", "admin", filename);
12719
11146
  const labelStem = filename.replace(/\.md$/, "");
12720
11147
  const row = await readAgentTemplateRow({
12721
11148
  id: `agent-template:admin:${filename}`,
@@ -12729,12 +11156,12 @@ async function fetchAgentTemplateRows(accountDir) {
12729
11156
  });
12730
11157
  if (row) rows.push(row);
12731
11158
  }
12732
- const overrideDir = resolve21(accountDir, "specialists", "agents");
12733
- const bundledDir = resolve21(PLATFORM_ROOT, "templates", "specialists", "agents");
11159
+ const overrideDir = resolve17(accountDir, "specialists", "agents");
11160
+ const bundledDir = resolve17(PLATFORM_ROOT, "templates", "specialists", "agents");
12734
11161
  const specialistNames = await unionSpecialistFilenames(overrideDir, bundledDir);
12735
11162
  for (const filename of specialistNames) {
12736
- const overridePath = resolve21(overrideDir, filename);
12737
- const bundledPath = resolve21(bundledDir, filename);
11163
+ const overridePath = resolve17(overrideDir, filename);
11164
+ const bundledPath = resolve17(bundledDir, filename);
12738
11165
  const row = await readAgentTemplateRow({
12739
11166
  id: `agent-template:specialist:${filename}`,
12740
11167
  displayName: filename.replace(/\.md$/, ""),
@@ -12752,7 +11179,7 @@ async function fetchAgentTemplateRows(accountDir) {
12752
11179
  async function unionSpecialistFilenames(overrideDir, bundledDir) {
12753
11180
  const names = /* @__PURE__ */ new Set();
12754
11181
  for (const dir of [overrideDir, bundledDir]) {
12755
- if (!existsSync20(dir)) continue;
11182
+ if (!existsSync17(dir)) continue;
12756
11183
  try {
12757
11184
  const entries = await readdir3(dir);
12758
11185
  for (const entry of entries) {
@@ -12767,7 +11194,7 @@ async function unionSpecialistFilenames(overrideDir, bundledDir) {
12767
11194
  }
12768
11195
  async function readAgentTemplateRow(inp) {
12769
11196
  let chosenPath = null;
12770
- if (existsSync20(inp.overridePath)) {
11197
+ if (existsSync17(inp.overridePath)) {
12771
11198
  try {
12772
11199
  validateFilePathInAccount(inp.overridePath, inp.overrideRoot);
12773
11200
  chosenPath = inp.overridePath;
@@ -12778,7 +11205,7 @@ async function readAgentTemplateRow(inp) {
12778
11205
  );
12779
11206
  return null;
12780
11207
  }
12781
- } else if (existsSync20(inp.bundledPath)) {
11208
+ } else if (existsSync17(inp.bundledPath)) {
12782
11209
  if (!isWithin(inp.bundledPath, inp.bundledRoot)) {
12783
11210
  console.error(
12784
11211
  `[admin/sidebar-artefacts] agent-template-read-failed agent=${inp.displayName} kind=${inp.logName} error="bundled path outside PLATFORM_ROOT"`
@@ -12819,8 +11246,8 @@ var sidebar_artefacts_default = app31;
12819
11246
 
12820
11247
  // server/routes/admin/sidebar-artefact-save.ts
12821
11248
  import { mkdir as mkdir4, readdir as readdir4, stat as stat6, writeFile as writeFile5 } from "fs/promises";
12822
- import { resolve as resolve22 } from "path";
12823
- import { existsSync as existsSync21 } from "fs";
11249
+ import { resolve as resolve18 } from "path";
11250
+ import { existsSync as existsSync18 } from "fs";
12824
11251
  var ADMIN_AGENT_FILES2 = /* @__PURE__ */ new Set(["IDENTITY.md", "SOUL.md", "KNOWLEDGE.md"]);
12825
11252
  var UUID_RE5 = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/;
12826
11253
  var app32 = new Hono();
@@ -12832,7 +11259,7 @@ app32.post("/", requireAdminSession, async (c) => {
12832
11259
  if (!body || typeof body.id !== "string" || typeof body.content !== "string") {
12833
11260
  return c.json({ error: "id and content required" }, 400);
12834
11261
  }
12835
- const accountDir = resolve22(ACCOUNTS_DIR, accountId);
11262
+ const accountDir = resolve18(ACCOUNTS_DIR, accountId);
12836
11263
  const resolved = await resolveSavePath(body.id, accountId, accountDir);
12837
11264
  if (resolved.kind === "reject") {
12838
11265
  console.error(
@@ -12873,22 +11300,22 @@ async function resolveSavePath(id, accountId, accountDir) {
12873
11300
  if (role !== "admin" || !ADMIN_AGENT_FILES2.has(filename)) {
12874
11301
  return { kind: "reject", status: 400, reason: "invalid-id" };
12875
11302
  }
12876
- const parent = resolve22(accountDir, "agents", "admin");
11303
+ const parent = resolve18(accountDir, "agents", "admin");
12877
11304
  await mkdir4(parent, { recursive: true });
12878
11305
  try {
12879
11306
  validateFilePathInAccount(parent, accountDir);
12880
11307
  } catch {
12881
11308
  return { kind: "reject", status: 400, reason: "containment-rejected" };
12882
11309
  }
12883
- return { kind: "admin-template", path: resolve22(parent, filename) };
11310
+ return { kind: "admin-template", path: resolve18(parent, filename) };
12884
11311
  }
12885
11312
  if (UUID_RE5.test(id)) {
12886
- const dir = resolve22(ATTACHMENTS_ROOT, accountId, id);
12887
- if (!existsSync21(dir)) {
11313
+ const dir = resolve18(ATTACHMENTS_ROOT, accountId, id);
11314
+ if (!existsSync18(dir)) {
12888
11315
  return { kind: "reject", status: 400, reason: "not-found" };
12889
11316
  }
12890
11317
  try {
12891
- validateFilePathInAccount(dir, resolve22(ATTACHMENTS_ROOT, accountId));
11318
+ validateFilePathInAccount(dir, resolve18(ATTACHMENTS_ROOT, accountId));
12892
11319
  } catch {
12893
11320
  return { kind: "reject", status: 400, reason: "containment-rejected" };
12894
11321
  }
@@ -12897,7 +11324,7 @@ async function resolveSavePath(id, accountId, accountDir) {
12897
11324
  if (!dataFile) {
12898
11325
  return { kind: "reject", status: 400, reason: "not-found" };
12899
11326
  }
12900
- return { kind: "knowledge-doc", path: resolve22(dir, dataFile) };
11327
+ return { kind: "knowledge-doc", path: resolve18(dir, dataFile) };
12901
11328
  }
12902
11329
  return { kind: "reject", status: 400, reason: "invalid-id" };
12903
11330
  }
@@ -12908,8 +11335,8 @@ var sidebar_artefact_save_default = app32;
12908
11335
 
12909
11336
  // server/routes/admin/sidebar-artefact-content.ts
12910
11337
  import { readFile as readFile6, readdir as readdir5 } from "fs/promises";
12911
- import { existsSync as existsSync22 } from "fs";
12912
- import { resolve as resolve23 } from "path";
11338
+ import { existsSync as existsSync19 } from "fs";
11339
+ import { resolve as resolve19 } from "path";
12913
11340
  var UUID_RE6 = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/;
12914
11341
  var app33 = new Hono();
12915
11342
  app33.get("/", requireAdminSession, async (c) => {
@@ -12921,14 +11348,14 @@ app33.get("/", requireAdminSession, async (c) => {
12921
11348
  console.error(`[admin/sidebar-artefact-content] not-found id=${id.slice(0, 8)}`);
12922
11349
  return new Response("Not found", { status: 404 });
12923
11350
  }
12924
- const dir = resolve23(ATTACHMENTS_ROOT, accountId, id);
12925
- if (!existsSync22(dir)) {
11351
+ const dir = resolve19(ATTACHMENTS_ROOT, accountId, id);
11352
+ if (!existsSync19(dir)) {
12926
11353
  console.error(`[admin/sidebar-artefact-content] not-found id=${id.slice(0, 8)}`);
12927
11354
  return new Response("Not found", { status: 404 });
12928
11355
  }
12929
11356
  let meta;
12930
11357
  try {
12931
- meta = JSON.parse(await readFile6(resolve23(dir, `${id}.meta.json`), "utf-8"));
11358
+ meta = JSON.parse(await readFile6(resolve19(dir, `${id}.meta.json`), "utf-8"));
12932
11359
  } catch {
12933
11360
  console.error(`[admin/sidebar-artefact-content] not-found id=${id.slice(0, 8)}`);
12934
11361
  return new Response("Not found", { status: 404 });
@@ -12940,7 +11367,7 @@ app33.get("/", requireAdminSession, async (c) => {
12940
11367
  return new Response("Not found", { status: 404 });
12941
11368
  }
12942
11369
  const start = Date.now();
12943
- const buffer = await readFile6(resolve23(dir, dataFile));
11370
+ const buffer = await readFile6(resolve19(dir, dataFile));
12944
11371
  const ms = Date.now() - start;
12945
11372
  console.log(
12946
11373
  `[admin/sidebar-artefact-content] account=${accountId} id=${id.slice(0, 8)} mime=${meta.mimeType} bytes=${buffer.length} ms=${ms}`
@@ -12984,8 +11411,8 @@ app34.route("/sidebar-artefact-content", sidebar_artefact_content_default);
12984
11411
  var admin_default = app34;
12985
11412
 
12986
11413
  // server/routes/sites.ts
12987
- import { existsSync as existsSync23, readFileSync as readFileSync17, realpathSync as realpathSync5, statSync as statSync8 } from "fs";
12988
- import { resolve as resolve24 } from "path";
11414
+ import { existsSync as existsSync20, readFileSync as readFileSync14, realpathSync as realpathSync5, statSync as statSync5 } from "fs";
11415
+ import { resolve as resolve20 } from "path";
12989
11416
  var SAFE_SEG_RE = /^[a-z0-9_][a-z0-9_.-]{0,99}$/i;
12990
11417
  var MIME = {
12991
11418
  ".html": "text/html; charset=utf-8",
@@ -13043,28 +11470,28 @@ app35.get("/:rel{.*}", (c) => {
13043
11470
  }
13044
11471
  segments.push(seg);
13045
11472
  }
13046
- const rootDir = resolve24(account.accountDir, "sites");
13047
- let filePath = segments.length === 0 ? rootDir : resolve24(rootDir, ...segments);
11473
+ const rootDir = resolve20(account.accountDir, "sites");
11474
+ let filePath = segments.length === 0 ? rootDir : resolve20(rootDir, ...segments);
13048
11475
  if (filePath !== rootDir && !filePath.startsWith(rootDir + "/")) {
13049
11476
  console.error(`[sites] path-traversal-rejected path=${reqPath} reason=escape status=403`);
13050
11477
  return c.text("Forbidden", 403);
13051
11478
  }
13052
11479
  let stat7;
13053
11480
  try {
13054
- stat7 = existsSync23(filePath) ? statSync8(filePath) : null;
11481
+ stat7 = existsSync20(filePath) ? statSync5(filePath) : null;
13055
11482
  } catch {
13056
11483
  stat7 = null;
13057
11484
  }
13058
11485
  if (stat7?.isDirectory()) {
13059
- filePath = resolve24(filePath, "index.html");
11486
+ filePath = resolve20(filePath, "index.html");
13060
11487
  } else if (stat7 === null && isDirRequest) {
13061
- filePath = resolve24(filePath, "index.html");
11488
+ filePath = resolve20(filePath, "index.html");
13062
11489
  }
13063
11490
  if (!filePath.startsWith(rootDir + "/")) {
13064
11491
  console.error(`[sites] path-traversal-rejected path=${reqPath} reason=escape status=403`);
13065
11492
  return c.text("Forbidden", 403);
13066
11493
  }
13067
- if (!existsSync23(filePath)) {
11494
+ if (!existsSync20(filePath)) {
13068
11495
  console.error(`[sites] not-found path=${reqPath} status=404`);
13069
11496
  return c.text("Not found", 404);
13070
11497
  }
@@ -13083,7 +11510,7 @@ app35.get("/:rel{.*}", (c) => {
13083
11510
  }
13084
11511
  let body;
13085
11512
  try {
13086
- body = readFileSync17(realPath);
11513
+ body = readFileSync14(realPath);
13087
11514
  } catch (err) {
13088
11515
  const code = err?.code;
13089
11516
  if (code === "EISDIR") {
@@ -13215,14 +11642,14 @@ function clientFrom(c) {
13215
11642
  );
13216
11643
  }
13217
11644
  var PLATFORM_ROOT7 = process.env.MAXY_PLATFORM_ROOT || "";
13218
- var BRAND_JSON_PATH = PLATFORM_ROOT7 ? join10(PLATFORM_ROOT7, "config", "brand.json") : "";
11645
+ var BRAND_JSON_PATH = PLATFORM_ROOT7 ? join9(PLATFORM_ROOT7, "config", "brand.json") : "";
13219
11646
  var BRAND = { productName: "Maxy", hostname: "maxy", configDir: ".maxy", domain: "getmaxy.com" };
13220
- if (BRAND_JSON_PATH && !existsSync24(BRAND_JSON_PATH)) {
11647
+ if (BRAND_JSON_PATH && !existsSync21(BRAND_JSON_PATH)) {
13221
11648
  console.error(`[brand] WARNING: brand.json not found at ${BRAND_JSON_PATH} \u2014 using Maxy defaults`);
13222
11649
  }
13223
- if (BRAND_JSON_PATH && existsSync24(BRAND_JSON_PATH)) {
11650
+ if (BRAND_JSON_PATH && existsSync21(BRAND_JSON_PATH)) {
13224
11651
  try {
13225
- const parsed = JSON.parse(readFileSync18(BRAND_JSON_PATH, "utf-8"));
11652
+ const parsed = JSON.parse(readFileSync15(BRAND_JSON_PATH, "utf-8"));
13226
11653
  BRAND = { ...BRAND, ...parsed };
13227
11654
  } catch (err) {
13228
11655
  console.error(`[brand] Failed to parse brand.json: ${err.message}`);
@@ -13241,11 +11668,11 @@ var brandLoginOpts = {
13241
11668
  bodyFont: BRAND.defaultFonts?.body,
13242
11669
  logoContainsName: !!BRAND.logoContainsName
13243
11670
  };
13244
- var ALIAS_DOMAINS_PATH2 = join10(homedir2(), BRAND.configDir, "alias-domains.json");
11671
+ var ALIAS_DOMAINS_PATH2 = join9(homedir2(), BRAND.configDir, "alias-domains.json");
13245
11672
  function loadAliasDomains() {
13246
11673
  try {
13247
- if (!existsSync24(ALIAS_DOMAINS_PATH2)) return null;
13248
- const parsed = JSON.parse(readFileSync18(ALIAS_DOMAINS_PATH2, "utf-8"));
11674
+ if (!existsSync21(ALIAS_DOMAINS_PATH2)) return null;
11675
+ const parsed = JSON.parse(readFileSync15(ALIAS_DOMAINS_PATH2, "utf-8"));
13249
11676
  if (!Array.isArray(parsed)) {
13250
11677
  console.error("[alias-domains] malformed alias-domains.json \u2014 expected array");
13251
11678
  return null;
@@ -13585,20 +12012,20 @@ app36.get("/agent-assets/:slug/:filename", (c) => {
13585
12012
  console.error(`[agent-assets] no-account slug=${slug} file=${filename}`);
13586
12013
  return c.text("Not found", 404);
13587
12014
  }
13588
- const filePath = resolve25(account.accountDir, "agents", slug, "assets", filename);
13589
- const expectedDir = resolve25(account.accountDir, "agents", slug, "assets");
12015
+ const filePath = resolve21(account.accountDir, "agents", slug, "assets", filename);
12016
+ const expectedDir = resolve21(account.accountDir, "agents", slug, "assets");
13590
12017
  if (!filePath.startsWith(expectedDir + "/")) {
13591
12018
  console.error(`[agent-assets] path-traversal-rejected slug=${slug} file=${filename}`);
13592
12019
  return c.text("Forbidden", 403);
13593
12020
  }
13594
- if (!existsSync24(filePath)) {
12021
+ if (!existsSync21(filePath)) {
13595
12022
  console.error(`[agent-assets] serve slug=${slug} file=${filename} status=404`);
13596
12023
  return c.text("Not found", 404);
13597
12024
  }
13598
12025
  const ext = "." + filename.split(".").pop()?.toLowerCase();
13599
12026
  const contentType = IMAGE_MIME[ext] || "application/octet-stream";
13600
12027
  console.log(`[agent-assets] serve slug=${slug} file=${filename} status=200`);
13601
- const body = readFileSync18(filePath);
12028
+ const body = readFileSync15(filePath);
13602
12029
  return c.body(body, 200, {
13603
12030
  "Content-Type": contentType,
13604
12031
  "Cache-Control": "public, max-age=3600"
@@ -13615,20 +12042,20 @@ app36.get("/generated/:filename", (c) => {
13615
12042
  console.error(`[generated] serve file=${filename} status=404`);
13616
12043
  return c.text("Not found", 404);
13617
12044
  }
13618
- const filePath = resolve25(account.accountDir, "generated", filename);
13619
- const expectedDir = resolve25(account.accountDir, "generated");
12045
+ const filePath = resolve21(account.accountDir, "generated", filename);
12046
+ const expectedDir = resolve21(account.accountDir, "generated");
13620
12047
  if (!filePath.startsWith(expectedDir + "/")) {
13621
12048
  console.error(`[generated] serve file=${filename} status=403`);
13622
12049
  return c.text("Forbidden", 403);
13623
12050
  }
13624
- if (!existsSync24(filePath)) {
12051
+ if (!existsSync21(filePath)) {
13625
12052
  console.error(`[generated] serve file=${filename} status=404`);
13626
12053
  return c.text("Not found", 404);
13627
12054
  }
13628
12055
  const ext = "." + filename.split(".").pop()?.toLowerCase();
13629
12056
  const contentType = IMAGE_MIME[ext] || "application/octet-stream";
13630
12057
  console.log(`[generated] serve file=${filename} status=200`);
13631
- const body = readFileSync18(filePath);
12058
+ const body = readFileSync15(filePath);
13632
12059
  return c.body(body, 200, {
13633
12060
  "Content-Type": contentType,
13634
12061
  "Cache-Control": "public, max-age=86400"
@@ -13638,9 +12065,9 @@ app36.route("/sites", sites_default);
13638
12065
  var htmlCache = /* @__PURE__ */ new Map();
13639
12066
  var brandLogoPath = "/brand/maxy-monochrome.png";
13640
12067
  var brandIconPath = "/brand/maxy-monochrome.png";
13641
- if (BRAND_JSON_PATH && existsSync24(BRAND_JSON_PATH)) {
12068
+ if (BRAND_JSON_PATH && existsSync21(BRAND_JSON_PATH)) {
13642
12069
  try {
13643
- const fullBrand = JSON.parse(readFileSync18(BRAND_JSON_PATH, "utf-8"));
12070
+ const fullBrand = JSON.parse(readFileSync15(BRAND_JSON_PATH, "utf-8"));
13644
12071
  if (fullBrand.assets?.logo) brandLogoPath = `/brand/${fullBrand.assets.logo}`;
13645
12072
  brandIconPath = fullBrand.assets?.icon ? `/brand/${fullBrand.assets.icon}` : brandLogoPath;
13646
12073
  } catch {
@@ -13657,9 +12084,9 @@ var brandScript = `<script>window.__BRAND__=${JSON.stringify({
13657
12084
  function readInstalledVersion() {
13658
12085
  try {
13659
12086
  if (!PLATFORM_ROOT7) return "unknown";
13660
- const versionFile = join10(PLATFORM_ROOT7, "config", `.${BRAND.hostname}-version`);
13661
- if (!existsSync24(versionFile)) return "unknown";
13662
- const content = readFileSync18(versionFile, "utf-8").trim();
12087
+ const versionFile = join9(PLATFORM_ROOT7, "config", `.${BRAND.hostname}-version`);
12088
+ if (!existsSync21(versionFile)) return "unknown";
12089
+ const content = readFileSync15(versionFile, "utf-8").trim();
13663
12090
  return content || "unknown";
13664
12091
  } catch {
13665
12092
  return "unknown";
@@ -13700,7 +12127,7 @@ var clientErrorReporterScript = `<script>
13700
12127
  function cachedHtml(file) {
13701
12128
  let html = htmlCache.get(file);
13702
12129
  if (!html) {
13703
- html = readFileSync18(resolve25(process.cwd(), "public", file), "utf-8");
12130
+ html = readFileSync15(resolve21(process.cwd(), "public", file), "utf-8");
13704
12131
  const productNameEsc = escapeHtml(BRAND.productName);
13705
12132
  html = html.replace(/<title>([^<]*)<\/title>/, (_match, inner) => `<title>${escapeHtml(inner).replace(/Maxy/g, productNameEsc)}</title>`);
13706
12133
  html = html.replace('href="/favicon.ico"', `href="${escapeHtml(brandFaviconPath)}"`);
@@ -13716,26 +12143,26 @@ ${clientErrorReporterScript}
13716
12143
  }
13717
12144
  var brandedHtmlCache = /* @__PURE__ */ new Map();
13718
12145
  function loadBrandingCache(agentSlug) {
13719
- const configDir2 = join10(homedir2(), BRAND.configDir);
12146
+ const configDir2 = join9(homedir2(), BRAND.configDir);
13720
12147
  try {
13721
- const accountJsonPath = join10(configDir2, "account.json");
13722
- if (!existsSync24(accountJsonPath)) return null;
13723
- const account = JSON.parse(readFileSync18(accountJsonPath, "utf-8"));
12148
+ const accountJsonPath = join9(configDir2, "account.json");
12149
+ if (!existsSync21(accountJsonPath)) return null;
12150
+ const account = JSON.parse(readFileSync15(accountJsonPath, "utf-8"));
13724
12151
  const accountId = account.accountId;
13725
12152
  if (!accountId) return null;
13726
- const cachePath = join10(configDir2, "branding-cache", accountId, `${agentSlug}.json`);
13727
- if (!existsSync24(cachePath)) return null;
13728
- return JSON.parse(readFileSync18(cachePath, "utf-8"));
12153
+ const cachePath = join9(configDir2, "branding-cache", accountId, `${agentSlug}.json`);
12154
+ if (!existsSync21(cachePath)) return null;
12155
+ return JSON.parse(readFileSync15(cachePath, "utf-8"));
13729
12156
  } catch {
13730
12157
  return null;
13731
12158
  }
13732
12159
  }
13733
12160
  function resolveDefaultSlug() {
13734
12161
  try {
13735
- const configDir2 = join10(homedir2(), BRAND.configDir);
13736
- const accountJsonPath = join10(configDir2, "account.json");
13737
- if (!existsSync24(accountJsonPath)) return null;
13738
- const account = JSON.parse(readFileSync18(accountJsonPath, "utf-8"));
12162
+ const configDir2 = join9(homedir2(), BRAND.configDir);
12163
+ const accountJsonPath = join9(configDir2, "account.json");
12164
+ if (!existsSync21(accountJsonPath)) return null;
12165
+ const account = JSON.parse(readFileSync15(accountJsonPath, "utf-8"));
13739
12166
  return account.defaultAgent || null;
13740
12167
  } catch {
13741
12168
  return null;
@@ -13808,7 +12235,7 @@ app36.use("/vnc-popout.html", logViewerFetch);
13808
12235
  app36.get("/vnc-popout.html", (c) => {
13809
12236
  let html = htmlCache.get("vnc-popout.html");
13810
12237
  if (!html) {
13811
- html = readFileSync18(resolve25(process.cwd(), "public", "vnc-popout.html"), "utf-8");
12238
+ html = readFileSync15(resolve21(process.cwd(), "public", "vnc-popout.html"), "utf-8");
13812
12239
  const name = escapeHtml(BRAND.productName);
13813
12240
  html = html.replace("<title>Browser \u2014 Maxy</title>", `<title>${name}</title>`);
13814
12241
  html = html.replace("</head>", ` ${brandScript}
@@ -13898,8 +12325,8 @@ try {
13898
12325
  (async () => {
13899
12326
  try {
13900
12327
  let userId = "";
13901
- if (existsSync24(USERS_FILE)) {
13902
- const users = JSON.parse(readFileSync18(USERS_FILE, "utf-8").trim() || "[]");
12328
+ if (existsSync21(USERS_FILE)) {
12329
+ const users = JSON.parse(readFileSync15(USERS_FILE, "utf-8").trim() || "[]");
13903
12330
  userId = users[0]?.userId ?? "";
13904
12331
  }
13905
12332
  await backfillNullUserIdConversations(userId);
@@ -13914,15 +12341,8 @@ try {
13914
12341
  console.error(`[migration] runBootMigrations rejected: ${err instanceof Error ? err.message : String(err)}`);
13915
12342
  }
13916
12343
  })();
13917
- (async () => {
13918
- try {
13919
- await startReviewDetector();
13920
- } catch (err) {
13921
- console.error(`[review] startReviewDetector rejected: ${err instanceof Error ? err.message : String(err)}`);
13922
- }
13923
- })();
13924
12344
  startGraphHealthTimer();
13925
- var configDirForWhatsApp = basename7(MAXY_DIR) || ".maxy";
12345
+ var configDirForWhatsApp = basename5(MAXY_DIR) || ".maxy";
13926
12346
  var bootAccount = resolveAccount();
13927
12347
  var bootAccountConfig = bootAccount?.config;
13928
12348
  var bootPublicAgent = bootAccount ? resolvePublicAgent(bootAccount.accountDir, { accountId: bootAccount.accountId })?.slug ?? null : null;
@@ -13966,7 +12386,7 @@ if (bootAccountConfig?.whatsapp) {
13966
12386
  }
13967
12387
  init({
13968
12388
  configDir: configDirForWhatsApp,
13969
- platformRoot: resolve25(process.env.MAXY_PLATFORM_ROOT ?? join10(__dirname, "..")),
12389
+ platformRoot: resolve21(process.env.MAXY_PLATFORM_ROOT ?? join9(__dirname, "..")),
13970
12390
  accountConfig: bootAccountConfig,
13971
12391
  onMessage: async (msg) => {
13972
12392
  try {
@@ -14103,11 +12523,6 @@ process.on("SIGTERM", async () => {
14103
12523
  } catch (err) {
14104
12524
  console.error(`[server] shutdown error: ${String(err)}`);
14105
12525
  }
14106
- try {
14107
- await shutdownReviewDetector();
14108
- } catch (err) {
14109
- console.error(`[server] review detector shutdown error: ${String(err)}`);
14110
- }
14111
12526
  console.error("[server] graceful shutdown complete \u2014 exiting");
14112
12527
  process.exit(0);
14113
12528
  });