pendingyou 0.10.0 → 0.11.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.
@@ -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
+ }