@agtnames/cli 0.0.0-stage → 0.1.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/dist/cli.js ADDED
@@ -0,0 +1,326 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * agt — register a .agt name, publish its signed manifest, set its records and verify it, from a terminal.
4
+ *
5
+ * agt available <name>
6
+ * agt register <name> [--years N] [--to 0x…] [--dry-run] [--yes]
7
+ * agt manifest init [name] [--out file.json] [--no-probe] [--shell bash|powershell] [--template]
8
+ * agt manifest publish <name> <file.json> [--host agts.dev|ipfs] [--no-records] [--skip-live-check] [--dry-run]
9
+ * agt records set <name> [--mcp URL] [--a2a URL] [--http URL] [--ws URL] [--wallet 0x…] [--addr 0x…] [--manifest URI] [--dry-run]
10
+ * agt verify <name> [--no-endpoints] [--json]
11
+ * agt keyfile create <file> encrypt a private key into a keyfile (prompts for the key and a passphrase)
12
+ * agt keyfile address <file> print the keyfile's address (no passphrase needed)
13
+ *
14
+ * Signing (writes only):
15
+ * default opens a local page; approve each request in MetaMask (the key never leaves the wallet)
16
+ * --signer key key from AGT_OWNER_KEY, or from the variable --key-env names
17
+ * --keyfile <file> key from an encrypted keyfile; passphrase from AGT_KEYFILE_PASSPHRASE or a prompt
18
+ * --no-open print the wallet page URL instead of opening a browser
19
+ *
20
+ * Network: --network polygon|amoy (AGT_NETWORK, default polygon), --rpc URL (AGT_RPC_URL), --site URL (AGT_SITE,
21
+ * default https://agtnames.com; required on amoy). --dry-run on any write signs what needs signing, prints the exact
22
+ * document and transactions, and sends nothing. --json prints the result as JSON on stdout.
23
+ *
24
+ * Exit codes: 0 done, 1 failed (or verify found problems), 2 usage error.
25
+ */
26
+ import { readFileSync, writeFileSync, existsSync } from "node:fs";
27
+ import { parseAddressFlag, parseArgs, parseHttpsFlag, parseYears, UsageError } from "./args.js";
28
+ import { browserSigner } from "./browser.js";
29
+ import { resolverAbi } from "./abi.js";
30
+ import { assertOwner, describePlan, dryTx, multicallTx, nameState, nodeOf, normalizeLabel, publicClient, recordCalls, sendAndWait } from "./chain.js";
31
+ import { networkFrom, requireSite } from "./config.js";
32
+ import { decryptKey, encryptKey, readKeyfile, writeKeyfile } from "./keyfile.js";
33
+ import { buildManifest, hostManifest, lintInput, MANIFEST_TEMPLATE, signManifestWith } from "./manifest.js";
34
+ import { detectShell, interactiveInit } from "./init.js";
35
+ import { confirm, promptHidden } from "./prompt.js";
36
+ import { CancelledError, isInteractive } from "./tui.js";
37
+ import { checkQuote, fetchQuote, planRegister, usdc } from "./register.js";
38
+ import { keySigner, parsePrivateKey } from "./signer.js";
39
+ import { checkEndpoint, verifyName } from "./verify.js";
40
+ const VERSION = JSON.parse(readFileSync(new URL("../package.json", import.meta.url), "utf8")).version;
41
+ function help() {
42
+ const src = readFileSync(new URL(import.meta.url), "utf8");
43
+ return src.slice(src.indexOf("/**") + 3, src.indexOf("*/")).replace(/^ \* ?/gm, "").trim();
44
+ }
45
+ async function signerFor(c) {
46
+ const f = c.args.flags;
47
+ const kind = f.keyfile ? "keyfile" : f["key-env"] ? "key" : (f.signer ?? "browser");
48
+ if (kind === "key") {
49
+ const v = f["key-env"] ?? "AGT_OWNER_KEY";
50
+ return keySigner(parsePrivateKey(process.env[v], v), c.net);
51
+ }
52
+ if (kind === "keyfile") {
53
+ if (!f.keyfile)
54
+ throw new UsageError("--signer keyfile needs --keyfile <file>");
55
+ const e = readKeyfile(f.keyfile);
56
+ const pass = process.env.AGT_KEYFILE_PASSPHRASE ?? await promptHidden(`Passphrase for ${f.keyfile} (${e.address}): `);
57
+ const s = keySigner(decryptKey(e, pass), c.net, "keyfile");
58
+ if (s.address.toLowerCase() !== e.address.toLowerCase())
59
+ throw new Error("the keyfile's stored address does not match its key; the file was edited");
60
+ return s;
61
+ }
62
+ if (kind !== "browser")
63
+ throw new UsageError("--signer must be browser, key or keyfile");
64
+ const noOpen = c.args.bools.has("no-open");
65
+ const timeout = f.timeout ? Number(f.timeout) * 60_000 : undefined;
66
+ c.log(`Opening the wallet page${noOpen ? "" : " in your browser"}. Keep it open until the command finishes.`);
67
+ const s = await browserSigner({ network: c.net, timeoutMs: timeout, open: noOpen ? () => { } : undefined, onUrl: (u) => c.log(` ${u}`) });
68
+ c.log(`Connected: ${s.address}`);
69
+ return s;
70
+ }
71
+ async function withSigner(c, fn) {
72
+ const s = await signerFor(c);
73
+ try {
74
+ return await fn(s);
75
+ }
76
+ finally {
77
+ await s.close();
78
+ }
79
+ }
80
+ const labelArg = (c, i, what = "name") => {
81
+ const v = c.args.positionals[i];
82
+ if (!v)
83
+ throw new UsageError(`missing <${what}>`);
84
+ return normalizeLabel(v);
85
+ };
86
+ // ── available ─────────────────────────────────────────────────────────────────────────────────────────────────────
87
+ async function available(c) {
88
+ const label = labelArg(c, 1);
89
+ const site = requireSite(c.net);
90
+ const r = await fetch(`${site}/api/search?name=${encodeURIComponent(label)}`);
91
+ const j = (await r.json().catch(() => ({})));
92
+ if (!r.ok || !j.success)
93
+ throw new Error(`lookup failed (${r.status}): ${j.error ?? "no reason given"}`);
94
+ const human = j.available
95
+ ? [`${label}.agt is available: $${j.registerUsd} for the first year, $${j.renewUsd}/year after.`, `Register it with: agt register ${label}`]
96
+ : [`${label}.agt is not available (${j.reserved ? "reserved" : j.status ?? "taken"}).`];
97
+ if (j.available && j.registrationOpen === false)
98
+ human.push("Registration is not open yet.");
99
+ c.out({ name: `${label}.agt`, available: !!j.available, status: j.status, tier: j.tier, registerUsd: j.registerUsd, renewUsd: j.renewUsd, registrationOpen: j.registrationOpen }, human);
100
+ }
101
+ // ── register ──────────────────────────────────────────────────────────────────────────────────────────────────────
102
+ async function register(c) {
103
+ const label = labelArg(c, 1);
104
+ const years = parseYears(c.args.flags.years);
105
+ const toFlag = parseAddressFlag(c.args.flags.to, "to");
106
+ const site = requireSite(c.net);
107
+ const pc = publicClient(c.net);
108
+ await withSigner(c, async (s) => {
109
+ const to = (toFlag ?? s.address);
110
+ const gift = to.toLowerCase() !== s.address.toLowerCase();
111
+ c.log(`Getting a quote for ${label}.agt (${years} year${years === 1 ? "" : "s"})…`);
112
+ const q = await fetchQuote(site, { label, to, years, payToken: "usdc" });
113
+ const quote = checkQuote(q, { label, to }, c.net);
114
+ const plan = await planRegister(pc, c.net, q, quote, s.address);
115
+ const mins = Math.max(0, Math.floor((Number(quote.validUntil) - Date.now() / 1000) / 60));
116
+ c.log(`Price ${usdc(quote.price)}. Your USDC balance: ${usdc(plan.balance)}. The quote is good for about ${mins} minutes.`);
117
+ if (gift)
118
+ c.log(`Note: ${label}.agt will be owned by ${to}, not you. Only that wallet can publish its manifest or set its records.`);
119
+ if (c.dry) {
120
+ c.out({ dryRun: true, quote: q, transactions: plan.txs.map((t) => dryTx(t, s.address)) }, ["Dry run: nothing sent. Transactions:", ...plan.txs.map((t) => ` ${t.description}`)]);
121
+ return;
122
+ }
123
+ if (plan.balance < quote.price)
124
+ throw new Error(`not enough USDC: need ${usdc(quote.price)}, have ${usdc(plan.balance)} (USDC on ${c.net.chain.name}, ${c.net.usdc})`);
125
+ const gas = await pc.getBalance({ address: s.address });
126
+ if (gas === 0n)
127
+ throw new Error(`${s.address} has no ${c.net.chain.nativeCurrency.symbol} for gas`);
128
+ if (s.kind !== "browser" && !c.args.bools.has("yes") && !(await confirm(`Send ${plan.txs.length} transaction${plan.txs.length === 1 ? "" : "s"} from ${s.address}?`)))
129
+ throw new Error("cancelled");
130
+ const hashes = [];
131
+ for (const tx of plan.txs)
132
+ hashes.push(await sendAndWait(pc, c.net, s, tx, c.log));
133
+ const after = await nameState(pc, c.net, label);
134
+ if (after.owner?.toLowerCase() !== to.toLowerCase())
135
+ throw new Error(`registered, but ${label}.agt reads as owned by ${after.owner ?? "nobody"}; check ${hashes.at(-1)}`);
136
+ c.out({ name: `${label}.agt`, owner: after.owner, transactions: hashes }, [
137
+ `✓ ${label}.agt is registered to ${after.owner}.`,
138
+ ...(gift ? [] : [`Next: agt manifest init ${label} then agt manifest publish ${label} ${label}.manifest.json`]),
139
+ ]);
140
+ });
141
+ }
142
+ // ── manifest init / publish ───────────────────────────────────────────────────────────────────────────────────────
143
+ async function manifestInit(c) {
144
+ const interactive = isInteractive() && !c.args.bools.has("template") && !c.json;
145
+ if (interactive) {
146
+ const shell = c.args.flags.shell ?? detectShell();
147
+ if (shell !== "bash" && shell !== "powershell")
148
+ throw new UsageError("--shell must be bash or powershell");
149
+ const v = c.args.positionals[2];
150
+ await interactiveInit({ label: v, out: c.args.flags.out, network: c.net, probe: !c.args.bools.has("no-probe"), shell, log: c.log });
151
+ return;
152
+ }
153
+ const label = labelArg(c, 2);
154
+ const file = c.args.flags.out ?? `${label}.manifest.json`;
155
+ if (existsSync(file))
156
+ throw new Error(`${file} already exists`);
157
+ writeFileSync(file, JSON.stringify(MANIFEST_TEMPLATE, null, 2) + "\n");
158
+ c.out({ file }, [`Wrote ${file}. Fill in the description and endpoints, then run:`, ` agt manifest publish ${label} ${file}`]);
159
+ }
160
+ async function manifestPublish(c) {
161
+ const label = labelArg(c, 2);
162
+ const file = c.args.positionals[3];
163
+ if (!file)
164
+ throw new UsageError("missing <file.json> (create one with `agt manifest init`)");
165
+ const host = (c.args.flags.host ?? "agts.dev");
166
+ if (host !== "agts.dev" && host !== "ipfs")
167
+ throw new UsageError("--host must be agts.dev or ipfs");
168
+ let input;
169
+ try {
170
+ input = JSON.parse(readFileSync(file, "utf8"));
171
+ }
172
+ catch (e) {
173
+ throw new Error(`could not read ${file}: ${e.message}`);
174
+ }
175
+ const problems = lintInput(input);
176
+ if (problems.length)
177
+ throw new Error(`fix ${file} first:\n - ${problems.join("\n - ")}`);
178
+ const site = requireSite(c.net);
179
+ const pc = publicClient(c.net);
180
+ if (!c.args.bools.has("skip-live-check")) {
181
+ const checks = await Promise.all((input.endpoints ?? []).map((e) => checkEndpoint(e.protocol, e.url)));
182
+ const dead = checks.filter((x) => !x.live);
183
+ if (dead.length)
184
+ throw new Error(`these endpoints are not reachable yet, so the manifest would point at nothing:\n - ${dead.map((d) => `${d.protocol} ${d.url} (${d.error})`).join("\n - ")}\nDeploy them first, or pass --skip-live-check to publish anyway.`);
185
+ }
186
+ await withSigner(c, async (s) => {
187
+ const state = await nameState(pc, c.net, label);
188
+ const resolver = assertOwner(state, s.address);
189
+ const signed = await signManifestWith(s, buildManifest(label, s.address, input));
190
+ const wallet = (signed.payments ?? []).find((p) => p.address)?.address;
191
+ const plan = { endpoints: (signed.endpoints ?? []).map((e) => ({ protocol: e.protocol, url: e.url })), addr: s.address, wallet };
192
+ const current = await pc.readContract({ address: resolver, abi: resolverAbi, functionName: "agentManifest", args: [nodeOf(label)] }).catch(() => "");
193
+ if (c.dry) {
194
+ const predicted = host === "agts.dev" ? `https://agts.dev/${label}.json` : "(the ipfs:// URI returned by hosting)";
195
+ const calls = recordCalls(label, { ...plan, manifest: host === "agts.dev" ? predicted : undefined }, current);
196
+ c.out({ dryRun: true, manifest: signed, host, records: c.args.bools.has("no-records") ? null : dryTx(multicallTx(resolver, calls, "set records"), s.address) }, ["Dry run: signed, nothing hosted or sent.", JSON.stringify(signed, null, 2), ...(c.args.bools.has("no-records") ? [] : describePlan(label, { ...plan, manifest: predicted }, current))]);
197
+ return;
198
+ }
199
+ const pin = await hostManifest(site, signed, host);
200
+ c.log(`Hosted (${pin.provider}): ${pin.uri}`);
201
+ if (!c.args.bools.has("no-records")) {
202
+ const full = { ...plan, manifest: pin.uri };
203
+ describePlan(label, full, current).forEach((l) => c.log(l));
204
+ await sendAndWait(pc, c.net, s, multicallTx(resolver, recordCalls(label, full, current), `Set ${label}.agt records (manifest, endpoints, addr${wallet ? ", wallet" : ""})`), c.log);
205
+ }
206
+ const report = await verifyName(c.net, label);
207
+ c.out({ name: `${label}.agt`, uri: pin.uri, verify: report }, verifyLines(report));
208
+ if (!report.ok)
209
+ process.exitCode = 1;
210
+ });
211
+ }
212
+ // ── records set ───────────────────────────────────────────────────────────────────────────────────────────────────
213
+ async function recordsSet(c) {
214
+ const label = labelArg(c, 2);
215
+ const f = c.args.flags;
216
+ const endpoints = ["mcp", "a2a", "http", "ws"].flatMap((p) => { const u = parseHttpsFlag(f[p], p); return u ? [{ protocol: p, url: u }] : []; });
217
+ const plan = { endpoints, wallet: parseAddressFlag(f.wallet, "wallet"), addr: parseAddressFlag(f.addr, "addr"), manifest: f.manifest };
218
+ if (plan.manifest && !/^(https|ipfs):\/\//.test(plan.manifest))
219
+ throw new UsageError("--manifest must be an https:// or ipfs:// URI");
220
+ if (!endpoints.length && !plan.wallet && !plan.addr && !plan.manifest)
221
+ throw new UsageError("nothing to set: pass --mcp, --a2a, --http, --ws, --wallet, --addr or --manifest");
222
+ const pc = publicClient(c.net);
223
+ await withSigner(c, async (s) => {
224
+ const resolver = assertOwner(await nameState(pc, c.net, label), s.address);
225
+ const current = plan.manifest ? await pc.readContract({ address: resolver, abi: resolverAbi, functionName: "agentManifest", args: [nodeOf(label)] }).catch(() => "") : "";
226
+ const calls = recordCalls(label, plan, current);
227
+ const tx = multicallTx(resolver, calls, `Set ${calls.length} record${calls.length === 1 ? "" : "s"} on ${label}.agt`);
228
+ const lines = describePlan(label, plan, current);
229
+ if (c.dry) {
230
+ c.out({ dryRun: true, transaction: dryTx(tx, s.address) }, ["Dry run: nothing sent.", ...lines]);
231
+ return;
232
+ }
233
+ if (!calls.length) {
234
+ c.out({ name: `${label}.agt`, changed: 0 }, ["Nothing to change."]);
235
+ return;
236
+ }
237
+ lines.forEach((l) => c.log(l));
238
+ const hash = await sendAndWait(pc, c.net, s, tx, c.log);
239
+ c.out({ name: `${label}.agt`, transaction: hash }, [`✓ ${calls.length} record${calls.length === 1 ? "" : "s"} set on ${label}.agt. Check it with: agt verify ${label}`]);
240
+ });
241
+ }
242
+ // ── verify ────────────────────────────────────────────────────────────────────────────────────────────────────────
243
+ function verifyLines(r) {
244
+ return [
245
+ `${r.ok ? "✓" : "✗"} ${r.name}: manifest ${r.manifestStatus}${r.owner ? `, owner ${r.owner}` : ""}${r.signer ? `, signed by ${r.signer}` : ""}`,
246
+ ...(r.manifestUri ? [` manifest ${r.manifestUri}`] : []),
247
+ ...r.endpoints.map((e) => ` ${e.live ? "live" : "DOWN"} ${e.protocol} ${e.url}${e.live ? ` (${e.via} ${e.status})` : ` (${e.error})`}`),
248
+ ...r.problems.map((p) => ` problem: ${p}`),
249
+ ];
250
+ }
251
+ async function verify(c) {
252
+ const label = labelArg(c, 1);
253
+ const r = await verifyName(c.net, label, { skipEndpoints: c.args.bools.has("no-endpoints") });
254
+ c.out(r, verifyLines(r));
255
+ if (!r.ok)
256
+ process.exitCode = 1;
257
+ }
258
+ // ── keyfile ───────────────────────────────────────────────────────────────────────────────────────────────────────
259
+ async function keyfile(c) {
260
+ const sub = c.args.positionals[1];
261
+ const file = c.args.positionals[2];
262
+ if (!file)
263
+ throw new UsageError(`missing <file>`);
264
+ if (sub === "address") {
265
+ const e = readKeyfile(file);
266
+ c.out({ file, address: e.address }, [e.address]);
267
+ return;
268
+ }
269
+ if (sub !== "create")
270
+ throw new UsageError("keyfile commands: create, address");
271
+ if (existsSync(file))
272
+ throw new Error(`${file} already exists`);
273
+ const pk = parsePrivateKey(await promptHidden("Private key (0x…, hidden): "), "the key you entered");
274
+ const pass = process.env.AGT_KEYFILE_PASSPHRASE ?? await promptHidden("New passphrase (12+ characters, hidden): ");
275
+ if (!process.env.AGT_KEYFILE_PASSPHRASE && (await promptHidden("Repeat the passphrase: ")) !== pass)
276
+ throw new Error("the passphrases do not match");
277
+ const e = encryptKey(pk, pass);
278
+ if (decryptKey(e, pass) !== pk.toLowerCase())
279
+ throw new Error("round-trip check failed; nothing written");
280
+ writeKeyfile(file, e);
281
+ c.out({ file, address: e.address }, [`Wrote ${file} for ${e.address}. Use it with --keyfile ${file}.`]);
282
+ }
283
+ // ── main ──────────────────────────────────────────────────────────────────────────────────────────────────────────
284
+ async function main() {
285
+ const args = parseArgs(process.argv.slice(2));
286
+ if (args.bools.has("version")) {
287
+ console.log(VERSION);
288
+ return;
289
+ }
290
+ const [cmd, sub] = args.positionals;
291
+ if (!cmd || args.bools.has("help") || cmd === "help") {
292
+ console.log(help());
293
+ return;
294
+ }
295
+ const json = args.bools.has("json");
296
+ const log = (s) => (json ? console.error(s) : console.log(s));
297
+ const out = (v, human) => { if (json)
298
+ console.log(JSON.stringify(v, (_, x) => (typeof x === "bigint" ? x.toString() : x), 2));
299
+ else
300
+ human.forEach((l) => console.log(l)); };
301
+ const c = { args, net: networkFrom(args.flags), json, dry: args.bools.has("dry-run"), log, out };
302
+ if (cmd === "available")
303
+ return available(c);
304
+ if (cmd === "register")
305
+ return register(c);
306
+ if (cmd === "verify")
307
+ return verify(c);
308
+ if (cmd === "keyfile")
309
+ return keyfile(c);
310
+ if (cmd === "manifest" && sub === "init")
311
+ return manifestInit(c);
312
+ if (cmd === "manifest" && sub === "publish")
313
+ return manifestPublish(c);
314
+ if (cmd === "records" && sub === "set")
315
+ return recordsSet(c);
316
+ throw new UsageError(`unknown command: ${args.positionals.slice(0, 2).join(" ")} (run agt --help)`);
317
+ }
318
+ main().catch((e) => {
319
+ if (e instanceof CancelledError) {
320
+ console.error("Stopped. Your answers so far are saved; run the same command to resume.");
321
+ process.exit(130);
322
+ }
323
+ const usage = e instanceof UsageError;
324
+ console.error(`agt: ${e instanceof Error ? e.message : String(e)}${usage ? "\nRun agt --help for usage." : ""}`);
325
+ process.exit(usage ? 2 : 1);
326
+ });
@@ -0,0 +1,24 @@
1
+ import type { Address, Chain } from "viem";
2
+ export type NetworkName = "polygon" | "amoy";
3
+ export interface Network {
4
+ name: NetworkName;
5
+ chainId: number;
6
+ chain: Chain;
7
+ rpcUrls: string[];
8
+ registry: Address;
9
+ resolver: Address;
10
+ controller: Address;
11
+ usdc: Address;
12
+ /** Site that issues quotes and hosts manifests. Amoy has no public site: pass --site (e.g. a local `next dev`). */
13
+ site: string | null;
14
+ explorer: string;
15
+ }
16
+ export interface NetworkOverrides {
17
+ network?: string;
18
+ rpc?: string;
19
+ site?: string;
20
+ }
21
+ /** Flags win over the environment (AGT_NETWORK, AGT_RPC_URL, AGT_SITE), which wins over the defaults. */
22
+ export declare function networkFrom(o: NetworkOverrides, env?: NodeJS.ProcessEnv): Network;
23
+ export declare function requireSite(n: Network): string;
24
+ export declare const txUrl: (n: Network, hash: string) => string;
package/dist/config.js ADDED
@@ -0,0 +1,48 @@
1
+ /**
2
+ * Networks the CLI writes to. Registry and resolver come from @agtnames/resolver's CHAINS; the controller and USDC
3
+ * addresses are the deployed proxies (source of truth: the operator's `_internal/deployments/<network>.json`).
4
+ * `register` also takes the controller from the signed quote and refuses if the two disagree.
5
+ */
6
+ import { CHAINS } from "@agtnames/resolver";
7
+ import { polygon, polygonAmoy } from "viem/chains";
8
+ const NETWORKS = {
9
+ polygon: {
10
+ name: "polygon",
11
+ chainId: 137,
12
+ chain: polygon,
13
+ registry: CHAINS.polygon.registry,
14
+ resolver: CHAINS.polygon.resolver,
15
+ controller: "0x875682c162eA6450b904d1A0AEe085cfb29b5C5C",
16
+ usdc: "0x3c499c542cEF5E3811e1192ce70d8cC03d5c3359",
17
+ site: "https://agtnames.com",
18
+ explorer: "https://polygonscan.com",
19
+ },
20
+ amoy: {
21
+ name: "amoy",
22
+ chainId: 80002,
23
+ chain: polygonAmoy,
24
+ registry: CHAINS.amoy.registry,
25
+ resolver: CHAINS.amoy.resolver,
26
+ controller: "0xbDBC4e0E934Cf39F6B6585c964eC8436dFD98791",
27
+ usdc: "0xC8a4A34f1Ac295Fce267c4aCF0C5587e1061c9E4",
28
+ site: null,
29
+ explorer: "https://amoy.polygonscan.com",
30
+ },
31
+ };
32
+ /** Flags win over the environment (AGT_NETWORK, AGT_RPC_URL, AGT_SITE), which wins over the defaults. */
33
+ export function networkFrom(o, env = process.env) {
34
+ const name = (o.network ?? env.AGT_NETWORK ?? "polygon").toLowerCase();
35
+ if (name !== "polygon" && name !== "amoy")
36
+ throw new Error(`unknown network "${name}" (use polygon or amoy)`);
37
+ const base = NETWORKS[name];
38
+ const rpc = o.rpc ?? env.AGT_RPC_URL;
39
+ const defaults = CHAINS[name].rpcUrls ?? [CHAINS[name].rpcUrl];
40
+ const site = (o.site ?? env.AGT_SITE ?? base.site)?.replace(/\/$/, "") ?? null;
41
+ return { ...base, rpcUrls: rpc ? [rpc] : [...defaults], site };
42
+ }
43
+ export function requireSite(n) {
44
+ if (!n.site)
45
+ throw new Error(`${n.name} has no public site: pass --site <url> (or set AGT_SITE) for quotes and manifest hosting`);
46
+ return n.site;
47
+ }
48
+ export const txUrl = (n, hash) => `${n.explorer}/tx/${hash}`;
@@ -0,0 +1,42 @@
1
+ import { type Frame, type Key, type Nav, type Widget } from "./tui.js";
2
+ export interface EndpointRow {
3
+ protocol: string;
4
+ label: string;
5
+ /** Format shown in the empty input, e.g. "https://… (your MCP endpoint URL)". */
6
+ format: string;
7
+ url: string;
8
+ on: boolean;
9
+ prefill?: string;
10
+ prefillSource?: string;
11
+ }
12
+ export interface EndpointsOptions {
13
+ label: string;
14
+ rows: EndpointRow[];
15
+ validate: (protocol: string, url: string) => string | null;
16
+ /** Live status for a saved row ("✓ live · 3 tools"). */
17
+ status: (protocol: string, url: string) => string | null;
18
+ /** Called when a row's URL is saved, so it can be read in the background. */
19
+ onSave: (protocol: string, url: string) => void;
20
+ /** Background results arrive: redraw. Returns an unsubscribe. */
21
+ onChange: (cb: () => void) => () => void;
22
+ }
23
+ export declare class EndpointsInput implements Widget<Record<string, string> | Nav> {
24
+ private o;
25
+ private i;
26
+ private editor;
27
+ private error;
28
+ private off?;
29
+ get label(): string;
30
+ constructor(o: EndpointsOptions);
31
+ attach(redraw: () => void): void;
32
+ detach(): void;
33
+ private row;
34
+ /** A URL next to one already entered, as a prefill for an empty row: https://host/mcp → https://host/a2a. */
35
+ private derived;
36
+ private openEditor;
37
+ render(): Frame;
38
+ onKey(str: string | undefined, key: Key): {
39
+ done: Record<string, string> | Nav;
40
+ } | void;
41
+ summary(v: Record<string, string> | Nav): string;
42
+ }
@@ -0,0 +1,142 @@
1
+ /**
2
+ * Endpoints: one list, one URL input per protocol row.
3
+ *
4
+ * list ↑/↓ move between rows · Tab focuses the row's URL input · Space toggles a row with a URL · Enter continues
5
+ * to the next step with every ticked row · j back · ; skip
6
+ * input the field shows the format it needs (https://…, wss://…) until you type; a known URL is a prefill (Tab
7
+ * fills). Enter saves the row, ticks it, starts reading the endpoint in the background, and moves focus to
8
+ * the next row. Enter on an empty input unticks the row. Esc on an empty input returns to the list.
9
+ *
10
+ * So the fast path is Tab, type, Enter for each row, then Enter again to move on.
11
+ */
12
+ import { TextInput } from "./widgets.js";
13
+ import { BACK, SKIP, style } from "./tui.js";
14
+ const LABEL_W = 10;
15
+ export class EndpointsInput {
16
+ o;
17
+ i = 0;
18
+ editor = null;
19
+ error = null;
20
+ off;
21
+ get label() { return this.o.label; }
22
+ constructor(o) {
23
+ this.o = o;
24
+ }
25
+ attach(redraw) { this.off = this.o.onChange(redraw); }
26
+ detach() { this.off?.(); this.editor?.detach(); }
27
+ row() { return this.o.rows[this.i]; }
28
+ /** A URL next to one already entered, as a prefill for an empty row: https://host/mcp → https://host/a2a. */
29
+ derived(r) {
30
+ const other = this.o.rows.find((x) => x !== r && x.on && x.url);
31
+ if (!other)
32
+ return undefined;
33
+ try {
34
+ const origin = new URL(other.url).origin;
35
+ return { url: r.protocol === "ws" ? `${origin.replace(/^https:/, "wss:")}/ws` : `${origin}/${r.protocol}`, source: `next to your ${other.label} endpoint` };
36
+ }
37
+ catch {
38
+ return undefined;
39
+ }
40
+ }
41
+ openEditor() {
42
+ const r = this.row();
43
+ const d = r.url ? undefined : r.prefill ? { url: r.prefill, source: r.prefillSource ?? "" } : this.derived(r);
44
+ this.editor = new TextInput({
45
+ label: r.label, instruction: r.format, initial: r.url || undefined,
46
+ prefill: d?.url, prefillSource: d?.source || undefined,
47
+ optional: true, nav: false,
48
+ validate: (v) => this.o.validate(r.protocol, v),
49
+ });
50
+ }
51
+ render() {
52
+ const lines = [`${style.cyan("?")} ${style.bold(this.o.label)} Add the URL for each protocol your agent serves`];
53
+ let cursor = { row: 1, col: 2 };
54
+ this.o.rows.forEach((r, idx) => {
55
+ const focused = idx === this.i;
56
+ const box = r.on ? style.green("[x]") : "[ ]";
57
+ const name = (focused ? style.bold(r.label.padEnd(LABEL_W)) : r.label.padEnd(LABEL_W));
58
+ const ptr = focused ? style.cyan("❯") : " ";
59
+ if (focused && this.editor) {
60
+ // The row's input, inline: the editor's field line and its hint, indented under the row.
61
+ const f = this.editor.render();
62
+ const field = f.lines[1].replace(/^› /, "");
63
+ const prefix = `${ptr} ${box} ${name} `;
64
+ lines.push(prefix + field);
65
+ cursor = { row: lines.length - 1, col: prefix.replace(/\x1b\[[0-9;]*m/g, "").length + (f.cursor.col - 2) };
66
+ for (const extra of f.lines.slice(2))
67
+ lines.push(" " + extra.replace(/^ {2}/, ""));
68
+ return;
69
+ }
70
+ const status = r.on && r.url ? this.o.status(r.protocol, r.url) : null;
71
+ lines.push(`${ptr} ${box} ${name} ${r.url ? r.url : style.dim(r.format)}${status ? " " + status : ""}`);
72
+ if (focused)
73
+ cursor = { row: lines.length - 1, col: 2 };
74
+ });
75
+ if (this.error)
76
+ lines.push(" " + style.red(this.error));
77
+ lines.push(style.dim(this.editor
78
+ ? " Enter saves and moves to the next row · Tab fills the suggestion · Esc back to the list"
79
+ : " ↑/↓ move · Tab edit the URL · Space tick/untick · Enter continue · j back · ; skip"));
80
+ return { lines, cursor };
81
+ }
82
+ onKey(str, key) {
83
+ this.error = null;
84
+ const r = this.row();
85
+ if (this.editor) {
86
+ if (key.name === "escape" && !this.editor.value) {
87
+ this.editor.detach();
88
+ this.editor = null;
89
+ return;
90
+ }
91
+ const res = this.editor.onKey(str, key);
92
+ if (res && typeof res.done === "string") {
93
+ const v = res.done;
94
+ r.url = v;
95
+ r.on = !!v;
96
+ if (v)
97
+ this.o.onSave(r.protocol, v);
98
+ this.editor.detach();
99
+ this.editor = null;
100
+ this.i = Math.min(this.o.rows.length - 1, this.i + 1);
101
+ }
102
+ return;
103
+ }
104
+ const n = this.o.rows.length;
105
+ if (str === "j")
106
+ return { done: BACK };
107
+ if (str === ";")
108
+ return { done: SKIP };
109
+ if (key.name === "up") {
110
+ this.i = (this.i - 1 + n) % n;
111
+ return;
112
+ }
113
+ if (key.name === "down") {
114
+ this.i = (this.i + 1) % n;
115
+ return;
116
+ }
117
+ if (key.name === "tab" || key.name === "right") {
118
+ this.openEditor();
119
+ return;
120
+ }
121
+ if (key.name === "space" || str === " ") {
122
+ if (r.url)
123
+ r.on = !r.on;
124
+ else
125
+ this.openEditor();
126
+ return;
127
+ }
128
+ if (key.name === "return" || key.name === "enter") {
129
+ const chosen = this.o.rows.filter((x) => x.on && x.url);
130
+ if (!chosen.length) {
131
+ this.error = "Add at least one URL (Tab to edit a row), or ; to skip";
132
+ return;
133
+ }
134
+ return { done: Object.fromEntries(chosen.map((x) => [x.protocol, x.url])) };
135
+ }
136
+ }
137
+ summary(v) {
138
+ if (typeof v !== "object")
139
+ return "";
140
+ return `${style.green("✔")} ${style.bold(this.o.label)} ${style.cyan(Object.entries(v).map(([p, u]) => `${p} ${u}`).join(" · "))}`;
141
+ }
142
+ }
package/dist/init.d.ts ADDED
@@ -0,0 +1,47 @@
1
+ import type { Network } from "./config.js";
2
+ import type { ManifestInput } from "./manifest.js";
3
+ import { CancelledError } from "./tui.js";
4
+ export interface InitAnswers {
5
+ protocols?: string[];
6
+ endpoints?: Record<string, string>;
7
+ description?: string;
8
+ website?: string;
9
+ capabilities?: string[];
10
+ pricing?: string;
11
+ paid?: {
12
+ currency: string;
13
+ amount: string;
14
+ unit: string;
15
+ };
16
+ freeTier?: string;
17
+ payment?: {
18
+ rail: string;
19
+ address: string;
20
+ } | null;
21
+ }
22
+ interface Draft {
23
+ label: string;
24
+ savedAt: string;
25
+ answers: InitAnswers;
26
+ }
27
+ export type ShellKind = "powershell" | "bash";
28
+ export declare function detectShell(env?: NodeJS.ProcessEnv, platform?: NodeJS.Platform): ShellKind;
29
+ /** Quote one argument for the shell, only when it needs it. */
30
+ export declare function shellQuote(arg: string, shell: ShellKind): string;
31
+ export declare function loadDraft(label: string): Draft | null;
32
+ export declare function saveDraft(label: string, answers: InitAnswers): void;
33
+ export declare function clearDraft(label: string): void;
34
+ export declare function answersToInput(a: InitAnswers): ManifestInput;
35
+ export interface InitOptions {
36
+ label?: string;
37
+ out?: string;
38
+ network: Network;
39
+ probe: boolean;
40
+ shell: ShellKind;
41
+ log: (s: string) => void;
42
+ }
43
+ export declare function interactiveInit(o: InitOptions): Promise<{
44
+ file: string;
45
+ label: string;
46
+ } | null>;
47
+ export { CancelledError };