@aixyz/cli 0.9.0 → 0.11.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.
package/bin.ts CHANGED
@@ -1,200 +1,23 @@
1
1
  #!/usr/bin/env bun
2
- import { program } from "commander";
3
- import { build } from "./build";
4
- import { dev } from "./dev";
5
- import { register } from "./register/register";
6
- import { update } from "./register/update";
2
+ import { Command } from "commander";
3
+ import { devCommand } from "./dev";
4
+ import { buildCommand } from "./build";
5
+ import { erc8004Command } from "./register";
7
6
  import pkg from "./package.json";
8
7
 
9
- function handleAction(
10
- action: (options: Record<string, unknown>) => Promise<void>,
11
- ): (options: Record<string, unknown>) => Promise<void> {
12
- return async (options) => {
13
- try {
14
- await action(options);
15
- } catch (error) {
16
- if (error instanceof Error && error.name === "ExitPromptError") {
17
- process.exit(130);
18
- }
19
- console.error(`Error: ${error instanceof Error ? error.message : String(error)}`);
20
- process.exit(1);
21
- }
22
- };
8
+ const cli = new Command();
9
+ cli.name("aixyz").description("CLI for building and deploying aixyz agents").version(pkg.version);
10
+
11
+ cli.addCommand(devCommand);
12
+ cli.addCommand(buildCommand);
13
+ cli.addCommand(erc8004Command);
14
+
15
+ try {
16
+ await cli.parseAsync();
17
+ } catch (error) {
18
+ if (error instanceof Error && error.name === "ExitPromptError") {
19
+ process.exit(130);
20
+ }
21
+ console.error(`Error: ${error instanceof Error ? error.message : String(error)}`);
22
+ process.exit(1);
23
23
  }
24
-
25
- program.name("aixyz").description("CLI for building and deploying aixyz agents").version(pkg.version);
26
-
27
- program
28
- .command("dev")
29
- .description("Start a local development server")
30
- .option("-p, --port <port>", "Port to listen on", "3000")
31
- .action(handleAction(dev));
32
-
33
- program
34
- .command("build")
35
- .description("Build the aixyz agent")
36
- .option("--output <type>", "Output format: 'standalone' or 'vercel'")
37
- .addHelpText(
38
- "after",
39
- `
40
- Details:
41
- Bundles your aixyz agent for deployment.
42
-
43
- Default behavior (auto-detected):
44
- Bundles into a single executable file for Standalone at ./.aixyz/output/server.js
45
-
46
- With --output vercel or VERCEL=1 env:
47
- Generates Vercel Build Output API v3 structure at .vercel/output/
48
- (Automatically detected when deploying to Vercel)
49
-
50
- The build process:
51
- 1. Loads aixyz.config.ts from the current directory
52
- 2. Detects entrypoint (app/server.ts or auto-generates from app/agent.ts + app/tools/)
53
- 3. Bundles the application
54
- 4. Copies static assets from public/ (if present)
55
-
56
- Prerequisites:
57
- - An aixyz.config.ts with a default export
58
- - An entrypoint at app/server.ts, or app/agent.ts + app/tools/ for auto-generation
59
-
60
- Examples:
61
- $ aixyz build # Build standalone (default)
62
- $ aixyz build --output standalone # Build standalone explicitly
63
- $ aixyz build --output vercel # Build for Vercel deployment
64
- $ VERCEL=1 aixyz build # Auto-detected Vercel build`,
65
- )
66
- .action(handleAction(build));
67
-
68
- const erc8004 = program.command("erc-8004").description("ERC-8004 IdentityRegistry operations");
69
-
70
- erc8004
71
- .command("register")
72
- .description("Register a new agent to the ERC-8004 IdentityRegistry")
73
- .option("--url <url>", "Agent deployment URL (e.g., https://my-agent.example.com)")
74
- .option("--chain <chain>", "Target chain (mainnet, sepolia, base-sepolia, localhost)")
75
- .option("--rpc-url <url>", "Custom RPC URL (uses default if not provided)")
76
- .option("--registry <address>", "Contract address of the IdentityRegistry (required for localhost)")
77
- .option("--keystore <path>", "Path to Ethereum keystore (V3) JSON file for local signing")
78
- .option("--browser", "Use browser extension wallet (any extension)")
79
- .option("--broadcast", "Sign and broadcast the transaction (default: dry-run)")
80
- .option("--out-dir <path>", "Write deployment result as JSON to the given directory")
81
- .addHelpText(
82
- "after",
83
- `
84
- Option Details:
85
- --url <url>
86
- Agent deployment URL (e.g., https://my-agent.example.com).
87
- The registration URI will be derived as <url>/_aixyz/erc-8004.json.
88
- If omitted, you will be prompted to enter the URL interactively.
89
-
90
- --chain <chain>
91
- Target chain for registration. Supported values:
92
- mainnet Ethereum mainnet (chain ID 1)
93
- sepolia Ethereum Sepolia testnet (chain ID 11155111)
94
- base-sepolia Base Sepolia testnet (chain ID 84532)
95
- localhost Local Foundry/Anvil node (chain ID 31337)
96
- If omitted, you will be prompted to select a chain interactively.
97
-
98
- --rpc-url <url>
99
- Custom RPC endpoint URL. Overrides the default RPC for the selected
100
- chain. Cannot be used with --browser since the browser wallet manages
101
- its own RPC connection.
102
-
103
- --registry <address>
104
- Contract address of the ERC-8004 IdentityRegistry. Only required for
105
- localhost, where there is no default deployment.
106
-
107
- --keystore <path>
108
- Path to an Ethereum keystore (V3) JSON file. You will be prompted for
109
- the keystore password to decrypt the private key for signing.
110
-
111
- --browser
112
- Opens a local page in your default browser for signing with any
113
- EIP-6963 compatible wallet extension (MetaMask, Rabby, etc.).
114
-
115
- --broadcast
116
- Sign and broadcast the transaction on-chain. Without this flag the
117
- command performs a dry-run.
118
-
119
- --out-dir <path>
120
- Directory to write the deployment result as a JSON file.
121
-
122
- Behavior:
123
- If app/erc-8004.ts does not exist, you will be prompted to create it
124
- (selecting supported trust mechanisms). After a successful on-chain
125
- registration, the new registration entry is written back to app/erc-8004.ts.
126
-
127
- Environment Variables:
128
- PRIVATE_KEY Private key (hex, with or without 0x prefix) used for signing.
129
-
130
- Examples:
131
- # Dry-run (default)
132
- $ aixyz erc-8004 register --url "https://my-agent.example.com" --chain sepolia
133
-
134
- # Sign and broadcast
135
- $ aixyz erc-8004 register --url "https://my-agent.example.com" --chain sepolia --keystore ~/.foundry/keystores/default --broadcast
136
- $ aixyz erc-8004 register --url "https://my-agent.example.com" --chain sepolia --browser --broadcast`,
137
- )
138
- .action(handleAction(register));
139
-
140
- erc8004
141
- .command("update")
142
- .description("Update the metadata URI of a registered agent")
143
- .option("--url <url>", "New agent deployment URL (e.g., https://my-agent.example.com)")
144
- .option("--rpc-url <url>", "Custom RPC URL (uses default if not provided)")
145
- .option("--registry <address>", "Contract address of the IdentityRegistry (required for localhost)")
146
- .option("--keystore <path>", "Path to Ethereum keystore (V3) JSON file for local signing")
147
- .option("--browser", "Use browser extension wallet (any extension)")
148
- .option("--broadcast", "Sign and broadcast the transaction (default: dry-run)")
149
- .option("--out-dir <path>", "Write result as JSON to the given directory")
150
- .addHelpText(
151
- "after",
152
- `
153
- Option Details:
154
- --url <url>
155
- New agent deployment URL (e.g., https://my-agent.example.com).
156
- The URI will be derived as <url>/_aixyz/erc-8004.json.
157
- If omitted, you will be prompted to enter the URL interactively.
158
-
159
- --rpc-url <url>
160
- Custom RPC endpoint URL. Overrides the default RPC for the selected
161
- chain. Cannot be used with --browser.
162
-
163
- --registry <address>
164
- Contract address of the ERC-8004 IdentityRegistry. Only required for
165
- localhost, where there is no default deployment.
166
-
167
- --keystore <path>
168
- Path to an Ethereum keystore (V3) JSON file.
169
-
170
- --browser
171
- Opens a local page in your default browser for signing with any
172
- EIP-6963 compatible wallet extension (MetaMask, Rabby, etc.).
173
-
174
- --broadcast
175
- Sign and broadcast the transaction on-chain. Without this flag the
176
- command performs a dry-run.
177
-
178
- --out-dir <path>
179
- Directory to write the result as a JSON file.
180
-
181
- Behavior:
182
- Reads existing registrations from app/erc-8004.ts. If there is one
183
- registration, confirms it. If multiple, prompts you to select which
184
- one to update. The chain and registry address are derived from the
185
- selected registration's agentRegistry field.
186
-
187
- Environment Variables:
188
- PRIVATE_KEY Private key (hex, with or without 0x prefix) used for signing.
189
-
190
- Examples:
191
- # Dry-run (default)
192
- $ aixyz erc-8004 update --url "https://new-domain.example.com"
193
-
194
- # Sign and broadcast
195
- $ aixyz erc-8004 update --url "https://new-domain.example.com" --keystore ~/.foundry/keystores/default --broadcast
196
- $ aixyz erc-8004 update --url "https://new-domain.example.com" --browser --broadcast`,
197
- )
198
- .action(handleAction(update));
199
-
200
- program.parse();
package/build/index.ts CHANGED
@@ -1,5 +1,6 @@
1
1
  import { resolve } from "path";
2
2
  import { existsSync, mkdirSync, cpSync, rmSync } from "fs";
3
+ import { Command } from "commander";
3
4
  import { AixyzConfigPlugin } from "./AixyzConfigPlugin";
4
5
  import { AixyzServerPlugin, getEntrypointMayGenerate } from "./AixyzServerPlugin";
5
6
  import { findIconFile, copyAgentIcon, generateFavicon } from "./icons";
@@ -11,9 +12,45 @@ interface BuildOptions {
11
12
  output?: string;
12
13
  }
13
14
 
14
- export async function build(options: BuildOptions = {}): Promise<void> {
15
+ export const buildCommand = new Command("build")
16
+ .description("Build the aixyz agent")
17
+ .option("--output <type>", "Output format: 'standalone' or 'vercel'")
18
+ .addHelpText(
19
+ "after",
20
+ `
21
+ Details:
22
+ Bundles your aixyz agent for deployment.
23
+
24
+ Default behavior (auto-detected):
25
+ Bundles into a single executable file for Standalone at ./.aixyz/output/server.js
26
+
27
+ With --output vercel or VERCEL=1 env:
28
+ Generates Vercel Build Output API v3 structure at .vercel/output/
29
+ (Automatically detected when deploying to Vercel)
30
+
31
+ The build process:
32
+ 1. Loads aixyz.config.ts from the current directory
33
+ 2. Detects entrypoint (app/server.ts or auto-generates from app/agent.ts + app/tools/)
34
+ 3. Bundles the application
35
+ 4. Copies static assets from public/ (if present)
36
+
37
+ Prerequisites:
38
+ - An aixyz.config.ts with a default export
39
+ - An entrypoint at app/server.ts, or app/agent.ts + app/tools/ for auto-generation
40
+
41
+ Examples:
42
+ $ aixyz build # Build standalone (default)
43
+ $ aixyz build --output standalone # Build standalone explicitly
44
+ $ aixyz build --output vercel # Build for Vercel deployment
45
+ $ VERCEL=1 aixyz build # Auto-detected Vercel build`,
46
+ )
47
+ .action(action);
48
+
49
+ async function action(options: BuildOptions = {}): Promise<void> {
15
50
  const cwd = process.cwd();
16
51
  loadEnvConfig(cwd, false);
52
+ process.env.NODE_ENV = "production";
53
+ process.env.AIXYZ_ENV = "production";
17
54
  const entrypoint = getEntrypointMayGenerate(cwd, "build");
18
55
 
19
56
  // Determine output target: explicit CLI flag takes precedence, then config file, then auto-detect VERCEL env
@@ -44,6 +81,10 @@ async function buildBun(entrypoint: string): Promise<void> {
44
81
  target: "bun",
45
82
  format: "esm",
46
83
  sourcemap: "linked",
84
+ define: {
85
+ "process.env.NODE_ENV": JSON.stringify("production"),
86
+ "process.env.AIXYZ_ENV": JSON.stringify("production"),
87
+ },
47
88
  plugins: [AixyzConfigPlugin(), AixyzServerPlugin(entrypoint, "standalone")],
48
89
  });
49
90
 
@@ -101,6 +142,10 @@ async function buildVercel(entrypoint: string): Promise<void> {
101
142
  target: "bun",
102
143
  format: "esm",
103
144
  sourcemap: "linked",
145
+ define: {
146
+ "process.env.NODE_ENV": JSON.stringify("production"),
147
+ "process.env.AIXYZ_ENV": JSON.stringify("production"),
148
+ },
104
149
  plugins: [AixyzConfigPlugin(), AixyzServerPlugin(entrypoint, "vercel")],
105
150
  });
106
151
 
package/dev/index.ts CHANGED
@@ -1,14 +1,22 @@
1
1
  import { resolve, relative } from "path";
2
2
  import { existsSync, watch } from "fs";
3
3
  import { loadEnvConfig } from "@next/env";
4
+ import { Command } from "commander";
4
5
  import { getEntrypointMayGenerate } from "../build/AixyzServerPlugin";
5
6
  import pkg from "../package.json";
6
7
 
7
- export async function dev(options: { port?: string }): Promise<void> {
8
+ export const devCommand = new Command("dev")
9
+ .description("Start a local development server")
10
+ .option("-p, --port <port>", "Port to listen on", "3000")
11
+ .action(action);
12
+
13
+ async function action(options: { port?: string }): Promise<void> {
8
14
  const cwd = process.cwd();
9
15
 
10
16
  // Load environment config
11
17
  const { loadedEnvFiles } = loadEnvConfig(cwd, true);
18
+ process.env.NODE_ENV = "development";
19
+ process.env.AIXYZ_ENV = "development";
12
20
  const envFileNames = loadedEnvFiles.map((f) => relative(cwd, f.path));
13
21
 
14
22
  const port = options.port || process.env.PORT || "3000";
@@ -35,7 +43,7 @@ export async function dev(options: { port?: string }): Promise<void> {
35
43
  cwd,
36
44
  stdout: "inherit",
37
45
  stderr: "inherit",
38
- env: process.env,
46
+ env: { ...process.env, NODE_ENV: "development", AIXYZ_ENV: "development" },
39
47
  });
40
48
  child.exited.then((code) => {
41
49
  if (!restarting && code !== 0) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aixyz/cli",
3
- "version": "0.9.0",
3
+ "version": "0.11.0",
4
4
  "description": "Payment-native SDK for AI Agent",
5
5
  "keywords": [
6
6
  "ai",
@@ -27,8 +27,8 @@
27
27
  "bin.ts"
28
28
  ],
29
29
  "dependencies": {
30
- "@aixyz/config": "0.9.0",
31
- "@aixyz/erc-8004": "0.9.0",
30
+ "@aixyz/config": "0.11.0",
31
+ "@aixyz/erc-8004": "0.11.0",
32
32
  "@inquirer/prompts": "^8.3.0",
33
33
  "@next/env": "^16.1.6",
34
34
  "boxen": "^8.0.1",
package/register/index.ts CHANGED
@@ -1,8 +1,146 @@
1
+ import { Command } from "commander";
2
+ import { register } from "./register";
3
+ import { update } from "./update";
1
4
  import type { WalletOptions } from "./wallet";
2
5
 
3
6
  export interface BaseOptions extends WalletOptions {
4
- chain?: string;
5
7
  rpcUrl?: string;
6
8
  registry?: string;
7
9
  outDir?: string;
8
10
  }
11
+
12
+ export const erc8004Command = new Command("erc-8004").description("ERC-8004 IdentityRegistry operations");
13
+
14
+ erc8004Command
15
+ .command("register")
16
+ .description("Register a new agent to the ERC-8004 IdentityRegistry")
17
+ .option("--url <url>", "Agent deployment URL (e.g., https://my-agent.example.com)")
18
+ .option("--chain-id <chainId>", "Target chain by numeric chain ID", parseInt)
19
+ .option("--rpc-url <url>", "Custom RPC URL (uses default if not provided)")
20
+ .option("--registry <address>", "Contract address of the IdentityRegistry (required for localhost and custom chains)")
21
+ .option("--keystore <path>", "Path to Ethereum keystore (V3) JSON file for local signing")
22
+ .option("--browser", "Use browser extension wallet (any extension)")
23
+ .option("--broadcast", "Sign and broadcast the transaction (default: dry-run)")
24
+ .option("--out-dir <path>", "Write deployment result as JSON to the given directory")
25
+ .addHelpText(
26
+ "after",
27
+ `
28
+ Option Details:
29
+ --url <url>
30
+ Agent deployment URL (e.g., https://my-agent.example.com).
31
+ The registration URI will be derived as <url>/_aixyz/erc-8004.json.
32
+ If omitted, you will be prompted to enter the URL interactively.
33
+
34
+ --chain-id <chainId>
35
+ Target chain by numeric chain ID. Supported chain IDs include:
36
+ 1 (mainnet), 8453 (base), 42161 (arbitrum), 10 (optimism),
37
+ 137 (polygon), 56 (bsc), 43114 (avalanche), 11155111 (sepolia),
38
+ 84532 (base-sepolia), and more (see @aixyz/erc-8004).
39
+ If omitted, you will be prompted to select a chain interactively.
40
+ Use any custom EVM chain ID with --rpc-url for BYO chains.
41
+
42
+ --rpc-url <url>
43
+ Custom RPC endpoint URL. Overrides the default RPC for the selected
44
+ chain. Required when using a custom chain ID with no default deployment.
45
+ Cannot be used with --browser since the browser wallet manages
46
+ its own RPC connection.
47
+
48
+ --registry <address>
49
+ Contract address of the ERC-8004 IdentityRegistry. Required for
50
+ localhost and custom chains, where there is no default deployment.
51
+
52
+ --keystore <path>
53
+ Path to an Ethereum keystore (V3) JSON file. You will be prompted for
54
+ the keystore password to decrypt the private key for signing.
55
+
56
+ --browser
57
+ Opens a local page in your default browser for signing with any
58
+ EIP-6963 compatible wallet extension (MetaMask, Rabby, etc.).
59
+
60
+ --broadcast
61
+ Sign and broadcast the transaction on-chain. Without this flag the
62
+ command performs a dry-run.
63
+
64
+ --out-dir <path>
65
+ Directory to write the deployment result as a JSON file.
66
+
67
+ Behavior:
68
+ If app/erc-8004.ts does not exist, you will be prompted to create it
69
+ (selecting supported trust mechanisms). After a successful on-chain
70
+ registration, the new registration entry is written back to app/erc-8004.ts.
71
+
72
+ Environment Variables:
73
+ PRIVATE_KEY Private key (hex, with or without 0x prefix) used for signing.
74
+
75
+ Examples:
76
+ # Dry-run (default)
77
+ $ aixyz erc-8004 register --url "https://my-agent.example.com" --chain-id 11155111
78
+
79
+ # Sign and broadcast (known chain)
80
+ $ aixyz erc-8004 register --url "https://my-agent.example.com" --chain-id 84532 --keystore ~/.foundry/keystores/default --broadcast
81
+ $ aixyz erc-8004 register --url "https://my-agent.example.com" --chain-id 84532 --browser --broadcast
82
+
83
+ # BYO: register on any custom EVM chain
84
+ $ aixyz erc-8004 register --url "https://my-agent.example.com" --chain-id 999999 --rpc-url https://my-rpc.example.com --registry 0xABCD... --broadcast`,
85
+ )
86
+ .action(register);
87
+
88
+ erc8004Command
89
+ .command("update")
90
+ .description("Update the metadata URI of a registered agent")
91
+ .option("--url <url>", "New agent deployment URL (e.g., https://my-agent.example.com)")
92
+ .option("--rpc-url <url>", "Custom RPC URL (uses default if not provided)")
93
+ .option("--registry <address>", "Contract address of the IdentityRegistry (required for localhost)")
94
+ .option("--keystore <path>", "Path to Ethereum keystore (V3) JSON file for local signing")
95
+ .option("--browser", "Use browser extension wallet (any extension)")
96
+ .option("--broadcast", "Sign and broadcast the transaction (default: dry-run)")
97
+ .option("--out-dir <path>", "Write result as JSON to the given directory")
98
+ .addHelpText(
99
+ "after",
100
+ `
101
+ Option Details:
102
+ --url <url>
103
+ New agent deployment URL (e.g., https://my-agent.example.com).
104
+ The URI will be derived as <url>/_aixyz/erc-8004.json.
105
+ If omitted, you will be prompted to enter the URL interactively.
106
+
107
+ --rpc-url <url>
108
+ Custom RPC endpoint URL. Overrides the default RPC for the selected
109
+ chain. Cannot be used with --browser.
110
+
111
+ --registry <address>
112
+ Contract address of the ERC-8004 IdentityRegistry. Only required for
113
+ localhost, where there is no default deployment.
114
+
115
+ --keystore <path>
116
+ Path to an Ethereum keystore (V3) JSON file.
117
+
118
+ --browser
119
+ Opens a local page in your default browser for signing with any
120
+ EIP-6963 compatible wallet extension (MetaMask, Rabby, etc.).
121
+
122
+ --broadcast
123
+ Sign and broadcast the transaction on-chain. Without this flag the
124
+ command performs a dry-run.
125
+
126
+ --out-dir <path>
127
+ Directory to write the result as a JSON file.
128
+
129
+ Behavior:
130
+ Reads existing registrations from app/erc-8004.ts. If there is one
131
+ registration, confirms it. If multiple, prompts you to select which
132
+ one to update. The chain and registry address are derived from the
133
+ selected registration's agentRegistry field.
134
+
135
+ Environment Variables:
136
+ PRIVATE_KEY Private key (hex, with or without 0x prefix) used for signing.
137
+
138
+ Examples:
139
+ # Dry-run (default)
140
+ $ aixyz erc-8004 update --url "https://new-domain.example.com"
141
+
142
+ # Sign and broadcast
143
+ $ aixyz erc-8004 update --url "https://new-domain.example.com" --keystore ~/.foundry/keystores/default --broadcast
144
+ $ aixyz erc-8004 update --url "https://new-domain.example.com" --browser --broadcast`,
145
+ )
146
+ .action(update);
@@ -1,17 +1,18 @@
1
1
  import { encodeFunctionData, formatEther, parseEventLogs, type Chain, type Log } from "viem";
2
2
  import { IdentityRegistryAbi } from "@aixyz/erc-8004";
3
- import { selectWalletMethod, type WalletOptions } from "./wallet";
3
+ import { selectWalletMethod } from "./wallet";
4
4
  import { signTransaction } from "./wallet/sign";
5
5
  import {
6
- resolveChainConfig,
6
+ resolveChainConfigById,
7
7
  selectChain,
8
8
  resolveRegistryAddress,
9
9
  validateBrowserRpcConflict,
10
10
  getExplorerUrl,
11
+ CHAINS,
11
12
  } from "./utils/chain";
12
13
  import { writeResultJson } from "./utils/result";
13
14
  import { label, truncateUri, broadcastAndConfirm, logSignResult } from "./utils/transaction";
14
- import { promptAgentUrl, promptSupportedTrust, deriveAgentUri } from "./utils/prompt";
15
+ import { promptAgentUrl, promptSupportedTrust, promptRegistryAddress, deriveAgentUri } from "./utils/prompt";
15
16
  import { hasErc8004File, createErc8004File, writeRegistrationEntry } from "./utils/erc8004-file";
16
17
  import { confirm } from "@inquirer/prompts";
17
18
  import chalk from "chalk";
@@ -20,7 +21,7 @@ import type { BaseOptions } from "./index";
20
21
 
21
22
  export interface RegisterOptions extends BaseOptions {
22
23
  url?: string;
23
- chain?: string;
24
+ chainId?: number;
24
25
  }
25
26
 
26
27
  export async function register(options: RegisterOptions): Promise<void> {
@@ -47,9 +48,10 @@ export async function register(options: RegisterOptions): Promise<void> {
47
48
  }
48
49
 
49
50
  // Step 3: Select chain
50
- const chainName = options.chain ?? (await selectChain());
51
- const chainConfig = resolveChainConfig(chainName);
52
- const registryAddress = resolveRegistryAddress(chainName, chainConfig.chainId, options.registry);
51
+ const chainId = options.chainId ?? (await selectChain());
52
+ const chainConfig = resolveChainConfigById(chainId, options.rpcUrl);
53
+ const chainName = Object.entries(CHAINS).find(([, c]) => c.chainId === chainId)?.[0] ?? `chain-${chainId}`;
54
+ const registryAddress = resolveRegistryAddress(chainId, options.registry) ?? (await promptRegistryAddress());
53
55
 
54
56
  // Step 4: Encode transaction
55
57
  const data = encodeFunctionData({
@@ -2,13 +2,7 @@ import { encodeFunctionData, formatEther, parseEventLogs, type Chain, type Log }
2
2
  import { IdentityRegistryAbi } from "@aixyz/erc-8004";
3
3
  import { selectWalletMethod } from "./wallet";
4
4
  import { signTransaction } from "./wallet/sign";
5
- import {
6
- resolveChainConfig,
7
- resolveRegistryAddress,
8
- validateBrowserRpcConflict,
9
- getExplorerUrl,
10
- CHAINS,
11
- } from "./utils/chain";
5
+ import { resolveChainConfigById, validateBrowserRpcConflict, getExplorerUrl, CHAINS } from "./utils/chain";
12
6
  import { writeResultJson } from "./utils/result";
13
7
  import { label, truncateUri, broadcastAndConfirm, logSignResult } from "./utils/transaction";
14
8
  import { promptAgentUrl, promptSelectRegistration, deriveAgentUri } from "./utils/prompt";
@@ -42,7 +36,7 @@ export async function update(options: UpdateOptions): Promise<void> {
42
36
  const chainId = Number(parts[1]);
43
37
  const registryAddress = parts.slice(2).join(":") as `0x${string}`;
44
38
  const chainName = Object.entries(CHAINS).find(([, config]) => config.chainId === chainId)?.[0] ?? `chain-${chainId}`;
45
- const chainConfig = resolveChainConfig(chainName);
39
+ const chainConfig = resolveChainConfigById(chainId, options.rpcUrl);
46
40
 
47
41
  // Step 4: Get new agent URL and derive URI
48
42
  const agentUrl = options.url ?? (await promptAgentUrl());
@@ -1,20 +1,128 @@
1
- import { isAddress, type Chain } from "viem";
2
- import { mainnet, sepolia, baseSepolia, foundry } from "viem/chains";
1
+ import { isAddress, defineChain, type Chain } from "viem";
2
+ import {
3
+ abstract,
4
+ abstractTestnet,
5
+ arbitrum,
6
+ arbitrumSepolia,
7
+ avalanche,
8
+ avalancheFuji,
9
+ base,
10
+ baseSepolia,
11
+ bsc,
12
+ bscTestnet,
13
+ celo,
14
+ celoSepolia,
15
+ foundry,
16
+ gnosis,
17
+ linea,
18
+ lineaSepolia,
19
+ mainnet,
20
+ mantle,
21
+ mantleSepoliaTestnet,
22
+ megaeth,
23
+ monad,
24
+ monadTestnet,
25
+ optimism,
26
+ optimismSepolia,
27
+ polygon,
28
+ polygonAmoy,
29
+ scroll,
30
+ scrollSepolia,
31
+ sepolia,
32
+ taiko,
33
+ } from "viem/chains";
3
34
  import { CHAIN_ID, getIdentityRegistryAddress } from "@aixyz/erc-8004";
4
- import { select } from "@inquirer/prompts";
35
+ import { input, select } from "@inquirer/prompts";
5
36
 
6
37
  export interface ChainConfig {
7
38
  chain: Chain;
8
39
  chainId: number;
9
40
  }
10
41
 
42
+ // Maps supported chain IDs to viem Chain objects (derived from @aixyz/erc-8004 CHAIN_ID)
43
+ const VIEM_CHAIN_BY_ID: Record<number, Chain> = {
44
+ [CHAIN_ID.ABSTRACT]: abstract,
45
+ [CHAIN_ID.ARBITRUM]: arbitrum,
46
+ [CHAIN_ID.AVALANCHE]: avalanche,
47
+ [CHAIN_ID.BASE]: base,
48
+ [CHAIN_ID.BSC]: bsc,
49
+ [CHAIN_ID.CELO]: celo,
50
+ [CHAIN_ID.GNOSIS]: gnosis,
51
+ [CHAIN_ID.LINEA]: linea,
52
+ [CHAIN_ID.MAINNET]: mainnet,
53
+ [CHAIN_ID.MANTLE]: mantle,
54
+ [CHAIN_ID.MEGAETH]: megaeth,
55
+ [CHAIN_ID.MONAD]: monad,
56
+ [CHAIN_ID.OPTIMISM]: optimism,
57
+ [CHAIN_ID.POLYGON]: polygon,
58
+ [CHAIN_ID.SCROLL]: scroll,
59
+ [CHAIN_ID.TAIKO]: taiko,
60
+ [CHAIN_ID.ABSTRACT_TESTNET]: abstractTestnet,
61
+ [CHAIN_ID.ARBITRUM_SEPOLIA]: arbitrumSepolia,
62
+ [CHAIN_ID.AVALANCHE_FUJI]: avalancheFuji,
63
+ [CHAIN_ID.BASE_SEPOLIA]: baseSepolia,
64
+ [CHAIN_ID.BSC_TESTNET]: bscTestnet,
65
+ [CHAIN_ID.CELO_SEPOLIA]: celoSepolia,
66
+ [CHAIN_ID.LINEA_SEPOLIA]: lineaSepolia,
67
+ [CHAIN_ID.MANTLE_SEPOLIA]: mantleSepoliaTestnet,
68
+ [CHAIN_ID.MONAD_TESTNET]: monadTestnet,
69
+ [CHAIN_ID.OPTIMISM_SEPOLIA]: optimismSepolia,
70
+ [CHAIN_ID.POLYGON_AMOY]: polygonAmoy,
71
+ [CHAIN_ID.SCROLL_SEPOLIA]: scrollSepolia,
72
+ [CHAIN_ID.SEPOLIA]: sepolia,
73
+ 31337: foundry,
74
+ };
75
+
76
+ // Build CHAINS from CHAIN_ID as the single source of truth from @aixyz/erc-8004.
77
+ // Chain names are derived from CHAIN_ID keys: lowercase with underscores replaced by hyphens.
11
78
  export const CHAINS: Record<string, ChainConfig> = {
12
- mainnet: { chain: mainnet, chainId: CHAIN_ID.MAINNET },
13
- sepolia: { chain: sepolia, chainId: CHAIN_ID.SEPOLIA },
14
- "base-sepolia": { chain: baseSepolia, chainId: CHAIN_ID.BASE_SEPOLIA },
79
+ ...Object.fromEntries(
80
+ (Object.entries(CHAIN_ID) as [string, number][])
81
+ .filter(([, id]) => id in VIEM_CHAIN_BY_ID)
82
+ .map(([key, id]) => [key.toLowerCase().replace(/_/g, "-"), { chain: VIEM_CHAIN_BY_ID[id]!, chainId: id }]),
83
+ ),
15
84
  localhost: { chain: foundry, chainId: 31337 },
16
85
  };
17
86
 
87
+ // Priority-ordered chain IDs for interactive selection — most popular first.
88
+ const CHAIN_SELECTION_ORDER: number[] = [
89
+ // Popular mainnets
90
+ CHAIN_ID.MAINNET,
91
+ CHAIN_ID.BASE,
92
+ CHAIN_ID.ARBITRUM,
93
+ CHAIN_ID.OPTIMISM,
94
+ CHAIN_ID.POLYGON,
95
+ CHAIN_ID.BSC,
96
+ CHAIN_ID.AVALANCHE,
97
+ CHAIN_ID.SCROLL,
98
+ CHAIN_ID.LINEA,
99
+ CHAIN_ID.CELO,
100
+ CHAIN_ID.GNOSIS,
101
+ CHAIN_ID.TAIKO,
102
+ CHAIN_ID.MANTLE,
103
+ CHAIN_ID.MONAD,
104
+ CHAIN_ID.MEGAETH,
105
+ CHAIN_ID.ABSTRACT,
106
+ // Popular testnets
107
+ CHAIN_ID.SEPOLIA,
108
+ CHAIN_ID.BASE_SEPOLIA,
109
+ CHAIN_ID.ARBITRUM_SEPOLIA,
110
+ CHAIN_ID.OPTIMISM_SEPOLIA,
111
+ CHAIN_ID.POLYGON_AMOY,
112
+ CHAIN_ID.AVALANCHE_FUJI,
113
+ CHAIN_ID.BSC_TESTNET,
114
+ CHAIN_ID.SCROLL_SEPOLIA,
115
+ CHAIN_ID.LINEA_SEPOLIA,
116
+ CHAIN_ID.CELO_SEPOLIA,
117
+ CHAIN_ID.MANTLE_SEPOLIA,
118
+ CHAIN_ID.MONAD_TESTNET,
119
+ CHAIN_ID.ABSTRACT_TESTNET,
120
+ // Special
121
+ 31337,
122
+ ];
123
+
124
+ const OTHER_CHAIN_ID = -1;
125
+
18
126
  export function resolveChainConfig(chainName: string): ChainConfig {
19
127
  const config = CHAINS[chainName];
20
128
  if (!config) {
@@ -23,24 +131,73 @@ export function resolveChainConfig(chainName: string): ChainConfig {
23
131
  return config;
24
132
  }
25
133
 
26
- export async function selectChain(): Promise<string> {
27
- return select({
28
- message: "Select target chain:",
29
- choices: Object.keys(CHAINS).map((name) => ({ name, value: name })),
30
- });
134
+ // Resolve chain config by numeric chain ID, supporting BYO chains via rpcUrl.
135
+ // For known chain IDs the viem chain object is used directly.
136
+ // For unknown chain IDs, a minimal chain is constructed using defineChain (requires rpcUrl).
137
+ export function resolveChainConfigById(chainId: number, rpcUrl?: string): ChainConfig {
138
+ const chain = VIEM_CHAIN_BY_ID[chainId];
139
+ if (chain) {
140
+ return { chain, chainId };
141
+ }
142
+ if (!rpcUrl) {
143
+ throw new Error(`Unknown chain ID ${chainId}. Provide --rpc-url to register on a custom chain.`);
144
+ }
145
+ return {
146
+ chain: defineChain({
147
+ id: chainId,
148
+ name: `chain-${chainId}`,
149
+ nativeCurrency: { name: "Ether", symbol: "ETH", decimals: 18 },
150
+ rpcUrls: { default: { http: [rpcUrl] } },
151
+ }),
152
+ chainId,
153
+ };
154
+ }
155
+
156
+ // Prompt user to select a chain interactively, returning the numeric chain ID.
157
+ // Chains are sorted by popularity. "Other" allows entering any custom chain ID.
158
+ export async function selectChain(): Promise<number> {
159
+ const chainById = new Map(Object.values(CHAINS).map((c) => [c.chainId, c]));
160
+ const nameById = new Map(Object.entries(CHAINS).map(([name, c]) => [c.chainId, name]));
161
+
162
+ const choices = CHAIN_SELECTION_ORDER.filter((id) => chainById.has(id)).map((id) => ({
163
+ name: `${nameById.get(id) ?? `chain-${id}`} (${id})`,
164
+ value: id,
165
+ }));
166
+ choices.push({ name: "Other (enter chain ID)", value: OTHER_CHAIN_ID });
167
+
168
+ const selected = await select({ message: "Select target chain:", choices });
169
+
170
+ if (selected === OTHER_CHAIN_ID) {
171
+ const raw = await input({
172
+ message: "Enter chain ID:",
173
+ validate: (v) => {
174
+ const n = parseInt(v, 10);
175
+ return Number.isInteger(n) && n > 0 ? true : "Must be a positive integer";
176
+ },
177
+ });
178
+ return parseInt(raw, 10);
179
+ }
180
+
181
+ return selected;
31
182
  }
32
183
 
33
- export function resolveRegistryAddress(chainName: string, chainId: number, registry?: string): `0x${string}` {
184
+ // Returns the registry address if known, or null if no default exists for the chain (requires interactive prompt).
185
+ // Throws only for an explicitly invalid registry address.
186
+ export function resolveRegistryAddress(chainId: number, registry?: string): `0x${string}` | null {
34
187
  if (registry) {
35
188
  if (!isAddress(registry)) {
36
189
  throw new Error(`Invalid registry address: ${registry}`);
37
190
  }
38
191
  return registry as `0x${string}`;
39
192
  }
40
- if (chainName === "localhost") {
41
- throw new Error("--registry is required for localhost (no default contract deployment)");
193
+ if (chainId === 31337) {
194
+ return null;
195
+ }
196
+ try {
197
+ return getIdentityRegistryAddress(chainId) as `0x${string}`;
198
+ } catch {
199
+ return null;
42
200
  }
43
- return getIdentityRegistryAddress(chainId) as `0x${string}`;
44
201
  }
45
202
 
46
203
  export function validateBrowserRpcConflict(browser: boolean | undefined, rpcUrl: string | undefined): void {
@@ -1,23 +1,7 @@
1
1
  import { checkbox, confirm, input, select } from "@inquirer/prompts";
2
+ import { isAddress } from "viem";
2
3
  import type { RegistrationEntry } from "@aixyz/erc-8004/schemas/registration";
3
4
 
4
- export async function promptAgentId(): Promise<string> {
5
- return input({
6
- message: "Agent ID (token ID) to update:",
7
- validate: (value) => {
8
- const n = Number(value);
9
- if (value.trim() === "" || !Number.isInteger(n) || n < 0) return "Must be a non-negative integer";
10
- return true;
11
- },
12
- });
13
- }
14
-
15
- export async function promptUri(): Promise<string> {
16
- return input({
17
- message: "New agent metadata URI or path to .json file (leave empty to clear):",
18
- });
19
- }
20
-
21
5
  export async function promptAgentUrl(): Promise<string> {
22
6
  return input({
23
7
  message: "Agent deployment URL (e.g., https://my-agent.example.com):",
@@ -71,6 +55,14 @@ export async function promptSelectRegistration(registrations: RegistrationEntry[
71
55
  });
72
56
  }
73
57
 
58
+ export async function promptRegistryAddress(): Promise<`0x${string}`> {
59
+ const value = await input({
60
+ message: "IdentityRegistry contract address (no default for this chain):",
61
+ validate: (v) => (isAddress(v) ? true : "Must be a valid Ethereum address (0x…)"),
62
+ });
63
+ return value as `0x${string}`;
64
+ }
65
+
74
66
  export function deriveAgentUri(url: string): string {
75
67
  // Ensure no trailing slash before appending path
76
68
  const base = url.replace(/\/+$/, "");
@@ -1,81 +0,0 @@
1
- import { describe, expect, test, afterAll, beforeAll } from "bun:test";
2
- import { rmSync } from "fs";
3
- import { mkdir } from "node:fs/promises";
4
- import { resolveUri } from "./utils";
5
- import { join } from "path";
6
-
7
- describe("resolveUri", () => {
8
- const testDir = join(import.meta.dir, "__test_fixtures__");
9
- const testJsonPath = join(testDir, "test-metadata.json");
10
- const testMetadata = { name: "Test Agent", description: "A test agent" };
11
-
12
- beforeAll(() => {
13
- // ensure test directory exists
14
- mkdir(testDir, { recursive: true });
15
- });
16
-
17
- afterAll(() => {
18
- // clean up test directory
19
- rmSync(testDir, { recursive: true, force: true });
20
- });
21
-
22
- test("returns ipfs:// URIs unchanged", () => {
23
- const uri = "ipfs://QmTest123";
24
- expect(resolveUri(uri)).toStrictEqual(uri);
25
- });
26
-
27
- test("returns https:// URIs unchanged", () => {
28
- const uri = "https://example.com/metadata.json";
29
- expect(resolveUri(uri)).toStrictEqual(uri);
30
- });
31
-
32
- test("returns http:// URIs unchanged", () => {
33
- const uri = "http://example.com/metadata.json";
34
- expect(resolveUri(uri)).toStrictEqual(uri);
35
- });
36
-
37
- test("converts .json file to base64 data URI", async () => {
38
- await Bun.write(testJsonPath, JSON.stringify(testMetadata));
39
-
40
- try {
41
- const result = resolveUri(testJsonPath);
42
- expect(result.startsWith("data:application/json;base64,")).toStrictEqual(true);
43
-
44
- // Decode and verify content
45
- const base64 = result.replace("data:application/json;base64,", "");
46
- const decoded = JSON.parse(Buffer.from(base64, "base64").toString("utf-8"));
47
- expect(decoded).toStrictEqual(testMetadata);
48
- } finally {
49
- await Bun.file(testJsonPath).unlink();
50
- }
51
- });
52
-
53
- test("throws for directory path", async () => {
54
- await mkdir(testDir, { recursive: true });
55
- const dirWithJsonSuffix = join(testDir, "not-a-file.json");
56
- await mkdir(dirWithJsonSuffix, { recursive: true });
57
-
58
- try {
59
- expect(() => resolveUri(dirWithJsonSuffix)).toThrow(Error);
60
- expect(() => resolveUri(dirWithJsonSuffix)).toThrow("Not a file");
61
- } finally {
62
- rmSync(dirWithJsonSuffix, { recursive: true, force: true });
63
- }
64
- });
65
-
66
- test("throws for non-existent .json file", () => {
67
- expect(() => resolveUri("./non-existent.json")).toThrow(Error);
68
- expect(() => resolveUri("./non-existent.json")).toThrow("File not found");
69
- });
70
-
71
- test("throws for invalid JSON content", async () => {
72
- await Bun.write(testJsonPath, "not valid json {{{");
73
-
74
- try {
75
- expect(() => resolveUri(testJsonPath)).toThrow(Error);
76
- expect(() => resolveUri(testJsonPath)).toThrow("Invalid JSON");
77
- } finally {
78
- await Bun.file(testJsonPath).unlink();
79
- }
80
- });
81
- });
package/register/utils.ts DELETED
@@ -1,38 +0,0 @@
1
- import { existsSync, readFileSync, statSync } from "node:fs";
2
- import { resolve } from "node:path";
3
-
4
- export function resolveUri(uri: string): string {
5
- // Return as-is for URLs (ipfs://, https://, data:, etc.)
6
- if (uri.startsWith("http://") || uri.startsWith("https://") || uri.startsWith("ipfs://") || uri.startsWith("data:")) {
7
- return uri;
8
- }
9
-
10
- // Check if it's a file path (ends with .json or exists as a file)
11
- if (uri.endsWith(".json") || existsSync(uri)) {
12
- const filePath = resolve(uri);
13
-
14
- if (!existsSync(filePath)) {
15
- throw new Error(`File not found: ${filePath}`);
16
- }
17
-
18
- if (!statSync(filePath).isFile()) {
19
- throw new Error(`Not a file: ${filePath}`);
20
- }
21
-
22
- const content = readFileSync(filePath, "utf-8");
23
-
24
- // Validate it's valid JSON
25
- try {
26
- JSON.parse(content);
27
- } catch {
28
- throw new Error(`Invalid JSON in file: ${filePath}`);
29
- }
30
-
31
- // Convert to base64 data URI
32
- const base64 = Buffer.from(content).toString("base64");
33
- return `data:application/json;base64,${base64}`;
34
- }
35
-
36
- // Return as-is for other URIs
37
- return uri;
38
- }