pendingyou 0.29.0 → 0.31.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 +19 -0
- package/README.md +12 -10
- package/dist/api.js +4 -1
- package/dist/apps/codex-wake.js +3 -3
- package/dist/format.js +13 -7
- package/dist/machine.js +8 -0
- package/dist/oauth.js +28 -7
- package/dist/version.js +1 -1
- package/mod/.claude-plugin/plugin.json +1 -1
- package/mod/README.md +1 -1
- package/mod/hooks/wake.js +3 -3
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,24 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.31.0 (2026-10-08)
|
|
4
|
+
|
|
5
|
+
- “I’ll handle it” has one meaning (guide 2.45): the person will handle it themselves. `hold`, the hooks, the wake mod
|
|
6
|
+
and the Codex wake say so in Pending You's own words, “The person will handle this themselves. Close it out: take no
|
|
7
|
+
action on it, leave things as they are (an email stays in their inbox, untouched), and don’t follow up or ask
|
|
8
|
+
again.”, then to ack_answer with an outcome like “Left to you”. The app no longer offers “Leave it”; a server since
|
|
9
|
+
guide 2.45 sends an older one as the same. `leave` now means only a to-do's Skip: “your person won’t do it”, don't do
|
|
10
|
+
it for them or follow up, and pick it up with an outcome like “Skipped”. Older command lines still stand down on
|
|
11
|
+
both.
|
|
12
|
+
|
|
13
|
+
## 0.30.0 (2026-10-07)
|
|
14
|
+
|
|
15
|
+
- This computer's own sign-ins (the connections `init` made for Claude Code, Codex, OpenCode and Pi) now last while
|
|
16
|
+
you use them. Each refresh carries a proof by this computer's key (a DPoP proof, RFC 9449, signed with the key in
|
|
17
|
+
`~/.config/pendingyou/machine.json`), and Pending You renews a sign-in for 30 days from that refresh when it's proven
|
|
18
|
+
by the key of the computer it's on. Before, every sign-in ended 30 days after it began, however much it was used. A
|
|
19
|
+
computer left unused for 30 days still signs out; Pending You says so 3 days before. Its tokens are unchanged, and a
|
|
20
|
+
refresh never fails for want of a proof: without a key, or when the proof isn't taken, it refreshes as before.
|
|
21
|
+
|
|
3
22
|
## 0.29.0 (2026-10-07)
|
|
4
23
|
|
|
5
24
|
- The command line links this computer's sign-ins to it by its key, so Pending You names every assistant here by this
|
package/README.md
CHANGED
|
@@ -339,8 +339,8 @@ hold to approve. Pending You says on the card and to the agent "Answered in Herd
|
|
|
339
339
|
Herdr (moving between panes, tabs and workspaces, or pressing keys in the popup) your phone stays quiet for those cards,
|
|
340
340
|
as it does while Pending You is open on your desk.
|
|
341
341
|
|
|
342
|
-
`npx pendingyou app status` says what each app signed in here may do and until when (a sign-in lasts 30 days
|
|
343
|
-
in the last 3), and `npx pendingyou app logout herdr` ends Herdr's sign-in. Settings › Apps and devices in Pending You
|
|
342
|
+
`npx pendingyou app status` says what each app signed in here may do and until when (a sign-in lasts 30 days from its
|
|
343
|
+
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
|
|
344
344
|
lists it too, with Remove. It needs `init` to have run here first: this computer's key vouches for the sign-in.
|
|
345
345
|
|
|
346
346
|
## How Claude Code hears your answer
|
|
@@ -378,10 +378,11 @@ next message brings any answer with it too.
|
|
|
378
378
|
When you're at the terminal, Claude Code asks you there and puts the same question in Pending You at once; the
|
|
379
379
|
first answer wins. If you answer in the terminal, it withdraws the card and the hold ends by itself.
|
|
380
380
|
|
|
381
|
-
**When you'll handle it yourself (0.20.0).** A card you end with “I’ll handle it” in Pending
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
`ack_answer` and an outcome like “Left to you”, and never
|
|
381
|
+
**When you'll handle it yourself (0.20.0; one meaning in 0.31.0).** A card you end with “I’ll handle it” in Pending
|
|
382
|
+
You reaches the agent the same ways, in these words: “The person will handle this themselves. Close it out: take no
|
|
383
|
+
action on it, leave things as they are (an email stays in their inbox, untouched), and don’t follow up or ask again.”
|
|
384
|
+
Then it picks it up with `ack_answer` and an outcome like “Left to you”, and never posts it again. A to-do you Skip
|
|
385
|
+
says you won't do it: the agent doesn't do it for you or follow up, and picks it up with an outcome like “Skipped”.
|
|
385
386
|
|
|
386
387
|
**Finishing setup by itself (0.11.0).** In `init`'s copy of the mod, a few seconds after a session you're at opens
|
|
387
388
|
(never `claude -p`), it asks Pending You whether Claude Code is set up here (`whoami`, once Pending You's tools are
|
|
@@ -440,8 +441,8 @@ The machine is called by its Tailscale name when Tailscale is installed and runn
|
|
|
440
441
|
else by its hostname, or by `--name build-01` on `init` or `login`; a Mac by its own name (Sharing in System Settings).
|
|
441
442
|
A cloud's own hostname (AWS's `ip-10-42-1-252`) is kept when there's nothing better, and `login` says how to change it:
|
|
442
443
|
`npx pendingyou login --name <name>` asks for one more approval and renames the connection, which stays the same one.
|
|
443
|
-
If an agent's sign-in ends (30 days, or you remove it on your Assistants page), `npx pendingyou login`
|
|
444
|
-
for Codex's) approves a new code and keeps the same connection.
|
|
444
|
+
If an agent's sign-in ends (30 days unused, or you remove it on your Assistants page), `npx pendingyou login`
|
|
445
|
+
(`--app codex` for Codex's; `--force` while it still works) approves a new code and keeps the same connection.
|
|
445
446
|
|
|
446
447
|
Only want this command's own sign-in (for `watch`, below)? `npx pendingyou login --device` signs it in with a code,
|
|
447
448
|
hearing answers only, and adds no connection. Your phone asks "Let build-01 hear your answers for the assistants
|
|
@@ -527,8 +528,9 @@ The hooks use the same sign-in, and hand a session only the cards its own app po
|
|
|
527
528
|
signs in by itself (`--oauth`, or one you kept) gets a sign-in that only hears its answers instead: it can't ask,
|
|
528
529
|
answer or change anything, and doesn't show up as an assistant. They're stored in `~/.config/pendingyou/credentials.json`
|
|
529
530
|
(readable only by you, in a folder with a `.gitignore` of `*` so a dotfiles repository never takes it), refresh
|
|
530
|
-
themselves,
|
|
531
|
-
|
|
531
|
+
themselves, last 30 days from their last use (0.30.0: each refresh proves this computer's key, and Pending You renews
|
|
532
|
+
the sign-in from then), and end once unused that long, or on `logout`. The headers helper hands an agent's token only
|
|
533
|
+
to that agent's Pending You (Claude Code says which server it's for; Codex's is the one `init` wrote). `clients.json` remembers which
|
|
532
534
|
sign-in each agent's connection used, so signing in again keeps it. `machine.json` (0.18.0) holds this computer's key
|
|
533
535
|
pair, private like the rest; only its proofs leave the computer, each for one sign-in, once. `apps/<app>.json` (0.21.0)
|
|
534
536
|
holds an app's own sign-in and the private key its tokens are bound to (DPoP), private like the rest; the Pending You
|
package/dist/api.js
CHANGED
|
@@ -33,8 +33,11 @@ async function refreshStored(io, origin, stored, options, app) {
|
|
|
33
33
|
return current;
|
|
34
34
|
if (!options.force && current.expiresAt - options.earlyMs > io.now())
|
|
35
35
|
return current;
|
|
36
|
+
const prove = current.kind === 'connection'
|
|
37
|
+
? await (await import("./machine.js")).machineProver(io)
|
|
38
|
+
: undefined;
|
|
36
39
|
try {
|
|
37
|
-
const tokens = await refresh(io, origin, current.clientId, current.refreshToken);
|
|
40
|
+
const tokens = await refresh(io, origin, current.clientId, current.refreshToken, prove);
|
|
38
41
|
return { ...current, ...tokens };
|
|
39
42
|
}
|
|
40
43
|
catch (error) {
|
package/dist/apps/codex-wake.js
CHANGED
|
@@ -414,12 +414,12 @@ function line(heard, card) {
|
|
|
414
414
|
if (handledOf(heard) === 'self')
|
|
415
415
|
return [
|
|
416
416
|
`your person will handle ${titled} themselves`,
|
|
417
|
-
`call ${get} to read it,
|
|
417
|
+
`call ${get} to read it, then close it out: take no action on it, leave things as they are and don’t follow up; ack_answer with its version and an outcome like “Left to you”`,
|
|
418
418
|
];
|
|
419
419
|
if (handledOf(heard) === 'leave')
|
|
420
420
|
return [
|
|
421
|
-
`your person
|
|
422
|
-
`call ${get} to read it, do
|
|
421
|
+
`your person won’t do ${titled}`,
|
|
422
|
+
`call ${get} to read it, then close it out: don’t do it for them and don’t follow up; ack_answer with its version and an outcome like “Skipped”`,
|
|
423
423
|
];
|
|
424
424
|
return [
|
|
425
425
|
`your person answered ${titled}`,
|
package/dist/format.js
CHANGED
|
@@ -12,7 +12,12 @@ export function handledOf(heard) {
|
|
|
12
12
|
const given = heard.handled;
|
|
13
13
|
return given === 'self' || given === 'leave' ? given : null;
|
|
14
14
|
}
|
|
15
|
-
const HANDLED_SAID = {
|
|
15
|
+
const HANDLED_SAID = {
|
|
16
|
+
self: ['I’ll handle it', 'I’ll do it myself'],
|
|
17
|
+
leave: ['I won’t do it', 'Leave it'],
|
|
18
|
+
};
|
|
19
|
+
export const HANDLED_MEANS = 'The person will handle this themselves. Close it out: take no action on it, leave things as they are (an email stays in their inbox, untouched), and don’t follow up or ask again.';
|
|
20
|
+
export const SKIPPED_MEANS = 'The person won’t do this to-do. Close it out: don’t do it for them, leave things as they are, and don’t follow up or ask again.';
|
|
16
21
|
export const APPROVED_DRAFT_NOTICE = 'The approved words are in get_request’s answer.approvedDrafts: send them exactly.';
|
|
17
22
|
export const approvesDraft = (heard) => heard.approvedDraft === true;
|
|
18
23
|
export const HANDED_PREFIX = 'Pending You: your person handed you';
|
|
@@ -140,11 +145,12 @@ function describeNotice(heard, notice) {
|
|
|
140
145
|
}
|
|
141
146
|
function standDown(heard, handled) {
|
|
142
147
|
const ack = `ack_answer with requestId ${heard.requestId}, name ${named(heard)}, expectedVersion ${heard.version}`;
|
|
143
|
-
const
|
|
144
|
-
const
|
|
148
|
+
const said = heard.said?.trim();
|
|
149
|
+
const noted = Boolean(said && !HANDLED_SAID[handled].includes(said));
|
|
150
|
+
const note = noted ? ' If their note asks you for one thing, do just that.' : '';
|
|
145
151
|
return handled === 'self'
|
|
146
|
-
?
|
|
147
|
-
:
|
|
152
|
+
? `${HANDLED_MEANS}${note} Then call ${ack} and an outcome like “Left to you”. Never post it again.`
|
|
153
|
+
: `${SKIPPED_MEANS}${note} Then call ${ack} and an outcome like “Skipped”. Never post it again.`;
|
|
148
154
|
}
|
|
149
155
|
export function describe(heard, again) {
|
|
150
156
|
const id = heard.requestId;
|
|
@@ -166,9 +172,9 @@ export function describe(heard, again) {
|
|
|
166
172
|
case 'answered': {
|
|
167
173
|
const handled = handledOf(heard);
|
|
168
174
|
if (handled) {
|
|
169
|
-
const answer = clip(heard.said ?? HANDLED_SAID[handled], MAX_ANSWER);
|
|
175
|
+
const answer = clip(heard.said ?? HANDLED_SAID[handled][0] ?? '', MAX_ANSWER);
|
|
170
176
|
cut ||= answer.cut;
|
|
171
|
-
lines.push(`Pending You: ${id} is answered: ${handled === 'self' ? 'your person will
|
|
177
|
+
lines.push(`Pending You: ${id} is answered: ${handled === 'self' ? 'your person will handle it themselves' : 'your person won’t do it'}.`, question, `Answer: ${answer.text}`);
|
|
172
178
|
if (wrote.length)
|
|
173
179
|
lines.push(`They also wrote: ${wrote.join(' ')}`);
|
|
174
180
|
lines.push(standDown(heard, handled));
|
package/dist/machine.js
CHANGED
|
@@ -7,6 +7,7 @@ import { PlainError } from "./errors.js";
|
|
|
7
7
|
import { configDir, readJson, withLock, writeWhole } from "./files.js";
|
|
8
8
|
import { discover } from "./oauth.js";
|
|
9
9
|
import { machineOf } from "./remote.js";
|
|
10
|
+
import { proof as dpopProof } from "./sdk/dpop.js";
|
|
10
11
|
export const ATTESTATION_TYPE = 'pendingyou-machine+jwt';
|
|
11
12
|
const ATTESTATION_SECONDS = 300;
|
|
12
13
|
export const MACHINE_PATH = '/mcp/cli/machine';
|
|
@@ -100,6 +101,13 @@ export async function attest(machine, claims, now) {
|
|
|
100
101
|
const signature = await webcrypto.subtle.sign({ name: 'ECDSA', hash: 'SHA-256' }, machine.signer, Buffer.from(signing));
|
|
101
102
|
return `${signing}.${base64url(new Uint8Array(signature))}`;
|
|
102
103
|
}
|
|
104
|
+
export async function machineProver(io) {
|
|
105
|
+
const machine = await readMachine(io).catch(() => null);
|
|
106
|
+
if (!machine)
|
|
107
|
+
return undefined;
|
|
108
|
+
const key = { jwk: machine.jwk, jkt: machine.thumbprint, privateKey: machine.signer };
|
|
109
|
+
return ({ method, url, nonce, now }) => dpopProof(key, { method, url, now, ...(nonce ? { nonce } : {}) });
|
|
110
|
+
}
|
|
103
111
|
export async function keyForSignIn(io) {
|
|
104
112
|
try {
|
|
105
113
|
const { machine, made } = await ensureMachine(io);
|
package/dist/oauth.js
CHANGED
|
@@ -128,14 +128,28 @@ async function listen(state, signal) {
|
|
|
128
128
|
},
|
|
129
129
|
};
|
|
130
130
|
}
|
|
131
|
-
async function tokenRequest(io, metadata, params, timeoutMs = 20_000) {
|
|
132
|
-
const
|
|
131
|
+
async function tokenRequest(io, metadata, params, timeoutMs = 20_000, prove) {
|
|
132
|
+
const signal = AbortSignal.timeout(timeoutMs);
|
|
133
|
+
const send = async (dpop) => io.fetch(metadata.token_endpoint, {
|
|
133
134
|
method: 'POST',
|
|
134
|
-
headers: {
|
|
135
|
+
headers: {
|
|
136
|
+
'content-type': 'application/x-www-form-urlencoded',
|
|
137
|
+
accept: 'application/json',
|
|
138
|
+
...(dpop ? { dpop } : {}),
|
|
139
|
+
},
|
|
135
140
|
body: new URLSearchParams(params).toString(),
|
|
136
|
-
signal
|
|
141
|
+
signal,
|
|
137
142
|
});
|
|
138
|
-
const
|
|
143
|
+
const proof = (nonce) => (prove ? prove(nonce).catch(() => null) : null);
|
|
144
|
+
let response = await send(await proof());
|
|
145
|
+
let body = (await response.json().catch(() => ({})));
|
|
146
|
+
for (const again of ['nonce', 'plain']) {
|
|
147
|
+
if (body.error !== 'use_dpop_nonce')
|
|
148
|
+
break;
|
|
149
|
+
const nonce = response.headers.get('dpop-nonce');
|
|
150
|
+
response = await send(again === 'nonce' && nonce ? await proof(nonce) : null);
|
|
151
|
+
body = (await response.json().catch(() => ({})));
|
|
152
|
+
}
|
|
139
153
|
if (!response.ok ||
|
|
140
154
|
typeof body.access_token !== 'string' ||
|
|
141
155
|
typeof body.refresh_token !== 'string') {
|
|
@@ -192,14 +206,21 @@ export async function signIn(io, options) {
|
|
|
192
206
|
loopback.close();
|
|
193
207
|
}
|
|
194
208
|
}
|
|
195
|
-
export async function refresh(io, origin, clientId, refreshToken) {
|
|
209
|
+
export async function refresh(io, origin, clientId, refreshToken, prove) {
|
|
196
210
|
const metadata = await discover(io, origin, 8_000);
|
|
197
211
|
return tokenRequest(io, metadata, {
|
|
198
212
|
grant_type: 'refresh_token',
|
|
199
213
|
refresh_token: refreshToken,
|
|
200
214
|
client_id: clientId,
|
|
201
215
|
resource: resourceOf(origin),
|
|
202
|
-
}, 12_000
|
|
216
|
+
}, 12_000, prove
|
|
217
|
+
? (nonce) => prove({
|
|
218
|
+
method: 'POST',
|
|
219
|
+
url: metadata.token_endpoint,
|
|
220
|
+
now: serverNow(io, metadata),
|
|
221
|
+
...(nonce ? { nonce } : {}),
|
|
222
|
+
})
|
|
223
|
+
: undefined);
|
|
203
224
|
}
|
|
204
225
|
export async function revoke(io, origin, clientId, refreshToken) {
|
|
205
226
|
try {
|
package/dist/version.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export const VERSION = '0.
|
|
1
|
+
export const VERSION = '0.31.0';
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pendingyou-wake",
|
|
3
3
|
"displayName": "Pending You",
|
|
4
|
-
"version": "2.
|
|
4
|
+
"version": "2.45.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.
|
|
7
|
+
Generated from recordplane/pendingyou (packages/claude-plugin/src/wake) with Pending You’s guide 2.45.0; send changes there.
|
package/mod/hooks/wake.js
CHANGED
|
@@ -391,12 +391,12 @@ function line(card) {
|
|
|
391
391
|
if (state.handled === 'self')
|
|
392
392
|
return [
|
|
393
393
|
`your person will handle ${titled(card)} themselves`,
|
|
394
|
-
`call ${get} to read it,
|
|
394
|
+
`call ${get} to read it, then close it out: take no action on it, leave things as they are and don’t follow up; ack_answer with its version and an outcome like “Left to you”`,
|
|
395
395
|
];
|
|
396
396
|
if (state.handled === 'leave')
|
|
397
397
|
return [
|
|
398
|
-
`your person
|
|
399
|
-
`call ${get} to read it, do
|
|
398
|
+
`your person won’t do ${titled(card)}`,
|
|
399
|
+
`call ${get} to read it, then close it out: don’t do it for them and don’t follow up; ack_answer with its version and an outcome like “Skipped”`,
|
|
400
400
|
];
|
|
401
401
|
return [
|
|
402
402
|
`your person answered ${titled(card)}`,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pendingyou",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.31.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
|
"license": "Apache-2.0",
|
|
6
6
|
"homepage": "https://www.pendingyou.com",
|