pendingyou 0.26.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 CHANGED
@@ -1,5 +1,19 @@
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
+
3
17
  ## 0.26.0 (2026-10-07)
4
18
 
5
19
  - A session that sat idle past its sign-in's 15-minute access token hears its answers again. The next message's
package/dist/api.js CHANGED
@@ -97,14 +97,22 @@ async function hookToken(io, origin, force, signal, app, expiredWaitMs) {
97
97
  }
98
98
  throw new Unavailable('The sign-in is being refreshed in the background.');
99
99
  }
100
+ export const REFRESH_RETRY_MS = 1500;
100
101
  export async function helperToken(io, origin, waitMs, app = DEFAULT_APP, force = false) {
101
102
  const stored = await readCredential(io, origin, app);
102
103
  if (!stored)
103
104
  throw new SignInNeeded(false);
104
105
  if (!force && stored.expiresAt - io.now() > SOON_MS)
105
106
  return stored.accessToken;
106
- if (!(await isLocked(credentialsPath(io))))
107
+ const lock = credentialsPath(io);
108
+ let started = Number.NEGATIVE_INFINITY;
109
+ const start = async () => {
110
+ if (await isLocked(lock))
111
+ return;
107
112
  io.background(refreshArgs(origin, app, force));
113
+ started = io.now();
114
+ };
115
+ await start();
108
116
  const until = io.now() + waitMs;
109
117
  while (io.now() < until) {
110
118
  await io.sleep(100);
@@ -113,6 +121,8 @@ export async function helperToken(io, origin, waitMs, app = DEFAULT_APP, force =
113
121
  throw new SignInNeeded(true);
114
122
  if (current.refreshToken !== stored.refreshToken)
115
123
  return current.accessToken;
124
+ if (io.now() - started >= REFRESH_RETRY_MS && io.now() + REFRESH_WAIT_MS < until)
125
+ await start();
116
126
  }
117
127
  if (!force && stored.expiresAt - io.now() > USABLE_MS)
118
128
  return stored.accessToken;
@@ -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
@@ -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";
@@ -142,6 +143,7 @@ export async function pickup(io, options, hook = { fallback: '' }) {
142
143
  let warning = null;
143
144
  let reminder = null;
144
145
  let unwoken = null;
146
+ let stuck = null;
145
147
  try {
146
148
  const input = await hookInput(io);
147
149
  const { cwd } = input;
@@ -176,6 +178,7 @@ export async function pickup(io, options, hook = { fallback: '' }) {
176
178
  hook.fallback = `${[REMINDER, warning].filter(Boolean).join('\n')}\n`;
177
179
  if (app === DEFAULT_APP)
178
180
  unwoken = await unwokenLine(io, origin, input.session, known);
181
+ stuck = await stuckLine(io, origin, app);
179
182
  }
180
183
  if (app !== DEFAULT_APP && input.session) {
181
184
  await touchThread(io, app, input.session, input.given ? input.cwd : null);
@@ -281,11 +284,13 @@ export async function pickup(io, options, hook = { fallback: '' }) {
281
284
  ...(warning ? [warning] : []),
282
285
  ...(reminder ? [reminder] : []),
283
286
  ...(unwoken ? [unwoken] : []),
287
+ ...(stuck ? [stuck] : []),
284
288
  ].join('\n')}\n`);
285
289
  line = null;
286
290
  warning = null;
287
291
  reminder = null;
288
292
  unwoken = null;
293
+ stuck = null;
289
294
  await markHanded(io, origin, fresh.map((heard) => ({ requestId: heard.requestId, moment: momentOf(heard) })));
290
295
  return 0;
291
296
  }
@@ -299,7 +304,7 @@ export async function pickup(io, options, hook = { fallback: '' }) {
299
304
  return 0;
300
305
  }
301
306
  finally {
302
- const last = [line, warning, reminder, unwoken].filter(Boolean);
307
+ const last = [line, warning, reminder, unwoken, stuck].filter(Boolean);
303
308
  if (last.length)
304
309
  io.out(`${last.join('\n')}\n`);
305
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
- io.out(`${JSON.stringify({ Authorization: `Bearer ${token}` })}\n`);
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.26.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.38.0",
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.38.0; send changes there.
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.26.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",