@aixyz/cli 0.7.0 → 0.9.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,8 @@
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 { update } from "./register/update";
5
7
  import pkg from "./package.json";
6
8
 
7
9
  function handleAction(
@@ -11,6 +13,9 @@ function handleAction(
11
13
  try {
12
14
  await action(options);
13
15
  } catch (error) {
16
+ if (error instanceof Error && error.name === "ExitPromptError") {
17
+ process.exit(130);
18
+ }
14
19
  console.error(`Error: ${error instanceof Error ? error.message : String(error)}`);
15
20
  process.exit(1);
16
21
  }
@@ -60,4 +65,136 @@ Examples:
60
65
  )
61
66
  .action(handleAction(build));
62
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
+
63
200
  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: /aixyz\/config\.ts$/ }, () => ({
40
+ build.onLoad({ filter: /aixyz[-/]config\/index\.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,12 +122,35 @@ 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
  }
118
129
 
130
+ // If app/erc-8004.ts exists, auto-register ERC-8004 endpoint
131
+ const hasErc8004 = existsSync(resolve(appDir, "erc-8004.ts"));
132
+ if (hasErc8004) {
133
+ imports.push('import { useERC8004 } from "aixyz/server/adapters/erc-8004";');
134
+ imports.push(`import * as erc8004 from "${importPrefix}/erc-8004";`);
135
+ body.push(
136
+ `useERC8004(server, { default: erc8004.default, options: { mcp: ${tools.length > 0}, a2a: ${hasAgent} } });`,
137
+ );
138
+ }
139
+
119
140
  body.push("export default server;");
120
141
 
121
142
  return [...imports, "", ...body].join("\n");
122
143
  }
144
+
145
+ /**
146
+ * Convert a kebab-case filename into a valid JS identifier.
147
+ *
148
+ * Examples:
149
+ * "lookup" → "lookup"
150
+ * "get-aggregator-v3-address" → "getAggregatorV3Address"
151
+ * "3d-model" → "_3dModel"
152
+ */
153
+ function toIdentifier(name: string): string {
154
+ const camel = name.replace(/-(.)/g, (_, c: string) => c.toUpperCase()).replace(/[^a-zA-Z0-9_$]/g, "_");
155
+ return /^\d/.test(camel) ? `_${camel}` : camel;
156
+ }
package/build/index.ts CHANGED
@@ -77,7 +77,7 @@ async function buildBun(entrypoint: string): Promise<void> {
77
77
  console.log("Build complete! Output:");
78
78
  console.log(" .aixyz/output/server.js");
79
79
  console.log(" .aixyz/output/package.json");
80
- if (existsSync(publicDir) || existsSync(iconFile)) {
80
+ if (existsSync(publicDir) || iconFile) {
81
81
  console.log(" .aixyz/output/public/ and assets");
82
82
  }
83
83
  console.log("");
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.7.0",
3
+ "version": "0.9.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,15 +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.7.0",
30
+ "@aixyz/config": "0.9.0",
31
+ "@aixyz/erc-8004": "0.9.0",
32
+ "@inquirer/prompts": "^8.3.0",
30
33
  "@next/env": "^16.1.6",
31
- "boxen": "^8.0.0",
32
- "chalk": "^5.0.0",
34
+ "boxen": "^8.0.1",
35
+ "chalk": "^5.6.2",
33
36
  "commander": "^14.0.3",
34
- "sharp": "^0.34.5"
37
+ "ethers": "^6.16.0",
38
+ "sharp": "^0.34.5",
39
+ "viem": "^2.46.3"
35
40
  },
36
41
  "engines": {
37
42
  "bun": ">=1.3.0"
@@ -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,66 @@
1
+ import { describe, expect, test } from "bun:test";
2
+ import { CHAIN_ID, getIdentityRegistryAddress } from "@aixyz/erc-8004";
3
+
4
+ describe("register command chain configuration", () => {
5
+ test("sepolia chain ID is correct", () => {
6
+ expect(CHAIN_ID.SEPOLIA).toStrictEqual(11155111);
7
+ });
8
+
9
+ test("base-sepolia chain ID is correct", () => {
10
+ expect(CHAIN_ID.BASE_SEPOLIA).toStrictEqual(84532);
11
+ });
12
+
13
+ test("identity registry address is returned for sepolia", () => {
14
+ const address = getIdentityRegistryAddress(CHAIN_ID.SEPOLIA);
15
+ expect(address).toMatch(/^0x[a-fA-F0-9]{40}$/);
16
+ });
17
+
18
+ test("identity registry address is returned for base-sepolia", () => {
19
+ const address = getIdentityRegistryAddress(CHAIN_ID.BASE_SEPOLIA);
20
+ expect(address).toMatch(/^0x[a-fA-F0-9]{40}$/);
21
+ });
22
+
23
+ test("throws for unsupported chain ID", () => {
24
+ expect(() => getIdentityRegistryAddress(999999)).toThrow("Unsupported chain ID");
25
+ });
26
+ });
27
+
28
+ describe("register command validation", () => {
29
+ test("supported chains list includes sepolia", () => {
30
+ const CHAINS: Record<string, { chainId: number }> = {
31
+ sepolia: { chainId: CHAIN_ID.SEPOLIA },
32
+ "base-sepolia": { chainId: CHAIN_ID.BASE_SEPOLIA },
33
+ };
34
+ expect(CHAINS["sepolia"]).toBeDefined();
35
+ expect(CHAINS["sepolia"].chainId).toStrictEqual(CHAIN_ID.SEPOLIA);
36
+ });
37
+
38
+ test("supported chains list includes base-sepolia", () => {
39
+ const CHAINS: Record<string, { chainId: number }> = {
40
+ sepolia: { chainId: CHAIN_ID.SEPOLIA },
41
+ "base-sepolia": { chainId: CHAIN_ID.BASE_SEPOLIA },
42
+ localhost: { chainId: 31337 },
43
+ };
44
+ expect(CHAINS["base-sepolia"]).toBeDefined();
45
+ expect(CHAINS["base-sepolia"].chainId).toStrictEqual(CHAIN_ID.BASE_SEPOLIA);
46
+ });
47
+
48
+ test("supported chains list includes localhost with chainId 31337", () => {
49
+ const CHAINS: Record<string, { chainId: number }> = {
50
+ sepolia: { chainId: CHAIN_ID.SEPOLIA },
51
+ "base-sepolia": { chainId: CHAIN_ID.BASE_SEPOLIA },
52
+ localhost: { chainId: 31337 },
53
+ };
54
+ expect(CHAINS["localhost"]).toBeDefined();
55
+ expect(CHAINS["localhost"].chainId).toStrictEqual(31337);
56
+ });
57
+
58
+ test("unsupported chain is not in list", () => {
59
+ const CHAINS: Record<string, { chainId: number }> = {
60
+ sepolia: { chainId: CHAIN_ID.SEPOLIA },
61
+ "base-sepolia": { chainId: CHAIN_ID.BASE_SEPOLIA },
62
+ localhost: { chainId: 31337 },
63
+ };
64
+ expect(CHAINS["mainnet"]).toBeUndefined();
65
+ });
66
+ });
@@ -0,0 +1,204 @@
1
+ import { encodeFunctionData, formatEther, parseEventLogs, type Chain, type Log } from "viem";
2
+ import { IdentityRegistryAbi } from "@aixyz/erc-8004";
3
+ import { selectWalletMethod, type WalletOptions } from "./wallet";
4
+ import { signTransaction } from "./wallet/sign";
5
+ import {
6
+ resolveChainConfig,
7
+ selectChain,
8
+ resolveRegistryAddress,
9
+ validateBrowserRpcConflict,
10
+ getExplorerUrl,
11
+ } from "./utils/chain";
12
+ import { writeResultJson } from "./utils/result";
13
+ import { label, truncateUri, broadcastAndConfirm, logSignResult } from "./utils/transaction";
14
+ import { promptAgentUrl, promptSupportedTrust, deriveAgentUri } from "./utils/prompt";
15
+ import { hasErc8004File, createErc8004File, writeRegistrationEntry } from "./utils/erc8004-file";
16
+ import { confirm } from "@inquirer/prompts";
17
+ import chalk from "chalk";
18
+ import boxen from "boxen";
19
+ import type { BaseOptions } from "./index";
20
+
21
+ export interface RegisterOptions extends BaseOptions {
22
+ url?: string;
23
+ chain?: string;
24
+ }
25
+
26
+ export async function register(options: RegisterOptions): Promise<void> {
27
+ // Step 1: Ensure app/erc-8004.ts exists
28
+ if (!hasErc8004File()) {
29
+ console.log(chalk.yellow("No app/erc-8004.ts found. Let's create one."));
30
+ console.log("");
31
+ const supportedTrust = await promptSupportedTrust();
32
+ createErc8004File(supportedTrust);
33
+ console.log(chalk.green("Created app/erc-8004.ts"));
34
+ console.log("");
35
+ }
36
+
37
+ // Step 2: Get agent URL and derive URI
38
+ const agentUrl = options.url ?? (await promptAgentUrl());
39
+ const resolvedUri = deriveAgentUri(agentUrl);
40
+
41
+ const yes = await confirm({
42
+ message: `Will register URI as: ${chalk.cyan(resolvedUri)} — confirm?`,
43
+ default: true,
44
+ });
45
+ if (!yes) {
46
+ throw new Error("Aborted.");
47
+ }
48
+
49
+ // 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);
53
+
54
+ // Step 4: Encode transaction
55
+ const data = encodeFunctionData({
56
+ abi: IdentityRegistryAbi,
57
+ functionName: "register",
58
+ args: [resolvedUri],
59
+ });
60
+
61
+ const printTxDetails = (header: string) => {
62
+ console.log("");
63
+ console.log(chalk.dim(header));
64
+ console.log(` ${label("To")}${registryAddress}`);
65
+ console.log(` ${label("Data")}${data.slice(0, 10)}${chalk.dim("\u2026" + (data.length - 2) / 2 + " bytes")}`);
66
+ console.log(` ${label("Chain")}${chainName}`);
67
+ console.log(` ${label("Function")}register(string memory agentURI)`);
68
+ console.log(` ${label("URI")}${truncateUri(resolvedUri)}`);
69
+ console.log("");
70
+ };
71
+
72
+ validateBrowserRpcConflict(options.browser, options.rpcUrl);
73
+
74
+ if (!options.broadcast) {
75
+ if (options.browser || options.keystore || process.env.PRIVATE_KEY) {
76
+ console.warn("Note: --browser/--keystore/PRIVATE_KEY ignored in dry-run mode. Pass --broadcast to use a wallet.");
77
+ }
78
+ printTxDetails("Transaction details (dry-run)");
79
+ console.log("Dry-run complete. To sign and broadcast, re-run with --broadcast.");
80
+ return;
81
+ }
82
+
83
+ const walletMethod = await selectWalletMethod(options);
84
+ validateBrowserRpcConflict(walletMethod.type === "browser" || undefined, options.rpcUrl);
85
+
86
+ printTxDetails("Signing transaction...");
87
+
88
+ const result = await signTransaction({
89
+ walletMethod,
90
+ tx: { to: registryAddress, data },
91
+ chain: chainConfig.chain,
92
+ rpcUrl: options.rpcUrl,
93
+ options: {
94
+ browser: { chainId: chainConfig.chainId, chainName, uri: resolvedUri, mode: "register" },
95
+ },
96
+ });
97
+ logSignResult(walletMethod.type, result);
98
+
99
+ const { hash, receipt, timestamp } = await broadcastAndConfirm({
100
+ result,
101
+ chain: chainConfig.chain,
102
+ rpcUrl: options.rpcUrl,
103
+ });
104
+
105
+ const resultData = printResult(receipt, timestamp, chainConfig.chain, chainConfig.chainId, hash);
106
+
107
+ // Step 5: Write registration entry back to app/erc-8004.ts
108
+ if (resultData.agentId !== undefined) {
109
+ const agentRegistry = `eip155:${chainConfig.chainId}:${registryAddress}`;
110
+ writeRegistrationEntry({ agentId: Number(resultData.agentId), agentRegistry });
111
+ console.log("");
112
+ console.log(chalk.green(`Updated app/erc-8004.ts with registration (agentId: ${resultData.agentId})`));
113
+ }
114
+
115
+ if (options.outDir) {
116
+ writeResultJson(options.outDir, "registration", resultData);
117
+ }
118
+ }
119
+
120
+ interface RegistrationResult {
121
+ agentId?: string;
122
+ owner?: string;
123
+ uri?: string;
124
+ chainId: number;
125
+ block: string;
126
+ timestamp: string;
127
+ gasPaid: string;
128
+ nativeCurrency: string;
129
+ txHash: string;
130
+ explorer?: string;
131
+ metadata?: Record<string, string>;
132
+ }
133
+
134
+ function printResult(
135
+ receipt: { blockNumber: bigint; gasUsed: bigint; effectiveGasPrice: bigint; logs: Log[] },
136
+ timestamp: bigint,
137
+ chain: Chain,
138
+ chainId: number,
139
+ hash: `0x${string}`,
140
+ ): RegistrationResult {
141
+ const events = parseEventLogs({ abi: IdentityRegistryAbi, logs: receipt.logs });
142
+ const registered = events.find((e) => e.eventName === "Registered");
143
+ const metadataEvents = events.filter((e) => e.eventName === "MetadataSet");
144
+
145
+ const lines: string[] = [];
146
+ const result: RegistrationResult = {
147
+ chainId,
148
+ block: receipt.blockNumber.toString(),
149
+ timestamp: new Date(Number(timestamp) * 1000).toUTCString(),
150
+ gasPaid: formatEther(receipt.gasUsed * receipt.effectiveGasPrice),
151
+ nativeCurrency: chain.nativeCurrency?.symbol ?? "ETH",
152
+ txHash: hash,
153
+ };
154
+
155
+ if (registered) {
156
+ const { agentId, agentURI, owner } = registered.args as { agentId: bigint; agentURI: string; owner: string };
157
+ result.agentId = agentId.toString();
158
+ result.owner = owner;
159
+ if (agentURI) result.uri = agentURI;
160
+
161
+ lines.push(`${label("Agent ID")}${chalk.bold(result.agentId)}`);
162
+ lines.push(`${label("Owner")}${owner}`);
163
+ if (agentURI) {
164
+ lines.push(`${label("URI")}${truncateUri(agentURI)}`);
165
+ }
166
+ lines.push(`${label("Block")}${receipt.blockNumber}`);
167
+ } else {
168
+ lines.push(`${label("Block")}${receipt.blockNumber}`);
169
+ }
170
+
171
+ lines.push(`${label("Timestamp")}${result.timestamp}`);
172
+ lines.push(`${label("Gas Paid")}${result.gasPaid} ${result.nativeCurrency}`);
173
+ lines.push(`${label("Tx Hash")}${hash}`);
174
+
175
+ const explorerUrl = getExplorerUrl(chain, hash);
176
+ if (explorerUrl) {
177
+ result.explorer = explorerUrl;
178
+ lines.push(`${label("Explorer")}${chalk.cyan(explorerUrl)}`);
179
+ }
180
+
181
+ if (metadataEvents.length > 0) {
182
+ result.metadata = {};
183
+ lines.push("");
184
+ lines.push(chalk.dim("Metadata"));
185
+ for (const event of metadataEvents) {
186
+ const { metadataKey, metadataValue } = event.args as { metadataKey: string; metadataValue: string };
187
+ result.metadata[metadataKey] = metadataValue;
188
+ lines.push(`${label(metadataKey)}${metadataValue}`);
189
+ }
190
+ }
191
+
192
+ console.log("");
193
+ console.log(
194
+ boxen(lines.join("\n"), {
195
+ padding: { left: 1, right: 1, top: 0, bottom: 0 },
196
+ borderStyle: "round",
197
+ borderColor: "green",
198
+ title: "Agent registered successfully",
199
+ titleAlignment: "left",
200
+ }),
201
+ );
202
+
203
+ return result;
204
+ }
@@ -0,0 +1,47 @@
1
+ import { describe, expect, test } from "bun:test";
2
+ import { CHAIN_ID, getIdentityRegistryAddress } from "@aixyz/erc-8004";
3
+ import { deriveAgentUri } from "./utils/prompt";
4
+
5
+ describe("update 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("deriveAgentUri", () => {
30
+ test("appends /_aixyz/erc-8004.json to base URL", () => {
31
+ expect(deriveAgentUri("https://my-agent.example.com")).toBe("https://my-agent.example.com/_aixyz/erc-8004.json");
32
+ });
33
+
34
+ test("strips trailing slash before appending", () => {
35
+ expect(deriveAgentUri("https://my-agent.example.com/")).toBe("https://my-agent.example.com/_aixyz/erc-8004.json");
36
+ });
37
+
38
+ test("strips multiple trailing slashes", () => {
39
+ expect(deriveAgentUri("https://my-agent.example.com///")).toBe("https://my-agent.example.com/_aixyz/erc-8004.json");
40
+ });
41
+
42
+ test("preserves path segments", () => {
43
+ expect(deriveAgentUri("https://example.com/agents/my-agent")).toBe(
44
+ "https://example.com/agents/my-agent/_aixyz/erc-8004.json",
45
+ );
46
+ });
47
+ });