@mrsf/cli 0.3.1 → 0.4.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
@@ -82,6 +82,15 @@ mrsf add docs/api.md \
82
82
  --author "alice" \
83
83
  --text "Good catch, fixed in abc123" \
84
84
  --reply-to 3eeccbd3
85
+
86
+ # Attach tool-specific x_* metadata
87
+ mrsf add docs/api.md \
88
+ --author "bot" \
89
+ --text "Flagging this for follow-up" \
90
+ --line 27 \
91
+ --ext x_source=triage-bot \
92
+ --ext x_score=0.91 \
93
+ --ext 'x_labels=["needs-review","docs"]'
85
94
  ```
86
95
 
87
96
  | Option | Description |
@@ -96,6 +105,9 @@ mrsf add docs/api.md \
96
105
  | `--severity <level>` | Importance: `low`, `medium`, `high` |
97
106
  | `--reply-to <id>` | Reply to an existing comment by ID |
98
107
  | `--selected-text <text>` | Override auto-detected selected text |
108
+ | `--ext <key=value>` | Add an `x_*` extension field; repeat for multiple fields |
109
+
110
+ `--ext` values are stored as flat `x_*` fields in the sidecar. Primitive JSON literals like numbers, `true`, `false`, and `null` are parsed automatically, and objects/arrays can be passed as JSON.
99
111
 
100
112
  ---
101
113
 
@@ -8,6 +8,46 @@ import { parseSidecar, readDocumentLines } from "../lib/parser.js";
8
8
  import { writeSidecar } from "../lib/writer.js";
9
9
  import { addComment, populateSelectedText } from "../lib/comments.js";
10
10
  import { findRepoRoot } from "../lib/git.js";
11
+ function collectOptionValues(value, previous) {
12
+ previous.push(value);
13
+ return previous;
14
+ }
15
+ function parseExtensionValue(rawValue) {
16
+ const trimmed = rawValue.trim();
17
+ if (/^(true|false|null)$/u.test(trimmed)) {
18
+ return JSON.parse(trimmed);
19
+ }
20
+ if (/^-?(0|[1-9]\d*)(\.\d+)?([eE][+-]?\d+)?$/u.test(trimmed)) {
21
+ return JSON.parse(trimmed);
22
+ }
23
+ if ((trimmed.startsWith("{") && trimmed.endsWith("}")) || (trimmed.startsWith("[") && trimmed.endsWith("]"))) {
24
+ try {
25
+ return JSON.parse(trimmed);
26
+ }
27
+ catch (error) {
28
+ const message = error instanceof Error ? error.message : String(error);
29
+ throw new Error(`Invalid JSON extension value '${rawValue}': ${message}`);
30
+ }
31
+ }
32
+ return rawValue;
33
+ }
34
+ function parseExtensionFlags(flags) {
35
+ if (!flags || flags.length === 0)
36
+ return undefined;
37
+ const entries = flags.map((flag) => {
38
+ const separatorIndex = flag.indexOf("=");
39
+ if (separatorIndex <= 0) {
40
+ throw new Error(`Invalid --ext value '${flag}'. Expected key=value.`);
41
+ }
42
+ const key = flag.slice(0, separatorIndex).trim();
43
+ const rawValue = flag.slice(separatorIndex + 1);
44
+ if (!key) {
45
+ throw new Error(`Invalid --ext value '${flag}'. Expected a non-empty key.`);
46
+ }
47
+ return [key, parseExtensionValue(rawValue)];
48
+ });
49
+ return Object.fromEntries(entries);
50
+ }
11
51
  export function registerAdd(program) {
12
52
  program
13
53
  .command("add <document>")
@@ -22,6 +62,7 @@ export function registerAdd(program) {
22
62
  .option("--severity <level>", "Severity: low | medium | high")
23
63
  .option("--reply-to <id>", "Reply to an existing comment")
24
64
  .option("--selected-text <text>", "Selected text to attach")
65
+ .option("--ext <key=value>", "Comment extension field keyed by x_* (repeatable)", collectOptionValues, [])
25
66
  .action(async (document, opts) => {
26
67
  const parentOpts = program.opts();
27
68
  const cwd = parentOpts.cwd ?? process.cwd();
@@ -58,6 +99,7 @@ export function registerAdd(program) {
58
99
  type: opts.type,
59
100
  severity: opts.severity,
60
101
  reply_to: opts.replyTo,
102
+ extensions: parseExtensionFlags(opts.ext),
61
103
  }, repoRoot ?? undefined);
62
104
  // Auto-populate selected_text from the document if not provided
63
105
  if (opts.selectedText) {
package/dist/index.d.ts CHANGED
@@ -4,7 +4,7 @@
4
4
  * Usage:
5
5
  * import { validate, reanchorFile, addComment, ... } from "mrsf";
6
6
  */
7
- export type { MrsfDocument, Comment, MrsfConfig, DiagnosticSeverity, ValidationDiagnostic, ValidationResult, ReanchorStatus, ReanchorResult, FuzzyCandidate, DiffHunk, AddCommentOptions, CommentFilter, AnchorHealth, StatusResult, BaseOptions, ReanchorOptions, ValidateOptions, } from "./lib/types.js";
7
+ export type { MrsfDocument, Comment, CommentExtensions, CommentExtensionValue, MrsfConfig, DiagnosticSeverity, ValidationDiagnostic, ValidationResult, ReanchorStatus, ReanchorResult, FuzzyCandidate, DiffHunk, AddCommentOptions, CommentFilter, AnchorHealth, StatusResult, BaseOptions, ReanchorOptions, ValidateOptions, } from "./lib/types.js";
8
8
  export { findWorkspaceRoot, loadConfig, discoverSidecar, sidecarToDocument, discoverAllSidecars, } from "./lib/discovery.js";
9
9
  export { resolveSidecarPaths } from "./lib/resolve-files.js";
10
10
  export { parseSidecar, parseSidecarContent, parseSidecarLenient, parseSidecarContentLenient, readDocumentLines, } from "./lib/parser.js";
@@ -14,6 +14,6 @@ export { validate, validateFile } from "./lib/validator.js";
14
14
  export { exactMatch, normalizedMatch, fuzzySearch, combinedScore, } from "./lib/fuzzy.js";
15
15
  export { isGitAvailable, findRepoRoot, getCurrentCommit, isStale, getDiff, getLineShift, getFileAtCommit, getStagedFiles, detectRenames, parseDiffHunks, } from "./lib/git.js";
16
16
  export { reanchorComment, reanchorDocument, applyReanchorResults, reanchorFile, } from "./lib/reanchor.js";
17
- export { addComment, populateSelectedText, resolveComment, unresolveComment, removeComment, filterComments, getThreads, summarize, } from "./lib/comments.js";
17
+ export { addComment, normalizeCommentExtensions, populateSelectedText, resolveComment, unresolveComment, removeComment, filterComments, getThreads, summarize, } from "./lib/comments.js";
18
18
  export type { CommentSummary, RemoveCommentOptions } from "./lib/comments.js";
19
19
  //# sourceMappingURL=index.d.ts.map
package/dist/index.js CHANGED
@@ -21,5 +21,5 @@ export { isGitAvailable, findRepoRoot, getCurrentCommit, isStale, getDiff, getLi
21
21
  // Re-anchoring
22
22
  export { reanchorComment, reanchorDocument, applyReanchorResults, reanchorFile, } from "./lib/reanchor.js";
23
23
  // Comments
24
- export { addComment, populateSelectedText, resolveComment, unresolveComment, removeComment, filterComments, getThreads, summarize, } from "./lib/comments.js";
24
+ export { addComment, normalizeCommentExtensions, populateSelectedText, resolveComment, unresolveComment, removeComment, filterComments, getThreads, summarize, } from "./lib/comments.js";
25
25
  //# sourceMappingURL=index.js.map
@@ -1,7 +1,8 @@
1
1
  /**
2
2
  * MRSF Comment Operations — CRUD for sidecar comments.
3
3
  */
4
- import type { AddCommentOptions, Comment, CommentFilter, MrsfDocument } from "./types.js";
4
+ import type { AddCommentOptions, Comment, CommentFilter, CommentExtensions, MrsfDocument } from "./types.js";
5
+ export declare function normalizeCommentExtensions(extensions?: Record<string, unknown>): CommentExtensions;
5
6
  /**
6
7
  * Add a new comment to a document. Mutates doc.comments in place.
7
8
  * Returns the created comment.
@@ -7,6 +7,49 @@ import { getCurrentCommit, isGitAvailable } from "./git.js";
7
7
  // ---------------------------------------------------------------------------
8
8
  // Add
9
9
  // ---------------------------------------------------------------------------
10
+ function isPlainObject(value) {
11
+ if (value === null || typeof value !== "object")
12
+ return false;
13
+ const prototype = Object.getPrototypeOf(value);
14
+ return prototype === Object.prototype || prototype === null;
15
+ }
16
+ function isCommentExtensionValue(value) {
17
+ if (value == null)
18
+ return true;
19
+ switch (typeof value) {
20
+ case "boolean":
21
+ case "string":
22
+ return true;
23
+ case "number":
24
+ return Number.isFinite(value);
25
+ case "object": {
26
+ if (Array.isArray(value)) {
27
+ return value.every((item) => isCommentExtensionValue(item));
28
+ }
29
+ if (!isPlainObject(value)) {
30
+ return false;
31
+ }
32
+ return Object.values(value).every((item) => isCommentExtensionValue(item));
33
+ }
34
+ default:
35
+ return false;
36
+ }
37
+ }
38
+ export function normalizeCommentExtensions(extensions) {
39
+ if (!extensions)
40
+ return {};
41
+ const normalizedEntries = [];
42
+ for (const [key, value] of Object.entries(extensions)) {
43
+ if (!key.startsWith("x_")) {
44
+ throw new Error(`Comment extension key '${key}' must start with 'x_'.`);
45
+ }
46
+ if (!isCommentExtensionValue(value)) {
47
+ throw new Error(`Comment extension '${key}' must be JSON-serializable (null, boolean, finite number, string, array, or plain object).`);
48
+ }
49
+ normalizedEntries.push([key, value]);
50
+ }
51
+ return Object.fromEntries(normalizedEntries);
52
+ }
10
53
  /**
11
54
  * Add a new comment to a document. Mutates doc.comments in place.
12
55
  * Returns the created comment.
@@ -43,6 +86,7 @@ export async function addComment(doc, opts, repoRoot) {
43
86
  comment.reply_to = opts.reply_to;
44
87
  if (commit)
45
88
  comment.commit = commit;
89
+ Object.assign(comment, normalizeCommentExtensions(opts.extensions));
46
90
  doc.comments.push(comment);
47
91
  return comment;
48
92
  }
@@ -30,6 +30,10 @@ export interface Comment {
30
30
  /** Catch-all for unknown / x_-prefixed extension fields. */
31
31
  [key: string]: unknown;
32
32
  }
33
+ export type CommentExtensionValue = null | boolean | number | string | CommentExtensionValue[] | {
34
+ [key: string]: CommentExtensionValue;
35
+ };
36
+ export type CommentExtensions = Record<`x_${string}`, CommentExtensionValue>;
33
37
  export interface MrsfConfig {
34
38
  sidecar_root?: string;
35
39
  }
@@ -106,6 +110,8 @@ export interface AddCommentOptions {
106
110
  id?: string;
107
111
  /** Override auto-generated timestamp. */
108
112
  timestamp?: string;
113
+ /** Tool-specific extension fields keyed by x_*. */
114
+ extensions?: CommentExtensions;
109
115
  }
110
116
  export interface CommentFilter {
111
117
  open?: boolean;
@@ -150,6 +150,16 @@ function valueToSource(v) {
150
150
  function valueType(v) {
151
151
  return valueToSource(v).startsWith('"') ? "double-quoted-scalar" : "scalar";
152
152
  }
153
+ function isStructuredValue(value) {
154
+ if (value == null)
155
+ return false;
156
+ if (Array.isArray(value))
157
+ return true;
158
+ return typeof value === "object" && !(value instanceof Date);
159
+ }
160
+ function hasStructuredCommentExtensions(doc) {
161
+ return doc.comments.some((comment) => Object.entries(comment).some(([key, value]) => !COMMENT_KEY_ORDER.includes(key) && value !== undefined && isStructuredValue(value)));
162
+ }
153
163
  /* ·· CST node construction ············································ */
154
164
  const NL = "\n";
155
165
  /**
@@ -351,17 +361,21 @@ export async function writeSidecar(filePath, doc) {
351
361
  */
352
362
  async function writeSidecarInternal(abs, doc) {
353
363
  const isJson = abs.endsWith(".review.json");
364
+ for (const comment of doc.comments)
365
+ syncHash(comment);
354
366
  if (isJson) {
355
- // For JSON, always sync hashes and write fresh
356
- for (const comment of doc.comments)
357
- syncHash(comment);
367
+ // For JSON, always write fresh
358
368
  await atomicWriteFile(abs, toJson(doc));
359
369
  return;
360
370
  }
371
+ // The CST patcher only supports scalar map values. Fall back to full YAML
372
+ // serialisation when comment extensions include arrays or objects.
373
+ if (hasStructuredCommentExtensions(doc)) {
374
+ await atomicWriteFile(abs, toYaml(doc));
375
+ return;
376
+ }
361
377
  // ── YAML round-trip path ──────────────────────────────────────────
362
378
  if (!existsSync(abs)) {
363
- for (const comment of doc.comments)
364
- syncHash(comment);
365
379
  await atomicWriteFile(abs, toYaml(doc));
366
380
  return;
367
381
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mrsf/cli",
3
- "version": "0.3.1",
3
+ "version": "0.4.0",
4
4
  "description": "CLI and library for the Markdown Review Sidecar Format (MRSF)",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",