@aixyz/cli 0.11.0 → 0.12.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/build/AixyzServerPlugin.ts +30 -5
- package/build/index.ts +68 -3
- package/package.json +3 -3
- package/register/index.ts +24 -1
- package/register/register.ts +20 -8
- package/register/update.test.ts +119 -1
- package/register/update.ts +22 -8
- package/register/utils/chain.ts +22 -19
- package/register/utils/prompt.ts +84 -37
- package/register/wallet/index.ts +31 -28
|
@@ -3,7 +3,7 @@ import { existsSync, mkdirSync, readdirSync, writeFileSync } from "fs";
|
|
|
3
3
|
import { resolve, relative, basename, join } from "path";
|
|
4
4
|
import { getAixyzConfig } from "@aixyz/config";
|
|
5
5
|
|
|
6
|
-
export function AixyzServerPlugin(entrypoint: string, mode: "vercel" | "standalone"): BunPlugin {
|
|
6
|
+
export function AixyzServerPlugin(entrypoint: string, mode: "vercel" | "standalone" | "executable"): BunPlugin {
|
|
7
7
|
return {
|
|
8
8
|
name: "aixyz-entrypoint",
|
|
9
9
|
setup(build) {
|
|
@@ -17,7 +17,7 @@ export function AixyzServerPlugin(entrypoint: string, mode: "vercel" | "standalo
|
|
|
17
17
|
const transformed = source.replace(/export\s+default\s+(\w+)\s*;/, "export default $1.express;");
|
|
18
18
|
return { contents: transformed, loader: "ts" };
|
|
19
19
|
} else {
|
|
20
|
-
// For standalone, keep the server export but add startup code
|
|
20
|
+
// For standalone and executable, keep the server export but add startup code
|
|
21
21
|
// TODO(@fuxingloh): use Bun.serve later.
|
|
22
22
|
const transformed = source.replace(
|
|
23
23
|
/export\s+default\s+(\w+)\s*;/,
|
|
@@ -64,9 +64,9 @@ class AixyzGlob {
|
|
|
64
64
|
}
|
|
65
65
|
|
|
66
66
|
/**
|
|
67
|
-
* Generate server.ts content by scanning the app directory for agent.ts and tools/.
|
|
67
|
+
* Generate server.ts content by scanning the app directory for agent.ts, agents/, and tools/.
|
|
68
68
|
*
|
|
69
|
-
* @param appDir - The app directory containing agent.ts and tools/
|
|
69
|
+
* @param appDir - The app directory containing agent.ts, agents/, and tools/
|
|
70
70
|
* @param entrypointDir - Directory where the generated file will live (for computing relative imports).
|
|
71
71
|
*/
|
|
72
72
|
function generateServer(appDir: string, entrypointDir: string): string {
|
|
@@ -92,6 +92,25 @@ function generateServer(appDir: string, entrypointDir: string): string {
|
|
|
92
92
|
imports.push(`import * as agent from "${importPrefix}/agent";`);
|
|
93
93
|
}
|
|
94
94
|
|
|
95
|
+
const agentsDir = resolve(appDir, "agents");
|
|
96
|
+
const subAgents: { name: string; identifier: string }[] = [];
|
|
97
|
+
if (existsSync(agentsDir)) {
|
|
98
|
+
for (const file of readdirSync(agentsDir)) {
|
|
99
|
+
if (glob.includes(`agents/${file}`)) {
|
|
100
|
+
const name = basename(file, ".ts");
|
|
101
|
+
const identifier = toIdentifier(name);
|
|
102
|
+
subAgents.push({ name, identifier });
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
if (!hasAgent && subAgents.length > 0) {
|
|
108
|
+
imports.push('import { useA2A } from "aixyz/server/adapters/a2a";');
|
|
109
|
+
}
|
|
110
|
+
for (const subAgent of subAgents) {
|
|
111
|
+
imports.push(`import * as ${subAgent.identifier} from "${importPrefix}/agents/${subAgent.name}";`);
|
|
112
|
+
}
|
|
113
|
+
|
|
95
114
|
const toolsDir = resolve(appDir, "tools");
|
|
96
115
|
const tools: { name: string; identifier: string }[] = [];
|
|
97
116
|
if (existsSync(toolsDir)) {
|
|
@@ -119,6 +138,9 @@ function generateServer(appDir: string, entrypointDir: string): string {
|
|
|
119
138
|
body.push("useA2A(server, agent);");
|
|
120
139
|
}
|
|
121
140
|
|
|
141
|
+
for (const subAgent of subAgents) {
|
|
142
|
+
body.push(`useA2A(server, ${subAgent.identifier}, "${subAgent.name}");`);
|
|
143
|
+
}
|
|
122
144
|
if (tools.length > 0) {
|
|
123
145
|
body.push("const mcp = new AixyzMCP(server);");
|
|
124
146
|
for (const tool of tools) {
|
|
@@ -132,8 +154,11 @@ function generateServer(appDir: string, entrypointDir: string): string {
|
|
|
132
154
|
if (hasErc8004) {
|
|
133
155
|
imports.push('import { useERC8004 } from "aixyz/server/adapters/erc-8004";');
|
|
134
156
|
imports.push(`import * as erc8004 from "${importPrefix}/erc-8004";`);
|
|
157
|
+
const a2aPaths: string[] = [];
|
|
158
|
+
if (hasAgent) a2aPaths.push("/.well-known/agent-card.json");
|
|
159
|
+
for (const subAgent of subAgents) a2aPaths.push(`/${subAgent.name}/.well-known/agent-card.json`);
|
|
135
160
|
body.push(
|
|
136
|
-
`useERC8004(server, { default: erc8004.default, options: { mcp: ${tools.length > 0}, a2a: ${
|
|
161
|
+
`useERC8004(server, { default: erc8004.default, options: { mcp: ${tools.length > 0}, a2a: ${JSON.stringify(a2aPaths)} } });`,
|
|
137
162
|
);
|
|
138
163
|
}
|
|
139
164
|
|
package/build/index.ts
CHANGED
|
@@ -14,7 +14,7 @@ interface BuildOptions {
|
|
|
14
14
|
|
|
15
15
|
export const buildCommand = new Command("build")
|
|
16
16
|
.description("Build the aixyz agent")
|
|
17
|
-
.option("--output <type>", "Output format: 'standalone' or '
|
|
17
|
+
.option("--output <type>", "Output format: 'standalone', 'vercel', or 'executable'")
|
|
18
18
|
.addHelpText(
|
|
19
19
|
"after",
|
|
20
20
|
`
|
|
@@ -28,6 +28,10 @@ Details:
|
|
|
28
28
|
Generates Vercel Build Output API v3 structure at .vercel/output/
|
|
29
29
|
(Automatically detected when deploying to Vercel)
|
|
30
30
|
|
|
31
|
+
With --output executable:
|
|
32
|
+
Compiles into a self-contained binary at ./.aixyz/output/server
|
|
33
|
+
(No Bun runtime required to run the output)
|
|
34
|
+
|
|
31
35
|
The build process:
|
|
32
36
|
1. Loads aixyz.config.ts from the current directory
|
|
33
37
|
2. Detects entrypoint (app/server.ts or auto-generates from app/agent.ts + app/tools/)
|
|
@@ -42,6 +46,7 @@ Examples:
|
|
|
42
46
|
$ aixyz build # Build standalone (default)
|
|
43
47
|
$ aixyz build --output standalone # Build standalone explicitly
|
|
44
48
|
$ aixyz build --output vercel # Build for Vercel deployment
|
|
49
|
+
$ aixyz build --output executable # Build self-contained binary
|
|
45
50
|
$ VERCEL=1 aixyz build # Auto-detected Vercel build`,
|
|
46
51
|
)
|
|
47
52
|
.action(action);
|
|
@@ -59,7 +64,10 @@ async function action(options: BuildOptions = {}): Promise<void> {
|
|
|
59
64
|
|
|
60
65
|
if (target === "vercel") {
|
|
61
66
|
console.log(chalk.cyan("▶") + " Building for " + chalk.bold("Vercel") + "...");
|
|
62
|
-
await buildVercel(entrypoint);
|
|
67
|
+
await buildVercel(entrypoint, config);
|
|
68
|
+
} else if (target === "executable") {
|
|
69
|
+
console.log(chalk.cyan("▶") + " Building " + chalk.bold("Executable") + "...");
|
|
70
|
+
await buildExecutable(entrypoint);
|
|
63
71
|
} else {
|
|
64
72
|
console.log(chalk.cyan("▶") + " Building for " + chalk.bold("Standalone") + "...");
|
|
65
73
|
await buildBun(entrypoint);
|
|
@@ -125,7 +133,63 @@ async function buildBun(entrypoint: string): Promise<void> {
|
|
|
125
133
|
console.log("To run: bun .aixyz/output/server.js");
|
|
126
134
|
}
|
|
127
135
|
|
|
128
|
-
async function
|
|
136
|
+
async function buildExecutable(entrypoint: string): Promise<void> {
|
|
137
|
+
const cwd = process.cwd();
|
|
138
|
+
|
|
139
|
+
const outputDir = resolve(cwd, ".aixyz/output");
|
|
140
|
+
rmSync(outputDir, { recursive: true, force: true });
|
|
141
|
+
mkdirSync(outputDir, { recursive: true });
|
|
142
|
+
|
|
143
|
+
const outfile = resolve(outputDir, "server");
|
|
144
|
+
|
|
145
|
+
// Build as a self-contained compiled binary using Bun's compile feature
|
|
146
|
+
const result = await Bun.build({
|
|
147
|
+
entrypoints: [entrypoint],
|
|
148
|
+
outdir: outputDir,
|
|
149
|
+
target: "bun",
|
|
150
|
+
sourcemap: "linked",
|
|
151
|
+
compile: { outfile },
|
|
152
|
+
define: {
|
|
153
|
+
"process.env.NODE_ENV": JSON.stringify("production"),
|
|
154
|
+
"process.env.AIXYZ_ENV": JSON.stringify("production"),
|
|
155
|
+
},
|
|
156
|
+
plugins: [AixyzConfigPlugin(), AixyzServerPlugin(entrypoint, "executable")],
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
if (!result.success) {
|
|
160
|
+
console.error("Build failed:");
|
|
161
|
+
for (const log of result.logs) {
|
|
162
|
+
console.error(log);
|
|
163
|
+
}
|
|
164
|
+
process.exit(1);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
// Copy static assets (public/ → .aixyz/output/public/)
|
|
168
|
+
const publicDir = resolve(cwd, "public");
|
|
169
|
+
if (existsSync(publicDir)) {
|
|
170
|
+
const destPublicDir = resolve(outputDir, "public");
|
|
171
|
+
cpSync(publicDir, destPublicDir, { recursive: true });
|
|
172
|
+
console.log("Copied public/ →", destPublicDir);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
const iconFile = findIconFile(resolve(cwd, "app"));
|
|
176
|
+
if (iconFile) {
|
|
177
|
+
await copyAgentIcon(iconFile, resolve(outputDir, "icon.png"));
|
|
178
|
+
await generateFavicon(iconFile, resolve(outputDir, "public/favicon.ico"));
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// Log summary
|
|
182
|
+
console.log("");
|
|
183
|
+
console.log("Build complete! Output:");
|
|
184
|
+
console.log(" .aixyz/output/server");
|
|
185
|
+
if (existsSync(publicDir) || iconFile) {
|
|
186
|
+
console.log(" .aixyz/output/public/ and assets");
|
|
187
|
+
}
|
|
188
|
+
console.log("");
|
|
189
|
+
console.log("To run: ./.aixyz/output/server");
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
async function buildVercel(entrypoint: string, config: ReturnType<typeof getAixyzConfig>): Promise<void> {
|
|
129
193
|
const cwd = process.cwd();
|
|
130
194
|
|
|
131
195
|
const outputDir = resolve(cwd, ".vercel/output");
|
|
@@ -165,6 +229,7 @@ async function buildVercel(entrypoint: string): Promise<void> {
|
|
|
165
229
|
handler: "server.js",
|
|
166
230
|
runtime: "bun1.x",
|
|
167
231
|
launcherType: "Bun",
|
|
232
|
+
maxDuration: config.vercel.maxDuration,
|
|
168
233
|
shouldAddHelpers: true,
|
|
169
234
|
shouldAddSourcemapSupport: true,
|
|
170
235
|
},
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@aixyz/cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.12.0",
|
|
4
4
|
"description": "Payment-native SDK for AI Agent",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"ai",
|
|
@@ -27,8 +27,8 @@
|
|
|
27
27
|
"bin.ts"
|
|
28
28
|
],
|
|
29
29
|
"dependencies": {
|
|
30
|
-
"@aixyz/config": "0.
|
|
31
|
-
"@aixyz/erc-8004": "0.
|
|
30
|
+
"@aixyz/config": "0.12.0",
|
|
31
|
+
"@aixyz/erc-8004": "0.12.0",
|
|
32
32
|
"@inquirer/prompts": "^8.3.0",
|
|
33
33
|
"@next/env": "^16.1.6",
|
|
34
34
|
"boxen": "^8.0.1",
|
package/register/index.ts
CHANGED
|
@@ -22,6 +22,10 @@ erc8004Command
|
|
|
22
22
|
.option("--browser", "Use browser extension wallet (any extension)")
|
|
23
23
|
.option("--broadcast", "Sign and broadcast the transaction (default: dry-run)")
|
|
24
24
|
.option("--out-dir <path>", "Write deployment result as JSON to the given directory")
|
|
25
|
+
.option(
|
|
26
|
+
"--supported-trust <values>",
|
|
27
|
+
'Comma-separated supported trust mechanisms (e.g., "reputation,tee-attestation"). If omitted and app/erc-8004.ts does not exist, you will be prompted interactively.',
|
|
28
|
+
)
|
|
25
29
|
.addHelpText(
|
|
26
30
|
"after",
|
|
27
31
|
`
|
|
@@ -64,6 +68,13 @@ Option Details:
|
|
|
64
68
|
--out-dir <path>
|
|
65
69
|
Directory to write the deployment result as a JSON file.
|
|
66
70
|
|
|
71
|
+
--supported-trust <values>
|
|
72
|
+
Comma-separated list of trust mechanisms to declare in app/erc-8004.ts
|
|
73
|
+
when it does not yet exist. Valid values: reputation, crypto-economic,
|
|
74
|
+
tee-attestation, social, governance.
|
|
75
|
+
Example: --supported-trust "reputation,tee-attestation"
|
|
76
|
+
If omitted, you will be prompted to select interactively.
|
|
77
|
+
|
|
67
78
|
Behavior:
|
|
68
79
|
If app/erc-8004.ts does not exist, you will be prompted to create it
|
|
69
80
|
(selecting supported trust mechanisms). After a successful on-chain
|
|
@@ -80,6 +91,9 @@ Examples:
|
|
|
80
91
|
$ aixyz erc-8004 register --url "https://my-agent.example.com" --chain-id 84532 --keystore ~/.foundry/keystores/default --broadcast
|
|
81
92
|
$ aixyz erc-8004 register --url "https://my-agent.example.com" --chain-id 84532 --browser --broadcast
|
|
82
93
|
|
|
94
|
+
# Non-interactive (CI-friendly)
|
|
95
|
+
$ aixyz erc-8004 register --url "https://my-agent.example.com" --chain-id 84532 --supported-trust "reputation,tee-attestation" --keystore ~/.foundry/keystores/default --broadcast
|
|
96
|
+
|
|
83
97
|
# BYO: register on any custom EVM chain
|
|
84
98
|
$ aixyz erc-8004 register --url "https://my-agent.example.com" --chain-id 999999 --rpc-url https://my-rpc.example.com --registry 0xABCD... --broadcast`,
|
|
85
99
|
)
|
|
@@ -89,6 +103,7 @@ erc8004Command
|
|
|
89
103
|
.command("update")
|
|
90
104
|
.description("Update the metadata URI of a registered agent")
|
|
91
105
|
.option("--url <url>", "New agent deployment URL (e.g., https://my-agent.example.com)")
|
|
106
|
+
.option("--agent-id <id>", "Agent ID to update (selects which registration from app/erc-8004.ts)", parseInt)
|
|
92
107
|
.option("--rpc-url <url>", "Custom RPC URL (uses default if not provided)")
|
|
93
108
|
.option("--registry <address>", "Contract address of the IdentityRegistry (required for localhost)")
|
|
94
109
|
.option("--keystore <path>", "Path to Ethereum keystore (V3) JSON file for local signing")
|
|
@@ -104,6 +119,11 @@ Option Details:
|
|
|
104
119
|
The URI will be derived as <url>/_aixyz/erc-8004.json.
|
|
105
120
|
If omitted, you will be prompted to enter the URL interactively.
|
|
106
121
|
|
|
122
|
+
--agent-id <id>
|
|
123
|
+
Agent ID (numeric) to select which registration from app/erc-8004.ts
|
|
124
|
+
to update. Required in non-interactive (non-TTY) mode when multiple
|
|
125
|
+
registrations exist.
|
|
126
|
+
|
|
107
127
|
--rpc-url <url>
|
|
108
128
|
Custom RPC endpoint URL. Overrides the default RPC for the selected
|
|
109
129
|
chain. Cannot be used with --browser.
|
|
@@ -141,6 +161,9 @@ Examples:
|
|
|
141
161
|
|
|
142
162
|
# Sign and broadcast
|
|
143
163
|
$ aixyz erc-8004 update --url "https://new-domain.example.com" --keystore ~/.foundry/keystores/default --broadcast
|
|
144
|
-
$ aixyz erc-8004 update --url "https://new-domain.example.com" --browser --broadcast
|
|
164
|
+
$ aixyz erc-8004 update --url "https://new-domain.example.com" --browser --broadcast
|
|
165
|
+
|
|
166
|
+
# Non-interactive (CI-friendly)
|
|
167
|
+
$ aixyz erc-8004 update --url "https://new-domain.example.com" --agent-id 42 --keystore ~/.foundry/keystores/default --broadcast`,
|
|
145
168
|
)
|
|
146
169
|
.action(update);
|
package/register/register.ts
CHANGED
|
@@ -12,7 +12,14 @@ import {
|
|
|
12
12
|
} from "./utils/chain";
|
|
13
13
|
import { writeResultJson } from "./utils/result";
|
|
14
14
|
import { label, truncateUri, broadcastAndConfirm, logSignResult } from "./utils/transaction";
|
|
15
|
-
import {
|
|
15
|
+
import {
|
|
16
|
+
promptAgentUrl,
|
|
17
|
+
promptSupportedTrust,
|
|
18
|
+
promptRegistryAddress,
|
|
19
|
+
deriveAgentUri,
|
|
20
|
+
isTTY,
|
|
21
|
+
parseSupportedTrust,
|
|
22
|
+
} from "./utils/prompt";
|
|
16
23
|
import { hasErc8004File, createErc8004File, writeRegistrationEntry } from "./utils/erc8004-file";
|
|
17
24
|
import { confirm } from "@inquirer/prompts";
|
|
18
25
|
import chalk from "chalk";
|
|
@@ -22,6 +29,7 @@ import type { BaseOptions } from "./index";
|
|
|
22
29
|
export interface RegisterOptions extends BaseOptions {
|
|
23
30
|
url?: string;
|
|
24
31
|
chainId?: number;
|
|
32
|
+
supportedTrust?: string;
|
|
25
33
|
}
|
|
26
34
|
|
|
27
35
|
export async function register(options: RegisterOptions): Promise<void> {
|
|
@@ -29,7 +37,9 @@ export async function register(options: RegisterOptions): Promise<void> {
|
|
|
29
37
|
if (!hasErc8004File()) {
|
|
30
38
|
console.log(chalk.yellow("No app/erc-8004.ts found. Let's create one."));
|
|
31
39
|
console.log("");
|
|
32
|
-
const supportedTrust =
|
|
40
|
+
const supportedTrust = options.supportedTrust
|
|
41
|
+
? parseSupportedTrust(options.supportedTrust)
|
|
42
|
+
: await promptSupportedTrust();
|
|
33
43
|
createErc8004File(supportedTrust);
|
|
34
44
|
console.log(chalk.green("Created app/erc-8004.ts"));
|
|
35
45
|
console.log("");
|
|
@@ -39,12 +49,14 @@ export async function register(options: RegisterOptions): Promise<void> {
|
|
|
39
49
|
const agentUrl = options.url ?? (await promptAgentUrl());
|
|
40
50
|
const resolvedUri = deriveAgentUri(agentUrl);
|
|
41
51
|
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
52
|
+
if (isTTY()) {
|
|
53
|
+
const yes = await confirm({
|
|
54
|
+
message: `Will register URI as: ${chalk.cyan(resolvedUri)} — confirm?`,
|
|
55
|
+
default: true,
|
|
56
|
+
});
|
|
57
|
+
if (!yes) {
|
|
58
|
+
throw new Error("Aborted.");
|
|
59
|
+
}
|
|
48
60
|
}
|
|
49
61
|
|
|
50
62
|
// Step 3: Select chain
|
package/register/update.test.ts
CHANGED
|
@@ -1,6 +1,15 @@
|
|
|
1
1
|
import { describe, expect, test } from "bun:test";
|
|
2
2
|
import { CHAIN_ID, getIdentityRegistryAddress } from "@aixyz/erc-8004";
|
|
3
|
-
import {
|
|
3
|
+
import {
|
|
4
|
+
deriveAgentUri,
|
|
5
|
+
isTTY,
|
|
6
|
+
parseSupportedTrust,
|
|
7
|
+
promptAgentUrl,
|
|
8
|
+
promptRegistryAddress,
|
|
9
|
+
promptSupportedTrust,
|
|
10
|
+
withTTY,
|
|
11
|
+
} from "./utils/prompt";
|
|
12
|
+
import { selectChain } from "./utils/chain";
|
|
4
13
|
|
|
5
14
|
describe("update command chain configuration", () => {
|
|
6
15
|
test("sepolia chain ID is correct", () => {
|
|
@@ -45,3 +54,112 @@ describe("deriveAgentUri", () => {
|
|
|
45
54
|
);
|
|
46
55
|
});
|
|
47
56
|
});
|
|
57
|
+
|
|
58
|
+
describe("parseSupportedTrust", () => {
|
|
59
|
+
test("parses a single value", () => {
|
|
60
|
+
expect(parseSupportedTrust("reputation")).toStrictEqual(["reputation"]);
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
test("parses multiple comma-separated values", () => {
|
|
64
|
+
expect(parseSupportedTrust("reputation,tee-attestation")).toStrictEqual(["reputation", "tee-attestation"]);
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
test("trims whitespace around values", () => {
|
|
68
|
+
expect(parseSupportedTrust("reputation, tee-attestation , social")).toStrictEqual([
|
|
69
|
+
"reputation",
|
|
70
|
+
"tee-attestation",
|
|
71
|
+
"social",
|
|
72
|
+
]);
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
test("throws for invalid trust mechanism", () => {
|
|
76
|
+
expect(() => parseSupportedTrust("reputation,invalid")).toThrow("Invalid trust mechanism(s): invalid");
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
test("throws for empty string", () => {
|
|
80
|
+
expect(() => parseSupportedTrust("")).toThrow("--supported-trust requires at least one value");
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
test("accepts all valid mechanisms", () => {
|
|
84
|
+
expect(parseSupportedTrust("reputation,crypto-economic,tee-attestation,social,governance")).toStrictEqual([
|
|
85
|
+
"reputation",
|
|
86
|
+
"crypto-economic",
|
|
87
|
+
"tee-attestation",
|
|
88
|
+
"social",
|
|
89
|
+
"governance",
|
|
90
|
+
]);
|
|
91
|
+
});
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
describe("isTTY", () => {
|
|
95
|
+
test("returns a boolean", () => {
|
|
96
|
+
expect(typeof isTTY()).toBe("boolean");
|
|
97
|
+
});
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
describe("withTTY", () => {
|
|
101
|
+
async function withNoTTYOverride<T>(fn: () => Promise<T>): Promise<T> {
|
|
102
|
+
const originalIsTTY = process.stdin.isTTY;
|
|
103
|
+
(process.stdin as NodeJS.ReadStream & { isTTY: boolean | undefined }).isTTY = undefined;
|
|
104
|
+
try {
|
|
105
|
+
return await fn();
|
|
106
|
+
} finally {
|
|
107
|
+
(process.stdin as NodeJS.ReadStream & { isTTY: boolean | undefined }).isTTY = originalIsTTY;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
test("throws the provided message when no TTY", async () => {
|
|
112
|
+
await withNoTTYOverride(async () => {
|
|
113
|
+
await expect(withTTY(() => Promise.resolve("ok"), "use --flag instead")).rejects.toThrow("use --flag instead");
|
|
114
|
+
});
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
test("returns the fn result when TTY is available", async () => {
|
|
118
|
+
const originalIsTTY = process.stdin.isTTY;
|
|
119
|
+
(process.stdin as NodeJS.ReadStream & { isTTY: boolean | undefined }).isTTY = true;
|
|
120
|
+
try {
|
|
121
|
+
const result = await withTTY(() => Promise.resolve("hello"), "should not throw");
|
|
122
|
+
expect(result).toBe("hello");
|
|
123
|
+
} finally {
|
|
124
|
+
(process.stdin as NodeJS.ReadStream & { isTTY: boolean | undefined }).isTTY = originalIsTTY;
|
|
125
|
+
}
|
|
126
|
+
});
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
describe("TTY-gated prompts throw in non-interactive environments", () => {
|
|
130
|
+
// Tests run in a piped (non-TTY) environment, so these error paths are exercised directly.
|
|
131
|
+
|
|
132
|
+
async function withNoTTY<T>(fn: () => Promise<T>): Promise<T> {
|
|
133
|
+
const originalIsTTY = process.stdin.isTTY;
|
|
134
|
+
(process.stdin as NodeJS.ReadStream & { isTTY: boolean | undefined }).isTTY = undefined;
|
|
135
|
+
try {
|
|
136
|
+
return await fn();
|
|
137
|
+
} finally {
|
|
138
|
+
(process.stdin as NodeJS.ReadStream & { isTTY: boolean | undefined }).isTTY = originalIsTTY;
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
test("promptAgentUrl throws when stdin is not a TTY", async () => {
|
|
143
|
+
await withNoTTY(async () => {
|
|
144
|
+
await expect(promptAgentUrl()).rejects.toThrow("No TTY detected. Provide --url");
|
|
145
|
+
});
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
test("promptSupportedTrust throws when stdin is not a TTY", async () => {
|
|
149
|
+
await withNoTTY(async () => {
|
|
150
|
+
await expect(promptSupportedTrust()).rejects.toThrow("No TTY detected");
|
|
151
|
+
});
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
test("promptRegistryAddress throws when stdin is not a TTY", async () => {
|
|
155
|
+
await withNoTTY(async () => {
|
|
156
|
+
await expect(promptRegistryAddress()).rejects.toThrow("No TTY detected. Provide --registry");
|
|
157
|
+
});
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
test("selectChain throws when stdin is not a TTY", async () => {
|
|
161
|
+
await withNoTTY(async () => {
|
|
162
|
+
await expect(selectChain()).rejects.toThrow("No TTY detected. Provide --chain-id");
|
|
163
|
+
});
|
|
164
|
+
});
|
|
165
|
+
});
|
package/register/update.ts
CHANGED
|
@@ -5,7 +5,7 @@ import { signTransaction } from "./wallet/sign";
|
|
|
5
5
|
import { resolveChainConfigById, validateBrowserRpcConflict, getExplorerUrl, CHAINS } from "./utils/chain";
|
|
6
6
|
import { writeResultJson } from "./utils/result";
|
|
7
7
|
import { label, truncateUri, broadcastAndConfirm, logSignResult } from "./utils/transaction";
|
|
8
|
-
import { promptAgentUrl, promptSelectRegistration, deriveAgentUri } from "./utils/prompt";
|
|
8
|
+
import { promptAgentUrl, promptSelectRegistration, deriveAgentUri, isTTY } from "./utils/prompt";
|
|
9
9
|
import { readRegistrations } from "./utils/erc8004-file";
|
|
10
10
|
import { confirm } from "@inquirer/prompts";
|
|
11
11
|
import chalk from "chalk";
|
|
@@ -14,6 +14,7 @@ import type { BaseOptions } from "./index";
|
|
|
14
14
|
|
|
15
15
|
export interface UpdateOptions extends BaseOptions {
|
|
16
16
|
url?: string;
|
|
17
|
+
agentId?: number;
|
|
17
18
|
}
|
|
18
19
|
|
|
19
20
|
export async function update(options: UpdateOptions): Promise<void> {
|
|
@@ -25,7 +26,18 @@ export async function update(options: UpdateOptions): Promise<void> {
|
|
|
25
26
|
}
|
|
26
27
|
|
|
27
28
|
// Step 2: Select which registration to update
|
|
28
|
-
|
|
29
|
+
let selected: (typeof registrations)[number];
|
|
30
|
+
if (options.agentId !== undefined) {
|
|
31
|
+
const match = registrations.find((r) => r.agentId === options.agentId);
|
|
32
|
+
if (!match) {
|
|
33
|
+
throw new Error(
|
|
34
|
+
`No registration found with agentId ${options.agentId}. Available: ${registrations.map((r) => r.agentId).join(", ")}`,
|
|
35
|
+
);
|
|
36
|
+
}
|
|
37
|
+
selected = match;
|
|
38
|
+
} else {
|
|
39
|
+
selected = await promptSelectRegistration(registrations);
|
|
40
|
+
}
|
|
29
41
|
|
|
30
42
|
// Step 3: Derive chain info from agentRegistry (eip155:<chainId>:<address>)
|
|
31
43
|
const parts = selected.agentRegistry.split(":");
|
|
@@ -42,12 +54,14 @@ export async function update(options: UpdateOptions): Promise<void> {
|
|
|
42
54
|
const agentUrl = options.url ?? (await promptAgentUrl());
|
|
43
55
|
const resolvedUri = deriveAgentUri(agentUrl);
|
|
44
56
|
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
57
|
+
if (isTTY()) {
|
|
58
|
+
const yes = await confirm({
|
|
59
|
+
message: `Will update URI to: ${chalk.cyan(resolvedUri)} — confirm?`,
|
|
60
|
+
default: true,
|
|
61
|
+
});
|
|
62
|
+
if (!yes) {
|
|
63
|
+
throw new Error("Aborted.");
|
|
64
|
+
}
|
|
51
65
|
}
|
|
52
66
|
|
|
53
67
|
// Step 5: Encode transaction
|
package/register/utils/chain.ts
CHANGED
|
@@ -33,6 +33,7 @@ import {
|
|
|
33
33
|
} from "viem/chains";
|
|
34
34
|
import { CHAIN_ID, getIdentityRegistryAddress } from "@aixyz/erc-8004";
|
|
35
35
|
import { input, select } from "@inquirer/prompts";
|
|
36
|
+
import { withTTY } from "./prompt";
|
|
36
37
|
|
|
37
38
|
export interface ChainConfig {
|
|
38
39
|
chain: Chain;
|
|
@@ -156,29 +157,31 @@ export function resolveChainConfigById(chainId: number, rpcUrl?: string): ChainC
|
|
|
156
157
|
// Prompt user to select a chain interactively, returning the numeric chain ID.
|
|
157
158
|
// Chains are sorted by popularity. "Other" allows entering any custom chain ID.
|
|
158
159
|
export async function selectChain(): Promise<number> {
|
|
159
|
-
|
|
160
|
-
|
|
160
|
+
return withTTY(async () => {
|
|
161
|
+
const chainById = new Map(Object.values(CHAINS).map((c) => [c.chainId, c]));
|
|
162
|
+
const nameById = new Map(Object.entries(CHAINS).map(([name, c]) => [c.chainId, name]));
|
|
161
163
|
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
164
|
+
const choices = CHAIN_SELECTION_ORDER.filter((id) => chainById.has(id)).map((id) => ({
|
|
165
|
+
name: `${nameById.get(id) ?? `chain-${id}`} (${id})`,
|
|
166
|
+
value: id,
|
|
167
|
+
}));
|
|
168
|
+
choices.push({ name: "Other (enter chain ID)", value: OTHER_CHAIN_ID });
|
|
167
169
|
|
|
168
|
-
|
|
170
|
+
const selected = await select({ message: "Select target chain:", choices });
|
|
169
171
|
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
172
|
+
if (selected === OTHER_CHAIN_ID) {
|
|
173
|
+
const raw = await input({
|
|
174
|
+
message: "Enter chain ID:",
|
|
175
|
+
validate: (v) => {
|
|
176
|
+
const n = parseInt(v, 10);
|
|
177
|
+
return Number.isInteger(n) && n > 0 ? true : "Must be a positive integer";
|
|
178
|
+
},
|
|
179
|
+
});
|
|
180
|
+
return parseInt(raw, 10);
|
|
181
|
+
}
|
|
180
182
|
|
|
181
|
-
|
|
183
|
+
return selected;
|
|
184
|
+
}, "No TTY detected. Provide --chain-id to specify the target chain.");
|
|
182
185
|
}
|
|
183
186
|
|
|
184
187
|
// Returns the registry address if known, or null if no default exists for the chain (requires interactive prompt).
|
package/register/utils/prompt.ts
CHANGED
|
@@ -2,40 +2,81 @@ import { checkbox, confirm, input, select } from "@inquirer/prompts";
|
|
|
2
2
|
import { isAddress } from "viem";
|
|
3
3
|
import type { RegistrationEntry } from "@aixyz/erc-8004/schemas/registration";
|
|
4
4
|
|
|
5
|
+
export function isTTY(): boolean {
|
|
6
|
+
return Boolean(process.stdin.isTTY);
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export async function withTTY<T>(fn: () => Promise<T>, nonTtyMessage: string): Promise<T> {
|
|
10
|
+
if (!isTTY()) {
|
|
11
|
+
throw new Error(nonTtyMessage);
|
|
12
|
+
}
|
|
13
|
+
return fn();
|
|
14
|
+
}
|
|
15
|
+
|
|
5
16
|
export async function promptAgentUrl(): Promise<string> {
|
|
6
|
-
return
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
17
|
+
return withTTY(
|
|
18
|
+
() =>
|
|
19
|
+
input({
|
|
20
|
+
message: "Agent deployment URL (e.g., https://my-agent.example.com):",
|
|
21
|
+
validate: (value) => {
|
|
22
|
+
try {
|
|
23
|
+
const url = new URL(value);
|
|
24
|
+
if (url.protocol !== "https:" && url.protocol !== "http:") {
|
|
25
|
+
return "URL must start with https:// or http://";
|
|
26
|
+
}
|
|
27
|
+
return true;
|
|
28
|
+
} catch {
|
|
29
|
+
return "Must be a valid URL (e.g., https://my-agent.example.com)";
|
|
30
|
+
}
|
|
31
|
+
},
|
|
32
|
+
}),
|
|
33
|
+
"No TTY detected. Provide --url to specify the agent deployment URL.",
|
|
34
|
+
);
|
|
20
35
|
}
|
|
21
36
|
|
|
22
37
|
export async function promptSupportedTrust(): Promise<string[]> {
|
|
23
|
-
return
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
38
|
+
return withTTY(
|
|
39
|
+
() =>
|
|
40
|
+
checkbox({
|
|
41
|
+
message: "Select supported trust mechanisms:",
|
|
42
|
+
choices: [
|
|
43
|
+
{ name: "reputation", value: "reputation", checked: true },
|
|
44
|
+
{ name: "crypto-economic", value: "crypto-economic" },
|
|
45
|
+
{ name: "tee-attestation", value: "tee-attestation" },
|
|
46
|
+
{ name: "social", value: "social" },
|
|
47
|
+
{ name: "governance", value: "governance" },
|
|
48
|
+
],
|
|
49
|
+
required: true,
|
|
50
|
+
}),
|
|
51
|
+
`No TTY detected. Provide --supported-trust to specify trust mechanisms (e.g., --supported-trust "reputation,tee-attestation").`,
|
|
52
|
+
);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const VALID_TRUST_MECHANISMS = ["reputation", "crypto-economic", "tee-attestation", "social", "governance"] as const;
|
|
56
|
+
|
|
57
|
+
export function parseSupportedTrust(value: string): string[] {
|
|
58
|
+
const items = value
|
|
59
|
+
.split(",")
|
|
60
|
+
.map((s) => s.trim())
|
|
61
|
+
.filter(Boolean);
|
|
62
|
+
const invalid = items.filter((i) => !(VALID_TRUST_MECHANISMS as readonly string[]).includes(i));
|
|
63
|
+
if (invalid.length > 0) {
|
|
64
|
+
throw new Error(
|
|
65
|
+
`Invalid trust mechanism(s): ${invalid.join(", ")}. Valid values: ${VALID_TRUST_MECHANISMS.join(", ")}`,
|
|
66
|
+
);
|
|
67
|
+
}
|
|
68
|
+
if (items.length === 0) {
|
|
69
|
+
throw new Error("--supported-trust requires at least one value.");
|
|
70
|
+
}
|
|
71
|
+
return items;
|
|
34
72
|
}
|
|
35
73
|
|
|
36
74
|
export async function promptSelectRegistration(registrations: RegistrationEntry[]): Promise<RegistrationEntry> {
|
|
37
75
|
if (registrations.length === 1) {
|
|
38
76
|
const reg = registrations[0]!;
|
|
77
|
+
if (!isTTY()) {
|
|
78
|
+
return reg;
|
|
79
|
+
}
|
|
39
80
|
const yes = await confirm({
|
|
40
81
|
message: `Update this registration? (agentId: ${reg.agentId}, registry: ${reg.agentRegistry})`,
|
|
41
82
|
default: true,
|
|
@@ -46,21 +87,27 @@ export async function promptSelectRegistration(registrations: RegistrationEntry[
|
|
|
46
87
|
return reg;
|
|
47
88
|
}
|
|
48
89
|
|
|
49
|
-
return
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
90
|
+
return withTTY(
|
|
91
|
+
() =>
|
|
92
|
+
select({
|
|
93
|
+
message: "Select registration to update:",
|
|
94
|
+
choices: registrations.map((reg) => ({
|
|
95
|
+
name: `agentId: ${reg.agentId} — ${reg.agentRegistry}`,
|
|
96
|
+
value: reg,
|
|
97
|
+
})),
|
|
98
|
+
}),
|
|
99
|
+
"No TTY detected. Multiple registrations found in app/erc-8004.ts; cannot select one non-interactively.",
|
|
100
|
+
);
|
|
56
101
|
}
|
|
57
102
|
|
|
58
103
|
export async function promptRegistryAddress(): Promise<`0x${string}`> {
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
104
|
+
return withTTY(async () => {
|
|
105
|
+
const value = await input({
|
|
106
|
+
message: "IdentityRegistry contract address (no default for this chain):",
|
|
107
|
+
validate: (v) => (isAddress(v) ? true : "Must be a valid Ethereum address (0x…)"),
|
|
108
|
+
});
|
|
109
|
+
return value as `0x${string}`;
|
|
110
|
+
}, "No TTY detected. Provide --registry to specify the IdentityRegistry contract address.");
|
|
64
111
|
}
|
|
65
112
|
|
|
66
113
|
export function deriveAgentUri(url: string): string {
|
package/register/wallet/index.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { homedir } from "node:os";
|
|
2
2
|
import type { Chain, WalletClient } from "viem";
|
|
3
3
|
import { select, input, password } from "@inquirer/prompts";
|
|
4
|
+
import { withTTY } from "../utils/prompt";
|
|
4
5
|
import { createPrivateKeyWallet } from "./privatekey";
|
|
5
6
|
import { createKeystoreWallet } from "./keystore";
|
|
6
7
|
|
|
@@ -33,36 +34,38 @@ export async function selectWalletMethod(options: WalletOptions): Promise<Wallet
|
|
|
33
34
|
}
|
|
34
35
|
|
|
35
36
|
// Interactive: prompt user to choose
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
37
|
+
return withTTY(async () => {
|
|
38
|
+
const method = await select({
|
|
39
|
+
message: "Select signing method:",
|
|
40
|
+
choices: [
|
|
41
|
+
{ name: "Keystore file", value: "keystore" },
|
|
42
|
+
{ name: "Browser wallet (any EIP-6963 compatible wallets)", value: "browser" },
|
|
43
|
+
{ name: "Private key (not recommended)", value: "privatekey" },
|
|
44
|
+
],
|
|
45
|
+
});
|
|
44
46
|
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
47
|
+
switch (method) {
|
|
48
|
+
case "keystore": {
|
|
49
|
+
const keystorePath = await input({
|
|
50
|
+
message: "Enter keystore path:",
|
|
51
|
+
default: `${homedir()}/.foundry/keystores/default`,
|
|
52
|
+
});
|
|
53
|
+
return { type: "keystore", path: keystorePath };
|
|
54
|
+
}
|
|
55
|
+
case "browser":
|
|
56
|
+
return { type: "browser" };
|
|
57
|
+
case "privatekey": {
|
|
58
|
+
const key = await password({
|
|
59
|
+
message: "Enter private key:",
|
|
60
|
+
mask: "*",
|
|
61
|
+
});
|
|
62
|
+
console.warn("Warning: Using raw private key is not recommended for production");
|
|
63
|
+
return { type: "privatekey", resolveKey: () => Promise.resolve(key) };
|
|
64
|
+
}
|
|
65
|
+
default:
|
|
66
|
+
throw new Error("No wallet method selected");
|
|
52
67
|
}
|
|
53
|
-
|
|
54
|
-
return { type: "browser" };
|
|
55
|
-
case "privatekey": {
|
|
56
|
-
const key = await password({
|
|
57
|
-
message: "Enter private key:",
|
|
58
|
-
mask: "*",
|
|
59
|
-
});
|
|
60
|
-
console.warn("Warning: Using raw private key is not recommended for production");
|
|
61
|
-
return { type: "privatekey", resolveKey: () => Promise.resolve(key) };
|
|
62
|
-
}
|
|
63
|
-
default:
|
|
64
|
-
throw new Error("No wallet method selected");
|
|
65
|
-
}
|
|
68
|
+
}, "No TTY detected. Provide --keystore, --browser, or PRIVATE_KEY environment variable to specify a signing method.");
|
|
66
69
|
}
|
|
67
70
|
|
|
68
71
|
export async function createWalletFromMethod(
|