@pushary/agent-hooks 0.28.3 → 0.30.0
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/README.md +5 -3
- package/dist/bin/pushary-clean.js +86 -6
- package/dist/bin/pushary-codex-hook.js +22 -1
- package/dist/bin/pushary-doctor.js +31 -5
- package/dist/bin/pushary-gemini-hook.js +22 -1
- package/dist/bin/pushary-hook.js +3 -2
- package/dist/bin/pushary-setup.js +14 -25
- package/dist/bin/pushary-stats.d.ts +1 -0
- package/dist/bin/pushary-stats.js +38 -0
- package/dist/bin/pushary-upgrade.d.ts +1 -0
- package/dist/bin/pushary-upgrade.js +58 -0
- package/dist/bin/pushary.js +7 -1
- package/dist/{chunk-P6QBZS27.js → chunk-CY5YP34L.js} +40 -2
- package/dist/{chunk-QNEYHDKR.js → chunk-H3LQRYMW.js} +4 -1
- package/dist/chunk-J7JWI3KU.js +49 -0
- package/dist/chunk-R5AJNXZS.js +64 -0
- package/dist/{chunk-X3YCHQQK.js → chunk-XY6OKUQ4.js} +1 -28
- package/dist/src/index.d.ts +1 -1
- package/dist/src/index.js +2 -1
- package/package.json +5 -3
package/README.md
CHANGED
|
@@ -22,7 +22,7 @@ There are three things.
|
|
|
22
22
|
|
|
23
23
|
**Ask.** The agent asks you questions through push: yes/no, multiple choice, or free text. It waits for your answer. When Pushary is connected, the agent sends its questions to your phone instead of stalling in the terminal.
|
|
24
24
|
|
|
25
|
-
**Gate.** Risky shell commands (`rm -rf`, force pushes, history rewrites, database drops, deploys) are checked before they run. What happens is set by your dashboard policy: auto approve trusted read-only commands, push to your phone for approval, or hold for the phone and never auto-deny. If the check cannot run, the
|
|
25
|
+
**Gate.** Risky shell commands (`rm -rf`, force pushes, history rewrites, database drops, deploys) are checked before they run. What happens is set by your dashboard policy: auto approve trusted read-only commands, push to your phone for approval, or hold for the phone and never auto-deny. If the check cannot run, the decision falls back to the agent's own permission prompt in the terminal; nothing is silently allowed. Without an API key configured, the hooks stay out of the way entirely and only count what they would have gated.
|
|
26
26
|
|
|
27
27
|
## Setup
|
|
28
28
|
|
|
@@ -62,8 +62,10 @@ Any other MCP client can use notify and ask by pointing at the Pushary MCP endpo
|
|
|
62
62
|
setup Configure Claude Code, Codex, Gemini CLI, Hermes, or Cursor with Pushary
|
|
63
63
|
doctor Verify your Pushary installation is working
|
|
64
64
|
clean Remove all Pushary configuration
|
|
65
|
-
mode Switch approval mode (push_only, push_first, terminal_only)
|
|
65
|
+
mode Switch approval mode (push_only, push_first, terminal_only, notify_only)
|
|
66
66
|
wait Show or set the "wait for your phone" ladder (pushary wait 45)
|
|
67
|
+
stats Show the approval moments your agents hit while not connected
|
|
68
|
+
upgrade Update the globally installed hooks to the latest version
|
|
67
69
|
hook Run as a PreToolUse hook (reads stdin, writes stdout)
|
|
68
70
|
```
|
|
69
71
|
|
|
@@ -97,7 +99,7 @@ The MCP server exposes `send_notification`, `ask_user`, `wait_for_answer`, and `
|
|
|
97
99
|
|
|
98
100
|
## Security
|
|
99
101
|
|
|
100
|
-
|
|
102
|
+
If a policy cannot be fetched or a hook errors, the decision falls back to the agent's native permission prompt, never a silent allow. The Cursor gate is the strictest surface: it blocks matched risky commands outright when its check cannot run. Secrets in command arguments and environment are redacted before anything is sent to a phone or stored. Read-only commands can be allowlisted so trusted work never interrupts you, while writes and destructive commands still route through approval.
|
|
101
103
|
|
|
102
104
|
## Links
|
|
103
105
|
|
|
@@ -2,19 +2,22 @@
|
|
|
2
2
|
import {
|
|
3
3
|
removeClaudeMcpServers,
|
|
4
4
|
removePusharySettings
|
|
5
|
-
} from "../chunk-
|
|
5
|
+
} from "../chunk-H3LQRYMW.js";
|
|
6
6
|
import {
|
|
7
|
-
execNpm,
|
|
8
7
|
removeCodexHooks,
|
|
9
8
|
removeGeminiSettings,
|
|
10
9
|
removeInstructionBlock
|
|
11
|
-
} from "../chunk-
|
|
10
|
+
} from "../chunk-XY6OKUQ4.js";
|
|
11
|
+
import {
|
|
12
|
+
execNpm
|
|
13
|
+
} from "../chunk-J7JWI3KU.js";
|
|
12
14
|
import "../chunk-Z5PL3K7C.js";
|
|
13
15
|
|
|
14
16
|
// bin/pushary-clean.ts
|
|
15
|
-
import { existsSync, readFileSync, writeFileSync, rmSync } from "fs";
|
|
17
|
+
import { existsSync, readFileSync, writeFileSync, rmSync, readdirSync } from "fs";
|
|
16
18
|
import { join } from "path";
|
|
17
|
-
import { homedir } from "os";
|
|
19
|
+
import { homedir, tmpdir } from "os";
|
|
20
|
+
import { execSync } from "child_process";
|
|
18
21
|
import { confirm } from "@inquirer/prompts";
|
|
19
22
|
import { parse as parseTOML, stringify as stringifyTOML } from "smol-toml";
|
|
20
23
|
var dim = (s) => `\x1B[2m${s}\x1B[0m`;
|
|
@@ -29,7 +32,25 @@ var CLAUDE_JSON = join(homedir(), ".claude.json");
|
|
|
29
32
|
var SKILL_DIR = join(homedir(), ".claude", "skills", "pushary");
|
|
30
33
|
var CURSOR_MCP = join(".cursor", "mcp.json");
|
|
31
34
|
var CURSOR_PLUGIN_DIR = join(homedir(), ".cursor", "plugins", "local", "pushary");
|
|
35
|
+
var CURSOR_USER_HOOKS = join(homedir(), ".cursor", "hooks.json");
|
|
36
|
+
var PUSHARY_DIR = join(homedir(), ".pushary");
|
|
32
37
|
var SHELL_FILES = [".zshrc", ".zprofile", ".bashrc", ".bash_profile"].map((f) => join(homedir(), f));
|
|
38
|
+
var resolveHermesPython = () => {
|
|
39
|
+
try {
|
|
40
|
+
const launcher = execSync("command -v hermes", { encoding: "utf-8", stdio: "pipe", timeout: 5e3 }).trim();
|
|
41
|
+
if (launcher) {
|
|
42
|
+
const shebang = readFileSync(launcher, "utf-8").split("\n", 1)[0];
|
|
43
|
+
if (shebang.startsWith("#!")) {
|
|
44
|
+
const interpreter = shebang.slice(2).trim().split(/\s+/)[0];
|
|
45
|
+
if (interpreter && /python/i.test(interpreter) && existsSync(interpreter)) return interpreter;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
} catch {
|
|
49
|
+
}
|
|
50
|
+
const venvPython = join(homedir(), ".hermes", "hermes-agent", "venv", "bin", "python3");
|
|
51
|
+
if (existsSync(venvPython)) return venvPython;
|
|
52
|
+
return null;
|
|
53
|
+
};
|
|
33
54
|
var readJson = (path) => {
|
|
34
55
|
try {
|
|
35
56
|
return JSON.parse(readFileSync(path, "utf-8"));
|
|
@@ -59,7 +80,12 @@ var main = async () => {
|
|
|
59
80
|
console.log(` ${bold("Pushary Clean")}`);
|
|
60
81
|
console.log(` ${dim("Removes all Pushary configuration")}`);
|
|
61
82
|
console.log();
|
|
62
|
-
const
|
|
83
|
+
const assumeYes = process.argv.includes("--yes") || process.argv.includes("-y");
|
|
84
|
+
if (!assumeYes && !process.stdin.isTTY) {
|
|
85
|
+
console.log(` ${yellow("!")} Non-interactive shell. Re-run with --yes to confirm removal.`);
|
|
86
|
+
process.exit(1);
|
|
87
|
+
}
|
|
88
|
+
const proceed = assumeYes || await confirm({ message: "Remove all Pushary configuration?", default: false });
|
|
63
89
|
if (!proceed) {
|
|
64
90
|
console.log(` ${dim("Cancelled.")}`);
|
|
65
91
|
process.exit(0);
|
|
@@ -97,6 +123,23 @@ var main = async () => {
|
|
|
97
123
|
} else {
|
|
98
124
|
console.log(` ${skip} Cursor plugin ${dim("(not installed)")}`);
|
|
99
125
|
}
|
|
126
|
+
const cursorHooks = readJson(CURSOR_USER_HOOKS);
|
|
127
|
+
if (cursorHooks) {
|
|
128
|
+
const hooks = cursorHooks.hooks ?? {};
|
|
129
|
+
const existing = Array.isArray(hooks.beforeShellExecution) ? hooks.beforeShellExecution : [];
|
|
130
|
+
const others = existing.filter((h) => !String(h.command ?? "").includes("pushary-gate"));
|
|
131
|
+
if (others.length !== existing.length) {
|
|
132
|
+
if (others.length === 0) delete hooks.beforeShellExecution;
|
|
133
|
+
else hooks.beforeShellExecution = others;
|
|
134
|
+
cursorHooks.hooks = hooks;
|
|
135
|
+
writeJson(CURSOR_USER_HOOKS, cursorHooks);
|
|
136
|
+
console.log(` ${check} Cursor gate ${dim("(removed from ~/.cursor/hooks.json)")}`);
|
|
137
|
+
} else {
|
|
138
|
+
console.log(` ${skip} Cursor gate ${dim("(no pushary entries)")}`);
|
|
139
|
+
}
|
|
140
|
+
} else {
|
|
141
|
+
console.log(` ${skip} Cursor gate ${dim("(no hooks.json)")}`);
|
|
142
|
+
}
|
|
100
143
|
if (existsSync(SKILL_DIR)) {
|
|
101
144
|
rmSync(SKILL_DIR, { recursive: true });
|
|
102
145
|
console.log(` ${check} Skill directory ${dim("(removed)")}`);
|
|
@@ -168,6 +211,24 @@ var main = async () => {
|
|
|
168
211
|
} else {
|
|
169
212
|
console.log(` ${skip} Gemini GEMINI.md ${dim("(no pushary block)")}`);
|
|
170
213
|
}
|
|
214
|
+
const hermesPython = resolveHermesPython();
|
|
215
|
+
if (hermesPython) {
|
|
216
|
+
const snippet = 'from hermes_cli.config import load_config, save_config; c = load_config(); p = c.get("plugins") if isinstance(c.get("plugins"), dict) else {}; e = p.get("enabled") if isinstance(p.get("enabled"), list) else []; p["enabled"] = [x for x in e if x != "pushary"]; c["plugins"] = p; a = c.get("agent") if isinstance(c.get("agent"), dict) else {}; d = a.get("disabled_toolsets") if isinstance(a.get("disabled_toolsets"), list) else []; a["disabled_toolsets"] = [x for x in d if x != "clarify"]; c["agent"] = a; save_config(c)';
|
|
217
|
+
try {
|
|
218
|
+
execSync(`"${hermesPython}" -c '${snippet}'`, { stdio: "pipe", timeout: 15e3 });
|
|
219
|
+
console.log(` ${check} Hermes config ${dim("(plugin disabled, clarify toolset restored)")}`);
|
|
220
|
+
} catch {
|
|
221
|
+
console.log(` ${skip} Hermes config ${dim("(could not update config.yaml)")}`);
|
|
222
|
+
}
|
|
223
|
+
try {
|
|
224
|
+
execSync(`"${hermesPython}" -m pip uninstall -y hermes-plugin-pushary`, { stdio: "pipe", timeout: 6e4 });
|
|
225
|
+
console.log(` ${check} Hermes plugin ${dim("(pip uninstalled)")}`);
|
|
226
|
+
} catch {
|
|
227
|
+
console.log(` ${skip} Hermes plugin ${dim("(not installed)")}`);
|
|
228
|
+
}
|
|
229
|
+
} else {
|
|
230
|
+
console.log(` ${skip} Hermes ${dim("(not found)")}`);
|
|
231
|
+
}
|
|
171
232
|
for (const shellFile of SHELL_FILES) {
|
|
172
233
|
try {
|
|
173
234
|
const content = readFileSync(shellFile, "utf-8");
|
|
@@ -179,6 +240,25 @@ var main = async () => {
|
|
|
179
240
|
} catch {
|
|
180
241
|
}
|
|
181
242
|
}
|
|
243
|
+
if (existsSync(PUSHARY_DIR)) {
|
|
244
|
+
rmSync(PUSHARY_DIR, { recursive: true });
|
|
245
|
+
console.log(` ${check} Local state ${dim("(~/.pushary removed: stored API key, ledger)")}`);
|
|
246
|
+
} else {
|
|
247
|
+
console.log(` ${skip} Local state ${dim("(~/.pushary not found)")}`);
|
|
248
|
+
}
|
|
249
|
+
try {
|
|
250
|
+
let swept = 0;
|
|
251
|
+
for (const f of readdirSync(tmpdir())) {
|
|
252
|
+
if (!f.startsWith("pushary-")) continue;
|
|
253
|
+
try {
|
|
254
|
+
rmSync(join(tmpdir(), f), { recursive: true, force: true });
|
|
255
|
+
swept++;
|
|
256
|
+
} catch {
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
if (swept > 0) console.log(` ${check} Temp state ${dim(`(${swept} pushary entries swept)`)}`);
|
|
260
|
+
} catch {
|
|
261
|
+
}
|
|
182
262
|
try {
|
|
183
263
|
execNpm("uninstall -g --no-workspaces @pushary/agent-hooks", { stdio: "ignore", timeout: 3e4 });
|
|
184
264
|
console.log(` ${check} Global package ${dim("(uninstalled)")}`);
|
|
@@ -3,6 +3,10 @@ import {
|
|
|
3
3
|
denyReasonFrom,
|
|
4
4
|
isDeferAnswer
|
|
5
5
|
} from "../chunk-KQYIHZ5E.js";
|
|
6
|
+
import {
|
|
7
|
+
isGatingMoment,
|
|
8
|
+
recordKeylessMoment
|
|
9
|
+
} from "../chunk-R5AJNXZS.js";
|
|
6
10
|
import {
|
|
7
11
|
CODEX_AGENT,
|
|
8
12
|
DEFAULT_SESSION,
|
|
@@ -201,8 +205,25 @@ var decidePermissionRequest = async (input) => {
|
|
|
201
205
|
}
|
|
202
206
|
};
|
|
203
207
|
var decidePreToolUse = async (input) => {
|
|
208
|
+
let apiKey;
|
|
209
|
+
try {
|
|
210
|
+
apiKey = getApiKey();
|
|
211
|
+
} catch {
|
|
212
|
+
try {
|
|
213
|
+
const lookup = toPolicyLookup(input.tool_name ?? "", input.tool_input ?? {}, input.cwd);
|
|
214
|
+
if (isGatingMoment(lookup.tool, lookup.input)) {
|
|
215
|
+
recordKeylessMoment({
|
|
216
|
+
ts: Date.now(),
|
|
217
|
+
tool: lookup.tool,
|
|
218
|
+
project: basename(input.cwd ?? process.cwd()),
|
|
219
|
+
sessionId: input.session_id
|
|
220
|
+
});
|
|
221
|
+
}
|
|
222
|
+
} catch {
|
|
223
|
+
}
|
|
224
|
+
return codexPass();
|
|
225
|
+
}
|
|
204
226
|
try {
|
|
205
|
-
const apiKey = getApiKey();
|
|
206
227
|
const modeState = await fetchModeState(apiKey, input.session_id);
|
|
207
228
|
if (modeState.kill) return codexDeny(KILL_REASON);
|
|
208
229
|
const lookup = toPolicyLookup(input.tool_name ?? "", input.tool_input ?? {}, input.cwd);
|
|
@@ -4,7 +4,6 @@ import {
|
|
|
4
4
|
} from "../chunk-IIENZE2J.js";
|
|
5
5
|
import {
|
|
6
6
|
GEMINI_HOOK_BINARY,
|
|
7
|
-
execNpm,
|
|
8
7
|
hasCodexHooks,
|
|
9
8
|
hasGeminiHooks,
|
|
10
9
|
hasInstructionBlock,
|
|
@@ -12,7 +11,13 @@ import {
|
|
|
12
11
|
missingGeminiHookEvents,
|
|
13
12
|
readCodexMcpAuth,
|
|
14
13
|
untrustedCodexHookEvents
|
|
15
|
-
} from "../chunk-
|
|
14
|
+
} from "../chunk-XY6OKUQ4.js";
|
|
15
|
+
import {
|
|
16
|
+
execNpm
|
|
17
|
+
} from "../chunk-J7JWI3KU.js";
|
|
18
|
+
import {
|
|
19
|
+
readLedgerSummary
|
|
20
|
+
} from "../chunk-R5AJNXZS.js";
|
|
16
21
|
import {
|
|
17
22
|
callMcpTool,
|
|
18
23
|
sendMcpRequest
|
|
@@ -315,12 +320,29 @@ var main = async () => {
|
|
|
315
320
|
globalVersion = "";
|
|
316
321
|
}
|
|
317
322
|
check(!!globalVersion, "Global package installed", globalVersion || "not found");
|
|
323
|
+
if (globalVersion) {
|
|
324
|
+
try {
|
|
325
|
+
const res = await fetch("https://registry.npmjs.org/@pushary/agent-hooks/latest", {
|
|
326
|
+
signal: AbortSignal.timeout(5e3)
|
|
327
|
+
});
|
|
328
|
+
const latest = res.ok ? (await res.json()).version ?? "" : "";
|
|
329
|
+
if (latest) {
|
|
330
|
+
const upToDate = latest === globalVersion;
|
|
331
|
+
check(upToDate, "Global package up to date", upToDate ? globalVersion : `${globalVersion} installed, ${latest} available \u2014 run npx @pushary/agent-hooks@latest upgrade`);
|
|
332
|
+
}
|
|
333
|
+
} catch {
|
|
334
|
+
}
|
|
335
|
+
}
|
|
318
336
|
console.log();
|
|
319
337
|
console.log(` ${dim("Connectivity")}`);
|
|
320
338
|
if (!apiKey) {
|
|
321
339
|
check(false, "MCP server reachable", "skipped \u2014 no API key");
|
|
322
340
|
check(false, "API key valid", "skipped");
|
|
323
341
|
check(false, "MCP handshake", "skipped");
|
|
342
|
+
const keylessSummary = readLedgerSummary(7);
|
|
343
|
+
if (keylessSummary.count > 0) {
|
|
344
|
+
check(false, "Keyless approval moments (7d)", `${keylessSummary.count} counted locally \u2014 npx @pushary/agent-hooks@latest stats`);
|
|
345
|
+
}
|
|
324
346
|
} else {
|
|
325
347
|
let sessionId = "";
|
|
326
348
|
try {
|
|
@@ -377,7 +399,7 @@ var main = async () => {
|
|
|
377
399
|
const msg = err instanceof Error ? err.message : "network error";
|
|
378
400
|
check(false, "Push notification sent", msg);
|
|
379
401
|
}
|
|
380
|
-
const testQuestion = await confirm({ message: "Test question roundtrip? (sends a push notification)", default: false });
|
|
402
|
+
const testQuestion = process.stdin.isTTY ? await confirm({ message: "Test question roundtrip? (sends a push notification)", default: false }) : false;
|
|
381
403
|
if (testQuestion) {
|
|
382
404
|
console.log();
|
|
383
405
|
console.log(` ${dim("Question Roundtrip")}`);
|
|
@@ -416,7 +438,8 @@ var main = async () => {
|
|
|
416
438
|
if (apiKey) {
|
|
417
439
|
try {
|
|
418
440
|
const res = await fetch(`${getBaseUrl()}/api/mcp/policy`, {
|
|
419
|
-
headers: { Authorization: `Bearer ${apiKey}` }
|
|
441
|
+
headers: { Authorization: `Bearer ${apiKey}` },
|
|
442
|
+
signal: AbortSignal.timeout(1e4)
|
|
420
443
|
});
|
|
421
444
|
if (res.ok) {
|
|
422
445
|
const policy = await res.json();
|
|
@@ -451,4 +474,7 @@ var main = async () => {
|
|
|
451
474
|
}
|
|
452
475
|
console.log();
|
|
453
476
|
};
|
|
454
|
-
main()
|
|
477
|
+
main().catch((err) => {
|
|
478
|
+
console.error(` doctor failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
479
|
+
process.exit(1);
|
|
480
|
+
});
|
|
@@ -3,6 +3,10 @@ import {
|
|
|
3
3
|
denyReasonFrom,
|
|
4
4
|
isDeferAnswer
|
|
5
5
|
} from "../chunk-KQYIHZ5E.js";
|
|
6
|
+
import {
|
|
7
|
+
isGatingMoment,
|
|
8
|
+
recordKeylessMoment
|
|
9
|
+
} from "../chunk-R5AJNXZS.js";
|
|
6
10
|
import {
|
|
7
11
|
DEFAULT_SESSION,
|
|
8
12
|
askUser,
|
|
@@ -181,8 +185,25 @@ var handlePushFirst = async (apiKey, input, lookup, pushFirstSeconds) => {
|
|
|
181
185
|
return geminiPass();
|
|
182
186
|
};
|
|
183
187
|
var decideBeforeTool = async (input) => {
|
|
188
|
+
let apiKey;
|
|
189
|
+
try {
|
|
190
|
+
apiKey = getApiKey();
|
|
191
|
+
} catch {
|
|
192
|
+
try {
|
|
193
|
+
const lookup = toGeminiPolicyLookup(input.tool_name ?? "", input.tool_input ?? {});
|
|
194
|
+
if (isGatingMoment(lookup.tool, lookup.input)) {
|
|
195
|
+
recordKeylessMoment({
|
|
196
|
+
ts: Date.now(),
|
|
197
|
+
tool: lookup.tool,
|
|
198
|
+
project: basename(input.cwd ?? process.cwd()),
|
|
199
|
+
sessionId: input.session_id
|
|
200
|
+
});
|
|
201
|
+
}
|
|
202
|
+
} catch {
|
|
203
|
+
}
|
|
204
|
+
return geminiPass();
|
|
205
|
+
}
|
|
184
206
|
try {
|
|
185
|
-
const apiKey = getApiKey();
|
|
186
207
|
const modeState = await fetchModeState(apiKey, input.session_id);
|
|
187
208
|
if (modeState.kill) return geminiDeny(KILL_REASON);
|
|
188
209
|
const lookup = toGeminiPolicyLookup(input.tool_name ?? "", input.tool_input ?? {});
|
package/dist/bin/pushary-hook.js
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import {
|
|
3
3
|
handlePreToolUse
|
|
4
|
-
} from "../chunk-
|
|
4
|
+
} from "../chunk-CY5YP34L.js";
|
|
5
5
|
import "../chunk-KQYIHZ5E.js";
|
|
6
|
+
import "../chunk-R5AJNXZS.js";
|
|
6
7
|
import "../chunk-IAOXM7X5.js";
|
|
7
8
|
import "../chunk-DWED7BS3.js";
|
|
8
9
|
import "../chunk-Z5PL3K7C.js";
|
|
@@ -28,7 +29,7 @@ var main = async () => {
|
|
|
28
29
|
}
|
|
29
30
|
try {
|
|
30
31
|
const output = await handlePreToolUse(input);
|
|
31
|
-
process.stdout.write(JSON.stringify(output));
|
|
32
|
+
if (output) process.stdout.write(JSON.stringify(output));
|
|
32
33
|
} catch (err) {
|
|
33
34
|
process.stderr.write(
|
|
34
35
|
`[pushary-hook] Error: ${err instanceof Error ? err.message : String(err)}
|
|
@@ -3,7 +3,7 @@ import {
|
|
|
3
3
|
addClaudeMcpServer,
|
|
4
4
|
addPusharyHooks,
|
|
5
5
|
addPusharyToolPermissions
|
|
6
|
-
} from "../chunk-
|
|
6
|
+
} from "../chunk-H3LQRYMW.js";
|
|
7
7
|
import {
|
|
8
8
|
GEMINI_HOOK_BINARY,
|
|
9
9
|
addCodexHookTrust,
|
|
@@ -11,12 +11,17 @@ import {
|
|
|
11
11
|
addCodexMcpServer,
|
|
12
12
|
addGeminiHooks,
|
|
13
13
|
addGeminiMcpServer,
|
|
14
|
-
execNpm,
|
|
15
|
-
npmErrorMessage,
|
|
16
14
|
renderAgentInstructions,
|
|
17
15
|
renderProjectAgentInstructions,
|
|
18
16
|
writeInstructionBlock
|
|
19
|
-
} from "../chunk-
|
|
17
|
+
} from "../chunk-XY6OKUQ4.js";
|
|
18
|
+
import {
|
|
19
|
+
execNpm,
|
|
20
|
+
npmErrorMessage,
|
|
21
|
+
quoteIfNeeded,
|
|
22
|
+
resolveGlobalBinDir,
|
|
23
|
+
resolveGlobalBinary
|
|
24
|
+
} from "../chunk-J7JWI3KU.js";
|
|
20
25
|
import {
|
|
21
26
|
reportEvent
|
|
22
27
|
} from "../chunk-IAOXM7X5.js";
|
|
@@ -411,7 +416,7 @@ var checkForUpdates = async (current) => {
|
|
|
411
416
|
const latest = data.version;
|
|
412
417
|
if (latest && latest !== current) {
|
|
413
418
|
console.log(` ${yellow2("!")} Update available: ${dim2(current)} \u2192 ${green2(latest)}`);
|
|
414
|
-
console.log(` ${dim2("Run:")} npx @pushary/agent-hooks
|
|
419
|
+
console.log(` ${dim2("Run:")} npx @pushary/agent-hooks@latest upgrade`);
|
|
415
420
|
console.log();
|
|
416
421
|
}
|
|
417
422
|
} catch {
|
|
@@ -461,13 +466,7 @@ var setupClaudeCode = async (apiKey) => {
|
|
|
461
466
|
});
|
|
462
467
|
await installGlobally();
|
|
463
468
|
await spinner("Adding hooks (PreToolUse, PostToolUse, UserPromptSubmit, Stop)", async () => {
|
|
464
|
-
|
|
465
|
-
try {
|
|
466
|
-
binDir = join(execNpm("prefix -g --no-workspaces", { timeout: 5e3 }).toString().trim(), "bin");
|
|
467
|
-
} catch {
|
|
468
|
-
binDir = void 0;
|
|
469
|
-
}
|
|
470
|
-
addPusharyHooks(settings, binDir);
|
|
469
|
+
addPusharyHooks(settings, resolveGlobalBinDir("pushary-hook"));
|
|
471
470
|
});
|
|
472
471
|
await spinner(`Writing ${CLAUDE_SETTINGS}`, async () => {
|
|
473
472
|
writeJson(CLAUDE_SETTINGS, settings);
|
|
@@ -589,9 +588,7 @@ var removeCodexNotifyEntry = (codexConfig) => {
|
|
|
589
588
|
writeFileSync(codexConfig, stringifyTOML(config), "utf-8");
|
|
590
589
|
};
|
|
591
590
|
var addCodexNotifyEntry = (codexConfig) => {
|
|
592
|
-
const
|
|
593
|
-
const pusharyCodexPath = join(globalPrefix, "bin", "pushary-codex");
|
|
594
|
-
if (!existsSync(pusharyCodexPath)) throw new Error("pushary-codex not found at " + pusharyCodexPath);
|
|
591
|
+
const pusharyCodexPath = resolveGlobalBinary("pushary-codex") ?? "pushary-codex";
|
|
595
592
|
let raw = "";
|
|
596
593
|
try {
|
|
597
594
|
raw = readFileSync(codexConfig, "utf-8");
|
|
@@ -631,10 +628,8 @@ var setupCodex = async (apiKey) => {
|
|
|
631
628
|
const trustAuto = codexTrustAutoSupported(codexVersion);
|
|
632
629
|
let trusted = false;
|
|
633
630
|
if (hooksSupported) {
|
|
634
|
-
const
|
|
635
|
-
const hookCommand = join(globalPrefix, "bin", "pushary-codex-hook");
|
|
631
|
+
const hookCommand = quoteIfNeeded(resolveGlobalBinary("pushary-codex-hook") ?? "pushary-codex-hook");
|
|
636
632
|
await spinner("Adding native hooks (~/.codex/hooks.json)", async () => {
|
|
637
|
-
if (!existsSync(hookCommand)) throw new Error("pushary-codex-hook not found at " + hookCommand);
|
|
638
633
|
const hooksConfig = readJson(CODEX_HOOKS_JSON);
|
|
639
634
|
addCodexHooks(hooksConfig, hookCommand);
|
|
640
635
|
writeJson(CODEX_HOOKS_JSON, hooksConfig);
|
|
@@ -820,13 +815,7 @@ var setupGemini = async (apiKey) => {
|
|
|
820
815
|
addGeminiMcpServer(settings, apiKey);
|
|
821
816
|
});
|
|
822
817
|
await spinner("Adding hooks (BeforeTool, AfterTool, BeforeAgent, SessionStart, SessionEnd)", async () => {
|
|
823
|
-
|
|
824
|
-
try {
|
|
825
|
-
const candidate = join(execNpm("prefix -g --no-workspaces", { timeout: 5e3 }).toString().trim(), "bin", GEMINI_HOOK_BINARY);
|
|
826
|
-
if (existsSync(candidate)) hookCommand = candidate;
|
|
827
|
-
} catch {
|
|
828
|
-
}
|
|
829
|
-
addGeminiHooks(settings, hookCommand);
|
|
818
|
+
addGeminiHooks(settings, quoteIfNeeded(resolveGlobalBinary(GEMINI_HOOK_BINARY) ?? GEMINI_HOOK_BINARY));
|
|
830
819
|
});
|
|
831
820
|
await spinner(`Writing ${GEMINI_SETTINGS}`, async () => {
|
|
832
821
|
writeJson(GEMINI_SETTINGS, settings);
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import {
|
|
3
|
+
readLedgerSummary
|
|
4
|
+
} from "../chunk-R5AJNXZS.js";
|
|
5
|
+
import "../chunk-Z5PL3K7C.js";
|
|
6
|
+
import {
|
|
7
|
+
getApiKey
|
|
8
|
+
} from "../chunk-NKXSILEW.js";
|
|
9
|
+
|
|
10
|
+
// bin/pushary-stats.ts
|
|
11
|
+
var DAYS = 7;
|
|
12
|
+
var summary = readLedgerSummary(DAYS);
|
|
13
|
+
var connected = true;
|
|
14
|
+
try {
|
|
15
|
+
getApiKey();
|
|
16
|
+
} catch {
|
|
17
|
+
connected = false;
|
|
18
|
+
}
|
|
19
|
+
var plural = (count, word) => count === 1 ? word : `${word}s`;
|
|
20
|
+
if (summary.count === 0) {
|
|
21
|
+
if (connected) {
|
|
22
|
+
console.log("No locally counted approval moments in the last 7 days. Connected sessions land in your dashboard: https://pushary.com/dashboard/agent/activity");
|
|
23
|
+
} else {
|
|
24
|
+
console.log("No approval moments counted yet. Run your agent with the Pushary hooks installed and check back after a session or two.");
|
|
25
|
+
}
|
|
26
|
+
process.exit(0);
|
|
27
|
+
}
|
|
28
|
+
var toolLines = Object.entries(summary.byTool).sort((a, b) => b[1] - a[1]).map(([tool, count]) => ` ${tool}: ${count}`).join("\n");
|
|
29
|
+
console.log(`In the last ${DAYS} days, your agents hit ${summary.count} approval ${plural(summary.count, "moment")} across ${summary.sessions} ${plural(summary.sessions, "session")} while Pushary was not connected:
|
|
30
|
+
`);
|
|
31
|
+
console.log(toolLines);
|
|
32
|
+
console.log();
|
|
33
|
+
if (connected) {
|
|
34
|
+
console.log("A key is configured now, so new moments reach your phone instead of waiting in the terminal.");
|
|
35
|
+
} else {
|
|
36
|
+
console.log("Each of those sat in the terminal until you noticed. With Pushary they land on your lock screen as a yes or no.");
|
|
37
|
+
console.log("7-day trial: https://pushary.com/sign-up?utm_source=cli&utm_medium=stats");
|
|
38
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import {
|
|
3
|
+
execNpm,
|
|
4
|
+
npmErrorMessage
|
|
5
|
+
} from "../chunk-J7JWI3KU.js";
|
|
6
|
+
|
|
7
|
+
// bin/pushary-upgrade.ts
|
|
8
|
+
var getInstalledVersion = () => {
|
|
9
|
+
try {
|
|
10
|
+
return execNpm("list -g --no-workspaces @pushary/agent-hooks --depth=0", { timeout: 1e4 }).toString().match(/@pushary\/agent-hooks@([\d.]+)/)?.[1] ?? "";
|
|
11
|
+
} catch {
|
|
12
|
+
return "";
|
|
13
|
+
}
|
|
14
|
+
};
|
|
15
|
+
var fetchLatestVersion = async () => {
|
|
16
|
+
try {
|
|
17
|
+
const res = await fetch("https://registry.npmjs.org/@pushary/agent-hooks/latest", {
|
|
18
|
+
signal: AbortSignal.timeout(5e3)
|
|
19
|
+
});
|
|
20
|
+
if (!res.ok) return void 0;
|
|
21
|
+
const data = await res.json();
|
|
22
|
+
return data.version;
|
|
23
|
+
} catch {
|
|
24
|
+
return void 0;
|
|
25
|
+
}
|
|
26
|
+
};
|
|
27
|
+
var isNewer = (a, b) => {
|
|
28
|
+
const pa = a.split(".").map((n) => parseInt(n, 10) || 0);
|
|
29
|
+
const pb = b.split(".").map((n) => parseInt(n, 10) || 0);
|
|
30
|
+
for (let i = 0; i < 3; i++) {
|
|
31
|
+
if ((pa[i] ?? 0) > (pb[i] ?? 0)) return true;
|
|
32
|
+
if ((pa[i] ?? 0) < (pb[i] ?? 0)) return false;
|
|
33
|
+
}
|
|
34
|
+
return false;
|
|
35
|
+
};
|
|
36
|
+
var main = async () => {
|
|
37
|
+
const installed = getInstalledVersion();
|
|
38
|
+
if (!installed) {
|
|
39
|
+
console.log("No global install of @pushary/agent-hooks found, so there is nothing to upgrade.");
|
|
40
|
+
console.log("Plugin installs update on their own. To set up the CLI hooks: npx @pushary/agent-hooks@latest setup");
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
const latest = await fetchLatestVersion();
|
|
44
|
+
if (latest && !isNewer(latest, installed)) {
|
|
45
|
+
console.log(`Already on the latest version (${installed}).`);
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
console.log(`Upgrading @pushary/agent-hooks ${installed} -> ${latest ?? "latest"}`);
|
|
49
|
+
try {
|
|
50
|
+
execNpm("install -g --no-workspaces @pushary/agent-hooks@latest", { timeout: 12e4 });
|
|
51
|
+
} catch (err) {
|
|
52
|
+
console.error(`Upgrade failed: ${npmErrorMessage(err)}`);
|
|
53
|
+
process.exit(1);
|
|
54
|
+
}
|
|
55
|
+
console.log("Done. Your hooks point at the global install, so agents pick this up on their next tool call.");
|
|
56
|
+
console.log("Verify with: pushary doctor");
|
|
57
|
+
};
|
|
58
|
+
main();
|
package/dist/bin/pushary.js
CHANGED
|
@@ -14,6 +14,10 @@ if (command === "setup") {
|
|
|
14
14
|
await import("./pushary-mode.js");
|
|
15
15
|
} else if (command === "wait") {
|
|
16
16
|
await import("./pushary-wait.js");
|
|
17
|
+
} else if (command === "stats") {
|
|
18
|
+
await import("./pushary-stats.js");
|
|
19
|
+
} else if (command === "upgrade") {
|
|
20
|
+
await import("./pushary-upgrade.js");
|
|
17
21
|
} else {
|
|
18
22
|
console.log(`
|
|
19
23
|
Pushary Agent Hooks
|
|
@@ -21,9 +25,11 @@ Pushary Agent Hooks
|
|
|
21
25
|
Commands:
|
|
22
26
|
setup Configure Claude Code, Codex, Gemini CLI, Hermes, or Cursor with Pushary
|
|
23
27
|
doctor Verify your Pushary installation is working
|
|
24
|
-
clean Remove all Pushary configuration
|
|
28
|
+
clean Remove all Pushary configuration (--yes for non-interactive)
|
|
25
29
|
mode Switch approval mode (push_only, push_first, terminal_only)
|
|
26
30
|
wait Show or set the "wait for your phone" ladder (pushary wait 45)
|
|
31
|
+
stats Show the approval moments your agents hit while not connected
|
|
32
|
+
upgrade Update the globally installed hooks to the latest version
|
|
27
33
|
hook Run as a PreToolUse hook (reads stdin, writes stdout)
|
|
28
34
|
|
|
29
35
|
Setup options:
|
|
@@ -2,6 +2,10 @@ import {
|
|
|
2
2
|
denyReasonFrom,
|
|
3
3
|
isDeferAnswer
|
|
4
4
|
} from "./chunk-KQYIHZ5E.js";
|
|
5
|
+
import {
|
|
6
|
+
isGatingMoment,
|
|
7
|
+
recordKeylessMoment
|
|
8
|
+
} from "./chunk-R5AJNXZS.js";
|
|
5
9
|
import {
|
|
6
10
|
DEFAULT_SESSION,
|
|
7
11
|
askUser,
|
|
@@ -31,7 +35,9 @@ import {
|
|
|
31
35
|
} from "./chunk-NKXSILEW.js";
|
|
32
36
|
|
|
33
37
|
// src/hook.ts
|
|
34
|
-
import { basename } from "path";
|
|
38
|
+
import { basename, join } from "path";
|
|
39
|
+
import { tmpdir } from "os";
|
|
40
|
+
import { existsSync, writeFileSync } from "fs";
|
|
35
41
|
var sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
36
42
|
var START_MS = Date.now();
|
|
37
43
|
var allow = () => ({
|
|
@@ -182,9 +188,41 @@ var handleNotifyOnly = async (apiKey, description, projectName, sessionId, machi
|
|
|
182
188
|
}
|
|
183
189
|
return ask();
|
|
184
190
|
};
|
|
191
|
+
var KEYLESS_NOTICE = "[pushary] Not connected (no API key), so your agent runs untouched. Approval moments are counted locally; see them with: npx @pushary/agent-hooks@latest stats. Connect: https://pushary.com/sign-up?utm_source=cli&utm_medium=keyless-hook";
|
|
192
|
+
var keylessNoticeOnce = (sessionId) => {
|
|
193
|
+
try {
|
|
194
|
+
const key = (sessionId || DEFAULT_SESSION).replace(/[^a-zA-Z0-9_-]/g, "");
|
|
195
|
+
const marker = join(tmpdir(), `pushary-keyless-${key}`);
|
|
196
|
+
if (existsSync(marker)) return;
|
|
197
|
+
writeFileSync(marker, "1", "utf-8");
|
|
198
|
+
process.stderr.write(`${KEYLESS_NOTICE}
|
|
199
|
+
`);
|
|
200
|
+
} catch {
|
|
201
|
+
}
|
|
202
|
+
};
|
|
203
|
+
var handleKeyless = (input) => {
|
|
204
|
+
try {
|
|
205
|
+
if (isGatingMoment(input.tool_name, input.tool_input ?? {})) {
|
|
206
|
+
recordKeylessMoment({
|
|
207
|
+
ts: Date.now(),
|
|
208
|
+
tool: input.tool_name,
|
|
209
|
+
project: basename(input.cwd ?? process.cwd()),
|
|
210
|
+
sessionId: input.session_id
|
|
211
|
+
});
|
|
212
|
+
}
|
|
213
|
+
keylessNoticeOnce(input.session_id);
|
|
214
|
+
} catch {
|
|
215
|
+
}
|
|
216
|
+
return void 0;
|
|
217
|
+
};
|
|
185
218
|
var handlePreToolUse = async (input) => {
|
|
219
|
+
let apiKey;
|
|
220
|
+
try {
|
|
221
|
+
apiKey = getApiKey();
|
|
222
|
+
} catch {
|
|
223
|
+
return handleKeyless(input);
|
|
224
|
+
}
|
|
186
225
|
try {
|
|
187
|
-
const apiKey = getApiKey();
|
|
188
226
|
const modeState = await fetchModeState(apiKey, input.session_id);
|
|
189
227
|
const policy = await getPolicy(apiKey, modeState.policyVersion);
|
|
190
228
|
if (modeState.kill) {
|
|
@@ -60,7 +60,10 @@ var addPusharyToolPermissions = (settings) => {
|
|
|
60
60
|
permissions.allow = filtered;
|
|
61
61
|
};
|
|
62
62
|
var addPusharyHooks = (settings, binDir) => {
|
|
63
|
-
const resolve = (name) =>
|
|
63
|
+
const resolve = (name) => {
|
|
64
|
+
const path = binDir ? join(binDir, name) : name;
|
|
65
|
+
return /\s/.test(path) ? `"${path}"` : path;
|
|
66
|
+
};
|
|
64
67
|
const hooks = ensureRecord(settings, "hooks");
|
|
65
68
|
const preToolUse = (Array.isArray(hooks.PreToolUse) ? hooks.PreToolUse : []).filter((entry) => !isPusharyHook(entry));
|
|
66
69
|
preToolUse.push({
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
// src/npm.ts
|
|
2
|
+
import { execSync } from "child_process";
|
|
3
|
+
import { existsSync } from "fs";
|
|
4
|
+
import { join } from "path";
|
|
5
|
+
var cleanNpmEnv = () => {
|
|
6
|
+
const env = {};
|
|
7
|
+
for (const [key, value] of Object.entries(process.env)) {
|
|
8
|
+
if (key.toLowerCase().startsWith("npm_config_workspace")) continue;
|
|
9
|
+
env[key] = value;
|
|
10
|
+
}
|
|
11
|
+
return env;
|
|
12
|
+
};
|
|
13
|
+
var npmErrorMessage = (err) => {
|
|
14
|
+
const e = err;
|
|
15
|
+
const text = [e?.stderr, e?.stdout].map((part) => part ? part.toString() : "").join("\n");
|
|
16
|
+
const line = text.split("\n").map((l) => l.replace(/^npm error\s*/i, "").trim()).find((l) => l && !l.startsWith("A complete log") && !/^code\s/i.test(l));
|
|
17
|
+
return line || e?.message || String(err);
|
|
18
|
+
};
|
|
19
|
+
var execNpm = (args, options = {}) => {
|
|
20
|
+
return execSync(`npm ${args}`, {
|
|
21
|
+
timeout: 12e4,
|
|
22
|
+
stdio: "pipe",
|
|
23
|
+
...options,
|
|
24
|
+
env: { ...cleanNpmEnv(), ...options.env ?? {} }
|
|
25
|
+
});
|
|
26
|
+
};
|
|
27
|
+
var resolveGlobalBinary = (name) => {
|
|
28
|
+
try {
|
|
29
|
+
const prefix = execNpm("prefix -g --no-workspaces", { timeout: 5e3 }).toString().trim();
|
|
30
|
+
const candidates = process.platform === "win32" ? [join(prefix, `${name}.cmd`), join(prefix, name)] : [join(prefix, "bin", name)];
|
|
31
|
+
return candidates.find(existsSync);
|
|
32
|
+
} catch {
|
|
33
|
+
return void 0;
|
|
34
|
+
}
|
|
35
|
+
};
|
|
36
|
+
var resolveGlobalBinDir = (probe) => {
|
|
37
|
+
if (process.platform === "win32") return void 0;
|
|
38
|
+
const resolved = resolveGlobalBinary(probe);
|
|
39
|
+
return resolved ? resolved.slice(0, resolved.length - probe.length - 1) : void 0;
|
|
40
|
+
};
|
|
41
|
+
var quoteIfNeeded = (path) => /\s/.test(path) ? `"${path}"` : path;
|
|
42
|
+
|
|
43
|
+
export {
|
|
44
|
+
npmErrorMessage,
|
|
45
|
+
execNpm,
|
|
46
|
+
resolveGlobalBinary,
|
|
47
|
+
resolveGlobalBinDir,
|
|
48
|
+
quoteIfNeeded
|
|
49
|
+
};
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import {
|
|
2
|
+
isSafeReadOnlyCommand
|
|
3
|
+
} from "./chunk-Z5PL3K7C.js";
|
|
4
|
+
|
|
5
|
+
// src/ledger.ts
|
|
6
|
+
import { appendFileSync, existsSync, mkdirSync, readFileSync, renameSync, rmSync, statSync } from "fs";
|
|
7
|
+
import { join, dirname } from "path";
|
|
8
|
+
import { homedir } from "os";
|
|
9
|
+
var ledgerPath = () => process.env.PUSHARY_LEDGER_FILE?.trim() || join(homedir(), ".pushary", "local-ledger.jsonl");
|
|
10
|
+
var MAX_LEDGER_BYTES = 1e6;
|
|
11
|
+
var isGatingMoment = (toolName, toolInput) => {
|
|
12
|
+
const command = typeof toolInput?.command === "string" ? toolInput.command : void 0;
|
|
13
|
+
if (toolName === "Bash" && command && isSafeReadOnlyCommand(command)) return false;
|
|
14
|
+
return true;
|
|
15
|
+
};
|
|
16
|
+
var recordKeylessMoment = (entry) => {
|
|
17
|
+
try {
|
|
18
|
+
const path = ledgerPath();
|
|
19
|
+
const dir = dirname(path);
|
|
20
|
+
if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
|
|
21
|
+
if (existsSync(path) && statSync(path).size > MAX_LEDGER_BYTES) {
|
|
22
|
+
rmSync(`${path}.1`, { force: true });
|
|
23
|
+
renameSync(path, `${path}.1`);
|
|
24
|
+
}
|
|
25
|
+
appendFileSync(path, `${JSON.stringify(entry)}
|
|
26
|
+
`, "utf-8");
|
|
27
|
+
} catch {
|
|
28
|
+
}
|
|
29
|
+
};
|
|
30
|
+
var readLedgerSummary = (days = 7) => {
|
|
31
|
+
const sinceMs = Date.now() - days * 864e5;
|
|
32
|
+
const summary = { count: 0, sessions: 0, byTool: {}, sinceMs };
|
|
33
|
+
const sessions = /* @__PURE__ */ new Set();
|
|
34
|
+
for (const path of [`${ledgerPath()}.1`, ledgerPath()]) {
|
|
35
|
+
let raw;
|
|
36
|
+
try {
|
|
37
|
+
raw = readFileSync(path, "utf-8");
|
|
38
|
+
} catch {
|
|
39
|
+
continue;
|
|
40
|
+
}
|
|
41
|
+
for (const line of raw.split("\n")) {
|
|
42
|
+
if (!line.trim()) continue;
|
|
43
|
+
let entry;
|
|
44
|
+
try {
|
|
45
|
+
entry = JSON.parse(line);
|
|
46
|
+
} catch {
|
|
47
|
+
continue;
|
|
48
|
+
}
|
|
49
|
+
if (typeof entry.ts !== "number" || entry.ts < sinceMs) continue;
|
|
50
|
+
summary.count += 1;
|
|
51
|
+
const tool = typeof entry.tool === "string" && entry.tool ? entry.tool : "Other";
|
|
52
|
+
summary.byTool[tool] = (summary.byTool[tool] ?? 0) + 1;
|
|
53
|
+
sessions.add(entry.sessionId || `${entry.project ?? "unknown"}:no-session`);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
summary.sessions = sessions.size;
|
|
57
|
+
return summary;
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
export {
|
|
61
|
+
isGatingMoment,
|
|
62
|
+
recordKeylessMoment,
|
|
63
|
+
readLedgerSummary
|
|
64
|
+
};
|
|
@@ -307,31 +307,6 @@ var hasInstructionBlock = (filePath) => {
|
|
|
307
307
|
}
|
|
308
308
|
};
|
|
309
309
|
|
|
310
|
-
// src/npm.ts
|
|
311
|
-
import { execSync } from "child_process";
|
|
312
|
-
var cleanNpmEnv = () => {
|
|
313
|
-
const env = {};
|
|
314
|
-
for (const [key, value] of Object.entries(process.env)) {
|
|
315
|
-
if (key.toLowerCase().startsWith("npm_config_workspace")) continue;
|
|
316
|
-
env[key] = value;
|
|
317
|
-
}
|
|
318
|
-
return env;
|
|
319
|
-
};
|
|
320
|
-
var npmErrorMessage = (err) => {
|
|
321
|
-
const e = err;
|
|
322
|
-
const text = [e?.stderr, e?.stdout].map((part) => part ? part.toString() : "").join("\n");
|
|
323
|
-
const line = text.split("\n").map((l) => l.replace(/^npm error\s*/i, "").trim()).find((l) => l && !l.startsWith("A complete log") && !/^code\s/i.test(l));
|
|
324
|
-
return line || e?.message || String(err);
|
|
325
|
-
};
|
|
326
|
-
var execNpm = (args, options = {}) => {
|
|
327
|
-
return execSync(`npm ${args}`, {
|
|
328
|
-
timeout: 12e4,
|
|
329
|
-
stdio: "pipe",
|
|
330
|
-
...options,
|
|
331
|
-
env: { ...cleanNpmEnv(), ...options.env ?? {} }
|
|
332
|
-
});
|
|
333
|
-
};
|
|
334
|
-
|
|
335
310
|
export {
|
|
336
311
|
addCodexMcpServer,
|
|
337
312
|
readCodexMcpAuth,
|
|
@@ -351,7 +326,5 @@ export {
|
|
|
351
326
|
renderProjectAgentInstructions,
|
|
352
327
|
writeInstructionBlock,
|
|
353
328
|
removeInstructionBlock,
|
|
354
|
-
hasInstructionBlock
|
|
355
|
-
npmErrorMessage,
|
|
356
|
-
execNpm
|
|
329
|
+
hasInstructionBlock
|
|
357
330
|
};
|
package/dist/src/index.d.ts
CHANGED
|
@@ -15,7 +15,7 @@ interface HookOutput {
|
|
|
15
15
|
permissionDecisionReason?: string;
|
|
16
16
|
};
|
|
17
17
|
}
|
|
18
|
-
declare const handlePreToolUse: (input: ToolInput) => Promise<HookOutput>;
|
|
18
|
+
declare const handlePreToolUse: (input: ToolInput) => Promise<HookOutput | undefined>;
|
|
19
19
|
|
|
20
20
|
type ReceiptKind = 'edit' | 'write' | 'bash' | 'commit';
|
|
21
21
|
interface ReceiptMeta {
|
package/dist/src/index.js
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pushary/agent-hooks",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.30.0",
|
|
4
4
|
"description": "Permission hooks for AI coding agents: route tool approvals through Pushary push notifications",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"pushary",
|
|
@@ -54,7 +54,9 @@
|
|
|
54
54
|
"pushary-clean": "./dist/bin/pushary-clean.js",
|
|
55
55
|
"pushary-doctor": "./dist/bin/pushary-doctor.js",
|
|
56
56
|
"pushary-mode": "./dist/bin/pushary-mode.js",
|
|
57
|
-
"pushary-wait": "./dist/bin/pushary-wait.js"
|
|
57
|
+
"pushary-wait": "./dist/bin/pushary-wait.js",
|
|
58
|
+
"pushary-stats": "./dist/bin/pushary-stats.js",
|
|
59
|
+
"pushary-upgrade": "./dist/bin/pushary-upgrade.js"
|
|
58
60
|
},
|
|
59
61
|
"files": [
|
|
60
62
|
"dist",
|
|
@@ -63,7 +65,7 @@
|
|
|
63
65
|
"scripts": {
|
|
64
66
|
"build": "node scripts/bundle-plugin.mjs && tsup",
|
|
65
67
|
"dev": "tsup --watch",
|
|
66
|
-
"test": "bun test src/api.test.ts && bun test src/claude-config.test.ts && bun test src/config.test.ts && bun test src/mcp-http.test.ts && bun test src/retry.test.ts && bun test src/usage.test.ts && bun test src/validate.test.ts && bun test src/policy.test.ts && bun test src/npm.test.ts && bun test src/identity.test.ts && bun test src/pending.test.ts && bun test src/events.test.ts && bun test src/describe.test.ts && bun test src/suggestions.test.ts && bun test src/safe-commands.test.ts && bun test src/hook.test.ts && bun test src/codex-adapter.test.ts && bun test src/codex-config.test.ts && bun test src/gemini-adapter.test.ts && bun test src/gemini-config.test.ts && bun test src/instruction-file.test.ts && bun test src/pairing.test.ts && bun test src/wait-ladder.test.ts"
|
|
68
|
+
"test": "bun test src/api.test.ts && bun test src/claude-config.test.ts && bun test src/config.test.ts && bun test src/mcp-http.test.ts && bun test src/retry.test.ts && bun test src/usage.test.ts && bun test src/validate.test.ts && bun test src/policy.test.ts && bun test src/npm.test.ts && bun test src/identity.test.ts && bun test src/pending.test.ts && bun test src/events.test.ts && bun test src/describe.test.ts && bun test src/suggestions.test.ts && bun test src/safe-commands.test.ts && bun test src/hook.test.ts && bun test src/codex-adapter.test.ts && bun test src/codex-config.test.ts && bun test src/gemini-adapter.test.ts && bun test src/gemini-config.test.ts && bun test src/instruction-file.test.ts && bun test src/pairing.test.ts && bun test src/wait-ladder.test.ts && bun test src/ledger.test.ts"
|
|
67
69
|
},
|
|
68
70
|
"dependencies": {
|
|
69
71
|
"@inquirer/prompts": "^8.4.2",
|