@erdemtuna/doc-review 0.9.0 → 0.10.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 (93) hide show
  1. package/README.md +52 -227
  2. package/lib/anchor-text.js +155 -0
  3. package/lib/atomic-write.js +54 -0
  4. package/lib/chrome-api.js +78 -0
  5. package/lib/chrome-client.js +2255 -0
  6. package/lib/chrome-session.js +76 -0
  7. package/lib/cli.js +255 -0
  8. package/lib/click-target.js +28 -0
  9. package/lib/comment-anchor.js +23 -0
  10. package/lib/comment-target.js +164 -0
  11. package/lib/comparison-view.js +380 -0
  12. package/lib/contracts/feedback.js +1 -0
  13. package/lib/contracts/frame.js +7 -0
  14. package/lib/contracts/history.js +1 -0
  15. package/lib/contracts/index.js +2 -0
  16. package/lib/contracts/page.js +23 -0
  17. package/lib/document-execution.js +115 -0
  18. package/lib/edit-limits.js +24 -0
  19. package/lib/editing.js +107 -0
  20. package/lib/execution-client.js +69 -0
  21. package/lib/feedback-controller.js +106 -0
  22. package/lib/frame-channel.js +42 -0
  23. package/lib/frame-controller.js +313 -0
  24. package/lib/frame-host.js +136 -0
  25. package/lib/frame-policy.js +50 -0
  26. package/lib/history-client.js +112 -0
  27. package/lib/history-coordinator.js +210 -0
  28. package/lib/history-policy.js +43 -0
  29. package/lib/history-server.js +464 -0
  30. package/lib/html-transform.js +52 -0
  31. package/lib/icons.js +249 -0
  32. package/{src → lib}/markdown.js +25 -26
  33. package/lib/paths.js +83 -0
  34. package/lib/poll-transport.js +220 -0
  35. package/lib/positioning.js +97 -0
  36. package/lib/review-controller.js +93 -0
  37. package/lib/review-mode.js +66 -0
  38. package/lib/revision-diff.js +482 -0
  39. package/lib/revision-schema.js +235 -0
  40. package/lib/revision-store.js +190 -0
  41. package/lib/save-controller.js +300 -0
  42. package/lib/sdk.js +2629 -0
  43. package/lib/semantic-snapshot.js +268 -0
  44. package/lib/serialize.js +32 -0
  45. package/lib/server-entry.js +26 -0
  46. package/lib/server-lock.js +109 -0
  47. package/lib/server.js +1501 -0
  48. package/lib/setup-guidance.js +119 -0
  49. package/{src → lib}/setup.js +44 -55
  50. package/lib/state.js +733 -0
  51. package/lib/view-identity.js +112 -0
  52. package/package.json +24 -14
  53. package/src/anchor-text.js +0 -160
  54. package/src/atomic-write.js +0 -53
  55. package/src/chrome-client.js +0 -2747
  56. package/src/chrome-session.js +0 -85
  57. package/src/cli.js +0 -271
  58. package/src/click-target.js +0 -26
  59. package/src/comment-anchor.js +0 -22
  60. package/src/comment-target.js +0 -164
  61. package/src/comparison-view.js +0 -337
  62. package/src/document-execution.js +0 -103
  63. package/src/edit-limits.js +0 -23
  64. package/src/editing.js +0 -99
  65. package/src/execution-client.js +0 -63
  66. package/src/frame-channel.js +0 -44
  67. package/src/frame-policy.js +0 -54
  68. package/src/history-client.js +0 -104
  69. package/src/history-coordinator.js +0 -186
  70. package/src/history-policy.js +0 -43
  71. package/src/history-server.js +0 -463
  72. package/src/html-transform.js +0 -62
  73. package/src/icons.js +0 -251
  74. package/src/paths.js +0 -91
  75. package/src/poll-transport.js +0 -222
  76. package/src/positioning.js +0 -107
  77. package/src/review-mode.js +0 -59
  78. package/src/revision-diff.js +0 -440
  79. package/src/revision-schema.js +0 -219
  80. package/src/revision-store.js +0 -177
  81. package/src/sdk.js +0 -2607
  82. package/src/semantic-snapshot.js +0 -230
  83. package/src/serialize.js +0 -32
  84. package/src/server-entry.js +0 -26
  85. package/src/server-lock.js +0 -101
  86. package/src/server.js +0 -1466
  87. package/src/setup-guidance.js +0 -128
  88. package/src/state.js +0 -732
  89. package/src/view-identity.js +0 -99
  90. /package/{src → lib}/SKILL.md +0 -0
  91. /package/{src → lib}/chrome.css +0 -0
  92. /package/{src → lib}/chrome.html +0 -0
  93. /package/{src → lib}/document-trust.js +0 -0
@@ -0,0 +1,119 @@
1
+ export const GUIDANCE_BEGIN = "<!-- BEGIN doc-review -->";
2
+ export const GUIDANCE_END = "<!-- END doc-review -->";
3
+ // Historical generated text is deliberately independent of the current guidance.
4
+ const LEGACY_TAIL = `
5
+ \`npx -y @erdemtuna/doc-review <file.html>\`. For a locally running web page, open the real
6
+ route with \`npx -y @erdemtuna/doc-review http://localhost:3000/path\` instead of recreating
7
+ it as a static file. Then block on
8
+ \`npx -y @erdemtuna/doc-review poll <target> --timeout 600\` until they send feedback.
9
+ If it prints \`{"status":"timeout"}\`, no feedback arrived yet — run the same
10
+ poll command again to keep waiting. When a \`{"status":"feedback"}\` batch
11
+ arrives, apply it, then run the exact acknowledgement command in its
12
+ \`next_step\`, which uses \`--ack <batch_id>\`.
13
+
14
+ Keep the poll command in the foreground and do not end the turn while it waits.
15
+ If the shell returns a process or session handle, keep waiting on that handle until
16
+ the command exits. \`npx -y @erdemtuna/doc-review status <target>\` reports instantly
17
+ whether feedback is already waiting, without blocking.
18
+
19
+ The batch groups feedback by page under \`pages\`, so fix every page listed. Items
20
+ under \`edits\` are changes the user already made: \`after\` is their exact wording,
21
+ so carry it across verbatim and never revert it — and if the HTML was generated
22
+ from MDX or Markdown, apply it to the source too. Markdown files open rendered
23
+ and are never written by doc-review: apply their comments and edits to the
24
+ Markdown source, keeping its syntax. There is no reply channel; the user sees
25
+ your work when the page reloads. For a localhost page, direct edits and deletions
26
+ arrive with \`kind: "url"\`; find and update the matching MDX, TSX, template, or
27
+ component source. Never write the rendered HTTP response over project source.`;
28
+ export const LEGACY_GUIDANCE = Object.freeze([
29
+ `## Reviewing files and localhost pages with doc-review
30
+
31
+ After writing an HTML or Markdown file the user will read, open it for them with${LEGACY_TAIL}`,
32
+ `## Reviewing files and localhost pages with doc-review
33
+
34
+ Start only when the user explicitly invokes /doc-review or requests an
35
+ interactive browser review. Writing, updating, discussing, or generically reviewing
36
+ content does not authorize opening a review or polling. Another skill's automatic
37
+ review step is not user permission. Otherwise respond normally.
38
+
39
+ After that explicit request, open the requested HTML or Markdown file with${LEGACY_TAIL}`,
40
+ ]);
41
+ const migrationMessage = "AGENTS.md contains custom or unrecognized doc-review guidance — left it unchanged. " +
42
+ "Manually reconcile that guidance, then wrap only the setup-owned section in " +
43
+ `${GUIDANCE_BEGIN} and ${GUIDANCE_END}, or remove it and re-run setup.`;
44
+ function invalidMarkers() {
45
+ throw new Error("AGENTS.md has malformed, duplicate, or ambiguous doc-review ownership markers. " +
46
+ "Keep exactly one standalone BEGIN/END pair in order, then re-run setup. No setup files were changed.");
47
+ }
48
+ function render(body, newline) {
49
+ return `${GUIDANCE_BEGIN}\n${body.trim()}\n${GUIDANCE_END}`.replaceAll("\n", newline);
50
+ }
51
+ /** Plan the complete edit before setup writes any files. Never normalize user text. */
52
+ export function updateGuidance(existing, body) {
53
+ const lines = [...existing.matchAll(/[^\n]*(?:\n|$)/g)]
54
+ .filter((match) => match[0])
55
+ .map((match) => ({
56
+ text: match[0].replace(/\r?\n$/, ""),
57
+ start: match.index,
58
+ end: match.index + match[0].replace(/\r?\n$/, "").length,
59
+ }));
60
+ const markers = lines.filter(({ text }) => /doc-review/i.test(text) &&
61
+ (/\b(?:BEGIN|END)\b/i.test(text) && /<!--|-->|^\s*(?:BEGIN|END)\b/i.test(text)));
62
+ if (markers.length) {
63
+ if (markers.length !== 2 || markers[0].text !== GUIDANCE_BEGIN || markers[1].text !== GUIDANCE_END) {
64
+ invalidMarkers();
65
+ }
66
+ const [begin, end] = markers;
67
+ const newline = existing.slice(begin.end).startsWith("\r\n") ? "\r\n" : "\n";
68
+ return {
69
+ contents: existing.slice(0, begin.start) + render(body, newline) + existing.slice(end.end),
70
+ message: "Updated setup-owned AGENTS.md guidance (Codex)",
71
+ };
72
+ }
73
+ const candidates = [];
74
+ for (const legacy of LEGACY_GUIDANCE) {
75
+ for (const command of ["npx -y @erdemtuna/doc-review", "doc-review"]) {
76
+ for (const newline of ["\n", "\r\n"]) {
77
+ const text = legacy.replaceAll("npx -y @erdemtuna/doc-review", command).replaceAll("\n", newline);
78
+ let start = existing.indexOf(text);
79
+ while (start !== -1) {
80
+ const end = start + text.length;
81
+ const before = existing.slice(0, start);
82
+ const after = existing.slice(end);
83
+ // A complete generated section, not a substring of customized prose.
84
+ if ((start === 0 || before.endsWith("\n") || before === "\uFEFF") &&
85
+ /^(?:\r?\n|$)/.test(after) &&
86
+ /^(?:\s*$|(?:\r?\n)+(?=#{1,2} ))/.test(after)) {
87
+ candidates.push({ start, end, newline });
88
+ }
89
+ start = existing.indexOf(text, start + text.length);
90
+ }
91
+ }
92
+ }
93
+ }
94
+ if (candidates.length > 1) {
95
+ throw new Error("AGENTS.md contains multiple legacy doc-review sections; ownership is ambiguous. " +
96
+ "Reconcile them before re-running setup. No setup files were changed.");
97
+ }
98
+ if (candidates.length === 1) {
99
+ const { start, end, newline } = candidates[0];
100
+ const surrounding = existing.slice(0, start) + existing.slice(end);
101
+ if (!/doc-review/i.test(surrounding)) {
102
+ return {
103
+ contents: existing.slice(0, start) + render(body, newline) + existing.slice(end),
104
+ message: "Migrated legacy AGENTS.md guidance to setup-owned markers (Codex)",
105
+ };
106
+ }
107
+ }
108
+ if (/doc-review/i.test(existing)) {
109
+ return { contents: existing, message: migrationMessage };
110
+ }
111
+ const newline = existing.includes("\r\n") ? "\r\n" : "\n";
112
+ const separator = !existing || existing.endsWith(newline + newline)
113
+ ? ""
114
+ : existing.endsWith("\n") ? newline : newline + newline;
115
+ return {
116
+ contents: existing + separator + render(body, newline) + newline,
117
+ message: `${existing ? "Updated" : "Created"} AGENTS.md (Codex)`,
118
+ };
119
+ }
@@ -4,12 +4,10 @@ import path from "node:path";
4
4
  import { spawnSync } from "node:child_process";
5
5
  import { fileURLToPath } from "node:url";
6
6
  import { updateGuidance } from "./setup-guidance.js";
7
-
8
7
  const here = path.dirname(fileURLToPath(import.meta.url));
9
8
  export const PACKAGE_NAME = "@erdemtuna/doc-review";
10
9
  export const COMMAND_NAME = "doc-review";
11
10
  export const NPX_COMMAND = `npx -y ${PACKAGE_NAME}`;
12
-
13
11
  /**
14
12
  * Teach agents the command that will actually work here. A global install or
15
13
  * `npm link` puts `doc-review` on PATH; otherwise fall back to npx, which only
@@ -22,32 +20,27 @@ export const NPX_COMMAND = `npx -y ${PACKAGE_NAME}`;
22
20
  * every later agent invocation dies with "command not found".
23
21
  */
24
22
  export function invocation(run = spawnSync) {
25
- const probe = process.platform === "win32" ? "where" : "which";
26
- const found = run(probe, [COMMAND_NAME], { encoding: "utf8", windowsHide: true });
27
- const resolved = found.status === 0 ? found.stdout.trim().split(/\r?\n/)[0].trim() : "";
28
- return resolved && !isNpxCachePath(resolved) ? COMMAND_NAME : NPX_COMMAND;
23
+ const probe = process.platform === "win32" ? "where" : "which";
24
+ const found = run(probe, [COMMAND_NAME], { encoding: "utf8", windowsHide: true });
25
+ const resolved = found.status === 0 ? found.stdout.trim().split(/\r?\n/)[0].trim() : "";
26
+ return resolved && !isNpxCachePath(resolved) ? COMMAND_NAME : NPX_COMMAND;
29
27
  }
30
-
31
28
  /** True for a binary npm placed in its transient `_npx` cache for one command. */
32
29
  export function isNpxCachePath(binPath) {
33
- return binPath.split(/[\\/]/).includes("_npx");
30
+ return binPath.split(/[\\/]/).includes("_npx");
34
31
  }
35
-
36
32
  /**
37
33
  * Quote a path for copy-paste into any shell. JSON.stringify would double
38
34
  * Windows backslashes; plain double quotes work in bash, zsh, cmd and
39
35
  * PowerShell alike, and paths cannot legally contain a double quote on Windows.
40
36
  */
41
37
  export function shellQuote(arg) {
42
- const text = String(arg);
43
- return /^[\w@%+=:,./-]+$/.test(text) ? text : `"${text.replaceAll('"', '\\"')}"`;
38
+ const text = String(arg);
39
+ return /^[\w@%+=:,./-]+$/.test(text) ? text : `"${text.replaceAll('"', '\\"')}"`;
44
40
  }
45
-
46
41
  /** The skill lives in its own markdown file so nothing needs escaping. */
47
42
  export const readSkill = () => fs.readFileSync(path.join(here, "SKILL.md"), "utf8");
48
-
49
43
  export const skillFor = (cmd) => readSkill().replaceAll(NPX_COMMAND, cmd);
50
-
51
44
  const CODEX_BLOCK = `
52
45
  ## Reviewing files and localhost pages with doc-review
53
46
 
@@ -97,47 +90,43 @@ your work when the page reloads. For a localhost page, direct edits and deletion
97
90
  arrive with \`kind: "url"\`; find and update the matching MDX, TSX, template, or
98
91
  component source. Never write the rendered HTTP response over project source.
99
92
  `;
100
-
101
93
  export function installSkills(cwd, { global: isGlobal = false, home = os.homedir(), command } = {}) {
102
- const done = [];
103
- const cmd = command || invocation();
104
- const agents = path.join(cwd, "AGENTS.md");
105
- let guidance;
106
- let existing;
107
- if (!isGlobal) {
108
- const bytes = fs.existsSync(agents) ? fs.readFileSync(agents) : Buffer.alloc(0);
109
- existing = bytes.toString("utf8");
110
- if (!Buffer.from(existing, "utf8").equals(bytes)) {
111
- throw new Error("AGENTS.md is not valid UTF-8; convert it before re-running setup. No setup files were changed.");
94
+ const done = [];
95
+ const cmd = command || invocation();
96
+ const agents = path.join(cwd, "AGENTS.md");
97
+ let guidance;
98
+ let existing;
99
+ if (!isGlobal) {
100
+ const bytes = fs.existsSync(agents) ? fs.readFileSync(agents) : Buffer.alloc(0);
101
+ existing = bytes.toString("utf8");
102
+ if (!Buffer.from(existing, "utf8").equals(bytes)) {
103
+ throw new Error("AGENTS.md is not valid UTF-8; convert it before re-running setup. No setup files were changed.");
104
+ }
105
+ guidance = updateGuidance(existing, CODEX_BLOCK.replaceAll(NPX_COMMAND, cmd));
112
106
  }
113
- guidance = updateGuidance(existing, CODEX_BLOCK.replaceAll(NPX_COMMAND, cmd));
114
- }
115
-
116
- const skillRoots = isGlobal
117
- ? [
118
- ["Claude Code", path.join(home, ".claude")],
119
- ["Codex", path.join(home, ".codex")],
120
- ["Shared agents", path.join(home, ".agents")],
121
- ]
122
- : [["Claude Code", path.join(cwd, ".claude")]];
123
-
124
- for (const [agent, base] of skillRoots) {
125
- const skillFile = path.join(base, "skills", "doc-review", "SKILL.md");
126
- fs.mkdirSync(path.dirname(skillFile), { recursive: true });
127
- fs.writeFileSync(skillFile, skillFor(cmd));
128
- done.push(`${agent} skill ${skillFile}${isGlobal ? " (all projects)" : ""}`);
129
- }
130
-
131
- if (!isGlobal) {
132
- if (guidance.contents !== existing) fs.writeFileSync(agents, guidance.contents);
133
- done.push(guidance.message);
134
- }
135
-
136
- done.push("", `Agents will be told to run: ${cmd}`);
137
- if (cmd.startsWith("npx")) {
138
- done.push(`Heads up: npx only works once ${PACKAGE_NAME} is published. Run \`npm link\` in the`);
139
- done.push("doc-review folder first if you want to use it locally, then re-run setup.");
140
- }
141
- done.push("Any other agent works too — see the JSON contract in the README.");
142
- return done;
107
+ const skillRoots = isGlobal
108
+ ? [
109
+ ["Claude Code", path.join(home, ".claude")],
110
+ ["Codex", path.join(home, ".codex")],
111
+ ["Shared agents", path.join(home, ".agents")],
112
+ ]
113
+ : [["Claude Code", path.join(cwd, ".claude")]];
114
+ for (const [agent, base] of skillRoots) {
115
+ const skillFile = path.join(base, "skills", "doc-review", "SKILL.md");
116
+ fs.mkdirSync(path.dirname(skillFile), { recursive: true });
117
+ fs.writeFileSync(skillFile, skillFor(cmd));
118
+ done.push(`${agent} skill ${skillFile}${isGlobal ? " (all projects)" : ""}`);
119
+ }
120
+ if (!isGlobal) {
121
+ if (guidance.contents !== existing)
122
+ fs.writeFileSync(agents, guidance.contents);
123
+ done.push(guidance.message);
124
+ }
125
+ done.push("", `Agents will be told to run: ${cmd}`);
126
+ if (cmd.startsWith("npx")) {
127
+ done.push(`Heads up: npx only works once ${PACKAGE_NAME} is published. Run \`npm link\` in the`);
128
+ done.push("doc-review folder first if you want to use it locally, then re-run setup.");
129
+ }
130
+ done.push("Any other agent works too — see the JSON contract in the README.");
131
+ return done;
143
132
  }