@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 +3 -1
- package/bin.mjs +125 -8
- package/package.json +20 -6
- package/skill/SKILL.md +5 -3
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
|
-
|
|
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
|
|
45
|
-
|
|
46
|
-
|
|
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.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"description": "Relay workspace CLI and skill for your own AI agents",
|
|
5
5
|
"type": "module",
|
|
6
|
-
"bin": {
|
|
7
|
-
|
|
8
|
-
|
|
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": {
|
|
11
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|