@aixyz/cli 0.6.0 → 0.8.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/README.md CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  [![npm](https://img.shields.io/npm/v/aixyz-cli)](https://www.npmjs.com/package/aixyz-cli)
4
4
 
5
- CLI for building and deploying [aixyz](https://ai-xyz.dev) agents.
5
+ CLI for building and deploying [aixyz](https://aixyz.sh) agents.
6
6
 
7
7
  ## Quick Start
8
8
 
package/bin.ts CHANGED
@@ -2,6 +2,9 @@
2
2
  import { program } from "commander";
3
3
  import { build } from "./build";
4
4
  import { dev } from "./dev";
5
+ import { register } from "./register/register";
6
+ import { setAgentUri } from "./register/set-agent-uri";
7
+ import { CliError } from "./register/utils";
5
8
  import pkg from "./package.json";
6
9
 
7
10
  function handleAction(
@@ -11,6 +14,13 @@ function handleAction(
11
14
  try {
12
15
  await action(options);
13
16
  } catch (error) {
17
+ if (error instanceof CliError) {
18
+ console.error(`Error: ${error.message}`);
19
+ process.exit(1);
20
+ }
21
+ if (error instanceof Error && error.name === "ExitPromptError") {
22
+ process.exit(130);
23
+ }
14
24
  console.error(`Error: ${error instanceof Error ? error.message : String(error)}`);
15
25
  process.exit(1);
16
26
  }
@@ -60,4 +70,169 @@ Examples:
60
70
  )
61
71
  .action(handleAction(build));
62
72
 
73
+ const erc8004 = program.command("erc8004").description("ERC-8004 IdentityRegistry operations");
74
+
75
+ erc8004
76
+ .command("register")
77
+ .description("Register a new agent to the ERC-8004 IdentityRegistry")
78
+ .option("--uri <uri>", "Agent metadata URI or path to .json file (converts to base64 data URI)")
79
+ .option("--chain <chain>", "Target chain (mainnet, sepolia, base-sepolia, localhost)")
80
+ .option("--rpc-url <url>", "Custom RPC URL (uses default if not provided)")
81
+ .option("--registry <address>", "Contract address of the IdentityRegistry (required for localhost)")
82
+ .option("--keystore <path>", "Path to Ethereum keystore (V3) JSON file for local signing")
83
+ .option("--browser", "Use browser extension wallet (any extension)")
84
+ .option("--broadcast", "Sign and broadcast the transaction (default: dry-run)")
85
+ .option("--out-dir <path>", "Write deployment result as JSON to the given directory")
86
+ .addHelpText(
87
+ "after",
88
+ `
89
+ Option Details:
90
+ --uri <uri>
91
+ Agent metadata as a URI or local file path. Accepts http://, https://,
92
+ ipfs://, and data: URIs directly.
93
+ If a .json file path is given, it is read and converted to a base64 data URI automatically.
94
+ Otherwise, the URI is used as-is and the validity of the URI is not checked.
95
+ If omitted, the agent is registered without metadata.
96
+
97
+ --chain <chain>
98
+ Target chain for registration. Supported values:
99
+ mainnet Ethereum mainnet (chain ID 1)
100
+ sepolia Ethereum Sepolia testnet (chain ID 11155111)
101
+ base-sepolia Base Sepolia testnet (chain ID 84532)
102
+ localhost Local Foundry/Anvil node (chain ID 31337)
103
+ If omitted, you will be prompted to select a chain interactively.
104
+ Each chain has a default RPC endpoint unless overridden with --rpc-url.
105
+
106
+ --rpc-url <url>
107
+ Custom RPC endpoint URL. Overrides the default RPC for the selected
108
+ chain. Cannot be used with --browser since the browser wallet manages
109
+ its own RPC connection.
110
+
111
+ --registry <address>
112
+ Contract address of the ERC-8004 IdentityRegistry. Only required for
113
+ localhost, where there is no default deployment. For mainnet, sepolia,
114
+ and base-sepolia the canonical registry address is used automatically.
115
+
116
+ --keystore <path>
117
+ Path to an Ethereum keystore (V3) JSON file. You will be prompted for
118
+ the keystore password to decrypt the private key for signing.
119
+
120
+ --browser
121
+ Opens a local page in your default browser for signing with any
122
+ EIP-6963 compatible wallet extension (MetaMask, Rabby, etc.).
123
+ The wallet handles both signing and broadcasting the transaction.
124
+ Cannot be combined with --rpc-url.
125
+
126
+ --broadcast
127
+ Sign and broadcast the transaction on-chain. Without this flag the
128
+ command performs a dry-run: it encodes the transaction and prints
129
+ its details but does not interact with any wallet or send anything
130
+ to the network.
131
+
132
+ --out-dir <path>
133
+ Directory to write the deployment result as a JSON file. The file
134
+ is named registration-<chainId>-<timestamp>.json.
135
+
136
+ Environment Variables:
137
+ PRIVATE_KEY Private key (hex, with or without 0x prefix) used for
138
+ signing. Detected automatically if set. Not recommended
139
+ for interactive use as the key may appear in shell history.
140
+
141
+ Examples:
142
+ # Dry-run (default) — shows encoded transaction, no wallet needed
143
+ $ aixyz erc8004 register --uri "./metadata.json" --chain sepolia
144
+
145
+ # Sign and broadcast
146
+ $ aixyz erc8004 register --uri "./metadata.json" --chain sepolia --keystore ~/.foundry/keystores/default --broadcast
147
+ $ PRIVATE_KEY=0x... aixyz erc8004 register --chain sepolia --broadcast
148
+ $ aixyz erc8004 register --chain localhost --registry 0x5FbDB2315678afecb367f032d93F642f64180aa3 --uri "./metadata.json" --broadcast
149
+ $ aixyz erc8004 register --uri "./metadata.json" --chain sepolia --browser --broadcast`,
150
+ )
151
+ .action(handleAction(register));
152
+
153
+ erc8004
154
+ .command("set-agent-uri")
155
+ .description("Update the metadata URI of a registered agent")
156
+ .option("--agent-id <id>", "Agent ID (token ID) to update")
157
+ .option("--uri <uri>", "New agent metadata URI or path to .json file")
158
+ .option("--chain <chain>", "Target chain (mainnet, sepolia, base-sepolia, localhost)")
159
+ .option("--rpc-url <url>", "Custom RPC URL (uses default if not provided)")
160
+ .option("--registry <address>", "Contract address of the IdentityRegistry (required for localhost)")
161
+ .option("--keystore <path>", "Path to Ethereum keystore (V3) JSON file for local signing")
162
+ .option("--browser", "Use browser extension wallet (any extension)")
163
+ .option("--broadcast", "Sign and broadcast the transaction (default: dry-run)")
164
+ .option("--out-dir <path>", "Write result as JSON to the given directory")
165
+ .addHelpText(
166
+ "after",
167
+ `
168
+ Option Details:
169
+ --agent-id <id>
170
+ The token ID of the agent whose URI you want to update.
171
+ Must be a non-negative integer. Only the agent owner, an approved
172
+ address, or an operator can update the URI.
173
+ If omitted, you will be prompted to enter the agent ID interactively.
174
+
175
+ --uri <uri>
176
+ New agent metadata as a URI or local file path. Accepts http://, https://,
177
+ ipfs://, and data: URIs directly.
178
+ If a .json file path is given, it is read and converted to a base64 data URI automatically.
179
+ Otherwise, the URI is used as-is and the validity of the URI is not checked.
180
+ If omitted, you will be prompted to enter the URI interactively.
181
+
182
+ --chain <chain>
183
+ Target chain. Supported values:
184
+ mainnet Ethereum mainnet (chain ID 1)
185
+ sepolia Ethereum Sepolia testnet (chain ID 11155111)
186
+ base-sepolia Base Sepolia testnet (chain ID 84532)
187
+ localhost Local Foundry/Anvil node (chain ID 31337)
188
+ If omitted, you will be prompted to select a chain interactively.
189
+ Each chain has a default RPC endpoint unless overridden with --rpc-url.
190
+
191
+ --rpc-url <url>
192
+ Custom RPC endpoint URL. Overrides the default RPC for the selected
193
+ chain. Cannot be used with --browser since the browser wallet manages
194
+ its own RPC connection.
195
+
196
+ --registry <address>
197
+ Contract address of the ERC-8004 IdentityRegistry. Only required for
198
+ localhost, where there is no default deployment. For mainnet, sepolia,
199
+ and base-sepolia the canonical registry address is used automatically.
200
+
201
+ --keystore <path>
202
+ Path to an Ethereum keystore (V3) JSON file. You will be prompted for
203
+ the keystore password to decrypt the private key for signing.
204
+
205
+ --browser
206
+ Opens a local page in your default browser for signing with any
207
+ EIP-6963 compatible wallet extension (MetaMask, Rabby, etc.).
208
+ The wallet handles both signing and broadcasting the transaction.
209
+ Cannot be combined with --rpc-url.
210
+
211
+ --broadcast
212
+ Sign and broadcast the transaction on-chain. Without this flag the
213
+ command performs a dry-run: it encodes the transaction and prints
214
+ its details but does not interact with any wallet or send anything
215
+ to the network.
216
+
217
+ --out-dir <path>
218
+ Directory to write the result as a JSON file. The file
219
+ is named set-agent-uri-<chainId>-<timestamp>.json.
220
+
221
+ Environment Variables:
222
+ PRIVATE_KEY Private key (hex, with or without 0x prefix) used for
223
+ signing. Detected automatically if set. Not recommended
224
+ for interactive use as the key may appear in shell history.
225
+
226
+ Examples:
227
+ # Dry-run (default) — shows encoded transaction, no wallet needed
228
+ $ aixyz erc8004 set-agent-uri --agent-id 1 --uri "./metadata.json" --chain sepolia
229
+
230
+ # Sign and broadcast
231
+ $ aixyz erc8004 set-agent-uri --agent-id 1 --uri "./metadata.json" --chain sepolia --keystore ~/.foundry/keystores/default --broadcast
232
+ $ PRIVATE_KEY=0x... aixyz erc8004 set-agent-uri --agent-id 42 --uri "https://example.com/agent.json" --chain sepolia --broadcast
233
+ $ aixyz erc8004 set-agent-uri --agent-id 1 --uri "./metadata.json" --chain localhost --registry 0x5FbDB2315678afecb367f032d93F642f64180aa3 --broadcast
234
+ $ aixyz erc8004 set-agent-uri --agent-id 1 --uri "./metadata.json" --chain sepolia --browser --broadcast`,
235
+ )
236
+ .action(handleAction(setAgentUri));
237
+
63
238
  program.parse();
@@ -37,7 +37,7 @@ export function AixyzConfigPlugin(): BunPlugin {
37
37
  return {
38
38
  name: "aixyz-config",
39
39
  setup(build) {
40
- build.onLoad({ filter: /packages\/aixyz\/config\.ts$/ }, () => ({
40
+ build.onLoad({ filter: /aixyz\/config\.ts$/ }, () => ({
41
41
  contents: `
42
42
  const config = ${JSON.stringify(materialized)};
43
43
  export function getAixyzConfig() {
@@ -1,6 +1,7 @@
1
1
  import type { BunPlugin } from "bun";
2
2
  import { existsSync, mkdirSync, readdirSync, writeFileSync } from "fs";
3
3
  import { resolve, relative, basename, join } from "path";
4
+ import { getAixyzConfig } from "@aixyz/config";
4
5
 
5
6
  export function AixyzServerPlugin(entrypoint: string, mode: "vercel" | "standalone"): BunPlugin {
6
7
  return {
@@ -51,6 +52,17 @@ export function getEntrypointMayGenerate(cwd: string, mode: "dev" | "build"): st
51
52
  return entrypoint;
52
53
  }
53
54
 
55
+ class AixyzGlob {
56
+ constructor(readonly config = getAixyzConfig()) {}
57
+
58
+ includes(file: string): boolean {
59
+ const included = this.config.build.includes.some((pattern) => new Bun.Glob(pattern).match(file));
60
+ if (!included) return false;
61
+ const excluded = this.config.build.excludes.some((pattern) => new Bun.Glob(pattern).match(file));
62
+ return !excluded;
63
+ }
64
+ }
65
+
54
66
  /**
55
67
  * Generate server.ts content by scanning the app directory for agent.ts and tools/.
56
68
  *
@@ -58,6 +70,7 @@ export function getEntrypointMayGenerate(cwd: string, mode: "dev" | "build"): st
58
70
  * @param entrypointDir - Directory where the generated file will live (for computing relative imports).
59
71
  */
60
72
  function generateServer(appDir: string, entrypointDir: string): string {
73
+ const glob = new AixyzGlob();
61
74
  const rel = relative(entrypointDir, appDir);
62
75
  const importPrefix = rel === "" ? "." : rel.startsWith(".") ? rel : `./${rel}`;
63
76
 
@@ -73,22 +86,20 @@ function generateServer(appDir: string, entrypointDir: string): string {
73
86
  imports.push('import { facilitator } from "aixyz/accepts";');
74
87
  }
75
88
 
76
- const hasAgent = existsSync(resolve(appDir, "agent.ts"));
89
+ const hasAgent = existsSync(resolve(appDir, "agent.ts")) && glob.includes("agent.ts");
77
90
  if (hasAgent) {
78
91
  imports.push('import { useA2A } from "aixyz/server/adapters/a2a";');
79
92
  imports.push(`import * as agent from "${importPrefix}/agent";`);
80
93
  }
81
94
 
82
95
  const toolsDir = resolve(appDir, "tools");
83
- const tools: { name: string }[] = [];
96
+ const tools: { name: string; identifier: string }[] = [];
84
97
  if (existsSync(toolsDir)) {
85
98
  for (const file of readdirSync(toolsDir)) {
86
- if (file.endsWith(".ts")) {
99
+ if (glob.includes(`tools/${file}`)) {
87
100
  const name = basename(file, ".ts");
88
- // Skip tools starting with underscore
89
- if (!name.startsWith("_")) {
90
- tools.push({ name });
91
- }
101
+ const identifier = toIdentifier(name);
102
+ tools.push({ name, identifier });
92
103
  }
93
104
  }
94
105
  }
@@ -96,7 +107,7 @@ function generateServer(appDir: string, entrypointDir: string): string {
96
107
  if (tools.length > 0) {
97
108
  imports.push('import { AixyzMCP } from "aixyz/server/adapters/mcp";');
98
109
  for (const tool of tools) {
99
- imports.push(`import * as ${tool.name} from "${importPrefix}/tools/${tool.name}";`);
110
+ imports.push(`import * as ${tool.identifier} from "${importPrefix}/tools/${tool.name}";`);
100
111
  }
101
112
  }
102
113
 
@@ -111,7 +122,7 @@ function generateServer(appDir: string, entrypointDir: string): string {
111
122
  if (tools.length > 0) {
112
123
  body.push("const mcp = new AixyzMCP(server);");
113
124
  for (const tool of tools) {
114
- body.push(`await mcp.register("${tool.name}", ${tool.name});`);
125
+ body.push(`await mcp.register("${tool.name}", ${tool.identifier});`);
115
126
  }
116
127
  body.push("await mcp.connect();");
117
128
  }
@@ -120,3 +131,16 @@ function generateServer(appDir: string, entrypointDir: string): string {
120
131
 
121
132
  return [...imports, "", ...body].join("\n");
122
133
  }
134
+
135
+ /**
136
+ * Convert a kebab-case filename into a valid JS identifier.
137
+ *
138
+ * Examples:
139
+ * "lookup" → "lookup"
140
+ * "get-aggregator-v3-address" → "getAggregatorV3Address"
141
+ * "3d-model" → "_3dModel"
142
+ */
143
+ function toIdentifier(name: string): string {
144
+ const camel = name.replace(/-(.)/g, (_, c: string) => c.toUpperCase()).replace(/[^a-zA-Z0-9_$]/g, "_");
145
+ return /^\d/.test(camel) ? `_${camel}` : camel;
146
+ }
package/build/icons.ts ADDED
@@ -0,0 +1,64 @@
1
+ import sharp from "sharp";
2
+ import { cpSync, existsSync, mkdirSync, writeFileSync } from "fs";
3
+ import { dirname, resolve } from "path";
4
+
5
+ /** Icon source file extensions to look for, in priority order */
6
+ const ICON_EXTENSIONS = ["svg", "png", "jpeg", "jpg"] as const;
7
+
8
+ /** Returns the first matching icon file path, or null if none found. */
9
+ export function findIconFile(appDir: string): string | null {
10
+ for (const ext of ICON_EXTENSIONS) {
11
+ const iconPath = resolve(appDir, `icon.${ext}`);
12
+ if (existsSync(iconPath)) return iconPath;
13
+ }
14
+ return null;
15
+ }
16
+
17
+ /**
18
+ * Copy the icon to destPath as icon.png.
19
+ * PNG sources are copied directly; other formats are converted via sharp.
20
+ */
21
+ export async function copyAgentIcon(iconPath: string, destPath: string): Promise<void> {
22
+ if (iconPath.endsWith(".png")) {
23
+ cpSync(iconPath, destPath);
24
+ } else {
25
+ await sharp(iconPath).png().toFile(destPath);
26
+ }
27
+ }
28
+
29
+ /**
30
+ * Generate a favicon.ico at destPath from the given icon source.
31
+ * Uses sharp to produce a 32×32 PNG buffer, then wraps it in an ICO container.
32
+ * Modern browsers support ICO files with embedded PNG data.
33
+ */
34
+ export async function generateFavicon(iconPath: string, destPath: string): Promise<void> {
35
+ const pngData = await sharp(iconPath)
36
+ .resize(32, 32, { fit: "contain", background: { r: 0, g: 0, b: 0, alpha: 0 } })
37
+ .png()
38
+ .toBuffer();
39
+
40
+ mkdirSync(dirname(destPath), { recursive: true });
41
+ writeFileSync(destPath, buildIco(pngData, 32, 32));
42
+ }
43
+
44
+ /** Build a single-image ICO buffer with embedded PNG data. */
45
+ function buildIco(pngData: Buffer, width: number, height: number): Buffer {
46
+ // ICONDIR header (6 bytes)
47
+ const header = Buffer.alloc(6);
48
+ header.writeUInt16LE(0, 0); // reserved
49
+ header.writeUInt16LE(1, 2); // type: 1 = ICO
50
+ header.writeUInt16LE(1, 4); // image count
51
+
52
+ // ICONDIRENTRY (16 bytes); image data starts at offset 6 + 16 = 22
53
+ const entry = Buffer.alloc(16);
54
+ entry.writeUInt8(width === 256 ? 0 : width, 0); // width (0 means 256)
55
+ entry.writeUInt8(height === 256 ? 0 : height, 1); // height (0 means 256)
56
+ entry.writeUInt8(0, 2); // color count (0 = true color)
57
+ entry.writeUInt8(0, 3); // reserved
58
+ entry.writeUInt16LE(1, 4); // planes
59
+ entry.writeUInt16LE(32, 6); // bits per pixel
60
+ entry.writeUInt32LE(pngData.length, 8); // size of image data
61
+ entry.writeUInt32LE(22, 12); // offset to image data
62
+
63
+ return Buffer.concat([header, entry, pngData]);
64
+ }
package/build/index.ts CHANGED
@@ -2,6 +2,7 @@ import { resolve } from "path";
2
2
  import { existsSync, mkdirSync, cpSync, rmSync } from "fs";
3
3
  import { AixyzConfigPlugin } from "./AixyzConfigPlugin";
4
4
  import { AixyzServerPlugin, getEntrypointMayGenerate } from "./AixyzServerPlugin";
5
+ import { findIconFile, copyAgentIcon, generateFavicon } from "./icons";
5
6
  import { getAixyzConfig } from "@aixyz/config";
6
7
  import { loadEnvConfig } from "@next/env";
7
8
  import chalk from "chalk";
@@ -65,9 +66,10 @@ async function buildBun(entrypoint: string): Promise<void> {
65
66
  console.log("Copied public/ →", destPublicDir);
66
67
  }
67
68
 
68
- const iconFile = resolve(cwd, "app/icon.png");
69
- if (existsSync(iconFile)) {
70
- cpSync(iconFile, resolve(outputDir, "icon.png"));
69
+ const iconFile = findIconFile(resolve(cwd, "app"));
70
+ if (iconFile) {
71
+ await copyAgentIcon(iconFile, resolve(outputDir, "icon.png"));
72
+ await generateFavicon(iconFile, resolve(outputDir, "public/favicon.ico"));
71
73
  }
72
74
 
73
75
  // Log summary
@@ -75,7 +77,7 @@ async function buildBun(entrypoint: string): Promise<void> {
75
77
  console.log("Build complete! Output:");
76
78
  console.log(" .aixyz/output/server.js");
77
79
  console.log(" .aixyz/output/package.json");
78
- if (existsSync(publicDir) || existsSync(iconFile)) {
80
+ if (existsSync(publicDir) || iconFile) {
79
81
  console.log(" .aixyz/output/public/ and assets");
80
82
  }
81
83
  console.log("");
@@ -151,11 +153,12 @@ async function buildVercel(entrypoint: string): Promise<void> {
151
153
  console.log("Copied public/ →", staticDir);
152
154
  }
153
155
 
154
- const iconFile = resolve(cwd, "app/icon.png");
155
- if (existsSync(iconFile)) {
156
+ const iconFile = findIconFile(resolve(cwd, "app"));
157
+ if (iconFile) {
156
158
  mkdirSync(staticDir, { recursive: true });
157
- cpSync(iconFile, resolve(staticDir, "icon.png"));
158
- console.log("Copied app/icon.png ", staticDir);
159
+ await copyAgentIcon(iconFile, resolve(staticDir, "icon.png"));
160
+ await generateFavicon(iconFile, resolve(staticDir, "favicon.ico"));
161
+ console.log("Copied app/icon →", staticDir);
159
162
  }
160
163
 
161
164
  // Log summary
package/dev/index.ts CHANGED
@@ -15,7 +15,7 @@ export async function dev(options: { port?: string }): Promise<void> {
15
15
  const baseUrl = `http://localhost:${port}`;
16
16
 
17
17
  console.log("");
18
- console.log(`⟡ ai-xyz.dev v${pkg.version}`);
18
+ console.log(`⟡ aixyz.sh v${pkg.version}`);
19
19
  console.log("");
20
20
  console.log(`- A2A: ${baseUrl}/.well-known/agent-card.json`);
21
21
  console.log(`- MCP: ${baseUrl}/mcp`);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aixyz/cli",
3
- "version": "0.6.0",
3
+ "version": "0.8.0",
4
4
  "description": "Payment-native SDK for AI Agent",
5
5
  "keywords": [
6
6
  "ai",
@@ -8,7 +8,7 @@
8
8
  "agent",
9
9
  "aixyz"
10
10
  ],
11
- "homepage": "https://ai-xyz.dev",
11
+ "homepage": "https://aixyz.sh",
12
12
  "bugs": "https://github.com/AgentlyHQ/aixyz/issues",
13
13
  "repository": {
14
14
  "type": "git",
@@ -23,14 +23,20 @@
23
23
  "files": [
24
24
  "build",
25
25
  "dev",
26
+ "register",
26
27
  "bin.ts"
27
28
  ],
28
29
  "dependencies": {
29
- "@aixyz/config": "0.6.0",
30
+ "@aixyz/config": "0.8.0",
31
+ "@aixyz/erc-8004": "0.8.0",
32
+ "@inquirer/prompts": "^8.3.0",
30
33
  "@next/env": "^16.1.6",
31
- "boxen": "^8.0.0",
32
- "chalk": "^5.0.0",
33
- "commander": "^14.0.3"
34
+ "boxen": "^8.0.1",
35
+ "chalk": "^5.6.2",
36
+ "commander": "^14.0.3",
37
+ "ethers": "^6.16.0",
38
+ "sharp": "^0.34.5",
39
+ "viem": "^2.46.3"
34
40
  },
35
41
  "engines": {
36
42
  "bun": ">=1.3.0"
@@ -0,0 +1,101 @@
1
+ # ERC-8004 Registry Commands
2
+
3
+ CLI commands for registering agents to the [ERC-8004](https://eips.ethereum.org/EIPS/eip-8004) IdentityRegistry.
4
+
5
+ These commands are part of the `aixyz` CLI under the `erc8004` subcommand.
6
+
7
+ ## Usage
8
+
9
+ ### Register an Agent
10
+
11
+ Register a new agent to the IdentityRegistry with multiple wallet options:
12
+
13
+ #### Using Keystore (Recommended)
14
+
15
+ Sign with an Ethereum keystore (V3) JSON file:
16
+
17
+ ```bash
18
+ aixyz erc8004 register --uri "./metadata.json" --chain sepolia --keystore ~/.foundry/keystores/default --broadcast
19
+ ```
20
+
21
+ #### Using Browser Wallet
22
+
23
+ Opens a localhost page to sign with any browser extension wallet (MetaMask, Rabby, etc.) that are `EIP-6963` compliant:
24
+
25
+ ```bash
26
+ aixyz erc8004 register --uri "ipfs://Qm..." --chain sepolia --browser --broadcast
27
+ ```
28
+
29
+ > **Note:** `--rpc-url` cannot be used with `--browser`. The browser wallet uses its own RPC endpoint.
30
+
31
+ #### Using Private Key Env (Not Recommended)
32
+
33
+ For scripting and CI:
34
+
35
+ ```bash
36
+ # Not recommended for interactive use
37
+ PRIVATE_KEY=0x... aixyz erc8004 register --uri "ipfs://Qm..." --chain sepolia --broadcast
38
+ ```
39
+
40
+ #### Interactive Mode
41
+
42
+ If no wallet option is provided, you'll be prompted to choose:
43
+
44
+ ```bash
45
+ aixyz erc8004 register --uri "ipfs://Qm..." --chain sepolia --broadcast
46
+ ```
47
+
48
+ #### Local Development
49
+
50
+ Register against a local Foundry/Anvil node:
51
+
52
+ ```bash
53
+ aixyz erc8004 register \
54
+ --chain localhost \
55
+ --registry 0x5FbDB2315678afecb367f032d93F642f64180aa3 \
56
+ --rpc-url http://localhost:8545 \
57
+ --uri "./metadata.json" \
58
+ --keystore ~/.foundry/keystores/default \
59
+ --broadcast
60
+ ```
61
+
62
+ ### Set Agent URI
63
+
64
+ Update the metadata URI of a registered agent:
65
+
66
+ ```bash
67
+ aixyz erc8004 set-agent-uri \
68
+ --agent-id 1 \
69
+ --uri "https://my-agent.vercel.app/.well-known/agent-card.json" \
70
+ --chain sepolia \
71
+ --keystore ~/.foundry/keystores/default \
72
+ --broadcast
73
+ ```
74
+
75
+ ### Options
76
+
77
+ | Option | Description |
78
+ | ---------------------- | ------------------------------------------------------------------------------------ |
79
+ | `--uri <uri>` | Agent metadata URI or path to `.json` file (converts to base64 data URI) |
80
+ | `--chain <chain>` | Target chain: `mainnet`, `sepolia`, `base-sepolia`, `localhost` (default: `sepolia`) |
81
+ | `--rpc-url <url>` | Custom RPC URL (cannot be used with `--browser`) |
82
+ | `--registry <address>` | IdentityRegistry contract address (required for `localhost`) |
83
+ | `--keystore <path>` | Path to Ethereum keystore (V3) JSON file |
84
+ | `--browser` | Use browser extension wallet |
85
+ | `--broadcast` | Sign and broadcast the transaction (default: dry-run) |
86
+ | `--out-dir <path>` | Write deployment result as JSON to the given directory |
87
+
88
+ ### Environment Variables
89
+
90
+ | Variable | Description |
91
+ | ------------- | ------------------------------------- |
92
+ | `PRIVATE_KEY` | Private key for signing (use caution) |
93
+
94
+ ### Supported Chains
95
+
96
+ | Chain | Chain ID | Network |
97
+ | -------------- | -------- | ------------------------ |
98
+ | `mainnet` | 1 | Ethereum mainnet |
99
+ | `sepolia` | 11155111 | Ethereum Sepolia testnet |
100
+ | `base-sepolia` | 84532 | Base Sepolia testnet |
101
+ | `localhost` | 31337 | Local Foundry/Anvil node |
@@ -0,0 +1,8 @@
1
+ import type { WalletOptions } from "./wallet";
2
+
3
+ export interface BaseOptions extends WalletOptions {
4
+ chain?: string;
5
+ rpcUrl?: string;
6
+ registry?: string;
7
+ outDir?: string;
8
+ }
@@ -0,0 +1,75 @@
1
+ import { describe, expect, test } from "bun:test";
2
+ import { CHAIN_ID, getIdentityRegistryAddress } from "@aixyz/erc-8004";
3
+ import { register } from "./register";
4
+
5
+ describe("register command chain configuration", () => {
6
+ test("sepolia chain ID is correct", () => {
7
+ expect(CHAIN_ID.SEPOLIA).toStrictEqual(11155111);
8
+ });
9
+
10
+ test("base-sepolia chain ID is correct", () => {
11
+ expect(CHAIN_ID.BASE_SEPOLIA).toStrictEqual(84532);
12
+ });
13
+
14
+ test("identity registry address is returned for sepolia", () => {
15
+ const address = getIdentityRegistryAddress(CHAIN_ID.SEPOLIA);
16
+ expect(address).toMatch(/^0x[a-fA-F0-9]{40}$/);
17
+ });
18
+
19
+ test("identity registry address is returned for base-sepolia", () => {
20
+ const address = getIdentityRegistryAddress(CHAIN_ID.BASE_SEPOLIA);
21
+ expect(address).toMatch(/^0x[a-fA-F0-9]{40}$/);
22
+ });
23
+
24
+ test("throws for unsupported chain ID", () => {
25
+ expect(() => getIdentityRegistryAddress(999999)).toThrow("Unsupported chain ID");
26
+ });
27
+ });
28
+
29
+ describe("register command validation", () => {
30
+ test("supported chains list includes sepolia", () => {
31
+ const CHAINS: Record<string, { chainId: number }> = {
32
+ sepolia: { chainId: CHAIN_ID.SEPOLIA },
33
+ "base-sepolia": { chainId: CHAIN_ID.BASE_SEPOLIA },
34
+ };
35
+ expect(CHAINS["sepolia"]).toBeDefined();
36
+ expect(CHAINS["sepolia"].chainId).toStrictEqual(CHAIN_ID.SEPOLIA);
37
+ });
38
+
39
+ test("supported chains list includes base-sepolia", () => {
40
+ const CHAINS: Record<string, { chainId: number }> = {
41
+ sepolia: { chainId: CHAIN_ID.SEPOLIA },
42
+ "base-sepolia": { chainId: CHAIN_ID.BASE_SEPOLIA },
43
+ localhost: { chainId: 31337 },
44
+ };
45
+ expect(CHAINS["base-sepolia"]).toBeDefined();
46
+ expect(CHAINS["base-sepolia"].chainId).toStrictEqual(CHAIN_ID.BASE_SEPOLIA);
47
+ });
48
+
49
+ test("supported chains list includes localhost with chainId 31337", () => {
50
+ const CHAINS: Record<string, { chainId: number }> = {
51
+ sepolia: { chainId: CHAIN_ID.SEPOLIA },
52
+ "base-sepolia": { chainId: CHAIN_ID.BASE_SEPOLIA },
53
+ localhost: { chainId: 31337 },
54
+ };
55
+ expect(CHAINS["localhost"]).toBeDefined();
56
+ expect(CHAINS["localhost"].chainId).toStrictEqual(31337);
57
+ });
58
+
59
+ test("unsupported chain is not in list", () => {
60
+ const CHAINS: Record<string, { chainId: number }> = {
61
+ sepolia: { chainId: CHAIN_ID.SEPOLIA },
62
+ "base-sepolia": { chainId: CHAIN_ID.BASE_SEPOLIA },
63
+ localhost: { chainId: 31337 },
64
+ };
65
+ expect(CHAINS["mainnet"]).toBeUndefined();
66
+ });
67
+
68
+ test("localhost requires --registry flag", async () => {
69
+ await expect(register({ chain: "localhost" })).rejects.toThrow("--registry is required for localhost");
70
+ });
71
+
72
+ test("dry-run completes without wallet interaction when --broadcast is not set", async () => {
73
+ await expect(register({ chain: "sepolia", uri: "https://example.com/agent.json" })).resolves.toBeUndefined();
74
+ });
75
+ });