transitions-refine 0.3.9 → 0.3.11

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.
@@ -1,11 +1,23 @@
1
1
  ---
2
2
  name: refine-live
3
- description: Become the live "Refine" agent for the Timeline Inspector. Use when the user runs `/refine live`, asks to "refine live", "go live", "answer refine jobs", or wants the timeline panel's Refine button (LLM mode), Accept button, or grouped scan to be backed by a real agent. Long-polls the local refine relay, reasons about each CSS transition with the transitions-dev skill, posts suggestions back to the browser panel, for "scan" jobs groups the page's transitions into components with open/close phases by reading the source, and for "apply" jobs writes the accepted timing changes into the user's source code.
3
+ description: In-chat fallback for the Timeline Inspector Refine agent. Use when the user runs `/refine live`, asks to "refine live", "go live", or answer refine jobs but ONLY when no persistent agent is wired (no `npx transitions-refine live`). Prefer `npx transitions-refine live` for run-and-forget (relay spawns agent per click, no idle credit burn). This skill long-polls the relay, posts suggestions, handles scan/apply jobs.
4
4
  ---
5
5
 
6
6
  # Refine Live
7
7
 
8
- Turn yourself into the LLM behind the Timeline Inspector's **Refine** button. While
8
+ ## Two modes
9
+
10
+ **Persistent (recommended — run and forget)**
11
+ Run `npx transitions-refine live` from your project. The CLI starts the relay and wires `REFINE_AGENT_CMD` so the relay spawns your agent CLI **per Refine click**. No chat loop; idle = zero credit burn. Works hours later as long as the relay process keeps running. Stop with Ctrl-C (or `npx transitions-refine stop`).
12
+
13
+ **In-chat loop (fallback — this skill)**
14
+ Run `/refine live` in Cursor/Claude/Codex when the relay is up but has **no** `REFINE_AGENT_CMD`. **You** become the poller via `GET /jobs/next`. The Agent tab stays available only while you keep polling — **each idle poll cycle consumes chat turns/credits**. Say "stop refine" to exit.
15
+
16
+ Use the in-chat loop only when you cannot wire a persistent agent CLI.
17
+
18
+ ---
19
+
20
+ Turn yourself into the LLM behind the Timeline Inspector's **Refine** button (**in-chat fallback mode**). While
9
21
  this loop runs, the panel's **LLM** tab is "available": each click sends one
10
22
  transition here, you reason about it, and your suggestions appear in the panel.
11
23
 
@@ -19,15 +31,28 @@ Browser (Refine, LLM tab) ──POST /jobs──► relay ──GET /jobs/next
19
31
  ◄──GET /jobs/:id── relay ◄──POST /jobs/:id/result── YOU
20
32
  ```
21
33
 
22
- ## The loop — stay live for the whole session
23
-
24
- **Keep polling continuously until the user explicitly stops you.** This is the
25
- only thing that keeps the panel's LLM tab "available", so do not give up on idle.
26
- A long stretch of `204` responses is *normal and expected* — it just means no one
27
- has clicked Refine yet. Re-poll immediately every time; never treat repeated
28
- `204`s as a reason to stop. The relay reports the agent as "available" for ~120s
29
- after your last poll, so as long as you keep looping you stay live and the user
30
- never has to re-run `/refine live`.
34
+ ## The loop — stay live, but don't burn credits forever
35
+
36
+ Keep polling so the panel's LLM tab stays "available", but this loop costs chat
37
+ turns/credits even while idle, so it is **not** truly run-and-forget it has
38
+ three exits, in priority order:
39
+
40
+ 1. **Relay stop signal (authoritative).** `GET /jobs/next` may return `200` with
41
+ `{"stop": true}`. The relay sends this when the user clicks **Stop** in the
42
+ panel, or automatically after ~10 min with no jobs. **Always honor it: stop
43
+ looping immediately**, tell the user the LLM tab will go unavailable and how to
44
+ resume (`/refine live`), and end your turn. Never re-poll after a stop signal.
45
+ 2. **The user says so** — "stop refine", "exit live", etc.
46
+ 3. **Your own idle backoff (safety net).** A long stretch of `204`s is normal —
47
+ it just means no one has clicked Refine yet — but to avoid spending credits on
48
+ a forgotten loop, **back off as idle grows** instead of hammering immediately:
49
+ re-poll right away for the first few empty cycles, then pause ~5s between polls,
50
+ and after ~10 min of unbroken idle stop on your own (same as the relay's
51
+ auto-stop) and tell the user how to resume. Any real job resets the backoff.
52
+
53
+ The relay reports the agent as "available" for ~120s after your last poll, so
54
+ short pauses keep you live. A successful job always resets idle, so an active
55
+ session never backs off.
31
56
 
32
57
  1. **Claim the next job (long-poll).** This call blocks up to ~25s, then returns.
33
58
 
@@ -35,8 +60,12 @@ never has to re-run `/refine live`.
35
60
  curl -s http://localhost:7331/jobs/next
36
61
  ```
37
62
 
38
- - HTTP `204` / empty body → no work yet. Immediately call it again.
39
- - HTTP `200` with JSON a job. Shape:
63
+ - HTTP `204` / empty body → no work yet. Poll again, applying the idle
64
+ backoff above (immediate at first, then ~5s pauses, then stop after ~10 min).
65
+ - HTTP `200` with `{"stop": true}` → **the loop must end.** Stop polling, tell
66
+ the user the LLM tab is now unavailable and that `/refine live` resumes it,
67
+ and end your turn. Do not treat it as a job.
68
+ - HTTP `200` with a job JSON → work to do. Shape:
40
69
 
41
70
  ```json
42
71
  {
@@ -64,11 +93,11 @@ never has to re-run `/refine live`.
64
93
  posting suggestions. Everything below (refineType, steps 3–4) is for the
65
94
  normal Refine flow.
66
95
  - `refineType` chooses what kinds of suggestions to make (it mirrors the
67
- panel's two tabs):
96
+ panel's two tabs). The tabs scan **independently**, so answer only the one
97
+ you were asked for:
68
98
  - `"small"` (or missing) → **Small refinements**: nudge the existing
69
- declarations toward the motion tokens (step 3a) **and**, when it's possible
70
- and sensible, *also* suggest swapping the whole transition for a
71
- transitions.dev recipe (step 3b).
99
+ declarations toward the motion tokens **only** (step 3a). Do **not**
100
+ propose a recipe swap here — that's the Replace tab's separate job.
72
101
  - `"replace"` → **Replace transition**: suggest a whole-transition recipe
73
102
  swap **only** (step 3b). Do **not** propose motion-token tweaks — skip
74
103
  step 3a entirely.
@@ -81,50 +110,74 @@ never has to re-run `/refine live`.
81
110
  -d '{"message":"Matching to transitions.dev motion tokens…"}'
82
111
  ```
83
112
 
84
- 3. **Reason about the transition.** Read `transitions-dev` and apply its
85
- `transitions refine` behaviour. Which steps you run depends on `refineType`:
86
- - `refineType === "small"` do step 3a **and** step 3b.
87
- - `refineType === "replace"` do step 3b **only** (skip 3a no token tweaks).
113
+ 3. **Answer in ONE shot — speed matters.** Each click should feel instant, so
114
+ resolve the job from the data below plus what's already in this skill. Do **not**
115
+ spawn subagents or run a broad codebase search, and do **not** open the
116
+ transitions-dev `SKILL.md` its tokens and decision rules are inlined here.
117
+ - `refineType === "small"` → step 3a only, with **zero file reads**.
118
+ - `refineType === "replace"` → step 3b only; open **at most the one** recipe
119
+ reference file you choose.
88
120
 
89
121
  First, infer each declaration's **usage** from `label` + `selector` (modal
90
- close, dropdown open, tooltip, badge, resize, color/theme change…). Every
91
- decision below keys off that usage — match on **intent, not the nearest
92
- number**.
93
-
94
- **3a. Motion-token tweaks (only for `refineType === "small"`).** Using
95
- `## Motion tokens`:
96
- - Pick the motion token that fits the usage — a modal close wants a fast exit;
97
- a color/theme change can be slower; spring/back easings suit playful slides,
98
- not opacity.
99
- - Only propose a change where the current value actually differs.
100
-
101
- **3b. Replace the whole transition (for `small` when sensible, always
102
- considered for `replace`).** Judge whether this transition would be better off
103
- re-built as one of the twenty-one transitions.dev recipes:
104
- - The `transitions-dev` skill stays in the loop — run its `## Decision rules`
105
- against the inferred usage to pick the **single** best-fit recipe, then open
106
- that recipe's reference file (e.g. `06-modal.md`, `05-menu-dropdown.md`) to
107
- read its real timings, easing, distance, scale, and blur.
108
- - Only propose a replacement when it is **possible and sensible**: the current
109
- declarations are clearly a hand-rolled version of a catalogued recipe, or are
110
- missing the structure the usage calls for (e.g. an opacity-only "modal" that
111
- should scale, a width tween that should be the card-resize recipe). If the
112
- transition already *is* the right recipe, or no recipe genuinely fits, **do
113
- not** force one.
114
- - For `refineType === "small"`: skip the replace suggestion and let the 3a
115
- token tweaks stand alone.
116
- - For `refineType === "replace"`: there are no token tweaks to fall back on,
117
- so return an **empty** `suggestions` array with a short `summary` saying the
118
- transition already fits / no recipe applies.
119
- - Emit at most **one** `kind: "replace"` suggestion per job. For `small` it sits
120
- alongside the token tweaks (don't drop those); for `replace` it is the only
121
- suggestion.
122
- - Make its `patch` apply what the panel *can* apply live — the recipe's
123
- recommended duration/easing for the property that already transitions (or
124
- `"all"`). Name the recipe and its reference file in `title` + `reason` so the
125
- user knows the structural parts (keyframes, extra properties, JS hooks) come
126
- from running `transitions apply <recipe>` / pasting that reference file. Never
127
- invent timingsquote the ones from the reference file.
122
+ close, dropdown open, tooltip, badge, resize, color/theme change…). Match on
123
+ **intent, not the nearest number**.
124
+
125
+ **3a. Motion-token tweaks (`refineType === "small"` only — no file reads).**
126
+ Pick the token that fits the usage and propose a change **only where the current
127
+ value actually differs**.
128
+
129
+ - **Durations:** 40ms Stagger (per-item offset) · 80ms Micro (tooltip delay,
130
+ shake segment) · 150ms Quick (modal/dropdown close, text swap, tooltip
131
+ appear) · 250ms Fast (icon swap, dropdown/modal open, tabs slide, page
132
+ slide) · 350ms Medium (panel/toast close) · 400ms Slow (panel open, skeleton
133
+ reveal, input clear) · 500ms Very slow (emphasis, badge appear, text reveal,
134
+ success check).
135
+ - **Default easing "Smooth ease out":** `cubic-bezier(0.22, 1, 0.36, 1)`
136
+ (modal/dropdown/panel open+close, page slide, resize, position change).
137
+ - **Other on-grid easings LEAVE UNCHANGED:** `ease-out` (tooltip),
138
+ `ease-in-out` (icon/text swap, text reveal, skeleton reveal), `linear`
139
+ (shimmer, pulse, spinner), `cubic-bezier(0.34, 1.36, 0.64, 1)` (badge pop),
140
+ `cubic-bezier(0.34, 3.85, 0.64, 1)` (avatar return).
141
+ - **Nudge toward Smooth ease out:** generic `ease`, `ease-in`, or any
142
+ hand-rolled cubic-bezier()/linear() that isn't a token above.
143
+
144
+ **3b. Whole-transition recipe swap (`refineType === "replace"` only no file
145
+ reads).** Match the inferred usage to ONE recipe below (this list *is* the
146
+ decision rules no SKILL.md or reference-file read needed). Emit ONE
147
+ `kind: "replace"` suggestion whose `patch` carries the **motion-token**
148
+ duration/easing for the recipe's phase (open vs close) on the property that
149
+ already transitions (or `"all"`), with a `reference` field naming the file and
150
+ the recipe in `title` + `reason`. The patch only drives the live preview —
151
+ exact keyframes/structure come from the user pasting that reference file, so you
152
+ never need to open it. If no recipe genuinely fits the usage, return an
153
+ **empty** `suggestions` array with a short `summary`.
154
+
155
+ - Card resize a container changes width/height on a layout change (`01-card-resize.md`)
156
+ - Number pop-in a number/digit updates (`02-number-pop-in.md`)
157
+ - Notification badge a small dot/badge appears on a trigger (`03-notification-badge.md`)
158
+ - Text states swap text content changes in place (`04-text-states-swap.md`)
159
+ - Menu dropdown an anchored surface grows from its trigger (`05-menu-dropdown.md`)
160
+ - Modal open/close — a centered dialog scales up, softer scale-down on close (`06-modal.md`)
161
+ - Panel reveal — a surface slides into a region with a cross-blur (`07-panel-reveal.md`)
162
+ - Page side-by-side — slide between list↔detail or step 1↔step 2 (`08-page-side-by-side.md`)
163
+ - Icon swap — two icons cross-fade in the same slot (`09-icon-swap.md`)
164
+ - Success check — a checkmark celebration: fade + rotate + bob + stroke-draw (`10-success-check.md`)
165
+ - Avatar group hover — hover lifts an item in a horizontal stack (`11-avatar-group-hover.md`)
166
+ - Error state shake — invalid-input shake (`12-error-state-shake.md`)
167
+ - Input clear with dissolve — clearing a text field (`13-input-clear-dissolve.md`)
168
+ - Skeleton loader and reveal — placeholder pulses then swaps to real content (`14-skeleton-reveal.md`)
169
+ - Shimmer text — in-progress / "thinking" text shimmer (`15-shimmer-text.md`)
170
+ - Tabs sliding — a moving highlight across segmented options (`16-tabs-sliding.md`)
171
+ - Tooltip open/close — delayed fade+scale in, instant out (`17-tooltip.md`)
172
+ - Texts reveal — staggered blurred rise of stacked text lines (`18-texts-reveal.md`)
173
+ - Card hover tilt — 3D tilt toward the pointer (`19-card-tilt.md`)
174
+ - Plus to menu morph — a circular trigger becomes the surface it opens (`20-plus-menu-morph.md`)
175
+ - Accordion expand — a collapsible body grows/shrinks in height (`21-accordion.md`)
176
+
177
+ Tie-break: prefer the lower-overhead recipe (card resize over panel reveal,
178
+ dropdown over modal). Only propose a swap when the current declarations are
179
+ clearly a hand-rolled version of a recipe or are missing the structure the usage
180
+ calls for; if the transition already *is* the right recipe, return empty.
128
181
 
129
182
  4. **Post the result** (this completes the job and renders cards in the panel):
130
183
 
@@ -148,9 +201,8 @@ never has to re-run `/refine live`.
148
201
  }'
149
202
  ```
150
203
 
151
- The example above is a `small` job (token tweaks). When a recipe genuinely
152
- fits, include a `kind: "replace"` card alongside the token tweaks for
153
- `small`, or as the **only** suggestion for `replace`:
204
+ The example above is a `small` job (token tweaks only). A `replace` job instead
205
+ returns a single `kind: "replace"` card as its **only** suggestion:
154
206
 
155
207
  ```json
156
208
  {
@@ -174,9 +226,10 @@ never has to re-run `/refine live`.
174
226
  -H 'Content-Type: application/json' -d '{"message":"…"}'
175
227
  ```
176
228
 
177
- 5. **Go back to step 1.** Keep looping indefinitely. **Only stop when the user
178
- explicitly tells you to** (e.g. "stop refine", "exit live"). Do not stop just
179
- because it's been quiet idle is the normal state between clicks. If you do
229
+ 5. **Go back to step 1.** Keep looping, but honor the three exits from
230
+ [the loop section](#the-loop--stay-live-but-dont-burn-credits-forever): a
231
+ `{"stop": true}` from the relay, the user telling you to stop, or your own idle
232
+ backoff/auto-stop after ~10 min quiet. A real job resets idle. Whenever you do
180
233
  stop, tell them the LLM tab will go unavailable and how to restart
181
234
  (`/refine live`).
182
235
 
package/README.md CHANGED
@@ -29,27 +29,30 @@ npx transitions-refine stop
29
29
 
30
30
  1. injects one `<script type="module" src=".../inject.js">` into your page (it looks for `index.html`, `public/index.html`, … or pass `--page <path>`),
31
31
  2. drops the `refine-live` + `transitions-dev` skills into `.agents/skills/` (so the agent makes token-aware picks),
32
- 3. starts the local relay (which serves the panel at `/inject.js`).
32
+ 3. **auto-wires an LLM backend** — it detects the agent hosting this run and points the relay at *its* CLI, so Refine uses the subscription you already have (Cursor → `cursor-agent`, Claude Code → `claude`, Codex → `codex`),
33
+ 4. starts the local relay (which serves the panel at `/inject.js`).
33
34
 
34
35
  Open your app — the panel is now on the page. Press Ctrl-C to stop the relay and remove the injected tag.
35
36
 
36
37
  ## LLM quality (recommended)
37
38
 
38
- The default answerer snaps each value to the nearest motion token. For *usage-aware* picks (a 300 ms modal close → `Quick` 150 ms, a dropdown open → `Fast` 250 ms), back the panel with an LLM. Two ways:
39
+ The default answerer snaps each value to the nearest motion token. For *usage-aware* picks (a 300 ms modal close → `Quick` 150 ms, a dropdown open → `Fast` 250 ms), back the panel with an LLM.
40
+
41
+ Plain `npx transitions-refine live` already does this when an agent CLI is available: it prefers the **host agent** so it bills your existing plan, persistently (no `/refine live` loop to keep alive). The CLI must be authenticated once — Cursor: run `cursor-agent` to log in (or set `CURSOR_API_KEY`); Claude Code: run `claude` to sign in; Codex: run `codex` to sign in (or set `CODEX_API_KEY`).
39
42
 
40
43
  ```bash
41
- # A) persistent: install/wire the Cursor CLI so the relay answers LLM jobs itself,
42
- # per click, with no /refine live loop to keep alive (one-time CLI install)
43
- npx transitions-refine live --llm
44
- ```
44
+ # auto: use whichever agent hosts this run (cursor-agent / claude / codex)
45
+ npx transitions-refine live
45
46
 
46
- After `--llm`, make sure the CLI is authenticated once: run `cursor-agent` to log in, or set `CURSOR_API_KEY`.
47
+ # force a specific agent regardless of host
48
+ npx transitions-refine live --agent claude # cursor | claude | codex
47
49
 
48
- ```
49
- # B) in-IDE agent: run this in your editor to become the answerer yourself
50
- /refine live
50
+ # no agent on the machine? install the Cursor CLI as a fallback
51
+ npx transitions-refine live --llm
51
52
  ```
52
53
 
54
+ Resolution order: `REFINE_AGENT_CMD` (explicit) → `--agent <name>` → detected host agent → any installed agent → (with `--llm`) install cursor-agent. If none is available the panel falls back to the in-IDE loop — run `/refine live` in your editor to answer jobs yourself (works in Cursor, Claude Code, or Codex; stays live only while that session keeps polling).
55
+
53
56
  You can also point the relay at any one-shot agent CLI via `REFINE_AGENT_CMD` (the relay feeds it the prompt on stdin and reads a JSON result from stdout):
54
57
 
55
58
  ```bash
@@ -102,6 +105,30 @@ Endpoints: `POST /jobs` (refine, `kind: "apply"`, or `kind: "scan"`), `GET /jobs
102
105
 
103
106
  Refine suggestions stay as live overrides until you press **Accept**, which is the explicit step that writes them into your source.
104
107
 
105
- ## License
106
-
107
- MIT
108
+ ## Terms & License
109
+
110
+ > Full terms: https://transitions.dev/terms.html
111
+
112
+ - **Beta software.** Refine is an early Beta. Features, commands, the panel, and
113
+ its behavior may change, regress, or be removed at any time without notice. No
114
+ guarantee of availability, stability, or fitness for any purpose.
115
+ - **Your agent credits.** Refine triggers *your own* AI coding agent (Cursor,
116
+ Claude Code, Codex, …). Every Refine click and any `npx transitions-refine
117
+ live` / `/refine live` session consumes *your* provider's tokens/credits —
118
+ including while a live session sits idle and keeps polling. You are solely
119
+ responsible for that spend; this project is not liable for and will not
120
+ reimburse any credits, fees, or overages. Run `npx transitions-refine stop`
121
+ (or say `stop refine`) when you're done.
122
+ - **The agent changes your code.** Accepting a suggestion writes changes into
123
+ your source files. Suggestions are AI-generated and may be wrong. Use version
124
+ control, review every change before committing, and keep backups. Use at your
125
+ own risk.
126
+ - **No warranty.** The software is provided **"AS IS"**, without warranty of any
127
+ kind. To the maximum extent permitted by law, the authors are not liable for
128
+ any damages — direct, indirect, incidental, or consequential, including lost
129
+ work or lost credits — arising from its use.
130
+
131
+ ### MIT License
132
+
133
+ MIT — Copyright (c) 2026 Jakub Antalik / Transitions.dev. See
134
+ https://transitions.dev/terms.html for the full license text.
package/bin/cli.mjs CHANGED
@@ -1,18 +1,21 @@
1
1
  #!/usr/bin/env node
2
2
  // Refine — transitions.dev live tool.
3
3
  //
4
- // npx transitions-refine live # inject the panel + start the relay
5
- // npx transitions-refine live --llm # + install/wire cursor-agent (persistent LLM)
6
- // npx transitions-refine stop # remove the injected <script> tag
4
+ // npx transitions-refine live # inject panel + relay; auto-wire your agent CLI
5
+ // npx transitions-refine live --agent claude # force an agent: cursor | claude | codex
6
+ // npx transitions-refine live --llm # install the Cursor CLI if no agent is found
7
+ // npx transitions-refine stop # remove the injected <script> tag
7
8
  //
8
9
  // `live` sets up the timeline + Refine with no npm install and no source edits
9
10
  // of your own:
10
11
  // 1. injects one <script type="module" src=".../inject.js"> into your page
11
12
  // 2. drops the `refine-live` + `transitions-dev` skills (for token-aware picks)
12
- // 3. ensures an LLM backend:
13
- // --llm → installs/wires the Cursor CLI (cursor-agent) so the relay
14
- // answers LLM jobs itself, persistently (no /refine live loop).
15
- // else → falls back to /refine live (in-IDE agent) + deterministic.
13
+ // 3. wires an LLM backend so the relay answers jobs itself, persistently (no
14
+ // /refine live loop). It prefers the agent HOSTING this run — Cursor →
15
+ // cursor-agent, Claude Code → claude, Codex codex so Refine uses the
16
+ // subscription you already have. Override with --agent <name> or by
17
+ // exporting REFINE_AGENT_CMD. With no agent available it falls back to the
18
+ // /refine live in-IDE loop (and --llm can install the Cursor CLI).
16
19
  // 4. starts the local refine relay (serves the panel at /inject.js).
17
20
 
18
21
  import { spawn, spawnSync } from "node:child_process";
@@ -48,6 +51,7 @@ function parseArgs(argv) {
48
51
  const a = argv[i];
49
52
  if (a === "--page" || a === "-p") args.page = argv[++i];
50
53
  else if (a === "--port") args.port = argv[++i];
54
+ else if (a === "--agent") args.agent = argv[++i];
51
55
  else if (a.startsWith("--")) args[a.slice(2)] = true;
52
56
  else args._.push(a);
53
57
  }
@@ -111,17 +115,68 @@ function dropSkill(name) {
111
115
  return existed ? "updated" : true;
112
116
  }
113
117
 
114
- // ── agent CLI (for the persistent LLM path) ──────────────────────────────────
115
- // The relay spawns `REFINE_AGENT_CMD` per job. We point it at cursor-agent so
116
- // LLM Refine works without a live `/refine live` loop. The binary may not be on
117
- // the non-interactive PATH, so we probe known install locations and use an
118
- // absolute path when wiring it up.
119
- const AGENT_BIN_CANDIDATES = [
120
- "cursor-agent",
121
- join(HOME, ".local/bin/cursor-agent"),
122
- join(HOME, ".cursor/bin/cursor-agent"),
118
+ // ── agent CLIs (for the persistent LLM path) ─────────────────────────────────
119
+ // The relay answers LLM jobs by spawning REFINE_AGENT_CMD per job (stdin =
120
+ // prompt, stdout = JSON). To bill against the account the user ALREADY pays for,
121
+ // we detect which agent is hosting this run and wire ITS CLI: a Claude Code user
122
+ // gets `claude`, a Codex user gets `codex`, a Cursor user gets `cursor-agent`.
123
+ // Detection is by each host's env markers; override with --agent <name> or by
124
+ // exporting REFINE_AGENT_CMD yourself.
125
+ const envHasPrefix = (p) => Object.keys(process.env).some((k) => k.startsWith(p));
126
+
127
+ const AGENTS = [
128
+ {
129
+ key: "cursor",
130
+ label: "Cursor",
131
+ // Cursor's agent terminal exports CURSOR_AGENT.
132
+ host: () => Boolean(process.env.CURSOR_AGENT),
133
+ bins: [
134
+ "cursor-agent",
135
+ join(HOME, ".local/bin/cursor-agent"),
136
+ join(HOME, ".cursor/bin/cursor-agent"),
137
+ ],
138
+ // -p = headless/stdin, --force = auto-allow tool calls. The relay also
139
+ // auto-appends -p/--trust/--force for cursor-agent; we wire them up front so
140
+ // the printed command is the real one.
141
+ cmd: (bin) => `${bin} -p --force`,
142
+ canInstall: true,
143
+ auth: "run `cursor-agent` once to log in, or set CURSOR_API_KEY",
144
+ },
145
+ {
146
+ key: "claude",
147
+ label: "Claude Code",
148
+ // Claude Code exports CLAUDECODE=1 (+ CLAUDE_CODE_*) in its tools/terminals.
149
+ host: () => Boolean(process.env.CLAUDECODE || process.env.CLAUDE_CODE_ENTRYPOINT),
150
+ bins: [
151
+ "claude",
152
+ join(HOME, ".claude/local/claude"),
153
+ join(HOME, ".local/bin/claude"),
154
+ ],
155
+ // -p = headless print (prompt on stdin); skip-permissions so apply jobs can
156
+ // edit files without an interactive approval prompt.
157
+ cmd: (bin) => `${bin} -p --dangerously-skip-permissions`,
158
+ canInstall: false,
159
+ auth: "run `claude` once to sign in",
160
+ },
161
+ {
162
+ key: "codex",
163
+ label: "Codex",
164
+ // Codex exec exports CODEX_SANDBOX (+ CODEX_* friends) in its sandbox.
165
+ host: () => Boolean(process.env.CODEX_SANDBOX) || envHasPrefix("CODEX_"),
166
+ bins: ["codex", join(HOME, ".local/bin/codex")],
167
+ // `codex exec -` reads the prompt on stdin; workspace-write so apply jobs can
168
+ // edit files; skip-git-repo-check so a non-git project root doesn't error out.
169
+ cmd: (bin) => `${bin} exec --sandbox workspace-write --skip-git-repo-check -`,
170
+ canInstall: false,
171
+ auth: "run `codex` once to sign in, or set CODEX_API_KEY",
172
+ },
123
173
  ];
124
174
 
175
+ // Host-detection precedence. Claude/Codex export very specific markers; check
176
+ // them BEFORE Cursor so a Claude Code or Codex session launched from inside a
177
+ // Cursor terminal (which still carries CURSOR_*) is not mis-wired to cursor-agent.
178
+ const HOST_PRECEDENCE = ["claude", "codex", "cursor"];
179
+
125
180
  function isRunnable(bin) {
126
181
  try {
127
182
  return spawnSync(bin, ["--version"], { stdio: "ignore" }).status === 0;
@@ -130,11 +185,20 @@ function isRunnable(bin) {
130
185
  }
131
186
  }
132
187
 
133
- function findAgentBin() {
134
- return AGENT_BIN_CANDIDATES.find(isRunnable) || null;
188
+ function findBin(agent) {
189
+ return agent.bins.find(isRunnable) || null;
190
+ }
191
+
192
+ function detectHostAgent() {
193
+ for (const key of HOST_PRECEDENCE) {
194
+ const a = AGENTS.find((x) => x.key === key);
195
+ if (a && a.host()) return a;
196
+ }
197
+ return null;
135
198
  }
136
199
 
137
- function installAgentCli() {
200
+ // Install the Cursor CLI (the only agent we can fetch non-interactively).
201
+ function installCursorCli() {
138
202
  log("• installing the Cursor CLI (cursor-agent) — one-time…");
139
203
  const r =
140
204
  process.platform === "win32"
@@ -147,15 +211,53 @@ function installAgentCli() {
147
211
  stdio: "inherit",
148
212
  });
149
213
  if (r.status !== 0) log("! the installer exited non-zero — see its output above.");
150
- return findAgentBin();
214
+ return findBin(AGENTS[0]);
151
215
  }
152
216
 
153
- // Returns an absolute-ish command string to put in REFINE_AGENT_CMD, or null.
154
- function ensureAgentCli({ autoInstall }) {
155
- const bin = findAgentBin();
156
- if (bin) return bin;
157
- if (!autoInstall) return null;
158
- return installAgentCli();
217
+ // Decide which agent CLI the relay should spawn. Returns either
218
+ // { cmd, agent, source } → wire this command (persistent LLM), or
219
+ // { cmd:null, agent?, reason } → couldn't wire; caller prints guidance.
220
+ // Precedence: explicit REFINE_AGENT_CMD → --agent <key> → host agent (same
221
+ // subscription) → any installed agent → (with --llm) install cursor-agent.
222
+ function resolveAgent({ wantLlm, forceKey }) {
223
+ if (process.env.REFINE_AGENT_CMD) {
224
+ return { cmd: process.env.REFINE_AGENT_CMD, source: "env" };
225
+ }
226
+
227
+ let target = null;
228
+ if (forceKey) {
229
+ target = AGENTS.find((a) => a.key === forceKey) || null;
230
+ if (!target) {
231
+ return { cmd: null, reason: `unknown --agent "${forceKey}" (use cursor | claude | codex)` };
232
+ }
233
+ }
234
+ if (!target) target = detectHostAgent();
235
+
236
+ if (target) {
237
+ let bin = findBin(target);
238
+ if (!bin && target.canInstall && wantLlm) bin = installCursorCli();
239
+ if (bin) return { cmd: target.cmd(bin), agent: target, source: forceKey ? "forced" : "host" };
240
+ return {
241
+ cmd: null,
242
+ agent: target,
243
+ reason:
244
+ `detected ${target.label} but its CLI isn't on PATH` +
245
+ (target.canInstall ? " — re-run with --llm to install it" : ` — install the ${target.label} CLI first`),
246
+ };
247
+ }
248
+
249
+ // No host detected (plain terminal): use any installed agent, in list order.
250
+ for (const a of AGENTS) {
251
+ const bin = findBin(a);
252
+ if (bin) return { cmd: a.cmd(bin), agent: a, source: "scan" };
253
+ }
254
+
255
+ // Nothing installed: only cursor-agent can be fetched non-interactively.
256
+ if (wantLlm) {
257
+ const bin = installCursorCli();
258
+ if (bin) return { cmd: AGENTS[0].cmd(bin), agent: AGENTS[0], source: "install" };
259
+ }
260
+ return { cmd: null, reason: "no agent CLI found" };
159
261
  }
160
262
 
161
263
  function log(msg) {
@@ -186,26 +288,28 @@ function cmdLive(args) {
186
288
  else if (r === "exists") log(`✓ ${name} skill already present (v${PKG_VERSION})`);
187
289
  }
188
290
 
189
- // 2.5) ensure an agent CLI so the relay can answer LLM jobs itself — this is
190
- // the persistent path (no `/refine live` loop to keep alive). Installing
191
- // fetches a system binary, so it only happens with explicit opt-in via
192
- // `--llm`. If REFINE_AGENT_CMD is already set we respect it as-is.
291
+ // 2.5) wire an agent CLI so the relay can answer LLM jobs itself — the
292
+ // persistent path (no `/refine live` loop to keep alive). We prefer the
293
+ // agent hosting this run so Refine bills the subscription the user already
294
+ // has. REFINE_AGENT_CMD (if set) always wins; --agent forces a choice.
193
295
  const wantLlm = Boolean(args.llm);
296
+ const forceKey = typeof args.agent === "string" ? args.agent : null;
194
297
  const env = { ...process.env, REFINE_RELAY_PORT: port };
195
- let agentBin = null;
196
- if (process.env.REFINE_AGENT_CMD) {
197
- log(`✓ using REFINE_AGENT_CMD from environment: ${process.env.REFINE_AGENT_CMD}`);
198
- } else {
199
- agentBin = ensureAgentCli({ autoInstall: wantLlm });
200
- if (agentBin) {
201
- // Absolute path: the relay spawns via `sh -c`, whose PATH may not include
202
- // the CLI's install dir (e.g. ~/.local/bin). `-p` = headless print mode;
203
- // `--force` clears the workspace-trust / tool-approval prompts that would
204
- // otherwise hang a non-interactive spawn.
205
- const cmd = `${agentBin} -p --force`;
206
- env.REFINE_AGENT_CMD = cmd;
207
- log(`✓ LLM path wired: relay will spawn ${cmd}`);
298
+ const resolved = resolveAgent({ wantLlm, forceKey });
299
+ if (resolved.cmd) {
300
+ env.REFINE_AGENT_CMD = resolved.cmd;
301
+ if (resolved.source === "env") {
302
+ log(`✓ using REFINE_AGENT_CMD from environment: ${resolved.cmd}`);
303
+ } else {
304
+ const via =
305
+ resolved.source === "host" ? `detected ${resolved.agent.label}`
306
+ : resolved.source === "forced" ? `forced ${resolved.agent.label}`
307
+ : resolved.source === "scan" ? `found ${resolved.agent.label}`
308
+ : `installed ${resolved.agent.label}`;
309
+ log(`✓ LLM path wired (${via}): relay will spawn ${resolved.cmd}`);
208
310
  }
311
+ } else if (resolved.reason) {
312
+ log(`• LLM not wired — ${resolved.reason}.`);
209
313
  }
210
314
 
211
315
  // 3) start the relay (foreground; Ctrl-C stops it + reverts the injection)
@@ -215,6 +319,7 @@ function cmdLive(args) {
215
319
  });
216
320
 
217
321
  const llmWired = Boolean(env.REFINE_AGENT_CMD);
322
+ const authHint = resolved.agent && resolved.agent.auth;
218
323
  log("");
219
324
  log("Next:");
220
325
  log(" 1. Open your app — the timeline panel is now on the page.");
@@ -222,17 +327,12 @@ function cmdLive(args) {
222
327
  if (llmWired) {
223
328
  log(" 3. LLM suggestions are ON — the relay runs the agent CLI per click,");
224
329
  log(" so you never have to run /refine live.");
225
- log(" One-time: make sure the CLI is authenticated —");
226
- log(" run `cursor-agent` once to log in, or set CURSOR_API_KEY.");
227
- } else if (wantLlm) {
228
- log(" 3. LLM was requested but cursor-agent isn't available (install failed).");
229
- log(" Install it manually, then re-run:");
230
- log(" curl https://cursor.com/install -fsS | bash");
231
- log(" …or run /refine live in your editor for the in-IDE-agent path.");
330
+ if (authHint) log(` One-time: make sure the CLI is authenticated — ${authHint}.`);
232
331
  } else {
233
- log(" 3. For persistent LLM (no /refine live needed), re-run with --llm");
234
- log(" (installs the Cursor CLI once). Or run /refine live in your editor");
235
- log(" to use the in-IDE agent.");
332
+ log(" 3. No agent CLI wired, so LLM features need a live answerer. Either:");
333
+ log(" run /refine live in your editor (Cursor / Claude Code / Codex), or");
334
+ log(" • re-run with --llm to install the Cursor CLI, or");
335
+ log(" • export REFINE_AGENT_CMD='<your agent CLI>' and re-run.");
236
336
  }
237
337
  log("");
238
338
  log("Press Ctrl-C to stop the relay and remove the injected tag.");
@@ -274,12 +374,21 @@ function main() {
274
374
  if (cmd === "live") return cmdLive(args);
275
375
  if (cmd === "stop") return cmdStop(args);
276
376
  log("Refine — transitions.dev live tool");
277
- log(" npx transitions-refine live # inject panel + start relay");
278
- log(" npx transitions-refine live --llm # + install/wire cursor-agent for persistent LLM");
279
- log(" npx transitions-refine stop # remove the injected tag");
377
+ log(" npx transitions-refine live # inject panel + relay; auto-wire your agent CLI");
378
+ log(" npx transitions-refine live --agent claude # force an agent: cursor | claude | codex");
379
+ log(" npx transitions-refine live --llm # install the Cursor CLI if no agent is found");
380
+ log(" npx transitions-refine stop # remove the injected tag");
280
381
  log("");
281
- log("Options: --page <html> --port <n> --llm (enable persistent LLM via the Cursor CLI)");
382
+ log("Options: --page <html> --port <n> --agent <cursor|claude|codex> --llm");
383
+ log("It prefers the agent hosting this run (Cursor/Claude Code/Codex) so Refine uses");
384
+ log("the subscription you already have. Or set REFINE_AGENT_CMD to wire any CLI.");
282
385
  process.exit(cmd ? 1 : 0);
283
386
  }
284
387
 
285
- main();
388
+ // Run only when invoked as the CLI entry (npx / node bin/cli.mjs), so tests can
389
+ // import the resolver helpers without triggering a live run.
390
+ if (process.argv[1] && fileURLToPath(import.meta.url) === process.argv[1]) {
391
+ main();
392
+ }
393
+
394
+ export { AGENTS, HOST_PRECEDENCE, detectHostAgent, resolveAgent, findBin };