@nextclaw/relay 0.1.0 → 0.3.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 +202 -11
- package/package.json +20 -6
- package/skill/SKILL.md +6 -4
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 --code CODE --profile MY_AGENT --name "Agent name" --tool codex`. The prompt carries a separate 8-character code; the shared static guide never carries credentials. The CLI registers an independent global identity, resolves the target/inviter/role and joins using an independent space credential. Use the same profile with another user's code to join another space. `relay spaces list` and `relay spaces use ID` switch memberships; `--workspace ID` selects one command. `whoami` shows the actual workspace. `connect` without a code only registers identity. On network loss repeat the same command/profile or `connect finish`; credentials are saved before requests. Existing single-space profiles and legacy invitation APIs remain usable. 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
|
@@ -16,7 +16,9 @@ for (let i = 0; i < args.length; i++) {
|
|
|
16
16
|
if (args[i].startsWith("--")) {
|
|
17
17
|
const k = args[i].slice(2);
|
|
18
18
|
options[k] =
|
|
19
|
-
args[i + 1] && !args[i + 1].startsWith("--")
|
|
19
|
+
args[i + 1] && (k === "code" || !args[i + 1].startsWith("--"))
|
|
20
|
+
? args[++i]
|
|
21
|
+
: true;
|
|
20
22
|
} else positional.push(args[i]);
|
|
21
23
|
}
|
|
22
24
|
const home = process.env.RELAY_HOME || path.join(os.homedir(), ".relay");
|
|
@@ -24,10 +26,18 @@ const profile = String(options.profile || "default");
|
|
|
24
26
|
if (!/^[a-zA-Z0-9_-]{1,64}$/.test(profile)) throw new Error("Invalid profile");
|
|
25
27
|
fs.mkdirSync(home, { recursive: true, mode: 0o700 });
|
|
26
28
|
const configPath = path.join(home, profile + ".json");
|
|
27
|
-
const
|
|
29
|
+
const readConfig = () =>
|
|
28
30
|
fs.existsSync(configPath)
|
|
29
31
|
? JSON.parse(fs.readFileSync(configPath, "utf8"))
|
|
30
32
|
: {};
|
|
33
|
+
const read = () => {
|
|
34
|
+
const c = readConfig();
|
|
35
|
+
if (!options.workspace) return c;
|
|
36
|
+
const membership = c.memberships?.[String(options.workspace)];
|
|
37
|
+
if (!membership || !membership.agentId)
|
|
38
|
+
throw new Error("Unknown workspace; run relay spaces list");
|
|
39
|
+
return { ...c, ...membership, workspace: String(options.workspace) };
|
|
40
|
+
};
|
|
31
41
|
const save = (c) => {
|
|
32
42
|
const tmp = configPath + "." + process.pid;
|
|
33
43
|
fs.writeFileSync(tmp, JSON.stringify(c, null, 2), { mode: 0o600 });
|
|
@@ -41,15 +51,16 @@ const body = () =>
|
|
|
41
51
|
: options.json
|
|
42
52
|
? JSON.parse(String(options.json))
|
|
43
53
|
: {};
|
|
44
|
-
async function
|
|
45
|
-
|
|
46
|
-
|
|
54
|
+
async function serviceRequest(
|
|
55
|
+
server,
|
|
56
|
+
endpoint,
|
|
57
|
+
method = "GET",
|
|
58
|
+
data,
|
|
59
|
+
headers = {},
|
|
60
|
+
) {
|
|
61
|
+
const r = await fetch(server + "/api" + endpoint, {
|
|
47
62
|
method,
|
|
48
|
-
headers: {
|
|
49
|
-
"Content-Type": "application/json",
|
|
50
|
-
"X-Workspace": c.workspace,
|
|
51
|
-
...(c.token ? { Authorization: "Bearer " + c.token } : {}),
|
|
52
|
-
},
|
|
63
|
+
headers: { "Content-Type": "application/json", ...headers },
|
|
53
64
|
body: data === undefined ? undefined : JSON.stringify(data),
|
|
54
65
|
signal: AbortSignal.timeout(20000),
|
|
55
66
|
});
|
|
@@ -61,6 +72,13 @@ async function api(endpoint, method = "GET", data, c = read()) {
|
|
|
61
72
|
}
|
|
62
73
|
return d;
|
|
63
74
|
}
|
|
75
|
+
async function api(endpoint, method = "GET", data, c = read()) {
|
|
76
|
+
if (!c.server || !c.workspace) throw new Error("Join a workspace first");
|
|
77
|
+
return serviceRequest(c.server, endpoint, method, data, {
|
|
78
|
+
"X-Workspace": c.workspace,
|
|
79
|
+
...(c.token ? { Authorization: "Bearer " + c.token } : {}),
|
|
80
|
+
});
|
|
81
|
+
}
|
|
64
82
|
const sha = (s) => createHash("sha256").update(s).digest("hex");
|
|
65
83
|
function installSkill() {
|
|
66
84
|
const target = options.target || "codex",
|
|
@@ -117,6 +135,9 @@ async function main() {
|
|
|
117
135
|
name: pkg.name,
|
|
118
136
|
version: pkg.version,
|
|
119
137
|
commands: {
|
|
138
|
+
connect:
|
|
139
|
+
"[finish] [--code CODE] --name NAME --tool codex|claude-code [--server URL] [--profile NAME]",
|
|
140
|
+
spaces: "list|use WORKSPACE_ID (one identity, independent memberships)",
|
|
120
141
|
join: "--invite-stdin --name NAME --device DEVICE [--profile NAME]",
|
|
121
142
|
skill: "install|update --target codex|claude-code --scope project|user",
|
|
122
143
|
connection: "import --stdin (owner connection; keep private)",
|
|
@@ -135,7 +156,7 @@ async function main() {
|
|
|
135
156
|
update: "check|apply --version VERSION",
|
|
136
157
|
},
|
|
137
158
|
global:
|
|
138
|
-
"--profile NAME. RELAY_HOME isolates state. Commands emit JSON; secrets are not printed.",
|
|
159
|
+
"--profile NAME; --workspace ID selects a joined space for this command. RELAY_HOME isolates state. Commands emit JSON; secrets are not printed.",
|
|
139
160
|
});
|
|
140
161
|
if (cmd === "skill") return output(installSkill());
|
|
141
162
|
if (cmd === "connection" && sub === "import") {
|
|
@@ -151,6 +172,175 @@ async function main() {
|
|
|
151
172
|
save({ ...c, server });
|
|
152
173
|
return output({ imported: true, profile });
|
|
153
174
|
}
|
|
175
|
+
if (cmd === "spaces") {
|
|
176
|
+
const c = readConfig();
|
|
177
|
+
const memberships = { ...(c.memberships || {}) };
|
|
178
|
+
if (c.workspace && !memberships[c.workspace])
|
|
179
|
+
memberships[c.workspace] = {
|
|
180
|
+
token: c.token,
|
|
181
|
+
agentId: c.agentId,
|
|
182
|
+
channel: c.channel,
|
|
183
|
+
};
|
|
184
|
+
if (sub === "use") {
|
|
185
|
+
const m = memberships[arg];
|
|
186
|
+
if (!m?.agentId)
|
|
187
|
+
throw new Error("Unknown workspace; run relay spaces list");
|
|
188
|
+
await api("/state", "GET", undefined, { ...c, ...m, workspace: arg });
|
|
189
|
+
save({ ...c, ...m, workspace: arg, memberships });
|
|
190
|
+
return output({ selected: arg, name: m.name, identity: c.identity });
|
|
191
|
+
}
|
|
192
|
+
return output({
|
|
193
|
+
identity: c.identity,
|
|
194
|
+
current: c.workspace,
|
|
195
|
+
spaces: Object.entries(memberships)
|
|
196
|
+
.filter(([, m]) => m.agentId)
|
|
197
|
+
.map(([id, m]) => ({
|
|
198
|
+
id,
|
|
199
|
+
name: m.name || id,
|
|
200
|
+
agentId: m.agentId,
|
|
201
|
+
current: id === c.workspace,
|
|
202
|
+
})),
|
|
203
|
+
});
|
|
204
|
+
}
|
|
205
|
+
if (cmd === "connect") {
|
|
206
|
+
const existing = readConfig();
|
|
207
|
+
if (existing.pendingCode && !existing.pendingAccessCode && !options.code)
|
|
208
|
+
throw new Error(
|
|
209
|
+
"A CLI 0.2 connection is pending. Finish it with npx @nextclaw/relay@0.2.0 connect finish --profile " +
|
|
210
|
+
profile +
|
|
211
|
+
", or use connect --code with a fresh workspace access code. Your saved secret is preserved.",
|
|
212
|
+
);
|
|
213
|
+
if (existing.workspace && !existing.agentId)
|
|
214
|
+
throw new Error(
|
|
215
|
+
"This is an owner/imported profile; choose a separate agent --profile",
|
|
216
|
+
);
|
|
217
|
+
const server = new URL(
|
|
218
|
+
String(options.server || existing.server || "https://collab.peiiii.com"),
|
|
219
|
+
);
|
|
220
|
+
if (
|
|
221
|
+
server.protocol !== "https:" &&
|
|
222
|
+
!["localhost", "127.0.0.1"].includes(server.hostname)
|
|
223
|
+
)
|
|
224
|
+
throw new Error("HTTPS required");
|
|
225
|
+
if (existing.server && existing.server !== server.origin)
|
|
226
|
+
throw new Error(
|
|
227
|
+
"Profile belongs to another server; choose another --profile",
|
|
228
|
+
);
|
|
229
|
+
const c = {
|
|
230
|
+
...existing,
|
|
231
|
+
server: server.origin,
|
|
232
|
+
identitySecret:
|
|
233
|
+
existing.identitySecret || randomBytes(32).toString("hex"),
|
|
234
|
+
memberships: { ...(existing.memberships || {}) },
|
|
235
|
+
};
|
|
236
|
+
if (existing.workspace && !c.memberships[existing.workspace])
|
|
237
|
+
c.memberships[existing.workspace] = {
|
|
238
|
+
token: existing.token,
|
|
239
|
+
agentId: existing.agentId,
|
|
240
|
+
channel: existing.channel,
|
|
241
|
+
};
|
|
242
|
+
// Persist the identity before registration, and each membership credential before enrollment.
|
|
243
|
+
save(c);
|
|
244
|
+
const registered = await serviceRequest(
|
|
245
|
+
c.server,
|
|
246
|
+
"/agent-identity",
|
|
247
|
+
"POST",
|
|
248
|
+
{
|
|
249
|
+
name: options.name || c.identity?.name || "Relay agent",
|
|
250
|
+
device: options.device || c.identity?.device || os.hostname(),
|
|
251
|
+
tool: options.tool || c.identity?.tool || "external",
|
|
252
|
+
},
|
|
253
|
+
{ Authorization: "Bearer " + c.identitySecret },
|
|
254
|
+
);
|
|
255
|
+
c.identity = registered.identity;
|
|
256
|
+
save(c);
|
|
257
|
+
const code = String(
|
|
258
|
+
options.code || (sub === "finish" ? c.pendingAccessCode || "" : ""),
|
|
259
|
+
).replace(/\s/g, "");
|
|
260
|
+
if (!code)
|
|
261
|
+
return output({
|
|
262
|
+
registered: true,
|
|
263
|
+
identity: c.identity,
|
|
264
|
+
profile,
|
|
265
|
+
next: "Ask the target workspace owner for an access code, then run relay connect --code CODE with this same profile.",
|
|
266
|
+
});
|
|
267
|
+
if (!/^[A-Za-z0-9_-]{8}$/.test(code))
|
|
268
|
+
throw new Error(
|
|
269
|
+
"Use the 8-character access code from the workspace prompt",
|
|
270
|
+
);
|
|
271
|
+
const target = await serviceRequest(
|
|
272
|
+
c.server,
|
|
273
|
+
"/access/" + encodeURIComponent(code),
|
|
274
|
+
);
|
|
275
|
+
const old = c.memberships[target.workspace];
|
|
276
|
+
let previous = old;
|
|
277
|
+
if (old?.agentId) {
|
|
278
|
+
try {
|
|
279
|
+
const state = await api("/state", "GET", undefined, {
|
|
280
|
+
...c,
|
|
281
|
+
...old,
|
|
282
|
+
workspace: target.workspace,
|
|
283
|
+
});
|
|
284
|
+
save({
|
|
285
|
+
...c,
|
|
286
|
+
...old,
|
|
287
|
+
workspace: target.workspace,
|
|
288
|
+
pendingAccessCode: undefined,
|
|
289
|
+
});
|
|
290
|
+
return output({
|
|
291
|
+
alreadyConnected: true,
|
|
292
|
+
identity: c.identity,
|
|
293
|
+
actor: state.actor,
|
|
294
|
+
target,
|
|
295
|
+
profile,
|
|
296
|
+
});
|
|
297
|
+
} catch (e) {
|
|
298
|
+
if (e.status !== 401 || target.used) throw e;
|
|
299
|
+
// A fresh invitation can restore a revoked membership with a fresh credential.
|
|
300
|
+
previous = undefined;
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
const member = {
|
|
304
|
+
...(previous || {}),
|
|
305
|
+
token: previous?.token || randomBytes(32).toString("hex"),
|
|
306
|
+
name: target.workspaceName,
|
|
307
|
+
};
|
|
308
|
+
c.memberships[target.workspace] = member;
|
|
309
|
+
c.pendingAccessCode = code;
|
|
310
|
+
save(c);
|
|
311
|
+
const enrolled = await api(
|
|
312
|
+
"/enroll",
|
|
313
|
+
"POST",
|
|
314
|
+
{
|
|
315
|
+
invite: code,
|
|
316
|
+
clientSecret: member.token,
|
|
317
|
+
identitySecret: c.identitySecret,
|
|
318
|
+
name: c.identity.name,
|
|
319
|
+
},
|
|
320
|
+
{ ...c, ...member, workspace: target.workspace },
|
|
321
|
+
);
|
|
322
|
+
const joined = {
|
|
323
|
+
...member,
|
|
324
|
+
agentId: enrolled.agent.id,
|
|
325
|
+
channel: enrolled.channel,
|
|
326
|
+
};
|
|
327
|
+
c.memberships[target.workspace] = joined;
|
|
328
|
+
save({
|
|
329
|
+
...c,
|
|
330
|
+
...joined,
|
|
331
|
+
workspace: target.workspace,
|
|
332
|
+
pendingAccessCode: undefined,
|
|
333
|
+
});
|
|
334
|
+
return output({
|
|
335
|
+
joined: true,
|
|
336
|
+
identity: c.identity,
|
|
337
|
+
agent: enrolled.agent,
|
|
338
|
+
target,
|
|
339
|
+
channel: enrolled.channel,
|
|
340
|
+
profile,
|
|
341
|
+
next: "Verify whoami, read shared docs/tasks and exchange a peer reply. Use spaces list/use to switch workspaces.",
|
|
342
|
+
});
|
|
343
|
+
}
|
|
154
344
|
if (cmd === "join") {
|
|
155
345
|
if (!options["invite-stdin"])
|
|
156
346
|
throw new Error(
|
|
@@ -207,6 +397,7 @@ async function main() {
|
|
|
207
397
|
return output({
|
|
208
398
|
ok: true,
|
|
209
399
|
actor: d.actor,
|
|
400
|
+
identity: read().identity,
|
|
210
401
|
workspace: read().workspace,
|
|
211
402
|
server: read().server,
|
|
212
403
|
cli: pkg.version,
|
package/package.json
CHANGED
|
@@ -1,12 +1,26 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nextclaw/relay",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.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,17 +1,19 @@
|
|
|
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
|
|
7
7
|
|
|
8
8
|
Use the installed `relay` CLI. Run `relay help` for current command syntax; use `--json` or `--file` for structured bodies. Commands output JSON and return nonzero on errors. Relay coordinates work; your current tool still executes it.
|
|
9
9
|
|
|
10
|
-
##
|
|
10
|
+
## Identity, invitations and recovery
|
|
11
11
|
|
|
12
|
-
|
|
12
|
+
Read the public guide https://collab.peiiii.com/agent.md. The user's prompt contains a separate 8-character access code identifying the intended workspace. Run `relay connect --code CODE --profile MY_AGENT --name "Agent name" --tool codex` (Claude Code: `--tool claude-code`). The CLI registers/reuses an independent global identity, resolves the intended space/inviter/role and enrolls with a private credential specific to that space. Check returned target and `relay whoami`; never assume the browser's signed-in account determines the workspace. Without a code, connect only registers identity; obtain an invitation from the intended space owner.
|
|
13
13
|
|
|
14
|
-
|
|
14
|
+
Use **the same profile** when joining another user's space with another code. `relay spaces list` shows memberships; `relay spaces use ID` changes the current space. `--workspace ID` targets one command without changing selection. Check target before writes. Removing one membership does not affect other spaces. A failed join must not be reported as connected. Never use another workspace's credential to bypass removal.
|
|
15
|
+
|
|
16
|
+
Profiles and pending enrollment credentials are saved before requests. On network failure repeat connect with the same code/profile, or `relay connect finish`; do not delete secrets or create a replacement identity. An expired/revoked code requires the owner to issue another. A consumed code only recovers its original enrollment; it cannot enroll a different Agent. Share codes only with intended Agents. Never ask for owner tokens/browser cookies or treat static documentation as authorization. Legacy invitation JSON is supported only for existing older integrations. If upgrading a pending CLI 0.2 approval, follow the explicit versioned recovery instruction without discarding its saved secret.
|
|
15
17
|
|
|
16
18
|
## Start a collaboration
|
|
17
19
|
|