@erdemtuna/doc-review 0.8.1 → 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 (82) hide show
  1. package/README.md +52 -137
  2. package/{src → lib}/SKILL.md +28 -4
  3. package/lib/anchor-text.js +155 -0
  4. package/lib/atomic-write.js +54 -0
  5. package/lib/chrome-api.js +78 -0
  6. package/lib/chrome-client.js +2255 -0
  7. package/lib/chrome-session.js +76 -0
  8. package/{src → lib}/chrome.css +253 -8
  9. package/lib/chrome.html +196 -0
  10. package/lib/cli.js +255 -0
  11. package/lib/click-target.js +28 -0
  12. package/lib/comment-anchor.js +23 -0
  13. package/lib/comment-target.js +164 -0
  14. package/lib/comparison-view.js +380 -0
  15. package/lib/contracts/feedback.js +1 -0
  16. package/lib/contracts/frame.js +7 -0
  17. package/lib/contracts/history.js +1 -0
  18. package/lib/contracts/index.js +2 -0
  19. package/lib/contracts/page.js +23 -0
  20. package/lib/document-execution.js +115 -0
  21. package/lib/document-trust.js +2 -0
  22. package/lib/edit-limits.js +24 -0
  23. package/lib/editing.js +107 -0
  24. package/lib/execution-client.js +69 -0
  25. package/lib/feedback-controller.js +106 -0
  26. package/lib/frame-channel.js +42 -0
  27. package/lib/frame-controller.js +313 -0
  28. package/lib/frame-host.js +136 -0
  29. package/lib/frame-policy.js +50 -0
  30. package/lib/history-client.js +112 -0
  31. package/lib/history-coordinator.js +210 -0
  32. package/lib/history-policy.js +43 -0
  33. package/lib/history-server.js +464 -0
  34. package/lib/html-transform.js +52 -0
  35. package/lib/icons.js +249 -0
  36. package/{src → lib}/markdown.js +25 -26
  37. package/lib/paths.js +83 -0
  38. package/lib/poll-transport.js +220 -0
  39. package/lib/positioning.js +97 -0
  40. package/lib/review-controller.js +93 -0
  41. package/lib/review-mode.js +66 -0
  42. package/lib/revision-diff.js +482 -0
  43. package/lib/revision-schema.js +235 -0
  44. package/lib/revision-store.js +190 -0
  45. package/lib/save-controller.js +300 -0
  46. package/lib/sdk.js +2629 -0
  47. package/lib/semantic-snapshot.js +268 -0
  48. package/lib/serialize.js +32 -0
  49. package/lib/server-entry.js +26 -0
  50. package/lib/server-lock.js +109 -0
  51. package/lib/server.js +1501 -0
  52. package/lib/setup-guidance.js +119 -0
  53. package/{src → lib}/setup.js +44 -55
  54. package/lib/state.js +733 -0
  55. package/lib/view-identity.js +112 -0
  56. package/package.json +24 -12
  57. package/src/anchor-text.js +0 -160
  58. package/src/atomic-write.js +0 -53
  59. package/src/chrome-client.js +0 -2006
  60. package/src/chrome-session.js +0 -85
  61. package/src/chrome.html +0 -129
  62. package/src/cli.js +0 -267
  63. package/src/click-target.js +0 -26
  64. package/src/comment-anchor.js +0 -22
  65. package/src/comment-target.js +0 -77
  66. package/src/edit-limits.js +0 -23
  67. package/src/editing.js +0 -99
  68. package/src/frame-channel.js +0 -44
  69. package/src/frame-policy.js +0 -16
  70. package/src/html-transform.js +0 -62
  71. package/src/icons.js +0 -251
  72. package/src/paths.js +0 -91
  73. package/src/poll-transport.js +0 -222
  74. package/src/positioning.js +0 -107
  75. package/src/review-mode.js +0 -59
  76. package/src/sdk.js +0 -2243
  77. package/src/serialize.js +0 -32
  78. package/src/server-entry.js +0 -26
  79. package/src/server-lock.js +0 -101
  80. package/src/server.js +0 -1298
  81. package/src/setup-guidance.js +0 -128
  82. package/src/state.js +0 -507
@@ -1,85 +0,0 @@
1
- export function pageUrl(key, sessionId) {
2
- return `/api/page/${key}?session=${encodeURIComponent(sessionId)}`;
3
- }
4
-
5
- export function replacePage(state, page) {
6
- state.page = page;
7
- state.others = page.others || [];
8
- }
9
-
10
- /** New and newly revised feedback belongs where the reviewer can see it. */
11
- export function newestComments(comments) {
12
- return [...(comments || [])].sort(
13
- (a, b) => (b.updatedAt || b.createdAt || 0) - (a.updatedAt || a.createdAt || 0)
14
- );
15
- }
16
-
17
- export function modePresentation(mode) {
18
- return mode === "edit"
19
- ? { label: "Edit", icon: "pencil", description: "Direct editing on" }
20
- : { label: "View", icon: "eye", description: "Editing off, comments enabled" };
21
- }
22
-
23
- export function createCommentUi() {
24
- return { menu: null, confirmation: null, edit: null };
25
- }
26
-
27
- export function ownMenu(ui, commentId, surface, triggerId) {
28
- if (ui.edit) return false;
29
- ui.menu = { commentId, surface, triggerId };
30
- ui.confirmation = null;
31
- return true;
32
- }
33
-
34
- export function ownConfirmation(ui, commentId, surface) {
35
- if (ui.edit) return false;
36
- ui.menu = null;
37
- ui.confirmation = { commentId, surface, status: "idle" };
38
- return true;
39
- }
40
-
41
- export function ownEdit(ui, comment, surface) {
42
- if (ui.edit) return ui.edit.commentId === comment.id;
43
- ui.menu = null;
44
- ui.confirmation = null;
45
- ui.edit = {
46
- commentId: comment.id,
47
- draft: comment.feedback,
48
- original: comment.feedback,
49
- originSurface: surface,
50
- status: "idle",
51
- selectionStart: comment.feedback.length,
52
- selectionEnd: comment.feedback.length,
53
- composing: false,
54
- validation: "",
55
- };
56
- return true;
57
- }
58
-
59
- export function clearOwned(ui, kind) {
60
- ui[kind] = null;
61
- return ui;
62
- }
63
-
64
- export function migrateCommentUi(ui, fromId, toId) {
65
- for (const key of ["menu", "confirmation", "edit"]) {
66
- if (ui[key]?.commentId === fromId) ui[key].commentId = toId;
67
- }
68
- return ui;
69
- }
70
-
71
- export function reconcileCommentUi(ui, comments) {
72
- const ids = new Set((comments || []).map((comment) => comment.id));
73
- const removed = new Set();
74
- for (const key of ["menu", "confirmation", "edit"]) {
75
- if (ui[key] && !ids.has(ui[key].commentId)) {
76
- removed.add(ui[key].commentId);
77
- ui[key] = null;
78
- }
79
- }
80
- return removed;
81
- }
82
-
83
- export function mutationIsCurrent(startEpoch, currentEpoch, comments, commentId) {
84
- return startEpoch === currentEpoch && (comments || []).some((comment) => comment.id === commentId);
85
- }
package/src/chrome.html DELETED
@@ -1,129 +0,0 @@
1
- <!doctype html>
2
- <html lang="en">
3
- <head>
4
- <meta charset="utf-8">
5
- <meta name="viewport" content="width=device-width, initial-scale=1">
6
- <title>doc-review</title>
7
- <link rel="stylesheet" href="/chrome.css">
8
- </head>
9
- <body data-session="__SESSION_ID__" data-token="__TOKEN__">
10
- <div class="app">
11
- <header class="toolbar" aria-label="Review controls">
12
- <div class="toolbar-product" aria-hidden="true"></div>
13
- <div class="mode-control">
14
- <button type="button" id="modeButton" class="toolbar-btn" aria-haspopup="menu" aria-expanded="false">
15
- <span id="modeIcon"></span><span id="modeLabel">View</span><span id="modeChevron"></span>
16
- </button>
17
- <div id="modeMenu" class="mode-menu" role="menu" hidden>
18
- <button type="button" role="menuitemradio" aria-checked="true" data-mode="view">
19
- <span data-icon="eye"></span><span><strong>View</strong><small>Editing off, comments enabled</small></span><span class="menu-check"></span>
20
- </button>
21
- <button type="button" role="menuitemradio" aria-checked="false" data-mode="edit">
22
- <span data-icon="pencil"></span><span><strong>Edit</strong><small>Direct editing on</small></span><span class="menu-check"></span>
23
- </button>
24
- </div>
25
- </div>
26
- <div class="toolbar-actions">
27
- <button type="button" id="commentsButton" class="toolbar-btn" aria-controls="drawer" aria-label="Comments" aria-expanded="false">
28
- <span data-icon="messages"></span><span class="toolbar-label">Comments</span><span id="toolbarCount" class="count-badge">0</span>
29
- </button>
30
- </div>
31
- </header>
32
- <main class="stage">
33
- <iframe
34
- id="frame"
35
- title="Artifact under review"
36
- sandbox="allow-scripts allow-forms allow-modals"></iframe>
37
- </main>
38
-
39
- <section id="compose" class="contextual-compose" role="dialog" aria-modal="false" aria-labelledby="composeTitle" hidden>
40
- <div class="compose-head">
41
- <div>
42
- <span class="chip" id="composeKind">Selection</span>
43
- <strong id="composeTitle">Add comment</strong>
44
- </div>
45
- <button type="button" id="composeClose" class="icon-btn" title="Cancel comment" aria-label="Cancel comment"></button>
46
- </div>
47
- <div id="composeDirection" class="compose-direction" hidden>
48
- <span id="composeDirectionText"></span>
49
- <button type="button" id="composeReveal" class="link-action">Back to selection</button>
50
- </div>
51
- <p class="quote" id="composeQuote"></p>
52
- <textarea id="composeText" rows="3" placeholder="What should change?" aria-label="Comment" aria-describedby="composeHelp"></textarea>
53
- <p id="composeHelp" class="compose-help"><span>Enter to comment</span><span>Shift+Enter for new line</span></p>
54
- <p id="composeError" class="compose-error" role="alert" hidden></p>
55
- <div class="compose-actions">
56
- <button type="button" id="composeAdd" class="btn-primary"><span data-icon="messageSquarePlus"></span><span id="composeAddLabel">Comment</span></button>
57
- <button type="button" id="composeCancel" class="btn-ghost">Cancel</button>
58
- </div>
59
- </section>
60
-
61
- <article id="alignedCard" class="aligned-card" hidden></article>
62
-
63
- <aside class="drawer" id="drawer" aria-label="Review feedback" aria-hidden="true">
64
- <div class="drawer-head">
65
- <div>
66
- <span class="section">Review</span>
67
- <h2 id="drawerTitle">Comments</h2>
68
- </div>
69
- <button type="button" id="theme" class="icon-btn" title="Switch chrome to dark" aria-label="Switch chrome to dark"></button>
70
- <button type="button" id="drawerClose" class="icon-btn" title="Close review drawer" aria-label="Close review drawer"></button>
71
- </div>
72
- <div id="commentsSection" class="drawer-inventory" tabindex="-1" aria-labelledby="inventoryHeading">
73
- <section>
74
- <div class="rail-head">
75
- <span class="section" id="inventoryHeading">Comments</span>
76
- <span class="dim" id="count">0</span>
77
- </div>
78
-
79
- <div id="cards" class="cards"></div>
80
-
81
- <div id="empty" class="empty">Select text or focus an element, then use the comment button</div>
82
- </section>
83
-
84
- <section id="othersBox" hidden>
85
- <div class="rail-head tight">
86
- <span class="section">Other pages</span>
87
- <span class="dim" id="othersCount">0</span>
88
- </div>
89
- <div class="edit-list" id="othersList"></div>
90
- </section>
91
-
92
- <section id="editsBox" hidden>
93
- <div class="rail-head tight">
94
- <span class="section">Your edits</span>
95
- <span class="dim" id="editCount">0</span>
96
- <button type="button" id="revert" class="link-btn danger-hover">Revert all</button>
97
- </div>
98
- <div class="save-line" id="saveLine"><span class="dot"></span><span id="saveText"></span></div>
99
- <div class="edit-list" id="editList"></div>
100
- </section>
101
-
102
- </div>
103
- <footer id="sendSection" class="drawer-footer send-section" tabindex="-1" aria-label="Send feedback">
104
- <div class="send-primary">
105
- <div class="rail-head tight">
106
- <span class="section">Send feedback</span>
107
- </div>
108
- <textarea id="note" rows="2" placeholder="Overall note…"></textarea>
109
- <button type="button" id="send" class="send" disabled>Nothing to send yet</button>
110
- <div class="agent-line" id="agentLine" hidden><span class="dot amber"></span><span id="agentText"></span></div>
111
- </div>
112
- <div class="send-secondary">
113
- <div class="handoff" id="handoff" hidden>
114
- <p class="handoff-title">Sent — but no agent is listening yet.</p>
115
- <p class="handoff-body">Paste this into your agent — Claude Code, Codex, or Cursor:</p>
116
- <div class="handoff-cmd"><code id="handoffCmd"></code></div>
117
- <button type="button" id="handoffCopy" class="btn-ghost">Copy prompt</button>
118
- </div>
119
- <button type="button" id="endReview" class="end-review" title="Stop this review and release the waiting agent">End review</button>
120
- </div>
121
- </footer>
122
- </aside>
123
- <div id="drawerBackdrop" class="drawer-backdrop" hidden></div>
124
- <div id="liveRegion" class="sr-only" role="status" aria-live="polite"></div>
125
- </div>
126
-
127
- <script type="module" src="/chrome.js"></script>
128
- </body>
129
- </html>
package/src/cli.js DELETED
@@ -1,267 +0,0 @@
1
- #!/usr/bin/env node
2
- import fs from "node:fs";
3
- import path from "node:path";
4
- import { spawn } from "node:child_process";
5
- import { fileURLToPath } from "node:url";
6
- import {
7
- canonicalTarget,
8
- ensureStateDir,
9
- SERVER_PROTOCOL,
10
- serverPath,
11
- serverProtocolMatches,
12
- statePath,
13
- targetKey,
14
- } from "./paths.js";
15
- import { readServerLock } from "./server-lock.js";
16
- import { installSkills, shellQuote } from "./setup.js";
17
- import { createDeadline, DEFAULT_POLL_SECONDS, isRecoverableTransportError, parseServerResponse, pollUntilDeadline, requestRaw } from "./poll-transport.js";
18
-
19
- const here = path.dirname(fileURLToPath(import.meta.url));
20
- const pkg = JSON.parse(fs.readFileSync(path.join(here, "..", "package.json"), "utf8"));
21
-
22
- const HELP = `doc-review ${pkg.version}
23
-
24
- doc-review <file-or-localhost-url> Open a file or localhost page for review
25
- doc-review poll <target> Wait for feedback, print it as JSON (for agents)
26
- --ack <batch_id> Acknowledge that exact delivered batch, then keep waiting
27
- --timeout <secs> End-to-end cutoff; default 12 hours (43200 seconds)
28
- doc-review status <target> Report whether feedback is waiting, without blocking
29
- doc-review setup Teach Claude Code / Codex how to use doc-review
30
- doc-review setup --global ...for every project, not just this one
31
-
32
- Everything runs locally. No account, no cloud, no database.
33
- `;
34
-
35
- // --------------------------------------------------------------- server glue
36
-
37
- function readServerRecord() {
38
- try {
39
- return JSON.parse(fs.readFileSync(serverPath(), "utf8"));
40
- } catch {
41
- return null;
42
- }
43
- }
44
-
45
- const request = requestRaw;
46
-
47
- async function alive(server, deadline) {
48
- if (!server?.port || !server.instance_id) return false;
49
- const lock = readServerLock();
50
- if (lock?.pid !== server.pid || lock?.instance_id !== server.instance_id) return false;
51
- try {
52
- deadline?.check();
53
- const res = await request(server, {
54
- method: "GET", path: "/health", timeout: Math.min(1200, deadline?.remaining() ?? 1200),
55
- }, undefined, { time: deadline?.time });
56
- deadline?.check();
57
- const health = parseServerResponse(res);
58
- if (health.pid !== server.pid || health.instance_id !== server.instance_id) {
59
- throw new Error("The doc-review server identity does not match its writer lock. End the review and restart the server.");
60
- }
61
- if (!serverProtocolMatches(health.protocol) || !serverProtocolMatches(server.protocol)) {
62
- throw new Error(
63
- `Incompatible live doc-review server (protocol ${health.protocol}; this CLI requires ${SERVER_PROTOCOL}). ` +
64
- "End active reviews and stop/restart the old doc-review server before retrying. " +
65
- "Its live writer lock and queued feedback have not been changed.",
66
- );
67
- }
68
- return true;
69
- } catch (err) {
70
- if (isRecoverableTransportError(err)) return false;
71
- throw err;
72
- }
73
- }
74
-
75
- async function ensureServer(deadline = createDeadline(20)) {
76
- deadline.check();
77
- ensureStateDir();
78
- for (let launch = 0; launch < 3; launch += 1) {
79
- const saved = readServerRecord();
80
- if (await alive(saved, deadline)) return saved;
81
- deadline.check();
82
-
83
- const child = spawn(process.execPath, [path.join(here, "server-entry.js")], {
84
- detached: true,
85
- stdio: "ignore",
86
- });
87
- let launchError;
88
- child.on("error", (err) => { launchError = err; });
89
- child.unref();
90
-
91
- for (let attempt = 0; attempt < 60; attempt += 1) {
92
- await deadline.sleep(100);
93
- if (launchError) throw launchError;
94
- const record = readServerRecord();
95
- if (await alive(record, deadline)) return record;
96
- if (child.exitCode !== null && child.exitCode !== 0) {
97
- throw new Error("The local doc-review server failed to start. Check the state directory permissions and server startup diagnostics before retrying.");
98
- }
99
- if (child.exitCode !== null && !readServerLock()) break;
100
- }
101
- }
102
- throw Object.assign(new Error("Could not start the local doc-review server yet."), { code: "SERVER_START_PENDING" });
103
- }
104
-
105
- function openBrowser(url) {
106
- const command =
107
- process.platform === "darwin" ? ["open", [url]] : process.platform === "win32" ? ["cmd", ["/c", "start", "", url]] : ["xdg-open", [url]];
108
- const child = spawn(command[0], command[1], { detached: true, stdio: "ignore" });
109
- // A missing opener (headless Linux without xdg-open) surfaces as an async
110
- // 'error' event, not a throw. Printing the URL below is the fallback.
111
- child.on("error", () => {});
112
- child.unref();
113
- }
114
-
115
- // ------------------------------------------------------------------ commands
116
-
117
- async function openCommand(input) {
118
- const target = canonicalTarget(input);
119
- if (target.kind === "file" && !fs.existsSync(target.value)) {
120
- console.error(`File not found: ${target.value}`);
121
- process.exit(1);
122
- }
123
- const server = await ensureServer();
124
- const res = await request(server, { method: "POST", path: "/api/session", headers: { "content-type": "application/json" } }, { target: target.value });
125
- const body = JSON.parse(res.raw);
126
- if (res.status !== 200) {
127
- console.error(body.error || "Could not open that file.");
128
- process.exit(1);
129
- }
130
- const url = `http://127.0.0.1:${server.port}${body.path}`;
131
- openBrowser(url);
132
- console.log(`Reviewing ${target.kind === "url" ? target.value : path.basename(target.value)}`);
133
- console.log(url);
134
- console.log(`\nWaiting for feedback? Run:\n doc-review poll ${shellQuote(target.value)}`);
135
- }
136
-
137
- /**
138
- * The consumer is an agent reading a pipe. process.exit() does not wait for
139
- * pending stdout writes, so a large payload could arrive truncated — always
140
- * wait for the write to hand off before returning.
141
- */
142
- function writeStdout(text) {
143
- return new Promise((resolve) => process.stdout.write(text, resolve));
144
- }
145
-
146
- async function pollCommand(input, { ackId = "", timeoutSecs = DEFAULT_POLL_SECONDS } = {}) {
147
- const deadline = createDeadline(timeoutSecs);
148
- const target = canonicalTarget(input).value;
149
-
150
- const label = /^https?:\/\//i.test(target) ? target : path.basename(target);
151
- process.stderr.write(`Waiting for feedback on ${label} — comment in the browser, then hit Send.\n`);
152
-
153
- const batch = await pollUntilDeadline({
154
- target, ackId, deadline, discover: ensureServer,
155
- diagnostic: (text) => process.stderr.write(text),
156
- });
157
- await writeStdout(`${JSON.stringify(batch, null, 2)}\n`);
158
- }
159
-
160
- /**
161
- * Instant answer, no blocking. Asks the running server when there is one;
162
- * otherwise reads the persisted state directly, so a dead server still
163
- * reports feedback that is waiting for a fresh poll.
164
- */
165
- async function statusCommand(input) {
166
- const target = canonicalTarget(input).value;
167
- const saved = readServerRecord();
168
- if (serverProtocolMatches(saved?.protocol) && saved.port && saved.instance_id && (await alive(saved))) {
169
- const res = await request(saved, { method: "GET", path: `/api/status?target=${encodeURIComponent(target)}` });
170
- if (res.status === 200) {
171
- process.stdout.write(`${JSON.stringify(JSON.parse(res.raw), null, 2)}\n`);
172
- return;
173
- }
174
- }
175
-
176
- let data = { pages: {}, batches: {} };
177
- try {
178
- data = JSON.parse(fs.readFileSync(statePath(), "utf8"));
179
- } catch {
180
- // No state yet: everything below reads as empty.
181
- }
182
- const key = targetKey(target);
183
- const pending = (data.batches || {})[key];
184
- const page = (data.pages || {})[key];
185
- const payload = {
186
- status: pending ? "feedback-waiting" : "idle",
187
- feedback_waiting: !!pending,
188
- agent_listening: false,
189
- server_running: false,
190
- unsent: {
191
- comments: page ? page.comments.length : 0,
192
- edits: page ? page.edits.length : 0,
193
- },
194
- };
195
- process.stdout.write(`${JSON.stringify(payload, null, 2)}\n`);
196
- }
197
-
198
- // ---------------------------------------------------------------------- main
199
-
200
- const argv = process.argv.slice(2);
201
-
202
- if (argv.length === 0 || argv[0] === "--help" || argv[0] === "-h" || argv[0] === "help") {
203
- console.log(HELP);
204
- process.exit(0);
205
- }
206
-
207
- if (argv[0] === "--version" || argv[0] === "-v") {
208
- console.log(pkg.version);
209
- process.exit(0);
210
- }
211
-
212
- process.on("SIGINT", () => {
213
- process.stderr.write("\nStopped waiting. Your feedback is safe — run the same command again to pick it up.\n");
214
- process.exit(130);
215
- });
216
-
217
- function parsePollArgs(rest) {
218
- const parsed = { file: "", ackId: "", timeoutSecs: DEFAULT_POLL_SECONDS };
219
- let sawTimeout = false;
220
- for (let i = 0; i < rest.length; i += 1) {
221
- const arg = rest[i];
222
- if (arg === "--ack") {
223
- const value = rest[(i += 1)];
224
- if (!value || value.startsWith("-")) {
225
- throw new Error("--ack requires the batch_id from the feedback response, e.g. --ack b_123");
226
- }
227
- parsed.ackId = value;
228
- }
229
- else if (arg.startsWith("--ack=")) {
230
- throw new Error("Use --ack <batch_id> with the batch ID as a separate argument.");
231
- }
232
- else if (arg === "--timeout") {
233
- sawTimeout = true;
234
- parsed.timeoutSecs = Number(rest[(i += 1)]);
235
- } else if (arg.startsWith("--timeout=")) {
236
- sawTimeout = true;
237
- parsed.timeoutSecs = Number(arg.slice("--timeout=".length));
238
- } else if (!arg.startsWith("-") && !parsed.file) parsed.file = arg;
239
- else throw new Error(`Unknown poll argument: ${arg}`);
240
- }
241
- // A malformed value must fail loudly — NaN or 0 silently waiting forever is
242
- // the exact hang the flag exists to prevent.
243
- if (sawTimeout && (!Number.isFinite(parsed.timeoutSecs) || parsed.timeoutSecs <= 0)) {
244
- throw new Error("--timeout wants a number of seconds, e.g. --timeout 300");
245
- }
246
- return parsed;
247
- }
248
-
249
- try {
250
- if (argv[0] === "poll") {
251
- const { file, ackId, timeoutSecs } = parsePollArgs(argv.slice(1));
252
- if (!file) throw new Error("Usage: doc-review poll <file-or-localhost-url> [--ack <batch_id>] [--timeout <secs>]");
253
- await pollCommand(file, { ackId, timeoutSecs });
254
- } else if (argv[0] === "status") {
255
- const file = argv.find((a, i) => i > 0 && !a.startsWith("-"));
256
- if (!file) throw new Error("Usage: doc-review status <file-or-localhost-url>");
257
- await statusCommand(file);
258
- } else if (argv[0] === "setup") {
259
- const isGlobal = argv.includes("--global") || argv.includes("-g");
260
- installSkills(process.cwd(), { global: isGlobal }).forEach((line) => console.log(line));
261
- } else {
262
- await openCommand(argv[0]);
263
- }
264
- } catch (err) {
265
- console.error(err.message || String(err));
266
- process.exit(1);
267
- }
@@ -1,26 +0,0 @@
1
- export function navigationHref(target) {
2
- const link = target?.closest?.("a[href]");
3
- if (link) return link.getAttribute("href") || "";
4
- const control = target?.closest?.("[data-href]");
5
- return control ? control.getAttribute("data-href") || "" : "";
6
- }
7
-
8
- /**
9
- * Decide what a modified click on a "#…" link should do. Setting the real
10
- * hash is what makes CSS :target routing (single-file "pages") show the
11
- * section — a bare scrollIntoView can't reach a display:none target and
12
- * never fires :target. Only when the hash is already current does scrolling
13
- * become the right move, since re-setting an identical hash is a no-op.
14
- */
15
- export function hashClickAction(href, currentHash) {
16
- const decode = (value) => {
17
- try {
18
- return decodeURIComponent(value);
19
- } catch {
20
- return value;
21
- }
22
- };
23
- const id = decode(href.slice(1));
24
- if (decode((currentHash || "").slice(1)) === id) return { kind: "scroll", id };
25
- return { kind: "navigate", hash: href };
26
- }
@@ -1,22 +0,0 @@
1
- const text = (value, limit) => typeof value === "string" ? value.slice(0, limit) : "";
2
-
3
- export function normalizeCommentAnchor(kind, anchor) {
4
- if (!anchor || typeof anchor !== "object" || Array.isArray(anchor)) return null;
5
- if (kind === "element") {
6
- const selector = text(anchor.selector, 2000);
7
- if (!selector) return null;
8
- return {
9
- selector,
10
- ...(text(anchor.label, 500) ? { label: text(anchor.label, 500) } : {}),
11
- };
12
- }
13
-
14
- const quote = text(anchor.quote, 4000);
15
- if (!quote) return null;
16
- return {
17
- quote,
18
- ...(text(anchor.prefix, 1000) ? { prefix: text(anchor.prefix, 1000) } : {}),
19
- ...(text(anchor.suffix, 1000) ? { suffix: text(anchor.suffix, 1000) } : {}),
20
- ...(text(anchor.selector, 2000) ? { selector: text(anchor.selector, 2000) } : {}),
21
- };
22
- }
@@ -1,77 +0,0 @@
1
- const finite = (value) => typeof value === "number" && Number.isFinite(value);
2
-
3
- export function sanitizeRect(rect) {
4
- if (!rect) return null;
5
- const values = ["left", "top", "right", "bottom", "width", "height"];
6
- if (!values.every((key) => finite(rect[key]))) return null;
7
- if (rect.width < 0 || rect.height < 0 || rect.right < rect.left || rect.bottom < rect.top) return null;
8
- return {
9
- left: rect.left,
10
- top: rect.top,
11
- right: rect.right,
12
- bottom: rect.bottom,
13
- width: rect.width,
14
- height: rect.height,
15
- };
16
- }
17
-
18
- export function sanitizeClientRect(rect, viewport) {
19
- const safe = sanitizeRect(rect);
20
- if (!safe || !viewport || !finite(viewport.width) || !finite(viewport.height)) return null;
21
- const { left: rawLeft, top: rawTop, right: rawRight, bottom: rawBottom } = safe;
22
- if (rawRight <= 0 || rawBottom <= 0 || rawLeft >= viewport.width || rawTop >= viewport.height) return null;
23
- const left = Math.max(0, Math.min(viewport.width, rawLeft));
24
- const top = Math.max(0, Math.min(viewport.height, rawTop));
25
- const right = Math.max(left, Math.min(viewport.width, rawRight));
26
- const bottom = Math.max(top, Math.min(viewport.height, rawBottom));
27
- return { left, top, right, bottom, width: right - left, height: bottom - top };
28
- }
29
-
30
- export function sanitizeClientRects(rects, viewport, limit = 64) {
31
- if (!Array.isArray(rects)) return [];
32
- return rects.slice(0, limit).map((rect) => sanitizeClientRect(rect, viewport)).filter(Boolean);
33
- }
34
-
35
- export function sanitizeClipRect(rect, viewport) {
36
- const safe = sanitizeClientRect(rect, viewport);
37
- return safe && safe.width > 0 && safe.height > 0 ? safe : null;
38
- }
39
-
40
- export function sanitizeRelation(value) {
41
- return ["visible", "above", "below", "unavailable"].includes(value) ? value : "unavailable";
42
- }
43
-
44
- export function targetMessage(
45
- { kind, quote, anchor, rects, generation, relation, clip, horizontal },
46
- viewport
47
- ) {
48
- const safeRects = sanitizeClientRects(rects, viewport);
49
- const safeClip = sanitizeClipRect(clip, viewport);
50
- if (!Number.isSafeInteger(generation) || generation < 1) return null;
51
- const safeRelation = sanitizeRelation(relation);
52
- if (!safeClip || (safeRelation === "visible" && !safeRects.length)) return null;
53
- return {
54
- kind: kind === "element" ? "element" : "selection",
55
- quote: String(quote || ""),
56
- anchor: anchor || null,
57
- rects: safeRects,
58
- generation,
59
- relation: safeRelation,
60
- clip: safeClip,
61
- horizontal: finite(horizontal) ? Math.max(safeClip.left, Math.min(safeClip.right, horizontal)) : null,
62
- };
63
- }
64
-
65
- export function acceptsTargetGeometry(currentGeneration, incoming) {
66
- return !!incoming && Number.isSafeInteger(incoming.generation) && incoming.generation === currentGeneration;
67
- }
68
-
69
- export function acceptedOpenGeneration(
70
- { pendingGeneration = null, retargetGeneration = null },
71
- { accepted, requestedGeneration }
72
- ) {
73
- if (!accepted) return pendingGeneration;
74
- if (requestedGeneration === retargetGeneration) return retargetGeneration;
75
- if (requestedGeneration === pendingGeneration) return pendingGeneration;
76
- return null;
77
- }
@@ -1,23 +0,0 @@
1
- export const MAX_EDIT_CHARACTERS = 200_000;
2
-
3
- const EDIT_FIELDS = ["before", "after", "before_html", "after_html", "moved_after", "moved_before"];
4
-
5
- export function limitEditFields(input) {
6
- const fields = {};
7
- const truncatedFields = [];
8
- for (const field of EDIT_FIELDS) {
9
- const value = input[field];
10
- if (typeof value !== "string") continue;
11
- let end = 0;
12
- let count = 0;
13
- // Count Unicode code points, not UTF-16 halves of an astral character.
14
- for (const character of value) {
15
- if (count === MAX_EDIT_CHARACTERS) break;
16
- end += character.length;
17
- count += 1;
18
- }
19
- fields[field] = value.slice(0, end);
20
- if (end < value.length) truncatedFields.push(field);
21
- }
22
- return { fields, truncated: truncatedFields.length > 0, truncated_fields: truncatedFields };
23
- }