relay-companion 0.1.474 → 0.1.476
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.
|
@@ -8,11 +8,11 @@ const { spawn } = require("node:child_process");
|
|
|
8
8
|
const STATUS_VERSION = 1;
|
|
9
9
|
|
|
10
10
|
function statusPath({ homeDir = os.homedir(), env = process.env } = {}) {
|
|
11
|
-
return env.RELAY_BACKGROUND_INSTALL_STATUS || path.join(homeDir, ".relay", "companion-install.json");
|
|
11
|
+
return env.RELAY_BACKGROUND_INSTALL_STATUS || path.join(env.RELAY_CONFIG_DIR || path.join(homeDir, ".relay"), "companion-install.json");
|
|
12
12
|
}
|
|
13
13
|
|
|
14
14
|
function logPath({ homeDir = os.homedir(), env = process.env } = {}) {
|
|
15
|
-
return env.RELAY_BACKGROUND_INSTALL_LOG || path.join(homeDir, ".relay", "companion-install.log");
|
|
15
|
+
return env.RELAY_BACKGROUND_INSTALL_LOG || path.join(env.RELAY_CONFIG_DIR || path.join(homeDir, ".relay"), "companion-install.log");
|
|
16
16
|
}
|
|
17
17
|
|
|
18
18
|
function atomicWriteJson(file, value) {
|
|
@@ -79,12 +79,33 @@ function defaultTargets({ homeDir = os.homedir(), env = process.env, host = "all
|
|
|
79
79
|
if (selected === "all" || selected === "codex") {
|
|
80
80
|
const root = env.CODEX_HOME || path.join(homeDir, ".codex");
|
|
81
81
|
targets.push({ host: "codex", directory: path.join(root, "skills", SKILL_NAME) });
|
|
82
|
+
targets.push({ host: "codex", directory: path.join(homeDir, ".agents", "skills", SKILL_NAME) });
|
|
82
83
|
}
|
|
83
84
|
if (selected === "all" || selected === "claude") {
|
|
84
85
|
const root = env.CLAUDE_HOME || path.join(homeDir, ".claude");
|
|
85
86
|
targets.push({ host: "claude", directory: path.join(root, "skills", SKILL_NAME) });
|
|
86
87
|
}
|
|
87
|
-
return targets;
|
|
88
|
+
return targets.filter((target, index) => targets.findIndex((other) => path.resolve(other.directory) === path.resolve(target.directory)) === index);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function configuredManifestUrl({ homeDir = os.homedir(), env = process.env, webOrigin } = {}) {
|
|
92
|
+
const configRoot = env.RELAY_CONFIG_DIR || path.join(homeDir, ".relay");
|
|
93
|
+
function read(file) {
|
|
94
|
+
try { return JSON.parse(fs.readFileSync(file, "utf8")); }
|
|
95
|
+
catch (error) { if (error.code === "ENOENT") return {}; throw error; }
|
|
96
|
+
}
|
|
97
|
+
const config = read(env.RELAY_CONFIG || path.join(configRoot, "config.json"));
|
|
98
|
+
const agent = config.webUrl || config.apiUrl ? {} : read(env.RELAY_AGENT_CONFIG || path.join(configRoot, "agent-protocol.json"));
|
|
99
|
+
const api = config.apiUrl || agent.apiUrl;
|
|
100
|
+
const knownOrigin = !api || api === "https://api.sendrelays.com" ? "https://sendrelays.com"
|
|
101
|
+
: api === "https://dev-api.sendrelays.com" ? "https://dev.sendrelays.com" : null;
|
|
102
|
+
const origin = webOrigin || env.RELAY_WEB_URL || config.webUrl || knownOrigin;
|
|
103
|
+
if (!origin) throw new Error("Relay requires a configured web origin for this API environment's skill updates.");
|
|
104
|
+
const url = new URL(origin);
|
|
105
|
+
if (url.protocol !== "https:" || url.username || url.password || url.pathname !== "/" || url.search || url.hash) {
|
|
106
|
+
throw new Error("Relay requires a secure configured web origin for skill updates.");
|
|
107
|
+
}
|
|
108
|
+
return new URL("/skills/relay/manifest.json", url).href;
|
|
88
109
|
}
|
|
89
110
|
|
|
90
111
|
function pathInside(parent, child) {
|
|
@@ -223,12 +244,13 @@ async function installBundled(options = {}) {
|
|
|
223
244
|
}
|
|
224
245
|
|
|
225
246
|
async function updateFromRemote(options = {}) {
|
|
226
|
-
const manifestUrl = String(options.manifestUrl ||
|
|
247
|
+
const manifestUrl = String(options.manifestUrl || configuredManifestUrl(options));
|
|
227
248
|
const parsedUrl = new URL(manifestUrl);
|
|
228
249
|
if (parsedUrl.protocol !== "https:" || parsedUrl.username || parsedUrl.password || parsedUrl.hash) {
|
|
229
250
|
throw new Error("Relay requires a secure skill manifest URL.");
|
|
230
251
|
}
|
|
231
252
|
const manifest = parseManifest(await fetchBytes(parsedUrl.href, options), { requireRemote: true });
|
|
253
|
+
if (new URL(manifest.baseUrl).origin !== parsedUrl.origin) throw new Error("Relay's skill bundle must use the configured web origin.");
|
|
232
254
|
return installManifest(manifest, (entry) => fetchBytes(`${manifest.baseUrl}/${entry.path.split("/").map(encodeURIComponent).join("/")}`, options), options);
|
|
233
255
|
}
|
|
234
256
|
|
|
@@ -297,6 +319,7 @@ module.exports = {
|
|
|
297
319
|
SKILL_NAME,
|
|
298
320
|
STATE_FILE,
|
|
299
321
|
compareVersions,
|
|
322
|
+
configuredManifestUrl,
|
|
300
323
|
defaultTargets,
|
|
301
324
|
installBundled,
|
|
302
325
|
installManifest,
|
package/package.json
CHANGED
package/skill/manifest.json
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
{
|
|
2
2
|
"schemaVersion": 1,
|
|
3
3
|
"name": "relay",
|
|
4
|
-
"version": "1.1.
|
|
4
|
+
"version": "1.1.7",
|
|
5
5
|
"consentVersion": 2,
|
|
6
|
-
"baseUrl": "https://sendrelays.com/skills/relay/v1.1.
|
|
6
|
+
"baseUrl": "https://sendrelays.com/skills/relay/v1.1.7",
|
|
7
7
|
"files": [
|
|
8
8
|
{
|
|
9
9
|
"path": "SKILL.md",
|
|
10
|
-
"sha256": "
|
|
10
|
+
"sha256": "ad86c448a5296a60ec1e4f974d941e6374f1863141d6b2dfbce11edca821cefa"
|
|
11
11
|
},
|
|
12
12
|
{
|
|
13
13
|
"path": "agents/openai.yaml",
|
|
@@ -15,11 +15,11 @@
|
|
|
15
15
|
},
|
|
16
16
|
{
|
|
17
17
|
"path": "scripts/relay-protocol.mjs",
|
|
18
|
-
"sha256": "
|
|
18
|
+
"sha256": "7077b7ce084705efc02314cd71dbf29495e274db7306ac9925794178fc2da86c"
|
|
19
19
|
},
|
|
20
20
|
{
|
|
21
21
|
"path": "scripts/relay-local.mjs",
|
|
22
|
-
"sha256": "
|
|
22
|
+
"sha256": "ee2687c5f060488dd292088c8197544e7b3354e2f7cbd5d5dffaed924aa955c9"
|
|
23
23
|
},
|
|
24
24
|
{
|
|
25
25
|
"path": "scripts/relay-attachments.mjs",
|
package/skill/relay/SKILL.md
CHANGED
|
@@ -1,17 +1,45 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: relay
|
|
3
|
-
description: Use Relay from Claude Code or Codex
|
|
3
|
+
description: Use Relay from Claude Code or Codex with Companion's local MCP tools and the protocol helper for setup and fallback. Use when the person asks to set up Relay, read or send a Relay, check messages, reply to a contact, share their invite link, or continue the first-run Relay tutorial. Preserve existing integrations.
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
# Relay
|
|
7
7
|
|
|
8
8
|
Use Relay inside the current agent conversation. Companion supplies a visual
|
|
9
|
-
view and, once connected,
|
|
9
|
+
view and, once connected, manages its credentials, encryption and outgoing queue.
|
|
10
10
|
Hosted/headless agents can use the authenticated HTTPS protocol directly.
|
|
11
11
|
|
|
12
|
+
## Agent transport
|
|
13
|
+
|
|
14
|
+
<!-- BEGIN GENERATED RELAY TRANSPORT -->
|
|
15
|
+
Use the available Relay MCP tools first when they answer. If they are absent or fail with an authentication or transport error, use the installed skill's protocol helper without repeatedly retrying MCP. Keep the exact approved message and idempotency key when switching transport. Do not treat permission refusals, invalid requests, account mismatches or encryption requirements as connection failures. The helper prefers the matching Companion and retains browser-approved HTTPS access for fallback. New setup uses the pinned helper immediately while Companion installs and registers local MCP for later sessions; registering MCP does not prove it is available in an already-open session. Existing hooks are preserved and new users receive no hooks.
|
|
16
|
+
<!-- END GENERATED RELAY TRANSPORT -->
|
|
17
|
+
|
|
18
|
+
## Keep the new user informed
|
|
19
|
+
|
|
20
|
+
Use ELI5 communication throughout setup and the first Relay: write for a capable person who is new to Relay. Explain what Relay lets them do, what is happening now, and the one next action they need to take. Use short, natural sentences and familiar words. Avoid jargon, childish analogies, command inventories and step-by-step tool narration. These rules are self-contained; if an ELI5 skill is already available, apply it too, but do not require the human to install another skill.
|
|
21
|
+
|
|
22
|
+
If new setup is needed, give a brief orientation before asking to set up: Relay lets them message people from their AI, and Companion gives them a visual view of their conversations. Explain that setup connects this AI to their Relay account and installs Relay's instructions and Companion with their permission. Keep access permissions and other decision-changing facts clear; plain language must not hide what they are approving.
|
|
23
|
+
|
|
24
|
+
For questions, choices and approvals, prefer the current host's built-in user-question interface over a question buried in ordinary chat text, when that tool is exposed and permitted for this kind of question in the current mode. Use the actual available tool and its documented constraints; do not invent a tool or change modes to obtain one. Ask one clear decision at a time, with concise choices including 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. If the interface cannot support the question, ask it 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.
|
|
25
|
+
|
|
26
|
+
During setup, give brief updates at meaningful changes or when the person needs to act. Explain delays or failures by their effect and the next step. Keep commands, paths, versions, authentication mechanics and diagnostic logs in tool work or optional detail unless they change a decision. Preserve material limitations: being connected here, Companion still installing, and another AI not yet verified are different states. Do not claim all setup is complete from partial success.
|
|
27
|
+
|
|
28
|
+
After new setup, finish with what the person can do now, any remaining action or limitation, and the copyable block for their other AI. For a contact-only invitation, simply report the contact result and any next action. Teach everyday use with a short natural-language example such as “Ask me to check your Relay inbox.” 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.
|
|
29
|
+
|
|
30
|
+
## Check for Relay before starting setup
|
|
31
|
+
|
|
32
|
+
A pasted invitation may be a request to add a contact from someone who already uses Relay. Before asking to install anything, make read-only checks in the current environment: look for an available Relay integration, the relay executable on PATH, an installed Relay skill and its supported helper, or an existing Companion installation. Do not install or update software merely to check whether it exists, and never inspect credential-file contents.
|
|
33
|
+
|
|
34
|
+
When the existing protocol helper is available, run its status command, then request GET /v1/me through that same helper to verify the live account. Status alone describes saved local state; it does not prove the connection works. An existing hosted integration can supply an equivalent read-only account check. Keep hosted integrations intact. Missing skill discovery, a command absent from PATH, a stopped Companion, expired authorization, a network failure, or a different local/remote environment does not prove Relay is uninstalled. Preserve what is installed; explain the specific issue and resolve the account or environment with the human before replacing any connection. Reopen an installed Companion when needed instead of installing it again.
|
|
35
|
+
|
|
36
|
+
If Relay is already installed or connected, treat this as contact-only unless the human explicitly asks for more setup. Use the invitation's contact page in the person's usual browser: append /contact to its /i/{token} URL. The invitation document supplies the exact link. Explain that accepting adds both people to each other's contacts without sending a message. Have the human check that the browser shows the same Relay account they use in their AI and choose Add contact. If the browser is signed out, sign in to the existing account. Use a supported system-browser opener; if unavailable, show the exact contact URL in a plain-text code block for the human to copy into their usual browser. Do not open sign-in in an AI-controlled browser. Do not request a new agent authorization, replace credentials, reinstall Companion, update or reinstall skills, or repeat the first-message tutorial just to accept another invite.
|
|
37
|
+
|
|
38
|
+
The contact page confirms connected, already connected, or own-invite results only after redemption succeeds. Do not claim a contact was added just because the page opened or a name-only contact search matched. If the result is unavailable or ambiguous, report that and verify before claiming success. Opening one's own invite adds no contact. Finish with the contact result; a separate message requires the human's explicit approval. Only continue into new setup when it is actually needed and authorized. If the installation state is uncertain, ask whether the person already uses Relay rather than assuming a fresh install.
|
|
39
|
+
|
|
12
40
|
## Connect
|
|
13
41
|
|
|
14
|
-
|
|
42
|
+
Only when the checks above establish that new setup is needed, ask once:
|
|
15
43
|
|
|
16
44
|
> May I set up Relay, install its agent skill, keep that skill updated, and
|
|
17
45
|
> install the Relay Companion in the background?
|
|
@@ -42,10 +70,9 @@ credential. If direct invocation is ever required, resolve `RELAY_SKILL_ROOT`
|
|
|
42
70
|
to the absolute directory containing this loaded `SKILL.md` and invoke
|
|
43
71
|
`node "<RELAY_SKILL_ROOT>/scripts/relay-protocol.mjs" ...`; never assume the
|
|
44
72
|
current working directory is the skill directory. The helper reads an
|
|
45
|
-
owner-only credential file and never prints the access token. Do not configure
|
|
46
|
-
an MCP server or install a hook.
|
|
73
|
+
owner-only credential file and never prints the access token. The background Companion installer registers local MCP. Do not add hooks or configure a hosted MCP server.
|
|
47
74
|
|
|
48
|
-
|
|
75
|
+
Except for contact-only invitations, once per new agent session, quietly run `relay skill update` only if the `relay`
|
|
49
76
|
executable is already available on `PATH`. If it is unavailable, skip the check
|
|
50
77
|
without installing anything or interrupting the task. Continue with the
|
|
51
78
|
installed skill if the network check fails. If Relay reports locally modified
|
|
@@ -53,6 +80,26 @@ files, preserve them and tell the person; never overwrite them. If it reports a
|
|
|
53
80
|
new consent version, explain the material change and ask before running
|
|
54
81
|
`relay skill update --renew-consent`.
|
|
55
82
|
|
|
83
|
+
### Open approval in the person's normal browser
|
|
84
|
+
|
|
85
|
+
On a local desktop, open the returned approvalUrl in the operating system's default browser using a supported external-browser action or OS URL opener. Do not use an AI-controlled browser, embedded preview, isolated browser profile, or browser automation for sign-in. Pass the exact URL as data to the opener, with safe argument handling; never interpolate it into executable shell text. On Windows, keep any launcher process hidden. Leave sign-in and approval to the human.
|
|
86
|
+
|
|
87
|
+
Tell the human: “Approve Relay in your usual browser, where you normally sign in.” If a normal-browser opener is unavailable or this is a remote/headless environment, show the exact approvalUrl in a plain-text code block and say: “Copy this URL into your usual browser to approve Relay, then return here.” Keep its full fragment intact. Use this copyable fallback if opening fails or lands in the wrong browser; a clickable chat link alone may reopen the AI browser. Never claim the browser opened or approval succeeded without evidence.
|
|
88
|
+
|
|
89
|
+
## Give the human a block for another AI
|
|
90
|
+
|
|
91
|
+
After verifying the connection and attempting setup, include a copyable plain-text block in the completion response, even if the human skips or declines the first-message tutorial. This is an optional way to start using Relay in another AI, not a prerequisite for finishing here or a workaround for skill discovery. Do not paste it into another conversation yourself.
|
|
92
|
+
|
|
93
|
+
Use the human's own reusable invitation from the helper's safe invite.url result or protocol invite-link. Never substitute the original inviter's link, invent a URL, or include credentials, pending-authorization data, or the one-time approval URL. Replace <my own Relay invite URL> below with that verified link. If it cannot be retrieved, report that limitation and give the block with the URL sentence omitted; do not delay the working connection or claim another surface is already connected.
|
|
94
|
+
|
|
95
|
+
Introduce it with: “To use Relay in another AI, paste this there:”
|
|
96
|
+
|
|
97
|
+
```text
|
|
98
|
+
I already use Relay. Help me use the same Relay account in this AI too.
|
|
99
|
+
My own Relay invite URL is: <my own Relay invite URL>
|
|
100
|
+
Read the current Relay agent guide at https://sendrelays.com/llm_guide.md and the agent instructions linked from my invitation if provided. First check for an existing Relay connection on this device using the supported helper's status command; preserve a working connection and verify it is my intended account. Ask before installing or updating the skill for this AI. If this environment needs a new connection, guide me through setup with my existing account in my usual browser. Never copy credentials between environments. Do not send a message or repeat the first-message tutorial. Verify what works here.
|
|
101
|
+
```
|
|
102
|
+
|
|
56
103
|
## Verify skill activation after setup
|
|
57
104
|
|
|
58
105
|
Own activation under the setup permission already given. Follow the activation
|
|
@@ -110,6 +157,11 @@ After the send succeeds:
|
|
|
110
157
|
5. Offer their reusable `invite-link` for sharing. It connects new people and
|
|
111
158
|
contains no prewritten message. Do not send the link to anyone yourself.
|
|
112
159
|
|
|
160
|
+
After the tutorial finishes or the person skips it, check the pinned Companion's
|
|
161
|
+
`background-status` once if background installation was started. Report whether
|
|
162
|
+
installation succeeded, is still running, or failed; keep the working helper
|
|
163
|
+
available. Installation success does not prove MCP is active in this session.
|
|
164
|
+
|
|
113
165
|
The tutorial activation event is the approved first Relay, not app installation.
|
|
114
166
|
|
|
115
167
|
## Everyday Relay work
|
|
@@ -119,9 +171,8 @@ result is ambiguous. Never invent an address or recipient identifier. Always
|
|
|
119
171
|
show the proposed human and agent payloads and obtain the person's approval for
|
|
120
172
|
a representational send.
|
|
121
173
|
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
payload. Preserve exact text when the person asks for a verbatim payload.
|
|
174
|
+
Before composing any Relay, apply the complete writing contract below. It is
|
|
175
|
+
part of this skill for every send path; no MCP tool description is needed.
|
|
125
176
|
|
|
126
177
|
Reading or summarizing an unread Relay should mark only the surfaced message as
|
|
127
178
|
read. The sequence is: run `inbox`; choose the intended Relay id; run `read`
|
|
@@ -137,6 +188,120 @@ same body and key; never generate a replacement key for a retry.
|
|
|
137
188
|
|
|
138
189
|
Use the absolute helper path with `help` for the exact local command surface.
|
|
139
190
|
|
|
191
|
+
<!-- BEGIN GENERATED RELAY WRITING -->
|
|
192
|
+
## Writing a Relay
|
|
193
|
+
|
|
194
|
+
Every regular Relay has two documents for two readers. The person is switching
|
|
195
|
+
contexts and needs to understand what this is about and what it means for them.
|
|
196
|
+
Their agent needs enough context to understand the whole matter and help them
|
|
197
|
+
continue without making them reconstruct the sender's work. Compose the complete
|
|
198
|
+
`forAgent` first, then write `forHuman`. A short human message must not mean a
|
|
199
|
+
thin agent handoff. Apply these rules to drafts and previews as well as sends.
|
|
200
|
+
|
|
201
|
+
### Preserve the sender's intent and voice
|
|
202
|
+
|
|
203
|
+
The human's informal instructions tell you what to communicate; they are not
|
|
204
|
+
usually a draft to lightly edit. Write what this person would naturally say to
|
|
205
|
+
this recipient. Supply the words, never additional meaning. Preserve every ask,
|
|
206
|
+
question, commitment, permission, deadline, urgency, opinion, evaluation and
|
|
207
|
+
next step without adding, removing, strengthening or softening any of them.
|
|
208
|
+
Keep suggestions tentative when the sender made them tentative. Include thanks
|
|
209
|
+
or other sentiments the sender explicitly requested. Preserve exact text when
|
|
210
|
+
the person requests a verbatim payload.
|
|
211
|
+
|
|
212
|
+
Sending information or attaching a file does not imply "please review",
|
|
213
|
+
"thoughts?", "let me know", or another request for a response. Do not invent a
|
|
214
|
+
closing ask, "nothing needed", or implementation assignment to complete a
|
|
215
|
+
template. Make ordinary wording choices yourself; ask only when a critical
|
|
216
|
+
uncertainty would materially change the meaning or commitment.
|
|
217
|
+
|
|
218
|
+
Use the sender's recipient-specific vocabulary, rhythm, directness, formality,
|
|
219
|
+
warmth, emphasis and sign-off. When relationship context matters, `sent` and
|
|
220
|
+
`chat <id>` can supply facts, referents and examples of messages the human typed.
|
|
221
|
+
Learn their voice from those, not earlier agent-written messages. History cannot
|
|
222
|
+
revive superseded intent. Do not copy the brevity or shorthand of messages they
|
|
223
|
+
wrote while already in a conversation; this reader may need fresh orientation.
|
|
224
|
+
|
|
225
|
+
### The agent document: carry the complete useful context
|
|
226
|
+
|
|
227
|
+
`forAgent` is required and non-empty for a regular Relay. Write a self-contained
|
|
228
|
+
document, as long and detailed as the authorized subject requires. Under-sending
|
|
229
|
+
here is worse than over-sending. Preserve the useful conclusions, constraints,
|
|
230
|
+
rejected options, failures, preferences, questions, next steps, sources,
|
|
231
|
+
mechanisms, evidence, code, paths, logs, reproduction steps, chronology, data
|
|
232
|
+
and verification guidance that are available and relevant. Use Markdown when
|
|
233
|
+
it helps. Do not invent missing evidence or include unrelated private context.
|
|
234
|
+
|
|
235
|
+
Retain each distinct point from the sender, its rationale when supplied, and
|
|
236
|
+
the qualifications needed to interpret it. Separate observations, suggestions,
|
|
237
|
+
open questions and authorized actions. Technical detail does not itself make a
|
|
238
|
+
message an assignment. Give the recipient's agent substantive context, not an
|
|
239
|
+
instruction to "capture these points" followed by a shorter paraphrase of the
|
|
240
|
+
human message. Avoid repeating `forHuman` verbatim; enough shared context to
|
|
241
|
+
make the agent document understandable is appropriate.
|
|
242
|
+
|
|
243
|
+
### The human document: write for someone arriving fresh
|
|
244
|
+
|
|
245
|
+
Start by plainly saying what this is about. Assume the reader has done a dozen
|
|
246
|
+
other things since it last came up. Give the minimum background needed before
|
|
247
|
+
the news; preserve that orientation when cutting. Do not refer to "the new
|
|
248
|
+
rule", "what we settled", an unexplained thread, or a coined term. Retell the
|
|
249
|
+
relevant thing in familiar words. A follow-up to an issue the recipient raised
|
|
250
|
+
within the last day may need only a sentence about the result and the closing
|
|
251
|
+
state the sender intended.
|
|
252
|
+
|
|
253
|
+
Then convey what happened, why it matters, and any decision, opinion or action
|
|
254
|
+
the sender actually wants. Keep the news to three sentences where possible;
|
|
255
|
+
use a fourth only when needed to preserve the intended meaning. Stay under 95
|
|
256
|
+
words by default. That is a ceiling, never a target: a small update is usually
|
|
257
|
+
a line or two. There is no required length ratio between the two documents;
|
|
258
|
+
their readers' needs determine the length.
|
|
259
|
+
|
|
260
|
+
Use complete, spoken sentences and plain words. Read it aloud: would the sender
|
|
261
|
+
say this to the recipient's face, and would the recipient understand it without
|
|
262
|
+
doing the work? Put one idea at a time. Avoid fragments, clipped shorthand,
|
|
263
|
+
clever lines, figures of speech, flourishes, or balanced rhetorical halves.
|
|
264
|
+
Use the sender's names for things. Words the recipient encounters in the
|
|
265
|
+
product or their own work are fine; avoid vocabulary learned only while doing
|
|
266
|
+
the underlying investigation. Say what happened to someone: "A supplier
|
|
267
|
+
charged us more than we agreed" or "People who opened the invite saw a blank
|
|
268
|
+
page."
|
|
269
|
+
|
|
270
|
+
Keep mechanisms, evidence, paths, commands, logs, versions, internal identifiers,
|
|
271
|
+
work chronology and implementation detail in `forAgent`, unless the sender
|
|
272
|
+
explicitly wants the person to read them or they change the person's decision.
|
|
273
|
+
Do not pack four findings into one sentence, squeeze a checklist into prose, or
|
|
274
|
+
turn the human document into an inventory of the agent document. Cut details
|
|
275
|
+
the person need not read before cutting meaning or necessary background.
|
|
276
|
+
Never add text just because space remains. No headings, lists, tables, code
|
|
277
|
+
blocks or repetition of the title in `forHuman`.
|
|
278
|
+
|
|
279
|
+
If Relay rejects an overlong draft, review it from the recipient's perspective,
|
|
280
|
+
remove repetition and move supporting detail to `forAgent`. Use any supported
|
|
281
|
+
length-review override only after rejection and only when the exact draft's
|
|
282
|
+
extra length is necessary to preserve intent; never preemptively or merely
|
|
283
|
+
because more detail is available.
|
|
284
|
+
|
|
285
|
+
### Title, message kind and final review
|
|
286
|
+
|
|
287
|
+
For a titled Relay, use a natural 3–6 word gist in the sender's register. Name
|
|
288
|
+
the single ask, outcome, update or decision someone should recognize at a
|
|
289
|
+
glance. Do not concatenate every finding or write a report headline.
|
|
290
|
+
|
|
291
|
+
Classify the requested outcome: `kind: "message"` is human correspondence,
|
|
292
|
+
including technical notes, suggestions, opinions and decisions. Use
|
|
293
|
+
`kind: "task"` only for requested external work by the recipient's agent, such
|
|
294
|
+
as inspecting, retrieving, changing, testing or verifying something. A small
|
|
295
|
+
operation or one addressed as "you" is still work; dense agent context alone
|
|
296
|
+
is not. Respect the account's available capabilities.
|
|
297
|
+
|
|
298
|
+
Before presenting or sending, check both documents against the user's request:
|
|
299
|
+
every intended point is preserved; no ask or commitment was invented; the person
|
|
300
|
+
can understand the message on its own; the agent has the complete useful context;
|
|
301
|
+
and the human message sounds like the sender speaking. If either document fails,
|
|
302
|
+
revise it before sending or requesting any required approval.
|
|
303
|
+
<!-- END GENERATED RELAY WRITING -->
|
|
304
|
+
|
|
140
305
|
## Attachments, channels and conversations
|
|
141
306
|
|
|
142
307
|
Use `groups` to find a channel, `chats` to find a conversation, and `chat <id>`
|
|
@@ -155,8 +320,16 @@ Use `attachment <relay-id> <attachment-id>` for an authorized download URL or
|
|
|
155
320
|
locally decrypted file path. Download URLs are private, temporary transport.
|
|
156
321
|
|
|
157
322
|
The same helper automatically uses Companion once it answers as the approved
|
|
158
|
-
account and environment. It
|
|
159
|
-
|
|
323
|
+
account and environment. It retains the browser-approved direct credential in
|
|
324
|
+
the protected credential file. If Companion is unavailable or its authentication
|
|
325
|
+
fails, the helper can use direct HTTPS after verifying the same account and that
|
|
326
|
+
the service's encryption mode is off. It never bypasses account mismatches,
|
|
327
|
+
permission refusals or encryption requirements. A lost send response retries
|
|
328
|
+
the same body and idempotency key; never create a second send to change transport.
|
|
329
|
+
Local destinations, delivery and outbox operations still require Companion.
|
|
330
|
+
If an older helper already deleted the direct credential, reopen Companion or
|
|
331
|
+
renew browser approval to restore direct fallback. Expired or revoked direct
|
|
332
|
+
authorization also requires renewed approval; never expose or copy a token.
|
|
160
333
|
`outbox` reports queued sends and failures. `outbox retry <idempotency-key>`
|
|
161
334
|
retries the stored, previously approved payload with the same key when asked.
|
|
162
335
|
Queued means held on this device,
|
|
@@ -29,6 +29,8 @@ export function readLocalDescriptor(file = localDescriptorPath()) {
|
|
|
29
29
|
export function localRequest(descriptor, request, { timeoutMs = 30000 } = {}) {
|
|
30
30
|
return new Promise((resolve, reject) => {
|
|
31
31
|
const socket = net.createConnection(descriptor.endpoint);
|
|
32
|
+
let possiblySent = false;
|
|
33
|
+
const transportError = (message) => Object.assign(new Error(message), { localTransportFailure: true, possiblySent });
|
|
32
34
|
let bytes = 0;
|
|
33
35
|
const chunks = [];
|
|
34
36
|
let finished = false;
|
|
@@ -39,8 +41,11 @@ export function localRequest(descriptor, request, { timeoutMs = 30000 } = {}) {
|
|
|
39
41
|
socket.destroy();
|
|
40
42
|
error ? reject(error) : resolve(value);
|
|
41
43
|
};
|
|
42
|
-
const timer = setTimeout(() => finish(
|
|
43
|
-
socket.once("connect", () =>
|
|
44
|
+
const timer = setTimeout(() => finish(transportError("Companion did not confirm the request. Retry with the same idempotency key.")), timeoutMs);
|
|
45
|
+
socket.once("connect", () => {
|
|
46
|
+
possiblySent = true;
|
|
47
|
+
socket.write(JSON.stringify({ ...request, capability: descriptor.capability }) + "\n");
|
|
48
|
+
});
|
|
44
49
|
socket.on("data", (chunk) => {
|
|
45
50
|
bytes += chunk.length;
|
|
46
51
|
if (bytes > LOCAL_MAX_BYTES) return finish(new Error("Relay's local response is too large."));
|
|
@@ -53,7 +58,7 @@ export function localRequest(descriptor, request, { timeoutMs = 30000 } = {}) {
|
|
|
53
58
|
} catch (error) { finish(error); }
|
|
54
59
|
}
|
|
55
60
|
});
|
|
56
|
-
socket.once("error", () => finish(
|
|
57
|
-
socket.once("end", () => { if (!finished) finish(
|
|
61
|
+
socket.once("error", () => finish(transportError("Companion is unavailable. Reopen Relay and retry; your agent conversation can stay open.")));
|
|
62
|
+
socket.once("end", () => { if (!finished) finish(transportError("Companion closed before confirming the request. Retry with the same idempotency key.")); });
|
|
58
63
|
});
|
|
59
64
|
}
|
|
@@ -23,6 +23,7 @@ const SAFE_GET = [
|
|
|
23
23
|
/^\/v1\/chats\/[A-Za-z0-9_-]+$/,
|
|
24
24
|
/^\/v1\/relays\/[A-Za-z0-9_-]+\/attachments\/[A-Za-z0-9_-]+\/download-url$/,
|
|
25
25
|
/^\/v1\/me$/,
|
|
26
|
+
/^\/v1\/e2ee\/status$/,
|
|
26
27
|
/^\/v1\/inbox(?:\?.*)?$/,
|
|
27
28
|
/^\/v1\/sent(?:\?.*)?$/,
|
|
28
29
|
/^\/v1\/contacts\/search\?q=.+$/,
|
|
@@ -37,11 +38,11 @@ const SAFE_POST = [
|
|
|
37
38
|
];
|
|
38
39
|
|
|
39
40
|
function configPath(env = process.env) {
|
|
40
|
-
return env.RELAY_AGENT_CONFIG || DEFAULT_CONFIG;
|
|
41
|
+
return env.RELAY_AGENT_CONFIG || (env.RELAY_CONFIG_DIR ? path.join(env.RELAY_CONFIG_DIR, "agent-protocol.json") : DEFAULT_CONFIG);
|
|
41
42
|
}
|
|
42
43
|
|
|
43
44
|
function pendingPath(env = process.env) {
|
|
44
|
-
return env.RELAY_AGENT_AUTHORIZATION || DEFAULT_PENDING;
|
|
45
|
+
return env.RELAY_AGENT_AUTHORIZATION || (env.RELAY_CONFIG_DIR ? path.join(env.RELAY_CONFIG_DIR, "agent-authorization.json") : DEFAULT_PENDING);
|
|
45
46
|
}
|
|
46
47
|
|
|
47
48
|
function relayApiOrigin(value, env = process.env) {
|
|
@@ -75,8 +76,14 @@ function readConfig(file = configPath()) {
|
|
|
75
76
|
let value;
|
|
76
77
|
try { value = JSON.parse(fs.readFileSync(file, "utf8")); }
|
|
77
78
|
catch (error) {
|
|
78
|
-
if (error?.code === "ENOENT")
|
|
79
|
-
|
|
79
|
+
if (error?.code === "ENOENT") {
|
|
80
|
+
const local = readLocalDescriptor();
|
|
81
|
+
if (!local) throw new Error("Relay is not connected in this agent yet. Complete the browser approval first.");
|
|
82
|
+
if (!/^usr_[A-Za-z0-9_-]+$/.test(String(local.accountId || ""))) throw new Error("Relay's local account is invalid. Reopen Companion.");
|
|
83
|
+
value = { local: true, consentVersion: 2, apiUrl: local.apiUrl, account: { relayUserId: local.accountId } };
|
|
84
|
+
} else {
|
|
85
|
+
throw new Error("Relay's agent credential file could not be read.");
|
|
86
|
+
}
|
|
80
87
|
}
|
|
81
88
|
const apiUrl = relayApiOrigin(value?.apiUrl);
|
|
82
89
|
const accessToken = String(value?.accessToken || "");
|
|
@@ -166,13 +173,31 @@ async function request(method, requestPath, body) {
|
|
|
166
173
|
const local = readLocalDescriptor();
|
|
167
174
|
if (local && (config.consentVersion ?? 1) >= 2) {
|
|
168
175
|
if (local.accountId !== config.account?.relayUserId || local.apiUrl !== config.apiUrl) throw new Error("Companion is connected to a different Relay account or environment. Nothing was sent or read.");
|
|
169
|
-
|
|
170
|
-
//
|
|
171
|
-
//
|
|
172
|
-
if (!config.local) { config.local = true;
|
|
173
|
-
|
|
176
|
+
// Retain the browser-approved credential independently of Companion. Record
|
|
177
|
+
// the local attempt before dispatch so a lost response still requires the
|
|
178
|
+
// encryption/account checks on a later direct retry.
|
|
179
|
+
if (!config.local) { config.local = true; atomicWrite(configPath(), config); }
|
|
180
|
+
try {
|
|
181
|
+
return await localRequest(local, { method: verb, path: cleanPath, body, accountId: config.account.relayUserId });
|
|
182
|
+
} catch (error) {
|
|
183
|
+
const stableMutation = typeof body?.idempotencyKey === "string" && body.idempotencyKey.trim().length >= 8;
|
|
184
|
+
// Permission and application refusals are authoritative; an expired local
|
|
185
|
+
// authentication can use the separately approved direct credential.
|
|
186
|
+
// An uncertain mutation can only cross transports with the same body
|
|
187
|
+
// and stable deduplication key.
|
|
188
|
+
if ((!error.localTransportFailure && error.status !== 401) || (error.possiblySent && verb !== "GET" && !stableMutation)) throw error;
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
if (!config.accessToken.startsWith("web_") || config.accessToken.length < 20) {
|
|
192
|
+
throw new Error("This connection has no direct Relay credential. Reopen Relay Companion, or renew browser approval to enable direct fallback.");
|
|
193
|
+
}
|
|
194
|
+
if (config.expiresAt && Date.parse(config.expiresAt) <= Date.now()) throw new Error("Relay's direct authorization expired. Renew browser approval to use direct fallback.");
|
|
195
|
+
if (config.local) {
|
|
196
|
+
const me = await authenticatedRequest(config.apiUrl, config.accessToken, "GET", "/v1/me");
|
|
197
|
+
if (!config.account?.relayUserId || me.user?.id !== config.account.relayUserId) throw new Error("Direct Relay is connected to a different account. Nothing was sent or read.");
|
|
198
|
+
const encryption = await authenticatedRequest(config.apiUrl, config.accessToken, "GET", "/v1/e2ee/status");
|
|
199
|
+
if (encryption.mode !== "off") throw new Error("Reopen Relay Companion for this connection's encryption. Direct fallback is unavailable.");
|
|
174
200
|
}
|
|
175
|
-
if (config.local) throw new Error("Reopen Relay Companion to use this connection. No agent restart is needed.");
|
|
176
201
|
try { return await authenticatedRequest(config.apiUrl, config.accessToken, verb, cleanPath, body); }
|
|
177
202
|
catch (error) {
|
|
178
203
|
if (verb !== "POST" || cleanPath !== "/v1/relays" || body?.longForHumanConfirmed !== true || error.code !== "human_message_review_required" || !error.body?.reviewToken) throw error;
|