pendingyou 0.33.0 → 0.34.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,16 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.34.0 (2026-10-09)
4
+
5
+ - Codex can ask on your phone first. `npx pendingyou codex-answers --wait <minutes>` (0 to 10; 0, the default, is off)
6
+ has Codex's permission prompts go to your Pending You card at once, with **Allow** and **Deny** and the whole command
7
+ or patch, its secrets masked. Codex shows nothing while its hook waits, so tap one and Codex goes on, or is refused
8
+ (“Denied in Pending You.”); with no answer within the wait, the card is withdrawn and Codex asks in its terminal as
9
+ usual. It sets the PermissionRequest hook's timeout in Codex's hooks.json to the wait plus 30 seconds (Codex sets no
10
+ upper limit for that hook), which changes the hook: trust it again in Codex's `/hooks`, as the command says. `init`
11
+ names the setting when it sets Codex up and keeps it, `status` shows it, and `uninstall` clears it. Needs Pending You
12
+ with `permissionPrompt` (0.33.0's server).
13
+
3
14
  ## 0.33.0 (2026-10-09)
4
15
 
5
16
  - Answer Claude Code's permission prompts on their Pending You card. Start Claude Code with `npx pendingyou claude
package/README.md CHANGED
@@ -289,6 +289,25 @@ each start (`pendingyou claude` runs `claude --dangerously-load-development-chan
289
289
  It needs Claude Code 2.1.234 or later and the permission cards on; a session started with plain `claude` gets the card
290
290
  without buttons, as above.
291
291
 
292
+ ### Codex: ask on my phone first (0.34.0)
293
+
294
+ Codex shows nothing while its PermissionRequest hook runs, so its prompt can't be on the card and in the terminal at
295
+ once. Instead you can have it ask on your phone first:
296
+
297
+ ```sh
298
+ npx pendingyou codex-answers --wait 2
299
+ ```
300
+
301
+ Now when Codex needs your OK, a card goes up at once with **Allow** and **Deny** and the whole command or patch, secrets
302
+ masked: “Allow Codex (infra) to run a command?”. Tap one and Codex goes on (or is refused, told “Denied in Pending
303
+ You.”). With no answer within the wait, the card goes and Codex asks in its terminal as usual. The wait is 0 to 10
304
+ minutes, per computer; 0, the default, turns it off (the card without buttons, as above). `init` names it when it sets
305
+ Codex up, `status` shows it, and `uninstall` clears it.
306
+
307
+ It gives Codex's PermissionRequest hook the wait plus 30 seconds (630 seconds for 10 minutes; Codex sets no upper limit
308
+ for that hook). Codex trusts a hook by its whole definition, timeout included, so after changing the wait trust the hook
309
+ again: open Codex, type `/hooks`, and trust it. Allow is for that one call only; nothing is allowed after it.
310
+
292
311
  ## Which sessions are open
293
312
 
294
313
  Since 0.15.0 Pending You hears which of your agents' sessions are open right now, so it can say so and offer an open
@@ -517,6 +536,7 @@ its environment. A failing command is tried twice more, then skipped. `--once` s
517
536
  | `status` | What's set up for each agent; exits 0 when each hears answers right away (`--app`) |
518
537
  | `login` / `logout` | Sign an agent in again, or out (`--app`, `--device`, `--browser`, `--force`, `--name`, `--all`) |
519
538
  | `claude [args…]` | Claude Code with the permission channel: a prompt's card has Allow and Deny; `claude`'s own arguments after it |
539
+ | `codex-answers --wait <minutes>` | Codex's permission prompts go to your card first, with Allow and Deny, for up to that many minutes (0 to 10; 0, the default, is off) before Codex asks in its terminal |
520
540
  | `watch -- <command>` | Run a command each time an answer is ready (`--once`) |
521
541
  | `pickup` / `handoff` | The session-start and next-message hooks (`--app`) |
522
542
  | `stopcheck` | The Stop hook: items for you left only in chat (`--app`) |
@@ -2,9 +2,10 @@ import { createHash } from 'node:crypto';
2
2
  import { realpath, rm } from 'node:fs/promises';
3
3
  import { join } from 'node:path';
4
4
  import { removeSkill, saveSkill } from "../claude.js";
5
+ import { codexManifestPath, isAnswerWait, minutes, permissionTimeout } from "../codex-answers.js";
5
6
  import { readCredential } from "../credentials.js";
6
7
  import { configDir, readJson, readText, writeWhole } from "../files.js";
7
- import { isOurHook, lineKey, mergeHooks, originArgs, removeHooks, shWord, } from "../hooks.js";
8
+ import { findHook, isOurHook, lineKey, mergeHooks, originArgs, removeHooks, shWord, } from "../hooks.js";
8
9
  import { atLeast } from "../mod.js";
9
10
  import { removePresenceFiles } from "../presence.js";
10
11
  import { checkHelper, foundBy, headlessReason, helperCommand, helperPath, isOurHelper, } from "../remote.js";
@@ -47,6 +48,7 @@ export const CODEX_PERMISSION_HOOKS = [
47
48
  { event: 'PermissionRequest', sub: 'permission' },
48
49
  { event: 'PostToolUse', sub: 'permission-done' },
49
50
  ];
51
+ export const codexPermissionHooks = (wait) => CODEX_PERMISSION_HOOKS.map((spec) => spec.event === 'PermissionRequest' ? { ...spec, timeout: permissionTimeout(wait) } : spec);
50
52
  export const CODEX_PRESENCE_HOOKS = [
51
53
  { event: 'SessionEnd', sub: 'presence', timeout: 3 },
52
54
  ];
@@ -82,7 +84,7 @@ async function codexHome(io) {
82
84
  const configPath = async (io) => join(await codexHome(io), 'config.toml');
83
85
  const hooksPath = async (io) => join(await codexHome(io), 'hooks.json');
84
86
  export const codexSkillPath = (io) => join(io.home, '.agents', 'skills', 'pendingyou', 'SKILL.md');
85
- const manifestPath = (io) => join(configDir(io), 'codex.json');
87
+ const manifestPath = codexManifestPath;
86
88
  export const readCodexManifest = (io) => readJson(manifestPath(io)).catch(() => null);
87
89
  export async function codexServer(io) {
88
90
  const doc = await readText(await configPath(io)).catch(() => null);
@@ -264,6 +266,51 @@ function keepTheirTrust(config, file, before) {
264
266
  }
265
267
  return next;
266
268
  }
269
+ export function answerWaitWords(wait) {
270
+ return wait > 0
271
+ ? `Ask on your phone first: Codex’s permission prompts wait up to ${minutes(wait)} for Allow or Deny on their Pending You card, then Codex asks in its terminal (npx pendingyou codex-answers --wait 0 turns it off).`
272
+ : 'Ask on your phone first is off: Codex asks for your OK in its terminal at once. To have it put each permission prompt on your Pending You card first, with Allow and Deny, and wait up to 2 minutes for you there: npx pendingyou codex-answers --wait 2';
273
+ }
274
+ export async function setAnswerWait(io, options) {
275
+ const { wait } = options;
276
+ const manifest = await readCodexManifest(io);
277
+ if (!manifest) {
278
+ io.err('Codex isn’t set up on this computer yet: run npx pendingyou init --app codex, then this again.\n');
279
+ return 1;
280
+ }
281
+ const file = await readHooksFile(io);
282
+ if ('invalid' in file) {
283
+ io.err(`${file.invalid}\n`);
284
+ return 1;
285
+ }
286
+ const hooks = file.json.hooks && typeof file.json.hooks === 'object' && !Array.isArray(file.json.hooks)
287
+ ? file.json.hooks
288
+ : {};
289
+ const hook = findHook(hooks, 'PermissionRequest', 'permission');
290
+ if (manifest.permissionCards === false || typeof hook?.command !== 'string') {
291
+ io.err('Codex’s permission-prompt hooks aren’t installed here, so nothing can ask you first: run npx pendingyou init --app codex --permission-cards, then this again.\n');
292
+ return 1;
293
+ }
294
+ const spec = codexPermissionHooks(wait).find((each) => each.event === 'PermissionRequest');
295
+ const merged = mergeHooks(hooks, [spec], { [lineKey(spec)]: hook.command }, file.path);
296
+ if (merged.changed)
297
+ await writeWhole(file.path, `${JSON.stringify({ ...file.json, hooks: merged.hooks }, null, 2)}\n`);
298
+ const next = { ...manifest };
299
+ if (wait > 0)
300
+ next.answerWait = wait;
301
+ else
302
+ delete next.answerWait;
303
+ await writeWhole(manifestPath(io), `${JSON.stringify(next, null, 2)}\n`, { secret: true });
304
+ io.out(wait > 0
305
+ ? `Codex’s permission prompts now go to your Pending You card first, with Allow and Deny, and wait up to ${minutes(wait)} for you there; with no answer by then, Codex asks in its terminal as usual. Codex shows nothing while it waits.\n`
306
+ : 'Turned off: Codex asks for your OK in its terminal at once again, and its card only says so.\n');
307
+ if (!merged.changed) {
308
+ io.out('That was already the setting.\n');
309
+ return 0;
310
+ }
311
+ io.out(`\nGave Codex’s PermissionRequest hook ${spec.timeout} seconds in ${file.path}: a changed hook, which Codex skips until you trust it again.\n${trustSteps(await findCodex(io)).join('\n')}\n`);
312
+ return 0;
313
+ }
267
314
  export const codex = {
268
315
  id: 'codex',
269
316
  name: NAME,
@@ -418,12 +465,14 @@ export const codex = {
418
465
  report(skill.step);
419
466
  const cards = ctx.permissionCards ?? before?.permissionCards !== false;
420
467
  const extra = mode === 'helper' && ictx.helper !== null;
468
+ const wait = isAnswerWait(before?.answerWait) ? before.answerWait : 0;
421
469
  const wanted = [
422
470
  ...CODEX_HOOKS,
423
- ...(extra && cards ? CODEX_PERMISSION_HOOKS : []),
471
+ ...(extra && cards ? codexPermissionHooks(wait) : []),
424
472
  ...(extra ? CODEX_PRESENCE_HOOKS : []),
425
473
  ];
426
- const unwanted = ALL_CODEX_HOOKS.filter((spec) => !wanted.includes(spec));
474
+ const wantedKeys = new Set(wanted.map(lineKey));
475
+ const unwanted = ALL_CODEX_HOOKS.filter((spec) => !wantedKeys.has(lineKey(spec)));
427
476
  const lines = Object.fromEntries(wanted.map((spec) => [lineKey(spec), ictx.hookLine(spec.sub)]));
428
477
  const file = await readHooksFile(io);
429
478
  if ('invalid' in file)
@@ -453,6 +502,8 @@ export const codex = {
453
502
  ? `Took out Codex’s permission-prompt hooks, as you asked: no card when Codex waits for your OK. To turn them on: npx pendingyou init --app codex --permission-cards${originArgs(ictx.origin)}`
454
503
  : 'Codex’s hooks were already installed.',
455
504
  });
505
+ if (extra && cards)
506
+ report({ ok: true, text: answerWaitWords(wait) });
456
507
  }
457
508
  catch (error) {
458
509
  report({
@@ -478,6 +529,7 @@ export const codex = {
478
529
  helper: mode === 'helper' ? ictx.helper : null,
479
530
  setup: { since: new Date(io.now()).toISOString() },
480
531
  permissionCards: cards,
532
+ ...(wait > 0 ? { answerWait: wait } : {}),
481
533
  };
482
534
  await writeWhole(manifestPath(io), `${JSON.stringify(manifest, null, 2)}\n`, {
483
535
  secret: true,
@@ -630,6 +682,12 @@ export const codex = {
630
682
  : manifest?.permissionCards === false
631
683
  ? `off, as you chose; for a card when Codex waits for your OK, run npx pendingyou init --app codex --permission-cards${flag}`
632
684
  : null);
685
+ const wait = isAnswerWait(manifest?.answerWait) ? manifest.answerWait : 0;
686
+ const askFirst = ownConnection && manifest?.permissionCards !== false
687
+ ? ` Ask on your phone first: ${wait > 0
688
+ ? `${minutes(wait)}: a permission prompt waits that long for Allow or Deny on its card, then Codex asks in its terminal (npx pendingyou codex-answers --wait 0 turns it off)`
689
+ : 'off: Codex asks in its terminal at once (npx pendingyou codex-answers --wait 2 sends its prompts to your card first, for up to 2 minutes)'}`
690
+ : null;
633
691
  const presenceText = extraLine('Presence:', 'tells Pending You when this session is open', CODEX_PRESENCE_HOOKS.map((spec) => spec.sub), ownConnection ? null : notOwn);
634
692
  const wakes = detected
635
693
  ? atLeast(detected.version, detected.command ? CODEX_APP_WAKE : CODEX_WAKE)
@@ -649,6 +707,7 @@ export const codex = {
649
707
  ` ${mark(skill !== null)} Skill: ${skill !== null ? `saved (${codexSkillPath(io)})` : `not saved; run npx pendingyou init --app codex${flag}`}`,
650
708
  ` ${mark(hooksOk)} Hooks: ${hooksText}`,
651
709
  permissionText,
710
+ ...(askFirst ? [askFirst] : []),
652
711
  presenceText,
653
712
  ` ${mark(wakes)} Wake: ${wakes
654
713
  ? detected?.command
package/dist/args.js CHANGED
@@ -1,4 +1,5 @@
1
1
  import { APP_IDS, APP_NAMES, DEFAULT_APP, isAppId } from "./apps/ids.js";
2
+ import { ANSWER_WAIT_MAX } from "./codex-answers.js";
2
3
  export const DEFAULT_ORIGIN = 'https://www.pendingyou.com';
3
4
  export const DEFAULT_TIMEOUT_MS = 4 * 60 * 60 * 1000;
4
5
  const MAX_TIMEOUT_MS = 24 * 60 * 60 * 1000;
@@ -86,6 +87,7 @@ const COMMANDS = new Set([
86
87
  'app',
87
88
  'channel',
88
89
  'claude',
90
+ 'codex-answers',
89
91
  ]);
90
92
  const FLAGS = {
91
93
  help: {},
@@ -143,6 +145,7 @@ const FLAGS = {
143
145
  mcp: { origin: true, app: true, check: false },
144
146
  channel: { origin: true, app: true },
145
147
  claude: {},
148
+ 'codex-answers': { origin: true, wait: true },
146
149
  herdr: {
147
150
  origin: true,
148
151
  app: true,
@@ -454,6 +457,13 @@ export function parseArgs(argv, env = {}) {
454
457
  ...apps(),
455
458
  ...permissionCards(),
456
459
  };
460
+ case 'codex-answers': {
461
+ none();
462
+ const given = text('wait');
463
+ if (given === undefined || !/^\d{1,2}$/.test(given) || Number(given) > ANSWER_WAIT_MAX)
464
+ throw new UsageError(`codex-answers needs --wait and a whole number of minutes, 0 (off) to ${ANSWER_WAIT_MAX}, like: pendingyou codex-answers --wait 2`);
465
+ return { name: 'codex-answers', wait: Number(given) };
466
+ }
457
467
  case 'watch':
458
468
  none();
459
469
  if (!command?.length || !command[0])
@@ -675,6 +685,10 @@ export const USAGE = `pendingyou: Pending You for your coding agents (Claude Cod
675
685
  Start Claude Code so its permission prompts can be answered on their
676
686
  Pending You cards: Allow or Deny there, or in the terminal; the first
677
687
  answer wins. Claude Code warns about the development channel each start
688
+ npx pendingyou codex-answers --wait <minutes>
689
+ Codex asks on your phone first: its permission prompts go to your
690
+ Pending You card, with Allow and Deny, and wait up to that many minutes
691
+ (0 to ${ANSWER_WAIT_MAX}; 0, the default, is off) before Codex asks in its terminal
678
692
  npx pendingyou watch -- <command> [args…]
679
693
  Run a command each time an answer is ready (always-on scripts)
680
694
  npx pendingyou uninstall Remove exactly what init added
@@ -0,0 +1,15 @@
1
+ import { join } from 'node:path';
2
+ import { configDir, readJson } from "./files.js";
3
+ import { HOOK_TIMEOUT } from "./hooks.js";
4
+ export const ANSWER_WAIT_MAX = 10;
5
+ export const ANSWER_MARGIN_S = 30;
6
+ export const ANSWER_DEADLINE_MARGIN_MS = 20_000;
7
+ export const codexManifestPath = (io) => join(configDir(io), 'codex.json');
8
+ export const isAnswerWait = (value) => typeof value === 'number' && Number.isInteger(value) && value >= 0 && value <= ANSWER_WAIT_MAX;
9
+ export async function readAnswerWait(io) {
10
+ const manifest = await readJson(codexManifestPath(io)).catch(() => null);
11
+ const wait = manifest?.answerWait;
12
+ return isAnswerWait(wait) ? wait : 0;
13
+ }
14
+ export const permissionTimeout = (wait) => wait > 0 ? wait * 60 + ANSWER_MARGIN_S : HOOK_TIMEOUT;
15
+ export const minutes = (wait) => `${wait} minute${wait === 1 ? '' : 's'}`;
package/dist/main.js CHANGED
@@ -1,4 +1,5 @@
1
1
  import { HOOK_DEADLINE_MS, refreshInBackground, SignInNeeded, Unavailable } from "./api.js";
2
+ import { setAnswerWait } from "./apps/codex.js";
2
3
  import { listen, listenHanded, posted } from "./apps/codex-wake.js";
3
4
  import { DEFAULT_APP } from "./apps/ids.js";
4
5
  import { appModule } from "./apps/registry.js";
@@ -6,6 +7,7 @@ import { commandOf, HOOK_COMMANDS, parseArgs, USAGE, UsageError } from "./args.j
6
7
  import { bridge } from "./bridge.js";
7
8
  import { channel } from "./channel.js";
8
9
  import { claudeWithChannel } from "./claude.js";
10
+ import { ANSWER_DEADLINE_MARGIN_MS, readAnswerWait } from "./codex-answers.js";
9
11
  import { PlainError } from "./errors.js";
10
12
  import { hold } from "./hold.js";
11
13
  import { init, status, uninstall } from "./init.js";
@@ -85,6 +87,8 @@ async function run(io, command, hook = { fallback: '' }) {
85
87
  return uninstall(io, command);
86
88
  case 'watch':
87
89
  return watch(io, command);
90
+ case 'codex-answers':
91
+ return setAnswerWait(io, command);
88
92
  case 'refresh':
89
93
  try {
90
94
  await refreshInBackground(io, command.origin, command.force, command.app ?? DEFAULT_APP);
@@ -108,6 +112,12 @@ async function run(io, command, hook = { fallback: '' }) {
108
112
  }
109
113
  }
110
114
  export { HOOK_DEADLINE_MS };
115
+ async function deadlineOf(io, command) {
116
+ if (command.name !== 'permission' || command.app !== 'codex')
117
+ return HOOK_DEADLINE_MS;
118
+ const wait = await readAnswerWait(io).catch(() => 0);
119
+ return wait > 0 ? wait * 60_000 + ANSWER_DEADLINE_MARGIN_MS : HOOK_DEADLINE_MS;
120
+ }
111
121
  async function runHook(io, command) {
112
122
  const hook = { fallback: '' };
113
123
  const controller = new AbortController();
@@ -127,9 +137,10 @@ async function runHook(io, command) {
127
137
  },
128
138
  signal: controller.signal,
129
139
  };
140
+ const limit = await deadlineOf(io, command);
130
141
  let timer;
131
142
  const deadline = new Promise((resolve) => {
132
- timer = setTimeout(() => resolve('deadline'), Math.max(0, HOOK_DEADLINE_MS - io.uptime()));
143
+ timer = setTimeout(() => resolve('deadline'), Math.max(0, limit - io.uptime()));
133
144
  });
134
145
  try {
135
146
  const outcome = await Promise.race([run(hookIo, command, hook), deadline]);
@@ -5,10 +5,11 @@ import { threadNames } from "./apps/codex-wake.js";
5
5
  import { APP_NAMES } from "./apps/ids.js";
6
6
  import { DEFAULT_ORIGIN } from "./args.js";
7
7
  import { McpFailure, mcpClient, SIGN_IN_ERROR, ToolRefused } from "./bridge.js";
8
+ import { minutes, readAnswerWait } from "./codex-answers.js";
8
9
  import { sessionLink } from "./confirm.js";
9
10
  import { connectionMachine, readCredential } from "./credentials.js";
10
11
  import { claudeDir, configDir, PERMISSION_FOLDER, readJson, withLock, writeWhole } from "./files.js";
11
- import { BLANK, cut, redactCommand, redactText, redactUrl, shortPath } from "./redact.js";
12
+ import { BLANK, cut, maskSecrets, redactCommand, redactText, redactUrl, shortPath, } from "./redact.js";
12
13
  import { machineOf } from "./remote.js";
13
14
  export const GRACE_MS = 10_000;
14
15
  export const TITLE_MAX = 90;
@@ -456,6 +457,11 @@ export async function permissionHook(io, options) {
456
457
  return 0;
457
458
  switch (input.hook_event_name) {
458
459
  case 'PermissionRequest':
460
+ if (app === 'codex') {
461
+ const wait = await readAnswerWait(io);
462
+ if (wait > 0 && (await askFirst(io, options.origin, input, wait)))
463
+ break;
464
+ }
459
465
  await asked(io, options.origin, input, app);
460
466
  break;
461
467
  case 'PostToolUse':
@@ -1353,3 +1359,216 @@ export async function removePermissionFiles(io) {
1353
1359
  await rm(folder, { recursive: true, force: true });
1354
1360
  return there;
1355
1361
  }
1362
+ export const ANSWER_READ_MS = 2000;
1363
+ const ANSWER_CALL_MS = 8000;
1364
+ const ANSWER_INPUT_MAX = 40_000;
1365
+ const ANSWER_LINES_MAX = 400;
1366
+ const ANSWER_LINE_MAX = 2000;
1367
+ export const DENIED = 'Denied in Pending You.';
1368
+ export function codexDecision(verdict) {
1369
+ const decision = verdict === 'allow' ? { behavior: 'allow' } : { behavior: 'deny', message: DENIED };
1370
+ return JSON.stringify({
1371
+ hookSpecificOutput: { hookEventName: 'PermissionRequest', decision },
1372
+ });
1373
+ }
1374
+ export function codexInputLines(tool, input) {
1375
+ const { description: _, ...rest } = input;
1376
+ const command = commandText(rest.command);
1377
+ const patch = command || text(rest.patch) || text(rest.input);
1378
+ let lines;
1379
+ let lang = 'text';
1380
+ if (tool === 'apply_patch' && patch)
1381
+ lines = patch.split('\n');
1382
+ else if (command) {
1383
+ lines = command.split('\n');
1384
+ lang = 'shell';
1385
+ }
1386
+ else
1387
+ lines = Object.entries(rest).flatMap(([key, value]) => `${key}: ${typeof value === 'string' ? value : JSON.stringify(value)}`.split('\n'));
1388
+ const kept = [];
1389
+ let size = 0;
1390
+ for (const line of lines) {
1391
+ if (kept.length >= ANSWER_LINES_MAX ||
1392
+ line.length > ANSWER_LINE_MAX ||
1393
+ size + line.length > ANSWER_INPUT_MAX) {
1394
+ kept.push('… (cut here: too long to show whole)');
1395
+ return { lines: kept, lang, cut: true };
1396
+ }
1397
+ kept.push(maskSecrets(line));
1398
+ size += line.length;
1399
+ }
1400
+ return { lines: kept.length ? kept : [''], lang, cut: false };
1401
+ }
1402
+ function tapOf(card, partial) {
1403
+ if (!['answered', 'resolved'].includes(text(card.status)))
1404
+ return null;
1405
+ const answer = isObject(card.answer) ? card.answer : null;
1406
+ const picked = Array.isArray(answer?.choiceIds) ? answer.choiceIds : [];
1407
+ if (picked.length !== 1)
1408
+ return 'terminal';
1409
+ if (picked[0] === DENY)
1410
+ return 'deny';
1411
+ if (picked[0] === ALLOW && !partial)
1412
+ return 'allow';
1413
+ return 'terminal';
1414
+ }
1415
+ async function askFirst(io, origin, input, wait) {
1416
+ if (io.platform === 'win32')
1417
+ return false;
1418
+ const where = whereOf(input);
1419
+ const tool = text(input.tool_name);
1420
+ if (!where || !tool)
1421
+ return false;
1422
+ if ((await readCredential(io, origin, 'codex'))?.kind !== 'connection')
1423
+ return false;
1424
+ const started = io.now();
1425
+ const toolInput = isObject(input.tool_input) ? input.tool_input : {};
1426
+ const cwd = text(input.cwd) || io.cwd;
1427
+ const known = await load(io, where.session).catch(() => null);
1428
+ const state = {
1429
+ version: 1,
1430
+ origin,
1431
+ app: 'codex',
1432
+ session: where.session,
1433
+ cwd,
1434
+ prompts: [],
1435
+ ...(known?.areaId && known.cwd === cwd ? { areaId: known.areaId } : {}),
1436
+ at: started,
1437
+ };
1438
+ const prompt = {
1439
+ key: promptKey(tool, keyInput('codex', toolInput)),
1440
+ tool: tool.slice(0, 100),
1441
+ ...promptWords(tool, toolInput, io.home, 'codex'),
1442
+ at: started,
1443
+ };
1444
+ const shown = codexInputLines(tool, toolInput);
1445
+ let client = null;
1446
+ let card;
1447
+ let machine;
1448
+ try {
1449
+ client = await mcpClient(io, origin, 'codex', ANSWER_CALL_MS);
1450
+ const name = await nameOf(io, state);
1451
+ await client.call('whoami', { name });
1452
+ const areaId = state.areaId ?? (await areaFor(io, client, state, name));
1453
+ machine = await computerOf(io, origin, 'codex');
1454
+ const title = relayTitle(state, prompt, name);
1455
+ const who = name === defaultName('codex') ? APP_NAMES.codex : `${APP_NAMES.codex} (${name})`;
1456
+ const wants = prompt.doing.startsWith('to ')
1457
+ ? `wants your OK ${prompt.doing}`
1458
+ : `wants your OK to use ${toolName(tool)}`;
1459
+ const reason = reasonOf('codex', toolInput);
1460
+ const why = reason ? ` It says: “${reason}”.` : '';
1461
+ const answer = shown.cut
1462
+ ? ' It’s too long to show whole, so only Deny is here; to allow it, choose “I’ll answer in Codex” and answer in its terminal.'
1463
+ : ` Allow or Deny here: Codex waits up to ${minutes(wait)} for you, showing nothing, then asks in its terminal.`;
1464
+ const session = createHash('sha256').update(where.session).digest('hex').slice(0, 24);
1465
+ const result = await client.call('post_request', {
1466
+ idempotencyKey: `codex-ask:${session}:${prompt.key}:${started}`,
1467
+ areaId,
1468
+ session: { label: name, machine, cwd: cut(tilde(io, cwd), 300) },
1469
+ kind: 'choice',
1470
+ intent: 'approve',
1471
+ title,
1472
+ summary: cut(`${who} in ${tilde(io, cwd)} on ${machine} ${wants}.${why}${answer}`, 400),
1473
+ options: shown.cut
1474
+ ? [
1475
+ { id: DENY, label: 'Deny' },
1476
+ { id: IN_TERMINAL, label: `I’ll answer in ${APP_NAMES.codex}` },
1477
+ ]
1478
+ : [
1479
+ { id: ALLOW, label: 'Allow', detail: 'This call only. Nothing is allowed after it.' },
1480
+ { id: DENY, label: 'Deny' },
1481
+ ],
1482
+ artifacts: [
1483
+ {
1484
+ kind: 'code',
1485
+ title: shown.lang === 'shell' ? 'The command' : `${toolName(tool)}’s input`,
1486
+ ref: `permission-${prompt.key.slice(0, 16)}`,
1487
+ lang: shown.lang,
1488
+ lines: shown.lines,
1489
+ },
1490
+ ],
1491
+ context: { ...contextOf(state, prompt), trace: [{ state: 'paused', text: title }] },
1492
+ blocking: false,
1493
+ urgency: 'now',
1494
+ permissionPrompt: { app: 'codex', relay: true },
1495
+ });
1496
+ const requestId = idOf(result, 'requestId', 'req_');
1497
+ if (!requestId)
1498
+ throw new McpFailure(-32000, 'Pending You posted no card.');
1499
+ card = {
1500
+ requestId,
1501
+ version: typeof result.version === 'number' ? result.version : 1,
1502
+ key: prompt.key,
1503
+ name,
1504
+ };
1505
+ }
1506
+ catch {
1507
+ await client?.close().catch(() => { });
1508
+ return false;
1509
+ }
1510
+ const pending = client;
1511
+ const read = () => pending.call('get_request', { requestId: card.requestId, name: card.name });
1512
+ const say = async (tap, now) => {
1513
+ if (tap !== 'terminal')
1514
+ io.out(`${codexDecision(tap)}\n`);
1515
+ if (now.status !== 'answered' || typeof now.version !== 'number')
1516
+ return;
1517
+ await pending
1518
+ .call('ack_answer', {
1519
+ requestId: card.requestId,
1520
+ name: card.name,
1521
+ expectedVersion: now.version,
1522
+ outcome: cut(tap === 'allow'
1523
+ ? `Allowed in Codex on ${machine}.`
1524
+ : tap === 'deny'
1525
+ ? `Denied in Codex on ${machine}.`
1526
+ : `Not allowed or denied here: Codex asks in its terminal on ${machine} now.`, 200),
1527
+ })
1528
+ .catch(() => { });
1529
+ };
1530
+ const until = started + wait * 60_000;
1531
+ try {
1532
+ while (!io.signal.aborted && io.now() < until) {
1533
+ await io.sleep(Math.min(ANSWER_READ_MS, until - io.now()), io.signal);
1534
+ if (io.signal.aborted)
1535
+ return true;
1536
+ let now;
1537
+ try {
1538
+ now = await read();
1539
+ }
1540
+ catch (error) {
1541
+ if (error instanceof McpFailure && error.code === SIGN_IN_ERROR)
1542
+ break;
1543
+ continue;
1544
+ }
1545
+ if (['pending', 'snoozed', 'delegated'].includes(text(now.status)))
1546
+ continue;
1547
+ const tap = tapOf(now, shown.cut);
1548
+ if (tap)
1549
+ await say(tap, now);
1550
+ return true;
1551
+ }
1552
+ if (io.signal.aborted)
1553
+ return true;
1554
+ try {
1555
+ await pending.call('cancel_request', {
1556
+ requestId: card.requestId,
1557
+ name: card.name,
1558
+ reason: cut(`No answer here in ${minutes(wait)}, so Codex is asking in its terminal on ${machine} now.`, 200),
1559
+ });
1560
+ }
1561
+ catch (error) {
1562
+ if (!(error instanceof ToolRefused))
1563
+ return true;
1564
+ const now = await read().catch(() => null);
1565
+ const tap = now ? tapOf(now, shown.cut) : null;
1566
+ if (now && tap)
1567
+ await say(tap, now);
1568
+ }
1569
+ return true;
1570
+ }
1571
+ finally {
1572
+ await pending.close().catch(() => { });
1573
+ }
1574
+ }
package/dist/version.js CHANGED
@@ -1 +1 @@
1
- export const VERSION = '0.33.0';
1
+ export const VERSION = '0.34.0';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pendingyou",
3
- "version": "0.33.0",
3
+ "version": "0.34.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
5
  "license": "Apache-2.0",
6
6
  "homepage": "https://www.pendingyou.com",