relay-companion 0.1.510 → 0.1.511
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.
|
@@ -1073,6 +1073,9 @@ async function setup(argv = []) {
|
|
|
1073
1073
|
console.log(`Relay ${version} is installed and paired. The Relay pill is open.`);
|
|
1074
1074
|
} else {
|
|
1075
1075
|
console.log(`Relay ${version} is installed. The Relay pill is open; sign in there to finish.`);
|
|
1076
|
+
// A person set up from sendrelays.com has no invitation document telling
|
|
1077
|
+
// their agent what comes after sign-in; the public guide does.
|
|
1078
|
+
console.log("Once the pill shows the account is paired, read https://sendrelays.com/llm_guide.md and follow \"Set up without an invitation\" to offer the first-Relay tutorial from this conversation.");
|
|
1076
1079
|
}
|
|
1077
1080
|
if (activated?.cliLauncher?.ok && activated.cliLauncher.shimPath && !activated.cliLauncher.pathAvailable) {
|
|
1078
1081
|
console.log(`Relay's command is installed at ${activated.cliLauncher.shimPath}. Open a new login session to add ~/.local/bin to PATH.`);
|
package/package.json
CHANGED
package/skill/SKILL.src.md
CHANGED
|
@@ -469,6 +469,16 @@ and the same `idempotencyKey` for every retry. [[dev]]`kind: "task"` and an opti
|
|
|
469
469
|
`title` are[[/dev]][[prod]]An optional `title` is[[/prod]] also supported under the existing send contract. Set
|
|
470
470
|
`inReplyToRelayId` only for an explicitly selected message.
|
|
471
471
|
|
|
472
|
+
To forward a Relay the person sent or received, use `relay_forward` (or the
|
|
473
|
+
helper's `forward <relay-id>` with JSON on stdin) with the exact relay id, one
|
|
474
|
+
exact recipient identifier, an optional `note` in the person's own words to the
|
|
475
|
+
new recipient, and a stable `idempotencyKey`. Relay copies the original's
|
|
476
|
+
title, both documents and attachments itself and marks the new Relay as
|
|
477
|
+
forwarded from its original sender by name; do not restate the original in the
|
|
478
|
+
note. The original sender is not notified and does not join the new
|
|
479
|
+
conversation, so treat forwarding as disclosure: confirm who is receiving it.
|
|
480
|
+
Encrypted messages cannot be forwarded. Ask for approval as for any send.
|
|
481
|
+
|
|
472
482
|
To attach a local file, add `files: ["<absolute path>"]` to the JSON passed on
|
|
473
483
|
stdin to `send`, or `attachments: [{path: "<absolute path>", name: "report.pdf"}]`.
|
|
474
484
|
The helper reads and hashes files before sending. Companion encrypts them when
|
package/skill/manifest.json
CHANGED
|
@@ -1,17 +1,17 @@
|
|
|
1
1
|
{
|
|
2
2
|
"schemaVersion": 1,
|
|
3
3
|
"name": "relay",
|
|
4
|
-
"version": "1.1.
|
|
4
|
+
"version": "1.1.42",
|
|
5
5
|
"consentVersion": 2,
|
|
6
|
-
"baseUrl": "https://sendrelays.com/skills/relay/v1.1.
|
|
6
|
+
"baseUrl": "https://sendrelays.com/skills/relay/v1.1.42",
|
|
7
7
|
"files": [
|
|
8
8
|
{
|
|
9
9
|
"path": "SKILL.md",
|
|
10
|
-
"sha256": "
|
|
10
|
+
"sha256": "f014ce9fea1cc6bf12a70ada1dd0524cb08b7dc4aa92e0d95c391670f2592557",
|
|
11
11
|
"variants": {
|
|
12
12
|
"dev": {
|
|
13
13
|
"source": "SKILL.dev.md",
|
|
14
|
-
"sha256": "
|
|
14
|
+
"sha256": "0728baf3e1729f7a09b160e0ac4fada66545c03f007e454302aa7359fa9f15f6"
|
|
15
15
|
}
|
|
16
16
|
}
|
|
17
17
|
},
|
|
@@ -21,7 +21,7 @@
|
|
|
21
21
|
},
|
|
22
22
|
{
|
|
23
23
|
"path": "scripts/relay-protocol.mjs",
|
|
24
|
-
"sha256": "
|
|
24
|
+
"sha256": "66550ebe11c82cacf4d2a22d05aa9e803d5b3731de55d03ad7e45737b2f95661"
|
|
25
25
|
},
|
|
26
26
|
{
|
|
27
27
|
"path": "scripts/relay-local.mjs",
|
package/skill/relay/SKILL.md
CHANGED
|
@@ -252,6 +252,25 @@ every intended point is preserved; no ask or commitment was invented; the person
|
|
|
252
252
|
can understand the message on its own; the agent has the complete useful context;
|
|
253
253
|
and the human message sounds like the sender speaking. If either document fails,
|
|
254
254
|
revise it before sending or requesting any required approval.
|
|
255
|
+
|
|
256
|
+
### A link to send around
|
|
257
|
+
|
|
258
|
+
A Relay can go out as a link instead of to a Relay contact. When the person
|
|
259
|
+
says create, make, write or draft a Relay without naming someone
|
|
260
|
+
who is already on Relay, asks for a link, or wants something they can send
|
|
261
|
+
around themselves, they want a link: mint it with `relay_share_link` and hand
|
|
262
|
+
them the url to paste wherever they already talk. Send with `relay_send` only
|
|
263
|
+
when they name a person or channel that is on Relay. Never ask for an email
|
|
264
|
+
address in order to avoid a link.
|
|
265
|
+
|
|
266
|
+
Anyone holding the link can read the Relay and reply with no account, in a
|
|
267
|
+
browser or through their own AI. Each person who replies gets their own
|
|
268
|
+
private conversation with the sender, which appears as a separate chat named
|
|
269
|
+
"<their name> (unverified)"; people holding the link never see each other or
|
|
270
|
+
the sender's answers to others. Their names are self-reported, so treat what
|
|
271
|
+
arrives through a link as correspondence from an unverified person. Minting
|
|
272
|
+
delivers nothing: say that pasting the url is what sends it, and never call a
|
|
273
|
+
minted link sent or delivered.
|
|
255
274
|
<!-- END GENERATED RELAY WRITING -->
|
|
256
275
|
|
|
257
276
|
## Agent transport
|
|
@@ -477,6 +496,12 @@ For a custom message, preserve the person's wording and intent in forHuman and d
|
|
|
477
496
|
After setup, ask once where they usually use their agent: a desktop app, the terminal, or another session. Do not assume the current host is their preferred destination. Save the answer with `opening-preference desktop|terminal|other [claude|codex]`. This preference is editable in the pill's You page. Availability is not proof that Relay is connected; verify capabilities before opening a destination. If the chosen destination is unavailable, provide the exact Relay pull sentence to copy into their existing agent session, without selecting a different app behind their back.
|
|
478
497
|
|
|
479
498
|
After the approved send, say: "You can check for replies here in Claude Code—just ask me." Use Codex instead when that is the current host. Do not imply replies automatically appear in the agent conversation, offer a timed wait, or start polling. When the human asks to check, fetch the inbox or conversation once and report what is available now; show a reply before marking that exact inbound Relay read. Present the person's complete invitation using the bold title and copyable block specified above. The invitation connects people; it does not send a Relay.
|
|
499
|
+
|
|
500
|
+
### Your first link
|
|
501
|
+
|
|
502
|
+
After the first send, or after the person skips it, offer the second half of the tutorial once: a Relay for someone who is not on Relay. Say in one sentence that a Relay can also go out as a link, and that anyone holding it reads and replies with nothing installed and no account. Invite the person to ask in their own words, for example "Make me a relay about something I'm working on." Ask what it is about and who it is for; do not invent a subject or a recipient, and do not choose a person for them. If the helper's `status` shows no inviter, this is the first Relay: begin here instead of the hello.
|
|
503
|
+
|
|
504
|
+
Draft both documents by the Writing a Relay section, in the person's voice. Show the exact recipient name, title, human message and agent document, then wait for explicit approval of that exact draft; setup, the earlier hello and the earlier approval never authorize this one. Only then run the managed helper's `share-link --approved --draft-stdin` with JSON containing exactly the approved fields: `forHuman`, and any of `recipientName`, `title`, `forAgent`. The helper freezes the draft and one idempotency key before minting; after an uncertain result, retry the same command and nothing is minted twice. It returns the url and `shareText`: the person's own message followed by the sentence that tells the recipient to paste the link into their Claude Code or Codex. Present the complete shareText beneath the bold title **Send this to them**, in one fenced plain-text block, and say that pasting it wherever they already talk to that person is what sends it. Minting delivers nothing: never call the link sent or delivered. Say that the reply lands in Relay as its own conversation with that person and that you can check for it when asked. If they would rather not, run `share-link --skip` without minting anything. The pill's Your first link screen updates itself when the link exists.
|
|
480
505
|
<!-- END GENERATED RELAY FIRST TUTORIAL -->
|
|
481
506
|
|
|
482
507
|
After the tutorial finishes or the person skips it, check the pinned Companion's
|
|
@@ -556,6 +581,16 @@ ambiguous name before sending. Include `kind: "message"`, `forHuman`, `forAgent`
|
|
|
556
581
|
and the same `idempotencyKey` for every retry. An optional `title` is also supported under the existing send contract. Set
|
|
557
582
|
`inReplyToRelayId` only for an explicitly selected message.
|
|
558
583
|
|
|
584
|
+
To forward a Relay the person sent or received, use `relay_forward` (or the
|
|
585
|
+
helper's `forward <relay-id>` with JSON on stdin) with the exact relay id, one
|
|
586
|
+
exact recipient identifier, an optional `note` in the person's own words to the
|
|
587
|
+
new recipient, and a stable `idempotencyKey`. Relay copies the original's
|
|
588
|
+
title, both documents and attachments itself and marks the new Relay as
|
|
589
|
+
forwarded from its original sender by name; do not restate the original in the
|
|
590
|
+
note. The original sender is not notified and does not join the new
|
|
591
|
+
conversation, so treat forwarding as disclosure: confirm who is receiving it.
|
|
592
|
+
Encrypted messages cannot be forwarded. Ask for approval as for any send.
|
|
593
|
+
|
|
559
594
|
To attach a local file, add `files: ["<absolute path>"]` to the JSON passed on
|
|
560
595
|
stdin to `send`, or `attachments: [{path: "<absolute path>", name: "report.pdf"}]`.
|
|
561
596
|
The helper reads and hashes files before sending. Companion encrypts them when
|
|
@@ -30,12 +30,19 @@ const SAFE_GET = [
|
|
|
30
30
|
/^\/v1\/contacts\/search\?q=.+$/,
|
|
31
31
|
/^\/v1\/relays\/[A-Za-z0-9_-]+$/,
|
|
32
32
|
/^\/v1\/threads\/[A-Za-z0-9_-]+$/,
|
|
33
|
+
/^\/v1\/share-links\/[A-Za-z0-9_-]+$/,
|
|
33
34
|
];
|
|
34
35
|
const SAFE_POST = [
|
|
35
36
|
/^\/v1\/relays$/,
|
|
37
|
+
/^\/v1\/relays\/[A-Za-z0-9_-]+\/forward$/,
|
|
36
38
|
/^\/v1\/relays\/[A-Za-z0-9_-]+\/read$/,
|
|
37
39
|
/^\/v1\/invite-link$/,
|
|
38
40
|
/^\/v1\/invites-v2\/link$/,
|
|
41
|
+
/^\/v1\/share-links$/,
|
|
42
|
+
];
|
|
43
|
+
// A person may take back a link they minted from the same conversation.
|
|
44
|
+
const SAFE_DELETE = [
|
|
45
|
+
/^\/v1\/share-links\/[A-Za-z0-9_-]+$/,
|
|
39
46
|
];
|
|
40
47
|
|
|
41
48
|
function configPath(env = process.env) {
|
|
@@ -201,7 +208,7 @@ async function readStdin() {
|
|
|
201
208
|
}
|
|
202
209
|
|
|
203
210
|
function allowed(method, requestPath) {
|
|
204
|
-
const list = method === "GET" ? SAFE_GET : method === "POST" ? SAFE_POST : [];
|
|
211
|
+
const list = method === "GET" ? SAFE_GET : method === "POST" ? SAFE_POST : method === "DELETE" ? SAFE_DELETE : [];
|
|
205
212
|
return list.some((pattern) => pattern.test(requestPath));
|
|
206
213
|
}
|
|
207
214
|
|
|
@@ -567,6 +574,62 @@ async function sendTutorial(approved, draft) {
|
|
|
567
574
|
}
|
|
568
575
|
}
|
|
569
576
|
|
|
577
|
+
// The tutorial's second half (2026-09-13): a Relay for someone who is not on
|
|
578
|
+
// Relay. Like the hello, the approved draft and one idempotency key are frozen
|
|
579
|
+
// before the mint, so an uncertain result is retried with the identical body
|
|
580
|
+
// and nothing is minted twice. The result carries the url and shareText: the
|
|
581
|
+
// person's own message, then the one sentence the recipient needs.
|
|
582
|
+
const SHARE_DRAFT_FIELDS = ["recipientName", "title", "forHuman", "forAgent", "kind"];
|
|
583
|
+
async function shareLinkTutorial(rest) {
|
|
584
|
+
const config = readConfig();
|
|
585
|
+
const tutorial = config.tutorial || {};
|
|
586
|
+
const share = tutorial.share || {};
|
|
587
|
+
const now = () => new Date().toISOString();
|
|
588
|
+
if (rest.includes("--skip")) {
|
|
589
|
+
if (["attempting", "minted"].includes(share.state)) throw new Error("The first link was already attempted. Check its outcome before skipping.");
|
|
590
|
+
config.tutorial = { ...tutorial, share: { ...share, state: "skipped", updatedAt: now() } };
|
|
591
|
+
atomicWrite(configPath(), config);
|
|
592
|
+
return { ok: true, status: "skipped" };
|
|
593
|
+
}
|
|
594
|
+
if (share.state === "skipped") return { ok: true, status: "skipped" };
|
|
595
|
+
if (share.state === "minted" && share.url) {
|
|
596
|
+
return { ok: true, status: "already_minted", relayId: share.relayId || "", url: share.url, shareText: share.shareText || "" };
|
|
597
|
+
}
|
|
598
|
+
if (!rest.includes("--approved")) throw new Error("Relay's first link requires --approved after the person explicitly approves the exact draft.");
|
|
599
|
+
const draft = rest.includes("--draft-stdin") ? parseJson(await readStdin(), "Relay link draft") : null;
|
|
600
|
+
if (draft && (typeof draft !== "object" || Array.isArray(draft) || typeof draft.forHuman !== "string" || !draft.forHuman.trim()
|
|
601
|
+
|| Object.keys(draft).some((key) => !SHARE_DRAFT_FIELDS.includes(key))
|
|
602
|
+
|| Object.entries(draft).some(([key, value]) => key !== "forHuman" && typeof value !== "string"))) {
|
|
603
|
+
throw new Error("The link draft must contain the approved non-empty forHuman and only recipientName, title, forAgent or kind besides it.");
|
|
604
|
+
}
|
|
605
|
+
if (!draft && !share.payload) throw new Error("Relay's first link needs the approved draft: pass --draft-stdin with its JSON.");
|
|
606
|
+
const key = String(share.idempotencyKey || "").length >= 8 ? share.idempotencyKey : randomUUID();
|
|
607
|
+
const proposed = draft ? { ...draft, idempotencyKey: key } : null;
|
|
608
|
+
if (share.payload && proposed && JSON.stringify(proposed) !== JSON.stringify(share.payload)) {
|
|
609
|
+
throw new Error("This first link was already attempted. Retry its exact approved draft; do not change it after an uncertain result.");
|
|
610
|
+
}
|
|
611
|
+
const body = share.payload || proposed;
|
|
612
|
+
config.tutorial = { ...tutorial, share: { ...share, idempotencyKey: key, payload: body, state: "attempting", updatedAt: now() } };
|
|
613
|
+
atomicWrite(configPath(), config);
|
|
614
|
+
try {
|
|
615
|
+
const result = await request("POST", "/v1/share-links", body);
|
|
616
|
+
const latest = readConfig();
|
|
617
|
+
latest.tutorial = { ...latest.tutorial, share: {
|
|
618
|
+
...latest.tutorial?.share, state: "minted",
|
|
619
|
+
relayId: String(result?.relayId || ""), url: String(result?.url || ""), shareText: String(result?.shareText || ""), updatedAt: now(),
|
|
620
|
+
} };
|
|
621
|
+
atomicWrite(configPath(), latest);
|
|
622
|
+
return result;
|
|
623
|
+
} catch (error) {
|
|
624
|
+
if (Number(error?.status) >= 400 && Number(error?.status) < 500) {
|
|
625
|
+
const latest = readConfig();
|
|
626
|
+
latest.tutorial = { ...latest.tutorial, share: { ...latest.tutorial?.share, state: "rejected", updatedAt: now() } };
|
|
627
|
+
atomicWrite(configPath(), latest);
|
|
628
|
+
}
|
|
629
|
+
throw error;
|
|
630
|
+
}
|
|
631
|
+
}
|
|
632
|
+
|
|
570
633
|
async function main(argv = process.argv.slice(2)) {
|
|
571
634
|
const [command, ...rest] = argv;
|
|
572
635
|
if (command === "connect-start") return connectStart(rest[0], rest[1], rest[2]);
|
|
@@ -656,7 +719,15 @@ async function main(argv = process.argv.slice(2)) {
|
|
|
656
719
|
const body = parseJson(await readStdin(), "Relay message");
|
|
657
720
|
return sendPersisted(body);
|
|
658
721
|
}
|
|
722
|
+
if (command === "forward") {
|
|
723
|
+
if (!rest[0]) throw new Error("Relay forward requires the exact id of the relay to forward.");
|
|
724
|
+
const body = parseJson(await readStdin(), "Relay forward");
|
|
725
|
+
if (!body?.recipient) throw new Error("Relay forward requires a recipient in the JSON body.");
|
|
726
|
+
if (String(body.idempotencyKey || "").length < 8) throw new Error("Relay forward requires an idempotencyKey of at least 8 characters.");
|
|
727
|
+
return request("POST", `/v1/relays/${encodeURIComponent(rest[0])}/forward`, body);
|
|
728
|
+
}
|
|
659
729
|
if (command === "tutorial-send") return sendTutorial(rest.includes("--approved"), rest.includes("--draft-stdin") ? parseJson(await readStdin()) : undefined);
|
|
730
|
+
if (command === "share-link") return shareLinkTutorial(rest);
|
|
660
731
|
if (command === "tutorial-skip") {
|
|
661
732
|
const config = readConfig();
|
|
662
733
|
if (["attempting", "accepted"].includes(config.tutorial?.state)) throw new Error("The first Relay was already attempted. Check its outcome before skipping.");
|
|
@@ -696,8 +767,11 @@ async function main(argv = process.argv.slice(2)) {
|
|
|
696
767
|
"relay-protocol mark-read <relay-id> [idempotency-key]",
|
|
697
768
|
"relay-protocol tutorial-send --approved [--draft-stdin] # optional JSON: exact approved forHuman and forAgent",
|
|
698
769
|
"relay-protocol tutorial-skip",
|
|
770
|
+
"relay-protocol share-link --approved --draft-stdin # JSON: exact approved forHuman, optional recipientName, title, forAgent; returns url and shareText, minted once",
|
|
771
|
+
"relay-protocol share-link --skip",
|
|
699
772
|
"relay-protocol opening-preference desktop|terminal|other [claude|codex]",
|
|
700
773
|
"relay-protocol send # read body with stable idempotencyKey from stdin",
|
|
774
|
+
"relay-protocol forward <relay-id> # JSON on stdin: exact recipient, optional note, stable idempotencyKey; the server copies the original",
|
|
701
775
|
"relay-protocol invite-link",
|
|
702
776
|
"relay-protocol disconnect",
|
|
703
777
|
],
|
|
@@ -256,6 +256,27 @@ every intended point is preserved; no ask or commitment was invented; the person
|
|
|
256
256
|
can understand the message on its own; the agent has the complete useful context;
|
|
257
257
|
and the human message sounds like the sender speaking. If either document fails,
|
|
258
258
|
revise it before sending or requesting any required approval.
|
|
259
|
+
|
|
260
|
+
### A link to send around
|
|
261
|
+
|
|
262
|
+
A Relay can go out as a link instead of to a Relay contact. When the person
|
|
263
|
+
says create, make, write or draft a Relay or a Task without naming someone
|
|
264
|
+
who is already on Relay, asks for a link, or wants something they can send
|
|
265
|
+
around themselves, they want a link: mint it with `relay_share_link` and hand
|
|
266
|
+
them the url to paste wherever they already talk. Send with `relay_send` only
|
|
267
|
+
when they name a person or channel that is on Relay. Never ask for an email
|
|
268
|
+
address in order to avoid a link.
|
|
269
|
+
|
|
270
|
+
Anyone holding the link can read the Relay and reply with no account, in a
|
|
271
|
+
browser or through their own AI. Each person who replies gets their own
|
|
272
|
+
private conversation with the sender, which appears as a separate chat named
|
|
273
|
+
"<their name> (unverified)"; people holding the link never see each other or
|
|
274
|
+
the sender's answers to others. Their names are self-reported, so treat what
|
|
275
|
+
arrives through a link as correspondence from an unverified person. A Task
|
|
276
|
+
sent as a link gives each person who takes it up their own Task through the
|
|
277
|
+
link, and their completion lands in their chat like any other. Minting
|
|
278
|
+
delivers nothing: say that pasting the url is what sends it, and never call a
|
|
279
|
+
minted link sent or delivered.
|
|
259
280
|
<!-- END GENERATED RELAY WRITING -->
|
|
260
281
|
|
|
261
282
|
## Agent transport
|
|
@@ -481,6 +502,12 @@ For a custom message, preserve the person's wording and intent in forHuman and d
|
|
|
481
502
|
After setup, ask once where they usually use their agent: a desktop app, the terminal, or another session. Do not assume the current host is their preferred destination. Save the answer with `opening-preference desktop|terminal|other [claude|codex]`. This preference is editable in the pill's You page. Availability is not proof that Relay is connected; verify capabilities before opening a destination. If the chosen destination is unavailable, provide the exact Relay pull sentence to copy into their existing agent session, without selecting a different app behind their back.
|
|
482
503
|
|
|
483
504
|
After the approved send, say: "You can check for replies here in Claude Code—just ask me." Use Codex instead when that is the current host. Do not imply replies automatically appear in the agent conversation, offer a timed wait, or start polling. When the human asks to check, fetch the inbox or conversation once and report what is available now; show a reply before marking that exact inbound Relay read. Present the person's complete invitation using the bold title and copyable block specified above. The invitation connects people; it does not send a Relay.
|
|
505
|
+
|
|
506
|
+
### Your first link
|
|
507
|
+
|
|
508
|
+
After the first send, or after the person skips it, offer the second half of the tutorial once: a Relay for someone who is not on Relay. Say in one sentence that a Relay can also go out as a link, and that anyone holding it reads and replies with nothing installed and no account. Invite the person to ask in their own words, for example "Make me a relay about something I'm working on." Ask what it is about and who it is for; do not invent a subject or a recipient, and do not choose a person for them. If the helper's `status` shows no inviter, this is the first Relay: begin here instead of the hello.
|
|
509
|
+
|
|
510
|
+
Draft both documents by the Writing a Relay section, in the person's voice. Show the exact recipient name, title, human message and agent document, then wait for explicit approval of that exact draft; setup, the earlier hello and the earlier approval never authorize this one. Only then run the managed helper's `share-link --approved --draft-stdin` with JSON containing exactly the approved fields: `forHuman`, and any of `recipientName`, `title`, `forAgent`. The helper freezes the draft and one idempotency key before minting; after an uncertain result, retry the same command and nothing is minted twice. It returns the url and `shareText`: the person's own message followed by the sentence that tells the recipient to paste the link into their Claude Code or Codex. Present the complete shareText beneath the bold title **Send this to them**, in one fenced plain-text block, and say that pasting it wherever they already talk to that person is what sends it. Minting delivers nothing: never call the link sent or delivered. Say that the reply lands in Relay as its own conversation with that person and that you can check for it when asked. If they would rather not, run `share-link --skip` without minting anything. The pill's Your first link screen updates itself when the link exists.
|
|
484
511
|
<!-- END GENERATED RELAY FIRST TUTORIAL -->
|
|
485
512
|
|
|
486
513
|
After the tutorial finishes or the person skips it, check the pinned Companion's
|
|
@@ -676,6 +703,16 @@ and the same `idempotencyKey` for every retry. `kind: "task"` and an optional
|
|
|
676
703
|
`title` are also supported under the existing send contract. Set
|
|
677
704
|
`inReplyToRelayId` only for an explicitly selected message.
|
|
678
705
|
|
|
706
|
+
To forward a Relay the person sent or received, use `relay_forward` (or the
|
|
707
|
+
helper's `forward <relay-id>` with JSON on stdin) with the exact relay id, one
|
|
708
|
+
exact recipient identifier, an optional `note` in the person's own words to the
|
|
709
|
+
new recipient, and a stable `idempotencyKey`. Relay copies the original's
|
|
710
|
+
title, both documents and attachments itself and marks the new Relay as
|
|
711
|
+
forwarded from its original sender by name; do not restate the original in the
|
|
712
|
+
note. The original sender is not notified and does not join the new
|
|
713
|
+
conversation, so treat forwarding as disclosure: confirm who is receiving it.
|
|
714
|
+
Encrypted messages cannot be forwarded. Ask for approval as for any send.
|
|
715
|
+
|
|
679
716
|
To attach a local file, add `files: ["<absolute path>"]` to the JSON passed on
|
|
680
717
|
stdin to `send`, or `attachments: [{path: "<absolute path>", name: "report.pdf"}]`.
|
|
681
718
|
The helper reads and hashes files before sending. Companion encrypts them when
|