pendingyou 0.32.3 → 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.
@@ -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
- import { configDir, PERMISSION_FOLDER, readJson, withLock, writeWhole } from "./files.js";
11
- import { cut, redactCommand, redactText, redactUrl, shortPath } from "./redact.js";
11
+ import { claudeDir, configDir, PERMISSION_FOLDER, readJson, withLock, writeWhole } from "./files.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;
@@ -24,7 +25,9 @@ export const isPromptApp = (app) => PROMPT_APPS.includes(app);
24
25
  export const waitingFor = (app) => `${APP_NAMES[app]} is waiting for your OK`;
25
26
  export const isPermissionCard = (title) => typeof title === 'string' &&
26
27
  PROMPT_APPS.some((app) => title.startsWith(waitingFor(app)) ||
27
- title.startsWith(`${APP_NAMES[app]} is waiting for your answer`));
28
+ title.startsWith(`${APP_NAMES[app]} is waiting for your answer`) ||
29
+ title.startsWith(`Allow ${APP_NAMES[app]} `) ||
30
+ title.startsWith(`Allow a subagent of ${APP_NAMES[app]}`));
28
31
  export const DEFAULT_NAME = 'Claude Code';
29
32
  const defaultName = (app) => APP_NAMES[app];
30
33
  const LEASE_STALE_MS = 120_000;
@@ -172,7 +175,10 @@ async function change(io, session, edit) {
172
175
  });
173
176
  }
174
177
  const alive = (state, now) => Boolean(state.worker && now - state.worker.at < LEASE_STALE_MS);
175
- const seen = (prompt) => !prompt.quiet || prompt.shown !== undefined || prompt.waited !== undefined;
178
+ const seen = (prompt) => !prompt.quiet ||
179
+ prompt.shown !== undefined ||
180
+ prompt.waited !== undefined ||
181
+ prompt.relay !== undefined;
176
182
  const unchecked = (prompt) => !seen(prompt) && !prompt.notice;
177
183
  function claim(state, now) {
178
184
  if (alive(state, now))
@@ -217,6 +223,27 @@ function reasonOf(app, input) {
217
223
  return reason || null;
218
224
  }
219
225
  const wentOn = (agent, now) => (prompt) => Boolean(prompt.quiet) && (prompt.agent ?? '') === agent && prompt.at <= now - BATCH_MS;
226
+ function closeCards(state, settled, how) {
227
+ const cards = settled.flatMap((prompt) => (prompt.card ? [{ ...prompt.card, how }] : []));
228
+ if (cards.length)
229
+ state.closing = [...(state.closing ?? []), ...cards];
230
+ }
231
+ function keepNewest(state) {
232
+ closeCards(state, state.prompts.slice(0, -MAX_PROMPTS), 'answered');
233
+ state.prompts = state.prompts.slice(-MAX_PROMPTS);
234
+ }
235
+ const ADOPT_MS = 30_000;
236
+ function adopt(state, prompt) {
237
+ const own = state.prompts.find((each) => each.relay?.own &&
238
+ each.tool === prompt.tool &&
239
+ Math.abs(prompt.at - each.relay.at) <= ADOPT_MS);
240
+ if (!own?.relay)
241
+ return false;
242
+ const { at: _at, quiet: _quiet, ...hook } = prompt;
243
+ Object.assign(own, hook);
244
+ delete own.relay.own;
245
+ return true;
246
+ }
220
247
  async function asked(io, origin, input, app) {
221
248
  if (io.platform === 'win32')
222
249
  return;
@@ -273,10 +300,12 @@ async function asked(io, origin, input, app) {
273
300
  const kept = app === 'claude-code'
274
301
  ? next.prompts.filter((each) => !wentOn(agent, now)(each))
275
302
  : next.prompts;
303
+ closeCards(next, next.prompts.filter((each) => !kept.includes(each)), 'answered');
276
304
  next.prompts = kept;
277
- if (!next.prompts.some((each) => each.key === prompt.key))
305
+ const adopted = app === 'claude-code' ? adopt(next, prompt) : false;
306
+ if (!adopted && !next.prompts.some((each) => each.key === prompt.key))
278
307
  next.prompts.push(prompt);
279
- next.prompts = next.prompts.slice(-MAX_PROMPTS);
308
+ keepNewest(next);
280
309
  delete next.settled;
281
310
  next.at = now;
282
311
  worker = claim(next, now);
@@ -299,10 +328,11 @@ async function settle(io, session, which, how) {
299
328
  const left = state.prompts.filter((prompt) => !which(prompt));
300
329
  if (left.length === state.prompts.length)
301
330
  return state;
331
+ closeCards(state, state.prompts.filter((prompt) => !left.includes(prompt)), how);
302
332
  state.prompts = left;
303
333
  state.settled = how;
304
334
  state.at = now;
305
- if (!state.card && left.length === 0)
335
+ if (!state.card && !state.closing?.length && left.length === 0)
306
336
  return alive(state, now) ? state : null;
307
337
  worker = claim(state, now);
308
338
  return state;
@@ -322,7 +352,11 @@ async function ran(io, input, app) {
322
352
  return;
323
353
  const exact = before.prompts.some((prompt) => prompt.key === key);
324
354
  const movedOn = wentOn(agent, io.now());
325
- await settle(io, where.session, (prompt) => prompt.key === key || (app === 'codex' ? !exact && prompt.tool === tool : movedOn(prompt)), 'answered');
355
+ await settle(io, where.session, (prompt) => prompt.key === key ||
356
+ (app === 'codex'
357
+ ? !exact && prompt.tool === tool
358
+ : movedOn(prompt) ||
359
+ (!exact && Boolean(prompt.relay?.own) && prompt.tool === tool && !agent)), 'answered');
326
360
  }
327
361
  export async function endPrompts(io, session) {
328
362
  try {
@@ -423,6 +457,11 @@ export async function permissionHook(io, options) {
423
457
  return 0;
424
458
  switch (input.hook_event_name) {
425
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
+ }
426
465
  await asked(io, options.origin, input, app);
427
466
  break;
428
467
  case 'PostToolUse':
@@ -447,7 +486,7 @@ export async function permissionHook(io, options) {
447
486
  const dueOf = (prompt, grace) => Math.min(prompt.quiet ? (prompt.waited ?? Number.POSITIVE_INFINITY) : prompt.at + grace, prompt.shown ?? Number.POSITIVE_INFINITY);
448
487
  const shownOf = (prompts) => (prompts.filter((prompt) => !prompt.notice).at(-1) ?? prompts.at(-1));
449
488
  export function planFor(state, now, grace, quietGrace = QUIET_GRACE_MS) {
450
- const plan = cardPlan(state, now, grace);
489
+ const plan = relayPlan(state, now, grace, cardPlan(state, now, grace));
451
490
  const pending = state.prompts.filter(unchecked);
452
491
  if (pending.length === 0)
453
492
  return plan;
@@ -460,8 +499,26 @@ export function planFor(state, now, grace, quietGrace = QUIET_GRACE_MS) {
460
499
  return { kind: 'wait', ms: Math.min(plan.ms, check) };
461
500
  return plan;
462
501
  }
502
+ function relayPlan(state, now, grace, then) {
503
+ const closing = state.closing?.[0];
504
+ if (closing)
505
+ return { kind: 'close', card: closing };
506
+ const waiting = state.prompts.filter((prompt) => prompt.relay && !prompt.card && !prompt.failed);
507
+ if (waiting.length === 0)
508
+ return then;
509
+ const dueAt = (prompt) => Math.min(Math.min(prompt.at, prompt.relay.at) + grace, prompt.shown ?? Number.POSITIVE_INFINITY);
510
+ const first = waiting.reduce((soonest, prompt) => dueAt(prompt) < dueAt(soonest) ? prompt : soonest);
511
+ const due = dueAt(first) - now;
512
+ if (due <= 0)
513
+ return { kind: 'relay', prompt: first };
514
+ if (then.kind === 'done')
515
+ return { kind: 'wait', ms: due };
516
+ if (then.kind === 'wait')
517
+ return { kind: 'wait', ms: Math.min(then.ms, due) };
518
+ return then;
519
+ }
463
520
  function cardPlan(state, now, grace) {
464
- const prompts = state.prompts.filter(seen);
521
+ const prompts = state.prompts.filter((prompt) => seen(prompt) && !prompt.relay);
465
522
  if (prompts.length === 0)
466
523
  return state.card ? { kind: 'cancel' } : { kind: 'done' };
467
524
  if (state.card) {
@@ -481,7 +538,7 @@ function graceOf(env) {
481
538
  const given = Number(env.PENDINGYOU_PERMISSION_GRACE_MS);
482
539
  return Number.isInteger(given) && given >= 0 ? given : GRACE_MS;
483
540
  }
484
- async function computerOf(io, origin, app) {
541
+ export async function computerOf(io, origin, app) {
485
542
  const named = await connectionMachine(io, origin, app).catch(() => null);
486
543
  return cut(named ?? (machineOf(io.host) || 'this computer'), 60);
487
544
  }
@@ -873,6 +930,7 @@ async function post(io, client, state, prompt, worker) {
873
930
  blocking: false,
874
931
  urgency: 'now',
875
932
  askedFirst: { where: 'terminal', askedAt: new Date(prompt.at).toISOString() },
933
+ permissionPrompt: { app: appOf(state), relay: false },
876
934
  });
877
935
  const requestId = idOf(result, 'requestId', 'req_');
878
936
  const version = typeof result.version === 'number' ? result.version : 1;
@@ -886,6 +944,89 @@ async function post(io, client, state, prompt, worker) {
886
944
  if (!kept)
887
945
  await withdraw(io, client, card, state, 'answered').catch(() => { });
888
946
  }
947
+ export const ALLOW = 'allow';
948
+ export const DENY = 'deny';
949
+ export const IN_TERMINAL = 'terminal';
950
+ export function relayTitle(state, prompt, name) {
951
+ const app = appOf(state);
952
+ const who = name === defaultName(app) ? APP_NAMES[app] : `${APP_NAMES[app]} (${name})`;
953
+ const subject = byAgent(state, prompt) ? `a subagent of ${who}` : who;
954
+ const doing = prompt.doing.startsWith('to ')
955
+ ? prompt.doing
956
+ : `to use ${toolName(prompt.tool ?? '')}`;
957
+ return cut(`Allow ${subject} ${doing}?`, TITLE_MAX);
958
+ }
959
+ async function postRelay(io, client, state, prompt, worker) {
960
+ const relay = prompt.relay;
961
+ const app = appOf(state);
962
+ const name = await nameOf(io, state);
963
+ await client.call('whoami', { name });
964
+ const areaId = state.areaId ?? (await areaFor(io, client, state, name));
965
+ const machine = await computerOf(io, state.origin, app);
966
+ const title = relayTitle(state, prompt, name);
967
+ const who = name === defaultName(app) ? APP_NAMES[app] : `${APP_NAMES[app]} (${name})`;
968
+ const where = `in ${tilde(io, state.cwd)} on ${machine}`;
969
+ const asker = byAgent(state, prompt)
970
+ ? `A subagent${prompt.agentType ? ` (${prompt.agentType})` : ''} of ${who}, ${where},`
971
+ : `${who} ${where}`;
972
+ const wants = prompt.doing.startsWith('to ')
973
+ ? `wants your OK ${prompt.doing}`
974
+ : `wants your OK to use ${toolName(prompt.tool ?? '')}`;
975
+ const why = relay.description ? ` It says: “${relay.description}”.` : '';
976
+ const answer = relay.unserializable
977
+ ? `Part of it couldn’t be shown, so only Deny is here; to allow it, answer in ${APP_NAMES[app]} on ${machine}.`
978
+ : `Allow or Deny here, or answer in ${APP_NAMES[app]} on ${machine}: the first answer wins.`;
979
+ const session = createHash('sha256').update(state.session).digest('hex').slice(0, 24);
980
+ const result = await client.call('post_request', {
981
+ idempotencyKey: `${app}-relay:${session}:${relay.id}:${relay.at}`,
982
+ areaId,
983
+ session: { label: name, machine, cwd: cut(tilde(io, state.cwd), 300) },
984
+ kind: 'choice',
985
+ intent: 'approve',
986
+ title,
987
+ summary: cut(`${asker} ${wants}.${why} ${answer}`, 400),
988
+ options: relay.unserializable
989
+ ? [
990
+ { id: DENY, label: 'Deny' },
991
+ { id: IN_TERMINAL, label: `I’ll answer in ${APP_NAMES[app]}` },
992
+ ]
993
+ : [
994
+ { id: ALLOW, label: 'Allow', detail: 'This call only. Nothing is allowed after it.' },
995
+ { id: DENY, label: 'Deny' },
996
+ ],
997
+ artifacts: [
998
+ {
999
+ kind: 'code',
1000
+ title: prompt.tool === 'Bash' ? 'The command' : `${toolName(prompt.tool ?? '')}’s input`,
1001
+ ref: `permission-${relay.id}`,
1002
+ lang: relay.lang,
1003
+ lines: relay.lines,
1004
+ },
1005
+ ],
1006
+ context: { ...contextOf(state, prompt), trace: [{ state: 'paused', text: title }] },
1007
+ blocking: false,
1008
+ urgency: 'now',
1009
+ askedFirst: { where: 'terminal', askedAt: new Date(prompt.at).toISOString() },
1010
+ permissionPrompt: { app, relay: true },
1011
+ });
1012
+ const requestId = idOf(result, 'requestId', 'req_');
1013
+ const version = typeof result.version === 'number' ? result.version : 1;
1014
+ if (!requestId)
1015
+ throw new McpFailure(-32000, 'Pending You posted no card.');
1016
+ const card = { requestId, version, key: prompt.key, name };
1017
+ const id = promptId(prompt);
1018
+ let kept = false;
1019
+ const ours = await record(io, state.session, worker, (state) => {
1020
+ const now = state.prompts.find((each) => promptId(each) === id && each.relay?.id === relay.id);
1021
+ if (!now)
1022
+ return;
1023
+ now.card = card;
1024
+ state.areaId = areaId;
1025
+ kept = true;
1026
+ });
1027
+ if (!ours || !kept)
1028
+ await withdraw(io, client, card, state, 'answered').catch(() => { });
1029
+ }
889
1030
  async function stillOpen(client, card) {
890
1031
  const now = await client.call('get_request', { requestId: card.requestId, name: card.name });
891
1032
  const open = ['pending', 'snoozed', 'delegated'].includes(text(now.status)) && now.turn !== 'agent';
@@ -985,7 +1126,7 @@ async function letGo(io, session, worker, grace) {
985
1126
  return state;
986
1127
  }
987
1128
  delete state.worker;
988
- return state.prompts.length === 0 && !state.card ? null : state;
1129
+ return state.prompts.length === 0 && !state.card && !state.closing?.length ? null : state;
989
1130
  });
990
1131
  return done;
991
1132
  }
@@ -1038,6 +1179,17 @@ export async function permissionWorker(io, options) {
1038
1179
  await post(io, client, state, plan.prompt, worker);
1039
1180
  else if (plan.kind === 'update')
1040
1181
  await update(io, client, state, plan.prompt, worker);
1182
+ else if (plan.kind === 'relay')
1183
+ await postRelay(io, client, state, plan.prompt, worker);
1184
+ else if (plan.kind === 'close') {
1185
+ const card = plan.card;
1186
+ await withdraw(io, client, card, state, card.how);
1187
+ await record(io, session, worker, (state) => {
1188
+ state.closing = state.closing?.filter((each) => each.requestId !== card.requestId);
1189
+ if (!state.closing?.length)
1190
+ delete state.closing;
1191
+ });
1192
+ }
1041
1193
  else {
1042
1194
  const card = state.card;
1043
1195
  await withdraw(io, client, card, state, state.settled ?? 'answered');
@@ -1057,8 +1209,17 @@ export async function permissionWorker(io, options) {
1057
1209
  if (failures >= BACKOFF_MS.length) {
1058
1210
  failures = 0;
1059
1211
  await record(io, session, worker, (state) => {
1060
- if (plan.kind === 'post')
1061
- state.prompts = state.prompts.map((prompt) => ({ ...prompt, failed: true }));
1212
+ if (plan.kind === 'relay') {
1213
+ const id = promptId(plan.prompt);
1214
+ state.prompts = state.prompts.map((prompt) => promptId(prompt) === id ? { ...prompt, failed: true } : prompt);
1215
+ }
1216
+ else if (plan.kind === 'close') {
1217
+ state.closing = state.closing?.filter((each) => each.requestId !== plan.card.requestId);
1218
+ if (!state.closing?.length)
1219
+ delete state.closing;
1220
+ }
1221
+ else if (plan.kind === 'post')
1222
+ state.prompts = state.prompts.map((prompt) => prompt.relay ? prompt : { ...prompt, failed: true });
1062
1223
  else if (plan.kind === 'update' && state.card)
1063
1224
  state.card = { ...state.card, key: plan.prompt.key };
1064
1225
  else
@@ -1075,9 +1236,339 @@ export async function permissionWorker(io, options) {
1075
1236
  await client?.close().catch(() => { });
1076
1237
  }
1077
1238
  }
1239
+ export const RELAY_MATCH_MS = 5000;
1240
+ function fit(prompt, heard) {
1241
+ if (prompt.relay || prompt.notice || prompt.tool !== heard.tool)
1242
+ return null;
1243
+ if (Math.abs(prompt.at - heard.relay.at) > RELAY_MATCH_MS)
1244
+ return null;
1245
+ const input = heard.relay.lines.join(' ').replace(/\s+/g, ' ');
1246
+ return prompt.what
1247
+ .split(BLANK)
1248
+ .map((part) => part.replace(/\s+/g, ' ').trim())
1249
+ .filter((part) => part.length >= 3 && input.includes(part)).length;
1250
+ }
1251
+ function bestFit(prompts, heard) {
1252
+ let best = null;
1253
+ for (const prompt of prompts) {
1254
+ const score = fit(prompt, heard);
1255
+ if (score === null)
1256
+ continue;
1257
+ const apart = Math.abs(prompt.at - heard.relay.at);
1258
+ if (!best ||
1259
+ score > best.score ||
1260
+ (score === best.score && apart < Math.abs(best.prompt.at - heard.relay.at)))
1261
+ best = { prompt, score };
1262
+ }
1263
+ return best?.prompt ?? null;
1264
+ }
1265
+ async function sessionsFrom(io, first) {
1266
+ const names = await readdir(folderOf(io)).catch(() => []);
1267
+ const others = names
1268
+ .filter((name) => name.endsWith('.json'))
1269
+ .map((name) => name.slice(0, -'.json'.length))
1270
+ .filter((session) => SESSION.test(session) && session !== first);
1271
+ return first ? [first, ...others] : others;
1272
+ }
1273
+ const transcriptOf = (io, cwd, session) => join(claudeDir(io), 'projects', cwd.replace(/[^A-Za-z0-9]/g, '-'), `${session}.jsonl`);
1274
+ export async function recordRelay(io, origin, heard, options) {
1275
+ if (io.platform === 'win32')
1276
+ return null;
1277
+ if ((await readCredential(io, origin, CLAUDE))?.kind !== 'connection')
1278
+ return null;
1279
+ for (const session of await sessionsFrom(io, options.session)) {
1280
+ const before = await load(io, session);
1281
+ if (!before || !bestFit(before.prompts, heard))
1282
+ continue;
1283
+ let worker = null;
1284
+ let matched = false;
1285
+ const state = await change(io, session, (state) => {
1286
+ const prompt = state ? bestFit(state.prompts, heard) : null;
1287
+ if (!state || !prompt)
1288
+ return state;
1289
+ prompt.relay = heard.relay;
1290
+ delete prompt.failed;
1291
+ matched = true;
1292
+ delete state.settled;
1293
+ worker = claim(state, io.now());
1294
+ return state;
1295
+ });
1296
+ if (!matched || !state)
1297
+ continue;
1298
+ if (worker)
1299
+ startWorker(io, state.origin, session, worker);
1300
+ return session;
1301
+ }
1302
+ if (!options.last)
1303
+ return null;
1304
+ const session = options.session ?? `channel-${io.ppid ?? process.pid}`;
1305
+ const now = io.now();
1306
+ const prompt = {
1307
+ key: promptKey(heard.tool, { relay: heard.relay.id }),
1308
+ tool: heard.tool.slice(0, 100),
1309
+ ...heard.words,
1310
+ at: heard.relay.at,
1311
+ relay: { ...heard.relay, own: true },
1312
+ };
1313
+ let worker = null;
1314
+ await change(io, session, (state) => {
1315
+ const next = state ?? {
1316
+ version: 1,
1317
+ origin,
1318
+ session,
1319
+ cwd: options.cwd,
1320
+ ...(options.session ? { transcript: transcriptOf(io, options.cwd, options.session) } : {}),
1321
+ prompts: [],
1322
+ at: now,
1323
+ };
1324
+ if (next.prompts.some((each) => each.relay?.id === heard.relay.id))
1325
+ return next;
1326
+ next.prompts.push(prompt);
1327
+ keepNewest(next);
1328
+ delete next.settled;
1329
+ next.at = now;
1330
+ worker = claim(next, now);
1331
+ return next;
1332
+ });
1333
+ if (worker)
1334
+ startWorker(io, origin, session, worker);
1335
+ return session;
1336
+ }
1337
+ export async function relayedPrompt(io, session, id) {
1338
+ const state = await load(io, session);
1339
+ const prompt = state?.prompts.find((each) => each.relay?.id === id);
1340
+ return state && prompt ? { state, prompt } : null;
1341
+ }
1342
+ export async function answeredOnCard(io, session, id) {
1343
+ await change(io, session, (state) => {
1344
+ if (!state)
1345
+ return null;
1346
+ const left = state.prompts.filter((prompt) => prompt.relay?.id !== id);
1347
+ if (left.length === state.prompts.length)
1348
+ return state;
1349
+ state.prompts = left;
1350
+ state.at = io.now();
1351
+ if (left.length === 0 && !state.card && !state.closing?.length && !alive(state, io.now()))
1352
+ return null;
1353
+ return state;
1354
+ });
1355
+ }
1078
1356
  export async function removePermissionFiles(io) {
1079
1357
  const folder = folderOf(io);
1080
1358
  const there = await stat(folder).then(() => true, () => false);
1081
1359
  await rm(folder, { recursive: true, force: true });
1082
1360
  return there;
1083
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/redact.js CHANGED
@@ -234,6 +234,12 @@ export function cut(text, max = SUMMARY_MAX) {
234
234
  export function redactText(text, max = SUMMARY_MAX) {
235
235
  return cut(tidy(redactTokens(redactSchemes(redactHeaders(redactUrls(text))))), max);
236
236
  }
237
+ export function maskSecrets(text) {
238
+ return text
239
+ .split('\n')
240
+ .map((line) => redactTokens(redactFlags(redactNamedValues(redactSchemes(redactHeaders(redactUrls(line)))))))
241
+ .join('\n');
242
+ }
237
243
  const INLINE_CODE = /(?:^|[\s;&|(`$/])(?:(?:ba|z|da|k)?sh|fish|python[0-9.]*|node(?:js)?|deno|bun|ruby|perl|php|osascript|pwsh|powershell|lua|Rscript)\b[^;&|]*?\s(?:-c|-e|-E|--eval|-p|--print|-r|-Command|-command|-EncodedCommand)(?=\s|=|$)/i;
238
244
  const TAKES_SECRET = /\bsudo\s+(?:-\w+\s+)*-S\b|--password-stdin|\b(?:ch)?passwd\b|\bhtpasswd\b|\bsecurity\s+(?:add|set|import)-|\bsecrets?\s+(?:set|put|add|create|edit)\b|\bvault\s+(?:kv\s+)?(?:put|write)\b|\bkubectl\s+create\s+secret\b|\bop\s+(?:item|read|run|inject)\b|\bssh-add\b|\bgpg\b|\bpass\s+(?:insert|edit)\b|\bbase64\s+(?:-d|-D|--decode)\b/i;
239
245
  export function unsafeCommand(command) {
package/dist/version.js CHANGED
@@ -1 +1 @@
1
- export const VERSION = '0.32.3';
1
+ export const VERSION = '0.34.0';