@pushary/agent-hooks 1.9.76 → 1.9.78

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 (125) hide show
  1. package/CHANGELOG.md +15 -0
  2. package/dist/{api-YDCHRRA4.js → api-4WCUP47K.js} +10 -10
  3. package/dist/bin/pushary-bell-hook.js +25 -25
  4. package/dist/bin/pushary-bell.js +10 -10
  5. package/dist/bin/pushary-claude.js +26 -26
  6. package/dist/bin/pushary-clean.js +34 -34
  7. package/dist/bin/pushary-codex-bridge.js +21 -21
  8. package/dist/bin/pushary-codex-hook.js +30 -30
  9. package/dist/bin/pushary-codex-session.js +8 -8
  10. package/dist/bin/pushary-codex.js +13 -13
  11. package/dist/bin/pushary-connect.js +26 -26
  12. package/dist/bin/pushary-cowork.js +5 -5
  13. package/dist/bin/pushary-daemon.js +122 -66
  14. package/dist/bin/pushary-disconnect.js +25 -25
  15. package/dist/bin/pushary-display-hook.js +16 -16
  16. package/dist/bin/pushary-doctor.js +42 -42
  17. package/dist/bin/pushary-elicitation-hook.js +25 -25
  18. package/dist/bin/pushary-gemini-bridge.js +15 -15
  19. package/dist/bin/pushary-gemini-hook.js +27 -27
  20. package/dist/bin/pushary-hook.js +29 -29
  21. package/dist/bin/pushary-login.js +20 -20
  22. package/dist/bin/pushary-logout.js +24 -24
  23. package/dist/bin/pushary-mcp.js +11 -11
  24. package/dist/bin/pushary-mode.js +15 -15
  25. package/dist/bin/pushary-notification-hook.js +24 -24
  26. package/dist/bin/pushary-opencode-hook.js +26 -26
  27. package/dist/bin/pushary-permission-denied-hook.js +29 -29
  28. package/dist/bin/pushary-permission-hook.js +29 -29
  29. package/dist/bin/pushary-post-hook.js +24 -24
  30. package/dist/bin/pushary-prompt-hook.js +24 -24
  31. package/dist/bin/pushary-session-end-hook.js +26 -26
  32. package/dist/bin/pushary-session-start-hook.js +34 -34
  33. package/dist/bin/pushary-setup.js +58 -55
  34. package/dist/bin/pushary-stats.js +5 -5
  35. package/dist/bin/pushary-status.js +31 -31
  36. package/dist/bin/pushary-stop-hook.js +24 -24
  37. package/dist/bin/pushary-stopfailure-hook.js +24 -24
  38. package/dist/bin/pushary-suggestions.js +15 -15
  39. package/dist/bin/pushary-transcript-register.js +46 -18
  40. package/dist/bin/pushary-transcripts.js +19 -19
  41. package/dist/bin/pushary-upgrade.js +40 -40
  42. package/dist/bin/pushary-wait.js +15 -15
  43. package/dist/bin/pushary.js +7 -7
  44. package/dist/{chunk-TFYFQM5C.js → chunk-22FLBWJH.js} +2 -2
  45. package/dist/{chunk-KGWLDNNT.js → chunk-3XH4YLCN.js} +4 -4
  46. package/dist/{chunk-5FV472Q2.js → chunk-42Y7IZSV.js} +1 -1
  47. package/dist/{chunk-PPD64MZW.js → chunk-4DOJLAGR.js} +1 -1
  48. package/dist/{chunk-CUD6SYDG.js → chunk-4FYY6YW3.js} +1 -1
  49. package/dist/{chunk-BUR2LYQI.js → chunk-4ZSQPGAU.js} +2 -2
  50. package/dist/{chunk-MMVIKXCP.js → chunk-56POFDRB.js} +3 -3
  51. package/dist/{chunk-JNV5LOZH.js → chunk-5F3L3QOO.js} +4 -4
  52. package/dist/{chunk-WK2L3XVQ.js → chunk-5QOMMZFZ.js} +1 -1
  53. package/dist/{chunk-USZJJWZL.js → chunk-5WU6534E.js} +2 -2
  54. package/dist/{chunk-DZF2M3LM.js → chunk-5YHI2FN4.js} +6 -6
  55. package/dist/{chunk-ZY25SNFQ.js → chunk-6HDYWRCP.js} +1 -1
  56. package/dist/{chunk-47TIRGS5.js → chunk-6WOG2SCC.js} +1 -0
  57. package/dist/{chunk-4LX5QUFV.js → chunk-74NZ3CJQ.js} +1 -1
  58. package/dist/{chunk-W5AUTVVL.js → chunk-74R4IZ7N.js} +5 -5
  59. package/dist/{chunk-VZSSL6US.js → chunk-7BMBXTN4.js} +14 -14
  60. package/dist/{chunk-DB3NOEDW.js → chunk-7KMI2G24.js} +6 -6
  61. package/dist/{chunk-UEY4SHY2.js → chunk-AOGM34LL.js} +1 -1
  62. package/dist/{chunk-2QFYEB3P.js → chunk-B4MQSH3R.js} +5 -5
  63. package/dist/{chunk-F7R7M5MG.js → chunk-BE5BA3GF.js} +1 -1
  64. package/dist/{chunk-74ONBRPF.js → chunk-BM3AT5AF.js} +4 -4
  65. package/dist/{chunk-JZ5KV2FK.js → chunk-C5LCOXAC.js} +5 -5
  66. package/dist/{chunk-XLGD5LDG.js → chunk-CHBK47DU.js} +3 -3
  67. package/dist/{chunk-USMUW3LI.js → chunk-DLOUKVLM.js} +2 -2
  68. package/dist/{chunk-UMBVIFP5.js → chunk-DQCQQ2PY.js} +1 -1
  69. package/dist/{chunk-MTRZQR2R.js → chunk-EDHOHS3C.js} +3 -3
  70. package/dist/{chunk-M5VJ3HXB.js → chunk-FMDP5KWV.js} +6 -6
  71. package/dist/{chunk-CHIOUBPB.js → chunk-IVBQ5ISS.js} +13 -13
  72. package/dist/{chunk-5M5F4IWR.js → chunk-IVY6LYTO.js} +1 -1
  73. package/dist/{chunk-WLHHDDVA.js → chunk-JLBLEFBK.js} +3 -3
  74. package/dist/{chunk-WRCM4XGY.js → chunk-JM4WO3JT.js} +2 -2
  75. package/dist/{chunk-GJVV2EUC.js → chunk-KJYZXX7X.js} +5 -3
  76. package/dist/{chunk-NJTAUEW6.js → chunk-L2S7KWMF.js} +1 -1
  77. package/dist/{chunk-5KPMDLXB.js → chunk-LXSGNGK5.js} +3 -3
  78. package/dist/{chunk-VELPOV2F.js → chunk-MJXP5MFI.js} +4 -4
  79. package/dist/{chunk-YLB4AGZJ.js → chunk-MPZFQP6O.js} +1 -1
  80. package/dist/{chunk-E7U4YM4A.js → chunk-NFK5ZCLK.js} +3 -3
  81. package/dist/{chunk-7LLC4R66.js → chunk-NH7C6QBI.js} +4 -4
  82. package/dist/{chunk-Y4OBAK4I.js → chunk-OFJUHEEG.js} +8 -5
  83. package/dist/{chunk-IRLHN5UP.js → chunk-OFQACQDL.js} +27 -7
  84. package/dist/{chunk-CMNB4IZO.js → chunk-OOODSAHW.js} +3 -3
  85. package/dist/{chunk-7ZOWQBHQ.js → chunk-PYW7VWVZ.js} +5 -5
  86. package/dist/{chunk-M7JFGDGP.js → chunk-Q53QJQLY.js} +1 -1
  87. package/dist/{chunk-R3HXGHRQ.js → chunk-RCDZ2XZ6.js} +1 -1
  88. package/dist/{chunk-XJ5DYPTM.js → chunk-RDU5HWKU.js} +4 -4
  89. package/dist/{chunk-MAWC4PJV.js → chunk-RIVCTSYV.js} +5 -5
  90. package/dist/{chunk-E2VJ7B7D.js → chunk-RJEG3TUZ.js} +6 -6
  91. package/dist/{chunk-QUGP76DQ.js → chunk-SV2C7PZV.js} +6 -6
  92. package/dist/{chunk-7SFP6Y5U.js → chunk-T2MJ5H2V.js} +603 -162
  93. package/dist/{chunk-3YU6NIIL.js → chunk-TDLK3ZU2.js} +4 -4
  94. package/dist/{chunk-C2AR7QXU.js → chunk-TKQGDSO4.js} +2 -2
  95. package/dist/{chunk-2BK2EQOB.js → chunk-TNNB7P6Z.js} +7 -7
  96. package/dist/{chunk-7KYQ24FI.js → chunk-TQYSBYYZ.js} +4 -4
  97. package/dist/{chunk-IRN6JLSM.js → chunk-TRTAYW7Z.js} +4 -4
  98. package/dist/{chunk-SWRNJ3BP.js → chunk-VIIQLZJS.js} +3 -3
  99. package/dist/{chunk-NFUV25JZ.js → chunk-VYELSTZK.js} +3 -3
  100. package/dist/{chunk-S7C5NY4R.js → chunk-VZHDSUXK.js} +1 -1
  101. package/dist/{chunk-FAGQAR6I.js → chunk-WHTWHHE3.js} +478 -161
  102. package/dist/{chunk-57MQNSM5.js → chunk-WJTUCQ47.js} +1 -1
  103. package/dist/{chunk-KWGOKO4E.js → chunk-XCBKGQ6N.js} +3 -3
  104. package/dist/{chunk-T2EJ3H6Y.js → chunk-Y4I5YX3L.js} +3 -3
  105. package/dist/{chunk-GII565PB.js → chunk-YH3RAWMV.js} +1 -1
  106. package/dist/{chunk-NQA56AMZ.js → chunk-YJK3LBAJ.js} +6 -6
  107. package/dist/{chunk-B2PFCS72.js → chunk-YOPFR22Y.js} +2 -2
  108. package/dist/{chunk-5NW7VM35.js → chunk-YP35CA5L.js} +2 -2
  109. package/dist/{chunk-ITLWEGLE.js → chunk-Z5VZWDGL.js} +15 -5
  110. package/dist/{chunk-Y2CYSVWU.js → chunk-Z6DZHUCO.js} +6 -6
  111. package/dist/{chunk-YDPGBBV7.js → chunk-ZFS2Y5GK.js} +5 -5
  112. package/dist/{chunk-NVLKH5FS.js → chunk-ZM3ELQPE.js} +9 -9
  113. package/dist/{codex-app-cards-switch-3OUTABVU.js → codex-app-cards-switch-GUZ7XMPB.js} +11 -11
  114. package/dist/{codex-desktop-launch-JH3E2CIA.js → codex-desktop-launch-Q5UINXU6.js} +1 -1
  115. package/dist/{codex-session-launch-GR363OQN.js → codex-session-launch-4G5AFXXU.js} +18 -18
  116. package/dist/{config-R43UFHX7.js → config-725HOJFK.js} +3 -3
  117. package/dist/{global-upgrade-73GP4MZM.js → global-upgrade-AXTMEH3I.js} +10 -10
  118. package/dist/{identity-CA7CZGDL.js → identity-HLIZDGEN.js} +4 -4
  119. package/dist/{live-capabilities-RCGMQIIZ.js → live-capabilities-A74OIHWN.js} +5 -5
  120. package/dist/{paths-H7VNNB46.js → paths-ZHDLQD25.js} +3 -3
  121. package/dist/{reapply-3UIA3CUO.js → reapply-RC2AMGCQ.js} +27 -27
  122. package/dist/src/index.js +28 -28
  123. package/dist/{upgrade-lock-ZTLX5PDF.js → upgrade-lock-4B3D3OCT.js} +4 -4
  124. package/dist/{usage-C53NVWC2.js → usage-VVPEFYKS.js} +3 -3
  125. package/package.json +1 -1
@@ -5,19 +5,19 @@ import {
5
5
  hermesAppWiringRecord,
6
6
  hermesConfigReceipt,
7
7
  hermesSetupReceipt
8
- } from "./chunk-7KYQ24FI.js";
8
+ } from "./chunk-TQYSBYYZ.js";
9
9
  import {
10
10
  readJsonSafe,
11
11
  resolvePathBinary,
12
12
  writeJsonAtomic
13
- } from "./chunk-T2EJ3H6Y.js";
13
+ } from "./chunk-Y4I5YX3L.js";
14
14
  import {
15
15
  isNewerVersion,
16
16
  isReleaseVersion
17
17
  } from "./chunk-QTOLNRET.js";
18
18
  import {
19
19
  isRecord
20
- } from "./chunk-Y4OBAK4I.js";
20
+ } from "./chunk-OFJUHEEG.js";
21
21
 
22
22
  // src/hermes/plugin-payload.ts
23
23
  import { createHash } from "crypto";
@@ -30,7 +30,7 @@ import os
30
30
  import threading
31
31
  from pathlib import Path
32
32
 
33
- from . import api, approval, host, identity, installation, schemas, tools
33
+ from . import api, approval, history, host, identity, installation, schemas, tools
34
34
 
35
35
  logger = logging.getLogger(__name__)
36
36
 
@@ -332,6 +332,7 @@ def _on_post_tool_call(tool_name, args, result, duration_ms=0, status=None, **kw
332
332
 
333
333
  def _on_session_start(session_id=None, **kwargs):
334
334
  identity.remember_session(session_id)
335
+ history.start_history(session_id, kwargs.get("parent_session_id"))
335
336
  _session_totals.update(tools=0, errors=0)
336
337
  _last_turn.update(completed=False, interrupted=False)
337
338
  _report_session_event("session_start", session_id, can_drain=True)
@@ -358,6 +359,7 @@ def _report_plugin_active_once(session_id):
358
359
 
359
360
  def _on_pre_llm_call(session_id=None, **kwargs):
360
361
  identity.remember_session(session_id)
362
+ history.start_history(session_id, kwargs.get("parent_session_id"))
361
363
  if not api.is_configured() or not identity.session_id():
362
364
  return None
363
365
  result = api.agent_event("user_prompt", identity.session_id(), can_drain=True)
@@ -402,6 +404,7 @@ def _on_session_end(session_id=None, completed=False, interrupted=False, failed=
402
404
 
403
405
 
404
406
  def _on_session_finalize(session_id=None, **kwargs):
407
+ history.stop_history(session_id or identity.session_id())
405
408
  _report_session_event("session_closed", session_id or identity.session_id())
406
409
  tool_count = _session_totals["tools"]
407
410
  error_count = _session_totals["errors"]
@@ -932,6 +935,261 @@ def get_end_user_decision(decision_id, wait_seconds=45):
932
935
  return _rest_call("GET", f"/decisions/{seg}", params={"wait": wait_seconds})
933
936
  ` },
934
937
  { path: "approval.py", contents: 'import time\n\nfrom . import api, identity\n\nHERMES_SHELL_TOOL = "terminal"\nDECISION_LINE_MAX = 500\nACTION_BODY_MAX = 4000\n\nPOLL_CEILING_SECONDS = 50.0\n\nCHOICE_LABELS = (\n ("once", "Allow once"),\n ("session", "Allow for this session"),\n ("always", "Always allow"),\n ("deny", "Deny"),\n)\n\n\nclass TransportUnavailable(Exception):\n pass\n\n\nQUESTION_COMMAND_MAX = 160\n\nPLUGIN_RULE_MARKER = "(plugin approval rule)"\n\n\ndef _offered(allowed):\n return [pair for pair in CHOICE_LABELS if pair[0] in set(allowed or ())]\n\n\ndef _command_head(command):\n return " ".join(str(command or "").split()[:2])[:80]\n\n\ndef _command_summary(command):\n collapsed = " ".join(str(command or "").split())\n if len(collapsed) <= QUESTION_COMMAND_MAX:\n return collapsed\n return collapsed[:QUESTION_COMMAND_MAX - 1] + "\u2026"\n\n\ndef _choice_from_answer(value, offered, uses_select):\n answer = str(value or "").strip()\n if uses_select:\n for choice, label in offered:\n if answer.lower() == label.lower():\n return choice\n return "deny"\n return "once" if api.is_affirmative(answer) else "deny"\n\n\ndef _subject(request):\n """What to put in the question.\n\n A dangerous-command approval carries the real command. A plugin rule\n escalated from pre_tool_call carries a synthetic display label instead, and\n the plugin\'s own message is the part worth reading, so use that.\n """\n command = str(request.command or "")\n if PLUGIN_RULE_MARKER in command:\n lines = str(request.description or "").strip().splitlines()\n if lines and lines[0].strip():\n return _command_summary(lines[0])\n return _command_summary(command)\n\n\ndef _open_question(request, offered, uses_select):\n command = str(request.command or "")\n return api.ask_user(\n question=f"Allow Hermes to run `{_subject(request)}`?",\n question_type="select" if uses_select else "confirm",\n options=[label for _, label in offered] if uses_select else None,\n context=str(request.description or "")[:500] or None,\n agent_name=identity.agent_name(),\n session_id=identity.session_id(),\n machine_id=identity.machine_id(),\n tool_name=HERMES_SHELL_TOOL,\n tool_target=_command_head(command),\n action=command[:DECISION_LINE_MAX] or None,\n action_body=command[:ACTION_BODY_MAX] or None,\n blocker=(\n f"Hermes flagged this command as {request.pattern_key}"[:DECISION_LINE_MAX]\n if request.pattern_key\n else None\n ),\n wait=False,\n )\n\n\ndef _stopped(result):\n return result.get("handoffAction") == "stop" or result.get("status") in ("cancelled", "unavailable", "stopped")\n\n\ndef _withdraw(correlation_id):\n try:\n cancelled = api.cancel_question(correlation_id)\n if _stopped(cancelled) or cancelled.get("error"):\n return {"handoffAction": "stop"}\n if cancelled.get("cancelled") is True:\n return {}\n answer = api.wait_for_answer(correlation_id, 1000)\n if answer.get("answered") or answer.get("status") in ("expired", "missing"):\n return answer\n except Exception:\n pass\n return {"handoffAction": "stop"}\n\n\ndef present(request):\n offered = _offered(request.allowed_choices)\n if not offered:\n return request.respond("deny")\n uses_select = [choice for choice, _ in offered] != ["once", "deny"]\n # Reserve time for withdrawal before the host discards a late decision.\n started_at = time.monotonic()\n deadline = started_at + max(float(request.timeout_seconds or 0) - 10, 0.0)\n created = _open_question(request, offered, uses_select)\n if created.get("answered"):\n return request.respond(_choice_from_answer(created.get("value"), offered, uses_select))\n if _stopped(created):\n return request.respond("deny")\n correlation_id = created.get("correlationId")\n if not correlation_id:\n raise TransportUnavailable("Pushary did not create an approval")\n\n policy_deadline = None\n window_ms = created.get("policyTimeoutMs")\n if created.get("deliveryMode") == "push_first" and isinstance(window_ms, (int, float)) and window_ms >= 0:\n policy_deadline = started_at + window_ms / 1000\n deadline = min(deadline, policy_deadline)\n handoff = created.get("noDevices") or created.get("suppressed") or created.get("status") in ("terminal", "notified")\n answer = created\n try:\n while not handoff and time.monotonic() < deadline:\n remaining = deadline - time.monotonic()\n answer = api.wait_for_answer(correlation_id, int(min(POLL_CEILING_SECONDS, max(1.0, remaining)) * 1000))\n if answer.get("answered"):\n return request.respond(_choice_from_answer(answer.get("value"), offered, uses_select))\n if _stopped(answer) or answer.get("error") or answer.get("status") in ("expired", "missing"):\n break\n # Some hosts/proxies return pending immediately instead of long-polling.\n time.sleep(min(0.1, max(0, deadline - time.monotonic())))\n except Exception:\n answer = {"handoffAction": "stop"}\n\n late = _withdraw(correlation_id)\n if _stopped(answer) or _stopped(late):\n return request.respond("deny")\n if late.get("answered"):\n return request.respond(_choice_from_answer(late.get("value"), offered, uses_select))\n if handoff or (policy_deadline is not None and time.monotonic() >= policy_deadline):\n # Only a fenced question may reach Hermes\'s configured builtin fallback.\n raise TransportUnavailable("Approval handed back to Hermes")\n return request.respond("deny")\n' },
938
+ { path: "history.py", contents: `from __future__ import annotations
939
+
940
+ import hashlib
941
+ import contextvars
942
+ import json
943
+ import logging
944
+ import math
945
+ import os
946
+ import re
947
+ import shutil
948
+ import subprocess
949
+ import tempfile
950
+ import threading
951
+ import time
952
+ from datetime import datetime, timezone
953
+ from pathlib import Path
954
+
955
+ from . import api
956
+
957
+ logger = logging.getLogger(__name__)
958
+ MAX_MESSAGES = 5000
959
+ MAX_SOURCE_BYTES = 20_000_000
960
+ POLL_SECONDS = 5
961
+ _workers: dict[tuple[str, str], tuple[threading.Event, threading.Thread]] = {}
962
+ _lock = threading.Lock()
963
+
964
+
965
+ def _text_blocks(content: object) -> list[dict[str, object]]:
966
+ if isinstance(content, str):
967
+ return [{"type": "text", "text": content}]
968
+ if not isinstance(content, list):
969
+ return []
970
+ return [{"type": "text", "text": block["text"]} for block in content
971
+ if isinstance(block, dict) and block.get("type") in ("text", "input_text", "output_text")
972
+ and isinstance(block.get("text"), str)]
973
+
974
+
975
+ def _tool_blocks(calls: object) -> list[dict[str, object]]:
976
+ blocks = []
977
+ if not isinstance(calls, list):
978
+ return blocks
979
+ for call in calls:
980
+ if not isinstance(call, dict) or not isinstance(call.get("id"), str):
981
+ continue
982
+ function = call.get("function")
983
+ if not isinstance(function, dict) or not isinstance(function.get("name"), str):
984
+ continue
985
+ arguments = function.get("arguments", {})
986
+ if isinstance(arguments, str):
987
+ arguments = json.loads(arguments)
988
+ if not isinstance(arguments, dict):
989
+ raise ValueError("Invalid Hermes tool arguments")
990
+ blocks.append({"type": "tool_use", "id": call["id"], "name": function["name"], "input": arguments})
991
+ return blocks
992
+
993
+
994
+ def normalize_messages(rows: list[object]) -> list[dict[str, object]]:
995
+ messages = []
996
+ for row in rows:
997
+ if not isinstance(row, dict):
998
+ raise ValueError("Invalid Hermes message")
999
+ if not (row.get("active") or row.get("compacted")) or row.get("display_kind") in ("hidden", "model_only"):
1000
+ continue
1001
+ role = row.get("role")
1002
+ if role not in ("user", "assistant", "tool") or row.get("_compressed_summary"):
1003
+ continue
1004
+ identity = row.get("id")
1005
+ stamp = row.get("timestamp")
1006
+ if not isinstance(identity, int) or isinstance(identity, bool) or identity <= 0:
1007
+ raise ValueError("Invalid Hermes message identity")
1008
+ if not isinstance(stamp, (int, float)) or not math.isfinite(stamp) or stamp <= 0:
1009
+ raise ValueError("Invalid Hermes message timestamp")
1010
+ content = _text_blocks(row.get("content"))
1011
+ if role == "assistant":
1012
+ content.extend(_tool_blocks(row.get("tool_calls")))
1013
+ elif role == "tool":
1014
+ call_id = row.get("tool_call_id")
1015
+ if not isinstance(call_id, str) or not call_id:
1016
+ continue
1017
+ content = [{"type": "tool_result", "tool_use_id": call_id, "content": content}]
1018
+ messages.append({
1019
+ "id": f"hermes:{identity}", "role": "user" if role == "tool" else role,
1020
+ "timestamp": datetime.fromtimestamp(stamp, timezone.utc).isoformat().replace("+00:00", "Z"),
1021
+ "content": content,
1022
+ })
1023
+ return messages
1024
+
1025
+
1026
+ def read_display_history(database_path: Path, session_id: str) -> list[dict[str, object]]:
1027
+ from hermes_state import SessionDB
1028
+
1029
+ database = SessionDB(db_path=database_path, read_only=True)
1030
+ try:
1031
+ connection = database._conn
1032
+ deadline = time.monotonic() + 2
1033
+ connection.set_progress_handler(lambda: int(time.monotonic() > deadline), 1000)
1034
+ connection.execute("BEGIN")
1035
+ if database.get_session(session_id) is None:
1036
+ raise ValueError("Hermes session is not in this profile")
1037
+ lineage = database._resume_lineage_ids(session_id)
1038
+ if not lineage or len(lineage) > 100:
1039
+ raise ValueError("Unsupported Hermes compression lineage")
1040
+ columns = [row[1] for row in connection.execute("PRAGMA table_info(messages)")]
1041
+ required = {"id", "session_id", "role", "content", "timestamp", "active", "compacted", "display_kind", "message_uid"}
1042
+ if not required.issubset(columns):
1043
+ raise ValueError("Unsupported Hermes history schema")
1044
+ identifiers = ['"' + column.replace('"', '""') + '"' for column in columns]
1045
+ row_bytes = " + ".join(f"COALESCE(length(CAST({column} AS BLOB)), 0)" for column in identifiers)
1046
+ placeholders = ",".join("?" for _ in lineage)
1047
+ count, size = connection.execute(
1048
+ f"SELECT COUNT(*), SUM({row_bytes}) FROM messages WHERE session_id IN ({placeholders})", lineage
1049
+ ).fetchone()
1050
+ if count > MAX_MESSAGES or (size or 0) > MAX_SOURCE_BYTES:
1051
+ raise ValueError("Hermes history exceeds the bounded reader limit")
1052
+ rows = database.get_messages(session_id, include_compacted=True, include_ancestors=True, limit=MAX_MESSAGES)
1053
+ return normalize_messages(rows)
1054
+ finally:
1055
+ database.close()
1056
+
1057
+
1058
+ def write_snapshot(path: Path, session_id: str, messages: list[dict[str, object]], error: str | None = None, completed: bool = False) -> None:
1059
+ value = {"version": 1, "sessionId": session_id, "updatedAtMs": int(time.time() * 1000), "messages": messages, "completed": completed}
1060
+ if error:
1061
+ value["error"] = error
1062
+ encoded = json.dumps(value, ensure_ascii=True, allow_nan=False).encode("utf-8")
1063
+ if len(encoded) > MAX_SOURCE_BYTES:
1064
+ raise ValueError("Hermes history export exceeds its size limit")
1065
+ path.parent.mkdir(mode=0o700, parents=True, exist_ok=True)
1066
+ descriptor, temporary = tempfile.mkstemp(dir=path.parent, prefix=".history-")
1067
+ try:
1068
+ with os.fdopen(descriptor, "wb") as handle:
1069
+ handle.write(encoded)
1070
+ handle.flush()
1071
+ os.fsync(handle.fileno())
1072
+ os.replace(temporary, path)
1073
+ finally:
1074
+ Path(temporary).unlink(missing_ok=True)
1075
+
1076
+
1077
+ def _registration_command(key: str) -> list[str] | None:
1078
+ root = Path.home() / ".pushary"
1079
+ try:
1080
+ if (root / "native-transcript-consent.json").exists():
1081
+ bridge = (root / "bin" / "pushary-bridge").resolve(strict=True)
1082
+ helper = str(bridge.parent / "pushary-native-transcripts")
1083
+ command = [helper, "register", "hermes"]
1084
+ else:
1085
+ helper = shutil.which("pushary-transcript-register")
1086
+ if helper is None:
1087
+ return None
1088
+ command = [helper, "hermes"]
1089
+ result = subprocess.run([helper, "authorize-export"],
1090
+ input=json.dumps({"apiKey": key, "baseUrl": api.BASE_URL}), text=True,
1091
+ stdout=subprocess.PIPE, stderr=subprocess.DEVNULL, timeout=5, check=False,
1092
+ env={**os.environ, "PUSHARY_API_KEY": key, "PUSHARY_BASE_URL": api.BASE_URL})
1093
+ if result.returncode != 0:
1094
+ return None
1095
+ authorization = json.loads(result.stdout)
1096
+ if not isinstance(authorization, dict) or type(authorization.get("version")) is not int or authorization.get("version") != 1 or authorization.get("authorized") is not True:
1097
+ return None
1098
+ return command
1099
+ except (OSError, ValueError, subprocess.TimeoutExpired):
1100
+ return None
1101
+
1102
+
1103
+ def _export_loop(database_path: Path, session_id: str, key: str, command: list[str], stop: threading.Event) -> None:
1104
+ binding = hashlib.sha256((api.BASE_URL.rstrip("/") + "\\0" + key + "\\0" + str(database_path)).encode()).hexdigest()
1105
+ path = Path.home() / ".pushary" / "hermes-history" / binding / f"{session_id}.json"
1106
+ next_registration = 0
1107
+ warned = False
1108
+ revoked = False
1109
+ try:
1110
+ while True:
1111
+ if api._get_api_key() != key or _registration_command(key) != command:
1112
+ revoked = True
1113
+ break
1114
+ ending = stop.is_set()
1115
+ try:
1116
+ messages = read_display_history(database_path, session_id)
1117
+ write_snapshot(path, session_id, messages, completed=ending)
1118
+ warned = False
1119
+ except Exception:
1120
+ if not warned:
1121
+ logger.warning("[pushary] Hermes history source is unavailable; existing phone history is preserved")
1122
+ warned = True
1123
+ write_snapshot(path, session_id, [], error="source_unavailable")
1124
+ if ending or time.monotonic() >= next_registration:
1125
+ payload = {"session_id": session_id, "cwd": str(database_path.parent), "transcript_path": str(path),
1126
+ "hook_event_name": "SessionEnd" if ending else "SessionStart"}
1127
+ environment = {**os.environ, "PUSHARY_API_KEY": key, "PUSHARY_BASE_URL": api.BASE_URL}
1128
+ result = subprocess.run(command, input=json.dumps(payload), text=True, env=environment,
1129
+ stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL, timeout=5, check=False)
1130
+ if result.returncode != 0:
1131
+ logger.warning("[pushary] Hermes history reader could not register")
1132
+ next_registration = time.monotonic() + 60
1133
+ if ending:
1134
+ break
1135
+ stop.wait(POLL_SECONDS)
1136
+ except Exception:
1137
+ logger.warning("[pushary] Hermes history reader stopped; reopen the session to retry")
1138
+ finally:
1139
+ if revoked:
1140
+ path.unlink(missing_ok=True)
1141
+ with _lock:
1142
+ _workers.pop((str(database_path), session_id), None)
1143
+
1144
+
1145
+ def start_history(session_id: object, parent_session_id: object = None) -> None:
1146
+ if parent_session_id or not isinstance(session_id, str) or not re.fullmatch(r"[A-Za-z0-9_-]{1,128}", session_id):
1147
+ return
1148
+ try:
1149
+ from hermes_constants import get_hermes_home
1150
+
1151
+ key = api._get_api_key()
1152
+ command = _registration_command(key)
1153
+ if command is None:
1154
+ return
1155
+ database_path = (get_hermes_home() / "state.db").resolve()
1156
+ identity = (str(database_path), session_id)
1157
+ with _lock:
1158
+ if identity in _workers or len(_workers) >= 32:
1159
+ return
1160
+ stop = threading.Event()
1161
+ context = contextvars.copy_context()
1162
+ thread = threading.Thread(target=context.run, args=(_export_loop, database_path, session_id, key, command, stop),
1163
+ name="pushary-history", daemon=True)
1164
+ with _lock:
1165
+ if identity in _workers or len(_workers) >= 32:
1166
+ return
1167
+ _workers[identity] = (stop, thread)
1168
+ try:
1169
+ thread.start()
1170
+ except Exception:
1171
+ with _lock:
1172
+ _workers.pop(identity, None)
1173
+ raise
1174
+ except Exception:
1175
+ logger.warning("[pushary] Hermes history reader could not start")
1176
+
1177
+
1178
+ def stop_history(session_id: str) -> None:
1179
+ try:
1180
+ from hermes_constants import get_hermes_home
1181
+ database_path = str((get_hermes_home() / "state.db").resolve())
1182
+ except Exception:
1183
+ return
1184
+ with _lock:
1185
+ worker = _workers.get((database_path, session_id))
1186
+ if worker:
1187
+ stop, thread = worker
1188
+ stop.set()
1189
+ thread.join(timeout=8)
1190
+ if thread.is_alive():
1191
+ logger.warning("[pushary] Hermes final history export is still pending")
1192
+ ` },
935
1193
  { path: "host.py", contents: 'import os\n\nTRANSPORT_NAME = "pushary"\n\nHOOK_TIMEOUT_DEFAULT_SECONDS = 30.0\nHOOK_TIMEOUT_MAX_SECONDS = 600.0\n\nGATE_HEADROOM_SECONDS = 5.0\nGATE_MIN_SECONDS = 5.0\nGATE_MAX_SECONDS = 55.0\n\n# The last Hermes that runs a pre_tool_call callback to completion. Anything\n# after this abandons one that outlives plugins.hook_callback_timeout.\nUNBOUNDED_THROUGH_VERSION = (0, 20, 5)\n\n\ndef _config():\n try:\n from hermes_cli.config import load_config_readonly\n\n loaded = load_config_readonly()\n except Exception:\n return {}\n return loaded if isinstance(loaded, dict) else {}\n\n\ndef _section(node, *path):\n for key in path:\n node = node.get(key) if isinstance(node, dict) else None\n if node is None:\n return {}\n return node if isinstance(node, dict) else {}\n\n\ndef profile_secret(name):\n try:\n from agent.secret_scope import get_secret\n except Exception:\n return os.environ.get(name) or None\n try:\n return get_secret(name) or None\n except Exception:\n return None\n\n\ndef hermes_version_label():\n try:\n from importlib.metadata import version\n\n return version("hermes-cli")\n except Exception:\n try:\n import hermes_cli\n\n label = getattr(hermes_cli, "__version__", None)\n except Exception:\n return None\n return str(label) if label else None\n\n\ndef _hermes_version():\n """This Hermes as a comparable tuple, or None when it will not say."""\n raw = hermes_version_label()\n if not raw:\n return None\n parts = []\n for piece in str(raw).split(".")[:3]:\n digits = ""\n for char in piece:\n if not char.isdigit():\n break\n digits += char\n if not digits:\n return None\n parts.append(int(digits))\n while len(parts) < 3:\n parts.append(0)\n return tuple(parts)\n\n\ndef bounds_pre_tool_call():\n """Whether this Hermes abandons a slow pre_tool_call callback.\n\n The timeout was added after 0.20.5, which runs plugin hooks to completion.\n Clamping the wait on a host that would never have cut it off just shortens\n the window the user has to answer, for nothing.\n\n Unreadable means assume bounded. A wait that is shorter than it had to be\n costs the user seconds; a wait longer than the host allows costs them the\n answer they already gave.\n\n That rule is why the private symbol is not asked on its own. `hasattr` on\n `_HOOK_TIMEOUT_FAIL_CLOSED_HOOKS` cannot tell "this host runs hooks to\n completion" apart from "the private name moved", and those two answers want\n opposite behaviour: the first earns the full window, the second spends 55\n seconds against a budget of 30 and loses an answer the user already gave.\n A rename is the likelier of the two, because a private name is the one thing\n a host is free to change. So the symbol is taken as proof of bounded, and\n only a version this side of the timeout landing is taken as proof of the\n other. Anything unreadable falls to bounded.\n """\n try:\n from hermes_cli import plugins\n except Exception:\n return True\n if hasattr(plugins, "_HOOK_TIMEOUT_FAIL_CLOSED_HOOKS"):\n return True\n version = _hermes_version()\n if version is None:\n return True\n return version > UNBOUNDED_THROUGH_VERSION\n\n\ndef hook_callback_timeout_seconds():\n raw = _section(_config(), "plugins").get("hook_callback_timeout")\n if raw is None:\n return HOOK_TIMEOUT_DEFAULT_SECONDS\n try:\n timeout = float(raw)\n except (TypeError, ValueError):\n return HOOK_TIMEOUT_DEFAULT_SECONDS\n if timeout < 0:\n return HOOK_TIMEOUT_DEFAULT_SECONDS\n return min(timeout, HOOK_TIMEOUT_MAX_SECONDS)\n\n\ndef gate_budget_seconds(requested_seconds):\n if not bounds_pre_tool_call():\n return max(1.0, min(float(requested_seconds), GATE_MAX_SECONDS))\n timeout = hook_callback_timeout_seconds()\n ceiling = (\n GATE_MAX_SECONDS\n if timeout <= 0\n else max(GATE_MIN_SECONDS, timeout - GATE_HEADROOM_SECONDS)\n )\n return max(1.0, min(float(requested_seconds), ceiling, GATE_MAX_SECONDS))\n\n\ndef pushary_is_selected_transport():\n selected = _section(_config(), "security", "approval").get("transport")\n return str(selected or "").strip().lower() == TRANSPORT_NAME\n' },
936
1194
  { path: "identity.py", contents: 'import hashlib\nimport os\nimport re\nimport socket\nfrom pathlib import Path\n\nDEFAULT_AGENT_NAME = "Hermes"\nMAX_ID_LENGTH = 128\nSHARED_MACHINE_ID_FILE = Path.home() / ".pushary" / "machine-id"\nSHARED_MACHINE_ID_PATTERN = re.compile(r"^[A-Za-z0-9-]{1,128}$")\n\n_session_id = None\n\n\ndef agent_name():\n return os.environ.get("PUSHARY_AGENT_NAME", DEFAULT_AGENT_NAME)\n\n\ndef labelled_agent_name(requested):\n project = str(requested or "").partition(" - ")[2].strip()\n return f"{agent_name()} - {project}" if project else agent_name()\n\n\ndef shared_machine_id(path=None):\n try:\n value = (path or SHARED_MACHINE_ID_FILE).read_text(encoding="utf-8").strip()\n except OSError:\n return None\n return value if SHARED_MACHINE_ID_PATTERN.match(value) else None\n\n\ndef hostname_machine_id():\n try:\n host = socket.gethostname()\n except Exception:\n return None\n if not host:\n return None\n return hashlib.sha256(host.encode("utf-8")).hexdigest()[:8]\n\n\ndef machine_id():\n return shared_machine_id() or hostname_machine_id()\n\n\ndef remember_session(value):\n global _session_id\n if value:\n _session_id = str(value)[:MAX_ID_LENGTH]\n\n\ndef forget_session():\n global _session_id\n _session_id = None\n\n\ndef session_id():\n return _session_id\n' },
937
1195
  { path: "installation.py", contents: `import json
@@ -1063,7 +1321,7 @@ def record_plugin_load(approval_transport_registered, home=None, pid=None, now=N
1063
1321
  logger.debug("[pushary] could not record the plugin load: %s", exc)
1064
1322
  return None
1065
1323
  ` },
1066
- { path: "plugin.yaml", contents: "name: pushary\nversion: 0.8.1\ndescription: Push notifications and human-in-the-loop for Hermes Agent via Pushary\nauthor: RalphNex OU\nrequires_env:\n - name: PUSHARY_API_KEY\n description: Your Pushary API key\n url: https://pushary.com/sign-up?from=hermes\n secret: true\nprovides_tools:\n - pushary_remind\n - pushary_notify\n - pushary_ask\n - pushary_wait\n - pushary_cancel\n - pushary_propose_scope\n - pushary_enroll\n - pushary_ask_end_user\nprovides_hooks:\n - pre_llm_call\n - pre_tool_call\n - post_tool_call\n - on_session_start\n - on_session_end\n - on_session_finalize\nprovides_approval_transports:\n - pushary\n" },
1324
+ { path: "plugin.yaml", contents: "name: pushary\nversion: 0.9.0\ndescription: Push notifications and human-in-the-loop for Hermes Agent via Pushary\nauthor: RalphNex OU\nrequires_env:\n - name: PUSHARY_API_KEY\n description: Your Pushary API key\n url: https://pushary.com/sign-up?from=hermes\n secret: true\nprovides_tools:\n - pushary_remind\n - pushary_notify\n - pushary_ask\n - pushary_wait\n - pushary_cancel\n - pushary_propose_scope\n - pushary_enroll\n - pushary_ask_end_user\nprovides_hooks:\n - pre_llm_call\n - pre_tool_call\n - post_tool_call\n - on_session_start\n - on_session_end\n - on_session_finalize\nprovides_approval_transports:\n - pushary\n" },
1067
1325
  { path: "schemas.py", contents: `PUSHARY_REMIND = {
1068
1326
  "name": "pushary_remind",
1069
1327
  "description": "Schedule a one-time personal reminder on Pushary, even after Hermes exits. Use body plus in_minutes or at (ISO timestamp with explicit UTC offset, within 30 days). Never guess the user's timezone. Confirm the returned time. No arguments lists pending reminders; cancel_reminder_id cancels a scheduled one. Use Hermes cron for agent work; use this for reminding the operator.",
@@ -1670,6 +1928,8 @@ var HERMES_INSTALL_MARKER_OWNER = "pushary";
1670
1928
  var HERMES_LOAD_RECORDS_PATH = "plugin-data/pushary/loaded";
1671
1929
  var HERMES_LEGACY_PIP_PACKAGE = "hermes-plugin-pushary";
1672
1930
  var HERMES_QUESTION_TOOLSET = "clarify";
1931
+ var HERMES_PLUGINS_SECTION = "plugins";
1932
+ var HERMES_APPROVAL_SECTION = "security.approval";
1673
1933
  var HERMES_APPROVAL_TRANSPORT_KEY = "security.approval.transport";
1674
1934
  var HERMES_APPROVAL_FALLBACK_KEY = "security.approval.transport_fallback";
1675
1935
  var HERMES_PLUGINS_ENABLED_KEY = "plugins.enabled";
@@ -1707,15 +1967,113 @@ var shippedHermesPlugin = (files = HERMES_PLUGIN_FILES) => {
1707
1967
  };
1708
1968
 
1709
1969
  // src/hermes/runtime.ts
1710
- import { execFileSync } from "child_process";
1970
+ import { execFile } from "child_process";
1711
1971
  import { accessSync, closeSync, constants, existsSync, mkdirSync, openSync, readFileSync, readSync, renameSync, rmSync, statSync, writeFileSync } from "fs";
1712
1972
  import { homedir } from "os";
1713
1973
  import { basename, dirname, isAbsolute, join } from "path";
1974
+ import { promisify } from "util";
1975
+
1976
+ // src/hermes/setup-receipt.ts
1977
+ var HERMES_SETUP_RECEIPT_VERSION = 2;
1978
+ var settingSnapshot = (key, reading) => reading.kind === "set" ? { key, wasSet: true, before: reading.value } : { key, wasSet: false, before: null };
1979
+ var isSettingSnapshot = (value) => isRecord(value) && typeof value.key === "string" && typeof value.wasSet === "boolean" && Object.hasOwn(value, "before");
1980
+ var parseCurrentReceipt = (value) => {
1981
+ const { settings, pluginWasEnabled, questionRoutingAdded } = value;
1982
+ if (!Array.isArray(settings) || !settings.every(isSettingSnapshot)) return null;
1983
+ if (typeof pluginWasEnabled !== "boolean" || typeof questionRoutingAdded !== "boolean") return null;
1984
+ return { settings, pluginWasEnabled, questionRoutingAdded };
1985
+ };
1986
+ var LEGACY_SNAPSHOT_KEYS = ["security", "approval", "transport", "fallback", "plugins", "enabled", "agent", "disabled"];
1987
+ var isLegacySnapshot = (value) => isRecord(value) && typeof value.present === "boolean" && Object.hasOwn(value, "value");
1988
+ var legacySetting = (key, snapshot) => ({ key, wasSet: snapshot.present, before: snapshot.present ? snapshot.value : null });
1989
+ var parseLegacyReceipt = (value) => {
1990
+ if (!LEGACY_SNAPSHOT_KEYS.every((key) => isLegacySnapshot(value[key]))) return null;
1991
+ if (typeof value.pushary_enabled !== "boolean" || typeof value.clarify_disabled !== "boolean") return null;
1992
+ const transport = value.transport;
1993
+ const fallback = value.fallback;
1994
+ if (!isLegacySnapshot(transport) || !isLegacySnapshot(fallback)) return null;
1995
+ return {
1996
+ settings: [legacySetting(HERMES_APPROVAL_TRANSPORT_KEY, transport), legacySetting(HERMES_APPROVAL_FALLBACK_KEY, fallback)],
1997
+ pluginWasEnabled: value.pushary_enabled,
1998
+ questionRoutingAdded: !value.clarify_disabled
1999
+ };
2000
+ };
2001
+ var parseHermesSetupReceipt = (value) => {
2002
+ if (!isRecord(value)) return null;
2003
+ return value.version === HERMES_SETUP_RECEIPT_VERSION ? parseCurrentReceipt(value) : parseLegacyReceipt(value);
2004
+ };
2005
+ var serializeHermesSetupReceipt = (receipt) => ({ version: HERMES_SETUP_RECEIPT_VERSION, ...receipt });
2006
+ var receiptWithoutSnapshots = () => ({
2007
+ settings: [
2008
+ { key: HERMES_APPROVAL_TRANSPORT_KEY, wasSet: false, before: null },
2009
+ { key: HERMES_APPROVAL_FALLBACK_KEY, wasSet: false, before: null }
2010
+ ],
2011
+ pluginWasEnabled: false,
2012
+ questionRoutingAdded: false
2013
+ });
2014
+ var receiptAfterQuestionRoutingReturned = (receipt) => ({ ...receipt, questionRoutingAdded: false });
2015
+ var PUSHARY_VALUES = {
2016
+ [HERMES_APPROVAL_TRANSPORT_KEY]: HERMES_PUSHARY_TRANSPORT,
2017
+ [HERMES_APPROVAL_FALLBACK_KEY]: HERMES_PUSHARY_FALLBACK
2018
+ };
2019
+ var hermesSettingRestore = (snapshot, current) => {
2020
+ const ours = PUSHARY_VALUES[snapshot.key];
2021
+ const stillOurs = current.kind === "set" && current.value === ours;
2022
+ const oursBeforeSetup = snapshot.wasSet && snapshot.before === ours;
2023
+ return stillOurs && !oursBeforeSetup ? { kind: "restore", key: snapshot.key, snapshot } : { kind: "leave", key: snapshot.key };
2024
+ };
2025
+ var sameSettingValue = (left, right) => JSON.stringify(left) === JSON.stringify(right);
2026
+
2027
+ // src/hermes/config-sections.ts
2028
+ var HERMES_CONFIG_SECTION_KEYS = [HERMES_PLUGINS_SECTION, HERMES_APPROVAL_SECTION, HERMES_DISABLED_TOOLSETS_KEY];
2029
+ var HERMES_SNAPSHOT_KEYS = [HERMES_APPROVAL_TRANSPORT_KEY, HERMES_APPROVAL_FALLBACK_KEY];
2030
+ var KEY_SEPARATOR = ".";
2031
+ var UNSET = { kind: "unset" };
2032
+ var childReading = (reading, name) => reading.kind === "set" && isRecord(reading.value) && Object.hasOwn(reading.value, name) ? { kind: "set", value: reading.value[name] } : UNSET;
2033
+ var sectionHolding = (key) => HERMES_CONFIG_SECTION_KEYS.find((section) => key === section || key.startsWith(`${section}${KEY_SEPARATOR}`)) ?? null;
2034
+ var readingFromSections = (sections, key) => {
2035
+ const section = sectionHolding(key);
2036
+ if (section === null) return null;
2037
+ const path = key === section ? [] : key.slice(section.length + KEY_SEPARATOR.length).split(KEY_SEPARATOR);
2038
+ return path.reduce(childReading, sections[section]);
2039
+ };
2040
+ var stringList = (value) => Array.isArray(value) ? value.filter((item) => typeof item === "string") : [];
2041
+ var readingValue = (reading) => reading.kind === "set" ? reading.value : void 0;
2042
+ var stringOrNull = (value) => typeof value === "string" ? value : null;
2043
+ var hermesSettingsFrom = (sections) => {
2044
+ const plugins = readingValue(sections[HERMES_PLUGINS_SECTION]);
2045
+ const approval = readingValue(sections[HERMES_APPROVAL_SECTION]);
2046
+ const enabled = isRecord(plugins) ? stringList(plugins.enabled) : [];
2047
+ const disabled = isRecord(plugins) ? stringList(plugins.disabled) : [];
2048
+ return {
2049
+ pluginEnabled: enabled.includes(HERMES_PLUGIN_KEY),
2050
+ pluginDisabled: disabled.includes(HERMES_PLUGIN_KEY),
2051
+ approvalTransport: isRecord(approval) ? stringOrNull(approval.transport) : null,
2052
+ approvalFallback: isRecord(approval) ? stringOrNull(approval.transport_fallback) : null,
2053
+ disabledToolsets: stringList(readingValue(sections[HERMES_DISABLED_TOOLSETS_KEY]))
2054
+ };
2055
+ };
2056
+ var pluginNeedsEnabling = (settings) => !settings.pluginEnabled || settings.pluginDisabled;
2057
+ var snapshotFrom = (sections) => (key) => {
2058
+ const reading = readingFromSections(sections, key);
2059
+ return reading === null ? null : settingSnapshot(key, reading);
2060
+ };
2061
+ var isSnapshot = (snapshot) => snapshot !== null;
2062
+ var setupReceiptFrom = (sections) => {
2063
+ const snapshots = HERMES_SNAPSHOT_KEYS.map(snapshotFrom(sections));
2064
+ if (!snapshots.every(isSnapshot)) return null;
2065
+ return { settings: snapshots, pluginWasEnabled: hermesSettingsFrom(sections).pluginEnabled, questionRoutingAdded: false };
2066
+ };
2067
+ var holdsValue = (reading, value) => reading?.kind === "set" && reading.value === value;
2068
+ var pusharySettingsToWrite = (sections) => HERMES_PUSHARY_SETTINGS.filter((setting) => !holdsValue(sections === null ? null : readingFromSections(sections, setting.key), setting.value));
2069
+
2070
+ // src/hermes/runtime.ts
1714
2071
  var HERMES_COMMAND_TIMEOUT_MS = 2e4;
2072
+ var HERMES_PLUGIN_COMMAND_TIMEOUT_MS = 18e4;
2073
+ var HERMES_OUTPUT_LIMIT_BYTES = 1024 * 1024;
1715
2074
  var LEGACY_PIP_TIMEOUT_MS = 6e4;
2075
+ var PIP_UNINSTALLED = "Successfully uninstalled";
1716
2076
  var HERMES_SETTING_NOT_SET = "Config key not set";
1717
- var HERMES_PLUGINS_SECTION = "plugins";
1718
- var HERMES_APPROVAL_SECTION = "security.approval";
1719
2077
  var INSTALL_METADATA_FILE = ".install-metadata.json";
1720
2078
  var GATEWAY_STATE_FILE = "gateway_state.json";
1721
2079
  var GATEWAY_PID_FILE = "gateway.pid";
@@ -1743,22 +2101,26 @@ var commandOutput = (error) => {
1743
2101
  if (!isRecord(error)) return String(error);
1744
2102
  return [error.stdout, error.stderr].map((part) => part === void 0 || part === null ? "" : String(part)).join("\n");
1745
2103
  };
1746
- var runHermesCommand = (launcher, args) => {
2104
+ var stoppedAtTimeout = (error) => isRecord(error) && error.killed === true;
2105
+ var runFileAsync = promisify(execFile);
2106
+ var runHermesCommand = async (launcher, args, timeoutMs = HERMES_COMMAND_TIMEOUT_MS) => {
1747
2107
  const shell = needsShell(launcher.path);
1748
2108
  try {
1749
- const stdout = execFileSync(
2109
+ const running = runFileAsync(
1750
2110
  shell ? quotedForWindowsShell(launcher.path) : launcher.path,
1751
2111
  shell ? args.map(quotedForWindowsShell) : [...args],
1752
- { encoding: "utf-8", stdio: ["ignore", "pipe", "pipe"], timeout: HERMES_COMMAND_TIMEOUT_MS, shell }
2112
+ { encoding: "utf-8", timeout: timeoutMs, maxBuffer: HERMES_OUTPUT_LIMIT_BYTES, shell, windowsHide: true }
1753
2113
  );
2114
+ running.child.stdin?.end();
2115
+ const { stdout } = await running;
1754
2116
  return { kind: "ok", stdout };
1755
2117
  } catch (error) {
1756
- return { kind: "failed", output: commandOutput(error) };
2118
+ return { kind: "failed", output: commandOutput(error), timedOut: stoppedAtTimeout(error) };
1757
2119
  }
1758
2120
  };
1759
2121
  var lastLine = (output) => output.trim().split(/\r?\n/).filter((line) => line.trim().length > 0).at(-1)?.trim() ?? "";
1760
- var readHermesHome = (launcher) => {
1761
- const result = runHermesCommand(launcher, ["config", "path"]);
2122
+ var readHermesHome = async (launcher) => {
2123
+ const result = await runHermesCommand(launcher, ["config", "path"]);
1762
2124
  if (result.kind !== "ok") return null;
1763
2125
  const configPath = lastLine(result.stdout);
1764
2126
  return isAbsolute(configPath) ? dirname(configPath) : null;
@@ -1770,42 +2132,35 @@ var parsedJson = (text) => {
1770
2132
  return { ok: false };
1771
2133
  }
1772
2134
  };
1773
- var readHermesSetting = (launcher, key) => {
1774
- const result = runHermesCommand(launcher, ["config", "get", key, "--json"]);
2135
+ var readHermesSetting = async (launcher, key) => {
2136
+ const result = await runHermesCommand(launcher, ["config", "get", key, "--json"]);
1775
2137
  if (result.kind === "failed") return result.output.includes(HERMES_SETTING_NOT_SET) ? { kind: "unset" } : null;
1776
2138
  const parsed = parsedJson(lastLine(result.stdout));
1777
2139
  return parsed.ok ? { kind: "set", value: parsed.value } : null;
1778
2140
  };
1779
2141
  var settingArgument = (value) => typeof value === "string" ? value : JSON.stringify(value);
1780
- var writeHermesSetting = (launcher, key, value) => runHermesCommand(launcher, ["config", "set", key, settingArgument(value)]).kind === "ok";
1781
- var unsetHermesSetting = (launcher, key) => {
1782
- const result = runHermesCommand(launcher, ["config", "unset", key]);
2142
+ var writeHermesSetting = async (launcher, key, value) => (await runHermesCommand(launcher, ["config", "set", key, settingArgument(value)])).kind === "ok";
2143
+ var unsetHermesSetting = async (launcher, key) => {
2144
+ const result = await runHermesCommand(launcher, ["config", "unset", key]);
1783
2145
  return result.kind === "ok" || result.output.includes(HERMES_SETTING_NOT_SET);
1784
2146
  };
1785
- var enableHermesPlugin = (launcher) => runHermesCommand(launcher, ["plugins", "enable", HERMES_PLUGIN_KEY]).kind === "ok";
1786
- var disableHermesPlugin = (launcher) => runHermesCommand(launcher, ["plugins", "disable", HERMES_PLUGIN_KEY]).kind === "ok";
1787
- var removeHermesPlugin = (launcher) => runHermesCommand(launcher, ["plugins", "remove", HERMES_PLUGIN_KEY]).kind === "ok";
1788
- var stringList = (value) => Array.isArray(value) ? value.filter((item) => typeof item === "string") : [];
1789
- var readingValue = (reading) => reading.kind === "set" ? reading.value : void 0;
1790
- var stringOrNull = (value) => typeof value === "string" ? value : null;
1791
- var readHermesSettings = (launcher) => {
1792
- const plugins = readHermesSetting(launcher, HERMES_PLUGINS_SECTION);
1793
- const approval = readHermesSetting(launcher, HERMES_APPROVAL_SECTION);
1794
- const toolsets = readHermesSetting(launcher, HERMES_DISABLED_TOOLSETS_KEY);
1795
- if (!plugins || !approval || !toolsets) return null;
1796
- const pluginSection = readingValue(plugins);
1797
- const approvalSection = readingValue(approval);
1798
- const enabled = isRecord(pluginSection) ? stringList(pluginSection.enabled) : [];
1799
- const disabled = isRecord(pluginSection) ? stringList(pluginSection.disabled) : [];
1800
- return {
1801
- pluginEnabled: enabled.includes(HERMES_PLUGIN_KEY),
1802
- pluginDisabled: disabled.includes(HERMES_PLUGIN_KEY),
1803
- approvalTransport: isRecord(approvalSection) ? stringOrNull(approvalSection.transport) : null,
1804
- approvalFallback: isRecord(approvalSection) ? stringOrNull(approvalSection.transport_fallback) : null,
1805
- disabledToolsets: stringList(readingValue(toolsets))
1806
- };
2147
+ var runHermesPluginCommand = (launcher, verb) => runHermesCommand(launcher, ["plugins", verb, HERMES_PLUGIN_KEY], HERMES_PLUGIN_COMMAND_TIMEOUT_MS);
2148
+ var pluginCommandSucceeded = async (launcher, verb) => (await runHermesPluginCommand(launcher, verb)).kind === "ok";
2149
+ var pluginEnablingFrom = (result) => {
2150
+ if (result.kind === "ok") return "enabled";
2151
+ return result.timedOut ? "timed_out" : "refused";
2152
+ };
2153
+ var enableHermesPlugin = async (launcher) => pluginEnablingFrom(await runHermesPluginCommand(launcher, "enable"));
2154
+ var disableHermesPlugin = (launcher) => pluginCommandSucceeded(launcher, "disable");
2155
+ var removeHermesPlugin = (launcher) => pluginCommandSucceeded(launcher, "remove");
2156
+ var readHermesConfigSections = async (launcher) => {
2157
+ const [plugins, approval, disabledToolsets] = await Promise.all([
2158
+ readHermesSetting(launcher, HERMES_PLUGINS_SECTION),
2159
+ readHermesSetting(launcher, HERMES_APPROVAL_SECTION),
2160
+ readHermesSetting(launcher, HERMES_DISABLED_TOOLSETS_KEY)
2161
+ ]);
2162
+ return plugins && approval && disabledToolsets ? { [HERMES_PLUGINS_SECTION]: plugins, [HERMES_APPROVAL_SECTION]: approval, [HERMES_DISABLED_TOOLSETS_KEY]: disabledToolsets } : null;
1807
2163
  };
1808
- var HERMES_SNAPSHOT_KEYS = [HERMES_APPROVAL_TRANSPORT_KEY, HERMES_APPROVAL_FALLBACK_KEY];
1809
2164
  var hermesPluginDirectory = (home) => join(home, "plugins", HERMES_PLUGIN_KEY);
1810
2165
  var HERMES_ROOT_DIRECTORY = ".hermes";
1811
2166
  var HERMES_CONFIG_FILE = "config.yaml";
@@ -1945,10 +2300,16 @@ var legacyHermesPython = (launcher) => {
1945
2300
  return null;
1946
2301
  }
1947
2302
  };
1948
- var removeLegacyPipCopy = (python, distribution) => {
2303
+ var removeLegacyPipCopy = async (python, distribution) => {
1949
2304
  try {
1950
- execFileSync(python, ["-m", "pip", "uninstall", "-y", distribution], { stdio: "ignore", timeout: LEGACY_PIP_TIMEOUT_MS });
1951
- return true;
2305
+ const running = runFileAsync(
2306
+ python,
2307
+ ["-m", "pip", "uninstall", "-y", distribution],
2308
+ { encoding: "utf-8", timeout: LEGACY_PIP_TIMEOUT_MS, maxBuffer: HERMES_OUTPUT_LIMIT_BYTES, windowsHide: true }
2309
+ );
2310
+ running.child.stdin?.end();
2311
+ const { stdout } = await running;
2312
+ return stdout.includes(PIP_UNINSTALLED);
1952
2313
  } catch {
1953
2314
  return false;
1954
2315
  }
@@ -2029,72 +2390,25 @@ var toolsetsWithoutPusharyQuestionRouting = (disabledToolsets, questionRoutingAd
2029
2390
 
2030
2391
  // src/hermes/install.ts
2031
2392
  import { existsSync as existsSync2, rmSync as rmSync2 } from "fs";
2032
-
2033
- // src/hermes/setup-receipt.ts
2034
- var HERMES_SETUP_RECEIPT_VERSION = 2;
2035
- var settingSnapshot = (key, reading) => reading.kind === "set" ? { key, wasSet: true, before: reading.value } : { key, wasSet: false, before: null };
2036
- var isSettingSnapshot = (value) => isRecord(value) && typeof value.key === "string" && typeof value.wasSet === "boolean" && Object.hasOwn(value, "before");
2037
- var parseCurrentReceipt = (value) => {
2038
- const { settings, pluginWasEnabled, questionRoutingAdded } = value;
2039
- if (!Array.isArray(settings) || !settings.every(isSettingSnapshot)) return null;
2040
- if (typeof pluginWasEnabled !== "boolean" || typeof questionRoutingAdded !== "boolean") return null;
2041
- return { settings, pluginWasEnabled, questionRoutingAdded };
2042
- };
2043
- var LEGACY_SNAPSHOT_KEYS = ["security", "approval", "transport", "fallback", "plugins", "enabled", "agent", "disabled"];
2044
- var isLegacySnapshot = (value) => isRecord(value) && typeof value.present === "boolean" && Object.hasOwn(value, "value");
2045
- var legacySetting = (key, snapshot) => ({ key, wasSet: snapshot.present, before: snapshot.present ? snapshot.value : null });
2046
- var parseLegacyReceipt = (value) => {
2047
- if (!LEGACY_SNAPSHOT_KEYS.every((key) => isLegacySnapshot(value[key]))) return null;
2048
- if (typeof value.pushary_enabled !== "boolean" || typeof value.clarify_disabled !== "boolean") return null;
2049
- const transport = value.transport;
2050
- const fallback = value.fallback;
2051
- if (!isLegacySnapshot(transport) || !isLegacySnapshot(fallback)) return null;
2052
- return {
2053
- settings: [legacySetting(HERMES_APPROVAL_TRANSPORT_KEY, transport), legacySetting(HERMES_APPROVAL_FALLBACK_KEY, fallback)],
2054
- pluginWasEnabled: value.pushary_enabled,
2055
- questionRoutingAdded: !value.clarify_disabled
2056
- };
2057
- };
2058
- var parseHermesSetupReceipt = (value) => {
2059
- if (!isRecord(value)) return null;
2060
- return value.version === HERMES_SETUP_RECEIPT_VERSION ? parseCurrentReceipt(value) : parseLegacyReceipt(value);
2061
- };
2062
- var serializeHermesSetupReceipt = (receipt) => ({ version: HERMES_SETUP_RECEIPT_VERSION, ...receipt });
2063
- var receiptWithoutSnapshots = () => ({
2064
- settings: [
2065
- { key: HERMES_APPROVAL_TRANSPORT_KEY, wasSet: false, before: null },
2066
- { key: HERMES_APPROVAL_FALLBACK_KEY, wasSet: false, before: null }
2067
- ],
2068
- pluginWasEnabled: false,
2069
- questionRoutingAdded: false
2070
- });
2071
- var receiptAfterQuestionRoutingReturned = (receipt) => ({ ...receipt, questionRoutingAdded: false });
2072
- var PUSHARY_VALUES = {
2073
- [HERMES_APPROVAL_TRANSPORT_KEY]: HERMES_PUSHARY_TRANSPORT,
2074
- [HERMES_APPROVAL_FALLBACK_KEY]: HERMES_PUSHARY_FALLBACK
2075
- };
2076
- var hermesSettingRestore = (snapshot, current) => {
2077
- const ours = PUSHARY_VALUES[snapshot.key];
2078
- const stillOurs = current.kind === "set" && current.value === ours;
2079
- const oursBeforeSetup = snapshot.wasSet && snapshot.before === ours;
2080
- return stillOurs && !oursBeforeSetup ? { kind: "restore", key: snapshot.key, snapshot } : { kind: "leave", key: snapshot.key };
2081
- };
2082
- var sameSettingValue = (left, right) => JSON.stringify(left) === JSON.stringify(right);
2083
-
2084
- // src/hermes/install.ts
2085
2393
  var RECEIPT_MODE = 384;
2086
- var inspectWithLauncher = (launcher) => {
2087
- const home = readHermesHome(launcher);
2088
- const settings = home ? readHermesSettings(launcher) : null;
2089
- if (!home || !settings) return { kind: "unreadable", home };
2394
+ var inspectionFrom = (launcher, home, sections) => {
2395
+ const settings = hermesSettingsFrom(sections);
2090
2396
  const folder = classifyHermesPluginFolder(readHermesPluginFolderFacts(home));
2091
2397
  const gateway = readHermesGateway(home);
2092
2398
  const gatewayLoad = gateway.kind === "running" ? readHermesLoadRecord(home, gateway.pid) : null;
2093
2399
  const state = hermesWiringState({ folder, settings, gateway, gatewayLoad });
2094
2400
  return { kind: "inspected", launcher, home, folder, settings, gateway, state };
2095
2401
  };
2402
+ var inspectAt = async (launcher, home) => {
2403
+ const sections = await readHermesConfigSections(launcher);
2404
+ return sections ? inspectionFrom(launcher, home, sections) : { kind: "unreadable", home };
2405
+ };
2406
+ var inspectWithLauncher = async (launcher) => {
2407
+ const [home, sections] = await Promise.all([readHermesHome(launcher), readHermesConfigSections(launcher)]);
2408
+ return home && sections ? inspectionFrom(launcher, home, sections) : { kind: "unreadable", home };
2409
+ };
2096
2410
  var findLauncherWhereHermesLives = () => existsSync2(defaultHermesHome()) ? findHermesLauncher() : null;
2097
- var inspectHermesWiring = (launcher) => {
2411
+ var inspectHermesWiring = async (launcher) => {
2098
2412
  const resolved = launcher === void 0 ? findLauncherWhereHermesLives() : launcher;
2099
2413
  return resolved ? inspectWithLauncher(resolved) : { kind: "no_launcher" };
2100
2414
  };
@@ -2124,67 +2438,62 @@ var removeSetupReceipts = (receipts) => {
2124
2438
  rmSync2(receipts.receiptPath, { force: true });
2125
2439
  rmSync2(receipts.legacyReceiptPath, { force: true });
2126
2440
  };
2127
- var snapshotSettings = (launcher) => {
2128
- const settings = readHermesSettings(launcher);
2129
- if (!settings) return null;
2130
- const snapshots = [];
2131
- for (const key of HERMES_SNAPSHOT_KEYS) {
2132
- const reading = readHermesSetting(launcher, key);
2133
- if (!reading) return null;
2134
- snapshots.push(settingSnapshot(key, reading));
2135
- }
2136
- return {
2137
- settings: snapshots,
2138
- pluginWasEnabled: settings.pluginEnabled,
2139
- questionRoutingAdded: false
2140
- };
2141
- };
2142
- var ensureSetupReceipt = (launcher, receipts) => {
2143
- const existing = readSetupReceipt(receipts);
2144
- if (existing.kind === "invalid") return { kind: "failed", reason: "receipt_invalid" };
2145
- const receipt = existing.kind === "ok" ? existing.receipt : snapshotSettings(launcher);
2441
+ var ensureSetupReceipt = (receipts, existing, sections) => {
2442
+ const receipt = existing.kind === "ok" ? existing.receipt : sections && setupReceiptFrom(sections);
2146
2443
  if (!receipt) return { kind: "failed", reason: "settings_unreadable" };
2147
2444
  if (!existsSync2(receipts.receiptPath)) writeSetupReceipt(receipts, receipt);
2148
2445
  return { kind: "ok", receipt };
2149
2446
  };
2150
- var settingNeedsWrite = (launcher, key, value) => {
2151
- const current = readHermesSetting(launcher, key);
2152
- return !(current?.kind === "set" && current.value === value);
2447
+ var ENABLING_FAILURES = {
2448
+ refused: "settings_failed",
2449
+ timed_out: "plugin_load_timed_out"
2450
+ };
2451
+ var writeSettingsInTurn = async (launcher, settings) => {
2452
+ for (const setting of settings) {
2453
+ if (!await writeHermesSetting(launcher, setting.key, setting.value)) return false;
2454
+ }
2455
+ return true;
2153
2456
  };
2154
- var applyPusharySettings = (launcher) => {
2155
- if (!enableHermesPlugin(launcher)) return false;
2156
- return HERMES_PUSHARY_SETTINGS.every((setting) => !settingNeedsWrite(launcher, setting.key, setting.value) || writeHermesSetting(launcher, setting.key, setting.value));
2457
+ var applyPusharySettings = async (launcher, sections) => {
2458
+ const enabling = await enableHermesPlugin(launcher);
2459
+ if (enabling !== "enabled") return { kind: "failed", reason: ENABLING_FAILURES[enabling] };
2460
+ const writes = pusharySettingsToWrite(sections);
2461
+ if (!await writeSettingsInTurn(launcher, writes)) return { kind: "failed", reason: "settings_failed" };
2462
+ const enableChangedConfig = sections === null || pluginNeedsEnabling(hermesSettingsFrom(sections));
2463
+ return { kind: "applied", changedHermes: enableChangedConfig || writes.length > 0 };
2157
2464
  };
2158
- var writeListSetting = (launcher, key, list) => {
2465
+ var writeListSetting = async (launcher, key, list) => {
2159
2466
  if (list.length > 0) return writeHermesSetting(launcher, key, list);
2160
- if (!unsetHermesSetting(launcher, key)) return false;
2161
- const after = readHermesSetting(launcher, key);
2467
+ if (!await unsetHermesSetting(launcher, key)) return false;
2468
+ const after = await readHermesSetting(launcher, key);
2162
2469
  return after?.kind !== "set" || sameSettingValue(after.value, []) || writeHermesSetting(launcher, key, []);
2163
2470
  };
2164
- var returnQuestionsToHermes = (launcher, receipt) => {
2165
- const toolsets = readHermesSetting(launcher, HERMES_DISABLED_TOOLSETS_KEY);
2471
+ var returnQuestionsToHermes = async (launcher, receipt) => {
2472
+ const toolsets = await readHermesSetting(launcher, HERMES_DISABLED_TOOLSETS_KEY);
2166
2473
  if (!toolsets) return false;
2167
2474
  const current = toolsets.kind === "set" && Array.isArray(toolsets.value) ? toolsets.value.filter((item) => typeof item === "string") : [];
2168
2475
  const next = toolsetsWithoutPusharyQuestionRouting(current, receipt.questionRoutingAdded);
2169
2476
  return next === null || writeListSetting(launcher, HERMES_DISABLED_TOOLSETS_KEY, next);
2170
2477
  };
2171
- var removeLegacyCopy = (launcher) => {
2478
+ var removeLegacyCopy = async (launcher) => {
2172
2479
  const python = legacyHermesPython(launcher);
2173
2480
  return python !== null && removeLegacyPipCopy(python, HERMES_LEGACY_PIP_PACKAGE);
2174
2481
  };
2175
- var returnQuestionsIfThePluginRoutesThem = (launcher, home, receipts, receipt) => {
2482
+ var returnQuestionsIfThePluginRoutesThem = async (launcher, home, receipts, receipt) => {
2176
2483
  const folder = classifyHermesPluginFolder(readHermesPluginFolderFacts(home));
2177
- if (!receipt.questionRoutingAdded || !routesHermesQuestions(folder)) return true;
2178
- if (!returnQuestionsToHermes(launcher, receipt)) return false;
2484
+ if (!receipt.questionRoutingAdded || !routesHermesQuestions(folder)) return "unchanged";
2485
+ if (!await returnQuestionsToHermes(launcher, receipt)) return "failed";
2179
2486
  writeSetupReceipt(receipts, receiptAfterQuestionRoutingReturned(receipt));
2180
- return true;
2487
+ return "returned";
2181
2488
  };
2182
- var installHermesPlugin = (request) => {
2489
+ var installHermesPlugin = async (request) => {
2183
2490
  const launcher = request.launcher === void 0 ? findHermesLauncher() : request.launcher;
2184
2491
  if (!launcher) return { kind: "no_launcher" };
2185
- const home = readHermesHome(launcher);
2492
+ const existingReceipt = readSetupReceipt(request.receipts);
2493
+ if (existingReceipt.kind === "invalid") return { kind: "failed", reason: "receipt_invalid" };
2494
+ const [home, sections] = await Promise.all([readHermesHome(launcher), readHermesConfigSections(launcher)]);
2186
2495
  if (!home) return { kind: "failed", reason: "home_unknown" };
2187
- const ensured = ensureSetupReceipt(launcher, request.receipts);
2496
+ const ensured = ensureSetupReceipt(request.receipts, existingReceipt, sections);
2188
2497
  if (ensured.kind === "failed") return ensured;
2189
2498
  const folder = classifyHermesPluginFolder(readHermesPluginFolderFacts(home));
2190
2499
  if (folder.kind === "unknown") return { kind: "failed", reason: "plugin_folder_not_ours" };
@@ -2196,12 +2505,14 @@ var installHermesPlugin = (request) => {
2196
2505
  return { kind: "failed", reason: "plugin_write_failed" };
2197
2506
  }
2198
2507
  }
2199
- if (!applyPusharySettings(launcher)) return { kind: "failed", reason: "settings_failed" };
2200
- if (!returnQuestionsIfThePluginRoutesThem(launcher, home, request.receipts, ensured.receipt)) {
2201
- return { kind: "failed", reason: "settings_failed" };
2202
- }
2203
- const legacyCopyRemoved = removeLegacyCopy(launcher);
2204
- return { kind: "installed", folderWritten, legacyCopyRemoved, inspection: inspectWithLauncher(launcher) };
2508
+ const applied = await applyPusharySettings(launcher, sections);
2509
+ if (applied.kind === "failed") return { kind: "failed", reason: applied.reason };
2510
+ const questions = await returnQuestionsIfThePluginRoutesThem(launcher, home, request.receipts, ensured.receipt);
2511
+ if (questions === "failed") return { kind: "failed", reason: "settings_failed" };
2512
+ const legacyCopyRemoved = await removeLegacyCopy(launcher);
2513
+ const configUnchanged = !applied.changedHermes && questions === "unchanged";
2514
+ const inspection = sections !== null && configUnchanged ? inspectionFrom(launcher, home, sections) : await inspectAt(launcher, home);
2515
+ return { kind: "installed", folderWritten, legacyCopyRemoved, inspection };
2205
2516
  };
2206
2517
  var refreshHermesPluginFiles = (installedBy, plugin, home = defaultHermesHome()) => {
2207
2518
  const folder = classifyHermesPluginFolder(readHermesPluginFolderFacts(home));
@@ -2215,9 +2526,9 @@ var refreshHermesPluginFiles = (installedBy, plugin, home = defaultHermesHome())
2215
2526
  }
2216
2527
  };
2217
2528
  var macAppOwnsHermes = (folder, request) => hermesWiringOwner(folder, existsSync2(request.macAppWiringRecord)) === "mac_app";
2218
- var removePluginEntry = (launcher, settings) => {
2219
- if (!settings.pluginEnabled || disableHermesPlugin(launcher)) return true;
2220
- const enabled = readHermesSetting(launcher, HERMES_PLUGINS_ENABLED_KEY);
2529
+ var removePluginEntry = async (launcher, settings) => {
2530
+ if (!settings.pluginEnabled || await disableHermesPlugin(launcher)) return true;
2531
+ const enabled = await readHermesSetting(launcher, HERMES_PLUGINS_ENABLED_KEY);
2221
2532
  const list = enabled?.kind === "set" && Array.isArray(enabled.value) ? enabled.value : [];
2222
2533
  return writeListSetting(
2223
2534
  launcher,
@@ -2225,32 +2536,38 @@ var removePluginEntry = (launcher, settings) => {
2225
2536
  list.filter((item) => typeof item === "string" && item !== HERMES_PLUGIN_KEY)
2226
2537
  );
2227
2538
  };
2228
- var removePushPlugin = (launcher, home, folder, settings, receipt) => {
2229
- removeLegacyCopy(launcher);
2539
+ var removePushPlugin = async (launcher, home, folder, settings, receipt) => {
2540
+ await removeLegacyCopy(launcher);
2230
2541
  if (folder.kind === "pushary") {
2231
- if (removeHermesPlugin(launcher)) return true;
2542
+ if (await removeHermesPlugin(launcher)) return true;
2232
2543
  rmSync2(hermesPluginDirectory(home), { recursive: true, force: true });
2233
2544
  return removePluginEntry(launcher, settings);
2234
2545
  }
2235
2546
  return receipt.pluginWasEnabled || removePluginEntry(launcher, settings);
2236
2547
  };
2237
- var restoreSetting = (launcher, snapshot) => {
2238
- const current = readHermesSetting(launcher, snapshot.key);
2548
+ var restoreSetting = async (launcher, snapshot) => {
2549
+ const current = await readHermesSetting(launcher, snapshot.key);
2239
2550
  if (!current) return false;
2240
2551
  if (hermesSettingRestore(snapshot, current).kind === "leave") return true;
2241
- if (!unsetHermesSetting(launcher, snapshot.key)) return false;
2552
+ if (!await unsetHermesSetting(launcher, snapshot.key)) return false;
2242
2553
  if (!snapshot.wasSet) return true;
2243
- const after = readHermesSetting(launcher, snapshot.key);
2554
+ const after = await readHermesSetting(launcher, snapshot.key);
2244
2555
  return after?.kind === "set" && sameSettingValue(after.value, snapshot.before) ? true : writeHermesSetting(launcher, snapshot.key, snapshot.before);
2245
2556
  };
2246
2557
  var macAppOwnsTheFolderOnDisk = (request) => macAppOwnsHermes(classifyHermesPluginFolder(readHermesPluginFolderFacts(request.home ?? defaultHermesHome())), request);
2247
- var removeHermesWiring = (request) => {
2558
+ var restoreSettingsInTurn = async (launcher, snapshots) => {
2559
+ for (const snapshot of snapshots) {
2560
+ if (!await restoreSetting(launcher, snapshot)) return false;
2561
+ }
2562
+ return true;
2563
+ };
2564
+ var removeHermesWiring = async (request) => {
2248
2565
  if (request.keepMacAppWiring && macAppOwnsTheFolderOnDisk(request)) return { kind: "kept_for_mac_app" };
2249
2566
  const receiptRead = readSetupReceipt(request.receipts);
2250
2567
  const launcher = request.launcher === void 0 ? receiptRead.kind === "missing" ? findLauncherWhereHermesLives() : findHermesLauncher() : request.launcher;
2251
2568
  if (!launcher) return receiptRead.kind === "missing" ? { kind: "not_found" } : { kind: "runtime_missing" };
2252
2569
  if (receiptRead.kind === "invalid") return { kind: "failed", step: "receipt_invalid" };
2253
- const inspection = inspectWithLauncher(launcher);
2570
+ const inspection = await inspectWithLauncher(launcher);
2254
2571
  if (inspection.kind !== "inspected") {
2255
2572
  return receiptRead.kind === "missing" ? { kind: "not_found" } : { kind: "failed", step: "home_unknown" };
2256
2573
  }
@@ -2259,8 +2576,8 @@ var removeHermesWiring = (request) => {
2259
2576
  if (state.kind === "not_wired" && receiptRead.kind === "missing") return { kind: "not_found" };
2260
2577
  const receipt = receiptRead.kind === "ok" ? receiptRead.receipt : receiptWithoutSnapshots();
2261
2578
  if (request.dryRun) return { kind: "removed", pluginRemoved: folder.kind === "pushary", settingsRestored: true, dryRun: true };
2262
- if (!removePushPlugin(launcher, home, folder, settings, receipt)) return { kind: "failed", step: "plugin" };
2263
- const settingsRestored = receipt.settings.every((snapshot) => restoreSetting(launcher, snapshot)) && returnQuestionsToHermes(launcher, receipt);
2579
+ if (!await removePushPlugin(launcher, home, folder, settings, receipt)) return { kind: "failed", step: "plugin" };
2580
+ const settingsRestored = await restoreSettingsInTurn(launcher, receipt.settings) && await returnQuestionsToHermes(launcher, receipt);
2264
2581
  if (!settingsRestored) return { kind: "failed", step: "settings" };
2265
2582
  removeSetupReceipts(request.receipts);
2266
2583
  return { kind: "removed", pluginRemoved: folder.kind === "pushary", settingsRestored, dryRun: false };