@comment-io/cli 0.1.14-alpha.347 → 0.1.14-alpha.365

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.
Binary file
Binary file
Binary file
Binary file
@@ -27047,7 +27047,7 @@ var agentRules = [
27047
27047
  { id: "markdown-line-breaks", severity: "must", summary: "Do not hard-wrap ordinary prose; use single newlines only for semantic line breaks." },
27048
27048
  { id: "never-send-by", severity: "must", summary: "Never send a client-authored by field." },
27049
27049
  { id: "always-get-before-editing", severity: "must", summary: "Read the current document before editing." },
27050
- { id: "per-doc-token-identify", severity: "must", summary: "Identify anonymous per-document tokens before writing." },
27050
+ { id: "per-doc-token-identify", severity: "must", summary: "Identify anonymous per-document tokens before or during the first write." },
27051
27051
  { id: "read-only-use-comments", severity: "must", summary: "Use comments and suggestions on read-only documents." },
27052
27052
  { id: "report-api-bugs", severity: "should", summary: "Report API behavior that contradicts the docs after recovery fails once." },
27053
27053
  { id: "do-not-duplicate-reports", severity: "must", summary: "Do not file duplicate feedback from repeated retries." }
@@ -27072,15 +27072,15 @@ function formatAgentRuleLine(id, context) {
27072
27072
  case "fetch-current-docs":
27073
27073
  return `**Memory:** Save doc URLs and tokens the user gives you. Fetch ${llmsUrl} each session for the latest API.`;
27074
27074
  case "bearer-token-is-identity":
27075
- return "**Identity:** Your Bearer token is your identity. Do **not** send a `by` field - it is rejected with `400 UNEXPECTED_FIELD`. Registered agents (`as_...` tokens) are identified automatically from their handle. Per-doc tokens must register a display name once with `POST /agents/identify { display_name, slug }` before writing, or writes return `412 IDENTIFY_REQUIRED`. The raw query token on a share URL is read-only: call `GET /docs/{slug}?token={share_token}` to mint your personal `your_token`, then use that as your Bearer token.";
27075
+ return "**Identity:** Your Bearer token is your identity. Do **not** send a `by` field - it is rejected with `400 UNEXPECTED_FIELD`. Registered agents (`as_...` tokens) are identified automatically from their handle. Per-doc tokens must identify once: include `display_name` in your first JSON write body, or call `POST /agents/identify { display_name, slug }` before writing. If a user gives you a token-bearing share URL, extract the token and call `GET /docs/{slug}` with `Authorization: Bearer {token}`; if the response includes `your_token`, switch to that personal bearer token for all later requests.";
27076
27076
  case "markdown-line-breaks":
27077
27077
  return "**Markdown line breaks:** When creating or editing docs, do not hard-wrap ordinary prose at 80 columns. Send each prose paragraph as one line with a blank line between paragraphs. Use single newlines only when line breaks are semantically meaningful, such as poems, addresses, short outlines, Markdown table/list syntax, code blocks, or intentional hard-break content.";
27078
27078
  case "never-send-by":
27079
- return "- **Identity is derived from your Bearer token.** Do **not** send a `by` field - requests that include it are rejected with `400 UNEXPECTED_FIELD`. If this is a per-doc token, register a display name once with `POST /agents/identify` (see below) before writing.";
27079
+ return "- **Identity is derived from your Bearer token.** Do **not** send a `by` field - requests that include it are rejected with `400 UNEXPECTED_FIELD`. If this is a per-doc token, include `display_name` in your first JSON write body or register once with `POST /agents/identify` (see below).";
27080
27080
  case "always-get-before-editing":
27081
27081
  return "- **Always GET the doc before editing.** Never guess at document content.";
27082
27082
  case "per-doc-token-identify":
27083
- return "- **Per-doc tokens must identify before writing.** Call `POST /agents/identify` once per document token, then retry the write.";
27083
+ return "- **Per-doc tokens must identify before or during the first write.** Include `display_name` in the first JSON write body, or call `POST /agents/identify` once per document token and then write.";
27084
27084
  case "read-only-use-comments":
27085
27085
  return "- **Read-only docs use comments and suggestions.** If `read_only` is true, route proposed changes through `POST /docs/:slug/comments` instead of PATCH unless you are the owner.";
27086
27086
  case "report-api-bugs":
@@ -27091,7 +27091,7 @@ function formatAgentRuleLine(id, context) {
27091
27091
  }
27092
27092
  function formatAgentRuleBullet(id, context) {
27093
27093
  if (id === "bearer-token-is-identity") {
27094
- return "- **Identity is derived from your Bearer token.** Do **not** send a `by` field - requests that include it are rejected with `400 UNEXPECTED_FIELD`. If this is a per-doc token, register a display name once with `POST /agents/identify` (see below) before writing.";
27094
+ return "- **Identity is derived from your Bearer token.** Do **not** send a `by` field - requests that include it are rejected with `400 UNEXPECTED_FIELD`. If this is a per-doc token, include `display_name` in your first JSON write body or register once with `POST /agents/identify` (see below).";
27095
27095
  }
27096
27096
  if (id === "report-api-bugs") {
27097
27097
  return "- **Report API bugs while you work.** If a response includes a `feedback` URL and the API behavior looks wrong, contradicts these docs, or documented recovery fails once, POST to that URL with the method, endpoint, request/response summary, `request_id`, what you expected, and what recovery you tried. Report potentially wrong 409/422 responses this way; do not report the same issue twice.";
@@ -27180,7 +27180,7 @@ function apiReference(baseUrl, slug, token, sid) {
27180
27180
  formatAgentRuleBullet("markdown-line-breaks", { baseUrl }),
27181
27181
  `- **The document title is derived from the first non-empty markdown line.** Do **not** send a \`title\` field to \`POST /docs\` or \`PATCH /docs/:slug\`; those requests return \`400 UNEXPECTED_FIELD\`. To rename a doc, edit the first heading/line in \`markdown\`. \`POST /docs\` also accepts optional \`library_target\` for v1 My Files, Team Wiki, or Botlets brain placement; Botlets targets accept stable \`botId\`, optionally paired with \`botSlug\` as a guard/alias, mismatches return \`409 BOT_TARGET_MISMATCH\`, and successful placements return \`library_target_resolution.bot_id\`, \`requested_bot_slug\`, \`canonical_bot_slug\`, and \`slug_resolution\`.`,
27182
27182
  formatAgentRuleBullet("report-api-bugs", { baseUrl }),
27183
- `- **\`quote\` is required** for suggestions and text-selected comments. Plain comments can instead target a durable block with \`block_id\` from \`content_blocks[].id\`; responses include a read-only \`anchor.version=2\` canonical mark anchor, and plain comments may also include \`anchor_block_id\`. Replies use \`reply_to\` and inherit the parent block. Chronological order within the block is the thread.`,
27183
+ `- **\`quote\` is required** for text-selected comments and span suggestions. Whole-block comments and whole-block suggestions can instead target a durable block with \`block_id\` from \`content_blocks[].id\`; whole-block suggestions also require \`expected_revision\`. Responses include a read-only \`anchor.version=2\` canonical mark anchor, and plain comments may also include \`anchor_block_id\`. Replies use \`reply_to\` and inherit the parent thread anchor; compact replies can also use \`thread_id\` from \`threads[].id\`. Plain replies work on orphaned threads; suggestion replies require \`expected_revision\` and a live target anchor.`,
27184
27184
  `- Prefer small targeted edits \u2014 other people may be editing concurrently.`,
27185
27185
  `- **Resource-creation POSTs return \`201 Created\`, not \`200\`.** \`POST /docs\`, \`POST /agents/register\`, and \`POST /docs/:slug/comments\` all succeed with **201**. Treat any \`2xx\` as success \u2014 a \`status === 200\` check misreads success as failure. **Never dump the response body on a non-OK status:** \`POST /agents/register\` returns your \`agent_secret\` in the body, so a naive "log the body on failure" path can leak it.`,
27186
27186
  `- **The created resource's id field differs by endpoint:** \`POST /docs\` and \`POST /agents/register\` return it as \`id\`; \`POST /docs/:slug/comments\` returns it as \`comment_id\`. Parse the field named in each endpoint's response shape below, not a single hard-coded key.`,
@@ -27190,41 +27190,66 @@ function apiReference(baseUrl, slug, token, sid) {
27190
27190
  `\`\`\`bash`,
27191
27191
  `curl -s -H "Authorization: Bearer ${token}"${skillH} "${api}"`,
27192
27192
  `\`\`\``,
27193
- `Response (200):`,
27193
+ `Response (200, compact thread shape):`,
27194
27194
  `\`\`\`json`,
27195
27195
  `{`,
27196
27196
  ` "id": "${slug}",`,
27197
27197
  ` "title": "Doc Title",`,
27198
+ ` "revision": 5,`,
27199
+ ` "your_role": "editor",`,
27200
+ ` "read_only": false,`,
27198
27201
  ` "markdown": "# Content\\n\\nDocument text.",`,
27199
- ` "blocks": [`,
27202
+ ` "content_blocks": [`,
27203
+ ` { "id": "bid_...", "range": { "from": 0, "to": 9 } }`,
27204
+ ` ],`,
27205
+ ` "threads": [`,
27200
27206
  ` {`,
27201
- ` "quote": "anchored text",`,
27207
+ ` "id": "thr_...",`,
27208
+ ` "block_id": "bid_...",`,
27202
27209
  ` "range": { "from": 142, "to": 186 },`,
27210
+ ` "quote": "anchored text",`,
27211
+ ` "resolved": false,`,
27212
+ ` "suggestions": { "pending": 1, "stale": 0 },`,
27203
27213
  ` "comments": [`,
27204
- ` { "id": "uuid", "kind": "comment", "by": "ai:max.reviewer", "text": "comment body", "created_at": "...", "resolved": false, "anchor_block_id": "bid_...", "anchor": { "version": 2, "kind": "block_segments", "scope": "block", "segments": [{ "block_id": "bid_...", "start_offset": 0, "end_offset": 13, "quote": "anchored text" }], "quote": "anchored text" } },`,
27205
- ` { "id": "uuid", "kind": "comment", "by": "human:max", "text": "Good catch, thanks", "created_at": "...", "resolved": false }`,
27206
- ` ]`,
27207
- ` },`,
27208
- ` {`,
27209
- ` "quote": "original text",`,
27210
- ` "range": { "from": 50, "to": 63 },`,
27211
- ` "comments": [`,
27212
- ` { "id": "uuid", "kind": "comment", "by": "ai:max.reviewer", "text": "This should be clearer", "created_at": "...", "resolved": false,`,
27213
- ` "suggestion": { "new_string": "better text", "status": "pending" }, "anchor": { "version": 2, "kind": "block_segments", "scope": "range", "segments": [{ "block_id": "bid_...", "start_offset": 0, "end_offset": 13, "quote": "original text" }], "quote": "original text" } }`,
27214
- ` ]`,
27214
+ ` { "id": "uuid", "by": "ai:max.reviewer", "at": "...", "text": "comment body" },`,
27215
+ ` { "id": "uuid", "by": "human:max", "at": "...", "text": "This should be clearer",`,
27216
+ ` "suggestion": { "old_string": "original text", "new_string": "better text", "status": "pending", "created_against_revision": 4 } }`,
27217
+ ` ],`,
27218
+ ` "comments_info": { "returned": 2, "total": 2, "omitted": 0, "next": null }`,
27215
27219
  ` }`,
27216
27220
  ` ],`,
27221
+ ` "threads_info": { "returned": 1, "total": 1, "next": null },`,
27222
+ ` "suggestions_info": { "pending": 1, "stale": 0 },`,
27217
27223
  ` "actors": {`,
27218
- ` "ai:max.reviewer": { "actor_id": "ai:max.reviewer", "handle": "max.reviewer", "name": "Max's Reviewer", "avatar_url": null, "avatar_emoji": "\u{1F4BB}", "is_human": false, "is_anonymous": false, "kind_label": "AI agent" },`,
27219
- ` "human:max": { "actor_id": "human:max", "handle": "max", "name": "Max", "avatar_url": "https://...", "avatar_emoji": null, "is_human": true, "is_anonymous": false, "kind_label": "Human" }`,
27224
+ ` "ai:max.reviewer": { "actor_id": "ai:max.reviewer", "handle": "max.reviewer", "name": "Max's Reviewer", "is_human": false, "kind_label": "AI agent" },`,
27225
+ ` "human:max": { "actor_id": "human:max", "handle": "max", "name": "Max", "is_human": true, "kind_label": "Human" }`,
27220
27226
  ` },`,
27221
- ` "authorship": [{ "from": 0, "to": 42, "author": "human:max" }],`,
27222
- ` "revision": 5, "active_agents": [], "your_role": "editor",`,
27223
- ` "created_at": "...", "updated_at": "...",`,
27224
- ` "api_docs": "..."`,
27227
+ ` "api_reference_url": "${baseUrl}/llms.txt"`,
27225
27228
  `}`,
27226
27229
  `\`\`\``,
27227
- `The \`api_docs\` field is only present when \`?docs\` is in the request URL.`,
27230
+ ``,
27231
+ `#### Compact read shape`,
27232
+ `Per-doc agent reads use the compact thread shape by default. The \`shape\` selector is retired; omit it. Use \`Authorization: Bearer ...\`; agent doc routes reject \`?token=\` query auth so pagination URLs never carry secrets. If a GET authenticated with a shared invite token returns \`your_token\`, switch to that personal bearer token before writing. The full response keeps \`markdown\`, slim \`content_blocks[]\` (\`id\` + UTF-16 \`range\`; add \`include=block_quotes\` only when needed), \`threads[]\`, \`threads_info\`, \`suggestions_info\`, \`actors\`, \`your_token\` when minted, \`display_name\`, and \`api_reference_url\`. Add \`include=authorship\` to include authorship ranges.`,
27233
+ `\`threads[]\` replaces legacy \`blocks[]\`. Each thread has a stable \`thr_...\` id, \`block_id\`, \`range\`, \`quote\` for span threads, \`whole_block: true\` with \`quote: null\` for non-orphaned whole-block threads, \`orphaned: true\` with \`block_id: null\` and empty collapsed range/quote when a stored mark or thread anchor no longer resolves, \`resolved\`, optional \`resolution\` with the latest resolver note, \`suggestions\`, a bounded \`comments[]\` window, and \`comments_info\`. Pending suggestions on orphaned threads report \`stale\`. \`threads_info.next\` returns lean pages \`{ revision, threads, threads_info, actors }\`; \`comments_info.next\` returns \`{ revision, thread_id, comments, comments_info, actors }\`. Cursor-page \`actors\` maps are page-local and cover the returned comments/meta. Follow every non-null \`next\` with the same Authorization header and dedupe comments defensively by \`id\`.`,
27234
+ `Thread pages accept post-capture filters: \`mentions=me\` returns threads whose stored mention metadata references the requesting credential actor, \`since_revision=N\` returns threads with a captured comment, resolve/unresolve, or suggestion-status write newer than N, \`resolved=false\` returns unresolved threads, and \`suggestions=pending|stale|open\` returns threads with matching suggestion state. Filtered \`threads_info.total\` counts matching threads; \`threads_info.doc_total\` appears when the unfiltered document thread count differs. Pagination URLs preserve the filters.`,
27235
+ `The compact thread page is byte-budgeted, not count-capped. The server fills the non-markdown envelope with newest threads first, then shrinks a large thread's comment window before deferring that thread to the next page. Every returned thread keeps at least its root comment and one recent reply when available; follow \`comments_info.next\` for omitted middle comments.`,
27236
+ `On compact cursor pages, thread ranges are for the returned \`revision\`; interpret them against a matching full response's \`markdown\` or re-GET and use \`quote\` as the drift check. Pre-cutover replies whose parent relationship was already removed from storage may group by their surviving anchor rather than the historical parent thread; new replies record server-side compact membership outside browser-visible mark state.`,
27237
+ ``,
27238
+ `#### Legacy field map`,
27239
+ `Older internal notes may mention \`blocks[]\`, \`content_blocks[].block_id\`, or \`api_docs\`. The live agent shape is \`threads[]\`, durable \`content_blocks[].id\` (\`bid_...\`), and \`quickstart\` plus \`api_reference_url\`. Block text from \`content_blocks[].quote\` is omitted by default and is available only with \`include=block_quotes\`; per-comment/block \`resolved\` is now thread-level \`threads[].resolved\`; bounded comment windows use \`threads[].comments[]\` with \`comments_info\`.`,
27240
+ `Auth rule: send \`Authorization: Bearer {token}\` for agent REST requests. Do not put credentials in \`?token=\` on agent doc routes; they return \`AUTH_HEADER_REQUIRED\`. Browser share links and the editor WebSocket are separate browser surfaces and may still use token-bearing URLs.`,
27241
+ `Write rule: keep using string-matched \`quote\`, \`old_string\`, \`after\`, and \`before\`. For block targets, prefer durable \`bid_...\` ids from \`content_blocks[].id\`; \`blk_...\` ids are a compatibility alias only. For replies and resolution, prefer stable \`thread_id\` routes when you are acting on a thread.`,
27242
+ ``,
27243
+ `#### Offset units`,
27244
+ `All response ranges and offsets use UTF-16 code units, the same units as JavaScript \`String.prototype.slice\`. \`authorship[].from/to\`, \`content_blocks[].range\`, \`threads[].range\`, and edit diagnostics such as \`snippets[].offset\` index into the exact \`markdown\` string from the same response. Comment anchor \`segments[].start_offset/end_offset\` index into the containing block text; \`segments[].quote\` is the expected slice result. ASCII offsets match byte and code point indexes; emoji and some composed characters do not.`,
27245
+ ``,
27246
+ `Non-JS clients should validate any range they plan to use by comparing the server quote to a UTF-16 slice of the exact \`markdown\` string from the same response:`,
27247
+ `\`\`\`python`,
27248
+ `def slice_utf16_units(text, start, end):`,
27249
+ ` data = text.encode("utf-16-le")`,
27250
+ ` return data[start * 2:end * 2].decode("utf-16-le")`,
27251
+ `\`\`\``,
27252
+ `If your local slice does not equal the server-provided \`quote\`, locate the \`quote\` with string search instead of trusting the offset. Write APIs match \`quote\`, \`old_string\`, \`after\`, and \`before\` strings server-side, so offset skew never blocks a comment, suggestion, or text edit. For block-level checks, slice \`markdown\` by \`content_blocks[].range\` or request \`include=block_quotes\` when you need server-materialized block text.`,
27228
27253
  ``,
27229
27254
  `Each comment's \`by\` field is the server-derived author actor_id (resolved from the author's Bearer token at write time). Look it up in the sibling \`actors\` map for display fields. **Never send \`by\` in a request body** \u2014 it is rejected with \`400 UNEXPECTED_FIELD\`.`,
27230
27255
  ``,
@@ -27267,8 +27292,8 @@ function apiReference(baseUrl, slug, token, sid) {
27267
27292
  `#### Read-only docs (\`read_only\`)`,
27268
27293
  `If the GET response has \`"read_only": true\`, the owner has locked the document. Only the owner can PATCH or accept suggestions; everyone else receives \`403\` with \`"code": "DOC_READ_ONLY"\`. Comments and suggestions still work \u2014 route all change proposals through \`POST /docs/:slug/comments\` until the owner unlocks the doc or accepts your suggestion.`,
27269
27294
  ``,
27270
- `Comments are grouped by block position in the \`blocks\` array. Each block has a \`quote\` (the anchored text) and its \`comments\` sorted chronologically \u2014 that is the thread structure. Use \`reply_to\` to attach a reply to a parent comment's block without re-quoting. IDs are UUIDs, stable forever. Use \`id\` from creation responses as \`:cid\` in subsequent routes.`,
27271
- `Do not read thread structure from a comment-level \`reply_to\` field in GET responses. During migration that field may appear as \`null\`; ignore it and use the surrounding block's ordered \`comments\` list.`,
27295
+ `Comments are grouped in \`threads[]\`. Each thread has a stable \`thr_...\` id, optional \`block_id\`, \`range\`, \`quote\` for span threads, \`resolved\`, \`suggestions\`, and chronological \`comments[]\`; use \`thread_id\` or \`reply_to\` for replies.`,
27296
+ `Do not read thread structure from a comment-level \`reply_to\` field in GET responses. It may appear as \`null\`; ignore it and use \`threads[]\`.`,
27272
27297
  ``,
27273
27298
  `#### Deep-link with \`?focus=\``,
27274
27299
  `Add \`?focus=comment-{id}\` to the GET request to receive a \`focused\` field in the response pointing to the specific comment:`,
@@ -27291,7 +27316,7 @@ function apiReference(baseUrl, slug, token, sid) {
27291
27316
  `\`\`\``,
27292
27317
  `Response (200): \`{ "actor_id": "ai:anon.tkn....", "display_name": "My Agent" }\``,
27293
27318
  ``,
27294
- `Idempotent \u2014 call it again to rename yourself. Until the token is identified, every mutating endpoint returns:`,
27319
+ `Idempotent \u2014 call it again to rename yourself. On JSON mutating endpoints you can also include \`display_name\` in the first write body to register the token without a separate roundtrip. Already-identified personal tokens ignore inline \`display_name\`; already-identified shared invite tokens reject a different inline name with \`409 IDENTITY_MISMATCH\`, so use the \`your_token\` from GET when it is present. Until the token is identified, writes without inline \`display_name\` return:`,
27295
27320
  `\`\`\`json`,
27296
27321
  `{ "error": "Register a display name with POST /agents/identify before making write requests with this token.",`,
27297
27322
  ` "code": "IDENTIFY_REQUIRED", "next": "POST /agents/identify", "slug": "${slug}" }`,
@@ -27324,6 +27349,7 @@ function apiReference(baseUrl, slug, token, sid) {
27324
27349
  `- \`after\`: insert after this text. \`null\` = insert at beginning of the document.`,
27325
27350
  `- \`before\`: insert before this text. \`null\` = insert at end of the document.`,
27326
27351
  `- At least one anchor is required. Do NOT combine with \`old_string\` (returns 400).`,
27352
+ `- Anchors are literal text positions: the insert lands exactly where the anchor text ends (or begins, for \`before\`) \u2014 on the SAME line. To insert a block (paragraph, list, table) after an anchor, start \`new_string\` with \`"\\n\\n"\`; otherwise the text is spliced into the anchor's line. Splices that would corrupt structure are rejected with \`INVALID_MARKDOWN\` (a list marker glued mid-line, or prose glued into a heading line).`,
27327
27353
  `- Anchors should match the canonical \`markdown\` from GET after server normalization/escaping; the server also applies the same Markdown-control canonicalization fallback as \`old_string\`. Use both anchors to disambiguate repeated text on current-base requests.`,
27328
27354
  `- \`base_revision\` is required for anchor-based inserts. If it is stale, exactly one non-empty \`after\` or \`before\` anchor can be rebased as cursor/paste semantics against the current markdown using that same canonical matching behavior. A single \`before: null\` boundary append rebases to the current document end, and a single \`after: null\` boundary prepend rebases to the current document start. Paired \`after\`+\`before\`, \`at\` offsets, paired/mixed block targets, and legacy \`blk_*\` block targets still require a current \`base_revision\` and return \`EDIT_STALE\` when stale.`,
27329
27355
  `- For list appends after a text anchor, send a block-separated list item such as \`"\\n\\n- note\\n"\`. A single leading newline before a list marker is normalized for anchor inserts. If multiple stale agents append list items after the same anchor, later commits append after the existing list under that anchor, preserving commit order.`,
@@ -27344,6 +27370,9 @@ function apiReference(baseUrl, slug, token, sid) {
27344
27370
  `- A cell value that repeats across rows (e.g. a status used in several rows) makes a plain \`old_string\` ambiguous and returns \`EDIT_AMBIGUOUS\`. To target one specific cell, retry with \`at\` set to that occurrence's offset from the error's \`snippets[].offset\`. \`at\` requires \`base_revision\`, so include it: \`{"edits": [{"at": OFFSET, "old_string": "in progress", "new_string": "done"}], "base_revision": REVISION}\`. \`at\` resolves the edit positionally, so duplicate values elsewhere no longer matter.`,
27345
27371
  `- Do not put a raw \`|\` in \`new_string\` \u2014 it adds a column and the edit is rejected (422, document unchanged). For a literal pipe inside a cell, escape it as \`\\|\`.`,
27346
27372
  `- Delete a whole table by matching its full markdown (every row, including the \`---\` separator line) with \`new_string: ""\`. Send it as its own edit \u2014 bundling a table delete with other edits in one batch may fail with a structural rejection. The document also needs other content besides the table \u2014 deleting the only block in a document is not supported and returns 422.`,
27373
+ `- Multi-row rewrites, adding/removing rows while editing others, and whole-table restructures are applied in one edit; when row identity cannot be preserved the server replaces the table as a block. Body rows with FEWER cells than the header are padded with empty cells; rows with EXTRA cells are rejected (usually an unescaped \`|\`).`,
27374
+ `- Convert a bullet list into a table (or a table into a list) by replacing the whole block's markdown in one edit. Comments anchored inside the converted block must be declared via \`comment_outcomes\`.`,
27375
+ `- Multi-item list edits (changing, adding, and removing several items in one edit) are supported; verbatim-unchanged items keep their comment anchors. Pure item reorders are rejected \u2014 move content with explicit anchored edits instead.`,
27347
27376
  ``,
27348
27377
  `#### Batch edits`,
27349
27378
  `Send multiple edits in one request. Current-base batches are applied sequentially \u2014 later edits see the result of earlier ones:`,
@@ -27393,20 +27422,20 @@ function apiReference(baseUrl, slug, token, sid) {
27393
27422
  ` -H "Content-Type: application/json" \\`,
27394
27423
  ` -d '{"text": "your comment", "quote": "exact text from doc"}'`,
27395
27424
  `\`\`\``,
27396
- `Response (201): \`{ "comment_id": "uuid", "created_at": "...", "revision": N, "anchor": { "version": 2, "kind": "block_segments", ... }, "anchor_block_id": "bid_..." }\` for plain comments. \`anchor_block_id\` is a convenience field for block comments; use the read-only \`anchor\` object as the canonical mark anchor. If mention dispatch is capped, blocked, or cannot resolve a visible/explicit handle, the response includes \`warning\` with details.`,
27397
- `The \`quote\` is matched against the document's markdown \u2014 slice it directly from the \`markdown\` field on GET (or from any \`content_blocks[].quote\`). Markdown markup like \`## \` headings, \`**bold**\`, list markers, and inline code spans are part of the match string, not stripped.`,
27425
+ `Response (201): \`{ "comment_id": "uuid", "created_at": "...", "revision": N, "anchor": { "version": 2, "kind": "block_segments", ... }, "anchor_block_id": "bid_..." }\` for plain comments. \`anchor_block_id\` is a convenience field for single-block plain comments; use the read-only \`anchor\` object as the canonical mark anchor. If mention dispatch is capped, blocked, or cannot resolve a visible/explicit handle, the response includes \`warning\` with details.`,
27426
+ `The \`quote\` is matched against the document's markdown \u2014 slice it directly from the \`markdown\` field on GET. For whole-block text, slice \`markdown\` by \`content_blocks[].range\` or request \`include=block_quotes\`. Markdown markup like \`## \` headings, \`**bold**\`, list markers, and inline code spans are part of the match string, not stripped.`,
27398
27427
  ``,
27399
- `Or anchor to a specific block by id. This is the most stable target form for a whole-block plain comment; the create response includes the canonical \`anchor.version=2\` object and usually the convenience \`anchor_block_id\`:`,
27428
+ `Or anchor to a specific block by id. This is the most stable target form for a whole-block plain comment or whole-block suggestion; the create response includes the canonical \`anchor.version=2\` object and plain comments usually include the convenience \`anchor_block_id\`:`,
27400
27429
  `\`\`\`bash`,
27401
27430
  `curl -s -X POST "${api}/comments" \\`,
27402
27431
  ` -H "Authorization: Bearer ${token}"${skillH} \\`,
27403
27432
  ` -H "Content-Type: application/json" \\`,
27404
27433
  ` -d '{"text": "your comment", "block_id": "bid_..."}'`,
27405
27434
  `\`\`\``,
27406
- `Use durable \`content_blocks[].id\` (\`bid_...\`) for \`block_id\`. Deprecated \`content_blocks[].block_id\` (\`blk_...\`) remains accepted only as a current-revision compatibility alias and returns \`Deprecation: block_id\`. \`block_id\` is mutually exclusive with \`quote\`, \`suggestion\`, and \`reply_to\`. Durable target failures return current \`markdown\`, \`revision\`, and \`content_blocks\` so you can re-read and retry.`,
27435
+ `Use durable \`content_blocks[].id\` (\`bid_...\`) for \`block_id\`. Deprecated \`content_blocks[].block_id\` (\`blk_...\`) remains accepted only as a current-revision compatibility alias and returns \`Deprecation: block_id\`. \`block_id\` is mutually exclusive with \`quote\`, \`reply_to\`, and \`thread_id\`; include \`suggestion.new_string\` with \`block_id\` to suggest replacing the whole block, with \`old_string\` captured from the block text at creation. Whole-block suggestions must also include \`expected_revision\` from the GET response; if that block changed since then, the server returns \`409 BLOCK_STALE\` with the live \`revision\` and \`current_text\`. Durable target failures return current \`markdown\`, \`revision\`, and \`content_blocks\` so you can re-read and retry.`,
27407
27436
  ``,
27408
27437
  `### Reply to a comment`,
27409
- `Use \`reply_to\` with a comment ID to post to the same block-thread. No \`quote\` needed:`,
27438
+ `Use \`reply_to\` with a comment ID to post to the same thread anchor. No \`quote\` needed:`,
27410
27439
  `\`\`\`bash`,
27411
27440
  `curl -s -X POST "${api}/comments" \\`,
27412
27441
  ` -H "Authorization: Bearer ${token}"${skillH} \\`,
@@ -27415,6 +27444,7 @@ function apiReference(baseUrl, slug, token, sid) {
27415
27444
  `\`\`\``,
27416
27445
  `Response (201): \`{ "comment_id": "uuid", "created_at": "...", "revision": N, "anchor_block_id": "bid_...", "anchor": { "version": 2, "kind": "block_segments", "scope": "block", "segments": [{ "block_id": "bid_...", "start_offset": 0, "end_offset": 13, "quote": "original text" }], "quote": "original text" } }\``,
27417
27446
  `The reply appears in the same block-thread as the parent. No separate parent pointer is stored or returned; during migration GET responses may include \`"reply_to": null\`, which should be ignored.`,
27447
+ `Replies can target a stable thread id: send \`POST /docs/:slug/comments\` with \`{"text":"your reply","thread_id":"thr_..."}\`. Include \`suggestion.new_string\` plus \`expected_revision\` from the GET response to create a competing suggestion on the same live thread anchor; stale target blocks return \`409 BLOCK_STALE\`, and orphaned threads reject new suggestions with \`ORPHANED_ANCHOR\` because the target text no longer resolves. Use Authorization: Bearer auth; query-token URLs return \`AUTH_HEADER_REQUIRED\`. Plain replies work for orphaned threads too. \`thread_id\` is mutually exclusive with \`quote\`, \`block_id\`, and \`kind:"resolution"\`; if you send both \`reply_to\` and \`thread_id\`, the comment id must already belong to that thread.`,
27418
27448
  ``,
27419
27449
  `### Suggest a change`,
27420
27450
  `Add a \`suggestion\` field to create a suggestion instead of a plain comment. \`quote\` is required \u2014 it identifies the text being replaced:`,
@@ -27436,8 +27466,10 @@ function apiReference(baseUrl, slug, token, sid) {
27436
27466
  `\`text\` is required and explains why the thread is resolved.`,
27437
27467
  `Response (200): \`{ "comment_id": "cid", "resolution_id": "...", "resolved": true, "revision": N }\``,
27438
27468
  ``,
27439
- `Resolving a thread creates a new comment with \`kind: "resolution"\` in the same block. The original comment is **not** mutated \u2014 its \`resolved\` flag stays \`false\`. To detect a resolved thread, check for a sibling comment with \`kind: "resolution"\`, or read \`block_resolved: true\` on peer comments in the block (server-computed annotation, display-only).`,
27469
+ `Resolving a thread sets the thread-level resolved state. Compact reads expose this as \`threads[].resolved: true\`; \`threads[].resolution\`, when present, contains the latest resolver note. Do not infer compact resolved state from entries in \`threads[].comments[]\`.`,
27440
27470
  `If the thread's canonical anchor no longer resolves, resolve returns \`422 MARK_ANCHOR_NOT_FOUND\`; re-GET the document and use a current visible comment id.`,
27471
+ `Resolve by stable thread id: \`POST /docs/:slug/threads/{thread_id}/resolve\` with \`{"text":"Resolved because ..."}\`. Use Authorization: Bearer auth; query-token URLs return \`AUTH_HEADER_REQUIRED\`, and a second resolve returns \`409 ALREADY_RESOLVED\`. This works for orphaned threads when their stored canonical anchor is available.`,
27472
+ `To reopen a thread resolved through the thread route, call \`POST /docs/:slug/threads/{thread_id}/unresolve\`. It deletes pointer-bearing resolution marks for that thread and returns \`{ "thread_id": "thr_...", "resolved": false, "removed_resolution_ids": ["..."], "revision": N }\`. Legacy block-level resolution marks that could affect multiple threads return \`409 THREAD_RESOLUTION_AMBIGUOUS\` instead of being deleted.`,
27441
27473
  ``,
27442
27474
  `### Delete a comment`,
27443
27475
  `\`\`\`bash`,
@@ -27559,7 +27591,7 @@ function apiReference(baseUrl, slug, token, sid) {
27559
27591
  `| Method | Path | Auth | Description |`,
27560
27592
  `|--------|------|------|-------------|`,
27561
27593
  `| POST | /docs | optional | Create doc (use Bearer token to appear as registered agent) |`,
27562
- `| GET | /docs/:slug | viewer+ | Read doc (comments, authorship). Add \`?docs\` for API reference. Add \`?focus=comment-{id}\` for a specific comment. |`,
27594
+ `| GET | /docs/:slug | viewer+ | Read doc (compact threads). Add \`?docs\` for API quickstart, \`?include=authorship\` for authorship, or \`?focus=comment-{id}\` for a specific comment. |`,
27563
27595
  `| PATCH | /docs/:slug | editor+ | Edit text (old_string/new_string, after/before anchors, or after_block/before_block targets). The title is derived from the first non-empty markdown line; do not send \`title\`. Add ?dryRun=true to preview. Batch up to 100 edits. |`,
27564
27596
  `| GET | /docs/:slug/archive | viewer+ | Get archive status |`,
27565
27597
  `| POST | /docs/:slug/archive | editor+ | Archive doc for 30 days; normal routes return 423 until restored |`,
@@ -27568,6 +27600,8 @@ function apiReference(baseUrl, slug, token, sid) {
27568
27600
  `| DELETE | /docs/:slug | owner | Deprecated archive alias; archives instead of purging. Prefer POST /archive; use DELETE /forever for permanent deletion |`,
27569
27601
  `| POST | /docs/:slug/comments | commenter+ | Add comment, suggestion, or reply |`,
27570
27602
  `| POST | /docs/:slug/comments/:cid/resolve | commenter+ | Resolve comment |`,
27603
+ `| POST | /docs/:slug/threads/:thread_id/resolve | commenter+ | Resolve thread by stable thread id |`,
27604
+ `| POST | /docs/:slug/threads/:thread_id/unresolve | commenter+ | Reopen thread by deleting pointer-bearing thread resolution marks |`,
27571
27605
  `| PATCH | /docs/:slug/comments/:cid | commenter+ | Edit comment text (author-only) |`,
27572
27606
  `| DELETE | /docs/:slug/comments/:cid | commenter+ | Delete comment (author-only) |`,
27573
27607
  `| POST | /docs/:slug/comments/:cid/accept | editor+ | Accept suggestion |`,
@@ -27610,6 +27644,10 @@ function apiReference(baseUrl, slug, token, sid) {
27610
27644
  `| \`BLOCK_ID_AMBIGUOUS\` | 409 | durable comment \`block_id\` maps to more than one current block; re-GET and retry after the document is repaired |`,
27611
27645
  `| \`BLOCK_ID_NOT_ADDRESSABLE\` | 409 | comment \`block_id\` points at a non-addressable wrapper; choose an addressable \`content_blocks[].id\` target |`,
27612
27646
  `| \`BLOCK_NOT_FOUND\` | 409 | deprecated blk_* block_id is stale or not from this revision \u2014 GET latest content_blocks and retry with a durable id |`,
27647
+ `| \`EXPECTED_REVISION_REQUIRED\` | 400 | whole-block suggestions and suggestion replies need \`expected_revision\` from the GET response because the request does not include a quote assertion |`,
27648
+ `| \`EXPECTED_REVISION_FUTURE\` | 409 | \`expected_revision\` is ahead of the current document revision \u2014 re-GET and retry with the returned revision |`,
27649
+ `| \`BLOCK_STALE\` | 409 | target block changed after \`expected_revision\`; retry against the returned \`revision\` and \`current_text\`, or re-GET if \`block_deleted: true\` |`,
27650
+ `| \`INVALID_FILTER\` | 400 | a compact thread filter was malformed; \`mentions\` currently supports only \`me\`, \`since_revision\` must be a non-negative integer, \`resolved\` must be \`true\` or \`false\`, and \`suggestions\` must be \`pending\`, \`stale\`, or \`open\` |`,
27613
27651
  `| \`COMMENT_ANCHOR_BLOCK_NOT_FOUND\` | 422 | quote-created plain comment could not resolve to one addressable durable block; GET latest and choose a block-level \`content_blocks[].id\` target |`,
27614
27652
  `| \`MARK_ANCHOR_NOT_FOUND\` | 422 | create/resolve could not build or resolve a canonical mark anchor; GET latest and use a currently visible mark or create a new comment |`,
27615
27653
  `| \`MULTI_BLOCK_COMMENT_UNSUPPORTED\` | 422 | plain comments must target one addressable block in this rollout; split the comment by block |`,
@@ -27626,14 +27664,17 @@ function apiReference(baseUrl, slug, token, sid) {
27626
27664
  `| \`POST_APPLY_MISMATCH\` | 422 | server refused an edit whose parsed result did not match the requested Markdown \u2014 GET latest and retry smaller |`,
27627
27665
  `| \`REPLY_TARGET_NOT_FOUND\` | 404 | reply_to ID doesn't exist \u2014 check comment IDs from the GET response |`,
27628
27666
  `| \`REPLY_TARGET_DELETED\` | 400 | cannot reply to a deleted comment \u2014 pick a different comment |`,
27629
- `| \`INVALID_REPLY\` | 400 | reply_to cannot be combined with suggestion \u2014 use quote instead |`,
27667
+ `| \`INVALID_REPLY\` | 400 | invalid reply combination, such as thread_id with kind:"resolution" \u2014 use the dedicated thread resolve route instead |`,
27630
27668
  `| \`REPLY_TARGET_ANCHOR_LOST\` | 409 | reply_to target no longer has a resolvable canonical anchor \u2014 GET latest and choose a visible comment or create a new top-level comment |`,
27669
+ `| \`THREAD_NOT_FOUND\` | 404 | thread_id does not match a current compact thread |`,
27670
+ `| \`THREAD_TARGET_MISMATCH\` | 400 | reply_to was supplied but does not belong to thread_id |`,
27671
+ `| \`ORPHANED_ANCHOR\` | 422 | thread_id target is orphaned for a write that needs live target text, or lacks a stored canonical anchor \u2014 GET latest and choose a visible thread/comment |`,
27631
27672
  ``,
27632
27673
  `## Limitations`,
27633
27674
  `- The editor does not support raw HTML. HTML tags and comments (e.g. \`<!-- ... -->\`, \`<div>\`) in edits return \`422 INVALID_MARKDOWN\`; escape them as literal text if they belong in the document. Standalone \`<br />\` lines from GET are the exception: they represent empty paragraph spacers and can be sent back in PATCH markdown.`,
27634
27675
  ``,
27635
27676
  `## Additional notes`,
27636
- `- **When sharing a document link with a user, always use \`share_url\`** (e.g. \`https://comment.io/d/{slug}?token={token}\`). Links without the token will not work.`,
27677
+ `- **When sharing a document link with a user, always use the exact \`share_url\` value from the API response.** Links without its embedded browser token will not work.`,
27637
27678
  `- For large replacements, GET the markdown programmatically \u2014 don't copy-paste through shell (Unicode issues).`
27638
27679
  ];
27639
27680
  }
@@ -27676,7 +27717,7 @@ function buildCompleteAgentDocs(baseUrl = "https://comment.io", sid) {
27676
27717
  ``,
27677
27718
  `## @mentions and polling`,
27678
27719
  ``,
27679
- `Other participants can @mention you by your handle in comments. Registered agents are automatically granted document access before a comment notification is appended, so your \`agent_secret\` works immediately on the mentioned doc. Document-body mentions are searchable but do not append notifications; to check for mentions without the daemon, webhooks, or lease API, poll \`GET /docs/{slug}\` every 10 seconds and search the \`markdown\` field and each \`blocks[].comments[].text\` for \`@{your-handle}\`. The \`participants\` array lists durable contributors and coordination actors with their type (\`anonymous_agent\`, \`registered_agent\`, or \`human\`); recent read-only visitors appear in \`active_agents\`.`,
27720
+ `Other participants can @mention you by your handle in comments. Registered agents are automatically granted document access before a comment notification is appended, so your \`agent_secret\` works immediately on the mentioned doc. Document-body mentions are searchable but do not append notifications; to check for mentions without the daemon, webhooks, or lease API, poll \`GET /docs/{slug}\` every 10 seconds and search the \`markdown\` field and each \`threads[].comments[].text\` for \`@{your-handle}\`. The \`participants\` array lists durable contributors and coordination actors with their type (\`anonymous_agent\`, \`registered_agent\`, or \`human\`); recent read-only visitors appear in \`active_agents\`.`,
27680
27721
  ``,
27681
27722
  `## AI Agent API`,
27682
27723
  ``,
@@ -27707,7 +27748,7 @@ function buildCompleteAgentDocs(baseUrl = "https://comment.io", sid) {
27707
27748
  ` "access_token_role": "editor",`,
27708
27749
  ` "url": "/docs/abc123",`,
27709
27750
  ` "api_url": "/docs/abc123",`,
27710
- ` "share_url": "/d/abc123?token=...",`,
27751
+ ` "share_url": "<token-bearing browser share URL>",`,
27711
27752
  ` "actor_id": "ai:anon.tkn.abc123def456",`,
27712
27753
  ` "identify_required": true,`,
27713
27754
  ` "identify_url": "/agents/identify",`,
@@ -27730,7 +27771,7 @@ function buildCompleteAgentDocs(baseUrl = "https://comment.io", sid) {
27730
27771
  ``,
27731
27772
  `For reading, editing, and commenting, use your \`agent_secret\` (registered agents) or the \`access_token\`. The \`id\` field is the document slug \u2014 use it in all \`/docs/{slug}\` API calls. Owner role is claimed automatically by the first human to open the \`share_url\` (agents can never be owner).`,
27732
27773
  ``,
27733
- `**If you're using the returned \`access_token\` (not a registered \`agent_secret\`), call \`POST /agents/identify\` next** \u2014 see "Identify yourself (once per doc)" below. The first write with an un-identified per-doc token returns \`412 IDENTIFY_REQUIRED\`.`,
27774
+ `**If you're using the returned \`access_token\` (not a registered \`agent_secret\`), identify before or during your first write** \u2014 include \`display_name\` in the first JSON write body, or call \`POST /agents/identify\` first. A write without either returns \`412 IDENTIFY_REQUIRED\`.`,
27734
27775
  ``,
27735
27776
  `**When sharing a comm with a user, always use \`share_url\` (prepend the base URL: \`${baseUrl}\` + \`share_url\`).** The share URL includes the auth token \u2014 without it, the link won't work. Never share a bare \`/d/{id}\` link.`,
27736
27777
  ``,
@@ -27756,7 +27797,7 @@ function buildCompleteAgentDocs(baseUrl = "https://comment.io", sid) {
27756
27797
  ``,
27757
27798
  `## Per-comm docs`,
27758
27799
  ``,
27759
- `If you have a comm URL like \`${baseUrl}/d/{slug}?token={token}\`, fetch it with \`Accept: text/markdown\` to get personalized API docs with your slug and token pre-filled.`,
27800
+ `If you have a browser comm share URL, fetch that URL with \`Accept: text/markdown\` to get personalized API docs with your slug and token pre-filled.`,
27760
27801
  ``,
27761
27802
  `## Agent Registration`,
27762
27803
  ``,
@@ -28268,7 +28309,7 @@ function buildHomeDocs(baseUrl = "https://comment.io", sid) {
28268
28309
  `# Read`,
28269
28310
  `curl -s -H "Authorization: Bearer {token}" "${baseUrl}/docs/{slug}"`,
28270
28311
  ``,
28271
- `# Identify once before the first write with a per-doc token`,
28312
+ `# Optional: identify once before the first write with a per-doc token`,
28272
28313
  `curl -s -X POST "${baseUrl}/agents/identify" \\`,
28273
28314
  ` -H "Authorization: Bearer {token}" \\`,
28274
28315
  ` -H "Content-Type: application/json" \\`,
@@ -28297,7 +28338,7 @@ function buildHomeDocs(baseUrl = "https://comment.io", sid) {
28297
28338
  ``,
28298
28339
  `## Per-comm docs`,
28299
28340
  ``,
28300
- `If you have a comm URL like \`${baseUrl}/d/{slug}?token={token}\`, fetch it with \`Accept: text/markdown\` to get personalized API docs with your slug and token pre-filled.`,
28341
+ `If you have a browser comm share URL, fetch that URL with \`Accept: text/markdown\` to get personalized API docs with your slug and token pre-filled.`,
28301
28342
  ``,
28302
28343
  `## Included API reference`,
28303
28344
  ``,
@@ -29948,8 +29989,7 @@ var CommentApiClient = class {
29948
29989
  }
29949
29990
  async getDocumentWithShareToken(slug, shareToken) {
29950
29991
  const url = new URL(this.url(`/docs/${slug}`));
29951
- url.searchParams.set("token", shareToken);
29952
- return this.request(url);
29992
+ return this.request(url, { token: shareToken });
29953
29993
  }
29954
29994
  async getJson(path, token, query = {}) {
29955
29995
  const url = new URL(this.url(path));
@@ -29958,8 +29998,12 @@ var CommentApiClient = class {
29958
29998
  }
29959
29999
  return this.request(url, { token });
29960
30000
  }
29961
- async postJson(path, token, body, headers = {}) {
29962
- return this.request(this.url(path), {
30001
+ async postJson(path, token, body, headers = {}, query = {}) {
30002
+ const url = new URL(this.url(path));
30003
+ for (const [key, value] of Object.entries(query)) {
30004
+ if (value !== void 0) url.searchParams.set(key, value);
30005
+ }
30006
+ return this.request(url, {
29963
30007
  method: "POST",
29964
30008
  token,
29965
30009
  headers: { "Content-Type": "application/json", ...headers },
@@ -30204,7 +30248,7 @@ function normalizeApiError(status, data, fallback) {
30204
30248
  }
30205
30249
  function nextForError(status, code) {
30206
30250
  if (status === 401) return "Refresh or replace the configured Comment.io credential.";
30207
- if (status === 412 || code === "IDENTIFY_REQUIRED") return "Call identify_agent for this document_ref before writing.";
30251
+ if (status === 412 || code === "IDENTIFY_REQUIRED") return "Retry the JSON write with display_name, or call identify_agent for this document_ref before writing.";
30208
30252
  if (status === 403 && code === "DOC_READ_ONLY") return "Use comment_on_comm or suggest_change instead of edit_comm.";
30209
30253
  if (status === 409 || status === 422) return "Read the returned current revision/markdown, then retry only if the target remains clear.";
30210
30254
  if (status === 429) return "Back off deterministically before retrying.";
@@ -30238,7 +30282,13 @@ var openCommInputSchema = external_exports.object({
30238
30282
  var readCommInputSchema = external_exports.object({
30239
30283
  document_ref: documentRefSchema,
30240
30284
  focus: external_exports.string().optional(),
30241
- include: external_exports.array(external_exports.enum(["comments", "authorship", "content_blocks"])).optional()
30285
+ include: external_exports.array(external_exports.enum(["comments", "authorship", "content_blocks"])).optional(),
30286
+ filters: external_exports.object({
30287
+ mentions: external_exports.literal("me").optional(),
30288
+ since_revision: external_exports.number().int().nonnegative().optional(),
30289
+ resolved: external_exports.boolean().optional(),
30290
+ suggestions: external_exports.enum(["pending", "stale", "open"]).optional()
30291
+ }).optional()
30242
30292
  });
30243
30293
  var identifyAgentInputSchema = external_exports.object({
30244
30294
  document_ref: documentRefSchema,
@@ -30249,7 +30299,8 @@ var editCommInputSchema = external_exports.object({
30249
30299
  edits: external_exports.array(editItemSchema).min(1).max(100),
30250
30300
  base_revision: external_exports.number().int().nonnegative().optional(),
30251
30301
  dry_run: external_exports.boolean().optional(),
30252
- idempotency_key: external_exports.string().min(8).max(160).optional()
30302
+ idempotency_key: external_exports.string().min(8).max(160).optional(),
30303
+ display_name: external_exports.string().min(1).max(100).optional()
30253
30304
  });
30254
30305
  var commentOnCommInputSchema = external_exports.object({
30255
30306
  document_ref: documentRefSchema,
@@ -30257,14 +30308,17 @@ var commentOnCommInputSchema = external_exports.object({
30257
30308
  quote: external_exports.string().optional(),
30258
30309
  block_id: external_exports.string().optional(),
30259
30310
  allow_mentions: external_exports.boolean().optional(),
30260
- mentions: external_exports.array(external_exports.string()).optional()
30311
+ mentions: external_exports.array(external_exports.string()).optional(),
30312
+ display_name: external_exports.string().min(1).max(100).optional()
30261
30313
  });
30262
30314
  var replyToCommentInputSchema = external_exports.object({
30263
30315
  document_ref: documentRefSchema,
30264
30316
  text: external_exports.string().min(1),
30265
- reply_to: external_exports.string().min(1),
30317
+ reply_to: external_exports.string().min(1).optional(),
30318
+ thread_id: external_exports.string().min(1).optional(),
30266
30319
  allow_mentions: external_exports.boolean().optional(),
30267
- mentions: external_exports.array(external_exports.string()).optional()
30320
+ mentions: external_exports.array(external_exports.string()).optional(),
30321
+ display_name: external_exports.string().min(1).max(100).optional()
30268
30322
  });
30269
30323
  var suggestChangeInputSchema = external_exports.object({
30270
30324
  document_ref: documentRefSchema,
@@ -30272,12 +30326,14 @@ var suggestChangeInputSchema = external_exports.object({
30272
30326
  quote: external_exports.string().min(1),
30273
30327
  new_string: external_exports.string(),
30274
30328
  allow_mentions: external_exports.boolean().optional(),
30275
- mentions: external_exports.array(external_exports.string()).optional()
30329
+ mentions: external_exports.array(external_exports.string()).optional(),
30330
+ display_name: external_exports.string().min(1).max(100).optional()
30276
30331
  });
30277
30332
  var resolveCommentInputSchema = external_exports.object({
30278
30333
  document_ref: documentRefSchema,
30279
30334
  comment_id: external_exports.string().min(1),
30280
- text: external_exports.string().min(1)
30335
+ text: external_exports.string().min(1),
30336
+ display_name: external_exports.string().min(1).max(100).optional()
30281
30337
  });
30282
30338
  var reportFeedbackInputSchema = external_exports.object({
30283
30339
  document_ref: documentRefSchema,
@@ -30382,7 +30438,7 @@ var CommentMcpRuntime = class {
30382
30438
  next_actions: ["Configure COMMENT_IO_AGENT_PROFILE or COMMENT_IO_AGENT_SECRET."]
30383
30439
  });
30384
30440
  }
30385
- const response = await this.api.getDocument(parsed.slug, this.config.agentSecret);
30441
+ const response = await this.api.getDocument(parsed.slug, this.config.agentSecret, documentReadQuery());
30386
30442
  if (!response.ok) return this.apiError(response.status, response.data, "Could not open comm.");
30387
30443
  const credential = this.store.create({
30388
30444
  baseUrl: this.config.baseUrl,
@@ -30404,17 +30460,22 @@ var CommentMcpRuntime = class {
30404
30460
  async readComm(input) {
30405
30461
  const credential = this.store.find(input.document_ref);
30406
30462
  if (!credential) return unknownDocumentRefResult(input.document_ref);
30407
- const { response, usedFallback } = await this.withCredentialFallback(credential, (token) => this.api.getDocument(credential.slug, token, { focus: input.focus }));
30463
+ const { response, usedFallback } = await this.withCredentialFallback(
30464
+ credential,
30465
+ (token) => this.api.getDocument(credential.slug, token, documentReadQuery(input.include, { focus: input.focus, filters: input.filters }))
30466
+ );
30408
30467
  if (!response.ok) return this.apiError(response.status, response.data, "Could not read comm.", credential);
30409
30468
  if (!usedFallback) {
30469
+ const mintedToken = stringValue(response.data.your_token);
30410
30470
  this.store.update(credential.documentRef, {
30471
+ ...mintedToken ? { token: mintedToken, tokenKind: "document_token" } : {},
30411
30472
  lastRevision: numberValue(response.data.revision),
30412
30473
  role: stringValue(response.data.your_role)
30413
30474
  });
30414
30475
  }
30415
30476
  return okResult({
30416
30477
  summary: `Read comm ${credential.slug} at revision ${numberValue(response.data.revision) ?? "unknown"}.`,
30417
- data: compactDocumentData(response.data, credential.documentRef, this.config.baseUrl, input.include, { forceReadOnly: usedFallback }),
30478
+ data: compactDocumentData(response.data, credential.documentRef, this.config.baseUrl, responseIncludeForRead(input), { forceReadOnly: usedFallback }),
30418
30479
  resource_links: resourceLinks(credential.documentRef),
30419
30480
  next_actions: usedFallback ? ["read_comm"] : nextActionsForDocument(response.data),
30420
30481
  warnings: warningsForDocument(response.data, usedFallback),
@@ -30447,7 +30508,11 @@ var CommentMcpRuntime = class {
30447
30508
  }
30448
30509
  const credential = this.store.find(input.document_ref);
30449
30510
  if (!credential) return unknownDocumentRefResult(input.document_ref);
30450
- const body = { edits: input.edits, ...input.base_revision !== void 0 ? { base_revision: input.base_revision } : {} };
30511
+ const body = {
30512
+ edits: input.edits,
30513
+ ...input.base_revision !== void 0 ? { base_revision: input.base_revision } : {},
30514
+ ...input.display_name ? { display_name: input.display_name } : {}
30515
+ };
30451
30516
  const headers = input.dry_run ? {} : { "Idempotency-Key": upstreamIdempotencyKey(input, credential) };
30452
30517
  const response = await this.api.patchJson(`/docs/${credential.slug}`, credential.token, body, input.dry_run ? { dryRun: "true" } : {}, headers);
30453
30518
  if (!response.ok) return this.apiError(response.status, response.data, "Could not edit comm.", credential);
@@ -30473,16 +30538,25 @@ var CommentMcpRuntime = class {
30473
30538
  return this.createComment(input.document_ref, {
30474
30539
  text: input.text,
30475
30540
  ...input.quote ? { quote: input.quote } : { block_id: input.block_id },
30476
- ...input.mentions ? { mentions: input.mentions } : {}
30541
+ ...input.mentions ? { mentions: input.mentions } : {},
30542
+ ...input.display_name ? { display_name: input.display_name } : {}
30477
30543
  }, "Comment created.", "Could not create comment.");
30478
30544
  }
30479
30545
  async replyToComment(input) {
30480
30546
  const guard = mentionGuard(input);
30481
30547
  if (guard) return guard;
30548
+ if (!input.reply_to && !input.thread_id) {
30549
+ return errorResult({
30550
+ summary: "reply_to_comment requires reply_to or thread_id.",
30551
+ error: { status: 400, code: "INVALID_TARGET", message: "Provide reply_to or thread_id." }
30552
+ });
30553
+ }
30482
30554
  return this.createComment(input.document_ref, {
30483
30555
  text: input.text,
30484
- reply_to: input.reply_to,
30485
- ...input.mentions ? { mentions: input.mentions } : {}
30556
+ ...input.reply_to ? { reply_to: input.reply_to } : {},
30557
+ ...input.thread_id ? { thread_id: input.thread_id } : {},
30558
+ ...input.mentions ? { mentions: input.mentions } : {},
30559
+ ...input.display_name ? { display_name: input.display_name } : {}
30486
30560
  }, "Reply created.", "Could not create reply.");
30487
30561
  }
30488
30562
  async suggestChange(input) {
@@ -30492,13 +30566,21 @@ var CommentMcpRuntime = class {
30492
30566
  text: input.text,
30493
30567
  quote: input.quote,
30494
30568
  suggestion: { new_string: input.new_string },
30495
- ...input.mentions ? { mentions: input.mentions } : {}
30569
+ ...input.mentions ? { mentions: input.mentions } : {},
30570
+ ...input.display_name ? { display_name: input.display_name } : {}
30496
30571
  }, "Suggestion created.", "Could not create suggestion.");
30497
30572
  }
30498
30573
  async resolveComment(input) {
30499
30574
  const credential = this.store.find(input.document_ref);
30500
30575
  if (!credential) return unknownDocumentRefResult(input.document_ref);
30501
- const response = await this.api.postJson(`/docs/${credential.slug}/comments/${encodeURIComponent(input.comment_id)}/resolve`, credential.token, { text: input.text });
30576
+ const response = await this.api.postJson(
30577
+ `/docs/${credential.slug}/comments/${encodeURIComponent(input.comment_id)}/resolve`,
30578
+ credential.token,
30579
+ {
30580
+ text: input.text,
30581
+ ...input.display_name ? { display_name: input.display_name } : {}
30582
+ }
30583
+ );
30502
30584
  if (!response.ok) return this.apiError(response.status, response.data, "Could not resolve comment.", credential);
30503
30585
  this.store.update(credential.documentRef, { lastRevision: numberValue(response.data.revision) });
30504
30586
  return okResult({
@@ -30579,19 +30661,28 @@ var CommentMcpRuntime = class {
30579
30661
  const nextUri = nextCursor ? historyNextUri(uri, nextCursor, query.limit) : null;
30580
30662
  return { mimeType: "application/json", text: JSON.stringify(untrusted({ history: response2.data, next_uri: nextUri }), null, 2) };
30581
30663
  }
30582
- const { response, usedFallback } = await this.withCredentialFallback(credential, (token) => this.api.getDocument(credential.slug, token));
30664
+ const { response, usedFallback } = await this.withCredentialFallback(
30665
+ credential,
30666
+ (token) => this.api.getDocument(credential.slug, token, documentReadQuery(resourceIncludeForProjection(projection)))
30667
+ );
30583
30668
  if (!response.ok) throw resourceReadError("Could not read document resource.", response.status, response.data);
30584
30669
  const data = response.data;
30585
30670
  if (projection === "markdown") return { mimeType: "text/markdown", text: `<!-- untrusted document markdown; canonical text for exact edits -->
30586
30671
  ${data.markdown ?? ""}` };
30587
- if (projection === "comments") return { mimeType: "application/json", text: JSON.stringify(untrusted({ blocks: data.blocks ?? [], focused: data.focused ?? null, actors: data.actors ?? {} }), null, 2) };
30672
+ if (projection === "comments") return { mimeType: "application/json", text: JSON.stringify(untrusted(commentProjection(data)), null, 2) };
30588
30673
  if (projection === "authorship") return { mimeType: "application/json", text: JSON.stringify(untrusted({ authorship: data.authorship ?? [] }), null, 2) };
30589
30674
  return { mimeType: "application/json", text: JSON.stringify(untrusted(compactDocumentData(data, credential.documentRef, this.config.baseUrl, [], { forceReadOnly: usedFallback })), null, 2) };
30590
30675
  }
30591
- async createComment(documentRef, body, summary, errorSummary) {
30676
+ async createComment(documentRef, body, summary, errorSummary, query = {}) {
30592
30677
  const credential = this.store.find(documentRef);
30593
30678
  if (!credential) return unknownDocumentRefResult(documentRef);
30594
- const response = await this.api.postJson(`/docs/${credential.slug}/comments`, credential.token, body);
30679
+ const response = await this.api.postJson(
30680
+ `/docs/${credential.slug}/comments`,
30681
+ credential.token,
30682
+ body,
30683
+ {},
30684
+ query
30685
+ );
30595
30686
  if (!response.ok) return this.apiError(response.status, response.data, errorSummary, credential);
30596
30687
  this.store.update(credential.documentRef, { lastRevision: numberValue(response.data.revision) });
30597
30688
  return okResult({
@@ -30657,14 +30748,47 @@ var CommentMcpResourceError = class extends Error {
30657
30748
  function parseDocumentLocator(input, baseUrl) {
30658
30749
  if (slugSchema.safeParse(input).success) return { slug: input };
30659
30750
  const url = new URL(input.startsWith("/") ? `${baseUrl}${input}` : input);
30660
- const docsMatch = url.pathname.match(/^\/docs\/([a-z0-9]{3,64})/);
30661
- const docMatch = url.pathname.match(/^\/d\/([a-z0-9]{3,64})/);
30662
- const slug = docsMatch?.[1] ?? docMatch?.[1];
30663
- if (!slug) throw new Error("Expected a Comment.io slug, /docs/{slug} URL, or /d/{slug}?token=... share URL.");
30664
- return { slug, shareToken: url.searchParams.get("token") ?? void 0, url };
30751
+ const docsMatch = url.pathname.match(/^\/docs\/([a-z0-9]{3,64})\/?$/);
30752
+ if (docsMatch) {
30753
+ if (url.searchParams.has("token")) {
30754
+ throw new Error("Token-bearing document URLs must use /d/{slug}?token=... share URLs; /docs/{slug} does not accept token query auth.");
30755
+ }
30756
+ return { slug: docsMatch[1], url };
30757
+ }
30758
+ const docMatch = url.pathname.match(/^\/d\/([a-z0-9]{3,64})\/?$/);
30759
+ if (docMatch) return { slug: docMatch[1], shareToken: url.searchParams.get("token") ?? void 0, url };
30760
+ throw new Error("Expected a Comment.io slug, /docs/{slug} URL, or /d/{slug}?token=... share URL.");
30761
+ }
30762
+ function documentReadQuery(include = [], options = {}) {
30763
+ const includeFlags = [];
30764
+ if (include?.includes("authorship")) includeFlags.push("authorship");
30765
+ if (include?.includes("content_blocks")) includeFlags.push("block_quotes");
30766
+ const filters = options.filters;
30767
+ return {
30768
+ focus: options.focus,
30769
+ mentions: filters?.mentions,
30770
+ since_revision: filters?.since_revision === void 0 ? void 0 : String(filters.since_revision),
30771
+ resolved: filters?.resolved === void 0 ? void 0 : String(filters.resolved),
30772
+ suggestions: filters?.suggestions,
30773
+ include: includeFlags.length ? includeFlags.join(",") : void 0
30774
+ };
30775
+ }
30776
+ function responseIncludeForRead(input) {
30777
+ if (!input.filters) return input.include;
30778
+ return [.../* @__PURE__ */ new Set([...input.include ?? [], "comments"])];
30779
+ }
30780
+ function resourceIncludeForProjection(projection) {
30781
+ if (projection === "authorship") return ["authorship"];
30782
+ return [];
30665
30783
  }
30666
30784
  function compactDocumentData(data, documentRef, baseUrl, include = [], options = {}) {
30667
30785
  const slug = getSlug(data);
30786
+ const comments = commentProjection(data);
30787
+ const returnedThreadCount = Array.isArray(data.threads) ? data.threads.length : 0;
30788
+ const threadTotal = numericRecordField(data.threads_info, "total") ?? returnedThreadCount;
30789
+ const returnedCommentCount = returnedCommentCountForDocument(data);
30790
+ const commentTotal = commentCount(data);
30791
+ const hasThreadPagination = threadTotal !== returnedThreadCount || numericRecordField(data.threads_info, "doc_total") !== void 0;
30668
30792
  const compact = {
30669
30793
  document_ref: documentRef,
30670
30794
  slug,
@@ -30676,17 +30800,59 @@ function compactDocumentData(data, documentRef, baseUrl, include = [], options =
30676
30800
  doc_url: `${baseUrl}/d/${slug}`,
30677
30801
  api_url: `${baseUrl}/docs/${slug}`,
30678
30802
  counts: {
30679
- blocks: Array.isArray(data.blocks) ? data.blocks.length : 0,
30803
+ threads: threadTotal,
30804
+ ...hasThreadPagination ? { threads_returned: returnedThreadCount } : {},
30805
+ comments: commentTotal,
30806
+ ...commentTotal !== returnedCommentCount || hasThreadPagination ? { comments_returned: returnedCommentCount } : {},
30680
30807
  content_blocks: Array.isArray(data.content_blocks) ? data.content_blocks.length : 0,
30681
30808
  authorship_ranges: Array.isArray(data.authorship) ? data.authorship.length : 0
30682
30809
  }
30683
30810
  };
30684
- if (include?.includes("comments")) compact.blocks = data.blocks ?? [];
30811
+ if (include?.includes("comments")) {
30812
+ compact.threads = comments.threads;
30813
+ if (comments.threads_info) compact.threads_info = comments.threads_info;
30814
+ if (comments.suggestions_info) compact.suggestions_info = comments.suggestions_info;
30815
+ }
30685
30816
  if (include?.includes("authorship")) compact.authorship = data.authorship ?? [];
30686
30817
  if (include?.includes("content_blocks")) compact.content_blocks = data.content_blocks ?? [];
30818
+ if (include?.includes("comments") || data.focused) compact.actors = comments.actors;
30687
30819
  if (data.focused) compact.focused = data.focused;
30688
30820
  return compact;
30689
30821
  }
30822
+ function commentProjection(data) {
30823
+ return {
30824
+ threads: Array.isArray(data.threads) ? data.threads : [],
30825
+ threads_info: data.threads_info ?? null,
30826
+ suggestions_info: data.suggestions_info ?? null,
30827
+ focused: data.focused ?? null,
30828
+ actors: data.actors ?? {}
30829
+ };
30830
+ }
30831
+ function commentCount(data) {
30832
+ if (Array.isArray(data.threads)) {
30833
+ return data.threads.reduce((total, thread) => {
30834
+ if (!thread || typeof thread !== "object") return total;
30835
+ const info = thread.comments_info;
30836
+ if (typeof info?.total === "number") return total + info.total;
30837
+ const comments = thread.comments;
30838
+ return total + (Array.isArray(comments) ? comments.length : 0);
30839
+ }, 0);
30840
+ }
30841
+ return 0;
30842
+ }
30843
+ function returnedCommentCountForDocument(data) {
30844
+ if (!Array.isArray(data.threads)) return 0;
30845
+ return data.threads.reduce((total, thread) => {
30846
+ if (!thread || typeof thread !== "object") return total;
30847
+ const comments = thread.comments;
30848
+ return total + (Array.isArray(comments) ? comments.length : 0);
30849
+ }, 0);
30850
+ }
30851
+ function numericRecordField(value, field) {
30852
+ if (!value || typeof value !== "object") return void 0;
30853
+ const fieldValue = value[field];
30854
+ return typeof fieldValue === "number" && Number.isFinite(fieldValue) ? fieldValue : void 0;
30855
+ }
30690
30856
  function compactEditData(data) {
30691
30857
  return {
30692
30858
  revision: data.revision ?? null,
@@ -30911,7 +31077,7 @@ function registerTools(server2, runtime) {
30911
31077
  }, async (args) => asCallToolResult(await runtime.commentOnComm(args)));
30912
31078
  server2.registerTool("reply_to_comment", {
30913
31079
  title: "Reply to comment",
30914
- description: "Reply to an existing comment thread without combining reply and suggestion fields.",
31080
+ description: "Reply to an existing comment or compact thread without combining reply and suggestion fields.",
30915
31081
  inputSchema: replyToCommentInputSchema,
30916
31082
  outputSchema: commentMcpToolResultSchema,
30917
31083
  annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: true }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@comment-io/cli",
3
- "version": "0.1.14-alpha.347",
3
+ "version": "0.1.14-alpha.365",
4
4
  "description": "Comment.io CLI and local notification daemon",
5
5
  "private": false,
6
6
  "type": "module",
@@ -41,6 +41,6 @@
41
41
  "node": ">=20"
42
42
  },
43
43
  "commentio": {
44
- "sourceSha": "6b7a8d749c723aba71dd45711d977c0d0b792422"
44
+ "sourceSha": "174de17130f732e643714a8d539134c2cf8f38fe"
45
45
  }
46
46
  }