@macintacos/caret 1.8.1 → 1.9.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.
- package/.claude-plugin/plugin.json +1 -1
- package/README.md +34 -7
- package/bin/caret-launcher +15 -10
- package/{opencode → bridge}/review-bridge.ts +153 -12
- package/commands/doctor.md +4 -0
- package/dist/cli.js +23980 -23370
- package/opencode/caret.core.ts +26 -113
- package/opencode/caret.plugin.ts +11 -5
- package/opencode/caret.plugin.v2.ts +12 -4
- package/opencode/caret.tui.ts +6 -18
- package/opencode/commands/demo.md +1 -1
- package/opencode/commands/doctor.md +4 -0
- package/package.json +18 -3
- package/pi/caret.ts +145 -0
- package/pi/demo.ts +79 -0
- package/pi/doctor.ts +149 -0
- package/pi/follow-up.ts +19 -0
- package/pi/gate.ts +49 -0
- package/pi/notice.ts +127 -0
- package/pi/plan.ts +182 -0
- package/pi/render.ts +51 -0
- package/pi/state.ts +74 -0
- package/pi/tool.ts +150 -0
- package/ui/dist/assets/{angular-html-BHGQgfyp.js → angular-html-DTAmLsI1.js} +1 -1
- package/ui/dist/assets/{angular-ts-CjJOXiTk.js → angular-ts-CnmIqVSv.js} +1 -1
- package/ui/dist/assets/{apl-BKCXbKuk.js → apl-BRrI8o6u.js} +1 -1
- package/ui/dist/assets/{astro-CbIGh4kW.js → astro-ws1SFlqZ.js} +1 -1
- package/ui/dist/assets/{blade-Dlz3TW50.js → blade-BAfome-F.js} +1 -1
- package/ui/dist/assets/{c-CNaIUpZJ.js → c-Cb8iBkqY.js} +1 -1
- package/ui/dist/assets/{chapel-BSTn-HqM.js → chapel-CwNxsGmt.js} +1 -1
- package/ui/dist/assets/{cobol-Ch_1nBvr.js → cobol-C6senUsW.js} +1 -1
- package/ui/dist/assets/{coffee-DlKg6sYw.js → coffee-BNmOwO3K.js} +1 -1
- package/ui/dist/assets/{cpp-CPx3-f9p.js → cpp-BBfuAVc8.js} +1 -1
- package/ui/dist/assets/{crystal-Cgvix66v.js → crystal-BIenSyu_.js} +1 -1
- package/ui/dist/assets/{css-CwPT7Xr2.js → css-BM7X2qQt.js} +1 -1
- package/ui/dist/assets/{dist-DH3vs96X.js → dist-8Qj1RsyX.js} +1 -1
- package/ui/dist/assets/{dist-DFQEDf6Q.js → dist-BBj5r4JZ.js} +1 -1
- package/ui/dist/assets/{dist-D2BIUS34.js → dist-B_l6M6p7.js} +1 -1
- package/ui/dist/assets/{dist-BdIbTHZ2.js → dist-Bsjq6LzO.js} +1 -1
- package/ui/dist/assets/{dist-BqPfD9yL.js → dist-Bxb_HUlD.js} +1 -1
- package/ui/dist/assets/{dist-Dze-hH7D.js → dist-C7V4XWMN.js} +1 -1
- package/ui/dist/assets/{dist-Cq6qfECE.js → dist-Cq8hWpOn.js} +1 -1
- package/ui/dist/assets/{dist-C6pK9dBg.js → dist-fytLbOcF.js} +1 -1
- package/ui/dist/assets/{dist-B7Ila1Oa.js → dist-zbP7SV6c.js} +1 -1
- package/ui/dist/assets/{dist-nsVj3Kzw.js → dist-zezPBQmO.js} +1 -1
- package/ui/dist/assets/{edge-C6856TSk.js → edge-BlclzlRw.js} +1 -1
- package/ui/dist/assets/{elixir-nUsO5D1t.js → elixir-BYumuPVp.js} +1 -1
- package/ui/dist/assets/{elm-BWmeidnG.js → elm-DNBykypS.js} +1 -1
- package/ui/dist/assets/{erb-DgYxypnH.js → erb-Cg-GLw-3.js} +1 -1
- package/ui/dist/assets/{git-rebase-Bu77Zji8.js → git-rebase-DcwetDCn.js} +1 -1
- package/ui/dist/assets/{glimmer-js-C-Er2CH_.js → glimmer-js-DwUQB02R.js} +1 -1
- package/ui/dist/assets/{glimmer-ts-DHJp3ZV-.js → glimmer-ts-DNxDwzad.js} +1 -1
- package/ui/dist/assets/{glsl-CkN2-uXa.js → glsl-CTQvGLl-.js} +1 -1
- package/ui/dist/assets/{graphql-DM_zS2Al.js → graphql-6N6VWnCL.js} +1 -1
- package/ui/dist/assets/{hack-BjOgct5l.js → hack-BIWJ2t_-.js} +1 -1
- package/ui/dist/assets/{haml-CY4ICINw.js → haml-CTaug-6I.js} +1 -1
- package/ui/dist/assets/{handlebars-DLsdnsgP.js → handlebars-EciX6Xtm.js} +1 -1
- package/ui/dist/assets/{html-Cw-p-zYu.js → html-5ZVMwxqg.js} +1 -1
- package/ui/dist/assets/{html-derivative-CDmHYwVW.js → html-derivative-Bo7PACv6.js} +1 -1
- package/ui/dist/assets/{http-Bqbe5jqD.js → http-D14aglwt.js} +1 -1
- package/ui/dist/assets/{hurl-Ca-chcCT.js → hurl-DGa-Lc1_.js} +1 -1
- package/ui/dist/assets/index-BocyxbG2.css +1 -0
- package/ui/dist/assets/{index-DLUt2ehA.js → index-Do48mTUl.js} +226 -225
- package/ui/dist/assets/{java-0P1dqfZ8.js → java-BUpF6706.js} +1 -1
- package/ui/dist/assets/{javascript-VI2-fvZG.js → javascript-CU7AY0Mj.js} +1 -1
- package/ui/dist/assets/{jinja-Dz73I98G.js → jinja-OREkRLMJ.js} +1 -1
- package/ui/dist/assets/{jison-BtT1z3MS.js → jison-Zf3gmndI.js} +1 -1
- package/ui/dist/assets/{json-C4tTi3F5.js → json-C8E_SzMr.js} +1 -1
- package/ui/dist/assets/{jsx-tTJjMA8e.js → jsx-CHMjfgSc.js} +1 -1
- package/ui/dist/assets/{julia-rokLSeEC.js → julia-DnO66Wng.js} +1 -1
- package/ui/dist/assets/{just-qeFPHb0X.js → just-BUkZWNEk.js} +1 -1
- package/ui/dist/assets/{latex-Beecofx6.js → latex-eFDA2ndR.js} +1 -1
- package/ui/dist/assets/{liquid-DAA5lUdB.js → liquid-BqLuL6hU.js} +1 -1
- package/ui/dist/assets/{lua-BsloMLJI.js → lua-CKNxXI-W.js} +1 -1
- package/ui/dist/assets/{marko-BohJpFVB.js → marko-CM7Qyeyg.js} +1 -1
- package/ui/dist/assets/{mdc-DcoaHcos.js → mdc-DQnmyYku.js} +1 -1
- package/ui/dist/assets/{nginx-rrqwgIhx.js → nginx-B7PJAw6W.js} +1 -1
- package/ui/dist/assets/{nim-bmLsCyNQ.js → nim-CmtAHkVL.js} +1 -1
- package/ui/dist/assets/{org-DBqD3zir.js → org-DXNJ0F8G.js} +1 -1
- package/ui/dist/assets/{perl-DTcxBbd6.js → perl-DCIWxaEc.js} +1 -1
- package/ui/dist/assets/{php-CtncUr2-.js → php-Dc66Nx9S.js} +1 -1
- package/ui/dist/assets/{pug-CIvCXkQY.js → pug-C_S0MlW-.js} +1 -1
- package/ui/dist/assets/{qml-NvdQJP41.js → qml-VqOrS5Wl.js} +1 -1
- package/ui/dist/assets/{r-CTxQa3JJ.js → r-Dl8VhfSg.js} +1 -1
- package/ui/dist/assets/{razor-6oObkjRl.js → razor-BEpGMKEn.js} +1 -1
- package/ui/dist/assets/{regexp-BoEgdKzz.js → regexp-X9VkDePO.js} +1 -1
- package/ui/dist/assets/{rst-C1BRN5ux.js → rst-CQz_65m4.js} +1 -1
- package/ui/dist/assets/{ruby-ll_ilmOa.js → ruby-DcUs-Hjd.js} +1 -1
- package/ui/dist/assets/{sas-nmRD3ilr.js → sas-0Aya4BIJ.js} +1 -1
- package/ui/dist/assets/{scss-DaWVZ0iP.js → scss-BiYXJFG-.js} +1 -1
- package/ui/dist/assets/{shellscript-CT8Jobwl.js → shellscript-D_GiG2j2.js} +1 -1
- package/ui/dist/assets/{shellsession-CxpwtalA.js → shellsession-CJaG9Dka.js} +1 -1
- package/ui/dist/assets/{soy-DEBXlOYH.js → soy-qeuP0wdW.js} +1 -1
- package/ui/dist/assets/{sql-3mfonMZP.js → sql-DjtVrjTT.js} +1 -1
- package/ui/dist/assets/{stata-DaVf6BI8.js → stata-CTtxT0Lq.js} +1 -1
- package/ui/dist/assets/{surrealql-BTM-6O5Q.js → surrealql-CLiRfZ3-.js} +1 -1
- package/ui/dist/assets/{svelte-CDPuyZop.js → svelte-DojibMCX.js} +1 -1
- package/ui/dist/assets/{templ-CbDJ7JLK.js → templ-BccZaOLv.js} +1 -1
- package/ui/dist/assets/{tex-DsKYCwQw.js → tex-DvrCGTlw.js} +1 -1
- package/ui/dist/assets/{ts-tags-BYhIdk0p.js → ts-tags-Dv3BB8TA.js} +1 -1
- package/ui/dist/assets/{tsx-BjLM2BRB.js → tsx-DogZPQbY.js} +1 -1
- package/ui/dist/assets/{twig-DqinkzV-.js → twig-DY6_W6um.js} +1 -1
- package/ui/dist/assets/{typescript-DH1pT4ZK.js → typescript-DwP-qhUH.js} +1 -1
- package/ui/dist/assets/{typst-BBE0SKCu.js → typst-DW555l0v.js} +1 -1
- package/ui/dist/assets/{vue-DIIUy5X_.js → vue-OZGLIqo2.js} +1 -1
- package/ui/dist/assets/{vue-html-BLERoY9d.js → vue-html-CjmVpLjs.js} +1 -1
- package/ui/dist/assets/{vue-vine-CGIG_xts.js → vue-vine-Cye_YqnL.js} +1 -1
- package/ui/dist/assets/{xml-BqbRoduq.js → xml-eBWMgcbX.js} +1 -1
- package/ui/dist/assets/{xsl-DZMfS9NA.js → xsl-BdSZmEF6.js} +1 -1
- package/ui/dist/assets/{yaml-BM5LLWf8.js → yaml-CbCy6UZM.js} +1 -1
- package/ui/dist/index.html +2 -2
- package/ui/dist/assets/index-P_fbn8LU.css +0 -1
package/opencode/caret.core.ts
CHANGED
|
@@ -1,35 +1,39 @@
|
|
|
1
|
-
// caret's host-neutral OpenCode core: the
|
|
2
|
-
//
|
|
3
|
-
//
|
|
1
|
+
// caret's host-neutral OpenCode core: runPlanReview, the planning steer, the caret
|
|
2
|
+
// version and plans-dir resolution, the warm runner, the toast bodies and tool-call
|
|
3
|
+
// metadata keys, and the update check. It re-uses the bridge's tool name, text, binary
|
|
4
|
+
// resolution and version comparison.
|
|
4
5
|
// caret.plugin.ts (v1), caret.plugin.v2.ts and caret.tui.ts are thin host adapters over
|
|
5
|
-
// it. Its imports are node builtins and review-bridge.ts only, so loading it
|
|
6
|
-
// evaluates either host's SDK; test/structure/dependency-placement.test.ts holds it
|
|
6
|
+
// it. Its imports are node builtins and ../bridge/review-bridge.ts only, so loading it
|
|
7
|
+
// never evaluates either host's SDK; test/structure/dependency-placement.test.ts holds it
|
|
8
|
+
// there.
|
|
7
9
|
|
|
8
|
-
import {
|
|
9
|
-
import { mkdirSync, readFileSync, statSync, writeFileSync } from "node:fs";
|
|
10
|
+
import { mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
10
11
|
import { homedir } from "node:os";
|
|
11
|
-
import { dirname
|
|
12
|
+
import { dirname } from "node:path";
|
|
12
13
|
import { fileURLToPath } from "node:url";
|
|
13
14
|
|
|
14
15
|
import {
|
|
15
16
|
buildEnvelope,
|
|
16
17
|
type CaretDecision,
|
|
17
18
|
decisionText,
|
|
19
|
+
isNewer,
|
|
18
20
|
PLAN_TITLE_INSTRUCTION,
|
|
21
|
+
REVIEW_TOOL,
|
|
22
|
+
readRegularFile,
|
|
23
|
+
resolvePlanSource,
|
|
19
24
|
runReviewViaCaret,
|
|
20
25
|
type SpawnRunner,
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
}
|
|
26
|
+
warmCaret,
|
|
27
|
+
} from "../bridge/review-bridge.ts";
|
|
28
|
+
|
|
29
|
+
export {
|
|
30
|
+
isNewer,
|
|
31
|
+
PLAN_ARG_DESCRIPTION,
|
|
32
|
+
REVIEW_TOOL,
|
|
33
|
+
REVIEW_TOOL_DESCRIPTION,
|
|
34
|
+
resolveCaretBin,
|
|
35
|
+
resolvePlanSource,
|
|
36
|
+
} from "../bridge/review-bridge.ts";
|
|
33
37
|
|
|
34
38
|
/** The plugin's own caret version, for the update check, read from the package.json
|
|
35
39
|
* shipped beside this module. "unknown" when it is unreadable — deliberately UNPARSEABLE
|
|
@@ -48,9 +52,6 @@ export function resolveCaretVersion(opts: {
|
|
|
48
52
|
}
|
|
49
53
|
}
|
|
50
54
|
|
|
51
|
-
/** The plan-review tool the Plan agent calls. */
|
|
52
|
-
export const REVIEW_TOOL = "caret_review_plan";
|
|
53
|
-
|
|
54
55
|
/** OpenCode's built-in primary planning agent — the one agent caret steers toward
|
|
55
56
|
* the review tool and warms the daemon for. */
|
|
56
57
|
export const PLANNING_AGENTS = ["plan"] as const;
|
|
@@ -123,49 +124,6 @@ export function decisionToast(outcome: ReviewOutcome): ToastBody {
|
|
|
123
124
|
}
|
|
124
125
|
}
|
|
125
126
|
|
|
126
|
-
type PlanSource = { plan: string; planFilePath?: string };
|
|
127
|
-
|
|
128
|
-
/** The plan the review tool submits, from exactly one of its `plan` / `path` args (an
|
|
129
|
-
* empty string counts as absent). A `path` resolves against the session directory and
|
|
130
|
-
* must be a readable `.md` file — mirroring the core's `isPlanFile`
|
|
131
|
-
* (src/plan/canonical-file.ts), repeated here because this module cannot import src/.
|
|
132
|
-
* `readFile` returns the text, or undefined when absPath is not a readable regular
|
|
133
|
-
* file. */
|
|
134
|
-
export function resolvePlanSource(
|
|
135
|
-
args: { plan?: string; path?: string },
|
|
136
|
-
directory: string,
|
|
137
|
-
readFile: (absPath: string) => string | undefined,
|
|
138
|
-
): PlanSource | { error: string } {
|
|
139
|
-
const plan = args.plan || undefined;
|
|
140
|
-
const path = args.path || undefined;
|
|
141
|
-
const notExactlyOne = {
|
|
142
|
-
error: `caret: ${REVIEW_TOOL} takes exactly one of \`plan\` or \`path\`. Pass the plan file as \`path\`, or the plan inline as \`plan\`.`,
|
|
143
|
-
};
|
|
144
|
-
if (path === undefined) return plan === undefined ? notExactlyOne : { plan };
|
|
145
|
-
if (plan !== undefined) return notExactlyOne;
|
|
146
|
-
const planFilePath = resolve(directory, path);
|
|
147
|
-
if (!planFilePath.endsWith(".md")) {
|
|
148
|
-
return {
|
|
149
|
-
error: `caret: ${REVIEW_TOOL} needs a markdown (.md) file as \`path\`, not ${planFilePath}. Write the plan to a .md file, or pass it inline as \`plan\`.`,
|
|
150
|
-
};
|
|
151
|
-
}
|
|
152
|
-
const text = readFile(planFilePath);
|
|
153
|
-
if (text === undefined) {
|
|
154
|
-
return {
|
|
155
|
-
error: `caret: ${REVIEW_TOOL} could not read ${planFilePath} as a regular file. Write the plan there first, or pass it inline as \`plan\`.`,
|
|
156
|
-
};
|
|
157
|
-
}
|
|
158
|
-
return { plan: text, planFilePath };
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
function readRegularFile(absPath: string): string | undefined {
|
|
162
|
-
try {
|
|
163
|
-
return statSync(absPath).isFile() ? readFileSync(absPath, "utf-8") : undefined;
|
|
164
|
-
} catch {
|
|
165
|
-
return undefined;
|
|
166
|
-
}
|
|
167
|
-
}
|
|
168
|
-
|
|
169
127
|
// --- version-check toast (EXC-794) -----------------------------------------
|
|
170
128
|
|
|
171
129
|
/** caret's latest-release endpoint. Unauthenticated GitHub API — throttled to at
|
|
@@ -179,30 +137,6 @@ const UPDATE_TOAST_MS = 5_000;
|
|
|
179
137
|
* OpenCode start. */
|
|
180
138
|
const UPDATE_CHECK_INTERVAL_MS = 24 * 60 * 60_000;
|
|
181
139
|
|
|
182
|
-
/** Semver triple `[major, minor, patch]`, or null when `v` is not `X.Y.Z` (an
|
|
183
|
-
* optional leading `v` is stripped; trailing prerelease/build metadata ignored). */
|
|
184
|
-
function parseVersionTriple(v: string): [number, number, number] | null {
|
|
185
|
-
const m = v
|
|
186
|
-
.trim()
|
|
187
|
-
.replace(/^v/, "")
|
|
188
|
-
.match(/^(\d+)\.(\d+)\.(\d+)/);
|
|
189
|
-
return m ? [Number(m[1]), Number(m[2]), Number(m[3])] : null;
|
|
190
|
-
}
|
|
191
|
-
|
|
192
|
-
/** True when `latest` is a strictly higher semver than `current`. Inline (no semver
|
|
193
|
-
* dep) so the deployed plugin stays self-contained; an unparseable version on
|
|
194
|
-
* either side compares false, so the check never nags on something it can't read. */
|
|
195
|
-
export function isNewer(latest: string, current: string): boolean {
|
|
196
|
-
const a = parseVersionTriple(latest);
|
|
197
|
-
const b = parseVersionTriple(current);
|
|
198
|
-
if (!a || !b) return false;
|
|
199
|
-
const [a0, a1, a2] = a;
|
|
200
|
-
const [b0, b1, b2] = b;
|
|
201
|
-
if (a0 !== b0) return a0 > b0;
|
|
202
|
-
if (a1 !== b1) return a1 > b1;
|
|
203
|
-
return a2 > b2;
|
|
204
|
-
}
|
|
205
|
-
|
|
206
140
|
/** The version (v-stripped) and release-page URL from GitHub's /releases/latest
|
|
207
141
|
* JSON, or null when the shape is unusable. */
|
|
208
142
|
export function parseLatestRelease(json: unknown): { version: string; url: string } | null {
|
|
@@ -365,29 +299,8 @@ export function planningSteer(plansDir: string): string {
|
|
|
365
299
|
* unit-testable without spawning a process. */
|
|
366
300
|
export type WarmRunner = (bin: string) => void;
|
|
367
301
|
|
|
368
|
-
/** Production warm
|
|
369
|
-
|
|
370
|
-
* because this spawn is what stands the daemon up and the daemon picks its adapter
|
|
371
|
-
* from that env; the 'error' handler is mandatory because spawn emits 'error'
|
|
372
|
-
* ASYNCHRONOUSLY (ENOENT on a bad bin), where the hook's synchronous try/catch
|
|
373
|
-
* cannot see it and an unhandled event would take OpenCode's whole process down. */
|
|
374
|
-
export const nodeWarmRunner: WarmRunner = (bin) => {
|
|
375
|
-
const child = spawn(bin, ["prewarm"], {
|
|
376
|
-
stdio: "ignore",
|
|
377
|
-
detached: true,
|
|
378
|
-
env: { ...process.env, CARET_AGENT: "opencode" },
|
|
379
|
-
});
|
|
380
|
-
child.on("error", () => {});
|
|
381
|
-
child.unref();
|
|
382
|
-
};
|
|
383
|
-
|
|
384
|
-
// The review tool's model-facing text. Both hosts' registrations read these, so v1 and
|
|
385
|
-
// v2 stay word-for-word identical (plugin-v2.test.ts pins the refusal texts).
|
|
386
|
-
export const REVIEW_TOOL_DESCRIPTION =
|
|
387
|
-
"Submit the current plan to caret for human review in a local browser UI. For plans only: caret presents what it receives as a plan, so do not use it for other documents or questions. Pass exactly one of `path` (preferred: a markdown file you write once and revise with edits) or `plan` (the plan inline). Blocks until the user approves or requests changes. On a change request, follow the result's instructions and call this tool again. Do not implement the plan until a call returns an approval.";
|
|
388
|
-
|
|
389
|
-
export const PLAN_ARG_DESCRIPTION =
|
|
390
|
-
"The complete plan, as markdown, to present for human review. The inline alternative to `path`.";
|
|
302
|
+
/** Production warm, as the OpenCode adapter. */
|
|
303
|
+
export const nodeWarmRunner: WarmRunner = (bin) => warmCaret(bin, "opencode");
|
|
391
304
|
|
|
392
305
|
export function pathArgDescription(plansDir: string): string {
|
|
393
306
|
return `Preferred. A markdown (.md) file holding the complete plan, absolute or relative to the session directory. (OpenCode's plan agent writes its plan files in ${plansDir}/.)`;
|
package/opencode/caret.plugin.ts
CHANGED
|
@@ -20,9 +20,9 @@
|
|
|
20
20
|
// under v1's `plugin` config key or v2's `plugins` key (v2 reads both, legacy `plugin`
|
|
21
21
|
// first). This module is the `server` half of index.ts's dual default: index.ts imports
|
|
22
22
|
// it on v1's first `server` call, so a v2 load never evaluates it. Its imports are node
|
|
23
|
-
// builtins, caret.core.ts, review-bridge.ts, and @opencode-ai/plugin (resolved
|
|
24
|
-
// OpenCode at runtime). Which ctx/tool/config shapes are live-verified and which are
|
|
25
|
-
// doc/agents/opencode-integration.md § Verified vs. follow-up.
|
|
23
|
+
// builtins, caret.core.ts, ../bridge/review-bridge.ts, and @opencode-ai/plugin (resolved
|
|
24
|
+
// by OpenCode at runtime). Which ctx/tool/config shapes are live-verified and which are
|
|
25
|
+
// not: doc/agents/opencode-integration.md § Verified vs. follow-up.
|
|
26
26
|
|
|
27
27
|
import { readFileSync } from "node:fs";
|
|
28
28
|
import { homedir } from "node:os";
|
|
@@ -30,6 +30,7 @@ import { relative } from "node:path";
|
|
|
30
30
|
|
|
31
31
|
import { type Hooks, type Plugin, tool } from "@opencode-ai/plugin";
|
|
32
32
|
|
|
33
|
+
import { nodeSpawnRunner, type SpawnRunner } from "../bridge/review-bridge.ts";
|
|
33
34
|
import {
|
|
34
35
|
decisionToast,
|
|
35
36
|
isPlanningAgent,
|
|
@@ -50,7 +51,6 @@ import {
|
|
|
50
51
|
toastBestEffort,
|
|
51
52
|
type WarmRunner,
|
|
52
53
|
} from "./caret.core.ts";
|
|
53
|
-
import { nodeSpawnRunner, type SpawnRunner } from "./review-bridge.ts";
|
|
54
54
|
|
|
55
55
|
/** OpenCode's plugin client, structurally narrowed to the calls caret makes.
|
|
56
56
|
* A structural type (rather than importing the SDK client) keeps this robust
|
|
@@ -163,7 +163,13 @@ export function createCaretPlugin(
|
|
|
163
163
|
plansDir?: string;
|
|
164
164
|
} = {},
|
|
165
165
|
): Plugin {
|
|
166
|
-
const bin =
|
|
166
|
+
const bin =
|
|
167
|
+
opts.bin ??
|
|
168
|
+
resolveCaretBin({
|
|
169
|
+
env: process.env,
|
|
170
|
+
importMetaUrl: import.meta.url,
|
|
171
|
+
overrideVar: "CARET_OPENCODE_BIN",
|
|
172
|
+
});
|
|
167
173
|
const run = opts.run ?? nodeSpawnRunner;
|
|
168
174
|
const warm = opts.warm ?? nodeWarmRunner;
|
|
169
175
|
const plansDir =
|
|
@@ -8,6 +8,12 @@ import { readFileSync } from "node:fs";
|
|
|
8
8
|
import { homedir } from "node:os";
|
|
9
9
|
import { join } from "node:path";
|
|
10
10
|
|
|
11
|
+
import {
|
|
12
|
+
decisionText,
|
|
13
|
+
errorMessage,
|
|
14
|
+
nodeSpawnRunner,
|
|
15
|
+
type SpawnRunner,
|
|
16
|
+
} from "../bridge/review-bridge.ts";
|
|
11
17
|
import {
|
|
12
18
|
CARET_DECISION_KEY,
|
|
13
19
|
CARET_URL_KEY,
|
|
@@ -24,7 +30,6 @@ import {
|
|
|
24
30
|
type WarmRunner,
|
|
25
31
|
} from "./caret.core.ts";
|
|
26
32
|
import { editChecks, type Rule } from "./permission.ts";
|
|
27
|
-
import { decisionText, nodeSpawnRunner, type SpawnRunner } from "./review-bridge.ts";
|
|
28
33
|
|
|
29
34
|
/** Appended last, it outranks a config deny-all: v2 disables a tool only when the last
|
|
30
35
|
* rule naming it is a `*` deny. */
|
|
@@ -210,12 +215,11 @@ export function createCaretSetup(opts: {
|
|
|
210
215
|
return { content, metadata: { [CARET_DECISION_KEY]: decision.behavior } };
|
|
211
216
|
} catch (error) {
|
|
212
217
|
if (declined !== undefined && error === declined) throw error;
|
|
213
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
214
218
|
return {
|
|
215
219
|
content: decisionText(
|
|
216
220
|
{
|
|
217
221
|
behavior: "deny",
|
|
218
|
-
feedback: `caret: review failed to run (${
|
|
222
|
+
feedback: `caret: review failed to run (${errorMessage(error)}) — denying to fail safe.`,
|
|
219
223
|
},
|
|
220
224
|
REVIEW_TOOL,
|
|
221
225
|
),
|
|
@@ -284,7 +288,11 @@ export function createCaretSetup(opts: {
|
|
|
284
288
|
|
|
285
289
|
/** The v2 `setup` OpenCode loads, wired to the production runners. */
|
|
286
290
|
const setup = createCaretSetup({
|
|
287
|
-
bin: resolveCaretBin({
|
|
291
|
+
bin: resolveCaretBin({
|
|
292
|
+
env: process.env,
|
|
293
|
+
importMetaUrl: import.meta.url,
|
|
294
|
+
overrideVar: "CARET_OPENCODE_BIN",
|
|
295
|
+
}),
|
|
288
296
|
run: nodeSpawnRunner,
|
|
289
297
|
warm: nodeWarmRunner,
|
|
290
298
|
plansDir: resolvePlansDir({
|
package/opencode/caret.tui.ts
CHANGED
|
@@ -10,8 +10,7 @@
|
|
|
10
10
|
// toasts. `theme` and `renderer` are optional too: without them the pending row renders in
|
|
11
11
|
// OpenTUI's default colours, or opens on every mouse-up, selection or not.
|
|
12
12
|
|
|
13
|
-
import {
|
|
14
|
-
|
|
13
|
+
import { type DetachedSpawn, nodeDetachedSpawn } from "../bridge/review-bridge.ts";
|
|
15
14
|
import {
|
|
16
15
|
CARET_DECISION_KEY,
|
|
17
16
|
CARET_URL_KEY,
|
|
@@ -94,21 +93,6 @@ type CaretCommand = {
|
|
|
94
93
|
run: () => void;
|
|
95
94
|
};
|
|
96
95
|
|
|
97
|
-
/** Spawns `bin args` detached and unwaited; `onError` hears a child that fails to start
|
|
98
|
-
* or exits non-zero. */
|
|
99
|
-
export type DetachedSpawn = (bin: string, args: string[], onError: (err: Error) => void) => void;
|
|
100
|
-
|
|
101
|
-
export const nodeDetachedSpawn: DetachedSpawn = (bin, args, onError) => {
|
|
102
|
-
// spawn reports ENOENT asynchronously; unhandled, that 'error' takes OpenCode down.
|
|
103
|
-
const child = spawn(bin, args, { stdio: "ignore", detached: true });
|
|
104
|
-
child.on("error", onError);
|
|
105
|
-
// band-link exits 0 on its own failures, so non-zero means caret never ran.
|
|
106
|
-
child.on("exit", (code) => {
|
|
107
|
-
if (code) onError(new Error(`exited with status ${code}`));
|
|
108
|
-
});
|
|
109
|
-
child.unref();
|
|
110
|
-
};
|
|
111
|
-
|
|
112
96
|
/** The `@opentui/solid` reconciler calls the pending row takes. */
|
|
113
97
|
type OpenTuiReconciler = {
|
|
114
98
|
createElement: (tag: "box" | "text" | "span" | "b") => unknown;
|
|
@@ -336,7 +320,11 @@ function pendingRow(
|
|
|
336
320
|
const loadOpenTui = (): Promise<OpenTuiReconciler> => import("@opentui/solid");
|
|
337
321
|
const setup = createCaretTui({
|
|
338
322
|
checkUpdate: productionUpdateCheck,
|
|
339
|
-
bin: resolveCaretBin({
|
|
323
|
+
bin: resolveCaretBin({
|
|
324
|
+
env: process.env,
|
|
325
|
+
importMetaUrl: import.meta.url,
|
|
326
|
+
overrideVar: "CARET_OPENCODE_BIN",
|
|
327
|
+
}),
|
|
340
328
|
spawn: nodeDetachedSpawn,
|
|
341
329
|
loadOpenTui,
|
|
342
330
|
});
|
|
@@ -3,7 +3,7 @@ description: Demo caret by submitting a short plan about this repo to caret's re
|
|
|
3
3
|
agent: plan
|
|
4
4
|
---
|
|
5
5
|
|
|
6
|
-
The demo template is at the end of this
|
|
6
|
+
The demo template is at the end of this message. Follow its leading HTML comment exactly
|
|
7
7
|
to fill the slots, then call the `caret_review_plan` tool with everything after that
|
|
8
8
|
comment as the `plan` argument. Do **no** other research and edit **no** file — this is
|
|
9
9
|
only to exercise caret's plan-review flow.
|
|
@@ -44,7 +44,11 @@ one. What each id means:
|
|
|
44
44
|
| `daemon-lock` | the lock names a dead pid, or a port caret is not configured to bind | the check's remedy verbatim |
|
|
45
45
|
| `agent-install` | the agent the check names does not have caret enabled | `caret install` |
|
|
46
46
|
| `log-errors` | a live log wrote an NDJSON error record in the last 24h; the detail names which log and when it last erred | step 4 |
|
|
47
|
+
| `opencode-host` | an `opencode` on PATH is below its major's floor (1.3.4 for v1, 2.0.29 for v2), or caret sits in a key or config file that OpenCode doesn't load | the check's remedy verbatim |
|
|
47
48
|
| `opencode-caret-version` | OpenCode would load a caret behind the published one | the check's remedy verbatim |
|
|
49
|
+
| `pi-host` | `pi` can't run, is older than 1.5.0, or is the pre-rename `@mariozechner/pi-coding-agent` | the check's remedy verbatim |
|
|
50
|
+
| `pi-caret-entry` | pi lists caret more than once, filters out its extension, or lists it without its package installed | the check's remedy verbatim |
|
|
51
|
+
| `pi-caret-version` | pi would load a caret behind the published one | the check's remedy verbatim |
|
|
48
52
|
|
|
49
53
|
An `unknown` check is not a failure: it names its own `reason` — no network, an unreadable
|
|
50
54
|
config, or a report section that never collected, which also shows as an `error` line and
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@macintacos/caret",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.9.0",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"description": "A Claude Code plugin that replaces the terminal plan-approval prompt with a local web UI for inline review, annotation, and approval.",
|
|
@@ -9,6 +9,15 @@
|
|
|
9
9
|
".": "./opencode/index.ts",
|
|
10
10
|
"./tui": "./opencode/caret.tui.ts"
|
|
11
11
|
},
|
|
12
|
+
"pi": {
|
|
13
|
+
"extensions": [
|
|
14
|
+
"./pi/caret.ts"
|
|
15
|
+
],
|
|
16
|
+
"changelogPath": ""
|
|
17
|
+
},
|
|
18
|
+
"keywords": [
|
|
19
|
+
"pi-package"
|
|
20
|
+
],
|
|
12
21
|
"bin": {
|
|
13
22
|
"caret": "./bin/caret"
|
|
14
23
|
},
|
|
@@ -28,6 +37,8 @@
|
|
|
28
37
|
"hooks/",
|
|
29
38
|
"commands/",
|
|
30
39
|
"opencode/",
|
|
40
|
+
"pi/",
|
|
41
|
+
"bridge/",
|
|
31
42
|
"templates/",
|
|
32
43
|
"mod/hooks/band.tsx",
|
|
33
44
|
"mod/band.d.ts",
|
|
@@ -49,6 +60,9 @@
|
|
|
49
60
|
"@codemirror/state": "^6.6.0",
|
|
50
61
|
"@codemirror/view": "^6.43.0",
|
|
51
62
|
"@commander-js/extra-typings": "^15.0.0",
|
|
63
|
+
"@earendil-works/pi-ai": "^1.5.1",
|
|
64
|
+
"@earendil-works/pi-coding-agent": "^1.5.0",
|
|
65
|
+
"@earendil-works/pi-tui": "^1.5.1",
|
|
52
66
|
"@happy-dom/global-registrator": "^21.0.4",
|
|
53
67
|
"@internationalized/date": "^3.12.2",
|
|
54
68
|
"@laststance/tailwind-suggest-canonical-classes": "^0.1.1",
|
|
@@ -70,10 +84,10 @@
|
|
|
70
84
|
"happy-dom": "^21.0.4",
|
|
71
85
|
"jsonc-parser": "^3.3.1",
|
|
72
86
|
"listr2": "^11.0.0",
|
|
73
|
-
"marked": "^18.
|
|
87
|
+
"marked": "^18.1.3",
|
|
74
88
|
"pino": "^10.3.1",
|
|
75
89
|
"pino-pretty": "^13.1.3",
|
|
76
|
-
"semver": "^7.8.
|
|
90
|
+
"semver": "^7.8.6",
|
|
77
91
|
"shiki": "^4.4.3",
|
|
78
92
|
"smol-toml": "^1.8.0",
|
|
79
93
|
"svelte": "^5.56.9",
|
|
@@ -82,6 +96,7 @@
|
|
|
82
96
|
"tailwind-variants": "^3.2.2",
|
|
83
97
|
"tailwindcss": "^4.3.3",
|
|
84
98
|
"tw-animate-css": "^1.4.0",
|
|
99
|
+
"typebox": "^1.3.27",
|
|
85
100
|
"typescript": "^6.0.3",
|
|
86
101
|
"vite": "^8.2.1",
|
|
87
102
|
"xss": "^1.0.15",
|
package/pi/caret.ts
ADDED
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
// caret's pi extension. pi loads this file through jiti on Node, from `package.json`
|
|
2
|
+
// `pi.extensions`. It may import only node builtins, pi's host-provided packages, its own
|
|
3
|
+
// pi/ modules, and ../bridge/review-bridge.ts — and nothing it reaches may use bun's
|
|
4
|
+
// runtime API, since pi runs on Node.
|
|
5
|
+
|
|
6
|
+
import { writeSync } from "node:fs";
|
|
7
|
+
import { join } from "node:path";
|
|
8
|
+
import { fileURLToPath } from "node:url";
|
|
9
|
+
|
|
10
|
+
import { getAgentDir, VERSION } from "@earendil-works/pi-coding-agent";
|
|
11
|
+
|
|
12
|
+
import {
|
|
13
|
+
type DetachedSpawn,
|
|
14
|
+
detachedSpawnWith,
|
|
15
|
+
isNewer,
|
|
16
|
+
nodeSpawnRunner,
|
|
17
|
+
parseVersionTriple,
|
|
18
|
+
resolveCaretBin,
|
|
19
|
+
type SpawnRunner,
|
|
20
|
+
warmCaret,
|
|
21
|
+
} from "../bridge/review-bridge.ts";
|
|
22
|
+
import { DEMO_COMMAND, DEMO_TEMPLATE, type DemoApi, installDemo } from "./demo.ts";
|
|
23
|
+
import { type DoctorApi, installDoctor } from "./doctor.ts";
|
|
24
|
+
import { installNotice, type NoticeApi } from "./notice.ts";
|
|
25
|
+
import { installPlanning, type PlanningApi } from "./plan.ts";
|
|
26
|
+
import { reviewStore } from "./state.ts";
|
|
27
|
+
import { type PiTool, reviewTool } from "./tool.ts";
|
|
28
|
+
|
|
29
|
+
/** Keep equal to src/adapters/pi/host.ts's PI_FLOOR (test/pi/caret.test.ts pins it);
|
|
30
|
+
* pi/ can't import src/. */
|
|
31
|
+
export const PI_FLOOR = "1.5.0";
|
|
32
|
+
|
|
33
|
+
/** The slice of pi's session context caret calls; pinned by test/pi/sdk-conformance.ts. */
|
|
34
|
+
export type PiNotifyContext = {
|
|
35
|
+
ui: { notify(message: string, type?: "info" | "warning" | "error"): void };
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
/** The slice of pi's extension API caret calls; pinned by test/pi/sdk-conformance.ts. */
|
|
39
|
+
export type PiApi = PlanningApi &
|
|
40
|
+
NoticeApi &
|
|
41
|
+
DemoApi &
|
|
42
|
+
DoctorApi & {
|
|
43
|
+
registerTool(tool: PiTool): void;
|
|
44
|
+
on(event: "session_start", handler: (event: unknown, ctx: PiNotifyContext) => void): void;
|
|
45
|
+
on(event: "session_shutdown", handler: () => void): void;
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
export interface CaretHost {
|
|
49
|
+
version: string;
|
|
50
|
+
/** This copy's module URL: its identity against other installed copies, and the base
|
|
51
|
+
* `../bin/caret` and the demo template resolve against. */
|
|
52
|
+
entryUrl: string;
|
|
53
|
+
/** globalThis in production: survives `/reload`, which loads a fresh module instance. */
|
|
54
|
+
holder: Record<symbol, unknown>;
|
|
55
|
+
env: Record<string, string | undefined>;
|
|
56
|
+
run: SpawnRunner;
|
|
57
|
+
/** pi's agent dir; the plans folder sits under it. */
|
|
58
|
+
agentDir: string;
|
|
59
|
+
/** Spawns `caret prewarm` for `bin`; best-effort. */
|
|
60
|
+
warm: (bin: string) => void;
|
|
61
|
+
/** Starts `caret band-link` detached. */
|
|
62
|
+
spawn: DetachedSpawn;
|
|
63
|
+
/** Writes to the real stdout, for print mode's report. */
|
|
64
|
+
stdout: (text: string) => void;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const OWNER = Symbol.for("caret.pi.owner");
|
|
68
|
+
const NOTIFIED = Symbol.for("caret.pi.notified");
|
|
69
|
+
|
|
70
|
+
/** Whether `version` parses and is at least PI_FLOOR. */
|
|
71
|
+
export function meetsFloor(version: string): boolean {
|
|
72
|
+
return parseVersionTriple(version) !== null && !isNewer(PI_FLOOR, version);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/** One caret warning per process, whichever refusal comes first. */
|
|
76
|
+
function notifyOnce(pi: PiApi, holder: Record<symbol, unknown>, message: string): void {
|
|
77
|
+
pi.on("session_start", (_event, ctx) => {
|
|
78
|
+
if (holder[NOTIFIED]) return;
|
|
79
|
+
holder[NOTIFIED] = true;
|
|
80
|
+
ctx.ui.notify(message, "warning");
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/** Registers the review tool, the planning mode, the pending-plan notice, and the
|
|
85
|
+
* `/caret:*` commands unless pi is below PI_FLOOR or another installed copy already owns
|
|
86
|
+
* `Symbol.for("caret.pi.owner")`. The owner is keyed by `entryUrl`, which differs between
|
|
87
|
+
* installed copies and is stable across `/reload`. It is released on `session_shutdown`
|
|
88
|
+
* so a surviving copy takes over after the owner is removed. Either refusal warns once
|
|
89
|
+
* per process. */
|
|
90
|
+
export function installCaret(pi: PiApi, host: CaretHost): void {
|
|
91
|
+
const { version, entryUrl, holder, env, run } = host;
|
|
92
|
+
if (!meetsFloor(version)) {
|
|
93
|
+
notifyOnce(
|
|
94
|
+
pi,
|
|
95
|
+
holder,
|
|
96
|
+
`caret needs pi ${PI_FLOOR} or later (this is pi ${version}), so its review tool is off. Update pi to use caret.`,
|
|
97
|
+
);
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
100
|
+
holder[OWNER] ??= entryUrl;
|
|
101
|
+
const owner = holder[OWNER];
|
|
102
|
+
if (owner !== entryUrl) {
|
|
103
|
+
notifyOnce(
|
|
104
|
+
pi,
|
|
105
|
+
holder,
|
|
106
|
+
`caret is loaded twice: the copy at ${fileURLToPath(String(owner))} runs and the copy at ${fileURLToPath(entryUrl)} stays off. Remove one, then run /reload.`,
|
|
107
|
+
);
|
|
108
|
+
return;
|
|
109
|
+
}
|
|
110
|
+
pi.on("session_shutdown", () => {
|
|
111
|
+
if (holder[OWNER] === entryUrl) delete holder[OWNER];
|
|
112
|
+
});
|
|
113
|
+
const bin = resolveCaretBin({ env, importMetaUrl: entryUrl, overrideVar: "CARET_PI_BIN" });
|
|
114
|
+
const store = reviewStore(holder);
|
|
115
|
+
pi.registerTool(reviewTool({ bin, run, store }));
|
|
116
|
+
installPlanning(pi, {
|
|
117
|
+
store,
|
|
118
|
+
plansDir: join(host.agentDir, "plans"),
|
|
119
|
+
warm: () => host.warm(bin),
|
|
120
|
+
});
|
|
121
|
+
installNotice(pi, { store, bin, spawn: host.spawn });
|
|
122
|
+
installDemo(pi, {
|
|
123
|
+
commandPath: fileURLToPath(new URL(DEMO_COMMAND, entryUrl)),
|
|
124
|
+
templatePath: fileURLToPath(new URL(DEMO_TEMPLATE, entryUrl)),
|
|
125
|
+
isPlanning: (id) => store.get(id).planning,
|
|
126
|
+
});
|
|
127
|
+
installDoctor(pi, { bin, run, env, stdout: host.stdout });
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
export default function caret(pi: PiApi): void {
|
|
131
|
+
installCaret(pi, {
|
|
132
|
+
version: VERSION,
|
|
133
|
+
entryUrl: import.meta.url,
|
|
134
|
+
holder: globalThis as unknown as Record<symbol, unknown>,
|
|
135
|
+
env: process.env,
|
|
136
|
+
run: nodeSpawnRunner,
|
|
137
|
+
agentDir: getAgentDir(),
|
|
138
|
+
warm: (bin) => warmCaret(bin, "pi"),
|
|
139
|
+
spawn: detachedSpawnWith({ CARET_AGENT: "pi" }),
|
|
140
|
+
// ponytail: fd 1, not process.stdout, since print mode reroutes process.stdout.write
|
|
141
|
+
// to stderr. A non-blocking stdout can throw EAGAIN on a report past the pipe buffer;
|
|
142
|
+
// retry as pi's own raw writer does if reports grow.
|
|
143
|
+
stdout: (text) => writeSync(1, text),
|
|
144
|
+
});
|
|
145
|
+
}
|
package/pi/demo.ts
ADDED
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
// `/caret:demo` for pi: sends the agent OpenCode's `/caret:demo` prompt with caret's demo
|
|
2
|
+
// template in place of its marker, both read from beside the package on every run, so a
|
|
3
|
+
// new user sees a review end to end.
|
|
4
|
+
|
|
5
|
+
import { readFile } from "node:fs/promises";
|
|
6
|
+
|
|
7
|
+
import { errorMessage } from "../bridge/review-bridge.ts";
|
|
8
|
+
import { sendAndWait } from "./follow-up.ts";
|
|
9
|
+
|
|
10
|
+
/** The demo template and OpenCode's `/caret:demo` command, relative to the extension
|
|
11
|
+
* entry `pi/caret.ts`. Both hosts submit through the same review tool, so the command's
|
|
12
|
+
* body is pi's prompt too. */
|
|
13
|
+
export const DEMO_TEMPLATE = "../templates/demo.md";
|
|
14
|
+
export const DEMO_COMMAND = "../opencode/commands/demo.md";
|
|
15
|
+
|
|
16
|
+
const TEMPLATE_MARKER = "__CARET_DEMO_TEMPLATE__";
|
|
17
|
+
const FRONTMATTER = /^---\n[\s\S]*?\n---\n+/;
|
|
18
|
+
|
|
19
|
+
/** `command`'s body with `template` taken literally in place of its marker. */
|
|
20
|
+
const demoPrompt = (command: string, template: string): string =>
|
|
21
|
+
command.replace(FRONTMATTER, "").replace(TEMPLATE_MARKER, () => template);
|
|
22
|
+
|
|
23
|
+
export type DemoCommandContext = {
|
|
24
|
+
ui: { notify(message: string, type?: "info" | "warning" | "error"): void };
|
|
25
|
+
waitForIdle(): Promise<void>;
|
|
26
|
+
sessionManager: { getSessionId(): string };
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
/** The slice of pi's extension API `/caret:demo` calls; pinned by
|
|
30
|
+
* test/pi/sdk-conformance.ts. */
|
|
31
|
+
export type DemoApi = {
|
|
32
|
+
registerCommand(
|
|
33
|
+
name: string,
|
|
34
|
+
options: {
|
|
35
|
+
description: string;
|
|
36
|
+
handler: (args: string, ctx: DemoCommandContext) => Promise<void>;
|
|
37
|
+
},
|
|
38
|
+
): void;
|
|
39
|
+
sendUserMessage(content: string, options: { deliverAs: "followUp" }): void;
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
/** Registers `/caret:demo`, which reads the command at `commandPath` and the template at
|
|
43
|
+
* `templatePath` on every run and refuses while the session is planning, where approving
|
|
44
|
+
* the demo could end planning for the real task. */
|
|
45
|
+
export function installDemo(
|
|
46
|
+
pi: DemoApi,
|
|
47
|
+
deps: {
|
|
48
|
+
commandPath: string;
|
|
49
|
+
templatePath: string;
|
|
50
|
+
isPlanning: (sessionId: string) => boolean;
|
|
51
|
+
},
|
|
52
|
+
): void {
|
|
53
|
+
const { commandPath, templatePath, isPlanning } = deps;
|
|
54
|
+
pi.registerCommand("caret:demo", {
|
|
55
|
+
description: "Demo caret: review a short plan about this repo",
|
|
56
|
+
handler: async (_args, ctx) => {
|
|
57
|
+
if (isPlanning(ctx.sessionManager.getSessionId())) {
|
|
58
|
+
ctx.ui.notify(
|
|
59
|
+
"caret's demo is off while planning; run /caret:plan to leave planning first.",
|
|
60
|
+
"warning",
|
|
61
|
+
);
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
let prompt: string;
|
|
65
|
+
try {
|
|
66
|
+
const [command, template] = await Promise.all([
|
|
67
|
+
readFile(commandPath, "utf-8"),
|
|
68
|
+
readFile(templatePath, "utf-8"),
|
|
69
|
+
]);
|
|
70
|
+
prompt = demoPrompt(command, template);
|
|
71
|
+
} catch (err) {
|
|
72
|
+
// The error names the file that failed.
|
|
73
|
+
ctx.ui.notify(`caret couldn't read its demo prompt: ${errorMessage(err)}`, "error");
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
await sendAndWait(pi, ctx, prompt);
|
|
77
|
+
},
|
|
78
|
+
});
|
|
79
|
+
}
|