@aixyz/cli 0.8.0 → 0.10.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,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,59 @@ 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
- import {
7
- resolveChainConfig,
8
- selectChain,
9
- resolveRegistryAddress,
10
- validateBrowserRpcConflict,
11
- getExplorerUrl,
12
- } from "./utils/chain";
5
+ import { resolveChainConfig, validateBrowserRpcConflict, getExplorerUrl, CHAINS } from "./utils/chain";
13
6
  import { writeResultJson } from "./utils/result";
14
7
  import { label, truncateUri, broadcastAndConfirm, logSignResult } from "./utils/transaction";
15
- import { confirm, input } from "@inquirer/prompts";
8
+ import { promptAgentUrl, promptSelectRegistration, deriveAgentUri } from "./utils/prompt";
9
+ import { readRegistrations } from "./utils/erc8004-file";
10
+ import { confirm } from "@inquirer/prompts";
16
11
  import chalk from "chalk";
17
12
  import boxen from "boxen";
18
13
  import type { BaseOptions } from "./index";
19
- import { promptAgentId, promptUri } from "./utils/prompt";
20
14
 
21
- export interface SetAgentUriOptions extends BaseOptions {
22
- agentId?: string;
23
- uri?: string;
15
+ export interface UpdateOptions extends BaseOptions {
16
+ url?: string;
24
17
  }
25
18
 
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.");
19
+ export async function update(options: UpdateOptions): Promise<void> {
20
+ // Step 1: Read registrations from app/erc-8004.ts
21
+ const registrations = await readRegistrations();
22
+
23
+ if (registrations.length === 0) {
24
+ throw new Error("No registrations found in app/erc-8004.ts. Run `aixyz erc-8004 register` first.");
33
25
  }
34
- }
35
26
 
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.`);
27
+ // Step 2: Select which registration to update
28
+ const selected = await promptSelectRegistration(registrations);
29
+
30
+ // Step 3: Derive chain info from agentRegistry (eip155:<chainId>:<address>)
31
+ const parts = selected.agentRegistry.split(":");
32
+ if (parts.length < 3 || parts[0] !== "eip155") {
33
+ throw new Error(`Invalid agentRegistry format: ${selected.agentRegistry}. Expected eip155:<chainId>:<address>`);
40
34
  }
41
- }
42
35
 
43
- export async function setAgentUri(options: SetAgentUriOptions): Promise<void> {
44
- const chainName = options.chain ?? (await selectChain());
36
+ const chainId = Number(parts[1]);
37
+ const registryAddress = parts.slice(2).join(":") as `0x${string}`;
38
+ const chainName = Object.entries(CHAINS).find(([, config]) => config.chainId === chainId)?.[0] ?? `chain-${chainId}`;
45
39
  const chainConfig = resolveChainConfig(chainName);
46
40
 
47
- const agentId = options.agentId ?? (await promptAgentId());
48
- validateAgentId(agentId);
41
+ // Step 4: Get new agent URL and derive URI
42
+ const agentUrl = options.url ?? (await promptAgentUrl());
43
+ const resolvedUri = deriveAgentUri(agentUrl);
49
44
 
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)`);
45
+ const yes = await confirm({
46
+ message: `Will update URI to: ${chalk.cyan(resolvedUri)} — confirm?`,
47
+ default: true,
48
+ });
49
+ if (!yes) {
50
+ throw new Error("Aborted.");
57
51
  }
58
52
 
59
- const registryAddress = resolveRegistryAddress(chainName, chainConfig.chainId, options.registry);
60
-
53
+ // Step 5: Encode transaction
61
54
  const data = encodeFunctionData({
62
55
  abi: IdentityRegistryAbi,
63
56
  functionName: "setAgentURI",
64
- args: [BigInt(agentId), resolvedUri],
57
+ args: [BigInt(selected.agentId), resolvedUri],
65
58
  });
66
59
 
67
60
  const printTxDetails = (header: string) => {
@@ -71,7 +64,7 @@ export async function setAgentUri(options: SetAgentUriOptions): Promise<void> {
71
64
  console.log(` ${label("Data")}${data.slice(0, 10)}${chalk.dim("\u2026" + (data.length - 2) / 2 + " bytes")}`);
72
65
  console.log(` ${label("Chain")}${chainName}`);
73
66
  console.log(` ${label("Function")}setAgentURI(uint256 agentId, string calldata newURI)`);
74
- console.log(` ${label("Agent ID")}${agentId}`);
67
+ console.log(` ${label("Agent ID")}${selected.agentId}`);
75
68
  console.log(` ${label("URI")}${truncateUri(resolvedUri)}`);
76
69
  console.log("");
77
70
  };
@@ -98,7 +91,7 @@ export async function setAgentUri(options: SetAgentUriOptions): Promise<void> {
98
91
  chain: chainConfig.chain,
99
92
  rpcUrl: options.rpcUrl,
100
93
  options: {
101
- browser: { chainId: chainConfig.chainId, chainName, uri: resolvedUri },
94
+ browser: { chainId: chainConfig.chainId, chainName, uri: resolvedUri, mode: "update" },
102
95
  },
103
96
  });
104
97
  logSignResult(walletMethod.type, result);
@@ -112,11 +105,11 @@ export async function setAgentUri(options: SetAgentUriOptions): Promise<void> {
112
105
  const resultData = printResult(receipt, timestamp, chainConfig.chain, chainConfig.chainId, hash);
113
106
 
114
107
  if (options.outDir) {
115
- writeResultJson(options.outDir, "set-agent-uri", resultData);
108
+ writeResultJson(options.outDir, "update", resultData);
116
109
  }
117
110
  }
118
111
 
119
- interface SetAgentUriResult {
112
+ interface UpdateResult {
120
113
  agentId?: string;
121
114
  newUri?: string;
122
115
  updatedBy?: `0x${string}`;
@@ -135,12 +128,12 @@ function printResult(
135
128
  chain: Chain,
136
129
  chainId: number,
137
130
  hash: `0x${string}`,
138
- ): SetAgentUriResult {
131
+ ): UpdateResult {
139
132
  const events = parseEventLogs({ abi: IdentityRegistryAbi, logs: receipt.logs as Log[] });
140
133
  const uriUpdated = events.find((e) => e.eventName === "URIUpdated");
141
134
 
142
135
  const lines: string[] = [];
143
- const result: SetAgentUriResult = {
136
+ const result: UpdateResult = {
144
137
  chainId,
145
138
  block: receipt.blockNumber.toString(),
146
139
  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
 
@@ -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,18 +1,61 @@
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
- export async function promptAgentId(): Promise<string> {
4
+ export async function promptAgentUrl(): Promise<string> {
4
5
  return input({
5
- message: "Agent ID (token ID) to update:",
6
+ message: "Agent deployment URL (e.g., https://my-agent.example.com):",
6
7
  validate: (value) => {
7
- const n = Number(value);
8
- if (value.trim() === "" || !Number.isInteger(n) || n < 0) return "Must be a non-negative integer";
9
- return true;
8
+ try {
9
+ const url = new URL(value);
10
+ if (url.protocol !== "https:" && url.protocol !== "http:") {
11
+ return "URL must start with https:// or http://";
12
+ }
13
+ return true;
14
+ } catch {
15
+ return "Must be a valid URL (e.g., https://my-agent.example.com)";
16
+ }
10
17
  },
11
18
  });
12
19
  }
13
20
 
14
- export async function promptUri(): Promise<string> {
15
- return input({
16
- message: "New agent metadata URI or path to .json file (leave empty to clear):",
21
+ export async function promptSupportedTrust(): Promise<string[]> {
22
+ return checkbox({
23
+ message: "Select supported trust mechanisms:",
24
+ choices: [
25
+ { name: "reputation", value: "reputation", checked: true },
26
+ { name: "crypto-economic", value: "crypto-economic" },
27
+ { name: "tee-attestation", value: "tee-attestation" },
28
+ { name: "social", value: "social" },
29
+ { name: "governance", value: "governance" },
30
+ ],
31
+ required: true,
17
32
  });
18
33
  }
34
+
35
+ export async function promptSelectRegistration(registrations: RegistrationEntry[]): Promise<RegistrationEntry> {
36
+ if (registrations.length === 1) {
37
+ const reg = registrations[0]!;
38
+ const yes = await confirm({
39
+ message: `Update this registration? (agentId: ${reg.agentId}, registry: ${reg.agentRegistry})`,
40
+ default: true,
41
+ });
42
+ if (!yes) {
43
+ throw new Error("Aborted.");
44
+ }
45
+ return reg;
46
+ }
47
+
48
+ return select({
49
+ message: "Select registration to update:",
50
+ choices: registrations.map((reg) => ({
51
+ name: `agentId: ${reg.agentId} — ${reg.agentRegistry}`,
52
+ value: reg,
53
+ })),
54
+ });
55
+ }
56
+
57
+ export function deriveAgentUri(url: string): string {
58
+ // Ensure no trailing slash before appending path
59
+ const base = url.replace(/\/+$/, "");
60
+ return `${base}/_aixyz/erc-8004.json`;
61
+ }
@@ -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({