relay-companion 0.1.3 → 0.1.4
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/bin/relay.js +20 -0
- package/overlay/inbox.html +18 -4
- package/package.json +1 -1
- package/src/client.js +4 -0
- package/src/mcp.js +16 -1
- package/src/notifications.js +7 -2
- package/src/setup-open.js +83 -0
package/bin/relay.js
CHANGED
|
@@ -13,6 +13,7 @@ import { runMcpServer } from "../src/mcp.js";
|
|
|
13
13
|
import { runSetupInstall, runUninstall } from "../src/install.js";
|
|
14
14
|
import { openRelay, openTask } from "../src/materializer.js";
|
|
15
15
|
import { resetCompanionStateForAccount } from "../src/notifications.js";
|
|
16
|
+
import { finishSetupOpenRelay, normalizeSetupHost, setupOpenRelayToken, setupOpenStatus } from "../src/setup-open.js";
|
|
16
17
|
|
|
17
18
|
function parseFlags(argv) {
|
|
18
19
|
const flags = {};
|
|
@@ -83,6 +84,24 @@ async function cmdSetup(flags) {
|
|
|
83
84
|
await cmdPair(flags, { promptForDefaults: Boolean(flags.interactive) });
|
|
84
85
|
console.log("");
|
|
85
86
|
applyInstall();
|
|
87
|
+
const token = setupOpenRelayToken(flags);
|
|
88
|
+
if (token) {
|
|
89
|
+
console.log("");
|
|
90
|
+
const host = normalizeSetupHost(flags.host || flags.in);
|
|
91
|
+
const result = await finishSetupOpenRelay({
|
|
92
|
+
token,
|
|
93
|
+
host,
|
|
94
|
+
log: (m) => process.stderr.write(`[relay] ${m}\n`),
|
|
95
|
+
});
|
|
96
|
+
const openStatus = setupOpenStatus(result.opened, openUrl);
|
|
97
|
+
if (openStatus.opened) {
|
|
98
|
+
console.log(`Opened relay ${result.relayId} in ${host === "codex" ? "Codex" : "Claude Code"}.`);
|
|
99
|
+
} else if (openStatus.url) {
|
|
100
|
+
console.log(`Relay ${result.relayId} is ready. Open this URL: ${openStatus.url}`);
|
|
101
|
+
} else {
|
|
102
|
+
console.log(`Relay ${result.relayId} is staged in the Relay pill.`);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
86
105
|
}
|
|
87
106
|
|
|
88
107
|
/** Install the tools + daemon on a device that is already paired. */
|
|
@@ -251,6 +270,7 @@ async function main() {
|
|
|
251
270
|
"",
|
|
252
271
|
"Usage:",
|
|
253
272
|
" relay setup [--code CODE] [--name NAME] Pair this machine and add Relay to Claude Code + Codex",
|
|
273
|
+
" relay setup --code CODE --open-relay TOKEN --host codex|claude",
|
|
254
274
|
" relay setup --interactive Prompt for API URL and device name during setup",
|
|
255
275
|
" relay install Add Relay to your agents (device already paired)",
|
|
256
276
|
" relay uninstall Remove Relay from your agents and stop the daemon",
|
package/overlay/inbox.html
CHANGED
|
@@ -14,6 +14,7 @@
|
|
|
14
14
|
--ink:#1f1a17; --ink-2:#2c2824;
|
|
15
15
|
--muted:#7c736b; --muted-2:#9a928a; --muted-3:#b7b1a6;
|
|
16
16
|
--accent:#305566; --accent-soft:rgba(48,85,102,.10);
|
|
17
|
+
--brand-anthropic:#D9704E;
|
|
17
18
|
--hair:rgba(31,26,23,.07); /* between relay/task items */
|
|
18
19
|
--hair-2:rgba(31,26,23,.06); /* between contact rows */
|
|
19
20
|
--serif:'Newsreader',Georgia,serif;
|
|
@@ -62,8 +63,10 @@
|
|
|
62
63
|
padding:0 18px; cursor:pointer;
|
|
63
64
|
}
|
|
64
65
|
.mark { display:block; }
|
|
65
|
-
.mark rect {
|
|
66
|
-
.
|
|
66
|
+
.mark rect { transition:fill .25s var(--settle); }
|
|
67
|
+
.mark rect:nth-child(1) { fill:var(--brand-anthropic); }
|
|
68
|
+
.mark rect:nth-child(2) { fill:var(--ink); }
|
|
69
|
+
.mark rect:nth-child(3) { fill:url(#relayCodexGradient); }
|
|
67
70
|
.word { font-family:var(--serif); font-size:19px; font-weight:500; letter-spacing:-.01em; color:var(--ink); line-height:1; }
|
|
68
71
|
.spacer { flex:1 1 auto; }
|
|
69
72
|
.minimize-hint {
|
|
@@ -276,7 +279,6 @@
|
|
|
276
279
|
height:100%; display:flex; flex-direction:column; align-items:center; justify-content:center;
|
|
277
280
|
gap:11px; padding:34px 28px; text-align:center;
|
|
278
281
|
}
|
|
279
|
-
.empty .mark rect { fill:var(--muted-3); }
|
|
280
282
|
.empty .t1 { font-family:var(--serif); font-size:16px; color:#4f4740; }
|
|
281
283
|
.empty .t2 { font-size:12.5px; color:var(--muted-2); max-width:210px; line-height:1.45; }
|
|
282
284
|
.gone { display:none !important; }
|
|
@@ -387,10 +389,22 @@
|
|
|
387
389
|
</style>
|
|
388
390
|
</head>
|
|
389
391
|
<body>
|
|
392
|
+
<svg width="0" height="0" viewBox="0 0 0 0" aria-hidden="true" focusable="false">
|
|
393
|
+
<defs>
|
|
394
|
+
<linearGradient id="relayCodexGradient" x1="0%" y1="0%" x2="100%" y2="100%">
|
|
395
|
+
<stop offset="0%" stop-color="#C1C1FA"/>
|
|
396
|
+
<stop offset="16%" stop-color="#C0B1F9"/>
|
|
397
|
+
<stop offset="38%" stop-color="#7790F7"/>
|
|
398
|
+
<stop offset="58%" stop-color="#5F74F6"/>
|
|
399
|
+
<stop offset="82%" stop-color="#342CF5"/>
|
|
400
|
+
<stop offset="100%" stop-color="#3229F5"/>
|
|
401
|
+
</linearGradient>
|
|
402
|
+
</defs>
|
|
403
|
+
</svg>
|
|
390
404
|
<div class="card" id="card">
|
|
391
405
|
<div class="lockup" id="lockup">
|
|
392
406
|
<svg class="mark" width="17" height="17" viewBox="0 0 16 16" aria-hidden="true">
|
|
393
|
-
<rect
|
|
407
|
+
<rect x="0" y="6" width="4" height="4"/>
|
|
394
408
|
<rect x="6" y="6" width="4" height="4"/>
|
|
395
409
|
<rect x="12" y="6" width="4" height="4"/>
|
|
396
410
|
</svg>
|
package/package.json
CHANGED
package/src/client.js
CHANGED
|
@@ -74,6 +74,10 @@ export class RelayClient {
|
|
|
74
74
|
return this.#req("GET", `/v1/open/${encodeURIComponent(token)}/packet`, undefined, { auth: false });
|
|
75
75
|
}
|
|
76
76
|
|
|
77
|
+
bindOpenRelay(token) {
|
|
78
|
+
return this.#req("POST", `/v1/open/${encodeURIComponent(token)}/bind`, {});
|
|
79
|
+
}
|
|
80
|
+
|
|
77
81
|
createFileUpload(payload) {
|
|
78
82
|
return this.#req("POST", "/v1/files", payload);
|
|
79
83
|
}
|
package/src/mcp.js
CHANGED
|
@@ -29,7 +29,22 @@ export const TOOLS = [
|
|
|
29
29
|
type: "array",
|
|
30
30
|
items: { type: "string", enum: ["codex", "claude_code", "claude_desktop", "claude_cowork"] },
|
|
31
31
|
},
|
|
32
|
-
attachments: {
|
|
32
|
+
attachments: {
|
|
33
|
+
type: "array",
|
|
34
|
+
description:
|
|
35
|
+
"Optional files to send with the relay. Each item must include id, name, contentType, bytes, and optionally sha256. If the recipient may not be on Relay and will receive email fallback, include contentBase64 with the exact file bytes so Relay can attach the files directly to the email; otherwise the API will reject the send instead of emailing a broken partial relay.",
|
|
36
|
+
items: {
|
|
37
|
+
type: "object",
|
|
38
|
+
properties: {
|
|
39
|
+
id: { type: "string" },
|
|
40
|
+
name: { type: "string" },
|
|
41
|
+
contentType: { type: "string" },
|
|
42
|
+
bytes: { type: "number" },
|
|
43
|
+
sha256: { type: "string" },
|
|
44
|
+
contentBase64: { type: "string" },
|
|
45
|
+
},
|
|
46
|
+
},
|
|
47
|
+
},
|
|
33
48
|
idempotencyKey: { type: "string" },
|
|
34
49
|
},
|
|
35
50
|
required: ["recipient", "title", "bodyMarkdown", "idempotencyKey"],
|
package/src/notifications.js
CHANGED
|
@@ -453,7 +453,10 @@ export function defaultStageRelayCompanionItem(notification) {
|
|
|
453
453
|
return stageRelayCompanionItem(notification);
|
|
454
454
|
}
|
|
455
455
|
|
|
456
|
-
export function stagePlainRelayItem(
|
|
456
|
+
export function stagePlainRelayItem(
|
|
457
|
+
{ item, packet, attachmentUrls = {} },
|
|
458
|
+
{ statePath = companionStatePath(), forceUnread = false } = {},
|
|
459
|
+
) {
|
|
457
460
|
if (!item?.relayId) throw new Error("stagePlainRelayItem requires an inbox item with relayId");
|
|
458
461
|
const state = readCompanionState(statePath);
|
|
459
462
|
state.packets ||= {};
|
|
@@ -474,7 +477,8 @@ export function stagePlainRelayItem({ item, packet, attachmentUrls = {} }, { sta
|
|
|
474
477
|
},
|
|
475
478
|
};
|
|
476
479
|
const contentPath = writeNotificationPacketContent({ id: item.relayId }, content, statePath);
|
|
477
|
-
const
|
|
480
|
+
const preserveSetupUnread = existing.setupImportedUnread === true && existing.state === "unread";
|
|
481
|
+
const readState = forceUnread || preserveSetupUnread ? "unread" : existing.state === "read" || item.state === "read" ? "read" : "unread";
|
|
478
482
|
const row = {
|
|
479
483
|
...existing,
|
|
480
484
|
id: item.relayId,
|
|
@@ -504,6 +508,7 @@ export function stagePlainRelayItem({ item, packet, attachmentUrls = {} }, { sta
|
|
|
504
508
|
attachmentUrls,
|
|
505
509
|
materializationDeferredReason: "relay_pill",
|
|
506
510
|
materializedSurfaces: existing.materializedSurfaces || { codex: false, claudeCode: false, claudeCowork: false },
|
|
511
|
+
setupImportedUnread: forceUnread || preserveSetupUnread || undefined,
|
|
507
512
|
};
|
|
508
513
|
state.packets[item.relayId] = row;
|
|
509
514
|
writeCompanionState(statePath, state);
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import { RelayClient } from "./client.js";
|
|
2
|
+
import { openRelay } from "./materializer.js";
|
|
3
|
+
import { stagePlainRelayItem } from "./notifications.js";
|
|
4
|
+
|
|
5
|
+
export function normalizeSetupHost(host) {
|
|
6
|
+
const clean = String(host || "").trim().toLowerCase();
|
|
7
|
+
return clean === "codex" ? "codex" : "claude";
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export function setupOpenRelayToken(flags = {}) {
|
|
11
|
+
return String(flags["open-relay"] || flags.openRelay || flags.relay || "").trim() || null;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function setupOpenStatus(opened, openUrlFn = () => false) {
|
|
15
|
+
if (!opened) return { opened: false, fallbackAttempted: false, url: null };
|
|
16
|
+
if (opened.openedInHost || opened.skipExternalOpen) {
|
|
17
|
+
return { opened: true, fallbackAttempted: false, url: opened.url || null };
|
|
18
|
+
}
|
|
19
|
+
if (!opened.url) return { opened: false, fallbackAttempted: false, url: null };
|
|
20
|
+
return { opened: Boolean(openUrlFn(opened.url)), fallbackAttempted: true, url: opened.url };
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
function inboxItemFromOpenRelay(relay) {
|
|
25
|
+
return {
|
|
26
|
+
relayId: relay.relayId,
|
|
27
|
+
state: "delivered",
|
|
28
|
+
createdAt: relay.createdAt,
|
|
29
|
+
updatedAt: relay.createdAt,
|
|
30
|
+
kind: relay.kind,
|
|
31
|
+
title: relay.title,
|
|
32
|
+
displayTitle: relay.title,
|
|
33
|
+
sender: relay.sender,
|
|
34
|
+
preview: relay.preview,
|
|
35
|
+
hasAttachments: Boolean(relay.attachments?.length),
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export async function stageCurrentInbox({ client = new RelayClient(), stage = stagePlainRelayItem, forceUnread = false, log = () => {} } = {}) {
|
|
40
|
+
const inbox = await client.inbox();
|
|
41
|
+
const staged = [];
|
|
42
|
+
for (const item of inbox.items || []) {
|
|
43
|
+
try {
|
|
44
|
+
const fetched = await client.fetchRelay(item.relayId);
|
|
45
|
+
stage({ item, packet: fetched.packet, attachmentUrls: fetched.attachmentUrls || {} }, { forceUnread });
|
|
46
|
+
staged.push(item.relayId);
|
|
47
|
+
} catch (error) {
|
|
48
|
+
log(`could not stage relay ${item.relayId}: ${error instanceof Error ? error.message : String(error)}`);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
return staged;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export async function finishSetupOpenRelay({
|
|
55
|
+
token,
|
|
56
|
+
host = "claude",
|
|
57
|
+
client = new RelayClient(),
|
|
58
|
+
stage = stagePlainRelayItem,
|
|
59
|
+
openRelayFn = openRelay,
|
|
60
|
+
log = () => {},
|
|
61
|
+
} = {}) {
|
|
62
|
+
const cleanToken = String(token || "").trim();
|
|
63
|
+
if (!cleanToken) return null;
|
|
64
|
+
|
|
65
|
+
const bound = await client.bindOpenRelay(cleanToken);
|
|
66
|
+
const relay = bound?.relay;
|
|
67
|
+
if (!relay?.relayId) {
|
|
68
|
+
throw new Error("Relay setup paired this device, but the relay link could not be attached to the account.");
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const stagedIds = await stageCurrentInbox({ client, stage, forceUnread: true, log });
|
|
72
|
+
if (!stagedIds.includes(relay.relayId)) {
|
|
73
|
+
const fetched = await client.fetchRelay(relay.relayId);
|
|
74
|
+
stage({
|
|
75
|
+
item: inboxItemFromOpenRelay(relay),
|
|
76
|
+
packet: fetched.packet,
|
|
77
|
+
attachmentUrls: fetched.attachmentUrls || {},
|
|
78
|
+
}, { forceUnread: true });
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const result = await openRelayFn({ id: relay.relayId, host: normalizeSetupHost(host), log });
|
|
82
|
+
return { relayId: relay.relayId, staged: true, opened: result };
|
|
83
|
+
}
|