@rine-network/cli 0.8.4 → 0.9.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 +1 -1
  2. package/dist/main.js +135 -47
  3. package/package.json +2 -2
package/README.md CHANGED
@@ -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
@@ -1326,6 +1326,15 @@ function registerRegister(program) {
1326
1326
  }
1327
1327
  //#endregion
1328
1328
  //#region src/commands/stream.ts
1329
+ function emitLifecycle(gOpts, data) {
1330
+ if (gOpts.json) console.log(JSON.stringify({
1331
+ event: "lifecycle",
1332
+ data
1333
+ }));
1334
+ }
1335
+ function jitteredDelayMs(backoffSeconds) {
1336
+ return Math.round(backoffSeconds * (.5 + Math.random()) * 1e3);
1337
+ }
1329
1338
  async function formatMessageLine(dataStr, agentId, configDir, client) {
1330
1339
  try {
1331
1340
  const data = JSON.parse(dataStr);
@@ -1360,72 +1369,151 @@ async function formatMessageLine(dataStr, agentId, configDir, client) {
1360
1369
  }
1361
1370
  }
1362
1371
  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) => {
1372
+ 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
1373
  try {
1365
1374
  const gOpts = program.opts();
1366
1375
  const configDir = resolveConfigDir();
1367
1376
  const apiUrl = resolveApiUrl();
1368
1377
  const { client, profileName, entry } = await createClient(configDir, apiUrl, gOpts.profile);
1369
1378
  const agentId = await resolveAgent(apiUrl, await fetchAgents(client), opts.agent, gOpts.as);
1370
- const url = `${apiUrl}/agents/${agentId}/stream`;
1379
+ let url = `${apiUrl}/agents/${agentId}/stream`;
1380
+ if (opts.persistent) url += "?persistent=true";
1381
+ const heartbeatTimeoutMs = Math.max(Number(opts.heartbeatTimeout) || 70, 10) * 1e3;
1371
1382
  let lastEventId;
1372
1383
  let backoff = 1;
1384
+ let attempt = 0;
1373
1385
  let stopped = false;
1374
1386
  process.on("SIGINT", () => {
1375
1387
  stopped = true;
1376
1388
  process.exitCode = 0;
1377
1389
  });
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: () => {
1390
+ while (!stopped) {
1391
+ attempt++;
1392
+ const state = { disconnectReason: "server_close" };
1393
+ try {
1394
+ const envToken = process.env.RINE_TOKEN;
1395
+ const headers = {
1396
+ Authorization: `Bearer ${await getOrRefreshToken(configDir, apiUrl, entry, profileName, {
1397
+ force: false,
1398
+ envToken
1399
+ })}`,
1400
+ Accept: "text/event-stream"
1401
+ };
1402
+ if (lastEventId) headers["Last-Event-ID"] = lastEventId;
1403
+ emitLifecycle(gOpts, {
1404
+ state: "connecting",
1405
+ attempt,
1406
+ url
1407
+ });
1408
+ await new Promise((resolve) => {
1409
+ if (stopped) {
1410
+ resolve();
1411
+ return;
1412
+ }
1413
+ let heartbeatTimer;
1414
+ const resetHeartbeatTimer = () => {
1415
+ if (heartbeatTimer) clearTimeout(heartbeatTimer);
1416
+ heartbeatTimer = setTimeout(() => {
1417
+ state.disconnectReason = "heartbeat_timeout";
1418
+ es.close();
1419
+ resolve();
1420
+ }, heartbeatTimeoutMs);
1421
+ };
1422
+ const es = createEventSource({
1423
+ url,
1424
+ headers,
1425
+ onMessage: ({ event, data, id }) => {
1426
+ resetHeartbeatTimer();
1427
+ if (id) lastEventId = id;
1428
+ if (gOpts.json) console.log(JSON.stringify({
1429
+ event,
1430
+ id,
1431
+ data
1432
+ }));
1433
+ else if (event === "message") formatMessageLine(data, agentId, configDir, client).then((line) => console.log(line), () => console.log(`[message] ${data.slice(0, 80)}`));
1434
+ else if (event === "heartbeat" && opts.verbose) process.stderr.write(`[heartbeat] ${data}\n`);
1435
+ },
1436
+ onDisconnect: () => {
1437
+ if (heartbeatTimer) clearTimeout(heartbeatTimer);
1438
+ state.disconnectReason = "server_close";
1439
+ resolve();
1440
+ },
1441
+ onScheduleReconnect: () => {
1442
+ if (heartbeatTimer) clearTimeout(heartbeatTimer);
1443
+ es.close();
1444
+ resolve();
1445
+ }
1446
+ });
1447
+ resetHeartbeatTimer();
1448
+ if (attempt === 1 && !gOpts.json) {
1449
+ process.stderr.write(`Connected to stream for agent ${agentId}\n`);
1450
+ if (opts.persistent) process.stderr.write("Persistent mode (no server timeout)\n");
1451
+ }
1452
+ emitLifecycle(gOpts, {
1453
+ state: "connected",
1454
+ attempt
1455
+ });
1456
+ const onStop = () => {
1457
+ if (heartbeatTimer) clearTimeout(heartbeatTimer);
1458
+ state.disconnectReason = "signal";
1408
1459
  es.close();
1409
1460
  resolve();
1461
+ };
1462
+ if (stopped) {
1463
+ onStop();
1464
+ return;
1410
1465
  }
1466
+ process.once("SIGINT", onStop);
1411
1467
  });
1412
- const onStop = () => {
1413
- es.close();
1414
- resolve();
1415
- };
1468
+ if (state.disconnectReason === "heartbeat_timeout") {
1469
+ const jitteredBackoff = jitteredDelayMs(backoff);
1470
+ emitLifecycle(gOpts, {
1471
+ state: "reconnecting",
1472
+ reason: "heartbeat_timeout",
1473
+ attempt: attempt + 1,
1474
+ backoff_ms: jitteredBackoff
1475
+ });
1476
+ if (!gOpts.json) process.stderr.write(`Reconnecting (attempt ${attempt + 1}, heartbeat timeout)...\n`);
1477
+ await new Promise((r) => setTimeout(r, jitteredBackoff));
1478
+ backoff = Math.min(backoff * 2, 30);
1479
+ } else if (state.disconnectReason === "signal") {
1480
+ emitLifecycle(gOpts, {
1481
+ state: "stopped",
1482
+ reason: "signal"
1483
+ });
1484
+ break;
1485
+ } else {
1486
+ emitLifecycle(gOpts, {
1487
+ state: "reconnecting",
1488
+ reason: "server_close",
1489
+ attempt: attempt + 1,
1490
+ backoff_ms: 0
1491
+ });
1492
+ if (!gOpts.json) process.stderr.write(`Reconnecting (attempt ${attempt + 1}, server close)...\n`);
1493
+ backoff = 1;
1494
+ }
1495
+ } catch (err) {
1416
1496
  if (stopped) {
1417
- onStop();
1418
- return;
1497
+ emitLifecycle(gOpts, {
1498
+ state: "stopped",
1499
+ reason: "signal"
1500
+ });
1501
+ break;
1419
1502
  }
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);
1503
+ const errorMsg = err instanceof Error ? err.message : String(err);
1504
+ const jitteredBackoff = jitteredDelayMs(backoff);
1505
+ emitLifecycle(gOpts, {
1506
+ state: "reconnecting",
1507
+ reason: "error",
1508
+ attempt: attempt + 1,
1509
+ backoff_ms: jitteredBackoff,
1510
+ error: errorMsg
1511
+ });
1512
+ if (opts.verbose) process.stderr.write(`Reconnecting in ${Math.round(jitteredBackoff / 1e3)}s (attempt ${attempt + 1}, error: ${errorMsg})...\n`);
1513
+ else process.stderr.write(`Reconnecting (attempt ${attempt + 1}, error)...\n`);
1514
+ await new Promise((r) => setTimeout(r, jitteredBackoff));
1515
+ backoff = Math.min(backoff * 2, 30);
1516
+ }
1429
1517
  }
1430
1518
  } catch (err) {
1431
1519
  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.0",
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.0",
32
32
  "commander": "^12.0.0",
33
33
  "eventsource-client": "^1.1.0"
34
34
  },