pendingyou 0.14.0 → 0.15.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 +61 -0
- package/README.md +50 -15
- package/dist/api.js +27 -0
- package/dist/apps/claude-code.js +14 -11
- package/dist/apps/codex-wake.js +1 -1
- package/dist/apps/codex.js +102 -32
- package/dist/apps/opencode-plugin.js +52 -2
- package/dist/apps/opencode.js +13 -10
- package/dist/apps/pi-extension.js +41 -1
- package/dist/apps/pi.js +13 -10
- package/dist/apps/switch.js +15 -0
- package/dist/args.js +65 -1
- package/dist/claude.js +6 -2
- package/dist/format.js +29 -0
- package/dist/handed.js +28 -12
- package/dist/hooks.js +2 -1
- package/dist/io.js +1 -0
- package/dist/main.js +26 -5
- package/dist/permission.js +117 -36
- package/dist/pickup.js +16 -5
- package/dist/presence.js +330 -0
- package/dist/stopcheck.js +4 -4
- package/dist/version.js +1 -1
- package/mod/.claude-plugin/plugin.json +1 -1
- package/mod/README.md +1 -1
- package/mod/hooks/register.js +41 -3
- package/mod/hooks/wake.js +71 -2
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,66 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.15.0 (2026-10-05)
|
|
4
|
+
|
|
5
|
+
- Presence: Pending You hears which of your agents' sessions are open right now. Each app tells it with its own
|
|
6
|
+
connection here, `POST <origin>/mcp/cli/presence` with `{"source", "name", "sessionId", "cwd", "state"}` (`name` the
|
|
7
|
+
name the session goes by with Pending You, else null; `cwd` only as `~/…`, else null; `state` `live` or `closed`).
|
|
8
|
+
Pending You counts a session live for 15 minutes after its last `live`, so an open session says so every 5 minutes,
|
|
9
|
+
with no model involved:
|
|
10
|
+
- Claude Code: its session-start hook says live; the wake mod (Claude Code 2.1.287 or later) says live as the
|
|
11
|
+
session starts, after /clear or /resume, and every 5 minutes while it's open, by running the hooks' script; a new
|
|
12
|
+
SessionEnd hook, `presence`, through the same script, says closed. Without the wake mod, the next-message hook
|
|
13
|
+
says live again once the last was 4 minutes ago. Only `init`'s copy of the mod does it, never the plugin's.
|
|
14
|
+
- Codex: its session-start and next-message hooks start a keeper for the thread (`presence --keep`, detached, a lease
|
|
15
|
+
of its own), which says live every 5 minutes while the Codex process the hooks ran under is running (Codex CLI, or
|
|
16
|
+
the Codex app's own codex), and closed once it's gone. A new SessionEnd hook (Codex fires it when a conversation
|
|
17
|
+
is archived or deleted, when Codex closes, or after 30 idle minutes) says closed and stops the keeper; its timeout
|
|
18
|
+
is 3 seconds, the most Codex gives one. Where the Codex process can't be found, the hooks say live as they run.
|
|
19
|
+
- OpenCode's plugin and Pi's extension say live as a session starts (OpenCode: when it's created, or its first
|
|
20
|
+
message) and every 5 minutes while the app runs, and closed when OpenCode deletes the session or goes, or Pi shuts
|
|
21
|
+
the session down.
|
|
22
|
+
A hook only starts the send in the background; a send gives up after 3 seconds; a Pending You from before presence
|
|
23
|
+
answers 404, which is taken quietly. A sign-in that only hears (`--oauth`) sends nothing. Events are written down in
|
|
24
|
+
`~/.config/pendingyou/presence/<app>.json` (ids, states and times), so a late `live` never overrides a newer
|
|
25
|
+
`closed`, and a reload's `live` goes after its `closed`. `status` has a line for each app: “Presence: tells Pending
|
|
26
|
+
You when this session is open”.
|
|
27
|
+
- Codex's permission prompts on a card, as Claude Code's since 0.13.0. `init` adds Codex's PermissionRequest hook (it
|
|
28
|
+
fires when Codex is about to ask for approval: a shell command that needs more than the sandbox gives, an
|
|
29
|
+
`apply_patch`, an MCP tool; `tool_name` Bash, apply_patch or the tool's, and `tool_input.description` when Codex has
|
|
30
|
+
a reason), through the shim with `--app codex`. Codex reads that hook's output as its decision, so it prints nothing
|
|
31
|
+
and exits 0: it declines, and Codex asks as before. Once a prompt has waited 10 seconds, the same detached worker
|
|
32
|
+
posts one card with Codex's own connection: “Codex is waiting for your OK: git push origin main”, “…: edit
|
|
33
|
+
src/app.ts and 1 more” for a patch (the files its `*** Update File:` lines name), “…: create_issue on github” for an
|
|
34
|
+
MCP tool, with Codex's reason (redacted) in the summary, under the name the thread went by on Pending You's card tools,
|
|
35
|
+
else “Codex”, and the same redaction. It's withdrawn when the call runs (a PostToolUse hook for every tool, which
|
|
36
|
+
also settles a prompt for the same tool when Codex gives the call that ran its input in another form), with your
|
|
37
|
+
next message, at the end of the turn, or when the thread ends (SessionEnd). `--no-permission-cards` leaves Codex's
|
|
38
|
+
out too. The Codex app's browser-use prompts run no PermissionRequest hook, so they get no card.
|
|
39
|
+
- Presence keeps to Pending You's contract: the name trimmed to one line of at most 60 characters, a folder over 300
|
|
40
|
+
characters sent as null, a 401 tried once more after a refresh in the background, a 429 waited out once (its
|
|
41
|
+
`retry-after`, 60 seconds) by a send in its own process (never a hook), and 400, 403 and 404 taken quietly; Codex's
|
|
42
|
+
keeper stops once a send is refused, its sign-in is gone, or the server has no presence.
|
|
43
|
+
- A question you handed an agent, when the assistant that asked answers what the helper asked it on the card (or you
|
|
44
|
+
write in to them both, guide 2.30), wakes the helper with those words, not the handed question again: “Pending You:
|
|
45
|
+
billing-webhooks replied on the question handed to you, “Which queue?” (req_…): “It's the jobs queue.”. Call
|
|
46
|
+
get_request (requestId req_…, name “infra”) to read what you've said to each other. If you know now,
|
|
47
|
+
answer_delegated …; if you need more, reply_in_thread to “asker”; if it's your person's to decide, reply_in_thread
|
|
48
|
+
with escalate true; if not, hand_back …” (“your person wrote to you both …” for yours). The same from the hooks, the
|
|
49
|
+
wake mod, OpenCode's plugin and Pi's extension; Codex's `codex queue` leaves the words out of its arguments, where any
|
|
50
|
+
program could read them, and get_request has them. The wake mod now wakes a session for such a reply after it read
|
|
51
|
+
the question itself (before, a question it had read never woke it again), and the Stop check and the hooks know a
|
|
52
|
+
turn such a wake started.
|
|
53
|
+
- Switching an agent's own sign-in to this computer's says what becomes of the old one: once the agent shows up on this
|
|
54
|
+
computer's sign-in, Pending You moves the old sign-in's agents over, with their cards and the questions you delegated
|
|
55
|
+
to them, and signs it out. Where there's no browser (an SSH session) init says what it switched instead of switching
|
|
56
|
+
without a word.
|
|
57
|
+
- Codex runs a hook only once you trust it, so the three new hooks (PermissionRequest, PostToolUse for every tool,
|
|
58
|
+
SessionEnd) need trusting once. They go at the end of their lists, so the ones you trusted stay trusted. `init` says
|
|
59
|
+
so when it adds them to a Codex set up before (“Codex has new Pending You hooks (session end and permission prompts),
|
|
60
|
+
which it skips until you trust them once: …”), and `status` has a line for each, with each hook's trust:
|
|
61
|
+
“Permission prompts: a card when Codex waits for your OK” and “Presence: …”. Neither keeps `status` from saying
|
|
62
|
+
Ready. Codex's record of its hooks (`codex.json`) now names each by its event and command.
|
|
63
|
+
|
|
3
64
|
## 0.14.0 (2026-10-05)
|
|
4
65
|
|
|
5
66
|
- Questions your person hands one of your agents from another assistant (Delegate: Pending You's card link, or D on the
|
package/README.md
CHANGED
|
@@ -24,8 +24,9 @@ Run it once on each computer, in a terminal. It finds the agents on the computer
|
|
|
24
24
|
Pi reads Codex's, else `~/.pi/agent/skills/pendingyou/SKILL.md`),
|
|
25
25
|
- adds the hooks that hand it your answers: a new session picks up answers that arrived while nothing was running,
|
|
26
26
|
your next message brings a waiting answer with it, and a check when it finishes a turn catches what it left you only
|
|
27
|
-
in chat ([below](#nothing-left-only-in-chat));
|
|
28
|
-
([below](#when-claude-code-waits-for-your-ok))
|
|
27
|
+
in chat ([below](#nothing-left-only-in-chat)); for Claude Code and Codex, a card when it waits for your OK to run
|
|
28
|
+
something ([below](#when-claude-code-waits-for-your-ok)); and they tell Pending You which sessions are open
|
|
29
|
+
([below](#which-sessions-are-open)),
|
|
29
30
|
- and wakes it when you answer: Claude Code 2.1.287 or later through the wake mod, Codex through `codex queue`, OpenCode
|
|
30
31
|
through its plugin and Pi through its extension (which also do the hooks' work there); and when you hand it a
|
|
31
32
|
question from another of your assistants ([below](#when-you-hand-an-agent-a-question)).
|
|
@@ -54,10 +55,11 @@ in each, every other byte as it was), and running it again changes nothing it do
|
|
|
54
55
|
**A pendingyou server that's there already** is decided on before anyone signs in. One at another Pending You
|
|
55
56
|
(staging, say) is named and replaced only once you say so (`--yes` replaces it without asking); keep it and that agent
|
|
56
57
|
is left as it is. One at this Pending You that signs in by itself (Claude Code's `/mcp` → Authenticate, Codex's
|
|
57
|
-
`codex mcp login`, or the Pending You plugin for Claude) is moved to this computer's sign-in:
|
|
58
|
-
|
|
59
|
-
ask it's kept, and `init` prints the command that switches it.
|
|
60
|
-
|
|
58
|
+
`codex mcp login`, or the Pending You plugin for Claude) is moved to this computer's sign-in: where there's no browser
|
|
59
|
+
(it can't finish there anyway) without asking, saying what it switched; on Yes (the default) at a terminal; or with
|
|
60
|
+
`--yes`; with nobody to ask it's kept, and `init` prints the command that switches it. Once the agent shows up on this
|
|
61
|
+
computer's sign-in, Pending You moves the old sign-in's agents over to it, with their cards and the questions you
|
|
62
|
+
delegated to them, and signs the old one out. A kept plugin
|
|
61
63
|
carries the skill, so none is saved. `--oauth` keeps every agent signing in by itself, as before 0.11.0.
|
|
62
64
|
|
|
63
65
|
The hooks run a copy of this version kept in `~/.config/pendingyou/cli/<version>/`, through a small script with a path
|
|
@@ -94,10 +96,13 @@ http_headers_helper = "/Users/ann/.config/pendingyou/bin/pendingyou-mcp-headers
|
|
|
94
96
|
default_tools_approval_mode = "approve"
|
|
95
97
|
```
|
|
96
98
|
|
|
97
|
-
and
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
99
|
+
and hooks in `~/.codex/hooks.json`: a session start, each message, Pending You's card tools (so the Stop check knows a
|
|
100
|
+
turn posted, and so the thread is woken), and the Stop check; and (0.15.0) Codex's permission prompt and every tool
|
|
101
|
+
call that ran (a card when Codex waits for your OK, [below](#when-claude-code-waits-for-your-ok)) and the session's end
|
|
102
|
+
([below](#which-sessions-are-open)). **Codex runs a hook only once you trust it**: in Codex CLI, start `codex` and
|
|
103
|
+
choose “Trust all and continue” (or trust them in `/hooks`); in the Codex app, click Review hooks › Allow all (or
|
|
104
|
+
Settings › Hooks › Trust all). `status` says which are trusted. Upgrading from 0.14.0 adds three hooks, which Codex
|
|
105
|
+
skips until you trust them once; `init` says so, and the ones you trusted stay trusted. A `pendingyou` server or
|
|
101
106
|
hooks Codex's `/import` copied from Claude Code (without its sign-in) are replaced.
|
|
102
107
|
|
|
103
108
|
**The Codex app** (on a Mac, in the ChatGPT app) shares all of this with Codex CLI: the same `~/.codex`, server, hooks
|
|
@@ -245,6 +250,32 @@ It needs Claude Code 2.1.119 or later, signing in through this computer's sign-i
|
|
|
245
250
|
Windows yet. Don't want it? `npx pendingyou init --no-permission-cards` (later runs keep it off; `--permission-cards`
|
|
246
251
|
turns it back on). `npx pendingyou status` says which.
|
|
247
252
|
|
|
253
|
+
**Codex** (0.15.0) gets the same: when Codex stops to ask for your approval (a command that needs more than its sandbox
|
|
254
|
+
gives, a patch, an MCP tool) and you don't answer within 10 seconds, a card says “Codex is waiting for your OK: git push
|
|
255
|
+
origin main” (“…: edit src/app.ts and 1 more” for a patch), with Codex's reason for asking, blanked like the rest. Its
|
|
256
|
+
PermissionRequest hook prints nothing, so Codex asks exactly as before; the card goes once the call runs, you write to
|
|
257
|
+
Codex, the turn ends or the thread closes. It needs Codex signing in through this computer's sign-in, and trusting the
|
|
258
|
+
new hooks once. The Codex app's browser-use prompts don't run that hook, so they get no card.
|
|
259
|
+
|
|
260
|
+
## Which sessions are open
|
|
261
|
+
|
|
262
|
+
Since 0.15.0 Pending You hears which of your agents' sessions are open right now, so it can say so and offer an open
|
|
263
|
+
one a question. Each app tells it with its own connection here: the session's id, the name it goes by with Pending You,
|
|
264
|
+
its folder (under `~` only) and `live` or `closed`. Pending You counts a session open for 15 minutes after its last
|
|
265
|
+
`live`, so an open session says so every 5 minutes, with no model involved:
|
|
266
|
+
|
|
267
|
+
- **Claude Code**: as a session starts (its session-start hook, and the wake mod), then every 5 minutes from the wake
|
|
268
|
+
mod while it's open (without the mod, with your messages); closed as it ends (a SessionEnd hook).
|
|
269
|
+
- **Codex**: as a thread starts or you write to it, a small process in the background says so every 5 minutes while
|
|
270
|
+
Codex (or the Codex app) runs, and closed once it's gone; its SessionEnd hook (a conversation archived or closed, or
|
|
271
|
+
idle 30 minutes) says closed.
|
|
272
|
+
- **OpenCode and Pi**: the plugin and the extension, as a session starts and every 5 minutes while the app runs, and
|
|
273
|
+
closed as it goes.
|
|
274
|
+
|
|
275
|
+
It's never in your way: the hooks only start it in the background, each send gives up after 3 seconds, and a Pending
|
|
276
|
+
You from before presence is taken quietly. A sign-in that only hears (`--oauth`) sends nothing. `npx pendingyou status`
|
|
277
|
+
has a Presence line for each agent.
|
|
278
|
+
|
|
248
279
|
## How Claude Code hears your answer
|
|
249
280
|
|
|
250
281
|
**Claude Code 2.1.287 or later: the wake mod.** There's nothing to run. In each session the mod learns the cards
|
|
@@ -386,8 +417,9 @@ Pending You (Claude Code says which server it's for; Codex's is the one `init` w
|
|
|
386
417
|
sign-in each agent's connection used, so signing in again keeps it.
|
|
387
418
|
|
|
388
419
|
`state.json` beside them remembers which answers were already handed over (ids only) and how many session starts
|
|
389
|
-
reminded an unfinished setup, and when; `permission-cards/` each Claude Code session's
|
|
390
|
-
and its card (ids, hashes and the card's redacted words: never what the tool was given), gone once nothing waits;
|
|
420
|
+
reminded an unfinished setup, and when; `permission-cards/` each Claude Code session's (and Codex thread's) prompts
|
|
421
|
+
still waiting on your OK and its card (ids, hashes and the card's redacted words: never what the tool was given), gone once nothing waits;
|
|
422
|
+
`presence/` the last presence each session said (ids, states and times), and Codex threads' keepers;
|
|
391
423
|
`claude-code.json`, `codex.json`, `opencode.json` and `pi.json` what `init`
|
|
392
424
|
added; `codex-threads.json`, `opencode-threads.json` and `pi-threads.json` which cards each Codex thread or OpenCode or
|
|
393
425
|
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
|
|
@@ -398,8 +430,9 @@ whether the agent has a `pendingyou` MCP server and where it points. With a sign
|
|
|
398
430
|
listeners tell this computer's handed questions from another's by its names: its hostname, the names its sign-ins by
|
|
399
431
|
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
|
|
400
432
|
the end of the Claude Code session's transcript only for the name the session gave Pending You, and `git remote
|
|
401
|
-
get-url origin` in its folder (sent without any user name or password in it).
|
|
402
|
-
|
|
433
|
+
get-url origin` in its folder (sent without any user name or password in it). To tell when a Codex thread's Codex has quit, its
|
|
434
|
+
keeper asks `ps` about the Codex process its hooks ran under (its id and program name, nothing else). Nothing is sent
|
|
435
|
+
anywhere but Pending You. OpenCode's plugin reads its session's messages only when it goes idle, for the check that
|
|
403
436
|
nothing was left in chat, and hands the hooks' script just that turn's words and tool names.
|
|
404
437
|
|
|
405
438
|
The wake mod runs inside Claude Code and reaches Pending You only through the session's own connection, with
|
|
@@ -407,7 +440,9 @@ The wake mod runs inside Claude Code and reaches Pending You only through the se
|
|
|
407
440
|
`list_pending` with the session's name and `get_request` on a question you handed it. It keeps each session's cards (id,
|
|
408
441
|
title, the agent's name, where each stands), and which handed questions a session was told of (ids and when), in Claude
|
|
409
442
|
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`).
|
|
443
|
+
acting for a session in that session's environment (`PENDINGYOU_WAKE_OWNER`). `init`'s copy (0.15.0) also reads
|
|
444
|
+
`~/.config/pendingyou/claude-code.json` for the Pending You it's for, and runs the hooks' script to say the session is
|
|
445
|
+
open.
|
|
411
446
|
|
|
412
447
|
## Development
|
|
413
448
|
|
package/dist/api.js
CHANGED
|
@@ -172,3 +172,30 @@ export async function getJson(io, origin, path, timeoutMs, signal, options = {})
|
|
|
172
172
|
return { status: response.status, body };
|
|
173
173
|
}
|
|
174
174
|
}
|
|
175
|
+
export async function postJson(io, origin, path, body, timeoutMs, options = {}) {
|
|
176
|
+
const app = options.app ?? DEFAULT_APP;
|
|
177
|
+
const token = await hookToken(io, origin, options.force === true, options.signal, app);
|
|
178
|
+
let response;
|
|
179
|
+
try {
|
|
180
|
+
response = await io.fetch(`${origin}${path}`, {
|
|
181
|
+
method: 'POST',
|
|
182
|
+
headers: {
|
|
183
|
+
authorization: `Bearer ${token}`,
|
|
184
|
+
'content-type': 'application/json',
|
|
185
|
+
accept: 'application/json',
|
|
186
|
+
},
|
|
187
|
+
body: JSON.stringify(body),
|
|
188
|
+
signal: either(options.signal, timeoutMs),
|
|
189
|
+
redirect: 'manual',
|
|
190
|
+
});
|
|
191
|
+
}
|
|
192
|
+
catch {
|
|
193
|
+
throw new Unavailable('Pending You couldn’t be reached.');
|
|
194
|
+
}
|
|
195
|
+
await response.body?.cancel().catch(() => { });
|
|
196
|
+
const retry = Number(response.headers.get('retry-after'));
|
|
197
|
+
return {
|
|
198
|
+
status: response.status,
|
|
199
|
+
...(Number.isFinite(retry) && retry > 0 ? { retryAfter: retry } : {}),
|
|
200
|
+
};
|
|
201
|
+
}
|
package/dist/apps/claude-code.js
CHANGED
|
@@ -7,16 +7,11 @@ import { checkHelper, foundBy, headlessReason, helperCommand, helperPath, isOurH
|
|
|
7
7
|
import { connectionTitle, FINISH_SAY, setupOf, setupStatus } from "../setup.js";
|
|
8
8
|
import { shimPath } from "../shim.js";
|
|
9
9
|
import { APP_NAMES } from "./ids.js";
|
|
10
|
+
import { switches } from "./switch.js";
|
|
10
11
|
const NAME = APP_NAMES['claude-code'];
|
|
11
12
|
const RESTART = 'restart Claude Code here (/exit, then claude --continue), or start it in the folder you work in';
|
|
12
|
-
async function
|
|
13
|
-
|
|
14
|
-
return true;
|
|
15
|
-
if (!io.interactive)
|
|
16
|
-
return false;
|
|
17
|
-
io.out(`${what} init can move Claude Code to this computer’s own sign-in, which the hooks use too: one sign-in for both, and nothing to Authenticate. The connection it has now stays on your Assistants page until you remove it, and answers to cards it asked through that one won’t reach Claude Code by themselves.\n`);
|
|
18
|
-
const answer = await io.ask('Switch Claude Code to this computer’s sign-in? [Y/n] ');
|
|
19
|
-
return !/^\s*no?\s*$/i.test(answer);
|
|
13
|
+
async function switchesFor(io, ctx, what) {
|
|
14
|
+
return switches(io, ctx, { name: 'Claude Code', what, uses: 'the hooks use', headless: true });
|
|
20
15
|
}
|
|
21
16
|
async function replaces(io, ctx, server, url) {
|
|
22
17
|
ctx.progress(`Claude Code’s pendingyou MCP server points at ${server.url}, not ${url}.\n`);
|
|
@@ -117,7 +112,7 @@ export const claudeCode = {
|
|
|
117
112
|
};
|
|
118
113
|
}
|
|
119
114
|
else if (helperMode &&
|
|
120
|
-
(await
|
|
115
|
+
(await switchesFor(io, ctx, 'Claude Code signs in to Pending You by itself here (/mcp → Authenticate).')))
|
|
121
116
|
plan = { action: 'switch', server };
|
|
122
117
|
else {
|
|
123
118
|
mode = 'oauth';
|
|
@@ -132,7 +127,7 @@ export const claudeCode = {
|
|
|
132
127
|
}
|
|
133
128
|
else if (plugin) {
|
|
134
129
|
if (helperMode &&
|
|
135
|
-
(await
|
|
130
|
+
(await switchesFor(io, ctx, 'The Pending You plugin signs Claude Code in to Pending You by itself here.')))
|
|
136
131
|
plan = { action: 'add', plugin: true };
|
|
137
132
|
else {
|
|
138
133
|
mode = 'oauth';
|
|
@@ -215,6 +210,14 @@ function permissionLine(io, claude, manifest, helper, flag) {
|
|
|
215
210
|
? ` ${label} no card when Claude Code waits for your OK: ${blocker}`
|
|
216
211
|
: ` missing ${label} no card when Claude Code waits for your OK; run npx pendingyou@latest init${flag}`;
|
|
217
212
|
}
|
|
213
|
+
function presenceLine(claude, connected, mod, flag) {
|
|
214
|
+
const label = 'Presence:';
|
|
215
|
+
if (!connected)
|
|
216
|
+
return ` ${label} off: it needs Claude Code signed in through this computer’s own connection`;
|
|
217
|
+
if (!claude.presence || !claude.hooks)
|
|
218
|
+
return ` missing ${label} Pending You isn’t told when a session closes; run npx pendingyou@latest init${flag}`;
|
|
219
|
+
return ` ok ${label} tells Pending You when this session is open (${mod ? 'as it starts, then every 5 minutes from the wake mod' : 'as it starts and with your messages'}; closed as it ends)`;
|
|
220
|
+
}
|
|
218
221
|
async function claudeStatus(io, ctx) {
|
|
219
222
|
const { origin, signIn } = ctx;
|
|
220
223
|
const [claude, here, manifest] = await Promise.all([
|
|
@@ -322,7 +325,7 @@ async function claudeStatus(io, ctx) {
|
|
|
322
325
|
? [
|
|
323
326
|
` ok Plugin: ${plugin} is enabled${pluginServer ? '' : mcp !== false && mcp !== 'unknown' ? '; Claude Code uses the server added by hand instead of its own' : ''}`,
|
|
324
327
|
]
|
|
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));
|
|
328
|
+
: []), ` ${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), presenceLine(claude, helper && connected, modOk, flag));
|
|
326
329
|
const setup = setupStatus(NAME, signIn.state === 'ok' ? setupOf(signIn.for) : null);
|
|
327
330
|
if (setup)
|
|
328
331
|
lines.push(` ${setup === 'finished' ? 'ok ' : ' '} Setup: ${setup}`);
|
package/dist/apps/codex-wake.js
CHANGED
|
@@ -636,7 +636,7 @@ export async function listenHanded(io, options) {
|
|
|
636
636
|
]);
|
|
637
637
|
}
|
|
638
638
|
for (const [thread, list] of byThread) {
|
|
639
|
-
const message = handedMessage(list.map(({ card }) => card));
|
|
639
|
+
const message = handedMessage(list.map(({ card }) => card), { words: false });
|
|
640
640
|
if (message && (await queue(thread, message)))
|
|
641
641
|
continue;
|
|
642
642
|
await giveBack(io, origin, list);
|
package/dist/apps/codex.js
CHANGED
|
@@ -4,13 +4,15 @@ import { join } from 'node:path';
|
|
|
4
4
|
import { removeSkill, saveSkill } from "../claude.js";
|
|
5
5
|
import { readCredential } from "../credentials.js";
|
|
6
6
|
import { configDir, readJson, readText, writeWhole } from "../files.js";
|
|
7
|
-
import { isOurHook, mergeHooks, originArgs, removeHooks, shWord, } from "../hooks.js";
|
|
7
|
+
import { isOurHook, lineKey, mergeHooks, originArgs, removeHooks, shWord, } from "../hooks.js";
|
|
8
8
|
import { atLeast } from "../mod.js";
|
|
9
|
+
import { removePresenceFiles } from "../presence.js";
|
|
9
10
|
import { checkHelper, foundBy, headlessReason, helperCommand, helperPath, isOurHelper, } from "../remote.js";
|
|
10
11
|
import { connectionTitle, FINISH_SAY, setupOf, setupStatus } from "../setup.js";
|
|
11
12
|
import { basicString, definedInline, headerOf, renameTable, tableSpans, tableValues, withTable, } from "../toml.js";
|
|
12
13
|
import { findCodex } from "./codex-app.js";
|
|
13
14
|
import { APP_NAMES } from "./ids.js";
|
|
15
|
+
import { switches } from "./switch.js";
|
|
14
16
|
const NAME = APP_NAMES.codex;
|
|
15
17
|
const SERVER = ['mcp_servers', 'pendingyou'];
|
|
16
18
|
export const CODEX_MIN = '0.152.0';
|
|
@@ -27,11 +29,25 @@ export const CODEX_HOOKS = [
|
|
|
27
29
|
{ event: 'PostToolUse', sub: 'posted', matcher: CARD_TOOLS },
|
|
28
30
|
{ event: 'Stop', sub: 'stopcheck' },
|
|
29
31
|
];
|
|
32
|
+
export const CODEX_PERMISSION_HOOKS = [
|
|
33
|
+
{ event: 'PermissionRequest', sub: 'permission' },
|
|
34
|
+
{ event: 'PostToolUse', sub: 'permission-done' },
|
|
35
|
+
];
|
|
36
|
+
export const CODEX_PRESENCE_HOOKS = [
|
|
37
|
+
{ event: 'SessionEnd', sub: 'presence', timeout: 3 },
|
|
38
|
+
];
|
|
39
|
+
export const ALL_CODEX_HOOKS = [
|
|
40
|
+
...CODEX_HOOKS,
|
|
41
|
+
...CODEX_PERMISSION_HOOKS,
|
|
42
|
+
...CODEX_PRESENCE_HOOKS,
|
|
43
|
+
];
|
|
30
44
|
const EVENT_LABELS = {
|
|
31
45
|
SessionStart: 'session_start',
|
|
32
46
|
UserPromptSubmit: 'user_prompt_submit',
|
|
33
47
|
PostToolUse: 'post_tool_use',
|
|
34
48
|
Stop: 'stop',
|
|
49
|
+
PermissionRequest: 'permission_request',
|
|
50
|
+
SessionEnd: 'session_end',
|
|
35
51
|
};
|
|
36
52
|
const NO_MATCHER = new Set(['UserPromptSubmit', 'Stop']);
|
|
37
53
|
const SIGN_IN_KEYS = new Set([
|
|
@@ -151,7 +167,7 @@ async function hookTrust(io) {
|
|
|
151
167
|
const hooks = 'json' in file && file.json.hooks && typeof file.json.hooks === 'object'
|
|
152
168
|
? file.json.hooks
|
|
153
169
|
: {};
|
|
154
|
-
return
|
|
170
|
+
return ALL_CODEX_HOOKS.map((spec) => {
|
|
155
171
|
const groups = Array.isArray(hooks[spec.event]) ? hooks[spec.event] : [];
|
|
156
172
|
for (const [g, group] of groups.entries())
|
|
157
173
|
for (const [h, handler] of (group?.hooks ?? []).entries()) {
|
|
@@ -159,13 +175,14 @@ async function hookTrust(io) {
|
|
|
159
175
|
continue;
|
|
160
176
|
const state = tableValues(config, ['hooks', 'state', trustKey(file.path, spec.event, g, h)]);
|
|
161
177
|
if (state.enabled === false)
|
|
162
|
-
return { event: spec.event, trust: 'off' };
|
|
178
|
+
return { event: spec.event, sub: spec.sub, trust: 'off' };
|
|
163
179
|
const hash = hookHash(spec.event, group.matcher, {
|
|
164
180
|
command: handler.command,
|
|
165
181
|
...(typeof handler.timeout === 'number' ? { timeout: handler.timeout } : {}),
|
|
166
182
|
});
|
|
167
183
|
return {
|
|
168
184
|
event: spec.event,
|
|
185
|
+
sub: spec.sub,
|
|
169
186
|
trust: state.trusted_hash === hash
|
|
170
187
|
? 'trusted'
|
|
171
188
|
: typeof state.trusted_hash === 'string'
|
|
@@ -173,7 +190,7 @@ async function hookTrust(io) {
|
|
|
173
190
|
: 'untrusted',
|
|
174
191
|
};
|
|
175
192
|
}
|
|
176
|
-
return { event: spec.event, trust: 'missing' };
|
|
193
|
+
return { event: spec.event, sub: spec.sub, trust: 'missing' };
|
|
177
194
|
});
|
|
178
195
|
}
|
|
179
196
|
async function signedInByItself(io, program) {
|
|
@@ -191,15 +208,12 @@ async function signedInByItself(io, program) {
|
|
|
191
208
|
return null;
|
|
192
209
|
}
|
|
193
210
|
}
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
const answer = await io.ask('Switch Codex to this computer’s sign-in? [Y/n] ');
|
|
201
|
-
return !/^\s*no?\s*$/i.test(answer);
|
|
202
|
-
}
|
|
211
|
+
const switchesFor = (io, ctx) => switches(io, ctx, {
|
|
212
|
+
name: 'Codex',
|
|
213
|
+
what: 'Codex signs in to Pending You by itself here (codex mcp login).',
|
|
214
|
+
uses: 'its hooks use',
|
|
215
|
+
headless: true,
|
|
216
|
+
});
|
|
203
217
|
async function writeConfig(path, text) {
|
|
204
218
|
return writeWhole(path, text).then(() => true, () => false);
|
|
205
219
|
}
|
|
@@ -210,24 +224,26 @@ function keepTheirTrust(config, file, before) {
|
|
|
210
224
|
'state',
|
|
211
225
|
trustKey(file, event, group, handler),
|
|
212
226
|
];
|
|
213
|
-
for (const
|
|
214
|
-
const
|
|
227
|
+
for (const event of [...new Set(ALL_CODEX_HOOKS.map((spec) => spec.event))]) {
|
|
228
|
+
const subs = ALL_CODEX_HOOKS.filter((spec) => spec.event === event).map((spec) => spec.sub);
|
|
229
|
+
const ours = (hook) => subs.some((sub) => isOurHook(hook?.command, sub));
|
|
230
|
+
const groups = Array.isArray(before[event]) ? before[event] : [];
|
|
215
231
|
for (const [g, group] of groups.entries())
|
|
216
232
|
for (const [h, hook] of (Array.isArray(group?.hooks) ? group.hooks : []).entries())
|
|
217
|
-
if (
|
|
218
|
-
next = withTable(next, state(
|
|
233
|
+
if (ours(hook))
|
|
234
|
+
next = withTable(next, state(event, g, h), null);
|
|
219
235
|
let at = 0;
|
|
220
236
|
for (const [g, group] of groups.entries()) {
|
|
221
237
|
if (!Array.isArray(group?.hooks)) {
|
|
222
238
|
at++;
|
|
223
239
|
continue;
|
|
224
240
|
}
|
|
225
|
-
const kept = group.hooks.flatMap((hook, h) => (
|
|
241
|
+
const kept = group.hooks.flatMap((hook, h) => (ours(hook) ? [] : [h]));
|
|
226
242
|
if (kept.length === 0)
|
|
227
243
|
continue;
|
|
228
244
|
for (const [h, was] of kept.entries())
|
|
229
245
|
if (g !== at || was !== h)
|
|
230
|
-
next = renameTable(next, state(
|
|
246
|
+
next = renameTable(next, state(event, g, was), state(event, at, h));
|
|
231
247
|
at++;
|
|
232
248
|
}
|
|
233
249
|
}
|
|
@@ -315,7 +331,7 @@ export const codex = {
|
|
|
315
331
|
else if ((await signedInByItself(io, program)) !== true) {
|
|
316
332
|
action = 'write';
|
|
317
333
|
}
|
|
318
|
-
else if (await
|
|
334
|
+
else if (await switchesFor(io, ctx))
|
|
319
335
|
action = 'write';
|
|
320
336
|
else {
|
|
321
337
|
mode = 'oauth';
|
|
@@ -327,6 +343,7 @@ export const codex = {
|
|
|
327
343
|
}
|
|
328
344
|
}
|
|
329
345
|
const version = detected.version;
|
|
346
|
+
let newHooks = null;
|
|
330
347
|
return {
|
|
331
348
|
app: codex,
|
|
332
349
|
detected,
|
|
@@ -384,7 +401,15 @@ export const codex = {
|
|
|
384
401
|
}
|
|
385
402
|
const skill = await saveSkill(io, ictx.origin, codexSkillPath(io), before?.skill ?? null);
|
|
386
403
|
report(skill.step);
|
|
387
|
-
const
|
|
404
|
+
const cards = ctx.permissionCards ?? before?.permissionCards !== false;
|
|
405
|
+
const extra = mode === 'helper' && ictx.helper !== null;
|
|
406
|
+
const wanted = [
|
|
407
|
+
...CODEX_HOOKS,
|
|
408
|
+
...(extra && cards ? CODEX_PERMISSION_HOOKS : []),
|
|
409
|
+
...(extra ? CODEX_PRESENCE_HOOKS : []),
|
|
410
|
+
];
|
|
411
|
+
const unwanted = ALL_CODEX_HOOKS.filter((spec) => !wanted.includes(spec));
|
|
412
|
+
const lines = Object.fromEntries(wanted.map((spec) => [lineKey(spec), ictx.hookLine(spec.sub)]));
|
|
388
413
|
const file = await readHooksFile(io);
|
|
389
414
|
if ('invalid' in file)
|
|
390
415
|
report({ ok: false, text: file.invalid });
|
|
@@ -393,14 +418,25 @@ export const codex = {
|
|
|
393
418
|
const hooks = file.json.hooks && typeof file.json.hooks === 'object'
|
|
394
419
|
? file.json.hooks
|
|
395
420
|
: {};
|
|
396
|
-
const merged = mergeHooks(hooks,
|
|
397
|
-
|
|
398
|
-
|
|
421
|
+
const merged = mergeHooks(hooks, wanted, lines, file.path);
|
|
422
|
+
const removed = removeHooks(merged.hooks, unwanted);
|
|
423
|
+
if (merged.changed || removed.changed)
|
|
424
|
+
await writeWhole(file.path, `${JSON.stringify({ ...file.json, hooks: removed.hooks }, null, 2)}\n`);
|
|
425
|
+
const fresh = wanted.filter((spec) => !CODEX_HOOKS.includes(spec) && !(lineKey(spec) in (before?.hooks ?? {})));
|
|
426
|
+
if (before && merged.changed && fresh.length)
|
|
427
|
+
newHooks = [
|
|
428
|
+
...(fresh.some((spec) => spec.sub === 'presence') ? ['session end'] : []),
|
|
429
|
+
...(fresh.some((spec) => spec.sub.startsWith('permission'))
|
|
430
|
+
? ['permission prompts']
|
|
431
|
+
: []),
|
|
432
|
+
].join(' and ');
|
|
399
433
|
report({
|
|
400
434
|
ok: true,
|
|
401
435
|
text: merged.changed
|
|
402
|
-
? `Installed or updated Codex’s session-start pickup, next-message hand-off, card and Stop check hooks in ${file.path}. Codex asks you to trust them once.`
|
|
403
|
-
:
|
|
436
|
+
? `Installed or updated Codex’s session-start pickup, next-message hand-off, card and Stop check hooks${extra ? `, ${cards ? 'its permission-prompt hooks (a card when Codex waits for your OK; --no-permission-cards leaves them out) ' : ''}and its session-end hook (Pending You hears when a thread is open)` : ''} in ${file.path}. Codex asks you to trust them once.`
|
|
437
|
+
: removed.changed
|
|
438
|
+
? `Took out Codex’s permission-prompt hooks, as you asked: no card when Codex waits for your OK. To turn them on: npx pendingyou init --app codex --permission-cards${originArgs(ictx.origin)}`
|
|
439
|
+
: 'Codex’s hooks were already installed.',
|
|
404
440
|
});
|
|
405
441
|
}
|
|
406
442
|
catch (error) {
|
|
@@ -426,6 +462,7 @@ export const codex = {
|
|
|
426
462
|
hooks: lines,
|
|
427
463
|
helper: mode === 'helper' ? ictx.helper : null,
|
|
428
464
|
setup: { since: new Date(io.now()).toISOString() },
|
|
465
|
+
permissionCards: cards,
|
|
429
466
|
};
|
|
430
467
|
await writeWhole(manifestPath(io), `${JSON.stringify(manifest, null, 2)}\n`, {
|
|
431
468
|
secret: true,
|
|
@@ -438,9 +475,19 @@ export const codex = {
|
|
|
438
475
|
const start = detected.command
|
|
439
476
|
? `open the Codex app, start a chat in the folder you work in, and trust Pending You’s hooks: click Review hooks › Allow all (or Settings › Hooks › Trust all)`
|
|
440
477
|
: `start codex here, or in the folder you work in, and trust Pending You’s hooks: ${TRUST_CLI}; ${TRUST_APP}`;
|
|
478
|
+
const again = newHooks
|
|
479
|
+
? `Codex has new Pending You hooks (${newHooks}), which it skips until you trust them once: ${detected.command ? TRUST_APP : `${TRUST_CLI}; ${TRUST_APP}`}.`
|
|
480
|
+
: null;
|
|
441
481
|
return {
|
|
442
|
-
lines: [
|
|
443
|
-
|
|
482
|
+
lines: [
|
|
483
|
+
`Next: ${first}${start}.`,
|
|
484
|
+
...(again ? [again] : []),
|
|
485
|
+
DONT_SAVE,
|
|
486
|
+
'Then say this to it:',
|
|
487
|
+
'',
|
|
488
|
+
FINISH_SAY,
|
|
489
|
+
],
|
|
490
|
+
together: `- Codex: ${first}${start}, then say the line below to it.${again ? ` ${again}` : ''} ${DONT_SAVE}`,
|
|
444
491
|
say: true,
|
|
445
492
|
};
|
|
446
493
|
},
|
|
@@ -454,7 +501,7 @@ export const codex = {
|
|
|
454
501
|
let doc = (await readText(config).catch(() => null)) ?? null;
|
|
455
502
|
if ('json' in file) {
|
|
456
503
|
const hooks = file.json.hooks && typeof file.json.hooks === 'object' ? file.json.hooks : {};
|
|
457
|
-
const removed = removeHooks(hooks,
|
|
504
|
+
const removed = removeHooks(hooks, ALL_CODEX_HOOKS);
|
|
458
505
|
if (removed.changed) {
|
|
459
506
|
const rest = { ...file.json, hooks: removed.hooks };
|
|
460
507
|
if (Object.keys(removed.hooks).length === 0)
|
|
@@ -484,6 +531,7 @@ export const codex = {
|
|
|
484
531
|
steps.push(skill);
|
|
485
532
|
await rm(join(configDir(io), 'codex-threads.json'), { force: true });
|
|
486
533
|
await rm(join(configDir(io), 'codex-listen'), { recursive: true, force: true });
|
|
534
|
+
await removePresenceFiles(io, 'codex');
|
|
487
535
|
await rm(manifestPath(io), { force: true });
|
|
488
536
|
return steps;
|
|
489
537
|
},
|
|
@@ -541,10 +589,30 @@ export const codex = {
|
|
|
541
589
|
off: 'turned off',
|
|
542
590
|
missing: 'missing',
|
|
543
591
|
};
|
|
544
|
-
const
|
|
545
|
-
const
|
|
592
|
+
const core = trust.filter((hook) => CODEX_HOOKS.some((spec) => spec.sub === hook.sub));
|
|
593
|
+
const hooksOk = core.every((hook) => hook.trust === 'trusted');
|
|
594
|
+
const hooksText = core.some((hook) => hook.trust === 'missing')
|
|
546
595
|
? `not installed; run npx pendingyou init --app codex${flag}`
|
|
547
|
-
: `${
|
|
596
|
+
: `${core.map((hook) => `${hook.event} ${words[hook.trust]}`).join(', ')}${hooksOk ? '' : `. Codex skips a hook you haven’t trusted: ${TRUST_CLI}; ${TRUST_APP}`}`;
|
|
597
|
+
const extraLine = (label, what, subs, off) => {
|
|
598
|
+
const hooks = trust.filter((hook) => subs.includes(hook.sub));
|
|
599
|
+
if (off)
|
|
600
|
+
return ` ${label} ${off}`;
|
|
601
|
+
if (hooks.some((hook) => hook.trust === 'missing'))
|
|
602
|
+
return ` missing ${label} ${what}: not installed; run npx pendingyou@latest init --app codex${flag}`;
|
|
603
|
+
const trusted = hooks.every((hook) => hook.trust === 'trusted');
|
|
604
|
+
return trusted
|
|
605
|
+
? ` ok ${label} ${what} (${hooks.map((hook) => `${hook.event} trusted`).join(', ')})`
|
|
606
|
+
: ` missing ${label} ${what}, once you trust ${hooks.length === 1 ? 'its hook' : 'its hooks'} (${hooks.map((hook) => `${hook.event} ${words[hook.trust]}`).join(', ')}): ${TRUST_CLI}; ${TRUST_APP}`;
|
|
607
|
+
};
|
|
608
|
+
const ownConnection = credential?.kind === 'connection' && helper;
|
|
609
|
+
const notOwn = 'off: it needs Codex signed in through this computer’s own connection';
|
|
610
|
+
const permissionText = extraLine('Permission prompts:', 'a card when Codex waits for your OK', CODEX_PERMISSION_HOOKS.map((spec) => spec.sub), !ownConnection
|
|
611
|
+
? notOwn
|
|
612
|
+
: manifest?.permissionCards === false
|
|
613
|
+
? `off, as you chose; for a card when Codex waits for your OK, run npx pendingyou init --app codex --permission-cards${flag}`
|
|
614
|
+
: null);
|
|
615
|
+
const presenceText = extraLine('Presence:', 'tells Pending You when this session is open', CODEX_PRESENCE_HOOKS.map((spec) => spec.sub), ownConnection ? null : notOwn);
|
|
548
616
|
const wakes = detected
|
|
549
617
|
? atLeast(detected.version, detected.command ? CODEX_APP_WAKE : CODEX_WAKE)
|
|
550
618
|
: false;
|
|
@@ -562,6 +630,8 @@ export const codex = {
|
|
|
562
630
|
: []),
|
|
563
631
|
` ${mark(skill !== null)} Skill: ${skill !== null ? `saved (${codexSkillPath(io)})` : `not saved; run npx pendingyou init --app codex${flag}`}`,
|
|
564
632
|
` ${mark(hooksOk)} Hooks: ${hooksText}`,
|
|
633
|
+
permissionText,
|
|
634
|
+
presenceText,
|
|
565
635
|
` ${mark(wakes)} Wake: ${wakes
|
|
566
636
|
? detected?.command
|
|
567
637
|
? 'codex queue wakes a chat the Codex app has open when you answer, while the app runs (one it let go, idle 3 hours or behind 10 newer idle chats, hears it when you open it again)'
|