@elisym/sdk 0.11.0 → 0.12.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.cjs CHANGED
@@ -1032,6 +1032,7 @@ async function createPaymentRequestWithOnchainConfig(rpc, programId, recipient,
1032
1032
  var RANKING_ACTIVITY_WINDOW_SECS = 30 * 24 * 60 * 60;
1033
1033
  var RANKING_BUCKET_SIZE_SECS = 60;
1034
1034
  var COLD_START_BUCKET = -Infinity;
1035
+ var NEVER_ABORTED_SIGNAL = new AbortController().signal;
1035
1036
  function toDTag(name) {
1036
1037
  const tag = name.toLowerCase().replace(/[^a-z0-9\s-]/g, (ch) => "_" + ch.charCodeAt(0).toString(16).padStart(2, "0")).replace(/\s+/g, "-").replace(/^-+|-+$/g, "");
1037
1038
  if (!tag) {
@@ -1061,13 +1062,63 @@ function compareAgentsByRank(a, b) {
1061
1062
  }
1062
1063
  return kb.lastSeen - ka.lastSeen;
1063
1064
  }
1065
+ function parseCapabilityEvent(event, network) {
1066
+ if (!nostrTools.verifyEvent(event)) {
1067
+ return null;
1068
+ }
1069
+ if (!event.content) {
1070
+ return null;
1071
+ }
1072
+ let raw;
1073
+ try {
1074
+ raw = JSON.parse(event.content);
1075
+ } catch {
1076
+ return null;
1077
+ }
1078
+ if (!raw || typeof raw !== "object") {
1079
+ return null;
1080
+ }
1081
+ const candidate = raw;
1082
+ if (typeof candidate.name !== "string" || !candidate.name) {
1083
+ return null;
1084
+ }
1085
+ if (typeof candidate.description !== "string") {
1086
+ return null;
1087
+ }
1088
+ if (!Array.isArray(candidate.capabilities) || !candidate.capabilities.every((cap) => typeof cap === "string")) {
1089
+ return null;
1090
+ }
1091
+ if (candidate.deleted) {
1092
+ return null;
1093
+ }
1094
+ const card = candidate;
1095
+ if (card.payment && (typeof card.payment.chain !== "string" || typeof card.payment.network !== "string" || typeof card.payment.address !== "string")) {
1096
+ return null;
1097
+ }
1098
+ if (card.payment?.job_price !== null && card.payment?.job_price !== void 0 && (!Number.isInteger(card.payment.job_price) || card.payment.job_price < 0)) {
1099
+ return null;
1100
+ }
1101
+ const agentNetwork = card.payment?.network ?? "devnet";
1102
+ if (agentNetwork !== network) {
1103
+ return null;
1104
+ }
1105
+ const kTags = event.tags.filter((tag) => tag[0] === "k").map((tag) => parseInt(tag[1] ?? "", 10)).filter((kind) => !isNaN(kind));
1106
+ return {
1107
+ pubkey: event.pubkey,
1108
+ npub: nostrTools.nip19.npubEncode(event.pubkey),
1109
+ cards: [card],
1110
+ eventId: event.id,
1111
+ supportedKinds: kTags,
1112
+ lastSeen: event.created_at
1113
+ };
1114
+ }
1064
1115
  function buildAgentsFromEvents(events, network) {
1065
1116
  const latestByDTag = /* @__PURE__ */ new Map();
1066
1117
  for (const event of events) {
1067
1118
  if (!nostrTools.verifyEvent(event)) {
1068
1119
  continue;
1069
1120
  }
1070
- const dTag = event.tags.find((t) => t[0] === "d")?.[1] ?? "";
1121
+ const dTag = event.tags.find((tag) => tag[0] === "d")?.[1] ?? "";
1071
1122
  const key = `${event.pubkey}:${dTag}`;
1072
1123
  const prev = latestByDTag.get(key);
1073
1124
  if (!prev || event.created_at > prev.created_at) {
@@ -1076,82 +1127,51 @@ function buildAgentsFromEvents(events, network) {
1076
1127
  }
1077
1128
  const accumMap = /* @__PURE__ */ new Map();
1078
1129
  for (const event of latestByDTag.values()) {
1079
- try {
1080
- if (!event.content) {
1081
- continue;
1082
- }
1083
- const raw = JSON.parse(event.content);
1084
- if (!raw || typeof raw !== "object") {
1085
- continue;
1086
- }
1087
- if (typeof raw.name !== "string" || !raw.name) {
1088
- continue;
1089
- }
1090
- if (typeof raw.description !== "string") {
1091
- continue;
1092
- }
1093
- if (!Array.isArray(raw.capabilities) || !raw.capabilities.every((c) => typeof c === "string")) {
1094
- continue;
1095
- }
1096
- if (raw.deleted) {
1097
- continue;
1098
- }
1099
- const card = raw;
1100
- if (card.payment && (typeof card.payment.chain !== "string" || typeof card.payment.network !== "string" || typeof card.payment.address !== "string")) {
1101
- continue;
1102
- }
1103
- if (card.payment?.job_price !== null && card.payment?.job_price !== void 0 && (!Number.isInteger(card.payment.job_price) || card.payment.job_price < 0)) {
1104
- continue;
1105
- }
1106
- const agentNetwork = card.payment?.network ?? "devnet";
1107
- if (agentNetwork !== network) {
1108
- continue;
1109
- }
1110
- const kTags = event.tags.filter((t) => t[0] === "k").map((t) => parseInt(t[1] ?? "", 10)).filter((k) => !isNaN(k));
1111
- const entry = { card, kTags, createdAt: event.created_at };
1112
- const existing = accumMap.get(event.pubkey);
1113
- if (existing) {
1114
- const dupIndex = existing.entries.findIndex((e) => e.card.name === card.name);
1115
- if (dupIndex >= 0) {
1116
- if (entry.createdAt >= existing.entries[dupIndex].createdAt) {
1117
- existing.entries[dupIndex] = entry;
1130
+ const parsed = parseCapabilityEvent(event, network);
1131
+ if (!parsed) {
1132
+ continue;
1133
+ }
1134
+ const card = parsed.cards[0];
1135
+ const cardKinds = parsed.supportedKinds;
1136
+ const createdAt = parsed.lastSeen;
1137
+ const existing = accumMap.get(parsed.pubkey);
1138
+ if (existing) {
1139
+ const prevForName = existing.perCard.get(card.name);
1140
+ if (prevForName) {
1141
+ if (createdAt >= prevForName.createdAt) {
1142
+ const idx = existing.agent.cards.findIndex(
1143
+ (existingCard) => existingCard.name === card.name
1144
+ );
1145
+ if (idx >= 0) {
1146
+ existing.agent.cards[idx] = card;
1118
1147
  }
1119
- } else {
1120
- existing.entries.push(entry);
1121
- }
1122
- if (event.created_at > existing.lastSeen) {
1123
- existing.lastSeen = event.created_at;
1124
- existing.eventId = event.id;
1148
+ existing.perCard.set(card.name, { createdAt, kTags: cardKinds });
1125
1149
  }
1126
1150
  } else {
1127
- accumMap.set(event.pubkey, {
1128
- pubkey: event.pubkey,
1129
- npub: nostrTools.nip19.npubEncode(event.pubkey),
1130
- entries: [entry],
1131
- eventId: event.id,
1132
- lastSeen: event.created_at
1133
- });
1151
+ existing.agent.cards.push(card);
1152
+ existing.perCard.set(card.name, { createdAt, kTags: cardKinds });
1134
1153
  }
1135
- } catch {
1154
+ if (createdAt > existing.agent.lastSeen) {
1155
+ existing.agent.lastSeen = createdAt;
1156
+ existing.agent.eventId = parsed.eventId;
1157
+ }
1158
+ } else {
1159
+ accumMap.set(parsed.pubkey, {
1160
+ agent: parsed,
1161
+ perCard: /* @__PURE__ */ new Map([[card.name, { createdAt, kTags: cardKinds }]])
1162
+ });
1136
1163
  }
1137
1164
  }
1138
1165
  const agentMap = /* @__PURE__ */ new Map();
1139
1166
  for (const [pubkey, acc] of accumMap) {
1140
1167
  const kindsSet = /* @__PURE__ */ new Set();
1141
- for (const e of acc.entries) {
1142
- for (const k of e.kTags) {
1143
- kindsSet.add(k);
1144
- }
1145
- }
1146
- const supportedKinds = [...kindsSet];
1147
- agentMap.set(pubkey, {
1148
- pubkey: acc.pubkey,
1149
- npub: acc.npub,
1150
- cards: acc.entries.map((e) => e.card),
1151
- eventId: acc.eventId,
1152
- supportedKinds,
1153
- lastSeen: acc.lastSeen
1154
- });
1168
+ for (const { kTags } of acc.perCard.values()) {
1169
+ for (const kind of kTags) {
1170
+ kindsSet.add(kind);
1171
+ }
1172
+ }
1173
+ acc.agent.supportedKinds = [...kindsSet];
1174
+ agentMap.set(pubkey, acc.agent);
1155
1175
  }
1156
1176
  return agentMap;
1157
1177
  }
@@ -1266,6 +1286,19 @@ var DiscoveryService = class {
1266
1286
  const events = await this.pool.querySync(filter);
1267
1287
  const agentMap = buildAgentsFromEvents(events, network);
1268
1288
  const agents = Array.from(agentMap.values());
1289
+ return this.runEnrichment(agents, agentMap, NEVER_ABORTED_SIGNAL);
1290
+ }
1291
+ /**
1292
+ * Enrich an agent map with paid-job stats, feedback counters, and kind:0
1293
+ * metadata, then return them sorted by `compareAgentsByRank`. Mutates the
1294
+ * passed-in `Agent` objects in place.
1295
+ *
1296
+ * Shared between `fetchAgents` (one-shot) and `streamAgents` (post-EOSE
1297
+ * second pass). The `signal` short-circuits the post-query work; in-flight
1298
+ * pool queries are not cancellable today (they fall through to the standard
1299
+ * timeout) and the caller drops the resolved value.
1300
+ */
1301
+ async runEnrichment(agents, agentMap, signal) {
1269
1302
  const agentPubkeys = Array.from(agentMap.keys());
1270
1303
  if (agentPubkeys.length === 0) {
1271
1304
  return agents;
@@ -1273,9 +1306,9 @@ var DiscoveryService = class {
1273
1306
  const activitySince = Math.floor(Date.now() / 1e3) - RANKING_ACTIVITY_WINDOW_SECS;
1274
1307
  const resultKinds = /* @__PURE__ */ new Set();
1275
1308
  for (const agent of agentMap.values()) {
1276
- for (const k of agent.supportedKinds) {
1277
- if (k >= KIND_JOB_REQUEST_BASE && k < KIND_JOB_RESULT_BASE) {
1278
- resultKinds.add(KIND_JOB_RESULT_BASE + (k - KIND_JOB_REQUEST_BASE));
1309
+ for (const supportedKind of agent.supportedKinds) {
1310
+ if (supportedKind >= KIND_JOB_REQUEST_BASE && supportedKind < KIND_JOB_RESULT_BASE) {
1311
+ resultKinds.add(KIND_JOB_RESULT_BASE + (supportedKind - KIND_JOB_REQUEST_BASE));
1279
1312
  }
1280
1313
  }
1281
1314
  }
@@ -1295,6 +1328,9 @@ var DiscoveryService = class {
1295
1328
  ),
1296
1329
  this.enrichWithMetadata(agents)
1297
1330
  ]);
1331
+ if (signal.aborted) {
1332
+ return agents;
1333
+ }
1298
1334
  const deliveredJobsByProvider = /* @__PURE__ */ new Map();
1299
1335
  for (const ev of resultEvents) {
1300
1336
  if (!nostrTools.verifyEvent(ev)) {
@@ -1307,7 +1343,7 @@ var DiscoveryService = class {
1307
1343
  if (ev.created_at > agent.lastSeen) {
1308
1344
  agent.lastSeen = ev.created_at;
1309
1345
  }
1310
- const jobEventId = ev.tags.find((t) => t[0] === "e")?.[1];
1346
+ const jobEventId = ev.tags.find((tag) => tag[0] === "e")?.[1];
1311
1347
  if (jobEventId) {
1312
1348
  let delivered = deliveredJobsByProvider.get(ev.pubkey);
1313
1349
  if (!delivered) {
@@ -1321,7 +1357,7 @@ var DiscoveryService = class {
1321
1357
  if (!nostrTools.verifyEvent(ev)) {
1322
1358
  continue;
1323
1359
  }
1324
- const targetPubkey = ev.tags.find((t) => t[0] === "p")?.[1];
1360
+ const targetPubkey = ev.tags.find((tag) => tag[0] === "p")?.[1];
1325
1361
  if (!targetPubkey) {
1326
1362
  continue;
1327
1363
  }
@@ -1332,17 +1368,17 @@ var DiscoveryService = class {
1332
1368
  if (ev.created_at > agent.lastSeen) {
1333
1369
  agent.lastSeen = ev.created_at;
1334
1370
  }
1335
- const rating = ev.tags.find((t) => t[0] === "rating")?.[1];
1371
+ const rating = ev.tags.find((tag) => tag[0] === "rating")?.[1];
1336
1372
  if (rating === "1" || rating === "0") {
1337
1373
  agent.totalRatingCount = (agent.totalRatingCount ?? 0) + 1;
1338
1374
  if (rating === "1") {
1339
1375
  agent.positiveCount = (agent.positiveCount ?? 0) + 1;
1340
1376
  }
1341
1377
  }
1342
- const status = ev.tags.find((t) => t[0] === "status")?.[1];
1343
- const txTag = ev.tags.find((t) => t[0] === "tx");
1378
+ const status = ev.tags.find((tag) => tag[0] === "status")?.[1];
1379
+ const txTag = ev.tags.find((tag) => tag[0] === "tx");
1344
1380
  const txSignature = txTag?.[1];
1345
- const jobEventId = ev.tags.find((t) => t[0] === "e")?.[1];
1381
+ const jobEventId = ev.tags.find((tag) => tag[0] === "e")?.[1];
1346
1382
  const hasDeliveredResult = jobEventId !== void 0 && deliveredJobsByProvider.get(targetPubkey)?.has(jobEventId) === true;
1347
1383
  if (status === "payment-completed" && typeof txSignature === "string" && txSignature && hasDeliveredResult) {
1348
1384
  if (!agent.lastPaidJobAt || ev.created_at > agent.lastPaidJobAt) {
@@ -1354,6 +1390,135 @@ var DiscoveryService = class {
1354
1390
  agents.sort(compareAgentsByRank);
1355
1391
  return agents;
1356
1392
  }
1393
+ /**
1394
+ * Stream elisym agents progressively as relays deliver events.
1395
+ *
1396
+ * Two live subscriptions:
1397
+ * - kind:31990 (capability cards) - emits `onAgent(agent)` for every new or
1398
+ * updated `(pubkey, d-tag)`. The emitted Agent is the merged view across
1399
+ * all surviving cards for that author.
1400
+ * - kind:6100 (default-offset job results) tagged `t=elisym` since 30d ago -
1401
+ * emits `onPaidJob(pubkey, ts)` for each delivered result. Custom-kind
1402
+ * results (offset != 100) are not on this stream; they enter the final
1403
+ * ranking via the post-EOSE enrichment pass.
1404
+ *
1405
+ * After capabilities EOSE, an enrichment pass runs in parallel to the live
1406
+ * subscriptions and produces a ranked snapshot via `onComplete`. The snapshot
1407
+ * is a clone, so further live updates do not mutate it.
1408
+ *
1409
+ * `closer.close()` tears down both subscriptions and aborts an in-flight
1410
+ * enrichment. If `opts.signal` is provided, aborting it does the same.
1411
+ */
1412
+ streamAgents(network, opts) {
1413
+ const eventsByPubkey = /* @__PURE__ */ new Map();
1414
+ const agentByPubkey = /* @__PURE__ */ new Map();
1415
+ const eoseSeen = { caps: false, results: false };
1416
+ let enrichmentStarted = false;
1417
+ const enrichmentAbort = new AbortController();
1418
+ const onExternalAbort = () => enrichmentAbort.abort();
1419
+ if (opts.signal) {
1420
+ if (opts.signal.aborted) {
1421
+ enrichmentAbort.abort();
1422
+ } else {
1423
+ opts.signal.addEventListener("abort", onExternalAbort, { once: true });
1424
+ }
1425
+ }
1426
+ const checkEose = () => {
1427
+ if (eoseSeen.caps && eoseSeen.results) {
1428
+ opts.onEose?.();
1429
+ }
1430
+ };
1431
+ const startEnrichment = () => {
1432
+ if (enrichmentStarted) {
1433
+ return;
1434
+ }
1435
+ enrichmentStarted = true;
1436
+ const snapshotAgents = Array.from(agentByPubkey.values()).map((agent) => ({ ...agent }));
1437
+ const snapshotMap = new Map(snapshotAgents.map((agent) => [agent.pubkey, agent]));
1438
+ void this.runEnrichment(snapshotAgents, snapshotMap, enrichmentAbort.signal).then(
1439
+ (sorted) => {
1440
+ if (enrichmentAbort.signal.aborted) {
1441
+ return;
1442
+ }
1443
+ opts.onComplete?.(sorted);
1444
+ },
1445
+ () => {
1446
+ }
1447
+ );
1448
+ };
1449
+ const capSub = this.pool.subscribe(
1450
+ { kinds: [KIND_APP_HANDLER], "#t": ["elisym"] },
1451
+ (event) => {
1452
+ const dTag = event.tags.find((tag) => tag[0] === "d")?.[1] ?? "";
1453
+ let perDTag = eventsByPubkey.get(event.pubkey);
1454
+ const prev = perDTag?.get(dTag);
1455
+ if (prev && event.created_at <= prev.created_at) {
1456
+ return;
1457
+ }
1458
+ if (!nostrTools.verifyEvent(event)) {
1459
+ return;
1460
+ }
1461
+ if (!event.content) {
1462
+ return;
1463
+ }
1464
+ let payload;
1465
+ try {
1466
+ payload = JSON.parse(event.content);
1467
+ } catch {
1468
+ return;
1469
+ }
1470
+ const isTombstone = payload !== null && typeof payload === "object" && Boolean(payload.deleted);
1471
+ if (!isTombstone && !parseCapabilityEvent(event, network)) {
1472
+ return;
1473
+ }
1474
+ if (!perDTag) {
1475
+ perDTag = /* @__PURE__ */ new Map();
1476
+ eventsByPubkey.set(event.pubkey, perDTag);
1477
+ }
1478
+ perDTag.set(dTag, event);
1479
+ const merged = buildAgentsFromEvents(Array.from(perDTag.values()), network).get(
1480
+ event.pubkey
1481
+ );
1482
+ if (!merged) {
1483
+ agentByPubkey.delete(event.pubkey);
1484
+ return;
1485
+ }
1486
+ agentByPubkey.set(event.pubkey, merged);
1487
+ opts.onAgent(merged);
1488
+ },
1489
+ {
1490
+ oneose: () => {
1491
+ eoseSeen.caps = true;
1492
+ startEnrichment();
1493
+ checkEose();
1494
+ }
1495
+ }
1496
+ );
1497
+ const activitySince = Math.floor(Date.now() / 1e3) - RANKING_ACTIVITY_WINDOW_SECS;
1498
+ const resultsSub = this.pool.subscribe(
1499
+ { kinds: [KIND_JOB_RESULT], "#t": ["elisym"], since: activitySince },
1500
+ (event) => {
1501
+ if (!nostrTools.verifyEvent(event)) {
1502
+ return;
1503
+ }
1504
+ opts.onPaidJob?.(event.pubkey, event.created_at);
1505
+ },
1506
+ {
1507
+ oneose: () => {
1508
+ eoseSeen.results = true;
1509
+ checkEose();
1510
+ }
1511
+ }
1512
+ );
1513
+ return {
1514
+ close: (reason) => {
1515
+ capSub.close(reason);
1516
+ resultsSub.close(reason);
1517
+ enrichmentAbort.abort();
1518
+ opts.signal?.removeEventListener("abort", onExternalAbort);
1519
+ }
1520
+ };
1521
+ }
1357
1522
  /**
1358
1523
  * Publish a capability card (kind:31990) as a provider.
1359
1524
  * Solana address is validated for Base58 format only - full decode
@@ -2569,8 +2734,11 @@ var NostrPool = class {
2569
2734
  throw new Error(`Failed to publish to all ${this.relays.length} relays`);
2570
2735
  }
2571
2736
  }
2572
- subscribe(filter, onEvent) {
2573
- const rawSub = this.pool.subscribeMany(this.relays, filter, { onevent: onEvent });
2737
+ subscribe(filter, onEvent, opts) {
2738
+ const rawSub = this.pool.subscribeMany(this.relays, filter, {
2739
+ onevent: onEvent,
2740
+ oneose: opts?.oneose
2741
+ });
2574
2742
  const tracked = {
2575
2743
  close: (reason) => {
2576
2744
  this.activeSubscriptions.delete(tracked);