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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "relay-companion",
3
- "version": "0.1.510",
3
+ "version": "0.1.511",
4
4
  "description": "Install Relay for Claude Code, Cowork, and Codex, then sign in from the Relay pill.",
5
5
  "homepage": "https://sendrelays.com/get-started",
6
6
  "repository": {
@@ -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
@@ -1,17 +1,17 @@
1
1
  {
2
2
  "schemaVersion": 1,
3
3
  "name": "relay",
4
- "version": "1.1.39",
4
+ "version": "1.1.42",
5
5
  "consentVersion": 2,
6
- "baseUrl": "https://sendrelays.com/skills/relay/v1.1.39",
6
+ "baseUrl": "https://sendrelays.com/skills/relay/v1.1.42",
7
7
  "files": [
8
8
  {
9
9
  "path": "SKILL.md",
10
- "sha256": "73b2eb867688ab4809e96796410731d81e7ba95a143840b50b56d1bbc475998a",
10
+ "sha256": "f014ce9fea1cc6bf12a70ada1dd0524cb08b7dc4aa92e0d95c391670f2592557",
11
11
  "variants": {
12
12
  "dev": {
13
13
  "source": "SKILL.dev.md",
14
- "sha256": "0804f4dd92819ca832e8dfe012f66eff672e5f6598ebed92281492bb6bea7d7a"
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": "4ebd73f6d8ad18dd4b706b3509800783bd3a751fefb0180bfaa746263d245e7c"
24
+ "sha256": "66550ebe11c82cacf4d2a22d05aa9e803d5b3731de55d03ad7e45737b2f95661"
25
25
  },
26
26
  {
27
27
  "path": "scripts/relay-local.mjs",
@@ -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