@pushary/agent-hooks 1.9.66 → 1.9.67
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 +9 -0
- package/data/SKILL.md +1 -1
- package/data/cursor-plugin/skills/pushary/SKILL.md +1 -1
- package/data/vscode-plugin/skills/pushary/SKILL.md +1 -1
- package/dist/{api-GDSVUTI2.js → api-3WIPD5GC.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 +73 -74
- 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 +28 -27
- package/dist/bin/pushary-cowork.js +5 -5
- package/dist/bin/pushary-daemon.js +40 -39
- package/dist/bin/pushary-disconnect.js +25 -25
- package/dist/bin/pushary-doctor.js +115 -71
- 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 -19
- package/dist/bin/pushary-logout.js +26 -27
- package/dist/bin/pushary-mcp.js +11 -11
- package/dist/bin/pushary-mode.js +16 -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 -33
- package/dist/bin/pushary-setup.js +107 -93
- package/dist/bin/pushary-stats.js +5 -5
- package/dist/bin/pushary-status.js +37 -35
- 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 -14
- package/dist/bin/pushary-transcript-register.js +16 -16
- package/dist/bin/pushary-transcripts.js +18 -17
- package/dist/bin/pushary-upgrade.js +45 -38
- package/dist/bin/pushary-wait.js +26 -20
- package/dist/bin/pushary.js +7 -7
- package/dist/{chunk-A47UKPC7.js → chunk-32OSSYGZ.js} +1 -1
- package/dist/{chunk-QR2QWGQI.js → chunk-3THDNNVL.js} +3 -3
- package/dist/{chunk-DPU6H4JY.js → chunk-564IPPBL.js} +3 -3
- package/dist/chunk-5MOJCOCF.js +25 -0
- package/dist/{chunk-V4EOFOB2.js → chunk-6H4E5UBA.js} +6 -6
- package/dist/{chunk-DKMVVBXO.js → chunk-6LXXKMV6.js} +38 -10
- package/dist/chunk-76SWHCAR.js +299 -0
- package/dist/{chunk-G6VJZ3RB.js → chunk-77L2TNJW.js} +1 -1
- package/dist/{chunk-6BGVSV2Q.js → chunk-7YJSDXLM.js} +1 -1
- package/dist/{chunk-AWGBL4P3.js → chunk-B3V2WOJX.js} +89 -41
- package/dist/{chunk-RJ6JEHCP.js → chunk-BBFGDYYI.js} +1 -1
- package/dist/{chunk-YQDCTID4.js → chunk-BFF3ABFV.js} +4 -4
- package/dist/{chunk-R7C2FS33.js → chunk-C4EDAHXI.js} +1 -1
- package/dist/{chunk-YB3HMDKE.js → chunk-CHYBOTQN.js} +2 -2
- package/dist/{chunk-26W46E46.js → chunk-CJZLIAJF.js} +27 -10
- package/dist/{chunk-ZJV7SFIJ.js → chunk-CPLQ5OHX.js} +1 -1
- package/dist/{chunk-ZAZUDYSF.js → chunk-D3U6CN7T.js} +5 -5
- package/dist/{chunk-MNNQEUQC.js → chunk-D7Z4USSB.js} +27 -8
- package/dist/{chunk-V3YBVOTZ.js → chunk-DP6LU6BB.js} +2 -2
- package/dist/{chunk-32ZT2ATY.js → chunk-DXCHIKAN.js} +8 -8
- package/dist/{chunk-QN25WBXL.js → chunk-EQR6JSFE.js} +7 -7
- package/dist/{chunk-IQ3GKPFM.js → chunk-FONWVVDD.js} +1 -1
- package/dist/{chunk-XXIJBYNW.js → chunk-GMHQYX3D.js} +1 -1
- package/dist/{chunk-3EZP5ZPS.js → chunk-ICQCQAUO.js} +4 -4
- package/dist/{chunk-5J7OZBJ4.js → chunk-IPG3RS23.js} +2 -2
- package/dist/{chunk-4APD2DPI.js → chunk-IRV2MNMA.js} +53 -3
- package/dist/{chunk-MDIGG3Q7.js → chunk-IVB5O6NY.js} +3 -3
- package/dist/{chunk-VURDLVAQ.js → chunk-IXTVUINH.js} +3 -3
- package/dist/{chunk-HUGVJBDN.js → chunk-JBRMIADM.js} +2 -2
- package/dist/{chunk-IGO3AGZK.js → chunk-JHB5SR5B.js} +4 -4
- package/dist/{chunk-CNJ7SI5V.js → chunk-JOBSKQ2Z.js} +4 -4
- package/dist/{chunk-5FJDODOA.js → chunk-JOD2325S.js} +3 -3
- package/dist/{chunk-376TUN72.js → chunk-JSPCGPSH.js} +3 -3
- package/dist/{chunk-T6YLXO6N.js → chunk-KE4TSJUD.js} +4 -4
- package/dist/{chunk-TQNFDNJA.js → chunk-KIXRHNCW.js} +6 -6
- package/dist/{chunk-Q5RNYE2L.js → chunk-KNUYUQBP.js} +4 -4
- package/dist/{chunk-3FQX74E4.js → chunk-KTLE7NIK.js} +23 -8
- package/dist/{chunk-57NTMVMI.js → chunk-LJHDUKBB.js} +6 -6
- package/dist/{chunk-CN6JFM62.js → chunk-LS42NV6E.js} +15 -6
- package/dist/{chunk-6ZXCWK27.js → chunk-LYTWK5TF.js} +5 -5
- package/dist/{chunk-XIJTXRWB.js → chunk-METUT7V4.js} +2 -2
- package/dist/{chunk-7LK3Y6ET.js → chunk-NSS2AIGI.js} +6 -6
- package/dist/{chunk-XEXF2OFZ.js → chunk-NWN6GGJL.js} +2 -2
- package/dist/{chunk-GECU7V44.js → chunk-P4RXSZDT.js} +3 -3
- package/dist/{chunk-IA5VWIHV.js → chunk-PIBWX3EF.js} +2 -2
- package/dist/{chunk-GWYCKTXS.js → chunk-PO2P3XDC.js} +5 -5
- package/dist/{chunk-DIUTSF6I.js → chunk-R2BZH2NK.js} +4 -4
- package/dist/{chunk-6RJZSBPD.js → chunk-RO2B4YBF.js} +1 -1
- package/dist/{chunk-EJR5JXMV.js → chunk-RQOT6QL3.js} +43 -5
- package/dist/{chunk-SVKCYUYP.js → chunk-SB6TYV4M.js} +9 -9
- package/dist/{chunk-GDLT32I4.js → chunk-TI4JYPGQ.js} +6 -2
- package/dist/{chunk-B6JC676X.js → chunk-UC2J5RR4.js} +1 -1
- package/dist/{chunk-XDDSURY3.js → chunk-UQIPWXBL.js} +3 -3
- package/dist/{chunk-EU2ZOZC3.js → chunk-UXN73YAE.js} +3 -3
- package/dist/{chunk-RSZQEEEW.js → chunk-VEYFXRZ6.js} +7 -6
- package/dist/{chunk-XFQAK35L.js → chunk-VHMZPN3N.js} +1 -1
- package/dist/{chunk-45KMS76L.js → chunk-VKZ3RJ5U.js} +1 -1
- package/dist/chunk-VVIQWGS7.js +2298 -0
- package/dist/{chunk-KS3O6O7E.js → chunk-WJLHZ6E6.js} +3 -3
- package/dist/{chunk-GO7YKPT3.js → chunk-X2MTVD4W.js} +1 -1
- package/dist/{chunk-2BL6KWN7.js → chunk-XFDWLDHG.js} +9 -9
- package/dist/{chunk-3ELJIG2R.js → chunk-XNUWKKGS.js} +3 -3
- package/dist/{chunk-QOIPUYUD.js → chunk-XZKSUJ55.js} +1 -1
- package/dist/{chunk-JL3SZJJP.js → chunk-Y4MRHAE5.js} +2 -2
- package/dist/{chunk-DBUHSUL7.js → chunk-Y6ZN454E.js} +1 -1
- package/dist/{chunk-XGFFNERF.js → chunk-YSD4SMHE.js} +1 -1
- package/dist/{chunk-UTAARXYQ.js → chunk-ZK5WYAMI.js} +1 -1
- package/dist/{chunk-AVWECJSN.js → chunk-ZPIYJLQ3.js} +2 -2
- package/dist/{codex-app-cards-switch-W7OLKOMO.js → codex-app-cards-switch-25TDTMUZ.js} +11 -11
- package/dist/{codex-desktop-launch-U6V3AXZF.js → codex-desktop-launch-JDKUCD2Y.js} +1 -1
- package/dist/{codex-session-launch-EFU46WBL.js → codex-session-launch-6N35AAEO.js} +18 -18
- package/dist/{config-UC4LMCNX.js → config-OBGYURFX.js} +3 -3
- package/dist/{global-upgrade-ABF5O3HU.js → global-upgrade-2QNQD5OM.js} +10 -10
- package/dist/{identity-3ZUZPHN6.js → identity-CUPX2U7Q.js} +4 -4
- package/dist/{live-capabilities-BXWXJARK.js → live-capabilities-CUEIWMSM.js} +5 -5
- package/dist/{paths-FO76CFRQ.js → paths-BHLBZ6FY.js} +7 -3
- package/dist/{reapply-3T3DFUAI.js → reapply-4GXLWZ5M.js} +27 -26
- package/dist/src/index.js +28 -28
- package/dist/{upgrade-lock-EZ5JIPJE.js → upgrade-lock-RDKTAHWR.js} +4 -4
- package/dist/{usage-Y6BL7RUD.js → usage-KX2HN47E.js} +3 -3
- package/package.json +1 -1
- package/dist/chunk-QBNICXMC.js +0 -615
package/dist/chunk-QBNICXMC.js
DELETED
|
@@ -1,615 +0,0 @@
|
|
|
1
|
-
import {
|
|
2
|
-
openCodeConfigDirFrom
|
|
3
|
-
} from "./chunk-7LK3Y6ET.js";
|
|
4
|
-
import {
|
|
5
|
-
nativeClaudeInstallPath
|
|
6
|
-
} from "./chunk-RJ6JEHCP.js";
|
|
7
|
-
import {
|
|
8
|
-
grokHome
|
|
9
|
-
} from "./chunk-3EZP5ZPS.js";
|
|
10
|
-
import {
|
|
11
|
-
claudeConfigDir,
|
|
12
|
-
clineDataDirectory,
|
|
13
|
-
codexHomeFrom
|
|
14
|
-
} from "./chunk-GDLT32I4.js";
|
|
15
|
-
import {
|
|
16
|
-
resolvePathBinary
|
|
17
|
-
} from "./chunk-KS3O6O7E.js";
|
|
18
|
-
import {
|
|
19
|
-
HOOK_COMPATIBLE_PROFILES
|
|
20
|
-
} from "./chunk-EJR5JXMV.js";
|
|
21
|
-
|
|
22
|
-
// src/hermes-config.ts
|
|
23
|
-
import { execFileSync } from "child_process";
|
|
24
|
-
import { existsSync, readFileSync, rmSync, writeFileSync } from "fs";
|
|
25
|
-
import { homedir, tmpdir } from "os";
|
|
26
|
-
import { join } from "path";
|
|
27
|
-
var isRecord = (value) => Boolean(value) && typeof value === "object" && !Array.isArray(value);
|
|
28
|
-
var isSnapshot = (value) => isRecord(value) && typeof value.present === "boolean" && Object.hasOwn(value, "value");
|
|
29
|
-
var isHermesConfigReceipt = (value) => {
|
|
30
|
-
if (!isRecord(value)) return false;
|
|
31
|
-
return ["security", "approval", "transport", "fallback", "plugins", "enabled", "agent", "disabled"].every((key) => isSnapshot(value[key])) && typeof value.pushary_enabled === "boolean" && typeof value.clarify_disabled === "boolean";
|
|
32
|
-
};
|
|
33
|
-
var HERMES_PIP_PACKAGE = "hermes-plugin-pushary";
|
|
34
|
-
var HERMES_PLUGIN_NAME = "pushary";
|
|
35
|
-
var HERMES_CONFIG_SCRIPT = String.raw`
|
|
36
|
-
import copy, importlib.util, json, os, sys
|
|
37
|
-
from hermes_cli.config import load_config, save_config
|
|
38
|
-
|
|
39
|
-
PUSHARY_TRANSPORT = "pushary"
|
|
40
|
-
PUSHARY_FALLBACK = "builtin"
|
|
41
|
-
PUSHARY_PLUGIN = "pushary"
|
|
42
|
-
PUSHARY_PLUGIN_MODULE = "pushary_plugin"
|
|
43
|
-
QUESTION_TOOLSET = "clarify"
|
|
44
|
-
|
|
45
|
-
mode = sys.argv[1]
|
|
46
|
-
receipt_path = sys.argv[2] if len(sys.argv) > 2 else None
|
|
47
|
-
c = load_config()
|
|
48
|
-
original = copy.deepcopy(c)
|
|
49
|
-
|
|
50
|
-
def section(obj, key):
|
|
51
|
-
value = obj.get(key)
|
|
52
|
-
return value if isinstance(value, dict) else {}
|
|
53
|
-
|
|
54
|
-
def members(obj, key):
|
|
55
|
-
value = obj.get(key)
|
|
56
|
-
return value if isinstance(value, list) else []
|
|
57
|
-
|
|
58
|
-
if mode == "status":
|
|
59
|
-
print(json.dumps({
|
|
60
|
-
"plugin_installed": importlib.util.find_spec(PUSHARY_PLUGIN_MODULE) is not None,
|
|
61
|
-
"plugin_enabled": PUSHARY_PLUGIN in members(section(c, "plugins"), "enabled"),
|
|
62
|
-
"approvals_routed": section(section(c, "security"), "approval").get("transport") == PUSHARY_TRANSPORT,
|
|
63
|
-
"questions_routed": QUESTION_TOOLSET in members(section(c, "agent"), "disabled_toolsets"),
|
|
64
|
-
}))
|
|
65
|
-
sys.exit(0)
|
|
66
|
-
|
|
67
|
-
def recorded(snapshot, value):
|
|
68
|
-
return bool(snapshot.get("present")) and snapshot.get("value") == value
|
|
69
|
-
|
|
70
|
-
def pop_if(obj, key, value):
|
|
71
|
-
if obj.get(key) == value:
|
|
72
|
-
obj.pop(key, None)
|
|
73
|
-
|
|
74
|
-
def snap(obj, key):
|
|
75
|
-
return {"present": key in obj, "value": obj.get(key)}
|
|
76
|
-
|
|
77
|
-
restore_conflicts = []
|
|
78
|
-
|
|
79
|
-
def restore(obj, key, before, ours):
|
|
80
|
-
if before.get("present") and before.get("value") == ours:
|
|
81
|
-
return
|
|
82
|
-
if before.get("present"):
|
|
83
|
-
if key in obj and obj.get(key) == before.get("value"):
|
|
84
|
-
return
|
|
85
|
-
elif key not in obj:
|
|
86
|
-
return
|
|
87
|
-
if obj.get(key) != ours:
|
|
88
|
-
restore_conflicts.append(key)
|
|
89
|
-
return
|
|
90
|
-
if before.get("present"):
|
|
91
|
-
obj[key] = before.get("value")
|
|
92
|
-
else:
|
|
93
|
-
obj.pop(key, None)
|
|
94
|
-
|
|
95
|
-
def restore_container(obj, key, before, current, changed):
|
|
96
|
-
raw = obj.get(key)
|
|
97
|
-
if isinstance(before.get("value"), dict):
|
|
98
|
-
if not isinstance(raw, dict):
|
|
99
|
-
if changed:
|
|
100
|
-
restore_conflicts.append(key)
|
|
101
|
-
return
|
|
102
|
-
if current or before.get("present"):
|
|
103
|
-
obj[key] = current
|
|
104
|
-
else:
|
|
105
|
-
obj.pop(key, None)
|
|
106
|
-
elif isinstance(raw, dict) and current:
|
|
107
|
-
restore_conflicts.append(key)
|
|
108
|
-
else:
|
|
109
|
-
restore(obj, key, before, {})
|
|
110
|
-
|
|
111
|
-
def restore_list_member(obj, key, before, ours, ours_before):
|
|
112
|
-
raw = obj.get(key)
|
|
113
|
-
if isinstance(before.get("value"), list):
|
|
114
|
-
if ours_before:
|
|
115
|
-
return
|
|
116
|
-
if not isinstance(raw, list):
|
|
117
|
-
restore_conflicts.append(key)
|
|
118
|
-
return
|
|
119
|
-
remaining = [item for item in raw if item != ours]
|
|
120
|
-
if remaining or before.get("present"):
|
|
121
|
-
obj[key] = remaining
|
|
122
|
-
else:
|
|
123
|
-
obj.pop(key, None)
|
|
124
|
-
return
|
|
125
|
-
if before.get("present") and raw == before.get("value"):
|
|
126
|
-
return
|
|
127
|
-
if not before.get("present") and key not in obj:
|
|
128
|
-
return
|
|
129
|
-
if not isinstance(raw, list):
|
|
130
|
-
restore_conflicts.append(key)
|
|
131
|
-
return
|
|
132
|
-
remaining = [item for item in raw if item != ours]
|
|
133
|
-
if remaining:
|
|
134
|
-
restore_conflicts.append(key)
|
|
135
|
-
else:
|
|
136
|
-
restore(obj, key, before, raw)
|
|
137
|
-
|
|
138
|
-
def valid_snapshot(value):
|
|
139
|
-
return isinstance(value, dict) and isinstance(value.get("present"), bool) and "value" in value
|
|
140
|
-
|
|
141
|
-
def valid_receipt(value):
|
|
142
|
-
snapshots = ["security", "approval", "transport", "fallback", "plugins", "enabled", "agent", "disabled"]
|
|
143
|
-
return (isinstance(value, dict)
|
|
144
|
-
and all(valid_snapshot(value.get(key)) for key in snapshots)
|
|
145
|
-
and isinstance(value.get("pushary_enabled"), bool)
|
|
146
|
-
and isinstance(value.get("clarify_disabled"), bool))
|
|
147
|
-
|
|
148
|
-
if mode == "setup":
|
|
149
|
-
security_before = snap(c, "security")
|
|
150
|
-
s = section(c, "security")
|
|
151
|
-
approval_before = snap(s, "approval")
|
|
152
|
-
ap = section(s, "approval")
|
|
153
|
-
plugins_before = snap(c, "plugins")
|
|
154
|
-
p = section(c, "plugins")
|
|
155
|
-
agent_before = snap(c, "agent")
|
|
156
|
-
a = section(c, "agent")
|
|
157
|
-
enabled = members(p, "enabled")
|
|
158
|
-
disabled = members(a, "disabled_toolsets")
|
|
159
|
-
receipt = {
|
|
160
|
-
"security": security_before,
|
|
161
|
-
"approval": approval_before,
|
|
162
|
-
"transport": snap(ap, "transport"),
|
|
163
|
-
"fallback": snap(ap, "transport_fallback"),
|
|
164
|
-
"plugins": plugins_before,
|
|
165
|
-
"enabled": snap(p, "enabled"),
|
|
166
|
-
"pushary_enabled": PUSHARY_PLUGIN in enabled,
|
|
167
|
-
"agent": agent_before,
|
|
168
|
-
"disabled": snap(a, "disabled_toolsets"),
|
|
169
|
-
"clarify_disabled": QUESTION_TOOLSET in disabled,
|
|
170
|
-
}
|
|
171
|
-
if os.path.exists(receipt_path):
|
|
172
|
-
with open(receipt_path, encoding="utf-8") as handle:
|
|
173
|
-
existing_receipt = json.load(handle)
|
|
174
|
-
if not valid_receipt(existing_receipt):
|
|
175
|
-
raise ValueError("invalid Pushary Hermes setup receipt")
|
|
176
|
-
else:
|
|
177
|
-
os.makedirs(os.path.dirname(receipt_path), exist_ok=True)
|
|
178
|
-
temp = receipt_path + ".tmp"
|
|
179
|
-
with open(temp, "x", encoding="utf-8") as handle:
|
|
180
|
-
json.dump(receipt, handle)
|
|
181
|
-
os.chmod(temp, 0o600)
|
|
182
|
-
os.replace(temp, receipt_path)
|
|
183
|
-
ap["transport"] = PUSHARY_TRANSPORT
|
|
184
|
-
ap["transport_fallback"] = PUSHARY_FALLBACK
|
|
185
|
-
s["approval"] = ap
|
|
186
|
-
c["security"] = s
|
|
187
|
-
p["enabled"] = enabled if PUSHARY_PLUGIN in enabled else enabled + [PUSHARY_PLUGIN]
|
|
188
|
-
c["plugins"] = p
|
|
189
|
-
a["disabled_toolsets"] = disabled if QUESTION_TOOLSET in disabled else disabled + [QUESTION_TOOLSET]
|
|
190
|
-
c["agent"] = a
|
|
191
|
-
else:
|
|
192
|
-
receipt_exists = os.path.exists(receipt_path)
|
|
193
|
-
try:
|
|
194
|
-
with open(receipt_path, encoding="utf-8") as handle:
|
|
195
|
-
receipt = json.load(handle)
|
|
196
|
-
except (OSError, ValueError):
|
|
197
|
-
if receipt_exists:
|
|
198
|
-
raise
|
|
199
|
-
receipt = None
|
|
200
|
-
s = section(c, "security")
|
|
201
|
-
ap = section(s, "approval")
|
|
202
|
-
if receipt:
|
|
203
|
-
if not valid_receipt(receipt):
|
|
204
|
-
raise ValueError("invalid Pushary Hermes setup receipt")
|
|
205
|
-
approval_changed = (not receipt["transport"].get("present")
|
|
206
|
-
or receipt["transport"].get("value") != PUSHARY_TRANSPORT
|
|
207
|
-
or not receipt["fallback"].get("present")
|
|
208
|
-
or receipt["fallback"].get("value") != PUSHARY_FALLBACK)
|
|
209
|
-
plugins_changed = not (isinstance(receipt["enabled"].get("value"), list)
|
|
210
|
-
and receipt["pushary_enabled"])
|
|
211
|
-
agent_changed = not (isinstance(receipt["disabled"].get("value"), list)
|
|
212
|
-
and receipt["clarify_disabled"])
|
|
213
|
-
transport_left_over = recorded(receipt["transport"], PUSHARY_TRANSPORT)
|
|
214
|
-
if transport_left_over:
|
|
215
|
-
pop_if(ap, "transport", PUSHARY_TRANSPORT)
|
|
216
|
-
else:
|
|
217
|
-
restore(ap, "transport", receipt["transport"], PUSHARY_TRANSPORT)
|
|
218
|
-
if transport_left_over and recorded(receipt["fallback"], PUSHARY_FALLBACK):
|
|
219
|
-
pop_if(ap, "transport_fallback", PUSHARY_FALLBACK)
|
|
220
|
-
else:
|
|
221
|
-
restore(ap, "transport_fallback", receipt["fallback"], PUSHARY_FALLBACK)
|
|
222
|
-
else:
|
|
223
|
-
pop_if(ap, "transport", PUSHARY_TRANSPORT)
|
|
224
|
-
pop_if(ap, "transport_fallback", PUSHARY_FALLBACK)
|
|
225
|
-
if receipt:
|
|
226
|
-
restore_container(s, "approval", receipt["approval"], ap, approval_changed)
|
|
227
|
-
restore_container(c, "security", receipt["security"], s, approval_changed)
|
|
228
|
-
else:
|
|
229
|
-
if ap: s["approval"] = ap
|
|
230
|
-
else: s.pop("approval", None)
|
|
231
|
-
if s: c["security"] = s
|
|
232
|
-
else: c.pop("security", None)
|
|
233
|
-
|
|
234
|
-
p = section(c, "plugins")
|
|
235
|
-
if receipt:
|
|
236
|
-
restore_list_member(p, "enabled", receipt["enabled"], PUSHARY_PLUGIN, False)
|
|
237
|
-
restore_container(c, "plugins", receipt["plugins"], p, plugins_changed)
|
|
238
|
-
else:
|
|
239
|
-
enabled = [item for item in members(p, "enabled") if item != PUSHARY_PLUGIN]
|
|
240
|
-
if enabled: p["enabled"] = enabled
|
|
241
|
-
else: p.pop("enabled", None)
|
|
242
|
-
if p: c["plugins"] = p
|
|
243
|
-
else: c.pop("plugins", None)
|
|
244
|
-
|
|
245
|
-
a = section(c, "agent")
|
|
246
|
-
if receipt:
|
|
247
|
-
clarify_was_users = receipt["clarify_disabled"] and not recorded(receipt["transport"], PUSHARY_TRANSPORT)
|
|
248
|
-
restore_list_member(a, "disabled_toolsets", receipt["disabled"], QUESTION_TOOLSET, clarify_was_users)
|
|
249
|
-
restore_container(c, "agent", receipt["agent"], a, agent_changed)
|
|
250
|
-
else:
|
|
251
|
-
disabled = [item for item in members(a, "disabled_toolsets") if item != QUESTION_TOOLSET]
|
|
252
|
-
if disabled: a["disabled_toolsets"] = disabled
|
|
253
|
-
else: a.pop("disabled_toolsets", None)
|
|
254
|
-
if a: c["agent"] = a
|
|
255
|
-
else: c.pop("agent", None)
|
|
256
|
-
|
|
257
|
-
if receipt and restore_conflicts:
|
|
258
|
-
raise ValueError("Hermes config changed after Pushary setup")
|
|
259
|
-
|
|
260
|
-
changed = c != original
|
|
261
|
-
if changed and mode != "clean-preview":
|
|
262
|
-
save_config(c)
|
|
263
|
-
print(json.dumps({"changed": changed}))
|
|
264
|
-
`;
|
|
265
|
-
var resolveHermesPython = () => {
|
|
266
|
-
const launcher = resolvePathBinary("hermes");
|
|
267
|
-
if (launcher && process.platform !== "win32") {
|
|
268
|
-
try {
|
|
269
|
-
const shebang = readFileSync(launcher, "utf-8").split("\n", 1)[0];
|
|
270
|
-
const interpreter = shebang?.startsWith("#!") ? shebang.slice(2).trim().split(/\s+/)[0] : void 0;
|
|
271
|
-
if (interpreter && /python/i.test(interpreter) && existsSync(interpreter)) return interpreter;
|
|
272
|
-
} catch {
|
|
273
|
-
}
|
|
274
|
-
}
|
|
275
|
-
const root = join(homedir(), ".hermes", "hermes-agent", "venv");
|
|
276
|
-
const candidates = process.platform === "win32" ? [join(root, "Scripts", "python.exe"), join(root, "Scripts", "python3.exe")] : [join(root, "bin", "python3"), join(root, "bin", "python")];
|
|
277
|
-
return candidates.find(existsSync) ?? null;
|
|
278
|
-
};
|
|
279
|
-
var HERMES_SCRIPT_TIMEOUT_MS = 15e3;
|
|
280
|
-
var runPythonScript = (python, script, args) => {
|
|
281
|
-
const path = join(tmpdir(), `pushary-hermes-${process.pid}-${Date.now()}.py`);
|
|
282
|
-
try {
|
|
283
|
-
writeFileSync(path, script, { encoding: "utf-8", mode: 384 });
|
|
284
|
-
return execFileSync(python, [path, ...args], {
|
|
285
|
-
env: { ...process.env },
|
|
286
|
-
encoding: "utf-8",
|
|
287
|
-
stdio: "pipe",
|
|
288
|
-
timeout: HERMES_SCRIPT_TIMEOUT_MS
|
|
289
|
-
});
|
|
290
|
-
} catch {
|
|
291
|
-
return null;
|
|
292
|
-
} finally {
|
|
293
|
-
try {
|
|
294
|
-
rmSync(path, { force: true });
|
|
295
|
-
} catch {
|
|
296
|
-
}
|
|
297
|
-
}
|
|
298
|
-
};
|
|
299
|
-
var lastReport = (stdout) => {
|
|
300
|
-
const line = stdout.trim().split("\n").at(-1) ?? "";
|
|
301
|
-
try {
|
|
302
|
-
return JSON.parse(line);
|
|
303
|
-
} catch {
|
|
304
|
-
return void 0;
|
|
305
|
-
}
|
|
306
|
-
};
|
|
307
|
-
var configRunFrom = (stdout) => {
|
|
308
|
-
const report = stdout === null ? void 0 : lastReport(stdout);
|
|
309
|
-
if (!isRecord(report) || typeof report.changed !== "boolean") return "failed";
|
|
310
|
-
return report.changed ? "changed" : "unchanged";
|
|
311
|
-
};
|
|
312
|
-
var runHermesConfigScript = (python, mode, receiptPath) => configRunFrom(runPythonScript(python, HERMES_CONFIG_SCRIPT, [mode, receiptPath]));
|
|
313
|
-
var HERMES_STATUS_FIELDS = ["plugin_installed", "plugin_enabled", "approvals_routed", "questions_routed"];
|
|
314
|
-
var isHermesStatusReport = (value) => isRecord(value) && HERMES_STATUS_FIELDS.every((field) => typeof value[field] === "boolean");
|
|
315
|
-
var hermesWiredByPushary = (status, setupRecorded) => setupRecorded || status.pluginEnabled || status.approvalsRouted;
|
|
316
|
-
var readHermesStatus = (python) => {
|
|
317
|
-
const stdout = runPythonScript(python, HERMES_CONFIG_SCRIPT, ["status"]);
|
|
318
|
-
const report = stdout === null ? void 0 : lastReport(stdout);
|
|
319
|
-
if (!isHermesStatusReport(report)) return null;
|
|
320
|
-
return {
|
|
321
|
-
pluginInstalled: report.plugin_installed,
|
|
322
|
-
pluginEnabled: report.plugin_enabled,
|
|
323
|
-
approvalsRouted: report.approvals_routed,
|
|
324
|
-
questionsRouted: report.questions_routed
|
|
325
|
-
};
|
|
326
|
-
};
|
|
327
|
-
|
|
328
|
-
// src/vscode-config.ts
|
|
329
|
-
import { homedir as homedir2 } from "os";
|
|
330
|
-
import { join as join2 } from "path";
|
|
331
|
-
var PLUGIN_LOCATIONS_KEY = "chat.pluginLocations";
|
|
332
|
-
var blankJsonComments = (text) => {
|
|
333
|
-
let out = "";
|
|
334
|
-
let i = 0;
|
|
335
|
-
let inString = false;
|
|
336
|
-
let escaped = false;
|
|
337
|
-
while (i < text.length) {
|
|
338
|
-
const ch = text[i];
|
|
339
|
-
if (inString) {
|
|
340
|
-
out += ch;
|
|
341
|
-
if (escaped) escaped = false;
|
|
342
|
-
else if (ch === "\\") escaped = true;
|
|
343
|
-
else if (ch === '"') inString = false;
|
|
344
|
-
i += 1;
|
|
345
|
-
continue;
|
|
346
|
-
}
|
|
347
|
-
if (ch === '"') {
|
|
348
|
-
inString = true;
|
|
349
|
-
out += ch;
|
|
350
|
-
i += 1;
|
|
351
|
-
continue;
|
|
352
|
-
}
|
|
353
|
-
if (ch === "/" && text[i + 1] === "/") {
|
|
354
|
-
while (i < text.length && text[i] !== "\n") {
|
|
355
|
-
out += " ";
|
|
356
|
-
i += 1;
|
|
357
|
-
}
|
|
358
|
-
continue;
|
|
359
|
-
}
|
|
360
|
-
if (ch === "/" && text[i + 1] === "*") {
|
|
361
|
-
const end = text.indexOf("*/", i + 2);
|
|
362
|
-
const stop = end === -1 ? text.length : end + 2;
|
|
363
|
-
while (i < stop) {
|
|
364
|
-
out += text[i] === "\n" ? "\n" : " ";
|
|
365
|
-
i += 1;
|
|
366
|
-
}
|
|
367
|
-
continue;
|
|
368
|
-
}
|
|
369
|
-
out += ch;
|
|
370
|
-
i += 1;
|
|
371
|
-
}
|
|
372
|
-
return out;
|
|
373
|
-
};
|
|
374
|
-
var asRecord = (value) => value && typeof value === "object" && !Array.isArray(value) ? value : void 0;
|
|
375
|
-
var parseJsonc = (text) => {
|
|
376
|
-
const blanked = blankJsonComments(text);
|
|
377
|
-
const withoutTrailingCommas = blanked.replace(/,(\s*[}\]])/g, "$1");
|
|
378
|
-
try {
|
|
379
|
-
return asRecord(JSON.parse(withoutTrailingCommas));
|
|
380
|
-
} catch {
|
|
381
|
-
return void 0;
|
|
382
|
-
}
|
|
383
|
-
};
|
|
384
|
-
var rootBraceIndex = (text) => blankJsonComments(text).indexOf("{");
|
|
385
|
-
var detectIndent = (text) => {
|
|
386
|
-
const match = /\n([ \t]+)"/.exec(text);
|
|
387
|
-
return match?.[1] ?? " ";
|
|
388
|
-
};
|
|
389
|
-
var freshDocument = (pluginDir, indent = " ") => `${JSON.stringify({ [PLUGIN_LOCATIONS_KEY]: { [pluginDir]: true } }, null, indent)}
|
|
390
|
-
`;
|
|
391
|
-
var registerPluginLocation = (existing, pluginDir) => {
|
|
392
|
-
if (existing === null || existing.trim() === "") {
|
|
393
|
-
return { kind: "created", content: freshDocument(pluginDir) };
|
|
394
|
-
}
|
|
395
|
-
const indent = detectIndent(existing);
|
|
396
|
-
try {
|
|
397
|
-
const parsed2 = asRecord(JSON.parse(existing));
|
|
398
|
-
if (parsed2) {
|
|
399
|
-
const locations2 = asRecord(parsed2[PLUGIN_LOCATIONS_KEY]) ?? {};
|
|
400
|
-
if (locations2[pluginDir] === true) return { kind: "already" };
|
|
401
|
-
parsed2[PLUGIN_LOCATIONS_KEY] = { ...locations2, [pluginDir]: true };
|
|
402
|
-
return { kind: "merged", content: `${JSON.stringify(parsed2, null, indent)}
|
|
403
|
-
` };
|
|
404
|
-
}
|
|
405
|
-
} catch {
|
|
406
|
-
}
|
|
407
|
-
const parsed = parseJsonc(existing);
|
|
408
|
-
if (!parsed) return { kind: "manual" };
|
|
409
|
-
const locations = asRecord(parsed[PLUGIN_LOCATIONS_KEY]);
|
|
410
|
-
if (locations?.[pluginDir] === true) return { kind: "already" };
|
|
411
|
-
if (parsed[PLUGIN_LOCATIONS_KEY] !== void 0) return { kind: "manual" };
|
|
412
|
-
const brace = rootBraceIndex(existing);
|
|
413
|
-
if (brace === -1) return { kind: "manual" };
|
|
414
|
-
const needsComma = Object.keys(parsed).length > 0;
|
|
415
|
-
const line = `
|
|
416
|
-
${indent}${JSON.stringify(PLUGIN_LOCATIONS_KEY)}: { ${JSON.stringify(pluginDir)}: true }${needsComma ? "," : ""}`;
|
|
417
|
-
return {
|
|
418
|
-
kind: "inserted",
|
|
419
|
-
content: `${existing.slice(0, brace + 1)}${line}${existing.slice(brace + 1)}`
|
|
420
|
-
};
|
|
421
|
-
};
|
|
422
|
-
var unregisterPluginLocation = (existing, pluginDir) => {
|
|
423
|
-
if (existing === null || existing.trim() === "") return { kind: "absent" };
|
|
424
|
-
const indent = detectIndent(existing);
|
|
425
|
-
try {
|
|
426
|
-
const parsed2 = asRecord(JSON.parse(existing));
|
|
427
|
-
if (parsed2) {
|
|
428
|
-
const locations2 = asRecord(parsed2[PLUGIN_LOCATIONS_KEY]);
|
|
429
|
-
if (!locations2 || !(pluginDir in locations2)) return { kind: "absent" };
|
|
430
|
-
const remaining = { ...locations2 };
|
|
431
|
-
delete remaining[pluginDir];
|
|
432
|
-
if (Object.keys(remaining).length === 0) delete parsed2[PLUGIN_LOCATIONS_KEY];
|
|
433
|
-
else parsed2[PLUGIN_LOCATIONS_KEY] = remaining;
|
|
434
|
-
return { kind: "removed", content: `${JSON.stringify(parsed2, null, indent)}
|
|
435
|
-
` };
|
|
436
|
-
}
|
|
437
|
-
} catch {
|
|
438
|
-
}
|
|
439
|
-
const parsed = parseJsonc(existing);
|
|
440
|
-
if (!parsed) return { kind: "manual" };
|
|
441
|
-
const locations = asRecord(parsed[PLUGIN_LOCATIONS_KEY]);
|
|
442
|
-
if (!locations || !(pluginDir in locations)) return { kind: "absent" };
|
|
443
|
-
const line = new RegExp(
|
|
444
|
-
`\\n[ \\t]*${escapeRegExp(JSON.stringify(PLUGIN_LOCATIONS_KEY))}\\s*:\\s*\\{\\s*${escapeRegExp(JSON.stringify(pluginDir))}\\s*:\\s*true\\s*\\},?`
|
|
445
|
-
);
|
|
446
|
-
if (!line.test(existing)) return { kind: "manual" };
|
|
447
|
-
return { kind: "removed", content: existing.replace(line, "") };
|
|
448
|
-
};
|
|
449
|
-
var escapeRegExp = (value) => value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
450
|
-
var isPluginRegistered = (existing, pluginDir) => {
|
|
451
|
-
if (!existing) return false;
|
|
452
|
-
const parsed = parseJsonc(existing);
|
|
453
|
-
return asRecord(parsed?.[PLUGIN_LOCATIONS_KEY])?.[pluginDir] === true;
|
|
454
|
-
};
|
|
455
|
-
var pluginLocationSnippet = (pluginDir) => `"${PLUGIN_LOCATIONS_KEY}": {
|
|
456
|
-
${JSON.stringify(pluginDir)}: true
|
|
457
|
-
}`;
|
|
458
|
-
var VSCODE_PRODUCTS = ["Code", "Code - Insiders"];
|
|
459
|
-
var settingsRoot = (home, platform) => {
|
|
460
|
-
if (platform === "darwin") return join2(home, "Library", "Application Support");
|
|
461
|
-
if (platform === "win32") return process.env.APPDATA?.trim() || join2(home, "AppData", "Roaming");
|
|
462
|
-
return process.env.XDG_CONFIG_HOME?.trim() || join2(home, ".config");
|
|
463
|
-
};
|
|
464
|
-
var vscodeSettingsCandidates = (home = homedir2(), platform = process.platform) => {
|
|
465
|
-
const root = settingsRoot(home, platform);
|
|
466
|
-
return VSCODE_PRODUCTS.map((product) => join2(root, product, "User", "settings.json"));
|
|
467
|
-
};
|
|
468
|
-
var vscodeSettingsTargets = (exists, home, platform) => {
|
|
469
|
-
const candidates = vscodeSettingsCandidates(home, platform);
|
|
470
|
-
const present = candidates.filter((path) => exists(path));
|
|
471
|
-
return present.length > 0 ? present : candidates.slice(0, 1);
|
|
472
|
-
};
|
|
473
|
-
|
|
474
|
-
// src/setup/detect.ts
|
|
475
|
-
import { execSync } from "child_process";
|
|
476
|
-
import { existsSync as existsSync2 } from "fs";
|
|
477
|
-
import { homedir as homedir3 } from "os";
|
|
478
|
-
import { dirname, join as join3 } from "path";
|
|
479
|
-
var whichCommand = () => process.platform === "win32" ? "where" : "which";
|
|
480
|
-
var binaryOnPath = (binary) => {
|
|
481
|
-
try {
|
|
482
|
-
execSync(`${whichCommand()} ${binary}`, { stdio: "ignore", timeout: 5e3 });
|
|
483
|
-
return true;
|
|
484
|
-
} catch {
|
|
485
|
-
return false;
|
|
486
|
-
}
|
|
487
|
-
};
|
|
488
|
-
var defaultDeps = { onPath: binaryOnPath, exists: existsSync2 };
|
|
489
|
-
var detectAgent = (probe, deps = defaultDeps) => {
|
|
490
|
-
const checked = [];
|
|
491
|
-
if (probe.binary) {
|
|
492
|
-
checked.push(`${whichCommand()} ${probe.binary}`);
|
|
493
|
-
if (deps.onPath(probe.binary)) {
|
|
494
|
-
return { kind: "installed", evidence: `${probe.binary} on PATH`, strong: true };
|
|
495
|
-
}
|
|
496
|
-
}
|
|
497
|
-
for (const path of probe.paths) {
|
|
498
|
-
checked.push(path);
|
|
499
|
-
if (deps.exists(path)) return { kind: "installed", evidence: path, strong: true };
|
|
500
|
-
}
|
|
501
|
-
for (const marker of probe.configMarkers) {
|
|
502
|
-
checked.push(marker);
|
|
503
|
-
if (deps.exists(marker)) return { kind: "installed", evidence: marker, strong: false };
|
|
504
|
-
}
|
|
505
|
-
if (probe.binary === null && probe.paths.length === 0 && probe.configMarkers.length === 0) {
|
|
506
|
-
return { kind: "unknown" };
|
|
507
|
-
}
|
|
508
|
-
return { kind: "not-found", checked };
|
|
509
|
-
};
|
|
510
|
-
var agentProbes = (home = homedir3(), platform = process.platform) => ({
|
|
511
|
-
claude_code: {
|
|
512
|
-
binary: "claude",
|
|
513
|
-
// The native installer puts it here, outside a default non-login PATH.
|
|
514
|
-
paths: [nativeClaudeInstallPath(home, platform)],
|
|
515
|
-
configMarkers: [claudeConfigDir(home)]
|
|
516
|
-
},
|
|
517
|
-
codex: {
|
|
518
|
-
binary: "codex",
|
|
519
|
-
paths: [],
|
|
520
|
-
configMarkers: [codexHomeFrom(home)]
|
|
521
|
-
},
|
|
522
|
-
gemini_cli: {
|
|
523
|
-
binary: "gemini",
|
|
524
|
-
paths: [],
|
|
525
|
-
configMarkers: [join3(home, ".gemini")]
|
|
526
|
-
},
|
|
527
|
-
hermes: {
|
|
528
|
-
binary: "hermes",
|
|
529
|
-
paths: [],
|
|
530
|
-
configMarkers: [join3(home, ".hermes")]
|
|
531
|
-
},
|
|
532
|
-
cursor: {
|
|
533
|
-
// GUI editor. `cursor` on PATH only exists if the user ran "Install 'cursor'
|
|
534
|
-
// command in PATH" from the command palette, which most never do.
|
|
535
|
-
binary: "cursor",
|
|
536
|
-
paths: platform === "darwin" ? ["/Applications/Cursor.app"] : platform === "win32" ? [join3(process.env.LOCALAPPDATA ?? join3(home, "AppData", "Local"), "Programs", "cursor")] : ["/usr/share/cursor", join3(home, ".local", "share", "cursor")],
|
|
537
|
-
configMarkers: [join3(home, ".cursor")]
|
|
538
|
-
},
|
|
539
|
-
vscode: {
|
|
540
|
-
// Like Cursor, a GUI editor: `code` on PATH only exists if the user ran
|
|
541
|
-
// "Shell Command: Install 'code' command in PATH", which on macOS is opt-in.
|
|
542
|
-
binary: "code",
|
|
543
|
-
paths: platform === "darwin" ? ["/Applications/Visual Studio Code.app"] : platform === "win32" ? [join3(process.env.LOCALAPPDATA ?? join3(home, "AppData", "Local"), "Programs", "Microsoft VS Code")] : ["/usr/share/code", join3(home, ".local", "share", "code")],
|
|
544
|
-
// The settings directory rather than ~/.vscode, because the settings
|
|
545
|
-
// directory is what this installer actually writes to. Insiders counts:
|
|
546
|
-
// someone running only Insiders still has VS Code.
|
|
547
|
-
configMarkers: vscodeSettingsCandidates(home).map((path) => dirname(path))
|
|
548
|
-
},
|
|
549
|
-
opencode: {
|
|
550
|
-
binary: "opencode",
|
|
551
|
-
paths: [],
|
|
552
|
-
// Resolved rather than spelled: OpenCode honours OPENCODE_CONFIG_DIR and then
|
|
553
|
-
// XDG_CONFIG_HOME, and `~/.opencode` — the obvious guess — is a path it has
|
|
554
|
-
// never used.
|
|
555
|
-
configMarkers: [openCodeConfigDirFrom(home)]
|
|
556
|
-
},
|
|
557
|
-
fx: {
|
|
558
|
-
binary: null,
|
|
559
|
-
paths: [join3(home, ".fx", "settings.json"), join3(home, ".fx", "sessions"), join3(home, ".fx", "history.jsonl")],
|
|
560
|
-
configMarkers: []
|
|
561
|
-
},
|
|
562
|
-
cline: {
|
|
563
|
-
binary: "cline",
|
|
564
|
-
paths: [clineDataDirectory({ home, environment: process.env })],
|
|
565
|
-
configMarkers: []
|
|
566
|
-
},
|
|
567
|
-
...compatibleProbes(),
|
|
568
|
-
grok: { binary: "grok", paths: [join3(home, ".local", "bin", "grok")], configMarkers: [grokHome(home)] },
|
|
569
|
-
// "Other" is a set of printed instructions for any MCP or HTTP client. There is
|
|
570
|
-
// nothing to detect, and it must never be auto-selected.
|
|
571
|
-
custom: { binary: null, paths: [], configMarkers: [] }
|
|
572
|
-
});
|
|
573
|
-
var compatibleProbes = () => HOOK_COMPATIBLE_PROFILES.reduce((probes, profile) => ({
|
|
574
|
-
...probes,
|
|
575
|
-
[profile.agentType]: {
|
|
576
|
-
binary: profile.binary,
|
|
577
|
-
paths: [],
|
|
578
|
-
configMarkers: []
|
|
579
|
-
}
|
|
580
|
-
}), {});
|
|
581
|
-
var detectAllAgents = (deps = defaultDeps, home) => {
|
|
582
|
-
const probes = agentProbes(home);
|
|
583
|
-
return Object.keys(probes).map((agent) => ({
|
|
584
|
-
agent,
|
|
585
|
-
result: detectAgent(probes[agent], deps)
|
|
586
|
-
}));
|
|
587
|
-
};
|
|
588
|
-
var isDetected = (result) => result.kind === "installed";
|
|
589
|
-
var shortenHome = (path, home = homedir3()) => home && path.startsWith(home) ? `~${path.slice(home.length)}` : path;
|
|
590
|
-
var describeDetection = (result, home) => {
|
|
591
|
-
if (result.kind !== "installed") return null;
|
|
592
|
-
const evidence = shortenHome(result.evidence, home);
|
|
593
|
-
return result.strong ? evidence : `${evidence}, config only`;
|
|
594
|
-
};
|
|
595
|
-
|
|
596
|
-
export {
|
|
597
|
-
isHermesConfigReceipt,
|
|
598
|
-
HERMES_PIP_PACKAGE,
|
|
599
|
-
HERMES_PLUGIN_NAME,
|
|
600
|
-
resolveHermesPython,
|
|
601
|
-
runHermesConfigScript,
|
|
602
|
-
hermesWiredByPushary,
|
|
603
|
-
readHermesStatus,
|
|
604
|
-
registerPluginLocation,
|
|
605
|
-
unregisterPluginLocation,
|
|
606
|
-
isPluginRegistered,
|
|
607
|
-
pluginLocationSnippet,
|
|
608
|
-
vscodeSettingsTargets,
|
|
609
|
-
detectAgent,
|
|
610
|
-
agentProbes,
|
|
611
|
-
detectAllAgents,
|
|
612
|
-
isDetected,
|
|
613
|
-
shortenHome,
|
|
614
|
-
describeDetection
|
|
615
|
-
};
|