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.
- package/.claude-plugin/marketplace.json +2 -2
- package/.claude-plugin/plugin.json +1 -1
- package/dist/relic-mcp.js +31 -3
- package/package.json +1 -1
- package/src/comments.ts +16 -0
- package/src/server.ts +38 -3
|
@@ -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.
|
|
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.
|
|
24
|
+
"version": "0.5.0"
|
|
25
25
|
}
|
|
26
26
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "relic",
|
|
3
|
-
"version": "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.
|
|
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
|
-
|
|
2045
|
-
|
|
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
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
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
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 =
|