pendingyou 0.10.0 → 0.12.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/api.js CHANGED
@@ -9,15 +9,15 @@
9
9
  // a hook that needs a new token starts `pendingyou refresh` detached (background), which runs to the end under the
10
10
  // lock whatever happens to the hook, and waits for it only briefly; until it lands the hook uses the token it has, or
11
11
  // gives up and prints its plain fallback.
12
+ //
13
+ // Since 0.11.0 each app has its own sign-in (credentials.ts), and every call names the app it's for: its sign-in is
14
+ // the one used, and a session's request names it as `source` (routing by sender, 2026-10-04), so Pending You returns
15
+ // only that app's cards, never one Codex posted from the same folder to a Claude Code session there. `watch` names no
16
+ // app: it hands answers to a script, not a session, with Claude Code's sign-in as before.
17
+ import { DEFAULT_APP } from "./apps/ids.js";
12
18
  import { credentialsPath, readCredential, updateCredential, } from "./credentials.js";
13
19
  import { isLocked } from "./files.js";
14
20
  import { OAuthError, refresh } from "./oauth.js";
15
- /**
16
- * The app whose sessions the command line hands answers to: its hooks and holds run in Claude Code. Routing by sender
17
- * (2026-10-04): every request for a session names it, so Pending You returns only Claude Code's cards, never one Codex
18
- * or another app posted from the same folder. `watch` names none: it hands answers to a script, not a session.
19
- */
20
- export const SESSION_APP = 'claude-code';
21
21
  /** Refresh this long before the access token runs out. */
22
22
  const EARLY_MS = 60_000;
23
23
  /** A hook starts a background refresh this long before the access token runs out, and goes on with the old one. */
@@ -49,7 +49,7 @@ export class Unavailable extends Error {
49
49
  * Refreshes the sign-in under the lock unless it no longer needs it: a token with more than `earlyMs` left (or, when
50
50
  * `force`d after a 401, a refresh token another process already replaced) is used as it is.
51
51
  */
52
- async function refreshStored(io, origin, stored, options) {
52
+ async function refreshStored(io, origin, stored, options, app) {
53
53
  let ended = false;
54
54
  const updated = await updateCredential(io, origin, async (current) => {
55
55
  if (!current)
@@ -71,35 +71,43 @@ async function refreshStored(io, origin, stored, options) {
71
71
  }
72
72
  throw error;
73
73
  }
74
- });
74
+ }, app);
75
75
  if (!updated)
76
76
  throw new SignInNeeded(ended);
77
77
  return updated;
78
78
  }
79
- async function freshToken(io, origin, force) {
80
- const stored = await readCredential(io, origin);
79
+ async function freshToken(io, origin, force, app) {
80
+ const stored = await readCredential(io, origin, app);
81
81
  if (!stored)
82
82
  throw new SignInNeeded(false);
83
83
  if (!force && stored.expiresAt - EARLY_MS > io.now())
84
84
  return stored.accessToken;
85
- return (await refreshStored(io, origin, stored, { force, earlyMs: EARLY_MS })).accessToken;
85
+ return (await refreshStored(io, origin, stored, { force, earlyMs: EARLY_MS }, app)).accessToken;
86
86
  }
87
+ /** What a process starts to refresh an app's sign-in in the background: `--app` only for an app but Claude Code. */
88
+ export const refreshArgs = (origin, app, force = false) => [
89
+ 'refresh',
90
+ '--origin',
91
+ origin,
92
+ ...(app === DEFAULT_APP ? [] : ['--app', app]),
93
+ ...(force ? ['--force'] : []),
94
+ ];
87
95
  /**
88
96
  * `pendingyou refresh`, what a hook starts in the background: refreshes the sign-in when it runs out within SOON_MS
89
97
  * (or `force`, after a 401), under the lock, to the end. Quiet; true when there's a sign-in afterwards.
90
98
  */
91
- export async function refreshInBackground(io, origin, force) {
92
- const stored = await readCredential(io, origin);
99
+ export async function refreshInBackground(io, origin, force, app = DEFAULT_APP) {
100
+ const stored = await readCredential(io, origin, app);
93
101
  if (!stored)
94
102
  return false;
95
103
  if (!force && stored.expiresAt - SOON_MS > io.now())
96
104
  return true;
97
- await refreshStored(io, origin, stored, { force, earlyMs: SOON_MS });
105
+ await refreshStored(io, origin, stored, { force, earlyMs: SOON_MS }, app);
98
106
  return true;
99
107
  }
100
108
  /** A hook's token: never refreshed here (see the top of this file). */
101
- async function hookToken(io, origin, force, signal) {
102
- const stored = await readCredential(io, origin);
109
+ async function hookToken(io, origin, force, signal, app) {
110
+ const stored = await readCredential(io, origin, app);
103
111
  if (!stored)
104
112
  throw new SignInNeeded(false);
105
113
  const left = stored.expiresAt - io.now();
@@ -107,14 +115,14 @@ async function hookToken(io, origin, force, signal) {
107
115
  return stored.accessToken;
108
116
  // One refresher at a time: a lock held now is a refresh (or a write) already under way.
109
117
  if (!(await isLocked(credentialsPath(io))))
110
- io.background(['refresh', '--origin', origin, ...(force ? ['--force'] : [])]);
118
+ io.background(refreshArgs(origin, app, force));
111
119
  // Still good for a few seconds: this request goes with it, the next one with the new token.
112
120
  if (!force && left > USABLE_MS)
113
121
  return stored.accessToken;
114
122
  const until = io.now() + REFRESH_WAIT_MS;
115
123
  while (io.now() < until && !signal?.aborted) {
116
124
  await io.sleep(50, signal);
117
- const current = await readCredential(io, origin);
125
+ const current = await readCredential(io, origin, app);
118
126
  if (!current)
119
127
  throw new SignInNeeded(true);
120
128
  if (current.refreshToken !== stored.refreshToken)
@@ -123,29 +131,31 @@ async function hookToken(io, origin, force, signal) {
123
131
  throw new Unavailable('The sign-in is being refreshed in the background.');
124
132
  }
125
133
  /**
126
- * The token `pendingyou mcp-headers` hands Claude Code (remote.ts). Claude Code runs the helper each time it connects
127
- * and gives it 10 seconds, so, as for a hook, a refresh is never made in this process: within 5 minutes of expiry it
134
+ * The token `pendingyou mcp-headers` hands an app's MCP server (remote.ts), and the stdio bridge sends with each
135
+ * message (bridge.ts). The app runs the helper each time it connects and gives it 10 seconds, and the bridge may be
136
+ * stopped at any moment, so, as for a hook, a refresh is never made in this process: within 5 minutes of expiry it
128
137
  * starts `pendingyou refresh` detached and waits for its token, up to `waitMs`, then uses the old one while it still
129
- * works. A token handed over has at least 10 seconds left; Claude Code runs the helper again on a 401.
138
+ * works. A token handed over has at least 10 seconds left; the app runs the helper again on a 401. `force` (the bridge,
139
+ * after a 401) refreshes whatever the expiry says, and hands over only the new token.
130
140
  */
131
- export async function helperToken(io, origin, waitMs) {
132
- const stored = await readCredential(io, origin);
141
+ export async function helperToken(io, origin, waitMs, app = DEFAULT_APP, force = false) {
142
+ const stored = await readCredential(io, origin, app);
133
143
  if (!stored)
134
144
  throw new SignInNeeded(false);
135
- if (stored.expiresAt - io.now() > SOON_MS)
145
+ if (!force && stored.expiresAt - io.now() > SOON_MS)
136
146
  return stored.accessToken;
137
147
  if (!(await isLocked(credentialsPath(io))))
138
- io.background(['refresh', '--origin', origin]);
148
+ io.background(refreshArgs(origin, app, force));
139
149
  const until = io.now() + waitMs;
140
150
  while (io.now() < until) {
141
151
  await io.sleep(100);
142
- const current = await readCredential(io, origin);
152
+ const current = await readCredential(io, origin, app);
143
153
  if (!current)
144
154
  throw new SignInNeeded(true);
145
155
  if (current.refreshToken !== stored.refreshToken)
146
156
  return current.accessToken;
147
157
  }
148
- if (stored.expiresAt - io.now() > USABLE_MS)
158
+ if (!force && stored.expiresAt - io.now() > USABLE_MS)
149
159
  return stored.accessToken;
150
160
  throw new Unavailable('Pending You couldn’t be reached to refresh the sign-in.');
151
161
  }
@@ -163,17 +173,19 @@ function either(signal, ms) {
163
173
  return controller.signal;
164
174
  }
165
175
  /**
166
- * GETs `path` on `origin` as the command line, and returns the JSON body with its status. 401 after a refresh means
167
- * the sign-in is over (revoked, or 30 days passed). Network failures and 5xx are `Unavailable`.
176
+ * GETs `path` on `origin` as the command line, with an app's sign-in (Claude Code's unless `app` says), and returns the
177
+ * JSON body with its status. 401 after a refresh means the sign-in is over (revoked, or 30 days passed). Network
178
+ * failures and 5xx are `Unavailable`.
168
179
  */
169
180
  export async function getJson(io, origin, path, timeoutMs, signal, options = {}) {
181
+ const app = options.app ?? DEFAULT_APP;
170
182
  for (let attempt = 0;; attempt++) {
171
183
  let token;
172
184
  try {
173
185
  token =
174
186
  options.hook && io.background
175
- ? await hookToken(io, origin, attempt > 0, signal)
176
- : await freshToken(io, origin, attempt > 0);
187
+ ? await hookToken(io, origin, attempt > 0, signal, app)
188
+ : await freshToken(io, origin, attempt > 0, app);
177
189
  }
178
190
  catch (error) {
179
191
  if (error instanceof Unavailable)
@@ -198,7 +210,7 @@ export async function getJson(io, origin, path, timeoutMs, signal, options = {})
198
210
  await response.body?.cancel();
199
211
  if (attempt === 0)
200
212
  continue;
201
- await updateCredential(io, origin, () => null);
213
+ await updateCredential(io, origin, () => null, app);
202
214
  throw new SignInNeeded(true);
203
215
  }
204
216
  if (response.status >= 500 || response.status === 429) {
@@ -0,0 +1,365 @@
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";
15
+ import { connectionMachine, readCredential } from "../credentials.js";
16
+ import { differs, serverForFolder, siteOf } from "../environment.js";
17
+ import { originArgs } from "../hooks.js";
18
+ import { claudeVersion, loadsMods, MOD_CLAUDE } from "../mod.js";
19
+ import { checkHelper, foundBy, headlessReason, helperCommand, helperPath, isOurHelper, machineOf, usesHelper, } from "../remote.js";
20
+ import { connectionTitle, FINISH_SAY, setupOf, setupStatus } from "../setup.js";
21
+ import { shimPath } from "../shim.js";
22
+ import { APP_NAMES } from "./ids.js";
23
+ const NAME = APP_NAMES['claude-code'];
24
+ /** The sentence's lead-in, and what restarting means, in init's last lines. */
25
+ 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);
38
+ }
39
+ /** The plan for a server at another Pending You: replaced once the person says so, or Claude Code left alone. */
40
+ async function replaces(io, ctx, server, url) {
41
+ ctx.progress(`Claude Code’s pendingyou MCP server points at ${server.url}, not ${url}.\n`);
42
+ if (ctx.yes)
43
+ return true;
44
+ if (!io.interactive)
45
+ return false;
46
+ const answer = await io.ask(`Replace it with ${url}? [y/N] `);
47
+ return /^\s*y(es)?\s*$/i.test(answer);
48
+ }
49
+ export const claudeCode = {
50
+ id: 'claude-code',
51
+ name: NAME,
52
+ minVersion: '2.0.0',
53
+ async detect(io) {
54
+ const result = await io.run('claude', ['--version'], 20_000);
55
+ if (result.code !== 0)
56
+ return null;
57
+ return {
58
+ version: claudeVersion(result.stdout) ?? (result.stdout.trim().split(/\s+/)[0] || 'installed'),
59
+ };
60
+ },
61
+ async installed(io) {
62
+ return (await readManifest(io)) !== null;
63
+ },
64
+ async usesHelper(io, origin) {
65
+ if (io.platform === 'win32')
66
+ return false;
67
+ const server = await mcpFromFile(io);
68
+ if (server !== 'unknown')
69
+ return usesHelper(server, origin);
70
+ const manifest = await readManifest(io);
71
+ return Boolean(manifest?.helper && manifest.origin === origin);
72
+ },
73
+ async prepare(io, ctx, detected) {
74
+ const url = `${ctx.origin}/mcp`;
75
+ const flag = originArgs(ctx.origin);
76
+ let server = await mcpFromFile(io);
77
+ if (server === 'unknown') {
78
+ ctx.progress('Checking Claude Code’s MCP servers (this can take a minute)…\n');
79
+ server = await mcpFromClaude(io);
80
+ }
81
+ const plugin = await pluginFor(io, ctx.origin);
82
+ const helperMode = io.platform !== 'win32' && !ctx.oauth;
83
+ let mode = helperMode ? 'helper' : 'oauth';
84
+ let plan;
85
+ const helperAdd = helperServer(url, helperCommand(io, ctx.origin));
86
+ const add = helperMode ? helperAdd.text : addCommand(url);
87
+ if (server === 'timeout') {
88
+ plan = {
89
+ action: 'keep',
90
+ ok: false,
91
+ text: `Claude Code took more than 3 minutes to list its MCP servers, so I left them alone. Check with: claude mcp get pendingyou. If it isn’t there, run: ${add}`,
92
+ };
93
+ }
94
+ else if (server?.url && !sameUrl(server.url, url)) {
95
+ // Another Pending You (staging, when production was asked for): asked before anyone signs in.
96
+ if (!(await replaces(io, ctx, server, url))) {
97
+ const commands = ` ${removeCommand(server.scope)}\n ${add}`;
98
+ return {
99
+ app: claudeCode,
100
+ detected,
101
+ signIn: null,
102
+ skipped: {
103
+ ok: false,
104
+ text: `Kept Claude Code’s pendingyou MCP server at ${server.url}, so Claude Code isn’t set up for ${ctx.origin}. To use ${url}, run init again with --yes, or run:\n ${commands}`,
105
+ },
106
+ install: async () => [],
107
+ next: () => null,
108
+ };
109
+ }
110
+ plan = { action: 'replace', server };
111
+ }
112
+ else if (server) {
113
+ const ours = isOurHelper(server.helper);
114
+ if (ours && helperMode)
115
+ plan =
116
+ server.helper === helperCommand(io, ctx.origin)
117
+ ? {
118
+ action: 'keep',
119
+ ok: true,
120
+ text: `The pendingyou MCP server (${url}) already signs in through this computer’s sign-in.`,
121
+ }
122
+ : { action: 'switch', server };
123
+ else if (ours)
124
+ plan = { action: 'switch', server };
125
+ else if (server.helper) {
126
+ // Someone else's headersHelper: theirs to change. The hooks hear through a sign-in of their own.
127
+ mode = 'oauth';
128
+ plan = helperMode
129
+ ? {
130
+ action: 'keep',
131
+ ok: false,
132
+ text: `Kept the pendingyou MCP server: it signs in through a headersHelper of its own (${server.helper}). To use this computer’s sign-in instead, run:\n ${removeCommand(server.scope)}\n ${helperAdd.text}`,
133
+ }
134
+ : {
135
+ action: 'keep',
136
+ ok: true,
137
+ text: 'The pendingyou MCP server was already in Claude Code.',
138
+ };
139
+ }
140
+ else if (helperMode &&
141
+ (await switches(io, ctx, 'Claude Code signs in to Pending You by itself here (/mcp → Authenticate).')))
142
+ plan = { action: 'switch', server };
143
+ else {
144
+ mode = 'oauth';
145
+ plan = {
146
+ action: 'keep',
147
+ ok: true,
148
+ text: helperMode
149
+ ? `Kept Claude Code’s own sign-in to Pending You (${url}). To move it to this computer’s, run: npx -y pendingyou@latest init --yes${flag}`
150
+ : `The pendingyou MCP server (${url}) was already in Claude Code.`,
151
+ };
152
+ }
153
+ }
154
+ 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
+ if (helperMode &&
157
+ (await switches(io, ctx, 'The Pending You plugin signs Claude Code in to Pending You by itself here.')))
158
+ plan = { action: 'add', plugin: true };
159
+ else {
160
+ mode = 'oauth';
161
+ plan = {
162
+ action: 'keep',
163
+ ok: true,
164
+ text: helperMode
165
+ ? `Kept the Pending You plugin as Claude Code’s connection. To move Claude Code to this computer’s sign-in, run: npx -y pendingyou@latest init --yes${flag}`
166
+ : 'The Pending You plugin is Claude Code’s connection here.',
167
+ };
168
+ }
169
+ }
170
+ else
171
+ plan = { action: 'add' };
172
+ const signIn = mode === 'helper' ? 'connection' : 'hear';
173
+ const added = plan.action !== 'keep';
174
+ return {
175
+ app: claudeCode,
176
+ detected,
177
+ signIn,
178
+ install: (ictx) => installClaude(io, ctx.origin, {
179
+ version: detected.version,
180
+ mcp: plan,
181
+ mode,
182
+ helper: ictx.helper,
183
+ plugin: Boolean(plugin),
184
+ copy: ictx.copy,
185
+ hookLine: ictx.hookLine,
186
+ report: ictx.report,
187
+ progress: ictx.progress,
188
+ }),
189
+ next: (ictx) => {
190
+ if (mode === 'oauth') {
191
+ const authenticate = added || !ictx.signedIn;
192
+ return {
193
+ lines: [
194
+ authenticate
195
+ ? 'Next, in Claude Code: if it’s open, restart it (/exit, then claude --continue), run /mcp, choose pendingyou and Authenticate. Then ask Claude Code to finish setting up Pending You.'
196
+ : 'Next, in Claude Code: if it’s open, restart it (/exit, then claude --continue), so the hooks load. Then ask Claude Code to finish setting up Pending You.',
197
+ ],
198
+ together: `- Claude Code: restart it (/exit, then claude --continue)${authenticate ? ', run /mcp, choose pendingyou and Authenticate' : ''}, then ask it to finish setting up Pending You.`,
199
+ say: false,
200
+ };
201
+ }
202
+ const version = claudeVersion(detected.version);
203
+ const byItself = version !== null && loadsMods(version);
204
+ return {
205
+ lines: [
206
+ `Next: ${RESTART}.`,
207
+ byItself
208
+ ? 'It finishes setting up by itself and sends you a test card. If it doesn’t, say this to it:'
209
+ : 'Then say this to it:',
210
+ '',
211
+ FINISH_SAY,
212
+ ],
213
+ together: `- Claude Code: ${RESTART}. ${byItself ? 'It finishes setting up by itself and sends you a test card.' : 'Then say the line below to it.'}`,
214
+ say: true,
215
+ };
216
+ },
217
+ };
218
+ },
219
+ async uninstall(io) {
220
+ return uninstallClaude(io);
221
+ },
222
+ async status(io, ctx) {
223
+ return claudeStatus(io, ctx);
224
+ },
225
+ };
226
+ /** Claude Code's section of `pendingyou status`: what's set up and what's missing, each with the command that fixes it. */
227
+ async function claudeStatus(io, ctx) {
228
+ const { origin, signIn } = ctx;
229
+ const [claude, here, manifest] = await Promise.all([
230
+ claudeState(io),
231
+ serverForFolder(io, io.cwd),
232
+ readManifest(io),
233
+ ]);
234
+ const credential = await readCredential(io, origin);
235
+ const flag = originArgs(origin);
236
+ 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
+ const helped = usesHelper(mcp, origin);
240
+ const helper = io.platform !== 'win32' &&
241
+ (helped ||
242
+ Boolean(manifest?.helper && manifest.origin === origin) ||
243
+ (mcp === false && !manifest && !claude.plugins.length));
244
+ const why = headlessReason(io);
245
+ const lines = [
246
+ `Pending You for Claude Code · ${origin}${why ? ` · no browser here (${why})` : ''}`,
247
+ ];
248
+ const mark = (ok) => (ok ? 'ok ' : 'missing');
249
+ const url = `${origin}/mcp`;
250
+ const plugin = claude.plugins.find((id) => siteOf(PLUGIN_ORIGINS[id] ?? '') === siteOf(origin));
251
+ 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
+ const pluginServer = mcp === false && Boolean(plugin);
254
+ const mcpOk = helper ? sameServer && helped : sameServer || pluginServer;
255
+ const mcpText = mcp === 'unknown'
256
+ ? 'Claude Code didn’t say in time; check with claude mcp get pendingyou'
257
+ : pluginServer
258
+ ? `the Pending You plugin’s (${plugin}), which signs in by itself`
259
+ : mcp === false
260
+ ? `not added; run npx pendingyou init${flag}`
261
+ : !sameServer
262
+ ? `points at ${mcp.url}, not ${url}, so sessions post cards where these hooks don’t hear them; run npx pendingyou init${flag} to replace it`
263
+ : !helper
264
+ ? `pendingyou${mcp.url ? ` (${mcp.url})` : ''}`
265
+ : helped
266
+ ? `pendingyou (${mcp.url}), signed in through ${helperPath(io)}`
267
+ : mcp.helper
268
+ ? `pendingyou (${mcp.url}) signs in through a headersHelper of its own (${mcp.helper}); run npx pendingyou init${flag} to use this computer’s sign-in`
269
+ : `pendingyou (${mcp.url}) signs in through a browser${why ? ', which can’t finish on this computer' : ''}; run npx pendingyou init${flag}`;
270
+ const form = claude.form;
271
+ 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
+ const mod = claude.mod;
274
+ const modOk = mod.loads && (mod.plugin || mod.present);
275
+ const modText = !claude.claude
276
+ ? `needs Claude Code ${MOD_CLAUDE} or later`
277
+ : !mod.loads
278
+ ? `needs Claude Code ${MOD_CLAUDE} or later (this is ${claude.claude}); until then, a background hold wakes it`
279
+ : mod.present
280
+ ? `in CLAUDE_CODE_PLUGIN_DIRS (${mod.dirs.join(', ')}); a session is woken when you answer, with no hold running`
281
+ : mod.plugin
282
+ ? 'in the Pending You plugin; a session is woken when you answer, with no hold running'
283
+ : mod.dirs.length
284
+ ? `${mod.dirs.join(', ')} is gone; run npx pendingyou@latest init${flag}`
285
+ : `not installed; run npx pendingyou@latest init${flag}`;
286
+ const via = form?.shim ? `, through ${shimPath(io)}` : '';
287
+ const formText = !form
288
+ ? ''
289
+ : form.form === 'direct'
290
+ ? form.runs
291
+ ? `they run pendingyou${form.version ? ` ${form.version}` : ''} directly (${form.script})${via}`
292
+ : `the Node or the copy of pendingyou they run is gone (${form.node}, ${form.script}); run npx pendingyou@latest init${flag}`
293
+ : form.form === 'npx'
294
+ ? `they run through npx${via}, which is slower; run npx pendingyou@latest init${flag} to run them directly`
295
+ : '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
+ 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
+ const title = signIn.state === 'ok' && signIn.for
300
+ ? connectionTitle(signIn.for)
301
+ : `${NAME} on ${(await connectionMachine(io, origin)) ?? (machineOf(io.host) || 'this computer')}`;
302
+ const connected = credential?.kind === 'connection';
303
+ const signOk = signIn.state === 'ok' && (!helper || connected);
304
+ const signText = helper
305
+ ? signIn.state === 'ok'
306
+ ? connected
307
+ ? `this computer’s own connection, ${title}; Claude Code here asks and hears through it`
308
+ : `this computer’s sign-in only hears answers, so Claude Code here can’t use it; run ${fix}`
309
+ : signIn.state === 'ended'
310
+ ? `ended; run ${fix}`
311
+ : signIn.state === 'none'
312
+ ? `not signed in; run ${fix}${why ? ' (you approve a code on your phone)' : ''}`
313
+ : 'Pending You couldn’t be reached'
314
+ : signIn.state === 'ok'
315
+ ? signIn.for
316
+ ? `hears for ${signIn.for.name} only (for pendingyou watch)`
317
+ : `hears for ${signIn.connections} Claude Code connection${signIn.connections === 1 ? '' : 's'}`
318
+ : signIn.state === 'ended'
319
+ ? `ended; run npx pendingyou login${flag}`
320
+ : signIn.state === 'none'
321
+ ? `not signed in; run npx pendingyou login${flag}`
322
+ : '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
+ const check = helper ? await checkHelper(io, origin) : null;
325
+ const skillOk = claude.skill || Boolean(plugin);
326
+ const skillText = claude.skill
327
+ ? 'saved'
328
+ : plugin
329
+ ? 'the Pending You plugin carries it'
330
+ : `not saved; run npx pendingyou init${flag}`;
331
+ lines.push(` ${mark(signOk)} Sign-in: ${signText}`, ` ${mark(claude.claude !== null)} Claude Code: ${claude.claude ?? 'not found on PATH'}`, ` ${mark(mcpOk)} MCP server: ${mcpText}`, ...(check
332
+ ? [
333
+ ` ${mark(check.ok)} Sign-in helper: ${check.ok
334
+ ? `${check.node} (${foundBy(check.from)}) runs pendingyou ${check.version}, even for a Claude Code started without your shell’s PATH`
335
+ : `${check.why}; run npx pendingyou init${flag}`}`,
336
+ ]
337
+ : []), ...(plugin
338
+ ? [
339
+ ` ok Plugin: ${plugin} is enabled${pluginServer ? '' : mcp !== false && mcp !== 'unknown' ? '; Claude Code uses the server added by hand instead of its own' : ''}`,
340
+ ]
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.
343
+ const setup = setupStatus(NAME, signIn.state === 'ok' ? setupOf(signIn.for) : null);
344
+ if (setup)
345
+ 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
+ if (here && (here.scope === 'local' || here.scope === 'project') && differs(here, origin))
349
+ 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
+ const user = mcp !== false && mcp !== 'unknown' && mcp.url ? { url: mcp.url, scope: 'user' } : null;
351
+ 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
+ const reaches = !helper || (signOk && mcpOk && check?.ok === true);
354
+ const ready = reaches && signIn.state === 'ok' && claude.hooks && hooksRun && !other;
355
+ if (other)
356
+ lines.push(`Mismatch: Claude Code’s MCP server is ${siteOf(other.url)} but the hooks and sign-in are ${siteOf(origin) ?? origin}. Use the one you look at (npx pendingyou init --origin <it>), then restart your Claude Code sessions.`);
357
+ lines.push(ready
358
+ ? helper
359
+ ? 'Ready: Claude Code here signs in through this computer’s sign-in, and hears answers right away (report_setup hears "instant").'
360
+ : 'Ready: Claude Code hears answers right away (report_setup hears "instant").'
361
+ : reaches
362
+ ? 'Not ready: Claude Code hears answers while it’s working (report_setup hears "while-working").'
363
+ : 'Not ready: Claude Code here can’t reach Pending You until the lines marked missing are fixed.');
364
+ return { lines, ready };
365
+ }