@rine-network/cli 0.8.4 → 0.9.1

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 +2 -2
  2. package/dist/main.js +149 -59
  3. package/package.json +2 -2
package/README.md CHANGED
@@ -59,7 +59,7 @@ npx @rine-network/cli onboard --email you@example.com --name "My Org" --slug my-
59
59
  | `rine webhook ...` | Webhook management |
60
60
  | `rine discover ...` | Browse and search the agent directory |
61
61
  | `rine poll-token` | Generate or revoke inbox polling token |
62
- | `rine stream` | SSE real-time agent message stream |
62
+ | `rine stream` | SSE real-time stream with lifecycle events, persistent mode, heartbeat timeout |
63
63
 
64
64
  Use `rine --help` or `rine <command> --help` for full usage.
65
65
 
@@ -81,7 +81,7 @@ The CLI resolves its config directory in order: `$RINE_CONFIG_DIR` > `~/.config/
81
81
 
82
82
  ## License
83
83
 
84
- [EUPL-1.2](LICENSE) — European Union Public Licence v1.2
84
+ [EUPL-1.2](https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12)
85
85
 
86
86
  ## For AI Agents
87
87
 
package/dist/main.js CHANGED
@@ -1,6 +1,6 @@
1
1
  import { createRequire } from "node:module";
2
2
  import { Command } from "commander";
3
- import { HttpClient, RineApiError, UUID_RE, agentKeysExist, cacheToken, decryptGroupMessage, decryptMessage, encryptGroupMessage, encryptMessage, fetchAgents, fetchAndIngestPendingSKDistributions, fetchOAuthToken, fetchRecipientEncryptionKey, formatError, fromBase64Url, generateAgentKeys, getAgentPublicKeys, getCredentialEntry, getOrCreateSenderKey, getOrRefreshToken, ingestSenderKeyDistribution, isBareAgentName, loadAgentKeys, loadCredentials, loadTokenCache, performAgentCreation, performRegistration, resolveAgent, resolveApiUrl, resolveConfigDir, resolveHandleViaWebFinger, resolveToUuid, saveAgentKeys, saveCredentials, saveTokenCache, toBase64Url, validateEncryptionKey, validateSigningKey, validateSlug } from "@rine-network/core";
3
+ import { HttpClient, RineApiError, UUID_RE, agentKeysExist, cacheToken, decryptGroupMessage, decryptMessage, encryptGroupMessage, encryptMessage, fetchAgents, fetchAndIngestPendingSKDistributions, fetchOAuthToken, fetchRecipientEncryptionKey, formatError, fromBase64Url, generateAgentKeys, getAgentPublicKeys, getCredentialEntry, getOrCreateSenderKey, getOrRefreshToken, ingestSenderKeyDistribution, isBareAgentName, loadAgentKeys, loadCredentials, loadTokenCache, normalizeHandle, performAgentCreation, performRegistration, resolveAgent, resolveApiUrl, resolveConfigDir, resolveHandleViaWebFinger, resolveToUuid, saveAgentKeys, saveCredentials, saveTokenCache, toBase64Url, validateEncryptionKey, validateSigningKey, validateSlug } from "@rine-network/core";
4
4
  import readline from "node:readline";
5
5
  import * as fs from "node:fs";
6
6
  import { join } from "node:path";
@@ -635,7 +635,7 @@ function registerDiscover(program) {
635
635
  let resolvedId = agentIdArg;
636
636
  if (agentIdArg.includes("@")) resolvedId = await resolveHandleViaWebFinger(apiUrl, agentIdArg);
637
637
  if (!UUID_RE.test(resolvedId)) {
638
- printError(`Could not resolve '${agentIdArg}' to an agent UUID. Provide a UUID or a valid handle (e.g. agent@org.rine.network).`);
638
+ printError(`Could not resolve '${agentIdArg}' to an agent UUID. Provide a UUID or a valid handle (e.g. agent@org).`);
639
639
  process.exitCode = 1;
640
640
  return;
641
641
  }
@@ -1027,7 +1027,7 @@ function resolvePayload(payload, payloadFile) {
1027
1027
  }
1028
1028
  }
1029
1029
  function addMessageCommands(parent, program) {
1030
- parent.command("send").description("Send an encrypted message").requiredOption("--to <address>", "Recipient: agent handle, agent ID, or #group@org handle").option("--type <type>", "Message type (default: rine.v1.dm)").option("--payload <json>", "Message payload as JSON string").option("--payload-file <path>", "Read payload JSON from file (use - for stdin)").option("--from <address>", "Sender address (agent ID or handle with @)").option("--idempotency-key <key>", "Idempotency key header").action(withClient(program, async ({ client, gOpts, configDir, apiUrl }, opts) => {
1030
+ parent.command("send").description("Send an encrypted message").requiredOption("--to <address>", "Recipient: agent handle (agent@org), agent ID, or group handle (#group@org)").option("--type <type>", "Message type (default: rine.v1.dm)").option("--payload <json>", "Message payload as JSON string").option("--payload-file <path>", "Read payload JSON from file (use - for stdin)").option("--from <address>", "Sender: agent handle (agent@org) or agent ID").option("--idempotency-key <key>", "Idempotency key header").action(withClient(program, async ({ client, gOpts, configDir, apiUrl }, opts) => {
1031
1031
  const result = resolvePayload(opts.payload, opts.payloadFile);
1032
1032
  if ("error" in result) {
1033
1033
  printError(result.error);
@@ -1035,10 +1035,12 @@ function addMessageCommands(parent, program) {
1035
1035
  return;
1036
1036
  }
1037
1037
  const parsedPayload = result.parsed;
1038
+ const to = normalizeHandle(opts.to);
1039
+ const from = opts.from ? normalizeHandle(opts.from) : opts.from;
1038
1040
  const extraHeaders = {};
1039
1041
  let senderAgentId;
1040
- if (opts.from !== void 0) {
1041
- senderAgentId = await resolveToUuid(apiUrl, opts.from);
1042
+ if (from !== void 0) {
1043
+ senderAgentId = await resolveToUuid(apiUrl, from);
1042
1044
  extraHeaders["X-Rine-Agent"] = senderAgentId;
1043
1045
  } else if (!gOpts.as) {
1044
1046
  senderAgentId = await resolveAgent(apiUrl, await fetchAgents(client), void 0, void 0);
@@ -1046,14 +1048,14 @@ function addMessageCommands(parent, program) {
1046
1048
  } else senderAgentId = await resolveAgent(apiUrl, await fetchAgents(client), void 0, gOpts.as);
1047
1049
  if (opts.idempotencyKey) extraHeaders["Idempotency-Key"] = opts.idempotencyKey;
1048
1050
  let recipientAgentId;
1049
- if (!opts.to.includes("@")) recipientAgentId = opts.to;
1050
- else if (!opts.to.startsWith("#")) recipientAgentId = await resolveToUuid(apiUrl, opts.to);
1051
+ if (!to.includes("@")) recipientAgentId = to;
1052
+ else if (!to.startsWith("#")) recipientAgentId = await resolveToUuid(apiUrl, to);
1051
1053
  const body = { type: opts.type ?? "rine.v1.dm" };
1052
- if (opts.to.includes("@")) body.to_handle = opts.to;
1053
- else body.to_agent_id = opts.to;
1054
- if (opts.from?.includes("@")) body.from_handle = opts.from;
1055
- if (opts.to.startsWith("#") && senderAgentId) {
1056
- const { state, groupId } = await getOrCreateSenderKey(client, configDir, senderAgentId, opts.to, Object.keys(extraHeaders).length > 0 ? extraHeaders : void 0);
1054
+ if (to.includes("@")) body.to_handle = to;
1055
+ else body.to_agent_id = to;
1056
+ if (from?.includes("@")) body.from_handle = from;
1057
+ if (to.startsWith("#") && senderAgentId) {
1058
+ const { state, groupId } = await getOrCreateSenderKey(client, configDir, senderAgentId, to, Object.keys(extraHeaders).length > 0 ? extraHeaders : void 0);
1057
1059
  const { result: encResult } = await encryptGroupMessage(configDir, senderAgentId, groupId, state, parsedPayload);
1058
1060
  Object.assign(body, encResult);
1059
1061
  } else if (senderAgentId && recipientAgentId) {
@@ -1326,6 +1328,15 @@ function registerRegister(program) {
1326
1328
  }
1327
1329
  //#endregion
1328
1330
  //#region src/commands/stream.ts
1331
+ function emitLifecycle(gOpts, data) {
1332
+ if (gOpts.json) console.log(JSON.stringify({
1333
+ event: "lifecycle",
1334
+ data
1335
+ }));
1336
+ }
1337
+ function jitteredDelayMs(backoffSeconds) {
1338
+ return Math.round(backoffSeconds * (.5 + Math.random()) * 1e3);
1339
+ }
1329
1340
  async function formatMessageLine(dataStr, agentId, configDir, client) {
1330
1341
  try {
1331
1342
  const data = JSON.parse(dataStr);
@@ -1360,72 +1371,151 @@ async function formatMessageLine(dataStr, agentId, configDir, client) {
1360
1371
  }
1361
1372
  }
1362
1373
  function registerStream(program) {
1363
- 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) => {
1374
+ 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").option("--persistent", "Disable server-side connection timeout").option("--heartbeat-timeout <seconds>", "Seconds of silence before reconnecting (default: 70)", "70").action(async (opts) => {
1364
1375
  try {
1365
1376
  const gOpts = program.opts();
1366
1377
  const configDir = resolveConfigDir();
1367
1378
  const apiUrl = resolveApiUrl();
1368
1379
  const { client, profileName, entry } = await createClient(configDir, apiUrl, gOpts.profile);
1369
1380
  const agentId = await resolveAgent(apiUrl, await fetchAgents(client), opts.agent, gOpts.as);
1370
- const url = `${apiUrl}/agents/${agentId}/stream`;
1381
+ let url = `${apiUrl}/agents/${agentId}/stream`;
1382
+ if (opts.persistent) url += "?persistent=true";
1383
+ const heartbeatTimeoutMs = Math.max(Number(opts.heartbeatTimeout) || 70, 10) * 1e3;
1371
1384
  let lastEventId;
1372
1385
  let backoff = 1;
1386
+ let attempt = 0;
1373
1387
  let stopped = false;
1374
1388
  process.on("SIGINT", () => {
1375
1389
  stopped = true;
1376
1390
  process.exitCode = 0;
1377
1391
  });
1378
- while (!stopped) try {
1379
- const envToken = process.env.RINE_TOKEN;
1380
- const headers = {
1381
- Authorization: `Bearer ${await getOrRefreshToken(configDir, apiUrl, entry, profileName, {
1382
- force: false,
1383
- envToken
1384
- })}`,
1385
- Accept: "text/event-stream"
1386
- };
1387
- if (lastEventId) headers["Last-Event-ID"] = lastEventId;
1388
- await new Promise((resolve) => {
1389
- if (stopped) {
1390
- resolve();
1391
- return;
1392
- }
1393
- const es = createEventSource({
1394
- url,
1395
- headers,
1396
- onMessage: ({ event, data, id }) => {
1397
- if (id) lastEventId = id;
1398
- if (gOpts.json) console.log(JSON.stringify({
1399
- event,
1400
- id,
1401
- data
1402
- }));
1403
- else if (event === "message") formatMessageLine(data, agentId, configDir, client).then((line) => console.log(line), () => console.log(`[message] ${data.slice(0, 80)}`));
1404
- else if (event === "heartbeat" && opts.verbose) process.stderr.write(`[heartbeat] ${data}\n`);
1405
- },
1406
- onDisconnect: () => resolve(),
1407
- onScheduleReconnect: () => {
1392
+ while (!stopped) {
1393
+ attempt++;
1394
+ const state = { disconnectReason: "server_close" };
1395
+ try {
1396
+ const envToken = process.env.RINE_TOKEN;
1397
+ const headers = {
1398
+ Authorization: `Bearer ${await getOrRefreshToken(configDir, apiUrl, entry, profileName, {
1399
+ force: false,
1400
+ envToken
1401
+ })}`,
1402
+ Accept: "text/event-stream"
1403
+ };
1404
+ if (lastEventId) headers["Last-Event-ID"] = lastEventId;
1405
+ emitLifecycle(gOpts, {
1406
+ state: "connecting",
1407
+ attempt,
1408
+ url
1409
+ });
1410
+ await new Promise((resolve) => {
1411
+ if (stopped) {
1412
+ resolve();
1413
+ return;
1414
+ }
1415
+ let heartbeatTimer;
1416
+ const resetHeartbeatTimer = () => {
1417
+ if (heartbeatTimer) clearTimeout(heartbeatTimer);
1418
+ heartbeatTimer = setTimeout(() => {
1419
+ state.disconnectReason = "heartbeat_timeout";
1420
+ es.close();
1421
+ resolve();
1422
+ }, heartbeatTimeoutMs);
1423
+ };
1424
+ const es = createEventSource({
1425
+ url,
1426
+ headers,
1427
+ onMessage: ({ event, data, id }) => {
1428
+ resetHeartbeatTimer();
1429
+ if (id) lastEventId = id;
1430
+ if (gOpts.json) console.log(JSON.stringify({
1431
+ event,
1432
+ id,
1433
+ data
1434
+ }));
1435
+ else if (event === "message") formatMessageLine(data, agentId, configDir, client).then((line) => console.log(line), () => console.log(`[message] ${data.slice(0, 80)}`));
1436
+ else if (event === "heartbeat" && opts.verbose) process.stderr.write(`[heartbeat] ${data}\n`);
1437
+ },
1438
+ onDisconnect: () => {
1439
+ if (heartbeatTimer) clearTimeout(heartbeatTimer);
1440
+ state.disconnectReason = "server_close";
1441
+ resolve();
1442
+ },
1443
+ onScheduleReconnect: () => {
1444
+ if (heartbeatTimer) clearTimeout(heartbeatTimer);
1445
+ es.close();
1446
+ resolve();
1447
+ }
1448
+ });
1449
+ resetHeartbeatTimer();
1450
+ if (attempt === 1 && !gOpts.json) {
1451
+ process.stderr.write(`Connected to stream for agent ${agentId}\n`);
1452
+ if (opts.persistent) process.stderr.write("Persistent mode (no server timeout)\n");
1453
+ }
1454
+ emitLifecycle(gOpts, {
1455
+ state: "connected",
1456
+ attempt
1457
+ });
1458
+ const onStop = () => {
1459
+ if (heartbeatTimer) clearTimeout(heartbeatTimer);
1460
+ state.disconnectReason = "signal";
1408
1461
  es.close();
1409
1462
  resolve();
1463
+ };
1464
+ if (stopped) {
1465
+ onStop();
1466
+ return;
1410
1467
  }
1468
+ process.once("SIGINT", onStop);
1411
1469
  });
1412
- const onStop = () => {
1413
- es.close();
1414
- resolve();
1415
- };
1470
+ if (state.disconnectReason === "heartbeat_timeout") {
1471
+ const jitteredBackoff = jitteredDelayMs(backoff);
1472
+ emitLifecycle(gOpts, {
1473
+ state: "reconnecting",
1474
+ reason: "heartbeat_timeout",
1475
+ attempt: attempt + 1,
1476
+ backoff_ms: jitteredBackoff
1477
+ });
1478
+ if (!gOpts.json) process.stderr.write(`Reconnecting (attempt ${attempt + 1}, heartbeat timeout)...\n`);
1479
+ await new Promise((r) => setTimeout(r, jitteredBackoff));
1480
+ backoff = Math.min(backoff * 2, 30);
1481
+ } else if (state.disconnectReason === "signal") {
1482
+ emitLifecycle(gOpts, {
1483
+ state: "stopped",
1484
+ reason: "signal"
1485
+ });
1486
+ break;
1487
+ } else {
1488
+ emitLifecycle(gOpts, {
1489
+ state: "reconnecting",
1490
+ reason: "server_close",
1491
+ attempt: attempt + 1,
1492
+ backoff_ms: 0
1493
+ });
1494
+ if (!gOpts.json) process.stderr.write(`Reconnecting (attempt ${attempt + 1}, server close)...\n`);
1495
+ backoff = 1;
1496
+ }
1497
+ } catch (err) {
1416
1498
  if (stopped) {
1417
- onStop();
1418
- return;
1499
+ emitLifecycle(gOpts, {
1500
+ state: "stopped",
1501
+ reason: "signal"
1502
+ });
1503
+ break;
1419
1504
  }
1420
- process.once("SIGINT", onStop);
1421
- });
1422
- backoff = 1;
1423
- } catch {
1424
- if (stopped) break;
1425
- if (opts.verbose) process.stderr.write(`Reconnecting in ${backoff}s...\n`);
1426
- else process.stderr.write("Reconnecting...\n");
1427
- await new Promise((r) => setTimeout(r, backoff * 1e3));
1428
- backoff = Math.min(backoff * 2, 30);
1505
+ const errorMsg = err instanceof Error ? err.message : String(err);
1506
+ const jitteredBackoff = jitteredDelayMs(backoff);
1507
+ emitLifecycle(gOpts, {
1508
+ state: "reconnecting",
1509
+ reason: "error",
1510
+ attempt: attempt + 1,
1511
+ backoff_ms: jitteredBackoff,
1512
+ error: errorMsg
1513
+ });
1514
+ if (opts.verbose) process.stderr.write(`Reconnecting in ${Math.round(jitteredBackoff / 1e3)}s (attempt ${attempt + 1}, error: ${errorMsg})...\n`);
1515
+ else process.stderr.write(`Reconnecting (attempt ${attempt + 1}, error)...\n`);
1516
+ await new Promise((r) => setTimeout(r, jitteredBackoff));
1517
+ backoff = Math.min(backoff * 2, 30);
1518
+ }
1429
1519
  }
1430
1520
  } catch (err) {
1431
1521
  printError(formatError(err));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rine-network/cli",
3
- "version": "0.8.4",
3
+ "version": "0.9.1",
4
4
  "description": "CLI client for rine.network \u2014 EU-first messaging infrastructure for AI agents",
5
5
  "author": "mmmbs <mmmbs@proton.me>",
6
6
  "license": "EUPL-1.2",
@@ -28,7 +28,7 @@
28
28
  "dev": "tsx src/main.ts"
29
29
  },
30
30
  "dependencies": {
31
- "@rine-network/core": "^0.3.5",
31
+ "@rine-network/core": "^0.4.1",
32
32
  "commander": "^12.0.0",
33
33
  "eventsource-client": "^1.1.0"
34
34
  },