@aixyz/cli 0.8.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/bin.ts CHANGED
@@ -3,8 +3,7 @@ import { program } from "commander";
3
3
  import { build } from "./build";
4
4
  import { dev } from "./dev";
5
5
  import { register } from "./register/register";
6
- import { setAgentUri } from "./register/set-agent-uri";
7
- import { CliError } from "./register/utils";
6
+ import { update } from "./register/update";
8
7
  import pkg from "./package.json";
9
8
 
10
9
  function handleAction(
@@ -14,10 +13,6 @@ function handleAction(
14
13
  try {
15
14
  await action(options);
16
15
  } catch (error) {
17
- if (error instanceof CliError) {
18
- console.error(`Error: ${error.message}`);
19
- process.exit(1);
20
- }
21
16
  if (error instanceof Error && error.name === "ExitPromptError") {
22
17
  process.exit(130);
23
18
  }
@@ -70,12 +65,12 @@ Examples:
70
65
  )
71
66
  .action(handleAction(build));
72
67
 
73
- const erc8004 = program.command("erc8004").description("ERC-8004 IdentityRegistry operations");
68
+ const erc8004 = program.command("erc-8004").description("ERC-8004 IdentityRegistry operations");
74
69
 
75
70
  erc8004
76
71
  .command("register")
77
72
  .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)")
73
+ .option("--url <url>", "Agent deployment URL (e.g., https://my-agent.example.com)")
79
74
  .option("--chain <chain>", "Target chain (mainnet, sepolia, base-sepolia, localhost)")
80
75
  .option("--rpc-url <url>", "Custom RPC URL (uses default if not provided)")
81
76
  .option("--registry <address>", "Contract address of the IdentityRegistry (required for localhost)")
@@ -87,12 +82,10 @@ erc8004
87
82
  "after",
88
83
  `
89
84
  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.
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.
96
89
 
97
90
  --chain <chain>
98
91
  Target chain for registration. Supported values:
@@ -101,7 +94,6 @@ Option Details:
101
94
  base-sepolia Base Sepolia testnet (chain ID 84532)
102
95
  localhost Local Foundry/Anvil node (chain ID 31337)
103
96
  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
97
 
106
98
  --rpc-url <url>
107
99
  Custom RPC endpoint URL. Overrides the default RPC for the selected
@@ -110,8 +102,7 @@ Option Details:
110
102
 
111
103
  --registry <address>
112
104
  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.
105
+ localhost, where there is no default deployment.
115
106
 
116
107
  --keystore <path>
117
108
  Path to an Ethereum keystore (V3) JSON file. You will be prompted for
@@ -120,42 +111,36 @@ Option Details:
120
111
  --browser
121
112
  Opens a local page in your default browser for signing with any
122
113
  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
114
 
126
115
  --broadcast
127
116
  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.
117
+ command performs a dry-run.
131
118
 
132
119
  --out-dir <path>
133
- Directory to write the deployment result as a JSON file. The file
134
- is named registration-<chainId>-<timestamp>.json.
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.
135
126
 
136
127
  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.
128
+ PRIVATE_KEY Private key (hex, with or without 0x prefix) used for signing.
140
129
 
141
130
  Examples:
142
- # Dry-run (default) — shows encoded transaction, no wallet needed
143
- $ aixyz erc8004 register --uri "./metadata.json" --chain sepolia
131
+ # Dry-run (default)
132
+ $ aixyz erc-8004 register --url "https://my-agent.example.com" --chain sepolia
144
133
 
145
134
  # 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`,
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`,
150
137
  )
151
138
  .action(handleAction(register));
152
139
 
153
140
  erc8004
154
- .command("set-agent-uri")
141
+ .command("update")
155
142
  .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)")
143
+ .option("--url <url>", "New agent deployment URL (e.g., https://my-agent.example.com)")
159
144
  .option("--rpc-url <url>", "Custom RPC URL (uses default if not provided)")
160
145
  .option("--registry <address>", "Contract address of the IdentityRegistry (required for localhost)")
161
146
  .option("--keystore <path>", "Path to Ethereum keystore (V3) JSON file for local signing")
@@ -166,73 +151,50 @@ erc8004
166
151
  "after",
167
152
  `
168
153
  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.
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.
190
158
 
191
159
  --rpc-url <url>
192
160
  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.
161
+ chain. Cannot be used with --browser.
195
162
 
196
163
  --registry <address>
197
164
  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.
165
+ localhost, where there is no default deployment.
200
166
 
201
167
  --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.
168
+ Path to an Ethereum keystore (V3) JSON file.
204
169
 
205
170
  --browser
206
171
  Opens a local page in your default browser for signing with any
207
172
  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
173
 
211
174
  --broadcast
212
175
  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.
176
+ command performs a dry-run.
216
177
 
217
178
  --out-dir <path>
218
- Directory to write the result as a JSON file. The file
219
- is named set-agent-uri-<chainId>-<timestamp>.json.
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.
220
186
 
221
187
  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.
188
+ PRIVATE_KEY Private key (hex, with or without 0x prefix) used for signing.
225
189
 
226
190
  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
191
+ # Dry-run (default)
192
+ $ aixyz erc-8004 update --url "https://new-domain.example.com"
229
193
 
230
194
  # 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`,
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`,
235
197
  )
236
- .action(handleAction(setAgentUri));
198
+ .action(handleAction(update));
237
199
 
238
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() {
@@ -127,6 +127,16 @@ function generateServer(appDir: string, entrypointDir: string): string {
127
127
  body.push("await mcp.connect();");
128
128
  }
129
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
+
130
140
  body.push("export default server;");
131
141
 
132
142
  return [...imports, "", ...body].join("\n");
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aixyz/cli",
3
- "version": "0.8.0",
3
+ "version": "0.9.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.8.0",
31
- "@aixyz/erc-8004": "0.8.0",
30
+ "@aixyz/config": "0.9.0",
31
+ "@aixyz/erc-8004": "0.9.0",
32
32
  "@inquirer/prompts": "^8.3.0",
33
33
  "@next/env": "^16.1.6",
34
34
  "boxen": "^8.0.1",
@@ -1,6 +1,5 @@
1
1
  import { describe, expect, test } from "bun:test";
2
2
  import { CHAIN_ID, getIdentityRegistryAddress } from "@aixyz/erc-8004";
3
- import { register } from "./register";
4
3
 
5
4
  describe("register command chain configuration", () => {
6
5
  test("sepolia chain ID is correct", () => {
@@ -64,12 +63,4 @@ describe("register command validation", () => {
64
63
  };
65
64
  expect(CHAINS["mainnet"]).toBeUndefined();
66
65
  });
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
66
  });
@@ -2,7 +2,6 @@ import { encodeFunctionData, formatEther, parseEventLogs, type Chain, type Log }
2
2
  import { IdentityRegistryAbi } from "@aixyz/erc-8004";
3
3
  import { selectWalletMethod, type WalletOptions } from "./wallet";
4
4
  import { signTransaction } from "./wallet/sign";
5
- import { resolveUri } from "./utils";
6
5
  import {
7
6
  resolveChainConfig,
8
7
  selectChain,
@@ -12,30 +11,51 @@ import {
12
11
  } from "./utils/chain";
13
12
  import { writeResultJson } from "./utils/result";
14
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";
15
17
  import chalk from "chalk";
16
18
  import boxen from "boxen";
17
19
  import type { BaseOptions } from "./index";
18
20
 
19
21
  export interface RegisterOptions extends BaseOptions {
20
- uri?: string;
22
+ url?: string;
21
23
  chain?: string;
22
24
  }
23
25
 
24
26
  export async function register(options: RegisterOptions): Promise<void> {
25
- const chainName = options.chain ?? (await selectChain());
26
- const chainConfig = resolveChainConfig(chainName);
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);
27
40
 
28
- const resolvedUri = options.uri ? resolveUri(options.uri) : undefined;
29
- if (options.uri && resolvedUri !== options.uri) {
30
- console.log(`Resolved ${options.uri} to data URI (${resolvedUri!.length} chars)`);
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.");
31
47
  }
32
48
 
49
+ // Step 3: Select chain
50
+ const chainName = options.chain ?? (await selectChain());
51
+ const chainConfig = resolveChainConfig(chainName);
33
52
  const registryAddress = resolveRegistryAddress(chainName, chainConfig.chainId, options.registry);
34
53
 
54
+ // Step 4: Encode transaction
35
55
  const data = encodeFunctionData({
36
56
  abi: IdentityRegistryAbi,
37
57
  functionName: "register",
38
- args: resolvedUri ? [resolvedUri] : [],
58
+ args: [resolvedUri],
39
59
  });
40
60
 
41
61
  const printTxDetails = (header: string) => {
@@ -44,10 +64,8 @@ export async function register(options: RegisterOptions): Promise<void> {
44
64
  console.log(` ${label("To")}${registryAddress}`);
45
65
  console.log(` ${label("Data")}${data.slice(0, 10)}${chalk.dim("\u2026" + (data.length - 2) / 2 + " bytes")}`);
46
66
  console.log(` ${label("Chain")}${chainName}`);
47
- console.log(` ${label("Function")}${resolvedUri ? "register(string memory agentURI)" : "register()"}`);
48
- if (resolvedUri) {
49
- console.log(` ${label("URI")}${truncateUri(resolvedUri)}`);
50
- }
67
+ console.log(` ${label("Function")}register(string memory agentURI)`);
68
+ console.log(` ${label("URI")}${truncateUri(resolvedUri)}`);
51
69
  console.log("");
52
70
  };
53
71
 
@@ -73,7 +91,7 @@ export async function register(options: RegisterOptions): Promise<void> {
73
91
  chain: chainConfig.chain,
74
92
  rpcUrl: options.rpcUrl,
75
93
  options: {
76
- browser: { chainId: chainConfig.chainId, chainName, uri: resolvedUri },
94
+ browser: { chainId: chainConfig.chainId, chainName, uri: resolvedUri, mode: "register" },
77
95
  },
78
96
  });
79
97
  logSignResult(walletMethod.type, result);
@@ -86,6 +104,14 @@ export async function register(options: RegisterOptions): Promise<void> {
86
104
 
87
105
  const resultData = printResult(receipt, timestamp, chainConfig.chain, chainConfig.chainId, hash);
88
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
+
89
115
  if (options.outDir) {
90
116
  writeResultJson(options.outDir, "registration", resultData);
91
117
  }
@@ -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
+ });
@@ -2,66 +2,65 @@ 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 { CliError, resolveUri } from "./utils";
6
5
  import {
7
6
  resolveChainConfig,
8
- selectChain,
9
7
  resolveRegistryAddress,
10
8
  validateBrowserRpcConflict,
11
9
  getExplorerUrl,
10
+ CHAINS,
12
11
  } from "./utils/chain";
13
12
  import { writeResultJson } from "./utils/result";
14
13
  import { label, truncateUri, broadcastAndConfirm, logSignResult } from "./utils/transaction";
15
- import { confirm, input } from "@inquirer/prompts";
14
+ import { promptAgentUrl, promptSelectRegistration, deriveAgentUri } from "./utils/prompt";
15
+ import { readRegistrations } from "./utils/erc8004-file";
16
+ import { confirm } from "@inquirer/prompts";
16
17
  import chalk from "chalk";
17
18
  import boxen from "boxen";
18
19
  import type { BaseOptions } from "./index";
19
- import { promptAgentId, promptUri } from "./utils/prompt";
20
20
 
21
- export interface SetAgentUriOptions extends BaseOptions {
22
- agentId?: string;
23
- uri?: string;
21
+ export interface UpdateOptions extends BaseOptions {
22
+ url?: string;
24
23
  }
25
24
 
26
- async function confirmEmptyUri(): Promise<void> {
27
- const yes = await confirm({
28
- message: "URI is empty. This will clear the agent's metadata URI. Are you sure?",
29
- default: false,
30
- });
31
- if (!yes) {
32
- throw new CliError("Aborted.");
25
+ export async function update(options: UpdateOptions): Promise<void> {
26
+ // Step 1: Read registrations from app/erc-8004.ts
27
+ const registrations = await readRegistrations();
28
+
29
+ if (registrations.length === 0) {
30
+ throw new Error("No registrations found in app/erc-8004.ts. Run `aixyz erc-8004 register` first.");
33
31
  }
34
- }
35
32
 
36
- export function validateAgentId(agentId: string): void {
37
- const n = Number(agentId);
38
- if (agentId.trim() === "" || !Number.isInteger(n) || n < 0) {
39
- throw new CliError(`Invalid agent ID: ${agentId}. Must be a non-negative integer.`);
33
+ // Step 2: Select which registration to update
34
+ const selected = await promptSelectRegistration(registrations);
35
+
36
+ // Step 3: Derive chain info from agentRegistry (eip155:<chainId>:<address>)
37
+ const parts = selected.agentRegistry.split(":");
38
+ if (parts.length < 3 || parts[0] !== "eip155") {
39
+ throw new Error(`Invalid agentRegistry format: ${selected.agentRegistry}. Expected eip155:<chainId>:<address>`);
40
40
  }
41
- }
42
41
 
43
- export async function setAgentUri(options: SetAgentUriOptions): Promise<void> {
44
- const chainName = options.chain ?? (await selectChain());
42
+ const chainId = Number(parts[1]);
43
+ const registryAddress = parts.slice(2).join(":") as `0x${string}`;
44
+ const chainName = Object.entries(CHAINS).find(([, config]) => config.chainId === chainId)?.[0] ?? `chain-${chainId}`;
45
45
  const chainConfig = resolveChainConfig(chainName);
46
46
 
47
- const agentId = options.agentId ?? (await promptAgentId());
48
- validateAgentId(agentId);
47
+ // Step 4: Get new agent URL and derive URI
48
+ const agentUrl = options.url ?? (await promptAgentUrl());
49
+ const resolvedUri = deriveAgentUri(agentUrl);
49
50
 
50
- const uri = options.uri ?? (await promptUri());
51
- if (uri === "") {
52
- await confirmEmptyUri();
53
- }
54
- const resolvedUri = uri === "" ? "" : resolveUri(uri);
55
- if (resolvedUri !== uri) {
56
- console.log(`Resolved ${uri} to data URI (${resolvedUri.length} chars)`);
51
+ const yes = await confirm({
52
+ message: `Will update URI to: ${chalk.cyan(resolvedUri)} — confirm?`,
53
+ default: true,
54
+ });
55
+ if (!yes) {
56
+ throw new Error("Aborted.");
57
57
  }
58
58
 
59
- const registryAddress = resolveRegistryAddress(chainName, chainConfig.chainId, options.registry);
60
-
59
+ // Step 5: Encode transaction
61
60
  const data = encodeFunctionData({
62
61
  abi: IdentityRegistryAbi,
63
62
  functionName: "setAgentURI",
64
- args: [BigInt(agentId), resolvedUri],
63
+ args: [BigInt(selected.agentId), resolvedUri],
65
64
  });
66
65
 
67
66
  const printTxDetails = (header: string) => {
@@ -71,7 +70,7 @@ export async function setAgentUri(options: SetAgentUriOptions): Promise<void> {
71
70
  console.log(` ${label("Data")}${data.slice(0, 10)}${chalk.dim("\u2026" + (data.length - 2) / 2 + " bytes")}`);
72
71
  console.log(` ${label("Chain")}${chainName}`);
73
72
  console.log(` ${label("Function")}setAgentURI(uint256 agentId, string calldata newURI)`);
74
- console.log(` ${label("Agent ID")}${agentId}`);
73
+ console.log(` ${label("Agent ID")}${selected.agentId}`);
75
74
  console.log(` ${label("URI")}${truncateUri(resolvedUri)}`);
76
75
  console.log("");
77
76
  };
@@ -98,7 +97,7 @@ export async function setAgentUri(options: SetAgentUriOptions): Promise<void> {
98
97
  chain: chainConfig.chain,
99
98
  rpcUrl: options.rpcUrl,
100
99
  options: {
101
- browser: { chainId: chainConfig.chainId, chainName, uri: resolvedUri },
100
+ browser: { chainId: chainConfig.chainId, chainName, uri: resolvedUri, mode: "update" },
102
101
  },
103
102
  });
104
103
  logSignResult(walletMethod.type, result);
@@ -112,11 +111,11 @@ export async function setAgentUri(options: SetAgentUriOptions): Promise<void> {
112
111
  const resultData = printResult(receipt, timestamp, chainConfig.chain, chainConfig.chainId, hash);
113
112
 
114
113
  if (options.outDir) {
115
- writeResultJson(options.outDir, "set-agent-uri", resultData);
114
+ writeResultJson(options.outDir, "update", resultData);
116
115
  }
117
116
  }
118
117
 
119
- interface SetAgentUriResult {
118
+ interface UpdateResult {
120
119
  agentId?: string;
121
120
  newUri?: string;
122
121
  updatedBy?: `0x${string}`;
@@ -135,12 +134,12 @@ function printResult(
135
134
  chain: Chain,
136
135
  chainId: number,
137
136
  hash: `0x${string}`,
138
- ): SetAgentUriResult {
137
+ ): UpdateResult {
139
138
  const events = parseEventLogs({ abi: IdentityRegistryAbi, logs: receipt.logs as Log[] });
140
139
  const uriUpdated = events.find((e) => e.eventName === "URIUpdated");
141
140
 
142
141
  const lines: string[] = [];
143
- const result: SetAgentUriResult = {
142
+ const result: UpdateResult = {
144
143
  chainId,
145
144
  block: receipt.blockNumber.toString(),
146
145
  timestamp: new Date(Number(timestamp) * 1000).toUTCString(),
@@ -2,7 +2,6 @@ import { isAddress, type Chain } from "viem";
2
2
  import { mainnet, sepolia, baseSepolia, foundry } from "viem/chains";
3
3
  import { CHAIN_ID, getIdentityRegistryAddress } from "@aixyz/erc-8004";
4
4
  import { select } from "@inquirer/prompts";
5
- import { CliError } from "../utils";
6
5
 
7
6
  export interface ChainConfig {
8
7
  chain: Chain;
@@ -19,7 +18,7 @@ export const CHAINS: Record<string, ChainConfig> = {
19
18
  export function resolveChainConfig(chainName: string): ChainConfig {
20
19
  const config = CHAINS[chainName];
21
20
  if (!config) {
22
- throw new CliError(`Unsupported chain: ${chainName}. Supported chains: ${Object.keys(CHAINS).join(", ")}`);
21
+ throw new Error(`Unsupported chain: ${chainName}. Supported chains: ${Object.keys(CHAINS).join(", ")}`);
23
22
  }
24
23
  return config;
25
24
  }
@@ -34,19 +33,19 @@ export async function selectChain(): Promise<string> {
34
33
  export function resolveRegistryAddress(chainName: string, chainId: number, registry?: string): `0x${string}` {
35
34
  if (registry) {
36
35
  if (!isAddress(registry)) {
37
- throw new CliError(`Invalid registry address: ${registry}`);
36
+ throw new Error(`Invalid registry address: ${registry}`);
38
37
  }
39
38
  return registry as `0x${string}`;
40
39
  }
41
40
  if (chainName === "localhost") {
42
- throw new CliError("--registry is required for localhost (no default contract deployment)");
41
+ throw new Error("--registry is required for localhost (no default contract deployment)");
43
42
  }
44
43
  return getIdentityRegistryAddress(chainId) as `0x${string}`;
45
44
  }
46
45
 
47
46
  export function validateBrowserRpcConflict(browser: boolean | undefined, rpcUrl: string | undefined): void {
48
47
  if (browser && rpcUrl) {
49
- throw new CliError("--rpc-url cannot be used with browser wallet. The browser wallet uses its own RPC endpoint.");
48
+ throw new Error("--rpc-url cannot be used with browser wallet. The browser wallet uses its own RPC endpoint.");
50
49
  }
51
50
  }
52
51