pendingyou 0.3.0 → 0.5.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,29 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.5.0
4
+
5
+ - A card with several questions (guide 2.6) prints each answer on its own line when `hold` ends (and when a hook
6
+ hands the answer over): `1. Pay Kaiser’s invoice 09/30 for $1,200? (pay): Approved`, numbered in the card's order,
7
+ with the question's id as `answer.answers` has it. A question your person split out into a card of its own is named
8
+ with its new request id: hear its answer there.
9
+ - When an answer is ready, the line telling Claude Code to call `ack_answer` (or `reply_in_thread`) now names the
10
+ session too (`name “billing-webhooks”`): since guide 2.5 the sessions sharing a computer's sign-in each act only on
11
+ their own cards, and pass their name to do so.
12
+ - The next-message hook's open-cards line names the session that asked each card (`(req_…, by dns-setup)`, from
13
+ servers on guide 2.5, cut to 24 characters) and says to cancel only one of yours, with `answeredHere` and your name.
14
+
15
+ ## 0.4.0
16
+
17
+ - The next-message hook's reminder now names this folder's open cards still waiting on you, so a question you answer
18
+ in chat gets its card cancelled: `Pending You: anything you need from your person also goes on a card. Your open
19
+ cards here: "Which DNS host?" (req_…); … +2 more. If this message answers or settles one, cancel_request it with
20
+ answeredHere (or update_request if it changed).` At most 3 cards, newest first, titles cut to 50 characters; the
21
+ plain reminder when none are open. One small request per message while signed in (answers too while a hold is
22
+ outstanding), given up after 1.5 seconds; on any error, or against a server that doesn't list open cards yet, the
23
+ line is the plain reminder.
24
+ - The Stop check no longer asks Claude Code to go on when its message says a card is coming ("you'll get a card to
25
+ publish it once it's merged", "I'll post a card", "I'll send you a card").
26
+
3
27
  ## 0.3.0
4
28
 
5
29
  - New Stop hook, `pendingyou stopcheck`: when Claude Code ends a turn with a question for you, a step only you can do
package/README.md CHANGED
@@ -28,9 +28,13 @@ Deep into a long conversation, Claude Code tends to end a turn with a question f
28
28
  "Your part:" list, and not put it in Pending You. Two small things catch that while you're signed in:
29
29
 
30
30
  - Each message you send carries one line for Claude Code (about 18 tokens): *Pending You: anything you need from your
31
- person also goes on a card.*
31
+ person also goes on a card.* While cards Claude Code posted from this folder are still waiting on you, the same line
32
+ names them (at most 3, newest first, about 20 tokens each) and asks it to `cancel_request` one with `answeredHere`
33
+ when your message answers it, so a question you answer in chat doesn't stay in your queue. That costs one small
34
+ request per message, given up after 1.5 seconds.
32
35
  - When Claude Code finishes a turn, `pendingyou stopcheck` looks at its last message. If it has something for you and
33
- nothing was posted, updated or replied to in Pending You that turn (and the message doesn't say it's on a card),
36
+ nothing was posted, updated or replied to in Pending You that turn (and the message doesn't say it's on a card, or
37
+ that one is coming),
34
38
  Claude Code is asked to go on once (about 31 tokens): post them as cards, or say they don't need one. It never asks
35
39
  twice in a row.
36
40
 
package/dist/format.js CHANGED
@@ -4,8 +4,11 @@
4
4
  const MAX_ANSWER = 600;
5
5
  const MAX_MESSAGE = 400;
6
6
  const MAX_TITLE = 140;
7
+ /** A grouped card's question and its answer, each: four of them stay well under one long answer. */
8
+ const MAX_QUESTION = 100;
9
+ const MAX_QUESTION_ANSWER = 300;
7
10
  /** Cuts text to `max` characters on one line, and says so. */
8
- function clip(text, max) {
11
+ export function clip(text, max) {
9
12
  const flat = text.replace(/\s+/g, ' ').trim();
10
13
  const chars = [...flat];
11
14
  if (chars.length <= max)
@@ -22,6 +25,11 @@ const quoted = (text, max) => {
22
25
  const { text: clipped, cut } = clip(text, max);
23
26
  return { line: `“${clipped}”`, cut };
24
27
  };
28
+ /**
29
+ * The session's name, as it asked (its session label): the card tools take it, and need it where other sessions share
30
+ * the sign-in (guide 2.5).
31
+ */
32
+ const named = (heard) => `“${clip(heard.sessionLabel, MAX_TITLE).text}”`;
25
33
  /** Closed for good: nothing more to wait for. */
26
34
  export const isClosed = (heard) => heard.status === 'answered' ||
27
35
  heard.status === 'resolved' ||
@@ -40,12 +48,24 @@ export function describe(heard, again) {
40
48
  const lines = [];
41
49
  switch (heard.status) {
42
50
  case 'answered': {
43
- const answer = clip(heard.said ?? 'Answered', MAX_ANSWER);
44
- cut ||= answer.cut;
45
- lines.push(`Pending You: ${id} is answered.`, question, `Answer: ${answer.text}`);
51
+ if (heard.answers?.length) {
52
+ // One line per question, so each answer reads against its own question (and its id, as answer.answers has it).
53
+ lines.push(`Pending You: ${id} is answered, all ${heard.answers.length} questions.`, question);
54
+ for (const [index, each] of heard.answers.entries()) {
55
+ const asked = clip(each.title, MAX_QUESTION);
56
+ const answer = clip(each.said, MAX_QUESTION_ANSWER);
57
+ cut ||= asked.cut || answer.cut;
58
+ lines.push(`${index + 1}. ${asked.text} (${each.questionId}): ${answer.text}`);
59
+ }
60
+ }
61
+ else {
62
+ const answer = clip(heard.said ?? 'Answered', MAX_ANSWER);
63
+ cut ||= answer.cut;
64
+ lines.push(`Pending You: ${id} is answered.`, question, `Answer: ${answer.text}`);
65
+ }
46
66
  if (wrote.length)
47
67
  lines.push(`They also wrote: ${wrote.join(' ')}`);
48
- lines.push(`Act on it, then call ack_answer with requestId ${id}, expectedVersion ${heard.version} and a one-line outcome.`);
68
+ lines.push(`Act on it, then call ack_answer with requestId ${id}, name ${named(heard)}, expectedVersion ${heard.version} and a one-line outcome.`);
49
69
  break;
50
70
  }
51
71
  case 'expired':
@@ -59,12 +79,17 @@ export function describe(heard, again) {
59
79
  break;
60
80
  default:
61
81
  if (heard.turn === 'agent') {
62
- lines.push(`Pending You: they wrote back on ${id}.`, question, `They wrote: ${wrote.join(' ') || '(a follow-up; call get_request to read it)'}`, `Answer with reply_in_thread (requestId ${id}), then ${again}.`);
82
+ lines.push(`Pending You: they wrote back on ${id}.`, question, `They wrote: ${wrote.join(' ') || '(a follow-up; call get_request to read it)'}`, `Answer with reply_in_thread (requestId ${id}, name ${named(heard)}), then ${again}.`);
63
83
  }
64
84
  else {
65
85
  lines.push(`Pending You: ${id} is still waiting for them (${heard.status}).`, question);
66
86
  }
67
87
  }
88
+ // Questions split out of a grouped card are cards of their own now, with answers of their own.
89
+ if (heard.splits?.length && heard.status !== 'cancelled' && heard.status !== 'resolved')
90
+ lines.push(`They split ${heard.splits
91
+ .map((split) => `“${clip(split.title, MAX_QUESTION).text}” (${split.requestId})`)
92
+ .join(', ')} out into ${heard.splits.length === 1 ? 'a card of its own' : 'cards of their own'}: hear ${heard.splits.length === 1 ? 'its answer' : 'their answers'} there.`);
68
93
  if (cut)
69
94
  lines.push(`(Cut short: get_request ${id} has all of it.)`);
70
95
  return lines;
package/dist/pickup.js CHANGED
@@ -3,19 +3,54 @@
3
3
  // - `pendingyou pickup` (SessionStart): a new or resumed session picks up answers that arrived while nothing was
4
4
  // running, for questions asked from this folder.
5
5
  // - `pendingyou handoff` (UserPromptSubmit): your next message brings a waiting answer with it ("You answered in
6
- // Pending You: …"). It only calls Pending You while a hold from this computer is outstanding, so an ordinary message
7
- // costs no network call. While signed in, it also adds one short line (REMINDER) to every message, so an assistant
8
- // deep in a long conversation still puts what it needs from the person on a card.
6
+ // Pending You: …"). While signed in, it also adds one short line (REMINDER) to every message, so an assistant
7
+ // deep in a long conversation still puts what it needs from the person on a card, and that line names this folder's
8
+ // cards still waiting on the person (openLine), so one they answer in chat gets cancelled with answeredHere. That's
9
+ // one small request per message (the open cards, and answers too while a hold from this computer is outstanding),
10
+ // time-boxed short; on any error, or from a server too old to list open cards, the line is the plain reminder.
9
11
  //
10
12
  // Whatever they print, Claude Code adds to the conversation. A hook must never get in the way: each is time-boxed,
11
13
  // prints nothing when there's nothing new, never repeats an answer it handed over, and always exits 0.
12
14
  import { getJson, SignInNeeded } from "./api.js";
13
15
  import { readCredential } from "./credentials.js";
14
- import { describe, isClosed } from "./format.js";
16
+ import { clip, describe, isClosed } from "./format.js";
15
17
  import { markHanded, momentOf, waitingOn, wasHanded } from "./state.js";
16
- const TIME_BOX_MS = { pickup: 5000, handoff: 2500 };
18
+ const TIME_BOX_MS = { pickup: 5000, handoff: 2500, open: 1500 };
17
19
  /** What every message carries while signed in (about 18 tokens). */
18
20
  export const REMINDER = 'Pending You: anything you need from your person also goes on a card.';
21
+ /** The most open cards the line names, and how much of each title. */
22
+ export const OPEN_SHOWN = 3;
23
+ export const OPEN_TITLE = 50;
24
+ /** How much of the asking session's name each card shows. */
25
+ export const OPEN_NAME = 24;
26
+ /**
27
+ * The reminder, naming this folder's open cards still waiting on the person when there are any (about 45 tokens plus
28
+ * 20 a card): `… Your open cards here: "Which DNS host?" (req_…); … +2 more. If this message answers …`.
29
+ */
30
+ export function openLine(open) {
31
+ if (typeof open !== 'object' || open === null)
32
+ return REMINDER;
33
+ const { requests, total } = open;
34
+ if (!Array.isArray(requests))
35
+ return REMINDER;
36
+ const cards = requests
37
+ .filter((card) => typeof card?.requestId === 'string' &&
38
+ /^req_[A-Za-z0-9-]{1,40}$/.test(card.requestId) &&
39
+ typeof card.title === 'string')
40
+ .slice(0, OPEN_SHOWN);
41
+ if (cards.length === 0)
42
+ return REMINDER;
43
+ const more = typeof total === 'number' && total > cards.length ? total - cards.length : 0;
44
+ const named = cards
45
+ .map((card) => {
46
+ const by = typeof card.name === 'string' && card.name.trim()
47
+ ? `, by ${clip(card.name.replaceAll('"', "'"), OPEN_NAME).text}`
48
+ : '';
49
+ return `"${clip(card.title.replaceAll('"', "'"), OPEN_TITLE).text}" (${card.requestId}${by})`;
50
+ })
51
+ .join('; ');
52
+ return `${REMINDER} Your open cards here: ${named}${more ? ` +${more} more` : ''}. If this message answers or settles one of yours, cancel_request it with answeredHere and your name (or update_request if it changed).`;
53
+ }
19
54
  /** The folder Claude Code says the session is in (the hook's stdin), or where this runs. */
20
55
  async function sessionFolder(io) {
21
56
  const text = await io.readStdin(500).catch(() => '');
@@ -38,19 +73,28 @@ export function folderForms(cwd, home) {
38
73
  export async function pickup(io, options) {
39
74
  const { origin, mode } = options;
40
75
  let remind = false;
76
+ let line = REMINDER;
41
77
  try {
42
78
  const cwd = await sessionFolder(io);
79
+ // The hand-off hears answers only while a hold from this computer is outstanding; it always asks for open cards.
80
+ let answers = true;
43
81
  if (mode === 'handoff') {
44
82
  remind = (await readCredential(io, origin)) !== null;
45
- if ((await waitingOn(io, origin)).length === 0)
83
+ if (!remind)
46
84
  return 0;
85
+ answers = (await waitingOn(io, origin)).length > 0;
47
86
  }
48
87
  const query = new URLSearchParams([
49
88
  ...folderForms(cwd, io.home).map((form) => ['cwd', form]),
50
- ['limit', '5'],
89
+ ['limit', answers ? '5' : '0'],
90
+ ...(mode === 'handoff' ? [['open', String(OPEN_SHOWN)]] : []),
51
91
  ]);
52
- const { status, body } = await getJson(io, origin, `/mcp/cli/answers?${query}`, TIME_BOX_MS[mode], io.signal);
53
- if (status !== 200 || !Array.isArray(body.requests))
92
+ const { status, body } = await getJson(io, origin, `/mcp/cli/answers?${query}`, answers ? TIME_BOX_MS[mode] : TIME_BOX_MS.open, io.signal);
93
+ if (status !== 200)
94
+ return 0;
95
+ if (mode === 'handoff')
96
+ line = openLine(body.open);
97
+ if (!answers || !Array.isArray(body.requests))
54
98
  return 0;
55
99
  const fresh = [];
56
100
  for (const heard of body.requests)
@@ -83,6 +127,6 @@ export async function pickup(io, options) {
83
127
  }
84
128
  finally {
85
129
  if (remind)
86
- io.out(`${REMINDER}\n`);
130
+ io.out(`${line}\n`);
87
131
  }
88
132
  }
package/dist/stopcheck.js CHANGED
@@ -14,8 +14,11 @@ const CHUNK = 64 * 1024;
14
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.';
15
15
  /** Tools that put something in front of the person: Pending You's, under any MCP server name. */
16
16
  const POSTING_TOOL = /^mcp__.+__(post_request|update_request|reply_in_thread)$/;
17
- /** Text that says the items are already in Pending You. */
18
- const SAYS_POSTED = /\b(posted|put|added|raised|filed)\b[^.\n]{0,60}\b(cards?|pending ?you)\b|\bon (a|the|its|their|your|that|this) cards?\b|\b(in|to|through|via|on) pending ?you\b|\bpending ?you cards?\b/i;
17
+ /**
18
+ * Text that says the items are already in Pending You, or will be: "you'll get a card to publish it once it's merged"
19
+ * announces a card to come, not a step left in chat.
20
+ */
21
+ const SAYS_POSTED = /\b(posted|put|added|raised|filed)\b[^.\n]{0,60}\b(cards?|pending ?you)\b|\bon (a|the|its|their|your|that|this) cards?\b|\b(in|to|through|via|on) pending ?you\b|\bpending ?you cards?\b|\b(i|we|you)(?:(?:'|’)ll| will)\s+(?:\w+\s+)?(post|send|put|add|raise|file|get|see|find)\b[^.\n]{0,40}\bcards?\b/i;
19
22
  /** Steps only the person can take, said to them. */
20
23
  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|waiting on you|waiting for you|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;
21
24
  /** A heading over a list of things for the person. */
package/dist/version.js CHANGED
@@ -1,2 +1,2 @@
1
1
  // The command line's version, as published (package.json). A test keeps them the same.
2
- export const VERSION = '0.3.0';
2
+ export const VERSION = '0.5.0';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pendingyou",
3
- "version": "0.3.0",
3
+ "version": "0.5.0",
4
4
  "description": "Pending You for Claude Code: hear your answers from Pending You in the background, and set Claude Code up in one command.",
5
5
  "license": "Apache-2.0",
6
6
  "homepage": "https://www.pendingyou.com",