pendingyou 0.12.0 → 0.14.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 (54) hide show
  1. package/CHANGELOG.md +142 -5
  2. package/README.md +112 -28
  3. package/dist/api.js +0 -50
  4. package/dist/apps/claude-code.js +16 -36
  5. package/dist/apps/codex-app.js +26 -0
  6. package/dist/apps/codex-wake.js +299 -120
  7. package/dist/apps/codex.js +51 -83
  8. package/dist/apps/ids.js +0 -11
  9. package/dist/apps/opencode-plugin.js +39 -65
  10. package/dist/apps/opencode.js +0 -70
  11. package/dist/apps/pi-extension.js +63 -73
  12. package/dist/apps/pi.js +0 -62
  13. package/dist/apps/registry.js +0 -2
  14. package/dist/args.js +45 -39
  15. package/dist/bridge.js +71 -83
  16. package/dist/claude.js +63 -87
  17. package/dist/cli.js +0 -2
  18. package/dist/confirm.js +0 -69
  19. package/dist/credentials.js +0 -27
  20. package/dist/environment.js +0 -28
  21. package/dist/errors.js +0 -1
  22. package/dist/files.js +1 -12
  23. package/dist/format.js +30 -13
  24. package/dist/handed.js +71 -0
  25. package/dist/hold.js +0 -24
  26. package/dist/hooks.js +8 -29
  27. package/dist/index.js +0 -2
  28. package/dist/init.js +2 -33
  29. package/dist/install.js +0 -32
  30. package/dist/io.js +2 -12
  31. package/dist/json.js +0 -29
  32. package/dist/jsonc.js +0 -33
  33. package/dist/main.js +15 -24
  34. package/dist/mod.js +0 -23
  35. package/dist/oauth.js +0 -45
  36. package/dist/permission.js +631 -0
  37. package/dist/pickup.js +109 -63
  38. package/dist/qr.js +0 -28
  39. package/dist/redact.js +292 -0
  40. package/dist/remote.js +0 -90
  41. package/dist/setup.js +8 -27
  42. package/dist/shim.js +8 -24
  43. package/dist/signin.js +0 -42
  44. package/dist/state.js +19 -10
  45. package/dist/stopcheck.js +50 -84
  46. package/dist/toml.js +0 -34
  47. package/dist/version.js +1 -2
  48. package/dist/watch.js +3 -20
  49. package/mod/.claude-plugin/plugin.json +1 -1
  50. package/mod/README.md +2 -2
  51. package/mod/hooks/register.js +109 -62
  52. package/mod/hooks/wake.js +119 -104
  53. package/package.json +2 -1
  54. package/mod/tests/wake.test.ts +0 -247
@@ -1,17 +1,4 @@
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
- import { addCommand, claudeState, helperServer, installClaude, mcpFromClaude, mcpFromFile, PLUGIN_ORIGINS, pluginFor, readManifest, removeCommand, sameUrl, uninstallClaude, } from "../claude.js";
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";
17
4
  import { originArgs } from "../hooks.js";
@@ -21,12 +8,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";
23
10
  const NAME = APP_NAMES['claude-code'];
24
- /** The sentence's lead-in, and what restarting means, in init's last lines. */
25
11
  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
12
  async function switches(io, ctx, what) {
31
13
  if (ctx.headless || ctx.yes)
32
14
  return true;
@@ -36,7 +18,6 @@ async function switches(io, ctx, what) {
36
18
  const answer = await io.ask('Switch Claude Code to this computer’s sign-in? [Y/n] ');
37
19
  return !/^\s*no?\s*$/i.test(answer);
38
20
  }
39
- /** The plan for a server at another Pending You: replaced once the person says so, or Claude Code left alone. */
40
21
  async function replaces(io, ctx, server, url) {
41
22
  ctx.progress(`Claude Code’s pendingyou MCP server points at ${server.url}, not ${url}.\n`);
42
23
  if (ctx.yes)
@@ -92,7 +73,6 @@ export const claudeCode = {
92
73
  };
93
74
  }
94
75
  else if (server?.url && !sameUrl(server.url, url)) {
95
- // Another Pending You (staging, when production was asked for): asked before anyone signs in.
96
76
  if (!(await replaces(io, ctx, server, url))) {
97
77
  const commands = ` ${removeCommand(server.scope)}\n ${add}`;
98
78
  return {
@@ -123,7 +103,6 @@ export const claudeCode = {
123
103
  else if (ours)
124
104
  plan = { action: 'switch', server };
125
105
  else if (server.helper) {
126
- // Someone else's headersHelper: theirs to change. The hooks hear through a sign-in of their own.
127
106
  mode = 'oauth';
128
107
  plan = helperMode
129
108
  ? {
@@ -152,7 +131,6 @@ export const claudeCode = {
152
131
  }
153
132
  }
154
133
  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
134
  if (helperMode &&
157
135
  (await switches(io, ctx, 'The Pending You plugin signs Claude Code in to Pending You by itself here.')))
158
136
  plan = { action: 'add', plugin: true };
@@ -185,6 +163,7 @@ export const claudeCode = {
185
163
  hookLine: ictx.hookLine,
186
164
  report: ictx.report,
187
165
  progress: ictx.progress,
166
+ ...(ctx.permissionCards === undefined ? {} : { permissionCards: ctx.permissionCards }),
188
167
  }),
189
168
  next: (ictx) => {
190
169
  if (mode === 'oauth') {
@@ -223,7 +202,19 @@ export const claudeCode = {
223
202
  return claudeStatus(io, ctx);
224
203
  },
225
204
  };
226
- /** Claude Code's section of `pendingyou status`: what's set up and what's missing, each with the command that fixes it. */
205
+ function permissionLine(io, claude, manifest, helper, flag) {
206
+ const label = 'Permission prompts:';
207
+ if (claude.permission)
208
+ return ` ok ${label} a card when Claude Code waits for your OK`;
209
+ if (manifest?.permissionCards === false)
210
+ return ` ${label} off, as you chose; for a card when Claude Code waits for your OK, run npx pendingyou init --permission-cards${flag}`;
211
+ const blocker = claude.claude
212
+ ? permissionBlocker(io, claude.claude, helper ? 'helper' : 'oauth')
213
+ : null;
214
+ return blocker
215
+ ? ` ${label} no card when Claude Code waits for your OK: ${blocker}`
216
+ : ` missing ${label} no card when Claude Code waits for your OK; run npx pendingyou@latest init${flag}`;
217
+ }
227
218
  async function claudeStatus(io, ctx) {
228
219
  const { origin, signIn } = ctx;
229
220
  const [claude, here, manifest] = await Promise.all([
@@ -234,8 +225,6 @@ async function claudeStatus(io, ctx) {
234
225
  const credential = await readCredential(io, origin);
235
226
  const flag = originArgs(origin);
236
227
  const mcp = claude.mcp;
237
- // Through the helper (this computer's own sign-in): Claude Code's server runs ours, init set it up so, or nothing is
238
- // set up yet and init would (everywhere but Windows).
239
228
  const helped = usesHelper(mcp, origin);
240
229
  const helper = io.platform !== 'win32' &&
241
230
  (helped ||
@@ -249,7 +238,6 @@ async function claudeStatus(io, ctx) {
249
238
  const url = `${origin}/mcp`;
250
239
  const plugin = claude.plugins.find((id) => siteOf(PLUGIN_ORIGINS[id] ?? '') === siteOf(origin));
251
240
  const sameServer = mcp !== false && mcp !== 'unknown' && (!mcp.url || mcp.url.replace(/\/+$/, '') === url);
252
- // The plugin's server is Claude Code's when nobody added one by hand.
253
241
  const pluginServer = mcp === false && Boolean(plugin);
254
242
  const mcpOk = helper ? sameServer && helped : sameServer || pluginServer;
255
243
  const mcpText = mcp === 'unknown'
@@ -269,7 +257,6 @@ async function claudeStatus(io, ctx) {
269
257
  : `pendingyou (${mcp.url}) signs in through a browser${why ? ', which can’t finish on this computer' : ''}; run npx pendingyou init${flag}`;
270
258
  const form = claude.form;
271
259
  const hooksRun = form?.runs ?? true;
272
- // The wake mod (0.10.0): Claude Code 2.1.287 or later loads it from the plugin or from CLAUDE_CODE_PLUGIN_DIRS.
273
260
  const mod = claude.mod;
274
261
  const modOk = mod.loads && (mod.plugin || mod.present);
275
262
  const modText = !claude.claude
@@ -293,9 +280,7 @@ async function claudeStatus(io, ctx) {
293
280
  : form.form === 'npx'
294
281
  ? `they run through npx${via}, which is slower; run npx pendingyou@latest init${flag} to run them directly`
295
282
  : 'they run pendingyou from PATH or a file of your own';
296
- // login makes a connection only once Claude Code's server signs in through the helper; init does both.
297
283
  const fix = `npx pendingyou ${helped ? 'login' : 'init'}${flag}`;
298
- // The connection's name as Pending You has it, so status never drifts from it; else this computer's own record.
299
284
  const title = signIn.state === 'ok' && signIn.for
300
285
  ? connectionTitle(signIn.for)
301
286
  : `${NAME} on ${(await connectionMachine(io, origin)) ?? (machineOf(io.host) || 'this computer')}`;
@@ -320,7 +305,6 @@ async function claudeStatus(io, ctx) {
320
305
  : signIn.state === 'none'
321
306
  ? `not signed in; run npx pendingyou login${flag}`
322
307
  : 'Pending You couldn’t be reached';
323
- // The helper as Claude Code runs it at its barest: found Node, and runs pendingyou's copy.
324
308
  const check = helper ? await checkHelper(io, origin) : null;
325
309
  const skillOk = claude.skill || Boolean(plugin);
326
310
  const skillText = claude.skill
@@ -338,18 +322,14 @@ async function claudeStatus(io, ctx) {
338
322
  ? [
339
323
  ` ok Plugin: ${plugin} is enabled${pluginServer ? '' : mcp !== false && mcp !== 'unknown' ? '; Claude Code uses the server added by hand instead of its own' : ''}`,
340
324
  ]
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}`);
342
- // How setup stands on Pending You (from servers that say): never a reason it isn't Ready.
325
+ : []), ` ${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));
343
326
  const setup = setupStatus(NAME, signIn.state === 'ok' ? setupOf(signIn.for) : null);
344
327
  if (setup)
345
328
  lines.push(` ${setup === 'finished' ? 'ok ' : ' '} Setup: ${setup}`);
346
- // A folder's own server (local or project scope) wins over the user one in Claude Code: say when it's elsewhere. (The
347
- // plugin's server counts only when no server was added by hand, and is production's.)
348
329
  if (here && (here.scope === 'local' || here.scope === 'project') && differs(here, origin))
349
330
  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`);
350
331
  const user = mcp !== false && mcp !== 'unknown' && mcp.url ? { url: mcp.url, scope: 'user' } : null;
351
332
  const other = differs(here, origin) ? here : differs(user, origin) ? user : null;
352
- // Through the helper, Claude Code reaches Pending You only with the helper, the connection and the server together.
353
333
  const reaches = !helper || (signOk && mcpOk && check?.ok === true);
354
334
  const ready = reaches && signIn.state === 'ok' && claude.hooks && hooksRun && !other;
355
335
  if (other)
@@ -0,0 +1,26 @@
1
+ import { stat } from 'node:fs/promises';
2
+ import { join } from 'node:path';
3
+ const APP_NAMES = ['ChatGPT.app', 'Codex.app'];
4
+ export function appCodexPaths(home, system = '/Applications') {
5
+ return [system, join(home, 'Applications')].flatMap((root) => APP_NAMES.flatMap((name) => {
6
+ const cli = join(root, name, 'Contents', 'Resources', 'codex-cli');
7
+ return [join(cli, 'bin', 'codex'), join(cli, 'CodexCLI.app', 'Contents', 'MacOS', 'codex')];
8
+ }));
9
+ }
10
+ const versionOf = (stdout) => /(\d+\.\d+\.\d+)/.exec(stdout)?.[1] ?? 'installed';
11
+ const executable = (path) => stat(path).then((info) => info.isFile() && (info.mode & 0o111) !== 0, () => false);
12
+ export async function findCodex(io, paths = appCodexPaths(io.home, io.env.PENDINGYOU_APPLICATIONS_DIR || undefined)) {
13
+ const onPath = await io.run('codex', ['--version'], 20_000);
14
+ if (onPath.code === 0)
15
+ return { command: 'codex', version: versionOf(onPath.stdout), app: false };
16
+ if (io.platform !== 'darwin')
17
+ return null;
18
+ for (const path of paths) {
19
+ if (!(await executable(path)))
20
+ continue;
21
+ const result = await io.run(path, ['--version'], 20_000);
22
+ if (result.code === 0)
23
+ return { command: path, version: versionOf(result.stdout), app: true };
24
+ }
25
+ return null;
26
+ }