pendingyou 0.2.0 → 0.2.1
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 +18 -0
- package/README.md +8 -1
- package/dist/args.js +35 -5
- package/dist/claude.js +22 -12
- package/dist/cli.js +13 -4
- package/dist/main.js +17 -2
- package/dist/version.js +1 -1
- package/package.json +2 -1
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
## 0.2.1
|
|
4
|
+
|
|
5
|
+
- Fixed: after `init --origin <url>` (any origin but the default), every Claude Code message was blocked with
|
|
6
|
+
`UserPromptSubmit operation blocked by hook: … There's no "--origin" command.` 0.2.0 wrote its hooks with `--origin`
|
|
7
|
+
before the command, which it couldn't read. To repair: `npx pendingyou@latest init --origin <url>` (or, if you ran
|
|
8
|
+
`uninstall` to get unblocked, the same command sets it up again).
|
|
9
|
+
- Hooks now name the command first (`pendingyou@0.2.1 handoff --origin <url>`), add `--origin` only when it isn't the
|
|
10
|
+
default, and end in `|| true`.
|
|
11
|
+
- Options can come before or after the command.
|
|
12
|
+
- `pickup` and `handoff` never block: on any error (bad options, no network, not signed in) they exit 0, print nothing
|
|
13
|
+
for Claude Code, and leave at most a short note on stderr.
|
|
14
|
+
- Running `init` again repairs hook lines an older version wrote.
|
|
15
|
+
|
|
16
|
+
## 0.2.0
|
|
17
|
+
|
|
18
|
+
- `login --device` for computers with no browser, and `watch` for always-on scripts.
|
package/README.md
CHANGED
|
@@ -19,6 +19,12 @@ Run it once on each computer. It:
|
|
|
19
19
|
|
|
20
20
|
Your other settings are kept as they are. `npx pendingyou uninstall` removes exactly what `init` added.
|
|
21
21
|
|
|
22
|
+
The hooks never get in your way: if anything goes wrong (no network, not signed in, a bad option), they print nothing
|
|
23
|
+
and let your message through. Running `init` again updates them in place.
|
|
24
|
+
|
|
25
|
+
**On 0.2.0 with another origin?** Its hooks blocked every message (`There's no "--origin" command`). Run
|
|
26
|
+
`npx pendingyou@latest init --origin <url>` again to repair them; see [CHANGELOG.md](CHANGELOG.md).
|
|
27
|
+
|
|
22
28
|
## How Claude Code uses it
|
|
23
29
|
|
|
24
30
|
After it posts a question to Pending You, Claude Code runs this in the background and keeps working:
|
|
@@ -71,7 +77,8 @@ more, then skipped. `--once` stops after the first.
|
|
|
71
77
|
| `pickup` / `handoff` | The session-start and next-message hooks |
|
|
72
78
|
| `uninstall` | Remove what `init` added, and sign out |
|
|
73
79
|
|
|
74
|
-
Every command takes `--origin` (default `https://www.pendingyou.com`, or `$PENDINGYOU_ORIGIN`).
|
|
80
|
+
Every command takes `--origin` (default `https://www.pendingyou.com`, or `$PENDINGYOU_ORIGIN`). Options can come
|
|
81
|
+
before or after the command.
|
|
75
82
|
|
|
76
83
|
## What it can see, and where it keeps things
|
|
77
84
|
|
package/dist/args.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
// The command line's arguments: one command
|
|
2
|
-
//
|
|
1
|
+
// The command line's arguments: one command and its flags, which may come before or after it (`pendingyou handoff
|
|
2
|
+
// --origin <url>` and `pendingyou --origin <url> handoff` are the same; 0.2.0 wrote its hooks the second way). No
|
|
3
|
+
// dependencies, no surprises: an unknown flag or a stray argument is an error, never ignored.
|
|
3
4
|
export const DEFAULT_ORIGIN = 'https://www.pendingyou.com';
|
|
4
5
|
/** How long `hold` waits for an answer before it lets go (4 hours). */
|
|
5
6
|
export const DEFAULT_TIMEOUT_MS = 4 * 60 * 60 * 1000;
|
|
@@ -62,14 +63,43 @@ const FLAGS = {
|
|
|
62
63
|
uninstall: { origin: true },
|
|
63
64
|
watch: { origin: true, once: false },
|
|
64
65
|
};
|
|
66
|
+
/** Options that take a value, whichever command they belong to: needed to find the command behind them. */
|
|
67
|
+
const VALUE_OPTIONS = new Set(['origin', 'timeout']);
|
|
68
|
+
/** Splits off the options given before the command: [those options, the command, everything after it]. */
|
|
69
|
+
function splitCommand(argv) {
|
|
70
|
+
let index = 0;
|
|
71
|
+
while (index < argv.length) {
|
|
72
|
+
const arg = argv[index];
|
|
73
|
+
if (arg === '--' || !arg.startsWith('-'))
|
|
74
|
+
break;
|
|
75
|
+
index += arg.startsWith('--') && !arg.includes('=') && VALUE_OPTIONS.has(arg.slice(2)) ? 2 : 1;
|
|
76
|
+
}
|
|
77
|
+
return [argv.slice(0, index), argv[index], argv.slice(index + 1)];
|
|
78
|
+
}
|
|
79
|
+
/** The command the arguments name, wherever its options are; undefined when there isn't one. */
|
|
80
|
+
export function commandOf(argv) {
|
|
81
|
+
return splitCommand(argv)[1];
|
|
82
|
+
}
|
|
83
|
+
/** The commands Claude Code's hooks run. Whatever goes wrong, they exit 0 and never block the person. */
|
|
84
|
+
export const HOOK_COMMANDS = new Set(['pickup', 'handoff']);
|
|
65
85
|
export function parseArgs(argv, env = {}) {
|
|
66
|
-
const [first,
|
|
67
|
-
|
|
86
|
+
const [leading, first, after] = splitCommand(argv);
|
|
87
|
+
const helps = (arg) => arg === '-h' || arg === '--help';
|
|
88
|
+
const versions = (arg) => arg === '-v' || arg === '--version';
|
|
89
|
+
if (leading.some(helps))
|
|
68
90
|
return { name: 'help' };
|
|
69
|
-
if (first ===
|
|
91
|
+
if (first === undefined) {
|
|
92
|
+
if (leading.some(versions))
|
|
93
|
+
return { name: 'version' };
|
|
94
|
+
if (leading.length === 0)
|
|
95
|
+
return { name: 'help' };
|
|
96
|
+
throw new UsageError('Which command? Like: pendingyou status');
|
|
97
|
+
}
|
|
98
|
+
if (leading.some(versions))
|
|
70
99
|
return { name: 'version' };
|
|
71
100
|
if (!COMMANDS.has(first))
|
|
72
101
|
throw new UsageError(`There’s no “${first}” command.`);
|
|
102
|
+
const rest = [...leading, ...after];
|
|
73
103
|
const allowed = FLAGS[first] ?? {};
|
|
74
104
|
const flags = new Map();
|
|
75
105
|
const positional = [];
|
package/dist/claude.js
CHANGED
|
@@ -31,16 +31,24 @@ const quote = (text) => /^[\w@%+=:,./-]+$/.test(text) ? text : `'${text.replaceA
|
|
|
31
31
|
* How a hook runs this command line: through npx when that's how it was started (pinned to this version, from npx's
|
|
32
32
|
* cache), the installed `pendingyou` when it's installed, or this very file.
|
|
33
33
|
*/
|
|
34
|
-
export function selfCommand(io
|
|
34
|
+
export function selfCommand(io) {
|
|
35
35
|
const script = io.script.replaceAll('\\', '/');
|
|
36
|
-
|
|
36
|
+
return io.env.PENDINGYOU_SELF
|
|
37
37
|
? io.env.PENDINGYOU_SELF
|
|
38
38
|
: !script || script.includes('/_npx/')
|
|
39
39
|
? `npx -y --prefer-offline pendingyou@${VERSION}`
|
|
40
40
|
: script.includes('/node_modules/pendingyou/')
|
|
41
41
|
? 'pendingyou'
|
|
42
42
|
: `node ${quote(io.script)}`;
|
|
43
|
-
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* A hook's command line: the command first, then `--origin` only when it isn't the default. `|| true` keeps the hook
|
|
46
|
+
* from ever blocking the person, even when npx can't fetch the package or the command fails before it starts (Claude
|
|
47
|
+
* Code blocks a message when its UserPromptSubmit hook exits 2, and flags any other non-zero exit).
|
|
48
|
+
*/
|
|
49
|
+
export function hookCommand(io, origin, sub) {
|
|
50
|
+
const flag = origin === DEFAULT_ORIGIN ? '' : ` --origin ${quote(origin)}`;
|
|
51
|
+
return `${selfCommand(io)} ${sub}${flag} || true`;
|
|
44
52
|
}
|
|
45
53
|
/** Whether a hook command is one of ours: it runs pendingyou's `pickup` or `handoff`. */
|
|
46
54
|
export function isOurHook(command, sub) {
|
|
@@ -71,7 +79,10 @@ const objectAt = (parent, key) => {
|
|
|
71
79
|
? value
|
|
72
80
|
: null;
|
|
73
81
|
};
|
|
74
|
-
/**
|
|
82
|
+
/**
|
|
83
|
+
* Adds our hooks and permissions, or updates our hooks in place (a new version, or a line an older version wrote, like
|
|
84
|
+
* 0.2.0's `… --origin <url> handoff`, which 0.2.0 itself can't read). Returns the permissions it added.
|
|
85
|
+
*/
|
|
75
86
|
export function mergeSettings(settings, commands) {
|
|
76
87
|
const next = structuredClone(settings);
|
|
77
88
|
let changed = false;
|
|
@@ -80,7 +91,7 @@ export function mergeSettings(settings, commands) {
|
|
|
80
91
|
throw new PlainError('Claude Code’s settings have a “hooks” that isn’t an object, so I left them alone.');
|
|
81
92
|
for (const event of Object.keys(HOOKS)) {
|
|
82
93
|
const sub = HOOKS[event];
|
|
83
|
-
const wanted =
|
|
94
|
+
const wanted = commands[event];
|
|
84
95
|
const groups = Array.isArray(hooks[event]) ? hooks[event] : [];
|
|
85
96
|
if (hooks[event] !== undefined && !Array.isArray(hooks[event]))
|
|
86
97
|
throw new PlainError(`Claude Code’s ${event} hooks aren’t a list, so I left them alone.`);
|
|
@@ -231,15 +242,17 @@ export async function installClaude(io, origin) {
|
|
|
231
242
|
steps.push(mcp.step);
|
|
232
243
|
const skill = await saveSkill(io, origin, before?.skill ?? null);
|
|
233
244
|
steps.push(skill.step);
|
|
234
|
-
const
|
|
235
|
-
|
|
245
|
+
const commands = {
|
|
246
|
+
SessionStart: hookCommand(io, origin, HOOKS.SessionStart),
|
|
247
|
+
UserPromptSubmit: hookCommand(io, origin, HOOKS.UserPromptSubmit),
|
|
248
|
+
};
|
|
236
249
|
const merged = mergeSettings(await readSettings(io), commands);
|
|
237
250
|
if (merged.changed)
|
|
238
251
|
await writeSettings(io, merged.settings);
|
|
239
252
|
steps.push({
|
|
240
253
|
ok: true,
|
|
241
254
|
text: merged.changed
|
|
242
|
-
? `Installed the session-start pickup and next-message hand-off hooks in ${settingsPath(io)}.`
|
|
255
|
+
? `Installed or updated the session-start pickup and next-message hand-off hooks in ${settingsPath(io)}.`
|
|
243
256
|
: 'The hooks were already installed.',
|
|
244
257
|
});
|
|
245
258
|
const manifest = {
|
|
@@ -248,10 +261,7 @@ export async function installClaude(io, origin) {
|
|
|
248
261
|
claudeDir: claudeDir(io),
|
|
249
262
|
mcpAdded: mcp.added || (before?.mcpAdded ?? false),
|
|
250
263
|
skill: skill.skill,
|
|
251
|
-
hooks:
|
|
252
|
-
SessionStart: `${self} ${HOOKS.SessionStart}`,
|
|
253
|
-
UserPromptSubmit: `${self} ${HOOKS.UserPromptSubmit}`,
|
|
254
|
-
},
|
|
264
|
+
hooks: commands,
|
|
255
265
|
permissions: [...new Set([...(before?.permissions ?? []), ...merged.permissionsAdded])],
|
|
256
266
|
};
|
|
257
267
|
await writeWhole(manifestPath(io), `${JSON.stringify(manifest, null, 2)}\n`, { secret: true });
|
package/dist/cli.js
CHANGED
|
@@ -1,8 +1,17 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
// `npx pendingyou …`: runs the command line with the real computer (realIo) and exits with its code.
|
|
3
3
|
import { realIo } from "./io.js";
|
|
4
|
-
import { main } from "./main.js";
|
|
5
|
-
|
|
4
|
+
import { isHook, main } from "./main.js";
|
|
5
|
+
const argv = process.argv.slice(2);
|
|
6
|
+
// A hook (pickup, handoff) exits 0 even here: it must never block the person's message.
|
|
7
|
+
const failed = isHook(argv) ? 0 : 1;
|
|
8
|
+
try {
|
|
9
|
+
main(argv, realIo()).then((code) => process.exit(code), () => {
|
|
10
|
+
process.stderr.write('pendingyou: Something went wrong.\n');
|
|
11
|
+
process.exit(failed);
|
|
12
|
+
});
|
|
13
|
+
}
|
|
14
|
+
catch {
|
|
6
15
|
process.stderr.write('pendingyou: Something went wrong.\n');
|
|
7
|
-
process.exit(
|
|
8
|
-
}
|
|
16
|
+
process.exit(failed);
|
|
17
|
+
}
|
package/dist/main.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// The command line: parse the arguments, run the command, return the exit code. `cli.ts` runs it for real; tests run
|
|
2
2
|
// it with their own Io.
|
|
3
3
|
import { getJson, SignInNeeded, Unavailable } from "./api.js";
|
|
4
|
-
import { parseArgs, USAGE, UsageError } from "./args.js";
|
|
4
|
+
import { commandOf, 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
7
|
import { PlainError } from "./errors.js";
|
|
@@ -143,18 +143,33 @@ async function run(io, command) {
|
|
|
143
143
|
return watch(io, command);
|
|
144
144
|
}
|
|
145
145
|
}
|
|
146
|
+
/**
|
|
147
|
+
* Whether these arguments run a Claude Code hook (pickup or handoff). A hook must never get in the way of the person:
|
|
148
|
+
* Claude Code blocks their message when a UserPromptSubmit hook exits 2 and flags any other failure, so a hook exits 0
|
|
149
|
+
* whatever goes wrong, with at most a short note on stderr (which Claude Code doesn't add to the conversation).
|
|
150
|
+
*/
|
|
151
|
+
export const isHook = (argv) => HOOK_COMMANDS.has(commandOf(argv) ?? '');
|
|
146
152
|
export async function main(argv, io) {
|
|
153
|
+
const hook = isHook(argv);
|
|
147
154
|
let command;
|
|
148
155
|
try {
|
|
149
156
|
command = parseArgs(argv, io.env);
|
|
150
157
|
}
|
|
151
158
|
catch (error) {
|
|
159
|
+
if (hook) {
|
|
160
|
+
const message = error instanceof UsageError ? error.message : 'Something went wrong.';
|
|
161
|
+
io.err(`pendingyou: ${message} Skipped this hook; run npx pendingyou@latest init to repair it.\n`);
|
|
162
|
+
return 0;
|
|
163
|
+
}
|
|
152
164
|
if (error instanceof UsageError) {
|
|
153
165
|
io.err(`${error.message}\n\n${USAGE}`);
|
|
154
166
|
return 2;
|
|
155
167
|
}
|
|
156
168
|
throw error;
|
|
157
169
|
}
|
|
170
|
+
// `pickup --help` and the like: nothing for Claude Code to add to the conversation.
|
|
171
|
+
if (hook && !HOOK_COMMANDS.has(command.name))
|
|
172
|
+
return 0;
|
|
158
173
|
try {
|
|
159
174
|
return await run(io, command);
|
|
160
175
|
}
|
|
@@ -167,6 +182,6 @@ export async function main(argv, io) {
|
|
|
167
182
|
? error.message
|
|
168
183
|
: 'Something went wrong.';
|
|
169
184
|
io.err(`pendingyou: ${message}\n`);
|
|
170
|
-
return 1;
|
|
185
|
+
return hook ? 0 : 1;
|
|
171
186
|
}
|
|
172
187
|
}
|
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.
|
|
2
|
+
export const VERSION = '0.2.1';
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pendingyou",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.1",
|
|
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",
|
|
@@ -11,6 +11,7 @@
|
|
|
11
11
|
"files": [
|
|
12
12
|
"dist",
|
|
13
13
|
"README.md",
|
|
14
|
+
"CHANGELOG.md",
|
|
14
15
|
"LICENSE"
|
|
15
16
|
],
|
|
16
17
|
"engines": {
|