@aixyz/cli 0.10.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.
@@ -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: ${hasAgent} } });`,
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 'vercel'")
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);
@@ -49,6 +54,8 @@ Examples:
49
54
  async function action(options: BuildOptions = {}): Promise<void> {
50
55
  const cwd = process.cwd();
51
56
  loadEnvConfig(cwd, false);
57
+ process.env.NODE_ENV = "production";
58
+ process.env.AIXYZ_ENV = "production";
52
59
  const entrypoint = getEntrypointMayGenerate(cwd, "build");
53
60
 
54
61
  // Determine output target: explicit CLI flag takes precedence, then config file, then auto-detect VERCEL env
@@ -57,7 +64,10 @@ async function action(options: BuildOptions = {}): Promise<void> {
57
64
 
58
65
  if (target === "vercel") {
59
66
  console.log(chalk.cyan("▶") + " Building for " + chalk.bold("Vercel") + "...");
60
- 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);
61
71
  } else {
62
72
  console.log(chalk.cyan("▶") + " Building for " + chalk.bold("Standalone") + "...");
63
73
  await buildBun(entrypoint);
@@ -79,6 +89,10 @@ async function buildBun(entrypoint: string): Promise<void> {
79
89
  target: "bun",
80
90
  format: "esm",
81
91
  sourcemap: "linked",
92
+ define: {
93
+ "process.env.NODE_ENV": JSON.stringify("production"),
94
+ "process.env.AIXYZ_ENV": JSON.stringify("production"),
95
+ },
82
96
  plugins: [AixyzConfigPlugin(), AixyzServerPlugin(entrypoint, "standalone")],
83
97
  });
84
98
 
@@ -119,7 +133,63 @@ async function buildBun(entrypoint: string): Promise<void> {
119
133
  console.log("To run: bun .aixyz/output/server.js");
120
134
  }
121
135
 
122
- async function buildVercel(entrypoint: string): Promise<void> {
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> {
123
193
  const cwd = process.cwd();
124
194
 
125
195
  const outputDir = resolve(cwd, ".vercel/output");
@@ -136,6 +206,10 @@ async function buildVercel(entrypoint: string): Promise<void> {
136
206
  target: "bun",
137
207
  format: "esm",
138
208
  sourcemap: "linked",
209
+ define: {
210
+ "process.env.NODE_ENV": JSON.stringify("production"),
211
+ "process.env.AIXYZ_ENV": JSON.stringify("production"),
212
+ },
139
213
  plugins: [AixyzConfigPlugin(), AixyzServerPlugin(entrypoint, "vercel")],
140
214
  });
141
215
 
@@ -155,6 +229,7 @@ async function buildVercel(entrypoint: string): Promise<void> {
155
229
  handler: "server.js",
156
230
  runtime: "bun1.x",
157
231
  launcherType: "Bun",
232
+ maxDuration: config.vercel.maxDuration,
158
233
  shouldAddHelpers: true,
159
234
  shouldAddSourcemapSupport: true,
160
235
  },
package/dev/index.ts CHANGED
@@ -15,6 +15,8 @@ async function action(options: { port?: string }): Promise<void> {
15
15
 
16
16
  // Load environment config
17
17
  const { loadedEnvFiles } = loadEnvConfig(cwd, true);
18
+ process.env.NODE_ENV = "development";
19
+ process.env.AIXYZ_ENV = "development";
18
20
  const envFileNames = loadedEnvFiles.map((f) => relative(cwd, f.path));
19
21
 
20
22
  const port = options.port || process.env.PORT || "3000";
@@ -41,7 +43,7 @@ async function action(options: { port?: string }): Promise<void> {
41
43
  cwd,
42
44
  stdout: "inherit",
43
45
  stderr: "inherit",
44
- env: process.env,
46
+ env: { ...process.env, NODE_ENV: "development", AIXYZ_ENV: "development" },
45
47
  });
46
48
  child.exited.then((code) => {
47
49
  if (!restarting && code !== 0) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aixyz/cli",
3
- "version": "0.10.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.10.0",
31
- "@aixyz/erc-8004": "0.10.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
@@ -4,7 +4,6 @@ import { update } from "./update";
4
4
  import type { WalletOptions } from "./wallet";
5
5
 
6
6
  export interface BaseOptions extends WalletOptions {
7
- chain?: string;
8
7
  rpcUrl?: string;
9
8
  registry?: string;
10
9
  outDir?: string;
@@ -16,13 +15,17 @@ erc8004Command
16
15
  .command("register")
17
16
  .description("Register a new agent to the ERC-8004 IdentityRegistry")
18
17
  .option("--url <url>", "Agent deployment URL (e.g., https://my-agent.example.com)")
19
- .option("--chain <chain>", "Target chain (mainnet, sepolia, base-sepolia, localhost)")
18
+ .option("--chain-id <chainId>", "Target chain by numeric chain ID", parseInt)
20
19
  .option("--rpc-url <url>", "Custom RPC URL (uses default if not provided)")
21
- .option("--registry <address>", "Contract address of the IdentityRegistry (required for localhost)")
20
+ .option("--registry <address>", "Contract address of the IdentityRegistry (required for localhost and custom chains)")
22
21
  .option("--keystore <path>", "Path to Ethereum keystore (V3) JSON file for local signing")
23
22
  .option("--browser", "Use browser extension wallet (any extension)")
24
23
  .option("--broadcast", "Sign and broadcast the transaction (default: dry-run)")
25
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
+ )
26
29
  .addHelpText(
27
30
  "after",
28
31
  `
@@ -32,22 +35,23 @@ Option Details:
32
35
  The registration URI will be derived as <url>/_aixyz/erc-8004.json.
33
36
  If omitted, you will be prompted to enter the URL interactively.
34
37
 
35
- --chain <chain>
36
- Target chain for registration. Supported values:
37
- mainnet Ethereum mainnet (chain ID 1)
38
- sepolia Ethereum Sepolia testnet (chain ID 11155111)
39
- base-sepolia Base Sepolia testnet (chain ID 84532)
40
- localhost Local Foundry/Anvil node (chain ID 31337)
38
+ --chain-id <chainId>
39
+ Target chain by numeric chain ID. Supported chain IDs include:
40
+ 1 (mainnet), 8453 (base), 42161 (arbitrum), 10 (optimism),
41
+ 137 (polygon), 56 (bsc), 43114 (avalanche), 11155111 (sepolia),
42
+ 84532 (base-sepolia), and more (see @aixyz/erc-8004).
41
43
  If omitted, you will be prompted to select a chain interactively.
44
+ Use any custom EVM chain ID with --rpc-url for BYO chains.
42
45
 
43
46
  --rpc-url <url>
44
47
  Custom RPC endpoint URL. Overrides the default RPC for the selected
45
- chain. Cannot be used with --browser since the browser wallet manages
48
+ chain. Required when using a custom chain ID with no default deployment.
49
+ Cannot be used with --browser since the browser wallet manages
46
50
  its own RPC connection.
47
51
 
48
52
  --registry <address>
49
- Contract address of the ERC-8004 IdentityRegistry. Only required for
50
- localhost, where there is no default deployment.
53
+ Contract address of the ERC-8004 IdentityRegistry. Required for
54
+ localhost and custom chains, where there is no default deployment.
51
55
 
52
56
  --keystore <path>
53
57
  Path to an Ethereum keystore (V3) JSON file. You will be prompted for
@@ -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
@@ -74,11 +85,17 @@ Environment Variables:
74
85
 
75
86
  Examples:
76
87
  # Dry-run (default)
77
- $ aixyz erc-8004 register --url "https://my-agent.example.com" --chain sepolia
88
+ $ aixyz erc-8004 register --url "https://my-agent.example.com" --chain-id 11155111
78
89
 
79
- # Sign and broadcast
80
- $ aixyz erc-8004 register --url "https://my-agent.example.com" --chain sepolia --keystore ~/.foundry/keystores/default --broadcast
81
- $ aixyz erc-8004 register --url "https://my-agent.example.com" --chain sepolia --browser --broadcast`,
90
+ # Sign and broadcast (known chain)
91
+ $ aixyz erc-8004 register --url "https://my-agent.example.com" --chain-id 84532 --keystore ~/.foundry/keystores/default --broadcast
92
+ $ aixyz erc-8004 register --url "https://my-agent.example.com" --chain-id 84532 --browser --broadcast
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
+
97
+ # BYO: register on any custom EVM chain
98
+ $ aixyz erc-8004 register --url "https://my-agent.example.com" --chain-id 999999 --rpc-url https://my-rpc.example.com --registry 0xABCD... --broadcast`,
82
99
  )
83
100
  .action(register);
84
101
 
@@ -86,6 +103,7 @@ erc8004Command
86
103
  .command("update")
87
104
  .description("Update the metadata URI of a registered agent")
88
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)
89
107
  .option("--rpc-url <url>", "Custom RPC URL (uses default if not provided)")
90
108
  .option("--registry <address>", "Contract address of the IdentityRegistry (required for localhost)")
91
109
  .option("--keystore <path>", "Path to Ethereum keystore (V3) JSON file for local signing")
@@ -101,6 +119,11 @@ Option Details:
101
119
  The URI will be derived as <url>/_aixyz/erc-8004.json.
102
120
  If omitted, you will be prompted to enter the URL interactively.
103
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
+
104
127
  --rpc-url <url>
105
128
  Custom RPC endpoint URL. Overrides the default RPC for the selected
106
129
  chain. Cannot be used with --browser.
@@ -138,6 +161,9 @@ Examples:
138
161
 
139
162
  # Sign and broadcast
140
163
  $ aixyz erc-8004 update --url "https://new-domain.example.com" --keystore ~/.foundry/keystores/default --broadcast
141
- $ 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`,
142
168
  )
143
169
  .action(update);
@@ -3,15 +3,23 @@ import { IdentityRegistryAbi } from "@aixyz/erc-8004";
3
3
  import { selectWalletMethod } from "./wallet";
4
4
  import { signTransaction } from "./wallet/sign";
5
5
  import {
6
- resolveChainConfig,
6
+ resolveChainConfigById,
7
7
  selectChain,
8
8
  resolveRegistryAddress,
9
9
  validateBrowserRpcConflict,
10
10
  getExplorerUrl,
11
+ CHAINS,
11
12
  } from "./utils/chain";
12
13
  import { writeResultJson } from "./utils/result";
13
14
  import { label, truncateUri, broadcastAndConfirm, logSignResult } from "./utils/transaction";
14
- import { promptAgentUrl, promptSupportedTrust, deriveAgentUri } from "./utils/prompt";
15
+ import {
16
+ promptAgentUrl,
17
+ promptSupportedTrust,
18
+ promptRegistryAddress,
19
+ deriveAgentUri,
20
+ isTTY,
21
+ parseSupportedTrust,
22
+ } from "./utils/prompt";
15
23
  import { hasErc8004File, createErc8004File, writeRegistrationEntry } from "./utils/erc8004-file";
16
24
  import { confirm } from "@inquirer/prompts";
17
25
  import chalk from "chalk";
@@ -20,7 +28,8 @@ import type { BaseOptions } from "./index";
20
28
 
21
29
  export interface RegisterOptions extends BaseOptions {
22
30
  url?: string;
23
- chain?: string;
31
+ chainId?: number;
32
+ supportedTrust?: string;
24
33
  }
25
34
 
26
35
  export async function register(options: RegisterOptions): Promise<void> {
@@ -28,7 +37,9 @@ export async function register(options: RegisterOptions): Promise<void> {
28
37
  if (!hasErc8004File()) {
29
38
  console.log(chalk.yellow("No app/erc-8004.ts found. Let's create one."));
30
39
  console.log("");
31
- const supportedTrust = await promptSupportedTrust();
40
+ const supportedTrust = options.supportedTrust
41
+ ? parseSupportedTrust(options.supportedTrust)
42
+ : await promptSupportedTrust();
32
43
  createErc8004File(supportedTrust);
33
44
  console.log(chalk.green("Created app/erc-8004.ts"));
34
45
  console.log("");
@@ -38,18 +49,21 @@ export async function register(options: RegisterOptions): Promise<void> {
38
49
  const agentUrl = options.url ?? (await promptAgentUrl());
39
50
  const resolvedUri = deriveAgentUri(agentUrl);
40
51
 
41
- const yes = await confirm({
42
- message: `Will register URI as: ${chalk.cyan(resolvedUri)} — confirm?`,
43
- default: true,
44
- });
45
- if (!yes) {
46
- throw new Error("Aborted.");
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
+ }
47
60
  }
48
61
 
49
62
  // Step 3: Select chain
50
- const chainName = options.chain ?? (await selectChain());
51
- const chainConfig = resolveChainConfig(chainName);
52
- const registryAddress = resolveRegistryAddress(chainName, chainConfig.chainId, options.registry);
63
+ const chainId = options.chainId ?? (await selectChain());
64
+ const chainConfig = resolveChainConfigById(chainId, options.rpcUrl);
65
+ const chainName = Object.entries(CHAINS).find(([, c]) => c.chainId === chainId)?.[0] ?? `chain-${chainId}`;
66
+ const registryAddress = resolveRegistryAddress(chainId, options.registry) ?? (await promptRegistryAddress());
53
67
 
54
68
  // Step 4: Encode transaction
55
69
  const data = encodeFunctionData({
@@ -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 { deriveAgentUri } from "./utils/prompt";
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
+ });
@@ -2,10 +2,10 @@ 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 { resolveChainConfig, validateBrowserRpcConflict, getExplorerUrl, CHAINS } from "./utils/chain";
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
- const selected = await promptSelectRegistration(registrations);
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(":");
@@ -36,18 +48,20 @@ export async function update(options: UpdateOptions): Promise<void> {
36
48
  const chainId = Number(parts[1]);
37
49
  const registryAddress = parts.slice(2).join(":") as `0x${string}`;
38
50
  const chainName = Object.entries(CHAINS).find(([, config]) => config.chainId === chainId)?.[0] ?? `chain-${chainId}`;
39
- const chainConfig = resolveChainConfig(chainName);
51
+ const chainConfig = resolveChainConfigById(chainId, options.rpcUrl);
40
52
 
41
53
  // Step 4: Get new agent URL and derive URI
42
54
  const agentUrl = options.url ?? (await promptAgentUrl());
43
55
  const resolvedUri = deriveAgentUri(agentUrl);
44
56
 
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
+ 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
@@ -1,20 +1,129 @@
1
- import { isAddress, type Chain } from "viem";
2
- import { mainnet, sepolia, baseSepolia, foundry } from "viem/chains";
1
+ import { isAddress, defineChain, type Chain } from "viem";
2
+ import {
3
+ abstract,
4
+ abstractTestnet,
5
+ arbitrum,
6
+ arbitrumSepolia,
7
+ avalanche,
8
+ avalancheFuji,
9
+ base,
10
+ baseSepolia,
11
+ bsc,
12
+ bscTestnet,
13
+ celo,
14
+ celoSepolia,
15
+ foundry,
16
+ gnosis,
17
+ linea,
18
+ lineaSepolia,
19
+ mainnet,
20
+ mantle,
21
+ mantleSepoliaTestnet,
22
+ megaeth,
23
+ monad,
24
+ monadTestnet,
25
+ optimism,
26
+ optimismSepolia,
27
+ polygon,
28
+ polygonAmoy,
29
+ scroll,
30
+ scrollSepolia,
31
+ sepolia,
32
+ taiko,
33
+ } from "viem/chains";
3
34
  import { CHAIN_ID, getIdentityRegistryAddress } from "@aixyz/erc-8004";
4
- import { select } from "@inquirer/prompts";
35
+ import { input, select } from "@inquirer/prompts";
36
+ import { withTTY } from "./prompt";
5
37
 
6
38
  export interface ChainConfig {
7
39
  chain: Chain;
8
40
  chainId: number;
9
41
  }
10
42
 
43
+ // Maps supported chain IDs to viem Chain objects (derived from @aixyz/erc-8004 CHAIN_ID)
44
+ const VIEM_CHAIN_BY_ID: Record<number, Chain> = {
45
+ [CHAIN_ID.ABSTRACT]: abstract,
46
+ [CHAIN_ID.ARBITRUM]: arbitrum,
47
+ [CHAIN_ID.AVALANCHE]: avalanche,
48
+ [CHAIN_ID.BASE]: base,
49
+ [CHAIN_ID.BSC]: bsc,
50
+ [CHAIN_ID.CELO]: celo,
51
+ [CHAIN_ID.GNOSIS]: gnosis,
52
+ [CHAIN_ID.LINEA]: linea,
53
+ [CHAIN_ID.MAINNET]: mainnet,
54
+ [CHAIN_ID.MANTLE]: mantle,
55
+ [CHAIN_ID.MEGAETH]: megaeth,
56
+ [CHAIN_ID.MONAD]: monad,
57
+ [CHAIN_ID.OPTIMISM]: optimism,
58
+ [CHAIN_ID.POLYGON]: polygon,
59
+ [CHAIN_ID.SCROLL]: scroll,
60
+ [CHAIN_ID.TAIKO]: taiko,
61
+ [CHAIN_ID.ABSTRACT_TESTNET]: abstractTestnet,
62
+ [CHAIN_ID.ARBITRUM_SEPOLIA]: arbitrumSepolia,
63
+ [CHAIN_ID.AVALANCHE_FUJI]: avalancheFuji,
64
+ [CHAIN_ID.BASE_SEPOLIA]: baseSepolia,
65
+ [CHAIN_ID.BSC_TESTNET]: bscTestnet,
66
+ [CHAIN_ID.CELO_SEPOLIA]: celoSepolia,
67
+ [CHAIN_ID.LINEA_SEPOLIA]: lineaSepolia,
68
+ [CHAIN_ID.MANTLE_SEPOLIA]: mantleSepoliaTestnet,
69
+ [CHAIN_ID.MONAD_TESTNET]: monadTestnet,
70
+ [CHAIN_ID.OPTIMISM_SEPOLIA]: optimismSepolia,
71
+ [CHAIN_ID.POLYGON_AMOY]: polygonAmoy,
72
+ [CHAIN_ID.SCROLL_SEPOLIA]: scrollSepolia,
73
+ [CHAIN_ID.SEPOLIA]: sepolia,
74
+ 31337: foundry,
75
+ };
76
+
77
+ // Build CHAINS from CHAIN_ID as the single source of truth from @aixyz/erc-8004.
78
+ // Chain names are derived from CHAIN_ID keys: lowercase with underscores replaced by hyphens.
11
79
  export const CHAINS: Record<string, ChainConfig> = {
12
- mainnet: { chain: mainnet, chainId: CHAIN_ID.MAINNET },
13
- sepolia: { chain: sepolia, chainId: CHAIN_ID.SEPOLIA },
14
- "base-sepolia": { chain: baseSepolia, chainId: CHAIN_ID.BASE_SEPOLIA },
80
+ ...Object.fromEntries(
81
+ (Object.entries(CHAIN_ID) as [string, number][])
82
+ .filter(([, id]) => id in VIEM_CHAIN_BY_ID)
83
+ .map(([key, id]) => [key.toLowerCase().replace(/_/g, "-"), { chain: VIEM_CHAIN_BY_ID[id]!, chainId: id }]),
84
+ ),
15
85
  localhost: { chain: foundry, chainId: 31337 },
16
86
  };
17
87
 
88
+ // Priority-ordered chain IDs for interactive selection — most popular first.
89
+ const CHAIN_SELECTION_ORDER: number[] = [
90
+ // Popular mainnets
91
+ CHAIN_ID.MAINNET,
92
+ CHAIN_ID.BASE,
93
+ CHAIN_ID.ARBITRUM,
94
+ CHAIN_ID.OPTIMISM,
95
+ CHAIN_ID.POLYGON,
96
+ CHAIN_ID.BSC,
97
+ CHAIN_ID.AVALANCHE,
98
+ CHAIN_ID.SCROLL,
99
+ CHAIN_ID.LINEA,
100
+ CHAIN_ID.CELO,
101
+ CHAIN_ID.GNOSIS,
102
+ CHAIN_ID.TAIKO,
103
+ CHAIN_ID.MANTLE,
104
+ CHAIN_ID.MONAD,
105
+ CHAIN_ID.MEGAETH,
106
+ CHAIN_ID.ABSTRACT,
107
+ // Popular testnets
108
+ CHAIN_ID.SEPOLIA,
109
+ CHAIN_ID.BASE_SEPOLIA,
110
+ CHAIN_ID.ARBITRUM_SEPOLIA,
111
+ CHAIN_ID.OPTIMISM_SEPOLIA,
112
+ CHAIN_ID.POLYGON_AMOY,
113
+ CHAIN_ID.AVALANCHE_FUJI,
114
+ CHAIN_ID.BSC_TESTNET,
115
+ CHAIN_ID.SCROLL_SEPOLIA,
116
+ CHAIN_ID.LINEA_SEPOLIA,
117
+ CHAIN_ID.CELO_SEPOLIA,
118
+ CHAIN_ID.MANTLE_SEPOLIA,
119
+ CHAIN_ID.MONAD_TESTNET,
120
+ CHAIN_ID.ABSTRACT_TESTNET,
121
+ // Special
122
+ 31337,
123
+ ];
124
+
125
+ const OTHER_CHAIN_ID = -1;
126
+
18
127
  export function resolveChainConfig(chainName: string): ChainConfig {
19
128
  const config = CHAINS[chainName];
20
129
  if (!config) {
@@ -23,24 +132,75 @@ export function resolveChainConfig(chainName: string): ChainConfig {
23
132
  return config;
24
133
  }
25
134
 
26
- export async function selectChain(): Promise<string> {
27
- return select({
28
- message: "Select target chain:",
29
- choices: Object.keys(CHAINS).map((name) => ({ name, value: name })),
30
- });
135
+ // Resolve chain config by numeric chain ID, supporting BYO chains via rpcUrl.
136
+ // For known chain IDs the viem chain object is used directly.
137
+ // For unknown chain IDs, a minimal chain is constructed using defineChain (requires rpcUrl).
138
+ export function resolveChainConfigById(chainId: number, rpcUrl?: string): ChainConfig {
139
+ const chain = VIEM_CHAIN_BY_ID[chainId];
140
+ if (chain) {
141
+ return { chain, chainId };
142
+ }
143
+ if (!rpcUrl) {
144
+ throw new Error(`Unknown chain ID ${chainId}. Provide --rpc-url to register on a custom chain.`);
145
+ }
146
+ return {
147
+ chain: defineChain({
148
+ id: chainId,
149
+ name: `chain-${chainId}`,
150
+ nativeCurrency: { name: "Ether", symbol: "ETH", decimals: 18 },
151
+ rpcUrls: { default: { http: [rpcUrl] } },
152
+ }),
153
+ chainId,
154
+ };
155
+ }
156
+
157
+ // Prompt user to select a chain interactively, returning the numeric chain ID.
158
+ // Chains are sorted by popularity. "Other" allows entering any custom chain ID.
159
+ export async function selectChain(): Promise<number> {
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]));
163
+
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 });
169
+
170
+ const selected = await select({ message: "Select target chain:", choices });
171
+
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
+ }
182
+
183
+ return selected;
184
+ }, "No TTY detected. Provide --chain-id to specify the target chain.");
31
185
  }
32
186
 
33
- export function resolveRegistryAddress(chainName: string, chainId: number, registry?: string): `0x${string}` {
187
+ // Returns the registry address if known, or null if no default exists for the chain (requires interactive prompt).
188
+ // Throws only for an explicitly invalid registry address.
189
+ export function resolveRegistryAddress(chainId: number, registry?: string): `0x${string}` | null {
34
190
  if (registry) {
35
191
  if (!isAddress(registry)) {
36
192
  throw new Error(`Invalid registry address: ${registry}`);
37
193
  }
38
194
  return registry as `0x${string}`;
39
195
  }
40
- if (chainName === "localhost") {
41
- throw new Error("--registry is required for localhost (no default contract deployment)");
196
+ if (chainId === 31337) {
197
+ return null;
198
+ }
199
+ try {
200
+ return getIdentityRegistryAddress(chainId) as `0x${string}`;
201
+ } catch {
202
+ return null;
42
203
  }
43
- return getIdentityRegistryAddress(chainId) as `0x${string}`;
44
204
  }
45
205
 
46
206
  export function validateBrowserRpcConflict(browser: boolean | undefined, rpcUrl: string | undefined): void {
@@ -1,40 +1,82 @@
1
1
  import { checkbox, confirm, input, select } from "@inquirer/prompts";
2
+ import { isAddress } from "viem";
2
3
  import type { RegistrationEntry } from "@aixyz/erc-8004/schemas/registration";
3
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
+
4
16
  export async function promptAgentUrl(): Promise<string> {
5
- return input({
6
- message: "Agent deployment URL (e.g., https://my-agent.example.com):",
7
- validate: (value) => {
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
- }
17
- },
18
- });
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
+ );
19
35
  }
20
36
 
21
37
  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,
32
- });
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;
33
72
  }
34
73
 
35
74
  export async function promptSelectRegistration(registrations: RegistrationEntry[]): Promise<RegistrationEntry> {
36
75
  if (registrations.length === 1) {
37
76
  const reg = registrations[0]!;
77
+ if (!isTTY()) {
78
+ return reg;
79
+ }
38
80
  const yes = await confirm({
39
81
  message: `Update this registration? (agentId: ${reg.agentId}, registry: ${reg.agentRegistry})`,
40
82
  default: true,
@@ -45,13 +87,27 @@ export async function promptSelectRegistration(registrations: RegistrationEntry[
45
87
  return reg;
46
88
  }
47
89
 
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
- });
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
+ );
101
+ }
102
+
103
+ export async function promptRegistryAddress(): Promise<`0x${string}`> {
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.");
55
111
  }
56
112
 
57
113
  export function deriveAgentUri(url: string): string {
@@ -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
- const method = await select({
37
- message: "Select signing method:",
38
- choices: [
39
- { name: "Keystore file", value: "keystore" },
40
- { name: "Browser wallet (any EIP-6963 compatible wallets)", value: "browser" },
41
- { name: "Private key (not recommended)", value: "privatekey" },
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
- switch (method) {
46
- case "keystore": {
47
- const keystorePath = await input({
48
- message: "Enter keystore path:",
49
- default: `${homedir()}/.foundry/keystores/default`,
50
- });
51
- return { type: "keystore", path: keystorePath };
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
- case "browser":
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(