pendingyou 0.28.0 → 0.30.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 +21 -0
- package/README.md +14 -7
- package/dist/api.js +4 -1
- package/dist/apps/codex-wake.js +2 -0
- package/dist/args.js +6 -3
- package/dist/hold.js +2 -0
- package/dist/index.js +1 -0
- package/dist/init.js +8 -1
- package/dist/link.js +143 -0
- package/dist/machine-command.js +3 -0
- package/dist/machine.js +8 -0
- package/dist/oauth.js +28 -7
- package/dist/pickup.js +2 -0
- package/dist/remote.js +2 -0
- package/dist/state.js +10 -0
- package/dist/version.js +1 -1
- package/mod/.claude-plugin/plugin.json +1 -1
- package/mod/README.md +1 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,26 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.30.0 (2026-10-07)
|
|
4
|
+
|
|
5
|
+
- This computer's own sign-ins (the connections `init` made for Claude Code, Codex, OpenCode and Pi) now last while
|
|
6
|
+
you use them. Each refresh carries a proof by this computer's key (a DPoP proof, RFC 9449, signed with the key in
|
|
7
|
+
`~/.config/pendingyou/machine.json`), and Pending You renews a sign-in for 30 days from that refresh when it's proven
|
|
8
|
+
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
|
|
9
|
+
computer left unused for 30 days still signs out; Pending You says so 3 days before. Its tokens are unchanged, and a
|
|
10
|
+
refresh never fails for want of a proof: without a key, or when the proof isn't taken, it refreshes as before.
|
|
11
|
+
|
|
12
|
+
## 0.29.0 (2026-10-07)
|
|
13
|
+
|
|
14
|
+
- The command line links this computer's sign-ins to it by its key, so Pending You names every assistant here by this
|
|
15
|
+
computer and shows one computer, not two. Besides the connections `init` made, it now finds the sign-ins your apps
|
|
16
|
+
made on their own at this Pending You (Claude Code's `/mcp` Authenticate, or the Pending You plugin's server, kept in
|
|
17
|
+
Claude Code's own credentials; Codex's and OpenCode's MCP sign-ins kept in their files) and proves this computer's key
|
|
18
|
+
for each, reading only their OAuth clients, never a token. Pending You links the ones that are yours and on no other
|
|
19
|
+
computer, and merges an app's older sign-ins here into the one in use. Nothing is ever linked by a computer's name.
|
|
20
|
+
`init` links at once; the hooks, `hold`, the MCP headers helper and the wake do it in the background every few hours.
|
|
21
|
+
`npx pendingyou machine link` does it now.
|
|
22
|
+
- Proving the key again no longer renames the computer: a name you gave it in Pending You stays.
|
|
23
|
+
|
|
3
24
|
## 0.28.0 (2026-10-07)
|
|
4
25
|
|
|
5
26
|
- When a card you asked was handed to another assistant and comes back to your person without its answer (handed
|
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
|
|
@@ -440,8 +440,8 @@ The machine is called by its Tailscale name when Tailscale is installed and runn
|
|
|
440
440
|
else by its hostname, or by `--name build-01` on `init` or `login`; a Mac by its own name (Sharing in System Settings).
|
|
441
441
|
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
442
|
`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.
|
|
443
|
+
If an agent's sign-in ends (30 days unused, or you remove it on your Assistants page), `npx pendingyou login`
|
|
444
|
+
(`--app codex` for Codex's; `--force` while it still works) approves a new code and keeps the same connection.
|
|
445
445
|
|
|
446
446
|
Only want this command's own sign-in (for `watch`, below)? `npx pendingyou login --device` signs it in with a code,
|
|
447
447
|
hearing answers only, and adds no connection. Your phone asks "Let build-01 hear your answers for the assistants
|
|
@@ -467,6 +467,12 @@ computer's agents.
|
|
|
467
467
|
account; the next `init` then proves the new one.
|
|
468
468
|
- `npx pendingyou machine attest --client-id <id>` prints a proof of this computer for an app's own sign-in with a code
|
|
469
469
|
(`--cnf <thumbprint>` for that app's own key, `--name` for another name). It never prints the key.
|
|
470
|
+
- `npx pendingyou machine link` (0.29.0) links this computer's sign-ins to it now: the agents' connections, and the
|
|
471
|
+
sign-ins your apps made on their own (Claude Code's `/mcp` Authenticate or the Pending You plugin, Codex's and
|
|
472
|
+
OpenCode's MCP sign-ins), found by their OAuth clients in those apps' own stores here, never a token. Pending You then
|
|
473
|
+
names them all by this computer and merges an app's older sign-ins here into the one in use. `init` does it, and the
|
|
474
|
+
hooks every few hours in the background; nothing is linked by a computer's name. Rename a computer once, in Settings
|
|
475
|
+
› Apps and devices, and every assistant on it follows.
|
|
470
476
|
|
|
471
477
|
`uninstall` leaves the key: it names the computer and grants nothing on its own.
|
|
472
478
|
|
|
@@ -500,7 +506,7 @@ its environment. A failing command is tried twice more, then skipped. `--once` s
|
|
|
500
506
|
| `uninstall` | Remove what `init` added, and sign out (`--app`) |
|
|
501
507
|
| `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`) |
|
|
502
508
|
| `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` |
|
|
503
|
-
| `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`),
|
|
509
|
+
| `machine status` / `attest` / `rotate` / `link` | 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`), a new key (`--yes`), or this computer's sign-ins linked to it now, your apps' own ones too (`--quiet`) |
|
|
504
510
|
| `app login` / `logout` / `status` | An app that answers your cards when you press a key in it, signed in on this computer (`herdr`, or another app with `--client-id` and `--scope`; `--device`, `--browser`, `--name`), out again, or what each may do and until when |
|
|
505
511
|
|
|
506
512
|
Every command takes `--origin` (default `https://www.pendingyou.com`, or `$PENDINGYOU_ORIGIN`). `--app` takes
|
|
@@ -521,8 +527,9 @@ The hooks use the same sign-in, and hand a session only the cards its own app po
|
|
|
521
527
|
signs in by itself (`--oauth`, or one you kept) gets a sign-in that only hears its answers instead: it can't ask,
|
|
522
528
|
answer or change anything, and doesn't show up as an assistant. They're stored in `~/.config/pendingyou/credentials.json`
|
|
523
529
|
(readable only by you, in a folder with a `.gitignore` of `*` so a dotfiles repository never takes it), refresh
|
|
524
|
-
themselves,
|
|
525
|
-
|
|
530
|
+
themselves, last 30 days from their last use (0.30.0: each refresh proves this computer's key, and Pending You renews
|
|
531
|
+
the sign-in from then), and end once unused that long, or on `logout`. The headers helper hands an agent's token only
|
|
532
|
+
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
|
|
526
533
|
sign-in each agent's connection used, so signing in again keeps it. `machine.json` (0.18.0) holds this computer's key
|
|
527
534
|
pair, private like the rest; only its proofs leave the computer, each for one sign-in, once. `apps/<app>.json` (0.21.0)
|
|
528
535
|
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
|
@@ -8,6 +8,7 @@ import { configDir, readJson, withLock, writeWhole } from "../files.js";
|
|
|
8
8
|
import { APPROVED_DRAFT_NOTICE, approvesDraft, clip, delegatedOf, handledOf, isClosed, noticeOf, } from "../format.js";
|
|
9
9
|
import { chooseThread, goesBy, HANDED_FOR_MS, handedCard, handedMessage, namesAnotherComputer, } from "../handed.js";
|
|
10
10
|
import { clearHerdr, herdrTarget, isUrgency, PANE_ID, reportCommand, writeHerdr, } from "../herdr.js";
|
|
11
|
+
import { linkSoon } from "../link.js";
|
|
11
12
|
import { macName, tailscaleName } from "../remote.js";
|
|
12
13
|
import { claimHanded, markHanded, momentOf, releaseHanded, wasHanded } from "../state.js";
|
|
13
14
|
import { findCodex } from "./codex-app.js";
|
|
@@ -563,6 +564,7 @@ export async function listen(io, options) {
|
|
|
563
564
|
const lease = await takeLease(io, app, thread);
|
|
564
565
|
if (!lease)
|
|
565
566
|
return 0;
|
|
567
|
+
await linkSoon(io, origin);
|
|
566
568
|
const controller = new AbortController();
|
|
567
569
|
const stop = () => controller.abort();
|
|
568
570
|
if (io.signal.aborted)
|
package/dist/args.js
CHANGED
|
@@ -3,7 +3,7 @@ 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
5
|
export const APP_SUBS = ['login', 'logout', 'status'];
|
|
6
|
-
export const MACHINE_SUBS = ['status', 'attest', 'rotate'];
|
|
6
|
+
export const MACHINE_SUBS = ['status', 'attest', 'rotate', 'link'];
|
|
7
7
|
export const HERDR_SUBS = [
|
|
8
8
|
'report',
|
|
9
9
|
'next',
|
|
@@ -152,7 +152,7 @@ const FLAGS = {
|
|
|
152
152
|
wait: false,
|
|
153
153
|
ensure: false,
|
|
154
154
|
},
|
|
155
|
-
machine: { origin: true, 'client-id': true, cnf: true, name: true, yes: false },
|
|
155
|
+
machine: { origin: true, 'client-id': true, cnf: true, name: true, yes: false, quiet: false },
|
|
156
156
|
app: {
|
|
157
157
|
origin: true,
|
|
158
158
|
'client-id': true,
|
|
@@ -560,6 +560,7 @@ function machineCommand(origin, positional, flags, machine) {
|
|
|
560
560
|
status: [],
|
|
561
561
|
attest: ['client-id', 'cnf', 'name'],
|
|
562
562
|
rotate: ['yes'],
|
|
563
|
+
link: ['quiet'],
|
|
563
564
|
};
|
|
564
565
|
for (const key of flags.keys())
|
|
565
566
|
if (key !== 'origin' && !allowed[sub].includes(key))
|
|
@@ -568,6 +569,8 @@ function machineCommand(origin, positional, flags, machine) {
|
|
|
568
569
|
return { name: 'machine', origin, sub };
|
|
569
570
|
if (sub === 'rotate')
|
|
570
571
|
return { name: 'machine', origin, sub, yes: flags.has('yes') };
|
|
572
|
+
if (sub === 'link')
|
|
573
|
+
return { name: 'machine', origin, sub, quiet: flags.has('quiet') };
|
|
571
574
|
const clientId = flags.get('client-id');
|
|
572
575
|
if (typeof clientId !== 'string' || !CLIENT_ID.test(clientId))
|
|
573
576
|
throw new UsageError('machine attest needs the client the app signs in with, like: --client-id <its client id>');
|
|
@@ -671,7 +674,7 @@ export const USAGE = `pendingyou: Pending You for your coding agents (Claude Cod
|
|
|
671
674
|
npx pendingyou herdr doctor Pending You in Herdr: the badges on your agents' panes, the plugin, its
|
|
672
675
|
toasts and its sidebar row. The plugin runs herdr next, open, setup,
|
|
673
676
|
view and unconfigure
|
|
674
|
-
npx pendingyou machine status | attest | rotate
|
|
677
|
+
npx pendingyou machine status | attest | rotate | link
|
|
675
678
|
This computer's key, which Pending You knows it by (init makes it; it
|
|
676
679
|
never leaves the computer): whether Pending You knows each agent's
|
|
677
680
|
connection by it, a proof of it for an app's own sign-in
|
package/dist/hold.js
CHANGED
|
@@ -4,6 +4,7 @@ import { readPiManifest } from "./apps/pi.js";
|
|
|
4
4
|
import { DEFAULT_ORIGIN, DEFAULT_TIMEOUT_MS } from "./args.js";
|
|
5
5
|
import { readCredential } from "./credentials.js";
|
|
6
6
|
import { describe } from "./format.js";
|
|
7
|
+
import { linkSoon } from "./link.js";
|
|
7
8
|
import { markHanded, momentOf } from "./state.js";
|
|
8
9
|
export const LONG_POLL_SECONDS = 25;
|
|
9
10
|
const PATIENT_AFTER_MS = 30 * 60 * 1000;
|
|
@@ -57,6 +58,7 @@ export async function hold(io, options) {
|
|
|
57
58
|
};
|
|
58
59
|
if (!(await readCredential(io, origin)))
|
|
59
60
|
return signInNeeded(false);
|
|
61
|
+
await linkSoon(io, origin);
|
|
60
62
|
let failures = 0;
|
|
61
63
|
for (;;) {
|
|
62
64
|
if (io.signal.aborted) {
|
package/dist/index.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
export { getJson, SignInNeeded, Unavailable } from "./api.js";
|
|
2
2
|
export { DEFAULT_ORIGIN, parseArgs } from "./args.js";
|
|
3
3
|
export { readCredential, updateCredential } from "./credentials.js";
|
|
4
|
+
export { appClients, clientsIn, LINK_PATH, linkClients, machineLink } from "./link.js";
|
|
4
5
|
export { attest, ensureMachine, machinePath, proveConnections, readMachine, thumbprintOf, } from "./machine.js";
|
|
5
6
|
export { main } from "./main.js";
|
|
6
7
|
export { CLIENT_NAME, deviceLink, discover, register, revoke, SCOPE, signIn, signInWithDevice, startDevice, waitForDevices, } from "./oauth.js";
|
package/dist/init.js
CHANGED
|
@@ -5,11 +5,13 @@ import { hookCommand } from "./claude.js";
|
|
|
5
5
|
import { readCredential, saveConnectionClient } from "./credentials.js";
|
|
6
6
|
import { hookLine, originArgs } from "./hooks.js";
|
|
7
7
|
import { installCli, pruneCli, removeCli } from "./install.js";
|
|
8
|
+
import { linkClients } from "./link.js";
|
|
8
9
|
import { keyForSignIn, proveConnections } from "./machine.js";
|
|
9
10
|
import { headlessReason, helperCommand, removeHelper, writeHelper } from "./remote.js";
|
|
10
11
|
import { FINISH_SAY } from "./setup.js";
|
|
11
12
|
import { removeShim, shimPath, writeShim } from "./shim.js";
|
|
12
13
|
import { checkSignIn, logout, signInApps, titleFrom, titleHere } from "./signin.js";
|
|
14
|
+
import { markLinked } from "./state.js";
|
|
13
15
|
import { trustHooks } from "./trust.js";
|
|
14
16
|
import { VERSION } from "./version.js";
|
|
15
17
|
const line = (step) => ` ${step.ok ? 'ok ' : 'check '} ${step.text}\n`;
|
|
@@ -155,8 +157,13 @@ export async function init(io, options) {
|
|
|
155
157
|
...(options.machine ? { machine: options.machine } : {}),
|
|
156
158
|
key,
|
|
157
159
|
});
|
|
158
|
-
if (key)
|
|
160
|
+
if (key) {
|
|
159
161
|
await proveConnections(io, options.origin, [...signed].flatMap(([app, how]) => (how === 'already' ? [app] : [])), key);
|
|
162
|
+
const linked = await linkClients(io, options.origin, key);
|
|
163
|
+
await markLinked(io, options.origin).catch(() => { });
|
|
164
|
+
if (linked.joined)
|
|
165
|
+
io.out(`Linked ${linked.joined === 1 ? 'a sign-in' : `${linked.joined} sign-ins`} your apps made on this computer to it, so Pending You names them by this computer.\n`);
|
|
166
|
+
}
|
|
160
167
|
}
|
|
161
168
|
else
|
|
162
169
|
io.out(going.length === 1
|
package/dist/link.js
ADDED
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
import { sendJson } from "./api.js";
|
|
4
|
+
import { APP_IDS } from "./apps/ids.js";
|
|
5
|
+
import { connectionMachine, readCredential } from "./credentials.js";
|
|
6
|
+
import { claudeDir, readJson } from "./files.js";
|
|
7
|
+
import { attest, clockOffset, proveConnections, readMachine } from "./machine.js";
|
|
8
|
+
import { machineOf } from "./remote.js";
|
|
9
|
+
import { linkDue, markLinked } from "./state.js";
|
|
10
|
+
export const LINK_PATH = '/mcp/cli/machine/link';
|
|
11
|
+
export const LINK_EVERY_MS = 6 * 60 * 60 * 1000;
|
|
12
|
+
const MAX_LINKS = 8;
|
|
13
|
+
const CLIENT_ID = /^[\x21-\x7e]{1,300}$/;
|
|
14
|
+
const isRecord = (value) => typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
15
|
+
const sameUrl = (a, b) => a.replace(/\/+$/, '') === b.replace(/\/+$/, '');
|
|
16
|
+
export function clientsIn(entries, url) {
|
|
17
|
+
if (!isRecord(entries))
|
|
18
|
+
return [];
|
|
19
|
+
const found = [];
|
|
20
|
+
for (const entry of Object.values(entries)) {
|
|
21
|
+
if (!isRecord(entry))
|
|
22
|
+
continue;
|
|
23
|
+
const address = entry.serverUrl ?? entry.server_url ?? entry.url;
|
|
24
|
+
const info = isRecord(entry.clientInfo) ? entry.clientInfo : {};
|
|
25
|
+
const client = entry.clientId ?? entry.client_id ?? info.clientId ?? info.client_id;
|
|
26
|
+
if (typeof address !== 'string' || !sameUrl(address, url))
|
|
27
|
+
continue;
|
|
28
|
+
if (typeof client === 'string' && CLIENT_ID.test(client) && !found.includes(client))
|
|
29
|
+
found.push(client);
|
|
30
|
+
}
|
|
31
|
+
return found;
|
|
32
|
+
}
|
|
33
|
+
async function claudeCodeStore(io) {
|
|
34
|
+
if (io.platform === 'darwin') {
|
|
35
|
+
const custom = io.env.CLAUDE_SECURESTORAGE_CONFIG_DIR ?? io.env.CLAUDE_CONFIG_DIR;
|
|
36
|
+
const suffix = custom
|
|
37
|
+
? `-${createHash('sha256').update(custom.normalize('NFC')).digest('hex').slice(0, 8)}`
|
|
38
|
+
: '';
|
|
39
|
+
const user = io.env.USER || io.env.LOGNAME || '';
|
|
40
|
+
const account = /^[a-zA-Z0-9._-]+$/.test(user) ? user : 'claude-code-user';
|
|
41
|
+
const found = await io
|
|
42
|
+
.run('security', ['find-generic-password', '-a', account, '-w', '-s', `Claude Code-credentials${suffix}`], 5000)
|
|
43
|
+
.catch(() => null);
|
|
44
|
+
if (found?.code === 0 && found.stdout.trim()) {
|
|
45
|
+
try {
|
|
46
|
+
return JSON.parse(found.stdout.trim());
|
|
47
|
+
}
|
|
48
|
+
catch { }
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
return readJson(join(claudeDir(io), '.credentials.json')).catch(() => null);
|
|
52
|
+
}
|
|
53
|
+
export async function appClients(io, origin) {
|
|
54
|
+
const url = `${origin}/mcp`;
|
|
55
|
+
const found = [];
|
|
56
|
+
const add = (app, clients) => {
|
|
57
|
+
for (const clientId of clients)
|
|
58
|
+
if (!found.some((each) => each.clientId === clientId))
|
|
59
|
+
found.push({ app, clientId });
|
|
60
|
+
};
|
|
61
|
+
const claude = await claudeCodeStore(io);
|
|
62
|
+
add('claude-code', clientsIn(isRecord(claude) ? claude.mcpOAuth : null, url));
|
|
63
|
+
const codexHome = io.env.CODEX_HOME || join(io.home, '.codex');
|
|
64
|
+
add('codex', clientsIn(await readJson(join(codexHome, '.credentials.json')).catch(() => null), url));
|
|
65
|
+
const data = io.env.XDG_DATA_HOME || join(io.home, '.local', 'share');
|
|
66
|
+
add('opencode', clientsIn(await readJson(join(data, 'opencode', 'mcp-auth.json')).catch(() => null), url));
|
|
67
|
+
return found.slice(0, MAX_LINKS);
|
|
68
|
+
}
|
|
69
|
+
export async function linkClients(io, origin, machine) {
|
|
70
|
+
const clients = await appClients(io, origin);
|
|
71
|
+
const none = { found: clients.length, joined: 0, merged: 0, failed: false };
|
|
72
|
+
if (clients.length === 0)
|
|
73
|
+
return none;
|
|
74
|
+
let signedIn = null;
|
|
75
|
+
for (const app of APP_IDS)
|
|
76
|
+
if (await readCredential(io, origin, app)) {
|
|
77
|
+
signedIn = app;
|
|
78
|
+
break;
|
|
79
|
+
}
|
|
80
|
+
if (!signedIn)
|
|
81
|
+
return { ...none, failed: true };
|
|
82
|
+
try {
|
|
83
|
+
const name = (await connectionMachine(io, origin, signedIn)) ?? machineOf(io.host);
|
|
84
|
+
const offset = await clockOffset(io, origin);
|
|
85
|
+
const links = [];
|
|
86
|
+
for (const { clientId } of clients)
|
|
87
|
+
links.push({
|
|
88
|
+
clientId,
|
|
89
|
+
attestation: await attest(machine, { aud: `${origin}${LINK_PATH}`, client_id: clientId, name }, io.now() + offset),
|
|
90
|
+
});
|
|
91
|
+
const { status, body } = await sendJson(io, origin, LINK_PATH, { links }, 15_000, { app: signedIn });
|
|
92
|
+
if (status !== 200)
|
|
93
|
+
return { ...none, failed: true };
|
|
94
|
+
return {
|
|
95
|
+
found: clients.length,
|
|
96
|
+
joined: (body.links ?? []).filter((each) => each.connection === 'joined').length,
|
|
97
|
+
merged: typeof body.merged === 'number' ? body.merged : 0,
|
|
98
|
+
failed: false,
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
catch {
|
|
102
|
+
return { ...none, failed: true };
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
export async function machineLink(io, command) {
|
|
106
|
+
const say = (text) => {
|
|
107
|
+
if (!command.quiet)
|
|
108
|
+
io.out(text);
|
|
109
|
+
};
|
|
110
|
+
const machine = await readMachine(io).catch(() => null);
|
|
111
|
+
if (!machine) {
|
|
112
|
+
say('This computer has no key yet. npx -y pendingyou@latest init makes one, and links its sign-ins.\n');
|
|
113
|
+
return 1;
|
|
114
|
+
}
|
|
115
|
+
await markLinked(io, command.origin).catch(() => { });
|
|
116
|
+
const own = [];
|
|
117
|
+
for (const app of APP_IDS)
|
|
118
|
+
if ((await readCredential(io, command.origin, app))?.kind === 'connection')
|
|
119
|
+
own.push(app);
|
|
120
|
+
const proven = own.length
|
|
121
|
+
? await proveConnections(io, command.origin, own, machine).catch(() => new Map())
|
|
122
|
+
: new Map();
|
|
123
|
+
const linked = await linkClients(io, command.origin, machine);
|
|
124
|
+
const joined = [...proven.values()].filter((each) => each === 'joined').length + linked.joined;
|
|
125
|
+
if (linked.failed && linked.found > 0)
|
|
126
|
+
say(`Found ${linked.found === 1 ? 'a sign-in' : `${linked.found} sign-ins`} of this computer’s apps, but Pending You couldn’t be asked to link ${linked.found === 1 ? 'it' : 'them'} now. Try again later.\n`);
|
|
127
|
+
else if (joined === 0)
|
|
128
|
+
say('Every sign-in on this computer is linked to it already.\n');
|
|
129
|
+
else
|
|
130
|
+
say(`Linked ${joined === 1 ? 'a sign-in' : `${joined} sign-ins`} on this computer to it${linked.merged ? `, and Pending You merged ${linked.merged === 1 ? 'an older one' : `${linked.merged} older ones`} into the one in use` : ''}.\n`);
|
|
131
|
+
return 0;
|
|
132
|
+
}
|
|
133
|
+
export async function linkSoon(io, origin) {
|
|
134
|
+
try {
|
|
135
|
+
if (!(await linkDue(io, origin, LINK_EVERY_MS)))
|
|
136
|
+
return;
|
|
137
|
+
if (!(await readMachine(io)))
|
|
138
|
+
return;
|
|
139
|
+
await markLinked(io, origin);
|
|
140
|
+
io.background(['machine', 'link', '--quiet', '--origin', origin]);
|
|
141
|
+
}
|
|
142
|
+
catch { }
|
|
143
|
+
}
|
package/dist/machine-command.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { APP_NAMES } from "./apps/ids.js";
|
|
2
2
|
import { readCredential, signedInSlots } from "./credentials.js";
|
|
3
3
|
import { originArgs } from "./hooks.js";
|
|
4
|
+
import { machineLink } from "./link.js";
|
|
4
5
|
import { attest, clockOffset, machinePath, readMachine, rotateMachine } from "./machine.js";
|
|
5
6
|
import { computerName } from "./remote.js";
|
|
6
7
|
import { checkSignIn } from "./signin.js";
|
|
@@ -97,5 +98,7 @@ export function machine(io, command) {
|
|
|
97
98
|
return machineAttest(io, command);
|
|
98
99
|
case 'rotate':
|
|
99
100
|
return machineRotate(io, command);
|
|
101
|
+
case 'link':
|
|
102
|
+
return machineLink(io, command);
|
|
100
103
|
}
|
|
101
104
|
}
|
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/pickup.js
CHANGED
|
@@ -8,6 +8,7 @@ import { clip, delegatedOf, describe, HANDED_PREFIX, isHandedWake, } from "./for
|
|
|
8
8
|
import { goesBy, HANDED_GRACE_MS, namesAnotherComputer } from "./handed.js";
|
|
9
9
|
import { herdrTarget, reportCommand } from "./herdr.js";
|
|
10
10
|
import { holdCommand } from "./hold.js";
|
|
11
|
+
import { linkSoon } from "./link.js";
|
|
11
12
|
import { unwokenLine } from "./loaded.js";
|
|
12
13
|
import { stuckLine } from "./mcp-health.js";
|
|
13
14
|
import { isPermissionCard, isPromptApp, POSTED_MAX, sessionName, sessionPosted, settlePrompts, } from "./permission.js";
|
|
@@ -147,6 +148,7 @@ export async function pickup(io, options, hook = { fallback: '' }) {
|
|
|
147
148
|
try {
|
|
148
149
|
const input = await hookInput(io);
|
|
149
150
|
const { cwd } = input;
|
|
151
|
+
await linkSoon(io, origin);
|
|
150
152
|
if (mode === 'handoff' && isPromptApp(app))
|
|
151
153
|
await settlePrompts(io, { session: input.session, event: 'prompt' });
|
|
152
154
|
const known = mode === 'handoff' && app === DEFAULT_APP && input.session
|
package/dist/remote.js
CHANGED
|
@@ -5,6 +5,7 @@ import { APP_NAMES, DEFAULT_APP } from "./apps/ids.js";
|
|
|
5
5
|
import { cleanName, DEFAULT_ORIGIN } from "./args.js";
|
|
6
6
|
import { readCredential } from "./credentials.js";
|
|
7
7
|
import { configDir, writeWhole } from "./files.js";
|
|
8
|
+
import { linkSoon } from "./link.js";
|
|
8
9
|
import { recordHelper } from "./mcp-health.js";
|
|
9
10
|
import { VERSION } from "./version.js";
|
|
10
11
|
export const HELPER = 'pendingyou-mcp-headers';
|
|
@@ -261,6 +262,7 @@ export async function mcpHeaders(io, options) {
|
|
|
261
262
|
const token = await helperToken(io, options.origin, HELPER_WAIT_MS, app);
|
|
262
263
|
headers(token);
|
|
263
264
|
await recordHelper(io, options.origin, app, 'ok');
|
|
265
|
+
await linkSoon(io, options.origin);
|
|
264
266
|
return 0;
|
|
265
267
|
}
|
|
266
268
|
catch (error) {
|
package/dist/state.js
CHANGED
|
@@ -12,6 +12,7 @@ async function load(io) {
|
|
|
12
12
|
handed: file?.handed && typeof file.handed === 'object' ? file.handed : {},
|
|
13
13
|
...(file?.reminded && typeof file.reminded === 'object' ? { reminded: file.reminded } : {}),
|
|
14
14
|
...(file?.unwoken && typeof file.unwoken === 'object' ? { unwoken: file.unwoken } : {}),
|
|
15
|
+
...(file?.linked && typeof file.linked === 'object' ? { linked: file.linked } : {}),
|
|
15
16
|
};
|
|
16
17
|
}
|
|
17
18
|
async function change(io, edit) {
|
|
@@ -85,3 +86,12 @@ export async function claimUnwoken(io, session) {
|
|
|
85
86
|
});
|
|
86
87
|
return claimed;
|
|
87
88
|
}
|
|
89
|
+
export async function linkDue(io, origin, every) {
|
|
90
|
+
const at = (await load(io)).linked?.[origin];
|
|
91
|
+
return typeof at !== 'number' || io.now() - at >= every || at > io.now();
|
|
92
|
+
}
|
|
93
|
+
export function markLinked(io, origin) {
|
|
94
|
+
return change(io, (state) => {
|
|
95
|
+
state.linked = { ...state.linked, [origin]: io.now() };
|
|
96
|
+
});
|
|
97
|
+
}
|
package/dist/version.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export const VERSION = '0.
|
|
1
|
+
export const VERSION = '0.30.0';
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pendingyou-wake",
|
|
3
3
|
"displayName": "Pending You",
|
|
4
|
-
"version": "2.44.
|
|
4
|
+
"version": "2.44.1",
|
|
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.44.
|
|
7
|
+
Generated from recordplane/pendingyou (packages/claude-plugin/src/wake) with Pending You’s guide 2.44.1; send changes there.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pendingyou",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.30.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",
|