@popoverinstall/cli 0.7.2 → 0.8.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 +139 -0
- package/README.md +90 -19
- package/dist/changelog.d.ts.map +1 -1
- package/dist/changelog.js +8 -1
- package/dist/changelog.js.map +1 -1
- package/dist/daemon-lock.d.ts +102 -0
- package/dist/daemon-lock.d.ts.map +1 -0
- package/dist/daemon-lock.js +321 -0
- package/dist/daemon-lock.js.map +1 -0
- package/dist/doctor.d.ts.map +1 -1
- package/dist/doctor.js +11 -0
- package/dist/doctor.js.map +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +107 -21
- package/dist/index.js.map +1 -1
- package/dist/login.d.ts.map +1 -1
- package/dist/login.js +38 -5
- package/dist/login.js.map +1 -1
- package/dist/next-steps.d.ts +1 -5
- package/dist/next-steps.d.ts.map +1 -1
- package/dist/next-steps.js +9 -5
- package/dist/next-steps.js.map +1 -1
- package/dist/rename.d.ts +18 -0
- package/dist/rename.d.ts.map +1 -0
- package/dist/rename.js +252 -0
- package/dist/rename.js.map +1 -0
- package/dist/snapshot.js +24 -4
- package/dist/snapshot.js.map +1 -1
- package/dist/terminal.d.ts.map +1 -1
- package/dist/terminal.js +21 -0
- package/dist/terminal.js.map +1 -1
- package/dist/theme.d.ts +5 -3
- package/dist/theme.d.ts.map +1 -1
- package/dist/theme.js +7 -5
- package/dist/theme.js.map +1 -1
- package/package.json +3 -3
- package/plugin/.claude-plugin/plugin.json +1 -1
- package/plugin/README.md +13 -4
- package/plugin/commands/ask.md +65 -0
- package/plugin/commands/fork.md +4 -3
- package/plugin/commands/team.md +37 -64
- package/plugin/commands/tell.md +66 -0
- package/plugin/scripts/announce-roster.mjs +21 -7
- package/plugin/scripts/emit-event.mjs +17 -0
- package/plugin/scripts/ensure-daemon.mjs +41 -1
- package/plugin/scripts/roster.mjs +5 -0
- package/plugin/skills/popover/SKILL.md +34 -11
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: Send a short heads-up into a teammate's live session — no answer comes back, and they are notified
|
|
3
|
+
# Quoted because an unquoted value starting with `[` is parsed as a YAML flow sequence,
|
|
4
|
+
# which fails and silently drops EVERY field here, allowed-tools included.
|
|
5
|
+
argument-hint: "[agent-handle] <message>"
|
|
6
|
+
allowed-tools: mcp__plugin_popover_popover__team_list, mcp__plugin_popover_popover__team_tell, AskUserQuestion
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
# Tell a teammate's agent
|
|
10
|
+
|
|
11
|
+
The user ran `/popover:tell`. Arguments, which may be empty: **$ARGUMENTS**
|
|
12
|
+
|
|
13
|
+
A tell is the one thing in popover that reaches a **running** agent: it is delivered into a
|
|
14
|
+
teammate's live session before its next prompt, and their user is notified. It costs a
|
|
15
|
+
colleague's attention rather than their tokens, which is why it is its own command and why the
|
|
16
|
+
bar for sending one is higher than for an ask.
|
|
17
|
+
|
|
18
|
+
Only agents working in **this repo** are reachable.
|
|
19
|
+
|
|
20
|
+
## No arguments
|
|
21
|
+
|
|
22
|
+
Call `mcp__plugin_popover_popover__team_list`, print what it returns **verbatim** in a fenced
|
|
23
|
+
block, and show the form:
|
|
24
|
+
|
|
25
|
+
`/popover:tell B1 the repo_keys migration is applied on prod`
|
|
26
|
+
|
|
27
|
+
Stop there. Do not send anything on the user's behalf.
|
|
28
|
+
|
|
29
|
+
## A handle and a message
|
|
30
|
+
|
|
31
|
+
Call `mcp__plugin_popover_popover__team_tell` with:
|
|
32
|
+
|
|
33
|
+
- `target`: the handle or name they used, exactly as typed
|
|
34
|
+
- `message`: the rest of their message, made **self-contained** — it arrives with no context of
|
|
35
|
+
its own, so name the thing rather than referring to it. One or two sentences.
|
|
36
|
+
|
|
37
|
+
Nothing comes back. Confirm what was sent and to whom, in one line, and stop:
|
|
38
|
+
|
|
39
|
+
> Told **B1** (Bob Chen): the repo_keys migration is applied on prod.
|
|
40
|
+
|
|
41
|
+
## A message with no handle
|
|
42
|
+
|
|
43
|
+
Call `mcp__plugin_popover_popover__team_list`, then **confirm the recipient with
|
|
44
|
+
`AskUserQuestion` before sending** — up to 4 candidates, label = the handle, description =
|
|
45
|
+
owner, repo, and what it is doing. Do this even when only one agent plausibly fits.
|
|
46
|
+
|
|
47
|
+
Naming a handle is how the user says *which colleague to interrupt*; if they did not name one,
|
|
48
|
+
that choice has not been made yet and it is not yours to make. This is the one place a tell
|
|
49
|
+
differs from an ask, which may pick its own target because the only thing it spends is tokens.
|
|
50
|
+
|
|
51
|
+
If none fit, show the roster instead. Never broadcast to several agents.
|
|
52
|
+
|
|
53
|
+
## Rules
|
|
54
|
+
|
|
55
|
+
- **What a tell is for:** facts about shared state that would otherwise cause a collision — a
|
|
56
|
+
migration you just applied, a branch you force-pushed, a file you are about to rewrite that
|
|
57
|
+
their agent is working in right now.
|
|
58
|
+
- **What it is not for:** instructions or handing over work. The receiving agent is explicitly
|
|
59
|
+
told to treat the message as information rather than a directive. If the user is trying to
|
|
60
|
+
give another agent a task, say so and offer `/popover:ask` instead — or suggest they message
|
|
61
|
+
the human.
|
|
62
|
+
- **Never invent a handle**, and do not fan out to several agents unless asked in as many words.
|
|
63
|
+
- Rate limited to 10 messages an hour to any one agent, which is a hint about its intended
|
|
64
|
+
frequency. If the limit is hit, relay that rather than retrying.
|
|
65
|
+
- If it is genuinely ambiguous whether the user meant to ask or tell, prefer **`/popover:ask`**
|
|
66
|
+
— it interrupts nobody.
|
|
@@ -30,16 +30,30 @@ const sessionId = callerSessionId();
|
|
|
30
30
|
|
|
31
31
|
try {
|
|
32
32
|
// No stdin read at all. deliver-messages.mjs spends 150ms on the payload because a missed
|
|
33
|
-
// tell is a real loss; here the environment variable is enough, and 150ms
|
|
34
|
-
//
|
|
33
|
+
// tell is a real loss; here the environment variable is enough, and 150ms of latency the
|
|
34
|
+
// user pays on every prompt buys nothing this cannot get for free.
|
|
35
35
|
if (!sessionId) process.exit(0);
|
|
36
36
|
|
|
37
|
-
// `refresh: false`
|
|
38
|
-
//
|
|
39
|
-
//
|
|
37
|
+
// `refresh: false` means "do not force a refresh". It does NOT mean "do not use the
|
|
38
|
+
// network", which is what a comment here used to claim: the daemon caches a roster for 5s,
|
|
39
|
+
// and past that this call awaits a cloud fetch measuring ~355ms. Since a human types
|
|
40
|
+
// prompts minutes apart, nearly every one of these went to the cloud — so the 250ms budget
|
|
41
|
+
// this used to carry lost the race almost every time, failed open, and printed nothing. A
|
|
42
|
+
// hook that silently does not fire is indistinguishable from having no teammates, which is
|
|
43
|
+
// why it went unnoticed for a release.
|
|
44
|
+
//
|
|
45
|
+
// 2000ms is a ceiling on how long a *degraded* backend may hold up a prompt, not a target:
|
|
46
|
+
// the happy path is 2ms cached or ~355ms fetched. It must stay in step with
|
|
47
|
+
// ROSTER_HOOK_TIMEOUT_MS in @popoverinstall/shared, which this dependency-free script
|
|
48
|
+
// cannot import — packages/shared/test/roster-cache.test.ts fails if they drift.
|
|
49
|
+
//
|
|
50
|
+
// `cwd` matters most here of anywhere: this fires on the first prompt of a session, which
|
|
51
|
+
// is exactly when the daemon may not yet have resolved that session's repo — and the line
|
|
52
|
+
// it prints claims the agents are "in this repo". Without the hint an unplaceable caller
|
|
53
|
+
// is scoped by recency across the whole machine, so the claim could be false.
|
|
40
54
|
const reply = await request(
|
|
41
|
-
{ t: "roster", id: "hook", refresh: false, fromSessionId: sessionId },
|
|
42
|
-
{ timeoutMs:
|
|
55
|
+
{ t: "roster", id: "hook", refresh: false, fromSessionId: sessionId, cwd: process.cwd() },
|
|
56
|
+
{ timeoutMs: 2000 },
|
|
43
57
|
);
|
|
44
58
|
|
|
45
59
|
// Daemon down, or signed out. Silence is right: the skill tells an agent how to read a
|
|
@@ -13,6 +13,23 @@
|
|
|
13
13
|
|
|
14
14
|
import { readStdin, sendFireAndForget } from "./_ipc.mjs";
|
|
15
15
|
|
|
16
|
+
/*
|
|
17
|
+
* Stay silent inside a fork — the same guard deliver-messages.mjs and announce-roster.mjs
|
|
18
|
+
* carry, and for a sharper reason than either of them.
|
|
19
|
+
*
|
|
20
|
+
* A fork is a real Claude Code session with its own session id, and it runs the plugin's
|
|
21
|
+
* hooks like any other. Without this, every answered ask reported PreToolUse, PostToolUse and
|
|
22
|
+
* Stop for that throwaway session, and the daemon created a session for it — so every ask put
|
|
23
|
+
* a ghost agent on the team roster, in the answering machine's repo, that nobody could reach
|
|
24
|
+
* and that never went away until it was reaped.
|
|
25
|
+
*
|
|
26
|
+
* Worse, it poisoned attribution. `SessionStore.resolveCaller` infers who is calling from the
|
|
27
|
+
* highest `lastEventAt`, and a fork that has just finished answering is by construction the
|
|
28
|
+
* most recent thing to have emitted an event on this machine — so a tell sent moments after
|
|
29
|
+
* answering an ask was recorded as coming from the fork.
|
|
30
|
+
*/
|
|
31
|
+
if (process.env.CLAUDE_CODE_ENTRYPOINT === "popover-fork") process.exit(0);
|
|
32
|
+
|
|
16
33
|
try {
|
|
17
34
|
const raw = await readStdin();
|
|
18
35
|
const payload = JSON.parse(raw);
|
|
@@ -12,6 +12,16 @@ import { daemonAddress, popoverHome, readStdin, request, sendFireAndForget } fro
|
|
|
12
12
|
|
|
13
13
|
const HERE = path.dirname(fileURLToPath(import.meta.url));
|
|
14
14
|
|
|
15
|
+
/*
|
|
16
|
+
* A fork has nothing to do here, and doing it is actively harmful.
|
|
17
|
+
*
|
|
18
|
+
* The daemon spawns forks itself, so a daemon demonstrably exists — and registering the
|
|
19
|
+
* fork's session id would put a ghost agent on the team roster for every ask answered. Same
|
|
20
|
+
* guard, same reason, as emit-event.mjs; see its header for what the ghost then does to
|
|
21
|
+
* attribution.
|
|
22
|
+
*/
|
|
23
|
+
if (process.env.CLAUDE_CODE_ENTRYPOINT === "popover-fork") process.exit(0);
|
|
24
|
+
|
|
15
25
|
async function main() {
|
|
16
26
|
const raw = await readStdin().catch(() => "");
|
|
17
27
|
let payload = null;
|
|
@@ -22,7 +32,12 @@ async function main() {
|
|
|
22
32
|
}
|
|
23
33
|
|
|
24
34
|
const alive = await ping();
|
|
25
|
-
|
|
35
|
+
// `updateInProgress()` is the interlock, not an optimisation. `popover update` stops the
|
|
36
|
+
// daemon and hands npm up to five minutes to replace the global install; a daemon started
|
|
37
|
+
// in that window runs out of the tree being replaced and holds its files open, so npm's
|
|
38
|
+
// rename fails EPERM/EBUSY on Windows and the update aborts half-done. Every session on the
|
|
39
|
+
// machine runs this hook, so without the check one of them wins that race almost every time.
|
|
40
|
+
if (!alive && !updateInProgress()) {
|
|
26
41
|
const entry = resolveDaemonEntry();
|
|
27
42
|
if (entry) {
|
|
28
43
|
launch(entry);
|
|
@@ -95,6 +110,31 @@ function resolveDaemonEntry() {
|
|
|
95
110
|
return null;
|
|
96
111
|
}
|
|
97
112
|
|
|
113
|
+
/**
|
|
114
|
+
* Whether `popover update` is replacing the install tree right now.
|
|
115
|
+
*
|
|
116
|
+
* Written by `beginUpdateLock` in packages/cli/src/daemon-lock.ts, which holds the readable
|
|
117
|
+
* version of why this exists. Duplicated here rather than imported for the same reason
|
|
118
|
+
* `resolveDaemonEntry` duplicates its path: this file must resolve no imports beyond the ones
|
|
119
|
+
* it already has, because a hook that throws breaks every session on the machine. Keep the
|
|
120
|
+
* two in step.
|
|
121
|
+
*
|
|
122
|
+
* Every branch fails towards starting the daemon. A marker with no deadline, or one whose
|
|
123
|
+
* deadline has passed, is a crashed update rather than a running one, and refusing to start
|
|
124
|
+
* on that basis would leave a machine permanently without a daemon and nothing on screen to
|
|
125
|
+
* explain it. Only a marker that is present, readable and unexpired holds us back.
|
|
126
|
+
*/
|
|
127
|
+
function updateInProgress() {
|
|
128
|
+
try {
|
|
129
|
+
const file = path.join(popoverHome(), "update.lock");
|
|
130
|
+
if (!existsSync(file)) return false;
|
|
131
|
+
const deadline = Number(JSON.parse(readFileSync(file, "utf8")).deadline);
|
|
132
|
+
return Number.isFinite(deadline) && Date.now() < deadline;
|
|
133
|
+
} catch {
|
|
134
|
+
return false;
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
98
138
|
function launch(entry) {
|
|
99
139
|
try {
|
|
100
140
|
// Detached with stdio to a log file: the daemon must outlive this hook, this session,
|
|
@@ -18,6 +18,11 @@ const reply = await request(
|
|
|
18
18
|
id: "cli",
|
|
19
19
|
refresh: true,
|
|
20
20
|
...(callerSessionId() ? { fromSessionId: callerSessionId() } : {}),
|
|
21
|
+
// Fallback for the window where the daemon has not yet resolved this session's repo —
|
|
22
|
+
// the same hint the MCP server sends. Without it an unplaceable caller falls through to
|
|
23
|
+
// pure recency across every session on the machine, so `/popover:team` run in one repo
|
|
24
|
+
// could print the roster for another, under a heading claiming it was this one.
|
|
25
|
+
cwd: process.cwd(),
|
|
21
26
|
},
|
|
22
27
|
{ timeoutMs: 8000 },
|
|
23
28
|
);
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: popover
|
|
3
|
-
description:
|
|
3
|
+
description: "Judgment for reaching a teammate's Claude Code agent *without being asked to* — when it is worth spending their tokens, and which of /popover:team, /popover:ask, /popover:tell and /popover:fork fits. Nothing to invoke by hand: use it when a question turns on what a colleague decided, ruled out, or is changing right now — why the code is the way it is when the repo does not say, whether work about to start is already in flight, or why something nobody touched is broken."
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
# popover
|
|
@@ -10,10 +10,21 @@ accumulated context you do not have: the alternatives they rejected, the constra
|
|
|
10
10
|
forced an awkward shape, what they changed twenty minutes ago and have not pushed. popover
|
|
11
11
|
lets you reach those agents.
|
|
12
12
|
|
|
13
|
-
|
|
13
|
+
**This skill is not a command, and there is nothing here for a user to invoke.** The four
|
|
14
|
+
commands are the things a user types:
|
|
15
|
+
|
|
16
|
+
| | |
|
|
17
|
+
| --- | --- |
|
|
18
|
+
| `/popover:team` | who is working in this repo right now |
|
|
19
|
+
| `/popover:ask` | one question to one agent, and you wait for the answer |
|
|
20
|
+
| `/popover:tell` | a heads-up into a teammate's live session; nothing comes back |
|
|
21
|
+
| `/popover:fork` | hand this whole conversation to a teammate |
|
|
22
|
+
|
|
23
|
+
This file is the judgment around them: when reaching for one is worth it, which one fits, and
|
|
24
|
+
what each one spends. It exists because the moment to use popover rarely announces itself. The
|
|
14
25
|
user asks why something is the way it is, and the honest answer is that the repo does not
|
|
15
|
-
record it — but a colleague's agent worked it out this morning. Nobody will type
|
|
16
|
-
|
|
26
|
+
record it — but a colleague's agent worked it out this morning. Nobody will type `/popover:ask`
|
|
27
|
+
for you. Noticing is your job.
|
|
17
28
|
|
|
18
29
|
## The three tools
|
|
19
30
|
|
|
@@ -119,11 +130,19 @@ match, relay that instead of trying a different agent.
|
|
|
119
130
|
|
|
120
131
|
A fork hands over **this entire conversation**, frozen, and the recipient continues it in
|
|
121
132
|
their own session. Nothing comes back. That includes the contents of every file read into the
|
|
122
|
-
context, so it is a far larger disclosure than an ask
|
|
123
|
-
|
|
133
|
+
context, so it is a far larger disclosure than an ask.
|
|
134
|
+
|
|
135
|
+
**Creating one is the user's alone.** It happens when they ask for it in as many words, never
|
|
136
|
+
on their behalf — and "that would be useful" is not a yes.
|
|
137
|
+
|
|
138
|
+
Saying that a fork would fit is a different act from making one, and it is worth doing once.
|
|
139
|
+
The moment for a fork is one the user is usually too deep in the work to notice: they are going
|
|
140
|
+
off shift, or about to re-explain an hour of debugging to a colleague by hand, or trading asks
|
|
141
|
+
back and forth on what was never really one question. Say so plainly, say what the fork would
|
|
142
|
+
include, and leave it there. If they pass, do not raise it again.
|
|
124
143
|
|
|
125
144
|
When the user wants an *answer* rather than to hand over the whole conversation, an ask is the
|
|
126
|
-
cheaper and more private thing. Offer
|
|
145
|
+
cheaper and more private thing. Offer that instead.
|
|
127
146
|
|
|
128
147
|
## When it does not work
|
|
129
148
|
|
|
@@ -139,7 +158,11 @@ The tools return errors as text; relay them rather than retrying with different
|
|
|
139
158
|
|
|
140
159
|
## The user-facing commands
|
|
141
160
|
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
161
|
+
The four commands carry the full flows for when the user drives this explicitly — how to
|
|
162
|
+
present a roster, how to word an ask, how to take delivery of a fork. Do not restate their
|
|
163
|
+
steps here or work around them: **when the user invokes a command, follow that command.** This
|
|
164
|
+
skill is for the other case, where nobody invoked anything and you noticed the moment yourself.
|
|
165
|
+
|
|
166
|
+
When you act on your own, prefer naming the command you are standing in for — "I'll run the
|
|
167
|
+
equivalent of `/popover:ask B1`" — so the user learns the surface they can drive directly next
|
|
168
|
+
time.
|