@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.
- package/README.md +2 -2
- package/dist/main.js +149 -59
- 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
|
|
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](
|
|
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
|
|
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
|
|
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 (
|
|
1041
|
-
senderAgentId = await resolveToUuid(apiUrl,
|
|
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 (!
|
|
1050
|
-
else if (!
|
|
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 (
|
|
1053
|
-
else body.to_agent_id =
|
|
1054
|
-
if (
|
|
1055
|
-
if (
|
|
1056
|
-
const { state, groupId } = await getOrCreateSenderKey(client, configDir, senderAgentId,
|
|
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
|
-
|
|
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)
|
|
1379
|
-
|
|
1380
|
-
const
|
|
1381
|
-
|
|
1382
|
-
|
|
1383
|
-
|
|
1384
|
-
|
|
1385
|
-
|
|
1386
|
-
|
|
1387
|
-
|
|
1388
|
-
|
|
1389
|
-
|
|
1390
|
-
|
|
1391
|
-
|
|
1392
|
-
|
|
1393
|
-
|
|
1394
|
-
url
|
|
1395
|
-
|
|
1396
|
-
|
|
1397
|
-
|
|
1398
|
-
|
|
1399
|
-
|
|
1400
|
-
|
|
1401
|
-
|
|
1402
|
-
|
|
1403
|
-
|
|
1404
|
-
|
|
1405
|
-
|
|
1406
|
-
|
|
1407
|
-
|
|
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
|
-
|
|
1413
|
-
|
|
1414
|
-
|
|
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
|
-
|
|
1418
|
-
|
|
1499
|
+
emitLifecycle(gOpts, {
|
|
1500
|
+
state: "stopped",
|
|
1501
|
+
reason: "signal"
|
|
1502
|
+
});
|
|
1503
|
+
break;
|
|
1419
1504
|
}
|
|
1420
|
-
|
|
1421
|
-
|
|
1422
|
-
|
|
1423
|
-
|
|
1424
|
-
|
|
1425
|
-
|
|
1426
|
-
|
|
1427
|
-
|
|
1428
|
-
|
|
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.
|
|
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.
|
|
31
|
+
"@rine-network/core": "^0.4.1",
|
|
32
32
|
"commander": "^12.0.0",
|
|
33
33
|
"eventsource-client": "^1.1.0"
|
|
34
34
|
},
|