pendingyou 0.20.0 → 0.21.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 +42 -0
- package/README.md +35 -3
- package/dist/app-login.js +304 -0
- package/dist/args.js +78 -0
- package/dist/herdr/answer.js +751 -0
- package/dist/herdr/command.js +47 -3
- package/dist/herdr/panes.js +3 -3
- package/dist/herdr/popup.js +18 -1
- package/dist/herdr/presence.js +22 -0
- package/dist/herdr/queue.js +488 -0
- package/dist/herdr/watch.js +96 -1
- package/dist/io.js +1 -0
- package/dist/main.js +3 -2
- package/dist/oauth.js +1 -1
- package/dist/pickup.js +7 -3
- package/dist/sdk/changes.js +56 -0
- package/dist/sdk/client.js +242 -0
- package/dist/sdk/device.js +98 -0
- package/dist/sdk/dpop.js +68 -0
- package/dist/sdk/errors.js +32 -0
- package/dist/sdk/index.js +8 -0
- package/dist/sdk/node.js +121 -0
- package/dist/sdk/oauth.js +48 -0
- package/dist/sdk/openapi.generated.js +1 -0
- package/dist/sdk/signout.js +23 -0
- package/dist/sdk/store.js +34 -0
- 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 +8 -1
- package/package.json +3 -2
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,47 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.21.0 (2026-10-06)
|
|
4
|
+
|
|
5
|
+
- Answer from Herdr. The Pending You plugin for Herdr (0.2) can now answer your cards from its popup, once you sign
|
|
6
|
+
Herdr in. The command line itself still never answers anything: the plugin's popup does, with Herdr's own sign-in
|
|
7
|
+
and its own key, only when you press a key in it, and never a high-stakes card.
|
|
8
|
+
- `pendingyou app login herdr` (also the plugin's setup) signs Herdr in to Pending You with a code, as `login --device`
|
|
9
|
+
does: your phone or browser shows one card naming Herdr and this computer, with a tick for all your cards (off: it
|
|
10
|
+
sees this computer's agents' cards only). This computer's key (0.18.0) vouches for the sign-in, naming Herdr and the
|
|
11
|
+
new sign-in's own key, so run `npx -y pendingyou@latest init` here first. The sign-in and its private key are kept
|
|
12
|
+
in `~/.config/pendingyou/apps/herdr.json` (readable only by you), which none of the agents' hooks, helpers or
|
|
13
|
+
`hold` read; its tokens are useless without that key (DPoP). Signing in again on the same computer replaces it.
|
|
14
|
+
`pendingyou app status` says what each app signed in here may do and until when (and warns in its last 3 days);
|
|
15
|
+
`pendingyou app logout herdr` ends it at Pending You and removes it. Another registered app signs in with
|
|
16
|
+
`--client-id` (and `--scope`).
|
|
17
|
+
- The popup (`prefix+y`), signed in: your cards from Pending You, the chosen one open with what it asks, how many wait
|
|
18
|
+
elsewhere, and a key per decision: 1–9 for a choice (and Enter to send several), y or n to approve or decline, Enter
|
|
19
|
+
to write an answer, y or n for a step done or not, Tab between a grouped card's questions. r writes to the agent, l
|
|
20
|
+
puts the card in Later, d hands it to another of your agents, u undoes for 5 seconds, t goes to the agent's pane
|
|
21
|
+
(from its badges, or its session running in this Herdr), o opens the card in Pending You (high-stakes cards are
|
|
22
|
+
answered there), a opens all your cards. It follows Pending You's change feed while it's open. Not signed in, it's
|
|
23
|
+
the read-only list it was, with a line saying how to sign in. `herdr open --demo` shows made-up cards, for
|
|
24
|
+
screenshots.
|
|
25
|
+
- Your phone stays quiet while you're active in Herdr (moving between panes, tabs and workspaces, or pressing keys in
|
|
26
|
+
the popup), for the cards Herdr can show, as it does while Pending You is open on your desk. The plugin's watcher
|
|
27
|
+
also says in a toast when Herdr's sign-in has ended, and in its last 3 days that it's about to.
|
|
28
|
+
- The plugin's `unconfigure` signs Herdr out too. `doctor` says whether it's signed in.
|
|
29
|
+
- The plugin 0.2 runs the command line `init` put here only once that one can answer (`pendingyou app` exists);
|
|
30
|
+
until then it runs 0.21.0 through npx. Run `npx pendingyou@latest init` once to switch.
|
|
31
|
+
- Answering works once Pending You turns on its person API for its own apps; until then sign-in says Pending You
|
|
32
|
+
doesn't let Herdr sign in yet.
|
|
33
|
+
|
|
34
|
+
## 0.20.1 (2026-10-06)
|
|
35
|
+
|
|
36
|
+
- An older answer is no longer hidden behind newer ones. The session-start and next-message hooks asked for a folder's
|
|
37
|
+
five newest answers and left out the ones already handed over, so a session with five newer answers it hadn't
|
|
38
|
+
acknowledged yet never heard an older one, even on the person's own message (seen 2026-10-06: two answers waited
|
|
39
|
+
over an hour). They now ask for 20 and hand over the newest five not handed over yet; the rest come with the next
|
|
40
|
+
message.
|
|
41
|
+
- The wake mod (guide 2.33.1) makes room in a session's list of 50 cards with the answers its agent already heard,
|
|
42
|
+
never a card still waiting on you while there's one of those: a session that ran many agents lost its oldest open
|
|
43
|
+
cards, and their answers were never told.
|
|
44
|
+
|
|
3
45
|
## 0.20.0 (2026-10-06)
|
|
4
46
|
|
|
5
47
|
- “I’ll handle it”: a card you ended yourself in Pending You says so, and tells the agent to stand down. An answer can
|
package/README.md
CHANGED
|
@@ -316,8 +316,30 @@ them after 15 minutes unless told again.
|
|
|
316
316
|
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
|
|
317
317
|
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
|
|
318
318
|
an Agents view with them first. Its commands are this command line's: `npx pendingyou herdr doctor` says what's set
|
|
319
|
-
up. Its `unconfigure` takes the badges off every pane and stops writing them on this computer
|
|
320
|
-
again.
|
|
319
|
+
up. Its `unconfigure` takes the badges off every pane and stops writing them on this computer (and signs Herdr out);
|
|
320
|
+
its setup turns them on again.
|
|
321
|
+
|
|
322
|
+
### Answering from Herdr (0.21.0)
|
|
323
|
+
|
|
324
|
+
Sign Herdr in, from the plugin's setup or here:
|
|
325
|
+
|
|
326
|
+
```sh
|
|
327
|
+
npx pendingyou app login herdr
|
|
328
|
+
```
|
|
329
|
+
|
|
330
|
+
Your phone or browser shows one card naming Herdr and this computer: press and hold Allow. Herdr then sees the cards
|
|
331
|
+
of this computer's agents (tick "all" on that card for every card of yours), and its popup (`prefix+y`) answers them
|
|
332
|
+
with a key: 1–9 for a choice (Enter sends several), y or n to approve or decline, Enter to write an answer, y or n for
|
|
333
|
+
a step done or not, Tab between a grouped card's questions; r writes to the agent, l puts it in Later, d hands it to
|
|
334
|
+
another of your agents, u undoes for 5 seconds, t goes to the agent's pane, o opens the card in Pending You. A
|
|
335
|
+
high-stakes card (it spends money, can't be undone or goes public) is never answered from Herdr: o opens it where you
|
|
336
|
+
hold to approve. Pending You says on the card and to the agent "Answered in Herdr on build-01". While you're active in
|
|
337
|
+
Herdr (moving between panes, tabs and workspaces, or pressing keys in the popup) your phone stays quiet for those cards,
|
|
338
|
+
as it does while Pending You is open on your desk.
|
|
339
|
+
|
|
340
|
+
`npx pendingyou app status` says what each app signed in here may do and until when (a sign-in lasts 30 days; it warns
|
|
341
|
+
in the last 3), and `npx pendingyou app logout herdr` ends Herdr's sign-in. Settings › Apps and devices in Pending You
|
|
342
|
+
lists it too, with Remove. It needs `init` to have run here first: this computer's key vouches for the sign-in.
|
|
321
343
|
|
|
322
344
|
## How Claude Code hears your answer
|
|
323
345
|
|
|
@@ -472,6 +494,7 @@ its environment. A failing command is tried twice more, then skipped. `--once` s
|
|
|
472
494
|
| `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`) |
|
|
473
495
|
| `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` |
|
|
474
496
|
| `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`) |
|
|
497
|
+
| `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 |
|
|
475
498
|
|
|
476
499
|
Every command takes `--origin` (default `https://www.pendingyou.com`, or `$PENDINGYOU_ORIGIN`). `--app` takes
|
|
477
500
|
`claude-code`, `codex`, `opencode` or `pi` (several with commas where a command sets up or reports several); with none,
|
|
@@ -479,6 +502,12 @@ a command that acts for one agent acts for Claude Code. Options can come before
|
|
|
479
502
|
|
|
480
503
|
## What it can see, and where it keeps things
|
|
481
504
|
|
|
505
|
+
**The command line never answers for you.** None of its commands, hooks or helpers answers, replies to, snoozes or
|
|
506
|
+
hands over a card, and no agent's sign-in can. An app you sign in with `pendingyou app login` (the Pending You plugin
|
|
507
|
+
for Herdr) answers only from its own sign-in and its own key, kept apart in `apps/<app>.json`, which nothing an agent
|
|
508
|
+
runs here reads, and only when you press a key in it; never a high-stakes card, and Pending You labels every answer
|
|
509
|
+
with the app and the computer.
|
|
510
|
+
|
|
482
511
|
Each agent's sign-in is that agent's own connection on Pending You, signed in from this computer: it can post cards,
|
|
483
512
|
change or cancel its own, read the answers to them and reply in their threads, and it can't answer anything for you.
|
|
484
513
|
The hooks use the same sign-in, and hand a session only the cards its own app posted from its folder. An agent that
|
|
@@ -488,7 +517,10 @@ answer or change anything, and doesn't show up as an assistant. They're stored i
|
|
|
488
517
|
themselves, and end after 30 days or on `logout`. The headers helper hands an agent's token only to that agent's
|
|
489
518
|
Pending You (Claude Code says which server it's for; Codex's is the one `init` wrote). `clients.json` remembers which
|
|
490
519
|
sign-in each agent's connection used, so signing in again keeps it. `machine.json` (0.18.0) holds this computer's key
|
|
491
|
-
pair, private like the rest; only its proofs leave the computer, each for one sign-in, once.
|
|
520
|
+
pair, private like the rest; only its proofs leave the computer, each for one sign-in, once. `apps/<app>.json` (0.21.0)
|
|
521
|
+
holds an app's own sign-in and the private key its tokens are bound to (DPoP), private like the rest; the Pending You
|
|
522
|
+
plugin for Herdr also keeps, in Herdr's state folder for it, when you last pressed a key in its popup (a time, nothing
|
|
523
|
+
else).
|
|
492
524
|
|
|
493
525
|
`state.json` beside them remembers which answers were already handed over (ids only) and how many session starts
|
|
494
526
|
reminded an unfinished setup, and when; `permission-cards/` each Claude Code session's (and Codex thread's) prompts
|
|
@@ -0,0 +1,304 @@
|
|
|
1
|
+
import { readdir, rm } from 'node:fs/promises';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
import { ignoreFolder } from "./credentials.js";
|
|
4
|
+
import { PlainError } from "./errors.js";
|
|
5
|
+
import { configDir } from "./files.js";
|
|
6
|
+
import { originArgs } from "./hooks.js";
|
|
7
|
+
import { attest, clockOffset, readMachine } from "./machine.js";
|
|
8
|
+
import { showDevices } from "./oauth.js";
|
|
9
|
+
import { computerName, headlessReason } from "./remote.js";
|
|
10
|
+
import { ApiError, createClient, deviceSignIn, SignedOutError, SignInError, signOut, } from "./sdk/index.js";
|
|
11
|
+
import { fileStore } from "./sdk/node.js";
|
|
12
|
+
import { memoryStore } from "./sdk/store.js";
|
|
13
|
+
export const APP_SCOPES = [
|
|
14
|
+
'cards:read',
|
|
15
|
+
'cards:read:all',
|
|
16
|
+
'cards:answer',
|
|
17
|
+
'cards:reply',
|
|
18
|
+
'cards:later',
|
|
19
|
+
'cards:delegate',
|
|
20
|
+
'assistants:read',
|
|
21
|
+
'presence:desk',
|
|
22
|
+
];
|
|
23
|
+
export const FIRST_PARTY_APPS = {
|
|
24
|
+
herdr: { name: 'Herdr', scopes: APP_SCOPES },
|
|
25
|
+
};
|
|
26
|
+
export const DEFAULT_SCOPES = ['cards:read'];
|
|
27
|
+
export const APP_NAME = /^[a-z0-9][a-z0-9-]{0,39}$/;
|
|
28
|
+
export const RENEW_WITHIN_MS = 3 * 24 * 60 * 60 * 1000;
|
|
29
|
+
export const appsDir = (io) => join(configDir(io), 'apps');
|
|
30
|
+
export const appPath = (io, app) => join(appsDir(io), `${app}.json`);
|
|
31
|
+
export const appStore = (io, app) => fileStore(appPath(io, app));
|
|
32
|
+
export const appLabel = (app) => FIRST_PARTY_APPS[app]?.name ?? app;
|
|
33
|
+
export async function readAppSignIn(io, app) {
|
|
34
|
+
try {
|
|
35
|
+
return await appStore(io, app).load();
|
|
36
|
+
}
|
|
37
|
+
catch {
|
|
38
|
+
throw new PlainError(`${appLabel(app)}’s sign-in (${appPath(io, app)}) can’t be read. Sign in again to replace it: npx pendingyou app login ${app}`);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
export function appClient(io, app) {
|
|
42
|
+
return createClient({
|
|
43
|
+
store: appStore(io, app),
|
|
44
|
+
fetch: io.fetch,
|
|
45
|
+
now: () => io.now(),
|
|
46
|
+
sleep: (ms, signal) => io.sleep(ms, signal),
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
const MAY = {
|
|
50
|
+
'cards:read': 'see your cards',
|
|
51
|
+
'cards:read:all': 'see all your cards',
|
|
52
|
+
'cards:answer': 'answer them',
|
|
53
|
+
'cards:reply': 'write to the assistant that asked',
|
|
54
|
+
'cards:later': 'put them in Later',
|
|
55
|
+
'cards:delegate': 'hand them to another assistant',
|
|
56
|
+
'assistants:read': 'see your assistants',
|
|
57
|
+
'presence:desk': 'keep your phone quiet while you use it',
|
|
58
|
+
};
|
|
59
|
+
const listed = (items) => items.length <= 1 ? (items[0] ?? '') : `${items.slice(0, -1).join(', ')} and ${items.at(-1)}`;
|
|
60
|
+
export function reachWords(me) {
|
|
61
|
+
return me.grant.reach === 'all' || !me.grant.machine
|
|
62
|
+
? 'all your cards'
|
|
63
|
+
: `cards from ${me.grant.machine.name}’s agents`;
|
|
64
|
+
}
|
|
65
|
+
export function signedInAs(app, me) {
|
|
66
|
+
const name = FIRST_PARTY_APPS[app] ? appLabel(app) : (me?.app.name ?? app);
|
|
67
|
+
return me?.grant.machine ? `${name} on ${me.grant.machine.name}` : name;
|
|
68
|
+
}
|
|
69
|
+
function signInWords(error, app) {
|
|
70
|
+
const name = appLabel(app);
|
|
71
|
+
switch (error.code) {
|
|
72
|
+
case 'access_denied':
|
|
73
|
+
return `You pressed Cancel, so ${name} isn’t signed in.`;
|
|
74
|
+
case 'expired_token':
|
|
75
|
+
return 'The code ran out. Run the command again for a new one.';
|
|
76
|
+
case 'invalid_client':
|
|
77
|
+
return FIRST_PARTY_APPS[app]
|
|
78
|
+
? `Pending You doesn’t let ${name} sign in yet.`
|
|
79
|
+
: 'Pending You doesn’t know that app, or it doesn’t sign in with a code.';
|
|
80
|
+
case 'invalid_request':
|
|
81
|
+
return `Pending You didn’t take this computer’s key. Run npx -y pendingyou@latest init, then try again.`;
|
|
82
|
+
case 'invalid_scope':
|
|
83
|
+
return `Pending You doesn’t let ${name} ask for that.`;
|
|
84
|
+
case 'slow_down':
|
|
85
|
+
return 'Too many sign-ins from here. Wait 10 minutes, then try again.';
|
|
86
|
+
case 'aborted':
|
|
87
|
+
return 'Signing in was stopped.';
|
|
88
|
+
default:
|
|
89
|
+
return error.message;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
export async function signInApp(io, options) {
|
|
93
|
+
const { app, origin } = options;
|
|
94
|
+
const name = appLabel(app);
|
|
95
|
+
const firstParty = FIRST_PARTY_APPS[app];
|
|
96
|
+
if (!firstParty && !options.clientId)
|
|
97
|
+
return {
|
|
98
|
+
ok: false,
|
|
99
|
+
said: `Name ${app}’s client with --client-id, or sign in Pending You’s own: ${Object.keys(FIRST_PARTY_APPS).join(', ')}.`,
|
|
100
|
+
};
|
|
101
|
+
const machine = await readMachine(io);
|
|
102
|
+
if (!machine)
|
|
103
|
+
return {
|
|
104
|
+
ok: false,
|
|
105
|
+
said: `This computer isn’t set up with Pending You yet: run npx -y pendingyou@latest init${originArgs(origin)} first, then sign ${name} in.`,
|
|
106
|
+
};
|
|
107
|
+
const computer = (await computerName(io, options.machine)).name;
|
|
108
|
+
const offset = await clockOffset(io, origin);
|
|
109
|
+
const before = await readAppSignIn(io, app).catch(() => null);
|
|
110
|
+
await ignoreFolder(io).catch(() => { });
|
|
111
|
+
let started;
|
|
112
|
+
try {
|
|
113
|
+
started = await deviceSignIn({
|
|
114
|
+
origin,
|
|
115
|
+
...(options.clientId ? { clientId: options.clientId } : { app }),
|
|
116
|
+
scopes: options.scopes ?? firstParty?.scopes ?? DEFAULT_SCOPES,
|
|
117
|
+
machine: computer,
|
|
118
|
+
attest: (request) => attest(machine, {
|
|
119
|
+
aud: request.audience,
|
|
120
|
+
...(request.clientId ? { client_id: request.clientId } : { app }),
|
|
121
|
+
name: computer,
|
|
122
|
+
cnf: { jkt: request.jkt },
|
|
123
|
+
}, io.now() + offset),
|
|
124
|
+
store: appStore(io, app),
|
|
125
|
+
fetch: io.fetch,
|
|
126
|
+
now: () => io.now(),
|
|
127
|
+
sleep: (ms, signal) => io.sleep(ms, signal),
|
|
128
|
+
signal: io.signal,
|
|
129
|
+
});
|
|
130
|
+
}
|
|
131
|
+
catch (error) {
|
|
132
|
+
if (error instanceof SignInError)
|
|
133
|
+
return { ok: false, said: signInWords(error, app) };
|
|
134
|
+
return { ok: false, said: `Pending You at ${origin} couldn’t be reached. Try again.` };
|
|
135
|
+
}
|
|
136
|
+
io.err(`Signing ${name} in on ${computer}. Once you allow it, ${name} can answer your cards when you press a key in it; high-stakes cards still open in Pending You.\n`);
|
|
137
|
+
await showDevices(io, [
|
|
138
|
+
{
|
|
139
|
+
flow: {
|
|
140
|
+
page: started.verificationUri,
|
|
141
|
+
direct: started.verificationUriComplete,
|
|
142
|
+
userCode: started.userCode,
|
|
143
|
+
},
|
|
144
|
+
},
|
|
145
|
+
], options.open);
|
|
146
|
+
try {
|
|
147
|
+
await started.signedIn;
|
|
148
|
+
}
|
|
149
|
+
catch (error) {
|
|
150
|
+
if (error instanceof SignInError)
|
|
151
|
+
return { ok: false, said: signInWords(error, app) };
|
|
152
|
+
return { ok: false, said: `Pending You at ${origin} couldn’t be reached. Try again.` };
|
|
153
|
+
}
|
|
154
|
+
if (before && before.origin !== origin)
|
|
155
|
+
await signOut({ store: memoryStore(before), fetch: io.fetch }).catch(() => 'unreachable');
|
|
156
|
+
const me = await appClient(io, app)
|
|
157
|
+
.me()
|
|
158
|
+
.catch(() => null);
|
|
159
|
+
return { ok: true, me };
|
|
160
|
+
}
|
|
161
|
+
async function login(io, command) {
|
|
162
|
+
const headless = headlessReason(io);
|
|
163
|
+
const open = command.browser && (command.device === false || (command.device === null && !headless));
|
|
164
|
+
const result = await signInApp(io, {
|
|
165
|
+
app: command.app,
|
|
166
|
+
origin: command.origin,
|
|
167
|
+
...(command.clientId ? { clientId: command.clientId } : {}),
|
|
168
|
+
...(command.machine ? { machine: command.machine } : {}),
|
|
169
|
+
...(command.scopes ? { scopes: command.scopes } : {}),
|
|
170
|
+
open,
|
|
171
|
+
});
|
|
172
|
+
if (!result.ok) {
|
|
173
|
+
io.err(`pendingyou: ${result.said}\n`);
|
|
174
|
+
return 1;
|
|
175
|
+
}
|
|
176
|
+
io.out(`${signedInWords(io, command.app, result.me)}\n`);
|
|
177
|
+
return 0;
|
|
178
|
+
}
|
|
179
|
+
export function signedInWords(io, app, me) {
|
|
180
|
+
const name = appLabel(app);
|
|
181
|
+
const answers = me ? me.grant.scopes.includes('cards:answer') : true;
|
|
182
|
+
return [
|
|
183
|
+
me
|
|
184
|
+
? `${signedInAs(app, me)} is signed in: it ${answers ? 'answers' : 'sees'} ${reachWords(me)}${answers ? ', only when you press a key in it' : ''}.`
|
|
185
|
+
: `${name} is signed in.`,
|
|
186
|
+
`Its sign-in is in ${appPath(io, app)}, with a key that never leaves this computer.`,
|
|
187
|
+
`To remove it: npx pendingyou app logout ${app}, or Settings › Apps and devices in Pending You.`,
|
|
188
|
+
].join('\n');
|
|
189
|
+
}
|
|
190
|
+
export async function signOutApp(io, app) {
|
|
191
|
+
const stored = await readAppSignIn(io, app).catch(() => undefined);
|
|
192
|
+
const path = appPath(io, app);
|
|
193
|
+
if (!stored) {
|
|
194
|
+
const there = await rm(path).then(() => true, () => false);
|
|
195
|
+
return { said: there ? 'unreadable' : 'none', origin: null };
|
|
196
|
+
}
|
|
197
|
+
const said = await signOut({ store: memoryStore(stored), fetch: io.fetch });
|
|
198
|
+
await rm(path, { force: true });
|
|
199
|
+
return { said, origin: stored.origin };
|
|
200
|
+
}
|
|
201
|
+
async function logout(io, app) {
|
|
202
|
+
const { said, origin } = await signOutApp(io, app);
|
|
203
|
+
const name = appLabel(app);
|
|
204
|
+
io.out(said === 'none'
|
|
205
|
+
? `${name} wasn’t signed in here.\n`
|
|
206
|
+
: said === 'signed-out'
|
|
207
|
+
? `Signed ${name} out of Pending You${origin ? ` at ${origin}` : ''}.\n`
|
|
208
|
+
: said === 'unreadable'
|
|
209
|
+
? `Removed ${name}’s sign-in here, which couldn’t be read: remove it in Settings › Apps and devices too, or it ends by itself within 30 days.\n`
|
|
210
|
+
: `Signed ${name} out on this computer. Pending You couldn’t be reached to end the sign-in there: remove it in Settings › Apps and devices, or it ends by itself within 30 days.\n`);
|
|
211
|
+
return 0;
|
|
212
|
+
}
|
|
213
|
+
export async function signedInApps(io) {
|
|
214
|
+
const names = await readdir(appsDir(io)).catch(() => []);
|
|
215
|
+
return names
|
|
216
|
+
.filter((name) => name.endsWith('.json'))
|
|
217
|
+
.map((name) => name.slice(0, -'.json'.length))
|
|
218
|
+
.filter((name) => APP_NAME.test(name))
|
|
219
|
+
.sort();
|
|
220
|
+
}
|
|
221
|
+
export async function appStatusLines(io, app) {
|
|
222
|
+
const name = appLabel(app);
|
|
223
|
+
let stored;
|
|
224
|
+
try {
|
|
225
|
+
stored = await readAppSignIn(io, app);
|
|
226
|
+
}
|
|
227
|
+
catch (error) {
|
|
228
|
+
return { ok: false, lines: [`${name}: ${error.message}`] };
|
|
229
|
+
}
|
|
230
|
+
if (!stored)
|
|
231
|
+
return {
|
|
232
|
+
ok: false,
|
|
233
|
+
lines: [`${name}: not signed in here. To sign it in: npx pendingyou app login ${app}`],
|
|
234
|
+
};
|
|
235
|
+
const again = `npx pendingyou app login ${app}${originArgs(stored.origin)}`;
|
|
236
|
+
let me;
|
|
237
|
+
try {
|
|
238
|
+
me = await appClient(io, app).me();
|
|
239
|
+
}
|
|
240
|
+
catch (error) {
|
|
241
|
+
if (error instanceof SignedOutError)
|
|
242
|
+
return {
|
|
243
|
+
ok: false,
|
|
244
|
+
lines: [
|
|
245
|
+
`${name}: signed out at ${stored.origin} (removed in Settings, replaced by another sign-in, or ended). To sign in again: ${again}`,
|
|
246
|
+
],
|
|
247
|
+
};
|
|
248
|
+
if (error instanceof ApiError && error.code === 'app_not_available')
|
|
249
|
+
return {
|
|
250
|
+
ok: false,
|
|
251
|
+
lines: [`${name}: signed in at ${stored.origin}, but Pending You doesn’t let it in now.`],
|
|
252
|
+
};
|
|
253
|
+
return {
|
|
254
|
+
ok: false,
|
|
255
|
+
lines: [
|
|
256
|
+
`${name}: signed in at ${stored.origin}; Pending You couldn’t be reached to say more.`,
|
|
257
|
+
],
|
|
258
|
+
};
|
|
259
|
+
}
|
|
260
|
+
const scopes = me.grant.scopes;
|
|
261
|
+
const may = scopes
|
|
262
|
+
.filter((scope) => scope !== 'cards:read' || !scopes.includes('cards:read:all'))
|
|
263
|
+
.map((scope) => MAY[scope] ?? scope);
|
|
264
|
+
const ends = Date.parse(me.grant.expiresAt);
|
|
265
|
+
const left = ends - io.now();
|
|
266
|
+
const days = Math.max(0, Math.ceil(left / 86_400_000));
|
|
267
|
+
return {
|
|
268
|
+
ok: true,
|
|
269
|
+
lines: [
|
|
270
|
+
`${name}: signed in at ${stored.origin} as ${signedInAs(app, me)}`,
|
|
271
|
+
` It reaches ${reachWords(me)}. It may ${listed(may)}; never a high-stakes card.`,
|
|
272
|
+
left <= RENEW_WITHIN_MS
|
|
273
|
+
? ` ! It ends in ${days} day${days === 1 ? '' : 's'} (${me.grant.expiresAt.slice(0, 10)}): sign in again before then: ${again}`
|
|
274
|
+
: ` It ends on ${me.grant.expiresAt.slice(0, 10)}; sign in again then.`,
|
|
275
|
+
` Kept in ${appPath(io, app)}`,
|
|
276
|
+
],
|
|
277
|
+
};
|
|
278
|
+
}
|
|
279
|
+
async function status(io, app) {
|
|
280
|
+
const apps = app ? [app] : await signedInApps(io);
|
|
281
|
+
if (apps.length === 0) {
|
|
282
|
+
io.out('No app is signed in on this computer. The Pending You plugin for Herdr signs in from its setup, or: npx pendingyou app login herdr\n');
|
|
283
|
+
return 1;
|
|
284
|
+
}
|
|
285
|
+
let ok = true;
|
|
286
|
+
const sections = [];
|
|
287
|
+
for (const each of apps) {
|
|
288
|
+
const said = await appStatusLines(io, each);
|
|
289
|
+
ok &&= said.ok;
|
|
290
|
+
sections.push(said.lines.join('\n'));
|
|
291
|
+
}
|
|
292
|
+
io.out(`${sections.join('\n\n')}\n`);
|
|
293
|
+
return ok ? 0 : 1;
|
|
294
|
+
}
|
|
295
|
+
export async function appCommand(io, command) {
|
|
296
|
+
switch (command.sub) {
|
|
297
|
+
case 'login':
|
|
298
|
+
return login(io, { ...command, app: command.app });
|
|
299
|
+
case 'logout':
|
|
300
|
+
return logout(io, command.app);
|
|
301
|
+
case 'status':
|
|
302
|
+
return status(io, command.app);
|
|
303
|
+
}
|
|
304
|
+
}
|
package/dist/args.js
CHANGED
|
@@ -2,6 +2,7 @@ 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 APP_SUBS = ['login', 'logout', 'status'];
|
|
5
6
|
export const MACHINE_SUBS = ['status', 'attest', 'rotate'];
|
|
6
7
|
export const HERDR_SUBS = [
|
|
7
8
|
'report',
|
|
@@ -82,6 +83,7 @@ const COMMANDS = new Set([
|
|
|
82
83
|
'presence',
|
|
83
84
|
'herdr',
|
|
84
85
|
'machine',
|
|
86
|
+
'app',
|
|
85
87
|
]);
|
|
86
88
|
const FLAGS = {
|
|
87
89
|
help: {},
|
|
@@ -151,6 +153,15 @@ const FLAGS = {
|
|
|
151
153
|
ensure: false,
|
|
152
154
|
},
|
|
153
155
|
machine: { origin: true, 'client-id': true, cnf: true, name: true, yes: false },
|
|
156
|
+
app: {
|
|
157
|
+
origin: true,
|
|
158
|
+
'client-id': true,
|
|
159
|
+
scope: true,
|
|
160
|
+
name: true,
|
|
161
|
+
device: false,
|
|
162
|
+
browser: false,
|
|
163
|
+
'no-browser': false,
|
|
164
|
+
},
|
|
154
165
|
};
|
|
155
166
|
const VALUE_OPTIONS = new Set([
|
|
156
167
|
'origin',
|
|
@@ -167,6 +178,7 @@ const VALUE_OPTIONS = new Set([
|
|
|
167
178
|
'at',
|
|
168
179
|
'client-id',
|
|
169
180
|
'cnf',
|
|
181
|
+
'scope',
|
|
170
182
|
]);
|
|
171
183
|
function splitCommand(argv) {
|
|
172
184
|
let index = 0;
|
|
@@ -417,6 +429,8 @@ export function parseArgs(argv, env = {}) {
|
|
|
417
429
|
return herdrCommand(origin, positional, flags, app);
|
|
418
430
|
case 'machine':
|
|
419
431
|
return machineCommand(origin, positional, flags, machine);
|
|
432
|
+
case 'app':
|
|
433
|
+
return appCommandOf(origin, positional, flags, machine, device);
|
|
420
434
|
case 'init':
|
|
421
435
|
none();
|
|
422
436
|
return {
|
|
@@ -569,6 +583,66 @@ function machineCommand(origin, positional, flags, machine) {
|
|
|
569
583
|
...machine(),
|
|
570
584
|
};
|
|
571
585
|
}
|
|
586
|
+
const APP_SCOPE_NAMES = [
|
|
587
|
+
'cards:read',
|
|
588
|
+
'cards:read:all',
|
|
589
|
+
'cards:answer',
|
|
590
|
+
'cards:reply',
|
|
591
|
+
'cards:later',
|
|
592
|
+
'cards:delegate',
|
|
593
|
+
'assistants:read',
|
|
594
|
+
'presence:desk',
|
|
595
|
+
];
|
|
596
|
+
function appCommandOf(origin, positional, flags, machine, device) {
|
|
597
|
+
const [sub, app, ...extra] = positional;
|
|
598
|
+
if (sub === undefined)
|
|
599
|
+
throw new UsageError('app needs what to do, like: pendingyou app login herdr');
|
|
600
|
+
if (!APP_SUBS.includes(sub))
|
|
601
|
+
throw new UsageError(`app has no “${sub}”. Use ${APP_SUBS.join(', ')}.`);
|
|
602
|
+
if (extra.length)
|
|
603
|
+
throw new UsageError(`app ${sub} takes one app, not “${extra[0]}”.`);
|
|
604
|
+
const allowed = {
|
|
605
|
+
login: ['client-id', 'scope', 'name', 'device', 'browser', 'no-browser'],
|
|
606
|
+
logout: [],
|
|
607
|
+
status: [],
|
|
608
|
+
};
|
|
609
|
+
for (const key of flags.keys())
|
|
610
|
+
if (key !== 'origin' && !allowed[sub].includes(key))
|
|
611
|
+
throw new UsageError(`app ${sub} has no --${key} option.`);
|
|
612
|
+
if (app !== undefined && !/^[a-z0-9][a-z0-9-]{0,39}$/.test(app))
|
|
613
|
+
throw new UsageError(`“${app}” isn’t an app’s name here: lower-case letters, digits and dashes.`);
|
|
614
|
+
if (app === undefined && sub !== 'status')
|
|
615
|
+
throw new UsageError(`app ${sub} needs the app, like: pendingyou app ${sub} herdr`);
|
|
616
|
+
const command = {
|
|
617
|
+
name: 'app',
|
|
618
|
+
origin,
|
|
619
|
+
sub: sub,
|
|
620
|
+
...(app ? { app } : {}),
|
|
621
|
+
device: null,
|
|
622
|
+
browser: true,
|
|
623
|
+
};
|
|
624
|
+
if (sub !== 'login')
|
|
625
|
+
return command;
|
|
626
|
+
const clientId = flags.get('client-id');
|
|
627
|
+
if (clientId !== undefined && (typeof clientId !== 'string' || !CLIENT_ID.test(clientId)))
|
|
628
|
+
throw new UsageError('--client-id needs the client the app signs in with.');
|
|
629
|
+
const scope = flags.get('scope');
|
|
630
|
+
let scopes;
|
|
631
|
+
if (typeof scope === 'string') {
|
|
632
|
+
scopes = [...new Set(scope.split(/[\s,]+/).filter(Boolean))];
|
|
633
|
+
const unknown = scopes.find((each) => !APP_SCOPE_NAMES.includes(each));
|
|
634
|
+
if (unknown || scopes.length === 0)
|
|
635
|
+
throw new UsageError(`--scope takes the person API’s scopes: ${APP_SCOPE_NAMES.join(', ')}.`);
|
|
636
|
+
}
|
|
637
|
+
return {
|
|
638
|
+
...command,
|
|
639
|
+
...(typeof clientId === 'string' ? { clientId } : {}),
|
|
640
|
+
...(scopes ? { scopes } : {}),
|
|
641
|
+
...machine(),
|
|
642
|
+
device: device(),
|
|
643
|
+
browser: !flags.has('no-browser'),
|
|
644
|
+
};
|
|
645
|
+
}
|
|
572
646
|
export const USAGE = `pendingyou: Pending You for your coding agents (Claude Code, Codex, OpenCode and Pi)
|
|
573
647
|
|
|
574
648
|
npx -y pendingyou@latest init
|
|
@@ -602,6 +676,10 @@ export const USAGE = `pendingyou: Pending You for your coding agents (Claude Cod
|
|
|
602
676
|
never leaves the computer): whether Pending You knows each agent's
|
|
603
677
|
connection by it, a proof of it for an app's own sign-in
|
|
604
678
|
(--client-id, --cnf), or a new key (--yes: without asking)
|
|
679
|
+
npx pendingyou app login herdr | logout herdr | status
|
|
680
|
+
Sign in an app that answers your cards when you press a key in it (the
|
|
681
|
+
Pending You plugin for Herdr), out again, or what each may do. This
|
|
682
|
+
command line never answers: the app does, with its own sign-in and key
|
|
605
683
|
|
|
606
684
|
Apps (for --app): ${APP_IDS.map((app) => `${app} (${APP_NAMES[app]})`).join(', ')}.
|
|
607
685
|
|