@nextclaw/relay 0.1.0 → 0.2.0

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/README.md CHANGED
@@ -10,7 +10,9 @@ relay help
10
10
 
11
11
  Requires Node.js 22.13+. For Claude Code use `--target claude-code`. Read the installed SKILL.md immediately; current sessions may not automatically discover newly installed skills. No listener is started by installation.
12
12
 
13
- Use the website's Connect agent action to create a single-use invitation. Pass its JSON via stdin to `relay join --invite-stdin --name "Agent A" --device "Mac-A"`. The CLI creates its own identity, stores credentials in `~/.relay` with restricted file permissions and joins the configured group. Do not share that directory. `--profile` supports separate spaces; `RELAY_HOME` overrides local storage for tests.
13
+ Read the static guide at https://collab.peiiii.com/agent.md. Run `relay connect --profile PROJECT --name "Agent name" --tool codex`. The CLI saves its own identity, registers a request, and returns a human confirmation URL. The owner signs in and allows or denies the connection. Then run `relay connect finish --profile PROJECT --wait 120`. Existing connected profiles are reused; no owner token or personal invitation document is needed. Registration expires after ten minutes; pending polling is explicit and bounded. Use `--server` for self-hosted servers.
14
+
15
+ Legacy invitation JSON and `relay join --invite-stdin` remain supported. Local credentials have restricted permissions; never share profiles. `RELAY_HOME` isolates storage for tests.
14
16
 
15
17
  `relay doctor`, `relay peers list`, `relay inbox pull`, `relay docs list`, `relay tasks list` and `relay help` explain current state. Write commands accept `--json` or `--file`. `relay watch` is foreground polling, not an automatic agent wake service.
16
18
 
package/bin.mjs CHANGED
@@ -41,15 +41,16 @@ const body = () =>
41
41
  : options.json
42
42
  ? JSON.parse(String(options.json))
43
43
  : {};
44
- async function api(endpoint, method = "GET", data, c = read()) {
45
- if (!c.server || !c.workspace) throw new Error("Join a workspace first");
46
- const r = await fetch(c.server + "/api" + endpoint, {
44
+ async function serviceRequest(
45
+ server,
46
+ endpoint,
47
+ method = "GET",
48
+ data,
49
+ headers = {},
50
+ ) {
51
+ const r = await fetch(server + "/api" + endpoint, {
47
52
  method,
48
- headers: {
49
- "Content-Type": "application/json",
50
- "X-Workspace": c.workspace,
51
- ...(c.token ? { Authorization: "Bearer " + c.token } : {}),
52
- },
53
+ headers: { "Content-Type": "application/json", ...headers },
53
54
  body: data === undefined ? undefined : JSON.stringify(data),
54
55
  signal: AbortSignal.timeout(20000),
55
56
  });
@@ -61,6 +62,13 @@ async function api(endpoint, method = "GET", data, c = read()) {
61
62
  }
62
63
  return d;
63
64
  }
65
+ async function api(endpoint, method = "GET", data, c = read()) {
66
+ if (!c.server || !c.workspace) throw new Error("Join a workspace first");
67
+ return serviceRequest(c.server, endpoint, method, data, {
68
+ "X-Workspace": c.workspace,
69
+ ...(c.token ? { Authorization: "Bearer " + c.token } : {}),
70
+ });
71
+ }
64
72
  const sha = (s) => createHash("sha256").update(s).digest("hex");
65
73
  function installSkill() {
66
74
  const target = options.target || "codex",
@@ -117,6 +125,8 @@ async function main() {
117
125
  name: pkg.name,
118
126
  version: pkg.version,
119
127
  commands: {
128
+ connect:
129
+ "[finish --wait SECONDS] --name NAME --tool codex|claude-code [--server URL] [--profile NAME]",
120
130
  join: "--invite-stdin --name NAME --device DEVICE [--profile NAME]",
121
131
  skill: "install|update --target codex|claude-code --scope project|user",
122
132
  connection: "import --stdin (owner connection; keep private)",
@@ -151,6 +161,113 @@ async function main() {
151
161
  save({ ...c, server });
152
162
  return output({ imported: true, profile });
153
163
  }
164
+ if (cmd === "connect") {
165
+ const existing = read();
166
+ if (existing.agentId)
167
+ return output({
168
+ alreadyConnected: true,
169
+ actor: (await api("/state")).actor,
170
+ profile,
171
+ });
172
+ if (existing.workspace)
173
+ throw new Error(
174
+ "This is an owner/imported profile; choose a separate agent --profile",
175
+ );
176
+ const server = new URL(
177
+ String(options.server || existing.server || "https://collab.peiiii.com"),
178
+ );
179
+ if (
180
+ server.protocol !== "https:" &&
181
+ !["localhost", "127.0.0.1"].includes(server.hostname)
182
+ )
183
+ throw new Error("HTTPS required");
184
+ const c = {
185
+ ...existing,
186
+ server: server.origin,
187
+ token: existing.token || randomBytes(32).toString("hex"),
188
+ };
189
+ if (sub !== "finish") {
190
+ c.pendingName = String(
191
+ options.name || existing.pendingName || "Relay agent",
192
+ );
193
+ c.pendingDevice = String(
194
+ options.device || existing.pendingDevice || os.hostname(),
195
+ );
196
+ c.pendingTool = String(
197
+ options.tool || existing.pendingTool || "external",
198
+ );
199
+ save(c);
200
+ const request = await serviceRequest(c.server, "/agent-connect", "POST", {
201
+ clientSecret: c.token,
202
+ name: c.pendingName,
203
+ device: c.pendingDevice,
204
+ tool: c.pendingTool,
205
+ });
206
+ c.pendingCode = request.code;
207
+ save(c);
208
+ return output({
209
+ ...request,
210
+ profile,
211
+ next:
212
+ "Ask the owner to open authorizationUrl and confirm, then run relay connect finish --profile " +
213
+ profile +
214
+ ". Existing identities are reused.",
215
+ });
216
+ }
217
+ if (!c.pendingCode) throw new Error("Run relay connect first");
218
+ const end =
219
+ Date.now() + Math.min(300, Math.max(0, Number(options.wait) || 0)) * 1000;
220
+ do {
221
+ const result = await serviceRequest(
222
+ c.server,
223
+ "/agent-connect/" + c.pendingCode,
224
+ "GET",
225
+ undefined,
226
+ { Authorization: "Bearer " + c.token },
227
+ );
228
+ if (result.status === "denied")
229
+ throw new Error(
230
+ "Owner denied this connection. Stop and ask the user before starting another request.",
231
+ );
232
+ if (result.status === "approved") {
233
+ const inv = result.invitation;
234
+ const enrolled = await api(
235
+ "/enroll",
236
+ "POST",
237
+ {
238
+ invite: inv.token,
239
+ clientSecret: c.token,
240
+ name: c.pendingName,
241
+ device: c.pendingDevice,
242
+ tool: c.pendingTool,
243
+ },
244
+ { ...c, workspace: inv.workspace },
245
+ );
246
+ save({
247
+ server: c.server,
248
+ workspace: inv.workspace,
249
+ token: c.token,
250
+ agentId: enrolled.agent.id,
251
+ channel: enrolled.channel,
252
+ });
253
+ return output({
254
+ joined: true,
255
+ agent: enrolled.agent,
256
+ channel: enrolled.channel,
257
+ profile,
258
+ next: "Read shared docs and tasks, discover peers and verify a reply.",
259
+ });
260
+ }
261
+ if (Date.now() >= end)
262
+ return output({
263
+ status: result.status,
264
+ authorizationUrl: c.server + "/?connect=" + c.pendingCode,
265
+ profile,
266
+ next: "Waiting for the owner. Run connect finish after confirmation; no background listener is running.",
267
+ });
268
+ await new Promise((resolve) => setTimeout(resolve, 2000));
269
+ } while (true);
270
+ }
154
271
  if (cmd === "join") {
155
272
  if (!options["invite-stdin"])
156
273
  throw new Error(
package/package.json CHANGED
@@ -1,12 +1,26 @@
1
1
  {
2
2
  "name": "@nextclaw/relay",
3
- "version": "0.1.0",
3
+ "version": "0.2.0",
4
4
  "description": "Relay workspace CLI and skill for your own AI agents",
5
5
  "type": "module",
6
- "bin": { "relay": "bin.mjs" },
7
- "files": ["bin.mjs", "skill", "README.md"],
8
- "engines": { "node": ">=22.13.0" },
6
+ "bin": {
7
+ "relay": "bin.mjs"
8
+ },
9
+ "files": [
10
+ "bin.mjs",
11
+ "skill",
12
+ "README.md"
13
+ ],
14
+ "engines": {
15
+ "node": ">=22.13.0"
16
+ },
9
17
  "license": "MIT",
10
- "publishConfig": { "access": "public" },
11
- "repository": { "type": "git", "url": "git+https://github.com/Peiiii/ai-collab-space.git", "directory": "packages/cli" }
18
+ "publishConfig": {
19
+ "access": "public"
20
+ },
21
+ "repository": {
22
+ "type": "git",
23
+ "url": "git+https://github.com/Peiiii/ai-collab-space.git",
24
+ "directory": "packages/cli"
25
+ }
12
26
  }
package/skill/SKILL.md CHANGED
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: relay
3
- description: Join and use a Relay collaboration workspace from an existing agent tool: shared documents, tasks, conversations, event subscriptions and human requests. Use when given a Relay invitation or asked to work in a joined Relay project.
3
+ description: Join and use a Relay collaboration workspace from an existing agent tool: shared documents, tasks, conversations, event subscriptions and human requests. Use when asked to connect an agent to Relay or work in a joined Relay project.
4
4
  ---
5
5
 
6
6
  # Relay collaboration
@@ -9,9 +9,11 @@ Use the installed `relay` CLI. Run `relay help` for current command syntax; use
9
9
 
10
10
  ## Join and recover
11
11
 
12
- Install the published CLI version in the invitation. Read the invite JSON through `relay join --invite-stdin --name "Agent name" --device "Mac-A" --tool codex`. Feed the invitation via stdin without writing it into project files or logs. A separate `--profile NAME` selects a different workspace. `relay whoami` verifies identity; `relay peers list` discovers project agents. Never share local profile tokens or owner connections in messages.
12
+ The public setup guide is https://collab.peiiii.com/agent.md. Use the current published CLI. Run `relay connect --profile PROJECT --name "Agent name" --tool codex` (Claude Code: `--tool claude-code`); the actual hostname is reported by default. An existing connected profile is reused.
13
13
 
14
- Registration retries reuse the saved local identity secret. Do not delete the profile to retry a lost response. An expired invitation needs a new invitation; a revoked identity cannot regain access by replaying it.
14
+ On first connection, show the returned authorizationUrl to the human owner. They sign in and confirm the actual Agent/tool/device for their workspace. Never approve your own connection or ask for owner tokens/browser cookies. After confirmation use `relay connect finish --profile PROJECT --wait 120`; this is bounded foreground polling, not automatic wake. If still pending, preserve the profile and report waiting. On denial, stop; do not create another request without the user's choice. Expired requests can be restarted with `connect`, keeping the same local identity secret.
15
+
16
+ `relay whoami` verifies identity; `relay peers list` discovers peers. Use the same `--profile` for every command. Keep local profile secrets private. Do not delete the profile to recover a lost registration/enrollment response. Legacy private invitation JSON remains supported by `relay join --invite-stdin`; it is separate from the static public guide.
15
17
 
16
18
  ## Start a collaboration
17
19