@pushary/agent-hooks 0.29.0 → 0.31.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.
@@ -2,21 +2,22 @@
2
2
  import {
3
3
  removeClaudeMcpServers,
4
4
  removePusharySettings
5
- } from "../chunk-QNEYHDKR.js";
5
+ } from "../chunk-H3LQRYMW.js";
6
6
  import {
7
7
  removeCodexHooks,
8
8
  removeGeminiSettings,
9
9
  removeInstructionBlock
10
- } from "../chunk-XY6OKUQ4.js";
10
+ } from "../chunk-WUNVKFFY.js";
11
11
  import {
12
12
  execNpm
13
- } from "../chunk-RSHN2AQ7.js";
13
+ } from "../chunk-J7JWI3KU.js";
14
14
  import "../chunk-Z5PL3K7C.js";
15
15
 
16
16
  // bin/pushary-clean.ts
17
- import { existsSync, readFileSync, writeFileSync, rmSync } from "fs";
17
+ import { existsSync, readFileSync, writeFileSync, rmSync, readdirSync } from "fs";
18
18
  import { join } from "path";
19
- import { homedir } from "os";
19
+ import { homedir, tmpdir } from "os";
20
+ import { execSync } from "child_process";
20
21
  import { confirm } from "@inquirer/prompts";
21
22
  import { parse as parseTOML, stringify as stringifyTOML } from "smol-toml";
22
23
  var dim = (s) => `\x1B[2m${s}\x1B[0m`;
@@ -31,7 +32,25 @@ var CLAUDE_JSON = join(homedir(), ".claude.json");
31
32
  var SKILL_DIR = join(homedir(), ".claude", "skills", "pushary");
32
33
  var CURSOR_MCP = join(".cursor", "mcp.json");
33
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");
34
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
+ };
35
54
  var readJson = (path) => {
36
55
  try {
37
56
  return JSON.parse(readFileSync(path, "utf-8"));
@@ -61,7 +80,12 @@ var main = async () => {
61
80
  console.log(` ${bold("Pushary Clean")}`);
62
81
  console.log(` ${dim("Removes all Pushary configuration")}`);
63
82
  console.log();
64
- const proceed = await confirm({ message: "Remove all Pushary configuration?", default: false });
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 });
65
89
  if (!proceed) {
66
90
  console.log(` ${dim("Cancelled.")}`);
67
91
  process.exit(0);
@@ -99,6 +123,23 @@ var main = async () => {
99
123
  } else {
100
124
  console.log(` ${skip} Cursor plugin ${dim("(not installed)")}`);
101
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
+ }
102
143
  if (existsSync(SKILL_DIR)) {
103
144
  rmSync(SKILL_DIR, { recursive: true });
104
145
  console.log(` ${check} Skill directory ${dim("(removed)")}`);
@@ -170,6 +211,24 @@ var main = async () => {
170
211
  } else {
171
212
  console.log(` ${skip} Gemini GEMINI.md ${dim("(no pushary block)")}`);
172
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
+ }
173
232
  for (const shellFile of SHELL_FILES) {
174
233
  try {
175
234
  const content = readFileSync(shellFile, "utf-8");
@@ -181,6 +240,25 @@ var main = async () => {
181
240
  } catch {
182
241
  }
183
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
+ }
184
262
  try {
185
263
  execNpm("uninstall -g --no-workspaces @pushary/agent-hooks", { stdio: "ignore", timeout: 3e4 });
186
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);
@@ -11,10 +11,13 @@ import {
11
11
  missingGeminiHookEvents,
12
12
  readCodexMcpAuth,
13
13
  untrustedCodexHookEvents
14
- } from "../chunk-XY6OKUQ4.js";
14
+ } from "../chunk-WUNVKFFY.js";
15
15
  import {
16
16
  execNpm
17
- } from "../chunk-RSHN2AQ7.js";
17
+ } from "../chunk-J7JWI3KU.js";
18
+ import {
19
+ readLedgerSummary
20
+ } from "../chunk-R5AJNXZS.js";
18
21
  import {
19
22
  callMcpTool,
20
23
  sendMcpRequest
@@ -317,12 +320,29 @@ var main = async () => {
317
320
  globalVersion = "";
318
321
  }
319
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
+ }
320
336
  console.log();
321
337
  console.log(` ${dim("Connectivity")}`);
322
338
  if (!apiKey) {
323
339
  check(false, "MCP server reachable", "skipped \u2014 no API key");
324
340
  check(false, "API key valid", "skipped");
325
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
+ }
326
346
  } else {
327
347
  let sessionId = "";
328
348
  try {
@@ -379,7 +399,7 @@ var main = async () => {
379
399
  const msg = err instanceof Error ? err.message : "network error";
380
400
  check(false, "Push notification sent", msg);
381
401
  }
382
- 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;
383
403
  if (testQuestion) {
384
404
  console.log();
385
405
  console.log(` ${dim("Question Roundtrip")}`);
@@ -418,7 +438,8 @@ var main = async () => {
418
438
  if (apiKey) {
419
439
  try {
420
440
  const res = await fetch(`${getBaseUrl()}/api/mcp/policy`, {
421
- headers: { Authorization: `Bearer ${apiKey}` }
441
+ headers: { Authorization: `Bearer ${apiKey}` },
442
+ signal: AbortSignal.timeout(1e4)
422
443
  });
423
444
  if (res.ok) {
424
445
  const policy = await res.json();
@@ -453,4 +474,7 @@ var main = async () => {
453
474
  }
454
475
  console.log();
455
476
  };
456
- 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 ?? {});
@@ -1,9 +1,9 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  handlePreToolUse
4
- } from "../chunk-GPLKEAFG.js";
5
- import "../chunk-WLF3NCHS.js";
4
+ } from "../chunk-CY5YP34L.js";
6
5
  import "../chunk-KQYIHZ5E.js";
6
+ import "../chunk-R5AJNXZS.js";
7
7
  import "../chunk-IAOXM7X5.js";
8
8
  import "../chunk-DWED7BS3.js";
9
9
  import "../chunk-Z5PL3K7C.js";
@@ -3,7 +3,7 @@ import {
3
3
  addClaudeMcpServer,
4
4
  addPusharyHooks,
5
5
  addPusharyToolPermissions
6
- } from "../chunk-QNEYHDKR.js";
6
+ } from "../chunk-H3LQRYMW.js";
7
7
  import {
8
8
  GEMINI_HOOK_BINARY,
9
9
  addCodexHookTrust,
@@ -14,11 +14,14 @@ import {
14
14
  renderAgentInstructions,
15
15
  renderProjectAgentInstructions,
16
16
  writeInstructionBlock
17
- } from "../chunk-XY6OKUQ4.js";
17
+ } from "../chunk-WUNVKFFY.js";
18
18
  import {
19
19
  execNpm,
20
- npmErrorMessage
21
- } from "../chunk-RSHN2AQ7.js";
20
+ npmErrorMessage,
21
+ quoteIfNeeded,
22
+ resolveGlobalBinDir,
23
+ resolveGlobalBinary
24
+ } from "../chunk-J7JWI3KU.js";
22
25
  import {
23
26
  reportEvent
24
27
  } from "../chunk-IAOXM7X5.js";
@@ -32,7 +35,7 @@ import "../chunk-NKXSILEW.js";
32
35
  import { existsSync, readFileSync, writeFileSync, appendFileSync, mkdirSync, cpSync, rmSync, chmodSync } from "fs";
33
36
  import { join, dirname, basename } from "path";
34
37
  import { homedir } from "os";
35
- import { execSync } from "child_process";
38
+ import { execSync as execSync2 } from "child_process";
36
39
  import { checkbox, input, confirm } from "@inquirer/prompts";
37
40
  import { fileURLToPath } from "url";
38
41
  import { parse as parseTOML, stringify as stringifyTOML } from "smol-toml";
@@ -306,6 +309,20 @@ var connectViaAppPairing = async () => {
306
309
  }
307
310
  };
308
311
 
312
+ // src/skills-cli.ts
313
+ import { execSync } from "child_process";
314
+ var installSkillViaSkillsCli = (agent) => {
315
+ try {
316
+ execSync(`npx -y skills@latest add Pushary/pushary-skill@pushary -y -g --agent ${agent}`, {
317
+ stdio: "pipe",
318
+ timeout: 9e4
319
+ });
320
+ return true;
321
+ } catch {
322
+ return false;
323
+ }
324
+ };
325
+
309
326
  // bin/pushary-setup.ts
310
327
  var CLAUDE_SETTINGS = join(homedir(), ".claude", "settings.json");
311
328
  var CLAUDE_JSON = join(homedir(), ".claude.json");
@@ -349,7 +366,7 @@ var parseConnectFlag = () => {
349
366
  var isInstalled = (command) => {
350
367
  const whichCmd = process.platform === "win32" ? "where" : "which";
351
368
  try {
352
- execSync(`${whichCmd} ${command}`, { stdio: "ignore", timeout: 5e3 });
369
+ execSync2(`${whichCmd} ${command}`, { stdio: "ignore", timeout: 5e3 });
353
370
  return true;
354
371
  } catch {
355
372
  return false;
@@ -441,11 +458,13 @@ var fetchSkillContent = async () => {
441
458
  _cachedSkillContent = readFileSync(source, "utf-8");
442
459
  return _cachedSkillContent;
443
460
  };
444
- var installSkillToDir = async (dir, label) => {
445
- await spinner(label, async () => {
461
+ var skillsCliInUse = () => isInstalled("skills") || existsSync(join(homedir(), ".agents", "skills"));
462
+ var installSkill = async (agent, fallbackDir) => {
463
+ await spinner("Installing Pushary skill", async () => {
464
+ if (skillsCliInUse() && installSkillViaSkillsCli(agent)) return;
446
465
  const content = await fetchSkillContent();
447
- if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
448
- writeFileSync(join(dir, "SKILL.md"), content, "utf-8");
466
+ if (!existsSync(fallbackDir)) mkdirSync(fallbackDir, { recursive: true });
467
+ writeFileSync(join(fallbackDir, "SKILL.md"), content, "utf-8");
449
468
  });
450
469
  };
451
470
  var setupClaudeCode = async (apiKey) => {
@@ -463,18 +482,12 @@ var setupClaudeCode = async (apiKey) => {
463
482
  });
464
483
  await installGlobally();
465
484
  await spinner("Adding hooks (PreToolUse, PostToolUse, UserPromptSubmit, Stop)", async () => {
466
- let binDir;
467
- try {
468
- binDir = join(execNpm("prefix -g --no-workspaces", { timeout: 5e3 }).toString().trim(), "bin");
469
- } catch {
470
- binDir = void 0;
471
- }
472
- addPusharyHooks(settings, binDir);
485
+ addPusharyHooks(settings, resolveGlobalBinDir("pushary-hook"));
473
486
  });
474
487
  await spinner(`Writing ${CLAUDE_SETTINGS}`, async () => {
475
488
  writeJson(CLAUDE_SETTINGS, settings);
476
489
  });
477
- await installSkillToDir(CLAUDE_SKILL_DIR, "Installing Pushary skill");
490
+ await installSkill("claude-code", CLAUDE_SKILL_DIR);
478
491
  console.log();
479
492
  console.log(` ${dim2("What this configured:")}`);
480
493
  console.log(` ${dim2("\u2022")} MCP server: your agent can send notifications and ask questions`);
@@ -484,7 +497,7 @@ var setupClaudeCode = async (apiKey) => {
484
497
  };
485
498
  var resolveHermesPython = () => {
486
499
  try {
487
- const launcher = execSync("command -v hermes", { encoding: "utf-8", stdio: "pipe", timeout: 5e3 }).trim();
500
+ const launcher = execSync2("command -v hermes", { encoding: "utf-8", stdio: "pipe", timeout: 5e3 }).trim();
488
501
  if (launcher) {
489
502
  const shebang = readFileSync(launcher, "utf-8").split("\n", 1)[0];
490
503
  if (shebang.startsWith("#!")) {
@@ -500,23 +513,23 @@ var resolveHermesPython = () => {
500
513
  };
501
514
  var ensurePip = (python) => {
502
515
  try {
503
- execSync(`"${python}" -m pip --version`, { stdio: "ignore", timeout: 15e3 });
516
+ execSync2(`"${python}" -m pip --version`, { stdio: "ignore", timeout: 15e3 });
504
517
  return;
505
518
  } catch {
506
519
  }
507
520
  try {
508
- execSync(`"${python}" -m ensurepip --upgrade`, { stdio: "pipe", timeout: 6e4 });
521
+ execSync2(`"${python}" -m ensurepip --upgrade`, { stdio: "pipe", timeout: 6e4 });
509
522
  } catch {
510
523
  }
511
524
  };
512
525
  var enablePusharyPlugin = (python) => {
513
526
  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"] = (e + ["pushary"]) if "pushary" not in e else e; 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"] = (d + ["clarify"]) if "clarify" not in d else d; c["agent"] = a; save_config(c)';
514
527
  try {
515
- execSync(`"${python}" -c '${snippet}'`, { stdio: "pipe", timeout: 15e3 });
528
+ execSync2(`"${python}" -c '${snippet}'`, { stdio: "pipe", timeout: 15e3 });
516
529
  return;
517
530
  } catch {
518
531
  }
519
- execSync("hermes plugins enable pushary", { stdio: "ignore", timeout: 1e4 });
532
+ execSync2("hermes plugins enable pushary", { stdio: "ignore", timeout: 1e4 });
520
533
  };
521
534
  var setupHermes = async (_apiKey) => {
522
535
  console.log(`
@@ -535,7 +548,7 @@ var setupHermes = async (_apiKey) => {
535
548
  }
536
549
  await spinner("Installing hermes-plugin-pushary into Hermes\u2019 environment", async () => {
537
550
  ensurePip(python);
538
- execSync(`"${python}" -m pip install --upgrade hermes-plugin-pushary`, { stdio: "pipe", timeout: 18e4 });
551
+ execSync2(`"${python}" -m pip install --upgrade hermes-plugin-pushary`, { stdio: "pipe", timeout: 18e4 });
539
552
  });
540
553
  await spinner("Enabling plugin + routing questions to push", async () => {
541
554
  enablePusharyPlugin(python);
@@ -564,7 +577,7 @@ var compareCodexVersion = (a, b) => {
564
577
  };
565
578
  var readCodexVersion = () => {
566
579
  try {
567
- return parseCodexVersion(execSync("codex --version", { encoding: "utf-8", stdio: "pipe", timeout: 1e4 }));
580
+ return parseCodexVersion(execSync2("codex --version", { encoding: "utf-8", stdio: "pipe", timeout: 1e4 }));
568
581
  } catch {
569
582
  return null;
570
583
  }
@@ -591,9 +604,7 @@ var removeCodexNotifyEntry = (codexConfig) => {
591
604
  writeFileSync(codexConfig, stringifyTOML(config), "utf-8");
592
605
  };
593
606
  var addCodexNotifyEntry = (codexConfig) => {
594
- const globalPrefix = execNpm("prefix -g --no-workspaces", { timeout: 5e3 }).toString().trim();
595
- const pusharyCodexPath = join(globalPrefix, "bin", "pushary-codex");
596
- if (!existsSync(pusharyCodexPath)) throw new Error("pushary-codex not found at " + pusharyCodexPath);
607
+ const pusharyCodexPath = resolveGlobalBinary("pushary-codex") ?? "pushary-codex";
597
608
  let raw = "";
598
609
  try {
599
610
  raw = readFileSync(codexConfig, "utf-8");
@@ -633,10 +644,8 @@ var setupCodex = async (apiKey) => {
633
644
  const trustAuto = codexTrustAutoSupported(codexVersion);
634
645
  let trusted = false;
635
646
  if (hooksSupported) {
636
- const globalPrefix = execNpm("prefix -g --no-workspaces", { timeout: 5e3 }).toString().trim();
637
- const hookCommand = join(globalPrefix, "bin", "pushary-codex-hook");
647
+ const hookCommand = quoteIfNeeded(resolveGlobalBinary("pushary-codex-hook") ?? "pushary-codex-hook");
638
648
  await spinner("Adding native hooks (~/.codex/hooks.json)", async () => {
639
- if (!existsSync(hookCommand)) throw new Error("pushary-codex-hook not found at " + hookCommand);
640
649
  const hooksConfig = readJson(CODEX_HOOKS_JSON);
641
650
  addCodexHooks(hooksConfig, hookCommand);
642
651
  writeJson(CODEX_HOOKS_JSON, hooksConfig);
@@ -665,7 +674,7 @@ var setupCodex = async (apiKey) => {
665
674
  addCodexNotifyEntry(codexConfig);
666
675
  });
667
676
  }
668
- await installSkillToDir(CODEX_SKILL_DIR, "Installing Pushary skill");
677
+ await installSkill("codex", CODEX_SKILL_DIR);
669
678
  await spinner("Teaching Codex to ask via push (~/.codex/AGENTS.md)", async () => {
670
679
  writeInstructionBlock(CODEX_AGENTS_MD, renderAgentInstructions("Codex"));
671
680
  });
@@ -822,13 +831,7 @@ var setupGemini = async (apiKey) => {
822
831
  addGeminiMcpServer(settings, apiKey);
823
832
  });
824
833
  await spinner("Adding hooks (BeforeTool, AfterTool, BeforeAgent, SessionStart, SessionEnd)", async () => {
825
- let hookCommand = GEMINI_HOOK_BINARY;
826
- try {
827
- const candidate = join(execNpm("prefix -g --no-workspaces", { timeout: 5e3 }).toString().trim(), "bin", GEMINI_HOOK_BINARY);
828
- if (existsSync(candidate)) hookCommand = candidate;
829
- } catch {
830
- }
831
- addGeminiHooks(settings, hookCommand);
834
+ addGeminiHooks(settings, quoteIfNeeded(resolveGlobalBinary(GEMINI_HOOK_BINARY) ?? GEMINI_HOOK_BINARY));
832
835
  });
833
836
  await spinner(`Writing ${GEMINI_SETTINGS}`, async () => {
834
837
  writeJson(GEMINI_SETTINGS, settings);
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  readLedgerSummary
4
- } from "../chunk-WLF3NCHS.js";
4
+ } from "../chunk-R5AJNXZS.js";
5
5
  import "../chunk-Z5PL3K7C.js";
6
6
  import {
7
7
  getApiKey
@@ -2,7 +2,7 @@
2
2
  import {
3
3
  execNpm,
4
4
  npmErrorMessage
5
- } from "../chunk-RSHN2AQ7.js";
5
+ } from "../chunk-J7JWI3KU.js";
6
6
 
7
7
  // bin/pushary-upgrade.ts
8
8
  var getInstalledVersion = () => {
@@ -25,7 +25,7 @@ Pushary Agent Hooks
25
25
  Commands:
26
26
  setup Configure Claude Code, Codex, Gemini CLI, Hermes, or Cursor with Pushary
27
27
  doctor Verify your Pushary installation is working
28
- clean Remove all Pushary configuration
28
+ clean Remove all Pushary configuration (--yes for non-interactive)
29
29
  mode Switch approval mode (push_only, push_first, terminal_only)
30
30
  wait Show or set the "wait for your phone" ladder (pushary wait 45)
31
31
  stats Show the approval moments your agents hit while not connected
@@ -1,11 +1,11 @@
1
- import {
2
- isGatingMoment,
3
- recordKeylessMoment
4
- } from "./chunk-WLF3NCHS.js";
5
1
  import {
6
2
  denyReasonFrom,
7
3
  isDeferAnswer
8
4
  } from "./chunk-KQYIHZ5E.js";
5
+ import {
6
+ isGatingMoment,
7
+ recordKeylessMoment
8
+ } from "./chunk-R5AJNXZS.js";
9
9
  import {
10
10
  DEFAULT_SESSION,
11
11
  askUser,
@@ -60,7 +60,10 @@ var addPusharyToolPermissions = (settings) => {
60
60
  permissions.allow = filtered;
61
61
  };
62
62
  var addPusharyHooks = (settings, binDir) => {
63
- const resolve = (name) => binDir ? join(binDir, name) : 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({
@@ -1,5 +1,7 @@
1
1
  // src/npm.ts
2
2
  import { execSync } from "child_process";
3
+ import { existsSync } from "fs";
4
+ import { join } from "path";
3
5
  var cleanNpmEnv = () => {
4
6
  const env = {};
5
7
  for (const [key, value] of Object.entries(process.env)) {
@@ -22,8 +24,26 @@ var execNpm = (args, options = {}) => {
22
24
  env: { ...cleanNpmEnv(), ...options.env ?? {} }
23
25
  });
24
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;
25
42
 
26
43
  export {
27
44
  npmErrorMessage,
28
- execNpm
45
+ execNpm,
46
+ resolveGlobalBinary,
47
+ resolveGlobalBinDir,
48
+ quoteIfNeeded
29
49
  };
@@ -30,26 +30,28 @@ var recordKeylessMoment = (entry) => {
30
30
  var readLedgerSummary = (days = 7) => {
31
31
  const sinceMs = Date.now() - days * 864e5;
32
32
  const summary = { count: 0, sessions: 0, byTool: {}, sinceMs };
33
- let raw;
34
- try {
35
- raw = readFileSync(ledgerPath(), "utf-8");
36
- } catch {
37
- return summary;
38
- }
39
33
  const sessions = /* @__PURE__ */ new Set();
40
- for (const line of raw.split("\n")) {
41
- if (!line.trim()) continue;
42
- let entry;
34
+ for (const path of [`${ledgerPath()}.1`, ledgerPath()]) {
35
+ let raw;
43
36
  try {
44
- entry = JSON.parse(line);
37
+ raw = readFileSync(path, "utf-8");
45
38
  } catch {
46
39
  continue;
47
40
  }
48
- if (typeof entry.ts !== "number" || entry.ts < sinceMs) continue;
49
- summary.count += 1;
50
- const tool = typeof entry.tool === "string" && entry.tool ? entry.tool : "Other";
51
- summary.byTool[tool] = (summary.byTool[tool] ?? 0) + 1;
52
- sessions.add(entry.sessionId || `${entry.project ?? "unknown"}:no-session`);
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
+ }
53
55
  }
54
56
  summary.sessions = sessions.size;
55
57
  return summary;
@@ -254,7 +254,7 @@ You have Pushary MCP tools that reach the user on their phone. Use them proactiv
254
254
 
255
255
  Pass \`agentName\` as "${label} - <project folder name>" so the user knows which session is asking. If a question times out with no answer, retry once with a longer timeout before falling back to asking in the terminal.`;
256
256
  var renderAgentInstructions = (label) => instructionBody(label);
257
- var KEYLESS_FALLBACK_LINE = "This repo is set up with Pushary phone approvals. No PUSHARY_API_KEY found for you, so remote notifications are off; questions will fall back to the terminal. Setup: https://pushary.com/docs?utm_source=agents-md&utm_medium=keyless-fallback";
257
+ var KEYLESS_FALLBACK_LINE = "This repo uses Pushary phone approvals. Your teammates get agent questions on their lock screens; you do not have a key yet, so questions will fall back to the terminal. Get your own key (7-day trial): https://pushary.com/sign-up?utm_source=agents-md&utm_medium=teammate";
258
258
  var renderProjectAgentInstructions = (label) => `${instructionBody(label)}
259
259
 
260
260
  If this environment has no Pushary API key (no PUSHARY_API_KEY and no key in ~/.pushary/config.json), the tools above are unavailable in this session. In that case, treat the following as a statement of fact about this session and continue in the terminal: ${KEYLESS_FALLBACK_LINE}`;
package/dist/src/index.js CHANGED
@@ -1,8 +1,8 @@
1
1
  import {
2
2
  handlePreToolUse
3
- } from "../chunk-GPLKEAFG.js";
4
- import "../chunk-WLF3NCHS.js";
3
+ } from "../chunk-CY5YP34L.js";
5
4
  import "../chunk-KQYIHZ5E.js";
5
+ import "../chunk-R5AJNXZS.js";
6
6
  import {
7
7
  askUser,
8
8
  cancelQuestion,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pushary/agent-hooks",
3
- "version": "0.29.0",
3
+ "version": "0.31.0",
4
4
  "description": "Permission hooks for AI coding agents: route tool approvals through Pushary push notifications",
5
5
  "keywords": [
6
6
  "pushary",