@macintacos/caret 0.14.0 → 1.0.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 (112) hide show
  1. package/.claude-plugin/plugin.json +9 -2
  2. package/README.md +55 -69
  3. package/bin/caret-launcher +208 -0
  4. package/commands/demo.md +11 -10
  5. package/dist/cli.js +49112 -27353
  6. package/hooks/hooks.json +16 -0
  7. package/opencode/caret.plugin.ts +173 -277
  8. package/opencode/commands/demo.md +9 -20
  9. package/opencode/index.ts +1 -2
  10. package/opencode/review-bridge.ts +241 -0
  11. package/package.json +9 -6
  12. package/templates/demo.md +89 -0
  13. package/ui/dist/assets/{angular-html-C0XElF0f.js → angular-html-CNIx69MF.js} +1 -1
  14. package/ui/dist/assets/{angular-ts-BVnV1Yg5.js → angular-ts-B6ob15kD.js} +1 -1
  15. package/ui/dist/assets/{apl-BFyMAB9c.js → apl-BzQg5wKE.js} +1 -1
  16. package/ui/dist/assets/{astro-DAtaAFYq.js → astro-DQ6PiTON.js} +1 -1
  17. package/ui/dist/assets/{blade-BoZH76v_.js → blade-DdGMUzbN.js} +1 -1
  18. package/ui/dist/assets/{c-DyRawzd0.js → c-DhrAH2di.js} +1 -1
  19. package/ui/dist/assets/{chapel-CAaCMdek.js → chapel-BIJH6n90.js} +1 -1
  20. package/ui/dist/assets/{cobol-BjXiLsat.js → cobol-CR-dFJ1T.js} +1 -1
  21. package/ui/dist/assets/{coffee-DvjcTlIE.js → coffee-DWT27RO5.js} +1 -1
  22. package/ui/dist/assets/{cpp-sRP7rZGr.js → cpp-CuONTEvg.js} +1 -1
  23. package/ui/dist/assets/{crystal-CaRkbKnA.js → crystal-Bvc0l-OM.js} +1 -1
  24. package/ui/dist/assets/{css-Igh3vvbt.js → css-BYejQHVV.js} +1 -1
  25. package/ui/dist/assets/{dist-D4FUs8dC.js → dist-BbAsmjOe.js} +1 -1
  26. package/ui/dist/assets/{dist-lE2sJQyW.js → dist-BgZhLp84.js} +1 -1
  27. package/ui/dist/assets/{dist-BkB3Dkvt.js → dist-Bn_ExXCc.js} +1 -1
  28. package/ui/dist/assets/{dist-CBxRdWpx.js → dist-BubrR0xG.js} +1 -1
  29. package/ui/dist/assets/dist-C-FIXuZc.js +2 -0
  30. package/ui/dist/assets/{dist-B5Dwvt_o.js → dist-CCPLuvXB.js} +1 -1
  31. package/ui/dist/assets/{dist-CF4FIhxu.js → dist-COogZ9jg.js} +1 -1
  32. package/ui/dist/assets/{dist-DHAfbOzU.js → dist-CfuU_6ln.js} +1 -1
  33. package/ui/dist/assets/{dist-CGl7bSLs.js → dist-CjIRqXSt.js} +1 -1
  34. package/ui/dist/assets/{dist-CZeI5UV9.js → dist-Cuerw0_4.js} +1 -1
  35. package/ui/dist/assets/{dist-CYIXdHF6.js → dist-D-lJ6tIg.js} +1 -1
  36. package/ui/dist/assets/{dist-C_lXyw3s.js → dist-D0Ats_4w.js} +1 -1
  37. package/ui/dist/assets/dist-DOswrDst.js +12 -0
  38. package/ui/dist/assets/dist-DV7t-p4N.js +1 -0
  39. package/ui/dist/assets/{dist-5Bv2mYWj.js → dist-DXn_f-MA.js} +1 -1
  40. package/ui/dist/assets/{dist-D_g9f9rz.js → dist-Lc2Es7Pl.js} +1 -1
  41. package/ui/dist/assets/{dist-CF48ImGm.js → dist-RKZBj-sl.js} +1 -1
  42. package/ui/dist/assets/{dist-Dt1D8SWK.js → dist-TxQM6vOw.js} +1 -1
  43. package/ui/dist/assets/{edge-CvHTOXgI.js → edge-B_-8Eolr.js} +1 -1
  44. package/ui/dist/assets/{elixir-CtokNUaA.js → elixir-CAhwbI74.js} +1 -1
  45. package/ui/dist/assets/{elm-B34p-CC5.js → elm-yg-VSwEl.js} +1 -1
  46. package/ui/dist/assets/{erb-FjaGN5LV.js → erb-CIMOpWGO.js} +1 -1
  47. package/ui/dist/assets/{git-rebase-BFUCuD7O.js → git-rebase-DI22snkc.js} +1 -1
  48. package/ui/dist/assets/{glimmer-js-CRs6sgzF.js → glimmer-js-CZHZNAs-.js} +1 -1
  49. package/ui/dist/assets/{glimmer-ts-Bkt0v-q2.js → glimmer-ts-C2rq-JGq.js} +1 -1
  50. package/ui/dist/assets/{glsl-Ckh3d22M.js → glsl-ByXN6hIN.js} +1 -1
  51. package/ui/dist/assets/{graphql-IqxoIj3x.js → graphql-DiiJwLqR.js} +1 -1
  52. package/ui/dist/assets/{hack-DG1p8zpS.js → hack-hQSSm1D1.js} +1 -1
  53. package/ui/dist/assets/{haml-DA2jQcOV.js → haml-cf9hcOgw.js} +1 -1
  54. package/ui/dist/assets/{handlebars-CqT0epoo.js → handlebars-B3tg892p.js} +1 -1
  55. package/ui/dist/assets/{html-CoTd-Axz.js → html-CkIr2qlC.js} +1 -1
  56. package/ui/dist/assets/{html-derivative-DDt5A1xL.js → html-derivative-U1EOyk5J.js} +1 -1
  57. package/ui/dist/assets/{http-TEp9u8BR.js → http-ek0-hDT8.js} +1 -1
  58. package/ui/dist/assets/{hurl-OLD-GtJJ.js → hurl-DbN52Bpk.js} +1 -1
  59. package/ui/dist/assets/index-CAl4rX7_.css +1 -0
  60. package/ui/dist/assets/{index-BucWCjoT.js → index-uBaTjG1U.js} +367 -318
  61. package/ui/dist/assets/{java-DYZ_BnxQ.js → java-DXEu0FGs.js} +1 -1
  62. package/ui/dist/assets/{javascript-B4vvB79r.js → javascript-Bi95ibpN.js} +1 -1
  63. package/ui/dist/assets/{jinja-BTulBbvB.js → jinja-BcKnkqjY.js} +1 -1
  64. package/ui/dist/assets/{jison-CDVJmcGj.js → jison-CFaHQ730.js} +1 -1
  65. package/ui/dist/assets/{json-DahQZwqh.js → json--ulVurkL.js} +1 -1
  66. package/ui/dist/assets/{jsx-Dzcw0XhS.js → jsx-ZonywBBD.js} +1 -1
  67. package/ui/dist/assets/{julia-BB_aSeOr.js → julia-Bnt3bGqT.js} +1 -1
  68. package/ui/dist/assets/{just-C3IsNIar.js → just-CBpil8Tz.js} +1 -1
  69. package/ui/dist/assets/{latex-D7dYt9yW.js → latex-D-qwKUqq.js} +1 -1
  70. package/ui/dist/assets/{liquid-CO9g2vFd.js → liquid-Bu8cI_b1.js} +1 -1
  71. package/ui/dist/assets/{lua-D6OK3PTP.js → lua-DO_5Mymc.js} +1 -1
  72. package/ui/dist/assets/{marko-YPWiYUQ_.js → marko-DJfs6dcc.js} +1 -1
  73. package/ui/dist/assets/{mdc-DoQ9fqeC.js → mdc-CnkU1WJT.js} +1 -1
  74. package/ui/dist/assets/{nginx-BuX_dG5x.js → nginx-B5LxXEA3.js} +1 -1
  75. package/ui/dist/assets/{nim-oEF2CEaX.js → nim-DLTorTuD.js} +1 -1
  76. package/ui/dist/assets/{org-C1kwr3b8.js → org-DnNvsXCU.js} +1 -1
  77. package/ui/dist/assets/{perl-CzZcVIM9.js → perl-Q5Q2rA31.js} +1 -1
  78. package/ui/dist/assets/{php-C_i1TWmy.js → php-S65hjGBt.js} +1 -1
  79. package/ui/dist/assets/{pug-Oz2JZVGZ.js → pug-k-MB8PEo.js} +1 -1
  80. package/ui/dist/assets/{qml-DWZFG7aa.js → qml-Bf9cLRPv.js} +1 -1
  81. package/ui/dist/assets/{r-CKwXeGZ_.js → r-Cr3hDQkZ.js} +1 -1
  82. package/ui/dist/assets/{razor-DT1mg2x6.js → razor-BO5-wpmp.js} +1 -1
  83. package/ui/dist/assets/{regexp-C2K6ptTb.js → regexp-ffQ9gsih.js} +1 -1
  84. package/ui/dist/assets/{rst-C5gmF0mo.js → rst-B4AQC5xj.js} +1 -1
  85. package/ui/dist/assets/{ruby-CrEJdgPT.js → ruby-Dr8kfvLX.js} +1 -1
  86. package/ui/dist/assets/{sas-D1TGFNd_.js → sas-Kuo2pLeL.js} +1 -1
  87. package/ui/dist/assets/{scss-DBO_Sp1C.js → scss-Ci63BQ8y.js} +1 -1
  88. package/ui/dist/assets/{shellscript-QIWVo1P5.js → shellscript-DBh4gnVx.js} +1 -1
  89. package/ui/dist/assets/{shellsession-iBMCyJwd.js → shellsession-L5KFvHI3.js} +1 -1
  90. package/ui/dist/assets/{soy-NHBvJjBh.js → soy-D6F5ilyd.js} +1 -1
  91. package/ui/dist/assets/{sql-Yf21hpnW.js → sql-xwRblxjL.js} +1 -1
  92. package/ui/dist/assets/{stata-CluZWHbV.js → stata-l8sBIGPT.js} +1 -1
  93. package/ui/dist/assets/{surrealql-Bu2dU1Qj.js → surrealql-DL2sOSzB.js} +1 -1
  94. package/ui/dist/assets/{svelte-Owb3qG7g.js → svelte-Dp5d2Ja3.js} +1 -1
  95. package/ui/dist/assets/{templ-DScnsiaP.js → templ-B3LfgeSM.js} +1 -1
  96. package/ui/dist/assets/{tex-DvCQ8DJI.js → tex-CfLQjgEV.js} +1 -1
  97. package/ui/dist/assets/{ts-tags-Fh6nOEbM.js → ts-tags-BCfi6Eh_.js} +1 -1
  98. package/ui/dist/assets/{tsx-C_2AyD6m.js → tsx-BXhT7ita.js} +1 -1
  99. package/ui/dist/assets/{twig-DjT0rOpv.js → twig-t6tdMpRg.js} +1 -1
  100. package/ui/dist/assets/{typescript-NDIZTg4J.js → typescript-CFIlDlxn.js} +1 -1
  101. package/ui/dist/assets/{typst-DNJZJFvq.js → typst-CVQ9ief2.js} +1 -1
  102. package/ui/dist/assets/{vue-BTZXwXJ_.js → vue-D92U7PL6.js} +1 -1
  103. package/ui/dist/assets/{vue-html-CBhLp-FO.js → vue-html-B_-4d7cG.js} +1 -1
  104. package/ui/dist/assets/{vue-vine-KGJI1qvi.js → vue-vine-LaG8znAe.js} +1 -1
  105. package/ui/dist/assets/{xml-CeV6vl7T.js → xml-DgAE0jfV.js} +1 -1
  106. package/ui/dist/assets/{xsl-CibkluSG.js → xsl-DkD8aIzS.js} +1 -1
  107. package/ui/dist/assets/{yaml-D89KUdIt.js → yaml-CEG92fN8.js} +1 -1
  108. package/ui/dist/index.html +3 -3
  109. package/ui/dist/assets/dist-BdaKUV8X.js +0 -2
  110. package/ui/dist/assets/dist-CiBs4R_k.js +0 -12
  111. package/ui/dist/assets/dist-YE9up3VR.js +0 -1
  112. package/ui/dist/assets/index-Djrj2rps.css +0 -1
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "caret",
3
- "version": "0.14.0",
3
+ "version": "1.0.1",
4
4
  "description": "Review, annotate, and approve Claude Code plans in a local web UI.",
5
5
  "author": { "name": "macintacos" },
6
6
  "homepage": "https://github.com/macintacos/caret",
@@ -14,5 +14,12 @@
14
14
  "hooks",
15
15
  "approval",
16
16
  "annotation"
17
- ]
17
+ ],
18
+ "mcpServers": {
19
+ "caret": {
20
+ "command": "${CLAUDE_PLUGIN_ROOT}/bin/caret",
21
+ "args": ["mcp"],
22
+ "timeout": 3900000
23
+ }
24
+ }
18
25
  }
package/README.md CHANGED
@@ -1,33 +1,27 @@
1
- # 🥕 caret
1
+ # 🥕 `caret`
2
2
 
3
- > [!WARNING]
4
- > caret is an early prototype and may change substantially over the next little while —
5
- > interfaces, hooks, storage, and the install flow are all still settling. Expect rough
6
- > edges and breaking changes.
3
+ `caret` is a plugin that replaces the terminal plan-approval prompt with a local web UI.
4
+ When your agent presents a plan, `caret` opens it in your browser so you can read it as
5
+ rendered HTML, **annotate passages inline** (Google-Docs style), and **approve** or
6
+ **request changes**. Your feedback flows straight back to the agent. A single local daemon
7
+ is shared across concurrent sessions, so several in-flight plans can be reviewed from one
8
+ browser tab via a switcher.
7
9
 
8
- caret is a Claude Code (and OpenCode) plugin that replaces the terminal plan-approval
9
- prompt with a local web UI. When your agent presents a plan, caret opens it in your
10
- browser so you can read it as rendered HTML, **annotate passages inline** (Google-Docs
11
- style), and **approve** or **request changes** — your feedback flows straight back to the
12
- agent. A single local daemon is shared across concurrent sessions, so several in-flight
13
- plans can be reviewed from one browser tab via a switcher.
14
-
15
- Want to develop caret rather than use it? Start with [CONTRIBUTING.md](CONTRIBUTING.md).
10
+ Want to develop `caret` rather than use it? Start with [CONTRIBUTING.md](CONTRIBUTING.md).
16
11
 
17
12
  ## Screenshots
18
13
 
19
- ![caret review UI with an inline annotation, stitched across four themes](doc/assets/caret-review-ui.png)
14
+ [Check out a short demo here!](https://youtu.be/zesv5AunUW4)
20
15
 
21
- [Watch a short review](https://github.com/user-attachments/assets/992b63b6-ed73-409a-a12a-bc14fd4c7d53)
22
- — comment on a line, send the plan back, approve the revision, and the agent picks up
23
- where you left it.
16
+ ![caret review UI with an inline annotation, stitched across four themes](doc/assets/caret-review-ui.png)
24
17
 
25
18
  ## Install
26
19
 
27
- caret needs [`bun`](https://bun.sh) on your `PATH` — it runs from a `bun` bundle.
20
+ `caret` needs [`bun`](https://bun.sh) on your `PATH` — it runs from a `bun` bundle.
28
21
 
29
22
  > [!NOTE]
30
- > caret is **macOS-first**; Linux and Windows are best-effort. See
23
+ > `caret` supports macOS and Linux, where the review UI can run as a long-lived login
24
+ > service. Windows is best-effort and runs `caret` on demand only. See
31
25
  > [`doc/CONFIGURING.md`](doc/CONFIGURING.md#platform-support) for what differs on each
32
26
  > platform and what to fall back on.
33
27
 
@@ -35,25 +29,27 @@ caret needs [`bun`](https://bun.sh) on your `PATH` — it runs from a `bun` bund
35
29
  bunx --no-cache @macintacos/caret@latest install
36
30
  ```
37
31
 
38
- That is the whole install — no `git clone`, no compile step. It detects which agents you
39
- have — [Claude Code](https://claude.com/claude-code), [OpenCode](https://opencode.ai), or
40
- both — asks which of them to install into, and registers the _published_ caret with each:
41
- prebuilt artifacts, the `/caret:*` slash commands, and the [rumdl](https://rumdl.dev/)
42
- plan formatter. Where it can't ask — off a terminal — it installs into every agent it
43
- detected.
32
+ Assuming that you installed it as a long-lived service (the default), you can navigate to
33
+ <http://caret.localhost:42718> to see the `caret` UI.
44
34
 
45
- Two steps finish the job:
35
+ After installing:
46
36
 
47
37
  1. **Restart the agent.** OpenCode installs the plugin package on its next start.
48
- 2. **Run `/caret:demo`.** It presents a short fake plan, so you can exercise the whole
49
- flow before a real one arrives.
38
+ 2. **Run `/caret:demo`.** It presents a short demo plan that points at files in the repo
39
+ you run it from, so you can exercise the whole flow before a real one arrives.
40
+
41
+ ### Running `caret` yourself
42
+
43
+ If you'd rather `caret` not start at login, answer **I'll run it myself** when
44
+ `caret install` asks. That removes the service if one is registered. Then, whenever you
45
+ want the review UI up, run:
46
+
47
+ ```sh
48
+ bunx @macintacos/caret@latest serve
49
+ ```
50
50
 
51
- | Flag | What it does |
52
- | ------------------- | ----------------------------------------------------------------- |
53
- | `--target <agents>` | Skip the chooser: `claude`, `opencode`, or both, comma-separated. |
54
- | `--dry-run` | Preview the run without changing anything. |
55
- | `--refresh` | Update an existing install. |
56
- | `--uninstall` | Remove caret, offering the same agent chooser. |
51
+ This will keep the UI up at `http://caret.localhost:42718` until the process is
52
+ terminated.
57
53
 
58
54
  ### Updating and uninstalling
59
55
 
@@ -64,34 +60,27 @@ bunx --no-cache @macintacos/caret@latest install --refresh # update
64
60
  bunx --no-cache @macintacos/caret@latest install --uninstall # remove
65
61
  ```
66
62
 
67
- `--target` pins the agents on both, exactly as it does on a fresh install. Restarting each
68
- agent applies an update. In OpenCode, caret toasts you at startup when a newer release is
69
- out; a plain `install` at a terminal runs its own check against npm and asks before taking
70
- it.
71
-
72
- caret's daemon runs the same check for itself, at most once a day. The call is
73
- unauthenticated and sends nothing about you — just a request to npm or GitHub, depending
74
- on how caret was installed, for the newest published version. When a newer caret is out,
75
- the review UI says so once: a toast on load, a mark on the settings button, and a
63
+ `caret`'s UI is designed to check for updates, at most once a day. When a newer `caret` is
64
+ out, the review UI says so once: a toast on load, a mark on the settings button, and a
76
65
  **Settings → Updates** pane naming the version and the exact command to take it.
77
66
 
78
- Turn the check off from that same pane, or by setting `updates.check` to `false` in
79
- `prefs.json` (`~/.local/state/caret/prefs.json`, or under `$XDG_STATE_HOME/caret`):
67
+ Turn the check off from that same pane, or by hand in
68
+ [`config.toml`](doc/CONFIGURING.md#the-updates-table):
80
69
 
81
- ```json
82
- { "updates": { "check": false } }
70
+ ```toml
71
+ [updates]
72
+ check = false
83
73
  ```
84
74
 
85
75
  See [the OpenCode adapter](doc/ARCHITECTURE.md#the-opencode-adapter) for the by-hand
86
76
  equivalents, for pinning a version in OpenCode's `plugin` array, and for what each agent's
87
77
  install touches; [the Claude Code adapter](doc/ARCHITECTURE.md#the-claude-code-adapter)
88
- covers the hooks caret registers there.
78
+ covers the hooks `caret` registers there.
89
79
 
90
- ## Using caret
80
+ ## Using `caret`
91
81
 
92
- Whenever your agent presents a plan — Claude Code's `ExitPlanMode`, or the Plan agent in
93
- OpenCode — caret intercepts it and opens it in your browser instead of the terminal
94
- prompt. There you:
82
+ Whenever your agent presents a plan, `caret` should intercept it and opens the plan in
83
+ your browser instead of the terminal prompt. There you:
95
84
 
96
85
  - **Read** the plan as rendered HTML.
97
86
  - **Annotate** — select any passage to attach an inline comment.
@@ -100,29 +89,19 @@ prompt. There you:
100
89
  and re-present.
101
90
 
102
91
  > [!TIP]
103
- > **In OpenCode**, you don't have to wait to be intercepted: caret registers a
104
- > `caret_review_plan` tool your agent can call directly, so a skill of your own can route
105
- > its approval step through the same review UI. Claude Code has no equivalent — see
92
+ > You don't have to wait to be intercepted: `caret` gives both agents a plan-review tool
93
+ > they can call directly — `review_plan` in Claude Code (from the plugin's MCP server) and
94
+ > `caret_review_plan` in OpenCode — so a skill of your own can route its plan through the
95
+ > same review UI. The tool is for plans only; see
106
96
  > [`doc/ARCHITECTURE.md`](doc/ARCHITECTURE.md#calling-the-review-tool-from-your-own-skill).
107
97
 
108
98
  ## Configuration
109
99
 
110
- caret runs with sensible defaults and needs no configuration. To tune it — the daemon
100
+ `caret` runs with sensible defaults and needs no configuration. To tune it — the daemon
111
101
  port, the review timeout, the log level — it reads an optional `config.toml` and `CARET_*`
112
102
  environment variables. Every key, every variable, and their defaults are in
113
103
  [`doc/CONFIGURING.md`](doc/CONFIGURING.md).
114
104
 
115
- ## Diagnostics
116
-
117
- - `/caret:discovery` prints a one-shot, read-only diagnostics snapshot of your install —
118
- always redacted, and it never contains plan, prompt, or feedback bodies, nor any log
119
- contents.
120
- - `/caret:debug` reviews the current session's plans and recent errors.
121
-
122
- Logs are written raw by default. `caret redact` writes scrubbed `*.redacted.log` copies
123
- alongside them — paste one of those into a chat, a gist, or an AI assistant, never the
124
- original. [`doc/RUNNING.md`](doc/RUNNING.md#logging--debugging) says where they live.
125
-
126
105
  ## Documentation
127
106
 
128
107
  [`doc/README.md`](doc/README.md) maps the `doc/` directory — start there and it routes you
@@ -130,11 +109,18 @@ to the reference page that answers your question.
130
109
 
131
110
  Two more live at the repo root:
132
111
 
133
- - [CONTRIBUTING.md](CONTRIBUTING.md) — develop caret locally: setup, the `mise` workflow,
134
- and where tests live.
112
+ - [CONTRIBUTING.md](CONTRIBUTING.md) — develop `caret` locally: setup, the `mise`
113
+ workflow, and where tests live.
135
114
  - [CLAUDE.md](CLAUDE.md) — for coding agents: routes a change to the rules-of-the-road
136
115
  that govern it.
137
116
 
117
+ ## Diagnostics
118
+
119
+ - `/caret:discovery` prints a one-shot, read-only diagnostics snapshot of your install —
120
+ always redacted, and it never contains plan, prompt, or feedback bodies, nor any log
121
+ contents.
122
+ - `/caret:debug` reviews the current session's plans and recent errors.
123
+
138
124
  ## License
139
125
 
140
126
  MIT — see [LICENSE](LICENSE). Vendored third-party assets (the Lucide icons) are itemized
@@ -0,0 +1,208 @@
1
+ #!/usr/bin/env bash
2
+ #
3
+ # The stable launcher a launchd plist or systemd unit names (EXC-1160). Those
4
+ # units must name an absolute executable path, but caret's runtime lives at a
5
+ # versioned path that changes on every upgrade — and both Claude Code and
6
+ # OpenCode replace their copy on their own schedule, without running
7
+ # `caret install`. So the unit names this file, which caret owns at
8
+ # $XDG_STATE_HOME/caret/bin/caret, and resolution happens here at exec time.
9
+ #
10
+ # Bash rather than TypeScript for the same reason bin/caret is: it has to locate
11
+ # `bun` before any TypeScript can run.
12
+ #
13
+ # Three single-value records under $XDG_STATE_HOME/caret/launcher/ steer it, each
14
+ # one line, each optional — a missing record degrades to the search path, to no
15
+ # pin, and to skipping service teardown:
16
+ #
17
+ # bun-path absolute `bun`, recorded at service-install time
18
+ # pinned-root a caret root to prefer over the highest installed version
19
+ # service the launchd label WITHOUT `.plist` on macOS; the full unit name
20
+ # WITH `.service` on Linux. Only macOS appends a suffix below.
21
+ #
22
+ # Each record must end with a newline: `read -r` reports EOF on a file without
23
+ # one, and the read is what decides the record exists.
24
+
25
+ set -euo pipefail
26
+
27
+ state="${XDG_STATE_HOME:-$HOME/.local/state}/caret"
28
+ records="$state/launcher"
29
+ claude_roots="${CLAUDE_CONFIG_DIR:-$HOME/.claude}/plugins/cache/caret/caret"
30
+ opencode_roots="${XDG_CACHE_HOME:-$HOME/.cache}/opencode/packages/@macintacos"
31
+
32
+ # The units name no log destination, so nothing opens this before caret does:
33
+ # openDaemonStderr's 0700/0600 guarantee (src/daemon/lifecycle.ts) happens here for a
34
+ # supervised daemon instead. Both modes are set at create time, so a chmod the
35
+ # filesystem refuses cannot leave a umask-derived 0755/0644 pair behind (EXC-539).
36
+ # Best-effort throughout: exiting under `set -e` would be a restart loop under
37
+ # Restart=always / KeepAlive, so an unwritable state dir degrades to the supervisor's
38
+ # own stream.
39
+ open_daemon_log() {
40
+ # One literal, not "$dir/daemon-stderr.log": test/structure/service-log-path.test.ts
41
+ # matches the path daemonStderrLogFile() resolves as a substring of this file.
42
+ local file="$state/logs/daemon-stderr.log"
43
+ local dir="${file%/*}"
44
+ (
45
+ umask 077
46
+ mkdir -p "$dir"
47
+ ) 2>/dev/null || return 0
48
+ chmod 700 "$state" "$dir" 2>/dev/null || true
49
+ # The probe is what keeps the unguarded exec below from exiting the shell.
50
+ (
51
+ umask 077
52
+ : >>"$file"
53
+ ) 2>/dev/null || return 0
54
+ chmod 600 "$file" 2>/dev/null || true
55
+ exec >>"$file" 2>&1
56
+ }
57
+
58
+ read_record() {
59
+ local value
60
+ [ -f "$1" ] && read -r value <"$1" && [ -n "$value" ] && printf '%s' "$value"
61
+ }
62
+
63
+ # Highest of the `<version><TAB><path>` lines on stdin. A numeric field sort
64
+ # rather than `sort -V`, which BSD sort does not carry everywhere. Ceiling: on an
65
+ # equal numeric prefix the whole-line tiebreak puts a prerelease ABOVE the stable
66
+ # release, so 0.15.0-rc.1 would beat 0.15.0. caret publishes no prereleases.
67
+ highest() { sort -t. -k1,1n -k2,2n -k3,3n | tail -1 | cut -f2-; }
68
+
69
+ # A candidate is a caret when it ships an executable bin/caret — the one file
70
+ # every install method carries, tested the way it is used, since a non-executable
71
+ # one would exec-fail at 126 and the supervisor would respawn into it forever.
72
+ # Version comes from package.json, never the directory name: OpenCode names its
73
+ # dir after the verbatim `plugin` specifier. The key is anchored so a minified
74
+ # manifest yields nothing rather than a later key's value — a miss falls into the
75
+ # branch that names the directory, a wrong version picks the wrong caret.
76
+ candidate_version() {
77
+ [ -x "$1/bin/caret" ] || return 1
78
+ sed -n 's/^[[:space:]]*"version"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p' "$1/package.json" 2>/dev/null | head -1
79
+ }
80
+
81
+ # Every dir either cache offers, runnable caret or not — the difference between
82
+ # a broken install (terminal) and an empty cache (eviction). The OpenCode entry
83
+ # is a two-line npm project, not a caret root; the root is one level down.
84
+ candidate_dirs() {
85
+ local dir
86
+ for dir in "$claude_roots"/* "$opencode_roots"/caret*/node_modules/@macintacos/caret; do
87
+ if [ -d "$dir" ]; then printf '%s\n' "$dir"; fi
88
+ done
89
+ }
90
+
91
+ candidates() {
92
+ local dir version
93
+ candidate_dirs | while read -r dir; do
94
+ version="$(candidate_version "$dir" || true)"
95
+ if [ -n "$version" ]; then printf '%s\t%s\n' "$version" "$dir"; fi
96
+ done
97
+ }
98
+
99
+ resolve_root() {
100
+ local pin
101
+ pin="$(read_record "$records/pinned-root" || true)"
102
+ if [ -n "$pin" ] && [ -x "$pin/bin/caret" ]; then
103
+ printf '%s' "$pin"
104
+ return 0
105
+ fi
106
+ candidates | highest
107
+ }
108
+
109
+ # The newest mise-managed bun, for a machine whose mise shims are not on the
110
+ # supervisor's PATH.
111
+ mise_bun_dir() {
112
+ local dir
113
+ for dir in "$1"/mise/installs/bun/*/bin; do
114
+ if [ -x "$dir/bun" ]; then printf '%s\t%s\n' "$(basename "$(dirname "$dir")")" "$dir"; fi
115
+ done | highest
116
+ }
117
+
118
+ find_bun() {
119
+ local recorded dir dirs data="${XDG_DATA_HOME:-$HOME/.local/share}"
120
+ recorded="$(read_record "$records/bun-path" || true)"
121
+ # A record naming something other than bun falls through to the search rather
122
+ # than short-circuiting it: an install running as the compiled binary records
123
+ # its own execPath, which is caret, not bun.
124
+ if [ -x "$recorded" ] && [ "$(basename "$recorded")" = bun ]; then
125
+ printf '%s' "$recorded"
126
+ return 0
127
+ fi
128
+ dirs=("$HOME/.bun/bin" /opt/homebrew/bin /usr/local/bin "$data/mise/shims"
129
+ "$(mise_bun_dir "$data")" "$HOME/.asdf/shims")
130
+ for dir in "${dirs[@]}"; do
131
+ if [ -n "$dir" ] && [ -x "$dir/bun" ]; then
132
+ printf '%s' "$dir/bun"
133
+ return 0
134
+ fi
135
+ done
136
+ printf 'caret-launcher: no bun (record %s: %s); searched %s\n' \
137
+ "$records/bun-path" "${recorded:-unset}" "${dirs[*]}" >&2
138
+ printf 'caret-launcher: install bun (https://bun.sh), then run: caret install\n' >&2
139
+ return 1
140
+ }
141
+
142
+ # launchd carries no per-status restart allowlist, so a launcher that cannot run
143
+ # stops its own agent rather than being respawned into the same failure. The unit
144
+ # file stays for a later `caret install`; systemd honours 78 on its own.
145
+ stop_agent() {
146
+ local service
147
+ [ "$(uname)" = Darwin ] || return 0
148
+ service="$(read_record "$records/service" || true)"
149
+ if [ -n "$service" ]; then launchctl bootout "gui/$(id -u)/$service" || true; fi
150
+ }
151
+
152
+ # caret cannot outlive its own install: with no caret left anywhere, the launcher
153
+ # tears its service down and deletes itself. Returns non-zero without touching
154
+ # anything when there is no `service` record — nothing here installed a
155
+ # supervisor, so this launcher cannot know what still names it, and deleting
156
+ # itself would leave whatever does respawning on ENOENT. Deleting only bin/ and
157
+ # the records leaves review state for a reinstall.
158
+ evict() {
159
+ local service unit
160
+ service="$(read_record "$records/service" || true)"
161
+ [ -n "$service" ] || return 1
162
+ if [ "$(uname)" = Darwin ]; then
163
+ launchctl bootout "gui/$(id -u)/$service" || true
164
+ unit="$HOME/Library/LaunchAgents/$service.plist"
165
+ else
166
+ systemctl --user disable --now "$service" || true
167
+ unit="${XDG_CONFIG_HOME:-$HOME/.config}/systemd/user/$service"
168
+ fi
169
+ printf 'caret-launcher: no caret installed anywhere; removing %s and %s.\n' \
170
+ "$unit" "$state/bin" >&2
171
+ rm -f "$unit"
172
+ rm -rf "${state:?}/bin" "${state:?}/launcher"
173
+ exit 0
174
+ }
175
+
176
+ # Only under a supervisor: a hand-run launcher keeps its diagnostics on the terminal.
177
+ if [ "${CARET_SUPERVISED:-}" = 1 ]; then open_daemon_log; fi
178
+
179
+ # Only a total miss re-probes. A populated version dir with no runnable bin/caret
180
+ # is far more likely a bad install than an extraction caught mid-flight, and that
181
+ # path is recoverable with `caret install`; an empty cache is the one whose branch
182
+ # deletes things, so it is the one worth waiting on.
183
+ root="$(resolve_root)"
184
+ for _ in 1 2; do
185
+ if [ -n "$root" ] || [ -n "$(candidate_dirs)" ]; then break; fi
186
+ sleep 5
187
+ root="$(resolve_root)"
188
+ done
189
+
190
+ if [ -z "$root" ]; then
191
+ found="$(candidate_dirs)"
192
+ if [ -z "$found" ]; then evict || true; fi
193
+ printf 'caret-launcher: no runnable bin/caret under any of:\n%s\n' \
194
+ "${found:-(no install directories at all)}" >&2
195
+ printf 'caret-launcher: run: caret install — to restore the service.\n' >&2
196
+ stop_agent
197
+ exit 78
198
+ fi
199
+
200
+ bun="$(find_bun || true)"
201
+ if [ -z "$bun" ]; then
202
+ stop_agent
203
+ exit 78
204
+ fi
205
+
206
+ # bun's directory and nothing else — no CARET_*, and in particular never
207
+ # BUN_FEATURE_FLAG_NO_ORPHANS.
208
+ PATH="$(dirname "$bun"):$PATH" exec "$root/bin/caret" "$@"
package/commands/demo.md CHANGED
@@ -1,14 +1,14 @@
1
1
  ---
2
- description: Demo caret by presenting a short fake plan via ExitPlanMode
2
+ description: Demo caret by presenting a short plan about this repo via ExitPlanMode
3
3
  ---
4
4
 
5
- Read the short demo-plan fixture at `${CLAUDE_PLUGIN_ROOT}/scripts/tasks/dev/demo-plan.md`
6
- and present its contents **verbatim** to the user by calling `ExitPlanMode` with that
7
- markdown as the `plan` argument. This is a small, self-contained plan meant only to show
8
- off the review flow — `mise run dev` seeds a different, larger fixture. Do **not** do any
9
- real work, research, or file edits — this is only to exercise the review flow. If the
10
- fixture can't be read, fall back to inventing a short, plausible-but-fake plan (a Context
11
- section, an Approach section, and a couple of Steps).
5
+ Call `EnterPlanMode` first: `ExitPlanMode` is refused outside plan mode. Then read
6
+ `${CLAUDE_PLUGIN_ROOT}/templates/demo.md` and follow its leading HTML comment exactly to
7
+ fill the slots. Write everything after that comment to the plan file your plan-mode
8
+ instructions name, then call `ExitPlanMode` with that same text as its `plan` argument —
9
+ caret reviews the argument, so a call without it reaches the review UI empty. Do **no**
10
+ other research and edit **no** file but the plan file — this is only to exercise the
11
+ review flow.
12
12
 
13
13
  This triggers caret's `PermissionRequest`/`ExitPlanMode` hook, which opens the plan in a
14
14
  local browser UI for inline review. When the user approves or requests changes there, the
@@ -17,5 +17,6 @@ decision flows back to you:
17
17
  - **Approved** → proceed (this is just a demo, so simply acknowledge that caret approved
18
18
  the plan and stop).
19
19
  - **Changes requested** → the feedback arrives as the denial reason. Revise the plan
20
- accordingly and present it again via `ExitPlanMode` (caret captures it as a new
21
- version).
20
+ accordingly, keeping it about the same size, update the plan file, and call
21
+ `ExitPlanMode` again with the revised text as its `plan` argument (caret captures it as
22
+ a new version).