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/CHANGELOG.md +125 -0
- package/README.md +207 -82
- package/dist/api.js +44 -32
- package/dist/apps/claude-code.js +365 -0
- package/dist/apps/codex-wake.js +479 -0
- package/dist/apps/codex.js +615 -0
- package/dist/apps/ids.js +26 -0
- package/dist/apps/opencode-plugin.js +440 -0
- package/dist/apps/opencode.js +734 -0
- package/dist/apps/pi-extension.js +391 -0
- package/dist/apps/pi.js +679 -0
- package/dist/apps/registry.js +14 -0
- package/dist/apps/types.js +1 -0
- package/dist/args.js +115 -36
- package/dist/bridge.js +566 -0
- package/dist/claude.js +164 -362
- package/dist/confirm.js +5 -4
- package/dist/credentials.js +68 -26
- package/dist/environment.js +17 -3
- package/dist/hold.js +18 -2
- package/dist/hooks.js +117 -0
- package/dist/index.js +3 -1
- package/dist/init.js +309 -0
- package/dist/io.js +34 -0
- package/dist/json.js +281 -0
- package/dist/jsonc.js +315 -0
- package/dist/main.js +38 -314
- package/dist/mod.js +7 -5
- package/dist/oauth.js +147 -51
- package/dist/pickup.js +62 -16
- package/dist/remote.js +116 -61
- package/dist/setup.js +118 -0
- package/dist/shim.js +93 -0
- package/dist/signin.js +227 -0
- package/dist/state.js +24 -3
- package/dist/stopcheck.js +109 -20
- package/dist/toml.js +277 -0
- package/dist/version.js +1 -1
- package/mod/.claude-plugin/plugin.json +1 -1
- package/mod/README.md +1 -1
- package/mod/hooks/register.js +66 -3
- package/mod/hooks/wake.js +54 -1
- package/mod/tests/wake.test.ts +50 -1
- package/package.json +2 -2
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(
|
|
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
|
|
127
|
-
*
|
|
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;
|
|
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(
|
|
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,
|
|
167
|
-
* the sign-in is over (revoked, or 30 days passed). Network
|
|
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
|
+
}
|