pendingyou 0.26.0 → 0.28.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 +22 -0
- package/dist/api.js +11 -1
- package/dist/apps/codex-wake.js +5 -0
- package/dist/format.js +22 -4
- package/dist/mcp-health.js +48 -0
- package/dist/pickup.js +6 -1
- package/dist/remote.js +13 -1
- package/dist/sdk/code.js +138 -0
- package/dist/sdk/index.js +1 -0
- package/dist/sdk/node.js +74 -0
- 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,27 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.28.0 (2026-10-07)
|
|
4
|
+
|
|
5
|
+
- When a card you asked was handed to another assistant and comes back to your person without its answer (handed
|
|
6
|
+
back, brought back after the two of you traded too many messages or went quiet, taken back, its answer back with
|
|
7
|
+
your person to send, or the helper removed), the session that asked hears it: “Pending You: req_… came back from
|
|
8
|
+
codex-setup to your person.”, Pending You's line saying why, and to ack_answer it (nothing to answer; your person's
|
|
9
|
+
answer reaches you as usual). Codex threads are woken with the same. Before, the card went quiet.
|
|
10
|
+
|
|
11
|
+
## 0.27.0 (2026-10-07)
|
|
12
|
+
|
|
13
|
+
- Claude Code's pendingyou MCP server no longer gets stuck on “needs authentication” after a computer sleeps. When it
|
|
14
|
+
reconnected during a brief wake, the sign-in's 15-minute access token had run out, the refresh couldn't land in the
|
|
15
|
+
headers helper's time, and the helper printed nothing. With no `Authorization` from its helper, Claude Code turns on
|
|
16
|
+
its own OAuth, and the 401 left the server needing authentication, every pendingyou tool failing, until `/mcp`. Now
|
|
17
|
+
the helper tries the refresh again every 1.5 seconds within its time when one fails (a network still coming up), and
|
|
18
|
+
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,
|
|
19
|
+
which Claude Code tries again by running the helper, never as a reason to start its own sign-in. It still prints
|
|
20
|
+
nothing when there's no sign-in or the sign-in has ended.
|
|
21
|
+
- When the helper last handed over a token that had run out, the next-message hook says so: if a pendingyou tool fails,
|
|
22
|
+
the agent tells you in chat to run `/mcp`, choose pendingyou and Reconnect (or restart Claude Code; Codex: restart
|
|
23
|
+
it), and asks in chat until it works. Said only within 12 hours, and no more once a later connect got a fresh token.
|
|
24
|
+
|
|
3
25
|
## 0.26.0 (2026-10-07)
|
|
4
26
|
|
|
5
27
|
- 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
|
-
|
|
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;
|
package/dist/apps/codex-wake.js
CHANGED
|
@@ -390,6 +390,11 @@ function line(heard, card) {
|
|
|
390
390
|
`your person handed your open cards to ${from}, ${titled} among them`,
|
|
391
391
|
`stop working on them: ${from} answers them and follows up; ack_answer ${heard.requestId} with its version to say you’ve stopped`,
|
|
392
392
|
];
|
|
393
|
+
if (notice?.kind === 'came_back')
|
|
394
|
+
return [
|
|
395
|
+
`${titled} came back from ${from} to your person`,
|
|
396
|
+
`call ${get} to read why (helper.ended), then ack_answer with its version to say you’ve read it; their answer reaches you as usual`,
|
|
397
|
+
];
|
|
393
398
|
if (notice?.kind === 'follow_up' && heard.status !== 'answered')
|
|
394
399
|
return [
|
|
395
400
|
`${from} followed up on ${titled}, which it answered for your person`,
|
package/dist/format.js
CHANGED
|
@@ -1,11 +1,12 @@
|
|
|
1
|
+
const NOTICES = ['follow_up', 'handed_over', 'came_back'];
|
|
1
2
|
export function noticeOf(heard) {
|
|
2
3
|
const given = heard.notice;
|
|
3
4
|
if (typeof given !== 'object' || given === null)
|
|
4
5
|
return null;
|
|
5
6
|
const { kind, from } = given;
|
|
6
|
-
if ((kind
|
|
7
|
+
if (!NOTICES.includes(kind) || typeof from !== 'string')
|
|
7
8
|
return null;
|
|
8
|
-
return { kind, from };
|
|
9
|
+
return { kind: kind, from };
|
|
9
10
|
}
|
|
10
11
|
export function handledOf(heard) {
|
|
11
12
|
const given = heard.handled;
|
|
@@ -32,10 +33,15 @@ export function delegatedOf(heard) {
|
|
|
32
33
|
const given = heard.delegated;
|
|
33
34
|
if (typeof given !== 'object' || given === null)
|
|
34
35
|
return null;
|
|
35
|
-
const { from, mode, note } = given;
|
|
36
|
+
const { from, mode, note, authority } = given;
|
|
36
37
|
if (typeof from !== 'string' || (mode !== 'freely' && mode !== 'loop'))
|
|
37
38
|
return null;
|
|
38
|
-
return {
|
|
39
|
+
return {
|
|
40
|
+
from,
|
|
41
|
+
mode,
|
|
42
|
+
...(typeof authority === 'string' && authority.trim() ? { authority } : {}),
|
|
43
|
+
...(typeof note === 'string' && note.trim() ? { note } : {}),
|
|
44
|
+
};
|
|
39
45
|
}
|
|
40
46
|
export const modeWords = (mode, from) => mode === 'freely'
|
|
41
47
|
? `answer freely: your answer goes straight to ${from}`
|
|
@@ -44,6 +50,7 @@ export const HANDED_TODO = 'If you know, answer_delegated with your answer and h
|
|
|
44
50
|
const MAX_ANSWER = 600;
|
|
45
51
|
const MAX_MESSAGE = 400;
|
|
46
52
|
const MAX_TITLE = 140;
|
|
53
|
+
const MAX_AUTHORITY = 500;
|
|
47
54
|
const MAX_QUESTION = 100;
|
|
48
55
|
const MAX_QUESTION_ANSWER = 300;
|
|
49
56
|
export function clip(text, max) {
|
|
@@ -91,6 +98,7 @@ function describeHanded(heard, delegated) {
|
|
|
91
98
|
}
|
|
92
99
|
return [
|
|
93
100
|
`${HANDED_PREFIX} ${id}, a question from ${from} (${modeWords(delegated.mode, from)}).`,
|
|
101
|
+
...(delegated.authority ? [clip(delegated.authority, MAX_AUTHORITY).text] : []),
|
|
94
102
|
`Question: ${clip(heard.title, MAX_TITLE).text}`,
|
|
95
103
|
...(note ? [`Their note: “${note.text}”`] : []),
|
|
96
104
|
`Call get_request (requestId ${id}, name ${named(heard)}) to read it. ${HANDED_TODO}`,
|
|
@@ -106,6 +114,16 @@ function describeNotice(heard, notice) {
|
|
|
106
114
|
`Pending You: your person handed your open cards to ${from}, ${id} among them.`,
|
|
107
115
|
`Stop working on them: ${from} answers them and follows up. Call ${ack} to say you’ve stopped.`,
|
|
108
116
|
];
|
|
117
|
+
if (notice.kind === 'came_back') {
|
|
118
|
+
const why = heard.messages.at(-1)?.body.split(' Nothing to answer:')[0] ?? '';
|
|
119
|
+
const said = clip(why, MAX_MESSAGE);
|
|
120
|
+
return [
|
|
121
|
+
`Pending You: ${id} came back from ${from} to your person.`,
|
|
122
|
+
...(said.text ? [said.text] : []),
|
|
123
|
+
`Nothing to answer: call ${ack} to say you’ve read it. Your person’s answer reaches you as usual.`,
|
|
124
|
+
...(said.cut ? [`(Cut short: get_request ${id} has all of it.)`] : []),
|
|
125
|
+
];
|
|
126
|
+
}
|
|
109
127
|
let cut = false;
|
|
110
128
|
const wrote = heard.messages.map((message) => {
|
|
111
129
|
const said = quoted(message.body, MAX_MESSAGE);
|
|
@@ -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
|
-
|
|
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/sdk/code.js
ADDED
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
import { generateKey, importKey, sha256 } from "./dpop.js";
|
|
2
|
+
import { SignInError } from "./errors.js";
|
|
3
|
+
import { nonces, tokenRequest } from "./oauth.js";
|
|
4
|
+
export const AUTHORIZE_PATH = '/oauth/authorize';
|
|
5
|
+
const random = (bytes) => {
|
|
6
|
+
let binary = '';
|
|
7
|
+
for (const byte of crypto.getRandomValues(new Uint8Array(bytes)))
|
|
8
|
+
binary += String.fromCharCode(byte);
|
|
9
|
+
return btoa(binary).replaceAll('+', '-').replaceAll('/', '_').replace(/=+$/, '');
|
|
10
|
+
};
|
|
11
|
+
export async function codeSignIn(options) {
|
|
12
|
+
const { jwk } = await generateKey();
|
|
13
|
+
const pending = {
|
|
14
|
+
version: 1,
|
|
15
|
+
origin: options.origin.replace(/\/+$/, ''),
|
|
16
|
+
clientId: options.clientId,
|
|
17
|
+
redirectUri: options.redirectUri,
|
|
18
|
+
scopes: [...options.scopes],
|
|
19
|
+
state: random(16),
|
|
20
|
+
verifier: random(32),
|
|
21
|
+
key: jwk,
|
|
22
|
+
};
|
|
23
|
+
return resumeCodeSignIn(pending, options);
|
|
24
|
+
}
|
|
25
|
+
export async function resumeCodeSignIn(pending, options = {}) {
|
|
26
|
+
const http = {
|
|
27
|
+
fetch: options.fetch ?? ((input, init) => fetch(input, init)),
|
|
28
|
+
now: options.now ?? (() => Date.now()),
|
|
29
|
+
};
|
|
30
|
+
const key = await importKey(pending.key);
|
|
31
|
+
const url = new URL(`${pending.origin}${AUTHORIZE_PATH}`);
|
|
32
|
+
url.search = new URLSearchParams({
|
|
33
|
+
response_type: 'code',
|
|
34
|
+
client_id: pending.clientId,
|
|
35
|
+
redirect_uri: pending.redirectUri,
|
|
36
|
+
scope: pending.scopes.join(' '),
|
|
37
|
+
state: pending.state,
|
|
38
|
+
code_challenge: await sha256(pending.verifier),
|
|
39
|
+
code_challenge_method: 'S256',
|
|
40
|
+
resource: `${pending.origin}/v1`,
|
|
41
|
+
dpop_jkt: key.jkt,
|
|
42
|
+
}).toString();
|
|
43
|
+
let finished = false;
|
|
44
|
+
async function finish(callback) {
|
|
45
|
+
if (finished)
|
|
46
|
+
throw new SignInError('finished', 'This sign-in is finished already.');
|
|
47
|
+
const back = new URL(String(callback));
|
|
48
|
+
if (back.searchParams.get('state') !== pending.state)
|
|
49
|
+
throw new SignInError('state_mismatch', 'This isn’t the answer to this sign-in.');
|
|
50
|
+
const iss = back.searchParams.get('iss');
|
|
51
|
+
if (iss !== null && iss !== pending.origin)
|
|
52
|
+
throw new SignInError('issuer_mismatch', 'This answer came from somewhere else.');
|
|
53
|
+
const error = back.searchParams.get('error');
|
|
54
|
+
if (error)
|
|
55
|
+
throw new SignInError(error, error === 'access_denied'
|
|
56
|
+
? 'The person didn’t allow it.'
|
|
57
|
+
: `Pending You refused the sign-in (${error}).`);
|
|
58
|
+
const code = back.searchParams.get('code');
|
|
59
|
+
if (!code)
|
|
60
|
+
throw new SignInError('no_code', 'The answer had no code.');
|
|
61
|
+
finished = true;
|
|
62
|
+
const result = await tokenRequest(http, pending.origin, key, {
|
|
63
|
+
grant_type: 'authorization_code',
|
|
64
|
+
code,
|
|
65
|
+
redirect_uri: pending.redirectUri,
|
|
66
|
+
client_id: pending.clientId,
|
|
67
|
+
code_verifier: pending.verifier,
|
|
68
|
+
resource: `${pending.origin}/v1`,
|
|
69
|
+
}, nonces());
|
|
70
|
+
if (!result.ok)
|
|
71
|
+
throw new SignInError(result.error, `Pending You didn’t finish the sign-in (${result.error}). Start it again.`);
|
|
72
|
+
const credentials = {
|
|
73
|
+
version: 1,
|
|
74
|
+
origin: pending.origin,
|
|
75
|
+
clientId: pending.clientId,
|
|
76
|
+
accessToken: result.tokens.access_token,
|
|
77
|
+
refreshToken: result.tokens.refresh_token,
|
|
78
|
+
expiresAt: http.now() + result.tokens.expires_in * 1000,
|
|
79
|
+
scopes: result.tokens.scope ? result.tokens.scope.split(' ') : [...pending.scopes],
|
|
80
|
+
key: pending.key,
|
|
81
|
+
};
|
|
82
|
+
const store = options.store;
|
|
83
|
+
if (store)
|
|
84
|
+
await store.exclusive(() => store.save(credentials));
|
|
85
|
+
return credentials;
|
|
86
|
+
}
|
|
87
|
+
return { url: url.toString(), pending, finish };
|
|
88
|
+
}
|
|
89
|
+
export function isPendingCodeSignIn(value) {
|
|
90
|
+
const entry = value;
|
|
91
|
+
return (typeof entry === 'object' &&
|
|
92
|
+
entry !== null &&
|
|
93
|
+
entry.version === 1 &&
|
|
94
|
+
typeof entry.origin === 'string' &&
|
|
95
|
+
typeof entry.clientId === 'string' &&
|
|
96
|
+
typeof entry.redirectUri === 'string' &&
|
|
97
|
+
Array.isArray(entry.scopes) &&
|
|
98
|
+
typeof entry.state === 'string' &&
|
|
99
|
+
typeof entry.verifier === 'string' &&
|
|
100
|
+
typeof entry.key === 'object' &&
|
|
101
|
+
entry.key !== null);
|
|
102
|
+
}
|
|
103
|
+
export const PENDING_KEY = 'pendingyou.signin';
|
|
104
|
+
const sessionStorageHere = () => globalThis.sessionStorage;
|
|
105
|
+
export async function redirectToSignIn(options) {
|
|
106
|
+
const storage = options.storage ?? sessionStorageHere();
|
|
107
|
+
if (!storage)
|
|
108
|
+
throw new TypeError('No sessionStorage here: pass `storage`');
|
|
109
|
+
const started = await codeSignIn(options);
|
|
110
|
+
storage.setItem(PENDING_KEY, JSON.stringify(started.pending));
|
|
111
|
+
const go = options.go ??
|
|
112
|
+
((url) => {
|
|
113
|
+
const here = globalThis.location;
|
|
114
|
+
if (!here)
|
|
115
|
+
throw new TypeError('No location here: pass `go`');
|
|
116
|
+
here.assign(url);
|
|
117
|
+
});
|
|
118
|
+
go(started.url);
|
|
119
|
+
}
|
|
120
|
+
export async function finishRedirectSignIn(options = {}) {
|
|
121
|
+
const storage = options.storage ?? sessionStorageHere();
|
|
122
|
+
if (!storage)
|
|
123
|
+
throw new TypeError('No sessionStorage here: pass `storage`');
|
|
124
|
+
const kept = storage.getItem(PENDING_KEY);
|
|
125
|
+
storage.removeItem(PENDING_KEY);
|
|
126
|
+
let pending = null;
|
|
127
|
+
try {
|
|
128
|
+
pending = kept ? JSON.parse(kept) : null;
|
|
129
|
+
}
|
|
130
|
+
catch { }
|
|
131
|
+
if (!isPendingCodeSignIn(pending))
|
|
132
|
+
throw new SignInError('not_started', 'No sign-in was started in this tab.');
|
|
133
|
+
const url = options.url ?? globalThis.location?.href ?? undefined;
|
|
134
|
+
if (!url)
|
|
135
|
+
throw new TypeError('No location here: pass `url`');
|
|
136
|
+
const started = await resumeCodeSignIn(pending, options);
|
|
137
|
+
return started.finish(url);
|
|
138
|
+
}
|
package/dist/sdk/index.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
export { backoffMs, RESYNC_MS } from "./changes.js";
|
|
2
2
|
export { createClient } from "./client.js";
|
|
3
|
+
export { codeSignIn, finishRedirectSignIn, redirectToSignIn, resumeCodeSignIn, } from "./code.js";
|
|
3
4
|
export { deviceSignIn, } from "./device.js";
|
|
4
5
|
export { generateKey, importKey, thumbprint } from "./dpop.js";
|
|
5
6
|
export { ApiError, SignedOutError, SignInError } from "./errors.js";
|
package/dist/sdk/node.js
CHANGED
|
@@ -1,7 +1,10 @@
|
|
|
1
1
|
import { execFile } from 'node:child_process';
|
|
2
2
|
import { randomBytes } from 'node:crypto';
|
|
3
3
|
import { chmod, mkdir, readFile, rename, rm, stat, writeFile } from 'node:fs/promises';
|
|
4
|
+
import { createServer } from 'node:http';
|
|
4
5
|
import { dirname, join } from 'node:path';
|
|
6
|
+
import { codeSignIn } from "./code.js";
|
|
7
|
+
import { SignInError } from "./errors.js";
|
|
5
8
|
import { isCredentials } from "./store.js";
|
|
6
9
|
export const STALE_LOCK_MS = 30_000;
|
|
7
10
|
export const LOCK_WAIT_MS = 15_000;
|
|
@@ -87,6 +90,77 @@ export function fileStore(path) {
|
|
|
87
90
|
exclusive: (work) => withFileLock(path, work),
|
|
88
91
|
};
|
|
89
92
|
}
|
|
93
|
+
const page = (title, body) => `<!doctype html><meta charset="utf-8"><meta name="viewport" content="width=device-width"><title>${title}</title><body style="font:16px/1.5 system-ui,sans-serif;max-width:32rem;margin:15vh auto;padding:0 16px"><h1 style="font-weight:500">${title}</h1><p>${body}</p></body>`;
|
|
94
|
+
export async function loopbackSignIn(options) {
|
|
95
|
+
const registered = new URL(options.redirectUri ?? 'http://127.0.0.1/callback');
|
|
96
|
+
if (registered.protocol !== 'http:' ||
|
|
97
|
+
!['127.0.0.1', '[::1]', 'localhost'].includes(registered.hostname))
|
|
98
|
+
throw new TypeError('A loopback sign-in comes back to http://127.0.0.1');
|
|
99
|
+
const server = createServer();
|
|
100
|
+
await new Promise((done, fail) => {
|
|
101
|
+
server.once('error', fail);
|
|
102
|
+
server.listen(options.port ?? 0, registered.hostname === '[::1]' ? '::1' : '127.0.0.1', () => done());
|
|
103
|
+
});
|
|
104
|
+
const address = server.address();
|
|
105
|
+
const port = typeof address === 'object' && address ? address.port : 0;
|
|
106
|
+
const back = new URL(registered.toString());
|
|
107
|
+
back.port = String(port);
|
|
108
|
+
const redirectUri = back.toString();
|
|
109
|
+
const started = await codeSignIn({ ...options, redirectUri });
|
|
110
|
+
let timer;
|
|
111
|
+
let closed = false;
|
|
112
|
+
const close = () => {
|
|
113
|
+
if (closed)
|
|
114
|
+
return;
|
|
115
|
+
closed = true;
|
|
116
|
+
if (timer)
|
|
117
|
+
clearTimeout(timer);
|
|
118
|
+
server.close();
|
|
119
|
+
};
|
|
120
|
+
const html = { 'content-type': 'text/html; charset=utf-8', connection: 'close' };
|
|
121
|
+
const signedIn = new Promise((resolve, reject) => {
|
|
122
|
+
const stop = (error) => {
|
|
123
|
+
close();
|
|
124
|
+
reject(error);
|
|
125
|
+
};
|
|
126
|
+
timer = setTimeout(() => stop(new SignInError('expired_token', 'The browser didn’t come back in time.')), options.timeoutMs ?? 10 * 60_000);
|
|
127
|
+
options.signal?.addEventListener('abort', () => stop(new SignInError('aborted', 'The sign-in was stopped.')));
|
|
128
|
+
let taken = false;
|
|
129
|
+
server.on('request', (request, response) => {
|
|
130
|
+
const url = new URL(request.url ?? '/', redirectUri);
|
|
131
|
+
if (request.method !== 'GET' || url.pathname !== back.pathname) {
|
|
132
|
+
response
|
|
133
|
+
.writeHead(404, { 'content-type': 'text/plain', connection: 'close' })
|
|
134
|
+
.end('Not found');
|
|
135
|
+
return;
|
|
136
|
+
}
|
|
137
|
+
if (taken) {
|
|
138
|
+
response
|
|
139
|
+
.writeHead(409, html)
|
|
140
|
+
.end(page('Already answered', 'Close this tab and go back to the app.'));
|
|
141
|
+
return;
|
|
142
|
+
}
|
|
143
|
+
taken = true;
|
|
144
|
+
started.finish(url).then((credentials) => {
|
|
145
|
+
response
|
|
146
|
+
.writeHead(200, html)
|
|
147
|
+
.end(page('You’re signed in', 'Close this tab and go back to the app.'));
|
|
148
|
+
close();
|
|
149
|
+
resolve(credentials);
|
|
150
|
+
}, (error) => {
|
|
151
|
+
response
|
|
152
|
+
.writeHead(400, html)
|
|
153
|
+
.end(page('Not signed in', 'Go back to the app and start signing in again.'));
|
|
154
|
+
close();
|
|
155
|
+
reject(error);
|
|
156
|
+
});
|
|
157
|
+
});
|
|
158
|
+
});
|
|
159
|
+
signedIn.catch(() => { });
|
|
160
|
+
if (options.open)
|
|
161
|
+
await options.open(started.url);
|
|
162
|
+
return { url: started.url, redirectUri, signedIn, close };
|
|
163
|
+
}
|
|
90
164
|
const runFile = (command, args) => new Promise((resolve, reject) => {
|
|
91
165
|
execFile(command, args, { timeout: 60_000, maxBuffer: 64 * 1024 }, (error, stdout, stderr) => {
|
|
92
166
|
if (error)
|
package/dist/version.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export const VERSION = '0.
|
|
1
|
+
export const VERSION = '0.28.0';
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pendingyou-wake",
|
|
3
3
|
"displayName": "Pending You",
|
|
4
|
-
"version": "2.
|
|
4
|
+
"version": "2.44.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.44.0; send changes there.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pendingyou",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.28.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",
|