@pushary/agent-hooks 0.77.0 → 0.78.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,69 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.78.0
4
+
5
+ ### Your agent can ask you several things at once, and reach your phone for all of them
6
+
7
+ An agent that stops to ask you something often asks more than one thing in the
8
+ same breath. Until this release the hook only recognised a single question, so
9
+ two or more meant nothing arrived on your phone at all and the whole exchange
10
+ waited at the terminal. That is exactly the moment this product exists for.
11
+
12
+ Up to four questions now come through in turn. The notification says which one
13
+ you are on, "(2 of 3)", so the first does not look like the last.
14
+
15
+ If any question goes unanswered, whether you deferred it, it timed out, or no
16
+ device was reachable, the entire exchange goes back to the terminal, including
17
+ anything you already answered. A partly filled answer sheet would leave your
18
+ agent guessing at questions it never put to you, and being asked twice is better
19
+ than being answered wrongly.
20
+
21
+ The whole exchange shares one wait budget. Four questions do not cost four times
22
+ the wait you agreed to.
23
+
24
+ Questions that let you tick several options at once still go to the terminal.
25
+ Those answers travel as a single piece of text and the packing is not something
26
+ this release can verify, so it is left alone rather than guessed at.
27
+
28
+ ## 0.77.0
29
+
30
+ ### Stop ends the process
31
+
32
+ Until this release Stop was a gate verdict. Every pending and new tool call was
33
+ denied, which is the guarantee and has not changed, but the agent kept running,
34
+ kept thinking and kept spending tokens. For someone who hits Stop because
35
+ something is going wrong, "it can no longer act" and "it stopped" are different
36
+ promises.
37
+
38
+ - every session event reports its own pid, so the server can name the process
39
+ - the daemon advertises a `stop` capability, which is how the server knows a
40
+ machine can act on one at all
41
+ - the daemon applies stops from the drain with SIGTERM, and refuses to signal
42
+ its own pid, since killing itself would take every other session's stop too
43
+
44
+ Two additive wire changes, both safe in either direction: `pid` on the event
45
+ payload, which an older server ignores, and `stops` on the drain response, which
46
+ an older daemon ignores.
47
+
48
+ ## 0.76.1
49
+
50
+ ### Actually closing the spawn argv injection
51
+
52
+ 0.76.0 shipped under the message "closing the spawn argv injection" and did not.
53
+ It sanitised `model` and `cwd` but not `prompt`, and `prompt` is the field the
54
+ daemon passes as the token immediately after `-p`, so a flag-shaped prompt was
55
+ read back as a flag:
56
+
57
+ ['claude','--remote','-p','--permission-mode=dontAsk'] -> {permissionMode:'dontAsk'}
58
+
59
+ `dontAsk` is a full-defer mode, so the gate stopped asking about every tool for
60
+ that session.
61
+
62
+ The server-side rejection landed in production first and is what actually
63
+ protects users, 0.76.0 included, with no upgrade required. This release carries
64
+ the defence in depth: `buildSpawnLaunch` returns null for a flag-shaped prompt,
65
+ so a daemon never builds that argv even if the server is bypassed.
66
+
3
67
  ## 0.76.0
4
68
 
5
69
  ### A phone-supplied field could switch your approval gate off
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  handlePreToolUse
4
- } from "../chunk-MY4VJPWW.js";
4
+ } from "../chunk-NN4GTN6Q.js";
5
5
  import "../chunk-3CBPY2G5.js";
6
6
  import {
7
7
  exitOnHelpFlag
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  handlePermissionDenied
4
- } from "../chunk-MY4VJPWW.js";
4
+ } from "../chunk-NN4GTN6Q.js";
5
5
  import "../chunk-3CBPY2G5.js";
6
6
  import "../chunk-FWWBEB6L.js";
7
7
  import "../chunk-HPDLDQC5.js";
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  handlePermissionRequest
4
- } from "../chunk-MY4VJPWW.js";
4
+ } from "../chunk-NN4GTN6Q.js";
5
5
  import "../chunk-3CBPY2G5.js";
6
6
  import "../chunk-FWWBEB6L.js";
7
7
  import "../chunk-HPDLDQC5.js";
@@ -345,47 +345,66 @@ var shouldDeferToNativeMode = (permissionMode, toolName) => {
345
345
  }
346
346
  return false;
347
347
  };
348
- var parseSingleQuestion = (toolInput) => {
349
- const questions = toolInput.questions;
350
- if (!Array.isArray(questions) || questions.length !== 1) return void 0;
351
- const q = questions[0];
348
+ var MAX_PHONE_QUESTIONS = 4;
349
+ var parseQuestion = (raw) => {
350
+ const q = raw;
352
351
  if (!q || typeof q.question !== "string" || q.multiSelect === true) return void 0;
353
352
  const labels = Array.isArray(q.options) ? q.options.map((o) => o && typeof o === "object" ? o.label : void 0).filter((l) => typeof l === "string" && l.length > 0) : [];
354
353
  if (labels.length < 2) return void 0;
355
354
  return { question: q.question, header: typeof q.header === "string" ? q.header : void 0, labels };
356
355
  };
356
+ var parseQuestions = (toolInput) => {
357
+ const questions = toolInput.questions;
358
+ if (!Array.isArray(questions) || questions.length === 0) return void 0;
359
+ if (questions.length > MAX_PHONE_QUESTIONS) return void 0;
360
+ const parsed = [];
361
+ for (const raw of questions) {
362
+ const one = parseQuestion(raw);
363
+ if (!one) return void 0;
364
+ parsed.push(one);
365
+ }
366
+ return parsed;
367
+ };
368
+ var questionContext = (q, index, total, project) => {
369
+ const subject = q.header ? `${q.header}: your agent is asking in ${project}` : `Your agent is asking in ${project}`;
370
+ return total > 1 ? `${subject} (${index + 1} of ${total})` : subject;
371
+ };
357
372
  var handleAskUserQuestion = async (apiKey, input) => {
358
- const parsed = parseSingleQuestion(input.tool_input ?? {});
373
+ const parsed = parseQuestions(input.tool_input ?? {});
359
374
  if (!parsed) return void 0;
360
375
  const projectName = basename(input.cwd ?? process.cwd());
361
- let result;
362
- try {
363
- result = await askUser(apiKey, {
364
- question: parsed.question,
365
- type: "select",
366
- options: [...parsed.labels],
367
- context: parsed.header ? `${parsed.header}: your agent is asking in ${projectName}` : `Your agent is asking in ${projectName}`,
368
- agentName: `Claude Code - ${projectName}`,
369
- sessionId: input.session_id,
370
- machineId: getMachineId(),
371
- toolName: "AskUserQuestion"
372
- });
373
- } catch {
374
- return void 0;
375
- }
376
- if (result.suppressed || result.noDevices) {
377
- await cancelQuestion(apiKey, result.correlationId).catch(() => {
378
- });
379
- return void 0;
380
- }
381
376
  const deadline = hookWaitDeadline(START_MS, effectiveWaitSeconds("wait", 0, "claude"), "claude", Date.now());
382
- const answer = await pollForAnswer(apiKey, result.correlationId, deadline);
383
- const value = answer.answered ? (answer.value ?? "").trim() : "";
384
- if (!value || isDeferAnswer(answer.value)) {
385
- savePendingQuestion(input.session_id || DEFAULT_SESSION, result.correlationId);
386
- return void 0;
377
+ const answers = {};
378
+ for (const [index, question] of parsed.entries()) {
379
+ let result;
380
+ try {
381
+ result = await askUser(apiKey, {
382
+ question: question.question,
383
+ type: "select",
384
+ options: [...question.labels],
385
+ context: questionContext(question, index, parsed.length, projectName),
386
+ agentName: `Claude Code - ${projectName}`,
387
+ sessionId: input.session_id,
388
+ machineId: getMachineId(),
389
+ toolName: "AskUserQuestion"
390
+ });
391
+ } catch {
392
+ return void 0;
393
+ }
394
+ if (result.suppressed || result.noDevices) {
395
+ await cancelQuestion(apiKey, result.correlationId).catch(() => {
396
+ });
397
+ return void 0;
398
+ }
399
+ const answer = await pollForAnswer(apiKey, result.correlationId, deadline);
400
+ const value = answer.answered ? (answer.value ?? "").trim() : "";
401
+ if (!value || isDeferAnswer(answer.value)) {
402
+ savePendingQuestion(input.session_id || DEFAULT_SESSION, result.correlationId);
403
+ return void 0;
404
+ }
405
+ answers[question.question] = value;
387
406
  }
388
- return allowWithInput({ ...input.tool_input ?? {}, answers: { [parsed.question]: value } });
407
+ return allowWithInput({ ...input.tool_input ?? {}, answers });
389
408
  };
390
409
  var handleKeyless = (input) => {
391
410
  try {
package/dist/src/index.js CHANGED
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  handlePreToolUse
3
- } from "../chunk-MY4VJPWW.js";
3
+ } from "../chunk-NN4GTN6Q.js";
4
4
  import "../chunk-3CBPY2G5.js";
5
5
  import "../chunk-FWWBEB6L.js";
6
6
  import {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pushary/agent-hooks",
3
- "version": "0.77.0",
3
+ "version": "0.78.0",
4
4
  "description": "Permission hooks for AI coding agents: route tool approvals through Pushary push notifications",
5
5
  "keywords": [
6
6
  "pushary",