@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
|
package/dist/bin/pushary-hook.js
CHANGED
|
@@ -345,47 +345,66 @@ var shouldDeferToNativeMode = (permissionMode, toolName) => {
|
|
|
345
345
|
}
|
|
346
346
|
return false;
|
|
347
347
|
};
|
|
348
|
-
var
|
|
349
|
-
|
|
350
|
-
|
|
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 =
|
|
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
|
|
383
|
-
const
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
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
|
|
407
|
+
return allowWithInput({ ...input.tool_input ?? {}, answers });
|
|
389
408
|
};
|
|
390
409
|
var handleKeyless = (input) => {
|
|
391
410
|
try {
|
package/dist/src/index.js
CHANGED