relay-companion 0.1.13 → 0.1.15
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 +3 -2
- package/src/attachments.js +161 -0
- package/src/mcp.js +69 -8
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "relay-companion",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.15",
|
|
4
4
|
"description": "Relay companion: connects local coding agents to Relay tasks, approvals, and connector tools.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -15,7 +15,8 @@
|
|
|
15
15
|
"dependencies": {
|
|
16
16
|
"@anthropic-ai/claude-agent-sdk": "^0.3.195",
|
|
17
17
|
"@modelcontextprotocol/sdk": "^1.0.4",
|
|
18
|
-
"electron": "^32.0.0"
|
|
18
|
+
"electron": "^32.0.0",
|
|
19
|
+
"ws": "^8.21.0"
|
|
19
20
|
},
|
|
20
21
|
"files": [
|
|
21
22
|
"bin",
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { promises as fs } from "node:fs";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
|
|
5
|
+
const CONTENT_TYPES = new Map([
|
|
6
|
+
[".csv", "text/csv"],
|
|
7
|
+
[".doc", "application/msword"],
|
|
8
|
+
[".docx", "application/vnd.openxmlformats-officedocument.wordprocessingml.document"],
|
|
9
|
+
[".gif", "image/gif"],
|
|
10
|
+
[".htm", "text/html"],
|
|
11
|
+
[".html", "text/html"],
|
|
12
|
+
[".jpeg", "image/jpeg"],
|
|
13
|
+
[".jpg", "image/jpeg"],
|
|
14
|
+
[".json", "application/json"],
|
|
15
|
+
[".md", "text/markdown"],
|
|
16
|
+
[".pdf", "application/pdf"],
|
|
17
|
+
[".png", "image/png"],
|
|
18
|
+
[".ppt", "application/vnd.ms-powerpoint"],
|
|
19
|
+
[".pptx", "application/vnd.openxmlformats-officedocument.presentationml.presentation"],
|
|
20
|
+
[".rtf", "application/rtf"],
|
|
21
|
+
[".tex", "application/x-tex"],
|
|
22
|
+
[".text", "text/plain"],
|
|
23
|
+
[".txt", "text/plain"],
|
|
24
|
+
[".webp", "image/webp"],
|
|
25
|
+
[".xls", "application/vnd.ms-excel"],
|
|
26
|
+
[".xlsx", "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"],
|
|
27
|
+
[".zip", "application/zip"],
|
|
28
|
+
]);
|
|
29
|
+
|
|
30
|
+
export async function prepareOrdinaryRelayAttachments(args = {}) {
|
|
31
|
+
const passthrough = passthroughAttachments(args);
|
|
32
|
+
const local = await readLocalAttachmentSpecs(localAttachmentSpecs(args));
|
|
33
|
+
return [
|
|
34
|
+
...passthrough,
|
|
35
|
+
...local.map((file, index) => ({
|
|
36
|
+
id: file.id || ordinaryAttachmentId(file, index),
|
|
37
|
+
name: file.name,
|
|
38
|
+
contentType: file.contentType,
|
|
39
|
+
bytes: file.bytes,
|
|
40
|
+
sha256: file.sha256,
|
|
41
|
+
contentBase64: file.body.toString("base64"),
|
|
42
|
+
})),
|
|
43
|
+
];
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export async function prepareTaskMessageAttachments(client, taskId, args = {}) {
|
|
47
|
+
const passthrough = passthroughAttachments(args);
|
|
48
|
+
const local = await readLocalAttachmentSpecs(localAttachmentSpecs(args));
|
|
49
|
+
const uploaded = await uploadLocalTaskFiles(client, taskId, local, args.idempotencyKey || "relay_mcp_task_file");
|
|
50
|
+
return [...passthrough, ...uploaded];
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export async function prepareTaskResult(client, taskId, result = {}, baseIdempotencyKey = "relay_mcp_result_file") {
|
|
54
|
+
return {
|
|
55
|
+
...result,
|
|
56
|
+
attachments: await prepareTaskMessageAttachments(client, taskId, {
|
|
57
|
+
attachments: result.attachments || [],
|
|
58
|
+
files: result.files || [],
|
|
59
|
+
idempotencyKey: baseIdempotencyKey,
|
|
60
|
+
}),
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function localAttachmentSpecs(args = {}) {
|
|
65
|
+
const specs = [];
|
|
66
|
+
for (const item of arrayOf(args.files)) {
|
|
67
|
+
if (typeof item === "string") specs.push({ path: item });
|
|
68
|
+
else if (item && typeof item === "object") specs.push({ ...item, path: item.path || item.filePath });
|
|
69
|
+
}
|
|
70
|
+
for (const item of arrayOf(args.attachments)) {
|
|
71
|
+
if (item && typeof item === "object" && (item.path || item.filePath)) {
|
|
72
|
+
specs.push({ ...item, path: item.path || item.filePath });
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
return specs.filter((spec) => typeof spec.path === "string" && spec.path.trim().length > 0);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function passthroughAttachments(args = {}) {
|
|
79
|
+
return arrayOf(args.attachments).filter((item) => !(item && typeof item === "object" && (item.path || item.filePath)));
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
async function readLocalAttachmentSpecs(specs) {
|
|
83
|
+
const out = [];
|
|
84
|
+
for (const [index, spec] of specs.entries()) {
|
|
85
|
+
const filePath = path.resolve(spec.path);
|
|
86
|
+
const stat = await fs.stat(filePath);
|
|
87
|
+
if (!stat.isFile()) throw new Error(`Attachment path is not a file: ${filePath}`);
|
|
88
|
+
const body = await fs.readFile(filePath);
|
|
89
|
+
const name = String(spec.name || spec.filename || path.basename(filePath)).trim() || `attachment-${index + 1}`;
|
|
90
|
+
const sha256 = createHash("sha256").update(body).digest("hex");
|
|
91
|
+
out.push({
|
|
92
|
+
id: spec.id,
|
|
93
|
+
path: filePath,
|
|
94
|
+
name,
|
|
95
|
+
filename: name,
|
|
96
|
+
contentType: spec.contentType || inferContentType(name),
|
|
97
|
+
bytes: body.length,
|
|
98
|
+
sha256,
|
|
99
|
+
body,
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
return out;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
async function uploadLocalTaskFiles(client, taskId, files, baseIdempotencyKey) {
|
|
106
|
+
const uploaded = [];
|
|
107
|
+
for (const [index, file] of files.entries()) {
|
|
108
|
+
const result = await client.createFileUpload({
|
|
109
|
+
taskId,
|
|
110
|
+
filename: file.name,
|
|
111
|
+
contentType: file.contentType,
|
|
112
|
+
sizeBytes: file.bytes,
|
|
113
|
+
sha256: file.sha256,
|
|
114
|
+
metadataScanStatus: "pending",
|
|
115
|
+
idempotencyKey: fileUploadIdempotencyKey(baseIdempotencyKey, file, index),
|
|
116
|
+
});
|
|
117
|
+
await uploadBytes(result.upload, file);
|
|
118
|
+
uploaded.push(result.attachment);
|
|
119
|
+
}
|
|
120
|
+
return uploaded;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
async function uploadBytes(upload, file) {
|
|
124
|
+
if (!upload?.url) throw new Error(`Relay did not return an upload URL for ${file.name}.`);
|
|
125
|
+
const method = upload.method || "PUT";
|
|
126
|
+
let res;
|
|
127
|
+
if (method === "POST") {
|
|
128
|
+
const form = new FormData();
|
|
129
|
+
for (const [key, value] of Object.entries(upload.fields || {})) form.append(key, value);
|
|
130
|
+
form.append("file", new Blob([file.body], { type: file.contentType }), file.name);
|
|
131
|
+
res = await fetch(upload.url, { method: "POST", body: form });
|
|
132
|
+
} else {
|
|
133
|
+
res = await fetch(upload.url, {
|
|
134
|
+
method: "PUT",
|
|
135
|
+
headers: { "Content-Type": file.contentType },
|
|
136
|
+
body: file.body,
|
|
137
|
+
});
|
|
138
|
+
}
|
|
139
|
+
if (!res.ok) {
|
|
140
|
+
const body = await res.text().catch(() => "");
|
|
141
|
+
const detail = body ? `: ${body.slice(0, 500)}` : "";
|
|
142
|
+
throw new Error(`Relay upload failed for ${file.name} with HTTP ${res.status}${detail}`);
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
function fileUploadIdempotencyKey(base, file, index) {
|
|
147
|
+
const safeBase = String(base || "relay_mcp_file").replace(/[^\w.-]+/g, "_").slice(0, 80);
|
|
148
|
+
return `${safeBase}_file_${index}_${file.sha256.slice(0, 16)}`;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
function ordinaryAttachmentId(file, index) {
|
|
152
|
+
return `att_${file.sha256.slice(0, 16)}_${index}`;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
function inferContentType(filename) {
|
|
156
|
+
return CONTENT_TYPES.get(path.extname(filename).toLowerCase()) || "application/octet-stream";
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
function arrayOf(value) {
|
|
160
|
+
return Array.isArray(value) ? value : [];
|
|
161
|
+
}
|
package/src/mcp.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
|
2
2
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
3
3
|
import { CallToolRequestSchema, ListToolsRequestSchema } from "@modelcontextprotocol/sdk/types.js";
|
|
4
|
+
import { prepareOrdinaryRelayAttachments, prepareTaskMessageAttachments, prepareTaskResult } from "./attachments.js";
|
|
4
5
|
import { RelayClient } from "./client.js";
|
|
5
6
|
|
|
6
7
|
export const TOOLS = [
|
|
@@ -32,12 +33,15 @@ export const TOOLS = [
|
|
|
32
33
|
attachments: {
|
|
33
34
|
type: "array",
|
|
34
35
|
description:
|
|
35
|
-
"Optional files to send with the relay.
|
|
36
|
+
"Optional files to send with the relay. Prefer files: [absolutePath] for local files. Low-level callers may pass fully prepared attachments with id, name, contentType, bytes, sha256, and contentBase64.",
|
|
36
37
|
items: {
|
|
37
38
|
type: "object",
|
|
38
39
|
properties: {
|
|
40
|
+
path: { type: "string", description: "Absolute local file path. Relay will read, hash, and attach the file." },
|
|
41
|
+
filePath: { type: "string", description: "Alias for path." },
|
|
39
42
|
id: { type: "string" },
|
|
40
43
|
name: { type: "string" },
|
|
44
|
+
filename: { type: "string", description: "Optional display filename for path attachments." },
|
|
41
45
|
contentType: { type: "string" },
|
|
42
46
|
bytes: { type: "number" },
|
|
43
47
|
sha256: { type: "string" },
|
|
@@ -45,6 +49,11 @@ export const TOOLS = [
|
|
|
45
49
|
},
|
|
46
50
|
},
|
|
47
51
|
},
|
|
52
|
+
files: {
|
|
53
|
+
type: "array",
|
|
54
|
+
description: "Absolute local file paths to attach. Relay reads, hashes, and includes the bytes safely.",
|
|
55
|
+
items: { type: "string" },
|
|
56
|
+
},
|
|
48
57
|
idempotencyKey: { type: "string" },
|
|
49
58
|
},
|
|
50
59
|
required: ["recipient", "title", "bodyMarkdown", "idempotencyKey"],
|
|
@@ -151,7 +160,25 @@ export const TOOLS = [
|
|
|
151
160
|
recipientParticipantIds: { type: "array", items: { type: "string" } },
|
|
152
161
|
bodyMarkdown: { type: "string" },
|
|
153
162
|
shareSummary: { type: "string" },
|
|
154
|
-
attachments: {
|
|
163
|
+
attachments: {
|
|
164
|
+
type: "array",
|
|
165
|
+
description: "Prefer files: [absolutePath] for local files. Existing Relay file attachment objects still work.",
|
|
166
|
+
items: {
|
|
167
|
+
type: "object",
|
|
168
|
+
properties: {
|
|
169
|
+
path: { type: "string", description: "Absolute local file path. Relay will upload and attach it." },
|
|
170
|
+
filePath: { type: "string", description: "Alias for path." },
|
|
171
|
+
filename: { type: "string" },
|
|
172
|
+
name: { type: "string" },
|
|
173
|
+
contentType: { type: "string" },
|
|
174
|
+
},
|
|
175
|
+
},
|
|
176
|
+
},
|
|
177
|
+
files: {
|
|
178
|
+
type: "array",
|
|
179
|
+
description: "Absolute local file paths to upload and attach to this task message.",
|
|
180
|
+
items: { type: "string" },
|
|
181
|
+
},
|
|
155
182
|
provenance: { type: "array", items: { type: "object" } },
|
|
156
183
|
senderAgentSessionId: { type: "string" },
|
|
157
184
|
idempotencyKey: { type: "string" },
|
|
@@ -171,7 +198,25 @@ export const TOOLS = [
|
|
|
171
198
|
recipientUserIds: { type: "array", items: { type: "string" } },
|
|
172
199
|
bodyMarkdown: { type: "string" },
|
|
173
200
|
shareSummary: { type: "string" },
|
|
174
|
-
attachments: {
|
|
201
|
+
attachments: {
|
|
202
|
+
type: "array",
|
|
203
|
+
description: "Prefer files: [absolutePath] for local files. Existing Relay file attachment objects still work.",
|
|
204
|
+
items: {
|
|
205
|
+
type: "object",
|
|
206
|
+
properties: {
|
|
207
|
+
path: { type: "string", description: "Absolute local file path. Relay will upload and attach it." },
|
|
208
|
+
filePath: { type: "string", description: "Alias for path." },
|
|
209
|
+
filename: { type: "string" },
|
|
210
|
+
name: { type: "string" },
|
|
211
|
+
contentType: { type: "string" },
|
|
212
|
+
},
|
|
213
|
+
},
|
|
214
|
+
},
|
|
215
|
+
files: {
|
|
216
|
+
type: "array",
|
|
217
|
+
description: "Absolute local file paths to upload and attach to this task message.",
|
|
218
|
+
items: { type: "string" },
|
|
219
|
+
},
|
|
175
220
|
provenance: { type: "array", items: { type: "object" } },
|
|
176
221
|
senderAgentSessionId: { type: "string" },
|
|
177
222
|
humanResponse: {
|
|
@@ -256,6 +301,12 @@ export const TOOLS = [
|
|
|
256
301
|
status: { type: "string", enum: ["success", "partial_success", "failed", "cancelled"] },
|
|
257
302
|
title: { type: "string" },
|
|
258
303
|
bodyMarkdown: { type: "string" },
|
|
304
|
+
files: {
|
|
305
|
+
type: "array",
|
|
306
|
+
description: "Absolute local file paths to upload and attach to the creator-scoped result.",
|
|
307
|
+
items: { type: "string" },
|
|
308
|
+
},
|
|
309
|
+
attachments: { type: "array", description: "Existing Relay file attachment objects or path attachments.", items: { type: "object" } },
|
|
259
310
|
sourceApprovalIds: { type: "array", items: { type: "string" } },
|
|
260
311
|
sourceResultIds: { type: "array", items: { type: "string" } },
|
|
261
312
|
},
|
|
@@ -270,6 +321,12 @@ export const TOOLS = [
|
|
|
270
321
|
status: { type: "string", enum: ["success", "partial_success", "failed", "cancelled"] },
|
|
271
322
|
title: { type: "string" },
|
|
272
323
|
bodyMarkdown: { type: "string" },
|
|
324
|
+
files: {
|
|
325
|
+
type: "array",
|
|
326
|
+
description: "Absolute local file paths to upload and attach to this participant-scoped result.",
|
|
327
|
+
items: { type: "string" },
|
|
328
|
+
},
|
|
329
|
+
attachments: { type: "array", description: "Existing Relay file attachment objects or path attachments.", items: { type: "object" } },
|
|
273
330
|
sourceApprovalIds: { type: "array", items: { type: "string" } },
|
|
274
331
|
sourceResultIds: { type: "array", items: { type: "string" } },
|
|
275
332
|
},
|
|
@@ -379,7 +436,7 @@ export async function handleCall(client, name, args) {
|
|
|
379
436
|
userInstructions: args.userInstructions || "",
|
|
380
437
|
source: { host: "relay-mcp" },
|
|
381
438
|
targetSurfaces: args.targetSurfaces || [],
|
|
382
|
-
attachments: args
|
|
439
|
+
attachments: await prepareOrdinaryRelayAttachments(args),
|
|
383
440
|
idempotencyKey: args.idempotencyKey,
|
|
384
441
|
})),
|
|
385
442
|
);
|
|
@@ -423,7 +480,7 @@ export async function handleCall(client, name, args) {
|
|
|
423
480
|
recipientParticipantIds: args.recipientParticipantIds || [],
|
|
424
481
|
shareSummary: args.shareSummary,
|
|
425
482
|
destinationKind: "agent",
|
|
426
|
-
attachments: args.
|
|
483
|
+
attachments: await prepareTaskMessageAttachments(client, args.taskId, args),
|
|
427
484
|
provenance: args.provenance || [],
|
|
428
485
|
senderAgentSessionId: args.senderAgentSessionId,
|
|
429
486
|
idempotencyKey: args.idempotencyKey,
|
|
@@ -438,7 +495,7 @@ export async function handleCall(client, name, args) {
|
|
|
438
495
|
recipientUserIds: args.recipientUserIds || [],
|
|
439
496
|
shareSummary: args.shareSummary,
|
|
440
497
|
destinationKind: "human",
|
|
441
|
-
attachments: args.
|
|
498
|
+
attachments: await prepareTaskMessageAttachments(client, args.taskId, args),
|
|
442
499
|
provenance: args.provenance || [],
|
|
443
500
|
humanResponse: args.humanResponse,
|
|
444
501
|
senderAgentSessionId: args.senderAgentSessionId,
|
|
@@ -477,8 +534,12 @@ export async function handleCall(client, name, args) {
|
|
|
477
534
|
case "relay_end_task":
|
|
478
535
|
return text(
|
|
479
536
|
await client.completeTask(args.taskId, {
|
|
480
|
-
creatorResult: args.creatorResult,
|
|
481
|
-
participantResults:
|
|
537
|
+
creatorResult: await prepareTaskResult(client, args.taskId, args.creatorResult, `${args.idempotencyKey}_creator`),
|
|
538
|
+
participantResults: await Promise.all(
|
|
539
|
+
(args.participantResults || []).map((result, index) =>
|
|
540
|
+
prepareTaskResult(client, args.taskId, result, `${args.idempotencyKey}_participant_${index}`),
|
|
541
|
+
),
|
|
542
|
+
),
|
|
482
543
|
senderAgentSessionId: args.senderAgentSessionId,
|
|
483
544
|
expectedVersion: args.expectedVersion,
|
|
484
545
|
idempotencyKey: args.idempotencyKey,
|