clawntenna 0.12.4 → 0.12.6

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 CHANGED
@@ -24,7 +24,7 @@ await client.sendMessage(1, 'gm from my agent!');
24
24
  // Read recent messages
25
25
  const messages = await client.readMessages(1, { limit: 20 });
26
26
  for (const msg of messages) {
27
- console.log(`${msg.sender}: ${msg.text}`);
27
+ console.log(msg.sender, msg.content);
28
28
  }
29
29
 
30
30
  // Set your nickname
@@ -32,7 +32,7 @@ await client.setNickname(1, 'MyAgent');
32
32
 
33
33
  // Listen for new messages
34
34
  const unsub = client.onMessage(1, (msg) => {
35
- console.log(`${msg.sender}: ${msg.text}`);
35
+ console.log(msg.sender, msg.content);
36
36
  });
37
37
  ```
38
38
 
@@ -40,10 +40,11 @@ const unsub = client.onMessage(1, (msg) => {
40
40
 
41
41
  ```bash
42
42
  npx clawntenna init # Create wallet at ~/.config/clawntenna/credentials.json
43
- npx clawntenna send 1 "gm!" # Send to #general
44
- npx clawntenna read 1 # Read #general
45
- npx clawntenna read 1 --chain avalanche # Read on Avalanche
46
- npx clawntenna read 1 --chain baseSepolia # Read on Base Sepolia (testnet)
43
+ npx clawntenna app create --name "Ops Mesh" --description "Wallet-native coordination" --url https://example.com
44
+ npx clawntenna topic create --app "Ops Mesh" --name "general" --description "Primary coordination" --access public
45
+ npx clawntenna send --app "Ops Mesh" --topic "general" "gm!"
46
+ npx clawntenna read --app "Ops Mesh" --topic "general" --chain avalanche
47
+ npx clawntenna read --topic-id 1 --chain baseSepolia # Exact read on Base Sepolia (testnet)
47
48
  ```
48
49
 
49
50
  ### Credentials
@@ -108,7 +109,7 @@ const msgs = await client.readMessages(topicId, {
108
109
  limit: 50, // Max messages (default 50)
109
110
  fromBlock: 12345678 // Optional absolute starting block
110
111
  });
111
- // Returns: { topicId, sender, text, replyTo, mentions, timestamp, txHash, blockNumber }[]
112
+ // Returns: { topicId, sender, content, timestamp, txHash, blockNumber }[]
112
113
 
113
114
  // Subscribe to real-time messages
114
115
  const unsub = client.onMessage(topicId, (msg) => { ... });
@@ -3,7 +3,7 @@ import {
3
3
  CONFIG_DIR,
4
4
  loadCredentials,
5
5
  output
6
- } from "./chunk-6VM633OH.js";
6
+ } from "./chunk-YYE3F3KA.js";
7
7
 
8
8
  // src/cli/state.ts
9
9
  import { existsSync, mkdirSync, writeFileSync } from "fs";
@@ -21,7 +21,7 @@ function initState(address) {
21
21
  startedAt: now,
22
22
  lastScanAt: now,
23
23
  mode: "active",
24
- skillVersion: "0.12.3",
24
+ skillVersion: "0.12.6",
25
25
  lastSkillCheck: now
26
26
  },
27
27
  chains: {
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  CONFIG_DIR
4
- } from "./chunk-6VM633OH.js";
4
+ } from "./chunk-YYE3F3KA.js";
5
5
 
6
6
  // src/cli/skill.ts
7
7
  import { readFileSync, existsSync, copyFileSync, mkdirSync } from "fs";
@@ -11,12 +11,14 @@ var CHAINS = {
11
11
  shortName: "Sepolia",
12
12
  rpc: "https://sepolia.base.org",
13
13
  explorer: "https://sepolia.basescan.org",
14
+ explorerApi: "https://api.routescan.io/v2/network/testnet/evm/84532/etherscan/api",
14
15
  registry: "0xf39b193aedC1Ec9FD6C5ccc24fBAe58ba9f52413",
15
16
  keyManager: "0x5562B553a876CBdc8AA4B3fb0687f22760F4759e",
16
17
  schemaRegistry: "0xB7eB50e9058198b99b5b2589E6D70b2d99d5440a",
17
18
  identityRegistry: "0x8004AA63c570c570eBF15376c0dB199918BFe9Fb",
18
19
  escrow: "0x74e376C53f4afd5Cd32a77dDc627f477FcFC2333",
19
- defaultLookback: 2e5
20
+ defaultLookback: 2e5,
21
+ logChunkSize: 1e3
20
22
  },
21
23
  base: {
22
24
  chainId: 8453,
@@ -24,12 +26,14 @@ var CHAINS = {
24
26
  shortName: "Base",
25
27
  rpc: "https://base.publicnode.com",
26
28
  explorer: "https://basescan.org",
29
+ explorerApi: "https://api.routescan.io/v2/network/mainnet/evm/8453/etherscan/api",
27
30
  registry: "0x5fF6BF04F1B5A78ae884D977a3C80A0D8E2072bF",
28
31
  keyManager: "0xdc302ff43a34F6aEa19426D60C9D150e0661E4f4",
29
32
  schemaRegistry: "0x5c11d2eA4470eD9025D810A21a885FE16dC987Bd",
30
33
  identityRegistry: "0x8004A169FB4a3325136EB29fA0ceB6D2e539a432",
31
34
  escrow: "0x04eC9a25C942192834F447eC9192831B56Ae2D7D",
32
- defaultLookback: 2e5
35
+ defaultLookback: 2e5,
36
+ logChunkSize: 1e3
33
37
  },
34
38
  avalanche: {
35
39
  chainId: 43114,
@@ -37,14 +41,21 @@ var CHAINS = {
37
41
  shortName: "Avalanche",
38
42
  rpc: "https://api.avax.network/ext/bc/C/rpc",
39
43
  explorer: "https://snowtrace.io",
44
+ explorerApi: "https://api.routescan.io/v2/network/mainnet/evm/43114/etherscan/api",
40
45
  registry: "0x3Ca2FF0bD1b3633513299EB5d3e2d63e058b0713",
41
46
  keyManager: "0x5a5ea9D408FBA984fFf6e243Dcc71ff6E00C73E4",
42
47
  schemaRegistry: "0x23D96e610E8E3DA5341a75B77F1BFF7EA9c3A62B",
43
48
  identityRegistry: "0x8004A169FB4a3325136EB29fA0ceB6D2e539a432",
44
49
  escrow: "0x4068245c35a498Da4336aD1Ab0Fb71ef534bfd03",
45
- defaultLookback: 5e5
50
+ defaultLookback: 5e5,
51
+ logChunkSize: 2e3
46
52
  }
47
53
  };
54
+ var CHAIN_IDS = {
55
+ 84532: "baseSepolia",
56
+ 8453: "base",
57
+ 43114: "avalanche"
58
+ };
48
59
 
49
60
  // src/contracts.ts
50
61
  var REGISTRY_ABI = [
@@ -59,6 +70,7 @@ var REGISTRY_ABI = [
59
70
  "function getTopic(uint256 topicId) view returns (tuple(uint256 id, uint256 applicationId, string name, string description, address owner, address creator, uint64 createdAt, uint64 lastMessageAt, uint256 messageCount, uint8 accessLevel, bool active))",
60
71
  "function topicCount() view returns (uint256)",
61
72
  "function getApplicationTopics(uint256 appId) view returns (uint256[])",
73
+ "function getTopicIdByName(uint256 appId, string name) view returns (uint256)",
62
74
  // Members
63
75
  "function members(uint256 appId, address user) view returns (address account, string nickname, uint8 roles, uint64 joinedAt)",
64
76
  "function getMember(uint256 appId, address account) view returns (tuple(address account, string nickname, uint8 roles, uint64 joinedAt))",
@@ -1415,20 +1427,19 @@ function decryptMessage(jsonStr, key) {
1415
1427
  const decrypted = decrypt(jsonStr, key);
1416
1428
  if (!decrypted) return null;
1417
1429
  try {
1418
- const content = JSON.parse(decrypted);
1419
- if (typeof content === "object" && content.text) {
1420
- return {
1421
- text: content.text,
1422
- replyTo: content.replyTo || null,
1423
- replyText: content.replyText || null,
1424
- replyAuthor: content.replyAuthor || null,
1425
- mentions: content.mentions || null
1426
- };
1427
- }
1428
- return { text: decrypted, replyTo: null, replyText: null, replyAuthor: null, mentions: null };
1430
+ return JSON.parse(decrypted);
1429
1431
  } catch {
1430
- return { text: decrypted, replyTo: null, replyText: null, replyAuthor: null, mentions: null };
1432
+ return decrypted;
1433
+ }
1434
+ }
1435
+ function getMessageText(content) {
1436
+ if (typeof content === "string") return content;
1437
+ if (content && typeof content === "object" && "text" in content) {
1438
+ const text = content.text;
1439
+ if (typeof text === "string") return text;
1440
+ return JSON.stringify(text);
1431
1441
  }
1442
+ return null;
1432
1443
  }
1433
1444
  function toBase64(bytes) {
1434
1445
  if (typeof Buffer !== "undefined") {
@@ -3447,6 +3458,7 @@ async function withRetry(fn, options) {
3447
3458
  var Clawntenna = class _Clawntenna {
3448
3459
  provider;
3449
3460
  chainName;
3461
+ historyApiKey;
3450
3462
  _signer;
3451
3463
  _address;
3452
3464
  _registry;
@@ -3484,12 +3496,13 @@ var Clawntenna = class _Clawntenna {
3484
3496
  tokenDecimalsCache = /* @__PURE__ */ new Map();
3485
3497
  static ERC20_DECIMALS_ABI = ["function decimals() view returns (uint8)"];
3486
3498
  constructor(options = {}) {
3487
- const chainName = options.chain ?? "base";
3499
+ const chainName = options.chain ?? (options.chainId != null ? CHAIN_IDS[options.chainId] : void 0) ?? "base";
3488
3500
  const chain = CHAINS[chainName];
3489
3501
  if (!chain) throw new Error(`Unsupported chain: ${chainName}`);
3490
3502
  this.chainName = chainName;
3491
3503
  const rpcUrl = options.rpcUrl ?? chain.rpc;
3492
3504
  this.provider = new ethers.JsonRpcProvider(rpcUrl);
3505
+ this.historyApiKey = options.historyApiKey ?? null;
3493
3506
  const registryAddr = options.registryAddress ?? chain.registry;
3494
3507
  const keyManagerAddr = options.keyManagerAddress ?? chain.keyManager;
3495
3508
  const schemaRegistryAddr = options.schemaRegistryAddress ?? chain.schemaRegistry;
@@ -3547,6 +3560,16 @@ var Clawntenna = class _Clawntenna {
3547
3560
  get address() {
3548
3561
  return this._address;
3549
3562
  }
3563
+ supportsIndexedHistory() {
3564
+ return Boolean(CHAINS[this.chainName].explorerApi);
3565
+ }
3566
+ getIndexedHistorySource() {
3567
+ const api = CHAINS[this.chainName].explorerApi;
3568
+ if (!api) return null;
3569
+ if (api.includes("routescan")) return "RouteScan";
3570
+ if (api.includes("basescan")) return "BaseScan";
3571
+ return "Explorer API";
3572
+ }
3550
3573
  // ===== MESSAGING =====
3551
3574
  /**
3552
3575
  * Send an encrypted message to a topic.
@@ -3567,7 +3590,10 @@ var Clawntenna = class _Clawntenna {
3567
3590
  const messages = await this.readMessages(topicId, { limit: 50 });
3568
3591
  const original = messages.find((m) => m.txHash === options.replyTo);
3569
3592
  if (original) {
3570
- replyText = replyText || original.text.slice(0, 100);
3593
+ const originalText = getMessageText(original.content);
3594
+ if (originalText) {
3595
+ replyText = replyText || originalText.slice(0, 100);
3596
+ }
3571
3597
  replyAuthor = replyAuthor || original.sender;
3572
3598
  }
3573
3599
  } catch {
@@ -3619,40 +3645,42 @@ var Clawntenna = class _Clawntenna {
3619
3645
  async readMessages(topicId, options) {
3620
3646
  const limit = options?.limit ?? 50;
3621
3647
  const key = await this.getEncryptionKey(topicId);
3648
+ const chain = CHAINS[this.chainName];
3649
+ if (options?.fromBlock == null && chain.explorerApi) {
3650
+ return this._readMessagesFromExplorer(topicId, limit, key);
3651
+ }
3622
3652
  const filter = this.registry.filters.MessageSent(topicId);
3623
- const CHUNK_SIZE = 2e3;
3624
3653
  const currentBlock = await this.provider.getBlockNumber();
3625
- const chain = CHAINS[this.chainName];
3654
+ const chunkSize = chain.logChunkSize;
3626
3655
  const maxRange = options?.fromBlock != null ? currentBlock - options.fromBlock : chain.defaultLookback;
3627
3656
  const startBlock = currentBlock - maxRange;
3628
3657
  const allEvents = [];
3629
3658
  let toBlock = currentBlock;
3659
+ let batchSpan = chunkSize;
3630
3660
  while (toBlock > startBlock && allEvents.length < limit) {
3631
- const chunkFrom = Math.max(toBlock - CHUNK_SIZE + 1, startBlock);
3632
- const events = await this._wrapRpcError(
3633
- () => this.registry.queryFilter(filter, chunkFrom, toBlock),
3634
- "readMessages"
3661
+ const batchFrom = Math.max(toBlock - batchSpan + 1, startBlock);
3662
+ const queryCount = Math.ceil((toBlock - batchFrom + 1) / chunkSize);
3663
+ options?.onProgress?.({
3664
+ fromBlock: batchFrom,
3665
+ toBlock,
3666
+ queryCount
3667
+ });
3668
+ const events = await this._queryFilterChunked(
3669
+ this.registry,
3670
+ filter,
3671
+ batchFrom,
3672
+ toBlock,
3673
+ chunkSize
3635
3674
  );
3636
3675
  allEvents.unshift(...events);
3637
- toBlock = chunkFrom - 1;
3638
- }
3639
- const recent = allEvents.slice(-limit);
3640
- const messages = [];
3641
- for (const log of recent) {
3642
- const payloadStr = ethers.toUtf8String(log.args.payload);
3643
- const parsed = decryptMessage(payloadStr, key);
3644
- messages.push({
3645
- topicId,
3646
- sender: log.args.sender,
3647
- text: parsed?.text ?? "[decryption failed]",
3648
- replyTo: parsed?.replyTo ?? null,
3649
- mentions: parsed?.mentions ?? null,
3650
- timestamp: Number(log.args.timestamp),
3651
- txHash: log.transactionHash,
3652
- blockNumber: log.blockNumber
3653
- });
3676
+ toBlock = batchFrom - 1;
3677
+ if (options?.fromBlock != null) {
3678
+ batchSpan = chunkSize;
3679
+ } else {
3680
+ batchSpan = Math.min(batchSpan * 2, Math.max(toBlock - startBlock + 1, chunkSize));
3681
+ }
3654
3682
  }
3655
- return messages;
3683
+ return allEvents.slice(-limit).map((log) => this._decodeMessageLog(topicId, key, log));
3656
3684
  }
3657
3685
  /**
3658
3686
  * Subscribe to real-time messages on a topic.
@@ -3666,13 +3694,11 @@ var Clawntenna = class _Clawntenna {
3666
3694
  const handler = (tId, sender, payload, timestamp, event) => {
3667
3695
  if (!key) return;
3668
3696
  const payloadStr = ethers.toUtf8String(payload);
3669
- const parsed = decryptMessage(payloadStr, key);
3697
+ const content = decryptMessage(payloadStr, key);
3670
3698
  callback({
3671
3699
  topicId: Number(tId),
3672
3700
  sender,
3673
- text: parsed?.text ?? "[decryption failed]",
3674
- replyTo: parsed?.replyTo ?? null,
3675
- mentions: parsed?.mentions ?? null,
3701
+ content,
3676
3702
  timestamp: Number(timestamp),
3677
3703
  txHash: event.transactionHash,
3678
3704
  blockNumber: event.blockNumber
@@ -3756,6 +3782,10 @@ var Clawntenna = class _Clawntenna {
3756
3782
  async getApplicationTopics(appId) {
3757
3783
  return this.registry.getApplicationTopics(appId);
3758
3784
  }
3785
+ async getTopicIdByName(appId, name) {
3786
+ const topicId = await this.registry.getTopicIdByName(appId, name);
3787
+ return Number(topicId);
3788
+ }
3759
3789
  async getTopicCount() {
3760
3790
  const count = await this.registry.topicCount();
3761
3791
  return Number(count);
@@ -3868,6 +3898,10 @@ var Clawntenna = class _Clawntenna {
3868
3898
  };
3869
3899
  }, "getApplication");
3870
3900
  }
3901
+ async getApplicationIdByName(name) {
3902
+ const appId = await this.registry.applicationNames(name);
3903
+ return Number(appId);
3904
+ }
3871
3905
  // ===== FEES =====
3872
3906
  async getTopicMessageFee(topicId) {
3873
3907
  const [token, amount] = await this.registry.getTopicMessageFee(topicId);
@@ -4200,6 +4234,65 @@ var Clawntenna = class _Clawntenna {
4200
4234
  }
4201
4235
  return results;
4202
4236
  }
4237
+ async _readMessagesFromExplorer(topicId, limit, key) {
4238
+ const chain = CHAINS[this.chainName];
4239
+ if (!chain.explorerApi) {
4240
+ throw new Error(`Indexed history is not configured for ${this.chainName}.`);
4241
+ }
4242
+ const url = new URL(chain.explorerApi);
4243
+ url.searchParams.set("module", "logs");
4244
+ url.searchParams.set("action", "getLogs");
4245
+ url.searchParams.set("fromBlock", "0");
4246
+ url.searchParams.set("toBlock", "latest");
4247
+ url.searchParams.set("address", chain.registry);
4248
+ url.searchParams.set("topic0", ethers.id("MessageSent(uint256,address,bytes,uint256)"));
4249
+ url.searchParams.set("topic1", ethers.zeroPadValue(ethers.toBeHex(topicId), 32));
4250
+ url.searchParams.set("topic0_1_opr", "and");
4251
+ url.searchParams.set("page", "1");
4252
+ url.searchParams.set("offset", String(limit));
4253
+ url.searchParams.set("sort", "desc");
4254
+ if (this.historyApiKey) {
4255
+ url.searchParams.set("apikey", this.historyApiKey);
4256
+ }
4257
+ const response = await withRetry(() => fetch(url));
4258
+ if (!response.ok) {
4259
+ throw new Error(`Historical message lookup failed with HTTP ${response.status}.`);
4260
+ }
4261
+ const body = await response.json();
4262
+ if (Array.isArray(body.result)) {
4263
+ const iface = new ethers.Interface(REGISTRY_ABI);
4264
+ const decoded = body.result.map((log) => {
4265
+ const parsed = iface.parseLog({
4266
+ topics: log.topics,
4267
+ data: log.data
4268
+ });
4269
+ if (!parsed || parsed.name !== "MessageSent") return null;
4270
+ return this._decodeMessageLog(topicId, key, {
4271
+ args: parsed.args,
4272
+ transactionHash: log.transactionHash,
4273
+ blockNumber: Number(log.blockNumber)
4274
+ });
4275
+ }).filter((msg) => msg !== null);
4276
+ decoded.sort((a, b) => a.blockNumber - b.blockNumber);
4277
+ return decoded;
4278
+ }
4279
+ if (body.message === "No records found") {
4280
+ return [];
4281
+ }
4282
+ throw new Error(`Historical message lookup failed: ${body.message ?? "unexpected explorer response"}.`);
4283
+ }
4284
+ _decodeMessageLog(topicId, key, log) {
4285
+ const payloadStr = ethers.toUtf8String(log.args.payload);
4286
+ const content = decryptMessage(payloadStr, key);
4287
+ return {
4288
+ topicId,
4289
+ sender: log.args.sender,
4290
+ content,
4291
+ timestamp: Number(log.args.timestamp),
4292
+ txHash: log.transactionHash,
4293
+ blockNumber: log.blockNumber
4294
+ };
4295
+ }
4203
4296
  // ===== ESCROW INBOX (deposit → message bridge) =====
4204
4297
  /**
4205
4298
  * Reverse lookup: find the transaction hash that created a deposit.
@@ -4266,7 +4359,7 @@ var Clawntenna = class _Clawntenna {
4266
4359
  const receipt = await this.provider.getTransactionReceipt(txHash);
4267
4360
  if (receipt) {
4268
4361
  const msg = await this._parseMessageFromReceipt(receipt);
4269
- messageText = msg?.text ?? null;
4362
+ messageText = msg ? getMessageText(msg.content) : null;
4270
4363
  }
4271
4364
  } catch {
4272
4365
  }
@@ -4307,20 +4400,17 @@ var Clawntenna = class _Clawntenna {
4307
4400
  const sender = parsed.args.sender;
4308
4401
  const payloadBytes = parsed.args.payload;
4309
4402
  const timestamp = Number(parsed.args.timestamp);
4310
- let text = "[unable to decrypt]";
4403
+ let content = null;
4311
4404
  try {
4312
4405
  const key = await this.getEncryptionKey(topicId);
4313
4406
  const payloadStr = ethers.toUtf8String(payloadBytes);
4314
- const result = decryptMessage(payloadStr, key);
4315
- if (result) text = result.text;
4407
+ content = decryptMessage(payloadStr, key);
4316
4408
  } catch {
4317
4409
  }
4318
4410
  return {
4319
4411
  topicId,
4320
4412
  sender,
4321
- text,
4322
- replyTo: null,
4323
- mentions: null,
4413
+ content,
4324
4414
  timestamp,
4325
4415
  txHash: receipt.hash,
4326
4416
  blockNumber: receipt.blockNumber
@@ -4906,8 +4996,8 @@ function validateKeyAddress(creds) {
4906
4996
  }
4907
4997
  }
4908
4998
  async function runPostInit(address) {
4909
- const { initState } = await import("./state-AIF4NWKT.js");
4910
- const { copySkillFiles } = await import("./skill-NOMAG55P.js");
4999
+ const { initState } = await import("./state-4PIASWQ4.js");
5000
+ const { copySkillFiles } = await import("./skill-Y3WBBCRF.js");
4911
5001
  const stateResult = initState(address);
4912
5002
  const skillResult = copySkillFiles();
4913
5003
  return { stateResult, skillResult };
@@ -5019,8 +5109,8 @@ async function init(json = false) {
5019
5109
  console.log(` Fund with ETH on Base or AVAX on Avalanche for gas`);
5020
5110
  console.log("");
5021
5111
  console.log("Next steps:");
5022
- console.log(' npx clawntenna send 1 "gm!" # Post to #general');
5023
- console.log(" npx clawntenna read 1 # Read #general");
5112
+ console.log(' npx clawntenna send --app "ClawtennaChat" --topic "general" "gm!"');
5113
+ console.log(' npx clawntenna read --app "ClawtennaChat" --topic "general"');
5024
5114
  }
5025
5115
  }
5026
5116
  function formatPostInit(stateResult, skillResult) {
@@ -5068,7 +5158,13 @@ function loadClient(flags, requireWallet = true) {
5068
5158
  const chainId = chainIdForCredentials(flags.chain);
5069
5159
  const credsRpc = creds?.chains[chainId]?.rpc;
5070
5160
  const rpcUrl = flags.rpc ?? process.env.CLAWNTENNA_RPC_URL ?? credsRpc;
5071
- return new Clawntenna({ chain: flags.chain, privateKey: privateKey ?? void 0, rpcUrl });
5161
+ const historyApiKey = process.env.ROUTESCAN_API_KEY ?? process.env.BASESCAN_API_KEY;
5162
+ return new Clawntenna({
5163
+ chain: flags.chain,
5164
+ privateKey: privateKey ?? void 0,
5165
+ rpcUrl,
5166
+ historyApiKey
5167
+ });
5072
5168
  }
5073
5169
  function output(data, json) {
5074
5170
  if (json) {