@spectratools/assembly-cli 0.7.0 → 0.8.2

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 +23 -0
  2. package/dist/cli.js +246 -54
  3. package/package.json +3 -3
package/README.md CHANGED
@@ -33,6 +33,29 @@ assembly-cli mcp add
33
33
  | `ABSTRACT_RPC_URL` | No | Abstract RPC URL override (default from package client) |
34
34
  | `ASSEMBLY_INDEXER_URL` | No | Optional member snapshot endpoint for `members list` (falls back to on-chain `Registered` events with a warning if unavailable) |
35
35
 
36
+ ## Shared tx integration pattern (`@spectratools/tx-shared`)
37
+
38
+ `assembly-cli` write flows are powered by `@spectratools/tx-shared` (`executeTx` lifecycle + structured `TxError`s).
39
+
40
+ If you're building a consumer CLI command (or extending Assembly write behavior), use the shared pattern:
41
+
42
+ 1. Parse signer flags/env via `toSignerOptions(...)`
43
+ 2. Resolve signer with `resolveSigner(...)`
44
+ 3. Build write request (`address`, `abi`, `functionName`, `args`/`value`)
45
+ 4. Execute with `executeTx(...)` and handle `dryRun` + `TxError.code`
46
+
47
+ Reference wiring:
48
+
49
+ - Assembly example: [`src/examples/tx-shared-register.ts`](./src/examples/tx-shared-register.ts)
50
+ - tx-shared docs: [`../tx-shared/README.md`](../tx-shared/README.md)
51
+ - tx-shared assembly-style example: [`../tx-shared/src/examples/assembly-write.ts`](../tx-shared/src/examples/assembly-write.ts)
52
+
53
+ Current `assembly-cli` write commands require `PRIVATE_KEY`, but tx-shared supports broader provider setup for consumer CLIs:
54
+
55
+ - private key (`PRIVATE_KEY`)
56
+ - keystore (`--keystore` + `--password` or `KEYSTORE_PASSWORD`)
57
+ - Privy (`PRIVY_APP_ID`, `PRIVY_WALLET_ID`, `PRIVY_AUTHORIZATION_KEY`; implementation tracked in [#117](https://github.com/spectra-the-bot/spectra-tools/issues/117))
58
+
36
59
  ## Command Group Intent Summary
37
60
 
38
61
  - `members` — Membership state, counts, and registry fee settings
package/dist/cli.js CHANGED
@@ -4410,10 +4410,14 @@ function decodeAuction(value) {
4410
4410
  const [highestBidder, highestBid, settled] = value;
4411
4411
  return { highestBidder, highestBid, settled };
4412
4412
  }
4413
- function deriveAuctionStatus(params) {
4414
- if (params.settled) return "settled";
4415
- if (params.currentTimestamp < params.windowEnd) return "bidding";
4416
- return "closed";
4413
+ function deriveAuctionExecutionState(params) {
4414
+ if (params.settled) return { executionStatus: "settled", bidExecutableNow: false };
4415
+ const isCurrentAuctionSlot = params.day === Number(params.currentAuctionDay) && params.slot === Number(params.currentAuctionSlot);
4416
+ if (params.currentTimestamp < params.windowEnd) {
4417
+ if (isCurrentAuctionSlot) return { executionStatus: "open_now", bidExecutableNow: true };
4418
+ return { executionStatus: "upcoming", bidExecutableNow: false };
4419
+ }
4420
+ return { executionStatus: "closed_unsettled", bidExecutableNow: false };
4417
4421
  }
4418
4422
  var council = Cli.create("council", {
4419
4423
  description: "Inspect council seats, members, auctions, and seat parameters."
@@ -4629,7 +4633,9 @@ council.command("auctions", {
4629
4633
  settled: z2.boolean(),
4630
4634
  windowEnd: timestampOutput,
4631
4635
  windowEndRelative: z2.string(),
4632
- status: z2.enum(["bidding", "closed", "settled"])
4636
+ bidExecutableNow: z2.boolean(),
4637
+ executionStatus: z2.enum(["open_now", "upcoming", "closed_unsettled", "settled"]),
4638
+ status: z2.enum(["open_now", "upcoming", "closed_unsettled", "settled"])
4633
4639
  })
4634
4640
  )
4635
4641
  }),
@@ -4687,19 +4693,27 @@ council.command("auctions", {
4687
4693
  currentSlot: asNum(slot),
4688
4694
  auctions: recent.map((x, i) => {
4689
4695
  const windowEnd = windowEnds[i];
4696
+ const dayValue = Number(x.day);
4697
+ const execution = deriveAuctionExecutionState({
4698
+ day: dayValue,
4699
+ slot: x.slot,
4700
+ currentAuctionDay: day,
4701
+ currentAuctionSlot: slot,
4702
+ settled: auctions[i].settled,
4703
+ windowEnd,
4704
+ currentTimestamp
4705
+ });
4690
4706
  return {
4691
- day: Number(x.day),
4707
+ day: dayValue,
4692
4708
  slot: x.slot,
4693
4709
  highestBidder: toChecksum(auctions[i].highestBidder),
4694
4710
  highestBid: eth(auctions[i].highestBid),
4695
4711
  settled: auctions[i].settled,
4696
4712
  windowEnd: timeValue(windowEnd, c.format),
4697
4713
  windowEndRelative: relTime(windowEnd),
4698
- status: deriveAuctionStatus({
4699
- settled: auctions[i].settled,
4700
- windowEnd,
4701
- currentTimestamp
4702
- })
4714
+ bidExecutableNow: execution.bidExecutableNow,
4715
+ executionStatus: execution.executionStatus,
4716
+ status: execution.executionStatus
4703
4717
  };
4704
4718
  })
4705
4719
  },
@@ -4730,7 +4744,9 @@ council.command("auction", {
4730
4744
  settled: z2.boolean(),
4731
4745
  windowEnd: timestampOutput,
4732
4746
  windowEndRelative: z2.string(),
4733
- status: z2.enum(["bidding", "closed", "settled"])
4747
+ bidExecutableNow: z2.boolean(),
4748
+ executionStatus: z2.enum(["open_now", "upcoming", "closed_unsettled", "settled"]),
4749
+ status: z2.enum(["open_now", "upcoming", "closed_unsettled", "settled"])
4734
4750
  }),
4735
4751
  examples: [{ args: { day: 0, slot: 0 }, description: "Inspect day 0, slot 0 auction" }],
4736
4752
  async run(c) {
@@ -4747,7 +4763,7 @@ council.command("auction", {
4747
4763
  retryable: false
4748
4764
  });
4749
4765
  }
4750
- const [auctionTuple, windowEnd, latestBlock] = await Promise.all([
4766
+ const [auctionTuple, windowEnd, latestBlock, currentAuctionDay, currentAuctionSlot] = await Promise.all([
4751
4767
  client.readContract({
4752
4768
  abi: councilSeatsAbi,
4753
4769
  address: ABSTRACT_MAINNET_ADDRESSES.councilSeats,
@@ -4760,11 +4776,30 @@ council.command("auction", {
4760
4776
  functionName: "auctionWindowEnd",
4761
4777
  args: [BigInt(c.args.day), c.args.slot]
4762
4778
  }),
4763
- client.getBlock({ blockTag: "latest" })
4779
+ client.getBlock({ blockTag: "latest" }),
4780
+ client.readContract({
4781
+ abi: councilSeatsAbi,
4782
+ address: ABSTRACT_MAINNET_ADDRESSES.councilSeats,
4783
+ functionName: "currentAuctionDay"
4784
+ }),
4785
+ client.readContract({
4786
+ abi: councilSeatsAbi,
4787
+ address: ABSTRACT_MAINNET_ADDRESSES.councilSeats,
4788
+ functionName: "currentAuctionSlot"
4789
+ })
4764
4790
  ]);
4765
4791
  const auction = decodeAuction(auctionTuple);
4766
4792
  const windowEndTimestamp = windowEnd;
4767
4793
  const currentTimestamp = latestBlock.timestamp;
4794
+ const execution = deriveAuctionExecutionState({
4795
+ day: c.args.day,
4796
+ slot: c.args.slot,
4797
+ currentAuctionDay,
4798
+ currentAuctionSlot,
4799
+ settled: auction.settled,
4800
+ windowEnd: windowEndTimestamp,
4801
+ currentTimestamp
4802
+ });
4768
4803
  return c.ok({
4769
4804
  day: c.args.day,
4770
4805
  slot: c.args.slot,
@@ -4773,11 +4808,9 @@ council.command("auction", {
4773
4808
  settled: auction.settled,
4774
4809
  windowEnd: timeValue(windowEndTimestamp, c.format),
4775
4810
  windowEndRelative: relTime(windowEndTimestamp),
4776
- status: deriveAuctionStatus({
4777
- settled: auction.settled,
4778
- windowEnd: windowEndTimestamp,
4779
- currentTimestamp
4780
- })
4811
+ bidExecutableNow: execution.bidExecutableNow,
4812
+ executionStatus: execution.executionStatus,
4813
+ status: execution.executionStatus
4781
4814
  });
4782
4815
  }
4783
4816
  });
@@ -4944,7 +4977,7 @@ council.command("bid", {
4944
4977
  retryable: false
4945
4978
  });
4946
4979
  }
4947
- const [auctionTuple, windowEnd, latestBlock] = await Promise.all([
4980
+ const [auctionTuple, windowEnd, latestBlock, currentAuctionDay, currentAuctionSlot] = await Promise.all([
4948
4981
  client.readContract({
4949
4982
  abi: councilSeatsAbi,
4950
4983
  address: ABSTRACT_MAINNET_ADDRESSES.councilSeats,
@@ -4957,24 +4990,45 @@ council.command("bid", {
4957
4990
  functionName: "auctionWindowEnd",
4958
4991
  args: [BigInt(c.args.day), c.args.slot]
4959
4992
  }),
4960
- client.getBlock({ blockTag: "latest" })
4993
+ client.getBlock({ blockTag: "latest" }),
4994
+ client.readContract({
4995
+ abi: councilSeatsAbi,
4996
+ address: ABSTRACT_MAINNET_ADDRESSES.councilSeats,
4997
+ functionName: "currentAuctionDay"
4998
+ }),
4999
+ client.readContract({
5000
+ abi: councilSeatsAbi,
5001
+ address: ABSTRACT_MAINNET_ADDRESSES.councilSeats,
5002
+ functionName: "currentAuctionSlot"
5003
+ })
4961
5004
  ]);
4962
5005
  const auction = decodeAuction(auctionTuple);
4963
5006
  const windowEndTimestamp = windowEnd;
4964
5007
  const currentTimestamp = latestBlock.timestamp;
4965
- const status = deriveAuctionStatus({
5008
+ const execution = deriveAuctionExecutionState({
5009
+ day: c.args.day,
5010
+ slot: c.args.slot,
5011
+ currentAuctionDay,
5012
+ currentAuctionSlot,
4966
5013
  settled: auction.settled,
4967
5014
  windowEnd: windowEndTimestamp,
4968
5015
  currentTimestamp
4969
5016
  });
4970
- if (status === "settled") {
5017
+ if (execution.executionStatus === "settled") {
4971
5018
  return c.error({
4972
5019
  code: "AUCTION_SETTLED",
4973
5020
  message: `Auction (day=${c.args.day}, slot=${c.args.slot}) is already settled. Winner: ${toChecksum(auction.highestBidder)}.`,
4974
5021
  retryable: false
4975
5022
  });
4976
5023
  }
4977
- if (status === "closed") {
5024
+ if (execution.executionStatus === "upcoming") {
5025
+ return c.error({
5026
+ code: "AUCTION_NOT_ACTIVE",
5027
+ message: `Auction (day=${c.args.day}, slot=${c.args.slot}) is not the currently active slot. Current bid-executable slot is day=${Number(currentAuctionDay)}, slot=${Number(currentAuctionSlot)}.`,
5028
+ retryable: false
5029
+ });
5030
+ }
5031
+ if (execution.executionStatus === "closed_unsettled") {
4978
5032
  return c.error({
4979
5033
  code: "AUCTION_CLOSED",
4980
5034
  message: `Auction (day=${c.args.day}, slot=${c.args.slot}) bidding window has ended. Use "council settle" instead.`,
@@ -5055,7 +5109,7 @@ council.command("settle", {
5055
5109
  retryable: false
5056
5110
  });
5057
5111
  }
5058
- const [auctionTuple, windowEnd, latestBlock] = await Promise.all([
5112
+ const [auctionTuple, windowEnd, latestBlock, currentAuctionDay, currentAuctionSlot] = await Promise.all([
5059
5113
  client.readContract({
5060
5114
  abi: councilSeatsAbi,
5061
5115
  address: ABSTRACT_MAINNET_ADDRESSES.councilSeats,
@@ -5068,30 +5122,51 @@ council.command("settle", {
5068
5122
  functionName: "auctionWindowEnd",
5069
5123
  args: [BigInt(c.args.day), c.args.slot]
5070
5124
  }),
5071
- client.getBlock({ blockTag: "latest" })
5125
+ client.getBlock({ blockTag: "latest" }),
5126
+ client.readContract({
5127
+ abi: councilSeatsAbi,
5128
+ address: ABSTRACT_MAINNET_ADDRESSES.councilSeats,
5129
+ functionName: "currentAuctionDay"
5130
+ }),
5131
+ client.readContract({
5132
+ abi: councilSeatsAbi,
5133
+ address: ABSTRACT_MAINNET_ADDRESSES.councilSeats,
5134
+ functionName: "currentAuctionSlot"
5135
+ })
5072
5136
  ]);
5073
5137
  const auction = decodeAuction(auctionTuple);
5074
5138
  const windowEndTimestamp = windowEnd;
5075
5139
  const currentTimestamp = latestBlock.timestamp;
5076
- const status = deriveAuctionStatus({
5140
+ const execution = deriveAuctionExecutionState({
5141
+ day: c.args.day,
5142
+ slot: c.args.slot,
5143
+ currentAuctionDay,
5144
+ currentAuctionSlot,
5077
5145
  settled: auction.settled,
5078
5146
  windowEnd: windowEndTimestamp,
5079
5147
  currentTimestamp
5080
5148
  });
5081
- if (status === "settled") {
5149
+ if (execution.executionStatus === "settled") {
5082
5150
  return c.error({
5083
5151
  code: "ALREADY_SETTLED",
5084
5152
  message: `Auction (day=${c.args.day}, slot=${c.args.slot}) is already settled.`,
5085
5153
  retryable: false
5086
5154
  });
5087
5155
  }
5088
- if (status === "bidding") {
5156
+ if (execution.executionStatus === "open_now") {
5089
5157
  return c.error({
5090
5158
  code: "AUCTION_STILL_ACTIVE",
5091
5159
  message: `Auction (day=${c.args.day}, slot=${c.args.slot}) is still accepting bids. Window ends ${relTime(windowEndTimestamp)}.`,
5092
5160
  retryable: false
5093
5161
  });
5094
5162
  }
5163
+ if (execution.executionStatus === "upcoming") {
5164
+ return c.error({
5165
+ code: "AUCTION_STILL_ACTIVE",
5166
+ message: `Auction (day=${c.args.day}, slot=${c.args.slot}) is not yet the active slot. Current slot is day=${Number(currentAuctionDay)}, slot=${Number(currentAuctionSlot)}.`,
5167
+ retryable: false
5168
+ });
5169
+ }
5095
5170
  const txResult = await assemblyWriteTx({
5096
5171
  env: c.env,
5097
5172
  options: c.options,
@@ -5872,12 +5947,21 @@ import { Cli as Cli4, z as z5 } from "incur";
5872
5947
  var DEFAULT_MEMBER_SNAPSHOT_URL = "https://www.theaiassembly.org/api/indexer/members";
5873
5948
  var REGISTERED_EVENT_SCAN_STEP = 100000n;
5874
5949
  var REGISTERED_EVENT_SCAN_TIMEOUT_MS = 2e4;
5950
+ var MAX_MEMBER_LOOKUP_SUGGESTIONS = 5;
5875
5951
  var env4 = z5.object({
5876
5952
  ABSTRACT_RPC_URL: z5.string().optional().describe("Abstract RPC URL override"),
5877
5953
  ASSEMBLY_INDEXER_URL: z5.string().optional().describe("Optional members snapshot endpoint (default: theaiassembly.org indexer)")
5878
5954
  });
5879
5955
  var timestampOutput4 = z5.union([z5.number(), z5.string()]);
5880
- var memberSnapshotSchema = z5.array(z5.string());
5956
+ var memberSnapshotEntrySchema = z5.union([
5957
+ z5.string(),
5958
+ z5.object({
5959
+ address: z5.string(),
5960
+ ens: z5.string().optional(),
5961
+ name: z5.string().optional()
5962
+ })
5963
+ ]);
5964
+ var memberSnapshotSchema = z5.array(memberSnapshotEntrySchema);
5881
5965
  var AssemblyApiValidationError = class extends Error {
5882
5966
  constructor(details) {
5883
5967
  super("Assembly API response validation failed");
@@ -5892,6 +5976,45 @@ var AssemblyIndexerUnavailableError = class extends Error {
5892
5976
  this.name = "AssemblyIndexerUnavailableError";
5893
5977
  }
5894
5978
  };
5979
+ function mergeMemberIdentities(entries) {
5980
+ const byAddress = /* @__PURE__ */ new Map();
5981
+ for (const entry of entries) {
5982
+ const key = entry.address.toLowerCase();
5983
+ const existing = byAddress.get(key);
5984
+ if (!existing) {
5985
+ byAddress.set(key, entry);
5986
+ continue;
5987
+ }
5988
+ const merged = { address: existing.address };
5989
+ const ens = existing.ens ?? entry.ens;
5990
+ const name = existing.name ?? entry.name;
5991
+ if (ens !== void 0) merged.ens = ens;
5992
+ if (name !== void 0) merged.name = name;
5993
+ byAddress.set(key, merged);
5994
+ }
5995
+ return [...byAddress.values()];
5996
+ }
5997
+ function memberSnapshotEntryToIdentity(entry) {
5998
+ if (typeof entry === "string") return { address: entry };
5999
+ const identity = { address: entry.address };
6000
+ if (entry.ens !== void 0) identity.ens = entry.ens;
6001
+ if (entry.name !== void 0) identity.name = entry.name;
6002
+ return identity;
6003
+ }
6004
+ function matchableAddressInput(query) {
6005
+ return query.startsWith("0x") && query.length === 42;
6006
+ }
6007
+ function searchMemberIdentities(query, members2) {
6008
+ const needle = query.trim().toLowerCase();
6009
+ if (needle.length === 0) return [];
6010
+ const exactMatches = members2.filter((member) => {
6011
+ return member.address.toLowerCase() === needle || member.ens?.toLowerCase() === needle || member.name?.toLowerCase() === needle;
6012
+ });
6013
+ if (exactMatches.length > 0) return exactMatches;
6014
+ return members2.filter((member) => {
6015
+ return member.address.toLowerCase().includes(needle) || member.ens?.toLowerCase().includes(needle) || member.name?.toLowerCase().includes(needle);
6016
+ });
6017
+ }
5895
6018
  async function memberSnapshot(url) {
5896
6019
  let res;
5897
6020
  try {
@@ -5914,7 +6037,7 @@ async function memberSnapshot(url) {
5914
6037
  const json = await res.json();
5915
6038
  const parsed = memberSnapshotSchema.safeParse(json);
5916
6039
  if (parsed.success) {
5917
- return parsed.data;
6040
+ return mergeMemberIdentities(parsed.data.map(memberSnapshotEntryToIdentity));
5918
6041
  }
5919
6042
  throw new AssemblyApiValidationError({
5920
6043
  code: "INVALID_ASSEMBLY_API_RESPONSE",
@@ -5958,7 +6081,28 @@ async function membersFromRegisteredEvents(client) {
5958
6081
  }
5959
6082
  }
5960
6083
  }
5961
- return [...addresses];
6084
+ return [...addresses].map((address) => ({ address }));
6085
+ }
6086
+ async function loadMemberIdentities(client, snapshotUrl) {
6087
+ try {
6088
+ return { members: await memberSnapshot(snapshotUrl) };
6089
+ } catch (error) {
6090
+ if (error instanceof AssemblyApiValidationError) {
6091
+ throw error;
6092
+ }
6093
+ if (!(error instanceof AssemblyIndexerUnavailableError)) {
6094
+ throw error;
6095
+ }
6096
+ const fallbackMembers = await withTimeout(
6097
+ membersFromRegisteredEvents(client),
6098
+ REGISTERED_EVENT_SCAN_TIMEOUT_MS,
6099
+ `Registered event fallback scan timed out after ${REGISTERED_EVENT_SCAN_TIMEOUT_MS}ms`
6100
+ );
6101
+ return {
6102
+ members: mergeMemberIdentities(fallbackMembers),
6103
+ fallbackReason: error.details
6104
+ };
6105
+ }
5962
6106
  }
5963
6107
  function indexerIssue(details) {
5964
6108
  if (typeof details.status === "number") {
@@ -5979,6 +6123,12 @@ function emitIndexerFallbackWarning(details) {
5979
6123
  `
5980
6124
  );
5981
6125
  }
6126
+ function describeMember(member) {
6127
+ const parts = [toChecksum(member.address)];
6128
+ if (member.ens) parts.push(`ens=${member.ens}`);
6129
+ if (member.name) parts.push(`name=${member.name}`);
6130
+ return parts.join(" ");
6131
+ }
5982
6132
  var members = Cli4.create("members", {
5983
6133
  description: "Inspect Assembly membership and registry fee state."
5984
6134
  });
@@ -6006,10 +6156,12 @@ members.command("list", {
6006
6156
  async run(c) {
6007
6157
  const client = createAssemblyPublicClient(c.env.ABSTRACT_RPC_URL);
6008
6158
  const snapshotUrl = c.env.ASSEMBLY_INDEXER_URL ?? DEFAULT_MEMBER_SNAPSHOT_URL;
6009
- let addresses;
6159
+ let memberIdentities;
6010
6160
  let fallbackReason;
6011
6161
  try {
6012
- addresses = await memberSnapshot(snapshotUrl);
6162
+ const loaded = await loadMemberIdentities(client, snapshotUrl);
6163
+ memberIdentities = loaded.members;
6164
+ fallbackReason = loaded.fallbackReason;
6013
6165
  } catch (error) {
6014
6166
  if (error instanceof AssemblyApiValidationError) {
6015
6167
  return c.error({
@@ -6018,27 +6170,19 @@ members.command("list", {
6018
6170
  retryable: false
6019
6171
  });
6020
6172
  }
6021
- if (!(error instanceof AssemblyIndexerUnavailableError)) {
6173
+ if (!(error instanceof Error)) {
6022
6174
  throw error;
6023
6175
  }
6024
- fallbackReason = error.details;
6025
- try {
6026
- addresses = await withTimeout(
6027
- membersFromRegisteredEvents(client),
6028
- REGISTERED_EVENT_SCAN_TIMEOUT_MS,
6029
- `Registered event fallback scan timed out after ${REGISTERED_EVENT_SCAN_TIMEOUT_MS}ms`
6030
- );
6031
- } catch (fallbackError) {
6032
- return c.error({
6033
- code: "MEMBER_LIST_SOURCE_UNAVAILABLE",
6034
- message: `Member indexer unavailable (${indexerIssue(error.details)} at ${error.details.url}) and on-chain Registered event fallback failed: ${fallbackError instanceof Error ? fallbackError.message : String(fallbackError)}`,
6035
- retryable: true
6036
- });
6037
- }
6176
+ return c.error({
6177
+ code: "MEMBER_LIST_SOURCE_UNAVAILABLE",
6178
+ message: `Unable to load member list from indexer (${snapshotUrl}) or on-chain fallback: ${error.message}`,
6179
+ retryable: true
6180
+ });
6038
6181
  }
6039
6182
  if (fallbackReason) {
6040
6183
  emitIndexerFallbackWarning(fallbackReason);
6041
6184
  }
6185
+ const addresses = memberIdentities.map((member) => member.address);
6042
6186
  const calls = addresses.flatMap((address) => [
6043
6187
  {
6044
6188
  abi: registryAbi,
@@ -6074,9 +6218,9 @@ members.command("list", {
6074
6218
  }
6075
6219
  });
6076
6220
  members.command("info", {
6077
- description: "Get registry record and active status for a member address.",
6221
+ description: "Get member registry record and active status by full address, partial address, ENS, or name.",
6078
6222
  args: z5.object({
6079
- address: z5.string().describe("Member wallet address")
6223
+ address: z5.string().describe("Member lookup query (full/partial address, ENS, or name metadata)")
6080
6224
  }),
6081
6225
  env: env4,
6082
6226
  output: z5.object({
@@ -6091,26 +6235,74 @@ members.command("info", {
6091
6235
  {
6092
6236
  args: { address: "0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045" },
6093
6237
  description: "Inspect one member address"
6238
+ },
6239
+ {
6240
+ args: { address: "a96045" },
6241
+ description: "Lookup a member by partial address"
6094
6242
  }
6095
6243
  ],
6096
6244
  async run(c) {
6097
6245
  const client = createAssemblyPublicClient(c.env.ABSTRACT_RPC_URL);
6246
+ const snapshotUrl = c.env.ASSEMBLY_INDEXER_URL ?? DEFAULT_MEMBER_SNAPSHOT_URL;
6247
+ let lookupAddress = c.args.address;
6248
+ if (!matchableAddressInput(c.args.address)) {
6249
+ let loadedMembers;
6250
+ let fallbackReason;
6251
+ try {
6252
+ const loaded = await loadMemberIdentities(client, snapshotUrl);
6253
+ loadedMembers = loaded.members;
6254
+ fallbackReason = loaded.fallbackReason;
6255
+ } catch (error) {
6256
+ if (error instanceof AssemblyApiValidationError) {
6257
+ return c.error({
6258
+ code: error.details.code,
6259
+ message: `Member snapshot response failed validation. url=${error.details.url}; issues=${JSON.stringify(error.details.issues)}; response=${JSON.stringify(error.details.response)}`,
6260
+ retryable: false
6261
+ });
6262
+ }
6263
+ return c.error({
6264
+ code: "MEMBER_LOOKUP_SOURCE_UNAVAILABLE",
6265
+ message: error instanceof Error ? `Unable to resolve member query from indexer (${snapshotUrl}) or on-chain fallback: ${error.message}` : `Unable to resolve member query from indexer (${snapshotUrl}) or on-chain fallback.`,
6266
+ retryable: true
6267
+ });
6268
+ }
6269
+ if (fallbackReason) {
6270
+ emitIndexerFallbackWarning(fallbackReason);
6271
+ }
6272
+ const matches = searchMemberIdentities(c.args.address, loadedMembers);
6273
+ if (matches.length === 0) {
6274
+ return c.error({
6275
+ code: "MEMBER_NOT_FOUND",
6276
+ message: `No Assembly member matched "${c.args.address}". Try a longer query or run \`assembly members list\` first.`,
6277
+ retryable: false
6278
+ });
6279
+ }
6280
+ if (matches.length > 1) {
6281
+ const suggestions = matches.slice(0, MAX_MEMBER_LOOKUP_SUGGESTIONS).map(describeMember).join("; ");
6282
+ return c.error({
6283
+ code: "AMBIGUOUS_MEMBER_QUERY",
6284
+ message: `Member query "${c.args.address}" matched ${matches.length} members. Be more specific. Matches: ${suggestions}`,
6285
+ retryable: false
6286
+ });
6287
+ }
6288
+ lookupAddress = matches[0].address;
6289
+ }
6098
6290
  const [member, active] = await Promise.all([
6099
6291
  client.readContract({
6100
6292
  abi: registryAbi,
6101
6293
  address: ABSTRACT_MAINNET_ADDRESSES.registry,
6102
6294
  functionName: "members",
6103
- args: [c.args.address]
6295
+ args: [lookupAddress]
6104
6296
  }),
6105
6297
  client.readContract({
6106
6298
  abi: registryAbi,
6107
6299
  address: ABSTRACT_MAINNET_ADDRESSES.registry,
6108
6300
  functionName: "isActive",
6109
- args: [c.args.address]
6301
+ args: [lookupAddress]
6110
6302
  })
6111
6303
  ]);
6112
6304
  return c.ok({
6113
- address: toChecksum(c.args.address),
6305
+ address: toChecksum(lookupAddress),
6114
6306
  active,
6115
6307
  activeUntil: timeValue(member.activeUntil, c.format),
6116
6308
  lastHeartbeatAt: timeValue(member.lastHeartbeatAt, c.format),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@spectratools/assembly-cli",
3
- "version": "0.7.0",
3
+ "version": "0.8.2",
4
4
  "description": "CLI for Assembly governance on Abstract (members, council, forum, proposals, and treasury).",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -32,8 +32,8 @@
32
32
  "incur": "^0.3.0",
33
33
  "ox": "^0.14.0",
34
34
  "viem": "^2.47.0",
35
- "@spectratools/tx-shared": "0.3.0",
36
- "@spectratools/cli-shared": "0.1.1"
35
+ "@spectratools/cli-shared": "0.1.1",
36
+ "@spectratools/tx-shared": "0.4.2"
37
37
  },
38
38
  "devDependencies": {
39
39
  "typescript": "5.7.3",