pendingyou 0.35.1 → 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 +21 -1
- package/README.md +22 -0
- package/dist/apps/codex-wake.js +19 -2
- package/dist/apps/codex.js +18 -1
- package/dist/apps/opencode-plugin.js +1 -1
- package/dist/apps/pi-extension.js +6 -2
- package/dist/asked.js +102 -0
- package/dist/permission.js +17 -5
- package/dist/pickup.js +49 -14
- package/dist/sdk/client.js +1 -0
- package/dist/stopcheck.js +80 -11
- package/dist/version.js +1 -1
- package/mod/.claude-plugin/plugin.json +1 -1
- package/mod/README.md +1 -1
- package/mod/hooks/register.js +3 -3
- package/package.json +11 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,6 +1,26 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
-
## 0.
|
|
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.
|
|
4
24
|
|
|
5
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
|
|
6
26
|
a question of such a kind). It shows the card and says "Answer this one in Pending You: o opens it."; reply, Later,
|
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
|
|
package/dist/apps/codex-wake.js
CHANGED
|
@@ -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
|
}
|
package/dist/apps/codex.js
CHANGED
|
@@ -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 (
|
|
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
|
+
}
|
package/dist/permission.js
CHANGED
|
@@ -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.*
|
|
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
|
|
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 {
|
|
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
|
|
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
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
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
|
-
...
|
|
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
|
}
|
package/dist/sdk/client.js
CHANGED
|
@@ -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
|
|
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
|
-
|
|
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 {
|
|
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
|
|
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
|
|
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)
|
|
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.
|
|
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.
|
|
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.
|
|
7
|
+
Generated from recordplane/pendingyou (packages/claude-plugin/src/wake) with Pending You’s guide 2.54.0; send changes there.
|
package/mod/hooks/register.js
CHANGED
|
@@ -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
|
|
212
|
+
const stored = await $.store.get(key);
|
|
213
213
|
if (!view) {
|
|
214
|
-
if (claimHeld(
|
|
214
|
+
if (claimHeld(stored, handed.updatedAt, me))
|
|
215
215
|
await $.store.delete(key);
|
|
216
216
|
continue;
|
|
217
217
|
}
|
|
218
|
-
if (!claimHeld(
|
|
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.
|
|
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": {
|