@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.
- package/.claude-plugin/plugin.json +9 -2
- package/README.md +55 -69
- package/bin/caret-launcher +208 -0
- package/commands/demo.md +11 -10
- package/dist/cli.js +49112 -27353
- package/hooks/hooks.json +16 -0
- package/opencode/caret.plugin.ts +173 -277
- package/opencode/commands/demo.md +9 -20
- package/opencode/index.ts +1 -2
- package/opencode/review-bridge.ts +241 -0
- package/package.json +9 -6
- package/templates/demo.md +89 -0
- package/ui/dist/assets/{angular-html-C0XElF0f.js → angular-html-CNIx69MF.js} +1 -1
- package/ui/dist/assets/{angular-ts-BVnV1Yg5.js → angular-ts-B6ob15kD.js} +1 -1
- package/ui/dist/assets/{apl-BFyMAB9c.js → apl-BzQg5wKE.js} +1 -1
- package/ui/dist/assets/{astro-DAtaAFYq.js → astro-DQ6PiTON.js} +1 -1
- package/ui/dist/assets/{blade-BoZH76v_.js → blade-DdGMUzbN.js} +1 -1
- package/ui/dist/assets/{c-DyRawzd0.js → c-DhrAH2di.js} +1 -1
- package/ui/dist/assets/{chapel-CAaCMdek.js → chapel-BIJH6n90.js} +1 -1
- package/ui/dist/assets/{cobol-BjXiLsat.js → cobol-CR-dFJ1T.js} +1 -1
- package/ui/dist/assets/{coffee-DvjcTlIE.js → coffee-DWT27RO5.js} +1 -1
- package/ui/dist/assets/{cpp-sRP7rZGr.js → cpp-CuONTEvg.js} +1 -1
- package/ui/dist/assets/{crystal-CaRkbKnA.js → crystal-Bvc0l-OM.js} +1 -1
- package/ui/dist/assets/{css-Igh3vvbt.js → css-BYejQHVV.js} +1 -1
- package/ui/dist/assets/{dist-D4FUs8dC.js → dist-BbAsmjOe.js} +1 -1
- package/ui/dist/assets/{dist-lE2sJQyW.js → dist-BgZhLp84.js} +1 -1
- package/ui/dist/assets/{dist-BkB3Dkvt.js → dist-Bn_ExXCc.js} +1 -1
- package/ui/dist/assets/{dist-CBxRdWpx.js → dist-BubrR0xG.js} +1 -1
- package/ui/dist/assets/dist-C-FIXuZc.js +2 -0
- package/ui/dist/assets/{dist-B5Dwvt_o.js → dist-CCPLuvXB.js} +1 -1
- package/ui/dist/assets/{dist-CF4FIhxu.js → dist-COogZ9jg.js} +1 -1
- package/ui/dist/assets/{dist-DHAfbOzU.js → dist-CfuU_6ln.js} +1 -1
- package/ui/dist/assets/{dist-CGl7bSLs.js → dist-CjIRqXSt.js} +1 -1
- package/ui/dist/assets/{dist-CZeI5UV9.js → dist-Cuerw0_4.js} +1 -1
- package/ui/dist/assets/{dist-CYIXdHF6.js → dist-D-lJ6tIg.js} +1 -1
- package/ui/dist/assets/{dist-C_lXyw3s.js → dist-D0Ats_4w.js} +1 -1
- package/ui/dist/assets/dist-DOswrDst.js +12 -0
- package/ui/dist/assets/dist-DV7t-p4N.js +1 -0
- package/ui/dist/assets/{dist-5Bv2mYWj.js → dist-DXn_f-MA.js} +1 -1
- package/ui/dist/assets/{dist-D_g9f9rz.js → dist-Lc2Es7Pl.js} +1 -1
- package/ui/dist/assets/{dist-CF48ImGm.js → dist-RKZBj-sl.js} +1 -1
- package/ui/dist/assets/{dist-Dt1D8SWK.js → dist-TxQM6vOw.js} +1 -1
- package/ui/dist/assets/{edge-CvHTOXgI.js → edge-B_-8Eolr.js} +1 -1
- package/ui/dist/assets/{elixir-CtokNUaA.js → elixir-CAhwbI74.js} +1 -1
- package/ui/dist/assets/{elm-B34p-CC5.js → elm-yg-VSwEl.js} +1 -1
- package/ui/dist/assets/{erb-FjaGN5LV.js → erb-CIMOpWGO.js} +1 -1
- package/ui/dist/assets/{git-rebase-BFUCuD7O.js → git-rebase-DI22snkc.js} +1 -1
- package/ui/dist/assets/{glimmer-js-CRs6sgzF.js → glimmer-js-CZHZNAs-.js} +1 -1
- package/ui/dist/assets/{glimmer-ts-Bkt0v-q2.js → glimmer-ts-C2rq-JGq.js} +1 -1
- package/ui/dist/assets/{glsl-Ckh3d22M.js → glsl-ByXN6hIN.js} +1 -1
- package/ui/dist/assets/{graphql-IqxoIj3x.js → graphql-DiiJwLqR.js} +1 -1
- package/ui/dist/assets/{hack-DG1p8zpS.js → hack-hQSSm1D1.js} +1 -1
- package/ui/dist/assets/{haml-DA2jQcOV.js → haml-cf9hcOgw.js} +1 -1
- package/ui/dist/assets/{handlebars-CqT0epoo.js → handlebars-B3tg892p.js} +1 -1
- package/ui/dist/assets/{html-CoTd-Axz.js → html-CkIr2qlC.js} +1 -1
- package/ui/dist/assets/{html-derivative-DDt5A1xL.js → html-derivative-U1EOyk5J.js} +1 -1
- package/ui/dist/assets/{http-TEp9u8BR.js → http-ek0-hDT8.js} +1 -1
- package/ui/dist/assets/{hurl-OLD-GtJJ.js → hurl-DbN52Bpk.js} +1 -1
- package/ui/dist/assets/index-CAl4rX7_.css +1 -0
- package/ui/dist/assets/{index-BucWCjoT.js → index-uBaTjG1U.js} +367 -318
- package/ui/dist/assets/{java-DYZ_BnxQ.js → java-DXEu0FGs.js} +1 -1
- package/ui/dist/assets/{javascript-B4vvB79r.js → javascript-Bi95ibpN.js} +1 -1
- package/ui/dist/assets/{jinja-BTulBbvB.js → jinja-BcKnkqjY.js} +1 -1
- package/ui/dist/assets/{jison-CDVJmcGj.js → jison-CFaHQ730.js} +1 -1
- package/ui/dist/assets/{json-DahQZwqh.js → json--ulVurkL.js} +1 -1
- package/ui/dist/assets/{jsx-Dzcw0XhS.js → jsx-ZonywBBD.js} +1 -1
- package/ui/dist/assets/{julia-BB_aSeOr.js → julia-Bnt3bGqT.js} +1 -1
- package/ui/dist/assets/{just-C3IsNIar.js → just-CBpil8Tz.js} +1 -1
- package/ui/dist/assets/{latex-D7dYt9yW.js → latex-D-qwKUqq.js} +1 -1
- package/ui/dist/assets/{liquid-CO9g2vFd.js → liquid-Bu8cI_b1.js} +1 -1
- package/ui/dist/assets/{lua-D6OK3PTP.js → lua-DO_5Mymc.js} +1 -1
- package/ui/dist/assets/{marko-YPWiYUQ_.js → marko-DJfs6dcc.js} +1 -1
- package/ui/dist/assets/{mdc-DoQ9fqeC.js → mdc-CnkU1WJT.js} +1 -1
- package/ui/dist/assets/{nginx-BuX_dG5x.js → nginx-B5LxXEA3.js} +1 -1
- package/ui/dist/assets/{nim-oEF2CEaX.js → nim-DLTorTuD.js} +1 -1
- package/ui/dist/assets/{org-C1kwr3b8.js → org-DnNvsXCU.js} +1 -1
- package/ui/dist/assets/{perl-CzZcVIM9.js → perl-Q5Q2rA31.js} +1 -1
- package/ui/dist/assets/{php-C_i1TWmy.js → php-S65hjGBt.js} +1 -1
- package/ui/dist/assets/{pug-Oz2JZVGZ.js → pug-k-MB8PEo.js} +1 -1
- package/ui/dist/assets/{qml-DWZFG7aa.js → qml-Bf9cLRPv.js} +1 -1
- package/ui/dist/assets/{r-CKwXeGZ_.js → r-Cr3hDQkZ.js} +1 -1
- package/ui/dist/assets/{razor-DT1mg2x6.js → razor-BO5-wpmp.js} +1 -1
- package/ui/dist/assets/{regexp-C2K6ptTb.js → regexp-ffQ9gsih.js} +1 -1
- package/ui/dist/assets/{rst-C5gmF0mo.js → rst-B4AQC5xj.js} +1 -1
- package/ui/dist/assets/{ruby-CrEJdgPT.js → ruby-Dr8kfvLX.js} +1 -1
- package/ui/dist/assets/{sas-D1TGFNd_.js → sas-Kuo2pLeL.js} +1 -1
- package/ui/dist/assets/{scss-DBO_Sp1C.js → scss-Ci63BQ8y.js} +1 -1
- package/ui/dist/assets/{shellscript-QIWVo1P5.js → shellscript-DBh4gnVx.js} +1 -1
- package/ui/dist/assets/{shellsession-iBMCyJwd.js → shellsession-L5KFvHI3.js} +1 -1
- package/ui/dist/assets/{soy-NHBvJjBh.js → soy-D6F5ilyd.js} +1 -1
- package/ui/dist/assets/{sql-Yf21hpnW.js → sql-xwRblxjL.js} +1 -1
- package/ui/dist/assets/{stata-CluZWHbV.js → stata-l8sBIGPT.js} +1 -1
- package/ui/dist/assets/{surrealql-Bu2dU1Qj.js → surrealql-DL2sOSzB.js} +1 -1
- package/ui/dist/assets/{svelte-Owb3qG7g.js → svelte-Dp5d2Ja3.js} +1 -1
- package/ui/dist/assets/{templ-DScnsiaP.js → templ-B3LfgeSM.js} +1 -1
- package/ui/dist/assets/{tex-DvCQ8DJI.js → tex-CfLQjgEV.js} +1 -1
- package/ui/dist/assets/{ts-tags-Fh6nOEbM.js → ts-tags-BCfi6Eh_.js} +1 -1
- package/ui/dist/assets/{tsx-C_2AyD6m.js → tsx-BXhT7ita.js} +1 -1
- package/ui/dist/assets/{twig-DjT0rOpv.js → twig-t6tdMpRg.js} +1 -1
- package/ui/dist/assets/{typescript-NDIZTg4J.js → typescript-CFIlDlxn.js} +1 -1
- package/ui/dist/assets/{typst-DNJZJFvq.js → typst-CVQ9ief2.js} +1 -1
- package/ui/dist/assets/{vue-BTZXwXJ_.js → vue-D92U7PL6.js} +1 -1
- package/ui/dist/assets/{vue-html-CBhLp-FO.js → vue-html-B_-4d7cG.js} +1 -1
- package/ui/dist/assets/{vue-vine-KGJI1qvi.js → vue-vine-LaG8znAe.js} +1 -1
- package/ui/dist/assets/{xml-CeV6vl7T.js → xml-DgAE0jfV.js} +1 -1
- package/ui/dist/assets/{xsl-CibkluSG.js → xsl-DkD8aIzS.js} +1 -1
- package/ui/dist/assets/{yaml-D89KUdIt.js → yaml-CEG92fN8.js} +1 -1
- package/ui/dist/index.html +3 -3
- package/ui/dist/assets/dist-BdaKUV8X.js +0 -2
- package/ui/dist/assets/dist-CiBs4R_k.js +0 -12
- package/ui/dist/assets/dist-YE9up3VR.js +0 -1
- package/ui/dist/assets/index-Djrj2rps.css +0 -1
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "caret",
|
|
3
|
-
"version": "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
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
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
|
-
|
|
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
|
-
|
|
14
|
+
[Check out a short demo here!](https://youtu.be/zesv5AunUW4)
|
|
20
15
|
|
|
21
|
-
[
|
|
22
|
-
— comment on a line, send the plan back, approve the revision, and the agent picks up
|
|
23
|
-
where you left it.
|
|
16
|
+

|
|
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
|
|
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
|
-
|
|
39
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
68
|
-
|
|
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
|
|
79
|
-
`
|
|
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
|
-
```
|
|
82
|
-
|
|
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
|
|
93
|
-
|
|
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
|
-
>
|
|
104
|
-
>
|
|
105
|
-
>
|
|
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`
|
|
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
|
|
2
|
+
description: Demo caret by presenting a short plan about this repo via ExitPlanMode
|
|
3
3
|
---
|
|
4
4
|
|
|
5
|
-
|
|
6
|
-
and
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
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
|
|
21
|
-
|
|
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).
|