relay-companion 0.1.481 → 0.1.483

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "relay-companion",
3
- "version": "0.1.481",
3
+ "version": "0.1.483",
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": {
@@ -1,13 +1,13 @@
1
1
  {
2
2
  "schemaVersion": 1,
3
3
  "name": "relay",
4
- "version": "1.1.21",
4
+ "version": "1.1.23",
5
5
  "consentVersion": 2,
6
- "baseUrl": "https://sendrelays.com/skills/relay/v1.1.21",
6
+ "baseUrl": "https://sendrelays.com/skills/relay/v1.1.23",
7
7
  "files": [
8
8
  {
9
9
  "path": "SKILL.md",
10
- "sha256": "bb046c0e9161424aa613844e76cd9c32ce2b52b59c411fdd58abae268d971a6a"
10
+ "sha256": "111f2da17eda6b980a1673894b1e5814131c383236f4c362f3cc8ad29ff56d90"
11
11
  },
12
12
  {
13
13
  "path": "agents/openai.yaml",
@@ -15,7 +15,7 @@
15
15
  },
16
16
  {
17
17
  "path": "scripts/relay-protocol.mjs",
18
- "sha256": "0d697c635793ba96b723ca4eb926a59b4112c8c8328d996916ce8ab4e98cd59b"
18
+ "sha256": "104b021e53f03aa6e57f06dbed616099b96ad21cbe42a2b64fa6e4ec11dea953"
19
19
  },
20
20
  {
21
21
  "path": "scripts/relay-local.mjs",
@@ -38,7 +38,7 @@ Use the current host's documented recovery mechanism, subject to its actual deni
38
38
 
39
39
  Never retry the denied action through another shell, tool, wrapper or transport, change permission settings, request a wildcard allow rule, or suggest bypassing the host's safeguards. Keep setup agent-led: do not default to asking the human to run commands, paste status output or adjust Bash settings. Never offer a .relay-rollback or other backup helper for manual execution. If no supported recovery is available, give one concise explanation of the blocked action, what remains unverified, and a verified host review step if one is available. Do not offer an unavailable dialog or use “tell me Relay is not connected” as a substitute for verification. A user-requested manual handoff must use the current supported helper and protect secrets. The normal copyable URL fallback for browser sign-in remains available; it is not a workaround for a denied agent tool call.
40
40
 
41
- For questions, choices and approvals, use the current host's built-in user-question interface whenever that tool is exposed and permitted for this kind of question in the current mode. This is required when the interface is available, even if the tool is optional, a prior check or command failed, or a progress update was already given: call the interface and wait instead of placing the question in ordinary assistant chat or a final response. Before asking for setup permission, inspect the tools actually available to the current turn. Claude Code commonly exposes `AskUserQuestion` and Codex commonly exposes `request_user_input`; use the current host's documented equivalent if its name differs. Do not invent a tool or change modes to obtain one.
41
+ For questions, choices and approvals, instruct the current host through its built-in user-question tool whenever that tool is exposed and permitted for this kind of question in the current mode. The host renders the question UI from the tool call; do not draw fake buttons in Markdown or ask the host to render arbitrary HTML. Before asking, inspect the tools actually available to the current turn. Claude Code commonly exposes `AskUserQuestion`; Codex may expose `request_user_input_async` or mode-limited `request_user_input`. Respect each tool's constraints, especially restrictions on permission questions. Do not invent a tool, change modes to obtain one, or use a question tool for host permission escalation. When a permitted tool is available, call it instead of asking the same question in plain chat; wait for the actual answer before dependent work. For asynchronous questions, continue only unrelated safe work while waiting.
42
42
 
43
43
  For setup permission, put the complete question with the exact package version and source in the interface's question field and offer concise affirmative and decline choices such as “Set up Relay” and “Not now.” Ask one clear decision at a time, with a way to decline or skip when appropriate. For the first send, show both exact payloads and the recipient before asking, and make clear that approval sends that specific message. Do not abbreviate the payloads to fit a question widget. Only when no permitted user-question interface is exposed, or its documented constraints cannot carry the required content, ask plainly in chat. Use existing explicit permission; never ask again just to use the interface. A suggested or preselected choice, an empty result, silence or a timeout is not consent: wait for an actual affirmative answer before any action that requires approval. Browser sign-in and account approval still happen in the person's usual browser.
44
44
 
@@ -53,9 +53,12 @@ After new setup, include the person's reusable invitation immediately below this
53
53
  ```text
54
54
  Join me on Relay so we can message each other from our AI conversations.
55
55
 
56
+ Open the invitation yourself, or copy the setup request below into Claude Code or Codex:
57
+
58
+ Please help me connect on Relay using this invitation:
56
59
  <your own Relay invitation URL>
57
60
 
58
- Open the link, or paste this whole message into Claude Code or Codex and ask it to help you connect.
61
+ Open this invitation and read its current agent setup instructions. First check whether I already use Relay and preserve any existing setup. Ask for my approval before installing software or connecting my account. I will approve account access in my browser; ask separately before sending a message.
59
62
  ```
60
63
 
61
64
  Put the separate optional block for using Relay in another AI after the invitation, under its own label; never mix that setup handoff into the message for a friend. For a contact-only invitation, simply report the contact result and any next action. Introduce further features only when useful or requested. Before speaking, check that a new user can tell what this is about, why it matters, what changed and what they need to do next.
@@ -196,40 +199,24 @@ session activation remains unverified or failed. Do not claim app-wide setup
196
199
  complete, ask for restarts or compatibility messages, kill processes, alter host
197
200
  internals, add hooks/connectors/plugins, or inject prompts into other sessions.
198
201
 
199
- ## First-run tutorial
202
+ <!-- BEGIN GENERATED RELAY FIRST TUTORIAL -->
203
+ ## First Relay tutorial
204
+
205
+ An invitation redemption connects the recipient and inviter; it does not send a message automatically. Use the inviter's exact `relayUserId` returned by redemption.
206
+
207
+ Never send the tutorial message automatically. Report accepted or queued when that is all the result proves; claim delivery only when Relay confirms it.
200
208
 
201
- After an invite is redeemed, inspect `status`. If its tutorial state is
202
- `skipped_self`, this was the person's own invite: skip the tutorial send. For a
203
- normal invite, use the validated inviter identity as the first contact. Draft,
204
- but do not send, this first message:
209
+ First offer one native question with three paths: write my own message, use a suggested hello, or skip for now. Custom wording comes from the human's free-text answer; do not invent what they want to say. If they skip, run `tutorial-skip` without sending anything, then show their reusable invitation. For the suggested hello, show both fields verbatim:
205
210
 
206
211
  - Human payload: `Hi — I’ve just joined you on Relay.`
207
212
  - Agent payload: `This is my first Relay after joining from your invite. Help the person reply if they want to welcome me.`
208
213
 
209
- Show both payloads and name the inviter. Explain that the human payload is what
210
- the person reads, while the agent payload gives their agent useful context. Ask
211
- for explicit approval to send. Only after approval, run the protocol command as
212
- `tutorial-send --approved`. It uses the validated inviter id, exact two
213
- payloads, and one idempotency key persisted when setup completed. If a response
214
- is lost, retry the same command: it reuses that key and will not send a second
215
- hello. Never send the tutorial message automatically.
216
-
217
- After the send succeeds:
218
-
219
- 1. Confirm only that Relay accepted or queued it. Do not claim the inviter
220
- received or read it unless Relay explicitly reports that later.
221
- 2. Say: "You can check for replies here in Claude Code—just ask me." Use Codex
222
- instead when that is the current host. Do not imply replies automatically
223
- appear in the agent conversation.
224
- 3. Do not offer a timed wait or start polling. When the human asks to check,
225
- fetch the inbox or conversation once and report what is available now.
226
- Show a reply before marking that exact inbound Relay read.
227
- 4. If the app is still installing or a setup issue remains, state that in one
228
- short sentence. Do not repeat the installation details or feature list.
229
- 5. Present their complete reusable invitation beneath **Invite someone to
230
- Relay**, in one fenced plain-text block, as specified in the communication
231
- guide above. Use their own verified human invitation URL. Do not merely
232
- offer a link, use the inviter's invitation, or send it to anyone yourself.
214
+ For a custom message, preserve the person's wording and intent in forHuman and draft a complete forAgent document that adds useful context without inventing asks or commitments. Show both exact fields and the verified inviter before approval. Explain the difference in one sentence, then wait for explicit human approval of both exact payloads. Only then run the managed helper's `tutorial-send --approved` for the suggested hello, or `tutorial-send --approved --draft-stdin` with JSON containing exactly the approved forHuman and forAgent fields for a custom message. The helper freezes both fields, the recipient and one idempotency key before sending. Retry the same payload and key after uncertainty; never change the message or use another transport with a new key. Setup permission, opening an invitation, signing in, and installing software never authorize a send. Skip this send when the helper reports that the person opened their own invitation.
215
+
216
+ 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.
217
+
218
+ 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.
219
+ <!-- END GENERATED RELAY FIRST TUTORIAL -->
233
220
 
234
221
  After the tutorial finishes or the person skips it, check the pinned Companion's
235
222
  `background-status` once if background installation was started. Report whether
@@ -475,26 +475,40 @@ async function sendPersisted(body) {
475
475
  }
476
476
  }
477
477
 
478
- async function sendTutorial(approved) {
478
+ async function sendTutorial(approved, draft) {
479
479
  if (!approved) throw new Error("Relay tutorial send requires --approved after the person explicitly approves both payloads.");
480
480
  const config = readConfig();
481
481
  const tutorial = config.tutorial || {};
482
- if (tutorial.state === "skipped_self") return { ok: true, status: "skipped_self" };
482
+ if (["skipped_self", "skipped"].includes(tutorial.state)) return { ok: true, status: tutorial.state };
483
483
  if (tutorial.state === "accepted" && tutorial.relayId) {
484
484
  return { ok: true, status: "already_accepted", relayId: tutorial.relayId, state: tutorial.responseState || "" };
485
485
  }
486
486
  const inviter = requiredRelayIdentity(config.inviter, "inviter");
487
487
  const key = String(tutorial.idempotencyKey || "");
488
488
  if (key.length < 8) throw new Error("Relay tutorial state is missing its stable idempotency key. Connect again.");
489
- config.tutorial = { ...tutorial, state: "attempting", updatedAt: new Date().toISOString() };
490
- atomicWrite(configPath(), config);
491
- const body = {
489
+ if (draft && (typeof draft.forHuman !== "string" || !draft.forHuman.trim()
490
+ || typeof draft.forAgent !== "string" || !draft.forAgent.trim()
491
+ || Object.keys(draft).some((key) => !["forHuman", "forAgent"].includes(key)))) {
492
+ throw new Error("The tutorial draft must contain only the two approved, non-empty forHuman and forAgent fields.");
493
+ }
494
+ const proposed = {
492
495
  recipient: { relayUserId: inviter.relayUserId },
493
496
  kind: "message",
494
- forHuman: TUTORIAL_HUMAN,
495
- forAgent: TUTORIAL_AGENT,
497
+ forHuman: draft?.forHuman ?? TUTORIAL_HUMAN,
498
+ forAgent: draft?.forAgent ?? TUTORIAL_AGENT,
496
499
  idempotencyKey: key,
497
500
  };
501
+ if (tutorial.payload && draft && JSON.stringify(proposed) !== JSON.stringify(tutorial.payload)) {
502
+ throw new Error("This tutorial send was already attempted. Retry its exact approved payload; do not change it after an uncertain result.");
503
+ }
504
+ // Older attempts used the fixed hello. Preserve that payload across upgrades.
505
+ if (!tutorial.payload && ["attempting", "rejected"].includes(tutorial.state) && draft
506
+ && (draft.forHuman !== TUTORIAL_HUMAN || draft.forAgent !== TUTORIAL_AGENT)) {
507
+ throw new Error("Retry the original tutorial hello without changing its payload.");
508
+ }
509
+ const body = tutorial.payload || proposed;
510
+ config.tutorial = { ...tutorial, payload: body, recipient: body.recipient, state: "attempting", updatedAt: new Date().toISOString() };
511
+ atomicWrite(configPath(), config);
498
512
  try {
499
513
  const result = await request("POST", "/v1/relays", body);
500
514
  const latest = readConfig();
@@ -588,7 +602,22 @@ async function main(argv = process.argv.slice(2)) {
588
602
  const body = parseJson(await readStdin(), "Relay message");
589
603
  return sendPersisted(body);
590
604
  }
591
- if (command === "tutorial-send") return sendTutorial(rest.includes("--approved"));
605
+ if (command === "tutorial-send") return sendTutorial(rest.includes("--approved"), rest.includes("--draft-stdin") ? parseJson(await readStdin()) : undefined);
606
+ if (command === "tutorial-skip") {
607
+ const config = readConfig();
608
+ if (["attempting", "accepted"].includes(config.tutorial?.state)) throw new Error("The first Relay was already attempted. Check its outcome before skipping.");
609
+ config.tutorial = { ...config.tutorial, state: "skipped", updatedAt: new Date().toISOString() };
610
+ atomicWrite(configPath(), config);
611
+ return { ok: true, status: "skipped" };
612
+ }
613
+ if (command === "opening-preference") {
614
+ const [surface, provider] = rest;
615
+ if (!["desktop", "terminal", "other"].includes(surface) || (provider && !["claude", "codex"].includes(provider))) throw new Error("Choose desktop, terminal, or other; optionally name claude or codex.");
616
+ const config = readConfig();
617
+ config.openingPreference = { surface, ...(provider ? { provider } : {}) };
618
+ atomicWrite(configPath(), config);
619
+ return { ok: true, openingPreference: config.openingPreference };
620
+ }
592
621
  if (command === "request") {
593
622
  const method = String(rest.shift() || "GET").toUpperCase();
594
623
  const requestPath = String(rest.shift() || "");
@@ -609,7 +638,9 @@ async function main(argv = process.argv.slice(2)) {
609
638
  "relay-protocol contacts <name-or-email>",
610
639
  "relay-protocol read <relay-id>",
611
640
  "relay-protocol mark-read <relay-id> [idempotency-key]",
612
- "relay-protocol tutorial-send --approved",
641
+ "relay-protocol tutorial-send --approved [--draft-stdin] # optional JSON: exact approved forHuman and forAgent",
642
+ "relay-protocol tutorial-skip",
643
+ "relay-protocol opening-preference desktop|terminal|other [claude|codex]",
613
644
  "relay-protocol send # read body with stable idempotencyKey from stdin",
614
645
  "relay-protocol invite-link",
615
646
  "relay-protocol disconnect",
@@ -619,7 +650,7 @@ async function main(argv = process.argv.slice(2)) {
619
650
 
620
651
  export { allowed, authenticatedRequest, readConfig, configPath, atomicWrite, protectOwnerOnly };
621
652
 
622
- if (process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url)) {
653
+ if (process.argv[1] && fs.realpathSync(process.argv[1]) === fs.realpathSync(fileURLToPath(import.meta.url))) {
623
654
  try {
624
655
  const result = await main();
625
656
  process.stdout.write(`${JSON.stringify(result, null, 2)}\n`);