@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.
- package/CHANGELOG.md +15 -0
- package/dist/{api-YDCHRRA4.js → api-4WCUP47K.js} +10 -10
- package/dist/bin/pushary-bell-hook.js +25 -25
- package/dist/bin/pushary-bell.js +10 -10
- package/dist/bin/pushary-claude.js +26 -26
- package/dist/bin/pushary-clean.js +34 -34
- package/dist/bin/pushary-codex-bridge.js +21 -21
- package/dist/bin/pushary-codex-hook.js +30 -30
- package/dist/bin/pushary-codex-session.js +8 -8
- package/dist/bin/pushary-codex.js +13 -13
- package/dist/bin/pushary-connect.js +26 -26
- package/dist/bin/pushary-cowork.js +5 -5
- package/dist/bin/pushary-daemon.js +122 -66
- package/dist/bin/pushary-disconnect.js +25 -25
- package/dist/bin/pushary-display-hook.js +16 -16
- package/dist/bin/pushary-doctor.js +42 -42
- package/dist/bin/pushary-elicitation-hook.js +25 -25
- package/dist/bin/pushary-gemini-bridge.js +15 -15
- package/dist/bin/pushary-gemini-hook.js +27 -27
- package/dist/bin/pushary-hook.js +29 -29
- package/dist/bin/pushary-login.js +20 -20
- package/dist/bin/pushary-logout.js +24 -24
- package/dist/bin/pushary-mcp.js +11 -11
- package/dist/bin/pushary-mode.js +15 -15
- package/dist/bin/pushary-notification-hook.js +24 -24
- package/dist/bin/pushary-opencode-hook.js +26 -26
- package/dist/bin/pushary-permission-denied-hook.js +29 -29
- package/dist/bin/pushary-permission-hook.js +29 -29
- package/dist/bin/pushary-post-hook.js +24 -24
- package/dist/bin/pushary-prompt-hook.js +24 -24
- package/dist/bin/pushary-session-end-hook.js +26 -26
- package/dist/bin/pushary-session-start-hook.js +34 -34
- package/dist/bin/pushary-setup.js +58 -55
- package/dist/bin/pushary-stats.js +5 -5
- package/dist/bin/pushary-status.js +31 -31
- package/dist/bin/pushary-stop-hook.js +24 -24
- package/dist/bin/pushary-stopfailure-hook.js +24 -24
- package/dist/bin/pushary-suggestions.js +15 -15
- package/dist/bin/pushary-transcript-register.js +46 -18
- package/dist/bin/pushary-transcripts.js +19 -19
- package/dist/bin/pushary-upgrade.js +40 -40
- package/dist/bin/pushary-wait.js +15 -15
- package/dist/bin/pushary.js +7 -7
- package/dist/{chunk-TFYFQM5C.js → chunk-22FLBWJH.js} +2 -2
- package/dist/{chunk-KGWLDNNT.js → chunk-3XH4YLCN.js} +4 -4
- package/dist/{chunk-5FV472Q2.js → chunk-42Y7IZSV.js} +1 -1
- package/dist/{chunk-PPD64MZW.js → chunk-4DOJLAGR.js} +1 -1
- package/dist/{chunk-CUD6SYDG.js → chunk-4FYY6YW3.js} +1 -1
- package/dist/{chunk-BUR2LYQI.js → chunk-4ZSQPGAU.js} +2 -2
- package/dist/{chunk-MMVIKXCP.js → chunk-56POFDRB.js} +3 -3
- package/dist/{chunk-JNV5LOZH.js → chunk-5F3L3QOO.js} +4 -4
- package/dist/{chunk-WK2L3XVQ.js → chunk-5QOMMZFZ.js} +1 -1
- package/dist/{chunk-USZJJWZL.js → chunk-5WU6534E.js} +2 -2
- package/dist/{chunk-DZF2M3LM.js → chunk-5YHI2FN4.js} +6 -6
- package/dist/{chunk-ZY25SNFQ.js → chunk-6HDYWRCP.js} +1 -1
- package/dist/{chunk-47TIRGS5.js → chunk-6WOG2SCC.js} +1 -0
- package/dist/{chunk-4LX5QUFV.js → chunk-74NZ3CJQ.js} +1 -1
- package/dist/{chunk-W5AUTVVL.js → chunk-74R4IZ7N.js} +5 -5
- package/dist/{chunk-VZSSL6US.js → chunk-7BMBXTN4.js} +14 -14
- package/dist/{chunk-DB3NOEDW.js → chunk-7KMI2G24.js} +6 -6
- package/dist/{chunk-UEY4SHY2.js → chunk-AOGM34LL.js} +1 -1
- package/dist/{chunk-2QFYEB3P.js → chunk-B4MQSH3R.js} +5 -5
- package/dist/{chunk-F7R7M5MG.js → chunk-BE5BA3GF.js} +1 -1
- package/dist/{chunk-74ONBRPF.js → chunk-BM3AT5AF.js} +4 -4
- package/dist/{chunk-JZ5KV2FK.js → chunk-C5LCOXAC.js} +5 -5
- package/dist/{chunk-XLGD5LDG.js → chunk-CHBK47DU.js} +3 -3
- package/dist/{chunk-USMUW3LI.js → chunk-DLOUKVLM.js} +2 -2
- package/dist/{chunk-UMBVIFP5.js → chunk-DQCQQ2PY.js} +1 -1
- package/dist/{chunk-MTRZQR2R.js → chunk-EDHOHS3C.js} +3 -3
- package/dist/{chunk-M5VJ3HXB.js → chunk-FMDP5KWV.js} +6 -6
- package/dist/{chunk-CHIOUBPB.js → chunk-IVBQ5ISS.js} +13 -13
- package/dist/{chunk-5M5F4IWR.js → chunk-IVY6LYTO.js} +1 -1
- package/dist/{chunk-WLHHDDVA.js → chunk-JLBLEFBK.js} +3 -3
- package/dist/{chunk-WRCM4XGY.js → chunk-JM4WO3JT.js} +2 -2
- package/dist/{chunk-GJVV2EUC.js → chunk-KJYZXX7X.js} +5 -3
- package/dist/{chunk-NJTAUEW6.js → chunk-L2S7KWMF.js} +1 -1
- package/dist/{chunk-5KPMDLXB.js → chunk-LXSGNGK5.js} +3 -3
- package/dist/{chunk-VELPOV2F.js → chunk-MJXP5MFI.js} +4 -4
- package/dist/{chunk-YLB4AGZJ.js → chunk-MPZFQP6O.js} +1 -1
- package/dist/{chunk-E7U4YM4A.js → chunk-NFK5ZCLK.js} +3 -3
- package/dist/{chunk-7LLC4R66.js → chunk-NH7C6QBI.js} +4 -4
- package/dist/{chunk-Y4OBAK4I.js → chunk-OFJUHEEG.js} +8 -5
- package/dist/{chunk-IRLHN5UP.js → chunk-OFQACQDL.js} +27 -7
- package/dist/{chunk-CMNB4IZO.js → chunk-OOODSAHW.js} +3 -3
- package/dist/{chunk-7ZOWQBHQ.js → chunk-PYW7VWVZ.js} +5 -5
- package/dist/{chunk-M7JFGDGP.js → chunk-Q53QJQLY.js} +1 -1
- package/dist/{chunk-R3HXGHRQ.js → chunk-RCDZ2XZ6.js} +1 -1
- package/dist/{chunk-XJ5DYPTM.js → chunk-RDU5HWKU.js} +4 -4
- package/dist/{chunk-MAWC4PJV.js → chunk-RIVCTSYV.js} +5 -5
- package/dist/{chunk-E2VJ7B7D.js → chunk-RJEG3TUZ.js} +6 -6
- package/dist/{chunk-QUGP76DQ.js → chunk-SV2C7PZV.js} +6 -6
- package/dist/{chunk-7SFP6Y5U.js → chunk-T2MJ5H2V.js} +603 -162
- package/dist/{chunk-3YU6NIIL.js → chunk-TDLK3ZU2.js} +4 -4
- package/dist/{chunk-C2AR7QXU.js → chunk-TKQGDSO4.js} +2 -2
- package/dist/{chunk-2BK2EQOB.js → chunk-TNNB7P6Z.js} +7 -7
- package/dist/{chunk-7KYQ24FI.js → chunk-TQYSBYYZ.js} +4 -4
- package/dist/{chunk-IRN6JLSM.js → chunk-TRTAYW7Z.js} +4 -4
- package/dist/{chunk-SWRNJ3BP.js → chunk-VIIQLZJS.js} +3 -3
- package/dist/{chunk-NFUV25JZ.js → chunk-VYELSTZK.js} +3 -3
- package/dist/{chunk-S7C5NY4R.js → chunk-VZHDSUXK.js} +1 -1
- package/dist/{chunk-FAGQAR6I.js → chunk-WHTWHHE3.js} +478 -161
- package/dist/{chunk-57MQNSM5.js → chunk-WJTUCQ47.js} +1 -1
- package/dist/{chunk-KWGOKO4E.js → chunk-XCBKGQ6N.js} +3 -3
- package/dist/{chunk-T2EJ3H6Y.js → chunk-Y4I5YX3L.js} +3 -3
- package/dist/{chunk-GII565PB.js → chunk-YH3RAWMV.js} +1 -1
- package/dist/{chunk-NQA56AMZ.js → chunk-YJK3LBAJ.js} +6 -6
- package/dist/{chunk-B2PFCS72.js → chunk-YOPFR22Y.js} +2 -2
- package/dist/{chunk-5NW7VM35.js → chunk-YP35CA5L.js} +2 -2
- package/dist/{chunk-ITLWEGLE.js → chunk-Z5VZWDGL.js} +15 -5
- package/dist/{chunk-Y2CYSVWU.js → chunk-Z6DZHUCO.js} +6 -6
- package/dist/{chunk-YDPGBBV7.js → chunk-ZFS2Y5GK.js} +5 -5
- package/dist/{chunk-NVLKH5FS.js → chunk-ZM3ELQPE.js} +9 -9
- package/dist/{codex-app-cards-switch-3OUTABVU.js → codex-app-cards-switch-GUZ7XMPB.js} +11 -11
- package/dist/{codex-desktop-launch-JH3E2CIA.js → codex-desktop-launch-Q5UINXU6.js} +1 -1
- package/dist/{codex-session-launch-GR363OQN.js → codex-session-launch-4G5AFXXU.js} +18 -18
- package/dist/{config-R43UFHX7.js → config-725HOJFK.js} +3 -3
- package/dist/{global-upgrade-73GP4MZM.js → global-upgrade-AXTMEH3I.js} +10 -10
- package/dist/{identity-CA7CZGDL.js → identity-HLIZDGEN.js} +4 -4
- package/dist/{live-capabilities-RCGMQIIZ.js → live-capabilities-A74OIHWN.js} +5 -5
- package/dist/{paths-H7VNNB46.js → paths-ZHDLQD25.js} +3 -3
- package/dist/{reapply-3UIA3CUO.js → reapply-RC2AMGCQ.js} +27 -27
- package/dist/src/index.js +28 -28
- package/dist/{upgrade-lock-ZTLX5PDF.js → upgrade-lock-4B3D3OCT.js} +4 -4
- package/dist/{usage-C53NVWC2.js → usage-VVPEFYKS.js} +3 -3
- package/package.json +1 -1
|
@@ -5,19 +5,19 @@ import {
|
|
|
5
5
|
hermesAppWiringRecord,
|
|
6
6
|
hermesConfigReceipt,
|
|
7
7
|
hermesSetupReceipt
|
|
8
|
-
} from "./chunk-
|
|
8
|
+
} from "./chunk-TQYSBYYZ.js";
|
|
9
9
|
import {
|
|
10
10
|
readJsonSafe,
|
|
11
11
|
resolvePathBinary,
|
|
12
12
|
writeJsonAtomic
|
|
13
|
-
} from "./chunk-
|
|
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-
|
|
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.
|
|
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 {
|
|
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
|
|
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
|
|
2109
|
+
const running = runFileAsync(
|
|
1750
2110
|
shell ? quotedForWindowsShell(launcher.path) : launcher.path,
|
|
1751
2111
|
shell ? args.map(quotedForWindowsShell) : [...args],
|
|
1752
|
-
{ encoding: "utf-8",
|
|
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
|
|
1786
|
-
var
|
|
1787
|
-
var
|
|
1788
|
-
|
|
1789
|
-
|
|
1790
|
-
|
|
1791
|
-
var
|
|
1792
|
-
|
|
1793
|
-
|
|
1794
|
-
|
|
1795
|
-
|
|
1796
|
-
|
|
1797
|
-
|
|
1798
|
-
|
|
1799
|
-
|
|
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
|
-
|
|
1951
|
-
|
|
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
|
|
2087
|
-
const
|
|
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
|
|
2128
|
-
const
|
|
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
|
|
2151
|
-
|
|
2152
|
-
|
|
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
|
-
|
|
2156
|
-
|
|
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
|
|
2178
|
-
if (!returnQuestionsToHermes(launcher, receipt)) return
|
|
2484
|
+
if (!receipt.questionRoutingAdded || !routesHermesQuestions(folder)) return "unchanged";
|
|
2485
|
+
if (!await returnQuestionsToHermes(launcher, receipt)) return "failed";
|
|
2179
2486
|
writeSetupReceipt(receipts, receiptAfterQuestionRoutingReturned(receipt));
|
|
2180
|
-
return
|
|
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
|
|
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(
|
|
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
|
-
|
|
2200
|
-
if (
|
|
2201
|
-
|
|
2202
|
-
}
|
|
2203
|
-
const legacyCopyRemoved = removeLegacyCopy(launcher);
|
|
2204
|
-
|
|
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
|
|
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 =
|
|
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 };
|