@aixyz/cli 0.6.0 → 0.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/bin.ts +175 -0
- package/build/AixyzConfigPlugin.ts +1 -1
- package/build/AixyzServerPlugin.ts +33 -9
- package/build/icons.ts +64 -0
- package/build/index.ts +11 -8
- package/dev/index.ts +1 -1
- package/package.json +12 -6
- package/register/README.md +101 -0
- package/register/index.ts +8 -0
- package/register/register.test.ts +75 -0
- package/register/register.ts +178 -0
- package/register/set-agent-uri.test.ts +156 -0
- package/register/set-agent-uri.ts +192 -0
- package/register/utils/chain.ts +57 -0
- package/register/utils/prompt.ts +18 -0
- package/register/utils/result.ts +14 -0
- package/register/utils/spinner.ts +39 -0
- package/register/utils/transaction.ts +94 -0
- package/register/utils.test.ts +154 -0
- package/register/utils.ts +55 -0
- package/register/wallet/browser.test.ts +106 -0
- package/register/wallet/browser.ts +748 -0
- package/register/wallet/index.test.ts +80 -0
- package/register/wallet/index.ts +84 -0
- package/register/wallet/keystore.test.ts +63 -0
- package/register/wallet/keystore.ts +33 -0
- package/register/wallet/privatekey.test.ts +30 -0
- package/register/wallet/privatekey.ts +14 -0
- package/register/wallet/sign.test.ts +78 -0
- package/register/wallet/sign.ts +109 -0
package/README.md
CHANGED
package/bin.ts
CHANGED
|
@@ -2,6 +2,9 @@
|
|
|
2
2
|
import { program } from "commander";
|
|
3
3
|
import { build } from "./build";
|
|
4
4
|
import { dev } from "./dev";
|
|
5
|
+
import { register } from "./register/register";
|
|
6
|
+
import { setAgentUri } from "./register/set-agent-uri";
|
|
7
|
+
import { CliError } from "./register/utils";
|
|
5
8
|
import pkg from "./package.json";
|
|
6
9
|
|
|
7
10
|
function handleAction(
|
|
@@ -11,6 +14,13 @@ function handleAction(
|
|
|
11
14
|
try {
|
|
12
15
|
await action(options);
|
|
13
16
|
} catch (error) {
|
|
17
|
+
if (error instanceof CliError) {
|
|
18
|
+
console.error(`Error: ${error.message}`);
|
|
19
|
+
process.exit(1);
|
|
20
|
+
}
|
|
21
|
+
if (error instanceof Error && error.name === "ExitPromptError") {
|
|
22
|
+
process.exit(130);
|
|
23
|
+
}
|
|
14
24
|
console.error(`Error: ${error instanceof Error ? error.message : String(error)}`);
|
|
15
25
|
process.exit(1);
|
|
16
26
|
}
|
|
@@ -60,4 +70,169 @@ Examples:
|
|
|
60
70
|
)
|
|
61
71
|
.action(handleAction(build));
|
|
62
72
|
|
|
73
|
+
const erc8004 = program.command("erc8004").description("ERC-8004 IdentityRegistry operations");
|
|
74
|
+
|
|
75
|
+
erc8004
|
|
76
|
+
.command("register")
|
|
77
|
+
.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)")
|
|
79
|
+
.option("--chain <chain>", "Target chain (mainnet, sepolia, base-sepolia, localhost)")
|
|
80
|
+
.option("--rpc-url <url>", "Custom RPC URL (uses default if not provided)")
|
|
81
|
+
.option("--registry <address>", "Contract address of the IdentityRegistry (required for localhost)")
|
|
82
|
+
.option("--keystore <path>", "Path to Ethereum keystore (V3) JSON file for local signing")
|
|
83
|
+
.option("--browser", "Use browser extension wallet (any extension)")
|
|
84
|
+
.option("--broadcast", "Sign and broadcast the transaction (default: dry-run)")
|
|
85
|
+
.option("--out-dir <path>", "Write deployment result as JSON to the given directory")
|
|
86
|
+
.addHelpText(
|
|
87
|
+
"after",
|
|
88
|
+
`
|
|
89
|
+
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.
|
|
96
|
+
|
|
97
|
+
--chain <chain>
|
|
98
|
+
Target chain for registration. Supported values:
|
|
99
|
+
mainnet Ethereum mainnet (chain ID 1)
|
|
100
|
+
sepolia Ethereum Sepolia testnet (chain ID 11155111)
|
|
101
|
+
base-sepolia Base Sepolia testnet (chain ID 84532)
|
|
102
|
+
localhost Local Foundry/Anvil node (chain ID 31337)
|
|
103
|
+
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
|
+
|
|
106
|
+
--rpc-url <url>
|
|
107
|
+
Custom RPC endpoint URL. Overrides the default RPC for the selected
|
|
108
|
+
chain. Cannot be used with --browser since the browser wallet manages
|
|
109
|
+
its own RPC connection.
|
|
110
|
+
|
|
111
|
+
--registry <address>
|
|
112
|
+
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.
|
|
115
|
+
|
|
116
|
+
--keystore <path>
|
|
117
|
+
Path to an Ethereum keystore (V3) JSON file. You will be prompted for
|
|
118
|
+
the keystore password to decrypt the private key for signing.
|
|
119
|
+
|
|
120
|
+
--browser
|
|
121
|
+
Opens a local page in your default browser for signing with any
|
|
122
|
+
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
|
+
|
|
126
|
+
--broadcast
|
|
127
|
+
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.
|
|
131
|
+
|
|
132
|
+
--out-dir <path>
|
|
133
|
+
Directory to write the deployment result as a JSON file. The file
|
|
134
|
+
is named registration-<chainId>-<timestamp>.json.
|
|
135
|
+
|
|
136
|
+
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.
|
|
140
|
+
|
|
141
|
+
Examples:
|
|
142
|
+
# Dry-run (default) — shows encoded transaction, no wallet needed
|
|
143
|
+
$ aixyz erc8004 register --uri "./metadata.json" --chain sepolia
|
|
144
|
+
|
|
145
|
+
# 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`,
|
|
150
|
+
)
|
|
151
|
+
.action(handleAction(register));
|
|
152
|
+
|
|
153
|
+
erc8004
|
|
154
|
+
.command("set-agent-uri")
|
|
155
|
+
.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)")
|
|
159
|
+
.option("--rpc-url <url>", "Custom RPC URL (uses default if not provided)")
|
|
160
|
+
.option("--registry <address>", "Contract address of the IdentityRegistry (required for localhost)")
|
|
161
|
+
.option("--keystore <path>", "Path to Ethereum keystore (V3) JSON file for local signing")
|
|
162
|
+
.option("--browser", "Use browser extension wallet (any extension)")
|
|
163
|
+
.option("--broadcast", "Sign and broadcast the transaction (default: dry-run)")
|
|
164
|
+
.option("--out-dir <path>", "Write result as JSON to the given directory")
|
|
165
|
+
.addHelpText(
|
|
166
|
+
"after",
|
|
167
|
+
`
|
|
168
|
+
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.
|
|
190
|
+
|
|
191
|
+
--rpc-url <url>
|
|
192
|
+
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.
|
|
195
|
+
|
|
196
|
+
--registry <address>
|
|
197
|
+
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.
|
|
200
|
+
|
|
201
|
+
--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.
|
|
204
|
+
|
|
205
|
+
--browser
|
|
206
|
+
Opens a local page in your default browser for signing with any
|
|
207
|
+
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
|
+
|
|
211
|
+
--broadcast
|
|
212
|
+
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.
|
|
216
|
+
|
|
217
|
+
--out-dir <path>
|
|
218
|
+
Directory to write the result as a JSON file. The file
|
|
219
|
+
is named set-agent-uri-<chainId>-<timestamp>.json.
|
|
220
|
+
|
|
221
|
+
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.
|
|
225
|
+
|
|
226
|
+
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
|
|
229
|
+
|
|
230
|
+
# 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`,
|
|
235
|
+
)
|
|
236
|
+
.action(handleAction(setAgentUri));
|
|
237
|
+
|
|
63
238
|
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: /
|
|
40
|
+
build.onLoad({ filter: /aixyz\/config\.ts$/ }, () => ({
|
|
41
41
|
contents: `
|
|
42
42
|
const config = ${JSON.stringify(materialized)};
|
|
43
43
|
export function getAixyzConfig() {
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { BunPlugin } from "bun";
|
|
2
2
|
import { existsSync, mkdirSync, readdirSync, writeFileSync } from "fs";
|
|
3
3
|
import { resolve, relative, basename, join } from "path";
|
|
4
|
+
import { getAixyzConfig } from "@aixyz/config";
|
|
4
5
|
|
|
5
6
|
export function AixyzServerPlugin(entrypoint: string, mode: "vercel" | "standalone"): BunPlugin {
|
|
6
7
|
return {
|
|
@@ -51,6 +52,17 @@ export function getEntrypointMayGenerate(cwd: string, mode: "dev" | "build"): st
|
|
|
51
52
|
return entrypoint;
|
|
52
53
|
}
|
|
53
54
|
|
|
55
|
+
class AixyzGlob {
|
|
56
|
+
constructor(readonly config = getAixyzConfig()) {}
|
|
57
|
+
|
|
58
|
+
includes(file: string): boolean {
|
|
59
|
+
const included = this.config.build.includes.some((pattern) => new Bun.Glob(pattern).match(file));
|
|
60
|
+
if (!included) return false;
|
|
61
|
+
const excluded = this.config.build.excludes.some((pattern) => new Bun.Glob(pattern).match(file));
|
|
62
|
+
return !excluded;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
54
66
|
/**
|
|
55
67
|
* Generate server.ts content by scanning the app directory for agent.ts and tools/.
|
|
56
68
|
*
|
|
@@ -58,6 +70,7 @@ export function getEntrypointMayGenerate(cwd: string, mode: "dev" | "build"): st
|
|
|
58
70
|
* @param entrypointDir - Directory where the generated file will live (for computing relative imports).
|
|
59
71
|
*/
|
|
60
72
|
function generateServer(appDir: string, entrypointDir: string): string {
|
|
73
|
+
const glob = new AixyzGlob();
|
|
61
74
|
const rel = relative(entrypointDir, appDir);
|
|
62
75
|
const importPrefix = rel === "" ? "." : rel.startsWith(".") ? rel : `./${rel}`;
|
|
63
76
|
|
|
@@ -73,22 +86,20 @@ function generateServer(appDir: string, entrypointDir: string): string {
|
|
|
73
86
|
imports.push('import { facilitator } from "aixyz/accepts";');
|
|
74
87
|
}
|
|
75
88
|
|
|
76
|
-
const hasAgent = existsSync(resolve(appDir, "agent.ts"));
|
|
89
|
+
const hasAgent = existsSync(resolve(appDir, "agent.ts")) && glob.includes("agent.ts");
|
|
77
90
|
if (hasAgent) {
|
|
78
91
|
imports.push('import { useA2A } from "aixyz/server/adapters/a2a";');
|
|
79
92
|
imports.push(`import * as agent from "${importPrefix}/agent";`);
|
|
80
93
|
}
|
|
81
94
|
|
|
82
95
|
const toolsDir = resolve(appDir, "tools");
|
|
83
|
-
const tools: { name: string }[] = [];
|
|
96
|
+
const tools: { name: string; identifier: string }[] = [];
|
|
84
97
|
if (existsSync(toolsDir)) {
|
|
85
98
|
for (const file of readdirSync(toolsDir)) {
|
|
86
|
-
if (
|
|
99
|
+
if (glob.includes(`tools/${file}`)) {
|
|
87
100
|
const name = basename(file, ".ts");
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
tools.push({ name });
|
|
91
|
-
}
|
|
101
|
+
const identifier = toIdentifier(name);
|
|
102
|
+
tools.push({ name, identifier });
|
|
92
103
|
}
|
|
93
104
|
}
|
|
94
105
|
}
|
|
@@ -96,7 +107,7 @@ function generateServer(appDir: string, entrypointDir: string): string {
|
|
|
96
107
|
if (tools.length > 0) {
|
|
97
108
|
imports.push('import { AixyzMCP } from "aixyz/server/adapters/mcp";');
|
|
98
109
|
for (const tool of tools) {
|
|
99
|
-
imports.push(`import * as ${tool.
|
|
110
|
+
imports.push(`import * as ${tool.identifier} from "${importPrefix}/tools/${tool.name}";`);
|
|
100
111
|
}
|
|
101
112
|
}
|
|
102
113
|
|
|
@@ -111,7 +122,7 @@ function generateServer(appDir: string, entrypointDir: string): string {
|
|
|
111
122
|
if (tools.length > 0) {
|
|
112
123
|
body.push("const mcp = new AixyzMCP(server);");
|
|
113
124
|
for (const tool of tools) {
|
|
114
|
-
body.push(`await mcp.register("${tool.name}", ${tool.
|
|
125
|
+
body.push(`await mcp.register("${tool.name}", ${tool.identifier});`);
|
|
115
126
|
}
|
|
116
127
|
body.push("await mcp.connect();");
|
|
117
128
|
}
|
|
@@ -120,3 +131,16 @@ function generateServer(appDir: string, entrypointDir: string): string {
|
|
|
120
131
|
|
|
121
132
|
return [...imports, "", ...body].join("\n");
|
|
122
133
|
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Convert a kebab-case filename into a valid JS identifier.
|
|
137
|
+
*
|
|
138
|
+
* Examples:
|
|
139
|
+
* "lookup" → "lookup"
|
|
140
|
+
* "get-aggregator-v3-address" → "getAggregatorV3Address"
|
|
141
|
+
* "3d-model" → "_3dModel"
|
|
142
|
+
*/
|
|
143
|
+
function toIdentifier(name: string): string {
|
|
144
|
+
const camel = name.replace(/-(.)/g, (_, c: string) => c.toUpperCase()).replace(/[^a-zA-Z0-9_$]/g, "_");
|
|
145
|
+
return /^\d/.test(camel) ? `_${camel}` : camel;
|
|
146
|
+
}
|
package/build/icons.ts
ADDED
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import sharp from "sharp";
|
|
2
|
+
import { cpSync, existsSync, mkdirSync, writeFileSync } from "fs";
|
|
3
|
+
import { dirname, resolve } from "path";
|
|
4
|
+
|
|
5
|
+
/** Icon source file extensions to look for, in priority order */
|
|
6
|
+
const ICON_EXTENSIONS = ["svg", "png", "jpeg", "jpg"] as const;
|
|
7
|
+
|
|
8
|
+
/** Returns the first matching icon file path, or null if none found. */
|
|
9
|
+
export function findIconFile(appDir: string): string | null {
|
|
10
|
+
for (const ext of ICON_EXTENSIONS) {
|
|
11
|
+
const iconPath = resolve(appDir, `icon.${ext}`);
|
|
12
|
+
if (existsSync(iconPath)) return iconPath;
|
|
13
|
+
}
|
|
14
|
+
return null;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Copy the icon to destPath as icon.png.
|
|
19
|
+
* PNG sources are copied directly; other formats are converted via sharp.
|
|
20
|
+
*/
|
|
21
|
+
export async function copyAgentIcon(iconPath: string, destPath: string): Promise<void> {
|
|
22
|
+
if (iconPath.endsWith(".png")) {
|
|
23
|
+
cpSync(iconPath, destPath);
|
|
24
|
+
} else {
|
|
25
|
+
await sharp(iconPath).png().toFile(destPath);
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Generate a favicon.ico at destPath from the given icon source.
|
|
31
|
+
* Uses sharp to produce a 32×32 PNG buffer, then wraps it in an ICO container.
|
|
32
|
+
* Modern browsers support ICO files with embedded PNG data.
|
|
33
|
+
*/
|
|
34
|
+
export async function generateFavicon(iconPath: string, destPath: string): Promise<void> {
|
|
35
|
+
const pngData = await sharp(iconPath)
|
|
36
|
+
.resize(32, 32, { fit: "contain", background: { r: 0, g: 0, b: 0, alpha: 0 } })
|
|
37
|
+
.png()
|
|
38
|
+
.toBuffer();
|
|
39
|
+
|
|
40
|
+
mkdirSync(dirname(destPath), { recursive: true });
|
|
41
|
+
writeFileSync(destPath, buildIco(pngData, 32, 32));
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** Build a single-image ICO buffer with embedded PNG data. */
|
|
45
|
+
function buildIco(pngData: Buffer, width: number, height: number): Buffer {
|
|
46
|
+
// ICONDIR header (6 bytes)
|
|
47
|
+
const header = Buffer.alloc(6);
|
|
48
|
+
header.writeUInt16LE(0, 0); // reserved
|
|
49
|
+
header.writeUInt16LE(1, 2); // type: 1 = ICO
|
|
50
|
+
header.writeUInt16LE(1, 4); // image count
|
|
51
|
+
|
|
52
|
+
// ICONDIRENTRY (16 bytes); image data starts at offset 6 + 16 = 22
|
|
53
|
+
const entry = Buffer.alloc(16);
|
|
54
|
+
entry.writeUInt8(width === 256 ? 0 : width, 0); // width (0 means 256)
|
|
55
|
+
entry.writeUInt8(height === 256 ? 0 : height, 1); // height (0 means 256)
|
|
56
|
+
entry.writeUInt8(0, 2); // color count (0 = true color)
|
|
57
|
+
entry.writeUInt8(0, 3); // reserved
|
|
58
|
+
entry.writeUInt16LE(1, 4); // planes
|
|
59
|
+
entry.writeUInt16LE(32, 6); // bits per pixel
|
|
60
|
+
entry.writeUInt32LE(pngData.length, 8); // size of image data
|
|
61
|
+
entry.writeUInt32LE(22, 12); // offset to image data
|
|
62
|
+
|
|
63
|
+
return Buffer.concat([header, entry, pngData]);
|
|
64
|
+
}
|
package/build/index.ts
CHANGED
|
@@ -2,6 +2,7 @@ import { resolve } from "path";
|
|
|
2
2
|
import { existsSync, mkdirSync, cpSync, rmSync } from "fs";
|
|
3
3
|
import { AixyzConfigPlugin } from "./AixyzConfigPlugin";
|
|
4
4
|
import { AixyzServerPlugin, getEntrypointMayGenerate } from "./AixyzServerPlugin";
|
|
5
|
+
import { findIconFile, copyAgentIcon, generateFavicon } from "./icons";
|
|
5
6
|
import { getAixyzConfig } from "@aixyz/config";
|
|
6
7
|
import { loadEnvConfig } from "@next/env";
|
|
7
8
|
import chalk from "chalk";
|
|
@@ -65,9 +66,10 @@ async function buildBun(entrypoint: string): Promise<void> {
|
|
|
65
66
|
console.log("Copied public/ →", destPublicDir);
|
|
66
67
|
}
|
|
67
68
|
|
|
68
|
-
const iconFile = resolve(cwd, "app
|
|
69
|
-
if (
|
|
70
|
-
|
|
69
|
+
const iconFile = findIconFile(resolve(cwd, "app"));
|
|
70
|
+
if (iconFile) {
|
|
71
|
+
await copyAgentIcon(iconFile, resolve(outputDir, "icon.png"));
|
|
72
|
+
await generateFavicon(iconFile, resolve(outputDir, "public/favicon.ico"));
|
|
71
73
|
}
|
|
72
74
|
|
|
73
75
|
// Log summary
|
|
@@ -75,7 +77,7 @@ async function buildBun(entrypoint: string): Promise<void> {
|
|
|
75
77
|
console.log("Build complete! Output:");
|
|
76
78
|
console.log(" .aixyz/output/server.js");
|
|
77
79
|
console.log(" .aixyz/output/package.json");
|
|
78
|
-
if (existsSync(publicDir) ||
|
|
80
|
+
if (existsSync(publicDir) || iconFile) {
|
|
79
81
|
console.log(" .aixyz/output/public/ and assets");
|
|
80
82
|
}
|
|
81
83
|
console.log("");
|
|
@@ -151,11 +153,12 @@ async function buildVercel(entrypoint: string): Promise<void> {
|
|
|
151
153
|
console.log("Copied public/ →", staticDir);
|
|
152
154
|
}
|
|
153
155
|
|
|
154
|
-
const iconFile = resolve(cwd, "app
|
|
155
|
-
if (
|
|
156
|
+
const iconFile = findIconFile(resolve(cwd, "app"));
|
|
157
|
+
if (iconFile) {
|
|
156
158
|
mkdirSync(staticDir, { recursive: true });
|
|
157
|
-
|
|
158
|
-
|
|
159
|
+
await copyAgentIcon(iconFile, resolve(staticDir, "icon.png"));
|
|
160
|
+
await generateFavicon(iconFile, resolve(staticDir, "favicon.ico"));
|
|
161
|
+
console.log("Copied app/icon →", staticDir);
|
|
159
162
|
}
|
|
160
163
|
|
|
161
164
|
// Log summary
|
package/dev/index.ts
CHANGED
|
@@ -15,7 +15,7 @@ export async function dev(options: { port?: string }): Promise<void> {
|
|
|
15
15
|
const baseUrl = `http://localhost:${port}`;
|
|
16
16
|
|
|
17
17
|
console.log("");
|
|
18
|
-
console.log(`⟡
|
|
18
|
+
console.log(`⟡ aixyz.sh v${pkg.version}`);
|
|
19
19
|
console.log("");
|
|
20
20
|
console.log(`- A2A: ${baseUrl}/.well-known/agent-card.json`);
|
|
21
21
|
console.log(`- MCP: ${baseUrl}/mcp`);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@aixyz/cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.8.0",
|
|
4
4
|
"description": "Payment-native SDK for AI Agent",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"ai",
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
"agent",
|
|
9
9
|
"aixyz"
|
|
10
10
|
],
|
|
11
|
-
"homepage": "https://
|
|
11
|
+
"homepage": "https://aixyz.sh",
|
|
12
12
|
"bugs": "https://github.com/AgentlyHQ/aixyz/issues",
|
|
13
13
|
"repository": {
|
|
14
14
|
"type": "git",
|
|
@@ -23,14 +23,20 @@
|
|
|
23
23
|
"files": [
|
|
24
24
|
"build",
|
|
25
25
|
"dev",
|
|
26
|
+
"register",
|
|
26
27
|
"bin.ts"
|
|
27
28
|
],
|
|
28
29
|
"dependencies": {
|
|
29
|
-
"@aixyz/config": "0.
|
|
30
|
+
"@aixyz/config": "0.8.0",
|
|
31
|
+
"@aixyz/erc-8004": "0.8.0",
|
|
32
|
+
"@inquirer/prompts": "^8.3.0",
|
|
30
33
|
"@next/env": "^16.1.6",
|
|
31
|
-
"boxen": "^8.0.
|
|
32
|
-
"chalk": "^5.
|
|
33
|
-
"commander": "^14.0.3"
|
|
34
|
+
"boxen": "^8.0.1",
|
|
35
|
+
"chalk": "^5.6.2",
|
|
36
|
+
"commander": "^14.0.3",
|
|
37
|
+
"ethers": "^6.16.0",
|
|
38
|
+
"sharp": "^0.34.5",
|
|
39
|
+
"viem": "^2.46.3"
|
|
34
40
|
},
|
|
35
41
|
"engines": {
|
|
36
42
|
"bun": ">=1.3.0"
|
|
@@ -0,0 +1,101 @@
|
|
|
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 |
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import { describe, expect, test } from "bun:test";
|
|
2
|
+
import { CHAIN_ID, getIdentityRegistryAddress } from "@aixyz/erc-8004";
|
|
3
|
+
import { register } from "./register";
|
|
4
|
+
|
|
5
|
+
describe("register 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("register command validation", () => {
|
|
30
|
+
test("supported chains list includes sepolia", () => {
|
|
31
|
+
const CHAINS: Record<string, { chainId: number }> = {
|
|
32
|
+
sepolia: { chainId: CHAIN_ID.SEPOLIA },
|
|
33
|
+
"base-sepolia": { chainId: CHAIN_ID.BASE_SEPOLIA },
|
|
34
|
+
};
|
|
35
|
+
expect(CHAINS["sepolia"]).toBeDefined();
|
|
36
|
+
expect(CHAINS["sepolia"].chainId).toStrictEqual(CHAIN_ID.SEPOLIA);
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
test("supported chains list includes base-sepolia", () => {
|
|
40
|
+
const CHAINS: Record<string, { chainId: number }> = {
|
|
41
|
+
sepolia: { chainId: CHAIN_ID.SEPOLIA },
|
|
42
|
+
"base-sepolia": { chainId: CHAIN_ID.BASE_SEPOLIA },
|
|
43
|
+
localhost: { chainId: 31337 },
|
|
44
|
+
};
|
|
45
|
+
expect(CHAINS["base-sepolia"]).toBeDefined();
|
|
46
|
+
expect(CHAINS["base-sepolia"].chainId).toStrictEqual(CHAIN_ID.BASE_SEPOLIA);
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
test("supported chains list includes localhost with chainId 31337", () => {
|
|
50
|
+
const CHAINS: Record<string, { chainId: number }> = {
|
|
51
|
+
sepolia: { chainId: CHAIN_ID.SEPOLIA },
|
|
52
|
+
"base-sepolia": { chainId: CHAIN_ID.BASE_SEPOLIA },
|
|
53
|
+
localhost: { chainId: 31337 },
|
|
54
|
+
};
|
|
55
|
+
expect(CHAINS["localhost"]).toBeDefined();
|
|
56
|
+
expect(CHAINS["localhost"].chainId).toStrictEqual(31337);
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
test("unsupported chain is not in list", () => {
|
|
60
|
+
const CHAINS: Record<string, { chainId: number }> = {
|
|
61
|
+
sepolia: { chainId: CHAIN_ID.SEPOLIA },
|
|
62
|
+
"base-sepolia": { chainId: CHAIN_ID.BASE_SEPOLIA },
|
|
63
|
+
localhost: { chainId: 31337 },
|
|
64
|
+
};
|
|
65
|
+
expect(CHAINS["mainnet"]).toBeUndefined();
|
|
66
|
+
});
|
|
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
|
+
});
|