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.
- package/CHANGELOG.md +26 -0
- package/README.md +36 -0
- package/dist/apps/claude-code.js +20 -2
- package/dist/apps/codex.js +63 -4
- package/dist/args.js +27 -0
- package/dist/channel.js +276 -0
- package/dist/claude.js +117 -1
- package/dist/codex-answers.js +15 -0
- package/dist/io.js +26 -0
- package/dist/main.js +18 -1
- package/dist/permission.js +505 -14
- package/dist/redact.js +6 -0
- package/dist/version.js +1 -1
- package/package.json +10 -2
package/dist/permission.js
CHANGED
|
@@ -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 ||
|
|
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
|
-
|
|
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
|
|
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 ||
|
|
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 === '
|
|
1061
|
-
|
|
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.
|
|
1
|
+
export const VERSION = '0.34.0';
|