pendingyou 0.35.0 → 0.36.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,33 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.36.0 (2026-10-19)
4
+
5
+ - Your agents' answers to your questions land in Pending You too (guide 2.54). When you ask an agent something and
6
+ its last message answers it (30 words or more outside code, or a table) but it posted nothing to Pending You that
7
+ turn, the check at the end of its turn asks it once: “Your person asked you something and your answer is only in
8
+ chat: post_answer it too, or say it doesn’t need one.” Your question is read from your own message that started the
9
+ turn, leniently, since dictation often has no “?”: “pending you how many calls did billing get last week” counts.
10
+ A request for work doesn't (“Can you fix the flaky test?”, “Deploy it to staging”, “When you're done, push it”),
11
+ nor anything under four words without a “?”, nor a turn Pending You started. Pattern matching only, on your
12
+ computer, as before. Claude Code reads your message from its transcript; Codex's next-message hook notes whether it
13
+ asked (never your words), OpenCode's plugin reads the turn's first message, and Pi's extension hands over the
14
+ prompt that started the run.
15
+ - Your next message marks the answers that session posted as read in Pending You, so they leave your Pending without
16
+ notifying you: you've seen them in the chat. Only that session's own, posted before your message, and only when it
17
+ posted one in the last day. It goes beside the hand-off's request, inside its 2 seconds, prints nothing, and
18
+ changes nothing if it fails.
19
+ - Codex: `init` gives the hook for Pending You's tools post_answer, which changes it, so trust it again in Codex's
20
+ `/hooks` (init and status say how). Until then the check for an answer left in chat stays off there.
21
+ - `post_answer` counts as a card call: the end-of-turn checks see it, and Codex's hook, OpenCode's plugin and Pi's
22
+ extension remember the answer for the session, so a Reply on it wakes the session there as an answer does. In
23
+ Claude Code a Reply comes with your next message, or at the next session start.
24
+
25
+ - Fixed: the Pending You popup in Herdr no longer breaks on a kind of card it doesn't know yet (or a grouped card with
26
+ a question of such a kind). It shows the card and says "Answer this one in Pending You: o opens it."; reply, Later,
27
+ hand over and open still work.
28
+ - The person API client inside the command line follows the SDK's 0.2.0 types: the enums in what /v1 sends are open
29
+ (any string beside the known values), since /v1 adds values over time.
30
+
3
31
  ## 0.35.0 (2026-10-09)
4
32
 
5
33
  - Claude Code hears a question handed to it within seconds. When you delegate a card to one of your Claude Code agents
package/README.md CHANGED
@@ -234,6 +234,28 @@ it or hand it back instead (about 45 tokens). Codex's hook for Pending You's too
234
234
  tools, which doesn't name those two, so there the check knows only that the turn started with one (when Codex gives the
235
235
  message to its next-message hook), and says “unless you already did”.
236
236
 
237
+ ### Answers to your questions (0.36.0)
238
+
239
+ When you ask an agent something and it answers, the answer goes in Pending You too (`post_answer`, guide 2.54): it
240
+ waits in your Pending under your question, quiet while you're in the chat. Two things help:
241
+
242
+ - **At the end of a turn**, when your own message started it and asked something, its last message answers it (30
243
+ words or more outside code, or a table, and not only a recap of cards), and nothing was posted to Pending You in
244
+ that turn, the agent is asked once (about 29 tokens): *Your person asked you something and your answer is only in
245
+ chat: post_answer it too, or say it doesn’t need one.* Your question is read leniently, since dictation often has
246
+ no "?": a "?" in its first sentence or one of its last three, a question word and three more words ("pending you how
247
+ many calls did billing get last week"), or asking for facts ("tell me what broke", "how many", "check whether"). Not
248
+ a request for work ("Can you fix the flaky test?", "Deploy it to staging", "When you're done, push it", "Do the
249
+ migration"), anything under four words without a "?" ("yes", "go on"), or a turn Pending You started. Claude Code's
250
+ question comes from its transcript; Codex's next-message hook notes whether it asked (never your words), and the
251
+ check stays off on a computer until `init` has given Codex's hook for Pending You's tools `post_answer` and you've
252
+ trusted it again in `/hooks`; OpenCode's plugin reads the turn's first message, and Pi's extension hands over the
253
+ prompt that started the run.
254
+ - **Your next message** in that session marks the answers it posted as read in Pending You (`POST /mcp/cli/read`), so
255
+ they leave your Pending without a notification: you've seen them in the chat. Only that session's own, posted before
256
+ your message, and only when it posted one in the last day; beside the hand-off's request, inside its 2 seconds. It
257
+ prints nothing, and when it fails nothing changes: an unread answer notifies you once, after your hand-off time.
258
+
237
259
  **On 0.2.0 with another origin?** Its hooks blocked every message (`There's no "--origin" command`). Run
238
260
  `npx pendingyou@latest init --origin <url>` again to repair them; see [CHANGELOG.md](CHANGELOG.md).
239
261
 
@@ -27,10 +27,10 @@ const REQUEST_ID = /^req_[A-Za-z0-9-]{1,40}$/;
27
27
  export const THREAD = /^[A-Za-z0-9][A-Za-z0-9_-]{0,99}$/;
28
28
  const HANDED = '_handed';
29
29
  const NAMES = 5;
30
- const MCP_CARD_TOOL = /^mcp__([A-Za-z0-9_]*pendingyou[A-Za-z0-9_]*)__(post_request|update_request|reply_in_thread|cancel_request|answer_delegated|hand_back)$/i;
30
+ const MCP_CARD_TOOL = /^mcp__([A-Za-z0-9_]*pendingyou[A-Za-z0-9_]*)__(post_request|post_answer|update_request|reply_in_thread|cancel_request|answer_delegated|hand_back)$/i;
31
31
  const CARD_TOOLS = {
32
32
  codex: MCP_CARD_TOOL,
33
- opencode: /^([A-Za-z0-9_-]*pendingyou[A-Za-z0-9_-]*)_(post_request|update_request|reply_in_thread|cancel_request|answer_delegated|hand_back)$/i,
33
+ opencode: /^([A-Za-z0-9_-]*pendingyou[A-Za-z0-9_-]*)_(post_request|post_answer|update_request|reply_in_thread|cancel_request|answer_delegated|hand_back)$/i,
34
34
  pi: MCP_CARD_TOOL,
35
35
  };
36
36
  const HELPER_CALLS = new Set(['answer_delegated', 'hand_back']);
@@ -58,6 +58,9 @@ function threadOf(value) {
58
58
  ...(names.length ? { names: names.slice(-NAMES) } : {}),
59
59
  ...(typeof value.cwd === 'string' && value.cwd ? { cwd: value.cwd } : {}),
60
60
  ...(typeof value.herdr === 'string' && PANE_ID.test(value.herdr) ? { herdr: value.herdr } : {}),
61
+ ...(typeof value.answeredAt === 'number' && Number.isFinite(value.answeredAt)
62
+ ? { answeredAt: value.answeredAt }
63
+ : {}),
61
64
  };
62
65
  }
63
66
  async function load(io, app) {
@@ -192,6 +195,8 @@ export async function posted(io, options) {
192
195
  waiting.cwd = cwd;
193
196
  if (pane)
194
197
  waiting.herdr = pane.pane;
198
+ if (call === 'post_answer' && requestId)
199
+ waiting.answeredAt = now;
195
200
  waiting.origin = options.origin;
196
201
  waiting.at = now;
197
202
  state.threads[thread] = waiting;
@@ -287,6 +292,17 @@ export async function threadCards(io, app, thread) {
287
292
  const cards = (await load(io, app).catch(() => null))?.threads[thread]?.cards ?? {};
288
293
  return Object.keys(cards).filter((id) => REQUEST_ID.test(id));
289
294
  }
295
+ export async function threadAnsweredAt(io, app, thread) {
296
+ if (!thread)
297
+ return null;
298
+ return (await load(io, app).catch(() => null))?.threads[thread]?.answeredAt ?? null;
299
+ }
300
+ export async function markAskedTurn(io, turn, app) {
301
+ const now = io.now();
302
+ await change(io, app, (state) => {
303
+ state.turns[turn] = { ...(state.turns[turn] ?? { cards: [] }), at: now, asked: true };
304
+ });
305
+ }
290
306
  export async function markHandedTurn(io, turn, app) {
291
307
  const now = io.now();
292
308
  await change(io, app, (state) => {
@@ -346,6 +362,7 @@ export async function turnRecord(io, turn, app = 'codex') {
346
362
  cards: Array.isArray(record.cards) ? record.cards : [],
347
363
  nudged: record.nudged === true,
348
364
  delegated: record.delegated === true,
365
+ asked: record.asked === true,
349
366
  }
350
367
  : null;
351
368
  }
@@ -37,7 +37,8 @@ export function trustSteps(found) {
37
37
  ];
38
38
  }
39
39
  const DONT_SAVE = 'In the Codex app, don’t press Save on pendingyou in Settings › MCP: that writes it again without this computer’s sign-in (if you did, run init again to put it back).';
40
- const CARD_TOOLS = '^mcp__pendingyou__(post_request|update_request|reply_in_thread|cancel_request)$';
40
+ const CARD_TOOLS = '^mcp__pendingyou__(post_request|post_answer|update_request|reply_in_thread|cancel_request)$';
41
+ const POST_ANSWER = 'mcp__pendingyou__post_answer';
41
42
  export const CODEX_HOOKS = [
42
43
  { event: 'SessionStart', sub: 'pickup', matcher: 'startup|resume|clear' },
43
44
  { event: 'UserPromptSubmit', sub: 'handoff' },
@@ -210,6 +211,22 @@ async function readHookTrust(io) {
210
211
  });
211
212
  }
212
213
  const untrusted = async (io) => (await readHookTrust(io)).filter((hook) => hook.trust === 'untrusted' || hook.trust === 'changed');
214
+ export async function codexSeesAnswers(io) {
215
+ try {
216
+ const file = await readHooksFile(io);
217
+ if (!('json' in file))
218
+ return false;
219
+ const hooks = file.json.hooks && typeof file.json.hooks === 'object' ? file.json.hooks : {};
220
+ const groups = Array.isArray(hooks.PostToolUse) ? hooks.PostToolUse : [];
221
+ const ours = groups.find((group) => (group?.hooks ?? []).some((hook) => isOurHook(hook?.command, 'posted')));
222
+ if (typeof ours?.matcher !== 'string' || !new RegExp(ours.matcher).test(POST_ANSWER))
223
+ return false;
224
+ return (await readHookTrust(io)).some((hook) => hook.sub === 'posted' && hook.trust === 'trusted');
225
+ }
226
+ catch {
227
+ return false;
228
+ }
229
+ }
213
230
  async function signedInByItself(io, program) {
214
231
  const result = await io.run(program, ['mcp', 'list', '--json'], 30_000);
215
232
  if (result.code !== 0)
@@ -10,7 +10,7 @@ const PRODUCTION = 'https://www.pendingyou.com';
10
10
  const HOOK_MS = 5000;
11
11
  const TURN_MESSAGES = 200;
12
12
  const TEXT_CHARS = 100_000;
13
- const CARD_TOOL = /^[A-Za-z0-9_-]*pendingyou[A-Za-z0-9_-]*_(post_request|update_request|reply_in_thread|cancel_request|answer_delegated|hand_back)$/i;
13
+ const CARD_TOOL = /^[A-Za-z0-9_-]*pendingyou[A-Za-z0-9_-]*_(post_request|post_answer|update_request|reply_in_thread|cancel_request|answer_delegated|hand_back)$/i;
14
14
  export const HANDED_FOR_MS = 12 * 60 * 60_000;
15
15
  const NUDGED = 'Asked OpenCode to put what it needs from you on a card.';
16
16
  const isObject = (value) => typeof value === 'object' && value !== null && !Array.isArray(value);
@@ -8,7 +8,7 @@ export const READY_EVERY_MS = 500;
8
8
  export const READY_FOR_MS = 30_000;
9
9
  const WHOAMI = /^mcp__[A-Za-z0-9_]*pendingyou[A-Za-z0-9_]*__whoami$/i;
10
10
  const STOP_MS = 2000;
11
- export const CARD_TOOL = /^mcp__[A-Za-z0-9_]*pendingyou[A-Za-z0-9_]*__(post_request|update_request|reply_in_thread|cancel_request|answer_delegated|hand_back)$/i;
11
+ export const CARD_TOOL = /^mcp__[A-Za-z0-9_]*pendingyou[A-Za-z0-9_]*__(post_request|post_answer|update_request|reply_in_thread|cancel_request|answer_delegated|hand_back)$/i;
12
12
  export const HANDED_FOR_MS = 12 * 60 * 60_000;
13
13
  export const HANDED_PREFIX = 'Pending You: your person handed you';
14
14
  const HANDED_REPLY = ' on the question handed to you, ';
@@ -148,6 +148,7 @@ export function pendingYou(setup, options = {}) {
148
148
  let cwd = '';
149
149
  let closed = false;
150
150
  let turn = null;
151
+ let prompt = null;
151
152
  let nudged = false;
152
153
  let posting = Promise.resolve();
153
154
  let listener = null;
@@ -328,7 +329,8 @@ export function pendingYou(setup, options = {}) {
328
329
  nextTurn(said);
329
330
  listen();
330
331
  });
331
- pi.on('before_agent_start', async (_event, ctx) => {
332
+ pi.on('before_agent_start', async (event, ctx) => {
333
+ prompt = typeof event?.prompt === 'string' ? event.prompt : null;
332
334
  listen();
333
335
  const said = await hook('handoff', { cwd: ctx.cwd || cwd, session_id: session });
334
336
  return said ? { message: { customType: TYPE, content: said, display: false } } : undefined;
@@ -373,6 +375,7 @@ export function pendingYou(setup, options = {}) {
373
375
  last_assistant_message: text,
374
376
  stop_hook_active: false,
375
377
  ...(startedHanded(event.context?.contextMessages, handedWakes) ? { handed: true } : {}),
378
+ ...(prompt !== null ? { prompt } : {}),
376
379
  }));
377
380
  if (!reason)
378
381
  return;
@@ -388,6 +391,7 @@ export function pendingYou(setup, options = {}) {
388
391
  });
389
392
  pi.on('agent_settled', () => {
390
393
  turn = null;
394
+ prompt = null;
391
395
  nudged = false;
392
396
  });
393
397
  pi.on('session_shutdown', async () => {
package/dist/asked.js ADDED
@@ -0,0 +1,102 @@
1
+ import { isHandedWake } from "./format.js";
2
+ const PENDING_YOU_LINE = /^Pending You: (?:your person|nobody answered|\d+ of your cards|this computer|anything you need|[\w .’'-]{1,40} is connected|.*\(req_[A-Za-z0-9-]+)/;
3
+ const TAGGED = /^<[a-z][\w-]*>/i;
4
+ const SLASH = /^\/[a-z]/i;
5
+ const NOT_THEIRS = /^(?:\[Request interrupted|Stop hook feedback)/i;
6
+ export function ownWords(text) {
7
+ const words = text.trim();
8
+ if (!words)
9
+ return null;
10
+ const first = words.split('\n')[0] ?? '';
11
+ if (PENDING_YOU_LINE.test(first) ||
12
+ isHandedWake(words) ||
13
+ TAGGED.test(words) ||
14
+ SLASH.test(words) ||
15
+ NOT_THEIRS.test(words))
16
+ return null;
17
+ return words;
18
+ }
19
+ function spoken(text) {
20
+ const kept = [];
21
+ let fence = null;
22
+ for (const line of text.split('\n')) {
23
+ const marker = /^\s*(`{3,}|~{3,})/.exec(line)?.[1];
24
+ if (fence) {
25
+ if (marker?.startsWith(fence))
26
+ fence = null;
27
+ continue;
28
+ }
29
+ if (marker) {
30
+ fence = marker;
31
+ continue;
32
+ }
33
+ if (/^\s*>/.test(line))
34
+ continue;
35
+ kept.push(line
36
+ .replace(/`[^`]*`/g, ' ')
37
+ .replace(/“[^”]*”|"[^"]*"/g, ' ')
38
+ .replace(/\b(?:https?:\/\/|www\.)\S+/gi, ' '));
39
+ }
40
+ return kept.join('\n');
41
+ }
42
+ const FILLER = /^(?:(?:pending\s*(?:you|new|view)|hey|hi|hello|yo|ok(?:ay)?|so|um+|uh+|well|alright|all right|right|oh|and|also|now|please|quick question)\b[\s,.:;!?-]*)+/i;
43
+ const ASSISTANT = /^(?:claude|codex|opencode|pi|cursor|grok|chatgpt|gemini)\b[\s,.:;!-]*/i;
44
+ const NAMED = /^[A-Z][\p{L}’'-]*,\s+/u;
45
+ function opening(sentence) {
46
+ let text = sentence.trim();
47
+ for (let tries = 0; tries < 6; tries++) {
48
+ const next = text.replace(FILLER, '').replace(ASSISTANT, '').replace(NAMED, '').trim();
49
+ if (next === text)
50
+ break;
51
+ text = next;
52
+ }
53
+ return text;
54
+ }
55
+ export const wordsOf = (text) => text.match(/[\p{L}\p{N}][\p{L}\p{N}’'.,:%-]*/gu) ?? [];
56
+ const QUESTION = /^(?:what|which|who|when|where|why|how|is|are|was|do|does|did|can|could|will|would|should|has|have|any)(?:n[’']t)?\b/i;
57
+ const DO_OR_HAVE = /^(?:do|have)\s+(?!(?:you|we|i|they|he|she|it|any|all|both|our|my|your|their)\b)/i;
58
+ const NEGATED = /^[a-z]+n[’']t\b/i;
59
+ const STATEMENT = /^(?:what|which|when|where|why|how)\s+(?:i|we|you|they|he|she)\b/i;
60
+ const FACTS = /\b(?:(?:tell me|let me know)\s+(?:what|which|who|whose|where|why|how|whether|about)|find out|look up|show me|how many|how much|i want to know|do you know|check (?:whether|if))\b/i;
61
+ const WORK = String.raw `(?:fix|add|make|run|deploy|write|create|update|change|remove|commit|push|merge|install|set\s+up|try)`;
62
+ const WORK_ASKED = new RegExp(String.raw `\b(?:can|could|would|will)\s+you\s+(?:please\s+|just\s+|also\s+|quickly\s+|now\s+)?${WORK}\b`, 'i');
63
+ const WORK_FIRST = new RegExp(String.raw `^${WORK}\b`, 'i');
64
+ const WORK_LATER = new RegExp(String.raw `(?:[,;:]\s*|\b(?:and|then)\s+)${WORK}\b`, 'i');
65
+ const sentencesOf = (text) => text
66
+ .split(/(?<=[.!?])\s+|\n+/)
67
+ .map((sentence) => sentence.trim())
68
+ .filter(Boolean);
69
+ function asks(sentence) {
70
+ const said = opening(sentence);
71
+ if (!said)
72
+ return false;
73
+ if (WORK_ASKED.test(said))
74
+ return false;
75
+ const facts = FACTS.test(said);
76
+ if (WORK_FIRST.test(said) && !facts)
77
+ return false;
78
+ if (said.includes('?'))
79
+ return true;
80
+ if (facts)
81
+ return true;
82
+ if (WORK_LATER.test(said))
83
+ return false;
84
+ return (QUESTION.test(said) &&
85
+ !DO_OR_HAVE.test(said) &&
86
+ !NEGATED.test(said) &&
87
+ !STATEMENT.test(said) &&
88
+ wordsOf(said).length >= 4);
89
+ }
90
+ export function askedQuestion(message) {
91
+ const plain = spoken(message);
92
+ const said = opening(plain);
93
+ if (!said.includes('?') && wordsOf(said).length < 4)
94
+ return false;
95
+ const sentences = sentencesOf(plain);
96
+ const looked = [...new Set([sentences[0], ...sentences.slice(-3)])].filter((sentence) => sentence !== undefined);
97
+ return looked.some(asks);
98
+ }
99
+ export function asksQuestion(message) {
100
+ const words = typeof message === 'string' ? ownWords(message) : null;
101
+ return words !== null && askedQuestion(words);
102
+ }
@@ -22,6 +22,25 @@ export const askerOf = (card) => `${card.asker.agent?.name ?? card.asker.assista
22
22
  const askerName = (card) => card.asker.agent?.name ?? card.asker.assistant.name;
23
23
  export const HIGH_STAKES = 'High stakes: answer it in Pending You, where you hold to approve. o opens it.';
24
24
  export const NOT_HERE = 'Answer this one in Pending You: o opens it.';
25
+ const KINDS = [
26
+ 'choice',
27
+ 'multi',
28
+ 'approve',
29
+ 'text',
30
+ 'review',
31
+ 'action',
32
+ 'fyi',
33
+ 'group',
34
+ ];
35
+ const QUESTION_KINDS = [
36
+ 'choice',
37
+ 'multi',
38
+ 'approve',
39
+ 'text',
40
+ ];
41
+ const answersHere = (card) => card.actions.includes('answer') &&
42
+ KINDS.includes(card.kind) &&
43
+ (card.questions ?? []).every((question) => QUESTION_KINDS.includes(question.kind));
25
44
  const NO_WORDS = 'Pending You takes no words on this card from Herdr: o opens it.';
26
45
  const NO_LATER = 'This card can’t go to Later from Herdr: o opens it.';
27
46
  const NO_HANDING = 'This card can’t be handed over from Herdr: o opens it.';
@@ -126,7 +145,7 @@ function listKey(state, key, now) {
126
145
  { kind: 'targets', card },
127
146
  ];
128
147
  }
129
- if (!card.actions.includes('answer'))
148
+ if (!answersHere(card))
130
149
  return [{ ...state, note: NOT_HERE }, NONE];
131
150
  return answerKey(state, card, key);
132
151
  }
@@ -211,6 +230,8 @@ function answerKey(state, card, key) {
211
230
  return [{ ...state, note: 'y: got it.' }, NONE];
212
231
  case 'group':
213
232
  return groupKey(state, card, key, send);
233
+ default:
234
+ return [{ ...state, note: NOT_HERE }, NONE];
214
235
  }
215
236
  }
216
237
  const toggled = (chosen, id) => chosen.includes(id) ? chosen.filter((each) => each !== id) : [...chosen, id];
@@ -289,6 +310,8 @@ function groupKey(state, card, key, send) {
289
310
  const suggestion = n ? question.text?.suggestions?.[n - 1] : undefined;
290
311
  return suggestion ? answered({ text: suggestion }) : [state, NONE];
291
312
  }
313
+ default:
314
+ return [{ ...state, note: NOT_HERE }, NONE];
292
315
  }
293
316
  }
294
317
  function writeKey(state, screen, key) {
@@ -564,7 +587,7 @@ function askLines(card, state, room, now) {
564
587
  lines.push(DIM(fit(`If you don’t answer ${fromNow(card.fallback.at, now)}: ${card.fallback.text}`)));
565
588
  if (card.highStakes)
566
589
  lines.push(...wrap(HIGH_STAKES, room, 2));
567
- else if (!card.actions.includes('answer'))
590
+ else if (!answersHere(card))
568
591
  lines.push(...wrap(NOT_HERE, room, 2));
569
592
  return lines;
570
593
  }
@@ -603,6 +626,8 @@ function answerKeys(card, state) {
603
626
  : [`1–${count(question?.options)}`];
604
627
  return [...own, 'Tab next question', 'Enter send all'];
605
628
  }
629
+ default:
630
+ return [];
606
631
  }
607
632
  }
608
633
  export function keysLine(state, now) {
@@ -632,7 +657,7 @@ export function keysLine(state, now) {
632
657
  if (card) {
633
658
  const busy = state.sent?.cardId === card.id && now < state.sent.until;
634
659
  if (!card.highStakes && !busy) {
635
- if (card.actions.includes('answer'))
660
+ if (answersHere(card))
636
661
  keys.push(...answerKeys(card, state));
637
662
  if (card.actions.includes('reply'))
638
663
  keys.push('r reply');
@@ -40,7 +40,7 @@ const KEEP_MS = 7 * 24 * 60 * 60_000;
40
40
  const STDIN_MAX = 8_000_000;
41
41
  const TRANSCRIPT_CAP = 4 * 1024 * 1024;
42
42
  const SESSION = /^[A-Za-z0-9][A-Za-z0-9_-]{0,99}$/;
43
- const NAMED_TOOL = /^mcp__.*pendingyou.*__(whoami|post_request|update_request|cancel_request|reply_in_thread|get_request|ack_answer|list_pending|get_attachment)$/i;
43
+ const NAMED_TOOL = /^mcp__.*pendingyou.*__(whoami|post_request|post_answer|update_request|cancel_request|reply_in_thread|get_request|ack_answer|list_pending|get_attachment)$/i;
44
44
  const isObject = (value) => typeof value === 'object' && value !== null && !Array.isArray(value);
45
45
  const folderOf = (io) => join(configDir(io), PERMISSION_FOLDER);
46
46
  const pathOf = (io, session) => join(folderOf(io), `${session}.json`);
@@ -623,7 +623,7 @@ export async function sessionName(path) {
623
623
  await file.close().catch(() => { });
624
624
  }
625
625
  }
626
- const POST_TOOL = /^mcp__.*pendingyou.*__post_request$/i;
626
+ const POST_TOOL = /^mcp__.*pendingyou.*__(post_request|post_answer)$/i;
627
627
  const POSTED_ID = /"requestId"\s*:\s*"(req_[A-Za-z0-9-]{1,40})"/;
628
628
  export const POSTED_MAX = 50;
629
629
  function resultText(content) {
@@ -650,9 +650,10 @@ export async function sessionPosted(path) {
650
650
  const results = new Map();
651
651
  const posted = [];
652
652
  let more = false;
653
+ let answeredAt;
653
654
  const read = (line) => {
654
655
  const asResult = line.includes('tool_result') && line.includes('req_');
655
- const asCall = line.includes('post_request');
656
+ const asCall = line.includes('post_request') || line.includes('post_answer');
656
657
  if (!asResult && !asCall)
657
658
  return;
658
659
  let entry;
@@ -678,7 +679,14 @@ export async function sessionPosted(path) {
678
679
  block.type === 'tool_use' &&
679
680
  POST_TOOL.test(text(block.name))) {
680
681
  const id = results.get(text(block.id));
681
- if (!id || posted.includes(id))
682
+ if (!id)
683
+ continue;
684
+ const at = Date.parse(text(entry.timestamp));
685
+ if (answeredAt === undefined &&
686
+ /post_answer$/i.test(text(block.name)) &&
687
+ Number.isFinite(at))
688
+ answeredAt = at;
689
+ if (posted.includes(id))
682
690
  continue;
683
691
  if (posted.length < POSTED_MAX)
684
692
  posted.push(id);
@@ -703,7 +711,11 @@ export async function sessionPosted(path) {
703
711
  }
704
712
  if (position === 0)
705
713
  read(carry.toString('utf8'));
706
- return { posted, whole: position === 0 && !more };
714
+ return {
715
+ posted,
716
+ whole: position === 0 && !more,
717
+ ...(answeredAt !== undefined ? { answeredAt } : {}),
718
+ };
707
719
  }
708
720
  catch {
709
721
  return null;
package/dist/pickup.js CHANGED
@@ -1,6 +1,7 @@
1
- import { getJson, SignInNeeded, Unavailable } from "./api.js";
2
- import { computerNames, hearsOnly, keepListening, markHandedTurn, sessionFor, threadCards, threadNames, touchThread, } from "./apps/codex-wake.js";
1
+ import { getJson, postJson, SignInNeeded, Unavailable } from "./api.js";
2
+ import { computerNames, hearsOnly, keepListening, markAskedTurn, markHandedTurn, sessionFor, threadAnsweredAt, threadCards, threadNames, touchThread, } from "./apps/codex-wake.js";
3
3
  import { APP_NAMES, DEFAULT_APP } from "./apps/ids.js";
4
+ import { asksQuestion, ownWords } from "./asked.js";
4
5
  import { connectionMachine, readCredential } from "./credentials.js";
5
6
  import { environmentWarning } from "./environment.js";
6
7
  import { configDir, readJson } from "./files.js";
@@ -16,6 +17,8 @@ import { announce, keepPresent, readSaid } from "./presence.js";
16
17
  import { setupReminder } from "./setup.js";
17
18
  import { claimHanded, markHanded, momentOf, releaseHanded, wasHanded } from "./state.js";
18
19
  const TIME_BOX_MS = { pickup: 2500, handoff: 2000 };
20
+ export const READ_PATH = '/mcp/cli/read';
21
+ export const READ_FOR_MS = 24 * 60 * 60_000;
19
22
  export const ANSWERS_ASKED = 20;
20
23
  export const ANSWERS_SHOWN = 5;
21
24
  export const REMINDER = 'Pending You: anything you need from your person also goes on a card.';
@@ -113,29 +116,54 @@ export function folderForms(cwd, home) {
113
116
  forms.push(`~${cwd.slice(base.length)}`);
114
117
  return forms;
115
118
  }
116
- export async function askerParams(io, origin, app, session) {
119
+ export async function askerOf(io, origin, app, session) {
117
120
  let name = session.name ?? undefined;
118
121
  let posted = [];
119
122
  let own = false;
123
+ let answeredAt = null;
120
124
  if (app === DEFAULT_APP) {
121
125
  const record = await sessionPosted(session.transcript ?? undefined);
122
126
  if (record) {
123
127
  posted = record.posted;
124
128
  own = record.whole;
129
+ answeredAt = record.answeredAt ?? null;
125
130
  }
126
131
  }
127
132
  else if (session.id) {
128
133
  name ??= (await threadNames(io, app, session.id).catch(() => [])).at(-1);
129
134
  posted = (await threadCards(io, app, session.id).catch(() => [])).slice(0, POSTED_MAX);
135
+ answeredAt = await threadAnsweredAt(io, app, session.id).catch(() => null);
130
136
  }
131
137
  const machine = await connectionMachine(io, origin, app).catch(() => null);
132
138
  const machines = [...new Set([io.host, machine].filter((each) => Boolean(each)))];
133
- return [
134
- ...(name ? [['name', name]] : []),
135
- ...posted.map((id) => ['posted', id]),
136
- ...(own ? [['own', '1']] : []),
137
- ...machines.slice(0, 4).map((each) => ['machine', each]),
138
- ];
139
+ return {
140
+ params: [
141
+ ...(name ? [['name', name]] : []),
142
+ ...posted.map((id) => ['posted', id]),
143
+ ...(own ? [['own', '1']] : []),
144
+ ...machines.slice(0, 4).map((each) => ['machine', each]),
145
+ ],
146
+ answeredAt,
147
+ };
148
+ }
149
+ export const askerParams = async (io, origin, app, session) => (await askerOf(io, origin, app, session)).params;
150
+ export const readsAnswers = (asker, prompt, now) => asker.answeredAt !== null &&
151
+ now - asker.answeredAt < READ_FOR_MS &&
152
+ (prompt === null || ownWords(prompt) !== null);
153
+ async function readHere(io, origin, app, cwd, asker) {
154
+ try {
155
+ const query = new URLSearchParams([
156
+ ...folderForms(cwd, io.home).map((form) => ['cwd', form]),
157
+ ['source', app],
158
+ ...asker.params,
159
+ ['at', new Date(io.now()).toISOString()],
160
+ ]);
161
+ await postJson(io, origin, `${READ_PATH}?${query}`, {}, TIME_BOX_MS.handoff, {
162
+ app,
163
+ signal: io.signal,
164
+ });
165
+ }
166
+ catch { }
139
167
  }
140
168
  export async function pickup(io, options, hook = { fallback: '' }) {
141
169
  const { origin, mode } = options;
@@ -145,6 +173,7 @@ export async function pickup(io, options, hook = { fallback: '' }) {
145
173
  let reminder = null;
146
174
  let unwoken = null;
147
175
  let stuck = null;
176
+ let reading = null;
148
177
  try {
149
178
  const input = await hookInput(io);
150
179
  const { cwd } = input;
@@ -186,6 +215,8 @@ export async function pickup(io, options, hook = { fallback: '' }) {
186
215
  await touchThread(io, app, input.session, input.given ? input.cwd : null);
187
216
  if (app === 'codex' && input.turn && isHandedWake(input.prompt ?? ''))
188
217
  await markHandedTurn(io, input.turn, app).catch(() => { });
218
+ else if (app === 'codex' && mode === 'handoff' && input.turn && asksQuestion(input.prompt))
219
+ await markAskedTurn(io, input.turn, app).catch(() => { });
189
220
  }
190
221
  if (app === 'codex' && input.session)
191
222
  await keepListening(io, origin, input.session);
@@ -193,16 +224,19 @@ export async function pickup(io, options, hook = { fallback: '' }) {
193
224
  ? sessionName(input.transcript ?? undefined).then((name) => (name ? [name] : []))
194
225
  : null;
195
226
  const named = claudeNames ? (await claudeNames)[0] : undefined;
227
+ const asker = await askerOf(io, origin, app, {
228
+ id: input.session,
229
+ transcript: input.transcript,
230
+ name: named ?? null,
231
+ });
232
+ if (mode === 'handoff' && readsAnswers(asker, input.prompt, io.now()))
233
+ reading = readHere(io, origin, app, cwd, asker);
196
234
  const query = new URLSearchParams([
197
235
  ...folderForms(cwd, io.home).map((form) => ['cwd', form]),
198
236
  ['limit', String(ANSWERS_ASKED)],
199
237
  ...(mode === 'handoff' ? [['open', String(OPEN_SHOWN)]] : []),
200
238
  ['source', app],
201
- ...(await askerParams(io, origin, app, {
202
- id: input.session,
203
- transcript: input.transcript,
204
- name: named ?? null,
205
- })),
239
+ ...asker.params,
206
240
  ]);
207
241
  const { status, body } = await getJson(io, origin, `/mcp/cli/answers?${query}`, TIME_BOX_MS[mode], io.signal, {
208
242
  hook: true,
@@ -309,5 +343,6 @@ export async function pickup(io, options, hook = { fallback: '' }) {
309
343
  const last = [line, warning, reminder, unwoken, stuck].filter(Boolean);
310
344
  if (last.length)
311
345
  io.out(`${last.join('\n')}\n`);
346
+ await reading;
312
347
  }
313
348
  }
@@ -177,6 +177,7 @@ export function createClient(options) {
177
177
  const keyed = (given) => ({ idempotencyKey: given ?? newKey() });
178
178
  const client = {
179
179
  me: (opts) => request('GET', '/v1/me', opts),
180
+ snapshot: (opts) => request('GET', '/v1/snapshot', opts),
180
181
  cards: {
181
182
  list: (query, opts) => request('GET', '/v1/cards', {
182
183
  ...opts,
package/dist/stopcheck.js CHANGED
@@ -1,7 +1,9 @@
1
1
  import { open } from 'node:fs/promises';
2
2
  import { getJson } from "./api.js";
3
+ import { codexSeesAnswers } from "./apps/codex.js";
3
4
  import { markNudged, turnRecord } from "./apps/codex-wake.js";
4
5
  import { APP_NAMES, DEFAULT_APP } from "./apps/ids.js";
6
+ import { asksQuestion, wordsOf } from "./asked.js";
5
7
  import { asksToTypeHere, cardFits, confirmReason, sessionLink } from "./confirm.js";
6
8
  import { readCredential } from "./credentials.js";
7
9
  import { isHandedWake } from "./format.js";
@@ -11,7 +13,9 @@ export const READ_CAP = 4 * 1024 * 1024;
11
13
  const CHUNK = 64 * 1024;
12
14
  export const STOP_REASON = 'Items for your person are in chat but not in Pending You: post them as cards (ask in both places), or say they don’t need one.';
13
15
  export const DELEGATED_REASON = 'Your person handed you that question to answer, not to ask them back: answer_delegated if you know, or hand_back with what you checked, unless you already did. Never post it as a card of your own.';
14
- const POSTING_TOOL = /^mcp__.+__(post_request|update_request|reply_in_thread|answer_delegated|hand_back)$/;
16
+ export const ANSWER_REASON = 'Your person asked you something and your answer is only in chat: post_answer it too, or say it doesn’t need one.';
17
+ export const ANSWER_WORDS = 30;
18
+ const POSTING_TOOL = /^mcp__.+__(post_request|post_answer|update_request|reply_in_thread|answer_delegated|hand_back)$/;
15
19
  export const OPEN_TIME_BOX_MS = 1500;
16
20
  const OPEN_CARDS = 5;
17
21
  const CARD_TEXT = 20_000;
@@ -35,7 +39,7 @@ const CARDS_HEADING = /\b(?:your|open|pending|waiting)\s+(?:(?:open|pending|wait
35
39
  const SENTENCE_END = /(?<=[.!?])\s+/;
36
40
  const OTHER_CARDS = /\b(?:credit|debit|payment|gift|sim|sd|business|graphics|video|sound|network|memory|smart)\s+cards?\b|\bcards?\s+(?:numbers?|readers?|details|holders?)\b/gi;
37
41
  const STEPS_FOR_THEM = /\b(you(?:'|’)ll need to|you will need to|you(?:'|’)ll have to|you will have to|your (?:next )?steps?\b|can you|could you|would you mind|please (?:run|confirm|approve|check|add|set|sign|create|open|paste|reply|send|choose|pick|decide|let me know)|in your terminal|only you can|(?<!\b(?:no|nothing|none|not)\b[^.,;:!?\n]{0,40})waiting (?:on|for) you|(?<!\bif (?:it|he|she|they|that|this|anything|something|someone|anyone|one) )needs? you to|over to you|parked for you|when you(?:'|’)re ready|up to you|your call\b|is yours\b|say the word|let me know (?:which|whether)|tell me (?:which|whether)|needs? your (?:ok|okay|approval|decision|sign-?off|go-ahead))/i;
38
- const SAYS_NO_CARD = /\bno (?:new |more |further )?cards? (?:is |are )?(?:needed|necessary|required)\b|\b(?:doesn(?:'|’)t|don(?:'|’)t|does not|do not|won(?:'|’)t|will not) need (?:a |any )?(?:new )?cards?\b|\bnothing (?:here |else |more |now )?(?:needs|requires) your (?:attention|input|action|answer)\b|\b(?:nothing|no cards?)\b[^.,;:!?\n]{0,40}\b(?:waiting|pending) (?:on|for) you\b/i;
42
+ const SAYS_NO_CARD = /\bno (?:new |more |further |answer )?cards? (?:is |are )?(?:needed|necessary|required)\b|\b(?:doesn(?:'|’)t|don(?:'|’)t|does not|do not|won(?:'|’)t|will not) need (?:a |an |any )?(?:new |answer )?cards?\b|\bnothing (?:here |else |more |now )?(?:needs|requires) your (?:attention|input|action|answer)\b|\b(?:nothing|no cards?)\b[^.,;:!?\n]{0,40}\b(?:waiting|pending) (?:on|for) you\b/i;
39
43
  const FOR_THEM_HEADING = /\b(your part|your steps?|your to-?dos?|your turn|your call|for you|waiting on you|left for you|parked for you|needs you|over to you|what you need to do|you(?:'|’)ll need)\b/i;
40
44
  const LIST_ITEM = /^\s*(?:\d+[.)]|[-*•]|\[ \])\s+\S/;
41
45
  const YOU = /\byou(?:r|rs|(?:'|’)(?:ll|d|re|ve))?\b/i;
@@ -123,6 +127,36 @@ function withoutRecaps(plain) {
123
127
  export function itemsOutsideRecaps(text) {
124
128
  return itemsIn(withoutRecaps(plainText(text)));
125
129
  }
130
+ function withoutCode(text) {
131
+ const kept = [];
132
+ let fence = null;
133
+ for (const line of text.split('\n')) {
134
+ const marker = /^\s*(`{3,}|~{3,})/.exec(line)?.[1];
135
+ if (fence) {
136
+ if (marker?.startsWith(fence))
137
+ fence = null;
138
+ continue;
139
+ }
140
+ if (marker) {
141
+ fence = marker;
142
+ continue;
143
+ }
144
+ kept.push(line.replace(/`[^`]*`/g, ' '));
145
+ }
146
+ return kept.join('\n');
147
+ }
148
+ function hasTable(text) {
149
+ const lines = text.split('\n');
150
+ return lines.some((line, index) => index > 0 &&
151
+ line.includes('|') &&
152
+ line.includes('-') &&
153
+ /^[\s|:-]+$/.test(line) &&
154
+ (lines[index - 1] ?? '').includes('|'));
155
+ }
156
+ export function isAnswer(text) {
157
+ const said = withoutRecaps(withoutCode(text));
158
+ return wordsOf(said).length >= ANSWER_WORDS || hasTable(said);
159
+ }
126
160
  export const saysPosted = (text) => SAYS_POSTED.test(plainText(text));
127
161
  export const saysNoCard = (text) => SAYS_NO_CARD.test(plainText(text));
128
162
  export const mentionsCards = (text) => /\bcards?\b/i.test(plainText(text).replace(OTHER_CARDS, ''));
@@ -176,6 +210,7 @@ export async function lastTurn(path, cap = READ_CAP) {
176
210
  let finalDone = false;
177
211
  let posted = false;
178
212
  let handed = false;
213
+ let asked = false;
179
214
  const cards = [];
180
215
  const take = (raw) => {
181
216
  if (!raw.trim())
@@ -190,7 +225,9 @@ export async function lastTurn(path, cap = READ_CAP) {
190
225
  if (entry.isSidechain)
191
226
  return false;
192
227
  if (startsTurn(entry)) {
193
- handed = startsHanded(startText(entry));
228
+ const words = startText(entry);
229
+ handed = startsHanded(words);
230
+ asked = !handed && asksQuestion(words);
194
231
  return true;
195
232
  }
196
233
  if (entry.type !== 'assistant' || !Array.isArray(entry.message?.content))
@@ -224,21 +261,29 @@ export async function lastTurn(path, cap = READ_CAP) {
224
261
  let newline = buffer.lastIndexOf(10);
225
262
  while (newline !== -1) {
226
263
  if (take(buffer.subarray(newline + 1).toString('utf8')))
227
- return { text: texts.join('\n'), posted, cards, handed, complete: true, bytesRead };
264
+ return { text: texts.join('\n'), posted, cards, handed, asked, complete: true, bytesRead };
228
265
  buffer = buffer.subarray(0, newline);
229
266
  newline = buffer.lastIndexOf(10);
230
267
  }
231
268
  carry = buffer;
232
269
  }
233
270
  if (position === 0 && take(carry.toString('utf8')))
234
- return { text: texts.join('\n'), posted, cards, handed, complete: true, bytesRead };
235
- return { text: texts.join('\n'), posted, cards, handed, complete: position === 0, bytesRead };
271
+ return { text: texts.join('\n'), posted, cards, handed, asked, complete: true, bytesRead };
272
+ return {
273
+ text: texts.join('\n'),
274
+ posted,
275
+ cards,
276
+ handed,
277
+ asked,
278
+ complete: position === 0,
279
+ bytesRead,
280
+ };
236
281
  }
237
282
  finally {
238
283
  await file.close();
239
284
  }
240
285
  }
241
- const OPENCODE_POSTING = /^[A-Za-z0-9_-]*pendingyou[A-Za-z0-9_-]*_(post_request|update_request|reply_in_thread|answer_delegated|hand_back)$/i;
286
+ const OPENCODE_POSTING = /^[A-Za-z0-9_-]*pendingyou[A-Za-z0-9_-]*_(post_request|post_answer|update_request|reply_in_thread|answer_delegated|hand_back)$/i;
242
287
  const isObject = (value) => typeof value === 'object' && value !== null && !Array.isArray(value);
243
288
  function opencodeTurn(input) {
244
289
  const messages = Array.isArray(input.messages) ? input.messages.filter(isObject) : [];
@@ -252,6 +297,17 @@ function opencodeTurn(input) {
252
297
  part.type === 'text' &&
253
298
  typeof part.text === 'string' &&
254
299
  startsHanded(part.text));
300
+ const own = partsOf(first).some((part) => part.pendingyou === 'wake')
301
+ ? ''
302
+ : partsOf(first)
303
+ .flatMap((part) => part.type === 'text' &&
304
+ part.synthetic !== true &&
305
+ part.pendingyou === undefined &&
306
+ typeof part.text === 'string'
307
+ ? [part.text]
308
+ : [])
309
+ .join('\n');
310
+ const asked = !handed && asksQuestion(own);
255
311
  const answers = rest.filter((message) => message.role === 'assistant');
256
312
  if (answers.at(-1)?.error === 'MessageAbortedError')
257
313
  return null;
@@ -273,7 +329,7 @@ function opencodeTurn(input) {
273
329
  final.push(part.text);
274
330
  }
275
331
  const text = final.join('\n').trim();
276
- return text ? { text, posted, cards, handed } : null;
332
+ return text ? { text, posted, cards, handed, asked } : null;
277
333
  }
278
334
  async function openTitles(io, origin, cwd, app, session) {
279
335
  try {
@@ -305,11 +361,16 @@ async function turnOf(io, input, app) {
305
361
  const record = await turnRecord(io, input.turn_id, app);
306
362
  if (record?.nudged)
307
363
  return null;
364
+ const handed = Boolean(record?.delegated) || input.handed === true;
308
365
  return {
309
366
  text: final,
310
367
  posted: Boolean(record?.cards.length),
311
368
  cards: record?.cards ?? [],
312
- handed: Boolean(record?.delegated) || input.handed === true,
369
+ handed,
370
+ asked: !handed &&
371
+ (app === 'codex'
372
+ ? record?.asked === true
373
+ : typeof input.prompt === 'string' && asksQuestion(input.prompt)),
313
374
  };
314
375
  }
315
376
  if (app === 'opencode') {
@@ -326,6 +387,7 @@ async function turnOf(io, input, app) {
326
387
  posted: turn.posted || Boolean(record?.cards.length),
327
388
  cards: [...turn.cards, ...(record?.cards ?? [])],
328
389
  handed: turn.handed,
390
+ asked: turn.asked,
329
391
  };
330
392
  }
331
393
  if (typeof input.transcript_path !== 'string' || !input.transcript_path)
@@ -334,8 +396,11 @@ async function turnOf(io, input, app) {
334
396
  if (!turn.complete)
335
397
  return null;
336
398
  const text = final || turn.text;
337
- return text ? { text, posted: turn.posted, cards: turn.cards, handed: turn.handed } : null;
399
+ return text
400
+ ? { text, posted: turn.posted, cards: turn.cards, handed: turn.handed, asked: turn.asked }
401
+ : null;
338
402
  }
403
+ const seesAnswers = (io, app) => app === 'codex' ? codexSeesAnswers(io).catch(() => false) : Promise.resolve(true);
339
404
  export async function stopcheck(io, options) {
340
405
  const app = options.app ?? DEFAULT_APP;
341
406
  try {
@@ -383,7 +448,11 @@ export async function stopcheck(io, options) {
383
448
  return 0;
384
449
  return block(confirmReason(ask.words, sessionLink(io.env), APP_NAMES[app]));
385
450
  }
386
- if (turn.posted || saysPosted(text) || saysNoCard(text) || !itemsOutsideRecaps(text))
451
+ if (turn.posted || saysPosted(text) || saysNoCard(text))
452
+ return 0;
453
+ if (turn.asked && isAnswer(text) && (await seesAnswers(io, app)))
454
+ return block(ANSWER_REASON);
455
+ if (!itemsOutsideRecaps(text))
387
456
  return 0;
388
457
  if (mentionsCards(text)) {
389
458
  const open = await openTitles(io, options.origin, cwd, app, session);
package/dist/version.js CHANGED
@@ -1 +1 @@
1
- export const VERSION = '0.35.0';
1
+ export const VERSION = '0.36.0';
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "pendingyou-wake",
3
3
  "displayName": "Pending You",
4
- "version": "2.50.0",
4
+ "version": "2.54.0",
5
5
  "description": "Wakes Claude Code when your person answers a Pending You card it posted, even while it sits idle. Installed by `npx pendingyou init`.",
6
6
  "author": {
7
7
  "name": "RecordPlane",
package/mod/README.md CHANGED
@@ -4,4 +4,4 @@ The pendingyou command line’s copy of the Pending You plugin’s wake mod, for
4
4
 
5
5
  In each session it learns the cards the session posts to Pending You, checks them through the session’s own Pending You connection, and starts a turn when your person answers, writes back or a fallback comes due, or hands the session a question from another of their assistants (CLI 0.14.0), even while it sits idle. It answers `npx pendingyou hold` itself, so nothing waits in the background, and shows “2 waiting on you” under the prompt, after the plugin’s name. With the Pending You plugin installed too, only one copy acts.
6
6
 
7
- Generated from recordplane/pendingyou (packages/claude-plugin/src/wake) with Pending You’s guide 2.50.0; send changes there.
7
+ Generated from recordplane/pendingyou (packages/claude-plugin/src/wake) with Pending You’s guide 2.54.0; send changes there.
@@ -209,13 +209,13 @@ async function deliverHanded($) {
209
209
  view = handedView(resultObject(await $.mcp.call(server, 'get_request', args)), handed, name);
210
210
  }
211
211
  catch { }
212
- const claim = await $.store.get(key);
212
+ const stored = await $.store.get(key);
213
213
  if (!view) {
214
- if (claimHeld(claim, handed.updatedAt, me))
214
+ if (claimHeld(stored, handed.updatedAt, me))
215
215
  await $.store.delete(key);
216
216
  continue;
217
217
  }
218
- if (!claimHeld(claim, handed.updatedAt, me)) {
218
+ if (!claimHeld(stored, handed.updatedAt, me)) {
219
219
  live.handedTold.set(handed.requestId, handed.updatedAt);
220
220
  continue;
221
221
  }
package/package.json CHANGED
@@ -1,7 +1,17 @@
1
1
  {
2
2
  "name": "pendingyou",
3
- "version": "0.35.0",
3
+ "version": "0.36.0",
4
4
  "description": "Pending You for your coding agents: one command sets up Claude Code, Codex, OpenCode and Pi to ask you through Pending You and hear your answers, even while they sit idle.",
5
+ "keywords": [
6
+ "pendingyou",
7
+ "claude-code",
8
+ "codex",
9
+ "opencode",
10
+ "pi",
11
+ "mcp",
12
+ "human-in-the-loop",
13
+ "ai-agents"
14
+ ],
5
15
  "license": "Apache-2.0",
6
16
  "homepage": "https://www.pendingyou.com",
7
17
  "repository": {