@rine-network/cli 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.
Files changed (3) hide show
  1. package/README.md +3 -1
  2. package/dist/main.js +1575 -743
  3. package/package.json +17 -5
package/dist/main.js CHANGED
@@ -2,8 +2,25 @@ import { createRequire } from "node:module";
2
2
  import { Command } from "commander";
3
3
  import * as fs from "node:fs";
4
4
  import { join } from "node:path";
5
+ import { ed25519, x25519 } from "@noble/curves/ed25519.js";
5
6
  import readline from "node:readline";
7
+ import { Aes256Gcm, CipherSuite, HkdfSha256 } from "@hpke/core";
8
+ import { DhkemX25519HkdfSha256 } from "@hpke/dhkem-x25519";
9
+ import { hmac } from "@noble/hashes/hmac.js";
10
+ import { sha256 } from "@noble/hashes/sha2.js";
6
11
  import { createEventSource } from "eventsource-client";
12
+ //#region \0rolldown/runtime.js
13
+ var __defProp = Object.defineProperty;
14
+ var __exportAll = (all, no_symbols) => {
15
+ let target = {};
16
+ for (var name in all) __defProp(target, name, {
17
+ get: all[name],
18
+ enumerable: true
19
+ });
20
+ if (!no_symbols) __defProp(target, Symbol.toStringTag, { value: "Module" });
21
+ return target;
22
+ };
23
+ //#endregion
7
24
  //#region src/errors.ts
8
25
  var RineApiError = class extends Error {
9
26
  constructor(status, detail, raw) {
@@ -67,6 +84,14 @@ function saveTokenCache(cache) {
67
84
  ensureDir(dir);
68
85
  writeAtomic(join(dir, "token_cache.json"), JSON.stringify(cache, null, 2));
69
86
  }
87
+ function cacheToken(profile, token) {
88
+ const cache = loadTokenCache();
89
+ cache[profile] = {
90
+ access_token: token.access_token,
91
+ expires_at: Date.now() / 1e3 + token.expires_in
92
+ };
93
+ saveTokenCache(cache);
94
+ }
70
95
  /**
71
96
  * Resolves credentials for a profile. Priority:
72
97
  * 1. RINE_CLIENT_ID + RINE_CLIENT_SECRET env vars (both required)
@@ -96,10 +121,12 @@ var HttpClient = class {
96
121
  baseUrl;
97
122
  tokenFn;
98
123
  canRefresh;
124
+ defaultHeaders;
99
125
  constructor(opts) {
100
- this.baseUrl = (opts.apiUrl ?? getApiUrl()).replace(/\/$/, "");
126
+ this.baseUrl = opts.apiUrl ?? getApiUrl();
101
127
  this.tokenFn = opts.tokenFn;
102
128
  this.canRefresh = opts.canRefresh ?? true;
129
+ this.defaultHeaders = opts.defaultHeaders ?? {};
103
130
  }
104
131
  async request(method, path, body, params, extraHeaders) {
105
132
  let url = this.baseUrl + path;
@@ -110,6 +137,7 @@ var HttpClient = class {
110
137
  const doFetch = async (force) => {
111
138
  const headers = {
112
139
  Authorization: `Bearer ${await this.tokenFn(force)}`,
140
+ ...this.defaultHeaders,
113
141
  ...extraHeaders
114
142
  };
115
143
  if (body !== void 0) headers["Content-Type"] = "application/json";
@@ -129,20 +157,32 @@ var HttpClient = class {
129
157
  if (res.status === 204) return void 0;
130
158
  return res.json();
131
159
  }
132
- get(path, params) {
133
- return this.request("GET", path, void 0, params);
160
+ get(path, params, extraHeaders) {
161
+ return this.request("GET", path, void 0, params, extraHeaders);
134
162
  }
135
163
  post(path, body, extraHeaders) {
136
164
  return this.request("POST", path, body, void 0, extraHeaders);
137
165
  }
138
- put(path, body) {
139
- return this.request("PUT", path, body);
166
+ put(path, body, extraHeaders) {
167
+ return this.request("PUT", path, body, void 0, extraHeaders);
168
+ }
169
+ patch(path, body, extraHeaders) {
170
+ return this.request("PATCH", path, body, void 0, extraHeaders);
140
171
  }
141
- patch(path, body) {
142
- return this.request("PATCH", path, body);
172
+ delete(path, extraHeaders) {
173
+ return this.request("DELETE", path, void 0, void 0, extraHeaders);
143
174
  }
144
- delete(path) {
145
- return this.request("DELETE", path);
175
+ /** Unauthenticated GET for public endpoints (e.g. /directory/*). */
176
+ static async publicGet(path, params) {
177
+ const base = getApiUrl();
178
+ const qs = params?.toString();
179
+ const url = qs ? `${base}${path}?${qs}` : `${base}${path}`;
180
+ const res = await fetch(url, { headers: { Accept: "application/json" } });
181
+ if (!res.ok) {
182
+ const detail = await parseErrorDetail(res);
183
+ throw new RineApiError(res.status, detail, res);
184
+ }
185
+ return res.json();
146
186
  }
147
187
  };
148
188
  /**
@@ -151,6 +191,27 @@ var HttpClient = class {
151
191
  * 2. Token cache — returned if not expired (60s margin)
152
192
  * 3. POST /oauth/token — fetches fresh token and caches it
153
193
  */
194
+ /**
195
+ * Fetches an OAuth token from POST /oauth/token.
196
+ * Shared by getOrRefreshToken, login, and register.
197
+ */
198
+ async function fetchOAuthToken(clientId, clientSecret) {
199
+ const apiUrl = getApiUrl();
200
+ const res = await fetch(`${apiUrl}/oauth/token`, {
201
+ method: "POST",
202
+ headers: { "Content-Type": "application/x-www-form-urlencoded" },
203
+ body: new URLSearchParams({
204
+ grant_type: "client_credentials",
205
+ client_id: clientId,
206
+ client_secret: clientSecret
207
+ }).toString()
208
+ });
209
+ if (!res.ok) {
210
+ const body = await res.json().catch(() => ({}));
211
+ throw new RineApiError(res.status, body.detail ?? "Token request failed");
212
+ }
213
+ return res.json();
214
+ }
154
215
  async function getOrRefreshToken(entry, profileName, force = false) {
155
216
  const envToken = process.env.RINE_TOKEN;
156
217
  if (envToken) return envToken;
@@ -160,28 +221,11 @@ async function getOrRefreshToken(entry, profileName, force = false) {
160
221
  if (cached !== void 0 && cached.expires_at - now > 60) return cached.access_token;
161
222
  }
162
223
  if (!entry) throw new RineApiError(0, "No credentials found. Run `rine login` first.");
163
- const apiUrl = getApiUrl();
164
- const res = await fetch(`${apiUrl}/oauth/token`, {
165
- method: "POST",
166
- headers: { "Content-Type": "application/x-www-form-urlencoded" },
167
- body: new URLSearchParams({
168
- grant_type: "client_credentials",
169
- client_id: entry.client_id,
170
- client_secret: entry.client_secret
171
- }).toString()
172
- });
173
- if (!res.ok) throw new RineApiError(res.status, "Token refresh failed");
174
- const data = await res.json();
175
- const freshNow = Date.now() / 1e3;
176
- const cache = loadTokenCache();
177
- cache[profileName] = {
178
- access_token: data.access_token,
179
- expires_at: freshNow + data.expires_in
180
- };
181
- saveTokenCache(cache);
224
+ const data = await fetchOAuthToken(entry.client_id, entry.client_secret);
225
+ cacheToken(profileName, data);
182
226
  return data.access_token;
183
227
  }
184
- async function createClient(profileFlag) {
228
+ async function createClient(profileFlag, defaultHeaders) {
185
229
  const profileName = profileFlag ?? "default";
186
230
  const entry = getCredentialEntry(profileName);
187
231
  const canRefresh = !process.env.RINE_TOKEN && entry !== void 0;
@@ -189,7 +233,8 @@ async function createClient(profileFlag) {
189
233
  return {
190
234
  client: new HttpClient({
191
235
  tokenFn,
192
- canRefresh
236
+ canRefresh,
237
+ defaultHeaders
193
238
  }),
194
239
  profileName,
195
240
  entry
@@ -234,31 +279,98 @@ function printText(msg) {
234
279
  function printError(msg) {
235
280
  console.error(msg);
236
281
  }
237
- function printResult(data, opts) {
238
- if (opts.json) {
239
- printJson(data);
240
- return;
241
- }
242
- if (opts.table && Array.isArray(data)) {
243
- printTable(data);
244
- return;
245
- }
246
- if (Array.isArray(data)) for (const item of data) {
247
- printObject(item);
248
- console.log("---");
249
- }
250
- else printObject(data);
282
+ //#endregion
283
+ //#region src/resolve-handle.ts
284
+ const UUID_ALIAS_RE = /\/agents\/([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})$/i;
285
+ const UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
286
+ /**
287
+ * Resolves a handle (e.g. "agent@org.rine.network") to a UUID
288
+ * via WebFinger (RFC 7033).
289
+ *
290
+ * Returns the UUID string on success, or the original input unchanged
291
+ * on failure (caller validates UUID format and reports the error).
292
+ */
293
+ async function resolveHandleViaWebFinger(handle) {
294
+ try {
295
+ const params = new URLSearchParams({ resource: `acct:${handle}` });
296
+ const data = await HttpClient.publicGet("/.well-known/webfinger", params);
297
+ for (const alias of data.aliases ?? []) {
298
+ const match = UUID_ALIAS_RE.exec(alias);
299
+ if (match?.[1]) return match[1];
300
+ }
301
+ } catch {}
302
+ return handle;
251
303
  }
252
- function printObject(data) {
253
- if (data === null || data === void 0) return;
254
- if (typeof data !== "object") {
255
- console.log(String(data));
256
- return;
257
- }
258
- for (const [k, v] of Object.entries(data)) {
259
- const val = typeof v === "object" ? JSON.stringify(v) : String(v ?? "");
260
- console.log(`${k}: ${val}`);
304
+ //#endregion
305
+ //#region src/resolve-agent.ts
306
+ let cached;
307
+ /**
308
+ * Resolve a value that may be a UUID or a handle (containing @) to a UUID.
309
+ * Handles are resolved via WebFinger. Required because local key storage
310
+ * is indexed by UUID.
311
+ */
312
+ async function resolveToUuid(value) {
313
+ if (!value.includes("@")) return value;
314
+ const resolved = await resolveHandleViaWebFinger(value);
315
+ if (!UUID_RE.test(resolved)) throw new Error(`Cannot resolve handle "${value}" to agent ID. Ensure WebFinger is available or use a UUID.`);
316
+ return resolved;
317
+ }
318
+ /**
319
+ * Resolve which agent ID to use.
320
+ * Priority: explicit --agent flag > --as global flag > auto-resolve (single-agent shortcut).
321
+ * Accepts both UUIDs and handles (e.g. "bot@org.rine.network").
322
+ */
323
+ async function resolveAgent(client, explicit, asFlag) {
324
+ if (explicit) return resolveToUuid(explicit);
325
+ if (asFlag) return resolveToUuid(asFlag);
326
+ if (cached === void 0) cached = (await client.get("/agents")).items;
327
+ if (cached.length === 1) {
328
+ const agent = cached[0];
329
+ if (agent) return agent.id;
261
330
  }
331
+ if (cached.length === 0) throw new Error("No active agents. Create one with 'rine agent create --name <name>'");
332
+ const lines = cached.map((a) => ` ${a.id} ${a.handle}`).join("\n");
333
+ throw new Error(`Multiple agents found. Specify with --agent <uuid>:\n${lines}`);
334
+ }
335
+ //#endregion
336
+ //#region src/action.ts
337
+ /**
338
+ * Wraps a command action with automatic client creation and error handling.
339
+ * Commander calls action(fn) with positional args first, then the options object.
340
+ * This wrapper forwards all arguments to the inner function.
341
+ */
342
+ function withClient(program, fn) {
343
+ return async (...args) => {
344
+ try {
345
+ const gOpts = program.opts();
346
+ const defaultHeaders = {};
347
+ if (gOpts.as) defaultHeaders["X-Rine-Agent"] = await resolveToUuid(gOpts.as);
348
+ const { client, profileName } = await createClient(gOpts.profile, defaultHeaders);
349
+ await fn({
350
+ client,
351
+ gOpts,
352
+ profileName,
353
+ extraHeaders: defaultHeaders
354
+ }, ...args);
355
+ } catch (err) {
356
+ printError(formatError(err));
357
+ process.exitCode = 1;
358
+ }
359
+ };
360
+ }
361
+ /**
362
+ * Wraps a command action with error handling only (no client creation).
363
+ * For commands like logout that don't need authentication.
364
+ */
365
+ function withErrorHandler(program, fn) {
366
+ return async (...args) => {
367
+ try {
368
+ await fn(program.opts(), ...args);
369
+ } catch (err) {
370
+ printError(formatError(err));
371
+ process.exitCode = 1;
372
+ }
373
+ };
262
374
  }
263
375
  //#endregion
264
376
  //#region src/commands/agent-profile.ts
@@ -307,6 +419,11 @@ function toRow(card) {
307
419
  Updated: String(card.updated_at ?? "—")
308
420
  };
309
421
  }
422
+ async function updateCard(client, agentId, modify) {
423
+ const card = await client.get(`/agents/${agentId}/card`);
424
+ const updated = modify(card) ?? card;
425
+ return await client.put(`/agents/${agentId}/card`, stripReadOnly(updated));
426
+ }
310
427
  function getAgentCmd(program) {
311
428
  const cmd = program.commands.find((c) => c.name() === "agent");
312
429
  if (!cmd) throw new Error("registerAgent must be called before registerAgentProfile");
@@ -314,109 +431,177 @@ function getAgentCmd(program) {
314
431
  }
315
432
  function registerAgentProfile(program) {
316
433
  const agent = getAgentCmd(program);
317
- agent.command("profile").description("Get agent card").argument("<agent-id>", "Agent ID").action(async (agentId) => {
318
- try {
319
- const gOpts = program.opts();
320
- const { client } = await createClient(gOpts.profile);
321
- const card = await client.get(`/agents/${agentId}/card`);
322
- if (gOpts.json) printJson(card);
323
- else printTable([toRow(card)]);
324
- } catch (err) {
325
- printError(formatError(err));
326
- process.exitCode = 1;
327
- }
328
- });
329
- agent.command("describe").description("Update agent description").argument("<agent-id>", "Agent ID").requiredOption("--description <desc>", "Agent description").action(async (agentId, opts) => {
330
- try {
331
- const gOpts = program.opts();
332
- const { client } = await createClient(gOpts.profile);
333
- const card = await client.get(`/agents/${agentId}/card`);
434
+ agent.command("profile").description("Get agent card").argument("<agent-id>", "Agent ID").action(withClient(program, async ({ client, gOpts }, agentId) => {
435
+ const card = await client.get(`/agents/${agentId}/card`);
436
+ if (gOpts.json) printJson(card);
437
+ else printTable([toRow(card)]);
438
+ }));
439
+ agent.command("describe").description("Update agent description").argument("<agent-id>", "Agent ID").requiredOption("--description <desc>", "Agent description").action(withClient(program, async ({ client, gOpts }, agentId, opts) => {
440
+ const result = await updateCard(client, agentId, (card) => {
334
441
  card.description = opts.description;
335
- await client.put(`/agents/${agentId}/card`, stripReadOnly(card));
336
- printMutationOk("Description updated", gOpts.json);
337
- } catch (err) {
338
- printError(formatError(err));
339
- process.exitCode = 1;
340
- }
341
- });
342
- agent.command("add-skill").description("Add a skill to the agent card").argument("<agent-id>", "Agent ID").requiredOption("--skill-id <id>", "Skill ID").requiredOption("--skill-name <name>", "Skill name").option("--skill-description <desc>", "Skill description").option("--tags <tags>", "Comma-separated tags").option("--examples <examples>", "Comma-separated examples").option("--input-modes <modes>", "Comma-separated input modes").option("--output-modes <modes>", "Comma-separated output modes").action(async (agentId, opts) => {
343
- try {
344
- const gOpts = program.opts();
345
- const { client } = await createClient(gOpts.profile);
346
- const card = await client.get(`/agents/${agentId}/card`);
442
+ });
443
+ if (gOpts.json) printJson(result);
444
+ else printSuccess("Description updated");
445
+ }));
446
+ agent.command("add-skill").description("Add a skill to the agent card").argument("<agent-id>", "Agent ID").requiredOption("--skill-id <id>", "Skill ID").requiredOption("--skill-name <name>", "Skill name").option("--skill-description <desc>", "Skill description").option("--tags <tags>", "Comma-separated tags").option("--examples <examples>", "Comma-separated examples").option("--input-modes <modes>", "Comma-separated input modes").option("--output-modes <modes>", "Comma-separated output modes").action(withClient(program, async ({ client, gOpts }, agentId, opts) => {
447
+ const result = await updateCard(client, agentId, (card) => {
347
448
  const skill = {
348
449
  id: opts.skillId,
349
- name: opts.skillName
450
+ name: opts.skillName,
451
+ description: opts.skillDescription ?? "",
452
+ tags: opts.tags ? opts.tags.split(",").map((t) => t.trim()) : []
350
453
  };
351
- if (opts.skillDescription) skill.description = opts.skillDescription;
352
- if (opts.tags) skill.tags = opts.tags.split(",").map((t) => t.trim());
353
454
  if (opts.examples) skill.examples = opts.examples.split(",").map((e) => e.trim());
354
455
  if (opts.inputModes) skill.inputModes = opts.inputModes.split(",").map((m) => m.trim());
355
456
  if (opts.outputModes) skill.outputModes = opts.outputModes.split(",").map((m) => m.trim());
356
457
  card.skills = [...Array.isArray(card.skills) ? card.skills : [], skill];
357
- await client.put(`/agents/${agentId}/card`, stripReadOnly(card));
358
- printMutationOk("Skill added", gOpts.json);
359
- } catch (err) {
360
- printError(formatError(err));
361
- process.exitCode = 1;
362
- }
363
- });
364
- agent.command("set-categories").description("Set agent categories").argument("<agent-id>", "Agent ID").requiredOption("--categories <cats>", "Comma-separated category names").action(async (agentId, opts) => {
365
- try {
366
- const gOpts = program.opts();
367
- const { client } = await createClient(gOpts.profile);
368
- const card = await client.get(`/agents/${agentId}/card`);
369
- const cats = opts.categories.split(",").map((c) => c.trim());
370
- await client.put(`/agents/${agentId}/card`, stripReadOnly(setRineField(card, "categories", cats)));
371
- printMutationOk("Categories updated", gOpts.json);
372
- } catch (err) {
373
- printError(formatError(err));
374
- process.exitCode = 1;
375
- }
376
- });
377
- agent.command("set-languages").description("Set agent supported languages").argument("<agent-id>", "Agent ID").requiredOption("--languages <langs>", "Comma-separated language codes").action(async (agentId, opts) => {
378
- try {
379
- const gOpts = program.opts();
380
- const { client } = await createClient(gOpts.profile);
381
- const card = await client.get(`/agents/${agentId}/card`);
382
- const langs = opts.languages.split(",").map((l) => l.trim());
383
- await client.put(`/agents/${agentId}/card`, stripReadOnly(setRineField(card, "languages", langs)));
384
- printMutationOk("Languages updated", gOpts.json);
385
- } catch (err) {
386
- printError(formatError(err));
387
- process.exitCode = 1;
388
- }
389
- });
390
- agent.command("set-pricing").description("Set agent pricing model").argument("<agent-id>", "Agent ID").requiredOption("--model <model>", `Pricing model (${VALID_PRICING.join("|")})`).action(async (agentId, opts) => {
391
- try {
392
- const gOpts = program.opts();
393
- if (!VALID_PRICING.includes(opts.model)) {
394
- printError(`Invalid pricing model '${opts.model}'. Valid: ${VALID_PRICING.join(", ")}`);
395
- process.exitCode = 2;
396
- return;
397
- }
398
- const { client } = await createClient(gOpts.profile);
399
- const card = await client.get(`/agents/${agentId}/card`);
400
- await client.put(`/agents/${agentId}/card`, stripReadOnly(setRineField(card, "pricing_model", opts.model)));
401
- printMutationOk("Pricing updated", gOpts.json);
402
- } catch (err) {
403
- printError(formatError(err));
404
- process.exitCode = 1;
458
+ });
459
+ if (gOpts.json) printJson(result);
460
+ else printSuccess("Skill added");
461
+ }));
462
+ agent.command("set-categories").description("Set agent categories").argument("<agent-id>", "Agent ID").requiredOption("--categories <cats>", "Comma-separated category names").action(withClient(program, async ({ client, gOpts }, agentId, opts) => {
463
+ const cats = opts.categories.split(",").map((c) => c.trim());
464
+ const result = await updateCard(client, agentId, (card) => setRineField(card, "categories", cats));
465
+ if (gOpts.json) printJson(result);
466
+ else printSuccess("Categories updated");
467
+ }));
468
+ agent.command("set-languages").description("Set agent supported languages").argument("<agent-id>", "Agent ID").requiredOption("--languages <langs>", "Comma-separated language codes").action(withClient(program, async ({ client, gOpts }, agentId, opts) => {
469
+ const langs = opts.languages.split(",").map((l) => l.trim());
470
+ const result = await updateCard(client, agentId, (card) => setRineField(card, "languages", langs));
471
+ if (gOpts.json) printJson(result);
472
+ else printSuccess("Languages updated");
473
+ }));
474
+ agent.command("set-pricing").description("Set agent pricing model").argument("<agent-id>", "Agent ID").requiredOption("--model <model>", `Pricing model (${VALID_PRICING.join("|")})`).action(withClient(program, async ({ client, gOpts }, agentId, opts) => {
475
+ if (!VALID_PRICING.includes(opts.model)) {
476
+ printError(`Invalid pricing model '${opts.model}'. Valid: ${VALID_PRICING.join(", ")}`);
477
+ process.exitCode = 2;
478
+ return;
405
479
  }
480
+ const result = await updateCard(client, agentId, (card) => setRineField(card, "pricing_model", opts.model));
481
+ if (gOpts.json) printJson(result);
482
+ else printSuccess("Pricing updated");
483
+ }));
484
+ agent.command("accept-types").description("Set accepted message types").argument("<agent-id>", "Agent ID").requiredOption("--types <types>", "Comma-separated message types").action(withClient(program, async ({ client, gOpts }, agentId, opts) => {
485
+ const types = opts.types.split(",").map((t) => t.trim());
486
+ const result = await updateCard(client, agentId, (card) => setRineField(card, "message_types_accepted", types));
487
+ if (gOpts.json) printJson(result);
488
+ else printSuccess("Accept types updated");
489
+ }));
490
+ }
491
+ //#endregion
492
+ //#region src/crypto/keys.ts
493
+ var keys_exports = /* @__PURE__ */ __exportAll({
494
+ agentIdFromKid: () => agentIdFromKid,
495
+ agentKeysExist: () => agentKeysExist,
496
+ encryptionPublicKeyToJWK: () => encryptionPublicKeyToJWK,
497
+ fromBase64Url: () => fromBase64Url,
498
+ generateAgentKeys: () => generateAgentKeys,
499
+ generateEncryptionKeyPair: () => generateEncryptionKeyPair,
500
+ generateSigningKeyPair: () => generateSigningKeyPair,
501
+ jwkToPublicKey: () => jwkToPublicKey,
502
+ loadAgentKeys: () => loadAgentKeys,
503
+ saveAgentKeys: () => saveAgentKeys,
504
+ signingPublicKeyToJWK: () => signingPublicKeyToJWK,
505
+ toBase64Url: () => toBase64Url,
506
+ validatePathId: () => validatePathId
507
+ });
508
+ function toBase64Url(bytes) {
509
+ return Buffer.from(bytes).toString("base64url");
510
+ }
511
+ function fromBase64Url(s) {
512
+ return new Uint8Array(Buffer.from(s, "base64url"));
513
+ }
514
+ function generateSigningKeyPair() {
515
+ const privateKey = ed25519.utils.randomSecretKey();
516
+ return {
517
+ privateKey,
518
+ publicKey: ed25519.getPublicKey(privateKey)
519
+ };
520
+ }
521
+ function generateEncryptionKeyPair() {
522
+ const privateKey = x25519.utils.randomSecretKey();
523
+ return {
524
+ privateKey,
525
+ publicKey: x25519.getPublicKey(privateKey)
526
+ };
527
+ }
528
+ function generateAgentKeys() {
529
+ return {
530
+ signing: generateSigningKeyPair(),
531
+ encryption: generateEncryptionKeyPair()
532
+ };
533
+ }
534
+ function signingPublicKeyToJWK(publicKey) {
535
+ return {
536
+ kty: "OKP",
537
+ crv: "Ed25519",
538
+ x: toBase64Url(publicKey)
539
+ };
540
+ }
541
+ function encryptionPublicKeyToJWK(publicKey) {
542
+ return {
543
+ kty: "OKP",
544
+ crv: "X25519",
545
+ x: toBase64Url(publicKey)
546
+ };
547
+ }
548
+ function jwkToPublicKey(jwk) {
549
+ const key = fromBase64Url(jwk.x);
550
+ if (key.length !== 32) throw new Error(`Invalid public key: expected 32 bytes, got ${key.length}`);
551
+ return key;
552
+ }
553
+ const KID_RE = /^rine:([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})$/i;
554
+ function agentIdFromKid(kid) {
555
+ const match = KID_RE.exec(kid);
556
+ if (!match) throw new Error(`Invalid sender KID format: ${kid}`);
557
+ return match[1];
558
+ }
559
+ const PATH_SAFE_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
560
+ function validatePathId(id, label) {
561
+ if (!PATH_SAFE_RE.test(id)) throw new Error(`Invalid ${label}: must be a UUID, got '${id}'`);
562
+ }
563
+ function getKeysDir$1() {
564
+ return join(getConfigDir(), "keys");
565
+ }
566
+ function agentKeysDir(agentId) {
567
+ validatePathId(agentId, "agent ID");
568
+ return join(getKeysDir$1(), agentId);
569
+ }
570
+ function saveAgentKeys(agentId, keys) {
571
+ const dir = agentKeysDir(agentId);
572
+ fs.mkdirSync(dir, {
573
+ recursive: true,
574
+ mode: 448
406
575
  });
407
- agent.command("accept-types").description("Set accepted message types").argument("<agent-id>", "Agent ID").requiredOption("--types <types>", "Comma-separated message types").action(async (agentId, opts) => {
408
- try {
409
- const gOpts = program.opts();
410
- const { client } = await createClient(gOpts.profile);
411
- const card = await client.get(`/agents/${agentId}/card`);
412
- const types = opts.types.split(",").map((t) => t.trim());
413
- await client.put(`/agents/${agentId}/card`, stripReadOnly(setRineField(card, "message_types_accepted", types)));
414
- printMutationOk("Accept types updated", gOpts.json);
415
- } catch (err) {
416
- printError(formatError(err));
417
- process.exitCode = 1;
576
+ const sigPath = join(dir, "signing.key");
577
+ fs.writeFileSync(sigPath, toBase64Url(keys.signing.privateKey), "utf-8");
578
+ fs.chmodSync(sigPath, 384);
579
+ const encPath = join(dir, "encryption.key");
580
+ fs.writeFileSync(encPath, toBase64Url(keys.encryption.privateKey), "utf-8");
581
+ fs.chmodSync(encPath, 384);
582
+ }
583
+ function loadAgentKeys(agentId) {
584
+ const dir = agentKeysDir(agentId);
585
+ const sigPriv = fromBase64Url(fs.readFileSync(join(dir, "signing.key"), "utf-8").trim());
586
+ if (sigPriv.length !== 32) throw new Error(`Corrupt signing key: expected 32 bytes, got ${sigPriv.length}`);
587
+ const sigPub = ed25519.getPublicKey(sigPriv);
588
+ const encPriv = fromBase64Url(fs.readFileSync(join(dir, "encryption.key"), "utf-8").trim());
589
+ if (encPriv.length !== 32) throw new Error(`Corrupt encryption key: expected 32 bytes, got ${encPriv.length}`);
590
+ const encPub = x25519.getPublicKey(encPriv);
591
+ return {
592
+ signing: {
593
+ privateKey: sigPriv,
594
+ publicKey: sigPub
595
+ },
596
+ encryption: {
597
+ privateKey: encPriv,
598
+ publicKey: encPub
418
599
  }
419
- });
600
+ };
601
+ }
602
+ function agentKeysExist(agentId) {
603
+ const dir = agentKeysDir(agentId);
604
+ return fs.existsSync(join(dir, "signing.key")) && fs.existsSync(join(dir, "encryption.key"));
420
605
  }
421
606
  //#endregion
422
607
  //#region src/prompt.ts
@@ -466,112 +651,96 @@ async function promptConfirm(question) {
466
651
  //#region src/commands/agent.ts
467
652
  function toAgentRow(a, opts = {}) {
468
653
  const row = {
469
- ID: String(a.id ?? ""),
470
- Name: String(a.name ?? ""),
471
- Handle: String(a.handle ?? ""),
472
- "Human Oversight": String(a.human_oversight ?? ""),
473
- Created: String(a.created_at ?? "")
654
+ ID: a.id,
655
+ Name: a.name,
656
+ Handle: a.handle,
657
+ "Human Oversight": String(a.human_oversight),
658
+ Created: a.created_at
474
659
  };
475
- if (opts.includeVerificationWords && a.verification_words) row["Verification Words"] = String(a.verification_words);
660
+ if (opts.includeVerificationWords && a.verification_words) row["Verification Words"] = a.verification_words;
476
661
  if (opts.includeCard) row.Card = a.unlisted ? "unlisted" : "public";
477
662
  if (opts.includeRevoked) row.Revoked = a.revoked_at ? String(a.revoked_at) : "—";
478
663
  return row;
479
664
  }
480
665
  function registerAgent(program) {
481
666
  const agent = program.command("agent").description("Agent management");
482
- agent.command("create").description("Create a new agent").option("--name <name>", "Agent name").option("--human-oversight <bool>", "Require human oversight (true/false)").option("--unlisted", "Mark agent as unlisted (not in public directory)").action(async (opts) => {
483
- try {
484
- const gOpts = program.opts();
485
- let name = opts.name;
667
+ agent.command("create").description("Create a new agent").option("--name <name>", "Agent name").option("--[no-]human-oversight", "Require human oversight (default: true)").option("--unlisted", "Mark agent as unlisted (not in public directory)").action(withClient(program, async ({ client, gOpts }, opts) => {
668
+ let name = opts.name;
669
+ if (!name) {
670
+ name = await promptText("Agent name: ");
486
671
  if (!name) {
487
- name = await promptText("Agent name: ");
488
- if (!name) {
489
- printError("Agent name is required");
490
- process.exitCode = 2;
491
- return;
492
- }
493
- }
494
- const body = { name };
495
- if (opts.humanOversight !== void 0) body.human_oversight = opts.humanOversight === "true";
496
- if (opts.unlisted) body.unlisted = true;
497
- const { client } = await createClient(gOpts.profile);
498
- const data = await client.post("/agents", body);
499
- if (gOpts.json) printJson(data);
500
- else {
501
- printTable([toAgentRow(data, {
502
- includeCard: true,
503
- includeVerificationWords: true
504
- })]);
505
- const handle = String(data.handle ?? "");
506
- const name = String(data.name ?? "");
507
- console.log(`
508
- Agent '${name}' created — reachable at ${handle}`);
509
- if (data.verification_words) console.log(`Verification words: ${data.verification_words}`);
672
+ printError("Agent name is required");
673
+ process.exitCode = 2;
674
+ return;
510
675
  }
511
- } catch (err) {
512
- printError(formatError(err));
513
- process.exitCode = 1;
514
676
  }
515
- });
516
- agent.command("list").description("List all agents").option("--include-revoked", "Include revoked agents").action(async (opts) => {
517
- try {
518
- const gOpts = program.opts();
519
- const { client } = await createClient(gOpts.profile);
520
- const params = opts.includeRevoked ? { include_revoked: true } : void 0;
521
- const data = await client.get("/agents", params);
522
- if (gOpts.json) printJson(data);
523
- else printTable(data.items.map((a) => toAgentRow(a, { includeRevoked: opts.includeRevoked })));
524
- } catch (err) {
525
- printError(formatError(err));
526
- process.exitCode = 1;
677
+ const agentKeys = generateAgentKeys();
678
+ const body = {
679
+ name,
680
+ signing_public_key: signingPublicKeyToJWK(agentKeys.signing.publicKey),
681
+ encryption_public_key: encryptionPublicKeyToJWK(agentKeys.encryption.publicKey)
682
+ };
683
+ if (opts.humanOversight !== void 0) body.human_oversight = opts.humanOversight;
684
+ if (opts.unlisted) body.unlisted = true;
685
+ const data = await client.post("/agents", body);
686
+ saveAgentKeys(data.id, agentKeys);
687
+ if (gOpts.json) printJson(data);
688
+ else {
689
+ printTable([toAgentRow(data, {
690
+ includeCard: true,
691
+ includeVerificationWords: true
692
+ })]);
693
+ console.log(`\nAgent '${data.name}' created \u2014 reachable at ${data.handle}`);
694
+ console.log("E2EE keys generated and stored locally.");
695
+ if (data.verification_words) console.log(`Verification words: ${data.verification_words}`);
527
696
  }
528
- });
529
- agent.command("get").description("Get agent details").argument("<agent-id>", "Agent ID").action(async (agentId) => {
530
- try {
531
- const gOpts = program.opts();
532
- const { client } = await createClient(gOpts.profile);
533
- const data = await client.get(`/agents/${agentId}`);
534
- if (gOpts.json) printJson(data);
535
- else printTable([toAgentRow(data, { includeRevoked: true })]);
536
- } catch (err) {
537
- printError(formatError(err));
538
- process.exitCode = 1;
697
+ }));
698
+ agent.command("list").description("List all agents").option("--include-revoked", "Include revoked agents").action(withClient(program, async ({ client, gOpts }, opts) => {
699
+ const params = opts.includeRevoked ? { include_revoked: true } : void 0;
700
+ const data = await client.get("/agents", params);
701
+ if (gOpts.json) printJson(data);
702
+ else printTable(data.items.map((a) => toAgentRow(a, { includeRevoked: opts.includeRevoked })));
703
+ }));
704
+ agent.command("get").description("Get agent details").argument("<agent-id>", "Agent ID").action(withClient(program, async ({ client, gOpts }, agentId) => {
705
+ const data = await client.get(`/agents/${agentId}`);
706
+ if (gOpts.json) printJson(data);
707
+ else printTable([{
708
+ ...toAgentRow(data, { includeRevoked: true }),
709
+ "Incoming Policy": data.incoming_policy ?? "accept_all",
710
+ "Outgoing Policy": data.outgoing_policy ?? "send_all"
711
+ }]);
712
+ }));
713
+ agent.command("update").description("Update agent details").argument("<agent-id>", "Agent ID").option("--name <name>", "Agent name").option("--[no-]human-oversight", "Require human oversight").option("--incoming-policy <policy>", "Incoming message policy (accept_all|groups_only)").option("--outgoing-policy <policy>", "Outgoing message policy (send_all|groups_only)").action(withClient(program, async ({ client, gOpts }, agentId, opts) => {
714
+ const body = {};
715
+ if (opts.name) body.name = opts.name;
716
+ if (opts.humanOversight !== void 0) body.human_oversight = opts.humanOversight;
717
+ if (opts.incomingPolicy) body.incoming_policy = opts.incomingPolicy;
718
+ if (opts.outgoingPolicy) body.outgoing_policy = opts.outgoingPolicy;
719
+ if (Object.keys(body).length === 0) {
720
+ printError("At least one of --name, --human-oversight, --incoming-policy, --outgoing-policy is required");
721
+ process.exitCode = 2;
722
+ return;
539
723
  }
540
- });
541
- agent.command("update").description("Update agent details").argument("<agent-id>", "Agent ID").option("--name <name>", "Agent name").option("--human-oversight <bool>", "Require human oversight (true/false)").action(async (agentId, opts) => {
542
- try {
543
- const gOpts = program.opts();
544
- const body = {};
545
- if (opts.name) body.name = opts.name;
546
- if (opts.humanOversight !== void 0) body.human_oversight = opts.humanOversight === "true";
547
- if (Object.keys(body).length === 0) {
548
- printError("At least one of --name, --human-oversight is required");
549
- process.exitCode = 2;
550
- return;
551
- }
552
- const { client } = await createClient(gOpts.profile);
553
- await client.patch(`/agents/${agentId}`, body);
724
+ const data = await client.patch(`/agents/${agentId}`, body);
725
+ if (gOpts.json) printJson(data);
726
+ else {
554
727
  printMutationOk("Agent updated", gOpts.json);
555
- } catch (err) {
556
- printError(formatError(err));
557
- process.exitCode = 1;
728
+ if (data.warnings && data.warnings.length > 0) for (const w of data.warnings) process.stderr.write(`Warning: ${w}\n`);
729
+ }
730
+ }));
731
+ agent.command("revoke").description("Revoke an agent").argument("<agent-id>", "Agent ID").option("--yes", "Skip confirmation prompt").action(withClient(program, async ({ client, gOpts }, agentId, opts) => {
732
+ if (!opts.yes && !gOpts.json) {
733
+ if (!await promptConfirm(`Revoke agent ${agentId}?`)) return;
558
734
  }
559
- });
560
- agent.command("revoke").description("Revoke an agent").argument("<agent-id>", "Agent ID").option("--yes", "Skip confirmation prompt").action(async (agentId, opts) => {
561
735
  try {
562
- const gOpts = program.opts();
563
- if (!opts.yes && !gOpts.json) {
564
- if (!await promptConfirm(`Revoke agent ${agentId}?`)) return;
565
- }
566
- const { client } = await createClient(gOpts.profile);
567
736
  await client.delete(`/agents/${agentId}`);
568
737
  printMutationOk("Agent revoked", gOpts.json);
569
738
  } catch (err) {
570
739
  if (err instanceof RineApiError && err.status === 409) printError("Agent already revoked");
571
- else printError(formatError(err));
740
+ else throw err;
572
741
  process.exitCode = 1;
573
742
  }
574
- });
743
+ }));
575
744
  }
576
745
  //#endregion
577
746
  //#region src/commands/auth.ts
@@ -581,126 +750,114 @@ function registerAuth(program) {
581
750
  const profile = program.opts().profile ?? "default";
582
751
  const clientId = opts.clientId ?? await promptText("Client ID: ");
583
752
  const clientSecret = opts.clientSecret ?? await promptPassword("Client secret: ");
584
- const apiUrl = getApiUrl();
585
- const res = await fetch(`${apiUrl}/oauth/token`, {
586
- method: "POST",
587
- headers: { "Content-Type": "application/x-www-form-urlencoded" },
588
- body: new URLSearchParams({
589
- grant_type: "client_credentials",
590
- client_id: clientId,
591
- client_secret: clientSecret
592
- }).toString()
593
- });
594
- if (!res.ok) {
595
- printError(`Login failed: ${(await res.json().catch(() => ({}))).detail ?? res.statusText}`);
596
- process.exitCode = 1;
597
- return;
598
- }
599
- const tokenData = await res.json();
753
+ const tokenData = await fetchOAuthToken(clientId, clientSecret);
600
754
  const creds = loadCredentials();
601
755
  creds[profile] = {
602
756
  client_id: clientId,
603
757
  client_secret: clientSecret
604
758
  };
605
759
  saveCredentials(creds);
606
- const cache = loadTokenCache();
607
- cache[profile] = {
608
- access_token: tokenData.access_token,
609
- expires_at: Date.now() / 1e3 + tokenData.expires_in
610
- };
611
- saveTokenCache(cache);
760
+ cacheToken(profile, tokenData);
612
761
  printSuccess(`Logged in as ${clientId}`);
613
762
  } catch (err) {
614
763
  printError(formatError(err));
615
764
  process.exitCode = 1;
616
765
  }
617
766
  });
618
- program.command("logout").description("Clear token cache for the current profile").action(() => {
619
- try {
620
- const profile = program.opts().profile ?? "default";
621
- const cache = loadTokenCache();
622
- delete cache[profile];
623
- saveTokenCache(cache);
624
- printSuccess("Logged out (credentials preserved — use 'rine auth token' to re-authenticate)");
625
- } catch (err) {
626
- printError(formatError(err));
627
- process.exitCode = 1;
628
- }
629
- });
630
- program.command("status").description("Show org status").action(async () => {
631
- await showOrgStatus(program);
632
- });
767
+ program.command("logout").description("Clear cached token. Credentials are preserved.").action(withErrorHandler(program, (gOpts) => {
768
+ const profile = gOpts.profile ?? "default";
769
+ const cache = loadTokenCache();
770
+ delete cache[profile];
771
+ saveTokenCache(cache);
772
+ printSuccess(`Logged out — token cache cleared for profile '${profile}'. Credentials preserved.`);
773
+ }));
774
+ program.command("status").description("Show org status").action(withClient(program, async ({ client, gOpts }) => {
775
+ await showOrgStatus(client, gOpts);
776
+ }));
633
777
  const authGroup = program.command("auth").description("Auth commands");
634
- authGroup.command("token").description("Get or refresh access token").option("--force", "Force token refresh (bypass cache)").action(async (opts) => {
635
- try {
636
- const gOpts = program.opts();
637
- const profile = gOpts.profile ?? "default";
638
- const force = opts.force === true;
639
- const now = Date.now() / 1e3;
640
- const envToken = process.env.RINE_TOKEN;
641
- const cached = loadTokenCache()[profile];
642
- const source = envToken !== void 0 || !force && cached !== void 0 && cached.expires_at - now > 60 ? "cache" : "server";
643
- const token = await getOrRefreshToken(getCredentialEntry(profile), profile, force);
644
- if (gOpts.json) printResult({
645
- access_token: token,
646
- source
647
- }, gOpts);
648
- else console.log(token);
649
- } catch (err) {
650
- printError(formatError(err));
651
- process.exitCode = 1;
778
+ authGroup.command("token").description("Get or refresh access token").option("--force", "Force token refresh (bypass cache)").action(withErrorHandler(program, async (gOpts, opts) => {
779
+ const profile = gOpts.profile ?? "default";
780
+ const force = opts.force === true;
781
+ const now = Date.now() / 1e3;
782
+ const envToken = process.env.RINE_TOKEN;
783
+ const cached = loadTokenCache()[profile];
784
+ const source = envToken !== void 0 || !force && cached !== void 0 && cached.expires_at - now > 60 ? "cache" : "server";
785
+ const token = await getOrRefreshToken(getCredentialEntry(profile), profile, force);
786
+ if (gOpts.json) printJson({
787
+ access_token: token,
788
+ source
789
+ });
790
+ else console.log(token);
791
+ }));
792
+ authGroup.command("status").description("Show org status").action(withClient(program, async ({ client, gOpts }) => {
793
+ await showOrgStatus(client, gOpts);
794
+ }));
795
+ }
796
+ async function showOrgStatus(client, gOpts) {
797
+ const data = await client.get("/org");
798
+ if (gOpts.json) {
799
+ printJson(data);
800
+ return;
801
+ }
802
+ printTable([
803
+ {
804
+ Field: "Name",
805
+ Value: data.name
806
+ },
807
+ {
808
+ Field: "Slug",
809
+ Value: data.slug
810
+ },
811
+ {
812
+ Field: "Trust Tier",
813
+ Value: String(data.trust_tier)
814
+ },
815
+ {
816
+ Field: "Agent Count",
817
+ Value: String(data.agent_count)
652
818
  }
653
- });
654
- authGroup.command("status").description("Show org status").action(async () => {
655
- await showOrgStatus(program);
656
- });
819
+ ]);
657
820
  }
658
- async function showOrgStatus(program) {
659
- try {
660
- const gOpts = program.opts();
661
- const { client } = await createClient(gOpts.profile);
662
- const data = await client.get("/org");
821
+ //#endregion
822
+ //#region src/commands/discover-groups.ts
823
+ function registerDiscoverGroups(discover, program) {
824
+ discover.command("groups").description("List public groups in the directory").option("-q, --query <query>", "Search query").option("--limit <n>", "Max results").option("--cursor <cursor>", "Pagination cursor").action(withErrorHandler(program, async (gOpts, opts) => {
825
+ const params = new URLSearchParams();
826
+ if (opts.query) params.append("q", opts.query);
827
+ if (opts.limit) params.append("limit", opts.limit);
828
+ if (opts.cursor) params.append("cursor", opts.cursor);
829
+ const data = await HttpClient.publicGet("/directory/groups", params.toString() ? params : void 0);
663
830
  if (gOpts.json) {
664
- printResult(data, gOpts);
831
+ printJson(data);
665
832
  return;
666
833
  }
667
- printTable([
668
- {
669
- Field: "Name",
670
- Value: String(data.name ?? "")
671
- },
672
- {
673
- Field: "Slug",
674
- Value: String(data.slug ?? "")
675
- },
676
- {
677
- Field: "Trust Tier",
678
- Value: String(data.trust_tier ?? "")
679
- },
680
- {
681
- Field: "Agent Count",
682
- Value: String(data.agent_count ?? "")
683
- }
684
- ]);
685
- } catch (err) {
686
- printError(formatError(err));
687
- process.exitCode = 1;
688
- }
834
+ printTable((data.items ?? []).map((g) => ({
835
+ Name: g.name,
836
+ Handle: g.handle,
837
+ Description: g.description ?? "",
838
+ Enrollment: g.enrollment_policy,
839
+ Members: g.member_count
840
+ })));
841
+ })).command("inspect").description("Inspect a public group").argument("<group-id>", "Group ID").action(withErrorHandler(program, async (gOpts, groupId) => {
842
+ const data = await HttpClient.publicGet(`/directory/groups/${groupId}`);
843
+ if (gOpts.json) {
844
+ printJson(data);
845
+ return;
846
+ }
847
+ printTable([{
848
+ ID: data.id,
849
+ Name: data.name,
850
+ Handle: data.handle,
851
+ Description: data.description ?? "",
852
+ "Org ID": data.org_id,
853
+ Enrollment: data.enrollment_policy,
854
+ Members: data.member_count,
855
+ Created: data.created_at
856
+ }]);
857
+ }));
689
858
  }
690
859
  //#endregion
691
860
  //#region src/commands/discover.ts
692
- const UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
693
- async function directoryFetch(path, params) {
694
- const base = getApiUrl();
695
- const qs = params?.toString();
696
- const url = qs ? `${base}${path}?${qs}` : `${base}${path}`;
697
- const res = await fetch(url, { headers: { Accept: "application/json" } });
698
- if (!res.ok) {
699
- const body = await res.text().catch(() => res.statusText);
700
- throw new RineApiError(res.status, body, res);
701
- }
702
- return res.json();
703
- }
704
861
  function trunc(s, len) {
705
862
  const str = String(s ?? "");
706
863
  return str.length > len ? `${str.slice(0, len - 1)}\u2026` : str;
@@ -720,7 +877,7 @@ async function doAgentSearch(opts, gOpts) {
720
877
  if (opts.limit) params.append("limit", opts.limit);
721
878
  if (opts.cursor) params.append("cursor", opts.cursor);
722
879
  if (opts.sort) params.append("sort", opts.sort);
723
- const data = await directoryFetch("/directory/agents", params);
880
+ const data = await HttpClient.publicGet("/directory/agents", params);
724
881
  if (gOpts.json) {
725
882
  printJson(data);
726
883
  return;
@@ -736,132 +893,832 @@ async function doAgentSearch(opts, gOpts) {
736
893
  if (result.next_cursor) printText(`Next cursor: ${result.next_cursor}`);
737
894
  }
738
895
  const collect = (v, prev) => [...prev, v];
739
- function addSearchOptions(cmd, requireQuery) {
896
+ function addAgentSearchCommand(parent, name, description, program, requireQuery) {
897
+ const cmd = parent.command(name).description(description);
740
898
  if (requireQuery) cmd.requiredOption("-q, --query <query>", "Search query");
741
899
  else cmd.option("-q, --query <query>", "Search query");
742
- return cmd.option("--category <cat>", "Category filter (repeatable)", collect, []).option("--tag <tag>", "Tag filter (repeatable)", collect, []).option("--jurisdiction <j>", "Jurisdiction filter").option("--language <lang>", "Language filter (repeatable)", collect, []).option("--verified", "Only verified agents").option("--pricing-model <model>", "Pricing model filter").option("--limit <n>", "Max results").option("--cursor <cursor>", "Pagination cursor").option("--sort <sort>", "Sort order (relevance|name|created_at)");
900
+ cmd.option("--category <cat>", "Category filter (repeatable)", collect, []).option("--tag <tag>", "Tag filter (repeatable)", collect, []).option("--jurisdiction <j>", "Jurisdiction filter").option("--language <lang>", "Language filter (repeatable)", collect, []).option("--verified", "Only verified agents").option("--no-verified", "Only unverified agents").option("--pricing-model <model>", "Pricing model filter").option("--limit <n>", "Max results").option("--cursor <cursor>", "Pagination cursor").option("--sort <sort>", "Sort order (relevance|name|created_at)").action(withErrorHandler(program, async (gOpts, opts) => {
901
+ await doAgentSearch(opts, gOpts);
902
+ }));
743
903
  }
744
904
  function registerDiscover(program) {
745
- const discover = program.command("discover").description("Discover agents in the directory");
746
- addSearchOptions(discover.command("agents").description("List agents in the directory"), false).action(async (opts) => {
747
- const gOpts = program.opts();
748
- try {
749
- await doAgentSearch(opts, gOpts);
750
- } catch (err) {
751
- printError(formatError(err));
752
- process.exitCode = 1;
905
+ const discover = program.command("discover").description("Discover agents and groups in the directory");
906
+ addAgentSearchCommand(discover, "agents", "List agents in the directory", program, false);
907
+ addAgentSearchCommand(discover, "search", "Search agents (--query required)", program, true);
908
+ discover.command("categories").description("List directory categories with counts").action(withErrorHandler(program, async (gOpts) => {
909
+ const data = await HttpClient.publicGet("/directory/categories");
910
+ if (gOpts.json) {
911
+ printJson(data);
912
+ return;
753
913
  }
754
- });
755
- addSearchOptions(discover.command("search").description("Search agents (--query required)"), true).action(async (opts) => {
756
- const gOpts = program.opts();
757
- try {
758
- await doAgentSearch(opts, gOpts);
759
- } catch (err) {
760
- printError(formatError(err));
914
+ printTable((data.items ?? []).sort((a, b) => a.name.localeCompare(b.name)).map((c) => ({
915
+ Category: c.name,
916
+ Count: c.count
917
+ })));
918
+ }));
919
+ discover.command("inspect").description("Inspect a specific agent in the directory").argument("<agent-id>", "Agent UUID or handle").action(withErrorHandler(program, async (gOpts, agentIdArg) => {
920
+ const UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
921
+ let resolvedId = agentIdArg;
922
+ if (agentIdArg.includes("@")) resolvedId = await resolveHandleViaWebFinger(agentIdArg);
923
+ if (!UUID_RE.test(resolvedId)) {
924
+ printError(`Could not resolve '${agentIdArg}' to an agent UUID. Provide a UUID or a valid handle (e.g. agent@org.rine.network).`);
761
925
  process.exitCode = 1;
926
+ return;
762
927
  }
763
- });
764
- discover.command("categories").description("List directory categories with counts").action(async () => {
765
- const gOpts = program.opts();
766
- try {
767
- const data = await directoryFetch("/directory/categories");
768
- if (gOpts.json) {
769
- printJson(data);
770
- return;
771
- }
772
- printTable((data.items ?? []).sort((a, b) => a.name.localeCompare(b.name)).map((c) => ({
773
- Category: c.name,
774
- Count: c.count
775
- })));
776
- } catch (err) {
777
- printError(formatError(err));
778
- process.exitCode = 1;
928
+ const data = await HttpClient.publicGet(`/directory/agents/${resolvedId}`);
929
+ if (gOpts.json) {
930
+ printJson(data);
931
+ return;
932
+ }
933
+ const result = data;
934
+ const card = result.card ?? data;
935
+ const meta = result.directory_metadata ?? {};
936
+ printTable([{
937
+ Name: String(card.name ?? ""),
938
+ Description: trunc(card.description, 120),
939
+ Categories: fmtList(card.rine?.categories ?? card.categories),
940
+ Tags: fmtList(card.rine?.tags ?? card.tags),
941
+ Pricing: String(card.rine?.pricing_model ?? card.pricing_model ?? ""),
942
+ "Registered At": String(meta.registered_at ?? ""),
943
+ "Messages (30d)": String(meta.messages_processed_30d ?? ""),
944
+ "Avg Response (ms)": String(meta.avg_response_time_ms ?? "")
945
+ }]);
946
+ }));
947
+ registerDiscoverGroups(discover, program);
948
+ }
949
+ //#endregion
950
+ //#region src/commands/group.ts
951
+ function groupRow(g) {
952
+ return {
953
+ ID: g.id,
954
+ Name: g.name,
955
+ Handle: g.handle,
956
+ Enrollment: g.enrollment_policy,
957
+ Visibility: g.visibility,
958
+ Members: g.member_count,
959
+ Created: g.created_at
960
+ };
961
+ }
962
+ function groupDetailRow(g) {
963
+ return {
964
+ ID: g.id,
965
+ Name: g.name,
966
+ Handle: g.handle,
967
+ Description: g.description ?? "",
968
+ Enrollment: g.enrollment_policy,
969
+ Visibility: g.visibility,
970
+ Isolated: g.isolated,
971
+ "Vote Duration": g.vote_duration_hours,
972
+ Members: g.member_count,
973
+ Created: g.created_at
974
+ };
975
+ }
976
+ function memberRow(m) {
977
+ return {
978
+ "Agent ID": m.agent_id,
979
+ Handle: m.agent_handle ?? "",
980
+ Role: m.role,
981
+ Joined: m.joined_at
982
+ };
983
+ }
984
+ function requestRow(r) {
985
+ return {
986
+ "Request ID": r.id,
987
+ "Agent ID": r.agent_id,
988
+ Message: r.message ?? "",
989
+ "Your Vote": r.your_vote ?? "",
990
+ Expires: r.expires_at ?? "",
991
+ Created: r.created_at
992
+ };
993
+ }
994
+ function registerGroup(program) {
995
+ const group = program.command("group").description("Group management");
996
+ group.command("create").description("Create a new group").requiredOption("--name <name>", "Group name (DNS-safe slug)").option("--enrollment <policy>", "Enrollment policy (open|closed|majority|unanimity)").option("--visibility <vis>", "Visibility (public|private)").option("--isolated", "Isolate group communication").option("--vote-duration <hours>", "Vote duration in hours (1-72)").action(withClient(program, async ({ client, gOpts }, opts) => {
997
+ const body = { name: opts.name };
998
+ if (opts.enrollment) body.enrollment_policy = opts.enrollment;
999
+ if (opts.visibility) body.visibility = opts.visibility;
1000
+ else {
1001
+ body.visibility = opts.enrollment === "open" ? "public" : "private";
1002
+ if (!gOpts.json) printText(`Visibility defaulted to "${body.visibility}" (enrollment: ${opts.enrollment ?? "closed"})`);
1003
+ }
1004
+ if (opts.isolated) body.isolated = true;
1005
+ if (opts.voteDuration) body.vote_duration_hours = Number(opts.voteDuration);
1006
+ const data = await client.post("/groups", body);
1007
+ if (gOpts.json) printJson(data);
1008
+ else {
1009
+ printTable([groupDetailRow(data)]);
1010
+ printText(`\nGroup created — handle: ${data.handle}`);
779
1011
  }
1012
+ }));
1013
+ group.command("list").description("List groups").action(withClient(program, async ({ client, gOpts }) => {
1014
+ const data = await client.get("/groups");
1015
+ if (gOpts.json) printJson(data);
1016
+ else printTable(data.items.map(groupRow));
1017
+ }));
1018
+ group.command("get").description("Get group details").argument("<group-id>", "Group ID").action(withClient(program, async ({ client, gOpts }, groupId) => {
1019
+ const data = await client.get(`/groups/${groupId}`);
1020
+ if (gOpts.json) printJson(data);
1021
+ else printTable([groupDetailRow(data)]);
1022
+ }));
1023
+ group.command("update").description("Update group settings").argument("<group-id>", "Group ID").option("--description <desc>", "Group description").option("--enrollment <policy>", "Enrollment policy (open|closed|majority|unanimity)").option("--visibility <vis>", "Visibility (public|private)").option("--vote-duration <hours>", "Vote duration in hours (1-72)").action(withClient(program, async ({ client, gOpts }, groupId, opts) => {
1024
+ const body = {};
1025
+ if (opts.description !== void 0) body.description = opts.description;
1026
+ if (opts.enrollment) body.enrollment_policy = opts.enrollment;
1027
+ if (opts.visibility) body.visibility = opts.visibility;
1028
+ if (opts.voteDuration) body.vote_duration_hours = Number(opts.voteDuration);
1029
+ if (Object.keys(body).length === 0) {
1030
+ printError("At least one of --description, --enrollment, --visibility, --vote-duration is required");
1031
+ process.exitCode = 2;
1032
+ return;
1033
+ }
1034
+ const data = await client.patch(`/groups/${groupId}`, body);
1035
+ if (gOpts.json) printJson(data);
1036
+ else {
1037
+ printTable([groupDetailRow(data)]);
1038
+ printText("Group updated");
1039
+ }
1040
+ }));
1041
+ group.command("delete").description("Delete a group").argument("<group-id>", "Group ID").option("--yes", "Skip confirmation prompt").action(withClient(program, async ({ client, gOpts }, groupId, opts) => {
1042
+ if (!opts.yes && !gOpts.json) {
1043
+ if (!await promptConfirm(`Delete group ${groupId}?`)) return;
1044
+ }
1045
+ await client.delete(`/groups/${groupId}`);
1046
+ printMutationOk("Group deleted", gOpts.json);
1047
+ }));
1048
+ group.command("members").description("List group members").argument("<group-id>", "Group ID").action(withClient(program, async ({ client, gOpts }, groupId) => {
1049
+ const data = await client.get(`/groups/${groupId}/members`);
1050
+ if (gOpts.json) printJson(data);
1051
+ else printTable(data.items.map(memberRow));
1052
+ }));
1053
+ group.command("join").description("Join a group").argument("<group-id>", "Group ID").option("--message <msg>", "Join request message").action(withClient(program, async ({ client, gOpts }, groupId, opts) => {
1054
+ const body = {};
1055
+ if (opts.message) body.message = opts.message;
1056
+ const data = await client.post(`/groups/${groupId}/join`, body);
1057
+ if (gOpts.json) printJson(data);
1058
+ else if ("role" in data) {
1059
+ printTable([memberRow(data)]);
1060
+ printText("Joined group (member)");
1061
+ } else {
1062
+ printTable([requestRow(data)]);
1063
+ printText("Join request submitted (pending approval)");
1064
+ }
1065
+ }));
1066
+ group.command("leave").description("Leave a group").argument("<group-id>", "Group ID").option("--agent <id>", "Agent ID (defaults to your agent)").action(withClient(program, async ({ client, gOpts }, groupId, opts) => {
1067
+ const agentId = await resolveAgent(client, opts.agent, gOpts.as);
1068
+ await client.delete(`/groups/${groupId}/members/${agentId}`);
1069
+ printMutationOk("Left group", gOpts.json);
1070
+ }));
1071
+ group.command("kick").description("Remove a member from the group (admin)").argument("<group-id>", "Group ID").argument("<agent-id>", "Agent ID to remove").action(withClient(program, async ({ client, gOpts }, groupId, agentId) => {
1072
+ await client.delete(`/groups/${groupId}/members/${agentId}`);
1073
+ printMutationOk("Member removed", gOpts.json);
1074
+ }));
1075
+ group.command("invite").description("Invite an agent to the group").argument("<group-id>", "Group ID").requiredOption("--agent <id>", "Agent ID to invite").option("--message <msg>", "Invitation message").action(withClient(program, async ({ client, gOpts }, groupId, opts) => {
1076
+ const body = { agent_id: opts.agent };
1077
+ if (opts.message) body.message = opts.message;
1078
+ const data = await client.post(`/groups/${groupId}/invite`, body);
1079
+ if (gOpts.json) printJson(data);
1080
+ else {
1081
+ printText("Invitation sent");
1082
+ if (data.id) printTable([requestRow(data)]);
1083
+ }
1084
+ }));
1085
+ group.command("requests").description("List pending join requests").argument("<group-id>", "Group ID").action(withClient(program, async ({ client, gOpts }, groupId) => {
1086
+ const data = await client.get(`/groups/${groupId}/requests`);
1087
+ if (gOpts.json) printJson(data);
1088
+ else printTable(data.items.map(requestRow));
1089
+ }));
1090
+ group.command("vote").description("Vote on a join request").argument("<group-id>", "Group ID").argument("<request-id>", "Join request ID").requiredOption("--vote <vote>", "Vote: approve or deny").action(withClient(program, async ({ client, gOpts }, groupId, requestId, opts) => {
1091
+ if (opts.vote !== "approve" && opts.vote !== "deny") {
1092
+ printError("--vote must be 'approve' or 'deny'");
1093
+ process.exitCode = 2;
1094
+ return;
1095
+ }
1096
+ const data = await client.post(`/groups/${groupId}/requests/${requestId}/vote`, { vote: opts.vote });
1097
+ if (gOpts.json) printJson(data);
1098
+ else printText(`Vote recorded. Request status: ${data.status}`);
1099
+ }));
1100
+ }
1101
+ //#endregion
1102
+ //#region src/crypto/envelope.ts
1103
+ const SIGNATURE_SIZE = 64;
1104
+ const MAX_KID_LENGTH = 255;
1105
+ function encodeEnvelope(kid, signature, payload) {
1106
+ const kidBytes = new TextEncoder().encode(kid);
1107
+ if (kidBytes.length > MAX_KID_LENGTH) throw new Error(`kid too long: ${kidBytes.length} bytes (max ${MAX_KID_LENGTH})`);
1108
+ if (signature.length !== SIGNATURE_SIZE) throw new Error(`signature must be ${SIGNATURE_SIZE} bytes, got ${signature.length}`);
1109
+ const buf = new Uint8Array(1 + kidBytes.length + SIGNATURE_SIZE + payload.length);
1110
+ buf[0] = kidBytes.length;
1111
+ buf.set(kidBytes, 1);
1112
+ buf.set(signature, 1 + kidBytes.length);
1113
+ buf.set(payload, 1 + kidBytes.length + SIGNATURE_SIZE);
1114
+ return buf;
1115
+ }
1116
+ function decodeEnvelope(data) {
1117
+ if (data.length < 1 + SIGNATURE_SIZE) throw new Error("envelope too short");
1118
+ const kidLen = data[0];
1119
+ const minLen = 1 + kidLen + SIGNATURE_SIZE;
1120
+ if (data.length < minLen) throw new Error("envelope too short for declared kid length");
1121
+ return {
1122
+ kid: new TextDecoder("utf-8", { fatal: true }).decode(data.subarray(1, 1 + kidLen)),
1123
+ signature: data.slice(1 + kidLen, 1 + kidLen + SIGNATURE_SIZE),
1124
+ payload: data.slice(1 + kidLen + SIGNATURE_SIZE)
1125
+ };
1126
+ }
1127
+ //#endregion
1128
+ //#region src/crypto/hpke.ts
1129
+ const VERSION_HPKE = 1;
1130
+ const ENC_SIZE = 32;
1131
+ const INFO = new TextEncoder().encode("rine.e2ee.v1.hpke");
1132
+ const suite = new CipherSuite({
1133
+ kem: new DhkemX25519HkdfSha256(),
1134
+ kdf: new HkdfSha256(),
1135
+ aead: new Aes256Gcm()
1136
+ });
1137
+ async function seal(recipientPublicKey, innerEnvelope, aad) {
1138
+ const pk = await suite.kem.deserializePublicKey(recipientPublicKey);
1139
+ const sender = await suite.createSenderContext({
1140
+ recipientPublicKey: pk,
1141
+ info: INFO
780
1142
  });
781
- discover.command("inspect").description("Inspect a specific agent in the directory").argument("<agent-id>", "Agent UUID or handle").action(async (agentId) => {
782
- const gOpts = program.opts();
783
- if (agentId.includes("@")) {
784
- const match = ((await directoryFetch("/directory/agents", new URLSearchParams({
785
- q: agentId,
786
- limit: "5"
787
- }))).items ?? []).find((i) => i.handle === agentId);
788
- if (!match?.id) {
789
- printError(`No agent found with handle: ${agentId}`);
790
- process.exitCode = 1;
791
- return;
792
- }
793
- agentId = match.id;
1143
+ const ciphertext = new Uint8Array(await sender.seal(innerEnvelope, aad));
1144
+ const enc = new Uint8Array(sender.enc);
1145
+ const out = new Uint8Array(1 + ENC_SIZE + ciphertext.length);
1146
+ out[0] = VERSION_HPKE;
1147
+ out.set(enc, 1);
1148
+ out.set(ciphertext, 1 + ENC_SIZE);
1149
+ return out;
1150
+ }
1151
+ async function open(recipientPrivateKey, encryptedPayload, aad) {
1152
+ if (encryptedPayload.length < 1 + ENC_SIZE + 1) throw new Error("encrypted payload too short");
1153
+ const version = encryptedPayload[0];
1154
+ if (version !== VERSION_HPKE) throw new Error(`unsupported version: 0x${version?.toString(16).padStart(2, "0")}`);
1155
+ const enc = encryptedPayload.slice(1, 1 + ENC_SIZE);
1156
+ const ct = encryptedPayload.slice(1 + ENC_SIZE);
1157
+ const sk = await suite.kem.deserializePrivateKey(recipientPrivateKey);
1158
+ const recipient = await suite.createRecipientContext({
1159
+ recipientKey: sk,
1160
+ enc,
1161
+ info: INFO
1162
+ });
1163
+ return new Uint8Array(await recipient.open(ct, aad));
1164
+ }
1165
+ //#endregion
1166
+ //#region src/crypto/sender-keys-helpers.ts
1167
+ function uuidToBytes(uuid) {
1168
+ const matches = uuid.replace(/-/g, "").match(/../g);
1169
+ if (!matches) throw new Error(`Invalid UUID: ${uuid}`);
1170
+ return new Uint8Array(matches.map((b) => Number.parseInt(b, 16)));
1171
+ }
1172
+ function bytesToUuid(bytes) {
1173
+ const hex = Array.from(bytes).map((b) => b.toString(16).padStart(2, "0")).join("");
1174
+ return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20)}`;
1175
+ }
1176
+ function senderKeysDir(agentId) {
1177
+ validatePathId(agentId, "agent ID");
1178
+ return join(getConfigDir(), "keys", agentId, "sender_keys");
1179
+ }
1180
+ function saveSenderKeyState(agentId, groupId, states) {
1181
+ validatePathId(groupId, "group ID");
1182
+ const dir = senderKeysDir(agentId);
1183
+ fs.mkdirSync(dir, {
1184
+ recursive: true,
1185
+ mode: 448
1186
+ });
1187
+ const filePath = join(dir, `${groupId}.json`);
1188
+ const data = states.map((s) => ({
1189
+ ...s,
1190
+ chainKey: toBase64Url(s.chainKey)
1191
+ }));
1192
+ fs.writeFileSync(filePath, JSON.stringify(data), "utf-8");
1193
+ fs.chmodSync(filePath, 384);
1194
+ }
1195
+ function loadSenderKeyStates(agentId, groupId) {
1196
+ validatePathId(groupId, "group ID");
1197
+ const filePath = join(senderKeysDir(agentId), `${groupId}.json`);
1198
+ if (!fs.existsSync(filePath)) return [];
1199
+ const raw = fs.readFileSync(filePath, "utf-8");
1200
+ return JSON.parse(raw).map((s) => ({
1201
+ ...s,
1202
+ chainKey: fromBase64Url(s.chainKey)
1203
+ }));
1204
+ }
1205
+ //#endregion
1206
+ //#region src/crypto/sign.ts
1207
+ function signPayload(signingPrivateKey, plaintext) {
1208
+ return ed25519.sign(plaintext, signingPrivateKey);
1209
+ }
1210
+ function verifySignature(signingPublicKey, plaintext, signature) {
1211
+ try {
1212
+ return ed25519.verify(signature, plaintext, signingPublicKey);
1213
+ } catch {
1214
+ return false;
1215
+ }
1216
+ }
1217
+ //#endregion
1218
+ //#region src/crypto/sender-keys.ts
1219
+ const ROTATION_MESSAGE_LIMIT = 100;
1220
+ const ROTATION_AGE_MS = 10080 * 60 * 1e3;
1221
+ function needsRotation(state) {
1222
+ if (state.messageIndex >= ROTATION_MESSAGE_LIMIT) return true;
1223
+ if (state.createdAt && Date.now() - state.createdAt > ROTATION_AGE_MS) return true;
1224
+ return false;
1225
+ }
1226
+ const VERSION_SENDER_KEY = 2;
1227
+ const SENDER_KEY_ID_SIZE = 16;
1228
+ const MESSAGE_INDEX_SIZE = 4;
1229
+ const NONCE_SIZE = 12;
1230
+ const MAX_SKIP = 1e3;
1231
+ /** Copy Uint8Array into a plain ArrayBuffer-backed Uint8Array (required by Web Crypto). */
1232
+ function toPlainBuffer(src) {
1233
+ const buf = new Uint8Array(src.length);
1234
+ buf.set(src);
1235
+ return buf;
1236
+ }
1237
+ function generateSenderKey(groupId, senderAgentId) {
1238
+ const chainKey = new Uint8Array(32);
1239
+ crypto.getRandomValues(chainKey);
1240
+ return {
1241
+ senderKeyId: crypto.randomUUID(),
1242
+ groupId,
1243
+ senderAgentId,
1244
+ chainKey,
1245
+ messageIndex: 0,
1246
+ createdAt: Date.now()
1247
+ };
1248
+ }
1249
+ function deriveMessageKey(chainKey) {
1250
+ return {
1251
+ messageKey: hmac(sha256, chainKey, new Uint8Array([1])),
1252
+ nextChainKey: hmac(sha256, chainKey, new Uint8Array([2]))
1253
+ };
1254
+ }
1255
+ function advanceChain(state, targetIndex) {
1256
+ if (targetIndex - state.messageIndex > MAX_SKIP) throw new Error(`too many skipped messages: ${targetIndex - state.messageIndex} (max ${MAX_SKIP})`);
1257
+ const skippedKeys = /* @__PURE__ */ new Map();
1258
+ let chainKey = state.chainKey;
1259
+ let index = state.messageIndex;
1260
+ while (index < targetIndex) {
1261
+ const { messageKey, nextChainKey } = deriveMessageKey(chainKey);
1262
+ skippedKeys.set(index, messageKey);
1263
+ chainKey = nextChainKey;
1264
+ index++;
1265
+ }
1266
+ return {
1267
+ updatedState: {
1268
+ ...state,
1269
+ chainKey,
1270
+ messageIndex: index
1271
+ },
1272
+ skippedKeys
1273
+ };
1274
+ }
1275
+ /** Build AAD for sender-key encryption: binds ciphertext to sender_key_id and message_index. */
1276
+ function buildSenderKeyAad(senderKeyId, messageIndex) {
1277
+ const aad = new Uint8Array(SENDER_KEY_ID_SIZE + MESSAGE_INDEX_SIZE);
1278
+ aad.set(senderKeyId, 0);
1279
+ new DataView(aad.buffer).setUint32(SENDER_KEY_ID_SIZE, messageIndex, false);
1280
+ return aad;
1281
+ }
1282
+ async function sealGroup(senderSigningPrivateKey, senderSigningKid, state, plaintext) {
1283
+ const innerEnvelope = encodeEnvelope(senderSigningKid, signPayload(senderSigningPrivateKey, plaintext), plaintext);
1284
+ const { messageKey, nextChainKey } = deriveMessageKey(state.chainKey);
1285
+ const nonce = new Uint8Array(NONCE_SIZE);
1286
+ crypto.getRandomValues(nonce);
1287
+ const idBytes = uuidToBytes(state.senderKeyId);
1288
+ const aad = buildSenderKeyAad(idBytes, state.messageIndex);
1289
+ const cryptoKey = await crypto.subtle.importKey("raw", toPlainBuffer(messageKey), "AES-GCM", false, ["encrypt"]);
1290
+ const ciphertext = new Uint8Array(await crypto.subtle.encrypt({
1291
+ name: "AES-GCM",
1292
+ iv: nonce,
1293
+ additionalData: toPlainBuffer(aad)
1294
+ }, cryptoKey, toPlainBuffer(innerEnvelope)));
1295
+ messageKey.fill(0);
1296
+ const out = new Uint8Array(1 + SENDER_KEY_ID_SIZE + MESSAGE_INDEX_SIZE + NONCE_SIZE + ciphertext.length);
1297
+ let offset = 0;
1298
+ out[offset++] = VERSION_SENDER_KEY;
1299
+ out.set(idBytes, offset);
1300
+ offset += SENDER_KEY_ID_SIZE;
1301
+ new DataView(out.buffer).setUint32(offset, state.messageIndex, false);
1302
+ offset += MESSAGE_INDEX_SIZE;
1303
+ out.set(nonce, offset);
1304
+ offset += NONCE_SIZE;
1305
+ out.set(ciphertext, offset);
1306
+ return {
1307
+ encryptedPayload: out,
1308
+ updatedState: {
1309
+ ...state,
1310
+ chainKey: nextChainKey,
1311
+ messageIndex: state.messageIndex + 1
1312
+ }
1313
+ };
1314
+ }
1315
+ async function openGroup(senderKeyStates, encryptedPayload) {
1316
+ const minLen = 1 + SENDER_KEY_ID_SIZE + MESSAGE_INDEX_SIZE + NONCE_SIZE;
1317
+ if (encryptedPayload.length < minLen) throw new Error("encrypted payload too short");
1318
+ const version = encryptedPayload[0];
1319
+ if (version !== VERSION_SENDER_KEY) throw new Error(`unsupported version: 0x${version?.toString(16).padStart(2, "0")}`);
1320
+ let offset = 1;
1321
+ const senderKeyId = bytesToUuid(encryptedPayload.slice(offset, offset + SENDER_KEY_ID_SIZE));
1322
+ offset += SENDER_KEY_ID_SIZE;
1323
+ const messageIndex = new DataView(encryptedPayload.buffer, encryptedPayload.byteOffset).getUint32(offset, false);
1324
+ offset += MESSAGE_INDEX_SIZE;
1325
+ const nonce = encryptedPayload.slice(offset, offset + NONCE_SIZE);
1326
+ offset += NONCE_SIZE;
1327
+ const ciphertext = encryptedPayload.slice(offset);
1328
+ const state = senderKeyStates.get(senderKeyId);
1329
+ if (!state) throw new Error(`unknown sender key id: ${senderKeyId}`);
1330
+ let messageKey;
1331
+ let updatedState;
1332
+ if (messageIndex < state.messageIndex) {
1333
+ const cached = state.skippedKeys?.[messageIndex];
1334
+ if (!cached) throw new Error("message key expired or already consumed");
1335
+ const { fromBase64Url } = await Promise.resolve().then(() => keys_exports);
1336
+ messageKey = fromBase64Url(cached);
1337
+ const newSkipped = { ...state.skippedKeys };
1338
+ delete newSkipped[messageIndex];
1339
+ updatedState = {
1340
+ ...state,
1341
+ skippedKeys: newSkipped
1342
+ };
1343
+ } else if (messageIndex === state.messageIndex) {
1344
+ const { messageKey: mk, nextChainKey } = deriveMessageKey(state.chainKey);
1345
+ messageKey = mk;
1346
+ updatedState = {
1347
+ ...state,
1348
+ chainKey: nextChainKey,
1349
+ messageIndex: state.messageIndex + 1
1350
+ };
1351
+ } else {
1352
+ const { updatedState: advanced, skippedKeys: newSkipped } = advanceChain(state, messageIndex);
1353
+ const { messageKey: mk, nextChainKey } = deriveMessageKey(advanced.chainKey);
1354
+ messageKey = mk;
1355
+ const mergedSkipped = { ...state.skippedKeys ?? {} };
1356
+ for (const [idx, key] of newSkipped) {
1357
+ mergedSkipped[idx] = toBase64Url(key);
1358
+ key.fill(0);
1359
+ }
1360
+ const entries = Object.entries(mergedSkipped);
1361
+ if (entries.length > MAX_SKIP) {
1362
+ entries.sort((a, b) => Number(a[0]) - Number(b[0]));
1363
+ const excess = entries.length - MAX_SKIP;
1364
+ for (let i = 0; i < excess; i++) delete mergedSkipped[Number(entries[i][0])];
1365
+ }
1366
+ updatedState = {
1367
+ ...advanced,
1368
+ chainKey: nextChainKey,
1369
+ messageIndex: messageIndex + 1,
1370
+ skippedKeys: mergedSkipped
1371
+ };
1372
+ }
1373
+ const aad = buildSenderKeyAad(encryptedPayload.slice(1, 1 + SENDER_KEY_ID_SIZE), messageIndex);
1374
+ const cryptoKey = await crypto.subtle.importKey("raw", toPlainBuffer(messageKey), "AES-GCM", false, ["decrypt"]);
1375
+ const innerEnvelope = new Uint8Array(await crypto.subtle.decrypt({
1376
+ name: "AES-GCM",
1377
+ iv: nonce,
1378
+ additionalData: toPlainBuffer(aad)
1379
+ }, cryptoKey, toPlainBuffer(ciphertext)));
1380
+ messageKey.fill(0);
1381
+ return {
1382
+ innerEnvelope,
1383
+ senderKeyId,
1384
+ messageIndex,
1385
+ updatedState
1386
+ };
1387
+ }
1388
+ //#endregion
1389
+ //#region src/crypto/message.ts
1390
+ async function verifyEnvelopeSender(decoded, client) {
1391
+ let verified = false;
1392
+ let verificationStatus = "unverifiable";
1393
+ try {
1394
+ const senderAgentId = agentIdFromKid(decoded.kid);
1395
+ verified = verifySignature(jwkToPublicKey((await client.get(`/agents/${senderAgentId}/keys`)).signing_public_key), decoded.payload, decoded.signature);
1396
+ verificationStatus = verified ? "verified" : "invalid";
1397
+ } catch {}
1398
+ return {
1399
+ verified,
1400
+ verificationStatus
1401
+ };
1402
+ }
1403
+ function signingKid(agentId) {
1404
+ return `rine:${agentId}`;
1405
+ }
1406
+ async function encryptMessage(senderAgentId, recipientEncryptionPk, payload) {
1407
+ const keys = loadAgentKeys(senderAgentId);
1408
+ const plaintext = new TextEncoder().encode(JSON.stringify(payload));
1409
+ const kid = signingKid(senderAgentId);
1410
+ return {
1411
+ encrypted_payload: toBase64Url(await seal(recipientEncryptionPk, encodeEnvelope(kid, signPayload(keys.signing.privateKey, plaintext), plaintext))),
1412
+ encryption_version: "hpke-v1",
1413
+ sender_signing_kid: kid
1414
+ };
1415
+ }
1416
+ async function fetchRecipientEncryptionKey(client, agentId) {
1417
+ return jwkToPublicKey((await client.get(`/agents/${agentId}/keys`)).encryption_public_key);
1418
+ }
1419
+ async function decryptMessage(recipientAgentId, encryptedPayloadB64, client) {
1420
+ const keys = loadAgentKeys(recipientAgentId);
1421
+ const encrypted = fromBase64Url(encryptedPayloadB64);
1422
+ const decoded = decodeEnvelope(await open(keys.encryption.privateKey, encrypted));
1423
+ const plaintext = new TextDecoder("utf-8", { fatal: true }).decode(decoded.payload);
1424
+ const { verified, verificationStatus } = await verifyEnvelopeSender(decoded, client);
1425
+ return {
1426
+ plaintext,
1427
+ senderKid: decoded.kid,
1428
+ verified,
1429
+ verificationStatus
1430
+ };
1431
+ }
1432
+ async function encryptGroupMessage(senderAgentId, groupId, senderKeyState, payload) {
1433
+ const keys = loadAgentKeys(senderAgentId);
1434
+ const plaintext = new TextEncoder().encode(JSON.stringify(payload));
1435
+ const kid = signingKid(senderAgentId);
1436
+ const selfReadKey = deriveMessageKey(senderKeyState.chainKey).messageKey;
1437
+ const selfReadIndex = senderKeyState.messageIndex;
1438
+ const { encryptedPayload, updatedState } = await sealGroup(keys.signing.privateKey, kid, senderKeyState, plaintext);
1439
+ updatedState.skippedKeys = {
1440
+ ...updatedState.skippedKeys ?? {},
1441
+ [selfReadIndex]: toBase64Url(selfReadKey)
1442
+ };
1443
+ selfReadKey.fill(0);
1444
+ saveSenderKeyState(senderAgentId, groupId, [...loadSenderKeyStates(senderAgentId, groupId).filter((s) => s.senderKeyId !== updatedState.senderKeyId), updatedState]);
1445
+ return {
1446
+ result: {
1447
+ encrypted_payload: toBase64Url(encryptedPayload),
1448
+ encryption_version: "sender-key-v1",
1449
+ sender_signing_kid: kid
1450
+ },
1451
+ updatedState
1452
+ };
1453
+ }
1454
+ async function decryptGroupMessage(recipientAgentId, groupId, encryptedPayloadB64, client) {
1455
+ const states = loadSenderKeyStates(recipientAgentId, groupId);
1456
+ const stateMap = /* @__PURE__ */ new Map();
1457
+ for (const s of states) stateMap.set(s.senderKeyId, s);
1458
+ const { innerEnvelope, updatedState, messageIndex } = await openGroup(stateMap, fromBase64Url(encryptedPayloadB64));
1459
+ const decoded = decodeEnvelope(innerEnvelope);
1460
+ const plaintext = new TextDecoder("utf-8", { fatal: true }).decode(decoded.payload);
1461
+ const { verified, verificationStatus } = await verifyEnvelopeSender(decoded, client);
1462
+ if (verificationStatus === "invalid") throw new Error("Sender signature verification failed");
1463
+ if (updatedState.senderAgentId === recipientAgentId) {
1464
+ const prevCached = stateMap.get(updatedState.senderKeyId)?.skippedKeys?.[messageIndex];
1465
+ if (prevCached && !updatedState.skippedKeys?.[messageIndex]) updatedState.skippedKeys = {
1466
+ ...updatedState.skippedKeys ?? {},
1467
+ [messageIndex]: prevCached
1468
+ };
1469
+ }
1470
+ stateMap.set(updatedState.senderKeyId, updatedState);
1471
+ saveSenderKeyState(recipientAgentId, groupId, Array.from(stateMap.values()));
1472
+ return {
1473
+ plaintext,
1474
+ senderKid: decoded.kid,
1475
+ verified,
1476
+ verificationStatus
1477
+ };
1478
+ }
1479
+ function getAgentPublicKeys(agentId, agentKeys) {
1480
+ const keys = agentKeys ?? loadAgentKeys(agentId);
1481
+ return {
1482
+ signing_public_key: signingPublicKeyToJWK(keys.signing.publicKey),
1483
+ encryption_public_key: {
1484
+ kty: "OKP",
1485
+ crv: "X25519",
1486
+ x: toBase64Url(keys.encryption.publicKey)
794
1487
  }
795
- if (!UUID_RE.test(agentId)) {
796
- printError("agent-id must be a valid UUID");
1488
+ };
1489
+ }
1490
+ //#endregion
1491
+ //#region src/commands/keys.ts
1492
+ function getKeysDir(agentId) {
1493
+ return join(getConfigDir(), "keys", agentId);
1494
+ }
1495
+ function backupKeys(agentId) {
1496
+ const dir = getKeysDir(agentId);
1497
+ const ts = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
1498
+ for (const name of ["signing.key", "encryption.key"]) {
1499
+ const src = join(dir, name);
1500
+ if (!fs.existsSync(src)) continue;
1501
+ for (const f of fs.readdirSync(dir)) if (f.startsWith(`${name}.bak.`)) fs.unlinkSync(join(dir, f));
1502
+ fs.copyFileSync(src, join(dir, `${name}.bak.${ts}`));
1503
+ }
1504
+ }
1505
+ async function verifyServerKeys(client, agentId, expected) {
1506
+ const server = await client.get(`/agents/${agentId}/keys`);
1507
+ if (server.signing_public_key.x !== expected.signing_public_key.x || server.encryption_public_key.x !== expected.encryption_public_key.x) throw new Error("Server key verification failed: uploaded keys do not match. Old keys preserved locally.");
1508
+ }
1509
+ function registerKeys(program) {
1510
+ const keys = program.command("keys").description("E2EE key management");
1511
+ keys.command("generate").description("Generate new E2EE keys for an agent").option("--agent <id>", "Agent ID").action(withClient(program, async ({ client, gOpts }, opts) => {
1512
+ const agentId = await resolveAgent(client, opts.agent, gOpts.as);
1513
+ if (agentKeysExist(agentId)) {
1514
+ printError(`Keys already exist for agent ${agentId}. Use 'rine keys rotate' to replace them.`);
1515
+ process.exitCode = 1;
1516
+ return;
1517
+ }
1518
+ const agentKeys = generateAgentKeys();
1519
+ const pubKeys = getAgentPublicKeys(agentId, agentKeys);
1520
+ await client.post(`/agents/${agentId}/keys`, pubKeys);
1521
+ await verifyServerKeys(client, agentId, pubKeys);
1522
+ saveAgentKeys(agentId, agentKeys);
1523
+ printMutationOk(`E2EE keys generated for agent ${agentId}`, gOpts.json);
1524
+ }));
1525
+ keys.command("rotate").description("Rotate E2EE keys for an agent").option("--agent <id>", "Agent ID").action(withClient(program, async ({ client, gOpts }, opts) => {
1526
+ const agentId = await resolveAgent(client, opts.agent, gOpts.as);
1527
+ if (agentKeysExist(agentId)) backupKeys(agentId);
1528
+ const agentKeys = generateAgentKeys();
1529
+ const pubKeys = getAgentPublicKeys(agentId, agentKeys);
1530
+ await client.post(`/agents/${agentId}/keys`, pubKeys);
1531
+ await verifyServerKeys(client, agentId, pubKeys);
1532
+ saveAgentKeys(agentId, agentKeys);
1533
+ printMutationOk(`E2EE keys rotated for agent ${agentId}`, gOpts.json);
1534
+ }));
1535
+ keys.command("export").description("Export private keys to a file").option("--agent <id>", "Agent ID").requiredOption("--output <file>", "Output file path").action(withClient(program, async ({ client, gOpts }, opts) => {
1536
+ const agentId = await resolveAgent(client, opts.agent, gOpts.as);
1537
+ if (!agentKeysExist(agentId)) {
1538
+ printError(`No keys found for agent ${agentId}`);
1539
+ process.exitCode = 1;
1540
+ return;
1541
+ }
1542
+ const agentKeys = loadAgentKeys(agentId);
1543
+ const exported = {
1544
+ agent_id: agentId,
1545
+ signing_private_key: toBase64Url(agentKeys.signing.privateKey),
1546
+ encryption_private_key: toBase64Url(agentKeys.encryption.privateKey)
1547
+ };
1548
+ fs.writeFileSync(opts.output, JSON.stringify(exported, null, 2));
1549
+ fs.chmodSync(opts.output, 384);
1550
+ if (gOpts.json) printJson({
1551
+ ok: true,
1552
+ path: opts.output
1553
+ });
1554
+ else console.log(`Keys exported to ${opts.output}`);
1555
+ }));
1556
+ keys.command("import").description("Import private keys from a file").requiredOption("--input <file>", "Input file path").action(withErrorHandler(program, (gOpts, opts) => {
1557
+ const raw = fs.readFileSync(opts.input, "utf-8");
1558
+ const data = JSON.parse(raw);
1559
+ if (!data.agent_id || !data.signing_private_key || !data.encryption_private_key) {
1560
+ printError("Invalid key export file");
1561
+ process.exitCode = 2;
1562
+ return;
1563
+ }
1564
+ const sigBytes = fromBase64Url(data.signing_private_key);
1565
+ if (sigBytes.length !== 32) {
1566
+ printError(`Invalid signing key: expected 32 bytes, got ${sigBytes.length}`);
797
1567
  process.exitCode = 2;
798
1568
  return;
799
1569
  }
800
1570
  try {
801
- const data = await directoryFetch(`/directory/agents/${agentId}`);
802
- if (gOpts.json) {
803
- printJson(data);
804
- return;
805
- }
806
- const result = data;
807
- const card = result.card ?? data;
808
- const meta = result.directory_metadata ?? {};
809
- printTable([{
810
- Name: String(card.name ?? ""),
811
- Description: trunc(card.description, 120),
812
- Categories: fmtList(card.rine?.categories ?? card.categories),
813
- Tags: fmtList(card.rine?.tags ?? card.tags),
814
- Pricing: String(card.rine?.pricing_model ?? card.pricing_model ?? ""),
815
- "Registered At": String(meta.registered_at ?? ""),
816
- "Messages (30d)": String(meta.messages_processed_30d ?? ""),
817
- "Avg Response (ms)": String(meta.avg_response_time_ms ?? "")
818
- }]);
819
- } catch (err) {
820
- printError(formatError(err));
821
- process.exitCode = 1;
1571
+ ed25519.getPublicKey(sigBytes);
1572
+ } catch {
1573
+ printError("Invalid signing key: not a valid Ed25519 private key");
1574
+ process.exitCode = 2;
1575
+ return;
822
1576
  }
823
- });
1577
+ const encBytes = fromBase64Url(data.encryption_private_key);
1578
+ if (encBytes.length !== 32) {
1579
+ printError(`Invalid encryption key: expected 32 bytes, got ${encBytes.length}`);
1580
+ process.exitCode = 2;
1581
+ return;
1582
+ }
1583
+ try {
1584
+ x25519.getPublicKey(encBytes);
1585
+ } catch {
1586
+ printError("Invalid encryption key: not a valid X25519 private key");
1587
+ process.exitCode = 2;
1588
+ return;
1589
+ }
1590
+ const dir = join(getConfigDir(), "keys", data.agent_id);
1591
+ fs.mkdirSync(dir, {
1592
+ recursive: true,
1593
+ mode: 448
1594
+ });
1595
+ const sigPath = join(dir, "signing.key");
1596
+ fs.writeFileSync(sigPath, data.signing_private_key, "utf-8");
1597
+ fs.chmodSync(sigPath, 384);
1598
+ const encPath = join(dir, "encryption.key");
1599
+ fs.writeFileSync(encPath, data.encryption_private_key, "utf-8");
1600
+ fs.chmodSync(encPath, 384);
1601
+ printMutationOk(`Keys imported for agent ${data.agent_id}`, gOpts.json);
1602
+ }));
824
1603
  }
825
1604
  //#endregion
826
- //#region src/commands/resolve.ts
1605
+ //#region src/crypto/ingest.ts
827
1606
  /**
828
- * Resolve the first active agent for the authenticated org.
829
- * Used by inbox and stream commands when no explicit --agent is given.
1607
+ * Auto-ingest a sender key distribution from a decrypted message.
1608
+ * Only ingests if the message is a `rine.v1.sender_key_distribution`,
1609
+ * the signature was verified, and the key ID is not already known.
1610
+ *
1611
+ * Returns true if a new key was ingested.
830
1612
  */
831
- async function resolveFirstAgent(client) {
832
- const agents = ((await client.get("/agents")).items ?? []).filter((a) => !a.revoked_at);
833
- if (agents.length === 0) throw new Error("No active agents found. Create one with `rine agent create`.");
834
- const first = agents[0];
835
- if (agents.length > 1) process.stderr.write(`Warning: multiple agents found, using first: ${first.id}\n`);
836
- return first.id;
1613
+ function ingestSenderKeyDistribution(agentId, messageType, result) {
1614
+ if (messageType !== "rine.v1.sender_key_distribution") return false;
1615
+ if (!result.verified) return false;
1616
+ try {
1617
+ const dist = JSON.parse(result.plaintext);
1618
+ const existing = loadSenderKeyStates(agentId, dist.group_id);
1619
+ if (existing.some((s) => s.senderKeyId === dist.sender_key_id)) return false;
1620
+ const newState = {
1621
+ senderKeyId: dist.sender_key_id,
1622
+ groupId: dist.group_id,
1623
+ senderAgentId: agentIdFromKid(result.senderKid),
1624
+ chainKey: fromBase64Url(dist.chain_key),
1625
+ messageIndex: dist.message_index
1626
+ };
1627
+ saveSenderKeyState(agentId, dist.group_id, [...existing, newState]);
1628
+ return true;
1629
+ } catch {
1630
+ return false;
1631
+ }
1632
+ }
1633
+ //#endregion
1634
+ //#region src/commands/sender-key-ops.ts
1635
+ async function distributeSenderKey(client, senderAgentId, state, groupId, recipientIds, extraHeaders) {
1636
+ if (recipientIds.length === 0) return [];
1637
+ const batchKeys = await client.get("/agents/keys", { ids: recipientIds.join(",") });
1638
+ const distPayload = {
1639
+ group_id: groupId,
1640
+ sender_key_id: state.senderKeyId,
1641
+ chain_key: toBase64Url(state.chainKey),
1642
+ message_index: state.messageIndex,
1643
+ sender_agent_id: senderAgentId
1644
+ };
1645
+ const succeeded = [];
1646
+ const failed = [];
1647
+ for (const recipientId of recipientIds) {
1648
+ const recipientKeyData = batchKeys.keys[recipientId];
1649
+ if (!recipientKeyData) {
1650
+ failed.push(recipientId);
1651
+ continue;
1652
+ }
1653
+ try {
1654
+ const encrypted = await encryptMessage(senderAgentId, fromBase64Url(recipientKeyData.encryption_public_key.x), distPayload);
1655
+ await client.post("/messages", {
1656
+ to_agent_id: recipientId,
1657
+ type: "rine.v1.sender_key_distribution",
1658
+ ...encrypted
1659
+ }, extraHeaders);
1660
+ succeeded.push(recipientId);
1661
+ } catch {
1662
+ failed.push(recipientId);
1663
+ }
1664
+ }
1665
+ if (failed.length > 0) printError(`Sender key distribution failed for ${failed.length} recipient(s): ${failed.join(", ")}`);
1666
+ return succeeded;
1667
+ }
1668
+ async function getOrCreateSenderKey(client, senderAgentId, groupHandle, extraHeaders) {
1669
+ const firstGroup = (await client.get("/groups", { handle: groupHandle })).items?.[0];
1670
+ if (!firstGroup) throw new Error(`Group not found: ${groupHandle}`);
1671
+ const groupId = firstGroup.id;
1672
+ const memberIds = (await client.get(`/groups/${groupId}/members`)).items.map((m) => m.agent_id);
1673
+ const senderIdLower = senderAgentId.toLowerCase();
1674
+ if (!memberIds.some((id) => id.toLowerCase() === senderIdLower)) throw new Error("You are not a member of this group");
1675
+ const recipientIds = memberIds.filter((id) => id.toLowerCase() !== senderIdLower);
1676
+ const states = loadSenderKeyStates(senderAgentId, groupId);
1677
+ const own = states.find((s) => s.senderAgentId === senderAgentId);
1678
+ const currentMemberSet = new Set(memberIds.map((id) => id.toLowerCase()));
1679
+ const memberRemoved = own?.distributedTo?.some((id) => !currentMemberSet.has(id.toLowerCase())) ?? false;
1680
+ if (own && !needsRotation(own) && !memberRemoved) {
1681
+ const alreadyDistributed = new Set((own.distributedTo ?? []).map((id) => id.toLowerCase()));
1682
+ const undistributed = recipientIds.filter((id) => !alreadyDistributed.has(id.toLowerCase()));
1683
+ if (undistributed.length > 0) {
1684
+ const succeeded = await distributeSenderKey(client, senderAgentId, own, groupId, undistributed, extraHeaders);
1685
+ if (succeeded.length > 0) {
1686
+ own.distributedTo = [...own.distributedTo ?? [], ...succeeded];
1687
+ saveSenderKeyState(senderAgentId, groupId, states);
1688
+ }
1689
+ }
1690
+ return {
1691
+ state: own,
1692
+ groupId
1693
+ };
1694
+ }
1695
+ const newState = generateSenderKey(groupId, senderAgentId);
1696
+ newState.distributedTo = await distributeSenderKey(client, senderAgentId, newState, groupId, recipientIds, extraHeaders);
1697
+ saveSenderKeyState(senderAgentId, groupId, [...memberRemoved ? states.filter((s) => currentMemberSet.has(s.senderAgentId.toLowerCase())) : states, newState]);
1698
+ return {
1699
+ state: newState,
1700
+ groupId
1701
+ };
837
1702
  }
838
1703
  //#endregion
839
1704
  //#region src/commands/messages.ts
840
1705
  function msgFrom(m) {
841
- return String(m.sender_handle ?? m.from_agent_id ?? "");
1706
+ return m.sender_handle ?? m.from_agent_id ?? "";
842
1707
  }
843
1708
  function msgTo(m) {
844
- return String(m.recipient_handle ?? m.to_agent_id ?? "");
1709
+ return m.recipient_handle ?? m.to_agent_id ?? "";
845
1710
  }
846
- function sendRow(m) {
847
- return {
848
- ID: m.id,
849
- "Conversation ID": m.conversation_id,
850
- From: msgFrom(m),
851
- To: msgTo(m),
852
- Type: m.type,
853
- Created: m.created_at
854
- };
1711
+ function msgGroup(m) {
1712
+ return m.group_handle ?? "";
855
1713
  }
856
- function readRow(m) {
1714
+ function sendRow(m) {
857
1715
  return {
858
1716
  ID: m.id,
859
1717
  "Conversation ID": m.conversation_id,
860
1718
  From: msgFrom(m),
861
1719
  To: msgTo(m),
1720
+ Group: msgGroup(m),
862
1721
  Type: m.type,
863
- Preview: m.payload_preview ? String(m.payload_preview).slice(0, 60) : "",
864
- Payload: JSON.stringify(m.payload ?? {}),
865
1722
  Created: m.created_at
866
1723
  };
867
1724
  }
@@ -869,98 +1726,133 @@ function inboxRow(m) {
869
1726
  return {
870
1727
  ID: m.id,
871
1728
  From: msgFrom(m),
1729
+ Group: msgGroup(m),
872
1730
  Type: m.type,
873
- Preview: m.payload_preview ? String(m.payload_preview).slice(0, 60) : "",
1731
+ Encryption: m.encryption_version,
874
1732
  Created: m.created_at
875
1733
  };
876
1734
  }
877
1735
  function addMessageCommands(parent, program) {
878
- parent.command("send").description("Send a message").requiredOption("--to <address>", "Recipient address (agent ID or handle with @)").requiredOption("--type <type>", "Message type (e.g. rine.v1.task_request)").requiredOption("--payload <json>", "Message payload as JSON string").option("--from <address>", "Sender address (agent ID or handle with @)").option("--idempotency-key <key>", "Idempotency key header").action(async (opts) => {
1736
+ parent.command("send").description("Send an encrypted message").requiredOption("--to <address>", "Recipient: agent handle, agent ID, or #group@org handle").requiredOption("--type <type>", "Message type (e.g. rine.v1.task_request)").requiredOption("--payload <json>", "Message payload as JSON string").option("--from <address>", "Sender address (agent ID or handle with @)").option("--idempotency-key <key>", "Idempotency key header").action(withClient(program, async ({ client, gOpts }, opts) => {
1737
+ let parsedPayload;
879
1738
  try {
880
- const gOpts = program.opts();
881
- const { client } = await createClient(gOpts.profile);
882
- let parsedPayload;
883
- try {
884
- parsedPayload = JSON.parse(opts.payload);
885
- } catch {
886
- printError("--payload must be valid JSON");
887
- process.exitCode = 2;
888
- return;
889
- }
890
- const body = {
891
- type: opts.type,
892
- payload: parsedPayload
893
- };
894
- if (opts.to.includes("@")) body.to_handle = opts.to;
895
- else body.to_agent_id = opts.to;
896
- if (opts.from !== void 0) if (opts.from.includes("@")) body.from_handle = opts.from;
897
- else body.from_agent_id = opts.from;
898
- const extraHeaders = {};
899
- if (opts.idempotencyKey) extraHeaders["Idempotency-Key"] = opts.idempotencyKey;
900
- const data = await client.post("/messages", body, Object.keys(extraHeaders).length > 0 ? extraHeaders : void 0);
901
- if (gOpts.json) printJson(data);
902
- else printTable([sendRow(data)]);
903
- } catch (err) {
904
- printError(formatError(err));
905
- process.exitCode = 1;
1739
+ parsedPayload = JSON.parse(opts.payload);
1740
+ } catch {
1741
+ printError("--payload must be valid JSON");
1742
+ process.exitCode = 2;
1743
+ return;
906
1744
  }
907
- });
908
- parent.command("read").description("Read a message by ID").argument("<message-id>", "Message ID").action(async (messageId) => {
909
- try {
910
- const gOpts = program.opts();
911
- const { client } = await createClient(gOpts.profile);
912
- const data = await client.get(`/messages/${messageId}`);
913
- if (gOpts.json) printJson(data);
914
- else printTable([readRow(data)]);
1745
+ const extraHeaders = {};
1746
+ let senderAgentId;
1747
+ if (opts.from !== void 0) {
1748
+ senderAgentId = await resolveToUuid(opts.from);
1749
+ extraHeaders["X-Rine-Agent"] = senderAgentId;
1750
+ } else if (!gOpts.as) {
1751
+ senderAgentId = await resolveAgent(client, void 0, void 0);
1752
+ extraHeaders["X-Rine-Agent"] = senderAgentId;
1753
+ } else senderAgentId = await resolveToUuid(gOpts.as);
1754
+ if (opts.idempotencyKey) extraHeaders["Idempotency-Key"] = opts.idempotencyKey;
1755
+ let recipientAgentId;
1756
+ if (!opts.to.includes("@")) recipientAgentId = opts.to;
1757
+ else if (!opts.to.startsWith("#")) recipientAgentId = await resolveToUuid(opts.to);
1758
+ const body = { type: opts.type };
1759
+ if (opts.to.includes("@")) body.to_handle = opts.to;
1760
+ else body.to_agent_id = opts.to;
1761
+ if (opts.from?.includes("@")) body.from_handle = opts.from;
1762
+ if (opts.to.startsWith("#") && senderAgentId) {
1763
+ const { state, groupId } = await getOrCreateSenderKey(client, senderAgentId, opts.to, Object.keys(extraHeaders).length > 0 ? extraHeaders : void 0);
1764
+ const { result } = await encryptGroupMessage(senderAgentId, groupId, state, parsedPayload);
1765
+ Object.assign(body, result);
1766
+ } else if (senderAgentId && recipientAgentId) {
1767
+ const recipientPk = await fetchRecipientEncryptionKey(client, recipientAgentId);
1768
+ const encrypted = await encryptMessage(senderAgentId, recipientPk, parsedPayload);
1769
+ Object.assign(body, encrypted);
1770
+ } else throw new Error("Cannot encrypt: unable to resolve sender or recipient. Use UUIDs or ensure WebFinger is available.");
1771
+ const data = await client.post("/messages", body, Object.keys(extraHeaders).length > 0 ? extraHeaders : void 0);
1772
+ if (gOpts.json) printJson(data);
1773
+ else printTable([sendRow(data)]);
1774
+ }));
1775
+ parent.command("read").description("Read and decrypt a message by ID").argument("<message-id>", "Message ID").option("--agent <id>", "Agent ID (for decryption key)").action(withClient(program, async ({ client, gOpts }, messageId, opts) => {
1776
+ const data = await client.get(`/messages/${messageId}`);
1777
+ const agentId = await resolveAgent(client, opts.agent, gOpts.as);
1778
+ if ((data.encryption_version === "hpke-v1" || data.encryption_version === "sender-key-v1") && agentKeysExist(agentId)) try {
1779
+ let result;
1780
+ if (data.encryption_version === "sender-key-v1" && data.group_id) result = await decryptGroupMessage(agentId, data.group_id, data.encrypted_payload, client);
1781
+ else result = await decryptMessage(agentId, data.encrypted_payload, client);
1782
+ if (ingestSenderKeyDistribution(agentId, data.type, result)) printText("Sender key ingested");
1783
+ if (gOpts.json) printJson({
1784
+ ...data,
1785
+ decrypted_payload: JSON.parse(result.plaintext),
1786
+ signature_verified: result.verified,
1787
+ verification_status: result.verificationStatus,
1788
+ sender_kid: result.senderKid
1789
+ });
1790
+ else printTable([{
1791
+ ID: data.id,
1792
+ "Conversation ID": data.conversation_id,
1793
+ From: msgFrom(data),
1794
+ To: msgTo(data),
1795
+ Group: msgGroup(data),
1796
+ Type: data.type,
1797
+ Verified: result.verificationStatus === "verified" ? "✓" : result.verificationStatus === "invalid" ? "✗ INVALID" : "?",
1798
+ Payload: result.plaintext.slice(0, 200),
1799
+ Created: data.created_at
1800
+ }]);
1801
+ return;
915
1802
  } catch (err) {
916
- printError(formatError(err));
917
- process.exitCode = 1;
1803
+ if (!gOpts.json) printError(`Decryption failed: ${err instanceof Error ? err.message : String(err)}`);
918
1804
  }
919
- });
920
- parent.command("inbox").description("List inbox messages").option("--agent <id>", "Agent ID (auto-resolved if omitted)").option("--limit <n>", "Maximum number of messages to return").option("--cursor <cursor>", "Pagination cursor").action(async (opts) => {
1805
+ if (gOpts.json) printJson(data);
1806
+ else printTable([{
1807
+ ID: data.id,
1808
+ "Conversation ID": data.conversation_id,
1809
+ From: msgFrom(data),
1810
+ To: msgTo(data),
1811
+ Group: msgGroup(data),
1812
+ Type: data.type,
1813
+ Encryption: data.encryption_version,
1814
+ "Encrypted Payload": `${data.encrypted_payload.slice(0, 40)}...`,
1815
+ Created: data.created_at
1816
+ }]);
1817
+ }));
1818
+ parent.command("inbox").description("List inbox messages").option("--agent <id>", "Agent ID (defaults to your agent)").option("--limit <n>", "Maximum number of messages to return").option("--cursor <cursor>", "Pagination cursor").action(withClient(program, async ({ client, gOpts }, opts) => {
1819
+ const agentId = await resolveAgent(client, opts.agent, gOpts.as);
1820
+ const params = {};
1821
+ if (opts.limit) params.limit = opts.limit;
1822
+ if (opts.cursor) params.cursor = opts.cursor;
1823
+ const data = await client.get(`/agents/${agentId}/messages`, params);
1824
+ if (gOpts.json) printJson(data);
1825
+ else {
1826
+ printTable(data.items.map(inboxRow));
1827
+ if (data.next_cursor) printText(`Next cursor: ${data.next_cursor}`);
1828
+ }
1829
+ }));
1830
+ parent.command("reply").description("Reply to a message (encrypted)").argument("<message-id>", "Message ID to reply to").requiredOption("--type <type>", "Message type").requiredOption("--payload <json>", "Reply payload as JSON string").action(withClient(program, async ({ client, gOpts }, messageId, opts) => {
1831
+ let parsedPayload;
921
1832
  try {
922
- const gOpts = program.opts();
923
- const { client } = await createClient(gOpts.profile);
924
- let agentId = opts.agent;
925
- if (!agentId) agentId = await resolveFirstAgent(client);
926
- const params = {};
927
- if (opts.limit) params.limit = opts.limit;
928
- if (opts.cursor) params.cursor = opts.cursor;
929
- const data = await client.get(`/agents/${agentId}/messages`, params);
930
- if (gOpts.json) printJson(data);
931
- else {
932
- printTable(data.items.map(inboxRow));
933
- if (data.next_cursor) printText(`Next cursor: ${data.next_cursor}`);
934
- }
935
- } catch (err) {
936
- printError(formatError(err));
937
- process.exitCode = 1;
1833
+ parsedPayload = JSON.parse(opts.payload);
1834
+ } catch {
1835
+ printError("--payload must be valid JSON");
1836
+ process.exitCode = 2;
1837
+ return;
938
1838
  }
939
- });
940
- parent.command("reply").description("Reply to a message").argument("<message-id>", "Message ID to reply to").requiredOption("--type <type>", "Message type").requiredOption("--payload <json>", "Reply payload as JSON string").action(async (messageId, opts) => {
1839
+ const original = await client.get(`/messages/${messageId}`);
1840
+ const senderAgentId = await resolveAgent(client, void 0, gOpts.as);
1841
+ const recipientAgentId = original.from_agent_id;
941
1842
  try {
942
- const gOpts = program.opts();
943
- const { client } = await createClient(gOpts.profile);
944
- let parsedPayload;
945
- try {
946
- parsedPayload = JSON.parse(opts.payload);
947
- } catch {
948
- printError("--payload must be valid JSON");
949
- process.exitCode = 2;
950
- return;
951
- }
952
- const data = await client.post(`/messages/${messageId}/reply`, {
953
- type: opts.type,
954
- payload: parsedPayload
955
- });
1843
+ const body = { type: opts.type };
1844
+ if (!recipientAgentId || !agentKeysExist(senderAgentId)) throw new Error("Cannot reply: encryption keys unavailable. Run 'rine keys generate' first.");
1845
+ const encrypted = await encryptMessage(senderAgentId, await fetchRecipientEncryptionKey(client, recipientAgentId), parsedPayload);
1846
+ Object.assign(body, encrypted);
1847
+ const data = await client.post(`/messages/${messageId}/reply`, body);
956
1848
  if (gOpts.json) printJson(data);
957
1849
  else printTable([sendRow(data)]);
958
1850
  } catch (err) {
959
1851
  if (err instanceof RineApiError && err.status === 409) printError("Conversation is closed");
960
- else printError(formatError(err));
1852
+ else throw err;
961
1853
  process.exitCode = 1;
962
1854
  }
963
- });
1855
+ }));
964
1856
  }
965
1857
  function registerMessages(program) {
966
1858
  addMessageCommands(program, program);
@@ -974,45 +1866,31 @@ function renderOrg(data, opts) {
974
1866
  return;
975
1867
  }
976
1868
  printTable([{
977
- ID: String(data.id ?? ""),
978
- Name: String(data.name ?? ""),
979
- "Contact Email": String(data.contact_email ?? ""),
980
- Country: String(data.country_code ?? ""),
981
- Created: String(data.created_at ?? "")
1869
+ ID: data.id,
1870
+ Name: data.name,
1871
+ "Contact Email": data.contact_email ?? "",
1872
+ Country: data.country_code ?? "",
1873
+ Created: data.created_at
982
1874
  }]);
983
1875
  }
984
1876
  function registerOrg(program) {
985
1877
  const org = program.command("org").description("Organization management");
986
- org.command("get").description("Get organization details").action(async () => {
987
- try {
988
- const gOpts = program.opts();
989
- const { client } = await createClient(gOpts.profile);
990
- renderOrg(await client.get("/org"), gOpts);
991
- } catch (err) {
992
- printError(formatError(err));
993
- process.exitCode = 1;
994
- }
995
- });
996
- org.command("update").description("Update organization details").option("--name <name>", "Organization name").option("--contact-email <email>", "Contact email address").option("--country-code <cc>", "ISO country code (e.g. DE)").action(async (opts) => {
997
- try {
998
- const gOpts = program.opts();
999
- const body = {};
1000
- if (opts.name) body.name = opts.name;
1001
- if (opts.contactEmail) body.contact_email = opts.contactEmail;
1002
- if (opts.countryCode) body.country_code = opts.countryCode;
1003
- if (Object.keys(body).length === 0) {
1004
- printError("At least one of --name, --contact-email, --country-code is required");
1005
- process.exitCode = 2;
1006
- return;
1007
- }
1008
- const { client } = await createClient(gOpts.profile);
1009
- await client.patch("/org", body);
1010
- printMutationOk("Org updated", gOpts.json);
1011
- } catch (err) {
1012
- printError(formatError(err));
1013
- process.exitCode = 1;
1878
+ org.command("get").description("Get organization details").action(withClient(program, async ({ client, gOpts }) => {
1879
+ renderOrg(await client.get("/org"), gOpts);
1880
+ }));
1881
+ org.command("update").description("Update organization details").option("--name <name>", "Organization name").option("--contact-email <email>", "Contact email address").option("--country-code <cc>", "ISO country code (e.g. DE)").action(withClient(program, async ({ client, gOpts }, opts) => {
1882
+ const body = {};
1883
+ if (opts.name) body.name = opts.name;
1884
+ if (opts.contactEmail) body.contact_email = opts.contactEmail;
1885
+ if (opts.countryCode) body.country_code = opts.countryCode;
1886
+ if (Object.keys(body).length === 0) {
1887
+ printError("At least one of --name, --contact-email, --country-code is required");
1888
+ process.exitCode = 2;
1889
+ return;
1014
1890
  }
1015
- });
1891
+ await client.patch("/org", body);
1892
+ printMutationOk("Org updated", gOpts.json);
1893
+ }));
1016
1894
  }
1017
1895
  //#endregion
1018
1896
  //#region src/timelock.ts
@@ -1032,129 +1910,122 @@ async function solveTimeLockWithProgress(baseHex, modulusHex, T, onProgress) {
1032
1910
  //#endregion
1033
1911
  //#region src/commands/register.ts
1034
1912
  function registerRegister(program) {
1035
- program.command("register").description("Register a new organization (two-step PoW flow)").requiredOption("--email <email>", "Email address").requiredOption("--name <name>", "Organization name").requiredOption("--slug <slug>", "Organization slug").action(async (opts) => {
1036
- try {
1037
- const gOpts = program.opts();
1038
- const profile = gOpts.profile ?? "default";
1039
- const apiUrl = getApiUrl();
1040
- const challengeRes = await fetch(`${apiUrl}/auth/register`, {
1041
- method: "POST",
1042
- headers: { "Content-Type": "application/json" },
1043
- body: JSON.stringify({
1044
- email: opts.email,
1045
- org_slug: opts.slug
1046
- })
1047
- });
1048
- if (challengeRes.status === 409) {
1049
- printError("Email or slug already registered");
1050
- process.exitCode = 1;
1051
- return;
1052
- }
1053
- if (challengeRes.status === 429) {
1054
- printError("Rate limited — please wait before retrying");
1055
- process.exitCode = 1;
1056
- return;
1057
- }
1058
- if (!challengeRes.ok) {
1059
- printError(`Registration failed: ${(await challengeRes.json().catch(() => ({}))).detail ?? challengeRes.statusText}`);
1060
- process.exitCode = 1;
1061
- return;
1062
- }
1063
- const challenge = await challengeRes.json();
1064
- if (challenge.algorithm !== "rsa-timelock-v1") {
1065
- printError(`Unsupported algorithm: ${challenge.algorithm}. Please upgrade rine-cli.`);
1066
- process.exitCode = 1;
1067
- return;
1068
- }
1069
- process.stderr.write("By registering, you accept the rine terms of service and privacy policy:\n https://rine.network/terms.html\n https://rine.network/privacy.html\n");
1070
- const nonce = await solveTimeLockWithProgress(challenge.prefix, challenge.modulus, challenge.difficulty, (pct) => {
1071
- process.stdout.write(`\rSolving PoW challenge: ${pct}%`);
1072
- });
1073
- process.stdout.write("\rSolving PoW challenge: 100%\n");
1074
- const solveRes = await fetch(`${apiUrl}/auth/register/solve`, {
1075
- method: "POST",
1076
- headers: { "Content-Type": "application/json" },
1077
- body: JSON.stringify({
1078
- challenge_id: challenge.challenge_id,
1079
- nonce,
1080
- org_name: opts.name,
1081
- org_slug: opts.slug,
1082
- consent: true
1083
- })
1084
- });
1085
- if (solveRes.status === 409) {
1086
- printError("Conflict: email or slug already registered");
1087
- process.exitCode = 1;
1088
- return;
1089
- }
1090
- if (solveRes.status === 410) {
1091
- printError("Challenge expired — please register again");
1092
- process.exitCode = 1;
1093
- return;
1094
- }
1095
- if (!solveRes.ok) {
1096
- printError(`Solve failed: ${(await solveRes.json().catch(() => ({}))).detail ?? solveRes.statusText}`);
1097
- process.exitCode = 1;
1098
- return;
1099
- }
1100
- const data = await solveRes.json();
1101
- const creds = loadCredentials();
1102
- creds[profile] = {
1103
- client_id: data.client_id,
1104
- client_secret: data.client_secret
1105
- };
1106
- saveCredentials(creds);
1107
- try {
1108
- const tokenRes = await fetch(`${apiUrl}/oauth/token`, {
1109
- method: "POST",
1110
- headers: { "Content-Type": "application/x-www-form-urlencoded" },
1111
- body: new URLSearchParams({
1112
- grant_type: "client_credentials",
1113
- client_id: data.client_id,
1114
- client_secret: data.client_secret
1115
- }).toString()
1116
- });
1117
- if (tokenRes.ok) {
1118
- const tokenData = await tokenRes.json();
1119
- const cache = loadTokenCache();
1120
- cache[profile] = {
1121
- access_token: tokenData.access_token,
1122
- expires_at: Date.now() / 1e3 + tokenData.expires_in
1123
- };
1124
- saveTokenCache(cache);
1125
- }
1126
- } catch {
1127
- process.stderr.write("Warning: initial token cache failed\n");
1128
- }
1129
- if (gOpts.json) printJson(data);
1130
- else {
1131
- printSuccess(`Registered '${opts.name}' (${opts.slug})`);
1132
- console.log("Credentials saved to .rine/credentials.json");
1133
- console.log("Next: create an agent with 'rine agent create --name <agent-name>'");
1134
- }
1135
- } catch (err) {
1136
- printError(formatError(err));
1913
+ program.command("register").description("Register a new organization (two-step PoW flow)").requiredOption("--email <email>", "Email address").requiredOption("--name <name>", "Organization name").requiredOption("--slug <slug>", "Organization slug").action(withErrorHandler(program, async (gOpts, opts) => {
1914
+ const profile = gOpts.profile ?? "default";
1915
+ const apiUrl = getApiUrl();
1916
+ const challengeRes = await fetch(`${apiUrl}/auth/register`, {
1917
+ method: "POST",
1918
+ headers: { "Content-Type": "application/json" },
1919
+ body: JSON.stringify({
1920
+ email: opts.email,
1921
+ org_slug: opts.slug
1922
+ })
1923
+ });
1924
+ if (challengeRes.status === 409) {
1925
+ printError("Email or slug already registered");
1137
1926
  process.exitCode = 1;
1927
+ return;
1138
1928
  }
1139
- });
1929
+ if (challengeRes.status === 429) {
1930
+ printError("Rate limited — please wait before retrying");
1931
+ process.exitCode = 1;
1932
+ return;
1933
+ }
1934
+ if (!challengeRes.ok) {
1935
+ printError(`Registration failed: ${(await challengeRes.json().catch(() => ({}))).detail ?? challengeRes.statusText}`);
1936
+ process.exitCode = 1;
1937
+ return;
1938
+ }
1939
+ const challenge = await challengeRes.json();
1940
+ if (challenge.algorithm !== "rsa-timelock-v1") {
1941
+ printError(`Unsupported algorithm: ${challenge.algorithm}. Please upgrade rine-cli.`);
1942
+ process.exitCode = 1;
1943
+ return;
1944
+ }
1945
+ process.stderr.write("By registering, you accept the rine terms of service and privacy policy:\n https://rine.network/terms.html\n https://rine.network/privacy.html\n");
1946
+ const progressStream = gOpts.json ? process.stderr : process.stdout;
1947
+ const nonce = await solveTimeLockWithProgress(challenge.prefix, challenge.modulus, challenge.difficulty, (pct) => {
1948
+ progressStream.write(`\rSolving PoW challenge: ${pct}%`);
1949
+ });
1950
+ progressStream.write("\rSolving PoW challenge: 100%\n");
1951
+ const solveRes = await fetch(`${apiUrl}/auth/register/solve`, {
1952
+ method: "POST",
1953
+ headers: { "Content-Type": "application/json" },
1954
+ body: JSON.stringify({
1955
+ challenge_id: challenge.challenge_id,
1956
+ nonce,
1957
+ org_name: opts.name,
1958
+ org_slug: opts.slug,
1959
+ consent: true
1960
+ })
1961
+ });
1962
+ if (solveRes.status === 409) {
1963
+ printError("Conflict: email or slug already registered");
1964
+ process.exitCode = 1;
1965
+ return;
1966
+ }
1967
+ if (solveRes.status === 410) {
1968
+ printError("Challenge expired — please register again");
1969
+ process.exitCode = 1;
1970
+ return;
1971
+ }
1972
+ if (!solveRes.ok) {
1973
+ printError(`Solve failed: ${(await solveRes.json().catch(() => ({}))).detail ?? solveRes.statusText}`);
1974
+ process.exitCode = 1;
1975
+ return;
1976
+ }
1977
+ const data = await solveRes.json();
1978
+ const creds = loadCredentials();
1979
+ creds[profile] = {
1980
+ client_id: data.client_id,
1981
+ client_secret: data.client_secret
1982
+ };
1983
+ saveCredentials(creds);
1984
+ try {
1985
+ cacheToken(profile, await fetchOAuthToken(data.client_id, data.client_secret));
1986
+ } catch {
1987
+ process.stderr.write("Warning: initial token cache failed\n");
1988
+ }
1989
+ if (gOpts.json) printJson(data);
1990
+ else {
1991
+ printSuccess(`Registered '${opts.name}' (${opts.slug})`);
1992
+ console.log("Credentials saved to .rine/credentials.json");
1993
+ console.log("Next: create an agent with 'rine agent create --name <agent-name>'");
1994
+ }
1995
+ }));
1140
1996
  }
1141
1997
  //#endregion
1142
1998
  //#region src/commands/stream.ts
1143
- function formatMessageLine(dataStr) {
1999
+ async function formatMessageLine(dataStr, agentId, client) {
1144
2000
  try {
1145
2001
  const data = JSON.parse(dataStr);
1146
- return `[${(data.created_at ?? "").slice(0, 19) || "?"}] ${data.sender_handle ?? String(data.from_agent_id ?? "?")} \u2192 ${data.type ?? "?"}: ${data.payload ? JSON.stringify(data.payload).slice(0, 60) : ""}`;
2002
+ const ts = (data.created_at ?? "").slice(0, 19) || "?";
2003
+ const sender = data.sender_handle ?? String(data.from_agent_id ?? "?");
2004
+ const msgType = data.type ?? "?";
2005
+ const target = data.group_handle ? `${data.group_handle} (${msgType})` : msgType;
2006
+ let preview = "[encrypted]";
2007
+ if (data.encrypted_payload && agentKeysExist(agentId)) try {
2008
+ let result;
2009
+ if (data.encryption_version === "sender-key-v1" && data.group_id) result = await decryptGroupMessage(agentId, data.group_id, data.encrypted_payload, client);
2010
+ else if (data.encryption_version === "hpke-v1") {
2011
+ result = await decryptMessage(agentId, data.encrypted_payload, client);
2012
+ ingestSenderKeyDistribution(agentId, data.type ?? "", result);
2013
+ } else return `[${ts}] ${sender} \u2192 ${target}: [encrypted]`;
2014
+ preview = `[${result.verified ? "✓" : "?"}] ${result.plaintext.slice(0, 60)}`;
2015
+ } catch {
2016
+ preview = "[decrypt failed]";
2017
+ }
2018
+ return `[${ts}] ${sender} \u2192 ${target}: ${preview}`;
1147
2019
  } catch {
1148
2020
  return `[message] ${dataStr.slice(0, 80)}`;
1149
2021
  }
1150
2022
  }
1151
2023
  function registerStream(program) {
1152
- program.command("stream").description("Stream incoming messages via SSE").option("--agent <id>", "Agent ID (auto-resolved if omitted)").option("--verbose", "Show heartbeats and reconnect details").action(async (opts) => {
2024
+ program.command("stream").description("Stream incoming messages via SSE").option("--agent <id>", "Agent ID to stream (defaults to your agent)").option("--verbose", "Show heartbeats and reconnect details").action(async (opts) => {
1153
2025
  try {
1154
2026
  const gOpts = program.opts();
1155
2027
  const { client, profileName, entry } = await createClient(gOpts.profile);
1156
- let agentId = opts.agent;
1157
- if (!agentId) agentId = await resolveFirstAgent(client);
2028
+ const agentId = await resolveAgent(client, opts.agent, gOpts.as);
1158
2029
  const url = `${getApiUrl()}/agents/${agentId}/stream`;
1159
2030
  let lastEventId;
1160
2031
  let backoff = 1;
@@ -1169,7 +2040,7 @@ function registerStream(program) {
1169
2040
  Accept: "text/event-stream"
1170
2041
  };
1171
2042
  if (lastEventId) headers["Last-Event-ID"] = lastEventId;
1172
- await new Promise((resolve, reject) => {
2043
+ await new Promise((resolve) => {
1173
2044
  if (stopped) {
1174
2045
  resolve();
1175
2046
  return;
@@ -1184,7 +2055,7 @@ function registerStream(program) {
1184
2055
  id,
1185
2056
  data
1186
2057
  }));
1187
- else if (event === "message") console.log(formatMessageLine(data));
2058
+ else if (event === "message") formatMessageLine(data, agentId, client).then((line) => console.log(line), () => console.log(`[message] ${data.slice(0, 80)}`));
1188
2059
  else if (event === "heartbeat" && opts.verbose) process.stderr.write(`[heartbeat] ${data}\n`);
1189
2060
  },
1190
2061
  onDisconnect: () => resolve(),
@@ -1204,7 +2075,7 @@ function registerStream(program) {
1204
2075
  process.once("SIGINT", onStop);
1205
2076
  });
1206
2077
  backoff = 1;
1207
- } catch (err) {
2078
+ } catch {
1208
2079
  if (stopped) break;
1209
2080
  if (opts.verbose) process.stderr.write(`Reconnecting in ${backoff}s...\n`);
1210
2081
  else process.stderr.write("Reconnecting...\n");
@@ -1221,125 +2092,86 @@ function registerStream(program) {
1221
2092
  //#region src/commands/webhook.ts
1222
2093
  function registerWebhook(program) {
1223
2094
  const webhook = program.command("webhook").description("Manage webhooks");
1224
- webhook.command("create").description("Create a new webhook").requiredOption("--agent <id>", "Agent ID to attach webhook to").requiredOption("--url <url>", "Webhook target URL (must be https://)").action(async (opts) => {
1225
- const gOpts = program.opts();
2095
+ webhook.command("create").description("Create a new webhook").option("--agent <id>", "Agent ID (defaults to your agent)").requiredOption("--url <url>", "Webhook target URL (must be https://)").action(withClient(program, async ({ client, gOpts }, opts) => {
1226
2096
  if (!opts.url.startsWith("https://")) {
1227
2097
  printError("Webhook URL must start with https://");
1228
2098
  process.exitCode = 2;
1229
2099
  return;
1230
2100
  }
1231
- try {
1232
- const { client } = await createClient(gOpts.profile);
1233
- const data = await client.post("/webhooks", {
1234
- agent_id: opts.agent,
1235
- url: opts.url
1236
- });
1237
- if (gOpts.json) printJson(data);
1238
- else printTable([{
1239
- ID: data.id,
1240
- "Agent ID": data.agent_id,
1241
- URL: data.url,
1242
- Secret: data.secret,
1243
- Active: data.active,
1244
- Created: data.created_at
1245
- }]);
1246
- } catch (err) {
1247
- printError(formatError(err));
1248
- process.exitCode = 1;
1249
- }
1250
- });
1251
- webhook.command("list").description("List webhooks").option("--agent <id>", "Filter by agent ID").option("--include-inactive", "Include inactive webhooks").action(async (opts) => {
1252
- const gOpts = program.opts();
1253
- try {
1254
- const { client } = await createClient(gOpts.profile);
1255
- const params = {};
1256
- if (opts.agent) params.agent_id = opts.agent;
1257
- if (opts.includeInactive) params.include_inactive = true;
1258
- const data = await client.get("/webhooks", params);
1259
- if (gOpts.json) printJson(data);
1260
- else printTable((data.items ?? []).map((w) => ({
1261
- ID: w.id,
1262
- "Agent ID": w.agent_id,
1263
- URL: w.url,
1264
- Active: w.active,
1265
- Created: w.created_at
1266
- })));
1267
- } catch (err) {
1268
- printError(formatError(err));
1269
- process.exitCode = 1;
1270
- }
1271
- });
1272
- webhook.command("delete").description("Delete a webhook").argument("<webhook-id>", "Webhook ID").option("--yes", "Skip confirmation prompt").action(async (webhookId, opts) => {
1273
- const gOpts = program.opts();
1274
- try {
1275
- if (!opts.yes && !gOpts.json) {
1276
- if (!await promptConfirm(`Delete webhook ${webhookId}?`)) return;
1277
- }
1278
- const { client } = await createClient(gOpts.profile);
1279
- await client.delete(`/webhooks/${webhookId}`);
1280
- printMutationOk("Webhook deleted", gOpts.json);
1281
- } catch (err) {
1282
- printError(formatError(err));
1283
- process.exitCode = 1;
1284
- }
1285
- });
1286
- webhook.command("deactivate").description("Deactivate a webhook").argument("<webhook-id>", "Webhook ID").action(async (webhookId) => {
1287
- const gOpts = program.opts();
1288
- try {
1289
- const { client } = await createClient(gOpts.profile);
1290
- await client.patch(`/webhooks/${webhookId}`, { active: false });
1291
- printMutationOk("Webhook deactivated", gOpts.json);
1292
- } catch (err) {
1293
- printError(formatError(err));
1294
- process.exitCode = 1;
1295
- }
1296
- });
1297
- webhook.command("activate").description("Activate a webhook").argument("<webhook-id>", "Webhook ID").action(async (webhookId) => {
1298
- const gOpts = program.opts();
1299
- try {
1300
- const { client } = await createClient(gOpts.profile);
1301
- await client.patch(`/webhooks/${webhookId}`, { active: true });
1302
- printMutationOk("Webhook activated", gOpts.json);
1303
- } catch (err) {
1304
- printError(formatError(err));
1305
- process.exitCode = 1;
1306
- }
1307
- });
1308
- webhook.command("deliveries").description("List deliveries for a webhook").argument("<webhook-id>", "Webhook ID").option("--status <status>", "Filter by delivery status").option("--limit <n>", "Maximum number of deliveries").action(async (webhookId, opts) => {
1309
- const gOpts = program.opts();
1310
- try {
1311
- const { client } = await createClient(gOpts.profile);
1312
- const params = {};
1313
- if (opts.status) params.status = opts.status;
1314
- if (opts.limit) params.limit = opts.limit;
1315
- const data = await client.get(`/webhooks/${webhookId}/deliveries`, params);
1316
- if (gOpts.json) printJson(data);
1317
- else printTable((data.items ?? []).map((d) => ({
1318
- ID: d.id,
1319
- "Message ID": d.message_id,
1320
- Status: d.status,
1321
- Attempts: d.attempts,
1322
- "Last Error": d.last_error ?? "—",
1323
- Created: d.created_at
1324
- })));
1325
- } catch (err) {
1326
- printError(formatError(err));
1327
- process.exitCode = 1;
2101
+ const agentId = await resolveAgent(client, opts.agent, gOpts.as);
2102
+ const data = await client.post("/webhooks", {
2103
+ agent_id: agentId,
2104
+ url: opts.url
2105
+ });
2106
+ if (gOpts.json) printJson(data);
2107
+ else printTable([{
2108
+ ID: data.id,
2109
+ "Agent ID": data.agent_id,
2110
+ URL: data.url,
2111
+ Secret: data.secret,
2112
+ Active: data.active,
2113
+ Created: data.created_at
2114
+ }]);
2115
+ }));
2116
+ webhook.command("list").description("List webhooks").option("--agent <id>", "Filter by agent ID").option("--include-inactive", "Include inactive webhooks").action(withClient(program, async ({ client, gOpts }, opts) => {
2117
+ const params = {};
2118
+ if (opts.agent) params.agent_id = opts.agent;
2119
+ if (opts.includeInactive) params.include_inactive = true;
2120
+ const data = await client.get("/webhooks", params);
2121
+ if (gOpts.json) printJson(data);
2122
+ else printTable(data.items.map((w) => ({
2123
+ ID: w.id,
2124
+ "Agent ID": w.agent_id,
2125
+ URL: w.url,
2126
+ Active: w.active,
2127
+ Created: w.created_at
2128
+ })));
2129
+ }));
2130
+ webhook.command("delete").description("Delete a webhook").argument("<webhook-id>", "Webhook ID").option("--yes", "Skip confirmation prompt").action(withClient(program, async ({ client, gOpts }, webhookId, opts) => {
2131
+ if (!opts.yes && !gOpts.json) {
2132
+ if (!await promptConfirm(`Delete webhook ${webhookId}?`)) return;
1328
2133
  }
1329
- });
2134
+ await client.delete(`/webhooks/${webhookId}`);
2135
+ printMutationOk("Webhook deleted", gOpts.json);
2136
+ }));
2137
+ webhook.command("deactivate").description("Deactivate a webhook").argument("<webhook-id>", "Webhook ID").action(withClient(program, async ({ client, gOpts }, webhookId) => {
2138
+ await client.patch(`/webhooks/${webhookId}`, { active: false });
2139
+ printMutationOk("Webhook deactivated", gOpts.json);
2140
+ }));
2141
+ webhook.command("activate").description("Activate a webhook").argument("<webhook-id>", "Webhook ID").action(withClient(program, async ({ client, gOpts }, webhookId) => {
2142
+ await client.patch(`/webhooks/${webhookId}`, { active: true });
2143
+ printMutationOk("Webhook activated", gOpts.json);
2144
+ }));
2145
+ webhook.command("deliveries").description("List deliveries for a webhook").argument("<webhook-id>", "Webhook ID").option("--status <status>", "Filter by delivery status").option("--limit <n>", "Maximum number of deliveries").action(withClient(program, async ({ client, gOpts }, webhookId, opts) => {
2146
+ const params = {};
2147
+ if (opts.status) params.status = opts.status;
2148
+ if (opts.limit) params.limit = opts.limit;
2149
+ const data = await client.get(`/webhooks/${webhookId}/deliveries`, params);
2150
+ if (gOpts.json) printJson(data);
2151
+ else printTable(data.items.map((d) => ({
2152
+ ID: d.id,
2153
+ "Message ID": d.message_id,
2154
+ Status: d.status,
2155
+ Attempts: d.attempts,
2156
+ "Last Error": d.last_error ?? "—",
2157
+ Created: d.created_at
2158
+ })));
2159
+ }));
1330
2160
  }
1331
2161
  //#endregion
1332
2162
  //#region src/main.ts
1333
2163
  const { version } = createRequire(import.meta.url)("../package.json");
1334
- const program = new Command("rine").version(version).description("rine.network CLI — messaging infrastructure for AI agents").option("--profile <name>", "credential profile to use", "default").option("--json", "output as JSON").option("--table", "output as table");
2164
+ const program = new Command("rine").version(version).description("rine.network CLI — messaging infrastructure for AI agents").option("--profile <name>", "credential profile to use", "default").option("--json", "output as JSON").option("--table", "output as table").option("--as <agent>", "act as a specific agent (UUID or handle, e.g. bot@org.rine.network)");
1335
2165
  registerAuth(program);
1336
2166
  registerRegister(program);
1337
2167
  registerOrg(program);
1338
2168
  registerAgent(program);
1339
2169
  registerAgentProfile(program);
1340
2170
  registerMessages(program);
2171
+ registerGroup(program);
1341
2172
  registerDiscover(program);
1342
2173
  registerWebhook(program);
2174
+ registerKeys(program);
1343
2175
  registerStream(program);
1344
2176
  program.parse();
1345
2177
  //#endregion