pendingyou 0.8.0 → 0.10.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 +72 -0
- package/README.md +89 -23
- package/dist/api.js +33 -0
- package/dist/args.js +78 -14
- package/dist/claude.js +268 -23
- package/dist/confirm.js +231 -0
- package/dist/credentials.js +39 -1
- package/dist/files.js +3 -3
- package/dist/hold.js +35 -17
- package/dist/index.js +1 -0
- package/dist/install.js +4 -2
- package/dist/io.js +3 -1
- package/dist/main.js +195 -35
- package/dist/mod.js +87 -0
- package/dist/oauth.js +27 -6
- package/dist/pickup.js +16 -18
- package/dist/qr.js +365 -0
- package/dist/remote.js +306 -0
- package/dist/state.js +5 -32
- package/dist/stopcheck.js +88 -8
- 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 +314 -0
- package/mod/hooks/wake.js +599 -0
- package/mod/tests/wake.test.ts +198 -0
- package/package.json +11 -4
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,77 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.10.0
|
|
4
|
+
|
|
5
|
+
- The wake mod: Claude Code 2.1.287 or later is woken when you answer, with nothing waiting in the background. A
|
|
6
|
+
session heard your answer only while `npx pendingyou hold` ran, and a hold stops: after its own 4 hours, or sooner
|
|
7
|
+
when Claude Code stops a background command. Then it said "Don't start another hold now", and an answer that came
|
|
8
|
+
later waited until you next typed in that session; two sessions missed answers that way overnight (2026-10-04).
|
|
9
|
+
`init` now adds the Pending You plugin's mod, which this package carries in `mod/`, to `CLAUDE_CODE_PLUGIN_DIRS`
|
|
10
|
+
(`env` in `~/.claude/settings.json`), from the hooks' own copy in `~/.config/pendingyou/cli/0.10.0/`, keeping any
|
|
11
|
+
other folders there and replacing an older version's copy. Each session then learns the cards it posts, checks them
|
|
12
|
+
through its own Pending You connection (every 20 seconds while a card changed in the last half hour, then every
|
|
13
|
+
minute), and starts one turn when you answer, write back or a fallback comes due: “Pending You: your person
|
|
14
|
+
answered … Call get_request for it …, act on their words, then ack_answer.” Under the prompt it shows
|
|
15
|
+
`⚠ pendingyou-wake: 2 waiting on you`. It answers `npx pendingyou hold` itself, so an agent that still runs one waits on
|
|
16
|
+
nothing. With the Pending You plugin installed too (it carries the same mod, plugin 2.24), only one copy acts. On an
|
|
17
|
+
older Claude Code, or none, `init` skips it without a word. `uninstall` takes it out; `status` says whether it's
|
|
18
|
+
loaded, from where, and what to do when it isn't.
|
|
19
|
+
- Without the mod, a hold that stops without an answer (its own `--timeout`, or Claude Code stopping it) says to start
|
|
20
|
+
it again in the background, with the exact command, `--origin` and `--timeout` included when they aren't the
|
|
21
|
+
defaults.
|
|
22
|
+
- The next-message hook looks for this folder's answers with every message, not only while a hold had left an entry
|
|
23
|
+
behind: one request, as before, given up after 2 seconds, inside the hook's 3-second deadline. `state.json` no
|
|
24
|
+
longer keeps holds' waits.
|
|
25
|
+
- A computer with no browser (a devbox or server you SSH into): one code approved on your phone sets Claude Code up.
|
|
26
|
+
`/mcp`'s Authenticate opened a browser on that machine, which nobody could reach, and `login --device` only signed
|
|
27
|
+
this command in. Now `init` there (an SSH session, or Linux with no display; `--device` forces it, `--browser` the
|
|
28
|
+
browser) signs the machine in by code as a Claude Code connection of its own ("Claude Code on devbox" on your
|
|
29
|
+
Assistants page, named by `--name`, else Tailscale's name for it, else its hostname; `login --name` with a new name
|
|
30
|
+
renames the connection with one more approval, which `login` suggests for a cloud's own name like `ip-10-42-1-252`;
|
|
31
|
+
QR code of the link at a terminal), then adds the MCP server with a `headersHelper` instead of OAuth
|
|
32
|
+
(`claude mcp add-json`), the wake mod in `env.CLAUDE_CODE_PLUGIN_DIRS` with `mcp__pendingyou` allowed, the skill and
|
|
33
|
+
the hooks. The helper, `~/.config/pendingyou/bin/pendingyou-mcp-headers`, is a small sh script Claude Code runs on
|
|
34
|
+
each connection: it finds Node on PATH, then nvm's default, then the Node `init` ran with, runs this version's own
|
|
35
|
+
copy (never npx) and prints only `{"Authorization":"Bearer …"}` (`pendingyou mcp-headers`), refreshing the sign-in in
|
|
36
|
+
the background when it's due. A browser sign-in at the same address (0.9.0's) is switched over; another's
|
|
37
|
+
`headersHelper` is kept. `login` there signs in that connection again, with the same client, so the machine keeps
|
|
38
|
+
one connection. `status` says what's set up and what's missing (it runs the helper as Claude Code would with a bare
|
|
39
|
+
PATH), each with the command that fixes it, `--origin` included off production. `uninstall` takes the helper out and
|
|
40
|
+
says the connection stays on your Assistants page. Needs Pending You's Worker from the same release.
|
|
41
|
+
- `login` over SSH signs in by code without `--device`. A sign-in by code names the computer, and the phone asks "Let
|
|
42
|
+
devbox hear your answers for the assistants running on it?"; in the browser, "Let this computer hear…". Neither offers
|
|
43
|
+
a choice of assistant any more (Darin, 2026-10-04): a computer hears for the assistants running on it. A sign-in
|
|
44
|
+
pointed at one assistant before then keeps hearing it, so `watch` for a Muse or another app can't be set up anew.
|
|
45
|
+
- Routing by sender: the hooks and `hold` ask Pending You for Claude Code's cards only (`source=claude-code`), so a card
|
|
46
|
+
Codex or another app posted from the same folder never reaches a Claude Code session, even with a sign-in pointed at
|
|
47
|
+
that app. `watch` names no app: it hands answers to a script, not a session. Pending You treats a folder's request
|
|
48
|
+
from an older version the same way.
|
|
49
|
+
- Hooks still run 0.9.0 from `~/.config/pendingyou/cli/0.9.0/`: run `npx pendingyou@latest init` once to switch, and
|
|
50
|
+
to add the mod.
|
|
51
|
+
|
|
52
|
+
## 0.9.0
|
|
53
|
+
|
|
54
|
+
- The Stop check catches a final message that asks you to type or reply something in Claude Code's own chat
|
|
55
|
+
("Reply “yes, erase disk12” here", "To confirm, type `yes`", "Confirm here"). Some steps an agent takes only on your
|
|
56
|
+
own words in its conversation, not on a card answer (something that can't be undone, credentials, anything outside
|
|
57
|
+
its sandbox), and that's right; but when it asks only in its chat, you never learn where you're needed. Unless a
|
|
58
|
+
Pending You card posted in that turn, or one still open from this folder, asks for the same thing, Claude Code is
|
|
59
|
+
asked once to post an action card that sends you there (about 76 tokens, plus the link): titled “Confirm in Claude
|
|
60
|
+
Code on <computer>: <step>”, with the exact words as a step's command (so the card shows them with a copy button),
|
|
61
|
+
where to type them, and what it'll do then; or, for a card you already answered, to reopen it with the same ask.
|
|
62
|
+
- The link is the session's own when Claude Code says it: `https://claude.ai/code/<id>` from
|
|
63
|
+
`CLAUDE_CODE_BRIDGE_SESSION_ID` while Remote Control is on, or from `CLAUDE_CODE_REMOTE_SESSION_ID` on the web.
|
|
64
|
+
- It counts as an ask: an imperative or a request to you ("please …", "can you …", "I need you to …") with the words
|
|
65
|
+
quoted, in backticks, or a plain "yes", tied to this chat ("here", "in this chat") or to going ahead ("to confirm",
|
|
66
|
+
"and I'll …"). Not in code, block quotes or quoted text; not what a program, prompt, page or message says; not a step
|
|
67
|
+
in your terminal, at a prompt or on a page; not a standing offer ("anytime"); not a command or slash command. A card
|
|
68
|
+
matches when its words name what's being confirmed ("erase" and "disk12"), or, with only a "yes" to go on, when it
|
|
69
|
+
was posted in the turn or its title asks to confirm.
|
|
70
|
+
- Only such a message costs a request: this folder's open cards (the same one the next-message hook makes), given up
|
|
71
|
+
after 1.5 seconds. When Pending You can't say (offline, slow, an older server), the message's own word that it's on
|
|
72
|
+
a card is taken. Everything else the Stop check does is as before, with no request.
|
|
73
|
+
- Hooks still run this version from `~/.config/pendingyou/cli/0.9.0/`: run `npx pendingyou@latest init` once to switch.
|
|
74
|
+
|
|
3
75
|
## 0.8.0
|
|
4
76
|
|
|
5
77
|
- The hooks say when a session is on another Pending You. A Claude Code session stayed connected to staging after its
|
package/README.md
CHANGED
|
@@ -11,20 +11,25 @@ Run it once on each computer. It:
|
|
|
11
11
|
|
|
12
12
|
- adds the Pending You MCP server to Claude Code for every project (unless it's already there),
|
|
13
13
|
- saves the Pending You skill to `~/.claude/skills/pendingyou/SKILL.md`,
|
|
14
|
-
- signs this computer in (your browser opens once; press Allow
|
|
14
|
+
- signs this computer in (your browser opens once; press Allow; over SSH, a code on your phone instead, as
|
|
15
|
+
[below](#a-computer-with-no-browser)),
|
|
15
16
|
- adds three hooks to `~/.claude/settings.json`: a new session picks up answers that arrived while nothing was
|
|
16
17
|
running, your next message brings a waiting answer with it, and a check when Claude Code finishes a turn (below),
|
|
17
18
|
- lets Claude Code start `npx pendingyou hold` and use Pending You's tools without a permission prompt, so a question
|
|
18
19
|
it asks while you're away doesn't wait on one,
|
|
19
20
|
- 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)
|
|
21
|
+
hundredths of a second instead of waiting on npx (about half a second each),
|
|
22
|
+
- on Claude Code 2.1.287 or later, adds the wake mod in that copy to `CLAUDE_CODE_PLUGIN_DIRS` (`env` in
|
|
23
|
+
`~/.claude/settings.json`), so every session is woken when you answer (below). Other folders already there are kept;
|
|
24
|
+
an older version's copy is replaced.
|
|
21
25
|
|
|
22
26
|
Your other settings are kept as they are. `npx pendingyou uninstall` removes exactly what `init` added.
|
|
23
27
|
|
|
24
28
|
Adding the MCP server can take Claude Code a minute or more (it checks its servers); `init` says when it's waiting on
|
|
25
29
|
that. If Claude Code already has a `pendingyou` server pointing somewhere else (staging, say), `init` names both and
|
|
26
30
|
asks before replacing it; `--yes` replaces it without asking, and with nobody to ask it prints the two commands to
|
|
27
|
-
run. Afterwards restart Claude Code, run `/mcp`, choose pendingyou and Authenticate
|
|
31
|
+
run. Afterwards restart Claude Code, run `/mcp`, choose pendingyou and Authenticate (on a computer with no browser
|
|
32
|
+
there's nothing to authenticate: [below](#a-computer-with-no-browser)).
|
|
28
33
|
|
|
29
34
|
If the `pendingyou` MCP server Claude Code uses in a session's folder is another Pending You than the hooks' (staging
|
|
30
35
|
and production, say), both hooks add one line telling Claude Code so, and to ask you which one you use: its cards
|
|
@@ -37,7 +42,7 @@ never refreshes the sign-in itself: when that's due, it starts the refresh in th
|
|
|
37
42
|
own. Running `init` again updates them in place, and adds any a newer version brings.
|
|
38
43
|
|
|
39
44
|
The hooks run that copy with Node directly, like
|
|
40
|
-
`"/opt/homebrew/bin/node" "~/.config/pendingyou/cli/0.
|
|
45
|
+
`"/opt/homebrew/bin/node" "~/.config/pendingyou/cli/0.10.0/node_modules/pendingyou/dist/cli.js" handoff || true` (the
|
|
41
46
|
paths in full). If the copy can't be made (no npm, no network), they run through `npx -y --prefer-offline
|
|
42
47
|
pendingyou@<version>` instead. `npx pendingyou status` says which. If you remove the Node they run with, run `init`
|
|
43
48
|
again.
|
|
@@ -51,48 +56,100 @@ Deep into a long conversation, Claude Code tends to end a turn with a question f
|
|
|
51
56
|
person also goes on a card.* While cards Claude Code posted from this folder are still waiting on you, the same line
|
|
52
57
|
names them (at most 3, newest first, about 20 tokens each) and asks it to `cancel_request` one with `answeredHere`
|
|
53
58
|
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
|
|
59
|
+
request per message, which brings any answer waiting for this folder too, given up after 2 seconds.
|
|
55
60
|
- When Claude Code finishes a turn, `pendingyou stopcheck` looks at its last message. If it has something for you and
|
|
56
61
|
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
62
|
that one is coming),
|
|
58
63
|
Claude Code is asked to go on once (about 31 tokens): post them as cards, or say they don't need one. It never asks
|
|
59
64
|
twice in a row.
|
|
60
65
|
|
|
61
|
-
The check is pattern matching on your computer: no model
|
|
66
|
+
The check is pattern matching on your computer: no model. It reads only the last turn from the end of the
|
|
62
67
|
transcript (at most 4 MB) and skips code blocks and quotes. It counts as for you: a question to you (a sentence ending
|
|
63
68
|
in "?" with "you" or "your"); "you'll need to", "your step", "can you", "please run", "in your terminal", "only you
|
|
64
69
|
can", "waiting on you" and the like; or a list under a heading like "Your part", "Your steps", "For you", "Waiting on
|
|
65
70
|
you" or "Left for you". It lets the turn end when the message says nothing needs a card ("No card needed.", "Nothing
|
|
66
71
|
needs your attention.", "There's nothing waiting on you.").
|
|
67
72
|
|
|
73
|
+
Since 0.9.0 it also catches a message asking you to type or reply something in Claude Code's own chat ("Reply “yes,
|
|
74
|
+
erase disk12” here", "To confirm, type `yes`", "Confirm here"). Some steps an agent takes only on your own words in
|
|
75
|
+
its conversation, never on a card answer; that's fine, but then a card has to send you there. Unless a card posted in
|
|
76
|
+
that turn, or one still open from this folder, asks for the same thing, Claude Code is asked once to post an action
|
|
77
|
+
card titled “Confirm in Claude Code on <computer>: <step>”, with the exact words to copy, where to type them (the
|
|
78
|
+
session's link when Remote Control or Claude Code on the web gives one) and what it'll do then. Only such a message
|
|
79
|
+
costs a request: this folder's open cards, given up after 1.5 seconds.
|
|
80
|
+
|
|
68
81
|
**On 0.2.0 with another origin?** Its hooks blocked every message (`There's no "--origin" command`). Run
|
|
69
82
|
`npx pendingyou@latest init --origin <url>` again to repair them; see [CHANGELOG.md](CHANGELOG.md).
|
|
70
83
|
|
|
71
|
-
## How Claude Code
|
|
84
|
+
## How Claude Code hears your answer
|
|
85
|
+
|
|
86
|
+
**Claude Code 2.1.287 or later: the wake mod.** There's nothing to run. In each session the mod learns the cards
|
|
87
|
+
Claude Code posts, checks them through the session's own Pending You connection (every 20 seconds while a card changed
|
|
88
|
+
in the last half hour, then every minute), and when you answer (after the 5-second window to undo), write back, or a
|
|
89
|
+
fallback comes due, it starts a turn naming the card and what to do: read it with `get_request`, act, then
|
|
90
|
+
`ack_answer`. That works while the session sits idle at the prompt, for as long as it's open, and across a restart
|
|
91
|
+
with `claude --continue`. Under the prompt it shows `⚠ pendingyou-wake: 2 waiting on you`. If Claude Code still runs
|
|
92
|
+
`npx pendingyou hold`, the mod answers it at once and nothing runs in the background. With the Pending You plugin
|
|
93
|
+
installed as well, only one copy acts. `npx pendingyou status` says whether the mod is loaded.
|
|
94
|
+
|
|
95
|
+
The mod's checks are Pending You tool calls like Claude Code's own, so they need Pending You's tools allowed, which
|
|
96
|
+
`init` does (`mcp__pendingyou`). If they aren't, the line under the prompt says which rule to add in `/permissions`, and
|
|
97
|
+
holds run as before.
|
|
72
98
|
|
|
73
|
-
After it posts a question to Pending You, Claude Code runs this in the
|
|
99
|
+
**Older Claude Code: a background hold.** After it posts a question to Pending You, Claude Code runs this in the
|
|
100
|
+
background and keeps working:
|
|
74
101
|
|
|
75
102
|
```sh
|
|
76
103
|
npx pendingyou hold req_0123456789abcdef0123
|
|
77
104
|
```
|
|
78
105
|
|
|
79
|
-
When you answer
|
|
80
|
-
|
|
81
|
-
|
|
106
|
+
When you answer, write back, or the question's fallback comes due, it prints a few lines (the question, your answer,
|
|
107
|
+
the next step) and exits. Claude Code wakes up with them. It stops on its own after 4 hours (`--timeout`), and Claude
|
|
108
|
+
Code may stop a background command sooner; either way it then says to start it again, with the exact command. Your
|
|
109
|
+
next message brings any answer with it too.
|
|
82
110
|
|
|
83
111
|
When you're at the terminal, Claude Code asks you there and puts the same question in Pending You at once; the
|
|
84
112
|
first answer wins. If you answer in the terminal, it withdraws the card and the hold ends by itself.
|
|
85
113
|
|
|
86
114
|
## A computer with no browser
|
|
87
115
|
|
|
88
|
-
On a
|
|
116
|
+
On a devbox or server you reach over SSH, `/mcp`'s Authenticate opens a browser on that machine, which you can't
|
|
117
|
+
reach. So SSH in, and at the prompt there run:
|
|
89
118
|
|
|
90
119
|
```sh
|
|
91
|
-
npx pendingyou
|
|
120
|
+
npx pendingyou init
|
|
92
121
|
```
|
|
93
122
|
|
|
94
|
-
|
|
95
|
-
code your
|
|
123
|
+
`init` sees it's an SSH session (or Linux with no display) and signs the machine in with a code instead: it shows the
|
|
124
|
+
code, `pendingyou.com/device` and a QR code of the link with the code in it. Open it on your phone, sign in, check the
|
|
125
|
+
code matches and press Allow. Only allow a code your own computer is showing you. That one approval makes the machine
|
|
126
|
+
a Claude Code connection of its own ("Claude Code on devbox" on your Assistants page), and `init` sets up the rest:
|
|
127
|
+
|
|
128
|
+
- adds the `pendingyou` MCP server for every project with a `headersHelper` instead of OAuth
|
|
129
|
+
(`claude mcp add-json --scope user pendingyou '{"type":"http","url":"…/mcp","headersHelper":"…"}'`), so Claude Code
|
|
130
|
+
signs in through this machine's sign-in and there's nothing to Authenticate,
|
|
131
|
+
- writes that helper, `~/.config/pendingyou/bin/pendingyou-mcp-headers`, a small sh script Claude Code runs each time
|
|
132
|
+
it connects. It finds Node on PATH, else nvm's default (`$NVM_DIR` or `~/.nvm`), else the Node `init` ran with,
|
|
133
|
+
runs this version's own copy (never npx), and prints only `{"Authorization":"Bearer …"}`, refreshing the sign-in
|
|
134
|
+
when it's due,
|
|
135
|
+
- adds the wake mod, the hooks and Pending You's tools to `~/.claude/settings.json` as above, so Remote Control,
|
|
136
|
+
terminal multiplexers and plain SSH sessions all get them, whatever shell profile they read.
|
|
137
|
+
|
|
138
|
+
Run it at an interactive prompt after you SSH in, not as `ssh host npx pendingyou init`: on stock Ubuntu that one-line
|
|
139
|
+
form doesn't read your shell's setup, so nvm's Node and `claude` may not be on PATH. `--device` sets a computer up this
|
|
140
|
+
way wherever it is, and `--browser` uses the browser instead. Then start Claude Code there and ask it to finish
|
|
141
|
+
setting up Pending You. `npx pendingyou status` says what's set up and what's missing, with the command that fixes it.
|
|
142
|
+
If the sign-in ends (30 days, or you remove the machine on your Assistants page), `npx pendingyou login` approves a
|
|
143
|
+
new code and keeps the same connection.
|
|
144
|
+
|
|
145
|
+
The machine is called by its Tailscale name when Tailscale is installed and running (as `tailscale status` shows it),
|
|
146
|
+
else by its hostname, or by `--name devbox` on `init` or `login`. A cloud's own hostname (AWS's `ip-10-42-1-252`) is
|
|
147
|
+
kept when there's nothing better, and `login` says how to change it: `npx pendingyou login --name <name>` asks for one
|
|
148
|
+
more approval on your phone and renames the connection, which stays the same one.
|
|
149
|
+
|
|
150
|
+
Only want this command's own sign-in (for `watch`, below)? `npx pendingyou login --device` signs it in with a code,
|
|
151
|
+
hearing answers only, and adds no connection. Your phone asks "Let devbox hear your answers for the assistants running
|
|
152
|
+
on it?", with no choice of assistant.
|
|
96
153
|
|
|
97
154
|
## Always-on scripts
|
|
98
155
|
|
|
@@ -102,8 +159,9 @@ code your own computer is showing you.
|
|
|
102
159
|
npx pendingyou watch -- ./on-answer.sh
|
|
103
160
|
```
|
|
104
161
|
|
|
105
|
-
|
|
106
|
-
|
|
162
|
+
It hears your Claude Code assistants' answers. Signing in asks no choice of assistant (since 2026-10-04: "Let pi hear
|
|
163
|
+
your answers for the assistants running on it?"); a sign-in pointed at one assistant before then keeps hearing that
|
|
164
|
+
one. The command runs without a shell, once per answer, message or fallback that comes due, oldest first. It
|
|
107
165
|
gets the request as JSON on stdin, and `PENDINGYOU_REQUEST_ID`, `PENDINGYOU_EVENT` (`answer_ready`, `message_ready` or
|
|
108
166
|
`fallback_due`), `PENDINGYOU_VERSION` and `PENDINGYOU_ORIGIN` in its environment. A failing command is tried twice
|
|
109
167
|
more, then skipped. `--once` stops after the first.
|
|
@@ -112,10 +170,10 @@ more, then skipped. `--once` stops after the first.
|
|
|
112
170
|
|
|
113
171
|
| Command | What it does |
|
|
114
172
|
| --- | --- |
|
|
115
|
-
| `init` | Set up Claude Code on this computer (`--no-login`, `--no-browser`, `--device`, `--yes`) |
|
|
116
|
-
| `hold <requestId>` | Wait for the answer, print it, exit (`--timeout 4h`) |
|
|
173
|
+
| `init` | Set up Claude Code on this computer (`--no-login`, `--no-browser`, `--device`, `--browser`, `--yes`) |
|
|
174
|
+
| `hold <requestId>` | Wait for the answer, print it, exit (`--timeout 4h`); the wake mod answers it itself |
|
|
117
175
|
| `status` | What's set up; exits 0 when Claude Code hears answers right away |
|
|
118
|
-
| `login` / `logout` | Sign this computer in or out (`--device`, `--force`, `--all`) |
|
|
176
|
+
| `login` / `logout` | Sign this computer in or out (`--device`, `--browser`, `--force`, `--all`) |
|
|
119
177
|
| `watch -- <command>` | Run a command each time an answer is ready (`--once`) |
|
|
120
178
|
| `pickup` / `handoff` | The session-start and next-message hooks |
|
|
121
179
|
| `stopcheck` | The Stop hook: items for you left only in chat |
|
|
@@ -126,14 +184,22 @@ before or after the command.
|
|
|
126
184
|
|
|
127
185
|
## What it can see, and where it keeps things
|
|
128
186
|
|
|
129
|
-
The sign-in only hears answers to your own Claude Code assistants' questions
|
|
130
|
-
answer or change anything, and it doesn't show up as one.
|
|
131
|
-
(
|
|
187
|
+
The sign-in only hears answers to your own Claude Code assistants' questions (or, for one made before 2026-10-04, to the one assistant you picked). It isn't an assistant: it can't ask,
|
|
188
|
+
answer or change anything, and it doesn't show up as one. The hooks and `hold` hand a session only the cards its own
|
|
189
|
+
app (Claude Code) posted from its folder, never one Codex or another app posted there. On a computer with no browser it's that computer's Claude
|
|
190
|
+
Code connection instead, which Claude Code asks through, and the hooks hear its answers only. It's stored in
|
|
191
|
+
`~/.config/pendingyou/credentials.json` (readable only by you), refreshes itself, and ends after 30 days or on
|
|
192
|
+
`logout`; `clients.json` remembers which sign-in a computer's connection used, so signing in again keeps it. `state.json` beside it remembers
|
|
132
193
|
which answers were already handed over (ids only), `claude-code.json` what `init` added, and `cli/` the hooks' copy
|
|
133
194
|
of the command line. `init`, `status` and the hooks read `~/.claude.json` (and a folder's `.mcp.json`, and
|
|
134
195
|
`~/.claude/settings.json`'s enabled plugins) only to see whether Claude Code has a `pendingyou` MCP server and where
|
|
135
196
|
it points. Nothing is sent anywhere but Pending You.
|
|
136
197
|
|
|
198
|
+
The wake mod runs inside Claude Code and reaches Pending You only through the session's own connection, with
|
|
199
|
+
`get_request` on the session's own cards. It keeps each session's cards (id, title, the agent's name, where each
|
|
200
|
+
stands) in Claude Code's store for mods, `~/.claude/plugins/store/`, and forgets them once they close or after 7 days.
|
|
201
|
+
It marks the copy acting for a session in that session's environment (`PENDINGYOU_WAKE_OWNER`).
|
|
202
|
+
|
|
137
203
|
## Development
|
|
138
204
|
|
|
139
205
|
In the Pending You repo: `pnpm --filter pendingyou test`, `pnpm --filter pendingyou build` (to `dist/`). Try it with a
|
package/dist/api.js
CHANGED
|
@@ -12,6 +12,12 @@
|
|
|
12
12
|
import { credentialsPath, readCredential, updateCredential, } from "./credentials.js";
|
|
13
13
|
import { isLocked } from "./files.js";
|
|
14
14
|
import { OAuthError, refresh } from "./oauth.js";
|
|
15
|
+
/**
|
|
16
|
+
* The app whose sessions the command line hands answers to: its hooks and holds run in Claude Code. Routing by sender
|
|
17
|
+
* (2026-10-04): every request for a session names it, so Pending You returns only Claude Code's cards, never one Codex
|
|
18
|
+
* or another app posted from the same folder. `watch` names none: it hands answers to a script, not a session.
|
|
19
|
+
*/
|
|
20
|
+
export const SESSION_APP = 'claude-code';
|
|
15
21
|
/** Refresh this long before the access token runs out. */
|
|
16
22
|
const EARLY_MS = 60_000;
|
|
17
23
|
/** A hook starts a background refresh this long before the access token runs out, and goes on with the old one. */
|
|
@@ -116,6 +122,33 @@ async function hookToken(io, origin, force, signal) {
|
|
|
116
122
|
}
|
|
117
123
|
throw new Unavailable('The sign-in is being refreshed in the background.');
|
|
118
124
|
}
|
|
125
|
+
/**
|
|
126
|
+
* The token `pendingyou mcp-headers` hands Claude Code (remote.ts). Claude Code runs the helper each time it connects
|
|
127
|
+
* and gives it 10 seconds, so, as for a hook, a refresh is never made in this process: within 5 minutes of expiry it
|
|
128
|
+
* starts `pendingyou refresh` detached and waits for its token, up to `waitMs`, then uses the old one while it still
|
|
129
|
+
* works. A token handed over has at least 10 seconds left; Claude Code runs the helper again on a 401.
|
|
130
|
+
*/
|
|
131
|
+
export async function helperToken(io, origin, waitMs) {
|
|
132
|
+
const stored = await readCredential(io, origin);
|
|
133
|
+
if (!stored)
|
|
134
|
+
throw new SignInNeeded(false);
|
|
135
|
+
if (stored.expiresAt - io.now() > SOON_MS)
|
|
136
|
+
return stored.accessToken;
|
|
137
|
+
if (!(await isLocked(credentialsPath(io))))
|
|
138
|
+
io.background(['refresh', '--origin', origin]);
|
|
139
|
+
const until = io.now() + waitMs;
|
|
140
|
+
while (io.now() < until) {
|
|
141
|
+
await io.sleep(100);
|
|
142
|
+
const current = await readCredential(io, origin);
|
|
143
|
+
if (!current)
|
|
144
|
+
throw new SignInNeeded(true);
|
|
145
|
+
if (current.refreshToken !== stored.refreshToken)
|
|
146
|
+
return current.accessToken;
|
|
147
|
+
}
|
|
148
|
+
if (stored.expiresAt - io.now() > USABLE_MS)
|
|
149
|
+
return stored.accessToken;
|
|
150
|
+
throw new Unavailable('Pending You couldn’t be reached to refresh the sign-in.');
|
|
151
|
+
}
|
|
119
152
|
/** Aborts on `signal` or after `ms`, whichever comes first (AbortSignal.any needs Node 20.3). */
|
|
120
153
|
function either(signal, ms) {
|
|
121
154
|
const timeout = AbortSignal.timeout(ms);
|
package/dist/args.js
CHANGED
|
@@ -20,6 +20,24 @@ export function parseDuration(text) {
|
|
|
20
20
|
const unit = match[2] ?? 'm';
|
|
21
21
|
return Math.round(amount * (unit === 's' ? 1000 : unit === 'm' ? 60_000 : 3_600_000));
|
|
22
22
|
}
|
|
23
|
+
/**
|
|
24
|
+
* A computer's name as Pending You keeps it (a connection's machine, "Claude Code on devbox"): control and format
|
|
25
|
+
* characters out, spaces collapsed, at most 60 characters. Empty when nothing is left.
|
|
26
|
+
*/
|
|
27
|
+
export function cleanName(text) {
|
|
28
|
+
const cleaned = [...text]
|
|
29
|
+
.map((char) => (/[\p{Cc}\p{Cf}]/u.test(char) ? ' ' : char))
|
|
30
|
+
.join('')
|
|
31
|
+
.replace(/\s+/g, ' ')
|
|
32
|
+
.trim();
|
|
33
|
+
let short = '';
|
|
34
|
+
for (const char of cleaned) {
|
|
35
|
+
if (short.length + char.length > 60)
|
|
36
|
+
break;
|
|
37
|
+
short += char;
|
|
38
|
+
}
|
|
39
|
+
return short.trim();
|
|
40
|
+
}
|
|
23
41
|
/** An origin Pending You can live at: https, or http on this computer (the local dev server). */
|
|
24
42
|
export function parseOrigin(text) {
|
|
25
43
|
let url;
|
|
@@ -50,25 +68,42 @@ const COMMANDS = new Set([
|
|
|
50
68
|
'uninstall',
|
|
51
69
|
'watch',
|
|
52
70
|
'refresh',
|
|
71
|
+
'mcp-headers',
|
|
53
72
|
]);
|
|
54
73
|
/** Flags each command takes; `true` when the flag takes a value. */
|
|
55
74
|
const FLAGS = {
|
|
56
75
|
help: {},
|
|
57
76
|
version: {},
|
|
58
|
-
login: {
|
|
77
|
+
login: {
|
|
78
|
+
origin: true,
|
|
79
|
+
'no-browser': false,
|
|
80
|
+
force: false,
|
|
81
|
+
device: false,
|
|
82
|
+
browser: false,
|
|
83
|
+
name: true,
|
|
84
|
+
},
|
|
59
85
|
logout: { origin: true, all: false },
|
|
60
86
|
status: { origin: true },
|
|
61
87
|
hold: { origin: true, timeout: true },
|
|
62
88
|
pickup: { origin: true },
|
|
63
89
|
handoff: { origin: true },
|
|
64
90
|
stopcheck: { origin: true },
|
|
65
|
-
init: {
|
|
91
|
+
init: {
|
|
92
|
+
origin: true,
|
|
93
|
+
'no-login': false,
|
|
94
|
+
'no-browser': false,
|
|
95
|
+
device: false,
|
|
96
|
+
browser: false,
|
|
97
|
+
yes: false,
|
|
98
|
+
name: true,
|
|
99
|
+
},
|
|
66
100
|
uninstall: { origin: true },
|
|
67
101
|
watch: { origin: true, once: false },
|
|
68
102
|
refresh: { origin: true, force: false },
|
|
103
|
+
'mcp-headers': { origin: true, check: false },
|
|
69
104
|
};
|
|
70
105
|
/** Options that take a value, whichever command they belong to: needed to find the command behind them. */
|
|
71
|
-
const VALUE_OPTIONS = new Set(['origin', 'timeout']);
|
|
106
|
+
const VALUE_OPTIONS = new Set(['origin', 'timeout', 'name']);
|
|
72
107
|
/** Splits off the options given before the command: [those options, the command, everything after it]. */
|
|
73
108
|
function splitCommand(argv) {
|
|
74
109
|
let index = 0;
|
|
@@ -147,6 +182,22 @@ export function parseArgs(argv, env = {}) {
|
|
|
147
182
|
if (positional.length)
|
|
148
183
|
throw new UsageError(`${first} takes no “${positional[0]}”.`);
|
|
149
184
|
};
|
|
185
|
+
/** `--name`, cleaned up as Pending You keeps it; none when it isn't given. */
|
|
186
|
+
const machine = () => {
|
|
187
|
+
const given = text('name');
|
|
188
|
+
if (given === undefined)
|
|
189
|
+
return {};
|
|
190
|
+
const name = cleanName(given);
|
|
191
|
+
if (!name)
|
|
192
|
+
throw new UsageError('--name needs a name for this computer, like: --name devbox');
|
|
193
|
+
return { machine: name };
|
|
194
|
+
};
|
|
195
|
+
/** `--device` or `--browser`, or neither (null: decided by looking). */
|
|
196
|
+
const device = () => {
|
|
197
|
+
if (flags.has('device') && flags.has('browser'))
|
|
198
|
+
throw new UsageError('Use --device or --browser, not both.');
|
|
199
|
+
return flags.has('device') ? true : flags.has('browser') ? false : null;
|
|
200
|
+
};
|
|
150
201
|
switch (first) {
|
|
151
202
|
case 'help':
|
|
152
203
|
return { name: 'help' };
|
|
@@ -159,7 +210,8 @@ export function parseArgs(argv, env = {}) {
|
|
|
159
210
|
origin,
|
|
160
211
|
browser: !flags.has('no-browser'),
|
|
161
212
|
force: flags.has('force'),
|
|
162
|
-
device:
|
|
213
|
+
device: device(),
|
|
214
|
+
...machine(),
|
|
163
215
|
};
|
|
164
216
|
case 'logout':
|
|
165
217
|
none();
|
|
@@ -183,6 +235,10 @@ export function parseArgs(argv, env = {}) {
|
|
|
183
235
|
case 'refresh':
|
|
184
236
|
none();
|
|
185
237
|
return { name: 'refresh', origin, force: flags.has('force') };
|
|
238
|
+
// Not in USAGE: what Claude Code's headersHelper runs on a computer with no browser (remote.ts).
|
|
239
|
+
case 'mcp-headers':
|
|
240
|
+
none();
|
|
241
|
+
return { name: 'mcp-headers', origin, check: flags.has('check') };
|
|
186
242
|
case 'init':
|
|
187
243
|
none();
|
|
188
244
|
return {
|
|
@@ -190,8 +246,9 @@ export function parseArgs(argv, env = {}) {
|
|
|
190
246
|
origin,
|
|
191
247
|
login: !flags.has('no-login'),
|
|
192
248
|
browser: !flags.has('no-browser'),
|
|
193
|
-
device:
|
|
249
|
+
device: device(),
|
|
194
250
|
yes: flags.has('yes'),
|
|
251
|
+
...machine(),
|
|
195
252
|
};
|
|
196
253
|
case 'watch':
|
|
197
254
|
none();
|
|
@@ -216,21 +273,28 @@ export function parseArgs(argv, env = {}) {
|
|
|
216
273
|
export const USAGE = `pendingyou: Pending You for Claude Code
|
|
217
274
|
|
|
218
275
|
npx pendingyou init Set up Claude Code on this computer: the MCP server, the skill,
|
|
219
|
-
this command's sign-in,
|
|
220
|
-
|
|
276
|
+
this command's sign-in, three hooks (answers, and a check that
|
|
277
|
+
what Claude Code leaves you in chat is also in Pending You), and
|
|
278
|
+
on Claude Code 2.1.287 or later the wake mod, which wakes a
|
|
279
|
+
session when you answer. Over SSH, or on Linux with no display,
|
|
280
|
+
you approve one code on your phone and Claude Code signs in
|
|
281
|
+
through this computer's sign-in: no browser needed
|
|
221
282
|
npx pendingyou hold <req_…> Wait in the background until the answer to a request is ready,
|
|
222
|
-
print it, and exit (which wakes Claude Code)
|
|
283
|
+
print it, and exit (which wakes Claude Code); with the wake mod,
|
|
284
|
+
the mod answers it and nothing runs
|
|
223
285
|
npx pendingyou status What's set up, and whether Claude Code hears answers right away
|
|
224
|
-
npx pendingyou login [--device] | logout
|
|
225
|
-
--device: on a computer with no browser, approve a short code on your
|
|
286
|
+
npx pendingyou login [--device | --browser] | logout
|
|
287
|
+
--device: on a computer with no browser, approve a short code on your
|
|
288
|
+
phone (the default over SSH); --browser: sign in in this computer's browser
|
|
226
289
|
npx pendingyou watch -- <command> [args…]
|
|
227
290
|
Run a command each time an answer is ready (always-on scripts)
|
|
228
291
|
npx pendingyou uninstall Remove exactly what init added
|
|
229
292
|
npx pendingyou pickup Answers waiting for this folder (the session-start hook)
|
|
230
|
-
npx pendingyou handoff The same,
|
|
293
|
+
npx pendingyou handoff The same, with your next message (the next-message hook)
|
|
231
294
|
npx pendingyou stopcheck Questions or steps for you left only in chat (the Stop hook)
|
|
232
295
|
|
|
233
|
-
Options: --origin <url> (default ${DEFAULT_ORIGIN}), --timeout 4h (hold), --no-browser and --
|
|
234
|
-
init), --
|
|
235
|
-
|
|
296
|
+
Options: --origin <url> (default ${DEFAULT_ORIGIN}), --timeout 4h (hold), --no-browser, --device and --browser
|
|
297
|
+
(login, init), --name <name> (login, init: what to call this computer when it signs in with a code; by default its
|
|
298
|
+
Tailscale name, else its hostname), --no-login (init), --yes (init: replace a pendingyou MCP server that points
|
|
299
|
+
elsewhere without asking), --all (logout), --force (login), --once (watch).
|
|
236
300
|
`;
|