SLOPSHOPPER

Pending You

One place for everything your AI agents are waiting on you for. When a decision is yours, Claude asks in the conversation and on a Pending You card at once…

newguardstatusprocesstimer
v2.49.0Apache-2.0updated 2026-10-09recordplane/pendingyou-plugin/plugins/pending-you
A shopper browsing a rack in a slop shop
README

Pending You

Pending You is one place for everything your AI agents are waiting on you for. With this plugin, when a decision, a fact or a step is yours, Claude asks in the conversation and on a Pending You card at once, so you can answer wherever you are, from your phone or your desk. The first answer wins, and Claude picks it up and carries on.

What it adds

  • The Pending You connector, at https://www.pendingyou.com/mcp. You sign in once in your browser and click Allow. No key or token is stored in the plugin.
  • The pending-you skill: when a decision is yours, and how to ask so you can answer in one tap.
  • The pending-you-setup skill: say “set up Pending You” (or type /pending-you:pending-you-setup in Claude Code). It connects, sets up check-ins (a Cowork scheduled task every hour, 7 AM–9 PM), and sends you a test question.
  • Wakes Claude Code when you answer (Claude Code 2.1.287 or later): a mod that checks the cards a session posted, through its own Pending You connection, and starts a turn when you answer, write back or a fallback comes due, or you hand the session a question from another of your assistants, even while it sits idle. Under the prompt it shows “2 waiting on you”, after the plugin’s name. Allow Pending You’s tools once in /permissions (mcp__plugin_pending-you_pendingyou) so it can check while you’re away.

Set it up

  • Claude Cowork: Customize › Plugins › Add marketplace › Add from a repository: recordplane/pendingyou-plugin. Install Pending You, connect its connector (sign in, Allow), and say “set up Pending You” in a Cowork task.
  • Claude Code: one command in Terminal sets it up, with one approval in your browser: npx -y pendingyou@latest init. Then restart Claude Code, and it finishes setting up by itself. It needs Node 20 or later; without it, install the plugin: claude plugin marketplace add recordplane/pendingyou-plugin, then claude plugin install pending-you@pendingyou. Restart Claude Code, run /mcp, choose plugin:pending-you:pendingyou and Authenticate, then say “set up Pending You” (or type /pending-you:pending-you-setup).

After that, every question Claude leaves you goes in both places: the conversation and Pending You. Answer in either; Claude takes back the other. The same command sets up every coding agent it finds on your computer: Claude Code, Codex (the CLI and the Codex app), OpenCode and Pi, each with its own connection and the one approval. Cursor is set up separately: its steps are in Help.

Data

Claude sends Pending You the questions it asks you (a title, a short summary, options and any drafts it attaches) and reads back your answers, through https://www.pendingyou.com. Never passwords, keys or card numbers. Pending You never answers for you. The only model it uses reads a Later time you typed when its own reader can’t, and you see the time before it’s set.

More

  • Help: setting up each app, and what to do when an answer doesn’t arrive.
  • The guide your agents read: when they ask you, and how. This is version 2.49.0.

Generated from Pending You’s guide 2.49.0.

Source 2 files
hooks/register.js 519 lines
1// Generated from packages/claude-plugin/src/wake/register.ts in recordplane/pendingyou: edit that, then run
2// `pnpm --filter @pendingyou/claude-plugin generate`.
3
4import { argumentsOf, blockedNote, CLAIM_PREFIX, CLI_COPY, cardTool, checked, claimLease, cliConfigOf, connectionOf, dueCards, emptyBook, failed, folderOf, HANDED_FAST_MS, HANDED_READS, HERDR_EVERY_MS, HERDR_RUN_MS, handedDue, handedNext, handedOf, handedText, handedView, herdrArgv, herdrPayload, holdOf, holdReply, inHerdr, isFresh, isRefusal, isStale, isWaitingOnYou, KEEP_MS, learn, leaseHolder, loadedNote, loadedPath, momentOf, originOf, PRESENCE_EVERY_MS, PRESENCE_RUN_MS, presenceArgv, prompted, put, readBook, readClaim, readSetup, readSince, resultObject, SETUP_AFTER_MS, SETUP_RETRY_MS, SETUP_SERVER, SETUP_TRIES, settle, setUpOf, setupNeeded, setupPrompt, stateOf, statusText, TICK_MS, takesHanded, told, toTell, untold, wakeText, } from "./wake.js";
5const RETRY_MS = 60_000;
6const live = {
7    me: '',
8    sessionId: '',
9    book: emptyBook(),
10    owner: false,
11    busy: false,
12    ticking: false,
13    pruned: false,
14    quietUntil: 0,
15    timer: null,
16    shown: undefined,
17    blocked: new Set(),
18    verified: new Set(),
19    noted: new Set(),
20    setupDue: false,
21    setupConnection: 'Claude Code',
22    seenAt: 0,
23    handedAt: 0,
24    handedTold: new Map(),
25    folders: [],
26    home: undefined,
27    presence: null,
28    presenceTimer: null,
29    herdr: null,
30    herdrSaid: undefined,
31    herdrTimer: null,
32};
33const keyOf = (sessionId) => `session:${sessionId}`;
34function blockedServer() {
35    for (const server of live.blocked)
36        if (Object.values(live.book.cards).some((card) => card.server === server))
37            return server;
38    return undefined;
39}
40async function save($) {
41    live.book = { ...live.book, updatedAt: await $.clock.now() };
42    if (Object.keys(live.book.cards).length)
43        await $.store.set(keyOf(live.sessionId), live.book);
44    else
45        await $.store.delete(keyOf(live.sessionId));
46}
47async function claim($) {
48    const now = await $.clock.now();
49    const value = claimLease(await $.env.get('PENDINGYOU_WAKE_OWNER'), live.me, now);
50    if (value)
51        await $.env.set('PENDINGYOU_WAKE_OWNER', value);
52    live.owner = leaseHolder(await $.env.get('PENDINGYOU_WAKE_OWNER'), now) === live.me;
53    return live.owner;
54}
55function show($, text) {
56    if (text === live.shown)
57        return;
58    live.shown = text;
59    try {
60        const shown = $.ui.status(text);
61        if (shown instanceof Promise)
62            shown.catch(() => { });
63    }
64    catch { }
65}
66const showCards = ($) => {
67    show($, live.owner ? statusText(live.book, blockedServer()) : undefined);
68    void sayHerdr($);
69};
70async function sayHerdr($, again = false) {
71    try {
72        if (!live.herdr || !live.sessionId || !live.owner)
73            return;
74        const payload = herdrPayload(live.book);
75        if (!again && payload === live.herdrSaid)
76            return;
77        live.herdrSaid = payload;
78        const argv = herdrArgv(live.herdr.config, { id: live.sessionId, at: await $.clock.now() });
79        $.process.run(argv, { stdin: payload, timeoutMs: HERDR_RUN_MS }).catch(() => { });
80    }
81    catch { }
82}
83async function startHerdr($) {
84    live.herdrTimer?.cancel();
85    live.herdrTimer = null;
86    live.herdr = null;
87    live.herdrSaid = undefined;
88    const config = cliConfigOf($.plugin.root, $.plugin.name);
89    if (!config)
90        return;
91    const herdr = inHerdr(await $.env.get('HERDR_ENV'), await $.env.get('HERDR_BIN_PATH'), await $.env.get('HERDR_PANE_ID'));
92    if (!herdr)
93        return;
94    live.herdr = { config };
95    await sayHerdr($);
96    live.herdrTimer = $.clock.every(HERDR_EVERY_MS, () => sayHerdr($, true));
97}
98async function allowed($, server, args, tool = 'get_request') {
99    try {
100        const decided = await $.tool.check({ tool: `mcp__${server}__${tool}`, input: args });
101        return decided.decision === 'allow';
102    }
103    catch {
104        return false;
105    }
106}
107async function check($, requestId) {
108    const card = live.book.cards[requestId];
109    if (!card)
110        return 'error';
111    let output = null;
112    let blocked = false;
113    const args = { requestId, ...(card.name ? { name: card.name } : {}) };
114    if (await allowed($, card.server, args)) {
115        try {
116            output = resultObject(await $.mcp.call(card.server, 'get_request', args));
117        }
118        catch (error) {
119            blocked = isRefusal(error);
120        }
121    }
122    else
123        blocked = true;
124    const outcome = output && stateOf(output) ? 'ok' : blocked ? 'blocked' : 'error';
125    if (outcome === 'ok') {
126        live.verified.add(card.server);
127        live.blocked.delete(card.server);
128    }
129    else if (outcome === 'blocked')
130        live.blocked.add(card.server);
131    const current = live.book.cards[requestId];
132    if (!current)
133        return outcome;
134    const now = await $.clock.now();
135    const next = output && outcome === 'ok' ? checked(current, output, now) : failed(current, now, blocked);
136    live.book = put(live.book, next);
137    if (!current.state || (next.state && momentOf(current.state) !== momentOf(next.state)))
138        await save($);
139    return outcome;
140}
141async function deliver($) {
142    if (live.busy)
143        return false;
144    if (toTell(live.book).length === 0 || (await $.clock.now()) < live.quietUntil)
145        return false;
146    if (!(await claim($)))
147        return false;
148    const before = await $.clock.now();
149    for (const card of toTell(live.book))
150        if (!isFresh(card, before))
151            await check($, card.requestId);
152    if (live.busy)
153        return false;
154    const now = await $.clock.now();
155    const cards = toTell(live.book).filter((card) => isFresh(card, now));
156    if (cards.length === 0)
157        return false;
158    live.book = told(live.book, cards);
159    await save($);
160    $.prompt.submit({ text: wakeText(cards) }).catch(async () => {
161        live.book = untold(live.book, cards);
162        live.quietUntil = (await $.clock.now()) + RETRY_MS;
163        await save($).catch(() => { });
164    });
165    return true;
166}
167async function deliverHanded($) {
168    const { name, server } = live.book;
169    const now = await $.clock.now();
170    if (live.busy || !name || !server || !handedDue(live.seenAt, live.handedAt, now))
171        return;
172    live.handedAt = handedNext(live.seenAt, now);
173    if (!(await allowed($, server, { name }, 'list_pending')))
174        return;
175    let listed;
176    try {
177        listed = handedOf(resultObject(await $.mcp.call(server, 'list_pending', { name })));
178    }
179    catch {
180        return;
181    }
182    const cards = [];
183    const claims = [];
184    for (const handed of listed) {
185        const known = live.handedTold.get(handed.requestId);
186        if (readSince(known, handed.updatedAt) || known === handed.updatedAt)
187            continue;
188        if (!takesHanded(handed, live.folders, live.home, now))
189            continue;
190        const key = `${CLAIM_PREFIX}${handed.requestId}`;
191        if (readClaim(await $.store.get(key))?.moment === handed.updatedAt) {
192            live.handedTold.set(handed.requestId, handed.updatedAt);
193            continue;
194        }
195        const args = { requestId: handed.requestId, name };
196        if (!(await allowed($, server, args)))
197            continue;
198        let view = null;
199        try {
200            view = handedView(resultObject(await $.mcp.call(server, 'get_request', args)), handed, name);
201        }
202        catch { }
203        if (!view)
204            continue;
205        await $.store.set(key, { moment: handed.updatedAt, at: now });
206        live.handedTold.set(handed.requestId, handed.updatedAt);
207        claims.push(key);
208        cards.push(view);
209    }
210    if (cards.length === 0)
211        return;
212    $.prompt.submit({ text: handedText(cards) }).catch(async () => {
213        for (const key of claims)
214            await $.store.delete(key).catch(() => { });
215        for (const card of cards)
216            live.handedTold.delete(card.requestId);
217        live.handedAt = (await $.clock.now()) + RETRY_MS;
218    });
219}
220async function prune($) {
221    const now = await $.clock.now();
222    for (const key of await $.store.keys()) {
223        if (key.startsWith(CLAIM_PREFIX)) {
224            if (now - (readClaim(await $.store.get(key))?.at ?? 0) > KEEP_MS)
225                await $.store.delete(key);
226            continue;
227        }
228        if (!key.startsWith('session:') || key === keyOf(live.sessionId))
229            continue;
230        if (isStale(readBook(await $.store.get(key)), now))
231            await $.store.delete(key);
232    }
233}
234async function setupDone($) {
235    const record = readSetup(await $.store.get('setup'));
236    if (!record.done)
237        await $.store.set('setup', { ...record, done: true });
238    live.setupDue = false;
239}
240async function deliverSetup($) {
241    if (!live.setupDue || live.busy)
242        return;
243    live.setupDue = false;
244    const now = await $.clock.now();
245    const record = readSetup(await $.store.get('setup'));
246    if (!setupNeeded(record, live.sessionId, now))
247        return;
248    await $.store.set('setup', prompted(record, live.sessionId, now));
249    $.prompt.submit({ text: setupPrompt(live.setupConnection) }).catch(() => { });
250}
251async function checkSetup($, attempt) {
252    try {
253        if (!setupNeeded(readSetup(await $.store.get('setup')), live.sessionId, await $.clock.now()))
254            return;
255        const decided = await $.tool.check({ tool: `mcp__${SETUP_SERVER}__whoami`, input: {} });
256        if (decided.decision !== 'allow')
257            return;
258        let setUp = null;
259        try {
260            const output = resultObject(await $.mcp.call(SETUP_SERVER, 'whoami', {}));
261            setUp = setUpOf(output);
262            live.setupConnection = connectionOf(output);
263        }
264        catch { }
265        if (setUp === null) {
266            if (attempt < SETUP_TRIES)
267                $.clock.after(SETUP_RETRY_MS, () => checkSetup($, attempt + 1));
268            return;
269        }
270        if (setUp)
271            await setupDone($);
272        else {
273            live.setupDue = true;
274            await deliverSetup($);
275        }
276    }
277    catch { }
278}
279async function tick($) {
280    if (live.ticking || !live.sessionId)
281        return;
282    live.ticking = true;
283    try {
284        await deliverSetup($);
285        if (!live.pruned) {
286            live.pruned = true;
287            await prune($);
288        }
289        if (!(await claim($))) {
290            show($, undefined);
291            return;
292        }
293        const now = await $.clock.now();
294        const settled = settle(live.book, now);
295        if (settled !== live.book) {
296            live.book = settled;
297            await save($);
298        }
299        for (const card of dueCards(live.book, now))
300            await check($, card.requestId);
301        if (!(await deliver($)))
302            await deliverHanded($);
303        showCards($);
304    }
305    catch {
306    }
307    finally {
308        live.ticking = false;
309    }
310}
311function soon($) {
312    try {
313        $.clock.after(1500, () => tick($));
314    }
315    catch { }
316}
317function freshHanded() {
318    live.seenAt = Object.keys(live.book.cards).length ? live.book.updatedAt : 0;
319    live.handedAt = 0;
320    live.handedTold = new Map();
321}
322async function folderOfSession($, cwd) {
323    live.folders = typeof cwd === 'string' && cwd ? [cwd] : [];
324    try {
325        live.home = (await $.env.get('HOME')) || undefined;
326    }
327    catch {
328        live.home = undefined;
329    }
330}
331async function noteLoaded($) {
332    try {
333        const config = cliConfigOf($.plugin.root, $.plugin.name);
334        const path = config && live.sessionId ? loadedPath(config, live.sessionId) : null;
335        if (path)
336            await $.fs.write(path, loadedNote(await $.clock.now()));
337    }
338    catch { }
339}
340async function sayLive($) {
341    try {
342        const presence = live.presence;
343        if (!presence || !live.sessionId)
344            return;
345        const argv = presenceArgv(presence.config, presence.origin, {
346            id: live.sessionId,
347            ...(live.folders[0] ? { cwd: live.folders[0] } : {}),
348            ...(live.book.name ? { name: live.book.name } : {}),
349            at: await $.clock.now(),
350        });
351        $.process.run(argv, { timeoutMs: PRESENCE_RUN_MS }).catch(() => { });
352    }
353    catch { }
354}
355async function startPresence($) {
356    live.presenceTimer?.cancel();
357    live.presenceTimer = null;
358    live.presence = null;
359    const config = cliConfigOf($.plugin.root, $.plugin.name);
360    if (!config)
361        return;
362    let manifest = null;
363    try {
364        manifest = await $.fs.read(`${config}/claude-code.json`);
365    }
366    catch { }
367    const origin = originOf(manifest);
368    if (!origin)
369        return;
370    live.presence = { config, origin };
371    await sayLive($);
372    live.presenceTimer = $.clock.every(PRESENCE_EVERY_MS, () => sayLive($));
373}
374async function start($, e) {
375    live.me = $.plugin.name;
376    live.sessionId = await $.session.id();
377    live.book = readBook(await $.store.get(keyOf(live.sessionId)));
378    freshHanded();
379    await folderOfSession($, e.cwd);
380    await noteLoaded($);
381    await claim($);
382    live.timer?.cancel();
383    live.timer = $.clock.every(TICK_MS, () => tick($));
384    soon($);
385    await startPresence($).catch(() => { });
386    await startHerdr($).catch(() => { });
387    if ($.plugin.name === CLI_COPY && e.isInteractive !== false)
388        $.clock.after(SETUP_AFTER_MS, () => checkSetup($, 1));
389}
390async function switchSession($, e) {
391    const id = typeof e.session_id === 'string' && e.session_id ? e.session_id : await $.session.id();
392    if (!id || id === live.sessionId)
393        return;
394    const carried = e.source === 'fork' ? live.book : null;
395    live.sessionId = id;
396    live.book = carried ?? readBook(await $.store.get(keyOf(id)));
397    live.noted.clear();
398    freshHanded();
399    await noteLoaded($);
400    if (typeof e.cwd === 'string' && e.cwd)
401        await folderOfSession($, e.cwd);
402    if (carried)
403        await save($);
404    live.herdrSaid = undefined;
405    showCards($);
406    await sayLive($);
407}
408async function observe($, e, result) {
409    const found = cardTool(e.tool);
410    if (!found || !live.sessionId)
411        return result;
412    const output = resultObject(result);
413    if ($.plugin.name === CLI_COPY && found.tool === 'whoami' && setUpOf(output) === true)
414        await setupDone($);
415    const now = await $.clock.now();
416    if (output) {
417        live.seenAt = now;
418        live.handedAt = Math.min(live.handedAt, now + HANDED_FAST_MS);
419    }
420    const read = e.requestId ?? output?.requestId;
421    if (HANDED_READS.has(found.tool) && typeof read === 'string')
422        live.handedTold.set(read, `*${now}`);
423    const folder = output ? folderOf(found.tool, argumentsOf(e)) : undefined;
424    if (folder && !live.folders.includes(folder))
425        live.folders = [...live.folders, folder].slice(-5);
426    const before = live.book;
427    live.book = learn(live.book, { ...found, input: argumentsOf(e), output }, now);
428    if (live.book === before)
429        return result;
430    await save($);
431    const id = output?.requestId ?? e.requestId;
432    const card = typeof id === 'string' ? live.book.cards[id] : undefined;
433    if (live.owner && card?.state && isWaitingOnYou(card.state) && !live.verified.has(card.server)) {
434        const outcome = await check($, card.requestId);
435        if (outcome === 'blocked' && !live.noted.has(card.server)) {
436            live.noted.add(card.server);
437            showCards($);
438            return { ...result, context: [...(result.context ?? []), blockedNote(card.server)] };
439        }
440    }
441    showCards($);
442    return result;
443}
444async function answerHold($, e) {
445    const requestId = typeof e.command === 'string' ? holdOf(e.command) : null;
446    if (!requestId || !live.sessionId)
447        return null;
448    const known = live.book.cards[requestId];
449    const server = known?.server ?? live.book.server;
450    if (!server)
451        return null;
452    if (!known) {
453        const now = await $.clock.now();
454        const name = live.book.name;
455        live.book = put(live.book, {
456            requestId,
457            server,
458            ...(name ? { name } : {}),
459            learnedAt: now,
460            changedAt: now,
461            nextAt: now,
462        });
463        await save($);
464    }
465    if (!(await claim($)))
466        return null;
467    if ((await check($, requestId)) !== 'ok')
468        return null;
469    showCards($);
470    return { result: { stdout: holdReply(requestId), stderr: '', interrupted: false } };
471}
472export function register(on) {
473    on('session.start', async ($, e, next) => {
474        try {
475            await start($, e);
476        }
477        catch { }
478        return next(e);
479    });
480    on('classic.SessionStart', { source: ['clear', 'resume', 'fork'] }, async ($, e, next) => {
481        try {
482            await switchSession($, e);
483        }
484        catch { }
485        return next(e);
486    });
487    on('turn.start', async (_, e, next) => {
488        if (!e.agentId)
489            live.busy = true;
490        return next(e);
491    });
492    on('turn.complete', async ($, e, next) => {
493        if (!e.agentId) {
494            live.busy = false;
495            soon($);
496        }
497        return next(e);
498    });
499    on('tool.call', {
500        tool: /^mcp__.+__(post_request|get_request|update_request|reply_in_thread|ack_answer|cancel_request|whoami|list_pending|answer_delegated|hand_back)$/,
501    }, async ($, e, next) => {
502        const result = await next(e);
503        try {
504            return await observe($, e, result);
505        }
506        catch {
507            return result;
508        }
509    });
510    on('tool.call', { tool: 'Bash' }, async ($, e, next) => {
511        let answer = null;
512        try {
513            answer = await answerHold($, e);
514        }
515        catch { }
516        return answer ?? next(e);
517    });
518}
519
hooks/wake.js 861 lines
1// Generated from packages/claude-plugin/src/wake/wake.ts in recordplane/pendingyou: edit that, then run
2// `pnpm --filter @pendingyou/claude-plugin generate`.
3
4export const TICK_MS = 5000;
5export const FAST_MS = 20_000;
6export const SLOW_MS = 60_000;
7export const RECENT_MS = 30 * 60_000;
8export const MAX_BACKOFF_MS = 15 * 60_000;
9export const HINT_FLOOR_MS = 5000;
10export const BLOCKED_MS = 60_000;
11export const FRESH_MS = 10_000;
12export const KEEP_MS = 7 * 24 * 60 * 60_000;
13export const MAX_CARDS = 50;
14export const LEASE_MS = 60_000;
15export const RENEW_MS = 20_000;
16export const TITLE_CHARS = 80;
17const REQUEST_ID = /^req_[A-Za-z0-9-]{1,40}$/;
18const OPEN = new Set(['pending', 'snoozed', 'delegated']);
19const CLOSED = new Set(['resolved', 'cancelled', 'expired']);
20const URGENCIES = new Set(['now', 'today', 'whenever']);
21const isUrgency = (value) => typeof value === 'string' && URGENCIES.has(value);
22const READS = new Set([
23    'post_request',
24    'get_request',
25    'update_request',
26    'reply_in_thread',
27    'ack_answer',
28    'cancel_request',
29    'whoami',
30    'list_pending',
31    'answer_delegated',
32    'hand_back',
33]);
34export const HANDED_READS = new Set(['get_request', 'answer_delegated', 'hand_back']);
35export const APPROVED_DRAFT_NOTICE = 'The approved words are in get_request’s answer.approvedDrafts: send them exactly.';
36const isObject = (value) => typeof value === 'object' && value !== null && !Array.isArray(value);
37const parseJson = (text) => {
38    try {
39        return JSON.parse(text);
40    }
41    catch {
42        return undefined;
43    }
44};
45const text = (value, max = 200) => typeof value === 'string' && value.trim() && value.length <= max ? value.trim() : undefined;
46const count = (value) => typeof value === 'number' && Number.isFinite(value) ? value : undefined;
47const requestIdOf = (value) => typeof value === 'string' && REQUEST_ID.test(value) ? value : undefined;
48export function clip(title) {
49    const flat = title.replace(/\s+/g, ' ').trim().replaceAll('“', '‘').replaceAll('”', '’');
50    const chars = [...flat];
51    return chars.length <= TITLE_CHARS
52        ? flat
53        : `${chars
54            .slice(0, TITLE_CHARS - 1)
55            .join('')
56            .trimEnd()}…`;
57}
58export const isPendingYou = (server) => server
59    .toLowerCase()
60    .replace(/[^a-z0-9]/g, '')
61    .includes('pendingyou');
62export function cardTool(name) {
63    const match = /^mcp__(.+)__([a-z_]+)$/.exec(name);
64    if (!match)
65        return null;
66    const [, server = '', tool = ''] = match;
67    return READS.has(tool) && isPendingYou(server) ? { server, tool } : null;
68}
69export function resultObject(result) {
70    if (!isObject(result) || result.deny !== undefined || result.isError === true)
71        return null;
72    if (isObject(result.structuredContent))
73        return result.structuredContent;
74    const texts = [];
75    const blocks = (value) => {
76        if (Array.isArray(value))
77            for (const block of value)
78                if (isObject(block) && typeof block.text === 'string')
79                    texts.push(block.text);
80    };
81    if (typeof result.text === 'string')
82        texts.push(result.text);
83    if (typeof result.result === 'string')
84        texts.push(result.result);
85    else if (isObject(result.result)) {
86        if (isObject(result.result.structuredContent))
87            return result.result.structuredContent;
88        blocks(result.result.content);
89    }
90    else
91        blocks(result.result);
92    blocks(result.content);
93    for (const each of texts) {
94        const parsed = parseJson(each);
95        if (isObject(parsed))
96            return parsed;
97    }
98    return null;
99}
100export function nameOf(tool, input) {
101    const name = text(input.name, 120);
102    if (name || tool !== 'post_request')
103        return name;
104    const session = typeof input.session === 'string' ? parseJson(input.session) : input.session;
105    return isObject(session) ? text(session.label, 120) : undefined;
106}
107export function argumentsOf(call) {
108    const { tool: _tool, tool_use_id: _id, agentId: _agent, consent: _consent, ...rest } = call;
109    return rest;
110}
111export const isClosed = (status) => CLOSED.has(status);
112export const isReady = (state) => state.turn === 'agent' || isClosed(state.status);
113export const isWaitingOnYou = (state) => state.turn === 'you' && OPEN.has(state.status);
114export const momentOf = (state) => `${state.status}/${state.turn}/${state.version}/${state.cursor}`;
115export function stateOf(output) {
116    const status = text(output.status, 40);
117    const turn = output.turn === 'you' || output.turn === 'agent' ? output.turn : undefined;
118    const version = count(output.version);
119    if (!status || !turn || version === undefined)
120        return null;
121    const given = isObject(output.answer) ? output.answer.handled : output.handled;
122    const handled = given === 'self' || given === 'leave' ? given : undefined;
123    const approvedDraft = isObject(output.answer)
124        ? Array.isArray(output.answer.approvedDrafts) && output.answer.approvedDrafts.length > 0
125        : output.approvedDraft === true;
126    return {
127        status,
128        turn,
129        version,
130        cursor: typeof output.cursor === 'string' ? output.cursor : '',
131        ...(handled ? { handled } : {}),
132        ...(approvedDraft ? { approvedDraft: true } : {}),
133    };
134}
135export const emptyBook = (now = 0) => ({ v: 1, updatedAt: now, cards: {} });
136export function readBook(value) {
137    if (!isObject(value) || value.v !== 1 || !isObject(value.cards))
138        return emptyBook();
139    const cards = {};
140    for (const [requestId, card] of Object.entries(value.cards)) {
141        if (!isObject(card) || card.requestId !== requestId || !requestIdOf(requestId))
142            continue;
143        if (typeof card.server !== 'string' || !isPendingYou(card.server))
144            continue;
145        const learnedAt = count(card.learnedAt);
146        const changedAt = count(card.changedAt);
147        if (learnedAt === undefined || changedAt === undefined)
148            continue;
149        const state = isObject(card.state) ? stateOf(card.state) : null;
150        cards[requestId] = {
151            requestId,
152            server: card.server,
153            learnedAt,
154            changedAt,
155            ...(text(card.name, 120) ? { name: text(card.name, 120) } : {}),
156            ...(text(card.title, 400) ? { title: text(card.title, 400) } : {}),
157            ...(isUrgency(card.urgency) ? { urgency: card.urgency } : {}),
158            ...(card.blocking === true ? { blocking: true } : {}),
159            ...(card.askedFirst === true ? { askedFirst: true } : {}),
160            ...(state ? { state } : {}),
161            ...(typeof card.told === 'string' ? { told: card.told } : {}),
162            ...(typeof card.seen === 'string' ? { seen: card.seen } : {}),
163            ...(count(card.failures) ? { failures: count(card.failures) } : {}),
164            ...(count(card.nextAt) !== undefined ? { nextAt: count(card.nextAt) } : {}),
165        };
166    }
167    return {
168        v: 1,
169        updatedAt: count(value.updatedAt) ?? 0,
170        ...(text(value.name, 120) ? { name: text(value.name, 120) } : {}),
171        ...(typeof value.server === 'string' && isPendingYou(value.server)
172            ? { server: value.server }
173            : {}),
174        cards,
175    };
176}
177const listening = (card) => {
178    const state = card.state;
179    if (!state || isWaitingOnYou(state))
180        return true;
181    const moment = momentOf(state);
182    return moment !== card.told && moment !== card.seen;
183};
184export function put(book, card) {
185    const cards = { ...book.cards, [card.requestId]: card };
186    const all = Object.values(cards);
187    if (all.length > MAX_CARDS)
188        for (const old of all
189            .sort((a, b) => Number(listening(a)) - Number(listening(b)) || a.changedAt - b.changedAt)
190            .slice(0, all.length - MAX_CARDS))
191            delete cards[old.requestId];
192    return { ...book, cards };
193}
194export function drop(book, requestId) {
195    if (!book.cards[requestId])
196        return book;
197    const { [requestId]: _gone, ...cards } = book.cards;
198    return { ...book, cards };
199}
200const pressingOfCard = (card) => ({
201    ...(card?.urgency ? { urgency: card.urgency } : {}),
202    ...(card?.blocking ? { blocking: true } : {}),
203    ...(card?.askedFirst ? { askedFirst: true } : {}),
204});
205function askedFirstIn(input) {
206    const asked = typeof input.askedFirst === 'string' ? parseJson(input.askedFirst) : input.askedFirst;
207    return isObject(asked);
208}
209function pressingFrom(tool, input, card) {
210    const urgency = isUrgency(input.urgency) ? input.urgency : card?.urgency;
211    const blocking = typeof input.blocking === 'boolean' ? input.blocking : card?.blocking === true;
212    const askedFirst = tool === 'post_request' ? askedFirstIn(input) : card?.askedFirst === true;
213    return {
214        ...(urgency ? { urgency } : {}),
215        ...(blocking ? { blocking: true } : {}),
216        ...(askedFirst ? { askedFirst: true } : {}),
217    };
218}
219function tracked(book, facts, now) {
220    const card = book.cards[facts.requestId];
221    const name = facts.name ?? card?.name;
222    const title = facts.title ?? card?.title;
223    return put(book, {
224        requestId: facts.requestId,
225        server: facts.server,
226        ...(name ? { name } : {}),
227        ...(title ? { title } : {}),
228        ...(facts.pressing ?? pressingOfCard(card)),
229        learnedAt: card?.learnedAt ?? now,
230        changedAt: now,
231        state: facts.state,
232        nextAt: now + FAST_MS,
233    });
234}
235function followedUp(book, follows, now) {
236    const card = follows ? book.cards[follows] : undefined;
237    if (!card)
238        return book;
239    const heard = card.state && isReady(card.state) ? { seen: momentOf(card.state) } : {};
240    return put(book, { ...card, ...heard, nextAt: now });
241}
242export function learn(book, call, now) {
243    const { server, tool, input, output } = call;
244    if (!output)
245        return book;
246    const name = nameOf(tool, input);
247    let next = book;
248    if (name && (book.name !== name || book.server !== server))
249        next = { ...next, name, server };
250    const requestId = requestIdOf(output.requestId) ?? requestIdOf(input.requestId);
251    if (!requestId)
252        return next;
253    const card = next.cards[requestId];
254    const version = count(output.version);
255    switch (tool) {
256        case 'post_request':
257        case 'update_request': {
258            const status = text(output.status, 40);
259            if (!status || !OPEN.has(status) || version === undefined)
260                return next;
261            const title = text(input.title, 400);
262            const posted = tracked(next, {
263                requestId,
264                server,
265                ...(name ? { name } : {}),
266                ...(title ? { title: clip(title) } : {}),
267                pressing: pressingFrom(tool, input, card),
268                state: { status, turn: 'you', version, cursor: card?.state?.cursor ?? '' },
269            }, now);
270            return tool === 'post_request' ? followedUp(posted, requestIdOf(input.follows), now) : posted;
271        }
272        case 'reply_in_thread': {
273            const reopen = isObject(input.reopen) ? input.reopen : null;
274            if (version === undefined || (!reopen && output.turn !== 'you'))
275                return next;
276            const title = reopen ? text(reopen.title, 400) : undefined;
277            const status = reopen ? 'pending' : (card?.state?.status ?? 'pending');
278            const cursor = typeof output.messageId === 'string' ? output.messageId : '';
279            return tracked(next, {
280                requestId,
281                server,
282                ...(name ? { name } : {}),
283                ...(title ? { title: clip(title) } : {}),
284                state: { status: OPEN.has(status) ? status : 'pending', turn: 'you', version, cursor },
285            }, now);
286        }
287        case 'get_request': {
288            const state = stateOf(output);
289            if (!state)
290                return next;
291            if (!card)
292                return isWaitingOnYou(state)
293                    ? tracked(next, { requestId, server, ...(name ? { name } : {}), state }, now)
294                    : next;
295            const moment = momentOf(state);
296            const changed = !card.state || momentOf(card.state) !== moment;
297            return put(next, {
298                ...card,
299                state,
300                ...(changed ? { changedAt: now } : {}),
301                ...(isReady(state) ? { seen: moment } : {}),
302            });
303        }
304        case 'ack_answer':
305            return output.status === 'resolved' ? drop(next, requestId) : next;
306        case 'cancel_request':
307            return output.status === 'cancelled' ? drop(next, requestId) : next;
308        default:
309            return next;
310    }
311}
312export function checked(card, output, now) {
313    const state = stateOf(output) ?? card.state;
314    if (!state)
315        return failed(card, now, false);
316    const changed = !card.state || momentOf(card.state) !== momentOf(state);
317    const changedAt = changed ? now : card.changedAt;
318    let wait = now - changedAt < RECENT_MS ? FAST_MS : SLOW_MS;
319    const pollAfter = count(output.pollAfterSeconds);
320    if (!isReady(state) && pollAfter !== undefined && pollAfter * 1000 < wait)
321        wait = Math.max(HINT_FLOOR_MS, pollAfter * 1000 + 1000);
322    const { failures: _failures, ...rest } = card;
323    return { ...rest, state, changedAt, nextAt: now + wait, checkedAt: now };
324}
325export function failed(card, now, blocked) {
326    if (blocked)
327        return { ...card, nextAt: now + BLOCKED_MS };
328    const failures = (card.failures ?? 0) + 1;
329    return {
330        ...card,
331        failures,
332        nextAt: now + Math.min(MAX_BACKOFF_MS, FAST_MS * 2 ** (failures - 1)),
333    };
334}
335export function dueCards(book, now) {
336    return Object.values(book.cards)
337        .filter((card) => !(card.state && isClosed(card.state.status)) && (card.nextAt ?? 0) <= now)
338        .sort((a, b) => (a.nextAt ?? 0) - (b.nextAt ?? 0));
339}
340export function needsTelling(card) {
341    const state = card.state;
342    if (!state || !isReady(state))
343        return false;
344    if (state.status === 'resolved')
345        return false;
346    const moment = momentOf(state);
347    if (moment === card.told || moment === card.seen)
348        return false;
349    if (state.status === 'cancelled' && (card.told || card.seen))
350        return false;
351    return true;
352}
353export const isFresh = (card, now) => card.checkedAt !== undefined && now - card.checkedAt <= FRESH_MS;
354export const toTell = (book) => Object.values(book.cards)
355    .filter(needsTelling)
356    .sort((a, b) => a.changedAt - b.changedAt);
357export function told(book, cards) {
358    let next = book;
359    for (const card of cards) {
360        const current = next.cards[card.requestId];
361        if (current?.state)
362            next = put(next, { ...current, told: momentOf(current.state) });
363    }
364    return next;
365}
366export function untold(book, before) {
367    let next = book;
368    for (const card of before) {
369        const current = next.cards[card.requestId];
370        if (!current?.state || current.told !== momentOf(current.state))
371            continue;
372        const { told: _told, ...rest } = current;
373        next = put(next, card.told === undefined ? rest : { ...rest, told: card.told });
374    }
375    return next;
376}
377export function settle(book, now) {
378    let next = book;
379    for (const card of Object.values(book.cards)) {
380        const done = card.state && isClosed(card.state.status) && !needsTelling(card);
381        if (done || now - Math.max(card.learnedAt, card.changedAt) > KEEP_MS)
382            next = drop(next, card.requestId);
383    }
384    return next;
385}
386export const isStale = (book, now) => now - book.updatedAt > KEEP_MS;
387const titled = (card) => card.title ? `“${clip(card.title)}” (${card.requestId})` : card.requestId;
388const named = (card) => (card.name ? `name “${card.name}”` : 'your name');
389function line(card) {
390    const state = card.state;
391    const get = `get_request (requestId ${card.requestId}, ${named(card)})`;
392    switch (state.status) {
393        case 'expired':
394            return [
395                `nobody answered ${titled(card)} in time, so its fallback is due`,
396                'do it now if you haven’t already; there’s nothing to acknowledge',
397            ];
398        case 'cancelled':
399            return [`${titled(card)} was withdrawn`, 'nothing to do'];
400        case 'answered':
401            if (state.handled === 'self')
402                return [
403                    `your person will handle ${titled(card)} themselves`,
404                    `call ${get} to read it, then close it out: take no action on it, leave things as they are and don’t follow up; ack_answer with its version and an outcome like “Left to you”`,
405                ];
406            if (state.handled === 'leave')
407                return [
408                    `your person won’t do ${titled(card)}`,
409                    `call ${get} to read it, then close it out: don’t do it for them and don’t follow up; ack_answer with its version and an outcome like “Skipped”`,
410                ];
411            return [
412                `your person answered ${titled(card)}`,
413                `call ${get}, act on their words, then ack_answer with its version and a one-line outcome${state.approvedDraft ? `. ${APPROVED_DRAFT_NOTICE.slice(0, -1)}` : ''}`,
414            ];
415        case 'delegated':
416            return [
417                `there’s word on ${titled(card)}, which your person handed to another assistant`,
418                `call ${get} and do what its helper.next says`,
419            ];
420        default:
421            return [
422                `your person wrote to you on ${titled(card)}`,
423                `call ${get} to read it, then answer with reply_in_thread`,
424            ];
425    }
426}
427const capital = (sentence) => sentence.charAt(0).toUpperCase() + sentence.slice(1);
428export function wakeText(cards) {
429    const asked = cards.some((card) => card.state?.turn === 'agent' &&
430        !isClosed(card.state.status) &&
431        card.state.status !== 'delegated' &&
432        !card.state.handled);
433    const reopen = asked
434        ? ' If you need more from them, or the next step, post a new card with follows: that card’s requestId; never reopen.'
435        : '';
436    if (cards.length === 1) {
437        const [happened, todo] = line(cards[0]);
438        return `Pending You: ${happened}. ${capital(todo)}.${reopen}`;
439    }
440    return [
441        `Pending You: ${cards.length} of your cards need you.`,
442        ...cards.map((card) => {
443            const [happened, todo] = line(card);
444            return `- ${capital(happened)}: ${todo}.`;
445        }),
446        ...(reopen ? [reopen.trim()] : []),
447    ].join('\n');
448}
449export function statusText(book, blocked) {
450    if (blocked)
451        return `to hear answers here, allow mcp__${blocked} in /permissions`;
452    const waiting = Object.values(book.cards).filter((card) => card.state && isWaitingOnYou(card.state)).length;
453    return waiting ? `${waiting} waiting on you` : undefined;
454}
455export const blockedNote = (server) => `Pending You can’t wake this session when your person answers: Claude Code doesn’t let it check your cards yet. Ask them to allow mcp__${server} in /permissions (Allow, user settings). Until then, check get_request at breakpoints.`;
456export const isRefusal = (error) => /refused|permission|not allowed|haven.t granted/i.test(error instanceof Error ? error.message : String(error));
457export function shellWords(command) {
458    const words = [];
459    let word = '';
460    let inWord = false;
461    let quote = null;
462    for (let index = 0; index < command.length; index++) {
463        const char = command[index];
464        if (quote === "'") {
465            if (char === "'")
466                quote = null;
467            else
468                word += char;
469            continue;
470        }
471        if (quote === '"') {
472            if (char === '"')
473                quote = null;
474            else if (char === '$' || char === '`')
475                return null;
476            else if (char === '\\' && '"\\$`'.includes(command[index + 1] ?? ''))
477                word += command[++index];
478            else
479                word += char;
480            continue;
481        }
482        if (char === "'" || char === '"') {
483            quote = char;
484            inWord = true;
485        }
486        else if (char === '\\') {
487            const escaped = command[++index];
488            if (escaped === undefined || escaped === '\n')
489                return null;
490            word += escaped;
491            inWord = true;
492        }
493        else if (char === ' ' || char === '\t') {
494            if (inWord)
495                words.push(word);
496            word = '';
497            inWord = false;
498        }
499        else if (char === '#' && !inWord)
500            break;
501        else if (';&|<>()`$\n\r'.includes(char))
502            return null;
503        else {
504            word += char;
505            inWord = true;
506        }
507    }
508    if (quote)
509        return null;
510    if (inWord)
511        words.push(word);
512    return words;
513}
514const ASSIGNMENT = /^[A-Za-z_][A-Za-z0-9_]*=/;
515const PACKAGE = /^pendingyou(@[\w.^~=<>*-]+)?$/;
516const NPX_FLAGS = new Set(['-y', '--yes', '-q', '--quiet', '--prefer-offline', '--prefer-online']);
517const OPTION = /^--(origin|timeout)=\S+$/;
518export function holdOf(command) {
519    const words = shellWords(command.trim());
520    if (!words?.length)
521        return null;
522    let index = 0;
523    const at = () => words[index] ?? '';
524    if (at() === 'env')
525        index++;
526    while (ASSIGNMENT.test(at()))
527        index++;
528    if (at() === 'npx') {
529        index++;
530        while (NPX_FLAGS.has(at()))
531            index++;
532        if (!PACKAGE.test(at()))
533            return null;
534        index++;
535    }
536    else if (at() === 'pnpm' && words[index + 1] === 'dlx') {
537        index += 2;
538        if (!PACKAGE.test(at()))
539            return null;
540        index++;
541    }
542    else if (at() === 'pendingyou')
543        index++;
544    else {
545        if (/(^|\/)node$/.test(at()))
546            index++;
547        if (!/(^|\/)pendingyou\/dist\/cli\.js$/.test(at()))
548            return null;
549        index++;
550    }
551    let requestId = null;
552    let held = false;
553    while (index < words.length) {
554        const word = at();
555        if (OPTION.test(word))
556            index++;
557        else if (word === '--origin' || word === '--timeout') {
558            if (index + 1 >= words.length)
559                return null;
560            index += 2;
561        }
562        else if (!held && word === 'hold') {
563            held = true;
564            index++;
565        }
566        else if (held && !requestId && REQUEST_ID.test(word)) {
567            requestId = word;
568            index++;
569        }
570        else
571            return null;
572    }
573    return held ? requestId : null;
574}
575export const holdReply = (requestId) => `Pending You: no hold is needed in this session. It’s woken when your person answers ${requestId}, writes to you on it, or its fallback runs, even while it’s idle. Nothing is running in the background and there’s nothing to wait on: keep working, or end your turn.`;
576function leaseOf(value) {
577    const match = /^(\S+) (\d+)$/.exec(value ?? '');
578    return match ? { holder: match[1], until: Number(match[2]) } : null;
579}
580export function leaseHolder(value, now) {
581    const lease = leaseOf(value);
582    return lease && lease.until > now ? lease.holder : null;
583}
584export function claimLease(value, me, now) {
585    const lease = leaseOf(value);
586    if (lease && lease.until > now && lease.holder !== me)
587        return null;
588    if (lease && lease.holder === me && lease.until - now > LEASE_MS - RENEW_MS)
589        return null;
590    return `${me} ${now + LEASE_MS}`;
591}
592export const HANDED_FAST_MS = 60_000;
593export const HANDED_SLOW_MS = 3 * 60_000;
594export const HANDED_FOR_MS = 12 * 60 * 60_000;
595export const HANDED_GRACE_MS = 5 * 60_000;
596export const CLAIM_PREFIX = 'handed:';
597export const HANDED_PREFIX = 'Pending You: your person handed you';
598export const HANDED_REPLY = 'on the question handed to you';
599export const HANDED_TALK_TODO = 'If you know now, answer_delegated with your answer and how you know; if you need more, reply_in_thread to “asker”; if it’s your person’s to decide, reply_in_thread with escalate true; if not, hand_back with what you checked.';
600const SAID_CHARS = 300;
601export const HANDED_TODO = 'If you know, answer_delegated with your answer and how you know; if not, hand_back with what you checked.';
602const NAME_CHARS = 60;
603export function latestSaid(talk, from) {
604    if (!Array.isArray(talk))
605        return null;
606    const said = talk.filter(isObject);
607    const mine = said.findLastIndex((each) => each.from === 'helper');
608    const last = said.slice(mine + 1).at(-1);
609    const words = last ? text(last.body, 4000) : undefined;
610    if (!last || !words || (last.from !== 'asker' && last.from !== 'you'))
611        return null;
612    return { who: last.from === 'you' ? 'your person' : from, words };
613}
614export function readSince(known, updatedAt) {
615    if (!known?.startsWith('*'))
616        return false;
617    const at = Number(known.slice(1));
618    return !Number.isFinite(at) || at === 0 || Date.parse(updatedAt) <= at;
619}
620export const handedDue = (seenAt, nextAt, now) => seenAt > 0 && now - seenAt < HANDED_FOR_MS && now >= nextAt;
621export const handedNext = (seenAt, now) => now + (now - seenAt < RECENT_MS ? HANDED_FAST_MS : HANDED_SLOW_MS);
622export function handedOf(output) {
623    const requests = output && Array.isArray(output.requests) ? output.requests : [];
624    return requests.flatMap((request) => {
625        if (!isObject(request) || request.delegated !== true)
626            return [];
627        if (request.status !== 'delegated' || request.turn !== 'agent')
628            return [];
629        const requestId = requestIdOf(request.id);
630        const title = text(request.title, 400);
631        const updatedAt = text(request.updatedAt, 40);
632        const cwd = text(request.cwd, 1000);
633        return requestId && title && updatedAt
634            ? [
635                {
636                    requestId,
637                    title,
638                    updatedAt,
639                    ...(cwd ? { cwd } : {}),
640                    ...(request.byName === true ? { byName: true } : {}),
641                },
642            ]
643            : [];
644    });
645}
646export function folderKey(path, home) {
647    const given = path
648        .trim()
649        .replaceAll('\\', '/')
650        .replace(/\/{2,}/g, '/');
651    const base = home?.replace(/\/+$/, '');
652    const full = base && given === '~'
653        ? base
654        : base && given.startsWith('~/')
655            ? `${base}${given.slice(1)}`
656            : given;
657    return (full.replace(/\/+$/, '') || '/').toLowerCase();
658}
659export function inTaskFolder(folders, task, home) {
660    const there = folderKey(task, home);
661    return folders.some((folder) => {
662        const here = folderKey(folder, home);
663        return here === there || here.startsWith(there === '/' ? '/' : `${there}/`);
664    });
665}
666export function takesHanded(handed, folders, home, now) {
667    if (handed.byName || !handed.cwd)
668        return true;
669    if (folders.length === 0)
670        return now - Date.parse(handed.updatedAt) >= HANDED_GRACE_MS;
671    return inTaskFolder(folders, handed.cwd, home);
672}
673export function folderOf(tool, input) {
674    if (tool !== 'post_request')
675        return undefined;
676    const session = typeof input.session === 'string' ? parseJson(input.session) : input.session;
677    return isObject(session) ? text(session.cwd, 1000) : undefined;
678}
679export function handedView(output, handed, name) {
680    const delegated = output && isObject(output.delegated) ? output.delegated : null;
681    if (!delegated || output?.status !== 'delegated' || output.turn !== 'agent')
682        return null;
683    const from = text(delegated.from, 200);
684    const mode = delegated.mode === 'freely' || delegated.mode === 'loop' ? delegated.mode : null;
685    if (!from || !mode)
686        return null;
687    const said = latestSaid(delegated.talk, from);
688    return {
689        requestId: handed.requestId,
690        title: text(delegated.title, 400) ?? handed.title,
691        from,
692        mode,
693        note: typeof delegated.note === 'string' && delegated.note.trim() !== '',
694        name,
695        ...(said ? { said } : {}),
696    };
697}
698export function readClaim(value) {
699    if (!isObject(value) || typeof value.moment !== 'string')
700        return null;
701    return { moment: value.moment, at: count(value.at) ?? 0 };
702}
703function cut(value, max) {
704    const chars = [...value.replace(/\s+/g, ' ').trim()];
705    return chars.length <= max
706        ? chars.join('')
707        : `${chars
708            .slice(0, max - 1)
709            .join('')
710            .trimEnd()}…`;
711}
712const handedTitled = (card) => `“${clip(card.title)}” (${card.requestId})`;
713function asked(card) {
714    const from = cut(card.from, NAME_CHARS);
715    return `from ${from} (${card.mode === 'freely' ? `answer freely: your answer goes straight to ${from}` : 'they’ll see your answer first'})`;
716}
717const readIt = (card) => `get_request (requestId ${card.requestId}, name “${card.name}”) to read it${card.note ? ' and their note' : ''}`;
718export function handedText(cards) {
719    const quotedSaid = (card) => card.said
720        ? `: “${cut(card.said.words, SAID_CHARS).replaceAll('“', '‘').replaceAll('”', '’')}”`
721        : '';
722    const replied = (said) => said.who === 'your person'
723        ? 'your person wrote to you both'
724        : `${cut(said.who, NAME_CHARS)} replied`;
725    const readTalk = (card) => `get_request (requestId ${card.requestId}, name “${card.name}”) to read what you’ve said to each other`;
726    if (cards.length === 1) {
727        const card = cards[0];
728        if (card.said)
729            return `Pending You: ${replied(card.said)} ${HANDED_REPLY}, ${handedTitled(card)}${quotedSaid(card)}. Call ${readTalk(card)}. ${HANDED_TALK_TODO}`;
730        return `${HANDED_PREFIX} ${handedTitled(card)}, a question ${asked(card)}. Call ${readIt(card)}. ${HANDED_TODO}`;
731    }
732    return [
733        `${HANDED_PREFIX} ${cards.length} questions from other assistants.`,
734        ...cards.map((card) => card.said
735            ? `- ${handedTitled(card)}, ${asked(card)}; ${replied(card.said)}${quotedSaid(card)}: call ${readTalk(card)}.`
736            : `- ${handedTitled(card)}, ${asked(card)}: call ${readIt(card)}.`),
737        `For each one: ${HANDED_TODO.charAt(0).toLowerCase()}${HANDED_TODO.slice(1)}${cards.some((card) => card.said) ? ' Where one replied, you may also reply_in_thread to “asker”, or escalate.' : ''}`,
738    ].join('\n');
739}
740export const CLI_COPY = 'pendingyou-wake';
741export const SETUP_SERVER = 'pendingyou';
742export const SETUP_AFTER_MS = 3000;
743export const SETUP_RETRY_MS = 10_000;
744export const SETUP_TRIES = 3;
745export const SETUP_SESSIONS = 3;
746export const SETUP_QUIET_MS = 10 * 60_000;
747export function readSetup(value) {
748    if (!isObject(value))
749        return { done: false, sessions: [], lastAt: 0 };
750    return {
751        done: value.done === true,
752        sessions: Array.isArray(value.sessions)
753            ? value.sessions.filter((id) => typeof id === 'string').slice(-SETUP_SESSIONS)
754            : [],
755        lastAt: count(value.lastAt) ?? 0,
756    };
757}
758export function setUpOf(output) {
759    const app = output && isObject(output.app) ? output.app : null;
760    return typeof app?.setUp === 'boolean' ? app.setUp : null;
761}
762export function setupNeeded(record, sessionId, now) {
763    return (!record.done &&
764        !record.sessions.includes(sessionId) &&
765        record.sessions.length < SETUP_SESSIONS &&
766        now - record.lastAt >= SETUP_QUIET_MS);
767}
768export const prompted = (record, sessionId, now) => ({
769    ...record,
770    sessions: [...record.sessions, sessionId].slice(-SETUP_SESSIONS),
771    lastAt: now,
772});
773export function connectionOf(output) {
774    const app = output && isObject(output.app) ? output.app : null;
775    const agent = output && isObject(output.agent) ? output.agent : null;
776    const name = typeof app?.name === 'string' && app.name.trim() ? app.name.trim() : 'Claude Code';
777    const machine = typeof agent?.machine === 'string' ? agent.machine.trim() : '';
778    return machine ? `${name} on ${machine}` : name;
779}
780export function setupPrompt(connection = 'Claude Code') {
781    return `Pending You: ${connection} is connected, but its setup isn’t finished. Three calls, nothing to install or approve: 1. whoami with your name. If it says app.setUp is true, another session did this: stop. 2. report_setup with source claude-code, your name, skillSaved true, hears instant, and skillVersion set to whoami’s skill.latest. Its answer has a testAreaId. 3. post_request a short test question in that testAreaId, tell your person it’s on its way, and end your turn: you’re woken when they answer, so don’t ask them to type anything.`;
782}
783export const PRESENCE_EVERY_MS = 5 * 60_000;
784export const PRESENCE_RUN_MS = 15_000;
785const PRODUCTION = 'https://www.pendingyou.com';
786export function cliConfigOf(root, name = CLI_COPY) {
787    if (name !== CLI_COPY)
788        return null;
789    return (/^(.+)[\\/]cli[\\/][^\\/]+[\\/]node_modules[\\/]pendingyou[\\/]mod[\\/]?$/.exec(root)?.[1] ??
790        /^(.+)[\\/]mod[\\/]?$/.exec(root)?.[1] ??
791        null);
792}
793const SESSION_FILE = /^[A-Za-z0-9][A-Za-z0-9_-]{0,99}$/;
794export function loadedPath(config, sessionId) {
795    return SESSION_FILE.test(sessionId) ? `${config}/wake/sessions/${sessionId}.json` : null;
796}
797export const loadedNote = (at) => `${JSON.stringify({ at: Math.floor(at) })}\n`;
798export function originOf(manifest) {
799    let parsed = manifest;
800    if (typeof manifest === 'string')
801        try {
802            parsed = JSON.parse(manifest);
803        }
804        catch {
805            return null;
806        }
807    const origin = typeof parsed === 'object' && parsed !== null ? parsed.origin : null;
808    return typeof origin === 'string' && /^https?:\/\/[^\s/]+$/.test(origin) ? origin : null;
809}
810export function presenceArgv(config, origin, session) {
811    return [
812        `${config}/bin/pendingyou-hook`,
813        'presence',
814        '--app',
815        'claude-code',
816        '--state',
817        'live',
818        '--session',
819        session.id,
820        ...(session.cwd ? ['--cwd', session.cwd] : []),
821        ...(session.name ? ['--name', session.name] : []),
822        '--at',
823        String(Math.floor(session.at)),
824        ...(origin === PRODUCTION ? [] : ['--origin', origin]),
825    ];
826}
827export const HERDR_EVERY_MS = 5 * 60_000;
828export const HERDR_RUN_MS = 15_000;
829export function inHerdr(on, bin, pane) {
830    return (on === '1' &&
831        typeof bin === 'string' &&
832        bin.startsWith('/') &&
833        typeof pane === 'string' &&
834        /^[A-Za-z0-9]{1,16}:[A-Za-z0-9]{1,16}$/.test(pane));
835}
836export function herdrCards(book) {
837    return Object.values(book.cards)
838        .filter((card) => card.state && isWaitingOnYou(card.state))
839        .sort((a, b) => (a.requestId < b.requestId ? -1 : a.requestId > b.requestId ? 1 : 0))
840        .map((card) => ({
841        requestId: card.requestId,
842        ...(card.title ? { title: card.title } : {}),
843        ...pressingOfCard(card),
844        at: card.learnedAt,
845    }));
846}
847export const herdrPayload = (book) => JSON.stringify({ name: book.name ?? null, cards: herdrCards(book) });
848export function herdrArgv(config, session) {
849    return [
850        `${config}/bin/pendingyou-hook`,
851        'herdr',
852        'report',
853        '--app',
854        'claude-code',
855        '--session',
856        session.id,
857        '--at',
858        String(Math.floor(session.at)),
859    ];
860}
861