@pushary/agent-hooks 0.68.0 → 0.69.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.
@@ -3,11 +3,14 @@ import {
3
3
  describeWaitLadder
4
4
  } from "../chunk-IIENZE2J.js";
5
5
  import {
6
+ agentProbes,
7
+ detectAgent,
6
8
  detectAllAgents,
7
9
  hasInstructionBlock,
10
+ isDetected,
8
11
  isPluginRegistered,
9
12
  vscodeSettingsTargets
10
- } from "../chunk-KB4ODLGB.js";
13
+ } from "../chunk-I4BYZIX4.js";
11
14
  import {
12
15
  GEMINI_HOOK_BINARY,
13
16
  hasGeminiHooks,
@@ -27,6 +30,7 @@ import {
27
30
  geminiSettings,
28
31
  hasCodexHooks,
29
32
  missingCodexHookEvents,
33
+ pusharyConfigFile,
30
34
  readCodexMcpAuth,
31
35
  untrustedCodexHookEvents,
32
36
  vscodePluginDir
@@ -35,7 +39,7 @@ import {
35
39
  describeReach,
36
40
  fetchChannels,
37
41
  reachVerdict
38
- } from "../chunk-YMFZUXL5.js";
42
+ } from "../chunk-P7VBAYQO.js";
39
43
  import "../chunk-3EGEA4KH.js";
40
44
  import {
41
45
  keyPrefix,
@@ -135,6 +139,42 @@ var redactText = (text) => text.replace(KEY_PATTERN, (match) => `${match.split("
135
139
  var redact = (bundle) => JSON.parse(redactText(JSON.stringify(bundle)));
136
140
  var bundleFileName = (bundle) => `pushary-diagnostics-${bundle.generatedAt.replace(/[:.]/g, "-")}.json`;
137
141
 
142
+ // src/diagnostics/exit-code.ts
143
+ var resolveDoctorExit = ({ keyExit, failures }) => {
144
+ if (keyExit !== null) return keyExit;
145
+ if (failures.length === 0) return EXIT.OK;
146
+ const every = (kind) => failures.every((f) => f.kind === kind);
147
+ if (every("unreachable")) return EXIT.UNREACHABLE;
148
+ if (every("reachability")) return EXIT.NO_DEVICE;
149
+ return EXIT.PROBLEMS_FOUND;
150
+ };
151
+
152
+ // src/diagnostics/wiring.ts
153
+ var record = (value) => value && typeof value === "object" && !Array.isArray(value) ? value : null;
154
+ var CLAUDE_HOOK_BINARY = /pushary-[a-z-]*hook/;
155
+ var claudeWired = ({ claudeJson: claudeJson2, settings, skillExists }) => {
156
+ if (record(record(claudeJson2)?.mcpServers)?.pushary) return true;
157
+ const s = record(settings);
158
+ if (s) {
159
+ if (CLAUDE_HOOK_BINARY.test(JSON.stringify(s.hooks ?? {}))) return true;
160
+ const allow = record(s.permissions)?.allow;
161
+ if (Array.isArray(allow) && allow.some((rule) => String(rule).includes("mcp__pushary__"))) return true;
162
+ }
163
+ return skillExists;
164
+ };
165
+ var codexWired = ({ config, hooks, skillExists }) => {
166
+ if (record(record(config)?.mcp_servers)?.pushary) return true;
167
+ const h = record(hooks);
168
+ if (h && hasCodexHooks(h)) return true;
169
+ return skillExists;
170
+ };
171
+ var geminiWired = ({ settings }) => {
172
+ const s = record(settings);
173
+ if (!s) return false;
174
+ if (record(s.mcpServers)?.pushary) return true;
175
+ return hasGeminiHooks(s);
176
+ };
177
+
138
178
  // bin/pushary-doctor.ts
139
179
  exitOnHelpFlag("doctor");
140
180
  var parseDoctorOptions = (argv) => {
@@ -215,12 +255,21 @@ var extractHookTimeout = (entries, needle) => {
215
255
  return null;
216
256
  };
217
257
  var results = [];
218
- var check = (passed, label, detail) => {
219
- results.push({ passed, label, detail });
258
+ var check = (passed, label, detail, kind = "config") => {
259
+ results.push({ passed, label, detail, kind });
220
260
  const icon = passed ? pass : fail;
221
261
  const suffix = detail ? ` ${dim(`(${detail})`)}` : "";
222
262
  console.log(` ${icon} ${label}${suffix}`);
223
263
  };
264
+ var probes = agentProbes();
265
+ var installed = (agent) => isDetected(detectAgent(probes[agent]));
266
+ var agentsChecked = [];
267
+ var skipped = (label, why) => {
268
+ console.log(` ${dim("\u2013")} ${label} ${dim(`(${why})`)}`);
269
+ };
270
+ var checking = (label) => {
271
+ agentsChecked.push(label);
272
+ };
224
273
  var main = async () => {
225
274
  console.log();
226
275
  console.log(` ${bold("Pushary Doctor")}`);
@@ -247,8 +296,12 @@ var main = async () => {
247
296
  if (keySource === "config-file") {
248
297
  console.log(` ${pass} API key in ${dim(source.origin ?? "the config file")} ${dim(`(${maskKey(apiKey)})`)}`);
249
298
  } else if (keySource === "profile") {
250
- console.log(` ${pass} API key in shell profile ${dim(`(${maskKey(apiKey)})`)}`);
251
- console.log(` ${warn} Re-run ${cyan("npx @pushary/agent-hooks setup")} so hooks read it from ${dim("~/.pushary/config.json")} without a shell reload`);
299
+ check(
300
+ false,
301
+ "API key readable by hooks",
302
+ `found only in a shell profile (${maskKey(apiKey)}), which hooks do not read \u2014 re-run setup, or open a new terminal`
303
+ );
304
+ console.log(` ${dim("Hooks read $PUSHARY_API_KEY, then")} ${dim(pusharyConfigFile())}`);
252
305
  } else {
253
306
  check(!!apiKey, "API key in environment", apiKey ? maskKey(apiKey) : "PUSHARY_API_KEY not set");
254
307
  }
@@ -282,19 +335,34 @@ var main = async () => {
282
335
  check(false, "Key format", "expected pk_xxx.xxx");
283
336
  break;
284
337
  case "unreachable":
285
- console.log(` ${warn} Could not verify the key with pushary.com ${dim(`(${keyVerdict.detail})`)}`);
338
+ check(false, "Key verified with pushary.com", `could not reach pushary.com (${keyVerdict.detail})`, "unreachable");
286
339
  break;
287
340
  }
288
341
  }
289
342
  const CLAUDE_JSON = claudeJson();
290
- const claudeJson2 = readJson(CLAUDE_JSON);
343
+ const claudeInPlay = claudeWired({
344
+ claudeJson: readJson(CLAUDE_JSON),
345
+ settings: readJson(CLAUDE_SETTINGS),
346
+ skillExists: existsSync(SKILL_PATH)
347
+ });
348
+ const claudeJson2 = claudeInPlay ? readJson(CLAUDE_JSON) : null;
291
349
  const mcpServers = claudeJson2?.mcpServers ?? {};
292
350
  const pusharyServer = mcpServers?.pushary;
293
- check(!!pusharyServer, "Claude Code: MCP server configured");
294
- if (pusharyServer) {
295
- check(pusharyServer.type === "http", "Claude Code: MCP server type", pusharyServer.type ? String(pusharyServer.type) : 'missing \u2014 add type: "http"');
351
+ if (!claudeInPlay) {
352
+ skipped("Claude Code", installed("claude_code") ? "installed, but Pushary is not set up for it" : "not installed");
353
+ } else {
354
+ checking("Claude Code");
355
+ check(!!pusharyServer, "Claude Code: MCP server configured");
356
+ if (pusharyServer) {
357
+ check(pusharyServer.type === "http", "Claude Code: MCP server type", pusharyServer.type ? String(pusharyServer.type) : 'missing \u2014 add type: "http"');
358
+ const claudeAuth = pusharyServer.headers?.Authorization;
359
+ if (typeof claudeAuth === "string" && apiKey) {
360
+ const matches = claudeAuth === `Bearer ${apiKey}`;
361
+ check(matches, "Claude Code: MCP key matches active key", matches ? void 0 : "the key in ~/.claude.json is not the key hooks use (rotated?) \u2014 re-run setup");
362
+ }
363
+ }
296
364
  }
297
- const settings = readJson(CLAUDE_SETTINGS);
365
+ const settings = claudeInPlay ? readJson(CLAUDE_SETTINGS) : null;
298
366
  if (settings) {
299
367
  const hooks = settings.hooks;
300
368
  const CLAUDE_HOOK_CHECKS = [
@@ -325,19 +393,24 @@ var main = async () => {
325
393
  if (hasLegacyPerms) {
326
394
  console.log(` ${warn} Legacy individual permissions detected ${dim("(run pushary clean, then setup again)")}`);
327
395
  }
328
- } else {
396
+ } else if (claudeInPlay) {
329
397
  check(false, "Claude Code: settings.json", "not found");
330
398
  }
331
399
  const codexConfigPath = codexConfigToml();
332
400
  const codexHooksPath = codexHooksJson();
333
401
  const codexHooksConfig = readJson(codexHooksPath);
334
- if (existsSync(codexConfigPath) || codexHooksConfig) {
335
- const codexConfig = existsSync(codexConfigPath) ? readFileSync(codexConfigPath, "utf-8") : "";
336
- let codexParsed = {};
337
- try {
338
- codexParsed = parseTOML(codexConfig);
339
- } catch {
340
- }
402
+ const codexConfig = existsSync(codexConfigPath) ? readFileSync(codexConfigPath, "utf-8") : "";
403
+ let codexParsed = {};
404
+ try {
405
+ codexParsed = parseTOML(codexConfig);
406
+ } catch {
407
+ }
408
+ if (codexWired({
409
+ config: codexParsed,
410
+ hooks: codexHooksConfig,
411
+ skillExists: existsSync(join(codexSkillDir(), "SKILL.md"))
412
+ })) {
413
+ checking("Codex");
341
414
  const codexMcp = codexParsed.mcp_servers?.pushary;
342
415
  const codexTransport = !!codexMcp && (typeof codexMcp.url === "string" || typeof codexMcp.command === "string");
343
416
  const codexAuth = readCodexMcpAuth(codexParsed);
@@ -401,10 +474,13 @@ var main = async () => {
401
474
  const codexAgentsMd2 = codexAgentsMd();
402
475
  const hasCodexInstructions = hasInstructionBlock(codexAgentsMd2);
403
476
  check(hasCodexInstructions, "Codex: proactive instructions (AGENTS.md)", hasCodexInstructions ? codexAgentsMd2 : "missing \u2014 re-run setup so Codex asks via push, not the terminal");
477
+ } else {
478
+ skipped("Codex", installed("codex") ? "installed, but Pushary is not set up for it" : "not installed");
404
479
  }
405
480
  const geminiSettingsPath = geminiSettings();
406
481
  const geminiSettings2 = readJson(geminiSettingsPath);
407
- if (geminiSettings2) {
482
+ if (geminiSettings2 && geminiWired({ settings: geminiSettings2 })) {
483
+ checking("Gemini CLI");
408
484
  const geminiServers = geminiSettings2.mcpServers ?? {};
409
485
  const geminiPushary = geminiServers.pushary;
410
486
  check(!!geminiPushary, "Gemini CLI: MCP server configured");
@@ -443,12 +519,15 @@ var main = async () => {
443
519
  const geminiMd2 = geminiMd();
444
520
  const hasGeminiInstructions = hasInstructionBlock(geminiMd2);
445
521
  check(hasGeminiInstructions, "Gemini CLI: proactive instructions (GEMINI.md)", hasGeminiInstructions ? geminiMd2 : "missing \u2014 re-run setup so Gemini asks via push, not the terminal");
522
+ } else {
523
+ skipped("Gemini CLI", installed("gemini_cli") ? "installed, but Pushary is not set up for it" : "not installed");
446
524
  }
447
525
  const cursorPluginDir2 = cursorPluginDir();
448
526
  if (existsSync(cursorPluginDir2)) {
527
+ checking("Cursor");
449
528
  const cursorUserHooks2 = cursorUserHooks();
450
529
  const userHooks = readJson(cursorUserHooks2);
451
- const gateEntry = userHooks.hooks?.beforeShellExecution?.find(
530
+ const gateEntry = userHooks?.hooks?.beforeShellExecution?.find(
452
531
  (h) => String(h.command ?? "").includes("pushary-gate")
453
532
  );
454
533
  if (!gateEntry) {
@@ -465,12 +544,17 @@ var main = async () => {
465
544
  check(false, "Cursor: API key linked", "no Authorization in plugin mcp.json \u2014 re-run setup");
466
545
  } else if (cursorAuth.includes("PUSHARY_API_KEY")) {
467
546
  check(false, "Cursor: API key linked", "plugin relies on $PUSHARY_API_KEY; re-run setup to embed the key so the gate works when Cursor is launched from the GUI");
547
+ } else if (apiKey && cursorAuth !== `Bearer ${apiKey}`) {
548
+ check(false, "Cursor: API key matches active key", "the embedded key is not the key hooks use (rotated?) \u2014 re-run setup");
468
549
  } else {
469
550
  check(true, "Cursor: API key linked", "embedded in plugin mcp.json");
470
551
  }
552
+ } else {
553
+ skipped("Cursor", installed("cursor") ? "installed, but Pushary is not set up for it" : "not installed");
471
554
  }
472
555
  const vscodePluginDir2 = vscodePluginDir();
473
556
  if (existsSync(vscodePluginDir2)) {
557
+ checking("VS Code");
474
558
  const registeredIn = vscodeSettingsTargets(existsSync).filter(
475
559
  (settingsPath) => existsSync(settingsPath) && isPluginRegistered(readFileSync(settingsPath, "utf-8"), vscodePluginDir2)
476
560
  );
@@ -499,11 +583,22 @@ var main = async () => {
499
583
  check(false, "VS Code: API key linked", "no Authorization in plugin .mcp.json \u2014 re-run setup");
500
584
  } else if (vscodeAuth.includes("PUSHARY_API_KEY")) {
501
585
  check(false, "VS Code: API key linked", "plugin relies on $PUSHARY_API_KEY; re-run setup to embed the key so the gate works when VS Code is launched from the GUI");
586
+ } else if (apiKey && vscodeAuth !== `Bearer ${apiKey}`) {
587
+ check(false, "VS Code: API key matches active key", "the embedded key is not the key hooks use (rotated?) \u2014 re-run setup");
502
588
  } else {
503
589
  check(true, "VS Code: API key linked", "embedded in plugin .mcp.json");
504
590
  }
591
+ } else {
592
+ skipped("VS Code", installed("vscode") ? "installed, but Pushary is not set up for it" : "not installed");
593
+ }
594
+ check(
595
+ agentsChecked.length > 0,
596
+ "Wired to at least one agent",
597
+ agentsChecked.length > 0 ? agentsChecked.join(", ") : "Pushary is installed but configured for no agent \u2014 run npx @pushary/agent-hooks@latest setup"
598
+ );
599
+ if (claudeInPlay) {
600
+ check(existsSync(SKILL_PATH), "Skill installed", existsSync(SKILL_PATH) ? SKILL_PATH : "not found");
505
601
  }
506
- check(existsSync(SKILL_PATH), "Skill installed", existsSync(SKILL_PATH) ? SKILL_PATH : "not found");
507
602
  let globalVersion2 = "";
508
603
  try {
509
604
  globalVersion2 = execNpm("list -g --no-workspaces @pushary/agent-hooks --depth=0", { timeout: 1e4 }).toString().match(/@pushary\/agent-hooks@([\d.]+)/)?.[1] ?? "";
@@ -613,11 +708,21 @@ var main = async () => {
613
708
  console.log(` ${dim("Notification Delivery")}`);
614
709
  const probe = await fetchChannels(apiKey);
615
710
  if (probe.kind !== "ok") {
616
- check(false, "Phone reachable", probe.kind === "unreachable" ? `could not read your devices (${probe.detail})` : probe.kind === "locked" ? "no plan active, so nothing can be delivered" : "the server rejected this key");
711
+ check(
712
+ false,
713
+ "Phone reachable",
714
+ probe.kind === "unreachable" ? `could not read your devices (${probe.detail})` : probe.kind === "locked" ? "no plan active, so nothing can be delivered" : "the server rejected this key",
715
+ probe.kind === "unreachable" ? "unreachable" : "config"
716
+ );
617
717
  } else if (probe.status.precise) {
618
718
  const channels = probe.status;
619
719
  const verdict = reachVerdict(channels);
620
- check(verdict.kind === "yours", "Phone reachable", verdict.kind === "yours" ? describeReach(channels) : verdict.kind === "nobody" ? "nothing connected, run pushary connect or get the app at https://pushary.com/download" : `${describeReach(channels)}; run pushary connect to add your own`);
720
+ check(
721
+ verdict.kind === "yours",
722
+ "Phone reachable",
723
+ verdict.kind === "yours" ? describeReach(channels) : verdict.kind === "nobody" ? "nothing connected, run pushary connect or get the app at https://pushary.com/download" : `${describeReach(channels)}; run pushary connect to add your own`,
724
+ "reachability"
725
+ );
621
726
  }
622
727
  const pushSuppressed = !options.roundtrip && (options.noPush || options.json || !process.stdout.isTTY);
623
728
  if (pushSuppressed) {
@@ -662,6 +767,14 @@ var main = async () => {
662
767
  });
663
768
  const elapsed = ((Date.now() - start) / 1e3).toFixed(1);
664
769
  check(waitResult.answered === true, "Answer received", waitResult.answered ? `"${waitResult.value}" (${elapsed}s roundtrip)` : "no response within timeout");
770
+ if (waitResult.answered !== true) {
771
+ await callMcpTool(apiKey, "cancel_question", { correlationId: askResult.correlationId }, {
772
+ id: 5,
773
+ sessionId,
774
+ timeoutMs: 1e4
775
+ }).catch(() => {
776
+ });
777
+ }
665
778
  } else {
666
779
  check(false, "Question sent", "failed to get correlationId");
667
780
  }
@@ -710,7 +823,7 @@ var main = async () => {
710
823
  }
711
824
  console.log();
712
825
  const keyExit = keyVerdict && (keyVerdict.kind === "rejected" || keyVerdict.kind === "locked" || keyVerdict.kind === "malformed") ? describeKeyCheck(keyVerdict).exitCode : null;
713
- const exitCode = keyExit ?? (failed.length > 0 ? EXIT.PROBLEMS_FOUND : EXIT.OK);
826
+ const exitCode = resolveDoctorExit({ keyExit, failures: failed });
714
827
  if (options.bundle) {
715
828
  try {
716
829
  const bundle = redact(
@@ -1,8 +1,10 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
+ KILL_REASON,
3
4
  denyReasonFrom,
4
- isDeferAnswer
5
- } from "../chunk-YHG74UFF.js";
5
+ isDeferAnswer,
6
+ resolveGate
7
+ } from "../chunk-3USMXNVB.js";
6
8
  import {
7
9
  DEFAULT_SESSION,
8
10
  askUser,
@@ -18,12 +20,13 @@ import {
18
20
  handleUserPrompt,
19
21
  readLastPrompt,
20
22
  readLastUserPrompt,
23
+ repoKeyFor,
21
24
  reportEvent,
22
- resolvePolicy,
23
25
  savePendingQuestion,
26
+ scopePathFor,
24
27
  sendNotification,
25
28
  waitForAnswer
26
- } from "../chunk-XOUYM27W.js";
29
+ } from "../chunk-H46LSQRT.js";
27
30
  import {
28
31
  isGatingMoment,
29
32
  recordKeylessMoment
@@ -49,6 +52,7 @@ import "../chunk-KZERVKTD.js";
49
52
  import { basename } from "path";
50
53
 
51
54
  // src/gemini-adapter.ts
55
+ import { isAbsolute, resolve } from "path";
52
56
  var GEMINI_AGENT = { type: "gemini_cli", label: "Gemini CLI" };
53
57
  var GEMINI_TOOL_MAP = {
54
58
  run_shell_command: "Bash",
@@ -57,7 +61,7 @@ var GEMINI_TOOL_MAP = {
57
61
  read_file: "Read"
58
62
  };
59
63
  var firstString = (...values) => values.find((v) => typeof v === "string" && v.length > 0);
60
- var toGeminiPolicyLookup = (toolName, toolInput) => {
64
+ var toGeminiPolicyLookup = (toolName, toolInput, cwd) => {
61
65
  const canonical = GEMINI_TOOL_MAP[toolName];
62
66
  if (!canonical) return { tool: toolName, input: toolInput };
63
67
  if (canonical === "Bash") {
@@ -65,7 +69,11 @@ var toGeminiPolicyLookup = (toolName, toolInput) => {
65
69
  return { tool: "Bash", input: command ? { command } : {} };
66
70
  }
67
71
  const filePath = firstString(toolInput.file_path, toolInput.absolute_path, toolInput.path);
68
- return { tool: canonical, input: filePath ? { file_path: filePath } : {} };
72
+ if (!filePath) return { tool: canonical, input: {} };
73
+ return {
74
+ tool: canonical,
75
+ input: { file_path: cwd && !isAbsolute(filePath) ? resolve(cwd, filePath) : filePath }
76
+ };
69
77
  };
70
78
  var geminiAllow = () => ({ kind: "allow" });
71
79
  var geminiDeny = (reason) => ({ kind: "deny", reason });
@@ -82,9 +90,8 @@ var geminiTimeoutDecision = (timeoutAction, denyReason = "No response within tim
82
90
  };
83
91
 
84
92
  // bin/pushary-gemini-hook.ts
85
- var KILL_REASON = "Stopped by user: this agent was halted from Pushary";
86
93
  var START_MS = Date.now();
87
- var sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
94
+ var sleep = (ms) => new Promise((resolve2) => setTimeout(resolve2, ms));
88
95
  var pollForAnswer = async (apiKey, correlationId, deadlineMs, pollInterval = 2e3) => {
89
96
  while (Date.now() < deadlineMs) {
90
97
  const remaining = Math.min(Math.max(deadlineMs - Date.now(), 1e3), 3e4);
@@ -210,14 +217,23 @@ var decideBeforeTool = async (input) => {
210
217
  }
211
218
  try {
212
219
  const modeState = await fetchModeState(apiKey, input.session_id);
213
- if (modeState.kill) return geminiDeny(KILL_REASON);
214
- const lookup = toGeminiPolicyLookup(input.tool_name ?? "", input.tool_input ?? {});
215
- const policy = await getPolicy(apiKey, modeState.policyVersion, "gemini_cli");
216
- const toolPolicy = resolvePolicy(policy, lookup.tool, modeState.mode, lookup.input);
217
- if (toolPolicy.timeoutSeconds === 0 && toolPolicy.timeoutAction === "approve") return geminiAllow();
218
- if (toolPolicy.timeoutSeconds === 0 && toolPolicy.timeoutAction === "deny") {
219
- return geminiDeny(`Denied by policy for ${toolPolicy.tool}`);
220
- }
220
+ const lookup = toGeminiPolicyLookup(input.tool_name ?? "", input.tool_input ?? {}, input.cwd);
221
+ const config = await getPolicy(apiKey, modeState.policyVersion, "gemini_cli");
222
+ const scopePath = modeState.scope ? scopePathFor(lookup.tool, lookup.input, input.cwd) : void 0;
223
+ const verdict = resolveGate({
224
+ modeState,
225
+ config,
226
+ toolName: lookup.tool,
227
+ toolInputs: [lookup.input],
228
+ scopePaths: scopePath ? [scopePath] : [],
229
+ cwd: input.cwd,
230
+ repoKey: repoKeyFor(input.cwd)
231
+ });
232
+ if (verdict.kind === "kill") return geminiDeny(KILL_REASON);
233
+ if (verdict.kind === "allow") return geminiAllow();
234
+ if (verdict.kind === "deny") return geminiDeny(verdict.reason);
235
+ if (verdict.kind === "defer") return geminiPass();
236
+ const toolPolicy = verdict.policy;
221
237
  switch (toolPolicy.mode) {
222
238
  case "push_only":
223
239
  return handlePushOnly(apiKey, input, lookup, toolPolicy.timeoutSeconds, toolPolicy.timeoutAction);
@@ -1,14 +1,14 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  handlePreToolUse
4
- } from "../chunk-SONXGCZY.js";
4
+ } from "../chunk-C5I2RAMT.js";
5
5
  import "../chunk-CAJZAFVS.js";
6
6
  import {
7
7
  exitOnHelpFlag
8
8
  } from "../chunk-ATP6UQPH.js";
9
9
  import "../chunk-JGH37QRB.js";
10
- import "../chunk-YHG74UFF.js";
11
- import "../chunk-XOUYM27W.js";
10
+ import "../chunk-3USMXNVB.js";
11
+ import "../chunk-H46LSQRT.js";
12
12
  import "../chunk-WLGEY4UX.js";
13
13
  import "../chunk-BSZYIAZL.js";
14
14
  import "../chunk-SAF6HGAA.js";
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  handleNotification
4
- } from "../chunk-XOUYM27W.js";
4
+ } from "../chunk-H46LSQRT.js";
5
5
  import "../chunk-BSZYIAZL.js";
6
6
  import "../chunk-SAF6HGAA.js";
7
7
  import "../chunk-7QLSKOSU.js";
@@ -1,10 +1,10 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  handlePermissionDenied
4
- } from "../chunk-SONXGCZY.js";
4
+ } from "../chunk-C5I2RAMT.js";
5
5
  import "../chunk-CAJZAFVS.js";
6
- import "../chunk-YHG74UFF.js";
7
- import "../chunk-XOUYM27W.js";
6
+ import "../chunk-3USMXNVB.js";
7
+ import "../chunk-H46LSQRT.js";
8
8
  import "../chunk-WLGEY4UX.js";
9
9
  import "../chunk-BSZYIAZL.js";
10
10
  import "../chunk-SAF6HGAA.js";
@@ -1,10 +1,10 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  handlePermissionRequest
4
- } from "../chunk-SONXGCZY.js";
4
+ } from "../chunk-C5I2RAMT.js";
5
5
  import "../chunk-CAJZAFVS.js";
6
- import "../chunk-YHG74UFF.js";
7
- import "../chunk-XOUYM27W.js";
6
+ import "../chunk-3USMXNVB.js";
7
+ import "../chunk-H46LSQRT.js";
8
8
  import "../chunk-WLGEY4UX.js";
9
9
  import "../chunk-BSZYIAZL.js";
10
10
  import "../chunk-SAF6HGAA.js";
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  handlePostToolUse
4
- } from "../chunk-XOUYM27W.js";
4
+ } from "../chunk-H46LSQRT.js";
5
5
  import "../chunk-BSZYIAZL.js";
6
6
  import "../chunk-SAF6HGAA.js";
7
7
  import "../chunk-7QLSKOSU.js";
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  handleUserPrompt
4
- } from "../chunk-XOUYM27W.js";
4
+ } from "../chunk-H46LSQRT.js";
5
5
  import "../chunk-BSZYIAZL.js";
6
6
  import "../chunk-SAF6HGAA.js";
7
7
  import "../chunk-7QLSKOSU.js";
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  handleSessionEnd
4
- } from "../chunk-XOUYM27W.js";
4
+ } from "../chunk-H46LSQRT.js";
5
5
  import "../chunk-BSZYIAZL.js";
6
6
  import "../chunk-SAF6HGAA.js";
7
7
  import "../chunk-7QLSKOSU.js";
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  handleSessionStart
4
- } from "../chunk-XOUYM27W.js";
4
+ } from "../chunk-H46LSQRT.js";
5
5
  import "../chunk-BSZYIAZL.js";
6
6
  import "../chunk-SAF6HGAA.js";
7
7
  import "../chunk-7QLSKOSU.js";
@@ -18,7 +18,7 @@ import {
18
18
  shortenHome,
19
19
  vscodeSettingsTargets,
20
20
  writeInstructionBlock
21
- } from "../chunk-KB4ODLGB.js";
21
+ } from "../chunk-I4BYZIX4.js";
22
22
  import {
23
23
  GEMINI_HOOK_BINARY,
24
24
  addGeminiHooks,
@@ -48,7 +48,7 @@ import {
48
48
  connectViaAppPairing,
49
49
  printConnectInstructions,
50
50
  setHumanStream
51
- } from "../chunk-YMFZUXL5.js";
51
+ } from "../chunk-P7VBAYQO.js";
52
52
  import "../chunk-3EGEA4KH.js";
53
53
  import {
54
54
  KEY_FILE_MODE,
@@ -89,7 +89,7 @@ import {
89
89
  } from "../chunk-JGH37QRB.js";
90
90
  import {
91
91
  reportEvent
92
- } from "../chunk-XOUYM27W.js";
92
+ } from "../chunk-H46LSQRT.js";
93
93
  import "../chunk-BSZYIAZL.js";
94
94
  import "../chunk-SAF6HGAA.js";
95
95
  import {
@@ -1239,7 +1239,9 @@ var reportDryRun = (apiKey, agents, keyCheck) => {
1239
1239
  console.log();
1240
1240
  console.log(` ${bold("Dry run")} ${dim("\u2014 nothing below has been written")}`);
1241
1241
  console.log();
1242
- console.log(` ${dim("Key:")} ${dim(`${apiKey.slice(0, 8)}...${apiKey.slice(-4)}`)} ${dim(`(${keyCheck?.kind ?? "not verified"})`)}`);
1242
+ console.log(
1243
+ apiKey ? ` ${dim("Key:")} ${dim(`${apiKey.slice(0, 8)}...${apiKey.slice(-4)}`)} ${dim(`(${keyCheck?.kind ?? "not verified"})`)}` : ` ${dim("Key:")} ${dim("none on this machine \u2014 a real run would acquire one")}`
1244
+ );
1243
1245
  console.log(` ${dim("Key store:")} ${PUSHARY_CONFIG_FILE}`);
1244
1246
  const rc = resolveShellRc();
1245
1247
  console.log(` ${dim("Shell rc:")} ${rc ? rc.path : dim("none for this shell; the export line would be printed instead")}`);
@@ -1323,7 +1325,7 @@ var main = async () => {
1323
1325
  const masked = `${envKey.slice(0, 8)}...${envKey.slice(-4)}`;
1324
1326
  console.log(` ${check} Found API key in environment: ${dim(masked)}`);
1325
1327
  console.log();
1326
- const useExisting = options.nonInteractive ? true : await confirm({ message: "Use this key?", default: true });
1328
+ const useExisting = options.nonInteractive || options.dryRun ? true : await confirm({ message: "Use this key?", default: true });
1327
1329
  if (useExisting) {
1328
1330
  trimmedKey = envKey;
1329
1331
  storedKeyCheck = envCheck;
@@ -1331,6 +1333,14 @@ var main = async () => {
1331
1333
  const apiKey = await input({ message: "API key:" });
1332
1334
  trimmedKey = apiKey.trim();
1333
1335
  }
1336
+ } else if (options.dryRun) {
1337
+ console.log(` ${dim("No key on this machine. A real run would connect one first.")}`);
1338
+ console.log();
1339
+ markStep("resolve-agents");
1340
+ const plannedAgents = await resolveAgents(options);
1341
+ reportDryRun(null, plannedAgents, null);
1342
+ emitJson(options, { ok: true, dryRun: true, agents: plannedAgents, keyCheck: "skipped", key: "none" });
1343
+ process.exit(EXIT.OK);
1334
1344
  } else if (connectMode === "app" && canRunBrowserLogin() && !options.skipPhone) {
1335
1345
  console.log(` ${dim("Pairing with your Pushary app.")}`);
1336
1346
  console.log(` ${dim("No app? Ctrl-C, then:")} ${cyan("npx @pushary/agent-hooks setup --connect browser")}`);
@@ -1405,7 +1415,7 @@ var main = async () => {
1405
1415
  `);
1406
1416
  process.exit(await abortWith("invalid-key", EXIT.USAGE));
1407
1417
  }
1408
- recordSetupKey(trimmedKey);
1418
+ if (!options.dryRun) recordSetupKey(trimmedKey);
1409
1419
  markStep("verify-key");
1410
1420
  const keyCheck = keyFromPairing ? null : await verifyKeyOrExit(trimmedKey, options, storedKeyCheck ?? void 0);
1411
1421
  markStep("resolve-agents");
@@ -7,7 +7,7 @@ import {
7
7
  describeReach,
8
8
  fetchChannels,
9
9
  reachVerdict
10
- } from "../chunk-YMFZUXL5.js";
10
+ } from "../chunk-P7VBAYQO.js";
11
11
  import "../chunk-3EGEA4KH.js";
12
12
  import {
13
13
  readKeySource
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  handleStop
4
- } from "../chunk-XOUYM27W.js";
4
+ } from "../chunk-H46LSQRT.js";
5
5
  import "../chunk-BSZYIAZL.js";
6
6
  import "../chunk-SAF6HGAA.js";
7
7
  import "../chunk-7QLSKOSU.js";
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  handleStopFailure
4
- } from "../chunk-XOUYM27W.js";
4
+ } from "../chunk-H46LSQRT.js";
5
5
  import "../chunk-BSZYIAZL.js";
6
6
  import "../chunk-SAF6HGAA.js";
7
7
  import "../chunk-7QLSKOSU.js";