relay-companion 0.1.231 → 0.1.232
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/src/mcp.js +94 -24
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "relay-companion",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.232",
|
|
4
4
|
"description": "Companion CLI for Relay (sendrelays.com): pairs this machine with your Relay account so coding agents like Claude Code and Codex can send and receive Relay messages.",
|
|
5
5
|
"homepage": "https://sendrelays.com",
|
|
6
6
|
"repository": {
|
package/src/mcp.js
CHANGED
|
@@ -5,7 +5,7 @@ import { prepareOrdinaryRelayAttachments } from "./attachments.js";
|
|
|
5
5
|
import { workspacePassportFromDeclaration } from "./repo-identity.js";
|
|
6
6
|
import { fragileLinkWarning } from "./links.js";
|
|
7
7
|
import { createRequire } from "node:module";
|
|
8
|
-
import { randomUUID } from "node:crypto";
|
|
8
|
+
import { createHash, randomUUID } from "node:crypto";
|
|
9
9
|
import { RelayClient } from "./client.js";
|
|
10
10
|
import { apiUrl, readConfig } from "./config.js";
|
|
11
11
|
import { productFeatures } from "./product-features.js";
|
|
@@ -18,11 +18,14 @@ import {
|
|
|
18
18
|
normalizeCompanionMode,
|
|
19
19
|
} from "./config.js";
|
|
20
20
|
|
|
21
|
+
export const FOR_HUMAN_SOFT_WORD_LIMIT = 80;
|
|
22
|
+
export const FOR_HUMAN_DEFAULT_SENTENCE_LIMIT = 4;
|
|
23
|
+
|
|
21
24
|
export const RELAY_MCP_INSTRUCTIONS = [
|
|
22
25
|
"Relay is general person-to-person messaging with optional agent context and agent work, not an engineering-only workflow.",
|
|
23
26
|
"A visible chat is one chronological room for an exact person or saved group. threadId is only opaque AI retrieval metadata: never invent or expose a thread/topic name or separate visible thread UI.",
|
|
24
27
|
"Reading message bodies through relay_inbox_list, relay_thread_fetch, or relay_chat_fetch marks the returned inbound Relays read and sends their read receipts. Summary-only relay_chats_list does not.",
|
|
25
|
-
|
|
28
|
+
`relay_send has a 3-6 word title, forHuman, and optional forAgent. forHuman normally uses no more than ${FOR_HUMAN_DEFAULT_SENTENCE_LIMIT} normally sized sentences and ${FOR_HUMAN_SOFT_WORD_LIMIT} words. A longer draft is stopped for a mandatory second review and may proceed only when shortening would lose the user's intended message. Preserve the sender's vocabulary, rhythm, directness, formality, warmth, and sign-off while making it digestible; history teaches voice and relationship register, not length. Put technical detail in forAgent, which may be as long and detailed as necessary, and never duplicate the two documents.`,
|
|
26
29
|
"Choose relay_send kind from the requested outcome: human correspondence is message; external work to be carried out by the recipient's agent is task (a visible Request), even when the sentence addresses 'you' or the operation is small.",
|
|
27
30
|
"Provider AI-session tools control native Claude/Codex sessions; they do not send Relay correspondence. A Relay-owned Request Run attaches its provider's terminal answer automatically; do not send a separate completion Relay.",
|
|
28
31
|
].join(" ");
|
|
@@ -31,7 +34,7 @@ const REQUESTS_DISABLED_INSTRUCTIONS = [
|
|
|
31
34
|
"Relay is general person-to-person messaging with optional agent context, not an engineering-only workflow.",
|
|
32
35
|
"A visible chat is one chronological room for an exact person or saved group. threadId is only opaque AI retrieval metadata: never invent or expose a thread/topic name or separate visible thread UI.",
|
|
33
36
|
"relay_send sends ordinary correspondence with kind='message'. Requests are unavailable in this Relay release. Never attempt kind='task' or promise that the recipient can Start agent work.",
|
|
34
|
-
|
|
37
|
+
`relay_send always has a 3-6 word title plus forHuman, and may have forAgent. Write complete agent context first whenever useful, then derive the concise human message. forHuman normally uses no more than ${FOR_HUMAN_DEFAULT_SENTENCE_LIMIT} normally sized sentences and ${FOR_HUMAN_SOFT_WORD_LIMIT} words; a longer draft must be reviewed a second time and explicitly confirmed only when shortening would lose the user's intended message. Preserve the sender's voice while making it digestible. Technical detail belongs in forAgent, not forHuman.`,
|
|
35
38
|
].join(" ");
|
|
36
39
|
|
|
37
40
|
export const TOOLS = [
|
|
@@ -92,7 +95,7 @@ export const TOOLS = [
|
|
|
92
95
|
{
|
|
93
96
|
name: "relay_send",
|
|
94
97
|
description:
|
|
95
|
-
|
|
98
|
+
`Send ordinary Relay correspondence or a direct Request. CLASSIFY THE OUTCOME, NOT THE SENTENCE'S ADDRESSEE: kind='message' is telling, asking, discussing, sharing, or seeking the PERSON'S opinion or decision; kind='task' asks the recipient's AGENT to perform external work. A technical subject or non-empty forAgent does not itself make a Request. Relay-owned Request Runs attach their provider's final answer automatically, so do not call relay_send merely to report completion. HUMAN COPY IS SHORT BY DEFAULT: forHuman normally uses no more than ${FOR_HUMAN_DEFAULT_SENTENCE_LIMIT} normally sized sentences and ${FOR_HUMAN_SOFT_WORD_LIMIT} words. Relay stops a longer first attempt for review. Shorten repetition and move mechanisms, evidence, paths, logs, chronology, and implementation detail into forAgent; confirm the exact longer draft only when shortening would lose the user's intended message. When agent context is useful, compose the complete forAgent document first, then derive forHuman as the concise message. Preserve the sender's vocabulary, rhythm, directness, formality, warmth, and sign-off; message history teaches voice and relationship register, not a default word count. forAgent may be as detailed as necessary—under-sending to the recipient's agent is worse than over-sending—and must not duplicate forHuman. Before continuing an exchange, inspect BOTH relay_sent_list and relay_inbox_list; your earlier sends never appear in the inbox. Set inReplyToRelayId to the latest related Relay from either side. A one-way series of updates can still be related: this is an unnamed reply chain only so an AI can fetch related Relays together, never a visible topic. For a Granular digital employee, use relay_contacts_search and the exact matching workspace-labelled contactId.`,
|
|
96
99
|
inputSchema: {
|
|
97
100
|
type: "object",
|
|
98
101
|
properties: {
|
|
@@ -111,12 +114,6 @@ export const TOOLS = [
|
|
|
111
114
|
"A contact-group id (grp_...) from relay_groups_list. Sends one Relay into the group chat for every member.",
|
|
112
115
|
},
|
|
113
116
|
},
|
|
114
|
-
anyOf: [
|
|
115
|
-
{ required: ["contactId"] },
|
|
116
|
-
{ required: ["relayUserId"] },
|
|
117
|
-
{ required: ["email"] },
|
|
118
|
-
{ required: ["groupId"] },
|
|
119
|
-
],
|
|
120
117
|
},
|
|
121
118
|
kind: {
|
|
122
119
|
type: "string",
|
|
@@ -132,9 +129,14 @@ export const TOOLS = [
|
|
|
132
129
|
forHuman: {
|
|
133
130
|
type: "string",
|
|
134
131
|
description:
|
|
135
|
-
|
|
132
|
+
`The sender's message to the person: what they should know, answer, feel, discuss, or decide, ghostwritten in the sender's voice. DEFAULT TO NO MORE THAN ${FOR_HUMAN_DEFAULT_SENTENCE_LIMIT} NORMALLY SIZED SENTENCES AND ${FOR_HUMAN_SOFT_WORD_LIMIT} WORDS. A longer draft is stopped for review; shorten it unless extra length is necessary to preserve the user's intended message. Preserve recipient-specific vocabulary, rhythm, directness, formality, warmth, emphasis, and sign-off while making it digestible. History from relay_sent_list and relay_chat_fetch teaches voice and relationship register, not target length. Lots of detail never justifies a longer human message. Keep only the high-level consequence, state, ask, or decision here; put mechanisms, evidence, code, paths, logs, reproduction steps, constraints, chronology, and implementation detail in forAgent. No headings, lists, tables, code blocks, title repetition, drafting narration, or duplication of forAgent.`,
|
|
133
|
+
},
|
|
134
|
+
longForHumanConfirmed: {
|
|
135
|
+
type: "boolean",
|
|
136
|
+
description:
|
|
137
|
+
`Set true only when Relay has already rejected this exact over-${FOR_HUMAN_SOFT_WORD_LIMIT}-word draft, you reviewed it again, and you genuinely believe shortening would lose what the user is trying to say to this recipient. Never set it preemptively or merely because more detail is available.`,
|
|
136
138
|
},
|
|
137
|
-
forAgent: { type: "string", description: "The recipient agent's complete document
|
|
139
|
+
forAgent: { type: "string", description: "The recipient agent's complete document, self-contained and containing everything useful that the person need not read. Draft it first whenever agent context is useful. It may be as long and detailed as necessary; under-sending here is worse than over-sending. Preserve conclusions, constraints, rejected options, failures, preferences, questions, next steps, sources, mechanisms, evidence, code, paths, logs, reproduction steps, chronology, data, and verification guidance. Use Markdown when useful and do not repeat forHuman. Leave empty only when the recipient's agent needs nothing beyond the human message; that makes the send plain text." },
|
|
138
140
|
targetSurfaces: {
|
|
139
141
|
type: "array",
|
|
140
142
|
description:
|
|
@@ -178,12 +180,6 @@ export const TOOLS = [
|
|
|
178
180
|
idempotencyKey: { type: "string" },
|
|
179
181
|
},
|
|
180
182
|
required: ["recipient", "kind", "title", "forHuman", "idempotencyKey"],
|
|
181
|
-
allOf: [
|
|
182
|
-
{
|
|
183
|
-
if: { properties: { kind: { const: "task" } } },
|
|
184
|
-
then: { properties: { recipient: { not: { required: ["groupId"] } } } },
|
|
185
|
-
},
|
|
186
|
-
],
|
|
187
183
|
},
|
|
188
184
|
},
|
|
189
185
|
{
|
|
@@ -337,7 +333,7 @@ export const TOOLS = [
|
|
|
337
333
|
{
|
|
338
334
|
name: "relay_chat_reply",
|
|
339
335
|
description:
|
|
340
|
-
|
|
336
|
+
`Reply inside an existing Relay chat; this always sends kind 'message' and cannot create a direct Request. Read it with relay_chat_fetch first. forHuman defaults to no more than ${FOR_HUMAN_DEFAULT_SENTENCE_LIMIT} normally sized sentences and ${FOR_HUMAN_SOFT_WORD_LIMIT} words; a longer first attempt is stopped for review and may be confirmed only when shortening would lose the user's intended message. Preserve the sender's voice while making it digestible. Never name a recipient: Relay addresses the reply from the message it answers and fans a group reply out to the whole roster. Use chatId or threadId. Use relay_send to start a chat, create a Request, or include forAgent context.`,
|
|
341
337
|
inputSchema: {
|
|
342
338
|
type: "object",
|
|
343
339
|
properties: {
|
|
@@ -346,7 +342,12 @@ export const TOOLS = [
|
|
|
346
342
|
type: "string",
|
|
347
343
|
description: "Opaque internal reply-chain key from a Relay. Resolves to its enclosing person/group chat. Pass this or chatId.",
|
|
348
344
|
},
|
|
349
|
-
forHuman: { type: "string", description:
|
|
345
|
+
forHuman: { type: "string", description: `A message ghostwritten as the SENDER speaking. Preserve their vocabulary, rhythm, directness, formality, warmth, and sign-off while making it digestible. Default to no more than ${FOR_HUMAN_DEFAULT_SENTENCE_LIMIT} normally sized sentences and ${FOR_HUMAN_SOFT_WORD_LIMIT} words. History teaches voice, not target length. No headings, lists, or wall of text. Put paths, commands, and verification detail in relay_send.forAgent instead.` },
|
|
346
|
+
longForHumanConfirmed: {
|
|
347
|
+
type: "boolean",
|
|
348
|
+
description:
|
|
349
|
+
`Set true only when Relay has already rejected this exact over-${FOR_HUMAN_SOFT_WORD_LIMIT}-word reply and, after reviewing it again, you genuinely believe shortening would lose the user's intended message.`,
|
|
350
|
+
},
|
|
350
351
|
title: {
|
|
351
352
|
type: "string",
|
|
352
353
|
description:
|
|
@@ -360,7 +361,6 @@ export const TOOLS = [
|
|
|
360
361
|
idempotencyKey: { type: "string", description: "A unique key of at least 8 characters for this reply." },
|
|
361
362
|
},
|
|
362
363
|
required: ["forHuman", "idempotencyKey"],
|
|
363
|
-
anyOf: [{ required: ["chatId"] }, { required: ["threadId"] }],
|
|
364
364
|
},
|
|
365
365
|
},
|
|
366
366
|
{
|
|
@@ -601,12 +601,11 @@ function toolsForFeatures(tools, { requests = true } = {}) {
|
|
|
601
601
|
if (tool.name !== "relay_send") return tool;
|
|
602
602
|
const send = structuredClone(tool);
|
|
603
603
|
send.description =
|
|
604
|
-
|
|
604
|
+
`Send ordinary person-to-person Relay correspondence. Requests are unavailable in this release, so kind must be 'message'. Use forHuman for the concise message the person needs and forAgent for complete agent context when useful. forHuman normally uses no more than ${FOR_HUMAN_DEFAULT_SENTENCE_LIMIT} normally sized sentences and ${FOR_HUMAN_SOFT_WORD_LIMIT} words; a longer draft is stopped for a mandatory second review and may proceed only when shortening would lose the user's intended message. Preserve the sender's voice while making the message digestible. Check relay_sent_list and relay_inbox_list before continuing an existing exchange, and pass the newest related Relay as inReplyToRelayId.`;
|
|
605
605
|
send.inputSchema.properties.kind.enum = ["message"];
|
|
606
606
|
send.inputSchema.properties.kind.description =
|
|
607
607
|
"Required. Must be 'message' for ordinary correspondence. Requests (kind='task') are unavailable in this release.";
|
|
608
608
|
delete send.inputSchema.properties.targetSurfaces;
|
|
609
|
-
delete send.inputSchema.allOf;
|
|
610
609
|
return send;
|
|
611
610
|
});
|
|
612
611
|
}
|
|
@@ -752,6 +751,74 @@ function relayTitleWordCount(value) {
|
|
|
752
751
|
return String(value || "").trim().split(/\s+/u).filter(Boolean).length;
|
|
753
752
|
}
|
|
754
753
|
|
|
754
|
+
const pendingLongForHumanReviews = new Map();
|
|
755
|
+
const MAX_PENDING_LONG_FOR_HUMAN_REVIEWS = 256;
|
|
756
|
+
|
|
757
|
+
function relayHumanWordCount(value) {
|
|
758
|
+
return String(value || "").trim().split(/\s+/u).filter(Boolean).length;
|
|
759
|
+
}
|
|
760
|
+
|
|
761
|
+
function longForHumanReviewKey(toolName, args) {
|
|
762
|
+
return `${toolName}:${String(args?.idempotencyKey || "").trim()}`;
|
|
763
|
+
}
|
|
764
|
+
|
|
765
|
+
function longForHumanFingerprint(toolName, args) {
|
|
766
|
+
return createHash("sha256")
|
|
767
|
+
.update(toolName)
|
|
768
|
+
.update("\0")
|
|
769
|
+
.update(String(args?.idempotencyKey || ""))
|
|
770
|
+
.update("\0")
|
|
771
|
+
.update(String(args?.forHuman || ""))
|
|
772
|
+
.digest("hex");
|
|
773
|
+
}
|
|
774
|
+
|
|
775
|
+
function rememberLongForHumanReview(key, fingerprint) {
|
|
776
|
+
pendingLongForHumanReviews.delete(key);
|
|
777
|
+
pendingLongForHumanReviews.set(key, fingerprint);
|
|
778
|
+
while (pendingLongForHumanReviews.size > MAX_PENDING_LONG_FOR_HUMAN_REVIEWS) {
|
|
779
|
+
pendingLongForHumanReviews.delete(pendingLongForHumanReviews.keys().next().value);
|
|
780
|
+
}
|
|
781
|
+
}
|
|
782
|
+
|
|
783
|
+
/**
|
|
784
|
+
* Make an overlong agent-written human message a deliberate second-pass choice,
|
|
785
|
+
* not a soft adjective the model can silently reinterpret. The first attempt is
|
|
786
|
+
* rejected before any fetch, attachment read, or API call. A confirmation is
|
|
787
|
+
* accepted only for that exact draft after Relay has already returned the review
|
|
788
|
+
* instruction in this MCP process; changing the draft starts a fresh review.
|
|
789
|
+
*/
|
|
790
|
+
function requireLongForHumanReview(toolName, args) {
|
|
791
|
+
const wordCount = relayHumanWordCount(args?.forHuman);
|
|
792
|
+
const key = longForHumanReviewKey(toolName, args);
|
|
793
|
+
if (wordCount <= FOR_HUMAN_SOFT_WORD_LIMIT) {
|
|
794
|
+
pendingLongForHumanReviews.delete(key);
|
|
795
|
+
return;
|
|
796
|
+
}
|
|
797
|
+
|
|
798
|
+
const fingerprint = longForHumanFingerprint(toolName, args);
|
|
799
|
+
const reviewedExactDraft = pendingLongForHumanReviews.get(key) === fingerprint;
|
|
800
|
+
if (args?.longForHumanConfirmed === true && reviewedExactDraft) {
|
|
801
|
+
pendingLongForHumanReviews.delete(key);
|
|
802
|
+
return;
|
|
803
|
+
}
|
|
804
|
+
|
|
805
|
+
rememberLongForHumanReview(key, fingerprint);
|
|
806
|
+
throw new Error(
|
|
807
|
+
`forHuman is ${wordCount} words; Relay's normal human-message limit is ${FOR_HUMAN_SOFT_WORD_LIMIT} words `
|
|
808
|
+
+ `(about ${FOR_HUMAN_DEFAULT_SENTENCE_LIMIT} normally sized sentences). Nothing was sent. Review this exact draft again. `
|
|
809
|
+
+ "Shorten it in the sender's own voice by removing repetition and moving mechanisms, evidence, paths, logs, chronology, and implementation detail into forAgent (use relay_send for a two-document Relay). "
|
|
810
|
+
+ "If, after that review, you genuinely believe the extra length is necessary to preserve what the user is trying to say to this recipient, retry this exact draft with longForHumanConfirmed: true and the same idempotencyKey. Do not confirm merely because more detail is available.",
|
|
811
|
+
);
|
|
812
|
+
}
|
|
813
|
+
|
|
814
|
+
function requireRelaySendRecipient(recipient) {
|
|
815
|
+
const supplied = [recipient?.contactId, recipient?.relayUserId, recipient?.email, recipient?.groupId]
|
|
816
|
+
.some((value) => String(value || "").trim());
|
|
817
|
+
if (!supplied) {
|
|
818
|
+
throw new Error("recipient must include contactId, relayUserId, email, or groupId");
|
|
819
|
+
}
|
|
820
|
+
}
|
|
821
|
+
|
|
755
822
|
export async function handleCall(client, name, args, { mode = DEFAULT_COMPANION_MODE, features = { requests: true } } = {}) {
|
|
756
823
|
const normalizedMode = normalizeCompanionMode(mode);
|
|
757
824
|
if (
|
|
@@ -819,6 +886,7 @@ export async function handleCall(client, name, args, { mode = DEFAULT_COMPANION_
|
|
|
819
886
|
);
|
|
820
887
|
}
|
|
821
888
|
case "relay_send": {
|
|
889
|
+
requireRelaySendRecipient(args.recipient);
|
|
822
890
|
if (!args.kind) {
|
|
823
891
|
throw new Error(
|
|
824
892
|
"kind is required: choose 'message' for correspondence with the person or 'task' for a direct Request to their agent",
|
|
@@ -837,9 +905,10 @@ export async function handleCall(client, name, args, { mode = DEFAULT_COMPANION_
|
|
|
837
905
|
if (titleWordCount < 3 || titleWordCount > 6) {
|
|
838
906
|
throw new Error(
|
|
839
907
|
`title must be a 3-6 word gist; received ${titleWordCount} words. `
|
|
840
|
-
+ "Move evidence, chronology, qualifications, and additional findings into forHuman or
|
|
908
|
+
+ "Move implementation evidence, chronology, technical qualifications, and additional findings into forAgent. Keep forHuman to the consequence, ask, opinion, or decision the recipient needs, then retry with the same idempotencyKey.",
|
|
841
909
|
);
|
|
842
910
|
}
|
|
911
|
+
requireLongForHumanReview("relay_send", args);
|
|
843
912
|
return text(
|
|
844
913
|
relaySendResultForAgent(await client.sendRelay({
|
|
845
914
|
recipient: args.recipient,
|
|
@@ -960,6 +1029,7 @@ export async function handleCall(client, name, args, { mode = DEFAULT_COMPANION_
|
|
|
960
1029
|
case "relay_chat_fetch":
|
|
961
1030
|
return text(withoutThreadTitles(await markFetchedInboundRead(client, await fetchChatForAgent(client, args))));
|
|
962
1031
|
case "relay_chat_reply": {
|
|
1032
|
+
requireLongForHumanReview("relay_chat_reply", args);
|
|
963
1033
|
const chat = await markFetchedInboundRead(client, await fetchChatForAgent(client, args));
|
|
964
1034
|
const replyTo = chat?.replyToRelayId;
|
|
965
1035
|
if (!replyTo) {
|