pendingyou 0.7.0 → 0.8.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 +17 -0
- package/README.md +9 -3
- package/dist/environment.js +98 -0
- package/dist/main.js +16 -3
- package/dist/pickup.js +15 -3
- package/dist/version.js +1 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,22 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.8.0
|
|
4
|
+
|
|
5
|
+
- The hooks say when a session is on another Pending You. A Claude Code session stayed connected to staging after its
|
|
6
|
+
person moved to production, so its cards went where they no longer looked. Now both hooks (`pickup` at session
|
|
7
|
+
start, `handoff` with every message) compare the `pendingyou` MCP server Claude Code uses in the session's folder
|
|
8
|
+
with the origin they run against (`--origin`), and when they're different sites add one line (about 45 tokens):
|
|
9
|
+
`Pending You: your MCP server is https://staging.pendingyou.com but your hooks and sign-in are
|
|
10
|
+
https://www.pendingyou.com, so your cards and their answers may not meet. Ask your person which one they use, and
|
|
11
|
+
restart the session after fixing it (npx pendingyou init --origin <that one>).` The server is the one Claude Code
|
|
12
|
+
would use there: local scope (`~/.claude.json`'s `projects`), then the folder's `.mcp.json`, then user scope, then
|
|
13
|
+
the Pending You plugin when `~/.claude/settings.json` enables it (production's server). All read only, and only the
|
|
14
|
+
`pendingyou` server's address is looked at; nothing is sent. `pendingyou.com` and `www.pendingyou.com` are the same
|
|
15
|
+
site; a file that can't be read is never a mismatch. The line is said even when the hooks' origin isn't signed in,
|
|
16
|
+
and it's kept when a hook reaches its 3-second deadline.
|
|
17
|
+
- `status` says the same: a `Mismatch:` line naming both sites and what to do, a line for a folder's own
|
|
18
|
+
(local- or project-scope) server that points elsewhere, and "Not ready" (exit 1) until they agree.
|
|
19
|
+
|
|
3
20
|
## 0.7.0
|
|
4
21
|
|
|
5
22
|
- Faster hooks. They ran `npx -y --prefer-offline pendingyou@<version> …`, and npx alone took about half a second
|
package/README.md
CHANGED
|
@@ -26,13 +26,18 @@ that. If Claude Code already has a `pendingyou` server pointing somewhere else (
|
|
|
26
26
|
asks before replacing it; `--yes` replaces it without asking, and with nobody to ask it prints the two commands to
|
|
27
27
|
run. Afterwards restart Claude Code, run `/mcp`, choose pendingyou and Authenticate.
|
|
28
28
|
|
|
29
|
+
If the `pendingyou` MCP server Claude Code uses in a session's folder is another Pending You than the hooks' (staging
|
|
30
|
+
and production, say), both hooks add one line telling Claude Code so, and to ask you which one you use: its cards
|
|
31
|
+
would otherwise go where you don't look. `npx pendingyou status` flags it too. Fix it with `npx pendingyou init
|
|
32
|
+
--origin <the one you use>`, then restart the session.
|
|
33
|
+
|
|
29
34
|
The hooks never get in your way: if anything goes wrong (no network, not signed in, a bad option), they print nothing
|
|
30
35
|
and let your message through. Each one stops within 3 seconds whatever happens, with at most its one-line reminder, and
|
|
31
36
|
never refreshes the sign-in itself: when that's due, it starts the refresh in the background, which finishes on its
|
|
32
37
|
own. Running `init` again updates them in place, and adds any a newer version brings.
|
|
33
38
|
|
|
34
39
|
The hooks run that copy with Node directly, like
|
|
35
|
-
`"/opt/homebrew/bin/node" "~/.config/pendingyou/cli/0.
|
|
40
|
+
`"/opt/homebrew/bin/node" "~/.config/pendingyou/cli/0.8.0/node_modules/pendingyou/dist/cli.js" handoff || true` (the
|
|
36
41
|
paths in full). If the copy can't be made (no npm, no network), they run through `npx -y --prefer-offline
|
|
37
42
|
pendingyou@<version>` instead. `npx pendingyou status` says which. If you remove the Node they run with, run `init`
|
|
38
43
|
again.
|
|
@@ -125,8 +130,9 @@ The sign-in only hears answers to your own Claude Code assistants' questions, or
|
|
|
125
130
|
answer or change anything, and it doesn't show up as one. It's stored in `~/.config/pendingyou/credentials.json`
|
|
126
131
|
(readable only by you), refreshes itself, and ends after 30 days or on `logout`. `state.json` beside it remembers
|
|
127
132
|
which answers were already handed over (ids only), `claude-code.json` what `init` added, and `cli/` the hooks' copy
|
|
128
|
-
of the command line. `init`
|
|
129
|
-
|
|
133
|
+
of the command line. `init`, `status` and the hooks read `~/.claude.json` (and a folder's `.mcp.json`, and
|
|
134
|
+
`~/.claude/settings.json`'s enabled plugins) only to see whether Claude Code has a `pendingyou` MCP server and where
|
|
135
|
+
it points. Nothing is sent anywhere but Pending You.
|
|
130
136
|
|
|
131
137
|
## Development
|
|
132
138
|
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
// Is this session on the Pending You its hooks are on? A Claude Code session stayed connected to staging after its
|
|
2
|
+
// person moved to production (2026-10-01): its cards went where they no longer looked, and the hooks, signed in to
|
|
3
|
+
// production, never heard them. So the hooks compare the `pendingyou` MCP server Claude Code has for the session's
|
|
4
|
+
// folder (read from ~/.claude.json and the folder's .mcp.json, read only) with the origin they run against (`--origin`),
|
|
5
|
+
// and when they differ, add one line telling the agent to ask its person which one they use. `status` says the same.
|
|
6
|
+
//
|
|
7
|
+
// Only the pendingyou server's url is read, never anything else from those files; nothing is written, nothing is sent.
|
|
8
|
+
// A server Claude Code can't be read for (no file, another shape, a plugin's server) is never called a mismatch.
|
|
9
|
+
import { join } from 'node:path';
|
|
10
|
+
import { claudeJsonPath, MCP_NAME } from "./claude.js";
|
|
11
|
+
import { claudeDir, readText } from "./files.js";
|
|
12
|
+
/**
|
|
13
|
+
* The published Pending You plugin for Claude (recordplane/pendingyou-plugin) and where its server is. A hand-added
|
|
14
|
+
* pendingyou server at any scope overrides it in Claude Code, so it counts only when there's none.
|
|
15
|
+
*/
|
|
16
|
+
export const PLUGIN_ID = 'pending-you@pendingyou';
|
|
17
|
+
export const PLUGIN_MCP_URL = 'https://www.pendingyou.com/mcp';
|
|
18
|
+
const objectAt = (value, key) => {
|
|
19
|
+
if (typeof value !== 'object' || value === null || Array.isArray(value))
|
|
20
|
+
return null;
|
|
21
|
+
const found = value[key];
|
|
22
|
+
return typeof found === 'object' && found !== null && !Array.isArray(found)
|
|
23
|
+
? found
|
|
24
|
+
: null;
|
|
25
|
+
};
|
|
26
|
+
const urlOf = (servers) => {
|
|
27
|
+
const url = objectAt(servers, MCP_NAME)?.url;
|
|
28
|
+
return typeof url === 'string' && url.length > 0 ? url : null;
|
|
29
|
+
};
|
|
30
|
+
async function readJsonQuietly(path) {
|
|
31
|
+
try {
|
|
32
|
+
const text = await readText(path);
|
|
33
|
+
return text === null ? null : JSON.parse(text);
|
|
34
|
+
}
|
|
35
|
+
catch {
|
|
36
|
+
return null;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* The `pendingyou` MCP server Claude Code uses in `folder`: local scope (~/.claude.json's projects[folder]), then the
|
|
41
|
+
* folder's own .mcp.json, then user scope (~/.claude.json's mcpServers), then the Pending You plugin when
|
|
42
|
+
* ~/.claude/settings.json enables it. Null when none of them has one with a url.
|
|
43
|
+
*/
|
|
44
|
+
export async function serverForFolder(io, folder) {
|
|
45
|
+
const [state, project, settings] = await Promise.all([
|
|
46
|
+
readJsonQuietly(claudeJsonPath(io)),
|
|
47
|
+
readJsonQuietly(join(folder, '.mcp.json')),
|
|
48
|
+
readJsonQuietly(join(claudeDir(io), 'settings.json')),
|
|
49
|
+
]);
|
|
50
|
+
const local = urlOf(objectAt(objectAt(objectAt(state, 'projects'), folder), 'mcpServers'));
|
|
51
|
+
if (local)
|
|
52
|
+
return { url: local, scope: 'local' };
|
|
53
|
+
const shared = urlOf(objectAt(project, 'mcpServers'));
|
|
54
|
+
if (shared)
|
|
55
|
+
return { url: shared, scope: 'project' };
|
|
56
|
+
const user = urlOf(objectAt(state, 'mcpServers'));
|
|
57
|
+
if (user)
|
|
58
|
+
return { url: user, scope: 'user' };
|
|
59
|
+
return objectAt(settings, 'enabledPlugins')?.[PLUGIN_ID] === true
|
|
60
|
+
? { url: PLUGIN_MCP_URL, scope: 'plugin' }
|
|
61
|
+
: null;
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* The site an address is on, as one name: www.pendingyou.com and pendingyou.com are the same Pending You (the bare
|
|
65
|
+
* name redirects), so both read https://www.pendingyou.com. Null when it isn't an address.
|
|
66
|
+
*/
|
|
67
|
+
export function siteOf(address) {
|
|
68
|
+
try {
|
|
69
|
+
const url = new URL(address);
|
|
70
|
+
if (url.protocol !== 'https:' && url.protocol !== 'http:')
|
|
71
|
+
return null;
|
|
72
|
+
const host = url.host === 'pendingyou.com' ? 'www.pendingyou.com' : url.host;
|
|
73
|
+
return `${url.protocol}//${host}`;
|
|
74
|
+
}
|
|
75
|
+
catch {
|
|
76
|
+
return null;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
/** Whether the server and the hooks' origin are different Pending Yous. Unknown addresses never differ. */
|
|
80
|
+
export function differs(server, origin) {
|
|
81
|
+
if (!server)
|
|
82
|
+
return false;
|
|
83
|
+
const a = siteOf(server.url);
|
|
84
|
+
const b = siteOf(origin);
|
|
85
|
+
return a !== null && b !== null && a !== b;
|
|
86
|
+
}
|
|
87
|
+
/** The line a hook adds when they differ (about 45 tokens). */
|
|
88
|
+
export const mismatchLine = (server, origin) => `Pending You: your MCP server is ${siteOf(server.url)} but your hooks and sign-in are ${siteOf(origin)}, so your cards and their answers may not meet. Ask your person which one they use, and restart the session after fixing it (npx pendingyou init --origin <that one>).`;
|
|
89
|
+
/** The hook's warning for this folder, or null when the two agree or can't be told. Never throws. */
|
|
90
|
+
export async function environmentWarning(io, folder, origin) {
|
|
91
|
+
try {
|
|
92
|
+
const server = await serverForFolder(io, folder);
|
|
93
|
+
return differs(server, origin) ? mismatchLine(server, origin) : null;
|
|
94
|
+
}
|
|
95
|
+
catch {
|
|
96
|
+
return null;
|
|
97
|
+
}
|
|
98
|
+
}
|
package/dist/main.js
CHANGED
|
@@ -4,6 +4,7 @@ import { getJson, refreshInBackground, SignInNeeded, Unavailable } from "./api.j
|
|
|
4
4
|
import { commandOf, DEFAULT_ORIGIN, HOOK_COMMANDS, parseArgs, USAGE, UsageError, } from "./args.js";
|
|
5
5
|
import { claudeState, installClaude, uninstallClaude } from "./claude.js";
|
|
6
6
|
import { readCredential, signedInOrigins, updateCredential } from "./credentials.js";
|
|
7
|
+
import { differs, serverForFolder, siteOf } from "./environment.js";
|
|
7
8
|
import { PlainError } from "./errors.js";
|
|
8
9
|
import { hold } from "./hold.js";
|
|
9
10
|
import { OAuthError, revoke, signIn, signInWithDevice } from "./oauth.js";
|
|
@@ -71,7 +72,11 @@ async function logout(io, options) {
|
|
|
71
72
|
return 0;
|
|
72
73
|
}
|
|
73
74
|
async function status(io, origin) {
|
|
74
|
-
const [signIn, claude] = await Promise.all([
|
|
75
|
+
const [signIn, claude, here] = await Promise.all([
|
|
76
|
+
checkSignIn(io, origin),
|
|
77
|
+
claudeState(io),
|
|
78
|
+
serverForFolder(io, io.cwd),
|
|
79
|
+
]);
|
|
75
80
|
const lines = [`Pending You for Claude Code · ${origin}`];
|
|
76
81
|
const mark = (ok) => (ok ? 'ok ' : 'missing');
|
|
77
82
|
const url = `${origin}/mcp`;
|
|
@@ -83,7 +88,7 @@ async function status(io, origin) {
|
|
|
83
88
|
? 'not added; run npx pendingyou init'
|
|
84
89
|
: mcpOk
|
|
85
90
|
? `pendingyou${mcp.url ? ` (${mcp.url})` : ''}`
|
|
86
|
-
: `points at ${mcp.url}, not ${url}; run npx pendingyou init${origin === DEFAULT_ORIGIN ? '' : ` --origin ${origin}`} to replace it`;
|
|
91
|
+
: `points at ${mcp.url}, not ${url}, so sessions post cards where these hooks don’t hear them; run npx pendingyou init${origin === DEFAULT_ORIGIN ? '' : ` --origin ${origin}`} to replace it`;
|
|
87
92
|
const form = claude.form;
|
|
88
93
|
const hooksRun = form?.runs ?? true;
|
|
89
94
|
const formText = !form
|
|
@@ -104,7 +109,15 @@ async function status(io, origin) {
|
|
|
104
109
|
: signIn.state === 'none'
|
|
105
110
|
? 'not signed in; run npx pendingyou login'
|
|
106
111
|
: 'Pending You couldn’t be reached'}`, ` ${mark(claude.claude !== null)} Claude Code: ${claude.claude ?? 'not found on PATH'}`, ` ${mark(mcpOk)} MCP server: ${mcpText}`, ` ${mark(claude.skill)} Skill: ${claude.skill ? 'saved' : 'not saved; run npx pendingyou init'}`, ` ${mark(claude.hooks && hooksRun)} Hooks: ${claude.hooks ? `session-start pickup and next-message hand-off; ${formText}` : 'not installed; run npx pendingyou init'}`, ` ${mark(claude.stopcheck)} Stop check: ${claude.stopcheck ? 'asks Claude Code to post what it leaves you in chat' : 'not installed; run npx pendingyou init'}`);
|
|
107
|
-
|
|
112
|
+
// A folder's own server (local or project scope) wins over the user one in Claude Code: say when it's elsewhere. (The
|
|
113
|
+
// plugin's server counts only when no server was added by hand, and is production's.)
|
|
114
|
+
if (here && (here.scope === 'local' || here.scope === 'project') && differs(here, origin))
|
|
115
|
+
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`);
|
|
116
|
+
const user = mcp !== false && mcp !== 'unknown' && mcp.url ? { url: mcp.url, scope: 'user' } : null;
|
|
117
|
+
const other = differs(here, origin) ? here : differs(user, origin) ? user : null;
|
|
118
|
+
const ready = signIn.state === 'ok' && claude.hooks && hooksRun && !other;
|
|
119
|
+
if (other)
|
|
120
|
+
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.`);
|
|
108
121
|
lines.push(ready
|
|
109
122
|
? 'Ready: Claude Code hears answers right away (report_setup hears "instant").'
|
|
110
123
|
: 'Not ready: Claude Code hears answers while it’s working (report_setup hears "while-working").');
|
package/dist/pickup.js
CHANGED
|
@@ -9,11 +9,15 @@
|
|
|
9
9
|
// one small request per message (the open cards, and answers too while a hold from this computer is outstanding),
|
|
10
10
|
// time-boxed short; on any error, or from a server too old to list open cards, the line is the plain reminder.
|
|
11
11
|
//
|
|
12
|
+
// Both also add one line when the session's pendingyou MCP server is another Pending You than the hooks' origin
|
|
13
|
+
// (environment.ts): its cards would go where these hooks, and maybe its person, don't look.
|
|
14
|
+
//
|
|
12
15
|
// Whatever they print, Claude Code adds to the conversation. A hook must never get in the way: each is time-boxed,
|
|
13
16
|
// has a hard deadline (main.ts) after which it prints only its fallback, never refreshes the sign-in itself (api.ts),
|
|
14
17
|
// prints nothing when there's nothing new, never repeats an answer it handed over, and always exits 0.
|
|
15
18
|
import { getJson, SignInNeeded } from "./api.js";
|
|
16
19
|
import { readCredential } from "./credentials.js";
|
|
20
|
+
import { environmentWarning } from "./environment.js";
|
|
17
21
|
import { clip, describe, isClosed } from "./format.js";
|
|
18
22
|
import { markHanded, momentOf, waitingOn, wasHanded } from "./state.js";
|
|
19
23
|
/** Each request's own limit, inside the hook's 3-second deadline (main.ts). */
|
|
@@ -76,15 +80,20 @@ export async function pickup(io, options, hook = { fallback: '' }) {
|
|
|
76
80
|
const { origin, mode } = options;
|
|
77
81
|
// The hand-off's line, printed at the end unless something else was printed with it.
|
|
78
82
|
let line = null;
|
|
83
|
+
// Said by either hook, signed in or not, when the session's MCP server is another Pending You (environment.ts).
|
|
84
|
+
let warning = null;
|
|
79
85
|
try {
|
|
80
86
|
const cwd = await sessionFolder(io);
|
|
87
|
+
warning = await environmentWarning(io, cwd, origin);
|
|
88
|
+
if (warning)
|
|
89
|
+
hook.fallback = `${warning}\n`;
|
|
81
90
|
// The hand-off hears answers only while a hold from this computer is outstanding; it always asks for open cards.
|
|
82
91
|
let answers = true;
|
|
83
92
|
if (mode === 'handoff') {
|
|
84
93
|
if ((await readCredential(io, origin)) === null)
|
|
85
94
|
return 0;
|
|
86
95
|
line = REMINDER;
|
|
87
|
-
hook.fallback = `${REMINDER}\n`;
|
|
96
|
+
hook.fallback = `${[REMINDER, warning].filter(Boolean).join('\n')}\n`;
|
|
88
97
|
answers = (await waitingOn(io, origin)).length > 0;
|
|
89
98
|
}
|
|
90
99
|
const query = new URLSearchParams([
|
|
@@ -116,8 +125,10 @@ export async function pickup(io, options, hook = { fallback: '' }) {
|
|
|
116
125
|
...fresh.flatMap((heard) => [...describe(heard, again(heard.requestId)), '']),
|
|
117
126
|
'Tell them in one line first (“You answered in Pending You: …”), then act.',
|
|
118
127
|
...(line ? [line] : []),
|
|
128
|
+
...(warning ? [warning] : []),
|
|
119
129
|
].join('\n')}\n`);
|
|
120
130
|
line = null;
|
|
131
|
+
warning = null;
|
|
121
132
|
await markHanded(io, origin, fresh.map((heard) => ({
|
|
122
133
|
requestId: heard.requestId,
|
|
123
134
|
moment: momentOf(heard),
|
|
@@ -133,7 +144,8 @@ export async function pickup(io, options, hook = { fallback: '' }) {
|
|
|
133
144
|
return 0;
|
|
134
145
|
}
|
|
135
146
|
finally {
|
|
136
|
-
|
|
137
|
-
|
|
147
|
+
const last = [line, warning].filter(Boolean);
|
|
148
|
+
if (last.length)
|
|
149
|
+
io.out(`${last.join('\n')}\n`);
|
|
138
150
|
}
|
|
139
151
|
}
|
package/dist/version.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
// The command line's version, as published (package.json). A test keeps them the same.
|
|
2
|
-
export const VERSION = '0.
|
|
2
|
+
export const VERSION = '0.8.0';
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pendingyou",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.8.0",
|
|
4
4
|
"description": "Pending You for Claude Code: hear your answers from Pending You in the background, and set Claude Code up in one command.",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"homepage": "https://www.pendingyou.com",
|