@pushary/agent-hooks 0.90.1 → 0.90.2
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 +13 -0
- package/dist/bin/pushary-bell.js +6 -61
- package/dist/bin/pushary-claude.js +5 -5
- package/dist/bin/pushary-clean.js +96 -13
- package/dist/bin/pushary-codex-bridge.js +3 -3
- package/dist/bin/pushary-codex-hook.js +3 -3
- package/dist/bin/pushary-codex.js +3 -3
- package/dist/bin/pushary-connect.js +2 -2
- package/dist/bin/pushary-daemon.js +9 -9
- package/dist/bin/pushary-disconnect.js +2 -2
- package/dist/bin/pushary-doctor.js +13 -13
- package/dist/bin/pushary-elicitation-hook.js +2 -2
- package/dist/bin/pushary-gemini-bridge.js +3 -3
- package/dist/bin/pushary-gemini-hook.js +3 -3
- package/dist/bin/pushary-hook.js +4 -4
- package/dist/bin/pushary-login.js +4 -4
- package/dist/bin/pushary-logout.js +1 -1
- package/dist/bin/pushary-mode.js +2 -2
- package/dist/bin/pushary-notification-hook.js +3 -3
- package/dist/bin/pushary-opencode-hook.js +3 -3
- package/dist/bin/pushary-permission-denied-hook.js +4 -4
- package/dist/bin/pushary-permission-hook.js +4 -4
- package/dist/bin/pushary-post-hook.js +3 -3
- package/dist/bin/pushary-prompt-hook.js +3 -3
- package/dist/bin/pushary-session-end-hook.js +3 -3
- package/dist/bin/pushary-session-start-hook.js +3 -3
- package/dist/bin/pushary-setup.js +104 -179
- package/dist/bin/pushary-status.js +4 -4
- package/dist/bin/pushary-stop-hook.js +3 -3
- package/dist/bin/pushary-stopfailure-hook.js +3 -3
- package/dist/bin/pushary-upgrade.js +8 -8
- package/dist/bin/pushary-wait.js +2 -2
- package/dist/{chunk-B6H2KAQC.js → chunk-4IEW4ZRF.js} +2 -2
- package/dist/chunk-ATUUQFDH.js +536 -0
- package/dist/{chunk-54ELC7D7.js → chunk-AWHCZOZO.js} +1 -1
- package/dist/{chunk-KUZWXPUS.js → chunk-BVNYTQ5I.js} +23 -0
- package/dist/{chunk-AJYV3VGU.js → chunk-BXTJVM7O.js} +2 -0
- package/dist/{chunk-FQL5E6NG.js → chunk-CRVJCKIN.js} +85 -2
- package/dist/chunk-DSTLA2TC.js +68 -0
- package/dist/{chunk-GL3FFM7A.js → chunk-ELHCIEMM.js} +1 -1
- package/dist/{chunk-Y6BWPERV.js → chunk-GQ5ZC2QY.js} +1 -1
- package/dist/chunk-GU3EJCVV.js +43 -0
- package/dist/{chunk-4LOPSGLP.js → chunk-TVZQMPB7.js} +1 -1
- package/dist/{chunk-BQBEH6TV.js → chunk-VAMBUKXH.js} +4 -4
- package/dist/{chunk-DZDI62LY.js → chunk-WGKC6PU2.js} +2 -2
- package/dist/{chunk-7TPQEWUZ.js → chunk-YTD6PJM7.js} +1 -1
- package/dist/{chunk-H2QZRQR2.js → chunk-YTDH3XCE.js} +1 -1
- package/dist/{chunk-F2ICEWZL.js → chunk-Z4XCN2HH.js} +2 -2
- package/dist/{chunk-DYP6SMOR.js → chunk-Z6RFZKY4.js} +1 -1
- package/dist/src/index.js +4 -4
- package/package.json +1 -1
- package/dist/chunk-AZPIQG4G.js +0 -274
- package/dist/chunk-S6LYP7QA.js +0 -127
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import {
|
|
3
3
|
reapplyAllAgents
|
|
4
|
-
} from "../chunk-
|
|
5
|
-
import "../chunk-
|
|
4
|
+
} from "../chunk-VAMBUKXH.js";
|
|
5
|
+
import "../chunk-YTDH3XCE.js";
|
|
6
6
|
import {
|
|
7
7
|
waitForMachineReadiness
|
|
8
|
-
} from "../chunk-
|
|
9
|
-
import "../chunk-
|
|
10
|
-
import "../chunk-
|
|
8
|
+
} from "../chunk-YTD6PJM7.js";
|
|
9
|
+
import "../chunk-WGKC6PU2.js";
|
|
10
|
+
import "../chunk-ELHCIEMM.js";
|
|
11
11
|
import "../chunk-2LAHXHBR.js";
|
|
12
12
|
import "../chunk-EZIEXHYM.js";
|
|
13
13
|
import {
|
|
@@ -20,7 +20,7 @@ import {
|
|
|
20
20
|
replaceDaemonService,
|
|
21
21
|
stopDaemonService,
|
|
22
22
|
waitForDaemonRemoteProof
|
|
23
|
-
} from "../chunk-
|
|
23
|
+
} from "../chunk-Z4XCN2HH.js";
|
|
24
24
|
import {
|
|
25
25
|
detectProviderLiveCapabilities,
|
|
26
26
|
hasSpawnProvider,
|
|
@@ -34,14 +34,14 @@ import {
|
|
|
34
34
|
npmErrorMessage,
|
|
35
35
|
resolveGlobalBinary
|
|
36
36
|
} from "../chunk-L4ZCFMWW.js";
|
|
37
|
-
import "../chunk-
|
|
37
|
+
import "../chunk-AWHCZOZO.js";
|
|
38
38
|
import {
|
|
39
39
|
claudeJson,
|
|
40
40
|
codexConfigToml,
|
|
41
41
|
cursorPluginDir,
|
|
42
42
|
geminiSettings,
|
|
43
43
|
vscodePluginDir
|
|
44
|
-
} from "../chunk-
|
|
44
|
+
} from "../chunk-BVNYTQ5I.js";
|
|
45
45
|
import {
|
|
46
46
|
exitOnHelpFlag
|
|
47
47
|
} from "../chunk-BVBEURWJ.js";
|
package/dist/bin/pushary-wait.js
CHANGED
|
@@ -4,8 +4,8 @@ import {
|
|
|
4
4
|
} from "../chunk-IIENZE2J.js";
|
|
5
5
|
import {
|
|
6
6
|
exitOnFailedResponse
|
|
7
|
-
} from "../chunk-
|
|
8
|
-
import "../chunk-
|
|
7
|
+
} from "../chunk-Z6RFZKY4.js";
|
|
8
|
+
import "../chunk-BXTJVM7O.js";
|
|
9
9
|
import {
|
|
10
10
|
createIo
|
|
11
11
|
} from "../chunk-5RUIFDTP.js";
|
|
@@ -21,7 +21,7 @@ import {
|
|
|
21
21
|
repoKeyFor,
|
|
22
22
|
settleQuestionsForToolUse,
|
|
23
23
|
throttlePass
|
|
24
|
-
} from "./chunk-
|
|
24
|
+
} from "./chunk-TVZQMPB7.js";
|
|
25
25
|
import {
|
|
26
26
|
DEFAULT_SESSION,
|
|
27
27
|
askUser,
|
|
@@ -41,7 +41,7 @@ import {
|
|
|
41
41
|
} from "./chunk-L4E2BIJ2.js";
|
|
42
42
|
import {
|
|
43
43
|
getMachineId
|
|
44
|
-
} from "./chunk-
|
|
44
|
+
} from "./chunk-AWHCZOZO.js";
|
|
45
45
|
import {
|
|
46
46
|
getApiKey,
|
|
47
47
|
getBaseUrl
|
|
@@ -0,0 +1,536 @@
|
|
|
1
|
+
import {
|
|
2
|
+
isBridgeLocator
|
|
3
|
+
} from "./chunk-CRVJCKIN.js";
|
|
4
|
+
import {
|
|
5
|
+
pusharyHookExecutable
|
|
6
|
+
} from "./chunk-WGKC6PU2.js";
|
|
7
|
+
import {
|
|
8
|
+
BELL_HOOK_BINARY
|
|
9
|
+
} from "./chunk-DSTLA2TC.js";
|
|
10
|
+
import {
|
|
11
|
+
resolvePathBinary
|
|
12
|
+
} from "./chunk-L4ZCFMWW.js";
|
|
13
|
+
import {
|
|
14
|
+
parseFlags
|
|
15
|
+
} from "./chunk-GMXKITVA.js";
|
|
16
|
+
import {
|
|
17
|
+
UsageError
|
|
18
|
+
} from "./chunk-7QLSKOSU.js";
|
|
19
|
+
import {
|
|
20
|
+
isValidApiKey
|
|
21
|
+
} from "./chunk-VNMKIJZB.js";
|
|
22
|
+
|
|
23
|
+
// src/hermes-config.ts
|
|
24
|
+
import { execFileSync } from "child_process";
|
|
25
|
+
import { existsSync, readFileSync, rmSync, writeFileSync } from "fs";
|
|
26
|
+
import { homedir, tmpdir } from "os";
|
|
27
|
+
import { join } from "path";
|
|
28
|
+
var isRecord = (value) => Boolean(value) && typeof value === "object" && !Array.isArray(value);
|
|
29
|
+
var isSnapshot = (value) => isRecord(value) && typeof value.present === "boolean" && Object.hasOwn(value, "value");
|
|
30
|
+
var isHermesConfigReceipt = (value) => {
|
|
31
|
+
if (!isRecord(value)) return false;
|
|
32
|
+
return ["security", "approval", "transport", "fallback", "plugins", "enabled", "agent", "disabled"].every((key) => isSnapshot(value[key])) && typeof value.pushary_enabled === "boolean" && typeof value.clarify_disabled === "boolean";
|
|
33
|
+
};
|
|
34
|
+
var HERMES_CONFIG_SCRIPT = String.raw`
|
|
35
|
+
import json, os, sys
|
|
36
|
+
from hermes_cli.config import load_config, save_config
|
|
37
|
+
|
|
38
|
+
mode, receipt_path = sys.argv[1], sys.argv[2]
|
|
39
|
+
c = load_config()
|
|
40
|
+
|
|
41
|
+
def snap(obj, key):
|
|
42
|
+
return {"present": key in obj, "value": obj.get(key)}
|
|
43
|
+
|
|
44
|
+
restore_conflicts = []
|
|
45
|
+
|
|
46
|
+
def restore(obj, key, before, ours):
|
|
47
|
+
if before.get("present") and before.get("value") == ours:
|
|
48
|
+
return
|
|
49
|
+
if before.get("present"):
|
|
50
|
+
if key in obj and obj.get(key) == before.get("value"):
|
|
51
|
+
return
|
|
52
|
+
elif key not in obj:
|
|
53
|
+
return
|
|
54
|
+
if obj.get(key) != ours:
|
|
55
|
+
restore_conflicts.append(key)
|
|
56
|
+
return
|
|
57
|
+
if before.get("present"):
|
|
58
|
+
obj[key] = before.get("value")
|
|
59
|
+
else:
|
|
60
|
+
obj.pop(key, None)
|
|
61
|
+
|
|
62
|
+
def restore_container(obj, key, before, current, changed):
|
|
63
|
+
raw = obj.get(key)
|
|
64
|
+
if isinstance(before.get("value"), dict):
|
|
65
|
+
if not isinstance(raw, dict):
|
|
66
|
+
if changed:
|
|
67
|
+
restore_conflicts.append(key)
|
|
68
|
+
return
|
|
69
|
+
if current or before.get("present"):
|
|
70
|
+
obj[key] = current
|
|
71
|
+
else:
|
|
72
|
+
obj.pop(key, None)
|
|
73
|
+
elif isinstance(raw, dict) and current:
|
|
74
|
+
restore_conflicts.append(key)
|
|
75
|
+
else:
|
|
76
|
+
restore(obj, key, before, {})
|
|
77
|
+
|
|
78
|
+
def restore_list_member(obj, key, before, ours, ours_before):
|
|
79
|
+
raw = obj.get(key)
|
|
80
|
+
if isinstance(before.get("value"), list):
|
|
81
|
+
if ours_before:
|
|
82
|
+
return
|
|
83
|
+
if not isinstance(raw, list):
|
|
84
|
+
restore_conflicts.append(key)
|
|
85
|
+
return
|
|
86
|
+
remaining = [item for item in raw if item != ours]
|
|
87
|
+
if remaining or before.get("present"):
|
|
88
|
+
obj[key] = remaining
|
|
89
|
+
else:
|
|
90
|
+
obj.pop(key, None)
|
|
91
|
+
return
|
|
92
|
+
if before.get("present") and raw == before.get("value"):
|
|
93
|
+
return
|
|
94
|
+
if not before.get("present") and key not in obj:
|
|
95
|
+
return
|
|
96
|
+
if not isinstance(raw, list):
|
|
97
|
+
restore_conflicts.append(key)
|
|
98
|
+
return
|
|
99
|
+
remaining = [item for item in raw if item != ours]
|
|
100
|
+
if remaining:
|
|
101
|
+
restore_conflicts.append(key)
|
|
102
|
+
else:
|
|
103
|
+
restore(obj, key, before, raw)
|
|
104
|
+
|
|
105
|
+
def valid_snapshot(value):
|
|
106
|
+
return isinstance(value, dict) and isinstance(value.get("present"), bool) and "value" in value
|
|
107
|
+
|
|
108
|
+
def valid_receipt(value):
|
|
109
|
+
snapshots = ["security", "approval", "transport", "fallback", "plugins", "enabled", "agent", "disabled"]
|
|
110
|
+
return (isinstance(value, dict)
|
|
111
|
+
and all(valid_snapshot(value.get(key)) for key in snapshots)
|
|
112
|
+
and isinstance(value.get("pushary_enabled"), bool)
|
|
113
|
+
and isinstance(value.get("clarify_disabled"), bool))
|
|
114
|
+
|
|
115
|
+
if mode == "setup":
|
|
116
|
+
security_before = snap(c, "security")
|
|
117
|
+
s = c.get("security") if isinstance(c.get("security"), dict) else {}
|
|
118
|
+
approval_before = snap(s, "approval")
|
|
119
|
+
ap = s.get("approval") if isinstance(s.get("approval"), dict) else {}
|
|
120
|
+
plugins_before = snap(c, "plugins")
|
|
121
|
+
p = c.get("plugins") if isinstance(c.get("plugins"), dict) else {}
|
|
122
|
+
agent_before = snap(c, "agent")
|
|
123
|
+
a = c.get("agent") if isinstance(c.get("agent"), dict) else {}
|
|
124
|
+
enabled = p.get("enabled") if isinstance(p.get("enabled"), list) else []
|
|
125
|
+
disabled = a.get("disabled_toolsets") if isinstance(a.get("disabled_toolsets"), list) else []
|
|
126
|
+
receipt = {
|
|
127
|
+
"security": security_before,
|
|
128
|
+
"approval": approval_before,
|
|
129
|
+
"transport": snap(ap, "transport"),
|
|
130
|
+
"fallback": snap(ap, "transport_fallback"),
|
|
131
|
+
"plugins": plugins_before,
|
|
132
|
+
"enabled": snap(p, "enabled"),
|
|
133
|
+
"pushary_enabled": "pushary" in enabled,
|
|
134
|
+
"agent": agent_before,
|
|
135
|
+
"disabled": snap(a, "disabled_toolsets"),
|
|
136
|
+
"clarify_disabled": "clarify" in disabled,
|
|
137
|
+
}
|
|
138
|
+
if os.path.exists(receipt_path):
|
|
139
|
+
with open(receipt_path, encoding="utf-8") as handle:
|
|
140
|
+
existing_receipt = json.load(handle)
|
|
141
|
+
if not valid_receipt(existing_receipt):
|
|
142
|
+
raise ValueError("invalid Pushary Hermes setup receipt")
|
|
143
|
+
else:
|
|
144
|
+
os.makedirs(os.path.dirname(receipt_path), exist_ok=True)
|
|
145
|
+
temp = receipt_path + ".tmp"
|
|
146
|
+
with open(temp, "x", encoding="utf-8") as handle:
|
|
147
|
+
json.dump(receipt, handle)
|
|
148
|
+
os.chmod(temp, 0o600)
|
|
149
|
+
os.replace(temp, receipt_path)
|
|
150
|
+
ap["transport"] = "pushary"
|
|
151
|
+
ap["transport_fallback"] = "builtin"
|
|
152
|
+
s["approval"] = ap
|
|
153
|
+
c["security"] = s
|
|
154
|
+
p["enabled"] = enabled if "pushary" in enabled else enabled + ["pushary"]
|
|
155
|
+
c["plugins"] = p
|
|
156
|
+
a["disabled_toolsets"] = disabled if "clarify" in disabled else disabled + ["clarify"]
|
|
157
|
+
c["agent"] = a
|
|
158
|
+
else:
|
|
159
|
+
receipt_exists = os.path.exists(receipt_path)
|
|
160
|
+
try:
|
|
161
|
+
with open(receipt_path, encoding="utf-8") as handle:
|
|
162
|
+
receipt = json.load(handle)
|
|
163
|
+
except (OSError, ValueError):
|
|
164
|
+
if receipt_exists:
|
|
165
|
+
raise
|
|
166
|
+
receipt = None
|
|
167
|
+
s = c.get("security") if isinstance(c.get("security"), dict) else {}
|
|
168
|
+
ap = s.get("approval") if isinstance(s.get("approval"), dict) else {}
|
|
169
|
+
if receipt:
|
|
170
|
+
if not valid_receipt(receipt):
|
|
171
|
+
raise ValueError("invalid Pushary Hermes setup receipt")
|
|
172
|
+
approval_changed = (not receipt["transport"].get("present")
|
|
173
|
+
or receipt["transport"].get("value") != "pushary"
|
|
174
|
+
or not receipt["fallback"].get("present")
|
|
175
|
+
or receipt["fallback"].get("value") != "builtin")
|
|
176
|
+
plugins_changed = not (isinstance(receipt["enabled"].get("value"), list)
|
|
177
|
+
and receipt["pushary_enabled"])
|
|
178
|
+
agent_changed = not (isinstance(receipt["disabled"].get("value"), list)
|
|
179
|
+
and receipt["clarify_disabled"])
|
|
180
|
+
restore(ap, "transport", receipt["transport"], "pushary")
|
|
181
|
+
restore(ap, "transport_fallback", receipt["fallback"], "builtin")
|
|
182
|
+
else:
|
|
183
|
+
if ap.get("transport") == "pushary": ap.pop("transport", None)
|
|
184
|
+
if ap.get("transport_fallback") == "builtin": ap.pop("transport_fallback", None)
|
|
185
|
+
if receipt:
|
|
186
|
+
restore_container(s, "approval", receipt["approval"], ap, approval_changed)
|
|
187
|
+
restore_container(c, "security", receipt["security"], s, approval_changed)
|
|
188
|
+
else:
|
|
189
|
+
if ap: s["approval"] = ap
|
|
190
|
+
else: s.pop("approval", None)
|
|
191
|
+
if s: c["security"] = s
|
|
192
|
+
else: c.pop("security", None)
|
|
193
|
+
|
|
194
|
+
p = c.get("plugins") if isinstance(c.get("plugins"), dict) else {}
|
|
195
|
+
if receipt:
|
|
196
|
+
restore_list_member(p, "enabled", receipt["enabled"], "pushary", receipt["pushary_enabled"])
|
|
197
|
+
restore_container(c, "plugins", receipt["plugins"], p, plugins_changed)
|
|
198
|
+
else:
|
|
199
|
+
enabled = p.get("enabled") if isinstance(p.get("enabled"), list) else []
|
|
200
|
+
enabled = [item for item in enabled if item != "pushary"]
|
|
201
|
+
if enabled: p["enabled"] = enabled
|
|
202
|
+
else: p.pop("enabled", None)
|
|
203
|
+
if p: c["plugins"] = p
|
|
204
|
+
else: c.pop("plugins", None)
|
|
205
|
+
|
|
206
|
+
a = c.get("agent") if isinstance(c.get("agent"), dict) else {}
|
|
207
|
+
if receipt:
|
|
208
|
+
restore_list_member(a, "disabled_toolsets", receipt["disabled"], "clarify", receipt["clarify_disabled"])
|
|
209
|
+
restore_container(c, "agent", receipt["agent"], a, agent_changed)
|
|
210
|
+
else:
|
|
211
|
+
disabled = a.get("disabled_toolsets") if isinstance(a.get("disabled_toolsets"), list) else []
|
|
212
|
+
disabled = [item for item in disabled if item != "clarify"]
|
|
213
|
+
if disabled: a["disabled_toolsets"] = disabled
|
|
214
|
+
else: a.pop("disabled_toolsets", None)
|
|
215
|
+
if a: c["agent"] = a
|
|
216
|
+
else: c.pop("agent", None)
|
|
217
|
+
|
|
218
|
+
if receipt and restore_conflicts:
|
|
219
|
+
raise ValueError("Hermes config changed after Pushary setup")
|
|
220
|
+
|
|
221
|
+
save_config(c)
|
|
222
|
+
`;
|
|
223
|
+
var resolveHermesPython = () => {
|
|
224
|
+
const launcher = resolvePathBinary("hermes");
|
|
225
|
+
if (launcher && process.platform !== "win32") {
|
|
226
|
+
try {
|
|
227
|
+
const shebang = readFileSync(launcher, "utf-8").split("\n", 1)[0];
|
|
228
|
+
const interpreter = shebang?.startsWith("#!") ? shebang.slice(2).trim().split(/\s+/)[0] : void 0;
|
|
229
|
+
if (interpreter && /python/i.test(interpreter) && existsSync(interpreter)) return interpreter;
|
|
230
|
+
} catch {
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
const root = join(homedir(), ".hermes", "hermes-agent", "venv");
|
|
234
|
+
const candidates = process.platform === "win32" ? [join(root, "Scripts", "python.exe"), join(root, "Scripts", "python3.exe")] : [join(root, "bin", "python3"), join(root, "bin", "python")];
|
|
235
|
+
return candidates.find(existsSync) ?? null;
|
|
236
|
+
};
|
|
237
|
+
var runHermesPythonScript = (python, script, ...args) => {
|
|
238
|
+
const path = join(tmpdir(), `pushary-hermes-${process.pid}-${Date.now()}.py`);
|
|
239
|
+
try {
|
|
240
|
+
writeFileSync(path, script, { encoding: "utf-8", mode: 384 });
|
|
241
|
+
execFileSync(python, [path, ...args], { env: { ...process.env }, stdio: "pipe", timeout: 15e3 });
|
|
242
|
+
return true;
|
|
243
|
+
} catch {
|
|
244
|
+
return false;
|
|
245
|
+
} finally {
|
|
246
|
+
try {
|
|
247
|
+
rmSync(path, { force: true });
|
|
248
|
+
} catch {
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
};
|
|
252
|
+
|
|
253
|
+
// src/cli/dry-run.ts
|
|
254
|
+
import { execSync } from "child_process";
|
|
255
|
+
import { rmSync as rmSync2, writeFileSync as writeFileSync2 } from "fs";
|
|
256
|
+
var createGuards = (isDryRun, note) => ({
|
|
257
|
+
write: (path, contents) => {
|
|
258
|
+
if (isDryRun()) return note("rewrite", path);
|
|
259
|
+
writeFileSync2(path, contents, "utf-8");
|
|
260
|
+
},
|
|
261
|
+
remove: (path, options = {}) => {
|
|
262
|
+
if (isDryRun()) return note("remove ", path);
|
|
263
|
+
rmSync2(path, { recursive: true, force: options.force ?? false });
|
|
264
|
+
},
|
|
265
|
+
exec: (command, options, describe) => {
|
|
266
|
+
if (isDryRun()) return note("run ", describe);
|
|
267
|
+
execSync(command, options);
|
|
268
|
+
}
|
|
269
|
+
});
|
|
270
|
+
var describeService = (effects) => {
|
|
271
|
+
if (effects.controlMode === "off") return "skip the phone-start background service (--control off)";
|
|
272
|
+
const where = effects.serviceDefinitionPath ?? "the phone-start service definition";
|
|
273
|
+
const condition = effects.controlMode === "on" ? "" : ", when a supported agent and a service manager are present";
|
|
274
|
+
return `write ${where} and register it with the OS${condition}`;
|
|
275
|
+
};
|
|
276
|
+
var describeSetupSideEffects = (effects) => [
|
|
277
|
+
`run npm install -g @pushary/agent-hooks@${effects.version} (the hooks run from the global copy; up to two minutes)`,
|
|
278
|
+
`write ${effects.configFile} (the API key, mode 600)`,
|
|
279
|
+
effects.rcPath ? `edit ${effects.rcPath} (export PUSHARY_API_KEY)` : "print the export line for your shell, since no rc file was found",
|
|
280
|
+
describeService(effects),
|
|
281
|
+
"ask Add Pushary instructions to this project's CLAUDE.md/AGENTS.md? (default no)",
|
|
282
|
+
"ask Alias claude to pushary claude? (default no)"
|
|
283
|
+
];
|
|
284
|
+
|
|
285
|
+
// src/setup/options.ts
|
|
286
|
+
var AGENT_CHOICES = ["claude_code", "codex", "gemini_cli", "hermes", "cursor", "vscode", "opencode", "custom"];
|
|
287
|
+
var CONNECT_MODES = ["app", "browser", "web", "none"];
|
|
288
|
+
var CONNECT_ALIASES = { auto: "app", pwa: "web" };
|
|
289
|
+
var parseConnectMode = (raw) => {
|
|
290
|
+
const value = raw.trim().toLowerCase();
|
|
291
|
+
if (Object.hasOwn(CONNECT_ALIASES, value)) return CONNECT_ALIASES[value];
|
|
292
|
+
if (CONNECT_MODES.includes(value)) return value;
|
|
293
|
+
throw new UsageError(`--connect must be one of: ${CONNECT_MODES.join(", ")} (got ${raw})`);
|
|
294
|
+
};
|
|
295
|
+
var VERIFY_MODES = ["roundtrip", "push", "none"];
|
|
296
|
+
var parseVerifyMode = (raw) => {
|
|
297
|
+
const value = raw.trim().toLowerCase();
|
|
298
|
+
if (VERIFY_MODES.includes(value)) return value;
|
|
299
|
+
throw new UsageError(`--verify must be one of: ${VERIFY_MODES.join(", ")} (got ${raw})`);
|
|
300
|
+
};
|
|
301
|
+
var CONTROL_MODES = ["auto", "on", "off"];
|
|
302
|
+
var parseControlMode = (raw) => {
|
|
303
|
+
const value = raw.trim().toLowerCase();
|
|
304
|
+
if (CONTROL_MODES.includes(value)) return value;
|
|
305
|
+
throw new UsageError(`--control must be one of: ${CONTROL_MODES.join(", ")} (got ${raw})`);
|
|
306
|
+
};
|
|
307
|
+
var effectiveVerify = (options, context) => {
|
|
308
|
+
if (options.verifyExplicit) return options.verify;
|
|
309
|
+
if (options.verify === "roundtrip") return "roundtrip";
|
|
310
|
+
return context.paired ? "roundtrip" : options.verify;
|
|
311
|
+
};
|
|
312
|
+
var SETUP_FLAGS = {
|
|
313
|
+
bools: ["key-stdin", "skip-phone", "yes", "y", "json", "dry-run", "take-over-hooks", "help", "h"],
|
|
314
|
+
values: ["key", "connect", "agents", "verify", "control", "transcripts"]
|
|
315
|
+
};
|
|
316
|
+
var effectiveControlMode = (options, stored, configPresent) => options.controlExplicit ? options.control : stored ?? (configPresent ? null : options.control);
|
|
317
|
+
var isAgentChoice = (value) => AGENT_CHOICES.includes(value);
|
|
318
|
+
var parseAgentSelection = (raw, yes) => {
|
|
319
|
+
if (raw === void 0) {
|
|
320
|
+
return yes ? { kind: "auto" } : { kind: "ask" };
|
|
321
|
+
}
|
|
322
|
+
const value = raw.trim().toLowerCase();
|
|
323
|
+
if (value === "auto") return { kind: "auto" };
|
|
324
|
+
if (value === "none") return { kind: "none" };
|
|
325
|
+
const parts = value.split(",").map((part) => part.trim()).filter((part) => part !== "");
|
|
326
|
+
if (parts.length === 0) throw new UsageError("--agents needs a value");
|
|
327
|
+
const unknown = parts.filter((part) => !isAgentChoice(part));
|
|
328
|
+
if (unknown.length > 0) {
|
|
329
|
+
throw new UsageError(
|
|
330
|
+
`--agents does not know: ${unknown.join(", ")}. Use auto, none, or any of: ${AGENT_CHOICES.join(", ")}`
|
|
331
|
+
);
|
|
332
|
+
}
|
|
333
|
+
return { kind: "explicit", agents: [...new Set(parts)] };
|
|
334
|
+
};
|
|
335
|
+
var parseSetupOptions = (input) => {
|
|
336
|
+
const flags = parseFlags(input.argv, SETUP_FLAGS);
|
|
337
|
+
const stray = [
|
|
338
|
+
...flags.command !== void 0 && flags.command !== "setup" ? [flags.command] : [],
|
|
339
|
+
...flags.rest
|
|
340
|
+
];
|
|
341
|
+
if (stray.length > 0) {
|
|
342
|
+
throw new UsageError(
|
|
343
|
+
`setup takes no positional arguments (got ${stray.join(", ")}). Did you mean --agents ${stray[0]}?`
|
|
344
|
+
);
|
|
345
|
+
}
|
|
346
|
+
const yes = flags.bools.has("yes") || flags.bools.has("y");
|
|
347
|
+
const help = flags.bools.has("help") || flags.bools.has("h");
|
|
348
|
+
const agentsFlag = flags.values.get("agents");
|
|
349
|
+
const agents = parseAgentSelection(agentsFlag, yes);
|
|
350
|
+
const connectRaw = flags.values.get("connect");
|
|
351
|
+
const connectMode = connectRaw === void 0 ? "app" : parseConnectMode(connectRaw);
|
|
352
|
+
const verifyRaw = flags.values.get("verify");
|
|
353
|
+
const controlRaw = flags.values.get("control");
|
|
354
|
+
const transcriptsRaw = flags.values.get("transcripts")?.trim().toLowerCase();
|
|
355
|
+
if (transcriptsRaw !== void 0 && transcriptsRaw !== "on" && transcriptsRaw !== "off") {
|
|
356
|
+
throw new UsageError(`--transcripts must be one of: on, off (got ${transcriptsRaw})`);
|
|
357
|
+
}
|
|
358
|
+
const nonInteractive = yes || agentsFlag !== void 0 || flags.bools.has("json") || !input.stdinIsTty;
|
|
359
|
+
const verify = verifyRaw !== void 0 ? parseVerifyMode(verifyRaw) : nonInteractive ? "push" : "roundtrip";
|
|
360
|
+
const key = flags.values.get("key")?.trim() || null;
|
|
361
|
+
const keyStdin = flags.bools.has("key-stdin");
|
|
362
|
+
if (key && keyStdin) {
|
|
363
|
+
throw new UsageError("Pass either --key or --key-stdin, not both");
|
|
364
|
+
}
|
|
365
|
+
if (key && !isValidApiKey(key)) {
|
|
366
|
+
throw new UsageError("--key is not a valid API key. Expected the pk_xxx.xxx shape from pushary.com/dashboard/agent/settings");
|
|
367
|
+
}
|
|
368
|
+
return {
|
|
369
|
+
connectMode,
|
|
370
|
+
connectExplicit: connectRaw !== void 0,
|
|
371
|
+
// `--connect none` and `--skip-phone` are the same instruction, so they
|
|
372
|
+
// resolve to the same field rather than being checked separately at the two
|
|
373
|
+
// call sites that would inevitably drift apart.
|
|
374
|
+
skipPhone: flags.bools.has("skip-phone") || connectMode === "none",
|
|
375
|
+
// Anything that means "there is nobody to answer a prompt". A non-TTY stdin
|
|
376
|
+
// is included because that is the case that used to hang: it is better to
|
|
377
|
+
// take the defaults and say so than to wait forever for a keypress.
|
|
378
|
+
//
|
|
379
|
+
// --json counts. Under it stdout carries exactly one object and every human
|
|
380
|
+
// line is redirected to stderr, so a prompt has nowhere to render: the
|
|
381
|
+
// question goes to stderr while the run blocks on a keypress the caller
|
|
382
|
+
// cannot see it is waiting for, and whatever is reading stdout waits forever
|
|
383
|
+
// for an object that never arrives. Asking for machine-readable output is
|
|
384
|
+
// itself a statement that no human is watching.
|
|
385
|
+
nonInteractive,
|
|
386
|
+
json: flags.bools.has("json"),
|
|
387
|
+
dryRun: flags.bools.has("dry-run"),
|
|
388
|
+
help,
|
|
389
|
+
agents,
|
|
390
|
+
key,
|
|
391
|
+
keyStdin,
|
|
392
|
+
verify,
|
|
393
|
+
verifyExplicit: verifyRaw !== void 0,
|
|
394
|
+
control: controlRaw === void 0 ? "auto" : parseControlMode(controlRaw),
|
|
395
|
+
controlExplicit: controlRaw !== void 0,
|
|
396
|
+
transcripts: transcriptsRaw === void 0 ? null : transcriptsRaw === "on",
|
|
397
|
+
takeOverHooks: flags.bools.has("take-over-hooks")
|
|
398
|
+
};
|
|
399
|
+
};
|
|
400
|
+
|
|
401
|
+
// src/setup/hook-ownership.ts
|
|
402
|
+
var commandsIn = (value) => {
|
|
403
|
+
if (Array.isArray(value)) return value.flatMap(commandsIn);
|
|
404
|
+
if (!value || typeof value !== "object") return [];
|
|
405
|
+
return Object.entries(value).flatMap(([key, item]) => key === "command" && typeof item === "string" ? [item] : commandsIn(item));
|
|
406
|
+
};
|
|
407
|
+
var isRecord2 = (value) => value !== null && typeof value === "object" && !Array.isArray(value);
|
|
408
|
+
var hasPaidPusharyCommand = (value) => commandsIn(value).some((command) => {
|
|
409
|
+
const executable = pusharyHookExecutable(command);
|
|
410
|
+
const name = executable?.split(/[/\\]/).pop()?.replace(/\.(cmd|exe|mjs)$/i, "");
|
|
411
|
+
return executable !== null && !isBridgeLocator(executable) && name !== BELL_HOOK_BINARY;
|
|
412
|
+
});
|
|
413
|
+
var hasAppCommand = (value) => commandsIn(value).some((command) => {
|
|
414
|
+
const executable = pusharyHookExecutable(command);
|
|
415
|
+
return executable !== null && isBridgeLocator(executable);
|
|
416
|
+
});
|
|
417
|
+
var mcpBearerKey = (document, containerKey, headersKey) => {
|
|
418
|
+
const servers = isRecord2(document?.[containerKey]) ? document[containerKey] : null;
|
|
419
|
+
const pushary = isRecord2(servers?.pushary) ? servers.pushary : null;
|
|
420
|
+
const headers = isRecord2(pushary?.[headersKey]) ? pushary[headersKey] : null;
|
|
421
|
+
const authorization = headers?.Authorization;
|
|
422
|
+
const match = typeof authorization === "string" ? authorization.trim().match(/^Bearer\s+(\S+)$/) : null;
|
|
423
|
+
return match?.[1] ?? null;
|
|
424
|
+
};
|
|
425
|
+
var captureMcpServer = (document, containerKey) => {
|
|
426
|
+
const container = document[containerKey];
|
|
427
|
+
const servers = isRecord2(container) ? container : null;
|
|
428
|
+
if (!servers && Object.hasOwn(document, containerKey)) {
|
|
429
|
+
return { present: false, containerValue: structuredClone(container) };
|
|
430
|
+
}
|
|
431
|
+
return servers && Object.hasOwn(servers, "pushary") ? { present: true, value: structuredClone(servers.pushary) } : { present: false };
|
|
432
|
+
};
|
|
433
|
+
var restoreMcpServer = (document, containerKey, snapshot) => {
|
|
434
|
+
const before = JSON.stringify(document);
|
|
435
|
+
const existing = isRecord2(document[containerKey]) ? document[containerKey] : null;
|
|
436
|
+
if (snapshot.present) {
|
|
437
|
+
const servers = existing ?? {};
|
|
438
|
+
servers.pushary = structuredClone(snapshot.value);
|
|
439
|
+
document[containerKey] = servers;
|
|
440
|
+
} else if (Object.hasOwn(snapshot, "containerValue")) {
|
|
441
|
+
document[containerKey] = structuredClone(snapshot.containerValue);
|
|
442
|
+
} else if (existing && Object.hasOwn(existing, "pushary")) {
|
|
443
|
+
delete existing.pushary;
|
|
444
|
+
if (Object.keys(existing).length === 0) delete document[containerKey];
|
|
445
|
+
}
|
|
446
|
+
return JSON.stringify(document) !== before;
|
|
447
|
+
};
|
|
448
|
+
var captureHookTakeover = (agent, document) => {
|
|
449
|
+
const hooks = isRecord2(document?.hooks) ? document.hooks : null;
|
|
450
|
+
if (!hooks) return null;
|
|
451
|
+
const captured = {};
|
|
452
|
+
for (const [event, entries] of Object.entries(hooks)) {
|
|
453
|
+
if (!Array.isArray(entries)) continue;
|
|
454
|
+
const appEntries = entries.flatMap((value, index) => isRecord2(value) && hasAppCommand(value) ? [{ index, value: structuredClone(value) }] : []);
|
|
455
|
+
if (appEntries.length > 0) captured[event] = appEntries;
|
|
456
|
+
}
|
|
457
|
+
return Object.keys(captured).length > 0 ? { version: 1, agent, hooks: captured } : null;
|
|
458
|
+
};
|
|
459
|
+
var isHookTakeoverReceipt = (value) => {
|
|
460
|
+
if (!isRecord2(value) || value.version !== 1 || typeof value.agent !== "string" || !AGENT_CHOICES.includes(value.agent) || !isRecord2(value.hooks)) return false;
|
|
461
|
+
const expectsMcp = ["claude_code", "codex", "gemini_cli", "vscode"].includes(value.agent);
|
|
462
|
+
if (value.mcpServer !== void 0 !== expectsMcp) return false;
|
|
463
|
+
if (value.codexTrust !== void 0 !== (value.agent === "codex")) return false;
|
|
464
|
+
if (value.codexTrust !== void 0 && !isRecord2(value.codexTrust)) return false;
|
|
465
|
+
if (value.mcpServer !== void 0 && (!isRecord2(value.mcpServer) || typeof value.mcpServer.present !== "boolean" || value.mcpServer.present && !Object.hasOwn(value.mcpServer, "value"))) return false;
|
|
466
|
+
return Object.values(value.hooks).every((entries) => Array.isArray(entries) && entries.length > 0 && entries.every((item) => isRecord2(item) && Number.isInteger(item.index) && item.index >= 0 && isRecord2(item.value)));
|
|
467
|
+
};
|
|
468
|
+
var restoreHookTakeover = (document, receipt) => {
|
|
469
|
+
const before = JSON.stringify(document);
|
|
470
|
+
const appOwnsNow = hookOwnerOf(document) === "app";
|
|
471
|
+
const hooks = isRecord2(document.hooks) ? document.hooks : {};
|
|
472
|
+
for (const [event, value] of Object.entries(hooks)) {
|
|
473
|
+
if (!Array.isArray(value)) continue;
|
|
474
|
+
const kept = value.filter((entry) => !hasPaidPusharyCommand(entry));
|
|
475
|
+
if (kept.length === 0) delete hooks[event];
|
|
476
|
+
else hooks[event] = kept;
|
|
477
|
+
}
|
|
478
|
+
if (!appOwnsNow) {
|
|
479
|
+
for (const [event, snapshots] of Object.entries(receipt.hooks)) {
|
|
480
|
+
const entries = Array.isArray(hooks[event]) ? [...hooks[event]] : [];
|
|
481
|
+
for (const snapshot of snapshots) {
|
|
482
|
+
entries.splice(Math.min(snapshot.index, entries.length), 0, structuredClone(snapshot.value));
|
|
483
|
+
}
|
|
484
|
+
hooks[event] = entries;
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
document.hooks = hooks;
|
|
488
|
+
return JSON.stringify(document) !== before;
|
|
489
|
+
};
|
|
490
|
+
var hookOwnerOf = (document, appAvailable = true) => {
|
|
491
|
+
const hooks = document && typeof document === "object" ? document.hooks : void 0;
|
|
492
|
+
const executables = commandsIn(hooks).map(pusharyHookExecutable).filter((executable) => executable !== null);
|
|
493
|
+
if (appAvailable && executables.some(isBridgeLocator)) return "app";
|
|
494
|
+
if (executables.some((executable) => !isBridgeLocator(executable))) return "cli";
|
|
495
|
+
return "none";
|
|
496
|
+
};
|
|
497
|
+
var planHooks = (context) => {
|
|
498
|
+
if (context.owner !== "app") return "install";
|
|
499
|
+
if (context.takeOverHooks) return "install";
|
|
500
|
+
return "keep";
|
|
501
|
+
};
|
|
502
|
+
var describeAppOwnership = (agentName) => `The Pushary Mac app owns ${agentName}'s hooks.`;
|
|
503
|
+
var KEEP_HINT = "Kept with the app's account. Pass --take-over-hooks to have the CLI own them instead.";
|
|
504
|
+
var OWNER_LABELS = {
|
|
505
|
+
app: "the Pushary Mac app",
|
|
506
|
+
cli: "the CLI"
|
|
507
|
+
};
|
|
508
|
+
var describeHookOwnership = (entries, names) => {
|
|
509
|
+
const owned = entries.filter((entry) => entry.owner !== "none");
|
|
510
|
+
if (owned.length === 0) return null;
|
|
511
|
+
const parts = owned.map((entry) => `${names[entry.agent]} by ${OWNER_LABELS[entry.owner]}${entry.kept ? " (kept)" : ""}`);
|
|
512
|
+
return `Hooks: ${parts.join(", ")}.`;
|
|
513
|
+
};
|
|
514
|
+
|
|
515
|
+
export {
|
|
516
|
+
isHermesConfigReceipt,
|
|
517
|
+
HERMES_CONFIG_SCRIPT,
|
|
518
|
+
resolveHermesPython,
|
|
519
|
+
runHermesPythonScript,
|
|
520
|
+
createGuards,
|
|
521
|
+
describeSetupSideEffects,
|
|
522
|
+
effectiveVerify,
|
|
523
|
+
effectiveControlMode,
|
|
524
|
+
parseSetupOptions,
|
|
525
|
+
mcpBearerKey,
|
|
526
|
+
captureMcpServer,
|
|
527
|
+
restoreMcpServer,
|
|
528
|
+
captureHookTakeover,
|
|
529
|
+
isHookTakeoverReceipt,
|
|
530
|
+
restoreHookTakeover,
|
|
531
|
+
hookOwnerOf,
|
|
532
|
+
planHooks,
|
|
533
|
+
describeAppOwnership,
|
|
534
|
+
KEEP_HINT,
|
|
535
|
+
describeHookOwnership
|
|
536
|
+
};
|
|
@@ -190,6 +190,25 @@ var codexHookPositions = (hooksConfig) => {
|
|
|
190
190
|
return positions;
|
|
191
191
|
};
|
|
192
192
|
var codexHookStateKey = (hooksJsonPath, event, position) => `${hooksJsonPath}:${CODEX_EVENT_KEY[event]}:${position[0]}:${position[1]}`;
|
|
193
|
+
var captureCodexHookTrust = (config, hooksJsonPath, positions) => {
|
|
194
|
+
const state = asRecord(asRecord(config.hooks)?.state);
|
|
195
|
+
if (!state) return {};
|
|
196
|
+
const snapshot = {};
|
|
197
|
+
for (const [event, position] of Object.entries(positions)) {
|
|
198
|
+
const value = state[codexHookStateKey(hooksJsonPath, event, position)];
|
|
199
|
+
if (value !== void 0) snapshot[event] = structuredClone(value);
|
|
200
|
+
}
|
|
201
|
+
return snapshot;
|
|
202
|
+
};
|
|
203
|
+
var restoreCodexHookTrust = (config, hooksJsonPath, positions, snapshot) => {
|
|
204
|
+
const restorable = Object.entries(snapshot).filter(([event]) => positions[event] !== void 0);
|
|
205
|
+
if (restorable.length === 0) return false;
|
|
206
|
+
const state = ensureRecord(ensureRecord(config, "hooks"), "state");
|
|
207
|
+
for (const [event, value] of restorable) {
|
|
208
|
+
state[codexHookStateKey(hooksJsonPath, event, positions[event])] = structuredClone(value);
|
|
209
|
+
}
|
|
210
|
+
return true;
|
|
211
|
+
};
|
|
193
212
|
var untrustedCodexHookEvents = (config, hooksJsonPath, command, positions) => {
|
|
194
213
|
const state = asRecord(asRecord(config.hooks)?.state);
|
|
195
214
|
return CODEX_HOOK_EVENTS.filter((definition) => {
|
|
@@ -270,6 +289,7 @@ var claudeSettingsLocal = () => join2(homedir2(), ".claude", "settings.local.jso
|
|
|
270
289
|
var pusharyDir = () => join2(homedir2(), ".pushary");
|
|
271
290
|
var claudeMcpReceipt = () => join2(pusharyDir(), "setup", "claude-mcp.json");
|
|
272
291
|
var hermesConfigReceipt = () => join2(pusharyDir(), "setup", "hermes-config.json");
|
|
292
|
+
var hookTakeoverReceipt = (agent) => join2(pusharyDir(), "setup", `hook-takeover-${agent}.json`);
|
|
273
293
|
var codexSkillDir2 = () => join2(codexHome(), "skills", "pushary");
|
|
274
294
|
var codexAgentsMd2 = () => join2(codexHome(), "AGENTS.md");
|
|
275
295
|
var geminiSettings = () => join2(homedir2(), ".gemini", "settings.json");
|
|
@@ -295,6 +315,8 @@ export {
|
|
|
295
315
|
missingCodexHookEvents,
|
|
296
316
|
codexTrustSchemeAgrees,
|
|
297
317
|
codexHookPositions,
|
|
318
|
+
captureCodexHookTrust,
|
|
319
|
+
restoreCodexHookTrust,
|
|
298
320
|
untrustedCodexHookEvents,
|
|
299
321
|
removeCodexHookTrust,
|
|
300
322
|
removeCodexSettings,
|
|
@@ -311,6 +333,7 @@ export {
|
|
|
311
333
|
pusharyDir,
|
|
312
334
|
claudeMcpReceipt,
|
|
313
335
|
hermesConfigReceipt,
|
|
336
|
+
hookTakeoverReceipt,
|
|
314
337
|
codexSkillDir2,
|
|
315
338
|
codexAgentsMd2,
|
|
316
339
|
geminiSettings,
|