@macintacos/caret 0.1.3 → 0.3.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 (101) hide show
  1. package/.claude-plugin/plugin.json +1 -1
  2. package/README.md +2 -2
  3. package/commands/demo.md +5 -4
  4. package/dist/cli.js +339 -49
  5. package/hooks/hooks.json +10 -0
  6. package/opencode/caret.plugin.ts +138 -20
  7. package/package.json +9 -1
  8. package/ui/dist/assets/{angular-html-Cg_qO4OQ.js → angular-html-B6NJ8Crx.js} +1 -1
  9. package/ui/dist/assets/{angular-ts-BmV-Wd4K.js → angular-ts-FdEJue4l.js} +1 -1
  10. package/ui/dist/assets/{apl-DDDJQLEy.js → apl-8jCzQ5pt.js} +1 -1
  11. package/ui/dist/assets/{astro-Cq94xJ2F.js → astro-D6Uv3Zd6.js} +1 -1
  12. package/ui/dist/assets/{blade-DZCNRrZB.js → blade-DdLBtQgs.js} +1 -1
  13. package/ui/dist/assets/{c-CwNkiPnO.js → c-BtgVJzRT.js} +1 -1
  14. package/ui/dist/assets/{cobol-CgSNuNV-.js → cobol-BOF3ihbS.js} +1 -1
  15. package/ui/dist/assets/{coffee-DWlIdtuG.js → coffee-Br1deUOy.js} +1 -1
  16. package/ui/dist/assets/{cpp-CmiYyU8y.js → cpp-BuRF7xA8.js} +1 -1
  17. package/ui/dist/assets/{crystal-BiyOLiea.js → crystal-C2zE7OWx.js} +1 -1
  18. package/ui/dist/assets/{css-DGiwxBdC.js → css-iBwOFG5O.js} +1 -1
  19. package/ui/dist/assets/{dist-CCQGXHXf.js → dist--moZHrIw.js} +1 -1
  20. package/ui/dist/assets/{dist-CbS4AEdX.js → dist-C4FPvSXr.js} +1 -1
  21. package/ui/dist/assets/{dist-CNR9H9hI.js → dist-CDXacocg.js} +1 -1
  22. package/ui/dist/assets/{dist-CfztD2mm.js → dist-CsIQFmL2.js} +1 -1
  23. package/ui/dist/assets/{dist-D_I8tzqw.js → dist-CtqCYfGw.js} +1 -1
  24. package/ui/dist/assets/{dist-BViEvwEG.js → dist-D0u8lgi2.js} +1 -1
  25. package/ui/dist/assets/{dist-BkSlyoLl.js → dist-D2IYj7rL.js} +1 -1
  26. package/ui/dist/assets/{dist-24WuufPm.js → dist-DIdA1vYB.js} +1 -1
  27. package/ui/dist/assets/{dist-C_IYb7Qh.js → dist-DSFoRH4B.js} +1 -1
  28. package/ui/dist/assets/{dist-B3mK9QMK.js → dist-DXeIzOJW.js} +1 -1
  29. package/ui/dist/assets/{dist-B-SHQUSo.js → dist-DhFSo_KF.js} +1 -1
  30. package/ui/dist/assets/{dist-Cr9g-IkY.js → dist-DheG7L5T.js} +1 -1
  31. package/ui/dist/assets/{dist-CKVYvLDM.js → dist-Diqv5yoL.js} +1 -1
  32. package/ui/dist/assets/{dist-BXtMxuI7.js → dist-Dm7ikdu5.js} +1 -1
  33. package/ui/dist/assets/{dist-C1Qk3ApG.js → dist-DvWv2GjQ.js} +1 -1
  34. package/ui/dist/assets/{dist-C9DszYV1.js → dist-jmyqwp8P.js} +1 -1
  35. package/ui/dist/assets/{dist-C-hQPg8K.js → dist-zkfAcw6C.js} +1 -1
  36. package/ui/dist/assets/{edge-CZXzS9pm.js → edge-C8C1WH_b.js} +1 -1
  37. package/ui/dist/assets/{elixir-BTnUmaBf.js → elixir-ChioPNrw.js} +1 -1
  38. package/ui/dist/assets/{elm-Bq5po3Zd.js → elm-DrMVy3GL.js} +1 -1
  39. package/ui/dist/assets/{erb-BOVmqWJf.js → erb-B3LvCl6V.js} +1 -1
  40. package/ui/dist/assets/{git-rebase-DLQM-CxK.js → git-rebase-Bv1Z90mV.js} +1 -1
  41. package/ui/dist/assets/{glimmer-js-DzMlcKY7.js → glimmer-js-_Cj2ZxqT.js} +1 -1
  42. package/ui/dist/assets/{glimmer-ts-Dz691XKF.js → glimmer-ts-eeir-ofz.js} +1 -1
  43. package/ui/dist/assets/{glsl-CMQZ1AkF.js → glsl-Zcj4mBmc.js} +1 -1
  44. package/ui/dist/assets/{graphql-DBKIWtF7.js → graphql-DcI0CImY.js} +1 -1
  45. package/ui/dist/assets/{hack-DZbb_fkF.js → hack-CRMO6OEY.js} +1 -1
  46. package/ui/dist/assets/{haml-CSjdx5hj.js → haml-vpaJ2U9N.js} +1 -1
  47. package/ui/dist/assets/{handlebars-Mu9SxQrl.js → handlebars-BmRnoKX5.js} +1 -1
  48. package/ui/dist/assets/{html-D4wtNI0H.js → html-CWopqLKh.js} +1 -1
  49. package/ui/dist/assets/{html-derivative-DHrkr74L.js → html-derivative-kjil5-1g.js} +1 -1
  50. package/ui/dist/assets/{http-DtBk1Y64.js → http-BsiD1_wo.js} +1 -1
  51. package/ui/dist/assets/{hurl-1qu6SWMo.js → hurl-BSOSzca8.js} +1 -1
  52. package/ui/dist/assets/index-C6mdkhzF.css +1 -0
  53. package/ui/dist/assets/index-anQLM8ZA.js +896 -0
  54. package/ui/dist/assets/{java-CLLSyNOn.js → java-B3dX7M_O.js} +1 -1
  55. package/ui/dist/assets/{javascript-B-_kYMSx.js → javascript-FM1qUZzw.js} +1 -1
  56. package/ui/dist/assets/{jinja-BtHF51nI.js → jinja-DVVPq-Qq.js} +1 -1
  57. package/ui/dist/assets/{jison-C93_a--j.js → jison-DZjXlKnT.js} +1 -1
  58. package/ui/dist/assets/{json-C4FWYbGR.js → json-psWWRfU7.js} +1 -1
  59. package/ui/dist/assets/{jsx-BdkuTVvH.js → jsx-Bv0NBlC9.js} +1 -1
  60. package/ui/dist/assets/{julia-BmboAp4o.js → julia-DRPZCNs5.js} +1 -1
  61. package/ui/dist/assets/{just-E1Wftakf.js → just-CznON7Md.js} +1 -1
  62. package/ui/dist/assets/{latex-DZFvvGDu.js → latex-99tFYwaR.js} +1 -1
  63. package/ui/dist/assets/{liquid-BUTRgRVi.js → liquid-8z3tzRYn.js} +1 -1
  64. package/ui/dist/assets/{lua-Dl1ciOfH.js → lua-BZoKCS17.js} +1 -1
  65. package/ui/dist/assets/{marko-Bh-A3HQZ.js → marko-BU9z-RZr.js} +1 -1
  66. package/ui/dist/assets/{mdc-C2XCGsSV.js → mdc-C38yLzkY.js} +1 -1
  67. package/ui/dist/assets/{nginx-igqyMv_F.js → nginx-UMVInepI.js} +1 -1
  68. package/ui/dist/assets/{nim-DelsCcX1.js → nim-Du4jXclQ.js} +1 -1
  69. package/ui/dist/assets/{perl-Cgh7mkol.js → perl-CoEbwzPf.js} +1 -1
  70. package/ui/dist/assets/{php-DXn-4Rf4.js → php-ubTDGUam.js} +1 -1
  71. package/ui/dist/assets/{pug-BNWZhj7f.js → pug-Bmsmvvm7.js} +1 -1
  72. package/ui/dist/assets/{qml-CHLXijrJ.js → qml-YXj-z032.js} +1 -1
  73. package/ui/dist/assets/{r-g949Oasl.js → r-BNkIpWO8.js} +1 -1
  74. package/ui/dist/assets/{razor-BUv8GTQg.js → razor-ZM--hAhl.js} +1 -1
  75. package/ui/dist/assets/{regexp-Dxi3LqhO.js → regexp-CHr2m78T.js} +1 -1
  76. package/ui/dist/assets/{rst-CMQNxthU.js → rst-Xx0XAo8H.js} +1 -1
  77. package/ui/dist/assets/{ruby-CDBQomnV.js → ruby-BWgyGqGH.js} +1 -1
  78. package/ui/dist/assets/{sas-uuqXrFx7.js → sas-CwTmTlh0.js} +1 -1
  79. package/ui/dist/assets/{scss-CniJSQ3f.js → scss-DNgS20lR.js} +1 -1
  80. package/ui/dist/assets/{shellscript-CuBMk1iS.js → shellscript-Dkezxj03.js} +1 -1
  81. package/ui/dist/assets/{shellsession-BzDlSDO2.js → shellsession-D6yCCjOL.js} +1 -1
  82. package/ui/dist/assets/{soy-e0u5_VID.js → soy-lzhm7a9Y.js} +1 -1
  83. package/ui/dist/assets/{sql-Cbg060Y9.js → sql-Yo8AWGPM.js} +1 -1
  84. package/ui/dist/assets/{stata-n_N_Lnpt.js → stata-D62k9gHq.js} +1 -1
  85. package/ui/dist/assets/{surrealql-Sa2566xv.js → surrealql-qp-qEg-3.js} +1 -1
  86. package/ui/dist/assets/{svelte-B0zgpD2X.js → svelte-DLztoC2n.js} +1 -1
  87. package/ui/dist/assets/{templ-BMfZts2f.js → templ-Cu6WsZcl.js} +1 -1
  88. package/ui/dist/assets/{tex-7PyiPoxe.js → tex-CqZU9zkk.js} +1 -1
  89. package/ui/dist/assets/{ts-tags-nHnZDDzD.js → ts-tags-ClqaCWh2.js} +1 -1
  90. package/ui/dist/assets/{tsx-BgvqLYni.js → tsx-Cj58ow-S.js} +1 -1
  91. package/ui/dist/assets/{twig-BqJ1gUqf.js → twig-CgaKnAdg.js} +1 -1
  92. package/ui/dist/assets/{typescript-C2nbCIiA.js → typescript-CKElJ_dH.js} +1 -1
  93. package/ui/dist/assets/{vue-B_l4_yyQ.js → vue-DtckUkhv.js} +1 -1
  94. package/ui/dist/assets/{vue-html-qalJD3cY.js → vue-html-Cqq7SmCi.js} +1 -1
  95. package/ui/dist/assets/{vue-vine-Cle_s8sp.js → vue-vine-Dp4BXiQa.js} +1 -1
  96. package/ui/dist/assets/{xml-CnKUcNAY.js → xml-C3UNSSfc.js} +1 -1
  97. package/ui/dist/assets/{xsl-D5cPGaiC.js → xsl-BL_LCJsy.js} +1 -1
  98. package/ui/dist/assets/{yaml-BcZKwY9y.js → yaml-CD_CHV62.js} +1 -1
  99. package/ui/dist/index.html +2 -2
  100. package/ui/dist/assets/index-DT45eblI.css +0 -1
  101. package/ui/dist/assets/index-Dqm3ZFVQ.js +0 -554
package/dist/cli.js CHANGED
@@ -5790,7 +5790,7 @@ import { createHash } from "crypto";
5790
5790
  // package.json
5791
5791
  var package_default = {
5792
5792
  name: "@macintacos/caret",
5793
- version: "0.1.3",
5793
+ version: "0.3.0",
5794
5794
  license: "MIT",
5795
5795
  type: "module",
5796
5796
  description: "A Claude Code plugin that replaces the terminal plan-approval prompt with a local web UI for inline review, annotation, and approval.",
@@ -5822,6 +5822,7 @@ var package_default = {
5822
5822
  "@opencode-ai/plugin": "^1.16.2",
5823
5823
  "@playwright/test": "^1.60.0",
5824
5824
  "@sveltejs/vite-plugin-svelte": "^7.1.2",
5825
+ "@tailwindcss/vite": "^4.3.2",
5825
5826
  "@types/bun": "^1.3.14",
5826
5827
  "@types/semver": "^7.7.1",
5827
5828
  "happy-dom": "^20.9.0",
@@ -5829,6 +5830,8 @@ var package_default = {
5829
5830
  "pino-pretty": "^13.1.3",
5830
5831
  svelte: "^5.55.9",
5831
5832
  "svelte-check": "^4.4.8",
5833
+ tailwindcss: "^4.3.2",
5834
+ "tw-animate-css": "^1.4.0",
5832
5835
  typescript: "^6.0.3",
5833
5836
  vite: "^8.0.14"
5834
5837
  },
@@ -5840,8 +5843,11 @@ var package_default = {
5840
5843
  "@codemirror/state": "^6.6.0",
5841
5844
  "@codemirror/view": "^6.43.0",
5842
5845
  "@commander-js/extra-typings": "^15.0.0",
5846
+ "@internationalized/date": "^3.12.2",
5843
5847
  "@lezer/highlight": "^1.2.3",
5844
5848
  "@pierre/diffs": "1.2.7",
5849
+ "bits-ui": "^2.18.1",
5850
+ clsx: "^2.1.1",
5845
5851
  commander: "^15.0.0",
5846
5852
  marked: "^18.0.4",
5847
5853
  pino: "^10.3.1",
@@ -5849,6 +5855,8 @@ var package_default = {
5849
5855
  semver: "^7.8.1",
5850
5856
  shiki: "^4.1.0",
5851
5857
  "smol-toml": "1.6.1",
5858
+ "tailwind-merge": "^3.6.0",
5859
+ "tailwind-variants": "^3.2.2",
5852
5860
  xss: "^1.0.15",
5853
5861
  zod: "4.4.3"
5854
5862
  }
@@ -5980,7 +5988,7 @@ function currentCommit() {
5980
5988
  }
5981
5989
 
5982
5990
  // src/commands/daemon.ts
5983
- import { randomUUID as randomUUID2 } from "crypto";
5991
+ import { randomUUID } from "crypto";
5984
5992
  import { existsSync as existsSync3, unlinkSync as unlinkSync3 } from "fs";
5985
5993
 
5986
5994
  // src/daemon-lifecycle.ts
@@ -6026,6 +6034,22 @@ async function longPoll(baseUrl, id) {
6026
6034
  throw new Error(`decision long-poll failed: ${res.status}`);
6027
6035
  return await res.json();
6028
6036
  }
6037
+ async function listReviews(baseUrl) {
6038
+ const res = await fetch(`${baseUrl}/api/reviews`, { signal: AbortSignal.timeout(1000) });
6039
+ if (!res.ok)
6040
+ throw new Error(`GET /api/reviews failed: ${res.status}`);
6041
+ return await res.json();
6042
+ }
6043
+ async function resolveReview(baseUrl, id, body) {
6044
+ const res = await fetch(`${baseUrl}/api/reviews/${id}/resolve`, {
6045
+ method: "POST",
6046
+ headers: { "Content-Type": "application/json" },
6047
+ body: JSON.stringify(body),
6048
+ signal: AbortSignal.timeout(1000)
6049
+ });
6050
+ if (!res.ok)
6051
+ throw new Error(`POST /resolve failed: ${res.status}`);
6052
+ }
6029
6053
 
6030
6054
  // src/log.ts
6031
6055
  var import_pino = __toESM(require_pino(), 1);
@@ -6202,7 +6226,7 @@ function toClientReview(review) {
6202
6226
  annotations: cur.annotations,
6203
6227
  versions: review.versions,
6204
6228
  generalCommentDraft: review.generalCommentDraft ?? "",
6205
- composerScratches: review.composerScratches ?? [],
6229
+ composerScratches: cur.composerScratches ?? [],
6206
6230
  createdAt: review.createdAt,
6207
6231
  updatedAt: review.updatedAt,
6208
6232
  decision: review.decision
@@ -20954,6 +20978,172 @@ function createDecisions(log = noopLogger) {
20954
20978
  };
20955
20979
  }
20956
20980
 
20981
+ // src/plan-files.ts
20982
+ import { readdirSync as readdirSync2, readFileSync as readFileSync6, realpathSync, statSync as statSync2 } from "fs";
20983
+ import { extname, isAbsolute, join as join6, relative as relative2, resolve, sep } from "path";
20984
+ var EXCERPT_RADIUS = 6;
20985
+ var EXCERPT_HEAD_LINES = 13;
20986
+ var MAX_EXCERPT_BYTES = 2 * 1024 * 1024;
20987
+ var MAX_SCAN_ENTRIES = 5000;
20988
+ var SKIP_DIRS = new Set(["node_modules", "dist", "build", "coverage", "out"]);
20989
+ var EXT_LANGUAGE = {
20990
+ ".ts": "typescript",
20991
+ ".mts": "typescript",
20992
+ ".cts": "typescript",
20993
+ ".tsx": "tsx",
20994
+ ".js": "javascript",
20995
+ ".mjs": "javascript",
20996
+ ".cjs": "javascript",
20997
+ ".jsx": "jsx",
20998
+ ".svelte": "svelte",
20999
+ ".vue": "vue",
21000
+ ".json": "json",
21001
+ ".jsonc": "jsonc",
21002
+ ".css": "css",
21003
+ ".scss": "scss",
21004
+ ".less": "less",
21005
+ ".html": "html",
21006
+ ".htm": "html",
21007
+ ".xml": "xml",
21008
+ ".svg": "xml",
21009
+ ".md": "markdown",
21010
+ ".mdx": "mdx",
21011
+ ".py": "python",
21012
+ ".rb": "ruby",
21013
+ ".rs": "rust",
21014
+ ".go": "go",
21015
+ ".java": "java",
21016
+ ".kt": "kotlin",
21017
+ ".c": "c",
21018
+ ".h": "c",
21019
+ ".cc": "cpp",
21020
+ ".cpp": "cpp",
21021
+ ".hpp": "cpp",
21022
+ ".sh": "shellscript",
21023
+ ".bash": "shellscript",
21024
+ ".zsh": "shellscript",
21025
+ ".toml": "toml",
21026
+ ".yaml": "yaml",
21027
+ ".yml": "yaml",
21028
+ ".ini": "ini",
21029
+ ".sql": "sql",
21030
+ ".graphql": "graphql",
21031
+ ".gql": "graphql",
21032
+ ".php": "php",
21033
+ ".swift": "swift",
21034
+ ".dart": "dart"
21035
+ };
21036
+ function languageForPath(path) {
21037
+ return EXT_LANGUAGE[extname(path).toLowerCase()] ?? "text";
21038
+ }
21039
+ function safeRealpath(path) {
21040
+ try {
21041
+ return realpathSync(path);
21042
+ } catch {
21043
+ return null;
21044
+ }
21045
+ }
21046
+ function containedFile(cwdReal, abs) {
21047
+ const real = safeRealpath(abs);
21048
+ if (real === null)
21049
+ return null;
21050
+ if (real !== cwdReal && !real.startsWith(cwdReal + sep))
21051
+ return null;
21052
+ try {
21053
+ if (!statSync2(real).isFile())
21054
+ return null;
21055
+ } catch {
21056
+ return null;
21057
+ }
21058
+ return real;
21059
+ }
21060
+ function basenameSearch(cwdReal, name) {
21061
+ if (name === "")
21062
+ return null;
21063
+ let scanned = 0;
21064
+ const queue = [cwdReal];
21065
+ while (queue.length > 0) {
21066
+ const currentDir = queue.shift();
21067
+ let entries;
21068
+ try {
21069
+ entries = readdirSync2(currentDir, { withFileTypes: true });
21070
+ } catch {
21071
+ continue;
21072
+ }
21073
+ for (const entry of entries) {
21074
+ if (++scanned > MAX_SCAN_ENTRIES)
21075
+ return null;
21076
+ if (entry.isFile()) {
21077
+ if (entry.name === name)
21078
+ return join6(currentDir, entry.name);
21079
+ } else if (entry.isDirectory() && !SKIP_DIRS.has(entry.name) && !entry.name.startsWith(".")) {
21080
+ queue.push(join6(currentDir, entry.name));
21081
+ }
21082
+ }
21083
+ }
21084
+ return null;
21085
+ }
21086
+ function resolveFileInCwd(cwd, candidate) {
21087
+ if (cwd === "" || !isAbsolute(cwd))
21088
+ return null;
21089
+ const cwdReal = safeRealpath(cwd);
21090
+ if (cwdReal === null)
21091
+ return null;
21092
+ const cleaned = candidate.trim();
21093
+ if (cleaned === "")
21094
+ return null;
21095
+ const direct = containedFile(cwdReal, resolve(cwdReal, cleaned));
21096
+ if (direct !== null)
21097
+ return direct;
21098
+ const name = cleaned.split("/").pop() ?? "";
21099
+ return basenameSearch(cwdReal, name);
21100
+ }
21101
+ function readFileExcerpt(cwd, candidate, line) {
21102
+ const abs = resolveFileInCwd(cwd, candidate);
21103
+ if (abs === null)
21104
+ return null;
21105
+ let size;
21106
+ try {
21107
+ size = statSync2(abs).size;
21108
+ } catch {
21109
+ return null;
21110
+ }
21111
+ if (size > MAX_EXCERPT_BYTES)
21112
+ return null;
21113
+ let content;
21114
+ try {
21115
+ content = readFileSync6(abs, "utf-8");
21116
+ } catch {
21117
+ return null;
21118
+ }
21119
+ if (content.includes("\x00"))
21120
+ return null;
21121
+ const allLines = content.split(`
21122
+ `);
21123
+ if (allLines.length > 1 && allLines[allLines.length - 1] === "")
21124
+ allLines.pop();
21125
+ const totalLines = allLines.length;
21126
+ let startLine;
21127
+ let endLine;
21128
+ if (line !== undefined && line >= 1) {
21129
+ const target = Math.min(line, totalLines);
21130
+ startLine = Math.max(1, target - EXCERPT_RADIUS);
21131
+ endLine = Math.min(totalLines, target + EXCERPT_RADIUS);
21132
+ } else {
21133
+ startLine = 1;
21134
+ endLine = Math.min(totalLines, EXCERPT_HEAD_LINES);
21135
+ }
21136
+ const cwdReal = safeRealpath(cwd) ?? cwd;
21137
+ return {
21138
+ path: relative2(cwdReal, abs),
21139
+ language: languageForPath(abs),
21140
+ startLine,
21141
+ endLine,
21142
+ lines: allLines.slice(startLine - 1, endLine),
21143
+ totalLines
21144
+ };
21145
+ }
21146
+
20957
21147
  // src/prefs.ts
20958
21148
  import { readFile as readFile2, writeFile } from "fs/promises";
20959
21149
  import { dirname as dirname3 } from "path";
@@ -20983,17 +21173,17 @@ async function writeApproveMode(mode, file2 = prefsFile(), log = noopLogger, set
20983
21173
  }
20984
21174
 
20985
21175
  // src/reviews.ts
20986
- import { randomUUID } from "crypto";
21176
+ import { randomBytes } from "crypto";
20987
21177
 
20988
21178
  // src/plan-file.ts
20989
- import { existsSync as existsSync2, statSync as statSync2, writeFileSync as writeFileSync2 } from "fs";
21179
+ import { existsSync as existsSync2, statSync as statSync3, writeFileSync as writeFileSync2 } from "fs";
20990
21180
  function writeCanonicalPlanFile(planFilePath, canonical, log) {
20991
21181
  if (!planFilePath)
20992
21182
  return;
20993
21183
  try {
20994
21184
  if (!planFilePath.endsWith(".md"))
20995
21185
  return;
20996
- if (!existsSync2(planFilePath) || !statSync2(planFilePath).isFile())
21186
+ if (!existsSync2(planFilePath) || !statSync3(planFilePath).isFile())
20997
21187
  return;
20998
21188
  writeFileSync2(planFilePath, canonical);
20999
21189
  } catch (err) {
@@ -27764,6 +27954,9 @@ async function formatPlanMarkdown(plan, log = noopLogger, doFormat = prettierFor
27764
27954
  }
27765
27955
 
27766
27956
  // src/reviews.ts
27957
+ function newReviewId() {
27958
+ return randomBytes(8).toString("base64url");
27959
+ }
27767
27960
  function deriveTitle(plan) {
27768
27961
  for (const line of plan.split(`
27769
27962
  `)) {
@@ -27812,7 +28005,7 @@ async function routeIncomingPlan(input, store, log = noopLogger) {
27812
28005
  expired
27813
28006
  };
27814
28007
  }
27815
- const id = randomUUID();
28008
+ const id = newReviewId();
27816
28009
  const planEpoch = store.epochOf(sessionId);
27817
28010
  const review = {
27818
28011
  id,
@@ -27921,6 +28114,8 @@ var PlanInputSchema = exports_external.object({
27921
28114
  plan: exports_external.string().optional(),
27922
28115
  planFilePath: exports_external.string().optional()
27923
28116
  }).catch({});
28117
+ var MAX_FILE_REFS = 200;
28118
+ var FileRefsBodySchema = exports_external.object({ paths: exports_external.array(exports_external.string()).catch([]) }).catch({ paths: [] });
27924
28119
  var BehaviorSchema = exports_external.enum(["allow", "deny"]);
27925
28120
  var ResolveBodySchema = exports_external.object({
27926
28121
  behavior: BehaviorSchema.catch("allow"),
@@ -27971,11 +28166,13 @@ var nullToUndefined = (v2) => v2 ?? undefined;
27971
28166
  var DraftBodySchema = exports_external.object({
27972
28167
  annotations: exports_external.array(AnnotationSchema).nullish().transform(nullToUndefined),
27973
28168
  generalCommentDraft: exports_external.string().nullish().transform(nullToUndefined),
27974
- composerScratches: exports_external.array(PersistedScratchSchema).nullish().transform(nullToUndefined).catch(undefined)
28169
+ composerScratches: exports_external.array(PersistedScratchSchema).nullish().transform(nullToUndefined).catch(undefined),
28170
+ version: exports_external.number().int().min(1).nullish().transform(nullToUndefined).catch(undefined)
27975
28171
  }).catch({
27976
28172
  annotations: undefined,
27977
28173
  generalCommentDraft: undefined,
27978
- composerScratches: undefined
28174
+ composerScratches: undefined,
28175
+ version: undefined
27979
28176
  });
27980
28177
  async function parseBody(req, schema) {
27981
28178
  return schema.parse(await req.json().catch(() => ({})));
@@ -27998,7 +28195,7 @@ function resolveOptions(opts) {
27998
28195
  log: opts.log ?? noopLogger
27999
28196
  };
28000
28197
  }
28001
- var ID_ROUTE_RE = /^\/api\/reviews\/([^/]+)(\/decision|\/resolve|\/draft|\/expire)?$/;
28198
+ var ID_ROUTE_RE = /^\/api\/reviews\/([^/]+)(\/decision|\/resolve|\/draft|\/expire|\/file-refs|\/file)?$/;
28002
28199
  function matchIdRoute(path) {
28003
28200
  const m = path.match(ID_ROUTE_RE);
28004
28201
  if (!m)
@@ -28012,11 +28209,11 @@ function createServer(opts) {
28012
28209
  const { awaitDecision, resolveDecision, clearDecision, openDecisionCount } = createDecisions(log);
28013
28210
  const approveModeSet = approveVariants && approveVariants.length > 0 ? { valid: approveVariants.map((v2) => v2.id), fallback: approveVariants[0]?.id ?? "default" } : { valid: ["default"], fallback: "default" };
28014
28211
  function raceDecision(id, ms2) {
28015
- return new Promise((resolve) => {
28016
- const t = setTimeout(() => resolve(null), ms2);
28212
+ return new Promise((resolve2) => {
28213
+ const t = setTimeout(() => resolve2(null), ms2);
28017
28214
  awaitDecision(id).then((d) => {
28018
28215
  clearTimeout(t);
28019
- resolve(d);
28216
+ resolve2(d);
28020
28217
  });
28021
28218
  });
28022
28219
  }
@@ -28148,6 +28345,34 @@ function createServer(opts) {
28148
28345
  const r = store.get(id);
28149
28346
  return r ? Response.json(toClientReview(r)) : notFound();
28150
28347
  }
28348
+ async function handleFileRefs(req, id) {
28349
+ const r = store.get(id);
28350
+ if (!r)
28351
+ return notFound();
28352
+ const { paths } = await parseBody(req, FileRefsBodySchema);
28353
+ const seen = new Set;
28354
+ const resolved = [];
28355
+ for (const p of paths.slice(0, MAX_FILE_REFS)) {
28356
+ if (seen.has(p))
28357
+ continue;
28358
+ seen.add(p);
28359
+ if (resolveFileInCwd(r.cwd, p) !== null)
28360
+ resolved.push(p);
28361
+ }
28362
+ log.debug("request", `file-refs resolved: ${resolved.length}/${seen.size}`, { reviewId: id });
28363
+ return Response.json({ resolved });
28364
+ }
28365
+ function handleFileExcerpt(req, id) {
28366
+ const r = store.get(id);
28367
+ if (!r)
28368
+ return notFound();
28369
+ const params = new URL(req.url).searchParams;
28370
+ const path = params.get("path") ?? "";
28371
+ const lineRaw = params.get("line");
28372
+ const line = lineRaw !== null && /^\d+$/.test(lineRaw) ? Number(lineRaw) : undefined;
28373
+ const excerpt = readFileExcerpt(r.cwd, path, line);
28374
+ return excerpt ? Response.json(excerpt) : notFound();
28375
+ }
28151
28376
  async function handleDecision(id) {
28152
28377
  const inMem = store.get(id);
28153
28378
  if (inMem?.decision) {
@@ -28181,8 +28406,9 @@ function createServer(opts) {
28181
28406
  if (body.generalCommentDraft != null) {
28182
28407
  r.generalCommentDraft = body.generalCommentDraft;
28183
28408
  }
28184
- if (body.composerScratches != null) {
28185
- r.composerScratches = body.composerScratches;
28409
+ const cur = currentVersion(r);
28410
+ if (body.composerScratches != null && (body.version == null || body.version === cur.version)) {
28411
+ cur.composerScratches = body.composerScratches;
28186
28412
  }
28187
28413
  });
28188
28414
  if (updated)
@@ -28204,7 +28430,7 @@ function createServer(opts) {
28204
28430
  r.decision = decision;
28205
28431
  r.status = decision.behavior === "allow" ? "approved" : "rejected";
28206
28432
  r.generalCommentDraft = "";
28207
- r.composerScratches = [];
28433
+ currentVersion(r).composerScratches = [];
28208
28434
  });
28209
28435
  if (decision.behavior === "allow") {
28210
28436
  store.bumpEpoch(existing.sessionId);
@@ -28257,6 +28483,10 @@ function createServer(opts) {
28257
28483
  const { id, sub } = route;
28258
28484
  if (method === "GET" && !sub)
28259
28485
  return handleGetReview(id);
28486
+ if (method === "GET" && sub === "/file")
28487
+ return handleFileExcerpt(req, id);
28488
+ if (method === "POST" && sub === "/file-refs")
28489
+ return handleFileRefs(req, id);
28260
28490
  if (method === "GET" && sub === "/decision")
28261
28491
  return handleDecision(id);
28262
28492
  if (method === "PUT" && sub === "/draft")
@@ -28347,7 +28577,7 @@ function createServer(opts) {
28347
28577
 
28348
28578
  // src/store.ts
28349
28579
  import { readdir, readFile as readFile3, writeFile as writeFile2 } from "fs/promises";
28350
- import { join as join6 } from "path";
28580
+ import { join as join7 } from "path";
28351
28581
  function createStore(dir, log = noopLogger) {
28352
28582
  const reviews = new Map;
28353
28583
  const epochs = new Map;
@@ -28356,7 +28586,7 @@ function createStore(dir, log = noopLogger) {
28356
28586
  const prev = writeChains.get(review.id) ?? Promise.resolve();
28357
28587
  const next = prev.catch(() => {}).then(async () => {
28358
28588
  ensureStateDir(dir);
28359
- await writeFile2(join6(dir, `${review.id}.json`), JSON.stringify(review, null, 2), {
28589
+ await writeFile2(join7(dir, `${review.id}.json`), JSON.stringify(review, null, 2), {
28360
28590
  mode: 384
28361
28591
  });
28362
28592
  log.debug("store", `review persisted: ${shortId(review.id)}`, { reviewId: review.id });
@@ -28399,14 +28629,14 @@ function createStore(dir, log = noopLogger) {
28399
28629
  return;
28400
28630
  review.status = "expired";
28401
28631
  review.generalCommentDraft = "";
28402
- review.composerScratches = [];
28632
+ currentVersion(review).composerScratches = [];
28403
28633
  review.updatedAt = Math.max(Date.now(), review.updatedAt + 1);
28404
28634
  reviews.delete(id);
28405
28635
  await persist(review);
28406
28636
  return review;
28407
28637
  },
28408
28638
  async persisted(id) {
28409
- return await readJsonFile(join6(dir, `${id}.json`)) ?? undefined;
28639
+ return await readJsonFile(join7(dir, `${id}.json`)) ?? undefined;
28410
28640
  },
28411
28641
  size() {
28412
28642
  return reviews.size;
@@ -28433,7 +28663,7 @@ function createStore(dir, log = noopLogger) {
28433
28663
  if (!file2.endsWith(".json"))
28434
28664
  continue;
28435
28665
  try {
28436
- const review = JSON.parse(await readFile3(join6(dir, file2), "utf-8"));
28666
+ const review = JSON.parse(await readFile3(join7(dir, file2), "utf-8"));
28437
28667
  if (isUnresolved(review.status)) {
28438
28668
  reviews.set(review.id, review);
28439
28669
  loaded++;
@@ -28481,7 +28711,7 @@ async function runDaemon(opts) {
28481
28711
  buildId: await currentBuildId(),
28482
28712
  commit: currentCommit(),
28483
28713
  stateDir: stateDir(),
28484
- instanceId: randomUUID2().slice(0, 8),
28714
+ instanceId: randomUUID().slice(0, 8),
28485
28715
  approveVariants: adapter.approveVariants,
28486
28716
  log
28487
28717
  });
@@ -28523,8 +28753,8 @@ import { existsSync as existsSync4 } from "fs";
28523
28753
  import { release } from "os";
28524
28754
 
28525
28755
  // src/discovery.ts
28526
- import { readdirSync as readdirSync2, statSync as statSync3 } from "fs";
28527
- import { basename, join as join7 } from "path";
28756
+ import { readdirSync as readdirSync3, statSync as statSync4 } from "fs";
28757
+ import { basename, join as join8 } from "path";
28528
28758
  var CARET_BIN = "caret-native";
28529
28759
  async function safe(build) {
28530
28760
  try {
@@ -28724,7 +28954,7 @@ function listReviewFiles() {
28724
28954
  const dir = reviewsDir();
28725
28955
  let files;
28726
28956
  try {
28727
- files = readdirSync2(dir);
28957
+ files = readdirSync3(dir);
28728
28958
  } catch {
28729
28959
  return [];
28730
28960
  }
@@ -28734,7 +28964,7 @@ function listReviewFiles() {
28734
28964
  continue;
28735
28965
  if (out.length >= 5000)
28736
28966
  break;
28737
- const raw = readJsonFileSync(join7(dir, file2));
28967
+ const raw = readJsonFileSync(join8(dir, file2));
28738
28968
  if (raw && typeof raw.id === "string" && typeof raw.status === "string") {
28739
28969
  out.push({ id: raw.id, status: raw.status });
28740
28970
  }
@@ -28745,7 +28975,7 @@ var TAIL_BYTES = 256 * 1024;
28745
28975
  async function logStats(path) {
28746
28976
  let size;
28747
28977
  try {
28748
- size = statSync3(path).size;
28978
+ size = statSync4(path).size;
28749
28979
  } catch {
28750
28980
  return { path, exists: false, size: 0, errors: 0, warns: 0 };
28751
28981
  }
@@ -28832,8 +29062,8 @@ async function runDiscoverySubcommand(opts) {
28832
29062
  }
28833
29063
 
28834
29064
  // src/commands/install-opencode.ts
28835
- import { existsSync as existsSync7, readFileSync as readFileSync7, rmSync as rmSync2, writeFileSync as writeFileSync5 } from "fs";
28836
- import { join as join9 } from "path";
29065
+ import { existsSync as existsSync7, readFileSync as readFileSync8, rmSync as rmSync2, writeFileSync as writeFileSync5 } from "fs";
29066
+ import { join as join10 } from "path";
28837
29067
 
28838
29068
  // src/adapters/opencode/deploy.ts
28839
29069
  import { existsSync as existsSync5, mkdirSync as mkdirSync2, rmSync, writeFileSync as writeFileSync4 } from "fs";
@@ -28892,27 +29122,27 @@ function removeFiles(targets, opts) {
28892
29122
  }
28893
29123
 
28894
29124
  // src/adapters/opencode/packaging.ts
28895
- import { existsSync as existsSync6, readFileSync as readFileSync6, readdirSync as readdirSync3 } from "fs";
28896
- import { dirname as dirname6, join as join8 } from "path";
29125
+ import { existsSync as existsSync6, readFileSync as readFileSync7, readdirSync as readdirSync4 } from "fs";
29126
+ import { dirname as dirname6, join as join9 } from "path";
28897
29127
  function resolveCaretRoot() {
28898
29128
  const candidates = [];
28899
29129
  const script = process.argv[1];
28900
29130
  if (script)
28901
- candidates.push(join8(dirname6(script), ".."));
28902
- candidates.push(join8(dirname6(process.execPath), ".."));
29131
+ candidates.push(join9(dirname6(script), ".."));
29132
+ candidates.push(join9(dirname6(process.execPath), ".."));
28903
29133
  for (const root of candidates) {
28904
- if (existsSync6(join8(root, "opencode", "caret.plugin.ts")))
29134
+ if (existsSync6(join9(root, "opencode", "caret.plugin.ts")))
28905
29135
  return root;
28906
29136
  }
28907
29137
  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.`);
28908
29138
  }
28909
29139
  function loadOpencodePackaging(root = resolveCaretRoot()) {
28910
- const pluginSource = readFileSync6(join8(root, "opencode", "caret.plugin.ts"), "utf-8");
28911
- const binPath = join8(root, "bin", "caret");
28912
- const commandsDir = join8(root, "opencode", "commands");
29140
+ const pluginSource = readFileSync7(join9(root, "opencode", "caret.plugin.ts"), "utf-8");
29141
+ const binPath = join9(root, "bin", "caret");
29142
+ const commandsDir = join9(root, "opencode", "commands");
28913
29143
  let commands = [];
28914
29144
  try {
28915
- commands = readdirSync3(commandsDir).filter((f) => f.endsWith(".md")).sort().map((f) => ({ name: f, contents: readFileSync6(join8(commandsDir, f), "utf-8") }));
29145
+ commands = readdirSync4(commandsDir).filter((f) => f.endsWith(".md")).sort().map((f) => ({ name: f, contents: readFileSync7(join9(commandsDir, f), "utf-8") }));
28916
29146
  } catch {
28917
29147
  commands = [];
28918
29148
  }
@@ -28924,7 +29154,7 @@ function runInstallOpencodeSubcommand(opts, deps = {}) {
28924
29154
  const dir = deps.configDir ?? opencodeConfigDir();
28925
29155
  const pkg = deps.packaging ?? loadOpencodePackaging();
28926
29156
  const pluginPath = pluginFilePath(dir);
28927
- const commandPaths = pkg.commands.map((c) => join9(commandDir(dir), namespacedCommandFilename(c.name)));
29157
+ const commandPaths = pkg.commands.map((c) => join10(commandDir(dir), namespacedCommandFilename(c.name)));
28928
29158
  if (opts.uninstall) {
28929
29159
  const result2 = removeFiles([pluginPath, ...commandPaths], { dryRun: opts.dryRun });
28930
29160
  const manifest2 = uninstallManifest(dir, opts.dryRun);
@@ -28937,7 +29167,7 @@ function runInstallOpencodeSubcommand(opts, deps = {}) {
28937
29167
  contents: stripNonDefaultExports(renderPlugin(pkg.pluginSource, { version: VERSION, binPath: pkg.binPath }))
28938
29168
  },
28939
29169
  ...pkg.commands.map((c) => ({
28940
- path: join9(commandDir(dir), namespacedCommandFilename(c.name)),
29170
+ path: join10(commandDir(dir), namespacedCommandFilename(c.name)),
28941
29171
  contents: renderPlugin(c.contents, { version: VERSION, binPath: pkg.binPath })
28942
29172
  }))
28943
29173
  ];
@@ -28974,7 +29204,7 @@ var bunInstall = (configDir2) => {
28974
29204
  };
28975
29205
  function installManifest(dir, dryRun) {
28976
29206
  const path = packageJsonPath(dir);
28977
- const existing = existsSync7(path) ? readFileSync7(path, "utf-8") : null;
29207
+ const existing = existsSync7(path) ? readFileSync8(path, "utf-8") : null;
28978
29208
  let next;
28979
29209
  try {
28980
29210
  next = addPluginDependency(existing, OPENCODE_PLUGIN_DEP, OPENCODE_PLUGIN_DEP_VERSION);
@@ -28991,7 +29221,7 @@ function uninstallManifest(dir, dryRun) {
28991
29221
  const path = packageJsonPath(dir);
28992
29222
  if (!existsSync7(path))
28993
29223
  return [];
28994
- const existing = readFileSync7(path, "utf-8");
29224
+ const existing = readFileSync8(path, "utf-8");
28995
29225
  let next;
28996
29226
  try {
28997
29227
  next = removePluginDependency(existing, OPENCODE_PLUGIN_DEP);
@@ -29029,6 +29259,51 @@ async function runPrewarm() {
29029
29259
  process.exit(0);
29030
29260
  }
29031
29261
 
29262
+ // src/reconcile.ts
29263
+ async function runReconcile(stdin, deps) {
29264
+ const ctx = {};
29265
+ try {
29266
+ const input = deps.parseHookInput(stdin);
29267
+ ctx.sessionId = input.sessionId;
29268
+ if (!input.sessionId)
29269
+ return;
29270
+ const match = (await deps.listReviews()).find((r) => r.sessionId === input.sessionId);
29271
+ if (!match) {
29272
+ logDebug("reconcile", "no pending review for session; nothing to reconcile", { ...ctx });
29273
+ return;
29274
+ }
29275
+ ctx.reviewId = match.id;
29276
+ await deps.resolveReview(match.id);
29277
+ logInfo("reconcile", "terminal approval reconciled", { ...ctx });
29278
+ } catch (err) {
29279
+ logDebug("reconcile", "reconcile skipped", { ...ctx, err: String(err) });
29280
+ }
29281
+ }
29282
+
29283
+ // src/commands/reconcile.ts
29284
+ function prodReconcileDeps(baseUrl, adapter) {
29285
+ return {
29286
+ parseHookInput: (stdin) => adapter.parseHookInput(stdin),
29287
+ listReviews: () => listReviews(baseUrl),
29288
+ resolveReview: (id) => resolveReview(baseUrl, id, { behavior: "allow" })
29289
+ };
29290
+ }
29291
+ async function runReconcileSubcommand() {
29292
+ try {
29293
+ const loaded = loadSettings();
29294
+ setLogLevel(loaded.logging.level);
29295
+ setRedact(loaded.logging.redact);
29296
+ warnInvalidEnvVars((msg) => logWarn("env", msg));
29297
+ const adapter = selectAdapter();
29298
+ const baseUrl = `http://localhost:${getPort(loaded)}`;
29299
+ const stdin = await Bun.stdin.text();
29300
+ await runReconcile(stdin, prodReconcileDeps(baseUrl, adapter));
29301
+ } catch (err) {
29302
+ logDebug("reconcile", "reconcile subcommand skipped", { err: String(err) });
29303
+ }
29304
+ process.exit(0);
29305
+ }
29306
+
29032
29307
  // src/commands/redact.ts
29033
29308
  function runRedactSubcommand() {
29034
29309
  try {
@@ -30404,17 +30679,24 @@ function denyDecision(reason) {
30404
30679
  class TimeoutError extends Error {
30405
30680
  }
30406
30681
  function withTimeout(p, ms2, message) {
30407
- return new Promise((resolve, reject) => {
30682
+ return new Promise((resolve2, reject) => {
30408
30683
  const t = setTimeout(() => reject(new TimeoutError(message)), ms2);
30409
30684
  p.then((v3) => {
30410
30685
  clearTimeout(t);
30411
- resolve(v3);
30686
+ resolve2(v3);
30412
30687
  }, (e) => {
30413
30688
  clearTimeout(t);
30414
30689
  reject(e);
30415
30690
  });
30416
30691
  });
30417
30692
  }
30693
+ async function expireAbandoned(expire, handle) {
30694
+ if (!handle)
30695
+ return;
30696
+ try {
30697
+ await expire(handle.baseUrl, handle.id);
30698
+ } catch {}
30699
+ }
30418
30700
  async function runReview(stdin, deps) {
30419
30701
  let step = "parse";
30420
30702
  const ctx = {};
@@ -30434,6 +30716,7 @@ async function runReview(stdin, deps) {
30434
30716
  step = "postReview";
30435
30717
  const { id, hasLiveClient } = await deps.postReview(baseUrl, input);
30436
30718
  ctx.reviewId = id;
30719
+ deps.onPosted?.(baseUrl, id);
30437
30720
  logDebug("review", `review created: ${shortId(id)}`, { ...ctx });
30438
30721
  const open = new URL(baseUrl);
30439
30722
  open.hostname = VANITY_HOST;
@@ -30509,6 +30792,7 @@ async function runReviewSubcommand() {
30509
30792
  warnInvalidEnvVars((msg) => logWarn("env", msg));
30510
30793
  const adapter = selectAdapter();
30511
30794
  let hookInput;
30795
+ let posted;
30512
30796
  let responded = false;
30513
30797
  const respond = (decision) => {
30514
30798
  if (responded)
@@ -30517,21 +30801,26 @@ async function runReviewSubcommand() {
30517
30801
  process.stdout.write(`${adapter.emitDecision(decision, hookInput)}
30518
30802
  `);
30519
30803
  };
30520
- const denyAndExit = (reason) => {
30804
+ const denyAndExit = async (reason) => {
30521
30805
  if (!responded)
30522
30806
  logError("signal", new Error(reason));
30523
30807
  respond({ behavior: "deny", feedback: `${reason} See ${logFile()}.`, decidedAt: Date.now() });
30808
+ await expireAbandoned(expireReview, posted);
30524
30809
  process.exit(0);
30525
30810
  };
30526
- process.once("SIGINT", () => denyAndExit("caret: interrupted (SIGINT) \u2014 denying to fail safe."));
30527
- process.once("SIGTERM", () => denyAndExit("caret: terminated (SIGTERM) \u2014 denying to fail safe."));
30811
+ process.once("SIGINT", () => void denyAndExit("caret: interrupted (SIGINT) \u2014 denying to fail safe."));
30812
+ process.once("SIGTERM", () => void denyAndExit("caret: terminated (SIGTERM) \u2014 denying to fail safe."));
30528
30813
  const stdin = await Bun.stdin.text();
30529
30814
  try {
30530
30815
  hookInput = adapter.parseHookInput(stdin);
30531
30816
  } catch {
30532
30817
  hookInput = undefined;
30533
30818
  }
30534
- const out = await runReview(stdin, prodReviewDeps(loaded, adapter));
30819
+ const deps = prodReviewDeps(loaded, adapter);
30820
+ deps.onPosted = (baseUrl, id) => {
30821
+ posted = { baseUrl, id };
30822
+ };
30823
+ const out = await runReview(stdin, deps);
30535
30824
  respond(out);
30536
30825
  process.exit(0);
30537
30826
  }
@@ -32589,10 +32878,11 @@ function runProgram(program2, onError) {
32589
32878
 
32590
32879
  // src/cli.ts
32591
32880
  function buildProgram() {
32592
- const program2 = createProgram("caret", "caret hook CLI: daemon | prewarm | review | redact | discovery | install-opencode").version(VERSION);
32881
+ const program2 = createProgram("caret", "caret hook CLI: daemon | prewarm | review | reconcile | redact | discovery | install-opencode").version(VERSION);
32593
32882
  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 }));
32594
32883
  program2.command("prewarm").description("warm-start the daemon").action(() => runPrewarm());
32595
32884
  program2.command("review").description("review a plan from stdin (ExitPlanMode hook)").action(() => runReviewSubcommand());
32885
+ program2.command("reconcile").description("reconcile a terminal plan approval into the daemon (ExitPlanMode PostToolUse hook)").action(() => runReconcileSubcommand());
32596
32886
  program2.command("redact").description("write redacted copies of the logs").action(() => runRedactSubcommand());
32597
32887
  program2.command("discovery").description("print a diagnostics report").option("--json", "emit the machine-readable JSON document").action((opts) => runDiscoverySubcommand({ json: opts.json ?? false }));
32598
32888
  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({