pendingyou 0.9.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 +49 -0
- package/README.md +80 -22
- package/dist/api.js +33 -0
- package/dist/args.js +78 -14
- package/dist/claude.js +268 -23
- 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 +2 -1
- 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 +3 -2
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,54 @@
|
|
|
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
|
+
|
|
3
52
|
## 0.9.0
|
|
4
53
|
|
|
5
54
|
- 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
|
@@ -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,7 +56,7 @@ 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),
|
|
@@ -76,31 +81,75 @@ costs a request: this folder's open cards, given up after 1.5 seconds.
|
|
|
76
81
|
**On 0.2.0 with another origin?** Its hooks blocked every message (`There's no "--origin" command`). Run
|
|
77
82
|
`npx pendingyou@latest init --origin <url>` again to repair them; see [CHANGELOG.md](CHANGELOG.md).
|
|
78
83
|
|
|
79
|
-
## How Claude Code
|
|
84
|
+
## How Claude Code hears your answer
|
|
80
85
|
|
|
81
|
-
|
|
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.
|
|
98
|
+
|
|
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:
|
|
82
101
|
|
|
83
102
|
```sh
|
|
84
103
|
npx pendingyou hold req_0123456789abcdef0123
|
|
85
104
|
```
|
|
86
105
|
|
|
87
|
-
When you answer
|
|
88
|
-
|
|
89
|
-
|
|
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.
|
|
90
110
|
|
|
91
111
|
When you're at the terminal, Claude Code asks you there and puts the same question in Pending You at once; the
|
|
92
112
|
first answer wins. If you answer in the terminal, it withdraws the card and the hold ends by itself.
|
|
93
113
|
|
|
94
114
|
## A computer with no browser
|
|
95
115
|
|
|
96
|
-
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:
|
|
97
118
|
|
|
98
119
|
```sh
|
|
99
|
-
npx pendingyou
|
|
120
|
+
npx pendingyou init
|
|
100
121
|
```
|
|
101
122
|
|
|
102
|
-
|
|
103
|
-
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.
|
|
104
153
|
|
|
105
154
|
## Always-on scripts
|
|
106
155
|
|
|
@@ -110,8 +159,9 @@ code your own computer is showing you.
|
|
|
110
159
|
npx pendingyou watch -- ./on-answer.sh
|
|
111
160
|
```
|
|
112
161
|
|
|
113
|
-
|
|
114
|
-
|
|
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
|
|
115
165
|
gets the request as JSON on stdin, and `PENDINGYOU_REQUEST_ID`, `PENDINGYOU_EVENT` (`answer_ready`, `message_ready` or
|
|
116
166
|
`fallback_due`), `PENDINGYOU_VERSION` and `PENDINGYOU_ORIGIN` in its environment. A failing command is tried twice
|
|
117
167
|
more, then skipped. `--once` stops after the first.
|
|
@@ -120,10 +170,10 @@ more, then skipped. `--once` stops after the first.
|
|
|
120
170
|
|
|
121
171
|
| Command | What it does |
|
|
122
172
|
| --- | --- |
|
|
123
|
-
| `init` | Set up Claude Code on this computer (`--no-login`, `--no-browser`, `--device`, `--yes`) |
|
|
124
|
-
| `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 |
|
|
125
175
|
| `status` | What's set up; exits 0 when Claude Code hears answers right away |
|
|
126
|
-
| `login` / `logout` | Sign this computer in or out (`--device`, `--force`, `--all`) |
|
|
176
|
+
| `login` / `logout` | Sign this computer in or out (`--device`, `--browser`, `--force`, `--all`) |
|
|
127
177
|
| `watch -- <command>` | Run a command each time an answer is ready (`--once`) |
|
|
128
178
|
| `pickup` / `handoff` | The session-start and next-message hooks |
|
|
129
179
|
| `stopcheck` | The Stop hook: items for you left only in chat |
|
|
@@ -134,14 +184,22 @@ before or after the command.
|
|
|
134
184
|
|
|
135
185
|
## What it can see, and where it keeps things
|
|
136
186
|
|
|
137
|
-
The sign-in only hears answers to your own Claude Code assistants' questions
|
|
138
|
-
answer or change anything, and it doesn't show up as one.
|
|
139
|
-
(
|
|
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
|
|
140
193
|
which answers were already handed over (ids only), `claude-code.json` what `init` added, and `cli/` the hooks' copy
|
|
141
194
|
of the command line. `init`, `status` and the hooks read `~/.claude.json` (and a folder's `.mcp.json`, and
|
|
142
195
|
`~/.claude/settings.json`'s enabled plugins) only to see whether Claude Code has a `pendingyou` MCP server and where
|
|
143
196
|
it points. Nothing is sent anywhere but Pending You.
|
|
144
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
|
+
|
|
145
203
|
## Development
|
|
146
204
|
|
|
147
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
|
`;
|