pendingyou 0.32.2 → 0.33.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 +24 -0
- package/README.md +16 -0
- package/dist/apps/claude-code.js +20 -2
- package/dist/args.js +13 -0
- package/dist/channel.js +276 -0
- package/dist/claude.js +117 -1
- package/dist/io.js +26 -0
- package/dist/main.js +6 -0
- package/dist/permission.js +297 -14
- package/dist/redact.js +6 -0
- package/dist/version.js +1 -1
- package/mod/.claude-plugin/plugin.json +1 -1
- package/mod/README.md +1 -1
- package/package.json +10 -2
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,29 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.33.0 (2026-10-09)
|
|
4
|
+
|
|
5
|
+
- Answer Claude Code's permission prompts on their Pending You card. Start Claude Code with `npx pendingyou claude
|
|
6
|
+
[args…]` (it runs `claude --dangerously-load-development-channels server:pendingyou-permissions [args…]`) and a
|
|
7
|
+
prompt left waiting gets a card with **Allow** and **Deny** and the whole input, its secrets masked, instead of a
|
|
8
|
+
card that only says where to answer. The terminal dialog stays open: whichever answer comes first wins, and Allow is
|
|
9
|
+
for that one call only. `init` registers the channel (`pendingyou channel --app claude-code`, a stdio MCP server that
|
|
10
|
+
sends nothing to the model) at user scope through the hooks' shim, beside the permission-prompt hooks, on Claude
|
|
11
|
+
Code 2.1.234 or later; `uninstall` and `--no-permission-cards` take it out; `status` says whether it's there. The card
|
|
12
|
+
still waits the same 10 seconds, so a prompt answered at the terminal gets none; answered there later, the card goes
|
|
13
|
+
as before. A session started with plain `claude` keeps today's card, which now says it's a permission
|
|
14
|
+
prompt's (`permissionPrompt` with `relay: false`). Needs Pending You with `post_request`'s `permissionPrompt`
|
|
15
|
+
(deploy the server first). Claude Code warns about the development channel at each start until
|
|
16
|
+
the channel is on its allowlist.
|
|
17
|
+
|
|
18
|
+
## 0.32.3 (2026-10-09)
|
|
19
|
+
|
|
20
|
+
- Claude Code's and Codex's permission prompts get their cards. The worker posted each card without `context`, which
|
|
21
|
+
Pending You's `post_request` requires, so every post was refused, retried, and given up without a word: a prompt
|
|
22
|
+
left waiting in the terminal never reached the queue. The card now carries a `context` with only what it already
|
|
23
|
+
says (the prompt, as its title says it, and the session's link). The tests' pretend Pending You checks every
|
|
24
|
+
`post_request` and `update_request` with Pending You's own input schemas, so a card the server would refuse fails
|
|
25
|
+
them.
|
|
26
|
+
|
|
3
27
|
## 0.32.2 (2026-10-08)
|
|
4
28
|
|
|
5
29
|
- A Claude Code dialog in bypassPermissions or dontAsk mode always gets its card. A subagent's `rm -rf $S/*` met Claude
|
package/README.md
CHANGED
|
@@ -274,6 +274,21 @@ PermissionRequest hook prints nothing, so Codex asks exactly as before; the card
|
|
|
274
274
|
Codex, the turn ends or the thread closes. It needs Codex signing in through this computer's sign-in, and trusting the
|
|
275
275
|
new hooks once. The Codex app's browser-use prompts don't run that hook, so they get no card.
|
|
276
276
|
|
|
277
|
+
### Allow or Deny on the card (0.33.0)
|
|
278
|
+
|
|
279
|
+
Start Claude Code with `npx pendingyou claude` (any of `claude`'s own arguments after it) and a prompt left waiting gets
|
|
280
|
+
a card with **Allow** and **Deny**, and the whole command or input it asks about, secrets masked: “Allow Claude Code
|
|
281
|
+
(billing-webhooks) to run a command?”. Tap one and the terminal's dialog closes and the call runs, or is refused. The
|
|
282
|
+
dialog stays open meanwhile, so you can still answer at the terminal: the first answer wins, and the card goes. Allow
|
|
283
|
+
is for that one call only; nothing is allowed after it.
|
|
284
|
+
|
|
285
|
+
It works through Claude Code's channels (a research preview): `init` registers a small local MCP server,
|
|
286
|
+
`pendingyou-permissions`, that Claude Code relays its permission prompts to in a session started this way, and that
|
|
287
|
+
sends Claude nothing. Until it's on Claude Code's allowlist, Claude Code shows a warning about development channels at
|
|
288
|
+
each start (`pendingyou claude` runs `claude --dangerously-load-development-channels server:pendingyou-permissions`).
|
|
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
|
+
without buttons, as above.
|
|
291
|
+
|
|
277
292
|
## Which sessions are open
|
|
278
293
|
|
|
279
294
|
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
|
|
@@ -501,6 +516,7 @@ its environment. A failing command is tried twice more, then skipped. `--once` s
|
|
|
501
516
|
| `hold <requestId>` | Wait for the answer, print it, exit (`--timeout 4h`); the wake mod answers it itself |
|
|
502
517
|
| `status` | What's set up for each agent; exits 0 when each hears answers right away (`--app`) |
|
|
503
518
|
| `login` / `logout` | Sign an agent in again, or out (`--app`, `--device`, `--browser`, `--force`, `--name`, `--all`) |
|
|
519
|
+
| `claude [args…]` | Claude Code with the permission channel: a prompt's card has Allow and Deny; `claude`'s own arguments after it |
|
|
504
520
|
| `watch -- <command>` | Run a command each time an answer is ready (`--once`) |
|
|
505
521
|
| `pickup` / `handoff` | The session-start and next-message hooks (`--app`) |
|
|
506
522
|
| `stopcheck` | The Stop hook: items for you left only in chat (`--app`) |
|
package/dist/apps/claude-code.js
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
|
+
import { CHANNEL_CLAUDE } from "../channel.js";
|
|
1
2
|
import { addCommand, claudeState, helperServer, installClaude, mcpFromClaude, mcpFromFile, PLUGIN_ORIGINS, permissionBlocker, pluginFor, readManifest, removeCommand, sameUrl, uninstallClaude, } from "../claude.js";
|
|
2
3
|
import { connectionMachine, readCredential } from "../credentials.js";
|
|
3
4
|
import { differs, serverForFolder, siteOf } from "../environment.js";
|
|
4
5
|
import { originArgs } from "../hooks.js";
|
|
5
6
|
import { modConfigured, pruneLoadedNotes, unwokenLines, unwokenSessions, } from "../loaded.js";
|
|
6
|
-
import { claudeVersion, isOurModDir, loadsMods, MOD_CLAUDE } from "../mod.js";
|
|
7
|
+
import { atLeast, claudeVersion, isOurModDir, loadsMods, MOD_CLAUDE } from "../mod.js";
|
|
7
8
|
import { checkHelper, foundBy, headlessReason, helperCommand, helperPath, isOurHelper, machineOf, usesHelper, } from "../remote.js";
|
|
8
9
|
import { connectionTitle, FINISH_SAY, setupOf, setupStatus } from "../setup.js";
|
|
9
10
|
import { shimPath } from "../shim.js";
|
|
@@ -223,6 +224,23 @@ function permissionLine(io, claude, manifest, helper, flag) {
|
|
|
223
224
|
? ` ${label} no card when Claude Code waits for your OK: ${blocker}`
|
|
224
225
|
: ` missing ${label} no card when Claude Code waits for your OK; run npx pendingyou@latest init${flag}`;
|
|
225
226
|
}
|
|
227
|
+
function channelLine(claude, flag) {
|
|
228
|
+
if (!claude.permission || claude.channel === 'unknown')
|
|
229
|
+
return [];
|
|
230
|
+
const label = 'Permission answers:';
|
|
231
|
+
if (claude.channel)
|
|
232
|
+
return [
|
|
233
|
+
` ok ${label} start Claude Code with npx pendingyou claude, and a prompt’s card has Allow and Deny`,
|
|
234
|
+
];
|
|
235
|
+
const version = claude.claude ? claudeVersion(claude.claude) : null;
|
|
236
|
+
if (!version || !atLeast(version, CHANNEL_CLAUDE))
|
|
237
|
+
return [
|
|
238
|
+
` ${label} Allow and Deny on a prompt’s card need Claude Code ${CHANNEL_CLAUDE} or later${version ? ` (this is ${version})` : ''}`,
|
|
239
|
+
];
|
|
240
|
+
return [
|
|
241
|
+
` missing ${label} no Allow or Deny on a prompt’s card; run npx pendingyou@latest init${flag}`,
|
|
242
|
+
];
|
|
243
|
+
}
|
|
226
244
|
function presenceLine(claude, connected, mod, flag) {
|
|
227
245
|
const label = 'Presence:';
|
|
228
246
|
if (!connected)
|
|
@@ -338,7 +356,7 @@ async function claudeStatus(io, ctx) {
|
|
|
338
356
|
? [
|
|
339
357
|
` ok Plugin: ${plugin} is enabled${pluginServer ? '' : mcp !== false && mcp !== 'unknown' ? '; Claude Code uses the server added by hand instead of its own' : ''}`,
|
|
340
358
|
]
|
|
341
|
-
: []), ` ${mark(skillOk)} Skill: ${skillText}`, ` ${mark(claude.hooks && hooksRun)} Hooks: ${claude.hooks ? `session-start pickup and next-message hand-off; ${formText}` : `not installed; run npx pendingyou init${flag}`}`, ` ${mark(claude.stopcheck)} Stop check: ${claude.stopcheck ? 'asks Claude Code to post what it leaves you in chat' : `not installed; run npx pendingyou init${flag}`}`, ` ${mark(modOk)} Wake mod: ${modText}`, permissionLine(io, claude, manifest, helper, flag), presenceLine(claude, helper && connected, modOk, flag));
|
|
359
|
+
: []), ` ${mark(skillOk)} Skill: ${skillText}`, ` ${mark(claude.hooks && hooksRun)} Hooks: ${claude.hooks ? `session-start pickup and next-message hand-off; ${formText}` : `not installed; run npx pendingyou init${flag}`}`, ` ${mark(claude.stopcheck)} Stop check: ${claude.stopcheck ? 'asks Claude Code to post what it leaves you in chat' : `not installed; run npx pendingyou init${flag}`}`, ` ${mark(modOk)} Wake mod: ${modText}`, permissionLine(io, claude, manifest, helper, flag), ...channelLine(claude, flag), presenceLine(claude, helper && connected, modOk, flag));
|
|
342
360
|
const setup = setupStatus(NAME, signIn.state === 'ok' ? setupOf(signIn.for) : null);
|
|
343
361
|
if (setup)
|
|
344
362
|
lines.push(` ${setup === 'finished' ? 'ok ' : ' '} Setup: ${setup}`);
|
package/dist/args.js
CHANGED
|
@@ -84,6 +84,8 @@ const COMMANDS = new Set([
|
|
|
84
84
|
'herdr',
|
|
85
85
|
'machine',
|
|
86
86
|
'app',
|
|
87
|
+
'channel',
|
|
88
|
+
'claude',
|
|
87
89
|
]);
|
|
88
90
|
const FLAGS = {
|
|
89
91
|
help: {},
|
|
@@ -139,6 +141,8 @@ const FLAGS = {
|
|
|
139
141
|
refresh: { origin: true, force: false, app: true },
|
|
140
142
|
'mcp-headers': { origin: true, check: false, app: true },
|
|
141
143
|
mcp: { origin: true, app: true, check: false },
|
|
144
|
+
channel: { origin: true, app: true },
|
|
145
|
+
claude: {},
|
|
142
146
|
herdr: {
|
|
143
147
|
origin: true,
|
|
144
148
|
app: true,
|
|
@@ -233,6 +237,8 @@ export function parseArgs(argv, env = {}) {
|
|
|
233
237
|
return { name: 'version' };
|
|
234
238
|
if (!COMMANDS.has(first))
|
|
235
239
|
throw new UsageError(`There’s no “${first}” command.`);
|
|
240
|
+
if (first === 'claude')
|
|
241
|
+
return { name: 'claude', args: [...after] };
|
|
236
242
|
const rest = [...leading, ...after];
|
|
237
243
|
const allowed = FLAGS[first] ?? {};
|
|
238
244
|
const flags = new Map();
|
|
@@ -425,6 +431,9 @@ export function parseArgs(argv, env = {}) {
|
|
|
425
431
|
case 'mcp':
|
|
426
432
|
none();
|
|
427
433
|
return { name: 'mcp', origin, ...app(), check: flags.has('check') };
|
|
434
|
+
case 'channel':
|
|
435
|
+
none();
|
|
436
|
+
return { name: 'channel', origin, ...app() };
|
|
428
437
|
case 'herdr':
|
|
429
438
|
return herdrCommand(origin, positional, flags, app);
|
|
430
439
|
case 'machine':
|
|
@@ -662,6 +671,10 @@ export const USAGE = `pendingyou: Pending You for your coding agents (Claude Cod
|
|
|
662
671
|
npx pendingyou status What's set up for each agent, and whether it hears answers right away
|
|
663
672
|
npx pendingyou login [--app <app>] | logout
|
|
664
673
|
Sign an agent in again (Claude Code's when none is named), or out
|
|
674
|
+
npx pendingyou claude [args…]
|
|
675
|
+
Start Claude Code so its permission prompts can be answered on their
|
|
676
|
+
Pending You cards: Allow or Deny there, or in the terminal; the first
|
|
677
|
+
answer wins. Claude Code warns about the development channel each start
|
|
665
678
|
npx pendingyou watch -- <command> [args…]
|
|
666
679
|
Run a command each time an answer is ready (always-on scripts)
|
|
667
680
|
npx pendingyou uninstall Remove exactly what init added
|
package/dist/channel.js
ADDED
|
@@ -0,0 +1,276 @@
|
|
|
1
|
+
import { DEFAULT_APP } from "./apps/ids.js";
|
|
2
|
+
import { McpFailure, mcpClient, SIGN_IN_ERROR } from "./bridge.js";
|
|
3
|
+
import { ALLOW, answeredOnCard, computerOf, DENY, promptWords, RELAY_MATCH_MS, recordRelay, relayedPrompt, } from "./permission.js";
|
|
4
|
+
import { cut, maskSecrets, redactText } from "./redact.js";
|
|
5
|
+
import { VERSION } from "./version.js";
|
|
6
|
+
export const CHANNEL = 'pendingyou-permissions';
|
|
7
|
+
export const CHANNEL_CLAUDE = '2.1.234';
|
|
8
|
+
export const READ_MS = 2000;
|
|
9
|
+
export const READ_FOR_MS = 30 * 60_000;
|
|
10
|
+
export const SLOW_READ_MS = 30_000;
|
|
11
|
+
const MATCH_STEP_MS = 500;
|
|
12
|
+
const CALL_MS = 15_000;
|
|
13
|
+
const INPUT_MAX = 40_000;
|
|
14
|
+
const LINES_MAX = 400;
|
|
15
|
+
const REQUEST_ID = /^[a-km-z]{5}$/;
|
|
16
|
+
const UNSERIALIZABLE = '(value unserializable)';
|
|
17
|
+
const NO_DESCRIPTION = 'Run shell command';
|
|
18
|
+
const isObject = (value) => typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
19
|
+
const INSTRUCTIONS = 'Pending You’s permission channel: it puts Claude Code’s permission prompts on the person’s Pending You card, with Allow and Deny. It sends you no messages and has no tools; ignore it.';
|
|
20
|
+
export function inputLines(tool, preview) {
|
|
21
|
+
let parsed = null;
|
|
22
|
+
try {
|
|
23
|
+
parsed = JSON.parse(preview);
|
|
24
|
+
}
|
|
25
|
+
catch { }
|
|
26
|
+
const input = isObject(parsed) ? parsed : {};
|
|
27
|
+
let lines;
|
|
28
|
+
let lang = 'text';
|
|
29
|
+
if (isObject(parsed) &&
|
|
30
|
+
(tool === 'Bash' || tool === 'PowerShell') &&
|
|
31
|
+
typeof parsed.command === 'string') {
|
|
32
|
+
lines = maskSecrets(parsed.command).split('\n');
|
|
33
|
+
lang = 'shell';
|
|
34
|
+
}
|
|
35
|
+
else if (isObject(parsed))
|
|
36
|
+
lines = Object.entries(parsed).flatMap(([key, value]) => maskSecrets(`${key}: ${typeof value === 'string' ? value : JSON.stringify(value)}`).split('\n'));
|
|
37
|
+
else
|
|
38
|
+
lines = maskSecrets(preview).split('\n');
|
|
39
|
+
const kept = [];
|
|
40
|
+
let size = 0;
|
|
41
|
+
for (const line of lines) {
|
|
42
|
+
if (kept.length >= LINES_MAX || size + line.length > INPUT_MAX) {
|
|
43
|
+
kept.push('… (cut here: the rest is in Claude Code’s dialog)');
|
|
44
|
+
break;
|
|
45
|
+
}
|
|
46
|
+
kept.push(line);
|
|
47
|
+
size += line.length;
|
|
48
|
+
}
|
|
49
|
+
return { lines: kept.length ? kept : [''], lang, input };
|
|
50
|
+
}
|
|
51
|
+
export function heardOf(io, params) {
|
|
52
|
+
if (!isObject(params))
|
|
53
|
+
return null;
|
|
54
|
+
const { request_id: id, tool_name: tool, description, input_preview: preview } = params;
|
|
55
|
+
if (typeof id !== 'string' || !REQUEST_ID.test(id))
|
|
56
|
+
return null;
|
|
57
|
+
if (typeof tool !== 'string' || !tool || tool.length > 200)
|
|
58
|
+
return null;
|
|
59
|
+
const text = typeof preview === 'string' ? preview : '';
|
|
60
|
+
const said = typeof description === 'string' ? description.trim() : '';
|
|
61
|
+
const shown = inputLines(tool, text);
|
|
62
|
+
return {
|
|
63
|
+
tool,
|
|
64
|
+
words: promptWords(tool, shown.input, io.home),
|
|
65
|
+
relay: {
|
|
66
|
+
id,
|
|
67
|
+
at: io.now(),
|
|
68
|
+
description: said && said !== NO_DESCRIPTION ? redactText(said, 160) : '',
|
|
69
|
+
lines: shown.lines,
|
|
70
|
+
lang: shown.lang,
|
|
71
|
+
...(text.includes(UNSERIALIZABLE) ? { unserializable: true } : {}),
|
|
72
|
+
},
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
export function verdictOf(card, relay) {
|
|
76
|
+
if (!['answered', 'resolved'].includes(String(card.status)))
|
|
77
|
+
return null;
|
|
78
|
+
const answer = isObject(card.answer) ? card.answer : null;
|
|
79
|
+
const picked = Array.isArray(answer?.choiceIds) ? answer.choiceIds : [];
|
|
80
|
+
if (picked.length !== 1)
|
|
81
|
+
return null;
|
|
82
|
+
if (picked[0] === DENY)
|
|
83
|
+
return 'deny';
|
|
84
|
+
if (picked[0] === ALLOW && !relay.unserializable)
|
|
85
|
+
return 'allow';
|
|
86
|
+
return null;
|
|
87
|
+
}
|
|
88
|
+
export async function channel(io, options) {
|
|
89
|
+
const app = options.app ?? DEFAULT_APP;
|
|
90
|
+
const controller = new AbortController();
|
|
91
|
+
const stop = () => controller.abort();
|
|
92
|
+
if (io.signal.aborted)
|
|
93
|
+
stop();
|
|
94
|
+
io.signal.addEventListener('abort', stop, { once: true });
|
|
95
|
+
const write = (message) => io.out(`${JSON.stringify({ jsonrpc: '2.0', ...message })}\n`);
|
|
96
|
+
const session = /^[A-Za-z0-9][A-Za-z0-9_-]{0,99}$/.test(io.env.CLAUDE_CODE_SESSION_ID ?? '')
|
|
97
|
+
? io.env.CLAUDE_CODE_SESSION_ID
|
|
98
|
+
: null;
|
|
99
|
+
const cwd = io.env.CLAUDE_PROJECT_DIR || io.cwd;
|
|
100
|
+
const following = new Set();
|
|
101
|
+
const lines = io.lines()[Symbol.asyncIterator]();
|
|
102
|
+
const stopped = new Promise((resolve) => {
|
|
103
|
+
const done = () => resolve({ done: true, value: undefined });
|
|
104
|
+
if (controller.signal.aborted)
|
|
105
|
+
done();
|
|
106
|
+
else
|
|
107
|
+
controller.signal.addEventListener('abort', done, { once: true });
|
|
108
|
+
});
|
|
109
|
+
let client = null;
|
|
110
|
+
const pending = {
|
|
111
|
+
async call(name, args) {
|
|
112
|
+
try {
|
|
113
|
+
client ??= await mcpClient(io, options.origin, 'claude-code', CALL_MS);
|
|
114
|
+
return await client.call(name, args);
|
|
115
|
+
}
|
|
116
|
+
catch (error) {
|
|
117
|
+
if (error instanceof McpFailure)
|
|
118
|
+
client = null;
|
|
119
|
+
throw error;
|
|
120
|
+
}
|
|
121
|
+
},
|
|
122
|
+
};
|
|
123
|
+
try {
|
|
124
|
+
for (;;) {
|
|
125
|
+
const next = await Promise.race([lines.next(), stopped]);
|
|
126
|
+
if (next.done)
|
|
127
|
+
break;
|
|
128
|
+
let message;
|
|
129
|
+
try {
|
|
130
|
+
message = JSON.parse(next.value);
|
|
131
|
+
}
|
|
132
|
+
catch {
|
|
133
|
+
continue;
|
|
134
|
+
}
|
|
135
|
+
if (!isObject(message) || typeof message.method !== 'string')
|
|
136
|
+
continue;
|
|
137
|
+
const id = message.id;
|
|
138
|
+
const asked = typeof id === 'string' || typeof id === 'number';
|
|
139
|
+
switch (message.method) {
|
|
140
|
+
case 'initialize': {
|
|
141
|
+
const params = isObject(message.params) ? message.params : {};
|
|
142
|
+
if (asked)
|
|
143
|
+
write({
|
|
144
|
+
id,
|
|
145
|
+
result: {
|
|
146
|
+
protocolVersion: typeof params.protocolVersion === 'string'
|
|
147
|
+
? params.protocolVersion
|
|
148
|
+
: '2025-06-18',
|
|
149
|
+
capabilities: {
|
|
150
|
+
experimental: { 'claude/channel': {}, 'claude/channel/permission': {} },
|
|
151
|
+
},
|
|
152
|
+
serverInfo: { name: CHANNEL, version: VERSION },
|
|
153
|
+
instructions: INSTRUCTIONS,
|
|
154
|
+
},
|
|
155
|
+
});
|
|
156
|
+
break;
|
|
157
|
+
}
|
|
158
|
+
case 'ping':
|
|
159
|
+
if (asked)
|
|
160
|
+
write({ id, result: {} });
|
|
161
|
+
break;
|
|
162
|
+
case 'tools/list':
|
|
163
|
+
if (asked)
|
|
164
|
+
write({ id, result: { tools: [] } });
|
|
165
|
+
break;
|
|
166
|
+
case 'notifications/claude/channel/permission_request': {
|
|
167
|
+
if (app !== 'claude-code')
|
|
168
|
+
break;
|
|
169
|
+
const heard = heardOf(io, message.params);
|
|
170
|
+
if (!heard)
|
|
171
|
+
break;
|
|
172
|
+
const task = follow(io, options.origin, heard, {
|
|
173
|
+
session,
|
|
174
|
+
cwd,
|
|
175
|
+
signal: controller.signal,
|
|
176
|
+
pending,
|
|
177
|
+
verdict: (behavior) => write({
|
|
178
|
+
method: 'notifications/claude/channel/permission',
|
|
179
|
+
params: { request_id: heard.relay.id, behavior },
|
|
180
|
+
}),
|
|
181
|
+
}).catch(() => { });
|
|
182
|
+
following.add(task);
|
|
183
|
+
task.then(() => following.delete(task));
|
|
184
|
+
break;
|
|
185
|
+
}
|
|
186
|
+
default:
|
|
187
|
+
if (asked)
|
|
188
|
+
write({ id, error: { code: -32601, message: 'Method not found' } });
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
finally {
|
|
193
|
+
controller.abort();
|
|
194
|
+
io.signal.removeEventListener('abort', stop);
|
|
195
|
+
lines.return?.().catch(() => undefined);
|
|
196
|
+
await Promise.allSettled([...following]);
|
|
197
|
+
await client?.close().catch(() => { });
|
|
198
|
+
}
|
|
199
|
+
await io.flush();
|
|
200
|
+
return 0;
|
|
201
|
+
}
|
|
202
|
+
async function follow(io, origin, heard, options) {
|
|
203
|
+
const { signal } = options;
|
|
204
|
+
const id = heard.relay.id;
|
|
205
|
+
let where = null;
|
|
206
|
+
const waitFrom = io.now();
|
|
207
|
+
for (;;) {
|
|
208
|
+
const last = io.now() - waitFrom >= RELAY_MATCH_MS;
|
|
209
|
+
where = await recordRelay(io, origin, heard, {
|
|
210
|
+
session: options.session,
|
|
211
|
+
cwd: options.cwd,
|
|
212
|
+
last,
|
|
213
|
+
});
|
|
214
|
+
if (where || last || signal.aborted)
|
|
215
|
+
break;
|
|
216
|
+
await io.sleep(MATCH_STEP_MS, signal);
|
|
217
|
+
}
|
|
218
|
+
if (!where)
|
|
219
|
+
return;
|
|
220
|
+
const started = io.now();
|
|
221
|
+
while (!signal.aborted) {
|
|
222
|
+
await io.sleep(io.now() - started < READ_FOR_MS ? READ_MS : SLOW_READ_MS, signal);
|
|
223
|
+
if (signal.aborted)
|
|
224
|
+
return;
|
|
225
|
+
const now = await relayedPrompt(io, where, id);
|
|
226
|
+
if (!now)
|
|
227
|
+
return;
|
|
228
|
+
const card = now.prompt.card;
|
|
229
|
+
if (!card)
|
|
230
|
+
continue;
|
|
231
|
+
let read;
|
|
232
|
+
try {
|
|
233
|
+
read = await options.pending.call('get_request', {
|
|
234
|
+
requestId: card.requestId,
|
|
235
|
+
name: card.name,
|
|
236
|
+
});
|
|
237
|
+
}
|
|
238
|
+
catch (error) {
|
|
239
|
+
if (error instanceof McpFailure && error.code === SIGN_IN_ERROR)
|
|
240
|
+
return;
|
|
241
|
+
continue;
|
|
242
|
+
}
|
|
243
|
+
const status = String(read.status);
|
|
244
|
+
if (['pending', 'snoozed', 'delegated'].includes(status))
|
|
245
|
+
continue;
|
|
246
|
+
const verdict = verdictOf(read, heard.relay);
|
|
247
|
+
if (!verdict) {
|
|
248
|
+
if (status === 'answered' && typeof read.version === 'number') {
|
|
249
|
+
const machine = await computerOf(io, origin, 'claude-code');
|
|
250
|
+
await options.pending
|
|
251
|
+
.call('ack_answer', {
|
|
252
|
+
requestId: card.requestId,
|
|
253
|
+
name: card.name,
|
|
254
|
+
expectedVersion: read.version,
|
|
255
|
+
outcome: cut(`Not allowed or denied: it waits for your answer in Claude Code on ${machine}.`, 200),
|
|
256
|
+
})
|
|
257
|
+
.catch(() => { });
|
|
258
|
+
}
|
|
259
|
+
return;
|
|
260
|
+
}
|
|
261
|
+
options.verdict(verdict);
|
|
262
|
+
await answeredOnCard(io, where, id);
|
|
263
|
+
if (status === 'answered' && typeof read.version === 'number') {
|
|
264
|
+
const machine = await computerOf(io, origin, 'claude-code');
|
|
265
|
+
await options.pending
|
|
266
|
+
.call('ack_answer', {
|
|
267
|
+
requestId: card.requestId,
|
|
268
|
+
name: card.name,
|
|
269
|
+
expectedVersion: read.version,
|
|
270
|
+
outcome: cut(`${verdict === 'allow' ? 'Allowed' : 'Denied'} in Claude Code on ${machine}.`, 200),
|
|
271
|
+
})
|
|
272
|
+
.catch(() => { });
|
|
273
|
+
}
|
|
274
|
+
return;
|
|
275
|
+
}
|
|
276
|
+
}
|
package/dist/claude.js
CHANGED
|
@@ -2,6 +2,7 @@ import { createHash } from 'node:crypto';
|
|
|
2
2
|
import { rm, rmdir, stat } from 'node:fs/promises';
|
|
3
3
|
import { basename, dirname, join } from 'node:path';
|
|
4
4
|
import { DEFAULT_ORIGIN } from "./args.js";
|
|
5
|
+
import { CHANNEL, CHANNEL_CLAUDE } from "./channel.js";
|
|
5
6
|
import { PlainError } from "./errors.js";
|
|
6
7
|
import { claudeDir, configDir, readJson, readText, writeWhole } from "./files.js";
|
|
7
8
|
import { findHook, hooksOf, mergeHooks, originArgs, removeHooks } from "./hooks.js";
|
|
@@ -11,7 +12,7 @@ import { atLeast, claudeVersion, isOurModDir, loadsMods, modDir, PLUGIN_DIRS, pl
|
|
|
11
12
|
import { removePermissionFiles } from "./permission.js";
|
|
12
13
|
import { removePresenceFiles } from "./presence.js";
|
|
13
14
|
import { helperPath } from "./remote.js";
|
|
14
|
-
import { readShim, SHIM } from "./shim.js";
|
|
15
|
+
import { readShim, SHIM, shimPath } from "./shim.js";
|
|
15
16
|
import { VERSION } from "./version.js";
|
|
16
17
|
export { isOurHook } from "./hooks.js";
|
|
17
18
|
export const MCP_NAME = 'pendingyou';
|
|
@@ -192,6 +193,105 @@ export function helperServer(url, command) {
|
|
|
192
193
|
text: `claude mcp add-json --scope user ${MCP_NAME} ${quote(json)}`,
|
|
193
194
|
};
|
|
194
195
|
}
|
|
196
|
+
export function channelEntry(io, origin) {
|
|
197
|
+
return {
|
|
198
|
+
type: 'stdio',
|
|
199
|
+
command: shimPath(io),
|
|
200
|
+
args: [
|
|
201
|
+
'channel',
|
|
202
|
+
'--app',
|
|
203
|
+
'claude-code',
|
|
204
|
+
...(origin === DEFAULT_ORIGIN ? [] : ['--origin', origin]),
|
|
205
|
+
],
|
|
206
|
+
};
|
|
207
|
+
}
|
|
208
|
+
export async function channelFromFile(io) {
|
|
209
|
+
let parsed;
|
|
210
|
+
try {
|
|
211
|
+
const text = await readText(claudeJsonPath(io));
|
|
212
|
+
if (text === null)
|
|
213
|
+
return 'unknown';
|
|
214
|
+
parsed = JSON.parse(text);
|
|
215
|
+
}
|
|
216
|
+
catch {
|
|
217
|
+
return 'unknown';
|
|
218
|
+
}
|
|
219
|
+
if (typeof parsed !== 'object' || parsed === null || Array.isArray(parsed))
|
|
220
|
+
return 'unknown';
|
|
221
|
+
const servers = objectAt(parsed, 'mcpServers');
|
|
222
|
+
return servers ? objectAt(servers, CHANNEL) : null;
|
|
223
|
+
}
|
|
224
|
+
const sameEntry = (entry, wanted) => entry.command === wanted.command && JSON.stringify(entry.args) === JSON.stringify(wanted.args);
|
|
225
|
+
async function applyChannel(io, origin, wanted, before, progress) {
|
|
226
|
+
const entry = channelEntry(io, origin);
|
|
227
|
+
const now = await channelFromFile(io);
|
|
228
|
+
const remove = () => io.run('claude', ['mcp', 'remove', '--scope', 'user', CHANNEL], MCP_TIMEOUT_MS);
|
|
229
|
+
if (!wanted) {
|
|
230
|
+
if (!before?.channel || now === null)
|
|
231
|
+
return { step: null, registered: false };
|
|
232
|
+
progress('Removing the permission channel from Claude Code (this can take a minute)…\n');
|
|
233
|
+
const removed = await remove();
|
|
234
|
+
return removed.code === 0
|
|
235
|
+
? {
|
|
236
|
+
step: { ok: true, text: `Took out the permission channel (${CHANNEL}).` },
|
|
237
|
+
registered: false,
|
|
238
|
+
}
|
|
239
|
+
: {
|
|
240
|
+
step: {
|
|
241
|
+
ok: false,
|
|
242
|
+
text: `Couldn’t take out the permission channel. Run: claude mcp remove --scope user ${CHANNEL}`,
|
|
243
|
+
},
|
|
244
|
+
registered: true,
|
|
245
|
+
};
|
|
246
|
+
}
|
|
247
|
+
if (now !== null && now !== 'unknown' && sameEntry(now, entry))
|
|
248
|
+
return {
|
|
249
|
+
step: { ok: true, text: 'The permission channel was already registered.' },
|
|
250
|
+
registered: true,
|
|
251
|
+
};
|
|
252
|
+
const json = JSON.stringify(entry);
|
|
253
|
+
const add = `claude mcp add-json --scope user ${CHANNEL} ${quote(json)}`;
|
|
254
|
+
if (now !== null && now !== 'unknown') {
|
|
255
|
+
progress('Updating the permission channel in Claude Code (this can take a minute)…\n');
|
|
256
|
+
if ((await remove()).code !== 0)
|
|
257
|
+
return {
|
|
258
|
+
step: {
|
|
259
|
+
ok: false,
|
|
260
|
+
text: `Couldn’t update the permission channel. Run:\n claude mcp remove --scope user ${CHANNEL}\n ${add}`,
|
|
261
|
+
},
|
|
262
|
+
registered: false,
|
|
263
|
+
};
|
|
264
|
+
}
|
|
265
|
+
else
|
|
266
|
+
progress('Adding the permission channel to Claude Code (this can take a minute)…\n');
|
|
267
|
+
const added = await io.run('claude', ['mcp', 'add-json', '--scope', 'user', CHANNEL, json], MCP_TIMEOUT_MS);
|
|
268
|
+
if (added.code !== 0)
|
|
269
|
+
return {
|
|
270
|
+
step: { ok: false, text: `Claude Code didn’t add the permission channel. Run: ${add}` },
|
|
271
|
+
registered: false,
|
|
272
|
+
};
|
|
273
|
+
return {
|
|
274
|
+
step: {
|
|
275
|
+
ok: true,
|
|
276
|
+
text: `Added the permission channel (${CHANNEL}): start Claude Code with npx pendingyou claude and a prompt’s card has Allow and Deny. The first answer wins, there or in the terminal.`,
|
|
277
|
+
},
|
|
278
|
+
registered: true,
|
|
279
|
+
};
|
|
280
|
+
}
|
|
281
|
+
export async function claudeWithChannel(io, args) {
|
|
282
|
+
if (!io.handOver)
|
|
283
|
+
throw new PlainError('This can’t start Claude Code here.');
|
|
284
|
+
if ((await channelFromFile(io)) === null)
|
|
285
|
+
io.err('pendingyou: the permission channel isn’t set up, so Claude Code’s prompts get no Allow or Deny on their cards. Run: npx pendingyou@latest init\n');
|
|
286
|
+
const code = await io.handOver('claude', [
|
|
287
|
+
'--dangerously-load-development-channels',
|
|
288
|
+
`server:${CHANNEL}`,
|
|
289
|
+
...args,
|
|
290
|
+
]);
|
|
291
|
+
if (code === 127)
|
|
292
|
+
io.err('pendingyou: Claude Code isn’t installed here (no claude on PATH).\n');
|
|
293
|
+
return code;
|
|
294
|
+
}
|
|
195
295
|
async function applyMcp(io, origin, plan, helper, progress) {
|
|
196
296
|
const url = `${origin}/mcp`;
|
|
197
297
|
const add = helper
|
|
@@ -363,6 +463,10 @@ export async function installClaude(io, origin, options) {
|
|
|
363
463
|
? `${merged.permissionChanged ? 'Took out the permission-prompt hooks' : 'Left out the permission-prompt hooks'}, as you asked: no card when Claude Code waits for your OK. To turn them on: npx pendingyou init --permission-cards${flag}`
|
|
364
464
|
: `${merged.permissionChanged ? 'Took out' : 'Left out'} the permission-prompt hooks: ${blocker}.`,
|
|
365
465
|
});
|
|
466
|
+
const found = claudeVersion(options.version);
|
|
467
|
+
const channel = await applyChannel(io, origin, Boolean(permissionLines) && found !== null && atLeast(found, CHANNEL_CLAUDE), before, options.progress);
|
|
468
|
+
if (channel.step)
|
|
469
|
+
report(channel.step);
|
|
366
470
|
const mod = await addMod(io, merged.settings, options.copy, options.version);
|
|
367
471
|
if (mod.step)
|
|
368
472
|
report(mod.step);
|
|
@@ -387,6 +491,7 @@ export async function installClaude(io, origin, options) {
|
|
|
387
491
|
...(options.plugin ? { plugin: true } : {}),
|
|
388
492
|
permissionHooks: permissionLines,
|
|
389
493
|
permissionCards: wanted,
|
|
494
|
+
...(channel.registered ? { channel: true } : {}),
|
|
390
495
|
};
|
|
391
496
|
await writeWhole(manifestPath(io), `${JSON.stringify(manifest, null, 2)}\n`, { secret: true });
|
|
392
497
|
return steps;
|
|
@@ -458,6 +563,16 @@ export async function uninstallClaude(io) {
|
|
|
458
563
|
const skill = await removeSkill(manifest?.skill ?? null);
|
|
459
564
|
if (skill)
|
|
460
565
|
steps.push(skill);
|
|
566
|
+
if (manifest?.channel && (await channelFromFile(io)) !== null) {
|
|
567
|
+
io.out('Removing the permission channel from Claude Code (this can take a minute)…\n');
|
|
568
|
+
const result = await io.run('claude', ['mcp', 'remove', '--scope', 'user', CHANNEL], MCP_TIMEOUT_MS);
|
|
569
|
+
steps.push(result.code === 0
|
|
570
|
+
? { ok: true, text: `Removed the permission channel (${CHANNEL}) from Claude Code.` }
|
|
571
|
+
: {
|
|
572
|
+
ok: false,
|
|
573
|
+
text: `Couldn’t remove the permission channel. Run: claude mcp remove --scope user ${CHANNEL}`,
|
|
574
|
+
});
|
|
575
|
+
}
|
|
461
576
|
if (manifest?.mcpAdded) {
|
|
462
577
|
io.out('Removing the pendingyou MCP server from Claude Code (this can take a minute)…\n');
|
|
463
578
|
const result = await io.run('claude', ['mcp', 'remove', '--scope', 'user', MCP_NAME], MCP_TIMEOUT_MS);
|
|
@@ -523,6 +638,7 @@ export async function claudeState(io) {
|
|
|
523
638
|
permission,
|
|
524
639
|
notify,
|
|
525
640
|
presence: Boolean(ours('SessionEnd')),
|
|
641
|
+
channel: await channelFromFile(io).then((entry) => entry === 'unknown' ? 'unknown' : entry !== null),
|
|
526
642
|
form: form ? { ...form, runs: await hookRuns(form), shim } : null,
|
|
527
643
|
mod,
|
|
528
644
|
plugins,
|
package/dist/io.js
CHANGED
|
@@ -68,6 +68,29 @@ export function execProgram(command, args, options) {
|
|
|
68
68
|
child.stdin?.end(options.stdin);
|
|
69
69
|
});
|
|
70
70
|
}
|
|
71
|
+
let handedTo = null;
|
|
72
|
+
function handOver(command, args) {
|
|
73
|
+
return new Promise((resolve) => {
|
|
74
|
+
let child;
|
|
75
|
+
try {
|
|
76
|
+
child = spawn(command, [...args], { stdio: 'inherit' });
|
|
77
|
+
}
|
|
78
|
+
catch {
|
|
79
|
+
resolve(127);
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
handedTo = (signal) => {
|
|
83
|
+
if (signal !== 'SIGINT')
|
|
84
|
+
child.kill(signal);
|
|
85
|
+
};
|
|
86
|
+
const done = (code) => {
|
|
87
|
+
handedTo = null;
|
|
88
|
+
resolve(code);
|
|
89
|
+
};
|
|
90
|
+
child.on('error', (error) => done(error.code === 'ENOENT' ? 127 : 1));
|
|
91
|
+
child.on('exit', (code, signal) => done(code ?? (signal === 'SIGINT' ? 130 : signal === 'SIGTERM' ? 143 : 1)));
|
|
92
|
+
});
|
|
93
|
+
}
|
|
71
94
|
export function sleep(ms, signal) {
|
|
72
95
|
return new Promise((resolve) => {
|
|
73
96
|
if (signal?.aborted || ms <= 0) {
|
|
@@ -232,6 +255,8 @@ export function realIo() {
|
|
|
232
255
|
const controller = new AbortController();
|
|
233
256
|
for (const name of ['SIGINT', 'SIGTERM', 'SIGHUP'])
|
|
234
257
|
process.on(name, () => {
|
|
258
|
+
if (handedTo)
|
|
259
|
+
return handedTo(name);
|
|
235
260
|
if (controller.signal.aborted)
|
|
236
261
|
process.exit(130);
|
|
237
262
|
controller.abort();
|
|
@@ -250,6 +275,7 @@ export function realIo() {
|
|
|
250
275
|
ask,
|
|
251
276
|
run: runProgram,
|
|
252
277
|
exec: execProgram,
|
|
278
|
+
handOver,
|
|
253
279
|
openBrowser,
|
|
254
280
|
readStdin,
|
|
255
281
|
lines: () => lines(controller.signal, () => controller.abort()),
|
package/dist/main.js
CHANGED
|
@@ -4,6 +4,8 @@ import { DEFAULT_APP } from "./apps/ids.js";
|
|
|
4
4
|
import { appModule } from "./apps/registry.js";
|
|
5
5
|
import { commandOf, HOOK_COMMANDS, parseArgs, USAGE, UsageError } from "./args.js";
|
|
6
6
|
import { bridge } from "./bridge.js";
|
|
7
|
+
import { channel } from "./channel.js";
|
|
8
|
+
import { claudeWithChannel } from "./claude.js";
|
|
7
9
|
import { PlainError } from "./errors.js";
|
|
8
10
|
import { hold } from "./hold.js";
|
|
9
11
|
import { init, status, uninstall } from "./init.js";
|
|
@@ -93,6 +95,10 @@ async function run(io, command, hook = { fallback: '' }) {
|
|
|
93
95
|
return mcpHeaders(io, command);
|
|
94
96
|
case 'mcp':
|
|
95
97
|
return bridge(io, command);
|
|
98
|
+
case 'channel':
|
|
99
|
+
return channel(io, command);
|
|
100
|
+
case 'claude':
|
|
101
|
+
return claudeWithChannel(io, command.args);
|
|
96
102
|
case 'herdr':
|
|
97
103
|
return (await import("./herdr/command.js")).herdr(io, command);
|
|
98
104
|
case 'machine':
|
package/dist/permission.js
CHANGED
|
@@ -7,8 +7,8 @@ import { DEFAULT_ORIGIN } from "./args.js";
|
|
|
7
7
|
import { McpFailure, mcpClient, SIGN_IN_ERROR, ToolRefused } from "./bridge.js";
|
|
8
8
|
import { sessionLink } from "./confirm.js";
|
|
9
9
|
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";
|
|
10
|
+
import { claudeDir, configDir, PERMISSION_FOLDER, readJson, withLock, writeWhole } from "./files.js";
|
|
11
|
+
import { BLANK, cut, redactCommand, redactText, redactUrl, shortPath } from "./redact.js";
|
|
12
12
|
import { machineOf } from "./remote.js";
|
|
13
13
|
export const GRACE_MS = 10_000;
|
|
14
14
|
export const TITLE_MAX = 90;
|
|
@@ -24,7 +24,9 @@ export const isPromptApp = (app) => PROMPT_APPS.includes(app);
|
|
|
24
24
|
export const waitingFor = (app) => `${APP_NAMES[app]} is waiting for your OK`;
|
|
25
25
|
export const isPermissionCard = (title) => typeof title === 'string' &&
|
|
26
26
|
PROMPT_APPS.some((app) => title.startsWith(waitingFor(app)) ||
|
|
27
|
-
title.startsWith(`${APP_NAMES[app]} is waiting for your answer`)
|
|
27
|
+
title.startsWith(`${APP_NAMES[app]} is waiting for your answer`) ||
|
|
28
|
+
title.startsWith(`Allow ${APP_NAMES[app]} `) ||
|
|
29
|
+
title.startsWith(`Allow a subagent of ${APP_NAMES[app]}`));
|
|
28
30
|
export const DEFAULT_NAME = 'Claude Code';
|
|
29
31
|
const defaultName = (app) => APP_NAMES[app];
|
|
30
32
|
const LEASE_STALE_MS = 120_000;
|
|
@@ -172,7 +174,10 @@ async function change(io, session, edit) {
|
|
|
172
174
|
});
|
|
173
175
|
}
|
|
174
176
|
const alive = (state, now) => Boolean(state.worker && now - state.worker.at < LEASE_STALE_MS);
|
|
175
|
-
const seen = (prompt) => !prompt.quiet ||
|
|
177
|
+
const seen = (prompt) => !prompt.quiet ||
|
|
178
|
+
prompt.shown !== undefined ||
|
|
179
|
+
prompt.waited !== undefined ||
|
|
180
|
+
prompt.relay !== undefined;
|
|
176
181
|
const unchecked = (prompt) => !seen(prompt) && !prompt.notice;
|
|
177
182
|
function claim(state, now) {
|
|
178
183
|
if (alive(state, now))
|
|
@@ -217,6 +222,27 @@ function reasonOf(app, input) {
|
|
|
217
222
|
return reason || null;
|
|
218
223
|
}
|
|
219
224
|
const wentOn = (agent, now) => (prompt) => Boolean(prompt.quiet) && (prompt.agent ?? '') === agent && prompt.at <= now - BATCH_MS;
|
|
225
|
+
function closeCards(state, settled, how) {
|
|
226
|
+
const cards = settled.flatMap((prompt) => (prompt.card ? [{ ...prompt.card, how }] : []));
|
|
227
|
+
if (cards.length)
|
|
228
|
+
state.closing = [...(state.closing ?? []), ...cards];
|
|
229
|
+
}
|
|
230
|
+
function keepNewest(state) {
|
|
231
|
+
closeCards(state, state.prompts.slice(0, -MAX_PROMPTS), 'answered');
|
|
232
|
+
state.prompts = state.prompts.slice(-MAX_PROMPTS);
|
|
233
|
+
}
|
|
234
|
+
const ADOPT_MS = 30_000;
|
|
235
|
+
function adopt(state, prompt) {
|
|
236
|
+
const own = state.prompts.find((each) => each.relay?.own &&
|
|
237
|
+
each.tool === prompt.tool &&
|
|
238
|
+
Math.abs(prompt.at - each.relay.at) <= ADOPT_MS);
|
|
239
|
+
if (!own?.relay)
|
|
240
|
+
return false;
|
|
241
|
+
const { at: _at, quiet: _quiet, ...hook } = prompt;
|
|
242
|
+
Object.assign(own, hook);
|
|
243
|
+
delete own.relay.own;
|
|
244
|
+
return true;
|
|
245
|
+
}
|
|
220
246
|
async function asked(io, origin, input, app) {
|
|
221
247
|
if (io.platform === 'win32')
|
|
222
248
|
return;
|
|
@@ -273,10 +299,12 @@ async function asked(io, origin, input, app) {
|
|
|
273
299
|
const kept = app === 'claude-code'
|
|
274
300
|
? next.prompts.filter((each) => !wentOn(agent, now)(each))
|
|
275
301
|
: next.prompts;
|
|
302
|
+
closeCards(next, next.prompts.filter((each) => !kept.includes(each)), 'answered');
|
|
276
303
|
next.prompts = kept;
|
|
277
|
-
|
|
304
|
+
const adopted = app === 'claude-code' ? adopt(next, prompt) : false;
|
|
305
|
+
if (!adopted && !next.prompts.some((each) => each.key === prompt.key))
|
|
278
306
|
next.prompts.push(prompt);
|
|
279
|
-
next
|
|
307
|
+
keepNewest(next);
|
|
280
308
|
delete next.settled;
|
|
281
309
|
next.at = now;
|
|
282
310
|
worker = claim(next, now);
|
|
@@ -299,10 +327,11 @@ async function settle(io, session, which, how) {
|
|
|
299
327
|
const left = state.prompts.filter((prompt) => !which(prompt));
|
|
300
328
|
if (left.length === state.prompts.length)
|
|
301
329
|
return state;
|
|
330
|
+
closeCards(state, state.prompts.filter((prompt) => !left.includes(prompt)), how);
|
|
302
331
|
state.prompts = left;
|
|
303
332
|
state.settled = how;
|
|
304
333
|
state.at = now;
|
|
305
|
-
if (!state.card && left.length === 0)
|
|
334
|
+
if (!state.card && !state.closing?.length && left.length === 0)
|
|
306
335
|
return alive(state, now) ? state : null;
|
|
307
336
|
worker = claim(state, now);
|
|
308
337
|
return state;
|
|
@@ -322,7 +351,11 @@ async function ran(io, input, app) {
|
|
|
322
351
|
return;
|
|
323
352
|
const exact = before.prompts.some((prompt) => prompt.key === key);
|
|
324
353
|
const movedOn = wentOn(agent, io.now());
|
|
325
|
-
await settle(io, where.session, (prompt) => prompt.key === key ||
|
|
354
|
+
await settle(io, where.session, (prompt) => prompt.key === key ||
|
|
355
|
+
(app === 'codex'
|
|
356
|
+
? !exact && prompt.tool === tool
|
|
357
|
+
: movedOn(prompt) ||
|
|
358
|
+
(!exact && Boolean(prompt.relay?.own) && prompt.tool === tool && !agent)), 'answered');
|
|
326
359
|
}
|
|
327
360
|
export async function endPrompts(io, session) {
|
|
328
361
|
try {
|
|
@@ -447,7 +480,7 @@ export async function permissionHook(io, options) {
|
|
|
447
480
|
const dueOf = (prompt, grace) => Math.min(prompt.quiet ? (prompt.waited ?? Number.POSITIVE_INFINITY) : prompt.at + grace, prompt.shown ?? Number.POSITIVE_INFINITY);
|
|
448
481
|
const shownOf = (prompts) => (prompts.filter((prompt) => !prompt.notice).at(-1) ?? prompts.at(-1));
|
|
449
482
|
export function planFor(state, now, grace, quietGrace = QUIET_GRACE_MS) {
|
|
450
|
-
const plan = cardPlan(state, now, grace);
|
|
483
|
+
const plan = relayPlan(state, now, grace, cardPlan(state, now, grace));
|
|
451
484
|
const pending = state.prompts.filter(unchecked);
|
|
452
485
|
if (pending.length === 0)
|
|
453
486
|
return plan;
|
|
@@ -460,8 +493,26 @@ export function planFor(state, now, grace, quietGrace = QUIET_GRACE_MS) {
|
|
|
460
493
|
return { kind: 'wait', ms: Math.min(plan.ms, check) };
|
|
461
494
|
return plan;
|
|
462
495
|
}
|
|
496
|
+
function relayPlan(state, now, grace, then) {
|
|
497
|
+
const closing = state.closing?.[0];
|
|
498
|
+
if (closing)
|
|
499
|
+
return { kind: 'close', card: closing };
|
|
500
|
+
const waiting = state.prompts.filter((prompt) => prompt.relay && !prompt.card && !prompt.failed);
|
|
501
|
+
if (waiting.length === 0)
|
|
502
|
+
return then;
|
|
503
|
+
const dueAt = (prompt) => Math.min(Math.min(prompt.at, prompt.relay.at) + grace, prompt.shown ?? Number.POSITIVE_INFINITY);
|
|
504
|
+
const first = waiting.reduce((soonest, prompt) => dueAt(prompt) < dueAt(soonest) ? prompt : soonest);
|
|
505
|
+
const due = dueAt(first) - now;
|
|
506
|
+
if (due <= 0)
|
|
507
|
+
return { kind: 'relay', prompt: first };
|
|
508
|
+
if (then.kind === 'done')
|
|
509
|
+
return { kind: 'wait', ms: due };
|
|
510
|
+
if (then.kind === 'wait')
|
|
511
|
+
return { kind: 'wait', ms: Math.min(then.ms, due) };
|
|
512
|
+
return then;
|
|
513
|
+
}
|
|
463
514
|
function cardPlan(state, now, grace) {
|
|
464
|
-
const prompts = state.prompts.filter(seen);
|
|
515
|
+
const prompts = state.prompts.filter((prompt) => seen(prompt) && !prompt.relay);
|
|
465
516
|
if (prompts.length === 0)
|
|
466
517
|
return state.card ? { kind: 'cancel' } : { kind: 'done' };
|
|
467
518
|
if (state.card) {
|
|
@@ -481,7 +532,7 @@ function graceOf(env) {
|
|
|
481
532
|
const given = Number(env.PENDINGYOU_PERMISSION_GRACE_MS);
|
|
482
533
|
return Number.isInteger(given) && given >= 0 ? given : GRACE_MS;
|
|
483
534
|
}
|
|
484
|
-
async function computerOf(io, origin, app) {
|
|
535
|
+
export async function computerOf(io, origin, app) {
|
|
485
536
|
const named = await connectionMachine(io, origin, app).catch(() => null);
|
|
486
537
|
return cut(named ?? (machineOf(io.host) || 'this computer'), 60);
|
|
487
538
|
}
|
|
@@ -835,6 +886,16 @@ function cardWords(io, state, prompt, name, machine) {
|
|
|
835
886
|
},
|
|
836
887
|
};
|
|
837
888
|
}
|
|
889
|
+
function contextOf(state, prompt) {
|
|
890
|
+
const app = appOf(state);
|
|
891
|
+
return {
|
|
892
|
+
background: `${APP_NAMES[app]} asked in its terminal; this card stands in for that dialog.`,
|
|
893
|
+
trace: [{ state: 'paused', text: prompt.title }],
|
|
894
|
+
filesTouched: [],
|
|
895
|
+
tried: [],
|
|
896
|
+
links: state.link ? [{ label: `The ${APP_NAMES[app]} session`, url: state.link }] : [],
|
|
897
|
+
};
|
|
898
|
+
}
|
|
838
899
|
async function record(io, session, worker, edit) {
|
|
839
900
|
let ours = false;
|
|
840
901
|
await change(io, session, (state) => {
|
|
@@ -859,9 +920,11 @@ async function post(io, client, state, prompt, worker) {
|
|
|
859
920
|
kind: 'action',
|
|
860
921
|
title: prompt.title,
|
|
861
922
|
...cardWords(io, state, prompt, name, machine),
|
|
923
|
+
context: contextOf(state, prompt),
|
|
862
924
|
blocking: false,
|
|
863
925
|
urgency: 'now',
|
|
864
926
|
askedFirst: { where: 'terminal', askedAt: new Date(prompt.at).toISOString() },
|
|
927
|
+
permissionPrompt: { app: appOf(state), relay: false },
|
|
865
928
|
});
|
|
866
929
|
const requestId = idOf(result, 'requestId', 'req_');
|
|
867
930
|
const version = typeof result.version === 'number' ? result.version : 1;
|
|
@@ -875,6 +938,89 @@ async function post(io, client, state, prompt, worker) {
|
|
|
875
938
|
if (!kept)
|
|
876
939
|
await withdraw(io, client, card, state, 'answered').catch(() => { });
|
|
877
940
|
}
|
|
941
|
+
export const ALLOW = 'allow';
|
|
942
|
+
export const DENY = 'deny';
|
|
943
|
+
export const IN_TERMINAL = 'terminal';
|
|
944
|
+
export function relayTitle(state, prompt, name) {
|
|
945
|
+
const app = appOf(state);
|
|
946
|
+
const who = name === defaultName(app) ? APP_NAMES[app] : `${APP_NAMES[app]} (${name})`;
|
|
947
|
+
const subject = byAgent(state, prompt) ? `a subagent of ${who}` : who;
|
|
948
|
+
const doing = prompt.doing.startsWith('to ')
|
|
949
|
+
? prompt.doing
|
|
950
|
+
: `to use ${toolName(prompt.tool ?? '')}`;
|
|
951
|
+
return cut(`Allow ${subject} ${doing}?`, TITLE_MAX);
|
|
952
|
+
}
|
|
953
|
+
async function postRelay(io, client, state, prompt, worker) {
|
|
954
|
+
const relay = prompt.relay;
|
|
955
|
+
const app = appOf(state);
|
|
956
|
+
const name = await nameOf(io, state);
|
|
957
|
+
await client.call('whoami', { name });
|
|
958
|
+
const areaId = state.areaId ?? (await areaFor(io, client, state, name));
|
|
959
|
+
const machine = await computerOf(io, state.origin, app);
|
|
960
|
+
const title = relayTitle(state, prompt, name);
|
|
961
|
+
const who = name === defaultName(app) ? APP_NAMES[app] : `${APP_NAMES[app]} (${name})`;
|
|
962
|
+
const where = `in ${tilde(io, state.cwd)} on ${machine}`;
|
|
963
|
+
const asker = byAgent(state, prompt)
|
|
964
|
+
? `A subagent${prompt.agentType ? ` (${prompt.agentType})` : ''} of ${who}, ${where},`
|
|
965
|
+
: `${who} ${where}`;
|
|
966
|
+
const wants = prompt.doing.startsWith('to ')
|
|
967
|
+
? `wants your OK ${prompt.doing}`
|
|
968
|
+
: `wants your OK to use ${toolName(prompt.tool ?? '')}`;
|
|
969
|
+
const why = relay.description ? ` It says: “${relay.description}”.` : '';
|
|
970
|
+
const answer = relay.unserializable
|
|
971
|
+
? `Part of it couldn’t be shown, so only Deny is here; to allow it, answer in ${APP_NAMES[app]} on ${machine}.`
|
|
972
|
+
: `Allow or Deny here, or answer in ${APP_NAMES[app]} on ${machine}: the first answer wins.`;
|
|
973
|
+
const session = createHash('sha256').update(state.session).digest('hex').slice(0, 24);
|
|
974
|
+
const result = await client.call('post_request', {
|
|
975
|
+
idempotencyKey: `${app}-relay:${session}:${relay.id}:${relay.at}`,
|
|
976
|
+
areaId,
|
|
977
|
+
session: { label: name, machine, cwd: cut(tilde(io, state.cwd), 300) },
|
|
978
|
+
kind: 'choice',
|
|
979
|
+
intent: 'approve',
|
|
980
|
+
title,
|
|
981
|
+
summary: cut(`${asker} ${wants}.${why} ${answer}`, 400),
|
|
982
|
+
options: relay.unserializable
|
|
983
|
+
? [
|
|
984
|
+
{ id: DENY, label: 'Deny' },
|
|
985
|
+
{ id: IN_TERMINAL, label: `I’ll answer in ${APP_NAMES[app]}` },
|
|
986
|
+
]
|
|
987
|
+
: [
|
|
988
|
+
{ id: ALLOW, label: 'Allow', detail: 'This call only. Nothing is allowed after it.' },
|
|
989
|
+
{ id: DENY, label: 'Deny' },
|
|
990
|
+
],
|
|
991
|
+
artifacts: [
|
|
992
|
+
{
|
|
993
|
+
kind: 'code',
|
|
994
|
+
title: prompt.tool === 'Bash' ? 'The command' : `${toolName(prompt.tool ?? '')}’s input`,
|
|
995
|
+
ref: `permission-${relay.id}`,
|
|
996
|
+
lang: relay.lang,
|
|
997
|
+
lines: relay.lines,
|
|
998
|
+
},
|
|
999
|
+
],
|
|
1000
|
+
context: { ...contextOf(state, prompt), trace: [{ state: 'paused', text: title }] },
|
|
1001
|
+
blocking: false,
|
|
1002
|
+
urgency: 'now',
|
|
1003
|
+
askedFirst: { where: 'terminal', askedAt: new Date(prompt.at).toISOString() },
|
|
1004
|
+
permissionPrompt: { app, relay: true },
|
|
1005
|
+
});
|
|
1006
|
+
const requestId = idOf(result, 'requestId', 'req_');
|
|
1007
|
+
const version = typeof result.version === 'number' ? result.version : 1;
|
|
1008
|
+
if (!requestId)
|
|
1009
|
+
throw new McpFailure(-32000, 'Pending You posted no card.');
|
|
1010
|
+
const card = { requestId, version, key: prompt.key, name };
|
|
1011
|
+
const id = promptId(prompt);
|
|
1012
|
+
let kept = false;
|
|
1013
|
+
const ours = await record(io, state.session, worker, (state) => {
|
|
1014
|
+
const now = state.prompts.find((each) => promptId(each) === id && each.relay?.id === relay.id);
|
|
1015
|
+
if (!now)
|
|
1016
|
+
return;
|
|
1017
|
+
now.card = card;
|
|
1018
|
+
state.areaId = areaId;
|
|
1019
|
+
kept = true;
|
|
1020
|
+
});
|
|
1021
|
+
if (!ours || !kept)
|
|
1022
|
+
await withdraw(io, client, card, state, 'answered').catch(() => { });
|
|
1023
|
+
}
|
|
878
1024
|
async function stillOpen(client, card) {
|
|
879
1025
|
const now = await client.call('get_request', { requestId: card.requestId, name: card.name });
|
|
880
1026
|
const open = ['pending', 'snoozed', 'delegated'].includes(text(now.status)) && now.turn !== 'agent';
|
|
@@ -974,7 +1120,7 @@ async function letGo(io, session, worker, grace) {
|
|
|
974
1120
|
return state;
|
|
975
1121
|
}
|
|
976
1122
|
delete state.worker;
|
|
977
|
-
return state.prompts.length === 0 && !state.card ? null : state;
|
|
1123
|
+
return state.prompts.length === 0 && !state.card && !state.closing?.length ? null : state;
|
|
978
1124
|
});
|
|
979
1125
|
return done;
|
|
980
1126
|
}
|
|
@@ -1027,6 +1173,17 @@ export async function permissionWorker(io, options) {
|
|
|
1027
1173
|
await post(io, client, state, plan.prompt, worker);
|
|
1028
1174
|
else if (plan.kind === 'update')
|
|
1029
1175
|
await update(io, client, state, plan.prompt, worker);
|
|
1176
|
+
else if (plan.kind === 'relay')
|
|
1177
|
+
await postRelay(io, client, state, plan.prompt, worker);
|
|
1178
|
+
else if (plan.kind === 'close') {
|
|
1179
|
+
const card = plan.card;
|
|
1180
|
+
await withdraw(io, client, card, state, card.how);
|
|
1181
|
+
await record(io, session, worker, (state) => {
|
|
1182
|
+
state.closing = state.closing?.filter((each) => each.requestId !== card.requestId);
|
|
1183
|
+
if (!state.closing?.length)
|
|
1184
|
+
delete state.closing;
|
|
1185
|
+
});
|
|
1186
|
+
}
|
|
1030
1187
|
else {
|
|
1031
1188
|
const card = state.card;
|
|
1032
1189
|
await withdraw(io, client, card, state, state.settled ?? 'answered');
|
|
@@ -1046,8 +1203,17 @@ export async function permissionWorker(io, options) {
|
|
|
1046
1203
|
if (failures >= BACKOFF_MS.length) {
|
|
1047
1204
|
failures = 0;
|
|
1048
1205
|
await record(io, session, worker, (state) => {
|
|
1049
|
-
if (plan.kind === '
|
|
1050
|
-
|
|
1206
|
+
if (plan.kind === 'relay') {
|
|
1207
|
+
const id = promptId(plan.prompt);
|
|
1208
|
+
state.prompts = state.prompts.map((prompt) => promptId(prompt) === id ? { ...prompt, failed: true } : prompt);
|
|
1209
|
+
}
|
|
1210
|
+
else if (plan.kind === 'close') {
|
|
1211
|
+
state.closing = state.closing?.filter((each) => each.requestId !== plan.card.requestId);
|
|
1212
|
+
if (!state.closing?.length)
|
|
1213
|
+
delete state.closing;
|
|
1214
|
+
}
|
|
1215
|
+
else if (plan.kind === 'post')
|
|
1216
|
+
state.prompts = state.prompts.map((prompt) => prompt.relay ? prompt : { ...prompt, failed: true });
|
|
1051
1217
|
else if (plan.kind === 'update' && state.card)
|
|
1052
1218
|
state.card = { ...state.card, key: plan.prompt.key };
|
|
1053
1219
|
else
|
|
@@ -1064,6 +1230,123 @@ export async function permissionWorker(io, options) {
|
|
|
1064
1230
|
await client?.close().catch(() => { });
|
|
1065
1231
|
}
|
|
1066
1232
|
}
|
|
1233
|
+
export const RELAY_MATCH_MS = 5000;
|
|
1234
|
+
function fit(prompt, heard) {
|
|
1235
|
+
if (prompt.relay || prompt.notice || prompt.tool !== heard.tool)
|
|
1236
|
+
return null;
|
|
1237
|
+
if (Math.abs(prompt.at - heard.relay.at) > RELAY_MATCH_MS)
|
|
1238
|
+
return null;
|
|
1239
|
+
const input = heard.relay.lines.join(' ').replace(/\s+/g, ' ');
|
|
1240
|
+
return prompt.what
|
|
1241
|
+
.split(BLANK)
|
|
1242
|
+
.map((part) => part.replace(/\s+/g, ' ').trim())
|
|
1243
|
+
.filter((part) => part.length >= 3 && input.includes(part)).length;
|
|
1244
|
+
}
|
|
1245
|
+
function bestFit(prompts, heard) {
|
|
1246
|
+
let best = null;
|
|
1247
|
+
for (const prompt of prompts) {
|
|
1248
|
+
const score = fit(prompt, heard);
|
|
1249
|
+
if (score === null)
|
|
1250
|
+
continue;
|
|
1251
|
+
const apart = Math.abs(prompt.at - heard.relay.at);
|
|
1252
|
+
if (!best ||
|
|
1253
|
+
score > best.score ||
|
|
1254
|
+
(score === best.score && apart < Math.abs(best.prompt.at - heard.relay.at)))
|
|
1255
|
+
best = { prompt, score };
|
|
1256
|
+
}
|
|
1257
|
+
return best?.prompt ?? null;
|
|
1258
|
+
}
|
|
1259
|
+
async function sessionsFrom(io, first) {
|
|
1260
|
+
const names = await readdir(folderOf(io)).catch(() => []);
|
|
1261
|
+
const others = names
|
|
1262
|
+
.filter((name) => name.endsWith('.json'))
|
|
1263
|
+
.map((name) => name.slice(0, -'.json'.length))
|
|
1264
|
+
.filter((session) => SESSION.test(session) && session !== first);
|
|
1265
|
+
return first ? [first, ...others] : others;
|
|
1266
|
+
}
|
|
1267
|
+
const transcriptOf = (io, cwd, session) => join(claudeDir(io), 'projects', cwd.replace(/[^A-Za-z0-9]/g, '-'), `${session}.jsonl`);
|
|
1268
|
+
export async function recordRelay(io, origin, heard, options) {
|
|
1269
|
+
if (io.platform === 'win32')
|
|
1270
|
+
return null;
|
|
1271
|
+
if ((await readCredential(io, origin, CLAUDE))?.kind !== 'connection')
|
|
1272
|
+
return null;
|
|
1273
|
+
for (const session of await sessionsFrom(io, options.session)) {
|
|
1274
|
+
const before = await load(io, session);
|
|
1275
|
+
if (!before || !bestFit(before.prompts, heard))
|
|
1276
|
+
continue;
|
|
1277
|
+
let worker = null;
|
|
1278
|
+
let matched = false;
|
|
1279
|
+
const state = await change(io, session, (state) => {
|
|
1280
|
+
const prompt = state ? bestFit(state.prompts, heard) : null;
|
|
1281
|
+
if (!state || !prompt)
|
|
1282
|
+
return state;
|
|
1283
|
+
prompt.relay = heard.relay;
|
|
1284
|
+
delete prompt.failed;
|
|
1285
|
+
matched = true;
|
|
1286
|
+
delete state.settled;
|
|
1287
|
+
worker = claim(state, io.now());
|
|
1288
|
+
return state;
|
|
1289
|
+
});
|
|
1290
|
+
if (!matched || !state)
|
|
1291
|
+
continue;
|
|
1292
|
+
if (worker)
|
|
1293
|
+
startWorker(io, state.origin, session, worker);
|
|
1294
|
+
return session;
|
|
1295
|
+
}
|
|
1296
|
+
if (!options.last)
|
|
1297
|
+
return null;
|
|
1298
|
+
const session = options.session ?? `channel-${io.ppid ?? process.pid}`;
|
|
1299
|
+
const now = io.now();
|
|
1300
|
+
const prompt = {
|
|
1301
|
+
key: promptKey(heard.tool, { relay: heard.relay.id }),
|
|
1302
|
+
tool: heard.tool.slice(0, 100),
|
|
1303
|
+
...heard.words,
|
|
1304
|
+
at: heard.relay.at,
|
|
1305
|
+
relay: { ...heard.relay, own: true },
|
|
1306
|
+
};
|
|
1307
|
+
let worker = null;
|
|
1308
|
+
await change(io, session, (state) => {
|
|
1309
|
+
const next = state ?? {
|
|
1310
|
+
version: 1,
|
|
1311
|
+
origin,
|
|
1312
|
+
session,
|
|
1313
|
+
cwd: options.cwd,
|
|
1314
|
+
...(options.session ? { transcript: transcriptOf(io, options.cwd, options.session) } : {}),
|
|
1315
|
+
prompts: [],
|
|
1316
|
+
at: now,
|
|
1317
|
+
};
|
|
1318
|
+
if (next.prompts.some((each) => each.relay?.id === heard.relay.id))
|
|
1319
|
+
return next;
|
|
1320
|
+
next.prompts.push(prompt);
|
|
1321
|
+
keepNewest(next);
|
|
1322
|
+
delete next.settled;
|
|
1323
|
+
next.at = now;
|
|
1324
|
+
worker = claim(next, now);
|
|
1325
|
+
return next;
|
|
1326
|
+
});
|
|
1327
|
+
if (worker)
|
|
1328
|
+
startWorker(io, origin, session, worker);
|
|
1329
|
+
return session;
|
|
1330
|
+
}
|
|
1331
|
+
export async function relayedPrompt(io, session, id) {
|
|
1332
|
+
const state = await load(io, session);
|
|
1333
|
+
const prompt = state?.prompts.find((each) => each.relay?.id === id);
|
|
1334
|
+
return state && prompt ? { state, prompt } : null;
|
|
1335
|
+
}
|
|
1336
|
+
export async function answeredOnCard(io, session, id) {
|
|
1337
|
+
await change(io, session, (state) => {
|
|
1338
|
+
if (!state)
|
|
1339
|
+
return null;
|
|
1340
|
+
const left = state.prompts.filter((prompt) => prompt.relay?.id !== id);
|
|
1341
|
+
if (left.length === state.prompts.length)
|
|
1342
|
+
return state;
|
|
1343
|
+
state.prompts = left;
|
|
1344
|
+
state.at = io.now();
|
|
1345
|
+
if (left.length === 0 && !state.card && !state.closing?.length && !alive(state, io.now()))
|
|
1346
|
+
return null;
|
|
1347
|
+
return state;
|
|
1348
|
+
});
|
|
1349
|
+
}
|
|
1067
1350
|
export async function removePermissionFiles(io) {
|
|
1068
1351
|
const folder = folderOf(io);
|
|
1069
1352
|
const there = await stat(folder).then(() => true, () => false);
|
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.33.0';
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pendingyou-wake",
|
|
3
3
|
"displayName": "Pending You",
|
|
4
|
-
"version": "2.
|
|
4
|
+
"version": "2.49.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.49.0; send changes there.
|
package/package.json
CHANGED
|
@@ -1,9 +1,16 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pendingyou",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.33.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",
|
|
7
|
+
"repository": {
|
|
8
|
+
"type": "git",
|
|
9
|
+
"url": "git+https://github.com/recordplane/pendingyou-cli.git"
|
|
10
|
+
},
|
|
11
|
+
"bugs": {
|
|
12
|
+
"url": "https://github.com/recordplane/pendingyou-cli/issues"
|
|
13
|
+
},
|
|
7
14
|
"type": "module",
|
|
8
15
|
"bin": {
|
|
9
16
|
"pendingyou": "dist/cli.js"
|
|
@@ -32,6 +39,7 @@
|
|
|
32
39
|
"prepublishOnly": "pnpm run build && pnpm run test",
|
|
33
40
|
"test": "vitest run",
|
|
34
41
|
"typecheck": "tsc -p tsconfig.json",
|
|
35
|
-
"sdk": "node scripts/sdk.ts"
|
|
42
|
+
"sdk": "node scripts/sdk.ts",
|
|
43
|
+
"sync:repo": "node scripts/sync.ts"
|
|
36
44
|
}
|
|
37
45
|
}
|