@spectratools/assembly-cli 0.2.0 → 0.3.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 +15 -7
  2. package/dist/cli.js +231 -27
  3. package/package.json +19 -2
package/README.md CHANGED
@@ -1,6 +1,11 @@
1
1
  # @spectratools/assembly-cli
2
2
 
3
- Assembly governance CLI for Abstract.
3
+ Assembly is the governance layer for protocols on the Abstract chain: it manages membership, council seats, proposals, forum participation, and treasury controls through onchain contracts. Abstract is an Ethereum L2 focused on consumer-facing apps and agent-friendly infrastructure. This CLI gives operators and agents one interface to query Assembly state, run checks, and power automation.
4
+
5
+ Learn more:
6
+
7
+ - Abstract site: https://abs.xyz
8
+ - Abstract docs: https://docs.abs.xyz
4
9
 
5
10
  ## Install
6
11
 
@@ -47,27 +52,30 @@ assembly-cli <group> <command> [args] [options]
47
52
 
48
53
  ```bash
49
54
  # 1) Agent startup snapshot: report system health in one call
50
- assembly-cli status --format json
55
+ assembly-cli status --json
51
56
 
52
57
  # 2) Verify whether an address can currently participate as council
53
58
  assembly-cli council is-member 0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045 --format json
54
59
 
55
60
  # 3) Pull active member roster with relative activity timings
56
- assembly-cli members list --format json
61
+ # Note: best results require ASSEMBLY_INDEXER_URL; if the indexer returns 404/unavailable,
62
+ # the CLI falls back to onchain Registered events and output may be slower or partial.
63
+ assembly-cli members list --json
57
64
 
58
65
  # 4) Pre-vote automation: list proposals and fetch one in detail
59
66
  assembly-cli governance proposals --format json
60
- assembly-cli governance proposal 1 --format json
67
+ assembly-cli governance proposal 1 --json
61
68
 
62
69
  # 5) Treasury monitoring loop: balance + spend lock status
63
70
  assembly-cli treasury balance --format json
64
- assembly-cli treasury major-spend-status --format json
71
+ assembly-cli treasury major-spend-status --json
65
72
  ```
66
73
 
67
74
  ## Output Mode
68
75
 
69
- All commands support structured JSON output for agents:
76
+ All commands support structured JSON output for agents with either `--json` or `--format json`:
70
77
 
71
78
  ```bash
79
+ assembly-cli forum threads --json
72
80
  assembly-cli forum threads --format json
73
- ```
81
+ ```
package/dist/cli.js CHANGED
@@ -4281,6 +4281,10 @@ var ABSTRACT_MAINNET_ADDRESSES = {
4281
4281
  governance: "0xe82a25937e07a3855d8B8352b85fF4B4Aa3fb0C0",
4282
4282
  treasury: "0xC2e6DDbdc1A8e4DcCc60A78B6Faa197967a8FEb9"
4283
4283
  };
4284
+ var ABSTRACT_MAINNET_DEPLOYMENT_BLOCKS = {
4285
+ // https://abscan.org/tx/0xe1dd27a739944c9847a7f366ea2363e7bf0fc2067377020fd749a8301e09b1ec
4286
+ registry: 43782651n
4287
+ };
4284
4288
 
4285
4289
  // src/contracts/client.ts
4286
4290
  import { http, createPublicClient, defineChain } from "viem";
@@ -4317,6 +4321,11 @@ function decodeAuction(value) {
4317
4321
  const [highestBidder, highestBid, settled] = value;
4318
4322
  return { highestBidder, highestBid, settled };
4319
4323
  }
4324
+ function deriveAuctionStatus(params) {
4325
+ if (params.settled) return "settled";
4326
+ if (params.currentTimestamp < params.windowEnd) return "bidding";
4327
+ return "closed";
4328
+ }
4320
4329
  var council = Cli.create("council", {
4321
4330
  description: "Inspect council seats, members, auctions, and seat parameters."
4322
4331
  });
@@ -4516,7 +4525,10 @@ council.command("auctions", {
4516
4525
  slot: z.number(),
4517
4526
  highestBidder: z.string(),
4518
4527
  highestBid: z.string(),
4519
- settled: z.boolean()
4528
+ settled: z.boolean(),
4529
+ windowEnd: z.number(),
4530
+ windowEndRelative: z.string(),
4531
+ status: z.enum(["bidding", "closed", "settled"])
4520
4532
  })
4521
4533
  )
4522
4534
  }),
@@ -4545,27 +4557,50 @@ council.command("auctions", {
4545
4557
  if (d < 0) continue;
4546
4558
  for (let s = 0; s < Number(slotsPerDay); s++) recent.push({ day: BigInt(d), slot: s });
4547
4559
  }
4548
- const auctionTuples = recent.length ? await client.multicall({
4549
- allowFailure: false,
4550
- contracts: recent.map((x) => ({
4551
- abi: councilSeatsAbi,
4552
- address: ABSTRACT_MAINNET_ADDRESSES.councilSeats,
4553
- functionName: "auctions",
4554
- args: [x.day, x.slot]
4555
- }))
4556
- }) : [];
4560
+ const [auctionTuples, windowEnds, latestBlock] = await Promise.all([
4561
+ recent.length ? client.multicall({
4562
+ allowFailure: false,
4563
+ contracts: recent.map((x) => ({
4564
+ abi: councilSeatsAbi,
4565
+ address: ABSTRACT_MAINNET_ADDRESSES.councilSeats,
4566
+ functionName: "auctions",
4567
+ args: [x.day, x.slot]
4568
+ }))
4569
+ }) : Promise.resolve([]),
4570
+ recent.length ? client.multicall({
4571
+ allowFailure: false,
4572
+ contracts: recent.map((x) => ({
4573
+ abi: councilSeatsAbi,
4574
+ address: ABSTRACT_MAINNET_ADDRESSES.councilSeats,
4575
+ functionName: "auctionWindowEnd",
4576
+ args: [x.day, x.slot]
4577
+ }))
4578
+ }) : Promise.resolve([]),
4579
+ client.getBlock({ blockTag: "latest" })
4580
+ ]);
4557
4581
  const auctions = auctionTuples.map(decodeAuction);
4582
+ const currentTimestamp = latestBlock.timestamp;
4558
4583
  return c.ok(
4559
4584
  {
4560
4585
  currentDay: asNum(day),
4561
4586
  currentSlot: asNum(slot),
4562
- auctions: recent.map((x, i) => ({
4563
- day: Number(x.day),
4564
- slot: x.slot,
4565
- highestBidder: toChecksum(auctions[i].highestBidder),
4566
- highestBid: eth(auctions[i].highestBid),
4567
- settled: auctions[i].settled
4568
- }))
4587
+ auctions: recent.map((x, i) => {
4588
+ const windowEnd = windowEnds[i];
4589
+ return {
4590
+ day: Number(x.day),
4591
+ slot: x.slot,
4592
+ highestBidder: toChecksum(auctions[i].highestBidder),
4593
+ highestBid: eth(auctions[i].highestBid),
4594
+ settled: auctions[i].settled,
4595
+ windowEnd: asNum(windowEnd),
4596
+ windowEndRelative: relTime(windowEnd),
4597
+ status: deriveAuctionStatus({
4598
+ settled: auctions[i].settled,
4599
+ windowEnd,
4600
+ currentTimestamp
4601
+ })
4602
+ };
4603
+ })
4569
4604
  },
4570
4605
  {
4571
4606
  cta: {
@@ -4591,24 +4626,45 @@ council.command("auction", {
4591
4626
  slot: z.number(),
4592
4627
  highestBidder: z.string(),
4593
4628
  highestBid: z.string(),
4594
- settled: z.boolean()
4629
+ settled: z.boolean(),
4630
+ windowEnd: z.number(),
4631
+ windowEndRelative: z.string(),
4632
+ status: z.enum(["bidding", "closed", "settled"])
4595
4633
  }),
4596
4634
  examples: [{ args: { day: 0, slot: 0 }, description: "Inspect day 0, slot 0 auction" }],
4597
4635
  async run(c) {
4598
4636
  const client = createAssemblyPublicClient(c.env.ABSTRACT_RPC_URL);
4599
- const auctionTuple = await client.readContract({
4600
- abi: councilSeatsAbi,
4601
- address: ABSTRACT_MAINNET_ADDRESSES.councilSeats,
4602
- functionName: "auctions",
4603
- args: [BigInt(c.args.day), c.args.slot]
4604
- });
4637
+ const [auctionTuple, windowEnd, latestBlock] = await Promise.all([
4638
+ client.readContract({
4639
+ abi: councilSeatsAbi,
4640
+ address: ABSTRACT_MAINNET_ADDRESSES.councilSeats,
4641
+ functionName: "auctions",
4642
+ args: [BigInt(c.args.day), c.args.slot]
4643
+ }),
4644
+ client.readContract({
4645
+ abi: councilSeatsAbi,
4646
+ address: ABSTRACT_MAINNET_ADDRESSES.councilSeats,
4647
+ functionName: "auctionWindowEnd",
4648
+ args: [BigInt(c.args.day), c.args.slot]
4649
+ }),
4650
+ client.getBlock({ blockTag: "latest" })
4651
+ ]);
4605
4652
  const auction = decodeAuction(auctionTuple);
4653
+ const windowEndTimestamp = windowEnd;
4654
+ const currentTimestamp = latestBlock.timestamp;
4606
4655
  return c.ok({
4607
4656
  day: c.args.day,
4608
4657
  slot: c.args.slot,
4609
4658
  highestBidder: toChecksum(auction.highestBidder),
4610
4659
  highestBid: eth(auction.highestBid),
4611
- settled: auction.settled
4660
+ settled: auction.settled,
4661
+ windowEnd: asNum(windowEndTimestamp),
4662
+ windowEndRelative: relTime(windowEndTimestamp),
4663
+ status: deriveAuctionStatus({
4664
+ settled: auction.settled,
4665
+ windowEnd: windowEndTimestamp,
4666
+ currentTimestamp
4667
+ })
4612
4668
  });
4613
4669
  }
4614
4670
  });
@@ -5301,6 +5357,7 @@ governance.command("params", {
5301
5357
  import { Cli as Cli4, z as z4 } from "incur";
5302
5358
  var DEFAULT_MEMBER_SNAPSHOT_URL = "https://www.theaiassembly.org/api/indexer/members";
5303
5359
  var REGISTERED_EVENT_SCAN_STEP = 100000n;
5360
+ var REGISTERED_EVENT_SCAN_TIMEOUT_MS = 2e4;
5304
5361
  var env4 = z4.object({
5305
5362
  ABSTRACT_RPC_URL: z4.string().optional().describe("Abstract RPC URL override"),
5306
5363
  ASSEMBLY_INDEXER_URL: z4.string().optional().describe("Optional members snapshot endpoint (default: theaiassembly.org indexer)")
@@ -5351,10 +5408,25 @@ async function memberSnapshot(url) {
5351
5408
  response: json
5352
5409
  });
5353
5410
  }
5411
+ async function withTimeout(promise, timeoutMs, timeoutMessage) {
5412
+ let timer;
5413
+ try {
5414
+ return await Promise.race([
5415
+ promise,
5416
+ new Promise((_, reject) => {
5417
+ timer = setTimeout(() => {
5418
+ reject(new Error(timeoutMessage));
5419
+ }, timeoutMs);
5420
+ })
5421
+ ]);
5422
+ } finally {
5423
+ if (timer) clearTimeout(timer);
5424
+ }
5425
+ }
5354
5426
  async function membersFromRegisteredEvents(client) {
5355
5427
  const latestBlock = await client.getBlockNumber();
5356
5428
  const addresses = /* @__PURE__ */ new Set();
5357
- for (let fromBlock = 0n; fromBlock <= latestBlock; fromBlock += REGISTERED_EVENT_SCAN_STEP) {
5429
+ for (let fromBlock = ABSTRACT_MAINNET_DEPLOYMENT_BLOCKS.registry; fromBlock <= latestBlock; fromBlock += REGISTERED_EVENT_SCAN_STEP) {
5358
5430
  const toBlock = fromBlock + REGISTERED_EVENT_SCAN_STEP - 1n > latestBlock ? latestBlock : fromBlock + REGISTERED_EVENT_SCAN_STEP - 1n;
5359
5431
  const events = await client.getContractEvents({
5360
5432
  abi: registryAbi,
@@ -5433,7 +5505,11 @@ members.command("list", {
5433
5505
  }
5434
5506
  fallbackReason = error.details;
5435
5507
  try {
5436
- addresses = await membersFromRegisteredEvents(client);
5508
+ addresses = await withTimeout(
5509
+ membersFromRegisteredEvents(client),
5510
+ REGISTERED_EVENT_SCAN_TIMEOUT_MS,
5511
+ `Registered event fallback scan timed out after ${REGISTERED_EVENT_SCAN_TIMEOUT_MS}ms`
5512
+ );
5437
5513
  } catch (fallbackError) {
5438
5514
  return c.error({
5439
5515
  code: "MEMBER_LIST_SOURCE_UNAVAILABLE",
@@ -5708,6 +5784,133 @@ treasury.command("executed", {
5708
5784
  }
5709
5785
  });
5710
5786
 
5787
+ // src/error-handling.ts
5788
+ import { AsyncLocalStorage } from "async_hooks";
5789
+ import { Errors } from "incur";
5790
+ var VIEM_VERSION_PATTERN = /\n*Version:\s*viem@[^\n]+/i;
5791
+ var VIEM_VERSION_PATTERN_GLOBAL = /\n*Version:\s*viem@[^\n]+/gi;
5792
+ var debugFlagStore = new AsyncLocalStorage();
5793
+ var VIEM_ERROR_NAMES = /* @__PURE__ */ new Set([
5794
+ "CallExecutionError",
5795
+ "ContractFunctionExecutionError",
5796
+ "ContractFunctionRevertedError",
5797
+ "HttpRequestError",
5798
+ "InvalidAddressError",
5799
+ "TransactionExecutionError"
5800
+ ]);
5801
+ function parseDebugFlag(argv) {
5802
+ const cleaned = [];
5803
+ let debug = false;
5804
+ for (let i = 0; i < argv.length; i += 1) {
5805
+ const token = argv[i];
5806
+ if (token === "--debug") {
5807
+ debug = true;
5808
+ continue;
5809
+ }
5810
+ if (token === "--debug=true" || token === "--debug=1") {
5811
+ debug = true;
5812
+ continue;
5813
+ }
5814
+ if (token === "--debug=false" || token === "--debug=0") {
5815
+ debug = false;
5816
+ continue;
5817
+ }
5818
+ cleaned.push(token);
5819
+ }
5820
+ return { argv: cleaned, debug };
5821
+ }
5822
+ function isViemLikeError(error) {
5823
+ if (!(error instanceof Error)) return false;
5824
+ const shortMessage = error.shortMessage;
5825
+ return VIEM_ERROR_NAMES.has(error.name) || VIEM_VERSION_PATTERN.test(error.message) || typeof shortMessage === "string" && error.message.includes("Docs: https://viem.sh");
5826
+ }
5827
+ function sanitizeViemMessage(message) {
5828
+ return message.replace(VIEM_VERSION_PATTERN_GLOBAL, "").trim();
5829
+ }
5830
+ function toFriendlyViemError(error) {
5831
+ const shortMessage = typeof error.shortMessage === "string" && error.shortMessage.trim().length > 0 ? error.shortMessage.trim() : void 0;
5832
+ if (error.name === "InvalidAddressError" || shortMessage?.startsWith('Address "')) {
5833
+ return {
5834
+ code: "INVALID_ADDRESS",
5835
+ message: `${shortMessage ?? "Invalid address."} Use a valid 0x-prefixed 20-byte address. Run with --debug for full error details.`
5836
+ };
5837
+ }
5838
+ if (shortMessage?.toLowerCase().includes("http request failed") || error.message.toLowerCase().includes("http request failed")) {
5839
+ return {
5840
+ code: "RPC_CONNECTION_FAILED",
5841
+ message: "RPC connection failed. Check ABSTRACT_RPC_URL and try again. Run with --debug for full error details."
5842
+ };
5843
+ }
5844
+ if (shortMessage || VIEM_VERSION_PATTERN.test(error.message)) {
5845
+ return {
5846
+ code: "UPSTREAM_ERROR",
5847
+ message: `${sanitizeViemMessage(shortMessage ?? error.message)} Run with --debug for full error details.`
5848
+ };
5849
+ }
5850
+ return void 0;
5851
+ }
5852
+ function isMissingRequiredArgValidation(error) {
5853
+ return error.fieldErrors.some((fieldError) => {
5854
+ const msg = fieldError.message.toLowerCase();
5855
+ return msg.includes("received undefined") || fieldError.received === "";
5856
+ });
5857
+ }
5858
+ function missingArgPaths(error) {
5859
+ const paths = /* @__PURE__ */ new Set();
5860
+ for (const fieldError of error.fieldErrors) {
5861
+ const msg = fieldError.message.toLowerCase();
5862
+ if (msg.includes("received undefined") || fieldError.received === "") {
5863
+ paths.add(fieldError.path);
5864
+ }
5865
+ }
5866
+ return [...paths];
5867
+ }
5868
+ function toFriendlyValidationError(ctx, error) {
5869
+ const missing = missingArgPaths(error);
5870
+ if (missing.length === 0) return void 0;
5871
+ const helpCommand = `${ctx.name} ${ctx.command} --help`;
5872
+ const argsList = missing.join(", ");
5873
+ return {
5874
+ code: "VALIDATION_ERROR",
5875
+ cta: {
5876
+ description: "See command usage:",
5877
+ commands: [{ command: helpCommand }]
5878
+ },
5879
+ message: missing.length === 1 ? `Missing required argument: ${argsList}. Run \`${helpCommand}\` for usage.` : `Missing required arguments: ${argsList}. Run \`${helpCommand}\` for usage.`
5880
+ };
5881
+ }
5882
+ function handleError(ctx, error) {
5883
+ if (debugFlagStore.getStore()) throw error;
5884
+ if (error instanceof Errors.ValidationError && isMissingRequiredArgValidation(error)) {
5885
+ const friendly = toFriendlyValidationError(ctx, error);
5886
+ if (friendly) {
5887
+ return ctx.error(friendly);
5888
+ }
5889
+ }
5890
+ if (isViemLikeError(error)) {
5891
+ const friendly = toFriendlyViemError(error);
5892
+ if (friendly) {
5893
+ return ctx.error(friendly);
5894
+ }
5895
+ }
5896
+ throw error;
5897
+ }
5898
+ function applyFriendlyErrorHandling(cli2) {
5899
+ const originalServe = cli2.serve.bind(cli2);
5900
+ cli2.serve = (async (argv, options) => {
5901
+ const rawArgv = argv ?? process.argv.slice(2);
5902
+ const parsed = parseDebugFlag(rawArgv);
5903
+ return debugFlagStore.run(parsed.debug, () => originalServe(parsed.argv, options));
5904
+ });
5905
+ cli2.use(async (context, next) => {
5906
+ try {
5907
+ await next();
5908
+ } catch (error) {
5909
+ return handleError(context, error);
5910
+ }
5911
+ });
5912
+ }
5913
+
5711
5914
  // src/cli.ts
5712
5915
  var cli = Cli6.create("assembly", {
5713
5916
  description: "Assembly governance CLI for Abstract chain."
@@ -5843,6 +6046,7 @@ cli.command("health", {
5843
6046
  });
5844
6047
  }
5845
6048
  });
6049
+ applyFriendlyErrorHandling(cli);
5846
6050
  var isMain = process.argv[1] === fileURLToPath(import.meta.url);
5847
6051
  if (isMain) {
5848
6052
  cli.serve();
package/package.json CHANGED
@@ -1,10 +1,27 @@
1
1
  {
2
2
  "name": "@spectratools/assembly-cli",
3
- "version": "0.2.0",
4
- "description": "Assembly CLI for spectra-the-bot",
3
+ "version": "0.3.0",
4
+ "description": "CLI for Assembly governance on Abstract (members, council, forum, proposals, and treasury).",
5
5
  "type": "module",
6
6
  "license": "MIT",
7
7
  "author": "spectra-the-bot",
8
+ "keywords": [
9
+ "assembly",
10
+ "abstract",
11
+ "governance",
12
+ "dao",
13
+ "ethereum",
14
+ "web3",
15
+ "cli",
16
+ "incur",
17
+ "spectratools"
18
+ ],
19
+ "repository": {
20
+ "type": "git",
21
+ "url": "git+https://github.com/spectra-the-bot/spectra-tools.git",
22
+ "directory": "packages/assembly"
23
+ },
24
+ "homepage": "https://github.com/spectra-the-bot/spectra-tools/tree/main/packages/assembly#readme",
8
25
  "engines": {
9
26
  "node": ">=20"
10
27
  },