md-annotator 0.8.0 → 0.9.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "md-annotator",
3
- "version": "0.8.0",
3
+ "version": "0.9.0",
4
4
  "description": "Browser-based Markdown annotator for AI-assisted review",
5
5
  "type": "module",
6
6
  "bin": {
@@ -43,7 +43,6 @@
43
43
  "open": "^10.1.0",
44
44
  "pako": "^2.1.0",
45
45
  "plantuml-encoder": "^1.4.0",
46
- "portfinder": "^1.0.32",
47
46
  "react": "^19.0.0",
48
47
  "react-dom": "^19.0.0",
49
48
  "web-highlighter": "^0.7.4"
@@ -3,13 +3,12 @@
3
3
  * Provides startAnnotatorServer() for both CLI and plugin usage.
4
4
  */
5
5
 
6
- import { existsSync } from 'node:fs'
6
+ import { existsSync, readFileSync } from 'node:fs'
7
7
  import { createHash } from 'node:crypto'
8
8
  import { fileURLToPath } from 'node:url'
9
9
  import { dirname, join } from 'node:path'
10
10
  import express from 'express'
11
11
  import cors from 'cors'
12
- import portfinder from 'portfinder'
13
12
  import { config } from './config.js'
14
13
  import { createApiRouter } from './routes.js'
15
14
  import { readMarkdownFile } from './file.js'
@@ -19,6 +18,15 @@ const __dirname = dirname(fileURLToPath(import.meta.url))
19
18
  const DIST_PATH = join(__dirname, '..', 'client', 'dist')
20
19
  const DEV_PATH = join(__dirname, '..', 'client')
21
20
 
21
+ // Pre-load index.html into memory at module load time (avoids per-request disk I/O)
22
+ const DIST_INDEX = join(DIST_PATH, 'index.html')
23
+ const DEV_INDEX = join(DEV_PATH, 'index.html')
24
+ const preloadedHtml = existsSync(DIST_INDEX)
25
+ ? readFileSync(DIST_INDEX, 'utf-8')
26
+ : existsSync(DEV_INDEX)
27
+ ? readFileSync(DEV_INDEX, 'utf-8')
28
+ : null
29
+
22
30
  /**
23
31
  * Resolve notes for a specific file from the feedbackNotes array.
24
32
  * Notes apply to the first file only (multi-file uses one invocation per file).
@@ -59,14 +67,14 @@ export async function startAnnotatorServer(options) {
59
67
  app.use(cors())
60
68
  app.use(express.json({ limit: config.jsonLimit }))
61
69
 
62
- // Serve static files or embedded HTML
63
- if (htmlContent) {
64
- // Plugin mode: serve embedded HTML
70
+ // Serve HTML from memory (pre-loaded or embedded)
71
+ const html = htmlContent || preloadedHtml
72
+ if (html) {
65
73
  app.get('/', (_req, res) => {
66
- res.type('html').send(htmlContent)
74
+ res.type('html').send(html)
67
75
  })
68
76
  } else {
69
- // CLI mode: serve from disk
77
+ // Fallback: serve from disk (dev mode without built index.html)
70
78
  const clientPath = existsSync(DIST_PATH) ? DIST_PATH : DEV_PATH
71
79
  app.use(express.static(clientPath))
72
80
  }
@@ -131,15 +139,14 @@ export async function startAnnotatorServer(options) {
131
139
  // API routes with multi-file support
132
140
  app.use(createApiRouter(filePaths, safeResolve, origin, stores))
133
141
 
134
- // Find available port
135
- portfinder.basePort = config.port
136
- const port = await portfinder.getPortPromise()
137
-
138
- // Start server
142
+ // Start server — use port 0 to let the OS assign a free port instantly,
143
+ // falling back to configured port if explicitly set via MD_ANNOTATOR_PORT
144
+ const requestedPort = config.portExplicit ? config.port : 0
139
145
  const server = await new Promise((resolve) => {
140
- const s = app.listen(port, () => resolve(s))
146
+ const s = app.listen(requestedPort, () => resolve(s))
141
147
  })
142
148
 
149
+ const port = server.address().port
143
150
  const url = `http://localhost:${port}`
144
151
 
145
152
  // Call onReady callback if provided
package/server/config.js CHANGED
@@ -45,6 +45,7 @@ function getKrokiServerUrl() {
45
45
 
46
46
  export const config = {
47
47
  port: getServerPort(),
48
+ portExplicit: !!process.env.MD_ANNOTATOR_PORT,
48
49
  browser: process.env.MD_ANNOTATOR_BROWSER || null,
49
50
  heartbeatTimeoutMs: getHeartbeatTimeoutMs(),
50
51
  forceExitTimeoutMs: 5000,
@@ -49,6 +49,27 @@ function formatAnnotation(ann, block, heading) {
49
49
  return output + '\n'
50
50
  }
51
51
 
52
+ // Source view annotations — blockId is "source-line-N" (0-indexed)
53
+ if (ann.targetType === 'source') {
54
+ const lineMatch = ann.blockId?.match(/^source-line-(\d+)$/)
55
+ const startLine = lineMatch ? parseInt(lineMatch[1], 10) + 1 : 1
56
+ const newlinesInSelection = (ann.originalText.match(/\n/g) || []).length
57
+ const endLine = startLine + newlinesInSelection
58
+ const lineRef = startLine === endLine ? `Line ${startLine}` : `Lines ${startLine}-${endLine}`
59
+
60
+ let output = `${heading} `
61
+ if (ann.type === 'DELETION') {
62
+ output += `Remove this (${lineRef}, source)\n`
63
+ output += `\`\`\`\n${ann.originalText}\n\`\`\`\n`
64
+ output += `> User wants this removed from the document.\n`
65
+ } else if (ann.type === 'COMMENT') {
66
+ output += `Comment on (${lineRef}, source)\n`
67
+ output += `\`\`\`\n${ann.originalText}\n\`\`\`\n`
68
+ output += `> ${(ann.text ?? '').replace(/\n/g, '\n> ')}\n`
69
+ }
70
+ return output + '\n'
71
+ }
72
+
52
73
  const blockContent = block?.content || ''
53
74
  const textBeforeSelection = blockContent.slice(0, ann.startOffset)
54
75
  const linesBeforeSelection = (textBeforeSelection.match(/\n/g) || []).length
@@ -79,10 +100,18 @@ function formatAnnotation(ann, block, heading) {
79
100
  return output + '\n'
80
101
  }
81
102
 
103
+ function getBlockOrder(blockId, blocks) {
104
+ const sourceMatch = blockId?.match(/^source-line-(\d+)$/)
105
+ if (sourceMatch) { return parseInt(sourceMatch[1], 10) + 1 }
106
+ const block = blocks.find(blk => blk.id === blockId)
107
+ if (block?.startLine) { return block.startLine }
108
+ return Infinity
109
+ }
110
+
82
111
  function sortAnnotations(annotations, blocks) {
83
112
  return [...annotations].sort((a, b) => {
84
- const blockA = blocks.findIndex(blk => blk.id === a.blockId)
85
- const blockB = blocks.findIndex(blk => blk.id === b.blockId)
113
+ const blockA = getBlockOrder(a.blockId, blocks)
114
+ const blockB = getBlockOrder(b.blockId, blocks)
86
115
  if (blockA !== blockB) {return blockA - blockB}
87
116
  return a.startOffset - b.startOffset
88
117
  })
@@ -1,44 +0,0 @@
1
- /**
2
- * Cookie-based storage utility.
3
- *
4
- * Uses cookies instead of localStorage so settings persist across
5
- * different ports (each session uses a random port).
6
- * Cookies are scoped by domain, not port, so localhost:54321 and
7
- * localhost:54322 share the same cookies.
8
- */
9
-
10
- const ONE_YEAR_SECONDS = 60 * 60 * 24 * 365
11
-
12
- function escapeRegex(str) {
13
- return str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
14
- }
15
-
16
- function getItem(key) {
17
- try {
18
- const match = document.cookie.match(new RegExp(`(?:^|; )${escapeRegex(key)}=([^;]*)`))
19
- return match ? decodeURIComponent(match[1]) : null
20
- } catch {
21
- return null
22
- }
23
- }
24
-
25
- function setItem(key, value) {
26
- try {
27
- const encoded = encodeURIComponent(value)
28
- document.cookie = `${key}=${encoded}; path=/; max-age=${ONE_YEAR_SECONDS}; SameSite=Lax`
29
- } catch {
30
- // Cookie not available
31
- }
32
- }
33
-
34
- const AUTO_CLOSE_KEY = 'md-annotator-auto-close'
35
-
36
- export function getAutoCloseDelay() {
37
- const val = getItem(AUTO_CLOSE_KEY)
38
- if (val === '0' || val === '3' || val === '5') {return val}
39
- return 'off'
40
- }
41
-
42
- export function setAutoCloseDelay(delay) {
43
- setItem(AUTO_CLOSE_KEY, delay)
44
- }