pendingyou 0.15.0 → 0.17.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 +75 -0
- package/README.md +36 -1
- package/dist/apps/claude-code.js +3 -1
- package/dist/apps/codex-wake.js +90 -6
- package/dist/args.js +118 -1
- package/dist/claude.js +12 -2
- package/dist/format.js +36 -1
- package/dist/herdr/command.js +301 -0
- package/dist/herdr/notice.js +67 -0
- package/dist/herdr/panes.js +121 -0
- package/dist/herdr/plugin.js +119 -0
- package/dist/herdr/popup.js +271 -0
- package/dist/herdr/socket.js +123 -0
- package/dist/herdr/watch.js +179 -0
- package/dist/herdr.js +229 -0
- package/dist/io.js +61 -0
- package/dist/main.js +5 -1
- package/dist/permission.js +171 -50
- package/dist/pickup.js +7 -0
- package/dist/presence.js +36 -9
- package/dist/version.js +1 -1
- package/mod/.claude-plugin/plugin.json +1 -1
- package/mod/README.md +1 -1
- package/mod/hooks/register.js +38 -2
- package/mod/hooks/wake.js +68 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,80 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.17.0 (2026-10-05)
|
|
4
|
+
|
|
5
|
+
- Herdr: an agent running in a Herdr pane (`HERDR_ENV=1` with `HERDR_BIN_PATH` and `HERDR_PANE_ID`) marks its pane with
|
|
6
|
+
what it's waiting on you for, as display-only metadata (`src/herdr.ts`): `"$HERDR_BIN_PATH" pane report-metadata
|
|
7
|
+
"$HERDR_PANE_ID" --source pendingyou` with `py_app`, `py_agent` (the name the session goes by with Pending You),
|
|
8
|
+
`py_waiting` (how many cards wait on you), `py_card` (the most pressing one's title, redacted as permission cards' words
|
|
9
|
+
are and cut to 80 characters), `py_cards` (their request ids, as many as fit in 80 characters), `py_urgency`
|
|
10
|
+
(`blocking`, else `now`, `today` or `whenever`) and `py_asked` (those also asked in the conversation), and the state
|
|
11
|
+
labels `idle` and `done` read "waiting on you" while something waits (`--agent` keeps them to the pane's own agent).
|
|
12
|
+
Cleared when nothing waits and as the session ends. Herdr's program is run with an argument list, given one second,
|
|
13
|
+
its outcome never thrown, and a hook only starts it in the background; `--seq` (the moment, in milliseconds) keeps a
|
|
14
|
+
late write from winning, and `--ttl-ms 900000` lets tokens nobody repeats expire. Herdr's limits are kept: one source,
|
|
15
|
+
7 tokens a report, its token names, 80 characters a value.
|
|
16
|
+
- Claude Code: init's copy of the wake mod writes them whenever they change and every 5 minutes, through the hooks'
|
|
17
|
+
script (`herdr report --app claude-code`, the session's cards on stdin), learning how pressing each card is from
|
|
18
|
+
post_request and update_request; the session-start hook names the session in the background (from its transcript);
|
|
19
|
+
the SessionEnd hook clears them in the background.
|
|
20
|
+
- Codex: its card hook (`posted`) marks the thread as the pane's and writes them in the background; the thread's
|
|
21
|
+
keeper writes them with each live (it now starts in Herdr even without a connection here, and keeps on for them when
|
|
22
|
+
Pending You won't take its presence) and clears them as Codex quits; the listener writes them once a card is
|
|
23
|
+
answered.
|
|
24
|
+
- OpenCode and Pi: through what their plugin and extension already run: `posted` after each card call, `presence
|
|
25
|
+
--state live` (the heartbeat) and `closed`, and the listener. A pane's badges are every session of the app marked
|
|
26
|
+
there: OpenCode runs several in one pane.
|
|
27
|
+
- `<app>-threads.json` remembers each card's urgency, whether it blocks and whether it was asked in the conversation,
|
|
28
|
+
and the Herdr pane each session runs in; `herdr.json` whether the badges are off here.
|
|
29
|
+
- `pendingyou herdr <what>`: the Pending You plugin for Herdr's commands (`src/herdr/`), which its manifest runs through
|
|
30
|
+
the hooks' script. `next` focuses the next pane waiting on you (blocking first, then by urgency); `open` is the popup
|
|
31
|
+
that lists them (Enter opens the card in Pending You through a terminal hyperlink, `t` takes you to the pane; `--demo`
|
|
32
|
+
shows made-up cards); `popup <pane>` opens one of the plugin's popups; `setup` checks Herdr's toasts, offers the
|
|
33
|
+
sidebar row (`$py_card`) as a managed block in Herdr's `config.toml`, written only with a yes and checked with `herdr
|
|
34
|
+
config check`, and prints the keybindings to add yourself; `doctor` says what's set up (also by hand in a terminal);
|
|
35
|
+
`view` turns the plugin's Agents view on or off; `watch` is the watcher (one per Herdr server, held by a lease) that
|
|
36
|
+
raises Herdr's toast when a pane gains a card, never for the tab you're looking at, cards that arrive together in one,
|
|
37
|
+
a card also asked in the conversation after 3 minutes; `event` makes sure it runs; `unconfigure` stops it, takes the
|
|
38
|
+
badges off every pane and turns them off here, clears the view, and takes the block out. Read-only: nothing asks
|
|
39
|
+
Pending You anything. Herdr's socket API through `HERDR_SOCKET_PATH`.
|
|
40
|
+
- Tests never reach a real Herdr: the test setup drops every `HERDR_*` variable from their environment.
|
|
41
|
+
|
|
42
|
+
## 0.16.0 (2026-10-05)
|
|
43
|
+
|
|
44
|
+
- A word on one of your own cards that's neither an answer nor your person's (guide 2.32): `/mcp/cli/answers` marks it
|
|
45
|
+
`notice` (`{ kind, from }`), and the hold, the pickup, the hooks and `watch` say what it is instead of “they wrote
|
|
46
|
+
back”:
|
|
47
|
+
- `follow_up`: the helper that answered it for your person followed up afterwards (“shipped in #231”): “Pending You:
|
|
48
|
+
pending-you-chief followed up on req_…, which it answered for your person.”, the question, its words, then “Act on
|
|
49
|
+
it if it needs anything, then call ack_answer … to mark it heard.” On an answer you haven't picked up yet, its
|
|
50
|
+
words come with the answer (“Also: …”).
|
|
51
|
+
- `handed_over`: your person handed your open cards to another assistant: “Pending You: your person handed your open
|
|
52
|
+
cards to pending-you-chief, req_… among them.” and “Stop working on them: pending-you-chief answers them and
|
|
53
|
+
follows up. Call ack_answer … to say you’ve stopped.”
|
|
54
|
+
`watch` gives both as `message_ready`. A command line before 0.16.0 hears the same words as a message from them.
|
|
55
|
+
- Claude Code's permission prompts get a card in bypassPermissions mode too: a dangerous request from a background
|
|
56
|
+
agent there waited minutes for an OK with no card. Claude Code still asks for a few things in that mode (a dangerous
|
|
57
|
+
`rm`, an ask rule, a safety check it can't verify), but the PermissionRequest hook skipped bypassPermissions and
|
|
58
|
+
dontAsk mode outright. It also runs for a call Claude Code then denies by itself, showing
|
|
59
|
+
nothing, so in those two modes it now writes the prompt down quietly, and its card goes up only once a dialog is on
|
|
60
|
+
screen. A new Notification hook says so: `pendingyou-hook notify --app claude-code`, with the matcher
|
|
61
|
+
`permission_prompt`, so Claude Code runs it only for a dialog that has waited about six seconds with nobody typing,
|
|
62
|
+
and never for its other notifications. A call denied without a dialog never gets a card: its PostToolUseFailure, the
|
|
63
|
+
same agent's next call, your next message or the end of the turn settles it. In the other modes a card still goes up
|
|
64
|
+
after 10 seconds, or as soon as the Notification comes.
|
|
65
|
+
- A dialog no PermissionRequest hook ran for (a sandboxed command's network request; one a Claude Code ran none for)
|
|
66
|
+
gets a card from its Notification alone: “Claude Code is waiting for your OK”, with Claude Code's own words, redacted,
|
|
67
|
+
in its summary (“It says: “Claude needs your permission”.”). Whose dialog it was isn't known, so only a turn that ends
|
|
68
|
+
with no subagent running, or the session's end, takes it down.
|
|
69
|
+
- A background agent's prompt says so: “Claude Code is waiting for your OK: pnpm lint && git push (a background
|
|
70
|
+
agent)”, and its summary names the agent's type (“A background agent (general-purpose) of Claude Code
|
|
71
|
+
(billing-webhooks), in ~/… on work-mbp, is waiting for your OK to run a command.”). It stays on the session's one card,
|
|
72
|
+
and only that agent's own calls settle it, not the session's or another agent's.
|
|
73
|
+
- `init` adds the Notification hook at the end of Claude Code's Notification hooks, after any of your own, and says so
|
|
74
|
+
on a computer set up before (“Added a Notification hook to the permission-prompt hooks: …”); `status` marks the
|
|
75
|
+
Permission prompts line missing without it; `uninstall` and `--no-permission-cards` take it out with the others.
|
|
76
|
+
Codex is unchanged: it has no hook that says a dialog is on screen (its `notify` runs only when a turn completes).
|
|
77
|
+
|
|
3
78
|
## 0.15.0 (2026-10-05)
|
|
4
79
|
|
|
5
80
|
- Presence: Pending You hears which of your agents' sessions are open right now. Each app tells it with its own
|
package/README.md
CHANGED
|
@@ -246,6 +246,14 @@ a key are blanked (`curl -H "Authorization: …" https://api.example.com/items?t
|
|
|
246
246
|
shown safely (several lines, inline code like `node -e`, one that takes a secret) shows only as “Bash: node …”. No card
|
|
247
247
|
for `claude -p` or the Agent SDK, where nobody sees a prompt.
|
|
248
248
|
|
|
249
|
+
In bypassPermissions mode (`--dangerously-skip-permissions`) Claude Code still asks for a few things: a dangerous `rm`,
|
|
250
|
+
an ask rule of yours, a safety check it can't verify. Since 0.16.0 those get a card too, once Claude Code says the
|
|
251
|
+
dialog has waited a few seconds with nobody typing (its Notification hook, which `init` adds for permission dialogs
|
|
252
|
+
only): a call it denies by itself, showing you nothing, never gets one. The same hook puts up a card for a dialog no
|
|
253
|
+
other hook sees (a sandboxed command asking for the network), in Claude Code's own words, and in the other modes puts
|
|
254
|
+
a card up as soon as Claude Code says so, without waiting the full 10 seconds. A background agent's prompt says so:
|
|
255
|
+
“Claude Code is waiting for your OK: pnpm lint && git push (a background agent)”.
|
|
256
|
+
|
|
249
257
|
It needs Claude Code 2.1.119 or later, signing in through this computer's sign-in (not `--oauth`), and isn't on
|
|
250
258
|
Windows yet. Don't want it? `npx pendingyou init --no-permission-cards` (later runs keep it off; `--permission-cards`
|
|
251
259
|
turns it back on). `npx pendingyou status` says which.
|
|
@@ -276,6 +284,29 @@ It's never in your way: the hooks only start it in the background, each send giv
|
|
|
276
284
|
You from before presence is taken quietly. A sign-in that only hears (`--oauth`) sends nothing. `npx pendingyou status`
|
|
277
285
|
has a Presence line for each agent.
|
|
278
286
|
|
|
287
|
+
## In Herdr
|
|
288
|
+
|
|
289
|
+
Since 0.17.0, an agent running in a [Herdr](https://herdr.dev) pane marks its pane with what it's waiting on you for,
|
|
290
|
+
so Herdr's sidebar and Agents panel can show it: display-only metadata Herdr keeps on this computer (`herdr agent list`
|
|
291
|
+
shows it), written with Herdr's own command line:
|
|
292
|
+
|
|
293
|
+
- `py_app` and `py_agent`: the app and the name the session goes by with Pending You;
|
|
294
|
+
- `py_waiting`: how many of its cards wait on you; `py_card` the most pressing one's title (anything that looks like a
|
|
295
|
+
secret taken out, at most 80 characters); `py_cards` their request ids; `py_urgency` how pressing (`blocking`, `now`,
|
|
296
|
+
`today`, `whenever`); `py_asked` the ones it also asked you in its conversation;
|
|
297
|
+
- and its state reads "waiting on you" instead of idle or done while something waits.
|
|
298
|
+
|
|
299
|
+
Claude Code's wake mod writes them whenever they change, Codex's card hook and keeper, OpenCode's plugin and Pi's
|
|
300
|
+
extension through the commands they already run; every 5 minutes again, and they're taken off when nothing waits and as
|
|
301
|
+
the session ends. Herdr is given one second for each, never through a shell, and never waited on by a hook; it forgets
|
|
302
|
+
them after 15 minutes unless told again.
|
|
303
|
+
|
|
304
|
+
The Pending You plugin for Herdr puts them to use: the card on each waiting agent's row in the sidebar, a toast when a
|
|
305
|
+
pane gains one, a key to the next pane waiting on you, a popup that lists them (Enter opens the card in Pending You), and
|
|
306
|
+
an Agents view with them first. Its commands are this command line's: `npx pendingyou herdr doctor` says what's set
|
|
307
|
+
up. Its `unconfigure` takes the badges off every pane and stops writing them on this computer; its setup turns them on
|
|
308
|
+
again.
|
|
309
|
+
|
|
279
310
|
## How Claude Code hears your answer
|
|
280
311
|
|
|
281
312
|
**Claude Code 2.1.287 or later: the wake mod.** There's nothing to run. In each session the mod learns the cards
|
|
@@ -399,6 +430,7 @@ its environment. A failing command is tried twice more, then skipped. `--once` s
|
|
|
399
430
|
| `stopcheck` | The Stop hook: items for you left only in chat (`--app`) |
|
|
400
431
|
| `uninstall` | Remove what `init` added, and sign out (`--app`) |
|
|
401
432
|
| `mcp --app <app>` | The stdio bridge: a local MCP server for an agent that can't run a headers helper (OpenCode, Pi), which passes each message on to Pending You with that agent's own sign-in here (`--check`) |
|
|
433
|
+
| `herdr doctor` | Pending You in Herdr: the badges, the plugin, its toasts and its sidebar row. The plugin runs `herdr next`, `open`, `setup`, `view` and `unconfigure`; the agents' writers `herdr report` |
|
|
402
434
|
|
|
403
435
|
Every command takes `--origin` (default `https://www.pendingyou.com`, or `$PENDINGYOU_ORIGIN`). `--app` takes
|
|
404
436
|
`claude-code`, `codex`, `opencode` or `pi` (several with commas where a command sets up or reports several); with none,
|
|
@@ -426,7 +458,10 @@ Pi session waits on, and (0.14.0) the names it went by and its folder, kept 12 h
|
|
|
426
458
|
(ids, names, folders and titles: never an answer or a note); `pi/` Pi's extension; and `cli/` the hooks' copy of the
|
|
427
459
|
command line. `init`, `status` and the hooks read `~/.claude.json` (and a folder's `.mcp.json`, and
|
|
428
460
|
`~/.claude/settings.json`'s enabled plugins), `~/.codex/config.toml`, OpenCode's config and Pi's `mcp.json` only to see
|
|
429
|
-
whether the agent has a `pendingyou` MCP server and where it points.
|
|
461
|
+
whether the agent has a `pendingyou` MCP server and where it points. In a Herdr pane (0.17.0) the agents' writers hand
|
|
462
|
+
Herdr's own command line, on this computer, the badges above: a card's title (redacted, 80 characters at most) and its
|
|
463
|
+
request id, never an answer or a note; `<app>-threads.json` remembers the pane each session runs in, and `herdr.json`
|
|
464
|
+
whether the badges are off. With a sign-in that only hears, the hooks and
|
|
430
465
|
listeners tell this computer's handed questions from another's by its names: its hostname, the names its sign-ins by
|
|
431
466
|
code were given, a Mac's own name (`scutil --get ComputerName`) and, outside a hook, Tailscale's (`tailscale status`). For a permission prompt's card, the worker reads
|
|
432
467
|
the end of the Claude Code session's transcript only for the name the session gave Pending You, and `git remote
|
package/dist/apps/claude-code.js
CHANGED
|
@@ -199,8 +199,10 @@ export const claudeCode = {
|
|
|
199
199
|
};
|
|
200
200
|
function permissionLine(io, claude, manifest, helper, flag) {
|
|
201
201
|
const label = 'Permission prompts:';
|
|
202
|
-
if (claude.permission)
|
|
202
|
+
if (claude.permission && claude.notify)
|
|
203
203
|
return ` ok ${label} a card when Claude Code waits for your OK`;
|
|
204
|
+
if (claude.permission)
|
|
205
|
+
return ` missing ${label} no card for a dialog in bypassPermissions mode, or one the other hooks don’t see; run npx pendingyou@latest init${flag}`;
|
|
204
206
|
if (manifest?.permissionCards === false)
|
|
205
207
|
return ` ${label} off, as you chose; for a card when Claude Code waits for your OK, run npx pendingyou init --permission-cards${flag}`;
|
|
206
208
|
const blocker = claude.claude
|
package/dist/apps/codex-wake.js
CHANGED
|
@@ -5,8 +5,9 @@ import { getJson, SignInNeeded, Unavailable } from "../api.js";
|
|
|
5
5
|
import { DEFAULT_ORIGIN } from "../args.js";
|
|
6
6
|
import { connectionMachine, readCredential } from "../credentials.js";
|
|
7
7
|
import { configDir, readJson, withLock, writeWhole } from "../files.js";
|
|
8
|
-
import { clip, delegatedOf, isClosed } from "../format.js";
|
|
8
|
+
import { clip, delegatedOf, isClosed, noticeOf } from "../format.js";
|
|
9
9
|
import { chooseThread, goesBy, HANDED_FOR_MS, handedCard, handedMessage, namesAnotherComputer, } from "../handed.js";
|
|
10
|
+
import { clearHerdr, herdrTarget, isUrgency, PANE_ID, reportCommand, writeHerdr, } from "../herdr.js";
|
|
10
11
|
import { macName, tailscaleName } from "../remote.js";
|
|
11
12
|
import { claimHanded, markHanded, momentOf, releaseHanded, wasHanded } from "../state.js";
|
|
12
13
|
import { findCodex } from "./codex-app.js";
|
|
@@ -55,6 +56,7 @@ function threadOf(value) {
|
|
|
55
56
|
at: value.at,
|
|
56
57
|
...(names.length ? { names: names.slice(-NAMES) } : {}),
|
|
57
58
|
...(typeof value.cwd === 'string' && value.cwd ? { cwd: value.cwd } : {}),
|
|
59
|
+
...(typeof value.herdr === 'string' && PANE_ID.test(value.herdr) ? { herdr: value.herdr } : {}),
|
|
58
60
|
};
|
|
59
61
|
}
|
|
60
62
|
async function load(io, app) {
|
|
@@ -155,6 +157,7 @@ export async function posted(io, options) {
|
|
|
155
157
|
const requestId = [output?.requestId, args.requestId].find((id) => typeof id === 'string' && REQUEST_ID.test(id));
|
|
156
158
|
const cwd = text(input.cwd, 1000);
|
|
157
159
|
const now = io.now();
|
|
160
|
+
const pane = await herdrTarget(io);
|
|
158
161
|
await change(io, app, (state) => {
|
|
159
162
|
if (turn && call !== 'cancel_request') {
|
|
160
163
|
const record = state.turns[turn] ?? { at: now, cards: [] };
|
|
@@ -171,19 +174,29 @@ export async function posted(io, options) {
|
|
|
171
174
|
const before = waiting.cards[requestId];
|
|
172
175
|
const name = nameOf(args) ?? before?.name;
|
|
173
176
|
const title = text(args.title, 400) ?? text(reopen?.title, 400) ?? before?.title;
|
|
177
|
+
const urgency = isUrgency(args.urgency) ? args.urgency : before?.urgency;
|
|
178
|
+
const blocking = typeof args.blocking === 'boolean' ? args.blocking : before?.blocking;
|
|
179
|
+
const askedFirst = call === 'post_request' ? askedIn(args) : before?.askedFirst;
|
|
174
180
|
waiting.cards[requestId] = {
|
|
175
181
|
...(name ? { name } : {}),
|
|
176
182
|
...(title ? { title } : {}),
|
|
177
183
|
at: now,
|
|
184
|
+
...(urgency ? { urgency } : {}),
|
|
185
|
+
...(blocking ? { blocking } : {}),
|
|
186
|
+
...(askedFirst ? { askedFirst } : {}),
|
|
178
187
|
};
|
|
179
188
|
}
|
|
180
189
|
}
|
|
181
190
|
if (cwd)
|
|
182
191
|
waiting.cwd = cwd;
|
|
192
|
+
if (pane)
|
|
193
|
+
waiting.herdr = pane.pane;
|
|
183
194
|
waiting.origin = options.origin;
|
|
184
195
|
waiting.at = now;
|
|
185
196
|
state.threads[thread] = waiting;
|
|
186
197
|
});
|
|
198
|
+
if (pane)
|
|
199
|
+
io.background(reportCommand(app, { session: thread, at: now }));
|
|
187
200
|
if (app === 'codex')
|
|
188
201
|
await keepListening(io, options.origin, thread);
|
|
189
202
|
return 0;
|
|
@@ -192,6 +205,60 @@ export async function posted(io, options) {
|
|
|
192
205
|
return 0;
|
|
193
206
|
}
|
|
194
207
|
}
|
|
208
|
+
function askedIn(args) {
|
|
209
|
+
let asked = args.askedFirst;
|
|
210
|
+
if (typeof asked === 'string')
|
|
211
|
+
try {
|
|
212
|
+
asked = JSON.parse(asked);
|
|
213
|
+
}
|
|
214
|
+
catch {
|
|
215
|
+
return false;
|
|
216
|
+
}
|
|
217
|
+
return isObject(asked);
|
|
218
|
+
}
|
|
219
|
+
export async function reportThreads(io, app, options) {
|
|
220
|
+
try {
|
|
221
|
+
const pane = await herdrTarget(io);
|
|
222
|
+
if (!pane)
|
|
223
|
+
return 'off';
|
|
224
|
+
const session = options.session && THREAD.test(options.session) ? options.session : null;
|
|
225
|
+
let state = await load(io, app);
|
|
226
|
+
const known = session ? state.threads[session] : undefined;
|
|
227
|
+
if (session && known) {
|
|
228
|
+
const wanted = options.closed
|
|
229
|
+
? known.herdr === pane.pane
|
|
230
|
+
? undefined
|
|
231
|
+
: known.herdr
|
|
232
|
+
: pane.pane;
|
|
233
|
+
if (wanted !== known.herdr)
|
|
234
|
+
state = await change(io, app, (next) => {
|
|
235
|
+
const thread = next.threads[session];
|
|
236
|
+
if (!thread)
|
|
237
|
+
return;
|
|
238
|
+
if (wanted)
|
|
239
|
+
thread.herdr = wanted;
|
|
240
|
+
else
|
|
241
|
+
delete thread.herdr;
|
|
242
|
+
});
|
|
243
|
+
}
|
|
244
|
+
const here = Object.values(state.threads).filter((thread) => thread.herdr === pane.pane);
|
|
245
|
+
if (here.length === 0 && options.closed)
|
|
246
|
+
return await clearHerdr(io, options.at, pane);
|
|
247
|
+
const newest = [...here].sort((a, b) => b.at - a.at)[0];
|
|
248
|
+
const cards = here.flatMap((thread) => Object.entries(thread.cards).map(([requestId, card]) => ({
|
|
249
|
+
requestId,
|
|
250
|
+
...(typeof card.title === 'string' ? { title: card.title } : {}),
|
|
251
|
+
...(isUrgency(card.urgency) ? { urgency: card.urgency } : {}),
|
|
252
|
+
...(card.blocking === true ? { blocking: true } : {}),
|
|
253
|
+
...(card.askedFirst === true ? { askedFirst: true } : {}),
|
|
254
|
+
at: typeof card.at === 'number' ? card.at : 0,
|
|
255
|
+
})));
|
|
256
|
+
return await writeHerdr(io, { app, agent: newest?.names?.at(-1) ?? null, cards, at: options.at }, pane);
|
|
257
|
+
}
|
|
258
|
+
catch {
|
|
259
|
+
return 'failed';
|
|
260
|
+
}
|
|
261
|
+
}
|
|
195
262
|
export async function touchThread(io, app, thread, cwd) {
|
|
196
263
|
try {
|
|
197
264
|
if (!THREAD.test(thread) || !(await load(io, app)).threads[thread])
|
|
@@ -310,6 +377,18 @@ function line(heard, card) {
|
|
|
310
377
|
.replaceAll('”', '’')}” (${heard.requestId})`;
|
|
311
378
|
const named = card.name ?? heard.sessionLabel;
|
|
312
379
|
const get = `get_request (requestId ${heard.requestId}, ${named ? `name “${named}”` : 'your name'})`;
|
|
380
|
+
const notice = noticeOf(heard);
|
|
381
|
+
const from = notice ? clip(notice.from, 60).text : '';
|
|
382
|
+
if (notice?.kind === 'handed_over')
|
|
383
|
+
return [
|
|
384
|
+
`your person handed your open cards to ${from}, ${titled} among them`,
|
|
385
|
+
`stop working on them: ${from} answers them and follows up; ack_answer ${heard.requestId} with its version to say you’ve stopped`,
|
|
386
|
+
];
|
|
387
|
+
if (notice?.kind === 'follow_up' && heard.status !== 'answered')
|
|
388
|
+
return [
|
|
389
|
+
`${from} followed up on ${titled}, which it answered for your person`,
|
|
390
|
+
`call ${get} to read it, act on it if it needs anything, then ack_answer with its version to mark it heard`,
|
|
391
|
+
];
|
|
313
392
|
switch (heard.status) {
|
|
314
393
|
case 'expired':
|
|
315
394
|
return [
|
|
@@ -339,7 +418,7 @@ export function wakeMessage(ready) {
|
|
|
339
418
|
});
|
|
340
419
|
if (lines.length === 0)
|
|
341
420
|
return null;
|
|
342
|
-
const asked = lines.some(({ heard }) => heard.turn === 'agent' && heard.status !== 'expired');
|
|
421
|
+
const asked = lines.some(({ heard }) => heard.turn === 'agent' && heard.status !== 'expired' && !noticeOf(heard));
|
|
343
422
|
const reopen = asked
|
|
344
423
|
? ' If they asked you something or you need more from them, reply_in_thread with reopen instead.'
|
|
345
424
|
: '';
|
|
@@ -520,9 +599,12 @@ export async function listen(io, options) {
|
|
|
520
599
|
else
|
|
521
600
|
ready.push({ heard, card });
|
|
522
601
|
}
|
|
523
|
-
const gone =
|
|
524
|
-
|
|
525
|
-
|
|
602
|
+
const gone = async () => {
|
|
603
|
+
await change(io, app, (state) => {
|
|
604
|
+
delete state.threads[thread];
|
|
605
|
+
});
|
|
606
|
+
await reportThreads(io, app, { at: io.now() });
|
|
607
|
+
};
|
|
526
608
|
const message = wakeMessage(ready);
|
|
527
609
|
if (message) {
|
|
528
610
|
if (!(await deliver(message, ready.map(({ heard }) => heard.requestId)))) {
|
|
@@ -544,13 +626,15 @@ export async function listen(io, options) {
|
|
|
544
626
|
for (const heard of requests)
|
|
545
627
|
if (cards[heard.requestId] && isClosed(heard) && !done.includes(heard.requestId))
|
|
546
628
|
done.push(heard.requestId);
|
|
547
|
-
if (done.length)
|
|
629
|
+
if (done.length) {
|
|
548
630
|
await change(io, app, (state) => {
|
|
549
631
|
const waiting = state.threads[thread];
|
|
550
632
|
if (waiting)
|
|
551
633
|
for (const id of done)
|
|
552
634
|
delete waiting.cards[id];
|
|
553
635
|
});
|
|
636
|
+
await reportThreads(io, app, { at: io.now() });
|
|
637
|
+
}
|
|
554
638
|
if (io.now() - started > PATIENT_AFTER_MS)
|
|
555
639
|
await io.sleep(PATIENT_PAUSE_MS, signal);
|
|
556
640
|
}
|
package/dist/args.js
CHANGED
|
@@ -1,7 +1,20 @@
|
|
|
1
|
-
import { APP_IDS, APP_NAMES, isAppId } from "./apps/ids.js";
|
|
1
|
+
import { APP_IDS, APP_NAMES, DEFAULT_APP, isAppId } from "./apps/ids.js";
|
|
2
2
|
export const DEFAULT_ORIGIN = 'https://www.pendingyou.com';
|
|
3
3
|
export const DEFAULT_TIMEOUT_MS = 4 * 60 * 60 * 1000;
|
|
4
4
|
const MAX_TIMEOUT_MS = 24 * 60 * 60 * 1000;
|
|
5
|
+
export const HERDR_SUBS = [
|
|
6
|
+
'report',
|
|
7
|
+
'next',
|
|
8
|
+
'open',
|
|
9
|
+
'setup',
|
|
10
|
+
'doctor',
|
|
11
|
+
'unconfigure',
|
|
12
|
+
'watch',
|
|
13
|
+
'event',
|
|
14
|
+
'view',
|
|
15
|
+
'popup',
|
|
16
|
+
];
|
|
17
|
+
export const HERDR_POPUPS = ['cards', 'setup', 'doctor', 'demo'];
|
|
5
18
|
export class UsageError extends Error {
|
|
6
19
|
name = 'UsageError';
|
|
7
20
|
}
|
|
@@ -63,8 +76,10 @@ const COMMANDS = new Set([
|
|
|
63
76
|
'mcp',
|
|
64
77
|
'permission',
|
|
65
78
|
'permission-done',
|
|
79
|
+
'notify',
|
|
66
80
|
'permission-card',
|
|
67
81
|
'presence',
|
|
82
|
+
'herdr',
|
|
68
83
|
]);
|
|
69
84
|
const FLAGS = {
|
|
70
85
|
help: {},
|
|
@@ -88,6 +103,7 @@ const FLAGS = {
|
|
|
88
103
|
listen: { origin: true, app: true, thread: true, handed: false },
|
|
89
104
|
permission: { origin: true, app: true },
|
|
90
105
|
'permission-done': { origin: true, app: true },
|
|
106
|
+
notify: { origin: true, app: true },
|
|
91
107
|
'permission-card': { origin: true, session: true, worker: true },
|
|
92
108
|
presence: {
|
|
93
109
|
origin: true,
|
|
@@ -119,6 +135,19 @@ const FLAGS = {
|
|
|
119
135
|
refresh: { origin: true, force: false, app: true },
|
|
120
136
|
'mcp-headers': { origin: true, check: false, app: true },
|
|
121
137
|
mcp: { origin: true, app: true, check: false },
|
|
138
|
+
herdr: {
|
|
139
|
+
origin: true,
|
|
140
|
+
app: true,
|
|
141
|
+
session: true,
|
|
142
|
+
transcript: true,
|
|
143
|
+
name: true,
|
|
144
|
+
state: true,
|
|
145
|
+
at: true,
|
|
146
|
+
demo: false,
|
|
147
|
+
yes: false,
|
|
148
|
+
wait: false,
|
|
149
|
+
ensure: false,
|
|
150
|
+
},
|
|
122
151
|
};
|
|
123
152
|
const VALUE_OPTIONS = new Set([
|
|
124
153
|
'origin',
|
|
@@ -154,6 +183,7 @@ export const HOOK_COMMANDS = new Set([
|
|
|
154
183
|
'posted',
|
|
155
184
|
'permission',
|
|
156
185
|
'permission-done',
|
|
186
|
+
'notify',
|
|
157
187
|
'presence',
|
|
158
188
|
]);
|
|
159
189
|
const THREAD = /^[A-Za-z0-9][A-Za-z0-9_-]{0,99}$/;
|
|
@@ -315,6 +345,7 @@ export function parseArgs(argv, env = {}) {
|
|
|
315
345
|
}
|
|
316
346
|
case 'permission':
|
|
317
347
|
case 'permission-done':
|
|
348
|
+
case 'notify':
|
|
318
349
|
none();
|
|
319
350
|
return { name: first, origin, ...app() };
|
|
320
351
|
case 'permission-card': {
|
|
@@ -377,6 +408,8 @@ export function parseArgs(argv, env = {}) {
|
|
|
377
408
|
case 'mcp':
|
|
378
409
|
none();
|
|
379
410
|
return { name: 'mcp', origin, ...app(), check: flags.has('check') };
|
|
411
|
+
case 'herdr':
|
|
412
|
+
return herdrCommand(origin, positional, flags, app);
|
|
380
413
|
case 'init':
|
|
381
414
|
none();
|
|
382
415
|
return {
|
|
@@ -411,6 +444,87 @@ export function parseArgs(argv, env = {}) {
|
|
|
411
444
|
}
|
|
412
445
|
throw new UsageError(`There’s no “${first}” command.`);
|
|
413
446
|
}
|
|
447
|
+
function herdrCommand(origin, positional, flags, app) {
|
|
448
|
+
const [sub, target, ...extra] = positional;
|
|
449
|
+
if (sub === undefined)
|
|
450
|
+
throw new UsageError('herdr needs what to do, like: pendingyou herdr doctor');
|
|
451
|
+
if (!HERDR_SUBS.includes(sub))
|
|
452
|
+
throw new UsageError(`herdr has no “${sub}”. Use ${HERDR_SUBS.join(', ')}.`);
|
|
453
|
+
const allowed = {
|
|
454
|
+
report: ['app', 'session', 'transcript', 'name', 'state', 'at'],
|
|
455
|
+
next: [],
|
|
456
|
+
open: ['demo'],
|
|
457
|
+
setup: ['yes'],
|
|
458
|
+
doctor: ['wait'],
|
|
459
|
+
unconfigure: [],
|
|
460
|
+
watch: ['ensure'],
|
|
461
|
+
event: [],
|
|
462
|
+
view: [],
|
|
463
|
+
popup: [],
|
|
464
|
+
};
|
|
465
|
+
for (const key of flags.keys())
|
|
466
|
+
if (key !== 'origin' && !allowed[sub].includes(key))
|
|
467
|
+
throw new UsageError(`herdr ${sub} has no --${key} option.`);
|
|
468
|
+
const command = { name: 'herdr', origin, sub: sub };
|
|
469
|
+
const takes = (choices) => {
|
|
470
|
+
if (extra.length || (target !== undefined && !choices?.includes(target)))
|
|
471
|
+
throw new UsageError(choices
|
|
472
|
+
? `herdr ${sub} takes ${choices.join(' or ')}.`
|
|
473
|
+
: `herdr ${sub} takes no “${target ?? extra[0]}”.`);
|
|
474
|
+
};
|
|
475
|
+
const text = (key) => {
|
|
476
|
+
const value = flags.get(key);
|
|
477
|
+
return typeof value === 'string' ? value : undefined;
|
|
478
|
+
};
|
|
479
|
+
switch (command.sub) {
|
|
480
|
+
case 'report': {
|
|
481
|
+
takes(null);
|
|
482
|
+
const session = text('session');
|
|
483
|
+
if (session !== undefined && !THREAD.test(session))
|
|
484
|
+
throw new UsageError('herdr report needs a session id, like: --session 0199a1b2-…');
|
|
485
|
+
const state = text('state');
|
|
486
|
+
if (state !== undefined && state !== 'live' && state !== 'closed')
|
|
487
|
+
throw new UsageError('--state is live or closed.');
|
|
488
|
+
const at = text('at');
|
|
489
|
+
if (at !== undefined && !/^\d{1,16}$/.test(at))
|
|
490
|
+
throw new UsageError('--at needs a number.');
|
|
491
|
+
const transcript = text('transcript');
|
|
492
|
+
const agentName = text('name');
|
|
493
|
+
return {
|
|
494
|
+
...command,
|
|
495
|
+
app: app().app ?? DEFAULT_APP,
|
|
496
|
+
...(session ? { session } : {}),
|
|
497
|
+
...(transcript ? { transcript } : {}),
|
|
498
|
+
...(agentName ? { agentName } : {}),
|
|
499
|
+
closed: state === 'closed',
|
|
500
|
+
...(at ? { at: Number(at) } : {}),
|
|
501
|
+
};
|
|
502
|
+
}
|
|
503
|
+
case 'popup':
|
|
504
|
+
if (target === undefined)
|
|
505
|
+
throw new UsageError(`herdr popup needs a pane: ${HERDR_POPUPS.join(', ')}.`);
|
|
506
|
+
takes(HERDR_POPUPS);
|
|
507
|
+
return { ...command, target };
|
|
508
|
+
case 'view':
|
|
509
|
+
takes(['on', 'off']);
|
|
510
|
+
return target ? { ...command, target } : command;
|
|
511
|
+
case 'open':
|
|
512
|
+
takes(null);
|
|
513
|
+
return { ...command, demo: flags.has('demo') };
|
|
514
|
+
case 'setup':
|
|
515
|
+
takes(null);
|
|
516
|
+
return { ...command, yes: flags.has('yes') };
|
|
517
|
+
case 'doctor':
|
|
518
|
+
takes(null);
|
|
519
|
+
return { ...command, wait: flags.has('wait') };
|
|
520
|
+
case 'watch':
|
|
521
|
+
takes(null);
|
|
522
|
+
return { ...command, ensure: flags.has('ensure') };
|
|
523
|
+
default:
|
|
524
|
+
takes(null);
|
|
525
|
+
return command;
|
|
526
|
+
}
|
|
527
|
+
}
|
|
414
528
|
export const USAGE = `pendingyou: Pending You for your coding agents (Claude Code, Codex, OpenCode and Pi)
|
|
415
529
|
|
|
416
530
|
npx -y pendingyou@latest init
|
|
@@ -436,6 +550,9 @@ export const USAGE = `pendingyou: Pending You for your coding agents (Claude Cod
|
|
|
436
550
|
npx pendingyou mcp --app <app>
|
|
437
551
|
A stdio MCP server for an agent that can't run a headers helper: it
|
|
438
552
|
passes each message on to Pending You with that agent's sign-in here
|
|
553
|
+
npx pendingyou herdr doctor Pending You in Herdr: the badges on your agents' panes, the plugin, its
|
|
554
|
+
toasts and its sidebar row. The plugin runs herdr next, open, setup,
|
|
555
|
+
view and unconfigure
|
|
439
556
|
|
|
440
557
|
Apps (for --app): ${APP_IDS.map((app) => `${app} (${APP_NAMES[app]})`).join(', ')}.
|
|
441
558
|
|
package/dist/claude.js
CHANGED
|
@@ -26,6 +26,7 @@ export const PERMISSION_HOOKS = [
|
|
|
26
26
|
{ event: 'PostToolUse', sub: 'permission-done' },
|
|
27
27
|
{ event: 'PostToolUseFailure', sub: 'permission-done' },
|
|
28
28
|
{ event: 'SessionEnd', sub: 'permission-done', timeout: null },
|
|
29
|
+
{ event: 'Notification', sub: 'notify', matcher: 'permission_prompt' },
|
|
29
30
|
];
|
|
30
31
|
export const PERMISSION_CLAUDE = '2.1.119';
|
|
31
32
|
export const PERMISSIONS = ['Bash(npx pendingyou hold:*)', `mcp__${MCP_NAME}`];
|
|
@@ -337,7 +338,11 @@ export async function installClaude(io, origin, options) {
|
|
|
337
338
|
const permissionLines = wanted && !blocker
|
|
338
339
|
? Object.fromEntries(PERMISSION_HOOKS.map((spec) => [spec.event, options.hookLine(spec.sub)]))
|
|
339
340
|
: null;
|
|
340
|
-
const
|
|
341
|
+
const settings = await readSettings(io);
|
|
342
|
+
const had = hooksOf(settings, 'Claude Code’s settings');
|
|
343
|
+
const upgrade = Boolean(findHook(had, 'PermissionRequest', 'permission')) &&
|
|
344
|
+
!findHook(had, 'Notification', 'notify');
|
|
345
|
+
const merged = mergeSettings(settings, commands, permissionLines);
|
|
341
346
|
report({
|
|
342
347
|
ok: true,
|
|
343
348
|
text: merged.hooksChanged
|
|
@@ -349,7 +354,9 @@ export async function installClaude(io, origin, options) {
|
|
|
349
354
|
ok: true,
|
|
350
355
|
text: permissionLines
|
|
351
356
|
? merged.permissionChanged
|
|
352
|
-
?
|
|
357
|
+
? upgrade
|
|
358
|
+
? 'Added a Notification hook to the permission-prompt hooks: a dialog in bypassPermissions mode gets a card too, and so does one the other hooks don’t see, once Claude Code says it has waited a few seconds.'
|
|
359
|
+
: 'Added the permission-prompt hooks: when Claude Code waits for your OK and you don’t answer within a few seconds, a card says so (a push only after your hand-off wait), and it goes once you answer in Claude Code.'
|
|
353
360
|
: 'The permission-prompt hooks were already installed.'
|
|
354
361
|
: !wanted
|
|
355
362
|
? `${merged.permissionChanged ? 'Took out the permission-prompt hooks' : 'Left out the permission-prompt hooks'}, as you asked: no card when Claude Code waits for your OK. To turn them on: npx pendingyou init --permission-cards${flag}`
|
|
@@ -470,6 +477,7 @@ export async function claudeState(io) {
|
|
|
470
477
|
const skill = (await readText(skillPath(io))) !== null;
|
|
471
478
|
let ours = (_event) => undefined;
|
|
472
479
|
let permission = false;
|
|
480
|
+
let notify = false;
|
|
473
481
|
const mod = { loads: false, dirs: [], present: false, plugin: false };
|
|
474
482
|
const loads = claude ? claudeVersion(claude) : null;
|
|
475
483
|
mod.loads = loads !== null && loadsMods(loads);
|
|
@@ -482,6 +490,7 @@ export async function claudeState(io) {
|
|
|
482
490
|
const all = objectAt(settings, 'hooks') ?? {};
|
|
483
491
|
ours = (event) => findHook(all, event, HOOKS[event]);
|
|
484
492
|
permission = Boolean(findHook(all, 'PermissionRequest', 'permission'));
|
|
493
|
+
notify = Boolean(findHook(all, 'Notification', 'notify'));
|
|
485
494
|
}
|
|
486
495
|
catch { }
|
|
487
496
|
const start = ours('SessionStart');
|
|
@@ -496,6 +505,7 @@ export async function claudeState(io) {
|
|
|
496
505
|
hooks: Boolean(start && ours('UserPromptSubmit')),
|
|
497
506
|
stopcheck: Boolean(ours('Stop')),
|
|
498
507
|
permission,
|
|
508
|
+
notify,
|
|
499
509
|
presence: Boolean(ours('SessionEnd')),
|
|
500
510
|
form: form ? { ...form, runs: await hookRuns(form), shim } : null,
|
|
501
511
|
mod,
|
package/dist/format.js
CHANGED
|
@@ -1,3 +1,12 @@
|
|
|
1
|
+
export function noticeOf(heard) {
|
|
2
|
+
const given = heard.notice;
|
|
3
|
+
if (typeof given !== 'object' || given === null)
|
|
4
|
+
return null;
|
|
5
|
+
const { kind, from } = given;
|
|
6
|
+
if ((kind !== 'follow_up' && kind !== 'handed_over') || typeof from !== 'string')
|
|
7
|
+
return null;
|
|
8
|
+
return { kind, from };
|
|
9
|
+
}
|
|
1
10
|
export const HANDED_PREFIX = 'Pending You: your person handed you';
|
|
2
11
|
export const HANDED_REPLY = 'on the question handed to you';
|
|
3
12
|
export const HANDED_TALK_TODO = 'If you know now, answer_delegated with your answer and how you know; if you need more, reply_in_thread to “asker”; if it’s your person’s to decide, reply_in_thread with escalate true; if not, hand_back with what you checked.';
|
|
@@ -81,11 +90,37 @@ function describeHanded(heard, delegated) {
|
|
|
81
90
|
...(note?.cut ? [`(Cut short: get_request ${id} has all of it.)`] : []),
|
|
82
91
|
];
|
|
83
92
|
}
|
|
93
|
+
function describeNotice(heard, notice) {
|
|
94
|
+
const id = heard.requestId;
|
|
95
|
+
const from = clip(notice.from, MAX_NAME).text;
|
|
96
|
+
const ack = `ack_answer with requestId ${id}, name ${named(heard)} and expectedVersion ${heard.version}`;
|
|
97
|
+
if (notice.kind === 'handed_over')
|
|
98
|
+
return [
|
|
99
|
+
`Pending You: your person handed your open cards to ${from}, ${id} among them.`,
|
|
100
|
+
`Stop working on them: ${from} answers them and follows up. Call ${ack} to say you’ve stopped.`,
|
|
101
|
+
];
|
|
102
|
+
let cut = false;
|
|
103
|
+
const wrote = heard.messages.map((message) => {
|
|
104
|
+
const said = quoted(message.body, MAX_MESSAGE);
|
|
105
|
+
cut ||= said.cut;
|
|
106
|
+
return said.line;
|
|
107
|
+
});
|
|
108
|
+
return [
|
|
109
|
+
`Pending You: ${from} followed up on ${id}, which it answered for your person.`,
|
|
110
|
+
`Question: ${clip(heard.title, MAX_TITLE).text}`,
|
|
111
|
+
...wrote,
|
|
112
|
+
`Act on it if it needs anything, then call ${ack} to mark it heard.`,
|
|
113
|
+
...(cut ? [`(Cut short: get_request ${id} has all of it.)`] : []),
|
|
114
|
+
];
|
|
115
|
+
}
|
|
84
116
|
export function describe(heard, again) {
|
|
85
117
|
const id = heard.requestId;
|
|
86
118
|
const delegated = delegatedOf(heard);
|
|
87
119
|
if (delegated)
|
|
88
120
|
return describeHanded(heard, delegated);
|
|
121
|
+
const notice = noticeOf(heard);
|
|
122
|
+
if (notice && heard.status !== 'answered')
|
|
123
|
+
return describeNotice(heard, notice);
|
|
89
124
|
const question = `Question: ${clip(heard.title, MAX_TITLE).text}`;
|
|
90
125
|
let cut = false;
|
|
91
126
|
const wrote = heard.messages.map((message) => {
|
|
@@ -111,7 +146,7 @@ export function describe(heard, again) {
|
|
|
111
146
|
lines.push(`Pending You: ${id} is answered.`, question, `Answer: ${answer.text}`);
|
|
112
147
|
}
|
|
113
148
|
if (wrote.length)
|
|
114
|
-
lines.push(
|
|
149
|
+
lines.push(`${noticeOf(heard) ? 'Also' : 'They also wrote'}: ${wrote.join(' ')}`);
|
|
115
150
|
lines.push(`Act on it, then call ack_answer with requestId ${id}, name ${named(heard)}, expectedVersion ${heard.version} and a one-line outcome.`);
|
|
116
151
|
break;
|
|
117
152
|
}
|