@spectratools/assembly-cli 0.2.0 → 0.3.1

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 +283 -65
  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
  });
@@ -4767,17 +4823,20 @@ var forum = Cli2.create("forum", {
4767
4823
  forum.command("threads", {
4768
4824
  description: "List forum threads with author and creation metadata.",
4769
4825
  env: env2,
4770
- output: z2.array(
4771
- z2.object({
4772
- id: z2.number(),
4773
- kind: z2.number(),
4774
- author: z2.string(),
4775
- createdAt: z2.number(),
4776
- createdAtRelative: z2.string(),
4777
- category: z2.string().nullable().optional(),
4778
- title: z2.string().nullable().optional()
4779
- })
4780
- ),
4826
+ output: z2.object({
4827
+ threads: z2.array(
4828
+ z2.object({
4829
+ id: z2.number(),
4830
+ kind: z2.number(),
4831
+ author: z2.string(),
4832
+ createdAt: z2.number(),
4833
+ createdAtRelative: z2.string(),
4834
+ category: z2.string().nullable().optional(),
4835
+ title: z2.string().nullable().optional()
4836
+ })
4837
+ ),
4838
+ count: z2.number()
4839
+ }),
4781
4840
  examples: [{ description: "List all forum threads" }],
4782
4841
  async run(c) {
4783
4842
  const client = createAssemblyPublicClient(c.env.ABSTRACT_RPC_URL);
@@ -4797,16 +4856,20 @@ forum.command("threads", {
4797
4856
  }))
4798
4857
  }) : [];
4799
4858
  const items = threadTuples.map(decodeThread);
4859
+ const threads = items.map((x) => ({
4860
+ id: x.id,
4861
+ kind: x.kind,
4862
+ author: x.author,
4863
+ createdAt: x.createdAt,
4864
+ createdAtRelative: relTime(x.createdAt),
4865
+ category: x.category ?? null,
4866
+ title: x.title ?? null
4867
+ }));
4800
4868
  return c.ok(
4801
- items.map((x) => ({
4802
- id: x.id,
4803
- kind: x.kind,
4804
- author: x.author,
4805
- createdAt: x.createdAt,
4806
- createdAtRelative: relTime(x.createdAt),
4807
- category: x.category ?? null,
4808
- title: x.title ?? null
4809
- })),
4869
+ {
4870
+ threads,
4871
+ count: threads.length
4872
+ },
4810
4873
  {
4811
4874
  cta: {
4812
4875
  description: "Inspect or comment:",
@@ -5131,16 +5194,19 @@ var governance = Cli3.create("governance", {
5131
5194
  governance.command("proposals", {
5132
5195
  description: "List governance proposals with status and vote end time.",
5133
5196
  env: env3,
5134
- output: z3.array(
5135
- z3.object({
5136
- id: z3.number(),
5137
- kind: z3.number(),
5138
- status: z3.number(),
5139
- title: z3.string().nullable().optional(),
5140
- voteEndAt: z3.number(),
5141
- voteEndRelative: z3.string()
5142
- })
5143
- ),
5197
+ output: z3.object({
5198
+ proposals: z3.array(
5199
+ z3.object({
5200
+ id: z3.number(),
5201
+ kind: z3.number(),
5202
+ status: z3.number(),
5203
+ title: z3.string().nullable().optional(),
5204
+ voteEndAt: z3.number(),
5205
+ voteEndRelative: z3.string()
5206
+ })
5207
+ ),
5208
+ count: z3.number()
5209
+ }),
5144
5210
  examples: [{ description: "List all proposals" }],
5145
5211
  async run(c) {
5146
5212
  const client = createAssemblyPublicClient(c.env.ABSTRACT_RPC_URL);
@@ -5160,15 +5226,19 @@ governance.command("proposals", {
5160
5226
  }))
5161
5227
  }) : [];
5162
5228
  const proposals = proposalTuples.map(decodeProposal);
5229
+ const items = proposals.map((p, i) => ({
5230
+ id: i + 1,
5231
+ kind: asNum(p.kind),
5232
+ status: asNum(p.status),
5233
+ title: p.title ?? null,
5234
+ voteEndAt: asNum(p.voteEndAt),
5235
+ voteEndRelative: relTime(p.voteEndAt)
5236
+ }));
5163
5237
  return c.ok(
5164
- proposals.map((p, i) => ({
5165
- id: i + 1,
5166
- kind: asNum(p.kind),
5167
- status: asNum(p.status),
5168
- title: p.title ?? null,
5169
- voteEndAt: asNum(p.voteEndAt),
5170
- voteEndRelative: relTime(p.voteEndAt)
5171
- })),
5238
+ {
5239
+ proposals: items,
5240
+ count: items.length
5241
+ },
5172
5242
  {
5173
5243
  cta: {
5174
5244
  description: "Inspect or vote:",
@@ -5301,6 +5371,7 @@ governance.command("params", {
5301
5371
  import { Cli as Cli4, z as z4 } from "incur";
5302
5372
  var DEFAULT_MEMBER_SNAPSHOT_URL = "https://www.theaiassembly.org/api/indexer/members";
5303
5373
  var REGISTERED_EVENT_SCAN_STEP = 100000n;
5374
+ var REGISTERED_EVENT_SCAN_TIMEOUT_MS = 2e4;
5304
5375
  var env4 = z4.object({
5305
5376
  ABSTRACT_RPC_URL: z4.string().optional().describe("Abstract RPC URL override"),
5306
5377
  ASSEMBLY_INDEXER_URL: z4.string().optional().describe("Optional members snapshot endpoint (default: theaiassembly.org indexer)")
@@ -5351,10 +5422,25 @@ async function memberSnapshot(url) {
5351
5422
  response: json
5352
5423
  });
5353
5424
  }
5425
+ async function withTimeout(promise, timeoutMs, timeoutMessage) {
5426
+ let timer;
5427
+ try {
5428
+ return await Promise.race([
5429
+ promise,
5430
+ new Promise((_, reject) => {
5431
+ timer = setTimeout(() => {
5432
+ reject(new Error(timeoutMessage));
5433
+ }, timeoutMs);
5434
+ })
5435
+ ]);
5436
+ } finally {
5437
+ if (timer) clearTimeout(timer);
5438
+ }
5439
+ }
5354
5440
  async function membersFromRegisteredEvents(client) {
5355
5441
  const latestBlock = await client.getBlockNumber();
5356
5442
  const addresses = /* @__PURE__ */ new Set();
5357
- for (let fromBlock = 0n; fromBlock <= latestBlock; fromBlock += REGISTERED_EVENT_SCAN_STEP) {
5443
+ for (let fromBlock = ABSTRACT_MAINNET_DEPLOYMENT_BLOCKS.registry; fromBlock <= latestBlock; fromBlock += REGISTERED_EVENT_SCAN_STEP) {
5358
5444
  const toBlock = fromBlock + REGISTERED_EVENT_SCAN_STEP - 1n > latestBlock ? latestBlock : fromBlock + REGISTERED_EVENT_SCAN_STEP - 1n;
5359
5445
  const events = await client.getContractEvents({
5360
5446
  abi: registryAbi,
@@ -5433,7 +5519,11 @@ members.command("list", {
5433
5519
  }
5434
5520
  fallbackReason = error.details;
5435
5521
  try {
5436
- addresses = await membersFromRegisteredEvents(client);
5522
+ addresses = await withTimeout(
5523
+ membersFromRegisteredEvents(client),
5524
+ REGISTERED_EVENT_SCAN_TIMEOUT_MS,
5525
+ `Registered event fallback scan timed out after ${REGISTERED_EVENT_SCAN_TIMEOUT_MS}ms`
5526
+ );
5437
5527
  } catch (fallbackError) {
5438
5528
  return c.error({
5439
5529
  code: "MEMBER_LIST_SOURCE_UNAVAILABLE",
@@ -5708,6 +5798,133 @@ treasury.command("executed", {
5708
5798
  }
5709
5799
  });
5710
5800
 
5801
+ // src/error-handling.ts
5802
+ import { AsyncLocalStorage } from "async_hooks";
5803
+ import { Errors } from "incur";
5804
+ var VIEM_VERSION_PATTERN = /\n*Version:\s*viem@[^\n]+/i;
5805
+ var VIEM_VERSION_PATTERN_GLOBAL = /\n*Version:\s*viem@[^\n]+/gi;
5806
+ var debugFlagStore = new AsyncLocalStorage();
5807
+ var VIEM_ERROR_NAMES = /* @__PURE__ */ new Set([
5808
+ "CallExecutionError",
5809
+ "ContractFunctionExecutionError",
5810
+ "ContractFunctionRevertedError",
5811
+ "HttpRequestError",
5812
+ "InvalidAddressError",
5813
+ "TransactionExecutionError"
5814
+ ]);
5815
+ function parseDebugFlag(argv) {
5816
+ const cleaned = [];
5817
+ let debug = false;
5818
+ for (let i = 0; i < argv.length; i += 1) {
5819
+ const token = argv[i];
5820
+ if (token === "--debug") {
5821
+ debug = true;
5822
+ continue;
5823
+ }
5824
+ if (token === "--debug=true" || token === "--debug=1") {
5825
+ debug = true;
5826
+ continue;
5827
+ }
5828
+ if (token === "--debug=false" || token === "--debug=0") {
5829
+ debug = false;
5830
+ continue;
5831
+ }
5832
+ cleaned.push(token);
5833
+ }
5834
+ return { argv: cleaned, debug };
5835
+ }
5836
+ function isViemLikeError(error) {
5837
+ if (!(error instanceof Error)) return false;
5838
+ const shortMessage = error.shortMessage;
5839
+ return VIEM_ERROR_NAMES.has(error.name) || VIEM_VERSION_PATTERN.test(error.message) || typeof shortMessage === "string" && error.message.includes("Docs: https://viem.sh");
5840
+ }
5841
+ function sanitizeViemMessage(message) {
5842
+ return message.replace(VIEM_VERSION_PATTERN_GLOBAL, "").trim();
5843
+ }
5844
+ function toFriendlyViemError(error) {
5845
+ const shortMessage = typeof error.shortMessage === "string" && error.shortMessage.trim().length > 0 ? error.shortMessage.trim() : void 0;
5846
+ if (error.name === "InvalidAddressError" || shortMessage?.startsWith('Address "')) {
5847
+ return {
5848
+ code: "INVALID_ADDRESS",
5849
+ message: `${shortMessage ?? "Invalid address."} Use a valid 0x-prefixed 20-byte address. Run with --debug for full error details.`
5850
+ };
5851
+ }
5852
+ if (shortMessage?.toLowerCase().includes("http request failed") || error.message.toLowerCase().includes("http request failed")) {
5853
+ return {
5854
+ code: "RPC_CONNECTION_FAILED",
5855
+ message: "RPC connection failed. Check ABSTRACT_RPC_URL and try again. Run with --debug for full error details."
5856
+ };
5857
+ }
5858
+ if (shortMessage || VIEM_VERSION_PATTERN.test(error.message)) {
5859
+ return {
5860
+ code: "UPSTREAM_ERROR",
5861
+ message: `${sanitizeViemMessage(shortMessage ?? error.message)} Run with --debug for full error details.`
5862
+ };
5863
+ }
5864
+ return void 0;
5865
+ }
5866
+ function isMissingRequiredArgValidation(error) {
5867
+ return error.fieldErrors.some((fieldError) => {
5868
+ const msg = fieldError.message.toLowerCase();
5869
+ return msg.includes("received undefined") || fieldError.received === "";
5870
+ });
5871
+ }
5872
+ function missingArgPaths(error) {
5873
+ const paths = /* @__PURE__ */ new Set();
5874
+ for (const fieldError of error.fieldErrors) {
5875
+ const msg = fieldError.message.toLowerCase();
5876
+ if (msg.includes("received undefined") || fieldError.received === "") {
5877
+ paths.add(fieldError.path);
5878
+ }
5879
+ }
5880
+ return [...paths];
5881
+ }
5882
+ function toFriendlyValidationError(ctx, error) {
5883
+ const missing = missingArgPaths(error);
5884
+ if (missing.length === 0) return void 0;
5885
+ const helpCommand = `${ctx.name} ${ctx.command} --help`;
5886
+ const argsList = missing.join(", ");
5887
+ return {
5888
+ code: "VALIDATION_ERROR",
5889
+ cta: {
5890
+ description: "See command usage:",
5891
+ commands: [{ command: helpCommand }]
5892
+ },
5893
+ message: missing.length === 1 ? `Missing required argument: ${argsList}. Run \`${helpCommand}\` for usage.` : `Missing required arguments: ${argsList}. Run \`${helpCommand}\` for usage.`
5894
+ };
5895
+ }
5896
+ function handleError(ctx, error) {
5897
+ if (debugFlagStore.getStore()) throw error;
5898
+ if (error instanceof Errors.ValidationError && isMissingRequiredArgValidation(error)) {
5899
+ const friendly = toFriendlyValidationError(ctx, error);
5900
+ if (friendly) {
5901
+ return ctx.error(friendly);
5902
+ }
5903
+ }
5904
+ if (isViemLikeError(error)) {
5905
+ const friendly = toFriendlyViemError(error);
5906
+ if (friendly) {
5907
+ return ctx.error(friendly);
5908
+ }
5909
+ }
5910
+ throw error;
5911
+ }
5912
+ function applyFriendlyErrorHandling(cli2) {
5913
+ const originalServe = cli2.serve.bind(cli2);
5914
+ cli2.serve = (async (argv, options) => {
5915
+ const rawArgv = argv ?? process.argv.slice(2);
5916
+ const parsed = parseDebugFlag(rawArgv);
5917
+ return debugFlagStore.run(parsed.debug, () => originalServe(parsed.argv, options));
5918
+ });
5919
+ cli2.use(async (context, next) => {
5920
+ try {
5921
+ await next();
5922
+ } catch (error) {
5923
+ return handleError(context, error);
5924
+ }
5925
+ });
5926
+ }
5927
+
5711
5928
  // src/cli.ts
5712
5929
  var cli = Cli6.create("assembly", {
5713
5930
  description: "Assembly governance CLI for Abstract chain."
@@ -5843,6 +6060,7 @@ cli.command("health", {
5843
6060
  });
5844
6061
  }
5845
6062
  });
6063
+ applyFriendlyErrorHandling(cli);
5846
6064
  var isMain = process.argv[1] === fileURLToPath(import.meta.url);
5847
6065
  if (isMain) {
5848
6066
  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.1",
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
  },