pendingyou 0.16.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 CHANGED
@@ -1,5 +1,44 @@
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
+
3
42
  ## 0.16.0 (2026-10-05)
4
43
 
5
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
package/README.md CHANGED
@@ -284,6 +284,29 @@ It's never in your way: the hooks only start it in the background, each send giv
284
284
  You from before presence is taken quietly. A sign-in that only hears (`--oauth`) sends nothing. `npx pendingyou status`
285
285
  has a Presence line for each agent.
286
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
+
287
310
  ## How Claude Code hears your answer
288
311
 
289
312
  **Claude Code 2.1.287 or later: the wake mod.** There's nothing to run. In each session the mod learns the cards
@@ -407,6 +430,7 @@ its environment. A failing command is tried twice more, then skipped. `--once` s
407
430
  | `stopcheck` | The Stop hook: items for you left only in chat (`--app`) |
408
431
  | `uninstall` | Remove what `init` added, and sign out (`--app`) |
409
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` |
410
434
 
411
435
  Every command takes `--origin` (default `https://www.pendingyou.com`, or `$PENDINGYOU_ORIGIN`). `--app` takes
412
436
  `claude-code`, `codex`, `opencode` or `pi` (several with commas where a command sets up or reports several); with none,
@@ -434,7 +458,10 @@ Pi session waits on, and (0.14.0) the names it went by and its folder, kept 12 h
434
458
  (ids, names, folders and titles: never an answer or a note); `pi/` Pi's extension; and `cli/` the hooks' copy of the
435
459
  command line. `init`, `status` and the hooks read `~/.claude.json` (and a folder's `.mcp.json`, and
436
460
  `~/.claude/settings.json`'s enabled plugins), `~/.codex/config.toml`, OpenCode's config and Pi's `mcp.json` only to see
437
- whether the agent has a `pendingyou` MCP server and where it points. With a sign-in that only hears, the hooks and
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
438
465
  listeners tell this computer's handed questions from another's by its names: its hostname, the names its sign-ins by
439
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
440
467
  the end of the Claude Code session's transcript only for the name the session gave Pending You, and `git remote
@@ -7,6 +7,7 @@ import { connectionMachine, readCredential } from "../credentials.js";
7
7
  import { configDir, readJson, withLock, writeWhole } from "../files.js";
8
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])
@@ -532,9 +599,12 @@ export async function listen(io, options) {
532
599
  else
533
600
  ready.push({ heard, card });
534
601
  }
535
- const gone = () => change(io, app, (state) => {
536
- delete state.threads[thread];
537
- });
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
+ };
538
608
  const message = wakeMessage(ready);
539
609
  if (message) {
540
610
  if (!(await deliver(message, ready.map(({ heard }) => heard.requestId)))) {
@@ -556,13 +626,15 @@ export async function listen(io, options) {
556
626
  for (const heard of requests)
557
627
  if (cards[heard.requestId] && isClosed(heard) && !done.includes(heard.requestId))
558
628
  done.push(heard.requestId);
559
- if (done.length)
629
+ if (done.length) {
560
630
  await change(io, app, (state) => {
561
631
  const waiting = state.threads[thread];
562
632
  if (waiting)
563
633
  for (const id of done)
564
634
  delete waiting.cards[id];
565
635
  });
636
+ await reportThreads(io, app, { at: io.now() });
637
+ }
566
638
  if (io.now() - started > PATIENT_AFTER_MS)
567
639
  await io.sleep(PATIENT_PAUSE_MS, signal);
568
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
  }
@@ -66,6 +79,7 @@ const COMMANDS = new Set([
66
79
  'notify',
67
80
  'permission-card',
68
81
  'presence',
82
+ 'herdr',
69
83
  ]);
70
84
  const FLAGS = {
71
85
  help: {},
@@ -121,6 +135,19 @@ const FLAGS = {
121
135
  refresh: { origin: true, force: false, app: true },
122
136
  'mcp-headers': { origin: true, check: false, app: true },
123
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
+ },
124
151
  };
125
152
  const VALUE_OPTIONS = new Set([
126
153
  'origin',
@@ -381,6 +408,8 @@ export function parseArgs(argv, env = {}) {
381
408
  case 'mcp':
382
409
  none();
383
410
  return { name: 'mcp', origin, ...app(), check: flags.has('check') };
411
+ case 'herdr':
412
+ return herdrCommand(origin, positional, flags, app);
384
413
  case 'init':
385
414
  none();
386
415
  return {
@@ -415,6 +444,87 @@ export function parseArgs(argv, env = {}) {
415
444
  }
416
445
  throw new UsageError(`There’s no “${first}” command.`);
417
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
+ }
418
528
  export const USAGE = `pendingyou: Pending You for your coding agents (Claude Code, Codex, OpenCode and Pi)
419
529
 
420
530
  npx -y pendingyou@latest init
@@ -440,6 +550,9 @@ export const USAGE = `pendingyou: Pending You for your coding agents (Claude Cod
440
550
  npx pendingyou mcp --app <app>
441
551
  A stdio MCP server for an agent that can't run a headers helper: it
442
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
443
556
 
444
557
  Apps (for --app): ${APP_IDS.map((app) => `${app} (${APP_NAMES[app]})`).join(', ')}.
445
558