pendingyou 0.9.0 → 0.11.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 +107 -0
- package/README.md +173 -56
- package/dist/api.js +60 -17
- package/dist/apps/claude-code.js +365 -0
- package/dist/apps/codex-wake.js +386 -0
- package/dist/apps/codex.js +615 -0
- package/dist/apps/ids.js +24 -0
- package/dist/apps/registry.js +12 -0
- package/dist/apps/types.js +1 -0
- package/dist/args.js +170 -33
- package/dist/claude.js +246 -199
- package/dist/confirm.js +5 -4
- package/dist/credentials.js +99 -19
- package/dist/environment.js +10 -3
- package/dist/files.js +3 -3
- package/dist/hold.js +35 -16
- package/dist/hooks.js +117 -0
- package/dist/index.js +4 -1
- package/dist/init.js +280 -0
- package/dist/install.js +4 -2
- package/dist/io.js +3 -1
- package/dist/main.js +39 -156
- package/dist/mod.js +89 -0
- package/dist/oauth.js +165 -48
- package/dist/pickup.js +67 -28
- package/dist/qr.js +365 -0
- package/dist/remote.js +347 -0
- package/dist/setup.js +109 -0
- package/dist/shim.js +93 -0
- package/dist/signin.js +227 -0
- package/dist/state.js +17 -28
- package/dist/stopcheck.js +45 -18
- package/dist/toml.js +277 -0
- package/dist/version.js +1 -1
- package/dist/watch.js +3 -5
- package/mod/.claude-plugin/plugin.json +12 -0
- package/mod/README.md +7 -0
- package/mod/hooks/hooks.json +7 -0
- package/mod/hooks/register.js +377 -0
- package/mod/hooks/wake.js +652 -0
- package/mod/tests/wake.test.ts +247 -0
- package/package.json +3 -2
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,112 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.11.0
|
|
4
|
+
|
|
5
|
+
- One step for the coding agents on a computer (Darin, 2026-10-04): `npx -y pendingyou@latest init` finds Claude Code
|
|
6
|
+
and Codex (`--app claude-code,codex` names some), says what it found and which it can't set up (an agent too old for
|
|
7
|
+
it is named, with the version it needs), and sets each up with one approval. Then: restart the agent, and its first
|
|
8
|
+
session finishes setting up by itself (whoami, report_setup, a test card). Asked for by name and not installed, or
|
|
9
|
+
nothing found at all: it says so and stops before anyone signs in.
|
|
10
|
+
- One sign-in everywhere: each agent's MCP server signs in through this computer's own connection ("Claude Code on
|
|
11
|
+
Ann-MBP", "Codex on Ann-MBP"), through the headers helper, on every computer but Windows; 0.10.0 did that only over
|
|
12
|
+
SSH. The hooks use the same connection, so there's nothing to Authenticate and no second sign-in. Each agent gets a
|
|
13
|
+
sign-in and a client of its own (`credentials.json`: Claude Code's under the address as before, another agent's under
|
|
14
|
+
`<address>#<app>`, so a 0.10.0 process still running keeps it; a `.gitignore` of `*` in the folder).
|
|
15
|
+
- Several agents, one approval: a device sign-in each, started together and shown as one link,
|
|
16
|
+
`/device?code=A&code=B`, which Pending You turns into one consent card naming each agent and the computer, with one
|
|
17
|
+
Allow (needs Pending You's Worker from this release). On a computer with a browser, the browser opens on it ("Your
|
|
18
|
+
browser is open on Pending You: press Allow once for Claude Code and Codex (codes …)"); over SSH, the link, a QR code
|
|
19
|
+
and each code. `--device` shows the codes anywhere, `--browser` opens the browser anyway, `--no-browser` only prints
|
|
20
|
+
the address. Cancel stops `init` before it changes anything. `login --app <app>` signs one agent in again, with its
|
|
21
|
+
own client, so it keeps its connection; `login --browser` no longer refuses a connection.
|
|
22
|
+
- Codex CLI (0.152 or later, whose `http_headers_helper` can give Authorization): its `[mcp_servers.pendingyou]` table
|
|
23
|
+
in `~/.codex/config.toml`, edited in place with every other line as it was, signed in through the helper
|
|
24
|
+
(`--app codex`) with `default_tools_approval_mode = "approve"`; a stored OAuth sign-in, which would win over the
|
|
25
|
+
helper, is logged out first. Its skill in `~/.agents/skills/pendingyou/SKILL.md`, and four hooks in
|
|
26
|
+
`~/.codex/hooks.json`, added after the person's own: a session start (startup, resume, clear), each message, Pending
|
|
27
|
+
You's card tools, and the Stop check (pattern matching on Codex's final message, once a turn at most). Codex runs a
|
|
28
|
+
hook only once the person trusts it: `init` says to choose “Trust all and continue”, `status` says which are trusted
|
|
29
|
+
(from Codex's own hash of each), and `uninstall` keeps the trust of the person's own hooks that move up when ours go.
|
|
30
|
+
When a card a Codex thread posted is answered, a listener the hooks start for that thread runs `codex queue` in the
|
|
31
|
+
wake mod's words (never with the answer itself, which other programs could read in its arguments) (Codex 0.157 or later starts a turn on an idle thread; `init` says so on an older
|
|
32
|
+
one). A server and hooks Codex's `/import` copied from Claude Code are replaced. Not on Windows yet.
|
|
33
|
+
- What happens to a `pendingyou` server that's there is decided before anyone signs in. At this Pending You, signing
|
|
34
|
+
in by itself (the agent's own OAuth, or the Pending You plugin with no server added by hand): moved to this
|
|
35
|
+
computer's sign-in without a word where there's no browser, on Yes (the default) at a terminal after saying what
|
|
36
|
+
that means, or with `--yes`; with nobody to ask, kept, with the command that switches it. Kept, the agent's hooks get
|
|
37
|
+
a sign-in that only hears, and a kept plugin carries the skill, so none is saved. At another Pending You: replaced
|
|
38
|
+
once the person says so (or `--yes`); kept, that agent is left as it is.
|
|
39
|
+
- `--oauth`: every agent signs in to its MCP server by itself, as before 0.11.0 (Claude Code's `claude mcp add`; Codex's
|
|
40
|
+
table without the helper, then `codex mcp login pendingyou`).
|
|
41
|
+
- Finishing setup by itself: a session start (new, resumed or cleared, never a compaction) adds one line while Pending
|
|
42
|
+
You says the agent's setup isn't finished (hear_answers' `for.setup`, on the request the hook makes anyway): what to
|
|
43
|
+
call, or that the test card's answer is waiting to be picked up; at most 3 times per `init`, none after 7 days, never
|
|
44
|
+
once it's verified. `init`'s copy of the wake mod starts that turn itself in Claude Code 2.1.287 or later. `init`
|
|
45
|
+
ends with the sentence to say when it doesn't, on its own line: “Finish setting up Pending You: it’s connected here
|
|
46
|
+
already.” (`FINISH_SAY`, the same as Pending You's pages).
|
|
47
|
+
- The hooks run a script with a path that never changes, `~/.config/pendingyou/bin/pendingyou-hook <command> --app
|
|
48
|
+
<app>`, which runs this version's private copy: upgrading rewrites the script, never the hook lines. Every request a
|
|
49
|
+
hook or the listener makes names its app (`source`).
|
|
50
|
+
- Node, without the shell's PATH (a Claude Code started from its desktop app or an IDE, Codex's stripped environment):
|
|
51
|
+
`init` records the Node it ran with by its stable path (`/opt/homebrew/bin/node`, not the Cellar folder an upgrade
|
|
52
|
+
removes), and the helper and the hooks' script also look in `/opt/homebrew/bin`, `/usr/local/bin` and `~/.volta/bin`.
|
|
53
|
+
A Mac is named by its own name (`scutil --get ComputerName`) unless `--name` says otherwise.
|
|
54
|
+
- `status` has a section for each agent `init` set up, a `Setup:` line (finished, waiting for the first session with
|
|
55
|
+
the sentence to say, the test card waiting on you, or answered and being picked up) that never changes Ready, and
|
|
56
|
+
"no browser here" in its header only when that's true. `uninstall` covers each agent, takes out the shared parts once
|
|
57
|
+
none is left, says which connections stay on your Assistants page, and ends with how to connect again.
|
|
58
|
+
- `pendingyou mcp --app <app>` is reserved for a stdio MCP server for an agent that can't run a headers helper; it says
|
|
59
|
+
it isn't ready yet.
|
|
60
|
+
|
|
61
|
+
## 0.10.0
|
|
62
|
+
|
|
63
|
+
- The wake mod: Claude Code 2.1.287 or later is woken when you answer, with nothing waiting in the background. A
|
|
64
|
+
session heard your answer only while `npx pendingyou hold` ran, and a hold stops: after its own 4 hours, or sooner
|
|
65
|
+
when Claude Code stops a background command. Then it said "Don't start another hold now", and an answer that came
|
|
66
|
+
later waited until you next typed in that session; two sessions missed answers that way overnight (2026-10-04).
|
|
67
|
+
`init` now adds the Pending You plugin's mod, which this package carries in `mod/`, to `CLAUDE_CODE_PLUGIN_DIRS`
|
|
68
|
+
(`env` in `~/.claude/settings.json`), from the hooks' own copy in `~/.config/pendingyou/cli/0.10.0/`, keeping any
|
|
69
|
+
other folders there and replacing an older version's copy. Each session then learns the cards it posts, checks them
|
|
70
|
+
through its own Pending You connection (every 20 seconds while a card changed in the last half hour, then every
|
|
71
|
+
minute), and starts one turn when you answer, write back or a fallback comes due: “Pending You: your person
|
|
72
|
+
answered … Call get_request for it …, act on their words, then ack_answer.” Under the prompt it shows
|
|
73
|
+
`⚠ pendingyou-wake: 2 waiting on you`. It answers `npx pendingyou hold` itself, so an agent that still runs one waits on
|
|
74
|
+
nothing. With the Pending You plugin installed too (it carries the same mod, plugin 2.24), only one copy acts. On an
|
|
75
|
+
older Claude Code, or none, `init` skips it without a word. `uninstall` takes it out; `status` says whether it's
|
|
76
|
+
loaded, from where, and what to do when it isn't.
|
|
77
|
+
- Without the mod, a hold that stops without an answer (its own `--timeout`, or Claude Code stopping it) says to start
|
|
78
|
+
it again in the background, with the exact command, `--origin` and `--timeout` included when they aren't the
|
|
79
|
+
defaults.
|
|
80
|
+
- The next-message hook looks for this folder's answers with every message, not only while a hold had left an entry
|
|
81
|
+
behind: one request, as before, given up after 2 seconds, inside the hook's 3-second deadline. `state.json` no
|
|
82
|
+
longer keeps holds' waits.
|
|
83
|
+
- A computer with no browser (a devbox or server you SSH into): one code approved on your phone sets Claude Code up.
|
|
84
|
+
`/mcp`'s Authenticate opened a browser on that machine, which nobody could reach, and `login --device` only signed
|
|
85
|
+
this command in. Now `init` there (an SSH session, or Linux with no display; `--device` forces it, `--browser` the
|
|
86
|
+
browser) signs the machine in by code as a Claude Code connection of its own ("Claude Code on devbox" on your
|
|
87
|
+
Assistants page, named by `--name`, else Tailscale's name for it, else its hostname; `login --name` with a new name
|
|
88
|
+
renames the connection with one more approval, which `login` suggests for a cloud's own name like `ip-10-42-1-252`;
|
|
89
|
+
QR code of the link at a terminal), then adds the MCP server with a `headersHelper` instead of OAuth
|
|
90
|
+
(`claude mcp add-json`), the wake mod in `env.CLAUDE_CODE_PLUGIN_DIRS` with `mcp__pendingyou` allowed, the skill and
|
|
91
|
+
the hooks. The helper, `~/.config/pendingyou/bin/pendingyou-mcp-headers`, is a small sh script Claude Code runs on
|
|
92
|
+
each connection: it finds Node on PATH, then nvm's default, then the Node `init` ran with, runs this version's own
|
|
93
|
+
copy (never npx) and prints only `{"Authorization":"Bearer …"}` (`pendingyou mcp-headers`), refreshing the sign-in in
|
|
94
|
+
the background when it's due. A browser sign-in at the same address (0.9.0's) is switched over; another's
|
|
95
|
+
`headersHelper` is kept. `login` there signs in that connection again, with the same client, so the machine keeps
|
|
96
|
+
one connection. `status` says what's set up and what's missing (it runs the helper as Claude Code would with a bare
|
|
97
|
+
PATH), each with the command that fixes it, `--origin` included off production. `uninstall` takes the helper out and
|
|
98
|
+
says the connection stays on your Assistants page. Needs Pending You's Worker from the same release.
|
|
99
|
+
- `login` over SSH signs in by code without `--device`. A sign-in by code names the computer, and the phone asks "Let
|
|
100
|
+
devbox hear your answers for the assistants running on it?"; in the browser, "Let this computer hear…". Neither offers
|
|
101
|
+
a choice of assistant any more (Darin, 2026-10-04): a computer hears for the assistants running on it. A sign-in
|
|
102
|
+
pointed at one assistant before then keeps hearing it, so `watch` for a Muse or another app can't be set up anew.
|
|
103
|
+
- Routing by sender: the hooks and `hold` ask Pending You for Claude Code's cards only (`source=claude-code`), so a card
|
|
104
|
+
Codex or another app posted from the same folder never reaches a Claude Code session, even with a sign-in pointed at
|
|
105
|
+
that app. `watch` names no app: it hands answers to a script, not a session. Pending You treats a folder's request
|
|
106
|
+
from an older version the same way.
|
|
107
|
+
- Hooks still run 0.9.0 from `~/.config/pendingyou/cli/0.9.0/`: run `npx pendingyou@latest init` once to switch, and
|
|
108
|
+
to add the mod.
|
|
109
|
+
|
|
3
110
|
## 0.9.0
|
|
4
111
|
|
|
5
112
|
- The Stop check catches a final message that asks you to type or reply something in Claude Code's own chat
|
package/README.md
CHANGED
|
@@ -1,46 +1,101 @@
|
|
|
1
1
|
# pendingyou
|
|
2
2
|
|
|
3
|
-
Pending You for
|
|
4
|
-
this command
|
|
3
|
+
Pending You for your coding agents. Your agents ask you in [Pending You](https://www.pendingyou.com) when you're away;
|
|
4
|
+
this command sets Claude Code and Codex up to ask there and to hear your answer the moment it's ready, even while they
|
|
5
|
+
sit idle at the prompt.
|
|
5
6
|
|
|
6
7
|
```sh
|
|
7
|
-
npx pendingyou init
|
|
8
|
+
npx -y pendingyou@latest init
|
|
8
9
|
```
|
|
9
10
|
|
|
10
|
-
Run it once on each computer. It
|
|
11
|
+
Run it once on each computer, in a terminal. It finds the agents on the computer (Claude Code, Codex; `--app` names
|
|
12
|
+
some), says what it found, and for each one:
|
|
13
|
+
|
|
14
|
+
- connects it to Pending You through **this computer's own sign-in**: its own connection ("Claude Code on Ann-MBP",
|
|
15
|
+
"Codex on Ann-MBP" on your Assistants page), which its MCP server and its hooks share, so there's nothing to
|
|
16
|
+
Authenticate in the agent and no second sign-in,
|
|
17
|
+
- signs them all in with **one approval**: your browser opens on Pending You with one card naming each agent and this
|
|
18
|
+
computer, and you press Allow once (over SSH: a code and a QR code for your phone, [below](#a-computer-with-no-browser)),
|
|
19
|
+
- adds the Pending You MCP server for every project, signed in through a small helper script, and lets the agent use
|
|
20
|
+
Pending You's tools without asking each time,
|
|
21
|
+
- saves the Pending You skill (Claude Code: `~/.claude/skills/pendingyou/SKILL.md`; Codex:
|
|
22
|
+
`~/.agents/skills/pendingyou/SKILL.md`),
|
|
23
|
+
- adds the hooks that hand it your answers: a new session picks up answers that arrived while nothing was running,
|
|
24
|
+
your next message brings a waiting answer with it, and a check when it finishes a turn catches what it left you only
|
|
25
|
+
in chat ([below](#nothing-left-only-in-chat)),
|
|
26
|
+
- and wakes it when you answer: Claude Code 2.1.287 or later through the wake mod, Codex through `codex queue`.
|
|
27
|
+
|
|
28
|
+
Then it says what to do next, and the one sentence to say if it's needed:
|
|
11
29
|
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
- adds three hooks to `~/.claude/settings.json`: a new session picks up answers that arrived while nothing was
|
|
16
|
-
running, your next message brings a waiting answer with it, and a check when Claude Code finishes a turn (below),
|
|
17
|
-
- lets Claude Code start `npx pendingyou hold` and use Pending You's tools without a permission prompt, so a question
|
|
18
|
-
it asks while you're away doesn't wait on one,
|
|
19
|
-
- keeps a copy of this version for the hooks in `~/.config/pendingyou/cli/<version>/`, so they start in a few
|
|
20
|
-
hundredths of a second instead of waiting on npx (about half a second each).
|
|
30
|
+
```
|
|
31
|
+
Next: restart Claude Code here (/exit, then claude --continue), or start it in the folder you work in.
|
|
32
|
+
It finishes setting up by itself and sends you a test card. If it doesn't, say this to it:
|
|
21
33
|
|
|
22
|
-
|
|
34
|
+
Finish setting up Pending You: it’s connected here already.
|
|
35
|
+
```
|
|
23
36
|
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
37
|
+
The first session finishes setting up on its own: it checks in with Pending You, reports what's set up and sends you a
|
|
38
|
+
test card. Claude Code 2.1.287 or later starts that turn by itself; otherwise (and in Codex, after you choose “Trust all
|
|
39
|
+
and continue” for its hooks) the session start adds one line saying what's left, and you say the sentence.
|
|
40
|
+
`npx pendingyou status` says what's set up for each agent, what's missing with the command that fixes it, and how setup
|
|
41
|
+
stands. `npx pendingyou uninstall` removes exactly what `init` added.
|
|
28
42
|
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
43
|
+
Your other settings are kept as they are: `init` merges into Claude Code's `~/.claude/settings.json`, Codex's
|
|
44
|
+
`~/.codex/config.toml` (its `[mcp_servers.pendingyou]` table and nothing else, every other line as it was) and
|
|
45
|
+
`~/.codex/hooks.json`, and running it again changes nothing it doesn't need to.
|
|
46
|
+
|
|
47
|
+
**A pendingyou server that's there already** is decided on before anyone signs in. One at another Pending You
|
|
48
|
+
(staging, say) is named and replaced only once you say so (`--yes` replaces it without asking); keep it and that agent
|
|
49
|
+
is left as it is. One at this Pending You that signs in by itself (Claude Code's `/mcp` → Authenticate, Codex's
|
|
50
|
+
`codex mcp login`, or the Pending You plugin for Claude) is moved to this computer's sign-in: without asking where
|
|
51
|
+
there's no browser (it can't finish there anyway), on Yes (the default) at a terminal, or with `--yes`; with nobody to
|
|
52
|
+
ask it's kept, and `init` prints the command that switches it. The connection it had stays on your Assistants page for
|
|
53
|
+
you to remove, and answers to the cards it asked through that one won't reach the agent by themselves. A kept plugin
|
|
54
|
+
carries the skill, so none is saved. `--oauth` keeps every agent signing in by itself, as before 0.11.0.
|
|
55
|
+
|
|
56
|
+
The hooks run a copy of this version kept in `~/.config/pendingyou/cli/<version>/`, through a small script with a path
|
|
57
|
+
that never changes, `~/.config/pendingyou/bin/pendingyou-hook`:
|
|
58
|
+
|
|
59
|
+
```
|
|
60
|
+
"~/.config/pendingyou/bin/pendingyou-hook" pickup --app claude-code || true
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
(the path in full). Upgrading rewrites that script, never the hook lines, so Codex, which runs a hook only while you
|
|
64
|
+
trust its exact definition, doesn't ask again. The script runs the copy with the Node `init` ran with, by its stable
|
|
65
|
+
path (Homebrew's `/opt/homebrew/bin/node`, which outlives an upgrade), or the first Node it finds. If the copy can't be
|
|
66
|
+
made (no npm, no network), the hooks run through `npx -y --prefer-offline pendingyou@<version>` instead.
|
|
33
67
|
|
|
34
68
|
The hooks never get in your way: if anything goes wrong (no network, not signed in, a bad option), they print nothing
|
|
35
69
|
and let your message through. Each one stops within 3 seconds whatever happens, with at most its one-line reminder, and
|
|
36
70
|
never refreshes the sign-in itself: when that's due, it starts the refresh in the background, which finishes on its
|
|
37
|
-
own.
|
|
71
|
+
own. Each hands an agent only its own app's cards from its folder, never one another agent posted there.
|
|
38
72
|
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
73
|
+
If the `pendingyou` MCP server Claude Code uses in a session's folder is another Pending You than the hooks' (staging
|
|
74
|
+
and production, say), both hooks add one line telling Claude Code so, and to ask you which one you use: its cards
|
|
75
|
+
would otherwise go where you don't look. `npx pendingyou status` flags it too. Fix it with `npx pendingyou init
|
|
76
|
+
--origin <the one you use>`, then restart the session. Codex's hooks do the same for Codex's server.
|
|
77
|
+
|
|
78
|
+
## Codex
|
|
79
|
+
|
|
80
|
+
Codex CLI 0.152 or later (its `http_headers_helper` gives the sign-in; 0.157 or later wakes an idle thread by itself).
|
|
81
|
+
`init` writes, in `~/.codex/config.toml`:
|
|
82
|
+
|
|
83
|
+
```toml
|
|
84
|
+
[mcp_servers.pendingyou]
|
|
85
|
+
url = "https://www.pendingyou.com/mcp"
|
|
86
|
+
http_headers_helper = "/Users/ann/.config/pendingyou/bin/pendingyou-mcp-headers --app codex"
|
|
87
|
+
default_tools_approval_mode = "approve"
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
and four hooks in `~/.codex/hooks.json`: a session start, each message, Pending You's card tools (so the Stop check
|
|
91
|
+
knows a turn posted, and so the thread is woken), and the Stop check. **Codex runs a hook only once you trust it**: start
|
|
92
|
+
`codex` and choose “Trust all and continue” (or trust them in `/hooks`). `status` says which are trusted. A
|
|
93
|
+
`pendingyou` server or hooks Codex's `/import` copied from Claude Code (without its sign-in) are replaced.
|
|
94
|
+
|
|
95
|
+
When you answer a card a Codex thread posted, a small listener `init`'s hooks start for that thread puts a message into
|
|
96
|
+
it with `codex queue`: “Pending You: your person answered “…” (req_…). Call get_request …”, which reads your answer (the
|
|
97
|
+
answer itself never goes in a command's arguments, where other programs could see it). Codex starts a turn at
|
|
98
|
+
once when the thread is open and idle, after the current one when it's working, or when you next open it.
|
|
44
99
|
|
|
45
100
|
## Nothing left only in chat
|
|
46
101
|
|
|
@@ -51,7 +106,7 @@ Deep into a long conversation, Claude Code tends to end a turn with a question f
|
|
|
51
106
|
person also goes on a card.* While cards Claude Code posted from this folder are still waiting on you, the same line
|
|
52
107
|
names them (at most 3, newest first, about 20 tokens each) and asks it to `cancel_request` one with `answeredHere`
|
|
53
108
|
when your message answers it, so a question you answer in chat doesn't stay in your queue. That costs one small
|
|
54
|
-
request per message, given up after
|
|
109
|
+
request per message, which brings any answer waiting for this folder too, given up after 2 seconds.
|
|
55
110
|
- When Claude Code finishes a turn, `pendingyou stopcheck` looks at its last message. If it has something for you and
|
|
56
111
|
nothing was posted, updated or replied to in Pending You that turn (and the message doesn't say it's on a card, or
|
|
57
112
|
that one is coming),
|
|
@@ -73,34 +128,78 @@ card titled “Confirm in Claude Code on <computer>: <step>”, with the exact w
|
|
|
73
128
|
session's link when Remote Control or Claude Code on the web gives one) and what it'll do then. Only such a message
|
|
74
129
|
costs a request: this folder's open cards, given up after 1.5 seconds.
|
|
75
130
|
|
|
131
|
+
Codex gets the same two (0.11.0). Its Stop check reads Codex's final message, and whether the turn posted, updated or
|
|
132
|
+
replied on a card comes from Codex's own hook for Pending You's tools in that turn; it asks once a turn at most, for an
|
|
133
|
+
action card titled “Confirm in Codex on <computer>: <step>” when it asked you to type something there.
|
|
134
|
+
|
|
76
135
|
**On 0.2.0 with another origin?** Its hooks blocked every message (`There's no "--origin" command`). Run
|
|
77
136
|
`npx pendingyou@latest init --origin <url>` again to repair them; see [CHANGELOG.md](CHANGELOG.md).
|
|
78
137
|
|
|
79
|
-
## How Claude Code
|
|
138
|
+
## How Claude Code hears your answer
|
|
80
139
|
|
|
81
|
-
|
|
140
|
+
**Claude Code 2.1.287 or later: the wake mod.** There's nothing to run. In each session the mod learns the cards
|
|
141
|
+
Claude Code posts, checks them through the session's own Pending You connection (every 20 seconds while a card changed
|
|
142
|
+
in the last half hour, then every minute), and when you answer (after the 5-second window to undo), write back, or a
|
|
143
|
+
fallback comes due, it starts a turn naming the card and what to do: read it with `get_request`, act, then
|
|
144
|
+
`ack_answer`. That works while the session sits idle at the prompt, for as long as it's open, and across a restart
|
|
145
|
+
with `claude --continue`. Under the prompt it shows `⚠ pendingyou-wake: 2 waiting on you`. If Claude Code still runs
|
|
146
|
+
`npx pendingyou hold`, the mod answers it at once and nothing runs in the background. With the Pending You plugin
|
|
147
|
+
installed as well, only one copy acts. `npx pendingyou status` says whether the mod is loaded.
|
|
148
|
+
|
|
149
|
+
The mod's checks are Pending You tool calls like Claude Code's own, so they need Pending You's tools allowed, which
|
|
150
|
+
`init` does (`mcp__pendingyou`). If they aren't, the line under the prompt says which rule to add in `/permissions`, and
|
|
151
|
+
holds run as before.
|
|
152
|
+
|
|
153
|
+
**Older Claude Code: a background hold.** After it posts a question to Pending You, Claude Code runs this in the
|
|
154
|
+
background and keeps working:
|
|
82
155
|
|
|
83
156
|
```sh
|
|
84
157
|
npx pendingyou hold req_0123456789abcdef0123
|
|
85
158
|
```
|
|
86
159
|
|
|
87
|
-
When you answer
|
|
88
|
-
|
|
89
|
-
|
|
160
|
+
When you answer, write back, or the question's fallback comes due, it prints a few lines (the question, your answer,
|
|
161
|
+
the next step) and exits. Claude Code wakes up with them. It stops on its own after 4 hours (`--timeout`), and Claude
|
|
162
|
+
Code may stop a background command sooner; either way it then says to start it again, with the exact command. Your
|
|
163
|
+
next message brings any answer with it too.
|
|
90
164
|
|
|
91
165
|
When you're at the terminal, Claude Code asks you there and puts the same question in Pending You at once; the
|
|
92
166
|
first answer wins. If you answer in the terminal, it withdraws the card and the hold ends by itself.
|
|
93
167
|
|
|
168
|
+
**Finishing setup by itself (0.11.0).** In `init`'s copy of the mod, a few seconds after a session you're at opens
|
|
169
|
+
(never `claude -p`), it asks Pending You whether Claude Code is set up here (`whoami`, once Pending You's tools are
|
|
170
|
+
allowed and the server has connected). When it isn't, it starts one turn once the session is idle: “Finish setting up
|
|
171
|
+
Pending You: it’s connected here already.” and exactly what to do. Once per session, in at most three sessions, never
|
|
172
|
+
two within ten minutes, and never again once Pending You says it's set up.
|
|
173
|
+
|
|
94
174
|
## A computer with no browser
|
|
95
175
|
|
|
96
|
-
On a
|
|
176
|
+
On a devbox or server you reach over SSH, an agent's own sign-in opens a browser on that machine, which you can't
|
|
177
|
+
reach. `init` signs in with a code there instead: SSH in, and at the prompt there run
|
|
97
178
|
|
|
98
179
|
```sh
|
|
99
|
-
npx pendingyou
|
|
180
|
+
npx -y pendingyou@latest init
|
|
100
181
|
```
|
|
101
182
|
|
|
102
|
-
It
|
|
103
|
-
|
|
183
|
+
It sees it's an SSH session (or Linux with no display) and shows a link for your phone, a QR code of it, and each code,
|
|
184
|
+
for every agent it found. Open it on your phone, sign in, check the codes match and press Allow once. Only allow codes
|
|
185
|
+
your own computer is showing you. `--device` shows codes like this anywhere; `--browser` opens the browser anyway;
|
|
186
|
+
`--no-browser` only prints the address.
|
|
187
|
+
|
|
188
|
+
Run it at an interactive prompt after you SSH in, not as `ssh host npx pendingyou init`: on stock Ubuntu that one-line
|
|
189
|
+
form doesn't read your shell's setup, so nvm's Node, `claude` and `codex` may not be on PATH. The helper script and the
|
|
190
|
+
hooks find Node anyway when the agents run without your shell's PATH (a systemd service, a terminal multiplexer,
|
|
191
|
+
Remote Control): on PATH, nvm's default, the one `init` ran with, or where Node usually is.
|
|
192
|
+
|
|
193
|
+
The machine is called by its Tailscale name when Tailscale is installed and running (as `tailscale status` shows it),
|
|
194
|
+
else by its hostname, or by `--name devbox` on `init` or `login`; a Mac by its own name (Sharing in System Settings). A
|
|
195
|
+
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:
|
|
196
|
+
`npx pendingyou login --name <name>` asks for one more approval and renames the connection, which stays the same one.
|
|
197
|
+
If an agent's sign-in ends (30 days, or you remove it on your Assistants page), `npx pendingyou login` (`--app codex`
|
|
198
|
+
for Codex's) approves a new code and keeps the same connection.
|
|
199
|
+
|
|
200
|
+
Only want this command's own sign-in (for `watch`, below)? `npx pendingyou login --device` signs it in with a code,
|
|
201
|
+
hearing answers only, and adds no connection. Your phone asks "Let devbox hear your answers for the assistants running
|
|
202
|
+
on it?", with no choice of assistant.
|
|
104
203
|
|
|
105
204
|
## Always-on scripts
|
|
106
205
|
|
|
@@ -110,8 +209,9 @@ code your own computer is showing you.
|
|
|
110
209
|
npx pendingyou watch -- ./on-answer.sh
|
|
111
210
|
```
|
|
112
211
|
|
|
113
|
-
|
|
114
|
-
|
|
212
|
+
It hears your Claude Code assistants' answers. Signing in asks no choice of assistant (since 2026-10-04: "Let pi hear
|
|
213
|
+
your answers for the assistants running on it?"); a sign-in pointed at one assistant before then keeps hearing that
|
|
214
|
+
one. The command runs without a shell, once per answer, message or fallback that comes due, oldest first. It
|
|
115
215
|
gets the request as JSON on stdin, and `PENDINGYOU_REQUEST_ID`, `PENDINGYOU_EVENT` (`answer_ready`, `message_ready` or
|
|
116
216
|
`fallback_due`), `PENDINGYOU_VERSION` and `PENDINGYOU_ORIGIN` in its environment. A failing command is tried twice
|
|
117
217
|
more, then skipped. `--once` stops after the first.
|
|
@@ -120,27 +220,44 @@ more, then skipped. `--once` stops after the first.
|
|
|
120
220
|
|
|
121
221
|
| Command | What it does |
|
|
122
222
|
| --- | --- |
|
|
123
|
-
| `init` | Set up
|
|
124
|
-
| `hold <requestId>` | Wait for the answer, print it, exit (`--timeout 4h`) |
|
|
125
|
-
| `status` | What's set up; exits 0 when
|
|
126
|
-
| `login` / `logout` | Sign
|
|
223
|
+
| `init` | Set up every agent on this computer (`--app`, `--oauth`, `--no-login`, `--no-browser`, `--device`, `--browser`, `--yes`, `--name`) |
|
|
224
|
+
| `hold <requestId>` | Wait for the answer, print it, exit (`--timeout 4h`); the wake mod answers it itself |
|
|
225
|
+
| `status` | What's set up for each agent; exits 0 when each hears answers right away (`--app`) |
|
|
226
|
+
| `login` / `logout` | Sign an agent in again, or out (`--app`, `--device`, `--browser`, `--force`, `--name`, `--all`) |
|
|
127
227
|
| `watch -- <command>` | Run a command each time an answer is ready (`--once`) |
|
|
128
|
-
| `pickup` / `handoff` | The session-start and next-message hooks |
|
|
129
|
-
| `stopcheck` | The Stop hook: items for you left only in chat |
|
|
130
|
-
| `uninstall` | Remove what `init` added, and sign out |
|
|
228
|
+
| `pickup` / `handoff` | The session-start and next-message hooks (`--app`) |
|
|
229
|
+
| `stopcheck` | The Stop hook: items for you left only in chat (`--app`) |
|
|
230
|
+
| `uninstall` | Remove what `init` added, and sign out (`--app`) |
|
|
231
|
+
| `mcp --app <app>` | Not yet: a stdio MCP server for an agent that can't run a headers helper |
|
|
131
232
|
|
|
132
|
-
Every command takes `--origin` (default `https://www.pendingyou.com`, or `$PENDINGYOU_ORIGIN`).
|
|
133
|
-
|
|
233
|
+
Every command takes `--origin` (default `https://www.pendingyou.com`, or `$PENDINGYOU_ORIGIN`). `--app` takes
|
|
234
|
+
`claude-code` or `codex` (several with commas where a command sets up or reports several); with none, a command that
|
|
235
|
+
acts for one agent acts for Claude Code. Options can come before or after the command.
|
|
134
236
|
|
|
135
237
|
## What it can see, and where it keeps things
|
|
136
238
|
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
239
|
+
Each agent's sign-in is that agent's own connection on Pending You, signed in from this computer: it can post cards,
|
|
240
|
+
change or cancel its own, read the answers to them and reply in their threads, and it can't answer anything for you.
|
|
241
|
+
The hooks use the same sign-in, and hand a session only the cards its own app posted from its folder. An agent that
|
|
242
|
+
signs in by itself (`--oauth`, or one you kept) gets a sign-in that only hears its answers instead: it can't ask,
|
|
243
|
+
answer or change anything, and doesn't show up as an assistant. They're stored in `~/.config/pendingyou/credentials.json`
|
|
244
|
+
(readable only by you, in a folder with a `.gitignore` of `*` so a dotfiles repository never takes it), refresh
|
|
245
|
+
themselves, and end after 30 days or on `logout`. The headers helper hands an agent's token only to that agent's
|
|
246
|
+
Pending You (Claude Code says which server it's for; Codex's is the one `init` wrote). `clients.json` remembers which
|
|
247
|
+
sign-in each agent's connection used, so signing in again keeps it.
|
|
248
|
+
|
|
249
|
+
`state.json` beside them remembers which answers were already handed over (ids only) and how many session starts
|
|
250
|
+
reminded an unfinished setup; `claude-code.json` and `codex.json` what `init` added; `codex-threads.json` which cards
|
|
251
|
+
each Codex thread waits on (ids, names and titles: never an answer); and `cli/` the hooks' copy of the command line.
|
|
252
|
+
`init`, `status` and the hooks read `~/.claude.json` (and a folder's `.mcp.json`, and `~/.claude/settings.json`'s
|
|
253
|
+
enabled plugins) and `~/.codex/config.toml` only to see whether the agent has a `pendingyou` MCP server and where it
|
|
254
|
+
points. Nothing is sent anywhere but Pending You.
|
|
255
|
+
|
|
256
|
+
The wake mod runs inside Claude Code and reaches Pending You only through the session's own connection, with
|
|
257
|
+
`get_request` on the session's own cards (and, in `init`'s copy, `whoami` to see whether setup is finished). It keeps
|
|
258
|
+
each session's cards (id, title, the agent's name, where each stands) in Claude Code's store for mods,
|
|
259
|
+
`~/.claude/plugins/store/`, and forgets them once they close or after 7 days. It marks the copy acting for a session in
|
|
260
|
+
that session's environment (`PENDINGYOU_WAKE_OWNER`).
|
|
144
261
|
|
|
145
262
|
## Development
|
|
146
263
|
|
package/dist/api.js
CHANGED
|
@@ -9,6 +9,12 @@
|
|
|
9
9
|
// a hook that needs a new token starts `pendingyou refresh` detached (background), which runs to the end under the
|
|
10
10
|
// lock whatever happens to the hook, and waits for it only briefly; until it lands the hook uses the token it has, or
|
|
11
11
|
// gives up and prints its plain fallback.
|
|
12
|
+
//
|
|
13
|
+
// Since 0.11.0 each app has its own sign-in (credentials.ts), and every call names the app it's for: its sign-in is
|
|
14
|
+
// the one used, and a session's request names it as `source` (routing by sender, 2026-10-04), so Pending You returns
|
|
15
|
+
// only that app's cards, never one Codex posted from the same folder to a Claude Code session there. `watch` names no
|
|
16
|
+
// app: it hands answers to a script, not a session, with Claude Code's sign-in as before.
|
|
17
|
+
import { DEFAULT_APP } from "./apps/ids.js";
|
|
12
18
|
import { credentialsPath, readCredential, updateCredential, } from "./credentials.js";
|
|
13
19
|
import { isLocked } from "./files.js";
|
|
14
20
|
import { OAuthError, refresh } from "./oauth.js";
|
|
@@ -43,7 +49,7 @@ export class Unavailable extends Error {
|
|
|
43
49
|
* Refreshes the sign-in under the lock unless it no longer needs it: a token with more than `earlyMs` left (or, when
|
|
44
50
|
* `force`d after a 401, a refresh token another process already replaced) is used as it is.
|
|
45
51
|
*/
|
|
46
|
-
async function refreshStored(io, origin, stored, options) {
|
|
52
|
+
async function refreshStored(io, origin, stored, options, app) {
|
|
47
53
|
let ended = false;
|
|
48
54
|
const updated = await updateCredential(io, origin, async (current) => {
|
|
49
55
|
if (!current)
|
|
@@ -65,35 +71,43 @@ async function refreshStored(io, origin, stored, options) {
|
|
|
65
71
|
}
|
|
66
72
|
throw error;
|
|
67
73
|
}
|
|
68
|
-
});
|
|
74
|
+
}, app);
|
|
69
75
|
if (!updated)
|
|
70
76
|
throw new SignInNeeded(ended);
|
|
71
77
|
return updated;
|
|
72
78
|
}
|
|
73
|
-
async function freshToken(io, origin, force) {
|
|
74
|
-
const stored = await readCredential(io, origin);
|
|
79
|
+
async function freshToken(io, origin, force, app) {
|
|
80
|
+
const stored = await readCredential(io, origin, app);
|
|
75
81
|
if (!stored)
|
|
76
82
|
throw new SignInNeeded(false);
|
|
77
83
|
if (!force && stored.expiresAt - EARLY_MS > io.now())
|
|
78
84
|
return stored.accessToken;
|
|
79
|
-
return (await refreshStored(io, origin, stored, { force, earlyMs: EARLY_MS })).accessToken;
|
|
85
|
+
return (await refreshStored(io, origin, stored, { force, earlyMs: EARLY_MS }, app)).accessToken;
|
|
80
86
|
}
|
|
87
|
+
/** What a process starts to refresh an app's sign-in in the background: `--app` only for an app but Claude Code. */
|
|
88
|
+
export const refreshArgs = (origin, app, force = false) => [
|
|
89
|
+
'refresh',
|
|
90
|
+
'--origin',
|
|
91
|
+
origin,
|
|
92
|
+
...(app === DEFAULT_APP ? [] : ['--app', app]),
|
|
93
|
+
...(force ? ['--force'] : []),
|
|
94
|
+
];
|
|
81
95
|
/**
|
|
82
96
|
* `pendingyou refresh`, what a hook starts in the background: refreshes the sign-in when it runs out within SOON_MS
|
|
83
97
|
* (or `force`, after a 401), under the lock, to the end. Quiet; true when there's a sign-in afterwards.
|
|
84
98
|
*/
|
|
85
|
-
export async function refreshInBackground(io, origin, force) {
|
|
86
|
-
const stored = await readCredential(io, origin);
|
|
99
|
+
export async function refreshInBackground(io, origin, force, app = DEFAULT_APP) {
|
|
100
|
+
const stored = await readCredential(io, origin, app);
|
|
87
101
|
if (!stored)
|
|
88
102
|
return false;
|
|
89
103
|
if (!force && stored.expiresAt - SOON_MS > io.now())
|
|
90
104
|
return true;
|
|
91
|
-
await refreshStored(io, origin, stored, { force, earlyMs: SOON_MS });
|
|
105
|
+
await refreshStored(io, origin, stored, { force, earlyMs: SOON_MS }, app);
|
|
92
106
|
return true;
|
|
93
107
|
}
|
|
94
108
|
/** A hook's token: never refreshed here (see the top of this file). */
|
|
95
|
-
async function hookToken(io, origin, force, signal) {
|
|
96
|
-
const stored = await readCredential(io, origin);
|
|
109
|
+
async function hookToken(io, origin, force, signal, app) {
|
|
110
|
+
const stored = await readCredential(io, origin, app);
|
|
97
111
|
if (!stored)
|
|
98
112
|
throw new SignInNeeded(false);
|
|
99
113
|
const left = stored.expiresAt - io.now();
|
|
@@ -101,14 +115,14 @@ async function hookToken(io, origin, force, signal) {
|
|
|
101
115
|
return stored.accessToken;
|
|
102
116
|
// One refresher at a time: a lock held now is a refresh (or a write) already under way.
|
|
103
117
|
if (!(await isLocked(credentialsPath(io))))
|
|
104
|
-
io.background(
|
|
118
|
+
io.background(refreshArgs(origin, app, force));
|
|
105
119
|
// Still good for a few seconds: this request goes with it, the next one with the new token.
|
|
106
120
|
if (!force && left > USABLE_MS)
|
|
107
121
|
return stored.accessToken;
|
|
108
122
|
const until = io.now() + REFRESH_WAIT_MS;
|
|
109
123
|
while (io.now() < until && !signal?.aborted) {
|
|
110
124
|
await io.sleep(50, signal);
|
|
111
|
-
const current = await readCredential(io, origin);
|
|
125
|
+
const current = await readCredential(io, origin, app);
|
|
112
126
|
if (!current)
|
|
113
127
|
throw new SignInNeeded(true);
|
|
114
128
|
if (current.refreshToken !== stored.refreshToken)
|
|
@@ -116,6 +130,33 @@ async function hookToken(io, origin, force, signal) {
|
|
|
116
130
|
}
|
|
117
131
|
throw new Unavailable('The sign-in is being refreshed in the background.');
|
|
118
132
|
}
|
|
133
|
+
/**
|
|
134
|
+
* The token `pendingyou mcp-headers` hands an app's MCP server (remote.ts). The app runs the helper each time it
|
|
135
|
+
* connects and gives it 10 seconds, so, as for a hook, a refresh is never made in this process: within 5 minutes of
|
|
136
|
+
* expiry it starts `pendingyou refresh` detached and waits for its token, up to `waitMs`, then uses the old one while it
|
|
137
|
+
* still works. A token handed over has at least 10 seconds left; the app runs the helper again on a 401.
|
|
138
|
+
*/
|
|
139
|
+
export async function helperToken(io, origin, waitMs, app = DEFAULT_APP) {
|
|
140
|
+
const stored = await readCredential(io, origin, app);
|
|
141
|
+
if (!stored)
|
|
142
|
+
throw new SignInNeeded(false);
|
|
143
|
+
if (stored.expiresAt - io.now() > SOON_MS)
|
|
144
|
+
return stored.accessToken;
|
|
145
|
+
if (!(await isLocked(credentialsPath(io))))
|
|
146
|
+
io.background(refreshArgs(origin, app));
|
|
147
|
+
const until = io.now() + waitMs;
|
|
148
|
+
while (io.now() < until) {
|
|
149
|
+
await io.sleep(100);
|
|
150
|
+
const current = await readCredential(io, origin, app);
|
|
151
|
+
if (!current)
|
|
152
|
+
throw new SignInNeeded(true);
|
|
153
|
+
if (current.refreshToken !== stored.refreshToken)
|
|
154
|
+
return current.accessToken;
|
|
155
|
+
}
|
|
156
|
+
if (stored.expiresAt - io.now() > USABLE_MS)
|
|
157
|
+
return stored.accessToken;
|
|
158
|
+
throw new Unavailable('Pending You couldn’t be reached to refresh the sign-in.');
|
|
159
|
+
}
|
|
119
160
|
/** Aborts on `signal` or after `ms`, whichever comes first (AbortSignal.any needs Node 20.3). */
|
|
120
161
|
function either(signal, ms) {
|
|
121
162
|
const timeout = AbortSignal.timeout(ms);
|
|
@@ -130,17 +171,19 @@ function either(signal, ms) {
|
|
|
130
171
|
return controller.signal;
|
|
131
172
|
}
|
|
132
173
|
/**
|
|
133
|
-
* GETs `path` on `origin` as the command line,
|
|
134
|
-
* the sign-in is over (revoked, or 30 days passed). Network
|
|
174
|
+
* GETs `path` on `origin` as the command line, with an app's sign-in (Claude Code's unless `app` says), and returns the
|
|
175
|
+
* JSON body with its status. 401 after a refresh means the sign-in is over (revoked, or 30 days passed). Network
|
|
176
|
+
* failures and 5xx are `Unavailable`.
|
|
135
177
|
*/
|
|
136
178
|
export async function getJson(io, origin, path, timeoutMs, signal, options = {}) {
|
|
179
|
+
const app = options.app ?? DEFAULT_APP;
|
|
137
180
|
for (let attempt = 0;; attempt++) {
|
|
138
181
|
let token;
|
|
139
182
|
try {
|
|
140
183
|
token =
|
|
141
184
|
options.hook && io.background
|
|
142
|
-
? await hookToken(io, origin, attempt > 0, signal)
|
|
143
|
-
: await freshToken(io, origin, attempt > 0);
|
|
185
|
+
? await hookToken(io, origin, attempt > 0, signal, app)
|
|
186
|
+
: await freshToken(io, origin, attempt > 0, app);
|
|
144
187
|
}
|
|
145
188
|
catch (error) {
|
|
146
189
|
if (error instanceof Unavailable)
|
|
@@ -165,7 +208,7 @@ export async function getJson(io, origin, path, timeoutMs, signal, options = {})
|
|
|
165
208
|
await response.body?.cancel();
|
|
166
209
|
if (attempt === 0)
|
|
167
210
|
continue;
|
|
168
|
-
await updateCredential(io, origin, () => null);
|
|
211
|
+
await updateCredential(io, origin, () => null, app);
|
|
169
212
|
throw new SignInNeeded(true);
|
|
170
213
|
}
|
|
171
214
|
if (response.status >= 500 || response.status === 429) {
|