@macintacos/caret 0.14.0 → 1.0.0

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 +48 -24
  3. package/bin/caret-launcher +208 -0
  4. package/commands/demo.md +11 -10
  5. package/dist/cli.js +49415 -27743
  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-BrP7X_fT.js} +1 -1
  14. package/ui/dist/assets/{angular-ts-BVnV1Yg5.js → angular-ts-CWSXPJ8_.js} +1 -1
  15. package/ui/dist/assets/{apl-BFyMAB9c.js → apl-DcDu9VfL.js} +1 -1
  16. package/ui/dist/assets/{astro-DAtaAFYq.js → astro-ZWhUFE02.js} +1 -1
  17. package/ui/dist/assets/{blade-BoZH76v_.js → blade-Bnfid1aX.js} +1 -1
  18. package/ui/dist/assets/{c-DyRawzd0.js → c-DGhlaWFa.js} +1 -1
  19. package/ui/dist/assets/{chapel-CAaCMdek.js → chapel-B80nA-Wv.js} +1 -1
  20. package/ui/dist/assets/{cobol-BjXiLsat.js → cobol-BxPmMgGq.js} +1 -1
  21. package/ui/dist/assets/{coffee-DvjcTlIE.js → coffee-qvFLvdPC.js} +1 -1
  22. package/ui/dist/assets/{cpp-sRP7rZGr.js → cpp-BGBu9Yb6.js} +1 -1
  23. package/ui/dist/assets/{crystal-CaRkbKnA.js → crystal-CV45eR73.js} +1 -1
  24. package/ui/dist/assets/{css-Igh3vvbt.js → css-CBPwQP3-.js} +1 -1
  25. package/ui/dist/assets/{dist-B5Dwvt_o.js → dist-1bUsa9So.js} +1 -1
  26. package/ui/dist/assets/dist-418qtW5O.js +2 -0
  27. package/ui/dist/assets/dist-BafAgSat.js +1 -0
  28. package/ui/dist/assets/{dist-CBxRdWpx.js → dist-BubrR0xG.js} +1 -1
  29. package/ui/dist/assets/{dist-DHAfbOzU.js → dist-CfuU_6ln.js} +1 -1
  30. package/ui/dist/assets/{dist-CF48ImGm.js → dist-Cj-JdNZd.js} +1 -1
  31. package/ui/dist/assets/{dist-CGl7bSLs.js → dist-CjIRqXSt.js} +1 -1
  32. package/ui/dist/assets/{dist-D4FUs8dC.js → dist-CpcSX-YP.js} +1 -1
  33. package/ui/dist/assets/{dist-C_lXyw3s.js → dist-D0Ats_4w.js} +1 -1
  34. package/ui/dist/assets/dist-DOswrDst.js +12 -0
  35. package/ui/dist/assets/{dist-CF4FIhxu.js → dist-DWzXdmaf.js} +1 -1
  36. package/ui/dist/assets/{dist-5Bv2mYWj.js → dist-DXn_f-MA.js} +1 -1
  37. package/ui/dist/assets/{dist-lE2sJQyW.js → dist-Dfm7Znx4.js} +1 -1
  38. package/ui/dist/assets/{dist-BkB3Dkvt.js → dist-Dj96YS5k.js} +1 -1
  39. package/ui/dist/assets/{dist-CZeI5UV9.js → dist-Dz8bokki.js} +1 -1
  40. package/ui/dist/assets/{dist-D_g9f9rz.js → dist-Lc2Es7Pl.js} +1 -1
  41. package/ui/dist/assets/{dist-Dt1D8SWK.js → dist-TxQM6vOw.js} +1 -1
  42. package/ui/dist/assets/{dist-CYIXdHF6.js → dist-cV3QBg-W.js} +1 -1
  43. package/ui/dist/assets/{edge-CvHTOXgI.js → edge-Co6ItTYI.js} +1 -1
  44. package/ui/dist/assets/{elixir-CtokNUaA.js → elixir-0YZEZ0gv.js} +1 -1
  45. package/ui/dist/assets/{elm-B34p-CC5.js → elm-BkRJOygl.js} +1 -1
  46. package/ui/dist/assets/{erb-FjaGN5LV.js → erb-BI2P49U3.js} +1 -1
  47. package/ui/dist/assets/{git-rebase-BFUCuD7O.js → git-rebase-Bq5iIXzt.js} +1 -1
  48. package/ui/dist/assets/{glimmer-js-CRs6sgzF.js → glimmer-js-BbOUbDIt.js} +1 -1
  49. package/ui/dist/assets/{glimmer-ts-Bkt0v-q2.js → glimmer-ts-CNnsk0yJ.js} +1 -1
  50. package/ui/dist/assets/{glsl-Ckh3d22M.js → glsl-BGnMm0dJ.js} +1 -1
  51. package/ui/dist/assets/{graphql-IqxoIj3x.js → graphql-7Z7p0nC9.js} +1 -1
  52. package/ui/dist/assets/{hack-DG1p8zpS.js → hack-DqHfN4B0.js} +1 -1
  53. package/ui/dist/assets/{haml-DA2jQcOV.js → haml-3nZzM1j8.js} +1 -1
  54. package/ui/dist/assets/{handlebars-CqT0epoo.js → handlebars-D76GLv5_.js} +1 -1
  55. package/ui/dist/assets/{html-CoTd-Axz.js → html-KRo6nO5U.js} +1 -1
  56. package/ui/dist/assets/{html-derivative-DDt5A1xL.js → html-derivative-45dsHZ7G.js} +1 -1
  57. package/ui/dist/assets/{http-TEp9u8BR.js → http-Cd6yAfd3.js} +1 -1
  58. package/ui/dist/assets/{hurl-OLD-GtJJ.js → hurl-DMRd-22O.js} +1 -1
  59. package/ui/dist/assets/index-CAl4rX7_.css +1 -0
  60. package/ui/dist/assets/{index-BucWCjoT.js → index-DqZ7qZxG.js} +367 -318
  61. package/ui/dist/assets/{java-DYZ_BnxQ.js → java-C5BRRLzP.js} +1 -1
  62. package/ui/dist/assets/{javascript-B4vvB79r.js → javascript-BEwOFxBm.js} +1 -1
  63. package/ui/dist/assets/{jinja-BTulBbvB.js → jinja-DC6SxQM2.js} +1 -1
  64. package/ui/dist/assets/{jison-CDVJmcGj.js → jison-BpM_G12u.js} +1 -1
  65. package/ui/dist/assets/{json-DahQZwqh.js → json-DhXVH33S.js} +1 -1
  66. package/ui/dist/assets/{jsx-Dzcw0XhS.js → jsx-QIa_kGnJ.js} +1 -1
  67. package/ui/dist/assets/{julia-BB_aSeOr.js → julia-BuS1ndKd.js} +1 -1
  68. package/ui/dist/assets/{just-C3IsNIar.js → just-Dms5nmY6.js} +1 -1
  69. package/ui/dist/assets/{latex-D7dYt9yW.js → latex-d0FSRalE.js} +1 -1
  70. package/ui/dist/assets/{liquid-CO9g2vFd.js → liquid-Bq_YWguE.js} +1 -1
  71. package/ui/dist/assets/{lua-D6OK3PTP.js → lua-CqusOKbI.js} +1 -1
  72. package/ui/dist/assets/{marko-YPWiYUQ_.js → marko-CfvdDxZz.js} +1 -1
  73. package/ui/dist/assets/{mdc-DoQ9fqeC.js → mdc-B_vZdcRe.js} +1 -1
  74. package/ui/dist/assets/{nginx-BuX_dG5x.js → nginx-YhyPrYEc.js} +1 -1
  75. package/ui/dist/assets/{nim-oEF2CEaX.js → nim-UGiA-q4U.js} +1 -1
  76. package/ui/dist/assets/{org-C1kwr3b8.js → org-BWLiCWoV.js} +1 -1
  77. package/ui/dist/assets/{perl-CzZcVIM9.js → perl-CXeAyAx6.js} +1 -1
  78. package/ui/dist/assets/{php-C_i1TWmy.js → php-C0Y5OILf.js} +1 -1
  79. package/ui/dist/assets/{pug-Oz2JZVGZ.js → pug-pavIHhZT.js} +1 -1
  80. package/ui/dist/assets/{qml-DWZFG7aa.js → qml-CbMVL_Hw.js} +1 -1
  81. package/ui/dist/assets/{r-CKwXeGZ_.js → r-VbESC49D.js} +1 -1
  82. package/ui/dist/assets/{razor-DT1mg2x6.js → razor-B4zmReq6.js} +1 -1
  83. package/ui/dist/assets/{regexp-C2K6ptTb.js → regexp-CvzL3gAC.js} +1 -1
  84. package/ui/dist/assets/{rst-C5gmF0mo.js → rst-B0YBcjx2.js} +1 -1
  85. package/ui/dist/assets/{ruby-CrEJdgPT.js → ruby-BFx1yg1l.js} +1 -1
  86. package/ui/dist/assets/{sas-D1TGFNd_.js → sas-Bdz0EWxy.js} +1 -1
  87. package/ui/dist/assets/{scss-DBO_Sp1C.js → scss-Ar5qG57q.js} +1 -1
  88. package/ui/dist/assets/{shellscript-QIWVo1P5.js → shellscript-BxZn-g_C.js} +1 -1
  89. package/ui/dist/assets/{shellsession-iBMCyJwd.js → shellsession-DdGjCUDJ.js} +1 -1
  90. package/ui/dist/assets/{soy-NHBvJjBh.js → soy-DNbG_b-4.js} +1 -1
  91. package/ui/dist/assets/{sql-Yf21hpnW.js → sql-BbO6oYzq.js} +1 -1
  92. package/ui/dist/assets/{stata-CluZWHbV.js → stata-CrT9A-Oi.js} +1 -1
  93. package/ui/dist/assets/{surrealql-Bu2dU1Qj.js → surrealql-_tFV7pCn.js} +1 -1
  94. package/ui/dist/assets/{svelte-Owb3qG7g.js → svelte-ZdvR-T2J.js} +1 -1
  95. package/ui/dist/assets/{templ-DScnsiaP.js → templ-CpOqX9Rk.js} +1 -1
  96. package/ui/dist/assets/{tex-DvCQ8DJI.js → tex-BnYgu1Tm.js} +1 -1
  97. package/ui/dist/assets/{ts-tags-Fh6nOEbM.js → ts-tags-CII1fKoY.js} +1 -1
  98. package/ui/dist/assets/{tsx-C_2AyD6m.js → tsx-CPIX6Zti.js} +1 -1
  99. package/ui/dist/assets/{twig-DjT0rOpv.js → twig-Cpr1_Fdx.js} +1 -1
  100. package/ui/dist/assets/{typescript-NDIZTg4J.js → typescript-CS1A4fih.js} +1 -1
  101. package/ui/dist/assets/{typst-DNJZJFvq.js → typst-BEc_7GFB.js} +1 -1
  102. package/ui/dist/assets/{vue-BTZXwXJ_.js → vue-DLe2p-nw.js} +1 -1
  103. package/ui/dist/assets/{vue-html-CBhLp-FO.js → vue-html-CBsH80UM.js} +1 -1
  104. package/ui/dist/assets/{vue-vine-KGJI1qvi.js → vue-vine-C6KcK89Z.js} +1 -1
  105. package/ui/dist/assets/{xml-CeV6vl7T.js → xml-CMmb8ycU.js} +1 -1
  106. package/ui/dist/assets/{xsl-CibkluSG.js → xsl-vCAAt3o3.js} +1 -1
  107. package/ui/dist/assets/{yaml-D89KUdIt.js → yaml-Be7JvL54.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.0",
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,10 +1,5 @@
1
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.
7
-
8
3
  caret is a Claude Code (and OpenCode) plugin that replaces the terminal plan-approval
9
4
  prompt with a local web UI. When your agent presents a plan, caret opens it in your
10
5
  browser so you can read it as rendered HTML, **annotate passages inline** (Google-Docs
@@ -18,16 +13,15 @@ Want to develop caret rather than use it? Start with [CONTRIBUTING.md](CONTRIBUT
18
13
 
19
14
  ![caret review UI with an inline annotation, stitched across four themes](doc/assets/caret-review-ui.png)
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
+ [Check out a short demo here!](https://youtu.be/zesv5AunUW4)
24
17
 
25
18
  ## Install
26
19
 
27
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 login service (launchd
24
+ > or systemd). 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
 
@@ -42,18 +36,45 @@ prebuilt artifacts, the `/caret:*` slash commands, and the [rumdl](https://rumdl
42
36
  plan formatter. Where it can't ask — off a terminal — it installs into every agent it
43
37
  detected.
44
38
 
39
+ Every install at a terminal also asks whether to keep caret's review UI running all the
40
+ time, and doesn't remember the answer. Keeping it running registers a service that starts
41
+ at login and serves the UI at `http://caret.localhost:42718`; the other answer is
42
+ [running caret yourself](#running-caret-yourself). Off a terminal it doesn't ask, and
43
+ leaves the service however it finds it. To turn the service off later, run `caret install`
44
+ and answer **I'll run it myself**, or use `--uninstall` — see
45
+ [turning caret off](doc/RUNNING.md#turning-caret-off) and
46
+ [the caret service](doc/RUNNING.md#the-caret-service).
47
+
45
48
  Two steps finish the job:
46
49
 
47
50
  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.
51
+ 2. **Run `/caret:demo`.** It presents a short demo plan that points at files in the repo
52
+ you run it from, so you can exercise the whole flow before a real one arrives.
53
+
54
+ | Flag | What it does |
55
+ | ------------- | ----------------------------------------------------- |
56
+ | `--dry-run` | Preview the run without changing anything. |
57
+ | `--refresh` | Update an existing install. |
58
+ | `--uninstall` | Remove caret from every agent, and from this machine. |
59
+
60
+ ### Running caret yourself
61
+
62
+ If you'd rather caret not start at login, answer **I'll run it myself** when
63
+ `caret install` asks. That removes the service if one is registered. Then, whenever you
64
+ want the review UI up, run:
65
+
66
+ ```sh
67
+ bunx --no-cache @macintacos/caret@latest serve
68
+ ```
69
+
70
+ It keeps the UI up at `http://caret.localhost:42718` until you press Ctrl+C. If caret
71
+ already started on demand, `serve` stops that copy and takes the port; if caret's service
72
+ still holds it, `serve` says so and exits. Without `serve`, caret still works: it starts
73
+ when your agent submits a plan, and stops about a minute after the last review is
74
+ resolved.
50
75
 
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. |
76
+ After updating caret, run `caret serve` again yourself: the first plan from the new
77
+ version stops the old one, and nothing brings it back.
57
78
 
58
79
  ### Updating and uninstalling
59
80
 
@@ -64,10 +85,12 @@ bunx --no-cache @macintacos/caret@latest install --refresh # update
64
85
  bunx --no-cache @macintacos/caret@latest install --uninstall # remove
65
86
  ```
66
87
 
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.
88
+ An update picks its agents exactly as a fresh install does — the chooser at a terminal,
89
+ detection otherwise — and at a terminal asks the review-UI question again; an uninstall
90
+ takes every agent. Where caret's service stays registered, `--refresh` also cycles it onto
91
+ the new version, and restarting each agent loads that agent's own updated copy. In
92
+ OpenCode, caret toasts you at startup when a newer release is out; a plain `install` at a
93
+ terminal runs its own check against npm and asks before taking it.
71
94
 
72
95
  caret's daemon runs the same check for itself, at most once a day. The call is
73
96
  unauthenticated and sends nothing about you — just a request to npm or GitHub, depending
@@ -100,9 +123,10 @@ prompt. There you:
100
123
  and re-present.
101
124
 
102
125
  > [!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
126
+ > You don't have to wait to be intercepted: caret gives both agents a plan-review tool
127
+ > they can call directly — `review_plan` in Claude Code (from the plugin's MCP server) and
128
+ > `caret_review_plan` in OpenCode — so a skill of your own can route its plan through the
129
+ > same review UI. The tool is for plans only; see
106
130
  > [`doc/ARCHITECTURE.md`](doc/ARCHITECTURE.md#calling-the-review-tool-from-your-own-skill).
107
131
 
108
132
  ## Configuration
@@ -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 prefs and 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).