@filipc77/cowrite 0.5.0 → 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
@@ -10,19 +10,19 @@ The solution: Cowrite opens a live preview of any text file where you can select
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
  └──────────────────────┘
@@ -105,6 +105,17 @@ var CommentStore = class extends EventEmitter {
105
105
  comment.resolvedAt = (/* @__PURE__ */ new Date()).toISOString();
106
106
  this.emit("change", comment);
107
107
  this.persist().catch((err) => process.stderr.write(`Persist error: ${err}
108
+ `));
109
+ return comment;
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}
108
119
  `));
109
120
  return comment;
110
121
  }
@@ -127,6 +138,13 @@ var CommentStore = class extends EventEmitter {
127
138
  createdAt: (/* @__PURE__ */ new Date()).toISOString()
128
139
  };
129
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
+ }
130
148
  this.emit("change", comment);
131
149
  this.persist().catch((err) => process.stderr.write(`Persist error: ${err}
132
150
  `));
@@ -153,6 +171,7 @@ var CommentStore = class extends EventEmitter {
153
171
  const fileComments = this.getForFile(file);
154
172
  if (fileComments.length === 0) return;
155
173
  for (const comment of fileComments) {
174
+ if (!comment.selectedText) continue;
156
175
  const searchStart = Math.max(0, comment.offset - 200);
157
176
  const searchEnd = Math.min(newContent.length, comment.offset + comment.length + 200);
158
177
  const searchRegion = newContent.slice(searchStart, searchEnd);
@@ -278,6 +297,11 @@ function annotateFileWithComments(content, comments) {
278
297
  const sorted = [...comments].sort((a, b) => b.offset - a.offset);
279
298
  let result = content;
280
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
+ }
281
305
  const marker = `[COMMENT #${c.id.slice(0, 8)}: "${c.comment}"]`;
282
306
  const end = c.offset + c.length;
283
307
  result = result.slice(0, end) + " " + marker + result.slice(end);
@@ -298,7 +322,7 @@ function createMcpServer(store, projectDir, getPreviewPort) {
298
322
  "Get comments from the live preview (0 pending). Call this first to catch comments posted before you started listening.",
299
323
  {
300
324
  file: z.string().optional().describe("Filter by file path"),
301
- 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)")
302
326
  },
303
327
  async ({ file, status }) => {
304
328
  const filter = {};
@@ -315,28 +339,9 @@ function createMcpServer(store, projectDir, getPreviewPort) {
315
339
  };
316
340
  }
317
341
  );
318
- server.tool(
319
- "resolve_comment",
320
- "Mark a comment as resolved/addressed.",
321
- {
322
- commentId: z.string().describe("The comment ID to resolve")
323
- },
324
- async ({ commentId }) => {
325
- const comment = store.resolve(commentId);
326
- if (!comment) {
327
- return {
328
- content: [{ type: "text", text: `Comment ${commentId} not found.` }],
329
- isError: true
330
- };
331
- }
332
- return {
333
- content: [{ type: "text", text: `Comment ${commentId} resolved.` }]
334
- };
335
- }
336
- );
337
342
  server.tool(
338
343
  "reply_to_comment",
339
- "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.",
340
345
  {
341
346
  commentId: z.string().describe("The comment ID to reply to"),
342
347
  reply: z.string().describe("The reply text")
@@ -423,9 +428,11 @@ function createMcpServer(store, projectDir, getPreviewPort) {
423
428
  const cleanup = () => {
424
429
  clearTimeout(timer);
425
430
  store.off("new_comment", onComment);
431
+ store.off("comment_reopened", onComment);
426
432
  signal?.removeEventListener("abort", onAbort);
427
433
  };
428
434
  store.on("new_comment", onComment);
435
+ store.on("comment_reopened", onComment);
429
436
  signal?.addEventListener("abort", onAbort, { once: true });
430
437
  if (signal?.aborted) {
431
438
  onAbort();
@@ -475,7 +482,7 @@ function createMcpServer(store, projectDir, getPreviewPort) {
475
482
  }).catch(() => {
476
483
  });
477
484
  });
478
- store.on("new_comment", (comment) => {
485
+ function notifyCommentNeedsAttention(comment, prefix) {
479
486
  if (!server.isConnected()) return;
480
487
  const count = store.getAll({ status: "pending" }).length;
481
488
  const file = relative(projectDir, comment.file);
@@ -487,15 +494,22 @@ function createMcpServer(store, projectDir, getPreviewPort) {
487
494
  server.sendToolListChanged();
488
495
  } catch {
489
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.`;
490
498
  server.sendLoggingMessage({
491
499
  level: "warning",
492
- data: `NEW COMMENT on ${file}: "${comment.comment}" (selected: "${selectedPreview}"). Call get_pending_comments to see it.`
500
+ data: logMsg
493
501
  }).catch(() => {
494
502
  });
495
503
  server.server.notification({
496
504
  method: "notifications/resources/list_changed"
497
505
  }).catch(() => {
498
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");
499
513
  });
500
514
  store.on("change", () => {
501
515
  const count = store.getAll({ status: "pending" }).length;
@@ -508,7 +522,7 @@ function createMcpServer(store, projectDir, getPreviewPort) {
508
522
  });
509
523
  server.prompt(
510
524
  "cowrite-workflow",
511
- "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",
512
526
  () => ({
513
527
  messages: [
514
528
  {
@@ -518,17 +532,21 @@ function createMcpServer(store, projectDir, getPreviewPort) {
518
532
  text: [
519
533
  "You are monitoring a live code preview where users leave comments on selected text.",
520
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
+ "",
521
538
  "Follow this loop:",
522
539
  "1. Call `get_pending_comments` to check for any comments already posted.",
523
- "2. Process each pending comment: read the file, make the requested change or reply, then call `resolve_comment`.",
524
- "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.",
525
543
  "4. When a comment arrives, process it the same way (step 2).",
526
544
  "5. Go back to step 3 and keep listening.",
527
545
  "",
528
546
  "Tips:",
529
547
  "- Use `get_file_with_annotations` to see comments in context within the file.",
530
548
  "- Use `reply_to_comment` to acknowledge or ask clarifying questions.",
531
- "- Always `resolve_comment` after addressing feedback."
549
+ "- Do NOT resolve comments \u2014 the user does that after reviewing your work."
532
550
  ].join("\n")
533
551
  }
534
552
  }
@@ -783,6 +801,9 @@ function createPreviewServer(store, projectDir, port, initialFile) {
783
801
  case "comment_resolve":
784
802
  store.resolve(msg.commentId);
785
803
  break;
804
+ case "comment_reopen":
805
+ store.reopen(msg.commentId);
806
+ break;
786
807
  case "comment_delete":
787
808
  store.delete(msg.commentId);
788
809
  break;
@@ -866,7 +887,7 @@ function createPreviewServer(store, projectDir, port, initialFile) {
866
887
  // bin/cowrite.ts
867
888
  import updateNotifier from "update-notifier";
868
889
  import "module";
869
- var version = true ? "0.5.0" : createRequire(import.meta.url)("../package.json").version;
890
+ var version = true ? "0.6.0" : createRequire(import.meta.url)("../package.json").version;
870
891
  var USAGE = `
871
892
  cowrite \u2014 Live commenting plugin for coding agent sessions
872
893
 
@@ -922,7 +943,7 @@ COMMENTS_FILE="\${CLAUDE_PROJECT_DIR:-.}/.cowrite-comments.json"
922
943
  if [ ! -f "$COMMENTS_FILE" ] || [ ! -s "$COMMENTS_FILE" ]; then exit 0; fi
923
944
  PENDING=$(jq '[.[] | select(.status == "pending")] | length' "$COMMENTS_FILE" 2>/dev/null || echo 0)
924
945
  if [ "$PENDING" -eq 0 ]; then exit 0; fi
925
- 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
926
947
  `;
927
948
  var HOOK_ENTRY = {
928
949
  matcher: "",
@@ -948,8 +969,7 @@ Check for any pending comments left in the Cowrite live preview and address them
948
969
  a. Read the comment text and the selected text it refers to.
949
970
  b. Use \`get_file_with_annotations\` to see the comment in context.
950
971
  c. Make the requested change or reply explaining why you can't.
951
- d. Call \`reply_to_comment\` to acknowledge the feedback.
952
- 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.
953
973
  3. Summarize what was done.
954
974
  `;
955
975
  var SKILL_WATCH = `---
@@ -966,12 +986,12 @@ Start a background agent that watches for cowrite comments and handles them as t
966
986
 
967
987
  1. First, handle any existing pending comments:
968
988
  a. Call \`get_pending_comments\` to check for unresolved comments.
969
- 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".
970
990
 
971
991
  2. Then, launch a **background** watcher using the Task tool:
972
992
  - Use \`subagent_type: "general-purpose"\` and \`run_in_background: true\`
973
993
  - The background agent should call \`wait_for_comment\` in a loop
974
- - 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)
975
995
  - On timeout, it re-calls \`wait_for_comment\` immediately
976
996
  - The loop continues until the user says stop
977
997
 
@@ -1 +1 @@
1
- {"version":3,"sources":["../../bin/cowrite.ts","../../src/comment-store.ts","../../src/mcp-server.ts","../../src/utils.ts","../../src/preview-server.ts","../../src/file-watcher.ts"],"sourcesContent":["import { parseArgs } from \"node:util\";\nimport { resolve, join } from \"node:path\";\nimport { existsSync, mkdirSync, writeFileSync, readFileSync, chmodSync, unlinkSync } from \"node:fs\";\nimport { StdioServerTransport } from \"@modelcontextprotocol/sdk/server/stdio.js\";\nimport { CommentStore } from \"../src/comment-store.js\";\nimport { createMcpServer } from \"../src/mcp-server.js\";\nimport { createPreviewServer } from \"../src/preview-server.js\";\nimport { openBrowser } from \"../src/utils.js\";\nimport updateNotifier from \"update-notifier\";\nimport { createRequire } from \"node:module\";\n\ndeclare const __COWRITE_VERSION__: string | undefined;\nconst version: string = typeof __COWRITE_VERSION__ !== \"undefined\"\n ? __COWRITE_VERSION__\n : (createRequire(import.meta.url)(\"../package.json\") as { version: string }).version;\n\nconst USAGE = `\ncowrite — Live commenting plugin for coding agent sessions\n\nUsage:\n cowrite init Install hooks and skills into .claude/ (run once per project)\n cowrite serve [--port N] Start MCP server + preview server (browse any file)\n cowrite preview <file> [--port N] Open browser preview for a specific file + start MCP server\n cowrite open [--port N] Open the browser to the preview URL\n\nOptions:\n --port, -p Port for preview server (default: 3377)\n --no-open Don't auto-open the browser\n --help, -h Show this help\n`;\n\nconst PORT_FILE = \".cowrite-port\";\n\nfunction writePortFile(projectDir: string, port: number): void {\n writeFileSync(join(projectDir, PORT_FILE), String(port), \"utf-8\");\n}\n\nfunction removePortFile(projectDir: string): void {\n try { unlinkSync(join(projectDir, PORT_FILE)); } catch {}\n}\n\nfunction readPortFile(projectDir: string): number | null {\n try {\n const content = readFileSync(join(projectDir, PORT_FILE), \"utf-8\").trim();\n const port = parseInt(content, 10);\n return isNaN(port) ? null : port;\n } catch {\n return null;\n }\n}\n\nfunction setupShutdown(store: CommentStore, preview: { stop: () => Promise<void>; [k: string]: any }, projectDir: string) {\n let shuttingDown = false;\n const shutdown = () => {\n if (shuttingDown) {\n // Second signal — force exit immediately\n process.exit(1);\n }\n shuttingDown = true;\n process.stderr.write(\"Shutting down...\\n\");\n removePortFile(projectDir);\n // Best-effort cleanup with a hard timeout\n Promise.allSettled([store.stopWatching(), preview.stop()])\n .finally(() => process.exit(0));\n setTimeout(() => process.exit(0), 2000);\n };\n process.on(\"SIGINT\", shutdown);\n process.on(\"SIGTERM\", shutdown);\n}\n\nconst HOOK_SCRIPT = `#!/bin/bash\n# Auto-installed by cowrite — injects pending comments into Claude Code context.\n# Only outputs when there are pending comments. Silent otherwise.\nCOMMENTS_FILE=\"\\${CLAUDE_PROJECT_DIR:-.}/.cowrite-comments.json\"\nif [ ! -f \"$COMMENTS_FILE\" ] || [ ! -s \"$COMMENTS_FILE\" ]; then exit 0; fi\nPENDING=$(jq '[.[] | select(.status == \"pending\")] | length' \"$COMMENTS_FILE\" 2>/dev/null || echo 0)\nif [ \"$PENDING\" -eq 0 ]; then exit 0; fi\njq -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 — 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\n`;\n\nconst HOOK_ENTRY = {\n matcher: \"\",\n hooks: [{\n type: \"command\",\n command: `bash \"$CLAUDE_PROJECT_DIR/.claude/hooks/inject-comments.sh\"`,\n }],\n};\n\nconst HOOK_SETTINGS = {\n hooks: {\n UserPromptSubmit: [HOOK_ENTRY],\n Stop: [HOOK_ENTRY],\n },\n};\n\nconst SKILL_REVIEW = `---\nname: review\ndescription: Check and address cowrite comments left by the user in the live preview\nuser_invocable: true\n---\n\n# Review Cowrite Comments\n\nCheck for any pending comments left in the Cowrite live preview and address them.\n\n## Steps\n\n1. Call the \\`get_pending_comments\\` tool to retrieve all unresolved comments.\n2. For each pending comment:\n a. Read the comment text and the selected text it refers to.\n b. Use \\`get_file_with_annotations\\` to see the comment in context.\n c. Make the requested change or reply explaining why you can't.\n d. Call \\`reply_to_comment\\` to acknowledge the feedback.\n e. Call \\`resolve_comment\\` to mark it as addressed.\n3. Summarize what was done.\n`;\n\nconst SKILL_WATCH = `---\nname: watch\ndescription: Start a background watcher for cowrite comments — does not block the main conversation\nuser_invocable: true\n---\n\n# Watch for Live Comments (Background)\n\nStart a background agent that watches for cowrite comments and handles them as they arrive. The main conversation stays free for other work.\n\n## Steps\n\n1. First, handle any existing pending comments:\n a. Call \\`get_pending_comments\\` to check for unresolved comments.\n b. For each pending comment, use \\`get_file_with_annotations\\` to see context, make the change, call \\`reply_to_comment\\`, and call \\`resolve_comment\\`.\n\n2. Then, launch a **background** watcher using the Task tool:\n - Use \\`subagent_type: \"general-purpose\"\\` and \\`run_in_background: true\\`\n - The background agent should call \\`wait_for_comment\\` in a loop\n - When a comment arrives, it handles it (read file, make change, reply, resolve)\n - On timeout, it re-calls \\`wait_for_comment\\` immediately\n - The loop continues until the user says stop\n\n3. Tell the user the background watcher is running and they can continue working normally. Comments will be handled automatically.\n`;\n\nfunction installClaudeIntegration(projectDir: string): void {\n const claudeDir = join(projectDir, \".claude\");\n const hooksDir = join(claudeDir, \"hooks\");\n const hookPath = join(hooksDir, \"inject-comments.sh\");\n const settingsPath = join(claudeDir, \"settings.json\");\n const reviewDir = join(claudeDir, \"skills\", \"review\");\n const watchDir = join(claudeDir, \"skills\", \"watch\");\n\n // Create directories\n for (const dir of [hooksDir, reviewDir, watchDir]) {\n if (!existsSync(dir)) {\n mkdirSync(dir, { recursive: true });\n }\n }\n\n // Write hook script (always overwrite to keep in sync with cowrite version)\n writeFileSync(hookPath, HOOK_SCRIPT, \"utf-8\");\n chmodSync(hookPath, 0o755);\n\n // Write skills (always overwrite to keep in sync)\n writeFileSync(join(reviewDir, \"SKILL.md\"), SKILL_REVIEW, \"utf-8\");\n writeFileSync(join(watchDir, \"SKILL.md\"), SKILL_WATCH, \"utf-8\");\n\n // Merge cowrite hooks into settings.json (preserve existing settings)\n let settings: any = {};\n if (existsSync(settingsPath)) {\n try {\n settings = JSON.parse(readFileSync(settingsPath, \"utf-8\"));\n } catch {\n // Corrupt JSON — overwrite\n }\n }\n if (!settings.hooks) settings.hooks = {};\n let changed = false;\n for (const eventType of [\"UserPromptSubmit\", \"Stop\"] as const) {\n if (!Array.isArray(settings.hooks[eventType])) settings.hooks[eventType] = [];\n const hasCowriteHook = settings.hooks[eventType].some((entry: any) =>\n entry.hooks?.some((h: any) => h.command?.includes(\"inject-comments.sh\"))\n );\n if (!hasCowriteHook) {\n settings.hooks[eventType].push(HOOK_ENTRY);\n changed = true;\n }\n }\n if (changed) {\n writeFileSync(settingsPath, JSON.stringify(settings, null, 2) + \"\\n\", \"utf-8\");\n }\n}\n\nasync function main() {\n const { values, positionals } = parseArgs({\n args: process.argv.slice(2),\n options: {\n port: { type: \"string\", short: \"p\", default: \"3377\" },\n \"no-open\": { type: \"boolean\", default: false },\n help: { type: \"boolean\", short: \"h\", default: false },\n },\n allowPositionals: true,\n strict: false,\n });\n\n if (values.help || positionals.length === 0) {\n process.stderr.write(USAGE);\n process.exit(positionals.length === 0 && !values.help ? 1 : 0);\n }\n\n const command = positionals[0];\n\n updateNotifier({ pkg: { name: \"@filipc77/cowrite\", version } }).notify({ isGlobal: true });\n\n const projectDir = process.cwd();\n const port = parseInt(values.port as string, 10);\n\n // Auto-install Claude Code hooks for comment propagation\n installClaudeIntegration(projectDir);\n\n if (command === \"serve\") {\n const store = new CommentStore(projectDir);\n await store.load();\n await store.startWatching();\n\n // Start preview server (non-fatal — MCP works even if port is taken)\n const preview = createPreviewServer(store, projectDir, port);\n let previewRunning = false;\n try {\n await preview.start();\n previewRunning = true;\n writePortFile(projectDir, preview.port);\n const previewUrl = `http://localhost:${preview.port}`;\n process.stderr.write(`Preview: ${previewUrl}\\n`);\n if (!values[\"no-open\"]) openBrowser(previewUrl);\n } catch (err) {\n process.stderr.write(`Preview server failed: ${err}\\n`);\n process.stderr.write(`MCP server will still run — comments sync via .cowrite-comments.json\\n`);\n }\n\n // Start MCP server on stdio\n const mcpServer = createMcpServer(store, projectDir, () => previewRunning ? preview.port : null);\n const transport = new StdioServerTransport();\n await mcpServer.connect(transport);\n\n process.stderr.write(`Cowrite MCP server running on stdio\\n`);\n\n setupShutdown(store, preview, projectDir);\n } else if (command === \"preview\") {\n const filePath = positionals[1];\n if (!filePath) {\n process.stderr.write(\"Error: preview command requires a file path\\n\");\n process.stderr.write(USAGE);\n process.exit(1);\n }\n\n const resolvedFile = resolve(projectDir, filePath);\n\n const store = new CommentStore(projectDir);\n await store.load();\n await store.startWatching();\n\n // Start preview server (non-fatal — MCP works even if port is taken)\n const preview = createPreviewServer(store, projectDir, port, resolvedFile);\n let previewRunning2 = false;\n try {\n await preview.start();\n previewRunning2 = true;\n writePortFile(projectDir, preview.port);\n const previewUrl = `http://localhost:${preview.port}`;\n process.stderr.write(`Preview: ${previewUrl}\\n`);\n if (!values[\"no-open\"]) openBrowser(previewUrl);\n } catch (err) {\n process.stderr.write(`Preview server failed: ${err}\\n`);\n process.stderr.write(`MCP server will still run — comments sync via .cowrite-comments.json\\n`);\n }\n\n // Start MCP server on stdio\n const mcpServer = createMcpServer(store, projectDir, () => previewRunning2 ? preview.port : null);\n const transport = new StdioServerTransport();\n await mcpServer.connect(transport);\n\n process.stderr.write(`Cowrite MCP server running on stdio\\n`);\n\n setupShutdown(store, preview, projectDir);\n } else if (command === \"init\") {\n process.stderr.write(\"Installed cowrite hooks and skills into .claude/\\n\");\n } else if (command === \"open\") {\n const discoveredPort = readPortFile(projectDir) ?? port;\n const url = `http://localhost:${discoveredPort}`;\n process.stderr.write(`Opening ${url}\\n`);\n await openBrowser(url);\n } else {\n process.stderr.write(`Unknown command: ${command}\\n`);\n process.stderr.write(USAGE);\n process.exit(1);\n }\n}\n\nmain().catch((err) => {\n process.stderr.write(`Fatal error: ${err}\\n`);\n process.exit(1);\n});\n","import { EventEmitter } from \"node:events\";\nimport { readFile, writeFile } from \"node:fs/promises\";\nimport { join, resolve } from \"node:path\";\nimport { randomUUID } from \"node:crypto\";\nimport { watch as chokidarWatch, type FSWatcher } from \"chokidar\";\nimport type { Comment, Reply } from \"./types.js\";\n\nconst PERSIST_FILE = \".cowrite-comments.json\";\n\nexport class CommentStore extends EventEmitter {\n private comments: Map<string, Comment> = new Map();\n private persistPath: string;\n private lastWriteTime = 0;\n private watcher: FSWatcher | null = null;\n\n constructor(projectDir: string) {\n super();\n this.persistPath = join(resolve(projectDir), PERSIST_FILE);\n }\n\n async load(): Promise<void> {\n try {\n const data = await readFile(this.persistPath, \"utf-8\");\n const arr: Comment[] = JSON.parse(data);\n for (const c of arr) {\n this.comments.set(c.id, c);\n }\n } catch {\n // No existing file, start fresh\n }\n }\n\n private async persist(): Promise<void> {\n this.lastWriteTime = Date.now();\n const arr = Array.from(this.comments.values());\n await writeFile(this.persistPath, JSON.stringify(arr, null, 2), \"utf-8\");\n }\n\n async reload(): Promise<void> {\n try {\n const data = await readFile(this.persistPath, \"utf-8\");\n const arr: Comment[] = JSON.parse(data);\n const oldIds = new Set(this.comments.keys());\n this.comments.clear();\n for (const c of arr) {\n this.comments.set(c.id, c);\n }\n // Emit \"new_comment\" for comments that didn't exist before\n for (const c of arr) {\n if (!oldIds.has(c.id)) {\n this.emit(\"new_comment\", c);\n }\n }\n this.emit(\"change\", null);\n } catch {\n // File missing or invalid, clear state\n this.comments.clear();\n this.emit(\"change\", null);\n }\n }\n\n async startWatching(): Promise<void> {\n if (this.watcher) return;\n this.watcher = chokidarWatch(this.persistPath, {\n ignoreInitial: true,\n awaitWriteFinish: { stabilityThreshold: 100, pollInterval: 50 },\n });\n this.watcher.on(\"change\", async () => {\n if (Date.now() - this.lastWriteTime < 200) return;\n await this.reload();\n });\n this.watcher.on(\"add\", async () => {\n if (Date.now() - this.lastWriteTime < 200) return;\n await this.reload();\n });\n }\n\n async stopWatching(): Promise<void> {\n if (this.watcher) {\n await this.watcher.close();\n this.watcher = null;\n }\n }\n\n add(params: {\n file: string;\n offset: number;\n length: number;\n selectedText: string;\n comment: string;\n }): Comment {\n const comment: Comment = {\n id: randomUUID(),\n file: params.file,\n offset: params.offset,\n length: params.length,\n selectedText: params.selectedText,\n comment: params.comment,\n status: \"pending\",\n replies: [],\n createdAt: new Date().toISOString(),\n resolvedAt: null,\n };\n this.comments.set(comment.id, comment);\n this.emit(\"change\", comment);\n this.emit(\"new_comment\", comment);\n this.persist().catch((err) => process.stderr.write(`Persist error: ${err}\\n`));\n return comment;\n }\n\n resolve(commentId: string): Comment | null {\n const comment = this.comments.get(commentId);\n if (!comment) return null;\n comment.status = \"resolved\";\n comment.resolvedAt = new Date().toISOString();\n this.emit(\"change\", comment);\n this.persist().catch((err) => process.stderr.write(`Persist error: ${err}\\n`));\n return comment;\n }\n\n delete(commentId: string): boolean {\n const existed = this.comments.delete(commentId);\n if (existed) {\n this.emit(\"change\", null);\n this.persist().catch((err) => process.stderr.write(`Persist error: ${err}\\n`));\n }\n return existed;\n }\n\n addReply(commentId: string, from: \"user\" | \"agent\", text: string): Reply | null {\n const comment = this.comments.get(commentId);\n if (!comment) return null;\n const reply: Reply = {\n id: randomUUID(),\n from,\n text,\n createdAt: new Date().toISOString(),\n };\n comment.replies.push(reply);\n this.emit(\"change\", comment);\n this.persist().catch((err) => process.stderr.write(`Persist error: ${err}\\n`));\n return reply;\n }\n\n get(commentId: string): Comment | null {\n return this.comments.get(commentId) ?? null;\n }\n\n getAll(filter?: { file?: string; status?: \"pending\" | \"resolved\" | \"all\" }): Comment[] {\n let results = Array.from(this.comments.values());\n if (filter?.file) {\n results = results.filter((c) => c.file === filter.file);\n }\n if (filter?.status && filter.status !== \"all\") {\n results = results.filter((c) => c.status === filter.status);\n }\n return results.sort((a, b) => a.offset - b.offset);\n }\n\n getForFile(file: string): Comment[] {\n return this.getAll({ file });\n }\n\n /** Adjust comment offsets when file content changes */\n adjustOffsets(file: string, oldContent: string, newContent: string): void {\n const fileComments = this.getForFile(file);\n if (fileComments.length === 0) return;\n\n for (const comment of fileComments) {\n // Try to find the selected text in the new content near original offset\n const searchStart = Math.max(0, comment.offset - 200);\n const searchEnd = Math.min(newContent.length, comment.offset + comment.length + 200);\n const searchRegion = newContent.slice(searchStart, searchEnd);\n const idx = searchRegion.indexOf(comment.selectedText);\n if (idx !== -1) {\n comment.offset = searchStart + idx;\n }\n // If not found, leave offset as-is (orphaned comment)\n }\n\n this.emit(\"change\", null);\n this.persist().catch((err) => process.stderr.write(`Persist error: ${err}\\n`));\n }\n\n clear(): void {\n this.comments.clear();\n this.emit(\"change\", null);\n this.persist().catch((err) => process.stderr.write(`Persist error: ${err}\\n`));\n }\n}\n","import { McpServer } from \"@modelcontextprotocol/sdk/server/mcp.js\";\nimport { z } from \"zod\";\nimport type { CommentStore } from \"./comment-store.js\";\nimport { annotateFileWithComments } from \"./utils.js\";\nimport { readFile } from \"node:fs/promises\";\nimport { relative, resolve } from \"node:path\";\n\nexport function createMcpServer(store: CommentStore, projectDir: string, getPreviewPort?: () => number | null): McpServer {\n const server = new McpServer(\n { name: \"cowrite\", version: \"0.1.0\" },\n { capabilities: { logging: {} } },\n );\n\n // Tool: get_pending_comments\n const getPendingTool = server.tool(\n \"get_pending_comments\",\n \"Get comments from the live preview (0 pending). Call this first to catch comments posted before you started listening.\",\n {\n file: z.string().optional().describe(\"Filter by file path\"),\n status: z.enum([\"pending\", \"resolved\", \"all\"]).optional().describe(\"Filter by status (default: pending)\"),\n },\n async ({ file, status }) => {\n const filter: { file?: string; status?: \"pending\" | \"resolved\" | \"all\" } = {};\n if (file) filter.file = resolve(projectDir, file);\n filter.status = status ?? \"pending\";\n const comments = store.getAll(filter);\n return {\n content: [\n {\n type: \"text\" as const,\n text: comments.length === 0\n ? \"No comments found.\"\n : JSON.stringify(comments, null, 2),\n },\n ],\n };\n }\n );\n\n // Tool: resolve_comment\n server.tool(\n \"resolve_comment\",\n \"Mark a comment as resolved/addressed.\",\n {\n commentId: z.string().describe(\"The comment ID to resolve\"),\n },\n async ({ commentId }) => {\n const comment = store.resolve(commentId);\n if (!comment) {\n return {\n content: [{ type: \"text\" as const, text: `Comment ${commentId} not found.` }],\n isError: true,\n };\n }\n return {\n content: [{ type: \"text\" as const, text: `Comment ${commentId} resolved.` }],\n };\n }\n );\n\n // Tool: reply_to_comment\n server.tool(\n \"reply_to_comment\",\n \"Reply to a comment from the agent.\",\n {\n commentId: z.string().describe(\"The comment ID to reply to\"),\n reply: z.string().describe(\"The reply text\"),\n },\n async ({ commentId, reply }) => {\n const replyObj = store.addReply(commentId, \"agent\", reply);\n if (!replyObj) {\n return {\n content: [{ type: \"text\" as const, text: `Comment ${commentId} not found.` }],\n isError: true,\n };\n }\n return {\n content: [{ type: \"text\" as const, text: `Reply added to comment ${commentId}.` }],\n };\n }\n );\n\n // Tool: get_file_with_annotations\n server.tool(\n \"get_file_with_annotations\",\n \"Get file content with inline comment markers showing where comments are anchored.\",\n {\n file: z.string().describe(\"File path to annotate\"),\n },\n async ({ file }) => {\n const filePath = resolve(projectDir, file);\n try {\n const content = await readFile(filePath, \"utf-8\");\n const comments = store.getForFile(filePath);\n const annotated = annotateFileWithComments(content, comments);\n return {\n content: [{ type: \"text\" as const, text: annotated }],\n };\n } catch (err) {\n return {\n content: [{ type: \"text\" as const, text: `Error reading file: ${err}` }],\n isError: true,\n };\n }\n }\n );\n\n // Tool: wait_for_comment\n server.tool(\n \"wait_for_comment\",\n \"Block until a new comment is posted in the live preview, then return it. This is the primary way to receive real-time comments — call it again immediately after handling each comment to keep listening. If it times out, call it again.\",\n {\n timeout: z.number().optional().describe(\"Max seconds to wait (default: 30)\"),\n },\n ({ timeout }, { signal }: { signal?: AbortSignal }) => {\n const maxWait = (timeout ?? 30) * 1000;\n\n // Check for comments that arrived while no one was listening\n const pending = store.getAll({ status: \"pending\" });\n if (pending.length > 0) {\n const latest = pending[pending.length - 1];\n const file = relative(projectDir, latest.file);\n return {\n content: [{\n type: \"text\" as const,\n text: JSON.stringify({ ...latest, file }, null, 2),\n }],\n };\n }\n\n return new Promise((resolve) => {\n const timer = setTimeout(() => {\n cleanup();\n const count = store.getAll({ status: \"pending\" }).length;\n resolve({\n content: [{ type: \"text\" as const, text: count > 0\n ? `Timeout, but ${count} pending comment(s) exist. Call get_pending_comments now.`\n : \"No new comments yet. Call wait_for_comment again to keep listening.\" }],\n });\n }, maxWait);\n\n const onComment = (comment: { id: string; file: string; selectedText: string; comment: string }) => {\n cleanup();\n const file = relative(projectDir, comment.file);\n resolve({\n content: [{\n type: \"text\" as const,\n text: JSON.stringify({ ...comment, file }, null, 2),\n }],\n });\n };\n\n const onAbort = () => {\n cleanup();\n resolve({\n content: [{ type: \"text\" as const, text: \"Cancelled. Call wait_for_comment again to resume listening.\" }],\n });\n };\n\n const cleanup = () => {\n clearTimeout(timer);\n store.off(\"new_comment\", onComment);\n signal?.removeEventListener(\"abort\", onAbort);\n };\n\n store.on(\"new_comment\", onComment);\n signal?.addEventListener(\"abort\", onAbort, { once: true });\n\n // If already aborted before we set up\n if (signal?.aborted) {\n onAbort();\n }\n });\n }\n );\n\n // Tool: get_preview_url\n server.tool(\n \"get_preview_url\",\n \"Get the URL of the Cowrite live preview. Share this with the user so they can open it in their browser.\",\n {},\n async () => {\n const port = getPreviewPort?.();\n if (!port) {\n return {\n content: [{ type: \"text\" as const, text: \"Preview server is not running.\" }],\n isError: true,\n };\n }\n return {\n content: [{ type: \"text\" as const, text: `http://localhost:${port}` }],\n };\n }\n );\n\n // Resource: cowrite://comments\n server.resource(\n \"all-comments\",\n \"cowrite://comments\",\n { description: \"Live list of all comments\", mimeType: \"application/json\" },\n async () => {\n const comments = store.getAll();\n return {\n contents: [\n {\n uri: \"cowrite://comments\",\n mimeType: \"application/json\",\n text: JSON.stringify(comments, null, 2),\n },\n ],\n };\n }\n );\n\n // Wire store changes to MCP resource notifications\n store.on(\"change\", () => {\n if (!server.isConnected()) return;\n server.server.notification({\n method: \"notifications/resources/updated\",\n params: { uri: \"cowrite://comments\" },\n }).catch(() => {});\n });\n\n // --- Comment propagation signals ---\n // Only send signals when an MCP client is actually connected.\n // 1. Update tool description with pending count + sendToolListChanged\n // 2. sendLoggingMessage as additional context\n // Primary real-time mechanism is wait_for_comment via the /watch skill.\n store.on(\"new_comment\", (comment: { file: string; selectedText: string; comment: string }) => {\n if (!server.isConnected()) return;\n\n const count = store.getAll({ status: \"pending\" }).length;\n const file = relative(projectDir, comment.file);\n const selectedPreview = comment.selectedText.length > 80\n ? comment.selectedText.slice(0, 80) + \"...\"\n : comment.selectedText;\n\n // Signal 1: Update tool description + notify tool list changed\n try {\n getPendingTool.update({\n description: `Get comments from the live preview (${count} pending). Call this first to catch comments posted before you started listening.`,\n });\n server.sendToolListChanged();\n } catch {\n // Not connected or transport issue — skip\n }\n\n // Signal 2: Logging message\n server.sendLoggingMessage({\n level: \"warning\",\n data: `NEW COMMENT on ${file}: \"${comment.comment}\" (selected: \"${selectedPreview}\"). Call get_pending_comments to see it.`,\n }).catch(() => {});\n\n // Signal 3: Resource list changed\n server.server.notification({\n method: \"notifications/resources/list_changed\",\n }).catch(() => {});\n });\n\n // Update description count when comments are resolved\n store.on(\"change\", () => {\n const count = store.getAll({ status: \"pending\" }).length;\n try {\n getPendingTool.update({\n description: `Get comments from the live preview (${count} pending). Call this first to catch comments posted before you started listening.`,\n });\n } catch {\n // Ignore — may not be connected yet\n }\n });\n\n // Prompt: cowrite-workflow\n server.prompt(\n \"cowrite-workflow\",\n \"How to process live preview comments in a wait-handle-resolve loop\",\n () => ({\n messages: [\n {\n role: \"user\" as const,\n content: {\n type: \"text\" as const,\n text: [\n \"You are monitoring a live code preview where users leave comments on selected text.\",\n \"\",\n \"Follow this loop:\",\n \"1. Call `get_pending_comments` to check for any comments already posted.\",\n \"2. Process each pending comment: read the file, make the requested change or reply, then call `resolve_comment`.\",\n \"3. Call `wait_for_comment` to block until the next comment arrives.\",\n \"4. When a comment arrives, process it the same way (step 2).\",\n \"5. Go back to step 3 and keep listening.\",\n \"\",\n \"Tips:\",\n \"- Use `get_file_with_annotations` to see comments in context within the file.\",\n \"- Use `reply_to_comment` to acknowledge or ask clarifying questions.\",\n \"- Always `resolve_comment` after addressing feedback.\",\n ].join(\"\\n\"),\n },\n },\n ],\n })\n );\n\n return server;\n}\n","import { exec } from \"node:child_process\";\nimport { marked, Renderer, type Tokens } from \"marked\";\nimport hljs from \"highlight.js\";\nimport type { Comment } from \"./types.js\";\n\n/**\n * Open a URL in the user's default browser.\n * Returns a promise so callers can await if needed (e.g. before process exit).\n */\nexport function openBrowser(url: string): Promise<void> {\n const cmd = process.platform === \"darwin\" ? `open \"${url}\"`\n : process.platform === \"win32\" ? `cmd /c start \"\" \"${url}\"`\n : `xdg-open \"${url}\"`;\n return new Promise((resolve) => {\n exec(cmd, (err) => {\n if (err) process.stderr.write(`Could not open browser: ${err.message}\\n`);\n resolve();\n });\n });\n}\n\n/**\n * Render file content as HTML. Markdown files get full rendering;\n * other text files are wrapped in <pre> with offset-tagged spans.\n */\nexport function renderToHtml(content: string, filePath: string): string {\n const isMarkdown = /\\.(md|markdown|mdx)$/i.test(filePath);\n if (isMarkdown) {\n return renderMarkdownWithOffsets(content);\n }\n return renderPlainTextWithOffsets(content);\n}\n\nfunction renderMarkdownWithOffsets(content: string): string {\n // Build block offset map from lexer tokens\n const tokens = marked.lexer(content);\n const blocks: Array<{ sourceStart: number; sourceEnd: number }> = [];\n let blockOffset = 0;\n for (const token of tokens) {\n if (token.type !== \"space\") {\n blocks.push({ sourceStart: blockOffset, sourceEnd: blockOffset + token.raw.length });\n }\n blockOffset += token.raw.length;\n }\n\n const renderer = new Renderer();\n const defaultCodeRenderer = renderer.code.bind(renderer);\n\n renderer.code = function (token: Tokens.Code) {\n if (token.lang === \"mermaid\") {\n return `<div class=\"mermaid-container\"><pre class=\"mermaid\">${token.text}</pre></div>`;\n }\n try {\n let highlighted: string;\n let lang: string;\n if (token.lang && hljs.getLanguage(token.lang)) {\n const result = hljs.highlight(token.text, { language: token.lang });\n highlighted = result.value;\n lang = token.lang;\n } else {\n const result = hljs.highlightAuto(token.text);\n highlighted = result.value;\n lang = result.language || \"plaintext\";\n }\n 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>`;\n } catch {\n return defaultCodeRenderer(token);\n }\n };\n\n const html = marked.parse(content, { async: false, renderer }) as string;\n const blocksAttr = JSON.stringify(blocks).replace(/\"/g, \"&quot;\");\n return `<div class=\"markdown-body\" data-source-length=\"${content.length}\" data-blocks=\"${blocksAttr}\">${html}</div>`;\n}\n\nfunction renderPlainTextWithOffsets(content: string): string {\n const lines = content.split(\"\\n\");\n let offset = 0;\n const blocks: Array<{ sourceStart: number; sourceEnd: number }> = [];\n const htmlParts: string[] = [];\n let currentLines: string[] = [];\n let blockStart = 0;\n let blockIndex = 0;\n\n for (let i = 0; i < lines.length; i++) {\n const line = lines[i];\n const lineOffset = offset;\n offset += line.length + 1; // +1 for the newline\n\n if (line.trim() === \"\") {\n if (currentLines.length > 0) {\n blocks.push({ sourceStart: blockStart, sourceEnd: lineOffset });\n htmlParts.push(`<div class=\"text-block\" data-block-index=\"${blockIndex}\">${currentLines.join(\"\\n\")}</div>`);\n blockIndex++;\n currentLines = [];\n }\n htmlParts.push(\"\");\n blockStart = offset;\n } else {\n if (currentLines.length === 0) {\n blockStart = lineOffset;\n }\n const escaped = escapeHtml(line);\n currentLines.push(`<span class=\"line\" data-offset=\"${lineOffset}\" data-length=\"${line.length}\">${escaped}</span>`);\n }\n }\n\n if (currentLines.length > 0) {\n blocks.push({ sourceStart: blockStart, sourceEnd: Math.min(offset, content.length) });\n htmlParts.push(`<div class=\"text-block\" data-block-index=\"${blockIndex}\">${currentLines.join(\"\\n\")}</div>`);\n }\n\n const blocksAttr = JSON.stringify(blocks).replace(/\"/g, \"&quot;\");\n return `<pre class=\"plain-text\" data-source-length=\"${content.length}\" data-blocks=\"${blocksAttr}\">${htmlParts.join(\"\\n\")}</pre>`;\n}\n\nfunction escapeHtml(text: string): string {\n return text\n .replace(/&/g, \"&amp;\")\n .replace(/</g, \"&lt;\")\n .replace(/>/g, \"&gt;\")\n .replace(/\"/g, \"&quot;\");\n}\n\n/**\n * Annotate file content with inline comment markers for the agent.\n * Inserts `[COMMENT #id: \"text\"]` at the comment offsets.\n */\nexport function annotateFileWithComments(content: string, comments: Comment[]): string {\n // Sort by offset descending so insertions don't shift earlier offsets\n const sorted = [...comments].sort((a, b) => b.offset - a.offset);\n let result = content;\n\n for (const c of sorted) {\n const marker = `[COMMENT #${c.id.slice(0, 8)}: \"${c.comment}\"]`;\n const end = c.offset + c.length;\n // Insert marker after the selected text\n result = result.slice(0, end) + \" \" + marker + result.slice(end);\n }\n\n return result;\n}\n","import { createServer, type IncomingMessage, type ServerResponse } from \"node:http\";\nimport { readFile, readdir, writeFile } from \"node:fs/promises\";\nimport { existsSync } from \"node:fs\";\nimport { join, resolve, relative } from \"node:path\";\nimport { WebSocketServer, type WebSocket } from \"ws\";\nimport type { CommentStore } from \"./comment-store.js\";\nimport { FileWatcher } from \"./file-watcher.js\";\nimport { renderToHtml } from \"./utils.js\";\nimport type { WSClientMessage, WSServerMessage } from \"./types.js\";\n\n// In dev (tsx): import.meta.dirname is src/, so ../ui works.\n// In built (dist/bin/cowrite.js): import.meta.dirname is dist/bin/, so ../../ui.\n// We find ui/ by checking which path actually contains index.html.\nfunction findUiDir(): string {\n const dir = import.meta.dirname ?? new URL(\".\", import.meta.url).pathname;\n // Try common locations relative to this file\n const candidates = [\n join(dir, \"..\", \"ui\"), // dev: src/../ui\n join(dir, \"..\", \"..\", \"ui\"), // built: dist/bin/../../ui\n ];\n return candidates.find((d) => {\n try { return existsSync(join(d, \"index.html\")); } catch { return false; }\n }) ?? candidates[0];\n}\nconst UI_DIR = findUiDir();\n\nconst MIME_TYPES: Record<string, string> = {\n \".html\": \"text/html\",\n \".css\": \"text/css\",\n \".js\": \"application/javascript\",\n};\n\nconst IGNORED_DIRS = new Set([\"node_modules\", \".git\", \"dist\", \".next\", \".cache\", \"coverage\", \"__pycache__\"]);\n\nexport function createPreviewServer(\n store: CommentStore,\n projectDir: string,\n port: number,\n initialFile?: string\n): { port: number; start: () => Promise<void>; stop: () => Promise<void> } {\n const clients = new Set<WebSocket>();\n const clientFiles = new Map<WebSocket, string>(); // ws -> absolute file path\n const watchers = new Map<string, FileWatcher>(); // absolute path -> watcher\n const watcherListeners = new Map<string, (...args: any[]) => void>(); // path -> change listener\n\n const resolvedProjectDir = resolve(projectDir);\n\n function isInsideProject(filePath: string): boolean {\n const resolved = resolve(resolvedProjectDir, filePath);\n return resolved.startsWith(resolvedProjectDir);\n }\n\n async function getOrCreateWatcher(absPath: string): Promise<FileWatcher> {\n let watcher = watchers.get(absPath);\n if (!watcher) {\n watcher = new FileWatcher(absPath);\n await watcher.start();\n watchers.set(absPath, watcher);\n\n // Subscribe to file changes and broadcast to relevant clients\n const listener = (event: { file: string; content: string; oldContent: string }) => {\n store.adjustOffsets(event.file, event.oldContent, event.content);\n const html = renderToHtml(event.content, event.file);\n for (const [ws, file] of clientFiles) {\n if (file === absPath) {\n send(ws, { type: \"file_update\", file: event.file, content: event.content, html });\n }\n }\n };\n watcher.on(\"change\", listener);\n watcherListeners.set(absPath, listener);\n }\n return watcher;\n }\n\n async function listFiles(dir: string, prefix = \"\"): Promise<string[]> {\n const files: string[] = [];\n try {\n const entries = await readdir(dir, { withFileTypes: true });\n for (const entry of entries) {\n if (entry.name.startsWith(\".\") || IGNORED_DIRS.has(entry.name)) continue;\n const relPath = prefix ? `${prefix}/${entry.name}` : entry.name;\n if (entry.isDirectory()) {\n const sub = await listFiles(join(dir, entry.name), relPath);\n files.push(...sub);\n } else {\n files.push(relPath);\n }\n }\n } catch {\n // Permission denied or gone — skip\n }\n return files;\n }\n\n const httpServer = createServer(async (req: IncomingMessage, res: ServerResponse) => {\n const url = new URL(req.url ?? \"/\", `http://localhost:${port}`);\n const pathname = url.pathname === \"/\" ? \"/index.html\" : url.pathname;\n\n // Serve static UI files\n const ext = pathname.slice(pathname.lastIndexOf(\".\"));\n const mimeType = MIME_TYPES[ext];\n if (mimeType) {\n try {\n const filePath = join(UI_DIR, pathname);\n const content = await readFile(filePath, \"utf-8\");\n res.writeHead(200, { \"Content-Type\": mimeType });\n res.end(content);\n return;\n } catch {\n // Fall through to 404\n }\n }\n\n // API: GET /api/files — list project files for the file picker\n if (pathname === \"/api/files\") {\n const files = await listFiles(resolvedProjectDir);\n res.writeHead(200, { \"Content-Type\": \"application/json\" });\n res.end(JSON.stringify({ files }));\n return;\n }\n\n // API: GET /api/state?file=... — state for a specific file\n if (pathname === \"/api/state\") {\n const fileParam = url.searchParams.get(\"file\");\n if (!fileParam) {\n res.writeHead(400, { \"Content-Type\": \"application/json\" });\n res.end(JSON.stringify({ error: \"Missing file parameter\" }));\n return;\n }\n const absPath = resolve(resolvedProjectDir, fileParam);\n if (!isInsideProject(absPath)) {\n res.writeHead(403, { \"Content-Type\": \"application/json\" });\n res.end(JSON.stringify({ error: \"Path outside project\" }));\n return;\n }\n try {\n const content = await readFile(absPath, \"utf-8\");\n const html = renderToHtml(content, absPath);\n const comments = store.getForFile(absPath);\n res.writeHead(200, { \"Content-Type\": \"application/json\" });\n res.end(JSON.stringify({ file: absPath, content, html, comments }));\n } catch {\n res.writeHead(404, { \"Content-Type\": \"application/json\" });\n res.end(JSON.stringify({ error: \"File not found\" }));\n }\n return;\n }\n\n res.writeHead(404, { \"Content-Type\": \"text/plain\" });\n res.end(\"Not found\");\n });\n\n const wss = new WebSocketServer({ server: httpServer });\n\n // Prevent unhandled WSS errors from crashing the process (e.g. EADDRINUSE)\n wss.on(\"error\", () => {});\n\n wss.on(\"connection\", async (ws: WebSocket) => {\n clients.add(ws);\n\n // If there's an initial file (preview mode), auto-assign it\n if (initialFile) {\n const absPath = resolve(resolvedProjectDir, initialFile);\n await switchClientFile(ws, absPath);\n }\n\n ws.on(\"message\", async (data) => {\n try {\n const msg: WSClientMessage = JSON.parse(data.toString());\n await handleClientMessage(ws, msg);\n } catch (err) {\n send(ws, { type: \"error\", message: `Invalid message: ${err}` });\n }\n });\n\n ws.on(\"close\", () => {\n clients.delete(ws);\n clientFiles.delete(ws);\n });\n });\n\n async function switchClientFile(ws: WebSocket, absPath: string): Promise<void> {\n if (!isInsideProject(absPath)) {\n send(ws, { type: \"error\", message: \"Path outside project\" });\n return;\n }\n try {\n const watcher = await getOrCreateWatcher(absPath);\n clientFiles.set(ws, absPath);\n const content = watcher.getContent();\n const html = renderToHtml(content, absPath);\n const comments = store.getForFile(absPath);\n send(ws, { type: \"file_update\", file: absPath, content, html });\n send(ws, { type: \"comments_update\", comments });\n } catch (err) {\n send(ws, { type: \"error\", message: `Cannot open file: ${err}` });\n }\n }\n\n async function handleClientMessage(ws: WebSocket, msg: WSClientMessage): Promise<void> {\n switch (msg.type) {\n case \"switch_file\": {\n const absPath = resolve(resolvedProjectDir, msg.file);\n await switchClientFile(ws, absPath);\n break;\n }\n case \"comment_add\": {\n const file = clientFiles.get(ws);\n if (!file) break;\n store.add({\n file,\n offset: msg.offset,\n length: msg.length,\n selectedText: msg.selectedText,\n comment: msg.comment,\n });\n break;\n }\n case \"comment_reply\":\n store.addReply(msg.commentId, \"user\", msg.text);\n break;\n case \"comment_resolve\":\n store.resolve(msg.commentId);\n break;\n case \"comment_delete\":\n store.delete(msg.commentId);\n break;\n case \"edit_apply\": {\n const file = clientFiles.get(ws);\n if (!file) break;\n const watcher = watchers.get(file);\n if (!watcher) break;\n const content = watcher.getContent();\n if (msg.offset < 0 || msg.offset + msg.length > content.length) {\n send(ws, { type: \"error\", message: \"Edit offset/length out of bounds\" });\n break;\n }\n const newContent = content.slice(0, msg.offset) + msg.newText + content.slice(msg.offset + msg.length);\n await writeFile(file, newContent, \"utf-8\");\n break;\n }\n }\n }\n\n // Broadcast comment updates to clients viewing the affected file\n store.on(\"change\", (comment: any) => {\n for (const [ws, file] of clientFiles) {\n // If we know which file changed, only notify relevant clients\n // If comment is null (e.g. adjustOffsets, reload), notify all\n if (!comment || comment.file === file) {\n const comments = store.getForFile(file);\n send(ws, { type: \"comments_update\", comments });\n }\n }\n });\n\n function send(ws: WebSocket, msg: WSServerMessage): void {\n if (ws.readyState === ws.OPEN) {\n ws.send(JSON.stringify(msg));\n }\n }\n\n let actualPort = port;\n\n return {\n get port() { return actualPort; },\n start: () => {\n const maxRetries = 10;\n const tryListen = (p: number, attempt: number): Promise<void> =>\n new Promise<void>((res, rej) => {\n const onError = (err: NodeJS.ErrnoException) => {\n if (err.code === \"EADDRINUSE\" && attempt < maxRetries) {\n httpServer.removeListener(\"error\", onError);\n res(tryListen(p + 1, attempt + 1));\n } else {\n rej(err);\n }\n };\n httpServer.on(\"error\", onError);\n httpServer.listen(p, () => {\n httpServer.removeListener(\"error\", onError);\n actualPort = p;\n process.stderr.write(`Cowrite preview server running at http://localhost:${p}\\n`);\n res();\n });\n });\n return tryListen(port, 0);\n },\n stop: async () => {\n for (const client of clients) {\n client.close();\n }\n for (const [path, watcher] of watchers) {\n const listener = watcherListeners.get(path);\n if (listener) watcher.off(\"change\", listener);\n await watcher.stop();\n }\n watchers.clear();\n watcherListeners.clear();\n await new Promise<void>((resolvePromise, reject) => {\n httpServer.close((err) => {\n if (err) reject(err);\n else resolvePromise();\n });\n });\n },\n };\n}\n","import { watch, type FSWatcher } from \"chokidar\";\nimport { readFile } from \"node:fs/promises\";\nimport { resolve } from \"node:path\";\nimport { EventEmitter } from \"node:events\";\n\nexport interface FileChangeEvent {\n file: string;\n content: string;\n}\n\nexport class FileWatcher extends EventEmitter {\n private watcher: FSWatcher | null = null;\n private filePath: string;\n private lastContent: string = \"\";\n\n constructor(filePath: string) {\n super();\n this.filePath = resolve(filePath);\n }\n\n async start(): Promise<string> {\n this.lastContent = await readFile(this.filePath, \"utf-8\");\n\n this.watcher = watch(this.filePath, {\n persistent: true,\n ignoreInitial: true,\n awaitWriteFinish: { stabilityThreshold: 100, pollInterval: 50 },\n });\n\n this.watcher.on(\"change\", async () => {\n try {\n const newContent = await readFile(this.filePath, \"utf-8\");\n if (newContent !== this.lastContent) {\n const oldContent = this.lastContent;\n this.lastContent = newContent;\n this.emit(\"change\", {\n file: this.filePath,\n content: newContent,\n oldContent,\n } as FileChangeEvent & { oldContent: string });\n }\n } catch (err) {\n process.stderr.write(`File watch read error: ${err}\\n`);\n }\n });\n\n return this.lastContent;\n }\n\n getContent(): string {\n return this.lastContent;\n }\n\n getFilePath(): string {\n return this.filePath;\n }\n\n async stop(): Promise<void> {\n if (this.watcher) {\n await this.watcher.close();\n this.watcher = null;\n }\n }\n}\n"],"mappings":"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","names":["resolve","join","existsSync","resolve","readFile","resolve","readFile","writeFile","join","resolve","readFile","resolve","EventEmitter","join","resolve","readFile","writeFile","join","join","existsSync","resolve"]}
1
+ {"version":3,"sources":["../../bin/cowrite.ts","../../src/comment-store.ts","../../src/mcp-server.ts","../../src/utils.ts","../../src/preview-server.ts","../../src/file-watcher.ts"],"sourcesContent":["import { parseArgs } from \"node:util\";\nimport { resolve, join } from \"node:path\";\nimport { existsSync, mkdirSync, writeFileSync, readFileSync, chmodSync, unlinkSync } from \"node:fs\";\nimport { StdioServerTransport } from \"@modelcontextprotocol/sdk/server/stdio.js\";\nimport { CommentStore } from \"../src/comment-store.js\";\nimport { createMcpServer } from \"../src/mcp-server.js\";\nimport { createPreviewServer } from \"../src/preview-server.js\";\nimport { openBrowser } from \"../src/utils.js\";\nimport updateNotifier from \"update-notifier\";\nimport { createRequire } from \"node:module\";\n\ndeclare const __COWRITE_VERSION__: string | undefined;\nconst version: string = typeof __COWRITE_VERSION__ !== \"undefined\"\n ? __COWRITE_VERSION__\n : (createRequire(import.meta.url)(\"../package.json\") as { version: string }).version;\n\nconst USAGE = `\ncowrite — Live commenting plugin for coding agent sessions\n\nUsage:\n cowrite init Install hooks and skills into .claude/ (run once per project)\n cowrite serve [--port N] Start MCP server + preview server (browse any file)\n cowrite preview <file> [--port N] Open browser preview for a specific file + start MCP server\n cowrite open [--port N] Open the browser to the preview URL\n\nOptions:\n --port, -p Port for preview server (default: 3377)\n --no-open Don't auto-open the browser\n --help, -h Show this help\n`;\n\nconst PORT_FILE = \".cowrite-port\";\n\nfunction writePortFile(projectDir: string, port: number): void {\n writeFileSync(join(projectDir, PORT_FILE), String(port), \"utf-8\");\n}\n\nfunction removePortFile(projectDir: string): void {\n try { unlinkSync(join(projectDir, PORT_FILE)); } catch {}\n}\n\nfunction readPortFile(projectDir: string): number | null {\n try {\n const content = readFileSync(join(projectDir, PORT_FILE), \"utf-8\").trim();\n const port = parseInt(content, 10);\n return isNaN(port) ? null : port;\n } catch {\n return null;\n }\n}\n\nfunction setupShutdown(store: CommentStore, preview: { stop: () => Promise<void>; [k: string]: any }, projectDir: string) {\n let shuttingDown = false;\n const shutdown = () => {\n if (shuttingDown) {\n // Second signal — force exit immediately\n process.exit(1);\n }\n shuttingDown = true;\n process.stderr.write(\"Shutting down...\\n\");\n removePortFile(projectDir);\n // Best-effort cleanup with a hard timeout\n Promise.allSettled([store.stopWatching(), preview.stop()])\n .finally(() => process.exit(0));\n setTimeout(() => process.exit(0), 2000);\n };\n process.on(\"SIGINT\", shutdown);\n process.on(\"SIGTERM\", shutdown);\n}\n\nconst HOOK_SCRIPT = `#!/bin/bash\n# Auto-installed by cowrite — injects pending comments into Claude Code context.\n# Only outputs when there are pending comments. Silent otherwise.\nCOMMENTS_FILE=\"\\${CLAUDE_PROJECT_DIR:-.}/.cowrite-comments.json\"\nif [ ! -f \"$COMMENTS_FILE\" ] || [ ! -s \"$COMMENTS_FILE\" ]; then exit 0; fi\nPENDING=$(jq '[.[] | select(.status == \"pending\")] | length' \"$COMMENTS_FILE\" 2>/dev/null || echo 0)\nif [ \"$PENDING\" -eq 0 ]; then exit 0; fi\njq -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\n`;\n\nconst HOOK_ENTRY = {\n matcher: \"\",\n hooks: [{\n type: \"command\",\n command: `bash \"$CLAUDE_PROJECT_DIR/.claude/hooks/inject-comments.sh\"`,\n }],\n};\n\nconst HOOK_SETTINGS = {\n hooks: {\n UserPromptSubmit: [HOOK_ENTRY],\n Stop: [HOOK_ENTRY],\n },\n};\n\nconst SKILL_REVIEW = `---\nname: review\ndescription: Check and address cowrite comments left by the user in the live preview\nuser_invocable: true\n---\n\n# Review Cowrite Comments\n\nCheck for any pending comments left in the Cowrite live preview and address them.\n\n## Steps\n\n1. Call the \\`get_pending_comments\\` tool to retrieve all unresolved comments.\n2. For each pending comment:\n a. Read the comment text and the selected text it refers to.\n b. Use \\`get_file_with_annotations\\` to see the comment in context.\n c. Make the requested change or reply explaining why you can't.\n d. Call \\`reply_to_comment\\` to acknowledge the feedback. Your reply automatically marks it as \"answered\". The user will review and resolve it.\n3. Summarize what was done.\n`;\n\nconst SKILL_WATCH = `---\nname: watch\ndescription: Start a background watcher for cowrite comments — does not block the main conversation\nuser_invocable: true\n---\n\n# Watch for Live Comments (Background)\n\nStart a background agent that watches for cowrite comments and handles them as they arrive. The main conversation stays free for other work.\n\n## Steps\n\n1. First, handle any existing pending comments:\n a. Call \\`get_pending_comments\\` to check for unresolved comments.\n 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\".\n\n2. Then, launch a **background** watcher using the Task tool:\n - Use \\`subagent_type: \"general-purpose\"\\` and \\`run_in_background: true\\`\n - The background agent should call \\`wait_for_comment\\` in a loop\n - When a comment arrives, it handles it (read file, make change, reply)\n - On timeout, it re-calls \\`wait_for_comment\\` immediately\n - The loop continues until the user says stop\n\n3. Tell the user the background watcher is running and they can continue working normally. Comments will be handled automatically.\n`;\n\nfunction installClaudeIntegration(projectDir: string): void {\n const claudeDir = join(projectDir, \".claude\");\n const hooksDir = join(claudeDir, \"hooks\");\n const hookPath = join(hooksDir, \"inject-comments.sh\");\n const settingsPath = join(claudeDir, \"settings.json\");\n const reviewDir = join(claudeDir, \"skills\", \"review\");\n const watchDir = join(claudeDir, \"skills\", \"watch\");\n\n // Create directories\n for (const dir of [hooksDir, reviewDir, watchDir]) {\n if (!existsSync(dir)) {\n mkdirSync(dir, { recursive: true });\n }\n }\n\n // Write hook script (always overwrite to keep in sync with cowrite version)\n writeFileSync(hookPath, HOOK_SCRIPT, \"utf-8\");\n chmodSync(hookPath, 0o755);\n\n // Write skills (always overwrite to keep in sync)\n writeFileSync(join(reviewDir, \"SKILL.md\"), SKILL_REVIEW, \"utf-8\");\n writeFileSync(join(watchDir, \"SKILL.md\"), SKILL_WATCH, \"utf-8\");\n\n // Merge cowrite hooks into settings.json (preserve existing settings)\n let settings: any = {};\n if (existsSync(settingsPath)) {\n try {\n settings = JSON.parse(readFileSync(settingsPath, \"utf-8\"));\n } catch {\n // Corrupt JSON — overwrite\n }\n }\n if (!settings.hooks) settings.hooks = {};\n let changed = false;\n for (const eventType of [\"UserPromptSubmit\", \"Stop\"] as const) {\n if (!Array.isArray(settings.hooks[eventType])) settings.hooks[eventType] = [];\n const hasCowriteHook = settings.hooks[eventType].some((entry: any) =>\n entry.hooks?.some((h: any) => h.command?.includes(\"inject-comments.sh\"))\n );\n if (!hasCowriteHook) {\n settings.hooks[eventType].push(HOOK_ENTRY);\n changed = true;\n }\n }\n if (changed) {\n writeFileSync(settingsPath, JSON.stringify(settings, null, 2) + \"\\n\", \"utf-8\");\n }\n}\n\nasync function main() {\n const { values, positionals } = parseArgs({\n args: process.argv.slice(2),\n options: {\n port: { type: \"string\", short: \"p\", default: \"3377\" },\n \"no-open\": { type: \"boolean\", default: false },\n help: { type: \"boolean\", short: \"h\", default: false },\n },\n allowPositionals: true,\n strict: false,\n });\n\n if (values.help || positionals.length === 0) {\n process.stderr.write(USAGE);\n process.exit(positionals.length === 0 && !values.help ? 1 : 0);\n }\n\n const command = positionals[0];\n\n updateNotifier({ pkg: { name: \"@filipc77/cowrite\", version } }).notify({ isGlobal: true });\n\n const projectDir = process.cwd();\n const port = parseInt(values.port as string, 10);\n\n // Auto-install Claude Code hooks for comment propagation\n installClaudeIntegration(projectDir);\n\n if (command === \"serve\") {\n const store = new CommentStore(projectDir);\n await store.load();\n await store.startWatching();\n\n // Start preview server (non-fatal — MCP works even if port is taken)\n const preview = createPreviewServer(store, projectDir, port);\n let previewRunning = false;\n try {\n await preview.start();\n previewRunning = true;\n writePortFile(projectDir, preview.port);\n const previewUrl = `http://localhost:${preview.port}`;\n process.stderr.write(`Preview: ${previewUrl}\\n`);\n if (!values[\"no-open\"]) openBrowser(previewUrl);\n } catch (err) {\n process.stderr.write(`Preview server failed: ${err}\\n`);\n process.stderr.write(`MCP server will still run — comments sync via .cowrite-comments.json\\n`);\n }\n\n // Start MCP server on stdio\n const mcpServer = createMcpServer(store, projectDir, () => previewRunning ? preview.port : null);\n const transport = new StdioServerTransport();\n await mcpServer.connect(transport);\n\n process.stderr.write(`Cowrite MCP server running on stdio\\n`);\n\n setupShutdown(store, preview, projectDir);\n } else if (command === \"preview\") {\n const filePath = positionals[1];\n if (!filePath) {\n process.stderr.write(\"Error: preview command requires a file path\\n\");\n process.stderr.write(USAGE);\n process.exit(1);\n }\n\n const resolvedFile = resolve(projectDir, filePath);\n\n const store = new CommentStore(projectDir);\n await store.load();\n await store.startWatching();\n\n // Start preview server (non-fatal — MCP works even if port is taken)\n const preview = createPreviewServer(store, projectDir, port, resolvedFile);\n let previewRunning2 = false;\n try {\n await preview.start();\n previewRunning2 = true;\n writePortFile(projectDir, preview.port);\n const previewUrl = `http://localhost:${preview.port}`;\n process.stderr.write(`Preview: ${previewUrl}\\n`);\n if (!values[\"no-open\"]) openBrowser(previewUrl);\n } catch (err) {\n process.stderr.write(`Preview server failed: ${err}\\n`);\n process.stderr.write(`MCP server will still run — comments sync via .cowrite-comments.json\\n`);\n }\n\n // Start MCP server on stdio\n const mcpServer = createMcpServer(store, projectDir, () => previewRunning2 ? preview.port : null);\n const transport = new StdioServerTransport();\n await mcpServer.connect(transport);\n\n process.stderr.write(`Cowrite MCP server running on stdio\\n`);\n\n setupShutdown(store, preview, projectDir);\n } else if (command === \"init\") {\n process.stderr.write(\"Installed cowrite hooks and skills into .claude/\\n\");\n } else if (command === \"open\") {\n const discoveredPort = readPortFile(projectDir) ?? port;\n const url = `http://localhost:${discoveredPort}`;\n process.stderr.write(`Opening ${url}\\n`);\n await openBrowser(url);\n } else {\n process.stderr.write(`Unknown command: ${command}\\n`);\n process.stderr.write(USAGE);\n process.exit(1);\n }\n}\n\nmain().catch((err) => {\n process.stderr.write(`Fatal error: ${err}\\n`);\n process.exit(1);\n});\n","import { EventEmitter } from \"node:events\";\nimport { readFile, writeFile } from \"node:fs/promises\";\nimport { join, resolve } from \"node:path\";\nimport { randomUUID } from \"node:crypto\";\nimport { watch as chokidarWatch, type FSWatcher } from \"chokidar\";\nimport type { Comment, Reply } from \"./types.js\";\n\nconst PERSIST_FILE = \".cowrite-comments.json\";\n\nexport class CommentStore extends EventEmitter {\n private comments: Map<string, Comment> = new Map();\n private persistPath: string;\n private lastWriteTime = 0;\n private watcher: FSWatcher | null = null;\n\n constructor(projectDir: string) {\n super();\n this.persistPath = join(resolve(projectDir), PERSIST_FILE);\n }\n\n async load(): Promise<void> {\n try {\n const data = await readFile(this.persistPath, \"utf-8\");\n const arr: Comment[] = JSON.parse(data);\n for (const c of arr) {\n this.comments.set(c.id, c);\n }\n } catch {\n // No existing file, start fresh\n }\n }\n\n private async persist(): Promise<void> {\n this.lastWriteTime = Date.now();\n const arr = Array.from(this.comments.values());\n await writeFile(this.persistPath, JSON.stringify(arr, null, 2), \"utf-8\");\n }\n\n async reload(): Promise<void> {\n try {\n const data = await readFile(this.persistPath, \"utf-8\");\n const arr: Comment[] = JSON.parse(data);\n const oldIds = new Set(this.comments.keys());\n this.comments.clear();\n for (const c of arr) {\n this.comments.set(c.id, c);\n }\n // Emit \"new_comment\" for comments that didn't exist before\n for (const c of arr) {\n if (!oldIds.has(c.id)) {\n this.emit(\"new_comment\", c);\n }\n }\n this.emit(\"change\", null);\n } catch {\n // File missing or invalid, clear state\n this.comments.clear();\n this.emit(\"change\", null);\n }\n }\n\n async startWatching(): Promise<void> {\n if (this.watcher) return;\n this.watcher = chokidarWatch(this.persistPath, {\n ignoreInitial: true,\n awaitWriteFinish: { stabilityThreshold: 100, pollInterval: 50 },\n });\n this.watcher.on(\"change\", async () => {\n if (Date.now() - this.lastWriteTime < 200) return;\n await this.reload();\n });\n this.watcher.on(\"add\", async () => {\n if (Date.now() - this.lastWriteTime < 200) return;\n await this.reload();\n });\n }\n\n async stopWatching(): Promise<void> {\n if (this.watcher) {\n await this.watcher.close();\n this.watcher = null;\n }\n }\n\n add(params: {\n file: string;\n offset: number;\n length: number;\n selectedText: string;\n comment: string;\n }): Comment {\n const comment: Comment = {\n id: randomUUID(),\n file: params.file,\n offset: params.offset,\n length: params.length,\n selectedText: params.selectedText,\n comment: params.comment,\n status: \"pending\",\n replies: [],\n createdAt: new Date().toISOString(),\n resolvedAt: null,\n };\n this.comments.set(comment.id, comment);\n this.emit(\"change\", comment);\n this.emit(\"new_comment\", comment);\n this.persist().catch((err) => process.stderr.write(`Persist error: ${err}\\n`));\n return comment;\n }\n\n resolve(commentId: string): Comment | null {\n const comment = this.comments.get(commentId);\n if (!comment) return null;\n comment.status = \"resolved\";\n comment.resolvedAt = new Date().toISOString();\n this.emit(\"change\", comment);\n this.persist().catch((err) => process.stderr.write(`Persist error: ${err}\\n`));\n return comment;\n }\n\n reopen(commentId: string): Comment | null {\n const comment = this.comments.get(commentId);\n if (!comment || comment.status !== \"resolved\") return null;\n comment.status = \"pending\";\n comment.resolvedAt = null;\n this.emit(\"change\", comment);\n this.emit(\"comment_reopened\", comment);\n this.persist().catch((err) => process.stderr.write(`Persist error: ${err}\\n`));\n return comment;\n }\n\n delete(commentId: string): boolean {\n const existed = this.comments.delete(commentId);\n if (existed) {\n this.emit(\"change\", null);\n this.persist().catch((err) => process.stderr.write(`Persist error: ${err}\\n`));\n }\n return existed;\n }\n\n addReply(commentId: string, from: \"user\" | \"agent\", text: string): Reply | null {\n const comment = this.comments.get(commentId);\n if (!comment) return null;\n const reply: Reply = {\n id: randomUUID(),\n from,\n text,\n createdAt: new Date().toISOString(),\n };\n comment.replies.push(reply);\n // Agent reply on pending → answered\n if (from === \"agent\" && comment.status === \"pending\") {\n comment.status = \"answered\";\n }\n // User reply on answered → back to pending (re-opens conversation)\n if (from === \"user\" && comment.status === \"answered\") {\n comment.status = \"pending\";\n this.emit(\"comment_reopened\", comment);\n }\n this.emit(\"change\", comment);\n this.persist().catch((err) => process.stderr.write(`Persist error: ${err}\\n`));\n return reply;\n }\n\n get(commentId: string): Comment | null {\n return this.comments.get(commentId) ?? null;\n }\n\n getAll(filter?: { file?: string; status?: \"pending\" | \"answered\" | \"resolved\" | \"all\" }): Comment[] {\n let results = Array.from(this.comments.values());\n if (filter?.file) {\n results = results.filter((c) => c.file === filter.file);\n }\n if (filter?.status && filter.status !== \"all\") {\n results = results.filter((c) => c.status === filter.status);\n }\n return results.sort((a, b) => a.offset - b.offset);\n }\n\n getForFile(file: string): Comment[] {\n return this.getAll({ file });\n }\n\n /** Adjust comment offsets when file content changes */\n adjustOffsets(file: string, oldContent: string, newContent: string): void {\n const fileComments = this.getForFile(file);\n if (fileComments.length === 0) return;\n\n for (const comment of fileComments) {\n if (!comment.selectedText) continue; // file comments don't re-anchor\n // Try to find the selected text in the new content near original offset\n const searchStart = Math.max(0, comment.offset - 200);\n const searchEnd = Math.min(newContent.length, comment.offset + comment.length + 200);\n const searchRegion = newContent.slice(searchStart, searchEnd);\n const idx = searchRegion.indexOf(comment.selectedText);\n if (idx !== -1) {\n comment.offset = searchStart + idx;\n }\n // If not found, leave offset as-is (orphaned comment)\n }\n\n this.emit(\"change\", null);\n this.persist().catch((err) => process.stderr.write(`Persist error: ${err}\\n`));\n }\n\n clear(): void {\n this.comments.clear();\n this.emit(\"change\", null);\n this.persist().catch((err) => process.stderr.write(`Persist error: ${err}\\n`));\n }\n}\n","import { McpServer } from \"@modelcontextprotocol/sdk/server/mcp.js\";\nimport { z } from \"zod\";\nimport type { CommentStore } from \"./comment-store.js\";\nimport { annotateFileWithComments } from \"./utils.js\";\nimport { readFile } from \"node:fs/promises\";\nimport { relative, resolve } from \"node:path\";\n\nexport function createMcpServer(store: CommentStore, projectDir: string, getPreviewPort?: () => number | null): McpServer {\n const server = new McpServer(\n { name: \"cowrite\", version: \"0.1.0\" },\n { capabilities: { logging: {} } },\n );\n\n // Tool: get_pending_comments\n const getPendingTool = server.tool(\n \"get_pending_comments\",\n \"Get comments from the live preview (0 pending). Call this first to catch comments posted before you started listening.\",\n {\n file: z.string().optional().describe(\"Filter by file path\"),\n status: z.enum([\"pending\", \"answered\", \"resolved\", \"all\"]).optional().describe(\"Filter by status (default: pending)\"),\n },\n async ({ file, status }) => {\n const filter: { file?: string; status?: \"pending\" | \"answered\" | \"resolved\" | \"all\" } = {};\n if (file) filter.file = resolve(projectDir, file);\n filter.status = status ?? \"pending\";\n const comments = store.getAll(filter);\n return {\n content: [\n {\n type: \"text\" as const,\n text: comments.length === 0\n ? \"No comments found.\"\n : JSON.stringify(comments, null, 2),\n },\n ],\n };\n }\n );\n\n // Tool: reply_to_comment\n server.tool(\n \"reply_to_comment\",\n \"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.\",\n {\n commentId: z.string().describe(\"The comment ID to reply to\"),\n reply: z.string().describe(\"The reply text\"),\n },\n async ({ commentId, reply }) => {\n const replyObj = store.addReply(commentId, \"agent\", reply);\n if (!replyObj) {\n return {\n content: [{ type: \"text\" as const, text: `Comment ${commentId} not found.` }],\n isError: true,\n };\n }\n return {\n content: [{ type: \"text\" as const, text: `Reply added to comment ${commentId}.` }],\n };\n }\n );\n\n // Tool: get_file_with_annotations\n server.tool(\n \"get_file_with_annotations\",\n \"Get file content with inline comment markers showing where comments are anchored.\",\n {\n file: z.string().describe(\"File path to annotate\"),\n },\n async ({ file }) => {\n const filePath = resolve(projectDir, file);\n try {\n const content = await readFile(filePath, \"utf-8\");\n const comments = store.getForFile(filePath);\n const annotated = annotateFileWithComments(content, comments);\n return {\n content: [{ type: \"text\" as const, text: annotated }],\n };\n } catch (err) {\n return {\n content: [{ type: \"text\" as const, text: `Error reading file: ${err}` }],\n isError: true,\n };\n }\n }\n );\n\n // Tool: wait_for_comment\n server.tool(\n \"wait_for_comment\",\n \"Block until a new comment is posted in the live preview, then return it. This is the primary way to receive real-time comments — call it again immediately after handling each comment to keep listening. If it times out, call it again.\",\n {\n timeout: z.number().optional().describe(\"Max seconds to wait (default: 30)\"),\n },\n ({ timeout }, { signal }: { signal?: AbortSignal }) => {\n const maxWait = (timeout ?? 30) * 1000;\n\n // Check for comments that arrived while no one was listening\n const pending = store.getAll({ status: \"pending\" });\n if (pending.length > 0) {\n const latest = pending[pending.length - 1];\n const file = relative(projectDir, latest.file);\n return {\n content: [{\n type: \"text\" as const,\n text: JSON.stringify({ ...latest, file }, null, 2),\n }],\n };\n }\n\n return new Promise((resolve) => {\n const timer = setTimeout(() => {\n cleanup();\n const count = store.getAll({ status: \"pending\" }).length;\n resolve({\n content: [{ type: \"text\" as const, text: count > 0\n ? `Timeout, but ${count} pending comment(s) exist. Call get_pending_comments now.`\n : \"No new comments yet. Call wait_for_comment again to keep listening.\" }],\n });\n }, maxWait);\n\n const onComment = (comment: { id: string; file: string; selectedText: string; comment: string }) => {\n cleanup();\n const file = relative(projectDir, comment.file);\n resolve({\n content: [{\n type: \"text\" as const,\n text: JSON.stringify({ ...comment, file }, null, 2),\n }],\n });\n };\n\n const onAbort = () => {\n cleanup();\n resolve({\n content: [{ type: \"text\" as const, text: \"Cancelled. Call wait_for_comment again to resume listening.\" }],\n });\n };\n\n const cleanup = () => {\n clearTimeout(timer);\n store.off(\"new_comment\", onComment);\n store.off(\"comment_reopened\", onComment);\n signal?.removeEventListener(\"abort\", onAbort);\n };\n\n store.on(\"new_comment\", onComment);\n store.on(\"comment_reopened\", onComment);\n signal?.addEventListener(\"abort\", onAbort, { once: true });\n\n // If already aborted before we set up\n if (signal?.aborted) {\n onAbort();\n }\n });\n }\n );\n\n // Tool: get_preview_url\n server.tool(\n \"get_preview_url\",\n \"Get the URL of the Cowrite live preview. Share this with the user so they can open it in their browser.\",\n {},\n async () => {\n const port = getPreviewPort?.();\n if (!port) {\n return {\n content: [{ type: \"text\" as const, text: \"Preview server is not running.\" }],\n isError: true,\n };\n }\n return {\n content: [{ type: \"text\" as const, text: `http://localhost:${port}` }],\n };\n }\n );\n\n // Resource: cowrite://comments\n server.resource(\n \"all-comments\",\n \"cowrite://comments\",\n { description: \"Live list of all comments\", mimeType: \"application/json\" },\n async () => {\n const comments = store.getAll();\n return {\n contents: [\n {\n uri: \"cowrite://comments\",\n mimeType: \"application/json\",\n text: JSON.stringify(comments, null, 2),\n },\n ],\n };\n }\n );\n\n // Wire store changes to MCP resource notifications\n store.on(\"change\", () => {\n if (!server.isConnected()) return;\n server.server.notification({\n method: \"notifications/resources/updated\",\n params: { uri: \"cowrite://comments\" },\n }).catch(() => {});\n });\n\n // --- Comment propagation signals ---\n // Only send signals when an MCP client is actually connected.\n // 1. Update tool description with pending count + sendToolListChanged\n // 2. sendLoggingMessage as additional context\n // Primary real-time mechanism is wait_for_comment via the /watch skill.\n function notifyCommentNeedsAttention(comment: { file: string; selectedText: string; comment: string }, prefix: string) {\n if (!server.isConnected()) return;\n\n const count = store.getAll({ status: \"pending\" }).length;\n const file = relative(projectDir, comment.file);\n const selectedPreview = comment.selectedText.length > 80\n ? comment.selectedText.slice(0, 80) + \"...\"\n : comment.selectedText;\n\n // Signal 1: Update tool description + notify tool list changed\n try {\n getPendingTool.update({\n description: `Get comments from the live preview (${count} pending). Call this first to catch comments posted before you started listening.`,\n });\n server.sendToolListChanged();\n } catch {\n // Not connected or transport issue — skip\n }\n\n // Signal 2: Logging message\n const logMsg = comment.selectedText\n ? `${prefix} on ${file}: \"${comment.comment}\" (selected: \"${selectedPreview}\"). Call get_pending_comments to see it.`\n : `${prefix} on ${file}: \"${comment.comment}\". Call get_pending_comments to see it.`;\n server.sendLoggingMessage({\n level: \"warning\",\n data: logMsg,\n }).catch(() => {});\n\n // Signal 3: Resource list changed\n server.server.notification({\n method: \"notifications/resources/list_changed\",\n }).catch(() => {});\n }\n\n store.on(\"new_comment\", (comment: { file: string; selectedText: string; comment: string }) => {\n notifyCommentNeedsAttention(comment, \"NEW COMMENT\");\n });\n\n store.on(\"comment_reopened\", (comment: { file: string; selectedText: string; comment: string }) => {\n notifyCommentNeedsAttention(comment, \"COMMENT REOPENED\");\n });\n\n // Update description count when comments are resolved\n store.on(\"change\", () => {\n const count = store.getAll({ status: \"pending\" }).length;\n try {\n getPendingTool.update({\n description: `Get comments from the live preview (${count} pending). Call this first to catch comments posted before you started listening.`,\n });\n } catch {\n // Ignore — may not be connected yet\n }\n });\n\n // Prompt: cowrite-workflow\n server.prompt(\n \"cowrite-workflow\",\n \"How to process live preview comments in a wait-handle-reply loop\",\n () => ({\n messages: [\n {\n role: \"user\" as const,\n content: {\n type: \"text\" as const,\n text: [\n \"You are monitoring a live code preview where users leave comments on selected text.\",\n \"\",\n \"Comment lifecycle: pending → answered (auto on your reply) → resolved (user only).\",\n \"If the user disagrees with your answer, they reply back and it returns to pending.\",\n \"\",\n \"Follow this loop:\",\n \"1. Call `get_pending_comments` to check for any comments already posted.\",\n \"2. Process each pending comment: read the file, make the requested change, then call `reply_to_comment` to explain what you did.\",\n \" Your reply automatically marks the comment as 'answered'. The user will review and resolve it.\",\n \"3. Call `wait_for_comment` to block until the next comment (or reopened comment) arrives.\",\n \"4. When a comment arrives, process it the same way (step 2).\",\n \"5. Go back to step 3 and keep listening.\",\n \"\",\n \"Tips:\",\n \"- Use `get_file_with_annotations` to see comments in context within the file.\",\n \"- Use `reply_to_comment` to acknowledge or ask clarifying questions.\",\n \"- Do NOT resolve comments — the user does that after reviewing your work.\",\n ].join(\"\\n\"),\n },\n },\n ],\n })\n );\n\n return server;\n}\n","import { exec } from \"node:child_process\";\nimport { marked, Renderer, type Tokens } from \"marked\";\nimport hljs from \"highlight.js\";\nimport type { Comment } from \"./types.js\";\n\n/**\n * Open a URL in the user's default browser.\n * Returns a promise so callers can await if needed (e.g. before process exit).\n */\nexport function openBrowser(url: string): Promise<void> {\n const cmd = process.platform === \"darwin\" ? `open \"${url}\"`\n : process.platform === \"win32\" ? `cmd /c start \"\" \"${url}\"`\n : `xdg-open \"${url}\"`;\n return new Promise((resolve) => {\n exec(cmd, (err) => {\n if (err) process.stderr.write(`Could not open browser: ${err.message}\\n`);\n resolve();\n });\n });\n}\n\n/**\n * Render file content as HTML. Markdown files get full rendering;\n * other text files are wrapped in <pre> with offset-tagged spans.\n */\nexport function renderToHtml(content: string, filePath: string): string {\n const isMarkdown = /\\.(md|markdown|mdx)$/i.test(filePath);\n if (isMarkdown) {\n return renderMarkdownWithOffsets(content);\n }\n return renderPlainTextWithOffsets(content);\n}\n\nfunction renderMarkdownWithOffsets(content: string): string {\n // Build block offset map from lexer tokens\n const tokens = marked.lexer(content);\n const blocks: Array<{ sourceStart: number; sourceEnd: number }> = [];\n let blockOffset = 0;\n for (const token of tokens) {\n if (token.type !== \"space\") {\n blocks.push({ sourceStart: blockOffset, sourceEnd: blockOffset + token.raw.length });\n }\n blockOffset += token.raw.length;\n }\n\n const renderer = new Renderer();\n const defaultCodeRenderer = renderer.code.bind(renderer);\n\n renderer.code = function (token: Tokens.Code) {\n if (token.lang === \"mermaid\") {\n return `<div class=\"mermaid-container\"><pre class=\"mermaid\">${token.text}</pre></div>`;\n }\n try {\n let highlighted: string;\n let lang: string;\n if (token.lang && hljs.getLanguage(token.lang)) {\n const result = hljs.highlight(token.text, { language: token.lang });\n highlighted = result.value;\n lang = token.lang;\n } else {\n const result = hljs.highlightAuto(token.text);\n highlighted = result.value;\n lang = result.language || \"plaintext\";\n }\n 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>`;\n } catch {\n return defaultCodeRenderer(token);\n }\n };\n\n const html = marked.parse(content, { async: false, renderer }) as string;\n const blocksAttr = JSON.stringify(blocks).replace(/\"/g, \"&quot;\");\n return `<div class=\"markdown-body\" data-source-length=\"${content.length}\" data-blocks=\"${blocksAttr}\">${html}</div>`;\n}\n\nfunction renderPlainTextWithOffsets(content: string): string {\n const lines = content.split(\"\\n\");\n let offset = 0;\n const blocks: Array<{ sourceStart: number; sourceEnd: number }> = [];\n const htmlParts: string[] = [];\n let currentLines: string[] = [];\n let blockStart = 0;\n let blockIndex = 0;\n\n for (let i = 0; i < lines.length; i++) {\n const line = lines[i];\n const lineOffset = offset;\n offset += line.length + 1; // +1 for the newline\n\n if (line.trim() === \"\") {\n if (currentLines.length > 0) {\n blocks.push({ sourceStart: blockStart, sourceEnd: lineOffset });\n htmlParts.push(`<div class=\"text-block\" data-block-index=\"${blockIndex}\">${currentLines.join(\"\\n\")}</div>`);\n blockIndex++;\n currentLines = [];\n }\n htmlParts.push(\"\");\n blockStart = offset;\n } else {\n if (currentLines.length === 0) {\n blockStart = lineOffset;\n }\n const escaped = escapeHtml(line);\n currentLines.push(`<span class=\"line\" data-offset=\"${lineOffset}\" data-length=\"${line.length}\">${escaped}</span>`);\n }\n }\n\n if (currentLines.length > 0) {\n blocks.push({ sourceStart: blockStart, sourceEnd: Math.min(offset, content.length) });\n htmlParts.push(`<div class=\"text-block\" data-block-index=\"${blockIndex}\">${currentLines.join(\"\\n\")}</div>`);\n }\n\n const blocksAttr = JSON.stringify(blocks).replace(/\"/g, \"&quot;\");\n return `<pre class=\"plain-text\" data-source-length=\"${content.length}\" data-blocks=\"${blocksAttr}\">${htmlParts.join(\"\\n\")}</pre>`;\n}\n\nfunction escapeHtml(text: string): string {\n return text\n .replace(/&/g, \"&amp;\")\n .replace(/</g, \"&lt;\")\n .replace(/>/g, \"&gt;\")\n .replace(/\"/g, \"&quot;\");\n}\n\n/**\n * Annotate file content with inline comment markers for the agent.\n * Inserts `[COMMENT #id: \"text\"]` at the comment offsets.\n */\nexport function annotateFileWithComments(content: string, comments: Comment[]): string {\n // Sort by offset descending so insertions don't shift earlier offsets\n const sorted = [...comments].sort((a, b) => b.offset - a.offset);\n let result = content;\n\n for (const c of sorted) {\n if (!c.selectedText) {\n // File comment — prepend to file\n result = `[FILE COMMENT #${c.id.slice(0, 8)}: \"${c.comment}\"]\\n` + result;\n continue;\n }\n const marker = `[COMMENT #${c.id.slice(0, 8)}: \"${c.comment}\"]`;\n const end = c.offset + c.length;\n // Insert marker after the selected text\n result = result.slice(0, end) + \" \" + marker + result.slice(end);\n }\n\n return result;\n}\n","import { createServer, type IncomingMessage, type ServerResponse } from \"node:http\";\nimport { readFile, readdir, writeFile } from \"node:fs/promises\";\nimport { existsSync } from \"node:fs\";\nimport { join, resolve, relative } from \"node:path\";\nimport { WebSocketServer, type WebSocket } from \"ws\";\nimport type { CommentStore } from \"./comment-store.js\";\nimport { FileWatcher } from \"./file-watcher.js\";\nimport { renderToHtml } from \"./utils.js\";\nimport type { WSClientMessage, WSServerMessage } from \"./types.js\";\n\n// In dev (tsx): import.meta.dirname is src/, so ../ui works.\n// In built (dist/bin/cowrite.js): import.meta.dirname is dist/bin/, so ../../ui.\n// We find ui/ by checking which path actually contains index.html.\nfunction findUiDir(): string {\n const dir = import.meta.dirname ?? new URL(\".\", import.meta.url).pathname;\n // Try common locations relative to this file\n const candidates = [\n join(dir, \"..\", \"ui\"), // dev: src/../ui\n join(dir, \"..\", \"..\", \"ui\"), // built: dist/bin/../../ui\n ];\n return candidates.find((d) => {\n try { return existsSync(join(d, \"index.html\")); } catch { return false; }\n }) ?? candidates[0];\n}\nconst UI_DIR = findUiDir();\n\nconst MIME_TYPES: Record<string, string> = {\n \".html\": \"text/html\",\n \".css\": \"text/css\",\n \".js\": \"application/javascript\",\n};\n\nconst IGNORED_DIRS = new Set([\"node_modules\", \".git\", \"dist\", \".next\", \".cache\", \"coverage\", \"__pycache__\"]);\n\nexport function createPreviewServer(\n store: CommentStore,\n projectDir: string,\n port: number,\n initialFile?: string\n): { port: number; start: () => Promise<void>; stop: () => Promise<void> } {\n const clients = new Set<WebSocket>();\n const clientFiles = new Map<WebSocket, string>(); // ws -> absolute file path\n const watchers = new Map<string, FileWatcher>(); // absolute path -> watcher\n const watcherListeners = new Map<string, (...args: any[]) => void>(); // path -> change listener\n\n const resolvedProjectDir = resolve(projectDir);\n\n function isInsideProject(filePath: string): boolean {\n const resolved = resolve(resolvedProjectDir, filePath);\n return resolved.startsWith(resolvedProjectDir);\n }\n\n async function getOrCreateWatcher(absPath: string): Promise<FileWatcher> {\n let watcher = watchers.get(absPath);\n if (!watcher) {\n watcher = new FileWatcher(absPath);\n await watcher.start();\n watchers.set(absPath, watcher);\n\n // Subscribe to file changes and broadcast to relevant clients\n const listener = (event: { file: string; content: string; oldContent: string }) => {\n store.adjustOffsets(event.file, event.oldContent, event.content);\n const html = renderToHtml(event.content, event.file);\n for (const [ws, file] of clientFiles) {\n if (file === absPath) {\n send(ws, { type: \"file_update\", file: event.file, content: event.content, html });\n }\n }\n };\n watcher.on(\"change\", listener);\n watcherListeners.set(absPath, listener);\n }\n return watcher;\n }\n\n async function listFiles(dir: string, prefix = \"\"): Promise<string[]> {\n const files: string[] = [];\n try {\n const entries = await readdir(dir, { withFileTypes: true });\n for (const entry of entries) {\n if (entry.name.startsWith(\".\") || IGNORED_DIRS.has(entry.name)) continue;\n const relPath = prefix ? `${prefix}/${entry.name}` : entry.name;\n if (entry.isDirectory()) {\n const sub = await listFiles(join(dir, entry.name), relPath);\n files.push(...sub);\n } else {\n files.push(relPath);\n }\n }\n } catch {\n // Permission denied or gone — skip\n }\n return files;\n }\n\n const httpServer = createServer(async (req: IncomingMessage, res: ServerResponse) => {\n const url = new URL(req.url ?? \"/\", `http://localhost:${port}`);\n const pathname = url.pathname === \"/\" ? \"/index.html\" : url.pathname;\n\n // Serve static UI files\n const ext = pathname.slice(pathname.lastIndexOf(\".\"));\n const mimeType = MIME_TYPES[ext];\n if (mimeType) {\n try {\n const filePath = join(UI_DIR, pathname);\n const content = await readFile(filePath, \"utf-8\");\n res.writeHead(200, { \"Content-Type\": mimeType });\n res.end(content);\n return;\n } catch {\n // Fall through to 404\n }\n }\n\n // API: GET /api/files — list project files for the file picker\n if (pathname === \"/api/files\") {\n const files = await listFiles(resolvedProjectDir);\n res.writeHead(200, { \"Content-Type\": \"application/json\" });\n res.end(JSON.stringify({ files }));\n return;\n }\n\n // API: GET /api/state?file=... — state for a specific file\n if (pathname === \"/api/state\") {\n const fileParam = url.searchParams.get(\"file\");\n if (!fileParam) {\n res.writeHead(400, { \"Content-Type\": \"application/json\" });\n res.end(JSON.stringify({ error: \"Missing file parameter\" }));\n return;\n }\n const absPath = resolve(resolvedProjectDir, fileParam);\n if (!isInsideProject(absPath)) {\n res.writeHead(403, { \"Content-Type\": \"application/json\" });\n res.end(JSON.stringify({ error: \"Path outside project\" }));\n return;\n }\n try {\n const content = await readFile(absPath, \"utf-8\");\n const html = renderToHtml(content, absPath);\n const comments = store.getForFile(absPath);\n res.writeHead(200, { \"Content-Type\": \"application/json\" });\n res.end(JSON.stringify({ file: absPath, content, html, comments }));\n } catch {\n res.writeHead(404, { \"Content-Type\": \"application/json\" });\n res.end(JSON.stringify({ error: \"File not found\" }));\n }\n return;\n }\n\n res.writeHead(404, { \"Content-Type\": \"text/plain\" });\n res.end(\"Not found\");\n });\n\n const wss = new WebSocketServer({ server: httpServer });\n\n // Prevent unhandled WSS errors from crashing the process (e.g. EADDRINUSE)\n wss.on(\"error\", () => {});\n\n wss.on(\"connection\", async (ws: WebSocket) => {\n clients.add(ws);\n\n // If there's an initial file (preview mode), auto-assign it\n if (initialFile) {\n const absPath = resolve(resolvedProjectDir, initialFile);\n await switchClientFile(ws, absPath);\n }\n\n ws.on(\"message\", async (data) => {\n try {\n const msg: WSClientMessage = JSON.parse(data.toString());\n await handleClientMessage(ws, msg);\n } catch (err) {\n send(ws, { type: \"error\", message: `Invalid message: ${err}` });\n }\n });\n\n ws.on(\"close\", () => {\n clients.delete(ws);\n clientFiles.delete(ws);\n });\n });\n\n async function switchClientFile(ws: WebSocket, absPath: string): Promise<void> {\n if (!isInsideProject(absPath)) {\n send(ws, { type: \"error\", message: \"Path outside project\" });\n return;\n }\n try {\n const watcher = await getOrCreateWatcher(absPath);\n clientFiles.set(ws, absPath);\n const content = watcher.getContent();\n const html = renderToHtml(content, absPath);\n const comments = store.getForFile(absPath);\n send(ws, { type: \"file_update\", file: absPath, content, html });\n send(ws, { type: \"comments_update\", comments });\n } catch (err) {\n send(ws, { type: \"error\", message: `Cannot open file: ${err}` });\n }\n }\n\n async function handleClientMessage(ws: WebSocket, msg: WSClientMessage): Promise<void> {\n switch (msg.type) {\n case \"switch_file\": {\n const absPath = resolve(resolvedProjectDir, msg.file);\n await switchClientFile(ws, absPath);\n break;\n }\n case \"comment_add\": {\n const file = clientFiles.get(ws);\n if (!file) break;\n store.add({\n file,\n offset: msg.offset,\n length: msg.length,\n selectedText: msg.selectedText,\n comment: msg.comment,\n });\n break;\n }\n case \"comment_reply\":\n store.addReply(msg.commentId, \"user\", msg.text);\n break;\n case \"comment_resolve\":\n store.resolve(msg.commentId);\n break;\n case \"comment_reopen\":\n store.reopen(msg.commentId);\n break;\n case \"comment_delete\":\n store.delete(msg.commentId);\n break;\n case \"edit_apply\": {\n const file = clientFiles.get(ws);\n if (!file) break;\n const watcher = watchers.get(file);\n if (!watcher) break;\n const content = watcher.getContent();\n if (msg.offset < 0 || msg.offset + msg.length > content.length) {\n send(ws, { type: \"error\", message: \"Edit offset/length out of bounds\" });\n break;\n }\n const newContent = content.slice(0, msg.offset) + msg.newText + content.slice(msg.offset + msg.length);\n await writeFile(file, newContent, \"utf-8\");\n break;\n }\n }\n }\n\n // Broadcast comment updates to clients viewing the affected file\n store.on(\"change\", (comment: any) => {\n for (const [ws, file] of clientFiles) {\n // If we know which file changed, only notify relevant clients\n // If comment is null (e.g. adjustOffsets, reload), notify all\n if (!comment || comment.file === file) {\n const comments = store.getForFile(file);\n send(ws, { type: \"comments_update\", comments });\n }\n }\n });\n\n function send(ws: WebSocket, msg: WSServerMessage): void {\n if (ws.readyState === ws.OPEN) {\n ws.send(JSON.stringify(msg));\n }\n }\n\n let actualPort = port;\n\n return {\n get port() { return actualPort; },\n start: () => {\n const maxRetries = 10;\n const tryListen = (p: number, attempt: number): Promise<void> =>\n new Promise<void>((res, rej) => {\n const onError = (err: NodeJS.ErrnoException) => {\n if (err.code === \"EADDRINUSE\" && attempt < maxRetries) {\n httpServer.removeListener(\"error\", onError);\n res(tryListen(p + 1, attempt + 1));\n } else {\n rej(err);\n }\n };\n httpServer.on(\"error\", onError);\n httpServer.listen(p, () => {\n httpServer.removeListener(\"error\", onError);\n actualPort = p;\n process.stderr.write(`Cowrite preview server running at http://localhost:${p}\\n`);\n res();\n });\n });\n return tryListen(port, 0);\n },\n stop: async () => {\n for (const client of clients) {\n client.close();\n }\n for (const [path, watcher] of watchers) {\n const listener = watcherListeners.get(path);\n if (listener) watcher.off(\"change\", listener);\n await watcher.stop();\n }\n watchers.clear();\n watcherListeners.clear();\n await new Promise<void>((resolvePromise, reject) => {\n httpServer.close((err) => {\n if (err) reject(err);\n else resolvePromise();\n });\n });\n },\n };\n}\n","import { watch, type FSWatcher } from \"chokidar\";\nimport { readFile } from \"node:fs/promises\";\nimport { resolve } from \"node:path\";\nimport { EventEmitter } from \"node:events\";\n\nexport interface FileChangeEvent {\n file: string;\n content: string;\n}\n\nexport class FileWatcher extends EventEmitter {\n private watcher: FSWatcher | null = null;\n private filePath: string;\n private lastContent: string = \"\";\n\n constructor(filePath: string) {\n super();\n this.filePath = resolve(filePath);\n }\n\n async start(): Promise<string> {\n this.lastContent = await readFile(this.filePath, \"utf-8\");\n\n this.watcher = watch(this.filePath, {\n persistent: true,\n ignoreInitial: true,\n awaitWriteFinish: { stabilityThreshold: 100, pollInterval: 50 },\n });\n\n this.watcher.on(\"change\", async () => {\n try {\n const newContent = await readFile(this.filePath, \"utf-8\");\n if (newContent !== this.lastContent) {\n const oldContent = this.lastContent;\n this.lastContent = newContent;\n this.emit(\"change\", {\n file: this.filePath,\n content: newContent,\n oldContent,\n } as FileChangeEvent & { oldContent: string });\n }\n } catch (err) {\n process.stderr.write(`File watch read error: ${err}\\n`);\n }\n });\n\n return this.lastContent;\n }\n\n getContent(): string {\n return this.lastContent;\n }\n\n getFilePath(): string {\n return this.filePath;\n }\n\n async stop(): Promise<void> {\n if (this.watcher) {\n await this.watcher.close();\n this.watcher = null;\n }\n }\n}\n"],"mappings":"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","names":["resolve","join","existsSync","resolve","readFile","resolve","readFile","writeFile","join","resolve","readFile","resolve","EventEmitter","join","resolve","readFile","writeFile","join","join","existsSync","resolve"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@filipc77/cowrite",
3
- "version": "0.5.0",
3
+ "version": "0.6.0",
4
4
  "description": "Live commenting and inline editing plugin for coding agent sessions",
5
5
  "type": "module",
6
6
  "bin": {
package/ui/client.js CHANGED
@@ -20,6 +20,7 @@ const highlightEditBtn = $("#highlightEditBtn");
20
20
  const undoBtn = $("#undoBtn");
21
21
  const filePicker = $("#filePicker");
22
22
  const fileList = $("#fileList");
23
+ const fileCommentBtn = $("#fileCommentBtn");
23
24
 
24
25
  /** @type {Comment[]} */
25
26
  let comments = [];
@@ -337,6 +338,10 @@ function hideTrigger() {
337
338
  function hidePopup() {
338
339
  popup.hidden = true;
339
340
  selectionInfo = null;
341
+ // Reset popup to default state for next use
342
+ popupSelection.hidden = false;
343
+ popup.querySelector(".popup-header").textContent = "Selection";
344
+ commentInput.placeholder = "Leave a comment...";
340
345
  }
341
346
 
342
347
  $("#commentCancel").addEventListener("click", hidePopup);
@@ -352,6 +357,24 @@ commentInput.addEventListener("keydown", (e) => {
352
357
  }
353
358
  });
354
359
 
360
+ fileCommentBtn.addEventListener("click", () => {
361
+ if (!currentFile) return;
362
+
363
+ // Position popup below the button
364
+ const btnRect = fileCommentBtn.getBoundingClientRect();
365
+ popup.style.left = `${Math.max(8, btnRect.left - 280)}px`;
366
+ popup.style.top = `${btnRect.bottom + 8}px`;
367
+
368
+ // Hide the selection preview, set file-comment mode
369
+ popupSelection.hidden = true;
370
+ popup.querySelector(".popup-header").textContent = "File comment";
371
+ commentInput.value = "";
372
+ commentInput.placeholder = "Comment on the whole file...";
373
+ selectionInfo = { offset: 0, length: 0, selectedText: "" };
374
+ popup.hidden = false;
375
+ commentInput.focus();
376
+ });
377
+
355
378
  function submitComment() {
356
379
  const text = commentInput.value.trim();
357
380
  if (!text || !selectionInfo) return;
@@ -376,7 +399,7 @@ function renderComments() {
376
399
  commentListEl.innerHTML = `
377
400
  <div class="empty-state">
378
401
  <p>No comments yet.</p>
379
- <p>Select text in the preview and add a comment.</p>
402
+ <p>Select text to comment, or use + for file comments.</p>
380
403
  </div>
381
404
  `;
382
405
  return;
@@ -387,7 +410,10 @@ function renderComments() {
387
410
  <button class="comment-delete-btn" onclick="deleteComment('${c.id}')" title="Delete comment">
388
411
  <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="3 6 5 6 21 6"/><path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"/></svg>
389
412
  </button>
390
- <div class="comment-selected-text">${escapeHtml(c.selectedText)}</div>
413
+ ${c.selectedText
414
+ ? `<div class="comment-selected-text">${escapeHtml(c.selectedText)}</div>`
415
+ : `<div class="comment-file-badge">Whole file</div>`
416
+ }
391
417
  <div class="comment-text">${escapeHtml(c.comment)}</div>
392
418
  ${c.replies.length > 0 ? `
393
419
  <div class="comment-replies">
@@ -403,12 +429,16 @@ function renderComments() {
403
429
  <span>${timeAgo(c.createdAt)}</span>
404
430
  <span class="comment-status ${c.status}">${c.status}</span>
405
431
  </div>
406
- ${c.status === "pending" ? `
432
+ ${c.status !== "resolved" ? `
407
433
  <div class="comment-actions">
408
434
  <button onclick="showReplyForm('${c.id}')">Reply</button>
409
435
  <button onclick="resolveComment('${c.id}')">Resolve</button>
410
436
  </div>
411
- ` : ""}
437
+ ` : `
438
+ <div class="comment-actions">
439
+ <button onclick="reopenComment('${c.id}')">Reopen</button>
440
+ </div>
441
+ `}
412
442
  <div class="reply-form" id="reply-form-${c.id}" hidden>
413
443
  <textarea rows="2" placeholder="Reply..."></textarea>
414
444
  <div class="reply-form-actions">
@@ -446,6 +476,10 @@ window.resolveComment = function (id) {
446
476
  send({ type: "comment_resolve", commentId: id });
447
477
  };
448
478
 
479
+ window.reopenComment = function (id) {
480
+ send({ type: "comment_reopen", commentId: id });
481
+ };
482
+
449
483
  window.deleteComment = function (id) {
450
484
  send({ type: "comment_delete", commentId: id });
451
485
  };
@@ -486,9 +520,10 @@ function applyHighlights() {
486
520
 
487
521
  const fullText = textNodes.map((n) => n.node.textContent).join("");
488
522
 
489
- // Only highlight pending comments — resolved ones act as regular text
523
+ // Highlight pending and answered comments — resolved ones act as regular text
490
524
  for (const comment of comments) {
491
- if (comment.status !== "pending") continue;
525
+ // Render highlights for all statuses (resolved ones are invisible via CSS but present for click-to-scroll)
526
+ if (!comment.selectedText) continue;
492
527
  const searchText = comment.selectedText;
493
528
  // Search near the expected offset first, then globally
494
529
  let textIdx = fullText.indexOf(searchText, Math.max(0, comment.offset - 50));
package/ui/index.html CHANGED
@@ -52,6 +52,7 @@
52
52
  <div class="sidebar" id="sidebar">
53
53
  <div class="sidebar-header">
54
54
  <h2>Comments <span class="comment-count" id="commentCount">0</span></h2>
55
+ <button class="file-comment-btn" id="fileCommentBtn" title="Comment on whole file">+</button>
55
56
  </div>
56
57
  <div id="commentList"></div>
57
58
  </div>
package/ui/styles.css CHANGED
@@ -431,10 +431,28 @@ main {
431
431
  background: rgba(212, 165, 84, 0.18);
432
432
  }
433
433
 
434
+ .comment-highlight.answered {
435
+ background: var(--blue-bg);
436
+ border-bottom: 2px solid var(--blue);
437
+ }
438
+
439
+ .comment-highlight.answered:hover {
440
+ background: rgba(107, 163, 212, 0.18);
441
+ }
442
+
443
+ .comment-highlight.resolved {
444
+ background: transparent;
445
+ border-bottom: none;
446
+ }
447
+
434
448
  [data-theme="light"] .comment-highlight:hover {
435
449
  background: rgba(176, 125, 46, 0.12);
436
450
  }
437
451
 
452
+ [data-theme="light"] .comment-highlight.answered:hover {
453
+ background: rgba(58, 114, 160, 0.12);
454
+ }
455
+
438
456
  /* ---- Sidebar ---- */
439
457
  .sidebar {
440
458
  width: 360px;
@@ -448,11 +466,45 @@ main {
448
466
  }
449
467
 
450
468
  .sidebar-header {
469
+ display: flex;
470
+ align-items: center;
471
+ justify-content: space-between;
451
472
  padding: 16px 20px;
452
473
  border-bottom: 1px solid var(--border);
453
474
  flex-shrink: 0;
454
475
  }
455
476
 
477
+ .file-comment-btn {
478
+ width: 26px;
479
+ height: 26px;
480
+ border-radius: 50%;
481
+ border: 1px solid var(--border);
482
+ background: transparent;
483
+ color: var(--text-dim);
484
+ font-size: 16px;
485
+ line-height: 1;
486
+ cursor: pointer;
487
+ display: flex;
488
+ align-items: center;
489
+ justify-content: center;
490
+ transition: all 0.15s ease;
491
+ flex-shrink: 0;
492
+ }
493
+
494
+ .file-comment-btn:hover {
495
+ color: var(--accent);
496
+ border-color: var(--accent);
497
+ }
498
+
499
+ .comment-file-badge {
500
+ font-size: 11px;
501
+ font-weight: 600;
502
+ color: var(--text-faint);
503
+ text-transform: uppercase;
504
+ letter-spacing: 0.5px;
505
+ margin-bottom: 10px;
506
+ }
507
+
456
508
  .sidebar-header h2 {
457
509
  font-size: 12px;
458
510
  font-weight: 600;
@@ -531,8 +583,21 @@ main {
531
583
  box-shadow: 0 0 0 2px var(--accent-glow), var(--shadow-glow);
532
584
  }
533
585
 
586
+ .comment-card.answered {
587
+ border-left: 3px solid var(--blue);
588
+ }
589
+
590
+ .comment-card.answered .comment-selected-text {
591
+ border-left-color: var(--blue);
592
+ }
593
+
594
+ .comment-card.resolved .comment-selected-text {
595
+ border-left-color: var(--green);
596
+ }
597
+
534
598
  .comment-card.resolved {
535
599
  opacity: 0.5;
600
+ border-left: 3px solid var(--green);
536
601
  }
537
602
 
538
603
  .comment-card.resolved:hover {
@@ -603,6 +668,11 @@ main {
603
668
  background: var(--yellow-bg);
604
669
  }
605
670
 
671
+ .comment-status.answered {
672
+ color: var(--blue);
673
+ background: var(--blue-bg);
674
+ }
675
+
606
676
  .comment-status.resolved {
607
677
  color: var(--green);
608
678
  background: var(--green-bg);