@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.
@@ -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 |
@@ -1,156 +0,0 @@
1
- import { describe, expect, test } from "bun:test";
2
- import { CHAIN_ID, getIdentityRegistryAddress } from "@aixyz/erc-8004";
3
- import { setAgentUri, validateAgentId } from "./set-agent-uri";
4
-
5
- describe("set-agent-uri 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("validateAgentId", () => {
30
- test("accepts 0", () => {
31
- expect(() => validateAgentId("0")).not.toThrow();
32
- });
33
-
34
- test("accepts positive integer", () => {
35
- expect(() => validateAgentId("42")).not.toThrow();
36
- });
37
-
38
- test("accepts large integer", () => {
39
- expect(() => validateAgentId("999999999")).not.toThrow();
40
- });
41
-
42
- test("rejects empty string", () => {
43
- expect(() => validateAgentId("")).toThrow("Invalid agent ID");
44
- });
45
-
46
- test("rejects whitespace-only string", () => {
47
- expect(() => validateAgentId(" ")).toThrow("Invalid agent ID");
48
- });
49
-
50
- test("accepts leading zeros", () => {
51
- expect(() => validateAgentId("007")).not.toThrow();
52
- });
53
-
54
- test("accepts single leading zero before digits", () => {
55
- expect(() => validateAgentId("042")).not.toThrow();
56
- });
57
-
58
- test("rejects negative number", () => {
59
- expect(() => validateAgentId("-1")).toThrow("Invalid agent ID");
60
- });
61
-
62
- test("rejects float", () => {
63
- expect(() => validateAgentId("1.5")).toThrow("Invalid agent ID");
64
- });
65
-
66
- test("rejects non-numeric string", () => {
67
- expect(() => validateAgentId("abc")).toThrow("Invalid agent ID");
68
- });
69
-
70
- test("rejects mixed string", () => {
71
- expect(() => validateAgentId("12abc")).toThrow("Invalid agent ID");
72
- });
73
-
74
- test("rejects Infinity", () => {
75
- expect(() => validateAgentId("Infinity")).toThrow("Invalid agent ID");
76
- });
77
-
78
- test("rejects NaN", () => {
79
- expect(() => validateAgentId("NaN")).toThrow("Invalid agent ID");
80
- });
81
- });
82
-
83
- describe("set-agent-uri command validation", () => {
84
- test("localhost requires --registry flag", async () => {
85
- await expect(
86
- setAgentUri({ agentId: "1", uri: "https://example.com/agent.json", chain: "localhost" }),
87
- ).rejects.toThrow("--registry is required for localhost");
88
- });
89
-
90
- test("rejects unsupported chain", async () => {
91
- await expect(
92
- setAgentUri({ agentId: "1", uri: "https://example.com/agent.json", chain: "fakenet" }),
93
- ).rejects.toThrow("Unsupported chain: fakenet");
94
- });
95
-
96
- test("rejects invalid registry address", async () => {
97
- await expect(
98
- setAgentUri({
99
- agentId: "1",
100
- uri: "https://example.com/agent.json",
101
- chain: "localhost",
102
- registry: "not-an-address",
103
- }),
104
- ).rejects.toThrow("Invalid registry address: not-an-address");
105
- });
106
-
107
- test("rejects --browser with --rpc-url", async () => {
108
- await expect(
109
- setAgentUri({
110
- agentId: "0",
111
- uri: "https://example.com/agent.json",
112
- chain: "sepolia",
113
- browser: true,
114
- rpcUrl: "http://localhost:8545",
115
- }),
116
- ).rejects.toThrow("--rpc-url cannot be used with browser wallet");
117
- });
118
-
119
- test("dry-run completes without wallet interaction when --broadcast is not set", async () => {
120
- await expect(
121
- setAgentUri({ agentId: "1", uri: "https://example.com/agent.json", chain: "sepolia" }),
122
- ).resolves.toBeUndefined();
123
- });
124
-
125
- test("rejects invalid agent ID (negative)", async () => {
126
- await expect(
127
- setAgentUri({ agentId: "-1", uri: "https://example.com/agent.json", chain: "sepolia" }),
128
- ).rejects.toThrow("Invalid agent ID");
129
- });
130
-
131
- test("rejects invalid agent ID (non-integer)", async () => {
132
- await expect(
133
- setAgentUri({ agentId: "abc", uri: "https://example.com/agent.json", chain: "sepolia" }),
134
- ).rejects.toThrow("Invalid agent ID");
135
- });
136
-
137
- test("rejects invalid agent ID (float)", async () => {
138
- await expect(
139
- setAgentUri({ agentId: "1.5", uri: "https://example.com/agent.json", chain: "sepolia" }),
140
- ).rejects.toThrow("Invalid agent ID");
141
- });
142
-
143
- test("accepts agent ID 0 as valid", async () => {
144
- // Agent ID 0 passes validation — triggers a later error (browser+rpc-url conflict)
145
- // proving the agent ID check did not reject it
146
- await expect(
147
- setAgentUri({
148
- agentId: "0",
149
- uri: "https://example.com/agent.json",
150
- chain: "sepolia",
151
- browser: true,
152
- rpcUrl: "http://localhost:8545",
153
- }),
154
- ).rejects.toThrow("--rpc-url cannot be used with browser wallet");
155
- });
156
- });
@@ -1,154 +0,0 @@
1
- import { describe, expect, test, afterAll, beforeAll } from "bun:test";
2
- import { rmSync } from "fs";
3
- import { mkdir } from "node:fs/promises";
4
- import { validatePrivateKey, CliError, resolveUri } from "./utils";
5
- import { join } from "path";
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
- describe("resolveUri", () => {
81
- const testDir = join(import.meta.dir, "__test_fixtures__");
82
- const testJsonPath = join(testDir, "test-metadata.json");
83
- const testMetadata = { name: "Test Agent", description: "A test agent" };
84
-
85
- beforeAll(() => {
86
- // ensure test directory exists
87
- mkdir(testDir, { recursive: true });
88
- });
89
-
90
- afterAll(() => {
91
- // clean up test directory
92
- rmSync(testDir, { recursive: true, force: true });
93
- });
94
-
95
- test("returns ipfs:// URIs unchanged", () => {
96
- const uri = "ipfs://QmTest123";
97
- expect(resolveUri(uri)).toStrictEqual(uri);
98
- });
99
-
100
- test("returns https:// URIs unchanged", () => {
101
- const uri = "https://example.com/metadata.json";
102
- expect(resolveUri(uri)).toStrictEqual(uri);
103
- });
104
-
105
- test("returns http:// URIs unchanged", () => {
106
- const uri = "http://example.com/metadata.json";
107
- expect(resolveUri(uri)).toStrictEqual(uri);
108
- });
109
-
110
- test("converts .json file to base64 data URI", async () => {
111
- await Bun.write(testJsonPath, JSON.stringify(testMetadata));
112
-
113
- try {
114
- const result = resolveUri(testJsonPath);
115
- expect(result.startsWith("data:application/json;base64,")).toStrictEqual(true);
116
-
117
- // Decode and verify content
118
- const base64 = result.replace("data:application/json;base64,", "");
119
- const decoded = JSON.parse(Buffer.from(base64, "base64").toString("utf-8"));
120
- expect(decoded).toStrictEqual(testMetadata);
121
- } finally {
122
- await Bun.file(testJsonPath).unlink();
123
- }
124
- });
125
-
126
- test("throws for directory path", async () => {
127
- await mkdir(testDir, { recursive: true });
128
- const dirWithJsonSuffix = join(testDir, "not-a-file.json");
129
- await mkdir(dirWithJsonSuffix, { recursive: true });
130
-
131
- try {
132
- expect(() => resolveUri(dirWithJsonSuffix)).toThrow(CliError);
133
- expect(() => resolveUri(dirWithJsonSuffix)).toThrow("Not a file");
134
- } finally {
135
- rmSync(dirWithJsonSuffix, { recursive: true, force: true });
136
- }
137
- });
138
-
139
- test("throws for non-existent .json file", () => {
140
- expect(() => resolveUri("./non-existent.json")).toThrow(CliError);
141
- expect(() => resolveUri("./non-existent.json")).toThrow("File not found");
142
- });
143
-
144
- test("throws for invalid JSON content", async () => {
145
- await Bun.write(testJsonPath, "not valid json {{{");
146
-
147
- try {
148
- expect(() => resolveUri(testJsonPath)).toThrow(CliError);
149
- expect(() => resolveUri(testJsonPath)).toThrow("Invalid JSON");
150
- } finally {
151
- await Bun.file(testJsonPath).unlink();
152
- }
153
- });
154
- });
package/register/utils.ts DELETED
@@ -1,55 +0,0 @@
1
- import { existsSync, readFileSync, statSync } from "node:fs";
2
- import { resolve } from "node:path";
3
-
4
- export function resolveUri(uri: string): string {
5
- // Return as-is for URLs (ipfs://, https://, data:, etc.)
6
- if (uri.startsWith("http://") || uri.startsWith("https://") || uri.startsWith("ipfs://") || uri.startsWith("data:")) {
7
- return uri;
8
- }
9
-
10
- // Check if it's a file path (ends with .json or exists as a file)
11
- if (uri.endsWith(".json") || existsSync(uri)) {
12
- const filePath = resolve(uri);
13
-
14
- if (!existsSync(filePath)) {
15
- throw new CliError(`File not found: ${filePath}`);
16
- }
17
-
18
- if (!statSync(filePath).isFile()) {
19
- throw new CliError(`Not a file: ${filePath}`);
20
- }
21
-
22
- const content = readFileSync(filePath, "utf-8");
23
-
24
- // Validate it's valid JSON
25
- try {
26
- JSON.parse(content);
27
- } catch {
28
- throw new CliError(`Invalid JSON in file: ${filePath}`);
29
- }
30
-
31
- // Convert to base64 data URI
32
- const base64 = Buffer.from(content).toString("base64");
33
- return `data:application/json;base64,${base64}`;
34
- }
35
-
36
- // Return as-is for other URIs
37
- return uri;
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
- }