pendingyou 0.12.0 → 0.13.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/dist/args.js CHANGED
@@ -76,6 +76,9 @@ const COMMANDS = new Set([
76
76
  'posted',
77
77
  'listen',
78
78
  'mcp',
79
+ 'permission',
80
+ 'permission-done',
81
+ 'permission-card',
79
82
  ]);
80
83
  /** Flags each command takes; `true` when the flag takes a value. */
81
84
  const FLAGS = {
@@ -98,6 +101,9 @@ const FLAGS = {
98
101
  stopcheck: { origin: true, app: true },
99
102
  posted: { origin: true, app: true },
100
103
  listen: { origin: true, app: true, thread: true },
104
+ permission: { origin: true, app: true },
105
+ 'permission-done': { origin: true, app: true },
106
+ 'permission-card': { origin: true, session: true, worker: true },
101
107
  init: {
102
108
  origin: true,
103
109
  'no-login': false,
@@ -108,6 +114,8 @@ const FLAGS = {
108
114
  name: true,
109
115
  oauth: false,
110
116
  app: true,
117
+ 'permission-cards': false,
118
+ 'no-permission-cards': false,
111
119
  },
112
120
  uninstall: { origin: true, app: true },
113
121
  watch: { origin: true, once: false },
@@ -116,7 +124,7 @@ const FLAGS = {
116
124
  mcp: { origin: true, app: true, check: false },
117
125
  };
118
126
  /** Options that take a value, whichever command they belong to: needed to find the command behind them. */
119
- const VALUE_OPTIONS = new Set(['origin', 'timeout', 'name', 'app', 'thread']);
127
+ const VALUE_OPTIONS = new Set(['origin', 'timeout', 'name', 'app', 'thread', 'session', 'worker']);
120
128
  /** Splits off the options given before the command: [those options, the command, everything after it]. */
121
129
  function splitCommand(argv) {
122
130
  let index = 0;
@@ -138,6 +146,8 @@ export const HOOK_COMMANDS = new Set([
138
146
  'handoff',
139
147
  'stopcheck',
140
148
  'posted',
149
+ 'permission',
150
+ 'permission-done',
141
151
  ]);
142
152
  /**
143
153
  * A thread's id, as an app's hooks give it (`session_id`): Codex's (letters, digits and dashes), an OpenCode session's
@@ -244,6 +254,16 @@ export function parseArgs(argv, env = {}) {
244
254
  throw new UsageError(`${first} takes one app, like: --app ${apps[0]}`);
245
255
  return { app: apps[0] };
246
256
  };
257
+ /** `--permission-cards` or `--no-permission-cards`, or neither (as before). */
258
+ const permissionCards = () => {
259
+ if (flags.has('permission-cards') && flags.has('no-permission-cards'))
260
+ throw new UsageError('Use --permission-cards or --no-permission-cards, not both.');
261
+ return flags.has('permission-cards')
262
+ ? { permissionCards: true }
263
+ : flags.has('no-permission-cards')
264
+ ? { permissionCards: false }
265
+ : {};
266
+ };
247
267
  /** `--app`, one or several; none when it isn't given (every app). */
248
268
  const apps = () => {
249
269
  const given = text('app');
@@ -295,6 +315,23 @@ export function parseArgs(argv, env = {}) {
295
315
  throw new UsageError('listen needs the thread it wakes, like: --thread 0199a1b2-…');
296
316
  return { name: 'listen', origin, ...app(), thread };
297
317
  }
318
+ // Not in USAGE: Claude Code's permission-prompt hooks (0.13.0, permission.ts).
319
+ case 'permission':
320
+ case 'permission-done':
321
+ none();
322
+ return { name: first, origin, ...app() };
323
+ // Not in USAGE: what those hooks start in the background, one per Claude Code session (permission.ts).
324
+ case 'permission-card': {
325
+ none();
326
+ const session = text('session');
327
+ const worker = text('worker');
328
+ if (session === undefined ||
329
+ !THREAD.test(session) ||
330
+ !worker ||
331
+ !/^[0-9a-f]{8,64}$/.test(worker))
332
+ throw new UsageError('permission-card needs --session <id> and --worker <id>.');
333
+ return { name: 'permission-card', origin, session, worker };
334
+ }
298
335
  case 'uninstall':
299
336
  none();
300
337
  return { name: 'uninstall', origin, ...apps() };
@@ -321,6 +358,7 @@ export function parseArgs(argv, env = {}) {
321
358
  ...machine(),
322
359
  ...(flags.has('oauth') ? { oauth: true } : {}),
323
360
  ...apps(),
361
+ ...permissionCards(),
324
362
  };
325
363
  case 'watch':
326
364
  none();
@@ -375,5 +413,6 @@ Options: --origin <url> (default ${DEFAULT_ORIGIN}), --app <app> (init, status,
375
413
  the default over SSH), --browser (open the browser anyway), --no-browser (only print the address), --name <name>
376
414
  (login, init: what to call this computer; by default its own name on a Mac, its Tailscale name, else its hostname),
377
415
  --no-login (init), --oauth (init: every agent signs in to Pending You by itself, as before 0.11.0), --yes (init: switch
378
- or replace a pendingyou MCP server without asking), --all (logout), --force (login), --once (watch).
416
+ or replace a pendingyou MCP server without asking), --no-permission-cards (init: no card when Claude Code waits for your
417
+ OK; --permission-cards turns them back on), --all (logout), --force (login), --once (watch).
379
418
  `;
package/dist/bridge.js CHANGED
@@ -536,6 +536,88 @@ export async function bridge(io, options) {
536
536
  await io.flush();
537
537
  return 0;
538
538
  }
539
+ /** A tool call Pending You refused, in its own words: a card that's closed, or changed since it was last read. */
540
+ export class ToolRefused extends Error {
541
+ name = 'ToolRefused';
542
+ }
543
+ /** Pending You couldn't be asked: the sign-in (SIGN_IN_ERROR: init has to run again) or the network (BRIDGE_ERROR). */
544
+ export class McpFailure extends Error {
545
+ name = 'McpFailure';
546
+ code;
547
+ constructor(code, message) {
548
+ super(message);
549
+ this.code = code;
550
+ }
551
+ }
552
+ /** A tool's answer as Pending You gives it (worker/mcp/server.ts): its structured content, or its text as JSON. */
553
+ function resultOf(result) {
554
+ if (isObject(result.structuredContent))
555
+ return result.structuredContent;
556
+ const blocks = Array.isArray(result.content) ? result.content : [];
557
+ for (const block of blocks) {
558
+ if (!isObject(block) || typeof block.text !== 'string')
559
+ continue;
560
+ try {
561
+ const parsed = JSON.parse(block.text);
562
+ if (isObject(parsed))
563
+ return parsed;
564
+ }
565
+ catch { }
566
+ }
567
+ return {};
568
+ }
569
+ /** The words of a refused tool call (its text content), cut short. */
570
+ function refusalOf(result) {
571
+ const blocks = Array.isArray(result.content) ? result.content : [];
572
+ const text = blocks.find((block) => isObject(block) && typeof block.text === 'string');
573
+ return typeof text?.text === 'string' ? text.text.slice(0, 300) : 'Pending You refused that.';
574
+ }
575
+ /**
576
+ * A small MCP client over the bridge's own relay (0.13.0): what the permission-prompt cards' worker (permission.ts)
577
+ * asks Pending You with. The handshake, then one tools/call at a time, each given up after `callMs`, with the app's
578
+ * own connection (its token refreshed in the background, never a redirect followed), exactly as the bridge sends an
579
+ * app's messages. Throws McpFailure when the handshake can't be made.
580
+ */
581
+ export async function mcpClient(io, origin, app, callMs = 15_000) {
582
+ const replies = new Map();
583
+ const relay = relayFor(io, origin, app, AbortSignal.timeout(callMs), (message) => {
584
+ for (const each of Array.isArray(message) ? message : [message])
585
+ if (isObject(each) && !('method' in each) && isId(each.id))
586
+ replies.set(each.id, each);
587
+ }, () => { });
588
+ let next = 0;
589
+ const send = async (message) => {
590
+ // Each message gets its own time limit; the session it keeps carries over.
591
+ relay.signal = AbortSignal.timeout(callMs);
592
+ await relayOne(relay, JSON.stringify(message), message);
593
+ };
594
+ const request = async (method, params) => {
595
+ const id = `pendingyou-${++next}`;
596
+ await send({ jsonrpc: '2.0', id, method, params });
597
+ const reply = replies.get(id);
598
+ replies.delete(id);
599
+ if (!reply)
600
+ throw new McpFailure(BRIDGE_ERROR, words.noAnswer);
601
+ if (isObject(reply.error))
602
+ throw new McpFailure(typeof reply.error.code === 'number' ? reply.error.code : BRIDGE_ERROR, typeof reply.error.message === 'string' ? reply.error.message : words.noAnswer);
603
+ return isObject(reply.result) ? reply.result : {};
604
+ };
605
+ await request('initialize', {
606
+ protocolVersion: '2025-06-18',
607
+ capabilities: {},
608
+ clientInfo: { name: 'pendingyou', version: VERSION },
609
+ });
610
+ await send({ jsonrpc: '2.0', method: 'notifications/initialized' });
611
+ return {
612
+ async call(name, args) {
613
+ const result = await request('tools/call', { name, arguments: args });
614
+ if (result.isError === true)
615
+ throw new ToolRefused(refusalOf(result));
616
+ return resultOf(result);
617
+ },
618
+ close: () => endSession(relay),
619
+ };
620
+ }
539
621
  /**
540
622
  * Whether the app's MCP server would reach Pending You through the bridge now (status): the handshake, sent as the
541
623
  * bridge sends it, with the app's sign-in. Null when it answered; else why not, in the words the bridge would use.
package/dist/claude.js CHANGED
@@ -23,7 +23,8 @@ import { PlainError } from "./errors.js";
23
23
  import { claudeDir, configDir, readJson, readText, writeWhole } from "./files.js";
24
24
  import { findHook, hooksOf, mergeHooks, originArgs, removeHooks } from "./hooks.js";
25
25
  import { cliPackage, hookForm, hookRuns, shellQuote, } from "./install.js";
26
- import { claudeVersion, isOurModDir, loadsMods, modDir, PLUGIN_DIRS, pluginDirs, pluginEnabled, separatorFor, withModDir, } from "./mod.js";
26
+ import { atLeast, claudeVersion, isOurModDir, loadsMods, modDir, PLUGIN_DIRS, pluginDirs, pluginEnabled, separatorFor, withModDir, } from "./mod.js";
27
+ import { removePermissionFiles } from "./permission.js";
27
28
  import { helperPath } from "./remote.js";
28
29
  import { readShim, SHIM } from "./shim.js";
29
30
  import { VERSION } from "./version.js";
@@ -37,6 +38,23 @@ export const HOOKS = {
37
38
  };
38
39
  /** The same, as hooks.ts takes them. SessionStart has no matcher: pickup itself skips a compaction (setup.ts). */
39
40
  export const CLAUDE_HOOKS = Object.keys(HOOKS).map((event) => ({ event, sub: HOOKS[event] }));
41
+ /**
42
+ * The permission-prompt hooks (0.13.0, permission.ts), for every tool: the prompt (PermissionRequest), the call it
43
+ * asked about running (PostToolUse, PostToolUseFailure) and the session ending (SessionEnd, with no timeout of ours:
44
+ * a longer one would make Claude Code wait longer for every SessionEnd hook). The next message's and the Stop hooks
45
+ * settle a session's prompts too.
46
+ */
47
+ export const PERMISSION_HOOKS = [
48
+ { event: 'PermissionRequest', sub: 'permission' },
49
+ { event: 'PostToolUse', sub: 'permission-done' },
50
+ { event: 'PostToolUseFailure', sub: 'permission-done' },
51
+ { event: 'SessionEnd', sub: 'permission-done', timeout: null },
52
+ ];
53
+ /**
54
+ * The first Claude Code with all of them (PermissionRequest came in 2.0.45, PostToolUseFailure by 2.1.119). An older
55
+ * one gets none: before 2.1.101, a hook event Claude Code didn't know made it ignore the whole settings file.
56
+ */
57
+ export const PERMISSION_CLAUDE = '2.1.119';
40
58
  /**
41
59
  * What Claude Code may do without stopping to ask: start the background wait, and use Pending You's own tools (ask
42
60
  * the person, read their answers). Without them, a question posted while the person is away would wait on a
@@ -100,12 +118,23 @@ const objectAt = (parent, key) => {
100
118
  /**
101
119
  * Adds our hooks and permissions, or updates our hooks in place (a new version, or a line an older version wrote, like
102
120
  * 0.2.0's `… --origin <url> handoff`, which 0.2.0 itself can't read). Returns the permissions it added.
121
+ * `permission` (0.13.0): the permission-prompt hooks' lines, by event, to add or update; null takes ours out (the
122
+ * person turned them off, or this Claude Code can't run them); left out, they're left as they are.
103
123
  */
104
- export function mergeSettings(settings, commands) {
124
+ export function mergeSettings(settings, commands, permission) {
105
125
  const next = structuredClone(settings);
106
126
  const merged = mergeHooks(hooksOf(next, 'Claude Code’s settings'), CLAUDE_HOOKS, commands, 'Claude Code');
107
- let changed = merged.changed;
108
- next.hooks = merged.hooks;
127
+ let hooks = merged.hooks;
128
+ let permissionChanged = false;
129
+ if (permission !== undefined) {
130
+ const prompts = permission
131
+ ? mergeHooks(hooks, PERMISSION_HOOKS, permission, 'Claude Code')
132
+ : removeHooks(hooks, PERMISSION_HOOKS);
133
+ hooks = prompts.hooks;
134
+ permissionChanged = prompts.changed;
135
+ }
136
+ let changed = merged.changed || permissionChanged;
137
+ next.hooks = hooks;
109
138
  const permissions = objectAt(next, 'permissions') ?? {};
110
139
  if (next.permissions !== undefined && objectAt(next, 'permissions') === null)
111
140
  throw new PlainError('Claude Code’s “permissions” isn’t an object, so I left the settings alone.');
@@ -116,7 +145,13 @@ export function mergeSettings(settings, commands) {
116
145
  next.permissions = permissions;
117
146
  changed = true;
118
147
  }
119
- return { settings: next, permissionsAdded, changed };
148
+ return {
149
+ settings: next,
150
+ permissionsAdded,
151
+ changed,
152
+ hooksChanged: merged.changed,
153
+ permissionChanged,
154
+ };
120
155
  }
121
156
  /** Removes our hooks, and the permissions init added; leaves everything else as it was. */
122
157
  export function unmergeSettings(settings, permissionsAdded) {
@@ -124,7 +159,7 @@ export function unmergeSettings(settings, permissionsAdded) {
124
159
  let changed = false;
125
160
  const hooks = objectAt(next, 'hooks');
126
161
  if (hooks) {
127
- const removed = removeHooks(hooks, CLAUDE_HOOKS);
162
+ const removed = removeHooks(hooks, [...CLAUDE_HOOKS, ...PERMISSION_HOOKS]);
128
163
  changed = removed.changed;
129
164
  if (Object.keys(removed.hooks).length === 0)
130
165
  delete next.hooks;
@@ -323,6 +358,17 @@ export async function removeSkill(skill) {
323
358
  }
324
359
  return text !== null ? { ok: true, text: `Kept ${skill.path}: it was changed after init.` } : null;
325
360
  }
361
+ /** Why Claude Code here can't have the permission-prompt hooks, in a line; null when it can. */
362
+ export function permissionBlocker(io, version, mode) {
363
+ if (io.platform === 'win32')
364
+ return 'not on Windows yet';
365
+ if (mode !== 'helper')
366
+ return 'they need Claude Code signed in through this computer’s sign-in, and here it signs in by itself';
367
+ const found = claudeVersion(version);
368
+ if (!found || !atLeast(found, PERMISSION_CLAUDE))
369
+ return `they need Claude Code ${PERMISSION_CLAUDE} or later${found ? ` (this is ${found})` : ''}`;
370
+ return null;
371
+ }
326
372
  /**
327
373
  * Everything init does in Claude Code (not the sign-in, nor the shared parts: the copy, the shim, the helper).
328
374
  * Idempotent. Each step is reported as it's done.
@@ -359,13 +405,30 @@ export async function installClaude(io, origin, options) {
359
405
  skill = await saveSkill(io, origin, skillPath(io), before?.skill ?? null);
360
406
  report(skill.step);
361
407
  const commands = Object.fromEntries(Object.keys(HOOKS).map((event) => [event, options.hookLine(HOOKS[event])]));
362
- const merged = mergeSettings(await readSettings(io), commands);
408
+ // The permission-prompt hooks (0.13.0): on unless the person turned them off, here or before.
409
+ const wanted = options.permissionCards ?? before?.permissionCards !== false;
410
+ const blocker = permissionBlocker(io, options.version, options.mode);
411
+ const permissionLines = wanted && !blocker
412
+ ? Object.fromEntries(PERMISSION_HOOKS.map((spec) => [spec.event, options.hookLine(spec.sub)]))
413
+ : null;
414
+ const merged = mergeSettings(await readSettings(io), commands, permissionLines);
363
415
  report({
364
416
  ok: true,
365
- text: merged.changed
417
+ text: merged.hooksChanged
366
418
  ? `Installed or updated the session-start pickup, next-message hand-off and Stop check hooks in ${settingsPath(io)}.`
367
419
  : 'The hooks were already installed.',
368
420
  });
421
+ const flag = originArgs(origin);
422
+ report({
423
+ ok: true,
424
+ text: permissionLines
425
+ ? merged.permissionChanged
426
+ ? 'Added the permission-prompt hooks: when Claude Code waits for your OK and you don’t answer within a few seconds, a card says so (a push only after your hand-off wait), and it goes once you answer in Claude Code.'
427
+ : 'The permission-prompt hooks were already installed.'
428
+ : !wanted
429
+ ? `${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}`
430
+ : `${merged.permissionChanged ? 'Took out' : 'Left out'} the permission-prompt hooks: ${blocker}.`,
431
+ });
369
432
  const mod = await addMod(io, merged.settings, options.copy, options.version);
370
433
  if (mod.step)
371
434
  report(mod.step);
@@ -389,6 +452,8 @@ export async function installClaude(io, origin, options) {
389
452
  : null,
390
453
  setup: { since: new Date(io.now()).toISOString() },
391
454
  ...(options.plugin ? { plugin: true } : {}),
455
+ permissionHooks: permissionLines,
456
+ permissionCards: wanted,
392
457
  };
393
458
  await writeWhole(manifestPath(io), `${JSON.stringify(manifest, null, 2)}\n`, { secret: true });
394
459
  return steps;
@@ -450,6 +515,8 @@ export async function uninstallClaude(io) {
450
515
  });
451
516
  if (modRemoved)
452
517
  steps.push({ ok: true, text: `Removed the wake mod from ${PLUGIN_DIRS}.` });
518
+ // Sessions' permission prompts: nothing will settle them now.
519
+ await removePermissionFiles(io);
453
520
  const skill = await removeSkill(manifest?.skill ?? null);
454
521
  if (skill)
455
522
  steps.push(skill);
@@ -489,6 +556,7 @@ export async function claudeState(io) {
489
556
  }
490
557
  const skill = (await readText(skillPath(io))) !== null;
491
558
  let ours = (_event) => undefined;
559
+ let permission = false;
492
560
  const mod = { loads: false, dirs: [], present: false, plugin: false };
493
561
  const loads = claude ? claudeVersion(claude) : null;
494
562
  mod.loads = loads !== null && loadsMods(loads);
@@ -500,6 +568,7 @@ export async function claudeState(io) {
500
568
  plugins = enabledPlugins(settings);
501
569
  const all = objectAt(settings, 'hooks') ?? {};
502
570
  ours = (event) => findHook(all, event, HOOKS[event]);
571
+ permission = Boolean(findHook(all, 'PermissionRequest', 'permission'));
503
572
  }
504
573
  catch { }
505
574
  const start = ours('SessionStart');
@@ -513,6 +582,7 @@ export async function claudeState(io) {
513
582
  skill,
514
583
  hooks: Boolean(start && ours('UserPromptSubmit')),
515
584
  stopcheck: Boolean(ours('Stop')),
585
+ permission,
516
586
  form: form ? { ...form, runs: await hookRuns(form), shim } : null,
517
587
  mod,
518
588
  plugins,
package/dist/files.js CHANGED
@@ -11,6 +11,11 @@ export function configDir(io) {
11
11
  const base = io.env.XDG_CONFIG_HOME || join(io.home, '.config');
12
12
  return join(base, 'pendingyou');
13
13
  }
14
+ /**
15
+ * The folder, under configDir, of Claude Code sessions' permission prompts (0.13.0, permission.ts): one file a session
16
+ * while it has a prompt or a card. The hooks' shim looks for any file here before it starts Node (shim.ts).
17
+ */
18
+ export const PERMISSION_FOLDER = 'permission-cards';
14
19
  /** Claude Code's own folder: ~/.claude, or $CLAUDE_CONFIG_DIR. */
15
20
  export function claudeDir(io) {
16
21
  return io.env.CLAUDE_CONFIG_DIR || join(io.home, '.claude');
package/dist/hooks.js CHANGED
@@ -67,19 +67,24 @@ export function mergeHooks(hooks, specs, lines, where) {
67
67
  throw new PlainError(`${where}’s ${spec.event} hooks aren’t a list, so I left them alone.`);
68
68
  const groups = Array.isArray(next[spec.event]) ? next[spec.event] : [];
69
69
  const ours = groups.filter((group) => (group?.hooks ?? []).some((hook) => isOurHook(hook?.command, spec.sub)));
70
+ const timeout = spec.timeout === undefined ? HOOK_TIMEOUT : spec.timeout;
70
71
  if (ours.length === 0) {
71
72
  groups.push({
72
73
  ...(spec.matcher ? { matcher: spec.matcher } : {}),
73
- hooks: [{ type: 'command', command: wanted, timeout: HOOK_TIMEOUT }],
74
+ hooks: [{ type: 'command', command: wanted, ...(timeout === null ? {} : { timeout }) }],
74
75
  });
75
76
  changed = true;
76
77
  }
77
78
  for (const group of ours) {
78
79
  for (const hook of group.hooks ?? [])
79
80
  if (isOurHook(hook.command, spec.sub) &&
80
- (hook.command !== wanted || hook.timeout !== HOOK_TIMEOUT)) {
81
+ (hook.command !== wanted ||
82
+ (timeout === null ? 'timeout' in hook : hook.timeout !== timeout))) {
81
83
  hook.command = wanted;
82
- hook.timeout = HOOK_TIMEOUT;
84
+ if (timeout === null)
85
+ delete hook.timeout;
86
+ else
87
+ hook.timeout = timeout;
83
88
  changed = true;
84
89
  }
85
90
  // The matcher is ours to set only on a group that holds nothing but our hook.
package/dist/init.js CHANGED
@@ -135,7 +135,7 @@ export async function init(io, options) {
135
135
  io.out(`Didn’t find ${namesOf([...APP_IDS], 'or')} on this computer’s PATH. Install one, then run: npx -y pendingyou@latest init${flag}\n`);
136
136
  return 1;
137
137
  }
138
- const versions = found.map(({ app, detected }) => `${app.name} ${detected.version}`);
138
+ const versions = found.map(({ app, detected }) => `${app.name} ${detected.version}${detected.from ? ` (${detected.from})` : ''}`);
139
139
  io.out(`Setting up Pending You (${options.origin}) for ${versions.length > 1 ? `${versions.slice(0, -1).join(', ')} and ${versions.at(-1)}` : versions[0]}.\n`);
140
140
  for (const problem of problems)
141
141
  io.out(`${problem}\n`);
@@ -145,6 +145,7 @@ export async function init(io, options) {
145
145
  oauth: Boolean(options.oauth),
146
146
  headless: headlessReason(io),
147
147
  progress: (text) => io.out(text),
148
+ ...(options.permissionCards === undefined ? {} : { permissionCards: options.permissionCards }),
148
149
  };
149
150
  // Everything decided before anyone signs in: Cancel then changes nothing.
150
151
  const prepared = [];
package/dist/io.js CHANGED
@@ -86,7 +86,7 @@ export function sleep(ms, signal) {
86
86
  signal?.addEventListener('abort', done, { once: true });
87
87
  });
88
88
  }
89
- function readStdin(timeoutMs) {
89
+ function readStdin(timeoutMs, maxChars = 1_000_000) {
90
90
  if (process.stdin.isTTY)
91
91
  return Promise.resolve('');
92
92
  return new Promise((resolve) => {
@@ -100,7 +100,7 @@ function readStdin(timeoutMs) {
100
100
  process.stdin.setEncoding('utf8');
101
101
  process.stdin.on('data', (chunk) => {
102
102
  text += chunk;
103
- if (text.length > 1_000_000)
103
+ if (text.length > maxChars)
104
104
  finish();
105
105
  });
106
106
  process.stdin.on('end', finish);
package/dist/main.js CHANGED
@@ -10,6 +10,7 @@ import { PlainError } from "./errors.js";
10
10
  import { hold } from "./hold.js";
11
11
  import { init, status, uninstall } from "./init.js";
12
12
  import { OAuthError } from "./oauth.js";
13
+ import { permissionHook, permissionWorker } from "./permission.js";
13
14
  import { pickup } from "./pickup.js";
14
15
  import { mcpHeaders } from "./remote.js";
15
16
  import { login, logout } from "./signin.js";
@@ -46,6 +47,19 @@ async function run(io, command, hook = { fallback: '' }) {
46
47
  return stopcheck(io, { origin: command.origin, app: command.app ?? DEFAULT_APP });
47
48
  case 'posted':
48
49
  return posted(io, { origin: command.origin, app: command.app ?? DEFAULT_APP });
50
+ case 'permission':
51
+ case 'permission-done':
52
+ // Claude Code's only (0.13.0): a hook line with another app does nothing.
53
+ if ((command.app ?? DEFAULT_APP) !== DEFAULT_APP)
54
+ return 0;
55
+ return permissionHook(io, { origin: command.origin });
56
+ case 'permission-card':
57
+ // Detached and quiet, like refresh: nobody reads what it prints.
58
+ try {
59
+ await permissionWorker(io, command);
60
+ }
61
+ catch { }
62
+ return 0;
49
63
  case 'listen':
50
64
  // Codex's is detached, started by `posted`: nobody reads what it prints. OpenCode's plugin and Pi's extension
51
65
  // read their own child's.