@erdemtuna/doc-review 0.9.0 → 0.11.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 (103) hide show
  1. package/README.md +58 -222
  2. package/{src → lib}/SKILL.md +5 -2
  3. package/lib/anchor-text.js +155 -0
  4. package/lib/atomic-write.js +54 -0
  5. package/lib/changes-controller.js +184 -0
  6. package/lib/chrome-api.js +78 -0
  7. package/lib/chrome-client.js +1700 -0
  8. package/lib/chrome-session.js +73 -0
  9. package/lib/chrome.html +53 -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/comments-controller.js +109 -0
  15. package/lib/contextual-controller.js +33 -0
  16. package/lib/contracts/feedback.js +1 -0
  17. package/lib/contracts/frame.js +7 -0
  18. package/lib/contracts/history.js +1 -0
  19. package/lib/contracts/index.js +2 -0
  20. package/lib/contracts/page.js +23 -0
  21. package/lib/controller-store.js +96 -0
  22. package/lib/document-execution.js +115 -0
  23. package/lib/edit-limits.js +24 -0
  24. package/lib/editing.js +107 -0
  25. package/lib/execution-client.js +19 -0
  26. package/lib/feedback-controller.js +113 -0
  27. package/lib/feedback-panel-controller.js +161 -0
  28. package/lib/frame-channel.js +42 -0
  29. package/lib/frame-controller.js +313 -0
  30. package/lib/frame-host.js +136 -0
  31. package/lib/frame-policy.js +50 -0
  32. package/lib/history-client.js +112 -0
  33. package/lib/history-coordinator.js +217 -0
  34. package/lib/history-policy.js +43 -0
  35. package/lib/history-server.js +464 -0
  36. package/lib/html-transform.js +52 -0
  37. package/lib/icons.js +325 -0
  38. package/{src → lib}/markdown.js +25 -26
  39. package/lib/paths.js +83 -0
  40. package/lib/poll-transport.js +220 -0
  41. package/lib/positioning.js +97 -0
  42. package/lib/recovery-controller.js +123 -0
  43. package/lib/review-controller.js +93 -0
  44. package/lib/review-mode.js +66 -0
  45. package/lib/revision-diff.js +482 -0
  46. package/lib/revision-schema.js +235 -0
  47. package/lib/revision-store.js +190 -0
  48. package/lib/save-controller.js +303 -0
  49. package/lib/sdk.js +2629 -0
  50. package/lib/semantic-snapshot.js +268 -0
  51. package/lib/serialize.js +32 -0
  52. package/lib/server-entry.js +26 -0
  53. package/lib/server-lock.js +109 -0
  54. package/lib/server.js +1501 -0
  55. package/lib/setup-guidance.js +119 -0
  56. package/{src → lib}/setup.js +44 -55
  57. package/lib/state.js +733 -0
  58. package/lib/toolbar-controller.js +32 -0
  59. package/lib/ui/THIRD_PARTY_NOTICES.md +1254 -0
  60. package/lib/ui/chrome.css +4881 -0
  61. package/lib/ui/chrome.js +52 -0
  62. package/lib/view-identity.js +112 -0
  63. package/package.json +50 -14
  64. package/src/anchor-text.js +0 -160
  65. package/src/atomic-write.js +0 -53
  66. package/src/chrome-client.js +0 -2747
  67. package/src/chrome-session.js +0 -85
  68. package/src/chrome.css +0 -652
  69. package/src/chrome.html +0 -196
  70. package/src/cli.js +0 -271
  71. package/src/click-target.js +0 -26
  72. package/src/comment-anchor.js +0 -22
  73. package/src/comment-target.js +0 -164
  74. package/src/comparison-view.js +0 -337
  75. package/src/document-execution.js +0 -103
  76. package/src/edit-limits.js +0 -23
  77. package/src/editing.js +0 -99
  78. package/src/execution-client.js +0 -63
  79. package/src/frame-channel.js +0 -44
  80. package/src/frame-policy.js +0 -54
  81. package/src/history-client.js +0 -104
  82. package/src/history-coordinator.js +0 -186
  83. package/src/history-policy.js +0 -43
  84. package/src/history-server.js +0 -463
  85. package/src/html-transform.js +0 -62
  86. package/src/icons.js +0 -251
  87. package/src/paths.js +0 -91
  88. package/src/poll-transport.js +0 -222
  89. package/src/positioning.js +0 -107
  90. package/src/review-mode.js +0 -59
  91. package/src/revision-diff.js +0 -440
  92. package/src/revision-schema.js +0 -219
  93. package/src/revision-store.js +0 -177
  94. package/src/sdk.js +0 -2607
  95. package/src/semantic-snapshot.js +0 -230
  96. package/src/serialize.js +0 -32
  97. package/src/server-entry.js +0 -26
  98. package/src/server-lock.js +0 -101
  99. package/src/server.js +0 -1466
  100. package/src/setup-guidance.js +0 -128
  101. package/src/state.js +0 -732
  102. package/src/view-identity.js +0 -99
  103. /package/{src → lib}/document-trust.js +0 -0
package/src/chrome.html DELETED
@@ -1,196 +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 history-toggle" role="group" aria-label="Review destination">
13
- <button type="button" id="latestVersion" class="toolbar-btn" aria-pressed="true" aria-controls="frame">Review</button>
14
- <button type="button" id="seeChanges" class="toolbar-btn" aria-pressed="false" aria-controls="historyPanel">Changes</button>
15
- </div>
16
- <div class="page-controls" role="group" aria-label="Page controls">
17
- <div class="mode-control">
18
- <button type="button" id="modeButton" class="toolbar-btn" aria-haspopup="menu" aria-expanded="false">
19
- <span id="modeIcon"></span><span id="modeLabel">View</span><span id="modeChevron"></span>
20
- </button>
21
- <div id="modeMenu" class="mode-menu" role="menu" hidden>
22
- <button type="button" role="menuitemradio" aria-checked="true" data-mode="view">
23
- <span data-icon="eye"></span><span><strong>View</strong><small>Editing off, comments enabled</small></span><span class="menu-check"></span>
24
- </button>
25
- <button type="button" role="menuitemradio" aria-checked="false" data-mode="edit">
26
- <span data-icon="pencil"></span><span><strong>Edit</strong><small>Direct editing on</small></span><span class="menu-check"></span>
27
- </button>
28
- </div>
29
- </div>
30
- </div>
31
- <div class="toolbar-actions">
32
- <div id="documentTrustControls" hidden></div>
33
- <button type="button" id="commentsButton" class="toolbar-btn" aria-controls="drawer" aria-label="Comments" aria-expanded="false">
34
- <span data-icon="messages"></span><span class="toolbar-label">Comments</span><span id="toolbarCount" class="count-badge">0</span>
35
- </button>
36
- <details id="reviewDetails" class="review-details">
37
- <summary>More <span aria-hidden="true">⌄</span></summary>
38
- <div class="review-details-menu">
39
- <p class="section">Page recovery</p>
40
- <button type="button" id="executionStatic">Reload without scripts</button>
41
- <button type="button" id="executionAuto" hidden>Use page interactions</button>
42
- <p id="executionDetails" class="history-context" hidden></p>
43
- </div>
44
- </details>
45
- </div>
46
- </header>
47
- <main class="stage">
48
- <p id="executionStatus" class="execution-notice" role="status" hidden></p>
49
- <section id="reloadNotice" class="reload-notice" aria-label="Source update" hidden>
50
- <p id="reloadMessage"></p>
51
- <button type="button" id="safeReload" class="btn-primary">Reload latest; keep comment drafts</button>
52
- <button type="button" id="keepCurrent" class="btn-ghost">Keep this page</button>
53
- </section>
54
- <iframe
55
- id="frame"
56
- title="Artifact under review"
57
- sandbox="allow-scripts allow-forms allow-modals"></iframe>
58
- <section id="historyPanel" class="history-panel" aria-labelledby="historyTitle" hidden>
59
- <h1 id="historyTitle" class="sr-only">Changes</h1>
60
- <div class="history-controls">
61
- <label class="history-round-label">Round <select id="roundPicker" aria-label="Review round"></select></label>
62
- <label id="historyTargetLabel" hidden>Page <select id="historyTarget" aria-label="Comparison page"></select></label>
63
- <div id="comparisonModes" role="group" aria-label="Comparison format"></div>
64
- <button type="button" id="captureResult" class="btn-primary" hidden>Capture result</button>
65
- </div>
66
- <div class="history-summary">
67
- <p id="historyStatus" role="status">No review rounds yet. Send feedback to start a comparison.</p>
68
- <div id="historyCounts" class="history-counts"></div>
69
- </div>
70
- <p id="historyError" class="compose-error" role="alert" hidden></p>
71
- <div class="comparison-header">
72
- <nav id="changeNavigation" class="history-navigation" aria-label="Change navigation" hidden>
73
- <button type="button" id="previousChange" class="btn-ghost" aria-label="Previous change">Previous</button>
74
- <span id="changePosition"></span>
75
- <label class="change-jump-label"><span>Jump to</span> <select id="changeJump" aria-label="Jump to change"></select></label>
76
- <button type="button" id="nextChange" class="btn-ghost" aria-label="Next change">Next</button>
77
- </nav>
78
- <div class="comparison-heading-slot"></div>
79
- </div>
80
- <article id="changeDetail" class="change-detail" aria-label="Continuous before and after comparison"></article>
81
- <details id="historyDiagnostics" class="history-diagnostics">
82
- <summary>Comparison details</summary>
83
- <p id="historyCurrentStatus" class="history-context" hidden></p>
84
- <p id="historyViewCoverage" class="history-context" hidden></p>
85
- <div id="historyUnavailable" class="history-context" hidden></div>
86
- <p>Content is a readable reconstruction of saved blocks, not the original page layout. Historical scripts, authored styles and image URLs are never replayed. Table rows use saved cell selectors; complex spanning structure is shown in row order. Source shows saved file text.</p>
87
- <dl id="historyTiming" class="history-timing" hidden></dl>
88
- <p id="historyCaptureDelay" class="history-context" hidden></p>
89
- <details id="historyLimitations" hidden>
90
- <summary>Comparison coverage and limits</summary>
91
- <ul id="historyLimitationsList"></ul>
92
- </details>
93
- <button type="button" id="finishCapture" class="btn-ghost" hidden>Finish with available snapshots</button>
94
- <p id="finishCaptureHelp" class="history-context" hidden>Finishing marks missing Content as unavailable; it does not invent a result. Existing Source comparisons remain available, and this target will no longer retry capture.</p>
95
- </details>
96
- </section>
97
- </main>
98
-
99
- <section id="compose" class="contextual-compose" role="dialog" aria-modal="false" aria-labelledby="composeTitle" hidden>
100
- <div class="compose-head">
101
- <div>
102
- <span class="chip" id="composeKind">Selection</span>
103
- <strong id="composeTitle">Add comment</strong>
104
- </div>
105
- <button type="button" id="composeClose" class="icon-btn" title="Cancel comment" aria-label="Cancel comment"></button>
106
- </div>
107
- <div id="composeDirection" class="compose-direction" hidden>
108
- <span id="composeDirectionText"></span>
109
- <button type="button" id="composeReveal" class="link-action">Back to selection</button>
110
- </div>
111
- <p class="quote" id="composeQuote"></p>
112
- <textarea id="composeText" rows="3" placeholder="What should change?" aria-label="Comment" aria-describedby="composeHelp"></textarea>
113
- <p id="composeHelp" class="compose-help"><span>Enter to comment</span><span>Shift+Enter for new line</span></p>
114
- <p id="composeError" class="compose-error" role="alert" hidden></p>
115
- <div class="compose-actions">
116
- <button type="button" id="composeAdd" class="btn-primary"><span data-icon="messageSquarePlus"></span><span id="composeAddLabel">Comment</span></button>
117
- <button type="button" id="composeCancel" class="btn-ghost">Cancel</button>
118
- </div>
119
- </section>
120
-
121
- <article id="alignedCard" class="aligned-card" hidden></article>
122
-
123
- <aside class="drawer" id="drawer" aria-label="Review feedback" aria-hidden="true">
124
- <div class="drawer-head">
125
- <div>
126
- <span class="section">Review</span>
127
- <h2 id="drawerTitle">Comments</h2>
128
- </div>
129
- <button type="button" id="theme" class="icon-btn" title="Switch chrome to dark" aria-label="Switch chrome to dark"></button>
130
- <button type="button" id="drawerClose" class="icon-btn" title="Close review drawer" aria-label="Close review drawer"></button>
131
- </div>
132
- <div id="commentsSection" class="drawer-inventory" tabindex="-1" aria-labelledby="inventoryHeading">
133
- <section>
134
- <div class="rail-head">
135
- <span class="section" id="inventoryHeading">Comments</span>
136
- <span class="dim" id="count">0</span>
137
- </div>
138
-
139
- <div id="cards" class="cards"></div>
140
-
141
- <div id="empty" class="empty">Select text or focus an element, then use the comment button</div>
142
- </section>
143
-
144
- <section id="othersBox" hidden>
145
- <div class="rail-head tight">
146
- <span class="section">Other pages</span>
147
- <span class="dim" id="othersCount">0</span>
148
- </div>
149
- <div class="edit-list" id="othersList"></div>
150
- </section>
151
-
152
- <section id="editsBox" hidden>
153
- <div class="rail-head tight">
154
- <span class="section">Your edits</span>
155
- <span class="dim" id="editCount">0</span>
156
- <button type="button" id="revert" class="link-btn danger-hover">Revert all</button>
157
- </div>
158
- <div class="save-line" id="saveLine"><span class="dot"></span><span id="saveText"></span></div>
159
- <div class="edit-list" id="editList"></div>
160
- </section>
161
-
162
- </div>
163
- <footer id="sendSection" class="drawer-footer send-section" tabindex="-1" aria-label="Send feedback">
164
- <div class="send-primary">
165
- <div class="rail-head tight">
166
- <span class="section">Send feedback</span>
167
- </div>
168
- <textarea id="note" rows="2" placeholder="Overall note…"></textarea>
169
- <button type="button" id="send" class="send" disabled>Nothing to send yet</button>
170
- <p id="draftWarning" class="dim" hidden></p>
171
- <div id="captureWarning" class="capture-warning" role="alert" hidden>
172
- <p id="captureWarningText"></p>
173
- <div id="captureTargets"></div>
174
- <button type="button" id="recaptureBaseline" class="btn-primary">Recapture and send</button>
175
- <button type="button" id="sendWithoutComparison" class="btn-ghost">Send without comparison</button>
176
- </div>
177
- <div class="agent-line" id="agentLine" hidden><span class="dot amber"></span><span id="agentText"></span></div>
178
- </div>
179
- <div class="send-secondary">
180
- <div class="handoff" id="handoff" hidden>
181
- <p class="handoff-title">Sent — but no agent is listening yet.</p>
182
- <p class="handoff-body">Paste this into your agent — Claude Code, Codex, or Cursor:</p>
183
- <div class="handoff-cmd"><code id="handoffCmd"></code></div>
184
- <button type="button" id="handoffCopy" class="btn-ghost">Copy prompt</button>
185
- </div>
186
- <button type="button" id="endReview" class="end-review" title="Stop this review and release the waiting agent">End review</button>
187
- </div>
188
- </footer>
189
- </aside>
190
- <div id="drawerBackdrop" class="drawer-backdrop" hidden></div>
191
- <div id="liveRegion" class="sr-only" role="status" aria-live="polite"></div>
192
- </div>
193
-
194
- <script type="module" src="/chrome.js"></script>
195
- </body>
196
- </html>
package/src/cli.js DELETED
@@ -1,271 +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
- Use Review / Changes in the browser for retained content history.
34
- Plain HTML edits autosave. Self-contained file scripts run automatically with feedback-only edits.
35
- Use More for optional script-disabled recovery. Comparison capture does not block sending feedback.
36
- Acknowledgement handles feedback; browser result capture may complete later.
37
- `;
38
-
39
- // --------------------------------------------------------------- server glue
40
-
41
- function readServerRecord() {
42
- try {
43
- return JSON.parse(fs.readFileSync(serverPath(), "utf8"));
44
- } catch {
45
- return null;
46
- }
47
- }
48
-
49
- const request = requestRaw;
50
-
51
- async function alive(server, deadline) {
52
- if (!server?.port || !server.instance_id) return false;
53
- const lock = readServerLock();
54
- if (lock?.pid !== server.pid || lock?.instance_id !== server.instance_id) return false;
55
- try {
56
- deadline?.check();
57
- const res = await request(server, {
58
- method: "GET", path: "/health", timeout: Math.min(1200, deadline?.remaining() ?? 1200),
59
- }, undefined, { time: deadline?.time });
60
- deadline?.check();
61
- const health = parseServerResponse(res);
62
- if (health.pid !== server.pid || health.instance_id !== server.instance_id) {
63
- throw new Error("The doc-review server identity does not match its writer lock. End the review and restart the server.");
64
- }
65
- if (!serverProtocolMatches(health.protocol) || !serverProtocolMatches(server.protocol)) {
66
- throw new Error(
67
- `Incompatible live doc-review server (protocol ${health.protocol}; this CLI requires ${SERVER_PROTOCOL}). ` +
68
- "End active reviews and stop/restart the old doc-review server before retrying. " +
69
- "Its live writer lock and queued feedback have not been changed.",
70
- );
71
- }
72
- return true;
73
- } catch (err) {
74
- if (isRecoverableTransportError(err)) return false;
75
- throw err;
76
- }
77
- }
78
-
79
- async function ensureServer(deadline = createDeadline(20)) {
80
- deadline.check();
81
- ensureStateDir();
82
- for (let launch = 0; launch < 3; launch += 1) {
83
- const saved = readServerRecord();
84
- if (await alive(saved, deadline)) return saved;
85
- deadline.check();
86
-
87
- const child = spawn(process.execPath, [path.join(here, "server-entry.js")], {
88
- detached: true,
89
- stdio: "ignore",
90
- });
91
- let launchError;
92
- child.on("error", (err) => { launchError = err; });
93
- child.unref();
94
-
95
- for (let attempt = 0; attempt < 60; attempt += 1) {
96
- await deadline.sleep(100);
97
- if (launchError) throw launchError;
98
- const record = readServerRecord();
99
- if (await alive(record, deadline)) return record;
100
- if (child.exitCode !== null && child.exitCode !== 0) {
101
- throw new Error("The local doc-review server failed to start. Check the state directory permissions and server startup diagnostics before retrying.");
102
- }
103
- if (child.exitCode !== null && !readServerLock()) break;
104
- }
105
- }
106
- throw Object.assign(new Error("Could not start the local doc-review server yet."), { code: "SERVER_START_PENDING" });
107
- }
108
-
109
- function openBrowser(url) {
110
- const command =
111
- process.platform === "darwin" ? ["open", [url]] : process.platform === "win32" ? ["cmd", ["/c", "start", "", url]] : ["xdg-open", [url]];
112
- const child = spawn(command[0], command[1], { detached: true, stdio: "ignore" });
113
- // A missing opener (headless Linux without xdg-open) surfaces as an async
114
- // 'error' event, not a throw. Printing the URL below is the fallback.
115
- child.on("error", () => {});
116
- child.unref();
117
- }
118
-
119
- // ------------------------------------------------------------------ commands
120
-
121
- async function openCommand(input) {
122
- const target = canonicalTarget(input);
123
- if (target.kind === "file" && !fs.existsSync(target.value)) {
124
- console.error(`File not found: ${target.value}`);
125
- process.exit(1);
126
- }
127
- const server = await ensureServer();
128
- const res = await request(server, { method: "POST", path: "/api/session", headers: { "content-type": "application/json" } }, { target: target.value });
129
- const body = JSON.parse(res.raw);
130
- if (res.status !== 200) {
131
- console.error(body.error || "Could not open that file.");
132
- process.exit(1);
133
- }
134
- const url = `http://127.0.0.1:${server.port}${body.path}`;
135
- openBrowser(url);
136
- console.log(`Reviewing ${target.kind === "url" ? target.value : path.basename(target.value)}`);
137
- console.log(url);
138
- console.log(`\nWaiting for feedback? Run:\n doc-review poll ${shellQuote(target.value)}`);
139
- }
140
-
141
- /**
142
- * The consumer is an agent reading a pipe. process.exit() does not wait for
143
- * pending stdout writes, so a large payload could arrive truncated — always
144
- * wait for the write to hand off before returning.
145
- */
146
- function writeStdout(text) {
147
- return new Promise((resolve) => process.stdout.write(text, resolve));
148
- }
149
-
150
- async function pollCommand(input, { ackId = "", timeoutSecs = DEFAULT_POLL_SECONDS } = {}) {
151
- const deadline = createDeadline(timeoutSecs);
152
- const target = canonicalTarget(input).value;
153
-
154
- const label = /^https?:\/\//i.test(target) ? target : path.basename(target);
155
- process.stderr.write(`Waiting for feedback on ${label} — comment in the browser, then hit Send.\n`);
156
-
157
- const batch = await pollUntilDeadline({
158
- target, ackId, deadline, discover: ensureServer,
159
- diagnostic: (text) => process.stderr.write(text),
160
- });
161
- await writeStdout(`${JSON.stringify(batch, null, 2)}\n`);
162
- }
163
-
164
- /**
165
- * Instant answer, no blocking. Asks the running server when there is one;
166
- * otherwise reads the persisted state directly, so a dead server still
167
- * reports feedback that is waiting for a fresh poll.
168
- */
169
- async function statusCommand(input) {
170
- const target = canonicalTarget(input).value;
171
- const saved = readServerRecord();
172
- if (serverProtocolMatches(saved?.protocol) && saved.port && saved.instance_id && (await alive(saved))) {
173
- const res = await request(saved, { method: "GET", path: `/api/status?target=${encodeURIComponent(target)}` });
174
- if (res.status === 200) {
175
- process.stdout.write(`${JSON.stringify(JSON.parse(res.raw), null, 2)}\n`);
176
- return;
177
- }
178
- }
179
-
180
- let data = { pages: {}, batches: {} };
181
- try {
182
- data = JSON.parse(fs.readFileSync(statePath(), "utf8"));
183
- } catch {
184
- // No state yet: everything below reads as empty.
185
- }
186
- const key = targetKey(target);
187
- const pending = (data.batches || {})[key];
188
- const page = (data.pages || {})[key];
189
- const payload = {
190
- status: pending ? "feedback-waiting" : "idle",
191
- feedback_waiting: !!pending,
192
- agent_listening: false,
193
- server_running: false,
194
- unsent: {
195
- comments: page ? page.comments.length : 0,
196
- edits: page ? page.edits.length : 0,
197
- },
198
- };
199
- process.stdout.write(`${JSON.stringify(payload, null, 2)}\n`);
200
- }
201
-
202
- // ---------------------------------------------------------------------- main
203
-
204
- const argv = process.argv.slice(2);
205
-
206
- if (argv.length === 0 || argv[0] === "--help" || argv[0] === "-h" || argv[0] === "help") {
207
- console.log(HELP);
208
- process.exit(0);
209
- }
210
-
211
- if (argv[0] === "--version" || argv[0] === "-v") {
212
- console.log(pkg.version);
213
- process.exit(0);
214
- }
215
-
216
- process.on("SIGINT", () => {
217
- process.stderr.write("\nStopped waiting. Your feedback is safe — run the same command again to pick it up.\n");
218
- process.exit(130);
219
- });
220
-
221
- function parsePollArgs(rest) {
222
- const parsed = { file: "", ackId: "", timeoutSecs: DEFAULT_POLL_SECONDS };
223
- let sawTimeout = false;
224
- for (let i = 0; i < rest.length; i += 1) {
225
- const arg = rest[i];
226
- if (arg === "--ack") {
227
- const value = rest[(i += 1)];
228
- if (!value || value.startsWith("-")) {
229
- throw new Error("--ack requires the batch_id from the feedback response, e.g. --ack b_123");
230
- }
231
- parsed.ackId = value;
232
- }
233
- else if (arg.startsWith("--ack=")) {
234
- throw new Error("Use --ack <batch_id> with the batch ID as a separate argument.");
235
- }
236
- else if (arg === "--timeout") {
237
- sawTimeout = true;
238
- parsed.timeoutSecs = Number(rest[(i += 1)]);
239
- } else if (arg.startsWith("--timeout=")) {
240
- sawTimeout = true;
241
- parsed.timeoutSecs = Number(arg.slice("--timeout=".length));
242
- } else if (!arg.startsWith("-") && !parsed.file) parsed.file = arg;
243
- else throw new Error(`Unknown poll argument: ${arg}`);
244
- }
245
- // A malformed value must fail loudly — NaN or 0 silently waiting forever is
246
- // the exact hang the flag exists to prevent.
247
- if (sawTimeout && (!Number.isFinite(parsed.timeoutSecs) || parsed.timeoutSecs <= 0)) {
248
- throw new Error("--timeout wants a number of seconds, e.g. --timeout 300");
249
- }
250
- return parsed;
251
- }
252
-
253
- try {
254
- if (argv[0] === "poll") {
255
- const { file, ackId, timeoutSecs } = parsePollArgs(argv.slice(1));
256
- if (!file) throw new Error("Usage: doc-review poll <file-or-localhost-url> [--ack <batch_id>] [--timeout <secs>]");
257
- await pollCommand(file, { ackId, timeoutSecs });
258
- } else if (argv[0] === "status") {
259
- const file = argv.find((a, i) => i > 0 && !a.startsWith("-"));
260
- if (!file) throw new Error("Usage: doc-review status <file-or-localhost-url>");
261
- await statusCommand(file);
262
- } else if (argv[0] === "setup") {
263
- const isGlobal = argv.includes("--global") || argv.includes("-g");
264
- installSkills(process.cwd(), { global: isGlobal }).forEach((line) => console.log(line));
265
- } else {
266
- await openCommand(argv[0]);
267
- }
268
- } catch (err) {
269
- console.error(err.message || String(err));
270
- process.exit(1);
271
- }
@@ -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
- }