@pushary/agent-hooks 0.89.4 → 0.89.5

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.
@@ -1,11 +1,11 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  handleBell
4
- } from "../chunk-7IJYOBHL.js";
4
+ } from "../chunk-WQGMUIDW.js";
5
5
  import {
6
6
  readHookInput
7
7
  } from "../chunk-6CUUA7HO.js";
8
- import "../chunk-W6NPWHBJ.js";
8
+ import "../chunk-L3YUHEB6.js";
9
9
  import "../chunk-BC3VCZ3E.js";
10
10
  import "../chunk-OFRNZWC4.js";
11
11
  import "../chunk-6MTNS63X.js";
@@ -5,7 +5,7 @@ import {
5
5
  liveAgentCount,
6
6
  ring,
7
7
  upgradeThreshold
8
- } from "../chunk-7IJYOBHL.js";
8
+ } from "../chunk-WQGMUIDW.js";
9
9
  import {
10
10
  guardBinary
11
11
  } from "../chunk-AIGDBRIJ.js";
@@ -16,14 +16,14 @@ import {
16
16
  import {
17
17
  createIo
18
18
  } from "../chunk-5RUIFDTP.js";
19
- import "../chunk-W6NPWHBJ.js";
19
+ import "../chunk-L3YUHEB6.js";
20
20
  import {
21
21
  parseFlags
22
22
  } from "../chunk-GMXKITVA.js";
23
23
  import {
24
24
  exitOnHelpFlag
25
- } from "../chunk-KJ57OPCC.js";
26
- import "../chunk-GPNOHKFO.js";
25
+ } from "../chunk-LURY4WNF.js";
26
+ import "../chunk-3XTFX4CL.js";
27
27
  import "../chunk-BC3VCZ3E.js";
28
28
  import "../chunk-DINDL2CF.js";
29
29
  import "../chunk-OFRNZWC4.js";
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  ensureDaemonRunning
4
- } from "../chunk-NLMSSKES.js";
4
+ } from "../chunk-TRQ46MXR.js";
5
5
  import {
6
6
  buildTranscriptRecords,
7
7
  decodePublicKey,
@@ -14,9 +14,8 @@ import {
14
14
  import "../chunk-ATBKJNWS.js";
15
15
  import "../chunk-AIGDBRIJ.js";
16
16
  import {
17
- manageDaemonService,
18
- stopDaemonRunning
19
- } from "../chunk-NLMSSKES.js";
17
+ disableDaemonService
18
+ } from "../chunk-TRQ46MXR.js";
20
19
  import "../chunk-XHKBHWLX.js";
21
20
  import {
22
21
  execNpm
@@ -48,8 +47,8 @@ import {
48
47
  } from "../chunk-GMXKITVA.js";
49
48
  import {
50
49
  exitOnHelpFlag
51
- } from "../chunk-KJ57OPCC.js";
52
- import "../chunk-GPNOHKFO.js";
50
+ } from "../chunk-LURY4WNF.js";
51
+ import "../chunk-3XTFX4CL.js";
53
52
  import {
54
53
  removeClaudeAlias
55
54
  } from "../chunk-BC3VCZ3E.js";
@@ -148,9 +147,7 @@ var guardedDaemonRemoval = async () => {
148
147
  wouldNote("stop and uninstall", "Pushary background service");
149
148
  return true;
150
149
  }
151
- const service = manageDaemonService("uninstall");
152
- const detached = await stopDaemonRunning();
153
- return service.kind === "ok" && detached.kind !== "unavailable";
150
+ return (await disableDaemonService()).kind === "ok";
154
151
  };
155
152
  var guardedInstructionBlockRemoval = (path) => {
156
153
  if (!dryRun) return removeInstructionBlock(path);
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env node
2
- import "../chunk-FWUEYXSD.js";
2
+ import "../chunk-NKHEHEUM.js";
3
3
  import {
4
4
  LiveTranscriptRelay,
5
5
  liveSessionSleep,
@@ -16,14 +16,14 @@ import {
16
16
  } from "../chunk-5RUIFDTP.js";
17
17
  import {
18
18
  readKeySource
19
- } from "../chunk-W6NPWHBJ.js";
19
+ } from "../chunk-L3YUHEB6.js";
20
20
  import {
21
21
  parseFlags
22
22
  } from "../chunk-GMXKITVA.js";
23
23
  import {
24
24
  exitOnHelpFlag
25
- } from "../chunk-KJ57OPCC.js";
26
- import "../chunk-GPNOHKFO.js";
25
+ } from "../chunk-LURY4WNF.js";
26
+ import "../chunk-3XTFX4CL.js";
27
27
  import "../chunk-BC3VCZ3E.js";
28
28
  import "../chunk-DINDL2CF.js";
29
29
  import "../chunk-BSZYIAZL.js";
@@ -13,14 +13,11 @@ import {
13
13
  replaceDaemonService,
14
14
  requestDaemonHandoff,
15
15
  stopDaemonRunning
16
- } from "../chunk-NLMSSKES.js";
16
+ } from "../chunk-TRQ46MXR.js";
17
17
  import {
18
- CLAUDE_BRIDGE_CAPABILITY,
19
- CLAUDE_RESUME_CAPABILITY,
20
- CODEX_BRIDGE_CAPABILITY,
21
- CODEX_RESUME_CAPABILITY,
22
- geminiLiveCapabilities
23
- } from "../chunk-FWUEYXSD.js";
18
+ detectProviderLiveCapabilities,
19
+ hasSpawnProvider
20
+ } from "../chunk-NKHEHEUM.js";
24
21
  import {
25
22
  findLiveProcessBySpawnId,
26
23
  ownsLiveProcess
@@ -29,9 +26,7 @@ import {
29
26
  spawnClaude
30
27
  } from "../chunk-XHKBHWLX.js";
31
28
  import {
32
- resolveBinary,
33
- resolveGlobalPackageVersion,
34
- resolvePackageVersionFromBinary
29
+ resolveBinary
35
30
  } from "../chunk-PZA7G4CN.js";
36
31
  import {
37
32
  getMachineId
@@ -39,8 +34,8 @@ import {
39
34
  import "../chunk-V37ZXMH3.js";
40
35
  import {
41
36
  exitOnHelpFlag
42
- } from "../chunk-KJ57OPCC.js";
43
- import "../chunk-GPNOHKFO.js";
37
+ } from "../chunk-LURY4WNF.js";
38
+ import "../chunk-3XTFX4CL.js";
44
39
  import "../chunk-DINDL2CF.js";
45
40
  import {
46
41
  EXIT
@@ -174,8 +169,14 @@ var isAuthFailure = (err) => {
174
169
  if (candidate.name !== "DrainError") return false;
175
170
  return candidate.status === 401 || candidate.status === 403;
176
171
  };
172
+ var normalizeLaunchAgent = (requestedAgent) => {
173
+ if (requestedAgent === void 0 || requestedAgent === "claude") return "claude";
174
+ if (requestedAgent === "codex" || requestedAgent === "gemini") return requestedAgent;
175
+ return null;
176
+ };
177
177
  var providerLaunchError = (requestedAgent, binaries) => {
178
- const agent = requestedAgent === "codex" || requestedAgent === "gemini" ? requestedAgent : "claude";
178
+ const agent = normalizeLaunchAgent(requestedAgent);
179
+ if (!agent) return `Unsupported agent provider: ${requestedAgent}`;
179
180
  if (binaries[agent]) return null;
180
181
  return agent === "claude" ? "Claude Code is not installed on this machine" : `${agent === "codex" ? "Codex" : "Gemini"} CLI is not installed on this machine`;
181
182
  };
@@ -200,7 +201,8 @@ var resolveSpawnDirectory = (requested, options = {}) => {
200
201
  var isFlagShapedPrompt = (prompt) => prompt.trimStart().startsWith("-");
201
202
  var buildSpawnLaunch = (req, opts) => {
202
203
  if (isFlagShapedPrompt(req.prompt)) return null;
203
- const agent = req.agent === "codex" || req.agent === "gemini" ? req.agent : "claude";
204
+ const agent = normalizeLaunchAgent(req.agent);
205
+ if (!agent) return null;
204
206
  const model = typeof req.model === "string" && req.model.length > 0 ? ["--model", req.model] : [];
205
207
  if (agent === "claude") {
206
208
  const claudeArgs = ["claude", "--remote", ...model, "-p", req.prompt];
@@ -239,12 +241,7 @@ var runSpawnDaemon = async () => {
239
241
  const machineId = getMachineId();
240
242
  const baseUrl = getBaseUrl();
241
243
  const pusharyBin = resolveBinary("pushary") ?? "pushary";
242
- const claudeBin = resolveBinary("claude");
243
- const codexBin = resolveBinary("codex");
244
- const geminiBin = resolveBinary("gemini");
245
- const geminiCapabilities = geminiBin ? geminiLiveCapabilities(
246
- resolvePackageVersionFromBinary(geminiBin, "@google/gemini-cli") ?? resolveGlobalPackageVersion("@google/gemini-cli")
247
- ) : [];
244
+ const providerCapabilities = detectProviderLiveCapabilities();
248
245
  const siblingEntry = process.argv[1] ? join(dirname(process.argv[1]), "pushary.js") : "";
249
246
  const codexBridgeEntry = process.argv[1] ? join(dirname(process.argv[1]), "pushary-codex-bridge.js") : "pushary-codex-bridge";
250
247
  const geminiBridgeEntry = process.argv[1] ? join(dirname(process.argv[1]), "pushary-gemini-bridge.js") : "pushary-gemini-bridge";
@@ -294,7 +291,8 @@ var runSpawnDaemon = async () => {
294
291
  }
295
292
  const directory = resolveSpawnDirectory(req.cwd);
296
293
  if (!directory.ok) return Promise.reject(new Error(directory.message));
297
- const agent = req.agent === "codex" || req.agent === "gemini" ? req.agent : "claude";
294
+ const agent = normalizeLaunchAgent(req.agent);
295
+ if (!agent) return Promise.reject(new Error(`Unsupported agent provider: ${req.agent}`));
298
296
  const providerBin = resolveBinary(agent);
299
297
  const unavailable = providerLaunchError(req.agent, {
300
298
  claude: agent === "claude" ? providerBin ?? null : null,
@@ -542,12 +540,10 @@ var runSpawnDaemon = async () => {
542
540
  const result = await sendHeartbeat(apiKey, {
543
541
  repoKey: basename(process.cwd()),
544
542
  capabilities: [
545
- "spawn",
543
+ ...hasSpawnProvider(providerCapabilities) ? ["spawn"] : [],
546
544
  STOP_CAPABILITY,
547
545
  STOP_ACK_CAPABILITY,
548
- ...claudeBin ? [CLAUDE_BRIDGE_CAPABILITY, CLAUDE_RESUME_CAPABILITY] : [],
549
- ...codexBin ? [CODEX_BRIDGE_CAPABILITY, CODEX_RESUME_CAPABILITY] : [],
550
- ...geminiCapabilities
546
+ ...providerCapabilities
551
547
  ]
552
548
  });
553
549
  if (result.kind === "ok") daemonLease.recordSuccessfulHeartbeat();
@@ -563,10 +559,9 @@ var runSpawnDaemon = async () => {
563
559
  versionWatch = setInterval(() => {
564
560
  if (daemonLease.shouldYield()) finish();
565
561
  }, VERSION_CHECK_INTERVAL_MS);
566
- stderr(
567
- `[pushary] daemon online \u2014 this machine (${machineId}) can now be sent a new session from your phone. Ctrl-C to stop.
568
- `
569
- );
562
+ stderr(hasSpawnProvider(providerCapabilities) ? `[pushary] daemon online \u2014 this machine (${machineId}) can now be sent a new session from your phone. Ctrl-C to stop.
563
+ ` : `[pushary] daemon online \u2014 no supported Claude, Codex, or Gemini CLI was found, so phone-start is unavailable. Ctrl-C to stop.
564
+ `);
570
565
  timer = setTimeout(tick, FAST_POLL_MS);
571
566
  const signals = daemonSignals();
572
567
  await new Promise((resolve) => {
@@ -32,8 +32,8 @@ import {
32
32
  } from "../chunk-V37ZXMH3.js";
33
33
  import {
34
34
  exitOnHelpFlag
35
- } from "../chunk-KJ57OPCC.js";
36
- import "../chunk-GPNOHKFO.js";
35
+ } from "../chunk-LURY4WNF.js";
36
+ import "../chunk-3XTFX4CL.js";
37
37
  import "../chunk-DINDL2CF.js";
38
38
  import "../chunk-KZERVKTD.js";
39
39
  import "../chunk-L4C2JXJS.js";
@@ -31,8 +31,15 @@ import {
31
31
  import "../chunk-ATBKJNWS.js";
32
32
  import "../chunk-AIGDBRIJ.js";
33
33
  import {
34
+ daemonServiceManagerAvailable,
35
+ hasDaemonServiceDefinition,
34
36
  readDaemonStatus
35
- } from "../chunk-NLMSSKES.js";
37
+ } from "../chunk-TRQ46MXR.js";
38
+ import {
39
+ detectProviderLiveCapabilities,
40
+ hasSpawnProvider,
41
+ resolveControlPolicy
42
+ } from "../chunk-NKHEHEUM.js";
36
43
  import "../chunk-XHKBHWLX.js";
37
44
  import {
38
45
  execNpm
@@ -81,15 +88,16 @@ import {
81
88
  } from "../chunk-5RUIFDTP.js";
82
89
  import {
83
90
  keyPrefix,
91
+ readControlMode,
84
92
  readKeySource
85
- } from "../chunk-W6NPWHBJ.js";
93
+ } from "../chunk-L3YUHEB6.js";
86
94
  import {
87
95
  parseFlags
88
96
  } from "../chunk-GMXKITVA.js";
89
97
  import {
90
98
  exitOnHelpFlag
91
- } from "../chunk-KJ57OPCC.js";
92
- import "../chunk-GPNOHKFO.js";
99
+ } from "../chunk-LURY4WNF.js";
100
+ import "../chunk-3XTFX4CL.js";
93
101
  import "../chunk-BC3VCZ3E.js";
94
102
  import {
95
103
  getPackageVersion
@@ -354,23 +362,6 @@ var main = async () => {
354
362
  break;
355
363
  }
356
364
  }
357
- const daemon = readDaemonStatus();
358
- const daemonOk = daemon.kind === "running";
359
- const daemonFailure = daemonOk ? null : `local state is ${daemon.kind}`;
360
- const daemonDetail = daemonFailure ? `${daemonFailure}; run \`pushary daemon install\` to install or repair the background service` : null;
361
- check(
362
- daemonOk,
363
- "Remote-control daemon",
364
- [
365
- daemonDetail,
366
- daemon.kind,
367
- `pid ${daemon.pid ?? "none"}`,
368
- `daemon ${daemon.daemonVersion ?? "none"}`,
369
- `installed ${daemon.installedVersion}`,
370
- `machine ${daemon.machineId ?? "none"}`,
371
- `heartbeat ${daemon.lastSuccessfulHeartbeatAt ?? "none yet"}`
372
- ].filter((value) => value !== null).join(" \xB7 ")
373
- );
374
365
  const CLAUDE_JSON = claudeJson();
375
366
  const claudeInPlay = claudeWired({
376
367
  claudeJson: readJson(CLAUDE_JSON),
@@ -438,11 +429,12 @@ var main = async () => {
438
429
  codexParsed = parseTOML(codexConfig);
439
430
  } catch {
440
431
  }
441
- if (codexWired({
432
+ const codexInPlay = codexWired({
442
433
  config: codexParsed,
443
434
  hooks: codexHooksConfig,
444
435
  skillExists: existsSync(join(codexSkillDir(), "SKILL.md"))
445
- })) {
436
+ });
437
+ if (codexInPlay) {
446
438
  checking("Codex");
447
439
  const codexMcp = codexParsed.mcp_servers?.pushary;
448
440
  const codexTransport = !!codexMcp && (typeof codexMcp.url === "string" || typeof codexMcp.command === "string");
@@ -517,7 +509,8 @@ var main = async () => {
517
509
  }
518
510
  const geminiSettingsPath = geminiSettings();
519
511
  const geminiSettings2 = readJson(geminiSettingsPath);
520
- if (geminiSettings2 && geminiWired({ settings: geminiSettings2 })) {
512
+ const geminiInPlay = geminiSettings2 !== null && geminiWired({ settings: geminiSettings2 });
513
+ if (geminiInPlay) {
521
514
  checking("Gemini CLI");
522
515
  const geminiServers = geminiSettings2.mcpServers ?? {};
523
516
  const geminiPushary = geminiServers.pushary;
@@ -560,6 +553,40 @@ var main = async () => {
560
553
  } else {
561
554
  skipped("Gemini CLI", installed("gemini_cli") ? "installed, but Pushary is not set up for it" : "not installed");
562
555
  }
556
+ const configuredManagedProviders = [
557
+ ...claudeInPlay ? ["claude"] : [],
558
+ ...codexInPlay ? ["codex"] : [],
559
+ ...geminiInPlay ? ["gemini"] : []
560
+ ];
561
+ const controlSupported = hasSpawnProvider(detectProviderLiveCapabilities(configuredManagedProviders));
562
+ const controlMode = readControlMode();
563
+ const daemon = readDaemonStatus();
564
+ const serviceDefined = hasDaemonServiceDefinition();
565
+ const managerAvailable = daemonServiceManagerAvailable();
566
+ const controlPolicy = resolveControlPolicy({
567
+ mode: controlMode,
568
+ providerAvailable: controlSupported,
569
+ managerAvailable,
570
+ servicePresent: serviceDefined || daemon.kind !== "absent"
571
+ });
572
+ const controlRequired = controlPolicy === "enabled";
573
+ const controlUnsupported = controlMode === "on" && controlPolicy !== "enabled";
574
+ const daemonOk = controlUnsupported ? false : controlPolicy === "disabled" ? daemon.kind === "absent" && !serviceDefined : controlPolicy === "enabled" ? daemon.kind === "running" : controlPolicy === "legacy" ? false : daemon.kind === "absent" && !serviceDefined;
575
+ const daemonFailure = daemonOk ? null : controlUnsupported ? "control is required but no usable provider/service-manager pair is available" : controlPolicy === "disabled" ? `control is disabled but ${serviceDefined ? "the service definition remains" : `local state is ${daemon.kind}`}` : controlPolicy === "legacy" ? "a legacy service exists without a usable provider/service-manager pair" : `local state is ${daemon.kind}`;
576
+ const daemonDetail = daemonFailure ? controlUnsupported ? `${daemonFailure}; re-run setup with \`--control off\`, or restore both requirements` : controlPolicy === "disabled" ? `${daemonFailure}; re-run setup with \`--control off\`` : controlPolicy === "legacy" ? `${daemonFailure}; run setup with \`--control off\`, or restore both requirements` : `${daemonFailure}; run \`pushary daemon install\` to install or repair the background service` : controlPolicy === "disabled" ? "disabled by setup" : controlPolicy === "preserve-absent" ? "legacy absence preserved" : !controlRequired && daemon.kind === "absent" ? "managed control is unsupported on this provider or operating-system session" : null;
577
+ check(
578
+ daemonOk,
579
+ "Remote-control daemon",
580
+ [
581
+ daemonDetail,
582
+ daemon.kind,
583
+ `pid ${daemon.pid ?? "none"}`,
584
+ `daemon ${daemon.daemonVersion ?? "none"}`,
585
+ `installed ${daemon.installedVersion}`,
586
+ `machine ${daemon.machineId ?? "none"}`,
587
+ `heartbeat ${daemon.lastSuccessfulHeartbeatAt ?? "none yet"}`
588
+ ].filter((value) => value !== null).join(" \xB7 ")
589
+ );
563
590
  const cursorPluginDir2 = cursorPluginDir();
564
591
  if (existsSync(cursorPluginDir2)) {
565
592
  checking("Cursor");
@@ -15,8 +15,8 @@ import "../chunk-2HYPZLYY.js";
15
15
  import "../chunk-V37ZXMH3.js";
16
16
  import {
17
17
  exitOnHelpFlag
18
- } from "../chunk-KJ57OPCC.js";
19
- import "../chunk-GPNOHKFO.js";
18
+ } from "../chunk-LURY4WNF.js";
19
+ import "../chunk-3XTFX4CL.js";
20
20
  import "../chunk-DINDL2CF.js";
21
21
  import "../chunk-BSZYIAZL.js";
22
22
  import "../chunk-SAF6HGAA.js";
@@ -13,14 +13,14 @@ import {
13
13
  import {
14
14
  readKeySource,
15
15
  writeKey
16
- } from "../chunk-W6NPWHBJ.js";
16
+ } from "../chunk-L3YUHEB6.js";
17
17
  import {
18
18
  parseFlags
19
19
  } from "../chunk-GMXKITVA.js";
20
20
  import {
21
21
  exitOnHelpFlag
22
- } from "../chunk-KJ57OPCC.js";
23
- import "../chunk-GPNOHKFO.js";
22
+ } from "../chunk-LURY4WNF.js";
23
+ import "../chunk-3XTFX4CL.js";
24
24
  import "../chunk-BC3VCZ3E.js";
25
25
  import {
26
26
  getPackageVersion
@@ -11,14 +11,14 @@ import {
11
11
  import {
12
12
  clearKey,
13
13
  readKeySource
14
- } from "../chunk-W6NPWHBJ.js";
14
+ } from "../chunk-L3YUHEB6.js";
15
15
  import {
16
16
  parseFlags
17
17
  } from "../chunk-GMXKITVA.js";
18
18
  import {
19
19
  exitOnHelpFlag
20
- } from "../chunk-KJ57OPCC.js";
21
- import "../chunk-GPNOHKFO.js";
20
+ } from "../chunk-LURY4WNF.js";
21
+ import "../chunk-3XTFX4CL.js";
22
22
  import "../chunk-BC3VCZ3E.js";
23
23
  import "../chunk-DINDL2CF.js";
24
24
  import "../chunk-OFRNZWC4.js";
@@ -11,8 +11,8 @@ import {
11
11
  } from "../chunk-GMXKITVA.js";
12
12
  import {
13
13
  exitOnHelpFlag
14
- } from "../chunk-KJ57OPCC.js";
15
- import "../chunk-GPNOHKFO.js";
14
+ } from "../chunk-LURY4WNF.js";
15
+ import "../chunk-3XTFX4CL.js";
16
16
  import "../chunk-DINDL2CF.js";
17
17
  import "../chunk-SAF6HGAA.js";
18
18
  import {
@@ -50,9 +50,18 @@ import {
50
50
  guardedArgv
51
51
  } from "../chunk-AIGDBRIJ.js";
52
52
  import {
53
+ daemonServiceManagerAvailable,
54
+ disableDaemonService,
53
55
  ensureDaemonRunning,
56
+ hasDaemonServiceDefinition,
57
+ readDaemonStatus,
54
58
  replaceDaemonService
55
- } from "../chunk-NLMSSKES.js";
59
+ } from "../chunk-TRQ46MXR.js";
60
+ import {
61
+ detectProviderLiveCapabilities,
62
+ hasSpawnProvider,
63
+ resolveControlPolicy
64
+ } from "../chunk-NKHEHEUM.js";
56
65
  import "../chunk-XHKBHWLX.js";
57
66
  import {
58
67
  installGlobally,
@@ -108,15 +117,16 @@ import {
108
117
  exportLine,
109
118
  keyPrefix,
110
119
  readConfigFileKey,
120
+ readControlMode,
111
121
  readKeySource,
112
122
  writeKey
113
- } from "../chunk-W6NPWHBJ.js";
123
+ } from "../chunk-L3YUHEB6.js";
114
124
  import {
115
125
  parseFlags
116
126
  } from "../chunk-GMXKITVA.js";
117
127
  import {
118
128
  renderCommandHelp
119
- } from "../chunk-GPNOHKFO.js";
129
+ } from "../chunk-3XTFX4CL.js";
120
130
  import {
121
131
  installClaudeAlias,
122
132
  resolveShellRc
@@ -487,6 +497,12 @@ var parseVerifyMode = (raw) => {
487
497
  if (VERIFY_MODES.includes(value)) return value;
488
498
  throw new UsageError(`--verify must be one of: ${VERIFY_MODES.join(", ")} (got ${raw})`);
489
499
  };
500
+ var CONTROL_MODES = ["auto", "on", "off"];
501
+ var parseControlMode = (raw) => {
502
+ const value = raw.trim().toLowerCase();
503
+ if (CONTROL_MODES.includes(value)) return value;
504
+ throw new UsageError(`--control must be one of: ${CONTROL_MODES.join(", ")} (got ${raw})`);
505
+ };
490
506
  var effectiveVerify = (options, context) => {
491
507
  if (options.verifyExplicit) return options.verify;
492
508
  if (options.verify === "roundtrip") return "roundtrip";
@@ -494,8 +510,9 @@ var effectiveVerify = (options, context) => {
494
510
  };
495
511
  var SETUP_FLAGS = {
496
512
  bools: ["key-stdin", "skip-phone", "yes", "y", "json", "dry-run", "help", "h"],
497
- values: ["key", "connect", "agents", "verify"]
513
+ values: ["key", "connect", "agents", "verify", "control"]
498
514
  };
515
+ var effectiveControlMode = (options, stored, configPresent) => options.controlExplicit ? options.control : stored ?? (configPresent ? null : options.control);
499
516
  var isAgentChoice = (value) => AGENT_CHOICES.includes(value);
500
517
  var parseAgentSelection = (raw, yes) => {
501
518
  if (raw === void 0) {
@@ -532,6 +549,7 @@ var parseSetupOptions = (input2) => {
532
549
  const connectRaw = flags.values.get("connect");
533
550
  const connectMode = connectRaw === void 0 ? "app" : parseConnectMode(connectRaw);
534
551
  const verifyRaw = flags.values.get("verify");
552
+ const controlRaw = flags.values.get("control");
535
553
  const nonInteractive = yes || agentsFlag !== void 0 || flags.bools.has("json") || !input2.stdinIsTty;
536
554
  const verify = verifyRaw !== void 0 ? parseVerifyMode(verifyRaw) : nonInteractive ? "push" : "roundtrip";
537
555
  const key = flags.values.get("key")?.trim() || null;
@@ -567,7 +585,9 @@ var parseSetupOptions = (input2) => {
567
585
  key,
568
586
  keyStdin,
569
587
  verify,
570
- verifyExplicit: verifyRaw !== void 0
588
+ verifyExplicit: verifyRaw !== void 0,
589
+ control: controlRaw === void 0 ? "auto" : parseControlMode(controlRaw),
590
+ controlExplicit: controlRaw !== void 0
571
591
  };
572
592
  };
573
593
 
@@ -719,7 +739,9 @@ var checkForUpdates = async (current) => {
719
739
  } catch {
720
740
  }
721
741
  };
742
+ var globalInstallReady = false;
722
743
  var installGlobally2 = async () => {
744
+ if (globalInstallReady) return;
723
745
  const version = getPackageVersion();
724
746
  await spinner(`Installing pushary-hook globally (${version})`, async () => {
725
747
  try {
@@ -728,6 +750,7 @@ var installGlobally2 = async () => {
728
750
  throw new Error(npmErrorMessage(err));
729
751
  }
730
752
  });
753
+ globalInstallReady = true;
731
754
  };
732
755
  var _cachedSkillContent = null;
733
756
  var fetchSkillContent = async () => {
@@ -1346,10 +1369,10 @@ ${pluginLocationSnippet(VSCODE_PLUGIN_DIR)}`
1346
1369
  console.log(` ${dim("\u2022")} Fully quit and reopen VS Code to load the plugin`);
1347
1370
  noteManual("Fully quit and reopen VS Code. A Reload Window may not pick up a new plugin location.");
1348
1371
  };
1349
- var saveApiKey = async (apiKey) => {
1372
+ var saveApiKey = async (apiKey, controlMode) => {
1350
1373
  let result;
1351
1374
  await spinner("Saving your API key", async () => {
1352
- result = writeKey(apiKey);
1375
+ result = writeKey(apiKey, controlMode === null ? {} : { controlMode });
1353
1376
  });
1354
1377
  if (!result) throw new Error("Could not save the API key");
1355
1378
  if (result.configBackupPath) {
@@ -1702,7 +1725,7 @@ var AGENT_TARGETS = {
1702
1725
  ],
1703
1726
  custom: ["nothing (prints connection details only)"]
1704
1727
  };
1705
- var reportDryRun = (apiKey, agents, keyCheck) => {
1728
+ var reportDryRun = (apiKey, agents, keyCheck, controlMode) => {
1706
1729
  console.log();
1707
1730
  console.log(` ${bold("Dry run")} ${dim("\u2014 nothing below has been written")}`);
1708
1731
  console.log();
@@ -1710,6 +1733,7 @@ var reportDryRun = (apiKey, agents, keyCheck) => {
1710
1733
  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")}`
1711
1734
  );
1712
1735
  console.log(` ${dim("Key store:")} ${PUSHARY_CONFIG_FILE}`);
1736
+ console.log(` ${dim("Control:")} ${controlMode ?? "unchanged (legacy install)"}`);
1713
1737
  const rc = resolveShellRc();
1714
1738
  console.log(` ${dim("Shell rc:")} ${rc ? rc.path : dim("none for this shell; the export line would be printed instead")}`);
1715
1739
  console.log();
@@ -1748,6 +1772,7 @@ var main = async () => {
1748
1772
  `);
1749
1773
  process.exit(EXIT.OK);
1750
1774
  }
1775
+ const controlMode = effectiveControlMode(options, readControlMode(), existsSync(PUSHARY_CONFIG_FILE));
1751
1776
  recordSetupOptions(options);
1752
1777
  if (options.json) {
1753
1778
  jsonMode = true;
@@ -1806,8 +1831,8 @@ var main = async () => {
1806
1831
  console.log();
1807
1832
  markStep("resolve-agents");
1808
1833
  const plannedAgents = await resolveAgents(options);
1809
- reportDryRun(null, plannedAgents, null);
1810
- emitJson(options, { ok: true, dryRun: true, agents: plannedAgents, keyCheck: "skipped", key: "none" });
1834
+ reportDryRun(null, plannedAgents, null, controlMode);
1835
+ emitJson(options, { ok: true, dryRun: true, agents: plannedAgents, control: controlMode, keyCheck: "skipped", key: "none" });
1811
1836
  process.exit(EXIT.OK);
1812
1837
  } else if (connectMode === "app" && canShowPairingQr() && !options.skipPhone) {
1813
1838
  console.log(` ${dim("Pairing with your Pushary app.")}`);
@@ -1894,12 +1919,12 @@ var main = async () => {
1894
1919
  markStep("resolve-agents");
1895
1920
  const agents = await resolveAgents(options);
1896
1921
  if (options.dryRun) {
1897
- reportDryRun(trimmedKey, agents, keyCheck);
1898
- emitJson(options, { ok: true, dryRun: true, agents, keyCheck: keyCheck?.kind ?? "skipped" });
1922
+ reportDryRun(trimmedKey, agents, keyCheck, controlMode);
1923
+ emitJson(options, { ok: true, dryRun: true, agents, control: controlMode, keyCheck: keyCheck?.kind ?? "skipped" });
1899
1924
  process.exit(EXIT.OK);
1900
1925
  }
1901
1926
  markStep("save-key");
1902
- const rc = await saveApiKey(trimmedKey);
1927
+ const rc = await saveApiKey(trimmedKey, controlMode);
1903
1928
  const context = { rc };
1904
1929
  if (envKey && envKey !== trimmedKey) {
1905
1930
  noteManual(
@@ -1909,6 +1934,7 @@ var main = async () => {
1909
1934
  const failed = [];
1910
1935
  const completed = [];
1911
1936
  const skipped = [];
1937
+ let controlDisableFailure = null;
1912
1938
  if (agents.length === 0) {
1913
1939
  console.log(`
1914
1940
  ${check} API key saved. No agents configured.`);
@@ -1947,6 +1973,16 @@ var main = async () => {
1947
1973
  await offerClaudeAlias(options);
1948
1974
  }
1949
1975
  }
1976
+ if (controlMode === "off") {
1977
+ markStep("control:disable");
1978
+ const disabled = await disableDaemonService();
1979
+ if (disabled.kind !== "ok") {
1980
+ controlDisableFailure = disabled.detail;
1981
+ console.log(` ${yellow("!")} Could not fully disable phone-start: ${controlDisableFailure}`);
1982
+ } else {
1983
+ console.log(` ${check} Phone-start background service disabled.`);
1984
+ }
1985
+ }
1950
1986
  markStep("connect-phone");
1951
1987
  const skipPhone = options.skipPhone;
1952
1988
  let phone = null;
@@ -2008,6 +2044,8 @@ var main = async () => {
2008
2044
  failed,
2009
2045
  keyCheck: "locked",
2010
2046
  rc: rc.kind,
2047
+ control: controlMode,
2048
+ daemon: controlMode === "off" ? controlDisableFailure ? "unavailable" : "disabled" : "skipped",
2011
2049
  exitCode: report.exitCode
2012
2050
  });
2013
2051
  await reportSetupOutcome({
@@ -2028,11 +2066,35 @@ var main = async () => {
2028
2066
  const reachesYou = phone?.reachesYou ?? true;
2029
2067
  markStep("closing");
2030
2068
  const keyIsTrusted = keyFromPairing || keyCheck?.kind === "ok";
2031
- const daemon = keyIsTrusted && completed.length > 0 ? await (async () => {
2032
- const service = await replaceDaemonService();
2033
- return service.kind === "ok" ? ensureDaemonRunning({ spawnDaemon: () => {
2034
- } }) : { kind: "unavailable", detail: service.detail };
2035
- })() : null;
2069
+ const managedControlAgents = ["claude_code", "codex", "gemini_cli"];
2070
+ const configuredControlAgents = managedControlAgents.filter((agent) => agentIsWired(agent));
2071
+ const controlCapabilities = detectProviderLiveCapabilities(configuredControlAgents.map(
2072
+ (agent) => agent === "claude_code" ? "claude" : agent === "gemini_cli" ? "gemini" : agent
2073
+ ));
2074
+ const providerAvailable = hasSpawnProvider(controlCapabilities);
2075
+ const managerAvailable = daemonServiceManagerAvailable();
2076
+ const servicePresent = readDaemonStatus().kind !== "absent" || hasDaemonServiceDefinition();
2077
+ const controlPolicy = resolveControlPolicy({
2078
+ mode: controlMode,
2079
+ providerAvailable,
2080
+ managerAvailable,
2081
+ servicePresent
2082
+ });
2083
+ if (controlMode === "on" && controlPolicy !== "enabled") {
2084
+ console.log(` ${yellow("!")} Phone-start was required, but this machine has no supported provider or native background-service manager.`);
2085
+ } else if (controlPolicy === "legacy") {
2086
+ console.log(` ${yellow("!")} A legacy phone-start service cannot be safely adopted in this session. Disable it with --control off or restore its provider and native service manager.`);
2087
+ }
2088
+ const daemon = keyIsTrusted && controlPolicy === "enabled" ? await (async () => {
2089
+ try {
2090
+ await installGlobally2();
2091
+ const service = await replaceDaemonService();
2092
+ return service.kind === "ok" ? ensureDaemonRunning({ spawnDaemon: () => {
2093
+ } }) : { kind: "unavailable", detail: service.detail };
2094
+ } catch (err) {
2095
+ return { kind: "unavailable", detail: formatError(err) };
2096
+ }
2097
+ })() : controlMode === "on" || controlPolicy === "legacy" ? { kind: "unavailable", detail: "Managed control is required but no usable provider/service-manager pair is available" } : null;
2036
2098
  const closing = describeClosing({
2037
2099
  agentsOk,
2038
2100
  configuredNothing,
@@ -2040,7 +2102,7 @@ var main = async () => {
2040
2102
  failed,
2041
2103
  reachChecked,
2042
2104
  reachesYou,
2043
- daemonReady: daemon === null ? null : daemon.kind === "running" || daemon.kind === "started",
2105
+ daemonReady: controlDisableFailure ? false : daemon === null ? null : daemon.kind === "running" || daemon.kind === "started",
2044
2106
  // null when the check did not run, so it changes nothing. Only an outright
2045
2107
  // `false` is allowed to hold back "Setup complete.".
2046
2108
  roundTripAnswered: roundTrip === null ? null : roundTrip.kind === "answered" ? true : roundTrip.kind === "unanswered" ? false : null
@@ -2085,7 +2147,10 @@ var main = async () => {
2085
2147
  roundTrip: roundTrip === null ? "skipped" : roundTrip.kind,
2086
2148
  rc: rc.kind,
2087
2149
  rcPath: rc.path,
2088
- daemon: daemon?.kind ?? "skipped",
2150
+ daemon: controlMode === "off" ? controlDisableFailure ? "unavailable" : "disabled" : controlPolicy === "unsupported" || controlPolicy === "preserve-absent" ? "unsupported" : daemon?.kind ?? "skipped",
2151
+ control: controlMode,
2152
+ controlPolicy,
2153
+ controlCapabilities,
2089
2154
  exitCode
2090
2155
  });
2091
2156
  if (exitCode !== EXIT.OK) {
@@ -4,8 +4,8 @@ import {
4
4
  } from "../chunk-VZKXBFUA.js";
5
5
  import {
6
6
  exitOnHelpFlag
7
- } from "../chunk-KJ57OPCC.js";
8
- import "../chunk-GPNOHKFO.js";
7
+ } from "../chunk-LURY4WNF.js";
8
+ import "../chunk-3XTFX4CL.js";
9
9
  import "../chunk-DINDL2CF.js";
10
10
  import "../chunk-KZERVKTD.js";
11
11
  import {
@@ -20,14 +20,14 @@ import {
20
20
  } from "../chunk-5RUIFDTP.js";
21
21
  import {
22
22
  readKeySource
23
- } from "../chunk-W6NPWHBJ.js";
23
+ } from "../chunk-L3YUHEB6.js";
24
24
  import {
25
25
  rejectUnknownFlags
26
26
  } from "../chunk-GMXKITVA.js";
27
27
  import {
28
28
  exitOnHelpFlag
29
- } from "../chunk-KJ57OPCC.js";
30
- import "../chunk-GPNOHKFO.js";
29
+ } from "../chunk-LURY4WNF.js";
30
+ import "../chunk-3XTFX4CL.js";
31
31
  import "../chunk-BC3VCZ3E.js";
32
32
  import "../chunk-DINDL2CF.js";
33
33
  import "../chunk-BSZYIAZL.js";
@@ -7,8 +7,8 @@ import {
7
7
  } from "../chunk-GMXKITVA.js";
8
8
  import {
9
9
  exitOnHelpFlag
10
- } from "../chunk-KJ57OPCC.js";
11
- import "../chunk-GPNOHKFO.js";
10
+ } from "../chunk-LURY4WNF.js";
11
+ import "../chunk-3XTFX4CL.js";
12
12
  import "../chunk-DINDL2CF.js";
13
13
  import {
14
14
  UsageError
@@ -16,9 +16,18 @@ import {
16
16
  guardBinary
17
17
  } from "../chunk-AIGDBRIJ.js";
18
18
  import {
19
+ daemonServiceManagerAvailable,
20
+ disableDaemonService,
19
21
  ensureDaemonRunning,
22
+ hasDaemonServiceDefinition,
23
+ readDaemonStatus,
20
24
  replaceDaemonService
21
- } from "../chunk-NLMSSKES.js";
25
+ } from "../chunk-TRQ46MXR.js";
26
+ import {
27
+ detectProviderLiveCapabilities,
28
+ hasSpawnProvider,
29
+ resolveControlPolicy
30
+ } from "../chunk-NKHEHEUM.js";
22
31
  import {
23
32
  needsShell
24
33
  } from "../chunk-XHKBHWLX.js";
@@ -44,13 +53,18 @@ import {
44
53
  vscodePluginDir,
45
54
  vscodePluginMcp
46
55
  } from "../chunk-V37ZXMH3.js";
56
+ import {
57
+ readControlMode
58
+ } from "../chunk-L3YUHEB6.js";
47
59
  import {
48
60
  exitOnHelpFlag
49
- } from "../chunk-KJ57OPCC.js";
50
- import "../chunk-GPNOHKFO.js";
61
+ } from "../chunk-LURY4WNF.js";
62
+ import "../chunk-3XTFX4CL.js";
63
+ import "../chunk-BC3VCZ3E.js";
51
64
  import {
52
65
  isNewerVersion
53
66
  } from "../chunk-DINDL2CF.js";
67
+ import "../chunk-OFRNZWC4.js";
54
68
  import "../chunk-KZERVKTD.js";
55
69
  import {
56
70
  readJsonSafe
@@ -92,7 +106,7 @@ var reapplyClaudeSettings = (apiKey, paths = {}) => {
92
106
  const settings = readJson(settingsPath);
93
107
  const claudeJson2 = readJson(claudeJsonPath);
94
108
  const configured = settings != null && hasPusharyClaudeHooks(settings) || claudeJson2 != null && hasPusharyMcp(claudeJson2);
95
- if (!configured) return { reapplied: false, hooks: false, mcp: false };
109
+ if (!configured) return { configured: false, reapplied: false, hooks: false, mcp: false };
96
110
  const settingsWritable = settings != null;
97
111
  const claudeJsonWritable = claudeJson2 != null;
98
112
  let hooks = false;
@@ -116,7 +130,7 @@ var reapplyClaudeSettings = (apiKey, paths = {}) => {
116
130
  } catch {
117
131
  }
118
132
  }
119
- return { reapplied: hooks || mcp, hooks, mcp };
133
+ return { configured: true, reapplied: hooks || mcp, hooks, mcp };
120
134
  };
121
135
  var readToml = (filePath) => {
122
136
  try {
@@ -132,7 +146,7 @@ var reapplyCodexSettings = (apiKey, paths = {}) => {
132
146
  const config = readToml(configPath);
133
147
  const hooksConfig = readJson(hooksPath);
134
148
  const configured = config != null && !!config.mcp_servers?.pushary || hooksConfig != null && hasCodexHooks(hooksConfig);
135
- if (!configured) return { reapplied: false, hooks: false, mcp: false };
149
+ if (!configured) return { configured: false, reapplied: false, hooks: false, mcp: false };
136
150
  const command = guardBinary(resolveGlobalBinary(CODEX_HOOK_BINARY) ?? CODEX_HOOK_BINARY);
137
151
  let hooks = false;
138
152
  let mcp = false;
@@ -155,14 +169,14 @@ var reapplyCodexSettings = (apiKey, paths = {}) => {
155
169
  } catch {
156
170
  }
157
171
  }
158
- return { reapplied: hooks || mcp, hooks, mcp };
172
+ return { configured: true, reapplied: hooks || mcp, hooks, mcp };
159
173
  };
160
174
  var reapplyGeminiSettings = (apiKey, paths = {}) => {
161
175
  const settingsPath = paths.settingsPath ?? join(homedir(), ".gemini", "settings.json");
162
176
  const settings = readJson(settingsPath);
163
- if (settings == null) return { reapplied: false, hooks: false, mcp: false };
177
+ if (settings == null) return { configured: false, reapplied: false, hooks: false, mcp: false };
164
178
  const configured = hasGeminiHooks(settings) || !!settings.mcpServers?.pushary;
165
- if (!configured) return { reapplied: false, hooks: false, mcp: false };
179
+ if (!configured) return { configured: false, reapplied: false, hooks: false, mcp: false };
166
180
  let hooks = false;
167
181
  let mcp = false;
168
182
  try {
@@ -175,20 +189,20 @@ var reapplyGeminiSettings = (apiKey, paths = {}) => {
175
189
  writeJson(settingsPath, settings);
176
190
  } catch {
177
191
  }
178
- return { reapplied: hooks || mcp, hooks, mcp };
192
+ return { configured: true, reapplied: hooks || mcp, hooks, mcp };
179
193
  };
180
194
  var reapplyPluginMcp = (mcpPath, apiKey) => {
181
- if (!apiKey) return { reapplied: false, hooks: false, mcp: false };
182
195
  const mcpConfig = readJson(mcpPath);
183
- if (mcpConfig == null) return { reapplied: false, hooks: false, mcp: false };
196
+ if (mcpConfig == null) return { configured: false, reapplied: false, hooks: false, mcp: false };
184
197
  const servers = mcpConfig.mcpServers;
185
- if (!servers?.pushary) return { reapplied: false, hooks: false, mcp: false };
198
+ if (!servers?.pushary) return { configured: false, reapplied: false, hooks: false, mcp: false };
199
+ if (!apiKey) return { configured: true, reapplied: false, hooks: false, mcp: false };
186
200
  try {
187
201
  addClaudeMcpServer(mcpConfig, apiKey);
188
202
  writeJson(mcpPath, mcpConfig);
189
- return { reapplied: true, hooks: false, mcp: true };
203
+ return { configured: true, reapplied: true, hooks: false, mcp: true };
190
204
  } catch {
191
- return { reapplied: false, hooks: false, mcp: false };
205
+ return { configured: true, reapplied: false, hooks: false, mcp: false };
192
206
  }
193
207
  };
194
208
  var reapplyCursorPlugin = (apiKey, paths = {}) => reapplyPluginMcp(
@@ -198,17 +212,17 @@ var reapplyCursorPlugin = (apiKey, paths = {}) => reapplyPluginMcp(
198
212
  var reapplyVsCodePlugin = (apiKey, paths = {}) => reapplyPluginMcp(paths.pluginMcpPath ?? vscodePluginMcp(), apiKey);
199
213
  var reapplyAllAgents = (apiKey) => {
200
214
  const runners = [
201
- { label: "Claude Code", run: () => reapplyClaudeSettings(apiKey) },
202
- { label: "Codex", run: () => reapplyCodexSettings(apiKey) },
203
- { label: "Gemini CLI", run: () => reapplyGeminiSettings(apiKey) },
204
- { label: "Cursor", run: () => reapplyCursorPlugin(apiKey) },
205
- { label: "VS Code", run: () => reapplyVsCodePlugin(apiKey) }
215
+ { id: "claude", label: "Claude Code", run: () => reapplyClaudeSettings(apiKey) },
216
+ { id: "codex", label: "Codex", run: () => reapplyCodexSettings(apiKey) },
217
+ { id: "gemini", label: "Gemini CLI", run: () => reapplyGeminiSettings(apiKey) },
218
+ { id: "cursor", label: "Cursor", run: () => reapplyCursorPlugin(apiKey) },
219
+ { id: "vscode", label: "VS Code", run: () => reapplyVsCodePlugin(apiKey) }
206
220
  ];
207
221
  const done = [];
208
222
  for (const runner of runners) {
209
223
  try {
210
224
  const result = runner.run();
211
- if (result.reapplied) done.push({ label: runner.label, result });
225
+ if (result.configured) done.push({ id: runner.id, label: runner.label, result });
212
226
  } catch {
213
227
  }
214
228
  }
@@ -278,13 +292,28 @@ var finishUpgrade = async () => {
278
292
  } catch {
279
293
  apiKey = void 0;
280
294
  }
281
- const refreshed = reapplyAllAgents(apiKey);
295
+ const inspectedAgents = reapplyAllAgents(apiKey);
296
+ const refreshed = inspectedAgents.filter((agent) => agent.result.reapplied);
297
+ const configuredControlProviders = inspectedAgents.filter((agent) => agent.result.configured && (agent.id === "claude" || agent.id === "codex" || agent.id === "gemini")).map((agent) => agent.id);
298
+ const controlMode = readControlMode();
299
+ const daemonStatus = readDaemonStatus();
300
+ const serviceWasPresent = daemonStatus.kind !== "absent" || hasDaemonServiceDefinition();
301
+ const controlPolicy = resolveControlPolicy({
302
+ mode: controlMode,
303
+ providerAvailable: hasSpawnProvider(detectProviderLiveCapabilities(configuredControlProviders)),
304
+ managerAvailable: daemonServiceManagerAvailable(),
305
+ servicePresent: serviceWasPresent
306
+ });
282
307
  let daemonReady = true;
283
- if (apiKey) {
308
+ if (controlPolicy === "disabled" && serviceWasPresent) {
309
+ daemonReady = (await disableDaemonService()).kind === "ok";
310
+ } else if (apiKey && controlPolicy === "enabled") {
284
311
  const service = await replaceDaemonService();
285
312
  const daemon = service.kind === "ok" ? await ensureDaemonRunning({ spawnDaemon: () => {
286
313
  } }) : null;
287
314
  daemonReady = service.kind === "ok" && (daemon?.kind === "running" || daemon?.kind === "started");
315
+ } else if (controlPolicy === "legacy" || controlMode === "on" && controlPolicy === "unsupported") {
316
+ daemonReady = false;
288
317
  }
289
318
  console.log();
290
319
  if (refreshed.length > 0) {
@@ -302,11 +331,11 @@ var finishUpgrade = async () => {
302
331
  }
303
332
  console.log();
304
333
  if (!daemonReady) {
305
- console.error("Upgrade installed, but the background service did not start. Run: pushary daemon install");
334
+ console.error(controlPolicy === "disabled" ? "Upgrade installed, but the disabled background service could not be fully removed. Re-run setup with --control off." : controlPolicy === "legacy" ? "Upgrade installed, but a legacy background service cannot be safely adopted in this session. Run setup with --control off, or restore its provider and native service manager." : "Upgrade installed, but the background service did not start. Run: pushary daemon install");
306
335
  process.exitCode = 1;
307
336
  return;
308
337
  }
309
- console.log("Done. Your hooks and background service use the new global install.");
338
+ console.log(controlPolicy === "disabled" ? "Done. Your hooks use the new global install; phone-start remains disabled." : controlPolicy === "unsupported" || controlPolicy === "preserve-absent" ? "Done. Your hooks use the new global install; no background service was added." : "Done. Your hooks and background service use the new global install.");
310
339
  console.log("Verify with: pushary doctor");
311
340
  };
312
341
  var main = async () => {
@@ -14,8 +14,8 @@ import {
14
14
  } from "../chunk-GMXKITVA.js";
15
15
  import {
16
16
  exitOnHelpFlag
17
- } from "../chunk-KJ57OPCC.js";
18
- import "../chunk-GPNOHKFO.js";
17
+ } from "../chunk-LURY4WNF.js";
18
+ import "../chunk-3XTFX4CL.js";
19
19
  import "../chunk-DINDL2CF.js";
20
20
  import "../chunk-SAF6HGAA.js";
21
21
  import {
@@ -3,7 +3,7 @@ import {
3
3
  COMMANDS,
4
4
  isCommandName,
5
5
  renderHelp
6
- } from "../chunk-GPNOHKFO.js";
6
+ } from "../chunk-3XTFX4CL.js";
7
7
  import {
8
8
  getPackageVersion
9
9
  } from "../chunk-DINDL2CF.js";
@@ -132,6 +132,7 @@ var SETUP_OPTIONS = [
132
132
  ["--connect browser", "Approve in a browser tab instead. For a machine with no app"],
133
133
  ["--connect web", "Legacy browser subscribe page"],
134
134
  ["--connect none", "Skip the phone connect step, same as --skip-phone"],
135
+ ["--control auto|on|off", "Manage phone-start automatically (default), require it, or disable its background service"],
135
136
  ["--skip-phone", "Skip the phone connect step"],
136
137
  ["--verify roundtrip", "Send a real question and wait for you to answer it (default when a human is present)"],
137
138
  ["--verify push", "Only prove a notification was delivered. Automatic when nobody is at the terminal"],
@@ -121,7 +121,12 @@ var writeKey = (apiKey, options = {}) => {
121
121
  }
122
122
  writeJsonAtomic(
123
123
  configPath,
124
- { ...current, apiKey, installSource: resolveInstallSourceFor(current) },
124
+ {
125
+ ...current,
126
+ apiKey,
127
+ installSource: resolveInstallSourceFor(current),
128
+ ...options.controlMode ? { controlMode: options.controlMode } : {}
129
+ },
125
130
  KEY_FILE_MODE
126
131
  );
127
132
  restrictFile(configPath);
@@ -177,6 +182,12 @@ var readConfigKey = (configPath) => {
177
182
  return key || null;
178
183
  };
179
184
  var readConfigFileKey = () => readConfigKey(configFilePath());
185
+ var readControlMode = () => {
186
+ const result = readJsonSafe(configFilePath());
187
+ if (result.kind !== "ok") return null;
188
+ const value = result.value.controlMode;
189
+ return value === "auto" || value === "on" || value === "off" ? value : null;
190
+ };
180
191
  var readKeySource = () => {
181
192
  const configPath = configFilePath();
182
193
  const envKey = process.env.PUSHARY_API_KEY?.trim() || null;
@@ -194,5 +205,6 @@ export {
194
205
  writeKey,
195
206
  clearKey,
196
207
  readConfigFileKey,
208
+ readControlMode,
197
209
  readKeySource
198
210
  };
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  renderCommandHelp
3
- } from "./chunk-GPNOHKFO.js";
3
+ } from "./chunk-3XTFX4CL.js";
4
4
  import {
5
5
  getPackageVersion
6
6
  } from "./chunk-DINDL2CF.js";
@@ -0,0 +1,56 @@
1
+ import {
2
+ resolveBinary,
3
+ resolveGlobalPackageVersion,
4
+ resolvePackageVersionFromBinary
5
+ } from "./chunk-PZA7G4CN.js";
6
+
7
+ // src/live-capabilities.ts
8
+ var CLAUDE_BRIDGE_CAPABILITY = "claude-bridge";
9
+ var CLAUDE_RESUME_CAPABILITY = "claude-resume";
10
+ var CODEX_BRIDGE_CAPABILITY = "codex-bridge";
11
+ var CODEX_RESUME_CAPABILITY = "codex-resume";
12
+ var GEMINI_BRIDGE_CAPABILITY = "gemini-bridge";
13
+ var GEMINI_RESUME_CAPABILITY = "gemini-resume";
14
+ var GEMINI_BRIDGE_MIN_VERSION = [0, 40, 0];
15
+ var version = (raw) => {
16
+ const match = raw?.match(/^(\d+)\.(\d+)\.(\d+)/);
17
+ return match ? [Number(match[1]), Number(match[2]), Number(match[3])] : null;
18
+ };
19
+ var geminiLiveCapabilities = (rawVersion) => {
20
+ const installed = version(rawVersion);
21
+ if (!installed) return [];
22
+ for (let index = 0; index < GEMINI_BRIDGE_MIN_VERSION.length; index++) {
23
+ if (installed[index] > GEMINI_BRIDGE_MIN_VERSION[index]) break;
24
+ if (installed[index] < GEMINI_BRIDGE_MIN_VERSION[index]) return [];
25
+ }
26
+ return [GEMINI_BRIDGE_CAPABILITY, GEMINI_RESUME_CAPABILITY];
27
+ };
28
+ var providerLiveCapabilities = (probe) => [
29
+ ...probe.claude ? [CLAUDE_BRIDGE_CAPABILITY, CLAUDE_RESUME_CAPABILITY] : [],
30
+ ...probe.codex ? [CODEX_BRIDGE_CAPABILITY, CODEX_RESUME_CAPABILITY] : [],
31
+ ...geminiLiveCapabilities(probe.geminiVersion)
32
+ ];
33
+ var hasSpawnProvider = (capabilities) => capabilities.some(
34
+ (capability) => capability === CLAUDE_BRIDGE_CAPABILITY || capability === CODEX_BRIDGE_CAPABILITY || capability === GEMINI_BRIDGE_CAPABILITY
35
+ );
36
+ var resolveControlPolicy = (input) => {
37
+ if (input.mode === "off") return "disabled";
38
+ if (input.mode === null && !input.servicePresent) return "preserve-absent";
39
+ if (input.providerAvailable && input.managerAvailable) return "enabled";
40
+ return input.servicePresent ? "legacy" : "unsupported";
41
+ };
42
+ var detectProviderLiveCapabilities = (providers = ["claude", "codex", "gemini"]) => {
43
+ const enabled = new Set(providers);
44
+ const geminiBin = enabled.has("gemini") ? resolveBinary("gemini") : void 0;
45
+ return providerLiveCapabilities({
46
+ claude: enabled.has("claude") && resolveBinary("claude") !== void 0,
47
+ codex: enabled.has("codex") && resolveBinary("codex") !== void 0,
48
+ geminiVersion: geminiBin ? resolvePackageVersionFromBinary(geminiBin, "@google/gemini-cli") ?? resolveGlobalPackageVersion("@google/gemini-cli") : void 0
49
+ });
50
+ };
51
+
52
+ export {
53
+ hasSpawnProvider,
54
+ resolveControlPolicy,
55
+ detectProviderLiveCapabilities
56
+ };
@@ -30,6 +30,31 @@ import { createHash, randomUUID } from "crypto";
30
30
  import { homedir } from "os";
31
31
  import { execFileSync } from "child_process";
32
32
  var daemonNeedsHandoff = (status) => status.kind === "version-mismatch" || status.kind === "configuration-mismatch" || status.kind === "unresponsive" && status.processAlive === true;
33
+ var daemonServiceManagerAvailable = (options = {}) => {
34
+ const platform = options.platform ?? process.platform;
35
+ const run = options.run ?? ((command, args) => {
36
+ execFileSync(command, [...args], { stdio: "ignore", windowsHide: true });
37
+ });
38
+ try {
39
+ if (platform === "darwin") {
40
+ const uid = options.uid ?? process.getuid?.();
41
+ if (uid === void 0) return false;
42
+ run("launchctl", ["print", `gui/${uid}`]);
43
+ return true;
44
+ }
45
+ if (platform === "linux") {
46
+ run("systemctl", ["--user", "show-environment"]);
47
+ return true;
48
+ }
49
+ if (platform === "win32") {
50
+ run("sc.exe", ["query", "Schedule"]);
51
+ return true;
52
+ }
53
+ return false;
54
+ } catch {
55
+ return false;
56
+ }
57
+ };
33
58
  var statePath = (dir) => join(dir, "daemon-state.json");
34
59
  var lockPath = (dir) => join(dir, "daemon.lock");
35
60
  var targetPath = (dir) => join(dir, "daemon-target.json");
@@ -78,7 +103,8 @@ var buildDaemonService = (input) => {
78
103
  ],
79
104
  start: [{ command: "launchctl", args: ["kickstart", "-k", target] }],
80
105
  stop: [{ command: "launchctl", args: ["kill", "SIGTERM", target], ignoreFailure: true }],
81
- uninstall: [{ command: "launchctl", args: ["bootout", domain, definitionPath], ignoreFailure: true }]
106
+ uninstall: [{ command: "launchctl", args: ["bootout", domain, definitionPath], ignoreFailure: true }],
107
+ registrationProbe: { command: "launchctl", args: ["print", target] }
82
108
  };
83
109
  }
84
110
  if (input.platform === "linux") {
@@ -111,6 +137,7 @@ WantedBy=default.target
111
137
  uninstall: [
112
138
  { command: "systemctl", args: ["--user", "disable", "--now", unit], ignoreFailure: true }
113
139
  ],
140
+ registrationProbe: { command: "systemctl", args: ["--user", "is-enabled", unit] },
114
141
  reloadAfterUninstall: { command: "systemctl", args: ["--user", "daemon-reload"] }
115
142
  };
116
143
  }
@@ -148,7 +175,8 @@ WantedBy=default.target
148
175
  ],
149
176
  start: [{ command: "schtasks.exe", args: ["/Run", "/TN", taskName] }],
150
177
  stop: [{ command: "schtasks.exe", args: ["/End", "/TN", taskName], ignoreFailure: true }],
151
- uninstall: [{ command: "schtasks.exe", args: ["/Delete", "/TN", taskName, "/F"], ignoreFailure: true }]
178
+ uninstall: [{ command: "schtasks.exe", args: ["/Delete", "/TN", taskName, "/F"], ignoreFailure: true }],
179
+ registrationProbe: { command: "schtasks.exe", args: ["/Query", "/TN", taskName] }
152
180
  };
153
181
  }
154
182
  return null;
@@ -179,12 +207,6 @@ var resolveDaemonServiceDefinition = (options = {}) => {
179
207
  }
180
208
  }
181
209
  }
182
- const argvEntry = options.argvEntry ?? process.argv[1];
183
- if (!daemonEntry && argvEntry) {
184
- const path = platform === "win32" ? win32Path : { dirname, join };
185
- const candidate = path.join(path.dirname(argvEntry), "pushary-daemon.js");
186
- if (exists(candidate)) daemonEntry = candidate;
187
- }
188
210
  if (!daemonEntry || !exists(daemonEntry)) return null;
189
211
  const homeDir = options.homeDir ?? homedir();
190
212
  return buildDaemonService({
@@ -212,6 +234,13 @@ var applyDaemonService = (action, definition, options = {}) => {
212
234
  }
213
235
  }
214
236
  if (action === "uninstall") {
237
+ let registered = false;
238
+ try {
239
+ run(definition.registrationProbe.command, definition.registrationProbe.args);
240
+ registered = true;
241
+ } catch {
242
+ }
243
+ if (registered) throw new Error("Native background service remains registered");
215
244
  safeUnlink(definition.definitionPath);
216
245
  const reload = definition.reloadAfterUninstall;
217
246
  if (reload) run(reload.command, reload.args);
@@ -251,8 +280,23 @@ var manageDaemonService = (action, options = {}) => {
251
280
  detail: "Could not find the globally installed daemon entry. Run Pushary setup again."
252
281
  };
253
282
  }
283
+ if (action === "uninstall" && !daemonServiceManagerAvailable(options)) {
284
+ return { kind: "failed", detail: "Native background-service manager is unavailable" };
285
+ }
254
286
  return applyDaemonService(action, definition, { run: options.run });
255
287
  };
288
+ var hasDaemonServiceDefinition = (options = {}) => {
289
+ const definition = buildDaemonService({
290
+ platform: options.platform ?? process.platform,
291
+ homeDir: options.homeDir ?? homedir(),
292
+ stateDir: options.stateDir ?? pusharyDir(),
293
+ execPath: options.execPath ?? process.execPath,
294
+ daemonEntry: options.daemonEntry ?? process.execPath,
295
+ pathEnv: options.pathEnv ?? process.env.PATH ?? "",
296
+ uid: options.uid ?? process.getuid?.()
297
+ });
298
+ return definition !== null && existsSync(definition.definitionPath);
299
+ };
256
300
  var spawnDetachedDaemon = (plan, options = {}) => {
257
301
  const spawnImpl = options.spawnImpl ?? spawnClaude;
258
302
  const child = spawnImpl(plan.command, plan.args, {
@@ -515,6 +559,19 @@ var stopDaemonRunning = async (options = {}) => {
515
559
  safeUnlink(lockPath(dir));
516
560
  return { kind: "stopped" };
517
561
  };
562
+ var disableDaemonService = async (options = {}) => {
563
+ const platform = options.platform ?? process.platform;
564
+ const supported = ["darwin", "linux", "win32"].includes(platform);
565
+ const managerAvailable = supported && daemonServiceManagerAvailable(options);
566
+ const definitionPresent = supported && hasDaemonServiceDefinition(options);
567
+ const service = definitionPresent && !managerAvailable ? { kind: "failed", detail: "Native background-service manager is unavailable" } : managerAvailable ? manageDaemonService("uninstall", options) : { kind: "ok" };
568
+ const stopped = await stopDaemonRunning(options);
569
+ const failures = [
570
+ service.kind === "ok" ? null : service.detail,
571
+ stopped.kind === "unavailable" ? stopped.detail : null
572
+ ].filter((detail) => detail !== null);
573
+ return failures.length > 0 ? { kind: "failed", detail: failures.join("; ") } : { kind: "ok" };
574
+ };
518
575
  var replaceDaemonService = async (options = {}) => {
519
576
  const stopped = await stopDaemonRunning(options);
520
577
  return stopped.kind === "unavailable" ? stopped : manageDaemonService("install", options);
@@ -576,13 +633,16 @@ var ensureDaemonRunning = async (options = {}) => {
576
633
  };
577
634
 
578
635
  export {
636
+ daemonServiceManagerAvailable,
579
637
  DAEMON_LEASE_RENEW_INTERVAL_MS,
580
638
  parseDaemonAction,
581
639
  manageDaemonService,
640
+ hasDaemonServiceDefinition,
582
641
  readDaemonStatus,
583
642
  acquireDaemonLease,
584
643
  requestDaemonHandoff,
585
644
  stopDaemonRunning,
645
+ disableDaemonService,
586
646
  replaceDaemonService,
587
647
  ensureDaemonRunning
588
648
  };
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  readConfigFileKey
3
- } from "./chunk-W6NPWHBJ.js";
3
+ } from "./chunk-L3YUHEB6.js";
4
4
 
5
5
  // src/bell/fleet.ts
6
6
  import { closeSync, constants, futimesSync, lstatSync, mkdirSync, openSync, readdirSync, unlinkSync } from "fs";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pushary/agent-hooks",
3
- "version": "0.89.4",
3
+ "version": "0.89.5",
4
4
  "description": "Permission hooks for AI coding agents: route tool approvals through Pushary push notifications",
5
5
  "keywords": [
6
6
  "pushary",
@@ -1,29 +0,0 @@
1
- // src/live-capabilities.ts
2
- var CLAUDE_BRIDGE_CAPABILITY = "claude-bridge";
3
- var CLAUDE_RESUME_CAPABILITY = "claude-resume";
4
- var CODEX_BRIDGE_CAPABILITY = "codex-bridge";
5
- var CODEX_RESUME_CAPABILITY = "codex-resume";
6
- var GEMINI_BRIDGE_CAPABILITY = "gemini-bridge";
7
- var GEMINI_RESUME_CAPABILITY = "gemini-resume";
8
- var GEMINI_BRIDGE_MIN_VERSION = [0, 40, 0];
9
- var version = (raw) => {
10
- const match = raw?.match(/^(\d+)\.(\d+)\.(\d+)/);
11
- return match ? [Number(match[1]), Number(match[2]), Number(match[3])] : null;
12
- };
13
- var geminiLiveCapabilities = (rawVersion) => {
14
- const installed = version(rawVersion);
15
- if (!installed) return [];
16
- for (let index = 0; index < GEMINI_BRIDGE_MIN_VERSION.length; index++) {
17
- if (installed[index] > GEMINI_BRIDGE_MIN_VERSION[index]) break;
18
- if (installed[index] < GEMINI_BRIDGE_MIN_VERSION[index]) return [];
19
- }
20
- return [GEMINI_BRIDGE_CAPABILITY, GEMINI_RESUME_CAPABILITY];
21
- };
22
-
23
- export {
24
- CLAUDE_BRIDGE_CAPABILITY,
25
- CLAUDE_RESUME_CAPABILITY,
26
- CODEX_BRIDGE_CAPABILITY,
27
- CODEX_RESUME_CAPABILITY,
28
- geminiLiveCapabilities
29
- };