pendingyou 0.13.0 → 0.14.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 +73 -11
- package/README.md +65 -20
- package/dist/api.js +0 -50
- package/dist/apps/claude-code.js +0 -38
- package/dist/apps/codex-app.js +0 -18
- package/dist/apps/codex-wake.js +290 -122
- package/dist/apps/codex.js +0 -68
- package/dist/apps/ids.js +0 -11
- package/dist/apps/opencode-plugin.js +39 -65
- package/dist/apps/opencode.js +0 -70
- package/dist/apps/pi-extension.js +60 -75
- package/dist/apps/pi.js +0 -62
- package/dist/apps/registry.js +0 -2
- package/dist/args.js +7 -40
- package/dist/bridge.js +0 -94
- package/dist/claude.js +0 -94
- package/dist/cli.js +0 -2
- package/dist/confirm.js +0 -69
- package/dist/credentials.js +0 -27
- package/dist/environment.js +0 -28
- package/dist/errors.js +0 -1
- package/dist/files.js +0 -16
- package/dist/format.js +30 -13
- package/dist/handed.js +71 -0
- package/dist/hold.js +0 -24
- package/dist/hooks.js +0 -26
- package/dist/index.js +0 -2
- package/dist/init.js +0 -32
- package/dist/install.js +0 -32
- package/dist/io.js +0 -10
- package/dist/json.js +0 -29
- package/dist/jsonc.js +0 -33
- package/dist/main.js +3 -26
- package/dist/mod.js +0 -23
- package/dist/oauth.js +0 -45
- package/dist/permission.js +0 -107
- package/dist/pickup.js +101 -62
- package/dist/qr.js +0 -28
- package/dist/redact.js +0 -77
- package/dist/remote.js +0 -90
- package/dist/setup.js +0 -30
- package/dist/shim.js +0 -23
- package/dist/signin.js +0 -42
- package/dist/state.js +19 -10
- package/dist/stopcheck.js +40 -84
- package/dist/toml.js +0 -34
- package/dist/version.js +1 -2
- package/dist/watch.js +3 -20
- package/mod/.claude-plugin/plugin.json +1 -1
- package/mod/README.md +2 -2
- package/mod/hooks/register.js +104 -61
- package/mod/hooks/wake.js +110 -108
- package/package.json +2 -1
- package/mod/tests/wake.test.ts +0 -247
package/CHANGELOG.md
CHANGED
|
@@ -1,11 +1,73 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.14.0 (2026-10-05)
|
|
4
|
+
|
|
5
|
+
- Questions your person hands one of your agents from another assistant (Delegate: Pending You's card link, or D on the
|
|
6
|
+
desk). Once the delegation's 5-second hold is over, Pending You's `/mcp/cli/answers` brings such a question to the
|
|
7
|
+
app's sign-in, marked `delegated` (who asked, `freely` or `loop`, the person's note), under the name the helper
|
|
8
|
+
answers with (`sessionLabel`, the task the person picked) and in that task's folder (`cwd`). The hooks used to say
|
|
9
|
+
“Pending You: they wrote back on req_…”; now a session start or message says “Pending You: your person handed you
|
|
10
|
+
req_…, a question from billing-webhooks (answer freely: your answer goes straight to billing-webhooks).”, the
|
|
11
|
+
question, “Their note: “…””, and “Call get_request (requestId req_…, name “infra”) to read it. If you know,
|
|
12
|
+
answer_delegated with your answer and how you know; if not, hand_back with what you checked.” (“they’ll see your
|
|
13
|
+
answer first” when the person kept themselves in the loop). The header says “your person handed you a question from
|
|
14
|
+
another assistant” (or “answered, and handed you …”), never “your person answered”, and the line to tell the person
|
|
15
|
+
first is “You handed me a question from …”.
|
|
16
|
+
- Which session hears one (`src/handed.ts`), the same for every app: a session that goes by its name in its folder
|
|
17
|
+
(the task the person picked; the most recent of several), else the session in that folder that used Pending You
|
|
18
|
+
most recently, never the one that asked it (a session that goes by the asking assistant's name), and never two: the
|
|
19
|
+
session that takes it claims it first in `state.json` (`claimHanded`), and gives it back if it couldn't be told. A
|
|
20
|
+
question with no folder (a task that never said one) goes to a session by its name anywhere, else the app's most
|
|
21
|
+
recent. A sign-in that only hears (`--oauth`) hears every computer's questions, so it leaves one whose `machine`
|
|
22
|
+
(the computer Pending You says it went to, as the Assistants page names it) is another computer than this one, by
|
|
23
|
+
its name or its hostname (the hostname, the names its code sign-ins were given, a Mac's own name, and, outside a
|
|
24
|
+
hook, Tailscale's); one with no `machine` goes by the folder. The hooks hand a session one at once when it goes by that name (or it's the session the listeners would
|
|
25
|
+
wake); another session in the folder only after 5 minutes, so the one it went to hears it first.
|
|
26
|
+
- Claude Code's wake mod wakes the session it's for: while a session used Pending You in the last 12 hours (as long as
|
|
27
|
+
Pending You offers its task as running), it asks `list_pending` with the session's name every minute for the first
|
|
28
|
+
half hour, then every 3 minutes; reads each new one with `get_request` (who asked, how its answer travels, whether
|
|
29
|
+
there's a note); and starts one turn, “Pending You: your person handed you “…” (req_…), a question from … Call
|
|
30
|
+
get_request (requestId req_…, name “infra”) to read it and their note. If you know, answer_delegated …; if not,
|
|
31
|
+
hand_back …”, never with the note. It claims each in its store (`handed:<requestId>`), so another session by the same
|
|
32
|
+
name doesn't hear it too, and never wakes a session for one it read or answered itself. Among sessions by one name,
|
|
33
|
+
the one working in the folder of the task it went to (`list_pending`'s `cwd`: the folder the session started in, or
|
|
34
|
+
one it gave its own cards, `~/…` or full) takes it, one elsewhere never does, and one that can't tell its folder
|
|
35
|
+
waits 5 minutes. The hand-off hook that
|
|
36
|
+
runs for the mod's prompt claims it in `state.json` without saying it twice. `answer_delegated` and `hand_back` are
|
|
37
|
+
among the calls it reads. Regenerated into the plugin's builds too, which reach installed plugins with the next guide
|
|
38
|
+
release.
|
|
39
|
+
- Codex: one listener for the app, `pendingyou listen --app codex --handed` (detached, a lease of its own), started by
|
|
40
|
+
the hooks while any thread may be handed a question, `codex queue`s each into the thread it's for, in the wake mod's
|
|
41
|
+
words, and gives it back (forgetting the thread) when Codex doesn't know the thread. Codex's hooks now remember each
|
|
42
|
+
thread's names and folder (`codex-threads.json`), and keep a thread for 12 hours after it last used the card tools,
|
|
43
|
+
with no card of its own waiting; a session start or message marks it active. A thread's own listener is never woken
|
|
44
|
+
for its own card while it's with another assistant.
|
|
45
|
+
- OpenCode and Pi: a session's listener also wakes it for a question handed to it, and keeps listening for one for 12
|
|
46
|
+
hours after the session last used the card tools (`opencode-threads.json`, `pi-threads.json`), while the app is
|
|
47
|
+
open. OpenCode's plugin starts it with each message while the record says so, and passes the session's folder to
|
|
48
|
+
`posted`; Pi's extension starts it as before, now for that too. Both start a listener again when a card call asked for
|
|
49
|
+
one while the last was on its way out: a card posted just as a session's first listener stopped (finding nothing to
|
|
50
|
+
listen for) went unheard until the next message.
|
|
51
|
+
- `watch` names a handed question's event `delegated_ready` (`PENDINGYOU_EVENT`), with `delegated` in the JSON on
|
|
52
|
+
stdin.
|
|
53
|
+
- The Stop check never asks a helper to post the question as a card of its own. `answer_delegated` and `hand_back`
|
|
54
|
+
count as the turn's card calls (Claude Code's transcript, OpenCode's turn, Pi's extension); a turn a handed
|
|
55
|
+
question's wake started (its first message the wake's words: Claude Code's transcript, OpenCode's turn, Pi's extension
|
|
56
|
+
says `handed`, Codex's next-message hook records it when Codex gives the message) that left the person something in
|
|
57
|
+
chat without either is asked once: “Your person handed you that question to answer, not to ask them back:
|
|
58
|
+
answer_delegated if you know, or hand_back with what you checked, unless you already did. Never post it as a card of
|
|
59
|
+
your own.” Codex's PostToolUse hook is trusted by its exact matcher, which names only the four card tools, so it never
|
|
60
|
+
sees those two calls (a new matcher would have to be trusted again).
|
|
61
|
+
- The package names no one, and carries only what runs. Its JavaScript is built without the source's comments, and so is
|
|
62
|
+
the wake mod's (`mod/hooks`); its examples and this changelog use made-up names (`--name build-01`); and the mod's
|
|
63
|
+
test, which only `claude plugin test` reads, stays out of the package. Nothing the command line does changes.
|
|
64
|
+
|
|
3
65
|
## 0.13.0 (2026-10-04)
|
|
4
66
|
|
|
5
|
-
- Claude Code's permission prompts on a card
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
to
|
|
67
|
+
- Claude Code's permission prompts on a card: a card and a push, cleared once you answer in the terminal. A Claude Code
|
|
68
|
+
agent kept asking to remove files and sat waiting until its person happened to look. `init` now adds Claude Code's
|
|
69
|
+
PermissionRequest hook (https://code.claude.com/docs/en/hooks: it runs as Claude Code is about to ask for permission
|
|
70
|
+
to use a tool, with `tool_name` and `tool_input`), through the shim with `--app claude-code`.
|
|
9
71
|
It never decides: it prints nothing, so the prompt shows exactly as before, and it's done in about a tenth of a
|
|
10
72
|
second. It writes the prompt down (its card's redacted words, never the tool's input) in the session's own file,
|
|
11
73
|
`~/.config/pendingyou/permission-cards/<session>.json`, and starts that session's one worker, detached (`pendingyou
|
|
@@ -67,8 +129,8 @@
|
|
|
67
129
|
approve: 1. whoami with your name. If it says app.setUp is true, another session did this: stop. 2. report_setup with
|
|
68
130
|
source codex, your name, skillSaved true, hears instant, and skillVersion set to whoami’s skill.latest. Its answer
|
|
69
131
|
has a testAreaId. 3. post_request a short test question in that testAreaId, tell your person it’s on its way, and end
|
|
70
|
-
your turn: you’re woken when they answer, so don’t ask them to type anything.” Pi's first setup
|
|
71
|
-
|
|
132
|
+
your turn: you’re woken when they answer, so don’t ask them to type anything.” Pi's first live setup spent about
|
|
133
|
+
20,000 tokens on the words before: “whoami with your name and guide true” answered with the whole guide
|
|
72
134
|
(87,000 characters), which Pi cut into a file the model then parsed, and “your test question in its testAreaId” sent
|
|
73
135
|
it looking for testAreaId in whoami's answer, where it isn't (report_setup's has it). A setup that has reported gets
|
|
74
136
|
the same calls without whoami's way out (its whoami says app.setUp is true); a test card answered says who picks it
|
|
@@ -144,7 +206,7 @@
|
|
|
144
206
|
|
|
145
207
|
## 0.11.0
|
|
146
208
|
|
|
147
|
-
- One step for the coding agents on a computer
|
|
209
|
+
- One step for the coding agents on a computer: `npx -y pendingyou@latest init` finds Claude Code
|
|
148
210
|
and Codex (`--app claude-code,codex` names some), says what it found and which it can't set up (an agent too old for
|
|
149
211
|
it is named, with the version it needs), and sets each up with one approval. Then: restart the agent, and its first
|
|
150
212
|
session finishes setting up by itself (whoami, report_setup, a test card). Asked for by name and not installed, or
|
|
@@ -222,10 +284,10 @@
|
|
|
222
284
|
- The next-message hook looks for this folder's answers with every message, not only while a hold had left an entry
|
|
223
285
|
behind: one request, as before, given up after 2 seconds, inside the hook's 3-second deadline. `state.json` no
|
|
224
286
|
longer keeps holds' waits.
|
|
225
|
-
- A computer with no browser (a
|
|
287
|
+
- A computer with no browser (a server you SSH into): one code approved on your phone sets Claude Code up.
|
|
226
288
|
`/mcp`'s Authenticate opened a browser on that machine, which nobody could reach, and `login --device` only signed
|
|
227
289
|
this command in. Now `init` there (an SSH session, or Linux with no display; `--device` forces it, `--browser` the
|
|
228
|
-
browser) signs the machine in by code as a Claude Code connection of its own ("Claude Code on
|
|
290
|
+
browser) signs the machine in by code as a Claude Code connection of its own ("Claude Code on build-01" on your
|
|
229
291
|
Assistants page, named by `--name`, else Tailscale's name for it, else its hostname; `login --name` with a new name
|
|
230
292
|
renames the connection with one more approval, which `login` suggests for a cloud's own name like `ip-10-42-1-252`;
|
|
231
293
|
QR code of the link at a terminal), then adds the MCP server with a `headersHelper` instead of OAuth
|
|
@@ -239,8 +301,8 @@
|
|
|
239
301
|
PATH), each with the command that fixes it, `--origin` included off production. `uninstall` takes the helper out and
|
|
240
302
|
says the connection stays on your Assistants page. Needs Pending You's Worker from the same release.
|
|
241
303
|
- `login` over SSH signs in by code without `--device`. A sign-in by code names the computer, and the phone asks "Let
|
|
242
|
-
|
|
243
|
-
a choice of assistant any more
|
|
304
|
+
build-01 hear your answers for the assistants running on it?"; in the browser, "Let this computer hear…". Neither
|
|
305
|
+
offers a choice of assistant any more: a computer hears for the assistants running on it. A sign-in
|
|
244
306
|
pointed at one assistant before then keeps hearing it, so `watch` for a Muse or another app can't be set up anew.
|
|
245
307
|
- Routing by sender: the hooks and `hold` ask Pending You for Claude Code's cards only (`source=claude-code`), so a card
|
|
246
308
|
Codex or another app posted from the same folder never reaches a Claude Code session, even with a sign-in pointed at
|
package/README.md
CHANGED
|
@@ -27,7 +27,8 @@ Run it once on each computer, in a terminal. It finds the agents on the computer
|
|
|
27
27
|
in chat ([below](#nothing-left-only-in-chat)); and, for Claude Code, a card when it waits for your OK to run something
|
|
28
28
|
([below](#when-claude-code-waits-for-your-ok)),
|
|
29
29
|
- and wakes it when you answer: Claude Code 2.1.287 or later through the wake mod, Codex through `codex queue`, OpenCode
|
|
30
|
-
through its plugin and Pi through its extension (which also do the hooks' work there)
|
|
30
|
+
through its plugin and Pi through its extension (which also do the hooks' work there); and when you hand it a
|
|
31
|
+
question from another of your assistants ([below](#when-you-hand-an-agent-a-question)).
|
|
31
32
|
|
|
32
33
|
Then it says what to do next, and the one sentence to say if it's needed:
|
|
33
34
|
|
|
@@ -111,7 +112,8 @@ it with `codex queue`: “Pending You: your person answered “…” (req_…).
|
|
|
111
112
|
answer itself never goes in a command's arguments, where other programs could see it). Codex starts a turn at
|
|
112
113
|
once when the thread is open and idle, after the current one when it's working, or when you next open it. In the Codex
|
|
113
114
|
app, a chat it has open is woken while the app runs (one it let go, idle 3 hours or behind 10 newer idle chats, when
|
|
114
|
-
you open it again).
|
|
115
|
+
you open it again). One more listener, for the whole app, queues a question you hand a thread from another assistant
|
|
116
|
+
([below](#when-you-hand-an-agent-a-question)).
|
|
115
117
|
|
|
116
118
|
## OpenCode
|
|
117
119
|
|
|
@@ -139,7 +141,8 @@ does in OpenCode what the hooks do elsewhere, through the same hooks' script: ea
|
|
|
139
141
|
reminder and any answer waiting (hidden in OpenCode's view, read by the model), a session's first message also says
|
|
140
142
|
what setup has left, a session that left you a question in chat is asked once to put it on a card (with a toast saying
|
|
141
143
|
why), and when you answer a card a session posted, the plugin starts a turn in that session: “Pending You: your person
|
|
142
|
-
answered “…” (req_…). Call get_request …” (never the answer itself). That works while OpenCode is open, idle or busy
|
|
144
|
+
answered “…” (req_…). Call get_request …” (never the answer itself). That works while OpenCode is open, idle or busy,
|
|
145
|
+
and for a question you hand the session from another assistant too ([below](#when-you-hand-an-agent-a-question)).
|
|
143
146
|
|
|
144
147
|
Then restart OpenCode (`/exit`, then `opencode --continue`): it finishes setting up the first time you write to it.
|
|
145
148
|
|
|
@@ -167,7 +170,9 @@ Pi has no hooks, so `init` gives it an extension: `~/.config/pendingyou/pi/pendi
|
|
|
167
170
|
the same hooks' script as a session starts, with each message, after Pending You's card tools and as a run ends (a run
|
|
168
171
|
that leaves you something only in chat goes on once), and when you answer a card a session posted it starts a turn in
|
|
169
172
|
that session, idle or busy: “Pending You: your person answered “…” (req_…). Call get_request …” (never the answer
|
|
170
|
-
itself)
|
|
173
|
+
itself), and the same for a question you hand it from another assistant
|
|
174
|
+
([below](#when-you-hand-an-agent-a-question)). In Pi's terminal the first session finishes setting up by itself, once
|
|
175
|
+
Pending You's tools are there.
|
|
171
176
|
|
|
172
177
|
Then run `/reload` in each Pi session that's open, or start `pi`. Pi's shell waits for a command to end, so in Pi
|
|
173
178
|
`npx pendingyou hold` answers at once instead of waiting.
|
|
@@ -208,6 +213,13 @@ replied on a card comes from Codex's own hook for Pending You's tools in that tu
|
|
|
208
213
|
action card titled “Confirm in Codex on <computer>: <step>” when it asked you to type something there. OpenCode's plugin
|
|
209
214
|
and Pi's extension (0.12.0) do the same in theirs.
|
|
210
215
|
|
|
216
|
+
A question you hand an agent from another assistant (0.14.0) is never asked to go on a card of its own: answering it
|
|
217
|
+
(`answer_delegated`) or giving it back (`hand_back`) puts it in front of you, so it counts as the turn's card call. A
|
|
218
|
+
turn that such a question started, and that left you something in chat without doing either, is asked once to answer
|
|
219
|
+
it or hand it back instead (about 45 tokens). Codex's hook for Pending You's tools is trusted by its exact list of
|
|
220
|
+
tools, which doesn't name those two, so there the check knows only that the turn started with one (when Codex gives the
|
|
221
|
+
message to its next-message hook), and says “unless you already did”.
|
|
222
|
+
|
|
211
223
|
**On 0.2.0 with another origin?** Its hooks blocked every message (`There's no "--origin" command`). Run
|
|
212
224
|
`npx pendingyou@latest init --origin <url>` again to repair them; see [CHANGELOG.md](CHANGELOG.md).
|
|
213
225
|
|
|
@@ -269,10 +281,38 @@ allowed and the server has connected). When it isn't, it starts one turn once th
|
|
|
269
281
|
Pending You: it’s connected here already.” and exactly what to do. Once per session, in at most three sessions, never
|
|
270
282
|
two within ten minutes, and never again once Pending You says it's set up.
|
|
271
283
|
|
|
284
|
+
## When you hand an agent a question
|
|
285
|
+
|
|
286
|
+
In Pending You you can hand a card to another of your assistants, a coding task included (Delegate: the card's link,
|
|
287
|
+
or D on the desk): it answers for you (“Answer freely”), or its answer comes back to you first (“Keep me in the loop”),
|
|
288
|
+
or it hands the card back with what it checked. Since 0.14.0 the task you picked hears it the way it hears your
|
|
289
|
+
answers, once the 5-second window to undo is over:
|
|
290
|
+
|
|
291
|
+
- **Which session**: the one that goes by the task's name in its folder (the most recent, when several do); else the
|
|
292
|
+
session in that folder that used Pending You most recently; never the one that asked, and never two. A sign-in that
|
|
293
|
+
only hears (`--oauth`) hears every computer's, so it leaves one Pending You says went to another computer (by its
|
|
294
|
+
name or its hostname); one that names no computer goes by the folder.
|
|
295
|
+
- **Claude Code**: the wake mod looks for questions handed to the session's name (`list_pending`, every minute while
|
|
296
|
+
the session used Pending You in the last half hour, then every 3 minutes, for 12 hours), reads each with
|
|
297
|
+
`get_request`, and starts a turn: “Pending You: your person handed you “Which AWS account for staging?” (req_…), a
|
|
298
|
+
question from billing-webhooks (answer freely: your answer goes straight to billing-webhooks). Call get_request
|
|
299
|
+
(requestId req_…, name “infra”) to read it and their note. If you know, answer_delegated with your answer and how you
|
|
300
|
+
know; if not, hand_back with what you checked.” Among sessions by one name, the one working in the task's folder
|
|
301
|
+
takes it (as `list_pending` gives it, `~/…` or a full path), one in another folder never does, and one that can't
|
|
302
|
+
tell its folder waits 5 minutes.
|
|
303
|
+
- **Codex**: one listener for the app queues it into the thread it's for (`codex queue`, the same words).
|
|
304
|
+
- **OpenCode and Pi**: the session's listener starts the turn through the plugin or extension; a session that used
|
|
305
|
+
Pending You's card tools keeps listening for 12 hours, while the app is open.
|
|
306
|
+
- **Otherwise**: a session start or your next message in that folder brings it, with your note: at once in the
|
|
307
|
+
session that goes by the task's name, after 5 minutes in any other.
|
|
308
|
+
|
|
309
|
+
Your note never goes in a program's arguments, where any other program on the computer could read it: the agent reads
|
|
310
|
+
it with `get_request`.
|
|
311
|
+
|
|
272
312
|
## A computer with no browser
|
|
273
313
|
|
|
274
|
-
On a
|
|
275
|
-
reach. `init` signs in with a code there instead: SSH in, and at the prompt there run
|
|
314
|
+
On a computer you reach over SSH (a build server, a machine in the cloud), an agent's own sign-in opens a browser on
|
|
315
|
+
that machine, which you can't reach. `init` signs in with a code there instead: SSH in, and at the prompt there run
|
|
276
316
|
|
|
277
317
|
```sh
|
|
278
318
|
npx -y pendingyou@latest init
|
|
@@ -289,15 +329,15 @@ hooks find Node anyway when the agents run without your shell's PATH (a systemd
|
|
|
289
329
|
Remote Control): on PATH, nvm's default, the one `init` ran with, or where Node usually is.
|
|
290
330
|
|
|
291
331
|
The machine is called by its Tailscale name when Tailscale is installed and running (as `tailscale status` shows it),
|
|
292
|
-
else by its hostname, or by `--name
|
|
293
|
-
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:
|
|
332
|
+
else by its hostname, or by `--name build-01` on `init` or `login`; a Mac by its own name (Sharing in System Settings).
|
|
333
|
+
A cloud's own hostname (AWS's `ip-10-42-1-252`) is kept when there's nothing better, and `login` says how to change it:
|
|
294
334
|
`npx pendingyou login --name <name>` asks for one more approval and renames the connection, which stays the same one.
|
|
295
335
|
If an agent's sign-in ends (30 days, or you remove it on your Assistants page), `npx pendingyou login` (`--app codex`
|
|
296
336
|
for Codex's) approves a new code and keeps the same connection.
|
|
297
337
|
|
|
298
338
|
Only want this command's own sign-in (for `watch`, below)? `npx pendingyou login --device` signs it in with a code,
|
|
299
|
-
hearing answers only, and adds no connection. Your phone asks "Let
|
|
300
|
-
on it?", with no choice of assistant.
|
|
339
|
+
hearing answers only, and adds no connection. Your phone asks "Let build-01 hear your answers for the assistants
|
|
340
|
+
running on it?", with no choice of assistant.
|
|
301
341
|
|
|
302
342
|
## Always-on scripts
|
|
303
343
|
|
|
@@ -309,10 +349,11 @@ npx pendingyou watch -- ./on-answer.sh
|
|
|
309
349
|
|
|
310
350
|
It hears your Claude Code assistants' answers. Signing in asks no choice of assistant (since 2026-10-04: "Let pi hear
|
|
311
351
|
your answers for the assistants running on it?"); a sign-in pointed at one assistant before then keeps hearing that
|
|
312
|
-
one. The command runs without a shell, once per answer, message or fallback that comes due,
|
|
313
|
-
gets the request as JSON on stdin
|
|
314
|
-
`
|
|
315
|
-
|
|
352
|
+
one. The command runs without a shell, once per answer, message or fallback that comes due, or (0.14.0) question you
|
|
353
|
+
hand the assistant from another one, oldest first. It gets the request as JSON on stdin (a handed question with
|
|
354
|
+
`delegated`: who asked, how its answer travels, your note), and `PENDINGYOU_REQUEST_ID`, `PENDINGYOU_EVENT`
|
|
355
|
+
(`answer_ready`, `message_ready`, `fallback_due` or `delegated_ready`), `PENDINGYOU_VERSION` and `PENDINGYOU_ORIGIN` in
|
|
356
|
+
its environment. A failing command is tried twice more, then skipped. `--once` stops after the first.
|
|
316
357
|
|
|
317
358
|
## Commands
|
|
318
359
|
|
|
@@ -349,20 +390,24 @@ reminded an unfinished setup, and when; `permission-cards/` each Claude Code ses
|
|
|
349
390
|
and its card (ids, hashes and the card's redacted words: never what the tool was given), gone once nothing waits;
|
|
350
391
|
`claude-code.json`, `codex.json`, `opencode.json` and `pi.json` what `init`
|
|
351
392
|
added; `codex-threads.json`, `opencode-threads.json` and `pi-threads.json` which cards each Codex thread or OpenCode or
|
|
352
|
-
Pi session waits on (
|
|
393
|
+
Pi session waits on, and (0.14.0) the names it went by and its folder, kept 12 hours for a question you may hand it
|
|
394
|
+
(ids, names, folders and titles: never an answer or a note); `pi/` Pi's extension; and `cli/` the hooks' copy of the
|
|
353
395
|
command line. `init`, `status` and the hooks read `~/.claude.json` (and a folder's `.mcp.json`, and
|
|
354
396
|
`~/.claude/settings.json`'s enabled plugins), `~/.codex/config.toml`, OpenCode's config and Pi's `mcp.json` only to see
|
|
355
|
-
whether the agent has a `pendingyou` MCP server and where it points.
|
|
397
|
+
whether the agent has a `pendingyou` MCP server and where it points. With a sign-in that only hears, the hooks and
|
|
398
|
+
listeners tell this computer's handed questions from another's by its names: its hostname, the names its sign-ins by
|
|
399
|
+
code were given, a Mac's own name (`scutil --get ComputerName`) and, outside a hook, Tailscale's (`tailscale status`). For a permission prompt's card, the worker reads
|
|
356
400
|
the end of the Claude Code session's transcript only for the name the session gave Pending You, and `git remote
|
|
357
401
|
get-url origin` in its folder (sent without any user name or password in it). Nothing is sent anywhere but Pending
|
|
358
402
|
You. OpenCode's plugin reads its session's messages only when it goes idle, for the check that
|
|
359
403
|
nothing was left in chat, and hands the hooks' script just that turn's words and tool names.
|
|
360
404
|
|
|
361
405
|
The wake mod runs inside Claude Code and reaches Pending You only through the session's own connection, with
|
|
362
|
-
`get_request` on the session's own cards (and, in `init`'s copy, `whoami` to see whether setup is finished)
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
406
|
+
`get_request` on the session's own cards (and, in `init`'s copy, `whoami` to see whether setup is finished), and (0.14.0)
|
|
407
|
+
`list_pending` with the session's name and `get_request` on a question you handed it. It keeps each session's cards (id,
|
|
408
|
+
title, the agent's name, where each stands), and which handed questions a session was told of (ids and when), in Claude
|
|
409
|
+
Code's store for mods, `~/.claude/plugins/store/`, and forgets them once they close or after 7 days. It marks the copy
|
|
410
|
+
acting for a session in that session's environment (`PENDINGYOU_WAKE_OWNER`).
|
|
366
411
|
|
|
367
412
|
## Development
|
|
368
413
|
|
package/dist/api.js
CHANGED
|
@@ -1,32 +1,11 @@
|
|
|
1
|
-
// Calls to Pending You with the command line's sign-in: a fresh access token (refreshed under the credentials lock,
|
|
2
|
-
// so a hold and a hook never spend the same refresh token), one retry after a 401, and plain errors.
|
|
3
|
-
//
|
|
4
|
-
// Hooks never refresh in their own process. A hook has a hard deadline (main.ts), and a refresh cut off halfway can
|
|
5
|
-
// end the sign-in: the server rotates the refresh token as it answers, and keeps exactly one earlier token valid
|
|
6
|
-
// (@cloudflare/workers-oauth-provider's previousRefreshTokenId). A refresh abandoned before its answer arrives is
|
|
7
|
-
// harmless on its own (the stored token is that earlier one, and still works), but if the server finishes it after a
|
|
8
|
-
// later refresh has stored a newer token, that newer token stops working and the next refresh gets invalid_grant. So
|
|
9
|
-
// a hook that needs a new token starts `pendingyou refresh` detached (background), which runs to the end under the
|
|
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
|
-
// 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
1
|
import { DEFAULT_APP } from "./apps/ids.js";
|
|
18
2
|
import { credentialsPath, readCredential, updateCredential, } from "./credentials.js";
|
|
19
3
|
import { isLocked } from "./files.js";
|
|
20
4
|
import { OAuthError, refresh } from "./oauth.js";
|
|
21
|
-
/** Refresh this long before the access token runs out. */
|
|
22
5
|
const EARLY_MS = 60_000;
|
|
23
|
-
/** A hook starts a background refresh this long before the access token runs out, and goes on with the old one. */
|
|
24
6
|
export const SOON_MS = 5 * 60_000;
|
|
25
|
-
/** How long a hook waits for a background refresh when it has no working token. */
|
|
26
7
|
export const REFRESH_WAIT_MS = 1200;
|
|
27
|
-
/** An access token with less than this left isn't worth sending. */
|
|
28
8
|
const USABLE_MS = 10_000;
|
|
29
|
-
/** Not signed in on this computer (for this origin), or the sign-in ended: the person runs `pendingyou login`. */
|
|
30
9
|
export class SignInNeeded extends Error {
|
|
31
10
|
name = 'SignInNeeded';
|
|
32
11
|
ended;
|
|
@@ -35,26 +14,19 @@ export class SignInNeeded extends Error {
|
|
|
35
14
|
this.ended = ended;
|
|
36
15
|
}
|
|
37
16
|
}
|
|
38
|
-
/** Pending You couldn't be reached, or had a problem: worth trying again later. */
|
|
39
17
|
export class Unavailable extends Error {
|
|
40
18
|
name = 'Unavailable';
|
|
41
|
-
/** Seconds the server asked to wait, when it said. */
|
|
42
19
|
retryAfter;
|
|
43
20
|
constructor(message, retryAfter) {
|
|
44
21
|
super(message);
|
|
45
22
|
this.retryAfter = retryAfter;
|
|
46
23
|
}
|
|
47
24
|
}
|
|
48
|
-
/**
|
|
49
|
-
* Refreshes the sign-in under the lock unless it no longer needs it: a token with more than `earlyMs` left (or, when
|
|
50
|
-
* `force`d after a 401, a refresh token another process already replaced) is used as it is.
|
|
51
|
-
*/
|
|
52
25
|
async function refreshStored(io, origin, stored, options, app) {
|
|
53
26
|
let ended = false;
|
|
54
27
|
const updated = await updateCredential(io, origin, async (current) => {
|
|
55
28
|
if (!current)
|
|
56
29
|
return null;
|
|
57
|
-
// Another process may have refreshed while this one waited for the lock.
|
|
58
30
|
if (current.refreshToken !== stored.refreshToken && current.expiresAt - EARLY_MS > io.now())
|
|
59
31
|
return current;
|
|
60
32
|
if (!options.force && current.expiresAt - options.earlyMs > io.now())
|
|
@@ -84,7 +56,6 @@ async function freshToken(io, origin, force, app) {
|
|
|
84
56
|
return stored.accessToken;
|
|
85
57
|
return (await refreshStored(io, origin, stored, { force, earlyMs: EARLY_MS }, app)).accessToken;
|
|
86
58
|
}
|
|
87
|
-
/** What a process starts to refresh an app's sign-in in the background: `--app` only for an app but Claude Code. */
|
|
88
59
|
export const refreshArgs = (origin, app, force = false) => [
|
|
89
60
|
'refresh',
|
|
90
61
|
'--origin',
|
|
@@ -92,10 +63,6 @@ export const refreshArgs = (origin, app, force = false) => [
|
|
|
92
63
|
...(app === DEFAULT_APP ? [] : ['--app', app]),
|
|
93
64
|
...(force ? ['--force'] : []),
|
|
94
65
|
];
|
|
95
|
-
/**
|
|
96
|
-
* `pendingyou refresh`, what a hook starts in the background: refreshes the sign-in when it runs out within SOON_MS
|
|
97
|
-
* (or `force`, after a 401), under the lock, to the end. Quiet; true when there's a sign-in afterwards.
|
|
98
|
-
*/
|
|
99
66
|
export async function refreshInBackground(io, origin, force, app = DEFAULT_APP) {
|
|
100
67
|
const stored = await readCredential(io, origin, app);
|
|
101
68
|
if (!stored)
|
|
@@ -105,7 +72,6 @@ export async function refreshInBackground(io, origin, force, app = DEFAULT_APP)
|
|
|
105
72
|
await refreshStored(io, origin, stored, { force, earlyMs: SOON_MS }, app);
|
|
106
73
|
return true;
|
|
107
74
|
}
|
|
108
|
-
/** A hook's token: never refreshed here (see the top of this file). */
|
|
109
75
|
async function hookToken(io, origin, force, signal, app) {
|
|
110
76
|
const stored = await readCredential(io, origin, app);
|
|
111
77
|
if (!stored)
|
|
@@ -113,10 +79,8 @@ async function hookToken(io, origin, force, signal, app) {
|
|
|
113
79
|
const left = stored.expiresAt - io.now();
|
|
114
80
|
if (!force && left > SOON_MS)
|
|
115
81
|
return stored.accessToken;
|
|
116
|
-
// One refresher at a time: a lock held now is a refresh (or a write) already under way.
|
|
117
82
|
if (!(await isLocked(credentialsPath(io))))
|
|
118
83
|
io.background(refreshArgs(origin, app, force));
|
|
119
|
-
// Still good for a few seconds: this request goes with it, the next one with the new token.
|
|
120
84
|
if (!force && left > USABLE_MS)
|
|
121
85
|
return stored.accessToken;
|
|
122
86
|
const until = io.now() + REFRESH_WAIT_MS;
|
|
@@ -130,14 +94,6 @@ async function hookToken(io, origin, force, signal, app) {
|
|
|
130
94
|
}
|
|
131
95
|
throw new Unavailable('The sign-in is being refreshed in the background.');
|
|
132
96
|
}
|
|
133
|
-
/**
|
|
134
|
-
* The token `pendingyou mcp-headers` hands an app's MCP server (remote.ts), and the stdio bridge sends with each
|
|
135
|
-
* message (bridge.ts). The app runs the helper each time it connects and gives it 10 seconds, and the bridge may be
|
|
136
|
-
* stopped at any moment, so, as for a hook, a refresh is never made in this process: within 5 minutes of expiry it
|
|
137
|
-
* starts `pendingyou refresh` detached and waits for its token, up to `waitMs`, then uses the old one while it still
|
|
138
|
-
* works. A token handed over has at least 10 seconds left; the app runs the helper again on a 401. `force` (the bridge,
|
|
139
|
-
* after a 401) refreshes whatever the expiry says, and hands over only the new token.
|
|
140
|
-
*/
|
|
141
97
|
export async function helperToken(io, origin, waitMs, app = DEFAULT_APP, force = false) {
|
|
142
98
|
const stored = await readCredential(io, origin, app);
|
|
143
99
|
if (!stored)
|
|
@@ -159,7 +115,6 @@ export async function helperToken(io, origin, waitMs, app = DEFAULT_APP, force =
|
|
|
159
115
|
return stored.accessToken;
|
|
160
116
|
throw new Unavailable('Pending You couldn’t be reached to refresh the sign-in.');
|
|
161
117
|
}
|
|
162
|
-
/** Aborts on `signal` or after `ms`, whichever comes first (AbortSignal.any needs Node 20.3). */
|
|
163
118
|
function either(signal, ms) {
|
|
164
119
|
const timeout = AbortSignal.timeout(ms);
|
|
165
120
|
if (!signal)
|
|
@@ -172,11 +127,6 @@ function either(signal, ms) {
|
|
|
172
127
|
timeout.addEventListener('abort', abort, { once: true });
|
|
173
128
|
return controller.signal;
|
|
174
129
|
}
|
|
175
|
-
/**
|
|
176
|
-
* GETs `path` on `origin` as the command line, with an app's sign-in (Claude Code's unless `app` says), and returns the
|
|
177
|
-
* JSON body with its status. 401 after a refresh means the sign-in is over (revoked, or 30 days passed). Network
|
|
178
|
-
* failures and 5xx are `Unavailable`.
|
|
179
|
-
*/
|
|
180
130
|
export async function getJson(io, origin, path, timeoutMs, signal, options = {}) {
|
|
181
131
|
const app = options.app ?? DEFAULT_APP;
|
|
182
132
|
for (let attempt = 0;; attempt++) {
|
package/dist/apps/claude-code.js
CHANGED
|
@@ -1,16 +1,3 @@
|
|
|
1
|
-
// Claude Code (0.11.0's module of what claude.ts carries out): what `pendingyou init` decides before anyone signs in,
|
|
2
|
-
// what it does after, `status`'s section and the lines init ends with.
|
|
3
|
-
//
|
|
4
|
-
// Its MCP server signs in through this computer's own connection ("Claude Code on devbox"), by the headers helper, on
|
|
5
|
-
// every computer but Windows (where Claude Code runs a headersHelper through cmd) or with `--oauth`; the hooks use the
|
|
6
|
-
// same connection, so one approval covers both. A pendingyou server that's there decides the rest, before signing in:
|
|
7
|
-
// - ours, through the helper, at this Pending You: kept.
|
|
8
|
-
// - one at this Pending You that signs in by itself (Claude Code's own /mcp → Authenticate), or no server beside an
|
|
9
|
-
// enabled Pending You plugin for it (whose server is OAuth): with no browser here it's switched without a word (as
|
|
10
|
-
// 0.10.0 did: that sign-in can't finish here); at a terminal init asks, Yes by default; `--yes` switches; with nobody
|
|
11
|
-
// to ask it's kept, and init says how to switch. Kept, Claude Code signs in by itself and the hooks get a sign-in
|
|
12
|
-
// that only hears; kept beside the plugin, no skill is saved either: the plugin carries it.
|
|
13
|
-
// - one at another Pending You: replaced once the person says so (or `--yes`), or else Claude Code is left alone.
|
|
14
1
|
import { addCommand, claudeState, helperServer, installClaude, mcpFromClaude, mcpFromFile, PLUGIN_ORIGINS, permissionBlocker, pluginFor, readManifest, removeCommand, sameUrl, uninstallClaude, } from "../claude.js";
|
|
15
2
|
import { connectionMachine, readCredential } from "../credentials.js";
|
|
16
3
|
import { differs, serverForFolder, siteOf } from "../environment.js";
|
|
@@ -21,12 +8,7 @@ import { connectionTitle, FINISH_SAY, setupOf, setupStatus } from "../setup.js";
|
|
|
21
8
|
import { shimPath } from "../shim.js";
|
|
22
9
|
import { APP_NAMES } from "./ids.js";
|
|
23
10
|
const NAME = APP_NAMES['claude-code'];
|
|
24
|
-
/** The sentence's lead-in, and what restarting means, in init's last lines. */
|
|
25
11
|
const RESTART = 'restart Claude Code here (/exit, then claude --continue), or start it in the folder you work in';
|
|
26
|
-
/**
|
|
27
|
-
* Whether to move a server that signs in by itself to this computer's sign-in: without a word where there's no
|
|
28
|
-
* browser, `--yes`, or Yes (the default) at a terminal; kept, with nobody to ask.
|
|
29
|
-
*/
|
|
30
12
|
async function switches(io, ctx, what) {
|
|
31
13
|
if (ctx.headless || ctx.yes)
|
|
32
14
|
return true;
|
|
@@ -36,7 +18,6 @@ async function switches(io, ctx, what) {
|
|
|
36
18
|
const answer = await io.ask('Switch Claude Code to this computer’s sign-in? [Y/n] ');
|
|
37
19
|
return !/^\s*no?\s*$/i.test(answer);
|
|
38
20
|
}
|
|
39
|
-
/** The plan for a server at another Pending You: replaced once the person says so, or Claude Code left alone. */
|
|
40
21
|
async function replaces(io, ctx, server, url) {
|
|
41
22
|
ctx.progress(`Claude Code’s pendingyou MCP server points at ${server.url}, not ${url}.\n`);
|
|
42
23
|
if (ctx.yes)
|
|
@@ -92,7 +73,6 @@ export const claudeCode = {
|
|
|
92
73
|
};
|
|
93
74
|
}
|
|
94
75
|
else if (server?.url && !sameUrl(server.url, url)) {
|
|
95
|
-
// Another Pending You (staging, when production was asked for): asked before anyone signs in.
|
|
96
76
|
if (!(await replaces(io, ctx, server, url))) {
|
|
97
77
|
const commands = ` ${removeCommand(server.scope)}\n ${add}`;
|
|
98
78
|
return {
|
|
@@ -123,7 +103,6 @@ export const claudeCode = {
|
|
|
123
103
|
else if (ours)
|
|
124
104
|
plan = { action: 'switch', server };
|
|
125
105
|
else if (server.helper) {
|
|
126
|
-
// Someone else's headersHelper: theirs to change. The hooks hear through a sign-in of their own.
|
|
127
106
|
mode = 'oauth';
|
|
128
107
|
plan = helperMode
|
|
129
108
|
? {
|
|
@@ -152,7 +131,6 @@ export const claudeCode = {
|
|
|
152
131
|
}
|
|
153
132
|
}
|
|
154
133
|
else if (plugin) {
|
|
155
|
-
// No server added by hand, so the Pending You plugin's is Claude Code's: one added by hand would win over it.
|
|
156
134
|
if (helperMode &&
|
|
157
135
|
(await switches(io, ctx, 'The Pending You plugin signs Claude Code in to Pending You by itself here.')))
|
|
158
136
|
plan = { action: 'add', plugin: true };
|
|
@@ -224,10 +202,6 @@ export const claudeCode = {
|
|
|
224
202
|
return claudeStatus(io, ctx);
|
|
225
203
|
},
|
|
226
204
|
};
|
|
227
|
-
/**
|
|
228
|
-
* status's line for the permission-prompt hooks (0.13.0): on; off, as the person chose; or why Claude Code here can't
|
|
229
|
-
* have them. Never a reason status isn't Ready.
|
|
230
|
-
*/
|
|
231
205
|
function permissionLine(io, claude, manifest, helper, flag) {
|
|
232
206
|
const label = 'Permission prompts:';
|
|
233
207
|
if (claude.permission)
|
|
@@ -241,7 +215,6 @@ function permissionLine(io, claude, manifest, helper, flag) {
|
|
|
241
215
|
? ` ${label} no card when Claude Code waits for your OK: ${blocker}`
|
|
242
216
|
: ` missing ${label} no card when Claude Code waits for your OK; run npx pendingyou@latest init${flag}`;
|
|
243
217
|
}
|
|
244
|
-
/** Claude Code's section of `pendingyou status`: what's set up and what's missing, each with the command that fixes it. */
|
|
245
218
|
async function claudeStatus(io, ctx) {
|
|
246
219
|
const { origin, signIn } = ctx;
|
|
247
220
|
const [claude, here, manifest] = await Promise.all([
|
|
@@ -252,8 +225,6 @@ async function claudeStatus(io, ctx) {
|
|
|
252
225
|
const credential = await readCredential(io, origin);
|
|
253
226
|
const flag = originArgs(origin);
|
|
254
227
|
const mcp = claude.mcp;
|
|
255
|
-
// Through the helper (this computer's own sign-in): Claude Code's server runs ours, init set it up so, or nothing is
|
|
256
|
-
// set up yet and init would (everywhere but Windows).
|
|
257
228
|
const helped = usesHelper(mcp, origin);
|
|
258
229
|
const helper = io.platform !== 'win32' &&
|
|
259
230
|
(helped ||
|
|
@@ -267,7 +238,6 @@ async function claudeStatus(io, ctx) {
|
|
|
267
238
|
const url = `${origin}/mcp`;
|
|
268
239
|
const plugin = claude.plugins.find((id) => siteOf(PLUGIN_ORIGINS[id] ?? '') === siteOf(origin));
|
|
269
240
|
const sameServer = mcp !== false && mcp !== 'unknown' && (!mcp.url || mcp.url.replace(/\/+$/, '') === url);
|
|
270
|
-
// The plugin's server is Claude Code's when nobody added one by hand.
|
|
271
241
|
const pluginServer = mcp === false && Boolean(plugin);
|
|
272
242
|
const mcpOk = helper ? sameServer && helped : sameServer || pluginServer;
|
|
273
243
|
const mcpText = mcp === 'unknown'
|
|
@@ -287,7 +257,6 @@ async function claudeStatus(io, ctx) {
|
|
|
287
257
|
: `pendingyou (${mcp.url}) signs in through a browser${why ? ', which can’t finish on this computer' : ''}; run npx pendingyou init${flag}`;
|
|
288
258
|
const form = claude.form;
|
|
289
259
|
const hooksRun = form?.runs ?? true;
|
|
290
|
-
// The wake mod (0.10.0): Claude Code 2.1.287 or later loads it from the plugin or from CLAUDE_CODE_PLUGIN_DIRS.
|
|
291
260
|
const mod = claude.mod;
|
|
292
261
|
const modOk = mod.loads && (mod.plugin || mod.present);
|
|
293
262
|
const modText = !claude.claude
|
|
@@ -311,9 +280,7 @@ async function claudeStatus(io, ctx) {
|
|
|
311
280
|
: form.form === 'npx'
|
|
312
281
|
? `they run through npx${via}, which is slower; run npx pendingyou@latest init${flag} to run them directly`
|
|
313
282
|
: 'they run pendingyou from PATH or a file of your own';
|
|
314
|
-
// login makes a connection only once Claude Code's server signs in through the helper; init does both.
|
|
315
283
|
const fix = `npx pendingyou ${helped ? 'login' : 'init'}${flag}`;
|
|
316
|
-
// The connection's name as Pending You has it, so status never drifts from it; else this computer's own record.
|
|
317
284
|
const title = signIn.state === 'ok' && signIn.for
|
|
318
285
|
? connectionTitle(signIn.for)
|
|
319
286
|
: `${NAME} on ${(await connectionMachine(io, origin)) ?? (machineOf(io.host) || 'this computer')}`;
|
|
@@ -338,7 +305,6 @@ async function claudeStatus(io, ctx) {
|
|
|
338
305
|
: signIn.state === 'none'
|
|
339
306
|
? `not signed in; run npx pendingyou login${flag}`
|
|
340
307
|
: 'Pending You couldn’t be reached';
|
|
341
|
-
// The helper as Claude Code runs it at its barest: found Node, and runs pendingyou's copy.
|
|
342
308
|
const check = helper ? await checkHelper(io, origin) : null;
|
|
343
309
|
const skillOk = claude.skill || Boolean(plugin);
|
|
344
310
|
const skillText = claude.skill
|
|
@@ -357,17 +323,13 @@ async function claudeStatus(io, ctx) {
|
|
|
357
323
|
` ok Plugin: ${plugin} is enabled${pluginServer ? '' : mcp !== false && mcp !== 'unknown' ? '; Claude Code uses the server added by hand instead of its own' : ''}`,
|
|
358
324
|
]
|
|
359
325
|
: []), ` ${mark(skillOk)} Skill: ${skillText}`, ` ${mark(claude.hooks && hooksRun)} Hooks: ${claude.hooks ? `session-start pickup and next-message hand-off; ${formText}` : `not installed; run npx pendingyou init${flag}`}`, ` ${mark(claude.stopcheck)} Stop check: ${claude.stopcheck ? 'asks Claude Code to post what it leaves you in chat' : `not installed; run npx pendingyou init${flag}`}`, ` ${mark(modOk)} Wake mod: ${modText}`, permissionLine(io, claude, manifest, helper, flag));
|
|
360
|
-
// How setup stands on Pending You (from servers that say): never a reason it isn't Ready.
|
|
361
326
|
const setup = setupStatus(NAME, signIn.state === 'ok' ? setupOf(signIn.for) : null);
|
|
362
327
|
if (setup)
|
|
363
328
|
lines.push(` ${setup === 'finished' ? 'ok ' : ' '} Setup: ${setup}`);
|
|
364
|
-
// A folder's own server (local or project scope) wins over the user one in Claude Code: say when it's elsewhere. (The
|
|
365
|
-
// plugin's server counts only when no server was added by hand, and is production's.)
|
|
366
329
|
if (here && (here.scope === 'local' || here.scope === 'project') && differs(here, origin))
|
|
367
330
|
lines.push(` ${mark(false)} This folder: its ${here.scope}-scope pendingyou MCP server (${here.url}) is another Pending You than ${origin}; Claude Code uses it here instead`);
|
|
368
331
|
const user = mcp !== false && mcp !== 'unknown' && mcp.url ? { url: mcp.url, scope: 'user' } : null;
|
|
369
332
|
const other = differs(here, origin) ? here : differs(user, origin) ? user : null;
|
|
370
|
-
// Through the helper, Claude Code reaches Pending You only with the helper, the connection and the server together.
|
|
371
333
|
const reaches = !helper || (signOk && mcpOk && check?.ok === true);
|
|
372
334
|
const ready = reaches && signIn.state === 'ok' && claude.hooks && hooksRun && !other;
|
|
373
335
|
if (other)
|
package/dist/apps/codex-app.js
CHANGED
|
@@ -1,32 +1,14 @@
|
|
|
1
|
-
// The Codex app for macOS (0.13.0; research 2026-10-04): "ChatGPT.app" (bundle id com.openai.codex; 26.928 carries codex
|
|
2
|
-
// 0.159.2) has its own codex command line inside it, and shares ~/.codex with Codex CLI: its MCP servers (the headers
|
|
3
|
-
// helper too), ~/.agents/skills, ~/.codex/hooks.json and the hooks' trust. Its hooks run with a PATH that ends in its own
|
|
4
|
-
// codex, and its private Codex server reads ~/.codex/queue_1.sqlite every 10 seconds, so `codex queue --thread` wakes a
|
|
5
|
-
// chat the app has loaded while it's open (an idle chat is let go after 3 hours, or behind 10 newer idle ones; then the
|
|
6
|
-
// message waits until the chat is opened again). Where `codex` isn't on PATH, init, status and the listener
|
|
7
|
-
// (codex-wake.ts) run the app's own instead: these paths, in /Applications or ~/Applications, under either name.
|
|
8
1
|
import { stat } from 'node:fs/promises';
|
|
9
2
|
import { join } from 'node:path';
|
|
10
|
-
/** The app's names: the ChatGPT desktop app that carries Codex, and Codex's own, should it ship as that. */
|
|
11
3
|
const APP_NAMES = ['ChatGPT.app', 'Codex.app'];
|
|
12
|
-
/**
|
|
13
|
-
* Where a Codex app keeps its own codex: `Contents/Resources/codex-cli/bin/codex`, and the CodexCLI.app inside it that
|
|
14
|
-
* the app's own code looks for (`codex-cli/CodexCLI.app/Contents/MacOS/codex`), in /Applications, then ~/Applications.
|
|
15
|
-
* `system` is /Applications unless PENDINGYOU_APPLICATIONS_DIR says otherwise (tests: they never look in the real one).
|
|
16
|
-
*/
|
|
17
4
|
export function appCodexPaths(home, system = '/Applications') {
|
|
18
5
|
return [system, join(home, 'Applications')].flatMap((root) => APP_NAMES.flatMap((name) => {
|
|
19
6
|
const cli = join(root, name, 'Contents', 'Resources', 'codex-cli');
|
|
20
7
|
return [join(cli, 'bin', 'codex'), join(cli, 'CodexCLI.app', 'Contents', 'MacOS', 'codex')];
|
|
21
8
|
}));
|
|
22
9
|
}
|
|
23
|
-
/** `codex --version`'s version ("codex-cli 0.159.2"), or "installed" when it can't be read. */
|
|
24
10
|
const versionOf = (stdout) => /(\d+\.\d+\.\d+)/.exec(stdout)?.[1] ?? 'installed';
|
|
25
11
|
const executable = (path) => stat(path).then((info) => info.isFile() && (info.mode & 0o111) !== 0, () => false);
|
|
26
|
-
/**
|
|
27
|
-
* The codex to run: `codex` on PATH; else, on a Mac, the Codex app's own (the first of `paths` there that answers
|
|
28
|
-
* `--version`); null when there's neither.
|
|
29
|
-
*/
|
|
30
12
|
export async function findCodex(io, paths = appCodexPaths(io.home, io.env.PENDINGYOU_APPLICATIONS_DIR || undefined)) {
|
|
31
13
|
const onPath = await io.run('codex', ['--version'], 20_000);
|
|
32
14
|
if (onPath.code === 0)
|