pendingyou 0.13.0 → 0.15.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.
Files changed (56) hide show
  1. package/CHANGELOG.md +134 -11
  2. package/README.md +114 -34
  3. package/dist/api.js +27 -50
  4. package/dist/apps/claude-code.js +14 -49
  5. package/dist/apps/codex-app.js +0 -18
  6. package/dist/apps/codex-wake.js +290 -122
  7. package/dist/apps/codex.js +102 -100
  8. package/dist/apps/ids.js +0 -11
  9. package/dist/apps/opencode-plugin.js +91 -67
  10. package/dist/apps/opencode.js +13 -80
  11. package/dist/apps/pi-extension.js +100 -75
  12. package/dist/apps/pi.js +13 -72
  13. package/dist/apps/registry.js +0 -2
  14. package/dist/apps/switch.js +15 -0
  15. package/dist/args.js +72 -41
  16. package/dist/bridge.js +0 -94
  17. package/dist/claude.js +6 -96
  18. package/dist/cli.js +0 -2
  19. package/dist/confirm.js +0 -69
  20. package/dist/credentials.js +0 -27
  21. package/dist/environment.js +0 -28
  22. package/dist/errors.js +0 -1
  23. package/dist/files.js +0 -16
  24. package/dist/format.js +59 -13
  25. package/dist/handed.js +87 -0
  26. package/dist/hold.js +0 -24
  27. package/dist/hooks.js +2 -27
  28. package/dist/index.js +0 -2
  29. package/dist/init.js +0 -32
  30. package/dist/install.js +0 -32
  31. package/dist/io.js +1 -10
  32. package/dist/json.js +0 -29
  33. package/dist/jsonc.js +0 -33
  34. package/dist/main.js +29 -31
  35. package/dist/mod.js +0 -23
  36. package/dist/oauth.js +0 -45
  37. package/dist/permission.js +118 -144
  38. package/dist/pickup.js +113 -63
  39. package/dist/presence.js +330 -0
  40. package/dist/qr.js +0 -28
  41. package/dist/redact.js +0 -77
  42. package/dist/remote.js +0 -90
  43. package/dist/setup.js +0 -30
  44. package/dist/shim.js +0 -23
  45. package/dist/signin.js +0 -42
  46. package/dist/state.js +19 -10
  47. package/dist/stopcheck.js +42 -86
  48. package/dist/toml.js +0 -34
  49. package/dist/version.js +1 -2
  50. package/dist/watch.js +3 -20
  51. package/mod/.claude-plugin/plugin.json +1 -1
  52. package/mod/README.md +2 -2
  53. package/mod/hooks/register.js +142 -61
  54. package/mod/hooks/wake.js +179 -108
  55. package/package.json +2 -1
  56. package/mod/tests/wake.test.ts +0 -247
@@ -1,16 +1,3 @@
1
- // Claude Code (0.11.0's module of what claude.ts carries out): what `pendingyou init` decides before anyone signs in,
2
- // what it does after, `status`'s section and the lines init ends with.
3
- //
4
- // Its MCP server signs in through this computer's own connection ("Claude Code on devbox"), by the headers helper, on
5
- // every computer but Windows (where Claude Code runs a headersHelper through cmd) or with `--oauth`; the hooks use the
6
- // same connection, so one approval covers both. A pendingyou server that's there decides the rest, before signing in:
7
- // - ours, through the helper, at this Pending You: kept.
8
- // - one at this Pending You that signs in by itself (Claude Code's own /mcp → Authenticate), or no server beside an
9
- // enabled Pending You plugin for it (whose server is OAuth): with no browser here it's switched without a word (as
10
- // 0.10.0 did: that sign-in can't finish here); at a terminal init asks, Yes by default; `--yes` switches; with nobody
11
- // to ask it's kept, and init says how to switch. Kept, Claude Code signs in by itself and the hooks get a sign-in
12
- // that only hears; kept beside the plugin, no skill is saved either: the plugin carries it.
13
- // - one at another Pending You: replaced once the person says so (or `--yes`), or else Claude Code is left alone.
14
1
  import { addCommand, claudeState, helperServer, installClaude, mcpFromClaude, mcpFromFile, PLUGIN_ORIGINS, permissionBlocker, pluginFor, readManifest, removeCommand, sameUrl, uninstallClaude, } from "../claude.js";
15
2
  import { connectionMachine, readCredential } from "../credentials.js";
16
3
  import { differs, serverForFolder, siteOf } from "../environment.js";
@@ -20,23 +7,12 @@ import { checkHelper, foundBy, headlessReason, helperCommand, helperPath, isOurH
20
7
  import { connectionTitle, FINISH_SAY, setupOf, setupStatus } from "../setup.js";
21
8
  import { shimPath } from "../shim.js";
22
9
  import { APP_NAMES } from "./ids.js";
10
+ import { switches } from "./switch.js";
23
11
  const NAME = APP_NAMES['claude-code'];
24
- /** The sentence's lead-in, and what restarting means, in init's last lines. */
25
12
  const RESTART = 'restart Claude Code here (/exit, then claude --continue), or start it in the folder you work in';
26
- /**
27
- * Whether to move a server that signs in by itself to this computer's sign-in: without a word where there's no
28
- * browser, `--yes`, or Yes (the default) at a terminal; kept, with nobody to ask.
29
- */
30
- async function switches(io, ctx, what) {
31
- if (ctx.headless || ctx.yes)
32
- return true;
33
- if (!io.interactive)
34
- return false;
35
- io.out(`${what} init can move Claude Code to this computer’s own sign-in, which the hooks use too: one sign-in for both, and nothing to Authenticate. The connection it has now stays on your Assistants page until you remove it, and answers to cards it asked through that one won’t reach Claude Code by themselves.\n`);
36
- const answer = await io.ask('Switch Claude Code to this computer’s sign-in? [Y/n] ');
37
- return !/^\s*no?\s*$/i.test(answer);
13
+ async function switchesFor(io, ctx, what) {
14
+ return switches(io, ctx, { name: 'Claude Code', what, uses: 'the hooks use', headless: true });
38
15
  }
39
- /** The plan for a server at another Pending You: replaced once the person says so, or Claude Code left alone. */
40
16
  async function replaces(io, ctx, server, url) {
41
17
  ctx.progress(`Claude Code’s pendingyou MCP server points at ${server.url}, not ${url}.\n`);
42
18
  if (ctx.yes)
@@ -92,7 +68,6 @@ export const claudeCode = {
92
68
  };
93
69
  }
94
70
  else if (server?.url && !sameUrl(server.url, url)) {
95
- // Another Pending You (staging, when production was asked for): asked before anyone signs in.
96
71
  if (!(await replaces(io, ctx, server, url))) {
97
72
  const commands = ` ${removeCommand(server.scope)}\n ${add}`;
98
73
  return {
@@ -123,7 +98,6 @@ export const claudeCode = {
123
98
  else if (ours)
124
99
  plan = { action: 'switch', server };
125
100
  else if (server.helper) {
126
- // Someone else's headersHelper: theirs to change. The hooks hear through a sign-in of their own.
127
101
  mode = 'oauth';
128
102
  plan = helperMode
129
103
  ? {
@@ -138,7 +112,7 @@ export const claudeCode = {
138
112
  };
139
113
  }
140
114
  else if (helperMode &&
141
- (await switches(io, ctx, 'Claude Code signs in to Pending You by itself here (/mcp → Authenticate).')))
115
+ (await switchesFor(io, ctx, 'Claude Code signs in to Pending You by itself here (/mcp → Authenticate).')))
142
116
  plan = { action: 'switch', server };
143
117
  else {
144
118
  mode = 'oauth';
@@ -152,9 +126,8 @@ export const claudeCode = {
152
126
  }
153
127
  }
154
128
  else if (plugin) {
155
- // No server added by hand, so the Pending You plugin's is Claude Code's: one added by hand would win over it.
156
129
  if (helperMode &&
157
- (await switches(io, ctx, 'The Pending You plugin signs Claude Code in to Pending You by itself here.')))
130
+ (await switchesFor(io, ctx, 'The Pending You plugin signs Claude Code in to Pending You by itself here.')))
158
131
  plan = { action: 'add', plugin: true };
159
132
  else {
160
133
  mode = 'oauth';
@@ -224,10 +197,6 @@ export const claudeCode = {
224
197
  return claudeStatus(io, ctx);
225
198
  },
226
199
  };
227
- /**
228
- * status's line for the permission-prompt hooks (0.13.0): on; off, as the person chose; or why Claude Code here can't
229
- * have them. Never a reason status isn't Ready.
230
- */
231
200
  function permissionLine(io, claude, manifest, helper, flag) {
232
201
  const label = 'Permission prompts:';
233
202
  if (claude.permission)
@@ -241,7 +210,14 @@ function permissionLine(io, claude, manifest, helper, flag) {
241
210
  ? ` ${label} no card when Claude Code waits for your OK: ${blocker}`
242
211
  : ` missing ${label} no card when Claude Code waits for your OK; run npx pendingyou@latest init${flag}`;
243
212
  }
244
- /** Claude Code's section of `pendingyou status`: what's set up and what's missing, each with the command that fixes it. */
213
+ function presenceLine(claude, connected, mod, flag) {
214
+ const label = 'Presence:';
215
+ if (!connected)
216
+ return ` ${label} off: it needs Claude Code signed in through this computer’s own connection`;
217
+ if (!claude.presence || !claude.hooks)
218
+ return ` missing ${label} Pending You isn’t told when a session closes; run npx pendingyou@latest init${flag}`;
219
+ return ` ok ${label} tells Pending You when this session is open (${mod ? 'as it starts, then every 5 minutes from the wake mod' : 'as it starts and with your messages'}; closed as it ends)`;
220
+ }
245
221
  async function claudeStatus(io, ctx) {
246
222
  const { origin, signIn } = ctx;
247
223
  const [claude, here, manifest] = await Promise.all([
@@ -252,8 +228,6 @@ async function claudeStatus(io, ctx) {
252
228
  const credential = await readCredential(io, origin);
253
229
  const flag = originArgs(origin);
254
230
  const mcp = claude.mcp;
255
- // Through the helper (this computer's own sign-in): Claude Code's server runs ours, init set it up so, or nothing is
256
- // set up yet and init would (everywhere but Windows).
257
231
  const helped = usesHelper(mcp, origin);
258
232
  const helper = io.platform !== 'win32' &&
259
233
  (helped ||
@@ -267,7 +241,6 @@ async function claudeStatus(io, ctx) {
267
241
  const url = `${origin}/mcp`;
268
242
  const plugin = claude.plugins.find((id) => siteOf(PLUGIN_ORIGINS[id] ?? '') === siteOf(origin));
269
243
  const sameServer = mcp !== false && mcp !== 'unknown' && (!mcp.url || mcp.url.replace(/\/+$/, '') === url);
270
- // The plugin's server is Claude Code's when nobody added one by hand.
271
244
  const pluginServer = mcp === false && Boolean(plugin);
272
245
  const mcpOk = helper ? sameServer && helped : sameServer || pluginServer;
273
246
  const mcpText = mcp === 'unknown'
@@ -287,7 +260,6 @@ async function claudeStatus(io, ctx) {
287
260
  : `pendingyou (${mcp.url}) signs in through a browser${why ? ', which can’t finish on this computer' : ''}; run npx pendingyou init${flag}`;
288
261
  const form = claude.form;
289
262
  const hooksRun = form?.runs ?? true;
290
- // The wake mod (0.10.0): Claude Code 2.1.287 or later loads it from the plugin or from CLAUDE_CODE_PLUGIN_DIRS.
291
263
  const mod = claude.mod;
292
264
  const modOk = mod.loads && (mod.plugin || mod.present);
293
265
  const modText = !claude.claude
@@ -311,9 +283,7 @@ async function claudeStatus(io, ctx) {
311
283
  : form.form === 'npx'
312
284
  ? `they run through npx${via}, which is slower; run npx pendingyou@latest init${flag} to run them directly`
313
285
  : 'they run pendingyou from PATH or a file of your own';
314
- // login makes a connection only once Claude Code's server signs in through the helper; init does both.
315
286
  const fix = `npx pendingyou ${helped ? 'login' : 'init'}${flag}`;
316
- // The connection's name as Pending You has it, so status never drifts from it; else this computer's own record.
317
287
  const title = signIn.state === 'ok' && signIn.for
318
288
  ? connectionTitle(signIn.for)
319
289
  : `${NAME} on ${(await connectionMachine(io, origin)) ?? (machineOf(io.host) || 'this computer')}`;
@@ -338,7 +308,6 @@ async function claudeStatus(io, ctx) {
338
308
  : signIn.state === 'none'
339
309
  ? `not signed in; run npx pendingyou login${flag}`
340
310
  : 'Pending You couldn’t be reached';
341
- // The helper as Claude Code runs it at its barest: found Node, and runs pendingyou's copy.
342
311
  const check = helper ? await checkHelper(io, origin) : null;
343
312
  const skillOk = claude.skill || Boolean(plugin);
344
313
  const skillText = claude.skill
@@ -356,18 +325,14 @@ async function claudeStatus(io, ctx) {
356
325
  ? [
357
326
  ` ok Plugin: ${plugin} is enabled${pluginServer ? '' : mcp !== false && mcp !== 'unknown' ? '; Claude Code uses the server added by hand instead of its own' : ''}`,
358
327
  ]
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));
360
- // How setup stands on Pending You (from servers that say): never a reason it isn't Ready.
328
+ : []), ` ${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));
361
329
  const setup = setupStatus(NAME, signIn.state === 'ok' ? setupOf(signIn.for) : null);
362
330
  if (setup)
363
331
  lines.push(` ${setup === 'finished' ? 'ok ' : ' '} Setup: ${setup}`);
364
- // A folder's own server (local or project scope) wins over the user one in Claude Code: say when it's elsewhere. (The
365
- // plugin's server counts only when no server was added by hand, and is production's.)
366
332
  if (here && (here.scope === 'local' || here.scope === 'project') && differs(here, origin))
367
333
  lines.push(` ${mark(false)} This folder: its ${here.scope}-scope pendingyou MCP server (${here.url}) is another Pending You than ${origin}; Claude Code uses it here instead`);
368
334
  const user = mcp !== false && mcp !== 'unknown' && mcp.url ? { url: mcp.url, scope: 'user' } : null;
369
335
  const other = differs(here, origin) ? here : differs(user, origin) ? user : null;
370
- // Through the helper, Claude Code reaches Pending You only with the helper, the connection and the server together.
371
336
  const reaches = !helper || (signOk && mcpOk && check?.ok === true);
372
337
  const ready = reaches && signIn.state === 'ok' && claude.hooks && hooksRun && !other;
373
338
  if (other)
@@ -1,32 +1,14 @@
1
- // The Codex app for macOS (0.13.0; research 2026-10-04): "ChatGPT.app" (bundle id com.openai.codex; 26.928 carries codex
2
- // 0.159.2) has its own codex command line inside it, and shares ~/.codex with Codex CLI: its MCP servers (the headers
3
- // helper too), ~/.agents/skills, ~/.codex/hooks.json and the hooks' trust. Its hooks run with a PATH that ends in its own
4
- // codex, and its private Codex server reads ~/.codex/queue_1.sqlite every 10 seconds, so `codex queue --thread` wakes a
5
- // chat the app has loaded while it's open (an idle chat is let go after 3 hours, or behind 10 newer idle ones; then the
6
- // message waits until the chat is opened again). Where `codex` isn't on PATH, init, status and the listener
7
- // (codex-wake.ts) run the app's own instead: these paths, in /Applications or ~/Applications, under either name.
8
1
  import { stat } from 'node:fs/promises';
9
2
  import { join } from 'node:path';
10
- /** The app's names: the ChatGPT desktop app that carries Codex, and Codex's own, should it ship as that. */
11
3
  const APP_NAMES = ['ChatGPT.app', 'Codex.app'];
12
- /**
13
- * Where a Codex app keeps its own codex: `Contents/Resources/codex-cli/bin/codex`, and the CodexCLI.app inside it that
14
- * the app's own code looks for (`codex-cli/CodexCLI.app/Contents/MacOS/codex`), in /Applications, then ~/Applications.
15
- * `system` is /Applications unless PENDINGYOU_APPLICATIONS_DIR says otherwise (tests: they never look in the real one).
16
- */
17
4
  export function appCodexPaths(home, system = '/Applications') {
18
5
  return [system, join(home, 'Applications')].flatMap((root) => APP_NAMES.flatMap((name) => {
19
6
  const cli = join(root, name, 'Contents', 'Resources', 'codex-cli');
20
7
  return [join(cli, 'bin', 'codex'), join(cli, 'CodexCLI.app', 'Contents', 'MacOS', 'codex')];
21
8
  }));
22
9
  }
23
- /** `codex --version`'s version ("codex-cli 0.159.2"), or "installed" when it can't be read. */
24
10
  const versionOf = (stdout) => /(\d+\.\d+\.\d+)/.exec(stdout)?.[1] ?? 'installed';
25
11
  const executable = (path) => stat(path).then((info) => info.isFile() && (info.mode & 0o111) !== 0, () => false);
26
- /**
27
- * The codex to run: `codex` on PATH; else, on a Mac, the Codex app's own (the first of `paths` there that answers
28
- * `--version`); null when there's neither.
29
- */
30
12
  export async function findCodex(io, paths = appCodexPaths(io.home, io.env.PENDINGYOU_APPLICATIONS_DIR || undefined)) {
31
13
  const onPath = await io.run('codex', ['--version'], 20_000);
32
14
  if (onPath.code === 0)