@pushary/agent-hooks 0.89.10 → 0.89.12

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.
Files changed (78) hide show
  1. package/CHANGELOG.md +69 -0
  2. package/README.md +3 -2
  3. package/dist/bin/pushary-bell-hook.js +5 -5
  4. package/dist/bin/pushary-bell.js +11 -11
  5. package/dist/bin/pushary-claude.js +125 -36
  6. package/dist/bin/pushary-clean.js +39 -28
  7. package/dist/bin/pushary-codex-bridge.js +17 -10
  8. package/dist/bin/pushary-codex-hook.js +12 -11
  9. package/dist/bin/pushary-codex.js +8 -8
  10. package/dist/bin/pushary-connect.js +12 -12
  11. package/dist/bin/pushary-daemon-supervisor.d.ts +1 -0
  12. package/dist/bin/pushary-daemon-supervisor.js +27 -0
  13. package/dist/bin/pushary-daemon.js +22 -22
  14. package/dist/bin/pushary-disconnect.js +9 -9
  15. package/dist/bin/pushary-doctor.js +59 -49
  16. package/dist/bin/pushary-elicitation-hook.js +5 -5
  17. package/dist/bin/pushary-gemini-bridge.js +17 -9
  18. package/dist/bin/pushary-gemini-hook.js +11 -10
  19. package/dist/bin/pushary-hook.js +14 -14
  20. package/dist/bin/pushary-login.js +11 -11
  21. package/dist/bin/pushary-logout.js +11 -11
  22. package/dist/bin/pushary-mode.js +5 -5
  23. package/dist/bin/pushary-notification-hook.js +8 -8
  24. package/dist/bin/pushary-opencode-hook.js +11 -10
  25. package/dist/bin/pushary-permission-denied-hook.js +12 -12
  26. package/dist/bin/pushary-permission-hook.js +12 -12
  27. package/dist/bin/pushary-post-hook.js +8 -8
  28. package/dist/bin/pushary-prompt-hook.js +8 -8
  29. package/dist/bin/pushary-session-end-hook.js +8 -8
  30. package/dist/bin/pushary-session-start-hook.js +8 -8
  31. package/dist/bin/pushary-setup.js +88 -60
  32. package/dist/bin/pushary-stats.js +4 -4
  33. package/dist/bin/pushary-status.js +14 -14
  34. package/dist/bin/pushary-stop-hook.js +8 -8
  35. package/dist/bin/pushary-stopfailure-hook.js +8 -8
  36. package/dist/bin/pushary-suggestions.js +2 -2
  37. package/dist/bin/pushary-upgrade.js +21 -21
  38. package/dist/bin/pushary-wait.js +5 -5
  39. package/dist/bin/pushary.js +1 -1
  40. package/dist/{chunk-253WC6O5.js → chunk-327YMKP5.js} +1 -1
  41. package/dist/{chunk-6XUABEX2.js → chunk-3OXHYPZG.js} +1 -1
  42. package/dist/{chunk-MZGWKIUZ.js → chunk-3XSP5SML.js} +7 -3
  43. package/dist/{chunk-XX2V4QS5.js → chunk-4LL6LG5X.js} +2 -2
  44. package/dist/{chunk-DKWM5T3P.js → chunk-5NDB3YOE.js} +2 -2
  45. package/dist/{chunk-TEZ2FZD6.js → chunk-6GU55AQQ.js} +2 -2
  46. package/dist/{chunk-Q223OM5J.js → chunk-6V7ZDH3A.js} +4 -4
  47. package/dist/{chunk-OLAGKKSR.js → chunk-6VXNA5JB.js} +16 -6
  48. package/dist/{chunk-34GN3IPX.js → chunk-7DK3ZH3N.js} +1 -1
  49. package/dist/{chunk-PZRWFWSR.js → chunk-7HYJPOGG.js} +7 -7
  50. package/dist/{chunk-TY5HVMXT.js → chunk-DHR4F2CR.js} +1 -1
  51. package/dist/{chunk-3HFK3UXM.js → chunk-FCUUVHTS.js} +1 -1
  52. package/dist/{chunk-NCVIWGVA.js → chunk-GKPURU7P.js} +1 -1
  53. package/dist/{chunk-UXHMWF4H.js → chunk-JHJHCMAF.js} +1 -1
  54. package/dist/chunk-JJV5Q4KG.js +20 -0
  55. package/dist/{chunk-TG3T3M4K.js → chunk-JVZX5YRJ.js} +6 -2
  56. package/dist/{chunk-6EKQZA6R.js → chunk-KVHP5ZME.js} +1 -1
  57. package/dist/{chunk-X4ZUUFKV.js → chunk-L4ZCFMWW.js} +63 -1
  58. package/dist/{chunk-2LPEZYMC.js → chunk-MFNQ5IMW.js} +1 -1
  59. package/dist/{chunk-KVE5RCQL.js → chunk-NBRM3FMD.js} +1 -1
  60. package/dist/{chunk-FZ73G2RO.js → chunk-NESKLV6R.js} +25 -6
  61. package/dist/{chunk-GTZLL7F4.js → chunk-OADGBOP7.js} +5 -5
  62. package/dist/{chunk-GSSFSVAQ.js → chunk-OCMOTJTW.js} +42 -24
  63. package/dist/{chunk-LURY4WNF.js → chunk-OTPJ4FUR.js} +1 -1
  64. package/dist/{chunk-QA72MKIT.js → chunk-OVEGEQSB.js} +5 -5
  65. package/dist/{chunk-3SQYKLBW.js → chunk-PS7DQ2SU.js} +46 -18
  66. package/dist/{chunk-UDBJHQMG.js → chunk-QFJZHVVL.js} +1 -1
  67. package/dist/chunk-QZCFVWA7.js +58 -0
  68. package/dist/{chunk-3XTFX4CL.js → chunk-RLQWNEPM.js} +3 -2
  69. package/dist/{chunk-3CUDMTL4.js → chunk-RNKPLUWC.js} +10 -3
  70. package/dist/{chunk-SYUIXXQW.js → chunk-VF3RTVWY.js} +1 -1
  71. package/dist/{chunk-TRVIRETT.js → chunk-VPLQFXCH.js} +1 -1
  72. package/dist/{chunk-IUN6VOKV.js → chunk-WDVQ6O4C.js} +2 -42
  73. package/dist/{chunk-IUWRVTQK.js → chunk-XREW7RQ3.js} +1 -1
  74. package/dist/{chunk-BLODMQKJ.js → chunk-Y4WVZIEI.js} +1 -1
  75. package/dist/src/index.d.ts +8 -1
  76. package/dist/src/index.js +16 -12
  77. package/package.json +2 -1
  78. /package/dist/{chunk-X6RXX2NF.js → chunk-3MCEES6Z.js} +0 -0
@@ -1,14 +1,14 @@
1
1
  import {
2
2
  PRETOOLUSE_HANDLED_TOOLS,
3
3
  PRETOOLUSE_PHONE_ANSWERED_TOOLS
4
- } from "./chunk-6EKQZA6R.js";
4
+ } from "./chunk-KVHP5ZME.js";
5
5
  import {
6
6
  defersToNativeMode
7
- } from "./chunk-NCVIWGVA.js";
7
+ } from "./chunk-GKPURU7P.js";
8
8
  import {
9
9
  isGatingMoment,
10
10
  recordKeylessMoment
11
- } from "./chunk-3HFK3UXM.js";
11
+ } from "./chunk-FCUUVHTS.js";
12
12
  import {
13
13
  denyReasonFrom,
14
14
  isDeferAnswer
@@ -20,7 +20,7 @@ import {
20
20
  readLastUserPrompt,
21
21
  repoKeyFor,
22
22
  throttlePass
23
- } from "./chunk-GTZLL7F4.js";
23
+ } from "./chunk-OADGBOP7.js";
24
24
  import {
25
25
  DEFAULT_SESSION,
26
26
  askUser,
@@ -28,18 +28,19 @@ import {
28
28
  pollForAnswer,
29
29
  savePendingQuestion,
30
30
  sendNotification
31
- } from "./chunk-UXHMWF4H.js";
31
+ } from "./chunk-JHJHCMAF.js";
32
32
  import {
33
+ UNNAMED_ACTION,
33
34
  deriveAction,
34
35
  deriveActionBody,
35
36
  deriveBlocker,
36
37
  deriveToolTarget,
37
38
  describeToolCall,
38
39
  scopePathFor
39
- } from "./chunk-TG3T3M4K.js";
40
+ } from "./chunk-JVZX5YRJ.js";
40
41
  import {
41
42
  getMachineId
42
- } from "./chunk-DKWM5T3P.js";
43
+ } from "./chunk-5NDB3YOE.js";
43
44
  import {
44
45
  getApiKey,
45
46
  getBaseUrl
@@ -54,7 +55,7 @@ import {
54
55
  parseAgentQuestionAnswers,
55
56
  parseAgentQuestions,
56
57
  resolveGate
57
- } from "./chunk-X6RXX2NF.js";
58
+ } from "./chunk-3MCEES6Z.js";
58
59
 
59
60
  // src/decision-episode.ts
60
61
  var EPISODE_PATH = "/api/agent/decision-episode";
@@ -87,11 +88,20 @@ var allow = () => claudePreToolUseOutput("allow");
87
88
  var deny = (reason) => claudePreToolUseOutput("deny", reason);
88
89
  var allowWithInput = (updatedInput) => claudePreToolUseOutput("allow", void 0, updatedInput);
89
90
  var ask = (reason) => claudePreToolUseOutput("ask", reason);
91
+ var storedWaitDeadline = (result, requestedDeadline) => {
92
+ const stored = result.waitEndsAt ? Date.parse(result.waitEndsAt) : Number.NaN;
93
+ return Number.isFinite(stored) ? Math.min(stored, requestedDeadline) : requestedDeadline;
94
+ };
95
+ var returnedAnswer = (result) => result.answered === true ? { answered: true, value: result.value, note: result.note } : null;
90
96
  var handlePushOnly = async (apiKey, description, projectName, timeoutSeconds, timeoutAction, sessionId, machineId, toolName, toolTarget, decision, signals) => {
97
+ const effectiveWait = effectiveWaitSeconds(timeoutAction, timeoutSeconds, "claude");
98
+ const now = Date.now();
99
+ const requestedDeadline = hookWaitDeadline(START_MS, effectiveWait, "claude", now);
100
+ const clampCut = hookWaitClamped(START_MS, effectiveWait, "claude", now);
91
101
  let result;
92
102
  try {
93
103
  result = await askUser(apiKey, {
94
- question: `Allow ${description}?`,
104
+ question: `Allow ${description || UNNAMED_ACTION}?`,
95
105
  type: "confirm",
96
106
  context: `Agent wants to run this in ${projectName}`,
97
107
  agentName: `Claude Code - ${projectName}`,
@@ -99,6 +109,7 @@ var handlePushOnly = async (apiKey, description, projectName, timeoutSeconds, ti
99
109
  machineId,
100
110
  toolName,
101
111
  toolTarget,
112
+ waitEndsAt: timeoutAction !== "approve" && timeoutAction !== "deny" || clampCut ? new Date(requestedDeadline).toISOString() : void 0,
102
113
  ...decision
103
114
  });
104
115
  } catch {
@@ -117,6 +128,8 @@ var handlePushOnly = async (apiKey, description, projectName, timeoutSeconds, ti
117
128
  return ask(result.suppressed ? "You are at the keyboard, approve here." : "Delivery mode is Terminal, approve here.");
118
129
  }
119
130
  if (result.noDevices) {
131
+ await cancelQuestion(apiKey, result.correlationId).catch(() => {
132
+ });
120
133
  switch (timeoutAction) {
121
134
  case "approve":
122
135
  return allow();
@@ -126,12 +139,9 @@ var handlePushOnly = async (apiKey, description, projectName, timeoutSeconds, ti
126
139
  return void 0;
127
140
  }
128
141
  }
129
- const effectiveWait = effectiveWaitSeconds(timeoutAction, timeoutSeconds, "claude");
130
- const now = Date.now();
131
- const deadline = hookWaitDeadline(START_MS, effectiveWait, "claude", now);
132
- const clampCut = hookWaitClamped(START_MS, effectiveWait, "claude", now);
142
+ const deadline = storedWaitDeadline(result, requestedDeadline);
133
143
  announceWait(effectiveWait, result.answerUrl, sessionId);
134
- const answer = await pollForAnswer(apiKey, result.correlationId, deadline);
144
+ const answer = returnedAnswer(result) ?? await pollForAnswer(apiKey, result.correlationId, deadline);
135
145
  if (answer.answered) {
136
146
  if (isDeferAnswer(answer.value)) return ask("Handling on your machine");
137
147
  signals.humanDecision = true;
@@ -147,6 +157,7 @@ var handlePushOnly = async (apiKey, description, projectName, timeoutSeconds, ti
147
157
  case "deny":
148
158
  return deny("No response within timeout");
149
159
  default:
160
+ savePendingQuestion(sessionId || DEFAULT_SESSION, result.correlationId);
150
161
  return ask("No push response, asking in terminal");
151
162
  }
152
163
  };
@@ -154,10 +165,11 @@ var handleTerminalOnly = () => {
154
165
  return ask();
155
166
  };
156
167
  var handlePushFirst = async (apiKey, description, projectName, pushFirstSeconds, sessionId, machineId, toolName, toolTarget, decision, signals) => {
168
+ const requestedDeadline = hookWaitDeadline(START_MS, pushFirstSeconds, "claude", Date.now());
157
169
  let result;
158
170
  try {
159
171
  result = await askUser(apiKey, {
160
- question: `Allow ${description}?`,
172
+ question: `Allow ${description || UNNAMED_ACTION}?`,
161
173
  type: "confirm",
162
174
  context: `Agent wants to run this in ${projectName}`,
163
175
  agentName: `Claude Code - ${projectName}`,
@@ -165,6 +177,7 @@ var handlePushFirst = async (apiKey, description, projectName, pushFirstSeconds,
165
177
  machineId,
166
178
  toolName,
167
179
  toolTarget,
180
+ waitEndsAt: new Date(requestedDeadline).toISOString(),
168
181
  ...decision
169
182
  });
170
183
  } catch {
@@ -176,11 +189,13 @@ var handlePushFirst = async (apiKey, description, projectName, pushFirstSeconds,
176
189
  return ask("You are at the keyboard, approve here.");
177
190
  }
178
191
  if (result.noDevices) {
192
+ await cancelQuestion(apiKey, result.correlationId).catch(() => {
193
+ });
179
194
  return void 0;
180
195
  }
181
196
  announceWait(pushFirstSeconds, result.answerUrl, sessionId);
182
- const deadline = hookWaitDeadline(START_MS, pushFirstSeconds, "claude", Date.now());
183
- const answer = await pollForAnswer(apiKey, result.correlationId, deadline, 1500);
197
+ const deadline = storedWaitDeadline(result, requestedDeadline);
198
+ const answer = returnedAnswer(result) ?? await pollForAnswer(apiKey, result.correlationId, deadline, 1500);
184
199
  if (answer.answered) {
185
200
  if (isDeferAnswer(answer.value)) return ask("Handling on your machine");
186
201
  signals.humanDecision = true;
@@ -330,7 +345,8 @@ var askAsOneCard = async (apiKey, input, parsed, projectName, deadline) => {
330
345
  sessionId: input.session_id,
331
346
  machineId: getMachineId(),
332
347
  toolName: "AskUserQuestion",
333
- toolUseId: input.tool_use_id
348
+ toolUseId: input.tool_use_id,
349
+ waitEndsAt: new Date(deadline).toISOString()
334
350
  });
335
351
  } catch {
336
352
  return void 0;
@@ -340,7 +356,7 @@ var askAsOneCard = async (apiKey, input, parsed, projectName, deadline) => {
340
356
  });
341
357
  return void 0;
342
358
  }
343
- const answer = await pollForAnswer(apiKey, result.correlationId, deadline);
359
+ const answer = returnedAnswer(result) ?? await pollForAnswer(apiKey, result.correlationId, storedWaitDeadline(result, deadline));
344
360
  const values = answer.answered && answer.value ? parseAgentQuestionAnswers(parsed, answer.value) : null;
345
361
  if (!values || isDeferAnswer(answer.value)) {
346
362
  savePendingQuestion(input.session_id || DEFAULT_SESSION, result.correlationId);
@@ -366,7 +382,8 @@ var askQuestionByQuestion = async (apiKey, input, parsed, projectName, deadline)
366
382
  // One AskUserQuestion call raises several questions, and their payload
367
383
  // fingerprints are identical (same tool, no target, no body). Without the
368
384
  // index they would all collapse onto the first decision.
369
- toolUseId: input.tool_use_id ? `${input.tool_use_id}#q${index}` : void 0
385
+ toolUseId: input.tool_use_id ? `${input.tool_use_id}#q${index}` : void 0,
386
+ waitEndsAt: new Date(deadline).toISOString()
370
387
  });
371
388
  } catch {
372
389
  return void 0;
@@ -376,7 +393,7 @@ var askQuestionByQuestion = async (apiKey, input, parsed, projectName, deadline)
376
393
  });
377
394
  return void 0;
378
395
  }
379
- const answer = await pollForAnswer(apiKey, result.correlationId, deadline);
396
+ const answer = returnedAnswer(result) ?? await pollForAnswer(apiKey, result.correlationId, storedWaitDeadline(result, deadline));
380
397
  const legacyValue = answer.answered ? answer.value?.trim() : void 0;
381
398
  const values = legacyValue ? parseAgentQuestionAnswers([question], legacyValue) ?? parseAgentQuestionAnswers([question], JSON.stringify({
382
399
  [question.question]: question.multiSelect ? JSON.stringify([legacyValue]) : legacyValue
@@ -404,6 +421,7 @@ var PLAN_KEEP_PLANNING = "Keep planning";
404
421
  var handleExitPlanMode = async (apiKey, input) => {
405
422
  const toolInput = input.tool_input ?? {};
406
423
  const projectName = basename(input.cwd ?? process.cwd());
424
+ const deadline = hookWaitDeadline(START_MS, effectiveWaitSeconds("wait", 0, "claude"), "claude", Date.now());
407
425
  let result;
408
426
  try {
409
427
  result = await askUser(apiKey, {
@@ -420,7 +438,8 @@ var handleExitPlanMode = async (apiKey, input) => {
420
438
  // so approving from a lock screen is a real decision and not a blind yes.
421
439
  actionBody: deriveActionBody("ExitPlanMode", toolInput),
422
440
  intent: readLastPrompt(input.session_id ?? DEFAULT_SESSION) ?? (input.transcript_path ? readLastUserPrompt(input.transcript_path) : void 0),
423
- blocker: "Waiting for you to approve the plan"
441
+ blocker: "Waiting for you to approve the plan",
442
+ waitEndsAt: new Date(deadline).toISOString()
424
443
  });
425
444
  } catch {
426
445
  return void 0;
@@ -430,8 +449,7 @@ var handleExitPlanMode = async (apiKey, input) => {
430
449
  });
431
450
  return void 0;
432
451
  }
433
- const deadline = hookWaitDeadline(START_MS, effectiveWaitSeconds("wait", 0, "claude"), "claude", Date.now());
434
- const answer = await pollForAnswer(apiKey, result.correlationId, deadline);
452
+ const answer = returnedAnswer(result) ?? await pollForAnswer(apiKey, result.correlationId, storedWaitDeadline(result, deadline));
435
453
  const value = answer.answered ? (answer.value ?? "").trim() : "";
436
454
  if (!value || isDeferAnswer(answer.value)) {
437
455
  savePendingQuestion(input.session_id || DEFAULT_SESSION, result.correlationId);
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  renderCommandHelp
3
- } from "./chunk-3XTFX4CL.js";
3
+ } from "./chunk-RLQWNEPM.js";
4
4
  import {
5
5
  getPackageVersion
6
6
  } from "./chunk-DINDL2CF.js";
@@ -1,17 +1,17 @@
1
1
  import {
2
2
  HOOK_BUDGETS,
3
3
  HOOK_LOCATOR_BINARY
4
- } from "./chunk-X6RXX2NF.js";
4
+ } from "./chunk-3MCEES6Z.js";
5
+
6
+ // src/removal-scope.ts
7
+ var namesAppLocator = (command) => command.includes(HOOK_LOCATOR_BINARY);
8
+ var isProtectedByScope = (command, scope) => scope?.keepAppWiring === true && namesAppLocator(command);
5
9
 
6
10
  // src/codex-config.ts
7
11
  import { createHash } from "crypto";
8
12
  import { homedir } from "os";
9
13
  import { join } from "path";
10
14
 
11
- // src/removal-scope.ts
12
- var namesAppLocator = (command) => command.includes(HOOK_LOCATOR_BINARY);
13
- var isProtectedByScope = (command, scope) => scope?.keepAppWiring === true && namesAppLocator(command);
14
-
15
15
  // src/canonical-json.ts
16
16
  var canonicalJson = (value) => {
17
17
  if (value === null || typeof value !== "object") return JSON.stringify(value);
@@ -4,13 +4,13 @@ import {
4
4
  } from "./chunk-XHKBHWLX.js";
5
5
  import {
6
6
  resolveGlobalBinary
7
- } from "./chunk-X4ZUUFKV.js";
7
+ } from "./chunk-L4ZCFMWW.js";
8
8
  import {
9
9
  getMachineId
10
- } from "./chunk-DKWM5T3P.js";
10
+ } from "./chunk-5NDB3YOE.js";
11
11
  import {
12
12
  pusharyDir
13
- } from "./chunk-QA72MKIT.js";
13
+ } from "./chunk-OVEGEQSB.js";
14
14
  import {
15
15
  getPackageVersion
16
16
  } from "./chunk-DINDL2CF.js";
@@ -19,7 +19,7 @@ import {
19
19
  writeFileAtomic,
20
20
  writeJsonAtomic,
21
21
  writeJsonAtomicDurable
22
- } from "./chunk-XX2V4QS5.js";
22
+ } from "./chunk-4LL6LG5X.js";
23
23
  import {
24
24
  getApiKey
25
25
  } from "./chunk-2UMNXADU.js";
@@ -28,7 +28,7 @@ import {
28
28
  import { dirname, join, win32 as win32Path } from "path";
29
29
  import { existsSync, mkdirSync, readFileSync, realpathSync, renameSync, unlinkSync, writeFileSync } from "fs";
30
30
  import { createHash, randomUUID } from "crypto";
31
- import { homedir } from "os";
31
+ import { homedir, userInfo } from "os";
32
32
  import { execFileSync } from "child_process";
33
33
  var daemonNeedsHandoff = (status) => status.kind === "version-mismatch" || status.kind === "configuration-mismatch" || status.kind === "unresponsive" && status.processAlive === true;
34
34
  var DAEMON_REMOTE_PROOF_MAX_AGE_MS = 18e4;
@@ -39,9 +39,14 @@ var remoteProofIsFresh = (timestamp, now) => {
39
39
  return Number.isFinite(age) && age >= -DAEMON_REMOTE_PROOF_CLOCK_SKEW_MS && age < DAEMON_REMOTE_PROOF_MAX_AGE_MS;
40
40
  };
41
41
  var daemonHasRemoteProof = (status, now = Date.now()) => status.kind === "running" && remoteProofIsFresh(status.lastSuccessfulHeartbeatAt, now) && remoteProofIsFresh(status.lastSuccessfulControlAt, now);
42
+ var readServiceDefinitionFile = (path) => {
43
+ const bytes = readFileSync(path);
44
+ const encoding = bytes.length >= 2 && bytes[0] === 255 && bytes[1] === 254 ? "utf16le" : "utf-8";
45
+ return { contents: bytes.toString(encoding), encoding };
46
+ };
42
47
  var runDaemonServiceCommand = (command, args) => execFileSync(command, [...args], {
43
48
  encoding: "utf-8",
44
- stdio: ["ignore", "pipe", "ignore"],
49
+ stdio: ["ignore", "pipe", "pipe"],
45
50
  windowsHide: true
46
51
  });
47
52
  var commandFailureOutput = (error) => {
@@ -163,7 +168,6 @@ After=network-online.target
163
168
  [Service]
164
169
  Type=simple
165
170
  ExecStart=${systemdQuote(input.execPath)} ${systemdQuote(input.daemonEntry)}
166
- WorkingDirectory=${systemdQuote(input.homeDir)}
167
171
  Environment=${systemdQuote(`PATH=${input.pathEnv}`)}
168
172
  Restart=on-failure
169
173
  RestartSec=10
@@ -199,12 +203,18 @@ WantedBy=default.target
199
203
  }
200
204
  if (input.platform === "win32") {
201
205
  const taskName = "Pushary Daemon";
206
+ const username = process.env.USERNAME?.trim() || userInfo().username;
207
+ const domain = process.env.USERDOMAIN?.trim();
208
+ const userId = input.userId?.trim() || (domain ? `${domain}\\${username}` : username);
202
209
  const definitionPath = win32Path.join(input.stateDir, "pushary-daemon-task.xml");
203
- const contents = `<?xml version="1.0" encoding="UTF-8"?>
210
+ const supervisorPath = win32Path.join(win32Path.dirname(input.daemonEntry), "pushary-daemon-supervisor.js");
211
+ const supervised = (input.entryExists ?? existsSync)(supervisorPath);
212
+ const args = supervised ? `&quot;${xml(supervisorPath)}&quot; &quot;${xml(input.daemonEntry)}&quot;` : `&quot;${xml(input.daemonEntry)}&quot;`;
213
+ const contents = `\uFEFF<?xml version="1.0" encoding="UTF-16"?>
204
214
  <Task version="1.4" xmlns="http://schemas.microsoft.com/windows/2004/02/mit/task">
205
215
  <RegistrationInfo><Description>Pushary phone-to-agent daemon</Description></RegistrationInfo>
206
- <Triggers><LogonTrigger><Enabled>true</Enabled></LogonTrigger></Triggers>
207
- <Principals><Principal id="Author"><LogonType>InteractiveToken</LogonType><RunLevel>LeastPrivilege</RunLevel></Principal></Principals>
216
+ <Triggers><LogonTrigger><UserId>${xml(userId)}</UserId><Enabled>true</Enabled></LogonTrigger></Triggers>
217
+ <Principals><Principal id="Author"><UserId>${xml(userId)}</UserId><LogonType>InteractiveToken</LogonType><RunLevel>LeastPrivilege</RunLevel></Principal></Principals>
208
218
  <Settings>
209
219
  <MultipleInstancesPolicy>IgnoreNew</MultipleInstancesPolicy>
210
220
  <StartWhenAvailable>true</StartWhenAvailable>
@@ -216,7 +226,7 @@ WantedBy=default.target
216
226
  <Actions Context="Author">
217
227
  <Exec>
218
228
  <Command>${xml(input.execPath)}</Command>
219
- <Arguments>&quot;${xml(input.daemonEntry)}&quot;</Arguments>
229
+ <Arguments>${args}</Arguments>
220
230
  <WorkingDirectory>${xml(input.homeDir)}</WorkingDirectory>
221
231
  </Exec>
222
232
  </Actions>
@@ -225,6 +235,7 @@ WantedBy=default.target
225
235
  return {
226
236
  definitionPath,
227
237
  contents,
238
+ encoding: "utf16le",
228
239
  install: [
229
240
  { command: "schtasks.exe", args: ["/Create", "/TN", taskName, "/XML", definitionPath, "/F"] },
230
241
  { command: "schtasks.exe", args: ["/Run", "/TN", taskName] }
@@ -302,7 +313,8 @@ var resolveDaemonServiceDefinition = (options = {}) => {
302
313
  execPath: options.execPath ?? process.execPath,
303
314
  daemonEntry,
304
315
  pathEnv: options.pathEnv ?? process.env.PATH ?? "",
305
- uid: options.uid ?? process.getuid?.()
316
+ uid: options.uid ?? process.getuid?.(),
317
+ entryExists: exists
306
318
  });
307
319
  };
308
320
  var runDaemonServiceSteps = (steps, run) => {
@@ -317,7 +329,9 @@ var runDaemonServiceSteps = (steps, run) => {
317
329
  var applyDaemonService = (action, definition, options = {}) => {
318
330
  const run = options.run ?? runDaemonServiceCommand;
319
331
  try {
320
- if (action === "install") writeFileAtomic(definition.definitionPath, definition.contents, 384);
332
+ if (action === "install") {
333
+ writeFileAtomic(definition.definitionPath, definition.contents, 384, definition.encoding);
334
+ }
321
335
  const steps = action === "uninstall" ? [...definition.stop, ...definition.uninstall] : definition[action];
322
336
  runDaemonServiceSteps(steps, run);
323
337
  if (action === "uninstall") {
@@ -674,7 +688,7 @@ var disableDaemonService = async (options = {}) => deactivateDaemonService("unin
674
688
  var parseDaemonServiceTransaction = (value) => {
675
689
  if (typeof value !== "object" || value === null) return null;
676
690
  const transaction = value;
677
- if (transaction.version !== 1 || transaction.phase !== "prepared" && transaction.phase !== "committed" || typeof transaction.definitionPath !== "string" || !(transaction.previousContents === null || typeof transaction.previousContents === "string") || typeof transaction.previousRegistered !== "boolean" || !(transaction.previousEnabled === void 0 || typeof transaction.previousEnabled === "boolean") || typeof transaction.previousRunning !== "boolean") return null;
691
+ if (transaction.version !== 1 || transaction.phase !== "prepared" && transaction.phase !== "committed" || typeof transaction.definitionPath !== "string" || !(transaction.previousContents === null || typeof transaction.previousContents === "string") || !(transaction.previousEncoding === void 0 || typeof transaction.previousEncoding === "string") || typeof transaction.previousRegistered !== "boolean" || !(transaction.previousEnabled === void 0 || typeof transaction.previousEnabled === "boolean") || typeof transaction.previousRunning !== "boolean") return null;
678
692
  return {
679
693
  ...transaction,
680
694
  previousEnabled: typeof transaction.previousEnabled === "boolean" ? transaction.previousEnabled : transaction.previousRegistered
@@ -723,12 +737,13 @@ var probeDaemonServiceState = (definition, run) => {
723
737
  return { kind: "ok", registered, enabled, running: false };
724
738
  }
725
739
  };
740
+ var previousEncoding = (snapshot) => snapshot.previousEncoding ?? "utf-8";
726
741
  var restoreDaemonServiceSnapshot = (definition, snapshot, run) => {
727
742
  const removed = applyDaemonService("uninstall", definition, { run });
728
743
  if (removed.kind !== "ok") return removed;
729
744
  try {
730
745
  if (snapshot.previousContents === null) safeUnlink(definition.definitionPath);
731
- else writeFileAtomic(definition.definitionPath, snapshot.previousContents, 384);
746
+ else writeFileAtomic(definition.definitionPath, snapshot.previousContents, 384, previousEncoding(snapshot));
732
747
  } catch (err) {
733
748
  return {
734
749
  kind: "failed",
@@ -767,7 +782,8 @@ var restoreDaemonServiceSnapshot = (definition, snapshot, run) => {
767
782
  }
768
783
  const restored = applyDaemonService("install", {
769
784
  ...definition,
770
- contents: snapshot.previousContents
785
+ contents: snapshot.previousContents,
786
+ encoding: previousEncoding(snapshot)
771
787
  }, { run });
772
788
  if (restored.kind !== "ok") return restored;
773
789
  if (!snapshot.previousRunning) applyDaemonService("stop", definition, { run });
@@ -829,9 +845,12 @@ var replaceDaemonService = async (options = {}) => {
829
845
  return { kind: "failed", detail: `Could not recover interrupted service replacement: ${recovered.detail}` };
830
846
  }
831
847
  let previousContents = null;
848
+ let previousDefinitionEncoding = "utf-8";
832
849
  try {
833
850
  if (existsSync(definition.definitionPath)) {
834
- previousContents = readFileSync(definition.definitionPath, "utf-8");
851
+ const previous = readServiceDefinitionFile(definition.definitionPath);
852
+ previousContents = previous.contents;
853
+ previousDefinitionEncoding = previous.encoding;
835
854
  }
836
855
  } catch (err) {
837
856
  return { kind: "failed", detail: `Could not snapshot the existing service: ${err instanceof Error ? err.message : String(err)}` };
@@ -846,6 +865,7 @@ var replaceDaemonService = async (options = {}) => {
846
865
  phase: "prepared",
847
866
  definitionPath: definition.definitionPath,
848
867
  previousContents,
868
+ previousEncoding: previousDefinitionEncoding,
849
869
  previousRegistered: previousState.registered,
850
870
  previousEnabled: previousState.enabled,
851
871
  previousRunning: previousState.running
@@ -869,7 +889,15 @@ var replaceDaemonService = async (options = {}) => {
869
889
  }
870
890
  let installed = applyDaemonService("install", definition, { run });
871
891
  if (installed.kind === "ok") {
872
- const state = probeDaemonServiceState(definition, run);
892
+ const sleep = options.sleep ?? wait;
893
+ const pollIntervalMs = Math.max(1, options.pollIntervalMs ?? 50);
894
+ const now = options.now ?? Date.now;
895
+ const deadline = now() + Math.max(0, options.waitTimeoutMs ?? 5e3);
896
+ let state = probeDaemonServiceState(definition, run);
897
+ while (state.kind === "ok" && (!state.registered || !state.enabled || !state.running) && now() < deadline) {
898
+ await sleep(pollIntervalMs);
899
+ state = probeDaemonServiceState(definition, run);
900
+ }
873
901
  if (state.kind !== "ok") {
874
902
  installed = { kind: "failed", detail: state.detail };
875
903
  } else if (!state.registered || !state.enabled || !state.running) {
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  resolvePathBinary
3
- } from "./chunk-X4ZUUFKV.js";
3
+ } from "./chunk-L4ZCFMWW.js";
4
4
 
5
5
  // src/hermes-config.ts
6
6
  import { execFileSync } from "child_process";
@@ -0,0 +1,58 @@
1
+ import {
2
+ getBaseUrl
3
+ } from "./chunk-2UMNXADU.js";
4
+
5
+ // src/transcript-api.ts
6
+ var fetchTranscriptRecipients = async (apiKey, baseUrl = getBaseUrl()) => {
7
+ try {
8
+ const res = await fetch(`${baseUrl}/api/agent/encryption-account`, {
9
+ headers: { Authorization: `Bearer ${apiKey}` },
10
+ signal: AbortSignal.timeout(5e3)
11
+ });
12
+ if (!res.ok) return null;
13
+ const data = await res.json();
14
+ if (typeof data.publicKey !== "string" || data.publicKey.length === 0) return null;
15
+ if (data.providerRecovery === void 0 || data.providerRecovery === null) {
16
+ return { phonePublicKey: data.publicKey, provider: null };
17
+ }
18
+ if (typeof data.providerRecovery !== "object") return null;
19
+ const provider = data.providerRecovery;
20
+ if (typeof provider.required !== "boolean" || typeof provider.keyId !== "string" || provider.keyId.length === 0 || typeof provider.publicKey !== "string" || provider.publicKey.length === 0) return null;
21
+ return {
22
+ phonePublicKey: data.publicKey,
23
+ provider: { required: provider.required, keyId: provider.keyId, publicKey: provider.publicKey }
24
+ };
25
+ } catch {
26
+ return null;
27
+ }
28
+ };
29
+ var fetchAccountPublicKey = async (apiKey, baseUrl = getBaseUrl()) => (await fetchTranscriptRecipients(apiKey, baseUrl))?.phonePublicKey ?? null;
30
+ var uploadTranscriptRecords = async (apiKey, params, baseUrl = getBaseUrl()) => {
31
+ try {
32
+ const res = await fetch(`${baseUrl}/api/agent/transcript`, {
33
+ method: "POST",
34
+ headers: { Authorization: `Bearer ${apiKey}`, "Content-Type": "application/json" },
35
+ body: JSON.stringify(params),
36
+ signal: AbortSignal.timeout(15e3)
37
+ });
38
+ const body = await res.json().catch(() => null);
39
+ const keyRegistered = body?.keyRegistered === true;
40
+ const keyConflict = body?.keyConflict === true;
41
+ const ids = (value) => Array.isArray(value) ? value.filter((v) => typeof v === "string") : [];
42
+ return {
43
+ ok: res.ok && !keyConflict && (params.dataEncryptionKey === void 0 || keyRegistered),
44
+ keyRegistered,
45
+ keyConflict,
46
+ settledIds: Array.isArray(body?.settledIds) ? ids(body.settledIds) : null,
47
+ rejectedIds: ids(body?.rejectedIds)
48
+ };
49
+ } catch {
50
+ return { ok: false, keyRegistered: false, keyConflict: false, settledIds: null, rejectedIds: [] };
51
+ }
52
+ };
53
+
54
+ export {
55
+ fetchTranscriptRecipients,
56
+ fetchAccountPublicKey,
57
+ uploadTranscriptRecords
58
+ };
@@ -3,7 +3,7 @@ var COMMAND_LIST = [
3
3
  {
4
4
  name: "setup",
5
5
  module: "pushary-setup",
6
- summary: "Configure Claude Code, Codex, Gemini CLI, Hermes, Cursor, or VS Code with Pushary"
6
+ summary: "Configure Claude Code, Codex, Gemini CLI, Hermes, Cursor, VS Code, or OpenCode with Pushary"
7
7
  },
8
8
  {
9
9
  name: "claude",
@@ -133,6 +133,7 @@ var SETUP_OPTIONS = [
133
133
  ["--connect web", "Legacy browser subscribe page"],
134
134
  ["--connect none", "Skip the phone connect step, same as --skip-phone"],
135
135
  ["--control auto|on|off", "Manage phone-start automatically (default), require it, or disable its background service"],
136
+ ["--transcripts on|off", "Enable or disable encrypted transcript sync on this machine"],
136
137
  ["--skip-phone", "Skip the phone connect step"],
137
138
  ["--verify roundtrip", "Send a real question and wait for you to answer it (default when a human is present)"],
138
139
  ["--verify push", "Only prove a notification was delivered. Automatic when nobody is at the terminal"],
@@ -140,7 +141,7 @@ var SETUP_OPTIONS = [
140
141
  ["--dry-run", "Print what would be written and change nothing"],
141
142
  ["--json", "Emit one machine-readable object instead of the human report"]
142
143
  ];
143
- var AGENT_IDS = "claude_code, codex, gemini_cli, hermes, cursor, custom";
144
+ var AGENT_IDS = "claude_code, codex, gemini_cli, hermes, cursor, vscode, opencode, custom";
144
145
  var USAGE_EXAMPLES = [
145
146
  "npx @pushary/agent-hooks@latest setup",
146
147
  "npx @pushary/agent-hooks@latest setup --agents auto --yes --skip-phone",
@@ -3,13 +3,13 @@ import {
3
3
  } from "./chunk-BC3VCZ3E.js";
4
4
  import {
5
5
  resolveInstallSourceFor
6
- } from "./chunk-IUWRVTQK.js";
6
+ } from "./chunk-XREW7RQ3.js";
7
7
  import {
8
8
  backupFile,
9
9
  readJsonSafe,
10
10
  writeFileAtomic,
11
11
  writeJsonAtomic
12
- } from "./chunk-XX2V4QS5.js";
12
+ } from "./chunk-4LL6LG5X.js";
13
13
  import {
14
14
  configFilePath
15
15
  } from "./chunk-2UMNXADU.js";
@@ -125,7 +125,8 @@ var writeKey = (apiKey, options = {}) => {
125
125
  ...current,
126
126
  apiKey,
127
127
  installSource: resolveInstallSourceFor(current),
128
- ...options.controlMode ? { controlMode: options.controlMode } : {}
128
+ ...options.controlMode ? { controlMode: options.controlMode } : {},
129
+ ...options.transcriptsEnabled === void 0 ? {} : { transcriptsEnabled: options.transcriptsEnabled }
129
130
  },
130
131
  KEY_FILE_MODE
131
132
  );
@@ -188,6 +189,11 @@ var readControlMode = () => {
188
189
  const value = result.value.controlMode;
189
190
  return value === "auto" || value === "on" || value === "off" ? value : null;
190
191
  };
192
+ var readTranscriptPreference = () => {
193
+ const result = readJsonSafe(configFilePath());
194
+ if (result.kind !== "ok") return null;
195
+ return typeof result.value.transcriptsEnabled === "boolean" ? result.value.transcriptsEnabled : null;
196
+ };
191
197
  var readKeySource = () => {
192
198
  const configPath = configFilePath();
193
199
  const envKey = process.env.PUSHARY_API_KEY?.trim() || null;
@@ -206,5 +212,6 @@ export {
206
212
  clearKey,
207
213
  readConfigFileKey,
208
214
  readControlMode,
215
+ readTranscriptPreference,
209
216
  readKeySource
210
217
  };
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  quoteIfNeeded
3
- } from "./chunk-X4ZUUFKV.js";
3
+ } from "./chunk-L4ZCFMWW.js";
4
4
 
5
5
  // src/hook-command.ts
6
6
  var HOOK_SHELL = "/bin/sh -c";
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  readConfigFileKey
3
- } from "./chunk-3CUDMTL4.js";
3
+ } from "./chunk-RNKPLUWC.js";
4
4
 
5
5
  // src/bell/fleet.ts
6
6
  import { closeSync, constants, futimesSync, lstatSync, mkdirSync, openSync, readdirSync, unlinkSync } from "fs";
@@ -1,9 +1,6 @@
1
- import {
2
- getBaseUrl
3
- } from "./chunk-2UMNXADU.js";
4
1
  import {
5
2
  redactSecrets
6
- } from "./chunk-X6RXX2NF.js";
3
+ } from "./chunk-3MCEES6Z.js";
7
4
 
8
5
  // src/crypto.ts
9
6
  import nacl from "tweetnacl";
@@ -79,46 +76,9 @@ var buildTranscriptRecords = (lines, sessionKey, seen = /* @__PURE__ */ new Set(
79
76
  return { records, seen: nextSeen };
80
77
  };
81
78
 
82
- // src/transcript-api.ts
83
- var fetchAccountPublicKey = async (apiKey, baseUrl = getBaseUrl()) => {
84
- try {
85
- const res = await fetch(`${baseUrl}/api/agent/encryption-account`, {
86
- headers: { Authorization: `Bearer ${apiKey}` },
87
- signal: AbortSignal.timeout(5e3)
88
- });
89
- if (!res.ok) return null;
90
- const data = await res.json();
91
- return typeof data.publicKey === "string" && data.publicKey.length > 0 ? data.publicKey : null;
92
- } catch {
93
- return null;
94
- }
95
- };
96
- var uploadTranscriptRecords = async (apiKey, params, baseUrl = getBaseUrl()) => {
97
- try {
98
- const res = await fetch(`${baseUrl}/api/agent/transcript`, {
99
- method: "POST",
100
- headers: { Authorization: `Bearer ${apiKey}`, "Content-Type": "application/json" },
101
- body: JSON.stringify(params),
102
- signal: AbortSignal.timeout(15e3)
103
- });
104
- const body = await res.json().catch(() => null);
105
- const keyRegistered = body?.keyRegistered === true;
106
- const keyConflict = body?.keyConflict === true;
107
- return {
108
- ok: res.ok && !keyConflict && (params.dataEncryptionKey === void 0 || keyRegistered),
109
- keyRegistered,
110
- keyConflict
111
- };
112
- } catch {
113
- return { ok: false, keyRegistered: false, keyConflict: false };
114
- }
115
- };
116
-
117
79
  export {
118
80
  decodePublicKey,
119
81
  generateSessionKey,
120
82
  wrapSessionKey,
121
- buildTranscriptRecords,
122
- fetchAccountPublicKey,
123
- uploadTranscriptRecords
83
+ buildTranscriptRecords
124
84
  };
@@ -3,7 +3,7 @@ import {
3
3
  } from "./chunk-2UMNXADU.js";
4
4
  import {
5
5
  normalizeInstallSource
6
- } from "./chunk-X6RXX2NF.js";
6
+ } from "./chunk-3MCEES6Z.js";
7
7
 
8
8
  // src/install-source.ts
9
9
  import { readFileSync } from "fs";