@filipc77/cowrite 0.4.28 → 0.6.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.
package/README.md CHANGED
@@ -1,34 +1,34 @@
1
1
  # Cowrite
2
2
 
3
- Live commenting plugin for coding agent sessions. Select text in a browser preview, leave comments, and your coding agent receives them in real time via MCP. Works with any MCP-compatible coding agent — optimized for Claude Code with auto-installed hooks and skills.
3
+ Live commenting and inline editing plugin for coding agent sessions. Select text to comment, click to edit your coding agent receives all changes in real time via MCP.
4
4
 
5
5
  **The problem:** When working with AI coding agents, there's no way to give inline, contextual feedback on specific parts of a file while the agent is working. You either interrupt with a chat message (losing spatial context) or wait until the agent is done.
6
6
 
7
- **The solution:** Cowrite opens a live preview of any text file where you can select text and leave comments. The comments propagate directly into the agent session via MCP tools — so the agent can act on your feedback immediately. Any agent that supports MCP can use Cowrite's tools (`get_pending_comments`, `resolve_comment`, etc.). Claude Code users get additional integration: auto-installed hooks that surface comments on every prompt, and `/review` + `/watch` skills.
7
+ The solution: Cowrite opens a live preview of any text file where you can select text and leave comments. The comments propagate directly into the agent ssss — so the agent can act on your feedback immediately. Any agent that supports MCP can use Cowrite's tools (get_pending_comments, resolve_comment, etc.). Claude Code users get additional integration: auto-installed hooks that surface comments on every prompt, and /review + /watch skills. The preview also supports inline editing: click any block to edit it, insert new blocks with the + button, and undo with Cmd+Z — all changes are saved to disk instantly.
8
8
 
9
9
  ## How it works
10
10
 
11
11
  ```
12
12
  Browser (Preview UI) Node.js Process Claude Code
13
- ┌─────────────────┐ ┌──────────────────────┐ ┌──────────────┐
14
- │ File preview │ │ HTTP + WebSocket │ │
15
- │ Text selection │◄───►│ server (port 3377) │ │ MCP tools:
16
- │ Comment creation │ │ │ │
17
- │ Comment sidebar │ │ ┌────────────────┐ │ │ get_pending
18
- └─────────────────┘ │ │ CommentStore │ │ │ resolve
19
- │ │ (shared memory)│ │ │ reply
20
- │ └───────┬────────┘ │ │ get_annotated│
21
- │ │ │ │ wait_for_comment│
22
- │ ┌───────▼────────┐ │ │
23
- │ │ MCP Server │◄─╋────►│
24
- │ │ (stdio) │ │ │
25
- │ └────────────────┘ │ └──────────────┘
13
+ ┌──────────────────┐ ┌──────────────────────┐ ┌────────────────────┐
14
+ │ File preview │ │ HTTP + WebSocket │ │
15
+ │ Text selection │◄───►│ server (port 3377) │ │ MCP tools:
16
+ │ Comment creation │ │ │ │
17
+ │ Comment sidebar │ │ ┌────────────────┐ │ │ get_pending
18
+ └──────────────────┘ │ │ CommentStore │ │ │ resolve
19
+ │ │ (shared memory)│ │ │ reply
20
+ │ └───────┬────────┘ │ │ get_annotated
21
+ │ │ │ │ wait_for_comment
22
+ │ ┌───────▼────────┐ │ │
23
+ │ │ MCP Server │◄─╋────►│
24
+ │ │ (stdio) │ │ │
25
+ │ └────────────────┘ │ └────────────────────┘
26
26
  │ │
27
27
  │ File Watcher │
28
28
  └──────────────────────┘
29
29
  ```
30
30
 
31
- A single Node.js process runs both the HTTP/WebSocket preview server and the MCP stdio server, sharing one in-memory comment store.
31
+ A single Node.js process runs both the HTTP/WebSocket preview server and the MCP stdio server, sharing one in-memory comment store. Inline edits from the browser are applied to the file on disk via WebSocket, and the file watcher propagates changes back to all connected clients.
32
32
 
33
33
  ## Quick Start
34
34
 
@@ -67,9 +67,11 @@ cowrite open
67
67
 
68
68
  Pick a file and you'll see a live preview.
69
69
 
70
- ### 4. Select text and comment
70
+ ### 4. Comment or edit
71
71
 
72
- Select any text in the preview. A **Comment** button appears — click it to open the comment form. Your text selection stays intact, so you can still copy-paste normally.
72
+ **Commenting:** Select any text in the preview. A **Comment** button appears — click it to open the comment form. Your text selection stays intact, so you can still copy-paste normally.
73
+
74
+ **Editing:** Click any block in the preview to edit it inline (contentEditable for text, textarea for code blocks and tables). Press the **+** button between blocks to insert a new block from a searchable menu. Press **Cmd+Enter** to save your edit, **Escape** to cancel, or **Cmd+Z** to undo (up to 50 levels).
73
75
 
74
76
  ### 5. The agent picks up your comments
75
77
 
@@ -168,6 +170,9 @@ Blocks until a new comment is posted in the live preview, then returns it. If pe
168
170
  - **Agent replies** — The agent can reply to comments, and replies appear in the browser sidebar in real time.
169
171
  - **Auto-port selection** — If port 3377 is in use (e.g. running cowrite in multiple repos), it automatically tries the next port (3378, 3379, etc.).
170
172
  - **Auto-installed hooks** — `cowrite serve` automatically installs Claude Code `UserPromptSubmit` and `Stop` hooks that surface pending comments to the agent. Merges with existing settings.
173
+ - **Inline editing** — click any block to edit directly (contentEditable for text, textarea for code/tables)
174
+ - **Block insertion** — + button between blocks, searchable menu
175
+ - **Undo** — Cmd+Z or header button, up to 50 levels, persisted across file switches
171
176
 
172
177
  ## Skills
173
178
 
@@ -190,6 +195,14 @@ Cowrite ships with two built-in Claude Code skills (auto-installed to `.claude/s
190
195
  6. The agent makes the change, calls `reply_to_comment` (visible in the browser), then `resolve_comment`
191
196
  7. The browser receives the update via WebSocket and shows the reply and resolved state
192
197
 
198
+ ## How Edits Flow
199
+
200
+ 1. You click a block in the browser preview to enter edit mode
201
+ 2. After editing, press Cmd+Enter to save — the browser sends an `edit_apply` message via WebSocket
202
+ 3. The server writes the updated content to the file on disk
203
+ 4. The file watcher detects the change and broadcasts the new content to all connected clients
204
+ 5. Comments re-anchor to their selected text in the updated file
205
+
193
206
  ## Development
194
207
 
195
208
  ```bash
@@ -108,6 +108,26 @@ var CommentStore = class extends EventEmitter {
108
108
  `));
109
109
  return comment;
110
110
  }
111
+ reopen(commentId) {
112
+ const comment = this.comments.get(commentId);
113
+ if (!comment || comment.status !== "resolved") return null;
114
+ comment.status = "pending";
115
+ comment.resolvedAt = null;
116
+ this.emit("change", comment);
117
+ this.emit("comment_reopened", comment);
118
+ this.persist().catch((err) => process.stderr.write(`Persist error: ${err}
119
+ `));
120
+ return comment;
121
+ }
122
+ delete(commentId) {
123
+ const existed = this.comments.delete(commentId);
124
+ if (existed) {
125
+ this.emit("change", null);
126
+ this.persist().catch((err) => process.stderr.write(`Persist error: ${err}
127
+ `));
128
+ }
129
+ return existed;
130
+ }
111
131
  addReply(commentId, from, text) {
112
132
  const comment = this.comments.get(commentId);
113
133
  if (!comment) return null;
@@ -118,6 +138,13 @@ var CommentStore = class extends EventEmitter {
118
138
  createdAt: (/* @__PURE__ */ new Date()).toISOString()
119
139
  };
120
140
  comment.replies.push(reply);
141
+ if (from === "agent" && comment.status === "pending") {
142
+ comment.status = "answered";
143
+ }
144
+ if (from === "user" && comment.status === "answered") {
145
+ comment.status = "pending";
146
+ this.emit("comment_reopened", comment);
147
+ }
121
148
  this.emit("change", comment);
122
149
  this.persist().catch((err) => process.stderr.write(`Persist error: ${err}
123
150
  `));
@@ -144,6 +171,7 @@ var CommentStore = class extends EventEmitter {
144
171
  const fileComments = this.getForFile(file);
145
172
  if (fileComments.length === 0) return;
146
173
  for (const comment of fileComments) {
174
+ if (!comment.selectedText) continue;
147
175
  const searchStart = Math.max(0, comment.offset - 200);
148
176
  const searchEnd = Math.min(newContent.length, comment.offset + comment.length + 200);
149
177
  const searchRegion = newContent.slice(searchStart, searchEnd);
@@ -170,7 +198,8 @@ import { z } from "zod";
170
198
 
171
199
  // src/utils.ts
172
200
  import { exec } from "child_process";
173
- import { marked } from "marked";
201
+ import { marked, Renderer } from "marked";
202
+ import hljs from "highlight.js";
174
203
  function openBrowser(url) {
175
204
  const cmd = process.platform === "darwin" ? `open "${url}"` : process.platform === "win32" ? `cmd /c start "" "${url}"` : `xdg-open "${url}"`;
176
205
  return new Promise((resolve6) => {
@@ -189,19 +218,77 @@ function renderToHtml(content, filePath) {
189
218
  return renderPlainTextWithOffsets(content);
190
219
  }
191
220
  function renderMarkdownWithOffsets(content) {
192
- const html = marked.parse(content, { async: false });
193
- return `<div class="markdown-body" data-source-length="${content.length}">${html}</div>`;
221
+ const tokens = marked.lexer(content);
222
+ const blocks = [];
223
+ let blockOffset = 0;
224
+ for (const token of tokens) {
225
+ if (token.type !== "space") {
226
+ blocks.push({ sourceStart: blockOffset, sourceEnd: blockOffset + token.raw.length });
227
+ }
228
+ blockOffset += token.raw.length;
229
+ }
230
+ const renderer = new Renderer();
231
+ const defaultCodeRenderer = renderer.code.bind(renderer);
232
+ renderer.code = function(token) {
233
+ if (token.lang === "mermaid") {
234
+ return `<div class="mermaid-container"><pre class="mermaid">${token.text}</pre></div>`;
235
+ }
236
+ try {
237
+ let highlighted;
238
+ let lang;
239
+ if (token.lang && hljs.getLanguage(token.lang)) {
240
+ const result = hljs.highlight(token.text, { language: token.lang });
241
+ highlighted = result.value;
242
+ lang = token.lang;
243
+ } else {
244
+ const result = hljs.highlightAuto(token.text);
245
+ highlighted = result.value;
246
+ lang = result.language || "plaintext";
247
+ }
248
+ return `<div class="code-block-wrapper" data-lang="${lang}"><div class="code-block-header"><span class="code-block-lang">${lang}</span><button class="code-copy-btn" type="button">Copy</button></div><pre><code class="hljs language-${lang}">${highlighted}</code></pre></div>`;
249
+ } catch {
250
+ return defaultCodeRenderer(token);
251
+ }
252
+ };
253
+ const html = marked.parse(content, { async: false, renderer });
254
+ const blocksAttr = JSON.stringify(blocks).replace(/"/g, "&quot;");
255
+ return `<div class="markdown-body" data-source-length="${content.length}" data-blocks="${blocksAttr}">${html}</div>`;
194
256
  }
195
257
  function renderPlainTextWithOffsets(content) {
196
258
  const lines = content.split("\n");
197
259
  let offset = 0;
198
- const htmlLines = [];
199
- for (const line of lines) {
200
- const escaped = escapeHtml(line);
201
- htmlLines.push(`<span class="line" data-offset="${offset}" data-length="${line.length}">${escaped}</span>`);
260
+ const blocks = [];
261
+ const htmlParts = [];
262
+ let currentLines = [];
263
+ let blockStart = 0;
264
+ let blockIndex = 0;
265
+ for (let i = 0; i < lines.length; i++) {
266
+ const line = lines[i];
267
+ const lineOffset = offset;
202
268
  offset += line.length + 1;
269
+ if (line.trim() === "") {
270
+ if (currentLines.length > 0) {
271
+ blocks.push({ sourceStart: blockStart, sourceEnd: lineOffset });
272
+ htmlParts.push(`<div class="text-block" data-block-index="${blockIndex}">${currentLines.join("\n")}</div>`);
273
+ blockIndex++;
274
+ currentLines = [];
275
+ }
276
+ htmlParts.push("");
277
+ blockStart = offset;
278
+ } else {
279
+ if (currentLines.length === 0) {
280
+ blockStart = lineOffset;
281
+ }
282
+ const escaped = escapeHtml(line);
283
+ currentLines.push(`<span class="line" data-offset="${lineOffset}" data-length="${line.length}">${escaped}</span>`);
284
+ }
285
+ }
286
+ if (currentLines.length > 0) {
287
+ blocks.push({ sourceStart: blockStart, sourceEnd: Math.min(offset, content.length) });
288
+ htmlParts.push(`<div class="text-block" data-block-index="${blockIndex}">${currentLines.join("\n")}</div>`);
203
289
  }
204
- return `<pre class="plain-text" data-source-length="${content.length}">${htmlLines.join("\n")}</pre>`;
290
+ const blocksAttr = JSON.stringify(blocks).replace(/"/g, "&quot;");
291
+ return `<pre class="plain-text" data-source-length="${content.length}" data-blocks="${blocksAttr}">${htmlParts.join("\n")}</pre>`;
205
292
  }
206
293
  function escapeHtml(text) {
207
294
  return text.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
@@ -210,6 +297,11 @@ function annotateFileWithComments(content, comments) {
210
297
  const sorted = [...comments].sort((a, b) => b.offset - a.offset);
211
298
  let result = content;
212
299
  for (const c of sorted) {
300
+ if (!c.selectedText) {
301
+ result = `[FILE COMMENT #${c.id.slice(0, 8)}: "${c.comment}"]
302
+ ` + result;
303
+ continue;
304
+ }
213
305
  const marker = `[COMMENT #${c.id.slice(0, 8)}: "${c.comment}"]`;
214
306
  const end = c.offset + c.length;
215
307
  result = result.slice(0, end) + " " + marker + result.slice(end);
@@ -230,7 +322,7 @@ function createMcpServer(store, projectDir, getPreviewPort) {
230
322
  "Get comments from the live preview (0 pending). Call this first to catch comments posted before you started listening.",
231
323
  {
232
324
  file: z.string().optional().describe("Filter by file path"),
233
- status: z.enum(["pending", "resolved", "all"]).optional().describe("Filter by status (default: pending)")
325
+ status: z.enum(["pending", "answered", "resolved", "all"]).optional().describe("Filter by status (default: pending)")
234
326
  },
235
327
  async ({ file, status }) => {
236
328
  const filter = {};
@@ -247,28 +339,9 @@ function createMcpServer(store, projectDir, getPreviewPort) {
247
339
  };
248
340
  }
249
341
  );
250
- server.tool(
251
- "resolve_comment",
252
- "Mark a comment as resolved/addressed.",
253
- {
254
- commentId: z.string().describe("The comment ID to resolve")
255
- },
256
- async ({ commentId }) => {
257
- const comment = store.resolve(commentId);
258
- if (!comment) {
259
- return {
260
- content: [{ type: "text", text: `Comment ${commentId} not found.` }],
261
- isError: true
262
- };
263
- }
264
- return {
265
- content: [{ type: "text", text: `Comment ${commentId} resolved.` }]
266
- };
267
- }
268
- );
269
342
  server.tool(
270
343
  "reply_to_comment",
271
- "Reply to a comment from the agent.",
344
+ "Reply to a comment from the agent. Your reply automatically marks the comment as 'answered'. The user reviews it and can resolve or reply back.",
272
345
  {
273
346
  commentId: z.string().describe("The comment ID to reply to"),
274
347
  reply: z.string().describe("The reply text")
@@ -355,9 +428,11 @@ function createMcpServer(store, projectDir, getPreviewPort) {
355
428
  const cleanup = () => {
356
429
  clearTimeout(timer);
357
430
  store.off("new_comment", onComment);
431
+ store.off("comment_reopened", onComment);
358
432
  signal?.removeEventListener("abort", onAbort);
359
433
  };
360
434
  store.on("new_comment", onComment);
435
+ store.on("comment_reopened", onComment);
361
436
  signal?.addEventListener("abort", onAbort, { once: true });
362
437
  if (signal?.aborted) {
363
438
  onAbort();
@@ -407,7 +482,7 @@ function createMcpServer(store, projectDir, getPreviewPort) {
407
482
  }).catch(() => {
408
483
  });
409
484
  });
410
- store.on("new_comment", (comment) => {
485
+ function notifyCommentNeedsAttention(comment, prefix) {
411
486
  if (!server.isConnected()) return;
412
487
  const count = store.getAll({ status: "pending" }).length;
413
488
  const file = relative(projectDir, comment.file);
@@ -419,15 +494,22 @@ function createMcpServer(store, projectDir, getPreviewPort) {
419
494
  server.sendToolListChanged();
420
495
  } catch {
421
496
  }
497
+ const logMsg = comment.selectedText ? `${prefix} on ${file}: "${comment.comment}" (selected: "${selectedPreview}"). Call get_pending_comments to see it.` : `${prefix} on ${file}: "${comment.comment}". Call get_pending_comments to see it.`;
422
498
  server.sendLoggingMessage({
423
499
  level: "warning",
424
- data: `NEW COMMENT on ${file}: "${comment.comment}" (selected: "${selectedPreview}"). Call get_pending_comments to see it.`
500
+ data: logMsg
425
501
  }).catch(() => {
426
502
  });
427
503
  server.server.notification({
428
504
  method: "notifications/resources/list_changed"
429
505
  }).catch(() => {
430
506
  });
507
+ }
508
+ store.on("new_comment", (comment) => {
509
+ notifyCommentNeedsAttention(comment, "NEW COMMENT");
510
+ });
511
+ store.on("comment_reopened", (comment) => {
512
+ notifyCommentNeedsAttention(comment, "COMMENT REOPENED");
431
513
  });
432
514
  store.on("change", () => {
433
515
  const count = store.getAll({ status: "pending" }).length;
@@ -440,7 +522,7 @@ function createMcpServer(store, projectDir, getPreviewPort) {
440
522
  });
441
523
  server.prompt(
442
524
  "cowrite-workflow",
443
- "How to process live preview comments in a wait-handle-resolve loop",
525
+ "How to process live preview comments in a wait-handle-reply loop",
444
526
  () => ({
445
527
  messages: [
446
528
  {
@@ -450,17 +532,21 @@ function createMcpServer(store, projectDir, getPreviewPort) {
450
532
  text: [
451
533
  "You are monitoring a live code preview where users leave comments on selected text.",
452
534
  "",
535
+ "Comment lifecycle: pending \u2192 answered (auto on your reply) \u2192 resolved (user only).",
536
+ "If the user disagrees with your answer, they reply back and it returns to pending.",
537
+ "",
453
538
  "Follow this loop:",
454
539
  "1. Call `get_pending_comments` to check for any comments already posted.",
455
- "2. Process each pending comment: read the file, make the requested change or reply, then call `resolve_comment`.",
456
- "3. Call `wait_for_comment` to block until the next comment arrives.",
540
+ "2. Process each pending comment: read the file, make the requested change, then call `reply_to_comment` to explain what you did.",
541
+ " Your reply automatically marks the comment as 'answered'. The user will review and resolve it.",
542
+ "3. Call `wait_for_comment` to block until the next comment (or reopened comment) arrives.",
457
543
  "4. When a comment arrives, process it the same way (step 2).",
458
544
  "5. Go back to step 3 and keep listening.",
459
545
  "",
460
546
  "Tips:",
461
547
  "- Use `get_file_with_annotations` to see comments in context within the file.",
462
548
  "- Use `reply_to_comment` to acknowledge or ask clarifying questions.",
463
- "- Always `resolve_comment` after addressing feedback."
549
+ "- Do NOT resolve comments \u2014 the user does that after reviewing your work."
464
550
  ].join("\n")
465
551
  }
466
552
  }
@@ -472,7 +558,7 @@ function createMcpServer(store, projectDir, getPreviewPort) {
472
558
 
473
559
  // src/preview-server.ts
474
560
  import { createServer } from "http";
475
- import { readFile as readFile4, readdir } from "fs/promises";
561
+ import { readFile as readFile4, readdir, writeFile as writeFile2 } from "fs/promises";
476
562
  import { existsSync } from "fs";
477
563
  import { join as join2, resolve as resolve4 } from "path";
478
564
  import { WebSocketServer } from "ws";
@@ -660,10 +746,10 @@ function createPreviewServer(store, projectDir, port, initialFile) {
660
746
  const absPath = resolve4(resolvedProjectDir, initialFile);
661
747
  await switchClientFile(ws, absPath);
662
748
  }
663
- ws.on("message", (data) => {
749
+ ws.on("message", async (data) => {
664
750
  try {
665
751
  const msg = JSON.parse(data.toString());
666
- handleClientMessage(ws, msg);
752
+ await handleClientMessage(ws, msg);
667
753
  } catch (err) {
668
754
  send(ws, { type: "error", message: `Invalid message: ${err}` });
669
755
  }
@@ -690,11 +776,11 @@ function createPreviewServer(store, projectDir, port, initialFile) {
690
776
  send(ws, { type: "error", message: `Cannot open file: ${err}` });
691
777
  }
692
778
  }
693
- function handleClientMessage(ws, msg) {
779
+ async function handleClientMessage(ws, msg) {
694
780
  switch (msg.type) {
695
781
  case "switch_file": {
696
782
  const absPath = resolve4(resolvedProjectDir, msg.file);
697
- switchClientFile(ws, absPath);
783
+ await switchClientFile(ws, absPath);
698
784
  break;
699
785
  }
700
786
  case "comment_add": {
@@ -715,6 +801,26 @@ function createPreviewServer(store, projectDir, port, initialFile) {
715
801
  case "comment_resolve":
716
802
  store.resolve(msg.commentId);
717
803
  break;
804
+ case "comment_reopen":
805
+ store.reopen(msg.commentId);
806
+ break;
807
+ case "comment_delete":
808
+ store.delete(msg.commentId);
809
+ break;
810
+ case "edit_apply": {
811
+ const file = clientFiles.get(ws);
812
+ if (!file) break;
813
+ const watcher = watchers.get(file);
814
+ if (!watcher) break;
815
+ const content = watcher.getContent();
816
+ if (msg.offset < 0 || msg.offset + msg.length > content.length) {
817
+ send(ws, { type: "error", message: "Edit offset/length out of bounds" });
818
+ break;
819
+ }
820
+ const newContent = content.slice(0, msg.offset) + msg.newText + content.slice(msg.offset + msg.length);
821
+ await writeFile2(file, newContent, "utf-8");
822
+ break;
823
+ }
718
824
  }
719
825
  }
720
826
  store.on("change", (comment) => {
@@ -779,6 +885,9 @@ function createPreviewServer(store, projectDir, port, initialFile) {
779
885
  }
780
886
 
781
887
  // bin/cowrite.ts
888
+ import updateNotifier from "update-notifier";
889
+ import "module";
890
+ var version = true ? "0.6.0" : createRequire(import.meta.url)("../package.json").version;
782
891
  var USAGE = `
783
892
  cowrite \u2014 Live commenting plugin for coding agent sessions
784
893
 
@@ -834,7 +943,7 @@ COMMENTS_FILE="\${CLAUDE_PROJECT_DIR:-.}/.cowrite-comments.json"
834
943
  if [ ! -f "$COMMENTS_FILE" ] || [ ! -s "$COMMENTS_FILE" ]; then exit 0; fi
835
944
  PENDING=$(jq '[.[] | select(.status == "pending")] | length' "$COMMENTS_FILE" 2>/dev/null || echo 0)
836
945
  if [ "$PENDING" -eq 0 ]; then exit 0; fi
837
- jq -r '[.[] | select(.status == "pending")] | "COWRITE: \\(length) pending comment(s) from the live preview. For EACH comment: (1) make the requested change, (2) call reply_to_comment to explain what you did \u2014 the user reads replies in the browser, and (3) call resolve_comment.\\n" + ([.[] | "- [\\(.id)] File: \\(.file | split("/") | last) | Text: \\"\\(.selectedText)\\" | Comment: \\(.comment)"] | join("\\n"))' "$COMMENTS_FILE" 2>/dev/null
946
+ jq -r '[.[] | select(.status == "pending")] | "COWRITE: \\(length) pending comment(s) from the live preview. For EACH comment: (1) make the requested change, (2) call reply_to_comment to explain what you did. Your reply automatically marks it as answered. The user will review and resolve it.\\n" + ([.[] | "- [\\(.id)] File: \\(.file | split("/") | last) | Text: \\"\\(.selectedText)\\" | Comment: \\(.comment)"] | join("\\n"))' "$COMMENTS_FILE" 2>/dev/null
838
947
  `;
839
948
  var HOOK_ENTRY = {
840
949
  matcher: "",
@@ -860,8 +969,7 @@ Check for any pending comments left in the Cowrite live preview and address them
860
969
  a. Read the comment text and the selected text it refers to.
861
970
  b. Use \`get_file_with_annotations\` to see the comment in context.
862
971
  c. Make the requested change or reply explaining why you can't.
863
- d. Call \`reply_to_comment\` to acknowledge the feedback.
864
- e. Call \`resolve_comment\` to mark it as addressed.
972
+ d. Call \`reply_to_comment\` to acknowledge the feedback. Your reply automatically marks it as "answered". The user will review and resolve it.
865
973
  3. Summarize what was done.
866
974
  `;
867
975
  var SKILL_WATCH = `---
@@ -878,12 +986,12 @@ Start a background agent that watches for cowrite comments and handles them as t
878
986
 
879
987
  1. First, handle any existing pending comments:
880
988
  a. Call \`get_pending_comments\` to check for unresolved comments.
881
- b. For each pending comment, use \`get_file_with_annotations\` to see context, make the change, call \`reply_to_comment\`, and call \`resolve_comment\`.
989
+ b. For each pending comment, use \`get_file_with_annotations\` to see context, make the change, and call \`reply_to_comment\`. Your reply automatically marks it as "answered".
882
990
 
883
991
  2. Then, launch a **background** watcher using the Task tool:
884
992
  - Use \`subagent_type: "general-purpose"\` and \`run_in_background: true\`
885
993
  - The background agent should call \`wait_for_comment\` in a loop
886
- - When a comment arrives, it handles it (read file, make change, reply, resolve)
994
+ - When a comment arrives, it handles it (read file, make change, reply)
887
995
  - On timeout, it re-calls \`wait_for_comment\` immediately
888
996
  - The loop continues until the user says stop
889
997
 
@@ -944,6 +1052,7 @@ async function main() {
944
1052
  process.exit(positionals.length === 0 && !values.help ? 1 : 0);
945
1053
  }
946
1054
  const command = positionals[0];
1055
+ updateNotifier({ pkg: { name: "@filipc77/cowrite", version } }).notify({ isGlobal: true });
947
1056
  const projectDir = process.cwd();
948
1057
  const port = parseInt(values.port, 10);
949
1058
  installClaudeIntegration(projectDir);