@macintacos/caret 0.1.3 → 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 +331 -49
  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
@@ -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.2.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.",
@@ -5980,7 +5980,7 @@ function currentCommit() {
5980
5980
  }
5981
5981
 
5982
5982
  // src/commands/daemon.ts
5983
- import { randomUUID as randomUUID2 } from "crypto";
5983
+ import { randomUUID } from "crypto";
5984
5984
  import { existsSync as existsSync3, unlinkSync as unlinkSync3 } from "fs";
5985
5985
 
5986
5986
  // src/daemon-lifecycle.ts
@@ -6026,6 +6026,22 @@ async function longPoll(baseUrl, id) {
6026
6026
  throw new Error(`decision long-poll failed: ${res.status}`);
6027
6027
  return await res.json();
6028
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
+ }
6029
6045
 
6030
6046
  // src/log.ts
6031
6047
  var import_pino = __toESM(require_pino(), 1);
@@ -6202,7 +6218,7 @@ function toClientReview(review) {
6202
6218
  annotations: cur.annotations,
6203
6219
  versions: review.versions,
6204
6220
  generalCommentDraft: review.generalCommentDraft ?? "",
6205
- composerScratches: review.composerScratches ?? [],
6221
+ composerScratches: cur.composerScratches ?? [],
6206
6222
  createdAt: review.createdAt,
6207
6223
  updatedAt: review.updatedAt,
6208
6224
  decision: review.decision
@@ -20954,6 +20970,172 @@ function createDecisions(log = noopLogger) {
20954
20970
  };
20955
20971
  }
20956
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
+
20957
21139
  // src/prefs.ts
20958
21140
  import { readFile as readFile2, writeFile } from "fs/promises";
20959
21141
  import { dirname as dirname3 } from "path";
@@ -20983,17 +21165,17 @@ async function writeApproveMode(mode, file2 = prefsFile(), log = noopLogger, set
20983
21165
  }
20984
21166
 
20985
21167
  // src/reviews.ts
20986
- import { randomUUID } from "crypto";
21168
+ import { randomBytes } from "crypto";
20987
21169
 
20988
21170
  // src/plan-file.ts
20989
- 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";
20990
21172
  function writeCanonicalPlanFile(planFilePath, canonical, log) {
20991
21173
  if (!planFilePath)
20992
21174
  return;
20993
21175
  try {
20994
21176
  if (!planFilePath.endsWith(".md"))
20995
21177
  return;
20996
- if (!existsSync2(planFilePath) || !statSync2(planFilePath).isFile())
21178
+ if (!existsSync2(planFilePath) || !statSync3(planFilePath).isFile())
20997
21179
  return;
20998
21180
  writeFileSync2(planFilePath, canonical);
20999
21181
  } catch (err) {
@@ -27764,6 +27946,9 @@ async function formatPlanMarkdown(plan, log = noopLogger, doFormat = prettierFor
27764
27946
  }
27765
27947
 
27766
27948
  // src/reviews.ts
27949
+ function newReviewId() {
27950
+ return randomBytes(8).toString("base64url");
27951
+ }
27767
27952
  function deriveTitle(plan) {
27768
27953
  for (const line of plan.split(`
27769
27954
  `)) {
@@ -27812,7 +27997,7 @@ async function routeIncomingPlan(input, store, log = noopLogger) {
27812
27997
  expired
27813
27998
  };
27814
27999
  }
27815
- const id = randomUUID();
28000
+ const id = newReviewId();
27816
28001
  const planEpoch = store.epochOf(sessionId);
27817
28002
  const review = {
27818
28003
  id,
@@ -27921,6 +28106,8 @@ var PlanInputSchema = exports_external.object({
27921
28106
  plan: exports_external.string().optional(),
27922
28107
  planFilePath: exports_external.string().optional()
27923
28108
  }).catch({});
28109
+ var MAX_FILE_REFS = 200;
28110
+ var FileRefsBodySchema = exports_external.object({ paths: exports_external.array(exports_external.string()).catch([]) }).catch({ paths: [] });
27924
28111
  var BehaviorSchema = exports_external.enum(["allow", "deny"]);
27925
28112
  var ResolveBodySchema = exports_external.object({
27926
28113
  behavior: BehaviorSchema.catch("allow"),
@@ -27971,11 +28158,13 @@ var nullToUndefined = (v2) => v2 ?? undefined;
27971
28158
  var DraftBodySchema = exports_external.object({
27972
28159
  annotations: exports_external.array(AnnotationSchema).nullish().transform(nullToUndefined),
27973
28160
  generalCommentDraft: exports_external.string().nullish().transform(nullToUndefined),
27974
- 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)
27975
28163
  }).catch({
27976
28164
  annotations: undefined,
27977
28165
  generalCommentDraft: undefined,
27978
- composerScratches: undefined
28166
+ composerScratches: undefined,
28167
+ version: undefined
27979
28168
  });
27980
28169
  async function parseBody(req, schema) {
27981
28170
  return schema.parse(await req.json().catch(() => ({})));
@@ -27998,7 +28187,7 @@ function resolveOptions(opts) {
27998
28187
  log: opts.log ?? noopLogger
27999
28188
  };
28000
28189
  }
28001
- var ID_ROUTE_RE = /^\/api\/reviews\/([^/]+)(\/decision|\/resolve|\/draft|\/expire)?$/;
28190
+ var ID_ROUTE_RE = /^\/api\/reviews\/([^/]+)(\/decision|\/resolve|\/draft|\/expire|\/file-refs|\/file)?$/;
28002
28191
  function matchIdRoute(path) {
28003
28192
  const m = path.match(ID_ROUTE_RE);
28004
28193
  if (!m)
@@ -28012,11 +28201,11 @@ function createServer(opts) {
28012
28201
  const { awaitDecision, resolveDecision, clearDecision, openDecisionCount } = createDecisions(log);
28013
28202
  const approveModeSet = approveVariants && approveVariants.length > 0 ? { valid: approveVariants.map((v2) => v2.id), fallback: approveVariants[0]?.id ?? "default" } : { valid: ["default"], fallback: "default" };
28014
28203
  function raceDecision(id, ms2) {
28015
- return new Promise((resolve) => {
28016
- const t = setTimeout(() => resolve(null), ms2);
28204
+ return new Promise((resolve2) => {
28205
+ const t = setTimeout(() => resolve2(null), ms2);
28017
28206
  awaitDecision(id).then((d) => {
28018
28207
  clearTimeout(t);
28019
- resolve(d);
28208
+ resolve2(d);
28020
28209
  });
28021
28210
  });
28022
28211
  }
@@ -28148,6 +28337,34 @@ function createServer(opts) {
28148
28337
  const r = store.get(id);
28149
28338
  return r ? Response.json(toClientReview(r)) : notFound();
28150
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
+ }
28151
28368
  async function handleDecision(id) {
28152
28369
  const inMem = store.get(id);
28153
28370
  if (inMem?.decision) {
@@ -28181,8 +28398,9 @@ function createServer(opts) {
28181
28398
  if (body.generalCommentDraft != null) {
28182
28399
  r.generalCommentDraft = body.generalCommentDraft;
28183
28400
  }
28184
- if (body.composerScratches != null) {
28185
- 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;
28186
28404
  }
28187
28405
  });
28188
28406
  if (updated)
@@ -28204,7 +28422,7 @@ function createServer(opts) {
28204
28422
  r.decision = decision;
28205
28423
  r.status = decision.behavior === "allow" ? "approved" : "rejected";
28206
28424
  r.generalCommentDraft = "";
28207
- r.composerScratches = [];
28425
+ currentVersion(r).composerScratches = [];
28208
28426
  });
28209
28427
  if (decision.behavior === "allow") {
28210
28428
  store.bumpEpoch(existing.sessionId);
@@ -28257,6 +28475,10 @@ function createServer(opts) {
28257
28475
  const { id, sub } = route;
28258
28476
  if (method === "GET" && !sub)
28259
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);
28260
28482
  if (method === "GET" && sub === "/decision")
28261
28483
  return handleDecision(id);
28262
28484
  if (method === "PUT" && sub === "/draft")
@@ -28347,7 +28569,7 @@ function createServer(opts) {
28347
28569
 
28348
28570
  // src/store.ts
28349
28571
  import { readdir, readFile as readFile3, writeFile as writeFile2 } from "fs/promises";
28350
- import { join as join6 } from "path";
28572
+ import { join as join7 } from "path";
28351
28573
  function createStore(dir, log = noopLogger) {
28352
28574
  const reviews = new Map;
28353
28575
  const epochs = new Map;
@@ -28356,7 +28578,7 @@ function createStore(dir, log = noopLogger) {
28356
28578
  const prev = writeChains.get(review.id) ?? Promise.resolve();
28357
28579
  const next = prev.catch(() => {}).then(async () => {
28358
28580
  ensureStateDir(dir);
28359
- 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), {
28360
28582
  mode: 384
28361
28583
  });
28362
28584
  log.debug("store", `review persisted: ${shortId(review.id)}`, { reviewId: review.id });
@@ -28399,14 +28621,14 @@ function createStore(dir, log = noopLogger) {
28399
28621
  return;
28400
28622
  review.status = "expired";
28401
28623
  review.generalCommentDraft = "";
28402
- review.composerScratches = [];
28624
+ currentVersion(review).composerScratches = [];
28403
28625
  review.updatedAt = Math.max(Date.now(), review.updatedAt + 1);
28404
28626
  reviews.delete(id);
28405
28627
  await persist(review);
28406
28628
  return review;
28407
28629
  },
28408
28630
  async persisted(id) {
28409
- return await readJsonFile(join6(dir, `${id}.json`)) ?? undefined;
28631
+ return await readJsonFile(join7(dir, `${id}.json`)) ?? undefined;
28410
28632
  },
28411
28633
  size() {
28412
28634
  return reviews.size;
@@ -28433,7 +28655,7 @@ function createStore(dir, log = noopLogger) {
28433
28655
  if (!file2.endsWith(".json"))
28434
28656
  continue;
28435
28657
  try {
28436
- const review = JSON.parse(await readFile3(join6(dir, file2), "utf-8"));
28658
+ const review = JSON.parse(await readFile3(join7(dir, file2), "utf-8"));
28437
28659
  if (isUnresolved(review.status)) {
28438
28660
  reviews.set(review.id, review);
28439
28661
  loaded++;
@@ -28481,7 +28703,7 @@ async function runDaemon(opts) {
28481
28703
  buildId: await currentBuildId(),
28482
28704
  commit: currentCommit(),
28483
28705
  stateDir: stateDir(),
28484
- instanceId: randomUUID2().slice(0, 8),
28706
+ instanceId: randomUUID().slice(0, 8),
28485
28707
  approveVariants: adapter.approveVariants,
28486
28708
  log
28487
28709
  });
@@ -28523,8 +28745,8 @@ import { existsSync as existsSync4 } from "fs";
28523
28745
  import { release } from "os";
28524
28746
 
28525
28747
  // src/discovery.ts
28526
- import { readdirSync as readdirSync2, statSync as statSync3 } from "fs";
28527
- 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";
28528
28750
  var CARET_BIN = "caret-native";
28529
28751
  async function safe(build) {
28530
28752
  try {
@@ -28724,7 +28946,7 @@ function listReviewFiles() {
28724
28946
  const dir = reviewsDir();
28725
28947
  let files;
28726
28948
  try {
28727
- files = readdirSync2(dir);
28949
+ files = readdirSync3(dir);
28728
28950
  } catch {
28729
28951
  return [];
28730
28952
  }
@@ -28734,7 +28956,7 @@ function listReviewFiles() {
28734
28956
  continue;
28735
28957
  if (out.length >= 5000)
28736
28958
  break;
28737
- const raw = readJsonFileSync(join7(dir, file2));
28959
+ const raw = readJsonFileSync(join8(dir, file2));
28738
28960
  if (raw && typeof raw.id === "string" && typeof raw.status === "string") {
28739
28961
  out.push({ id: raw.id, status: raw.status });
28740
28962
  }
@@ -28745,7 +28967,7 @@ var TAIL_BYTES = 256 * 1024;
28745
28967
  async function logStats(path) {
28746
28968
  let size;
28747
28969
  try {
28748
- size = statSync3(path).size;
28970
+ size = statSync4(path).size;
28749
28971
  } catch {
28750
28972
  return { path, exists: false, size: 0, errors: 0, warns: 0 };
28751
28973
  }
@@ -28832,8 +29054,8 @@ async function runDiscoverySubcommand(opts) {
28832
29054
  }
28833
29055
 
28834
29056
  // 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";
29057
+ import { existsSync as existsSync7, readFileSync as readFileSync8, rmSync as rmSync2, writeFileSync as writeFileSync5 } from "fs";
29058
+ import { join as join10 } from "path";
28837
29059
 
28838
29060
  // src/adapters/opencode/deploy.ts
28839
29061
  import { existsSync as existsSync5, mkdirSync as mkdirSync2, rmSync, writeFileSync as writeFileSync4 } from "fs";
@@ -28892,27 +29114,27 @@ function removeFiles(targets, opts) {
28892
29114
  }
28893
29115
 
28894
29116
  // 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";
29117
+ import { existsSync as existsSync6, readFileSync as readFileSync7, readdirSync as readdirSync4 } from "fs";
29118
+ import { dirname as dirname6, join as join9 } from "path";
28897
29119
  function resolveCaretRoot() {
28898
29120
  const candidates = [];
28899
29121
  const script = process.argv[1];
28900
29122
  if (script)
28901
- candidates.push(join8(dirname6(script), ".."));
28902
- candidates.push(join8(dirname6(process.execPath), ".."));
29123
+ candidates.push(join9(dirname6(script), ".."));
29124
+ candidates.push(join9(dirname6(process.execPath), ".."));
28903
29125
  for (const root of candidates) {
28904
- if (existsSync6(join8(root, "opencode", "caret.plugin.ts")))
29126
+ if (existsSync6(join9(root, "opencode", "caret.plugin.ts")))
28905
29127
  return root;
28906
29128
  }
28907
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.`);
28908
29130
  }
28909
29131
  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");
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");
28913
29135
  let commands = [];
28914
29136
  try {
28915
- 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") }));
28916
29138
  } catch {
28917
29139
  commands = [];
28918
29140
  }
@@ -28924,7 +29146,7 @@ function runInstallOpencodeSubcommand(opts, deps = {}) {
28924
29146
  const dir = deps.configDir ?? opencodeConfigDir();
28925
29147
  const pkg = deps.packaging ?? loadOpencodePackaging();
28926
29148
  const pluginPath = pluginFilePath(dir);
28927
- 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)));
28928
29150
  if (opts.uninstall) {
28929
29151
  const result2 = removeFiles([pluginPath, ...commandPaths], { dryRun: opts.dryRun });
28930
29152
  const manifest2 = uninstallManifest(dir, opts.dryRun);
@@ -28937,7 +29159,7 @@ function runInstallOpencodeSubcommand(opts, deps = {}) {
28937
29159
  contents: stripNonDefaultExports(renderPlugin(pkg.pluginSource, { version: VERSION, binPath: pkg.binPath }))
28938
29160
  },
28939
29161
  ...pkg.commands.map((c) => ({
28940
- path: join9(commandDir(dir), namespacedCommandFilename(c.name)),
29162
+ path: join10(commandDir(dir), namespacedCommandFilename(c.name)),
28941
29163
  contents: renderPlugin(c.contents, { version: VERSION, binPath: pkg.binPath })
28942
29164
  }))
28943
29165
  ];
@@ -28974,7 +29196,7 @@ var bunInstall = (configDir2) => {
28974
29196
  };
28975
29197
  function installManifest(dir, dryRun) {
28976
29198
  const path = packageJsonPath(dir);
28977
- const existing = existsSync7(path) ? readFileSync7(path, "utf-8") : null;
29199
+ const existing = existsSync7(path) ? readFileSync8(path, "utf-8") : null;
28978
29200
  let next;
28979
29201
  try {
28980
29202
  next = addPluginDependency(existing, OPENCODE_PLUGIN_DEP, OPENCODE_PLUGIN_DEP_VERSION);
@@ -28991,7 +29213,7 @@ function uninstallManifest(dir, dryRun) {
28991
29213
  const path = packageJsonPath(dir);
28992
29214
  if (!existsSync7(path))
28993
29215
  return [];
28994
- const existing = readFileSync7(path, "utf-8");
29216
+ const existing = readFileSync8(path, "utf-8");
28995
29217
  let next;
28996
29218
  try {
28997
29219
  next = removePluginDependency(existing, OPENCODE_PLUGIN_DEP);
@@ -29029,6 +29251,51 @@ async function runPrewarm() {
29029
29251
  process.exit(0);
29030
29252
  }
29031
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
+
29032
29299
  // src/commands/redact.ts
29033
29300
  function runRedactSubcommand() {
29034
29301
  try {
@@ -30404,17 +30671,24 @@ function denyDecision(reason) {
30404
30671
  class TimeoutError extends Error {
30405
30672
  }
30406
30673
  function withTimeout(p, ms2, message) {
30407
- return new Promise((resolve, reject) => {
30674
+ return new Promise((resolve2, reject) => {
30408
30675
  const t = setTimeout(() => reject(new TimeoutError(message)), ms2);
30409
30676
  p.then((v3) => {
30410
30677
  clearTimeout(t);
30411
- resolve(v3);
30678
+ resolve2(v3);
30412
30679
  }, (e) => {
30413
30680
  clearTimeout(t);
30414
30681
  reject(e);
30415
30682
  });
30416
30683
  });
30417
30684
  }
30685
+ async function expireAbandoned(expire, handle) {
30686
+ if (!handle)
30687
+ return;
30688
+ try {
30689
+ await expire(handle.baseUrl, handle.id);
30690
+ } catch {}
30691
+ }
30418
30692
  async function runReview(stdin, deps) {
30419
30693
  let step = "parse";
30420
30694
  const ctx = {};
@@ -30434,6 +30708,7 @@ async function runReview(stdin, deps) {
30434
30708
  step = "postReview";
30435
30709
  const { id, hasLiveClient } = await deps.postReview(baseUrl, input);
30436
30710
  ctx.reviewId = id;
30711
+ deps.onPosted?.(baseUrl, id);
30437
30712
  logDebug("review", `review created: ${shortId(id)}`, { ...ctx });
30438
30713
  const open = new URL(baseUrl);
30439
30714
  open.hostname = VANITY_HOST;
@@ -30509,6 +30784,7 @@ async function runReviewSubcommand() {
30509
30784
  warnInvalidEnvVars((msg) => logWarn("env", msg));
30510
30785
  const adapter = selectAdapter();
30511
30786
  let hookInput;
30787
+ let posted;
30512
30788
  let responded = false;
30513
30789
  const respond = (decision) => {
30514
30790
  if (responded)
@@ -30517,21 +30793,26 @@ async function runReviewSubcommand() {
30517
30793
  process.stdout.write(`${adapter.emitDecision(decision, hookInput)}
30518
30794
  `);
30519
30795
  };
30520
- const denyAndExit = (reason) => {
30796
+ const denyAndExit = async (reason) => {
30521
30797
  if (!responded)
30522
30798
  logError("signal", new Error(reason));
30523
30799
  respond({ behavior: "deny", feedback: `${reason} See ${logFile()}.`, decidedAt: Date.now() });
30800
+ await expireAbandoned(expireReview, posted);
30524
30801
  process.exit(0);
30525
30802
  };
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."));
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."));
30528
30805
  const stdin = await Bun.stdin.text();
30529
30806
  try {
30530
30807
  hookInput = adapter.parseHookInput(stdin);
30531
30808
  } catch {
30532
30809
  hookInput = undefined;
30533
30810
  }
30534
- const out = await runReview(stdin, prodReviewDeps(loaded, adapter));
30811
+ const deps = prodReviewDeps(loaded, adapter);
30812
+ deps.onPosted = (baseUrl, id) => {
30813
+ posted = { baseUrl, id };
30814
+ };
30815
+ const out = await runReview(stdin, deps);
30535
30816
  respond(out);
30536
30817
  process.exit(0);
30537
30818
  }
@@ -32589,10 +32870,11 @@ function runProgram(program2, onError) {
32589
32870
 
32590
32871
  // src/cli.ts
32591
32872
  function buildProgram() {
32592
- 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);
32593
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 }));
32594
32875
  program2.command("prewarm").description("warm-start the daemon").action(() => runPrewarm());
32595
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());
32596
32878
  program2.command("redact").description("write redacted copies of the logs").action(() => runRedactSubcommand());
32597
32879
  program2.command("discovery").description("print a diagnostics report").option("--json", "emit the machine-readable JSON document").action((opts) => runDiscoverySubcommand({ json: opts.json ?? false }));
32598
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({