pendingyou 0.25.0 → 0.27.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 +24 -0
- package/dist/api.js +18 -4
- package/dist/main.js +2 -2
- package/dist/mcp-health.js +48 -0
- package/dist/pickup.js +11 -2
- package/dist/remote.js +13 -1
- package/dist/version.js +1 -1
- package/mod/.claude-plugin/plugin.json +1 -1
- package/mod/README.md +1 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,29 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.27.0 (2026-10-07)
|
|
4
|
+
|
|
5
|
+
- Claude Code's pendingyou MCP server no longer gets stuck on “needs authentication” after a computer sleeps. When it
|
|
6
|
+
reconnected during a brief wake, the sign-in's 15-minute access token had run out, the refresh couldn't land in the
|
|
7
|
+
headers helper's time, and the helper printed nothing. With no `Authorization` from its helper, Claude Code turns on
|
|
8
|
+
its own OAuth, and the 401 left the server needing authentication, every pendingyou tool failing, until `/mcp`. Now
|
|
9
|
+
the helper tries the refresh again every 1.5 seconds within its time when one fails (a network still coming up), and
|
|
10
|
+
while it has a sign-in it always hands over a token, even one that has run out: that's refused as a failed connect,
|
|
11
|
+
which Claude Code tries again by running the helper, never as a reason to start its own sign-in. It still prints
|
|
12
|
+
nothing when there's no sign-in or the sign-in has ended.
|
|
13
|
+
- When the helper last handed over a token that had run out, the next-message hook says so: if a pendingyou tool fails,
|
|
14
|
+
the agent tells you in chat to run `/mcp`, choose pendingyou and Reconnect (or restart Claude Code; Codex: restart
|
|
15
|
+
it), and asks in chat until it works. Said only within 12 hours, and no more once a later connect got a fresh token.
|
|
16
|
+
|
|
17
|
+
## 0.26.0 (2026-10-07)
|
|
18
|
+
|
|
19
|
+
- A session that sat idle past its sign-in's 15-minute access token hears its answers again. The next message's
|
|
20
|
+
hand-off started the background refresh, waited 1.2 seconds for it and gave up, often just before it landed, so an
|
|
21
|
+
answered card went unannounced. Now the hand-off and the session start's pickup wait for an expired token until 0.9
|
|
22
|
+
seconds before their 3-second deadline, leaving room to ask for answers; a token with seconds left still waits 1.2.
|
|
23
|
+
- Never silent: when the hand-off or the session start's pickup can't check for answers (Pending You unreachable, a
|
|
24
|
+
server error, or the sign-in still being refreshed), they add “Pending You couldn't check for answers just now: call
|
|
25
|
+
list_pending before you go on.” instead of only the plain reminder.
|
|
26
|
+
|
|
3
27
|
## 0.25.0 (2026-10-06)
|
|
4
28
|
|
|
5
29
|
- An answer reaches only the session that asked. The hooks asked Pending You for their folder's answers and open
|
package/dist/api.js
CHANGED
|
@@ -5,6 +5,8 @@ import { OAuthError, refresh } from "./oauth.js";
|
|
|
5
5
|
const EARLY_MS = 60_000;
|
|
6
6
|
export const SOON_MS = 5 * 60_000;
|
|
7
7
|
export const REFRESH_WAIT_MS = 1200;
|
|
8
|
+
export const HOOK_DEADLINE_MS = 3000;
|
|
9
|
+
export const ANSWERS_ROOM_MS = 900;
|
|
8
10
|
const USABLE_MS = 10_000;
|
|
9
11
|
export class SignInNeeded extends Error {
|
|
10
12
|
name = 'SignInNeeded';
|
|
@@ -72,7 +74,7 @@ export async function refreshInBackground(io, origin, force, app = DEFAULT_APP)
|
|
|
72
74
|
await refreshStored(io, origin, stored, { force, earlyMs: SOON_MS }, app);
|
|
73
75
|
return true;
|
|
74
76
|
}
|
|
75
|
-
async function hookToken(io, origin, force, signal, app) {
|
|
77
|
+
async function hookToken(io, origin, force, signal, app, expiredWaitMs) {
|
|
76
78
|
const stored = await readCredential(io, origin, app);
|
|
77
79
|
if (!stored)
|
|
78
80
|
throw new SignInNeeded(false);
|
|
@@ -83,7 +85,8 @@ async function hookToken(io, origin, force, signal, app) {
|
|
|
83
85
|
io.background(refreshArgs(origin, app, force));
|
|
84
86
|
if (!force && left > USABLE_MS)
|
|
85
87
|
return stored.accessToken;
|
|
86
|
-
const
|
|
88
|
+
const wait = !force && left <= 0 && expiredWaitMs !== undefined ? expiredWaitMs : REFRESH_WAIT_MS;
|
|
89
|
+
const until = io.now() + wait;
|
|
87
90
|
while (io.now() < until && !signal?.aborted) {
|
|
88
91
|
await io.sleep(50, signal);
|
|
89
92
|
const current = await readCredential(io, origin, app);
|
|
@@ -94,14 +97,22 @@ async function hookToken(io, origin, force, signal, app) {
|
|
|
94
97
|
}
|
|
95
98
|
throw new Unavailable('The sign-in is being refreshed in the background.');
|
|
96
99
|
}
|
|
100
|
+
export const REFRESH_RETRY_MS = 1500;
|
|
97
101
|
export async function helperToken(io, origin, waitMs, app = DEFAULT_APP, force = false) {
|
|
98
102
|
const stored = await readCredential(io, origin, app);
|
|
99
103
|
if (!stored)
|
|
100
104
|
throw new SignInNeeded(false);
|
|
101
105
|
if (!force && stored.expiresAt - io.now() > SOON_MS)
|
|
102
106
|
return stored.accessToken;
|
|
103
|
-
|
|
107
|
+
const lock = credentialsPath(io);
|
|
108
|
+
let started = Number.NEGATIVE_INFINITY;
|
|
109
|
+
const start = async () => {
|
|
110
|
+
if (await isLocked(lock))
|
|
111
|
+
return;
|
|
104
112
|
io.background(refreshArgs(origin, app, force));
|
|
113
|
+
started = io.now();
|
|
114
|
+
};
|
|
115
|
+
await start();
|
|
105
116
|
const until = io.now() + waitMs;
|
|
106
117
|
while (io.now() < until) {
|
|
107
118
|
await io.sleep(100);
|
|
@@ -110,6 +121,8 @@ export async function helperToken(io, origin, waitMs, app = DEFAULT_APP, force =
|
|
|
110
121
|
throw new SignInNeeded(true);
|
|
111
122
|
if (current.refreshToken !== stored.refreshToken)
|
|
112
123
|
return current.accessToken;
|
|
124
|
+
if (io.now() - started >= REFRESH_RETRY_MS && io.now() + REFRESH_WAIT_MS < until)
|
|
125
|
+
await start();
|
|
113
126
|
}
|
|
114
127
|
if (!force && stored.expiresAt - io.now() > USABLE_MS)
|
|
115
128
|
return stored.accessToken;
|
|
@@ -133,6 +146,7 @@ export function getJson(io, origin, path, timeoutMs, signal, options = {}) {
|
|
|
133
146
|
export function sendJson(io, origin, path, body, timeoutMs, options = {}) {
|
|
134
147
|
return callJson(io, origin, path, { body }, timeoutMs, undefined, options);
|
|
135
148
|
}
|
|
149
|
+
export const expiredWait = (uptime) => Math.max(0, HOOK_DEADLINE_MS - ANSWERS_ROOM_MS - uptime);
|
|
136
150
|
async function callJson(io, origin, path, send, timeoutMs, signal, options = {}) {
|
|
137
151
|
const app = options.app ?? DEFAULT_APP;
|
|
138
152
|
for (let attempt = 0;; attempt++) {
|
|
@@ -140,7 +154,7 @@ async function callJson(io, origin, path, send, timeoutMs, signal, options = {})
|
|
|
140
154
|
try {
|
|
141
155
|
token =
|
|
142
156
|
options.hook && io.background
|
|
143
|
-
? await hookToken(io, origin, attempt > 0, signal, app)
|
|
157
|
+
? await hookToken(io, origin, attempt > 0, signal, app, options.patient && io.uptime ? expiredWait(io.uptime()) : undefined)
|
|
144
158
|
: await freshToken(io, origin, attempt > 0, app);
|
|
145
159
|
}
|
|
146
160
|
catch (error) {
|
package/dist/main.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { refreshInBackground, SignInNeeded, Unavailable } from "./api.js";
|
|
1
|
+
import { HOOK_DEADLINE_MS, refreshInBackground, SignInNeeded, Unavailable } from "./api.js";
|
|
2
2
|
import { listen, listenHanded, posted } from "./apps/codex-wake.js";
|
|
3
3
|
import { DEFAULT_APP } from "./apps/ids.js";
|
|
4
4
|
import { appModule } from "./apps/registry.js";
|
|
@@ -101,7 +101,7 @@ async function run(io, command, hook = { fallback: '' }) {
|
|
|
101
101
|
return (await import("./app-login.js")).appCommand(io, command);
|
|
102
102
|
}
|
|
103
103
|
}
|
|
104
|
-
export
|
|
104
|
+
export { HOOK_DEADLINE_MS };
|
|
105
105
|
async function runHook(io, command) {
|
|
106
106
|
const hook = { fallback: '' };
|
|
107
107
|
const controller = new AbortController();
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { join } from 'node:path';
|
|
2
|
+
import { APP_NAMES, DEFAULT_APP } from "./apps/ids.js";
|
|
3
|
+
import { slotOf } from "./credentials.js";
|
|
4
|
+
import { configDir, readJson, writeWhole } from "./files.js";
|
|
5
|
+
export const STUCK_FOR_MS = 12 * 60 * 60_000;
|
|
6
|
+
const helperStatePath = (io) => join(configDir(io), 'helper.json');
|
|
7
|
+
async function readAll(io) {
|
|
8
|
+
const file = await readJson(helperStatePath(io)).catch(() => null);
|
|
9
|
+
return file && typeof file.slots === 'object' && file.slots !== null ? file.slots : {};
|
|
10
|
+
}
|
|
11
|
+
export async function recordHelper(io, origin, app, outcome) {
|
|
12
|
+
try {
|
|
13
|
+
const slots = await readAll(io);
|
|
14
|
+
const key = slotOf(origin, app);
|
|
15
|
+
if (slots[key]?.outcome === outcome && outcome === 'ok')
|
|
16
|
+
return;
|
|
17
|
+
slots[key] = { at: io.now(), outcome };
|
|
18
|
+
await writeWhole(helperStatePath(io), `${JSON.stringify({ version: 1, slots }, null, 2)}\n`, {
|
|
19
|
+
secret: true,
|
|
20
|
+
});
|
|
21
|
+
}
|
|
22
|
+
catch { }
|
|
23
|
+
}
|
|
24
|
+
export async function helperOutcome(io, origin, app = DEFAULT_APP) {
|
|
25
|
+
const entry = (await readAll(io).catch(() => ({})))[slotOf(origin, app)];
|
|
26
|
+
return entry &&
|
|
27
|
+
typeof entry.at === 'number' &&
|
|
28
|
+
(entry.outcome === 'ok' || entry.outcome === 'stale')
|
|
29
|
+
? entry
|
|
30
|
+
: null;
|
|
31
|
+
}
|
|
32
|
+
export function stuckText(app) {
|
|
33
|
+
const back = app === DEFAULT_APP
|
|
34
|
+
? 'run /mcp, choose pendingyou and Reconnect (no need to Authenticate), or restart Claude Code'
|
|
35
|
+
: `restart ${APP_NAMES[app]}`;
|
|
36
|
+
return `Pending You: when ${APP_NAMES[app]} last connected to its pendingyou MCP server, this computer couldn’t refresh its sign-in in time, so that connection may have been refused. If a pendingyou tool fails (not connected, or needs authentication), tell your person in chat: ${back}. Until it works, ask them in chat.`;
|
|
37
|
+
}
|
|
38
|
+
export async function stuckLine(io, origin, app) {
|
|
39
|
+
try {
|
|
40
|
+
const last = await helperOutcome(io, origin, app);
|
|
41
|
+
if (last?.outcome !== 'stale' || io.now() - last.at > STUCK_FOR_MS)
|
|
42
|
+
return null;
|
|
43
|
+
return stuckText(app);
|
|
44
|
+
}
|
|
45
|
+
catch {
|
|
46
|
+
return null;
|
|
47
|
+
}
|
|
48
|
+
}
|
package/dist/pickup.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { getJson, SignInNeeded } from "./api.js";
|
|
1
|
+
import { getJson, SignInNeeded, Unavailable } from "./api.js";
|
|
2
2
|
import { computerNames, hearsOnly, keepListening, markHandedTurn, sessionFor, threadCards, threadNames, touchThread, } from "./apps/codex-wake.js";
|
|
3
3
|
import { APP_NAMES, DEFAULT_APP } from "./apps/ids.js";
|
|
4
4
|
import { connectionMachine, readCredential } from "./credentials.js";
|
|
@@ -9,6 +9,7 @@ import { goesBy, HANDED_GRACE_MS, namesAnotherComputer } from "./handed.js";
|
|
|
9
9
|
import { herdrTarget, reportCommand } from "./herdr.js";
|
|
10
10
|
import { holdCommand } from "./hold.js";
|
|
11
11
|
import { unwokenLine } from "./loaded.js";
|
|
12
|
+
import { stuckLine } from "./mcp-health.js";
|
|
12
13
|
import { isPermissionCard, isPromptApp, POSTED_MAX, sessionName, sessionPosted, settlePrompts, } from "./permission.js";
|
|
13
14
|
import { announce, keepPresent, readSaid } from "./presence.js";
|
|
14
15
|
import { setupReminder } from "./setup.js";
|
|
@@ -17,6 +18,7 @@ const TIME_BOX_MS = { pickup: 2500, handoff: 2000 };
|
|
|
17
18
|
export const ANSWERS_ASKED = 20;
|
|
18
19
|
export const ANSWERS_SHOWN = 5;
|
|
19
20
|
export const REMINDER = 'Pending You: anything you need from your person also goes on a card.';
|
|
21
|
+
export const UNCHECKED = "Pending You couldn't check for answers just now: call list_pending before you go on.";
|
|
20
22
|
export const OPEN_SHOWN = 3;
|
|
21
23
|
export const OPEN_TITLE = 50;
|
|
22
24
|
export const OPEN_NAME = 24;
|
|
@@ -141,6 +143,7 @@ export async function pickup(io, options, hook = { fallback: '' }) {
|
|
|
141
143
|
let warning = null;
|
|
142
144
|
let reminder = null;
|
|
143
145
|
let unwoken = null;
|
|
146
|
+
let stuck = null;
|
|
144
147
|
try {
|
|
145
148
|
const input = await hookInput(io);
|
|
146
149
|
const { cwd } = input;
|
|
@@ -175,6 +178,7 @@ export async function pickup(io, options, hook = { fallback: '' }) {
|
|
|
175
178
|
hook.fallback = `${[REMINDER, warning].filter(Boolean).join('\n')}\n`;
|
|
176
179
|
if (app === DEFAULT_APP)
|
|
177
180
|
unwoken = await unwokenLine(io, origin, input.session, known);
|
|
181
|
+
stuck = await stuckLine(io, origin, app);
|
|
178
182
|
}
|
|
179
183
|
if (app !== DEFAULT_APP && input.session) {
|
|
180
184
|
await touchThread(io, app, input.session, input.given ? input.cwd : null);
|
|
@@ -200,6 +204,7 @@ export async function pickup(io, options, hook = { fallback: '' }) {
|
|
|
200
204
|
]);
|
|
201
205
|
const { status, body } = await getJson(io, origin, `/mcp/cli/answers?${query}`, TIME_BOX_MS[mode], io.signal, {
|
|
202
206
|
hook: true,
|
|
207
|
+
patient: true,
|
|
203
208
|
app,
|
|
204
209
|
});
|
|
205
210
|
if (status !== 200)
|
|
@@ -279,11 +284,13 @@ export async function pickup(io, options, hook = { fallback: '' }) {
|
|
|
279
284
|
...(warning ? [warning] : []),
|
|
280
285
|
...(reminder ? [reminder] : []),
|
|
281
286
|
...(unwoken ? [unwoken] : []),
|
|
287
|
+
...(stuck ? [stuck] : []),
|
|
282
288
|
].join('\n')}\n`);
|
|
283
289
|
line = null;
|
|
284
290
|
warning = null;
|
|
285
291
|
reminder = null;
|
|
286
292
|
unwoken = null;
|
|
293
|
+
stuck = null;
|
|
287
294
|
await markHanded(io, origin, fresh.map((heard) => ({ requestId: heard.requestId, moment: momentOf(heard) })));
|
|
288
295
|
return 0;
|
|
289
296
|
}
|
|
@@ -292,10 +299,12 @@ export async function pickup(io, options, hook = { fallback: '' }) {
|
|
|
292
299
|
io.out(`Pending You: this computer’s pendingyou sign-in has ended, so answers can’t wake ${APP_NAMES[app]}. Ask your person to run \`npx pendingyou login${app === DEFAULT_APP ? '' : ` --app ${app}`}\`.\n`);
|
|
293
300
|
if (error instanceof SignInNeeded)
|
|
294
301
|
line = null;
|
|
302
|
+
if (error instanceof Unavailable)
|
|
303
|
+
line = [line, UNCHECKED].filter(Boolean).join('\n');
|
|
295
304
|
return 0;
|
|
296
305
|
}
|
|
297
306
|
finally {
|
|
298
|
-
const last = [line, warning, reminder, unwoken].filter(Boolean);
|
|
307
|
+
const last = [line, warning, reminder, unwoken, stuck].filter(Boolean);
|
|
299
308
|
if (last.length)
|
|
300
309
|
io.out(`${last.join('\n')}\n`);
|
|
301
310
|
}
|
package/dist/remote.js
CHANGED
|
@@ -5,6 +5,7 @@ import { APP_NAMES, DEFAULT_APP } from "./apps/ids.js";
|
|
|
5
5
|
import { cleanName, DEFAULT_ORIGIN } from "./args.js";
|
|
6
6
|
import { readCredential } from "./credentials.js";
|
|
7
7
|
import { configDir, writeWhole } from "./files.js";
|
|
8
|
+
import { recordHelper } from "./mcp-health.js";
|
|
8
9
|
import { VERSION } from "./version.js";
|
|
9
10
|
export const HELPER = 'pendingyou-mcp-headers';
|
|
10
11
|
export const helperPath = (io) => join(configDir(io), 'bin', HELPER);
|
|
@@ -255,12 +256,23 @@ export async function mcpHeaders(io, options) {
|
|
|
255
256
|
io.err(`pendingyou: this computer’s sign-in only hears answers, so ${name} can’t use it. Run: ${login}\n`);
|
|
256
257
|
return 1;
|
|
257
258
|
}
|
|
259
|
+
const headers = (token) => io.out(`${JSON.stringify({ Authorization: `Bearer ${token}` })}\n`);
|
|
258
260
|
try {
|
|
259
261
|
const token = await helperToken(io, options.origin, HELPER_WAIT_MS, app);
|
|
260
|
-
|
|
262
|
+
headers(token);
|
|
263
|
+
await recordHelper(io, options.origin, app, 'ok');
|
|
261
264
|
return 0;
|
|
262
265
|
}
|
|
263
266
|
catch (error) {
|
|
267
|
+
if (error instanceof Unavailable) {
|
|
268
|
+
const current = await readCredential(io, options.origin, app).catch(() => null);
|
|
269
|
+
if (current?.kind === 'connection') {
|
|
270
|
+
headers(current.accessToken);
|
|
271
|
+
io.err('pendingyou: couldn’t refresh the sign-in in time (is this computer online?), so this token may be refused; the refresh goes on in the background.\n');
|
|
272
|
+
await recordHelper(io, options.origin, app, current.expiresAt > io.now() ? 'ok' : 'stale');
|
|
273
|
+
return 0;
|
|
274
|
+
}
|
|
275
|
+
}
|
|
264
276
|
io.err(error instanceof SignInNeeded
|
|
265
277
|
? `pendingyou: this computer’s sign-in has ended. Run: ${login}\n`
|
|
266
278
|
: error instanceof Unavailable
|
package/dist/version.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export const VERSION = '0.
|
|
1
|
+
export const VERSION = '0.27.0';
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pendingyou-wake",
|
|
3
3
|
"displayName": "Pending You",
|
|
4
|
-
"version": "2.
|
|
4
|
+
"version": "2.39.0",
|
|
5
5
|
"description": "Wakes Claude Code when your person answers a Pending You card it posted, even while it sits idle. Installed by `npx pendingyou init`.",
|
|
6
6
|
"author": {
|
|
7
7
|
"name": "RecordPlane",
|
package/mod/README.md
CHANGED
|
@@ -4,4 +4,4 @@ The pendingyou command line’s copy of the Pending You plugin’s wake mod, for
|
|
|
4
4
|
|
|
5
5
|
In each session it learns the cards the session posts to Pending You, checks them through the session’s own Pending You connection, and starts a turn when your person answers, writes back or a fallback comes due, or hands the session a question from another of their assistants (CLI 0.14.0), even while it sits idle. It answers `npx pendingyou hold` itself, so nothing waits in the background, and shows “2 waiting on you” under the prompt, after the plugin’s name. With the Pending You plugin installed too, only one copy acts.
|
|
6
6
|
|
|
7
|
-
Generated from recordplane/pendingyou (packages/claude-plugin/src/wake) with Pending You’s guide 2.
|
|
7
|
+
Generated from recordplane/pendingyou (packages/claude-plugin/src/wake) with Pending You’s guide 2.39.0; send changes there.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pendingyou",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.27.0",
|
|
4
4
|
"description": "Pending You for your coding agents: one command sets up Claude Code, Codex, OpenCode and Pi to ask you through Pending You and hear your answers, even while they sit idle.",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"homepage": "https://www.pendingyou.com",
|