@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.
@@ -0,0 +1,65 @@
1
+ import { existsSync, readFileSync, writeFileSync } from "node:fs";
2
+ import { resolve } from "node:path";
3
+ import type { RegistrationEntry } from "@aixyz/erc-8004/schemas/registration";
4
+
5
+ function getFilePath(cwd: string = process.cwd()): string {
6
+ return resolve(cwd, "app/erc-8004.ts");
7
+ }
8
+
9
+ export function hasErc8004File(cwd?: string): boolean {
10
+ return existsSync(getFilePath(cwd));
11
+ }
12
+
13
+ export function createErc8004File(supportedTrust: string[], cwd?: string): void {
14
+ const filePath = getFilePath(cwd);
15
+ const trustArray = supportedTrust.map((t) => `"${t}"`).join(", ");
16
+
17
+ const content = `import type { ERC8004Registration } from "aixyz/erc-8004";
18
+
19
+ const metadata: ERC8004Registration = {
20
+ registrations: [],
21
+ supportedTrust: [${trustArray}],
22
+ };
23
+
24
+ export default metadata;
25
+ `;
26
+
27
+ writeFileSync(filePath, content, "utf-8");
28
+ }
29
+
30
+ export async function readRegistrations(cwd?: string): Promise<RegistrationEntry[]> {
31
+ const filePath = getFilePath(cwd);
32
+
33
+ if (!existsSync(filePath)) {
34
+ throw new Error(`No app/erc-8004.ts found. Run \`aixyz erc-8004 register\` first.`);
35
+ }
36
+
37
+ const mod = await import(filePath);
38
+ const data = mod.default;
39
+
40
+ if (!data || !Array.isArray(data.registrations)) {
41
+ return [];
42
+ }
43
+
44
+ return data.registrations;
45
+ }
46
+
47
+ export function writeRegistrationEntry(entry: { agentId: number; agentRegistry: string }, cwd?: string): void {
48
+ const filePath = getFilePath(cwd);
49
+ const content = readFileSync(filePath, "utf-8");
50
+ const entryStr = `{ agentId: ${entry.agentId}, agentRegistry: "${entry.agentRegistry}" }`;
51
+
52
+ // Try to find `registrations: [...]` and insert the entry
53
+ const match = content.match(/registrations:\s*\[([^\]]*)\]/s);
54
+ if (match) {
55
+ const existing = match[1]!.trim();
56
+ const newEntries = existing ? `${existing}, ${entryStr}` : entryStr;
57
+ const updated = content.replace(/registrations:\s*\[([^\]]*)\]/s, `registrations: [${newEntries}]`);
58
+ writeFileSync(filePath, updated, "utf-8");
59
+ return;
60
+ }
61
+
62
+ // Fallback: append as comment
63
+ const comment = `\n// Registration added by \`aixyz erc-8004 register\`:\n// ${entryStr}\n`;
64
+ writeFileSync(filePath, content + comment, "utf-8");
65
+ }
@@ -1,4 +1,5 @@
1
- import { input } from "@inquirer/prompts";
1
+ import { checkbox, confirm, input, select } from "@inquirer/prompts";
2
+ import type { RegistrationEntry } from "@aixyz/erc-8004/schemas/registration";
2
3
 
3
4
  export async function promptAgentId(): Promise<string> {
4
5
  return input({
@@ -16,3 +17,62 @@ export async function promptUri(): Promise<string> {
16
17
  message: "New agent metadata URI or path to .json file (leave empty to clear):",
17
18
  });
18
19
  }
20
+
21
+ export async function promptAgentUrl(): Promise<string> {
22
+ return input({
23
+ message: "Agent deployment URL (e.g., https://my-agent.example.com):",
24
+ validate: (value) => {
25
+ try {
26
+ const url = new URL(value);
27
+ if (url.protocol !== "https:" && url.protocol !== "http:") {
28
+ return "URL must start with https:// or http://";
29
+ }
30
+ return true;
31
+ } catch {
32
+ return "Must be a valid URL (e.g., https://my-agent.example.com)";
33
+ }
34
+ },
35
+ });
36
+ }
37
+
38
+ export async function promptSupportedTrust(): Promise<string[]> {
39
+ return checkbox({
40
+ message: "Select supported trust mechanisms:",
41
+ choices: [
42
+ { name: "reputation", value: "reputation", checked: true },
43
+ { name: "crypto-economic", value: "crypto-economic" },
44
+ { name: "tee-attestation", value: "tee-attestation" },
45
+ { name: "social", value: "social" },
46
+ { name: "governance", value: "governance" },
47
+ ],
48
+ required: true,
49
+ });
50
+ }
51
+
52
+ export async function promptSelectRegistration(registrations: RegistrationEntry[]): Promise<RegistrationEntry> {
53
+ if (registrations.length === 1) {
54
+ const reg = registrations[0]!;
55
+ const yes = await confirm({
56
+ message: `Update this registration? (agentId: ${reg.agentId}, registry: ${reg.agentRegistry})`,
57
+ default: true,
58
+ });
59
+ if (!yes) {
60
+ throw new Error("Aborted.");
61
+ }
62
+ return reg;
63
+ }
64
+
65
+ return select({
66
+ message: "Select registration to update:",
67
+ choices: registrations.map((reg) => ({
68
+ name: `agentId: ${reg.agentId} — ${reg.agentRegistry}`,
69
+ value: reg,
70
+ })),
71
+ });
72
+ }
73
+
74
+ export function deriveAgentUri(url: string): string {
75
+ // Ensure no trailing slash before appending path
76
+ const base = url.replace(/\/+$/, "");
77
+ return `${base}/_aixyz/erc-8004.json`;
78
+ }
@@ -1,82 +1,9 @@
1
1
  import { describe, expect, test, afterAll, beforeAll } from "bun:test";
2
2
  import { rmSync } from "fs";
3
3
  import { mkdir } from "node:fs/promises";
4
- import { validatePrivateKey, CliError, resolveUri } from "./utils";
4
+ import { resolveUri } from "./utils";
5
5
  import { join } from "path";
6
6
 
7
- describe("validatePrivateKey", () => {
8
- test("accepts valid 64-char hex key with 0x prefix", () => {
9
- const key = "0x0000000000000000000000000000000000000000000000000000000000000001";
10
- const result = validatePrivateKey(key);
11
- expect(result).toStrictEqual(key);
12
- });
13
-
14
- test("accepts valid 64-char hex key without 0x prefix", () => {
15
- const key = "0000000000000000000000000000000000000000000000000000000000000001";
16
- const result = validatePrivateKey(key);
17
- expect(result).toStrictEqual(`0x${key}`);
18
- });
19
-
20
- test("accepts mixed case hex characters", () => {
21
- const key = "0xaAbBcCdDeEfF0000000000000000000000000000000000000000000000000001";
22
- const result = validatePrivateKey(key);
23
- expect(result).toStrictEqual(key);
24
- });
25
-
26
- test("rejects key that is too short", () => {
27
- const key = "0x1234";
28
- expect(() => validatePrivateKey(key)).toThrow(CliError);
29
- expect(() => validatePrivateKey(key)).toThrow("Invalid private key format");
30
- });
31
-
32
- test("rejects key that is too long", () => {
33
- const key = "0x00000000000000000000000000000000000000000000000000000000000000001";
34
- expect(() => validatePrivateKey(key)).toThrow(CliError);
35
- });
36
-
37
- test("rejects key with invalid characters", () => {
38
- const key = "0xGGGG000000000000000000000000000000000000000000000000000000000001";
39
- expect(() => validatePrivateKey(key)).toThrow(CliError);
40
- });
41
-
42
- test("rejects empty string", () => {
43
- expect(() => validatePrivateKey("")).toThrow(CliError);
44
- });
45
-
46
- test("rejects random string", () => {
47
- expect(() => validatePrivateKey("not-a-key")).toThrow(CliError);
48
- });
49
- });
50
-
51
- describe("CliError", () => {
52
- test("is an instance of Error", () => {
53
- const error = new CliError("test message");
54
- expect(error).toBeInstanceOf(Error);
55
- });
56
-
57
- test("has correct name property", () => {
58
- const error = new CliError("test message");
59
- expect(error.name).toStrictEqual("CliError");
60
- });
61
-
62
- test("has correct message property", () => {
63
- const error = new CliError("test message");
64
- expect(error.message).toStrictEqual("test message");
65
- });
66
-
67
- test("can be caught as Error", () => {
68
- let caught = false;
69
- try {
70
- throw new CliError("test");
71
- } catch (e) {
72
- if (e instanceof Error) {
73
- caught = true;
74
- }
75
- }
76
- expect(caught).toStrictEqual(true);
77
- });
78
- });
79
-
80
7
  describe("resolveUri", () => {
81
8
  const testDir = join(import.meta.dir, "__test_fixtures__");
82
9
  const testJsonPath = join(testDir, "test-metadata.json");
@@ -129,7 +56,7 @@ describe("resolveUri", () => {
129
56
  await mkdir(dirWithJsonSuffix, { recursive: true });
130
57
 
131
58
  try {
132
- expect(() => resolveUri(dirWithJsonSuffix)).toThrow(CliError);
59
+ expect(() => resolveUri(dirWithJsonSuffix)).toThrow(Error);
133
60
  expect(() => resolveUri(dirWithJsonSuffix)).toThrow("Not a file");
134
61
  } finally {
135
62
  rmSync(dirWithJsonSuffix, { recursive: true, force: true });
@@ -137,7 +64,7 @@ describe("resolveUri", () => {
137
64
  });
138
65
 
139
66
  test("throws for non-existent .json file", () => {
140
- expect(() => resolveUri("./non-existent.json")).toThrow(CliError);
67
+ expect(() => resolveUri("./non-existent.json")).toThrow(Error);
141
68
  expect(() => resolveUri("./non-existent.json")).toThrow("File not found");
142
69
  });
143
70
 
@@ -145,7 +72,7 @@ describe("resolveUri", () => {
145
72
  await Bun.write(testJsonPath, "not valid json {{{");
146
73
 
147
74
  try {
148
- expect(() => resolveUri(testJsonPath)).toThrow(CliError);
75
+ expect(() => resolveUri(testJsonPath)).toThrow(Error);
149
76
  expect(() => resolveUri(testJsonPath)).toThrow("Invalid JSON");
150
77
  } finally {
151
78
  await Bun.file(testJsonPath).unlink();
package/register/utils.ts CHANGED
@@ -12,11 +12,11 @@ export function resolveUri(uri: string): string {
12
12
  const filePath = resolve(uri);
13
13
 
14
14
  if (!existsSync(filePath)) {
15
- throw new CliError(`File not found: ${filePath}`);
15
+ throw new Error(`File not found: ${filePath}`);
16
16
  }
17
17
 
18
18
  if (!statSync(filePath).isFile()) {
19
- throw new CliError(`Not a file: ${filePath}`);
19
+ throw new Error(`Not a file: ${filePath}`);
20
20
  }
21
21
 
22
22
  const content = readFileSync(filePath, "utf-8");
@@ -25,7 +25,7 @@ export function resolveUri(uri: string): string {
25
25
  try {
26
26
  JSON.parse(content);
27
27
  } catch {
28
- throw new CliError(`Invalid JSON in file: ${filePath}`);
28
+ throw new Error(`Invalid JSON in file: ${filePath}`);
29
29
  }
30
30
 
31
31
  // Convert to base64 data URI
@@ -36,20 +36,3 @@ export function resolveUri(uri: string): string {
36
36
  // Return as-is for other URIs
37
37
  return uri;
38
38
  }
39
-
40
- export function validatePrivateKey(key: string): `0x${string}` {
41
- const normalizedKey = key.startsWith("0x") ? key : `0x${key}`;
42
-
43
- if (!/^0x[0-9a-fA-F]{64}$/.test(normalizedKey)) {
44
- throw new CliError("Invalid private key format. Expected 64 hex characters (with or without 0x prefix).");
45
- }
46
-
47
- return normalizedKey as `0x${string}`;
48
- }
49
-
50
- export class CliError extends Error {
51
- constructor(message: string) {
52
- super(message);
53
- this.name = "CliError";
54
- }
55
- }
@@ -103,4 +103,22 @@ describe("buildHtml", () => {
103
103
  expect(html).not.toContain('<script>alert("xss")</script>');
104
104
  expect(html).toContain("&lt;script&gt;");
105
105
  });
106
+
107
+ test("shows 'Register Agent' by default (no mode)", () => {
108
+ const html = buildHtml(baseParams);
109
+ expect(html).toContain("Register Agent");
110
+ expect(html).not.toContain("Update Agent");
111
+ });
112
+
113
+ test("shows 'Register Agent' when mode is 'register'", () => {
114
+ const html = buildHtml({ ...baseParams, mode: "register" });
115
+ expect(html).toContain("Register Agent");
116
+ expect(html).not.toContain("Update Agent");
117
+ });
118
+
119
+ test("shows 'Update Agent' when mode is 'update'", () => {
120
+ const html = buildHtml({ ...baseParams, mode: "update" });
121
+ expect(html).toContain("Update Agent");
122
+ expect(html).not.toContain("Register Agent");
123
+ });
106
124
  });
@@ -7,15 +7,16 @@ export interface BrowserSignParams {
7
7
  chainName: string;
8
8
  uri?: string;
9
9
  gas?: bigint;
10
+ mode?: "register" | "update";
10
11
  }
11
12
 
12
13
  const TIMEOUT_MS = 5 * 60 * 1000; // 5 minutes
13
14
 
14
15
  export async function signWithBrowser(params: BrowserSignParams): Promise<{ txHash: string }> {
15
- const { registryAddress, calldata, chainId, chainName, uri, gas } = params;
16
+ const { registryAddress, calldata, chainId, chainName, uri, gas, mode } = params;
16
17
 
17
18
  const nonce = crypto.randomUUID();
18
- const html = buildHtml({ registryAddress, calldata, chainId, chainName, uri, gas, nonce });
19
+ const html = buildHtml({ registryAddress, calldata, chainId, chainName, uri, gas, nonce, mode });
19
20
 
20
21
  const { promise: resultPromise, resolve, reject } = Promise.withResolvers<{ txHash: string }>();
21
22
  let settled = false;
@@ -134,8 +135,11 @@ export function buildHtml(params: {
134
135
  uri?: string;
135
136
  gas?: bigint;
136
137
  nonce: string;
138
+ mode?: "register" | "update";
137
139
  }): string {
138
- const { registryAddress, calldata, chainId, chainName, uri, gas, nonce } = params;
140
+ const { registryAddress, calldata, chainId, chainName, uri, gas, nonce, mode } = params;
141
+ const isUpdate = mode === "update";
142
+ const actionLabel = isUpdate ? "Update Agent" : "Register Agent";
139
143
  const chainIdHex = `0x${chainId.toString(16)}`;
140
144
 
141
145
  const displayUri = uri && uri.length > 80 ? uri.slice(0, 80) + "..." : (uri ?? "");
@@ -145,7 +149,7 @@ export function buildHtml(params: {
145
149
  <head>
146
150
  <meta charset="UTF-8">
147
151
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
148
- <title>agently-cliRegister Agent</title>
152
+ <title>aixyz.shERC-8004 ${actionLabel}</title>
149
153
  <link rel="preconnect" href="https://fonts.googleapis.com">
150
154
  <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
151
155
  <link href="https://fonts.googleapis.com/css2?family=DM+Mono:wght@300;400;500&family=DM+Sans:wght@400;500;600&display=swap" rel="stylesheet">
@@ -475,8 +479,8 @@ export function buildHtml(params: {
475
479
  <body>
476
480
  <div class="container">
477
481
  <div class="header">
478
- <div class="brand">agently-cli</div>
479
- <h1>Register Agent</h1>
482
+ <div class="brand">aixyz erc-8004</div>
483
+ <h1>${actionLabel}</h1>
480
484
  </div>
481
485
 
482
486
  <div class="details" id="details">
@@ -513,7 +517,7 @@ export function buildHtml(params: {
513
517
  <div id="walletList"></div>
514
518
  </div>
515
519
 
516
- <button id="registerBtn" disabled>Register Agent</button>
520
+ <button id="registerBtn" disabled>${actionLabel}</button>
517
521
 
518
522
  <div class="status" id="status"></div>
519
523
  </div>
@@ -524,6 +528,7 @@ export function buildHtml(params: {
524
528
  const CHAIN_ID_HEX = ${safeJsonEmbed(chainIdHex)};
525
529
  const CHAIN_ID = ${chainId};
526
530
  const GAS = ${gas ? safeJsonEmbed(`0x${gas.toString(16)}`) : "undefined"};
531
+ const ACTION_LABEL = ${safeJsonEmbed(actionLabel)};
527
532
 
528
533
  const registerBtn = document.getElementById("registerBtn");
529
534
  const statusEl = document.getElementById("status");
@@ -543,7 +548,7 @@ export function buildHtml(params: {
543
548
  walletInfo.style.display = "none";
544
549
  registerBtn.style.display = "none";
545
550
  registerBtn.disabled = true;
546
- registerBtn.textContent = "Register Agent";
551
+ registerBtn.textContent = ACTION_LABEL;
547
552
  walletSectionEl.style.display = "";
548
553
  statusEl.className = "status";
549
554
  if (discoveredWallets.size > 0) {
@@ -663,7 +668,7 @@ export function buildHtml(params: {
663
668
  walletSectionEl.style.display = "none";
664
669
  registerBtn.style.display = "block";
665
670
  registerBtn.disabled = false;
666
- setStatus("Wallet connected. Ready to register.", "success");
671
+ setStatus("Wallet connected. Ready to ${isUpdate ? "update" : "register"}.", "success");
667
672
 
668
673
  // Listen for account/chain changes on the selected provider
669
674
  if (selectedProvider.on) {
@@ -739,7 +744,7 @@ export function buildHtml(params: {
739
744
  setStatus("Failed: " + err.message + " — You can try again.", "error");
740
745
  }
741
746
  registerBtn.disabled = false;
742
- registerBtn.textContent = "Register Agent";
747
+ registerBtn.textContent = ACTION_LABEL;
743
748
  }
744
749
  });
745
750
  </script>
@@ -3,7 +3,6 @@ import type { Chain, WalletClient } from "viem";
3
3
  import { select, input, password } from "@inquirer/prompts";
4
4
  import { createPrivateKeyWallet } from "./privatekey";
5
5
  import { createKeystoreWallet } from "./keystore";
6
- import { CliError } from "../utils";
7
6
 
8
7
  export interface WalletOptions {
9
8
  keystore?: string;
@@ -62,7 +61,7 @@ export async function selectWalletMethod(options: WalletOptions): Promise<Wallet
62
61
  return { type: "privatekey", resolveKey: () => Promise.resolve(key) };
63
62
  }
64
63
  default:
65
- throw new CliError("No wallet method selected");
64
+ throw new Error("No wallet method selected");
66
65
  }
67
66
  }
68
67
 
@@ -77,7 +76,7 @@ export async function createWalletFromMethod(
77
76
  case "keystore":
78
77
  return createKeystoreWallet(method.path, chain, rpcUrl);
79
78
  case "browser":
80
- throw new CliError("Browser wallets should use registerWithBrowser, not createWalletFromMethod");
79
+ throw new Error("Browser wallets should use registerWithBrowser, not createWalletFromMethod");
81
80
  }
82
81
  }
83
82
 
@@ -9,7 +9,7 @@ const TEST_PRIVATE_KEY = "0x0000000000000000000000000000000000000000000000000000
9
9
  const TEST_ADDRESS = "0x7E5F4552091A69125d5DfCb7b8C2659029395Bdf";
10
10
  const TEST_PASSWORD = "testpassword";
11
11
 
12
- const testDir = join(tmpdir(), "agently-cli-keystore-test");
12
+ const testDir = join(tmpdir(), "aixyz-cli-keystore-test");
13
13
  const testKeystorePath = join(testDir, "test-keystore.json");
14
14
 
15
15
  // Mock the password prompt to return TEST_PASSWORD
@@ -3,17 +3,16 @@ import { decryptKeystoreJson, isKeystoreJson } from "ethers";
3
3
  import { createWalletClient, http, type Chain, type WalletClient } from "viem";
4
4
  import { privateKeyToAccount } from "viem/accounts";
5
5
  import { password } from "@inquirer/prompts";
6
- import { CliError } from "../utils";
7
6
 
8
7
  export async function decryptKeystore(keystorePath: string): Promise<`0x${string}`> {
9
8
  const file = Bun.file(keystorePath);
10
9
  if (!(await file.exists())) {
11
- throw new CliError(`Keystore file not found: ${keystorePath}`);
10
+ throw new Error(`Keystore file not found: ${keystorePath}`);
12
11
  }
13
12
 
14
13
  const json = await file.text();
15
14
  if (!isKeystoreJson(json)) {
16
- throw new CliError(`Invalid keystore file: ${keystorePath}`);
15
+ throw new Error(`Invalid keystore file: ${keystorePath}`);
17
16
  }
18
17
  const pass = await password({ message: "Enter keystore password:", mask: "*" });
19
18
  const account = await decryptKeystoreJson(json, pass);
@@ -1,10 +1,15 @@
1
1
  import { createWalletClient, http, type Chain, type WalletClient } from "viem";
2
2
  import { privateKeyToAccount } from "viem/accounts";
3
- import { validatePrivateKey } from "../utils";
4
3
 
5
4
  export function createPrivateKeyWallet(privateKey: string, chain: Chain, rpcUrl?: string): WalletClient {
6
- const key = validatePrivateKey(privateKey);
7
- const account = privateKeyToAccount(key);
5
+ const key = (privateKey.startsWith("0x") ? privateKey : `0x${privateKey}`) as `0x${string}`;
6
+
7
+ let account;
8
+ try {
9
+ account = privateKeyToAccount(key);
10
+ } catch {
11
+ throw new Error("Invalid private key format. Expected 64 hex characters (with or without 0x prefix).");
12
+ }
8
13
 
9
14
  return createWalletClient({
10
15
  account,
@@ -1,7 +1,6 @@
1
1
  import type { Chain } from "viem";
2
2
  import { signWithBrowser } from "./browser";
3
3
  import { createWalletFromMethod, type WalletMethod } from "./index";
4
- import { CliError } from "../utils";
5
4
 
6
5
  export interface TxRequest {
7
6
  to: `0x${string}`;
@@ -10,7 +9,7 @@ export interface TxRequest {
10
9
  }
11
10
 
12
11
  export interface SignOptions {
13
- browser?: { chainId: number; chainName: string; uri?: string };
12
+ browser?: { chainId: number; chainName: string; uri?: string; mode?: "register" | "update" };
14
13
  }
15
14
 
16
15
  export type SignTransactionResult =
@@ -35,13 +34,14 @@ export async function signTransaction({
35
34
  switch (walletMethod.type) {
36
35
  case "browser": {
37
36
  if (!options?.browser) {
38
- throw new CliError("Browser wallet requires chainId and chainName parameters");
37
+ throw new Error("Browser wallet requires chainId and chainName parameters");
39
38
  }
40
39
  return signViaBrowser({
41
40
  tx,
42
41
  chainId: options.browser.chainId,
43
42
  chainName: options.browser.chainName,
44
43
  uri: options.browser.uri,
44
+ mode: options.browser.mode,
45
45
  });
46
46
  }
47
47
  default: {
@@ -56,11 +56,13 @@ async function signViaBrowser({
56
56
  chainId,
57
57
  chainName,
58
58
  uri,
59
+ mode,
59
60
  }: {
60
61
  tx: TxRequest;
61
62
  chainId: number;
62
63
  chainName: string;
63
64
  uri?: string;
65
+ mode?: "register" | "update";
64
66
  }): Promise<SignTransactionResult> {
65
67
  const { txHash } = await signWithBrowser({
66
68
  registryAddress: tx.to,
@@ -69,10 +71,11 @@ async function signViaBrowser({
69
71
  chainName,
70
72
  uri,
71
73
  gas: tx.gas,
74
+ mode,
72
75
  });
73
76
 
74
77
  if (typeof txHash !== "string" || !/^0x[0-9a-f]{64}$/i.test(txHash)) {
75
- throw new CliError(`Invalid transaction hash received from browser wallet: ${txHash}`);
78
+ throw new Error(`Invalid transaction hash received from browser wallet: ${txHash}`);
76
79
  }
77
80
 
78
81
  return { kind: "sent", txHash: txHash as `0x${string}` };
@@ -93,7 +96,7 @@ async function signWithWalletClient({
93
96
 
94
97
  const account = walletClient.account;
95
98
  if (!account) {
96
- throw new CliError("Wallet client does not have an account configured");
99
+ throw new Error("Wallet client does not have an account configured");
97
100
  }
98
101
 
99
102
  const request = await walletClient.prepareTransactionRequest({
@@ -1,101 +0,0 @@
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 |