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…

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.
pending-you skill: when a decision is yours, and how to ask so you can answer in one tap.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./permissions (mcp__plugin_pending-you_pendingyou) so it can check while you’re away.recordplane/pendingyou-plugin. Install Pending You, connect its connector (sign in, Allow), and say “set up Pending You” in a Cowork task.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.
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.
Generated from Pending You’s guide 2.49.0.
hooks/register.js 519 lines1// 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}
519hooks/wake.js 861 lines1// 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