relic-mcp 0.4.0 → 0.5.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.
@@ -10,7 +10,7 @@
10
10
  {
11
11
  "name": "relic",
12
12
  "description": "Publish a file from your machine as an encrypted, shareable link. The agent encrypts locally, uploads only ciphertext, and hands back a URL whose fragment holds the key, so the service stores something it cannot read.",
13
- "version": "0.4.0",
13
+ "version": "0.5.0",
14
14
  "source": "./",
15
15
  "author": {
16
16
  "name": "The Bushido Collective",
@@ -21,6 +21,6 @@
21
21
  }
22
22
  ],
23
23
  "metadata": {
24
- "version": "0.4.0"
24
+ "version": "0.5.0"
25
25
  }
26
26
  }
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "relic",
3
- "version": "0.4.0",
3
+ "version": "0.5.0",
4
4
  "description": "Publish a file from your machine as an encrypted, shareable link. The agent encrypts locally, uploads only ciphertext, and hands back a URL whose fragment holds the key, so the service stores something it cannot read.",
5
5
  "mcpServers": "./mcp-servers.json",
6
6
  "author": {
package/dist/relic-mcp.js CHANGED
@@ -1317,6 +1317,7 @@ async function readComments(relicId, deps) {
1317
1317
  created_at: createdAt,
1318
1318
  display_name: null,
1319
1319
  body: null,
1320
+ anchor: null,
1320
1321
  readable: false,
1321
1322
  unreadable_reason: "the row carried no ciphertext"
1322
1323
  });
@@ -1330,6 +1331,7 @@ async function readComments(relicId, deps) {
1330
1331
  created_at: createdAt,
1331
1332
  display_name: plaintext.display_name,
1332
1333
  body: plaintext.body,
1334
+ anchor: plaintext.anchor ?? null,
1333
1335
  readable: true,
1334
1336
  unreadable_reason: null
1335
1337
  });
@@ -1341,6 +1343,7 @@ async function readComments(relicId, deps) {
1341
1343
  created_at: createdAt,
1342
1344
  display_name: null,
1343
1345
  body: null,
1346
+ anchor: null,
1344
1347
  readable: false,
1345
1348
  unreadable_reason: `it did not decrypt under this relic's comment key: ${error.message}`
1346
1349
  });
@@ -1673,6 +1676,16 @@ var READ_COMMENTS_TOOL_DEFINITION = {
1673
1676
  created_at: { type: "string" },
1674
1677
  display_name: { type: ["string", "null"] },
1675
1678
  body: { type: ["string", "null"] },
1679
+ anchor: {
1680
+ type: ["object", "null"],
1681
+ description: "What the comment marks, or null for a freeform one. " + '`{kind:"text", quote}` is a passage the reader selected, so ' + 'the quote names the line to act on. `{kind:"pin", x, y}` is ' + "a point on the rendered page in unit coordinates.",
1682
+ properties: {
1683
+ kind: { type: "string", enum: ["text", "pin"] },
1684
+ quote: { type: "string" },
1685
+ x: { type: "number" },
1686
+ y: { type: "number" }
1687
+ }
1688
+ },
1676
1689
  readable: { type: "boolean" },
1677
1690
  unreadable_reason: { type: ["string", "null"] }
1678
1691
  },
@@ -1682,6 +1695,7 @@ var READ_COMMENTS_TOOL_DEFINITION = {
1682
1695
  "created_at",
1683
1696
  "display_name",
1684
1697
  "body",
1698
+ "anchor",
1685
1699
  "readable",
1686
1700
  "unreadable_reason"
1687
1701
  ],
@@ -1738,7 +1752,7 @@ var DESCRIBE_TOOL_DEFINITION = {
1738
1752
  additionalProperties: false
1739
1753
  }
1740
1754
  };
1741
- var SERVER_VERSION = "0.4.0";
1755
+ var SERVER_VERSION = "0.5.0";
1742
1756
  var SERVER_INFO = {
1743
1757
  name: "relic",
1744
1758
  title: "Relic",
@@ -2035,15 +2049,29 @@ async function callComment(id2, params, deps) {
2035
2049
  return { jsonrpc: "2.0", id: id2, result: toolError(error) };
2036
2050
  }
2037
2051
  }
2052
+ function markLine(anchor) {
2053
+ if (anchor === null)
2054
+ return "";
2055
+ if (anchor.kind === "text")
2056
+ return `on "${anchor.quote}"
2057
+ `;
2058
+ const percent = (value) => Math.round(value * 100);
2059
+ return `at ${percent(anchor.x)}% across, ${percent(anchor.y)}% down
2060
+ `;
2061
+ }
2038
2062
  function commentTranscript(result) {
2039
2063
  if (result.count === 0) {
2040
2064
  return `No comments on relic ${result.relic_id} yet.`;
2041
2065
  }
2042
2066
  const lines = result.comments.map((comment) => {
2043
2067
  const who = comment.display_name === null ? comment.author : `${comment.display_name} (${comment.author})`;
2044
- return comment.readable ? `${comment.created_at} ${who}:
2045
- ${comment.body}` : `${comment.created_at} ${who}:
2068
+ if (!comment.readable) {
2069
+ return `${comment.created_at} ${who}:
2046
2070
  [unreadable: ${comment.unreadable_reason}]`;
2071
+ }
2072
+ const mark = markLine(comment.anchor);
2073
+ return `${comment.created_at} ${who}:
2074
+ ${mark}${comment.body}`;
2047
2075
  });
2048
2076
  const header = result.unreadable_count === 0 ? `${result.count} comment(s) on relic ${result.relic_id}, oldest first.` : `${result.count} comment(s) on relic ${result.relic_id}, oldest ` + `first. ${result.unreadable_count} did not decrypt and are shown as ` + "unreadable rather than dropped, so treat this conversation as " + "partially unread.";
2049
2077
  return `${header}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "relic-mcp",
3
- "version": "0.4.0",
3
+ "version": "0.5.0",
4
4
  "description": "Publish a file as an encrypted relic. The key is generated on your machine and never sent to the service.",
5
5
  "license": "MIT",
6
6
  "type": "module",
package/src/comments.ts CHANGED
@@ -30,6 +30,7 @@
30
30
  import {
31
31
  COMMENT_BODY_LIMIT_BYTES,
32
32
  COMMENT_DISPLAY_NAME_LIMIT_BYTES,
33
+ type CommentAnchor,
33
34
  decodeKey,
34
35
  decryptComment,
35
36
  deriveCommentKey,
@@ -59,6 +60,16 @@ export interface CommentRecord {
59
60
  readonly display_name: string | null;
60
61
  /** Null exactly when `readable` is false. */
61
62
  readonly body: string | null;
63
+ /**
64
+ * What the comment is a mark on, or null for a freeform one.
65
+ *
66
+ * Dropping this was a defect rather than an omission. A reviewer's "this
67
+ * line is wrong" is not actionable without the line, and an agent reading a
68
+ * thread back could not tell a mark from a general remark, which is the
69
+ * difference between fixing the right sentence and guessing. It lives inside
70
+ * the encrypted envelope, so it arrives with the body or not at all.
71
+ */
72
+ readonly anchor: CommentAnchor | null;
62
73
  readonly readable: boolean;
63
74
  /** Null exactly when `readable` is true. */
64
75
  readonly unreadable_reason: string | null;
@@ -138,6 +149,7 @@ export async function readComments(
138
149
  created_at: createdAt,
139
150
  display_name: null,
140
151
  body: null,
152
+ anchor: null,
141
153
  readable: false,
142
154
  unreadable_reason: 'the row carried no ciphertext',
143
155
  });
@@ -152,6 +164,9 @@ export async function readComments(
152
164
  created_at: createdAt,
153
165
  display_name: plaintext.display_name,
154
166
  body: plaintext.body,
167
+ // Absent and null both mean freeform. One shape crosses to the agent,
168
+ // so a caller never has to distinguish two ways of saying no mark.
169
+ anchor: plaintext.anchor ?? null,
155
170
  readable: true,
156
171
  unreadable_reason: null,
157
172
  });
@@ -165,6 +180,7 @@ export async function readComments(
165
180
  created_at: createdAt,
166
181
  display_name: null,
167
182
  body: null,
183
+ anchor: null,
168
184
  readable: false,
169
185
  unreadable_reason: `it did not decrypt under this relic's comment key: ${
170
186
  (error as Error).message
package/src/server.ts CHANGED
@@ -393,6 +393,20 @@ export const READ_COMMENTS_TOOL_DEFINITION = {
393
393
  created_at: { type: 'string' },
394
394
  display_name: { type: ['string', 'null'] },
395
395
  body: { type: ['string', 'null'] },
396
+ anchor: {
397
+ type: ['object', 'null'],
398
+ description:
399
+ 'What the comment marks, or null for a freeform one. ' +
400
+ '`{kind:"text", quote}` is a passage the reader selected, so ' +
401
+ 'the quote names the line to act on. `{kind:"pin", x, y}` is ' +
402
+ 'a point on the rendered page in unit coordinates.',
403
+ properties: {
404
+ kind: { type: 'string', enum: ['text', 'pin'] },
405
+ quote: { type: 'string' },
406
+ x: { type: 'number' },
407
+ y: { type: 'number' },
408
+ },
409
+ },
396
410
  readable: { type: 'boolean' },
397
411
  unreadable_reason: { type: ['string', 'null'] },
398
412
  },
@@ -402,6 +416,7 @@ export const READ_COMMENTS_TOOL_DEFINITION = {
402
416
  'created_at',
403
417
  'display_name',
404
418
  'body',
419
+ 'anchor',
405
420
  'readable',
406
421
  'unreadable_reason',
407
422
  ],
@@ -1013,6 +1028,21 @@ async function callComment(
1013
1028
  }
1014
1029
  }
1015
1030
 
1031
+ /**
1032
+ * The one line that says what a comment is attached to.
1033
+ *
1034
+ * A quote is printed verbatim, because the reader selected those exact words
1035
+ * and an agent's next move is usually to find them. A pin has no words, so it
1036
+ * gets its position rounded to whole percent: more precision than that is
1037
+ * noise a person cannot act on.
1038
+ */
1039
+ function markLine(anchor: CommentRecord['anchor']): string {
1040
+ if (anchor === null) return '';
1041
+ if (anchor.kind === 'text') return `on "${anchor.quote}"\n`;
1042
+ const percent = (value: number): number => Math.round(value * 100);
1043
+ return `at ${percent(anchor.x)}% across, ${percent(anchor.y)}% down\n`;
1044
+ }
1045
+
1016
1046
  /**
1017
1047
  * The comments as a person would read them, because a JSON array of rows is
1018
1048
  * not a conversation.
@@ -1037,9 +1067,14 @@ function commentTranscript(result: {
1037
1067
  comment.display_name === null
1038
1068
  ? comment.author
1039
1069
  : `${comment.display_name} (${comment.author})`;
1040
- return comment.readable
1041
- ? `${comment.created_at} ${who}:\n${comment.body}`
1042
- : `${comment.created_at} ${who}:\n[unreadable: ${comment.unreadable_reason}]`;
1070
+ if (!comment.readable) {
1071
+ return `${comment.created_at} ${who}:\n[unreadable: ${comment.unreadable_reason}]`;
1072
+ }
1073
+ // The mark goes above the body, because it is what the body is about. A
1074
+ // transcript that printed the remark and withheld the line it points at
1075
+ // would be the same defect this fixed, one layer up.
1076
+ const mark = markLine(comment.anchor);
1077
+ return `${comment.created_at} ${who}:\n${mark}${comment.body}`;
1043
1078
  });
1044
1079
 
1045
1080
  const header =