pendingyou 0.16.0 → 0.18.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 +70 -0
- package/README.md +60 -2
- package/dist/api.js +13 -2
- package/dist/apps/codex-wake.js +76 -4
- package/dist/args.js +163 -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 +243 -0
- package/dist/index.js +1 -0
- package/dist/init.js +5 -0
- package/dist/io.js +61 -0
- package/dist/machine-command.js +101 -0
- package/dist/machine.js +151 -0
- package/dist/main.js +6 -0
- package/dist/oauth.js +23 -12
- package/dist/pickup.js +7 -0
- package/dist/presence.js +43 -10
- package/dist/signin.js +6 -0
- package/dist/version.js +1 -1
- package/mod/hooks/register.js +38 -2
- package/mod/hooks/wake.js +60 -0
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,75 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.18.0 (2026-10-05)
|
|
4
|
+
|
|
5
|
+
- Presence says which terminal: in a Herdr pane (`HERDR_ENV=1` with `HERDR_BIN_PATH` and `HERDR_PANE_ID`), every
|
|
6
|
+
presence report (`POST /mcp/cli/presence`: each session's start, its 5-minute heartbeat and its end, for Claude Code,
|
|
7
|
+
Codex, OpenCode and Pi) also says where the session runs: `"terminal": { "app": "herdr", "paneId": "w2:p1", "server":
|
|
8
|
+
"<16 hex>" }`. `server` names the Herdr server without the path of its socket: the first 16 hex characters of a
|
|
9
|
+
SHA-256 of the computer's name and `HERDR_SOCKET_PATH`, left out when there's no socket path. Pending You keeps it with
|
|
10
|
+
the session and lists it with the agent's sessions running now, so the Pending You plugin for Herdr can go from a card
|
|
11
|
+
to its agent's pane. It goes whether or not the badges are on. Anywhere else a report is exactly as before, with no
|
|
12
|
+
`terminal`. A Pending You from before it refuses the field (400): the same report goes again without it.
|
|
13
|
+
- This computer's key: Pending You knows a computer by a key, not by its name. The first time `init` (or `login`) signs
|
|
14
|
+
anything in, it makes a P-256 key pair in `~/.config/pendingyou/machine.json` (0600, in the 0700 folder with its
|
|
15
|
+
`.gitignore`), shared by every Pending You address. The private key never leaves the computer and is never printed or
|
|
16
|
+
logged; Pending You keeps only its public key's RFC 7638 thumbprint. One that can't be read is never replaced: `init`
|
|
17
|
+
says so and signs in without it.
|
|
18
|
+
- Every device sign-in it starts (`POST /oauth/device`) carries `machine_attestation`: a JWS the key signs (ES256,
|
|
19
|
+
`typ` `pendingyou-machine+jwt`, the public key in its header) for `<origin>/oauth/device`, the client signing in and
|
|
20
|
+
the computer's name, good for 5 minutes and once (`jti`). It's dated by Pending You's own clock (its answer's
|
|
21
|
+
`Date`), so a computer whose clock is off still proves its key. Whoever allows the sign-in enrolls the computer, and
|
|
22
|
+
the connections it makes belong to it. A client registered again (a forgotten one) gets a new attestation of its
|
|
23
|
+
own.
|
|
24
|
+
- On the next `init`, each agent already signed in proves the key with its own sign-in (`POST /mcp/cli/machine`,
|
|
25
|
+
`{ "attestation": … }` for `<origin>/mcp/cli/machine`): nothing to approve and no new sign-in, and the agents'
|
|
26
|
+
sign-ins stay exactly as they were. That's how a computer set up before 0.18.0 gets its key; until then its hooks
|
|
27
|
+
keep running the copy `init` installed, and nothing changes on it. A Pending You from before machines answers 404,
|
|
28
|
+
taken quietly.
|
|
29
|
+
- `pendingyou machine status` says whether Pending You knows each agent's connection by this key (from
|
|
30
|
+
`/mcp/cli/answers`' `for.machineKey`), and exits 0 only when it knows every one. `machine attest --client-id <id>`
|
|
31
|
+
(`--cnf <thumbprint>`, `--name`) prints an attestation for an app's own device sign-in, and nothing else.
|
|
32
|
+
`machine rotate` makes a new key, after a yes (`--yes`, or at the terminal).
|
|
33
|
+
|
|
34
|
+
## 0.17.0 (2026-10-05)
|
|
35
|
+
|
|
36
|
+
- Herdr: an agent running in a Herdr pane (`HERDR_ENV=1` with `HERDR_BIN_PATH` and `HERDR_PANE_ID`) marks its pane with
|
|
37
|
+
what it's waiting on you for, as display-only metadata (`src/herdr.ts`): `"$HERDR_BIN_PATH" pane report-metadata
|
|
38
|
+
"$HERDR_PANE_ID" --source pendingyou` with `py_app`, `py_agent` (the name the session goes by with Pending You),
|
|
39
|
+
`py_waiting` (how many cards wait on you), `py_card` (the most pressing one's title, redacted as permission cards' words
|
|
40
|
+
are and cut to 80 characters), `py_cards` (their request ids, as many as fit in 80 characters), `py_urgency`
|
|
41
|
+
(`blocking`, else `now`, `today` or `whenever`) and `py_asked` (those also asked in the conversation), and the state
|
|
42
|
+
labels `idle` and `done` read "waiting on you" while something waits (`--agent` keeps them to the pane's own agent).
|
|
43
|
+
Cleared when nothing waits and as the session ends. Herdr's program is run with an argument list, given one second,
|
|
44
|
+
its outcome never thrown, and a hook only starts it in the background; `--seq` (the moment, in milliseconds) keeps a
|
|
45
|
+
late write from winning, and `--ttl-ms 900000` lets tokens nobody repeats expire. Herdr's limits are kept: one source,
|
|
46
|
+
7 tokens a report, its token names, 80 characters a value.
|
|
47
|
+
- Claude Code: init's copy of the wake mod writes them whenever they change and every 5 minutes, through the hooks'
|
|
48
|
+
script (`herdr report --app claude-code`, the session's cards on stdin), learning how pressing each card is from
|
|
49
|
+
post_request and update_request; the session-start hook names the session in the background (from its transcript);
|
|
50
|
+
the SessionEnd hook clears them in the background.
|
|
51
|
+
- Codex: its card hook (`posted`) marks the thread as the pane's and writes them in the background; the thread's
|
|
52
|
+
keeper writes them with each live (it now starts in Herdr even without a connection here, and keeps on for them when
|
|
53
|
+
Pending You won't take its presence) and clears them as Codex quits; the listener writes them once a card is
|
|
54
|
+
answered.
|
|
55
|
+
- OpenCode and Pi: through what their plugin and extension already run: `posted` after each card call, `presence
|
|
56
|
+
--state live` (the heartbeat) and `closed`, and the listener. A pane's badges are every session of the app marked
|
|
57
|
+
there: OpenCode runs several in one pane.
|
|
58
|
+
- `<app>-threads.json` remembers each card's urgency, whether it blocks and whether it was asked in the conversation,
|
|
59
|
+
and the Herdr pane each session runs in; `herdr.json` whether the badges are off here.
|
|
60
|
+
- `pendingyou herdr <what>`: the Pending You plugin for Herdr's commands (`src/herdr/`), which its manifest runs through
|
|
61
|
+
the hooks' script. `next` focuses the next pane waiting on you (blocking first, then by urgency); `open` is the popup
|
|
62
|
+
that lists them (Enter opens the card in Pending You through a terminal hyperlink, `t` takes you to the pane; `--demo`
|
|
63
|
+
shows made-up cards); `popup <pane>` opens one of the plugin's popups; `setup` checks Herdr's toasts, offers the
|
|
64
|
+
sidebar row (`$py_card`) as a managed block in Herdr's `config.toml`, written only with a yes and checked with `herdr
|
|
65
|
+
config check`, and prints the keybindings to add yourself; `doctor` says what's set up (also by hand in a terminal);
|
|
66
|
+
`view` turns the plugin's Agents view on or off; `watch` is the watcher (one per Herdr server, held by a lease) that
|
|
67
|
+
raises Herdr's toast when a pane gains a card, never for the tab you're looking at, cards that arrive together in one,
|
|
68
|
+
a card also asked in the conversation after 3 minutes; `event` makes sure it runs; `unconfigure` stops it, takes the
|
|
69
|
+
badges off every pane and turns them off here, clears the view, and takes the block out. Read-only: nothing asks
|
|
70
|
+
Pending You anything. Herdr's socket API through `HERDR_SOCKET_PATH`.
|
|
71
|
+
- Tests never reach a real Herdr: the test setup drops every `HERDR_*` variable from their environment.
|
|
72
|
+
|
|
3
73
|
## 0.16.0 (2026-10-05)
|
|
4
74
|
|
|
5
75
|
- 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,34 @@ 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
|
+
Since 0.18.0, a session in a [Herdr](https://herdr.dev) pane also says which pane (`HERDR_PANE_ID`, such as `w2:p1`)
|
|
288
|
+
and which Herdr server (16 hex characters of a hash of the computer's name and Herdr's socket path, never the path
|
|
289
|
+
itself), so Pending You can list where each of an agent's running sessions is and the Pending You plugin for Herdr can
|
|
290
|
+
take you to it. Anywhere else nothing of the kind is sent.
|
|
291
|
+
|
|
292
|
+
## In Herdr
|
|
293
|
+
|
|
294
|
+
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,
|
|
295
|
+
so Herdr's sidebar and Agents panel can show it: display-only metadata Herdr keeps on this computer (`herdr agent list`
|
|
296
|
+
shows it), written with Herdr's own command line:
|
|
297
|
+
|
|
298
|
+
- `py_app` and `py_agent`: the app and the name the session goes by with Pending You;
|
|
299
|
+
- `py_waiting`: how many of its cards wait on you; `py_card` the most pressing one's title (anything that looks like a
|
|
300
|
+
secret taken out, at most 80 characters); `py_cards` their request ids; `py_urgency` how pressing (`blocking`, `now`,
|
|
301
|
+
`today`, `whenever`); `py_asked` the ones it also asked you in its conversation;
|
|
302
|
+
- and its state reads "waiting on you" instead of idle or done while something waits.
|
|
303
|
+
|
|
304
|
+
Claude Code's wake mod writes them whenever they change, Codex's card hook and keeper, OpenCode's plugin and Pi's
|
|
305
|
+
extension through the commands they already run; every 5 minutes again, and they're taken off when nothing waits and as
|
|
306
|
+
the session ends. Herdr is given one second for each, never through a shell, and never waited on by a hook; it forgets
|
|
307
|
+
them after 15 minutes unless told again.
|
|
308
|
+
|
|
309
|
+
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
|
|
310
|
+
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
|
|
311
|
+
an Agents view with them first. Its commands are this command line's: `npx pendingyou herdr doctor` says what's set
|
|
312
|
+
up. Its `unconfigure` takes the badges off every pane and stops writing them on this computer; its setup turns them on
|
|
313
|
+
again.
|
|
314
|
+
|
|
287
315
|
## How Claude Code hears your answer
|
|
288
316
|
|
|
289
317
|
**Claude Code 2.1.287 or later: the wake mod.** There's nothing to run. In each session the mod learns the cards
|
|
@@ -378,6 +406,29 @@ Only want this command's own sign-in (for `watch`, below)? `npx pendingyou login
|
|
|
378
406
|
hearing answers only, and adds no connection. Your phone asks "Let build-01 hear your answers for the assistants
|
|
379
407
|
running on it?", with no choice of assistant.
|
|
380
408
|
|
|
409
|
+
## This computer's key
|
|
410
|
+
|
|
411
|
+
The first time `init` (or `login`) signs anything in, it makes this computer a key pair, kept in
|
|
412
|
+
`~/.config/pendingyou/machine.json`: readable only by you, beside your sign-ins. Pending You knows the computer by that
|
|
413
|
+
key, not by its name: two computers called "MacBook Pro" are two computers, and one you rename is still one. The
|
|
414
|
+
private key never leaves this computer and is never printed; Pending You keeps only its public key's fingerprint.
|
|
415
|
+
|
|
416
|
+
Every sign-in with a code proves the key, for that sign-in only and only once, so pressing Allow adds this computer,
|
|
417
|
+
by its key, with the agents' connections it makes. A computer set up before 0.18.0 gets its key the next time you run
|
|
418
|
+
`npx -y pendingyou@latest init`: each agent already signed in proves it with its own sign-in, with nothing to approve,
|
|
419
|
+
and its sign-ins stay exactly as they are. Until then nothing changes on it. Pending You uses the key to keep your
|
|
420
|
+
computers apart: an app you sign in on one computer later (such as the Pending You plugin for Herdr) sees only that
|
|
421
|
+
computer's agents.
|
|
422
|
+
|
|
423
|
+
- `npx pendingyou machine status` says whether Pending You knows each agent's connection here by this key.
|
|
424
|
+
- `npx pendingyou machine rotate` makes a new key (it asks first; `--yes` doesn't). Pending You then knows this
|
|
425
|
+
computer as a new one, and its agents' connections stay with the old key until you remove that computer from your
|
|
426
|
+
account; the next `init` then proves the new one.
|
|
427
|
+
- `npx pendingyou machine attest --client-id <id>` prints a proof of this computer for an app's own sign-in with a code
|
|
428
|
+
(`--cnf <thumbprint>` for that app's own key, `--name` for another name). It never prints the key.
|
|
429
|
+
|
|
430
|
+
`uninstall` leaves the key: it names the computer and grants nothing on its own.
|
|
431
|
+
|
|
381
432
|
## Always-on scripts
|
|
382
433
|
|
|
383
434
|
`watch` runs a command each time an answer is ready, for a script on your assistant's own machine:
|
|
@@ -407,6 +458,8 @@ its environment. A failing command is tried twice more, then skipped. `--once` s
|
|
|
407
458
|
| `stopcheck` | The Stop hook: items for you left only in chat (`--app`) |
|
|
408
459
|
| `uninstall` | Remove what `init` added, and sign out (`--app`) |
|
|
409
460
|
| `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`) |
|
|
461
|
+
| `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` |
|
|
462
|
+
| `machine status` / `attest` / `rotate` | This computer's key: whether Pending You knows each agent's connection by it (exits 0 when it does), a proof of it for an app's own sign-in (`--client-id`, `--cnf`, `--name`), or a new key (`--yes`) |
|
|
410
463
|
|
|
411
464
|
Every command takes `--origin` (default `https://www.pendingyou.com`, or `$PENDINGYOU_ORIGIN`). `--app` takes
|
|
412
465
|
`claude-code`, `codex`, `opencode` or `pi` (several with commas where a command sets up or reports several); with none,
|
|
@@ -422,7 +475,8 @@ answer or change anything, and doesn't show up as an assistant. They're stored i
|
|
|
422
475
|
(readable only by you, in a folder with a `.gitignore` of `*` so a dotfiles repository never takes it), refresh
|
|
423
476
|
themselves, and end after 30 days or on `logout`. The headers helper hands an agent's token only to that agent's
|
|
424
477
|
Pending You (Claude Code says which server it's for; Codex's is the one `init` wrote). `clients.json` remembers which
|
|
425
|
-
sign-in each agent's connection used, so signing in again keeps it.
|
|
478
|
+
sign-in each agent's connection used, so signing in again keeps it. `machine.json` (0.18.0) holds this computer's key
|
|
479
|
+
pair, private like the rest; only its proofs leave the computer, each for one sign-in, once.
|
|
426
480
|
|
|
427
481
|
`state.json` beside them remembers which answers were already handed over (ids only) and how many session starts
|
|
428
482
|
reminded an unfinished setup, and when; `permission-cards/` each Claude Code session's (and Codex thread's) prompts
|
|
@@ -434,7 +488,11 @@ Pi session waits on, and (0.14.0) the names it went by and its folder, kept 12 h
|
|
|
434
488
|
(ids, names, folders and titles: never an answer or a note); `pi/` Pi's extension; and `cli/` the hooks' copy of the
|
|
435
489
|
command line. `init`, `status` and the hooks read `~/.claude.json` (and a folder's `.mcp.json`, and
|
|
436
490
|
`~/.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.
|
|
491
|
+
whether the agent has a `pendingyou` MCP server and where it points. In a Herdr pane (0.17.0) the agents' writers hand
|
|
492
|
+
Herdr's own command line, on this computer, the badges above: a card's title (redacted, 80 characters at most) and its
|
|
493
|
+
request id, never an answer or a note; `<app>-threads.json` remembers the pane each session runs in, and `herdr.json`
|
|
494
|
+
whether the badges are off. Presence there (0.18.0) also sends Pending You the pane's id and the hash that names its
|
|
495
|
+
Herdr server. With a sign-in that only hears, the hooks and
|
|
438
496
|
listeners tell this computer's handed questions from another's by its names: its hostname, the names its sign-ins by
|
|
439
497
|
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
498
|
the end of the Claude Code session's transcript only for the name the session gave Pending You, and `git remote
|
package/dist/api.js
CHANGED
|
@@ -127,7 +127,13 @@ function either(signal, ms) {
|
|
|
127
127
|
timeout.addEventListener('abort', abort, { once: true });
|
|
128
128
|
return controller.signal;
|
|
129
129
|
}
|
|
130
|
-
export
|
|
130
|
+
export function getJson(io, origin, path, timeoutMs, signal, options = {}) {
|
|
131
|
+
return callJson(io, origin, path, null, timeoutMs, signal, options);
|
|
132
|
+
}
|
|
133
|
+
export function sendJson(io, origin, path, body, timeoutMs, options = {}) {
|
|
134
|
+
return callJson(io, origin, path, { body }, timeoutMs, undefined, options);
|
|
135
|
+
}
|
|
136
|
+
async function callJson(io, origin, path, send, timeoutMs, signal, options = {}) {
|
|
131
137
|
const app = options.app ?? DEFAULT_APP;
|
|
132
138
|
for (let attempt = 0;; attempt++) {
|
|
133
139
|
let token;
|
|
@@ -147,7 +153,12 @@ export async function getJson(io, origin, path, timeoutMs, signal, options = {})
|
|
|
147
153
|
let response;
|
|
148
154
|
try {
|
|
149
155
|
response = await io.fetch(`${origin}${path}`, {
|
|
150
|
-
|
|
156
|
+
...(send ? { method: 'POST', body: JSON.stringify(send.body), redirect: 'manual' } : {}),
|
|
157
|
+
headers: {
|
|
158
|
+
authorization: `Bearer ${token}`,
|
|
159
|
+
accept: 'application/json',
|
|
160
|
+
...(send ? { 'content-type': 'application/json' } : {}),
|
|
161
|
+
},
|
|
151
162
|
signal: either(signal, timeoutMs),
|
|
152
163
|
});
|
|
153
164
|
}
|
package/dist/apps/codex-wake.js
CHANGED
|
@@ -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 =
|
|
536
|
-
|
|
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,21 @@
|
|
|
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 MACHINE_SUBS = ['status', 'attest', 'rotate'];
|
|
6
|
+
export const HERDR_SUBS = [
|
|
7
|
+
'report',
|
|
8
|
+
'next',
|
|
9
|
+
'open',
|
|
10
|
+
'setup',
|
|
11
|
+
'doctor',
|
|
12
|
+
'unconfigure',
|
|
13
|
+
'watch',
|
|
14
|
+
'event',
|
|
15
|
+
'view',
|
|
16
|
+
'popup',
|
|
17
|
+
];
|
|
18
|
+
export const HERDR_POPUPS = ['cards', 'setup', 'doctor', 'demo'];
|
|
5
19
|
export class UsageError extends Error {
|
|
6
20
|
name = 'UsageError';
|
|
7
21
|
}
|
|
@@ -66,6 +80,8 @@ const COMMANDS = new Set([
|
|
|
66
80
|
'notify',
|
|
67
81
|
'permission-card',
|
|
68
82
|
'presence',
|
|
83
|
+
'herdr',
|
|
84
|
+
'machine',
|
|
69
85
|
]);
|
|
70
86
|
const FLAGS = {
|
|
71
87
|
help: {},
|
|
@@ -121,6 +137,20 @@ const FLAGS = {
|
|
|
121
137
|
refresh: { origin: true, force: false, app: true },
|
|
122
138
|
'mcp-headers': { origin: true, check: false, app: true },
|
|
123
139
|
mcp: { origin: true, app: true, check: false },
|
|
140
|
+
herdr: {
|
|
141
|
+
origin: true,
|
|
142
|
+
app: true,
|
|
143
|
+
session: true,
|
|
144
|
+
transcript: true,
|
|
145
|
+
name: true,
|
|
146
|
+
state: true,
|
|
147
|
+
at: true,
|
|
148
|
+
demo: false,
|
|
149
|
+
yes: false,
|
|
150
|
+
wait: false,
|
|
151
|
+
ensure: false,
|
|
152
|
+
},
|
|
153
|
+
machine: { origin: true, 'client-id': true, cnf: true, name: true, yes: false },
|
|
124
154
|
};
|
|
125
155
|
const VALUE_OPTIONS = new Set([
|
|
126
156
|
'origin',
|
|
@@ -135,6 +165,8 @@ const VALUE_OPTIONS = new Set([
|
|
|
135
165
|
'transcript',
|
|
136
166
|
'pid',
|
|
137
167
|
'at',
|
|
168
|
+
'client-id',
|
|
169
|
+
'cnf',
|
|
138
170
|
]);
|
|
139
171
|
function splitCommand(argv) {
|
|
140
172
|
let index = 0;
|
|
@@ -381,6 +413,10 @@ export function parseArgs(argv, env = {}) {
|
|
|
381
413
|
case 'mcp':
|
|
382
414
|
none();
|
|
383
415
|
return { name: 'mcp', origin, ...app(), check: flags.has('check') };
|
|
416
|
+
case 'herdr':
|
|
417
|
+
return herdrCommand(origin, positional, flags, app);
|
|
418
|
+
case 'machine':
|
|
419
|
+
return machineCommand(origin, positional, flags, machine);
|
|
384
420
|
case 'init':
|
|
385
421
|
none();
|
|
386
422
|
return {
|
|
@@ -415,6 +451,124 @@ export function parseArgs(argv, env = {}) {
|
|
|
415
451
|
}
|
|
416
452
|
throw new UsageError(`There’s no “${first}” command.`);
|
|
417
453
|
}
|
|
454
|
+
function herdrCommand(origin, positional, flags, app) {
|
|
455
|
+
const [sub, target, ...extra] = positional;
|
|
456
|
+
if (sub === undefined)
|
|
457
|
+
throw new UsageError('herdr needs what to do, like: pendingyou herdr doctor');
|
|
458
|
+
if (!HERDR_SUBS.includes(sub))
|
|
459
|
+
throw new UsageError(`herdr has no “${sub}”. Use ${HERDR_SUBS.join(', ')}.`);
|
|
460
|
+
const allowed = {
|
|
461
|
+
report: ['app', 'session', 'transcript', 'name', 'state', 'at'],
|
|
462
|
+
next: [],
|
|
463
|
+
open: ['demo'],
|
|
464
|
+
setup: ['yes'],
|
|
465
|
+
doctor: ['wait'],
|
|
466
|
+
unconfigure: [],
|
|
467
|
+
watch: ['ensure'],
|
|
468
|
+
event: [],
|
|
469
|
+
view: [],
|
|
470
|
+
popup: [],
|
|
471
|
+
};
|
|
472
|
+
for (const key of flags.keys())
|
|
473
|
+
if (key !== 'origin' && !allowed[sub].includes(key))
|
|
474
|
+
throw new UsageError(`herdr ${sub} has no --${key} option.`);
|
|
475
|
+
const command = { name: 'herdr', origin, sub: sub };
|
|
476
|
+
const takes = (choices) => {
|
|
477
|
+
if (extra.length || (target !== undefined && !choices?.includes(target)))
|
|
478
|
+
throw new UsageError(choices
|
|
479
|
+
? `herdr ${sub} takes ${choices.join(' or ')}.`
|
|
480
|
+
: `herdr ${sub} takes no “${target ?? extra[0]}”.`);
|
|
481
|
+
};
|
|
482
|
+
const text = (key) => {
|
|
483
|
+
const value = flags.get(key);
|
|
484
|
+
return typeof value === 'string' ? value : undefined;
|
|
485
|
+
};
|
|
486
|
+
switch (command.sub) {
|
|
487
|
+
case 'report': {
|
|
488
|
+
takes(null);
|
|
489
|
+
const session = text('session');
|
|
490
|
+
if (session !== undefined && !THREAD.test(session))
|
|
491
|
+
throw new UsageError('herdr report needs a session id, like: --session 0199a1b2-…');
|
|
492
|
+
const state = text('state');
|
|
493
|
+
if (state !== undefined && state !== 'live' && state !== 'closed')
|
|
494
|
+
throw new UsageError('--state is live or closed.');
|
|
495
|
+
const at = text('at');
|
|
496
|
+
if (at !== undefined && !/^\d{1,16}$/.test(at))
|
|
497
|
+
throw new UsageError('--at needs a number.');
|
|
498
|
+
const transcript = text('transcript');
|
|
499
|
+
const agentName = text('name');
|
|
500
|
+
return {
|
|
501
|
+
...command,
|
|
502
|
+
app: app().app ?? DEFAULT_APP,
|
|
503
|
+
...(session ? { session } : {}),
|
|
504
|
+
...(transcript ? { transcript } : {}),
|
|
505
|
+
...(agentName ? { agentName } : {}),
|
|
506
|
+
closed: state === 'closed',
|
|
507
|
+
...(at ? { at: Number(at) } : {}),
|
|
508
|
+
};
|
|
509
|
+
}
|
|
510
|
+
case 'popup':
|
|
511
|
+
if (target === undefined)
|
|
512
|
+
throw new UsageError(`herdr popup needs a pane: ${HERDR_POPUPS.join(', ')}.`);
|
|
513
|
+
takes(HERDR_POPUPS);
|
|
514
|
+
return { ...command, target };
|
|
515
|
+
case 'view':
|
|
516
|
+
takes(['on', 'off']);
|
|
517
|
+
return target ? { ...command, target } : command;
|
|
518
|
+
case 'open':
|
|
519
|
+
takes(null);
|
|
520
|
+
return { ...command, demo: flags.has('demo') };
|
|
521
|
+
case 'setup':
|
|
522
|
+
takes(null);
|
|
523
|
+
return { ...command, yes: flags.has('yes') };
|
|
524
|
+
case 'doctor':
|
|
525
|
+
takes(null);
|
|
526
|
+
return { ...command, wait: flags.has('wait') };
|
|
527
|
+
case 'watch':
|
|
528
|
+
takes(null);
|
|
529
|
+
return { ...command, ensure: flags.has('ensure') };
|
|
530
|
+
default:
|
|
531
|
+
takes(null);
|
|
532
|
+
return command;
|
|
533
|
+
}
|
|
534
|
+
}
|
|
535
|
+
const CLIENT_ID = /^[\x21-\x7e]{1,2048}$/;
|
|
536
|
+
const THUMBPRINT = /^[A-Za-z0-9_-]{43}$/;
|
|
537
|
+
function machineCommand(origin, positional, flags, machine) {
|
|
538
|
+
const [sub, ...extra] = positional;
|
|
539
|
+
if (sub === undefined)
|
|
540
|
+
throw new UsageError('machine needs what to do, like: pendingyou machine status');
|
|
541
|
+
if (!MACHINE_SUBS.includes(sub))
|
|
542
|
+
throw new UsageError(`machine has no “${sub}”. Use ${MACHINE_SUBS.join(', ')}.`);
|
|
543
|
+
if (extra.length)
|
|
544
|
+
throw new UsageError(`machine ${sub} takes no “${extra[0]}”.`);
|
|
545
|
+
const allowed = {
|
|
546
|
+
status: [],
|
|
547
|
+
attest: ['client-id', 'cnf', 'name'],
|
|
548
|
+
rotate: ['yes'],
|
|
549
|
+
};
|
|
550
|
+
for (const key of flags.keys())
|
|
551
|
+
if (key !== 'origin' && !allowed[sub].includes(key))
|
|
552
|
+
throw new UsageError(`machine ${sub} has no --${key} option.`);
|
|
553
|
+
if (sub === 'status')
|
|
554
|
+
return { name: 'machine', origin, sub };
|
|
555
|
+
if (sub === 'rotate')
|
|
556
|
+
return { name: 'machine', origin, sub, yes: flags.has('yes') };
|
|
557
|
+
const clientId = flags.get('client-id');
|
|
558
|
+
if (typeof clientId !== 'string' || !CLIENT_ID.test(clientId))
|
|
559
|
+
throw new UsageError('machine attest needs the client the app signs in with, like: --client-id <its client id>');
|
|
560
|
+
const cnf = flags.get('cnf');
|
|
561
|
+
if (cnf !== undefined && (typeof cnf !== 'string' || !THUMBPRINT.test(cnf)))
|
|
562
|
+
throw new UsageError('--cnf is the app’s own key’s RFC 7638 thumbprint: 43 base64url characters.');
|
|
563
|
+
return {
|
|
564
|
+
name: 'machine',
|
|
565
|
+
origin,
|
|
566
|
+
sub: 'attest',
|
|
567
|
+
clientId,
|
|
568
|
+
...(typeof cnf === 'string' ? { cnf } : {}),
|
|
569
|
+
...machine(),
|
|
570
|
+
};
|
|
571
|
+
}
|
|
418
572
|
export const USAGE = `pendingyou: Pending You for your coding agents (Claude Code, Codex, OpenCode and Pi)
|
|
419
573
|
|
|
420
574
|
npx -y pendingyou@latest init
|
|
@@ -440,6 +594,14 @@ export const USAGE = `pendingyou: Pending You for your coding agents (Claude Cod
|
|
|
440
594
|
npx pendingyou mcp --app <app>
|
|
441
595
|
A stdio MCP server for an agent that can't run a headers helper: it
|
|
442
596
|
passes each message on to Pending You with that agent's sign-in here
|
|
597
|
+
npx pendingyou herdr doctor Pending You in Herdr: the badges on your agents' panes, the plugin, its
|
|
598
|
+
toasts and its sidebar row. The plugin runs herdr next, open, setup,
|
|
599
|
+
view and unconfigure
|
|
600
|
+
npx pendingyou machine status | attest | rotate
|
|
601
|
+
This computer's key, which Pending You knows it by (init makes it; it
|
|
602
|
+
never leaves the computer): whether Pending You knows each agent's
|
|
603
|
+
connection by it, a proof of it for an app's own sign-in
|
|
604
|
+
(--client-id, --cnf), or a new key (--yes: without asking)
|
|
443
605
|
|
|
444
606
|
Apps (for --app): ${APP_IDS.map((app) => `${app} (${APP_NAMES[app]})`).join(', ')}.
|
|
445
607
|
|