pendingyou 0.37.0 → 0.39.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,32 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.39.0 (2026-10-25)
4
+
5
+ - In tmux, your agents' sessions now say which pane they run in, as they have in Herdr since 0.18.0: each presence
6
+ report carries the pane (`TMUX_PANE`, such as `%12`) and which tmux server it is (16 hex characters of a hash of the
7
+ computer's name and tmux's socket path, never the path itself). In a Herdr pane Herdr's is the one it says, even when
8
+ Herdr runs inside tmux. That's what lets an app you signed in on this computer bring the agent's tmux pane to the front
9
+ when you press Show on its card: the Pending You SDK's `show-in-tmux` example is one, to run on your own account.
10
+ Nothing else of tmux is read, and nothing is ever run in it. A Pending You from before tmux refuses the pane, and the
11
+ report goes again without it.
12
+
13
+ ## 0.38.0 (2026-10-25)
14
+
15
+ - Show in Herdr: press it on a card in Pending You, at your desk or on your phone, and Herdr moves to the pane of the
16
+ agent that asked, on every client attached to it. The Pending You plugin for Herdr's watcher listens for your
17
+ presses once Herdr's sign-in allows it (`assistants:show`), one wait at a time beside what it already does, and
18
+ checks each before anything moves: the pane must be on its own Herdr server, be there now, and be the agent's (its
19
+ badge lists the card, or it runs that kind of agent: `claude` for Claude Code). Otherwise Pending You hears "not
20
+ found" and nothing moves. It only ever brings a pane to the front: it never types into one, runs or opens anything,
21
+ and logs nothing. What it hears is ids and the agent's name, never a card's words.
22
+ - `npx pendingyou app login herdr` asks for `assistants:show` too, and `app status` lists it: "bring the assistant
23
+ that asked to the front when you press Show". A sign-in from before doesn't hold it: the plugin's setup and doctor
24
+ say "Sign Herdr in again to use Show in Herdr: npx pendingyou app login herdr", and setup offers to.
25
+ - Setup starts this version's watcher in place of one from an older pendingyou, which doesn't listen for Show in
26
+ Herdr; doctor says when the one running is older. The watcher's lease now says which pendingyou it is.
27
+ - Fixed: Herdr's toast "Pending You · Herdr is signed out" could show twice when the sign-in ended just after its
28
+ tokens were renewed. The watcher now tells sign-ins apart by their key, which renewing doesn't change.
29
+
3
30
  ## 0.37.0 (2026-10-22)
4
31
 
5
32
  - A step your agent hands you now goes on a card (guide 2.55). When its last message gives you a step to do now (“Go:
package/README.md CHANGED
@@ -365,7 +365,10 @@ has a Presence line for each agent.
365
365
  Since 0.18.0, a session in a [Herdr](https://herdr.dev) pane also says which pane (`HERDR_PANE_ID`, such as `w2:p1`)
366
366
  and which Herdr server (16 hex characters of a hash of the computer's name and Herdr's socket path, never the path
367
367
  itself), so Pending You can list where each of an agent's running sessions is and the Pending You plugin for Herdr can
368
- take you to it. Anywhere else nothing of the kind is sent.
368
+ take you to it. Since 0.39.0, a session in a tmux pane says which the same way (`TMUX_PANE`, such as `%12`, and 16 hex
369
+ characters of a hash of the computer's name and tmux's socket path), so an app you sign in on this computer can bring
370
+ it to the front when you press Show on its card; in a Herdr pane, even one inside tmux, Herdr's is the one it says.
371
+ Anywhere else nothing of the kind is sent.
369
372
 
370
373
  ## In Herdr
371
374
 
@@ -412,6 +415,17 @@ as it does while Pending You is open on your desk.
412
415
  last use here; it warns in the last 3), and `npx pendingyou app logout herdr` ends Herdr's sign-in. Settings › Apps and devices in Pending You
413
416
  lists it too, with Remove. It needs `init` to have run here first: this computer's key vouches for the sign-in.
414
417
 
418
+ ### Show in Herdr (0.38.0)
419
+
420
+ Press Show in Herdr on a card in Pending You, at your desk or on your phone, and Herdr moves to the pane of the agent
421
+ that asked, on every client attached to it. The plugin's watcher listens for your presses once Herdr's sign-in allows it
422
+ (`assistants:show`: `npx pendingyou app login herdr` asks for it; a sign-in from before 0.38.0 doesn't hold it, and the
423
+ plugin's setup and doctor say to sign in again). It checks each press before anything moves: the pane must be on its own
424
+ Herdr server, be there now, and be the agent's (its badge lists the card, or it runs that kind of agent, `claude` for
425
+ Claude Code). Otherwise Pending You hears "not found", and nothing moves. It only ever brings a pane to the front: it
426
+ never types into one, runs or opens anything, and logs nothing. Setup starts this version's watcher in place of one
427
+ from an older pendingyou, which doesn't listen.
428
+
415
429
  ## How Claude Code hears your answer
416
430
 
417
431
  **Claude Code 2.1.287 or later: the wake mod.** There's nothing to run. In each session the mod learns the cards
@@ -593,7 +607,9 @@ a command that acts for one agent acts for Claude Code. Options can come before
593
607
  hands over a card, and no agent's sign-in can. An app you sign in with `pendingyou app login` (the Pending You plugin
594
608
  for Herdr) answers only from its own sign-in and its own key, kept apart in `apps/<app>.json`, which nothing an agent
595
609
  runs here reads, and only when you press a key in it; never a high-stakes card, and Pending You labels every answer
596
- with the app and the computer.
610
+ with the app and the computer. Signed in with `assistants:show` (0.38.0), Herdr's watcher also keeps one wait open with
611
+ Pending You for your presses of Show in Herdr, which name a card, an agent and its pane, never a card's words, and it
612
+ only ever brings that pane to the front.
597
613
 
598
614
  Each agent's sign-in is that agent's own connection on Pending You, signed in from this computer: it can post cards,
599
615
  change or cancel its own, read the answers to them and reply in their threads, and it can't answer anything for you.
@@ -624,7 +640,7 @@ whether the agent has a `pendingyou` MCP server and where it points. In a Herdr
624
640
  Herdr's own command line, on this computer, the badges above: a card's title (redacted, 80 characters at most) and its
625
641
  request id, never an answer or a note; `<app>-threads.json` remembers the pane each session runs in, and `herdr.json`
626
642
  whether the badges are off. Presence there (0.18.0) also sends Pending You the pane's id and the hash that names its
627
- Herdr server. With a sign-in that only hears, the hooks and
643
+ Herdr server, and in a tmux pane (0.39.0) the tmux pane's id and the hash that names its tmux server. With a sign-in that only hears, the hooks and
628
644
  listeners tell this computer's handed questions from another's by its names: its hostname, the names its sign-ins by
629
645
  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
630
646
  the end of the Claude Code session's transcript only for the name the session gave Pending You, and `git remote
package/dist/app-login.js CHANGED
@@ -18,6 +18,7 @@ export const APP_SCOPES = [
18
18
  'cards:later',
19
19
  'cards:delegate',
20
20
  'assistants:read',
21
+ 'assistants:show',
21
22
  'presence:desk',
22
23
  ];
23
24
  export const FIRST_PARTY_APPS = {
@@ -54,6 +55,7 @@ const MAY = {
54
55
  'cards:later': 'put them in Later',
55
56
  'cards:delegate': 'hand them to another assistant',
56
57
  'assistants:read': 'see your assistants',
58
+ 'assistants:show': 'bring the assistant that asked to the front when you press Show',
57
59
  'presence:desk': 'keep your phone quiet while you use it',
58
60
  };
59
61
  const listed = (items) => items.length <= 1 ? (items[0] ?? '') : `${items.slice(0, -1).join(', ')} and ${items.at(-1)}`;
package/dist/args.js CHANGED
@@ -619,6 +619,7 @@ const APP_SCOPE_NAMES = [
619
619
  'cards:later',
620
620
  'cards:delegate',
621
621
  'assistants:read',
622
+ 'assistants:show',
622
623
  'presence:desk',
623
624
  ];
624
625
  function appCommandOf(origin, positional, flags, machine, device) {
@@ -5,6 +5,7 @@ import { reportThreads } from "../apps/codex-wake.js";
5
5
  import { APP_IDS, APP_NAMES } from "../apps/ids.js";
6
6
  import { configDir, readText, writeWhole } from "../files.js";
7
7
  import { badgesOn, clearHerdr, HERDR_SOURCE, herdrBin, herdrPane, herdrTarget, readPayload, setBadges, TOKEN_NAMES, writeHerdr, } from "../herdr.js";
8
+ import { originArgs } from "../hooks.js";
8
9
  import { sessionName } from "../permission.js";
9
10
  import { presenceName } from "../presence.js";
10
11
  import { headlessReason } from "../remote.js";
@@ -14,7 +15,7 @@ import { badgedPanes, nextWaiting, readWaiting } from "./panes.js";
14
15
  import { agentView, CARD_ROW, hasBlock, herdrConfigPath, KEYS_SNIPPET, ownSidebar, pluginOff, pluginPlace, SIDEBAR_BLOCK, saveView, setPluginOff, TOAST_SNIPPET, toastDelivery, viewOn, withBlock, withoutBlock, } from "./plugin.js";
15
16
  import { cardsPopup, originFor } from "./popup.js";
16
17
  import { HerdrError, herdrRequest } from "./socket.js";
17
- import { pluginEnabled, stopWatcher, watch, watching } from "./watch.js";
18
+ import { olderWatcher, pluginEnabled, stopWatcher, watch, watching } from "./watch.js";
18
19
  async function toast(place, title, body) {
19
20
  if (!place.socket)
20
21
  return;
@@ -157,6 +158,16 @@ async function originHere(io, fallback) {
157
158
  return fallback;
158
159
  }
159
160
  const line = (mark, text) => `${mark} ${text}\n`;
161
+ const showsHere = (stored) => stored.scopes.includes('assistants:show');
162
+ const signInAgainForShow = (origin) => `Sign Herdr in again to use Show in Herdr: npx pendingyou app login herdr${originArgs(origin)}`;
163
+ async function startWatcher(io, place) {
164
+ if (!place.socket)
165
+ return;
166
+ if (await olderWatcher(place))
167
+ await stopWatcher(place);
168
+ if (!(await watching(place)))
169
+ io.background(['herdr', 'watch']);
170
+ }
160
171
  async function setup(io, place, options) {
161
172
  const ask = async (question) => {
162
173
  if (options.yes)
@@ -202,8 +213,7 @@ async function setup(io, place, options) {
202
213
  io.out(line('✓', 'Agents waiting on you come first in the Agents panel.'));
203
214
  }
204
215
  }
205
- if (place.socket && !(await watching(place)))
206
- io.background(['herdr', 'watch']);
216
+ await startWatcher(io, place);
207
217
  if (io.interactive && !options.yes)
208
218
  await io.ask('\nPress Enter to close. ');
209
219
  return 0;
@@ -212,6 +222,14 @@ async function signInStep(io, options, ask) {
212
222
  const stored = await readAppSignIn(io, 'herdr').catch(() => null);
213
223
  if (stored) {
214
224
  io.out(`${line('✓', `Herdr is signed in to Pending You at ${stored.origin}: its popup (prefix+y) answers your cards with a key.`)} npx pendingyou app status herdr says what it may do and for how long.\n\n`);
225
+ if (showsHere(stored))
226
+ return;
227
+ io.out(line('!', signInAgainForShow(stored.origin)));
228
+ if (options.yes || !io.interactive || !(await ask('Sign Herdr in again now?'))) {
229
+ io.out('\n');
230
+ return;
231
+ }
232
+ await signInHerdr(io, stored.origin);
215
233
  return;
216
234
  }
217
235
  io.out(`\nAnswer from Herdr: once Herdr is signed in to Pending You, its popup (prefix+y) answers your cards with a key (a choice, yes or no, your words, Later, or handing it to another agent), only when you press one. High-stakes cards still open in Pending You. It sees only this computer's agents' cards unless you tick "all" when you allow it.\n`);
@@ -219,7 +237,9 @@ async function signInStep(io, options, ask) {
219
237
  io.out(`${line('·', 'Herdr isn’t signed in: its popup lists what’s waiting. Sign it in any time: setup again, or npx pendingyou app login herdr')}\n`);
220
238
  return;
221
239
  }
222
- const origin = await originHere(io, options.origin);
240
+ await signInHerdr(io, await originHere(io, options.origin));
241
+ }
242
+ async function signInHerdr(io, origin) {
223
243
  const result = await signInApp(io, {
224
244
  app: 'herdr',
225
245
  origin,
@@ -271,9 +291,14 @@ async function doctor(io, place, options) {
271
291
  io.out(shim
272
292
  ? line('✓', `Your agents' hooks run ${shimPath(io)}.`)
273
293
  : line('!', 'Pending You isn’t set up for your agents here: run npx -y pendingyou@latest init'));
274
- if (await readAppSignIn(io, 'herdr').catch(() => true)) {
294
+ const stored = await readAppSignIn(io, 'herdr').catch(() => undefined);
295
+ if (stored !== null) {
275
296
  const status = await appStatusLines(io, 'herdr');
276
297
  io.out(line(status.ok ? '✓' : '!', status.lines[0] ?? ''));
298
+ if (stored && status.ok)
299
+ io.out(showsHere(stored)
300
+ ? line('✓', 'Show in Herdr: press it on a card in Pending You, and Herdr goes to the pane of the agent that asked.')
301
+ : line('!', signInAgainForShow(stored.origin)));
277
302
  }
278
303
  else
279
304
  io.out(line('·', 'Herdr isn’t signed in to Pending You: its popup lists what’s waiting. Setup signs it in.'));
@@ -294,9 +319,11 @@ async function doctor(io, place, options) {
294
319
  const { snapshot, waiting } = await readWaiting(place.socket);
295
320
  const badged = badgedPanes(snapshot).length;
296
321
  io.out(line('·', `${badged} pane${badged === 1 ? '' : 's'} with Pending You's badges, ${waiting.length} waiting on you.`));
297
- io.out((await watching(place))
298
- ? line('✓', 'Its watcher is running (toasts when a pane gains a card).')
299
- : line('!', 'Its watcher isn’t running: it starts with Herdr, or with setup.'));
322
+ io.out(!(await watching(place))
323
+ ? line('!', 'Its watcher isn’t running: it starts with Herdr, or with setup.')
324
+ : (await olderWatcher(place))
325
+ ? line('!', `Its watcher runs an older pendingyou than this one (${VERSION}): setup restarts it.`)
326
+ : line('✓', 'Its watcher is running (toasts when a pane gains a card).'));
300
327
  io.out(line('·', `Its Agents view is ${(await viewOn(place)) ? 'on' : 'off'}.`));
301
328
  }
302
329
  catch (error) {
@@ -0,0 +1,35 @@
1
+ import { isAppId } from "../apps/ids.js";
2
+ import { HERDR_AGENTS, TOKENS } from "../herdr.js";
3
+ import { idsOf, readSnapshot } from "./panes.js";
4
+ import { herdrRequest } from "./socket.js";
5
+ export const SHOW_PLACES = ['herdr'];
6
+ export const placeHere = (server) => (place) => place.app === 'herdr' && place.server === server;
7
+ const herdrAgentOf = (app) => (isAppId(app) ? HERDR_AGENTS[app] : null);
8
+ export async function showPane(socket, show) {
9
+ const result = await herdrRequest(socket, 'session.snapshot');
10
+ const { panes } = readSnapshot(result?.snapshot);
11
+ const agent = herdrAgentOf(show.agent.app);
12
+ for (const place of show.places) {
13
+ const pane = panes.find((each) => each.paneId === place.paneId);
14
+ if (!pane)
15
+ continue;
16
+ if (idsOf(pane.tokens[TOKENS.cards]).includes(show.card.id))
17
+ return { paneId: pane.paneId };
18
+ if (agent !== null && pane.agent === agent)
19
+ return { paneId: pane.paneId };
20
+ }
21
+ return null;
22
+ }
23
+ export async function focusPane(socket, pane) {
24
+ await herdrRequest(socket, 'pane.focus', { pane_id: pane.paneId });
25
+ }
26
+ export function followHerdrShows(client, options) {
27
+ const { socket, server, signal } = options;
28
+ return client.followShows({
29
+ places: [...SHOW_PLACES],
30
+ mine: placeHere(server),
31
+ find: (show) => showPane(socket, show),
32
+ focus: (pane) => focusPane(socket, pane),
33
+ signal,
34
+ });
35
+ }
@@ -2,11 +2,15 @@ import { randomBytes } from 'node:crypto';
2
2
  import { mkdir, readFile, rm, stat, utimes, writeFile } from 'node:fs/promises';
3
3
  import { join } from 'node:path';
4
4
  import { appClient, RENEW_WITHIN_MS, readAppSignIn } from "../app-login.js";
5
+ import { herdrServer } from "../herdr.js";
6
+ import { atLeast } from "../mod.js";
5
7
  import { SignedOutError } from "../sdk/index.js";
8
+ import { VERSION } from "../version.js";
6
9
  import { due, emptyNotices, look } from "./notice.js";
7
10
  import { readWaiting } from "./panes.js";
8
11
  import { agentView, pluginOff, viewOn } from "./plugin.js";
9
12
  import { awayDesk, deskStep, lastActive } from "./presence.js";
13
+ import { followHerdrShows } from "./show.js";
10
14
  import { HerdrError, herdrRequest, herdrSubscribe } from "./socket.js";
11
15
  export const LEASE_STALE_MS = 60_000;
12
16
  export const RENEW_MS = 20_000;
@@ -32,7 +36,10 @@ async function takeLease(place) {
32
36
  const id = randomBytes(8).toString('hex');
33
37
  await rm(path, { force: true });
34
38
  try {
35
- await writeFile(path, JSON.stringify({ id, pid: process.pid }), { flag: 'wx', mode: 0o600 });
39
+ await writeFile(path, JSON.stringify({ id, pid: process.pid, version: VERSION }), {
40
+ flag: 'wx',
41
+ mode: 0o600,
42
+ });
36
43
  return id;
37
44
  }
38
45
  catch {
@@ -43,13 +50,23 @@ async function holder(place) {
43
50
  try {
44
51
  const held = JSON.parse(await readFile(leasePath(place), 'utf8'));
45
52
  return typeof held.id === 'string' && typeof held.pid === 'number'
46
- ? { id: held.id, pid: held.pid }
53
+ ? {
54
+ id: held.id,
55
+ pid: held.pid,
56
+ version: typeof held.version === 'string' ? held.version : null,
57
+ }
47
58
  : null;
48
59
  }
49
60
  catch {
50
61
  return null;
51
62
  }
52
63
  }
64
+ export async function olderWatcher(place) {
65
+ if (!(await watching(place)))
66
+ return false;
67
+ const held = await holder(place);
68
+ return held !== null && (held.version === null || !atLeast(held.version, VERSION));
69
+ }
53
70
  async function renew(place, id) {
54
71
  if ((await holder(place))?.id !== id)
55
72
  return false;
@@ -121,6 +138,8 @@ const FOCUS_EVENTS = new Set([
121
138
  'workspace_focused',
122
139
  ]);
123
140
  const EXPIRY_CHECK_MS = 6 * 60 * 60_000;
141
+ export const SHOWS_AGAIN_MS = 10 * 60_000;
142
+ const signInOf = (stored) => `${stored.key.x}.${stored.key.y}`;
124
143
  export const SIGNED_OUT_TOAST = {
125
144
  title: 'Pending You · Herdr is signed out',
126
145
  body: 'Removed in Settings, signed in again, or ended. To answer from Herdr again: Pending You: set up.',
@@ -131,19 +150,50 @@ export const endingToast = (days) => ({
131
150
  });
132
151
  export function signInKeeper(io, socket, place) {
133
152
  let client = null;
134
- let token = null;
153
+ let current = null;
135
154
  let presence = false;
155
+ let shows = false;
136
156
  let desk = awayDesk();
137
157
  let checkedAt = Number.NEGATIVE_INFINITY;
138
- let endedToken = null;
158
+ let endedSignIn = null;
139
159
  let warnedAt = Number.NEGATIVE_INFINITY;
140
160
  let focusAt = null;
141
- const ended = async () => {
142
- if (token && endedToken !== token) {
143
- endedToken = token;
161
+ const server = herdrServer(io.env, io.host);
162
+ let following = null;
163
+ let refused = null;
164
+ const stopShows = () => {
165
+ following?.stop.abort();
166
+ following = null;
167
+ };
168
+ const ended = async (which = current) => {
169
+ if (which === null || which !== current)
170
+ return;
171
+ client = null;
172
+ stopShows();
173
+ if (endedSignIn !== which) {
174
+ endedSignIn = which;
144
175
  await showToast(socket, SIGNED_OUT_TOAST);
145
176
  }
146
- client = null;
177
+ };
178
+ const followShows = () => {
179
+ if (!client || !current || !shows || !server || following || io.signal.aborted)
180
+ return;
181
+ if (refused?.signIn === current && io.now() - refused.at < SHOWS_AGAIN_MS)
182
+ return;
183
+ const run = { signIn: current, stop: new AbortController() };
184
+ following = run;
185
+ void followHerdrShows(client, { socket, server, signal: run.stop.signal })
186
+ .catch(async (error) => {
187
+ if (error instanceof SignedOutError)
188
+ await ended(run.signIn);
189
+ else
190
+ refused = { signIn: run.signIn, at: io.now() };
191
+ })
192
+ .catch(() => { })
193
+ .finally(() => {
194
+ if (following === run)
195
+ following = null;
196
+ });
147
197
  };
148
198
  return {
149
199
  heard(event) {
@@ -152,16 +202,27 @@ export function signInKeeper(io, socket, place) {
152
202
  },
153
203
  async renew() {
154
204
  const stored = await readAppSignIn(io, 'herdr').catch(() => null);
155
- if (!stored || stored.refreshToken === endedToken) {
205
+ const signIn = stored ? signInOf(stored) : null;
206
+ if (!stored || signIn === endedSignIn) {
156
207
  client = null;
157
- token = stored?.refreshToken ?? null;
208
+ current = signIn;
209
+ stopShows();
158
210
  return;
159
211
  }
160
- if (token !== stored.refreshToken || !client) {
161
- token = stored.refreshToken;
212
+ if (signIn !== current || !client) {
213
+ current = signIn;
162
214
  client = appClient(io, 'herdr');
163
- presence = stored.scopes.includes('presence:desk');
215
+ stopShows();
164
216
  }
217
+ presence = stored.scopes.includes('presence:desk');
218
+ shows = stored.scopes.includes('assistants:show');
219
+ if (shows)
220
+ followShows();
221
+ else
222
+ stopShows();
223
+ },
224
+ stop() {
225
+ stopShows();
165
226
  },
166
227
  async tick() {
167
228
  if (!client)
@@ -268,6 +329,7 @@ export async function watch(io, place, options) {
268
329
  return 0;
269
330
  }
270
331
  finally {
332
+ signIn.stop();
271
333
  if ((await holder(place))?.id === lease)
272
334
  await rm(leasePath(place), { force: true }).catch(() => { });
273
335
  }
package/dist/presence.js CHANGED
@@ -6,8 +6,9 @@ import { reportThreads, threadNames } from "./apps/codex-wake.js";
6
6
  import { DEFAULT_ORIGIN } from "./args.js";
7
7
  import { readCredential } from "./credentials.js";
8
8
  import { configDir, readJson, withLock, writeWhole } from "./files.js";
9
- import { herdrTarget, herdrTerminal, reportCommand } from "./herdr.js";
9
+ import { herdrTarget, reportCommand } from "./herdr.js";
10
10
  import { endPrompts, sessionName } from "./permission.js";
11
+ import { terminalOf } from "./terminal.js";
11
12
  export const PRESENCE_PATH = '/mcp/cli/presence';
12
13
  export const PRESENCE_EVERY_MS = 5 * 60_000;
13
14
  export const LIVE_AGAIN_MS = 4 * 60_000;
@@ -67,7 +68,7 @@ export async function sendPresence(io, origin, presence, timeoutMs = SEND_MS, op
67
68
  cwd: presence.cwd && presence.cwd.length <= CWD_MAX ? presence.cwd : null,
68
69
  state: presence.state,
69
70
  };
70
- const terminal = herdrTerminal(io.env, io.host);
71
+ const terminal = terminalOf(io.env, io.host);
71
72
  let body = terminal ? { ...plain, terminal } : plain;
72
73
  const send = (force) => postJson(io, origin, PRESENCE_PATH, body, timeoutMs, {
73
74
  app: presence.app,
@@ -2,9 +2,11 @@ import { changeFeed } from "./changes.js";
2
2
  import { importKey, proof, wantsNonce } from "./dpop.js";
3
3
  import { ApiError, SignedOutError } from "./errors.js";
4
4
  import { nonces, tokenRequest } from "./oauth.js";
5
+ import { followShowsWith, showFeed } from "./shows.js";
5
6
  export const REFRESH_SKEW_MS = 60_000;
6
7
  const CACHE_MAX = 200;
7
8
  const path = (id, action) => `/v1/cards/${encodeURIComponent(id)}${action ? `/${action}` : ''}`;
9
+ const showPath = (id) => `/v1/shows/${encodeURIComponent(id)}`;
8
10
  export function queryString(query) {
9
11
  const params = new URLSearchParams();
10
12
  for (const [name, value] of Object.entries(query ?? {})) {
@@ -218,6 +220,16 @@ export function createClient(options) {
218
220
  ...(query ? { query: query } : {}),
219
221
  }),
220
222
  changes: (opts) => changeFeed((query, signal) => client.changesPage(query, signal ? { signal } : {}), { now: http.now, sleep }, opts),
223
+ showsPage: (query, opts) => request('GET', '/v1/shows', {
224
+ ...opts,
225
+ query: query,
226
+ }),
227
+ shows: (opts) => showFeed((query, signal) => client.showsPage(query, signal ? { signal } : {}), { now: http.now, sleep }, opts),
228
+ answerShow: (id, result, opts) => request('POST', showPath(id), {
229
+ body: { result },
230
+ ...keyed(opts?.idempotencyKey),
231
+ }),
232
+ followShows: (opts) => followShowsWith((follow) => client.shows(follow), (id, result) => client.answerShow(id, result), opts),
221
233
  request,
222
234
  };
223
235
  return client;
package/dist/sdk/index.js CHANGED
@@ -5,5 +5,6 @@ export { deviceSignIn, } from "./device.js";
5
5
  export { generateKey, importKey, thumbprint } from "./dpop.js";
6
6
  export { ApiError, SignedOutError, SignInError } from "./errors.js";
7
7
  export * from "./openapi.generated.js";
8
+ export { REPEAT_MS } from "./shows.js";
8
9
  export { signOut } from "./signout.js";
9
10
  export { memoryStore } from "./store.js";
@@ -0,0 +1,86 @@
1
+ import { backoffMs, WAIT_SECONDS } from "./changes.js";
2
+ import { ApiError, SignedOutError } from "./errors.js";
3
+ export const REPEAT_MS = 1000;
4
+ const KEPT_MS = 60_000;
5
+ export async function* showFeed(look, clock, options) {
6
+ const { signal } = options;
7
+ const wait = Math.max(0, Math.min(WAIT_SECONDS, options.wait ?? WAIT_SECONDS));
8
+ const yielded = new Map();
9
+ let failures = 0;
10
+ while (!signal?.aborted) {
11
+ let page;
12
+ try {
13
+ page = await look({ places: [...options.places], wait }, signal);
14
+ }
15
+ catch (error) {
16
+ if (signal?.aborted)
17
+ return;
18
+ if (error instanceof SignedOutError)
19
+ throw error;
20
+ if (error instanceof ApiError) {
21
+ if (error.status === 429) {
22
+ failures++;
23
+ await clock.sleep((error.retryAfterSeconds ?? 60) * 1000, signal);
24
+ continue;
25
+ }
26
+ if (error.status < 500)
27
+ throw error;
28
+ }
29
+ failures++;
30
+ await clock.sleep(backoffMs(failures), signal);
31
+ continue;
32
+ }
33
+ failures = 0;
34
+ const now = clock.now();
35
+ for (const [id, until] of yielded)
36
+ if (until < now)
37
+ yielded.delete(id);
38
+ const fresh = page.shows.filter((show) => !yielded.has(show.id));
39
+ for (const show of fresh) {
40
+ const ends = Date.parse(show.expiresAt);
41
+ yielded.set(show.id, (Number.isFinite(ends) ? ends : now) + KEPT_MS);
42
+ yield show;
43
+ if (signal?.aborted)
44
+ return;
45
+ }
46
+ if (page.shows.length > 0 && fresh.length === 0)
47
+ await clock.sleep(REPEAT_MS, signal);
48
+ }
49
+ }
50
+ function isMine(options, place, show) {
51
+ if (!options.mine)
52
+ return true;
53
+ try {
54
+ return options.mine(place, show);
55
+ }
56
+ catch {
57
+ return false;
58
+ }
59
+ }
60
+ export async function followShowsWith(shows, answer, options) {
61
+ const { places, wait, signal, find, focus } = options;
62
+ for await (const show of shows({ places, wait, signal })) {
63
+ const theirs = show.places.filter((place) => isMine(options, place, show));
64
+ if (!theirs.length)
65
+ continue;
66
+ const ours = { ...show, places: theirs };
67
+ let result = 'not_found';
68
+ try {
69
+ const window = await find(ours);
70
+ if (window !== null && window !== undefined) {
71
+ await focus(window, ours);
72
+ result = 'shown';
73
+ }
74
+ }
75
+ catch {
76
+ result = 'not_found';
77
+ }
78
+ try {
79
+ await answer(show.id, result);
80
+ }
81
+ catch (error) {
82
+ if (error instanceof SignedOutError)
83
+ throw error;
84
+ }
85
+ }
86
+ }
@@ -0,0 +1,20 @@
1
+ import { createHash } from 'node:crypto';
2
+ import { isAbsolute } from 'node:path';
3
+ import { herdrTerminal } from "./herdr.js";
4
+ export const TMUX_PANE = /^%[0-9]{1,9}$/;
5
+ export function tmuxServer(env, host) {
6
+ const [socket = ''] = (env.TMUX ?? '').split(',');
7
+ if (!socket || !isAbsolute(socket) || /[\0\r\n]/.test(socket))
8
+ return null;
9
+ return createHash('sha256').update(`${host}${socket}`).digest('hex').slice(0, 16);
10
+ }
11
+ export function tmuxTerminal(env, host) {
12
+ const pane = env.TMUX_PANE ?? '';
13
+ if (!env.TMUX || !TMUX_PANE.test(pane))
14
+ return null;
15
+ const server = tmuxServer(env, host);
16
+ return { app: 'tmux', paneId: pane, ...(server ? { server } : {}) };
17
+ }
18
+ export function terminalOf(env, host) {
19
+ return herdrTerminal(env, host) ?? tmuxTerminal(env, host);
20
+ }
package/dist/version.js CHANGED
@@ -1 +1 @@
1
- export const VERSION = '0.37.0';
1
+ export const VERSION = '0.39.0';
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "pendingyou-wake",
3
3
  "displayName": "Pending You",
4
- "version": "2.54.1",
4
+ "version": "2.56.0",
5
5
  "description": "Wakes Claude Code when your person answers a Pending You card it posted, even while it sits idle. Installed by `npx pendingyou init`.",
6
6
  "author": {
7
7
  "name": "RecordPlane",
package/mod/README.md CHANGED
@@ -4,4 +4,4 @@ The pendingyou command line’s copy of the Pending You plugin’s wake mod, for
4
4
 
5
5
  In each session it learns the cards the session posts to Pending You, checks them through the session’s own Pending You connection, and starts a turn when your person answers, writes back or a fallback comes due, or hands the session a question from another of their assistants (CLI 0.14.0), even while it sits idle. It answers `npx pendingyou hold` itself, so nothing waits in the background, and shows “2 waiting on you” under the prompt, after the plugin’s name. With the Pending You plugin installed too, only one copy acts.
6
6
 
7
- Generated from recordplane/pendingyou (packages/claude-plugin/src/wake) with Pending You’s guide 2.54.1; send changes there.
7
+ Generated from recordplane/pendingyou (packages/claude-plugin/src/wake) with Pending You’s guide 2.56.0; send changes there.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pendingyou",
3
- "version": "0.37.0",
3
+ "version": "0.39.0",
4
4
  "description": "Pending You for your coding agents: one command sets up Claude Code, Codex, OpenCode and Pi to ask you through Pending You and hear your answers, even while they sit idle.",
5
5
  "keywords": [
6
6
  "pendingyou",