@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.
Files changed (47) hide show
  1. package/CHANGELOG.md +139 -0
  2. package/README.md +90 -19
  3. package/dist/changelog.d.ts.map +1 -1
  4. package/dist/changelog.js +8 -1
  5. package/dist/changelog.js.map +1 -1
  6. package/dist/daemon-lock.d.ts +102 -0
  7. package/dist/daemon-lock.d.ts.map +1 -0
  8. package/dist/daemon-lock.js +321 -0
  9. package/dist/daemon-lock.js.map +1 -0
  10. package/dist/doctor.d.ts.map +1 -1
  11. package/dist/doctor.js +11 -0
  12. package/dist/doctor.js.map +1 -1
  13. package/dist/index.d.ts.map +1 -1
  14. package/dist/index.js +107 -21
  15. package/dist/index.js.map +1 -1
  16. package/dist/login.d.ts.map +1 -1
  17. package/dist/login.js +38 -5
  18. package/dist/login.js.map +1 -1
  19. package/dist/next-steps.d.ts +1 -5
  20. package/dist/next-steps.d.ts.map +1 -1
  21. package/dist/next-steps.js +9 -5
  22. package/dist/next-steps.js.map +1 -1
  23. package/dist/rename.d.ts +18 -0
  24. package/dist/rename.d.ts.map +1 -0
  25. package/dist/rename.js +252 -0
  26. package/dist/rename.js.map +1 -0
  27. package/dist/snapshot.js +24 -4
  28. package/dist/snapshot.js.map +1 -1
  29. package/dist/terminal.d.ts.map +1 -1
  30. package/dist/terminal.js +21 -0
  31. package/dist/terminal.js.map +1 -1
  32. package/dist/theme.d.ts +5 -3
  33. package/dist/theme.d.ts.map +1 -1
  34. package/dist/theme.js +7 -5
  35. package/dist/theme.js.map +1 -1
  36. package/package.json +3 -3
  37. package/plugin/.claude-plugin/plugin.json +1 -1
  38. package/plugin/README.md +13 -4
  39. package/plugin/commands/ask.md +65 -0
  40. package/plugin/commands/fork.md +4 -3
  41. package/plugin/commands/team.md +37 -64
  42. package/plugin/commands/tell.md +66 -0
  43. package/plugin/scripts/announce-roster.mjs +21 -7
  44. package/plugin/scripts/emit-event.mjs +17 -0
  45. package/plugin/scripts/ensure-daemon.mjs +41 -1
  46. package/plugin/scripts/roster.mjs +5 -0
  47. 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 would be a
34
- // sixth of the whole budget.
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` is what makes the tight timeout survivable this reads whatever the
38
- // daemon already has, so it costs one socket round trip and no network. A hook sitting
39
- // between a keystroke and the model must never be the thing that calls the cloud.
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: 250 },
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
- if (!alive) {
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: Reach a teammate's Claude Code agent working in this same repo. Use when a question is about what a colleague decided, already ruled out, or is changing right now — why the code is the way it is when the repo does not say, whether work you are about to start is already in flight, or why something you did not touch is broken. Also covers handing this whole conversation to a teammate as a fork.
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
- The reason this skill exists is that the moment to use popover rarely announces itself. The
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
- `/popover:team` for you. Noticing is your job.
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, and it is only ever something the user
123
- asks for in as many words — never something you suggest doing on their behalf and then do.
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 it.
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
- `/popover:team` and `/popover:fork` carry the full flows for when the user drives this
143
- explicitly, including how to present a roster and how to take delivery of a fork. Do not
144
- restate their steps here or work around them when the user invokes a command, follow it.
145
- This skill is for the case where nobody invoked anything and you noticed the moment yourself.
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.