@macintacos/caret 0.1.1 → 0.2.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 (99) hide show
  1. package/.claude-plugin/plugin.json +1 -1
  2. package/commands/demo.md +5 -4
  3. package/dist/cli.js +354 -53
  4. package/hooks/hooks.json +10 -0
  5. package/opencode/caret.plugin.ts +138 -20
  6. package/package.json +1 -1
  7. package/ui/dist/assets/{angular-html-Cg_qO4OQ.js → angular-html-Qbpn1MTX.js} +1 -1
  8. package/ui/dist/assets/{angular-ts-BmV-Wd4K.js → angular-ts-DOJ1C-2P.js} +1 -1
  9. package/ui/dist/assets/{apl-DDDJQLEy.js → apl-Y5-ddbi_.js} +1 -1
  10. package/ui/dist/assets/{astro-Cq94xJ2F.js → astro-CT31GiG6.js} +1 -1
  11. package/ui/dist/assets/{blade-DZCNRrZB.js → blade-DstC0Y7u.js} +1 -1
  12. package/ui/dist/assets/{c-CwNkiPnO.js → c-1h9wmDaJ.js} +1 -1
  13. package/ui/dist/assets/{cobol-CgSNuNV-.js → cobol-BJsqFKRW.js} +1 -1
  14. package/ui/dist/assets/{coffee-DWlIdtuG.js → coffee-lAZsrYrG.js} +1 -1
  15. package/ui/dist/assets/{cpp-CmiYyU8y.js → cpp-BwPvHcji.js} +1 -1
  16. package/ui/dist/assets/{crystal-BiyOLiea.js → crystal-C9nhMRLM.js} +1 -1
  17. package/ui/dist/assets/{css-DGiwxBdC.js → css-zV5baxCq.js} +1 -1
  18. package/ui/dist/assets/{dist-B-SHQUSo.js → dist-B2afH7tP.js} +1 -1
  19. package/ui/dist/assets/{dist-CCQGXHXf.js → dist-BL3PTdx3.js} +1 -1
  20. package/ui/dist/assets/{dist-B3mK9QMK.js → dist-Ba7qEsX8.js} +1 -1
  21. package/ui/dist/assets/{dist-C9DszYV1.js → dist-BcfPYvB6.js} +1 -1
  22. package/ui/dist/assets/{dist-24WuufPm.js → dist-C94yZ6__.js} +1 -1
  23. package/ui/dist/assets/{dist-C1Qk3ApG.js → dist-CBdstVS8.js} +1 -1
  24. package/ui/dist/assets/{dist-Cr9g-IkY.js → dist-CWdEcyiK.js} +1 -1
  25. package/ui/dist/assets/{dist-CbS4AEdX.js → dist-CnTQMJTD.js} +1 -1
  26. package/ui/dist/assets/{dist-CfztD2mm.js → dist-Cuw-4MzZ.js} +1 -1
  27. package/ui/dist/assets/{dist-CNR9H9hI.js → dist-D1Xb0cdg.js} +1 -1
  28. package/ui/dist/assets/{dist-C-hQPg8K.js → dist-D5aJA_uS.js} +1 -1
  29. package/ui/dist/assets/{dist-D_I8tzqw.js → dist-DGObAN16.js} +1 -1
  30. package/ui/dist/assets/{dist-BXtMxuI7.js → dist-DTwEdaPW.js} +1 -1
  31. package/ui/dist/assets/{dist-BViEvwEG.js → dist-EeYINWCK.js} +1 -1
  32. package/ui/dist/assets/{dist-CKVYvLDM.js → dist-ZPJnos5t.js} +1 -1
  33. package/ui/dist/assets/{dist-BkSlyoLl.js → dist-ap3lqXWD.js} +1 -1
  34. package/ui/dist/assets/{dist-C_IYb7Qh.js → dist-eJbkdDpi.js} +1 -1
  35. package/ui/dist/assets/{edge-CZXzS9pm.js → edge-DtPr1ved.js} +1 -1
  36. package/ui/dist/assets/{elixir-BTnUmaBf.js → elixir-DxdQpZmO.js} +1 -1
  37. package/ui/dist/assets/{elm-Bq5po3Zd.js → elm-PyFaNcFd.js} +1 -1
  38. package/ui/dist/assets/{erb-BOVmqWJf.js → erb-4d0ztAHA.js} +1 -1
  39. package/ui/dist/assets/{git-rebase-DLQM-CxK.js → git-rebase-CF959Xij.js} +1 -1
  40. package/ui/dist/assets/{glimmer-js-DzMlcKY7.js → glimmer-js-DOoPHQhz.js} +1 -1
  41. package/ui/dist/assets/{glimmer-ts-Dz691XKF.js → glimmer-ts-B-wO1BbM.js} +1 -1
  42. package/ui/dist/assets/{glsl-CMQZ1AkF.js → glsl-DhUIrRGp.js} +1 -1
  43. package/ui/dist/assets/{graphql-DBKIWtF7.js → graphql-DHm0YGdI.js} +1 -1
  44. package/ui/dist/assets/{hack-DZbb_fkF.js → hack-KmeknXZp.js} +1 -1
  45. package/ui/dist/assets/{haml-CSjdx5hj.js → haml-CqH8UVt6.js} +1 -1
  46. package/ui/dist/assets/{handlebars-Mu9SxQrl.js → handlebars-xVW6rqZc.js} +1 -1
  47. package/ui/dist/assets/{html-D4wtNI0H.js → html-Dm0ku22r.js} +1 -1
  48. package/ui/dist/assets/{html-derivative-DHrkr74L.js → html-derivative-ByxJ1rMF.js} +1 -1
  49. package/ui/dist/assets/{http-DtBk1Y64.js → http--f7CCugj.js} +1 -1
  50. package/ui/dist/assets/{hurl-1qu6SWMo.js → hurl-hbsn4uwO.js} +1 -1
  51. package/ui/dist/assets/index-2kPU99Wr.css +1 -0
  52. package/ui/dist/assets/{index-Dqm3ZFVQ.js → index-CIXjaWlx.js} +285 -95
  53. package/ui/dist/assets/{java-CLLSyNOn.js → java-DIuHCmu6.js} +1 -1
  54. package/ui/dist/assets/{javascript-B-_kYMSx.js → javascript-gLvwRdLL.js} +1 -1
  55. package/ui/dist/assets/{jinja-BtHF51nI.js → jinja-CllNBGTW.js} +1 -1
  56. package/ui/dist/assets/{jison-C93_a--j.js → jison--nGCTsNX.js} +1 -1
  57. package/ui/dist/assets/{json-C4FWYbGR.js → json-C56ryB74.js} +1 -1
  58. package/ui/dist/assets/{jsx-BdkuTVvH.js → jsx-4Pk1mTn6.js} +1 -1
  59. package/ui/dist/assets/{julia-BmboAp4o.js → julia-C0bepdRl.js} +1 -1
  60. package/ui/dist/assets/{just-E1Wftakf.js → just-DZULmPRj.js} +1 -1
  61. package/ui/dist/assets/{latex-DZFvvGDu.js → latex-D16Jgu4V.js} +1 -1
  62. package/ui/dist/assets/{liquid-BUTRgRVi.js → liquid-C8coUMRS.js} +1 -1
  63. package/ui/dist/assets/{lua-Dl1ciOfH.js → lua-Cf5MxbTu.js} +1 -1
  64. package/ui/dist/assets/{marko-Bh-A3HQZ.js → marko-Cb70KBLA.js} +1 -1
  65. package/ui/dist/assets/{mdc-C2XCGsSV.js → mdc-C7MhKLIm.js} +1 -1
  66. package/ui/dist/assets/{nginx-igqyMv_F.js → nginx-BXtCcpzS.js} +1 -1
  67. package/ui/dist/assets/{nim-DelsCcX1.js → nim-DGCttsC9.js} +1 -1
  68. package/ui/dist/assets/{perl-Cgh7mkol.js → perl-QjSbxj8-.js} +1 -1
  69. package/ui/dist/assets/{php-DXn-4Rf4.js → php-C3F5od73.js} +1 -1
  70. package/ui/dist/assets/{pug-BNWZhj7f.js → pug-C5_Ubyev.js} +1 -1
  71. package/ui/dist/assets/{qml-CHLXijrJ.js → qml-ClOBZo_4.js} +1 -1
  72. package/ui/dist/assets/{r-g949Oasl.js → r-CRs34ME2.js} +1 -1
  73. package/ui/dist/assets/{razor-BUv8GTQg.js → razor-Da5e4iwK.js} +1 -1
  74. package/ui/dist/assets/{regexp-Dxi3LqhO.js → regexp-CIgnY5KL.js} +1 -1
  75. package/ui/dist/assets/{rst-CMQNxthU.js → rst-B8IBFGFC.js} +1 -1
  76. package/ui/dist/assets/{ruby-CDBQomnV.js → ruby-DCWmsWCN.js} +1 -1
  77. package/ui/dist/assets/{sas-uuqXrFx7.js → sas-C6G9MYN0.js} +1 -1
  78. package/ui/dist/assets/{scss-CniJSQ3f.js → scss-CNd_k2F5.js} +1 -1
  79. package/ui/dist/assets/{shellscript-CuBMk1iS.js → shellscript-CwXf9FTq.js} +1 -1
  80. package/ui/dist/assets/{shellsession-BzDlSDO2.js → shellsession-BJzcxmip.js} +1 -1
  81. package/ui/dist/assets/{soy-e0u5_VID.js → soy-KzIeqqPW.js} +1 -1
  82. package/ui/dist/assets/{sql-Cbg060Y9.js → sql-DBDZ7dWg.js} +1 -1
  83. package/ui/dist/assets/{stata-n_N_Lnpt.js → stata-SsEZ25vz.js} +1 -1
  84. package/ui/dist/assets/{surrealql-Sa2566xv.js → surrealql-C7tSrOv6.js} +1 -1
  85. package/ui/dist/assets/{svelte-B0zgpD2X.js → svelte-BHETRygr.js} +1 -1
  86. package/ui/dist/assets/{templ-BMfZts2f.js → templ-mCQ9V1Fb.js} +1 -1
  87. package/ui/dist/assets/{tex-7PyiPoxe.js → tex-DgKGHlQ1.js} +1 -1
  88. package/ui/dist/assets/{ts-tags-nHnZDDzD.js → ts-tags-D-31aQRg.js} +1 -1
  89. package/ui/dist/assets/{tsx-BgvqLYni.js → tsx-CKRZAYYe.js} +1 -1
  90. package/ui/dist/assets/{twig-BqJ1gUqf.js → twig-COT1S6WR.js} +1 -1
  91. package/ui/dist/assets/{typescript-C2nbCIiA.js → typescript-IBoewa4A.js} +1 -1
  92. package/ui/dist/assets/{vue-B_l4_yyQ.js → vue-44M0YEZN.js} +1 -1
  93. package/ui/dist/assets/{vue-html-qalJD3cY.js → vue-html-C1gtv8Ck.js} +1 -1
  94. package/ui/dist/assets/{vue-vine-Cle_s8sp.js → vue-vine-DEW0Tzyn.js} +1 -1
  95. package/ui/dist/assets/{xml-CnKUcNAY.js → xml-CSnYabNH.js} +1 -1
  96. package/ui/dist/assets/{xsl-D5cPGaiC.js → xsl-DrkZvWNe.js} +1 -1
  97. package/ui/dist/assets/{yaml-BcZKwY9y.js → yaml-D6QWa51n.js} +1 -1
  98. package/ui/dist/index.html +2 -2
  99. package/ui/dist/assets/index-DT45eblI.css +0 -1
package/dist/cli.js CHANGED
@@ -4438,9 +4438,12 @@ function setModeFor(id) {
4438
4438
  }
4439
4439
 
4440
4440
  // src/adapters/claude/feedback.ts
4441
- function toHookOutput(input) {
4441
+ function toHookOutput(input, updatedInput) {
4442
4442
  if (input.behavior === "allow") {
4443
4443
  const decision = { behavior: "allow" };
4444
+ if (updatedInput) {
4445
+ decision.updatedInput = updatedInput;
4446
+ }
4444
4447
  const mode = setModeFor(input.acceptMode);
4445
4448
  if (mode) {
4446
4449
  decision.updatedPermissions = [{ type: "setMode", mode, destination: "session" }];
@@ -4544,6 +4547,16 @@ function readHookInUserSettings(path) {
4544
4547
  }
4545
4548
 
4546
4549
  // src/adapters/claude/index.ts
4550
+ function toolInputEcho(input) {
4551
+ if (!input)
4552
+ return;
4553
+ const echo = {};
4554
+ if (input.plan !== undefined)
4555
+ echo.plan = input.plan;
4556
+ if (input.planFilePath !== undefined)
4557
+ echo.planFilePath = input.planFilePath;
4558
+ return Object.keys(echo).length > 0 ? echo : undefined;
4559
+ }
4547
4560
  var claudeAdapter = {
4548
4561
  approveVariants: APPROVE_VARIANTS,
4549
4562
  parseHookInput(stdin) {
@@ -4560,8 +4573,8 @@ var claudeAdapter = {
4560
4573
  planFilePath: hook.tool_input?.planFilePath
4561
4574
  };
4562
4575
  },
4563
- emitDecision(decision) {
4564
- return JSON.stringify(toHookOutput(decision));
4576
+ emitDecision(decision, input) {
4577
+ return JSON.stringify(toHookOutput(decision, toolInputEcho(input)));
4565
4578
  },
4566
4579
  fatalDenyLine(reason) {
4567
4580
  return fatalDenyLine(reason);
@@ -5777,7 +5790,7 @@ import { createHash } from "crypto";
5777
5790
  // package.json
5778
5791
  var package_default = {
5779
5792
  name: "@macintacos/caret",
5780
- version: "0.1.1",
5793
+ version: "0.2.0",
5781
5794
  license: "MIT",
5782
5795
  type: "module",
5783
5796
  description: "A Claude Code plugin that replaces the terminal plan-approval prompt with a local web UI for inline review, annotation, and approval.",
@@ -5967,7 +5980,7 @@ function currentCommit() {
5967
5980
  }
5968
5981
 
5969
5982
  // src/commands/daemon.ts
5970
- import { randomUUID as randomUUID2 } from "crypto";
5983
+ import { randomUUID } from "crypto";
5971
5984
  import { existsSync as existsSync3, unlinkSync as unlinkSync3 } from "fs";
5972
5985
 
5973
5986
  // src/daemon-lifecycle.ts
@@ -6013,6 +6026,22 @@ async function longPoll(baseUrl, id) {
6013
6026
  throw new Error(`decision long-poll failed: ${res.status}`);
6014
6027
  return await res.json();
6015
6028
  }
6029
+ async function listReviews(baseUrl) {
6030
+ const res = await fetch(`${baseUrl}/api/reviews`, { signal: AbortSignal.timeout(1000) });
6031
+ if (!res.ok)
6032
+ throw new Error(`GET /api/reviews failed: ${res.status}`);
6033
+ return await res.json();
6034
+ }
6035
+ async function resolveReview(baseUrl, id, body) {
6036
+ const res = await fetch(`${baseUrl}/api/reviews/${id}/resolve`, {
6037
+ method: "POST",
6038
+ headers: { "Content-Type": "application/json" },
6039
+ body: JSON.stringify(body),
6040
+ signal: AbortSignal.timeout(1000)
6041
+ });
6042
+ if (!res.ok)
6043
+ throw new Error(`POST /resolve failed: ${res.status}`);
6044
+ }
6016
6045
 
6017
6046
  // src/log.ts
6018
6047
  var import_pino = __toESM(require_pino(), 1);
@@ -6189,7 +6218,7 @@ function toClientReview(review) {
6189
6218
  annotations: cur.annotations,
6190
6219
  versions: review.versions,
6191
6220
  generalCommentDraft: review.generalCommentDraft ?? "",
6192
- composerScratches: review.composerScratches ?? [],
6221
+ composerScratches: cur.composerScratches ?? [],
6193
6222
  createdAt: review.createdAt,
6194
6223
  updatedAt: review.updatedAt,
6195
6224
  decision: review.decision
@@ -20941,6 +20970,172 @@ function createDecisions(log = noopLogger) {
20941
20970
  };
20942
20971
  }
20943
20972
 
20973
+ // src/plan-files.ts
20974
+ import { readdirSync as readdirSync2, readFileSync as readFileSync6, realpathSync, statSync as statSync2 } from "fs";
20975
+ import { extname, isAbsolute, join as join6, relative as relative2, resolve, sep } from "path";
20976
+ var EXCERPT_RADIUS = 6;
20977
+ var EXCERPT_HEAD_LINES = 13;
20978
+ var MAX_EXCERPT_BYTES = 2 * 1024 * 1024;
20979
+ var MAX_SCAN_ENTRIES = 5000;
20980
+ var SKIP_DIRS = new Set(["node_modules", "dist", "build", "coverage", "out"]);
20981
+ var EXT_LANGUAGE = {
20982
+ ".ts": "typescript",
20983
+ ".mts": "typescript",
20984
+ ".cts": "typescript",
20985
+ ".tsx": "tsx",
20986
+ ".js": "javascript",
20987
+ ".mjs": "javascript",
20988
+ ".cjs": "javascript",
20989
+ ".jsx": "jsx",
20990
+ ".svelte": "svelte",
20991
+ ".vue": "vue",
20992
+ ".json": "json",
20993
+ ".jsonc": "jsonc",
20994
+ ".css": "css",
20995
+ ".scss": "scss",
20996
+ ".less": "less",
20997
+ ".html": "html",
20998
+ ".htm": "html",
20999
+ ".xml": "xml",
21000
+ ".svg": "xml",
21001
+ ".md": "markdown",
21002
+ ".mdx": "mdx",
21003
+ ".py": "python",
21004
+ ".rb": "ruby",
21005
+ ".rs": "rust",
21006
+ ".go": "go",
21007
+ ".java": "java",
21008
+ ".kt": "kotlin",
21009
+ ".c": "c",
21010
+ ".h": "c",
21011
+ ".cc": "cpp",
21012
+ ".cpp": "cpp",
21013
+ ".hpp": "cpp",
21014
+ ".sh": "shellscript",
21015
+ ".bash": "shellscript",
21016
+ ".zsh": "shellscript",
21017
+ ".toml": "toml",
21018
+ ".yaml": "yaml",
21019
+ ".yml": "yaml",
21020
+ ".ini": "ini",
21021
+ ".sql": "sql",
21022
+ ".graphql": "graphql",
21023
+ ".gql": "graphql",
21024
+ ".php": "php",
21025
+ ".swift": "swift",
21026
+ ".dart": "dart"
21027
+ };
21028
+ function languageForPath(path) {
21029
+ return EXT_LANGUAGE[extname(path).toLowerCase()] ?? "text";
21030
+ }
21031
+ function safeRealpath(path) {
21032
+ try {
21033
+ return realpathSync(path);
21034
+ } catch {
21035
+ return null;
21036
+ }
21037
+ }
21038
+ function containedFile(cwdReal, abs) {
21039
+ const real = safeRealpath(abs);
21040
+ if (real === null)
21041
+ return null;
21042
+ if (real !== cwdReal && !real.startsWith(cwdReal + sep))
21043
+ return null;
21044
+ try {
21045
+ if (!statSync2(real).isFile())
21046
+ return null;
21047
+ } catch {
21048
+ return null;
21049
+ }
21050
+ return real;
21051
+ }
21052
+ function basenameSearch(cwdReal, name) {
21053
+ if (name === "")
21054
+ return null;
21055
+ let scanned = 0;
21056
+ const queue = [cwdReal];
21057
+ while (queue.length > 0) {
21058
+ const currentDir = queue.shift();
21059
+ let entries;
21060
+ try {
21061
+ entries = readdirSync2(currentDir, { withFileTypes: true });
21062
+ } catch {
21063
+ continue;
21064
+ }
21065
+ for (const entry of entries) {
21066
+ if (++scanned > MAX_SCAN_ENTRIES)
21067
+ return null;
21068
+ if (entry.isFile()) {
21069
+ if (entry.name === name)
21070
+ return join6(currentDir, entry.name);
21071
+ } else if (entry.isDirectory() && !SKIP_DIRS.has(entry.name) && !entry.name.startsWith(".")) {
21072
+ queue.push(join6(currentDir, entry.name));
21073
+ }
21074
+ }
21075
+ }
21076
+ return null;
21077
+ }
21078
+ function resolveFileInCwd(cwd, candidate) {
21079
+ if (cwd === "" || !isAbsolute(cwd))
21080
+ return null;
21081
+ const cwdReal = safeRealpath(cwd);
21082
+ if (cwdReal === null)
21083
+ return null;
21084
+ const cleaned = candidate.trim();
21085
+ if (cleaned === "")
21086
+ return null;
21087
+ const direct = containedFile(cwdReal, resolve(cwdReal, cleaned));
21088
+ if (direct !== null)
21089
+ return direct;
21090
+ const name = cleaned.split("/").pop() ?? "";
21091
+ return basenameSearch(cwdReal, name);
21092
+ }
21093
+ function readFileExcerpt(cwd, candidate, line) {
21094
+ const abs = resolveFileInCwd(cwd, candidate);
21095
+ if (abs === null)
21096
+ return null;
21097
+ let size;
21098
+ try {
21099
+ size = statSync2(abs).size;
21100
+ } catch {
21101
+ return null;
21102
+ }
21103
+ if (size > MAX_EXCERPT_BYTES)
21104
+ return null;
21105
+ let content;
21106
+ try {
21107
+ content = readFileSync6(abs, "utf-8");
21108
+ } catch {
21109
+ return null;
21110
+ }
21111
+ if (content.includes("\x00"))
21112
+ return null;
21113
+ const allLines = content.split(`
21114
+ `);
21115
+ if (allLines.length > 1 && allLines[allLines.length - 1] === "")
21116
+ allLines.pop();
21117
+ const totalLines = allLines.length;
21118
+ let startLine;
21119
+ let endLine;
21120
+ if (line !== undefined && line >= 1) {
21121
+ const target = Math.min(line, totalLines);
21122
+ startLine = Math.max(1, target - EXCERPT_RADIUS);
21123
+ endLine = Math.min(totalLines, target + EXCERPT_RADIUS);
21124
+ } else {
21125
+ startLine = 1;
21126
+ endLine = Math.min(totalLines, EXCERPT_HEAD_LINES);
21127
+ }
21128
+ const cwdReal = safeRealpath(cwd) ?? cwd;
21129
+ return {
21130
+ path: relative2(cwdReal, abs),
21131
+ language: languageForPath(abs),
21132
+ startLine,
21133
+ endLine,
21134
+ lines: allLines.slice(startLine - 1, endLine),
21135
+ totalLines
21136
+ };
21137
+ }
21138
+
20944
21139
  // src/prefs.ts
20945
21140
  import { readFile as readFile2, writeFile } from "fs/promises";
20946
21141
  import { dirname as dirname3 } from "path";
@@ -20970,17 +21165,17 @@ async function writeApproveMode(mode, file2 = prefsFile(), log = noopLogger, set
20970
21165
  }
20971
21166
 
20972
21167
  // src/reviews.ts
20973
- import { randomUUID } from "crypto";
21168
+ import { randomBytes } from "crypto";
20974
21169
 
20975
21170
  // src/plan-file.ts
20976
- import { existsSync as existsSync2, statSync as statSync2, writeFileSync as writeFileSync2 } from "fs";
21171
+ import { existsSync as existsSync2, statSync as statSync3, writeFileSync as writeFileSync2 } from "fs";
20977
21172
  function writeCanonicalPlanFile(planFilePath, canonical, log) {
20978
21173
  if (!planFilePath)
20979
21174
  return;
20980
21175
  try {
20981
21176
  if (!planFilePath.endsWith(".md"))
20982
21177
  return;
20983
- if (!existsSync2(planFilePath) || !statSync2(planFilePath).isFile())
21178
+ if (!existsSync2(planFilePath) || !statSync3(planFilePath).isFile())
20984
21179
  return;
20985
21180
  writeFileSync2(planFilePath, canonical);
20986
21181
  } catch (err) {
@@ -27751,6 +27946,9 @@ async function formatPlanMarkdown(plan, log = noopLogger, doFormat = prettierFor
27751
27946
  }
27752
27947
 
27753
27948
  // src/reviews.ts
27949
+ function newReviewId() {
27950
+ return randomBytes(8).toString("base64url");
27951
+ }
27754
27952
  function deriveTitle(plan) {
27755
27953
  for (const line of plan.split(`
27756
27954
  `)) {
@@ -27799,7 +27997,7 @@ async function routeIncomingPlan(input, store, log = noopLogger) {
27799
27997
  expired
27800
27998
  };
27801
27999
  }
27802
- const id = randomUUID();
28000
+ const id = newReviewId();
27803
28001
  const planEpoch = store.epochOf(sessionId);
27804
28002
  const review = {
27805
28003
  id,
@@ -27908,6 +28106,8 @@ var PlanInputSchema = exports_external.object({
27908
28106
  plan: exports_external.string().optional(),
27909
28107
  planFilePath: exports_external.string().optional()
27910
28108
  }).catch({});
28109
+ var MAX_FILE_REFS = 200;
28110
+ var FileRefsBodySchema = exports_external.object({ paths: exports_external.array(exports_external.string()).catch([]) }).catch({ paths: [] });
27911
28111
  var BehaviorSchema = exports_external.enum(["allow", "deny"]);
27912
28112
  var ResolveBodySchema = exports_external.object({
27913
28113
  behavior: BehaviorSchema.catch("allow"),
@@ -27958,11 +28158,13 @@ var nullToUndefined = (v2) => v2 ?? undefined;
27958
28158
  var DraftBodySchema = exports_external.object({
27959
28159
  annotations: exports_external.array(AnnotationSchema).nullish().transform(nullToUndefined),
27960
28160
  generalCommentDraft: exports_external.string().nullish().transform(nullToUndefined),
27961
- composerScratches: exports_external.array(PersistedScratchSchema).nullish().transform(nullToUndefined).catch(undefined)
28161
+ composerScratches: exports_external.array(PersistedScratchSchema).nullish().transform(nullToUndefined).catch(undefined),
28162
+ version: exports_external.number().int().min(1).nullish().transform(nullToUndefined).catch(undefined)
27962
28163
  }).catch({
27963
28164
  annotations: undefined,
27964
28165
  generalCommentDraft: undefined,
27965
- composerScratches: undefined
28166
+ composerScratches: undefined,
28167
+ version: undefined
27966
28168
  });
27967
28169
  async function parseBody(req, schema) {
27968
28170
  return schema.parse(await req.json().catch(() => ({})));
@@ -27985,7 +28187,7 @@ function resolveOptions(opts) {
27985
28187
  log: opts.log ?? noopLogger
27986
28188
  };
27987
28189
  }
27988
- var ID_ROUTE_RE = /^\/api\/reviews\/([^/]+)(\/decision|\/resolve|\/draft|\/expire)?$/;
28190
+ var ID_ROUTE_RE = /^\/api\/reviews\/([^/]+)(\/decision|\/resolve|\/draft|\/expire|\/file-refs|\/file)?$/;
27989
28191
  function matchIdRoute(path) {
27990
28192
  const m = path.match(ID_ROUTE_RE);
27991
28193
  if (!m)
@@ -27999,11 +28201,11 @@ function createServer(opts) {
27999
28201
  const { awaitDecision, resolveDecision, clearDecision, openDecisionCount } = createDecisions(log);
28000
28202
  const approveModeSet = approveVariants && approveVariants.length > 0 ? { valid: approveVariants.map((v2) => v2.id), fallback: approveVariants[0]?.id ?? "default" } : { valid: ["default"], fallback: "default" };
28001
28203
  function raceDecision(id, ms2) {
28002
- return new Promise((resolve) => {
28003
- const t = setTimeout(() => resolve(null), ms2);
28204
+ return new Promise((resolve2) => {
28205
+ const t = setTimeout(() => resolve2(null), ms2);
28004
28206
  awaitDecision(id).then((d) => {
28005
28207
  clearTimeout(t);
28006
- resolve(d);
28208
+ resolve2(d);
28007
28209
  });
28008
28210
  });
28009
28211
  }
@@ -28135,6 +28337,34 @@ function createServer(opts) {
28135
28337
  const r = store.get(id);
28136
28338
  return r ? Response.json(toClientReview(r)) : notFound();
28137
28339
  }
28340
+ async function handleFileRefs(req, id) {
28341
+ const r = store.get(id);
28342
+ if (!r)
28343
+ return notFound();
28344
+ const { paths } = await parseBody(req, FileRefsBodySchema);
28345
+ const seen = new Set;
28346
+ const resolved = [];
28347
+ for (const p of paths.slice(0, MAX_FILE_REFS)) {
28348
+ if (seen.has(p))
28349
+ continue;
28350
+ seen.add(p);
28351
+ if (resolveFileInCwd(r.cwd, p) !== null)
28352
+ resolved.push(p);
28353
+ }
28354
+ log.debug("request", `file-refs resolved: ${resolved.length}/${seen.size}`, { reviewId: id });
28355
+ return Response.json({ resolved });
28356
+ }
28357
+ function handleFileExcerpt(req, id) {
28358
+ const r = store.get(id);
28359
+ if (!r)
28360
+ return notFound();
28361
+ const params = new URL(req.url).searchParams;
28362
+ const path = params.get("path") ?? "";
28363
+ const lineRaw = params.get("line");
28364
+ const line = lineRaw !== null && /^\d+$/.test(lineRaw) ? Number(lineRaw) : undefined;
28365
+ const excerpt = readFileExcerpt(r.cwd, path, line);
28366
+ return excerpt ? Response.json(excerpt) : notFound();
28367
+ }
28138
28368
  async function handleDecision(id) {
28139
28369
  const inMem = store.get(id);
28140
28370
  if (inMem?.decision) {
@@ -28168,8 +28398,9 @@ function createServer(opts) {
28168
28398
  if (body.generalCommentDraft != null) {
28169
28399
  r.generalCommentDraft = body.generalCommentDraft;
28170
28400
  }
28171
- if (body.composerScratches != null) {
28172
- r.composerScratches = body.composerScratches;
28401
+ const cur = currentVersion(r);
28402
+ if (body.composerScratches != null && (body.version == null || body.version === cur.version)) {
28403
+ cur.composerScratches = body.composerScratches;
28173
28404
  }
28174
28405
  });
28175
28406
  if (updated)
@@ -28191,7 +28422,7 @@ function createServer(opts) {
28191
28422
  r.decision = decision;
28192
28423
  r.status = decision.behavior === "allow" ? "approved" : "rejected";
28193
28424
  r.generalCommentDraft = "";
28194
- r.composerScratches = [];
28425
+ currentVersion(r).composerScratches = [];
28195
28426
  });
28196
28427
  if (decision.behavior === "allow") {
28197
28428
  store.bumpEpoch(existing.sessionId);
@@ -28244,6 +28475,10 @@ function createServer(opts) {
28244
28475
  const { id, sub } = route;
28245
28476
  if (method === "GET" && !sub)
28246
28477
  return handleGetReview(id);
28478
+ if (method === "GET" && sub === "/file")
28479
+ return handleFileExcerpt(req, id);
28480
+ if (method === "POST" && sub === "/file-refs")
28481
+ return handleFileRefs(req, id);
28247
28482
  if (method === "GET" && sub === "/decision")
28248
28483
  return handleDecision(id);
28249
28484
  if (method === "PUT" && sub === "/draft")
@@ -28334,7 +28569,7 @@ function createServer(opts) {
28334
28569
 
28335
28570
  // src/store.ts
28336
28571
  import { readdir, readFile as readFile3, writeFile as writeFile2 } from "fs/promises";
28337
- import { join as join6 } from "path";
28572
+ import { join as join7 } from "path";
28338
28573
  function createStore(dir, log = noopLogger) {
28339
28574
  const reviews = new Map;
28340
28575
  const epochs = new Map;
@@ -28343,7 +28578,7 @@ function createStore(dir, log = noopLogger) {
28343
28578
  const prev = writeChains.get(review.id) ?? Promise.resolve();
28344
28579
  const next = prev.catch(() => {}).then(async () => {
28345
28580
  ensureStateDir(dir);
28346
- await writeFile2(join6(dir, `${review.id}.json`), JSON.stringify(review, null, 2), {
28581
+ await writeFile2(join7(dir, `${review.id}.json`), JSON.stringify(review, null, 2), {
28347
28582
  mode: 384
28348
28583
  });
28349
28584
  log.debug("store", `review persisted: ${shortId(review.id)}`, { reviewId: review.id });
@@ -28386,14 +28621,14 @@ function createStore(dir, log = noopLogger) {
28386
28621
  return;
28387
28622
  review.status = "expired";
28388
28623
  review.generalCommentDraft = "";
28389
- review.composerScratches = [];
28624
+ currentVersion(review).composerScratches = [];
28390
28625
  review.updatedAt = Math.max(Date.now(), review.updatedAt + 1);
28391
28626
  reviews.delete(id);
28392
28627
  await persist(review);
28393
28628
  return review;
28394
28629
  },
28395
28630
  async persisted(id) {
28396
- return await readJsonFile(join6(dir, `${id}.json`)) ?? undefined;
28631
+ return await readJsonFile(join7(dir, `${id}.json`)) ?? undefined;
28397
28632
  },
28398
28633
  size() {
28399
28634
  return reviews.size;
@@ -28420,7 +28655,7 @@ function createStore(dir, log = noopLogger) {
28420
28655
  if (!file2.endsWith(".json"))
28421
28656
  continue;
28422
28657
  try {
28423
- const review = JSON.parse(await readFile3(join6(dir, file2), "utf-8"));
28658
+ const review = JSON.parse(await readFile3(join7(dir, file2), "utf-8"));
28424
28659
  if (isUnresolved(review.status)) {
28425
28660
  reviews.set(review.id, review);
28426
28661
  loaded++;
@@ -28468,7 +28703,7 @@ async function runDaemon(opts) {
28468
28703
  buildId: await currentBuildId(),
28469
28704
  commit: currentCommit(),
28470
28705
  stateDir: stateDir(),
28471
- instanceId: randomUUID2().slice(0, 8),
28706
+ instanceId: randomUUID().slice(0, 8),
28472
28707
  approveVariants: adapter.approveVariants,
28473
28708
  log
28474
28709
  });
@@ -28510,8 +28745,8 @@ import { existsSync as existsSync4 } from "fs";
28510
28745
  import { release } from "os";
28511
28746
 
28512
28747
  // src/discovery.ts
28513
- import { readdirSync as readdirSync2, statSync as statSync3 } from "fs";
28514
- import { basename, join as join7 } from "path";
28748
+ import { readdirSync as readdirSync3, statSync as statSync4 } from "fs";
28749
+ import { basename, join as join8 } from "path";
28515
28750
  var CARET_BIN = "caret-native";
28516
28751
  async function safe(build) {
28517
28752
  try {
@@ -28711,7 +28946,7 @@ function listReviewFiles() {
28711
28946
  const dir = reviewsDir();
28712
28947
  let files;
28713
28948
  try {
28714
- files = readdirSync2(dir);
28949
+ files = readdirSync3(dir);
28715
28950
  } catch {
28716
28951
  return [];
28717
28952
  }
@@ -28721,7 +28956,7 @@ function listReviewFiles() {
28721
28956
  continue;
28722
28957
  if (out.length >= 5000)
28723
28958
  break;
28724
- const raw = readJsonFileSync(join7(dir, file2));
28959
+ const raw = readJsonFileSync(join8(dir, file2));
28725
28960
  if (raw && typeof raw.id === "string" && typeof raw.status === "string") {
28726
28961
  out.push({ id: raw.id, status: raw.status });
28727
28962
  }
@@ -28732,7 +28967,7 @@ var TAIL_BYTES = 256 * 1024;
28732
28967
  async function logStats(path) {
28733
28968
  let size;
28734
28969
  try {
28735
- size = statSync3(path).size;
28970
+ size = statSync4(path).size;
28736
28971
  } catch {
28737
28972
  return { path, exists: false, size: 0, errors: 0, warns: 0 };
28738
28973
  }
@@ -28819,8 +29054,8 @@ async function runDiscoverySubcommand(opts) {
28819
29054
  }
28820
29055
 
28821
29056
  // src/commands/install-opencode.ts
28822
- import { existsSync as existsSync7, readFileSync as readFileSync7, rmSync as rmSync2, writeFileSync as writeFileSync5 } from "fs";
28823
- import { join as join9 } from "path";
29057
+ import { existsSync as existsSync7, readFileSync as readFileSync8, rmSync as rmSync2, writeFileSync as writeFileSync5 } from "fs";
29058
+ import { join as join10 } from "path";
28824
29059
 
28825
29060
  // src/adapters/opencode/deploy.ts
28826
29061
  import { existsSync as existsSync5, mkdirSync as mkdirSync2, rmSync, writeFileSync as writeFileSync4 } from "fs";
@@ -28879,27 +29114,27 @@ function removeFiles(targets, opts) {
28879
29114
  }
28880
29115
 
28881
29116
  // src/adapters/opencode/packaging.ts
28882
- import { existsSync as existsSync6, readFileSync as readFileSync6, readdirSync as readdirSync3 } from "fs";
28883
- import { dirname as dirname6, join as join8 } from "path";
29117
+ import { existsSync as existsSync6, readFileSync as readFileSync7, readdirSync as readdirSync4 } from "fs";
29118
+ import { dirname as dirname6, join as join9 } from "path";
28884
29119
  function resolveCaretRoot() {
28885
29120
  const candidates = [];
28886
29121
  const script = process.argv[1];
28887
29122
  if (script)
28888
- candidates.push(join8(dirname6(script), ".."));
28889
- candidates.push(join8(dirname6(process.execPath), ".."));
29123
+ candidates.push(join9(dirname6(script), ".."));
29124
+ candidates.push(join9(dirname6(process.execPath), ".."));
28890
29125
  for (const root of candidates) {
28891
- if (existsSync6(join8(root, "opencode", "caret.plugin.ts")))
29126
+ if (existsSync6(join9(root, "opencode", "caret.plugin.ts")))
28892
29127
  return root;
28893
29128
  }
28894
29129
  throw new Error(`caret OpenCode packaging not found (no opencode/caret.plugin.ts under: ${candidates.join(", ") || "(none)"}). The OpenCode plugin source must ship with caret.`);
28895
29130
  }
28896
29131
  function loadOpencodePackaging(root = resolveCaretRoot()) {
28897
- const pluginSource = readFileSync6(join8(root, "opencode", "caret.plugin.ts"), "utf-8");
28898
- const binPath = join8(root, "bin", "caret");
28899
- const commandsDir = join8(root, "opencode", "commands");
29132
+ const pluginSource = readFileSync7(join9(root, "opencode", "caret.plugin.ts"), "utf-8");
29133
+ const binPath = join9(root, "bin", "caret");
29134
+ const commandsDir = join9(root, "opencode", "commands");
28900
29135
  let commands = [];
28901
29136
  try {
28902
- commands = readdirSync3(commandsDir).filter((f) => f.endsWith(".md")).sort().map((f) => ({ name: f, contents: readFileSync6(join8(commandsDir, f), "utf-8") }));
29137
+ commands = readdirSync4(commandsDir).filter((f) => f.endsWith(".md")).sort().map((f) => ({ name: f, contents: readFileSync7(join9(commandsDir, f), "utf-8") }));
28903
29138
  } catch {
28904
29139
  commands = [];
28905
29140
  }
@@ -28911,7 +29146,7 @@ function runInstallOpencodeSubcommand(opts, deps = {}) {
28911
29146
  const dir = deps.configDir ?? opencodeConfigDir();
28912
29147
  const pkg = deps.packaging ?? loadOpencodePackaging();
28913
29148
  const pluginPath = pluginFilePath(dir);
28914
- const commandPaths = pkg.commands.map((c) => join9(commandDir(dir), namespacedCommandFilename(c.name)));
29149
+ const commandPaths = pkg.commands.map((c) => join10(commandDir(dir), namespacedCommandFilename(c.name)));
28915
29150
  if (opts.uninstall) {
28916
29151
  const result2 = removeFiles([pluginPath, ...commandPaths], { dryRun: opts.dryRun });
28917
29152
  const manifest2 = uninstallManifest(dir, opts.dryRun);
@@ -28924,7 +29159,7 @@ function runInstallOpencodeSubcommand(opts, deps = {}) {
28924
29159
  contents: stripNonDefaultExports(renderPlugin(pkg.pluginSource, { version: VERSION, binPath: pkg.binPath }))
28925
29160
  },
28926
29161
  ...pkg.commands.map((c) => ({
28927
- path: join9(commandDir(dir), namespacedCommandFilename(c.name)),
29162
+ path: join10(commandDir(dir), namespacedCommandFilename(c.name)),
28928
29163
  contents: renderPlugin(c.contents, { version: VERSION, binPath: pkg.binPath })
28929
29164
  }))
28930
29165
  ];
@@ -28961,7 +29196,7 @@ var bunInstall = (configDir2) => {
28961
29196
  };
28962
29197
  function installManifest(dir, dryRun) {
28963
29198
  const path = packageJsonPath(dir);
28964
- const existing = existsSync7(path) ? readFileSync7(path, "utf-8") : null;
29199
+ const existing = existsSync7(path) ? readFileSync8(path, "utf-8") : null;
28965
29200
  let next;
28966
29201
  try {
28967
29202
  next = addPluginDependency(existing, OPENCODE_PLUGIN_DEP, OPENCODE_PLUGIN_DEP_VERSION);
@@ -28978,7 +29213,7 @@ function uninstallManifest(dir, dryRun) {
28978
29213
  const path = packageJsonPath(dir);
28979
29214
  if (!existsSync7(path))
28980
29215
  return [];
28981
- const existing = readFileSync7(path, "utf-8");
29216
+ const existing = readFileSync8(path, "utf-8");
28982
29217
  let next;
28983
29218
  try {
28984
29219
  next = removePluginDependency(existing, OPENCODE_PLUGIN_DEP);
@@ -29016,6 +29251,51 @@ async function runPrewarm() {
29016
29251
  process.exit(0);
29017
29252
  }
29018
29253
 
29254
+ // src/reconcile.ts
29255
+ async function runReconcile(stdin, deps) {
29256
+ const ctx = {};
29257
+ try {
29258
+ const input = deps.parseHookInput(stdin);
29259
+ ctx.sessionId = input.sessionId;
29260
+ if (!input.sessionId)
29261
+ return;
29262
+ const match = (await deps.listReviews()).find((r) => r.sessionId === input.sessionId);
29263
+ if (!match) {
29264
+ logDebug("reconcile", "no pending review for session; nothing to reconcile", { ...ctx });
29265
+ return;
29266
+ }
29267
+ ctx.reviewId = match.id;
29268
+ await deps.resolveReview(match.id);
29269
+ logInfo("reconcile", "terminal approval reconciled", { ...ctx });
29270
+ } catch (err) {
29271
+ logDebug("reconcile", "reconcile skipped", { ...ctx, err: String(err) });
29272
+ }
29273
+ }
29274
+
29275
+ // src/commands/reconcile.ts
29276
+ function prodReconcileDeps(baseUrl, adapter) {
29277
+ return {
29278
+ parseHookInput: (stdin) => adapter.parseHookInput(stdin),
29279
+ listReviews: () => listReviews(baseUrl),
29280
+ resolveReview: (id) => resolveReview(baseUrl, id, { behavior: "allow" })
29281
+ };
29282
+ }
29283
+ async function runReconcileSubcommand() {
29284
+ try {
29285
+ const loaded = loadSettings();
29286
+ setLogLevel(loaded.logging.level);
29287
+ setRedact(loaded.logging.redact);
29288
+ warnInvalidEnvVars((msg) => logWarn("env", msg));
29289
+ const adapter = selectAdapter();
29290
+ const baseUrl = `http://localhost:${getPort(loaded)}`;
29291
+ const stdin = await Bun.stdin.text();
29292
+ await runReconcile(stdin, prodReconcileDeps(baseUrl, adapter));
29293
+ } catch (err) {
29294
+ logDebug("reconcile", "reconcile subcommand skipped", { err: String(err) });
29295
+ }
29296
+ process.exit(0);
29297
+ }
29298
+
29019
29299
  // src/commands/redact.ts
29020
29300
  function runRedactSubcommand() {
29021
29301
  try {
@@ -30391,17 +30671,24 @@ function denyDecision(reason) {
30391
30671
  class TimeoutError extends Error {
30392
30672
  }
30393
30673
  function withTimeout(p, ms2, message) {
30394
- return new Promise((resolve, reject) => {
30674
+ return new Promise((resolve2, reject) => {
30395
30675
  const t = setTimeout(() => reject(new TimeoutError(message)), ms2);
30396
30676
  p.then((v3) => {
30397
30677
  clearTimeout(t);
30398
- resolve(v3);
30678
+ resolve2(v3);
30399
30679
  }, (e) => {
30400
30680
  clearTimeout(t);
30401
30681
  reject(e);
30402
30682
  });
30403
30683
  });
30404
30684
  }
30685
+ async function expireAbandoned(expire, handle) {
30686
+ if (!handle)
30687
+ return;
30688
+ try {
30689
+ await expire(handle.baseUrl, handle.id);
30690
+ } catch {}
30691
+ }
30405
30692
  async function runReview(stdin, deps) {
30406
30693
  let step = "parse";
30407
30694
  const ctx = {};
@@ -30421,6 +30708,7 @@ async function runReview(stdin, deps) {
30421
30708
  step = "postReview";
30422
30709
  const { id, hasLiveClient } = await deps.postReview(baseUrl, input);
30423
30710
  ctx.reviewId = id;
30711
+ deps.onPosted?.(baseUrl, id);
30424
30712
  logDebug("review", `review created: ${shortId(id)}`, { ...ctx });
30425
30713
  const open = new URL(baseUrl);
30426
30714
  open.hostname = VANITY_HOST;
@@ -30495,24 +30783,36 @@ async function runReviewSubcommand() {
30495
30783
  setRedact(loaded.logging.redact);
30496
30784
  warnInvalidEnvVars((msg) => logWarn("env", msg));
30497
30785
  const adapter = selectAdapter();
30786
+ let hookInput;
30787
+ let posted;
30498
30788
  let responded = false;
30499
30789
  const respond = (decision) => {
30500
30790
  if (responded)
30501
30791
  return;
30502
30792
  responded = true;
30503
- process.stdout.write(`${adapter.emitDecision(decision)}
30793
+ process.stdout.write(`${adapter.emitDecision(decision, hookInput)}
30504
30794
  `);
30505
30795
  };
30506
- const denyAndExit = (reason) => {
30796
+ const denyAndExit = async (reason) => {
30507
30797
  if (!responded)
30508
30798
  logError("signal", new Error(reason));
30509
30799
  respond({ behavior: "deny", feedback: `${reason} See ${logFile()}.`, decidedAt: Date.now() });
30800
+ await expireAbandoned(expireReview, posted);
30510
30801
  process.exit(0);
30511
30802
  };
30512
- process.once("SIGINT", () => denyAndExit("caret: interrupted (SIGINT) \u2014 denying to fail safe."));
30513
- process.once("SIGTERM", () => denyAndExit("caret: terminated (SIGTERM) \u2014 denying to fail safe."));
30803
+ process.once("SIGINT", () => void denyAndExit("caret: interrupted (SIGINT) \u2014 denying to fail safe."));
30804
+ process.once("SIGTERM", () => void denyAndExit("caret: terminated (SIGTERM) \u2014 denying to fail safe."));
30514
30805
  const stdin = await Bun.stdin.text();
30515
- const out = await runReview(stdin, prodReviewDeps(loaded, adapter));
30806
+ try {
30807
+ hookInput = adapter.parseHookInput(stdin);
30808
+ } catch {
30809
+ hookInput = undefined;
30810
+ }
30811
+ const deps = prodReviewDeps(loaded, adapter);
30812
+ deps.onPosted = (baseUrl, id) => {
30813
+ posted = { baseUrl, id };
30814
+ };
30815
+ const out = await runReview(stdin, deps);
30516
30816
  respond(out);
30517
30817
  process.exit(0);
30518
30818
  }
@@ -32570,10 +32870,11 @@ function runProgram(program2, onError) {
32570
32870
 
32571
32871
  // src/cli.ts
32572
32872
  function buildProgram() {
32573
- const program2 = createProgram("caret", "caret hook CLI: daemon | prewarm | review | redact | discovery | install-opencode").version(VERSION);
32873
+ const program2 = createProgram("caret", "caret hook CLI: daemon | prewarm | review | reconcile | redact | discovery | install-opencode").version(VERSION);
32574
32874
  program2.command("daemon").description("run the review daemon").option("--ephemeral", "bind an OS-assigned port instead of the configured one").action((opts) => runDaemon({ ephemeral: opts.ephemeral ?? false }));
32575
32875
  program2.command("prewarm").description("warm-start the daemon").action(() => runPrewarm());
32576
32876
  program2.command("review").description("review a plan from stdin (ExitPlanMode hook)").action(() => runReviewSubcommand());
32877
+ program2.command("reconcile").description("reconcile a terminal plan approval into the daemon (ExitPlanMode PostToolUse hook)").action(() => runReconcileSubcommand());
32577
32878
  program2.command("redact").description("write redacted copies of the logs").action(() => runRedactSubcommand());
32578
32879
  program2.command("discovery").description("print a diagnostics report").option("--json", "emit the machine-readable JSON document").action((opts) => runDiscoverySubcommand({ json: opts.json ?? false }));
32579
32880
  program2.command("install-opencode").description("install (or with --uninstall, remove) caret's OpenCode plugin + commands").option("--uninstall", "remove caret's OpenCode files instead of installing them").option("--dry-run", "print what would change without writing").action((opts) => runInstallOpencodeSubcommand({