clawntenna 0.8.2 → 0.8.4

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/cli/index.js CHANGED
@@ -1341,6 +1341,8 @@ function decrypt(jsonStr, key) {
1341
1341
  function encryptMessage(text, key, options) {
1342
1342
  const content = { text };
1343
1343
  if (options?.replyTo) content.replyTo = options.replyTo;
1344
+ if (options?.replyText) content.replyText = options.replyText;
1345
+ if (options?.replyAuthor) content.replyAuthor = options.replyAuthor;
1344
1346
  if (options?.mentions) content.mentions = options.mentions;
1345
1347
  return encrypt2(JSON.stringify(content), key);
1346
1348
  }
@@ -1353,12 +1355,14 @@ function decryptMessage(jsonStr, key) {
1353
1355
  return {
1354
1356
  text: content.text,
1355
1357
  replyTo: content.replyTo || null,
1358
+ replyText: content.replyText || null,
1359
+ replyAuthor: content.replyAuthor || null,
1356
1360
  mentions: content.mentions || null
1357
1361
  };
1358
1362
  }
1359
- return { text: decrypted, replyTo: null, mentions: null };
1363
+ return { text: decrypted, replyTo: null, replyText: null, replyAuthor: null, mentions: null };
1360
1364
  } catch {
1361
- return { text: decrypted, replyTo: null, mentions: null };
1365
+ return { text: decrypted, replyTo: null, replyText: null, replyAuthor: null, mentions: null };
1362
1366
  }
1363
1367
  }
1364
1368
  function toBase64(bytes) {
@@ -3343,9 +3347,24 @@ var Clawntenna = class {
3343
3347
  */
3344
3348
  async sendMessage(topicId, text, options) {
3345
3349
  if (!this.wallet) throw new Error("Wallet required to send messages");
3350
+ let replyText = options?.replyText;
3351
+ let replyAuthor = options?.replyAuthor;
3352
+ if (options?.replyTo && (!replyText || !replyAuthor)) {
3353
+ try {
3354
+ const messages = await this.readMessages(topicId, { limit: 50 });
3355
+ const original = messages.find((m) => m.txHash === options.replyTo);
3356
+ if (original) {
3357
+ replyText = replyText || original.text.slice(0, 100);
3358
+ replyAuthor = replyAuthor || original.sender;
3359
+ }
3360
+ } catch {
3361
+ }
3362
+ }
3346
3363
  const key = await this.getEncryptionKey(topicId);
3347
3364
  const encrypted = encryptMessage(text, key, {
3348
3365
  replyTo: options?.replyTo,
3366
+ replyText,
3367
+ replyAuthor,
3349
3368
  mentions: options?.mentions
3350
3369
  });
3351
3370
  return this.registry.sendMessage(topicId, ethers.toUtf8Bytes(encrypted));
@@ -3355,14 +3374,23 @@ var Clawntenna = class {
3355
3374
  */
3356
3375
  async readMessages(topicId, options) {
3357
3376
  const limit = options?.limit ?? 50;
3358
- const fromBlock = options?.fromBlock ?? -1e5;
3359
3377
  const key = await this.getEncryptionKey(topicId);
3360
3378
  const filter = this.registry.filters.MessageSent(topicId);
3361
- const events = await this.registry.queryFilter(filter, fromBlock);
3379
+ const CHUNK_SIZE = 2e3;
3380
+ const currentBlock = await this.provider.getBlockNumber();
3381
+ const maxRange = options?.fromBlock != null ? currentBlock - options.fromBlock : 1e5;
3382
+ const startBlock = currentBlock - maxRange;
3383
+ const allEvents = [];
3384
+ let toBlock = currentBlock;
3385
+ while (toBlock > startBlock && allEvents.length < limit) {
3386
+ const chunkFrom = Math.max(toBlock - CHUNK_SIZE + 1, startBlock);
3387
+ const events = await this.registry.queryFilter(filter, chunkFrom, toBlock);
3388
+ allEvents.unshift(...events);
3389
+ toBlock = chunkFrom - 1;
3390
+ }
3391
+ const recent = allEvents.slice(-limit);
3362
3392
  const messages = [];
3363
- const recent = events.slice(-limit);
3364
- for (const event of recent) {
3365
- const log = event;
3393
+ for (const log of recent) {
3366
3394
  const payloadStr = ethers.toUtf8String(log.args.payload);
3367
3395
  const parsed = decryptMessage(payloadStr, key);
3368
3396
  messages.push({
@@ -3601,6 +3629,12 @@ var Clawntenna = class {
3601
3629
  async grantKeyAccess(topicId, userAddress, topicKey) {
3602
3630
  if (!this.wallet) throw new Error("Wallet required");
3603
3631
  if (!this.ecdhPrivateKey) throw new Error("ECDH key not derived yet");
3632
+ const hasKey = await this.keyManager.hasPublicKey(userAddress);
3633
+ if (!hasKey) {
3634
+ throw new Error(
3635
+ `User ${userAddress} has no ECDH public key registered. They must run 'keys register' first.`
3636
+ );
3637
+ }
3604
3638
  const userPubKeyBytes = ethers.getBytes(await this.keyManager.getPublicKey(userAddress));
3605
3639
  const encrypted = encryptTopicKeyForUser(topicKey, this.ecdhPrivateKey, userPubKeyBytes);
3606
3640
  return this.keyManager.grantKeyAccess(topicId, userAddress, encrypted);
@@ -3974,8 +4008,11 @@ var Clawntenna = class {
3974
4008
  if (storedKey) return storedKey;
3975
4009
  const topic = await this.getTopic(topicId);
3976
4010
  if (topic.accessLevel === 2 /* PRIVATE */) {
4011
+ if (this.ecdhPrivateKey) {
4012
+ return this.fetchAndDecryptTopicKey(topicId);
4013
+ }
3977
4014
  throw new Error(
3978
- `Topic ${topicId} is PRIVATE. Call fetchAndDecryptTopicKey() or setTopicKey() first.`
4015
+ `Topic ${topicId} is PRIVATE. Load ECDH keys first (loadECDHKeypair or deriveECDHFromWallet), then call fetchAndDecryptTopicKey() or setTopicKey().`
3979
4016
  );
3980
4017
  }
3981
4018
  return derivePublicTopicKey(topicId);
@@ -4149,6 +4186,12 @@ async function send(topicId, message, flags) {
4149
4186
  const client = loadClient(flags);
4150
4187
  const json = flags.json ?? false;
4151
4188
  const noWait = flags.noWait ?? false;
4189
+ const creds = loadCredentials();
4190
+ const chainId = flags.chain === "base" ? "8453" : "43114";
4191
+ const ecdhCreds = creds?.chains[chainId]?.ecdh;
4192
+ if (ecdhCreds?.privateKey) {
4193
+ client.loadECDHKeypair(ecdhCreds.privateKey);
4194
+ }
4152
4195
  if (!json) console.log(`Sending to topic ${topicId} on ${flags.chain}...`);
4153
4196
  const sendOptions = {
4154
4197
  replyTo: flags.replyTo,
@@ -4189,6 +4232,12 @@ async function send(topicId, message, flags) {
4189
4232
  async function read(topicId, flags) {
4190
4233
  const client = loadClient(flags, false);
4191
4234
  const json = flags.json ?? false;
4235
+ const creds = loadCredentials();
4236
+ const chainId = flags.chain === "base" ? "8453" : "43114";
4237
+ const ecdhCreds = creds?.chains[chainId]?.ecdh;
4238
+ if (ecdhCreds?.privateKey) {
4239
+ client.loadECDHKeypair(ecdhCreds.privateKey);
4240
+ }
4192
4241
  if (!json) console.log(`Reading topic ${topicId} on ${flags.chain} (last ${flags.limit} messages)...
4193
4242
  `);
4194
4243
  const messages = await client.readMessages(topicId, { limit: flags.limit });
@@ -4286,6 +4335,7 @@ async function whoami(appId, flags) {
4286
4335
  }
4287
4336
 
4288
4337
  // src/cli/app.ts
4338
+ import { ethers as ethers4 } from "ethers";
4289
4339
  async function appInfo(appId, flags) {
4290
4340
  const client = loadClient(flags, false);
4291
4341
  const json = flags.json ?? false;
@@ -4322,9 +4372,22 @@ async function appCreate(name, description, url, isPublic, flags) {
4322
4372
  const tx = await client.createApplication(name, description, url, isPublic);
4323
4373
  if (!json) console.log(`TX submitted: ${tx.hash}`);
4324
4374
  const receipt = await tx.wait();
4375
+ let appId = null;
4376
+ if (receipt) {
4377
+ const iface = new ethers4.Interface(REGISTRY_ABI);
4378
+ const parsed = receipt.logs.map((l) => {
4379
+ try {
4380
+ return iface.parseLog(l);
4381
+ } catch {
4382
+ return null;
4383
+ }
4384
+ }).find((l) => l?.name === "ApplicationCreated");
4385
+ appId = parsed?.args?.applicationId?.toString() ?? null;
4386
+ }
4325
4387
  if (json) {
4326
- output({ txHash: tx.hash, blockNumber: receipt?.blockNumber, chain: flags.chain }, true);
4388
+ output({ txHash: tx.hash, blockNumber: receipt?.blockNumber, appId, chain: flags.chain }, true);
4327
4389
  } else {
4390
+ if (appId) console.log(`Application created with ID: ${appId}`);
4328
4391
  console.log(`Confirmed in block ${receipt?.blockNumber}`);
4329
4392
  }
4330
4393
  }
@@ -4343,6 +4406,7 @@ async function appUpdateUrl(appId, url, flags) {
4343
4406
  }
4344
4407
 
4345
4408
  // src/cli/topics.ts
4409
+ import { ethers as ethers5 } from "ethers";
4346
4410
  var ACCESS_NAMES = ["public", "limited", "private"];
4347
4411
  async function topicsList(appId, flags) {
4348
4412
  const client = loadClient(flags, false);
@@ -4422,9 +4486,22 @@ async function topicCreate(appId, name, description, access, flags) {
4422
4486
  if (!json) console.log(`Creating topic "${name}" in app ${appId} (${access})...`);
4423
4487
  const tx = await client.createTopic(appId, name, description, level);
4424
4488
  const receipt = await tx.wait();
4489
+ let topicId = null;
4490
+ if (receipt) {
4491
+ const iface = new ethers5.Interface(REGISTRY_ABI);
4492
+ const parsed = receipt.logs.map((l) => {
4493
+ try {
4494
+ return iface.parseLog(l);
4495
+ } catch {
4496
+ return null;
4497
+ }
4498
+ }).find((l) => l?.name === "TopicCreated");
4499
+ topicId = parsed?.args?.topicId?.toString() ?? null;
4500
+ }
4425
4501
  if (json) {
4426
- output({ txHash: tx.hash, blockNumber: receipt?.blockNumber, appId, access }, true);
4502
+ output({ txHash: tx.hash, blockNumber: receipt?.blockNumber, topicId, appId, access }, true);
4427
4503
  } else {
4504
+ if (topicId) console.log(`Topic created with ID: ${topicId}`);
4428
4505
  console.log(`TX: ${tx.hash}`);
4429
4506
  console.log(`Confirmed in block ${receipt?.blockNumber}`);
4430
4507
  }
@@ -4469,10 +4546,12 @@ async function nicknameClear(appId, flags) {
4469
4546
  }
4470
4547
 
4471
4548
  // src/cli/members.ts
4549
+ import { ethers as ethers6 } from "ethers";
4472
4550
  async function membersList(appId, flags) {
4473
4551
  const client = loadClient(flags, false);
4474
4552
  const json = flags.json ?? false;
4475
- const addresses = await client.getApplicationMembers(appId);
4553
+ const raw = await client.getApplicationMembers(appId);
4554
+ const addresses = [...new Set(raw)].filter((a) => a !== ethers6.ZeroAddress);
4476
4555
  const members = await Promise.all(
4477
4556
  addresses.map(async (addr) => {
4478
4557
  const m = await client.getMember(appId, addr);
@@ -4921,7 +5000,7 @@ async function subscribe(topicId, flags) {
4921
5000
  }
4922
5001
 
4923
5002
  // src/cli/fees.ts
4924
- import { ethers as ethers4 } from "ethers";
5003
+ import { ethers as ethers7 } from "ethers";
4925
5004
  async function feeTopicCreationSet(appId, token, amount, flags) {
4926
5005
  const client = loadClient(flags);
4927
5006
  const json = flags.json ?? false;
@@ -4952,7 +5031,7 @@ async function feeMessageGet(topicId, flags) {
4952
5031
  const client = loadClient(flags, false);
4953
5032
  const json = flags.json ?? false;
4954
5033
  const fee = await client.getTopicMessageFee(topicId);
4955
- const isZero = fee.token === ethers4.ZeroAddress && fee.amount === 0n;
5034
+ const isZero = fee.token === ethers7.ZeroAddress && fee.amount === 0n;
4956
5035
  if (json) {
4957
5036
  output({ topicId, token: fee.token, amount: fee.amount.toString() }, true);
4958
5037
  } else {
@@ -4966,8 +5045,57 @@ async function feeMessageGet(topicId, flags) {
4966
5045
  }
4967
5046
  }
4968
5047
 
5048
+ // src/cli/errors.ts
5049
+ var ERROR_MAP = {
5050
+ "0xea8e4eb5": "NotAuthorized \u2014 you lack permission for this action",
5051
+ "0x291fc442": "NotMember \u2014 address is not a member of this app",
5052
+ "0x810074be": "AlreadyMember \u2014 address is already a member",
5053
+ "0x5e03d55f": "CannotRemoveSelf \u2014 owner cannot remove themselves",
5054
+ "0x17b29d2e": "ApplicationNotFound \u2014 app ID does not exist",
5055
+ "0x04a29d55": "TopicNotFound \u2014 topic ID does not exist",
5056
+ "0x430f13b3": "InvalidName \u2014 name is empty or invalid",
5057
+ "0x9e4b2685": "NameTaken \u2014 that name is already in use",
5058
+ "0xa2d0fee8": "InvalidPublicKey \u2014 must be 33-byte compressed secp256k1 key",
5059
+ "0x16ea6d54": "PublicKeyNotRegistered \u2014 user has no ECDH key (run: keys register)",
5060
+ "0x5303c506": "InvalidEncryptedKey \u2014 encrypted key too short or malformed",
5061
+ "0xf4d678b8": "InsufficientBalance \u2014 not enough tokens",
5062
+ "0x13be252b": "InsufficientAllowance \u2014 token allowance too low",
5063
+ "0x0c79a8da": "InvalidAccessLevel \u2014 use public, limited, or private",
5064
+ "0x15b3521e": "NicknameCooldownActive \u2014 wait before changing nickname again",
5065
+ "0xae0ca2dd": "SchemaNotFound \u2014 schema ID does not exist",
5066
+ "0x03230700": "AppNameTaken \u2014 schema name already used in this app"
5067
+ };
5068
+ function decodeContractError(err) {
5069
+ if (!(err instanceof Error)) return String(err);
5070
+ const message = err.message;
5071
+ const dataMatch = message.match(/data="(0x[0-9a-fA-F]+)"/) ?? message.match(/error=\{[^}]*"data":"(0x[0-9a-fA-F]+)"/) ?? message.match(/(0x[0-9a-fA-F]{8})/);
5072
+ if (dataMatch) {
5073
+ const selector = dataMatch[1].slice(0, 10).toLowerCase();
5074
+ const decoded = ERROR_MAP[selector];
5075
+ if (decoded) return decoded;
5076
+ }
5077
+ const anyErr = err;
5078
+ if (typeof anyErr.data === "string" && anyErr.data.startsWith("0x")) {
5079
+ const selector = anyErr.data.slice(0, 10).toLowerCase();
5080
+ const decoded = ERROR_MAP[selector];
5081
+ if (decoded) return decoded;
5082
+ }
5083
+ if (anyErr.info && typeof anyErr.info === "object") {
5084
+ const info = anyErr.info;
5085
+ if (info.error && typeof info.error === "object") {
5086
+ const innerErr = info.error;
5087
+ if (typeof innerErr.data === "string" && innerErr.data.startsWith("0x")) {
5088
+ const selector = innerErr.data.slice(0, 10).toLowerCase();
5089
+ const decoded = ERROR_MAP[selector];
5090
+ if (decoded) return decoded;
5091
+ }
5092
+ }
5093
+ }
5094
+ return message;
5095
+ }
5096
+
4969
5097
  // src/cli/index.ts
4970
- var VERSION = "0.8.2";
5098
+ var VERSION = "0.8.4";
4971
5099
  var HELP = `
4972
5100
  clawntenna v${VERSION}
4973
5101
  On-chain encrypted messaging for AI agents
@@ -5414,7 +5542,7 @@ async function main() {
5414
5542
  outputError(`Unknown command: ${command}. Run 'clawntenna --help' for usage.`, json);
5415
5543
  }
5416
5544
  } catch (err) {
5417
- const message = err instanceof Error ? err.message : String(err);
5545
+ const message = decodeContractError(err);
5418
5546
  if (json) {
5419
5547
  console.error(JSON.stringify({ error: message }));
5420
5548
  } else {
package/dist/index.cjs CHANGED
@@ -364,6 +364,8 @@ function decrypt(jsonStr, key) {
364
364
  function encryptMessage(text, key, options) {
365
365
  const content = { text };
366
366
  if (options?.replyTo) content.replyTo = options.replyTo;
367
+ if (options?.replyText) content.replyText = options.replyText;
368
+ if (options?.replyAuthor) content.replyAuthor = options.replyAuthor;
367
369
  if (options?.mentions) content.mentions = options.mentions;
368
370
  return encrypt(JSON.stringify(content), key);
369
371
  }
@@ -376,12 +378,14 @@ function decryptMessage(jsonStr, key) {
376
378
  return {
377
379
  text: content.text,
378
380
  replyTo: content.replyTo || null,
381
+ replyText: content.replyText || null,
382
+ replyAuthor: content.replyAuthor || null,
379
383
  mentions: content.mentions || null
380
384
  };
381
385
  }
382
- return { text: decrypted, replyTo: null, mentions: null };
386
+ return { text: decrypted, replyTo: null, replyText: null, replyAuthor: null, mentions: null };
383
387
  } catch {
384
- return { text: decrypted, replyTo: null, mentions: null };
388
+ return { text: decrypted, replyTo: null, replyText: null, replyAuthor: null, mentions: null };
385
389
  }
386
390
  }
387
391
  function toBase64(bytes) {
@@ -500,9 +504,24 @@ var Clawntenna = class {
500
504
  */
501
505
  async sendMessage(topicId, text, options) {
502
506
  if (!this.wallet) throw new Error("Wallet required to send messages");
507
+ let replyText = options?.replyText;
508
+ let replyAuthor = options?.replyAuthor;
509
+ if (options?.replyTo && (!replyText || !replyAuthor)) {
510
+ try {
511
+ const messages = await this.readMessages(topicId, { limit: 50 });
512
+ const original = messages.find((m) => m.txHash === options.replyTo);
513
+ if (original) {
514
+ replyText = replyText || original.text.slice(0, 100);
515
+ replyAuthor = replyAuthor || original.sender;
516
+ }
517
+ } catch {
518
+ }
519
+ }
503
520
  const key = await this.getEncryptionKey(topicId);
504
521
  const encrypted = encryptMessage(text, key, {
505
522
  replyTo: options?.replyTo,
523
+ replyText,
524
+ replyAuthor,
506
525
  mentions: options?.mentions
507
526
  });
508
527
  return this.registry.sendMessage(topicId, import_ethers.ethers.toUtf8Bytes(encrypted));
@@ -512,14 +531,23 @@ var Clawntenna = class {
512
531
  */
513
532
  async readMessages(topicId, options) {
514
533
  const limit = options?.limit ?? 50;
515
- const fromBlock = options?.fromBlock ?? -1e5;
516
534
  const key = await this.getEncryptionKey(topicId);
517
535
  const filter = this.registry.filters.MessageSent(topicId);
518
- const events = await this.registry.queryFilter(filter, fromBlock);
536
+ const CHUNK_SIZE = 2e3;
537
+ const currentBlock = await this.provider.getBlockNumber();
538
+ const maxRange = options?.fromBlock != null ? currentBlock - options.fromBlock : 1e5;
539
+ const startBlock = currentBlock - maxRange;
540
+ const allEvents = [];
541
+ let toBlock = currentBlock;
542
+ while (toBlock > startBlock && allEvents.length < limit) {
543
+ const chunkFrom = Math.max(toBlock - CHUNK_SIZE + 1, startBlock);
544
+ const events = await this.registry.queryFilter(filter, chunkFrom, toBlock);
545
+ allEvents.unshift(...events);
546
+ toBlock = chunkFrom - 1;
547
+ }
548
+ const recent = allEvents.slice(-limit);
519
549
  const messages = [];
520
- const recent = events.slice(-limit);
521
- for (const event of recent) {
522
- const log = event;
550
+ for (const log of recent) {
523
551
  const payloadStr = import_ethers.ethers.toUtf8String(log.args.payload);
524
552
  const parsed = decryptMessage(payloadStr, key);
525
553
  messages.push({
@@ -758,6 +786,12 @@ var Clawntenna = class {
758
786
  async grantKeyAccess(topicId, userAddress, topicKey) {
759
787
  if (!this.wallet) throw new Error("Wallet required");
760
788
  if (!this.ecdhPrivateKey) throw new Error("ECDH key not derived yet");
789
+ const hasKey = await this.keyManager.hasPublicKey(userAddress);
790
+ if (!hasKey) {
791
+ throw new Error(
792
+ `User ${userAddress} has no ECDH public key registered. They must run 'keys register' first.`
793
+ );
794
+ }
761
795
  const userPubKeyBytes = import_ethers.ethers.getBytes(await this.keyManager.getPublicKey(userAddress));
762
796
  const encrypted = encryptTopicKeyForUser(topicKey, this.ecdhPrivateKey, userPubKeyBytes);
763
797
  return this.keyManager.grantKeyAccess(topicId, userAddress, encrypted);
@@ -1131,8 +1165,11 @@ var Clawntenna = class {
1131
1165
  if (storedKey) return storedKey;
1132
1166
  const topic = await this.getTopic(topicId);
1133
1167
  if (topic.accessLevel === 2 /* PRIVATE */) {
1168
+ if (this.ecdhPrivateKey) {
1169
+ return this.fetchAndDecryptTopicKey(topicId);
1170
+ }
1134
1171
  throw new Error(
1135
- `Topic ${topicId} is PRIVATE. Call fetchAndDecryptTopicKey() or setTopicKey() first.`
1172
+ `Topic ${topicId} is PRIVATE. Load ECDH keys first (loadECDHKeypair or deriveECDHFromWallet), then call fetchAndDecryptTopicKey() or setTopicKey().`
1136
1173
  );
1137
1174
  }
1138
1175
  return derivePublicTopicKey(topicId);