pendingyou 0.14.0 → 0.16.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,102 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.16.0 (2026-10-05)
4
+
5
+ - 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
6
+ `notice` (`{ kind, from }`), and the hold, the pickup, the hooks and `watch` say what it is instead of “they wrote
7
+ back”:
8
+ - `follow_up`: the helper that answered it for your person followed up afterwards (“shipped in #231”): “Pending You:
9
+ pending-you-chief followed up on req_…, which it answered for your person.”, the question, its words, then “Act on
10
+ it if it needs anything, then call ack_answer … to mark it heard.” On an answer you haven't picked up yet, its
11
+ words come with the answer (“Also: …”).
12
+ - `handed_over`: your person handed your open cards to another assistant: “Pending You: your person handed your open
13
+ cards to pending-you-chief, req_… among them.” and “Stop working on them: pending-you-chief answers them and
14
+ follows up. Call ack_answer … to say you’ve stopped.”
15
+ `watch` gives both as `message_ready`. A command line before 0.16.0 hears the same words as a message from them.
16
+ - Claude Code's permission prompts get a card in bypassPermissions mode too: a dangerous request from a background
17
+ agent there waited minutes for an OK with no card. Claude Code still asks for a few things in that mode (a dangerous
18
+ `rm`, an ask rule, a safety check it can't verify), but the PermissionRequest hook skipped bypassPermissions and
19
+ dontAsk mode outright. It also runs for a call Claude Code then denies by itself, showing
20
+ nothing, so in those two modes it now writes the prompt down quietly, and its card goes up only once a dialog is on
21
+ screen. A new Notification hook says so: `pendingyou-hook notify --app claude-code`, with the matcher
22
+ `permission_prompt`, so Claude Code runs it only for a dialog that has waited about six seconds with nobody typing,
23
+ and never for its other notifications. A call denied without a dialog never gets a card: its PostToolUseFailure, the
24
+ 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
25
+ after 10 seconds, or as soon as the Notification comes.
26
+ - A dialog no PermissionRequest hook ran for (a sandboxed command's network request; one a Claude Code ran none for)
27
+ gets a card from its Notification alone: “Claude Code is waiting for your OK”, with Claude Code's own words, redacted,
28
+ in its summary (“It says: “Claude needs your permission”.”). Whose dialog it was isn't known, so only a turn that ends
29
+ with no subagent running, or the session's end, takes it down.
30
+ - A background agent's prompt says so: “Claude Code is waiting for your OK: pnpm lint && git push (a background
31
+ agent)”, and its summary names the agent's type (“A background agent (general-purpose) of Claude Code
32
+ (billing-webhooks), in ~/… on work-mbp, is waiting for your OK to run a command.”). It stays on the session's one card,
33
+ and only that agent's own calls settle it, not the session's or another agent's.
34
+ - `init` adds the Notification hook at the end of Claude Code's Notification hooks, after any of your own, and says so
35
+ on a computer set up before (“Added a Notification hook to the permission-prompt hooks: …”); `status` marks the
36
+ Permission prompts line missing without it; `uninstall` and `--no-permission-cards` take it out with the others.
37
+ Codex is unchanged: it has no hook that says a dialog is on screen (its `notify` runs only when a turn completes).
38
+
39
+ ## 0.15.0 (2026-10-05)
40
+
41
+ - Presence: Pending You hears which of your agents' sessions are open right now. Each app tells it with its own
42
+ connection here, `POST <origin>/mcp/cli/presence` with `{"source", "name", "sessionId", "cwd", "state"}` (`name` the
43
+ name the session goes by with Pending You, else null; `cwd` only as `~/…`, else null; `state` `live` or `closed`).
44
+ Pending You counts a session live for 15 minutes after its last `live`, so an open session says so every 5 minutes,
45
+ with no model involved:
46
+ - Claude Code: its session-start hook says live; the wake mod (Claude Code 2.1.287 or later) says live as the
47
+ session starts, after /clear or /resume, and every 5 minutes while it's open, by running the hooks' script; a new
48
+ SessionEnd hook, `presence`, through the same script, says closed. Without the wake mod, the next-message hook
49
+ says live again once the last was 4 minutes ago. Only `init`'s copy of the mod does it, never the plugin's.
50
+ - Codex: its session-start and next-message hooks start a keeper for the thread (`presence --keep`, detached, a lease
51
+ of its own), which says live every 5 minutes while the Codex process the hooks ran under is running (Codex CLI, or
52
+ the Codex app's own codex), and closed once it's gone. A new SessionEnd hook (Codex fires it when a conversation
53
+ is archived or deleted, when Codex closes, or after 30 idle minutes) says closed and stops the keeper; its timeout
54
+ is 3 seconds, the most Codex gives one. Where the Codex process can't be found, the hooks say live as they run.
55
+ - OpenCode's plugin and Pi's extension say live as a session starts (OpenCode: when it's created, or its first
56
+ message) and every 5 minutes while the app runs, and closed when OpenCode deletes the session or goes, or Pi shuts
57
+ the session down.
58
+ A hook only starts the send in the background; a send gives up after 3 seconds; a Pending You from before presence
59
+ answers 404, which is taken quietly. A sign-in that only hears (`--oauth`) sends nothing. Events are written down in
60
+ `~/.config/pendingyou/presence/<app>.json` (ids, states and times), so a late `live` never overrides a newer
61
+ `closed`, and a reload's `live` goes after its `closed`. `status` has a line for each app: “Presence: tells Pending
62
+ You when this session is open”.
63
+ - Codex's permission prompts on a card, as Claude Code's since 0.13.0. `init` adds Codex's PermissionRequest hook (it
64
+ fires when Codex is about to ask for approval: a shell command that needs more than the sandbox gives, an
65
+ `apply_patch`, an MCP tool; `tool_name` Bash, apply_patch or the tool's, and `tool_input.description` when Codex has
66
+ a reason), through the shim with `--app codex`. Codex reads that hook's output as its decision, so it prints nothing
67
+ and exits 0: it declines, and Codex asks as before. Once a prompt has waited 10 seconds, the same detached worker
68
+ posts one card with Codex's own connection: “Codex is waiting for your OK: git push origin main”, “…: edit
69
+ src/app.ts and 1 more” for a patch (the files its `*** Update File:` lines name), “…: create_issue on github” for an
70
+ MCP tool, with Codex's reason (redacted) in the summary, under the name the thread went by on Pending You's card tools,
71
+ else “Codex”, and the same redaction. It's withdrawn when the call runs (a PostToolUse hook for every tool, which
72
+ also settles a prompt for the same tool when Codex gives the call that ran its input in another form), with your
73
+ next message, at the end of the turn, or when the thread ends (SessionEnd). `--no-permission-cards` leaves Codex's
74
+ out too. The Codex app's browser-use prompts run no PermissionRequest hook, so they get no card.
75
+ - Presence keeps to Pending You's contract: the name trimmed to one line of at most 60 characters, a folder over 300
76
+ characters sent as null, a 401 tried once more after a refresh in the background, a 429 waited out once (its
77
+ `retry-after`, 60 seconds) by a send in its own process (never a hook), and 400, 403 and 404 taken quietly; Codex's
78
+ keeper stops once a send is refused, its sign-in is gone, or the server has no presence.
79
+ - A question you handed an agent, when the assistant that asked answers what the helper asked it on the card (or you
80
+ write in to them both, guide 2.30), wakes the helper with those words, not the handed question again: “Pending You:
81
+ billing-webhooks replied on the question handed to you, “Which queue?” (req_…): “It's the jobs queue.”. Call
82
+ get_request (requestId req_…, name “infra”) to read what you've said to each other. If you know now,
83
+ answer_delegated …; if you need more, reply_in_thread to “asker”; if it's your person's to decide, reply_in_thread
84
+ with escalate true; if not, hand_back …” (“your person wrote to you both …” for yours). The same from the hooks, the
85
+ wake mod, OpenCode's plugin and Pi's extension; Codex's `codex queue` leaves the words out of its arguments, where any
86
+ program could read them, and get_request has them. The wake mod now wakes a session for such a reply after it read
87
+ the question itself (before, a question it had read never woke it again), and the Stop check and the hooks know a
88
+ turn such a wake started.
89
+ - Switching an agent's own sign-in to this computer's says what becomes of the old one: once the agent shows up on this
90
+ computer's sign-in, Pending You moves the old sign-in's agents over, with their cards and the questions you delegated
91
+ to them, and signs it out. Where there's no browser (an SSH session) init says what it switched instead of switching
92
+ without a word.
93
+ - Codex runs a hook only once you trust it, so the three new hooks (PermissionRequest, PostToolUse for every tool,
94
+ SessionEnd) need trusting once. They go at the end of their lists, so the ones you trusted stay trusted. `init` says
95
+ so when it adds them to a Codex set up before (“Codex has new Pending You hooks (session end and permission prompts),
96
+ which it skips until you trust them once: …”), and `status` has a line for each, with each hook's trust:
97
+ “Permission prompts: a card when Codex waits for your OK” and “Presence: …”. Neither keeps `status` from saying
98
+ Ready. Codex's record of its hooks (`codex.json`) now names each by its event and command.
99
+
3
100
  ## 0.14.0 (2026-10-05)
4
101
 
5
102
  - Questions your person hands one of your agents from another assistant (Delegate: Pending You's card link, or D on the
package/README.md CHANGED
@@ -24,8 +24,9 @@ Run it once on each computer, in a terminal. It finds the agents on the computer
24
24
  Pi reads Codex's, else `~/.pi/agent/skills/pendingyou/SKILL.md`),
25
25
  - adds the hooks that hand it your answers: a new session picks up answers that arrived while nothing was running,
26
26
  your next message brings a waiting answer with it, and a check when it finishes a turn catches what it left you only
27
- in chat ([below](#nothing-left-only-in-chat)); and, for Claude Code, a card when it waits for your OK to run something
28
- ([below](#when-claude-code-waits-for-your-ok)),
27
+ in chat ([below](#nothing-left-only-in-chat)); for Claude Code and Codex, a card when it waits for your OK to run
28
+ something ([below](#when-claude-code-waits-for-your-ok)); and they tell Pending You which sessions are open
29
+ ([below](#which-sessions-are-open)),
29
30
  - and wakes it when you answer: Claude Code 2.1.287 or later through the wake mod, Codex through `codex queue`, OpenCode
30
31
  through its plugin and Pi through its extension (which also do the hooks' work there); and when you hand it a
31
32
  question from another of your assistants ([below](#when-you-hand-an-agent-a-question)).
@@ -54,10 +55,11 @@ in each, every other byte as it was), and running it again changes nothing it do
54
55
  **A pendingyou server that's there already** is decided on before anyone signs in. One at another Pending You
55
56
  (staging, say) is named and replaced only once you say so (`--yes` replaces it without asking); keep it and that agent
56
57
  is left as it is. One at this Pending You that signs in by itself (Claude Code's `/mcp` → Authenticate, Codex's
57
- `codex mcp login`, or the Pending You plugin for Claude) is moved to this computer's sign-in: without asking where
58
- there's no browser (it can't finish there anyway), on Yes (the default) at a terminal, or with `--yes`; with nobody to
59
- ask it's kept, and `init` prints the command that switches it. The connection it had stays on your Assistants page for
60
- you to remove, and answers to the cards it asked through that one won't reach the agent by themselves. A kept plugin
58
+ `codex mcp login`, or the Pending You plugin for Claude) is moved to this computer's sign-in: where there's no browser
59
+ (it can't finish there anyway) without asking, saying what it switched; on Yes (the default) at a terminal; or with
60
+ `--yes`; with nobody to ask it's kept, and `init` prints the command that switches it. Once the agent shows up on this
61
+ computer's sign-in, Pending You moves the old sign-in's agents over to it, with their cards and the questions you
62
+ delegated to them, and signs the old one out. A kept plugin
61
63
  carries the skill, so none is saved. `--oauth` keeps every agent signing in by itself, as before 0.11.0.
62
64
 
63
65
  The hooks run a copy of this version kept in `~/.config/pendingyou/cli/<version>/`, through a small script with a path
@@ -94,10 +96,13 @@ http_headers_helper = "/Users/ann/.config/pendingyou/bin/pendingyou-mcp-headers
94
96
  default_tools_approval_mode = "approve"
95
97
  ```
96
98
 
97
- and four hooks in `~/.codex/hooks.json`: a session start, each message, Pending You's card tools (so the Stop check
98
- knows a turn posted, and so the thread is woken), and the Stop check. **Codex runs a hook only once you trust it**: in
99
- Codex CLI, start `codex` and choose “Trust all and continue” (or trust them in `/hooks`); in the Codex app, click
100
- Review hooks › Allow all (or Settings › Hooks › Trust all). `status` says which are trusted. A `pendingyou` server or
99
+ and hooks in `~/.codex/hooks.json`: a session start, each message, Pending You's card tools (so the Stop check knows a
100
+ turn posted, and so the thread is woken), and the Stop check; and (0.15.0) Codex's permission prompt and every tool
101
+ call that ran (a card when Codex waits for your OK, [below](#when-claude-code-waits-for-your-ok)) and the session's end
102
+ ([below](#which-sessions-are-open)). **Codex runs a hook only once you trust it**: in Codex CLI, start `codex` and
103
+ choose “Trust all and continue” (or trust them in `/hooks`); in the Codex app, click Review hooks › Allow all (or
104
+ Settings › Hooks › Trust all). `status` says which are trusted. Upgrading from 0.14.0 adds three hooks, which Codex
105
+ skips until you trust them once; `init` says so, and the ones you trusted stay trusted. A `pendingyou` server or
101
106
  hooks Codex's `/import` copied from Claude Code (without its sign-in) are replaced.
102
107
 
103
108
  **The Codex app** (on a Mac, in the ChatGPT app) shares all of this with Codex CLI: the same `~/.codex`, server, hooks
@@ -241,10 +246,44 @@ a key are blanked (`curl -H "Authorization: …" https://api.example.com/items?t
241
246
  shown safely (several lines, inline code like `node -e`, one that takes a secret) shows only as “Bash: node …”. No card
242
247
  for `claude -p` or the Agent SDK, where nobody sees a prompt.
243
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
+
244
257
  It needs Claude Code 2.1.119 or later, signing in through this computer's sign-in (not `--oauth`), and isn't on
245
258
  Windows yet. Don't want it? `npx pendingyou init --no-permission-cards` (later runs keep it off; `--permission-cards`
246
259
  turns it back on). `npx pendingyou status` says which.
247
260
 
261
+ **Codex** (0.15.0) gets the same: when Codex stops to ask for your approval (a command that needs more than its sandbox
262
+ gives, a patch, an MCP tool) and you don't answer within 10 seconds, a card says “Codex is waiting for your OK: git push
263
+ origin main” (“…: edit src/app.ts and 1 more” for a patch), with Codex's reason for asking, blanked like the rest. Its
264
+ PermissionRequest hook prints nothing, so Codex asks exactly as before; the card goes once the call runs, you write to
265
+ Codex, the turn ends or the thread closes. It needs Codex signing in through this computer's sign-in, and trusting the
266
+ new hooks once. The Codex app's browser-use prompts don't run that hook, so they get no card.
267
+
268
+ ## Which sessions are open
269
+
270
+ Since 0.15.0 Pending You hears which of your agents' sessions are open right now, so it can say so and offer an open
271
+ one a question. Each app tells it with its own connection here: the session's id, the name it goes by with Pending You,
272
+ its folder (under `~` only) and `live` or `closed`. Pending You counts a session open for 15 minutes after its last
273
+ `live`, so an open session says so every 5 minutes, with no model involved:
274
+
275
+ - **Claude Code**: as a session starts (its session-start hook, and the wake mod), then every 5 minutes from the wake
276
+ mod while it's open (without the mod, with your messages); closed as it ends (a SessionEnd hook).
277
+ - **Codex**: as a thread starts or you write to it, a small process in the background says so every 5 minutes while
278
+ Codex (or the Codex app) runs, and closed once it's gone; its SessionEnd hook (a conversation archived or closed, or
279
+ idle 30 minutes) says closed.
280
+ - **OpenCode and Pi**: the plugin and the extension, as a session starts and every 5 minutes while the app runs, and
281
+ closed as it goes.
282
+
283
+ It's never in your way: the hooks only start it in the background, each send gives up after 3 seconds, and a Pending
284
+ You from before presence is taken quietly. A sign-in that only hears (`--oauth`) sends nothing. `npx pendingyou status`
285
+ has a Presence line for each agent.
286
+
248
287
  ## How Claude Code hears your answer
249
288
 
250
289
  **Claude Code 2.1.287 or later: the wake mod.** There's nothing to run. In each session the mod learns the cards
@@ -386,8 +425,9 @@ Pending You (Claude Code says which server it's for; Codex's is the one `init` w
386
425
  sign-in each agent's connection used, so signing in again keeps it.
387
426
 
388
427
  `state.json` beside them remembers which answers were already handed over (ids only) and how many session starts
389
- reminded an unfinished setup, and when; `permission-cards/` each Claude Code session's prompts still waiting on your OK
390
- and its card (ids, hashes and the card's redacted words: never what the tool was given), gone once nothing waits;
428
+ reminded an unfinished setup, and when; `permission-cards/` each Claude Code session's (and Codex thread's) prompts
429
+ still waiting on your OK and its card (ids, hashes and the card's redacted words: never what the tool was given), gone once nothing waits;
430
+ `presence/` the last presence each session said (ids, states and times), and Codex threads' keepers;
391
431
  `claude-code.json`, `codex.json`, `opencode.json` and `pi.json` what `init`
392
432
  added; `codex-threads.json`, `opencode-threads.json` and `pi-threads.json` which cards each Codex thread or OpenCode or
393
433
  Pi session waits on, and (0.14.0) the names it went by and its folder, kept 12 hours for a question you may hand it
@@ -398,8 +438,9 @@ whether the agent has a `pendingyou` MCP server and where it points. With a sign
398
438
  listeners tell this computer's handed questions from another's by its names: its hostname, the names its sign-ins by
399
439
  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
400
440
  the end of the Claude Code session's transcript only for the name the session gave Pending You, and `git remote
401
- get-url origin` in its folder (sent without any user name or password in it). Nothing is sent anywhere but Pending
402
- You. OpenCode's plugin reads its session's messages only when it goes idle, for the check that
441
+ get-url origin` in its folder (sent without any user name or password in it). To tell when a Codex thread's Codex has quit, its
442
+ keeper asks `ps` about the Codex process its hooks ran under (its id and program name, nothing else). Nothing is sent
443
+ anywhere but Pending You. OpenCode's plugin reads its session's messages only when it goes idle, for the check that
403
444
  nothing was left in chat, and hands the hooks' script just that turn's words and tool names.
404
445
 
405
446
  The wake mod runs inside Claude Code and reaches Pending You only through the session's own connection, with
@@ -407,7 +448,9 @@ The wake mod runs inside Claude Code and reaches Pending You only through the se
407
448
  `list_pending` with the session's name and `get_request` on a question you handed it. It keeps each session's cards (id,
408
449
  title, the agent's name, where each stands), and which handed questions a session was told of (ids and when), in Claude
409
450
  Code's store for mods, `~/.claude/plugins/store/`, and forgets them once they close or after 7 days. It marks the copy
410
- acting for a session in that session's environment (`PENDINGYOU_WAKE_OWNER`).
451
+ acting for a session in that session's environment (`PENDINGYOU_WAKE_OWNER`). `init`'s copy (0.15.0) also reads
452
+ `~/.config/pendingyou/claude-code.json` for the Pending You it's for, and runs the hooks' script to say the session is
453
+ open.
411
454
 
412
455
  ## Development
413
456
 
package/dist/api.js CHANGED
@@ -172,3 +172,30 @@ export async function getJson(io, origin, path, timeoutMs, signal, options = {})
172
172
  return { status: response.status, body };
173
173
  }
174
174
  }
175
+ export async function postJson(io, origin, path, body, timeoutMs, options = {}) {
176
+ const app = options.app ?? DEFAULT_APP;
177
+ const token = await hookToken(io, origin, options.force === true, options.signal, app);
178
+ let response;
179
+ try {
180
+ response = await io.fetch(`${origin}${path}`, {
181
+ method: 'POST',
182
+ headers: {
183
+ authorization: `Bearer ${token}`,
184
+ 'content-type': 'application/json',
185
+ accept: 'application/json',
186
+ },
187
+ body: JSON.stringify(body),
188
+ signal: either(options.signal, timeoutMs),
189
+ redirect: 'manual',
190
+ });
191
+ }
192
+ catch {
193
+ throw new Unavailable('Pending You couldn’t be reached.');
194
+ }
195
+ await response.body?.cancel().catch(() => { });
196
+ const retry = Number(response.headers.get('retry-after'));
197
+ return {
198
+ status: response.status,
199
+ ...(Number.isFinite(retry) && retry > 0 ? { retryAfter: retry } : {}),
200
+ };
201
+ }
@@ -7,16 +7,11 @@ import { checkHelper, foundBy, headlessReason, helperCommand, helperPath, isOurH
7
7
  import { connectionTitle, FINISH_SAY, setupOf, setupStatus } from "../setup.js";
8
8
  import { shimPath } from "../shim.js";
9
9
  import { APP_NAMES } from "./ids.js";
10
+ import { switches } from "./switch.js";
10
11
  const NAME = APP_NAMES['claude-code'];
11
12
  const RESTART = 'restart Claude Code here (/exit, then claude --continue), or start it in the folder you work in';
12
- async function switches(io, ctx, what) {
13
- if (ctx.headless || ctx.yes)
14
- return true;
15
- if (!io.interactive)
16
- return false;
17
- io.out(`${what} init can move Claude Code to this computer’s own sign-in, which the hooks use too: one sign-in for both, and nothing to Authenticate. The connection it has now stays on your Assistants page until you remove it, and answers to cards it asked through that one won’t reach Claude Code by themselves.\n`);
18
- const answer = await io.ask('Switch Claude Code to this computer’s sign-in? [Y/n] ');
19
- return !/^\s*no?\s*$/i.test(answer);
13
+ async function switchesFor(io, ctx, what) {
14
+ return switches(io, ctx, { name: 'Claude Code', what, uses: 'the hooks use', headless: true });
20
15
  }
21
16
  async function replaces(io, ctx, server, url) {
22
17
  ctx.progress(`Claude Code’s pendingyou MCP server points at ${server.url}, not ${url}.\n`);
@@ -117,7 +112,7 @@ export const claudeCode = {
117
112
  };
118
113
  }
119
114
  else if (helperMode &&
120
- (await switches(io, ctx, 'Claude Code signs in to Pending You by itself here (/mcp → Authenticate).')))
115
+ (await switchesFor(io, ctx, 'Claude Code signs in to Pending You by itself here (/mcp → Authenticate).')))
121
116
  plan = { action: 'switch', server };
122
117
  else {
123
118
  mode = 'oauth';
@@ -132,7 +127,7 @@ export const claudeCode = {
132
127
  }
133
128
  else if (plugin) {
134
129
  if (helperMode &&
135
- (await switches(io, ctx, 'The Pending You plugin signs Claude Code in to Pending You by itself here.')))
130
+ (await switchesFor(io, ctx, 'The Pending You plugin signs Claude Code in to Pending You by itself here.')))
136
131
  plan = { action: 'add', plugin: true };
137
132
  else {
138
133
  mode = 'oauth';
@@ -204,8 +199,10 @@ export const claudeCode = {
204
199
  };
205
200
  function permissionLine(io, claude, manifest, helper, flag) {
206
201
  const label = 'Permission prompts:';
207
- if (claude.permission)
202
+ if (claude.permission && claude.notify)
208
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}`;
209
206
  if (manifest?.permissionCards === false)
210
207
  return ` ${label} off, as you chose; for a card when Claude Code waits for your OK, run npx pendingyou init --permission-cards${flag}`;
211
208
  const blocker = claude.claude
@@ -215,6 +212,14 @@ function permissionLine(io, claude, manifest, helper, flag) {
215
212
  ? ` ${label} no card when Claude Code waits for your OK: ${blocker}`
216
213
  : ` missing ${label} no card when Claude Code waits for your OK; run npx pendingyou@latest init${flag}`;
217
214
  }
215
+ function presenceLine(claude, connected, mod, flag) {
216
+ const label = 'Presence:';
217
+ if (!connected)
218
+ return ` ${label} off: it needs Claude Code signed in through this computer’s own connection`;
219
+ if (!claude.presence || !claude.hooks)
220
+ return ` missing ${label} Pending You isn’t told when a session closes; run npx pendingyou@latest init${flag}`;
221
+ return ` ok ${label} tells Pending You when this session is open (${mod ? 'as it starts, then every 5 minutes from the wake mod' : 'as it starts and with your messages'}; closed as it ends)`;
222
+ }
218
223
  async function claudeStatus(io, ctx) {
219
224
  const { origin, signIn } = ctx;
220
225
  const [claude, here, manifest] = await Promise.all([
@@ -322,7 +327,7 @@ async function claudeStatus(io, ctx) {
322
327
  ? [
323
328
  ` ok Plugin: ${plugin} is enabled${pluginServer ? '' : mcp !== false && mcp !== 'unknown' ? '; Claude Code uses the server added by hand instead of its own' : ''}`,
324
329
  ]
325
- : []), ` ${mark(skillOk)} Skill: ${skillText}`, ` ${mark(claude.hooks && hooksRun)} Hooks: ${claude.hooks ? `session-start pickup and next-message hand-off; ${formText}` : `not installed; run npx pendingyou init${flag}`}`, ` ${mark(claude.stopcheck)} Stop check: ${claude.stopcheck ? 'asks Claude Code to post what it leaves you in chat' : `not installed; run npx pendingyou init${flag}`}`, ` ${mark(modOk)} Wake mod: ${modText}`, permissionLine(io, claude, manifest, helper, flag));
330
+ : []), ` ${mark(skillOk)} Skill: ${skillText}`, ` ${mark(claude.hooks && hooksRun)} Hooks: ${claude.hooks ? `session-start pickup and next-message hand-off; ${formText}` : `not installed; run npx pendingyou init${flag}`}`, ` ${mark(claude.stopcheck)} Stop check: ${claude.stopcheck ? 'asks Claude Code to post what it leaves you in chat' : `not installed; run npx pendingyou init${flag}`}`, ` ${mark(modOk)} Wake mod: ${modText}`, permissionLine(io, claude, manifest, helper, flag), presenceLine(claude, helper && connected, modOk, flag));
326
331
  const setup = setupStatus(NAME, signIn.state === 'ok' ? setupOf(signIn.for) : null);
327
332
  if (setup)
328
333
  lines.push(` ${setup === 'finished' ? 'ok ' : ' '} Setup: ${setup}`);
@@ -5,7 +5,7 @@ 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
10
  import { macName, tailscaleName } from "../remote.js";
11
11
  import { claimHanded, markHanded, momentOf, releaseHanded, wasHanded } from "../state.js";
@@ -310,6 +310,18 @@ function line(heard, card) {
310
310
  .replaceAll('”', '’')}” (${heard.requestId})`;
311
311
  const named = card.name ?? heard.sessionLabel;
312
312
  const get = `get_request (requestId ${heard.requestId}, ${named ? `name “${named}”` : 'your name'})`;
313
+ const notice = noticeOf(heard);
314
+ const from = notice ? clip(notice.from, 60).text : '';
315
+ if (notice?.kind === 'handed_over')
316
+ return [
317
+ `your person handed your open cards to ${from}, ${titled} among them`,
318
+ `stop working on them: ${from} answers them and follows up; ack_answer ${heard.requestId} with its version to say you’ve stopped`,
319
+ ];
320
+ if (notice?.kind === 'follow_up' && heard.status !== 'answered')
321
+ return [
322
+ `${from} followed up on ${titled}, which it answered for your person`,
323
+ `call ${get} to read it, act on it if it needs anything, then ack_answer with its version to mark it heard`,
324
+ ];
313
325
  switch (heard.status) {
314
326
  case 'expired':
315
327
  return [
@@ -339,7 +351,7 @@ export function wakeMessage(ready) {
339
351
  });
340
352
  if (lines.length === 0)
341
353
  return null;
342
- const asked = lines.some(({ heard }) => heard.turn === 'agent' && heard.status !== 'expired');
354
+ const asked = lines.some(({ heard }) => heard.turn === 'agent' && heard.status !== 'expired' && !noticeOf(heard));
343
355
  const reopen = asked
344
356
  ? ' If they asked you something or you need more from them, reply_in_thread with reopen instead.'
345
357
  : '';
@@ -636,7 +648,7 @@ export async function listenHanded(io, options) {
636
648
  ]);
637
649
  }
638
650
  for (const [thread, list] of byThread) {
639
- const message = handedMessage(list.map(({ card }) => card));
651
+ const message = handedMessage(list.map(({ card }) => card), { words: false });
640
652
  if (message && (await queue(thread, message)))
641
653
  continue;
642
654
  await giveBack(io, origin, list);